/**** * 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