/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highscoresData: "[]" }); /**** * Classes ****/ // Air Enemy Class (Snake Segment - Simplified for MVP) var AirEnemy = Container.expand(function (startX, startY) { var self = Container.call(this); var enemyGraphics = self.attachAsset('airEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.x = startX; self.y = startY; self.speedX = 4 + Math.random() * 2; // Horizontal speed self.amplitudeY = 100 + Math.random() * 100; // Vertical movement range self.frequencyY = 0.01 + Math.random() * 0.01; // Vertical movement speed self.fireRate = 150; // Ticks between shots self.fireCooldown = Math.random() * self.fireRate; self.update = function () { // Basic horizontal and sinusoidal vertical movement self.x -= self.speedX; self.y = startY + Math.sin(LK.ticks * self.frequencyY + startX) * self.amplitudeY; // Use startX for phase offset // Firing logic self.fireCooldown++; if (self.fireCooldown >= self.fireRate) { self.fireCooldown = 0; // Fire a single bullet straight left var bulletSpeed = 8; fireEnemyBullet(self.x, self.y, -bulletSpeed, 0); // vx = -speed, vy = 0 } // Boundary check (simple respawn logic) if (self.x < -enemyGraphics.width) { self.x = 2048 + enemyGraphics.width; // Respawn on the right startY = Math.random() * (2732 - 400) + 200; // Random Y position } }; return self; }); // Boss Class (Basic structure for future) var Boss = Container.expand(function () { var self = Container.call(this); var bossGraphics = self.attachAsset('boss', { anchorX: 0.5, anchorY: 0.5 }); self.x = 2048 - 300; // Position on the right self.y = 2732 / 2; // Center vertically self.fireRate = 180; // Ticks between laser bursts self.fireCooldown = 0; self.health = 100; // Example health self.update = function () { // Add movement logic if needed self.fireCooldown++; if (self.fireCooldown >= self.fireRate) { self.fireCooldown = 0; fireBossLasers(self.x, self.y); } }; return self; }); // Boss Laser Class (Basic structure for future) var BossLaser = Container.expand(function (startX, startY, vx, vy) { var self = Container.call(this); var laserGraphics = self.attachAsset('bossLaser', { anchorX: 0.5, anchorY: 0.5 // Anchor center }); self.x = startX; self.y = startY; self.vx = vx; self.vy = vy; // Set rotation based on velocity direction self.rotation = Math.atan2(vy, vx) + Math.PI / 2; // Point laser in direction of travel (+90deg adjustment needed depending on asset orientation) self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; }); // Bottom Mountain Obstacle Class var BottomMountain = Container.expand(function () { var self = Container.call(this); var mountainGraphics = self.attachAsset('bottommountain', { anchorX: 0.5, anchorY: 0.75 // Anchor base at terrain edge }); self.speed = 5; // Should match terrain speed self.update = function () { self.x -= self.speed; }; return self; }); // DiagonalEnemy Class for Level 2 var DiagonalEnemy = Container.expand(function (xPos, yPos) { var self = Container.call(this); var graphics = self.attachAsset('diagonalEnemy', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = Math.PI / 4; self.x = xPos; self.y = yPos; self.speed = terrainSpeed + 1; // Slightly faster than terrain self.health = 4; self.fireRate = 160; self.fireCooldown = Math.random() * self.fireRate; self.isDestroyed = false; self.update = function () { if (self.isDestroyed) return; self.x -= self.speed; self.fireCooldown++; if (self.fireCooldown >= self.fireRate) { self.fireCooldown = 0; var bulletSpeed = 7; fireEnemyBullet(self.x, self.y, bulletSpeed, 0, 'enemyBulletBlue'); // Right fireEnemyBullet(self.x, self.y, -bulletSpeed, 0, 'enemyBulletBlue'); // Left fireEnemyBullet(self.x, self.y, 0, bulletSpeed, 'enemyBulletBlue'); // Down fireEnemyBullet(self.x, self.y, 0, -bulletSpeed, 'enemyBulletBlue'); // Up } }; self.takeDamage = function () { if (self.isDestroyed) return false; self.health--; LK.effects.flashObject(self, 0xFFFFFF, 100); if (self.health <= 0) { self.isDestroyed = true; LK.getSound('enemyExplosion').play(); var index = diagonalEnemies.indexOf(self); if (index > -1) { diagonalEnemies.splice(index, 1); } self.destroy(); return true; } return false; }; return self; }); // Enemy Bullet Class var EnemyBullet = Container.expand(function (startX, startY, vx, vy, assetId) { var self = Container.call(this); // Added assetId var bulletGraphics = self.attachAsset(assetId || 'enemyBullet', { // Use assetId or default anchorX: 0.5, anchorY: 0.5 }); self.x = startX; self.y = startY; self.vx = vx; // Horizontal velocity self.vy = vy; // Vertical velocity self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; }); // Ground Enemy Class var GroundEnemy = Container.expand(function (isTop) { var self = Container.call(this); var enemyGraphics = self.attachAsset(isTop ? 'groundEnemytop' : 'groundEnemybottom', { anchorX: 0.5, anchorY: isTop ? 0 : 1 // Anchor base at terrain edge }); self.isTop = isTop; self.speed = 5; // Should match terrain speed self.fireRate = 120; // Ticks between firing sequences (2 seconds) self.fireCooldown = Math.random() * self.fireRate; // Random initial delay self.shotsInBurst = 3; self.burstDelay = 10; // Ticks between shots in a burst self.update = function () { self.x -= self.speed; self.fireCooldown++; if (self.fireCooldown >= self.fireRate) { self.fireCooldown = 0; // Reset cooldown // Fire burst for (var i = 0; i < self.shotsInBurst; i++) { LK.setTimeout(function () { // Check if enemy still exists before firing if (!self.destroyed) { var bulletSpeed = 8; // Calculate direction logic is now in fireEnemyBullet, but still need to pass initial values var vx = -bulletSpeed * 0.707; // Default direction if player not available var vy = self.isTop ? bulletSpeed * 0.707 : -bulletSpeed * 0.707; // Default direction fireEnemyBullet(self.x, self.y, vx, vy); } }, i * self.burstDelay); } } }; return self; }); // Player Bullet Class var PlayerBullet = Container.expand(function (startX, startY) { var self = Container.call(this); var bulletGraphics = self.attachAsset('playerBullet', { anchorX: 0.5, anchorY: 0.5 }); self.x = startX; self.y = startY; self.speed = 20; // Moves to the right self.update = function () { self.x += self.speed; }; return self; }); var SerpentEnemyShip = Container.expand(function (formation, relX, relY) { var self = Container.call(this); var graphics = self.attachAsset('serpentEnemyShip', { // Or 'airEnemy' if using that asset ID directly anchorX: 0.5, anchorY: 0.5 }); self.parentFormation = formation; self.x = relX; // Relative X to formation anchor self.y = relY; // Relative Y to formation anchor self.health = 2; self.fireRate = 140; // Slightly slower than some other enemies self.fireCooldown = Math.random() * self.fireRate; self.isDestroyed = false; // update() is not strictly needed here if all positioning is handled by parentFormation setting self.x/y // However, firing logic will be here. self.updateShipLogic = function () { if (self.isDestroyed || !self.parentFormation || self.parentFormation.isDestroyed) { if (!self.isDestroyed) self.destroy(); return; } // Firing logic self.fireCooldown++; if (self.fireCooldown >= self.fireRate && player && !player.isDead) { self.fireCooldown = 0; // Calculate global position for bullet spawn var globalPos = self.parentFormation.toGlobal(self.position); var gamePos = game.toLocal(globalPos); var bulletSpeed = 7; // Basic targeting or straight shot var vx = -bulletSpeed; var vy = 0; if (player && !player.isDead) { var dx = player.x - gamePos.x; var dy = player.y - gamePos.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { vx = dx / dist * bulletSpeed; vy = dy / dist * bulletSpeed; } } fireEnemyBullet(gamePos.x, gamePos.y, vx, vy, 'enemyBulletBlue'); } }; self.takeDamage = function () { if (self.isDestroyed) return false; self.health--; LK.effects.flashObject(self, 0xFFFFFF, 100); if (self.health <= 0) { self.isDestroyed = true; LK.getSound('enemyExplosion').play(); if (self.parentFormation) { self.parentFormation.shipDestroyed(self); } self.destroy(); return true; } return false; }; return self; }); var SerpentFormation = Container.expand(function () { var self = Container.call(this); self.ships = []; self.stage = 0; // 0: approach, 1: retreat, 2: pass, 3: offscreen/done self.anchorX = 2048 + 400; // Initial X off-screen right self.anchorY = 2732 / 2 + (Math.random() * 600 - 300); // Initial Y with some variance self.x = self.anchorX; self.y = self.anchorY; self.targetAnchorX = 0; self.targetAnchorY = 0; self.movementSpeed = 6; self.retreatDirection = Math.random() < 0.5 ? 1 : -1; // 1 for bottom-right, -1 for top-right var APPROACH_TARGET_X = 1024; var RETREAT_DELTA_X = 500; var RETREAT_DELTA_Y = 700; var PASS_TARGET_X = -600; // Further off-screen left var SERPENT_SHIP_OFFSETS = [{ x: 0, y: 0 }, { x: -70, y: 35 }, { x: -140, y: -35 }, { x: -210, y: 70 }, { x: -280, y: -70 }, { x: -350, y: 105 }, { x: -420, y: -105 }]; self.initializeShips = function () { for (var i = 0; i < 7; i++) { var offset = SERPENT_SHIP_OFFSETS[i]; var ship = new SerpentEnemyShip(self, offset.x, offset.y); self.addChild(ship); // Add ship as child of the formation container self.ships.push(ship); } }; self.setMovementStage = function (newStage) { self.stage = newStage; switch (self.stage) { case 0: // Approach self.targetAnchorX = APPROACH_TARGET_X; self.targetAnchorY = self.anchorY; // Maintain initial Y for approach break; case 1: // Diagonal Retreat self.targetAnchorX = self.anchorX + RETREAT_DELTA_X; self.targetAnchorY = self.anchorY + self.retreatDirection * RETREAT_DELTA_Y; break; case 2: // Forward Pass self.targetAnchorX = PASS_TARGET_X; // self.targetAnchorY remains from retreat or can be adjusted break; case 3: // Done // No target, just indicates it's finished its path break; } }; self.update = function () { if (self.stage === 3) return; // Done moving var dx = self.targetAnchorX - self.x; var dy = self.targetAnchorY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.movementSpeed * 1.5) { // Reached target for current stage self.x = self.targetAnchorX; self.y = self.targetAnchorY; self.anchorX = self.x; // Update internal anchor tracking self.anchorY = self.y; self.setMovementStage(self.stage + 1); } else { self.x += dx / dist * self.movementSpeed; self.y += dy / dist * self.movementSpeed; self.anchorX = self.x; self.anchorY = self.y; } // Update individual ships' logic (like firing) for (var i = self.ships.length - 1; i >= 0; i--) { var ship = self.ships[i]; if (ship.isDestroyed) { // Ship already removed itself from parent (self) and called shipDestroyed } else { ship.updateShipLogic(); } } if (self.stage === 3 && self.x < PASS_TARGET_X + SERPENT_SHIP_OFFSETS[SERPENT_SHIP_OFFSETS.length - 1].x - 200) { // Check if fully off-screen // Handled by isDone check in main game loop } }; self.shipDestroyed = function (ship) { var index = self.ships.indexOf(ship); if (index > -1) { self.ships.splice(index, 1); } // If the formation itself is a container, the ship's destroy method handles removing from parent. }; self.isDone = function () { // Formation is done if all ships are destroyed OR if it has completed its path and is off-screen if (self.ships.length === 0) return true; // Check based on the leader (first ship offset) being sufficiently off-screen var leaderGlobalX = self.x + SERPENT_SHIP_OFFSETS[0].x; if (self.stage === 3 && leaderGlobalX < PASS_TARGET_X + SERPENT_SHIP_OFFSETS[0].x - 100) { // Check leader's x pos return true; } return false; }; // Initialize self.initializeShips(); self.setMovementStage(0); // Start with approaching stage return self; }); // SmallTerrain Class for Level 2 Area 4 var SmallTerrain = Container.expand(function (isTop, initialX) { var self = Container.call(this); var graphics = self.attachAsset('smallTerrain', { anchorX: 0.5, anchorY: 0.5 }); self.x = initialX; self.y = isTop ? 400 + 50 : 2732 - 400 - 50; // 400px from edges, centered on terrain height self.isTop = isTop; self.speed = terrainSpeed; self.tank = null; // Will hold the tank on this terrain self.isDestroyed = false; self.update = function () { if (self.isDestroyed) return; self.x -= self.speed; }; self.addTank = function () { if (!self.tank && !self.isDestroyed) { self.tank = new Tank(self.isTop, self.x, self); // Pass terrain reference game.addChild(self.tank); tanks.push(self.tank); } }; self.destroy = function () { if (self.tank && !self.tank.isDestroyed) { self.tank.destroy(); } self.isDestroyed = true; Container.prototype.destroy.call(self); }; return self; }); // Tank Class for Level 2 var Tank = Container.expand(function (isTop, initialX, terrain) { var self = Container.call(this); var terrainHeight = terrain ? 100 : 200; // Use 100 for small terrains, 200 for regular var graphics = self.attachAsset('tank', { anchorX: 0.5, anchorY: isTop ? 0 : 1 }); self.x = initialX; self.y = terrain ? isTop ? 400 + 50 : 2732 - 400 - 50 : isTop ? terrainHeight : 2732 - terrainHeight; self.isTop = isTop; self.speed = terrainSpeed; self.health = 6; // Tanks are sturdy self.fireRate = 170; self.fireCooldown = Math.random() * self.fireRate; self.isDestroyed = false; self.parentTerrain = terrain; // Reference to terrain for area4 tanks self.horizontalSpeed = 3; // Speed for left-right movement on small terrains self.movingRight = Math.random() < 0.5; // Random initial direction self.update = function () { if (self.isDestroyed) return; if (self.parentTerrain) { // Area4 tank: move horizontally on small terrain var terrainLeft = self.parentTerrain.x - 150; // Half width of small terrain var terrainRight = self.parentTerrain.x + 150; if (self.movingRight) { self.x += self.horizontalSpeed; if (self.x >= terrainRight) { self.movingRight = false; } } else { self.x -= self.horizontalSpeed; if (self.x <= terrainLeft) { self.movingRight = true; } } // Move with terrain self.x -= self.speed; } else { // Regular area2 tank: just move with terrain self.x -= self.speed; } self.fireCooldown++; if (self.fireCooldown >= self.fireRate) { self.fireCooldown = 0; var bulletSpeed = 8; var vx = -bulletSpeed * 0.707; // Aim slightly forward var vyDirection = self.isTop ? bulletSpeed * 0.707 : -bulletSpeed * 0.707; fireEnemyBullet(self.x, self.y, vx, vyDirection, 'enemyBulletBlue'); } }; self.takeDamage = function () { if (self.isDestroyed) return false; self.health--; LK.effects.flashObject(self, 0xFFFFFF, 100); if (self.health <= 0) { self.isDestroyed = true; LK.getSound('enemyExplosion').play(); var index = tanks.indexOf(self); if (index > -1) { tanks.splice(index, 1); } self.destroy(); return true; } return false; }; return self; }); // Terrain Segment Class var TerrainSegment = Container.expand(function (isTop) { var self = Container.call(this); var terrainGraphics = self.attachAsset('terrain', { anchorX: 0, anchorY: isTop ? 0 : 1 // Anchor at top for top terrain, bottom for bottom terrain }); self.isTop = isTop; self.speed = 5; // Horizontal scroll speed self.update = function () { self.x -= self.speed; }; return self; }); // Top Mountain Obstacle Class var TopMountain = Container.expand(function () { var self = Container.call(this); var mountainGraphics = self.attachAsset('topmountain', { anchorX: 0.5, anchorY: 0.3 // Anchor base at terrain edge }); self.speed = 5; // Should match terrain speed self.update = function () { self.x -= self.speed; }; return self; }); // Player UFO Class var UFO = Container.expand(function () { var self = Container.call(this); var ufoGraphics = self.attachAsset('ufo', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 10; // Movement speed multiplier, adjust as needed self.isDead = false; self.invincibleUntil = 0; // Keep UFO within game boundaries self.clampPosition = function () { var halfWidth = ufoGraphics.width / 2; var halfHeight = ufoGraphics.height / 2; if (self.x < halfWidth) { self.x = halfWidth; } if (self.x > 2048 - halfWidth) { self.x = 2048 - halfWidth; } if (self.y < halfHeight + 100) { self.y = halfHeight + 100; } // Avoid top-left menu area if (self.y > 2732 - halfHeight) { self.y = 2732 - halfHeight; } // Adjust based on terrain phase (Level 1 Phase 0 or if it's Level 2 which also has terrain) if (gamePhase === 0 || currentLevel === 2) { // Find the current terrain height at the UFO's x position (simplified) var terrainHeight = 200; // Assuming constant terrain height for now if (self.y < terrainHeight + halfHeight) { self.y = terrainHeight + halfHeight; } if (self.y > 2732 - terrainHeight - halfHeight) { self.y = 2732 - terrainHeight - halfHeight; } } }; return self; }); // VerticalWall Class for Level 2 var VerticalWall = Container.expand(function (xPos) { var self = Container.call(this); var wallGraphicAsset = LK.getAsset('verticalWall', {}); var graphics = self.attachAsset('verticalWall', { anchorX: 0.5, anchorY: 0.5 }); self.width = wallGraphicAsset.width; // Store actual width for calculations self.height = wallGraphicAsset.height; // Store actual height self.x = xPos; self.y = 2732 / 2; self.speed = terrainSpeed; self.turrets = []; self.activeTurrets = 4; self.isDestroyed = false; // Wall graphics anchor is 0.5, 0.5. // Turrets are positioned on the front-facing side of the wall. // Spread 4 turrets along the height. var turretPositions = [-0.35 * self.height, -0.15 * self.height, 0.15 * self.height, 0.35 * self.height]; // Relative Y from center for (var i = 0; i < 4; i++) { // Turrets are on the "front" (left edge for player) of the wall var turret = new WallTurret(self, -0.4, turretPositions[i]); // relX as factor of half-width, relY absolute self.turrets.push(turret); game.addChild(turret); } self.update = function () { if (self.isDestroyed) return; self.x -= self.speed; }; self.turretDestroyed = function (turret) { if (self.isDestroyed) return; self.activeTurrets--; if (self.activeTurrets <= 0) { self.isDestroyed = true; LK.getSound('bossExplosion').play(); var wallIndex = verticalWalls.indexOf(self); if (wallIndex > -1) { verticalWalls.splice(wallIndex, 1); } // Ensure remaining turrets are cleaned up if any edge case self.turrets.forEach(function (t) { if (t && !t.isDestroyed) t.destroy(); }); self.destroy(); } }; return self; }); // WallTurret Class for Level 2 var WallTurret = Container.expand(function (wall, relX, relY) { var self = Container.call(this); var graphics = self.attachAsset('wallTurret', { anchorX: 0.5, anchorY: 0.5 }); self.parentWall = wall; self.relX = relX; self.relY = relY; self.health = 2; // Turrets are a bit tougher self.fireRate = 130; // Initial and current fire rate self.minFireRate = 60; // Minimum fire rate (e.g., 60 ticks = 1 shot per second) self.fireRateReductionAmount = 15; // Amount to reduce fireRate by each time self.shotsFiredCount = 0; // Counter for total shots fired by this turret self.shotsPerRateReduction = 3; // Reduce fire rate every this many shots self.fireCooldown = Math.random() * self.fireRate; self.isDestroyed = false; self.update = function () { if (self.isDestroyed || !self.parentWall || self.parentWall.isDestroyed) { if (!self.isDestroyed) self.destroy(); // Self-cleanup if parent is gone return; } // Update position relative to the moving wall self.x = self.parentWall.x + self.relX * (self.parentWall.width / 2); // relX is factor of half-width self.y = self.parentWall.y + self.relY; self.fireCooldown++; if (self.fireCooldown >= self.fireRate) { self.fireCooldown = 0; fireEnemyBullet(self.x, self.y, -9, 0, 'enemyBulletBlue'); // Shoots blue bullets left self.shotsFiredCount++; // Check if it's time to reduce the fire rate if (self.shotsFiredCount > 0 && self.shotsFiredCount % self.shotsPerRateReduction === 0) { if (self.fireRate > self.minFireRate) { self.fireRate = Math.max(self.fireRate - self.fireRateReductionAmount, self.minFireRate); // The fire cooldown will naturally be shorter for the next cycle due to the reduced self.fireRate } } } }; self.takeDamage = function () { if (self.isDestroyed) return false; self.health--; LK.effects.flashObject(self, 0xFFFFFF, 100); if (self.health <= 0) { self.isDestroyed = true; LK.getSound('enemyExplosion').play(); if (self.parentWall && !self.parentWall.isDestroyed) { self.parentWall.turretDestroyed(self); } self.destroy(); return true; } return false; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x101030 // Dark space blue background }); /**** * Game Code ****/ // Reusing airEnemy asset for serpent ship // Placeholder for Level 2 music // Deep Sky Blue // Forest Green // Dark Orange // Grey turret // Brownish wall // Level 2 Assets // Game State /**** * Assets * Assets are automatically created and loaded either dynamically during gameplay * or via static code analysis based on their usage in the code. ****/ // Initialize assets used in this game. Scale them according to what is needed for the game. // Player UFO // Ground/Ceiling // Mountain Obstacle (simplified) // Ground Enemy // Enemy Bullet // Air Enemy (Placeholder shape) // Boss // Boss Laser Beam // Minimalistic tween library which should be used for animations over time // LK.init.image('mountain', {width:150, height:250, id:'6819884bc4a0c8bae9e84ae0'}) // Removed - Replaced by top/bottom mountains var isGameStarted = false; // Flag to track if game has started var showingHighscores = false; // Flag to track if highscore screen is shown var gamePhase = 0; // 0: Terrain, 1: Space, 2: Boss var phaseStartTime = LK.ticks; var phaseDuration = 3600; // 1 minute (60 seconds * 60 fps = 3600 ticks) var terrainSpeed = 5; var scoreIncrementTimer = 0; var highscoreBackground = null; var highscoreTexts = []; // Original, may become vestigial or be fully removed depending on final showHighscore structure var elementsShownByShowHighscore = []; // New global array for showHighscore's element management // Game Objects var player = null; var terrainSegmentsTop = []; var terrainSegmentsBottom = []; var mountains = []; var groundEnemies = []; var enemyBullets = []; var airEnemies = []; // Simple air enemies for MVP phase 2 var boss = null; var bossLasers = []; var playerBullets = []; // Array for player bullets // Player Control var dragNode = null; var playerFireRate = 15; // Ticks between shots (4 shots per second) var playerFireCooldown = 0; // Level 2 State and Constants var currentLevel = 1; var level2Progress = 0; // In pixels, for current level 2 progression var level2Started = false; var lastWallSpawnProgress = 0; var lastDiagonalEnemySpawnProgress = 0; var lastTankSpawnProgress = 0; var verticalWalls = []; var diagonalEnemies = []; var tanks = []; // enemyBulletsBlue will use the existing enemyBullets array var PIXELS_PER_MILE = 250; // Adjusted for gameplay feel var LEVEL2_AREA1_END_MILES = 500; var LEVEL2_AREA2_DURATION_MILES = 500; // Duration of Area 2 var WALL_SPAWN_INTERVAL_MILES = 50; var DIAGONAL_ENEMY_SPAWN_INTERVAL_MILES = 50; var TANK_SPAWN_INTERVAL_MILES = 10; // Level 2 Area 3: Serpent Formations var serpentFormations = []; var MAX_SERPENT_FORMATIONS = 2; // Max 1-2 active at once var lastSerpentSpawnProgress = 0; // Tracks level2Progress for serpent spawning var SERPENT_SPAWN_INTERVAL_MILES = 100; // Spawn a new serpent formation every 100 miles in Area 3 var LEVEL2_AREA3_DURATION_MILES = 700; // Duration of Area 3 var LEVEL2_AREA4_DURATION_MILES = 500; // Duration of Area 4 var SMALL_TERRAIN_SPAWN_INTERVAL_MILES = 80; // Spawn interval for small terrains in area4 var lastSmallTerrainSpawnProgress = 0; // Tracks spawning of small terrains var smallTerrains = []; // Array to track small terrain pieces // Player Lives var MAX_PLAYER_LIVES = 5; var playerLives = MAX_PLAYER_LIVES; var livesTxt; // Score Display var scoreTxt = new Text2('0', { size: 100, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Position score at top center // Helper function to create terrain segments function createTerrainSegment(isTop, xPos) { var segment = new TerrainSegment(isTop); segment.x = xPos; segment.y = isTop ? 0 : 2732; segment.speed = terrainSpeed; game.addChild(segment); if (isTop) { terrainSegmentsTop.push(segment); } else { terrainSegmentsBottom.push(segment); } return segment; } // Helper function to spawn mountains on terrain function spawnMountain(terrainSegment) { var mountain; if (terrainSegment.isTop) { mountain = new TopMountain(); } else { mountain = new BottomMountain(); } // Position relative to the terrain segment // Ensure we use the actual height of the terrain graphic for positioning var terrainGraphic = terrainSegment.children[0]; // Assuming the graphic is the first child mountain.x = terrainSegment.x + Math.random() * terrainGraphic.width; // Position based on whether it's a top or bottom mountain mountain.y = terrainSegment.isTop ? terrainGraphic.height : 2732 - terrainGraphic.height; mountain.speed = terrainSpeed; game.addChild(mountain); mountains.push(mountain); } // Helper function to spawn ground enemies on terrain function spawnGroundEnemy(terrainSegment) { var enemy = new GroundEnemy(terrainSegment.isTop); // Position relative to the terrain segment // Ensure we use the actual height of the terrain graphic for positioning var terrainGraphic = terrainSegment.children[0]; // Assuming the graphic is the first child enemy.x = terrainSegment.x + Math.random() * terrainGraphic.width; enemy.y = terrainSegment.isTop ? terrainGraphic.height : 2732 - terrainGraphic.height; enemy.speed = terrainSpeed; game.addChild(enemy); groundEnemies.push(enemy); } // Helper function to fire enemy bullets function fireEnemyBullet(x, y, vx, vy, assetId) { // Added assetId parameter // If player exists and is not dead, target the player instead of using fixed direction if (player && !player.isDead) { // Calculate direction vector to player var dx = player.x - x; var dy = player.y - y; // Normalize the vector var distance = Math.sqrt(dx * dx + dy * dy); // Use the bullet speed provided (or calculate from vx and vy if assetId is for styling only) // For simplicity, let's assume the passed vx,vy define the base speed. var baseSpeed = Math.sqrt(vx * vx + vy * vy); if (distance > 0 && baseSpeed > 0) { // Ensure distance and baseSpeed are valid vx = dx / distance * baseSpeed; vy = dy / distance * baseSpeed; } // If player is not available or too close, use original vx, vy } var bullet = new EnemyBullet(x, y, vx, vy, assetId); // Pass assetId to constructor game.addChild(bullet); enemyBullets.push(bullet); LK.getSound('enemyShoot').play(); } // Helper function to spawn air enemies (simple version) function spawnAirEnemy() { var startY = Math.random() * (2732 - 400) + 200; // Avoid edges var enemy = new AirEnemy(2048 + 100, startY); // Start off-screen right game.addChild(enemy); airEnemies.push(enemy); } // Helper function to fire boss lasers function fireBossLasers(x, y) { var directions = 5; var laserSpeed = 12; var verticalSpread = 4; // Max vertical speed component for (var i = 0; i < directions; i++) { // Calculate vertical velocity component for spread // Example: i=0 -> -4, i=1 -> -2, i=2 -> 0, i=3 -> 2, i=4 -> 4 var vy = -verticalSpread + verticalSpread * 2 / (directions - 1) * i; // Keep horizontal speed constant (moving left) var vx = -laserSpeed; var laser = new BossLaser(x, y, vx, vy); game.addChild(laser); bossLasers.push(laser); } // Add sound effect for laser fire (consider adding one e.g., LK.getSound('bossShoot').play(); if asset exists) } // Initialize Game Elements function initGame() { // Reset state LK.setScore(0); scoreTxt.setText('0'); // Hide score during intro scoreTxt.visible = !isGameStarted; // Initialize Lives playerLives = MAX_PLAYER_LIVES; if (!livesTxt) { // Create only if it doesn't exist (for game restarts) livesTxt = new Text2("Lives: x" + playerLives, { size: 80, // Slightly smaller than score fill: 0xFFFFFF }); livesTxt.anchor.set(1, 0); // Anchor top-right LK.gui.topRight.addChild(livesTxt); } else { livesTxt.setText("Lives: x" + playerLives); } gamePhase = 0; phaseStartTime = LK.ticks; dragNode = null; playerBullets = []; // Clear player bullets playerFireCooldown = 0; // Reset fire cooldown // Clear existing elements from previous game (if any) // Note: LK engine handles full reset on GameOver/YouWin, but manual cleanup might be needed if restarting mid-game (not typical) // Let's assume full reset is handled by LK. // Create Player player = new UFO(); player.x = 300; player.y = 2732 / 2; player.isDead = false; player.invincibleUntil = 0; player.alpha = 1; // Ensure player is visible game.addChild(player); // Create initial terrain var terrainWidth = LK.getAsset('terrain', {}).width; // Get width from asset for (var i = 0; i < Math.ceil(2048 / terrainWidth) + 1; i++) { createTerrainSegment(true, i * terrainWidth); createTerrainSegment(false, i * terrainWidth); } // Start Phase 1 Music LK.playMusic('phase1Music'); } function initLevel2() { if (level2Started) return; level2Started = true; currentLevel = 2; // Explicitly set, though might be set at transition point too level2Progress = 0; // Reset progress for Level 2 console.log("Initializing Level 2"); LK.playMusic('level2Music'); // Play Level 2 music // Clean up any remaining Level 1 specific elements like air enemies airEnemies.forEach(function (ae) { if (ae && !ae.destroyed) ae.destroy(); }); airEnemies = []; // Boss should already be gone if (boss && !boss.destroyed) boss.destroy(); boss = null; bossLasers.forEach(function (l) { if (l && !l.destroyed) l.destroy(); }); bossLasers = []; // Reset spawn progress markers for Level 2 elements lastWallSpawnProgress = 0; lastDiagonalEnemySpawnProgress = 0; lastTankSpawnProgress = 0; // Ensure terrain continues if it was cleared (e.g., after boss fight if phases stopped it) // The main terrain update loop should be modified to run during currentLevel === 2 var terrainWidth = LK.getAsset('terrain', {}).width; var screenWidth = 2048; var numSegmentsNeeded = Math.ceil(screenWidth / terrainWidth) + 1; if (terrainSegmentsTop.length < numSegmentsNeeded) { for (var i = terrainSegmentsTop.length; i < numSegmentsNeeded; i++) { var xPos = i * terrainWidth; // Find max X of existing segments to append correctly if (terrainSegmentsTop.length > 0) { xPos = terrainSegmentsTop.reduce(function (max, s) { return s.x > max ? s.x : max; }, 0) + terrainWidth; } createTerrainSegment(true, xPos); } } if (terrainSegmentsBottom.length < numSegmentsNeeded) { for (var i = terrainSegmentsBottom.length; i < numSegmentsNeeded; i++) { var xPos = i * terrainWidth; if (terrainSegmentsBottom.length > 0) { xPos = terrainSegmentsBottom.reduce(function (max, s) { return s.x > max ? s.x : max; }, 0) + terrainWidth; } createTerrainSegment(false, xPos); } } // Announce Level 2 Start (Optional visual cue) var level2Text = new Text2("LEVEL 2", { size: 150, fill: 0xFFFF00 }); level2Text.anchor.set(0.5, 0.5); level2Text.x = 2048 / 2; level2Text.y = 2732 / 3; game.addChild(level2Text); LK.setTimeout(function () { if (level2Text && !level2Text.destroyed) level2Text.destroy(); }, 3000); // Display for 3 seconds // gamePhase variable might be set to a new value (e.g., 3) if other systems rely on it. // For now, currentLevel = 2 will gate Level 2 logic. gamePhase = 3; // Indicate a new phase distinct from level 1's 0, 1, 2. } // Helper function already moved above // Helper function to save highscore function saveHighscore() { // Removed score argument, will use LK.getScore() internally var currentScore = LK.getScore(); var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest"; var currentPlayerName = LK.profile && LK.profile.name ? LK.profile.name : "FRVR Player"; var highscores = []; try { if (storage && storage.available && storage.highscoresData) { var parsedData = JSON.parse(storage.highscoresData); if (Array.isArray(parsedData)) { highscores = parsedData; } } } catch (e) { console.log("Error parsing existing highscoresData, resetting: " + e.message); highscores = []; } // Add current score highscores.push({ id: currentPlayerId, name: currentPlayerName, score: currentScore, date: Date.now() // Store timestamp for sorting or display }); // Sort by score (descending), then by date (newest first for tie-breaking) highscores.sort(function (a, b) { if (b.score === a.score) { return b.date - a.date; } return b.score - a.score; }); // Limit to a reasonable number of stored highscores, e.g., top 50 var MAX_STORED_HIGHSCORES = 50; if (highscores.length > MAX_STORED_HIGHSCORES) { highscores = highscores.slice(0, MAX_STORED_HIGHSCORES); } // Save back to storage try { if (storage && storage.available) { storage.highscoresData = JSON.stringify(highscores); } } catch (e) { console.log("Error stringifying or saving highscoresData: " + e.message); } // Update the player's personal best score, stored separately for quick access // This key is used by createScoreBackground for "Your Best" display if (currentPlayerId !== "guest") { var personalBestKey = 'player_' + currentPlayerId; var currentPersonalBest = 0; try { if (storage && storage.available && storage[personalBestKey]) { currentPersonalBest = parseInt(storage[personalBestKey], 10) || 0; } } catch (parseError) { console.log("Error parsing personal best for key " + personalBestKey + ": " + parseError.message); currentPersonalBest = 0; } if (currentScore > currentPersonalBest) { if (storage && storage.available) { try { storage[personalBestKey] = currentScore.toString(); } catch (saveError) { console.log("Error saving personal best for key " + personalBestKey + ": " + saveError.message); } } } } } // The createScoreBackground function is now defined earlier in the code // This duplicate definition has been moved earlier in the code // Helper function to handle player death function handlePlayerDeath() { if (!player || player.isDead) { return; } // Event Handlers player.isDead = true; player.alpha = 0; // Make player invisible during death processing LK.getSound('playerExplosion').play(); // Optional: Flash the player before making them fully invisible, or flash screen // LK.effects.flashObject(player, 0xFF0000, 500); LK.effects.flashScreen(0xFF0000, 200); // Short screen flash playerLives--; if (livesTxt) { livesTxt.setText("Lives: x" + playerLives); } LK.setTimeout(function () { if (playerLives > 0) { respawnPlayer(); } else { // Save highscore before showing Game Over saveHighscore(); //{6o} // Argument removed // Create background for scores before showing game over createScoreBackground(); LK.showGameOver(); } }, 1000); // 1 second delay for effects and sound } // Helper function to create score background when game is won or lost function createScoreBackground() { var createdTextElements = []; // Store reference to background for later access if (highscoreBackground) { highscoreBackground.destroy(); } // Get current player information var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest"; var currentPlayerName = LK.profile && LK.profile.name ? LK.profile.name : "FRVR Player"; var currentScore = LK.getScore(); // Get highscores from storage var highscores = []; try { if (storage && storage.available && storage.highscoresData) { highscores = JSON.parse(storage.highscoresData); } } catch (e) { console.log("Error parsing highscores:", e); highscores = []; } // Check if the current score is already in the list (for game over after saving score) var scoreExists = false; for (var i = 0; i < highscores.length; i++) { if (highscores[i].id === currentPlayerId && highscores[i].score === currentScore && Date.now() - highscores[i].date < 60000) { // Added in last minute scoreExists = true; highscores[i].isCurrent = true; // Mark as current game's score break; } } // Add the current score to the highscores immediately if not already there if (!scoreExists) { highscores.push({ id: currentPlayerId, name: currentPlayerName, score: currentScore, date: Date.now(), isCurrent: true // Mark as current game's score }); } // Sort highscores by date (most recent first) highscores.sort(function (a, b) { return b.date - a.date; // Most recent date first }); var personalBest = 0; // Check if player has a personal best stored if (currentPlayerId !== "guest") { var personalKey = "player_" + currentPlayerId; try { if (storage && storage.available && storage[personalKey]) { personalBest = parseInt(storage[personalKey], 10) || 0; } // Also check from cloud storage if (storage && storage.available) { storage.load('player_score_' + currentPlayerId, function (savedScore) { if (savedScore) { var cloudScore = parseInt(savedScore, 10) || 0; if (cloudScore > personalBest) { personalBest = cloudScore; storage[personalKey] = personalBest.toString(); } } }); } } catch (e) { console.log("Error retrieving personal best:", e); } } // Create background highscoreBackground = game.addChild(LK.getAsset('Background2', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 20.48, scaleY: 27.32, alpha: 0.9 })); // Create title var titleText = new Text2("GAME RESULTS", { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0); titleText.x = 2048 / 2; titleText.y = 200; game.addChild(titleText); createdTextElements.push(titleText); // Current score var currentScoreText = new Text2("Your Score: " + currentScore, { size: 100, fill: 0x00FF00 }); currentScoreText.anchor.set(0.5, 0); currentScoreText.x = 2048 / 2; currentScoreText.y = 350; game.addChild(currentScoreText); createdTextElements.push(currentScoreText); // Show personal best if it exists if (personalBest > 0 && personalBest !== currentScore) { var personalBestText = new Text2("Your Best: " + personalBest, { size: 80, fill: 0xFFD700 }); personalBestText.anchor.set(0.5, 0); personalBestText.x = 2048 / 2; personalBestText.y = 470; game.addChild(personalBestText); createdTextElements.push(personalBestText); // Show new record indicator if (currentScore > personalBest) { var newRecordText = new Text2("NEW RECORD!", { size: 80, fill: 0xFF00FF }); newRecordText.anchor.set(0.5, 0); newRecordText.x = 2048 / 2; newRecordText.y = 560; game.addChild(newRecordText); createdTextElements.push(newRecordText); } } // Display up to 20 highscores instead of 10 var maxToShow = Math.min(highscores.length, 20); if (maxToShow > 0) { // Display title for scores var titleScoreText = new Text2("Recent Player Scores", { size: 90, fill: 0xFFD700 }); titleScoreText.anchor.set(0.5, 0); titleScoreText.x = 2048 / 2; titleScoreText.y = 700; game.addChild(titleScoreText); createdTextElements.push(titleScoreText); // Adjust spacing based on number of scores var spacing = maxToShow > 10 ? 90 : maxToShow > 5 ? 120 : 150; var fontSize = maxToShow > 10 ? 60 : maxToShow > 5 ? 70 : 80; // Display player scores for (var i = 0; i < maxToShow; i++) { var entry = highscores[i]; var isCurrentPlayer = entry.id === currentPlayerId; var isCurrentScore = entry.isCurrent === true; // Format score with comma separators var formattedScore = entry.score.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); // Get player name, using ID if name is missing var playerName = entry.name || "Player " + entry.id.substring(0, 5); var scoreEntry = new Text2(i + 1 + ". " + playerName + ": " + formattedScore, { size: fontSize, fill: isCurrentScore ? "#FF00FF" : isCurrentPlayer ? "#00FF00" : "#FFFFFF" // Magenta for current score, green for other scores by current player }); // If more than 10 entries, create two columns if (maxToShow > 10 && i >= 10) { // Second column (for entries 11-20) scoreEntry.anchor.set(0, 0); // Left align for second column scoreEntry.x = 2048 * 0.25; // Position at 1/4 of screen width scoreEntry.y = 800 + (i - 10) * spacing; } else if (maxToShow > 10) { // First column (for entries 1-10) scoreEntry.anchor.set(1, 0); // Right align for first column scoreEntry.x = 2048 * 0.75; // Position at 3/4 of screen width scoreEntry.y = 800 + i * spacing; } else { // Single column layout (for 10 or fewer entries) scoreEntry.anchor.set(0.5, 0); scoreEntry.x = 2048 / 2; scoreEntry.y = 800 + i * spacing; } game.addChild(scoreEntry); createdTextElements.push(scoreEntry); } } else { // No scores message var noScoresText = new Text2("No scores yet!", { size: 80, fill: 0xFFFFFF }); noScoresText.anchor.set(0.5, 0); noScoresText.x = 2048 / 2; noScoresText.y = 800; game.addChild(noScoresText); createdTextElements.push(noScoresText); } return createdTextElements; } // The createScoreBackground function has been moved earlier in the code // Helper function to respawn player at checkpoint function respawnPlayer() { if (!player) { return; } player.x = 300; player.y = 2732 / 2; player.clampPosition(); player.alpha = 1; // Make player visible again player.isDead = false; player.invincibleUntil = LK.ticks + 120; // 2 seconds of invincibility // Clear active threats for (var i = enemyBullets.length - 1; i >= 0; i--) { if (enemyBullets[i] && !enemyBullets[i].destroyed) { enemyBullets[i].destroy(); } } enemyBullets = []; if (gamePhase === 2) { // Boss phase for (var i = bossLasers.length - 1; i >= 0; i--) { if (bossLasers[i] && !bossLasers[i].destroyed) { bossLasers[i].destroy(); } } bossLasers = []; } // Player is reset, other game elements (enemies, boss health) remain. } // Create intro screen elements var backgroundAsset = LK.getAsset('Background0', {}); var scaleX = 2048 / backgroundAsset.width; var scaleY = 2732 / backgroundAsset.height; var background = game.addChild(LK.getAsset('Background0', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: scaleX, scaleY: scaleY })); // Play intro music LK.playMusic('Intromusic1'); var startButton = game.addChild(LK.getAsset('Startgamebutton', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 + 700, scaleX: 1.5, scaleY: 1.5 })); var highscoreButton = game.addChild(LK.getAsset('Highscorebutton', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 + 1050, scaleX: 1.5, scaleY: 1.5 })); function startGame() { // Remove intro elements background.destroy(); startButton.destroy(); highscoreButton.destroy(); // Set game as started isGameStarted = true; // Show score scoreTxt.visible = true; // Initialize the game initGame(); } function showHighscore() { if (showingHighscores) { // If currently showing, hide everything if (highscoreBackground) { // highscoreBackground is global, set by createScoreBackground highscoreBackground.destroy(); highscoreBackground = null; } elementsShownByShowHighscore.forEach(function (el) { if (el && !el.destroyed) { el.destroy(); } }); elementsShownByShowHighscore = []; showingHighscores = false; return; } // Not showing, so display the highscore screen showingHighscores = true; // Clear any residual elements from a previous display, just in case if (highscoreBackground && !highscoreBackground.destroyed) { highscoreBackground.destroy(); highscoreBackground = null; } elementsShownByShowHighscore.forEach(function (el) { if (el && !el.destroyed) { el.destroy(); } }); elementsShownByShowHighscore = []; // Call createScoreBackground to generate the main content. // This function sets the global 'highscoreBackground' and returns an array of its text elements. var textsFromCreateScoreBg = createScoreBackground(); elementsShownByShowHighscore = elementsShownByShowHighscore.concat(textsFromCreateScoreBg); // Add "TAP ANYWHERE TO RETURN" text, specific to this highscore view var backText = new Text2("TAP ANYWHERE TO RETURN", { size: 70, fill: 0xFFFFFF //{dT} // Kept original fill and size }); backText.anchor.set(0.5, 0); backText.x = 2048 / 2; backText.y = 2200; // Adjusted Y to be a bit lower if needed, or use original from createScoreBackground if it had one game.addChild(backText); elementsShownByShowHighscore.push(backText); // Add to our list for cleanup // Fallback to LK.showLeaderboard if our custom screen is up for too long if (typeof LK.showLeaderboard === 'function') { LK.setTimeout(function () { if (showingHighscores) { // Check if our screen is still active showingHighscores = false; // Toggle off state if (highscoreBackground && !highscoreBackground.destroyed) { highscoreBackground.destroy(); highscoreBackground = null; } elementsShownByShowHighscore.forEach(function (el) { if (el && !el.destroyed) { el.destroy(); } }); elementsShownByShowHighscore = []; LK.showLeaderboard(); // Show the engine's leaderboard } }, 5000); // 5 second timeout } } // Add interactive behavior to buttons startButton.interactive = true; startButton.buttonMode = true; startButton.down = function () { startGame(); }; highscoreButton.interactive = true; highscoreButton.buttonMode = true; highscoreButton.down = function () { showHighscore(); }; game.down = function (x, y, obj) { // Handle tapping on highscore screen to return to intro if (showingHighscores) { showHighscore(); // This will toggle off the highscore display return; } // Only process player dragging if game has started if (isGameStarted && player) { // Check if touch is on the player UFO var localPos = player.toLocal(game.toGlobal({ x: x, y: y })); // Convert game coords to player's local coords // Use a slightly larger hit area for easier dragging var hitWidth = player.width * 1.5; var hitHeight = player.height * 1.5; if (Math.abs(localPos.x) < hitWidth / 2 && Math.abs(localPos.y) < hitHeight / 2) { dragNode = player; // Instantly move player to touch position for responsive feel var gamePos = game.toLocal(obj.global); // Use obj.global for precise position player.x = gamePos.x; player.y = gamePos.y; player.clampPosition(); } else { dragNode = null; } } }; game.move = function (x, y, obj) { if (dragNode) { var gamePos = game.toLocal(obj.global); // Convert event global position to game coordinates dragNode.x = gamePos.x; dragNode.y = gamePos.y; // Clamp player position within bounds immediately after move if (dragNode === player) { player.clampPosition(); } } }; game.up = function (x, y, obj) { dragNode = null; }; // Game Update Logic game.update = function () { // If game hasn't started yet or player is gone, don't process game logic if (!isGameStarted || !player || player.destroyed) { // Check if we need to initialize level 2 due to a reload/restart mid-level 2 // This is an edge case, main L2 init is after boss defeat. // if (currentLevel === 2 && !level2Started && isGameStarted && player) { initLevel2(); } // Make sure score is hidden during intro scoreTxt.visible = false; return; // Game not started or player fully gone, nothing to do. } // Make sure score is visible during gameplay scoreTxt.visible = true; // Initialize Level 2 if conditions are met (transitioned from Level 1) if (currentLevel === 2 && !level2Started && isGameStarted && player && !player.isDead) { initLevel2(); } // If player is dead, stop their specific logic & wait for respawn/game over timeout // Other game elements (enemies, bullets) might still update. // Player input and collisions will be gated by player.isDead or invincibility. // Player invincibility blinking if (player && !player.isDead && LK.ticks < player.invincibleUntil) { player.alpha = LK.ticks % 20 < 10 ? 0.5 : 1; // Blink } else if (player && !player.isDead && player.alpha !== 1) { player.alpha = 1; // Ensure alpha is reset } // --- Global Updates --- // Player Shooting playerFireCooldown++; if (!player.isDead && dragNode === player && playerFireCooldown >= playerFireRate) { // Only shoot while dragging/controlling and alive playerFireCooldown = 0; var bullet = new PlayerBullet(player.x + player.width / 2, player.y); game.addChild(bullet); playerBullets.push(bullet); LK.getSound('playerShoot').play(); } // Score increases over time scoreIncrementTimer++; if (scoreIncrementTimer >= 60) { // Add 10 points every second scoreIncrementTimer = 0; LK.setScore(LK.getScore() + 10); scoreTxt.setText(LK.getScore()); } // Increment distance scrolled (used by Level 2) if (player && !player.isDead) { // Only scroll if player is active and game is running // gameDistanceScrolled += terrainSpeed; // This was for overall game, level2Progress is specific if (currentLevel === 2 && level2Started) { level2Progress += terrainSpeed; } } // --- Phase Management --- var elapsedTicks = LK.ticks - phaseStartTime; if (gamePhase === 0 && elapsedTicks >= phaseDuration) { // Transition to Phase 1 (Space) gamePhase = 1; phaseStartTime = LK.ticks; // Reset timer for next phase if needed console.log("Transitioning to Phase 1: Space"); // Clean up terrain-specific elements terrainSegmentsTop.forEach(function (s) { return s.destroy(); }); terrainSegmentsBottom.forEach(function (s) { return s.destroy(); }); mountains.forEach(function (m) { return m.destroy(); }); groundEnemies.forEach(function (ge) { return ge.destroy(); }); terrainSegmentsTop = []; terrainSegmentsBottom = []; mountains = []; groundEnemies = []; // Spawn initial air enemies for (var i = 0; i < 5; i++) { // Start with 5 air enemies spawnAirEnemy(); } LK.playMusic('phase2Music'); // Switch music } else if (gamePhase === 1 && elapsedTicks >= phaseDuration * 1.5) { // Example: Boss after 1.5x phase duration // Transition to Phase 2 (Boss) if (!boss) { // Ensure boss only spawns once gamePhase = 2; phaseStartTime = LK.ticks; console.log("Transitioning to Phase 2: Boss"); // Clean up air enemies airEnemies.forEach(function (ae) { return ae.destroy(); }); airEnemies = []; // Spawn Boss boss = new Boss(); game.addChild(boss); LK.playMusic('bossMusic'); // Boss music } } // --- Terrain Logic (Active in Level 1 Phase 0 and Level 2 except Area 4) --- var isArea4 = currentLevel === 2 && level2Progress >= LEVEL2_AREA3_END_PX; if ((gamePhase === 0 || currentLevel === 2) && !isArea4) { // Update and manage terrain segments var terrainWidth = LK.getAsset('terrain', {}).width; for (var i = terrainSegmentsTop.length - 1; i >= 0; i--) { var segment = terrainSegmentsTop[i]; if (segment.x < -terrainWidth) { // Reposition segment to the right var maxX = 0; terrainSegmentsTop.forEach(function (s) { if (s.x > maxX) { maxX = s.x; } }); segment.x = maxX + terrainWidth; // Always spawn an enemy on the reused segment spawnGroundEnemy(segment); // Additionally, 50% chance for a mountain if (Math.random() < 0.5) { spawnMountain(segment); } } } // Repeat for bottom terrain for (var i = terrainSegmentsBottom.length - 1; i >= 0; i--) { var segment = terrainSegmentsBottom[i]; if (segment.x < -terrainWidth) { var maxX = 0; terrainSegmentsBottom.forEach(function (s) { if (s.x > maxX) { maxX = s.x; } }); segment.x = maxX + terrainWidth; // Always spawn an enemy on the reused segment spawnGroundEnemy(segment); // Additionally, 50% chance for a mountain if (Math.random() < 0.5) { spawnMountain(segment); } } } // Update mountains & check collision for (var i = mountains.length - 1; i >= 0; i--) { var mountain = mountains[i]; if (mountain.x < -mountain.width) { mountain.destroy(); mountains.splice(i, 1); } else { if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(mountain)) { handlePlayerDeath(); return; // Stop update processing } } } // Update ground enemies & check collision for (var i = groundEnemies.length - 1; i >= 0; i--) { var enemy = groundEnemies[i]; if (enemy.x < -enemy.width) { enemy.destroy(); groundEnemies.splice(i, 1); } else { // Collision check: Player vs Ground Enemy if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(enemy)) { LK.getSound('enemyExplosion').play(); //{3L} // Enemy explodes enemy.destroy(); // Destroy enemy on collision too groundEnemies.splice(i, 1); handlePlayerDeath(); return; } } } // Re-clamp player position based on potentially moving terrain (simple clamp for now) player.clampPosition(); } // --- Phase 1/2: Air Enemy Logic --- if (gamePhase === 1) { // Add more air enemies periodically? if (LK.ticks % 180 === 0 && airEnemies.length < 10) { // Spawn if less than 10, every 3 seconds spawnAirEnemy(); } // Update air enemies & check collision for (var i = airEnemies.length - 1; i >= 0; i--) { var enemy = airEnemies[i]; // Air enemies handle their own off-screen logic (respawn) in their update // Collision check: Player vs Air Enemy if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(enemy)) { LK.getSound('enemyExplosion').play(); //{41} // Enemy explodes enemy.destroy(); // Destroy enemy on collision airEnemies.splice(i, 1); // LK.setScore(LK.getScore() + 50); // Score for destroying enemy - player died, maybe no score for this? Or keep it. Let's keep for now. // scoreTxt.setText(LK.getScore()); handlePlayerDeath(); return; } } } // --- Phase 3: Boss Logic --- if (gamePhase === 2 && boss) { // Check collision: Player vs Boss if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(boss)) { // Don't destroy boss on collision handlePlayerDeath(); return; } // Update boss lasers & check collision for (var i = bossLasers.length - 1; i >= 0; i--) { var laser = bossLasers[i]; // Check if laser is off-screen if (laser.x < -laser.width || laser.x > 2048 + laser.width || laser.y < -laser.height || laser.y > 2732 + laser.height) { laser.destroy(); bossLasers.splice(i, 1); } else { // Collision check: Player vs Boss Laser if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(laser)) { laser.destroy(); // Destroy laser bossLasers.splice(i, 1); handlePlayerDeath(); return; } } } // Note: Boss defeat condition is now handled within the player bullet collision check loop. } // --- Update Enemy Bullets & Check Collision (All Phases) --- for (var i = enemyBullets.length - 1; i >= 0; i--) { var bullet = enemyBullets[i]; // Check if bullet is off-screen if (bullet.y < -bullet.height || bullet.y > 2732 + bullet.height) { bullet.destroy(); enemyBullets.splice(i, 1); } else { // Collision check: Player vs Enemy Bullet if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(bullet)) { bullet.destroy(); // Destroy bullet enemyBullets.splice(i, 1); handlePlayerDeath(); // This function now handles the delay and sound return; // Stop update processing } } } // The saveHighscore function has been moved before handlePlayerDeath to fix the undefined error // --- Update Player Bullets & Check Collision --- for (var i = playerBullets.length - 1; i >= 0; i--) { var bullet = playerBullets[i]; // Check if bullet is off-screen (right side) if (bullet.x > 2048 + bullet.width) { bullet.destroy(); playerBullets.splice(i, 1); continue; // Skip collision checks if off-screen } // Collision Check: Player Bullet vs Ground Enemy (Phase 0) if (gamePhase === 0) { for (var j = groundEnemies.length - 1; j >= 0; j--) { var enemy = groundEnemies[j]; if (bullet.intersects(enemy)) { LK.getSound('enemyExplosion').play(); LK.effects.flashObject(enemy, 0xFFFFFF, 100); // Flash enemy white enemy.destroy(); groundEnemies.splice(j, 1); bullet.destroy(); playerBullets.splice(i, 1); LK.setScore(LK.getScore() + 100); // Score for destroying ground enemy scoreTxt.setText(LK.getScore()); break; // Bullet can only hit one enemy } } if (!bullet.exists) { continue; } // Check if bullet was destroyed in previous loop } // Collision Check: Player Bullet vs Air Enemy (Phase 1) if (gamePhase === 1) { for (var j = airEnemies.length - 1; j >= 0; j--) { var enemy = airEnemies[j]; if (bullet.intersects(enemy)) { LK.getSound('enemyExplosion').play(); LK.effects.flashObject(enemy, 0xFFFFFF, 100); // Flash enemy white enemy.destroy(); airEnemies.splice(j, 1); bullet.destroy(); playerBullets.splice(i, 1); LK.setScore(LK.getScore() + 150); // Score for destroying air enemy scoreTxt.setText(LK.getScore()); break; // Bullet can only hit one enemy } } if (!bullet.exists) { continue; } // Check if bullet was destroyed in previous loop } // Collision Check: Player Bullet vs Boss (Phase 2) if (gamePhase === 2 && boss) { if (bullet.intersects(boss)) { LK.getSound('enemyExplosion').play(); // Use enemy explosion for hit sound LK.effects.flashObject(boss, 0xFFFFFF, 100); // Flash boss white bullet.destroy(); playerBullets.splice(i, 1); boss.health -= 1; // Decrease boss health LK.setScore(LK.getScore() + 10); // Small score for hitting boss scoreTxt.setText(LK.getScore()); // Boss defeat check is now inside the bullet loop if (boss.health <= 0) { LK.getSound('bossExplosion').play(); // Play boss explosion sound boss.destroy(); boss = null; // Cleanup remaining lasers bossLasers.forEach(function (l) { return l.destroy(); }); bossLasers = []; LK.setScore(LK.getScore() + 5000); // Big score bonus scoreTxt.setText(LK.getScore()); // Save highscore as a milestone for defeating the boss saveHighscore(); //{be} // Argument removed // Transition to Level 2 currentLevel = 2; level2Started = false; // Flag to trigger initLevel2() once at the start of next update cycle // Note: Removed createScoreBackground(); call here as game transitions to Level 2 LK.setScore(LK.getScore() + 5000); // Bonus for defeating boss (applied again, consider if this is intended or consolidate) scoreTxt.setText(LK.getScore()); // saveHighscore(LK.getScore()); // Score is already saved above, this might be redundant unless intended for separate tracking // Clear boss specific elements if (boss && !boss.destroyed) boss.destroy(); boss = null; bossLasers.forEach(function (l) { if (l && !l.destroyed) l.destroy(); }); bossLasers = []; console.log("Boss defeated! Preparing Level 2."); // initLevel2() will be called in the next game.update() loop at the top return; // Stop update processing this frame to allow initLevel2 to run cleanly } break; // Bullet is destroyed after hitting boss } } // --- Level 2 Logic --- if (currentLevel === 2 && level2Started) { var LEVEL2_AREA1_END_PX = LEVEL2_AREA1_END_MILES * PIXELS_PER_MILE; var LEVEL2_AREA2_END_PX = (LEVEL2_AREA1_END_MILES + LEVEL2_AREA2_DURATION_MILES) * PIXELS_PER_MILE; var LEVEL2_AREA3_END_PX = (LEVEL2_AREA1_END_MILES + LEVEL2_AREA2_DURATION_MILES + LEVEL2_AREA3_DURATION_MILES) * PIXELS_PER_MILE; var LEVEL2_FINAL_END_PX = (LEVEL2_AREA1_END_MILES + LEVEL2_AREA2_DURATION_MILES + LEVEL2_AREA3_DURATION_MILES + LEVEL2_AREA4_DURATION_MILES) * PIXELS_PER_MILE; // --- Level 2, Area 1 (Vertical Walls) --- if (level2Progress < LEVEL2_AREA1_END_PX) { if (level2Progress > lastWallSpawnProgress + WALL_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) { lastWallSpawnProgress = level2Progress; var wall = new VerticalWall(2048 + 200); // Spawn off-screen right game.addChild(wall); verticalWalls.push(wall); } } // --- Level 2, Area 2 (Diagonal Enemies, Tanks) --- else if (level2Progress < LEVEL2_AREA2_END_PX) { // Clear any remaining walls from Area 1 when transitioning if (verticalWalls.length > 0 && level2Progress >= LEVEL2_AREA1_END_PX && level2Progress < LEVEL2_AREA1_END_PX + terrainSpeed * 2) { verticalWalls.forEach(function (w) { if (w && !w.isDestroyed) { w.turrets.forEach(function (t) { if (t && !t.isDestroyed) t.destroy(); }); w.destroy(); } }); verticalWalls = []; } if (level2Progress > lastDiagonalEnemySpawnProgress + DIAGONAL_ENEMY_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) { lastDiagonalEnemySpawnProgress = level2Progress; var diagEnemy = new DiagonalEnemy(2048 + 150, 2732 / 2); game.addChild(diagEnemy); diagonalEnemies.push(diagEnemy); } if (level2Progress > lastTankSpawnProgress + TANK_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) { lastTankSpawnProgress = level2Progress; var tankTop = new Tank(true, 2048 + 100); var tankBottom = new Tank(false, 2048 + 100); game.addChild(tankTop); game.addChild(tankBottom); tanks.push(tankTop); tanks.push(tankBottom); } } // --- Level 2, Area 3 (Serpent Formations) --- else if (level2Progress < LEVEL2_FINAL_END_PX) { // Clear any remaining Diagonal Enemies and Tanks from Area 2 if ((diagonalEnemies.length > 0 || tanks.length > 0) && level2Progress >= LEVEL2_AREA2_END_PX && level2Progress < LEVEL2_AREA2_END_PX + terrainSpeed * 2) { diagonalEnemies.forEach(function (de) { if (de && !de.isDestroyed) de.destroy(); }); diagonalEnemies = []; tanks.forEach(function (t) { if (t && !t.isDestroyed) t.destroy(); }); tanks = []; } // Spawn Serpent Formations if (serpentFormations.length < MAX_SERPENT_FORMATIONS && level2Progress > lastSerpentSpawnProgress + SERPENT_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) { lastSerpentSpawnProgress = level2Progress; var newSerpent = new SerpentFormation(); game.addChild(newSerpent); // The formation is a Container serpentFormations.push(newSerpent); } } // --- Level 2, Area 4 (Small Terrains with Moving Tanks) --- else if (level2Progress < LEVEL2_FINAL_END_PX + LEVEL2_AREA4_DURATION_MILES * PIXELS_PER_MILE) { // Clear serpent formations from Area 3 when transitioning to Area 4 if (serpentFormations.length > 0 && level2Progress >= LEVEL2_FINAL_END_PX && level2Progress < LEVEL2_FINAL_END_PX + terrainSpeed * 2) { serpentFormations.forEach(function (sf) { if (sf && !sf.isDestroyed()) sf.destroy(); }); serpentFormations = []; // Clear main terrain since area4 has no ground terrainSegmentsTop.forEach(function (s) { return s.destroy(); }); terrainSegmentsBottom.forEach(function (s) { return s.destroy(); }); terrainSegmentsTop = []; terrainSegmentsBottom = []; } // Spawn Small Terrains with tanks if (level2Progress > lastSmallTerrainSpawnProgress + SMALL_TERRAIN_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) { lastSmallTerrainSpawnProgress = level2Progress; var topTerrain = new SmallTerrain(true, 2048 + 200); var bottomTerrain = new SmallTerrain(false, 2048 + 200); game.addChild(topTerrain); game.addChild(bottomTerrain); smallTerrains.push(topTerrain); smallTerrains.push(bottomTerrain); // Add tanks to the terrains topTerrain.addTank(); bottomTerrain.addTank(); } } // --- Level 2 Completed --- else { if (player && !player.isDead) { // Ensure win condition only if player alive console.log("Level 2 Complete!"); // Clean up any remaining serpent formations serpentFormations.forEach(function (sf) { if (sf && !sf.isDestroyed()) sf.destroy(); }); serpentFormations = []; saveHighscore(); //{bI} // Argument removed createScoreBackground(); LK.setTimeout(function () { LK.showYouWin(); // Game ends after Level 2 (Area 3) }, 500); player.isDead = true; // Prevent further actions return; } } // Update and Collision for Level 2 Elements // Vertical Walls (and their turrets) for (var i = verticalWalls.length - 1; i >= 0; i--) { var wall = verticalWalls[i]; if (wall.isDestroyed) { // Already handled by wall itself // verticalWalls.splice(i, 1); // Wall removes itself from array continue; } if (wall.x < -wall.width) { // Off-screen left wall.turrets.forEach(function (t) { if (t && !t.isDestroyed) t.destroy(); }); wall.destroy(); verticalWalls.splice(i, 1); } else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(wall)) { handlePlayerDeath(); return; } } // Turrets are updated via their own class or Wall. Bullets handled by main enemyBullets loop. // Diagonal Enemies for (var i = diagonalEnemies.length - 1; i >= 0; i--) { var diagEnemy = diagonalEnemies[i]; if (diagEnemy.isDestroyed) { // diagonalEnemies.splice(i, 1); // Enemy removes itself continue; } if (diagEnemy.x < -diagEnemy.width) { diagEnemy.destroy(); diagonalEnemies.splice(i, 1); } else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(diagEnemy)) { diagEnemy.takeDamage(); handlePlayerDeath(); return; } } // Tanks for (var i = tanks.length - 1; i >= 0; i--) { var tank = tanks[i]; if (tank.isDestroyed) { // tanks.splice(i, 1); // Enemy removes itself continue; } if (tank.x < -tank.width) { tank.destroy(); tanks.splice(i, 1); } else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(tank)) { tank.takeDamage(); handlePlayerDeath(); return; } } // Small Terrains (Area 4) for (var i = smallTerrains.length - 1; i >= 0; i--) { var smallTerrain = smallTerrains[i]; if (smallTerrain.isDestroyed) { continue; } if (smallTerrain.x < -smallTerrain.width) { smallTerrain.destroy(); smallTerrains.splice(i, 1); } else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(smallTerrain)) { handlePlayerDeath(); return; } } // Update Serpent Formations (Area 3 specific, but update loop runs for all formations) for (var i = serpentFormations.length - 1; i >= 0; i--) { var serpent = serpentFormations[i]; // serpent.update(); // Already called because it's a child of game if (serpent.isDone()) { serpent.destroy(); serpentFormations.splice(i, 1); } else { // Player collision with individual serpent ships if (!player.isDead && LK.ticks > player.invincibleUntil) { for (var j = serpent.ships.length - 1; j >= 0; j--) { var ship = serpent.ships[j]; if (!ship.isDestroyed) { // Need to get global position of the ship for intersection check var shipGlobalPos = serpent.toGlobal(ship.position); var shipGameAreaRect = new Rectangle(shipGlobalPos.x - ship.width / 2, shipGlobalPos.y - ship.height / 2, ship.width, ship.height); // Create a temporary object for player bounds for intersects method if player itself isn't a simple rectangle var playerBounds = new Rectangle(player.x - player.width / 2, player.y - player.height / 2, player.width, player.height); // AABB intersection check if (playerBounds.x < shipGameAreaRect.x + shipGameAreaRect.width && playerBounds.x + playerBounds.width > shipGameAreaRect.x && playerBounds.y < shipGameAreaRect.y + shipGameAreaRect.height && playerBounds.y + playerBounds.height > shipGameAreaRect.y) { ship.takeDamage(); // Serpent ship takes damage handlePlayerDeath(); // Player also dies return; // Stop further processing this frame } } } } } } } // End if (currentLevel === 2 && level2Started) // --- Update Player Bullets & Check Collision --- (Continued with Level 2 targets) // (This section is appended to the existing playerBullets loop, before the final closing brace of the loop) // The existing player bullet loop needs to be modified to include these checks. // This block should be inserted *inside* the `for (var i = playerBullets.length - 1; i >= 0; i--)` loop for player bullets, // after existing collision checks or as part of a conditional block for `currentLevel === 2`. // Existing player bullet loop: // for (var i = playerBullets.length - 1; i >= 0; i--) { // var bullet = playerBullets[i]; // ... // if (gamePhase === 2 && boss) { ... } // Existing boss collision // // ADD LEVEL 2 COLLISION LOGIC HERE: if (currentLevel === 2 && bullet.exists) { // Check bullet.exists in case prior collision destroyed it // Vs WallTurrets (part of VerticalWalls) for (var k = verticalWalls.length - 1; k >= 0; k--) { var wall = verticalWalls[k]; if (wall.isDestroyed) continue; for (var l = wall.turrets.length - 1; l >= 0; l--) { var turret = wall.turrets[l]; if (turret.isDestroyed) continue; if (bullet.intersects(turret)) { if (turret.takeDamage()) { // true if destroyed LK.setScore(LK.getScore() + 75); scoreTxt.setText(LK.getScore()); } bullet.destroy(); playerBullets.splice(i, 1); break; } } if (!bullet.exists) break; } if (!bullet.exists) continue; // To next player bullet if this one was destroyed // Vs DiagonalEnemies for (var k = diagonalEnemies.length - 1; k >= 0; k--) { var diagEnemy = diagonalEnemies[k]; if (diagEnemy.isDestroyed) continue; if (bullet.intersects(diagEnemy)) { if (diagEnemy.takeDamage()) { LK.setScore(LK.getScore() + 250); scoreTxt.setText(LK.getScore()); } bullet.destroy(); playerBullets.splice(i, 1); break; } } if (!bullet.exists) continue; // Vs Tanks for (var k = tanks.length - 1; k >= 0; k--) { var tank = tanks[k]; if (tank.isDestroyed) continue; if (bullet.intersects(tank)) { if (tank.takeDamage()) { LK.setScore(LK.getScore() + 175); scoreTxt.setText(LK.getScore()); } bullet.destroy(); playerBullets.splice(i, 1); break; } } if (!bullet.exists) continue; // Vs SerpentEnemyShips (within SerpentFormations) if (bullet.exists) { // Check again in case previous collision destroyed it for (var k = serpentFormations.length - 1; k >= 0; k--) { var formation = serpentFormations[k]; if (formation.isDestroyed()) continue; for (var l = formation.ships.length - 1; l >= 0; l--) { var serpentShip = formation.ships[l]; if (serpentShip.isDestroyed) continue; // Get global position of the serpent ship for accurate intersection var shipGlobalPos = formation.toGlobal(serpentShip.position); // Create a temporary rectangle for the serpent ship in game coordinates // Assuming serpentShip.width and serpentShip.height are available (from its graphic) var serpentShipGameRect = new Rectangle(shipGlobalPos.x - serpentShip.width / 2, shipGlobalPos.y - serpentShip.height / 2, serpentShip.width, serpentShip.height); // Create a temporary rectangle for the bullet var bulletRect = new Rectangle(bullet.x - bullet.width / 2, bullet.y - bullet.height / 2, bullet.width, bullet.height); // Manual AABB intersection check if (bulletRect.x < serpentShipGameRect.x + serpentShipGameRect.width && bulletRect.x + bulletRect.width > serpentShipGameRect.x && bulletRect.y < serpentShipGameRect.y + serpentShipGameRect.height && bulletRect.y + bulletRect.height > serpentShipGameRect.y) { if (serpentShip.takeDamage()) { // true if destroyed LK.setScore(LK.getScore() + 200); // Score for serpent ship scoreTxt.setText(LK.getScore()); } bullet.destroy(); playerBullets.splice(i, 1); break; // Bullet hits one serpent ship } } if (!bullet.exists) break; // If bullet was destroyed, move to next player bullet } } if (!bullet.exists) continue; // To next player bullet } // } // End of playerBullets loop (this closing brace is illustrative, don't add a new one) } // End of playerBullets loop };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highscoresData: "[]"
});
/****
* Classes
****/
// Air Enemy Class (Snake Segment - Simplified for MVP)
var AirEnemy = Container.expand(function (startX, startY) {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('airEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = startX;
self.y = startY;
self.speedX = 4 + Math.random() * 2; // Horizontal speed
self.amplitudeY = 100 + Math.random() * 100; // Vertical movement range
self.frequencyY = 0.01 + Math.random() * 0.01; // Vertical movement speed
self.fireRate = 150; // Ticks between shots
self.fireCooldown = Math.random() * self.fireRate;
self.update = function () {
// Basic horizontal and sinusoidal vertical movement
self.x -= self.speedX;
self.y = startY + Math.sin(LK.ticks * self.frequencyY + startX) * self.amplitudeY; // Use startX for phase offset
// Firing logic
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate) {
self.fireCooldown = 0;
// Fire a single bullet straight left
var bulletSpeed = 8;
fireEnemyBullet(self.x, self.y, -bulletSpeed, 0); // vx = -speed, vy = 0
}
// Boundary check (simple respawn logic)
if (self.x < -enemyGraphics.width) {
self.x = 2048 + enemyGraphics.width; // Respawn on the right
startY = Math.random() * (2732 - 400) + 200; // Random Y position
}
};
return self;
});
// Boss Class (Basic structure for future)
var Boss = Container.expand(function () {
var self = Container.call(this);
var bossGraphics = self.attachAsset('boss', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = 2048 - 300; // Position on the right
self.y = 2732 / 2; // Center vertically
self.fireRate = 180; // Ticks between laser bursts
self.fireCooldown = 0;
self.health = 100; // Example health
self.update = function () {
// Add movement logic if needed
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate) {
self.fireCooldown = 0;
fireBossLasers(self.x, self.y);
}
};
return self;
});
// Boss Laser Class (Basic structure for future)
var BossLaser = Container.expand(function (startX, startY, vx, vy) {
var self = Container.call(this);
var laserGraphics = self.attachAsset('bossLaser', {
anchorX: 0.5,
anchorY: 0.5 // Anchor center
});
self.x = startX;
self.y = startY;
self.vx = vx;
self.vy = vy;
// Set rotation based on velocity direction
self.rotation = Math.atan2(vy, vx) + Math.PI / 2; // Point laser in direction of travel (+90deg adjustment needed depending on asset orientation)
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self;
});
// Bottom Mountain Obstacle Class
var BottomMountain = Container.expand(function () {
var self = Container.call(this);
var mountainGraphics = self.attachAsset('bottommountain', {
anchorX: 0.5,
anchorY: 0.75 // Anchor base at terrain edge
});
self.speed = 5; // Should match terrain speed
self.update = function () {
self.x -= self.speed;
};
return self;
});
// DiagonalEnemy Class for Level 2
var DiagonalEnemy = Container.expand(function (xPos, yPos) {
var self = Container.call(this);
var graphics = self.attachAsset('diagonalEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = Math.PI / 4;
self.x = xPos;
self.y = yPos;
self.speed = terrainSpeed + 1; // Slightly faster than terrain
self.health = 4;
self.fireRate = 160;
self.fireCooldown = Math.random() * self.fireRate;
self.isDestroyed = false;
self.update = function () {
if (self.isDestroyed) return;
self.x -= self.speed;
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate) {
self.fireCooldown = 0;
var bulletSpeed = 7;
fireEnemyBullet(self.x, self.y, bulletSpeed, 0, 'enemyBulletBlue'); // Right
fireEnemyBullet(self.x, self.y, -bulletSpeed, 0, 'enemyBulletBlue'); // Left
fireEnemyBullet(self.x, self.y, 0, bulletSpeed, 'enemyBulletBlue'); // Down
fireEnemyBullet(self.x, self.y, 0, -bulletSpeed, 'enemyBulletBlue'); // Up
}
};
self.takeDamage = function () {
if (self.isDestroyed) return false;
self.health--;
LK.effects.flashObject(self, 0xFFFFFF, 100);
if (self.health <= 0) {
self.isDestroyed = true;
LK.getSound('enemyExplosion').play();
var index = diagonalEnemies.indexOf(self);
if (index > -1) {
diagonalEnemies.splice(index, 1);
}
self.destroy();
return true;
}
return false;
};
return self;
});
// Enemy Bullet Class
var EnemyBullet = Container.expand(function (startX, startY, vx, vy, assetId) {
var self = Container.call(this);
// Added assetId
var bulletGraphics = self.attachAsset(assetId || 'enemyBullet', {
// Use assetId or default
anchorX: 0.5,
anchorY: 0.5
});
self.x = startX;
self.y = startY;
self.vx = vx; // Horizontal velocity
self.vy = vy; // Vertical velocity
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self;
});
// Ground Enemy Class
var GroundEnemy = Container.expand(function (isTop) {
var self = Container.call(this);
var enemyGraphics = self.attachAsset(isTop ? 'groundEnemytop' : 'groundEnemybottom', {
anchorX: 0.5,
anchorY: isTop ? 0 : 1 // Anchor base at terrain edge
});
self.isTop = isTop;
self.speed = 5; // Should match terrain speed
self.fireRate = 120; // Ticks between firing sequences (2 seconds)
self.fireCooldown = Math.random() * self.fireRate; // Random initial delay
self.shotsInBurst = 3;
self.burstDelay = 10; // Ticks between shots in a burst
self.update = function () {
self.x -= self.speed;
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate) {
self.fireCooldown = 0; // Reset cooldown
// Fire burst
for (var i = 0; i < self.shotsInBurst; i++) {
LK.setTimeout(function () {
// Check if enemy still exists before firing
if (!self.destroyed) {
var bulletSpeed = 8;
// Calculate direction logic is now in fireEnemyBullet, but still need to pass initial values
var vx = -bulletSpeed * 0.707; // Default direction if player not available
var vy = self.isTop ? bulletSpeed * 0.707 : -bulletSpeed * 0.707; // Default direction
fireEnemyBullet(self.x, self.y, vx, vy);
}
}, i * self.burstDelay);
}
}
};
return self;
});
// Player Bullet Class
var PlayerBullet = Container.expand(function (startX, startY) {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('playerBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = startX;
self.y = startY;
self.speed = 20; // Moves to the right
self.update = function () {
self.x += self.speed;
};
return self;
});
var SerpentEnemyShip = Container.expand(function (formation, relX, relY) {
var self = Container.call(this);
var graphics = self.attachAsset('serpentEnemyShip', {
// Or 'airEnemy' if using that asset ID directly
anchorX: 0.5,
anchorY: 0.5
});
self.parentFormation = formation;
self.x = relX; // Relative X to formation anchor
self.y = relY; // Relative Y to formation anchor
self.health = 2;
self.fireRate = 140; // Slightly slower than some other enemies
self.fireCooldown = Math.random() * self.fireRate;
self.isDestroyed = false;
// update() is not strictly needed here if all positioning is handled by parentFormation setting self.x/y
// However, firing logic will be here.
self.updateShipLogic = function () {
if (self.isDestroyed || !self.parentFormation || self.parentFormation.isDestroyed) {
if (!self.isDestroyed) self.destroy();
return;
}
// Firing logic
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate && player && !player.isDead) {
self.fireCooldown = 0;
// Calculate global position for bullet spawn
var globalPos = self.parentFormation.toGlobal(self.position);
var gamePos = game.toLocal(globalPos);
var bulletSpeed = 7;
// Basic targeting or straight shot
var vx = -bulletSpeed;
var vy = 0;
if (player && !player.isDead) {
var dx = player.x - gamePos.x;
var dy = player.y - gamePos.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
vx = dx / dist * bulletSpeed;
vy = dy / dist * bulletSpeed;
}
}
fireEnemyBullet(gamePos.x, gamePos.y, vx, vy, 'enemyBulletBlue');
}
};
self.takeDamage = function () {
if (self.isDestroyed) return false;
self.health--;
LK.effects.flashObject(self, 0xFFFFFF, 100);
if (self.health <= 0) {
self.isDestroyed = true;
LK.getSound('enemyExplosion').play();
if (self.parentFormation) {
self.parentFormation.shipDestroyed(self);
}
self.destroy();
return true;
}
return false;
};
return self;
});
var SerpentFormation = Container.expand(function () {
var self = Container.call(this);
self.ships = [];
self.stage = 0; // 0: approach, 1: retreat, 2: pass, 3: offscreen/done
self.anchorX = 2048 + 400; // Initial X off-screen right
self.anchorY = 2732 / 2 + (Math.random() * 600 - 300); // Initial Y with some variance
self.x = self.anchorX;
self.y = self.anchorY;
self.targetAnchorX = 0;
self.targetAnchorY = 0;
self.movementSpeed = 6;
self.retreatDirection = Math.random() < 0.5 ? 1 : -1; // 1 for bottom-right, -1 for top-right
var APPROACH_TARGET_X = 1024;
var RETREAT_DELTA_X = 500;
var RETREAT_DELTA_Y = 700;
var PASS_TARGET_X = -600; // Further off-screen left
var SERPENT_SHIP_OFFSETS = [{
x: 0,
y: 0
}, {
x: -70,
y: 35
}, {
x: -140,
y: -35
}, {
x: -210,
y: 70
}, {
x: -280,
y: -70
}, {
x: -350,
y: 105
}, {
x: -420,
y: -105
}];
self.initializeShips = function () {
for (var i = 0; i < 7; i++) {
var offset = SERPENT_SHIP_OFFSETS[i];
var ship = new SerpentEnemyShip(self, offset.x, offset.y);
self.addChild(ship); // Add ship as child of the formation container
self.ships.push(ship);
}
};
self.setMovementStage = function (newStage) {
self.stage = newStage;
switch (self.stage) {
case 0:
// Approach
self.targetAnchorX = APPROACH_TARGET_X;
self.targetAnchorY = self.anchorY; // Maintain initial Y for approach
break;
case 1:
// Diagonal Retreat
self.targetAnchorX = self.anchorX + RETREAT_DELTA_X;
self.targetAnchorY = self.anchorY + self.retreatDirection * RETREAT_DELTA_Y;
break;
case 2:
// Forward Pass
self.targetAnchorX = PASS_TARGET_X;
// self.targetAnchorY remains from retreat or can be adjusted
break;
case 3:
// Done
// No target, just indicates it's finished its path
break;
}
};
self.update = function () {
if (self.stage === 3) return; // Done moving
var dx = self.targetAnchorX - self.x;
var dy = self.targetAnchorY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.movementSpeed * 1.5) {
// Reached target for current stage
self.x = self.targetAnchorX;
self.y = self.targetAnchorY;
self.anchorX = self.x; // Update internal anchor tracking
self.anchorY = self.y;
self.setMovementStage(self.stage + 1);
} else {
self.x += dx / dist * self.movementSpeed;
self.y += dy / dist * self.movementSpeed;
self.anchorX = self.x;
self.anchorY = self.y;
}
// Update individual ships' logic (like firing)
for (var i = self.ships.length - 1; i >= 0; i--) {
var ship = self.ships[i];
if (ship.isDestroyed) {
// Ship already removed itself from parent (self) and called shipDestroyed
} else {
ship.updateShipLogic();
}
}
if (self.stage === 3 && self.x < PASS_TARGET_X + SERPENT_SHIP_OFFSETS[SERPENT_SHIP_OFFSETS.length - 1].x - 200) {
// Check if fully off-screen
// Handled by isDone check in main game loop
}
};
self.shipDestroyed = function (ship) {
var index = self.ships.indexOf(ship);
if (index > -1) {
self.ships.splice(index, 1);
}
// If the formation itself is a container, the ship's destroy method handles removing from parent.
};
self.isDone = function () {
// Formation is done if all ships are destroyed OR if it has completed its path and is off-screen
if (self.ships.length === 0) return true;
// Check based on the leader (first ship offset) being sufficiently off-screen
var leaderGlobalX = self.x + SERPENT_SHIP_OFFSETS[0].x;
if (self.stage === 3 && leaderGlobalX < PASS_TARGET_X + SERPENT_SHIP_OFFSETS[0].x - 100) {
// Check leader's x pos
return true;
}
return false;
};
// Initialize
self.initializeShips();
self.setMovementStage(0); // Start with approaching stage
return self;
});
// SmallTerrain Class for Level 2 Area 4
var SmallTerrain = Container.expand(function (isTop, initialX) {
var self = Container.call(this);
var graphics = self.attachAsset('smallTerrain', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = initialX;
self.y = isTop ? 400 + 50 : 2732 - 400 - 50; // 400px from edges, centered on terrain height
self.isTop = isTop;
self.speed = terrainSpeed;
self.tank = null; // Will hold the tank on this terrain
self.isDestroyed = false;
self.update = function () {
if (self.isDestroyed) return;
self.x -= self.speed;
};
self.addTank = function () {
if (!self.tank && !self.isDestroyed) {
self.tank = new Tank(self.isTop, self.x, self); // Pass terrain reference
game.addChild(self.tank);
tanks.push(self.tank);
}
};
self.destroy = function () {
if (self.tank && !self.tank.isDestroyed) {
self.tank.destroy();
}
self.isDestroyed = true;
Container.prototype.destroy.call(self);
};
return self;
});
// Tank Class for Level 2
var Tank = Container.expand(function (isTop, initialX, terrain) {
var self = Container.call(this);
var terrainHeight = terrain ? 100 : 200; // Use 100 for small terrains, 200 for regular
var graphics = self.attachAsset('tank', {
anchorX: 0.5,
anchorY: isTop ? 0 : 1
});
self.x = initialX;
self.y = terrain ? isTop ? 400 + 50 : 2732 - 400 - 50 : isTop ? terrainHeight : 2732 - terrainHeight;
self.isTop = isTop;
self.speed = terrainSpeed;
self.health = 6; // Tanks are sturdy
self.fireRate = 170;
self.fireCooldown = Math.random() * self.fireRate;
self.isDestroyed = false;
self.parentTerrain = terrain; // Reference to terrain for area4 tanks
self.horizontalSpeed = 3; // Speed for left-right movement on small terrains
self.movingRight = Math.random() < 0.5; // Random initial direction
self.update = function () {
if (self.isDestroyed) return;
if (self.parentTerrain) {
// Area4 tank: move horizontally on small terrain
var terrainLeft = self.parentTerrain.x - 150; // Half width of small terrain
var terrainRight = self.parentTerrain.x + 150;
if (self.movingRight) {
self.x += self.horizontalSpeed;
if (self.x >= terrainRight) {
self.movingRight = false;
}
} else {
self.x -= self.horizontalSpeed;
if (self.x <= terrainLeft) {
self.movingRight = true;
}
}
// Move with terrain
self.x -= self.speed;
} else {
// Regular area2 tank: just move with terrain
self.x -= self.speed;
}
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate) {
self.fireCooldown = 0;
var bulletSpeed = 8;
var vx = -bulletSpeed * 0.707; // Aim slightly forward
var vyDirection = self.isTop ? bulletSpeed * 0.707 : -bulletSpeed * 0.707;
fireEnemyBullet(self.x, self.y, vx, vyDirection, 'enemyBulletBlue');
}
};
self.takeDamage = function () {
if (self.isDestroyed) return false;
self.health--;
LK.effects.flashObject(self, 0xFFFFFF, 100);
if (self.health <= 0) {
self.isDestroyed = true;
LK.getSound('enemyExplosion').play();
var index = tanks.indexOf(self);
if (index > -1) {
tanks.splice(index, 1);
}
self.destroy();
return true;
}
return false;
};
return self;
});
// Terrain Segment Class
var TerrainSegment = Container.expand(function (isTop) {
var self = Container.call(this);
var terrainGraphics = self.attachAsset('terrain', {
anchorX: 0,
anchorY: isTop ? 0 : 1 // Anchor at top for top terrain, bottom for bottom terrain
});
self.isTop = isTop;
self.speed = 5; // Horizontal scroll speed
self.update = function () {
self.x -= self.speed;
};
return self;
});
// Top Mountain Obstacle Class
var TopMountain = Container.expand(function () {
var self = Container.call(this);
var mountainGraphics = self.attachAsset('topmountain', {
anchorX: 0.5,
anchorY: 0.3 // Anchor base at terrain edge
});
self.speed = 5; // Should match terrain speed
self.update = function () {
self.x -= self.speed;
};
return self;
});
// Player UFO Class
var UFO = Container.expand(function () {
var self = Container.call(this);
var ufoGraphics = self.attachAsset('ufo', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10; // Movement speed multiplier, adjust as needed
self.isDead = false;
self.invincibleUntil = 0;
// Keep UFO within game boundaries
self.clampPosition = function () {
var halfWidth = ufoGraphics.width / 2;
var halfHeight = ufoGraphics.height / 2;
if (self.x < halfWidth) {
self.x = halfWidth;
}
if (self.x > 2048 - halfWidth) {
self.x = 2048 - halfWidth;
}
if (self.y < halfHeight + 100) {
self.y = halfHeight + 100;
} // Avoid top-left menu area
if (self.y > 2732 - halfHeight) {
self.y = 2732 - halfHeight;
}
// Adjust based on terrain phase (Level 1 Phase 0 or if it's Level 2 which also has terrain)
if (gamePhase === 0 || currentLevel === 2) {
// Find the current terrain height at the UFO's x position (simplified)
var terrainHeight = 200; // Assuming constant terrain height for now
if (self.y < terrainHeight + halfHeight) {
self.y = terrainHeight + halfHeight;
}
if (self.y > 2732 - terrainHeight - halfHeight) {
self.y = 2732 - terrainHeight - halfHeight;
}
}
};
return self;
});
// VerticalWall Class for Level 2
var VerticalWall = Container.expand(function (xPos) {
var self = Container.call(this);
var wallGraphicAsset = LK.getAsset('verticalWall', {});
var graphics = self.attachAsset('verticalWall', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = wallGraphicAsset.width; // Store actual width for calculations
self.height = wallGraphicAsset.height; // Store actual height
self.x = xPos;
self.y = 2732 / 2;
self.speed = terrainSpeed;
self.turrets = [];
self.activeTurrets = 4;
self.isDestroyed = false;
// Wall graphics anchor is 0.5, 0.5.
// Turrets are positioned on the front-facing side of the wall.
// Spread 4 turrets along the height.
var turretPositions = [-0.35 * self.height, -0.15 * self.height, 0.15 * self.height, 0.35 * self.height]; // Relative Y from center
for (var i = 0; i < 4; i++) {
// Turrets are on the "front" (left edge for player) of the wall
var turret = new WallTurret(self, -0.4, turretPositions[i]); // relX as factor of half-width, relY absolute
self.turrets.push(turret);
game.addChild(turret);
}
self.update = function () {
if (self.isDestroyed) return;
self.x -= self.speed;
};
self.turretDestroyed = function (turret) {
if (self.isDestroyed) return;
self.activeTurrets--;
if (self.activeTurrets <= 0) {
self.isDestroyed = true;
LK.getSound('bossExplosion').play();
var wallIndex = verticalWalls.indexOf(self);
if (wallIndex > -1) {
verticalWalls.splice(wallIndex, 1);
}
// Ensure remaining turrets are cleaned up if any edge case
self.turrets.forEach(function (t) {
if (t && !t.isDestroyed) t.destroy();
});
self.destroy();
}
};
return self;
});
// WallTurret Class for Level 2
var WallTurret = Container.expand(function (wall, relX, relY) {
var self = Container.call(this);
var graphics = self.attachAsset('wallTurret', {
anchorX: 0.5,
anchorY: 0.5
});
self.parentWall = wall;
self.relX = relX;
self.relY = relY;
self.health = 2; // Turrets are a bit tougher
self.fireRate = 130; // Initial and current fire rate
self.minFireRate = 60; // Minimum fire rate (e.g., 60 ticks = 1 shot per second)
self.fireRateReductionAmount = 15; // Amount to reduce fireRate by each time
self.shotsFiredCount = 0; // Counter for total shots fired by this turret
self.shotsPerRateReduction = 3; // Reduce fire rate every this many shots
self.fireCooldown = Math.random() * self.fireRate;
self.isDestroyed = false;
self.update = function () {
if (self.isDestroyed || !self.parentWall || self.parentWall.isDestroyed) {
if (!self.isDestroyed) self.destroy(); // Self-cleanup if parent is gone
return;
}
// Update position relative to the moving wall
self.x = self.parentWall.x + self.relX * (self.parentWall.width / 2); // relX is factor of half-width
self.y = self.parentWall.y + self.relY;
self.fireCooldown++;
if (self.fireCooldown >= self.fireRate) {
self.fireCooldown = 0;
fireEnemyBullet(self.x, self.y, -9, 0, 'enemyBulletBlue'); // Shoots blue bullets left
self.shotsFiredCount++;
// Check if it's time to reduce the fire rate
if (self.shotsFiredCount > 0 && self.shotsFiredCount % self.shotsPerRateReduction === 0) {
if (self.fireRate > self.minFireRate) {
self.fireRate = Math.max(self.fireRate - self.fireRateReductionAmount, self.minFireRate);
// The fire cooldown will naturally be shorter for the next cycle due to the reduced self.fireRate
}
}
}
};
self.takeDamage = function () {
if (self.isDestroyed) return false;
self.health--;
LK.effects.flashObject(self, 0xFFFFFF, 100);
if (self.health <= 0) {
self.isDestroyed = true;
LK.getSound('enemyExplosion').play();
if (self.parentWall && !self.parentWall.isDestroyed) {
self.parentWall.turretDestroyed(self);
}
self.destroy();
return true;
}
return false;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x101030 // Dark space blue background
});
/****
* Game Code
****/
// Reusing airEnemy asset for serpent ship
// Placeholder for Level 2 music
// Deep Sky Blue
// Forest Green
// Dark Orange
// Grey turret
// Brownish wall
// Level 2 Assets
// Game State
/****
* Assets
* Assets are automatically created and loaded either dynamically during gameplay
* or via static code analysis based on their usage in the code.
****/
// Initialize assets used in this game. Scale them according to what is needed for the game.
// Player UFO
// Ground/Ceiling
// Mountain Obstacle (simplified)
// Ground Enemy
// Enemy Bullet
// Air Enemy (Placeholder shape)
// Boss
// Boss Laser Beam
// Minimalistic tween library which should be used for animations over time
// LK.init.image('mountain', {width:150, height:250, id:'6819884bc4a0c8bae9e84ae0'}) // Removed - Replaced by top/bottom mountains
var isGameStarted = false; // Flag to track if game has started
var showingHighscores = false; // Flag to track if highscore screen is shown
var gamePhase = 0; // 0: Terrain, 1: Space, 2: Boss
var phaseStartTime = LK.ticks;
var phaseDuration = 3600; // 1 minute (60 seconds * 60 fps = 3600 ticks)
var terrainSpeed = 5;
var scoreIncrementTimer = 0;
var highscoreBackground = null;
var highscoreTexts = []; // Original, may become vestigial or be fully removed depending on final showHighscore structure
var elementsShownByShowHighscore = []; // New global array for showHighscore's element management
// Game Objects
var player = null;
var terrainSegmentsTop = [];
var terrainSegmentsBottom = [];
var mountains = [];
var groundEnemies = [];
var enemyBullets = [];
var airEnemies = []; // Simple air enemies for MVP phase 2
var boss = null;
var bossLasers = [];
var playerBullets = []; // Array for player bullets
// Player Control
var dragNode = null;
var playerFireRate = 15; // Ticks between shots (4 shots per second)
var playerFireCooldown = 0;
// Level 2 State and Constants
var currentLevel = 1;
var level2Progress = 0; // In pixels, for current level 2 progression
var level2Started = false;
var lastWallSpawnProgress = 0;
var lastDiagonalEnemySpawnProgress = 0;
var lastTankSpawnProgress = 0;
var verticalWalls = [];
var diagonalEnemies = [];
var tanks = [];
// enemyBulletsBlue will use the existing enemyBullets array
var PIXELS_PER_MILE = 250; // Adjusted for gameplay feel
var LEVEL2_AREA1_END_MILES = 500;
var LEVEL2_AREA2_DURATION_MILES = 500; // Duration of Area 2
var WALL_SPAWN_INTERVAL_MILES = 50;
var DIAGONAL_ENEMY_SPAWN_INTERVAL_MILES = 50;
var TANK_SPAWN_INTERVAL_MILES = 10;
// Level 2 Area 3: Serpent Formations
var serpentFormations = [];
var MAX_SERPENT_FORMATIONS = 2; // Max 1-2 active at once
var lastSerpentSpawnProgress = 0; // Tracks level2Progress for serpent spawning
var SERPENT_SPAWN_INTERVAL_MILES = 100; // Spawn a new serpent formation every 100 miles in Area 3
var LEVEL2_AREA3_DURATION_MILES = 700; // Duration of Area 3
var LEVEL2_AREA4_DURATION_MILES = 500; // Duration of Area 4
var SMALL_TERRAIN_SPAWN_INTERVAL_MILES = 80; // Spawn interval for small terrains in area4
var lastSmallTerrainSpawnProgress = 0; // Tracks spawning of small terrains
var smallTerrains = []; // Array to track small terrain pieces
// Player Lives
var MAX_PLAYER_LIVES = 5;
var playerLives = MAX_PLAYER_LIVES;
var livesTxt;
// Score Display
var scoreTxt = new Text2('0', {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt); // Position score at top center
// Helper function to create terrain segments
function createTerrainSegment(isTop, xPos) {
var segment = new TerrainSegment(isTop);
segment.x = xPos;
segment.y = isTop ? 0 : 2732;
segment.speed = terrainSpeed;
game.addChild(segment);
if (isTop) {
terrainSegmentsTop.push(segment);
} else {
terrainSegmentsBottom.push(segment);
}
return segment;
}
// Helper function to spawn mountains on terrain
function spawnMountain(terrainSegment) {
var mountain;
if (terrainSegment.isTop) {
mountain = new TopMountain();
} else {
mountain = new BottomMountain();
}
// Position relative to the terrain segment
// Ensure we use the actual height of the terrain graphic for positioning
var terrainGraphic = terrainSegment.children[0]; // Assuming the graphic is the first child
mountain.x = terrainSegment.x + Math.random() * terrainGraphic.width;
// Position based on whether it's a top or bottom mountain
mountain.y = terrainSegment.isTop ? terrainGraphic.height : 2732 - terrainGraphic.height;
mountain.speed = terrainSpeed;
game.addChild(mountain);
mountains.push(mountain);
}
// Helper function to spawn ground enemies on terrain
function spawnGroundEnemy(terrainSegment) {
var enemy = new GroundEnemy(terrainSegment.isTop);
// Position relative to the terrain segment
// Ensure we use the actual height of the terrain graphic for positioning
var terrainGraphic = terrainSegment.children[0]; // Assuming the graphic is the first child
enemy.x = terrainSegment.x + Math.random() * terrainGraphic.width;
enemy.y = terrainSegment.isTop ? terrainGraphic.height : 2732 - terrainGraphic.height;
enemy.speed = terrainSpeed;
game.addChild(enemy);
groundEnemies.push(enemy);
}
// Helper function to fire enemy bullets
function fireEnemyBullet(x, y, vx, vy, assetId) {
// Added assetId parameter
// If player exists and is not dead, target the player instead of using fixed direction
if (player && !player.isDead) {
// Calculate direction vector to player
var dx = player.x - x;
var dy = player.y - y;
// Normalize the vector
var distance = Math.sqrt(dx * dx + dy * dy);
// Use the bullet speed provided (or calculate from vx and vy if assetId is for styling only)
// For simplicity, let's assume the passed vx,vy define the base speed.
var baseSpeed = Math.sqrt(vx * vx + vy * vy);
if (distance > 0 && baseSpeed > 0) {
// Ensure distance and baseSpeed are valid
vx = dx / distance * baseSpeed;
vy = dy / distance * baseSpeed;
}
// If player is not available or too close, use original vx, vy
}
var bullet = new EnemyBullet(x, y, vx, vy, assetId); // Pass assetId to constructor
game.addChild(bullet);
enemyBullets.push(bullet);
LK.getSound('enemyShoot').play();
}
// Helper function to spawn air enemies (simple version)
function spawnAirEnemy() {
var startY = Math.random() * (2732 - 400) + 200; // Avoid edges
var enemy = new AirEnemy(2048 + 100, startY); // Start off-screen right
game.addChild(enemy);
airEnemies.push(enemy);
}
// Helper function to fire boss lasers
function fireBossLasers(x, y) {
var directions = 5;
var laserSpeed = 12;
var verticalSpread = 4; // Max vertical speed component
for (var i = 0; i < directions; i++) {
// Calculate vertical velocity component for spread
// Example: i=0 -> -4, i=1 -> -2, i=2 -> 0, i=3 -> 2, i=4 -> 4
var vy = -verticalSpread + verticalSpread * 2 / (directions - 1) * i;
// Keep horizontal speed constant (moving left)
var vx = -laserSpeed;
var laser = new BossLaser(x, y, vx, vy);
game.addChild(laser);
bossLasers.push(laser);
}
// Add sound effect for laser fire (consider adding one e.g., LK.getSound('bossShoot').play(); if asset exists)
}
// Initialize Game Elements
function initGame() {
// Reset state
LK.setScore(0);
scoreTxt.setText('0');
// Hide score during intro
scoreTxt.visible = !isGameStarted;
// Initialize Lives
playerLives = MAX_PLAYER_LIVES;
if (!livesTxt) {
// Create only if it doesn't exist (for game restarts)
livesTxt = new Text2("Lives: x" + playerLives, {
size: 80,
// Slightly smaller than score
fill: 0xFFFFFF
});
livesTxt.anchor.set(1, 0); // Anchor top-right
LK.gui.topRight.addChild(livesTxt);
} else {
livesTxt.setText("Lives: x" + playerLives);
}
gamePhase = 0;
phaseStartTime = LK.ticks;
dragNode = null;
playerBullets = []; // Clear player bullets
playerFireCooldown = 0; // Reset fire cooldown
// Clear existing elements from previous game (if any)
// Note: LK engine handles full reset on GameOver/YouWin, but manual cleanup might be needed if restarting mid-game (not typical)
// Let's assume full reset is handled by LK.
// Create Player
player = new UFO();
player.x = 300;
player.y = 2732 / 2;
player.isDead = false;
player.invincibleUntil = 0;
player.alpha = 1; // Ensure player is visible
game.addChild(player);
// Create initial terrain
var terrainWidth = LK.getAsset('terrain', {}).width; // Get width from asset
for (var i = 0; i < Math.ceil(2048 / terrainWidth) + 1; i++) {
createTerrainSegment(true, i * terrainWidth);
createTerrainSegment(false, i * terrainWidth);
}
// Start Phase 1 Music
LK.playMusic('phase1Music');
}
function initLevel2() {
if (level2Started) return;
level2Started = true;
currentLevel = 2; // Explicitly set, though might be set at transition point too
level2Progress = 0; // Reset progress for Level 2
console.log("Initializing Level 2");
LK.playMusic('level2Music'); // Play Level 2 music
// Clean up any remaining Level 1 specific elements like air enemies
airEnemies.forEach(function (ae) {
if (ae && !ae.destroyed) ae.destroy();
});
airEnemies = [];
// Boss should already be gone
if (boss && !boss.destroyed) boss.destroy();
boss = null;
bossLasers.forEach(function (l) {
if (l && !l.destroyed) l.destroy();
});
bossLasers = [];
// Reset spawn progress markers for Level 2 elements
lastWallSpawnProgress = 0;
lastDiagonalEnemySpawnProgress = 0;
lastTankSpawnProgress = 0;
// Ensure terrain continues if it was cleared (e.g., after boss fight if phases stopped it)
// The main terrain update loop should be modified to run during currentLevel === 2
var terrainWidth = LK.getAsset('terrain', {}).width;
var screenWidth = 2048;
var numSegmentsNeeded = Math.ceil(screenWidth / terrainWidth) + 1;
if (terrainSegmentsTop.length < numSegmentsNeeded) {
for (var i = terrainSegmentsTop.length; i < numSegmentsNeeded; i++) {
var xPos = i * terrainWidth;
// Find max X of existing segments to append correctly
if (terrainSegmentsTop.length > 0) {
xPos = terrainSegmentsTop.reduce(function (max, s) {
return s.x > max ? s.x : max;
}, 0) + terrainWidth;
}
createTerrainSegment(true, xPos);
}
}
if (terrainSegmentsBottom.length < numSegmentsNeeded) {
for (var i = terrainSegmentsBottom.length; i < numSegmentsNeeded; i++) {
var xPos = i * terrainWidth;
if (terrainSegmentsBottom.length > 0) {
xPos = terrainSegmentsBottom.reduce(function (max, s) {
return s.x > max ? s.x : max;
}, 0) + terrainWidth;
}
createTerrainSegment(false, xPos);
}
}
// Announce Level 2 Start (Optional visual cue)
var level2Text = new Text2("LEVEL 2", {
size: 150,
fill: 0xFFFF00
});
level2Text.anchor.set(0.5, 0.5);
level2Text.x = 2048 / 2;
level2Text.y = 2732 / 3;
game.addChild(level2Text);
LK.setTimeout(function () {
if (level2Text && !level2Text.destroyed) level2Text.destroy();
}, 3000); // Display for 3 seconds
// gamePhase variable might be set to a new value (e.g., 3) if other systems rely on it.
// For now, currentLevel = 2 will gate Level 2 logic.
gamePhase = 3; // Indicate a new phase distinct from level 1's 0, 1, 2.
}
// Helper function already moved above
// Helper function to save highscore
function saveHighscore() {
// Removed score argument, will use LK.getScore() internally
var currentScore = LK.getScore();
var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest";
var currentPlayerName = LK.profile && LK.profile.name ? LK.profile.name : "FRVR Player";
var highscores = [];
try {
if (storage && storage.available && storage.highscoresData) {
var parsedData = JSON.parse(storage.highscoresData);
if (Array.isArray(parsedData)) {
highscores = parsedData;
}
}
} catch (e) {
console.log("Error parsing existing highscoresData, resetting: " + e.message);
highscores = [];
}
// Add current score
highscores.push({
id: currentPlayerId,
name: currentPlayerName,
score: currentScore,
date: Date.now() // Store timestamp for sorting or display
});
// Sort by score (descending), then by date (newest first for tie-breaking)
highscores.sort(function (a, b) {
if (b.score === a.score) {
return b.date - a.date;
}
return b.score - a.score;
});
// Limit to a reasonable number of stored highscores, e.g., top 50
var MAX_STORED_HIGHSCORES = 50;
if (highscores.length > MAX_STORED_HIGHSCORES) {
highscores = highscores.slice(0, MAX_STORED_HIGHSCORES);
}
// Save back to storage
try {
if (storage && storage.available) {
storage.highscoresData = JSON.stringify(highscores);
}
} catch (e) {
console.log("Error stringifying or saving highscoresData: " + e.message);
}
// Update the player's personal best score, stored separately for quick access
// This key is used by createScoreBackground for "Your Best" display
if (currentPlayerId !== "guest") {
var personalBestKey = 'player_' + currentPlayerId;
var currentPersonalBest = 0;
try {
if (storage && storage.available && storage[personalBestKey]) {
currentPersonalBest = parseInt(storage[personalBestKey], 10) || 0;
}
} catch (parseError) {
console.log("Error parsing personal best for key " + personalBestKey + ": " + parseError.message);
currentPersonalBest = 0;
}
if (currentScore > currentPersonalBest) {
if (storage && storage.available) {
try {
storage[personalBestKey] = currentScore.toString();
} catch (saveError) {
console.log("Error saving personal best for key " + personalBestKey + ": " + saveError.message);
}
}
}
}
}
// The createScoreBackground function is now defined earlier in the code
// This duplicate definition has been moved earlier in the code
// Helper function to handle player death
function handlePlayerDeath() {
if (!player || player.isDead) {
return;
}
// Event Handlers
player.isDead = true;
player.alpha = 0; // Make player invisible during death processing
LK.getSound('playerExplosion').play();
// Optional: Flash the player before making them fully invisible, or flash screen
// LK.effects.flashObject(player, 0xFF0000, 500);
LK.effects.flashScreen(0xFF0000, 200); // Short screen flash
playerLives--;
if (livesTxt) {
livesTxt.setText("Lives: x" + playerLives);
}
LK.setTimeout(function () {
if (playerLives > 0) {
respawnPlayer();
} else {
// Save highscore before showing Game Over
saveHighscore(); //{6o} // Argument removed
// Create background for scores before showing game over
createScoreBackground();
LK.showGameOver();
}
}, 1000); // 1 second delay for effects and sound
}
// Helper function to create score background when game is won or lost
function createScoreBackground() {
var createdTextElements = [];
// Store reference to background for later access
if (highscoreBackground) {
highscoreBackground.destroy();
}
// Get current player information
var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest";
var currentPlayerName = LK.profile && LK.profile.name ? LK.profile.name : "FRVR Player";
var currentScore = LK.getScore();
// Get highscores from storage
var highscores = [];
try {
if (storage && storage.available && storage.highscoresData) {
highscores = JSON.parse(storage.highscoresData);
}
} catch (e) {
console.log("Error parsing highscores:", e);
highscores = [];
}
// Check if the current score is already in the list (for game over after saving score)
var scoreExists = false;
for (var i = 0; i < highscores.length; i++) {
if (highscores[i].id === currentPlayerId && highscores[i].score === currentScore && Date.now() - highscores[i].date < 60000) {
// Added in last minute
scoreExists = true;
highscores[i].isCurrent = true; // Mark as current game's score
break;
}
}
// Add the current score to the highscores immediately if not already there
if (!scoreExists) {
highscores.push({
id: currentPlayerId,
name: currentPlayerName,
score: currentScore,
date: Date.now(),
isCurrent: true // Mark as current game's score
});
}
// Sort highscores by date (most recent first)
highscores.sort(function (a, b) {
return b.date - a.date; // Most recent date first
});
var personalBest = 0;
// Check if player has a personal best stored
if (currentPlayerId !== "guest") {
var personalKey = "player_" + currentPlayerId;
try {
if (storage && storage.available && storage[personalKey]) {
personalBest = parseInt(storage[personalKey], 10) || 0;
}
// Also check from cloud storage
if (storage && storage.available) {
storage.load('player_score_' + currentPlayerId, function (savedScore) {
if (savedScore) {
var cloudScore = parseInt(savedScore, 10) || 0;
if (cloudScore > personalBest) {
personalBest = cloudScore;
storage[personalKey] = personalBest.toString();
}
}
});
}
} catch (e) {
console.log("Error retrieving personal best:", e);
}
}
// Create background
highscoreBackground = game.addChild(LK.getAsset('Background2', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 20.48,
scaleY: 27.32,
alpha: 0.9
}));
// Create title
var titleText = new Text2("GAME RESULTS", {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0);
titleText.x = 2048 / 2;
titleText.y = 200;
game.addChild(titleText);
createdTextElements.push(titleText);
// Current score
var currentScoreText = new Text2("Your Score: " + currentScore, {
size: 100,
fill: 0x00FF00
});
currentScoreText.anchor.set(0.5, 0);
currentScoreText.x = 2048 / 2;
currentScoreText.y = 350;
game.addChild(currentScoreText);
createdTextElements.push(currentScoreText);
// Show personal best if it exists
if (personalBest > 0 && personalBest !== currentScore) {
var personalBestText = new Text2("Your Best: " + personalBest, {
size: 80,
fill: 0xFFD700
});
personalBestText.anchor.set(0.5, 0);
personalBestText.x = 2048 / 2;
personalBestText.y = 470;
game.addChild(personalBestText);
createdTextElements.push(personalBestText);
// Show new record indicator
if (currentScore > personalBest) {
var newRecordText = new Text2("NEW RECORD!", {
size: 80,
fill: 0xFF00FF
});
newRecordText.anchor.set(0.5, 0);
newRecordText.x = 2048 / 2;
newRecordText.y = 560;
game.addChild(newRecordText);
createdTextElements.push(newRecordText);
}
}
// Display up to 20 highscores instead of 10
var maxToShow = Math.min(highscores.length, 20);
if (maxToShow > 0) {
// Display title for scores
var titleScoreText = new Text2("Recent Player Scores", {
size: 90,
fill: 0xFFD700
});
titleScoreText.anchor.set(0.5, 0);
titleScoreText.x = 2048 / 2;
titleScoreText.y = 700;
game.addChild(titleScoreText);
createdTextElements.push(titleScoreText);
// Adjust spacing based on number of scores
var spacing = maxToShow > 10 ? 90 : maxToShow > 5 ? 120 : 150;
var fontSize = maxToShow > 10 ? 60 : maxToShow > 5 ? 70 : 80;
// Display player scores
for (var i = 0; i < maxToShow; i++) {
var entry = highscores[i];
var isCurrentPlayer = entry.id === currentPlayerId;
var isCurrentScore = entry.isCurrent === true;
// Format score with comma separators
var formattedScore = entry.score.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// Get player name, using ID if name is missing
var playerName = entry.name || "Player " + entry.id.substring(0, 5);
var scoreEntry = new Text2(i + 1 + ". " + playerName + ": " + formattedScore, {
size: fontSize,
fill: isCurrentScore ? "#FF00FF" : isCurrentPlayer ? "#00FF00" : "#FFFFFF" // Magenta for current score, green for other scores by current player
});
// If more than 10 entries, create two columns
if (maxToShow > 10 && i >= 10) {
// Second column (for entries 11-20)
scoreEntry.anchor.set(0, 0); // Left align for second column
scoreEntry.x = 2048 * 0.25; // Position at 1/4 of screen width
scoreEntry.y = 800 + (i - 10) * spacing;
} else if (maxToShow > 10) {
// First column (for entries 1-10)
scoreEntry.anchor.set(1, 0); // Right align for first column
scoreEntry.x = 2048 * 0.75; // Position at 3/4 of screen width
scoreEntry.y = 800 + i * spacing;
} else {
// Single column layout (for 10 or fewer entries)
scoreEntry.anchor.set(0.5, 0);
scoreEntry.x = 2048 / 2;
scoreEntry.y = 800 + i * spacing;
}
game.addChild(scoreEntry);
createdTextElements.push(scoreEntry);
}
} else {
// No scores message
var noScoresText = new Text2("No scores yet!", {
size: 80,
fill: 0xFFFFFF
});
noScoresText.anchor.set(0.5, 0);
noScoresText.x = 2048 / 2;
noScoresText.y = 800;
game.addChild(noScoresText);
createdTextElements.push(noScoresText);
}
return createdTextElements;
}
// The createScoreBackground function has been moved earlier in the code
// Helper function to respawn player at checkpoint
function respawnPlayer() {
if (!player) {
return;
}
player.x = 300;
player.y = 2732 / 2;
player.clampPosition();
player.alpha = 1; // Make player visible again
player.isDead = false;
player.invincibleUntil = LK.ticks + 120; // 2 seconds of invincibility
// Clear active threats
for (var i = enemyBullets.length - 1; i >= 0; i--) {
if (enemyBullets[i] && !enemyBullets[i].destroyed) {
enemyBullets[i].destroy();
}
}
enemyBullets = [];
if (gamePhase === 2) {
// Boss phase
for (var i = bossLasers.length - 1; i >= 0; i--) {
if (bossLasers[i] && !bossLasers[i].destroyed) {
bossLasers[i].destroy();
}
}
bossLasers = [];
}
// Player is reset, other game elements (enemies, boss health) remain.
}
// Create intro screen elements
var backgroundAsset = LK.getAsset('Background0', {});
var scaleX = 2048 / backgroundAsset.width;
var scaleY = 2732 / backgroundAsset.height;
var background = game.addChild(LK.getAsset('Background0', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: scaleX,
scaleY: scaleY
}));
// Play intro music
LK.playMusic('Intromusic1');
var startButton = game.addChild(LK.getAsset('Startgamebutton', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2 + 700,
scaleX: 1.5,
scaleY: 1.5
}));
var highscoreButton = game.addChild(LK.getAsset('Highscorebutton', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2 + 1050,
scaleX: 1.5,
scaleY: 1.5
}));
function startGame() {
// Remove intro elements
background.destroy();
startButton.destroy();
highscoreButton.destroy();
// Set game as started
isGameStarted = true;
// Show score
scoreTxt.visible = true;
// Initialize the game
initGame();
}
function showHighscore() {
if (showingHighscores) {
// If currently showing, hide everything
if (highscoreBackground) {
// highscoreBackground is global, set by createScoreBackground
highscoreBackground.destroy();
highscoreBackground = null;
}
elementsShownByShowHighscore.forEach(function (el) {
if (el && !el.destroyed) {
el.destroy();
}
});
elementsShownByShowHighscore = [];
showingHighscores = false;
return;
}
// Not showing, so display the highscore screen
showingHighscores = true;
// Clear any residual elements from a previous display, just in case
if (highscoreBackground && !highscoreBackground.destroyed) {
highscoreBackground.destroy();
highscoreBackground = null;
}
elementsShownByShowHighscore.forEach(function (el) {
if (el && !el.destroyed) {
el.destroy();
}
});
elementsShownByShowHighscore = [];
// Call createScoreBackground to generate the main content.
// This function sets the global 'highscoreBackground' and returns an array of its text elements.
var textsFromCreateScoreBg = createScoreBackground();
elementsShownByShowHighscore = elementsShownByShowHighscore.concat(textsFromCreateScoreBg);
// Add "TAP ANYWHERE TO RETURN" text, specific to this highscore view
var backText = new Text2("TAP ANYWHERE TO RETURN", {
size: 70,
fill: 0xFFFFFF //{dT} // Kept original fill and size
});
backText.anchor.set(0.5, 0);
backText.x = 2048 / 2;
backText.y = 2200; // Adjusted Y to be a bit lower if needed, or use original from createScoreBackground if it had one
game.addChild(backText);
elementsShownByShowHighscore.push(backText); // Add to our list for cleanup
// Fallback to LK.showLeaderboard if our custom screen is up for too long
if (typeof LK.showLeaderboard === 'function') {
LK.setTimeout(function () {
if (showingHighscores) {
// Check if our screen is still active
showingHighscores = false; // Toggle off state
if (highscoreBackground && !highscoreBackground.destroyed) {
highscoreBackground.destroy();
highscoreBackground = null;
}
elementsShownByShowHighscore.forEach(function (el) {
if (el && !el.destroyed) {
el.destroy();
}
});
elementsShownByShowHighscore = [];
LK.showLeaderboard(); // Show the engine's leaderboard
}
}, 5000); // 5 second timeout
}
}
// Add interactive behavior to buttons
startButton.interactive = true;
startButton.buttonMode = true;
startButton.down = function () {
startGame();
};
highscoreButton.interactive = true;
highscoreButton.buttonMode = true;
highscoreButton.down = function () {
showHighscore();
};
game.down = function (x, y, obj) {
// Handle tapping on highscore screen to return to intro
if (showingHighscores) {
showHighscore(); // This will toggle off the highscore display
return;
}
// Only process player dragging if game has started
if (isGameStarted && player) {
// Check if touch is on the player UFO
var localPos = player.toLocal(game.toGlobal({
x: x,
y: y
})); // Convert game coords to player's local coords
// Use a slightly larger hit area for easier dragging
var hitWidth = player.width * 1.5;
var hitHeight = player.height * 1.5;
if (Math.abs(localPos.x) < hitWidth / 2 && Math.abs(localPos.y) < hitHeight / 2) {
dragNode = player;
// Instantly move player to touch position for responsive feel
var gamePos = game.toLocal(obj.global); // Use obj.global for precise position
player.x = gamePos.x;
player.y = gamePos.y;
player.clampPosition();
} else {
dragNode = null;
}
}
};
game.move = function (x, y, obj) {
if (dragNode) {
var gamePos = game.toLocal(obj.global); // Convert event global position to game coordinates
dragNode.x = gamePos.x;
dragNode.y = gamePos.y;
// Clamp player position within bounds immediately after move
if (dragNode === player) {
player.clampPosition();
}
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Game Update Logic
game.update = function () {
// If game hasn't started yet or player is gone, don't process game logic
if (!isGameStarted || !player || player.destroyed) {
// Check if we need to initialize level 2 due to a reload/restart mid-level 2
// This is an edge case, main L2 init is after boss defeat.
// if (currentLevel === 2 && !level2Started && isGameStarted && player) { initLevel2(); }
// Make sure score is hidden during intro
scoreTxt.visible = false;
return; // Game not started or player fully gone, nothing to do.
}
// Make sure score is visible during gameplay
scoreTxt.visible = true;
// Initialize Level 2 if conditions are met (transitioned from Level 1)
if (currentLevel === 2 && !level2Started && isGameStarted && player && !player.isDead) {
initLevel2();
}
// If player is dead, stop their specific logic & wait for respawn/game over timeout
// Other game elements (enemies, bullets) might still update.
// Player input and collisions will be gated by player.isDead or invincibility.
// Player invincibility blinking
if (player && !player.isDead && LK.ticks < player.invincibleUntil) {
player.alpha = LK.ticks % 20 < 10 ? 0.5 : 1; // Blink
} else if (player && !player.isDead && player.alpha !== 1) {
player.alpha = 1; // Ensure alpha is reset
}
// --- Global Updates ---
// Player Shooting
playerFireCooldown++;
if (!player.isDead && dragNode === player && playerFireCooldown >= playerFireRate) {
// Only shoot while dragging/controlling and alive
playerFireCooldown = 0;
var bullet = new PlayerBullet(player.x + player.width / 2, player.y);
game.addChild(bullet);
playerBullets.push(bullet);
LK.getSound('playerShoot').play();
}
// Score increases over time
scoreIncrementTimer++;
if (scoreIncrementTimer >= 60) {
// Add 10 points every second
scoreIncrementTimer = 0;
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
}
// Increment distance scrolled (used by Level 2)
if (player && !player.isDead) {
// Only scroll if player is active and game is running
// gameDistanceScrolled += terrainSpeed; // This was for overall game, level2Progress is specific
if (currentLevel === 2 && level2Started) {
level2Progress += terrainSpeed;
}
}
// --- Phase Management ---
var elapsedTicks = LK.ticks - phaseStartTime;
if (gamePhase === 0 && elapsedTicks >= phaseDuration) {
// Transition to Phase 1 (Space)
gamePhase = 1;
phaseStartTime = LK.ticks; // Reset timer for next phase if needed
console.log("Transitioning to Phase 1: Space");
// Clean up terrain-specific elements
terrainSegmentsTop.forEach(function (s) {
return s.destroy();
});
terrainSegmentsBottom.forEach(function (s) {
return s.destroy();
});
mountains.forEach(function (m) {
return m.destroy();
});
groundEnemies.forEach(function (ge) {
return ge.destroy();
});
terrainSegmentsTop = [];
terrainSegmentsBottom = [];
mountains = [];
groundEnemies = [];
// Spawn initial air enemies
for (var i = 0; i < 5; i++) {
// Start with 5 air enemies
spawnAirEnemy();
}
LK.playMusic('phase2Music'); // Switch music
} else if (gamePhase === 1 && elapsedTicks >= phaseDuration * 1.5) {
// Example: Boss after 1.5x phase duration
// Transition to Phase 2 (Boss)
if (!boss) {
// Ensure boss only spawns once
gamePhase = 2;
phaseStartTime = LK.ticks;
console.log("Transitioning to Phase 2: Boss");
// Clean up air enemies
airEnemies.forEach(function (ae) {
return ae.destroy();
});
airEnemies = [];
// Spawn Boss
boss = new Boss();
game.addChild(boss);
LK.playMusic('bossMusic'); // Boss music
}
}
// --- Terrain Logic (Active in Level 1 Phase 0 and Level 2 except Area 4) ---
var isArea4 = currentLevel === 2 && level2Progress >= LEVEL2_AREA3_END_PX;
if ((gamePhase === 0 || currentLevel === 2) && !isArea4) {
// Update and manage terrain segments
var terrainWidth = LK.getAsset('terrain', {}).width;
for (var i = terrainSegmentsTop.length - 1; i >= 0; i--) {
var segment = terrainSegmentsTop[i];
if (segment.x < -terrainWidth) {
// Reposition segment to the right
var maxX = 0;
terrainSegmentsTop.forEach(function (s) {
if (s.x > maxX) {
maxX = s.x;
}
});
segment.x = maxX + terrainWidth;
// Always spawn an enemy on the reused segment
spawnGroundEnemy(segment);
// Additionally, 50% chance for a mountain
if (Math.random() < 0.5) {
spawnMountain(segment);
}
}
}
// Repeat for bottom terrain
for (var i = terrainSegmentsBottom.length - 1; i >= 0; i--) {
var segment = terrainSegmentsBottom[i];
if (segment.x < -terrainWidth) {
var maxX = 0;
terrainSegmentsBottom.forEach(function (s) {
if (s.x > maxX) {
maxX = s.x;
}
});
segment.x = maxX + terrainWidth;
// Always spawn an enemy on the reused segment
spawnGroundEnemy(segment);
// Additionally, 50% chance for a mountain
if (Math.random() < 0.5) {
spawnMountain(segment);
}
}
}
// Update mountains & check collision
for (var i = mountains.length - 1; i >= 0; i--) {
var mountain = mountains[i];
if (mountain.x < -mountain.width) {
mountain.destroy();
mountains.splice(i, 1);
} else {
if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(mountain)) {
handlePlayerDeath();
return; // Stop update processing
}
}
}
// Update ground enemies & check collision
for (var i = groundEnemies.length - 1; i >= 0; i--) {
var enemy = groundEnemies[i];
if (enemy.x < -enemy.width) {
enemy.destroy();
groundEnemies.splice(i, 1);
} else {
// Collision check: Player vs Ground Enemy
if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(enemy)) {
LK.getSound('enemyExplosion').play(); //{3L} // Enemy explodes
enemy.destroy(); // Destroy enemy on collision too
groundEnemies.splice(i, 1);
handlePlayerDeath();
return;
}
}
}
// Re-clamp player position based on potentially moving terrain (simple clamp for now)
player.clampPosition();
}
// --- Phase 1/2: Air Enemy Logic ---
if (gamePhase === 1) {
// Add more air enemies periodically?
if (LK.ticks % 180 === 0 && airEnemies.length < 10) {
// Spawn if less than 10, every 3 seconds
spawnAirEnemy();
}
// Update air enemies & check collision
for (var i = airEnemies.length - 1; i >= 0; i--) {
var enemy = airEnemies[i];
// Air enemies handle their own off-screen logic (respawn) in their update
// Collision check: Player vs Air Enemy
if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(enemy)) {
LK.getSound('enemyExplosion').play(); //{41} // Enemy explodes
enemy.destroy(); // Destroy enemy on collision
airEnemies.splice(i, 1);
// LK.setScore(LK.getScore() + 50); // Score for destroying enemy - player died, maybe no score for this? Or keep it. Let's keep for now.
// scoreTxt.setText(LK.getScore());
handlePlayerDeath();
return;
}
}
}
// --- Phase 3: Boss Logic ---
if (gamePhase === 2 && boss) {
// Check collision: Player vs Boss
if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(boss)) {
// Don't destroy boss on collision
handlePlayerDeath();
return;
}
// Update boss lasers & check collision
for (var i = bossLasers.length - 1; i >= 0; i--) {
var laser = bossLasers[i];
// Check if laser is off-screen
if (laser.x < -laser.width || laser.x > 2048 + laser.width || laser.y < -laser.height || laser.y > 2732 + laser.height) {
laser.destroy();
bossLasers.splice(i, 1);
} else {
// Collision check: Player vs Boss Laser
if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(laser)) {
laser.destroy(); // Destroy laser
bossLasers.splice(i, 1);
handlePlayerDeath();
return;
}
}
}
// Note: Boss defeat condition is now handled within the player bullet collision check loop.
}
// --- Update Enemy Bullets & Check Collision (All Phases) ---
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var bullet = enemyBullets[i];
// Check if bullet is off-screen
if (bullet.y < -bullet.height || bullet.y > 2732 + bullet.height) {
bullet.destroy();
enemyBullets.splice(i, 1);
} else {
// Collision check: Player vs Enemy Bullet
if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(bullet)) {
bullet.destroy(); // Destroy bullet
enemyBullets.splice(i, 1);
handlePlayerDeath(); // This function now handles the delay and sound
return; // Stop update processing
}
}
}
// The saveHighscore function has been moved before handlePlayerDeath to fix the undefined error
// --- Update Player Bullets & Check Collision ---
for (var i = playerBullets.length - 1; i >= 0; i--) {
var bullet = playerBullets[i];
// Check if bullet is off-screen (right side)
if (bullet.x > 2048 + bullet.width) {
bullet.destroy();
playerBullets.splice(i, 1);
continue; // Skip collision checks if off-screen
}
// Collision Check: Player Bullet vs Ground Enemy (Phase 0)
if (gamePhase === 0) {
for (var j = groundEnemies.length - 1; j >= 0; j--) {
var enemy = groundEnemies[j];
if (bullet.intersects(enemy)) {
LK.getSound('enemyExplosion').play();
LK.effects.flashObject(enemy, 0xFFFFFF, 100); // Flash enemy white
enemy.destroy();
groundEnemies.splice(j, 1);
bullet.destroy();
playerBullets.splice(i, 1);
LK.setScore(LK.getScore() + 100); // Score for destroying ground enemy
scoreTxt.setText(LK.getScore());
break; // Bullet can only hit one enemy
}
}
if (!bullet.exists) {
continue;
} // Check if bullet was destroyed in previous loop
}
// Collision Check: Player Bullet vs Air Enemy (Phase 1)
if (gamePhase === 1) {
for (var j = airEnemies.length - 1; j >= 0; j--) {
var enemy = airEnemies[j];
if (bullet.intersects(enemy)) {
LK.getSound('enemyExplosion').play();
LK.effects.flashObject(enemy, 0xFFFFFF, 100); // Flash enemy white
enemy.destroy();
airEnemies.splice(j, 1);
bullet.destroy();
playerBullets.splice(i, 1);
LK.setScore(LK.getScore() + 150); // Score for destroying air enemy
scoreTxt.setText(LK.getScore());
break; // Bullet can only hit one enemy
}
}
if (!bullet.exists) {
continue;
} // Check if bullet was destroyed in previous loop
}
// Collision Check: Player Bullet vs Boss (Phase 2)
if (gamePhase === 2 && boss) {
if (bullet.intersects(boss)) {
LK.getSound('enemyExplosion').play(); // Use enemy explosion for hit sound
LK.effects.flashObject(boss, 0xFFFFFF, 100); // Flash boss white
bullet.destroy();
playerBullets.splice(i, 1);
boss.health -= 1; // Decrease boss health
LK.setScore(LK.getScore() + 10); // Small score for hitting boss
scoreTxt.setText(LK.getScore());
// Boss defeat check is now inside the bullet loop
if (boss.health <= 0) {
LK.getSound('bossExplosion').play(); // Play boss explosion sound
boss.destroy();
boss = null;
// Cleanup remaining lasers
bossLasers.forEach(function (l) {
return l.destroy();
});
bossLasers = [];
LK.setScore(LK.getScore() + 5000); // Big score bonus
scoreTxt.setText(LK.getScore());
// Save highscore as a milestone for defeating the boss
saveHighscore(); //{be} // Argument removed
// Transition to Level 2
currentLevel = 2;
level2Started = false; // Flag to trigger initLevel2() once at the start of next update cycle
// Note: Removed createScoreBackground(); call here as game transitions to Level 2
LK.setScore(LK.getScore() + 5000); // Bonus for defeating boss (applied again, consider if this is intended or consolidate)
scoreTxt.setText(LK.getScore());
// saveHighscore(LK.getScore()); // Score is already saved above, this might be redundant unless intended for separate tracking
// Clear boss specific elements
if (boss && !boss.destroyed) boss.destroy();
boss = null;
bossLasers.forEach(function (l) {
if (l && !l.destroyed) l.destroy();
});
bossLasers = [];
console.log("Boss defeated! Preparing Level 2.");
// initLevel2() will be called in the next game.update() loop at the top
return; // Stop update processing this frame to allow initLevel2 to run cleanly
}
break; // Bullet is destroyed after hitting boss
}
}
// --- Level 2 Logic ---
if (currentLevel === 2 && level2Started) {
var LEVEL2_AREA1_END_PX = LEVEL2_AREA1_END_MILES * PIXELS_PER_MILE;
var LEVEL2_AREA2_END_PX = (LEVEL2_AREA1_END_MILES + LEVEL2_AREA2_DURATION_MILES) * PIXELS_PER_MILE;
var LEVEL2_AREA3_END_PX = (LEVEL2_AREA1_END_MILES + LEVEL2_AREA2_DURATION_MILES + LEVEL2_AREA3_DURATION_MILES) * PIXELS_PER_MILE;
var LEVEL2_FINAL_END_PX = (LEVEL2_AREA1_END_MILES + LEVEL2_AREA2_DURATION_MILES + LEVEL2_AREA3_DURATION_MILES + LEVEL2_AREA4_DURATION_MILES) * PIXELS_PER_MILE;
// --- Level 2, Area 1 (Vertical Walls) ---
if (level2Progress < LEVEL2_AREA1_END_PX) {
if (level2Progress > lastWallSpawnProgress + WALL_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) {
lastWallSpawnProgress = level2Progress;
var wall = new VerticalWall(2048 + 200); // Spawn off-screen right
game.addChild(wall);
verticalWalls.push(wall);
}
}
// --- Level 2, Area 2 (Diagonal Enemies, Tanks) ---
else if (level2Progress < LEVEL2_AREA2_END_PX) {
// Clear any remaining walls from Area 1 when transitioning
if (verticalWalls.length > 0 && level2Progress >= LEVEL2_AREA1_END_PX && level2Progress < LEVEL2_AREA1_END_PX + terrainSpeed * 2) {
verticalWalls.forEach(function (w) {
if (w && !w.isDestroyed) {
w.turrets.forEach(function (t) {
if (t && !t.isDestroyed) t.destroy();
});
w.destroy();
}
});
verticalWalls = [];
}
if (level2Progress > lastDiagonalEnemySpawnProgress + DIAGONAL_ENEMY_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) {
lastDiagonalEnemySpawnProgress = level2Progress;
var diagEnemy = new DiagonalEnemy(2048 + 150, 2732 / 2);
game.addChild(diagEnemy);
diagonalEnemies.push(diagEnemy);
}
if (level2Progress > lastTankSpawnProgress + TANK_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) {
lastTankSpawnProgress = level2Progress;
var tankTop = new Tank(true, 2048 + 100);
var tankBottom = new Tank(false, 2048 + 100);
game.addChild(tankTop);
game.addChild(tankBottom);
tanks.push(tankTop);
tanks.push(tankBottom);
}
}
// --- Level 2, Area 3 (Serpent Formations) ---
else if (level2Progress < LEVEL2_FINAL_END_PX) {
// Clear any remaining Diagonal Enemies and Tanks from Area 2
if ((diagonalEnemies.length > 0 || tanks.length > 0) && level2Progress >= LEVEL2_AREA2_END_PX && level2Progress < LEVEL2_AREA2_END_PX + terrainSpeed * 2) {
diagonalEnemies.forEach(function (de) {
if (de && !de.isDestroyed) de.destroy();
});
diagonalEnemies = [];
tanks.forEach(function (t) {
if (t && !t.isDestroyed) t.destroy();
});
tanks = [];
}
// Spawn Serpent Formations
if (serpentFormations.length < MAX_SERPENT_FORMATIONS && level2Progress > lastSerpentSpawnProgress + SERPENT_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) {
lastSerpentSpawnProgress = level2Progress;
var newSerpent = new SerpentFormation();
game.addChild(newSerpent); // The formation is a Container
serpentFormations.push(newSerpent);
}
}
// --- Level 2, Area 4 (Small Terrains with Moving Tanks) ---
else if (level2Progress < LEVEL2_FINAL_END_PX + LEVEL2_AREA4_DURATION_MILES * PIXELS_PER_MILE) {
// Clear serpent formations from Area 3 when transitioning to Area 4
if (serpentFormations.length > 0 && level2Progress >= LEVEL2_FINAL_END_PX && level2Progress < LEVEL2_FINAL_END_PX + terrainSpeed * 2) {
serpentFormations.forEach(function (sf) {
if (sf && !sf.isDestroyed()) sf.destroy();
});
serpentFormations = [];
// Clear main terrain since area4 has no ground
terrainSegmentsTop.forEach(function (s) {
return s.destroy();
});
terrainSegmentsBottom.forEach(function (s) {
return s.destroy();
});
terrainSegmentsTop = [];
terrainSegmentsBottom = [];
}
// Spawn Small Terrains with tanks
if (level2Progress > lastSmallTerrainSpawnProgress + SMALL_TERRAIN_SPAWN_INTERVAL_MILES * PIXELS_PER_MILE) {
lastSmallTerrainSpawnProgress = level2Progress;
var topTerrain = new SmallTerrain(true, 2048 + 200);
var bottomTerrain = new SmallTerrain(false, 2048 + 200);
game.addChild(topTerrain);
game.addChild(bottomTerrain);
smallTerrains.push(topTerrain);
smallTerrains.push(bottomTerrain);
// Add tanks to the terrains
topTerrain.addTank();
bottomTerrain.addTank();
}
}
// --- Level 2 Completed ---
else {
if (player && !player.isDead) {
// Ensure win condition only if player alive
console.log("Level 2 Complete!");
// Clean up any remaining serpent formations
serpentFormations.forEach(function (sf) {
if (sf && !sf.isDestroyed()) sf.destroy();
});
serpentFormations = [];
saveHighscore(); //{bI} // Argument removed
createScoreBackground();
LK.setTimeout(function () {
LK.showYouWin(); // Game ends after Level 2 (Area 3)
}, 500);
player.isDead = true; // Prevent further actions
return;
}
}
// Update and Collision for Level 2 Elements
// Vertical Walls (and their turrets)
for (var i = verticalWalls.length - 1; i >= 0; i--) {
var wall = verticalWalls[i];
if (wall.isDestroyed) {
// Already handled by wall itself
// verticalWalls.splice(i, 1); // Wall removes itself from array
continue;
}
if (wall.x < -wall.width) {
// Off-screen left
wall.turrets.forEach(function (t) {
if (t && !t.isDestroyed) t.destroy();
});
wall.destroy();
verticalWalls.splice(i, 1);
} else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(wall)) {
handlePlayerDeath();
return;
}
}
// Turrets are updated via their own class or Wall. Bullets handled by main enemyBullets loop.
// Diagonal Enemies
for (var i = diagonalEnemies.length - 1; i >= 0; i--) {
var diagEnemy = diagonalEnemies[i];
if (diagEnemy.isDestroyed) {
// diagonalEnemies.splice(i, 1); // Enemy removes itself
continue;
}
if (diagEnemy.x < -diagEnemy.width) {
diagEnemy.destroy();
diagonalEnemies.splice(i, 1);
} else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(diagEnemy)) {
diagEnemy.takeDamage();
handlePlayerDeath();
return;
}
}
// Tanks
for (var i = tanks.length - 1; i >= 0; i--) {
var tank = tanks[i];
if (tank.isDestroyed) {
// tanks.splice(i, 1); // Enemy removes itself
continue;
}
if (tank.x < -tank.width) {
tank.destroy();
tanks.splice(i, 1);
} else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(tank)) {
tank.takeDamage();
handlePlayerDeath();
return;
}
}
// Small Terrains (Area 4)
for (var i = smallTerrains.length - 1; i >= 0; i--) {
var smallTerrain = smallTerrains[i];
if (smallTerrain.isDestroyed) {
continue;
}
if (smallTerrain.x < -smallTerrain.width) {
smallTerrain.destroy();
smallTerrains.splice(i, 1);
} else if (!player.isDead && LK.ticks > player.invincibleUntil && player.intersects(smallTerrain)) {
handlePlayerDeath();
return;
}
}
// Update Serpent Formations (Area 3 specific, but update loop runs for all formations)
for (var i = serpentFormations.length - 1; i >= 0; i--) {
var serpent = serpentFormations[i];
// serpent.update(); // Already called because it's a child of game
if (serpent.isDone()) {
serpent.destroy();
serpentFormations.splice(i, 1);
} else {
// Player collision with individual serpent ships
if (!player.isDead && LK.ticks > player.invincibleUntil) {
for (var j = serpent.ships.length - 1; j >= 0; j--) {
var ship = serpent.ships[j];
if (!ship.isDestroyed) {
// Need to get global position of the ship for intersection check
var shipGlobalPos = serpent.toGlobal(ship.position);
var shipGameAreaRect = new Rectangle(shipGlobalPos.x - ship.width / 2, shipGlobalPos.y - ship.height / 2, ship.width, ship.height);
// Create a temporary object for player bounds for intersects method if player itself isn't a simple rectangle
var playerBounds = new Rectangle(player.x - player.width / 2, player.y - player.height / 2, player.width, player.height);
// AABB intersection check
if (playerBounds.x < shipGameAreaRect.x + shipGameAreaRect.width && playerBounds.x + playerBounds.width > shipGameAreaRect.x && playerBounds.y < shipGameAreaRect.y + shipGameAreaRect.height && playerBounds.y + playerBounds.height > shipGameAreaRect.y) {
ship.takeDamage(); // Serpent ship takes damage
handlePlayerDeath(); // Player also dies
return; // Stop further processing this frame
}
}
}
}
}
}
} // End if (currentLevel === 2 && level2Started)
// --- Update Player Bullets & Check Collision --- (Continued with Level 2 targets)
// (This section is appended to the existing playerBullets loop, before the final closing brace of the loop)
// The existing player bullet loop needs to be modified to include these checks.
// This block should be inserted *inside* the `for (var i = playerBullets.length - 1; i >= 0; i--)` loop for player bullets,
// after existing collision checks or as part of a conditional block for `currentLevel === 2`.
// Existing player bullet loop:
// for (var i = playerBullets.length - 1; i >= 0; i--) {
// var bullet = playerBullets[i];
// ...
// if (gamePhase === 2 && boss) { ... } // Existing boss collision
// // ADD LEVEL 2 COLLISION LOGIC HERE:
if (currentLevel === 2 && bullet.exists) {
// Check bullet.exists in case prior collision destroyed it
// Vs WallTurrets (part of VerticalWalls)
for (var k = verticalWalls.length - 1; k >= 0; k--) {
var wall = verticalWalls[k];
if (wall.isDestroyed) continue;
for (var l = wall.turrets.length - 1; l >= 0; l--) {
var turret = wall.turrets[l];
if (turret.isDestroyed) continue;
if (bullet.intersects(turret)) {
if (turret.takeDamage()) {
// true if destroyed
LK.setScore(LK.getScore() + 75);
scoreTxt.setText(LK.getScore());
}
bullet.destroy();
playerBullets.splice(i, 1);
break;
}
}
if (!bullet.exists) break;
}
if (!bullet.exists) continue; // To next player bullet if this one was destroyed
// Vs DiagonalEnemies
for (var k = diagonalEnemies.length - 1; k >= 0; k--) {
var diagEnemy = diagonalEnemies[k];
if (diagEnemy.isDestroyed) continue;
if (bullet.intersects(diagEnemy)) {
if (diagEnemy.takeDamage()) {
LK.setScore(LK.getScore() + 250);
scoreTxt.setText(LK.getScore());
}
bullet.destroy();
playerBullets.splice(i, 1);
break;
}
}
if (!bullet.exists) continue;
// Vs Tanks
for (var k = tanks.length - 1; k >= 0; k--) {
var tank = tanks[k];
if (tank.isDestroyed) continue;
if (bullet.intersects(tank)) {
if (tank.takeDamage()) {
LK.setScore(LK.getScore() + 175);
scoreTxt.setText(LK.getScore());
}
bullet.destroy();
playerBullets.splice(i, 1);
break;
}
}
if (!bullet.exists) continue;
// Vs SerpentEnemyShips (within SerpentFormations)
if (bullet.exists) {
// Check again in case previous collision destroyed it
for (var k = serpentFormations.length - 1; k >= 0; k--) {
var formation = serpentFormations[k];
if (formation.isDestroyed()) continue;
for (var l = formation.ships.length - 1; l >= 0; l--) {
var serpentShip = formation.ships[l];
if (serpentShip.isDestroyed) continue;
// Get global position of the serpent ship for accurate intersection
var shipGlobalPos = formation.toGlobal(serpentShip.position);
// Create a temporary rectangle for the serpent ship in game coordinates
// Assuming serpentShip.width and serpentShip.height are available (from its graphic)
var serpentShipGameRect = new Rectangle(shipGlobalPos.x - serpentShip.width / 2, shipGlobalPos.y - serpentShip.height / 2, serpentShip.width, serpentShip.height);
// Create a temporary rectangle for the bullet
var bulletRect = new Rectangle(bullet.x - bullet.width / 2, bullet.y - bullet.height / 2, bullet.width, bullet.height);
// Manual AABB intersection check
if (bulletRect.x < serpentShipGameRect.x + serpentShipGameRect.width && bulletRect.x + bulletRect.width > serpentShipGameRect.x && bulletRect.y < serpentShipGameRect.y + serpentShipGameRect.height && bulletRect.y + bulletRect.height > serpentShipGameRect.y) {
if (serpentShip.takeDamage()) {
// true if destroyed
LK.setScore(LK.getScore() + 200); // Score for serpent ship
scoreTxt.setText(LK.getScore());
}
bullet.destroy();
playerBullets.splice(i, 1);
break; // Bullet hits one serpent ship
}
}
if (!bullet.exists) break; // If bullet was destroyed, move to next player bullet
}
}
if (!bullet.exists) continue; // To next player bullet
}
// } // End of playerBullets loop (this closing brace is illustrative, don't add a new one)
} // End of playerBullets loop
};
Alien Airships of boss, HD colors. In-Game asset. 2d. High contrast. No shadows
pack mountain, yellow, HD colors. In-Game asset. 2d. High contrast. No shadows.no black lines
shot circle. blur, light, HD colors In-Game asset. 2d. High contrast. No shadows
gunTurret aiming diagonal. yellow, HD colors. In-Game asset. 2d. High contrast. No shadows
Square & diagonal tank enemies from the future have 4 turrets on its 4 sides, HD colors. In-Game asset. 2d. High contrast. No shadows