โš”๏ธ FRONTLINE
๐ŸŽฏ Campaign
๐Ÿ’ฐ 200
๐Ÿ“ Level 1 ยท โšก Preparation
The Skirmish โ€“ Basic infantry clashes across open fields.
Enemy Tech: Basic
๐Ÿ“… Round 0 ยท 0/3

๐Ÿ›ก๏ธ YOUR FORCES

โš”๏ธ Swordsmen 8
๐Ÿ”ซ Gunmen 6
๐Ÿฐ Defenses 4

๐Ÿ‘น ENEMY FORCES

โš”๏ธ Swordsmen 6
๐Ÿ”ซ Gunmen 4
๐Ÿฐ Defenses 3
๐Ÿ›ก๏ธ Welcome, Commander. The campaign awaits.
// ============================================================ // WEATHER (light rain) // ============================================================ function spawnWeather() { const intensity = 0.2; for (let i = 0; i < intensity * 10; i++) { weatherParticles.push({ x: Math.random() * W, y: -10 - Math.random() * 30, speed: 2 + Math.random() * 3, size: 1 + Math.random() * 2, wind: (Math.random() - 0.5) * 0.3, opacity: 0.2 + Math.random() * 0.2 }); } } function updateWeather() { weatherParticles.forEach(p => { p.x += p.wind; p.y += p.speed; if (p.y > H + 10) { p.y = -10 - Math.random() * 30; p.x = Math.random() * W; } }); weatherParticles = weatherParticles.filter(p => p.y < H + 20); } function drawWeather() { weatherParticles.forEach(p => { ctx.globalAlpha = p.opacity * 0.3; ctx.fillStyle = '#aabbcc'; ctx.fillRect(p.x, p.y, p.size, p.size * 0.6); }); ctx.globalAlpha = 1; } // ============================================================ // PARTICLES & EXPLOSIONS // ============================================================ function spawnExplosion(x, y, isBig = false) { const count = isBig ? 40 : 20; playSound('explosion'); for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 1 + Math.random() * (isBig ? 8 : 5); const color = ['#ff6b35', '#ffd700', '#ff4444', '#ffaa00', '#ff8833'][Math.floor(Math.random() * 5)]; particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 0.5, life: 20 + Math.random() * (isBig ? 50 : 30), maxLife: 60, size: 2 + Math.random() * (isBig ? 10 : 6), color: color, gravity: 0.08 }); } for (let i = 0; i < (isBig ? 15 : 8); i++) { smokeParticles.push({ x: x + (Math.random() - 0.5) * 20, y: y + (Math.random() - 0.5) * 20, vx: (Math.random() - 0.5) * 0.8, vy: -0.3 - Math.random() * 0.5, life: 40 + Math.random() * 40, maxLife: 80, size: 10 + Math.random() * 20, opacity: 0.2 + Math.random() * 0.3 }); } } function spawnMuzzleFlash(x, y) { playSound('gunshot'); for (let i = 0; i < 8; i++) { const angle = Math.random() * Math.PI * 2; const speed = 2 + Math.random() * 5; particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 8 + Math.random() * 12, maxLife: 20, size: 1 + Math.random() * 3, color: '#ffdd44', gravity: 0 }); } } function updateParticles() { particles.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.gravity) p.vy += p.gravity; p.life--; }); particles = particles.filter(p => p.life > 0); smokeParticles.forEach(s => { s.x += s.vx; s.y += s.vy; s.vy -= 0.008; s.size += 0.3; s.life--; s.opacity *= 0.99; }); smokeParticles = smokeParticles.filter(s => s.life > 0 && s.opacity > 0.01); explosions.forEach(e => e.life--); explosions = explosions.filter(e => e.life > 0); } function drawParticles() { smokeParticles.forEach(s => { ctx.globalAlpha = s.opacity * (s.life / s.maxLife) * 0.4; ctx.fillStyle = '#888888'; ctx.shadowColor = '#444444'; ctx.shadowBlur = 20; ctx.beginPath(); ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; }); ctx.globalAlpha = 1; particles.forEach(p => { const alpha = p.life / p.maxLife; ctx.globalAlpha = alpha; ctx.fillStyle = p.color; ctx.shadowColor = p.color; ctx.shadowBlur = 15; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; }); ctx.globalAlpha = 1; explosions.forEach(e => { const progress = 1 - e.life / e.maxLife; const radius = e.radius + progress * 35; const alpha = 1 - progress; ctx.globalAlpha = alpha * 0.5; ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(e.x, e.y, radius, 0, Math.PI * 2); ctx.stroke(); ctx.globalAlpha = 1; }); } // ============================================================ // BATTLE ENGINE โ€“ start and animate // ============================================================ function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); } function findNearestEnemy(unit) { const enemies = animUnits.filter(u => u.alive && u.side !== unit.side); if (enemies.length === 0) return null; return enemies.reduce((a, b) => dist(unit, a) < dist(unit, b) ? a : b); } function startBattle() { if (battleRunning) return; if (isPlayerDead() || isBotDead()) { addLog('โš ๏ธ One army is already wiped out!', 'system'); return; } battleRunning = true; fightBtn.disabled = true; phaseDisplay.textContent = 'โš”๏ธ ENGAGEMENT'; playSound('gunshot'); const mult = getBotMultiplier(); const b = game.bot; const boostedBot = { swordsmen: { count: Math.floor(b.swordsmen.count * mult), damage: b.swordsmen.damage + Math.floor(mult * 1), hp: b.swordsmen.hp + Math.floor(mult * 2) }, gunmen: { count: Math.floor(b.gunmen.count * mult), damage: b.gunmen.damage + Math.floor(mult * 1), hp: b.gunmen.hp + Math.floor(mult * 1) }, defenses: { count: Math.floor(b.defenses.count * mult), damage: 0, hp: b.defenses.hp + Math.floor(mult * 5) } }; const p = game.player; const playerUnits = createUnitsForArmy(p, 'player', 60); const botUnits = createUnitsForArmy(boostedBot, 'bot', W - 60); animUnits = [...playerUnits, ...botUnits]; animUnits.forEach(u => { if (u.side === 'player') { const enemies = botUnits.filter(e => e.alive); if (enemies.length) u.target = enemies.reduce((a, b) => dist(u, a) < dist(u, b) ? a : b); } else { const enemies = playerUnits.filter(e => e.alive); if (enemies.length) u.target = enemies.reduce((a, b) => dist(u, a) < dist(u, b) ? a : b); } }); spawnWeather(); if (animFrame) cancelAnimationFrame(animFrame); animateBattle(); } function animateBattle() { frameCount++; const alivePlayer = animUnits.filter(u => u.alive && u.side === 'player'); const aliveBot = animUnits.filter(u => u.alive && u.side === 'bot'); if (alivePlayer.length === 0 || aliveBot.length === 0) { endBattle(alivePlayer.length === 0 ? 'bot' : 'player'); return; } animUnits.forEach(u => { if (!u.alive) return; if (u.isDefense) { if (u.attackTimer > 0) u.attackTimer--; const enemy = findNearestEnemy(u); if (enemy && dist(u, enemy) < u.attackRange) { if (u.attackTimer === 0) { enemy.hp -= u.damage; u.attackTimer = u.attackCooldown; spawnExplosion(enemy.x, enemy.y, false); spawnMuzzleFlash(u.x, u.y); u.isShooting = true; u.shootTimer = 10; playSound('hit'); if (enemy.hp <= 0) { enemy.alive = false; spawnExplosion(enemy.x, enemy.y, true); playSound('explosion'); addLog(`๐Ÿ’ฅ Enemy ${enemy.type} destroyed!`, 'system'); } } } } else { if (u.target && !u.target.alive) u.target = findNearestEnemy(u); if (u.target) { const d = dist(u, u.target); if (d > u.attackRange) { const angle = Math.atan2(u.target.y - u.y, u.target.x - u.x); u.x += Math.cos(angle) * u.moveSpeed; u.y += Math.sin(angle) * u.moveSpeed; } else { if (u.attackTimer > 0) u.attackTimer--; if (u.attackTimer === 0) { u.target.hp -= u.damage; u.attackTimer = u.attackCooldown; spawnExplosion(u.target.x, u.target.y, false); spawnMuzzleFlash(u.x, u.y); u.isShooting = true; u.shootTimer = 10; playSound('hit'); if (u.target.hp <= 0) { u.target.alive = false; spawnExplosion(u.target.x, u.target.y, true); playSound('explosion'); addLog(`๐Ÿ’ฅ Enemy ${u.target.type} destroyed!`, 'system'); } } } } } if (u.shootTimer > 0) u.shootTimer--; else u.isShooting = false; }); animUnits.forEach(u => { if (!u.alive) return; if (u.isDefense) return; if (!u.target || !u.target.alive) u.target = findNearestEnemy(u); }); drawBattle(); updateWeather(); updateParticles(); animFrame = requestAnimationFrame(animateBattle); } // ============================================================ // PRE-GENERATE STATIC LANDSCAPE (grass, trees, bushes) // ============================================================ const staticGrass = []; const staticTrees = []; const staticBushes = []; function generateLandscape() { staticGrass.length = 0; staticTrees.length = 0; staticBushes.length = 0; // Grass blades for (let i = 0; i < 300; i++) { const x = Math.random() * W; const y = H * 0.5 + Math.random() * (H * 0.5); const len = 3 + Math.random() * 8; const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.6; staticGrass.push({ x, y, len, angle, color: Math.random() > 0.5 ? '#2e7d32' : '#4caf50' }); } // Trees for (let i = 0; i < 6; i++) { const x = 50 + Math.random() * (W - 100); const y = H * 0.5 + 10 + Math.random() * (H * 0.3); const trunkH = 20 + Math.random() * 15; const radius = 25 + Math.random() * 15; staticTrees.push({ x, y, trunkH, radius }); } // Bushes for (let i = 0; i < 12; i++) { const x = 30 + Math.random() * (W - 60); const y = H * 0.5 + 20 + Math.random() * (H * 0.4); const r = 10 + Math.random() * 20; staticBushes.push({ x, y, r }); } } generateLandscape(); // ============================================================ // DRAW BATTLEFIELD โ€“ STATIC GREEN FIELD (FIXED) // ============================================================ function drawBattle() { ctx.clearRect(0, 0, W, H); // ---- SKY ---- const skyGrad = ctx.createLinearGradient(0, 0, 0, H * 0.5); skyGrad.addColorStop(0, '#87CEEB'); skyGrad.addColorStop(1, '#b5dffa'); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, W, H * 0.5); // ---- SUN ---- ctx.fillStyle = '#fff9e6'; ctx.shadowColor = '#ffd700'; ctx.shadowBlur = 40; ctx.beginPath(); ctx.arc(90, 60, 35, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; // ---- GROUND ---- const groundGrad = ctx.createLinearGradient(0, H * 0.5, 0, H); groundGrad.addColorStop(0, '#4caf50'); groundGrad.addColorStop(0.3, '#388e3c'); groundGrad.addColorStop(0.7, '#2e7d32'); groundGrad.addColorStop(1, '#1b5e20'); ctx.fillStyle = groundGrad; ctx.fillRect(0, H * 0.5, W, H * 0.5); // ---- STATIC GRASS ---- ctx.lineWidth = 1.5; for (let g of staticGrass) { ctx.strokeStyle = g.color; ctx.beginPath(); ctx.moveTo(g.x, g.y); ctx.lineTo(g.x + Math.cos(g.angle) * g.len, g.y + Math.sin(g.angle) * g.len); ctx.stroke(); } // ---- STATIC BUSHES ---- for (let b of staticBushes) { ctx.fillStyle = '#2e7d32'; ctx.shadowColor = '#1b5e20'; ctx.shadowBlur = 10; ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#388e3c'; ctx.beginPath(); ctx.arc(b.x - b.r * 0.3, b.y - b.r * 0.2, b.r * 0.7, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#4caf50'; ctx.beginPath(); ctx.arc(b.x + b.r * 0.3, b.y - b.r * 0.1, b.r * 0.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } // ---- STATIC TREES ---- for (let t of staticTrees) { // Trunk ctx.fillStyle = '#4a3a2a'; ctx.fillRect(t.x - 3, t.y, 6, t.trunkH); // Canopy ctx.fillStyle = '#1b5e20'; ctx.shadowColor = '#0d3b0d'; ctx.shadowBlur = 15; ctx.beginPath(); ctx.arc(t.x, t.y - 5, t.radius, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#2e7d32'; ctx.beginPath(); ctx.arc(t.x - 10, t.y - 15, t.radius * 0.7, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#4caf50'; ctx.beginPath(); ctx.arc(t.x + 12, t.y - 12, t.radius * 0.6, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } // ---- FRONT LINES (subtle trenches) ---- ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = 2; ctx.setLineDash([10, 15]); ctx.beginPath(); ctx.moveTo(20, H * 0.5 + 10); ctx.lineTo(20, H - 20); ctx.stroke(); ctx.beginPath(); ctx.moveTo(W - 20, H * 0.5 + 10); ctx.lineTo(W - 20, H - 20); ctx.stroke(); ctx.setLineDash([]); // ---- DRAW UNITS ---- animUnits.forEach(u => { if (!u.alive) return; if (u.isDefense) { // Draw turret ctx.fillStyle = '#6d4c41'; ctx.fillRect(u.x - 12, u.y - 10, 24, 20); ctx.fillStyle = '#3e2723'; ctx.fillRect(u.x - 8, u.y - 8, 16, 8); ctx.fillStyle = '#333'; ctx.fillRect(u.x - 4, u.y - 14, 8, 6); drawHealthBar(u); return; } const x = u.x, y = u.y; const dir = u.side === 'player' ? 1 : -1; // Shadow ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.beginPath(); ctx.ellipse(x, y + 14, 8, 3, 0, 0, Math.PI * 2); ctx.fill(); // Body ctx.fillStyle = u.color; ctx.fillRect(x - 5, y - 4, 10, 14); // Legs ctx.fillStyle = u.side === 'player' ? '#2e4a2e' : '#4a2a2a'; ctx.fillRect(x - 5, y + 8, 4, 8); ctx.fillRect(x + 1, y + 8, 4, 8); // Arms ctx.fillStyle = u.color; if (u.weapon === 'sword') { ctx.fillRect(x - 8, y - 2, 4, 10); ctx.fillRect(x + 4, y - 2, 4, 10); // Sword ctx.fillStyle = '#ccc'; ctx.fillRect(x - 12 + dir * 2, y - 6, 2, 10); ctx.fillStyle = '#ffd700'; ctx.fillRect(x - 13 + dir * 2, y - 7, 4, 2); } else { // Gun arms ctx.fillRect(x - 8, y - 2, 4, 10); ctx.fillRect(x + 4, y - 2, 4, 10); // Gun ctx.fillStyle = '#111'; if (u.side === 'player') { ctx.fillRect(x - 12, y - 2, 8, 3); ctx.fillRect(x - 16, y - 1, 6, 2); } else { ctx.fillRect(x + 4, y - 2, 8, 3); ctx.fillRect(x + 10, y - 1, 6, 2); } } // Helmet ctx.fillStyle = u.helmetColor; ctx.beginPath(); ctx.arc(x, y - 7, 6, Math.PI, 0); ctx.fill(); ctx.fillStyle = u.side === 'player' ? '#1a3a1a' : '#3a1a1a'; ctx.fillRect(x - 2, y - 6, 4, 3); // Face ctx.fillStyle = u.skinColor; ctx.beginPath(); ctx.arc(x, y - 4, 3, 0, Math.PI); ctx.fill(); // Eyes ctx.fillStyle = '#000'; ctx.fillRect(x - 2 - dir * 1, y - 5, 1, 1); ctx.fillRect(x + 1 - dir * 1, y - 5, 1, 1); // Shooting flash (muzzle) if (u.isShooting && u.weapon === 'gun') { ctx.fillStyle = '#ffdd44'; ctx.shadowColor = '#ffaa00'; ctx.shadowBlur = 15; const flashX = u.side === 'player' ? x - 18 : x + 18; ctx.beginPath(); ctx.arc(flashX, y - 1, 4 + Math.random() * 3, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } drawHealthBar(u); }); drawParticles(); drawWeather(); // ---- LABELS ---- ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.font = '12px monospace'; ctx.textAlign = 'left'; ctx.fillText('โ–ถ YOUR FRONT', 30, 30); ctx.textAlign = 'right'; ctx.fillText('ENEMY FRONT โ—€', W - 30, 30); ctx.fillStyle = 'rgba(255,215,0,0.15)'; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.fillText(`Level ${getLevel().id} ยท ${getLevel().name}`, 30, H - 15); } function drawHealthBar(u) { const barW = 20, barH = 3; const hpP = u.hp / u.maxHp; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(u.x - barW/2, u.y - 15, barW, barH); ctx.fillStyle = hpP > 0.6 ? '#4caf50' : hpP > 0.3 ? '#ff9800' : '#f44336'; ctx.fillRect(u.x - barW/2, u.y - 15, barW * hpP, barH); } // ============================================================ // END BATTLE // ============================================================ function endBattle(winner) { battleRunning = false; if (animFrame) cancelAnimationFrame(animFrame); phaseDisplay.textContent = '๐Ÿ“‹ Post-Battle'; const p = game.player, b = game.bot; const pCount = p.swordsmen.count + p.gunmen.count + p.defenses.count; const bCount = b.swordsmen.count + b.gunmen.count + b.defenses.count; if (winner === 'player') { const reward = getLevel().reward + Math.floor(Math.random() * 20); game.gold += reward; const pLoss = Math.floor(pCount * (0.05 + Math.random() * 0.12)); const bLoss = Math.floor(bCount * (0.5 + Math.random() * 0.3)); applyLosses('player', pLoss); applyLosses('bot', bLoss); game.levelWins++; addLog(`๐Ÿ† VICTORY! Earned ${reward} gold. (${game.levelWins}/${game.maxLevelWins} wins)`, 'win'); if (game.levelWins >= game.maxLevelWins) { if (game.level < LEVELS.length - 1) { game.level++; game.levelWins = 0; const bonus = 30 + game.level * 10; game.gold += bonus; addLog(`๐ŸŽฏ LEVEL ${getLevel().id} UNLOCKED! Bonus ${bonus} gold.`, 'level'); b.swordsmen.damage += 1; b.gunmen.damage += 1; b.defenses.hp += 5; } else { addLog('๐Ÿ† CAMPAIGN COMPLETE! You are the ultimate commander!', 'win'); } } } else if (winner === 'bot') { const loss = Math.floor(pCount * (0.2 + Math.random() * 0.2)); game.gold = Math.max(0, game.gold - 20); applyLosses('player', loss); const bLoss = Math.floor(bCount * (0.1 + Math.random() * 0.15)); applyLosses('bot', bLoss); addLog('๐Ÿ’€ DEFEAT! Lost 20 gold.', 'lose'); } else { addLog('โšก DRAW! Both sides retreat.', 'system'); } game.round++; phaseDisplay.textContent = 'โšก Preparation'; if (game.round % 2 === 0) { const scale = 1 + game.level * 0.08; b.swordsmen.damage += 1 * scale; b.gunmen.damage += 1 * scale; b.defenses.hp += 4 * scale; b.swordsmen.hp += 2 * scale; b.gunmen.hp += 1 * scale; addLog('๐Ÿ‘น Enemy forces have evolved!', 'system'); } updateUI(); updateCampaignUI(); fightBtn.disabled = false; } // ============================================================ // HELPERS // ============================================================ function applyLosses(side, count) { const army = side === 'player' ? game.player : game.bot; const total = army.swordsmen.count + army.gunmen.count + army.defenses.count; if (total === 0) return; const ratio = Math.max(0.15, 1 - count / total); army.swordsmen.count = Math.max(1, Math.floor(army.swordsmen.count * ratio)); army.gunmen.count = Math.max(1, Math.floor(army.gunmen.count * ratio)); army.defenses.count = Math.max(0, Math.floor(army.defenses.count * ratio)); if (side === 'player') { if (army.swordsmen.count + army.gunmen.count + army.defenses.count < 3) { army.swordsmen.count = 3; army.gunmen.count = 2; army.defenses.count = 1; addLog('๐Ÿ”„ Reinforcements arrived!', 'system'); } } else { if (army.swordsmen.count + army.gunmen.count + army.defenses.count < 3) { army.swordsmen.count = 3; army.gunmen.count = 2; army.defenses.count = 1; } } } function isPlayerDead() { const p = game.player; return p.swordsmen.count + p.gunmen.count + p.defenses.count === 0; } function isBotDead() { const b = game.bot; return b.swordsmen.count + b.gunmen.count + b.defenses.count === 0; } function addLog(msg, type = 'system') { const entry = document.createElement('div'); entry.className = type; entry.textContent = msg; logArea.appendChild(entry); logArea.scrollTop = logArea.scrollHeight; if (logArea.children.length > 50) logArea.removeChild(logArea.firstChild); } // ============================================================ // UI UPDATE // ============================================================ function updateUI() { goldDisplay.textContent = game.gold; roundDisplay.textContent = game.round; const p = game.player, b = game.bot; pSoldiers.textContent = p.swordsmen.count; pGuns.textContent = p.gunmen.count; pDefenses.textContent = p.defenses.count; bSoldiers.textContent = b.swordsmen.count; bGuns.textContent = b.gunmen.count; bDefenses.textContent = b.defenses.count; soldierLv.textContent = p.swordsmen.level; gunLv.textContent = p.gunmen.level; defenseLv.textContent = p.defenses.level; armyLv.textContent = p.armyLevel; soldierCost.textContent = game.costs.swordsmen; gunCost.textContent = game.costs.gunmen; defenseCost.textContent = game.costs.defenses; armyCost.textContent = game.costs.army; const afford = (c) => game.gold >= c; upgradeSoldiersBtn.disabled = !afford(game.costs.swordsmen); upgradeGunsBtn.disabled = !afford(game.costs.gunmen); upgradeDefensesBtn.disabled = !afford(game.costs.defenses); upgradeArmyBtn.disabled = !afford(game.costs.army); fightBtn.disabled = battleRunning || isPlayerDead() || isBotDead(); updateCampaignUI(); } // ============================================================ // UPGRADES // ============================================================ function upgrade(type) { const cost = game.costs[type]; if (game.gold < cost) return; game.gold -= cost; if (type === 'army') { game.player.armyLevel++; game.player.swordsmen.count += 2; game.player.gunmen.count += 2; game.player.defenses.count += 1; game.costs.army = Math.floor(game.costs.army * 1.7); addLog('๐Ÿ“ˆ Army size increased!', 'system'); } else { const unit = game.player[type]; if (!unit) { addLog(`โŒ Unknown unit type: ${type}`, 'system'); return; } unit.level++; if (type === 'swordsmen') { unit.damage += 3; unit.hp += 6; game.costs.swordsmen = Math.floor(game.costs.swordsmen * 1.6); addLog('โš”๏ธ Swordsmen upgraded!', 'system'); } else if (type === 'gunmen') { unit.damage += 2; unit.hp += 3; game.costs.gunmen = Math.floor(game.costs.gunmen * 1.6); addLog('๐Ÿ”ซ Gunmen upgraded!', 'system'); } else if (type === 'defenses') { unit.hp += 20; game.costs.defenses = Math.floor(game.costs.defenses * 1.6); addLog('๐Ÿฐ Defenses reinforced!', 'system'); } } updateUI(); } // ============================================================ // RESET // ============================================================ function resetGame() { game.gold = 200; game.round = 0; game.level = 0; game.levelWins = 0; game.player.swordsmen = { count: 8, damage: 8, hp: 30, level: 0 }; game.player.gunmen = { count: 6, damage: 5, hp: 18, level: 0 }; game.player.defenses = { count: 4, damage: 0, hp: 45, level: 0 }; game.player.armyLevel = 0; game.bot.swordsmen = { count: 6, damage: 6, hp: 25 }; game.bot.gunmen = { count: 4, damage: 4, hp: 15 }; game.bot.defenses = { count: 3, damage: 0, hp: 40 }; game.bot.techLevel = 0; game.costs = { swordsmen: 40, gunmen: 60, defenses: 100, army: 70 }; battleRunning = false; if (animFrame) cancelAnimationFrame(animFrame); animUnits = []; particles = []; smokeParticles = []; weatherParticles = []; explosions = []; ctx.clearRect(0, 0, W, H); phaseDisplay.textContent = 'โšก Preparation'; logArea.innerHTML = ''; addLog('๐Ÿ”„ Campaign reset. Commander, rebuild your forces.', 'system'); updateUI(); updateCampaignUI(); } // ============================================================ // EVENT LISTENERS // ============================================================ fightBtn.addEventListener('click', startBattle); upgradeSoldiersBtn.addEventListener('click', () => upgrade('swordsmen')); upgradeGunsBtn.addEventListener('click', () => upgrade('gunmen')); upgradeDefensesBtn.addEventListener('click', () => upgrade('defenses')); upgradeArmyBtn.addEventListener('click', () => upgrade('army')); resetBtn.addEventListener('click', resetGame); // ============================================================ // INIT // ============================================================ window.addEventListener('load', function() { generateLandscape(); updateUI(); updateCampaignUI(); const initUnits = createUnitsForArmy(game.player, 'player', 60); const initBot = createUnitsForArmy(game.bot, 'bot', W - 60); animUnits = [...initUnits, ...initBot]; drawBattle(); addLog('๐Ÿ›ก๏ธ Frontline: Tactical Command initialized.', 'system'); addLog(`๐Ÿ“‹ Level ${getLevel().id}: ${getLevel().name} โ€“ ${getLevel().desc}`, 'system'); addLog('๐ŸŽฏ Win 3 battles to advance to the next level.', 'system'); spawnWeather(); });