code game máy bay chiến đấu vũ trụ | VN-Z | Cộng đồng Chia Sẻ Kiến Thức Công Nghệ và Phần Mềm Máy Tính

code game máy bay chiến đấu vũ trụ

omaigot

Gà con
trải nghiệm phiên bản mới nhất:
menu game:
screenshot-2026-04-13-130341-png.71774

Xem phần đính kèm 71774
  • Dễ (Easy): HP và Khiên người chơi x1.5; máu kẻ địch giảm còn 0.6; vàng nhận được x1.5. Có hồi máu tự động.
  • Trung bình (Normal): Trải nghiệm tiêu chuẩn với 15 đợt tấn công (waves) và 3 Boss.
  • Khó (Hard): Kẻ địch mạnh hơn (x1.5); không có hồi máu tự động; Boss nổi giận sớm hơn.
  • Bất khả thi (Impossible): Kẻ địch cực mạnh (x2.5); có đạn độc; Boss xuất hiện ngay từ Wave 1; giá vật phẩm trong Shop gấp đôi.
    • Di chuyển: Sử dụng các phím WASD.
    • Tấn công: Sử dụng Chuột để nhắm và bắn.
    • Kỹ năng đặc biệt:
      • Shift: Lướt (Dash) để né tránh.
      • Q: Nova Bomb (Nổ diện rộng).
      • W: Barrier (Lớp bảo vệ/Khiên).
      • F: EMP (Làm tê liệt kẻ địch).
      • R: Vortex (Hố đen hút kẻ địch).
      • T: Meteor (Thiên thạch tấn công).Hệ thống Combo: Ghi nhận số lượng kẻ địch tiêu diệt liên tiếp để tăng điểm số.
HTML:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Galaxy Roguelike — V4</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;user-select:none}
body{background:#0a0a12;margin:0;padding:0;font-family:'Courier New',monospace;overflow:hidden}
#wrap{position:relative;width:100vw;height:100vh;overflow:hidden;background:#000;border-radius:0;border:none;box-shadow:none}
#gc{display:block;cursor:crosshair}
#ui{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}

/* HUD */
.hud-top{display:flex;justify-content:space-between;align-items:flex-start;padding:12px 18px;background:linear-gradient(rgba(0,0,0,.8),transparent);gap:20px}
.stat-col{display:flex;flex-direction:column;gap:6px}
.bar-row{display:flex;align-items:center;gap:7px;font-size:11px}
.bar-bg{border-radius:4px;overflow:hidden;background:#ffffff12;position:relative}
.bar-fill{height:8px;border-radius:4px;transition:width .25s}
#hp-fill{background:linear-gradient(90deg,#a32d2d,#e24b4a)}
#sh-fill{background:linear-gradient(90deg,#185fa5,#378add)}
#xp-fill{background:linear-gradient(90deg,#0f6e56,#1d9e75)}
.wave-center{text-align:center}
.wave-label{font-size:10px;color:#555;letter-spacing:2px;text-transform:uppercase}
.wave-num{font-size:38px;font-weight:700;color:#0ff;line-height:1;text-shadow:0 0 20px #0ff6}
.stats-right{text-align:right;font-size:11px;color:#555;line-height:2.1}
.stats-right b{color:#fac775;font-size:13px}

/* skill bar */
#skillbar{position:absolute;bottom:0;left:50%;transform:translateX(-50%);display:flex;gap:6px;padding:10px 14px;background:linear-gradient(transparent,rgba(0,0,0,.75));border-radius:0 0 16px 16px;pointer-events:none}
.sk{width:48px;height:48px;border-radius:10px;border:1px solid #fff2;background:#ffffff08;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:20px;position:relative;overflow:hidden}
.sk-overlay{position:absolute;bottom:0;left:0;right:0;background:#0009;text-align:center;font-size:9px;color:#aaa;padding:1px 0 2px}
.sk-key{position:absolute;top:2px;right:4px;font-size:8px;color:#ffffff50;font-family:monospace}
.sk-label{position:absolute;bottom:26px;font-size:7px;color:#ffffff40;white-space:nowrap}

/* boss bar */
#bossbar{position:absolute;top:62px;left:50%;transform:translateX(-50%);width:340px;display:none;pointer-events:none;text-align:center}
#boss-name{font-size:11px;color:#ff5555;letter-spacing:2px;margin-bottom:4px;text-transform:uppercase}
#boss-bg{background:#fff1;border-radius:5px;height:12px;overflow:hidden;border:1px solid #ff333330}
#boss-fill{height:100%;background:linear-gradient(90deg,#791f1f,#e24b4a);border-radius:5px;transition:width .15s}
#boss-phase{font-size:9px;color:#ff555580;margin-top:3px;letter-spacing:1px}

/* combo */
#combo-el{position:absolute;top:68px;left:50%;transform:translateX(-50%);font-size:20px;font-weight:700;color:#fac775;opacity:0;transition:opacity .3s;pointer-events:none;text-shadow:0 0 12px #fac77580;white-space:nowrap}

/* floating dmg */
.floatdmg{position:absolute;pointer-events:none;font-size:13px;font-weight:700;transition:none}

/* minimap */
#minimap{position:absolute;bottom:68px;right:12px;border:1px solid #fff2;border-radius:8px;opacity:.8;pointer-events:none}
#minimap-label{position:absolute;bottom:142px;right:12px;font-size:9px;color:#fff3;letter-spacing:1px;pointer-events:none}

/* messages */
#msg{position:absolute;top:48%;left:50%;transform:translate(-50%,-50%);color:#fff;font-size:24px;font-weight:700;text-align:center;pointer-events:none;opacity:0;transition:opacity .35s;line-height:1.6;text-shadow:0 0 20px #0ff8}

/* panels */
.panel{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:16px;padding:24px;color:#fff;pointer-events:all;display:none;backdrop-filter:blur(2px)}
#shop-panel{background:rgba(2,4,20,.97);border:1px solid #0ff3;width:420px;max-height:520px;overflow-y:auto}
#shop-panel h3{color:#0ff;margin-bottom:6px;font-size:13px;text-align:center;letter-spacing:2px}
.shop-tabs{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid #ffffff12;padding-bottom:8px}
.stab{flex:1;padding:6px 4px;text-align:center;font-size:10px;color:#555;cursor:pointer;border-radius:6px 6px 0 0;transition:.15s}
.stab.active{color:#0ff;background:#0ff08;border-bottom:2px solid #0ff}
.shop-item{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;margin:4px 0;background:#ffffff07;border-radius:10px;border:1px solid #ffffff12;cursor:pointer;transition:.15s;gap:10px}
.shop-item:hover{background:#0ff08;border-color:#0ff4}
.shop-item.cant{opacity:.3;cursor:not-allowed}
.shop-item.bought{border-color:#1d9e7540;background:#1d9e7508;opacity:.6;cursor:not-allowed}
.si-icon{font-size:20px;min-width:26px;text-align:center}
.si-body{flex:1}
.si-name{font-size:12px;font-weight:700;color:#ddd}
.si-desc{font-size:10px;color:#555;margin-top:2px}
.si-effect{font-size:9px;color:#1d9e75;margin-top:2px}
.si-cost{display:flex;flex-direction:column;align-items:flex-end;gap:2px}
.si-price{color:#fac775;font-size:12px;font-weight:700;white-space:nowrap}
.si-stock{font-size:9px;color:#555}
.shop-gold-bar{display:flex;align-items:center;gap:6px;margin-bottom:10px;padding:8px 10px;background:#fac77510;border-radius:8px;border:1px solid #fac77520}
.shop-gold-icon{font-size:16px}
.shop-gold-val{font-size:16px;font-weight:700;color:#fac775}
.shop-gold-label{font-size:10px;color:#777}

#talent-panel{background:rgba(4,2,22,.98);border:1px solid #7f77dd;width:500px;max-height:520px;overflow-y:auto}
#talent-panel h3{color:#afa9ec;margin-bottom:4px;text-align:center;font-size:13px;letter-spacing:2px}
.talent-tabs{display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid #ffffff12;padding-bottom:8px}
.ttab{flex:1;padding:6px 4px;text-align:center;font-size:10px;color:#555;cursor:pointer;border-radius:6px 6px 0 0;transition:.15s;letter-spacing:.5px}
.ttab.active{color:#afa9ec;background:#7f77dd18;border-bottom:2px solid #7f77dd}
.talent-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}
.tc{background:#ffffff07;border:1px solid #7f77dd30;border-radius:12px;padding:12px 8px;cursor:pointer;text-align:center;transition:.18s;position:relative}
.tc:hover{background:#7f77dd18;border-color:#7f77dd;transform:translateY(-2px)}
.tc.taken{border-color:#fac77560;background:#fac77508;cursor:default}
.tc.taken::after{content:'✓';position:absolute;top:6px;right:8px;font-size:10px;color:#fac775}
.tc-icon{font-size:26px;margin-bottom:5px}
.tc-name{font-size:11px;font-weight:700;color:#afa9ec;margin-bottom:2px}
.tc-desc{font-size:9px;color:#555}
.tc-tag{display:inline-block;font-size:8px;padding:1px 5px;border-radius:4px;margin-top:4px;color:#afa9ec;background:#7f77dd25;border:1px solid #7f77dd30}
.tc-tag.off{color:#e24b4a;background:#e24b4a18;border-color:#e24b4a30}
.tc-tag.atk{color:#fac775;background:#fac77518;border-color:#fac77530}
.tc-tag.def{color:#378add;background:#378add18;border-color:#378add30}
.tc-tag.uti{color:#1d9e75;background:#1d9e7518;border-color:#1d9e7530}
.tc-synergy{font-size:9px;color:#7f77dd80;margin-top:3px;font-style:italic}

/* achievement */
#ach{position:absolute;top:90px;right:14px;background:rgba(2,12,2,.95);border:1px solid #1d9e75;border-radius:10px;padding:10px 14px;font-size:11px;pointer-events:none;opacity:0;transition:opacity .4s;max-width:220px}
#ach-title{color:#1d9e75;font-size:9px;letter-spacing:2px;margin-bottom:3px;text-transform:uppercase}

/* skill upgrade panel */
#skill-panel{background:rgba(4,2,22,.98);border:1px solid #d4537e;width:400px}
#skill-panel h3{color:#ed93b1;margin-bottom:16px;text-align:center;font-size:13px;letter-spacing:2px}
.skill-upgrade-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.su-card{background:#ffffff07;border:1px solid #d4537e30;border-radius:12px;padding:14px;cursor:pointer;transition:.18s}
.su-card:hover{background:#d4537e18;border-color:#d4537e}
.su-card .su-icon{font-size:26px;margin-bottom:6px}
.su-card .su-name{font-size:12px;font-weight:700;color:#ed93b1}
.su-card .su-desc{font-size:10px;color:#555;margin-top:3px}
.su-card .su-lv{font-size:10px;color:#fac775;margin-top:4px}

/* controls hint */
#hint{position:absolute;bottom:68px;left:12px;font-size:9px;color:#ffffff25;line-height:2;pointer-events:none}

/* ── MENU SCREEN ── */
#menu-screen{position:absolute;inset:0;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000008;border-radius:16px;overflow:hidden}
#menu-bg{position:absolute;inset:0;pointer-events:none}
#menu-content{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;width:100%;gap:0}

/* Logo */
.menu-logo{text-align:center;margin-bottom:32px}
.menu-logo-title{font-size:52px;font-weight:900;color:#fff;letter-spacing:8px;line-height:1;text-transform:uppercase;text-shadow:0 0 40px #0ff4,0 0 80px #0ff1}
.menu-logo-title span{color:#0ff;text-shadow:0 0 20px #0ff}
.menu-logo-sub{font-size:10px;color:#ffffff35;letter-spacing:6px;margin-top:8px;text-transform:uppercase}

/* Difficulty selector */
.diff-row{display:flex;gap:8px;margin-bottom:24px}
.diff-btn{
  padding:8px 18px;border-radius:8px;border:1px solid transparent;
  font-family:'Courier New',monospace;font-size:11px;font-weight:700;letter-spacing:2px;
  cursor:pointer;transition:.18s;text-transform:uppercase;background:transparent;color:#555;position:relative;
}
.diff-btn:not(.locked):hover{color:#fff;border-color:#fff3;background:#ffffff08}
.diff-btn.locked{cursor:not-allowed;opacity:.4}
.diff-btn.locked::after{content:'🔒';font-size:8px;margin-left:4px}
.diff-btn.active-easy{color:#1d9e75;border-color:#1d9e7560;background:#1d9e7510;text-shadow:0 0 10px #1d9e7580}
.diff-btn.active-normal{color:#378add;border-color:#378add60;background:#378add10;text-shadow:0 0 10px #378add80}
.diff-btn.active-hard{color:#fac775;border-color:#fac77560;background:#fac77510;text-shadow:0 0 10px #fac77580}
.diff-btn.active-impossible{color:#e24b4a;border-color:#e24b4a60;background:#e24b4a10;text-shadow:0 0 10px #e24b4a80}

/* Info card */
#diff-info{
  width:340px;border-radius:14px;padding:18px 22px;margin-bottom:24px;
  border:1px solid transparent;transition:.3s;text-align:center;min-height:110px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;
}
#diff-info .di-icon{font-size:30px}
#diff-info .di-name{font-size:15px;font-weight:700;letter-spacing:2px}
#diff-info .di-desc{font-size:10px;color:#ffffff60;line-height:1.7}
#diff-info .di-tags{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin-top:4px}
#diff-info .di-tag{font-size:9px;padding:2px 8px;border-radius:20px;font-weight:700;letter-spacing:.5px}
#diff-info .di-record{font-size:9px;color:#ffffff35;margin-top:2px}

/* Play button */
#play-btn{
  width:200px;padding:14px 0;border-radius:12px;border:none;
  font-family:'Courier New',monospace;font-size:14px;font-weight:900;letter-spacing:4px;
  cursor:pointer;transition:.18s;text-transform:uppercase;
  background:linear-gradient(135deg,#0ff2,#0055ff18);
  color:#0ff;border:1px solid #0ff40;
  box-shadow:0 0 30px #0ff1,inset 0 0 20px #0ff05;
  margin-bottom:20px;
}
#play-btn:hover{transform:translateY(-2px);box-shadow:0 0 40px #0ff3,inset 0 0 30px #0ff10;border-color:#0ff8}
#play-btn:active{transform:scale(.97)}

.menu-footer{font-size:9px;color:#ffffff20;letter-spacing:1px;text-align:center}
.menu-footer b{color:#ffffff40}



/* difficulty info themes */
#diff-info.theme-easy{background:rgba(29,158,117,.08);border-color:#1d9e7530}
#diff-info.theme-easy .di-name{color:#1d9e75}
#diff-info.theme-easy .di-tag{background:#1d9e7520;color:#5dcaa5;border:1px solid #1d9e7530}
#diff-info.theme-normal{background:rgba(55,138,221,.08);border-color:#378add30}
#diff-info.theme-normal .di-name{color:#378add}
#diff-info.theme-normal .di-tag{background:#378add20;color:#85b7eb;border:1px solid #378add30}
#diff-info.theme-hard{background:rgba(250,199,117,.08);border-color:#fac77530}
#diff-info.theme-hard .di-name{color:#fac775}
#diff-info.theme-hard .di-tag{background:#fac77520;color:#ef9f27;border:1px solid #fac77530}
#diff-info.theme-impossible{background:rgba(226,75,74,.08);border-color:#e24b4a30}
#diff-info.theme-impossible .di-name{color:#e24b4a}
#diff-info.theme-impossible .di-tag{background:#e24b4a20;color:#f09595;border:1px solid #e24b4a30}

/* in-game difficulty badge */
#diff-badge{position:absolute;top:10px;left:50%;transform:translateX(-50%);font-size:10px;font-weight:700;padding:3px 12px;border-radius:20px;pointer-events:none;letter-spacing:1px;opacity:.8}

/* win/lose screen over the canvas */
#end-screen{position:absolute;inset:0;z-index:90;display:none;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,8,.85);border-radius:16px}
.end-title{font-size:38px;font-weight:700;text-align:center;line-height:1.2;margin-bottom:8px}
.end-stats{font-size:13px;color:#ffffff70;text-align:center;line-height:2;margin-bottom:28px}
.end-btns{display:flex;gap:12px}
.end-btn{padding:11px 28px;border-radius:10px;font-size:12px;font-weight:700;letter-spacing:1px;cursor:pointer;border:1px solid transparent;font-family:inherit;transition:.15s}
.end-btn:hover{transform:translateY(-2px)}
.end-btn.primary{background:#0ff1;border-color:#0ff4;color:#0ff}
.end-btn.secondary{background:#fff1;border-color:#fff2;color:#fff9}
</style>
</head>
<body>
<div id="wrap">
<canvas id="gc"></canvas>


<div id="menu-screen">
  <canvas id="menu-bg"></canvas>
  <div id="menu-content">
    <div class="menu-logo">
      <div class="menu-logo-title">GALAXY <span>ROGUELIKE</span></div>
      <div class="menu-logo-sub" id="menu-sub">V5 — Chọn độ khó</div>
    </div>

    <!-- Difficulty selector tabs -->
    <div class="diff-row" id="diff-row">
      <button class="diff-btn" id="mc-easy"      onclick="selectDiff('easy')">🌿 Dễ</button>
      <button class="diff-btn" id="mc-normal"    onclick="selectDiff('normal')">⚔️ Trung Bình</button>
      <button class="diff-btn" id="mc-hard"       onclick="selectDiff('hard')">💀 Khó</button>
      <button class="diff-btn" id="mc-impossible" onclick="selectDiff('impossible')">☠️ Bất Khả Thi</button>
    </div>

    <!-- Info card for selected difficulty -->
    <div id="diff-info" class="theme-normal">
      <div class="di-icon">⚔️</div>
      <div class="di-name">TRUNG BÌNH</div>
      <div class="di-desc">Trải nghiệm chuẩn. 15 waves, 3 boss.</div>
      <div class="di-tags">
        <span class="di-tag">Chuẩn</span>
        <span class="di-tag">15 Waves</span>
        <span class="di-tag">3 Boss</span>
      </div>
      <div class="di-record" id="di-record">Chưa có kỷ lục</div>
    </div>

    <!-- Play button -->
    <button id="play-btn" onclick="startGame(selectedDiff)">▶ CHƠI NGAY</button>

    <div class="menu-footer"><b>WASD</b> di chuyển &nbsp;•&nbsp; <b>Chuột</b> bắn &nbsp;•&nbsp; <b>E</b> Shop &nbsp;•&nbsp; <b>X</b> Upgrade</div>
  </div>
</div>

<!-- END SCREEN -->
<div id="end-screen">
  <div class="end-title" id="end-title">GAME OVER</div>
  <div class="end-stats" id="end-stats"></div>
  <div class="end-btns">
    <button class="end-btn primary" onclick="restartSame()">Chơi lại</button>
    <button class="end-btn secondary" onclick="goMenu()">Menu chính</button>
  </div>
</div>
<div id="ui">
  <div class="hud-top">
    <div class="stat-col">
      <div class="bar-row"><span style="color:#e24b4a;width:20px">HP</span><div class="bar-bg" style="width:130px"><div id="hp-fill" class="bar-fill" style="width:100%;height:8px"></div></div><span id="hp-val" style="color:#e24b4a;min-width:36px">100</span></div>
      <div class="bar-row"><span style="color:#378add;width:20px">SH</span><div class="bar-bg" style="width:130px"><div id="sh-fill" class="bar-fill" style="width:100%;height:8px"></div></div><span id="sh-val" style="color:#378add;min-width:36px">100</span></div>
      <div class="bar-row"><span style="color:#1d9e75;width:20px">XP</span><div class="bar-bg" style="width:130px"><div id="xp-fill" class="bar-fill" style="width:0%;height:8px"></div></div><span id="lv-val" style="color:#1d9e75;min-width:36px">Lv1</span></div>
    </div>
    <div class="wave-center">
      <div class="wave-label">wave</div>
      <div class="wave-num" id="wn">1</div>
    </div>
    <div class="stats-right">
      SCORE <b id="sv">0</b><br>
      KILLS <b id="kv" style="color:#e24b4a">0</b><br>
      GOLD <b id="gv">0</b>
    </div>
  </div>
  <div id="combo-el"></div>
  <div id="diff-badge"></div>
  <div id="bossbar">
    <div id="boss-name">BOSS</div>
    <div id="boss-bg"><div id="boss-fill" style="width:100%"></div></div>
    <div id="boss-phase"></div>
  </div>
  <div id="skillbar">
    <div class="sk" id="sk0"><span>💨</span><span class="sk-key">Shift</span><div class="sk-overlay" id="scd0"></div></div>
    <div class="sk" id="sk1"><span>💥</span><span class="sk-key">Q</span><div class="sk-overlay" id="scd1"></div></div>
    <div class="sk" id="sk2"><span>🛡️</span><span class="sk-key">W</span><div class="sk-overlay" id="scd2"></div></div>
    <div class="sk" id="sk3"><span>⚡</span><span class="sk-key">F</span><div class="sk-overlay" id="scd3"></div></div>
    <div class="sk" id="sk4"><span>🌀</span><span class="sk-key">R</span><div class="sk-overlay" id="scd4"></div></div>
    <div class="sk" id="sk5"><span>☄️</span><span class="sk-key">T</span><div class="sk-overlay" id="scd5"></div></div>
    <div class="sk"><span>🏪</span><span class="sk-key">E</span></div>
  </div>
  <canvas id="minimap" width="110" height="80"></canvas>
  <div id="minimap-label">RADAR</div>
  <div id="hint">WASD: Di chuyển | Chuột: Bắn<br>Shift:Dash Q:Nuke W:Shield F:EMP<br>R:Vortex T:Meteor | E:Shop | X:Upgrade</div>
  <div id="msg"></div>
  <div id="ach"><div id="ach-title">achievement</div><div id="ach-text"></div></div>
</div>

<div class="panel" id="shop-panel">
  <h3>🏪 UPGRADE SHOP</h3>
  <div class="shop-gold-bar"><span class="shop-gold-icon">💰</span><span class="shop-gold-val" id="shop-gold">0</span><span class="shop-gold-label">gold available</span></div>
  <div class="shop-tabs">
    <div class="stab active" onclick="setShopTab('combat')">⚔️ Combat</div>
    <div class="stab" onclick="setShopTab('defense')">🛡️ Defense</div>
    <div class="stab" onclick="setShopTab('utility')">⚙️ Utility</div>
    <div class="stab" onclick="setShopTab('special')">✨ Special</div>
  </div>
  <div id="shop-items"></div>
  <div style="margin-top:10px;color:#333;font-size:10px;text-align:center">E để đóng • Items có giới hạn số lần mua</div>
</div>

<div class="panel" id="talent-panel">
  <h3>★ LEVEL UP — CHỌN 1 KỸ NĂNG THỤ ĐỘNG</h3>
  <div class="talent-tabs" id="talent-tabs"></div>
  <div class="talent-grid" id="tg"></div>
  <div id="talent-synergy-hint" style="margin-top:10px;font-size:10px;color:#7f77dd60;text-align:center;font-style:italic"></div>
</div>

<div class="panel" id="skill-panel">
  <h3>🔧 NÂNG CẤP KỸ NĂNG</h3>
  <div class="skill-upgrade-grid" id="su-grid"></div>
  <div style="margin-top:12px;color:#333;font-size:10px;text-align:center">X để đóng</div>
</div>
</div>

<script>
const C=document.getElementById('gc'),ctx=C.getContext('2d');
const MM=document.getElementById('minimap'),mctx=MM.getContext('2d');
const WRAP=document.getElementById('wrap');
function rsz(){C.width=WRAP.clientWidth;C.height=WRAP.clientHeight}rsz();
window.addEventListener('resize',rsz);
const W=()=>C.width,H=()=>C.height;

/* ── difficulty config ── */
const DIFF_CONFIGS={
  easy:{
    label:'DỄ',color:'#1d9e75',bgColor:'rgba(29,158,117,.25)',
    playerHpMult:1.5,playerShieldMult:1.5,
    enemyHpMult:.6,enemySpeedMult:.7,enemyGoldMult:1.5,
    bossHpMult:.6,bossSpeedMult:.7,
    shopCostMult:.8,regenForced:true,
    startGold:40,waveTarget:6,totalWaves:12,
    poisonBullets:false,bossFromWave1:false,
    desc:'Dễ',tagColor:'#5dcaa5'
  },
  normal:{
    label:'TRUNG BÌNH',color:'#378add',bgColor:'rgba(55,138,221,.25)',
    playerHpMult:1,playerShieldMult:1,
    enemyHpMult:1,enemySpeedMult:1,enemyGoldMult:1,
    bossHpMult:1,bossSpeedMult:1,
    shopCostMult:1,regenForced:false,
    startGold:0,waveTarget:8,totalWaves:15,
    poisonBullets:false,bossFromWave1:false,
    desc:'Trung Bình',tagColor:'#85b7eb'
  },
  hard:{
    label:'KHÓ',color:'#fac775',bgColor:'rgba(250,199,117,.25)',
    playerHpMult:.8,playerShieldMult:.8,
    enemyHpMult:1.5,enemySpeedMult:1.35,enemyGoldMult:1.3,
    bossHpMult:2,bossSpeedMult:1.4,
    shopCostMult:1.3,regenForced:false,
    startGold:0,waveTarget:12,totalWaves:18,
    poisonBullets:false,bossFromWave1:false,noRegen:true,
    desc:'Khó',tagColor:'#ef9f27'
  },
  impossible:{
    label:'BẤT KHẢ THI',color:'#e24b4a',bgColor:'rgba(226,75,74,.25)',
    playerHpMult:.6,playerShieldMult:.6,
    enemyHpMult:2.5,enemySpeedMult:1.8,enemyGoldMult:.8,
    bossHpMult:3,bossSpeedMult:2,
    shopCostMult:2,regenForced:false,
    startGold:0,waveTarget:15,totalWaves:20,
    poisonBullets:true,bossFromWave1:true,noRegen:true,
    desc:'Bất Khả Thi',tagColor:'#f09595'
  }
};

/* ── save / unlock system ── */
function getSave(){try{return JSON.parse(localStorage.getItem('glx_save')||'{}')}catch{return{}}}
function setSave(d){try{localStorage.setItem('glx_save',JSON.stringify(d))}catch{}}

function isUnlocked(diff){
  return true;
}

function recordClear(diff,score,won){
  const save=getSave();
  if(won&&diff==='normal')save.normalCleared=true;
  if(won&&diff==='hard')save.hardCleared=true;
  if(!save.records)save.records={};
  if(!save.records[diff]||score>save.records[diff])save.records[diff]=score;
  setSave(save);
}

let selectedDiff='normal';

const DIFF_META={
  easy:{icon:'🌿',name:'DỄ',desc:'Dành cho người mới. Kẻ địch yếu hơn, hồi HP nhiều hơn.',tags:['HP ×1.5','Enemy ×0.6','Gold ×1.5']},
  normal:{icon:'⚔️',name:'TRUNG BÌNH',desc:'Trải nghiệm chuẩn. Cân bằng giữa thách thức và phần thưởng.',tags:['Chuẩn','15 Waves','3 Boss']},
  hard:{icon:'💀',name:'KHÓ',desc:'Không có HP regen. Boss enrage sớm. Cần kỹ năng thực sự.',tags:['Enemy ×1.5','No Regen','Boss ×2']},
  impossible:{icon:'☠️',name:'BẤT KHẢ THI',desc:'Địch bầy đàn. Đạn độc. Boss từ wave 1. Shop ×2.',tags:['Enemy ×2.5','Poison','Boss W1','Shop ×2']}
};

function selectDiff(diff){
  if(!isUnlocked(diff))return;
  selectedDiff=diff;
  const cfg=DIFF_CONFIGS[diff];
  const meta=DIFF_META[diff];
  const save=getSave();

  // update button active states
  ['easy','normal','hard','impossible'].forEach(d=>{
    const btn=document.getElementById('mc-'+d);
    btn.classList.remove('active-easy','active-normal','active-hard','active-impossible');
  });
  document.getElementById('mc-'+diff).classList.add('active-'+diff);

  // update info card
  const info=document.getElementById('diff-info');
  info.className='theme-'+diff;
  info.querySelector('.di-icon').textContent=meta.icon;
  info.querySelector('.di-name').textContent=meta.name;
  info.querySelector('.di-desc').textContent=meta.desc;
  const tagsEl=info.querySelector('.di-tags');
  tagsEl.innerHTML=meta.tags.map(t=>`<span class="di-tag">${t}</span>`).join('');

  // record
  const rec=document.getElementById('di-record');
  if(save.records&&save.records[diff])rec.textContent='🏆 Kỷ lục: '+save.records[diff].toLocaleString()+' pts';
  else rec.textContent='Chưa có kỷ lục';

  // play button color
  const pb=document.getElementById('play-btn');
  pb.style.color=cfg.color;
  pb.style.borderColor=cfg.color+'60';
  pb.style.boxShadow=`0 0 30px ${cfg.color}22,inset 0 0 20px ${cfg.color}08`;
}

function updateMenuUI(){
  const save=getSave();
  ['easy','normal','hard','impossible'].forEach(d=>{
    const btn=document.getElementById('mc-'+d);
    const isLocked=!isUnlocked(d);
    btn.classList.toggle('locked',isLocked);
  });
  selectDiff(selectedDiff);
}

let currentDiff='normal';
let menuAnimFrame=null;
let menuStars=[];
let menuParticles=[];
let menuFrame=0;

function initMenuStars(){
  menuStars=[];
  for(let i=0;i<180;i++)menuStars.push({
    x:Math.random()*1920,y:Math.random()*1080,
    s:Math.random()*1.8+.2,b:Math.random(),
    sp:.05+Math.random()*.3,
    col:Math.random()<.12?[100,180,255]:Math.random()<.05?[255,150,200]:[255,255,255]
  });
}
initMenuStars();

function drawMenuBG(){
  const mc=document.getElementById('menu-bg');
  const MW=WRAP.clientWidth||960,MH=WRAP.clientHeight||620;
  mc.width=MW;mc.height=MH;
  const mctx2=mc.getContext('2d');
  mctx2.clearRect(0,0,MW,MH);
  menuFrame++;

  // deep space gradient
  const grd=mctx2.createRadialGradient(MW/2,MH/2,50,MW/2,MH/2,Math.max(MW,MH)*.52);
  grd.addColorStop(0,'#050518');grd.addColorStop(1,'#000003');
  mctx2.fillStyle=grd;mctx2.fillRect(0,0,MW,MH);

  // nebula blobs
  const nebHues=[200,280,340];
  nebHues.forEach((hue,i)=>{
    const ox=Math.sin(menuFrame*.0012+i*1.2)*30,oy=Math.cos(menuFrame*.001+i)*20;
    const cx=MW*.21+i*(MW*.29)+ox,cy=MH*.24+i*(MH*.19)+oy;
    const gr=mctx2.createRadialGradient(cx,cy,20,cx,cy,200);
    gr.addColorStop(0,`hsla(${hue},80%,50%,.07)`);gr.addColorStop(1,'transparent');
    mctx2.fillStyle=gr;mctx2.beginPath();mctx2.arc(cx,cy,200,0,Math.PI*2);mctx2.fill();
  });

  // stars with parallax
  menuStars.forEach(s=>{
    const px=(s.x+menuFrame*s.sp*.4)%MW;
    const py=(s.y+menuFrame*s.sp*.15)%MH;
    const flicker=.55+Math.sin(menuFrame*.05+s.b*8)*.45;
    const [r,g,b]=s.col;
    mctx2.fillStyle=`rgba(${r},${g},${b},${s.b*flicker*.85})`;
    mctx2.beginPath();mctx2.arc(px,py,s.s,0,Math.PI*2);mctx2.fill();
    if(s.s>1.3){
      mctx2.strokeStyle=`rgba(${r},${g},${b},${s.b*.2*flicker})`;mctx2.lineWidth=.5;
      mctx2.beginPath();mctx2.moveTo(px-s.s*3,py);mctx2.lineTo(px+s.s*3,py);
      mctx2.moveTo(px,py-s.s*3);mctx2.lineTo(px,py+s.s*3);mctx2.stroke();
    }
  });

  // floating particles
  if(Math.random()<.08)menuParticles.push({x:Math.random()*MW,y:MH+10,vx:(Math.random()-.5)*.5,vy:-(Math.random()*1+.5),life:200+Math.random()*200,s:Math.random()*2+1,hue:Math.random()*360});
  menuParticles=menuParticles.filter(p=>{
    p.x+=p.vx;p.y+=p.vy;p.life--;
    mctx2.globalAlpha=p.life/250*.6;
    mctx2.fillStyle=`hsl(${p.hue},70%,60%)`;
    mctx2.beginPath();mctx2.arc(p.x,p.y,p.s,0,Math.PI*2);mctx2.fill();
    return p.life>0&&p.y>-20;
  });
  mctx2.globalAlpha=1;

  if(document.getElementById('menu-screen').style.display!=='none'){
    menuAnimFrame=requestAnimationFrame(drawMenuBG);
  }
}

function showMenu(){
  document.getElementById('menu-screen').style.display='flex';
  document.getElementById('end-screen').style.display='none';
  document.getElementById('ui').style.display='none';
  updateMenuUI();
  if(menuAnimFrame)cancelAnimationFrame(menuAnimFrame);
  drawMenuBG();
}

function startGame(diff){
  if(!isUnlocked(diff))return;
  currentDiff=diff;
  const cfg=DIFF_CONFIGS[diff];

  // hide menu, show UI
  document.getElementById('menu-screen').style.display='none';
  document.getElementById('end-screen').style.display='none';
  document.getElementById('ui').style.display='block';
  if(menuAnimFrame)cancelAnimationFrame(menuAnimFrame);

  // set difficulty badge
  const badge=document.getElementById('diff-badge');
  badge.textContent=cfg.label;
  badge.style.background=cfg.bgColor;
  badge.style.color=cfg.color;
  badge.style.border='1px solid '+cfg.color+'50';

  initGame(cfg);
  if(!loopRunning){loopRunning=true;loop();}
}

function goMenu(){
  document.getElementById('end-screen').style.display='none';
  showMenu();
}

function restartSame(){
  document.getElementById('end-screen').style.display='none';
  startGame(currentDiff);
}

function showEndScreen(won){
  const cfg=DIFF_CONFIGS[currentDiff];
  recordClear(currentDiff,G.score,won);
  const es=document.getElementById('end-screen');
  const title=document.getElementById('end-title');
  const stats=document.getElementById('end-stats');
  es.style.display='flex';
  if(won){
    title.innerHTML=`<span style="color:#fac775">🏆 CHIẾN THẮNG!</span>`;
  } else {
    title.innerHTML=`<span style="color:#e24b4a">☠️ GAME OVER</span>`;
  }
  stats.innerHTML=`
    Chế độ: <b style="color:${cfg.color}">${cfg.label}</b><br>
    Điểm số: <b style="color:#fac775">${G.score.toLocaleString()}</b><br>
    Kẻ địch tiêu diệt: <b style="color:#e24b4a">${G.kills}</b>&nbsp;&nbsp;
    Boss: <b style="color:#ff5555">${G.bossKills}</b><br>
    Wave đạt được: <b style="color:#0ff">${G.wave}</b>&nbsp;&nbsp;
    Level: <b style="color:#1d9e75">${G.level}</b>
  `;
  updateMenuUI();
}

let loopRunning=false;

/* ── input ── */
const keys={},M={x:480,y:310,dn:false};
C.addEventListener('mousemove',e=>{const r=C.getBoundingClientRect();M.x=e.clientX-r.left;M.y=e.clientY-r.top});
C.addEventListener('mousedown',e=>{if(e.button===0)M.dn=true});
C.addEventListener('mouseup',e=>{if(e.button===0)M.dn=false});
document.addEventListener('keydown',e=>{if(!keys[e.code]){keys[e.code]=true;onKey(e.code)}});
document.addEventListener('keyup',e=>{keys[e.code]=false});

/* ── skill definitions ── */
const SKILL_DEFS={
  dash:{name:'Dash',icon:'💨',maxLv:5,base:{cd:100,power:1},perLv:{cd:-10,power:.25},desc:lv=>`Teleport • CD: ${((100-lv*10)/60).toFixed(1)}s • Lực: ${(1+lv*.25).toFixed(1)}x`},
  nuke:{name:'Nova Bomb',icon:'💥',maxLv:5,base:{cd:240,radius:1,dmg:200},perLv:{cd:-20,radius:.3,dmg:60},desc:lv=>`AoE nổ • CD: ${((240-lv*20)/60).toFixed(1)}s • R: ${(1+lv*.3).toFixed(1)}x`},
  shield:{name:'Barrier',icon:'🛡️',maxLv:5,base:{cd:160,restore:75},perLv:{cd:-15,restore:25},desc:lv=>`Lá chắn • CD: ${((160-lv*15)/60).toFixed(1)}s • +${75+lv*25}HP`},
  emp:{name:'EMP Pulse',icon:'⚡',maxLv:5,base:{cd:200,dur:185},perLv:{cd:-25,dur:40},desc:lv=>`Đóng băng • CD: ${((200-lv*25)/60).toFixed(1)}s • Dur: ${((185+lv*40)/60).toFixed(1)}s`},
  vortex:{name:'Vortex',icon:'🌀',maxLv:4,base:{cd:300,dur:120,suck:3},perLv:{cd:-30,dur:20,suck:1},desc:lv=>`Hút địch • CD: ${((300-lv*30)/60).toFixed(1)}s • Lực: ${3+lv}x`},
  meteor:{name:'Meteor',icon:'☄️',maxLv:4,base:{cd:280,dmg:80,count:3},perLv:{cd:-25,dmg:30,count:1},desc:lv=>`Mưa sao • CD: ${((280-lv*25)/60).toFixed(1)}s • ${80+lv*30}dmg x${3+lv}`},
};

/* ── passive talents — full overhaul ── */
// category: atk=attack, def=defense, uti=utility, off=offensive special
const TALENT_CATS={
  atk:{label:'⚔️ Attack',color:'#fac775'},
  def:{label:'🛡️ Defense',color:'#378add'},
  uti:{label:'⚙️ Utility',color:'#1d9e75'},
  off:{label:'💥 Special',color:'#d4537e'},
};

const TALENTS=[
  // ── ATTACK ──
  {id:'rapidfire',name:'Rapid Fire',cat:'atk',desc:'Tốc độ bắn +20%',detail:'Giảm cooldown mỗi viên đạn',icon:'🔥',synergy:'multishot',apply:p=>{p.fireRate=Math.max(4,Math.floor(p.fireRate*.8))},stackable:true},
  {id:'multishot',name:'Multi Shot',cat:'atk',desc:'+2 đạn mỗi phát',detail:'Mỗi lần bắn ra thêm 2 đạn',icon:'💫',synergy:'rapidfire',apply:p=>{p.multiShot+=2},stackable:true},
  {id:'pierce',name:'Pierce Shot',cat:'atk',desc:'Đạn xuyên kẻ địch',detail:'Đạn không dừng lại khi trúng',icon:'🏹',synergy:null,apply:p=>{p.pierce=true},stackable:false},
  {id:'crit',name:'Critical Hit',cat:'atk',desc:'Crit +15% (×3.5 dmg)',detail:'Đạn vàng = crit',icon:'💢',synergy:'crit',apply:p=>{p.critChance+=.15;p.critMult=(p.critMult||3)+.5},stackable:true},
  {id:'overload',name:'Overload',cat:'atk',desc:'Tốc đạn +40%',detail:'Đạn bay nhanh hơn nhiều',icon:'⚡',synergy:'rapidfire',apply:p=>{p.bulletSpeed*=1.4},stackable:false},
  {id:'ricochet',name:'Ricochet',cat:'atk',desc:'Đạn nảy tường 2 lần',detail:'Đạn phản xạ khi ra mép',icon:'🎯',synergy:'pierce',apply:p=>{p.ricochet=true},stackable:false},
  {id:'burst',name:'Burst Fire',cat:'atk',desc:'Mỗi 5 phát: burst 3 đạn',detail:'Trigger tự động',icon:'🔫',synergy:'rapidfire',apply:p=>{p.burst=true;p.burstCounter=0},stackable:false},
  {id:'plasma',name:'Plasma Round',cat:'atk',desc:'Đạn lớn +50% kích cỡ & dmg',detail:'Đạn plasma xanh đậm',icon:'🔵',synergy:'pierce',apply:p=>{p.plasma=true},stackable:false},
  {id:'sniper',name:'Sniper Mode',cat:'atk',desc:'Dmg +80% nhưng bắn chậm -30%',detail:'Ít đạn nhưng cực mạnh',icon:'🎯',synergy:'crit',apply:p=>{p.dmgMult=(p.dmgMult||1)*1.8;p.fireRate=Math.floor(p.fireRate*1.3)},stackable:false},
  {id:'shotgun',name:'Shotgun Spread',cat:'atk',desc:'+4 đạn tản, tầm gần mạnh',detail:'Bắn thêm 4 đạn lan toả',icon:'💥',synergy:'multishot',apply:p=>{p.multiShot+=4;p.shotgunSpread=true},stackable:false},
  {id:'incendiary',name:'Incendiary',cat:'atk',desc:'Đạn đốt địch 3s',detail:'Kẻ địch bốc lửa sau khi trúng',icon:'🔥',synergy:'blast',apply:p=>{p.incendiary=true},stackable:false},
  {id:'explosive',name:'Explosive Tip',cat:'atk',desc:'Đạn nổ AoE nhỏ khi trúng',detail:'Vụ nổ bán kính 35px',icon:'💣',synergy:'ricochet',apply:p=>{p.explosive=true},stackable:false},
  {id:'dualshot',name:'Dual Cannon',cat:'atk',desc:'Bắn 2 đạn song song',detail:'Hai luồng đạn cùng lúc',icon:'🔱',synergy:'rapidfire',apply:p=>{p.dualCannon=true},stackable:false},
  {id:'homing',name:'Homing Bullets',cat:'atk',desc:'Đạn tự dẫn hướng nhẹ',detail:'Đạn cong theo mục tiêu gần',icon:'🧲',synergy:'multishot',apply:p=>{p.homingBullets=true},stackable:false},
  // ── DEFENSE ──
  {id:'tank',name:'Iron Hull',cat:'def',desc:'Max HP +100',detail:'Tăng sinh lực tối đa',icon:'🛡️',synergy:'regen',apply:p=>{p.maxHp+=100;p.hp=Math.min(p.hp+100,p.maxHp)},stackable:true},
  {id:'battery',name:'Power Cell',cat:'def',desc:'Max Shield +100',detail:'Tăng tấm chắn tối đa',icon:'🔋',synergy:'shieldRegen',apply:p=>{p.maxShield+=100;p.shield=Math.min(p.shield+100,p.maxShield)},stackable:true},
  {id:'regen',name:'Regen Core',cat:'def',desc:'Hồi 4 HP/giây',detail:'Tự động hồi HP',icon:'💚',synergy:'tank',apply:p=>{p.regen=true;p.regenAmt=(p.regenAmt||0)+4},stackable:true},
  {id:'vamp',name:'Vampire',cat:'def',desc:'Hút máu +10/kill',detail:'Mỗi kill hồi HP',icon:'🧛',synergy:'blast',apply:p=>{p.lifesteal+=10},stackable:true},
  {id:'thorns',name:'Thorns',cat:'def',desc:'Phản 15dmg khi bị đánh',detail:'Kẻ địch nhận sát thương',icon:'🌵',synergy:null,apply:p=>{p.thorns=(p.thorns||0)+15},stackable:true},
  {id:'fortress',name:'Fortress',cat:'def',desc:'Giảm 20% sát thương nhận',detail:'Armor thụ động',icon:'🏰',synergy:'tank',apply:p=>{p.armor=(p.armor||0)+.2},stackable:true},
  {id:'shieldregen',name:'Shield Regen',cat:'def',desc:'Shield tự hồi 3/giây',detail:'Tấm chắn tự phục hồi',icon:'🔷',synergy:'battery',apply:p=>{p.shieldRegen=true;p.shieldRegenAmt=(p.shieldRegenAmt||0)+3},stackable:true},
  {id:'laststand',name:'Last Stand',cat:'def',desc:'HP<20%: sát thương +50%',detail:'Nguy hiểm càng mạnh',icon:'❤️‍🔥',synergy:'vamp',apply:p=>{p.lastStand=true},stackable:false},
  {id:'overclock_shield',name:'Overclock Shield',cat:'def',desc:'Nhận dmg: shield bật tức thì',detail:'Kích hoạt shield tự động',icon:'⚡',synergy:'battery',apply:p=>{p.autoShield=true},stackable:false},
  {id:'evasion',name:'Evasion',cat:'def',desc:'20% né hoàn toàn đòn tấn công',detail:'Dodge cơ học thụ động',icon:'💨',synergy:'haste',apply:p=>{p.evasion=(p.evasion||0)+.2},stackable:true},
  {id:'phoenix',name:'Phoenix Core',cat:'def',desc:'Hồi sống 1 lần với 50% HP',detail:'Hồi sinh khi HP về 0',icon:'🦅',synergy:'regen',apply:p=>{p.phoenix=true},stackable:false},
  // ── UTILITY ──
  {id:'haste',name:'Haste',cat:'uti',desc:'Tốc độ di chuyển +20%',detail:'Phản ứng nhanh hơn',icon:'👟',synergy:null,apply:p=>{p.speed*=1.2},stackable:true},
  {id:'drone',name:'Combat Drone',cat:'uti',desc:'+1 drone tự bắn',detail:'Drone bay quanh và tấn công',icon:'🤖',synergy:'drone',apply:p=>{addDrone()},stackable:true},
  {id:'magnet',name:'Magnet Field',cat:'uti',desc:'Hút vàng & item trong 120px',detail:'Thu thập tự động',icon:'🧲',synergy:null,apply:p=>{p.magnet=true;p.magnetRange=(p.magnetRange||120)+20},stackable:true},
  {id:'goldaura',name:'Gold Aura',cat:'uti',desc:'Vàng từ kill +70%',detail:'Kiếm nhiều tiền hơn',icon:'💰',synergy:'magnet',apply:p=>{p.goldBonus=(p.goldBonus||1)*1.7},stackable:false},
  {id:'cooldown',name:'Overclock',cat:'uti',desc:'CD tất cả skill -15%',detail:'Skill dùng được thường hơn',icon:'⏱️',synergy:null,apply:p=>{p.cdReduction=(p.cdReduction||1)*.85},stackable:true},
  {id:'missile',name:'Auto Missile',cat:'uti',desc:'Tên lửa homing mỗi 3s',detail:'Tự tìm và tấn công mục tiêu',icon:'🚀',synergy:'drone',apply:p=>{p.missiles=true},stackable:false},
  {id:'xpboost',name:'Scholar',cat:'uti',desc:'XP từ kill +60%',detail:'Lên level nhanh hơn nhiều',icon:'📚',synergy:null,apply:p=>{p.xpBonus=(p.xpBonus||1)*1.6},stackable:true},
  {id:'turret',name:'Auto Turret',cat:'uti',desc:'Tự bắn địch gần nhất mỗi 1s',detail:'Turret thụ động 360°',icon:'🗼',synergy:'drone',apply:p=>{p.turret=true},stackable:false},
  {id:'scanner',name:'Radar Scanner',cat:'uti',desc:'Thấy vị trí boss trước 3s',detail:'Cảnh báo sớm boss xuất hiện',icon:'📡',synergy:null,apply:p=>{p.scanner=true},stackable:false},
  {id:'banker',name:'Banker',cat:'uti',desc:'Mỗi wave còn sống: +30 vàng',detail:'Thu nhập thụ động mỗi wave',icon:'🏦',synergy:'goldaura',apply:p=>{p.banker=true},stackable:true},
  {id:'recycler',name:'Recycler',cat:'uti',desc:'Đạn miss có 25% hoàn lại CD',detail:'Hiệu quả đạn tăng',icon:'♻️',synergy:'cooldown',apply:p=>{p.recycler=true},stackable:false},
  // ── SPECIAL ──
  {id:'blast',name:'Blast Kill',cat:'off',desc:'Giết tạo vụ nổ AoE',detail:'Mỗi kill gây sát thương vùng',icon:'💣',synergy:'vamp',apply:p=>{p.blast=true},stackable:false},
  {id:'chainbolt',name:'Chain Bolt',cat:'off',desc:'Đạn nối kẻ địch gần',detail:'Sau khi trúng lan sang 2 địch',icon:'🌩️',synergy:'multishot',apply:p=>{p.chain=true},stackable:false},
  {id:'blackhole',name:'Black Hole',cat:'off',desc:'Giết có 5% tạo lỗ đen nhỏ',detail:'Hút địch trong 2 giây',icon:'🌀',synergy:'blast',apply:p=>{p.blackhole=true},stackable:false},
  {id:'timewarp',name:'Time Warp',cat:'off',desc:'Mỗi 8s: slowmo 2s',detail:'Mọi thứ chậm lại 60%',icon:'⏰',synergy:'cooldown',apply:p=>{p.timewarp=true;p.timewarpCd=0},stackable:false},
  {id:'shockwave',name:'Shockwave',cat:'off',desc:'Mỗi 6s phát sóng đẩy địch',detail:'Sóng xung AoE quanh tàu',icon:'🌊',synergy:'blast',apply:p=>{p.shockwave=true;p.shockwaveCd=0},stackable:false},
  {id:'mirror',name:'Mirror Image',cat:'off',desc:'20% tạo bản sao đạn đôi',detail:'Phản chiếu đạn theo hướng ngược',icon:'🪞',synergy:'multishot',apply:p=>{p.mirror=true},stackable:false},
  {id:'freeze',name:'Cryo Rounds',cat:'off',desc:'Đạn làm chậm địch -35%',detail:'Hiệu ứng băng giá lên địch',icon:'❄️',synergy:'chainbolt',apply:p=>{p.freezeRounds=true},stackable:false},
  {id:'gravity',name:'Gravity Well',cat:'off',desc:'Mỗi 10s: hút toàn bộ địch vào giữa',detail:'Lực hút hành tinh siêu mạnh',icon:'🌍',synergy:'blackhole',apply:p=>{p.gravityWell=true;p.gravityWellCd=0},stackable:false},
  {id:'disruptor',name:'Disruptor',cat:'off',desc:'Địch trúng đạn: giảm dmg -40%',detail:'Làm suy yếu kẻ địch',icon:'📵',synergy:'freeze',apply:p=>{p.disruptor=true},stackable:false},
  {id:'nova',name:'Supernova',cat:'off',desc:'Khi HP<10%: nổ tung AoE cực lớn',detail:'Kích hoạt 1 lần/game',icon:'🌟',synergy:'blast',apply:p=>{p.supernova=true;p.supernovaUsed=false},stackable:false},
];

let takenTalentIds=[];
let currentTalentTab='atk';
let shopTab='combat';

/* ── shop items — full overhaul ── */
const SHOP_DATA={
  combat:[
    {name:'Bullet Upgrade',icon:'🔫',desc:'Sát thương đạn +30%',effect:'+30% base dmg',cost:60,stock:3,bought:0,apply:p=>{p.dmgMult=(p.dmgMult||1)*1.3}},
    {name:'Overcharge Ammo',icon:'⚡',desc:'Tốc đạn +35%',effect:'+35% bullet speed',cost:55,stock:3,bought:0,apply:p=>{p.bulletSpeed*=1.35}},
    {name:'Fire Rate Chip',icon:'🔥',desc:'Tốc bắn +15%',effect:'-15% fire cooldown',cost:65,stock:4,bought:0,apply:p=>{p.fireRate=Math.max(4,Math.floor(p.fireRate*.85))}},
    {name:'Hollow Point',icon:'💎',desc:'Crit damage +1x',effect:'critMult +1',cost:80,stock:2,bought:0,apply:p=>{p.critMult=(p.critMult||3)+1}},
    {name:'Drone Armament',icon:'🤖',desc:'Drone damage +50%',effect:'+50% drone dmg',cost:70,stock:3,bought:0,apply:p=>{p.droneDmgMult=(p.droneDmgMult||1)*1.5}},
    {name:'Plasma Core',icon:'🔵',desc:'Tất cả đạn xuyên giáp',effect:'pierce mode on',cost:120,stock:1,bought:0,apply:p=>{p.pierce=true}},
    {name:'Scatter Shot',icon:'💥',desc:'+3 đạn tản ra khi bắn',effect:'+3 spread bullets',cost:75,stock:2,bought:0,apply:p=>{p.multiShot+=3}},
    {name:'Incendiary Rounds',icon:'🔥',desc:'Đạn đốt địch 3 giây',effect:'burn on hit 3s',cost:90,stock:2,bought:0,apply:p=>{p.incendiary=true}},
    {name:'Crit Enhancer',icon:'💢',desc:'Crit chance +20%',effect:'+20% crit rate',cost:85,stock:2,bought:0,apply:p=>{p.critChance+=.2}},
    {name:'Dual Barrels',icon:'🔱',desc:'Bắn 2 luồng song song',effect:'dual cannon mode',cost:110,stock:1,bought:0,apply:p=>{p.dualCannon=true}},
    {name:'Explosive Tip',icon:'💣',desc:'Đạn nổ AoE nhỏ khi trúng',effect:'AoE explosion r=35',cost:100,stock:2,bought:0,apply:p=>{p.explosive=true}},
    {name:'Ricochet Module',icon:'🎯',desc:'Đạn nảy tường 2 lần',effect:'wall bounce ×2',cost:80,stock:2,bought:0,apply:p=>{p.ricochet=true}},
    {name:'Homing Upgrade',icon:'🧲',desc:'Đạn tự hướng mục tiêu nhẹ',effect:'soft homing bullets',cost:95,stock:2,bought:0,apply:p=>{p.homingBullets=true}},
    {name:'Burst Chip',icon:'⚙️',desc:'Mỗi 5 phát burst 3 đạn',effect:'auto burst mode',cost:70,stock:2,bought:0,apply:p=>{p.burst=true;p.burstCounter=0}},
    {name:'Chain Lightning',icon:'🌩️',desc:'Đạn trúng truyền sang 2 địch',effect:'chain on hit ×2',cost:115,stock:1,bought:0,apply:p=>{p.chain=true}},
    {name:'Cryo Ammo',icon:'❄️',desc:'Đạn làm chậm địch -35%',effect:'slow on hit',cost:85,stock:2,bought:0,apply:p=>{p.freezeRounds=true}},
    {name:'Overpower',icon:'🌟',desc:'Sát thương +60% trong 5s khi giết boss',effect:'+60% dmg after boss kill',cost:130,stock:1,bought:0,apply:p=>{p.overpower=true}},
  ],
  defense:[
    {name:'Medkit',icon:'💊',desc:'Hồi ngay 120 HP',effect:'instant +120 HP',cost:50,stock:5,bought:0,apply:p=>{p.hp=Math.min(p.maxHp,p.hp+120)}},
    {name:'Shield Recharge',icon:'🔵',desc:'Nạp đầy toàn bộ shield',effect:'shield → max',cost:45,stock:5,bought:0,apply:p=>{p.shield=p.maxShield}},
    {name:'Armor Plate',icon:'🧱',desc:'Max HP +80',effect:'+80 max HP',cost:65,stock:4,bought:0,apply:p=>{p.maxHp+=80;p.hp=Math.min(p.hp+80,p.maxHp)}},
    {name:'Shield Capacitor',icon:'🔋',desc:'Max Shield +80',effect:'+80 max shield',cost:60,stock:4,bought:0,apply:p=>{p.maxShield+=80;p.shield=Math.min(p.shield+80,p.maxShield)}},
    {name:'Nano Repair',icon:'💚',desc:'Regen +6 HP/giây',effect:'+6 HP regen/s',cost:75,stock:3,bought:0,apply:p=>{p.regen=true;p.regenAmt=(p.regenAmt||0)+6}},
    {name:'Reactive Armor',icon:'🛡️',desc:'Giảm 15% sát thương nhận',effect:'-15% dmg taken',cost:90,stock:2,bought:0,apply:p=>{p.armor=(p.armor||0)+.15}},
    {name:'Thorns Matrix',icon:'🌵',desc:'Phản 20 dmg khi bị đánh',effect:'reflect 20 dmg',cost:70,stock:3,bought:0,apply:p=>{p.thorns=(p.thorns||0)+20}},
    {name:'Lifesteal Chip',icon:'🧛',desc:'Hút máu +15/kill',effect:'+15 HP per kill',cost:80,stock:3,bought:0,apply:p=>{p.lifesteal+=15}},
    {name:'Emergency Repair',icon:'🔧',desc:'Khi HP<25%: hồi 80 HP tức thì (1 lần)',effect:'auto-heal at low HP',cost:100,stock:2,bought:0,apply:p=>{p.emergencyRepair=true;p.emergencyRepairUsed=false}},
    {name:'Shield Regen Module',icon:'🔷',desc:'Shield tự hồi 4/giây',effect:'+4 shield regen/s',cost:85,stock:3,bought:0,apply:p=>{p.shieldRegen=true;p.shieldRegenAmt=(p.shieldRegenAmt||0)+4}},
    {name:'Evasion Drive',icon:'💨',desc:'15% né hoàn toàn đòn tấn công',effect:'+15% dodge chance',cost:90,stock:2,bought:0,apply:p=>{p.evasion=(p.evasion||0)+.15}},
    {name:'Heavy Plating',icon:'🏰',desc:'Max HP +150 nhưng tốc độ -5%',effect:'+150 HP, -5% speed',cost:100,stock:2,bought:0,apply:p=>{p.maxHp+=150;p.hp=Math.min(p.hp+150,p.maxHp);p.speed*=.95}},
    {name:'Phoenix Feather',icon:'🦅',desc:'Hồi sống 1 lần với 50% HP',effect:'1× revive at 50% HP',cost:150,stock:1,bought:0,apply:p=>{p.phoenix=true}},
    {name:'Stasis Field',icon:'🔮',desc:'Bất tử 1.5s sau khi nhận đòn chí mạng',effect:'invincible frames',cost:110,stock:2,bought:0,apply:p=>{p.stasis=true}},
    {name:'Full Restore',icon:'✨',desc:'Hồi toàn bộ HP và Shield',effect:'full HP+Shield restore',cost:120,stock:2,bought:0,apply:p=>{p.hp=p.maxHp;p.shield=p.maxShield}},
  ],
  utility:[
    {name:'Gold Booster',icon:'💰',desc:'Vàng từ kill +80%',effect:'+80% gold per kill',cost:85,stock:2,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.8}},
    {name:'XP Boost',icon:'📈',desc:'XP từ kill +50%',effect:'+50% XP gain',cost:70,stock:2,bought:0,apply:p=>{p.xpBonus=(p.xpBonus||1)*1.5}},
    {name:'Skill Cooldown',icon:'⏱️',desc:'CD tất cả skill -20%',effect:'-20% all cooldowns',cost:95,stock:2,bought:0,apply:p=>{p.cdReduction=(p.cdReduction||1)*.8}},
    {name:'Speed Module',icon:'👟',desc:'Tốc độ di chuyển +15%',effect:'+15% move speed',cost:50,stock:3,bought:0,apply:p=>{p.speed*=1.15}},
    {name:'Magnet Field',icon:'🧲',desc:'Thu item trong vòng 150px',effect:'auto collect range',cost:40,stock:2,bought:0,apply:p=>{p.magnet=true;p.magnetRange=(p.magnetRange||120)+40}},
    {name:'Combo Extender',icon:'🔗',desc:'Combo timer +2 giây',effect:'+120 combo duration',cost:55,stock:3,bought:0,apply:p=>{p.comboBonus=(p.comboBonus||0)+120}},
    {name:'Scout Drone',icon:'🔍',desc:'+1 drone trinh thám, tăng phạm vi minimap',effect:'radar range ×1.5',cost:60,stock:2,bought:0,apply:p=>{addDrone();p.radarBoost=true}},
    {name:'Dash Booster',icon:'🌌',desc:'Dash mạnh hơn ×1.8',effect:'dash power ×1.8',cost:75,stock:2,bought:0,apply:p=>{p.dashPower=(p.dashPower||1)*1.8}},
    {name:'Overclock Drive',icon:'🖥️',desc:'CD skill -25%, tốc bắn +10%',effect:'speed+cooldown combo',cost:110,stock:1,bought:0,apply:p=>{p.cdReduction=(p.cdReduction||1)*.75;p.fireRate=Math.max(4,Math.floor(p.fireRate*.9))}},
    {name:'Gold Magnet',icon:'💛',desc:'Hút vàng từ xa +100%, +50% gold/kill',effect:'gold combo upgrade',cost:95,stock:2,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.5;p.magnet=true;p.magnetRange=(p.magnetRange||120)+60}},
    {name:'Wave Bonus',icon:'🌊',desc:'Nhận 25 vàng mỗi lần đánh bại 1 wave',effect:'+25g per wave clear',cost:80,stock:2,bought:0,apply:p=>{p.waveBonus=(p.waveBonus||0)+25}},
    {name:'Auto Turret',icon:'🗼',desc:'Turret tự bắn địch gần mỗi 1s',effect:'auto turret 360°',cost:120,stock:1,bought:0,apply:p=>{p.turret=true}},
    {name:'Score Multiplier',icon:'🏆',desc:'Điểm từ kill ×2',effect:'score ×2',cost:90,stock:2,bought:0,apply:p=>{p.scoreMult=(p.scoreMult||1)*2}},
    {name:'Missile Launcher',icon:'🚀',desc:'Tên lửa homing tự động mỗi 3s',effect:'auto homing missile',cost:100,stock:2,bought:0,apply:p=>{p.missiles=true}},
    {name:'Resource Recycler',icon:'♻️',desc:'Địch chết rải vàng +1 mỗi lần',effect:'bonus gold on kill',cost:65,stock:3,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.3}},
  ],
  special:[
    {name:'Drone Upgrade',icon:'🤖',desc:'+1 Combat Drone mới',effect:'spawn extra drone',cost:100,stock:3,bought:0,apply:p=>{addDrone()}},
    {name:'Nova Core',icon:'💥',desc:'Nuke radius +70%',effect:'+70% nuke AoE',cost:90,stock:2,bought:0,apply:p=>{if(G.p.skills.nuke)G.p.skills.nuke.radiusBonus=(G.p.skills.nuke.radiusBonus||1)*1.7}},
    {name:'Warp Drive',icon:'🌌',desc:'Dash teleport distance ×2',effect:'dash power ×2',cost:80,stock:2,bought:0,apply:p=>{p.dashPower=(p.dashPower||1)*2}},
    {name:'Chain Reactor',icon:'🌩️',desc:'Đạn bắn xích sang địch kế',effect:'chain lightning on hit',cost:110,stock:1,bought:0,apply:p=>{p.chain=true}},
    {name:'Black Market',icon:'💀',desc:'Mở khoá 3 item ẩn đặc biệt',effect:'secret items unlocked',cost:150,stock:1,bought:0,apply:p=>{p.blackMarket=true;setTimeout(()=>renderShop(),100)}},
    {name:'Omni Shield',icon:'⭕',desc:'Shield không bao giờ về 0 (min 10)',effect:'shield floor = 10',cost:130,stock:1,bought:0,apply:p=>{p.shieldFloor=10}},
    {name:'Supernova Charge',icon:'🌟',desc:'Khi HP<10%: nổ AoE cực lớn 1 lần',effect:'emergency supernova',cost:140,stock:1,bought:0,apply:p=>{p.supernova=true;p.supernovaUsed=false}},
    {name:'Black Hole Generator',icon:'🌀',desc:'Giết có 8% tạo lỗ đen hút địch',effect:'8% black hole proc',cost:120,stock:1,bought:0,apply:p=>{p.blackhole=true}},
    {name:'Time Distortion',icon:'⏰',desc:'Mỗi 8s: toàn bộ địch chậm 2s',detail:'Slowmo field 60%',effect:'timewarp field',cost:125,stock:1,bought:0,apply:p=>{p.timewarp=true;p.timewarpCd=0}},
    {name:'Gravity Well',icon:'🌍',desc:'Mỗi 10s: hút toàn bộ địch vào tâm',effect:'gravity pull ×all',cost:135,stock:1,bought:0,apply:p=>{p.gravityWell=true;p.gravityWellCd=0}},
    {name:'Shockwave Emitter',icon:'🌊',desc:'Mỗi 6s: sóng đẩy địch ra ngoài',effect:'shockwave push AoE',cost:115,stock:1,bought:0,apply:p=>{p.shockwave=true;p.shockwaveCd=0}},
    {name:'Disruptor Field',icon:'📵',desc:'Địch trúng đạn: giảm dmg -40%',effect:'weaken on hit',cost:110,stock:2,bought:0,apply:p=>{p.disruptor=true}},
    // secret items (unlocked by Black Market)
    {name:'Death Ray',icon:'☠️',desc:'[ẨN] Dmg đạn ×3, xuyên mọi thứ',effect:'triple dmg + pierce',cost:200,stock:1,bought:0,secret:true,apply:p=>{p.dmgMult=(p.dmgMult||1)*3;p.pierce=true}},
    {name:'God Mode',icon:'🌈',desc:'[ẨN] Bất tử 8 giây',effect:'invincible 8s',cost:250,stock:1,bought:0,secret:true,apply:p=>{p.invincible=true;setTimeout(()=>{if(p)p.invincible=false},8000)}},
    {name:'Annihilator',icon:'💀',desc:'[ẨN] Giết tất cả địch trên màn hình',effect:'instant kill all enemies',cost:300,stock:1,bought:0,secret:true,apply:p=>{if(G&&G.enemies){G.enemies.forEach(e=>{e.hp=0});}}},
  ],
};

const ACH=[
  {id:'k10',label:'First Blood',check:g=>g.kills>=10,done:false},
  {id:'k50',label:'Warrior — 50 kills',check:g=>g.kills>=50,done:false},
  {id:'k200',label:'Slayer — 200 kills',check:g=>g.kills>=200,done:false},
  {id:'c15',label:'Unstoppable — x15 combo',check:g=>g.combo>=15,done:false},
  {id:'w5',label:'Veteran — Wave 5',check:g=>g.wave>=5,done:false},
  {id:'w10',label:'Legend — Wave 10',check:g=>g.wave>=10,done:false},
  {id:'s10k',label:'10000 điểm',check:g=>g.score>=10000,done:false},
  {id:'lv8',label:'Master — Level 8',check:g=>g.level>=8,done:false},
  {id:'boss3',label:'Boss Killer — 3 boss',check:g=>g.bossKills>=3,done:false},
];

/* ── drones ── */
let drones=[];
function addDrone(){drones.push({a:Math.random()*Math.PI*2,orb:60+drones.length*28,cd:0,x:0,y:0,lv:1})}

/* ── vortex & meteors ── */
let vortexActive={on:false,x:0,y:0,timer:0,suck:3};
let meteors=[];
let floatDmg=[];

/* ── game state ── */
let G;
function initGame(cfg){
  cfg=cfg||DIFF_CONFIGS[currentDiff]||DIFF_CONFIGS.normal;
  // close all open panels before starting
  ['shop-panel','talent-panel','skill-panel'].forEach(id=>{
    const el=document.getElementById(id);if(el)el.style.display='none';
  });
  drones=[];vortexActive={on:false};meteors=[];floatDmg=[];takenTalentIds=[];shopTab='combat';
  Object.values(SHOP_DATA).forEach(cat=>cat.forEach(it=>{it.bought=0;it._base=it._base||it.cost;it.cost=Math.round(it._base*(cfg.shopCostMult||1))}));
  const baseHp=Math.round(100*(cfg.playerHpMult||1));
  const baseSh=Math.round(100*(cfg.playerShieldMult||1));
  G={
    p:{x:W()/2,y:H()/2,hp:baseHp,maxHp:baseHp,shield:baseSh,maxShield:baseSh,
      speed:3.1,vx:0,vy:0,angle:0,
      fireRate:14,fireCd:0,bulletSpeed:7.5,multiShot:0,
      lifesteal:0,pierce:false,critChance:0,critMult:3,
      magnet:false,magnetRange:120,missiles:false,blast:false,
      regen:cfg.regenForced||false,regenAmt:3,ricochet:false,
      goldBonus:1,dmgMult:1,xpBonus:1,cdReduction:1,dashPower:1,
      armor:0,thorns:0,shieldFloor:0,chain:false,blackhole:false,timewarp:false,timewarpCd:0,
      burst:false,burstCounter:0,plasma:false,droneDmgMult:1,comboBonus:0,
      skills:{dash:{lv:0,cd:0},nuke:{lv:0,cd:0},shield:{lv:0,cd:0},emp:{lv:0,cd:0},vortex:{lv:0,cd:0},meteor:{lv:0,cd:0}}
    },
    bullets:[],enemies:[],particles:[],powerups:[],orbs:[],trails:[],
    score:0,kills:0,gold:cfg.startGold||0,totalGold:cfg.startGold||0,bossKills:0,
    wave:1,waveKills:0,waveTarget:cfg.waveTarget||8,
    waveBreak:false,waveBreakTimer:0,
    xp:0,xpNeeded:65,level:1,combo:0,comboTimer:0,
    shopOpen:false,talentOpen:false,skillUpOpen:false,
    gameOver:false,won:false,frame:0,
    spawnTimer:60,boss:null,bossSpawned:false,
    diffCfg:cfg,totalWaves:cfg.totalWaves||15,
    stars:[],nebulas:[],dustClouds:[],
  };
  for(let i=0;i<200;i++)G.stars.push({x:Math.random()*1800,y:Math.random()*1000,s:Math.random()*2+.2,b:Math.random(),sp:.04+Math.random()*.4,col:Math.random()<.15?[180,180,255]:[255,255,255]});
  for(let i=0;i<7;i++)G.nebulas.push({x:Math.random()*W(),y:Math.random()*H(),rx:140+Math.random()*200,ry:80+Math.random()*150,hue:Math.random()*360,a:.055+Math.random()*.04});
  for(let i=0;i<12;i++)G.dustClouds.push({x:Math.random()*W(),y:Math.random()*H(),r:30+Math.random()*60,a:.03+Math.random()*.04,sp:.01+Math.random()*.02,hue:Math.random()*360});
  ACH.forEach(a=>a.done=false);
  if(cfg.bossFromWave1)setTimeout(()=>{if(G&&!G.boss)spawnBoss()},3000);
}

/* ── helpers ── */
let msgT=0;
function showMsg(t,d){const e=document.getElementById('msg');e.innerHTML=t;e.style.opacity='1';clearTimeout(msgT);if(d<99999)msgT=setTimeout(()=>e.style.opacity='0',d||2000)}
function showAch(l){const e=document.getElementById('ach');document.getElementById('ach-text').textContent=l;e.style.opacity='1';setTimeout(()=>e.style.opacity='0',3000)}
function checkAch(){ACH.forEach(a=>{if(!a.done&&a.check(G)){a.done=true;showAch(a.label)}})}

function part(x,y,col,n,spd,sz,life){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2,s=(Math.random()+.3)*(spd||3);
    G.particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:life||(28+Math.random()*22),color:col,size:(sz||2)+Math.random()*2,maxLife:life||50});
  }
}

function floatDmgPop(x,y,val,col){
  floatDmg.push({x,y,val,col:col||'#fff',life:45,vy:-1.5});
}

function skillCd(id){
  const sk=G.p.skills[id],def=SKILL_DEFS[id];
  if(!def)return Infinity;
  const lv=sk.lv;
  return def.base.cd + (lv>0?def.perLv.cd*lv:0);
}

function skillVal(id,key){
  const sk=G.p.skills[id],def=SKILL_DEFS[id];
  const lv=sk.lv;
  return def.base[key]+(lv>0&&def.perLv[key]?def.perLv[key]*lv:0);
}

/* ── skills ── */
function doDash(){
  const sk=G.p.skills.dash;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('dash');
  const dx=M.x-G.p.x,dy=M.y-G.p.y,d=Math.hypot(dx,dy)||1;
  const power=skillVal('dash','power');
  G.p.vx+=(dx/d)*15*power;G.p.vy+=(dy/d)*15*power;
  part(G.p.x,G.p.y,'#378add',20,6,3,25);
  // afterimage trail
  for(let i=0;i<5;i++)G.trails.push({x:G.p.x,y:G.p.y,a:.6-i*.1,life:15,angle:G.p.angle});
}

function doNuke(){
  const sk=G.p.skills.nuke;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('nuke');
  const r=skillVal('nuke','radius')*200;
  const dmg=skillVal('nuke','dmg');
  const toKill=[];
  G.enemies.forEach(e=>{if(Math.hypot(e.x-G.p.x,e.y-G.p.y)<r){killEnemy(e);toKill.push(e)}});
  G.enemies=G.enemies.filter(e=>!toKill.includes(e));
  if(G.boss&&Math.hypot(G.boss.x-G.p.x,G.boss.y-G.p.y)<r){G.boss.hp-=dmg;if(G.boss.hp<=0)killBoss()}
  part(G.p.x,G.p.y,'#fac775',120,12,4,35);
  G.particles.push({x:G.p.x,y:G.p.y,ring:true,r:0,maxR:r,life:30,color:'#fac775'});
}

function doShield(){
  const sk=G.p.skills.shield;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('shield');
  const restore=skillVal('shield','restore');
  G.p.shield=Math.min(G.p.maxShield,G.p.shield+restore);
  part(G.p.x,G.p.y,'#0ff',30,4,3,30);
}

function doEMP(){
  const sk=G.p.skills.emp;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('emp');
  const dur=skillVal('emp','dur');
  G.enemies.forEach(e=>{e.frozen=dur;part(e.x,e.y,'#afa9ec',6,2,2,20)});
  if(G.boss)G.boss.frozen=dur*.5;
  part(G.p.x,G.p.y,'#afa9ec',60,7,3,25);
  showMsg('⚡ EMP PULSE',700);
}

function doVortex(){
  const sk=G.p.skills.vortex;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  if(sk.lv===0){showMsg('Nâng cấp Vortex trước! (X)',1200);return}
  sk.cd=skillCd('vortex');
  vortexActive={on:true,x:M.x,y:M.y,timer:skillVal('vortex','dur'),suck:skillVal('vortex','suck')};
  part(M.x,M.y,'#7f77dd',40,5,4,40);
}

function doMeteor(){
  const sk=G.p.skills.meteor;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  if(sk.lv===0){showMsg('Nâng cấp Meteor trước! (X)',1200);return}
  sk.cd=skillCd('meteor');
  const count=skillVal('meteor','count');
  const dmg=skillVal('meteor','dmg');
  for(let i=0;i<count;i++){
    const target=G.enemies.length?G.enemies[Math.floor(Math.random()*G.enemies.length)]:null;
    const tx=target?target.x+Math.random()*60-30:Math.random()*W();
    const ty=target?target.y+Math.random()*60-30:Math.random()*H();
    setTimeout(()=>{
      meteors.push({x:tx-80,y:-60,tx,ty,progress:0,dmg,r:14});
    },i*200);
  }
}

/* ── skill upgrade panel ── */
function openSkillUp(){
  if(G.talentOpen||G.gameOver)return;
  G.skillUpOpen=!G.skillUpOpen;
  const panel=document.getElementById('skill-panel');
  panel.style.display=G.skillUpOpen?'block':'none';
  if(G.skillUpOpen)renderSkillUp();
}

function renderSkillUp(){
  const grid=document.getElementById('su-grid');grid.innerHTML='';
  const ids=['dash','nuke','shield','emp','vortex','meteor'];
  ids.forEach(id=>{
    const def=SKILL_DEFS[id],sk=G.p.skills[id];
    const lv=sk.lv,maxLv=def.maxLv;
    const upgCost=lv===0?30:40+lv*25;
    const d=document.createElement('div');d.className='su-card';
    const canBuy=G.gold>=upgCost&&lv<maxLv;
    d.innerHTML=`<div class="su-icon">${def.icon}</div>
      <div class="su-name">${def.name} <span style="color:#555">Lv${lv}/${maxLv}</span></div>
      <div class="su-desc">${def.desc(lv)}</div>
      <div class="su-lv">${lv<maxLv?`Nâng cấp: ${upgCost}g`:'MAX LEVEL'}</div>`;
    if(canBuy)d.onclick=()=>{G.gold-=upgCost;sk.lv=Math.min(maxLv,sk.lv+1);part(G.p.x,G.p.y,'#d4537e',16,4,3,25);renderSkillUp();updateHUD()};
    else if(lv>=maxLv)d.style.borderColor='#fac77540';
    else d.style.opacity='.4';
    grid.appendChild(d);
  });
}

/* ── fire ── */
function fireBullet(fx,fy,angle,spd,col,dmg,owner,pierce,ricochet,customSize){
  const count=owner==='player'?1+(G.p.multiShot||0):1;
  const isDual=owner==='player'&&G.p.dualCannon&&!customSize;
  const baseAngles=isDual?[angle-.08,angle+.08]:[angle];
  baseAngles.forEach(baseAngle=>{
  for(let i=0;i<count;i++){
    const sp=(i-(count-1)/2)*.17;
    const isCrit=owner==='player'&&Math.random()<(G.p.critChance||0);
    const lastStandMult=owner==='player'&&G.p.lastStand&&G.p.hp/G.p.maxHp<.2?1.5:1;
    const realDmg=(isCrit?dmg*(G.p.critMult||3):dmg)*(owner==='player'?(G.p.dmgMult||1):1)*lastStandMult;
    const sz=customSize||(isCrit?7:4.5);
    let bulletAngle=baseAngle+sp;
    if(owner==='player'&&G.p.homingBullets){
      // homing — find nearest enemy
      let nearest=null,nd=9999;
      G.enemies.forEach(e=>{const d=Math.hypot(e.x-fx,e.y-fy);if(d<nd){nd=d;nearest=e}});
      if(!nearest&&G.boss)nearest=G.boss;
      G.bullets.push({x:fx,y:fy,vx:Math.cos(bulletAngle)*spd,vy:Math.sin(bulletAngle)*spd,
        life:100,size:sz,color:isCrit?'#fac775':col,dmg:realDmg,
        owner,pierce:pierce||false,ricochet:ricochet||false,bounces:0,hit:[],
        chain:owner==='player'&&(G.p.chain||false),chainDone:false,
        freeze:owner==='player'&&(G.p.freezeRounds||false),
        explosive:owner==='player'&&(G.p.explosive||false),
        incendiary:owner==='player'&&(G.p.incendiary||false),
        disruptor:owner==='player'&&(G.p.disruptor||false),
        homing:nearest});
    } else {
      G.bullets.push({x:fx,y:fy,vx:Math.cos(bulletAngle)*spd,vy:Math.sin(bulletAngle)*spd,
        life:100,size:sz,color:isCrit?'#fac775':col,dmg:realDmg,
        owner,pierce:pierce||false,ricochet:ricochet||false,bounces:0,hit:[],
        chain:owner==='player'&&(G.p.chain||false),chainDone:false,
        freeze:owner==='player'&&(G.p.freezeRounds||false),
        explosive:owner==='player'&&(G.p.explosive||false),
        incendiary:owner==='player'&&(G.p.incendiary||false),
        disruptor:owner==='player'&&(G.p.disruptor||false)});
    }
  }
  });
}

function fireHoming(fx,fy){
  let t=null,nd=9999;
  G.enemies.forEach(e=>{const d=Math.hypot(e.x-fx,e.y-fy);if(d<nd){nd=d;t=e}});
  if(t&&nd<420)G.bullets.push({x:fx,y:fy,vx:0,vy:0,life:140,size:6,color:'#1d9e75',dmg:20*(G.p.dmgMult||1),owner:'player',homing:t,pierce:false,ricochet:false,hit:[]});
}

/* ── enemy spawn ── */
function spawnEnemy(){
  const wv=G.wave,side=Math.floor(Math.random()*4);
  let x,y;const pad=40;
  if(side===0){x=Math.random()*W();y=-pad}
  else if(side===1){x=W()+pad;y=Math.random()*H()}
  else if(side===2){x=Math.random()*W();y=H()+pad}
  else{x=-pad;y=Math.random()*H()}
  const roll=Math.random();
  let type;
  if(wv>=9&&roll<.07)type='guardian';
  else if(wv>=6&&roll<.14)type='elite';
  else if(wv>=4&&roll<.22)type='bomber';
  else if(wv>=3&&roll<.32)type='tank';
  else if(wv>=2&&roll<.46)type='fast';
  else type='normal';
  const cfgs={
    normal:{hp:20+wv*10,speed:1.1+wv*.12,size:13,color:'#e24b4a',xp:10,gold:5,shoot:false,shield:0},
    fast:{hp:12+wv*5,speed:2.2+wv*.18,size:10,color:'#fac775',xp:14,gold:7,shoot:true,shootRate:85,shield:0},
    tank:{hp:80+wv*28,speed:.6+wv*.06,size:22,color:'#7f77dd',xp:30,gold:18,shoot:false,shield:30},
    bomber:{hp:30+wv*9,speed:1.3+wv*.1,size:15,color:'#d85a30',xp:22,gold:13,shoot:true,shootRate:58,shotType:'spread',shield:0},
    elite:{hp:140+wv*35,speed:1.6+wv*.13,size:18,color:'#d4537e',xp:55,gold:38,shoot:true,shootRate:42,shotType:'spiral',shield:20},
    guardian:{hp:220+wv*50,speed:.9+wv*.08,size:24,color:'#639922',xp:80,gold:55,shoot:true,shootRate:55,shotType:'burst',shield:50},
  };
  const dc=G.diffCfg||DIFF_CONFIGS.normal;
  const base=cfgs[type];
  const hp=Math.round(base.hp*(dc.enemyHpMult||1));
  const speed=base.speed*(dc.enemySpeedMult||1);
  const gold=Math.round((base.gold||5)*(dc.enemyGoldMult||1));
  // impossible: bullets are poison (green, extra dmg over time tracked on player)
  const isPoisonMode=dc.poisonBullets;
  G.enemies.push({x,y,...base,hp,maxHp:hp,speed,gold,maxShield:base.shield||0,
    type,angle:0,shootCd:0,spiralAngle:0,frozen:0,burstCount:0,
    poisonShot:isPoisonMode&&base.shoot});
}

function spawnBoss(){
  const wv=G.wave;
  const dc=G.diffCfg||DIFF_CONFIGS.normal;
  const x=Math.random()<.5?-80:W()+80,y=H()/2;
  const phase=wv>=10?3:wv>=6?2:1;
  const hp=Math.round((600+wv*180)*(dc.bossHpMult||1));
  const speed=(.8+wv*.08)*(dc.bossSpeedMult||1);
  G.boss={x,y,hp,maxHp:hp,speed,size:40,angle:0,shootCd:0,spiralA:0,frozen:0,phase,currentPhase:1,enrage:false,shield:100,maxShield:100};
  document.getElementById('bossbar').style.display='block';
  showMsg(`⚠️  BOSS PHASE ${phase} — ${dc.label}`,2200);
}

function killEnemy(e){
  const gold=Math.round((e.gold||5)*(G.p.goldBonus||1));
  const xpGain=Math.round(e.xp*(G.p.xpBonus||1));
  const scoreMult=(G.p.scoreMult||1)*(1+Math.floor(G.combo/5));
  G.score+=Math.round(e.xp*scoreMult);
  G.kills++;G.waveKills++;G.gold+=gold;G.totalGold+=gold;
  G.xp+=xpGain;G.combo++;G.comboTimer=150+(G.p.comboBonus||0);
  if(G.p.lifesteal)G.p.hp=Math.min(G.p.maxHp,G.p.hp+G.p.lifesteal);
  if(G.p.blast){
    part(e.x,e.y,'#fac775',20,6,4,30);
    G.enemies.forEach(ne=>{if(ne!==e&&Math.hypot(ne.x-e.x,ne.y-e.y)<65){ne.hp-=35;if(ne.hp<=0)killEnemy(ne)}});
  }
  if(G.p.blackhole&&Math.random()<.05){
    vortexActive={on:true,x:e.x,y:e.y,timer:120,suck:4};
    part(e.x,e.y,'#7f77dd',20,4,3,35);
  }
  if(Math.random()<.14)G.powerups.push({x:e.x,y:e.y,type:Math.random()<.55?'hp':'shield',life:380});
  if(Math.random()<.1)G.orbs.push({x:e.x,y:e.y,val:Math.max(1,Math.round(gold*.5)),life:440});
  part(e.x,e.y,e.color,12,4,2.5,30);
  checkXP();checkAch();
}

function killBoss(){
  part(G.boss.x,G.boss.y,'#e24b4a',100,10,5,45);
  for(let i=0;i<5;i++)G.particles.push({x:G.boss.x,y:G.boss.y,ring:true,r:0,maxR:120+i*40,life:40,color:'#ff5555'});
  G.score+=800;G.gold+=220;G.totalGold+=220;G.kills+=6;G.bossKills++;
  G.boss=null;document.getElementById('bossbar').style.display='none';
  showMsg('💀 BOSS TIÊU DIỆT! +800pts',2500);
  checkAch();
}

function hitPlayer(dmg){
  const p=G.p;
  if(p.invincible||p.timewarpActive)return;
  if(p.evasion&&Math.random()<p.evasion){part(p.x,p.y,'#ffffff',4,3,2,15);return}
  const reduced=dmg*(1-(p.armor||0));
  if(p.thorns>0){
    G.enemies.forEach(e=>{if(Math.hypot(e.x-p.x,e.y-p.y)<80)e.hp-=p.thorns});
  }
  const floor=p.shieldFloor||0;
  if(p.shield>floor){
    const absorbed=Math.min(p.shield-floor,reduced);
    p.shield-=absorbed;
    const overflow=reduced-absorbed;
    if(overflow>0){p.hp=Math.max(0,p.hp-overflow);part(p.x,p.y,'#e24b4a',5,2,2,15)}
    else{part(p.x,p.y,'#378add',5,2,2,15)}
  } else {
    p.hp=Math.max(0,p.hp-reduced);
    part(p.x,p.y,'#e24b4a',5,2,2,15);
  }
  if(p.hp<=0&&!G.gameOver){
    if(p.phoenix&&!p.phoenixUsed){p.phoenixUsed=true;p.hp=p.maxHp*.5;showMsg('🦅 PHOENIX REBORN!',1500);part(p.x,p.y,'#fac775',40,8,4,50);return}
    G.gameOver=true;setTimeout(()=>showEndScreen(false),800)
  }
}

function checkXP(){
  if(G.xp>=G.xpNeeded){G.level++;G.xp=0;G.xpNeeded=Math.floor(G.xpNeeded*1.65);openTalent();checkAch()}
}

function openTalent(){
  G.talentOpen=true;
  const panel=document.getElementById('talent-panel');panel.style.display='block';
  currentTalentTab='atk';
  renderTalentTabs();
  renderTalentGrid();
}

function renderTalentTabs(){
  const tabs=document.getElementById('talent-tabs');tabs.innerHTML='';
  Object.entries(TALENT_CATS).forEach(([key,cat])=>{
    const t=document.createElement('div');t.className='ttab'+(key===currentTalentTab?' active':'');
    t.textContent=cat.label;t.style.color=key===currentTalentTab?cat.color:'';
    t.onclick=()=>{currentTalentTab=key;renderTalentTabs();renderTalentGrid()};
    tabs.appendChild(t);
  });
}

function renderTalentGrid(){
  const grid=document.getElementById('tg');grid.innerHTML='';
  const cat=currentTalentTab;
  const pool=TALENTS.filter(t=>t.cat===cat);
  // pick 3 — prefer not taken (unless stackable and already taken is ok), shuffle
  const notTaken=pool.filter(t=>t.stackable||!takenTalentIds.includes(t.id));
  const taken=pool.filter(t=>!t.stackable&&takenTalentIds.includes(t.id));
  const options=[...notTaken.sort(()=>Math.random()-.5).slice(0,3)];
  // pad with taken ones if not enough
  if(options.length<3)options.push(...taken.sort(()=>Math.random()-.5).slice(0,3-options.length));
  const shown=options.slice(0,3);

  shown.forEach(t=>{
    const alreadyTaken=!t.stackable&&takenTalentIds.includes(t.id);
    const d=document.createElement('div');
    d.className='tc'+(alreadyTaken?' taken':'');
    const hasSynergy=t.synergy&&takenTalentIds.includes(t.synergy);
    const catColor=TALENT_CATS[t.cat].color;
    d.innerHTML=`
      <div class="tc-icon">${t.icon}</div>
      <div class="tc-name" style="color:${catColor}">${t.name}</div>
      <div class="tc-desc">${t.desc}</div>
      <div class="tc-tag ${t.cat}">${t.detail}</div>
      ${hasSynergy?`<div class="tc-synergy">✦ Synergy boost!</div>`:''}
      ${alreadyTaken?'<div style="font-size:9px;color:#fac77580;margin-top:3px">Đã có — chọn lại để stack</div>':''}
    `;
    if(!alreadyTaken){
      d.onclick=()=>{
        t.apply(G.p);
        if(hasSynergy){applyTalentSynergy(t,G.p)}
        takenTalentIds.push(t.id);
        document.getElementById('talent-panel').style.display='none';
        G.talentOpen=false;
        showMsg('★ '+t.name+(hasSynergy?' ✦ SYNERGY!':''),1600);
        part(G.p.x,G.p.y,'#afa9ec',28,5,3,32);
      };
    }
    grid.appendChild(d);
  });

  document.getElementById('talent-synergy-hint').textContent=
    takenTalentIds.length>0?`Bạn có: ${[...new Set(takenTalentIds)].map(id=>TALENTS.find(t=>t.id===id)?.name||id).join(' • ')}`:'';
}

function applyTalentSynergy(talent,p){
  // bonus effects when synergy detected
  if(talent.id==='multishot'&&takenTalentIds.includes('rapidfire'))p.fireRate=Math.max(4,p.fireRate-1);
  if(talent.id==='blast'&&takenTalentIds.includes('vamp'))p.lifesteal+=5;
  if(talent.id==='drone'&&takenTalentIds.includes('missile'))addDrone();
  if(talent.id==='crit'&&takenTalentIds.includes('crit'))p.critMult+=.5;
}

function toggleShop(){
  if(G.talentOpen||G.skillUpOpen||G.gameOver||G.won)return;
  G.shopOpen=!G.shopOpen;
  const s=document.getElementById('shop-panel');s.style.display=G.shopOpen?'block':'none';
  if(G.shopOpen)renderShop();
}

function setShopTab(tab){
  shopTab=tab;
  document.querySelectorAll('.stab').forEach((el,i)=>{
    el.classList.toggle('active',['combat','defense','utility','special'][i]===tab);
  });
  renderShop();
}

function renderShop(){
  document.getElementById('shop-gold').textContent=G.gold;
  const el=document.getElementById('shop-items');el.innerHTML='';
  const items=SHOP_DATA[shopTab]||[];
  const unlockSecret=G.p.blackMarket;

  items.forEach(it=>{
    if(it.secret&&!unlockSecret)return;
    const remaining=it.stock-it.bought;
    const canBuy=G.gold>=it.cost&&remaining>0;
    const d=document.createElement('div');
    d.className='shop-item'+(remaining<=0?' bought':!canBuy?' cant':'');
    d.innerHTML=`
      <span class="si-icon">${it.icon}</span>
      <div class="si-body">
        <div class="si-name">${it.name}</div>
        <div class="si-desc">${it.desc}</div>
        <div class="si-effect">${it.effect}</div>
      </div>
      <div class="si-cost">
        <div class="si-price">${remaining<=0?'SOLD OUT':it.cost+'g'}</div>
        <div class="si-stock">${remaining>0?`còn ${remaining}/${it.stock}`:'—'}</div>
      </div>
    `;
    if(canBuy)d.onclick=()=>{
      G.gold-=it.cost;it.bought++;it.apply(G.p);
      part(G.p.x,G.p.y,'#1d9e75',16,4,3,25);
      renderShop();updateHUD();
    };
    el.appendChild(d);
  });

  if(items.length===0){
    el.innerHTML='<div style="text-align:center;color:#555;padding:20px;font-size:12px">Không có item trong danh mục này</div>';
  }
}

function onKey(code){
  if(code==='KeyE')toggleShop();
  if(code==='KeyX')openSkillUp();
  if(code==='KeyQ')doNuke();
  if(code==='KeyW')doShield();
  if(code==='KeyF')doEMP();
  if(code==='ShiftLeft'||code==='ShiftRight')doDash();
  if(code==='KeyR'&&!G.shopOpen&&!G.talentOpen&&!G.skillUpOpen)doVortex();
  if(code==='KeyT')doMeteor();
  if(code==='Escape'&&(G.shopOpen||G.skillUpOpen)){G.shopOpen=false;G.skillUpOpen=false;document.getElementById('shop-panel').style.display='none';document.getElementById('skill-panel').style.display='none'}
}

/* ── HUD ── */
function updateHUD(){
  const p=G.p;
  document.getElementById('hp-fill').style.width=(p.hp/p.maxHp*100)+'%';
  document.getElementById('sh-fill').style.width=(p.shield/p.maxShield*100)+'%';
  document.getElementById('xp-fill').style.width=(G.xp/G.xpNeeded*100)+'%';
  document.getElementById('hp-val').textContent=Math.ceil(p.hp)+'/'+p.maxHp;
  document.getElementById('sh-val').textContent=Math.ceil(p.shield)+'/'+p.maxShield;
  document.getElementById('lv-val').textContent='Lv'+G.level;
  document.getElementById('wn').textContent=G.wave;
  document.getElementById('sv').textContent=G.score.toLocaleString();
  document.getElementById('kv').textContent=G.kills;
  document.getElementById('gv').textContent=G.gold;
  const sk=p.skills;const ids=['dash','nuke','shield','emp','vortex','meteor'];
  ids.forEach((id,i)=>{
    const s=sk[id];const el=document.getElementById('scd'+i);
    el.textContent=s.cd>0?Math.ceil(s.cd/60)+'s':(s.lv>0?'Lv'+s.lv:'—');
    document.getElementById('sk'+i).style.borderColor=s.cd>0?'#ff333355':s.lv>0?'#0ff5':'#fff2';
  });
  const cel=document.getElementById('combo-el');
  if(G.combo>2){cel.textContent='COMBO ×'+G.combo+(G.combo>10?' 🔥':'');cel.style.opacity='1'}else cel.style.opacity='0';
  if(G.boss){
    document.getElementById('boss-fill').style.width=(G.boss.hp/G.boss.maxHp*100)+'%';
    document.getElementById('boss-name').textContent=(G.boss.enrage?'⚠️ ENRAGED ':'')+`BOSS PHASE ${G.boss.currentPhase}/${G.boss.phase}`;
    document.getElementById('boss-phase').textContent=G.boss.enrage?'RAGE MODE ACTIVE':'';
  }
}

/* ── minimap ── */
function drawMinimap(){
  mctx.clearRect(0,0,110,80);
  mctx.fillStyle='rgba(0,0,15,.65)';mctx.fillRect(0,0,110,80);
  const sx=110/W(),sy=80/H();
  G.enemies.forEach(e=>{mctx.fillStyle=e.color;mctx.fillRect(e.x*sx-1.5,e.y*sy-1.5,3,3)});
  if(G.boss){mctx.fillStyle='#ff3333';mctx.beginPath();mctx.arc(G.boss.x*sx,G.boss.y*sy,5,0,Math.PI*2);mctx.fill()}
  G.powerups.forEach(p=>{mctx.fillStyle=p.type==='hp'?'#e24b4a':'#378add';mctx.fillRect(p.x*sx,p.y*sy,3,3)});
  drones.forEach(d=>{mctx.fillStyle='#1d9e75';mctx.fillRect(d.x*sx-1,d.y*sy-1,2,2)});
  mctx.fillStyle='#0ff';mctx.beginPath();mctx.arc(G.p.x*sx,G.p.y*sy,3,0,Math.PI*2);mctx.fill();
  // scan line
  const sl=(G.frame*.02)%(Math.PI*2);
  mctx.strokeStyle='rgba(0,255,150,.2)';mctx.lineWidth=1;
  mctx.beginPath();mctx.moveTo(G.p.x*sx,G.p.y*sy);
  mctx.lineTo(G.p.x*sx+Math.cos(sl)*50,G.p.y*sy+Math.sin(sl)*50);mctx.stroke();
}

/* ── draw background ── */
function drawBG(){
  ctx.fillStyle='#000005';ctx.fillRect(0,0,W(),H());
  // nebulas
  G.nebulas.forEach((n,i)=>{
    const ox=Math.sin(G.frame*.0016+i)*18,oy=Math.cos(G.frame*.0012+i)*14;
    const grd=ctx.createRadialGradient(n.x+ox,n.y+oy,8,n.x+ox,n.y+oy,n.rx);
    grd.addColorStop(0,`hsla(${n.hue},80%,55%,${n.a})`);grd.addColorStop(.5,`hsla(${n.hue+30},60%,40%,${n.a*.5})`);grd.addColorStop(1,'transparent');
    ctx.fillStyle=grd;ctx.save();ctx.scale(1,n.ry/n.rx);ctx.beginPath();ctx.arc(n.x+ox,(n.y+oy)*n.rx/n.ry,n.rx,0,Math.PI*2);ctx.fill();ctx.restore();
  });
  // dust
  G.dustClouds.forEach((dc,i)=>{
    const x=(dc.x+G.frame*dc.sp*.3)%(W()+80);
    const grd=ctx.createRadialGradient(x,dc.y,4,x,dc.y,dc.r);
    grd.addColorStop(0,`hsla(${dc.hue},50%,60%,${dc.a})`);grd.addColorStop(1,'transparent');
    ctx.fillStyle=grd;ctx.beginPath();ctx.arc(x,dc.y,dc.r,0,Math.PI*2);ctx.fill();
  });
  // stars
  G.stars.forEach(s=>{
    const px=(s.x+G.frame*s.sp*.5)%(W()+50);
    const py=(s.y+G.frame*s.sp*.18)%(H()+50);
    const flicker=.55+Math.sin(G.frame*.04+s.b*9)*.45;
    const [r,g,b]=s.col;
    ctx.fillStyle=`rgba(${r},${g},${b},${s.b*flicker*.9})`;
    ctx.beginPath();ctx.arc(px,py,s.s,0,Math.PI*2);ctx.fill();
    if(s.s>1.4){
      ctx.strokeStyle=`rgba(${r},${g},${b},${s.b*.25*flicker})`;ctx.lineWidth=.5;
      ctx.beginPath();ctx.moveTo(px-s.s*2.5,py);ctx.lineTo(px+s.s*2.5,py);ctx.moveTo(px,py-s.s*2.5);ctx.lineTo(px,py+s.s*2.5);ctx.stroke();
    }
  });
}

/* ── draw player ── */
function drawPlayer(){
  const p=G.p;
  // trails (afterimage)
  G.trails.forEach(t=>{
    ctx.save();ctx.translate(t.x,t.y);ctx.rotate(t.angle);ctx.globalAlpha=t.a*.5;
    ctx.fillStyle='#378add';ctx.beginPath();ctx.moveTo(19,0);ctx.lineTo(-11,12);ctx.lineTo(-6,0);ctx.lineTo(-11,-12);ctx.closePath();ctx.fill();
    ctx.restore();
  });
  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.angle);
  // engine glow
  ctx.fillStyle='rgba(0,180,255,0.08)';ctx.beginPath();ctx.arc(0,0,28,0,Math.PI*2);ctx.fill();
  // wings
  ctx.fillStyle='#8899aa';ctx.beginPath();ctx.moveTo(-5,-6);ctx.lineTo(-14,-18);ctx.lineTo(-10,-7);ctx.closePath();ctx.fill();
  ctx.beginPath();ctx.moveTo(-5,6);ctx.lineTo(-14,18);ctx.lineTo(-10,7);ctx.closePath();ctx.fill();
  // body
  ctx.fillStyle='#c0c8d8';ctx.beginPath();ctx.moveTo(20,0);ctx.lineTo(-12,13);ctx.lineTo(-7,0);ctx.lineTo(-12,-13);ctx.closePath();ctx.fill();
  // center stripe
  ctx.fillStyle='#ffffff20';ctx.beginPath();ctx.moveTo(16,0);ctx.lineTo(-8,8);ctx.lineTo(-5,0);ctx.lineTo(-8,-8);ctx.closePath();ctx.fill();
  // engine pods
  ctx.fillStyle='#4466aa';ctx.fillRect(-15,-14,8,7);ctx.fillRect(-15,7,8,7);
  // cockpit
  ctx.fillStyle='rgba(0,230,255,0.95)';ctx.beginPath();ctx.arc(7,0,6,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.7)';ctx.beginPath();ctx.arc(8.5,-1.5,2.2,0,Math.PI*2);ctx.fill();
  // thruster flame
  const fl=5+Math.random()*7;
  ctx.fillStyle='rgba(255,140,30,0.9)';ctx.beginPath();ctx.moveTo(-12,-4.5);ctx.lineTo(-12-fl,0);ctx.lineTo(-12,4.5);ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(255,220,100,0.6)';ctx.beginPath();ctx.moveTo(-12,-2.5);ctx.lineTo(-12-fl*.55,0);ctx.lineTo(-12,2.5);ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(200,230,255,0.8)';ctx.beginPath();ctx.moveTo(-12,-1);ctx.lineTo(-12-fl*.25,0);ctx.lineTo(-12,1);ctx.closePath();ctx.fill();
  ctx.restore();

  // shield ring
  if(G.p.skills.shield.cd>130){
    const alpha=.5+Math.sin(G.frame*.3)*.3;
    ctx.strokeStyle=`rgba(0,255,255,${alpha})`;ctx.lineWidth=2.5;
    ctx.beginPath();ctx.arc(p.x,p.y,34,0,Math.PI*2);ctx.stroke();
    ctx.strokeStyle=`rgba(0,255,255,${alpha*.3})`;ctx.lineWidth=6;
    ctx.stroke();
  }
  // vortex indicator
  if(vortexActive.on){
    ctx.strokeStyle='rgba(127,119,221,0.6)';ctx.lineWidth=1.5;
    ctx.setLineDash([4,4]);
    ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(vortexActive.x,vortexActive.y);ctx.stroke();
    ctx.setLineDash([]);
  }
}

function drawEnemy(e){
  if(e.frozen>0){ctx.save();ctx.globalAlpha=.5+.4*Math.abs(Math.sin(G.frame*.35))}
  ctx.save();ctx.translate(e.x,e.y);
  const spin=e.type==='tank'?.022:.05;e.angle+=spin;ctx.rotate(e.angle);
  ctx.fillStyle=e.color;
  if(e.type==='guardian'){
    // hexagonal with inner detail
    ctx.beginPath();for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.25)';ctx.lineWidth=1.5;ctx.stroke();
    ctx.fillStyle='rgba(0,0,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.5,0,Math.PI*2);ctx.fill();
    ctx.fillStyle=e.color+'88';ctx.beginPath();ctx.arc(0,0,e.size*.25,0,Math.PI*2);ctx.fill();
  } else if(e.type==='tank'){
    ctx.beginPath();for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(0,0,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.42,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.2)';ctx.lineWidth=1;ctx.stroke();
  } else if(e.type==='fast'){
    ctx.beginPath();ctx.moveTo(e.size,0);ctx.lineTo(-e.size*.6,e.size*.9);ctx.lineTo(-e.size*.3,0);ctx.lineTo(-e.size*.6,-e.size*.9);ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.15)';ctx.beginPath();ctx.moveTo(e.size*.5,0);ctx.lineTo(-e.size*.3,e.size*.4);ctx.lineTo(-e.size*.1,0);ctx.lineTo(-e.size*.3,-e.size*.4);ctx.closePath();ctx.fill();
  } else if(e.type==='bomber'){
    ctx.beginPath();for(let i=0;i<5;i++){const a=i/5*Math.PI*2-Math.PI/2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,80,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.38,0,Math.PI*2);ctx.fill();
  } else if(e.type==='elite'){
    ctx.beginPath();for(let i=0;i<8;i++){const a=i/8*Math.PI*2,r=i%2===0?e.size:e.size*.5;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.3)';ctx.lineWidth=1;ctx.stroke();
    ctx.fillStyle='rgba(255,255,255,.12)';ctx.beginPath();ctx.arc(0,0,e.size*.3,0,Math.PI*2);ctx.fill();
  } else {
    ctx.beginPath();for(let i=0;i<4;i++){const a=i/4*Math.PI*2+Math.PI/4;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.18)';ctx.lineWidth=1;ctx.stroke();
  }
  ctx.restore();
  // shield
  if(e.shield>0){
    ctx.strokeStyle=`rgba(55,138,221,${.4+e.shield/e.maxShield*.4})`;ctx.lineWidth=2;
    ctx.beginPath();ctx.arc(e.x,e.y,e.size+5,0,Math.PI*2);ctx.stroke();
  }
  if(e.frozen>0)ctx.restore();
  // burn effect
  if(e.burning>0){ctx.strokeStyle=`rgba(255,100,0,${.5+Math.sin(G.frame*.2)*.3})`;ctx.lineWidth=2;ctx.beginPath();ctx.arc(e.x,e.y,e.size+3,0,Math.PI*2);ctx.stroke()}
  // disrupted effect
  if(e.disrupted>0){ctx.strokeStyle=`rgba(80,80,255,.35)`;ctx.lineWidth=1.5;ctx.setLineDash([3,3]);ctx.beginPath();ctx.arc(e.x,e.y,e.size+7,0,Math.PI*2);ctx.stroke();ctx.setLineDash([])}
  // HP bar
  const bw=e.size*2.5,bh=4;
  ctx.fillStyle='#fff1';ctx.fillRect(e.x-bw/2,e.y-e.size-12,bw,bh);
  ctx.fillStyle=e.color;ctx.fillRect(e.x-bw/2,e.y-e.size-12,bw*(e.hp/e.maxHp),bh);
}

function drawBoss(b){
  const enrage=b.enrage;
  ctx.save();ctx.translate(b.x,b.y);
  const outerSpin=G.frame*.025*(enrage?2:1);ctx.rotate(outerSpin);
  // outer ring
  ctx.strokeStyle=enrage?'rgba(255,60,0,.35)':'rgba(226,75,74,.2)';ctx.lineWidth=3;
  ctx.beginPath();for(let i=0;i<16;i++){const a=i/16*Math.PI*2,r=i%2===0?b.size*2:b.size*1.7;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.stroke();
  ctx.restore();
  ctx.save();ctx.translate(b.x,b.y);b.angle+=enrage?.055:.032;ctx.rotate(b.angle);
  const pulse=.94+Math.sin(G.frame*.09)*.06;
  // aura
  ctx.fillStyle=enrage?'rgba(255,50,50,.12)':'rgba(226,75,74,.1)';
  ctx.beginPath();ctx.arc(0,0,b.size*1.85*pulse,0,Math.PI*2);ctx.fill();
  // body
  ctx.fillStyle=enrage?'#ff2222':'#e24b4a';
  ctx.beginPath();for(let i=0;i<12;i++){const a=i/12*Math.PI*2,r=i%2===0?b.size:b.size*.55;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();
  if(enrage){ctx.strokeStyle='rgba(255,200,0,.6)';ctx.lineWidth=2.5;ctx.stroke()}
  // inner
  ctx.fillStyle='rgba(255,80,80,.7)';ctx.beginPath();ctx.arc(0,0,b.size*.38,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='rgba(255,200,200,.5)';ctx.beginPath();ctx.arc(0,0,b.size*.16,0,Math.PI*2);ctx.fill();
  ctx.restore();
  // HP bar above
  const bw=90,bh=7;
  ctx.fillStyle='#fff1';ctx.fillRect(b.x-bw/2,b.y-b.size-16,bw,bh);
  ctx.fillStyle=enrage?'#ff2222':'#e24b4a';ctx.fillRect(b.x-bw/2,b.y-b.size-16,bw*(b.hp/b.maxHp),bh);
  // shield ring
  if(b.shield>0){
    ctx.strokeStyle=`rgba(55,138,221,${.3+b.shield/b.maxShield*.4})`;ctx.lineWidth=3;
    ctx.beginPath();ctx.arc(b.x,b.y,b.size+10,0,Math.PI*2);ctx.stroke();
  }
}

function drawDrone(dr){
  const x=G.p.x+Math.cos(dr.a)*dr.orb,y=G.p.y+Math.sin(dr.a)*dr.orb;
  dr.x=x;dr.y=y;
  // orbit path
  ctx.strokeStyle='rgba(29,158,117,.18)';ctx.lineWidth=1;ctx.setLineDash([3,7]);
  ctx.beginPath();ctx.arc(G.p.x,G.p.y,dr.orb,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);
  // body
  ctx.save();ctx.translate(x,y);ctx.rotate(G.frame*.06);
  ctx.fillStyle='#1d9e75';
  ctx.beginPath();for(let i=0;i<4;i++){const a=i/4*Math.PI*2+Math.PI/4;ctx.lineTo(Math.cos(a)*7,Math.sin(a)*7)}ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(93,202,165,.8)';ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();
  ctx.restore();
}

function drawMeteors(){
  meteors.forEach(m=>{
    m.progress=Math.min(1,m.progress+.025);
    m.x=m.x+(m.tx-m.x)*m.progress;m.y=m.y+(m.ty-m.y)*m.progress;
    // trail
    ctx.strokeStyle='rgba(255,100,30,.6)';ctx.lineWidth=3;
    ctx.beginPath();ctx.moveTo(m.x-Math.cos(Math.atan2(m.ty-(-60),m.tx-(m.x-80)))*20,m.y-Math.sin(Math.atan2(m.ty-(-60),m.tx-(m.x-80)))*20);
    ctx.lineTo(m.x,m.y);ctx.stroke();
    ctx.fillStyle='#fac775';ctx.beginPath();ctx.arc(m.x,m.y,m.r,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='rgba(255,200,100,.6)';ctx.beginPath();ctx.arc(m.x,m.y,m.r*.5,0,Math.PI*2);ctx.fill();
    // impact
    if(m.progress>=1){
      part(m.x,m.y,'#fac775',20,7,4,30);
      G.enemies.forEach(e=>{if(Math.hypot(e.x-m.x,e.y-m.y)<50){e.hp-=m.dmg;floatDmgPop(e.x,e.y-20,Math.round(m.dmg),'#fac775');if(e.hp<=0){killEnemy(e)}}});
      if(G.boss&&Math.hypot(G.boss.x-m.x,G.boss.y-m.y)<60){G.boss.hp-=m.dmg;if(G.boss.hp<=0)killBoss()}
    }
  });
  meteors=meteors.filter(m=>m.progress<1);
}

function drawVortex(){
  if(!vortexActive.on)return;
  vortexActive.timer--;
  if(vortexActive.timer<=0){vortexActive.on=false;return}
  const {x,y,suck}=vortexActive;
  const spin=G.frame*.12,r=80;
  // rings
  for(let i=0;i<3;i++){
    ctx.strokeStyle=`rgba(127,119,221,${.3-i*.08})`;ctx.lineWidth=2-i*.4;
    ctx.beginPath();ctx.arc(x,y,r*(i+1)*.4,0,Math.PI*2);ctx.stroke();
  }
  // spiraling particles
  for(let i=0;i<3;i++){
    const a=spin+i*2.094;const pr=r*(.3+Math.abs(Math.sin(G.frame*.08+i))*.7);
    ctx.fillStyle='rgba(175,169,236,.7)';ctx.beginPath();ctx.arc(x+Math.cos(a)*pr,y+Math.sin(a)*pr,3,0,Math.PI*2);ctx.fill();
  }
  // suck enemies
  G.enemies.forEach(e=>{
    const dx=x-e.x,dy=y-e.y,dist=Math.hypot(dx,dy)||1;
    if(dist<r*2){e.x+=dx/dist*suck;e.y+=dy/dist*suck}
  });
}

function drawParticles(){
  G.particles.forEach(p=>{
    if(p.ring){
      p.r+=(p.maxR-p.r)*.12;p.life--;
      ctx.strokeStyle=p.color.replace(')',`,${p.life/30})`).replace('rgb','rgba').replace('#','rgba(').replace(/^rgba\(([^)]+)\)$/,(m,c)=>{const hex=parseInt(c,16);return`rgba(${hex>>16&255},${hex>>8&255},${hex&255},${p.life/30})`});
      ctx.strokeStyle=p.color;ctx.globalAlpha=p.life/30;ctx.lineWidth=3;
      ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.stroke();ctx.globalAlpha=1;
    } else {
      p.x+=p.vx;p.y+=p.vy;p.vx*=.89;p.vy*=.89;p.life--;
      ctx.globalAlpha=p.life/45;ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;
    }
  });
  G.particles=G.particles.filter(p=>p.life>0);
}

/* ── main update ── */
function update(){
  if(!G||G.gameOver||G.won||G.talentOpen)return;
  G.frame++;
  const p=G.p;
  // movement
  let mx=0,my=0;
  if(!G.shopOpen&&!G.skillUpOpen){
    if(keys['ArrowLeft']||keys['KeyA'])mx=-1;
    if(keys['ArrowRight']||keys['KeyD'])mx=1;
    if(keys['ArrowUp']||keys['KeyW'])my=-1;
    if(keys['ArrowDown']||keys['KeyS'])my=1;
  }
  if(mx||my){const l=Math.hypot(mx,my)||1;p.vx=mx/l*p.speed;p.vy=my/l*p.speed}
  p.vx*=.82;p.vy*=.82;
  p.x=Math.max(20,Math.min(W()-20,p.x+p.vx));
  p.y=Math.max(20,Math.min(H()-20,p.y+p.vy));
  p.angle=Math.atan2(M.y-p.y,M.x-p.x);

  // passive
  const shieldSk=p.skills.shield;
  if(shieldSk.cd===0)p.shield=Math.min(p.maxShield,p.shield+.08);
  if(p.regen&&!(G.diffCfg&&G.diffCfg.noRegen)&&G.frame%60===0)p.hp=Math.min(p.maxHp,p.hp+(p.regenAmt||3));
  // shield regen talent
  if(p.shieldRegen&&G.frame%60===0)p.shield=Math.min(p.maxShield,p.shield+(p.shieldRegenAmt||3));
  // emergency repair
  if(p.emergencyRepair&&!p.emergencyRepairUsed&&p.hp<p.maxHp*.25){p.emergencyRepairUsed=true;p.hp=Math.min(p.maxHp,p.hp+80);showMsg('🔧 EMERGENCY REPAIR',1000);part(p.x,p.y,'#1d9e75',20,5,3,30)}
  // last stand
  if(p.lastStand)p._lastStandMult=p.hp/p.maxHp<.2?1.5:1;

  // shockwave passive
  if(p.shockwave){
    p.shockwaveCd=(p.shockwaveCd||0)+1;
    if(p.shockwaveCd>=360){p.shockwaveCd=0;
      G.enemies.forEach(e=>{const dx=e.x-p.x,dy=e.y-p.y,dist=Math.hypot(dx,dy)||1;if(dist<180){e.x+=dx/dist*60;e.y+=dy/dist*60;e.hp-=15}});
      part(p.x,p.y,'#378add',30,8,3,40);showMsg('🌊 SHOCKWAVE',600);}
  }
  // gravity well passive
  if(p.gravityWell){
    p.gravityWellCd=(p.gravityWellCd||0)+1;
    if(p.gravityWellCd>=600){p.gravityWellCd=0;
      G.enemies.forEach(e=>{const dx=p.x-e.x,dy=p.y-e.y,dist=Math.hypot(dx,dy)||1;if(dist<400){e.x+=dx/dist*80;e.y+=dy/dist*80}});
      if(G.boss){const dx=p.x-G.boss.x,dy=p.y-G.boss.y,dist=Math.hypot(dx,dy)||1;G.boss.x+=dx/dist*30;G.boss.y+=dy/dist*30}
      part(p.x,p.y,'#7f77dd',50,10,4,60);showMsg('🌍 GRAVITY WELL',700);}
  }
  // supernova
  if(p.supernova&&!p.supernovaUsed&&p.hp/p.maxHp<.1){
    p.supernovaUsed=true;
    G.enemies.forEach(e=>{if(Math.hypot(e.x-p.x,e.y-p.y)<350){e.hp-=500}});
    part(p.x,p.y,'#fac775',80,12,5,80);showMsg('🌟 SUPERNOVA!!!',2000);
  }
  // turret auto-fire
  if(p.turret&&G.frame%60===0){
    let nearest=null,nd=999;
    G.enemies.forEach(e=>{const d=Math.hypot(e.x-p.x,e.y-p.y);if(d<nd){nd=d;nearest=e}});
    if(!nearest&&G.boss)nearest=G.boss;
    if(nearest){const a=Math.atan2(nearest.y-p.y,nearest.x-p.x);fireBullet(p.x,p.y,a,8,'#fac775',18,'player',p.pierce,false)}
  }

  // timewarp passive
  if(p.timewarp){
    p.timewarpCd=(p.timewarpCd||0)+1;
    if(p.timewarpCd>=480){p.timewarpCd=0;p.timewarpActive=true;setTimeout(()=>{p.timewarpActive=false},2000);showMsg('⏰ TIME WARP',700);part(p.x,p.y,'#afa9ec',30,4,3,40)}
  }

  // cooldowns — apply cdReduction multiplier
  const cdr=p.cdReduction||1;
  Object.values(p.skills).forEach(sk=>{if(sk.cd>0)sk.cd=Math.max(0,sk.cd-1/cdr)});

  // fire
  p.fireCd--;
  if(M.dn&&p.fireCd<=0&&!G.shopOpen&&!G.skillUpOpen){
    p.fireCd=p.fireRate;
    const bSize=p.plasma?7:undefined;
    const bDmg=p.plasma?18:10;
    const bCol=p.plasma?'#378add':'#0af';
    fireBullet(p.x,p.y,p.angle,p.bulletSpeed,bCol,bDmg,'player',p.pierce,p.ricochet,bSize);
    // burst fire
    if(p.burst){
      p.burstCounter=(p.burstCounter||0)+1;
      if(p.burstCounter%5===0){
        for(let i=-1;i<=1;i++)fireBullet(p.x,p.y,p.angle+i*.25,p.bulletSpeed*1.1,'#fac775',14,'player',p.pierce,false);
      }
    }
    if(p.missiles&&G.frame%52===0)fireHoming(p.x,p.y);
  }

  if(G.comboTimer>0)G.comboTimer--;else G.combo=0;

  // isPoisonMode must be declared BEFORE bullet filter (used inside callback)
  const isPoisonMode=G.diffCfg&&G.diffCfg.poisonBullets;

  // bullets
  G.bullets=G.bullets.filter(b=>{
    if(b.homing&&b.homing&&b.homing.hp>0){
      const tx=b.homing.x-b.x,ty=b.homing.y-b.y,d=Math.hypot(tx,ty)||1;
      b.vx+=tx/d*.55;b.vy+=ty/d*.55;const sp=Math.hypot(b.vx,b.vy);if(sp>8){b.vx=b.vx/sp*8;b.vy=b.vy/sp*8}
    }
    b.x+=b.vx;b.y+=b.vy;b.life--;
    if(b.life<=0||b.x<-40||b.x>W()+40||b.y<-40||b.y>H()+40){
      if(b.ricochet&&b.bounces<2){
        if(b.x<-20||b.x>W()+20)b.vx*=-1;else b.vy*=-1;
        b.bounces++;b.life=80;return true;
      }
      return false;
    }
    if(b.owner==='player'){
      const toKill=[];
      for(let i=G.enemies.length-1;i>=0;i--){
        const e=G.enemies[i];
        if(b.hit.includes(e))continue;
        if(Math.hypot(b.x-e.x,b.y-e.y)<e.size+b.size){
          const dmg=e.frozen>0?b.dmg*1.6:b.dmg;
          if(e.shield>0){e.shield=Math.max(0,e.shield-dmg*.5)}
          else{e.hp-=dmg;floatDmgPop(e.x,e.y-e.size-15,Math.round(dmg),b.color)}
          part(b.x,b.y,b.color,4,3,2,18);
          // freeze on hit
          if(b.freeze){e.frozen=Math.max(e.frozen||0,120);part(e.x,e.y,'#aaddff',6,2,2,20)}
          // explosive on hit
          if(b.explosive){part(e.x,e.y,'#fac775',14,6,4,25);G.enemies.forEach(ne=>{if(ne!==e&&Math.hypot(ne.x-e.x,ne.y-e.y)<40){ne.hp-=b.dmg*.5;floatDmgPop(ne.x,ne.y-ne.size-10,Math.round(b.dmg*.5),'#fac775')}})}
          // incendiary on hit
          if(b.incendiary){e.burning=(e.burning||0)+180;e.burnDmg=b.dmg*.15}
          // disruptor on hit
          if(b.disruptor){e.disrupted=180}
          // chain bolt — arc to 2 nearby enemies
          if(b.chain&&!b.chainDone){
            b.chainDone=true;
            let chained=0;
            G.enemies.forEach(ne=>{if(ne!==e&&chained<2&&Math.hypot(ne.x-e.x,ne.y-e.y)<120){ne.hp-=b.dmg*.5;chained++;part(ne.x,ne.y,'#afa9ec',5,3,2,15);floatDmgPop(ne.x,ne.y-ne.size-10,Math.round(b.dmg*.5),'#afa9ec')}});
          }
          if(e.hp<=0)toKill.push(i);
          if(b.pierce){b.hit.push(e)}else{
            const kens=[...toKill];kens.forEach(idx=>{const en=G.enemies[idx];killEnemy(en)});
            G.enemies=G.enemies.filter((_,i)=>!toKill.includes(i));return false;
          }
        }
      }
      if(toKill.length){const kens=toKill.map(i=>G.enemies[i]);kens.forEach(en=>killEnemy(en));G.enemies=G.enemies.filter((_,i)=>!toKill.includes(i))}
      if(G.boss&&Math.hypot(b.x-G.boss.x,b.y-G.boss.y)<G.boss.size+b.size){
        if(G.boss.shield>0)G.boss.shield=Math.max(0,G.boss.shield-b.dmg*.4);
        else{G.boss.hp-=b.dmg;floatDmgPop(G.boss.x,G.boss.y-G.boss.size-15,Math.round(b.dmg),'#ff5555')}
        part(b.x,b.y,b.color,3,3,2,15);if(G.boss.hp<=0)killBoss();return false;
      }
    } else {
      if(Math.hypot(b.x-p.x,b.y-p.y)<22){
        hitPlayer(b.dmg);
        if(isPoisonMode&&b.color==='#5dcaa5')p.poisonStacks=Math.min(5,(p.poisonStacks||0)+1);
        return false;
      }
    }
    return true;
  });

  // enemies AI
  const twSlow=G.p.timewarpActive?0.35:1;
  // poison tick
  if(isPoisonMode&&G.p.poisonStacks&&G.frame%90===0){
    G.p.hp=Math.max(1,G.p.hp-G.p.poisonStacks*3);
    part(G.p.x,G.p.y,'#5dcaa5',4,2,2,18);
    G.p.poisonStacks=Math.max(0,G.p.poisonStacks-1);
  }
  G.enemies.forEach(e=>{
    // burning tick
    if(e.burning>0){e.burning--;if(e.burning%20===0){e.hp-=(e.burnDmg||2);floatDmgPop(e.x,e.y-e.size-10,Math.round(e.burnDmg||2),'#ff6600');part(e.x,e.y,'#ff6600',3,2,2,12)}}
    // disrupted: enemy deals 40% less damage (checked in hitPlayer via enemy property)
    if(e.disrupted>0)e.disrupted--;
    if(e.frozen>0){e.frozen--;return}
    const dx=p.x-e.x,dy=p.y-e.y,dist=Math.hypot(dx,dy)||1;
    e.x+=dx/dist*e.speed*twSlow;e.y+=dy/dist*e.speed*twSlow;
    if(dist<28)hitPlayer(1.5);
    if(e.shoot){
      e.shootCd-=twSlow;
      if(e.shootCd<=0){
        e.shootCd=e.shootRate||80;
        const angle=Math.atan2(dy,dx);
        const bcol=isPoisonMode?'#5dcaa5':null;
        if(e.shotType==='spread'){for(let i=-1;i<=1;i++)fireBullet(e.x,e.y,angle+i*.3,3.2*twSlow,bcol||'#d85a30',7,'enemy')}
        else if(e.shotType==='spiral'){e.spiralAngle+=.42;for(let i=0;i<3;i++)fireBullet(e.x,e.y,e.spiralAngle+i*2.094,3.3*twSlow,bcol||'#d4537e',7,'enemy')}
        else if(e.shotType==='burst'){
          e.burstCount=(e.burstCount||0)+1;
          fireBullet(e.x,e.y,angle+(Math.random()-.5)*.2,3.8*twSlow,bcol||'#97c459',9,'enemy');
          if(e.burstCount%3===0)for(let i=0;i<6;i++)fireBullet(e.x,e.y,i/6*Math.PI*2,2.8*twSlow,bcol||'#639922',7,'enemy');
        }
        else fireBullet(e.x,e.y,angle+(Math.random()-.5)*.15,3.2*twSlow,bcol||'#fac775',7,'enemy');
      }
    }
  });

  // boss AI
  if(G.boss){
    const b=G.boss;
    if(b.frozen>0){b.frozen--}
    else{
      const dx=p.x-b.x,dy=p.y-b.y,dist=Math.hypot(dx,dy)||1;
      b.x+=dx/dist*b.speed;b.y+=dy/dist*b.speed;
      if(dist<52)hitPlayer(2.5);
      const enrage=b.hp<b.maxHp*.3;
      if(!b.enrage&&enrage){b.enrage=true;b.currentPhase=Math.min(b.phase,b.currentPhase+1);showMsg('⚠️ BOSS ENRAGED!',2000)}
      b.shootCd--;
      if(b.shootCd<=0){
        b.shootCd=enrage?18:32;b.spiralA+=enrage?.62:.34;
        const shots=enrage?7:4,spd=enrage?4.5:3.8;
        for(let i=0;i<shots;i++)fireBullet(b.x,b.y,b.spiralA+i*(Math.PI*2/shots),spd,'#ff5555',enrage?16:12,'enemy');
        if(enrage&&G.frame%90===0)for(let i=0;i<12;i++)fireBullet(b.x,b.y,i/12*Math.PI*2,2.5,'#ff3333',8,'enemy');
      }
      if(b.shield>0)b.shield=Math.min(b.maxShield,b.shield+.05);
    }
  }

  // drones
  drones.forEach(dr=>{
    dr.a+=.038;dr.cd--;
    const x=G.p.x+Math.cos(dr.a)*dr.orb,y=G.p.y+Math.sin(dr.a)*dr.orb;dr.x=x;dr.y=y;
    if(dr.cd<=0){
      dr.cd=32;let near=null,nd=9999;
      G.enemies.forEach(e=>{const d=Math.hypot(e.x-x,e.y-y);if(d<nd){nd=d;near=e}});
      if(near&&nd<250)fireBullet(x,y,Math.atan2(near.y-y,near.x-x),7.5,'#1d9e75',10*(G.p.dmgMult||1),'player');
    }
  });

  // powerups & orbs
  G.powerups=G.powerups.filter(pu=>{
    pu.life--;
    const d=Math.hypot(pu.x-p.x,pu.y-p.y);
    if(p.magnet&&d<130){pu.x+=(p.x-pu.x)*.14;pu.y+=(p.y-pu.y)*.14}
    if(d<20){if(pu.type==='hp')p.hp=Math.min(p.maxHp,p.hp+30);else p.shield=Math.min(p.maxShield,p.shield+40);part(pu.x,pu.y,pu.type==='hp'?'#e24b4a':'#378add',10,3,2.5,25);return false}
    return pu.life>0;
  });
  G.orbs=G.orbs.filter(o=>{
    o.life--;
    const d=Math.hypot(o.x-p.x,o.y-p.y);
    if(p.magnet&&d<140){o.x+=(p.x-o.x)*.12;o.y+=(p.y-o.y)*.12}
    if(d<24){G.gold+=o.val;G.totalGold+=o.val;part(o.x,o.y,'#fac775',6,3,2,20);return false}
    return o.life>0;
  });

  // trails
  G.trails=G.trails.filter(t=>{t.life--;t.a-=.04;return t.life>0});
  // floatdmg
  floatDmg=floatDmg.filter(f=>{f.y+=f.vy;f.life--;return f.life>0});

  // wave management
  if(!G.waveBreak&&!G.boss){
    G.spawnTimer--;
    const rate=Math.max(20,105-G.wave*9),maxE=Math.min(28,10+G.wave*2);
    if(G.spawnTimer<=0&&G.enemies.length<maxE){G.spawnTimer=rate;spawnEnemy()}
    if(G.waveKills>=G.waveTarget&&G.enemies.length===0){
      if(G.wave%3===0&&!G.bossSpawned){G.bossSpawned=true;spawnBoss()}
      else if(G.wave%3!==0||!G.boss){
        G.waveBreak=true;G.waveBreakTimer=220;G.bossSpawned=false;
        const waveBonusGold=(G.p.waveBonus||0);
        const baseGold=20+G.wave*8;
        showMsg(`✓ WAVE ${G.wave} CLEAR!\n+${baseGold+waveBonusGold} vàng`,2200);
        G.gold+=baseGold+waveBonusGold;G.totalGold+=baseGold+waveBonusGold;
        G.wave++;G.waveKills=0;G.waveTarget=Math.ceil(G.waveTarget*1.3);
        if(G.wave>G.totalWaves){G.won=true;setTimeout(()=>showEndScreen(true),800)}
        checkAch();
      }
    }
  } else if(G.waveBreak){G.waveBreakTimer--;if(G.waveBreakTimer<=0){G.waveBreak=false;G.spawnTimer=90}}

  checkAch();updateHUD();drawMinimap();
}

/* ── draw ── */
function draw(){
  if(!G)return;
  drawBG();
  // vortex
  drawVortex();
  // orbs
  G.orbs.forEach(o=>{
    ctx.fillStyle='rgba(250,199,117,.9)';ctx.beginPath();ctx.arc(o.x,o.y,7,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#fff';ctx.font='bold 8px monospace';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(o.val,o.x,o.y);
  });
  // powerups
  G.powerups.forEach(pu=>{
    const pulse=.85+Math.sin(G.frame*.14)*.15;
    ctx.fillStyle=pu.type==='hp'?`rgba(226,75,74,${pulse})`:`rgba(55,138,221,${pulse})`;
    ctx.beginPath();ctx.arc(pu.x,pu.y,10,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.9)';ctx.font='11px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(pu.type==='hp'?'♥':'⚡',pu.x,pu.y);
  });
  // drones
  drones.forEach(drawDrone);
  // particles
  drawParticles();
  // bullets
  G.bullets.forEach(b=>{
    if(b.homing){
      ctx.strokeStyle=b.color;ctx.lineWidth=2.5;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.stroke();
      ctx.fillStyle=b.color+'60';ctx.beginPath();ctx.arc(b.x,b.y,b.size+3,0,Math.PI*2);ctx.fill();
    } else {
      ctx.fillStyle=b.color;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.fill();
      if(b.size>5){ctx.fillStyle=b.color+'40';ctx.beginPath();ctx.arc(b.x,b.y,b.size*1.8,0,Math.PI*2);ctx.fill()}
    }
  });
  // enemies
  G.enemies.forEach(drawEnemy);
  if(G.boss)drawBoss(G.boss);
  // meteors
  drawMeteors();
  // player
  if(!G.gameOver)drawPlayer();
  // nuke ring
  if(G.p.skills.nuke.cd>200){
    const prog=(G.p.skills.nuke.cd-200)/40;
    const r=skillVal('nuke','radius')*200*(1-prog);
    ctx.strokeStyle=`rgba(250,199,117,${prog*.8})`;ctx.lineWidth=4;
    ctx.beginPath();ctx.arc(G.p.x,G.p.y,r,0,Math.PI*2);ctx.stroke();
  }
  // wave flash
  if(G.waveBreak){const t=G.waveBreakTimer/220;ctx.fillStyle=`rgba(0,255,180,${t*.1})`;ctx.fillRect(0,0,W(),H())}
  // timewarp overlay
  if(G.p.timewarpActive){
    ctx.fillStyle='rgba(80,60,180,0.08)';ctx.fillRect(0,0,W(),H());
    ctx.strokeStyle='rgba(175,169,236,0.15)';ctx.lineWidth=2;
    for(let i=0;i<4;i++){ctx.beginPath();ctx.arc(G.p.x,G.p.y,60+i*40,0,Math.PI*2);ctx.stroke()}
    ctx.fillStyle='rgba(175,169,236,0.6)';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.textBaseline='top';ctx.fillText('⏰ TIME WARP',W()/2,70);
  }
  // float damage numbers
  floatDmg.forEach(f=>{
    ctx.globalAlpha=f.life/45;ctx.font=`bold ${10+Math.min(6,f.val/20)}px monospace`;ctx.fillStyle=f.col;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(f.val,f.x,f.y);
  });
  ctx.globalAlpha=1;
}

function loop(){
  requestAnimationFrame(loop); // always schedule first — errors can't break the loop
  if(!G)return;
  try{if(!G.shopOpen&&!G.talentOpen&&!G.skillUpOpen)update()}catch(e){console.warn('[update]',e)}
  try{draw()}catch(e){console.warn('[draw]',e)}
}




// ═══════════════════════════════════════════════
// BOOT
// ═══════════════════════════════════════════════
loopRunning=true;
loop();
showMenu();
</script>
</body>
</html>
chơi ở đây

Anh-man-hinh-2026-04-09-luc-10.32.19.webp

Anh-man-hinh-2026-04-08-luc-14.57.28.webp

Chơi thử và trải nghiệm tại đây



Mã:
<!DOCTYPE html>
<html lang="vi">

<head>
    <meta charset="UTF-8">
    <title>Galaxy Roguelike — V4</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            user-select: none
        }

        body {
            background: #0a0a12;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: 'Courier New', monospace;
            overflow: hidden
        }

        #wrap {
            position: relative;
            width: 960px;
            height: 620px;
            overflow: hidden;
            background: #000;
            border-radius: 16px;
            border: 1px solid #ffffff12;
            box-shadow: 0 0 60px #0006
        }

        #gc {
            display: block;
            cursor: crosshair
        }

        #ui {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none
        }

        /* HUD */
        .hud-top {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            padding: 12px 18px;
            background: linear-gradient(rgba(0, 0, 0, .8), transparent);
            gap: 20px
        }

        .stat-col {
            display: flex;
            flex-direction: column;
            gap: 6px
        }

        .bar-row {
            display: flex;
            align-items: center;
            gap: 7px;
            font-size: 11px
        }

        .bar-bg {
            border-radius: 4px;
            overflow: hidden;
            background: #ffffff12;
            position: relative
        }

        .bar-fill {
            height: 8px;
            border-radius: 4px;
            transition: width .25s
        }

        #hp-fill {
            background: linear-gradient(90deg, #a32d2d, #e24b4a)
        }

        #sh-fill {
            background: linear-gradient(90deg, #185fa5, #378add)
        }

        #xp-fill {
            background: linear-gradient(90deg, #0f6e56, #1d9e75)
        }

        .wave-center {
            text-align: center
        }

        .wave-label {
            font-size: 10px;
            color: #555;
            letter-spacing: 2px;
            text-transform: uppercase
        }

        .wave-num {
            font-size: 38px;
            font-weight: 700;
            color: #0ff;
            line-height: 1;
            text-shadow: 0 0 20px #0ff6
        }

        .stats-right {
            text-align: right;
            font-size: 11px;
            color: #555;
            line-height: 2.1
        }

        .stats-right b {
            color: #fac775;
            font-size: 13px
        }

        /* skill bar */
        #skillbar {
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 6px;
            padding: 10px 14px;
            background: linear-gradient(transparent, rgba(0, 0, 0, .75));
            border-radius: 0 0 16px 16px;
            pointer-events: none
        }

        .sk {
            width: 48px;
            height: 48px;
            border-radius: 10px;
            border: 1px solid #fff2;
            background: #ffffff08;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-size: 20px;
            position: relative;
            overflow: hidden
        }

        .sk-overlay {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            background: #0009;
            text-align: center;
            font-size: 9px;
            color: #aaa;
            padding: 1px 0 2px
        }

        .sk-key {
            position: absolute;
            top: 2px;
            right: 4px;
            font-size: 8px;
            color: #ffffff50;
            font-family: monospace
        }

        .sk-label {
            position: absolute;
            bottom: 26px;
            font-size: 7px;
            color: #ffffff40;
            white-space: nowrap
        }

        /* boss bar */
        #bossbar {
            position: absolute;
            top: 62px;
            left: 50%;
            transform: translateX(-50%);
            width: 340px;
            display: none;
            pointer-events: none;
            text-align: center
        }

        #boss-name {
            font-size: 11px;
            color: #ff5555;
            letter-spacing: 2px;
            margin-bottom: 4px;
            text-transform: uppercase
        }

        #boss-bg {
            background: #fff1;
            border-radius: 5px;
            height: 12px;
            overflow: hidden;
            border: 1px solid #ff333330
        }

        #boss-fill {
            height: 100%;
            background: linear-gradient(90deg, #791f1f, #e24b4a);
            border-radius: 5px;
            transition: width .15s
        }

        #boss-phase {
            font-size: 9px;
            color: #ff555580;
            margin-top: 3px;
            letter-spacing: 1px
        }

        /* combo */
        #combo-el {
            position: absolute;
            top: 68px;
            left: 50%;
            transform: translateX(-50%);
            font-size: 20px;
            font-weight: 700;
            color: #fac775;
            opacity: 0;
            transition: opacity .3s;
            pointer-events: none;
            text-shadow: 0 0 12px #fac77580;
            white-space: nowrap
        }

        /* floating dmg */
        .floatdmg {
            position: absolute;
            pointer-events: none;
            font-size: 13px;
            font-weight: 700;
            transition: none
        }

        /* minimap */
        #minimap {
            position: absolute;
            bottom: 68px;
            right: 12px;
            border: 1px solid #fff2;
            border-radius: 8px;
            opacity: .8;
            pointer-events: none
        }

        #minimap-label {
            position: absolute;
            bottom: 142px;
            right: 12px;
            font-size: 9px;
            color: #fff3;
            letter-spacing: 1px;
            pointer-events: none
        }

        /* messages */
        #msg {
            position: absolute;
            top: 48%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #fff;
            font-size: 24px;
            font-weight: 700;
            text-align: center;
            pointer-events: none;
            opacity: 0;
            transition: opacity .35s;
            line-height: 1.6;
            text-shadow: 0 0 20px #0ff8
        }

        /* panels */
        .panel {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            border-radius: 16px;
            padding: 24px;
            color: #fff;
            pointer-events: all;
            display: none;
            backdrop-filter: blur(2px)
        }

        #shop-panel {
            background: rgba(2, 4, 20, .97);
            border: 1px solid #0ff3;
            width: 420px;
            max-height: 520px;
            overflow-y: auto
        }

        #shop-panel h3 {
            color: #0ff;
            margin-bottom: 6px;
            font-size: 13px;
            text-align: center;
            letter-spacing: 2px
        }

        .shop-tabs {
            display: flex;
            gap: 4px;
            margin-bottom: 12px;
            border-bottom: 1px solid #ffffff12;
            padding-bottom: 8px
        }

        .stab {
            flex: 1;
            padding: 6px 4px;
            text-align: center;
            font-size: 10px;
            color: #555;
            cursor: pointer;
            border-radius: 6px 6px 0 0;
            transition: .15s
        }

        .stab.active {
            color: #0ff;
            background: #0ff08;
            border-bottom: 2px solid #0ff
        }

        .shop-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 12px;
            margin: 4px 0;
            background: #ffffff07;
            border-radius: 10px;
            border: 1px solid #ffffff12;
            cursor: pointer;
            transition: .15s;
            gap: 10px
        }

        .shop-item:hover {
            background: #0ff08;
            border-color: #0ff4
        }

        .shop-item.cant {
            opacity: .3;
            cursor: not-allowed
        }

        .shop-item.bought {
            border-color: #1d9e7540;
            background: #1d9e7508;
            opacity: .6;
            cursor: not-allowed
        }

        .si-icon {
            font-size: 20px;
            min-width: 26px;
            text-align: center
        }

        .si-body {
            flex: 1
        }

        .si-name {
            font-size: 12px;
            font-weight: 700;
            color: #ddd
        }

        .si-desc {
            font-size: 10px;
            color: #555;
            margin-top: 2px
        }

        .si-effect {
            font-size: 9px;
            color: #1d9e75;
            margin-top: 2px
        }

        .si-cost {
            display: flex;
            flex-direction: column;
            align-items: flex-end;
            gap: 2px
        }

        .si-price {
            color: #fac775;
            font-size: 12px;
            font-weight: 700;
            white-space: nowrap
        }

        .si-stock {
            font-size: 9px;
            color: #555
        }

        .shop-gold-bar {
            display: flex;
            align-items: center;
            gap: 6px;
            margin-bottom: 10px;
            padding: 8px 10px;
            background: #fac77510;
            border-radius: 8px;
            border: 1px solid #fac77520
        }

        .shop-gold-icon {
            font-size: 16px
        }

        .shop-gold-val {
            font-size: 16px;
            font-weight: 700;
            color: #fac775
        }

        .shop-gold-label {
            font-size: 10px;
            color: #777
        }

        #talent-panel {
            background: rgba(4, 2, 22, .98);
            border: 1px solid #7f77dd;
            width: 500px;
            max-height: 520px;
            overflow-y: auto
        }

        #talent-panel h3 {
            color: #afa9ec;
            margin-bottom: 4px;
            text-align: center;
            font-size: 13px;
            letter-spacing: 2px
        }

        .talent-tabs {
            display: flex;
            gap: 4px;
            margin-bottom: 14px;
            border-bottom: 1px solid #ffffff12;
            padding-bottom: 8px
        }

        .ttab {
            flex: 1;
            padding: 6px 4px;
            text-align: center;
            font-size: 10px;
            color: #555;
            cursor: pointer;
            border-radius: 6px 6px 0 0;
            transition: .15s;
            letter-spacing: .5px
        }

        .ttab.active {
            color: #afa9ec;
            background: #7f77dd18;
            border-bottom: 2px solid #7f77dd
        }

        .talent-grid {
            display: grid;
            grid-template-columns: 1fr 1fr 1fr;
            gap: 8px
        }

        .tc {
            background: #ffffff07;
            border: 1px solid #7f77dd30;
            border-radius: 12px;
            padding: 12px 8px;
            cursor: pointer;
            text-align: center;
            transition: .18s;
            position: relative
        }

        .tc:hover {
            background: #7f77dd18;
            border-color: #7f77dd;
            transform: translateY(-2px)
        }

        .tc.taken {
            border-color: #fac77560;
            background: #fac77508;
            cursor: default
        }

        .tc.taken::after {
            content: '✓';
            position: absolute;
            top: 6px;
            right: 8px;
            font-size: 10px;
            color: #fac775
        }

        .tc-icon {
            font-size: 26px;
            margin-bottom: 5px
        }

        .tc-name {
            font-size: 11px;
            font-weight: 700;
            color: #afa9ec;
            margin-bottom: 2px
        }

        .tc-desc {
            font-size: 9px;
            color: #555
        }

        .tc-tag {
            display: inline-block;
            font-size: 8px;
            padding: 1px 5px;
            border-radius: 4px;
            margin-top: 4px;
            color: #afa9ec;
            background: #7f77dd25;
            border: 1px solid #7f77dd30
        }

        .tc-tag.off {
            color: #e24b4a;
            background: #e24b4a18;
            border-color: #e24b4a30
        }

        .tc-tag.atk {
            color: #fac775;
            background: #fac77518;
            border-color: #fac77530
        }

        .tc-tag.def {
            color: #378add;
            background: #378add18;
            border-color: #378add30
        }

        .tc-tag.uti {
            color: #1d9e75;
            background: #1d9e7518;
            border-color: #1d9e7530
        }

        .tc-synergy {
            font-size: 9px;
            color: #7f77dd80;
            margin-top: 3px;
            font-style: italic
        }

        /* achievement */
        #ach {
            position: absolute;
            top: 90px;
            right: 14px;
            background: rgba(2, 12, 2, .95);
            border: 1px solid #1d9e75;
            border-radius: 10px;
            padding: 10px 14px;
            font-size: 11px;
            pointer-events: none;
            opacity: 0;
            transition: opacity .4s;
            max-width: 220px
        }

        #ach-title {
            color: #1d9e75;
            font-size: 9px;
            letter-spacing: 2px;
            margin-bottom: 3px;
            text-transform: uppercase
        }

        /* skill upgrade panel */
        #skill-panel {
            background: rgba(4, 2, 22, .98);
            border: 1px solid #d4537e;
            width: 400px
        }

        #skill-panel h3 {
            color: #ed93b1;
            margin-bottom: 16px;
            text-align: center;
            font-size: 13px;
            letter-spacing: 2px
        }

        .skill-upgrade-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px
        }

        .su-card {
            background: #ffffff07;
            border: 1px solid #d4537e30;
            border-radius: 12px;
            padding: 14px;
            cursor: pointer;
            transition: .18s
        }

        .su-card:hover {
            background: #d4537e18;
            border-color: #d4537e
        }

        .su-card .su-icon {
            font-size: 26px;
            margin-bottom: 6px
        }

        .su-card .su-name {
            font-size: 12px;
            font-weight: 700;
            color: #ed93b1
        }

        .su-card .su-desc {
            font-size: 10px;
            color: #555;
            margin-top: 3px
        }

        .su-card .su-lv {
            font-size: 10px;
            color: #fac775;
            margin-top: 4px
        }

        /* controls hint */
        #hint {
            position: absolute;
            bottom: 68px;
            left: 12px;
            font-size: 9px;
            color: #ffffff25;
            line-height: 2;
            pointer-events: none
        }
    </style>
</head>

<body>
    <div id="wrap">
        <canvas id="gc"></canvas>
        <div id="ui">
            <div class="hud-top">
                <div class="stat-col">
                    <div class="bar-row"><span style="color:#e24b4a;width:20px">HP</span>
                        <div class="bar-bg" style="width:130px">
                            <div id="hp-fill" class="bar-fill" style="width:100%;height:8px"></div>
                        </div><span id="hp-val" style="color:#e24b4a;min-width:36px">100</span>
                    </div>
                    <div class="bar-row"><span style="color:#378add;width:20px">SH</span>
                        <div class="bar-bg" style="width:130px">
                            <div id="sh-fill" class="bar-fill" style="width:100%;height:8px"></div>
                        </div><span id="sh-val" style="color:#378add;min-width:36px">100</span>
                    </div>
                    <div class="bar-row"><span style="color:#1d9e75;width:20px">XP</span>
                        <div class="bar-bg" style="width:130px">
                            <div id="xp-fill" class="bar-fill" style="width:0%;height:8px"></div>
                        </div><span id="lv-val" style="color:#1d9e75;min-width:36px">Lv1</span>
                    </div>
                </div>
                <div class="wave-center">
                    <div class="wave-label">wave</div>
                    <div class="wave-num" id="wn">1</div>
                </div>
                <div class="stats-right">
                    SCORE <b id="sv">0</b><br>
      KILLS <b id="kv" style="color:#e24b4a">0</b><br>
      GOLD <b id="gv">0</b>
                </div>
            </div>
            <div id="combo-el"></div>
            <div id="bossbar">
                <div id="boss-name">BOSS</div>
                <div id="boss-bg">
                    <div id="boss-fill" style="width:100%"></div>
                </div>
                <div id="boss-phase"></div>
            </div>
            <div id="skillbar">
                <div class="sk" id="sk0"><span>💨</span><span class="sk-key">Shift</span>
                    <div class="sk-overlay" id="scd0"></div>
                </div>
                <div class="sk" id="sk1"><span>💥</span><span class="sk-key">Q</span>
                    <div class="sk-overlay" id="scd1"></div>
                </div>
                <div class="sk" id="sk2"><span>🛡️</span><span class="sk-key">W</span>
                    <div class="sk-overlay" id="scd2"></div>
                </div>
                <div class="sk" id="sk3"><span>⚡</span><span class="sk-key">F</span>
                    <div class="sk-overlay" id="scd3"></div>
                </div>
                <div class="sk" id="sk4"><span>🌀</span><span class="sk-key">R</span>
                    <div class="sk-overlay" id="scd4"></div>
                </div>
                <div class="sk" id="sk5"><span>☄️</span><span class="sk-key">T</span>
                    <div class="sk-overlay" id="scd5"></div>
                </div>
                <div class="sk"><span>🏪</span><span class="sk-key">E</span></div>
            </div>
            <canvas id="minimap" width="110" height="80"></canvas>
            <div id="minimap-label">RADAR</div>
            <div id="hint">WASD: Di chuyển | Chuột: Bắn<br>Shift:Dash Q:Nuke W:Shield F:EMP<br>R:Vortex T:Meteor | E:Shop | X:Upgrade
            </div>
            <div id="msg"></div>
            <div id="ach">
                <div id="ach-title">achievement</div>
                <div id="ach-text"></div>
            </div>
        </div>

        <div class="panel" id="shop-panel">
            <h3>🏪 UPGRADE SHOP</h3>
            <div class="shop-gold-bar">
                <span class="shop-gold-icon">💰</span><span class="shop-gold-val" id="shop-gold">0</span><span class="shop-gold-label">gold available</span>
            </div>
            <div class="shop-tabs">
                <div class="stab active" onclick="setShopTab('combat')">⚔️ Combat</div>
                <div class="stab" onclick="setShopTab('defense')">🛡️ Defense</div>
                <div class="stab" onclick="setShopTab('utility')">⚙️ Utility</div>
                <div class="stab" onclick="setShopTab('special')">✨ Special</div>
            </div>
            <div id="shop-items"></div>
            <div style="margin-top:10px;color:#333;font-size:10px;text-align:center">E để đóng • Items có giới hạn số
                lần mua</div>
        </div>

        <div class="panel" id="talent-panel">
            <h3>★ LEVEL UP — CHỌN 1 KỸ NĂNG THỤ ĐỘNG</h3>
            <div class="talent-tabs" id="talent-tabs"></div>
            <div class="talent-grid" id="tg"></div>
            <div id="talent-synergy-hint"
                style="margin-top:10px;font-size:10px;color:#7f77dd60;text-align:center;font-style:italic"></div>
        </div>

        <div class="panel" id="skill-panel">
            <h3>🔧 NÂNG CẤP KỸ NĂNG</h3>
            <div class="skill-upgrade-grid" id="su-grid"></div>
            <div style="margin-top:12px;color:#333;font-size:10px;text-align:center">X để đóng</div>
        </div>
    </div>

    <script>
        const C=document.getElementById('gc'),ctx=C.getContext('2d');
const MM=document.getElementById('minimap'),mctx=MM.getContext('2d');
const WRAP=document.getElementById('wrap');
function rsz(){C.width=WRAP.clientWidth;C.height=WRAP.clientHeight}rsz();
window.addEventListener('resize',rsz);
const W=()=>C.width,H=()=>C.height;

/* ── input ── */
const keys={},M={x:480,y:310,dn:false};
C.addEventListener('mousemove',e=>{const r=C.getBoundingClientRect();M.x=e.clientX-r.left;M.y=e.clientY-r.top});
C.addEventListener('mousedown',e=>{if(e.button===0)M.dn=true});
C.addEventListener('mouseup',e=>{if(e.button===0)M.dn=false});
document.addEventListener('keydown',e=>{if(!keys[e.code]){keys[e.code]=true;onKey(e.code)}});
document.addEventListener('keyup',e=>{keys[e.code]=false});

/* ── skill definitions ── */
const SKILL_DEFS={
  dash:{name:'Dash',icon:'💨',maxLv:5,base:{cd:100,power:1},perLv:{cd:-10,power:.25},desc:lv=>`Teleport • CD: ${100-lv*10}f • Lực: ${(1+lv*.25).toFixed(1)}x`},
  nuke:{name:'Nova Bomb',icon:'💥',maxLv:5,base:{cd:240,radius:1,dmg:200},perLv:{cd:-20,radius:.3,dmg:60},desc:lv=>`AoE nổ • CD: ${(240-lv*20)/60}s • R: ${(1+lv*.3).toFixed(1)}x`},
  shield:{name:'Barrier',icon:'🛡️',maxLv:5,base:{cd:160,restore:75},perLv:{cd:-15,restore:25},desc:lv=>`Lá chắn • CD: ${(160-lv*15)/60}s • +${75+lv*25}HP`},
  emp:{name:'EMP Pulse',icon:'⚡',maxLv:5,base:{cd:200,dur:185},perLv:{cd:-25,dur:40},desc:lv=>`Đóng băng • CD: ${(200-lv*25)/60}s • ${(185+lv*40)/60}s`},
  vortex:{name:'Vortex',icon:'🌀',maxLv:4,base:{cd:300,dur:120,suck:3},perLv:{cd:-30,dur:20,suck:1},desc:lv=>`Hút địch • CD: ${(300-lv*30)/60}s • Lực: ${3+lv}x`},
  meteor:{name:'Meteor',icon:'☄️',maxLv:4,base:{cd:280,dmg:80,count:3},perLv:{cd:-25,dmg:30,count:1},desc:lv=>`Mưa sao • CD: ${(280-lv*25)/60}s • ${80+lv*30}dmg x${3+lv}`},
};

/* ── passive talents — full overhaul ── */
// category: atk=attack, def=defense, uti=utility, off=offensive special
const TALENT_CATS={
  atk:{label:'⚔️ Attack',color:'#fac775'},
  def:{label:'🛡️ Defense',color:'#378add'},
  uti:{label:'⚙️ Utility',color:'#1d9e75'},
  off:{label:'💥 Special',color:'#d4537e'},
};

const TALENTS=[
  // ── ATTACK ──
  {id:'rapidfire',name:'Rapid Fire',cat:'atk',desc:'Tốc độ bắn +20%',detail:'Giảm cooldown mỗi viên đạn',icon:'🔥',synergy:'multishot',apply:p=>{p.fireRate=Math.max(4,Math.floor(p.fireRate*.8))},stackable:true},
  {id:'multishot',name:'Multi Shot',cat:'atk',desc:'+2 đạn mỗi phát',detail:'Mỗi lần bắn ra thêm 2 đạn',icon:'💫',synergy:'rapidfire',apply:p=>{p.multiShot+=2},stackable:true},
  {id:'pierce',name:'Pierce Shot',cat:'atk',desc:'Đạn xuyên kẻ địch',detail:'Đạn không dừng lại khi trúng',icon:'🏹',synergy:null,apply:p=>{p.pierce=true},stackable:false},
  {id:'crit',name:'Critical Hit',cat:'atk',desc:'Crit +15% (×3.5 dmg)',detail:'Đạn vàng = crit',icon:'💢',synergy:'crit',apply:p=>{p.critChance+=.15;p.critMult=(p.critMult||3)+.5},stackable:true},
  {id:'overload',name:'Overload',cat:'atk',desc:'Tốc đạn +40%',detail:'Đạn bay nhanh hơn nhiều',icon:'⚡',synergy:'rapidfire',apply:p=>{p.bulletSpeed*=1.4},stackable:false},
  {id:'ricochet',name:'Ricochet',cat:'atk',desc:'Đạn nảy tường 2 lần',detail:'Đạn phản xạ khi ra mép',icon:'🎯',synergy:'pierce',apply:p=>{p.ricochet=true},stackable:false},
  {id:'burst',name:'Burst Fire',cat:'atk',desc:'Mỗi 5 phát: burst 3 đạn',detail:'Trigger tự động',icon:'🔫',synergy:'rapidfire',apply:p=>{p.burst=true;p.burstCounter=0},stackable:false},
  {id:'plasma',name:'Plasma Round',cat:'atk',desc:'Đạn lớn +50% kích cỡ & dmg',detail:'Đạn plasma xanh đậm',icon:'🔵',synergy:'pierce',apply:p=>{p.plasma=true},stackable:false},
  // ── DEFENSE ──
  {id:'tank',name:'Iron Hull',cat:'def',desc:'Max HP +100',detail:'Tăng sinh lực tối đa',icon:'🛡️',synergy:'regen',apply:p=>{p.maxHp+=100;p.hp=Math.min(p.hp+100,p.maxHp)},stackable:true},
  {id:'battery',name:'Power Cell',cat:'def',desc:'Max Shield +100',detail:'Tăng tấm chắn tối đa',icon:'🔋',synergy:'shieldRegen',apply:p=>{p.maxShield+=100;p.shield=Math.min(p.shield+100,p.maxShield)},stackable:true},
  {id:'regen',name:'Regen Core',cat:'def',desc:'Hồi 4 HP/giây',detail:'Tự động hồi HP',icon:'💚',synergy:'tank',apply:p=>{p.regen=true;p.regenAmt=(p.regenAmt||0)+4},stackable:true},
  {id:'vamp',name:'Vampire',cat:'def',desc:'Hút máu +10/kill',detail:'Mỗi kill hồi HP',icon:'🧛',synergy:'blast',apply:p=>{p.lifesteal+=10},stackable:true},
  {id:'thorns',name:'Thorns',cat:'def',desc:'Phản 15dmg khi bị đánh',detail:'Kẻ địch nhận sát thương',icon:'🌵',synergy:null,apply:p=>{p.thorns=(p.thorns||0)+15},stackable:true},
  {id:'fortress',name:'Fortress',cat:'def',desc:'Giảm 20% sát thương nhận',detail:'Armor thụ động',icon:'🏰',synergy:'tank',apply:p=>{p.armor=(p.armor||0)+.2},stackable:true},
  // ── UTILITY ──
  {id:'haste',name:'Haste',cat:'uti',desc:'Tốc độ di chuyển +20%',detail:'Phản ứng nhanh hơn',icon:'👟',synergy:null,apply:p=>{p.speed*=1.2},stackable:true},
  {id:'drone',name:'Combat Drone',cat:'uti',desc:'+1 drone tự bắn',detail:'Drone bay quanh và tấn công',icon:'🤖',synergy:'drone',apply:p=>{addDrone()},stackable:true},
  {id:'magnet',name:'Magnet Field',cat:'uti',desc:'Hút vàng & item trong 120px',detail:'Thu thập tự động',icon:'🧲',synergy:null,apply:p=>{p.magnet=true;p.magnetRange=(p.magnetRange||120)+20},stackable:true},
  {id:'goldaura',name:'Gold Aura',cat:'uti',desc:'Vàng từ kill +70%',detail:'Kiếm nhiều tiền hơn',icon:'💰',synergy:'magnet',apply:p=>{p.goldBonus=(p.goldBonus||1)*1.7},stackable:false},
  {id:'cooldown',name:'Overclock',cat:'uti',desc:'CD tất cả skill -15%',detail:'Skill dùng được thường hơn',icon:'⏱️',synergy:null,apply:p=>{p.cdReduction=(p.cdReduction||1)*.85},stackable:true},
  {id:'missile',name:'Auto Missile',cat:'uti',desc:'Tên lửa homing mỗi 3s',detail:'Tự tìm và tấn công mục tiêu',icon:'🚀',synergy:'drone',apply:p=>{p.missiles=true},stackable:false},
  // ── SPECIAL ──
  {id:'blast',name:'Blast Kill',cat:'off',desc:'Giết tạo vụ nổ AoE',detail:'Mỗi kill gây sát thương vùng',icon:'💣',synergy:'vamp',apply:p=>{p.blast=true},stackable:false},
  {id:'chainbolt',name:'Chain Bolt',cat:'off',desc:'Đạn nối kẻ địch gần',detail:'Sau khi trúng lan sang 2 địch',icon:'🌩️',synergy:'multishot',apply:p=>{p.chain=true},stackable:false},
  {id:'blackhole',name:'Black Hole',cat:'off',desc:'Giết có 5% tạo lỗ đen nhỏ',detail:'Hút địch trong 2 giây',icon:'🌀',synergy:'blast',apply:p=>{p.blackhole=true},stackable:false},
  {id:'timewarp',name:'Time Warp',cat:'off',desc:'Mỗi 8s: slowmo 2s',detail:'Mọi thứ chậm lại 60%',icon:'⏰',synergy:'cooldown',apply:p=>{p.timewarp=true;p.timewarpCd=0},stackable:false},
];

let takenTalentIds=[];
let currentTalentTab='atk';
let shopTab='combat';

/* ── shop items — full overhaul ── */
const SHOP_DATA={
  combat:[
    {name:'Bullet Upgrade',icon:'🔫',desc:'Sát thương đạn +30%',effect:'+30% base dmg',cost:60,stock:3,bought:0,apply:p=>{p.dmgMult=(p.dmgMult||1)*1.3}},
    {name:'Overcharge Ammo',icon:'⚡',desc:'Tốc đạn +35%',effect:'+35% bullet speed',cost:55,stock:3,bought:0,apply:p=>{p.bulletSpeed*=1.35}},
    {name:'Fire Rate Chip',icon:'🔥',desc:'Tốc bắn +15%',effect:'-15% fire cooldown',cost:65,stock:4,bought:0,apply:p=>{p.fireRate=Math.max(4,Math.floor(p.fireRate*.85))}},
    {name:'Hollow Point',icon:'💎',desc:'Crit damage +1x',effect:'critMult +1',cost:80,stock:2,bought:0,apply:p=>{p.critMult=(p.critMult||3)+1}},
    {name:'Drone Armament',icon:'🤖',desc:'Drone damage +50%',effect:'+50% drone dmg',cost:70,stock:3,bought:0,apply:p=>{p.droneDmgMult=(p.droneDmgMult||1)*1.5}},
    {name:'Plasma Core',icon:'🔵',desc:'Tất cả đạn xuyên giáp',effect:'pierce mode on',cost:120,stock:1,bought:0,apply:p=>{p.pierce=true}},
  ],
  defense:[
    {name:'Medkit',icon:'💊',desc:'Hồi ngay 120 HP',effect:'instant +120 HP',cost:50,stock:5,bought:0,apply:p=>{p.hp=Math.min(p.maxHp,p.hp+120)}},
    {name:'Shield Recharge',icon:'🔵',desc:'Nạp đầy toàn bộ shield',effect:'shield → max',cost:45,stock:5,bought:0,apply:p=>{p.shield=p.maxShield}},
    {name:'Armor Plate',icon:'🧱',desc:'Max HP +80',effect:'+80 max HP',cost:65,stock:4,bought:0,apply:p=>{p.maxHp+=80;p.hp=Math.min(p.hp+80,p.maxHp)}},
    {name:'Shield Capacitor',icon:'🔋',desc:'Max Shield +80',effect:'+80 max shield',cost:60,stock:4,bought:0,apply:p=>{p.maxShield+=80;p.shield=Math.min(p.shield+80,p.maxShield)}},
    {name:'Nano Repair',icon:'💚',desc:'Regen +6 HP/giây',effect:'+6 HP regen/s',cost:75,stock:3,bought:0,apply:p=>{p.regen=true;p.regenAmt=(p.regenAmt||0)+6}},
    {name:'Reactive Armor',icon:'🛡️',desc:'Giảm 15% sát thương nhận',effect:'-15% dmg taken',cost:90,stock:2,bought:0,apply:p=>{p.armor=(p.armor||0)+.15}},
  ],
  utility:[
    {name:'Gold Booster',icon:'💰',desc:'Vàng từ kill +80%',effect:'+80% gold per kill',cost:85,stock:2,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.8}},
    {name:'XP Boost',icon:'📈',desc:'XP từ kill +50%',effect:'+50% XP gain',cost:70,stock:2,bought:0,apply:p=>{p.xpBonus=(p.xpBonus||1)*1.5}},
    {name:'Skill Cooldown',icon:'⏱️',desc:'CD tất cả skill -20%',effect:'-20% all cooldowns',cost:95,stock:2,bought:0,apply:p=>{p.cdReduction=(p.cdReduction||1)*.8}},
    {name:'Speed Module',icon:'👟',desc:'Tốc độ di chuyển +15%',effect:'+15% move speed',cost:50,stock:3,bought:0,apply:p=>{p.speed*=1.15}},
    {name:'Magnet Field',icon:'🧲',desc:'Thu item trong vòng 150px',effect:'auto collect range',cost:40,stock:2,bought:0,apply:p=>{p.magnet=true;p.magnetRange=(p.magnetRange||120)+40}},
    {name:'Combo Extender',icon:'🔗',desc:'Combo timer +2 giây',effect:'+120 combo duration',cost:55,stock:3,bought:0,apply:p=>{p.comboBonus=(p.comboBonus||0)+120}},
  ],
  special:[
    {name:'Drone Upgrade',icon:'🤖',desc:'+1 Combat Drone mới',effect:'spawn extra drone',cost:100,stock:3,bought:0,apply:p=>{addDrone()}},
    {name:'Nova Core',icon:'💥',desc:'Nuke radius +70%',effect:'+70% nuke AoE',cost:90,stock:2,bought:0,apply:p=>{G.p.skills.nuke&&(G.p.skills.nuke.radiusBonus=(G.p.skills.nuke.radiusBonus||1)*1.7)}},
    {name:'Warp Drive',icon:'🌌',desc:'Dash teleport distance ×2',effect:'dash power ×2',cost:80,stock:2,bought:0,apply:p=>{p.dashPower=(p.dashPower||1)*2}},
    {name:'Chain Reactor',icon:'🌩️',desc:'Đạn bắn xích sang địch kế',effect:'chain lightning on hit',cost:110,stock:1,bought:0,apply:p=>{p.chain=true}},
    {name:'Black Market',icon:'💀',desc:'Mở khoá 3 item ẩn đặc biệt',effect:'secret items unlocked',cost:150,stock:1,bought:0,apply:p=>{p.blackMarket=true;setTimeout(()=>renderShop(),100)}},
    {name:'Omni Shield',icon:'⭕',desc:'Shield không bao giờ về 0 (min 10)',effect:'shield floor = 10',cost:130,stock:1,bought:0,apply:p=>{p.shieldFloor=10}},
  ],
};

const ACH=[
  {id:'k10',label:'First Blood',check:g=>g.kills>=10,done:false},
  {id:'k50',label:'Warrior — 50 kills',check:g=>g.kills>=50,done:false},
  {id:'k200',label:'Slayer — 200 kills',check:g=>g.kills>=200,done:false},
  {id:'c15',label:'Unstoppable — x15 combo',check:g=>g.combo>=15,done:false},
  {id:'w5',label:'Veteran — Wave 5',check:g=>g.wave>=5,done:false},
  {id:'w10',label:'Legend — Wave 10',check:g=>g.wave>=10,done:false},
  {id:'s10k',label:'10000 điểm',check:g=>g.score>=10000,done:false},
  {id:'lv8',label:'Master — Level 8',check:g=>g.level>=8,done:false},
  {id:'boss3',label:'Boss Killer — 3 boss',check:g=>g.bossKills>=3,done:false},
];

/* ── drones ── */
let drones=[];
function addDrone(){drones.push({a:Math.random()*Math.PI*2,orb:60+drones.length*28,cd:0,x:0,y:0,lv:1})}

/* ── vortex & meteors ── */
let vortexActive={on:false,x:0,y:0,timer:0,suck:3};
let meteors=[];
let floatDmg=[];

/* ── game state ── */
let G;
function initGame(){
  drones=[];vortexActive={on:false};meteors=[];floatDmg=[];takenTalentIds=[];shopTab='combat';
  // reset shop stock
  Object.values(SHOP_DATA).forEach(cat=>cat.forEach(it=>{it.bought=0}));
  G={
    p:{x:W()/2,y:H()/2,hp:100,maxHp:100,shield:100,maxShield:100,
      speed:3.1,vx:0,vy:0,angle:0,
      fireRate:14,fireCd:0,bulletSpeed:7.5,multiShot:0,
      lifesteal:0,pierce:false,critChance:0,critMult:3,
      magnet:false,magnetRange:120,missiles:false,blast:false,regen:false,regenAmt:3,ricochet:false,
      goldBonus:1,dmgMult:1,xpBonus:1,cdReduction:1,dashPower:1,
      armor:0,thorns:0,shieldFloor:0,chain:false,blackhole:false,timewarp:false,timewarpCd:0,
      burst:false,burstCounter:0,plasma:false,droneDmgMult:1,comboBonus:0,
      skills:{dash:{lv:0,cd:0},nuke:{lv:0,cd:0},shield:{lv:0,cd:0},emp:{lv:0,cd:0},vortex:{lv:0,cd:0},meteor:{lv:0,cd:0}}
    },
    bullets:[],enemies:[],particles:[],powerups:[],orbs:[],trails:[],
    score:0,kills:0,gold:0,totalGold:0,bossKills:0,
    wave:1,waveKills:0,waveTarget:8,
    waveBreak:false,waveBreakTimer:0,
    xp:0,xpNeeded:65,level:1,combo:0,comboTimer:0,
    shopOpen:false,talentOpen:false,skillUpOpen:false,
    gameOver:false,won:false,frame:0,
    spawnTimer:60,boss:null,bossSpawned:false,
    stars:[],nebulas:[],dustClouds:[],
  };
  // background
  for(let i=0;i<200;i++)G.stars.push({x:Math.random()*1800,y:Math.random()*1000,s:Math.random()*2+.2,b:Math.random(),sp:.04+Math.random()*.4,col:Math.random()<.15?[180,180,255]:[255,255,255]});
  for(let i=0;i<7;i++)G.nebulas.push({x:Math.random()*960,y:Math.random()*620,rx:140+Math.random()*200,ry:80+Math.random()*150,hue:Math.random()*360,a:.055+Math.random()*.04});
  for(let i=0;i<12;i++)G.dustClouds.push({x:Math.random()*960,y:Math.random()*620,r:30+Math.random()*60,a:.03+Math.random()*.04,sp:.01+Math.random()*.02,hue:Math.random()*360});
  ACH.forEach(a=>a.done=false);
}
initGame();

/* ── helpers ── */
let msgT=0;
function showMsg(t,d){const e=document.getElementById('msg');e.innerHTML=t;e.style.opacity='1';clearTimeout(msgT);if(d<99999)msgT=setTimeout(()=>e.style.opacity='0',d||2000)}
function showAch(l){const e=document.getElementById('ach');document.getElementById('ach-text').textContent=l;e.style.opacity='1';setTimeout(()=>e.style.opacity='0',3000)}
function checkAch(){ACH.forEach(a=>{if(!a.done&&a.check(G)){a.done=true;showAch(a.label)}})}

function part(x,y,col,n,spd,sz,life){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2,s=(Math.random()+.3)*(spd||3);
    G.particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:life||(28+Math.random()*22),color:col,size:(sz||2)+Math.random()*2,maxLife:life||50});
  }
}

function floatDmgPop(x,y,val,col){
  floatDmg.push({x,y,val,col:col||'#fff',life:45,vy:-1.5});
}

function skillCd(id){
  const sk=G.p.skills[id],def=SKILL_DEFS[id];
  if(!def)return Infinity;
  const lv=sk.lv;
  return def.base.cd + (lv>0?def.perLv.cd*lv:0);
}

function skillVal(id,key){
  const sk=G.p.skills[id],def=SKILL_DEFS[id];
  const lv=sk.lv;
  return def.base[key]+(lv>0&&def.perLv[key]?def.perLv[key]*lv:0);
}

/* ── skills ── */
function doDash(){
  const sk=G.p.skills.dash;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('dash');
  const dx=M.x-G.p.x,dy=M.y-G.p.y,d=Math.hypot(dx,dy)||1;
  const power=skillVal('dash','power');
  G.p.vx+=(dx/d)*15*power;G.p.vy+=(dy/d)*15*power;
  part(G.p.x,G.p.y,'#378add',20,6,3,25);
  // afterimage trail
  for(let i=0;i<5;i++)G.trails.push({x:G.p.x,y:G.p.y,a:.6-i*.1,life:15,angle:G.p.angle});
}

function doNuke(){
  const sk=G.p.skills.nuke;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('nuke');
  const r=skillVal('nuke','radius')*200;
  const dmg=skillVal('nuke','dmg');
  const toKill=[];
  G.enemies.forEach(e=>{if(Math.hypot(e.x-G.p.x,e.y-G.p.y)<r){killEnemy(e);toKill.push(e)}});
  G.enemies=G.enemies.filter(e=>!toKill.includes(e));
  if(G.boss&&Math.hypot(G.boss.x-G.p.x,G.boss.y-G.p.y)<r){G.boss.hp-=dmg;if(G.boss.hp<=0)killBoss()}
  part(G.p.x,G.p.y,'#fac775',120,12,4,35);
  G.particles.push({x:G.p.x,y:G.p.y,ring:true,r:0,maxR:r,life:30,color:'#fac775'});
}

function doShield(){
  const sk=G.p.skills.shield;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('shield');
  const restore=skillVal('shield','restore');
  G.p.shield=Math.min(G.p.maxShield,G.p.shield+restore);
  part(G.p.x,G.p.y,'#0ff',30,4,3,30);
}

function doEMP(){
  const sk=G.p.skills.emp;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('emp');
  const dur=skillVal('emp','dur');
  G.enemies.forEach(e=>{e.frozen=dur;part(e.x,e.y,'#afa9ec',6,2,2,20)});
  if(G.boss)G.boss.frozen=dur*.5;
  part(G.p.x,G.p.y,'#afa9ec',60,7,3,25);
  showMsg('⚡ EMP PULSE',700);
}

function doVortex(){
  const sk=G.p.skills.vortex;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  if(sk.lv===0){showMsg('Nâng cấp Vortex trước! (X)',1200);return}
  sk.cd=skillCd('vortex');
  vortexActive={on:true,x:M.x,y:M.y,timer:skillVal('vortex','dur'),suck:skillVal('vortex','suck')};
  part(M.x,M.y,'#7f77dd',40,5,4,40);
}

function doMeteor(){
  const sk=G.p.skills.meteor;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  if(sk.lv===0){showMsg('Nâng cấp Meteor trước! (X)',1200);return}
  sk.cd=skillCd('meteor');
  const count=skillVal('meteor','count');
  const dmg=skillVal('meteor','dmg');
  for(let i=0;i<count;i++){
    const target=G.enemies.length?G.enemies[Math.floor(Math.random()*G.enemies.length)]:null;
    const tx=target?target.x+Math.random()*60-30:Math.random()*W();
    const ty=target?target.y+Math.random()*60-30:Math.random()*H();
    setTimeout(()=>{
      meteors.push({x:tx-80,y:-60,tx,ty,progress:0,dmg,r:14});
    },i*200);
  }
}

/* ── skill upgrade panel ── */
function openSkillUp(){
  if(G.talentOpen||G.gameOver)return;
  G.skillUpOpen=!G.skillUpOpen;
  const panel=document.getElementById('skill-panel');
  panel.style.display=G.skillUpOpen?'block':'none';
  if(G.skillUpOpen)renderSkillUp();
}

function renderSkillUp(){
  const grid=document.getElementById('su-grid');grid.innerHTML='';
  const ids=['dash','nuke','shield','emp','vortex','meteor'];
  ids.forEach(id=>{
    const def=SKILL_DEFS[id],sk=G.p.skills[id];
    const lv=sk.lv,maxLv=def.maxLv;
    const upgCost=lv===0?30:40+lv*25;
    const d=document.createElement('div');d.className='su-card';
    const canBuy=G.gold>=upgCost&&lv<maxLv;
    d.innerHTML=`<div class="su-icon">${def.icon}</div>
      <div class="su-name">${def.name} <span style="color:#555">Lv${lv}/${maxLv}</span></div>
      <div class="su-desc">${def.desc(lv)}</div>
      <div class="su-lv">${lv<maxLv?`Nâng cấp: ${upgCost}g`:'MAX LEVEL'}</div>`;
    if(canBuy)d.onclick=()=>{G.gold-=upgCost;sk.lv=Math.min(maxLv,sk.lv+1);part(G.p.x,G.p.y,'#d4537e',16,4,3,25);renderSkillUp();updateHUD()};
    else if(lv>=maxLv)d.style.borderColor='#fac77540';
    else d.style.opacity='.4';
    grid.appendChild(d);
  });
}

/* ── fire ── */
function fireBullet(fx,fy,angle,spd,col,dmg,owner,pierce,ricochet){
  const count=owner==='player'?1+(G.p.multiShot||0):1;
  for(let i=0;i<count;i++){
    const sp=(i-(count-1)/2)*.17;
    const isCrit=owner==='player'&&Math.random()<(G.p.critChance||0);
    const realDmg=(isCrit?dmg*(G.p.critMult||3):dmg)*(owner==='player'?(G.p.dmgMult||1):1);
    G.bullets.push({x:fx,y:fy,vx:Math.cos(angle+sp)*spd,vy:Math.sin(angle+sp)*spd,
      life:100,size:isCrit?7:4.5,color:isCrit?'#fac775':col,dmg:realDmg,
      owner,pierce:pierce||false,ricochet:ricochet||false,bounces:0,hit:[]});
  }
}

function fireHoming(fx,fy){
  let t=null,nd=9999;
  G.enemies.forEach(e=>{const d=Math.hypot(e.x-fx,e.y-fy);if(d<nd){nd=d;t=e}});
  if(t&&nd<420)G.bullets.push({x:fx,y:fy,vx:0,vy:0,life:140,size:6,color:'#1d9e75',dmg:20*(G.p.dmgMult||1),owner:'player',homing:t,pierce:false,ricochet:false,hit:[]});
}

/* ── enemy spawn ── */
function spawnEnemy(){
  const wv=G.wave,side=Math.floor(Math.random()*4);
  let x,y;const pad=40;
  if(side===0){x=Math.random()*W();y=-pad}
  else if(side===1){x=W()+pad;y=Math.random()*H()}
  else if(side===2){x=Math.random()*W();y=H()+pad}
  else{x=-pad;y=Math.random()*H()}
  const roll=Math.random();
  let type;
  if(wv>=9&&roll<.07)type='guardian';
  else if(wv>=6&&roll<.14)type='elite';
  else if(wv>=4&&roll<.22)type='bomber';
  else if(wv>=3&&roll<.32)type='tank';
  else if(wv>=2&&roll<.46)type='fast';
  else type='normal';
  const cfgs={
    normal:{hp:20+wv*10,speed:1.1+wv*.12,size:13,color:'#e24b4a',xp:10,gold:5,shoot:false,shield:0},
    fast:{hp:12+wv*5,speed:2.2+wv*.18,size:10,color:'#fac775',xp:14,gold:7,shoot:true,shootRate:85,shield:0},
    tank:{hp:80+wv*28,speed:.6+wv*.06,size:22,color:'#7f77dd',xp:30,gold:18,shoot:false,shield:30},
    bomber:{hp:30+wv*9,speed:1.3+wv*.1,size:15,color:'#d85a30',xp:22,gold:13,shoot:true,shootRate:58,shotType:'spread',shield:0},
    elite:{hp:140+wv*35,speed:1.6+wv*.13,size:18,color:'#d4537e',xp:55,gold:38,shoot:true,shootRate:42,shotType:'spiral',shield:20},
    guardian:{hp:220+wv*50,speed:.9+wv*.08,size:24,color:'#639922',xp:80,gold:55,shoot:true,shootRate:55,shotType:'burst',shield:50},
  };
  const cfg=cfgs[type];
  G.enemies.push({x,y,...cfg,maxHp:cfg.hp,maxShield:cfg.shield||0,type,angle:0,shootCd:0,spiralAngle:0,frozen:0,burstCount:0});
}

function spawnBoss(){
  const wv=G.wave;
  const x=Math.random()<.5?-80:W()+80,y=H()/2;
  const phase=wv>=10?3:wv>=6?2:1;
  G.boss={x,y,hp:600+wv*180,maxHp:600+wv*180,speed:.8+wv*.08,size:40,angle:0,shootCd:0,spiralA:0,frozen:0,phase,currentPhase:1,enrage:false,shield:100,maxShield:100};
  document.getElementById('bossbar').style.display='block';
  showMsg(`⚠️  BOSS PHASE ${phase} XUẤT HIỆN!`,2200);
}

function killEnemy(e){
  const gold=Math.round((e.gold||5)*(G.p.goldBonus||1));
  G.score+=e.xp*(1+Math.floor(G.combo/5));
  G.kills++;G.waveKills++;G.gold+=gold;G.totalGold+=gold;
  G.xp+=e.xp;G.combo++;G.comboTimer=150;
  if(G.p.lifesteal)G.p.hp=Math.min(G.p.maxHp,G.p.hp+G.p.lifesteal);
  if(G.p.blast){
    part(e.x,e.y,'#fac775',20,6,4,30);
    G.enemies.forEach(ne=>{if(ne!==e&&Math.hypot(ne.x-e.x,ne.y-e.y)<65){ne.hp-=35;if(ne.hp<=0)killEnemy(ne)}});
  }
  if(Math.random()<.14)G.powerups.push({x:e.x,y:e.y,type:Math.random()<.55?'hp':'shield',life:380});
  if(Math.random()<.1)G.orbs.push({x:e.x,y:e.y,val:Math.max(1,Math.round(gold*.5)),life:440});
  part(e.x,e.y,e.color,12,4,2.5,30);
  checkXP();checkAch();
}

function killBoss(){
  part(G.boss.x,G.boss.y,'#e24b4a',100,10,5,45);
  for(let i=0;i<5;i++)G.particles.push({x:G.boss.x,y:G.boss.y,ring:true,r:0,maxR:120+i*40,life:40,color:'#ff5555'});
  G.score+=800;G.gold+=220;G.totalGold+=220;G.kills+=6;G.bossKills++;
  G.boss=null;document.getElementById('bossbar').style.display='none';
  showMsg('💀 BOSS TIÊU DIỆT! +800pts',2500);
  checkAch();
}

function hitPlayer(dmg){
  const p=G.p;
  if(p.shield>0){p.shield=Math.max(0,p.shield-dmg);part(p.x,p.y,'#378add',5,2,2,15)}
  else{p.hp=Math.max(0,p.hp-dmg);part(p.x,p.y,'#e24b4a',5,2,2,15)}
  if(p.hp<=0&&!G.gameOver){G.gameOver=true;showMsg('☠️ GAME OVER\nScore: '+G.score+'\nNhấn R để chơi lại',99999)}
}

function checkXP(){
  if(G.xp>=G.xpNeeded){G.level++;G.xp=0;G.xpNeeded=Math.floor(G.xpNeeded*1.65);openTalent();checkAch()}
}

function openTalent(){
  G.talentOpen=true;
  const panel=document.getElementById('talent-panel');panel.style.display='block';
  currentTalentTab='atk';
  renderTalentTabs();
  renderTalentGrid();
}

function renderTalentTabs(){
  const tabs=document.getElementById('talent-tabs');tabs.innerHTML='';
  Object.entries(TALENT_CATS).forEach(([key,cat])=>{
    const t=document.createElement('div');t.className='ttab'+(key===currentTalentTab?' active':'');
    t.textContent=cat.label;t.style.color=key===currentTalentTab?cat.color:'';
    t.onclick=()=>{currentTalentTab=key;renderTalentTabs();renderTalentGrid()};
    tabs.appendChild(t);
  });
}

function renderTalentGrid(){
  const grid=document.getElementById('tg');grid.innerHTML='';
  const cat=currentTalentTab;
  const pool=TALENTS.filter(t=>t.cat===cat);
  // pick 3 — prefer not taken (unless stackable and already taken is ok), shuffle
  const notTaken=pool.filter(t=>t.stackable||!takenTalentIds.includes(t.id));
  const taken=pool.filter(t=>!t.stackable&&takenTalentIds.includes(t.id));
  const options=[...notTaken.sort(()=>Math.random()-.5).slice(0,3)];
  // pad with taken ones if not enough
  if(options.length<3)options.push(...taken.sort(()=>Math.random()-.5).slice(0,3-options.length));
  const shown=options.slice(0,3);

  shown.forEach(t=>{
    const alreadyTaken=!t.stackable&&takenTalentIds.includes(t.id);
    const d=document.createElement('div');
    d.className='tc'+(alreadyTaken?' taken':'');
    const hasSynergy=t.synergy&&takenTalentIds.includes(t.synergy);
    const catColor=TALENT_CATS[t.cat].color;
    d.innerHTML=`
      <div class="tc-icon">${t.icon}</div>
      <div class="tc-name" style="color:${catColor}">${t.name}</div>
      <div class="tc-desc">${t.desc}</div>
      <div class="tc-tag ${t.cat}">${t.detail}</div>
      ${hasSynergy?`<div class="tc-synergy">✦ Synergy boost!</div>`:''}
      ${alreadyTaken?'<div style="font-size:9px;color:#fac77580;margin-top:3px">Đã có — chọn lại để stack</div>':''}
    `;
    if(!alreadyTaken){
      d.onclick=()=>{
        t.apply(G.p);
        if(hasSynergy){applyTalentSynergy(t,G.p)}
        takenTalentIds.push(t.id);
        document.getElementById('talent-panel').style.display='none';
        G.talentOpen=false;
        showMsg('★ '+t.name+(hasSynergy?' ✦ SYNERGY!':''),1600);
        part(G.p.x,G.p.y,'#afa9ec',28,5,3,32);
      };
    }
    grid.appendChild(d);
  });

  document.getElementById('talent-synergy-hint').textContent=
    takenTalentIds.length>0?`Bạn có: ${[...new Set(takenTalentIds)].map(id=>TALENTS.find(t=>t.id===id)?.name||id).join(' • ')}`:'';
}

function applyTalentSynergy(talent,p){
  // bonus effects when synergy detected
  if(talent.id==='multishot'&&takenTalentIds.includes('rapidfire'))p.fireRate=Math.max(4,p.fireRate-1);
  if(talent.id==='blast'&&takenTalentIds.includes('vamp'))p.lifesteal+=5;
  if(talent.id==='drone'&&takenTalentIds.includes('missile'))addDrone();
  if(talent.id==='crit'&&takenTalentIds.includes('crit'))p.critMult+=.5;
}

function toggleShop(){
  if(G.talentOpen||G.skillUpOpen||G.gameOver||G.won)return;
  G.shopOpen=!G.shopOpen;
  const s=document.getElementById('shop-panel');s.style.display=G.shopOpen?'block':'none';
  if(G.shopOpen)renderShop();
}

function setShopTab(tab){
  shopTab=tab;
  document.querySelectorAll('.stab').forEach((el,i)=>{
    el.classList.toggle('active',['combat','defense','utility','special'][i]===tab);
  });
  renderShop();
}

function renderShop(){
  document.getElementById('shop-gold').textContent=G.gold;
  const el=document.getElementById('shop-items');el.innerHTML='';
  const items=SHOP_DATA[shopTab]||[];
  const unlockSecret=G.p.blackMarket;

  items.forEach(it=>{
    if(it.secret&&!unlockSecret)return;
    const remaining=it.stock-it.bought;
    const canBuy=G.gold>=it.cost&&remaining>0;
    const d=document.createElement('div');
    d.className='shop-item'+(remaining<=0?' bought':!canBuy?' cant':'');
    d.innerHTML=`
      <span class="si-icon">${it.icon}</span>
      <div class="si-body">
        <div class="si-name">${it.name}</div>
        <div class="si-desc">${it.desc}</div>
        <div class="si-effect">${it.effect}</div>
      </div>
      <div class="si-cost">
        <div class="si-price">${remaining<=0?'SOLD OUT':it.cost+'g'}</div>
        <div class="si-stock">${remaining>0?`còn ${remaining}/${it.stock}`:'—'}</div>
      </div>
    `;
    if(canBuy)d.onclick=()=>{
      G.gold-=it.cost;it.bought++;it.apply(G.p);
      part(G.p.x,G.p.y,'#1d9e75',16,4,3,25);
      renderShop();updateHUD();
    };
    el.appendChild(d);
  });

  if(items.length===0){
    el.innerHTML='<div style="text-align:center;color:#555;padding:20px;font-size:12px">Không có item trong danh mục này</div>';
  }
}

function onKey(code){
  if(code==='KeyE')toggleShop();
  if(code==='KeyX')openSkillUp();
  if(code==='KeyQ')doNuke();
  if(code==='KeyF')doEMP();
  if(code==='KeyW'&&!G.shopOpen&&!G.skillUpOpen&&!G.talentOpen){}
  if(code==='ShiftLeft'||code==='ShiftRight')doDash();
  if(code==='KeyR'&&(G.gameOver||G.won)){initGame();document.getElementById('msg').style.opacity='0'}
  if(code==='KeyT')doMeteor();
}

/* ── HUD ── */
function updateHUD(){
  const p=G.p;
  document.getElementById('hp-fill').style.width=(p.hp/p.maxHp*100)+'%';
  document.getElementById('sh-fill').style.width=(p.shield/p.maxShield*100)+'%';
  document.getElementById('xp-fill').style.width=(G.xp/G.xpNeeded*100)+'%';
  document.getElementById('hp-val').textContent=Math.ceil(p.hp)+'/'+p.maxHp;
  document.getElementById('sh-val').textContent=Math.ceil(p.shield)+'/'+p.maxShield;
  document.getElementById('lv-val').textContent='Lv'+G.level;
  document.getElementById('wn').textContent=G.wave;
  document.getElementById('sv').textContent=G.score.toLocaleString();
  document.getElementById('kv').textContent=G.kills;
  document.getElementById('gv').textContent=G.gold;
  const sk=p.skills;const ids=['dash','nuke','shield','emp','vortex','meteor'];
  ids.forEach((id,i)=>{
    const s=sk[id];const el=document.getElementById('scd'+i);
    el.textContent=s.cd>0?Math.ceil(s.cd/60)+'s':(s.lv>0?'Lv'+s.lv:'—');
    document.getElementById('sk'+i).style.borderColor=s.cd>0?'#ff333355':s.lv>0?'#0ff5':'#fff2';
  });
  const cel=document.getElementById('combo-el');
  if(G.combo>2){cel.textContent='COMBO ×'+G.combo+(G.combo>10?' 🔥':'');cel.style.opacity='1'}else cel.style.opacity='0';
  if(G.boss){
    document.getElementById('boss-fill').style.width=(G.boss.hp/G.boss.maxHp*100)+'%';
    document.getElementById('boss-name').textContent=(G.boss.enrage?'⚠️ ENRAGED ':'')+`BOSS PHASE ${G.boss.currentPhase}/${G.boss.phase}`;
    document.getElementById('boss-phase').textContent=G.boss.enrage?'RAGE MODE ACTIVE':'';
  }
}

/* ── minimap ── */
function drawMinimap(){
  mctx.clearRect(0,0,110,80);
  mctx.fillStyle='rgba(0,0,15,.65)';mctx.fillRect(0,0,110,80);
  const sx=110/W(),sy=80/H();
  G.enemies.forEach(e=>{mctx.fillStyle=e.color;mctx.fillRect(e.x*sx-1.5,e.y*sy-1.5,3,3)});
  if(G.boss){mctx.fillStyle='#ff3333';mctx.beginPath();mctx.arc(G.boss.x*sx,G.boss.y*sy,5,0,Math.PI*2);mctx.fill()}
  G.powerups.forEach(p=>{mctx.fillStyle=p.type==='hp'?'#e24b4a':'#378add';mctx.fillRect(p.x*sx,p.y*sy,3,3)});
  drones.forEach(d=>{mctx.fillStyle='#1d9e75';mctx.fillRect(d.x*sx-1,d.y*sy-1,2,2)});
  mctx.fillStyle='#0ff';mctx.beginPath();mctx.arc(G.p.x*sx,G.p.y*sy,3,0,Math.PI*2);mctx.fill();
  // scan line
  const sl=(G.frame*.02)%(Math.PI*2);
  mctx.strokeStyle='rgba(0,255,150,.2)';mctx.lineWidth=1;
  mctx.beginPath();mctx.moveTo(G.p.x*sx,G.p.y*sy);
  mctx.lineTo(G.p.x*sx+Math.cos(sl)*50,G.p.y*sy+Math.sin(sl)*50);mctx.stroke();
}

/* ── draw background ── */
function drawBG(){
  ctx.fillStyle='#000005';ctx.fillRect(0,0,W(),H());
  // nebulas
  G.nebulas.forEach((n,i)=>{
    const ox=Math.sin(G.frame*.0016+i)*18,oy=Math.cos(G.frame*.0012+i)*14;
    const grd=ctx.createRadialGradient(n.x+ox,n.y+oy,8,n.x+ox,n.y+oy,n.rx);
    grd.addColorStop(0,`hsla(${n.hue},80%,55%,${n.a})`);grd.addColorStop(.5,`hsla(${n.hue+30},60%,40%,${n.a*.5})`);grd.addColorStop(1,'transparent');
    ctx.fillStyle=grd;ctx.save();ctx.scale(1,n.ry/n.rx);ctx.beginPath();ctx.arc(n.x+ox,(n.y+oy)*n.rx/n.ry,n.rx,0,Math.PI*2);ctx.fill();ctx.restore();
  });
  // dust
  G.dustClouds.forEach((dc,i)=>{
    const x=(dc.x+G.frame*dc.sp*.3)%(W()+80);
    const grd=ctx.createRadialGradient(x,dc.y,4,x,dc.y,dc.r);
    grd.addColorStop(0,`hsla(${dc.hue},50%,60%,${dc.a})`);grd.addColorStop(1,'transparent');
    ctx.fillStyle=grd;ctx.beginPath();ctx.arc(x,dc.y,dc.r,0,Math.PI*2);ctx.fill();
  });
  // stars
  G.stars.forEach(s=>{
    const px=(s.x+G.frame*s.sp*.5)%(W()+50);
    const py=(s.y+G.frame*s.sp*.18)%(H()+50);
    const flicker=.55+Math.sin(G.frame*.04+s.b*9)*.45;
    const [r,g,b]=s.col;
    ctx.fillStyle=`rgba(${r},${g},${b},${s.b*flicker*.9})`;
    ctx.beginPath();ctx.arc(px,py,s.s,0,Math.PI*2);ctx.fill();
    if(s.s>1.4){
      ctx.strokeStyle=`rgba(${r},${g},${b},${s.b*.25*flicker})`;ctx.lineWidth=.5;
      ctx.beginPath();ctx.moveTo(px-s.s*2.5,py);ctx.lineTo(px+s.s*2.5,py);ctx.moveTo(px,py-s.s*2.5);ctx.lineTo(px,py+s.s*2.5);ctx.stroke();
    }
  });
}

/* ── draw player ── */
function drawPlayer(){
  const p=G.p;
  // trails (afterimage)
  G.trails.forEach(t=>{
    ctx.save();ctx.translate(t.x,t.y);ctx.rotate(t.angle);ctx.globalAlpha=t.a*.5;
    ctx.fillStyle='#378add';ctx.beginPath();ctx.moveTo(19,0);ctx.lineTo(-11,12);ctx.lineTo(-6,0);ctx.lineTo(-11,-12);ctx.closePath();ctx.fill();
    ctx.restore();
  });
  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.angle);
  // engine glow
  ctx.fillStyle='rgba(0,180,255,0.08)';ctx.beginPath();ctx.arc(0,0,28,0,Math.PI*2);ctx.fill();
  // wings
  ctx.fillStyle='#8899aa';ctx.beginPath();ctx.moveTo(-5,-6);ctx.lineTo(-14,-18);ctx.lineTo(-10,-7);ctx.closePath();ctx.fill();
  ctx.beginPath();ctx.moveTo(-5,6);ctx.lineTo(-14,18);ctx.lineTo(-10,7);ctx.closePath();ctx.fill();
  // body
  ctx.fillStyle='#c0c8d8';ctx.beginPath();ctx.moveTo(20,0);ctx.lineTo(-12,13);ctx.lineTo(-7,0);ctx.lineTo(-12,-13);ctx.closePath();ctx.fill();
  // center stripe
  ctx.fillStyle='#ffffff20';ctx.beginPath();ctx.moveTo(16,0);ctx.lineTo(-8,8);ctx.lineTo(-5,0);ctx.lineTo(-8,-8);ctx.closePath();ctx.fill();
  // engine pods
  ctx.fillStyle='#4466aa';ctx.fillRect(-15,-14,8,7);ctx.fillRect(-15,7,8,7);
  // cockpit
  ctx.fillStyle='rgba(0,230,255,0.95)';ctx.beginPath();ctx.arc(7,0,6,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.7)';ctx.beginPath();ctx.arc(8.5,-1.5,2.2,0,Math.PI*2);ctx.fill();
  // thruster flame
  const fl=5+Math.random()*7;
  ctx.fillStyle='rgba(255,140,30,0.9)';ctx.beginPath();ctx.moveTo(-12,-4.5);ctx.lineTo(-12-fl,0);ctx.lineTo(-12,4.5);ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(255,220,100,0.6)';ctx.beginPath();ctx.moveTo(-12,-2.5);ctx.lineTo(-12-fl*.55,0);ctx.lineTo(-12,2.5);ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(200,230,255,0.8)';ctx.beginPath();ctx.moveTo(-12,-1);ctx.lineTo(-12-fl*.25,0);ctx.lineTo(-12,1);ctx.closePath();ctx.fill();
  ctx.restore();

  // shield ring
  if(G.p.skills.shield.cd>130){
    const alpha=.5+Math.sin(G.frame*.3)*.3;
    ctx.strokeStyle=`rgba(0,255,255,${alpha})`;ctx.lineWidth=2.5;
    ctx.beginPath();ctx.arc(p.x,p.y,34,0,Math.PI*2);ctx.stroke();
    ctx.strokeStyle=`rgba(0,255,255,${alpha*.3})`;ctx.lineWidth=6;
    ctx.stroke();
  }
  // vortex indicator
  if(vortexActive.on){
    ctx.strokeStyle='rgba(127,119,221,0.6)';ctx.lineWidth=1.5;
    ctx.setLineDash([4,4]);
    ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(vortexActive.x,vortexActive.y);ctx.stroke();
    ctx.setLineDash([]);
  }
}

function drawEnemy(e){
  if(e.frozen>0){ctx.save();ctx.globalAlpha=.5+.4*Math.abs(Math.sin(G.frame*.35))}
  ctx.save();ctx.translate(e.x,e.y);
  const spin=e.type==='tank'?.022:.05;e.angle+=spin;ctx.rotate(e.angle);
  ctx.fillStyle=e.color;
  if(e.type==='guardian'){
    // hexagonal with inner detail
    ctx.beginPath();for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.25)';ctx.lineWidth=1.5;ctx.stroke();
    ctx.fillStyle='rgba(0,0,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.5,0,Math.PI*2);ctx.fill();
    ctx.fillStyle=e.color+'88';ctx.beginPath();ctx.arc(0,0,e.size*.25,0,Math.PI*2);ctx.fill();
  } else if(e.type==='tank'){
    ctx.beginPath();for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(0,0,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.42,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.2)';ctx.lineWidth=1;ctx.stroke();
  } else if(e.type==='fast'){
    ctx.beginPath();ctx.moveTo(e.size,0);ctx.lineTo(-e.size*.6,e.size*.9);ctx.lineTo(-e.size*.3,0);ctx.lineTo(-e.size*.6,-e.size*.9);ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.15)';ctx.beginPath();ctx.moveTo(e.size*.5,0);ctx.lineTo(-e.size*.3,e.size*.4);ctx.lineTo(-e.size*.1,0);ctx.lineTo(-e.size*.3,-e.size*.4);ctx.closePath();ctx.fill();
  } else if(e.type==='bomber'){
    ctx.beginPath();for(let i=0;i<5;i++){const a=i/5*Math.PI*2-Math.PI/2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,80,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.38,0,Math.PI*2);ctx.fill();
  } else if(e.type==='elite'){
    ctx.beginPath();for(let i=0;i<8;i++){const a=i/8*Math.PI*2,r=i%2===0?e.size:e.size*.5;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.3)';ctx.lineWidth=1;ctx.stroke();
    ctx.fillStyle='rgba(255,255,255,.12)';ctx.beginPath();ctx.arc(0,0,e.size*.3,0,Math.PI*2);ctx.fill();
  } else {
    ctx.beginPath();for(let i=0;i<4;i++){const a=i/4*Math.PI*2+Math.PI/4;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.18)';ctx.lineWidth=1;ctx.stroke();
  }
  ctx.restore();
  // shield
  if(e.shield>0){
    ctx.strokeStyle=`rgba(55,138,221,${.4+e.shield/e.maxShield*.4})`;ctx.lineWidth=2;
    ctx.beginPath();ctx.arc(e.x,e.y,e.size+5,0,Math.PI*2);ctx.stroke();
  }
  if(e.frozen>0)ctx.restore();
  // HP bar
  const bw=e.size*2.5,bh=4;
  ctx.fillStyle='#fff1';ctx.fillRect(e.x-bw/2,e.y-e.size-12,bw,bh);
  ctx.fillStyle=e.color;ctx.fillRect(e.x-bw/2,e.y-e.size-12,bw*(e.hp/e.maxHp),bh);
}

function drawBoss(b){
  const enrage=b.enrage;
  ctx.save();ctx.translate(b.x,b.y);
  const outerSpin=G.frame*.025*(enrage?2:1);ctx.rotate(outerSpin);
  // outer ring
  ctx.strokeStyle=enrage?'rgba(255,60,0,.35)':'rgba(226,75,74,.2)';ctx.lineWidth=3;
  ctx.beginPath();for(let i=0;i<16;i++){const a=i/16*Math.PI*2,r=i%2===0?b.size*2:b.size*1.7;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.stroke();
  ctx.restore();
  ctx.save();ctx.translate(b.x,b.y);b.angle+=enrage?.055:.032;ctx.rotate(b.angle);
  const pulse=.94+Math.sin(G.frame*.09)*.06;
  // aura
  ctx.fillStyle=enrage?'rgba(255,50,50,.12)':'rgba(226,75,74,.1)';
  ctx.beginPath();ctx.arc(0,0,b.size*1.85*pulse,0,Math.PI*2);ctx.fill();
  // body
  ctx.fillStyle=enrage?'#ff2222':'#e24b4a';
  ctx.beginPath();for(let i=0;i<12;i++){const a=i/12*Math.PI*2,r=i%2===0?b.size:b.size*.55;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();
  if(enrage){ctx.strokeStyle='rgba(255,200,0,.6)';ctx.lineWidth=2.5;ctx.stroke()}
  // inner
  ctx.fillStyle='rgba(255,80,80,.7)';ctx.beginPath();ctx.arc(0,0,b.size*.38,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='rgba(255,200,200,.5)';ctx.beginPath();ctx.arc(0,0,b.size*.16,0,Math.PI*2);ctx.fill();
  ctx.restore();
  // HP bar above
  const bw=90,bh=7;
  ctx.fillStyle='#fff1';ctx.fillRect(b.x-bw/2,b.y-b.size-16,bw,bh);
  ctx.fillStyle=enrage?'#ff2222':'#e24b4a';ctx.fillRect(b.x-bw/2,b.y-b.size-16,bw*(b.hp/b.maxHp),bh);
  // shield ring
  if(b.shield>0){
    ctx.strokeStyle=`rgba(55,138,221,${.3+b.shield/b.maxShield*.4})`;ctx.lineWidth=3;
    ctx.beginPath();ctx.arc(b.x,b.y,b.size+10,0,Math.PI*2);ctx.stroke();
  }
}

function drawDrone(dr){
  const x=G.p.x+Math.cos(dr.a)*dr.orb,y=G.p.y+Math.sin(dr.a)*dr.orb;
  dr.x=x;dr.y=y;
  // orbit path
  ctx.strokeStyle='rgba(29,158,117,.18)';ctx.lineWidth=1;ctx.setLineDash([3,7]);
  ctx.beginPath();ctx.arc(G.p.x,G.p.y,dr.orb,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);
  // body
  ctx.save();ctx.translate(x,y);ctx.rotate(G.frame*.06);
  ctx.fillStyle='#1d9e75';
  ctx.beginPath();for(let i=0;i<4;i++){const a=i/4*Math.PI*2+Math.PI/4;ctx.lineTo(Math.cos(a)*7,Math.sin(a)*7)}ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(93,202,165,.8)';ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();
  ctx.restore();
}

function drawMeteors(){
  meteors.forEach(m=>{
    m.progress=Math.min(1,m.progress+.025);
    m.x=m.x+(m.tx-m.x)*m.progress;m.y=m.y+(m.ty-m.y)*m.progress;
    // trail
    ctx.strokeStyle='rgba(255,100,30,.6)';ctx.lineWidth=3;
    ctx.beginPath();ctx.moveTo(m.x-Math.cos(Math.atan2(m.ty-(-60),m.tx-(m.x-80)))*20,m.y-Math.sin(Math.atan2(m.ty-(-60),m.tx-(m.x-80)))*20);
    ctx.lineTo(m.x,m.y);ctx.stroke();
    ctx.fillStyle='#fac775';ctx.beginPath();ctx.arc(m.x,m.y,m.r,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='rgba(255,200,100,.6)';ctx.beginPath();ctx.arc(m.x,m.y,m.r*.5,0,Math.PI*2);ctx.fill();
    // impact
    if(m.progress>=1){
      part(m.x,m.y,'#fac775',20,7,4,30);
      G.enemies.forEach(e=>{if(Math.hypot(e.x-m.x,e.y-m.y)<50){e.hp-=m.dmg;floatDmgPop(e.x,e.y-20,Math.round(m.dmg),'#fac775');if(e.hp<=0){killEnemy(e)}}});
      if(G.boss&&Math.hypot(G.boss.x-m.x,G.boss.y-m.y)<60){G.boss.hp-=m.dmg;if(G.boss.hp<=0)killBoss()}
    }
  });
  meteors=meteors.filter(m=>m.progress<1);
}

function drawVortex(){
  if(!vortexActive.on)return;
  vortexActive.timer--;
  if(vortexActive.timer<=0){vortexActive.on=false;return}
  const {x,y,suck}=vortexActive;
  const spin=G.frame*.12,r=80;
  // rings
  for(let i=0;i<3;i++){
    ctx.strokeStyle=`rgba(127,119,221,${.3-i*.08})`;ctx.lineWidth=2-i*.4;
    ctx.beginPath();ctx.arc(x,y,r*(i+1)*.4,0,Math.PI*2);ctx.stroke();
  }
  // spiraling particles
  for(let i=0;i<3;i++){
    const a=spin+i*2.094;const pr=r*(.3+Math.abs(Math.sin(G.frame*.08+i))*.7);
    ctx.fillStyle='rgba(175,169,236,.7)';ctx.beginPath();ctx.arc(x+Math.cos(a)*pr,y+Math.sin(a)*pr,3,0,Math.PI*2);ctx.fill();
  }
  // suck enemies
  G.enemies.forEach(e=>{
    const dx=x-e.x,dy=y-e.y,dist=Math.hypot(dx,dy)||1;
    if(dist<r*2){e.x+=dx/dist*suck;e.y+=dy/dist*suck}
  });
}

function drawParticles(){
  G.particles.forEach(p=>{
    if(p.ring){
      p.r+=(p.maxR-p.r)*.12;p.life--;
      ctx.strokeStyle=p.color.replace(')',`,${p.life/30})`).replace('rgb','rgba').replace('#','rgba(').replace(/^rgba\(([^)]+)\)$/,(m,c)=>{const hex=parseInt(c,16);return`rgba(${hex>>16&255},${hex>>8&255},${hex&255},${p.life/30})`});
      ctx.strokeStyle=p.color;ctx.globalAlpha=p.life/30;ctx.lineWidth=3;
      ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.stroke();ctx.globalAlpha=1;
    } else {
      p.x+=p.vx;p.y+=p.vy;p.vx*=.89;p.vy*=.89;p.life--;
      ctx.globalAlpha=p.life/45;ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;
    }
  });
  G.particles=G.particles.filter(p=>p.life>0);
}

/* ── main update ── */
function update(){
  if(G.gameOver||G.won||G.talentOpen)return;
  G.frame++;
  const p=G.p;
  // movement
  let mx=0,my=0;
  if(!G.shopOpen&&!G.skillUpOpen){
    if(keys['ArrowLeft']||keys['KeyA'])mx=-1;
    if(keys['ArrowRight']||keys['KeyD'])mx=1;
    if(keys['ArrowUp']||keys['KeyW'])my=-1;
    if(keys['ArrowDown']||keys['KeyS'])my=1;
  }
  if(mx||my){const l=Math.hypot(mx,my)||1;p.vx=mx/l*p.speed;p.vy=my/l*p.speed}
  p.vx*=.82;p.vy*=.82;
  p.x=Math.max(20,Math.min(W()-20,p.x+p.vx));
  p.y=Math.max(20,Math.min(H()-20,p.y+p.vy));
  p.angle=Math.atan2(M.y-p.y,M.x-p.x);

  // passive
  const shieldSk=p.skills.shield;
  if(shieldSk.cd===0)p.shield=Math.min(p.maxShield,p.shield+.08);
  if(p.regen&&G.frame%60===0)p.hp=Math.min(p.maxHp,p.hp+(p.regenAmt||3));

  // cooldowns
  Object.values(p.skills).forEach(sk=>{if(sk.cd>0)sk.cd--});

  // manual shield activation
  if(keys['KeyW']&&!G.shopOpen&&!G.skillUpOpen)doShield();

  // fire
  p.fireCd--;
  if(M.dn&&p.fireCd<=0&&!G.shopOpen&&!G.skillUpOpen){
    p.fireCd=p.fireRate;
    fireBullet(p.x,p.y,p.angle,p.bulletSpeed,'#0af',10,'player',p.pierce,p.ricochet);
    if(p.missiles&&G.frame%52===0)fireHoming(p.x,p.y);
  }

  if(G.comboTimer>0)G.comboTimer--;else G.combo=0;

  // bullets
  G.bullets=G.bullets.filter(b=>{
    if(b.homing&&b.homing&&b.homing.hp>0){
      const tx=b.homing.x-b.x,ty=b.homing.y-b.y,d=Math.hypot(tx,ty)||1;
      b.vx+=tx/d*.55;b.vy+=ty/d*.55;const sp=Math.hypot(b.vx,b.vy);if(sp>8){b.vx=b.vx/sp*8;b.vy=b.vy/sp*8}
    }
    b.x+=b.vx;b.y+=b.vy;b.life--;
    if(b.life<=0||b.x<-40||b.x>W()+40||b.y<-40||b.y>H()+40){
      if(b.ricochet&&b.bounces<2){
        if(b.x<-20||b.x>W()+20)b.vx*=-1;else b.vy*=-1;
        b.bounces++;b.life=80;return true;
      }
      return false;
    }
    if(b.owner==='player'){
      const toKill=[];
      for(let i=G.enemies.length-1;i>=0;i--){
        const e=G.enemies[i];
        if(b.hit.includes(e))continue;
        if(Math.hypot(b.x-e.x,b.y-e.y)<e.size+b.size){
          const dmg=e.frozen>0?b.dmg*1.6:b.dmg;
          if(e.shield>0){e.shield=Math.max(0,e.shield-dmg*.5)}
          else{e.hp-=dmg;floatDmgPop(e.x,e.y-e.size-15,Math.round(dmg),b.color)}
          part(b.x,b.y,b.color,4,3,2,18);
          if(e.hp<=0)toKill.push(i);
          if(b.pierce){b.hit.push(e)}else{
            const kens=[...toKill];kens.forEach(idx=>{const en=G.enemies[idx];killEnemy(en)});
            G.enemies=G.enemies.filter((_,i)=>!toKill.includes(i));return false;
          }
        }
      }
      if(toKill.length){const kens=toKill.map(i=>G.enemies[i]);kens.forEach(en=>killEnemy(en));G.enemies=G.enemies.filter((_,i)=>!toKill.includes(i))}
      if(G.boss&&Math.hypot(b.x-G.boss.x,b.y-G.boss.y)<G.boss.size+b.size){
        if(G.boss.shield>0)G.boss.shield=Math.max(0,G.boss.shield-b.dmg*.4);
        else{G.boss.hp-=b.dmg;floatDmgPop(G.boss.x,G.boss.y-G.boss.size-15,Math.round(b.dmg),'#ff5555')}
        part(b.x,b.y,b.color,3,3,2,15);if(G.boss.hp<=0)killBoss();return false;
      }
    } else {
      if(Math.hypot(b.x-p.x,b.y-p.y)<22){hitPlayer(b.dmg);return false}
    }
    return true;
  });

  // enemies AI
  G.enemies.forEach(e=>{
    if(e.frozen>0){e.frozen--;return}
    const dx=p.x-e.x,dy=p.y-e.y,dist=Math.hypot(dx,dy)||1;
    e.x+=dx/dist*e.speed;e.y+=dy/dist*e.speed;
    if(dist<28)hitPlayer(1.5);
    if(e.shoot){
      e.shootCd--;
      if(e.shootCd<=0){
        e.shootCd=e.shootRate||80;
        const angle=Math.atan2(dy,dx);
        if(e.shotType==='spread'){for(let i=-1;i<=1;i++)fireBullet(e.x,e.y,angle+i*.3,3.2,'#d85a30',7,'enemy')}
        else if(e.shotType==='spiral'){e.spiralAngle+=.42;for(let i=0;i<3;i++)fireBullet(e.x,e.y,e.spiralAngle+i*2.094,3.3,'#d4537e',7,'enemy')}
        else if(e.shotType==='burst'){
          e.burstCount=(e.burstCount||0)+1;
          fireBullet(e.x,e.y,angle+(Math.random()-.5)*.2,3.8,'#97c459',9,'enemy');
          if(e.burstCount%3===0)for(let i=0;i<6;i++)fireBullet(e.x,e.y,i/6*Math.PI*2,2.8,'#639922',7,'enemy');
        }
        else fireBullet(e.x,e.y,angle+(Math.random()-.5)*.15,3.2,'#fac775',7,'enemy');
      }
    }
  });

  // boss AI
  if(G.boss){
    const b=G.boss;
    if(b.frozen>0){b.frozen--}
    else{
      const dx=p.x-b.x,dy=p.y-b.y,dist=Math.hypot(dx,dy)||1;
      b.x+=dx/dist*b.speed;b.y+=dy/dist*b.speed;
      if(dist<52)hitPlayer(2.5);
      const enrage=b.hp<b.maxHp*.3;
      if(!b.enrage&&enrage){b.enrage=true;b.currentPhase=Math.min(b.phase,b.currentPhase+1);showMsg('⚠️ BOSS ENRAGED!',2000)}
      b.shootCd--;
      if(b.shootCd<=0){
        b.shootCd=enrage?18:32;b.spiralA+=enrage?.62:.34;
        const shots=enrage?7:4,spd=enrage?4.5:3.8;
        for(let i=0;i<shots;i++)fireBullet(b.x,b.y,b.spiralA+i*(Math.PI*2/shots),spd,'#ff5555',enrage?16:12,'enemy');
        if(enrage&&G.frame%90===0)for(let i=0;i<12;i++)fireBullet(b.x,b.y,i/12*Math.PI*2,2.5,'#ff3333',8,'enemy');
      }
      if(b.shield>0)b.shield=Math.min(b.maxShield,b.shield+.05);
    }
  }

  // drones
  drones.forEach(dr=>{
    dr.a+=.038;dr.cd--;
    const x=G.p.x+Math.cos(dr.a)*dr.orb,y=G.p.y+Math.sin(dr.a)*dr.orb;dr.x=x;dr.y=y;
    if(dr.cd<=0){
      dr.cd=32;let near=null,nd=9999;
      G.enemies.forEach(e=>{const d=Math.hypot(e.x-x,e.y-y);if(d<nd){nd=d;near=e}});
      if(near&&nd<250)fireBullet(x,y,Math.atan2(near.y-y,near.x-x),7.5,'#1d9e75',10*(G.p.dmgMult||1),'player');
    }
  });

  // powerups & orbs
  G.powerups=G.powerups.filter(pu=>{
    pu.life--;
    const d=Math.hypot(pu.x-p.x,pu.y-p.y);
    if(p.magnet&&d<130){pu.x+=(p.x-pu.x)*.14;pu.y+=(p.y-pu.y)*.14}
    if(d<20){if(pu.type==='hp')p.hp=Math.min(p.maxHp,p.hp+30);else p.shield=Math.min(p.maxShield,p.shield+40);part(pu.x,pu.y,pu.type==='hp'?'#e24b4a':'#378add',10,3,2.5,25);return false}
    return pu.life>0;
  });
  G.orbs=G.orbs.filter(o=>{
    o.life--;
    const d=Math.hypot(o.x-p.x,o.y-p.y);
    if(p.magnet&&d<140){o.x+=(p.x-o.x)*.12;o.y+=(p.y-o.y)*.12}
    if(d<24){G.gold+=o.val;G.totalGold+=o.val;part(o.x,o.y,'#fac775',6,3,2,20);return false}
    return o.life>0;
  });

  // trails
  G.trails=G.trails.filter(t=>{t.life--;t.a-=.04;return t.life>0});
  // floatdmg
  floatDmg=floatDmg.filter(f=>{f.y+=f.vy;f.life--;return f.life>0});

  // wave management
  if(!G.waveBreak&&!G.boss){
    G.spawnTimer--;
    const rate=Math.max(20,105-G.wave*9),maxE=Math.min(28,10+G.wave*2);
    if(G.spawnTimer<=0&&G.enemies.length<maxE){G.spawnTimer=rate;spawnEnemy()}
    if(G.waveKills>=G.waveTarget&&G.enemies.length===0){
      if(G.wave%3===0&&!G.bossSpawned){G.bossSpawned=true;spawnBoss()}
      else if(G.wave%3!==0||!G.boss){
        G.waveBreak=true;G.waveBreakTimer=220;G.bossSpawned=false;
        showMsg(`✓ WAVE ${G.wave} CLEAR!\n+${20+G.wave*8} vàng`,2200);
        G.gold+=20+G.wave*8;G.totalGold+=20+G.wave*8;
        G.wave++;G.waveKills=0;G.waveTarget=Math.ceil(G.waveTarget*1.3);
        if(G.wave>15){G.won=true;showMsg('🏆 CHIẾN THẮNG!\nScore: '+G.score.toLocaleString()+'\nNhấn R để chơi lại',99999)}
        checkAch();
      }
    }
  } else if(G.waveBreak){G.waveBreakTimer--;if(G.waveBreakTimer<=0){G.waveBreak=false;G.spawnTimer=90}}

  checkAch();updateHUD();drawMinimap();
}

/* ── draw ── */
function draw(){
  drawBG();
  // vortex
  drawVortex();
  // orbs
  G.orbs.forEach(o=>{
    ctx.fillStyle='rgba(250,199,117,.9)';ctx.beginPath();ctx.arc(o.x,o.y,7,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#fff';ctx.font='bold 8px monospace';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(o.val,o.x,o.y);
  });
  // powerups
  G.powerups.forEach(pu=>{
    const pulse=.85+Math.sin(G.frame*.14)*.15;
    ctx.fillStyle=pu.type==='hp'?`rgba(226,75,74,${pulse})`:`rgba(55,138,221,${pulse})`;
    ctx.beginPath();ctx.arc(pu.x,pu.y,10,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.9)';ctx.font='11px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(pu.type==='hp'?'♥':'⚡',pu.x,pu.y);
  });
  // drones
  drones.forEach(drawDrone);
  // particles
  drawParticles();
  // bullets
  G.bullets.forEach(b=>{
    if(b.homing){
      ctx.strokeStyle=b.color;ctx.lineWidth=2.5;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.stroke();
      ctx.fillStyle=b.color+'60';ctx.beginPath();ctx.arc(b.x,b.y,b.size+3,0,Math.PI*2);ctx.fill();
    } else {
      ctx.fillStyle=b.color;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.fill();
      if(b.size>5){ctx.fillStyle=b.color+'40';ctx.beginPath();ctx.arc(b.x,b.y,b.size*1.8,0,Math.PI*2);ctx.fill()}
    }
  });
  // enemies
  G.enemies.forEach(drawEnemy);
  if(G.boss)drawBoss(G.boss);
  // meteors
  drawMeteors();
  // player
  if(!G.gameOver)drawPlayer();
  // nuke ring
  if(G.p.skills.nuke.cd>200){
    const prog=(G.p.skills.nuke.cd-200)/40;
    const r=skillVal('nuke','radius')*200*(1-prog);
    ctx.strokeStyle=`rgba(250,199,117,${prog*.8})`;ctx.lineWidth=4;
    ctx.beginPath();ctx.arc(G.p.x,G.p.y,r,0,Math.PI*2);ctx.stroke();
  }
  // wave flash
  if(G.waveBreak){const t=G.waveBreakTimer/220;ctx.fillStyle=`rgba(0,255,180,${t*.1})`;ctx.fillRect(0,0,W(),H())}
  // float damage numbers
  floatDmg.forEach(f=>{
    ctx.globalAlpha=f.life/45;ctx.font=`bold ${10+Math.min(6,f.val/20)}px monospace`;ctx.fillStyle=f.col;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(f.val,f.x,f.y);
  });
  ctx.globalAlpha=1;
}

function loop(){
  if(!G.shopOpen&&!G.talentOpen&&!G.skillUpOpen)update();
  draw();requestAnimationFrame(loop);
}
loop();
setTimeout(()=>showMsg('WASD di chuyển • Chuột bắn • Q Nuke • Shift Dash • F EMP • R Vortex • T Meteor • E Shop • X Nâng cấp kỹ năng',4000),600);
    </script>
</body>

</html>
 

AnNTH

🔫 Súng Lục Nhựa
Hello em!
Anh là mod quản lý khu vực AI và kích hoạt Windows/Office. Ngoài đời anh cũng làm chuyên ngành liên quan tới lập trình.

Đầu tiên anh rất là ấn tượng về việc em đang nghiên cứu lập trình ở độ tuổi nhỏ và đang tập ứng dụng AI vào nghiên cứu, học tập, cuộc sống,... Về tựa game em nhờ AI viết theo ý tưởng của em rất là chi tiết, đầy đủ chức năng cho một webpage tĩnh. Anh sử dụng code của em deploy lên trang host miễn phí, đường dẫn tại: jade-puppy-ad9472.netlify.app

Em cũng có thể nghiên cứu cách sử dụng ở https://www.netlify.com/ để tải lên những web tĩnh sau này em làm. Có thể nhờ AI hướng dẫn hoặc nhắn anh nếu em có câu hỏi về host nói riêng cũng như lập trình nói chung.

Nghiên cứu code theo sở thích là rất tốt và ấn tượng, nhưng đừng để ảnh hưởng tới việc học tập các môn học trên trường nha.

Nhớ sửa lại bài viết nha. Ví dụ tạo một file code .html lưu lên google drive rồi gửi link vào đây, mô tả ngắn gọn về code, chức năng,... để người đọc biết code này dùng để làm gì. Gửi link tĩnh như anh làm để người đọc có thể xem ví dụ.

Chúc em nghiên cứu về lập trình vui.
 

vietcuber

Búa Gỗ Đôi
Hôm qua thấy file code dài quá mà vẫn gắng copy hết đê test
Chơi cũng hay đó. Nay thấy được mọi người update ủng hộ lại thì vui hơn :D
1775704875677.png
 

SieuSaiyanRose

Búa Gỗ
Game ổn đấy ad, thiết kế thêm phi thuyền cho sinh động lên nữa, update giao diện thêm nữa là đẹp.
 

naroto

Hoài niệm về năm tháng ấy...!
Góc review games:

Cảm nghĩ của học sinh lớp 4 khi mới chơi:
khi em vào chơi em thấy rất hay, từng level càng ngài level càng khó nhưng em rất vui ạ vì em chơi tới level 10 ;) rồi ạ. bye
 

plesiosaurus

Rìu Sắt Đôi
Quăng cho cái link donate đi, để anh em ủng hộ ly cafe, chứ ông định làm vì đam mê thật à {big_smile}
 

omaigot

Gà con
trải nghiệm phiên bản mới nhất:
menu game:
Screenshot 2026-04-13 130341.png

  • Dễ (Easy): HP và Khiên người chơi x1.5; máu kẻ địch giảm còn 0.6; vàng nhận được x1.5. Có hồi máu tự động.
  • Trung bình (Normal): Trải nghiệm tiêu chuẩn với 15 đợt tấn công (waves) và 3 Boss.
  • Khó (Hard): Kẻ địch mạnh hơn (x1.5); không có hồi máu tự động; Boss nổi giận sớm hơn.
  • Bất khả thi (Impossible): Kẻ địch cực mạnh (x2.5); có đạn độc; Boss xuất hiện ngay từ Wave 1; giá vật phẩm trong Shop gấp đôi.
    • Di chuyển: Sử dụng các phím WASD.
    • Tấn công: Sử dụng Chuột để nhắm và bắn.
    • Kỹ năng đặc biệt:
      • Shift: Lướt (Dash) để né tránh.
      • Q: Nova Bomb (Nổ diện rộng).
      • W: Barrier (Lớp bảo vệ/Khiên).
      • F: EMP (Làm tê liệt kẻ địch).
      • R: Vortex (Hố đen hút kẻ địch).
      • T: Meteor (Thiên thạch tấn công).Hệ thống Combo: Ghi nhận số lượng kẻ địch tiêu diệt liên tiếp để tăng điểm số.
HTML:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Galaxy Roguelike — V4</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;user-select:none}
body{background:#0a0a12;margin:0;padding:0;font-family:'Courier New',monospace;overflow:hidden}
#wrap{position:relative;width:100vw;height:100vh;overflow:hidden;background:#000;border-radius:0;border:none;box-shadow:none}
#gc{display:block;cursor:crosshair}
#ui{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}

/* HUD */
.hud-top{display:flex;justify-content:space-between;align-items:flex-start;padding:12px 18px;background:linear-gradient(rgba(0,0,0,.8),transparent);gap:20px}
.stat-col{display:flex;flex-direction:column;gap:6px}
.bar-row{display:flex;align-items:center;gap:7px;font-size:11px}
.bar-bg{border-radius:4px;overflow:hidden;background:#ffffff12;position:relative}
.bar-fill{height:8px;border-radius:4px;transition:width .25s}
#hp-fill{background:linear-gradient(90deg,#a32d2d,#e24b4a)}
#sh-fill{background:linear-gradient(90deg,#185fa5,#378add)}
#xp-fill{background:linear-gradient(90deg,#0f6e56,#1d9e75)}
.wave-center{text-align:center}
.wave-label{font-size:10px;color:#555;letter-spacing:2px;text-transform:uppercase}
.wave-num{font-size:38px;font-weight:700;color:#0ff;line-height:1;text-shadow:0 0 20px #0ff6}
.stats-right{text-align:right;font-size:11px;color:#555;line-height:2.1}
.stats-right b{color:#fac775;font-size:13px}

/* skill bar */
#skillbar{position:absolute;bottom:0;left:50%;transform:translateX(-50%);display:flex;gap:6px;padding:10px 14px;background:linear-gradient(transparent,rgba(0,0,0,.75));border-radius:0 0 16px 16px;pointer-events:none}
.sk{width:48px;height:48px;border-radius:10px;border:1px solid #fff2;background:#ffffff08;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:20px;position:relative;overflow:hidden}
.sk-overlay{position:absolute;bottom:0;left:0;right:0;background:#0009;text-align:center;font-size:9px;color:#aaa;padding:1px 0 2px}
.sk-key{position:absolute;top:2px;right:4px;font-size:8px;color:#ffffff50;font-family:monospace}
.sk-label{position:absolute;bottom:26px;font-size:7px;color:#ffffff40;white-space:nowrap}

/* boss bar */
#bossbar{position:absolute;top:62px;left:50%;transform:translateX(-50%);width:340px;display:none;pointer-events:none;text-align:center}
#boss-name{font-size:11px;color:#ff5555;letter-spacing:2px;margin-bottom:4px;text-transform:uppercase}
#boss-bg{background:#fff1;border-radius:5px;height:12px;overflow:hidden;border:1px solid #ff333330}
#boss-fill{height:100%;background:linear-gradient(90deg,#791f1f,#e24b4a);border-radius:5px;transition:width .15s}
#boss-phase{font-size:9px;color:#ff555580;margin-top:3px;letter-spacing:1px}

/* combo */
#combo-el{position:absolute;top:68px;left:50%;transform:translateX(-50%);font-size:20px;font-weight:700;color:#fac775;opacity:0;transition:opacity .3s;pointer-events:none;text-shadow:0 0 12px #fac77580;white-space:nowrap}

/* floating dmg */
.floatdmg{position:absolute;pointer-events:none;font-size:13px;font-weight:700;transition:none}

/* minimap */
#minimap{position:absolute;bottom:68px;right:12px;border:1px solid #fff2;border-radius:8px;opacity:.8;pointer-events:none}
#minimap-label{position:absolute;bottom:142px;right:12px;font-size:9px;color:#fff3;letter-spacing:1px;pointer-events:none}

/* messages */
#msg{position:absolute;top:48%;left:50%;transform:translate(-50%,-50%);color:#fff;font-size:24px;font-weight:700;text-align:center;pointer-events:none;opacity:0;transition:opacity .35s;line-height:1.6;text-shadow:0 0 20px #0ff8}

/* panels */
.panel{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:16px;padding:24px;color:#fff;pointer-events:all;display:none;backdrop-filter:blur(2px)}
#shop-panel{background:rgba(2,4,20,.97);border:1px solid #0ff3;width:420px;max-height:520px;overflow-y:auto}
#shop-panel h3{color:#0ff;margin-bottom:6px;font-size:13px;text-align:center;letter-spacing:2px}
.shop-tabs{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid #ffffff12;padding-bottom:8px}
.stab{flex:1;padding:6px 4px;text-align:center;font-size:10px;color:#555;cursor:pointer;border-radius:6px 6px 0 0;transition:.15s}
.stab.active{color:#0ff;background:#0ff08;border-bottom:2px solid #0ff}
.shop-item{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;margin:4px 0;background:#ffffff07;border-radius:10px;border:1px solid #ffffff12;cursor:pointer;transition:.15s;gap:10px}
.shop-item:hover{background:#0ff08;border-color:#0ff4}
.shop-item.cant{opacity:.3;cursor:not-allowed}
.shop-item.bought{border-color:#1d9e7540;background:#1d9e7508;opacity:.6;cursor:not-allowed}
.si-icon{font-size:20px;min-width:26px;text-align:center}
.si-body{flex:1}
.si-name{font-size:12px;font-weight:700;color:#ddd}
.si-desc{font-size:10px;color:#555;margin-top:2px}
.si-effect{font-size:9px;color:#1d9e75;margin-top:2px}
.si-cost{display:flex;flex-direction:column;align-items:flex-end;gap:2px}
.si-price{color:#fac775;font-size:12px;font-weight:700;white-space:nowrap}
.si-stock{font-size:9px;color:#555}
.shop-gold-bar{display:flex;align-items:center;gap:6px;margin-bottom:10px;padding:8px 10px;background:#fac77510;border-radius:8px;border:1px solid #fac77520}
.shop-gold-icon{font-size:16px}
.shop-gold-val{font-size:16px;font-weight:700;color:#fac775}
.shop-gold-label{font-size:10px;color:#777}

#talent-panel{background:rgba(4,2,22,.98);border:1px solid #7f77dd;width:500px;max-height:520px;overflow-y:auto}
#talent-panel h3{color:#afa9ec;margin-bottom:4px;text-align:center;font-size:13px;letter-spacing:2px}
.talent-tabs{display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid #ffffff12;padding-bottom:8px}
.ttab{flex:1;padding:6px 4px;text-align:center;font-size:10px;color:#555;cursor:pointer;border-radius:6px 6px 0 0;transition:.15s;letter-spacing:.5px}
.ttab.active{color:#afa9ec;background:#7f77dd18;border-bottom:2px solid #7f77dd}
.talent-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}
.tc{background:#ffffff07;border:1px solid #7f77dd30;border-radius:12px;padding:12px 8px;cursor:pointer;text-align:center;transition:.18s;position:relative}
.tc:hover{background:#7f77dd18;border-color:#7f77dd;transform:translateY(-2px)}
.tc.taken{border-color:#fac77560;background:#fac77508;cursor:default}
.tc.taken::after{content:'✓';position:absolute;top:6px;right:8px;font-size:10px;color:#fac775}
.tc-icon{font-size:26px;margin-bottom:5px}
.tc-name{font-size:11px;font-weight:700;color:#afa9ec;margin-bottom:2px}
.tc-desc{font-size:9px;color:#555}
.tc-tag{display:inline-block;font-size:8px;padding:1px 5px;border-radius:4px;margin-top:4px;color:#afa9ec;background:#7f77dd25;border:1px solid #7f77dd30}
.tc-tag.off{color:#e24b4a;background:#e24b4a18;border-color:#e24b4a30}
.tc-tag.atk{color:#fac775;background:#fac77518;border-color:#fac77530}
.tc-tag.def{color:#378add;background:#378add18;border-color:#378add30}
.tc-tag.uti{color:#1d9e75;background:#1d9e7518;border-color:#1d9e7530}
.tc-synergy{font-size:9px;color:#7f77dd80;margin-top:3px;font-style:italic}

/* achievement */
#ach{position:absolute;top:90px;right:14px;background:rgba(2,12,2,.95);border:1px solid #1d9e75;border-radius:10px;padding:10px 14px;font-size:11px;pointer-events:none;opacity:0;transition:opacity .4s;max-width:220px}
#ach-title{color:#1d9e75;font-size:9px;letter-spacing:2px;margin-bottom:3px;text-transform:uppercase}

/* skill upgrade panel */
#skill-panel{background:rgba(4,2,22,.98);border:1px solid #d4537e;width:400px}
#skill-panel h3{color:#ed93b1;margin-bottom:16px;text-align:center;font-size:13px;letter-spacing:2px}
.skill-upgrade-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.su-card{background:#ffffff07;border:1px solid #d4537e30;border-radius:12px;padding:14px;cursor:pointer;transition:.18s}
.su-card:hover{background:#d4537e18;border-color:#d4537e}
.su-card .su-icon{font-size:26px;margin-bottom:6px}
.su-card .su-name{font-size:12px;font-weight:700;color:#ed93b1}
.su-card .su-desc{font-size:10px;color:#555;margin-top:3px}
.su-card .su-lv{font-size:10px;color:#fac775;margin-top:4px}

/* controls hint */
#hint{position:absolute;bottom:68px;left:12px;font-size:9px;color:#ffffff25;line-height:2;pointer-events:none}

/* ── MENU SCREEN ── */
#menu-screen{position:absolute;inset:0;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000008;border-radius:16px;overflow:hidden}
#menu-bg{position:absolute;inset:0;pointer-events:none}
#menu-content{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;width:100%;gap:0}

/* Logo */
.menu-logo{text-align:center;margin-bottom:32px}
.menu-logo-title{font-size:52px;font-weight:900;color:#fff;letter-spacing:8px;line-height:1;text-transform:uppercase;text-shadow:0 0 40px #0ff4,0 0 80px #0ff1}
.menu-logo-title span{color:#0ff;text-shadow:0 0 20px #0ff}
.menu-logo-sub{font-size:10px;color:#ffffff35;letter-spacing:6px;margin-top:8px;text-transform:uppercase}

/* Difficulty selector */
.diff-row{display:flex;gap:8px;margin-bottom:24px}
.diff-btn{
  padding:8px 18px;border-radius:8px;border:1px solid transparent;
  font-family:'Courier New',monospace;font-size:11px;font-weight:700;letter-spacing:2px;
  cursor:pointer;transition:.18s;text-transform:uppercase;background:transparent;color:#555;position:relative;
}
.diff-btn:not(.locked):hover{color:#fff;border-color:#fff3;background:#ffffff08}
.diff-btn.locked{cursor:not-allowed;opacity:.4}
.diff-btn.locked::after{content:'🔒';font-size:8px;margin-left:4px}
.diff-btn.active-easy{color:#1d9e75;border-color:#1d9e7560;background:#1d9e7510;text-shadow:0 0 10px #1d9e7580}
.diff-btn.active-normal{color:#378add;border-color:#378add60;background:#378add10;text-shadow:0 0 10px #378add80}
.diff-btn.active-hard{color:#fac775;border-color:#fac77560;background:#fac77510;text-shadow:0 0 10px #fac77580}
.diff-btn.active-impossible{color:#e24b4a;border-color:#e24b4a60;background:#e24b4a10;text-shadow:0 0 10px #e24b4a80}

/* Info card */
#diff-info{
  width:340px;border-radius:14px;padding:18px 22px;margin-bottom:24px;
  border:1px solid transparent;transition:.3s;text-align:center;min-height:110px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;
}
#diff-info .di-icon{font-size:30px}
#diff-info .di-name{font-size:15px;font-weight:700;letter-spacing:2px}
#diff-info .di-desc{font-size:10px;color:#ffffff60;line-height:1.7}
#diff-info .di-tags{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin-top:4px}
#diff-info .di-tag{font-size:9px;padding:2px 8px;border-radius:20px;font-weight:700;letter-spacing:.5px}
#diff-info .di-record{font-size:9px;color:#ffffff35;margin-top:2px}

/* Play button */
#play-btn{
  width:200px;padding:14px 0;border-radius:12px;border:none;
  font-family:'Courier New',monospace;font-size:14px;font-weight:900;letter-spacing:4px;
  cursor:pointer;transition:.18s;text-transform:uppercase;
  background:linear-gradient(135deg,#0ff2,#0055ff18);
  color:#0ff;border:1px solid #0ff40;
  box-shadow:0 0 30px #0ff1,inset 0 0 20px #0ff05;
  margin-bottom:20px;
}
#play-btn:hover{transform:translateY(-2px);box-shadow:0 0 40px #0ff3,inset 0 0 30px #0ff10;border-color:#0ff8}
#play-btn:active{transform:scale(.97)}

.menu-footer{font-size:9px;color:#ffffff20;letter-spacing:1px;text-align:center}
.menu-footer b{color:#ffffff40}



/* difficulty info themes */
#diff-info.theme-easy{background:rgba(29,158,117,.08);border-color:#1d9e7530}
#diff-info.theme-easy .di-name{color:#1d9e75}
#diff-info.theme-easy .di-tag{background:#1d9e7520;color:#5dcaa5;border:1px solid #1d9e7530}
#diff-info.theme-normal{background:rgba(55,138,221,.08);border-color:#378add30}
#diff-info.theme-normal .di-name{color:#378add}
#diff-info.theme-normal .di-tag{background:#378add20;color:#85b7eb;border:1px solid #378add30}
#diff-info.theme-hard{background:rgba(250,199,117,.08);border-color:#fac77530}
#diff-info.theme-hard .di-name{color:#fac775}
#diff-info.theme-hard .di-tag{background:#fac77520;color:#ef9f27;border:1px solid #fac77530}
#diff-info.theme-impossible{background:rgba(226,75,74,.08);border-color:#e24b4a30}
#diff-info.theme-impossible .di-name{color:#e24b4a}
#diff-info.theme-impossible .di-tag{background:#e24b4a20;color:#f09595;border:1px solid #e24b4a30}

/* in-game difficulty badge */
#diff-badge{position:absolute;top:10px;left:50%;transform:translateX(-50%);font-size:10px;font-weight:700;padding:3px 12px;border-radius:20px;pointer-events:none;letter-spacing:1px;opacity:.8}

/* win/lose screen over the canvas */
#end-screen{position:absolute;inset:0;z-index:90;display:none;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,8,.85);border-radius:16px}
.end-title{font-size:38px;font-weight:700;text-align:center;line-height:1.2;margin-bottom:8px}
.end-stats{font-size:13px;color:#ffffff70;text-align:center;line-height:2;margin-bottom:28px}
.end-btns{display:flex;gap:12px}
.end-btn{padding:11px 28px;border-radius:10px;font-size:12px;font-weight:700;letter-spacing:1px;cursor:pointer;border:1px solid transparent;font-family:inherit;transition:.15s}
.end-btn:hover{transform:translateY(-2px)}
.end-btn.primary{background:#0ff1;border-color:#0ff4;color:#0ff}
.end-btn.secondary{background:#fff1;border-color:#fff2;color:#fff9}
</style>
</head>
<body>
<div id="wrap">
<canvas id="gc"></canvas>


<div id="menu-screen">
  <canvas id="menu-bg"></canvas>
  <div id="menu-content">
    <div class="menu-logo">
      <div class="menu-logo-title">GALAXY <span>ROGUELIKE</span></div>
      <div class="menu-logo-sub" id="menu-sub">V5 — Chọn độ khó</div>
    </div>

    <!-- Difficulty selector tabs -->
    <div class="diff-row" id="diff-row">
      <button class="diff-btn" id="mc-easy"      onclick="selectDiff('easy')">🌿 Dễ</button>
      <button class="diff-btn" id="mc-normal"    onclick="selectDiff('normal')">⚔️ Trung Bình</button>
      <button class="diff-btn" id="mc-hard"       onclick="selectDiff('hard')">💀 Khó</button>
      <button class="diff-btn" id="mc-impossible" onclick="selectDiff('impossible')">☠️ Bất Khả Thi</button>
    </div>

    <!-- Info card for selected difficulty -->
    <div id="diff-info" class="theme-normal">
      <div class="di-icon">⚔️</div>
      <div class="di-name">TRUNG BÌNH</div>
      <div class="di-desc">Trải nghiệm chuẩn. 15 waves, 3 boss.</div>
      <div class="di-tags">
        <span class="di-tag">Chuẩn</span>
        <span class="di-tag">15 Waves</span>
        <span class="di-tag">3 Boss</span>
      </div>
      <div class="di-record" id="di-record">Chưa có kỷ lục</div>
    </div>

    <!-- Play button -->
    <button id="play-btn" onclick="startGame(selectedDiff)">▶ CHƠI NGAY</button>

    <div class="menu-footer"><b>WASD</b> di chuyển &nbsp;•&nbsp; <b>Chuột</b> bắn &nbsp;•&nbsp; <b>E</b> Shop &nbsp;•&nbsp; <b>X</b> Upgrade</div>
  </div>
</div>

<!-- END SCREEN -->
<div id="end-screen">
  <div class="end-title" id="end-title">GAME OVER</div>
  <div class="end-stats" id="end-stats"></div>
  <div class="end-btns">
    <button class="end-btn primary" onclick="restartSame()">Chơi lại</button>
    <button class="end-btn secondary" onclick="goMenu()">Menu chính</button>
  </div>
</div>
<div id="ui">
  <div class="hud-top">
    <div class="stat-col">
      <div class="bar-row"><span style="color:#e24b4a;width:20px">HP</span><div class="bar-bg" style="width:130px"><div id="hp-fill" class="bar-fill" style="width:100%;height:8px"></div></div><span id="hp-val" style="color:#e24b4a;min-width:36px">100</span></div>
      <div class="bar-row"><span style="color:#378add;width:20px">SH</span><div class="bar-bg" style="width:130px"><div id="sh-fill" class="bar-fill" style="width:100%;height:8px"></div></div><span id="sh-val" style="color:#378add;min-width:36px">100</span></div>
      <div class="bar-row"><span style="color:#1d9e75;width:20px">XP</span><div class="bar-bg" style="width:130px"><div id="xp-fill" class="bar-fill" style="width:0%;height:8px"></div></div><span id="lv-val" style="color:#1d9e75;min-width:36px">Lv1</span></div>
    </div>
    <div class="wave-center">
      <div class="wave-label">wave</div>
      <div class="wave-num" id="wn">1</div>
    </div>
    <div class="stats-right">
      SCORE <b id="sv">0</b><br>
      KILLS <b id="kv" style="color:#e24b4a">0</b><br>
      GOLD <b id="gv">0</b>
    </div>
  </div>
  <div id="combo-el"></div>
  <div id="diff-badge"></div>
  <div id="bossbar">
    <div id="boss-name">BOSS</div>
    <div id="boss-bg"><div id="boss-fill" style="width:100%"></div></div>
    <div id="boss-phase"></div>
  </div>
  <div id="skillbar">
    <div class="sk" id="sk0"><span>💨</span><span class="sk-key">Shift</span><div class="sk-overlay" id="scd0"></div></div>
    <div class="sk" id="sk1"><span>💥</span><span class="sk-key">Q</span><div class="sk-overlay" id="scd1"></div></div>
    <div class="sk" id="sk2"><span>🛡️</span><span class="sk-key">W</span><div class="sk-overlay" id="scd2"></div></div>
    <div class="sk" id="sk3"><span>⚡</span><span class="sk-key">F</span><div class="sk-overlay" id="scd3"></div></div>
    <div class="sk" id="sk4"><span>🌀</span><span class="sk-key">R</span><div class="sk-overlay" id="scd4"></div></div>
    <div class="sk" id="sk5"><span>☄️</span><span class="sk-key">T</span><div class="sk-overlay" id="scd5"></div></div>
    <div class="sk"><span>🏪</span><span class="sk-key">E</span></div>
  </div>
  <canvas id="minimap" width="110" height="80"></canvas>
  <div id="minimap-label">RADAR</div>
  <div id="hint">WASD: Di chuyển | Chuột: Bắn<br>Shift:Dash Q:Nuke W:Shield F:EMP<br>R:Vortex T:Meteor | E:Shop | X:Upgrade</div>
  <div id="msg"></div>
  <div id="ach"><div id="ach-title">achievement</div><div id="ach-text"></div></div>
</div>

<div class="panel" id="shop-panel">
  <h3>🏪 UPGRADE SHOP</h3>
  <div class="shop-gold-bar"><span class="shop-gold-icon">💰</span><span class="shop-gold-val" id="shop-gold">0</span><span class="shop-gold-label">gold available</span></div>
  <div class="shop-tabs">
    <div class="stab active" onclick="setShopTab('combat')">⚔️ Combat</div>
    <div class="stab" onclick="setShopTab('defense')">🛡️ Defense</div>
    <div class="stab" onclick="setShopTab('utility')">⚙️ Utility</div>
    <div class="stab" onclick="setShopTab('special')">✨ Special</div>
  </div>
  <div id="shop-items"></div>
  <div style="margin-top:10px;color:#333;font-size:10px;text-align:center">E để đóng • Items có giới hạn số lần mua</div>
</div>

<div class="panel" id="talent-panel">
  <h3>★ LEVEL UP — CHỌN 1 KỸ NĂNG THỤ ĐỘNG</h3>
  <div class="talent-tabs" id="talent-tabs"></div>
  <div class="talent-grid" id="tg"></div>
  <div id="talent-synergy-hint" style="margin-top:10px;font-size:10px;color:#7f77dd60;text-align:center;font-style:italic"></div>
</div>

<div class="panel" id="skill-panel">
  <h3>🔧 NÂNG CẤP KỸ NĂNG</h3>
  <div class="skill-upgrade-grid" id="su-grid"></div>
  <div style="margin-top:12px;color:#333;font-size:10px;text-align:center">X để đóng</div>
</div>
</div>

<script>
const C=document.getElementById('gc'),ctx=C.getContext('2d');
const MM=document.getElementById('minimap'),mctx=MM.getContext('2d');
const WRAP=document.getElementById('wrap');
function rsz(){C.width=WRAP.clientWidth;C.height=WRAP.clientHeight}rsz();
window.addEventListener('resize',rsz);
const W=()=>C.width,H=()=>C.height;

/* ── difficulty config ── */
const DIFF_CONFIGS={
  easy:{
    label:'DỄ',color:'#1d9e75',bgColor:'rgba(29,158,117,.25)',
    playerHpMult:1.5,playerShieldMult:1.5,
    enemyHpMult:.6,enemySpeedMult:.7,enemyGoldMult:1.5,
    bossHpMult:.6,bossSpeedMult:.7,
    shopCostMult:.8,regenForced:true,
    startGold:40,waveTarget:6,totalWaves:12,
    poisonBullets:false,bossFromWave1:false,
    desc:'Dễ',tagColor:'#5dcaa5'
  },
  normal:{
    label:'TRUNG BÌNH',color:'#378add',bgColor:'rgba(55,138,221,.25)',
    playerHpMult:1,playerShieldMult:1,
    enemyHpMult:1,enemySpeedMult:1,enemyGoldMult:1,
    bossHpMult:1,bossSpeedMult:1,
    shopCostMult:1,regenForced:false,
    startGold:0,waveTarget:8,totalWaves:15,
    poisonBullets:false,bossFromWave1:false,
    desc:'Trung Bình',tagColor:'#85b7eb'
  },
  hard:{
    label:'KHÓ',color:'#fac775',bgColor:'rgba(250,199,117,.25)',
    playerHpMult:.8,playerShieldMult:.8,
    enemyHpMult:1.5,enemySpeedMult:1.35,enemyGoldMult:1.3,
    bossHpMult:2,bossSpeedMult:1.4,
    shopCostMult:1.3,regenForced:false,
    startGold:0,waveTarget:12,totalWaves:18,
    poisonBullets:false,bossFromWave1:false,noRegen:true,
    desc:'Khó',tagColor:'#ef9f27'
  },
  impossible:{
    label:'BẤT KHẢ THI',color:'#e24b4a',bgColor:'rgba(226,75,74,.25)',
    playerHpMult:.6,playerShieldMult:.6,
    enemyHpMult:2.5,enemySpeedMult:1.8,enemyGoldMult:.8,
    bossHpMult:3,bossSpeedMult:2,
    shopCostMult:2,regenForced:false,
    startGold:0,waveTarget:15,totalWaves:20,
    poisonBullets:true,bossFromWave1:true,noRegen:true,
    desc:'Bất Khả Thi',tagColor:'#f09595'
  }
};

/* ── save / unlock system ── */
function getSave(){try{return JSON.parse(localStorage.getItem('glx_save')||'{}')}catch{return{}}}
function setSave(d){try{localStorage.setItem('glx_save',JSON.stringify(d))}catch{}}

function isUnlocked(diff){
  return true;
}

function recordClear(diff,score,won){
  const save=getSave();
  if(won&&diff==='normal')save.normalCleared=true;
  if(won&&diff==='hard')save.hardCleared=true;
  if(!save.records)save.records={};
  if(!save.records[diff]||score>save.records[diff])save.records[diff]=score;
  setSave(save);
}

let selectedDiff='normal';

const DIFF_META={
  easy:{icon:'🌿',name:'DỄ',desc:'Dành cho người mới. Kẻ địch yếu hơn, hồi HP nhiều hơn.',tags:['HP ×1.5','Enemy ×0.6','Gold ×1.5']},
  normal:{icon:'⚔️',name:'TRUNG BÌNH',desc:'Trải nghiệm chuẩn. Cân bằng giữa thách thức và phần thưởng.',tags:['Chuẩn','15 Waves','3 Boss']},
  hard:{icon:'💀',name:'KHÓ',desc:'Không có HP regen. Boss enrage sớm. Cần kỹ năng thực sự.',tags:['Enemy ×1.5','No Regen','Boss ×2']},
  impossible:{icon:'☠️',name:'BẤT KHẢ THI',desc:'Địch bầy đàn. Đạn độc. Boss từ wave 1. Shop ×2.',tags:['Enemy ×2.5','Poison','Boss W1','Shop ×2']}
};

function selectDiff(diff){
  if(!isUnlocked(diff))return;
  selectedDiff=diff;
  const cfg=DIFF_CONFIGS[diff];
  const meta=DIFF_META[diff];
  const save=getSave();

  // update button active states
  ['easy','normal','hard','impossible'].forEach(d=>{
    const btn=document.getElementById('mc-'+d);
    btn.classList.remove('active-easy','active-normal','active-hard','active-impossible');
  });
  document.getElementById('mc-'+diff).classList.add('active-'+diff);

  // update info card
  const info=document.getElementById('diff-info');
  info.className='theme-'+diff;
  info.querySelector('.di-icon').textContent=meta.icon;
  info.querySelector('.di-name').textContent=meta.name;
  info.querySelector('.di-desc').textContent=meta.desc;
  const tagsEl=info.querySelector('.di-tags');
  tagsEl.innerHTML=meta.tags.map(t=>`<span class="di-tag">${t}</span>`).join('');

  // record
  const rec=document.getElementById('di-record');
  if(save.records&&save.records[diff])rec.textContent='🏆 Kỷ lục: '+save.records[diff].toLocaleString()+' pts';
  else rec.textContent='Chưa có kỷ lục';

  // play button color
  const pb=document.getElementById('play-btn');
  pb.style.color=cfg.color;
  pb.style.borderColor=cfg.color+'60';
  pb.style.boxShadow=`0 0 30px ${cfg.color}22,inset 0 0 20px ${cfg.color}08`;
}

function updateMenuUI(){
  const save=getSave();
  ['easy','normal','hard','impossible'].forEach(d=>{
    const btn=document.getElementById('mc-'+d);
    const isLocked=!isUnlocked(d);
    btn.classList.toggle('locked',isLocked);
  });
  selectDiff(selectedDiff);
}

let currentDiff='normal';
let menuAnimFrame=null;
let menuStars=[];
let menuParticles=[];
let menuFrame=0;

function initMenuStars(){
  menuStars=[];
  for(let i=0;i<180;i++)menuStars.push({
    x:Math.random()*1920,y:Math.random()*1080,
    s:Math.random()*1.8+.2,b:Math.random(),
    sp:.05+Math.random()*.3,
    col:Math.random()<.12?[100,180,255]:Math.random()<.05?[255,150,200]:[255,255,255]
  });
}
initMenuStars();

function drawMenuBG(){
  const mc=document.getElementById('menu-bg');
  const MW=WRAP.clientWidth||960,MH=WRAP.clientHeight||620;
  mc.width=MW;mc.height=MH;
  const mctx2=mc.getContext('2d');
  mctx2.clearRect(0,0,MW,MH);
  menuFrame++;

  // deep space gradient
  const grd=mctx2.createRadialGradient(MW/2,MH/2,50,MW/2,MH/2,Math.max(MW,MH)*.52);
  grd.addColorStop(0,'#050518');grd.addColorStop(1,'#000003');
  mctx2.fillStyle=grd;mctx2.fillRect(0,0,MW,MH);

  // nebula blobs
  const nebHues=[200,280,340];
  nebHues.forEach((hue,i)=>{
    const ox=Math.sin(menuFrame*.0012+i*1.2)*30,oy=Math.cos(menuFrame*.001+i)*20;
    const cx=MW*.21+i*(MW*.29)+ox,cy=MH*.24+i*(MH*.19)+oy;
    const gr=mctx2.createRadialGradient(cx,cy,20,cx,cy,200);
    gr.addColorStop(0,`hsla(${hue},80%,50%,.07)`);gr.addColorStop(1,'transparent');
    mctx2.fillStyle=gr;mctx2.beginPath();mctx2.arc(cx,cy,200,0,Math.PI*2);mctx2.fill();
  });

  // stars with parallax
  menuStars.forEach(s=>{
    const px=(s.x+menuFrame*s.sp*.4)%MW;
    const py=(s.y+menuFrame*s.sp*.15)%MH;
    const flicker=.55+Math.sin(menuFrame*.05+s.b*8)*.45;
    const [r,g,b]=s.col;
    mctx2.fillStyle=`rgba(${r},${g},${b},${s.b*flicker*.85})`;
    mctx2.beginPath();mctx2.arc(px,py,s.s,0,Math.PI*2);mctx2.fill();
    if(s.s>1.3){
      mctx2.strokeStyle=`rgba(${r},${g},${b},${s.b*.2*flicker})`;mctx2.lineWidth=.5;
      mctx2.beginPath();mctx2.moveTo(px-s.s*3,py);mctx2.lineTo(px+s.s*3,py);
      mctx2.moveTo(px,py-s.s*3);mctx2.lineTo(px,py+s.s*3);mctx2.stroke();
    }
  });

  // floating particles
  if(Math.random()<.08)menuParticles.push({x:Math.random()*MW,y:MH+10,vx:(Math.random()-.5)*.5,vy:-(Math.random()*1+.5),life:200+Math.random()*200,s:Math.random()*2+1,hue:Math.random()*360});
  menuParticles=menuParticles.filter(p=>{
    p.x+=p.vx;p.y+=p.vy;p.life--;
    mctx2.globalAlpha=p.life/250*.6;
    mctx2.fillStyle=`hsl(${p.hue},70%,60%)`;
    mctx2.beginPath();mctx2.arc(p.x,p.y,p.s,0,Math.PI*2);mctx2.fill();
    return p.life>0&&p.y>-20;
  });
  mctx2.globalAlpha=1;

  if(document.getElementById('menu-screen').style.display!=='none'){
    menuAnimFrame=requestAnimationFrame(drawMenuBG);
  }
}

function showMenu(){
  document.getElementById('menu-screen').style.display='flex';
  document.getElementById('end-screen').style.display='none';
  document.getElementById('ui').style.display='none';
  updateMenuUI();
  if(menuAnimFrame)cancelAnimationFrame(menuAnimFrame);
  drawMenuBG();
}

function startGame(diff){
  if(!isUnlocked(diff))return;
  currentDiff=diff;
  const cfg=DIFF_CONFIGS[diff];

  // hide menu, show UI
  document.getElementById('menu-screen').style.display='none';
  document.getElementById('end-screen').style.display='none';
  document.getElementById('ui').style.display='block';
  if(menuAnimFrame)cancelAnimationFrame(menuAnimFrame);

  // set difficulty badge
  const badge=document.getElementById('diff-badge');
  badge.textContent=cfg.label;
  badge.style.background=cfg.bgColor;
  badge.style.color=cfg.color;
  badge.style.border='1px solid '+cfg.color+'50';

  initGame(cfg);
  if(!loopRunning){loopRunning=true;loop();}
}

function goMenu(){
  document.getElementById('end-screen').style.display='none';
  showMenu();
}

function restartSame(){
  document.getElementById('end-screen').style.display='none';
  startGame(currentDiff);
}

function showEndScreen(won){
  const cfg=DIFF_CONFIGS[currentDiff];
  recordClear(currentDiff,G.score,won);
  const es=document.getElementById('end-screen');
  const title=document.getElementById('end-title');
  const stats=document.getElementById('end-stats');
  es.style.display='flex';
  if(won){
    title.innerHTML=`<span style="color:#fac775">🏆 CHIẾN THẮNG!</span>`;
  } else {
    title.innerHTML=`<span style="color:#e24b4a">☠️ GAME OVER</span>`;
  }
  stats.innerHTML=`
    Chế độ: <b style="color:${cfg.color}">${cfg.label}</b><br>
    Điểm số: <b style="color:#fac775">${G.score.toLocaleString()}</b><br>
    Kẻ địch tiêu diệt: <b style="color:#e24b4a">${G.kills}</b>&nbsp;&nbsp;
    Boss: <b style="color:#ff5555">${G.bossKills}</b><br>
    Wave đạt được: <b style="color:#0ff">${G.wave}</b>&nbsp;&nbsp;
    Level: <b style="color:#1d9e75">${G.level}</b>
  `;
  updateMenuUI();
}

let loopRunning=false;

/* ── input ── */
const keys={},M={x:480,y:310,dn:false};
C.addEventListener('mousemove',e=>{const r=C.getBoundingClientRect();M.x=e.clientX-r.left;M.y=e.clientY-r.top});
C.addEventListener('mousedown',e=>{if(e.button===0)M.dn=true});
C.addEventListener('mouseup',e=>{if(e.button===0)M.dn=false});
document.addEventListener('keydown',e=>{if(!keys[e.code]){keys[e.code]=true;onKey(e.code)}});
document.addEventListener('keyup',e=>{keys[e.code]=false});

/* ── skill definitions ── */
const SKILL_DEFS={
  dash:{name:'Dash',icon:'💨',maxLv:5,base:{cd:100,power:1},perLv:{cd:-10,power:.25},desc:lv=>`Teleport • CD: ${((100-lv*10)/60).toFixed(1)}s • Lực: ${(1+lv*.25).toFixed(1)}x`},
  nuke:{name:'Nova Bomb',icon:'💥',maxLv:5,base:{cd:240,radius:1,dmg:200},perLv:{cd:-20,radius:.3,dmg:60},desc:lv=>`AoE nổ • CD: ${((240-lv*20)/60).toFixed(1)}s • R: ${(1+lv*.3).toFixed(1)}x`},
  shield:{name:'Barrier',icon:'🛡️',maxLv:5,base:{cd:160,restore:75},perLv:{cd:-15,restore:25},desc:lv=>`Lá chắn • CD: ${((160-lv*15)/60).toFixed(1)}s • +${75+lv*25}HP`},
  emp:{name:'EMP Pulse',icon:'⚡',maxLv:5,base:{cd:200,dur:185},perLv:{cd:-25,dur:40},desc:lv=>`Đóng băng • CD: ${((200-lv*25)/60).toFixed(1)}s • Dur: ${((185+lv*40)/60).toFixed(1)}s`},
  vortex:{name:'Vortex',icon:'🌀',maxLv:4,base:{cd:300,dur:120,suck:3},perLv:{cd:-30,dur:20,suck:1},desc:lv=>`Hút địch • CD: ${((300-lv*30)/60).toFixed(1)}s • Lực: ${3+lv}x`},
  meteor:{name:'Meteor',icon:'☄️',maxLv:4,base:{cd:280,dmg:80,count:3},perLv:{cd:-25,dmg:30,count:1},desc:lv=>`Mưa sao • CD: ${((280-lv*25)/60).toFixed(1)}s • ${80+lv*30}dmg x${3+lv}`},
};

/* ── passive talents — full overhaul ── */
// category: atk=attack, def=defense, uti=utility, off=offensive special
const TALENT_CATS={
  atk:{label:'⚔️ Attack',color:'#fac775'},
  def:{label:'🛡️ Defense',color:'#378add'},
  uti:{label:'⚙️ Utility',color:'#1d9e75'},
  off:{label:'💥 Special',color:'#d4537e'},
};

const TALENTS=[
  // ── ATTACK ──
  {id:'rapidfire',name:'Rapid Fire',cat:'atk',desc:'Tốc độ bắn +20%',detail:'Giảm cooldown mỗi viên đạn',icon:'🔥',synergy:'multishot',apply:p=>{p.fireRate=Math.max(4,Math.floor(p.fireRate*.8))},stackable:true},
  {id:'multishot',name:'Multi Shot',cat:'atk',desc:'+2 đạn mỗi phát',detail:'Mỗi lần bắn ra thêm 2 đạn',icon:'💫',synergy:'rapidfire',apply:p=>{p.multiShot+=2},stackable:true},
  {id:'pierce',name:'Pierce Shot',cat:'atk',desc:'Đạn xuyên kẻ địch',detail:'Đạn không dừng lại khi trúng',icon:'🏹',synergy:null,apply:p=>{p.pierce=true},stackable:false},
  {id:'crit',name:'Critical Hit',cat:'atk',desc:'Crit +15% (×3.5 dmg)',detail:'Đạn vàng = crit',icon:'💢',synergy:'crit',apply:p=>{p.critChance+=.15;p.critMult=(p.critMult||3)+.5},stackable:true},
  {id:'overload',name:'Overload',cat:'atk',desc:'Tốc đạn +40%',detail:'Đạn bay nhanh hơn nhiều',icon:'⚡',synergy:'rapidfire',apply:p=>{p.bulletSpeed*=1.4},stackable:false},
  {id:'ricochet',name:'Ricochet',cat:'atk',desc:'Đạn nảy tường 2 lần',detail:'Đạn phản xạ khi ra mép',icon:'🎯',synergy:'pierce',apply:p=>{p.ricochet=true},stackable:false},
  {id:'burst',name:'Burst Fire',cat:'atk',desc:'Mỗi 5 phát: burst 3 đạn',detail:'Trigger tự động',icon:'🔫',synergy:'rapidfire',apply:p=>{p.burst=true;p.burstCounter=0},stackable:false},
  {id:'plasma',name:'Plasma Round',cat:'atk',desc:'Đạn lớn +50% kích cỡ & dmg',detail:'Đạn plasma xanh đậm',icon:'🔵',synergy:'pierce',apply:p=>{p.plasma=true},stackable:false},
  {id:'sniper',name:'Sniper Mode',cat:'atk',desc:'Dmg +80% nhưng bắn chậm -30%',detail:'Ít đạn nhưng cực mạnh',icon:'🎯',synergy:'crit',apply:p=>{p.dmgMult=(p.dmgMult||1)*1.8;p.fireRate=Math.floor(p.fireRate*1.3)},stackable:false},
  {id:'shotgun',name:'Shotgun Spread',cat:'atk',desc:'+4 đạn tản, tầm gần mạnh',detail:'Bắn thêm 4 đạn lan toả',icon:'💥',synergy:'multishot',apply:p=>{p.multiShot+=4;p.shotgunSpread=true},stackable:false},
  {id:'incendiary',name:'Incendiary',cat:'atk',desc:'Đạn đốt địch 3s',detail:'Kẻ địch bốc lửa sau khi trúng',icon:'🔥',synergy:'blast',apply:p=>{p.incendiary=true},stackable:false},
  {id:'explosive',name:'Explosive Tip',cat:'atk',desc:'Đạn nổ AoE nhỏ khi trúng',detail:'Vụ nổ bán kính 35px',icon:'💣',synergy:'ricochet',apply:p=>{p.explosive=true},stackable:false},
  {id:'dualshot',name:'Dual Cannon',cat:'atk',desc:'Bắn 2 đạn song song',detail:'Hai luồng đạn cùng lúc',icon:'🔱',synergy:'rapidfire',apply:p=>{p.dualCannon=true},stackable:false},
  {id:'homing',name:'Homing Bullets',cat:'atk',desc:'Đạn tự dẫn hướng nhẹ',detail:'Đạn cong theo mục tiêu gần',icon:'🧲',synergy:'multishot',apply:p=>{p.homingBullets=true},stackable:false},
  // ── DEFENSE ──
  {id:'tank',name:'Iron Hull',cat:'def',desc:'Max HP +100',detail:'Tăng sinh lực tối đa',icon:'🛡️',synergy:'regen',apply:p=>{p.maxHp+=100;p.hp=Math.min(p.hp+100,p.maxHp)},stackable:true},
  {id:'battery',name:'Power Cell',cat:'def',desc:'Max Shield +100',detail:'Tăng tấm chắn tối đa',icon:'🔋',synergy:'shieldRegen',apply:p=>{p.maxShield+=100;p.shield=Math.min(p.shield+100,p.maxShield)},stackable:true},
  {id:'regen',name:'Regen Core',cat:'def',desc:'Hồi 4 HP/giây',detail:'Tự động hồi HP',icon:'💚',synergy:'tank',apply:p=>{p.regen=true;p.regenAmt=(p.regenAmt||0)+4},stackable:true},
  {id:'vamp',name:'Vampire',cat:'def',desc:'Hút máu +10/kill',detail:'Mỗi kill hồi HP',icon:'🧛',synergy:'blast',apply:p=>{p.lifesteal+=10},stackable:true},
  {id:'thorns',name:'Thorns',cat:'def',desc:'Phản 15dmg khi bị đánh',detail:'Kẻ địch nhận sát thương',icon:'🌵',synergy:null,apply:p=>{p.thorns=(p.thorns||0)+15},stackable:true},
  {id:'fortress',name:'Fortress',cat:'def',desc:'Giảm 20% sát thương nhận',detail:'Armor thụ động',icon:'🏰',synergy:'tank',apply:p=>{p.armor=(p.armor||0)+.2},stackable:true},
  {id:'shieldregen',name:'Shield Regen',cat:'def',desc:'Shield tự hồi 3/giây',detail:'Tấm chắn tự phục hồi',icon:'🔷',synergy:'battery',apply:p=>{p.shieldRegen=true;p.shieldRegenAmt=(p.shieldRegenAmt||0)+3},stackable:true},
  {id:'laststand',name:'Last Stand',cat:'def',desc:'HP<20%: sát thương +50%',detail:'Nguy hiểm càng mạnh',icon:'❤️‍🔥',synergy:'vamp',apply:p=>{p.lastStand=true},stackable:false},
  {id:'overclock_shield',name:'Overclock Shield',cat:'def',desc:'Nhận dmg: shield bật tức thì',detail:'Kích hoạt shield tự động',icon:'⚡',synergy:'battery',apply:p=>{p.autoShield=true},stackable:false},
  {id:'evasion',name:'Evasion',cat:'def',desc:'20% né hoàn toàn đòn tấn công',detail:'Dodge cơ học thụ động',icon:'💨',synergy:'haste',apply:p=>{p.evasion=(p.evasion||0)+.2},stackable:true},
  {id:'phoenix',name:'Phoenix Core',cat:'def',desc:'Hồi sống 1 lần với 50% HP',detail:'Hồi sinh khi HP về 0',icon:'🦅',synergy:'regen',apply:p=>{p.phoenix=true},stackable:false},
  // ── UTILITY ──
  {id:'haste',name:'Haste',cat:'uti',desc:'Tốc độ di chuyển +20%',detail:'Phản ứng nhanh hơn',icon:'👟',synergy:null,apply:p=>{p.speed*=1.2},stackable:true},
  {id:'drone',name:'Combat Drone',cat:'uti',desc:'+1 drone tự bắn',detail:'Drone bay quanh và tấn công',icon:'🤖',synergy:'drone',apply:p=>{addDrone()},stackable:true},
  {id:'magnet',name:'Magnet Field',cat:'uti',desc:'Hút vàng & item trong 120px',detail:'Thu thập tự động',icon:'🧲',synergy:null,apply:p=>{p.magnet=true;p.magnetRange=(p.magnetRange||120)+20},stackable:true},
  {id:'goldaura',name:'Gold Aura',cat:'uti',desc:'Vàng từ kill +70%',detail:'Kiếm nhiều tiền hơn',icon:'💰',synergy:'magnet',apply:p=>{p.goldBonus=(p.goldBonus||1)*1.7},stackable:false},
  {id:'cooldown',name:'Overclock',cat:'uti',desc:'CD tất cả skill -15%',detail:'Skill dùng được thường hơn',icon:'⏱️',synergy:null,apply:p=>{p.cdReduction=(p.cdReduction||1)*.85},stackable:true},
  {id:'missile',name:'Auto Missile',cat:'uti',desc:'Tên lửa homing mỗi 3s',detail:'Tự tìm và tấn công mục tiêu',icon:'🚀',synergy:'drone',apply:p=>{p.missiles=true},stackable:false},
  {id:'xpboost',name:'Scholar',cat:'uti',desc:'XP từ kill +60%',detail:'Lên level nhanh hơn nhiều',icon:'📚',synergy:null,apply:p=>{p.xpBonus=(p.xpBonus||1)*1.6},stackable:true},
  {id:'turret',name:'Auto Turret',cat:'uti',desc:'Tự bắn địch gần nhất mỗi 1s',detail:'Turret thụ động 360°',icon:'🗼',synergy:'drone',apply:p=>{p.turret=true},stackable:false},
  {id:'scanner',name:'Radar Scanner',cat:'uti',desc:'Thấy vị trí boss trước 3s',detail:'Cảnh báo sớm boss xuất hiện',icon:'📡',synergy:null,apply:p=>{p.scanner=true},stackable:false},
  {id:'banker',name:'Banker',cat:'uti',desc:'Mỗi wave còn sống: +30 vàng',detail:'Thu nhập thụ động mỗi wave',icon:'🏦',synergy:'goldaura',apply:p=>{p.banker=true},stackable:true},
  {id:'recycler',name:'Recycler',cat:'uti',desc:'Đạn miss có 25% hoàn lại CD',detail:'Hiệu quả đạn tăng',icon:'♻️',synergy:'cooldown',apply:p=>{p.recycler=true},stackable:false},
  // ── SPECIAL ──
  {id:'blast',name:'Blast Kill',cat:'off',desc:'Giết tạo vụ nổ AoE',detail:'Mỗi kill gây sát thương vùng',icon:'💣',synergy:'vamp',apply:p=>{p.blast=true},stackable:false},
  {id:'chainbolt',name:'Chain Bolt',cat:'off',desc:'Đạn nối kẻ địch gần',detail:'Sau khi trúng lan sang 2 địch',icon:'🌩️',synergy:'multishot',apply:p=>{p.chain=true},stackable:false},
  {id:'blackhole',name:'Black Hole',cat:'off',desc:'Giết có 5% tạo lỗ đen nhỏ',detail:'Hút địch trong 2 giây',icon:'🌀',synergy:'blast',apply:p=>{p.blackhole=true},stackable:false},
  {id:'timewarp',name:'Time Warp',cat:'off',desc:'Mỗi 8s: slowmo 2s',detail:'Mọi thứ chậm lại 60%',icon:'⏰',synergy:'cooldown',apply:p=>{p.timewarp=true;p.timewarpCd=0},stackable:false},
  {id:'shockwave',name:'Shockwave',cat:'off',desc:'Mỗi 6s phát sóng đẩy địch',detail:'Sóng xung AoE quanh tàu',icon:'🌊',synergy:'blast',apply:p=>{p.shockwave=true;p.shockwaveCd=0},stackable:false},
  {id:'mirror',name:'Mirror Image',cat:'off',desc:'20% tạo bản sao đạn đôi',detail:'Phản chiếu đạn theo hướng ngược',icon:'🪞',synergy:'multishot',apply:p=>{p.mirror=true},stackable:false},
  {id:'freeze',name:'Cryo Rounds',cat:'off',desc:'Đạn làm chậm địch -35%',detail:'Hiệu ứng băng giá lên địch',icon:'❄️',synergy:'chainbolt',apply:p=>{p.freezeRounds=true},stackable:false},
  {id:'gravity',name:'Gravity Well',cat:'off',desc:'Mỗi 10s: hút toàn bộ địch vào giữa',detail:'Lực hút hành tinh siêu mạnh',icon:'🌍',synergy:'blackhole',apply:p=>{p.gravityWell=true;p.gravityWellCd=0},stackable:false},
  {id:'disruptor',name:'Disruptor',cat:'off',desc:'Địch trúng đạn: giảm dmg -40%',detail:'Làm suy yếu kẻ địch',icon:'📵',synergy:'freeze',apply:p=>{p.disruptor=true},stackable:false},
  {id:'nova',name:'Supernova',cat:'off',desc:'Khi HP<10%: nổ tung AoE cực lớn',detail:'Kích hoạt 1 lần/game',icon:'🌟',synergy:'blast',apply:p=>{p.supernova=true;p.supernovaUsed=false},stackable:false},
];

let takenTalentIds=[];
let currentTalentTab='atk';
let shopTab='combat';

/* ── shop items — full overhaul ── */
const SHOP_DATA={
  combat:[
    {name:'Bullet Upgrade',icon:'🔫',desc:'Sát thương đạn +30%',effect:'+30% base dmg',cost:60,stock:3,bought:0,apply:p=>{p.dmgMult=(p.dmgMult||1)*1.3}},
    {name:'Overcharge Ammo',icon:'⚡',desc:'Tốc đạn +35%',effect:'+35% bullet speed',cost:55,stock:3,bought:0,apply:p=>{p.bulletSpeed*=1.35}},
    {name:'Fire Rate Chip',icon:'🔥',desc:'Tốc bắn +15%',effect:'-15% fire cooldown',cost:65,stock:4,bought:0,apply:p=>{p.fireRate=Math.max(4,Math.floor(p.fireRate*.85))}},
    {name:'Hollow Point',icon:'💎',desc:'Crit damage +1x',effect:'critMult +1',cost:80,stock:2,bought:0,apply:p=>{p.critMult=(p.critMult||3)+1}},
    {name:'Drone Armament',icon:'🤖',desc:'Drone damage +50%',effect:'+50% drone dmg',cost:70,stock:3,bought:0,apply:p=>{p.droneDmgMult=(p.droneDmgMult||1)*1.5}},
    {name:'Plasma Core',icon:'🔵',desc:'Tất cả đạn xuyên giáp',effect:'pierce mode on',cost:120,stock:1,bought:0,apply:p=>{p.pierce=true}},
    {name:'Scatter Shot',icon:'💥',desc:'+3 đạn tản ra khi bắn',effect:'+3 spread bullets',cost:75,stock:2,bought:0,apply:p=>{p.multiShot+=3}},
    {name:'Incendiary Rounds',icon:'🔥',desc:'Đạn đốt địch 3 giây',effect:'burn on hit 3s',cost:90,stock:2,bought:0,apply:p=>{p.incendiary=true}},
    {name:'Crit Enhancer',icon:'💢',desc:'Crit chance +20%',effect:'+20% crit rate',cost:85,stock:2,bought:0,apply:p=>{p.critChance+=.2}},
    {name:'Dual Barrels',icon:'🔱',desc:'Bắn 2 luồng song song',effect:'dual cannon mode',cost:110,stock:1,bought:0,apply:p=>{p.dualCannon=true}},
    {name:'Explosive Tip',icon:'💣',desc:'Đạn nổ AoE nhỏ khi trúng',effect:'AoE explosion r=35',cost:100,stock:2,bought:0,apply:p=>{p.explosive=true}},
    {name:'Ricochet Module',icon:'🎯',desc:'Đạn nảy tường 2 lần',effect:'wall bounce ×2',cost:80,stock:2,bought:0,apply:p=>{p.ricochet=true}},
    {name:'Homing Upgrade',icon:'🧲',desc:'Đạn tự hướng mục tiêu nhẹ',effect:'soft homing bullets',cost:95,stock:2,bought:0,apply:p=>{p.homingBullets=true}},
    {name:'Burst Chip',icon:'⚙️',desc:'Mỗi 5 phát burst 3 đạn',effect:'auto burst mode',cost:70,stock:2,bought:0,apply:p=>{p.burst=true;p.burstCounter=0}},
    {name:'Chain Lightning',icon:'🌩️',desc:'Đạn trúng truyền sang 2 địch',effect:'chain on hit ×2',cost:115,stock:1,bought:0,apply:p=>{p.chain=true}},
    {name:'Cryo Ammo',icon:'❄️',desc:'Đạn làm chậm địch -35%',effect:'slow on hit',cost:85,stock:2,bought:0,apply:p=>{p.freezeRounds=true}},
    {name:'Overpower',icon:'🌟',desc:'Sát thương +60% trong 5s khi giết boss',effect:'+60% dmg after boss kill',cost:130,stock:1,bought:0,apply:p=>{p.overpower=true}},
  ],
  defense:[
    {name:'Medkit',icon:'💊',desc:'Hồi ngay 120 HP',effect:'instant +120 HP',cost:50,stock:5,bought:0,apply:p=>{p.hp=Math.min(p.maxHp,p.hp+120)}},
    {name:'Shield Recharge',icon:'🔵',desc:'Nạp đầy toàn bộ shield',effect:'shield → max',cost:45,stock:5,bought:0,apply:p=>{p.shield=p.maxShield}},
    {name:'Armor Plate',icon:'🧱',desc:'Max HP +80',effect:'+80 max HP',cost:65,stock:4,bought:0,apply:p=>{p.maxHp+=80;p.hp=Math.min(p.hp+80,p.maxHp)}},
    {name:'Shield Capacitor',icon:'🔋',desc:'Max Shield +80',effect:'+80 max shield',cost:60,stock:4,bought:0,apply:p=>{p.maxShield+=80;p.shield=Math.min(p.shield+80,p.maxShield)}},
    {name:'Nano Repair',icon:'💚',desc:'Regen +6 HP/giây',effect:'+6 HP regen/s',cost:75,stock:3,bought:0,apply:p=>{p.regen=true;p.regenAmt=(p.regenAmt||0)+6}},
    {name:'Reactive Armor',icon:'🛡️',desc:'Giảm 15% sát thương nhận',effect:'-15% dmg taken',cost:90,stock:2,bought:0,apply:p=>{p.armor=(p.armor||0)+.15}},
    {name:'Thorns Matrix',icon:'🌵',desc:'Phản 20 dmg khi bị đánh',effect:'reflect 20 dmg',cost:70,stock:3,bought:0,apply:p=>{p.thorns=(p.thorns||0)+20}},
    {name:'Lifesteal Chip',icon:'🧛',desc:'Hút máu +15/kill',effect:'+15 HP per kill',cost:80,stock:3,bought:0,apply:p=>{p.lifesteal+=15}},
    {name:'Emergency Repair',icon:'🔧',desc:'Khi HP<25%: hồi 80 HP tức thì (1 lần)',effect:'auto-heal at low HP',cost:100,stock:2,bought:0,apply:p=>{p.emergencyRepair=true;p.emergencyRepairUsed=false}},
    {name:'Shield Regen Module',icon:'🔷',desc:'Shield tự hồi 4/giây',effect:'+4 shield regen/s',cost:85,stock:3,bought:0,apply:p=>{p.shieldRegen=true;p.shieldRegenAmt=(p.shieldRegenAmt||0)+4}},
    {name:'Evasion Drive',icon:'💨',desc:'15% né hoàn toàn đòn tấn công',effect:'+15% dodge chance',cost:90,stock:2,bought:0,apply:p=>{p.evasion=(p.evasion||0)+.15}},
    {name:'Heavy Plating',icon:'🏰',desc:'Max HP +150 nhưng tốc độ -5%',effect:'+150 HP, -5% speed',cost:100,stock:2,bought:0,apply:p=>{p.maxHp+=150;p.hp=Math.min(p.hp+150,p.maxHp);p.speed*=.95}},
    {name:'Phoenix Feather',icon:'🦅',desc:'Hồi sống 1 lần với 50% HP',effect:'1× revive at 50% HP',cost:150,stock:1,bought:0,apply:p=>{p.phoenix=true}},
    {name:'Stasis Field',icon:'🔮',desc:'Bất tử 1.5s sau khi nhận đòn chí mạng',effect:'invincible frames',cost:110,stock:2,bought:0,apply:p=>{p.stasis=true}},
    {name:'Full Restore',icon:'✨',desc:'Hồi toàn bộ HP và Shield',effect:'full HP+Shield restore',cost:120,stock:2,bought:0,apply:p=>{p.hp=p.maxHp;p.shield=p.maxShield}},
  ],
  utility:[
    {name:'Gold Booster',icon:'💰',desc:'Vàng từ kill +80%',effect:'+80% gold per kill',cost:85,stock:2,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.8}},
    {name:'XP Boost',icon:'📈',desc:'XP từ kill +50%',effect:'+50% XP gain',cost:70,stock:2,bought:0,apply:p=>{p.xpBonus=(p.xpBonus||1)*1.5}},
    {name:'Skill Cooldown',icon:'⏱️',desc:'CD tất cả skill -20%',effect:'-20% all cooldowns',cost:95,stock:2,bought:0,apply:p=>{p.cdReduction=(p.cdReduction||1)*.8}},
    {name:'Speed Module',icon:'👟',desc:'Tốc độ di chuyển +15%',effect:'+15% move speed',cost:50,stock:3,bought:0,apply:p=>{p.speed*=1.15}},
    {name:'Magnet Field',icon:'🧲',desc:'Thu item trong vòng 150px',effect:'auto collect range',cost:40,stock:2,bought:0,apply:p=>{p.magnet=true;p.magnetRange=(p.magnetRange||120)+40}},
    {name:'Combo Extender',icon:'🔗',desc:'Combo timer +2 giây',effect:'+120 combo duration',cost:55,stock:3,bought:0,apply:p=>{p.comboBonus=(p.comboBonus||0)+120}},
    {name:'Scout Drone',icon:'🔍',desc:'+1 drone trinh thám, tăng phạm vi minimap',effect:'radar range ×1.5',cost:60,stock:2,bought:0,apply:p=>{addDrone();p.radarBoost=true}},
    {name:'Dash Booster',icon:'🌌',desc:'Dash mạnh hơn ×1.8',effect:'dash power ×1.8',cost:75,stock:2,bought:0,apply:p=>{p.dashPower=(p.dashPower||1)*1.8}},
    {name:'Overclock Drive',icon:'🖥️',desc:'CD skill -25%, tốc bắn +10%',effect:'speed+cooldown combo',cost:110,stock:1,bought:0,apply:p=>{p.cdReduction=(p.cdReduction||1)*.75;p.fireRate=Math.max(4,Math.floor(p.fireRate*.9))}},
    {name:'Gold Magnet',icon:'💛',desc:'Hút vàng từ xa +100%, +50% gold/kill',effect:'gold combo upgrade',cost:95,stock:2,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.5;p.magnet=true;p.magnetRange=(p.magnetRange||120)+60}},
    {name:'Wave Bonus',icon:'🌊',desc:'Nhận 25 vàng mỗi lần đánh bại 1 wave',effect:'+25g per wave clear',cost:80,stock:2,bought:0,apply:p=>{p.waveBonus=(p.waveBonus||0)+25}},
    {name:'Auto Turret',icon:'🗼',desc:'Turret tự bắn địch gần mỗi 1s',effect:'auto turret 360°',cost:120,stock:1,bought:0,apply:p=>{p.turret=true}},
    {name:'Score Multiplier',icon:'🏆',desc:'Điểm từ kill ×2',effect:'score ×2',cost:90,stock:2,bought:0,apply:p=>{p.scoreMult=(p.scoreMult||1)*2}},
    {name:'Missile Launcher',icon:'🚀',desc:'Tên lửa homing tự động mỗi 3s',effect:'auto homing missile',cost:100,stock:2,bought:0,apply:p=>{p.missiles=true}},
    {name:'Resource Recycler',icon:'♻️',desc:'Địch chết rải vàng +1 mỗi lần',effect:'bonus gold on kill',cost:65,stock:3,bought:0,apply:p=>{p.goldBonus=(p.goldBonus||1)*1.3}},
  ],
  special:[
    {name:'Drone Upgrade',icon:'🤖',desc:'+1 Combat Drone mới',effect:'spawn extra drone',cost:100,stock:3,bought:0,apply:p=>{addDrone()}},
    {name:'Nova Core',icon:'💥',desc:'Nuke radius +70%',effect:'+70% nuke AoE',cost:90,stock:2,bought:0,apply:p=>{if(G.p.skills.nuke)G.p.skills.nuke.radiusBonus=(G.p.skills.nuke.radiusBonus||1)*1.7}},
    {name:'Warp Drive',icon:'🌌',desc:'Dash teleport distance ×2',effect:'dash power ×2',cost:80,stock:2,bought:0,apply:p=>{p.dashPower=(p.dashPower||1)*2}},
    {name:'Chain Reactor',icon:'🌩️',desc:'Đạn bắn xích sang địch kế',effect:'chain lightning on hit',cost:110,stock:1,bought:0,apply:p=>{p.chain=true}},
    {name:'Black Market',icon:'💀',desc:'Mở khoá 3 item ẩn đặc biệt',effect:'secret items unlocked',cost:150,stock:1,bought:0,apply:p=>{p.blackMarket=true;setTimeout(()=>renderShop(),100)}},
    {name:'Omni Shield',icon:'⭕',desc:'Shield không bao giờ về 0 (min 10)',effect:'shield floor = 10',cost:130,stock:1,bought:0,apply:p=>{p.shieldFloor=10}},
    {name:'Supernova Charge',icon:'🌟',desc:'Khi HP<10%: nổ AoE cực lớn 1 lần',effect:'emergency supernova',cost:140,stock:1,bought:0,apply:p=>{p.supernova=true;p.supernovaUsed=false}},
    {name:'Black Hole Generator',icon:'🌀',desc:'Giết có 8% tạo lỗ đen hút địch',effect:'8% black hole proc',cost:120,stock:1,bought:0,apply:p=>{p.blackhole=true}},
    {name:'Time Distortion',icon:'⏰',desc:'Mỗi 8s: toàn bộ địch chậm 2s',detail:'Slowmo field 60%',effect:'timewarp field',cost:125,stock:1,bought:0,apply:p=>{p.timewarp=true;p.timewarpCd=0}},
    {name:'Gravity Well',icon:'🌍',desc:'Mỗi 10s: hút toàn bộ địch vào tâm',effect:'gravity pull ×all',cost:135,stock:1,bought:0,apply:p=>{p.gravityWell=true;p.gravityWellCd=0}},
    {name:'Shockwave Emitter',icon:'🌊',desc:'Mỗi 6s: sóng đẩy địch ra ngoài',effect:'shockwave push AoE',cost:115,stock:1,bought:0,apply:p=>{p.shockwave=true;p.shockwaveCd=0}},
    {name:'Disruptor Field',icon:'📵',desc:'Địch trúng đạn: giảm dmg -40%',effect:'weaken on hit',cost:110,stock:2,bought:0,apply:p=>{p.disruptor=true}},
    // secret items (unlocked by Black Market)
    {name:'Death Ray',icon:'☠️',desc:'[ẨN] Dmg đạn ×3, xuyên mọi thứ',effect:'triple dmg + pierce',cost:200,stock:1,bought:0,secret:true,apply:p=>{p.dmgMult=(p.dmgMult||1)*3;p.pierce=true}},
    {name:'God Mode',icon:'🌈',desc:'[ẨN] Bất tử 8 giây',effect:'invincible 8s',cost:250,stock:1,bought:0,secret:true,apply:p=>{p.invincible=true;setTimeout(()=>{if(p)p.invincible=false},8000)}},
    {name:'Annihilator',icon:'💀',desc:'[ẨN] Giết tất cả địch trên màn hình',effect:'instant kill all enemies',cost:300,stock:1,bought:0,secret:true,apply:p=>{if(G&&G.enemies){G.enemies.forEach(e=>{e.hp=0});}}},
  ],
};

const ACH=[
  {id:'k10',label:'First Blood',check:g=>g.kills>=10,done:false},
  {id:'k50',label:'Warrior — 50 kills',check:g=>g.kills>=50,done:false},
  {id:'k200',label:'Slayer — 200 kills',check:g=>g.kills>=200,done:false},
  {id:'c15',label:'Unstoppable — x15 combo',check:g=>g.combo>=15,done:false},
  {id:'w5',label:'Veteran — Wave 5',check:g=>g.wave>=5,done:false},
  {id:'w10',label:'Legend — Wave 10',check:g=>g.wave>=10,done:false},
  {id:'s10k',label:'10000 điểm',check:g=>g.score>=10000,done:false},
  {id:'lv8',label:'Master — Level 8',check:g=>g.level>=8,done:false},
  {id:'boss3',label:'Boss Killer — 3 boss',check:g=>g.bossKills>=3,done:false},
];

/* ── drones ── */
let drones=[];
function addDrone(){drones.push({a:Math.random()*Math.PI*2,orb:60+drones.length*28,cd:0,x:0,y:0,lv:1})}

/* ── vortex & meteors ── */
let vortexActive={on:false,x:0,y:0,timer:0,suck:3};
let meteors=[];
let floatDmg=[];

/* ── game state ── */
let G;
function initGame(cfg){
  cfg=cfg||DIFF_CONFIGS[currentDiff]||DIFF_CONFIGS.normal;
  // close all open panels before starting
  ['shop-panel','talent-panel','skill-panel'].forEach(id=>{
    const el=document.getElementById(id);if(el)el.style.display='none';
  });
  drones=[];vortexActive={on:false};meteors=[];floatDmg=[];takenTalentIds=[];shopTab='combat';
  Object.values(SHOP_DATA).forEach(cat=>cat.forEach(it=>{it.bought=0;it._base=it._base||it.cost;it.cost=Math.round(it._base*(cfg.shopCostMult||1))}));
  const baseHp=Math.round(100*(cfg.playerHpMult||1));
  const baseSh=Math.round(100*(cfg.playerShieldMult||1));
  G={
    p:{x:W()/2,y:H()/2,hp:baseHp,maxHp:baseHp,shield:baseSh,maxShield:baseSh,
      speed:3.1,vx:0,vy:0,angle:0,
      fireRate:14,fireCd:0,bulletSpeed:7.5,multiShot:0,
      lifesteal:0,pierce:false,critChance:0,critMult:3,
      magnet:false,magnetRange:120,missiles:false,blast:false,
      regen:cfg.regenForced||false,regenAmt:3,ricochet:false,
      goldBonus:1,dmgMult:1,xpBonus:1,cdReduction:1,dashPower:1,
      armor:0,thorns:0,shieldFloor:0,chain:false,blackhole:false,timewarp:false,timewarpCd:0,
      burst:false,burstCounter:0,plasma:false,droneDmgMult:1,comboBonus:0,
      skills:{dash:{lv:0,cd:0},nuke:{lv:0,cd:0},shield:{lv:0,cd:0},emp:{lv:0,cd:0},vortex:{lv:0,cd:0},meteor:{lv:0,cd:0}}
    },
    bullets:[],enemies:[],particles:[],powerups:[],orbs:[],trails:[],
    score:0,kills:0,gold:cfg.startGold||0,totalGold:cfg.startGold||0,bossKills:0,
    wave:1,waveKills:0,waveTarget:cfg.waveTarget||8,
    waveBreak:false,waveBreakTimer:0,
    xp:0,xpNeeded:65,level:1,combo:0,comboTimer:0,
    shopOpen:false,talentOpen:false,skillUpOpen:false,
    gameOver:false,won:false,frame:0,
    spawnTimer:60,boss:null,bossSpawned:false,
    diffCfg:cfg,totalWaves:cfg.totalWaves||15,
    stars:[],nebulas:[],dustClouds:[],
  };
  for(let i=0;i<200;i++)G.stars.push({x:Math.random()*1800,y:Math.random()*1000,s:Math.random()*2+.2,b:Math.random(),sp:.04+Math.random()*.4,col:Math.random()<.15?[180,180,255]:[255,255,255]});
  for(let i=0;i<7;i++)G.nebulas.push({x:Math.random()*W(),y:Math.random()*H(),rx:140+Math.random()*200,ry:80+Math.random()*150,hue:Math.random()*360,a:.055+Math.random()*.04});
  for(let i=0;i<12;i++)G.dustClouds.push({x:Math.random()*W(),y:Math.random()*H(),r:30+Math.random()*60,a:.03+Math.random()*.04,sp:.01+Math.random()*.02,hue:Math.random()*360});
  ACH.forEach(a=>a.done=false);
  if(cfg.bossFromWave1)setTimeout(()=>{if(G&&!G.boss)spawnBoss()},3000);
}

/* ── helpers ── */
let msgT=0;
function showMsg(t,d){const e=document.getElementById('msg');e.innerHTML=t;e.style.opacity='1';clearTimeout(msgT);if(d<99999)msgT=setTimeout(()=>e.style.opacity='0',d||2000)}
function showAch(l){const e=document.getElementById('ach');document.getElementById('ach-text').textContent=l;e.style.opacity='1';setTimeout(()=>e.style.opacity='0',3000)}
function checkAch(){ACH.forEach(a=>{if(!a.done&&a.check(G)){a.done=true;showAch(a.label)}})}

function part(x,y,col,n,spd,sz,life){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2,s=(Math.random()+.3)*(spd||3);
    G.particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:life||(28+Math.random()*22),color:col,size:(sz||2)+Math.random()*2,maxLife:life||50});
  }
}

function floatDmgPop(x,y,val,col){
  floatDmg.push({x,y,val,col:col||'#fff',life:45,vy:-1.5});
}

function skillCd(id){
  const sk=G.p.skills[id],def=SKILL_DEFS[id];
  if(!def)return Infinity;
  const lv=sk.lv;
  return def.base.cd + (lv>0?def.perLv.cd*lv:0);
}

function skillVal(id,key){
  const sk=G.p.skills[id],def=SKILL_DEFS[id];
  const lv=sk.lv;
  return def.base[key]+(lv>0&&def.perLv[key]?def.perLv[key]*lv:0);
}

/* ── skills ── */
function doDash(){
  const sk=G.p.skills.dash;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('dash');
  const dx=M.x-G.p.x,dy=M.y-G.p.y,d=Math.hypot(dx,dy)||1;
  const power=skillVal('dash','power');
  G.p.vx+=(dx/d)*15*power;G.p.vy+=(dy/d)*15*power;
  part(G.p.x,G.p.y,'#378add',20,6,3,25);
  // afterimage trail
  for(let i=0;i<5;i++)G.trails.push({x:G.p.x,y:G.p.y,a:.6-i*.1,life:15,angle:G.p.angle});
}

function doNuke(){
  const sk=G.p.skills.nuke;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('nuke');
  const r=skillVal('nuke','radius')*200;
  const dmg=skillVal('nuke','dmg');
  const toKill=[];
  G.enemies.forEach(e=>{if(Math.hypot(e.x-G.p.x,e.y-G.p.y)<r){killEnemy(e);toKill.push(e)}});
  G.enemies=G.enemies.filter(e=>!toKill.includes(e));
  if(G.boss&&Math.hypot(G.boss.x-G.p.x,G.boss.y-G.p.y)<r){G.boss.hp-=dmg;if(G.boss.hp<=0)killBoss()}
  part(G.p.x,G.p.y,'#fac775',120,12,4,35);
  G.particles.push({x:G.p.x,y:G.p.y,ring:true,r:0,maxR:r,life:30,color:'#fac775'});
}

function doShield(){
  const sk=G.p.skills.shield;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('shield');
  const restore=skillVal('shield','restore');
  G.p.shield=Math.min(G.p.maxShield,G.p.shield+restore);
  part(G.p.x,G.p.y,'#0ff',30,4,3,30);
}

function doEMP(){
  const sk=G.p.skills.emp;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  sk.cd=skillCd('emp');
  const dur=skillVal('emp','dur');
  G.enemies.forEach(e=>{e.frozen=dur;part(e.x,e.y,'#afa9ec',6,2,2,20)});
  if(G.boss)G.boss.frozen=dur*.5;
  part(G.p.x,G.p.y,'#afa9ec',60,7,3,25);
  showMsg('⚡ EMP PULSE',700);
}

function doVortex(){
  const sk=G.p.skills.vortex;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  if(sk.lv===0){showMsg('Nâng cấp Vortex trước! (X)',1200);return}
  sk.cd=skillCd('vortex');
  vortexActive={on:true,x:M.x,y:M.y,timer:skillVal('vortex','dur'),suck:skillVal('vortex','suck')};
  part(M.x,M.y,'#7f77dd',40,5,4,40);
}

function doMeteor(){
  const sk=G.p.skills.meteor;
  if(sk.cd>0||G.shopOpen||G.talentOpen||G.skillUpOpen)return;
  if(sk.lv===0){showMsg('Nâng cấp Meteor trước! (X)',1200);return}
  sk.cd=skillCd('meteor');
  const count=skillVal('meteor','count');
  const dmg=skillVal('meteor','dmg');
  for(let i=0;i<count;i++){
    const target=G.enemies.length?G.enemies[Math.floor(Math.random()*G.enemies.length)]:null;
    const tx=target?target.x+Math.random()*60-30:Math.random()*W();
    const ty=target?target.y+Math.random()*60-30:Math.random()*H();
    setTimeout(()=>{
      meteors.push({x:tx-80,y:-60,tx,ty,progress:0,dmg,r:14});
    },i*200);
  }
}

/* ── skill upgrade panel ── */
function openSkillUp(){
  if(G.talentOpen||G.gameOver)return;
  G.skillUpOpen=!G.skillUpOpen;
  const panel=document.getElementById('skill-panel');
  panel.style.display=G.skillUpOpen?'block':'none';
  if(G.skillUpOpen)renderSkillUp();
}

function renderSkillUp(){
  const grid=document.getElementById('su-grid');grid.innerHTML='';
  const ids=['dash','nuke','shield','emp','vortex','meteor'];
  ids.forEach(id=>{
    const def=SKILL_DEFS[id],sk=G.p.skills[id];
    const lv=sk.lv,maxLv=def.maxLv;
    const upgCost=lv===0?30:40+lv*25;
    const d=document.createElement('div');d.className='su-card';
    const canBuy=G.gold>=upgCost&&lv<maxLv;
    d.innerHTML=`<div class="su-icon">${def.icon}</div>
      <div class="su-name">${def.name} <span style="color:#555">Lv${lv}/${maxLv}</span></div>
      <div class="su-desc">${def.desc(lv)}</div>
      <div class="su-lv">${lv<maxLv?`Nâng cấp: ${upgCost}g`:'MAX LEVEL'}</div>`;
    if(canBuy)d.onclick=()=>{G.gold-=upgCost;sk.lv=Math.min(maxLv,sk.lv+1);part(G.p.x,G.p.y,'#d4537e',16,4,3,25);renderSkillUp();updateHUD()};
    else if(lv>=maxLv)d.style.borderColor='#fac77540';
    else d.style.opacity='.4';
    grid.appendChild(d);
  });
}

/* ── fire ── */
function fireBullet(fx,fy,angle,spd,col,dmg,owner,pierce,ricochet,customSize){
  const count=owner==='player'?1+(G.p.multiShot||0):1;
  const isDual=owner==='player'&&G.p.dualCannon&&!customSize;
  const baseAngles=isDual?[angle-.08,angle+.08]:[angle];
  baseAngles.forEach(baseAngle=>{
  for(let i=0;i<count;i++){
    const sp=(i-(count-1)/2)*.17;
    const isCrit=owner==='player'&&Math.random()<(G.p.critChance||0);
    const lastStandMult=owner==='player'&&G.p.lastStand&&G.p.hp/G.p.maxHp<.2?1.5:1;
    const realDmg=(isCrit?dmg*(G.p.critMult||3):dmg)*(owner==='player'?(G.p.dmgMult||1):1)*lastStandMult;
    const sz=customSize||(isCrit?7:4.5);
    let bulletAngle=baseAngle+sp;
    if(owner==='player'&&G.p.homingBullets){
      // homing — find nearest enemy
      let nearest=null,nd=9999;
      G.enemies.forEach(e=>{const d=Math.hypot(e.x-fx,e.y-fy);if(d<nd){nd=d;nearest=e}});
      if(!nearest&&G.boss)nearest=G.boss;
      G.bullets.push({x:fx,y:fy,vx:Math.cos(bulletAngle)*spd,vy:Math.sin(bulletAngle)*spd,
        life:100,size:sz,color:isCrit?'#fac775':col,dmg:realDmg,
        owner,pierce:pierce||false,ricochet:ricochet||false,bounces:0,hit:[],
        chain:owner==='player'&&(G.p.chain||false),chainDone:false,
        freeze:owner==='player'&&(G.p.freezeRounds||false),
        explosive:owner==='player'&&(G.p.explosive||false),
        incendiary:owner==='player'&&(G.p.incendiary||false),
        disruptor:owner==='player'&&(G.p.disruptor||false),
        homing:nearest});
    } else {
      G.bullets.push({x:fx,y:fy,vx:Math.cos(bulletAngle)*spd,vy:Math.sin(bulletAngle)*spd,
        life:100,size:sz,color:isCrit?'#fac775':col,dmg:realDmg,
        owner,pierce:pierce||false,ricochet:ricochet||false,bounces:0,hit:[],
        chain:owner==='player'&&(G.p.chain||false),chainDone:false,
        freeze:owner==='player'&&(G.p.freezeRounds||false),
        explosive:owner==='player'&&(G.p.explosive||false),
        incendiary:owner==='player'&&(G.p.incendiary||false),
        disruptor:owner==='player'&&(G.p.disruptor||false)});
    }
  }
  });
}

function fireHoming(fx,fy){
  let t=null,nd=9999;
  G.enemies.forEach(e=>{const d=Math.hypot(e.x-fx,e.y-fy);if(d<nd){nd=d;t=e}});
  if(t&&nd<420)G.bullets.push({x:fx,y:fy,vx:0,vy:0,life:140,size:6,color:'#1d9e75',dmg:20*(G.p.dmgMult||1),owner:'player',homing:t,pierce:false,ricochet:false,hit:[]});
}

/* ── enemy spawn ── */
function spawnEnemy(){
  const wv=G.wave,side=Math.floor(Math.random()*4);
  let x,y;const pad=40;
  if(side===0){x=Math.random()*W();y=-pad}
  else if(side===1){x=W()+pad;y=Math.random()*H()}
  else if(side===2){x=Math.random()*W();y=H()+pad}
  else{x=-pad;y=Math.random()*H()}
  const roll=Math.random();
  let type;
  if(wv>=9&&roll<.07)type='guardian';
  else if(wv>=6&&roll<.14)type='elite';
  else if(wv>=4&&roll<.22)type='bomber';
  else if(wv>=3&&roll<.32)type='tank';
  else if(wv>=2&&roll<.46)type='fast';
  else type='normal';
  const cfgs={
    normal:{hp:20+wv*10,speed:1.1+wv*.12,size:13,color:'#e24b4a',xp:10,gold:5,shoot:false,shield:0},
    fast:{hp:12+wv*5,speed:2.2+wv*.18,size:10,color:'#fac775',xp:14,gold:7,shoot:true,shootRate:85,shield:0},
    tank:{hp:80+wv*28,speed:.6+wv*.06,size:22,color:'#7f77dd',xp:30,gold:18,shoot:false,shield:30},
    bomber:{hp:30+wv*9,speed:1.3+wv*.1,size:15,color:'#d85a30',xp:22,gold:13,shoot:true,shootRate:58,shotType:'spread',shield:0},
    elite:{hp:140+wv*35,speed:1.6+wv*.13,size:18,color:'#d4537e',xp:55,gold:38,shoot:true,shootRate:42,shotType:'spiral',shield:20},
    guardian:{hp:220+wv*50,speed:.9+wv*.08,size:24,color:'#639922',xp:80,gold:55,shoot:true,shootRate:55,shotType:'burst',shield:50},
  };
  const dc=G.diffCfg||DIFF_CONFIGS.normal;
  const base=cfgs[type];
  const hp=Math.round(base.hp*(dc.enemyHpMult||1));
  const speed=base.speed*(dc.enemySpeedMult||1);
  const gold=Math.round((base.gold||5)*(dc.enemyGoldMult||1));
  // impossible: bullets are poison (green, extra dmg over time tracked on player)
  const isPoisonMode=dc.poisonBullets;
  G.enemies.push({x,y,...base,hp,maxHp:hp,speed,gold,maxShield:base.shield||0,
    type,angle:0,shootCd:0,spiralAngle:0,frozen:0,burstCount:0,
    poisonShot:isPoisonMode&&base.shoot});
}

function spawnBoss(){
  const wv=G.wave;
  const dc=G.diffCfg||DIFF_CONFIGS.normal;
  const x=Math.random()<.5?-80:W()+80,y=H()/2;
  const phase=wv>=10?3:wv>=6?2:1;
  const hp=Math.round((600+wv*180)*(dc.bossHpMult||1));
  const speed=(.8+wv*.08)*(dc.bossSpeedMult||1);
  G.boss={x,y,hp,maxHp:hp,speed,size:40,angle:0,shootCd:0,spiralA:0,frozen:0,phase,currentPhase:1,enrage:false,shield:100,maxShield:100};
  document.getElementById('bossbar').style.display='block';
  showMsg(`⚠️  BOSS PHASE ${phase} — ${dc.label}`,2200);
}

function killEnemy(e){
  const gold=Math.round((e.gold||5)*(G.p.goldBonus||1));
  const xpGain=Math.round(e.xp*(G.p.xpBonus||1));
  const scoreMult=(G.p.scoreMult||1)*(1+Math.floor(G.combo/5));
  G.score+=Math.round(e.xp*scoreMult);
  G.kills++;G.waveKills++;G.gold+=gold;G.totalGold+=gold;
  G.xp+=xpGain;G.combo++;G.comboTimer=150+(G.p.comboBonus||0);
  if(G.p.lifesteal)G.p.hp=Math.min(G.p.maxHp,G.p.hp+G.p.lifesteal);
  if(G.p.blast){
    part(e.x,e.y,'#fac775',20,6,4,30);
    G.enemies.forEach(ne=>{if(ne!==e&&Math.hypot(ne.x-e.x,ne.y-e.y)<65){ne.hp-=35;if(ne.hp<=0)killEnemy(ne)}});
  }
  if(G.p.blackhole&&Math.random()<.05){
    vortexActive={on:true,x:e.x,y:e.y,timer:120,suck:4};
    part(e.x,e.y,'#7f77dd',20,4,3,35);
  }
  if(Math.random()<.14)G.powerups.push({x:e.x,y:e.y,type:Math.random()<.55?'hp':'shield',life:380});
  if(Math.random()<.1)G.orbs.push({x:e.x,y:e.y,val:Math.max(1,Math.round(gold*.5)),life:440});
  part(e.x,e.y,e.color,12,4,2.5,30);
  checkXP();checkAch();
}

function killBoss(){
  part(G.boss.x,G.boss.y,'#e24b4a',100,10,5,45);
  for(let i=0;i<5;i++)G.particles.push({x:G.boss.x,y:G.boss.y,ring:true,r:0,maxR:120+i*40,life:40,color:'#ff5555'});
  G.score+=800;G.gold+=220;G.totalGold+=220;G.kills+=6;G.bossKills++;
  G.boss=null;document.getElementById('bossbar').style.display='none';
  showMsg('💀 BOSS TIÊU DIỆT! +800pts',2500);
  checkAch();
}

function hitPlayer(dmg){
  const p=G.p;
  if(p.invincible||p.timewarpActive)return;
  if(p.evasion&&Math.random()<p.evasion){part(p.x,p.y,'#ffffff',4,3,2,15);return}
  const reduced=dmg*(1-(p.armor||0));
  if(p.thorns>0){
    G.enemies.forEach(e=>{if(Math.hypot(e.x-p.x,e.y-p.y)<80)e.hp-=p.thorns});
  }
  const floor=p.shieldFloor||0;
  if(p.shield>floor){
    const absorbed=Math.min(p.shield-floor,reduced);
    p.shield-=absorbed;
    const overflow=reduced-absorbed;
    if(overflow>0){p.hp=Math.max(0,p.hp-overflow);part(p.x,p.y,'#e24b4a',5,2,2,15)}
    else{part(p.x,p.y,'#378add',5,2,2,15)}
  } else {
    p.hp=Math.max(0,p.hp-reduced);
    part(p.x,p.y,'#e24b4a',5,2,2,15);
  }
  if(p.hp<=0&&!G.gameOver){
    if(p.phoenix&&!p.phoenixUsed){p.phoenixUsed=true;p.hp=p.maxHp*.5;showMsg('🦅 PHOENIX REBORN!',1500);part(p.x,p.y,'#fac775',40,8,4,50);return}
    G.gameOver=true;setTimeout(()=>showEndScreen(false),800)
  }
}

function checkXP(){
  if(G.xp>=G.xpNeeded){G.level++;G.xp=0;G.xpNeeded=Math.floor(G.xpNeeded*1.65);openTalent();checkAch()}
}

function openTalent(){
  G.talentOpen=true;
  const panel=document.getElementById('talent-panel');panel.style.display='block';
  currentTalentTab='atk';
  renderTalentTabs();
  renderTalentGrid();
}

function renderTalentTabs(){
  const tabs=document.getElementById('talent-tabs');tabs.innerHTML='';
  Object.entries(TALENT_CATS).forEach(([key,cat])=>{
    const t=document.createElement('div');t.className='ttab'+(key===currentTalentTab?' active':'');
    t.textContent=cat.label;t.style.color=key===currentTalentTab?cat.color:'';
    t.onclick=()=>{currentTalentTab=key;renderTalentTabs();renderTalentGrid()};
    tabs.appendChild(t);
  });
}

function renderTalentGrid(){
  const grid=document.getElementById('tg');grid.innerHTML='';
  const cat=currentTalentTab;
  const pool=TALENTS.filter(t=>t.cat===cat);
  // pick 3 — prefer not taken (unless stackable and already taken is ok), shuffle
  const notTaken=pool.filter(t=>t.stackable||!takenTalentIds.includes(t.id));
  const taken=pool.filter(t=>!t.stackable&&takenTalentIds.includes(t.id));
  const options=[...notTaken.sort(()=>Math.random()-.5).slice(0,3)];
  // pad with taken ones if not enough
  if(options.length<3)options.push(...taken.sort(()=>Math.random()-.5).slice(0,3-options.length));
  const shown=options.slice(0,3);

  shown.forEach(t=>{
    const alreadyTaken=!t.stackable&&takenTalentIds.includes(t.id);
    const d=document.createElement('div');
    d.className='tc'+(alreadyTaken?' taken':'');
    const hasSynergy=t.synergy&&takenTalentIds.includes(t.synergy);
    const catColor=TALENT_CATS[t.cat].color;
    d.innerHTML=`
      <div class="tc-icon">${t.icon}</div>
      <div class="tc-name" style="color:${catColor}">${t.name}</div>
      <div class="tc-desc">${t.desc}</div>
      <div class="tc-tag ${t.cat}">${t.detail}</div>
      ${hasSynergy?`<div class="tc-synergy">✦ Synergy boost!</div>`:''}
      ${alreadyTaken?'<div style="font-size:9px;color:#fac77580;margin-top:3px">Đã có — chọn lại để stack</div>':''}
    `;
    if(!alreadyTaken){
      d.onclick=()=>{
        t.apply(G.p);
        if(hasSynergy){applyTalentSynergy(t,G.p)}
        takenTalentIds.push(t.id);
        document.getElementById('talent-panel').style.display='none';
        G.talentOpen=false;
        showMsg('★ '+t.name+(hasSynergy?' ✦ SYNERGY!':''),1600);
        part(G.p.x,G.p.y,'#afa9ec',28,5,3,32);
      };
    }
    grid.appendChild(d);
  });

  document.getElementById('talent-synergy-hint').textContent=
    takenTalentIds.length>0?`Bạn có: ${[...new Set(takenTalentIds)].map(id=>TALENTS.find(t=>t.id===id)?.name||id).join(' • ')}`:'';
}

function applyTalentSynergy(talent,p){
  // bonus effects when synergy detected
  if(talent.id==='multishot'&&takenTalentIds.includes('rapidfire'))p.fireRate=Math.max(4,p.fireRate-1);
  if(talent.id==='blast'&&takenTalentIds.includes('vamp'))p.lifesteal+=5;
  if(talent.id==='drone'&&takenTalentIds.includes('missile'))addDrone();
  if(talent.id==='crit'&&takenTalentIds.includes('crit'))p.critMult+=.5;
}

function toggleShop(){
  if(G.talentOpen||G.skillUpOpen||G.gameOver||G.won)return;
  G.shopOpen=!G.shopOpen;
  const s=document.getElementById('shop-panel');s.style.display=G.shopOpen?'block':'none';
  if(G.shopOpen)renderShop();
}

function setShopTab(tab){
  shopTab=tab;
  document.querySelectorAll('.stab').forEach((el,i)=>{
    el.classList.toggle('active',['combat','defense','utility','special'][i]===tab);
  });
  renderShop();
}

function renderShop(){
  document.getElementById('shop-gold').textContent=G.gold;
  const el=document.getElementById('shop-items');el.innerHTML='';
  const items=SHOP_DATA[shopTab]||[];
  const unlockSecret=G.p.blackMarket;

  items.forEach(it=>{
    if(it.secret&&!unlockSecret)return;
    const remaining=it.stock-it.bought;
    const canBuy=G.gold>=it.cost&&remaining>0;
    const d=document.createElement('div');
    d.className='shop-item'+(remaining<=0?' bought':!canBuy?' cant':'');
    d.innerHTML=`
      <span class="si-icon">${it.icon}</span>
      <div class="si-body">
        <div class="si-name">${it.name}</div>
        <div class="si-desc">${it.desc}</div>
        <div class="si-effect">${it.effect}</div>
      </div>
      <div class="si-cost">
        <div class="si-price">${remaining<=0?'SOLD OUT':it.cost+'g'}</div>
        <div class="si-stock">${remaining>0?`còn ${remaining}/${it.stock}`:'—'}</div>
      </div>
    `;
    if(canBuy)d.onclick=()=>{
      G.gold-=it.cost;it.bought++;it.apply(G.p);
      part(G.p.x,G.p.y,'#1d9e75',16,4,3,25);
      renderShop();updateHUD();
    };
    el.appendChild(d);
  });

  if(items.length===0){
    el.innerHTML='<div style="text-align:center;color:#555;padding:20px;font-size:12px">Không có item trong danh mục này</div>';
  }
}

function onKey(code){
  if(code==='KeyE')toggleShop();
  if(code==='KeyX')openSkillUp();
  if(code==='KeyQ')doNuke();
  if(code==='KeyW')doShield();
  if(code==='KeyF')doEMP();
  if(code==='ShiftLeft'||code==='ShiftRight')doDash();
  if(code==='KeyR'&&!G.shopOpen&&!G.talentOpen&&!G.skillUpOpen)doVortex();
  if(code==='KeyT')doMeteor();
  if(code==='Escape'&&(G.shopOpen||G.skillUpOpen)){G.shopOpen=false;G.skillUpOpen=false;document.getElementById('shop-panel').style.display='none';document.getElementById('skill-panel').style.display='none'}
}

/* ── HUD ── */
function updateHUD(){
  const p=G.p;
  document.getElementById('hp-fill').style.width=(p.hp/p.maxHp*100)+'%';
  document.getElementById('sh-fill').style.width=(p.shield/p.maxShield*100)+'%';
  document.getElementById('xp-fill').style.width=(G.xp/G.xpNeeded*100)+'%';
  document.getElementById('hp-val').textContent=Math.ceil(p.hp)+'/'+p.maxHp;
  document.getElementById('sh-val').textContent=Math.ceil(p.shield)+'/'+p.maxShield;
  document.getElementById('lv-val').textContent='Lv'+G.level;
  document.getElementById('wn').textContent=G.wave;
  document.getElementById('sv').textContent=G.score.toLocaleString();
  document.getElementById('kv').textContent=G.kills;
  document.getElementById('gv').textContent=G.gold;
  const sk=p.skills;const ids=['dash','nuke','shield','emp','vortex','meteor'];
  ids.forEach((id,i)=>{
    const s=sk[id];const el=document.getElementById('scd'+i);
    el.textContent=s.cd>0?Math.ceil(s.cd/60)+'s':(s.lv>0?'Lv'+s.lv:'—');
    document.getElementById('sk'+i).style.borderColor=s.cd>0?'#ff333355':s.lv>0?'#0ff5':'#fff2';
  });
  const cel=document.getElementById('combo-el');
  if(G.combo>2){cel.textContent='COMBO ×'+G.combo+(G.combo>10?' 🔥':'');cel.style.opacity='1'}else cel.style.opacity='0';
  if(G.boss){
    document.getElementById('boss-fill').style.width=(G.boss.hp/G.boss.maxHp*100)+'%';
    document.getElementById('boss-name').textContent=(G.boss.enrage?'⚠️ ENRAGED ':'')+`BOSS PHASE ${G.boss.currentPhase}/${G.boss.phase}`;
    document.getElementById('boss-phase').textContent=G.boss.enrage?'RAGE MODE ACTIVE':'';
  }
}

/* ── minimap ── */
function drawMinimap(){
  mctx.clearRect(0,0,110,80);
  mctx.fillStyle='rgba(0,0,15,.65)';mctx.fillRect(0,0,110,80);
  const sx=110/W(),sy=80/H();
  G.enemies.forEach(e=>{mctx.fillStyle=e.color;mctx.fillRect(e.x*sx-1.5,e.y*sy-1.5,3,3)});
  if(G.boss){mctx.fillStyle='#ff3333';mctx.beginPath();mctx.arc(G.boss.x*sx,G.boss.y*sy,5,0,Math.PI*2);mctx.fill()}
  G.powerups.forEach(p=>{mctx.fillStyle=p.type==='hp'?'#e24b4a':'#378add';mctx.fillRect(p.x*sx,p.y*sy,3,3)});
  drones.forEach(d=>{mctx.fillStyle='#1d9e75';mctx.fillRect(d.x*sx-1,d.y*sy-1,2,2)});
  mctx.fillStyle='#0ff';mctx.beginPath();mctx.arc(G.p.x*sx,G.p.y*sy,3,0,Math.PI*2);mctx.fill();
  // scan line
  const sl=(G.frame*.02)%(Math.PI*2);
  mctx.strokeStyle='rgba(0,255,150,.2)';mctx.lineWidth=1;
  mctx.beginPath();mctx.moveTo(G.p.x*sx,G.p.y*sy);
  mctx.lineTo(G.p.x*sx+Math.cos(sl)*50,G.p.y*sy+Math.sin(sl)*50);mctx.stroke();
}

/* ── draw background ── */
function drawBG(){
  ctx.fillStyle='#000005';ctx.fillRect(0,0,W(),H());
  // nebulas
  G.nebulas.forEach((n,i)=>{
    const ox=Math.sin(G.frame*.0016+i)*18,oy=Math.cos(G.frame*.0012+i)*14;
    const grd=ctx.createRadialGradient(n.x+ox,n.y+oy,8,n.x+ox,n.y+oy,n.rx);
    grd.addColorStop(0,`hsla(${n.hue},80%,55%,${n.a})`);grd.addColorStop(.5,`hsla(${n.hue+30},60%,40%,${n.a*.5})`);grd.addColorStop(1,'transparent');
    ctx.fillStyle=grd;ctx.save();ctx.scale(1,n.ry/n.rx);ctx.beginPath();ctx.arc(n.x+ox,(n.y+oy)*n.rx/n.ry,n.rx,0,Math.PI*2);ctx.fill();ctx.restore();
  });
  // dust
  G.dustClouds.forEach((dc,i)=>{
    const x=(dc.x+G.frame*dc.sp*.3)%(W()+80);
    const grd=ctx.createRadialGradient(x,dc.y,4,x,dc.y,dc.r);
    grd.addColorStop(0,`hsla(${dc.hue},50%,60%,${dc.a})`);grd.addColorStop(1,'transparent');
    ctx.fillStyle=grd;ctx.beginPath();ctx.arc(x,dc.y,dc.r,0,Math.PI*2);ctx.fill();
  });
  // stars
  G.stars.forEach(s=>{
    const px=(s.x+G.frame*s.sp*.5)%(W()+50);
    const py=(s.y+G.frame*s.sp*.18)%(H()+50);
    const flicker=.55+Math.sin(G.frame*.04+s.b*9)*.45;
    const [r,g,b]=s.col;
    ctx.fillStyle=`rgba(${r},${g},${b},${s.b*flicker*.9})`;
    ctx.beginPath();ctx.arc(px,py,s.s,0,Math.PI*2);ctx.fill();
    if(s.s>1.4){
      ctx.strokeStyle=`rgba(${r},${g},${b},${s.b*.25*flicker})`;ctx.lineWidth=.5;
      ctx.beginPath();ctx.moveTo(px-s.s*2.5,py);ctx.lineTo(px+s.s*2.5,py);ctx.moveTo(px,py-s.s*2.5);ctx.lineTo(px,py+s.s*2.5);ctx.stroke();
    }
  });
}

/* ── draw player ── */
function drawPlayer(){
  const p=G.p;
  // trails (afterimage)
  G.trails.forEach(t=>{
    ctx.save();ctx.translate(t.x,t.y);ctx.rotate(t.angle);ctx.globalAlpha=t.a*.5;
    ctx.fillStyle='#378add';ctx.beginPath();ctx.moveTo(19,0);ctx.lineTo(-11,12);ctx.lineTo(-6,0);ctx.lineTo(-11,-12);ctx.closePath();ctx.fill();
    ctx.restore();
  });
  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.angle);
  // engine glow
  ctx.fillStyle='rgba(0,180,255,0.08)';ctx.beginPath();ctx.arc(0,0,28,0,Math.PI*2);ctx.fill();
  // wings
  ctx.fillStyle='#8899aa';ctx.beginPath();ctx.moveTo(-5,-6);ctx.lineTo(-14,-18);ctx.lineTo(-10,-7);ctx.closePath();ctx.fill();
  ctx.beginPath();ctx.moveTo(-5,6);ctx.lineTo(-14,18);ctx.lineTo(-10,7);ctx.closePath();ctx.fill();
  // body
  ctx.fillStyle='#c0c8d8';ctx.beginPath();ctx.moveTo(20,0);ctx.lineTo(-12,13);ctx.lineTo(-7,0);ctx.lineTo(-12,-13);ctx.closePath();ctx.fill();
  // center stripe
  ctx.fillStyle='#ffffff20';ctx.beginPath();ctx.moveTo(16,0);ctx.lineTo(-8,8);ctx.lineTo(-5,0);ctx.lineTo(-8,-8);ctx.closePath();ctx.fill();
  // engine pods
  ctx.fillStyle='#4466aa';ctx.fillRect(-15,-14,8,7);ctx.fillRect(-15,7,8,7);
  // cockpit
  ctx.fillStyle='rgba(0,230,255,0.95)';ctx.beginPath();ctx.arc(7,0,6,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.7)';ctx.beginPath();ctx.arc(8.5,-1.5,2.2,0,Math.PI*2);ctx.fill();
  // thruster flame
  const fl=5+Math.random()*7;
  ctx.fillStyle='rgba(255,140,30,0.9)';ctx.beginPath();ctx.moveTo(-12,-4.5);ctx.lineTo(-12-fl,0);ctx.lineTo(-12,4.5);ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(255,220,100,0.6)';ctx.beginPath();ctx.moveTo(-12,-2.5);ctx.lineTo(-12-fl*.55,0);ctx.lineTo(-12,2.5);ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(200,230,255,0.8)';ctx.beginPath();ctx.moveTo(-12,-1);ctx.lineTo(-12-fl*.25,0);ctx.lineTo(-12,1);ctx.closePath();ctx.fill();
  ctx.restore();

  // shield ring
  if(G.p.skills.shield.cd>130){
    const alpha=.5+Math.sin(G.frame*.3)*.3;
    ctx.strokeStyle=`rgba(0,255,255,${alpha})`;ctx.lineWidth=2.5;
    ctx.beginPath();ctx.arc(p.x,p.y,34,0,Math.PI*2);ctx.stroke();
    ctx.strokeStyle=`rgba(0,255,255,${alpha*.3})`;ctx.lineWidth=6;
    ctx.stroke();
  }
  // vortex indicator
  if(vortexActive.on){
    ctx.strokeStyle='rgba(127,119,221,0.6)';ctx.lineWidth=1.5;
    ctx.setLineDash([4,4]);
    ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(vortexActive.x,vortexActive.y);ctx.stroke();
    ctx.setLineDash([]);
  }
}

function drawEnemy(e){
  if(e.frozen>0){ctx.save();ctx.globalAlpha=.5+.4*Math.abs(Math.sin(G.frame*.35))}
  ctx.save();ctx.translate(e.x,e.y);
  const spin=e.type==='tank'?.022:.05;e.angle+=spin;ctx.rotate(e.angle);
  ctx.fillStyle=e.color;
  if(e.type==='guardian'){
    // hexagonal with inner detail
    ctx.beginPath();for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.25)';ctx.lineWidth=1.5;ctx.stroke();
    ctx.fillStyle='rgba(0,0,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.5,0,Math.PI*2);ctx.fill();
    ctx.fillStyle=e.color+'88';ctx.beginPath();ctx.arc(0,0,e.size*.25,0,Math.PI*2);ctx.fill();
  } else if(e.type==='tank'){
    ctx.beginPath();for(let i=0;i<6;i++){const a=i/6*Math.PI*2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(0,0,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.42,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.2)';ctx.lineWidth=1;ctx.stroke();
  } else if(e.type==='fast'){
    ctx.beginPath();ctx.moveTo(e.size,0);ctx.lineTo(-e.size*.6,e.size*.9);ctx.lineTo(-e.size*.3,0);ctx.lineTo(-e.size*.6,-e.size*.9);ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.15)';ctx.beginPath();ctx.moveTo(e.size*.5,0);ctx.lineTo(-e.size*.3,e.size*.4);ctx.lineTo(-e.size*.1,0);ctx.lineTo(-e.size*.3,-e.size*.4);ctx.closePath();ctx.fill();
  } else if(e.type==='bomber'){
    ctx.beginPath();for(let i=0;i<5;i++){const a=i/5*Math.PI*2-Math.PI/2;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,80,0,.4)';ctx.beginPath();ctx.arc(0,0,e.size*.38,0,Math.PI*2);ctx.fill();
  } else if(e.type==='elite'){
    ctx.beginPath();for(let i=0;i<8;i++){const a=i/8*Math.PI*2,r=i%2===0?e.size:e.size*.5;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.3)';ctx.lineWidth=1;ctx.stroke();
    ctx.fillStyle='rgba(255,255,255,.12)';ctx.beginPath();ctx.arc(0,0,e.size*.3,0,Math.PI*2);ctx.fill();
  } else {
    ctx.beginPath();for(let i=0;i<4;i++){const a=i/4*Math.PI*2+Math.PI/4;ctx.lineTo(Math.cos(a)*e.size,Math.sin(a)*e.size)}ctx.closePath();ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.18)';ctx.lineWidth=1;ctx.stroke();
  }
  ctx.restore();
  // shield
  if(e.shield>0){
    ctx.strokeStyle=`rgba(55,138,221,${.4+e.shield/e.maxShield*.4})`;ctx.lineWidth=2;
    ctx.beginPath();ctx.arc(e.x,e.y,e.size+5,0,Math.PI*2);ctx.stroke();
  }
  if(e.frozen>0)ctx.restore();
  // burn effect
  if(e.burning>0){ctx.strokeStyle=`rgba(255,100,0,${.5+Math.sin(G.frame*.2)*.3})`;ctx.lineWidth=2;ctx.beginPath();ctx.arc(e.x,e.y,e.size+3,0,Math.PI*2);ctx.stroke()}
  // disrupted effect
  if(e.disrupted>0){ctx.strokeStyle=`rgba(80,80,255,.35)`;ctx.lineWidth=1.5;ctx.setLineDash([3,3]);ctx.beginPath();ctx.arc(e.x,e.y,e.size+7,0,Math.PI*2);ctx.stroke();ctx.setLineDash([])}
  // HP bar
  const bw=e.size*2.5,bh=4;
  ctx.fillStyle='#fff1';ctx.fillRect(e.x-bw/2,e.y-e.size-12,bw,bh);
  ctx.fillStyle=e.color;ctx.fillRect(e.x-bw/2,e.y-e.size-12,bw*(e.hp/e.maxHp),bh);
}

function drawBoss(b){
  const enrage=b.enrage;
  ctx.save();ctx.translate(b.x,b.y);
  const outerSpin=G.frame*.025*(enrage?2:1);ctx.rotate(outerSpin);
  // outer ring
  ctx.strokeStyle=enrage?'rgba(255,60,0,.35)':'rgba(226,75,74,.2)';ctx.lineWidth=3;
  ctx.beginPath();for(let i=0;i<16;i++){const a=i/16*Math.PI*2,r=i%2===0?b.size*2:b.size*1.7;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.stroke();
  ctx.restore();
  ctx.save();ctx.translate(b.x,b.y);b.angle+=enrage?.055:.032;ctx.rotate(b.angle);
  const pulse=.94+Math.sin(G.frame*.09)*.06;
  // aura
  ctx.fillStyle=enrage?'rgba(255,50,50,.12)':'rgba(226,75,74,.1)';
  ctx.beginPath();ctx.arc(0,0,b.size*1.85*pulse,0,Math.PI*2);ctx.fill();
  // body
  ctx.fillStyle=enrage?'#ff2222':'#e24b4a';
  ctx.beginPath();for(let i=0;i<12;i++){const a=i/12*Math.PI*2,r=i%2===0?b.size:b.size*.55;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();
  if(enrage){ctx.strokeStyle='rgba(255,200,0,.6)';ctx.lineWidth=2.5;ctx.stroke()}
  // inner
  ctx.fillStyle='rgba(255,80,80,.7)';ctx.beginPath();ctx.arc(0,0,b.size*.38,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='rgba(255,200,200,.5)';ctx.beginPath();ctx.arc(0,0,b.size*.16,0,Math.PI*2);ctx.fill();
  ctx.restore();
  // HP bar above
  const bw=90,bh=7;
  ctx.fillStyle='#fff1';ctx.fillRect(b.x-bw/2,b.y-b.size-16,bw,bh);
  ctx.fillStyle=enrage?'#ff2222':'#e24b4a';ctx.fillRect(b.x-bw/2,b.y-b.size-16,bw*(b.hp/b.maxHp),bh);
  // shield ring
  if(b.shield>0){
    ctx.strokeStyle=`rgba(55,138,221,${.3+b.shield/b.maxShield*.4})`;ctx.lineWidth=3;
    ctx.beginPath();ctx.arc(b.x,b.y,b.size+10,0,Math.PI*2);ctx.stroke();
  }
}

function drawDrone(dr){
  const x=G.p.x+Math.cos(dr.a)*dr.orb,y=G.p.y+Math.sin(dr.a)*dr.orb;
  dr.x=x;dr.y=y;
  // orbit path
  ctx.strokeStyle='rgba(29,158,117,.18)';ctx.lineWidth=1;ctx.setLineDash([3,7]);
  ctx.beginPath();ctx.arc(G.p.x,G.p.y,dr.orb,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);
  // body
  ctx.save();ctx.translate(x,y);ctx.rotate(G.frame*.06);
  ctx.fillStyle='#1d9e75';
  ctx.beginPath();for(let i=0;i<4;i++){const a=i/4*Math.PI*2+Math.PI/4;ctx.lineTo(Math.cos(a)*7,Math.sin(a)*7)}ctx.closePath();ctx.fill();
  ctx.fillStyle='rgba(93,202,165,.8)';ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();
  ctx.restore();
}

function drawMeteors(){
  meteors.forEach(m=>{
    m.progress=Math.min(1,m.progress+.025);
    m.x=m.x+(m.tx-m.x)*m.progress;m.y=m.y+(m.ty-m.y)*m.progress;
    // trail
    ctx.strokeStyle='rgba(255,100,30,.6)';ctx.lineWidth=3;
    ctx.beginPath();ctx.moveTo(m.x-Math.cos(Math.atan2(m.ty-(-60),m.tx-(m.x-80)))*20,m.y-Math.sin(Math.atan2(m.ty-(-60),m.tx-(m.x-80)))*20);
    ctx.lineTo(m.x,m.y);ctx.stroke();
    ctx.fillStyle='#fac775';ctx.beginPath();ctx.arc(m.x,m.y,m.r,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='rgba(255,200,100,.6)';ctx.beginPath();ctx.arc(m.x,m.y,m.r*.5,0,Math.PI*2);ctx.fill();
    // impact
    if(m.progress>=1){
      part(m.x,m.y,'#fac775',20,7,4,30);
      G.enemies.forEach(e=>{if(Math.hypot(e.x-m.x,e.y-m.y)<50){e.hp-=m.dmg;floatDmgPop(e.x,e.y-20,Math.round(m.dmg),'#fac775');if(e.hp<=0){killEnemy(e)}}});
      if(G.boss&&Math.hypot(G.boss.x-m.x,G.boss.y-m.y)<60){G.boss.hp-=m.dmg;if(G.boss.hp<=0)killBoss()}
    }
  });
  meteors=meteors.filter(m=>m.progress<1);
}

function drawVortex(){
  if(!vortexActive.on)return;
  vortexActive.timer--;
  if(vortexActive.timer<=0){vortexActive.on=false;return}
  const {x,y,suck}=vortexActive;
  const spin=G.frame*.12,r=80;
  // rings
  for(let i=0;i<3;i++){
    ctx.strokeStyle=`rgba(127,119,221,${.3-i*.08})`;ctx.lineWidth=2-i*.4;
    ctx.beginPath();ctx.arc(x,y,r*(i+1)*.4,0,Math.PI*2);ctx.stroke();
  }
  // spiraling particles
  for(let i=0;i<3;i++){
    const a=spin+i*2.094;const pr=r*(.3+Math.abs(Math.sin(G.frame*.08+i))*.7);
    ctx.fillStyle='rgba(175,169,236,.7)';ctx.beginPath();ctx.arc(x+Math.cos(a)*pr,y+Math.sin(a)*pr,3,0,Math.PI*2);ctx.fill();
  }
  // suck enemies
  G.enemies.forEach(e=>{
    const dx=x-e.x,dy=y-e.y,dist=Math.hypot(dx,dy)||1;
    if(dist<r*2){e.x+=dx/dist*suck;e.y+=dy/dist*suck}
  });
}

function drawParticles(){
  G.particles.forEach(p=>{
    if(p.ring){
      p.r+=(p.maxR-p.r)*.12;p.life--;
      ctx.strokeStyle=p.color.replace(')',`,${p.life/30})`).replace('rgb','rgba').replace('#','rgba(').replace(/^rgba\(([^)]+)\)$/,(m,c)=>{const hex=parseInt(c,16);return`rgba(${hex>>16&255},${hex>>8&255},${hex&255},${p.life/30})`});
      ctx.strokeStyle=p.color;ctx.globalAlpha=p.life/30;ctx.lineWidth=3;
      ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.stroke();ctx.globalAlpha=1;
    } else {
      p.x+=p.vx;p.y+=p.vy;p.vx*=.89;p.vy*=.89;p.life--;
      ctx.globalAlpha=p.life/45;ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;
    }
  });
  G.particles=G.particles.filter(p=>p.life>0);
}

/* ── main update ── */
function update(){
  if(!G||G.gameOver||G.won||G.talentOpen)return;
  G.frame++;
  const p=G.p;
  // movement
  let mx=0,my=0;
  if(!G.shopOpen&&!G.skillUpOpen){
    if(keys['ArrowLeft']||keys['KeyA'])mx=-1;
    if(keys['ArrowRight']||keys['KeyD'])mx=1;
    if(keys['ArrowUp']||keys['KeyW'])my=-1;
    if(keys['ArrowDown']||keys['KeyS'])my=1;
  }
  if(mx||my){const l=Math.hypot(mx,my)||1;p.vx=mx/l*p.speed;p.vy=my/l*p.speed}
  p.vx*=.82;p.vy*=.82;
  p.x=Math.max(20,Math.min(W()-20,p.x+p.vx));
  p.y=Math.max(20,Math.min(H()-20,p.y+p.vy));
  p.angle=Math.atan2(M.y-p.y,M.x-p.x);

  // passive
  const shieldSk=p.skills.shield;
  if(shieldSk.cd===0)p.shield=Math.min(p.maxShield,p.shield+.08);
  if(p.regen&&!(G.diffCfg&&G.diffCfg.noRegen)&&G.frame%60===0)p.hp=Math.min(p.maxHp,p.hp+(p.regenAmt||3));
  // shield regen talent
  if(p.shieldRegen&&G.frame%60===0)p.shield=Math.min(p.maxShield,p.shield+(p.shieldRegenAmt||3));
  // emergency repair
  if(p.emergencyRepair&&!p.emergencyRepairUsed&&p.hp<p.maxHp*.25){p.emergencyRepairUsed=true;p.hp=Math.min(p.maxHp,p.hp+80);showMsg('🔧 EMERGENCY REPAIR',1000);part(p.x,p.y,'#1d9e75',20,5,3,30)}
  // last stand
  if(p.lastStand)p._lastStandMult=p.hp/p.maxHp<.2?1.5:1;

  // shockwave passive
  if(p.shockwave){
    p.shockwaveCd=(p.shockwaveCd||0)+1;
    if(p.shockwaveCd>=360){p.shockwaveCd=0;
      G.enemies.forEach(e=>{const dx=e.x-p.x,dy=e.y-p.y,dist=Math.hypot(dx,dy)||1;if(dist<180){e.x+=dx/dist*60;e.y+=dy/dist*60;e.hp-=15}});
      part(p.x,p.y,'#378add',30,8,3,40);showMsg('🌊 SHOCKWAVE',600);}
  }
  // gravity well passive
  if(p.gravityWell){
    p.gravityWellCd=(p.gravityWellCd||0)+1;
    if(p.gravityWellCd>=600){p.gravityWellCd=0;
      G.enemies.forEach(e=>{const dx=p.x-e.x,dy=p.y-e.y,dist=Math.hypot(dx,dy)||1;if(dist<400){e.x+=dx/dist*80;e.y+=dy/dist*80}});
      if(G.boss){const dx=p.x-G.boss.x,dy=p.y-G.boss.y,dist=Math.hypot(dx,dy)||1;G.boss.x+=dx/dist*30;G.boss.y+=dy/dist*30}
      part(p.x,p.y,'#7f77dd',50,10,4,60);showMsg('🌍 GRAVITY WELL',700);}
  }
  // supernova
  if(p.supernova&&!p.supernovaUsed&&p.hp/p.maxHp<.1){
    p.supernovaUsed=true;
    G.enemies.forEach(e=>{if(Math.hypot(e.x-p.x,e.y-p.y)<350){e.hp-=500}});
    part(p.x,p.y,'#fac775',80,12,5,80);showMsg('🌟 SUPERNOVA!!!',2000);
  }
  // turret auto-fire
  if(p.turret&&G.frame%60===0){
    let nearest=null,nd=999;
    G.enemies.forEach(e=>{const d=Math.hypot(e.x-p.x,e.y-p.y);if(d<nd){nd=d;nearest=e}});
    if(!nearest&&G.boss)nearest=G.boss;
    if(nearest){const a=Math.atan2(nearest.y-p.y,nearest.x-p.x);fireBullet(p.x,p.y,a,8,'#fac775',18,'player',p.pierce,false)}
  }

  // timewarp passive
  if(p.timewarp){
    p.timewarpCd=(p.timewarpCd||0)+1;
    if(p.timewarpCd>=480){p.timewarpCd=0;p.timewarpActive=true;setTimeout(()=>{p.timewarpActive=false},2000);showMsg('⏰ TIME WARP',700);part(p.x,p.y,'#afa9ec',30,4,3,40)}
  }

  // cooldowns — apply cdReduction multiplier
  const cdr=p.cdReduction||1;
  Object.values(p.skills).forEach(sk=>{if(sk.cd>0)sk.cd=Math.max(0,sk.cd-1/cdr)});

  // fire
  p.fireCd--;
  if(M.dn&&p.fireCd<=0&&!G.shopOpen&&!G.skillUpOpen){
    p.fireCd=p.fireRate;
    const bSize=p.plasma?7:undefined;
    const bDmg=p.plasma?18:10;
    const bCol=p.plasma?'#378add':'#0af';
    fireBullet(p.x,p.y,p.angle,p.bulletSpeed,bCol,bDmg,'player',p.pierce,p.ricochet,bSize);
    // burst fire
    if(p.burst){
      p.burstCounter=(p.burstCounter||0)+1;
      if(p.burstCounter%5===0){
        for(let i=-1;i<=1;i++)fireBullet(p.x,p.y,p.angle+i*.25,p.bulletSpeed*1.1,'#fac775',14,'player',p.pierce,false);
      }
    }
    if(p.missiles&&G.frame%52===0)fireHoming(p.x,p.y);
  }

  if(G.comboTimer>0)G.comboTimer--;else G.combo=0;

  // isPoisonMode must be declared BEFORE bullet filter (used inside callback)
  const isPoisonMode=G.diffCfg&&G.diffCfg.poisonBullets;

  // bullets
  G.bullets=G.bullets.filter(b=>{
    if(b.homing&&b.homing&&b.homing.hp>0){
      const tx=b.homing.x-b.x,ty=b.homing.y-b.y,d=Math.hypot(tx,ty)||1;
      b.vx+=tx/d*.55;b.vy+=ty/d*.55;const sp=Math.hypot(b.vx,b.vy);if(sp>8){b.vx=b.vx/sp*8;b.vy=b.vy/sp*8}
    }
    b.x+=b.vx;b.y+=b.vy;b.life--;
    if(b.life<=0||b.x<-40||b.x>W()+40||b.y<-40||b.y>H()+40){
      if(b.ricochet&&b.bounces<2){
        if(b.x<-20||b.x>W()+20)b.vx*=-1;else b.vy*=-1;
        b.bounces++;b.life=80;return true;
      }
      return false;
    }
    if(b.owner==='player'){
      const toKill=[];
      for(let i=G.enemies.length-1;i>=0;i--){
        const e=G.enemies[i];
        if(b.hit.includes(e))continue;
        if(Math.hypot(b.x-e.x,b.y-e.y)<e.size+b.size){
          const dmg=e.frozen>0?b.dmg*1.6:b.dmg;
          if(e.shield>0){e.shield=Math.max(0,e.shield-dmg*.5)}
          else{e.hp-=dmg;floatDmgPop(e.x,e.y-e.size-15,Math.round(dmg),b.color)}
          part(b.x,b.y,b.color,4,3,2,18);
          // freeze on hit
          if(b.freeze){e.frozen=Math.max(e.frozen||0,120);part(e.x,e.y,'#aaddff',6,2,2,20)}
          // explosive on hit
          if(b.explosive){part(e.x,e.y,'#fac775',14,6,4,25);G.enemies.forEach(ne=>{if(ne!==e&&Math.hypot(ne.x-e.x,ne.y-e.y)<40){ne.hp-=b.dmg*.5;floatDmgPop(ne.x,ne.y-ne.size-10,Math.round(b.dmg*.5),'#fac775')}})}
          // incendiary on hit
          if(b.incendiary){e.burning=(e.burning||0)+180;e.burnDmg=b.dmg*.15}
          // disruptor on hit
          if(b.disruptor){e.disrupted=180}
          // chain bolt — arc to 2 nearby enemies
          if(b.chain&&!b.chainDone){
            b.chainDone=true;
            let chained=0;
            G.enemies.forEach(ne=>{if(ne!==e&&chained<2&&Math.hypot(ne.x-e.x,ne.y-e.y)<120){ne.hp-=b.dmg*.5;chained++;part(ne.x,ne.y,'#afa9ec',5,3,2,15);floatDmgPop(ne.x,ne.y-ne.size-10,Math.round(b.dmg*.5),'#afa9ec')}});
          }
          if(e.hp<=0)toKill.push(i);
          if(b.pierce){b.hit.push(e)}else{
            const kens=[...toKill];kens.forEach(idx=>{const en=G.enemies[idx];killEnemy(en)});
            G.enemies=G.enemies.filter((_,i)=>!toKill.includes(i));return false;
          }
        }
      }
      if(toKill.length){const kens=toKill.map(i=>G.enemies[i]);kens.forEach(en=>killEnemy(en));G.enemies=G.enemies.filter((_,i)=>!toKill.includes(i))}
      if(G.boss&&Math.hypot(b.x-G.boss.x,b.y-G.boss.y)<G.boss.size+b.size){
        if(G.boss.shield>0)G.boss.shield=Math.max(0,G.boss.shield-b.dmg*.4);
        else{G.boss.hp-=b.dmg;floatDmgPop(G.boss.x,G.boss.y-G.boss.size-15,Math.round(b.dmg),'#ff5555')}
        part(b.x,b.y,b.color,3,3,2,15);if(G.boss.hp<=0)killBoss();return false;
      }
    } else {
      if(Math.hypot(b.x-p.x,b.y-p.y)<22){
        hitPlayer(b.dmg);
        if(isPoisonMode&&b.color==='#5dcaa5')p.poisonStacks=Math.min(5,(p.poisonStacks||0)+1);
        return false;
      }
    }
    return true;
  });

  // enemies AI
  const twSlow=G.p.timewarpActive?0.35:1;
  // poison tick
  if(isPoisonMode&&G.p.poisonStacks&&G.frame%90===0){
    G.p.hp=Math.max(1,G.p.hp-G.p.poisonStacks*3);
    part(G.p.x,G.p.y,'#5dcaa5',4,2,2,18);
    G.p.poisonStacks=Math.max(0,G.p.poisonStacks-1);
  }
  G.enemies.forEach(e=>{
    // burning tick
    if(e.burning>0){e.burning--;if(e.burning%20===0){e.hp-=(e.burnDmg||2);floatDmgPop(e.x,e.y-e.size-10,Math.round(e.burnDmg||2),'#ff6600');part(e.x,e.y,'#ff6600',3,2,2,12)}}
    // disrupted: enemy deals 40% less damage (checked in hitPlayer via enemy property)
    if(e.disrupted>0)e.disrupted--;
    if(e.frozen>0){e.frozen--;return}
    const dx=p.x-e.x,dy=p.y-e.y,dist=Math.hypot(dx,dy)||1;
    e.x+=dx/dist*e.speed*twSlow;e.y+=dy/dist*e.speed*twSlow;
    if(dist<28)hitPlayer(1.5);
    if(e.shoot){
      e.shootCd-=twSlow;
      if(e.shootCd<=0){
        e.shootCd=e.shootRate||80;
        const angle=Math.atan2(dy,dx);
        const bcol=isPoisonMode?'#5dcaa5':null;
        if(e.shotType==='spread'){for(let i=-1;i<=1;i++)fireBullet(e.x,e.y,angle+i*.3,3.2*twSlow,bcol||'#d85a30',7,'enemy')}
        else if(e.shotType==='spiral'){e.spiralAngle+=.42;for(let i=0;i<3;i++)fireBullet(e.x,e.y,e.spiralAngle+i*2.094,3.3*twSlow,bcol||'#d4537e',7,'enemy')}
        else if(e.shotType==='burst'){
          e.burstCount=(e.burstCount||0)+1;
          fireBullet(e.x,e.y,angle+(Math.random()-.5)*.2,3.8*twSlow,bcol||'#97c459',9,'enemy');
          if(e.burstCount%3===0)for(let i=0;i<6;i++)fireBullet(e.x,e.y,i/6*Math.PI*2,2.8*twSlow,bcol||'#639922',7,'enemy');
        }
        else fireBullet(e.x,e.y,angle+(Math.random()-.5)*.15,3.2*twSlow,bcol||'#fac775',7,'enemy');
      }
    }
  });

  // boss AI
  if(G.boss){
    const b=G.boss;
    if(b.frozen>0){b.frozen--}
    else{
      const dx=p.x-b.x,dy=p.y-b.y,dist=Math.hypot(dx,dy)||1;
      b.x+=dx/dist*b.speed;b.y+=dy/dist*b.speed;
      if(dist<52)hitPlayer(2.5);
      const enrage=b.hp<b.maxHp*.3;
      if(!b.enrage&&enrage){b.enrage=true;b.currentPhase=Math.min(b.phase,b.currentPhase+1);showMsg('⚠️ BOSS ENRAGED!',2000)}
      b.shootCd--;
      if(b.shootCd<=0){
        b.shootCd=enrage?18:32;b.spiralA+=enrage?.62:.34;
        const shots=enrage?7:4,spd=enrage?4.5:3.8;
        for(let i=0;i<shots;i++)fireBullet(b.x,b.y,b.spiralA+i*(Math.PI*2/shots),spd,'#ff5555',enrage?16:12,'enemy');
        if(enrage&&G.frame%90===0)for(let i=0;i<12;i++)fireBullet(b.x,b.y,i/12*Math.PI*2,2.5,'#ff3333',8,'enemy');
      }
      if(b.shield>0)b.shield=Math.min(b.maxShield,b.shield+.05);
    }
  }

  // drones
  drones.forEach(dr=>{
    dr.a+=.038;dr.cd--;
    const x=G.p.x+Math.cos(dr.a)*dr.orb,y=G.p.y+Math.sin(dr.a)*dr.orb;dr.x=x;dr.y=y;
    if(dr.cd<=0){
      dr.cd=32;let near=null,nd=9999;
      G.enemies.forEach(e=>{const d=Math.hypot(e.x-x,e.y-y);if(d<nd){nd=d;near=e}});
      if(near&&nd<250)fireBullet(x,y,Math.atan2(near.y-y,near.x-x),7.5,'#1d9e75',10*(G.p.dmgMult||1),'player');
    }
  });

  // powerups & orbs
  G.powerups=G.powerups.filter(pu=>{
    pu.life--;
    const d=Math.hypot(pu.x-p.x,pu.y-p.y);
    if(p.magnet&&d<130){pu.x+=(p.x-pu.x)*.14;pu.y+=(p.y-pu.y)*.14}
    if(d<20){if(pu.type==='hp')p.hp=Math.min(p.maxHp,p.hp+30);else p.shield=Math.min(p.maxShield,p.shield+40);part(pu.x,pu.y,pu.type==='hp'?'#e24b4a':'#378add',10,3,2.5,25);return false}
    return pu.life>0;
  });
  G.orbs=G.orbs.filter(o=>{
    o.life--;
    const d=Math.hypot(o.x-p.x,o.y-p.y);
    if(p.magnet&&d<140){o.x+=(p.x-o.x)*.12;o.y+=(p.y-o.y)*.12}
    if(d<24){G.gold+=o.val;G.totalGold+=o.val;part(o.x,o.y,'#fac775',6,3,2,20);return false}
    return o.life>0;
  });

  // trails
  G.trails=G.trails.filter(t=>{t.life--;t.a-=.04;return t.life>0});
  // floatdmg
  floatDmg=floatDmg.filter(f=>{f.y+=f.vy;f.life--;return f.life>0});

  // wave management
  if(!G.waveBreak&&!G.boss){
    G.spawnTimer--;
    const rate=Math.max(20,105-G.wave*9),maxE=Math.min(28,10+G.wave*2);
    if(G.spawnTimer<=0&&G.enemies.length<maxE){G.spawnTimer=rate;spawnEnemy()}
    if(G.waveKills>=G.waveTarget&&G.enemies.length===0){
      if(G.wave%3===0&&!G.bossSpawned){G.bossSpawned=true;spawnBoss()}
      else if(G.wave%3!==0||!G.boss){
        G.waveBreak=true;G.waveBreakTimer=220;G.bossSpawned=false;
        const waveBonusGold=(G.p.waveBonus||0);
        const baseGold=20+G.wave*8;
        showMsg(`✓ WAVE ${G.wave} CLEAR!\n+${baseGold+waveBonusGold} vàng`,2200);
        G.gold+=baseGold+waveBonusGold;G.totalGold+=baseGold+waveBonusGold;
        G.wave++;G.waveKills=0;G.waveTarget=Math.ceil(G.waveTarget*1.3);
        if(G.wave>G.totalWaves){G.won=true;setTimeout(()=>showEndScreen(true),800)}
        checkAch();
      }
    }
  } else if(G.waveBreak){G.waveBreakTimer--;if(G.waveBreakTimer<=0){G.waveBreak=false;G.spawnTimer=90}}

  checkAch();updateHUD();drawMinimap();
}

/* ── draw ── */
function draw(){
  if(!G)return;
  drawBG();
  // vortex
  drawVortex();
  // orbs
  G.orbs.forEach(o=>{
    ctx.fillStyle='rgba(250,199,117,.9)';ctx.beginPath();ctx.arc(o.x,o.y,7,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#fff';ctx.font='bold 8px monospace';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(o.val,o.x,o.y);
  });
  // powerups
  G.powerups.forEach(pu=>{
    const pulse=.85+Math.sin(G.frame*.14)*.15;
    ctx.fillStyle=pu.type==='hp'?`rgba(226,75,74,${pulse})`:`rgba(55,138,221,${pulse})`;
    ctx.beginPath();ctx.arc(pu.x,pu.y,10,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.9)';ctx.font='11px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(pu.type==='hp'?'♥':'⚡',pu.x,pu.y);
  });
  // drones
  drones.forEach(drawDrone);
  // particles
  drawParticles();
  // bullets
  G.bullets.forEach(b=>{
    if(b.homing){
      ctx.strokeStyle=b.color;ctx.lineWidth=2.5;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.stroke();
      ctx.fillStyle=b.color+'60';ctx.beginPath();ctx.arc(b.x,b.y,b.size+3,0,Math.PI*2);ctx.fill();
    } else {
      ctx.fillStyle=b.color;ctx.beginPath();ctx.arc(b.x,b.y,b.size,0,Math.PI*2);ctx.fill();
      if(b.size>5){ctx.fillStyle=b.color+'40';ctx.beginPath();ctx.arc(b.x,b.y,b.size*1.8,0,Math.PI*2);ctx.fill()}
    }
  });
  // enemies
  G.enemies.forEach(drawEnemy);
  if(G.boss)drawBoss(G.boss);
  // meteors
  drawMeteors();
  // player
  if(!G.gameOver)drawPlayer();
  // nuke ring
  if(G.p.skills.nuke.cd>200){
    const prog=(G.p.skills.nuke.cd-200)/40;
    const r=skillVal('nuke','radius')*200*(1-prog);
    ctx.strokeStyle=`rgba(250,199,117,${prog*.8})`;ctx.lineWidth=4;
    ctx.beginPath();ctx.arc(G.p.x,G.p.y,r,0,Math.PI*2);ctx.stroke();
  }
  // wave flash
  if(G.waveBreak){const t=G.waveBreakTimer/220;ctx.fillStyle=`rgba(0,255,180,${t*.1})`;ctx.fillRect(0,0,W(),H())}
  // timewarp overlay
  if(G.p.timewarpActive){
    ctx.fillStyle='rgba(80,60,180,0.08)';ctx.fillRect(0,0,W(),H());
    ctx.strokeStyle='rgba(175,169,236,0.15)';ctx.lineWidth=2;
    for(let i=0;i<4;i++){ctx.beginPath();ctx.arc(G.p.x,G.p.y,60+i*40,0,Math.PI*2);ctx.stroke()}
    ctx.fillStyle='rgba(175,169,236,0.6)';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.textBaseline='top';ctx.fillText('⏰ TIME WARP',W()/2,70);
  }
  // float damage numbers
  floatDmg.forEach(f=>{
    ctx.globalAlpha=f.life/45;ctx.font=`bold ${10+Math.min(6,f.val/20)}px monospace`;ctx.fillStyle=f.col;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(f.val,f.x,f.y);
  });
  ctx.globalAlpha=1;
}

function loop(){
  requestAnimationFrame(loop); // always schedule first — errors can't break the loop
  if(!G)return;
  try{if(!G.shopOpen&&!G.talentOpen&&!G.skillUpOpen)update()}catch(e){console.warn('[update]',e)}
  try{draw()}catch(e){console.warn('[draw]',e)}
}




// ═══════════════════════════════════════════════
// BOOT
// ═══════════════════════════════════════════════
loopRunning=true;
loop();
showMenu();
</script>
</body>
</html>
chơi ở đây
 
Sửa lần cuối:


Top