User prompt
gold should be automatically transferred to our account. A new soldier can be bought with every 10 gold.
User prompt
Add zoom in and zoom out.
User prompt
Gold should be collectable. And we can buy new soldiers with each 5 gold. Slow down bullets by 50%.
User prompt
make the gate in the middle of the towers
User prompt
Decrease zombies spead. Gate shouldbe middle of castle
Code edit (1 edits merged)
Please save this source code
User prompt
Last Gate: Castle Defense
Initial prompt
Game Prompt Title: “Last Gate: Castle Defense” Prompt: You have one castle. Watchtowers stand on its walls. Every 3 seconds, waves of zombies approach from all directions. Your soldiers on the towers fight them off automatically. Each defeated zombie drops gold. Use the gold to recruit new soldiers and upgrade existing ones. Your goal: protect the castle gate at all costs.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Bullet fired by soldiers var Bullet = Container.expand(function () { var self = Container.call(this); var bulletAsset = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 16; self.target = null; // Zombie instance self.damage = 1; self.update = function () { if (!self.target || self.target.isDead) { 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 < 32) { // Hit! self.target.takeDamage(self.damage); self.destroy(); return; } var step = self.speed; self.x += dx / dist * step; self.y += dy / dist * step; }; return self; }); // Fireball projectile thrown by FireballZombie var Fireball = Container.expand(function () { var self = Container.call(this); var fbAsset = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); fbAsset.tint = 0xFF6600; self.targetX = 0; self.targetY = 0; self.speed = 18; self.update = function () { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 32) { // Hit gate! game.gateHp--; LK.effects.flashObject(game.castleGate, 0xFF6600, 400); if (game.gateHp <= 0) { LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } self.destroy(); return; } var step = self.speed; self.x += dx / dist * step; self.y += dy / dist * step; }; return self; }); // FireballZombie: throws fireballs at the castle gate after 20s var FireballZombie = Container.expand(function () { var self = Container.call(this); var zombieAsset = self.attachAsset('zombie', { anchorX: 0.5, anchorY: 0.5 }); self.maxHp = 3; self.hp = self.maxHp; self.speed = (1.2 + Math.random() * 1.2) / 2; self.isDead = false; self.targetX = 0; self.targetY = 0; self.fireballCooldown = 90 + Math.floor(Math.random() * 60); // 1.5s-2.5s self.lastFireballTick = 0; self.takeDamage = function (dmg) { if (self.isDead) return; self.hp -= dmg; if (self.hp <= 0) { self.die(); } else { tween(self, { tint: 0xFF6600 }, { duration: 80, onFinish: function onFinish() { tween(self, { tint: 0x4CAF50 }, { duration: 120 }); } }); } }; self.die = function () { if (self.isDead) return; self.isDead = true; LK.getSound('zombieDie').play(); game.gold += 2; updateGoldText(); tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); }; self.update = function () { if (self.isDead) return; // Find the wall segment this zombie is closest to (the wall between two towers) var closestWall = null; var minWallDist = 99999; for (var i = 0; i < walls.length; i++) { var wall = walls[i]; if (wall.isDestroyed) continue; var dxw = wall.x - self.x; var dyw = wall.y - self.y; var dWall = Math.sqrt(dxw * dxw + dyw * dyw); if (dWall < minWallDist) { minWallDist = dWall; closestWall = wall; } } // If there is a wall in the way, attack it first if (closestWall && minWallDist < closestWall.width / 2 + 40) { self.attackTimer = self.attackTimer || 0; self.attackTimer--; if (self.attackTimer <= 0) { closestWall.takeDamage(1); tween(self, { scaleX: 1.15, scaleY: 0.85 }, { duration: 60, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 60 }); } }); self.attackTimer = 40; } return; } // Fireball attack if in range of gate (distance < 500) var dxGate = game.castleGate.x - self.x; var dyGate = game.castleGate.y - self.y; var distGate = Math.sqrt(dxGate * dxGate + dyGate * dyGate); if (distGate < 500) { self.fireballCooldown--; if (self.fireballCooldown <= 0) { // Throw fireball at gate var fb = new Fireball(); fb.x = self.x; fb.y = self.y; fb.targetX = game.castleGate.x; fb.targetY = game.castleGate.y; fireballs.push(fb); game.addChild(fb); self.fireballCooldown = 90 + Math.floor(Math.random() * 60); } } // Move toward the gate var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 30) { self.isDead = true; self.destroy(); game.gateHp--; LK.effects.flashObject(game.castleGate, 0xFF6600, 400); if (game.gateHp <= 0) { LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } return; } var step = self.speed; self.x += dx / dist * step; self.y += dy / dist * step; }; return self; }); // Gold coin dropped by zombies var Gold = Container.expand(function () { var self = Container.call(this); var goldAsset = self.attachAsset('gold', { anchorX: 0.5, anchorY: 0.5 }); self.value = 1; self.life = 180; // 3 seconds self.update = function () { self.life--; if (self.life <= 0) { self.destroy(); } }; return self; }); // Zombie that shoots bullets at the castle gate after 20s var ShooterZombie = Container.expand(function () { var self = Container.call(this); var zombieAsset = self.attachAsset('zombie', { anchorX: 0.5, anchorY: 0.5 }); self.maxHp = 2; self.hp = self.maxHp; self.speed = (1.2 + Math.random() * 1.2) / 2; self.isDead = false; self.targetX = 0; self.targetY = 0; self.bulletCooldown = 70 + Math.floor(Math.random() * 40); // 1.2s-1.8s self.lastBulletTick = 0; self.takeDamage = function (dmg) { if (self.isDead) return; self.hp -= dmg; if (self.hp <= 0) { self.die(); } else { tween(self, { tint: 0x99CCFF }, { duration: 80, onFinish: function onFinish() { tween(self, { tint: 0x4CAF50 }, { duration: 120 }); } }); } }; self.die = function () { if (self.isDead) return; self.isDead = true; LK.getSound('zombieDie').play(); game.gold += 2; updateGoldText(); tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); }; self.update = function () { if (self.isDead) return; // Find the wall segment this zombie is closest to var closestWall = null; var minWallDist = 99999; for (var i = 0; i < walls.length; i++) { var wall = walls[i]; if (wall.isDestroyed) continue; var dxw = wall.x - self.x; var dyw = wall.y - self.y; var dWall = Math.sqrt(dxw * dxw + dyw * dyw); if (dWall < minWallDist) { minWallDist = dWall; closestWall = wall; } } // If there is a wall in the way, attack it first if (closestWall && minWallDist < closestWall.width / 2 + 40) { self.attackTimer = self.attackTimer || 0; self.attackTimer--; if (self.attackTimer <= 0) { closestWall.takeDamage(1); tween(self, { scaleX: 1.15, scaleY: 0.85 }, { duration: 60, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 60 }); } }); self.attackTimer = 40; } return; } // Bullet attack if in range of gate (distance < 500) var dxGate = game.castleGate.x - self.x; var dyGate = game.castleGate.y - self.y; var distGate = Math.sqrt(dxGate * dxGate + dyGate * dyGate); if (distGate < 500) { self.bulletCooldown--; if (self.bulletCooldown <= 0) { // Shoot bullet at gate var bullet = new Bullet(); bullet.x = self.x; bullet.y = self.y; bullet.target = { x: game.castleGate.x, y: game.castleGate.y, isDead: false, takeDamage: function takeDamage() { game.gateHp--; LK.effects.flashObject(game.castleGate, 0x99CCFF, 400); if (game.gateHp <= 0) { LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } } }; bullet.damage = 1; bullets.push(bullet); game.addChild(bullet); self.bulletCooldown = 70 + Math.floor(Math.random() * 40); } } // Move toward the gate var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 30) { self.isDead = true; self.destroy(); game.gateHp--; LK.effects.flashObject(game.castleGate, 0x99CCFF, 400); if (game.gateHp <= 0) { LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } return; } var step = self.speed; self.x += dx / dist * step; self.y += dy / dist * step; }; return self; }); // Soldier on a watchtower var Soldier = Container.expand(function () { var self = Container.call(this); var soldierAsset = self.attachAsset('soldier', { anchorX: 0.5, anchorY: 0.5 }); self.level = 1; self.fireRate = 60; // frames between shots self.fireTimer = 0; self.range = 600; self.upgradeCost = 5; self.parentTower = null; self.update = function () { if (!self.parentTower) return; self.fireTimer--; if (self.fireTimer <= 0) { // Find nearest zombie in range var nearest = null; var minDist = self.range; for (var i = 0; i < zombies.length; i++) { var z = zombies[i]; if (z.isDead) continue; var dx = z.x - self.x; var dy = z.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = z; } } if (nearest) { // Fire! var bullet = new Bullet(); bullet.x = self.x; bullet.y = self.y; bullet.target = nearest; bullet.damage = self.level; bullets.push(bullet); game.addChild(bullet); LK.getSound('shoot').play(); self.fireTimer = Math.max(20, self.fireRate - self.level * 8); } } }; self.upgrade = function () { self.level++; self.fireRate = Math.max(20, self.fireRate - 8); self.upgradeCost += 5; // Animate upgrade tween(self, { scaleX: 1.2, scaleY: 1.2 }, { duration: 120, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 120 }); } }); }; return self; }); // Wall between towers var Wall = Container.expand(function () { var self = Container.call(this); // Use a simple box shape for the wall var wallAsset = self.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); self.maxHp = 50; self.hp = self.maxHp; self.indexA = 0; // index of first tower self.indexB = 0; // index of second tower self.isDestroyed = false; self.takeDamage = function (dmg) { if (self.isDestroyed) return; self.hp -= dmg; if (self.hp <= 0) { self.hp = 0; self.isDestroyed = true; // Animate wall breaking tween(self, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { self.visible = false; } }); } else { // Flash red tween(self, { tint: 0xFF0000 }, { duration: 80, onFinish: function onFinish() { tween(self, { tint: 0xCCCCCC }, { duration: 120 }); } }); } }; self.repair = function () { self.hp = self.maxHp; self.isDestroyed = false; self.visible = true; self.alpha = 1; self.tint = 0xCCCCCC; }; return self; }); // Watchtower (can have a soldier) var Watchtower = Container.expand(function () { var self = Container.call(this); var towerAsset = self.attachAsset('watchtower', { anchorX: 0.5, anchorY: 1 }); self.soldier = null; self.index = 0; // For tap detection self.down = function (x, y, obj) { if (!self.soldier) { // Recruit soldier if enough gold (5) if (game.gold >= 5) { game.gold -= 5; var s = new Soldier(); s.x = self.x; s.y = self.y - 80; s.parentTower = self; self.soldier = s; soldiers.push(s); game.addChild(s); updateGoldText(); // Animate tween(s, { alpha: 0 }, { duration: 0, onFinish: function onFinish() { tween(s, { alpha: 1 }, { duration: 200 }); } }); } } else { // Upgrade soldier if (game.gold >= self.soldier.upgradeCost) { game.gold -= self.soldier.upgradeCost; self.soldier.upgrade(); updateGoldText(); } } }; return self; }); // Zombie enemy var Zombie = Container.expand(function () { var self = Container.call(this); var zombieAsset = self.attachAsset('zombie', { anchorX: 0.5, anchorY: 0.5 }); self.maxHp = 2; self.hp = self.maxHp; self.speed = (2 + Math.random() * 1.5) / 2; self.isDead = false; self.targetX = 0; self.targetY = 0; self.takeDamage = function (dmg) { if (self.isDead) return; self.hp -= dmg; if (self.hp <= 0) { self.die(); } else { // Flash red tween(self, { tint: 0xFF0000 }, { duration: 80, onFinish: function onFinish() { tween(self, { tint: 0x4CAF50 }, { duration: 120 }); } }); } }; self.die = function () { if (self.isDead) return; self.isDead = true; LK.getSound('zombieDie').play(); // Instantly collect gold game.gold += 1; updateGoldText(); // Animate fade out tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); }; self.update = function () { if (self.isDead) return; // Find the wall segment this zombie is closest to (the wall between two towers) var closestWall = null; var minWallDist = 99999; for (var i = 0; i < walls.length; i++) { var wall = walls[i]; // Only consider walls that are not destroyed if (wall.isDestroyed) continue; // Distance from zombie to wall center var dxw = wall.x - self.x; var dyw = wall.y - self.y; var dWall = Math.sqrt(dxw * dxw + dyw * dyw); if (dWall < minWallDist) { minWallDist = dWall; closestWall = wall; } } // If there is a wall in the way, attack it first if (closestWall && minWallDist < closestWall.width / 2 + 40) { // Attack wall self.attackTimer = self.attackTimer || 0; self.attackTimer--; if (self.attackTimer <= 0) { closestWall.takeDamage(1); // Animate zombie attack tween(self, { scaleX: 1.15, scaleY: 0.85 }, { duration: 60, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 60 }); } }); self.attackTimer = 40; } // Don't move through the wall return; } // If no wall blocks the way, move toward the gate var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 30) { // Reached gate self.isDead = true; self.destroy(); // Damage gate game.gateHp--; LK.effects.flashObject(game.castleGate, 0xFF0000, 400); if (game.gateHp <= 0) { LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } return; } var step = self.speed; self.x += dx / dist * step; self.y += dy / dist * step; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222233 }); /**** * Game Code ****/ // Music // Sound for zombie death // Sound for collecting gold // Sound for shooting // Gold coin // Bullet // Zombie // Soldier // Watchtower base // Castle gate (center bottom) // Play music LK.playMusic('castleTheme'); // Game variables var zombies = []; var bullets = []; var golds = []; var soldiers = []; var towers = []; var fireballs = []; game.gateHp = 5; game.gold = 10; game.wave = 1; game.waveTimer = 0; // Arrange towers in a regular octagon shape and center the gate at the bottom var octCenterX = 1024; var octCenterY = 1400; var octRadius = 700; var numTowers = 8; var towerPositions = []; for (var i = 0; i < numTowers; i++) { // Octagon: 8 points, 360/8 = 45deg per point, start at -45deg (rotated 45deg from top) var angle = -Math.PI / 4 + i * (2 * Math.PI / numTowers); var x = octCenterX + Math.cos(angle) * octRadius; var y = octCenterY + Math.sin(angle) * octRadius; towerPositions.push({ x: x, y: y }); } // Place walls between each pair of adjacent towers var walls = []; for (var i = 0; i < numTowers; i++) { var a = towerPositions[i]; var b = towerPositions[(i + 1) % numTowers]; var wall = new Wall(); wall.indexA = i; wall.indexB = (i + 1) % numTowers; // Position wall at midpoint between towers wall.x = (a.x + b.x) / 2; wall.y = (a.y + b.y) / 2; // Set wall rotation to match the line between towers var dx = b.x - a.x; var dy = b.y - a.y; wall.rotation = Math.atan2(dy, dx); // Set wall length to distance between towers minus some margin var dist = Math.sqrt(dx * dx + dy * dy); wall.width = dist - 40; wall.height = 40; wall.zIndex = 1; walls.push(wall); game.addChild(wall); } // Place the castle gate exactly at the center of the octagon var gateX = octCenterX; var gateY = octCenterY; game.castleGate = LK.getAsset('castleGate', { anchorX: 0.5, anchorY: 0.5, x: gateX, y: gateY }); game.addChild(game.castleGate); for (var i = 0; i < towerPositions.length; i++) { var pos = towerPositions[i]; var prev = towerPositions[(i - 1 + numTowers) % numTowers]; var next = towerPositions[(i + 1) % numTowers]; // Find the direction from this tower to the center var toCenterX = octCenterX - pos.x; var toCenterY = octCenterY - pos.y; var toCenterLen = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY); // The tower asset's anchor is (0.5, 1), so y is at the base. // Move the tower so its base is flush with the wall ends. // The wall thickness is 40, so offset by half the wall height (20) toward the center. var offset = 20; var adjX = pos.x + toCenterX / toCenterLen * offset; var adjY = pos.y + toCenterY / toCenterLen * offset; var tower = new Watchtower(); tower.x = adjX; tower.y = adjY; tower.index = i; // Rotate each tower to face outward from the center, forming a connected octagon // The outward angle is the angle from the center to the tower, plus 90deg (PI/2) to align the tower's front outward var outwardAngle = Math.atan2(pos.y - octCenterY, pos.x - octCenterX) + Math.PI / 2; tower.rotation = outwardAngle; towers.push(tower); game.addChild(tower); // Add a number label above each tower (1-based) var towerNumTxt = new Text2('' + (i + 1), { size: 80, fill: 0xFFFFFF }); towerNumTxt.anchor.set(0.5, 1); // Place the label just above the tower top (tower asset is 220px tall, anchorY=1) towerNumTxt.x = 0; towerNumTxt.y = -230; tower.addChild(towerNumTxt); } // GUI: Gold display var goldTxt = new Text2('Gold: 10', { size: 90, fill: 0xFFD700 }); goldTxt.anchor.set(0.5, 0); LK.gui.top.addChild(goldTxt); // GUI: Gate HP display var hpTxt = new Text2('Gate: 5', { size: 90, fill: 0xFFFFFF }); hpTxt.anchor.set(0.5, 0); LK.gui.topRight.addChild(hpTxt); // GUI: Wave display var waveTxt = new Text2('Wave: 1', { size: 80, fill: 0x00BFFF }); waveTxt.anchor.set(0.5, 0); LK.gui.bottom.addChild(waveTxt); // --- Zoom Controls --- var zoomLevel = 1; var minZoom = 0.5; var maxZoom = 2.0; var zoomStep = 0.2; // Zoom In Button var zoomInBtn = new Text2('+', { size: 120, fill: 0xFFFFFF }); zoomInBtn.anchor.set(0.5, 0.5); zoomInBtn.x = 1800; zoomInBtn.y = 200; zoomInBtn.interactive = true; zoomInBtn.buttonMode = true; zoomInBtn.down = function (x, y, obj) { if (zoomLevel < maxZoom) { zoomLevel += zoomStep; if (zoomLevel > maxZoom) zoomLevel = maxZoom; game.scaleX = zoomLevel; game.scaleY = zoomLevel; } }; LK.gui.right.addChild(zoomInBtn); // Zoom Out Button var zoomOutBtn = new Text2('-', { size: 120, fill: 0xFFFFFF }); zoomOutBtn.anchor.set(0.5, 0.5); zoomOutBtn.x = 1800; zoomOutBtn.y = 350; zoomOutBtn.interactive = true; zoomOutBtn.buttonMode = true; zoomOutBtn.down = function (x, y, obj) { if (zoomLevel > minZoom) { zoomLevel -= zoomStep; if (zoomLevel < minZoom) zoomLevel = minZoom; game.scaleX = zoomLevel; game.scaleY = zoomLevel; } }; LK.gui.right.addChild(zoomOutBtn); function updateGoldText() { goldTxt.setText('Gold: ' + game.gold); } function updateHpText() { hpTxt.setText('Gate: ' + game.gateHp); } function updateWaveText() { waveTxt.setText('Wave: ' + game.wave); } // Handle buying soldiers/upgrades game.down = function (x, y, obj) { // Check if a tower was tapped for (var j = 0; j < towers.length; j++) { var t = towers[j]; var tx = t.x, ty = t.y; if (x > tx - 60 && x < tx + 60 && y > ty - 220 && y < ty) { t.down(x, y, obj); return; } } }; // Main update loop game.update = function () { // Update gate HP and gold updateHpText(); // Track zombie kills for extra spawn if (typeof game.zombieKillCount === "undefined") { game.zombieKillCount = 0; game.extraZombieSpawned = false; } // Update all zombies for (var i = zombies.length - 1; i >= 0; i--) { var z = zombies[i]; z.update(); if (z.isDead && z.alpha === 0) { z.destroy(); zombies.splice(i, 1); // Count zombie kills game.zombieKillCount++; // After 2 kills, spawn an extra Zombie (only once) if (game.zombieKillCount === 2 && !game.extraZombieSpawned) { var extraZ = new Zombie(); // Spawn from random edge var edge = Math.floor(Math.random() * 4); var zx, zy; if (edge === 0) { zx = -60; zy = 1000 + Math.random() * 1200; } else if (edge === 1) { zx = 2048 + 60; zy = 1000 + Math.random() * 1200; } else if (edge === 2) { zx = 400 + Math.random() * 1200; zy = -80; } else { zx = 400 + Math.random() * 1200; zy = 2732 + 80; } extraZ.x = zx; extraZ.y = zy; extraZ.targetX = game.castleGate.x; extraZ.targetY = game.castleGate.y; extraZ.maxHp = 2 + Math.floor(game.wave / 2); extraZ.hp = extraZ.maxHp; extraZ.speed = (1 + Math.random() * 0.7 + game.wave * 0.07) / 2; zombies.push(extraZ); game.addChild(extraZ); game.extraZombieSpawned = true; } } } // Update all bullets for (var i = bullets.length - 1; i >= 0; i--) { var b = bullets[i]; b.update(); if (b.destroyed) { bullets.splice(i, 1); } } // Update all golds for (var i = golds.length - 1; i >= 0; i--) { var g = golds[i]; g.update(); if (g.life <= 0 || g.destroyed) { g.destroy(); golds.splice(i, 1); } } // Update all fireballs for (var i = fireballs.length - 1; i >= 0; i--) { var fb = fireballs[i]; fb.update(); if (fb.destroyed) { fireballs.splice(i, 1); } } // Update all soldiers for (var i = 0; i < soldiers.length; i++) { soldiers[i].update(); } // Spawn new wave every 180 ticks (3 seconds) game.waveTimer++; if (game.waveTimer >= 180) { game.waveTimer = 0; spawnWave(); } }; function spawnWave() { var n = 2 + Math.floor(game.wave * 0.7); for (var i = 0; i < n; i++) { // After 20 seconds (1200 ticks), spawn FireballZombies or ShooterZombies with 30% chance each var useFireballZombie = LK.ticks >= 1200 && Math.random() < 0.3; var useShooterZombie = !useFireballZombie && LK.ticks >= 1200 && Math.random() < 0.3; var z; if (useFireballZombie) { z = new FireballZombie(); } else if (useShooterZombie) { z = new ShooterZombie(); } else { z = new Zombie(); } // Spawn from random edge var edge = Math.floor(Math.random() * 4); var zx, zy; if (edge === 0) { // left zx = -60; zy = 1000 + Math.random() * 1200; } else if (edge === 1) { // right zx = 2048 + 60; zy = 1000 + Math.random() * 1200; } else if (edge === 2) { // top zx = 400 + Math.random() * 1200; zy = -80; } else { // bottom (rare) zx = 400 + Math.random() * 1200; zy = 2732 + 80; } z.x = zx; z.y = zy; // Gate is at the middle of the basket (centered between lowest towers) z.targetX = game.castleGate.x; z.targetY = game.castleGate.y; // Decrease zombie speed (slower base, slower scaling) z.maxHp = 2 + Math.floor(game.wave / 2); z.hp = z.maxHp; z.speed = (1 + Math.random() * 0.7 + game.wave * 0.07) / 2; zombies.push(z); game.addChild(z); } game.wave++; updateWaveText(); }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bullet fired by soldiers
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletAsset = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 16;
self.target = null; // Zombie instance
self.damage = 1;
self.update = function () {
if (!self.target || self.target.isDead) {
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 < 32) {
// Hit!
self.target.takeDamage(self.damage);
self.destroy();
return;
}
var step = self.speed;
self.x += dx / dist * step;
self.y += dy / dist * step;
};
return self;
});
// Fireball projectile thrown by FireballZombie
var Fireball = Container.expand(function () {
var self = Container.call(this);
var fbAsset = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
fbAsset.tint = 0xFF6600;
self.targetX = 0;
self.targetY = 0;
self.speed = 18;
self.update = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 32) {
// Hit gate!
game.gateHp--;
LK.effects.flashObject(game.castleGate, 0xFF6600, 400);
if (game.gateHp <= 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
self.destroy();
return;
}
var step = self.speed;
self.x += dx / dist * step;
self.y += dy / dist * step;
};
return self;
});
// FireballZombie: throws fireballs at the castle gate after 20s
var FireballZombie = Container.expand(function () {
var self = Container.call(this);
var zombieAsset = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.maxHp = 3;
self.hp = self.maxHp;
self.speed = (1.2 + Math.random() * 1.2) / 2;
self.isDead = false;
self.targetX = 0;
self.targetY = 0;
self.fireballCooldown = 90 + Math.floor(Math.random() * 60); // 1.5s-2.5s
self.lastFireballTick = 0;
self.takeDamage = function (dmg) {
if (self.isDead) return;
self.hp -= dmg;
if (self.hp <= 0) {
self.die();
} else {
tween(self, {
tint: 0xFF6600
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
tint: 0x4CAF50
}, {
duration: 120
});
}
});
}
};
self.die = function () {
if (self.isDead) return;
self.isDead = true;
LK.getSound('zombieDie').play();
game.gold += 2;
updateGoldText();
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
self.update = function () {
if (self.isDead) return;
// Find the wall segment this zombie is closest to (the wall between two towers)
var closestWall = null;
var minWallDist = 99999;
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (wall.isDestroyed) continue;
var dxw = wall.x - self.x;
var dyw = wall.y - self.y;
var dWall = Math.sqrt(dxw * dxw + dyw * dyw);
if (dWall < minWallDist) {
minWallDist = dWall;
closestWall = wall;
}
}
// If there is a wall in the way, attack it first
if (closestWall && minWallDist < closestWall.width / 2 + 40) {
self.attackTimer = self.attackTimer || 0;
self.attackTimer--;
if (self.attackTimer <= 0) {
closestWall.takeDamage(1);
tween(self, {
scaleX: 1.15,
scaleY: 0.85
}, {
duration: 60,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 60
});
}
});
self.attackTimer = 40;
}
return;
}
// Fireball attack if in range of gate (distance < 500)
var dxGate = game.castleGate.x - self.x;
var dyGate = game.castleGate.y - self.y;
var distGate = Math.sqrt(dxGate * dxGate + dyGate * dyGate);
if (distGate < 500) {
self.fireballCooldown--;
if (self.fireballCooldown <= 0) {
// Throw fireball at gate
var fb = new Fireball();
fb.x = self.x;
fb.y = self.y;
fb.targetX = game.castleGate.x;
fb.targetY = game.castleGate.y;
fireballs.push(fb);
game.addChild(fb);
self.fireballCooldown = 90 + Math.floor(Math.random() * 60);
}
}
// Move toward the gate
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 30) {
self.isDead = true;
self.destroy();
game.gateHp--;
LK.effects.flashObject(game.castleGate, 0xFF6600, 400);
if (game.gateHp <= 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
return;
}
var step = self.speed;
self.x += dx / dist * step;
self.y += dy / dist * step;
};
return self;
});
// Gold coin dropped by zombies
var Gold = Container.expand(function () {
var self = Container.call(this);
var goldAsset = self.attachAsset('gold', {
anchorX: 0.5,
anchorY: 0.5
});
self.value = 1;
self.life = 180; // 3 seconds
self.update = function () {
self.life--;
if (self.life <= 0) {
self.destroy();
}
};
return self;
});
// Zombie that shoots bullets at the castle gate after 20s
var ShooterZombie = Container.expand(function () {
var self = Container.call(this);
var zombieAsset = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.maxHp = 2;
self.hp = self.maxHp;
self.speed = (1.2 + Math.random() * 1.2) / 2;
self.isDead = false;
self.targetX = 0;
self.targetY = 0;
self.bulletCooldown = 70 + Math.floor(Math.random() * 40); // 1.2s-1.8s
self.lastBulletTick = 0;
self.takeDamage = function (dmg) {
if (self.isDead) return;
self.hp -= dmg;
if (self.hp <= 0) {
self.die();
} else {
tween(self, {
tint: 0x99CCFF
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
tint: 0x4CAF50
}, {
duration: 120
});
}
});
}
};
self.die = function () {
if (self.isDead) return;
self.isDead = true;
LK.getSound('zombieDie').play();
game.gold += 2;
updateGoldText();
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
self.update = function () {
if (self.isDead) return;
// Find the wall segment this zombie is closest to
var closestWall = null;
var minWallDist = 99999;
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (wall.isDestroyed) continue;
var dxw = wall.x - self.x;
var dyw = wall.y - self.y;
var dWall = Math.sqrt(dxw * dxw + dyw * dyw);
if (dWall < minWallDist) {
minWallDist = dWall;
closestWall = wall;
}
}
// If there is a wall in the way, attack it first
if (closestWall && minWallDist < closestWall.width / 2 + 40) {
self.attackTimer = self.attackTimer || 0;
self.attackTimer--;
if (self.attackTimer <= 0) {
closestWall.takeDamage(1);
tween(self, {
scaleX: 1.15,
scaleY: 0.85
}, {
duration: 60,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 60
});
}
});
self.attackTimer = 40;
}
return;
}
// Bullet attack if in range of gate (distance < 500)
var dxGate = game.castleGate.x - self.x;
var dyGate = game.castleGate.y - self.y;
var distGate = Math.sqrt(dxGate * dxGate + dyGate * dyGate);
if (distGate < 500) {
self.bulletCooldown--;
if (self.bulletCooldown <= 0) {
// Shoot bullet at gate
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
bullet.target = {
x: game.castleGate.x,
y: game.castleGate.y,
isDead: false,
takeDamage: function takeDamage() {
game.gateHp--;
LK.effects.flashObject(game.castleGate, 0x99CCFF, 400);
if (game.gateHp <= 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
}
};
bullet.damage = 1;
bullets.push(bullet);
game.addChild(bullet);
self.bulletCooldown = 70 + Math.floor(Math.random() * 40);
}
}
// Move toward the gate
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 30) {
self.isDead = true;
self.destroy();
game.gateHp--;
LK.effects.flashObject(game.castleGate, 0x99CCFF, 400);
if (game.gateHp <= 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
return;
}
var step = self.speed;
self.x += dx / dist * step;
self.y += dy / dist * step;
};
return self;
});
// Soldier on a watchtower
var Soldier = Container.expand(function () {
var self = Container.call(this);
var soldierAsset = self.attachAsset('soldier', {
anchorX: 0.5,
anchorY: 0.5
});
self.level = 1;
self.fireRate = 60; // frames between shots
self.fireTimer = 0;
self.range = 600;
self.upgradeCost = 5;
self.parentTower = null;
self.update = function () {
if (!self.parentTower) return;
self.fireTimer--;
if (self.fireTimer <= 0) {
// Find nearest zombie in range
var nearest = null;
var minDist = self.range;
for (var i = 0; i < zombies.length; i++) {
var z = zombies[i];
if (z.isDead) continue;
var dx = z.x - self.x;
var dy = z.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = z;
}
}
if (nearest) {
// Fire!
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
bullet.target = nearest;
bullet.damage = self.level;
bullets.push(bullet);
game.addChild(bullet);
LK.getSound('shoot').play();
self.fireTimer = Math.max(20, self.fireRate - self.level * 8);
}
}
};
self.upgrade = function () {
self.level++;
self.fireRate = Math.max(20, self.fireRate - 8);
self.upgradeCost += 5;
// Animate upgrade
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120
});
}
});
};
return self;
});
// Wall between towers
var Wall = Container.expand(function () {
var self = Container.call(this);
// Use a simple box shape for the wall
var wallAsset = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
self.maxHp = 50;
self.hp = self.maxHp;
self.indexA = 0; // index of first tower
self.indexB = 0; // index of second tower
self.isDestroyed = false;
self.takeDamage = function (dmg) {
if (self.isDestroyed) return;
self.hp -= dmg;
if (self.hp <= 0) {
self.hp = 0;
self.isDestroyed = true;
// Animate wall breaking
tween(self, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
self.visible = false;
}
});
} else {
// Flash red
tween(self, {
tint: 0xFF0000
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
tint: 0xCCCCCC
}, {
duration: 120
});
}
});
}
};
self.repair = function () {
self.hp = self.maxHp;
self.isDestroyed = false;
self.visible = true;
self.alpha = 1;
self.tint = 0xCCCCCC;
};
return self;
});
// Watchtower (can have a soldier)
var Watchtower = Container.expand(function () {
var self = Container.call(this);
var towerAsset = self.attachAsset('watchtower', {
anchorX: 0.5,
anchorY: 1
});
self.soldier = null;
self.index = 0;
// For tap detection
self.down = function (x, y, obj) {
if (!self.soldier) {
// Recruit soldier if enough gold (5)
if (game.gold >= 5) {
game.gold -= 5;
var s = new Soldier();
s.x = self.x;
s.y = self.y - 80;
s.parentTower = self;
self.soldier = s;
soldiers.push(s);
game.addChild(s);
updateGoldText();
// Animate
tween(s, {
alpha: 0
}, {
duration: 0,
onFinish: function onFinish() {
tween(s, {
alpha: 1
}, {
duration: 200
});
}
});
}
} else {
// Upgrade soldier
if (game.gold >= self.soldier.upgradeCost) {
game.gold -= self.soldier.upgradeCost;
self.soldier.upgrade();
updateGoldText();
}
}
};
return self;
});
// Zombie enemy
var Zombie = Container.expand(function () {
var self = Container.call(this);
var zombieAsset = self.attachAsset('zombie', {
anchorX: 0.5,
anchorY: 0.5
});
self.maxHp = 2;
self.hp = self.maxHp;
self.speed = (2 + Math.random() * 1.5) / 2;
self.isDead = false;
self.targetX = 0;
self.targetY = 0;
self.takeDamage = function (dmg) {
if (self.isDead) return;
self.hp -= dmg;
if (self.hp <= 0) {
self.die();
} else {
// Flash red
tween(self, {
tint: 0xFF0000
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
tint: 0x4CAF50
}, {
duration: 120
});
}
});
}
};
self.die = function () {
if (self.isDead) return;
self.isDead = true;
LK.getSound('zombieDie').play();
// Instantly collect gold
game.gold += 1;
updateGoldText();
// Animate fade out
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
self.update = function () {
if (self.isDead) return;
// Find the wall segment this zombie is closest to (the wall between two towers)
var closestWall = null;
var minWallDist = 99999;
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
// Only consider walls that are not destroyed
if (wall.isDestroyed) continue;
// Distance from zombie to wall center
var dxw = wall.x - self.x;
var dyw = wall.y - self.y;
var dWall = Math.sqrt(dxw * dxw + dyw * dyw);
if (dWall < minWallDist) {
minWallDist = dWall;
closestWall = wall;
}
}
// If there is a wall in the way, attack it first
if (closestWall && minWallDist < closestWall.width / 2 + 40) {
// Attack wall
self.attackTimer = self.attackTimer || 0;
self.attackTimer--;
if (self.attackTimer <= 0) {
closestWall.takeDamage(1);
// Animate zombie attack
tween(self, {
scaleX: 1.15,
scaleY: 0.85
}, {
duration: 60,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 60
});
}
});
self.attackTimer = 40;
}
// Don't move through the wall
return;
}
// If no wall blocks the way, move toward the gate
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 30) {
// Reached gate
self.isDead = true;
self.destroy();
// Damage gate
game.gateHp--;
LK.effects.flashObject(game.castleGate, 0xFF0000, 400);
if (game.gateHp <= 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
return;
}
var step = self.speed;
self.x += dx / dist * step;
self.y += dy / dist * step;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222233
});
/****
* Game Code
****/
// Music
// Sound for zombie death
// Sound for collecting gold
// Sound for shooting
// Gold coin
// Bullet
// Zombie
// Soldier
// Watchtower base
// Castle gate (center bottom)
// Play music
LK.playMusic('castleTheme');
// Game variables
var zombies = [];
var bullets = [];
var golds = [];
var soldiers = [];
var towers = [];
var fireballs = [];
game.gateHp = 5;
game.gold = 10;
game.wave = 1;
game.waveTimer = 0;
// Arrange towers in a regular octagon shape and center the gate at the bottom
var octCenterX = 1024;
var octCenterY = 1400;
var octRadius = 700;
var numTowers = 8;
var towerPositions = [];
for (var i = 0; i < numTowers; i++) {
// Octagon: 8 points, 360/8 = 45deg per point, start at -45deg (rotated 45deg from top)
var angle = -Math.PI / 4 + i * (2 * Math.PI / numTowers);
var x = octCenterX + Math.cos(angle) * octRadius;
var y = octCenterY + Math.sin(angle) * octRadius;
towerPositions.push({
x: x,
y: y
});
}
// Place walls between each pair of adjacent towers
var walls = [];
for (var i = 0; i < numTowers; i++) {
var a = towerPositions[i];
var b = towerPositions[(i + 1) % numTowers];
var wall = new Wall();
wall.indexA = i;
wall.indexB = (i + 1) % numTowers;
// Position wall at midpoint between towers
wall.x = (a.x + b.x) / 2;
wall.y = (a.y + b.y) / 2;
// Set wall rotation to match the line between towers
var dx = b.x - a.x;
var dy = b.y - a.y;
wall.rotation = Math.atan2(dy, dx);
// Set wall length to distance between towers minus some margin
var dist = Math.sqrt(dx * dx + dy * dy);
wall.width = dist - 40;
wall.height = 40;
wall.zIndex = 1;
walls.push(wall);
game.addChild(wall);
}
// Place the castle gate exactly at the center of the octagon
var gateX = octCenterX;
var gateY = octCenterY;
game.castleGate = LK.getAsset('castleGate', {
anchorX: 0.5,
anchorY: 0.5,
x: gateX,
y: gateY
});
game.addChild(game.castleGate);
for (var i = 0; i < towerPositions.length; i++) {
var pos = towerPositions[i];
var prev = towerPositions[(i - 1 + numTowers) % numTowers];
var next = towerPositions[(i + 1) % numTowers];
// Find the direction from this tower to the center
var toCenterX = octCenterX - pos.x;
var toCenterY = octCenterY - pos.y;
var toCenterLen = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY);
// The tower asset's anchor is (0.5, 1), so y is at the base.
// Move the tower so its base is flush with the wall ends.
// The wall thickness is 40, so offset by half the wall height (20) toward the center.
var offset = 20;
var adjX = pos.x + toCenterX / toCenterLen * offset;
var adjY = pos.y + toCenterY / toCenterLen * offset;
var tower = new Watchtower();
tower.x = adjX;
tower.y = adjY;
tower.index = i;
// Rotate each tower to face outward from the center, forming a connected octagon
// The outward angle is the angle from the center to the tower, plus 90deg (PI/2) to align the tower's front outward
var outwardAngle = Math.atan2(pos.y - octCenterY, pos.x - octCenterX) + Math.PI / 2;
tower.rotation = outwardAngle;
towers.push(tower);
game.addChild(tower);
// Add a number label above each tower (1-based)
var towerNumTxt = new Text2('' + (i + 1), {
size: 80,
fill: 0xFFFFFF
});
towerNumTxt.anchor.set(0.5, 1);
// Place the label just above the tower top (tower asset is 220px tall, anchorY=1)
towerNumTxt.x = 0;
towerNumTxt.y = -230;
tower.addChild(towerNumTxt);
}
// GUI: Gold display
var goldTxt = new Text2('Gold: 10', {
size: 90,
fill: 0xFFD700
});
goldTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(goldTxt);
// GUI: Gate HP display
var hpTxt = new Text2('Gate: 5', {
size: 90,
fill: 0xFFFFFF
});
hpTxt.anchor.set(0.5, 0);
LK.gui.topRight.addChild(hpTxt);
// GUI: Wave display
var waveTxt = new Text2('Wave: 1', {
size: 80,
fill: 0x00BFFF
});
waveTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(waveTxt);
// --- Zoom Controls ---
var zoomLevel = 1;
var minZoom = 0.5;
var maxZoom = 2.0;
var zoomStep = 0.2;
// Zoom In Button
var zoomInBtn = new Text2('+', {
size: 120,
fill: 0xFFFFFF
});
zoomInBtn.anchor.set(0.5, 0.5);
zoomInBtn.x = 1800;
zoomInBtn.y = 200;
zoomInBtn.interactive = true;
zoomInBtn.buttonMode = true;
zoomInBtn.down = function (x, y, obj) {
if (zoomLevel < maxZoom) {
zoomLevel += zoomStep;
if (zoomLevel > maxZoom) zoomLevel = maxZoom;
game.scaleX = zoomLevel;
game.scaleY = zoomLevel;
}
};
LK.gui.right.addChild(zoomInBtn);
// Zoom Out Button
var zoomOutBtn = new Text2('-', {
size: 120,
fill: 0xFFFFFF
});
zoomOutBtn.anchor.set(0.5, 0.5);
zoomOutBtn.x = 1800;
zoomOutBtn.y = 350;
zoomOutBtn.interactive = true;
zoomOutBtn.buttonMode = true;
zoomOutBtn.down = function (x, y, obj) {
if (zoomLevel > minZoom) {
zoomLevel -= zoomStep;
if (zoomLevel < minZoom) zoomLevel = minZoom;
game.scaleX = zoomLevel;
game.scaleY = zoomLevel;
}
};
LK.gui.right.addChild(zoomOutBtn);
function updateGoldText() {
goldTxt.setText('Gold: ' + game.gold);
}
function updateHpText() {
hpTxt.setText('Gate: ' + game.gateHp);
}
function updateWaveText() {
waveTxt.setText('Wave: ' + game.wave);
}
// Handle buying soldiers/upgrades
game.down = function (x, y, obj) {
// Check if a tower was tapped
for (var j = 0; j < towers.length; j++) {
var t = towers[j];
var tx = t.x,
ty = t.y;
if (x > tx - 60 && x < tx + 60 && y > ty - 220 && y < ty) {
t.down(x, y, obj);
return;
}
}
};
// Main update loop
game.update = function () {
// Update gate HP and gold
updateHpText();
// Track zombie kills for extra spawn
if (typeof game.zombieKillCount === "undefined") {
game.zombieKillCount = 0;
game.extraZombieSpawned = false;
}
// Update all zombies
for (var i = zombies.length - 1; i >= 0; i--) {
var z = zombies[i];
z.update();
if (z.isDead && z.alpha === 0) {
z.destroy();
zombies.splice(i, 1);
// Count zombie kills
game.zombieKillCount++;
// After 2 kills, spawn an extra Zombie (only once)
if (game.zombieKillCount === 2 && !game.extraZombieSpawned) {
var extraZ = new Zombie();
// Spawn from random edge
var edge = Math.floor(Math.random() * 4);
var zx, zy;
if (edge === 0) {
zx = -60;
zy = 1000 + Math.random() * 1200;
} else if (edge === 1) {
zx = 2048 + 60;
zy = 1000 + Math.random() * 1200;
} else if (edge === 2) {
zx = 400 + Math.random() * 1200;
zy = -80;
} else {
zx = 400 + Math.random() * 1200;
zy = 2732 + 80;
}
extraZ.x = zx;
extraZ.y = zy;
extraZ.targetX = game.castleGate.x;
extraZ.targetY = game.castleGate.y;
extraZ.maxHp = 2 + Math.floor(game.wave / 2);
extraZ.hp = extraZ.maxHp;
extraZ.speed = (1 + Math.random() * 0.7 + game.wave * 0.07) / 2;
zombies.push(extraZ);
game.addChild(extraZ);
game.extraZombieSpawned = true;
}
}
}
// Update all bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.update();
if (b.destroyed) {
bullets.splice(i, 1);
}
}
// Update all golds
for (var i = golds.length - 1; i >= 0; i--) {
var g = golds[i];
g.update();
if (g.life <= 0 || g.destroyed) {
g.destroy();
golds.splice(i, 1);
}
}
// Update all fireballs
for (var i = fireballs.length - 1; i >= 0; i--) {
var fb = fireballs[i];
fb.update();
if (fb.destroyed) {
fireballs.splice(i, 1);
}
}
// Update all soldiers
for (var i = 0; i < soldiers.length; i++) {
soldiers[i].update();
}
// Spawn new wave every 180 ticks (3 seconds)
game.waveTimer++;
if (game.waveTimer >= 180) {
game.waveTimer = 0;
spawnWave();
}
};
function spawnWave() {
var n = 2 + Math.floor(game.wave * 0.7);
for (var i = 0; i < n; i++) {
// After 20 seconds (1200 ticks), spawn FireballZombies or ShooterZombies with 30% chance each
var useFireballZombie = LK.ticks >= 1200 && Math.random() < 0.3;
var useShooterZombie = !useFireballZombie && LK.ticks >= 1200 && Math.random() < 0.3;
var z;
if (useFireballZombie) {
z = new FireballZombie();
} else if (useShooterZombie) {
z = new ShooterZombie();
} else {
z = new Zombie();
}
// Spawn from random edge
var edge = Math.floor(Math.random() * 4);
var zx, zy;
if (edge === 0) {
// left
zx = -60;
zy = 1000 + Math.random() * 1200;
} else if (edge === 1) {
// right
zx = 2048 + 60;
zy = 1000 + Math.random() * 1200;
} else if (edge === 2) {
// top
zx = 400 + Math.random() * 1200;
zy = -80;
} else {
// bottom (rare)
zx = 400 + Math.random() * 1200;
zy = 2732 + 80;
}
z.x = zx;
z.y = zy;
// Gate is at the middle of the basket (centered between lowest towers)
z.targetX = game.castleGate.x;
z.targetY = game.castleGate.y;
// Decrease zombie speed (slower base, slower scaling)
z.maxHp = 2 + Math.floor(game.wave / 2);
z.hp = z.maxHp;
z.speed = (1 + Math.random() * 0.7 + game.wave * 0.07) / 2;
zombies.push(z);
game.addChild(z);
}
game.wave++;
updateWaveText();
}
Green zombie. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Gold coin with dollar icon. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Arrow. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Add fingers to left hand. Make the bow and arrow wooden color.
Make it a rectangle.
Castle wall. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Smurf home. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
zombie with fire in hand. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat