/* ============================================================================
   GamesJi Top War — script.js
   Original vanilla-JS game engine. No frameworks, no external libraries.
   Organized in sections:
     1. Config & Definitions   6. Merge / Unit System     11. Daily Rewards
     2. Level Generator        7. Drag & Drop Input       12. Ads/Analytics stubs
     3. State & Save System    8. Battle Engine           13. Navigation & Wiring
     4. Audio Engine           9. Campaign Progression     14. Boot Sequence
     5. UI Rendering          10. Achievements
   ============================================================================ */
'use strict';

/* ============================================================================
   1. CONFIG & DEFINITIONS
   ============================================================================ */
const CONFIG = {
  SAVE_KEY: 'gamesji_topwar_save_v1',
  MERGE_BASE_SLOTS: 12,
  MERGE_SLOTS_PER_GARAGE_LVL: 2,
  MERGE_MAX_SLOTS: 30,
  MERGE_GRID_COLS: 6,
  SUB_LEVEL_CAP: 5,        // levels within one unit tier before it evolves
  TOP_TIER_LEVEL_CAP: 30,  // max level for the final tier of a line
  OFFLINE_EARN_CAP_HOURS_BASE: 2,
  OFFLINE_EARN_CAP_HOURS_PER_WAREHOUSE: 0.5,
  BATTLE_ARENA_MARGIN: 40,
  AUTOSAVE_INTERVAL_MS: 15000,
};

const RESOURCE_DEFS = {
  coins:    { name: 'Coins',    icon: 'assets/images/resource_coins.png' },
  energy:   { name: 'Energy',   icon: 'assets/images/resource_energy.png' },
  steel:    { name: 'Steel',    icon: 'assets/images/resource_steel.png' },
  fuel:     { name: 'Fuel',     icon: 'assets/images/resource_fuel.png' },
  diamonds: { name: 'Diamonds', icon: 'assets/images/resource_diamonds.png' },
};
const RESOURCE_ORDER = ['coins', 'energy', 'steel', 'fuel', 'diamonds'];

/* ---------------- Buildings ---------------- */
// effect(level) returns a human string; effectValue(level) returns the raw bonus number used in formulas.
const BUILDING_DEFS = {
  hq: {
    id: 'hq', name: 'Headquarters', icon: 'assets/images/building_hq.png',
    desc: 'The heart of your base. Raising its level unlocks higher levels for every other building and opens new campaign chapters.',
    maxLevel: 20, requiresHq: 0,
    baseCost: { coins: 200, steel: 40 }, growth: 1.65,
    effect: (lvl) => `+${(lvl * 2)}% global resource gain`,
    effectValue: (lvl) => 1 + lvl * 0.02,
  },
  barracks: {
    id: 'barracks', name: 'Barracks', icon: 'assets/images/building_barracks.png',
    desc: 'Trains and empowers Ground units (Infantry → Sniper → Heavy Soldier).',
    maxLevel: 10, requiresHq: 1,
    baseCost: { coins: 120 }, growth: 1.5,
    effect: (lvl) => `+${lvl * 3}% Ground unit power`,
    effectValue: (lvl) => 1 + lvl * 0.03,
  },
  factory: {
    id: 'factory', name: 'Factory', icon: 'assets/images/building_factory.png',
    desc: 'Assembles and empowers Armor units (Tank → Heavy Tank → Rocket Tank).',
    maxLevel: 10, requiresHq: 2,
    baseCost: { coins: 160, steel: 20 }, growth: 1.5,
    effect: (lvl) => `+${lvl * 3}% Armor unit power`,
    effectValue: (lvl) => 1 + lvl * 0.03,
  },
  airport: {
    id: 'airport', name: 'Airport', icon: 'assets/images/building_airport.png',
    desc: 'Launches and empowers Air units (Helicopter → Missile Heli → Drone).',
    maxLevel: 10, requiresHq: 4,
    baseCost: { coins: 220, steel: 30, fuel: 20 }, growth: 1.52,
    effect: (lvl) => `+${lvl * 3}% Air unit power`,
    effectValue: (lvl) => 1 + lvl * 0.03,
  },
  garage: {
    id: 'garage', name: 'Garage', icon: 'assets/images/building_garage.png',
    desc: 'Expands the Merge Yard so you can hold and merge more units at once.',
    maxLevel: 9, requiresHq: 2,
    baseCost: { coins: 150 }, growth: 1.55,
    effect: (lvl) => `+${lvl * CONFIG.MERGE_SLOTS_PER_GARAGE_LVL} merge yard slots`,
    effectValue: (lvl) => lvl * CONFIG.MERGE_SLOTS_PER_GARAGE_LVL,
  },
  lab: {
    id: 'lab', name: 'Research Lab', icon: 'assets/images/building_research_lab.png',
    desc: 'Researches technologies that boost the damage of your entire army.',
    maxLevel: 10, requiresHq: 3,
    baseCost: { coins: 200, diamonds: 0 }, growth: 1.58,
    effect: (lvl) => `+${lvl * 2}% all unit Damage`,
    effectValue: (lvl) => 1 + lvl * 0.02,
  },
  radar: {
    id: 'radar', name: 'Radar', icon: 'assets/images/building_radar.png',
    desc: 'Scouts enemy positions in advance, improving campaign rewards.',
    maxLevel: 10, requiresHq: 3,
    baseCost: { coins: 180, steel: 15 }, growth: 1.5,
    effect: (lvl) => `+${lvl * 2}% campaign rewards`,
    effectValue: (lvl) => 1 + lvl * 0.02,
  },
  power: {
    id: 'power', name: 'Power Plant', icon: 'assets/images/building_power_plant.png',
    desc: 'Generates Energy passively over time, even while you are away.',
    maxLevel: 10, requiresHq: 2,
    baseCost: { coins: 170, steel: 25 }, growth: 1.53,
    effect: (lvl) => `+${lvl * 6} Energy / hour`,
    effectValue: (lvl) => lvl * 6,
  },
  warehouse: {
    id: 'warehouse', name: 'Warehouse', icon: 'assets/images/building_warehouse.png',
    desc: 'Expands storage for offline earnings while you are away.',
    maxLevel: 10, requiresHq: 1,
    baseCost: { coins: 130 }, growth: 1.5,
    effect: (lvl) => `+${(lvl * CONFIG.OFFLINE_EARN_CAP_HOURS_PER_WAREHOUSE).toFixed(1)}h offline cap`,
    effectValue: (lvl) => lvl * CONFIG.OFFLINE_EARN_CAP_HOURS_PER_WAREHOUSE,
  },
  hospital: {
    id: 'hospital', name: 'Hospital', icon: 'assets/images/building_hospital.png',
    desc: 'Trains combat medics, raising the max HP of your entire army.',
    maxLevel: 10, requiresHq: 5,
    baseCost: { coins: 210, steel: 20 }, growth: 1.55,
    effect: (lvl) => `+${lvl * 2}% all unit HP`,
    effectValue: (lvl) => 1 + lvl * 0.02,
  },
};
const BUILDING_ORDER = ['hq', 'barracks', 'factory', 'airport', 'garage', 'lab', 'radar', 'power', 'warehouse', 'hospital'];

/* ---------------- Units ---------------- */
// Three merge lines. Within a line, merging two identical (type, level) units at
// the sub-level cap evolves them into level-1 of the next tier.
const UNIT_LINES = {
  ground: ['infantry', 'sniper', 'heavy_soldier'],
  armor: ['tank', 'heavy_tank', 'rocket_tank'],
  air: ['helicopter', 'missile_helicopter', 'drone'],
};
const LINE_BUILDING = { ground: 'barracks', armor: 'factory', air: 'airport' };
const LINE_LABEL = { ground: 'Ground', armor: 'Armor', air: 'Air' };

const UNIT_DEFS = {
  infantry:            { name: 'Infantry',            line: 'ground', tier: 1, icon: 'assets/images/unit_infantry.png',            hp: 80,  dmg: 8,  atkSpeed: 1.0, moveSpeed: 46, range: 34  },
  sniper:              { name: 'Sniper',               line: 'ground', tier: 2, icon: 'assets/images/unit_sniper.png',              hp: 60,  dmg: 22, atkSpeed: 0.6, moveSpeed: 42, range: 150 },
  heavy_soldier:       { name: 'Heavy Soldier',        line: 'ground', tier: 3, icon: 'assets/images/unit_heavy_soldier.png',       hp: 190, dmg: 14, atkSpeed: 0.9, moveSpeed: 34, range: 46  },
  tank:                { name: 'Tank',                 line: 'armor',  tier: 1, icon: 'assets/images/unit_tank.png',                hp: 220, dmg: 16, atkSpeed: 0.8, moveSpeed: 28, range: 60  },
  heavy_tank:          { name: 'Heavy Tank',           line: 'armor',  tier: 2, icon: 'assets/images/unit_heavy_tank.png',          hp: 420, dmg: 26, atkSpeed: 0.6, moveSpeed: 20, range: 70  },
  rocket_tank:         { name: 'Rocket Tank',          line: 'armor',  tier: 3, icon: 'assets/images/unit_rocket_tank.png',         hp: 260, dmg: 42, atkSpeed: 0.4, moveSpeed: 22, range: 165 },
  helicopter:          { name: 'Helicopter',           line: 'air',    tier: 1, icon: 'assets/images/unit_helicopter.png',          hp: 130, dmg: 15, atkSpeed: 1.2, moveSpeed: 60, range: 100 },
  missile_helicopter:  { name: 'Missile Helicopter',   line: 'air',    tier: 2, icon: 'assets/images/unit_missile_helicopter.png',  hp: 165, dmg: 27, atkSpeed: 0.8, moveSpeed: 54, range: 150 },
  drone:               { name: 'Drone',                line: 'air',    tier: 3, icon: 'assets/images/unit_drone.png',               hp: 90,  dmg: 12, atkSpeed: 1.7, moveSpeed: 72, range: 95  },
};

const LEVEL_GROWTH = 1.20; // stat multiplier per unit level
function statAtLevel(base, level) { return base * Math.pow(LEVEL_GROWTH, level - 1); }

function unitMaxSubLevel(type) {
  const def = UNIT_DEFS[type];
  return def.tier < 3 ? CONFIG.SUB_LEVEL_CAP : CONFIG.TOP_TIER_LEVEL_CAP;
}
function nextTierType(type) {
  const def = UNIT_DEFS[type];
  const line = UNIT_LINES[def.line];
  const idx = line.indexOf(type);
  return idx >= 0 && idx < line.length - 1 ? line[idx + 1] : null;
}
function recruitCost(line, ownedCountInLine) {
  return Math.round(10 + ownedCountInLine * 6);
}

/* ============================================================================
   2. LEVEL GENERATOR — 100 campaign levels across 10 chapters
   ============================================================================ */
const CHAPTER_NAMES = [
  'Coastal Outpost', 'Desert Ridge', 'Iron Valley', 'Frozen Frontier', 'Highland Pass',
  'Storm Harbor', 'Ashfall Basin', 'Crimson Delta', 'Shadow Peaks', 'Apex Command',
];
// Unit types available to the enemy roster, unlocked progressively by chapter.
const CHAPTER_UNIT_POOL = [
  ['infantry', 'tank'],
  ['infantry', 'tank', 'sniper', 'helicopter'],
  ['infantry', 'tank', 'sniper', 'helicopter', 'heavy_tank'],
  ['sniper', 'tank', 'heavy_tank', 'helicopter', 'missile_helicopter'],
  ['sniper', 'heavy_tank', 'missile_helicopter', 'heavy_soldier', 'infantry'],
  ['heavy_soldier', 'heavy_tank', 'rocket_tank', 'missile_helicopter'],
  ['heavy_soldier', 'rocket_tank', 'missile_helicopter', 'drone', 'sniper'],
  ['heavy_soldier', 'rocket_tank', 'drone', 'heavy_tank', 'missile_helicopter'],
  ['heavy_soldier', 'rocket_tank', 'drone', 'missile_helicopter', 'heavy_tank'],
  ['heavy_soldier', 'rocket_tank', 'drone'],
];

function generateLevel(n) {
  const chapter = Math.ceil(n / 10);           // 1..10
  const posInChapter = ((n - 1) % 10) + 1;      // 1..10
  const isBoss = posInChapter === 10;
  const pool = CHAPTER_UNIT_POOL[chapter - 1];

  const enemyCount = Math.min(16, 3 + Math.floor(n / 6)) + (isBoss ? 2 : 0);
  const enemyLevel = Math.min(24, 1 + Math.floor(n / 7));

  const enemies = [];
  for (let i = 0; i < enemyCount; i++) {
    const type = pool[i % pool.length];
    enemies.push({ type, level: enemyLevel });
  }
  if (isBoss) {
    const bossType = pool[pool.length - 1];
    enemies.push({ type: bossType, level: enemyLevel + 6, isBoss: true });
  }

  const rewards = {
    coins: Math.round(90 + n * 34 * (isBoss ? 1.6 : 1)),
    energy: Math.round(14 + n * 3.4 * (isBoss ? 1.5 : 1)),
    steel: Math.round(8 + n * 2.6 * (isBoss ? 1.5 : 1)),
    fuel: Math.round(6 + n * 1.8 * (isBoss ? 1.4 : 1)),
    diamonds: isBoss ? 8 + chapter * 2 : (n % 5 === 0 ? 2 : 0),
  };

  return {
    id: n, chapter, posInChapter, isBoss,
    name: isBoss ? `${CHAPTER_NAMES[chapter - 1]} — Boss` : `Level ${n}`,
    enemies, rewards,
    fuelCost: Math.round(5 + n * 0.35),
  };
}
const LEVELS = Array.from({ length: 100 }, (_, i) => generateLevel(i + 1));
const CHAPTERS = CHAPTER_NAMES.map((name, i) => ({
  index: i + 1, name,
  from: i * 10 + 1, to: i * 10 + 10,
}));

/* ============================================================================
   3. STATE & SAVE SYSTEM
   ============================================================================ */
function defaultState() {
  const buildings = {};
  BUILDING_ORDER.forEach((id) => { buildings[id] = { level: id === 'hq' ? 1 : id === 'barracks' ? 1 : 0 }; });
  return {
    version: 1,
    resources: { coins: 300, energy: 60, steel: 80, fuel: 40, diamonds: 25 },
    buildings,
    // New commanders start with a small standing squad — matches "player starts
    // with a small military base" rather than an empty Merge Yard.
    units: [
      { id: 1, type: 'infantry', level: 1, slot: 0 },
      { id: 2, type: 'infantry', level: 1, slot: 1 },
      { id: 3, type: 'infantry', level: 1, slot: 2 },
    ],
    nextUnitId: 4,
    maxLevelUnlocked: 1,
    levelStars: {},       // {levelId: 1|2|3}
    achievementsClaimed: {},
    achievementProgressCache: {},
    dailyStreak: 0,
    lastDailyClaim: 0,     // epoch ms of last claimed day
    lastSeenTime: Date.now(),
    settings: { music: true, sfx: true, vibrate: true },
    stats: { battlesWon: 0, battlesLost: 0, totalMerges: 0, buildingsUpgraded: 0, unitsRecruited: 0 },
  };
}

let state = null;

function loadState() {
  try {
    const raw = localStorage.getItem(CONFIG.SAVE_KEY);
    if (!raw) return defaultState();
    const parsed = JSON.parse(raw);
    const fresh = defaultState();
    // shallow-merge to tolerate older saves missing newer fields
    const merged = Object.assign({}, fresh, parsed);
    merged.resources = Object.assign({}, fresh.resources, parsed.resources || {});
    merged.buildings = Object.assign({}, fresh.buildings, parsed.buildings || {});
    merged.settings = Object.assign({}, fresh.settings, parsed.settings || {});
    merged.stats = Object.assign({}, fresh.stats, parsed.stats || {});
    merged.units = Array.isArray(parsed.units) ? parsed.units : [];
    merged.levelStars = parsed.levelStars || {};
    merged.achievementsClaimed = parsed.achievementsClaimed || {};
    return merged;
  } catch (e) {
    console.warn('Save data was unreadable, starting fresh.', e);
    return defaultState();
  }
}

function saveState() {
  try {
    state.lastSeenTime = Date.now();
    localStorage.setItem(CONFIG.SAVE_KEY, JSON.stringify(state));
    return true;
  } catch (e) {
    console.warn('Could not save game (storage full or unavailable).', e);
    return false;
  }
}

function resetProgress() {
  localStorage.removeItem(CONFIG.SAVE_KEY);
  state = defaultState();
  saveState();
}

/* ---------------- Resource helpers ---------------- */
function getGlobalMultiplier() {
  return BUILDING_DEFS.hq.effectValue(state.buildings.hq.level);
}
function addResource(type, amount, opts) {
  opts = opts || {};
  let amt = amount;
  if (!opts.skipGlobalBonus && amt > 0) amt *= getGlobalMultiplier();
  amt = Math.round(amt);
  state.resources[type] = Math.max(0, (state.resources[type] || 0) + amt);
  return amt;
}
function canAfford(cost) {
  return Object.entries(cost).every(([res, amt]) => (state.resources[res] || 0) >= amt);
}
function spendResources(cost) {
  if (!canAfford(cost)) return false;
  Object.entries(cost).forEach(([res, amt]) => { state.resources[res] -= amt; });
  return true;
}

/* ---------------- Offline earnings ---------------- */
function computeOfflineEarnings() {
  const now = Date.now();
  const last = state.lastSeenTime || now;
  const elapsedHours = Math.max(0, (now - last) / 3600000);
  if (elapsedHours < 0.02) return null; // ignore trivial gaps (<~1 min)

  const capHours = CONFIG.OFFLINE_EARN_CAP_HOURS_BASE +
    BUILDING_DEFS.warehouse.effectValue(state.buildings.warehouse.level);
  const cappedHours = Math.min(elapsedHours, capHours);
  const energyPerHour = BUILDING_DEFS.power.effectValue(state.buildings.power.level);
  const coinsPerHour = 20 + state.buildings.hq.level * 6;

  const earnedEnergy = Math.round(energyPerHour * cappedHours);
  const earnedCoins = Math.round(coinsPerHour * cappedHours);
  if (earnedEnergy <= 0 && earnedCoins <= 0) return null;

  addResource('energy', earnedEnergy, { skipGlobalBonus: true });
  addResource('coins', earnedCoins, { skipGlobalBonus: true });
  return {
    hours: cappedHours, capped: elapsedHours > capHours,
    coins: earnedCoins, energy: earnedEnergy,
  };
}

/* ============================================================================
   4. AUDIO ENGINE — Web Audio API for SFX, <audio> element for music.
   All sound files are original synthesized assets (see assets/sounds, assets/music).
   ============================================================================ */
const SFX_FILES = {
  click: 'assets/sounds/click.mp3',
  back: 'assets/sounds/click_back.mp3',
  merge: 'assets/sounds/merge.mp3',
  upgrade: 'assets/sounds/upgrade.mp3',
  build: 'assets/sounds/build.mp3',
  coin: 'assets/sounds/coin.mp3',
  explosion: 'assets/sounds/explosion.mp3',
  gunshot: 'assets/sounds/gunshot.mp3',
  rocket: 'assets/sounds/rocket.mp3',
  victory: 'assets/sounds/victory.mp3',
  defeat: 'assets/sounds/defeat.mp3',
  notification: 'assets/sounds/notification.mp3',
  levelup: 'assets/sounds/level_up.mp3',
  chest: 'assets/sounds/chest_open.mp3',
  tap: 'assets/sounds/ui_tap.mp3',
};
const MUSIC_FILES = {
  menu: 'assets/music/theme_menu.mp3',
  battle: 'assets/music/theme_battle.mp3',
  victory: 'assets/music/theme_victory.mp3',
};

const AudioEngine = {
  ctx: null,
  buffers: {},
  unlocked: false,
  currentMusicKey: null,
  musicEl: null,

  init() {
    this.musicEl = document.getElementById('audio-music');
    // Decode SFX lazily in the background; failures are non-fatal.
    this._preloadPromise = null;
  },

  _ensureContext() {
    if (this.ctx) return this.ctx;
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return null;
    this.ctx = new AC();
    return this.ctx;
  },

  unlock() {
    if (this.unlocked) return;
    this.unlocked = true;
    const ctx = this._ensureContext();
    if (ctx && ctx.state === 'suspended') ctx.resume().catch(() => {});
    this._preloadAll();
    // Kick off music if it was requested before unlock happened.
    if (this._pendingMusic) { this.playMusic(this._pendingMusic); this._pendingMusic = null; }
  },

  async _preloadAll() {
    const ctx = this._ensureContext();
    if (!ctx) return;
    const entries = Object.entries(SFX_FILES);
    for (const [name, url] of entries) {
      if (this.buffers[name]) continue;
      try {
        const res = await fetch(url);
        const arr = await res.arrayBuffer();
        const buf = await ctx.decodeAudioData(arr);
        this.buffers[name] = buf;
      } catch (e) {
        // Non-fatal: game continues silently for that one effect.
        console.warn('SFX preload failed for', name, e);
      }
    }
  },

  playSfx(name) {
    if (!state || !state.settings.sfx) return;
    const ctx = this._ensureContext();
    if (!ctx || ctx.state === 'suspended') { ctx && ctx.resume().catch(() => {}); }
    const buf = this.buffers[name];
    if (!ctx || !buf) return; // silently skip if not decoded yet (e.g. very first tap)
    try {
      const src = ctx.createBufferSource();
      src.buffer = buf;
      const gain = ctx.createGain();
      gain.gain.value = 0.9;
      src.connect(gain).connect(ctx.destination);
      src.start(0);
    } catch (e) { /* ignore playback errors */ }
  },

  playMusic(key) {
    if (!this.musicEl) return;
    if (!this.unlocked) { this._pendingMusic = key; return; }
    if (this.currentMusicKey === key && !this.musicEl.paused) return;
    this.currentMusicKey = key;
    this.musicEl.src = MUSIC_FILES[key];
    this.musicEl.loop = key !== 'victory';
    this.musicEl.volume = 0.55;
    if (state.settings.music) {
      this.musicEl.play().catch(() => {});
    }
  },

  setMusicEnabled(on) {
    state.settings.music = on;
    if (!this.musicEl) return;
    if (on) { this.musicEl.play().catch(() => {}); }
    else { this.musicEl.pause(); }
  },

  setSfxEnabled(on) { state.settings.sfx = on; },

  vibrate(pattern) {
    if (state && state.settings.vibrate && navigator.vibrate) {
      try { navigator.vibrate(pattern || 15); } catch (e) {}
    }
  },
};

/* ============================================================================
   5. UI RENDERING HELPERS
   ============================================================================ */
function formatNumber(n) {
  n = Math.floor(n);
  if (n < 1000) return String(n);
  if (n < 1e6) return (n / 1e3).toFixed(n < 10000 ? 1 : 0) + 'K';
  if (n < 1e9) return (n / 1e6).toFixed(n < 1e7 ? 1 : 0) + 'M';
  return (n / 1e9).toFixed(1) + 'B';
}

let currentLine = 'ground';
let currentView = 'base';

function renderResources(pulseTypes) {
  RESOURCE_ORDER.forEach((res) => {
    const el = document.getElementById('res-' + res);
    if (el) el.textContent = formatNumber(state.resources[res] || 0);
  });
  if (pulseTypes && pulseTypes.length) {
    pulseTypes.forEach((res) => {
      const pill = document.querySelector(`.resource-pill[data-resource="${res}"]`);
      if (!pill) return;
      pill.classList.remove('pulse');
      // Force reflow so the animation can restart if it's already mid-way.
      void pill.offsetWidth;
      pill.classList.add('pulse');
    });
  }
  const hqLabel = document.getElementById('hq-level-label');
  if (hqLabel) hqLabel.textContent = 'HQ ' + state.buildings.hq.level;
}

function showToast(msg, ms) {
  const toast = document.getElementById('toast');
  toast.textContent = msg;
  toast.classList.remove('hidden');
  clearTimeout(showToast._t);
  showToast._t = setTimeout(() => toast.classList.add('hidden'), ms || 1900);
}

function openModal(id) {
  document.getElementById('modal-overlay').classList.remove('hidden');
  document.querySelectorAll('.modal').forEach((m) => m.classList.remove('active'));
  document.getElementById(id).classList.add('active');
}
function closeModal() {
  document.getElementById('modal-overlay').classList.add('hidden');
  document.querySelectorAll('.modal').forEach((m) => m.classList.remove('active'));
}
function confirmDialog(message, onConfirm) {
  document.getElementById('confirm-message').textContent = message;
  openModal('modal-confirm');
  const okBtn = document.getElementById('confirm-ok');
  const cancelBtn = document.getElementById('confirm-cancel');
  const cleanup = () => { okBtn.onclick = null; cancelBtn.onclick = null; };
  okBtn.onclick = () => { cleanup(); closeModal(); onConfirm(); };
  cancelBtn.onclick = () => { cleanup(); closeModal(); };
}

/* ---------------- Base view: building grid ---------------- */
function maxAllowedLevel(buildingId) {
  const def = BUILDING_DEFS[buildingId];
  if (buildingId === 'hq') return def.maxLevel;
  return Math.min(def.maxLevel, state.buildings.hq.level);
}
function buildingCostAt(buildingId, targetLevel) {
  const def = BUILDING_DEFS[buildingId];
  const cost = {};
  Object.entries(def.baseCost).forEach(([res, base]) => {
    if (base <= 0) return;
    cost[res] = Math.round(base * Math.pow(def.growth, targetLevel - 1));
  });
  return cost;
}

function renderBaseGrid() {
  const grid = document.getElementById('base-grid');
  grid.innerHTML = '';
  BUILDING_ORDER.forEach((id) => {
    const def = BUILDING_DEFS[id];
    const b = state.buildings[id];
    const unlocked = state.buildings.hq.level >= def.requiresHq;
    const tile = document.createElement('button');
    tile.className = 'building-tile' + (id === 'hq' ? ' hq-tile' : '') + (!unlocked ? ' locked' : '');
    if (b.level >= def.maxLevel) tile.classList.add('maxed');
    tile.dataset.building = id;

    if (!unlocked) {
      tile.innerHTML = `
        <img class="building-lock-icon" src="assets/icons/ui_lock.png" alt="">
        <img class="building-icon" src="${def.icon}" alt="">
        <span class="building-name">${def.name}</span>
        <span class="building-name" style="color:var(--steel-400);font-size:9px;">HQ ${def.requiresHq} req.</span>`;
    } else {
      const canUp = b.level < maxAllowedLevel(id) && canAfford(buildingCostAt(id, b.level + 1));
      tile.innerHTML = `
        <span class="building-level-badge">Lv ${b.level}</span>
        <img class="building-icon" src="${def.icon}" alt="">
        <span class="building-name">${def.name}</span>
        ${canUp ? '<span class="building-upgrade-flag">UPGRADE</span>' : ''}`;
    }
    tile.addEventListener('click', () => unlocked ? openBuildingModal(id) : showToast(`Requires HQ Level ${def.requiresHq}`));
    grid.appendChild(tile);
  });
}

function openBuildingModal(id) {
  AudioEngine.playSfx('click');
  const def = BUILDING_DEFS[id];
  const b = state.buildings[id];
  document.getElementById('bm-icon').src = def.icon;
  document.getElementById('bm-name').textContent = def.name;
  document.getElementById('bm-desc').textContent = def.desc;
  document.getElementById('bm-level').textContent = b.level + ' / ' + def.maxLevel;
  document.getElementById('bm-effect').textContent = b.level > 0 ? def.effect(b.level) : 'None yet';

  const maxAllowed = maxAllowedLevel(id);
  const upgradeBtn = document.getElementById('bm-upgrade-btn');
  const costRow = document.getElementById('bm-cost-row');
  const nextEffectEl = document.getElementById('bm-next-effect');

  if (b.level >= def.maxLevel) {
    nextEffectEl.textContent = 'Max level reached';
    costRow.innerHTML = '';
    upgradeBtn.textContent = 'MAX LEVEL';
    upgradeBtn.disabled = true;
  } else if (b.level >= maxAllowed) {
    nextEffectEl.textContent = def.effect(b.level + 1);
    costRow.innerHTML = `<span style="color:var(--text-dim);font-size:12px;">Requires Headquarters Level ${state.buildings.hq.level + 1}+</span>`;
    upgradeBtn.textContent = 'UPGRADE HQ FIRST';
    upgradeBtn.disabled = true;
  } else {
    nextEffectEl.textContent = def.effect(b.level + 1);
    const cost = buildingCostAt(id, b.level + 1);
    costRow.innerHTML = Object.entries(cost).map(([res, amt]) => `
      <span class="cost-chip ${(state.resources[res]||0) < amt ? 'insufficient' : ''}">
        <img src="${RESOURCE_DEFS[res].icon}" alt="">${formatNumber(amt)}
      </span>`).join('');
    upgradeBtn.textContent = 'UPGRADE';
    upgradeBtn.disabled = !canAfford(cost);
  }
  upgradeBtn.onclick = () => { upgradeBuilding(id); openBuildingModal(id); };
  openModal('modal-building');
}

function upgradeBuilding(id) {
  const def = BUILDING_DEFS[id];
  const b = state.buildings[id];
  if (b.level >= def.maxLevel || b.level >= maxAllowedLevel(id)) return;
  const cost = buildingCostAt(id, b.level + 1);
  if (!spendResources(cost)) { showToast('Not enough resources'); return; }
  b.level += 1;
  state.stats.buildingsUpgraded++;
  AudioEngine.playSfx('build');
  AudioEngine.vibrate(20);
  showToast(`${def.name} upgraded to Level ${b.level}!`);
  renderResources(Object.keys(cost));
  renderBaseGrid();
  checkAchievements();
  saveState();
}

/* ============================================================================
   6. MERGE / UNIT SYSTEM
   ============================================================================ */
function totalMergeSlots() {
  const lvl = state.buildings.garage.level;
  return Math.min(CONFIG.MERGE_MAX_SLOTS, CONFIG.MERGE_BASE_SLOTS + lvl * CONFIG.MERGE_SLOTS_PER_GARAGE_LVL);
}
function unitsInLine(line) { return state.units.filter((u) => UNIT_DEFS[u.type].line === line); }
function firstEmptySlot(line) {
  const used = new Set(unitsInLine(line).map((u) => u.slot));
  const total = totalMergeSlots();
  for (let i = 0; i < total; i++) if (!used.has(i)) return i;
  return -1;
}
function unitStatsFor(type, level) {
  const base = UNIT_DEFS[type];
  const buildingId = LINE_BUILDING[base.line];
  const lineMult = BUILDING_DEFS[buildingId].effectValue(state.buildings[buildingId].level);
  const labMult = BUILDING_DEFS.lab.effectValue(state.buildings.lab.level);
  const hospMult = BUILDING_DEFS.hospital.effectValue(state.buildings.hospital.level);
  return {
    hp: Math.round(statAtLevel(base.hp, level) * lineMult * hospMult),
    dmg: Math.round(statAtLevel(base.dmg, level) * lineMult * labMult),
    atkSpeed: base.atkSpeed,
    moveSpeed: base.moveSpeed,
    range: base.range,
  };
}

function recruitUnit() {
  const total = totalMergeSlots();
  const slot = firstEmptySlot(currentLine);
  if (slot === -1) { showToast('Merge yard is full — merge units or upgrade the Garage'); return; }
  const buildingId = LINE_BUILDING[currentLine];
  if (state.buildings[buildingId].level <= 0) {
    showToast(`Unlock the ${BUILDING_DEFS[buildingId].name} first (requires HQ ${BUILDING_DEFS[buildingId].requiresHq})`);
    return;
  }
  const cost = recruitCost(currentLine, unitsInLine(currentLine).length);
  if ((state.resources.energy || 0) < cost) { showToast(`Need ${cost} Energy to recruit`); return; }
  state.resources.energy -= cost;
  const baseType = UNIT_LINES[currentLine][0];
  state.units.push({ id: state.nextUnitId++, type: baseType, level: 1, slot });
  state.stats.unitsRecruited++;
  AudioEngine.playSfx('coin');
  AudioEngine.vibrate(12);
  renderResources(['energy']);
  renderMergeYard();
  checkAchievements();
  saveState();
}

function performMerge(unitA, unitB, targetSlot) {
  const type = unitA.type;
  let newLevel = unitA.level + 1;
  let newType = type;
  const cap = unitMaxSubLevel(type);
  if (newLevel > cap) {
    const next = nextTierType(type);
    if (next) { newType = next; newLevel = 1; }
    else { newLevel = cap; }
  }
  state.units = state.units.filter((u) => u.id !== unitA.id && u.id !== unitB.id);
  const merged = { id: state.nextUnitId++, type: newType, level: newLevel, slot: targetSlot };
  state.units.push(merged);
  state.stats.totalMerges++;
  AudioEngine.playSfx('merge');
  AudioEngine.vibrate([10, 30, 10]);
  checkAchievements();
  saveState();
  return merged;
}

function sellUnit(unitId) {
  const u = state.units.find((x) => x.id === unitId);
  if (!u) return;
  const value = Math.round(10 * Math.pow(1.5, u.level - 1));
  state.units = state.units.filter((x) => x.id !== unitId);
  addResource('coins', value, { skipGlobalBonus: true });
  AudioEngine.playSfx('coin');
  renderResources(['coins']);
  renderMergeYard();
  closeModal();
  saveState();
}

function tierClassFor(type) {
  const t = UNIT_DEFS[type].tier;
  return t >= 3 ? 'tier-3' : t === 2 ? 'tier-2' : '';
}

function renderMergeYard() {
  const grid = document.getElementById('merge-yard-grid');
  grid.innerHTML = '';
  const total = totalMergeSlots();
  const bySlot = {};
  unitsInLine(currentLine).forEach((u) => { bySlot[u.slot] = u; });

  for (let i = 0; i < total; i++) {
    const cell = document.createElement('div');
    cell.className = 'merge-cell';
    cell.dataset.slot = String(i);
    const u = bySlot[i];
    if (u) {
      const chip = document.createElement('div');
      chip.className = 'unit-chip ' + tierClassFor(u.type);
      chip.dataset.unitId = String(u.id);
      chip.innerHTML = `<img src="${UNIT_DEFS[u.type].icon}" alt="${UNIT_DEFS[u.type].name}" draggable="false">
                         <span class="unit-level-badge">${u.level}</span>`;
      chip.addEventListener('click', (e) => {
        if (chip._suppressClick) { chip._suppressClick = false; return; }
        openUnitModal(u.id);
      });
      attachDragHandlers(chip, cell);
      cell.appendChild(chip);
    }
    grid.appendChild(cell);
  }
}

function openUnitModal(unitId) {
  const u = state.units.find((x) => x.id === unitId);
  if (!u) return;
  AudioEngine.playSfx('click');
  const def = UNIT_DEFS[u.type];
  const stats = unitStatsFor(u.type, u.level);
  document.getElementById('um-icon').src = def.icon;
  document.getElementById('um-name').textContent = def.name + ' (' + LINE_LABEL[def.line] + ')';
  document.getElementById('um-hp').textContent = stats.hp;
  document.getElementById('um-dmg').textContent = stats.dmg;
  document.getElementById('um-aspd').textContent = stats.atkSpeed.toFixed(1) + '/s';
  document.getElementById('um-mspd').textContent = stats.moveSpeed;
  document.getElementById('um-range').textContent = stats.range;
  document.getElementById('um-level').textContent = u.level + (UNIT_DEFS[u.type].tier < 3 ? ' / ' + CONFIG.SUB_LEVEL_CAP : '');
  document.getElementById('um-sell-btn').onclick = () => sellUnit(u.id);
  openModal('modal-unit');
}

/* ============================================================================
   7. DRAG & DROP INPUT (Pointer Events — unified mouse + touch)
   ============================================================================ */
let dragState = null;

function elementFromPointIgnoring(x, y, ignoreEl) {
  if (ignoreEl) ignoreEl.style.display = 'none';
  const el = document.elementFromPoint(x, y);
  if (ignoreEl) ignoreEl.style.display = '';
  return el;
}

function attachDragHandlers(chip, cell) {
  chip.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    dragState = {
      unitId: Number(chip.dataset.unitId), fromSlot: Number(cell.dataset.slot),
      chip, startX: e.clientX, startY: e.clientY, moved: false, ghost: null,
    };
    try { chip.setPointerCapture(e.pointerId); } catch (err) {}
  });

  chip.addEventListener('pointermove', (e) => {
    if (!dragState) return;
    const dx = e.clientX - dragState.startX, dy = e.clientY - dragState.startY;
    if (!dragState.moved && Math.hypot(dx, dy) > 6) {
      dragState.moved = true;
      chip.classList.add('dragging');
      const ghost = document.createElement('div');
      ghost.className = 'drag-ghost';
      const imgEl = chip.querySelector('img');
      if (imgEl) ghost.innerHTML = imgEl.outerHTML;
      document.body.appendChild(ghost);
      dragState.ghost = ghost;
    }
    if (dragState.moved && dragState.ghost) {
      dragState.ghost.style.left = e.clientX + 'px';
      dragState.ghost.style.top = e.clientY + 'px';
      document.querySelectorAll('.merge-cell.drag-over, .merge-cell.can-merge').forEach((c) => c.classList.remove('drag-over', 'can-merge'));
      const target = elementFromPointIgnoring(e.clientX, e.clientY, dragState.ghost);
      const targetCell = target && target.closest && target.closest('.merge-cell');
      if (targetCell) {
        targetCell.classList.add('drag-over');
        const targetChipEl = targetCell.querySelector('.unit-chip');
        if (targetChipEl) {
          const targetUnit = state.units.find((u) => u.id === Number(targetChipEl.dataset.unitId));
          const dragUnit = state.units.find((u) => u.id === dragState.unitId);
          if (targetUnit && dragUnit && targetUnit.id !== dragUnit.id && targetUnit.type === dragUnit.type && targetUnit.level === dragUnit.level) {
            targetCell.classList.add('can-merge');
          }
        }
      }
    }
  });

  const endDrag = (e) => {
    if (!dragState) return;
    chip.classList.remove('dragging');
    if (dragState.ghost) dragState.ghost.remove();
    document.querySelectorAll('.merge-cell.drag-over, .merge-cell.can-merge').forEach((c) => c.classList.remove('drag-over', 'can-merge'));
    if (dragState.moved) {
      chip._suppressClick = true;
      const target = elementFromPointIgnoring(e.clientX, e.clientY, dragState.ghost);
      const targetCell = target && target.closest && target.closest('.merge-cell');
      handleDrop(dragState.unitId, dragState.fromSlot, targetCell);
    }
    dragState = null;
  };
  chip.addEventListener('pointerup', endDrag);
  chip.addEventListener('pointercancel', endDrag);
}

function handleDrop(draggedUnitId, fromSlot, targetCell) {
  const dragUnit = state.units.find((u) => u.id === draggedUnitId);
  if (!dragUnit || !targetCell) { renderMergeYard(); return; }
  const toSlot = Number(targetCell.dataset.slot);
  if (toSlot === fromSlot) { renderMergeYard(); return; }

  const targetChipEl = targetCell.querySelector('.unit-chip');
  if (!targetChipEl) {
    dragUnit.slot = toSlot;
    renderMergeYard();
    saveState();
    return;
  }
  const targetUnit = state.units.find((u) => u.id === Number(targetChipEl.dataset.unitId));
  if (!targetUnit) { renderMergeYard(); return; }

  if (targetUnit.type === dragUnit.type && targetUnit.level === dragUnit.level) {
    const merged = performMerge(dragUnit, targetUnit, toSlot);
    renderMergeYard();
    playMergeBurstAt(toSlot);
    showToast(`Merged into ${UNIT_DEFS[merged.type].name} Lv${merged.level}!`);
  } else {
    dragUnit.slot = toSlot;
    targetUnit.slot = fromSlot;
    renderMergeYard();
    saveState();
  }
}

function playMergeBurstAt(slot) {
  const cell = document.querySelector(`.merge-cell[data-slot="${slot}"]`);
  if (!cell) return;
  const burst = document.createElement('div');
  burst.className = 'merge-burst play';
  cell.appendChild(burst);
  setTimeout(() => burst.remove(), 550);
}

/* ============================================================================
   8. BATTLE ENGINE
   ============================================================================ */
const UnitIconCache = { images: {}, tinted: {} };

function makeTintedCanvas(img, color) {
  const size = 96;
  const c = document.createElement('canvas');
  c.width = size; c.height = size;
  const cx = c.getContext('2d');
  cx.beginPath(); cx.arc(size / 2, size / 2, size * 0.46, 0, Math.PI * 2);
  cx.fillStyle = color + '2A'; cx.fill();
  cx.lineWidth = 4; cx.strokeStyle = color; cx.stroke();
  const iconSize = size * 0.78;
  cx.drawImage(img, (size - iconSize) / 2, (size - iconSize) / 2, iconSize, iconSize);
  return c;
}

function preloadUnitIcons(onDone) {
  const types = Object.keys(UNIT_DEFS);
  let remaining = types.length;
  const finish = () => { remaining--; if (remaining <= 0 && onDone) onDone(); };
  types.forEach((type) => {
    const img = new Image();
    img.onload = () => {
      UnitIconCache.images[type] = img;
      UnitIconCache.tinted[type] = { player: makeTintedCanvas(img, '#3fd9f5'), enemy: makeTintedCanvas(img, '#e64444') };
      finish();
    };
    img.onerror = finish;
    img.src = UNIT_DEFS[type].icon;
  });
}

const Battle = {
  active: false, entities: [], particles: [], canvas: null, ctx: null,
  dpr: 1, width: 0, height: 0, speed: 1, animId: null, lastTime: 0,
  playerTotalHp: 1, enemyTotalHp: 1, levelIndex: 1, started: false, nextEntityId: 1,
  floaterLayer: null,

  setup(levelIndex) {
    this.levelIndex = levelIndex;
    this.canvas = document.getElementById('battle-canvas');
    this.ctx = this.canvas.getContext('2d');
    this.entities = []; this.particles = []; this.started = false; this.speed = 1; this.paused = false;
    document.getElementById('btn-battle-speed').textContent = '1x';
    document.getElementById('btn-deploy-wave').textContent = 'DEPLOY UNITS';
    document.getElementById('btn-deploy-wave').disabled = false;
    this._resize();
    if (!this._resizeBound) {
      window.addEventListener('resize', () => this._resize());
      this._resizeBound = true;
    }
    document.getElementById('battle-level-label').textContent = LEVELS[levelIndex - 1].name;
    this._updateHpBars();
    this._render(); // idle first frame
  },

  _resize() {
    if (!this.canvas) return;
    const rect = this.canvas.getBoundingClientRect();
    this.dpr = Math.min(2, window.devicePixelRatio || 1);
    this.width = rect.width; this.height = rect.height;
    this.canvas.width = Math.round(rect.width * this.dpr);
    this.canvas.height = Math.round(rect.height * this.dpr);
    this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
  },

  deploy() {
    const level = LEVELS[this.levelIndex - 1];
    if (!this.started) {
      if (state.units.length === 0) { showToast('Recruit and merge units in the Base first!'); return; }
      if ((state.resources.fuel || 0) < level.fuelCost) { showToast(`Need ${level.fuelCost} Fuel to deploy`); return; }
      state.resources.fuel -= level.fuelCost;
      renderResources(['fuel']);
      this.started = true;
      document.getElementById('btn-deploy-wave').textContent = 'REINFORCE (3 fuel)';
      this._spawnPlayerArmy();
      this._spawnEnemyArmy(level);
      this._updateHpBars(true);
      AudioEngine.playMusic('battle');
      this.lastTime = performance.now();
      this._loop(this.lastTime);
      saveState();
    } else {
      if ((state.resources.fuel || 0) < 3) { showToast('Not enough Fuel to reinforce'); return; }
      if (state.units.length === 0) return;
      state.resources.fuel -= 3;
      renderResources(['fuel']);
      const pick = state.units[Math.floor(Math.random() * state.units.length)];
      this._spawnEntity('player', pick.type, pick.level, 30 + Math.random() * 30, Math.random() * this.height);
      showToast('Reinforcement deployed!');
      saveState();
    }
  },

  toggleSpeed() {
    this.speed = this.speed === 1 ? 2 : 1;
    document.getElementById('btn-battle-speed').textContent = this.speed + 'x';
  },

  _spawnPlayerArmy() {
    const n = state.units.length;
    state.units.forEach((u, i) => {
      const y = ((i + 0.5) / n) * this.height;
      const x = 26 + (i % 2) * 26 + Math.random() * 10;
      this._spawnEntity('player', u.type, u.level, x, y);
    });
  },
  _spawnEnemyArmy(level) {
    const n = level.enemies.length;
    level.enemies.forEach((e, i) => {
      const y = ((i + 0.5) / n) * this.height;
      const x = this.width - 26 - (i % 2) * 26 - Math.random() * 10;
      this._spawnEntity('enemy', e.type, e.level, x, y, !!e.isBoss);
    });
  },
  _spawnEntity(team, type, level, x, y, isBoss) {
    const stats = unitStatsFor(type, level);
    const scale = isBoss ? 1.7 : 1;
    this.entities.push({
      id: this.nextEntityId++, team, type, level, x, y,
      hp: stats.hp * (isBoss ? 3.4 : 1), maxHp: stats.hp * (isBoss ? 3.4 : 1),
      dmg: stats.dmg * (isBoss ? 2.2 : 1), atkSpeed: stats.atkSpeed,
      moveSpeed: stats.moveSpeed * (isBoss ? 0.8 : 1), range: stats.range,
      radius: 16 * scale, isBoss: !!isBoss, cooldown: Math.random() * 0.3, alive: true, targetId: null,
    });
  },

  _findNearestEnemy(e) {
    let best = null, bestD = Infinity;
    for (const other of this.entities) {
      if (!other.alive || other.team === e.team) continue;
      const d = Math.hypot(other.x - e.x, other.y - e.y);
      if (d < bestD) { bestD = d; best = other; }
    }
    return { target: best, dist: bestD };
  },

  _loop(ts) {
    if (!this.started) return;
    this.animId = requestAnimationFrame((t) => this._loop(t));
    let dt = (ts - this.lastTime) / 1000;
    this.lastTime = ts;
    dt = Math.min(dt, 0.05) * this.speed;
    this._update(dt);
    this._render();
  },

  _update(dt) {
    let playerAlive = 0, enemyAlive = 0;
    for (const e of this.entities) {
      if (!e.alive) continue;
      if (e.team === 'player') playerAlive++; else enemyAlive++;
    }

    for (const e of this.entities) {
      if (!e.alive) continue;
      const { target, dist } = this._findNearestEnemy(e);
      if (!target) continue;
      if (dist > e.range + e.radius) {
        const dx = (target.x - e.x) / dist, dy = (target.y - e.y) / dist;
        e.x += dx * e.moveSpeed * dt;
        e.y += dy * e.moveSpeed * dt;
        // light separation from teammates to avoid total overlap
        for (const other of this.entities) {
          if (other === e || !other.alive || other.team !== e.team) continue;
          const od = Math.hypot(other.x - e.x, other.y - e.y);
          if (od > 0 && od < e.radius * 1.3) {
            e.x += (e.x - other.x) / od * 8 * dt;
            e.y += (e.y - other.y) / od * 8 * dt;
          }
        }
        e.y = Math.max(20, Math.min(this.height - 20, e.y));
      } else {
        e.cooldown -= dt;
        if (e.cooldown <= 0) {
          e.cooldown = 1 / e.atkSpeed;
          this._dealDamage(e, target);
        }
      }
    }
    this.entities = this.entities.filter((e) => e.alive || e._deathTimer > 0);

    playerAlive = this.entities.some((e) => e.team === 'player' && e.alive);
    enemyAlive = this.entities.some((e) => e.team === 'enemy' && e.alive);
    if (this.started && !enemyAlive) { this._end(true); }
    else if (this.started && !playerAlive) { this._end(false); }

    this._updateHpBars();
    this._updateParticles(dt);
  },

  _dealDamage(attacker, target) {
    const isCrit = Math.random() < 0.12;
    const dmg = Math.round(attacker.dmg * (isCrit ? 1.8 : 1));
    target.hp -= dmg;
    this._spawnFloater(target.x, target.y - target.radius, dmg, isCrit);
    this._spawnHitParticle(target.x, target.y, attacker.team === 'player' ? '#3fd9f5' : '#ff6b35');
    if (Math.random() < 0.35) AudioEngine.playSfx(attacker.type.includes('tank') ? 'gunshot' : (attacker.type.includes('heli') || attacker.type === 'drone' ? 'rocket' : 'gunshot'));
    if (target.hp <= 0 && target.alive) {
      target.alive = false;
      target._deathTimer = 0.01;
      this._spawnExplosion(target.x, target.y);
      AudioEngine.playSfx('explosion');
    }
  },

  _spawnFloater(x, y, amount, crit) {
    const wrap = document.getElementById('view-battle');
    const el = document.createElement('div');
    el.className = 'dmg-float' + (crit ? ' crit' : '');
    el.textContent = (crit ? '-' : '-') + amount;
    const rect = this.canvas.getBoundingClientRect();
    el.style.left = (rect.left + x) + 'px';
    el.style.top = (rect.top + y) + 'px';
    document.body.appendChild(el);
    setTimeout(() => el.remove(), 720);
  },

  _spawnHitParticle(x, y, color) {
    for (let i = 0; i < 4; i++) {
      this.particles.push({ x, y, vx: (Math.random() - 0.5) * 90, vy: (Math.random() - 0.5) * 90, life: 0.3, maxLife: 0.3, color, size: 3 });
    }
  },
  _spawnExplosion(x, y) {
    for (let i = 0; i < 14; i++) {
      const ang = Math.random() * Math.PI * 2, spd = 40 + Math.random() * 120;
      this.particles.push({ x, y, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, life: 0.5, maxLife: 0.5, color: Math.random() < 0.5 ? '#ff6b35' : '#ffb727', size: 4 + Math.random() * 3 });
    }
  },
  _updateParticles(dt) {
    for (const p of this.particles) { p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt; p.vx *= 0.94; p.vy *= 0.94; }
    this.particles = this.particles.filter((p) => p.life > 0);
  },

  _updateHpBars(instant) {
    const totalPlayerMax = this.entities.filter((e) => e.team === 'player').reduce((s, e) => s + e.maxHp, 0) || 1;
    const totalPlayerCur = this.entities.filter((e) => e.team === 'player' && e.alive).reduce((s, e) => s + Math.max(0, e.hp), 0);
    const totalEnemyMax = this.entities.filter((e) => e.team === 'enemy').reduce((s, e) => s + e.maxHp, 0) || 1;
    const totalEnemyCur = this.entities.filter((e) => e.team === 'enemy' && e.alive).reduce((s, e) => s + Math.max(0, e.hp), 0);
    const pPct = this.started ? Math.max(0, totalPlayerCur / totalPlayerMax * 100) : 100;
    const ePct = this.started ? Math.max(0, totalEnemyCur / totalEnemyMax * 100) : 100;
    document.getElementById('player-hp-fill').style.width = pPct + '%';
    document.getElementById('enemy-hp-fill').style.width = ePct + '%';
    this._lastPlayerPct = pPct;
  },

  _render() {
    const ctx = this.ctx;
    ctx.clearRect(0, 0, this.width, this.height);
    // faint center line
    ctx.strokeStyle = 'rgba(255,255,255,0.08)';
    ctx.setLineDash([6, 8]);
    ctx.beginPath(); ctx.moveTo(this.width / 2, 0); ctx.lineTo(this.width / 2, this.height); ctx.stroke();
    ctx.setLineDash([]);

    for (const e of this.entities) {
      const img = UnitIconCache.tinted[e.type] ? UnitIconCache.tinted[e.type][e.team] : null;
      const r = e.radius;
      if (img) ctx.drawImage(img, e.x - r, e.y - r, r * 2, r * 2);
      if (e.isBoss) {
        ctx.font = `${r}px sans-serif`;
        ctx.textAlign = 'center';
        ctx.fillText('★', e.x, e.y - r - 6);
      }
      if (e.alive) {
        const w = r * 1.7, h = 4, ratio = Math.max(0, e.hp / e.maxHp);
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(e.x - w / 2, e.y - r - 12, w, h);
        ctx.fillStyle = ratio > 0.5 ? '#4fc981' : ratio > 0.25 ? '#ffb727' : '#e64444';
        ctx.fillRect(e.x - w / 2, e.y - r - 12, w * ratio, h);
      }
    }
    for (const p of this.particles) {
      ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
      ctx.fillStyle = p.color;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
      ctx.globalAlpha = 1;
    }
  },

  _end(win) {
    this.started = false;
    cancelAnimationFrame(this.animId);
    document.getElementById('btn-deploy-wave').disabled = true;
    setTimeout(() => onBattleEnd(win, this.levelIndex, this._lastPlayerPct || 0), 450);
  },

  stop() {
    this.started = false;
    if (this.animId) cancelAnimationFrame(this.animId);
    this.entities = []; this.particles = [];
  },
};

/* ============================================================================
   9. CAMPAIGN PROGRESSION — map view, level select, battle resolution
   ============================================================================ */
let selectedChapter = 1;

function renderChapterScroll() {
  const wrap = document.getElementById('chapter-scroll');
  wrap.innerHTML = '';
  CHAPTERS.forEach((ch) => {
    const unlocked = state.maxLevelUnlocked >= ch.from;
    const chip = document.createElement('button');
    chip.className = 'chapter-chip' + (ch.index === selectedChapter ? ' active' : '') + (!unlocked ? ' locked' : '');
    chip.textContent = `${ch.index}. ${ch.name}`;
    chip.addEventListener('click', () => { selectedChapter = ch.index; renderMapView(); });
    wrap.appendChild(chip);
  });
}

function renderMapView() {
  renderChapterScroll();
  const ch = CHAPTERS[selectedChapter - 1];
  document.getElementById('map-chapter-title').textContent = `Chapter ${ch.index}: ${ch.name}`;
  document.getElementById('map-chapter-sub').textContent = `Levels ${ch.from}–${ch.to}`;
  const grid = document.getElementById('level-grid');
  grid.innerHTML = '';
  for (let n = ch.from; n <= ch.to; n++) {
    const level = LEVELS[n - 1];
    const locked = n > state.maxLevelUnlocked;
    const stars = state.levelStars[n] || 0;
    const node = document.createElement('button');
    node.className = 'level-node' + (locked ? ' locked' : '') + (stars > 0 ? ' completed' : '') + (level.isBoss ? ' boss' : '');
    if (locked) {
      node.innerHTML = `<img class="level-lock" src="assets/icons/ui_lock.png" alt="Locked">`;
    } else {
      node.innerHTML = `${n}` + `<span class="level-stars">${[0, 1, 2].map((i) => `<img src="assets/icons/ui_star${i < stars ? '' : '_empty'}.png">`).join('')}</span>`;
    }
    node.addEventListener('click', () => locked ? showToast('Complete the previous level to unlock this one') : openBattleView(n));
    grid.appendChild(node);
  }
}

function openBattleView(levelIndex) {
  switchView('battle');
  Battle.setup(levelIndex);
}

function onBattleEnd(win, levelIndex, playerPct) {
  const level = LEVELS[levelIndex - 1];
  const resultBanner = document.getElementById('result-banner');
  const starsWrap = document.getElementById('result-stars');
  const rewardsWrap = document.getElementById('result-rewards');
  const nextBtn = document.getElementById('btn-result-next');
  const retryBtn = document.getElementById('btn-result-retry');

  if (win) {
    const stars = playerPct >= 66 ? 3 : playerPct >= 33 ? 2 : 1;
    state.levelStars[levelIndex] = Math.max(state.levelStars[levelIndex] || 0, stars);
    if (levelIndex === state.maxLevelUnlocked && levelIndex < 100) state.maxLevelUnlocked = levelIndex + 1;
    else if (levelIndex === state.maxLevelUnlocked && levelIndex === 100) state.maxLevelUnlocked = 100;
    state.stats.battlesWon++;

    const radarMult = BUILDING_DEFS.radar.effectValue(state.buildings.radar.level);
    const rewards = {};
    Object.entries(level.rewards).forEach(([res, amt]) => { if (amt > 0) rewards[res] = Math.round(amt * radarMult); });
    grantRewards(rewards);

    resultBanner.textContent = 'VICTORY';
    resultBanner.classList.remove('defeat');
    starsWrap.querySelectorAll('.star').forEach((s, i) => {
      s.src = `assets/icons/ui_star${i < stars ? '' : '_empty'}.png`;
      s.classList.toggle('earned', i < stars);
    });
    rewardsWrap.innerHTML = Object.entries(rewards).map(([res, amt]) => `
      <div class="reward-chip"><img src="${RESOURCE_DEFS[res].icon}">+${formatNumber(amt)}</div>`).join('');
    nextBtn.classList.toggle('hidden', levelIndex >= 100);
    retryBtn.classList.add('hidden');
    nextBtn.onclick = () => { closeModal(); maybeShowInterstitial(() => openBattleView(levelIndex + 1)); };
    AudioEngine.playSfx('victory');
    AudioEngine.playMusic('victory');
    setTimeout(() => AudioEngine.playMusic('menu'), 2200);
    trackEvent('level_complete', { level: levelIndex, stars });
  } else {
    state.stats.battlesLost++;
    const consolation = { coins: Math.round(level.rewards.coins * 0.2) };
    grantRewards(consolation);
    resultBanner.textContent = 'DEFEAT';
    resultBanner.classList.add('defeat');
    starsWrap.querySelectorAll('.star').forEach((s) => { s.src = 'assets/icons/ui_star_empty.png'; s.classList.remove('earned'); });
    rewardsWrap.innerHTML = Object.entries(consolation).map(([res, amt]) => `
      <div class="reward-chip"><img src="${RESOURCE_DEFS[res].icon}">+${formatNumber(amt)}</div>`).join('');
    nextBtn.classList.add('hidden');
    retryBtn.classList.remove('hidden');
    retryBtn.onclick = () => { closeModal(); openBattleView(levelIndex); };
    AudioEngine.playSfx('defeat');
    trackEvent('level_failed', { level: levelIndex });
  }
  document.getElementById('btn-result-home').onclick = () => {
    closeModal(); Battle.stop(); switchView('map'); AudioEngine.playMusic('menu');
  };
  renderResources();
  checkAchievements();
  saveState();
  openModal('modal-result');
}

let battlesSinceAd = 0;
function maybeShowInterstitial(onDone) {
  battlesSinceAd++;
  if (battlesSinceAd >= 3) {
    battlesSinceAd = 0;
    showInterstitialAd(onDone);
  } else {
    onDone();
  }
}

/* Centralized reward granting so lifetime totals stay accurate for achievements. */
function grantRewards(rewards) {
  Object.entries(rewards).forEach(([res, amt]) => {
    if (amt <= 0) return;
    addResource(res, amt, { skipGlobalBonus: res !== 'coins' });
    if (res === 'diamonds') state.stats.totalDiamondsEarned = (state.stats.totalDiamondsEarned || 0) + amt;
    if (res === 'coins') state.stats.totalCoinsEarned = (state.stats.totalCoinsEarned || 0) + amt;
  });
}

/* ============================================================================
   10. ACHIEVEMENTS
   ============================================================================ */
const ACHIEVEMENT_DEFS = [
  { id: 'first_merge', name: 'First Merge', desc: 'Merge 2 units together', icon: 'assets/icons/fx_merge_glow.png', target: 1, get: (s) => s.stats.totalMerges, reward: { coins: 100 } },
  { id: 'merge_25', name: 'Merge Enthusiast', desc: 'Perform 25 merges', icon: 'assets/icons/fx_merge_glow.png', target: 25, get: (s) => s.stats.totalMerges, reward: { coins: 400, energy: 40 } },
  { id: 'merge_100', name: 'Merge Master', desc: 'Perform 100 merges', icon: 'assets/icons/fx_merge_glow.png', target: 100, get: (s) => s.stats.totalMerges, reward: { diamonds: 15 } },
  { id: 'recruit_10', name: 'Recruiter', desc: 'Recruit 10 units', icon: 'assets/images/unit_infantry.png', target: 10, get: (s) => s.stats.unitsRecruited, reward: { coins: 200 } },
  { id: 'recruit_50', name: 'Army Builder', desc: 'Recruit 50 units', icon: 'assets/images/unit_tank.png', target: 50, get: (s) => s.stats.unitsRecruited, reward: { steel: 150 } },
  { id: 'build_5', name: 'Builder', desc: 'Upgrade buildings 5 times', icon: 'assets/images/building_hq.png', target: 5, get: (s) => s.stats.buildingsUpgraded, reward: { coins: 300 } },
  { id: 'build_25', name: 'Chief Engineer', desc: 'Upgrade buildings 25 times', icon: 'assets/images/building_factory.png', target: 25, get: (s) => s.stats.buildingsUpgraded, reward: { diamonds: 20 } },
  { id: 'hq_5', name: 'Rising Command', desc: 'Reach Headquarters Level 5', icon: 'assets/images/building_hq.png', target: 5, get: (s) => s.buildings.hq.level, reward: { steel: 200, fuel: 100 } },
  { id: 'hq_10', name: 'Fortified', desc: 'Reach Headquarters Level 10', icon: 'assets/images/building_hq.png', target: 10, get: (s) => s.buildings.hq.level, reward: { diamonds: 30 } },
  { id: 'win_1', name: 'First Blood', desc: 'Win your first battle', icon: 'assets/icons/ui_trophy.png', target: 1, get: (s) => s.stats.battlesWon, reward: { coins: 150 } },
  { id: 'win_25', name: 'Veteran Commander', desc: 'Win 25 battles', icon: 'assets/icons/ui_trophy.png', target: 25, get: (s) => s.stats.battlesWon, reward: { fuel: 150 } },
  { id: 'win_75', name: 'War Hero', desc: 'Win 75 battles', icon: 'assets/icons/ui_trophy.png', target: 75, get: (s) => s.stats.battlesWon, reward: { diamonds: 40 } },
  { id: 'chapter1', name: 'Coastal Victory', desc: 'Clear Chapter 1', icon: 'assets/icons/ui_star.png', target: 11, get: (s) => s.maxLevelUnlocked, reward: { diamonds: 10 } },
  { id: 'chapter5', name: 'Halfway Hero', desc: 'Clear Chapter 5', icon: 'assets/icons/ui_star.png', target: 51, get: (s) => s.maxLevelUnlocked, reward: { diamonds: 25 } },
  { id: 'chapter10', name: 'Apex Commander', desc: 'Clear all 100 levels', icon: 'assets/icons/ui_trophy.png', target: 100, get: (s) => s.maxLevelUnlocked, reward: { diamonds: 100 } },
];

function checkAchievements() {
  const dot = document.getElementById('daily-reward-dot');
  const anyClaimable = ACHIEVEMENT_DEFS.some((a) => !state.achievementsClaimed[a.id] && a.get(state) >= a.target);
  document.querySelectorAll('.nav-btn[data-view="achievements"] .notif-dot-slot').forEach((n) => n.remove());
  const awardsBtn = document.querySelector('.nav-btn[data-view="achievements"]');
  if (awardsBtn) {
    let existingDot = awardsBtn.querySelector('.notif-dot');
    if (anyClaimable && !existingDot) {
      const d = document.createElement('span'); d.className = 'notif-dot'; awardsBtn.style.position = 'relative'; awardsBtn.appendChild(d);
    } else if (!anyClaimable && existingDot) { existingDot.remove(); }
  }
}

function renderAchievementList() {
  const wrap = document.getElementById('achievement-list');
  wrap.innerHTML = '';
  ACHIEVEMENT_DEFS.forEach((a) => {
    const progress = Math.min(a.target, a.get(state));
    const done = progress >= a.target;
    const claimed = !!state.achievementsClaimed[a.id];
    const item = document.createElement('div');
    item.className = 'achievement-item' + (claimed ? ' claimed' : '');
    const rewardText = Object.entries(a.reward).map(([r, amt]) => `+${amt} ${RESOURCE_DEFS[r].name}`).join(', ');
    item.innerHTML = `
      <img src="${a.icon}" alt="">
      <div class="achievement-info">
        <div class="a-title">${a.name}</div>
        <div class="a-desc">${a.desc} · Reward: ${rewardText}</div>
        <div class="achievement-progress-track"><div class="achievement-progress-fill" style="width:${(progress / a.target) * 100}%"></div></div>
      </div>
      <button class="btn btn-tiny ${done && !claimed ? 'btn-gold' : 'btn-secondary'}" ${claimed ? 'disabled' : ''} data-ach="${a.id}">
        ${claimed ? 'Done' : done ? 'Claim' : progress + '/' + a.target}
      </button>`;
    wrap.appendChild(item);
  });
  wrap.querySelectorAll('button[data-ach]').forEach((btn) => {
    btn.addEventListener('click', () => claimAchievement(btn.dataset.ach));
  });
}

function claimAchievement(id) {
  const a = ACHIEVEMENT_DEFS.find((x) => x.id === id);
  if (!a || state.achievementsClaimed[id] || a.get(state) < a.target) return;
  state.achievementsClaimed[id] = true;
  grantRewards(a.reward);
  AudioEngine.playSfx('chest');
  showToast(`Achievement claimed: ${a.name}!`);
  renderAchievementList();
  renderResources(Object.keys(a.reward));
  checkAchievements();
  saveState();
}

/* ============================================================================
   11. DAILY LOGIN REWARDS
   ============================================================================ */
const DAILY_REWARDS = [
  { coins: 200 }, { energy: 50 }, { steel: 60 }, { fuel: 40 },
  { coins: 500, steel: 40 }, { diamonds: 5 }, { diamonds: 15, coins: 1000 },
];

function isDailyAvailable() {
  const now = Date.now();
  const hoursSince = (now - (state.lastDailyClaim || 0)) / 3600000;
  return hoursSince >= 20; // allow claiming roughly once per day with reasonable timezone slack
}

function updateDailyDot() {
  const dot = document.getElementById('daily-reward-dot');
  dot.classList.toggle('hidden', !isDailyAvailable());
}

function renderDailyGrid() {
  const grid = document.getElementById('daily-grid');
  grid.innerHTML = '';
  const nextDay = isDailyAvailable() ? (state.dailyStreak % 7) + 1 : ((state.dailyStreak - 1) % 7) + 1;
  for (let day = 1; day <= 7; day++) {
    const reward = DAILY_REWARDS[day - 1];
    const claimedAlready = day < nextDay || (day === nextDay && !isDailyAvailable());
    const isToday = day === nextDay && isDailyAvailable();
    const cell = document.createElement('div');
    cell.className = 'daily-day' + (isToday ? ' today' : '') + (claimedAlready ? ' claimed' : '');
    const [res, amt] = Object.entries(reward)[0];
    cell.innerHTML = `<div class="d-label">Day ${day}</div><img src="${RESOURCE_DEFS[res].icon}"><div class="d-amount">+${amt}</div>`;
    grid.appendChild(cell);
  }
  const claimBtn = document.getElementById('btn-claim-daily');
  claimBtn.disabled = !isDailyAvailable();
  claimBtn.textContent = isDailyAvailable() ? `CLAIM DAY ${nextDay}` : 'COME BACK TOMORROW';
}

function claimDailyReward() {
  if (!isDailyAvailable()) return;
  const hoursSince = (Date.now() - (state.lastDailyClaim || 0)) / 3600000;
  state.dailyStreak = hoursSince <= 48 ? state.dailyStreak + 1 : 1;
  const day = ((state.dailyStreak - 1) % 7) + 1;
  grantRewards(DAILY_REWARDS[day - 1]);
  state.lastDailyClaim = Date.now();
  AudioEngine.playSfx('chest');
  AudioEngine.vibrate([10, 40, 10]);
  showToast(`Day ${day} reward claimed!`);
  renderDailyGrid();
  updateDailyDot();
  renderResources(Object.keys(DAILY_REWARDS[day - 1]));
  saveState();
}

/* ============================================================================
   12. SHOP / ADS / ANALYTICS  (placeholders only — no ad network code, per spec)
   ============================================================================ */
const SHOP_ITEMS = [
  { id: 'ad_coins', title: 'Watch Ad\n+150 Coins', icon: 'assets/images/resource_coins.png', type: 'ad', reward: { coins: 150 } },
  { id: 'ad_energy', title: 'Watch Ad\n+40 Energy', icon: 'assets/images/resource_energy.png', type: 'ad', reward: { energy: 40 } },
  { id: 'ad_fuel', title: 'Watch Ad\n+30 Fuel', icon: 'assets/images/resource_fuel.png', type: 'ad', reward: { fuel: 30 } },
  { id: 'ad_chest', title: 'Watch Ad\nFree Chest', icon: 'assets/icons/ui_chest.png', type: 'ad', reward: { coins: 300, steel: 40, energy: 20 } },
  { id: 'ex_coins', title: 'Exchange\n10 💎 → 800 Coins', icon: 'assets/images/resource_coins.png', type: 'exchange', cost: { diamonds: 10 }, reward: { coins: 800 } },
  { id: 'ex_steel', title: 'Exchange\n15 💎 → 300 Steel', icon: 'assets/images/resource_steel.png', type: 'exchange', cost: { diamonds: 15 }, reward: { steel: 300 } },
];

function renderShopGrid() {
  const grid = document.getElementById('shop-grid');
  grid.innerHTML = '';
  SHOP_ITEMS.forEach((item) => {
    const card = document.createElement('div');
    card.className = 'shop-card';
    const isExchange = item.type === 'exchange';
    const affordable = !isExchange || canAfford(item.cost);
    card.innerHTML = `
      <img src="${item.icon}" alt="">
      <div class="shop-title">${item.title.replace('\n', '<br>')}</div>
      <button class="btn ${isExchange ? 'btn-secondary' : 'btn-gold'}" ${!affordable ? 'disabled' : ''} data-shop="${item.id}">
        ${isExchange ? 'Exchange' : '▶ Watch'}
      </button>`;
    grid.appendChild(card);
  });
  grid.querySelectorAll('button[data-shop]').forEach((btn) => {
    btn.addEventListener('click', () => purchaseShopItem(btn.dataset.shop));
  });
}

function purchaseShopItem(id) {
  const item = SHOP_ITEMS.find((x) => x.id === id);
  if (!item) return;
  if (item.type === 'exchange') {
    if (!spendResources(item.cost)) { showToast('Not enough diamonds'); return; }
    grantRewards(item.reward);
    AudioEngine.playSfx('coin');
    showToast('Exchange complete!');
    renderResources([...Object.keys(item.cost), ...Object.keys(item.reward)]);
    renderShopGrid();
    saveState();
  } else {
    showRewardedAd(() => {
      grantRewards(item.reward);
      AudioEngine.playSfx('chest');
      showToast('Reward granted!');
      renderResources(Object.keys(item.reward));
      renderShopGrid();
      trackEvent('rewarded_ad_complete', { item: id });
      saveState();
    });
  }
}

/**
 * Rewarded-ad placeholder.
 * ---- INTEGRATION POINT ----
 * Swap the setTimeout simulation below for your ad network's real call, e.g.:
 *   AdNetworkSDK.showRewardedVideo({
 *     onReward: () => { closeModal(); onReward(); },
 *     onFail:   () => { closeModal(); showToast('Ad not available'); }
 *   });
 * No third-party ad SDK is bundled in this project, per project requirements.
 */
function showRewardedAd(onReward) {
  trackEvent('rewarded_ad_start');
  document.getElementById('ad-message').textContent = 'Loading video ad…';
  openModal('modal-ad');
  setTimeout(() => {
    document.getElementById('ad-message').textContent = 'Ad complete — reward granted!';
    setTimeout(() => { closeModal(); onReward(); }, 500);
  }, 1500);
}

/**
 * Interstitial placeholder shown between levels.
 * ---- INTEGRATION POINT ----
 * Replace the manual "Continue" button with your ad network's close callback.
 */
function showInterstitialAd(onDone) {
  trackEvent('interstitial_shown');
  const el = document.getElementById('ad-slot-interstitial');
  el.classList.remove('hidden');
  document.getElementById('btn-skip-interstitial').onclick = () => {
    el.classList.add('hidden');
    onDone && onDone();
  };
}

/**
 * Analytics placeholder.
 * ---- INTEGRATION POINT ----
 * Add the Google Analytics (GA4) snippet to index.html <head> with your
 * Measurement ID, then replace the console.log below with:
 *   if (window.gtag) gtag('event', name, params);
 */
function trackEvent(name, params) {
  console.log('[analytics]', name, params || {});
}

/* ============================================================================
   13. NAVIGATION, VIEW SWITCHING & UI WIRING
   ============================================================================ */
function switchView(view) {
  document.querySelectorAll('.view-panel').forEach((p) => p.classList.add('hidden'));
  const panel = document.getElementById('view-' + view);
  if (panel) panel.classList.remove('hidden');
  document.querySelectorAll('.nav-btn').forEach((b) => b.classList.toggle('active', b.dataset.view === view));
  currentView = view;
  if (view === 'base') { renderBaseGrid(); renderMergeYard(); }
  if (view === 'map') { renderMapView(); }
}

function syncSettingsToggles() {
  document.getElementById('toggle-music').checked = state.settings.music;
  document.getElementById('toggle-sfx').checked = state.settings.sfx;
  document.getElementById('toggle-vibrate').checked = state.settings.vibrate;
  const icon = document.getElementById('btn-mute-splash-icon');
  if (icon) icon.src = (state.settings.music || state.settings.sfx) ? 'assets/icons/ui_sound_on.png' : 'assets/icons/ui_sound_off.png';
}

function maybeShowOfflineEarnings() {
  const earn = computeOfflineEarnings();
  if (earn) {
    const parts = [];
    if (earn.coins) parts.push(`+${formatNumber(earn.coins)} Coins`);
    if (earn.energy) parts.push(`+${formatNumber(earn.energy)} Energy`);
    if (parts.length) {
      showToast(`Welcome back! Earned ${parts.join(', ')} while away.`, 3400);
      renderResources(['coins', 'energy']);
      saveState();
    }
  }
}

function wireUI() {
  document.addEventListener('pointerdown', () => AudioEngine.unlock(), { once: true });

  // ---------- Splash screen ----------
  document.getElementById('btn-play').addEventListener('click', () => {
    AudioEngine.playSfx('click');
    document.getElementById('splash-screen').classList.add('hidden');
    document.getElementById('game-screen').classList.remove('hidden');
    switchView('base');
    AudioEngine.playMusic('menu');
    maybeShowOfflineEarnings();
    checkAchievements();
  });
  document.getElementById('btn-daily-reward').addEventListener('click', () => {
    AudioEngine.playSfx('click'); renderDailyGrid(); openModal('modal-daily');
  });
  document.getElementById('btn-settings').addEventListener('click', () => { AudioEngine.playSfx('click'); openModal('modal-settings'); });
  document.getElementById('btn-achievements').addEventListener('click', () => {
    AudioEngine.playSfx('click'); renderAchievementList(); openModal('modal-achievements');
  });
  document.getElementById('btn-mute-splash').addEventListener('click', () => {
    const turnOn = !(state.settings.music || state.settings.sfx);
    state.settings.sfx = turnOn;
    AudioEngine.setMusicEnabled(turnOn);
    syncSettingsToggles();
    saveState();
  });

  // ---------- Top HUD ----------
  document.getElementById('hq-badge').addEventListener('click', () => openBuildingModal('hq'));
  document.getElementById('btn-pause').addEventListener('click', () => { AudioEngine.playSfx('click'); openModal('modal-settings'); });

  // ---------- Bottom nav ----------
  document.querySelectorAll('.nav-btn').forEach((btn) => {
    btn.addEventListener('click', () => {
      AudioEngine.playSfx('click');
      const view = btn.dataset.view;
      if (view === 'shop') { renderShopGrid(); openModal('modal-shop'); return; }
      if (view === 'achievements') { renderAchievementList(); openModal('modal-achievements'); return; }
      if (view === 'battle') { openBattleView(Math.min(100, state.maxLevelUnlocked)); return; }
      switchView(view);
    });
  });

  // ---------- Base view ----------
  document.querySelectorAll('#merge-line-tabs .tab-chip').forEach((chip) => {
    chip.addEventListener('click', () => {
      AudioEngine.playSfx('click');
      document.querySelectorAll('#merge-line-tabs .tab-chip').forEach((c) => c.classList.remove('active'));
      chip.classList.add('active');
      currentLine = chip.dataset.line;
      renderMergeYard();
    });
  });
  document.getElementById('btn-spawn-unit').addEventListener('click', recruitUnit);

  // ---------- Battle view ----------
  document.getElementById('btn-deploy-wave').addEventListener('click', () => Battle.deploy());
  document.getElementById('btn-battle-speed').addEventListener('click', () => Battle.toggleSpeed());
  document.getElementById('btn-battle-back').addEventListener('click', () => {
    confirmDialog('Retreat from this battle?', () => { Battle.stop(); switchView('map'); AudioEngine.playMusic('menu'); });
  });
  document.getElementById('btn-battle-pause').addEventListener('click', () => {
    if (!Battle.started) return;
    if (Battle.paused) { openModal('modal-pause'); return; }
    Battle.paused = true;
    if (Battle.animId) cancelAnimationFrame(Battle.animId);
    openModal('modal-pause');
  });
  document.getElementById('btn-resume').addEventListener('click', () => {
    closeModal();
    if (Battle.paused) { Battle.paused = false; Battle.lastTime = performance.now(); Battle._loop(Battle.lastTime); }
  });
  document.getElementById('btn-pause-settings').addEventListener('click', () => openModal('modal-settings'));
  document.getElementById('btn-pause-home').addEventListener('click', () => {
    confirmDialog('Exit to home? This battle will be abandoned.', () => {
      Battle.stop(); switchView('base'); AudioEngine.playMusic('menu');
    });
  });

  // ---------- Modal chrome ----------
  document.querySelectorAll('[data-close]').forEach((btn) => btn.addEventListener('click', closeModal));
  document.getElementById('modal-overlay').addEventListener('click', (e) => {
    if (e.target.id !== 'modal-overlay') return;
    const openEl = document.querySelector('.modal.active');
    if (!openEl) return;
    const noBackdrop = ['modal-result', 'modal-confirm', 'modal-pause', 'modal-ad'];
    if (!noBackdrop.includes(openEl.id)) closeModal();
  });

  // ---------- Settings ----------
  document.getElementById('toggle-music').addEventListener('change', (e) => { AudioEngine.setMusicEnabled(e.target.checked); saveState(); });
  document.getElementById('toggle-sfx').addEventListener('change', (e) => { AudioEngine.setSfxEnabled(e.target.checked); saveState(); });
  document.getElementById('toggle-vibrate').addEventListener('change', (e) => { state.settings.vibrate = e.target.checked; saveState(); });
  document.getElementById('btn-reset-progress').addEventListener('click', () => {
    confirmDialog('Reset ALL progress? This cannot be undone.', () => { resetProgress(); location.reload(); });
  });
  document.getElementById('btn-claim-daily').addEventListener('click', claimDailyReward);

  // ---------- PWA install ----------
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    window._deferredInstallPrompt = e;
  });
  document.getElementById('btn-install-pwa').addEventListener('click', async () => {
    const evt = window._deferredInstallPrompt;
    if (evt) { evt.prompt(); await evt.userChoice; window._deferredInstallPrompt = null; }
    else { showToast('Use your browser menu → "Add to Home Screen" to install'); }
  });
}

/* ============================================================================
   14. BOOT SEQUENCE
   ============================================================================ */
const LOADING_TIPS = [
  'Tip: Merge two units of the same level to create a stronger unit.',
  'Tip: Upgrade your Headquarters to unlock higher levels for every building.',
  'Tip: The Garage expands how many units you can hold in the Merge Yard.',
  'Tip: Boss levels appear every 10th level and drop bonus Diamonds.',
  'Tip: Power Plants keep generating Energy even while you are away.',
  'Tip: Reinforce mid-battle for a small Fuel cost if the fight gets close.',
  'Tip: The Research Lab boosts the damage of your entire army at once.',
  'Tip: Swipe a unit onto a matching one to merge them into something stronger.',
];

function setLoadingProgress(pct) {
  document.getElementById('loading-bar-fill').style.width = pct + '%';
  const bar = document.getElementById('loading-progressbar');
  if (bar) bar.setAttribute('aria-valuenow', String(pct));
}

function finishBoot() {
  setLoadingProgress(100);
  setTimeout(() => {
    document.getElementById('loading-screen').classList.add('hidden');
    document.getElementById('splash-screen').classList.remove('hidden');
  }, 260);
}

function registerServiceWorker() {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('sw.js').catch((err) => console.warn('Service worker registration failed:', err));
    });
  }
}

function boot() {
  state = loadState();
  AudioEngine.init();
  syncSettingsToggles();
  updateDailyDot();
  checkAchievements();
  wireUI();
  registerServiceWorker();

  document.getElementById('loading-tip').textContent = LOADING_TIPS[Math.floor(Math.random() * LOADING_TIPS.length)];
  const tipTimer = setInterval(() => {
    document.getElementById('loading-tip').textContent = LOADING_TIPS[Math.floor(Math.random() * LOADING_TIPS.length)];
  }, 1700);

  const bgImages = ['assets/images/bg_menu.jpg', 'assets/images/bg_battle.jpg'];
  const steps = [
    (done) => preloadUnitIcons(done),
    (done) => {
      let remaining = bgImages.length;
      bgImages.forEach((src) => {
        const im = new Image();
        im.onload = im.onerror = () => { remaining--; if (remaining <= 0) done(); };
        im.src = src;
      });
    },
    (done) => setTimeout(done, 150),
  ];
  let idx = 0;
  setLoadingProgress(6);
  function step() {
    if (idx >= steps.length) { clearInterval(tipTimer); finishBoot(); return; }
    steps[idx](() => { idx++; setLoadingProgress(Math.round((idx / steps.length) * 100)); step(); });
  }
  step();
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', boot);
} else {
  boot();
}

setInterval(() => { if (state) saveState(); }, CONFIG.AUTOSAVE_INTERVAL_MS);
window.addEventListener('beforeunload', () => { if (state) saveState(); });
document.addEventListener('visibilitychange', () => { if (document.hidden && state) saveState(); });
