/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Alien class var Alien = Container.expand(function () { var self = Container.call(this); // Attach a default sprite, but allow it to be replaced after construction var alienSprite = self.attachAsset('alien', { anchorX: 0.5, anchorY: 0.5 }); self.width = alienSprite.width; self.height = alienSprite.height; self.speed = 2.5; self.hp = 1; self.wave = 1; self.lastX = self.x; self.lastY = self.y; self.lastIntersecting = false; self.isDead = false; self.update = function () { self.lastY = self.y; self.y += self.speed; }; return self; }); // Bonus box class var BonusBox = Container.expand(function () { var self = Container.call(this); var bonusSprite = self.attachAsset('bonus', { anchorX: 0.5, anchorY: 0.5 }); self.width = bonusSprite.width; self.height = bonusSprite.height; self.lastY = self.y; self.lastIntersecting = false; self.type = null; // Set on spawn self.fallSpeed = 10; // Speed at which the bonus moves toward the player self.update = function () { // Move toward the player's current position if (typeof player !== "undefined" && player !== null) { var dx = player.x - self.x; var dy = player.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 2) { // Normalize and move toward player self.x += dx / dist * self.fallSpeed; self.y += dy / dist * self.fallSpeed; } } }; return self; }); // Boss class var Boss = Container.expand(function () { var self = Container.call(this); var bossSprite = self.attachAsset('alien4', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); self.width = bossSprite.width * 2; self.height = bossSprite.height * 2; self.speed = 4; // Use global bossBaseHp for initial boss health self.hp = typeof bossBaseHp !== "undefined" ? bossBaseHp : 10; self.lastY = self.y; self.lastIntersecting = false; self.isDead = false; self.update = function () { self.lastY = self.y; self.y += self.speed; }; return self; }); // Crystal class var Crystal = Container.expand(function () { var self = Container.call(this); var crystalSprite = self.attachAsset('crystal', { anchorX: 0.5, anchorY: 0.5 }); self.width = crystalSprite.width; self.height = crystalSprite.height; self.lastY = self.y; self.lastIntersecting = false; self.fallSpeed = 12; // Speed at which the crystal falls toward the player self.update = function () { // Move toward the player's current position if (typeof player !== "undefined" && player !== null) { var dx = player.x - self.x; var dy = player.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 2) { // Normalize and move toward player self.x += dx / dist * self.fallSpeed; self.y += dy / dist * self.fallSpeed; } } }; return self; }); // Laser class var Laser = Container.expand(function () { var self = Container.call(this); var laserSprite = self.attachAsset('laser', { anchorX: 0.5, anchorY: 1 }); self.width = laserSprite.width; self.height = laserSprite.height; self.speed = 32; self.pierce = false; self.lastY = self.y; self.lastIntersecting = false; self.update = function () { self.y -= self.speed; }; return self; }); // Player class var Player = Container.expand(function () { var self = Container.call(this); var playerSprite = self.attachAsset('player', { anchorX: 0.5, anchorY: 0.5 }); self.width = playerSprite.width; self.height = playerSprite.height; self.shootCooldown = 0; self.moveSpeed = 32; // px per tick when dragged self.leftLimit = 0 + self.width / 2 + 40; self.rightLimit = 2048 - self.width / 2 - 40; self.powered = false; self.powerTimer = 0; self.powerType = null; // For powerup visuals self.setPower = function (type, duration) { self.powered = true; self.powerType = type; self.powerTimer = duration; if (type === 'rapid') { playerSprite.tint = 0xffe100; } else if (type === 'pierce') { playerSprite.tint = 0x00ffb0; } else if (type === 'shield') { playerSprite.tint = 0x00eaff; } }; self.clearPower = function () { self.powered = false; self.powerType = null; self.powerTimer = 0; playerSprite.tint = 0xffffff; }; self.update = function () { if (self.powered) { self.powerTimer--; if (self.powerTimer <= 0) { self.clearPower(); } } }; return self; }); // Star class for background stars var Star = Container.expand(function () { var self = Container.call(this); // Use a white ellipse as a star, randomize size (smaller, more variety) // Reduced min/max size for smaller stars var size = randInt(1, 2); var starSprite = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: size / 120, scaleY: size / 120, tint: 0xffffff }); self.starSprite = starSprite; // Store for tinting self.width = size; self.height = size; // More parallax: slower stars are smaller, faster are bigger // Speed: 0.2 to 2.2, with more slow stars var speedBase = Math.pow(Math.random(), 2.2); // bias toward 0 self.speed = 0.2 + speedBase * 2.0; self.alpha = 0.4 + Math.random() * 0.6; self.setColor = function (color) { if (self.starSprite) self.starSprite.tint = color; }; self.update = function () { // Only update if star is visible or about to be visible if (self.y > -self.height && self.y < GAME_HEIGHT + self.height * 2) { self.y += self.speed; } else { self.y = -self.height; self.x = randInt(0, GAME_WIDTH); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Music // Sound effects // Bonus box // Crystal (collectible) // Player bullet (laser) // Alien (enemy) // Player // Game constants var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; // --- Starfield background --- // Star color palette (cycle every 2 waves) var STAR_COLORS = [0xffffff, // white 0xffe100, // yellow 0x00eaff, // cyan 0xff00cc, // magenta 0x00ffb0, // green 0xffa500, // orange 0xAAFFFF, // light blue 0xff0000 // red ]; function getStarColorForWave(wave) { var idx = Math.floor((wave - 1) / 2) % STAR_COLORS.length; return STAR_COLORS[idx]; } var NUM_STARS = 320; var stars = []; for (var i = 0; i < NUM_STARS; i++) { var star = new Star(); star.x = randInt(0, GAME_WIDTH); star.y = randInt(0, GAME_HEIGHT); stars.push(star); game.addChild(star); } var GROUND_Y = GAME_HEIGHT - 220; var PLAYER_START_X = GAME_WIDTH / 2; var PLAYER_START_Y = GROUND_Y; var PLAYER_MIN_X = 160 + 40; var PLAYER_MAX_X = GAME_WIDTH - 160 - 40; var ENEMY_START_X_MIN = 160; var ENEMY_START_X_MAX = GAME_WIDTH - 160; // Define Y spawn range for enemies (top margin to bottom margin for bonus box spawn) var ENEMY_START_Y_MIN = 0; var ENEMY_START_Y_MAX = GAME_HEIGHT - 400; // Define 3 fixed road X positions for aliens to spawn and move along (top to bottom) var ROAD_X_POSITIONS = [ENEMY_START_X_MIN + Math.floor((ENEMY_START_X_MAX - ENEMY_START_X_MIN) * 0.2), ENEMY_START_X_MIN + Math.floor((ENEMY_START_X_MAX - ENEMY_START_X_MIN) * 0.5), ENEMY_START_X_MIN + Math.floor((ENEMY_START_X_MAX - ENEMY_START_X_MIN) * 0.8)]; var LASER_COOLDOWN = 18; // ticks var LASER_COOLDOWN_RAPID = 6; // Track current firing cooldown, increases as waves progress var currentLaserCooldown = LASER_COOLDOWN; var currentLaserCooldownRapid = LASER_COOLDOWN_RAPID; var CRYSTAL_COLLECT_DIST = 120; var BONUS_COLLECT_DIST = 120; var CRYSTAL_MOVE_STEP = 180; // How much movement area expands per crystal var MAX_CRYSTALS = 8; var WAVE_ENEMY_BASE = 10; var WAVE_ENEMY_INC = 5; // Increase by 5 each wave var WAVE_SPEED_INC = 0.10; var WAVE_HP_INC = 1; var BONUS_WAVE_INTERVAL = 3; var POWERUP_DURATION = 360; // 6 seconds at 60fps // Game state var player = null; var aliens = []; var lasers = []; var crystals = []; var bonuses = []; var boss = null; var bossActive = false; var bossDefeated = false; // Boss health scaling var bossBaseHp = 10; var bossHpInc = 5; var dragNode = null; var dragOffsetX = 0; var dragOffsetY = 0; var lastMoveX = 0; var lastMoveY = 0; var lastAlienSpawnTick = 0; var wave = 1; var enemiesLeft = 0; var enemiesToSpawn = 0; var enemiesKilled = 0; var aliensKilledThisWave = 0; // Track aliens killed in current wave var crystalsCollected = 0; var leftLimit = PLAYER_MIN_X; var rightLimit = PLAYER_MAX_X; var bonusActive = false; var bonusType = null; // Double shot state for player bonus var doubleShotActive = false; var doubleShotTimer = 0; // Triple shot state for player bonus (after 5th wave) var tripleShotActive = false; var tripleShotTimer = 0; // Removed score and scoreTxt, replaced by crystalTxt var waveTxt = null; var powerupTxt = null; var gameOver = false; var youWin = false; // GUI // Crystal icon in upper left (avoid top left 100x100 for menu) // Crystal icon and count, aligned and sized together var crystalIcon = LK.getAsset('crystal', { anchorX: 0, anchorY: 0.5 }); crystalIcon.x = 20; crystalIcon.y = 160 + 40; // center of 80px icon at 160+40=200 crystalIcon.width = 100; crystalIcon.height = 100; LK.gui.topLeft.addChild(crystalIcon); // Crystal count text, vertically centered with icon, same height var crystalTxt = new Text2('0', { size: 100, fill: 0x00eaff }); crystalTxt.anchor.set(0, 0.5); crystalTxt.x = crystalIcon.x + crystalIcon.width + 24; crystalTxt.y = crystalIcon.y; LK.gui.topLeft.addChild(crystalTxt); waveTxt = new Text2('Wave 1', { size: 80, fill: 0xAAFFFF }); waveTxt.anchor.set(0.5, 0); LK.gui.top.addChild(waveTxt); waveTxt.y = 120; powerupTxt = new Text2('', { size: 70, fill: 0xFFE100 }); powerupTxt.anchor.set(0.5, 0); LK.gui.top.addChild(powerupTxt); powerupTxt.y = 220; // Start music LK.playMusic('bgmusic'); // Spawn player player = new Player(); player.x = PLAYER_START_X; player.y = PLAYER_START_Y; game.addChild(player); // Set initial movement limits leftLimit = player.leftLimit; rightLimit = player.rightLimit; // Start first wave function startWave(waveNum) { wave = waveNum; waveTxt.setText('Wave ' + wave); // Set enemiesToSpawn to 30 for each wave, so wave increases every 30 aliens // After 5th wave, increase number of aliens per wave by 5 for each wave after 5 if (wave > 5) { enemiesToSpawn = 30 + (wave - 5) * 5; } else { enemiesToSpawn = 30; } enemiesLeft = enemiesToSpawn; lastAlienSpawnTick = LK.ticks; aliensKilledThisWave = 0; // Reset for new wave // Change star color every 2 waves // Use the new wave number (after increment) for correct color var starColor = getStarColorForWave(wave); for (var i = 0; i < stars.length; i++) { stars[i].setColor(starColor); } // Reset firing speed to base values every wave, then increase firing speed by 0.15x per wave currentLaserCooldown = Math.max(2, Math.round(LASER_COOLDOWN * Math.pow(0.85, wave - 1))); currentLaserCooldownRapid = Math.max(1, Math.round(LASER_COOLDOWN_RAPID * Math.pow(0.85, wave - 1))); // Increase boss health after each wave if (wave > 1) { bossBaseHp += bossHpInc; } // Enable double shot after 5th wave if (wave > 5) { doubleShotActive = true; doubleShotTimer = 0; // 0 means infinite duration powerupTxt.setText('Double Shot!'); if (player && player.children && player.children.length > 0) { player.children[0].tint = 0xff0000; } } else if (wave === 5) { // If returning to wave 5, disable double shot doubleShotActive = false; doubleShotTimer = 0; if (player && player.children && player.children.length > 0) { player.children[0].tint = 0xffffff; } powerupTxt.setText(''); } } // Set initial star color var initialStarColor = getStarColorForWave(1); for (var i = 0; i < stars.length; i++) { stars[i].setColor(initialStarColor); } startWave(1); // Utility: clamp function clamp(val, min, max) { if (val < min) return min; if (val > max) return max; return val; } // Utility: random int function randInt(min, max) { return min + Math.floor(Math.random() * (max - min + 1)); } // Utility: random powerup function randomPowerup() { var arr = ['rapid', 'pierce', 'shield']; return arr[randInt(0, arr.length - 1)]; } // Handle movement (dragging) function handleMove(x, y, obj) { if (dragNode === player) { // Clamp to current movement limits var newX = clamp(x, leftLimit, rightLimit); // Prevent player from entering top left 100x100 area (menu) if (newX - player.width / 2 < 100) { newX = 100 + player.width / 2; } player.x = newX; lastMoveX = newX; lastMoveY = player.y; } } game.move = handleMove; game.down = function (x, y, obj) { // Only allow drag if touch/click is on player or below if (y > GROUND_Y - 200) { dragNode = player; handleMove(x, y, obj); } }; game.up = function (x, y, obj) { dragNode = null; }; // Main game update game.update = function () { // Update background stars for (var i = 0; i < stars.length; i++) { stars[i].update(); } if (gameOver || youWin) return; // Player update (powerup timer) player.update(); // Aliens update for (var i = aliens.length - 1; i >= 0; i--) { var alien = aliens[i]; alien.update(); // Check if alien reached bottom edge (player dies) if (alien.y > GAME_HEIGHT + alien.height / 2) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); gameOver = true; return; } // Check collision with player (if shielded, destroy alien) if (player.powered && player.powerType === 'shield' && alien.intersects(player)) { LK.getSound('alien_die').play(); LK.effects.flashObject(alien, 0x00eaff, 400); alien.isDead = true; aliens.splice(i, 1); alien.destroy(); continue; } // If not shielded and alien collides with player, game over if ((!player.powered || player.powerType !== 'shield') && alien.intersects(player)) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); gameOver = true; return; } } // Boss update if (bossActive && boss) { boss.update(); // Check if boss reached bottom edge (player dies) if (boss.y > GAME_HEIGHT + boss.height / 2) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); gameOver = true; return; } // Check collision with player (if shielded, destroy boss) if (player.powered && player.powerType === 'shield' && boss.intersects(player)) { LK.getSound('alien_die').play(); LK.effects.flashObject(boss, 0x00eaff, 400); boss.isDead = true; bossActive = false; bossDefeated = true; boss.destroy(); boss = null; } } // Lasers update for (var i = lasers.length - 1; i >= 0; i--) { var laser = lasers[i]; laser.update(); // Remove if off screen if (laser.y < -100) { lasers.splice(i, 1); laser.destroy(); continue; } // Check collision with aliens var hit = false; for (var j = aliens.length - 1; j >= 0; j--) { var alien = aliens[j]; if (!alien.isDead && laser.intersects(alien)) { alien.hp -= player.powered && player.powerType === 'pierce' ? 2 : 1; if (alien.hp <= 0) { // Alien dies LK.getSound('alien_die').play(); LK.effects.flashObject(alien, 0xff0000, 400); alien.isDead = true; // Drop crystal var crystal = new Crystal(); crystal.x = alien.x; crystal.y = alien.y; crystals.push(crystal); game.addChild(crystal); // Remove alien aliens.splice(j, 1); alien.destroy(); enemiesLeft--; enemiesKilled++; aliensKilledThisWave++; // Every 30 aliens, increase wave by 1 (only if all 30 were killed in this wave) if (aliensKilledThisWave > 0 && aliensKilledThisWave % 30 === 0 && enemiesLeft === 0) { // After each wave, spawn boss bossActive = true; bossDefeated = false; boss = new Boss(); boss.x = GAME_WIDTH / 2; boss.y = -boss.height / 2; game.addChild(boss); // Only start next wave after boss is defeated // Bonus box every BONUS_WAVE_INTERVAL after wave increment (handled after boss defeat) return; // Prevent double wave start if also enemiesLeft==0 } // New wave? if (enemiesLeft <= 0) { // No wave increase here, handled by 30 aliens killed logic } } hit = true; if (!(player.powered && player.powerType === 'pierce')) { break; } } } // Boss hit by laser if (bossActive && boss && laser.intersects(boss)) { boss.hp -= player.powered && player.powerType === 'pierce' ? 2 : 1; if (boss.hp <= 0) { LK.getSound('alien_die').play(); LK.effects.flashObject(boss, 0xff0000, 600); // Drop multiple crystals for (var c = 0; c < 3; c++) { var crystal = new Crystal(); crystal.x = boss.x + randInt(-60, 60); crystal.y = boss.y + randInt(-60, 60); crystals.push(crystal); game.addChild(crystal); } boss.isDead = true; bossActive = false; bossDefeated = true; boss.destroy(); boss = null; // Start next wave startWave(wave + 1); // Bonus box every BONUS_WAVE_INTERVAL after wave increment if ((wave + 1) % BONUS_WAVE_INTERVAL === 0) { var bonus = new BonusBox(); bonus.x = randInt(leftLimit + 100, rightLimit - 100); bonus.y = randInt(ENEMY_START_Y_MIN + 100, ENEMY_START_Y_MAX - 100); bonus.type = randomPowerup(); bonuses.push(bonus); game.addChild(bonus); } } hit = true; } if (hit && !(player.powered && player.powerType === 'pierce')) { lasers.splice(i, 1); laser.destroy(); } } // Crystals update for (var i = crystals.length - 1; i >= 0; i--) { var crystal = crystals[i]; // If player close enough, collect var dx = player.x - crystal.x; var dy = player.y - crystal.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < CRYSTAL_COLLECT_DIST) { LK.getSound('crystal_get').play(); LK.effects.flashObject(crystal, 0x00eaff, 400); crystals.splice(i, 1); crystal.destroy(); // Expand movement area and always update score crystalsCollected++; crystalTxt.setText(crystalsCollected); if (crystalsCollected <= MAX_CRYSTALS) { leftLimit = Math.max(PLAYER_MIN_X - crystalsCollected * CRYSTAL_MOVE_STEP, 80 + player.width / 2); rightLimit = Math.min(PLAYER_MAX_X + crystalsCollected * CRYSTAL_MOVE_STEP, GAME_WIDTH - 80 - player.width / 2); } } } // Bonus boxes update for (var i = bonuses.length - 1; i >= 0; i--) { var bonus = bonuses[i]; var dx = player.x - bonus.x; var dy = player.y - bonus.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < BONUS_COLLECT_DIST) { LK.getSound('bonus_get').play(); LK.effects.flashObject(bonus, 0xffe100, 400); bonuses.splice(i, 1); bonus.destroy(); // After 5th wave, randomly grant either alien slowdown for 5 seconds or triple shot for 5 seconds if (wave > 5) { if (Math.random() < 0.5) { // Alien slowdown: halve alien and boss speed for 5 seconds (300 ticks) powerupTxt.setText('Alien Slowdown!'); for (var j = 0; j < aliens.length; j++) { if (typeof aliens[j].originalSpeed === "undefined") aliens[j].originalSpeed = aliens[j].speed; aliens[j].speed = aliens[j].originalSpeed * 0.5; } if (bossActive && boss) { if (typeof boss.originalSpeed === "undefined") boss.originalSpeed = boss.speed; boss.speed = boss.originalSpeed * 0.5; } // Set timer to restore speed if (typeof alienSlowdownTimer === "undefined") alienSlowdownTimer = 0; alienSlowdownTimer = 300; player.setPower(null, 0); // No player power, just visual } else { // Triple shot: grant for 5 seconds (300 ticks) powerupTxt.setText('Triple Shot!'); tripleShotActive = true; tripleShotTimer = 300; player.setPower(null, 0); // No player power, just visual } } else { // Before or at 5th wave, randomly grant either alien slowdown for 5 seconds or double shot for 5 seconds if (Math.random() < 0.5) { // Alien slowdown: halve alien and boss speed for 5 seconds (300 ticks) powerupTxt.setText('Alien Slowdown!'); for (var j = 0; j < aliens.length; j++) { if (typeof aliens[j].originalSpeed === "undefined") aliens[j].originalSpeed = aliens[j].speed; aliens[j].speed = aliens[j].originalSpeed * 0.5; } if (bossActive && boss) { if (typeof boss.originalSpeed === "undefined") boss.originalSpeed = boss.speed; boss.speed = boss.originalSpeed * 0.5; } // Set timer to restore speed if (typeof alienSlowdownTimer === "undefined") alienSlowdownTimer = 0; alienSlowdownTimer = 300; player.setPower(null, 0); // No player power, just visual } else { // Double shot: always grant for 5 seconds (300 ticks) when bonus is collected powerupTxt.setText('Double Shot!'); doubleShotActive = true; doubleShotTimer = 300; player.setPower(null, 0); // No player power, just visual } } } } // Powerup text timer if (player.powered) { powerupTxt.alpha = 1; } else { if (powerupTxt.alpha > 0) { powerupTxt.alpha -= 0.04; if (powerupTxt.alpha < 0) powerupTxt.alpha = 0; } powerupTxt.setText(''); } // Alien slowdown timer if (typeof alienSlowdownTimer !== "undefined" && alienSlowdownTimer > 0) { alienSlowdownTimer--; if (alienSlowdownTimer <= 0) { // Restore all aliens' and boss's speed, and clear originalSpeed to prevent stacking for (var j = 0; j < aliens.length; j++) { if (typeof aliens[j].originalSpeed !== "undefined") { aliens[j].speed = aliens[j].originalSpeed; delete aliens[j].originalSpeed; } } if (bossActive && boss && typeof boss.originalSpeed !== "undefined") { boss.speed = boss.originalSpeed; delete boss.originalSpeed; } powerupTxt.setText(''); } } // Double shot timer if (doubleShotActive) { // Set player and all lasers to red tint if (player && player.children && player.children.length > 0) { // Player sprite is always first child if (player.children[0].tint !== 0xff0000) player.children[0].tint = 0xff0000; } for (var i = 0; i < lasers.length; i++) { if (lasers[i].children && lasers[i].children.length > 0) { if (lasers[i].children[0].tint !== 0xff0000) lasers[i].children[0].tint = 0xff0000; } } // Only decrement timer if timer is not 0 (0 means infinite double shot) if (doubleShotTimer !== 0) { doubleShotTimer--; if (doubleShotTimer <= 0) { doubleShotActive = false; // Restore player tint (respect powerup color if active) if (player && player.children && player.children.length > 0) { if (player.powered) { if (player.powerType === 'rapid') { player.children[0].tint = 0xffe100; } else if (player.powerType === 'pierce') { player.children[0].tint = 0x00ffb0; } else if (player.powerType === 'shield') { player.children[0].tint = 0x00eaff; } } else { player.children[0].tint = 0xffffff; } } // Restore all lasers to normal tint for (var i = 0; i < lasers.length; i++) { if (lasers[i].children && lasers[i].children.length > 0) { lasers[i].children[0].tint = 0xffffff; } } } } } // Triple shot timer if (tripleShotActive) { // Set player and all lasers to magenta tint if (player && player.children && player.children.length > 0) { if (player.children[0].tint !== 0xff00cc) player.children[0].tint = 0xff00cc; } for (var i = 0; i < lasers.length; i++) { if (lasers[i].children && lasers[i].children.length > 0) { if (lasers[i].children[0].tint !== 0xff00cc) lasers[i].children[0].tint = 0xff00cc; } } if (tripleShotTimer !== 0) { tripleShotTimer--; if (tripleShotTimer <= 0) { tripleShotActive = false; // Restore player tint (respect powerup color if active) if (player && player.children && player.children.length > 0) { if (player.powered) { if (player.powerType === 'rapid') { player.children[0].tint = 0xffe100; } else if (player.powerType === 'pierce') { player.children[0].tint = 0x00ffb0; } else if (player.powerType === 'shield') { player.children[0].tint = 0x00eaff; } } else { player.children[0].tint = 0xffffff; } } // Restore all lasers to normal tint for (var i = 0; i < lasers.length; i++) { if (lasers[i].children && lasers[i].children.length > 0) { lasers[i].children[0].tint = 0xffffff; } } } } } // Player auto-fire var cooldown = player.powered && player.powerType === 'rapid' ? currentLaserCooldownRapid : currentLaserCooldown; if (player.shootCooldown > 0) player.shootCooldown--; // Prevent shooting if game is over or won if (gameOver || youWin) return; // Always fire if cooldown is ready, regardless of nearest alien if (player.shootCooldown <= 0) { // Fire laser(s) if (tripleShotActive) { // Triple shot: fire three lasers, center and two offset var laser1 = new Laser(); laser1.x = player.x - 48; laser1.y = player.y - player.height / 2 + 10; if (player.powered && player.powerType === 'pierce') { laser1.pierce = true; } // Set laser magenta if triple shot if (laser1.children && laser1.children.length > 0) { laser1.children[0].tint = 0xff00cc; } lasers.push(laser1); game.addChild(laser1); var laser2 = new Laser(); laser2.x = player.x; laser2.y = player.y - player.height / 2 + 10; if (player.powered && player.powerType === 'pierce') { laser2.pierce = true; } if (laser2.children && laser2.children.length > 0) { laser2.children[0].tint = 0xff00cc; } lasers.push(laser2); game.addChild(laser2); var laser3 = new Laser(); laser3.x = player.x + 48; laser3.y = player.y - player.height / 2 + 10; if (player.powered && player.powerType === 'pierce') { laser3.pierce = true; } if (laser3.children && laser3.children.length > 0) { laser3.children[0].tint = 0xff00cc; } lasers.push(laser3); game.addChild(laser3); player.shootCooldown = cooldown; LK.getSound('laser_shoot').play(); } else if (doubleShotActive) { // Double shot: fire two lasers, slightly offset var laser1 = new Laser(); laser1.x = player.x - 32; laser1.y = player.y - player.height / 2 + 10; if (player.powered && player.powerType === 'pierce') { laser1.pierce = true; } // Set laser red if double shot if (laser1.children && laser1.children.length > 0) { laser1.children[0].tint = 0xff0000; } lasers.push(laser1); game.addChild(laser1); var laser2 = new Laser(); laser2.x = player.x + 32; laser2.y = player.y - player.height / 2 + 10; if (player.powered && player.powerType === 'pierce') { laser2.pierce = true; } // Set laser red if double shot if (laser2.children && laser2.children.length > 0) { laser2.children[0].tint = 0xff0000; } lasers.push(laser2); game.addChild(laser2); player.shootCooldown = cooldown; LK.getSound('laser_shoot').play(); } else { // Normal single shot var laser = new Laser(); laser.x = player.x; laser.y = player.y - player.height / 2 + 10; if (player.powered && player.powerType === 'pierce') { laser.pierce = true; } // Set laser red if double shot is active if (doubleShotActive && laser.children && laser.children.length > 0) { laser.children[0].tint = 0xff0000; } lasers.push(laser); game.addChild(laser); player.shootCooldown = cooldown; LK.getSound('laser_shoot').play(); } } // Alien image pool for 4 types var ALIEN_IMAGES = ['alien', 'alien2', 'alien3', 'alien4']; // Helper: get alien image(s) for this wave function getAlienImagesForWave(wave) { // Every 3 waves, cycle to next image var idx = Math.floor((wave - 1) / 3) % ALIEN_IMAGES.length; // 30% chance to mix all images in this wave if (Math.random() < 0.3) { // Return all images for mixing return ALIEN_IMAGES.slice(); } // Otherwise, use only the current image for this 3-wave block return [ALIEN_IMAGES[idx]]; } // Track which images to use for this wave if (typeof currentWaveAlienImages === "undefined") { var currentWaveAlienImages = getAlienImagesForWave(wave); } // If wave changed, update currentWaveAlienImages if (typeof lastAlienWave === "undefined" || lastAlienWave !== wave) { currentWaveAlienImages = getAlienImagesForWave(wave); lastAlienWave = wave; } // Spawn aliens for current wave if (!bossActive && enemiesToSpawn > 0 && LK.ticks - lastAlienSpawnTick > 24) { // Pick a random image from currentWaveAlienImages var imgIdx = randInt(0, currentWaveAlienImages.length - 1); var alienImage = currentWaveAlienImages[imgIdx]; // Create a new Alien and set its image var alien = new Alien(); // Remove the default sprite and attach the correct one if (alien.children && alien.children.length > 0) { alien.removeChild(alien.children[0]); } var alienSprite = alien.attachAsset(alienImage, { anchorX: 0.5, anchorY: 0.5 }); alien.width = alienSprite.width; alien.height = alienSprite.height; // Pick a random road for the alien to spawn on (X position) var roadIdx = randInt(0, ROAD_X_POSITIONS.length - 1); alien.x = ROAD_X_POSITIONS[roadIdx]; alien.y = -alien.height / 2; alien.wave = wave; // After 5th wave, increase speed more rapidly if (wave > 5) { alien.speed = 2.5 + (wave - 1) * WAVE_SPEED_INC + (wave - 5) * 0.5; } else { alien.speed = 2.5 + (wave - 1) * WAVE_SPEED_INC; } alien.hp = 1 + Math.floor((wave - 1) * WAVE_HP_INC / 2); aliens.push(alien); game.addChild(alien); enemiesToSpawn--; lastAlienSpawnTick = LK.ticks; } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Alien class
var Alien = Container.expand(function () {
var self = Container.call(this);
// Attach a default sprite, but allow it to be replaced after construction
var alienSprite = self.attachAsset('alien', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = alienSprite.width;
self.height = alienSprite.height;
self.speed = 2.5;
self.hp = 1;
self.wave = 1;
self.lastX = self.x;
self.lastY = self.y;
self.lastIntersecting = false;
self.isDead = false;
self.update = function () {
self.lastY = self.y;
self.y += self.speed;
};
return self;
});
// Bonus box class
var BonusBox = Container.expand(function () {
var self = Container.call(this);
var bonusSprite = self.attachAsset('bonus', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = bonusSprite.width;
self.height = bonusSprite.height;
self.lastY = self.y;
self.lastIntersecting = false;
self.type = null; // Set on spawn
self.fallSpeed = 10; // Speed at which the bonus moves toward the player
self.update = function () {
// Move toward the player's current position
if (typeof player !== "undefined" && player !== null) {
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 2) {
// Normalize and move toward player
self.x += dx / dist * self.fallSpeed;
self.y += dy / dist * self.fallSpeed;
}
}
};
return self;
});
// Boss class
var Boss = Container.expand(function () {
var self = Container.call(this);
var bossSprite = self.attachAsset('alien4', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
self.width = bossSprite.width * 2;
self.height = bossSprite.height * 2;
self.speed = 4;
// Use global bossBaseHp for initial boss health
self.hp = typeof bossBaseHp !== "undefined" ? bossBaseHp : 10;
self.lastY = self.y;
self.lastIntersecting = false;
self.isDead = false;
self.update = function () {
self.lastY = self.y;
self.y += self.speed;
};
return self;
});
// Crystal class
var Crystal = Container.expand(function () {
var self = Container.call(this);
var crystalSprite = self.attachAsset('crystal', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = crystalSprite.width;
self.height = crystalSprite.height;
self.lastY = self.y;
self.lastIntersecting = false;
self.fallSpeed = 12; // Speed at which the crystal falls toward the player
self.update = function () {
// Move toward the player's current position
if (typeof player !== "undefined" && player !== null) {
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 2) {
// Normalize and move toward player
self.x += dx / dist * self.fallSpeed;
self.y += dy / dist * self.fallSpeed;
}
}
};
return self;
});
// Laser class
var Laser = Container.expand(function () {
var self = Container.call(this);
var laserSprite = self.attachAsset('laser', {
anchorX: 0.5,
anchorY: 1
});
self.width = laserSprite.width;
self.height = laserSprite.height;
self.speed = 32;
self.pierce = false;
self.lastY = self.y;
self.lastIntersecting = false;
self.update = function () {
self.y -= self.speed;
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var playerSprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = playerSprite.width;
self.height = playerSprite.height;
self.shootCooldown = 0;
self.moveSpeed = 32; // px per tick when dragged
self.leftLimit = 0 + self.width / 2 + 40;
self.rightLimit = 2048 - self.width / 2 - 40;
self.powered = false;
self.powerTimer = 0;
self.powerType = null;
// For powerup visuals
self.setPower = function (type, duration) {
self.powered = true;
self.powerType = type;
self.powerTimer = duration;
if (type === 'rapid') {
playerSprite.tint = 0xffe100;
} else if (type === 'pierce') {
playerSprite.tint = 0x00ffb0;
} else if (type === 'shield') {
playerSprite.tint = 0x00eaff;
}
};
self.clearPower = function () {
self.powered = false;
self.powerType = null;
self.powerTimer = 0;
playerSprite.tint = 0xffffff;
};
self.update = function () {
if (self.powered) {
self.powerTimer--;
if (self.powerTimer <= 0) {
self.clearPower();
}
}
};
return self;
});
// Star class for background stars
var Star = Container.expand(function () {
var self = Container.call(this);
// Use a white ellipse as a star, randomize size (smaller, more variety)
// Reduced min/max size for smaller stars
var size = randInt(1, 2);
var starSprite = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: size / 120,
scaleY: size / 120,
tint: 0xffffff
});
self.starSprite = starSprite; // Store for tinting
self.width = size;
self.height = size;
// More parallax: slower stars are smaller, faster are bigger
// Speed: 0.2 to 2.2, with more slow stars
var speedBase = Math.pow(Math.random(), 2.2); // bias toward 0
self.speed = 0.2 + speedBase * 2.0;
self.alpha = 0.4 + Math.random() * 0.6;
self.setColor = function (color) {
if (self.starSprite) self.starSprite.tint = color;
};
self.update = function () {
// Only update if star is visible or about to be visible
if (self.y > -self.height && self.y < GAME_HEIGHT + self.height * 2) {
self.y += self.speed;
} else {
self.y = -self.height;
self.x = randInt(0, GAME_WIDTH);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Music
// Sound effects
// Bonus box
// Crystal (collectible)
// Player bullet (laser)
// Alien (enemy)
// Player
// Game constants
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
// --- Starfield background ---
// Star color palette (cycle every 2 waves)
var STAR_COLORS = [0xffffff,
// white
0xffe100,
// yellow
0x00eaff,
// cyan
0xff00cc,
// magenta
0x00ffb0,
// green
0xffa500,
// orange
0xAAFFFF,
// light blue
0xff0000 // red
];
function getStarColorForWave(wave) {
var idx = Math.floor((wave - 1) / 2) % STAR_COLORS.length;
return STAR_COLORS[idx];
}
var NUM_STARS = 320;
var stars = [];
for (var i = 0; i < NUM_STARS; i++) {
var star = new Star();
star.x = randInt(0, GAME_WIDTH);
star.y = randInt(0, GAME_HEIGHT);
stars.push(star);
game.addChild(star);
}
var GROUND_Y = GAME_HEIGHT - 220;
var PLAYER_START_X = GAME_WIDTH / 2;
var PLAYER_START_Y = GROUND_Y;
var PLAYER_MIN_X = 160 + 40;
var PLAYER_MAX_X = GAME_WIDTH - 160 - 40;
var ENEMY_START_X_MIN = 160;
var ENEMY_START_X_MAX = GAME_WIDTH - 160;
// Define Y spawn range for enemies (top margin to bottom margin for bonus box spawn)
var ENEMY_START_Y_MIN = 0;
var ENEMY_START_Y_MAX = GAME_HEIGHT - 400;
// Define 3 fixed road X positions for aliens to spawn and move along (top to bottom)
var ROAD_X_POSITIONS = [ENEMY_START_X_MIN + Math.floor((ENEMY_START_X_MAX - ENEMY_START_X_MIN) * 0.2), ENEMY_START_X_MIN + Math.floor((ENEMY_START_X_MAX - ENEMY_START_X_MIN) * 0.5), ENEMY_START_X_MIN + Math.floor((ENEMY_START_X_MAX - ENEMY_START_X_MIN) * 0.8)];
var LASER_COOLDOWN = 18; // ticks
var LASER_COOLDOWN_RAPID = 6;
// Track current firing cooldown, increases as waves progress
var currentLaserCooldown = LASER_COOLDOWN;
var currentLaserCooldownRapid = LASER_COOLDOWN_RAPID;
var CRYSTAL_COLLECT_DIST = 120;
var BONUS_COLLECT_DIST = 120;
var CRYSTAL_MOVE_STEP = 180; // How much movement area expands per crystal
var MAX_CRYSTALS = 8;
var WAVE_ENEMY_BASE = 10;
var WAVE_ENEMY_INC = 5; // Increase by 5 each wave
var WAVE_SPEED_INC = 0.10;
var WAVE_HP_INC = 1;
var BONUS_WAVE_INTERVAL = 3;
var POWERUP_DURATION = 360; // 6 seconds at 60fps
// Game state
var player = null;
var aliens = [];
var lasers = [];
var crystals = [];
var bonuses = [];
var boss = null;
var bossActive = false;
var bossDefeated = false;
// Boss health scaling
var bossBaseHp = 10;
var bossHpInc = 5;
var dragNode = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var lastMoveX = 0;
var lastMoveY = 0;
var lastAlienSpawnTick = 0;
var wave = 1;
var enemiesLeft = 0;
var enemiesToSpawn = 0;
var enemiesKilled = 0;
var aliensKilledThisWave = 0; // Track aliens killed in current wave
var crystalsCollected = 0;
var leftLimit = PLAYER_MIN_X;
var rightLimit = PLAYER_MAX_X;
var bonusActive = false;
var bonusType = null;
// Double shot state for player bonus
var doubleShotActive = false;
var doubleShotTimer = 0;
// Triple shot state for player bonus (after 5th wave)
var tripleShotActive = false;
var tripleShotTimer = 0;
// Removed score and scoreTxt, replaced by crystalTxt
var waveTxt = null;
var powerupTxt = null;
var gameOver = false;
var youWin = false;
// GUI
// Crystal icon in upper left (avoid top left 100x100 for menu)
// Crystal icon and count, aligned and sized together
var crystalIcon = LK.getAsset('crystal', {
anchorX: 0,
anchorY: 0.5
});
crystalIcon.x = 20;
crystalIcon.y = 160 + 40; // center of 80px icon at 160+40=200
crystalIcon.width = 100;
crystalIcon.height = 100;
LK.gui.topLeft.addChild(crystalIcon);
// Crystal count text, vertically centered with icon, same height
var crystalTxt = new Text2('0', {
size: 100,
fill: 0x00eaff
});
crystalTxt.anchor.set(0, 0.5);
crystalTxt.x = crystalIcon.x + crystalIcon.width + 24;
crystalTxt.y = crystalIcon.y;
LK.gui.topLeft.addChild(crystalTxt);
waveTxt = new Text2('Wave 1', {
size: 80,
fill: 0xAAFFFF
});
waveTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(waveTxt);
waveTxt.y = 120;
powerupTxt = new Text2('', {
size: 70,
fill: 0xFFE100
});
powerupTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(powerupTxt);
powerupTxt.y = 220;
// Start music
LK.playMusic('bgmusic');
// Spawn player
player = new Player();
player.x = PLAYER_START_X;
player.y = PLAYER_START_Y;
game.addChild(player);
// Set initial movement limits
leftLimit = player.leftLimit;
rightLimit = player.rightLimit;
// Start first wave
function startWave(waveNum) {
wave = waveNum;
waveTxt.setText('Wave ' + wave);
// Set enemiesToSpawn to 30 for each wave, so wave increases every 30 aliens
// After 5th wave, increase number of aliens per wave by 5 for each wave after 5
if (wave > 5) {
enemiesToSpawn = 30 + (wave - 5) * 5;
} else {
enemiesToSpawn = 30;
}
enemiesLeft = enemiesToSpawn;
lastAlienSpawnTick = LK.ticks;
aliensKilledThisWave = 0; // Reset for new wave
// Change star color every 2 waves
// Use the new wave number (after increment) for correct color
var starColor = getStarColorForWave(wave);
for (var i = 0; i < stars.length; i++) {
stars[i].setColor(starColor);
}
// Reset firing speed to base values every wave, then increase firing speed by 0.15x per wave
currentLaserCooldown = Math.max(2, Math.round(LASER_COOLDOWN * Math.pow(0.85, wave - 1)));
currentLaserCooldownRapid = Math.max(1, Math.round(LASER_COOLDOWN_RAPID * Math.pow(0.85, wave - 1)));
// Increase boss health after each wave
if (wave > 1) {
bossBaseHp += bossHpInc;
}
// Enable double shot after 5th wave
if (wave > 5) {
doubleShotActive = true;
doubleShotTimer = 0; // 0 means infinite duration
powerupTxt.setText('Double Shot!');
if (player && player.children && player.children.length > 0) {
player.children[0].tint = 0xff0000;
}
} else if (wave === 5) {
// If returning to wave 5, disable double shot
doubleShotActive = false;
doubleShotTimer = 0;
if (player && player.children && player.children.length > 0) {
player.children[0].tint = 0xffffff;
}
powerupTxt.setText('');
}
}
// Set initial star color
var initialStarColor = getStarColorForWave(1);
for (var i = 0; i < stars.length; i++) {
stars[i].setColor(initialStarColor);
}
startWave(1);
// Utility: clamp
function clamp(val, min, max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
// Utility: random int
function randInt(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
}
// Utility: random powerup
function randomPowerup() {
var arr = ['rapid', 'pierce', 'shield'];
return arr[randInt(0, arr.length - 1)];
}
// Handle movement (dragging)
function handleMove(x, y, obj) {
if (dragNode === player) {
// Clamp to current movement limits
var newX = clamp(x, leftLimit, rightLimit);
// Prevent player from entering top left 100x100 area (menu)
if (newX - player.width / 2 < 100) {
newX = 100 + player.width / 2;
}
player.x = newX;
lastMoveX = newX;
lastMoveY = player.y;
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only allow drag if touch/click is on player or below
if (y > GROUND_Y - 200) {
dragNode = player;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Main game update
game.update = function () {
// Update background stars
for (var i = 0; i < stars.length; i++) {
stars[i].update();
}
if (gameOver || youWin) return;
// Player update (powerup timer)
player.update();
// Aliens update
for (var i = aliens.length - 1; i >= 0; i--) {
var alien = aliens[i];
alien.update();
// Check if alien reached bottom edge (player dies)
if (alien.y > GAME_HEIGHT + alien.height / 2) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
gameOver = true;
return;
}
// Check collision with player (if shielded, destroy alien)
if (player.powered && player.powerType === 'shield' && alien.intersects(player)) {
LK.getSound('alien_die').play();
LK.effects.flashObject(alien, 0x00eaff, 400);
alien.isDead = true;
aliens.splice(i, 1);
alien.destroy();
continue;
}
// If not shielded and alien collides with player, game over
if ((!player.powered || player.powerType !== 'shield') && alien.intersects(player)) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
gameOver = true;
return;
}
}
// Boss update
if (bossActive && boss) {
boss.update();
// Check if boss reached bottom edge (player dies)
if (boss.y > GAME_HEIGHT + boss.height / 2) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
gameOver = true;
return;
}
// Check collision with player (if shielded, destroy boss)
if (player.powered && player.powerType === 'shield' && boss.intersects(player)) {
LK.getSound('alien_die').play();
LK.effects.flashObject(boss, 0x00eaff, 400);
boss.isDead = true;
bossActive = false;
bossDefeated = true;
boss.destroy();
boss = null;
}
}
// Lasers update
for (var i = lasers.length - 1; i >= 0; i--) {
var laser = lasers[i];
laser.update();
// Remove if off screen
if (laser.y < -100) {
lasers.splice(i, 1);
laser.destroy();
continue;
}
// Check collision with aliens
var hit = false;
for (var j = aliens.length - 1; j >= 0; j--) {
var alien = aliens[j];
if (!alien.isDead && laser.intersects(alien)) {
alien.hp -= player.powered && player.powerType === 'pierce' ? 2 : 1;
if (alien.hp <= 0) {
// Alien dies
LK.getSound('alien_die').play();
LK.effects.flashObject(alien, 0xff0000, 400);
alien.isDead = true;
// Drop crystal
var crystal = new Crystal();
crystal.x = alien.x;
crystal.y = alien.y;
crystals.push(crystal);
game.addChild(crystal);
// Remove alien
aliens.splice(j, 1);
alien.destroy();
enemiesLeft--;
enemiesKilled++;
aliensKilledThisWave++;
// Every 30 aliens, increase wave by 1 (only if all 30 were killed in this wave)
if (aliensKilledThisWave > 0 && aliensKilledThisWave % 30 === 0 && enemiesLeft === 0) {
// After each wave, spawn boss
bossActive = true;
bossDefeated = false;
boss = new Boss();
boss.x = GAME_WIDTH / 2;
boss.y = -boss.height / 2;
game.addChild(boss);
// Only start next wave after boss is defeated
// Bonus box every BONUS_WAVE_INTERVAL after wave increment (handled after boss defeat)
return; // Prevent double wave start if also enemiesLeft==0
}
// New wave?
if (enemiesLeft <= 0) {
// No wave increase here, handled by 30 aliens killed logic
}
}
hit = true;
if (!(player.powered && player.powerType === 'pierce')) {
break;
}
}
}
// Boss hit by laser
if (bossActive && boss && laser.intersects(boss)) {
boss.hp -= player.powered && player.powerType === 'pierce' ? 2 : 1;
if (boss.hp <= 0) {
LK.getSound('alien_die').play();
LK.effects.flashObject(boss, 0xff0000, 600);
// Drop multiple crystals
for (var c = 0; c < 3; c++) {
var crystal = new Crystal();
crystal.x = boss.x + randInt(-60, 60);
crystal.y = boss.y + randInt(-60, 60);
crystals.push(crystal);
game.addChild(crystal);
}
boss.isDead = true;
bossActive = false;
bossDefeated = true;
boss.destroy();
boss = null;
// Start next wave
startWave(wave + 1);
// Bonus box every BONUS_WAVE_INTERVAL after wave increment
if ((wave + 1) % BONUS_WAVE_INTERVAL === 0) {
var bonus = new BonusBox();
bonus.x = randInt(leftLimit + 100, rightLimit - 100);
bonus.y = randInt(ENEMY_START_Y_MIN + 100, ENEMY_START_Y_MAX - 100);
bonus.type = randomPowerup();
bonuses.push(bonus);
game.addChild(bonus);
}
}
hit = true;
}
if (hit && !(player.powered && player.powerType === 'pierce')) {
lasers.splice(i, 1);
laser.destroy();
}
}
// Crystals update
for (var i = crystals.length - 1; i >= 0; i--) {
var crystal = crystals[i];
// If player close enough, collect
var dx = player.x - crystal.x;
var dy = player.y - crystal.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < CRYSTAL_COLLECT_DIST) {
LK.getSound('crystal_get').play();
LK.effects.flashObject(crystal, 0x00eaff, 400);
crystals.splice(i, 1);
crystal.destroy();
// Expand movement area and always update score
crystalsCollected++;
crystalTxt.setText(crystalsCollected);
if (crystalsCollected <= MAX_CRYSTALS) {
leftLimit = Math.max(PLAYER_MIN_X - crystalsCollected * CRYSTAL_MOVE_STEP, 80 + player.width / 2);
rightLimit = Math.min(PLAYER_MAX_X + crystalsCollected * CRYSTAL_MOVE_STEP, GAME_WIDTH - 80 - player.width / 2);
}
}
}
// Bonus boxes update
for (var i = bonuses.length - 1; i >= 0; i--) {
var bonus = bonuses[i];
var dx = player.x - bonus.x;
var dy = player.y - bonus.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < BONUS_COLLECT_DIST) {
LK.getSound('bonus_get').play();
LK.effects.flashObject(bonus, 0xffe100, 400);
bonuses.splice(i, 1);
bonus.destroy();
// After 5th wave, randomly grant either alien slowdown for 5 seconds or triple shot for 5 seconds
if (wave > 5) {
if (Math.random() < 0.5) {
// Alien slowdown: halve alien and boss speed for 5 seconds (300 ticks)
powerupTxt.setText('Alien Slowdown!');
for (var j = 0; j < aliens.length; j++) {
if (typeof aliens[j].originalSpeed === "undefined") aliens[j].originalSpeed = aliens[j].speed;
aliens[j].speed = aliens[j].originalSpeed * 0.5;
}
if (bossActive && boss) {
if (typeof boss.originalSpeed === "undefined") boss.originalSpeed = boss.speed;
boss.speed = boss.originalSpeed * 0.5;
}
// Set timer to restore speed
if (typeof alienSlowdownTimer === "undefined") alienSlowdownTimer = 0;
alienSlowdownTimer = 300;
player.setPower(null, 0); // No player power, just visual
} else {
// Triple shot: grant for 5 seconds (300 ticks)
powerupTxt.setText('Triple Shot!');
tripleShotActive = true;
tripleShotTimer = 300;
player.setPower(null, 0); // No player power, just visual
}
} else {
// Before or at 5th wave, randomly grant either alien slowdown for 5 seconds or double shot for 5 seconds
if (Math.random() < 0.5) {
// Alien slowdown: halve alien and boss speed for 5 seconds (300 ticks)
powerupTxt.setText('Alien Slowdown!');
for (var j = 0; j < aliens.length; j++) {
if (typeof aliens[j].originalSpeed === "undefined") aliens[j].originalSpeed = aliens[j].speed;
aliens[j].speed = aliens[j].originalSpeed * 0.5;
}
if (bossActive && boss) {
if (typeof boss.originalSpeed === "undefined") boss.originalSpeed = boss.speed;
boss.speed = boss.originalSpeed * 0.5;
}
// Set timer to restore speed
if (typeof alienSlowdownTimer === "undefined") alienSlowdownTimer = 0;
alienSlowdownTimer = 300;
player.setPower(null, 0); // No player power, just visual
} else {
// Double shot: always grant for 5 seconds (300 ticks) when bonus is collected
powerupTxt.setText('Double Shot!');
doubleShotActive = true;
doubleShotTimer = 300;
player.setPower(null, 0); // No player power, just visual
}
}
}
}
// Powerup text timer
if (player.powered) {
powerupTxt.alpha = 1;
} else {
if (powerupTxt.alpha > 0) {
powerupTxt.alpha -= 0.04;
if (powerupTxt.alpha < 0) powerupTxt.alpha = 0;
}
powerupTxt.setText('');
}
// Alien slowdown timer
if (typeof alienSlowdownTimer !== "undefined" && alienSlowdownTimer > 0) {
alienSlowdownTimer--;
if (alienSlowdownTimer <= 0) {
// Restore all aliens' and boss's speed, and clear originalSpeed to prevent stacking
for (var j = 0; j < aliens.length; j++) {
if (typeof aliens[j].originalSpeed !== "undefined") {
aliens[j].speed = aliens[j].originalSpeed;
delete aliens[j].originalSpeed;
}
}
if (bossActive && boss && typeof boss.originalSpeed !== "undefined") {
boss.speed = boss.originalSpeed;
delete boss.originalSpeed;
}
powerupTxt.setText('');
}
}
// Double shot timer
if (doubleShotActive) {
// Set player and all lasers to red tint
if (player && player.children && player.children.length > 0) {
// Player sprite is always first child
if (player.children[0].tint !== 0xff0000) player.children[0].tint = 0xff0000;
}
for (var i = 0; i < lasers.length; i++) {
if (lasers[i].children && lasers[i].children.length > 0) {
if (lasers[i].children[0].tint !== 0xff0000) lasers[i].children[0].tint = 0xff0000;
}
}
// Only decrement timer if timer is not 0 (0 means infinite double shot)
if (doubleShotTimer !== 0) {
doubleShotTimer--;
if (doubleShotTimer <= 0) {
doubleShotActive = false;
// Restore player tint (respect powerup color if active)
if (player && player.children && player.children.length > 0) {
if (player.powered) {
if (player.powerType === 'rapid') {
player.children[0].tint = 0xffe100;
} else if (player.powerType === 'pierce') {
player.children[0].tint = 0x00ffb0;
} else if (player.powerType === 'shield') {
player.children[0].tint = 0x00eaff;
}
} else {
player.children[0].tint = 0xffffff;
}
}
// Restore all lasers to normal tint
for (var i = 0; i < lasers.length; i++) {
if (lasers[i].children && lasers[i].children.length > 0) {
lasers[i].children[0].tint = 0xffffff;
}
}
}
}
}
// Triple shot timer
if (tripleShotActive) {
// Set player and all lasers to magenta tint
if (player && player.children && player.children.length > 0) {
if (player.children[0].tint !== 0xff00cc) player.children[0].tint = 0xff00cc;
}
for (var i = 0; i < lasers.length; i++) {
if (lasers[i].children && lasers[i].children.length > 0) {
if (lasers[i].children[0].tint !== 0xff00cc) lasers[i].children[0].tint = 0xff00cc;
}
}
if (tripleShotTimer !== 0) {
tripleShotTimer--;
if (tripleShotTimer <= 0) {
tripleShotActive = false;
// Restore player tint (respect powerup color if active)
if (player && player.children && player.children.length > 0) {
if (player.powered) {
if (player.powerType === 'rapid') {
player.children[0].tint = 0xffe100;
} else if (player.powerType === 'pierce') {
player.children[0].tint = 0x00ffb0;
} else if (player.powerType === 'shield') {
player.children[0].tint = 0x00eaff;
}
} else {
player.children[0].tint = 0xffffff;
}
}
// Restore all lasers to normal tint
for (var i = 0; i < lasers.length; i++) {
if (lasers[i].children && lasers[i].children.length > 0) {
lasers[i].children[0].tint = 0xffffff;
}
}
}
}
}
// Player auto-fire
var cooldown = player.powered && player.powerType === 'rapid' ? currentLaserCooldownRapid : currentLaserCooldown;
if (player.shootCooldown > 0) player.shootCooldown--;
// Prevent shooting if game is over or won
if (gameOver || youWin) return;
// Always fire if cooldown is ready, regardless of nearest alien
if (player.shootCooldown <= 0) {
// Fire laser(s)
if (tripleShotActive) {
// Triple shot: fire three lasers, center and two offset
var laser1 = new Laser();
laser1.x = player.x - 48;
laser1.y = player.y - player.height / 2 + 10;
if (player.powered && player.powerType === 'pierce') {
laser1.pierce = true;
}
// Set laser magenta if triple shot
if (laser1.children && laser1.children.length > 0) {
laser1.children[0].tint = 0xff00cc;
}
lasers.push(laser1);
game.addChild(laser1);
var laser2 = new Laser();
laser2.x = player.x;
laser2.y = player.y - player.height / 2 + 10;
if (player.powered && player.powerType === 'pierce') {
laser2.pierce = true;
}
if (laser2.children && laser2.children.length > 0) {
laser2.children[0].tint = 0xff00cc;
}
lasers.push(laser2);
game.addChild(laser2);
var laser3 = new Laser();
laser3.x = player.x + 48;
laser3.y = player.y - player.height / 2 + 10;
if (player.powered && player.powerType === 'pierce') {
laser3.pierce = true;
}
if (laser3.children && laser3.children.length > 0) {
laser3.children[0].tint = 0xff00cc;
}
lasers.push(laser3);
game.addChild(laser3);
player.shootCooldown = cooldown;
LK.getSound('laser_shoot').play();
} else if (doubleShotActive) {
// Double shot: fire two lasers, slightly offset
var laser1 = new Laser();
laser1.x = player.x - 32;
laser1.y = player.y - player.height / 2 + 10;
if (player.powered && player.powerType === 'pierce') {
laser1.pierce = true;
}
// Set laser red if double shot
if (laser1.children && laser1.children.length > 0) {
laser1.children[0].tint = 0xff0000;
}
lasers.push(laser1);
game.addChild(laser1);
var laser2 = new Laser();
laser2.x = player.x + 32;
laser2.y = player.y - player.height / 2 + 10;
if (player.powered && player.powerType === 'pierce') {
laser2.pierce = true;
}
// Set laser red if double shot
if (laser2.children && laser2.children.length > 0) {
laser2.children[0].tint = 0xff0000;
}
lasers.push(laser2);
game.addChild(laser2);
player.shootCooldown = cooldown;
LK.getSound('laser_shoot').play();
} else {
// Normal single shot
var laser = new Laser();
laser.x = player.x;
laser.y = player.y - player.height / 2 + 10;
if (player.powered && player.powerType === 'pierce') {
laser.pierce = true;
}
// Set laser red if double shot is active
if (doubleShotActive && laser.children && laser.children.length > 0) {
laser.children[0].tint = 0xff0000;
}
lasers.push(laser);
game.addChild(laser);
player.shootCooldown = cooldown;
LK.getSound('laser_shoot').play();
}
}
// Alien image pool for 4 types
var ALIEN_IMAGES = ['alien', 'alien2', 'alien3', 'alien4'];
// Helper: get alien image(s) for this wave
function getAlienImagesForWave(wave) {
// Every 3 waves, cycle to next image
var idx = Math.floor((wave - 1) / 3) % ALIEN_IMAGES.length;
// 30% chance to mix all images in this wave
if (Math.random() < 0.3) {
// Return all images for mixing
return ALIEN_IMAGES.slice();
}
// Otherwise, use only the current image for this 3-wave block
return [ALIEN_IMAGES[idx]];
}
// Track which images to use for this wave
if (typeof currentWaveAlienImages === "undefined") {
var currentWaveAlienImages = getAlienImagesForWave(wave);
}
// If wave changed, update currentWaveAlienImages
if (typeof lastAlienWave === "undefined" || lastAlienWave !== wave) {
currentWaveAlienImages = getAlienImagesForWave(wave);
lastAlienWave = wave;
}
// Spawn aliens for current wave
if (!bossActive && enemiesToSpawn > 0 && LK.ticks - lastAlienSpawnTick > 24) {
// Pick a random image from currentWaveAlienImages
var imgIdx = randInt(0, currentWaveAlienImages.length - 1);
var alienImage = currentWaveAlienImages[imgIdx];
// Create a new Alien and set its image
var alien = new Alien();
// Remove the default sprite and attach the correct one
if (alien.children && alien.children.length > 0) {
alien.removeChild(alien.children[0]);
}
var alienSprite = alien.attachAsset(alienImage, {
anchorX: 0.5,
anchorY: 0.5
});
alien.width = alienSprite.width;
alien.height = alienSprite.height;
// Pick a random road for the alien to spawn on (X position)
var roadIdx = randInt(0, ROAD_X_POSITIONS.length - 1);
alien.x = ROAD_X_POSITIONS[roadIdx];
alien.y = -alien.height / 2;
alien.wave = wave;
// After 5th wave, increase speed more rapidly
if (wave > 5) {
alien.speed = 2.5 + (wave - 1) * WAVE_SPEED_INC + (wave - 5) * 0.5;
} else {
alien.speed = 2.5 + (wave - 1) * WAVE_SPEED_INC;
}
alien.hp = 1 + Math.floor((wave - 1) * WAVE_HP_INC / 2);
aliens.push(alien);
game.addChild(alien);
enemiesToSpawn--;
lastAlienSpawnTick = LK.ticks;
}
};
A triangular spaceship. In-Game asset. 2d. High contrast. No shadows
alien creature drawing. In-Game asset. 2d. High contrast. No shadows
alien creature drawing. In-Game asset. 2d. High contrast. No shadows
alien creature drawing. In-Game asset. 2d. High contrast. No shadows
alien creature drawing. In-Game asset. 2d. High contrast. No shadows
blue crystal. In-Game asset. 2d. High contrast. No shadows
draw thick long laser bullet. In-Game asset. 2d. High contrast. No shadows
remove bonus text
spaceship. In-Game asset. 2d. High contrast. No shadows