Hexagonic
by NeonFlare131967 lines158.5 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Hexagonic</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
:root {
--bg-body: #0a0a1a; --bg-container: #141428; --bg-canvas: #0f1a2e;
--bg-menu: #1a1a3e; --bg-menu-card: rgba(255,255,255,0.03);
--text-primary: #eee; --text-secondary: #aaa; --text-muted: #666;
--border-color: rgba(255,255,255,0.06); --border-light: rgba(255,255,255,0.12);
--shadow-color: rgba(0,0,0,0.9); --stat-bg: rgba(255,255,255,0.05);
--btn-bg: rgba(255,255,255,0.06); --btn-border: rgba(255,255,255,0.08);
--color-player: #ff6b6b; --color-ai: #4ecca3; --color-neutral: #3a5570;
--color-water: #0a1525; --color-selected: #ffd700;
--glow-player: rgba(255,107,107,0.3); --glow-ai: rgba(78,204,163,0.3);
--glow-selected: rgba(255,215,0,0.5);
--menu-overlay: rgba(0,0,0,0.85); --accent: #ff6b6b;
}
body.light-theme { --bg-body: #eef2f8; --bg-container: #f5f8fc; --bg-canvas: #e8eef7; --bg-menu: #ffffff; --bg-menu-card: rgba(0,0,0,0.02); --text-primary: #1a2238; --text-secondary: #5a6478; --text-muted: #98a2b8; --border-color: rgba(26,34,56,0.08); --border-light: rgba(26,34,56,0.15); --shadow-color: rgba(26,34,56,0.12); --stat-bg: rgba(255,255,255,0.75); --btn-bg: rgba(255,255,255,0.9); --btn-border: rgba(26,34,56,0.08); --color-neutral: #b8c4d4; --color-water: #dce4f0; --glow-player: rgba(255,107,107,0.25); --glow-ai: rgba(78,204,163,0.25); --glow-selected: rgba(255,215,0,0.4); --menu-overlay: rgba(238,242,248,0.92); }
body.duck-theme { --bg-body: #fff9e6; --bg-container: #ffeaa7; --bg-canvas: #d4f1f4; --bg-menu: #fffdf5; --bg-menu-card: rgba(255,255,255,0.85); --text-primary: #5a4a1a; --text-secondary: #8a6a2a; --text-muted: #b09a5a; --border-color: rgba(180,140,40,0.2); --border-light: rgba(180,140,40,0.35); --shadow-color: rgba(180,140,40,0.3); --stat-bg: rgba(255,255,255,0.75); --btn-bg: rgba(255,255,255,0.8); --btn-border: rgba(180,140,40,0.3); --color-player: #ffd93d; --color-ai: #ff8c42; --color-neutral: #f5e6c8; --color-water: #7ec8d3; --color-selected: #ff6b35; --glow-player: rgba(255,217,61,0.6); --glow-ai: rgba(255,140,66,0.5); --glow-selected: rgba(255,107,53,0.5); --menu-overlay: rgba(255,249,230,0.94); --accent: #ff9f1c; }
body.retro-theme { --bg-body: #3a3a3a; --bg-container: #c0c0c0; --bg-canvas: #c0c0c0; --bg-menu: #d4d0c8; --bg-menu-card: #e0e0e0; --text-primary: #000000; --text-secondary: #303030; --text-muted: #606060; --border-color: rgba(0,0,0,0.4); --border-light: rgba(0,0,0,0.25); --shadow-color: rgba(0,0,0,0.5); --stat-bg: #e8e8e8; --btn-bg: #d4d0c8; --btn-border: #808080; --color-player: #d02020; --color-ai: #107010; --color-neutral: #b0b0b0; --color-water: #a0a0a0; --color-selected: #0000c0; --glow-player: rgba(208,32,32,0.35); --glow-ai: rgba(16,112,16,0.35); --glow-selected: rgba(0,0,192,0.4); --menu-overlay: rgba(192,192,192,0.94); --accent: #0000c0; }
body.nature-theme { --bg-body: #2d4a2b; --bg-container: #3d5c35; --bg-canvas: #4a7c3f; --bg-menu: #f4ecd8; --bg-menu-card: rgba(255,255,255,0.55); --text-primary: #2a3d20; --text-secondary: #4a5c3a; --text-muted: #7a8a6a; --border-color: rgba(60,90,45,0.25); --border-light: rgba(60,90,45,0.4); --shadow-color: rgba(20,40,15,0.55); --stat-bg: rgba(244,236,216,0.88); --btn-bg: rgba(244,236,216,0.9); --btn-border: rgba(60,90,45,0.3); --color-player: #d97a1a; --color-ai: #2e8b57; --color-neutral: #8b7355; --color-water: #3d6b8a; --color-selected: #ffd700; --glow-player: rgba(217,122,26,0.5); --glow-ai: rgba(46,139,87,0.5); --glow-selected: rgba(255,215,0,0.6); --menu-overlay: rgba(45,74,43,0.94); --accent: #2e8b57; }
body.sunset-theme { --bg-body: #2a1540; --bg-container: #3d2050; --bg-canvas: #6b3a5a; --bg-menu: #2a1540; --bg-menu-card: rgba(255,255,255,0.06); --text-primary: #ffe8d6; --text-secondary: #e0b8a0; --text-muted: #b88a78; --border-color: rgba(255,180,120,0.15); --border-light: rgba(255,180,120,0.3); --shadow-color: rgba(20,8,30,0.85); --stat-bg: rgba(255,200,150,0.14); --btn-bg: rgba(255,200,150,0.12); --btn-border: rgba(255,200,150,0.25); --color-player: #ff8c42; --color-ai: #b84edb; --color-neutral: #6b5a70; --color-water: #2a1540; --color-selected: #ffd700; --glow-player: rgba(255,140,66,0.65); --glow-ai: rgba(184,78,219,0.65); --glow-selected: rgba(255,215,0,0.65); --menu-overlay: rgba(42,21,64,0.94); --accent: #ff8c42; }
body.puzzletheme-theme { --bg-body: #1a0f2e; --bg-container: #2a1a4a; --bg-canvas: #35205c; --bg-menu: #2a1a4a; --bg-menu-card: rgba(255,255,255,0.05); --text-primary: #f0e8ff; --text-secondary: #b8a8d8; --text-muted: #8878a8; --border-color: rgba(200,150,255,0.12); --border-light: rgba(200,150,255,0.28); --shadow-color: rgba(10,0,30,0.88); --stat-bg: rgba(200,150,255,0.12); --btn-bg: rgba(200,150,255,0.1); --btn-border: rgba(200,150,255,0.22); --color-player: #ff7043; --color-ai: #42a5f5; --color-neutral: #8d9aab; --color-water: #1a0f2e; --color-selected: #ffd54f; --glow-player: rgba(255,112,67,0.65); --glow-ai: rgba(66,165,245,0.65); --glow-selected: rgba(255,213,79,0.75); --menu-overlay: rgba(26,15,46,0.94); --accent: #ff7043; }
body.coins-theme { --bg-body: #1a1208; --bg-container: #2a1e10; --bg-canvas: #3a2a14; --bg-menu: #2a1e10; --bg-menu-card: rgba(255,200,80,0.08); --text-primary: #ffe8a8; --text-secondary: #c8a058; --text-muted: #907038; --border-color: rgba(255,200,80,0.18); --border-light: rgba(255,200,80,0.35); --shadow-color: rgba(0,0,0,0.9); --stat-bg: rgba(255,200,80,0.12); --btn-bg: rgba(255,200,80,0.12); --btn-border: rgba(255,200,80,0.28); --color-player: #ffd93d; --color-ai: #b87333; --color-neutral: #8a6a30; --color-water: #1a1208; --color-selected: #fff4c0; --glow-player: rgba(255,217,61,0.75); --glow-ai: rgba(184,115,51,0.75); --glow-selected: rgba(255,244,192,0.75); --menu-overlay: rgba(26,18,8,0.94); --accent: #ffd93d; }
body.sakura-theme { --bg-body: #fef0f5; --bg-container: #fce4ec; --bg-canvas: #fdf2f7; --bg-menu: #fffafc; --bg-menu-card: rgba(255,183,197,0.12); --text-primary: #6b2a3e; --text-secondary: #a05070; --text-muted: #c890a8; --border-color: rgba(255,143,171,0.25); --border-light: rgba(255,143,171,0.45); --shadow-color: rgba(200,100,140,0.2); --stat-bg: rgba(255,255,255,0.75); --btn-bg: rgba(255,255,255,0.85); --btn-border: rgba(255,143,171,0.4); --color-player: #ff5a8a; --color-ai: #b06aff; --color-neutral: #e8c8d4; --color-water: #fce4ec; --color-selected: #ffb347; --glow-player: rgba(255,90,138,0.5); --glow-ai: rgba(176,106,255,0.5); --glow-selected: rgba(255,179,71,0.6); --menu-overlay: rgba(254,240,245,0.94); --accent: #ff5a8a; }
body.space-theme { --bg-body: #05030f; --bg-container: #0e0a24; --bg-canvas: #0a0620; --bg-menu: #120c2e; --bg-menu-card: rgba(150,120,255,0.08); --text-primary: #e8e0ff; --text-secondary: #a89ad8; --text-muted: #6a5a98; --border-color: rgba(150,120,255,0.18); --border-light: rgba(150,120,255,0.35); --shadow-color: rgba(0,0,10,0.95); --stat-bg: rgba(150,120,255,0.1); --btn-bg: rgba(150,120,255,0.12); --btn-border: rgba(150,120,255,0.28); --color-player: #ff6b9d; --color-ai: #6bcaff; --color-neutral: #3a3060; --color-water: #05030f; --color-selected: #ffe066; --glow-player: rgba(255,107,157,0.7); --glow-ai: rgba(107,202,255,0.7); --glow-selected: rgba(255,224,102,0.75); --menu-overlay: rgba(5,3,15,0.94); --accent: #b06bff; }
body { background: var(--bg-body); display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: 'Segoe UI', Arial, sans-serif; overflow: hidden; transition: background 0.4s ease; position: relative; }
body.retro-theme h1, body.retro-theme h2, body.retro-theme .stat span, body.retro-theme .menu-main-btn { font-family: 'Courier New', monospace; font-weight: bold; letter-spacing: 1px; }
#duckFallingCanvas { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 0; display: none; }
body.duck-theme #duckFallingCanvas { display: block; }
.game-container { background: var(--bg-container); padding: 15px; border-radius: 20px; box-shadow: 0 10px 40px var(--shadow-color), 0 0 0 1px var(--border-color); display: flex; flex-direction: column; align-items: center; max-width: 98vw; position: relative; z-index: 1; }
body.retro-theme .game-container { border-radius: 0; border: 3px outset #ffffff; box-shadow: none; }
h1 { color: var(--color-player); font-size: 22px; margin-bottom: 8px; letter-spacing: 4px; font-weight: 800; position: relative; display: inline-block; text-shadow: 0 0 20px var(--glow-player), 0 5px 8px rgba(0,0,0,0.6); animation: logo-breathe 4s ease-in-out infinite; }
@keyframes logo-breathe { 0%, 100% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.03); filter: brightness(1.12); } }
body.retro-theme h1 { color: #000080; text-shadow: 1px 1px 0 #ffffff, 2px 2px 0 #808080, 3px 3px 0 #000000; font-family: 'Courier New', monospace; animation: none; }
body.light-theme h1 { color: #d04040; }
body.duck-theme h1 { background: linear-gradient(180deg, #ffe680 0%, #ffb800 50%, #ff8c00 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.nature-theme h1 { background: linear-gradient(180deg, #ffe8a0 0%, #7fc060 50%, #2e8b57 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.sunset-theme h1 { background: linear-gradient(180deg, #ffd700 0%, #ff8c42 40%, #ff6b8a 70%, #b84edb 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.puzzletheme-theme h1 { background: linear-gradient(180deg, #ffe0b2 0%, #ff7043 40%, #b83a1a 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.coins-theme h1 { background: linear-gradient(180deg, #fff8d0 0%, #ffd93d 35%, #c8742a 70%, #8a4a10 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.sakura-theme h1 { background: linear-gradient(180deg, #fff0f5 0%, #ffb7c5 40%, #ff5a8a 75%, #a03060 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.space-theme h1 { background: linear-gradient(180deg, #e8e0ff 0%, #b06bff 40%, #6bcaff 75%, #2a1a5c 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.top-bar { display: flex; justify-content: space-between; width: 100%; margin-bottom: 8px; gap: 8px; flex-wrap: wrap; align-items: center; }
.stats { display: flex; gap: 12px; background: var(--stat-bg); padding: 5px 12px; border-radius: 10px; flex-wrap: wrap; border: 1px solid var(--border-color); }
body.retro-theme .stats { border: 2px inset #ffffff; border-radius: 0; }
.stat { color: var(--text-secondary); font-size: 12px; }
.stat span { font-weight: bold; color: var(--text-primary); }
.stat .color-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: middle; margin-right: 3px; border: 2px solid rgba(255,255,255,0.2); box-shadow: 0 0 6px currentColor; }
body.retro-theme .stat .color-dot { border-radius: 0; border: 1px solid #000; box-shadow: none; }
.controls { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.btn { color: var(--text-primary); padding: 5px 16px; border-radius: 30px; font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.25s ease; background: var(--btn-bg); border: 1px solid var(--btn-border); font-family: inherit; }
.btn:hover:not(:disabled) { transform: translateY(-2px) scale(1.02); box-shadow: 0 6px 20px var(--glow-player); }
.btn:active:not(:disabled) { transform: scale(0.96); }
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
body.retro-theme .btn { border-radius: 0; border: 2px outset #ffffff; box-shadow: none; background: #d4d0c8; color: #000; }
body.duck-theme .btn:hover:not(:disabled) { animation: duck-bounce 0.6s ease infinite; }
@keyframes duck-bounce { 0%, 100% { transform: translateY(-2px) scale(1.02) rotate(-1deg); } 50% { transform: translateY(-5px) scale(1.03) rotate(1deg); } }
.btn-green { background: linear-gradient(145deg, var(--color-ai), #3db892); border-color: var(--color-ai); color: #fff; }
body.retro-theme .btn-green { background: #d4d0c8; border: 2px outset #ffffff; color: #006000; }
.btn-gold { background: linear-gradient(145deg, #ffd700, #f0c000); border-color: #ffd700; color: #1a1a2e; }
body.retro-theme .btn-gold { background: #d4d0c8; border: 2px outset #ffffff; color: #806000; }
.btn-outline { background: transparent; border: 1.5px solid var(--border-color); color: var(--text-secondary); }
body.retro-theme .btn-outline { background: #d4d0c8; border: 2px outset #ffffff; color: #000; }
.game-menu-btn { width: 34px; height: 34px; border-radius: 50%; background: var(--btn-bg); border: 1.5px solid var(--border-color); color: var(--text-primary); font-size: 17px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; font-family: inherit; padding: 0; line-height: 1; }
.game-menu-btn:hover { background: var(--border-light); transform: scale(1.08) rotate(45deg); box-shadow: 0 0 20px var(--glow-player); }
body.retro-theme .game-menu-btn { border-radius: 0; border: 2px outset #ffffff; background: #d4d0c8; color: #000; }
.turn-indicator { color: var(--color-player); font-size: 14px; font-weight: bold; padding: 3px 12px; border-radius: 30px; background: var(--stat-bg); border: 1px solid var(--border-color); box-shadow: 0 0 15px var(--glow-player); }
body.retro-theme .turn-indicator { border-radius: 0; border: 2px inset #ffffff; box-shadow: none; }
.message { color: var(--color-selected); font-size: 12px; min-height: 18px; text-align: center; margin-top: 4px; max-width: 100%; font-weight: 600; text-shadow: 0 0 10px var(--glow-selected); }
body.retro-theme .message { color: #000080; text-shadow: none; }
canvas { pointer-events: auto !important; touch-action: none !important; user-select: none; border-radius: 12px; background: var(--bg-canvas); display: block; cursor: pointer; max-width: 100%; height: auto; border: 2px solid var(--border-color); }
body.retro-theme canvas { border-radius: 0; border: 3px inset #ffffff; }
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--menu-overlay); z-index: 10000; justify-content: center; align-items: center; backdrop-filter: blur(16px); padding: 20px; }
.modal-overlay.active { display: flex; }
.modal-box { background: var(--bg-menu); border-radius: 28px; border: 1px solid var(--border-color); box-shadow: 0 40px 100px var(--shadow-color), 0 0 60px var(--glow-player); padding: 28px 30px 24px; max-width: 780px; width: 100%; max-height: 88vh; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
body.retro-theme .modal-box { border-radius: 0; border: 3px outset #ffffff; box-shadow: none; }
.modal-box::-webkit-scrollbar { width: 6px; }
.modal-box::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 10px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.modal-title { font-size: 20px; font-weight: 700; letter-spacing: 3px; color: var(--color-player); text-shadow: 0 0 30px var(--glow-player); }
body.retro-theme .modal-title { text-shadow: 1px 1px 0 #ffffff, -1px -1px 0 #000000; color: #000080; font-family: 'Courier New', monospace; }
.modal-close { background: var(--btn-bg); border: 1px solid var(--border-color); color: var(--text-secondary); width: 34px; height: 34px; border-radius: 50%; font-size: 18px; font-weight: 700; cursor: pointer; font-family: inherit; line-height: 1; }
body.retro-theme .modal-close { border-radius: 0; border: 2px outset #ffffff; }
.modal-close:hover { background: var(--border-light); color: var(--text-primary); }
.modal-section-title { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 5px; opacity: 0.8; margin-top: 4px; }
.modal-divider { width: 100%; height: 1px; background: linear-gradient(90deg, transparent, var(--color-player), transparent); opacity: 0.35; }
.theme-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; width: 100%; }
@media (min-width: 480px) { .theme-selector.cells-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 720px) { .theme-selector.cells-grid { grid-template-columns: repeat(4, 1fr); } }
.theme-btn { padding: 12px 10px; border-radius: 16px; border: 2px solid var(--border-color); background: var(--bg-menu-card); color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.25s ease; display: flex; flex-direction: column; align-items: center; gap: 4px; }
body.retro-theme .theme-btn { border-radius: 0; border: 2px outset #ffffff; }
.theme-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px var(--glow-player); }
.theme-btn.active { border-color: var(--color-player); background: rgba(255,107,107,0.08); color: var(--text-primary); box-shadow: 0 0 20px var(--glow-player); }
.theme-btn .theme-icon { font-size: 22px; display: block; }
.theme-btn .theme-name { font-size: 11px; }
.cell-preview { display: flex; gap: 3px; margin-bottom: 2px; }
.cell-preview span { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.2); }
.color-palette { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; width: 100%; margin-top: 4px; }
.color-swatch { aspect-ratio: 1; border-radius: 14px; border: 3px solid var(--border-color); cursor: pointer; position: relative; transition: all 0.25s ease; overflow: hidden; }
.color-swatch:hover { transform: scale(1.08); }
.color-swatch.active { border-color: var(--color-selected); box-shadow: 0 0 20px var(--glow-selected); transform: scale(1.08); }
.color-swatch .sw-label { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.6); color: #fff; font-size: 9px; text-align: center; padding: 2px 0; font-weight: 600; }
.menu-overlay { display: none; position: fixed; inset: 0; background: var(--menu-overlay); z-index: 9999; justify-content: center; align-items: center; backdrop-filter: blur(16px); overflow: hidden; }
.menu-overlay.active { display: flex; }
.main-menu { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 40px 50px 30px; background: var(--bg-menu); border-radius: 40px; min-width: 460px; max-width: 94vw; border: 1px solid var(--border-color); box-shadow: 0 50px 120px var(--shadow-color), 0 0 80px var(--glow-player); max-height: 92vh; overflow-y: auto; position: relative; z-index: 1; }
body.retro-theme .main-menu { border-radius: 0; border: 3px outset #ffffff; box-shadow: none; }
.main-menu::-webkit-scrollbar { width: 6px; }
.main-menu::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 10px; }
.menu-header { text-align: center; }
.main-menu h2 { color: var(--color-player); font-size: 42px; font-weight: 800; letter-spacing: 9px; margin-bottom: 4px; text-shadow: 0 0 30px var(--glow-player), 0 6px 12px rgba(0,0,0,0.7); }
body.retro-theme .main-menu h2 { color: #000080; text-shadow: 2px 2px 0 #ffffff, 4px 4px 0 #808080, 6px 6px 0 #000000; font-family: 'Courier New', monospace; letter-spacing: 4px; }
body.duck-theme .main-menu h2 { background: linear-gradient(180deg, #fff0a0 0%, #ffd93d 35%, #ffb800 65%, #ff8c00 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.nature-theme .main-menu h2 { background: linear-gradient(180deg, #ffe8a0 0%, #a8d060 30%, #4a8c3f 65%, #1d3a1b 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.sunset-theme .main-menu h2 { background: linear-gradient(180deg, #ffd700 0%, #ff8c42 30%, #ff6b8a 60%, #b84edb 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.puzzletheme-theme .main-menu h2 { background: linear-gradient(180deg, #ffe0b2 0%, #ff8a5c 30%, #ff7043 60%, #b83a1a 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.coins-theme .main-menu h2 { background: linear-gradient(180deg, #fff8d0 0%, #ffd93d 35%, #c8742a 70%, #8a4a10 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.sakura-theme .main-menu h2 { background: linear-gradient(180deg, #fff0f5 0%, #ffb7c5 40%, #ff5a8a 75%, #a03060 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
body.space-theme .main-menu h2 { background: linear-gradient(180deg, #e8e0ff 0%, #b06bff 40%, #6bcaff 75%, #2a1a5c 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.menu-subtitle { color: var(--text-muted); font-size: 13px; letter-spacing: 4px; opacity: 0.75; font-weight: 300; }
.menu-divider { width: 60%; height: 1px; background: linear-gradient(90deg, transparent, var(--color-player), transparent); margin: 2px 0 4px 0; opacity: 0.35; }
.menu-section-title { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 5px; opacity: 0.75; }
.menu-main-buttons { display: flex; flex-direction: column; gap: 14px; width: 100%; padding: 6px 0; }
.menu-main-btn { padding: 16px 30px; border-radius: 20px; border: 1.5px solid var(--border-color); background: var(--bg-menu-card); color: var(--text-primary); font-size: 19px; font-weight: 700; cursor: pointer; transition: all 0.3s ease; text-align: center; letter-spacing: 2px; font-family: inherit; }
body.retro-theme .menu-main-btn { border-radius: 0; border: 3px outset #ffffff; }
.menu-main-btn:hover { transform: translateY(-3px) scale(1.01); box-shadow: 0 8px 30px var(--glow-player); }
.menu-main-btn .btn-icon { font-size: 24px; margin-right: 12px; vertical-align: middle; display: inline-block; }
.menu-main-btn .btn-label { vertical-align: middle; }
.menu-sub { display: none; flex-direction: column; align-items: center; gap: 14px; width: 100%; }
.menu-sub.active { display: flex; }
.menu-sub-back { align-self: flex-start; background: var(--btn-bg); border: 1px solid var(--border-color); border-radius: 30px; color: var(--text-muted); padding: 8px 20px; font-size: 13px; cursor: pointer; font-weight: 600; font-family: inherit; }
.menu-sub-back:hover { background: var(--border-light); color: var(--text-primary); }
.game-options { display: flex; flex-direction: column; align-items: center; gap: 14px; width: 100%; }
.quick-selectors { display: flex; flex-direction: column; gap: 12px; width: 100%; padding: 4px 0; }
.quick-row { display: flex; align-items: center; gap: 10px; width: 100%; }
.quick-label { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 2px; font-weight: 600; min-width: 100px; flex-shrink: 0; }
.quick-options { display: flex; gap: 6px; flex-wrap: wrap; flex: 1; }
.quick-btn { padding: 7px 12px; border-radius: 12px; background: var(--bg-menu-card); border: 1.5px solid var(--border-color); color: var(--text-secondary); font-size: 11px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.2s ease; display: flex; align-items: center; gap: 5px; }
.quick-btn:hover { transform: translateY(-2px); }
.quick-btn.active { border-color: var(--color-player); background: rgba(255,107,107,0.1); color: var(--text-primary); box-shadow: 0 0 15px var(--glow-player); }
.quick-btn .qb-icon { font-size: 14px; }
.quick-btn .qb-label { font-size: 11px; }
.fog-toggle-row { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 14px; background: var(--bg-menu-card); border: 1.5px solid var(--border-color); cursor: pointer; font-family: inherit; transition: all 0.2s ease; width: 100%; }
.fog-toggle-row:hover { transform: translateY(-2px); }
.fog-toggle-row.active { border-color: var(--color-player); background: rgba(120,180,220,0.1); box-shadow: 0 0 20px rgba(120,180,220,0.35); }
.fog-toggle-check { width: 24px; height: 24px; border-radius: 6px; background: var(--bg-menu-card); border: 2px solid var(--border-color); display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; color: transparent; }
.fog-toggle-row.active .fog-toggle-check { background: var(--color-player); border-color: var(--color-player); color: #fff; }
.fog-toggle-info { flex: 1; }
.fog-toggle-label { font-size: 13px; font-weight: 700; color: var(--text-primary); display: block; }
.fog-toggle-desc { font-size: 11px; color: var(--text-muted); display: block; margin-top: 2px; }
.difficulty-list { display: flex; flex-direction: column; gap: 10px; width: 100%; min-width: 280px; }
.diff-card { padding: 12px 18px; border-radius: 16px; background: var(--bg-menu-card); border: 1.5px solid var(--border-color); cursor: pointer; transition: all 0.35s; text-align: left; color: var(--text-primary); font-weight: 600; display: flex; align-items: center; gap: 14px; font-family: inherit; }
.diff-card:hover { transform: translateX(6px) scale(1.01); box-shadow: 0 6px 20px var(--glow-player); }
.diff-card .diff-icon { font-size: 26px; display: inline-block; }
.diff-card .diff-info { flex: 1; }
.diff-card .diff-label { font-size: 16px; font-weight: 700; display: block; }
.diff-card .diff-desc { font-size: 11px; opacity: 0.6; font-weight: 400; display: block; }
.diff-card .diff-badge { font-size: 9px; text-transform: uppercase; letter-spacing: 1px; padding: 2px 10px; border-radius: 20px; background: var(--btn-bg); border: 1px solid var(--border-color); opacity: 0.65; }
.map-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; width: 100%; }
.map-btn { padding: 10px 12px; border-radius: 14px; background: var(--bg-menu-card); border: 1.5px solid var(--border-color); color: var(--text-muted); font-size: 12px; font-weight: 600; cursor: pointer; text-align: center; font-family: inherit; transition: all 0.25s ease; }
.map-btn:hover { transform: translateY(-2px); }
.map-btn.active { border-color: var(--color-player); background: rgba(255,107,107,0.06); color: var(--text-primary); box-shadow: 0 0 25px var(--glow-player); }
.map-btn .map-icon { font-size: 22px; display: block; margin-bottom: 2px; }
.map-btn .map-name { display: block; font-size: 12px; }
.map-btn .map-desc { display: block; font-size: 9px; opacity: 0.6; margin-top: 2px; }
.tutorial-container { width: 100%; max-width: 600px; }
.tutorial-slide { display: none; flex-direction: column; gap: 12px; padding: 18px 22px 22px; background: var(--bg-menu-card); border-radius: 18px; border: 1px solid var(--border-color); }
.tutorial-slide.active { display: flex; }
.tutorial-slide .step-badge { font-size: 10px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-muted); }
.tutorial-slide .step-title { font-size: 18px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 10px; }
.tutorial-slide .step-title .emoji { font-size: 24px; }
.tutorial-slide .step-desc { color: var(--text-secondary); font-size: 14px; line-height: 1.7; }
.tutorial-slide .step-desc strong { color: var(--text-primary); }
.tutorial-slide .step-desc ul { margin: 8px 0 8px 22px; }
.tutorial-slide .step-desc li { margin: 4px 0; }
.tutorial-slide .step-desc .hl-red { color: var(--color-player); font-weight: 600; }
.tutorial-slide .step-desc .hl-green { color: var(--color-ai); font-weight: 600; }
.tutorial-slide .step-desc .hl-gold { color: var(--color-selected); font-weight: 600; }
.tutorial-slide .example-box { background: var(--bg-menu-card); border-radius: 12px; padding: 10px 14px; border-left: 3px solid var(--ex-color, var(--color-player)); }
.tutorial-slide .example-box .ex-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 3px; }
.tutorial-slide .example-box .ex-text { color: var(--text-secondary); font-size: 13px; line-height: 1.5; }
.tutorial-nav { display: flex; justify-content: space-between; align-items: center; gap: 12px; width: 100%; margin-top: 2px; }
.tutorial-nav .dots { display: flex; gap: 6px; flex-wrap: wrap; }
.tutorial-nav .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border-color); transition: all 0.3s; cursor: pointer; }
.tutorial-nav .dot.active { background: var(--color-player); box-shadow: 0 0 12px var(--glow-player); width: 18px; border-radius: 10px; }
.tutorial-nav .tut-btn { background: var(--btn-bg); border: 1px solid var(--border-color); color: var(--text-secondary); padding: 6px 18px; border-radius: 30px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; }
.tutorial-nav .tut-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.tutorial-nav .tut-btn.primary { background: rgba(78,204,163,0.15); color: var(--color-ai); }
.settings-container { width: 100%; max-width: 520px; display: flex; flex-direction: column; gap: 14px; }
.audio-setting-card { background: var(--bg-menu-card); border: 1px solid var(--border-color); border-radius: 18px; padding: 15px 16px; }
.audio-setting-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 11px; }
.audio-setting-title { color: var(--text-primary); font-size: 14px; font-weight: 700; }
.audio-setting-value { color: var(--text-muted); font-size: 12px; }
.audio-row { display: flex; align-items: center; gap: 12px; }
.audio-range { flex: 1; accent-color: var(--color-player); cursor: pointer; }
.audio-toggle { min-width: 112px; padding: 8px 14px; border-radius: 30px; border: 1px solid var(--border-color); background: var(--btn-bg); color: var(--text-secondary); font: inherit; font-size: 11px; font-weight: 700; cursor: pointer; }
.audio-toggle.enabled { color: var(--color-ai); border-color: rgba(78,204,163,0.3); background: rgba(78,204,163,0.08); }
.result-overlay { display: none; position: fixed; inset: 0; background: var(--menu-overlay); z-index: 10001; justify-content: center; align-items: center; backdrop-filter: blur(18px); padding: 20px; }
.result-overlay.active { display: flex; }
.result-box { background: var(--bg-menu); border-radius: 32px; border: 1px solid var(--border-color); box-shadow: 0 50px 120px var(--shadow-color), 0 0 80px var(--glow-player); padding: 36px 40px 28px; max-width: 520px; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 18px; animation: result-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes result-pop { 0% { transform: scale(0.7); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
.result-icon { font-size: 72px; line-height: 1; animation: result-bounce 1.6s ease-in-out infinite; filter: drop-shadow(0 0 30px var(--glow-player)); }
@keyframes result-bounce { 0%, 100% { transform: translateY(0) rotate(-2deg); } 50% { transform: translateY(-8px) rotate(2deg); } }
.result-title { font-size: 34px; font-weight: 800; letter-spacing: 4px; text-align: center; text-shadow: 0 0 30px var(--glow-player); }
.result-title.win { color: var(--color-player); }
.result-title.lose { color: var(--color-ai); }
.result-subtitle { color: var(--text-secondary); font-size: 14px; text-align: center; }
.result-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; width: 100%; margin-top: 4px; }
.result-stat { background: var(--bg-menu-card); border: 1px solid var(--border-color); border-radius: 14px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; }
.result-stat .rs-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-muted); }
.result-stat .rs-value { font-size: 20px; font-weight: 800; color: var(--text-primary); }
.result-stat .rs-value.accent-player { color: var(--color-player); }
.result-stat .rs-value.accent-ai { color: var(--color-ai); }
.result-buttons { display: flex; gap: 12px; width: 100%; margin-top: 6px; flex-wrap: wrap; }
.result-buttons .btn { flex: 1; padding: 14px 20px; font-size: 14px; border-radius: 16px; min-width: 140px; }
.game-menu-overlay { display: none; position: fixed; inset: 0; background: var(--menu-overlay); z-index: 10000; justify-content: center; align-items: center; backdrop-filter: blur(16px); padding: 20px; }
.game-menu-overlay.active { display: flex; }
.game-menu-box { background: var(--bg-menu); border-radius: 28px; border: 1px solid var(--border-color); box-shadow: 0 40px 100px var(--shadow-color); padding: 26px 28px 22px; max-width: 640px; width: 100%; max-height: 88vh; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; }
.game-menu-box::-webkit-scrollbar { width: 6px; }
.game-menu-box::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 10px; }
.gm-title { font-size: 18px; font-weight: 700; letter-spacing: 3px; color: var(--color-player); text-align: center; }
.gm-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; width: 100%; }
.gm-action-btn { padding: 14px 16px; border-radius: 14px; background: var(--bg-menu-card); border: 1.5px solid var(--border-color); color: var(--text-primary); font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.25s ease; display: flex; align-items: center; justify-content: center; gap: 8px; }
.gm-action-btn:hover:not(:disabled) { transform: translateY(-2px); }
.gm-action-btn.full { grid-column: 1 / -1; }
.gm-section { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.gm-section-title { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 4px; opacity: 0.8; padding-left: 2px; margin-top: 4px; }
.mechanics-list { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.mech-card { display: flex; align-items: flex-start; gap: 14px; padding: 12px 16px; background: var(--bg-menu-card); border-radius: 14px; border: 1px solid var(--border-color); }
.mech-icon { font-size: 28px; width: 40px; text-align: center; flex-shrink: 0; }
.mech-info { flex: 1; }
.mech-name { font-size: 14px; font-weight: 700; color: var(--text-primary); margin-bottom: 3px; }
.mech-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.55; }
.mech-desc strong { color: var(--text-primary); }
.gm-back-btn { align-self: flex-start; background: var(--btn-bg); border: 1px solid var(--border-color); border-radius: 30px; color: var(--text-muted); padding: 7px 18px; font-size: 12px; cursor: pointer; font-weight: 600; font-family: inherit; }
.gm-back-btn:hover { background: var(--border-light); color: var(--text-primary); }
.gm-panel { display: none; flex-direction: column; gap: 12px; width: 100%; }
.gm-panel.active { display: flex; }
@media (max-width: 600px) {
.game-container { padding: 8px; }
h1 { font-size: 17px; letter-spacing: 2px; }
.stat { font-size: 10px; }
.btn { padding: 4px 12px; font-size: 10px; }
.top-bar { flex-direction: column; align-items: center; }
.main-menu { min-width: unset; width: 92vw; padding: 28px 16px 24px; }
.main-menu h2 { font-size: 30px; letter-spacing: 5px; }
.modal-box { padding: 20px 16px 18px; }
.result-box { padding: 24px 20px 20px; }
.result-title { font-size: 24px; }
.result-icon { font-size: 54px; }
.quick-row { flex-direction: column; align-items: stretch; }
.quick-label { min-width: unset; }
.gm-actions { grid-template-columns: 1fr; }
.color-palette { grid-template-columns: repeat(3, 1fr); }
}
</style>
</head>
<body>
<canvas id="duckFallingCanvas"></canvas>
<!-- ===== МОДАЛЬНОЕ ОКНО ТЕМ ===== -->
<div class="modal-overlay" id="themeModal">
<div class="modal-box">
<div class="modal-header">
<div class="modal-title">🎨 ТЕМЫ</div>
<button class="modal-close" onclick="closeThemeModal()">✕</button>
</div>
<div class="modal-divider"></div>
<div class="modal-section-title">🖼 Оформление игры</div>
<div class="theme-selector" id="themeSelector">
<button class="theme-btn" data-theme="dark" onclick="setTheme('dark')"><span class="theme-icon">🌙</span><span class="theme-name">Тёмная</span></button>
<button class="theme-btn" data-theme="light" onclick="setTheme('light')"><span class="theme-icon">☀️</span><span class="theme-name">Светлая</span></button>
<button class="theme-btn" data-theme="duck" onclick="setTheme('duck')"><span class="theme-icon">🦆</span><span class="theme-name">Уточки</span></button>
<button class="theme-btn" data-theme="retro" onclick="setTheme('retro')"><span class="theme-icon">💣</span><span class="theme-name">Ретро</span></button>
<button class="theme-btn" data-theme="nature" onclick="setTheme('nature')"><span class="theme-icon">🌲</span><span class="theme-name">Природа</span></button>
<button class="theme-btn" data-theme="sunset" onclick="setTheme('sunset')"><span class="theme-icon">🌆</span><span class="theme-name">Закат</span></button>
<button class="theme-btn" data-theme="puzzletheme" onclick="setTheme('puzzletheme')"><span class="theme-icon">🧩</span><span class="theme-name">Гекс-пазл</span></button>
<button class="theme-btn" data-theme="coins" onclick="setTheme('coins')"><span class="theme-icon">💰</span><span class="theme-name">Монетки</span></button>
<button class="theme-btn" data-theme="sakura" onclick="setTheme('sakura')"><span class="theme-icon">🌸</span><span class="theme-name">Сакура</span></button>
<button class="theme-btn" data-theme="space" onclick="setTheme('space')"><span class="theme-icon">🚀</span><span class="theme-name">Космос</span></button>
</div>
<div class="modal-divider"></div>
<div class="modal-section-title">⬡ Стиль клеток</div>
<div class="theme-selector cells-grid" id="cellStyleSelector">
<button class="theme-btn" data-cellstyle="auto" onclick="setCellStyle('auto')"><span class="theme-icon">🔄</span><span class="theme-name">Как в теме</span></button>
<button class="theme-btn" data-cellstyle="classic" onclick="setCellStyle('classic')"><span class="cell-preview"><span style="background:#ff6b6b"></span><span style="background:#4ecca3"></span><span style="background:#3a5570"></span></span><span class="theme-name">Классика</span></button>
<button class="theme-btn" data-cellstyle="neon" onclick="setCellStyle('neon')"><span class="cell-preview"><span style="background:#ff006e"></span><span style="background:#00f5a0"></span><span style="background:#3d0066"></span></span><span class="theme-name">Неон</span></button>
<button class="theme-btn" data-cellstyle="pastel" onclick="setCellStyle('pastel')"><span class="cell-preview"><span style="background:#ffb3c6"></span><span style="background:#b8e6c1"></span><span style="background:#d8d0e8"></span></span><span class="theme-name">Пастель</span></button>
<button class="theme-btn" data-cellstyle="mono" onclick="setCellStyle('mono')"><span class="cell-preview"><span style="background:#e8e8e8"></span><span style="background:#888888"></span><span style="background:#3a3a3a"></span></span><span class="theme-name">Моно</span></button>
<button class="theme-btn" data-cellstyle="duck" onclick="setCellStyle('duck')"><span class="theme-icon">🦆</span><span class="theme-name">Уточки</span></button>
<button class="theme-btn" data-cellstyle="_67" onclick="setCellStyle('_67')"><span class="theme-icon">6️⃣7️⃣</span><span class="theme-name">67</span></button>
<button class="theme-btn" data-cellstyle="lava" onclick="setCellStyle('lava')"><span class="cell-preview"><span style="background:#ff4500"></span><span style="background:#ffb300"></span><span style="background:#8b0000"></span></span><span class="theme-name">Лава</span></button>
<button class="theme-btn" data-cellstyle="space" onclick="setCellStyle('space')"><span class="cell-preview"><span style="background:#7b2cbf"></span><span style="background:#00b4d8"></span><span style="background:#240046"></span></span><span class="theme-name">Космос</span></button>
<button class="theme-btn" data-cellstyle="forest" onclick="setCellStyle('forest')"><span class="cell-preview"><span style="background:#d97a1a"></span><span style="background:#2e8b57"></span><span style="background:#8b7355"></span></span><span class="theme-name">Лес</span></button>
<button class="theme-btn" data-cellstyle="sunset" onclick="setCellStyle('sunset')"><span class="cell-preview"><span style="background:#ff8c42"></span><span style="background:#b84edb"></span><span style="background:#6b5a70"></span></span><span class="theme-name">Закат</span></button>
<button class="theme-btn" data-cellstyle="crystal" onclick="setCellStyle('crystal')"><span class="cell-preview"><span style="background:#4dd0e1"></span><span style="background:#b388ff"></span><span style="background:#a0d8e8"></span></span><span class="theme-name">Кристалл</span></button>
<button class="theme-btn" data-cellstyle="puzzle" onclick="setCellStyle('puzzle')"><span class="theme-icon">🧩</span><span class="theme-name">Гекс-пазл</span></button>
<button class="theme-btn" data-cellstyle="coin" onclick="setCellStyle('coin')"><span class="theme-icon">💰</span><span class="theme-name">Монетки</span></button>
<button class="theme-btn" data-cellstyle="sakura" onclick="setCellStyle('sakura')"><span class="theme-icon">🌸</span><span class="theme-name">Сакура</span></button>
<button class="theme-btn" data-cellstyle="space" onclick="setCellStyle('space')"><span class="theme-icon">🚀</span><span class="theme-name">Космос</span></button>
</div>
<div class="modal-divider"></div>
<div class="modal-section-title">🎨 Цвет игрока</div>
<div class="color-palette" id="colorPalette"></div>
</div>
</div>
<!-- ===== ГЛАВНОЕ МЕНЮ ===== -->
<div class="menu-overlay active" id="menuOverlay">
<div class="main-menu" id="mainMenu">
<div class="menu-header">
<h2>✦ HEXAGONIC ✦</h2>
<div class="menu-subtitle">Гексагональная стратегия</div>
</div>
<div class="menu-divider"></div>
<div class="menu-main-buttons" id="menuMain">
<button class="menu-main-btn" onclick="showSubMenu('play')"><span class="btn-icon">▶</span><span class="btn-label">Играть</span></button>
<button class="menu-main-btn" onclick="showSubMenu('tutorial')"><span class="btn-icon">📖</span><span class="btn-label">Обучение</span></button>
<button class="menu-main-btn" onclick="showSubMenu('settings')"><span class="btn-icon">⚙</span><span class="btn-label">Настройки</span></button>
<button class="menu-main-btn" onclick="showThemeMenu()"><span class="btn-icon">🎨</span><span class="btn-label">Темы</span></button>
</div>
<div class="menu-sub" id="menuSubPlay">
<button class="menu-sub-back" onclick="showMainMenu()">← Назад</button>
<div class="game-options">
<div class="quick-selectors">
<div class="quick-row">
<span class="quick-label">📐 Размер</span>
<div class="quick-options" id="sizeSelector">
<button class="quick-btn" data-size="small" onclick="selectSize('small')"><span class="qb-icon">▫️</span><span class="qb-label">8×6</span></button>
<button class="quick-btn active" data-size="medium" onclick="selectSize('medium')"><span class="qb-icon">◻️</span><span class="qb-label">12×10</span></button>
<button class="quick-btn" data-size="large" onclick="selectSize('large')"><span class="qb-icon">⬜</span><span class="qb-label">16×12</span></button>
<button class="quick-btn" data-size="huge" onclick="selectSize('huge')"><span class="qb-icon">🟦</span><span class="qb-label">20×16</span></button>
</div>
</div>
<div class="quick-row">
<span class="quick-label">🎮 Режим</span>
<div class="quick-options" id="modeSelector">
<button class="quick-btn active" data-mode="classic" onclick="selectMode('classic')"><span class="qb-icon">♟️</span><span class="qb-label">Классика</span></button>
<button class="quick-btn" data-mode="elements" onclick="selectMode('elements')"><span class="qb-icon">💎</span><span class="qb-label">Стихии</span></button>
</div>
</div>
<div class="quick-row">
<span class="quick-label">⚔️ Противники</span>
<div class="quick-options" id="aiCountSelector">
<button class="quick-btn active" data-ai="1" onclick="selectAiCount(1)"><span class="qb-icon">1️⃣</span><span class="qb-label">1 противник</span></button>
<button class="quick-btn" data-ai="2" onclick="selectAiCount(2)"><span class="qb-icon">2️⃣</span><span class="qb-label">2 противника</span></button>
<button class="quick-btn" data-ai="3" onclick="selectAiCount(3)"><span class="qb-icon">3️⃣</span><span class="qb-label">3 противника</span></button>
</div>
</div>
</div>
<div class="fog-toggle-row" id="fogToggleRow" onclick="toggleFog()">
<div class="fog-toggle-check" id="fogToggleCheck">✓</div>
<div class="fog-toggle-info">
<span class="fog-toggle-label">🌫️ Туман войны</span>
<span class="fog-toggle-desc">Видно только клетки в радиусе 2 от ваших</span>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-section-title">🗺️ Выберите карту</div>
<div class="map-grid">
<button class="map-btn active" data-map="1" onclick="selectMap(1)"><span class="map-icon">🏝️</span><span class="map-name">Острова</span><span class="map-desc">с водой, по углам</span></button>
<button class="map-btn" data-map="2" onclick="selectMap(2)"><span class="map-icon">🌿</span><span class="map-name">Долина</span><span class="map-desc">с водой, по бокам</span></button>
<button class="map-btn" data-map="3" onclick="selectMap(3)"><span class="map-icon">🏜️</span><span class="map-name">Пустыня</span><span class="map-desc">без воды, по углам</span></button>
<button class="map-btn" data-map="4" onclick="selectMap(4)"><span class="map-icon">⛰️</span><span class="map-name">Нагорье</span><span class="map-desc">без воды, центр-край</span></button>
</div>
<div class="menu-divider"></div>
<div class="menu-section-title">⚔️ Выберите сложность</div>
<div class="difficulty-list">
<button class="diff-card" onclick="startGame(1)"><span class="diff-icon">🌱</span><div class="diff-info"><span class="diff-label">Лёгкий</span><span class="diff-desc">2 атаки, редкие пропуски</span></div><span class="diff-badge">спокойный</span></button>
<button class="diff-card" onclick="startGame(2)"><span class="diff-icon">⚡</span><div class="diff-info"><span class="diff-label">Средний</span><span class="diff-desc">3 атаки, иногда пропускает</span></div><span class="diff-badge">сбалансированный</span></button>
<button class="diff-card" onclick="startGame(3)"><span class="diff-icon">🔥</span><div class="diff-info"><span class="diff-label">Сложный</span><span class="diff-desc">Все атаки, фокус на игроке</span></div><span class="diff-badge">вызов</span></button>
<button class="diff-card" onclick="startGame(4)"><span class="diff-icon">💀</span><div class="diff-info"><span class="diff-label">Невозможный</span><span class="diff-desc">Все атаки, только игрок</span></div><span class="diff-badge">экстрим</span></button>
</div>
</div>
</div>
<div class="menu-sub" id="menuSubTutorial">
<button class="menu-sub-back" onclick="showMainMenu()">← Назад</button>
<div class="tutorial-container">
<div class="tutorial-slide active" data-step="0">
<div class="step-badge">📖 Шаг 1 из 12</div>
<div class="step-title"><span class="emoji">🎯</span> Цель игры</div>
<div class="step-desc">
<strong>Hexagonic</strong> — пошаговая стратегия на гексагональном поле. Ваша цель — <strong>захватить все клетки противников</strong> или остаться единственным, у кого есть клетки на поле.
</div>
</div>
<div class="tutorial-slide" data-step="1">
<div class="step-badge">📖 Шаг 2 из 12</div>
<div class="step-title"><span class="emoji">⚡</span> Очки прокачки</div>
<div class="step-desc">В начале <strong>каждого своего хода</strong> вы получаете <strong>1 очко прокачки за каждую свою клетку</strong>.</div>
</div>
<div class="tutorial-slide" data-step="2">
<div class="step-badge">📖 Шаг 3 из 12</div>
<div class="step-title"><span class="emoji">📤</span> Кнопки управления</div>
<div class="step-desc"><strong>📤 Распределить</strong>, <strong>🔱 MAX</strong>, <strong>⏭ Пропустить</strong>.</div>
</div>
<div class="tutorial-slide" data-step="3">
<div class="step-badge">📖 Шаг 4 из 12</div>
<div class="step-title"><span class="emoji">⚔️</span> Атака</div>
<div class="step-desc">Кликните по <strong>своей клетке</strong>, затем по <strong>соседней</strong> вражеской или нейтральной.</div>
</div>
<div class="tutorial-slide" data-step="4">
<div class="step-badge">📖 Шаг 5 из 12</div>
<div class="step-title"><span class="emoji">⛓️</span> Цепочка захватов</div>
<div class="step-desc">После захвата выделение <strong>переходит на захваченную клетку</strong>.</div>
</div>
<div class="tutorial-slide" data-step="5">
<div class="step-badge">📖 Шаг 6 из 12</div>
<div class="step-title"><span class="emoji">🖱️</span> Управление выделением</div>
<div class="step-desc">Клик по своей — выбрать, повторный — снять, по другой своей — переключить, по соседней — атака.</div>
</div>
<div class="tutorial-slide" data-step="6">
<div class="step-badge">📖 Шаг 7 из 12</div>
<div class="step-title"><span class="emoji">💎</span> Режим «Стихии»</div>
<div class="step-desc">5 типов спец-клеток: 💎 Кристалл, 🏰 Крепость, ⚡ Энергия, 🌋 Вулкан, 🎁 Бонус.</div>
</div>
<div class="tutorial-slide" data-step="7">
<div class="step-badge">📖 Шаг 8 из 12</div>
<div class="step-title"><span class="emoji">💎</span> Кристалл и Энергия</div>
<div class="step-desc">Кристалл даёт +3 силы, Энергия даёт +3 очка прокачки.</div>
</div>
<div class="tutorial-slide" data-step="8">
<div class="step-badge">📖 Шаг 9 из 12</div>
<div class="step-title"><span class="emoji">🏰</span> Крепость и Вулкан</div>
<div class="step-desc">Крепость — защита +2. Вулкан — взрывается при захвате.</div>
</div>
<div class="tutorial-slide" data-step="9">
<div class="step-badge">📖 Шаг 10 из 12</div>
<div class="step-title"><span class="emoji">🎁</span> Бонус</div>
<div class="step-desc">Случайный эффект: +4 силы, +2 очка или +1 силы соседям.</div>
</div>
<div class="tutorial-slide" data-step="10">
<div class="step-badge">📖 Шаг 11 из 12</div>
<div class="step-title"><span class="emoji">🤖</span> Противники</div>
<div class="step-desc">Можно играть против 1, 2 или 3 противников.</div>
</div>
<div class="tutorial-slide" data-step="11">
<div class="step-badge">📖 Шаг 12 из 12</div>
<div class="step-title"><span class="emoji">🏆</span> Победа</div>
<div class="step-desc">Игра заканчивается, когда у кого-то <strong>не остаётся клеток</strong>.</div>
</div>
<div class="tutorial-nav">
<button class="tut-btn" id="tutPrev" onclick="tutorialPrev()" disabled>← Назад</button>
<div class="dots" id="tutDots"></div>
<button class="tut-btn primary" id="tutNext" onclick="tutorialNext()">Далее →</button>
</div>
</div>
</div>
<div class="menu-sub" id="menuSubSettings">
<button class="menu-sub-back" onclick="showMainMenu()">← Назад</button>
<div class="menu-divider"></div>
<div class="menu-section-title">🔊 Звук</div>
<div class="settings-container">
<div class="audio-setting-card">
<div class="audio-setting-head">
<span class="audio-setting-title">🎵 Громкость музыки</span>
<span class="audio-setting-value" id="musicVolumeValue">35%</span>
</div>
<div class="audio-row">
<input class="audio-range" id="musicVolume" type="range" min="0" max="100" value="35" oninput="setMusicVolume(this.value)">
<button class="audio-toggle enabled" id="musicToggleBtn" onclick="toggleMusic()">🎵 Вкл.</button>
</div>
</div>
<div class="audio-setting-card">
<div class="audio-setting-head">
<span class="audio-setting-title">🔔 Громкость звуков</span>
<span class="audio-setting-value" id="soundVolumeValue">100%</span>
</div>
<div class="audio-row">
<input class="audio-range" id="soundVolume" type="range" min="0" max="100" value="100" oninput="setSoundVolume(this.value)">
<button class="audio-toggle enabled" id="soundToggleBtn" onclick="toggleSound()">🔊 Вкл.</button>
</div>
</div>
<div class="audio-setting-card">
<div class="audio-setting-head">
<span class="audio-setting-title">🎼 Трек</span>
<span class="audio-setting-value" id="trackNameValue">—</span>
</div>
<div class="audio-row">
<button class="audio-toggle enabled" id="trackPrevBtn" onclick="prevTrack()">⏮</button>
<button class="audio-toggle enabled" id="trackNextBtn" onclick="nextTrack()">⏭</button>
</div>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-section-title">💬 Подсказки</div>
<div class="settings-container">
<div class="audio-setting-card">
<div class="audio-setting-head">
<span class="audio-setting-title">💬 Подсказки под полем</span>
<span class="audio-setting-value" id="hintsStateLabel">Включены</span>
</div>
<div class="audio-row">
<button class="audio-toggle enabled" id="hintsToggleBtn" onclick="toggleHints()">💬 Вкл.</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== ИГРОВОЕ ПОЛЕ ===== -->
<div class="game-container">
<h1>✦ HEXAGONIC ✦</h1>
<div class="top-bar">
<div class="stats">
<div class="stat"><span class="color-dot" id="playerDot" style="background:var(--color-player);"></span> Вы: <span id="playerScore">0</span></div>
<div class="stat" id="ai1Stat"><span class="color-dot" id="ai1Dot" style="background:#4ecca3;"></span> Пр1: <span id="ai1Score">0</span></div>
<div class="stat" id="ai2Stat" style="display:none;"><span class="color-dot" id="ai2Dot" style="background:#42a5f5;"></span> Пр2: <span id="ai2Score">0</span></div>
<div class="stat" id="ai3Stat" style="display:none;"><span class="color-dot" id="ai3Dot" style="background:#b84edb;"></span> Пр3: <span id="ai3Score">0</span></div>
<div class="stat">Ход: <span id="turnNum">1</span></div>
<div class="stat">⚡ <span id="upgradePoints">0</span></div>
<div class="stat">⚔ <span id="canAttack">0</span> атак</div>
</div>
<div class="controls">
<button class="btn btn-green" id="distributeBtn" onclick="distributePoints()" disabled>📤 Распределить</button>
<button class="btn btn-gold" id="maxBtn" onclick="toggleMaxMode()" disabled>🔱 MAX</button>
<button class="btn btn-outline" id="skipBtn" onclick="skipTurn()">⏭ Пропустить</button>
<button class="game-menu-btn" id="gameMenuBtn" onclick="openGameMenu()" title="Меню игры">⚙</button>
</div>
</div>
<div class="turn-indicator" id="turnIndicator">🔴 Ваш ход</div>
<canvas id="gameCanvas"></canvas>
<div class="message" id="message"></div>
</div>
<!-- ===== ИГРОВОЕ МЕНЮ (ШЕСТЕРЁНКА) ===== -->
<div class="game-menu-overlay" id="gameMenuOverlay">
<div class="game-menu-box">
<div class="gm-title">⚙ МЕНЮ ИГРЫ</div>
<div class="gm-panel active" id="gmPanelActions">
<div class="gm-actions">
<button class="gm-action-btn" onclick="resetGame(); closeGameMenu();">🔄 Новая партия</button>
<button class="gm-action-btn" onclick="showMechanicsPanel()">📖 Механики игры</button>
<button class="gm-action-btn full" onclick="closeGameMenu(); showMenu();">📋 Главное меню</button>
</div>
</div>
<div class="gm-panel" id="gmPanelMechanics">
<button class="gm-back-btn" onclick="showActionsPanel()">← Назад</button>
<div class="gm-section-title">⚙ Базовые механики</div>
<div class="mechanics-list">
<div class="mech-card"><div class="mech-icon">⚡</div><div class="mech-info"><div class="mech-name">Очки прокачки</div><div class="mech-desc">В начале хода вы получаете <strong>1 очко за каждую свою клетку</strong>. Максимум силы — <strong>20</strong>.</div></div></div>
<div class="mech-card"><div class="mech-icon">⚔</div><div class="mech-info"><div class="mech-name">Атака и потери</div><div class="mech-desc">Ваша клетка должна быть силой > 1. После атаки она падает до <strong>1</strong>.</div></div></div>
<div class="mech-card"><div class="mech-icon">🤝</div><div class="mech-info"><div class="mech-name">Стычки равных сил</div><div class="mech-desc">Если силы равны — <strong>50% захват</strong>, <strong>50% обе клетки до 1</strong>.</div></div></div>
<div class="mech-card"><div class="mech-icon">⛓️</div><div class="mech-info"><div class="mech-name">Цепочка захватов</div><div class="mech-desc">После захвата выделение <strong>переходит на захваченную клетку</strong>.</div></div></div>
</div>
<div class="gm-section-title">💎 Спец-клетки</div>
<div class="mechanics-list">
<div class="mech-card"><div class="mech-icon">💎</div><div class="mech-info"><div class="mech-name">Кристалл</div><div class="mech-desc">При первом захвате даёт <strong>+3 силы</strong> новой клетке.</div></div></div>
<div class="mech-card"><div class="mech-icon">🏰</div><div class="mech-info"><div class="mech-name">Крепость</div><div class="mech-desc">Защита <strong>+2</strong> при каждой атаке.</div></div></div>
<div class="mech-card"><div class="mech-icon">⚡</div><div class="mech-info"><div class="mech-name">Энергия</div><div class="mech-desc">При первом захвате даёт вам <strong>+3 очка прокачки</strong>.</div></div></div>
<div class="mech-card"><div class="mech-icon">🌋</div><div class="mech-info"><div class="mech-name">Вулкан</div><div class="mech-desc">При захвате <strong>взрывается</strong>: 1 урон всем соседям.</div></div></div>
<div class="mech-card"><div class="mech-icon">🎁</div><div class="mech-info"><div class="mech-name">Бонус</div><div class="mech-desc">Случайный эффект: <strong>+4 силы</strong>, <strong>+2 очка</strong> или <strong>+1 силы соседям</strong>.</div></div></div>
</div>
</div>
</div>
</div>
<!-- ===== РЕЗУЛЬТАТ ===== -->
<div class="result-overlay" id="resultOverlay">
<div class="result-box">
<div class="result-icon" id="resultIcon">🏆</div>
<div class="result-title win" id="resultTitle">ПОБЕДА!</div>
<div class="result-subtitle" id="resultSubtitle">Вы захватили все клетки</div>
<div class="result-stats">
<div class="result-stat"><span class="rs-label">🔴 Ваши клетки</span><span class="rs-value accent-player" id="rsPlayerCells">0</span></div>
<div class="result-stat"><span class="rs-label">🟢 Клетки противников</span><span class="rs-value accent-ai" id="rsAiCells">0</span></div>
<div class="result-stat"><span class="rs-label">🔄 Ходов</span><span class="rs-value" id="rsTurns">0</span></div>
<div class="result-stat"><span class="rs-label">⚔ Атак</span><span class="rs-value" id="rsAttacks">0</span></div>
<div class="result-stat"><span class="rs-label">⚡ Прокачек</span><span class="rs-value" id="rsUpgrades">0</span></div>
<div class="result-stat"><span class="rs-label">🎯 Захватов</span><span class="rs-value" id="rsCaptures">0</span></div>
</div>
<div class="result-buttons">
<button class="btn btn-green" onclick="rematch()">🔄 Реванш</button>
<button class="btn btn-secondary" onclick="closeResult()">📋 В меню</button>
</div>
</div>
</div>
<script>
/* ============================================================
HEXAGONIC 1.57.0
============================================================
+ Надпись «КРЯ» уменьшена в 2.5 раза (было 26px → 10px)
+ Добавлено 5 общих треков (разные жанры/настроения)
+ Добавлено 3 уникальных трека для темы «Космос»
+ Автопереключение трека при смене темы
+ Кнопки ⏮/⏭ в настройках
============================================================ */
const SIZE_PRESETS = {
small: { cols: 8, rows: 6, hex: 34 },
medium: { cols: 12, rows: 10, hex: 30 },
large: { cols: 16, rows: 12, hex: 24 },
huge: { cols: 20, rows: 16, hex: 20 }
};
let COLS = 12, ROWS = 10, HEX_SIZE = 30, SPACING = HEX_SIZE * 1.75;
let W = (COLS - 1) * SPACING + SPACING / 2 + HEX_SIZE * 2;
let H = (ROWS - 1) * (HEX_SIZE * 1.5) + HEX_SIZE * 2;
const PLAYER = 0;
const MAX_POWER = 20;
const FOG_RADIUS = 2;
const AI_PALETTE = [
null,
{ key: 'p1', name: 'Противник 1', short: 'Пр1', base: '#4ecca3', edge: '#2a9a75', glow: 'rgba(78,204,163,0.4)', dot: '#4ecca3' },
{ key: 'p2', name: 'Противник 2', short: 'Пр2', base: '#42a5f5', edge: '#1e6bb0', glow: 'rgba(66,165,245,0.4)', dot: '#42a5f5' },
{ key: 'p3', name: 'Противник 3', short: 'Пр3', base: '#b84edb', edge: '#8028a0', glow: 'rgba(184,78,219,0.4)', dot: '#b84edb' }
];
const DIFF_MAX_ATTACKS = [0, 2, 3, 999, 999];
const DIFF_PLAYER_PRIO = [0, 0.5, 0.7, 0.9, 1.0];
const DIFF_SKIP_CHANCE = [0, 0.30, 0.08, 0, 0];
const CELL_TYPES = {
crystal: { icon: '💎', label: 'Кристалл' },
fortress: { icon: '🏰', label: 'Крепость' },
energy: { icon: '⚡', label: 'Энергия' },
volcano: { icon: '🌋', label: 'Вулкан' },
bonus: { icon: '🎁', label: 'Бонус' }
};
const PLAYER_COLORS = {
red: { name: 'Красный', base: '#ff6b6b', edge: '#c04040', glow: 'rgba(255,107,107,0.35)', light: '#d04040', lightEdge: '#a03030', lightGlow: 'rgba(208,64,64,0.35)', duck: '#ff5a5a', duckEdge: '#c04040', duckGlow: 'rgba(255,90,90,0.5)', retro: '#d02020', retroEdge: '#800000', retroGlow: 'rgba(208,32,32,0.35)', nature: '#d94a4a', natureEdge: '#8b2020', natureGlow: 'rgba(217,74,74,0.5)', sunset: '#ff5a6e', sunsetEdge: '#b03040', sunsetGlow: 'rgba(255,90,110,0.6)', puzzletheme: '#ff5a5a', puzzlethemeEdge: '#c04040', puzzlethemeGlow: 'rgba(255,90,90,0.6)', coins: '#ff5a5a', coinsEdge: '#b02020', coinsGlow: 'rgba(255,90,90,0.6)', sakura: '#ff5a8a', sakuraEdge: '#c04070', sakuraGlow: 'rgba(255,90,138,0.5)', space: '#ff6b9d', spaceEdge: '#b03060', spaceGlow: 'rgba(255,107,157,0.7)' },
blue: { name: 'Синий', base: '#4d9fff', edge: '#2a6ac0', glow: 'rgba(77,159,255,0.35)', light: '#3070d0', lightEdge: '#2050a0', lightGlow: 'rgba(48,112,208,0.35)', duck: '#4da6ff', duckEdge: '#2a70c0', duckGlow: 'rgba(77,166,255,0.5)', retro: '#0000c0', retroEdge: '#000080', retroGlow: 'rgba(0,0,192,0.4)', nature: '#3d8bd9', natureEdge: '#1a5090', natureGlow: 'rgba(61,139,217,0.5)', sunset: '#6a8aff', sunsetEdge: '#3040b0', sunsetGlow: 'rgba(106,138,255,0.6)', puzzletheme: '#4d9fff', puzzlethemeEdge: '#2a6ac0', puzzlethemeGlow: 'rgba(77,159,255,0.6)', coins: '#5a9aff', coinsEdge: '#2a60b0', coinsGlow: 'rgba(90,154,255,0.6)', sakura: '#8ab8ff', sakuraEdge: '#4070c0', sakuraGlow: 'rgba(138,184,255,0.5)', space: '#6bcaff', spaceEdge: '#2a80b0', spaceGlow: 'rgba(107,202,255,0.7)' },
purple: { name: 'Фиолет', base: '#b06aff', edge: '#8040c0', glow: 'rgba(176,106,255,0.4)', light: '#9050e0', lightEdge: '#6030b0', lightGlow: 'rgba(144,80,224,0.35)', duck: '#b06aff', duckEdge: '#8040c0', duckGlow: 'rgba(176,106,255,0.5)', retro: '#800080', retroEdge: '#400040', retroGlow: 'rgba(128,0,128,0.4)', nature: '#a060d0', natureEdge: '#603090', natureGlow: 'rgba(160,96,208,0.5)', sunset: '#c86aff', sunsetEdge: '#8040b0', sunsetGlow: 'rgba(200,106,255,0.6)', puzzletheme: '#b06aff', puzzlethemeEdge: '#8040c0', puzzlethemeGlow: 'rgba(176,106,255,0.6)', coins: '#c87aff', coinsEdge: '#9050c0', coinsGlow: 'rgba(200,122,255,0.6)', sakura: '#d08aff', sakuraEdge: '#9050c0', sakuraGlow: 'rgba(208,138,255,0.5)', space: '#b06bff', spaceEdge: '#7030c0', spaceGlow: 'rgba(176,107,255,0.7)' },
green: { name: 'Зелёный', base: '#5adc8a', edge: '#2aa05a', glow: 'rgba(90,220,138,0.4)', light: '#30b060', lightEdge: '#108040', lightGlow: 'rgba(48,176,96,0.35)', duck: '#5adc8a', duckEdge: '#2aa05a', duckGlow: 'rgba(90,220,138,0.5)', retro: '#00a000', retroEdge: '#006000', retroGlow: 'rgba(0,160,0,0.4)', nature: '#5adc8a', natureEdge: '#2aa05a', natureGlow: 'rgba(90,220,138,0.5)', sunset: '#5adc8a', sunsetEdge: '#2aa05a', sunsetGlow: 'rgba(90,220,138,0.6)', puzzletheme: '#5adc8a', puzzlethemeEdge: '#2aa05a', puzzlethemeGlow: 'rgba(90,220,138,0.6)', coins: '#a8d84a', coinsEdge: '#709020', coinsGlow: 'rgba(168,216,74,0.6)', sakura: '#8ae0a0', sakuraEdge: '#40a060', sakuraGlow: 'rgba(138,224,160,0.5)', space: '#6bffb0', spaceEdge: '#20a060', spaceGlow: 'rgba(107,255,176,0.7)' },
orange: { name: 'Оранж', base: '#ff9a3d', edge: '#c06010', glow: 'rgba(255,154,61,0.4)', light: '#e07020', lightEdge: '#a05010', lightGlow: 'rgba(224,112,32,0.35)', duck: '#ff9a3d', duckEdge: '#c06010', duckGlow: 'rgba(255,154,61,0.5)', retro: '#c06000', retroEdge: '#804000', retroGlow: 'rgba(192,96,0,0.4)', nature: '#e08030', natureEdge: '#a05010', natureGlow: 'rgba(224,128,48,0.5)', sunset: '#ff9a3d', sunsetEdge: '#c06010', sunsetGlow: 'rgba(255,154,61,0.6)', puzzletheme: '#ff7043', puzzlethemeEdge: '#c04828', puzzlethemeGlow: 'rgba(255,112,67,0.7)', coins: '#ffb840', coinsEdge: '#c08010', coinsGlow: 'rgba(255,184,64,0.7)', sakura: '#ffa0c0', sakuraEdge: '#c06080', sakuraGlow: 'rgba(255,160,192,0.5)', space: '#ffaa66', spaceEdge: '#b06020', spaceGlow: 'rgba(255,170,102,0.7)' },
cyan: { name: 'Голубой', base: '#4dd0e1', edge: '#2090a0', glow: 'rgba(77,208,225,0.4)', light: '#30a8b8', lightEdge: '#107880', lightGlow: 'rgba(48,168,184,0.35)', duck: '#4dd0e1', duckEdge: '#2090a0', duckGlow: 'rgba(77,208,225,0.5)', retro: '#008080', retroEdge: '#004040', retroGlow: 'rgba(0,128,128,0.4)', nature: '#40b0c0', natureEdge: '#207080', natureGlow: 'rgba(64,176,192,0.5)', sunset: '#6ad8ff', sunsetEdge: '#3090b0', sunsetGlow: 'rgba(106,216,255,0.6)', puzzletheme: '#4dd0e1', puzzlethemeEdge: '#2090a0', puzzlethemeGlow: 'rgba(77,208,225,0.7)', coins: '#6ad8e8', coinsEdge: '#30a0b0', coinsGlow: 'rgba(106,216,232,0.6)', sakura: '#8ae0f0', sakuraEdge: '#40a0b0', sakuraGlow: 'rgba(138,224,240,0.5)', space: '#7be0ff', spaceEdge: '#2090b0', spaceGlow: 'rgba(123,224,255,0.75)' },
yellow: { name: 'Жёлтый', base: '#ffd93d', edge: '#b8801a', glow: 'rgba(255,217,61,0.45)', light: '#e0b820', lightEdge: '#a08010', lightGlow: 'rgba(224,184,32,0.4)', duck: '#ffd93d', duckEdge: '#b8801a', duckGlow: 'rgba(255,217,61,0.55)', retro: '#c0a000', retroEdge: '#806800', retroGlow: 'rgba(192,160,0,0.4)', nature: '#e8c830', natureEdge: '#a08818', natureGlow: 'rgba(232,200,48,0.55)', sunset: '#ffd93d', sunsetEdge: '#b8801a', sunsetGlow: 'rgba(255,217,61,0.65)', puzzletheme: '#ffd93d', puzzlethemeEdge: '#b8801a', puzzlethemeGlow: 'rgba(255,217,61,0.65)', coins: '#fff8d0', coinsEdge: '#c0a020', coinsGlow: 'rgba(255,248,208,0.7)', sakura: '#ffe0a0', sakuraEdge: '#c0a060', sakuraGlow: 'rgba(255,224,160,0.55)', space: '#ffe066', spaceEdge: '#b0a020', spaceGlow: 'rgba(255,224,102,0.8)' },
pink: { name: 'Розовый', base: '#ff7ab8', edge: '#c04080', glow: 'rgba(255,122,184,0.4)', light: '#e050a0', lightEdge: '#a03070', lightGlow: 'rgba(224,80,160,0.35)', duck: '#ff7ab8', duckEdge: '#c04080', duckGlow: 'rgba(255,122,184,0.55)', retro: '#c00080', retroEdge: '#800040', retroGlow: 'rgba(192,0,128,0.4)', nature: '#e870a8', natureEdge: '#a04070', natureGlow: 'rgba(232,112,168,0.55)', sunset: '#ff8ac8', sunsetEdge: '#c05090', sunsetGlow: 'rgba(255,138,200,0.65)', puzzletheme: '#ff7ab8', puzzlethemeEdge: '#c04080', puzzlethemeGlow: 'rgba(255,122,184,0.65)', coins: '#ff9ac8', coinsEdge: '#c060a0', coinsGlow: 'rgba(255,154,200,0.65)', sakura: '#ffa0c8', sakuraEdge: '#c06090', sakuraGlow: 'rgba(255,160,200,0.6)', space: '#ff8ac8', spaceEdge: '#c05090', spaceGlow: 'rgba(255,138,200,0.75)' },
teal: { name: 'Бирюза', base: '#2ecc9c', edge: '#108060', glow: 'rgba(46,204,156,0.4)', light: '#20a878', lightEdge: '#086848', lightGlow: 'rgba(32,168,120,0.35)', duck: '#2ecc9c', duckEdge: '#108060', duckGlow: 'rgba(46,204,156,0.55)', retro: '#008060', retroEdge: '#004030', retroGlow: 'rgba(0,128,96,0.4)', nature: '#2ecc9c', natureEdge: '#108060', natureGlow: 'rgba(46,204,156,0.55)', sunset: '#2ecc9c', sunsetEdge: '#108060', sunsetGlow: 'rgba(46,204,156,0.65)', puzzletheme: '#2ecc9c', puzzlethemeEdge: '#108060', puzzlethemeGlow: 'rgba(46,204,156,0.65)', coins: '#4dd8b0', coinsEdge: '#209070', coinsGlow: 'rgba(77,216,176,0.65)', sakura: '#4dd8b0', sakuraEdge: '#209070', sakuraGlow: 'rgba(77,216,176,0.55)', space: '#3effd0', spaceEdge: '#10a080', spaceGlow: 'rgba(62,255,208,0.75)' },
coral: { name: 'Коралл', base: '#ff7f50', edge: '#b85030', glow: 'rgba(255,127,80,0.4)', light: '#e06040', lightEdge: '#a04020', lightGlow: 'rgba(224,96,64,0.35)', duck: '#ff7f50', duckEdge: '#b85030', duckGlow: 'rgba(255,127,80,0.55)', retro: '#c04020', retroEdge: '#802010', retroGlow: 'rgba(192,64,32,0.4)', nature: '#e07040', natureEdge: '#a04020', natureGlow: 'rgba(224,112,64,0.55)', sunset: '#ff7f50', sunsetEdge: '#b85030', sunsetGlow: 'rgba(255,127,80,0.65)', puzzletheme: '#ff8a5c', puzzlethemeEdge: '#c05030', puzzlethemeGlow: 'rgba(255,138,92,0.7)', coins: '#ff9a70', coinsEdge: '#c06040', coinsGlow: 'rgba(255,154,112,0.65)', sakura: '#ff9a8a', sakuraEdge: '#c06050', sakuraGlow: 'rgba(255,154,138,0.55)', space: '#ff9066', spaceEdge: '#c05030', spaceGlow: 'rgba(255,144,102,0.75)' }
};
let currentPlayerColor = 'red';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = W; canvas.height = H;
const duckCanvas = document.getElementById('duckFallingCanvas');
const duckCtx = duckCanvas.getContext('2d');
let grid = [];
let currentPlayer = PLAYER;
let turnNumber = 1;
let selectedCell = null;
let gameOver = false;
let aiThinking = false;
let upgradePoints = 0;
let usedUpgrades = 0;
let totalUpgrades = 0;
let currentMap = 1;
let turnPhase = 'idle';
let playerTurnActive = false;
let difficulty = 2;
let gameStarted = false;
let maxModeActive = false;
let currentTheme = 'dark';
let currentCellStyle = 'auto';
let currentSize = 'medium';
let currentMode = 'classic';
let aiCount = 1;
let fogOfWar = false;
let turnOrder = [0, 1];
let currentTurnIndex = 0;
let stats = { attacks: 0, captures: 0, upgrades: 0 };
let popupEffects = [];
let popupFrameId = null;
let popupLastTime = 0;
let hintsEnabled = true;
const THEMES = {
dark: { label: 'Тёмная' }, light: { label: 'Светлая' }, duck: { label: 'Уточки' },
retro: { label: 'Ретро' }, nature: { label: 'Природа' }, sunset: { label: 'Закат' },
puzzletheme: { label: 'Гекс-пазл' }, coins: { label: 'Монетки' },
sakura: { label: 'Сакура' }, space: { label: 'Космос' }
};
const CELL_STYLES = {
classic: { neutral: '#3a5570', neutralEdge: '#1e3448', neutralGlow: 'rgba(58,85,112,0.2)', selected: '#ffd700', selectedGlow: 'rgba(255,215,0,0.5)' },
neon: { neutral: '#3d0066', neutralEdge: '#1f0033', neutralGlow: 'rgba(61,0,102,0.5)', selected: '#fff700', selectedGlow: 'rgba(255,247,0,0.8)' },
pastel: { neutral: '#d8d0e8', neutralEdge: '#b0a5c4', neutralGlow: 'rgba(216,208,232,0.4)', selected: '#ffe680', selectedGlow: 'rgba(255,230,128,0.5)' },
mono: { neutral: '#3a3a3a', neutralEdge: '#1a1a1a', neutralGlow: 'rgba(58,58,58,0.3)', selected: '#ffffff', selectedGlow: 'rgba(255,255,255,0.6)' },
duck: { neutral: '#f5e6c8', neutralEdge: '#d4c0a0', neutralGlow: 'rgba(245,230,200,0.4)', selected: '#ff6b35', selectedGlow: 'rgba(255,107,53,0.55)' },
_67: { neutral: '#555555', neutralEdge: '#2a2a2a', neutralGlow: 'rgba(120,120,120,0.5)', selected: '#ffd700', selectedGlow: 'rgba(255,215,0,0.8)' },
lava: { neutral: '#8b0000', neutralEdge: '#4a0000', neutralGlow: 'rgba(139,0,0,0.5)', selected: '#fff700', selectedGlow: 'rgba(255,247,0,0.8)' },
space: { neutral: '#2a1a5c', neutralEdge: '#150a30', neutralGlow: 'rgba(42,26,92,0.6)', selected: '#ffe066', selectedGlow: 'rgba(255,224,102,0.8)' },
forest: { neutral: '#8b7355', neutralEdge: '#5a4a35', neutralGlow: 'rgba(139,115,85,0.4)', selected: '#ffd700', selectedGlow: 'rgba(255,215,0,0.7)' },
sunset: { neutral: '#6b5a70', neutralEdge: '#3a2a40', neutralGlow: 'rgba(107,90,112,0.4)', selected: '#ffd700', selectedGlow: 'rgba(255,215,0,0.7)' },
crystal: { neutral: '#a0d8e8', neutralEdge: '#6090a8', neutralGlow: 'rgba(160,216,232,0.5)', selected: '#ffffff', selectedGlow: 'rgba(255,255,255,0.85)' },
puzzle: { neutral: '#8d9aab', neutralEdge: '#5a6778', neutralGlow: 'rgba(141,154,171,0.4)', selected: '#ffd54f', selectedGlow: 'rgba(255,213,79,0.75)' },
coin: { neutral: '#8a6a30', neutralEdge: '#4a3010', neutralGlow: 'rgba(138,106,48,0.5)', selected: '#fff4c0', selectedGlow: 'rgba(255,244,192,0.85)' },
sakura: { neutral: '#f8dce8', neutralEdge: '#e0b8d0', neutralGlow: 'rgba(248,220,232,0.5)', selected: '#ffd080', selectedGlow: 'rgba(255,208,128,0.7)' }
};
const MAX_LEVEL_COLORS = {
player: { color: '#ffd700', edge: '#b89010', glow: 'rgba(255,215,0,0.55)', text: '#1a1a2e', border: '#fff4c0' },
ai: { color: '#e8eef5', edge: '#8090a0', glow: 'rgba(200,220,240,0.6)', text: '#2a3a4a', border: '#ffffff' }
};
/* Хелперы */
function setMsg(t) { if (!hintsEnabled) return; const el = document.getElementById('message'); if (el) el.textContent = t; }
function setStatusMsg(t) { const el = document.getElementById('message'); if (el) el.textContent = t; }
function updateHintsUI() { const lbl = document.getElementById('hintsStateLabel'); const btn = document.getElementById('hintsToggleBtn'); if (lbl) lbl.textContent = hintsEnabled ? 'Включены' : 'Выключены'; if (btn) { btn.textContent = hintsEnabled ? '💬 Вкл.' : '🔇 Выкл.'; btn.classList.toggle('enabled', hintsEnabled); } }
function toggleHints() { hintsEnabled = !hintsEnabled; try { localStorage.setItem('hexagonic_hints_enabled', String(hintsEnabled)); } catch(e){} updateHintsUI(); }
function getHexCenter(col, row) { const x = HEX_SIZE + col * SPACING + (row % 2 === 0 ? 0 : SPACING / 2); const y = HEX_SIZE + row * (HEX_SIZE * 1.5); return { x, y }; }
function getNeighbors(col, row) { const out = []; const dirs = (row % 2 === 0) ? [{dc:-1,dr:0},{dc:1,dr:0},{dc:0,dr:-1},{dc:0,dr:1},{dc:-1,dr:-1},{dc:-1,dr:1}] : [{dc:-1,dr:0},{dc:1,dr:0},{dc:0,dr:-1},{dc:0,dr:1},{dc:1,dr:-1},{dc:1,dr:1}]; for (const d of dirs) { const nc = col + d.dc, nr = row + d.dr; if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && grid[nr][nc] !== null) out.push({ col: nc, row: nr }); } return out; }
function countAttackers(owner) { let n = 0; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const g = grid[r][c]; if (g && g.owner === owner && g.power > 1) n++; } return n; }
function countCells(owner) { let n = 0; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (grid[r][c] && grid[r][c].owner === owner) n++; return n; }
function lightenColor(hex, percent) { const num = parseInt(hex.replace('#',''), 16); const amt = Math.round(2.55 * percent); const R = Math.min(255, (num >> 16) + amt); const G = Math.min(255, ((num >> 8) & 0xFF) + amt); const B = Math.min(255, (num & 0xFF) + amt); return `#${(1 << 24 | R << 16 | G << 8 | B).toString(16).slice(1)}`; }
function darkenColor(hex, percent) { const num = parseInt(hex.replace('#',''), 16); const amt = Math.round(2.55 * percent); const R = Math.max(0, (num >> 16) - amt); const G = Math.max(0, ((num >> 8) & 0xFF) - amt); const B = Math.max(0, (num & 0xFF) - amt); return `#${(1 << 24 | R << 16 | G << 8 | B).toString(16).slice(1)}`; }
function drawHexPath(context, x, y, size) { context.beginPath(); for (let i = 0; i < 6; i++) { const angle = Math.PI / 180 * (60 * i - 30); const px = x + size * Math.cos(angle), py = y + size * Math.sin(angle); if (i === 0) context.moveTo(px, py); else context.lineTo(px, py); } context.closePath(); }
function getPlayerColorPalette() { const pc = PLAYER_COLORS[currentPlayerColor] || PLAYER_COLORS.red; if (currentTheme === 'light') return { color: pc.light, edge: pc.lightEdge, glow: pc.lightGlow }; if (currentTheme === 'duck') return { color: pc.duck, edge: pc.duckEdge, glow: pc.duckGlow }; if (currentTheme === 'retro') return { color: pc.retro, edge: pc.retroEdge, glow: pc.retroGlow }; if (currentTheme === 'nature') return { color: pc.nature, edge: pc.natureEdge, glow: pc.natureGlow }; if (currentTheme === 'sunset') return { color: pc.sunset, edge: pc.sunsetEdge, glow: pc.sunsetGlow }; if (currentTheme === 'puzzletheme') return { color: pc.puzzletheme, edge: pc.puzzlethemeEdge, glow: pc.puzzlethemeGlow }; if (currentTheme === 'coins') return { color: pc.coins, edge: pc.coinsEdge, glow: pc.coinsGlow }; if (currentTheme === 'sakura') return { color: pc.sakura, edge: pc.sakuraEdge, glow: pc.sakuraGlow }; if (currentTheme === 'space') return { color: pc.space, edge: pc.spaceEdge, glow: pc.spaceGlow }; return { color: pc.base, edge: pc.edge, glow: pc.glow }; }
function updatePlayerDot() { const dot = document.getElementById('playerDot'); if (dot) dot.style.background = getPlayerColorPalette().color; for (let i = 1; i <= 3; i++) { const d = document.getElementById('ai' + i + 'Dot'); if (d) d.style.background = AI_PALETTE[i].dot; } }
function renderColorPalette() { const container = document.getElementById('colorPalette'); if (!container) return; container.innerHTML = ''; for (const key in PLAYER_COLORS) { const pc = PLAYER_COLORS[key]; const swatch = document.createElement('div'); swatch.className = 'color-swatch' + (key === currentPlayerColor ? ' active' : ''); swatch.style.background = `linear-gradient(145deg, ${lightenColor(pc.base, 15)}, ${pc.base} 60%, ${pc.edge})`; swatch.innerHTML = `<span class="sw-label">${pc.name}</span>`; swatch.onclick = () => setPlayerColor(key); container.appendChild(swatch); } }
function setPlayerColor(key) { if (!PLAYER_COLORS[key]) return; currentPlayerColor = key; try { localStorage.setItem('hexagonic_player_color', key); } catch(e){} renderColorPalette(); updatePlayerDot(); draw(); }
function loadTheme() {
try {
const s = localStorage.getItem('hexagonic_theme');
if (s && THEMES[s]) currentTheme = s;
const cs = localStorage.getItem('hexagonic_cell_style');
if (cs && (cs === 'auto' || CELL_STYLES[cs])) currentCellStyle = cs;
const he = localStorage.getItem('hexagonic_hints_enabled');
if (he !== null) hintsEnabled = he !== 'false';
const sz = localStorage.getItem('hexagonic_size');
if (sz && SIZE_PRESETS[sz]) currentSize = sz;
const md = localStorage.getItem('hexagonic_mode');
if (md === 'classic' || md === 'elements') currentMode = md;
const ac = localStorage.getItem('hexagonic_ai_count');
if (ac === '1' || ac === '2' || ac === '3') aiCount = parseInt(ac);
const fw = localStorage.getItem('hexagonic_fog');
if (fw !== null) fogOfWar = fw === 'true';
const pc = localStorage.getItem('hexagonic_player_color');
if (pc && PLAYER_COLORS[pc]) currentPlayerColor = pc;
} catch(e) {}
applyTheme(currentTheme);
updateThemeUI(); updateHintsUI(); renderColorPalette();
updatePlayerDot(); updateFogUI(); updateAiCountUI();
}
function applyTheme(theme) {
if (!THEMES[theme]) theme = 'dark';
currentTheme = theme;
document.body.classList.remove('light-theme', 'duck-theme', 'retro-theme', 'nature-theme', 'sunset-theme', 'puzzletheme-theme', 'coins-theme', 'sakura-theme', 'space-theme');
if (theme === 'light') document.body.classList.add('light-theme');
else if (theme === 'duck') document.body.classList.add('duck-theme');
else if (theme === 'retro') document.body.classList.add('retro-theme');
else if (theme === 'nature') document.body.classList.add('nature-theme');
else if (theme === 'sunset') document.body.classList.add('sunset-theme');
else if (theme === 'puzzletheme') document.body.classList.add('puzzletheme-theme');
else if (theme === 'coins') document.body.classList.add('coins-theme');
else if (theme === 'sakura') document.body.classList.add('sakura-theme');
else if (theme === 'space') document.body.classList.add('space-theme');
try { localStorage.setItem('hexagonic_theme', theme); } catch(e){}
// Меняем плейлист под тему
onThemeChangedPlaylist();
updateThemeUI(); updatePlayerDot(); draw();
}
function setTheme(t) { applyTheme(t); }
function setCellStyle(cs) { if (cs !== 'auto' && !CELL_STYLES[cs]) cs = 'auto'; currentCellStyle = cs; try { localStorage.setItem('hexagonic_cell_style', cs); } catch(e){} updateThemeUI(); draw(); }
function updateThemeUI() { document.querySelectorAll('#themeSelector .theme-btn').forEach(b => { b.classList.toggle('active', b.dataset.theme === currentTheme); }); document.querySelectorAll('#cellStyleSelector .theme-btn').forEach(b => { b.classList.toggle('active', b.dataset.cellstyle === currentCellStyle); }); }
function showThemeMenu() { document.getElementById('menuOverlay').classList.remove('active'); document.getElementById('themeModal').classList.add('active'); updateThemeUI(); renderColorPalette(); }
function closeThemeModal() { document.getElementById('themeModal').classList.remove('active'); document.getElementById('menuOverlay').classList.add('active'); showMainMenu(); }
document.getElementById('themeModal').addEventListener('click', function(e) { if (e.target === this) closeThemeModal(); });
function applyBoardSize(size) { const preset = SIZE_PRESETS[size] || SIZE_PRESETS.medium; COLS = preset.cols; ROWS = preset.rows; HEX_SIZE = preset.hex; SPACING = HEX_SIZE * 1.75; W = (COLS - 1) * SPACING + SPACING / 2 + HEX_SIZE * 2; H = (ROWS - 1) * (HEX_SIZE * 1.5) + HEX_SIZE * 2; canvas.width = W; canvas.height = H; }
function selectSize(size) { if (!SIZE_PRESETS[size]) size = 'medium'; currentSize = size; try { localStorage.setItem('hexagonic_size', size); } catch(e){} document.querySelectorAll('#sizeSelector .quick-btn').forEach(b => { b.classList.toggle('active', b.dataset.size === size); }); applyBoardSize(size); initGrid(currentMap); updateUI(); draw(); }
function selectMode(mode) { if (mode !== 'classic' && mode !== 'elements') mode = 'classic'; currentMode = mode; try { localStorage.setItem('hexagonic_mode', mode); } catch(e){} document.querySelectorAll('#modeSelector .quick-btn').forEach(b => { b.classList.toggle('active', b.dataset.mode === mode); }); }
function selectAiCount(n) { if (n !== 1 && n !== 2 && n !== 3) n = 1; aiCount = n; try { localStorage.setItem('hexagonic_ai_count', String(n)); } catch(e){} updateAiCountUI(); }
function updateAiCountUI() { document.querySelectorAll('#aiCountSelector .quick-btn').forEach(b => { b.classList.toggle('active', parseInt(b.dataset.ai) === aiCount); }); }
function toggleFog() { fogOfWar = !fogOfWar; try { localStorage.setItem('hexagonic_fog', String(fogOfWar)); } catch(e){} updateFogUI(); draw(); }
function updateFogUI() { const row = document.getElementById('fogToggleRow'); const check = document.getElementById('fogToggleCheck'); if (row) row.classList.toggle('active', fogOfWar); if (check) check.textContent = fogOfWar ? '✓' : ''; }
function computeVisibleCells() { const visible = new Set(); if (!fogOfWar || gameOver) { for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (grid[r][c]) visible.add(r * COLS + c); return visible; } const queue = []; const dist = new Map(); for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const g = grid[r][c]; if (g && g.owner === PLAYER) { const k = r * COLS + c; dist.set(k, 0); queue.push({ col: c, row: r, d: 0 }); } } while (queue.length) { const cur = queue.shift(); visible.add(cur.row * COLS + cur.col); if (cur.d >= FOG_RADIUS) continue; for (const n of getNeighbors(cur.col, cur.row)) { const k = n.row * COLS + n.col; if (!dist.has(k) || dist.get(k) > cur.d + 1) { dist.set(k, cur.d + 1); queue.push({ col: n.col, row: n.row, d: cur.d + 1 }); } } } return visible; }
function isCellVisible(col, row, visibleSet) { if (!fogOfWar) return true; return visibleSet.has(row * COLS + col); }
/* ===== ФОНОВЫЕ ПАДАЮЩИЕ УТКИ ===== */
let fallingDucks = null;
let fallingDucksFrame = null;
function ensureFallingDucks() {
if (fallingDucks) return fallingDucks;
fallingDucks = [];
for (let i = 0; i < 22; i++) fallingDucks.push(makeFallingDuck(true));
return fallingDucks;
}
function makeFallingDuck(randomY) {
const w = window.innerWidth || 1200;
const h = window.innerHeight || 800;
const side = Math.random() < 0.5 ? 'left' : 'right';
const x = side === 'left' ? Math.random() * (w * 0.25) : w * 0.75 + Math.random() * (w * 0.25);
return {
x: x, y: randomY ? Math.random() * h - h : -40 - Math.random() * 100,
size: 14 + Math.random() * 16, vy: 0.5 + Math.random() * 0.9, vx: (Math.random() - 0.5) * 0.5,
rot: (Math.random() - 0.5) * 0.4, rotSpeed: (Math.random() - 0.5) * 0.012,
phase: Math.random() * Math.PI * 2, phaseSpeed: 0.002 + Math.random() * 0.003,
flap: Math.random() * Math.PI * 2, flapSpeed: 0.06 + Math.random() * 0.07,
color: ['#ffffff', '#fff4d0', '#ffe8b0', '#ffd93d', '#f5e6c8'][Math.floor(Math.random() * 5)]
};
}
function resizeDuckCanvas() { duckCanvas.width = window.innerWidth; duckCanvas.height = window.innerHeight; }
function drawDuckShape(c, d, alpha) {
const s = d.size;
c.save();
c.globalAlpha = alpha;
c.translate(d.x, d.y);
c.rotate(d.rot);
c.fillStyle = d.color;
c.beginPath(); c.ellipse(0, 0, s * 0.7, s * 0.45, 0, 0, Math.PI * 2); c.fill();
c.beginPath(); c.arc(s * 0.65, -s * 0.25, s * 0.32, 0, Math.PI * 2); c.fill();
c.fillStyle = '#ff8c00';
c.beginPath(); c.moveTo(s * 0.9, -s * 0.25); c.lineTo(s * 1.25, -s * 0.15); c.lineTo(s * 0.9, -s * 0.05); c.closePath(); c.fill();
const wingAngle = Math.sin(d.flap) * 0.5;
c.save(); c.translate(-s * 0.1, -s * 0.1); c.rotate(wingAngle);
c.fillStyle = d.color;
c.beginPath(); c.ellipse(-s * 0.2, -s * 0.1, s * 0.45, s * 0.22, -0.4, 0, Math.PI * 2); c.fill();
c.restore();
c.fillStyle = '#1a1a2e';
c.beginPath(); c.arc(s * 0.72, -s * 0.32, s * 0.07, 0, Math.PI * 2); c.fill();
c.restore();
}
function updateFallingDucks() {
if (!document.body.classList.contains('duck-theme')) { fallingDucksFrame = requestAnimationFrame(updateFallingDucks); return; }
const w = duckCanvas.width, h = duckCanvas.height;
duckCtx.clearRect(0, 0, w, h);
const ducks = ensureFallingDucks();
for (const d of ducks) {
d.phase += d.phaseSpeed * 16;
d.y += d.vy * 0.6;
d.x += d.vx * 0.3 + Math.sin(d.phase) * 0.5;
d.rot += d.rotSpeed;
d.flap += d.flapSpeed * 16;
if (d.y > h + 60) { const nd = makeFallingDuck(false); Object.assign(d, nd); }
if (d.x < 8) d.x = 8; if (d.x > w - 8) d.x = w - 8;
drawDuckShape(duckCtx, d, 0.6);
}
fallingDucksFrame = requestAnimationFrame(updateFallingDucks);
}
/* ===== УТКИ НА ИГРОВОМ ПОЛЕ ===== */
let pondDucks = null;
let lilyPads = null;
function ensurePondDucks() {
if (pondDucks) return pondDucks;
pondDucks = []; lilyPads = [];
const padCount = 8;
for (let i = 0; i < padCount; i++) {
let x, y, onCell = false;
if (Math.random() < 0.6) {
let tries = 0;
while (tries < 50) {
const rr = Math.floor(Math.random() * ROWS);
const cc = Math.floor(Math.random() * COLS);
if (grid[rr] && grid[rr][cc]) { const p = getHexCenter(cc, rr); x = p.x; y = p.y; onCell = true; break; }
tries++;
}
if (!onCell) { x = 40 + Math.random() * (W - 80); y = 40 + Math.random() * (H - 80); }
} else { x = 40 + Math.random() * (W - 80); y = 40 + Math.random() * (H - 80); }
const pad = { x, y, r: 18 + Math.random() * 10, rot: Math.random() * Math.PI * 2, bob: Math.random() * Math.PI * 2, bobSpeed: 0.002 + Math.random() * 0.002, flower: Math.random() < 0.4, flowerColor: ['#ffb7c5', '#fff0a0', '#ffe0e8'][Math.floor(Math.random() * 3)], duckOnIt: null };
lilyPads.push(pad);
if (Math.random() < 0.7) {
const d = { size: 12 + Math.random() * 5, bob: Math.random() * Math.PI * 2, bobSpeed: 0.003 + Math.random() * 0.003, color: ['#ffffff', '#fff4d0', '#ffe8b0'][Math.floor(Math.random() * 3)], padRef: pad, onLily: true };
pad.duckOnIt = d;
pondDucks.push(d);
}
}
for (let i = 0; i < 5; i++) {
pondDucks.push({ x: 40 + Math.random() * (W - 80), y: 40 + Math.random() * (H - 80), size: 11 + Math.random() * 5, bob: Math.random() * Math.PI * 2, bobSpeed: 0.003 + Math.random() * 0.003, vx: (Math.random() - 0.5) * 0.25, vy: (Math.random() - 0.5) * 0.1, color: ['#ffffff', '#fff4d0', '#ffe8b0'][Math.floor(Math.random() * 3)], onLily: false, padRef: null });
}
return pondDucks;
}
function drawPondDucks() {
const ducks = ensurePondDucks();
for (const pad of lilyPads) {
pad.bob += pad.bobSpeed * 16;
const bobY = Math.sin(pad.bob) * 1.5;
ctx.save(); ctx.translate(pad.x, pad.y + bobY); ctx.rotate(pad.rot);
ctx.beginPath(); ctx.arc(0, 0, pad.r, 0.35, Math.PI * 2 - 0.35); ctx.lineTo(0, 0); ctx.closePath();
ctx.fillStyle = 'rgba(60,140,70,0.88)'; ctx.fill();
ctx.strokeStyle = 'rgba(30,90,40,0.95)'; ctx.lineWidth = 1.2; ctx.stroke();
ctx.strokeStyle = 'rgba(40,110,50,0.6)'; ctx.lineWidth = 0.8;
for (let v = 0; v < 5; v++) { const a = (v / 5) * Math.PI * 2 + 0.5; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(a) * pad.r * 0.85, Math.sin(a) * pad.r * 0.85); ctx.stroke(); }
if (pad.flower) { ctx.fillStyle = pad.flowerColor; for (let p = 0; p < 5; p++) { const a = (p / 5) * Math.PI * 2; ctx.beginPath(); ctx.ellipse(Math.cos(a) * 4, Math.sin(a) * 4, 3, 2, a, 0, Math.PI * 2); ctx.fill(); } ctx.fillStyle = '#ffe066'; ctx.beginPath(); ctx.arc(0, 0, 2, 0, Math.PI * 2); ctx.fill(); }
ctx.restore();
}
for (const d of ducks) {
d.bob += d.bobSpeed * 16;
let baseX, baseY;
if (d.onLily && d.padRef) {
const bobY = Math.sin(d.padRef.bob) * 1.5;
baseX = d.padRef.x; baseY = d.padRef.y + bobY - 3;
} else {
d.x += d.vx; d.y += d.vy;
if (d.x < 30) { d.x = 30; d.vx *= -1; } if (d.x > W - 30) { d.x = W - 30; d.vx *= -1; }
if (d.y < 30) { d.y = 30; d.vy *= -1; } if (d.y > H - 30) { d.y = H - 30; d.vy *= -1; }
baseX = d.x; baseY = d.y + Math.sin(d.bob) * 1.5;
}
const s = d.size;
ctx.save(); ctx.translate(baseX, baseY);
ctx.globalAlpha = 0.15; ctx.fillStyle = '#1a3a4a';
ctx.beginPath(); ctx.ellipse(0, s * 0.55, s * 0.85, s * 0.22, 0, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = d.color;
ctx.beginPath(); ctx.ellipse(0, 0, s * 0.75, s * 0.5, 0, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = 'rgba(180,160,100,0.5)'; ctx.lineWidth = 0.8; ctx.stroke();
ctx.beginPath(); ctx.moveTo(-s * 0.7, -s * 0.1); ctx.lineTo(-s * 1.0, -s * 0.35); ctx.lineTo(-s * 0.75, -s * 0.15); ctx.closePath();
ctx.fillStyle = d.color; ctx.fill();
ctx.beginPath(); ctx.arc(s * 0.7, -s * 0.35, s * 0.32, 0, Math.PI * 2); ctx.fillStyle = d.color; ctx.fill();
ctx.fillStyle = '#ff8c00';
ctx.beginPath(); ctx.moveTo(s * 0.95, -s * 0.4); ctx.lineTo(s * 1.3, -s * 0.3); ctx.lineTo(s * 0.95, -s * 0.2); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#1a1a2e';
ctx.beginPath(); ctx.arc(s * 0.78, -s * 0.45, s * 0.07, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = lightenColor(d.color, 10);
ctx.beginPath(); ctx.ellipse(-s * 0.1, -s * 0.05, s * 0.4, s * 0.22, -0.3, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = 'rgba(180,160,100,0.4)'; ctx.lineWidth = 0.6; ctx.stroke();
ctx.restore();
}
}
function drawDuckPondBackground() {
const grad = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W/2);
grad.addColorStop(0, '#b8ecf4'); grad.addColorStop(0.5, '#9adce8'); grad.addColorStop(1, '#7ec8d3');
ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1.5;
const now = performance.now();
for (let i = 0; i < 14; i++) {
const wy = (i / 14) * H + Math.sin(now * 0.0008 + i) * 4;
ctx.beginPath();
for (let x = 0; x <= W; x += 25) { const yOff = Math.sin(x * 0.02 + now * 0.0015 + i) * 2.5; if (x === 0) ctx.moveTo(x, wy + yOff); else ctx.lineTo(x, wy + yOff); }
ctx.stroke();
}
}
/* Космос фон */
let spaceStars = null, spaceNebulae = null, spaceShootingStars = null;
function ensureSpaceStars() { if (spaceStars) return spaceStars; spaceStars = []; for (let i = 0; i < 140; i++) spaceStars.push({ x: Math.random() * W, y: Math.random() * H, r: 0.3 + Math.random() * 1.6, phase: Math.random() * Math.PI * 2, phaseSpeed: 0.0008 + Math.random() * 0.002, color: ['#ffffff', '#e0e8ff', '#ffe8c0', '#c0e0ff', '#ffd0e8'][Math.floor(Math.random() * 5)] }); return spaceStars; }
function ensureSpaceNebulae() { if (spaceNebulae) return spaceNebulae; spaceNebulae = []; for (let i = 0; i < 4; i++) spaceNebulae.push({ x: Math.random() * W, y: Math.random() * H, rx: 120 + Math.random() * 200, ry: 80 + Math.random() * 150, rot: Math.random() * Math.PI, phase: Math.random() * Math.PI * 2, phaseSpeed: 0.0004 + Math.random() * 0.0006, color: [['rgba(120,60,220,0.16)', 'rgba(60,20,120,0)'], ['rgba(220,60,140,0.14)', 'rgba(120,20,80,0)'], ['rgba(60,140,240,0.14)', 'rgba(20,60,140,0)'], ['rgba(180,80,240,0.12)', 'rgba(80,20,140,0)']][i % 4] }); return spaceNebulae; }
function ensureShootingStars() { if (spaceShootingStars) return spaceShootingStars; spaceShootingStars = []; for (let i = 0; i < 2; i++) spaceShootingStars.push({ active: false, x: 0, y: 0, len: 0, vx: 0, vy: 0, life: 0, maxLife: 0, next: Math.random() * 400 + 200 }); return spaceShootingStars; }
function drawSpaceBackground() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0e0828'); grad.addColorStop(0.4, '#0a0620'); grad.addColorStop(1, '#05030f');
ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
const nebulae = ensureSpaceNebulae();
for (const n of nebulae) { n.phase += n.phaseSpeed * 16; const a = 0.5 + Math.sin(n.phase) * 0.5; ctx.save(); ctx.translate(n.x, n.y); ctx.rotate(n.rot); const ng = ctx.createRadialGradient(0, 0, 0, 0, 0, Math.max(n.rx, n.ry)); ng.addColorStop(0, n.color[0]); ng.addColorStop(1, n.color[1]); ctx.fillStyle = ng; ctx.globalAlpha = a; ctx.beginPath(); ctx.ellipse(0, 0, n.rx, n.ry, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }
ctx.globalAlpha = 1;
const stars = ensureSpaceStars();
for (const s of stars) { s.phase += s.phaseSpeed * 16; const tw = 0.4 + Math.sin(s.phase) * 0.6; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = s.color; ctx.globalAlpha = tw; ctx.fill(); if (s.r > 1.2) { const gg = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 6); gg.addColorStop(0, s.color); gg.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = gg; ctx.globalAlpha = tw * 0.3; ctx.beginPath(); ctx.arc(s.x, s.y, s.r * 6, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; }
const shooters = ensureShootingStars();
for (const sh of shooters) { if (!sh.active) { sh.next -= 16; if (sh.next <= 0) { sh.active = true; sh.x = Math.random() * W * 0.6 + W * 0.2; sh.y = -20; sh.vx = -2 - Math.random() * 2; sh.vy = 3 + Math.random() * 2; sh.len = 60 + Math.random() * 80; sh.maxLife = 1.2; sh.life = sh.maxLife; sh.next = Math.random() * 600 + 400; } } else { sh.x += sh.vx * 0.5; sh.y += sh.vy * 0.5; sh.life -= 0.016; if (sh.life <= 0 || sh.x < -100 || sh.y > H + 100) { sh.active = false; continue; } const alpha = sh.life / sh.maxLife; const tailX = sh.x - sh.vx * sh.len * 0.04; const tailY = sh.y - sh.vy * sh.len * 0.04; const lg = ctx.createLinearGradient(sh.x, sh.y, tailX, tailY); lg.addColorStop(0, `rgba(255,255,255,${alpha})`); lg.addColorStop(0.5, `rgba(180,220,255,${alpha * 0.5})`); lg.addColorStop(1, 'rgba(180,220,255,0)'); ctx.strokeStyle = lg; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(sh.x, sh.y); ctx.lineTo(tailX, tailY); ctx.stroke(); ctx.beginPath(); ctx.arc(sh.x, sh.y, 2, 0, Math.PI * 2); ctx.fillStyle = `rgba(255,255,255,${alpha})`; ctx.fill(); } }
drawSpacePlanets(performance.now());
}
function drawSpacePlanets(now) {
const p1x = W * 0.78, p1y = H * 0.22, p1r = Math.min(W, H) * 0.09;
const p1Grad = ctx.createRadialGradient(p1x - p1r * 0.4, p1y - p1r * 0.4, p1r * 0.1, p1x, p1y, p1r);
p1Grad.addColorStop(0, '#b06bff'); p1Grad.addColorStop(0.5, '#6a3aa0'); p1Grad.addColorStop(1, '#2a1548');
ctx.beginPath(); ctx.arc(p1x, p1y, p1r, 0, Math.PI * 2); ctx.fillStyle = p1Grad; ctx.fill();
ctx.save(); ctx.translate(p1x, p1y); ctx.rotate(-0.35);
ctx.strokeStyle = 'rgba(180,140,255,0.5)'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.ellipse(0, 0, p1r * 1.8, p1r * 0.5, 0, 0, Math.PI * 2); ctx.stroke();
ctx.strokeStyle = 'rgba(120,80,200,0.35)'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.ellipse(0, 0, p1r * 2.1, p1r * 0.6, 0, 0, Math.PI * 2); ctx.stroke();
ctx.restore();
const p1Glow = ctx.createRadialGradient(p1x, p1y, p1r * 0.8, p1x, p1y, p1r * 2.5);
p1Glow.addColorStop(0, 'rgba(176,107,255,0.25)'); p1Glow.addColorStop(1, 'rgba(176,107,255,0)');
ctx.fillStyle = p1Glow; ctx.beginPath(); ctx.arc(p1x, p1y, p1r * 2.5, 0, Math.PI * 2); ctx.fill();
const p2x = W * 0.12, p2y = H * 0.75, p2r = Math.min(W, H) * 0.045;
const p2Grad = ctx.createRadialGradient(p2x - p2r * 0.4, p2y - p2r * 0.4, p2r * 0.1, p2x, p2y, p2r);
p2Grad.addColorStop(0, '#6bcaff'); p2Grad.addColorStop(0.6, '#2a6aa0'); p2Grad.addColorStop(1, '#0a2a4a');
ctx.beginPath(); ctx.arc(p2x, p2y, p2r, 0, Math.PI * 2); ctx.fillStyle = p2Grad; ctx.fill();
const p2Glow = ctx.createRadialGradient(p2x, p2y, p2r * 0.8, p2x, p2y, p2r * 2.5);
p2Glow.addColorStop(0, 'rgba(107,202,255,0.25)'); p2Glow.addColorStop(1, 'rgba(107,202,255,0)');
ctx.fillStyle = p2Glow; ctx.beginPath(); ctx.arc(p2x, p2y, p2r * 2.5, 0, Math.PI * 2); ctx.fill();
}
/* Эффекты: надпись КРЯ (уменьшена) + перья */
function spawnPopupEffect(col, row, config) {
const pos = getHexCenter(col, row);
const particles = [];
for (let i = 0; i < 14; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 0.8 + Math.random() * 1.8;
particles.push({ x: pos.x, y: pos.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 0.5, size: 2 + Math.random() * 3, rot: Math.random() * Math.PI * 2, rotSpeed: (Math.random() - 0.5) * 0.4, color: config.feathers[Math.floor(Math.random() * config.feathers.length)] });
}
popupEffects.push({ x: pos.x, y: pos.y, text: config.text, life: 1.2, startLife: 1.2, particles, colorStops: config.colorStops, strokeColor: config.strokeColor });
if (!popupFrameId) { popupLastTime = performance.now(); popupFrameId = requestAnimationFrame(popupLoop); }
}
function popupLoop(now) {
const dt = Math.min(50, now - popupLastTime); popupLastTime = now;
let anyAlive = false;
for (const q of popupEffects) {
q.life -= dt / 600; if (q.life <= 0) { q.life = 0; continue; } anyAlive = true;
for (const f of q.particles) { f.x += f.vx * dt * 0.06; f.y += f.vy * dt * 0.06; f.vy += 0.006 * dt * 0.06; f.vx *= 0.985; f.vy *= 0.985; f.rot += f.rotSpeed; }
}
popupEffects = popupEffects.filter(q => q.life > 0);
draw();
if (anyAlive) popupFrameId = requestAnimationFrame(popupLoop);
else { popupFrameId = null; draw(); }
}
function drawPopupEffects() {
for (const q of popupEffects) {
const t = q.life / q.startLife;
const alpha = Math.min(1, q.life * 1.5);
const scale = 0.7 + (1 - t) * 0.6;
for (const f of q.particles) {
ctx.save(); ctx.globalAlpha = alpha * 0.95;
ctx.translate(f.x, f.y); ctx.rotate(f.rot);
ctx.fillStyle = f.color;
ctx.beginPath(); ctx.ellipse(0, 0, f.size * 1.3, f.size * 0.45, 0, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = 'rgba(180,150,80,0.5)'; ctx.lineWidth = 0.4; ctx.stroke();
ctx.restore();
}
ctx.save(); ctx.globalAlpha = alpha;
ctx.translate(q.x, q.y - (1 - t) * 30);
ctx.scale(scale, scale);
// НАДПИСЬ УМЕНЬШЕНА: было 26px → стало 10px (в 2.6 раза)
ctx.font = 'bold 10px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.lineWidth = 2;
ctx.strokeStyle = q.strokeColor;
ctx.strokeText(q.text, 0, 0);
const grad = ctx.createLinearGradient(0, -6, 0, 6);
grad.addColorStop(0, q.colorStops[0]); grad.addColorStop(0.5, q.colorStops[1]); grad.addColorStop(1, q.colorStops[2] || q.colorStops[1]);
ctx.fillStyle = grad;
ctx.fillText(q.text, 0, 0);
ctx.restore();
}
}
function trySpawnQuack(col, row) {
if (Math.random() > 0.20) return;
spawnPopupEffect(col, row, { text: 'КРЯ!', colorStops: ['#fff8d0', '#ffd93d', '#ff8c1a'], strokeColor: 'rgba(120,60,0,0.95)', feathers: ['#ffffff', '#fff4d0', '#ffe8b0', '#ffd93d', '#ffb84d', '#f5e6c8'] });
}
function trySpawnCoin(col, row) {
if (Math.random() > 0.20) return;
spawnPopupEffect(col, row, { text: '+💰', colorStops: ['#fff8d0', '#ffd93d', '#c8742a'], strokeColor: 'rgba(80,40,0,0.9)', feathers: ['#fff8d0', '#ffd93d', '#ffb300', '#c8742a'] });
}
/* ===== АУДИО ===== */
let audioCtx = null, soundGain = null, musicGain = null;
let soundEnabled = true, musicEnabled = true;
let soundVolume = 1.0, musicVolume = 0.35;
/* ===== МУЗЫКАЛЬНЫЕ ТРЕКИ ===== */
// 5 общих + 3 космических
const MUSIC_TRACKS = [
// --- ОБЩИЕ (5) ---
{
id: 'calm-ambient', name: '🌊 Спокойный эмбиент', theme: 'general',
// Медленная мягкая мелодия, длинные ноты
bpm: 60, baseFreq: 220,
melody: [0, 4, 7, 12, 7, 4, 0, -5, 0, 4, 7, 12, 16, 12, 7, 4],
chords: [[0, 4, 7], [-5, 0, 4], [-3, 2, 5], [0, 4, 7]],
waveType: 'sine', volume: 0.35
},
{
id: 'medieval', name: '🏰 Средневековая', theme: 'general',
// Фолк-мотив в миноре
bpm: 90, baseFreq: 262,
melody: [0, 3, 5, 7, 5, 3, 0, -2, 0, 3, 5, 8, 7, 5, 3, 0],
chords: [[0, 3, 7], [-2, 3, 5], [-4, 0, 3], [0, 3, 7]],
waveType: 'triangle', volume: 0.3
},
{
id: 'oriental', name: '🏮 Восточная', theme: 'general',
// Пентатоника, восточный колорит
bpm: 80, baseFreq: 294,
melody: [0, 2, 5, 7, 9, 7, 5, 2, 0, -3, 0, 2, 5, 9, 7, 5],
chords: [[0, 5, 9], [-3, 2, 7], [0, 5, 9], [-5, 0, 5]],
waveType: 'triangle', volume: 0.32
},
{
id: 'epic-battle', name: '⚔️ Эпическая битва', theme: 'general',
// Быстрая эпичная
bpm: 120, baseFreq: 196,
melody: [0, 0, 3, 7, 10, 7, 3, 0, 5, 5, 7, 12, 10, 7, 3, 0],
chords: [[0, 3, 7], [5, 8, 12], [-2, 3, 7], [0, 3, 7]],
waveType: 'sawtooth', volume: 0.25
},
{
id: 'mystic', name: '🔮 Мистическая', theme: 'general',
// Загадочная, с широкими интервалами
bpm: 70, baseFreq: 233,
melody: [0, 7, 10, 14, 12, 7, 5, 0, -2, 5, 10, 12, 10, 5, 0, -5],
chords: [[0, 5, 10], [-2, 5, 8], [-5, 0, 7], [0, 5, 10]],
waveType: 'sine', volume: 0.33
},
// --- КОСМИЧЕСКИЕ (3, только для темы Космос) ---
{
id: 'space-station', name: '🛰️ Космическая станция', theme: 'space',
// Ритмичный синтезаторный трек — «работа систем»
bpm: 110, baseFreq: 175,
melody: [0, 0, 5, 5, 7, 7, 5, 3, 0, 0, 5, 5, 8, 7, 5, 3],
chords: [[0, 5, 8], [0, 5, 8], [-2, 3, 7], [0, 5, 8]],
waveType: 'square', volume: 0.22
},
{
id: 'star-voyage', name: '⭐ Звёздный путь', theme: 'space',
// Эпичный медленный космический полёт
bpm: 65, baseFreq: 130,
melody: [0, 7, 12, 19, 17, 12, 10, 7, 5, 0, 7, 12, 15, 12, 7, 5],
chords: [[0, 7, 12], [-3, 5, 10], [-5, 2, 7], [0, 7, 12]],
waveType: 'sine', volume: 0.38
},
{
id: 'nebula-drift', name: '🌌 Дрейф в туманности', theme: 'space',
// Медитативный эмбиент — плывущие звуки
bpm: 50, baseFreq: 110,
melody: [0, 0, 0, 3, 0, 0, 5, 3, 0, 0, 0, 7, 5, 3, 0, 0],
chords: [[0, 5, 10], [0, 5, 10], [-2, 3, 8], [0, 5, 10]],
waveType: 'sine', volume: 0.4
}
];
let currentTrackIndex = 0;
let musicScheduler = null;
let musicState = {
step: 0,
nextNoteTime: 0,
running: false
};
function getTracksForTheme(theme) {
// Возвращает список треков для темы: если есть специфичные — только они + общие
const specific = MUSIC_TRACKS.filter(t => t.theme === theme);
const general = MUSIC_TRACKS.filter(t => t.theme === 'general');
// Для темы space: 3 космических + 5 общих = 8 треков
// Для остальных: 5 общих
return specific.length > 0 ? [...specific, ...general] : general;
}
function getCurrentTrack() {
const tracks = getTracksForTheme(currentTheme);
if (currentTrackIndex >= tracks.length) currentTrackIndex = 0;
return tracks[currentTrackIndex];
}
function updateTrackNameUI() {
const el = document.getElementById('trackNameValue');
if (el) {
const track = getCurrentTrack();
el.textContent = track ? track.name : '—';
}
}
function nextTrack() {
const tracks = getTracksForTheme(currentTheme);
currentTrackIndex = (currentTrackIndex + 1) % tracks.length;
restartMusic();
updateTrackNameUI();
}
function prevTrack() {
const tracks = getTracksForTheme(currentTheme);
currentTrackIndex = (currentTrackIndex - 1 + tracks.length) % tracks.length;
restartMusic();
updateTrackNameUI();
}
function onThemeChangedPlaylist() {
// При смене темы сбрасываем индекс и перезапускаем
currentTrackIndex = 0;
restartMusic();
updateTrackNameUI();
}
function restartMusic() {
if (musicScheduler) { clearTimeout(musicScheduler); musicScheduler = null; }
musicState.running = false;
if (musicEnabled && audioCtx) {
setTimeout(() => startMusic(), 50);
}
}
function startMusic() {
if (!musicEnabled || !ensureAudio()) return;
if (musicState.running) return;
const track = getCurrentTrack();
if (!track) return;
musicState.running = true;
musicState.step = 0;
musicState.nextNoteTime = audioCtx.currentTime + 0.05;
scheduleMusic();
}
function scheduleMusic() {
if (!musicState.running || !audioCtx || !musicEnabled) return;
const track = getCurrentTrack();
if (!track) return;
const beatDuration = 60 / track.bpm / 2; // восьмые
while (musicState.nextNoteTime < audioCtx.currentTime + 0.15) {
const step = musicState.step % track.melody.length;
const noteOffset = track.melody[step];
// Мелодия
playNote(track.baseFreq * Math.pow(2, noteOffset / 12), musicState.nextNoteTime, beatDuration * 1.6, track.waveType, track.volume);
// Басовая нота каждые 4 шага
if (step % 4 === 0) {
const chordIdx = Math.floor(step / 4) % track.chords.length;
const chord = track.chords[chordIdx];
playNote(track.baseFreq / 2 * Math.pow(2, chord[0] / 12), musicState.nextNoteTime, beatDuration * 3.5, 'sine', track.volume * 0.7);
}
// Гармония каждые 2 шага
if (step % 2 === 0) {
const chordIdx = Math.floor(step / 4) % track.chords.length;
const chord = track.chords[chordIdx];
const harm = chord[1 % chord.length];
playNote(track.baseFreq * Math.pow(2, harm / 12), musicState.nextNoteTime, beatDuration * 1.2, 'triangle', track.volume * 0.4);
}
musicState.nextNoteTime += beatDuration;
musicState.step++;
if (musicState.step >= track.melody.length * 4) {
// Зацикливаем
musicState.step = 0;
}
}
musicScheduler = setTimeout(scheduleMusic, 50);
}
function playNote(freq, time, duration, waveType, vol) {
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = waveType || 'sine';
osc.frequency.setValueAtTime(freq, time);
const attack = 0.05;
const release = 0.3;
gain.gain.setValueAtTime(0.0001, time);
gain.gain.exponentialRampToValueAtTime(Math.max(0.001, vol), time + attack);
gain.gain.setValueAtTime(Math.max(0.001, vol), time + duration - release);
gain.gain.exponentialRampToValueAtTime(0.0001, time + duration);
osc.connect(gain);
gain.connect(musicGain || audioCtx.destination);
osc.start(time);
osc.stop(time + duration + 0.05);
} catch(e) {}
}
function stopMusic() {
musicState.running = false;
if (musicScheduler) { clearTimeout(musicScheduler); musicScheduler = null; }
}
/* Стандартные аудио-настройки */
function loadAudioSettings() {
try {
const se = localStorage.getItem('hexagonic_sound_enabled');
const me = localStorage.getItem('hexagonic_music_enabled');
const sv = localStorage.getItem('hexagonic_sound_volume');
const mv = localStorage.getItem('hexagonic_music_volume');
if (se !== null) soundEnabled = se !== 'false';
if (me !== null) musicEnabled = me !== 'false';
if (sv !== null) soundVolume = Math.max(0, Math.min(1, Number(sv)));
if (mv !== null) musicVolume = Math.max(0, Math.min(1, Number(mv)));
} catch(e) {}
const ss = document.getElementById('soundVolume');
const ms = document.getElementById('musicVolume');
if (ss) ss.value = Math.round(soundVolume * 100);
if (ms) ms.value = Math.round(musicVolume * 100);
updateAudioUI();
updateTrackNameUI();
}
function updateAudioUI() {
const sv = document.getElementById('soundVolumeValue');
const mv = document.getElementById('musicVolumeValue');
const sb = document.getElementById('soundToggleBtn');
const mb = document.getElementById('musicToggleBtn');
if (sv) sv.textContent = Math.round(soundVolume * 100) + '%';
if (mv) mv.textContent = Math.round(musicVolume * 100) + '%';
if (sb) { sb.textContent = soundEnabled ? '🔊 Вкл.' : '🔇 Выкл.'; sb.classList.toggle('enabled', soundEnabled); }
if (mb) { mb.textContent = musicEnabled ? '🎵 Вкл.' : '🔇 Выкл.'; mb.classList.toggle('enabled', musicEnabled); }
}
function ensureAudio() {
try {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return false;
audioCtx = new AC();
soundGain = audioCtx.createGain();
musicGain = audioCtx.createGain();
soundGain.gain.value = soundVolume;
musicGain.gain.value = musicEnabled ? musicVolume : 0;
soundGain.connect(audioCtx.destination);
musicGain.connect(audioCtx.destination);
}
if (audioCtx.state === 'suspended') audioCtx.resume();
return true;
} catch(e) { return false; }
}
function unlockAudio() { ensureAudio(); if (musicEnabled) startMusic(); }
function setSoundVolume(v) { soundVolume = Math.max(0, Math.min(1, Number(v)/100)); if (soundGain && audioCtx) soundGain.gain.value = soundVolume; try { localStorage.setItem('hexagonic_sound_volume', String(soundVolume)); } catch(e){} updateAudioUI(); }
function setMusicVolume(v) { musicVolume = Math.max(0, Math.min(1, Number(v)/100)); if (musicGain && audioCtx) musicGain.gain.value = musicEnabled ? musicVolume : 0; try { localStorage.setItem('hexagonic_music_volume', String(musicVolume)); } catch(e){} updateAudioUI(); }
function toggleSound() { soundEnabled = !soundEnabled; try { localStorage.setItem('hexagonic_sound_enabled', String(soundEnabled)); } catch(e){} if (soundEnabled) { unlockAudio(); playSound('click'); } updateAudioUI(); }
function toggleMusic() {
musicEnabled = !musicEnabled;
try { localStorage.setItem('hexagonic_music_enabled', String(musicEnabled)); } catch(e){}
if (musicGain && audioCtx) musicGain.gain.value = musicEnabled ? musicVolume : 0;
if (musicEnabled) { unlockAudio(); startMusic(); }
else stopMusic();
updateAudioUI();
}
function playSound(type) {
if (!soundEnabled || !ensureAudio()) return;
const now = audioCtx.currentTime;
const cfg = {
click: [520, 0.045, 'sine', 0.035],
attack: [150, 0.11, 'triangle', 0.055],
capture: [280, 0.16, 'sine', 0.06],
upgrade: [620, 0.14, 'triangle', 0.045],
turn: [380, 0.12, 'sine', 0.04],
tie: [440, 0.20, 'sawtooth', 0.05]
}[type] || [520, 0.045, 'sine', 0.035];
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = cfg[2];
osc.frequency.setValueAtTime(cfg[0], now);
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(cfg[3], now + 0.008);
gain.gain.exponentialRampToValueAtTime(0.0001, now + cfg[1]);
osc.connect(gain);
gain.connect(soundGain || audioCtx.destination);
osc.start(now);
osc.stop(now + cfg[1] + 0.02);
}
/* Карты */
function getRandomStartPositions(count) { const pool = []; for (let r = 1; r < ROWS - 1; r += 2) { for (let c = 1; c < COLS - 1; c += 2) pool.push({ col: c, row: r }); } const result = []; const shuffled = pool.sort(() => Math.random() - 0.5); for (const p of shuffled) { let ok = true; for (const r of result) { const d = Math.abs(r.col - p.col) + Math.abs(r.row - p.row); if (d < Math.max(3, Math.floor(COLS / (count + 1)))) { ok = false; break; } } if (ok) result.push(p); if (result.length >= count) break; } while (result.length < count) result.push({ col: 2 + result.length * 3, row: 2 }); return result; }
function ensureStartTiles(map, positions) { for (const p of positions) { if (map[p.row] && map[p.row][p.col] !== true) map[p.row][p.col] = true; } }
function generateMapIslands() { const map = []; for (let r = 0; r < ROWS; r++) { map[r] = []; for (let c = 0; c < COLS; c++) map[r][c] = true; } const d = 0.06 + Math.random() * 0.08; for (let r = 1; r < ROWS-1; r++) for (let c = 1; c < COLS-1; c++) if (Math.random() < d) map[r][c] = false; const starts = getRandomStartPositions(aiCount + 1); ensureStartTiles(map, starts); return { map, starts }; }
function generateMapValley() { const map = []; for (let r = 0; r < ROWS; r++) { map[r] = []; for (let c = 0; c < COLS; c++) map[r][c] = true; } const rw = 1 + Math.floor(Math.random() * 2); const midC = Math.floor(COLS/2); for (let r = 2; r < ROWS-2; r++) { const cen = midC + Math.floor(Math.sin(r * 0.5) * 1.5); const w = rw + Math.floor(Math.random() * 2); for (let c = cen - w; c <= cen + w; c++) if (c > 1 && c < COLS-2) map[r][c] = false; } const starts = getRandomStartPositions(aiCount + 1); ensureStartTiles(map, starts); return { map, starts }; }
function generateMapDesert() { const map = []; for (let r = 0; r < ROWS; r++) { map[r] = []; for (let c = 0; c < COLS; c++) map[r][c] = true; } const n = Math.floor(ROWS * COLS * 0.1); for (let i = 0; i < n; i++) { const r = 1 + Math.floor(Math.random() * (ROWS-2)); const c = 1 + Math.floor(Math.random() * (COLS-2)); const cs = 1 + Math.floor(Math.random() * 3); for (let j = 0; j < cs && r+j < ROWS-1 && c+j < COLS-1; j++) map[r+j][c+j] = false; } const starts = getRandomStartPositions(aiCount + 1); ensureStartTiles(map, starts); return { map, starts }; }
function generateMapHighland() { const map = []; for (let r = 0; r < ROWS; r++) { map[r] = []; for (let c = 0; c < COLS; c++) map[r][c] = true; } const cr = Math.floor(ROWS/2), cc = Math.floor(COLS/2); const rad = 2 + Math.floor(Math.random() * 2); for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const dist = Math.sqrt((r-cr)**2 + (c-cc)**2); if (dist > rad - 0.5 && dist < rad + 1.5) { const ang = Math.atan2(r-cr, c-cc); const isGate = Math.abs(Math.sin(ang*2)) < 0.4; if (!isGate) map[r][c] = false; } } const starts = getRandomStartPositions(aiCount + 1); ensureStartTiles(map, starts); return { map, starts }; }
function initGrid(mapType) {
let data;
if (mapType === 1) data = generateMapIslands();
else if (mapType === 2) data = generateMapValley();
else if (mapType === 3) data = generateMapDesert();
else if (mapType === 4) data = generateMapHighland();
else data = generateMapIslands();
grid = [];
for (let r = 0; r < ROWS; r++) { grid[r] = []; for (let c = 0; c < COLS; c++) grid[r][c] = data.map[r][c] ? { owner: null, power: 1, type: null } : null; }
const starts = data.starts;
for (let i = 0; i < starts.length; i++) { const p = starts[i]; if (!grid[p.row][p.col]) grid[p.row][p.col] = { owner: null, power: 1, type: null }; grid[p.row][p.col].owner = (i === 0) ? PLAYER : i; grid[p.row][p.col].power = 2; }
if (currentMode === 'elements') placeSpecialCells(starts);
pondDucks = null; lilyPads = null;
}
function placeSpecialCells(starts) {
const types = ['crystal', 'fortress', 'energy', 'volcano', 'bonus'];
const weights = [3, 2, 3, 2, 2];
const totalW = weights.reduce((a,b) => a+b, 0);
const freeCells = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
if (!grid[r][c]) continue;
if (grid[r][c].owner !== null) continue;
let nearStart = false;
for (const s of starts) { const d = Math.abs(r - s.row) + Math.abs(c - s.col); if (d < 2) { nearStart = true; break; } }
if (nearStart) continue;
freeCells.push({ col: c, row: r });
}
const count = Math.max(3, Math.floor(freeCells.length * 0.14));
for (let i = 0; i < count && freeCells.length; i++) {
const idx = Math.floor(Math.random() * freeCells.length);
const cell = freeCells.splice(idx, 1)[0];
let roll = Math.random() * totalW;
let type = types[0];
for (let j = 0; j < types.length; j++) { roll -= weights[j]; if (roll <= 0) { type = types[j]; break; } }
grid[cell.row][cell.col].type = type;
}
}
function performAttack(fromCol, fromRow, toCol, toRow) {
const atk = grid[fromRow][fromCol]; const def = grid[toRow][toCol];
if (!atk || !def) return { ok: false };
if (atk.owner === def.owner) return { ok: false };
if (atk.power <= 1) return { ok: false };
const isNeighbor = getNeighbors(fromCol, fromRow).some(n => n.col === toCol && n.row === toRow);
if (!isNeighbor) return { ok: false };
let defEffective = def.power;
if (def.type === 'fortress') defEffective += 2;
const attackerOwner = atk.owner;
const atkPower = atk.power;
stats.attacks++;
let captured = false, tie = false;
if (atkPower > defEffective) captured = true;
else if (atkPower === defEffective) { if (Math.random() < 0.5) captured = true; else tie = true; }
if (captured) {
const diff = Math.max(1, atkPower - defEffective);
def.owner = attackerOwner;
def.power = Math.min(diff, MAX_POWER);
atk.power = 1;
if (atkPower === defEffective) def.power = 1;
if (def.type === 'crystal') { def.power = Math.min(def.power + 3, MAX_POWER); def.type = null; }
else if (def.type === 'energy') { if (attackerOwner === PLAYER) { upgradePoints += 3; totalUpgrades += 3; } def.type = null; }
else if (def.type === 'bonus') { const r2 = Math.random(); if (r2 < 0.33) def.power = Math.min(def.power + 4, MAX_POWER); else if (r2 < 0.66) { if (attackerOwner === PLAYER) { upgradePoints += 2; totalUpgrades += 2; } } else { for (const n of getNeighbors(toCol, toRow)) { const nc = grid[n.row][n.col]; if (nc && nc.owner === attackerOwner) nc.power = Math.min(nc.power + 1, MAX_POWER); } } def.type = null; }
else if (def.type === 'volcano') { def.type = null; def.power = 1; def.owner = null; for (const n of getNeighbors(toCol, toRow)) { const nc = grid[n.row][n.col]; if (nc && nc.power > 1) nc.power--; } }
stats.captures++;
playSound('capture');
const isDuckTheme = currentTheme === 'duck' || currentCellStyle === 'duck';
if (isDuckTheme) trySpawnQuack(toCol, toRow);
else if (currentCellStyle === 'coin' || (currentCellStyle === 'auto' && currentTheme === 'coins')) trySpawnCoin(toCol, toRow);
return { ok: true, captured: true };
} else if (tie) { atk.power = 1; def.power = 1; playSound('tie'); return { ok: true, captured: false }; }
else { atk.power = 1; def.power = Math.max(1, Math.min(Math.abs(atkPower - defEffective), MAX_POWER)); playSound('attack'); return { ok: true, captured: false }; }
}
function applyVolcanoDamage() { if (currentMode !== 'elements') return; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const g = grid[r][c]; if (g && g.type === 'volcano' && g.owner !== null && g.power > 1) { g.power--; if (g.power <= 1) grid[r][c] = null; } } }
function startPlayerTurn() {
if (gameOver) return;
if (playerTurnActive) return;
playerTurnActive = true;
currentPlayer = PLAYER;
const count = countCells(PLAYER);
totalUpgrades = count;
upgradePoints = count;
usedUpgrades = 0;
selectedCell = null;
turnPhase = 'idle';
maxModeActive = false;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) { maxBtn.classList.remove('btn-gold'); maxBtn.textContent = '🔱 MAX'; maxBtn.disabled = true; }
document.getElementById('distributeBtn').disabled = true;
document.getElementById('skipBtn').disabled = false;
const ti = document.getElementById('turnIndicator');
ti.textContent = `🔴 Ваш ход (${turnNumber})`;
ti.style.color = getPlayerColorPalette().color;
let extra = fogOfWar ? ' 🌫️ Туман' : '';
setMsg(`⚡ У вас ${upgradePoints} очков. Нажмите "Пропустить" чтобы начать прокачку${extra}`);
updateUI(); draw();
}
function endPlayerTurn() {
if (gameOver) return;
playerTurnActive = false;
turnPhase = 'idle';
selectedCell = null;
document.getElementById('distributeBtn').disabled = true;
document.getElementById('skipBtn').disabled = true;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) { maxBtn.disabled = true; if (maxModeActive) { maxModeActive = false; maxBtn.classList.remove('btn-gold'); maxBtn.textContent = '🔱 MAX'; } }
if (checkGameOver()) return;
advanceTurn();
}
function skipTurn() {
if (gameOver) { setMsg('⏳ Игра окончена!'); return; }
if (aiThinking) { setMsg('⏳ Противник думает...'); return; }
if (currentPlayer !== PLAYER) { setMsg('⏳ Не ваш ход!'); return; }
if (turnPhase === 'idle') {
if (upgradePoints > 0) {
turnPhase = 'upgrade';
document.getElementById('distributeBtn').disabled = false;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) maxBtn.disabled = false;
document.getElementById('skipBtn').disabled = true;
selectedCell = null;
setMsg(`📤 Режим прокачки! Кликайте по своим клеткам (${upgradePoints})`);
draw();
} else { turnPhase = 'done'; playSound('turn'); endPlayerTurn(); }
return;
}
if (turnPhase === 'upgrade') {
if (upgradePoints > 0) { setMsg(`⚠ У вас ещё ${upgradePoints} очков!`); return; }
turnPhase = 'done';
document.getElementById('skipBtn').disabled = false;
setMsg('⚔ Прокачка завершена! Атакуйте или нажмите «Пропустить»');
draw();
return;
}
if (turnPhase === 'done') { playSound('turn'); endPlayerTurn(); return; }
}
function upgradeCell(col, row) {
if (!grid[row][col]) return false;
if (grid[row][col].owner !== PLAYER) return false;
if (gameOver || aiThinking) return false;
if (turnPhase !== 'upgrade') { setMsg('⚠️ Сначала нажмите "Пропустить"!'); return false; }
if (upgradePoints <= 0) return false;
if (grid[row][col].power >= MAX_POWER) { setMsg('⚠️ Уже максимальная сила!'); return false; }
let spend = 1;
if (maxModeActive) spend = Math.min(upgradePoints, MAX_POWER - grid[row][col].power);
grid[row][col].power = Math.min(grid[row][col].power + spend, MAX_POWER);
upgradePoints -= spend;
usedUpgrades += spend;
stats.upgrades += spend;
playSound('upgrade');
setMsg(`⚡ Прокачка +${spend}! Сила: ${grid[row][col].power} (осталось ${upgradePoints})`);
if (upgradePoints === 0) {
maxModeActive = false;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) { maxBtn.classList.remove('btn-gold'); maxBtn.textContent = '🔱 MAX'; maxBtn.disabled = true; }
document.getElementById('distributeBtn').disabled = true;
document.getElementById('skipBtn').disabled = false;
}
updateUI(); draw();
return true;
}
function distributePoints() {
if (currentPlayer !== PLAYER || gameOver || aiThinking) { setMsg('⏳ Нельзя сейчас!'); return; }
if (upgradePoints <= 0) { setMsg('❌ Нет очков!'); return; }
if (turnPhase !== 'upgrade') { setMsg('⚠️ Сначала нажмите "Пропустить"!'); return; }
maxModeActive = false;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) { maxBtn.classList.remove('btn-gold'); maxBtn.textContent = '🔱 MAX'; }
const cells = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (grid[r][c] && grid[r][c].owner === PLAYER) cells.push({ col: c, row: r });
if (!cells.length) { setMsg('❌ Нет клеток!'); return; }
cells.sort((a, b) => grid[a.row][a.col].power - grid[b.row][b.col].power);
let upgraded = 0, attempts = 0;
while (upgradePoints > 0 && attempts < 200) {
attempts++;
let ok = false;
for (const cell of cells) {
if (upgradePoints <= 0) break;
const g = grid[cell.row][cell.col];
if (g && g.owner === PLAYER && g.power < MAX_POWER) { g.power++; upgradePoints--; upgraded++; usedUpgrades++; stats.upgrades++; ok = true; }
}
if (!ok) break;
}
setMsg(`✅ Распределено +${upgraded}. Осталось ${upgradePoints}`);
if (upgradePoints === 0) {
document.getElementById('distributeBtn').disabled = true;
if (maxBtn) maxBtn.disabled = true;
document.getElementById('skipBtn').disabled = false;
}
updateUI(); draw();
}
function toggleMaxMode() {
if (gameOver || aiThinking || currentPlayer !== PLAYER || turnPhase !== 'upgrade') return;
if (upgradePoints <= 0) return;
maxModeActive = !maxModeActive;
const btn = document.getElementById('maxBtn');
if (!btn) return;
if (maxModeActive) { btn.classList.add('btn-gold'); btn.textContent = '✓ MAX'; }
else { btn.classList.remove('btn-gold'); btn.textContent = '🔱 MAX'; }
}
function advanceTurn() {
if (gameOver) return;
currentTurnIndex++;
if (currentTurnIndex >= turnOrder.length) currentTurnIndex = 0;
const nextOwner = turnOrder[currentTurnIndex];
if (nextOwner === PLAYER) { turnNumber++; setTimeout(startPlayerTurn, 250); }
else startAITurnFor(nextOwner);
}
function startAITurnFor(ownerId) {
if (gameOver) return;
aiThinking = true;
currentPlayer = ownerId;
const ti = document.getElementById('turnIndicator');
ti.textContent = `🟢 ${AI_PALETTE[ownerId].name} (${turnNumber})`;
ti.style.color = AI_PALETTE[ownerId].base;
document.getElementById('skipBtn').disabled = true;
setTimeout(() => aiTurnFor(ownerId), 400);
}
function aiTurnFor(ownerId) {
if (gameOver) { aiThinking = false; return; }
let aiCells = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (grid[r][c] && grid[r][c].owner === ownerId) aiCells.push({ col: c, row: r });
let pts = aiCells.length;
let attempts = 0;
while (pts > 0 && attempts < 300) {
attempts++;
let ok = false;
aiCells.sort((a, b) => grid[a.row][a.col].power - grid[b.row][b.col].power);
for (const cell of aiCells) {
if (pts <= 0) break;
const g = grid[cell.row][cell.col];
if (g && g.owner === ownerId && g.power < MAX_POWER) { g.power++; pts--; ok = true; }
}
if (!ok) break;
}
const playerPriority = DIFF_PLAYER_PRIO[difficulty] || 0.6;
const skipChance = DIFF_SKIP_CHANCE[difficulty] || 0;
const initialAttackers = new Set();
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const g = grid[r][c]; if (g && g.owner === ownerId && g.power > 1) initialAttackers.add(r * COLS + c); }
const readyAttackers = initialAttackers.size;
const maxAttacks = Math.min(DIFF_MAX_ATTACKS[difficulty] || 1, readyAttackers);
let attacks = 0;
function findBestAttack() {
let best = null, bestScore = -Infinity;
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const g = grid[r][c];
if (!g || g.owner !== ownerId || g.power <= 1) continue;
if (!initialAttackers.has(r * COLS + c)) continue;
const neighbors = getNeighbors(c, r);
for (const n of neighbors) {
const t = grid[n.row][n.col];
if (!t || t.owner === ownerId) continue;
let defEff = t.power;
if (t.type === 'fortress') defEff += 2;
if (g.power < defEff) continue;
let score = 0;
const diff = g.power - defEff;
if (t.owner === PLAYER) score += 25 * playerPriority + t.power * 1.5;
else score += 10 * (1 - playerPriority) + t.power * 0.5;
score += 40 + Math.min(diff, 10) * 1.5 + Math.random() * 4;
if (score > bestScore) { bestScore = score; best = { from: { col: c, row: r }, to: { col: n.col, row: n.row } }; }
}
}
return best;
}
for (let round = 0; round < maxAttacks; round++) {
if (skipChance > 0 && Math.random() < skipChance / (round + 1)) continue;
const best = findBestAttack();
if (!best) break;
const res = performAttack(best.from.col, best.from.row, best.to.col, best.to.row);
if (res.ok) attacks++; else break;
}
if (attacks === 0) setMsg(`🤖 ${AI_PALETTE[ownerId].name} пропускает атаку`);
else setMsg(`🤖 ${AI_PALETTE[ownerId].name} совершил ${attacks} атак!`);
aiThinking = false;
applyVolcanoDamage();
if (checkGameOver()) return;
advanceTurn();
}
function checkGameOver() {
let p = countCells(PLAYER);
let aiAlive = 0;
for (let i = 1; i <= aiCount; i++) if (countCells(i) > 0) aiAlive++;
if (p === 0 || aiAlive === 0) {
gameOver = true;
document.getElementById('turnIndicator').textContent = 'Игра завершена';
document.getElementById('distributeBtn').disabled = true;
document.getElementById('skipBtn').disabled = true;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) maxBtn.disabled = true;
updateUI(); draw();
setTimeout(() => showResultScreen(p), 500);
return true;
}
return false;
}
function showResultScreen(p) {
const overlay = document.getElementById('resultOverlay');
const icon = document.getElementById('resultIcon');
const title = document.getElementById('resultTitle');
const subtitle = document.getElementById('resultSubtitle');
let aiTotal = 0;
for (let i = 1; i <= aiCount; i++) aiTotal += countCells(i);
if (p === 0) { icon.textContent = '💀'; title.textContent = 'ПОРАЖЕНИЕ'; title.className = 'result-title lose'; subtitle.textContent = 'Все ваши клетки захвачены'; }
else { icon.textContent = '🏆'; title.textContent = 'ПОБЕДА!'; title.className = 'result-title win'; subtitle.textContent = `Вы победили ${aiCount} ${aiCount === 1 ? 'противника' : 'противников'}!`; }
document.getElementById('rsPlayerCells').textContent = p;
document.getElementById('rsAiCells').textContent = aiTotal;
document.getElementById('rsTurns').textContent = turnNumber;
document.getElementById('rsAttacks').textContent = stats.attacks;
document.getElementById('rsUpgrades').textContent = stats.upgrades;
document.getElementById('rsCaptures').textContent = stats.captures;
overlay.classList.add('active');
}
function closeResult() { document.getElementById('resultOverlay').classList.remove('active'); showMenu(); }
function rematch() { document.getElementById('resultOverlay').classList.remove('active'); resetGame(); }
function updateUI() {
document.getElementById('playerScore').textContent = countCells(PLAYER);
for (let i = 1; i <= 3; i++) {
const el = document.getElementById('ai' + i + 'Score');
const st = document.getElementById('ai' + i + 'Stat');
if (el) el.textContent = countCells(i);
if (st) st.style.display = (i <= aiCount) ? '' : 'none';
}
document.getElementById('turnNum').textContent = turnNumber;
document.getElementById('upgradePoints').textContent = upgradePoints;
document.getElementById('canAttack').textContent = countAttackers(PLAYER);
}
function openGameMenu() { document.getElementById('gameMenuOverlay').classList.add('active'); showActionsPanel(); }
function closeGameMenu() { document.getElementById('gameMenuOverlay').classList.remove('active'); }
function showActionsPanel() { document.getElementById('gmPanelActions').classList.add('active'); document.getElementById('gmPanelMechanics').classList.remove('active'); }
function showMechanicsPanel() { document.getElementById('gmPanelActions').classList.remove('active'); document.getElementById('gmPanelMechanics').classList.add('active'); }
document.getElementById('gameMenuOverlay').addEventListener('click', function(e) { if (e.target === this) closeGameMenu(); });
function getBaseThemeColors() {
const t = currentTheme;
const base = { neutral: '#3a5570', neutralEdge: '#1e3448', neutralGlow: 'rgba(58,85,112,0.2)', selected: '#ffd700', selectedGlow: 'rgba(255,215,0,0.5)', water: '#0a1525', darkText: '#fff', bg: '#0f1a2e', bgEdge: '#060d18' };
if (t === 'light') return Object.assign({}, base, { neutral: '#b8c4d4', neutralEdge: '#98a8bc', water: '#dce4f0', bg: '#e8eef7', bgEdge: '#d4dced' });
if (t === 'duck') return Object.assign({}, base, { neutral: '#f5e6c8', neutralEdge: '#d4c0a0', water: '#7ec8d3', bg: '#d4f1f4', bgEdge: '#a8e0e8', selected: '#ff6b35', darkText: '#5a4a1a' });
if (t === 'retro') return Object.assign({}, base, { neutral: '#b0b0b0', neutralEdge: '#808080', water: '#a0a0a0', bg: '#c0c0c0', bgEdge: '#a0a0a0', selected: '#0000c0', darkText: '#000' });
if (t === 'nature') return Object.assign({}, base, { neutral: '#8b7355', neutralEdge: '#5a4a35', water: '#3d6b8a', bg: '#4a7c3f', bgEdge: '#2d4a2b', darkText: '#2a3d20' });
if (t === 'sunset') return Object.assign({}, base, { neutral: '#6b5a70', neutralEdge: '#3a2a40', water: '#2a1540', bg: '#6b3a5a', bgEdge: '#2a1540', darkText: '#2a1540' });
if (t === 'puzzletheme') return Object.assign({}, base, { neutral: '#8d9aab', neutralEdge: '#5a6778', water: '#1a0f2e', bg: '#35205c', bgEdge: '#1a0f2e', selected: '#ffd54f', darkText: '#1a0f2e' });
if (t === 'coins') return Object.assign({}, base, { neutral: '#8a6a30', neutralEdge: '#4a3010', water: '#1a1208', bg: '#3a2a14', bgEdge: '#1a1208', selected: '#fff4c0', darkText: '#1a1208' });
if (t === 'sakura') return Object.assign({}, base, { neutral: '#f8dce8', neutralEdge: '#e0b8d0', water: '#fce4ec', bg: '#fdf2f7', bgEdge: '#f8d8e4', selected: '#ffd080', darkText: '#6b2a3e' });
if (t === 'space') return Object.assign({}, base, { neutral: '#2a1a5c', neutralEdge: '#150a30', water: '#05030f', bg: '#0a0620', bgEdge: '#05030f', selected: '#ffe066', darkText: '#e8e0ff' });
return base;
}
function getThemeColors() {
const base = getBaseThemeColors();
if (currentCellStyle !== 'auto' && CELL_STYLES[currentCellStyle]) {
const style = CELL_STYLES[currentCellStyle];
base.neutral = style.neutral; base.neutralEdge = style.neutralEdge; base.neutralGlow = style.neutralGlow;
base.selected = style.selected; base.selectedGlow = style.selectedGlow;
}
return base;
}
function drawGenericCell(hex, cell, isSel, isMax, C) {
let color, edge, glow, pcolor, bc;
if (cell.owner === PLAYER) {
const pc = getPlayerColorPalette();
if (isMax) { color = MAX_LEVEL_COLORS.player.color; edge = MAX_LEVEL_COLORS.player.edge; glow = MAX_LEVEL_COLORS.player.glow; pcolor = MAX_LEVEL_COLORS.player.text; bc = MAX_LEVEL_COLORS.player.border; }
else { color = pc.color; edge = pc.edge; glow = pc.glow; pcolor = C.darkText; bc = pc.edge; }
} else if (cell.owner === null) { color = C.neutral; edge = C.neutralEdge; glow = C.neutralGlow; pcolor = C.darkText; bc = C.neutralEdge; }
else {
const ai = AI_PALETTE[cell.owner] || AI_PALETTE[1];
if (isMax) { color = MAX_LEVEL_COLORS.ai.color; edge = MAX_LEVEL_COLORS.ai.edge; glow = MAX_LEVEL_COLORS.ai.glow; pcolor = MAX_LEVEL_COLORS.ai.text; bc = MAX_LEVEL_COLORS.ai.border; }
else { color = ai.base; edge = ai.edge; glow = ai.glow; pcolor = '#fff'; bc = ai.edge; }
}
ctx.shadowColor = isSel ? C.selectedGlow : glow;
ctx.shadowBlur = isSel ? 24 : 12;
const g = ctx.createRadialGradient(hex.x - HEX_SIZE * 0.2, hex.y - HEX_SIZE * 0.2, 2, hex.x, hex.y, HEX_SIZE * 0.9);
g.addColorStop(0, lightenColor(color, 18));
g.addColorStop(0.75, color);
g.addColorStop(1, edge);
drawHexPath(ctx, hex.x, hex.y, HEX_SIZE - 2);
ctx.fillStyle = g; ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = isSel ? C.selected : bc;
ctx.lineWidth = isSel ? 3 : (cell.owner !== null ? 2 : 1.4);
ctx.stroke();
if (cell.type && CELL_TYPES[cell.type]) { ctx.font = '13px "Segoe UI", Arial, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(CELL_TYPES[cell.type].icon, hex.x + HEX_SIZE * 0.45, hex.y - HEX_SIZE * 0.5); }
ctx.fillStyle = pcolor;
ctx.font = 'bold 16px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0,0,0,0.35)'; ctx.shadowBlur = 5;
ctx.fillText(cell.power, hex.x + 1, hex.y + 1);
ctx.shadowBlur = 0;
}
function draw() {
if (!canvas.width || !canvas.height) return;
const C = getThemeColors();
const isDuckTheme = currentTheme === 'duck';
const isNature = currentTheme === 'nature';
const isSunset = currentTheme === 'sunset';
const isPuzzleTheme = currentTheme === 'puzzletheme';
const isCoinsTheme = currentTheme === 'coins';
const isLight = currentTheme === 'light';
const isDark = currentTheme === 'dark';
const isSakura = currentTheme === 'sakura';
const isSpace = currentTheme === 'space';
const visibleSet = computeVisibleCells();
ctx.clearRect(0, 0, W, H);
if (isDuckTheme) drawDuckPondBackground();
else if (isNature) { const grad = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W/2); grad.addColorStop(0, '#5a8c45'); grad.addColorStop(0.6, '#4a7c3f'); grad.addColorStop(1, '#2d4a2b'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); }
else if (isSunset) { const horizon = H * 0.72; const skyGrad = ctx.createLinearGradient(0, 0, 0, horizon); skyGrad.addColorStop(0, '#2a1540'); skyGrad.addColorStop(0.4, '#5c2a5c'); skyGrad.addColorStop(0.7, '#c8506a'); skyGrad.addColorStop(1, '#ff8c42'); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, W, horizon); const groundGrad = ctx.createLinearGradient(0, horizon, 0, H); groundGrad.addColorStop(0, '#3d2050'); groundGrad.addColorStop(1, '#1a0a2a'); ctx.fillStyle = groundGrad; ctx.fillRect(0, horizon, W, H - horizon); }
else if (isSakura) { const grad = ctx.createLinearGradient(0, 0, 0, H); grad.addColorStop(0, '#fff0f5'); grad.addColorStop(0.5, '#fce4ec'); grad.addColorStop(1, '#f8d8e4'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); const sunGrad = ctx.createRadialGradient(W * 0.8, H * 0.2, 0, W * 0.8, H * 0.2, W * 0.5); sunGrad.addColorStop(0, 'rgba(255,240,200,0.5)'); sunGrad.addColorStop(1, 'rgba(255,240,200,0)'); ctx.fillStyle = sunGrad; ctx.fillRect(0, 0, W, H); if (!sakuraPetals) { sakuraPetals = []; for (let i = 0; i < 30; i++) sakuraPetals.push({ x: Math.random() * W, y: Math.random() * H, size: 4 + Math.random() * 5, rot: Math.random() * Math.PI * 2, rotSpeed: (Math.random() - 0.5) * 0.02, vy: 0.2 + Math.random() * 0.5, vx: (Math.random() - 0.5) * 0.3, phase: Math.random() * Math.PI * 2, phaseSpeed: 0.001 + Math.random() * 0.002, color: ['#ffb7c5','#ffc8d4','#ffdce5','#ff9eb5'][Math.floor(Math.random()*4)] }); } for (const p of sakuraPetals) { p.phase += p.phaseSpeed * 16; p.y += p.vy * 0.5; p.x += p.vx + Math.sin(p.phase) * 0.3; p.rot += p.rotSpeed; if (p.y > H + 10) { p.y = -10; p.x = Math.random() * W; } if (p.x < -10) p.x = W + 10; if (p.x > W + 10) p.x = -10; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.globalAlpha = 0.7; ctx.beginPath(); ctx.ellipse(0, 0, p.size, p.size * 0.5, 0, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; ctx.restore(); } }
else if (isSpace) drawSpaceBackground();
else { const grad0 = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W/2); grad0.addColorStop(0, C.bg); grad0.addColorStop(1, C.bgEdge); ctx.fillStyle = grad0; ctx.fillRect(0, 0, W, H); }
if (isDark) { const now = performance.now(); for (let i = 0; i < 50; i++) { const x = (i * 173) % W, y = (i * 89) % H; const a = 0.35 + Math.sin(now * 0.001 + i) * 0.3; ctx.fillStyle = `rgba(255,255,255,${a})`; ctx.beginPath(); ctx.arc(x, y, 1, 0, Math.PI * 2); ctx.fill(); } }
if (isLight) { ctx.strokeStyle = 'rgba(180,200,240,0.2)'; ctx.lineWidth = 2; for (let i = 0; i < 3; i++) { ctx.beginPath(); for (let x = 0; x <= W; x += 20) { const y = H * (0.25 + i * 0.3) + Math.sin(x * 0.015 + performance.now() * 0.001) * 10; if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } }
if (isPuzzleTheme) { for (let i = 0; i < 18; i++) { ctx.save(); ctx.globalAlpha = 0.1; const x = (i * 97) % W, y = (i * 53) % H; ctx.translate(x, y); ctx.rotate(performance.now() * 0.0002 + i); ctx.fillStyle = ['#ff7043','#42a5f5','#ffd54f'][i % 3]; ctx.fillRect(-8, -8, 16, 16); ctx.restore(); } }
if (isCoinsTheme) { for (let i = 0; i < 8; i++) { ctx.save(); ctx.globalAlpha = 0.15; const x = (i * 137) % W, y = (i * 89) % H; ctx.beginPath(); ctx.arc(x, y, 8, 0, Math.PI * 2); ctx.fillStyle = '#ffd93d'; ctx.fill(); ctx.restore(); } }
if (isDuckTheme) drawPondDucks();
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const hex = getHexCenter(c, r);
if (!grid[r][c]) {
drawHexPath(ctx, hex.x, hex.y, HEX_SIZE - 2);
if (isDuckTheme) ctx.fillStyle = 'rgba(90,170,185,0.35)';
else if (isSakura) ctx.fillStyle = 'rgba(248,216,228,0.7)';
else if (isSpace) ctx.fillStyle = 'rgba(5,3,15,0.7)';
else ctx.fillStyle = C.water;
ctx.fill();
continue;
}
const cell = grid[r][c];
const visible = isCellVisible(c, r, visibleSet);
if (!visible) {
drawHexPath(ctx, hex.x, hex.y, HEX_SIZE - 2);
ctx.fillStyle = 'rgba(30,45,65,0.92)'; ctx.fill();
ctx.fillStyle = 'rgba(220,235,250,0.55)';
ctx.font = 'bold 15px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('?', hex.x, hex.y);
continue;
}
const isSel = selectedCell && selectedCell.col === c && selectedCell.row === r;
const isMax = cell.power >= MAX_POWER;
drawGenericCell(hex, cell, isSel, isMax, C);
}
}
if (selectedCell) {
const src = grid[selectedCell.row] && grid[selectedCell.row][selectedCell.col];
if (src && src.owner === PLAYER) {
const neighbors = getNeighbors(selectedCell.col, selectedCell.row);
for (const n of neighbors) {
const t = grid[n.row][n.col];
if (!t || t.owner === PLAYER) continue;
if (!isCellVisible(n.col, n.row, visibleSet)) continue;
const pos = getHexCenter(n.col, n.row);
ctx.beginPath();
for (let i = 0; i < 6; i++) { const a = Math.PI / 3 * i + Math.PI / 6; const px = pos.x + (HEX_SIZE + 4) * Math.cos(a); const py = pos.y + (HEX_SIZE + 4) * Math.sin(a); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); }
ctx.closePath();
if (t.owner === null) ctx.strokeStyle = 'rgba(120,220,255,0.9)';
else ctx.strokeStyle = 'rgba(255,80,80,0.95)';
ctx.lineWidth = 3;
ctx.setLineDash([6, 4]);
ctx.stroke(); ctx.setLineDash([]);
}
}
}
drawPopupEffects();
}
function pickCell(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
if (!rect.width || !rect.height) return null;
const x = (clientX - rect.left) * canvas.width / rect.width;
const y = (clientY - rect.top) * canvas.height / rect.height;
let best = null, bestD = Infinity;
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
if (!grid[r][c]) continue;
const p = getHexCenter(c, r);
const d = Math.hypot(x - p.x, y - p.y);
if (d < bestD) { bestD = d; best = { col: c, row: r }; }
}
if (!best || bestD > HEX_SIZE * 1.05) return null;
return best;
}
function handleClick(clientX, clientY) {
if (!gameStarted) { setMsg('▶ Сначала нажмите «Играть»'); return; }
if (gameOver) return;
if (aiThinking) { setMsg('⏳ Противник думает...'); return; }
if (currentPlayer !== PLAYER) { setMsg('⏳ Не ваш ход'); return; }
const cell = pickCell(clientX, clientY);
if (!cell) { setMsg('⚠ Нажмите прямо на клетку'); return; }
const target = grid[cell.row][cell.col];
if (!target) return;
if (fogOfWar) { const vis = computeVisibleCells(); if (!isCellVisible(cell.col, cell.row, vis)) { setMsg('🌫️ Клетка скрыта туманом'); return; } }
if (turnPhase === 'upgrade' && target.owner === PLAYER) { upgradeCell(cell.col, cell.row); return; }
if (selectedCell) {
const src = selectedCell;
const srcCell = grid[src.row][src.col];
if (src.col === cell.col && src.row === cell.row) { selectedCell = null; setMsg('Выбор отменён'); draw(); return; }
if (target.owner === PLAYER) { selectedCell = { col: cell.col, row: cell.row }; if (target.power <= 1) setMsg('⚠ У клетки сила 1'); else setMsg('🔴 Выбрана другая клетка'); draw(); return; }
if (srcCell && srcCell.owner === PLAYER) {
const isNeighbor = getNeighbors(src.col, src.row).some(n => n.col === cell.col && n.row === cell.row);
if (!isNeighbor) { setMsg('❌ Можно атаковать только соседние клетки'); return; }
if (target.owner !== PLAYER) {
if (srcCell.power <= 1) { setMsg('⚔ У этой клетки сила 1'); return; }
const res = performAttack(src.col, src.row, cell.col, cell.row);
if (res.ok) {
if (res.captured) { selectedCell = { col: cell.col, row: cell.row }; const newPower = grid[cell.row][cell.col] ? grid[cell.row][cell.col].power : 0; if (newPower > 1) setMsg(`⛓️ Захват! Новая клетка (сила ${newPower}) выбрана`); else setMsg('⛓️ Захват! Новая клетка сила 1'); }
else setMsg('⚔ Атака не удалась');
}
updateUI(); draw();
return;
}
}
if (!srcCell || srcCell.owner !== PLAYER) { selectedCell = null; draw(); return; }
}
if (target.owner === PLAYER) { selectedCell = { col: cell.col, row: cell.row }; if (target.power <= 1) setMsg('⚠ У клетки сила 1'); else setMsg('🔴 Клетка выбрана. Кликайте по соседним!'); draw(); return; }
setMsg('❌ Выберите свою клетку');
}
(function installInput() { let last = 0; const TH = 60; function onPress(e) { if (e.button !== undefined && e.button !== 0) return; const now = performance.now(); if (now - last < TH) return; last = now; const t = e.touches && e.touches[0]; const cx = t ? t.clientX : e.clientX; const cy = t ? t.clientY : e.clientY; if (cx === undefined) return; e.preventDefault(); handleClick(cx, cy); } canvas.addEventListener('mousedown', onPress, false); canvas.addEventListener('touchstart', onPress, { passive: false }); })();
function showMenu() { document.getElementById('menuOverlay').classList.add('active'); showMainMenu(); }
function showMainMenu() { document.getElementById('menuMain').style.display = 'flex'; ['menuSubPlay','menuSubTutorial','menuSubSettings'].forEach(id => { const el = document.getElementById(id); el.classList.remove('active'); el.style.display = 'none'; }); }
function showSubMenu(type) { document.getElementById('menuMain').style.display = 'none'; const id = 'menuSub' + type.charAt(0).toUpperCase() + type.slice(1); const el = document.getElementById(id); if (!el) return; el.style.display = 'flex'; setTimeout(() => el.classList.add('active'), 10); if (type === 'play') { document.querySelectorAll('.map-btn').forEach(b => { b.classList.toggle('active', parseInt(b.dataset.map) === currentMap); }); document.querySelectorAll('#sizeSelector .quick-btn').forEach(b => { b.classList.toggle('active', b.dataset.size === currentSize); }); document.querySelectorAll('#modeSelector .quick-btn').forEach(b => { b.classList.toggle('active', b.dataset.mode === currentMode); }); updateAiCountUI(); updateFogUI(); } else if (type === 'tutorial') { tutorialStep = 0; initTutorialDots(); updateTutorialUI(); } else if (type === 'settings') { updateAudioUI(); updateHintsUI(); updateTrackNameUI(); } }
function selectMap(m) { currentMap = m; document.querySelectorAll('.map-btn').forEach(b => { b.classList.toggle('active', parseInt(b.dataset.map) === m); }); }
function buildTurnOrder() { turnOrder = [PLAYER]; for (let i = 1; i <= aiCount; i++) turnOrder.push(i); }
function startGame(diff) {
difficulty = diff;
document.getElementById('menuOverlay').classList.remove('active');
document.getElementById('resultOverlay').classList.remove('active');
closeGameMenu();
gameStarted = true; gameOver = false; currentPlayer = PLAYER; turnNumber = 1; selectedCell = null; aiThinking = false; playerTurnActive = false; turnPhase = 'idle'; maxModeActive = false; popupEffects = [];
stats = { attacks: 0, captures: 0, upgrades: 0 };
if (popupFrameId) { cancelAnimationFrame(popupFrameId); popupFrameId = null; }
buildTurnOrder(); currentTurnIndex = 0;
applyBoardSize(currentSize); initGrid(currentMap); updateUI(); draw();
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) { maxBtn.classList.remove('btn-gold'); maxBtn.textContent = '🔱 MAX'; maxBtn.disabled = true; }
document.getElementById('distributeBtn').disabled = true;
document.getElementById('skipBtn').disabled = false;
unlockAudio(); playSound('turn');
startPlayerTurn();
}
function resetGame() {
document.getElementById('resultOverlay').classList.remove('active');
closeGameMenu();
if (popupFrameId) { cancelAnimationFrame(popupFrameId); popupFrameId = null; }
popupEffects = []; stats = { attacks: 0, captures: 0, upgrades: 0 };
applyBoardSize(currentSize); initGrid(currentMap);
currentPlayer = PLAYER; turnNumber = 1; selectedCell = null; gameOver = false; aiThinking = false; playerTurnActive = false; turnPhase = 'idle'; usedUpgrades = 0; maxModeActive = false; gameStarted = true;
buildTurnOrder(); currentTurnIndex = 0;
const maxBtn = document.getElementById('maxBtn');
if (maxBtn) { maxBtn.classList.remove('btn-gold'); maxBtn.textContent = '🔱 MAX'; maxBtn.disabled = true; }
document.getElementById('distributeBtn').disabled = true;
document.getElementById('skipBtn').disabled = false;
document.getElementById('turnIndicator').textContent = '🔴 Ваш ход';
setStatusMsg('🔄 Новая игра!');
updateUI(); draw();
setTimeout(startPlayerTurn, 200);
}
let tutorialStep = 0;
const TUT_TOTAL = 12;
function initTutorialDots() { const dots = document.getElementById('tutDots'); if (!dots) return; dots.innerHTML = ''; for (let i = 0; i < TUT_TOTAL; i++) { const d = document.createElement('div'); d.className = 'dot' + (i === 0 ? ' active' : ''); d.onclick = () => { tutorialStep = i; updateTutorialUI(); }; dots.appendChild(d); } }
function updateTutorialUI() { document.querySelectorAll('#menuSubTutorial .tutorial-slide').forEach((s, i) => { s.classList.toggle('active', i === tutorialStep); }); document.querySelectorAll('#tutDots .dot').forEach((d, i) => { d.classList.toggle('active', i === tutorialStep); }); document.getElementById('tutPrev').disabled = tutorialStep === 0; document.getElementById('tutNext').textContent = tutorialStep === TUT_TOTAL-1 ? 'Готово ✓' : 'Далее →'; }
function tutorialPrev() { if (tutorialStep > 0) { tutorialStep--; updateTutorialUI(); } }
function tutorialNext() { if (tutorialStep < TUT_TOTAL - 1) { tutorialStep++; updateTutorialUI(); } else showMainMenu(); }
/* Init */
loadTheme();
loadAudioSettings();
applyBoardSize(currentSize);
initGrid(1);
resizeDuckCanvas();
window.addEventListener('resize', resizeDuckCanvas);
fallingDucksFrame = requestAnimationFrame(updateFallingDucks);
updateUI();
draw();
document.getElementById('menuOverlay').classList.add('active');
showMainMenu();
initTutorialDots();
renderColorPalette();
updatePlayerDot();
updateFogUI();
updateAiCountUI();
updateTrackNameUI();
document.querySelectorAll('#sizeSelector .quick-btn').forEach(b => { b.classList.toggle('active', b.dataset.size === currentSize); });
document.querySelectorAll('#modeSelector .quick-btn').forEach(b => { b.classList.toggle('active', b.dataset.mode === currentMode); });
setMsg('🎮 Выберите сложность в меню "Играть"');
console.log('✦ HEXAGONIC 1.57.0 — КРЯ уменьшена, 5 общих треков + 3 космических');
</script>
</body>
</html>Game Source: Hexagonic
Creator: NeonFlare13
Libraries: none
Complexity: complex (1967 lines, 158.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: hexagonic-neonflare13" to link back to the original. Then publish at arcadelab.ai/publish.