User prompt
change the code to this one // Save high score with account ID before game over var currentScore = LK.getScore(); var accountId = storage.accountId; if (accountId) { // Save score with account ID by directly setting storage property storage['highScore_' + accountId] = Math.max(storage['highScore_' + accountId] || 0, currentScore); // Also save player name if available if (!storage['playerName_' + accountId]) { storage['playerName_' + accountId] = storage.playerName || "Player"; } } else { // Fallback for users without account ID storage.highScore = Math.max(storage.highScore || 0, currentScore); } ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add scoretext to background2 to display new scores in it when the game is finished update the background2 with the new scores. ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add score texts in background2 for the new scores.
User prompt
Update scores in one background 'background2' when Button Highscore is clicked not in the screen that appears after win lose.
User prompt
Don't show scores by ID show it by name of the Upit account if logged in.
User prompt
Remove background2 from Highscore button
User prompt
Remove background2 from Highscore button
User prompt
Remove background2 from Highscore button
User prompt
Duplicate the same score of the new players in background of highescorebutton that is in the intro!
User prompt
Replace the background screen of score that appears after win/lose with background2 asset.
User prompt
Replace the background2 with background that appears after win/los.
User prompt
Use the background2 for showing scores don't create background before win or lose!
User prompt
Fix score background2 when click its button nothing showing there no scores of any player!
User prompt
Don't show scores only in the end background, show it in background2 of the HighScorebutton always update it as a list there.
User prompt
save the score that the player get at the end of the game in the Highscorebutton background2. add more score texts to save in it as slots below each others. ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Don't clear the list of scores save it forever, if the same player do new highscore remove its old one and replace the highest one. Show all new scores when the button of highscore is clicked.
User prompt
I get 560 score and it's not in the list when i click highscorebutton add score texts by Id for the players that are already loggedin to their accounts of Upit.
User prompt
I get 560 score and it's not in the list when i click highscorebutton add score texts by Id for the players that are already loggedin to their accounts of Upit. ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
I get 560 score and it's not in the list when i click highscorebutton add score texts by Id for the players that are already loggedin to their accounts of Upit. ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
I get 560 score and it's not in the list when i click highscorebutton add score texts by Id for the players that are already loggedin to their accounts of Upit.
User prompt
I get 560 score and it's not in the list when i click highscorebutton add score texts by Id for the players that are already loggedin to their accounts of Upit.
User prompt
I did a new score but i didn't find it in background2 list of scores?
User prompt
Display any new score not just highest scores below each other in background2 as a list of scores of 20 player. ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Show the scores in background2 after the game is finished
/**** * 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; }); // Enemy Bullet Class var EnemyBullet = Container.expand(function (startX, startY, vx, vy) { var self = Container.call(this); var bulletGraphics = self.attachAsset('enemyBullet', { 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; }); // 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 if (gamePhase === 0) { // 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; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x101030 // Dark space blue background }); /**** * Game Code ****/ // LK.init.image('mountain', {width:150, height:250, id:'6819884bc4a0c8bae9e84ae0'}) // Removed - Replaced by top/bottom mountains // Minimalistic tween library which should be used for animations over time // Boss Laser Beam // Boss // Air Enemy (Placeholder shape) // Enemy Bullet // Ground Enemy // Mountain Obstacle (simplified) // Ground/Ceiling // Player UFO // Initialize assets used in this game. Scale them according to what is needed for the game. /**** * Assets * Assets are automatically created and loaded either dynamically during gameplay * or via static code analysis based on their usage in the code. ****/ // Game State 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 = []; // 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; // 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) { // 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) var bulletSpeed = Math.sqrt(vx * vx + vy * vy); // Set velocity components to target player vx = dx / distance * bulletSpeed; vy = dy / distance * bulletSpeed; } var bullet = new EnemyBullet(x, y, vx, vy); 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'); } // Helper function already moved above // Helper function to save highscore function saveHighscore(score) { // Get existing highscores or create empty array var highscores = []; try { // Try to parse the serialized highscores data if (storage && storage.available) { highscores = JSON.parse(storage.highscoresData || "[]"); } } catch (e) { console.log("Error parsing existing highscores:", e); highscores = []; } // Get player name and ID from Upit if available var playerName = "FRVR Player"; // Default fallback name var playerId = "guest"; // Default fallback ID // Check if we have user info available through Upit if (LK.profile && LK.profile.name) { playerName = LK.profile.name; // Use real player name } if (LK.profile && LK.profile.id) { playerId = LK.profile.id; // Use actual player ID for tracking playerName = playerName || "Player " + LK.profile.id.substring(0, 5); // Use ID fragment if no name } // Add new score entry with player ID and name for identification highscores.push({ id: playerId, name: playerName, score: score, date: Date.now() // Add timestamp for sorting by recency if needed }); // Sort highscores by score (descending), then by date (descending for ties) highscores.sort(function (a, b) { if (b.score === a.score) { return b.date - a.date; // Most recent date first for ties in score } return b.score - a.score; // Highest score first }); // Filter to keep only the highest score per player ID, up to 10 players total var uniquePlayerScores = []; var seenPlayerIds = {}; for (var i = 0; i < highscores.length; i++) { var entry = highscores[i]; if (!seenPlayerIds[entry.id]) { seenPlayerIds[entry.id] = true; uniquePlayerScores.push(entry); if (uniquePlayerScores.length >= 10) break; // Keep only top 10 unique players } } highscores = uniquePlayerScores; // If we have user ID, also store their personal best separately if (playerId !== "guest" && storage && storage.available) { var personalKey = "player_" + playerId; var currentBest = 0; try { if (storage[personalKey]) { currentBest = parseInt(storage[personalKey], 10) || 0; } } catch (e) { console.log("Error retrieving personal best:", e); } if (score > currentBest) { storage[personalKey] = score.toString(); } } // Save back to storage - serialize scores to avoid complex objects // The storage only supports literal values and simple structures // Convert highscores array to string to ensure compatibility try { // Convert the complex object to a JSON string var highscoresString = JSON.stringify(highscores); // Store the string representation storage.highscoresData = highscoresString; // Save to Upit account if player is logged in if (storage && storage.available) { storage.save('highscores', highscoresString); } // When retrieving it, we'll need to parse it back - modify the retrieval in showHighscore function } catch (e) { console.log("Error saving highscores:", e); } } // 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(LK.getScore()); // 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() { // 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 = []; } // Sort highscores by score (descending) highscores.sort(function (a, b) { if (b.score === a.score) { return b.date - a.date; // Most recent date first for ties in score } return b.score - a.score; }); // Get current player ID var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest"; 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; } } catch (e) { console.log("Error retrieving personal best:", e); } } // Create background var scoreBackground = 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); // Current score var currentScoreText = new Text2("Your Score: " + LK.getScore(), { size: 100, fill: 0x00FF00 }); currentScoreText.anchor.set(0.5, 0); currentScoreText.x = 2048 / 2; currentScoreText.y = 350; game.addChild(currentScoreText); // Show personal best if it exists if (personalBest > 0 && personalBest !== LK.getScore()) { 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); // Show new record indicator if (LK.getScore() > 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); } } // Display top 5 highscores var maxToShow = Math.min(highscores.length, 5); if (maxToShow > 0) { // Display title for scores var titleScoreText = new Text2("Global Leaderboard", { size: 90, fill: 0xFFD700 }); titleScoreText.anchor.set(0.5, 0); titleScoreText.x = 2048 / 2; titleScoreText.y = 700; game.addChild(titleScoreText); // Display player scores for (var i = 0; i < maxToShow; i++) { var entry = highscores[i]; var isCurrentPlayer = entry.id === currentPlayerId; // 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: 80, fill: isCurrentPlayer ? "#00FF00" : i === 0 ? "#FFD700" : "#FFFFFF" // Green for current player }); scoreEntry.anchor.set(0.5, 0); scoreEntry.x = 2048 / 2; scoreEntry.y = 800 + i * 150; game.addChild(scoreEntry); } } else { // No scores message var noScoresText = new Text2("No global scores yet!", { size: 80, fill: 0xFFFFFF }); noScoresText.anchor.set(0.5, 0); noScoresText.x = 2048 / 2; noScoresText.y = 800; game.addChild(noScoresText); } } // 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 already showing highscores, hide them and return to intro if (showingHighscores) { // Remove highscore display if (highscoreBackground) { highscoreBackground.destroy(); highscoreBackground = null; } // Remove all highscore texts highscoreTexts.forEach(function (text) { text.destroy(); }); highscoreTexts = []; showingHighscores = false; return; } // Show custom highscore screen showingHighscores = true; // 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.85 // Slightly more transparent for better readability })); // Create highscore title var titleText = new Text2("HIGHSCORES", { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0); titleText.x = 2048 / 2; titleText.y = 200; game.addChild(titleText); highscoreTexts.push(titleText); // Add personal best section if player is logged in if (LK.profile && LK.profile.id) { var personalKey = "player_" + LK.profile.id; var personalBest = 0; try { if (storage && storage.available && storage[personalKey]) { personalBest = parseInt(storage[personalKey], 10) || 0; } } catch (e) { console.log("Error retrieving personal best:", e); } if (personalBest > 0) { var personalTitle = new Text2("YOUR BEST SCORE", { size: 90, fill: 0x00FF00 }); personalTitle.anchor.set(0.5, 0); personalTitle.x = 2048 / 2; personalTitle.y = 350; game.addChild(personalTitle); highscoreTexts.push(personalTitle); var personalScore = new Text2(personalBest.toString(), { size: 100, fill: 0xFFD700 }); personalScore.anchor.set(0.5, 0); personalScore.x = 2048 / 2; personalScore.y = 450; game.addChild(personalScore); highscoreTexts.push(personalScore); } } // Get highscores from storage or use default ones if none exist var highscores = []; try { // Try to load from Upit account storage if available if (storage && storage.available) { // First try local storage data immediately if (storage.highscoresData) { try { highscores = JSON.parse(storage.highscoresData); displayHighscores(highscores); // Display immediately from local storage } catch (localErr) { console.log("Error parsing local highscores:", localErr); displayHighscores([]); // Display empty scores if error } } else { displayHighscores([]); // Display empty scores if no data } // Then try to load from cloud storage (will update if available) storage.load('highscores', function (savedScores) { if (savedScores) { try { var cloudScores = JSON.parse(savedScores); storage.highscoresData = savedScores; // Update local reference // Clear existing scores display highscoreTexts.forEach(function (text) { text.destroy(); }); highscoreTexts = []; // Keep title text var titleText = new Text2("HIGHSCORES", { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0); titleText.x = 2048 / 2; titleText.y = 200; game.addChild(titleText); highscoreTexts.push(titleText); // Display cloud scores displayHighscores(cloudScores); } catch (parseErr) { console.log("Error parsing highscores from Upit:", parseErr); } } else if (highscores.length === 0) { // If no cloud scores and we haven't displayed local scores displayHighscores([]); } }); } else { // Try to parse the serialized highscores data if (storage.highscoresData) { highscores = JSON.parse(storage.highscoresData); } displayHighscores(highscores); } } catch (e) { console.log("Error parsing highscores:", e); displayHighscores([]); } // Helper function to display highscores function displayHighscores(highscores) { // Sort highscores by score (descending) highscores.sort(function (a, b) { if (b.score === a.score) { return b.date - a.date; // For tied scores, show most recent first } return b.score - a.score; }); // Check if current player ID exists var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest"; // Get title Y position (adjust if personal best was shown) var titleY = 600; var startY = 700; // Display title for scores var titleScoreText = new Text2("Global High Scores", { size: 90, fill: 0xFFD700 }); titleScoreText.anchor.set(0.5, 0); titleScoreText.x = 2048 / 2; titleScoreText.y = titleY; game.addChild(titleScoreText); highscoreTexts.push(titleScoreText); // Display top 10 highscores var maxToShow = Math.min(highscores.length, 10); // Display player scores for (var i = 0; i < maxToShow; i++) { var entry = highscores[i]; var isCurrentPlayer = entry.id === currentPlayerId; // Get player name, using ID if name is missing var playerName = entry.name || "Player " + entry.id.substring(0, 5); // Format score with comma separators for readability var formattedScore = entry.score.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); var scoreEntry = new Text2(i + 1 + ". " + playerName + ": " + formattedScore, { size: 80, fill: isCurrentPlayer ? "#00FF00" : i === 0 ? "#FFD700" : "#FFFFFF" // Green for current player, Gold for highest score }); scoreEntry.anchor.set(0.5, 0); scoreEntry.x = 2048 / 2; scoreEntry.y = startY + i * 150; game.addChild(scoreEntry); highscoreTexts.push(scoreEntry); } // If no scores to show if (maxToShow === 0) { var noScoresText = new Text2("No high scores yet!", { size: 80, fill: 0xFFFFFF }); noScoresText.anchor.set(0.5, 0); noScoresText.x = 2048 / 2; noScoresText.y = startY; game.addChild(noScoresText); highscoreTexts.push(noScoresText); } } // The createScoreBackground function has been moved earlier in the code // Add back button text var backText = new Text2("TAP ANYWHERE TO RETURN", { size: 70, fill: 0xFFFFFF }); backText.anchor.set(0.5, 0); backText.x = 2048 / 2; backText.y = 2200; game.addChild(backText); highscoreTexts.push(backText); // Check if there's a LK API for showing leaderboard as backup if (typeof LK.showLeaderboard === 'function') { // Add fallback to standard leaderboard LK.setTimeout(function () { if (showingHighscores) { // If our custom screen hasn't been dismissed after 5 seconds, show official leaderboard showingHighscores = false; if (highscoreBackground) { highscoreBackground.destroy(); highscoreBackground = null; } highscoreTexts.forEach(function (text) { text.destroy(); }); highscoreTexts = []; LK.showLeaderboard(); } }, 5000); } } // 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) { // 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; // 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()); } // --- 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 } } // --- Phase 1: Terrain Logic --- if (gamePhase === 0) { // 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 before showing You Win saveHighscore(LK.getScore()); // Create background for scores before showing you win createScoreBackground(); // Delay showing YouWin slightly to allow sound to play LK.setTimeout(function () { LK.showYouWin(); }, 500); // Adjust delay as needed for sound length return; // Stop update processing } break; // Bullet is destroyed after hitting boss } } } // 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;
});
// Enemy Bullet Class
var EnemyBullet = Container.expand(function (startX, startY, vx, vy) {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('enemyBullet', {
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;
});
// 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
if (gamePhase === 0) {
// 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;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x101030 // Dark space blue background
});
/****
* Game Code
****/
// LK.init.image('mountain', {width:150, height:250, id:'6819884bc4a0c8bae9e84ae0'}) // Removed - Replaced by top/bottom mountains
// Minimalistic tween library which should be used for animations over time
// Boss Laser Beam
// Boss
// Air Enemy (Placeholder shape)
// Enemy Bullet
// Ground Enemy
// Mountain Obstacle (simplified)
// Ground/Ceiling
// Player UFO
// Initialize assets used in this game. Scale them according to what is needed for the game.
/****
* Assets
* Assets are automatically created and loaded either dynamically during gameplay
* or via static code analysis based on their usage in the code.
****/
// Game State
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 = [];
// 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;
// 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) {
// 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)
var bulletSpeed = Math.sqrt(vx * vx + vy * vy);
// Set velocity components to target player
vx = dx / distance * bulletSpeed;
vy = dy / distance * bulletSpeed;
}
var bullet = new EnemyBullet(x, y, vx, vy);
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');
}
// Helper function already moved above
// Helper function to save highscore
function saveHighscore(score) {
// Get existing highscores or create empty array
var highscores = [];
try {
// Try to parse the serialized highscores data
if (storage && storage.available) {
highscores = JSON.parse(storage.highscoresData || "[]");
}
} catch (e) {
console.log("Error parsing existing highscores:", e);
highscores = [];
}
// Get player name and ID from Upit if available
var playerName = "FRVR Player"; // Default fallback name
var playerId = "guest"; // Default fallback ID
// Check if we have user info available through Upit
if (LK.profile && LK.profile.name) {
playerName = LK.profile.name; // Use real player name
}
if (LK.profile && LK.profile.id) {
playerId = LK.profile.id; // Use actual player ID for tracking
playerName = playerName || "Player " + LK.profile.id.substring(0, 5); // Use ID fragment if no name
}
// Add new score entry with player ID and name for identification
highscores.push({
id: playerId,
name: playerName,
score: score,
date: Date.now() // Add timestamp for sorting by recency if needed
});
// Sort highscores by score (descending), then by date (descending for ties)
highscores.sort(function (a, b) {
if (b.score === a.score) {
return b.date - a.date; // Most recent date first for ties in score
}
return b.score - a.score; // Highest score first
});
// Filter to keep only the highest score per player ID, up to 10 players total
var uniquePlayerScores = [];
var seenPlayerIds = {};
for (var i = 0; i < highscores.length; i++) {
var entry = highscores[i];
if (!seenPlayerIds[entry.id]) {
seenPlayerIds[entry.id] = true;
uniquePlayerScores.push(entry);
if (uniquePlayerScores.length >= 10) break; // Keep only top 10 unique players
}
}
highscores = uniquePlayerScores;
// If we have user ID, also store their personal best separately
if (playerId !== "guest" && storage && storage.available) {
var personalKey = "player_" + playerId;
var currentBest = 0;
try {
if (storage[personalKey]) {
currentBest = parseInt(storage[personalKey], 10) || 0;
}
} catch (e) {
console.log("Error retrieving personal best:", e);
}
if (score > currentBest) {
storage[personalKey] = score.toString();
}
}
// Save back to storage - serialize scores to avoid complex objects
// The storage only supports literal values and simple structures
// Convert highscores array to string to ensure compatibility
try {
// Convert the complex object to a JSON string
var highscoresString = JSON.stringify(highscores);
// Store the string representation
storage.highscoresData = highscoresString;
// Save to Upit account if player is logged in
if (storage && storage.available) {
storage.save('highscores', highscoresString);
}
// When retrieving it, we'll need to parse it back - modify the retrieval in showHighscore function
} catch (e) {
console.log("Error saving highscores:", e);
}
}
// 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(LK.getScore());
// 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() {
// 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 = [];
}
// Sort highscores by score (descending)
highscores.sort(function (a, b) {
if (b.score === a.score) {
return b.date - a.date; // Most recent date first for ties in score
}
return b.score - a.score;
});
// Get current player ID
var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest";
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;
}
} catch (e) {
console.log("Error retrieving personal best:", e);
}
}
// Create background
var scoreBackground = 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);
// Current score
var currentScoreText = new Text2("Your Score: " + LK.getScore(), {
size: 100,
fill: 0x00FF00
});
currentScoreText.anchor.set(0.5, 0);
currentScoreText.x = 2048 / 2;
currentScoreText.y = 350;
game.addChild(currentScoreText);
// Show personal best if it exists
if (personalBest > 0 && personalBest !== LK.getScore()) {
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);
// Show new record indicator
if (LK.getScore() > 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);
}
}
// Display top 5 highscores
var maxToShow = Math.min(highscores.length, 5);
if (maxToShow > 0) {
// Display title for scores
var titleScoreText = new Text2("Global Leaderboard", {
size: 90,
fill: 0xFFD700
});
titleScoreText.anchor.set(0.5, 0);
titleScoreText.x = 2048 / 2;
titleScoreText.y = 700;
game.addChild(titleScoreText);
// Display player scores
for (var i = 0; i < maxToShow; i++) {
var entry = highscores[i];
var isCurrentPlayer = entry.id === currentPlayerId;
// 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: 80,
fill: isCurrentPlayer ? "#00FF00" : i === 0 ? "#FFD700" : "#FFFFFF" // Green for current player
});
scoreEntry.anchor.set(0.5, 0);
scoreEntry.x = 2048 / 2;
scoreEntry.y = 800 + i * 150;
game.addChild(scoreEntry);
}
} else {
// No scores message
var noScoresText = new Text2("No global scores yet!", {
size: 80,
fill: 0xFFFFFF
});
noScoresText.anchor.set(0.5, 0);
noScoresText.x = 2048 / 2;
noScoresText.y = 800;
game.addChild(noScoresText);
}
}
// 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 already showing highscores, hide them and return to intro
if (showingHighscores) {
// Remove highscore display
if (highscoreBackground) {
highscoreBackground.destroy();
highscoreBackground = null;
}
// Remove all highscore texts
highscoreTexts.forEach(function (text) {
text.destroy();
});
highscoreTexts = [];
showingHighscores = false;
return;
}
// Show custom highscore screen
showingHighscores = true;
// 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.85 // Slightly more transparent for better readability
}));
// Create highscore title
var titleText = new Text2("HIGHSCORES", {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0);
titleText.x = 2048 / 2;
titleText.y = 200;
game.addChild(titleText);
highscoreTexts.push(titleText);
// Add personal best section if player is logged in
if (LK.profile && LK.profile.id) {
var personalKey = "player_" + LK.profile.id;
var personalBest = 0;
try {
if (storage && storage.available && storage[personalKey]) {
personalBest = parseInt(storage[personalKey], 10) || 0;
}
} catch (e) {
console.log("Error retrieving personal best:", e);
}
if (personalBest > 0) {
var personalTitle = new Text2("YOUR BEST SCORE", {
size: 90,
fill: 0x00FF00
});
personalTitle.anchor.set(0.5, 0);
personalTitle.x = 2048 / 2;
personalTitle.y = 350;
game.addChild(personalTitle);
highscoreTexts.push(personalTitle);
var personalScore = new Text2(personalBest.toString(), {
size: 100,
fill: 0xFFD700
});
personalScore.anchor.set(0.5, 0);
personalScore.x = 2048 / 2;
personalScore.y = 450;
game.addChild(personalScore);
highscoreTexts.push(personalScore);
}
}
// Get highscores from storage or use default ones if none exist
var highscores = [];
try {
// Try to load from Upit account storage if available
if (storage && storage.available) {
// First try local storage data immediately
if (storage.highscoresData) {
try {
highscores = JSON.parse(storage.highscoresData);
displayHighscores(highscores); // Display immediately from local storage
} catch (localErr) {
console.log("Error parsing local highscores:", localErr);
displayHighscores([]); // Display empty scores if error
}
} else {
displayHighscores([]); // Display empty scores if no data
}
// Then try to load from cloud storage (will update if available)
storage.load('highscores', function (savedScores) {
if (savedScores) {
try {
var cloudScores = JSON.parse(savedScores);
storage.highscoresData = savedScores; // Update local reference
// Clear existing scores display
highscoreTexts.forEach(function (text) {
text.destroy();
});
highscoreTexts = [];
// Keep title text
var titleText = new Text2("HIGHSCORES", {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0);
titleText.x = 2048 / 2;
titleText.y = 200;
game.addChild(titleText);
highscoreTexts.push(titleText);
// Display cloud scores
displayHighscores(cloudScores);
} catch (parseErr) {
console.log("Error parsing highscores from Upit:", parseErr);
}
} else if (highscores.length === 0) {
// If no cloud scores and we haven't displayed local scores
displayHighscores([]);
}
});
} else {
// Try to parse the serialized highscores data
if (storage.highscoresData) {
highscores = JSON.parse(storage.highscoresData);
}
displayHighscores(highscores);
}
} catch (e) {
console.log("Error parsing highscores:", e);
displayHighscores([]);
}
// Helper function to display highscores
function displayHighscores(highscores) {
// Sort highscores by score (descending)
highscores.sort(function (a, b) {
if (b.score === a.score) {
return b.date - a.date; // For tied scores, show most recent first
}
return b.score - a.score;
});
// Check if current player ID exists
var currentPlayerId = LK.profile && LK.profile.id ? LK.profile.id : "guest";
// Get title Y position (adjust if personal best was shown)
var titleY = 600;
var startY = 700;
// Display title for scores
var titleScoreText = new Text2("Global High Scores", {
size: 90,
fill: 0xFFD700
});
titleScoreText.anchor.set(0.5, 0);
titleScoreText.x = 2048 / 2;
titleScoreText.y = titleY;
game.addChild(titleScoreText);
highscoreTexts.push(titleScoreText);
// Display top 10 highscores
var maxToShow = Math.min(highscores.length, 10);
// Display player scores
for (var i = 0; i < maxToShow; i++) {
var entry = highscores[i];
var isCurrentPlayer = entry.id === currentPlayerId;
// Get player name, using ID if name is missing
var playerName = entry.name || "Player " + entry.id.substring(0, 5);
// Format score with comma separators for readability
var formattedScore = entry.score.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
var scoreEntry = new Text2(i + 1 + ". " + playerName + ": " + formattedScore, {
size: 80,
fill: isCurrentPlayer ? "#00FF00" : i === 0 ? "#FFD700" : "#FFFFFF" // Green for current player, Gold for highest score
});
scoreEntry.anchor.set(0.5, 0);
scoreEntry.x = 2048 / 2;
scoreEntry.y = startY + i * 150;
game.addChild(scoreEntry);
highscoreTexts.push(scoreEntry);
}
// If no scores to show
if (maxToShow === 0) {
var noScoresText = new Text2("No high scores yet!", {
size: 80,
fill: 0xFFFFFF
});
noScoresText.anchor.set(0.5, 0);
noScoresText.x = 2048 / 2;
noScoresText.y = startY;
game.addChild(noScoresText);
highscoreTexts.push(noScoresText);
}
}
// The createScoreBackground function has been moved earlier in the code
// Add back button text
var backText = new Text2("TAP ANYWHERE TO RETURN", {
size: 70,
fill: 0xFFFFFF
});
backText.anchor.set(0.5, 0);
backText.x = 2048 / 2;
backText.y = 2200;
game.addChild(backText);
highscoreTexts.push(backText);
// Check if there's a LK API for showing leaderboard as backup
if (typeof LK.showLeaderboard === 'function') {
// Add fallback to standard leaderboard
LK.setTimeout(function () {
if (showingHighscores) {
// If our custom screen hasn't been dismissed after 5 seconds, show official leaderboard
showingHighscores = false;
if (highscoreBackground) {
highscoreBackground.destroy();
highscoreBackground = null;
}
highscoreTexts.forEach(function (text) {
text.destroy();
});
highscoreTexts = [];
LK.showLeaderboard();
}
}, 5000);
}
}
// 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) {
// 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;
// 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());
}
// --- 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
}
}
// --- Phase 1: Terrain Logic ---
if (gamePhase === 0) {
// 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 before showing You Win
saveHighscore(LK.getScore());
// Create background for scores before showing you win
createScoreBackground();
// Delay showing YouWin slightly to allow sound to play
LK.setTimeout(function () {
LK.showYouWin();
}, 500); // Adjust delay as needed for sound length
return; // Stop update processing
}
break; // Bullet is destroyed after hitting boss
}
}
} // 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