/**** * Plugins ****/ var storage = LK.import("@upit/storage.v1"); var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Arrow class for castle arrow shooting var Arrow = Container.expand(function () { var self = Container.call(this); // Increase arrow speed for longer distance self.speed = 40; // px per frame (was 32) self.damage = 2 + Math.floor(Math.random() * 3); // 2-4 damage self.target = null; self.team = null; self.destroyed = false; self.asset = null; self.lastX = 0; self.lastY = 0; // Initialize arrow self.init = function (x, y, target, damage, team) { self.x = x; self.y = y; self.target = target; // Arrow damage is randomized 2-4, ignore passed damage param for castle arrows self.damage = 2 + Math.floor(Math.random() * 3); self.team = team; // Remove previous asset if any if (self.asset) { self.removeChild(self.asset); } // Use a visible arrow using a rectangle shape since centerCircle is removed self.asset = self.attachAsset('arrow_shape', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 0.3, tint: team === 'player' ? 0x00aaff : 0xffaa00 // more visible blue/orange }); }; // Update arrow position and check for hit self.update = function () { self.lastX = self.x; self.lastY = self.y; if (!self.target || self.target.destroyed || self.destroyed) { self.destroyed = true; self.destroy(); return; } // Move towards target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 1) dist = 1; var vx = dx / dist * self.speed; var vy = dy / dist * self.speed; self.x += vx; self.y += vy; // Rotate arrow to face target self.asset.rotation = Math.atan2(dy, dx); // Check for collision with target (simple distance check) if (dist < 60) { // Hit! if (typeof self.target.health === "number") { self.target.health -= self.damage; self.target.health = Math.round(self.target.health); // If the target is a soldier and its health drops to 0 or below, destroy it and remove from array if (self.target.health <= 0 && typeof self.target.team === "string") { self.target.destroyed = true; self.target.destroy(); // Remove from correct array if (self.target.team === "enemy" && typeof enemySoldiers !== "undefined") { for (var i = 0; i < enemySoldiers.length; i++) { if (enemySoldiers[i] === self.target) { enemySoldiers.splice(i, 1); break; } } } else if (self.target.team === "player" && typeof playerSoldiers !== "undefined") { for (var i = 0; i < playerSoldiers.length; i++) { if (playerSoldiers[i] === self.target) { playerSoldiers.splice(i, 1); break; } } } } } self.destroyed = true; self.destroy(); } // Remove if out of bounds (double the distance for longer arrows) if (self.x < -600 || self.x > 3200 || self.y < -600 || self.y > 4000) { self.destroyed = true; self.destroy(); } }; return self; }); // Castle class for player and enemy castles var Castle = Container.expand(function () { var self = Container.call(this); // Properties self.team = null; self.health = CASTLE_HEALTH_INIT; self.hasUpgrade = false; self.attack = 0; self.arrowCooldown = 0; self.arrowCooldownMax = 60; // frames between shots self.attackRadius = 500; // px, for arrow shooting self.asset = null; // Initialize castle self.init = function (team, x, y) { self.team = team; self.x = x; self.y = y; self.health = CASTLE_HEALTH_INIT; self.hasUpgrade = false; self.attack = 0; self.arrowCooldown = 0; // Remove previous asset if any if (self.asset) { self.removeChild(self.asset); } // Add correct asset if (team === 'player') { var assetId = 'castle_player'; if (self.hasUpgrade2) { assetId = 'castle_player_3'; } else if (self.hasUpgrade) { assetId = 'castle_player_2'; } self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); } else { var assetId = 'castle_enemy'; if (self.hasUpgrade2 || self.health >= 2500) { assetId = 'castle_enemy_3'; } else if (self.hasUpgrade) { assetId = 'castle_enemy_2'; } self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); } }; // Upgrade castle self.upgrade = function () { self.hasUpgrade = true; self.health = 1500; self.attack = 1 + Math.floor(Math.random() * 3); // 1-3 self.arrowCooldown = 0; // Update asset for first upgrade if (self.asset) { self.removeChild(self.asset); } var assetId = self.team === 'player' ? 'castle_player_2' : 'castle_enemy_2'; self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Second upgrade handled externally (see gamecode.js) }; // Update castle (shoot arrows if upgraded) self.update = function () { // Always round health to integer for display and logic if (typeof self.health === "number") { self.health = Math.round(self.health); } // Switch to castle_enemy_3 asset if health reaches 2500 and not already set if (self.team === 'enemy' && self.health >= 2500 && (!self.asset || self.asset.assetId !== 'castle_enemy_3')) { if (self.asset) { self.removeChild(self.asset); } self.asset = self.attachAsset('castle_enemy_3', { anchorX: 0.5, anchorY: 0.5 }); // Ensure assetId is set for future checks self.asset.assetId = 'castle_enemy_3'; // Immediately update the castleEnemy reference in game if this is the main enemy castle if (typeof castleEnemy !== "undefined" && castleEnemy === self) { castleEnemy.asset = self.asset; } } if (self.hasUpgrade && self.health > 0) { // Only shoot at enemy soldiers in range var targets = []; if (self.team === 'player') { for (var i = 0; i < enemySoldiers.length; i++) { var e = enemySoldiers[i]; var dx = e.x - self.x; var dy = e.y - self.y; if (dx * dx + dy * dy <= self.attackRadius * self.attackRadius) { targets.push(e); } } } else { for (var i = 0; i < playerSoldiers.length; i++) { var e = playerSoldiers[i]; var dx = e.x - self.x; var dy = e.y - self.y; if (dx * dx + dy * dy <= self.attackRadius * self.attackRadius) { targets.push(e); } } } // Shoot at first target if cooldown is ready if (targets.length > 0 && self.arrowCooldown <= 0) { var target = targets[0]; var arrow = new Arrow(); arrow.init(self.x, self.y, target, self.attack, self.team); arrows.push(arrow); game.addChild(arrow); self.arrowCooldown = self.arrowCooldownMax; } if (self.arrowCooldown > 0) self.arrowCooldown--; } }; return self; }); // Soldier class for both player and enemy soldiers var Soldier = Container.expand(function () { var self = Container.call(this); self.team = null; self.health = 100; self.attack = 10; self.speed = SOLDIER_SPEED; self.inCombat = false; self.asset = null; self.lastX = 0; self.lastY = 0; // Initialize soldier self.init = function (team, health, attack) { self.team = team; self.health = health; self.attack = attack; self.inCombat = false; // Remove previous asset if any if (self.asset) { self.removeChild(self.asset); } // Add correct asset if (team === 'player') { self.asset = self.attachAsset('soldier_player', { anchorX: 0.5, anchorY: 0.5 }); } else { self.asset = self.attachAsset('soldier_enemy', { anchorX: 0.5, anchorY: 0.5 }); } // Add health text label above the soldier if (self.healthTxt && typeof self.healthTxt.destroy === "function") { self.healthTxt.destroy(); } self.healthTxt = new Text2(Math.round(self.health) + "", { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); self.healthTxt.anchor.set(0.5, 1.2); self.healthTxt.x = 0; self.healthTxt.y = -90; self.addChild(self.healthTxt); }; // Update soldier position (move forward if not in combat) self.update = function () { self.lastX = self.x; self.lastY = self.y; // Always round health to integer for display and logic if (typeof self.health === "number") { self.health = Math.round(self.health); } // Update health text label if (self.healthTxt) { self.healthTxt.setText(self.health + ""); } if (!self.inCombat) { if (self.team === 'player') { self.x += self.speed; } else { self.x -= self.speed; } } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xBFFF00 }); /**** * Game Code ****/ // --- Add background image --- // Ottoman-inspired church music with kanun, tambourine, zurna melodies function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } var background = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: GAME_W, height: GAME_H }); game.addChild(background); // Play Ottoman-inspired church music with kanun, tambourine, zurna melodies LK.playMusic('ottoman_church_bg'); // --- Game constants --- // Player castle (blue) // Enemy castle (red) // Player soldier (green) // --- Game constants --- var GAME_W = 2048, GAME_H = 2732; var CASTLE_OFFSET_X = 120, CASTLE_OFFSET_Y = GAME_H - 520; // moved up by 200px var CASTLE_HEALTH_INIT = 1000; var GOLD_INIT = 200; var GOLD_PER_TICK = 5; // After enemy 800 gold upgrade, this will be increased for mine-like feature var GOLD_TICK_MS = 200; var SOLDIER_COST = 100; var SOLDIER_HEALTH_MIN = 80, SOLDIER_HEALTH_MAX = 160; var SOLDIER_ATTACK_MIN = 20, SOLDIER_ATTACK_MAX = 50; var SOLDIER_SPEED = 14 / 5 * 0.1 * 4.0; // px per frame (now 100% faster than previous) // --- Speed mode state --- var speedMode = false; // --- Speed mode toggle button (top right, not in topLeft 100x100) --- var speedBtn = new Text2("SPEED x1", { size: 60, fill: "#fff", font: "PressStart2P,Pixel,monospace" }); speedBtn.anchor.set(1, 0); speedBtn.x = LK.gui.width - 40; speedBtn.y = 40; LK.gui.top.addChild(speedBtn); speedBtn.down = function (x, y, obj) { speedMode = !speedMode; speedBtn.setText(speedMode ? "SPEED x3" : "SPEED x1"); }; // --- Difficulty selection --- var difficulty = null; var ENEMY_SOLDIER_COST = 100; // default, will be set by difficulty // Show difficulty selection overlay var diffOverlay = new Container(); diffOverlay.zIndex = 10000; // ensure on top diffOverlay.width = GAME_W; diffOverlay.height = GAME_H; // Use a Text2 as a fake overlay background (solid block) since centerCircle is removed var overlayBg = new Text2(" ", { size: 10, fill: 0x000000 }); overlayBg.width = GAME_W; overlayBg.height = GAME_H; overlayBg.alpha = 0.7; overlayBg.x = 0; overlayBg.y = 0; diffOverlay.addChild(overlayBg); // Difficulty title with black background var diffTitleBg = new Text2(" ", { size: 10, fill: 0x000000 }); diffTitleBg.width = 900; diffTitleBg.height = 160; diffTitleBg.alpha = 0.95; diffTitleBg.anchor.set(0.5, 0.5); diffTitleBg.x = GAME_W / 2; diffTitleBg.y = GAME_H / 2 - 300; diffOverlay.addChild(diffTitleBg); var diffTitle = new Text2("Select Difficulty", { size: 120, fill: "#fff", font: "PressStart2P,Pixel,monospace" }); diffTitle.anchor.set(0.5, 0.5); diffTitle.x = GAME_W / 2; diffTitle.y = GAME_H / 2 - 300; diffOverlay.addChild(diffTitle); // Easy button with black background var btnEasyBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnEasyBg.width = 600; btnEasyBg.height = 120; btnEasyBg.alpha = 0.95; btnEasyBg.anchor.set(0.5, 0.5); btnEasyBg.x = GAME_W / 2; btnEasyBg.y = GAME_H / 2 - 80; diffOverlay.addChild(btnEasyBg); var btnEasy = new Text2("Easy", { size: 100, fill: 0x00FF00, font: "PressStart2P,Pixel,monospace" }); btnEasy.anchor.set(0.5, 0.5); btnEasy.x = GAME_W / 2; btnEasy.y = GAME_H / 2 - 80; diffOverlay.addChild(btnEasy); // Normal button with black background var btnNormalBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnNormalBg.width = 600; btnNormalBg.height = 120; btnNormalBg.alpha = 0.95; btnNormalBg.anchor.set(0.5, 0.5); btnNormalBg.x = GAME_W / 2; btnNormalBg.y = GAME_H / 2 + 80; diffOverlay.addChild(btnNormalBg); var btnNormal = new Text2("Normal", { size: 100, fill: 0xFFFF00, font: "PressStart2P,Pixel,monospace" }); btnNormal.anchor.set(0.5, 0.5); btnNormal.x = GAME_W / 2; btnNormal.y = GAME_H / 2 + 80; diffOverlay.addChild(btnNormal); // Hard button with black background var btnHardBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnHardBg.width = 600; btnHardBg.height = 120; btnHardBg.alpha = 0.95; btnHardBg.anchor.set(0.5, 0.5); btnHardBg.x = GAME_W / 2; btnHardBg.y = GAME_H / 2 + 240; diffOverlay.addChild(btnHardBg); var btnHard = new Text2("Hard", { size: 100, fill: 0xFF0000, font: "PressStart2P,Pixel,monospace" }); btnHard.anchor.set(0.5, 0.5); btnHard.x = GAME_W / 2; btnHard.y = GAME_H / 2 + 240; diffOverlay.addChild(btnHard); // Speed Mode button (below hard) var btnSpeedBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnSpeedBg.width = 600; btnSpeedBg.height = 120; btnSpeedBg.alpha = 0.95; btnSpeedBg.anchor.set(0.5, 0.5); btnSpeedBg.x = GAME_W / 2; btnSpeedBg.y = GAME_H / 2 + 400; diffOverlay.addChild(btnSpeedBg); var btnSpeed = new Text2("Speed Mode: OFF", { size: 80, fill: 0x00E0FF, font: "PressStart2P,Pixel,monospace" }); btnSpeed.anchor.set(0.5, 0.5); btnSpeed.x = GAME_W / 2; btnSpeed.y = GAME_H / 2 + 400; diffOverlay.addChild(btnSpeed); var speedModeMenuSelected = false; btnSpeed.down = function (x, y, obj) { speedModeMenuSelected = !speedModeMenuSelected; btnSpeed.setText(speedModeMenuSelected ? "Speed Mode: ON" : "Speed Mode: OFF"); }; game.addChild(diffOverlay); // Disable game input until difficulty is chosen var gameInputEnabled = false; // Helper to start game with selected difficulty function selectDifficulty(level) { difficulty = level; if (difficulty === "easy") { ENEMY_SOLDIER_COST = SOLDIER_COST; // same as player } else if (difficulty === "normal") { ENEMY_SOLDIER_COST = Math.floor(SOLDIER_COST * 0.9); // 0.9 of player } else if (difficulty === "hard") { ENEMY_SOLDIER_COST = Math.floor(SOLDIER_COST * 0.7); // 0.7 of player } // Enable speed mode if selected in menu if (typeof speedModeMenuSelected !== "undefined" && speedModeMenuSelected) { speedMode = true; if (typeof speedBtn !== "undefined") speedBtn.setText("SPEED x3"); } else { speedMode = false; if (typeof speedBtn !== "undefined") speedBtn.setText("SPEED x1"); } // Set enemy deploy interval based on difficulty if (typeof enemyDeployTimer !== "undefined") { LK.clearInterval(enemyDeployTimer); } var enemyDeployInterval = 600; // default if (difficulty === "easy") { enemyDeployInterval = 900; } else if (difficulty === "normal") { enemyDeployInterval = 700; } else if (difficulty === "hard") { enemyDeployInterval = 500; } enemyDeployTimer = LK.setInterval(function () { if (!gameInputEnabled) return; // --- Initial Phase: 0-30 seconds --- if (LK.ticks < 1800) { // Only send soldiers, no upgrades, no archers. // If player sends soldiers, let enemy build walls. // If more than 3 enemy soldiers alive, let them build walls. // (Blowing up walls is allowed, but not archers or upgrades.) // Only send soldiers if (enemyGold >= ENEMY_SOLDIER_COST) { if (deploySoldier('enemy')) { updateGui(); enemySoldierDeployCount++; enemyTotalSoldierDeployed++; enemySoldierSinceLastWall++; } } // If player has sent soldiers (playerSoldiers.length > 0) or more than 3 enemy soldiers alive, build wall if possible if ((playerSoldiers.length > 0 || enemySoldiers.length > 3) && enemyGold >= 50 && wallEnemies.length === 0) { deployWallEnemy(); } return; } // --- Middle Phase: 30-60 seconds --- if (LK.ticks >= 1800 && LK.ticks < 3600) { // Soldier + Archer + Wall alternately, each as rolling 1-3 dice and send that many // Do not send archers without doing the 1st upgrade // 50% chance to upgrade if enough gold and not immediately if (typeof enemyMiddlePhase === "undefined") { enemyMiddlePhase = { step: 0, upgradeTried: false }; } // 0: soldier, 1: archer, 2: wall, repeat var phaseType = enemyMiddlePhase.step % 3; // Roll 1-3 dice for how many to send var toSend = 1 + Math.floor(Math.random() * 3); if (phaseType === 0) { // Soldier for (var i = 0; i < toSend; i++) { if (enemyGold >= ENEMY_SOLDIER_COST) { if (deploySoldier('enemy')) { updateGui(); enemyTotalSoldierDeployed++; enemySoldierSinceLastWall++; } } } } else if (phaseType === 1) { // Archer if (castleEnemy.hasUpgrade) { for (var i = 0; i < toSend; i++) { if (enemyGold >= 150) { enemyGold -= 150; var s = new Soldier(); var baseSoldierHealth = 25; s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6)); s.speed = SOLDIER_SPEED * 0.8; s.inCombat = false; if (s.asset) { s.removeChild(s.asset); } s.asset = s.attachAsset('archer_enemy', { anchorX: 0.5, anchorY: 0.5 }); s.y = CASTLE_OFFSET_Y + 120; s.x = castleEnemy.x - 120; enemySoldiers.push(s); game.addChild(s); LK.getSound('unit_leave_castle').play(); updateGui(); } } } // If not upgraded, skip archer phase (do nothing) } else if (phaseType === 2) { // Wall for (var i = 0; i < toSend; i++) { if (enemyGold >= 50 && wallEnemies.length === 0) { deployWallEnemy(); } } } enemyMiddlePhase.step++; // 50% chance to upgrade if enough gold and not upgraded yet, but not immediately if (!castleEnemy.hasUpgrade && !enemyMiddlePhase.upgradeTried && enemyGold >= 350) { if (Math.random() < 0.5) { enemyGold -= 350; castleEnemy.upgrade(); updateGui(); } enemyMiddlePhase.upgradeTried = true; } // Second upgrade logic (after player upgrades or 10 units lost) if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) { castleEnemy._upgrade2Started = true; enemyGold -= 800; castleEnemy.hasUpgrade2 = true; castleEnemy.health = 2500; castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6 castleEnemy.arrowCooldownMax = 40; GOLD_PER_TICK = GOLD_PER_TICK + 5; updateGui(); return; } return; } // --- Advanced Phase: 60s+ --- // Upgradeable, soldier + archer + wall alternately, dice-based, archers only after upgrade if (typeof enemyAdvancedPhase === "undefined") { enemyAdvancedPhase = { step: 0 }; } var advType = enemyAdvancedPhase.step % 3; var advToSend = 1 + Math.floor(Math.random() * 3); if (advType === 0) { // Soldier for (var i = 0; i < advToSend; i++) { if (enemyGold >= ENEMY_SOLDIER_COST) { if (deploySoldier('enemy')) { updateGui(); enemyTotalSoldierDeployed++; enemySoldierSinceLastWall++; } } } } else if (advType === 1) { // Archer if (castleEnemy.hasUpgrade) { for (var i = 0; i < advToSend; i++) { if (enemyGold >= 150) { enemyGold -= 150; var s = new Soldier(); var baseSoldierHealth = 25; s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6)); s.speed = SOLDIER_SPEED * 0.8; s.inCombat = false; if (s.asset) { s.removeChild(s.asset); } s.asset = s.attachAsset('archer_enemy', { anchorX: 0.5, anchorY: 0.5 }); s.y = CASTLE_OFFSET_Y + 120; s.x = castleEnemy.x - 120; enemySoldiers.push(s); game.addChild(s); LK.getSound('unit_leave_castle').play(); updateGui(); } } } // If not upgraded, skip archer phase (do nothing) } else if (advType === 2) { // Wall for (var i = 0; i < advToSend; i++) { if (enemyGold >= 50 && wallEnemies.length === 0) { deployWallEnemy(); } } } enemyAdvancedPhase.step++; // Allow upgrades at any time if enough gold and not upgraded if (!castleEnemy.hasUpgrade && enemyGold >= 350) { enemyGold -= 350; castleEnemy.upgrade(); updateGui(); } // Second upgrade logic (after player upgrades or 10 units lost) if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) { castleEnemy._upgrade2Started = true; enemyGold -= 800; castleEnemy.hasUpgrade2 = true; castleEnemy.health = 2500; castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6 castleEnemy.arrowCooldownMax = 40; GOLD_PER_TICK = GOLD_PER_TICK + 5; updateGui(); return; } }, enemyDeployInterval); gameInputEnabled = true; diffOverlay.destroy(); // Show solder button in the middle section (archerBtn will appear after upgrade) archerBtn.visible = false; solderBtn.visible = true; solderBtnLabel.visible = true; wallBtn.visible = true; wallBtnLabel.visible = true; } // Add touch/click handlers for buttons btnEasy.down = function (x, y, obj) { selectDifficulty("easy"); }; btnNormal.down = function (x, y, obj) { selectDifficulty("normal"); }; btnHard.down = function (x, y, obj) { selectDifficulty("hard"); }; // --- State variables --- // (removed, handled by castlePlayer.health and castleEnemy.health) var playerGold = GOLD_INIT; var enemyGold = GOLD_INIT; var playerSoldiers = []; var enemySoldiers = []; // --- Custom score tracking variables --- var scoreStats = { solderEnemyKills: 0, archerEnemyKills: 0, wallEnemyDestroyed: 0, solderPlayerDeaths: 0, archerPlayerDeaths: 0, wallPlayerDestroyed: 0, castlePlayerFullHealth: 0, // 1 if full health at win castlePlayerHealthLost: 0, // amount lost castlePlayerNoDamageGiven: 0 // 1 if no damage given to enemy }; // --- Game over state variables --- var gameOverState = null; // 'win' or 'lose' var gameOverOverlay = null; var gameOverTimer = null; var pointsCalculated = false; var totalPoints = 0; // --- Castles --- var castlePlayer = new Castle(); castlePlayer.init('player', CASTLE_OFFSET_X, CASTLE_OFFSET_Y); game.addChild(castlePlayer); var castleEnemy = new Castle(); castleEnemy.init('enemy', GAME_W - CASTLE_OFFSET_X, CASTLE_OFFSET_Y); game.addChild(castleEnemy); // --- Enemy castle health display above castle --- var castleEnemyHealthTxt = new Text2(castleEnemy.health + '', { size: 60, fill: '#fff', font: "PressStart2P,Pixel,monospace" }); castleEnemyHealthTxt.anchor.set(0.5, 1.2); castleEnemyHealthTxt.x = castleEnemy.x; castleEnemyHealthTxt.y = castleEnemy.y - 260; // above the castle game.addChild(castleEnemyHealthTxt); // --- Player castle unit cost labels above castle --- // --- Player castle upgrade cost label under player castle --- var playerCastleUpgradeTxt = new Text2("YOU NEED 350 GOLD\nTO UPGRADE.", { size: 54, fill: "#000", font: "PressStart2P,Pixel,monospace" }); playerCastleUpgradeTxt.anchor.set(0.5, -0.2); // anchor above the text baseline playerCastleUpgradeTxt.x = castlePlayer.x + 180; playerCastleUpgradeTxt.y = castlePlayer.y + 320; // under the castle game.addChild(playerCastleUpgradeTxt); // --- Player castle second upgrade cost label (800 gold, only after first upgrade) --- var playerCastleUpgrade2Txt = new Text2("YOU NEED 800 GOLD\nTO UPGRADE AGAIN.", { size: 54, fill: "#000", font: "PressStart2P,Pixel,monospace" }); playerCastleUpgrade2Txt.anchor.set(0.5, -0.2); playerCastleUpgrade2Txt.x = castlePlayer.x + 180; playerCastleUpgrade2Txt.y = castlePlayer.y + 320; playerCastleUpgrade2Txt.visible = false; game.addChild(playerCastleUpgrade2Txt); // --- Track second upgrade state for both castles --- castlePlayer.hasUpgrade2 = false; castleEnemy.hasUpgrade2 = false; // After 800 gold upgrade, mine-like feature is enabled for enemy // --- Left side: archer_button and solder_button side by side, visible on screen --- var leftButtonsY = GAME_H / 2 + 100; var leftButtonSpacing = 40; var leftButtonStartX = 120 + 75; // 120px margin + half button width (150/2) // Archer button (leftmost) var archerBtn = LK.getAsset('archer_button', { anchorX: 0.5, anchorY: 0.5, x: leftButtonStartX, y: leftButtonsY }); archerBtn.visible = false; game.addChild(archerBtn); // Add "150 G" label under archerBtn, but only show after castle is upgraded var archerBtnLabel = new Text2("150 G", { size: 48, fill: "#000", font: "PressStart2P,Pixel,monospace" }); archerBtnLabel.anchor.set(0.5, 0); archerBtnLabel.x = archerBtn.x; archerBtnLabel.y = archerBtn.y + 90; // 90px below center of button (button is 150px tall) archerBtnLabel.visible = false; game.addChild(archerBtnLabel); // Solder button (right of archer) var solderBtn = LK.getAsset('solder_button', { anchorX: 0.5, anchorY: 0.5, x: leftButtonStartX + 150 + leftButtonSpacing, // 150 is button width y: leftButtonsY }); solderBtn.visible = false; game.addChild(solderBtn); // Add "100 G" label under solderBtn var solderBtnLabel = new Text2("100 G", { size: 48, fill: "#000", font: "PressStart2P,Pixel,monospace" }); solderBtnLabel.anchor.set(0.5, 0); solderBtnLabel.x = solderBtn.x; solderBtnLabel.y = solderBtn.y + 90; // 90px below center of button (button is 150px tall) solderBtnLabel.visible = false; game.addChild(solderBtnLabel); // Wall button (right of solderBtn) var wallBtn = LK.getAsset('wall_button', { anchorX: 0.5, anchorY: 0.5, x: solderBtn.x + 150 + leftButtonSpacing, y: leftButtonsY }); wallBtn.visible = false; game.addChild(wallBtn); // Add "50 G" label under wallBtn var wallBtnLabel = new Text2("50 G", { size: 48, fill: "#000", font: "PressStart2P,Pixel,monospace" }); wallBtnLabel.anchor.set(0.5, 0); wallBtnLabel.x = wallBtn.x; wallBtnLabel.y = wallBtn.y + 90; wallBtnLabel.visible = false; game.addChild(wallBtnLabel); // --- Healing Tent Button and 250G label (appear after 850 gold upgrade) --- var healingTentBtn = LK.getAsset('HEALING_TENT_KEY_PLAYER', { anchorX: 0.5, anchorY: 0.5, x: wallBtn.x + 150 + leftButtonSpacing, y: leftButtonsY }); healingTentBtn.visible = false; game.addChild(healingTentBtn); var healingTentLabel = new Text2("250 G", { size: 48, fill: "#000", font: "PressStart2P,Pixel,monospace" }); healingTentLabel.anchor.set(0.5, 0); healingTentLabel.x = healingTentBtn.x; healingTentLabel.y = healingTentBtn.y + 90; healingTentLabel.visible = false; game.addChild(healingTentLabel); // --- Healing tent state --- var healingTentPlayer = null; var healingTentTimer = null; var healingTentCountdownTxt = null; // --- Enemy healing tent state --- var healingTentEnemy = null; var healingTentEnemyTimer = null; var healingTentEnemyCountdownTxt = null; var healingTentEnemyActive = false; var healingTentEnemyNextReady = false; var healingTentEnemyCooldownTimer = null; var healingTentEnemyCountdown = 0; var healingTentEnemyBoostActive = false; var healingTentEnemyLastTick = 0; // --- Healing tent purchase and placement --- healingTentBtn.down = function (x, y, obj) { if (!gameInputEnabled) return; if (playerGold < 250) { LK.getSound('dont-click').play(); return; } if (healingTentPlayer) return; // Only one tent at a time playerGold -= 250; updateGui(); // Find position between player castle and wall (or default if no wall) var tentX; var tentY = CASTLE_OFFSET_Y + 120 + 120; // Move tent further down (120px) if (wallPlayers.length > 0) { // Place between castle and first wall var wall = wallPlayers[0]; tentX = (castlePlayer.x + wall.x) / 2; } else { // Place at 1/3 between castle and enemy castle tentX = castlePlayer.x + (castleEnemy.x - castlePlayer.x) / 3; } healingTentPlayer = LK.getAsset('HEALING_TENT_PLAYER', { anchorX: 0.5, anchorY: 0.5, x: tentX, y: tentY }); // Add health text label above healing tent (use 30 as "health" for tent, or just show "30" for 30s) if (healingTentPlayer.healthTxt && typeof healingTentPlayer.healthTxt.destroy === "function") { healingTentPlayer.healthTxt.destroy(); } healingTentPlayer.healthTxt = new Text2("30", { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); healingTentPlayer.healthTxt.anchor.set(0.5, 1.2); healingTentPlayer.healthTxt.x = 0; healingTentPlayer.healthTxt.y = -170; healingTentPlayer.addChild(healingTentPlayer.healthTxt); game.addChild(healingTentPlayer); // Ensure tent is in front of all other units by re-adding as last child if (typeof healingTentPlayer !== "undefined" && _typeof(healingTentPlayer.parent) === "object") { healingTentPlayer.parent.removeChild(healingTentPlayer); game.addChild(healingTentPlayer); } // Remove previous countdown if any if (healingTentCountdownTxt && typeof healingTentCountdownTxt.destroy === "function") { healingTentCountdownTxt.destroy(); } // Create countdown text below tent healingTentCountdownTxt = new Text2("30", { size: 60, fill: 0xFF0000, font: "PressStart2P,Pixel,monospace" }); healingTentCountdownTxt.anchor.set(0.5, 0); healingTentCountdownTxt.x = tentX; healingTentCountdownTxt.y = tentY + 170; // below tent (tent is 300px tall) game.addChild(healingTentCountdownTxt); // Remove previous timer if any if (healingTentTimer) { LK.clearInterval(healingTentTimer); healingTentTimer = null; } var tentCountdown = 30; healingTentCountdownTxt.setText(tentCountdown + ""); healingTentTimer = LK.setInterval(function () { tentCountdown--; if (tentCountdown >= 0) { healingTentCountdownTxt.setText(tentCountdown + ""); if (healingTentPlayer && healingTentPlayer.healthTxt) { healingTentPlayer.healthTxt.setText(tentCountdown + ""); } } if (tentCountdown <= 0) { // Remove tent and countdown if (healingTentPlayer && typeof healingTentPlayer.destroy === "function") { if (healingTentPlayer.healthTxt && typeof healingTentPlayer.healthTxt.destroy === "function") { healingTentPlayer.healthTxt.destroy(); } healingTentPlayer.destroy(); } healingTentPlayer = null; if (healingTentCountdownTxt && typeof healingTentCountdownTxt.destroy === "function") { healingTentCountdownTxt.destroy(); } healingTentCountdownTxt = null; if (healingTentTimer) { LK.clearInterval(healingTentTimer); healingTentTimer = null; } } }, 1000); }; // --- Walls array to track all wall_player objects --- var wallPlayers = []; // --- Walls array to track all wall_enemy objects --- var wallEnemies = []; // Deduct 50 gold from player when wallBtn is pressed wallBtn.down = function (x, y, obj) { if (!gameInputEnabled) return; if (playerGold >= 50) { playerGold -= 50; updateGui(); // Place wall_player in the middle of the two castles var wallX = (castlePlayer.x + castleEnemy.x) / 2; var wallY = CASTLE_OFFSET_Y + 120; // align with soldiers' path var wallPlayer = LK.getAsset('wall_player', { anchorX: 0.5, anchorY: 0.5, x: wallX, y: wallY }); // Wall health: 400 base, +200 per castlePlayer upgrade var wallBaseHealth = 400 + (castlePlayer.hasUpgrade ? 200 : 0) + (castlePlayer.hasUpgrade2 ? 200 : 0); wallPlayer.health = wallBaseHealth; wallPlayer.maxHealth = wallBaseHealth; wallPlayer.destroyed = false; // Add health text label above wall if (wallPlayer.healthTxt && typeof wallPlayer.healthTxt.destroy === "function") { wallPlayer.healthTxt.destroy(); } wallPlayer.healthTxt = new Text2(Math.round(wallPlayer.health) + "", { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); wallPlayer.healthTxt.anchor.set(0.5, 1.2); wallPlayer.healthTxt.x = 0; wallPlayer.healthTxt.y = -140; wallPlayer.addChild(wallPlayer.healthTxt); game.addChild(wallPlayer); wallPlayers.push(wallPlayer); } else { LK.getSound('dont-click').play(); } }; // --- Enemy AI: Deploy wall_enemy if enough gold and no wall exists --- // Track total wall_enemy deployed since game start for enemy 2nd upgrade var enemyWallEnemyDeployCount = 0; function deployWallEnemy() { // Only one wall_enemy at a time if (wallEnemies.length > 0) return false; if (enemyGold < 50) return false; enemyGold -= 50; updateGui(); var wallX = (castlePlayer.x + castleEnemy.x) / 2; var wallY = CASTLE_OFFSET_Y + 120; var wallEnemy = LK.getAsset('wall_enemy', { anchorX: 0.5, anchorY: 0.5, x: wallX, y: wallY }); // Wall health: 400 base, +200 per castleEnemy upgrade var wallBaseHealth = 400 + (castleEnemy.hasUpgrade ? 200 : 0) + (castleEnemy.hasUpgrade2 ? 200 : 0); wallEnemy.health = wallBaseHealth; wallEnemy.maxHealth = wallBaseHealth; wallEnemy.destroyed = false; // Add health text label above wall_enemy if (wallEnemy.healthTxt && typeof wallEnemy.healthTxt.destroy === "function") { wallEnemy.healthTxt.destroy(); } wallEnemy.healthTxt = new Text2(Math.round(wallEnemy.health) + "", { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); wallEnemy.healthTxt.anchor.set(0.5, 1.2); wallEnemy.healthTxt.x = 0; wallEnemy.healthTxt.y = -140; wallEnemy.addChild(wallEnemy.healthTxt); game.addChild(wallEnemy); wallEnemies.push(wallEnemy); // Increment wall_enemy deploy count for enemy enemyWallEnemyDeployCount++; // If enemy has not upgraded, and has not yet started upgrade, and wall_enemy count >= 0, do first upgrade automatically (even if gold goes negative) if (!castleEnemy.hasUpgrade && enemyWallEnemyDeployCount >= 0 && !castleEnemy._upgradeStarted) { castleEnemy._upgradeStarted = true; enemyGold -= 350; castleEnemy.upgrade(); updateGui(); } // If enemy has first upgrade, not yet second, and deployed 15 wall_enemy, do 2nd upgrade automatically (even if gold goes negative) if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && enemyWallEnemyDeployCount >= 15 && !castleEnemy._upgrade2Started) { castleEnemy._upgrade2Started = true; enemyGold -= 800; castleEnemy.hasUpgrade2 = true; // Apply second upgrade effects (same as player) castleEnemy.health = 2500; castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6 castleEnemy.arrowCooldownMax = 40; // faster arrows // --- MINE-LIKE FEATURE: Increase enemy gold income after 2nd upgrade --- GOLD_PER_TICK = GOLD_PER_TICK + 5; // Increase gold income for both, or you can use a separate enemyGoldPerTick if needed updateGui(); } return true; } // --- GUI: Health and Gold displays --- var playerHealthTxt = new Text2('1000', { size: 70, fill: '#fff', font: "PressStart2P,Pixel,monospace" }); playerHealthTxt.anchor.set(0, 0); LK.gui.top.addChild(playerHealthTxt); // Player LIFE label var playerLifeLabel = new Text2('LIFE', { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); playerLifeLabel.anchor.set(0, 0); LK.gui.top.addChild(playerLifeLabel); var playerGoldTxt = new Text2('200', { size: 60, fill: '#ffe600', font: "PressStart2P,Pixel,monospace" }); playerGoldTxt.anchor.set(0, 0); LK.gui.top.addChild(playerGoldTxt); // Add soldier/cavalry cost labels to the left of the tan score // Removed soldier/cavalry cost labels from GUI // Player GOLD label var playerGoldLabel = new Text2('GOLD', { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); playerGoldLabel.anchor.set(0, 0); LK.gui.top.addChild(playerGoldLabel); var enemyHealthTxt = new Text2('1000', { size: 70, fill: '#fff', font: "PressStart2P,Pixel,monospace" }); enemyHealthTxt.anchor.set(1, 0); LK.gui.top.addChild(enemyHealthTxt); // Enemy LIFE label var enemyLifeLabel = new Text2('LIFE', { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); enemyLifeLabel.anchor.set(1, 0); LK.gui.top.addChild(enemyLifeLabel); var enemyGoldTxt = new Text2('200', { size: 60, fill: '#ffe600', font: "PressStart2P,Pixel,monospace" }); enemyGoldTxt.anchor.set(1, 0); LK.gui.top.addChild(enemyGoldTxt); // Enemy GOLD label var enemyGoldLabel = new Text2('GOLD', { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); enemyGoldLabel.anchor.set(1, 0); LK.gui.top.addChild(enemyGoldLabel); // --- Win Counter (Rounds Won) --- // Use storage plugin for persistence var roundsWon = storage.roundsWon || 0; var roundsWonTxt = new Text2('Rounds Won: ' + roundsWon, { size: 60, fill: '#00ff00', font: "PressStart2P,Pixel,monospace" }); roundsWonTxt.anchor.set(1, 0); LK.gui.top.addChild(roundsWonTxt); roundsWonTxt.x = LK.gui.width - 40; roundsWonTxt.y = 220; // Position GUI elements playerHealthTxt.x = 120; playerHealthTxt.y = 40; playerLifeLabel.x = playerHealthTxt.x + playerHealthTxt.width + 18; playerLifeLabel.y = playerHealthTxt.y + 10; playerGoldTxt.x = 120; playerGoldTxt.y = 120; playerGoldLabel.x = playerGoldTxt.x + playerGoldTxt.width + 18; playerGoldLabel.y = playerGoldTxt.y + 8; // (Removed: Position soldier/cavalry cost labels to the left of the tan score) enemyHealthTxt.x = LK.gui.width - 120; enemyHealthTxt.y = 40; enemyLifeLabel.x = enemyHealthTxt.x - enemyHealthTxt.width - 18; enemyLifeLabel.y = enemyHealthTxt.y + 10; enemyGoldTxt.x = LK.gui.width - 120; enemyGoldTxt.y = 120; enemyGoldLabel.x = enemyGoldTxt.x - enemyGoldTxt.width - 18; enemyGoldLabel.y = enemyGoldTxt.y + 8; // --- Gold income timer --- var goldTimer = LK.setInterval(function () { // Only increase gold if difficulty has been chosen if (!gameInputEnabled) return; if (speedMode) { playerGold += GOLD_PER_TICK * 3; enemyGold += GOLD_PER_TICK * 3; } else { playerGold += GOLD_PER_TICK; enemyGold += GOLD_PER_TICK; } updateGui(); }, GOLD_TICK_MS); // --- GUI update function --- function updateGui() { playerHealthTxt.setText(castlePlayer.health); playerLifeLabel.x = playerHealthTxt.x + playerHealthTxt.width + 18; playerLifeLabel.y = playerHealthTxt.y + 10; playerGoldTxt.setText(playerGold); playerGoldLabel.x = playerGoldTxt.x + playerGoldTxt.width + 18; playerGoldLabel.y = playerGoldTxt.y + 8; enemyHealthTxt.setText(castleEnemy.health); enemyLifeLabel.x = enemyHealthTxt.x - enemyHealthTxt.width - 18; enemyLifeLabel.y = enemyHealthTxt.y + 10; enemyGoldTxt.setText(enemyGold); enemyGoldLabel.x = enemyGoldTxt.x - enemyGoldTxt.width - 18; enemyGoldLabel.y = enemyGoldTxt.y + 8; // Update enemy castle health text and position castleEnemyHealthTxt.setText(castleEnemy.health); castleEnemyHealthTxt.x = castleEnemy.x; castleEnemyHealthTxt.y = castleEnemy.y - 260; } // --- Deploy soldier function --- function deploySoldier(team) { // Block deployment if difficulty not chosen if (!gameInputEnabled) return false; var gold = team === 'player' ? playerGold : enemyGold; var cost = team === 'player' ? SOLDIER_COST : ENEMY_SOLDIER_COST; if (gold < cost) return false; // Deduct gold if (team === 'player') playerGold -= SOLDIER_COST;else enemyGold -= ENEMY_SOLDIER_COST; // Fixed health and random attack var health = 25; var attack = 5 + Math.floor(Math.random() * 6); // 5-10 inclusive // Create soldier var s = new Soldier(); s.init(team, health, attack); s.speed = SOLDIER_SPEED; s.inCombat = false; // Position s.y = CASTLE_OFFSET_Y + 120; // Move soldiers' exit position down by 120px if (team === 'player') { s.x = castlePlayer.x + 120; playerSoldiers.push(s); } else { s.x = castleEnemy.x - 120; enemySoldiers.push(s); } game.addChild(s); // Play sound when unit leaves the castle LK.getSound('unit_leave_castle').play(); return true; } // --- Player tap to upgrade castle only (soldier deploy moved to solderBtn.down) --- game.down = function (x, y, obj) { // Play click sound on any touch/click LK.getSound('click').play(); // Block input until difficulty is chosen if (!gameInputEnabled) return; // --- Block all upgrades in first 30 seconds --- // (Removed: No upgrades allowed in first 30 seconds for player) // Check if player clicked on their castle for upgrade var dx = x - castlePlayer.x; var dy = y - castlePlayer.y; if (dx * dx + dy * dy < 250 * 250 && !castlePlayer.hasUpgrade && playerGold >= 350) { playerGold -= 350; castlePlayer.upgrade(); updateGui(); // Show archerBtn after upgrade archerBtn.visible = true; // Show '150 G' label under archerBtn after upgrade archerBtnLabel.visible = true; // Hide the first upgrade text, show the second upgrade text if (playerCastleUpgradeTxt && typeof playerCastleUpgradeTxt.destroy === "function") { playerCastleUpgradeTxt.visible = false; } playerCastleUpgrade2Txt.visible = true; return; } // Second upgrade: only if first upgrade is done, not already done, and enough gold if (dx * dx + dy * dy < 250 * 250 && castlePlayer.hasUpgrade && !castlePlayer.hasUpgrade2 && playerGold >= 800) { playerGold -= 800; castlePlayer.hasUpgrade2 = true; // Apply second upgrade effects castlePlayer.health = 2500; castlePlayer.attack = 4 + Math.floor(Math.random() * 3); // 4-6 castlePlayer.arrowCooldownMax = 40; // faster arrows // Update asset for second upgrade if (castlePlayer.asset) { castlePlayer.removeChild(castlePlayer.asset); } castlePlayer.asset = castlePlayer.attachAsset('castle_player_3', { anchorX: 0.5, anchorY: 0.5 }); updateGui(); // Hide the second upgrade text playerCastleUpgrade2Txt.visible = false; // Show healing tent button and 250G label healingTentBtn.visible = true; healingTentLabel.visible = true; return; } // (Removed: deploySoldier on screen tap) }; // --- Deploy soldier when solderBtn is pressed --- solderBtn.down = function (x, y, obj) { if (!gameInputEnabled) return; if (playerGold >= SOLDIER_COST) { deploySoldier('player'); updateGui(); } else { LK.getSound('dont-click').play(); } }; // --- Deploy archer_player when archerBtn is pressed (uses archer_player asset, cost 10 gold) --- archerBtn.down = function (x, y, obj) { if (!gameInputEnabled) return; if (playerGold >= 150) { playerGold -= 150; // Create a new Soldier but use archer_player asset var s = new Soldier(); // Set archer_player health to be twice that of soldier_player var baseSoldierHealth = 25; s.init('player', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6)); // health 50, attack 5-10 s.speed = SOLDIER_SPEED * 0.8; // Already uses updated SOLDIER_SPEED, now 75% faster s.inCombat = false; // Remove previous asset and attach archer_player asset if (s.asset) { s.removeChild(s.asset); } s.asset = s.attachAsset('archer_player', { anchorX: 0.5, anchorY: 0.5 }); // Position s.y = CASTLE_OFFSET_Y + 120; s.x = castlePlayer.x + 120; playerSoldiers.push(s); game.addChild(s); // Play sound when archer leaves the castle LK.getSound('unit_leave_castle').play(); updateGui(); } else { LK.getSound('dont-click').play(); } }; // --- Enemy AI: let the enemy play like a player: send soldiers, build walls, send archers, upgrade castle, and upgrade again, in a natural order --- // Track enemy AI state var enemySoldierDeployCount = 0; var enemyNextUnit = 'soldier'; // always start with soldier var enemyTotalSoldierDeployed = 0; var enemyUpgradeGoldReserved = 200; var enemySoldierSinceLastWall = 0; castleEnemy.hasUpgrade2 = false; // Track enemy AI upgrade state castleEnemy._upgradeStarted = false; castleEnemy._upgrade2Started = false; // Track archer unlock for enemy (after first upgrade) var enemyArcherUnlocked = false; // Track how many enemy units have been removed (killed by player) var enemyUnitsRemoved = 0; // Patch: Increment enemyUnitsRemoved when enemy soldier is killed (player gets gold for kill) var _oldPlayerSoldierCombat = true; if (!_oldPlayerSoldierCombat) {// never runs, just for context // see player soldier combat loop } // Patch: Increment enemyUnitsRemoved when enemy soldier is killed // (Find the code awarding playerGold += 10 for killing enemy soldier, and increment enemyUnitsRemoved there) var _oldEnemyUnitsRemovedPatch = true; // Enemy AI main loop // --- Add a flag and timer for enemy first upgrade delay --- var enemyFirstUpgradeAllowed = false; LK.setTimeout(function () { enemyFirstUpgradeAllowed = true; }, 15000); var enemyDeployTimer = LK.setInterval(function () { if (!gameInputEnabled) return; // --- Initial Phase: 0-30 seconds --- if (LK.ticks < 1800) { // Only send soldiers, no upgrades, no archers. // If player sends soldiers, let enemy build walls. // If more than 3 enemy soldiers alive, let them build walls. // (Blowing up walls is allowed, but not archers or upgrades.) // Only send soldiers if (enemyGold >= ENEMY_SOLDIER_COST) { if (deploySoldier('enemy')) { updateGui(); enemySoldierDeployCount++; enemyTotalSoldierDeployed++; enemySoldierSinceLastWall++; } } // If player has sent soldiers (playerSoldiers.length > 0) or more than 3 enemy soldiers alive, build wall if possible if ((playerSoldiers.length > 0 || enemySoldiers.length > 3) && enemyGold >= 50 && wallEnemies.length === 0) { deployWallEnemy(); } return; } // --- Middle Phase: 30-60 seconds --- if (LK.ticks >= 1800 && LK.ticks < 3600) { // Soldier + Archer + Wall alternately, each as rolling 1-3 dice and send that many // Do not send archers without doing the 1st upgrade // 50% chance to upgrade if enough gold and not immediately if (typeof enemyMiddlePhase === "undefined") { enemyMiddlePhase = { step: 0, upgradeTried: false }; } // 0: soldier, 1: archer, 2: wall, repeat var phaseType = enemyMiddlePhase.step % 3; // Roll 1-3 dice for how many to send var toSend = 1 + Math.floor(Math.random() * 3); if (phaseType === 0) { // Soldier for (var i = 0; i < toSend; i++) { if (enemyGold >= ENEMY_SOLDIER_COST) { if (deploySoldier('enemy')) { updateGui(); enemyTotalSoldierDeployed++; enemySoldierSinceLastWall++; } } } } else if (phaseType === 1) { // Archer if (castleEnemy.hasUpgrade) { for (var i = 0; i < toSend; i++) { if (enemyGold >= 150) { enemyGold -= 150; var s = new Soldier(); var baseSoldierHealth = 25; s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6)); s.speed = SOLDIER_SPEED * 0.8; s.inCombat = false; if (s.asset) { s.removeChild(s.asset); } s.asset = s.attachAsset('archer_enemy', { anchorX: 0.5, anchorY: 0.5 }); s.y = CASTLE_OFFSET_Y + 120; s.x = castleEnemy.x - 120; enemySoldiers.push(s); game.addChild(s); LK.getSound('unit_leave_castle').play(); updateGui(); } } } // If not upgraded, skip archer phase (do nothing) } else if (phaseType === 2) { // Wall for (var i = 0; i < toSend; i++) { if (enemyGold >= 50 && wallEnemies.length === 0) { deployWallEnemy(); } } } enemyMiddlePhase.step++; // 50% chance to upgrade if enough gold and not upgraded yet, but not immediately if (!castleEnemy.hasUpgrade && !enemyMiddlePhase.upgradeTried && enemyGold >= 350) { if (Math.random() < 0.5) { enemyGold -= 350; castleEnemy.upgrade(); updateGui(); } enemyMiddlePhase.upgradeTried = true; } // Second upgrade logic (after player upgrades or 10 units lost) if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) { castleEnemy._upgrade2Started = true; enemyGold -= 800; castleEnemy.hasUpgrade2 = true; castleEnemy.health = 2500; castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6 castleEnemy.arrowCooldownMax = 40; GOLD_PER_TICK = GOLD_PER_TICK + 5; updateGui(); return; } return; } // --- Advanced Phase: 60s+ --- // Upgradeable, soldier + archer + wall alternately, dice-based, archers only after upgrade if (typeof enemyAdvancedPhase === "undefined") { enemyAdvancedPhase = { step: 0 }; } var advType = enemyAdvancedPhase.step % 3; var advToSend = 1 + Math.floor(Math.random() * 3); if (advType === 0) { // Soldier for (var i = 0; i < advToSend; i++) { if (enemyGold >= ENEMY_SOLDIER_COST) { if (deploySoldier('enemy')) { updateGui(); enemyTotalSoldierDeployed++; enemySoldierSinceLastWall++; } } } } else if (advType === 1) { // Archer if (castleEnemy.hasUpgrade) { for (var i = 0; i < advToSend; i++) { if (enemyGold >= 150) { enemyGold -= 150; var s = new Soldier(); var baseSoldierHealth = 25; s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6)); s.speed = SOLDIER_SPEED * 0.8; s.inCombat = false; if (s.asset) { s.removeChild(s.asset); } s.asset = s.attachAsset('archer_enemy', { anchorX: 0.5, anchorY: 0.5 }); s.y = CASTLE_OFFSET_Y + 120; s.x = castleEnemy.x - 120; enemySoldiers.push(s); game.addChild(s); LK.getSound('unit_leave_castle').play(); updateGui(); } } } // If not upgraded, skip archer phase (do nothing) } else if (advType === 2) { // Wall for (var i = 0; i < advToSend; i++) { if (enemyGold >= 50 && wallEnemies.length === 0) { deployWallEnemy(); } } } enemyAdvancedPhase.step++; // Allow upgrades at any time if enough gold and not upgraded if (!castleEnemy.hasUpgrade && enemyGold >= 350) { enemyGold -= 350; castleEnemy.upgrade(); updateGui(); } // Second upgrade logic (after player upgrades or 10 units lost) if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) { castleEnemy._upgrade2Started = true; enemyGold -= 800; castleEnemy.hasUpgrade2 = true; castleEnemy.health = 2500; castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6 castleEnemy.arrowCooldownMax = 40; GOLD_PER_TICK = GOLD_PER_TICK + 5; updateGui(); return; } }, 600); // --- Arrows array --- var arrows = []; // --- Main update loop --- game.update = function () { // --- Update castles (health, arrows) --- castlePlayer.update(); castleEnemy.update(); // --- Enemy healing tent logic --- // Only allow after enemy's 2nd upgrade if (castleEnemy.hasUpgrade2) { // Start the healing tent cycle if not already started if (!healingTentEnemyCooldownTimer && !healingTentEnemyActive) { healingTentEnemyNextReady = true; healingTentEnemyCountdown = 0; healingTentEnemyLastTick = LK.ticks; healingTentEnemyCooldownTimer = LK.setInterval(function () { if (!healingTentEnemyActive && castleEnemy.hasUpgrade2) { healingTentEnemyCountdown++; if (healingTentEnemyCountdown >= 100) { // 100 seconds healingTentEnemyNextReady = true; healingTentEnemyCountdown = 0; } } }, 1000); } // If ready and not active, spawn the tent if (healingTentEnemyNextReady && !healingTentEnemyActive) { // Place tent between enemy castle and wall (or default if no wall) var tentX; var tentY = CASTLE_OFFSET_Y + 120 + 120; if (wallEnemies.length > 0) { var wall = wallEnemies[0]; tentX = (castleEnemy.x + wall.x) / 2; } else { tentX = castleEnemy.x - (castleEnemy.x - castlePlayer.x) / 3; } healingTentEnemy = LK.getAsset('HEALING_TENT_ENEMY', { anchorX: 0.5, anchorY: 0.5, x: tentX, y: tentY }); // Add health text label above healing tent (use 30 as "health" for tent, or just show "30" for 30s) if (healingTentEnemy.healthTxt && typeof healingTentEnemy.healthTxt.destroy === "function") { healingTentEnemy.healthTxt.destroy(); } healingTentEnemy.healthTxt = new Text2("30", { size: 38, fill: "#000", font: "PressStart2P,Pixel,monospace" }); healingTentEnemy.healthTxt.anchor.set(0.5, 1.2); healingTentEnemy.healthTxt.x = 0; healingTentEnemy.healthTxt.y = -170; healingTentEnemy.addChild(healingTentEnemy.healthTxt); game.addChild(healingTentEnemy); // Ensure tent is in front of all other units by re-adding as last child if (typeof healingTentEnemy !== "undefined" && healingTentEnemy.parent) { healingTentEnemy.parent.removeChild(healingTentEnemy); game.addChild(healingTentEnemy); } // Remove previous countdown if any if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") { healingTentEnemyCountdownTxt.destroy(); } // Create countdown text below tent healingTentEnemyCountdownTxt = new Text2("30", { size: 60, fill: 0xFF0000, font: "PressStart2P,Pixel,monospace" }); healingTentEnemyCountdownTxt.anchor.set(0.5, 0); healingTentEnemyCountdownTxt.x = tentX; healingTentEnemyCountdownTxt.y = tentY + 170; game.addChild(healingTentEnemyCountdownTxt); // Remove previous timer if any if (healingTentEnemyTimer) { LK.clearInterval(healingTentEnemyTimer); healingTentEnemyTimer = null; } var tentCountdown = 30; healingTentEnemyCountdownTxt.setText(tentCountdown + ""); healingTentEnemyActive = true; healingTentEnemyBoostActive = true; healingTentEnemyTimer = LK.setInterval(function () { tentCountdown--; if (tentCountdown >= 0) { healingTentEnemyCountdownTxt.setText(tentCountdown + ""); if (healingTentEnemy && healingTentEnemy.healthTxt) { healingTentEnemy.healthTxt.setText(tentCountdown + ""); } } if (tentCountdown <= 0) { // Remove tent and countdown if (healingTentEnemy && typeof healingTentEnemy.destroy === "function") { if (healingTentEnemy.healthTxt && typeof healingTentEnemy.healthTxt.destroy === "function") { healingTentEnemy.healthTxt.destroy(); } healingTentEnemy.destroy(); } healingTentEnemy = null; if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") { healingTentEnemyCountdownTxt.destroy(); } healingTentEnemyCountdownTxt = null; if (healingTentEnemyTimer) { LK.clearInterval(healingTentEnemyTimer); healingTentEnemyTimer = null; } healingTentEnemyActive = false; healingTentEnemyBoostActive = false; healingTentEnemyNextReady = false; healingTentEnemyCountdown = 0; } }, 1000); } } else { // If enemy loses 2nd upgrade, clear timers and tent if (healingTentEnemyCooldownTimer) { LK.clearInterval(healingTentEnemyCooldownTimer); healingTentEnemyCooldownTimer = null; } if (healingTentEnemyTimer) { LK.clearInterval(healingTentEnemyTimer); healingTentEnemyTimer = null; } if (healingTentEnemy && typeof healingTentEnemy.destroy === "function") { if (healingTentEnemy.healthTxt && typeof healingTentEnemy.healthTxt.destroy === "function") { healingTentEnemy.healthTxt.destroy(); } healingTentEnemy.destroy(); } healingTentEnemy = null; if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") { healingTentEnemyCountdownTxt.destroy(); } healingTentEnemyCountdownTxt = null; healingTentEnemyActive = false; healingTentEnemyBoostActive = false; healingTentEnemyNextReady = false; healingTentEnemyCountdown = 0; } // --- Apply healing tent effect: 15% health boost to all player units while tent is present --- if (healingTentPlayer) { // Always keep healing tent in front of all units, even those created after it if (healingTentPlayer.parent) { healingTentPlayer.parent.removeChild(healingTentPlayer); game.addChild(healingTentPlayer); } for (var i = 0; i < playerSoldiers.length; i++) { var s = playerSoldiers[i]; if (!s._healingTentBoosted) { s.health = Math.ceil(s.health * 1.15); s._healingTentBoosted = true; } } } else { // Remove boost flag if tent is gone (do not revert health, only allow boost once per tent appearance) for (var i = 0; i < playerSoldiers.length; i++) { var s = playerSoldiers[i]; if (s._healingTentBoosted) { s._healingTentBoosted = false; } } } // --- Apply healing tent effect: 15% health boost to all enemy units while enemy tent is present --- if (healingTentEnemyBoostActive) { // Always keep healing tent in front of all units, even those created after it if (healingTentEnemy && healingTentEnemy.parent) { healingTentEnemy.parent.removeChild(healingTentEnemy); game.addChild(healingTentEnemy); } for (var i = 0; i < enemySoldiers.length; i++) { var s = enemySoldiers[i]; if (!s._healingTentEnemyBoosted) { s.health = Math.ceil(s.health * 1.15); s._healingTentEnemyBoosted = true; } } } else { // Remove boost flag if tent is gone (do not revert health, only allow boost once per tent appearance) for (var i = 0; i < enemySoldiers.length; i++) { var s = enemySoldiers[i]; if (s._healingTentEnemyBoosted) { s._healingTentEnemyBoosted = false; } } } // --- Player soldiers --- for (var i = playerSoldiers.length - 1; i >= 0; i--) { var s = playerSoldiers[i]; s.inCombat = false; // --- Check for wall_enemy collision --- var wallEngaged = false; for (var w = 0; w < wallEnemies.length; w++) { var wall = wallEnemies[w]; if (wall.destroyed) continue; // Simple collision: check if close enough horizontally and vertically if (Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) { s.inCombat = true; wallEngaged = true; // Player attacks wall only if at least 2 player soldiers are engaging this wall if (LK.ticks % 12 === 0) { var engagedCount = 0; for (var ps = 0; ps < playerSoldiers.length; ps++) { var psold = playerSoldiers[ps]; if (Math.abs(psold.x - wall.x) < 80 && Math.abs(psold.y - wall.y) < 180 && !psold.destroyed) { engagedCount++; } } if (engagedCount >= 2) { wall.health -= s.attack; if (typeof wall.health === "number") { wall.health = Math.round(wall.health); } } } // If wall destroyed, remove from game and array if (wall.healthTxt) { wall.healthTxt.setText(Math.max(0, Math.round(wall.health)) + ""); } if (wall.health <= 0) { scoreStats.wallEnemyDestroyed++; wall.destroyed = true; if (wall.healthTxt && typeof wall.healthTxt.destroy === "function") { wall.healthTxt.destroy(); } if (typeof wall.destroy === "function") wall.destroy(); wallEnemies.splice(w, 1); w--; } break; // Only engage one wall at a time } } if (wallEngaged) continue; // Check for enemy soldier in range var engaged = false; // Prevent player soldiers from damaging enemy units if any wall_enemy is present and not destroyed and player soldier is engaged with it var wallEnemyBlocking = false; for (var w = 0; w < wallEnemies.length; w++) { var wall = wallEnemies[w]; if (!wall.destroyed && Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) { wallEnemyBlocking = true; break; } } for (var j = 0; j < enemySoldiers.length; j++) { var e = enemySoldiers[j]; // If close enough (overlap) if (Math.abs(s.x - e.x) < 80) { // Engage in combat s.inCombat = true; e.inCombat = true; // Both attack each other if (LK.ticks % 12 === 0) { // Only allow damage if not blocked by wall_enemy if (!wallEnemyBlocking) { // Attack every 12 frames (~5 times/sec) e.health -= s.attack; s.health -= e.attack; } } // Remove dead soldiers if (e.health <= 0) { // Determine if enemy is archer or soldier if (e.asset && e.asset.assetId === 'archer_enemy') { scoreStats.archerEnemyKills++; } else { scoreStats.solderEnemyKills++; } e.destroy(); enemySoldiers.splice(j, 1); j--; // Award 10 gold to player for killing enemy soldier playerGold += 10; enemyUnitsRemoved = (typeof enemyUnitsRemoved === "number" ? enemyUnitsRemoved : 0) + 1; // increment removed count updateGui(); } if (s.health <= 0) { // Determine if player is archer or soldier if (s.asset && s.asset.assetId === 'archer_player') { scoreStats.archerPlayerDeaths++; } else { scoreStats.solderPlayerDeaths++; } s.destroy(); playerSoldiers.splice(i, 1); i--; // Award 10 gold to enemy for killing player soldier enemyGold += 10; updateGui(); engaged = true; break; } engaged = true; break; } } if (engaged) continue; // If not in combat, check if at enemy castle if (Math.abs(s.x - castleEnemy.x) < 120) { s.inCombat = true; if (LK.ticks % 12 === 0) { castleEnemy.health -= s.attack; if (castleEnemy.health < 0) castleEnemy.health = 0; updateGui(); } // Remove soldier if castle destroyed if (castleEnemy.health <= 0) { s.destroy(); playerSoldiers.splice(i, 1); i--; } } } // --- Enemy soldiers --- for (var i = enemySoldiers.length - 1; i >= 0; i--) { var s = enemySoldiers[i]; s.inCombat = false; // --- Check for wall_player collision --- var wallEngaged = false; for (var w = 0; w < wallPlayers.length; w++) { var wall = wallPlayers[w]; if (wall.destroyed) continue; // Simple collision: check if close enough horizontally and vertically if (Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) { s.inCombat = true; wallEngaged = true; // Enemy attacks wall only if at least 2 enemy soldiers are engaging this wall if (LK.ticks % 12 === 0) { var engagedCount = 0; for (var es = 0; es < enemySoldiers.length; es++) { var esold = enemySoldiers[es]; if (Math.abs(esold.x - wall.x) < 80 && Math.abs(esold.y - wall.y) < 180 && !esold.destroyed) { engagedCount++; } } if (engagedCount >= 2) { wall.health -= s.attack; if (typeof wall.health === "number") { wall.health = Math.round(wall.health); } } } // If wall destroyed, remove from game and array if (wall.healthTxt) { wall.healthTxt.setText(Math.max(0, Math.round(wall.health)) + ""); } if (wall.health <= 0) { scoreStats.wallPlayerDestroyed++; wall.destroyed = true; if (wall.healthTxt && typeof wall.healthTxt.destroy === "function") { wall.healthTxt.destroy(); } if (typeof wall.destroy === "function") wall.destroy(); wallPlayers.splice(w, 1); w--; } break; // Only engage one wall at a time } } if (wallEngaged) continue; // Check for player soldier in range var engaged = false; // Prevent enemy soldiers from damaging player units if any wall_player is present and not destroyed and enemy soldier is engaged with it var wallPlayerBlocking = false; for (var w = 0; w < wallPlayers.length; w++) { var wall = wallPlayers[w]; if (!wall.destroyed && Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) { wallPlayerBlocking = true; break; } } for (var j = 0; j < playerSoldiers.length; j++) { var e = playerSoldiers[j]; if (Math.abs(s.x - e.x) < 80) { s.inCombat = true; e.inCombat = true; if (LK.ticks % 12 === 0) { // Only allow damage if not blocked by wall_player if (!wallPlayerBlocking) { e.health -= s.attack; s.health -= e.attack; } } if (e.health <= 0) { e.destroy(); playerSoldiers.splice(j, 1); j--; // Award 10 gold to enemy for killing player soldier enemyGold += 10; updateGui(); } if (s.health <= 0) { s.destroy(); enemySoldiers.splice(i, 1); i--; // Award 10 gold to player for killing enemy soldier playerGold += 10; updateGui(); engaged = true; break; } engaged = true; break; } } if (engaged) continue; // If not in combat, check if at player castle if (Math.abs(s.x - castlePlayer.x) < 120) { s.inCombat = true; if (LK.ticks % 12 === 0) { castlePlayer.health -= s.attack; if (castlePlayer.health < 0) castlePlayer.health = 0; updateGui(); } if (castlePlayer.health <= 0) { s.destroy(); enemySoldiers.splice(i, 1); i--; } } } // --- Move soldiers --- for (var i = 0; i < playerSoldiers.length; i++) { playerSoldiers[i].update(); } for (var i = 0; i < enemySoldiers.length; i++) { enemySoldiers[i].update(); } // --- Update arrows --- for (var i = arrows.length - 1; i >= 0; i--) { var a = arrows[i]; a.update(); if (a.destroyed) { arrows.splice(i, 1); } } // --- Check win/lose --- if (castlePlayer.health <= 0 && !gameOverState) { gameOverState = 'lose'; showCustomGameOver(); return; } if (castleEnemy.health <= 0 && !gameOverState) { gameOverState = 'win'; // --- Custom score stats for win --- scoreStats.castlePlayerFullHealth = castlePlayer.health === CASTLE_HEALTH_INIT || castlePlayer.health === 2500 ? 1 : 0; scoreStats.castlePlayerHealthLost = castlePlayer.health < (castlePlayer.hasUpgrade2 ? 2500 : CASTLE_HEALTH_INIT) ? (castlePlayer.hasUpgrade2 ? 2500 : CASTLE_HEALTH_INIT) - castlePlayer.health : 0; scoreStats.castlePlayerNoDamageGiven = castlePlayer.health === (castlePlayer.hasUpgrade2 ? 2500 : CASTLE_HEALTH_INIT) && scoreStats.solderPlayerDeaths === 0 && scoreStats.archerPlayerDeaths === 0 && scoreStats.wallPlayerDestroyed === 0 ? 1 : 0; // Increase and persist rounds won roundsWon = (storage.roundsWon || 0) + 1; storage.roundsWon = roundsWon; roundsWonTxt.setText('Rounds Won: ' + roundsWon); showCustomGameOver(); return; } }; // --- Custom game over function --- function showCustomGameOver() { // Create overlay gameOverOverlay = new Container(); gameOverOverlay.zIndex = 20000; // Background var overlayBg = new Text2(" ", { size: 10, fill: 0x000000 }); overlayBg.width = GAME_W; overlayBg.height = GAME_H; overlayBg.alpha = 0.85; overlayBg.x = 0; overlayBg.y = 0; gameOverOverlay.addChild(overlayBg); // Main message var mainMessage; if (gameOverState === 'win') { mainMessage = new Text2("YOU WON THE GAME", { size: 120, fill: 0x00FF00, font: "PressStart2P,Pixel,monospace" }); LK.effects.flashScreen(0x00ff00, 1000); } else { mainMessage = new Text2("YOU LOST THE GAME", { size: 120, fill: 0xFF0000, font: "PressStart2P,Pixel,monospace" }); LK.effects.flashScreen(0xff0000, 1000); } mainMessage.anchor.set(0.5, 0.5); mainMessage.x = GAME_W / 2; mainMessage.y = GAME_H / 2 - 200; gameOverOverlay.addChild(mainMessage); game.addChild(gameOverOverlay); // Start timer sequence gameOverTimer = LK.setTimeout(function () { if (gameOverState === 'win') { showPointsEarned(); } else { // For lose, wait 5 seconds then go to login screen gameOverTimer = LK.setTimeout(function () { goToLoginScreen(); }, 5000); } }, 5000); } function showPointsEarned() { if (pointsCalculated) return; pointsCalculated = true; // Calculate points totalPoints = 0; totalPoints += scoreStats.solderEnemyKills * 10; totalPoints += scoreStats.archerEnemyKills * 25; totalPoints += scoreStats.wallEnemyDestroyed * 50; if (scoreStats.solderPlayerDeaths < 50) totalPoints += 1000; if (scoreStats.archerPlayerDeaths < 50) totalPoints += 3000; if (scoreStats.wallPlayerDestroyed < 20) totalPoints += 2000; if (scoreStats.castlePlayerFullHealth) totalPoints += 5000; if (scoreStats.castlePlayerHealthLost > 0) totalPoints += scoreStats.castlePlayerHealthLost; if (scoreStats.castlePlayerNoDamageGiven) totalPoints += 10000; // Store total points var allTimePoints = storage.allTimePoints || 0; allTimePoints += totalPoints; storage.allTimePoints = allTimePoints; // Create points text var pointsText = new Text2("POINTS EARNED: " + totalPoints, { size: 100, fill: 0xFFD700, font: "PressStart2P,Pixel,monospace" }); pointsText.anchor.set(0.5, 0.5); pointsText.x = GAME_W / 2; pointsText.y = GAME_H / 2 + 100; pointsText.alpha = 0; gameOverOverlay.addChild(pointsText); // Animate points in tween(pointsText, { alpha: 1 }, { duration: 1000 }); // Wait 5 seconds then go to login gameOverTimer = LK.setTimeout(function () { goToLoginScreen(); }, 5000); } function goToLoginScreen() { // Wait 5 seconds, then reset and return to difficulty selection LK.setTimeout(function () { // Reset all game state variables gameOverState = null; pointsCalculated = false; totalPoints = 0; difficulty = null; gameInputEnabled = false; speedMode = false; speedModeMenuSelected = false; // Reset castle states castlePlayer.health = CASTLE_HEALTH_INIT; castlePlayer.hasUpgrade = false; castlePlayer.hasUpgrade2 = false; castlePlayer._upgradeStarted = false; castlePlayer._upgrade2Started = false; castleEnemy.health = CASTLE_HEALTH_INIT; castleEnemy.hasUpgrade = false; castleEnemy.hasUpgrade2 = false; castleEnemy._upgradeStarted = false; castleEnemy._upgrade2Started = false; // Reset gold playerGold = GOLD_INIT; enemyGold = GOLD_INIT; GOLD_PER_TICK = 5; // Clear all soldiers and arrows for (var i = 0; i < playerSoldiers.length; i++) { if (playerSoldiers[i] && typeof playerSoldiers[i].destroy === "function") { playerSoldiers[i].destroy(); } } for (var i = 0; i < enemySoldiers.length; i++) { if (enemySoldiers[i] && typeof enemySoldiers[i].destroy === "function") { enemySoldiers[i].destroy(); } } for (var i = 0; i < arrows.length; i++) { if (arrows[i] && typeof arrows[i].destroy === "function") { arrows[i].destroy(); } } playerSoldiers = []; enemySoldiers = []; arrows = []; // Clear walls for (var i = 0; i < wallPlayers.length; i++) { if (wallPlayers[i] && typeof wallPlayers[i].destroy === "function") { wallPlayers[i].destroy(); } } for (var i = 0; i < wallEnemies.length; i++) { if (wallEnemies[i] && typeof wallEnemies[i].destroy === "function") { wallEnemies[i].destroy(); } } wallPlayers = []; wallEnemies = []; // Clear healing tents and timers if (healingTentPlayer && typeof healingTentPlayer.destroy === "function") { healingTentPlayer.destroy(); } if (healingTentEnemy && typeof healingTentEnemy.destroy === "function") { healingTentEnemy.destroy(); } if (healingTentCountdownTxt && typeof healingTentCountdownTxt.destroy === "function") { healingTentCountdownTxt.destroy(); } if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") { healingTentEnemyCountdownTxt.destroy(); } if (healingTentTimer) { LK.clearInterval(healingTentTimer); healingTentTimer = null; } if (healingTentEnemyTimer) { LK.clearInterval(healingTentEnemyTimer); healingTentEnemyTimer = null; } if (healingTentEnemyCooldownTimer) { LK.clearInterval(healingTentEnemyCooldownTimer); healingTentEnemyCooldownTimer = null; } healingTentPlayer = null; healingTentEnemy = null; healingTentCountdownTxt = null; healingTentEnemyCountdownTxt = null; healingTentEnemyActive = false; healingTentEnemyNextReady = false; healingTentEnemyCountdown = 0; healingTentEnemyBoostActive = false; // Reset enemy AI variables enemySoldierDeployCount = 0; enemyTotalSoldierDeployed = 0; enemyUnitsRemoved = 0; enemySoldierSinceLastWall = 0; enemyWallEnemyDeployCount = 0; if (typeof enemyMiddlePhase !== "undefined") { enemyMiddlePhase = undefined; } if (typeof enemyAdvancedPhase !== "undefined") { enemyAdvancedPhase = undefined; } // Reset castle assets castlePlayer.init('player', CASTLE_OFFSET_X, CASTLE_OFFSET_Y); castleEnemy.init('enemy', GAME_W - CASTLE_OFFSET_X, CASTLE_OFFSET_Y); // Reset button visibility archerBtn.visible = false; archerBtnLabel.visible = false; solderBtn.visible = false; solderBtnLabel.visible = false; wallBtn.visible = false; wallBtnLabel.visible = false; healingTentBtn.visible = false; healingTentLabel.visible = false; // Reset upgrade text visibility playerCastleUpgradeTxt.visible = true; playerCastleUpgrade2Txt.visible = false; // Reset score stats scoreStats = { solderEnemyKills: 0, archerEnemyKills: 0, wallEnemyDestroyed: 0, solderPlayerDeaths: 0, archerPlayerDeaths: 0, wallPlayerDestroyed: 0, castlePlayerFullHealth: 0, castlePlayerHealthLost: 0, castlePlayerNoDamageGiven: 0 }; // Clear game over overlays if (gameOverOverlay && typeof gameOverOverlay.destroy === "function") { gameOverOverlay.destroy(); gameOverOverlay = null; } if (gameOverTimer) { LK.clearTimeout(gameOverTimer); gameOverTimer = null; } // Clear enemy deploy timer if (enemyDeployTimer) { LK.clearInterval(enemyDeployTimer); } // Update GUI updateGui(); // Recreate and show difficulty selection overlay diffOverlay = new Container(); diffOverlay.zIndex = 10000; diffOverlay.width = GAME_W; diffOverlay.height = GAME_H; // Background overlayBg = new Text2(" ", { size: 10, fill: 0x000000 }); overlayBg.width = GAME_W; overlayBg.height = GAME_H; overlayBg.alpha = 0.7; overlayBg.x = 0; overlayBg.y = 0; diffOverlay.addChild(overlayBg); // Recreate all difficulty selection elements diffTitleBg = new Text2(" ", { size: 10, fill: 0x000000 }); diffTitleBg.width = 900; diffTitleBg.height = 160; diffTitleBg.alpha = 0.95; diffTitleBg.anchor.set(0.5, 0.5); diffTitleBg.x = GAME_W / 2; diffTitleBg.y = GAME_H / 2 - 300; diffOverlay.addChild(diffTitleBg); diffTitle = new Text2("Select Difficulty", { size: 120, fill: "#fff", font: "PressStart2P,Pixel,monospace" }); diffTitle.anchor.set(0.5, 0.5); diffTitle.x = GAME_W / 2; diffTitle.y = GAME_H / 2 - 300; diffOverlay.addChild(diffTitle); // Easy button btnEasyBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnEasyBg.width = 600; btnEasyBg.height = 120; btnEasyBg.alpha = 0.95; btnEasyBg.anchor.set(0.5, 0.5); btnEasyBg.x = GAME_W / 2; btnEasyBg.y = GAME_H / 2 - 80; diffOverlay.addChild(btnEasyBg); btnEasy = new Text2("Easy", { size: 100, fill: 0x00FF00, font: "PressStart2P,Pixel,monospace" }); btnEasy.anchor.set(0.5, 0.5); btnEasy.x = GAME_W / 2; btnEasy.y = GAME_H / 2 - 80; btnEasy.down = function (x, y, obj) { selectDifficulty("easy"); }; diffOverlay.addChild(btnEasy); // Normal button btnNormalBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnNormalBg.width = 600; btnNormalBg.height = 120; btnNormalBg.alpha = 0.95; btnNormalBg.anchor.set(0.5, 0.5); btnNormalBg.x = GAME_W / 2; btnNormalBg.y = GAME_H / 2 + 80; diffOverlay.addChild(btnNormalBg); btnNormal = new Text2("Normal", { size: 100, fill: 0xFFFF00, font: "PressStart2P,Pixel,monospace" }); btnNormal.anchor.set(0.5, 0.5); btnNormal.x = GAME_W / 2; btnNormal.y = GAME_H / 2 + 80; btnNormal.down = function (x, y, obj) { selectDifficulty("normal"); }; diffOverlay.addChild(btnNormal); // Hard button btnHardBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnHardBg.width = 600; btnHardBg.height = 120; btnHardBg.alpha = 0.95; btnHardBg.anchor.set(0.5, 0.5); btnHardBg.x = GAME_W / 2; btnHardBg.y = GAME_H / 2 + 240; diffOverlay.addChild(btnHardBg); btnHard = new Text2("Hard", { size: 100, fill: 0xFF0000, font: "PressStart2P,Pixel,monospace" }); btnHard.anchor.set(0.5, 0.5); btnHard.x = GAME_W / 2; btnHard.y = GAME_H / 2 + 240; btnHard.down = function (x, y, obj) { selectDifficulty("hard"); }; diffOverlay.addChild(btnHard); // Speed Mode button btnSpeedBg = new Text2(" ", { size: 10, fill: 0x000000 }); btnSpeedBg.width = 600; btnSpeedBg.height = 120; btnSpeedBg.alpha = 0.95; btnSpeedBg.anchor.set(0.5, 0.5); btnSpeedBg.x = GAME_W / 2; btnSpeedBg.y = GAME_H / 2 + 400; diffOverlay.addChild(btnSpeedBg); btnSpeed = new Text2("Speed Mode: OFF", { size: 80, fill: 0x00E0FF, font: "PressStart2P,Pixel,monospace" }); btnSpeed.anchor.set(0.5, 0.5); btnSpeed.x = GAME_W / 2; btnSpeed.y = GAME_H / 2 + 400; btnSpeed.down = function (x, y, obj) { speedModeMenuSelected = !speedModeMenuSelected; btnSpeed.setText(speedModeMenuSelected ? "Speed Mode: ON" : "Speed Mode: OFF"); }; diffOverlay.addChild(btnSpeed); game.addChild(diffOverlay); }, 5000); } // --- Clean up timers on game over --- game.destroy = function () { LK.clearInterval(goldTimer); LK.clearInterval(enemyDeployTimer); };
/****
* Plugins
****/
var storage = LK.import("@upit/storage.v1");
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Arrow class for castle arrow shooting
var Arrow = Container.expand(function () {
var self = Container.call(this);
// Increase arrow speed for longer distance
self.speed = 40; // px per frame (was 32)
self.damage = 2 + Math.floor(Math.random() * 3); // 2-4 damage
self.target = null;
self.team = null;
self.destroyed = false;
self.asset = null;
self.lastX = 0;
self.lastY = 0;
// Initialize arrow
self.init = function (x, y, target, damage, team) {
self.x = x;
self.y = y;
self.target = target;
// Arrow damage is randomized 2-4, ignore passed damage param for castle arrows
self.damage = 2 + Math.floor(Math.random() * 3);
self.team = team;
// Remove previous asset if any
if (self.asset) {
self.removeChild(self.asset);
}
// Use a visible arrow using a rectangle shape since centerCircle is removed
self.asset = self.attachAsset('arrow_shape', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 0.3,
tint: team === 'player' ? 0x00aaff : 0xffaa00 // more visible blue/orange
});
};
// Update arrow position and check for hit
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
if (!self.target || self.target.destroyed || self.destroyed) {
self.destroyed = true;
self.destroy();
return;
}
// Move towards target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 1) dist = 1;
var vx = dx / dist * self.speed;
var vy = dy / dist * self.speed;
self.x += vx;
self.y += vy;
// Rotate arrow to face target
self.asset.rotation = Math.atan2(dy, dx);
// Check for collision with target (simple distance check)
if (dist < 60) {
// Hit!
if (typeof self.target.health === "number") {
self.target.health -= self.damage;
self.target.health = Math.round(self.target.health);
// If the target is a soldier and its health drops to 0 or below, destroy it and remove from array
if (self.target.health <= 0 && typeof self.target.team === "string") {
self.target.destroyed = true;
self.target.destroy();
// Remove from correct array
if (self.target.team === "enemy" && typeof enemySoldiers !== "undefined") {
for (var i = 0; i < enemySoldiers.length; i++) {
if (enemySoldiers[i] === self.target) {
enemySoldiers.splice(i, 1);
break;
}
}
} else if (self.target.team === "player" && typeof playerSoldiers !== "undefined") {
for (var i = 0; i < playerSoldiers.length; i++) {
if (playerSoldiers[i] === self.target) {
playerSoldiers.splice(i, 1);
break;
}
}
}
}
}
self.destroyed = true;
self.destroy();
}
// Remove if out of bounds (double the distance for longer arrows)
if (self.x < -600 || self.x > 3200 || self.y < -600 || self.y > 4000) {
self.destroyed = true;
self.destroy();
}
};
return self;
});
// Castle class for player and enemy castles
var Castle = Container.expand(function () {
var self = Container.call(this);
// Properties
self.team = null;
self.health = CASTLE_HEALTH_INIT;
self.hasUpgrade = false;
self.attack = 0;
self.arrowCooldown = 0;
self.arrowCooldownMax = 60; // frames between shots
self.attackRadius = 500; // px, for arrow shooting
self.asset = null;
// Initialize castle
self.init = function (team, x, y) {
self.team = team;
self.x = x;
self.y = y;
self.health = CASTLE_HEALTH_INIT;
self.hasUpgrade = false;
self.attack = 0;
self.arrowCooldown = 0;
// Remove previous asset if any
if (self.asset) {
self.removeChild(self.asset);
}
// Add correct asset
if (team === 'player') {
var assetId = 'castle_player';
if (self.hasUpgrade2) {
assetId = 'castle_player_3';
} else if (self.hasUpgrade) {
assetId = 'castle_player_2';
}
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
} else {
var assetId = 'castle_enemy';
if (self.hasUpgrade2 || self.health >= 2500) {
assetId = 'castle_enemy_3';
} else if (self.hasUpgrade) {
assetId = 'castle_enemy_2';
}
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
}
};
// Upgrade castle
self.upgrade = function () {
self.hasUpgrade = true;
self.health = 1500;
self.attack = 1 + Math.floor(Math.random() * 3); // 1-3
self.arrowCooldown = 0;
// Update asset for first upgrade
if (self.asset) {
self.removeChild(self.asset);
}
var assetId = self.team === 'player' ? 'castle_player_2' : 'castle_enemy_2';
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Second upgrade handled externally (see gamecode.js)
};
// Update castle (shoot arrows if upgraded)
self.update = function () {
// Always round health to integer for display and logic
if (typeof self.health === "number") {
self.health = Math.round(self.health);
}
// Switch to castle_enemy_3 asset if health reaches 2500 and not already set
if (self.team === 'enemy' && self.health >= 2500 && (!self.asset || self.asset.assetId !== 'castle_enemy_3')) {
if (self.asset) {
self.removeChild(self.asset);
}
self.asset = self.attachAsset('castle_enemy_3', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure assetId is set for future checks
self.asset.assetId = 'castle_enemy_3';
// Immediately update the castleEnemy reference in game if this is the main enemy castle
if (typeof castleEnemy !== "undefined" && castleEnemy === self) {
castleEnemy.asset = self.asset;
}
}
if (self.hasUpgrade && self.health > 0) {
// Only shoot at enemy soldiers in range
var targets = [];
if (self.team === 'player') {
for (var i = 0; i < enemySoldiers.length; i++) {
var e = enemySoldiers[i];
var dx = e.x - self.x;
var dy = e.y - self.y;
if (dx * dx + dy * dy <= self.attackRadius * self.attackRadius) {
targets.push(e);
}
}
} else {
for (var i = 0; i < playerSoldiers.length; i++) {
var e = playerSoldiers[i];
var dx = e.x - self.x;
var dy = e.y - self.y;
if (dx * dx + dy * dy <= self.attackRadius * self.attackRadius) {
targets.push(e);
}
}
}
// Shoot at first target if cooldown is ready
if (targets.length > 0 && self.arrowCooldown <= 0) {
var target = targets[0];
var arrow = new Arrow();
arrow.init(self.x, self.y, target, self.attack, self.team);
arrows.push(arrow);
game.addChild(arrow);
self.arrowCooldown = self.arrowCooldownMax;
}
if (self.arrowCooldown > 0) self.arrowCooldown--;
}
};
return self;
});
// Soldier class for both player and enemy soldiers
var Soldier = Container.expand(function () {
var self = Container.call(this);
self.team = null;
self.health = 100;
self.attack = 10;
self.speed = SOLDIER_SPEED;
self.inCombat = false;
self.asset = null;
self.lastX = 0;
self.lastY = 0;
// Initialize soldier
self.init = function (team, health, attack) {
self.team = team;
self.health = health;
self.attack = attack;
self.inCombat = false;
// Remove previous asset if any
if (self.asset) {
self.removeChild(self.asset);
}
// Add correct asset
if (team === 'player') {
self.asset = self.attachAsset('soldier_player', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
self.asset = self.attachAsset('soldier_enemy', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Add health text label above the soldier
if (self.healthTxt && typeof self.healthTxt.destroy === "function") {
self.healthTxt.destroy();
}
self.healthTxt = new Text2(Math.round(self.health) + "", {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
self.healthTxt.anchor.set(0.5, 1.2);
self.healthTxt.x = 0;
self.healthTxt.y = -90;
self.addChild(self.healthTxt);
};
// Update soldier position (move forward if not in combat)
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
// Always round health to integer for display and logic
if (typeof self.health === "number") {
self.health = Math.round(self.health);
}
// Update health text label
if (self.healthTxt) {
self.healthTxt.setText(self.health + "");
}
if (!self.inCombat) {
if (self.team === 'player') {
self.x += self.speed;
} else {
self.x -= self.speed;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xBFFF00
});
/****
* Game Code
****/
// --- Add background image ---
// Ottoman-inspired church music with kanun, tambourine, zurna melodies
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
var background = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: GAME_W,
height: GAME_H
});
game.addChild(background);
// Play Ottoman-inspired church music with kanun, tambourine, zurna melodies
LK.playMusic('ottoman_church_bg');
// --- Game constants ---
// Player castle (blue)
// Enemy castle (red)
// Player soldier (green)
// --- Game constants ---
var GAME_W = 2048,
GAME_H = 2732;
var CASTLE_OFFSET_X = 120,
CASTLE_OFFSET_Y = GAME_H - 520; // moved up by 200px
var CASTLE_HEALTH_INIT = 1000;
var GOLD_INIT = 200;
var GOLD_PER_TICK = 5;
// After enemy 800 gold upgrade, this will be increased for mine-like feature
var GOLD_TICK_MS = 200;
var SOLDIER_COST = 100;
var SOLDIER_HEALTH_MIN = 80,
SOLDIER_HEALTH_MAX = 160;
var SOLDIER_ATTACK_MIN = 20,
SOLDIER_ATTACK_MAX = 50;
var SOLDIER_SPEED = 14 / 5 * 0.1 * 4.0; // px per frame (now 100% faster than previous)
// --- Speed mode state ---
var speedMode = false;
// --- Speed mode toggle button (top right, not in topLeft 100x100) ---
var speedBtn = new Text2("SPEED x1", {
size: 60,
fill: "#fff",
font: "PressStart2P,Pixel,monospace"
});
speedBtn.anchor.set(1, 0);
speedBtn.x = LK.gui.width - 40;
speedBtn.y = 40;
LK.gui.top.addChild(speedBtn);
speedBtn.down = function (x, y, obj) {
speedMode = !speedMode;
speedBtn.setText(speedMode ? "SPEED x3" : "SPEED x1");
};
// --- Difficulty selection ---
var difficulty = null;
var ENEMY_SOLDIER_COST = 100; // default, will be set by difficulty
// Show difficulty selection overlay
var diffOverlay = new Container();
diffOverlay.zIndex = 10000; // ensure on top
diffOverlay.width = GAME_W;
diffOverlay.height = GAME_H;
// Use a Text2 as a fake overlay background (solid block) since centerCircle is removed
var overlayBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
overlayBg.width = GAME_W;
overlayBg.height = GAME_H;
overlayBg.alpha = 0.7;
overlayBg.x = 0;
overlayBg.y = 0;
diffOverlay.addChild(overlayBg);
// Difficulty title with black background
var diffTitleBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
diffTitleBg.width = 900;
diffTitleBg.height = 160;
diffTitleBg.alpha = 0.95;
diffTitleBg.anchor.set(0.5, 0.5);
diffTitleBg.x = GAME_W / 2;
diffTitleBg.y = GAME_H / 2 - 300;
diffOverlay.addChild(diffTitleBg);
var diffTitle = new Text2("Select Difficulty", {
size: 120,
fill: "#fff",
font: "PressStart2P,Pixel,monospace"
});
diffTitle.anchor.set(0.5, 0.5);
diffTitle.x = GAME_W / 2;
diffTitle.y = GAME_H / 2 - 300;
diffOverlay.addChild(diffTitle);
// Easy button with black background
var btnEasyBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnEasyBg.width = 600;
btnEasyBg.height = 120;
btnEasyBg.alpha = 0.95;
btnEasyBg.anchor.set(0.5, 0.5);
btnEasyBg.x = GAME_W / 2;
btnEasyBg.y = GAME_H / 2 - 80;
diffOverlay.addChild(btnEasyBg);
var btnEasy = new Text2("Easy", {
size: 100,
fill: 0x00FF00,
font: "PressStart2P,Pixel,monospace"
});
btnEasy.anchor.set(0.5, 0.5);
btnEasy.x = GAME_W / 2;
btnEasy.y = GAME_H / 2 - 80;
diffOverlay.addChild(btnEasy);
// Normal button with black background
var btnNormalBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnNormalBg.width = 600;
btnNormalBg.height = 120;
btnNormalBg.alpha = 0.95;
btnNormalBg.anchor.set(0.5, 0.5);
btnNormalBg.x = GAME_W / 2;
btnNormalBg.y = GAME_H / 2 + 80;
diffOverlay.addChild(btnNormalBg);
var btnNormal = new Text2("Normal", {
size: 100,
fill: 0xFFFF00,
font: "PressStart2P,Pixel,monospace"
});
btnNormal.anchor.set(0.5, 0.5);
btnNormal.x = GAME_W / 2;
btnNormal.y = GAME_H / 2 + 80;
diffOverlay.addChild(btnNormal);
// Hard button with black background
var btnHardBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnHardBg.width = 600;
btnHardBg.height = 120;
btnHardBg.alpha = 0.95;
btnHardBg.anchor.set(0.5, 0.5);
btnHardBg.x = GAME_W / 2;
btnHardBg.y = GAME_H / 2 + 240;
diffOverlay.addChild(btnHardBg);
var btnHard = new Text2("Hard", {
size: 100,
fill: 0xFF0000,
font: "PressStart2P,Pixel,monospace"
});
btnHard.anchor.set(0.5, 0.5);
btnHard.x = GAME_W / 2;
btnHard.y = GAME_H / 2 + 240;
diffOverlay.addChild(btnHard);
// Speed Mode button (below hard)
var btnSpeedBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnSpeedBg.width = 600;
btnSpeedBg.height = 120;
btnSpeedBg.alpha = 0.95;
btnSpeedBg.anchor.set(0.5, 0.5);
btnSpeedBg.x = GAME_W / 2;
btnSpeedBg.y = GAME_H / 2 + 400;
diffOverlay.addChild(btnSpeedBg);
var btnSpeed = new Text2("Speed Mode: OFF", {
size: 80,
fill: 0x00E0FF,
font: "PressStart2P,Pixel,monospace"
});
btnSpeed.anchor.set(0.5, 0.5);
btnSpeed.x = GAME_W / 2;
btnSpeed.y = GAME_H / 2 + 400;
diffOverlay.addChild(btnSpeed);
var speedModeMenuSelected = false;
btnSpeed.down = function (x, y, obj) {
speedModeMenuSelected = !speedModeMenuSelected;
btnSpeed.setText(speedModeMenuSelected ? "Speed Mode: ON" : "Speed Mode: OFF");
};
game.addChild(diffOverlay);
// Disable game input until difficulty is chosen
var gameInputEnabled = false;
// Helper to start game with selected difficulty
function selectDifficulty(level) {
difficulty = level;
if (difficulty === "easy") {
ENEMY_SOLDIER_COST = SOLDIER_COST; // same as player
} else if (difficulty === "normal") {
ENEMY_SOLDIER_COST = Math.floor(SOLDIER_COST * 0.9); // 0.9 of player
} else if (difficulty === "hard") {
ENEMY_SOLDIER_COST = Math.floor(SOLDIER_COST * 0.7); // 0.7 of player
}
// Enable speed mode if selected in menu
if (typeof speedModeMenuSelected !== "undefined" && speedModeMenuSelected) {
speedMode = true;
if (typeof speedBtn !== "undefined") speedBtn.setText("SPEED x3");
} else {
speedMode = false;
if (typeof speedBtn !== "undefined") speedBtn.setText("SPEED x1");
}
// Set enemy deploy interval based on difficulty
if (typeof enemyDeployTimer !== "undefined") {
LK.clearInterval(enemyDeployTimer);
}
var enemyDeployInterval = 600; // default
if (difficulty === "easy") {
enemyDeployInterval = 900;
} else if (difficulty === "normal") {
enemyDeployInterval = 700;
} else if (difficulty === "hard") {
enemyDeployInterval = 500;
}
enemyDeployTimer = LK.setInterval(function () {
if (!gameInputEnabled) return;
// --- Initial Phase: 0-30 seconds ---
if (LK.ticks < 1800) {
// Only send soldiers, no upgrades, no archers.
// If player sends soldiers, let enemy build walls.
// If more than 3 enemy soldiers alive, let them build walls.
// (Blowing up walls is allowed, but not archers or upgrades.)
// Only send soldiers
if (enemyGold >= ENEMY_SOLDIER_COST) {
if (deploySoldier('enemy')) {
updateGui();
enemySoldierDeployCount++;
enemyTotalSoldierDeployed++;
enemySoldierSinceLastWall++;
}
}
// If player has sent soldiers (playerSoldiers.length > 0) or more than 3 enemy soldiers alive, build wall if possible
if ((playerSoldiers.length > 0 || enemySoldiers.length > 3) && enemyGold >= 50 && wallEnemies.length === 0) {
deployWallEnemy();
}
return;
}
// --- Middle Phase: 30-60 seconds ---
if (LK.ticks >= 1800 && LK.ticks < 3600) {
// Soldier + Archer + Wall alternately, each as rolling 1-3 dice and send that many
// Do not send archers without doing the 1st upgrade
// 50% chance to upgrade if enough gold and not immediately
if (typeof enemyMiddlePhase === "undefined") {
enemyMiddlePhase = {
step: 0,
upgradeTried: false
};
}
// 0: soldier, 1: archer, 2: wall, repeat
var phaseType = enemyMiddlePhase.step % 3;
// Roll 1-3 dice for how many to send
var toSend = 1 + Math.floor(Math.random() * 3);
if (phaseType === 0) {
// Soldier
for (var i = 0; i < toSend; i++) {
if (enemyGold >= ENEMY_SOLDIER_COST) {
if (deploySoldier('enemy')) {
updateGui();
enemyTotalSoldierDeployed++;
enemySoldierSinceLastWall++;
}
}
}
} else if (phaseType === 1) {
// Archer
if (castleEnemy.hasUpgrade) {
for (var i = 0; i < toSend; i++) {
if (enemyGold >= 150) {
enemyGold -= 150;
var s = new Soldier();
var baseSoldierHealth = 25;
s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6));
s.speed = SOLDIER_SPEED * 0.8;
s.inCombat = false;
if (s.asset) {
s.removeChild(s.asset);
}
s.asset = s.attachAsset('archer_enemy', {
anchorX: 0.5,
anchorY: 0.5
});
s.y = CASTLE_OFFSET_Y + 120;
s.x = castleEnemy.x - 120;
enemySoldiers.push(s);
game.addChild(s);
LK.getSound('unit_leave_castle').play();
updateGui();
}
}
}
// If not upgraded, skip archer phase (do nothing)
} else if (phaseType === 2) {
// Wall
for (var i = 0; i < toSend; i++) {
if (enemyGold >= 50 && wallEnemies.length === 0) {
deployWallEnemy();
}
}
}
enemyMiddlePhase.step++;
// 50% chance to upgrade if enough gold and not upgraded yet, but not immediately
if (!castleEnemy.hasUpgrade && !enemyMiddlePhase.upgradeTried && enemyGold >= 350) {
if (Math.random() < 0.5) {
enemyGold -= 350;
castleEnemy.upgrade();
updateGui();
}
enemyMiddlePhase.upgradeTried = true;
}
// Second upgrade logic (after player upgrades or 10 units lost)
if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) {
castleEnemy._upgrade2Started = true;
enemyGold -= 800;
castleEnemy.hasUpgrade2 = true;
castleEnemy.health = 2500;
castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6
castleEnemy.arrowCooldownMax = 40;
GOLD_PER_TICK = GOLD_PER_TICK + 5;
updateGui();
return;
}
return;
}
// --- Advanced Phase: 60s+ ---
// Upgradeable, soldier + archer + wall alternately, dice-based, archers only after upgrade
if (typeof enemyAdvancedPhase === "undefined") {
enemyAdvancedPhase = {
step: 0
};
}
var advType = enemyAdvancedPhase.step % 3;
var advToSend = 1 + Math.floor(Math.random() * 3);
if (advType === 0) {
// Soldier
for (var i = 0; i < advToSend; i++) {
if (enemyGold >= ENEMY_SOLDIER_COST) {
if (deploySoldier('enemy')) {
updateGui();
enemyTotalSoldierDeployed++;
enemySoldierSinceLastWall++;
}
}
}
} else if (advType === 1) {
// Archer
if (castleEnemy.hasUpgrade) {
for (var i = 0; i < advToSend; i++) {
if (enemyGold >= 150) {
enemyGold -= 150;
var s = new Soldier();
var baseSoldierHealth = 25;
s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6));
s.speed = SOLDIER_SPEED * 0.8;
s.inCombat = false;
if (s.asset) {
s.removeChild(s.asset);
}
s.asset = s.attachAsset('archer_enemy', {
anchorX: 0.5,
anchorY: 0.5
});
s.y = CASTLE_OFFSET_Y + 120;
s.x = castleEnemy.x - 120;
enemySoldiers.push(s);
game.addChild(s);
LK.getSound('unit_leave_castle').play();
updateGui();
}
}
}
// If not upgraded, skip archer phase (do nothing)
} else if (advType === 2) {
// Wall
for (var i = 0; i < advToSend; i++) {
if (enemyGold >= 50 && wallEnemies.length === 0) {
deployWallEnemy();
}
}
}
enemyAdvancedPhase.step++;
// Allow upgrades at any time if enough gold and not upgraded
if (!castleEnemy.hasUpgrade && enemyGold >= 350) {
enemyGold -= 350;
castleEnemy.upgrade();
updateGui();
}
// Second upgrade logic (after player upgrades or 10 units lost)
if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) {
castleEnemy._upgrade2Started = true;
enemyGold -= 800;
castleEnemy.hasUpgrade2 = true;
castleEnemy.health = 2500;
castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6
castleEnemy.arrowCooldownMax = 40;
GOLD_PER_TICK = GOLD_PER_TICK + 5;
updateGui();
return;
}
}, enemyDeployInterval);
gameInputEnabled = true;
diffOverlay.destroy();
// Show solder button in the middle section (archerBtn will appear after upgrade)
archerBtn.visible = false;
solderBtn.visible = true;
solderBtnLabel.visible = true;
wallBtn.visible = true;
wallBtnLabel.visible = true;
}
// Add touch/click handlers for buttons
btnEasy.down = function (x, y, obj) {
selectDifficulty("easy");
};
btnNormal.down = function (x, y, obj) {
selectDifficulty("normal");
};
btnHard.down = function (x, y, obj) {
selectDifficulty("hard");
};
// --- State variables ---
// (removed, handled by castlePlayer.health and castleEnemy.health)
var playerGold = GOLD_INIT;
var enemyGold = GOLD_INIT;
var playerSoldiers = [];
var enemySoldiers = [];
// --- Custom score tracking variables ---
var scoreStats = {
solderEnemyKills: 0,
archerEnemyKills: 0,
wallEnemyDestroyed: 0,
solderPlayerDeaths: 0,
archerPlayerDeaths: 0,
wallPlayerDestroyed: 0,
castlePlayerFullHealth: 0,
// 1 if full health at win
castlePlayerHealthLost: 0,
// amount lost
castlePlayerNoDamageGiven: 0 // 1 if no damage given to enemy
};
// --- Game over state variables ---
var gameOverState = null; // 'win' or 'lose'
var gameOverOverlay = null;
var gameOverTimer = null;
var pointsCalculated = false;
var totalPoints = 0;
// --- Castles ---
var castlePlayer = new Castle();
castlePlayer.init('player', CASTLE_OFFSET_X, CASTLE_OFFSET_Y);
game.addChild(castlePlayer);
var castleEnemy = new Castle();
castleEnemy.init('enemy', GAME_W - CASTLE_OFFSET_X, CASTLE_OFFSET_Y);
game.addChild(castleEnemy);
// --- Enemy castle health display above castle ---
var castleEnemyHealthTxt = new Text2(castleEnemy.health + '', {
size: 60,
fill: '#fff',
font: "PressStart2P,Pixel,monospace"
});
castleEnemyHealthTxt.anchor.set(0.5, 1.2);
castleEnemyHealthTxt.x = castleEnemy.x;
castleEnemyHealthTxt.y = castleEnemy.y - 260; // above the castle
game.addChild(castleEnemyHealthTxt);
// --- Player castle unit cost labels above castle ---
// --- Player castle upgrade cost label under player castle ---
var playerCastleUpgradeTxt = new Text2("YOU NEED 350 GOLD\nTO UPGRADE.", {
size: 54,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
playerCastleUpgradeTxt.anchor.set(0.5, -0.2); // anchor above the text baseline
playerCastleUpgradeTxt.x = castlePlayer.x + 180;
playerCastleUpgradeTxt.y = castlePlayer.y + 320; // under the castle
game.addChild(playerCastleUpgradeTxt);
// --- Player castle second upgrade cost label (800 gold, only after first upgrade) ---
var playerCastleUpgrade2Txt = new Text2("YOU NEED 800 GOLD\nTO UPGRADE AGAIN.", {
size: 54,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
playerCastleUpgrade2Txt.anchor.set(0.5, -0.2);
playerCastleUpgrade2Txt.x = castlePlayer.x + 180;
playerCastleUpgrade2Txt.y = castlePlayer.y + 320;
playerCastleUpgrade2Txt.visible = false;
game.addChild(playerCastleUpgrade2Txt);
// --- Track second upgrade state for both castles ---
castlePlayer.hasUpgrade2 = false;
castleEnemy.hasUpgrade2 = false; // After 800 gold upgrade, mine-like feature is enabled for enemy
// --- Left side: archer_button and solder_button side by side, visible on screen ---
var leftButtonsY = GAME_H / 2 + 100;
var leftButtonSpacing = 40;
var leftButtonStartX = 120 + 75; // 120px margin + half button width (150/2)
// Archer button (leftmost)
var archerBtn = LK.getAsset('archer_button', {
anchorX: 0.5,
anchorY: 0.5,
x: leftButtonStartX,
y: leftButtonsY
});
archerBtn.visible = false;
game.addChild(archerBtn);
// Add "150 G" label under archerBtn, but only show after castle is upgraded
var archerBtnLabel = new Text2("150 G", {
size: 48,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
archerBtnLabel.anchor.set(0.5, 0);
archerBtnLabel.x = archerBtn.x;
archerBtnLabel.y = archerBtn.y + 90; // 90px below center of button (button is 150px tall)
archerBtnLabel.visible = false;
game.addChild(archerBtnLabel);
// Solder button (right of archer)
var solderBtn = LK.getAsset('solder_button', {
anchorX: 0.5,
anchorY: 0.5,
x: leftButtonStartX + 150 + leftButtonSpacing,
// 150 is button width
y: leftButtonsY
});
solderBtn.visible = false;
game.addChild(solderBtn);
// Add "100 G" label under solderBtn
var solderBtnLabel = new Text2("100 G", {
size: 48,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
solderBtnLabel.anchor.set(0.5, 0);
solderBtnLabel.x = solderBtn.x;
solderBtnLabel.y = solderBtn.y + 90; // 90px below center of button (button is 150px tall)
solderBtnLabel.visible = false;
game.addChild(solderBtnLabel);
// Wall button (right of solderBtn)
var wallBtn = LK.getAsset('wall_button', {
anchorX: 0.5,
anchorY: 0.5,
x: solderBtn.x + 150 + leftButtonSpacing,
y: leftButtonsY
});
wallBtn.visible = false;
game.addChild(wallBtn);
// Add "50 G" label under wallBtn
var wallBtnLabel = new Text2("50 G", {
size: 48,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
wallBtnLabel.anchor.set(0.5, 0);
wallBtnLabel.x = wallBtn.x;
wallBtnLabel.y = wallBtn.y + 90;
wallBtnLabel.visible = false;
game.addChild(wallBtnLabel);
// --- Healing Tent Button and 250G label (appear after 850 gold upgrade) ---
var healingTentBtn = LK.getAsset('HEALING_TENT_KEY_PLAYER', {
anchorX: 0.5,
anchorY: 0.5,
x: wallBtn.x + 150 + leftButtonSpacing,
y: leftButtonsY
});
healingTentBtn.visible = false;
game.addChild(healingTentBtn);
var healingTentLabel = new Text2("250 G", {
size: 48,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
healingTentLabel.anchor.set(0.5, 0);
healingTentLabel.x = healingTentBtn.x;
healingTentLabel.y = healingTentBtn.y + 90;
healingTentLabel.visible = false;
game.addChild(healingTentLabel);
// --- Healing tent state ---
var healingTentPlayer = null;
var healingTentTimer = null;
var healingTentCountdownTxt = null;
// --- Enemy healing tent state ---
var healingTentEnemy = null;
var healingTentEnemyTimer = null;
var healingTentEnemyCountdownTxt = null;
var healingTentEnemyActive = false;
var healingTentEnemyNextReady = false;
var healingTentEnemyCooldownTimer = null;
var healingTentEnemyCountdown = 0;
var healingTentEnemyBoostActive = false;
var healingTentEnemyLastTick = 0;
// --- Healing tent purchase and placement ---
healingTentBtn.down = function (x, y, obj) {
if (!gameInputEnabled) return;
if (playerGold < 250) {
LK.getSound('dont-click').play();
return;
}
if (healingTentPlayer) return; // Only one tent at a time
playerGold -= 250;
updateGui();
// Find position between player castle and wall (or default if no wall)
var tentX;
var tentY = CASTLE_OFFSET_Y + 120 + 120; // Move tent further down (120px)
if (wallPlayers.length > 0) {
// Place between castle and first wall
var wall = wallPlayers[0];
tentX = (castlePlayer.x + wall.x) / 2;
} else {
// Place at 1/3 between castle and enemy castle
tentX = castlePlayer.x + (castleEnemy.x - castlePlayer.x) / 3;
}
healingTentPlayer = LK.getAsset('HEALING_TENT_PLAYER', {
anchorX: 0.5,
anchorY: 0.5,
x: tentX,
y: tentY
});
// Add health text label above healing tent (use 30 as "health" for tent, or just show "30" for 30s)
if (healingTentPlayer.healthTxt && typeof healingTentPlayer.healthTxt.destroy === "function") {
healingTentPlayer.healthTxt.destroy();
}
healingTentPlayer.healthTxt = new Text2("30", {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
healingTentPlayer.healthTxt.anchor.set(0.5, 1.2);
healingTentPlayer.healthTxt.x = 0;
healingTentPlayer.healthTxt.y = -170;
healingTentPlayer.addChild(healingTentPlayer.healthTxt);
game.addChild(healingTentPlayer);
// Ensure tent is in front of all other units by re-adding as last child
if (typeof healingTentPlayer !== "undefined" && _typeof(healingTentPlayer.parent) === "object") {
healingTentPlayer.parent.removeChild(healingTentPlayer);
game.addChild(healingTentPlayer);
}
// Remove previous countdown if any
if (healingTentCountdownTxt && typeof healingTentCountdownTxt.destroy === "function") {
healingTentCountdownTxt.destroy();
}
// Create countdown text below tent
healingTentCountdownTxt = new Text2("30", {
size: 60,
fill: 0xFF0000,
font: "PressStart2P,Pixel,monospace"
});
healingTentCountdownTxt.anchor.set(0.5, 0);
healingTentCountdownTxt.x = tentX;
healingTentCountdownTxt.y = tentY + 170; // below tent (tent is 300px tall)
game.addChild(healingTentCountdownTxt);
// Remove previous timer if any
if (healingTentTimer) {
LK.clearInterval(healingTentTimer);
healingTentTimer = null;
}
var tentCountdown = 30;
healingTentCountdownTxt.setText(tentCountdown + "");
healingTentTimer = LK.setInterval(function () {
tentCountdown--;
if (tentCountdown >= 0) {
healingTentCountdownTxt.setText(tentCountdown + "");
if (healingTentPlayer && healingTentPlayer.healthTxt) {
healingTentPlayer.healthTxt.setText(tentCountdown + "");
}
}
if (tentCountdown <= 0) {
// Remove tent and countdown
if (healingTentPlayer && typeof healingTentPlayer.destroy === "function") {
if (healingTentPlayer.healthTxt && typeof healingTentPlayer.healthTxt.destroy === "function") {
healingTentPlayer.healthTxt.destroy();
}
healingTentPlayer.destroy();
}
healingTentPlayer = null;
if (healingTentCountdownTxt && typeof healingTentCountdownTxt.destroy === "function") {
healingTentCountdownTxt.destroy();
}
healingTentCountdownTxt = null;
if (healingTentTimer) {
LK.clearInterval(healingTentTimer);
healingTentTimer = null;
}
}
}, 1000);
};
// --- Walls array to track all wall_player objects ---
var wallPlayers = [];
// --- Walls array to track all wall_enemy objects ---
var wallEnemies = [];
// Deduct 50 gold from player when wallBtn is pressed
wallBtn.down = function (x, y, obj) {
if (!gameInputEnabled) return;
if (playerGold >= 50) {
playerGold -= 50;
updateGui();
// Place wall_player in the middle of the two castles
var wallX = (castlePlayer.x + castleEnemy.x) / 2;
var wallY = CASTLE_OFFSET_Y + 120; // align with soldiers' path
var wallPlayer = LK.getAsset('wall_player', {
anchorX: 0.5,
anchorY: 0.5,
x: wallX,
y: wallY
});
// Wall health: 400 base, +200 per castlePlayer upgrade
var wallBaseHealth = 400 + (castlePlayer.hasUpgrade ? 200 : 0) + (castlePlayer.hasUpgrade2 ? 200 : 0);
wallPlayer.health = wallBaseHealth;
wallPlayer.maxHealth = wallBaseHealth;
wallPlayer.destroyed = false;
// Add health text label above wall
if (wallPlayer.healthTxt && typeof wallPlayer.healthTxt.destroy === "function") {
wallPlayer.healthTxt.destroy();
}
wallPlayer.healthTxt = new Text2(Math.round(wallPlayer.health) + "", {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
wallPlayer.healthTxt.anchor.set(0.5, 1.2);
wallPlayer.healthTxt.x = 0;
wallPlayer.healthTxt.y = -140;
wallPlayer.addChild(wallPlayer.healthTxt);
game.addChild(wallPlayer);
wallPlayers.push(wallPlayer);
} else {
LK.getSound('dont-click').play();
}
};
// --- Enemy AI: Deploy wall_enemy if enough gold and no wall exists ---
// Track total wall_enemy deployed since game start for enemy 2nd upgrade
var enemyWallEnemyDeployCount = 0;
function deployWallEnemy() {
// Only one wall_enemy at a time
if (wallEnemies.length > 0) return false;
if (enemyGold < 50) return false;
enemyGold -= 50;
updateGui();
var wallX = (castlePlayer.x + castleEnemy.x) / 2;
var wallY = CASTLE_OFFSET_Y + 120;
var wallEnemy = LK.getAsset('wall_enemy', {
anchorX: 0.5,
anchorY: 0.5,
x: wallX,
y: wallY
});
// Wall health: 400 base, +200 per castleEnemy upgrade
var wallBaseHealth = 400 + (castleEnemy.hasUpgrade ? 200 : 0) + (castleEnemy.hasUpgrade2 ? 200 : 0);
wallEnemy.health = wallBaseHealth;
wallEnemy.maxHealth = wallBaseHealth;
wallEnemy.destroyed = false;
// Add health text label above wall_enemy
if (wallEnemy.healthTxt && typeof wallEnemy.healthTxt.destroy === "function") {
wallEnemy.healthTxt.destroy();
}
wallEnemy.healthTxt = new Text2(Math.round(wallEnemy.health) + "", {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
wallEnemy.healthTxt.anchor.set(0.5, 1.2);
wallEnemy.healthTxt.x = 0;
wallEnemy.healthTxt.y = -140;
wallEnemy.addChild(wallEnemy.healthTxt);
game.addChild(wallEnemy);
wallEnemies.push(wallEnemy);
// Increment wall_enemy deploy count for enemy
enemyWallEnemyDeployCount++;
// If enemy has not upgraded, and has not yet started upgrade, and wall_enemy count >= 0, do first upgrade automatically (even if gold goes negative)
if (!castleEnemy.hasUpgrade && enemyWallEnemyDeployCount >= 0 && !castleEnemy._upgradeStarted) {
castleEnemy._upgradeStarted = true;
enemyGold -= 350;
castleEnemy.upgrade();
updateGui();
}
// If enemy has first upgrade, not yet second, and deployed 15 wall_enemy, do 2nd upgrade automatically (even if gold goes negative)
if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && enemyWallEnemyDeployCount >= 15 && !castleEnemy._upgrade2Started) {
castleEnemy._upgrade2Started = true;
enemyGold -= 800;
castleEnemy.hasUpgrade2 = true;
// Apply second upgrade effects (same as player)
castleEnemy.health = 2500;
castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6
castleEnemy.arrowCooldownMax = 40; // faster arrows
// --- MINE-LIKE FEATURE: Increase enemy gold income after 2nd upgrade ---
GOLD_PER_TICK = GOLD_PER_TICK + 5; // Increase gold income for both, or you can use a separate enemyGoldPerTick if needed
updateGui();
}
return true;
}
// --- GUI: Health and Gold displays ---
var playerHealthTxt = new Text2('1000', {
size: 70,
fill: '#fff',
font: "PressStart2P,Pixel,monospace"
});
playerHealthTxt.anchor.set(0, 0);
LK.gui.top.addChild(playerHealthTxt);
// Player LIFE label
var playerLifeLabel = new Text2('LIFE', {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
playerLifeLabel.anchor.set(0, 0);
LK.gui.top.addChild(playerLifeLabel);
var playerGoldTxt = new Text2('200', {
size: 60,
fill: '#ffe600',
font: "PressStart2P,Pixel,monospace"
});
playerGoldTxt.anchor.set(0, 0);
LK.gui.top.addChild(playerGoldTxt);
// Add soldier/cavalry cost labels to the left of the tan score
// Removed soldier/cavalry cost labels from GUI
// Player GOLD label
var playerGoldLabel = new Text2('GOLD', {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
playerGoldLabel.anchor.set(0, 0);
LK.gui.top.addChild(playerGoldLabel);
var enemyHealthTxt = new Text2('1000', {
size: 70,
fill: '#fff',
font: "PressStart2P,Pixel,monospace"
});
enemyHealthTxt.anchor.set(1, 0);
LK.gui.top.addChild(enemyHealthTxt);
// Enemy LIFE label
var enemyLifeLabel = new Text2('LIFE', {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
enemyLifeLabel.anchor.set(1, 0);
LK.gui.top.addChild(enemyLifeLabel);
var enemyGoldTxt = new Text2('200', {
size: 60,
fill: '#ffe600',
font: "PressStart2P,Pixel,monospace"
});
enemyGoldTxt.anchor.set(1, 0);
LK.gui.top.addChild(enemyGoldTxt);
// Enemy GOLD label
var enemyGoldLabel = new Text2('GOLD', {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
enemyGoldLabel.anchor.set(1, 0);
LK.gui.top.addChild(enemyGoldLabel);
// --- Win Counter (Rounds Won) ---
// Use storage plugin for persistence
var roundsWon = storage.roundsWon || 0;
var roundsWonTxt = new Text2('Rounds Won: ' + roundsWon, {
size: 60,
fill: '#00ff00',
font: "PressStart2P,Pixel,monospace"
});
roundsWonTxt.anchor.set(1, 0);
LK.gui.top.addChild(roundsWonTxt);
roundsWonTxt.x = LK.gui.width - 40;
roundsWonTxt.y = 220;
// Position GUI elements
playerHealthTxt.x = 120;
playerHealthTxt.y = 40;
playerLifeLabel.x = playerHealthTxt.x + playerHealthTxt.width + 18;
playerLifeLabel.y = playerHealthTxt.y + 10;
playerGoldTxt.x = 120;
playerGoldTxt.y = 120;
playerGoldLabel.x = playerGoldTxt.x + playerGoldTxt.width + 18;
playerGoldLabel.y = playerGoldTxt.y + 8;
// (Removed: Position soldier/cavalry cost labels to the left of the tan score)
enemyHealthTxt.x = LK.gui.width - 120;
enemyHealthTxt.y = 40;
enemyLifeLabel.x = enemyHealthTxt.x - enemyHealthTxt.width - 18;
enemyLifeLabel.y = enemyHealthTxt.y + 10;
enemyGoldTxt.x = LK.gui.width - 120;
enemyGoldTxt.y = 120;
enemyGoldLabel.x = enemyGoldTxt.x - enemyGoldTxt.width - 18;
enemyGoldLabel.y = enemyGoldTxt.y + 8;
// --- Gold income timer ---
var goldTimer = LK.setInterval(function () {
// Only increase gold if difficulty has been chosen
if (!gameInputEnabled) return;
if (speedMode) {
playerGold += GOLD_PER_TICK * 3;
enemyGold += GOLD_PER_TICK * 3;
} else {
playerGold += GOLD_PER_TICK;
enemyGold += GOLD_PER_TICK;
}
updateGui();
}, GOLD_TICK_MS);
// --- GUI update function ---
function updateGui() {
playerHealthTxt.setText(castlePlayer.health);
playerLifeLabel.x = playerHealthTxt.x + playerHealthTxt.width + 18;
playerLifeLabel.y = playerHealthTxt.y + 10;
playerGoldTxt.setText(playerGold);
playerGoldLabel.x = playerGoldTxt.x + playerGoldTxt.width + 18;
playerGoldLabel.y = playerGoldTxt.y + 8;
enemyHealthTxt.setText(castleEnemy.health);
enemyLifeLabel.x = enemyHealthTxt.x - enemyHealthTxt.width - 18;
enemyLifeLabel.y = enemyHealthTxt.y + 10;
enemyGoldTxt.setText(enemyGold);
enemyGoldLabel.x = enemyGoldTxt.x - enemyGoldTxt.width - 18;
enemyGoldLabel.y = enemyGoldTxt.y + 8;
// Update enemy castle health text and position
castleEnemyHealthTxt.setText(castleEnemy.health);
castleEnemyHealthTxt.x = castleEnemy.x;
castleEnemyHealthTxt.y = castleEnemy.y - 260;
}
// --- Deploy soldier function ---
function deploySoldier(team) {
// Block deployment if difficulty not chosen
if (!gameInputEnabled) return false;
var gold = team === 'player' ? playerGold : enemyGold;
var cost = team === 'player' ? SOLDIER_COST : ENEMY_SOLDIER_COST;
if (gold < cost) return false;
// Deduct gold
if (team === 'player') playerGold -= SOLDIER_COST;else enemyGold -= ENEMY_SOLDIER_COST;
// Fixed health and random attack
var health = 25;
var attack = 5 + Math.floor(Math.random() * 6); // 5-10 inclusive
// Create soldier
var s = new Soldier();
s.init(team, health, attack);
s.speed = SOLDIER_SPEED;
s.inCombat = false;
// Position
s.y = CASTLE_OFFSET_Y + 120; // Move soldiers' exit position down by 120px
if (team === 'player') {
s.x = castlePlayer.x + 120;
playerSoldiers.push(s);
} else {
s.x = castleEnemy.x - 120;
enemySoldiers.push(s);
}
game.addChild(s);
// Play sound when unit leaves the castle
LK.getSound('unit_leave_castle').play();
return true;
}
// --- Player tap to upgrade castle only (soldier deploy moved to solderBtn.down) ---
game.down = function (x, y, obj) {
// Play click sound on any touch/click
LK.getSound('click').play();
// Block input until difficulty is chosen
if (!gameInputEnabled) return;
// --- Block all upgrades in first 30 seconds ---
// (Removed: No upgrades allowed in first 30 seconds for player)
// Check if player clicked on their castle for upgrade
var dx = x - castlePlayer.x;
var dy = y - castlePlayer.y;
if (dx * dx + dy * dy < 250 * 250 && !castlePlayer.hasUpgrade && playerGold >= 350) {
playerGold -= 350;
castlePlayer.upgrade();
updateGui();
// Show archerBtn after upgrade
archerBtn.visible = true;
// Show '150 G' label under archerBtn after upgrade
archerBtnLabel.visible = true;
// Hide the first upgrade text, show the second upgrade text
if (playerCastleUpgradeTxt && typeof playerCastleUpgradeTxt.destroy === "function") {
playerCastleUpgradeTxt.visible = false;
}
playerCastleUpgrade2Txt.visible = true;
return;
}
// Second upgrade: only if first upgrade is done, not already done, and enough gold
if (dx * dx + dy * dy < 250 * 250 && castlePlayer.hasUpgrade && !castlePlayer.hasUpgrade2 && playerGold >= 800) {
playerGold -= 800;
castlePlayer.hasUpgrade2 = true;
// Apply second upgrade effects
castlePlayer.health = 2500;
castlePlayer.attack = 4 + Math.floor(Math.random() * 3); // 4-6
castlePlayer.arrowCooldownMax = 40; // faster arrows
// Update asset for second upgrade
if (castlePlayer.asset) {
castlePlayer.removeChild(castlePlayer.asset);
}
castlePlayer.asset = castlePlayer.attachAsset('castle_player_3', {
anchorX: 0.5,
anchorY: 0.5
});
updateGui();
// Hide the second upgrade text
playerCastleUpgrade2Txt.visible = false;
// Show healing tent button and 250G label
healingTentBtn.visible = true;
healingTentLabel.visible = true;
return;
}
// (Removed: deploySoldier on screen tap)
};
// --- Deploy soldier when solderBtn is pressed ---
solderBtn.down = function (x, y, obj) {
if (!gameInputEnabled) return;
if (playerGold >= SOLDIER_COST) {
deploySoldier('player');
updateGui();
} else {
LK.getSound('dont-click').play();
}
};
// --- Deploy archer_player when archerBtn is pressed (uses archer_player asset, cost 10 gold) ---
archerBtn.down = function (x, y, obj) {
if (!gameInputEnabled) return;
if (playerGold >= 150) {
playerGold -= 150;
// Create a new Soldier but use archer_player asset
var s = new Soldier();
// Set archer_player health to be twice that of soldier_player
var baseSoldierHealth = 25;
s.init('player', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6)); // health 50, attack 5-10
s.speed = SOLDIER_SPEED * 0.8; // Already uses updated SOLDIER_SPEED, now 75% faster
s.inCombat = false;
// Remove previous asset and attach archer_player asset
if (s.asset) {
s.removeChild(s.asset);
}
s.asset = s.attachAsset('archer_player', {
anchorX: 0.5,
anchorY: 0.5
});
// Position
s.y = CASTLE_OFFSET_Y + 120;
s.x = castlePlayer.x + 120;
playerSoldiers.push(s);
game.addChild(s);
// Play sound when archer leaves the castle
LK.getSound('unit_leave_castle').play();
updateGui();
} else {
LK.getSound('dont-click').play();
}
};
// --- Enemy AI: let the enemy play like a player: send soldiers, build walls, send archers, upgrade castle, and upgrade again, in a natural order ---
// Track enemy AI state
var enemySoldierDeployCount = 0;
var enemyNextUnit = 'soldier'; // always start with soldier
var enemyTotalSoldierDeployed = 0;
var enemyUpgradeGoldReserved = 200;
var enemySoldierSinceLastWall = 0;
castleEnemy.hasUpgrade2 = false;
// Track enemy AI upgrade state
castleEnemy._upgradeStarted = false;
castleEnemy._upgrade2Started = false;
// Track archer unlock for enemy (after first upgrade)
var enemyArcherUnlocked = false;
// Track how many enemy units have been removed (killed by player)
var enemyUnitsRemoved = 0;
// Patch: Increment enemyUnitsRemoved when enemy soldier is killed (player gets gold for kill)
var _oldPlayerSoldierCombat = true;
if (!_oldPlayerSoldierCombat) {// never runs, just for context
// see player soldier combat loop
}
// Patch: Increment enemyUnitsRemoved when enemy soldier is killed
// (Find the code awarding playerGold += 10 for killing enemy soldier, and increment enemyUnitsRemoved there)
var _oldEnemyUnitsRemovedPatch = true;
// Enemy AI main loop
// --- Add a flag and timer for enemy first upgrade delay ---
var enemyFirstUpgradeAllowed = false;
LK.setTimeout(function () {
enemyFirstUpgradeAllowed = true;
}, 15000);
var enemyDeployTimer = LK.setInterval(function () {
if (!gameInputEnabled) return;
// --- Initial Phase: 0-30 seconds ---
if (LK.ticks < 1800) {
// Only send soldiers, no upgrades, no archers.
// If player sends soldiers, let enemy build walls.
// If more than 3 enemy soldiers alive, let them build walls.
// (Blowing up walls is allowed, but not archers or upgrades.)
// Only send soldiers
if (enemyGold >= ENEMY_SOLDIER_COST) {
if (deploySoldier('enemy')) {
updateGui();
enemySoldierDeployCount++;
enemyTotalSoldierDeployed++;
enemySoldierSinceLastWall++;
}
}
// If player has sent soldiers (playerSoldiers.length > 0) or more than 3 enemy soldiers alive, build wall if possible
if ((playerSoldiers.length > 0 || enemySoldiers.length > 3) && enemyGold >= 50 && wallEnemies.length === 0) {
deployWallEnemy();
}
return;
}
// --- Middle Phase: 30-60 seconds ---
if (LK.ticks >= 1800 && LK.ticks < 3600) {
// Soldier + Archer + Wall alternately, each as rolling 1-3 dice and send that many
// Do not send archers without doing the 1st upgrade
// 50% chance to upgrade if enough gold and not immediately
if (typeof enemyMiddlePhase === "undefined") {
enemyMiddlePhase = {
step: 0,
upgradeTried: false
};
}
// 0: soldier, 1: archer, 2: wall, repeat
var phaseType = enemyMiddlePhase.step % 3;
// Roll 1-3 dice for how many to send
var toSend = 1 + Math.floor(Math.random() * 3);
if (phaseType === 0) {
// Soldier
for (var i = 0; i < toSend; i++) {
if (enemyGold >= ENEMY_SOLDIER_COST) {
if (deploySoldier('enemy')) {
updateGui();
enemyTotalSoldierDeployed++;
enemySoldierSinceLastWall++;
}
}
}
} else if (phaseType === 1) {
// Archer
if (castleEnemy.hasUpgrade) {
for (var i = 0; i < toSend; i++) {
if (enemyGold >= 150) {
enemyGold -= 150;
var s = new Soldier();
var baseSoldierHealth = 25;
s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6));
s.speed = SOLDIER_SPEED * 0.8;
s.inCombat = false;
if (s.asset) {
s.removeChild(s.asset);
}
s.asset = s.attachAsset('archer_enemy', {
anchorX: 0.5,
anchorY: 0.5
});
s.y = CASTLE_OFFSET_Y + 120;
s.x = castleEnemy.x - 120;
enemySoldiers.push(s);
game.addChild(s);
LK.getSound('unit_leave_castle').play();
updateGui();
}
}
}
// If not upgraded, skip archer phase (do nothing)
} else if (phaseType === 2) {
// Wall
for (var i = 0; i < toSend; i++) {
if (enemyGold >= 50 && wallEnemies.length === 0) {
deployWallEnemy();
}
}
}
enemyMiddlePhase.step++;
// 50% chance to upgrade if enough gold and not upgraded yet, but not immediately
if (!castleEnemy.hasUpgrade && !enemyMiddlePhase.upgradeTried && enemyGold >= 350) {
if (Math.random() < 0.5) {
enemyGold -= 350;
castleEnemy.upgrade();
updateGui();
}
enemyMiddlePhase.upgradeTried = true;
}
// Second upgrade logic (after player upgrades or 10 units lost)
if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) {
castleEnemy._upgrade2Started = true;
enemyGold -= 800;
castleEnemy.hasUpgrade2 = true;
castleEnemy.health = 2500;
castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6
castleEnemy.arrowCooldownMax = 40;
GOLD_PER_TICK = GOLD_PER_TICK + 5;
updateGui();
return;
}
return;
}
// --- Advanced Phase: 60s+ ---
// Upgradeable, soldier + archer + wall alternately, dice-based, archers only after upgrade
if (typeof enemyAdvancedPhase === "undefined") {
enemyAdvancedPhase = {
step: 0
};
}
var advType = enemyAdvancedPhase.step % 3;
var advToSend = 1 + Math.floor(Math.random() * 3);
if (advType === 0) {
// Soldier
for (var i = 0; i < advToSend; i++) {
if (enemyGold >= ENEMY_SOLDIER_COST) {
if (deploySoldier('enemy')) {
updateGui();
enemyTotalSoldierDeployed++;
enemySoldierSinceLastWall++;
}
}
}
} else if (advType === 1) {
// Archer
if (castleEnemy.hasUpgrade) {
for (var i = 0; i < advToSend; i++) {
if (enemyGold >= 150) {
enemyGold -= 150;
var s = new Soldier();
var baseSoldierHealth = 25;
s.init('enemy', baseSoldierHealth * 2, 5 + Math.floor(Math.random() * 6));
s.speed = SOLDIER_SPEED * 0.8;
s.inCombat = false;
if (s.asset) {
s.removeChild(s.asset);
}
s.asset = s.attachAsset('archer_enemy', {
anchorX: 0.5,
anchorY: 0.5
});
s.y = CASTLE_OFFSET_Y + 120;
s.x = castleEnemy.x - 120;
enemySoldiers.push(s);
game.addChild(s);
LK.getSound('unit_leave_castle').play();
updateGui();
}
}
}
// If not upgraded, skip archer phase (do nothing)
} else if (advType === 2) {
// Wall
for (var i = 0; i < advToSend; i++) {
if (enemyGold >= 50 && wallEnemies.length === 0) {
deployWallEnemy();
}
}
}
enemyAdvancedPhase.step++;
// Allow upgrades at any time if enough gold and not upgraded
if (!castleEnemy.hasUpgrade && enemyGold >= 350) {
enemyGold -= 350;
castleEnemy.upgrade();
updateGui();
}
// Second upgrade logic (after player upgrades or 10 units lost)
if (castleEnemy.hasUpgrade && !castleEnemy.hasUpgrade2 && !castleEnemy._upgrade2Started && (castlePlayer.hasUpgrade || enemyUnitsRemoved >= 10)) {
castleEnemy._upgrade2Started = true;
enemyGold -= 800;
castleEnemy.hasUpgrade2 = true;
castleEnemy.health = 2500;
castleEnemy.attack = 4 + Math.floor(Math.random() * 3); // 4-6
castleEnemy.arrowCooldownMax = 40;
GOLD_PER_TICK = GOLD_PER_TICK + 5;
updateGui();
return;
}
}, 600);
// --- Arrows array ---
var arrows = [];
// --- Main update loop ---
game.update = function () {
// --- Update castles (health, arrows) ---
castlePlayer.update();
castleEnemy.update();
// --- Enemy healing tent logic ---
// Only allow after enemy's 2nd upgrade
if (castleEnemy.hasUpgrade2) {
// Start the healing tent cycle if not already started
if (!healingTentEnemyCooldownTimer && !healingTentEnemyActive) {
healingTentEnemyNextReady = true;
healingTentEnemyCountdown = 0;
healingTentEnemyLastTick = LK.ticks;
healingTentEnemyCooldownTimer = LK.setInterval(function () {
if (!healingTentEnemyActive && castleEnemy.hasUpgrade2) {
healingTentEnemyCountdown++;
if (healingTentEnemyCountdown >= 100) {
// 100 seconds
healingTentEnemyNextReady = true;
healingTentEnemyCountdown = 0;
}
}
}, 1000);
}
// If ready and not active, spawn the tent
if (healingTentEnemyNextReady && !healingTentEnemyActive) {
// Place tent between enemy castle and wall (or default if no wall)
var tentX;
var tentY = CASTLE_OFFSET_Y + 120 + 120;
if (wallEnemies.length > 0) {
var wall = wallEnemies[0];
tentX = (castleEnemy.x + wall.x) / 2;
} else {
tentX = castleEnemy.x - (castleEnemy.x - castlePlayer.x) / 3;
}
healingTentEnemy = LK.getAsset('HEALING_TENT_ENEMY', {
anchorX: 0.5,
anchorY: 0.5,
x: tentX,
y: tentY
});
// Add health text label above healing tent (use 30 as "health" for tent, or just show "30" for 30s)
if (healingTentEnemy.healthTxt && typeof healingTentEnemy.healthTxt.destroy === "function") {
healingTentEnemy.healthTxt.destroy();
}
healingTentEnemy.healthTxt = new Text2("30", {
size: 38,
fill: "#000",
font: "PressStart2P,Pixel,monospace"
});
healingTentEnemy.healthTxt.anchor.set(0.5, 1.2);
healingTentEnemy.healthTxt.x = 0;
healingTentEnemy.healthTxt.y = -170;
healingTentEnemy.addChild(healingTentEnemy.healthTxt);
game.addChild(healingTentEnemy);
// Ensure tent is in front of all other units by re-adding as last child
if (typeof healingTentEnemy !== "undefined" && healingTentEnemy.parent) {
healingTentEnemy.parent.removeChild(healingTentEnemy);
game.addChild(healingTentEnemy);
}
// Remove previous countdown if any
if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") {
healingTentEnemyCountdownTxt.destroy();
}
// Create countdown text below tent
healingTentEnemyCountdownTxt = new Text2("30", {
size: 60,
fill: 0xFF0000,
font: "PressStart2P,Pixel,monospace"
});
healingTentEnemyCountdownTxt.anchor.set(0.5, 0);
healingTentEnemyCountdownTxt.x = tentX;
healingTentEnemyCountdownTxt.y = tentY + 170;
game.addChild(healingTentEnemyCountdownTxt);
// Remove previous timer if any
if (healingTentEnemyTimer) {
LK.clearInterval(healingTentEnemyTimer);
healingTentEnemyTimer = null;
}
var tentCountdown = 30;
healingTentEnemyCountdownTxt.setText(tentCountdown + "");
healingTentEnemyActive = true;
healingTentEnemyBoostActive = true;
healingTentEnemyTimer = LK.setInterval(function () {
tentCountdown--;
if (tentCountdown >= 0) {
healingTentEnemyCountdownTxt.setText(tentCountdown + "");
if (healingTentEnemy && healingTentEnemy.healthTxt) {
healingTentEnemy.healthTxt.setText(tentCountdown + "");
}
}
if (tentCountdown <= 0) {
// Remove tent and countdown
if (healingTentEnemy && typeof healingTentEnemy.destroy === "function") {
if (healingTentEnemy.healthTxt && typeof healingTentEnemy.healthTxt.destroy === "function") {
healingTentEnemy.healthTxt.destroy();
}
healingTentEnemy.destroy();
}
healingTentEnemy = null;
if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") {
healingTentEnemyCountdownTxt.destroy();
}
healingTentEnemyCountdownTxt = null;
if (healingTentEnemyTimer) {
LK.clearInterval(healingTentEnemyTimer);
healingTentEnemyTimer = null;
}
healingTentEnemyActive = false;
healingTentEnemyBoostActive = false;
healingTentEnemyNextReady = false;
healingTentEnemyCountdown = 0;
}
}, 1000);
}
} else {
// If enemy loses 2nd upgrade, clear timers and tent
if (healingTentEnemyCooldownTimer) {
LK.clearInterval(healingTentEnemyCooldownTimer);
healingTentEnemyCooldownTimer = null;
}
if (healingTentEnemyTimer) {
LK.clearInterval(healingTentEnemyTimer);
healingTentEnemyTimer = null;
}
if (healingTentEnemy && typeof healingTentEnemy.destroy === "function") {
if (healingTentEnemy.healthTxt && typeof healingTentEnemy.healthTxt.destroy === "function") {
healingTentEnemy.healthTxt.destroy();
}
healingTentEnemy.destroy();
}
healingTentEnemy = null;
if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") {
healingTentEnemyCountdownTxt.destroy();
}
healingTentEnemyCountdownTxt = null;
healingTentEnemyActive = false;
healingTentEnemyBoostActive = false;
healingTentEnemyNextReady = false;
healingTentEnemyCountdown = 0;
}
// --- Apply healing tent effect: 15% health boost to all player units while tent is present ---
if (healingTentPlayer) {
// Always keep healing tent in front of all units, even those created after it
if (healingTentPlayer.parent) {
healingTentPlayer.parent.removeChild(healingTentPlayer);
game.addChild(healingTentPlayer);
}
for (var i = 0; i < playerSoldiers.length; i++) {
var s = playerSoldiers[i];
if (!s._healingTentBoosted) {
s.health = Math.ceil(s.health * 1.15);
s._healingTentBoosted = true;
}
}
} else {
// Remove boost flag if tent is gone (do not revert health, only allow boost once per tent appearance)
for (var i = 0; i < playerSoldiers.length; i++) {
var s = playerSoldiers[i];
if (s._healingTentBoosted) {
s._healingTentBoosted = false;
}
}
}
// --- Apply healing tent effect: 15% health boost to all enemy units while enemy tent is present ---
if (healingTentEnemyBoostActive) {
// Always keep healing tent in front of all units, even those created after it
if (healingTentEnemy && healingTentEnemy.parent) {
healingTentEnemy.parent.removeChild(healingTentEnemy);
game.addChild(healingTentEnemy);
}
for (var i = 0; i < enemySoldiers.length; i++) {
var s = enemySoldiers[i];
if (!s._healingTentEnemyBoosted) {
s.health = Math.ceil(s.health * 1.15);
s._healingTentEnemyBoosted = true;
}
}
} else {
// Remove boost flag if tent is gone (do not revert health, only allow boost once per tent appearance)
for (var i = 0; i < enemySoldiers.length; i++) {
var s = enemySoldiers[i];
if (s._healingTentEnemyBoosted) {
s._healingTentEnemyBoosted = false;
}
}
}
// --- Player soldiers ---
for (var i = playerSoldiers.length - 1; i >= 0; i--) {
var s = playerSoldiers[i];
s.inCombat = false;
// --- Check for wall_enemy collision ---
var wallEngaged = false;
for (var w = 0; w < wallEnemies.length; w++) {
var wall = wallEnemies[w];
if (wall.destroyed) continue;
// Simple collision: check if close enough horizontally and vertically
if (Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) {
s.inCombat = true;
wallEngaged = true;
// Player attacks wall only if at least 2 player soldiers are engaging this wall
if (LK.ticks % 12 === 0) {
var engagedCount = 0;
for (var ps = 0; ps < playerSoldiers.length; ps++) {
var psold = playerSoldiers[ps];
if (Math.abs(psold.x - wall.x) < 80 && Math.abs(psold.y - wall.y) < 180 && !psold.destroyed) {
engagedCount++;
}
}
if (engagedCount >= 2) {
wall.health -= s.attack;
if (typeof wall.health === "number") {
wall.health = Math.round(wall.health);
}
}
}
// If wall destroyed, remove from game and array
if (wall.healthTxt) {
wall.healthTxt.setText(Math.max(0, Math.round(wall.health)) + "");
}
if (wall.health <= 0) {
scoreStats.wallEnemyDestroyed++;
wall.destroyed = true;
if (wall.healthTxt && typeof wall.healthTxt.destroy === "function") {
wall.healthTxt.destroy();
}
if (typeof wall.destroy === "function") wall.destroy();
wallEnemies.splice(w, 1);
w--;
}
break; // Only engage one wall at a time
}
}
if (wallEngaged) continue;
// Check for enemy soldier in range
var engaged = false;
// Prevent player soldiers from damaging enemy units if any wall_enemy is present and not destroyed and player soldier is engaged with it
var wallEnemyBlocking = false;
for (var w = 0; w < wallEnemies.length; w++) {
var wall = wallEnemies[w];
if (!wall.destroyed && Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) {
wallEnemyBlocking = true;
break;
}
}
for (var j = 0; j < enemySoldiers.length; j++) {
var e = enemySoldiers[j];
// If close enough (overlap)
if (Math.abs(s.x - e.x) < 80) {
// Engage in combat
s.inCombat = true;
e.inCombat = true;
// Both attack each other
if (LK.ticks % 12 === 0) {
// Only allow damage if not blocked by wall_enemy
if (!wallEnemyBlocking) {
// Attack every 12 frames (~5 times/sec)
e.health -= s.attack;
s.health -= e.attack;
}
}
// Remove dead soldiers
if (e.health <= 0) {
// Determine if enemy is archer or soldier
if (e.asset && e.asset.assetId === 'archer_enemy') {
scoreStats.archerEnemyKills++;
} else {
scoreStats.solderEnemyKills++;
}
e.destroy();
enemySoldiers.splice(j, 1);
j--;
// Award 10 gold to player for killing enemy soldier
playerGold += 10;
enemyUnitsRemoved = (typeof enemyUnitsRemoved === "number" ? enemyUnitsRemoved : 0) + 1; // increment removed count
updateGui();
}
if (s.health <= 0) {
// Determine if player is archer or soldier
if (s.asset && s.asset.assetId === 'archer_player') {
scoreStats.archerPlayerDeaths++;
} else {
scoreStats.solderPlayerDeaths++;
}
s.destroy();
playerSoldiers.splice(i, 1);
i--;
// Award 10 gold to enemy for killing player soldier
enemyGold += 10;
updateGui();
engaged = true;
break;
}
engaged = true;
break;
}
}
if (engaged) continue;
// If not in combat, check if at enemy castle
if (Math.abs(s.x - castleEnemy.x) < 120) {
s.inCombat = true;
if (LK.ticks % 12 === 0) {
castleEnemy.health -= s.attack;
if (castleEnemy.health < 0) castleEnemy.health = 0;
updateGui();
}
// Remove soldier if castle destroyed
if (castleEnemy.health <= 0) {
s.destroy();
playerSoldiers.splice(i, 1);
i--;
}
}
}
// --- Enemy soldiers ---
for (var i = enemySoldiers.length - 1; i >= 0; i--) {
var s = enemySoldiers[i];
s.inCombat = false;
// --- Check for wall_player collision ---
var wallEngaged = false;
for (var w = 0; w < wallPlayers.length; w++) {
var wall = wallPlayers[w];
if (wall.destroyed) continue;
// Simple collision: check if close enough horizontally and vertically
if (Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) {
s.inCombat = true;
wallEngaged = true;
// Enemy attacks wall only if at least 2 enemy soldiers are engaging this wall
if (LK.ticks % 12 === 0) {
var engagedCount = 0;
for (var es = 0; es < enemySoldiers.length; es++) {
var esold = enemySoldiers[es];
if (Math.abs(esold.x - wall.x) < 80 && Math.abs(esold.y - wall.y) < 180 && !esold.destroyed) {
engagedCount++;
}
}
if (engagedCount >= 2) {
wall.health -= s.attack;
if (typeof wall.health === "number") {
wall.health = Math.round(wall.health);
}
}
}
// If wall destroyed, remove from game and array
if (wall.healthTxt) {
wall.healthTxt.setText(Math.max(0, Math.round(wall.health)) + "");
}
if (wall.health <= 0) {
scoreStats.wallPlayerDestroyed++;
wall.destroyed = true;
if (wall.healthTxt && typeof wall.healthTxt.destroy === "function") {
wall.healthTxt.destroy();
}
if (typeof wall.destroy === "function") wall.destroy();
wallPlayers.splice(w, 1);
w--;
}
break; // Only engage one wall at a time
}
}
if (wallEngaged) continue;
// Check for player soldier in range
var engaged = false;
// Prevent enemy soldiers from damaging player units if any wall_player is present and not destroyed and enemy soldier is engaged with it
var wallPlayerBlocking = false;
for (var w = 0; w < wallPlayers.length; w++) {
var wall = wallPlayers[w];
if (!wall.destroyed && Math.abs(s.x - wall.x) < 80 && Math.abs(s.y - wall.y) < 180) {
wallPlayerBlocking = true;
break;
}
}
for (var j = 0; j < playerSoldiers.length; j++) {
var e = playerSoldiers[j];
if (Math.abs(s.x - e.x) < 80) {
s.inCombat = true;
e.inCombat = true;
if (LK.ticks % 12 === 0) {
// Only allow damage if not blocked by wall_player
if (!wallPlayerBlocking) {
e.health -= s.attack;
s.health -= e.attack;
}
}
if (e.health <= 0) {
e.destroy();
playerSoldiers.splice(j, 1);
j--;
// Award 10 gold to enemy for killing player soldier
enemyGold += 10;
updateGui();
}
if (s.health <= 0) {
s.destroy();
enemySoldiers.splice(i, 1);
i--;
// Award 10 gold to player for killing enemy soldier
playerGold += 10;
updateGui();
engaged = true;
break;
}
engaged = true;
break;
}
}
if (engaged) continue;
// If not in combat, check if at player castle
if (Math.abs(s.x - castlePlayer.x) < 120) {
s.inCombat = true;
if (LK.ticks % 12 === 0) {
castlePlayer.health -= s.attack;
if (castlePlayer.health < 0) castlePlayer.health = 0;
updateGui();
}
if (castlePlayer.health <= 0) {
s.destroy();
enemySoldiers.splice(i, 1);
i--;
}
}
}
// --- Move soldiers ---
for (var i = 0; i < playerSoldiers.length; i++) {
playerSoldiers[i].update();
}
for (var i = 0; i < enemySoldiers.length; i++) {
enemySoldiers[i].update();
}
// --- Update arrows ---
for (var i = arrows.length - 1; i >= 0; i--) {
var a = arrows[i];
a.update();
if (a.destroyed) {
arrows.splice(i, 1);
}
}
// --- Check win/lose ---
if (castlePlayer.health <= 0 && !gameOverState) {
gameOverState = 'lose';
showCustomGameOver();
return;
}
if (castleEnemy.health <= 0 && !gameOverState) {
gameOverState = 'win';
// --- Custom score stats for win ---
scoreStats.castlePlayerFullHealth = castlePlayer.health === CASTLE_HEALTH_INIT || castlePlayer.health === 2500 ? 1 : 0;
scoreStats.castlePlayerHealthLost = castlePlayer.health < (castlePlayer.hasUpgrade2 ? 2500 : CASTLE_HEALTH_INIT) ? (castlePlayer.hasUpgrade2 ? 2500 : CASTLE_HEALTH_INIT) - castlePlayer.health : 0;
scoreStats.castlePlayerNoDamageGiven = castlePlayer.health === (castlePlayer.hasUpgrade2 ? 2500 : CASTLE_HEALTH_INIT) && scoreStats.solderPlayerDeaths === 0 && scoreStats.archerPlayerDeaths === 0 && scoreStats.wallPlayerDestroyed === 0 ? 1 : 0;
// Increase and persist rounds won
roundsWon = (storage.roundsWon || 0) + 1;
storage.roundsWon = roundsWon;
roundsWonTxt.setText('Rounds Won: ' + roundsWon);
showCustomGameOver();
return;
}
};
// --- Custom game over function ---
function showCustomGameOver() {
// Create overlay
gameOverOverlay = new Container();
gameOverOverlay.zIndex = 20000;
// Background
var overlayBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
overlayBg.width = GAME_W;
overlayBg.height = GAME_H;
overlayBg.alpha = 0.85;
overlayBg.x = 0;
overlayBg.y = 0;
gameOverOverlay.addChild(overlayBg);
// Main message
var mainMessage;
if (gameOverState === 'win') {
mainMessage = new Text2("YOU WON THE GAME", {
size: 120,
fill: 0x00FF00,
font: "PressStart2P,Pixel,monospace"
});
LK.effects.flashScreen(0x00ff00, 1000);
} else {
mainMessage = new Text2("YOU LOST THE GAME", {
size: 120,
fill: 0xFF0000,
font: "PressStart2P,Pixel,monospace"
});
LK.effects.flashScreen(0xff0000, 1000);
}
mainMessage.anchor.set(0.5, 0.5);
mainMessage.x = GAME_W / 2;
mainMessage.y = GAME_H / 2 - 200;
gameOverOverlay.addChild(mainMessage);
game.addChild(gameOverOverlay);
// Start timer sequence
gameOverTimer = LK.setTimeout(function () {
if (gameOverState === 'win') {
showPointsEarned();
} else {
// For lose, wait 5 seconds then go to login screen
gameOverTimer = LK.setTimeout(function () {
goToLoginScreen();
}, 5000);
}
}, 5000);
}
function showPointsEarned() {
if (pointsCalculated) return;
pointsCalculated = true;
// Calculate points
totalPoints = 0;
totalPoints += scoreStats.solderEnemyKills * 10;
totalPoints += scoreStats.archerEnemyKills * 25;
totalPoints += scoreStats.wallEnemyDestroyed * 50;
if (scoreStats.solderPlayerDeaths < 50) totalPoints += 1000;
if (scoreStats.archerPlayerDeaths < 50) totalPoints += 3000;
if (scoreStats.wallPlayerDestroyed < 20) totalPoints += 2000;
if (scoreStats.castlePlayerFullHealth) totalPoints += 5000;
if (scoreStats.castlePlayerHealthLost > 0) totalPoints += scoreStats.castlePlayerHealthLost;
if (scoreStats.castlePlayerNoDamageGiven) totalPoints += 10000;
// Store total points
var allTimePoints = storage.allTimePoints || 0;
allTimePoints += totalPoints;
storage.allTimePoints = allTimePoints;
// Create points text
var pointsText = new Text2("POINTS EARNED: " + totalPoints, {
size: 100,
fill: 0xFFD700,
font: "PressStart2P,Pixel,monospace"
});
pointsText.anchor.set(0.5, 0.5);
pointsText.x = GAME_W / 2;
pointsText.y = GAME_H / 2 + 100;
pointsText.alpha = 0;
gameOverOverlay.addChild(pointsText);
// Animate points in
tween(pointsText, {
alpha: 1
}, {
duration: 1000
});
// Wait 5 seconds then go to login
gameOverTimer = LK.setTimeout(function () {
goToLoginScreen();
}, 5000);
}
function goToLoginScreen() {
// Wait 5 seconds, then reset and return to difficulty selection
LK.setTimeout(function () {
// Reset all game state variables
gameOverState = null;
pointsCalculated = false;
totalPoints = 0;
difficulty = null;
gameInputEnabled = false;
speedMode = false;
speedModeMenuSelected = false;
// Reset castle states
castlePlayer.health = CASTLE_HEALTH_INIT;
castlePlayer.hasUpgrade = false;
castlePlayer.hasUpgrade2 = false;
castlePlayer._upgradeStarted = false;
castlePlayer._upgrade2Started = false;
castleEnemy.health = CASTLE_HEALTH_INIT;
castleEnemy.hasUpgrade = false;
castleEnemy.hasUpgrade2 = false;
castleEnemy._upgradeStarted = false;
castleEnemy._upgrade2Started = false;
// Reset gold
playerGold = GOLD_INIT;
enemyGold = GOLD_INIT;
GOLD_PER_TICK = 5;
// Clear all soldiers and arrows
for (var i = 0; i < playerSoldiers.length; i++) {
if (playerSoldiers[i] && typeof playerSoldiers[i].destroy === "function") {
playerSoldiers[i].destroy();
}
}
for (var i = 0; i < enemySoldiers.length; i++) {
if (enemySoldiers[i] && typeof enemySoldiers[i].destroy === "function") {
enemySoldiers[i].destroy();
}
}
for (var i = 0; i < arrows.length; i++) {
if (arrows[i] && typeof arrows[i].destroy === "function") {
arrows[i].destroy();
}
}
playerSoldiers = [];
enemySoldiers = [];
arrows = [];
// Clear walls
for (var i = 0; i < wallPlayers.length; i++) {
if (wallPlayers[i] && typeof wallPlayers[i].destroy === "function") {
wallPlayers[i].destroy();
}
}
for (var i = 0; i < wallEnemies.length; i++) {
if (wallEnemies[i] && typeof wallEnemies[i].destroy === "function") {
wallEnemies[i].destroy();
}
}
wallPlayers = [];
wallEnemies = [];
// Clear healing tents and timers
if (healingTentPlayer && typeof healingTentPlayer.destroy === "function") {
healingTentPlayer.destroy();
}
if (healingTentEnemy && typeof healingTentEnemy.destroy === "function") {
healingTentEnemy.destroy();
}
if (healingTentCountdownTxt && typeof healingTentCountdownTxt.destroy === "function") {
healingTentCountdownTxt.destroy();
}
if (healingTentEnemyCountdownTxt && typeof healingTentEnemyCountdownTxt.destroy === "function") {
healingTentEnemyCountdownTxt.destroy();
}
if (healingTentTimer) {
LK.clearInterval(healingTentTimer);
healingTentTimer = null;
}
if (healingTentEnemyTimer) {
LK.clearInterval(healingTentEnemyTimer);
healingTentEnemyTimer = null;
}
if (healingTentEnemyCooldownTimer) {
LK.clearInterval(healingTentEnemyCooldownTimer);
healingTentEnemyCooldownTimer = null;
}
healingTentPlayer = null;
healingTentEnemy = null;
healingTentCountdownTxt = null;
healingTentEnemyCountdownTxt = null;
healingTentEnemyActive = false;
healingTentEnemyNextReady = false;
healingTentEnemyCountdown = 0;
healingTentEnemyBoostActive = false;
// Reset enemy AI variables
enemySoldierDeployCount = 0;
enemyTotalSoldierDeployed = 0;
enemyUnitsRemoved = 0;
enemySoldierSinceLastWall = 0;
enemyWallEnemyDeployCount = 0;
if (typeof enemyMiddlePhase !== "undefined") {
enemyMiddlePhase = undefined;
}
if (typeof enemyAdvancedPhase !== "undefined") {
enemyAdvancedPhase = undefined;
}
// Reset castle assets
castlePlayer.init('player', CASTLE_OFFSET_X, CASTLE_OFFSET_Y);
castleEnemy.init('enemy', GAME_W - CASTLE_OFFSET_X, CASTLE_OFFSET_Y);
// Reset button visibility
archerBtn.visible = false;
archerBtnLabel.visible = false;
solderBtn.visible = false;
solderBtnLabel.visible = false;
wallBtn.visible = false;
wallBtnLabel.visible = false;
healingTentBtn.visible = false;
healingTentLabel.visible = false;
// Reset upgrade text visibility
playerCastleUpgradeTxt.visible = true;
playerCastleUpgrade2Txt.visible = false;
// Reset score stats
scoreStats = {
solderEnemyKills: 0,
archerEnemyKills: 0,
wallEnemyDestroyed: 0,
solderPlayerDeaths: 0,
archerPlayerDeaths: 0,
wallPlayerDestroyed: 0,
castlePlayerFullHealth: 0,
castlePlayerHealthLost: 0,
castlePlayerNoDamageGiven: 0
};
// Clear game over overlays
if (gameOverOverlay && typeof gameOverOverlay.destroy === "function") {
gameOverOverlay.destroy();
gameOverOverlay = null;
}
if (gameOverTimer) {
LK.clearTimeout(gameOverTimer);
gameOverTimer = null;
}
// Clear enemy deploy timer
if (enemyDeployTimer) {
LK.clearInterval(enemyDeployTimer);
}
// Update GUI
updateGui();
// Recreate and show difficulty selection overlay
diffOverlay = new Container();
diffOverlay.zIndex = 10000;
diffOverlay.width = GAME_W;
diffOverlay.height = GAME_H;
// Background
overlayBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
overlayBg.width = GAME_W;
overlayBg.height = GAME_H;
overlayBg.alpha = 0.7;
overlayBg.x = 0;
overlayBg.y = 0;
diffOverlay.addChild(overlayBg);
// Recreate all difficulty selection elements
diffTitleBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
diffTitleBg.width = 900;
diffTitleBg.height = 160;
diffTitleBg.alpha = 0.95;
diffTitleBg.anchor.set(0.5, 0.5);
diffTitleBg.x = GAME_W / 2;
diffTitleBg.y = GAME_H / 2 - 300;
diffOverlay.addChild(diffTitleBg);
diffTitle = new Text2("Select Difficulty", {
size: 120,
fill: "#fff",
font: "PressStart2P,Pixel,monospace"
});
diffTitle.anchor.set(0.5, 0.5);
diffTitle.x = GAME_W / 2;
diffTitle.y = GAME_H / 2 - 300;
diffOverlay.addChild(diffTitle);
// Easy button
btnEasyBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnEasyBg.width = 600;
btnEasyBg.height = 120;
btnEasyBg.alpha = 0.95;
btnEasyBg.anchor.set(0.5, 0.5);
btnEasyBg.x = GAME_W / 2;
btnEasyBg.y = GAME_H / 2 - 80;
diffOverlay.addChild(btnEasyBg);
btnEasy = new Text2("Easy", {
size: 100,
fill: 0x00FF00,
font: "PressStart2P,Pixel,monospace"
});
btnEasy.anchor.set(0.5, 0.5);
btnEasy.x = GAME_W / 2;
btnEasy.y = GAME_H / 2 - 80;
btnEasy.down = function (x, y, obj) {
selectDifficulty("easy");
};
diffOverlay.addChild(btnEasy);
// Normal button
btnNormalBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnNormalBg.width = 600;
btnNormalBg.height = 120;
btnNormalBg.alpha = 0.95;
btnNormalBg.anchor.set(0.5, 0.5);
btnNormalBg.x = GAME_W / 2;
btnNormalBg.y = GAME_H / 2 + 80;
diffOverlay.addChild(btnNormalBg);
btnNormal = new Text2("Normal", {
size: 100,
fill: 0xFFFF00,
font: "PressStart2P,Pixel,monospace"
});
btnNormal.anchor.set(0.5, 0.5);
btnNormal.x = GAME_W / 2;
btnNormal.y = GAME_H / 2 + 80;
btnNormal.down = function (x, y, obj) {
selectDifficulty("normal");
};
diffOverlay.addChild(btnNormal);
// Hard button
btnHardBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnHardBg.width = 600;
btnHardBg.height = 120;
btnHardBg.alpha = 0.95;
btnHardBg.anchor.set(0.5, 0.5);
btnHardBg.x = GAME_W / 2;
btnHardBg.y = GAME_H / 2 + 240;
diffOverlay.addChild(btnHardBg);
btnHard = new Text2("Hard", {
size: 100,
fill: 0xFF0000,
font: "PressStart2P,Pixel,monospace"
});
btnHard.anchor.set(0.5, 0.5);
btnHard.x = GAME_W / 2;
btnHard.y = GAME_H / 2 + 240;
btnHard.down = function (x, y, obj) {
selectDifficulty("hard");
};
diffOverlay.addChild(btnHard);
// Speed Mode button
btnSpeedBg = new Text2(" ", {
size: 10,
fill: 0x000000
});
btnSpeedBg.width = 600;
btnSpeedBg.height = 120;
btnSpeedBg.alpha = 0.95;
btnSpeedBg.anchor.set(0.5, 0.5);
btnSpeedBg.x = GAME_W / 2;
btnSpeedBg.y = GAME_H / 2 + 400;
diffOverlay.addChild(btnSpeedBg);
btnSpeed = new Text2("Speed Mode: OFF", {
size: 80,
fill: 0x00E0FF,
font: "PressStart2P,Pixel,monospace"
});
btnSpeed.anchor.set(0.5, 0.5);
btnSpeed.x = GAME_W / 2;
btnSpeed.y = GAME_H / 2 + 400;
btnSpeed.down = function (x, y, obj) {
speedModeMenuSelected = !speedModeMenuSelected;
btnSpeed.setText(speedModeMenuSelected ? "Speed Mode: ON" : "Speed Mode: OFF");
};
diffOverlay.addChild(btnSpeed);
game.addChild(diffOverlay);
}, 5000);
}
// --- Clean up timers on game over ---
game.destroy = function () {
LK.clearInterval(goldTimer);
LK.clearInterval(enemyDeployTimer);
};
ottoman castle. In-Game asset. 2d. High contrast. No shadows. pixel art
roma knight. In-Game asset. 2d. High contrast. No shadows
Ottoman Janissary. In-Game asset. 2d. High contrast. No shadows
sword. In-Game asset. 2d. High contrast. No shadows
Ottoman camel warrior. In-Game asset. 2d. High contrast. No shadows. pixel art
Roman cavalry. In-Game asset. 2d. High contrast. No shadows
camel face. In-Game asset. 2d. High contrast. No shadows. pixel art
wall icon game. In-Game asset. 2d. High contrast. No shadows
RED CRESCENT ICON. In-Game asset. 2d. High contrast. No shadows
HEALING TENT OTTOMAN. In-Game asset. 2d. High contrast. No shadows