/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { coins: 0, claimed: {} }); /**** * Classes ****/ // FlyingObstacle class (flying block for wave mode) var FlyingObstacle = Container.expand(function () { var self = Container.call(this); var obsSprite = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 28; self.amplitude = 350 + Math.random() * 250; self.frequency = 0.0015 + Math.random() * 0.001; self.baseY = 1366 + (Math.random() - 0.5) * 900; self.phase = Math.random() * Math.PI * 2; self.update = function () { self.x -= self.speed; self.y = self.baseY + Math.cos(self.x * self.frequency + self.phase) * self.amplitude; }; return self; }); // Obstacle class (tall block) var Obstacle = Container.expand(function () { var self = Container.call(this); var obsSprite = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 1 }); self.speed = 22; self.update = function () { self.x -= self.speed; }; return self; }); // Player class var Player = Container.expand(function () { var self = Container.call(this); // Use image asset for red, green, or gold skin, otherwise use shape asset and tint for other skins var playerSprite; if (storage.selectedSkin === "skin_red") { playerSprite = self.attachAsset('Red', { anchorX: 0.5, anchorY: 1 }); } else if (storage.selectedSkin === "skin_green") { playerSprite = self.attachAsset('Green', { anchorX: 0.5, anchorY: 1 }); } else if (storage.selectedSkin === "skin_gold") { playerSprite = self.attachAsset('Gold', { anchorX: 0.5, anchorY: 1 }); } else { playerSprite = self.attachAsset('player', { anchorX: 0.5, anchorY: 1 }); // Apply skin color if selected and not red, green, or gold var skinColors = { "skin_red": 0xe74c3c, "skin_green": 0x2ecc71, "skin_gold": 0xf1c40f }; if (storage.selectedSkin && skinColors[storage.selectedSkin] && storage.selectedSkin !== "skin_red" && storage.selectedSkin !== "skin_green" && storage.selectedSkin !== "skin_gold") { playerSprite.color = skinColors[storage.selectedSkin]; } } // Physics self.vy = 0; self.isJumping = false; self.gravity = 2.2; self.jumpStrength = -48; self.groundY = 0; // Set after ground is created self.isWave = false; // Always initialize wave mode state // Update method self.update = function () { if (self.isWave) { // Wave mode: apply constant gravity, but allow upward thrust on press self.vy += 3.2; // slightly higher gravity for wave feel self.y += self.vy; // Clamp to screen bounds (not off top or bottom) if (self.y < 60) { self.y = 60; self.vy = 0; } if (self.y > 2732 - 60) { self.y = 2732 - 60; self.vy = 0; } } else { // Normal mode self.vy += self.gravity; self.y += self.vy; // Ground collision if (self.y > self.groundY) { self.y = self.groundY; self.vy = 0; self.isJumping = false; } } }; // Jump method self.jump = function () { if (!self.isJumping && self.y >= self.groundY) { self.vy = self.jumpStrength; self.isJumping = true; // Start 360-degree rotation tween on playerSprite tween(playerSprite, { rotation: playerSprite.rotation + Math.PI * 2 }, { duration: 400, easing: tween.linear }); } }; return self; }); // Spike class (obstacle) var Spike = Container.expand(function () { var self = Container.call(this); var spikeSprite = self.attachAsset('spike', { anchorX: 0.5, anchorY: 1 }); self.speed = 22; // Moves left self.update = function () { self.x -= self.speed; }; return self; }); // WaveSpike class (flying spike for wave mode) var WaveSpike = Container.expand(function () { var self = Container.call(this); var spikeSprite = self.attachAsset('spike', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 28; self.amplitude = 400 + Math.random() * 300; // vertical wave amplitude self.frequency = 0.002 + Math.random() * 0.0015; // wave frequency self.baseY = 1366 + (Math.random() - 0.5) * 900; // center Y self.phase = Math.random() * Math.PI * 2; self.update = function () { self.x -= self.speed; self.y = self.baseY + Math.sin(self.x * self.frequency + self.phase) * self.amplitude; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222831 }); /**** * Game Code ****/ // Player: blue square // Ground: gray rectangle // Spike: red triangle (approximate with a tall, thin box for MVP) // Obstacle: purple rectangle var LEVELS = [{ name: "First Steps", difficulty: "Easy", color: 0x2ecc71, minGap: 500, maxGap: 900, types: ['spike', 'obstacle', 'spike'], intro: "Welcome to your first steps!" }, { name: "Erin's Adventure", difficulty: "Medium", color: 0xf1c40f, minGap: 500, maxGap: 900, types: ['spike', 'obstacle', 'spike'], intro: "A trickier journey awaits." }, { name: "Final Steps", difficulty: "Hard", color: 0xe74c3c, minGap: 500, maxGap: 900, types: ['spike', 'obstacle', 'spike'], intro: "Only the brave survive!" }, { name: "Erin’s atventure continuation", difficulty: "Hard", color: 0x8e44ad, minGap: 320, maxGap: 480, types: ['spike', 'spike', 'obstacle', 'spike', 'spike'], intro: "The spikes are closer than ever. Time your jumps perfectly!" }, { name: "Dash", difficulty: "Extreme", color: 0x16a085, minGap: 220, maxGap: 350, types: ['spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike'], intro: "Dash through a gauntlet of spikes! Can you survive?" }, { name: "Secret", difficulty: "Demon", color: 0x000000, minGap: 220, maxGap: 350, types: ['spike', 'obstacle', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'obstacle'], intro: "The secret Demon level. Only the best will survive. Prepare for the hardest challenge!" }]; // --- MENU STATE --- var MENU_STATE = { MAIN: 0, LEVEL_SELECT: 1, PLAYING: 2 }; var menuState = MENU_STATE.MAIN; var selectedLevel = 0; // --- UI ELEMENTS --- var menuContainer = new Container(); var levelSelectContainer = new Container(); var introText = null; // --- GAME CONSTANTS --- var GROUND_HEIGHT = 120; var PLAYER_SIZE = 120; var PLAYER_START_X = 400; var OBSTACLE_MIN_GAP = 600; var OBSTACLE_MAX_GAP = 1100; var OBSTACLE_TYPES = ['spike', 'obstacle']; var OBSTACLE_Y = 2732 - GROUND_HEIGHT; // --- GAME STATE --- var player; var ground; var obstacles = []; var score = 0; var scoreTxt; var lastObstacleX = 0; var isGameOver = false; // --- WAVE MODE STATE (Dash level only) --- var waveMode = false; // true if in wave mode var waveObstacles = []; // flying spikes/obstacles in wave mode // --- UI HELPERS --- function clearMenuUI() { if (menuContainer.parent) { menuContainer.parent.removeChild(menuContainer); } if (levelSelectContainer.parent) { levelSelectContainer.parent.removeChild(levelSelectContainer); } if (introText && introText.parent) { introText.parent.removeChild(introText); } } // --- MAIN MENU --- function showMainMenu() { clearMenuUI(); menuState = MENU_STATE.MAIN; menuContainer.removeChildren(); // Title var title = new Text2("Geometry Dash", { size: 180, fill: 0xFFFFFF, fontWeight: "bold", stroke: 0x000000, strokeThickness: 12 }); title.anchor.set(0.5, 0); title.x = 2048 / 2; title.y = 400; menuContainer.addChild(title); // Play button var playBtn = new Text2("Play", { size: 140, fill: 0x2ECC71, fontWeight: "bold", stroke: 0x000000, strokeThickness: 10 }); playBtn.anchor.set(0.5, 0.5); playBtn.x = 2048 / 2; playBtn.y = 1000; playBtn.interactive = true; playBtn.buttonMode = true; playBtn.down = function () { showLevelSelect(); }; menuContainer.addChild(playBtn); // Shop button var shopBtn = new Text2("Shop", { size: 120, fill: 0xf1c40f, fontWeight: "bold", stroke: 0x000000, strokeThickness: 8 }); shopBtn.anchor.set(0.5, 0.5); shopBtn.x = 2048 / 2; shopBtn.y = 1200; shopBtn.interactive = true; shopBtn.buttonMode = true; shopBtn.down = function () { showShop(); }; menuContainer.addChild(shopBtn); // Add to game game.addChild(menuContainer); // Remove 'Back to Main Menu' button if present if (game.goBackBtn && game.goBackBtn.parent) { game.goBackBtn.parent.removeChild(game.goBackBtn); game.goBackBtn.visible = false; } // Hide score text on main menu if (scoreTxt) { scoreTxt.visible = false; // Always ensure scoreTxt is in GUI if (!scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } } // Always ensure goBackBtn is in GUI and hidden if (game.goBackBtn && !game.goBackBtn.parent) { LK.gui.topRight.addChild(game.goBackBtn); } game.goBackBtn.visible = false; } // --- SHOP --- var shopContainer = new Container(); var SHOP_ITEMS = [{ id: "skin_red", name: "Red Skin", price: 30, color: 0xe74c3c }, { id: "skin_green", name: "Green Skin", price: 30, color: 0x2ecc71 }, { id: "skin_gold", name: "Gold Skin", price: 100, color: 0xf1c40f }]; function showShop() { clearMenuUI(); menuState = MENU_STATE.MAIN; shopContainer.removeChildren(); // Title var shopTitle = new Text2("Shop", { size: 150, fill: 0xFFFFFF, fontWeight: "bold", stroke: 0x000000, strokeThickness: 10 }); shopTitle.anchor.set(0.5, 0); shopTitle.x = 2048 / 2; shopTitle.y = 300; shopContainer.addChild(shopTitle); // Coin display var coins = storage.coins || 0; var coinTxt = new Text2("Coins: " + coins, { size: 100, fill: 0xf1c40f, fontWeight: "bold", stroke: 0x000000, strokeThickness: 7 }); coinTxt.anchor.set(0.5, 0); coinTxt.x = 2048 / 2; coinTxt.y = 500; shopContainer.addChild(coinTxt); // List items for (var i = 0; i < SHOP_ITEMS.length; i++) { (function (idx) { var item = SHOP_ITEMS[idx]; var owned = storage["owned_" + item.id] === true; var y = 700 + idx * 220; var itemTxt = new Text2(item.name, { size: 100, fill: "#" + item.color.toString(16).padStart(6, "0"), fontWeight: "bold", stroke: 0x000000, strokeThickness: 7 }); itemTxt.anchor.set(0, 0.5); itemTxt.x = 400; itemTxt.y = y; shopContainer.addChild(itemTxt); var priceTxt = new Text2(owned ? "Owned" : item.price + " coins", { size: 90, fill: owned ? 0x2ecc71 : 0xffffff, fontWeight: "bold", stroke: 0x000000, strokeThickness: 6 }); priceTxt.anchor.set(0.5, 0.5); priceTxt.x = 1200; priceTxt.y = y; shopContainer.addChild(priceTxt); // Buy/select button var btnTxt = owned ? storage.selectedSkin === item.id ? "Selected" : "Select" : "Buy"; var btn = new Text2(btnTxt, { size: 90, fill: owned ? storage.selectedSkin === item.id ? 0x2ecc71 : 0x3498db : 0xf1c40f, fontWeight: "bold", stroke: 0x000000, strokeThickness: 6 }); btn.anchor.set(0.5, 0.5); btn.x = 1700; btn.y = y; btn.interactive = true; btn.buttonMode = true; btn.down = function () { if (owned) { // Select skin storage.selectedSkin = item.id; // If player exists, update its color immediately if (player && player.childAt && typeof player.childAt === "function") { // Remove old sprite var oldSprite = player.childAt(0); if (oldSprite) { player.removeChild(oldSprite); } var newSprite; if (item.id === "skin_red") { newSprite = player.attachAsset('Red', { anchorX: 0.5, anchorY: 1 }); } else if (item.id === "skin_green") { newSprite = player.attachAsset('Green', { anchorX: 0.5, anchorY: 1 }); } else if (item.id === "skin_gold") { newSprite = player.attachAsset('Gold', { anchorX: 0.5, anchorY: 1 }); } else { newSprite = player.attachAsset('player', { anchorX: 0.5, anchorY: 1 }); var skinColors = { "skin_red": 0xe74c3c, "skin_green": 0x2ecc71, "skin_gold": 0xf1c40f }; if (skinColors[item.id] && item.id !== "skin_red" && item.id !== "skin_green" && item.id !== "skin_gold") { newSprite.color = skinColors[item.id]; } } } showShop(); } else { // Try to buy var coins = storage.coins || 0; if (coins >= item.price) { storage.coins = coins - item.price; storage["owned_" + item.id] = true; storage.selectedSkin = item.id; showShop(); } else { priceTxt.setText("Not enough!"); priceTxt.fill = 0xe74c3c; } } }; shopContainer.addChild(btn); })(i); } // Back button var backBtn = new Text2("Back", { size: 90, fill: 0x888888, fontWeight: "bold", stroke: 0x000000, strokeThickness: 6 }); backBtn.anchor.set(0.5, 0.5); backBtn.x = 2048 / 2; backBtn.y = 700 + SHOP_ITEMS.length * 220 + 100; backBtn.interactive = true; backBtn.buttonMode = true; backBtn.down = function () { // Remove shop UI before showing main menu if (shopContainer.parent) { shopContainer.parent.removeChild(shopContainer); } showMainMenu(); }; shopContainer.addChild(backBtn); game.addChild(shopContainer); // Always ensure scoreTxt is in GUI and hidden if (scoreTxt && !scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } scoreTxt.visible = false; // Always ensure goBackBtn is in GUI and hidden if (game.goBackBtn && !game.goBackBtn.parent) { LK.gui.topRight.addChild(game.goBackBtn); } game.goBackBtn.visible = false; } // --- LEVEL SELECT MENU --- function showLevelSelect() { clearMenuUI(); menuState = MENU_STATE.LEVEL_SELECT; levelSelectContainer.removeChildren(); // Title var selTitle = new Text2("Select Level", { size: 150, fill: 0xFFFFFF, fontWeight: "bold", stroke: 0x000000, strokeThickness: 10 }); selTitle.anchor.set(0.5, 0); selTitle.x = 2048 / 2; selTitle.y = 300; levelSelectContainer.addChild(selTitle); // Level buttons for (var i = 0; i < LEVELS.length; i++) { (function (idx) { var lvl = LEVELS[idx]; var btn = new Text2(lvl.name + " (" + lvl.difficulty + ")", { size: 110, fill: "#" + lvl.color.toString(16).padStart(6, "0"), fontWeight: "bold", stroke: 0x000000, strokeThickness: 8 }); btn.anchor.set(0.5, 0.5); btn.x = 2048 / 2; btn.y = 600 + idx * 250; btn.interactive = true; btn.buttonMode = true; btn.down = function () { selectedLevel = idx; showLevelIntro(); }; levelSelectContainer.addChild(btn); // Insert "NEW UPDATES COMING SOON..." after Erin’s atventure continuation (index 3) if (idx === 3) { var updatesText = new Text2("NEW UPDATES COMING SOON...", { size: 100, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 7 }); updatesText.anchor.set(0.5, 0.5); updatesText.x = 2048 / 2; updatesText.y = btn.y + 180; levelSelectContainer.addChild(updatesText); } // Highlight Secret Demon level in red and with a demon icon if desired if (idx === 5) { btn.setText(lvl.name + " (" + lvl.difficulty + " 😈)"); btn.fill = "#ff0000"; } })(i); } // Back button var backBtn = new Text2("Back", { size: 90, fill: 0x888888, fontWeight: "bold", stroke: 0x000000, strokeThickness: 6 }); backBtn.anchor.set(0.5, 0.5); backBtn.x = 2048 / 2; backBtn.y = 600 + LEVELS.length * 250 + 100; backBtn.interactive = true; backBtn.buttonMode = true; backBtn.down = function () { showMainMenu(); }; levelSelectContainer.addChild(backBtn); game.addChild(levelSelectContainer); // Remove 'Back to Main Menu' button if present if (game.goBackBtn && game.goBackBtn.parent) { game.goBackBtn.parent.removeChild(game.goBackBtn); game.goBackBtn.visible = false; } // Always ensure scoreTxt is in GUI and hidden if (scoreTxt && !scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } scoreTxt.visible = false; // Always ensure goBackBtn is in GUI and hidden if (game.goBackBtn && !game.goBackBtn.parent) { LK.gui.topRight.addChild(game.goBackBtn); } game.goBackBtn.visible = false; } // --- LEVEL INTRO --- function showLevelIntro() { clearMenuUI(); menuState = MENU_STATE.LEVEL_SELECT; var lvl = LEVELS[selectedLevel]; // Coin rewards per level var COIN_REWARDS = [20, 50, 100]; var rewardText = lvl.name + "\nDifficulty: " + lvl.difficulty + "\n\n" + lvl.intro + "\n\nTap to Start"; var showClaim = false; var coinsToClaim = 0; if (storage.claimed && storage.claimed[selectedLevel] === false && storage.lastCompletedLevel !== undefined && storage.lastCompletedLevel == selectedLevel) { // Player just beat this level and hasn't claimed coins yet showClaim = true; coinsToClaim = COIN_REWARDS[selectedLevel]; rewardText += "\n\n🎉 Level Complete! 🎉\nClaim +" + coinsToClaim + " coins!"; } introText = new Text2(rewardText, { size: 100, fill: "#" + lvl.color.toString(16).padStart(6, "0"), align: "center", fontWeight: "bold", stroke: 0x000000, strokeThickness: 7 }); introText.anchor.set(0.5, 0.5); introText.x = 2048 / 2; introText.y = 1200; introText.interactive = true; introText.buttonMode = true; introText.down = function () { if (showClaim) { return; } // Don't start level if claim is available startLevel(selectedLevel); }; game.addChild(introText); // Show claim button if eligible if (showClaim) { var claimBtn = new Text2("Claim +" + coinsToClaim + " coins", { size: 110, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 8 }); claimBtn.anchor.set(0.5, 0.5); claimBtn.x = 2048 / 2; claimBtn.y = introText.y + 350; claimBtn.interactive = true; claimBtn.buttonMode = true; claimBtn.down = function () { // Add coins and mark as claimed if (selectedLevel === 3) { storage.coins = (storage.coins || 0) + 150; } else { storage.coins = (storage.coins || 0) + coinsToClaim; } if (!storage.claimed) { storage.claimed = {}; } storage.claimed[selectedLevel] = true; // Remove lastCompletedLevel so claim can't be repeated delete storage.lastCompletedLevel; // Update UI claimBtn.setText("Claimed!"); claimBtn.interactive = false; claimBtn.buttonMode = false; }; game.addChild(claimBtn); } // Always show 'Back to Main Menu' button on every level intro var backToMenuBtn = new Text2("Back to Main Menu", { size: 90, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 6 }); // Anchor to center, place further down under "Tap to Start" and Claim button if present backToMenuBtn.anchor.set(0.5, 0.5); // If claimBtn is present, place below it, else below introText var btnY = introText.y + 250; // Find claimBtn if present var claimBtnY = null; var children = game.children ? game.children.slice() : []; for (var i = 0; i < children.length; i++) { if (children[i] && children[i].text && typeof children[i].text === "string" && children[i].text.indexOf("Claim") === 0) { claimBtnY = children[i].y; break; } } if (claimBtnY !== null) { btnY = claimBtnY + 250; } // Move button a little to the left (was +700, now +550) and even higher (was btnY - 200, then btnY - 350, then btnY - 500, then btnY - 650, then btnY - 800, then btnY - 950, then btnY - 1100, now btnY - 1250) backToMenuBtn.x = 2048 / 2 + 550; backToMenuBtn.y = btnY - 1250; backToMenuBtn.interactive = true; backToMenuBtn.buttonMode = true; backToMenuBtn.visible = true; backToMenuBtn.down = function () { // Remove all game children (reset game state) while (game.children && game.children.length > 0) { var child = game.children[0]; if (child && child.parent) { child.parent.removeChild(child); } } // Remove score text from GUI if present if (scoreTxt && scoreTxt.parent) { scoreTxt.parent.removeChild(scoreTxt); } // Remove goBackBtn from GUI if present if (game.goBackBtn && game.goBackBtn.parent) { game.goBackBtn.parent.removeChild(game.goBackBtn); game.goBackBtn.visible = false; } // Reset gameplay state variables isGameOver = false; menuState = MENU_STATE.MAIN; // Show main menu showMainMenu(); }; game.addChild(backToMenuBtn); // Remove 'Back to Main Menu' button if present (from gameplay) if (game.goBackBtn && game.goBackBtn.parent) { game.goBackBtn.parent.removeChild(game.goBackBtn); game.goBackBtn.visible = false; } // Hide score text on level intro if (scoreTxt) { scoreTxt.visible = false; // Always ensure scoreTxt is in GUI if (!scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } } // Always ensure goBackBtn is in GUI and hidden if (game.goBackBtn && !game.goBackBtn.parent) { LK.gui.topRight.addChild(game.goBackBtn); } game.goBackBtn.visible = false; } // --- START LEVEL --- function startLevel(levelIdx) { clearMenuUI(); menuState = MENU_STATE.PLAYING; // Set level params var lvl = LEVELS[levelIdx]; OBSTACLE_MIN_GAP = lvl.minGap; OBSTACLE_MAX_GAP = lvl.maxGap; OBSTACLE_TYPES = lvl.types; // Set background color to a slightly darker blue for First Steps level if (levelIdx === 0) { game.setBackgroundColor(0x2176ae); // Stop any music before playing Stereo Madness to ensure it starts LK.stopMusic(); LK.playMusic('stereo_madness'); } else { game.setBackgroundColor(0x222831); // Stop Stereo Madness if switching away from First Steps LK.stopMusic(); } // Reset game state resetGame(); // Show score score = 0; scoreTxt.setText("0"); scoreTxt.visible = true; // Show ground and player if (!ground) { ground = LK.getAsset('ground', { anchorX: 0, anchorY: 1, x: 0, y: 2732 }); game.addChild(ground); } else if (!ground.parent) { game.addChild(ground); } if (!player) { player = new Player(); player.groundY = 2732 - GROUND_HEIGHT; if (levelIdx === 4) { // Dash level: start in center for wave countdown player.x = 2048 / 2; player.y = 1366; } else if (levelIdx === 5) { // Secret Demon: start in center of screen, in air, always wave mode player.x = 2048 / 2; player.y = 1366; // center vertically player.vy = 0; player.isWave = true; } else { player.x = PLAYER_START_X; player.y = player.groundY; } game.addChild(player); } else if (!player.parent) { game.addChild(player); if (levelIdx === 4) { player.x = 2048 / 2; player.y = 1366; } else if (levelIdx === 5) { // Secret Demon: start in center, in air, always wave mode player.x = 2048 / 2; player.y = 700; player.vy = 0; player.isWave = true; } else { player.x = PLAYER_START_X; player.y = player.groundY; } } // --- Dash level: prepare for 5s countdown at 20 points (do not show countdown yet) --- if (levelIdx === 4) { // Set player to center for Dash level start, do not enable wave mode yet player.x = 2048 / 2; player.y = 1366; player.vy = 0; player.isWave = false; // Remove all obstacles for clean start for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; lastObstacleX = 1200 + 700; // --- Dash level: 5s countdown at start --- game._dashCountdownActive = true; var dashCountdown = 5; var dashCountdownText = new Text2(dashCountdown + "", { size: 220, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 12 }); dashCountdownText.anchor.set(0.5, 0.5); dashCountdownText.x = 2048 / 2; dashCountdownText.y = 900; game.addChild(dashCountdownText); var dashCountdownTimer = LK.setInterval(function () { dashCountdown--; if (dashCountdown > 0) { dashCountdownText.setText(dashCountdown + ""); } else { LK.clearInterval(dashCountdownTimer); if (dashCountdownText.parent) dashCountdownText.parent.removeChild(dashCountdownText); game._dashCountdownActive = false; // After countdown, enter wave mode waveMode = true; // Remove all ground obstacles for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; // Move player to center Y for wave mode (start in mid air at y=700) player.y = 700; player.vy = 0; // Change player sprite to wave asset if (player.childAt && typeof player.childAt === "function") { var oldSprite = player.childAt(0); if (oldSprite) { player.removeChild(oldSprite); } // Attach wave asset, anchor center var waveSprite = player.attachAsset('Wave', { anchorX: 0.5, anchorY: 0.5 }); } // Enable wave mode controls player.isWave = true; // Set player.groundY for after wave mode if (typeof player.groundY !== "number") { player.groundY = 2732 - GROUND_HEIGHT; } } }, 1000); // Remove all obstacles and prevent spawning until countdown ends for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; lastObstacleX = 1200 + 700; // Set player to wave mode start position, but do not enable wave mode yet player.x = 2048 / 2; player.y = 1366; player.vy = 0; player.isWave = false; // Block update and input until countdown ends } // --- Secret Demon: always wave mode, no countdown, clear obstacles, set up wave obstacles --- if (levelIdx === 5) { // Remove all obstacles for clean start for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; lastObstacleX = 1200 + 700; // Remove all wave obstacles if (typeof waveObstacles !== "undefined") { for (var i = 0; i < waveObstacles.length; i++) { waveObstacles[i].destroy && waveObstacles[i].destroy(); } waveObstacles = []; } // Set player to wave mode, attach wave sprite if (player.childAt && typeof player.childAt === "function") { var oldSprite = player.childAt(0); if (oldSprite) { player.removeChild(oldSprite); } // Attach wave asset, anchor center var waveSprite = player.attachAsset('Wave', { anchorX: 0.5, anchorY: 0.5 }); } player.isWave = true; // --- Add static ground spikes for Secret Demon level --- // Place even fewer spikes, more space between them, leaving a small gap at left/right var spikeW = 100; var spikeH = 150; var groundY = 2732 - GROUND_HEIGHT + 2; // +2 to ensure overlap var startX = 60; // leave a gap at left var endX = 2048 - 60; // Increase spacing to every 4 spike widths (fewer spikes) for (var sx = startX; sx < endX; sx += spikeW * 4) { var groundSpike = new Spike(); groundSpike.x = sx + spikeW / 2; groundSpike.y = groundY; groundSpike.speed = 0; // static // Mark as ground spike for collision logic if needed groundSpike.isGroundSpike = true; obstacles.push(groundSpike); game.addChild(groundSpike); } } // Always ensure scoreTxt is in GUI and visible if (scoreTxt && !scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } scoreTxt.visible = true; // Always ensure goBackBtn is in GUI and visible at gameplay start if (game.goBackBtn && !game.goBackBtn.parent) { LK.gui.topRight.addChild(game.goBackBtn); } game.goBackBtn.visible = true; // Initial obstacle lastObstacleX = 1200 + 700; //{2I} // Add extra distance for a longer start spawnObstacle(); // Remove 'Back to Main Menu' button if present (should only show after level complete) if (game.goBackBtn && game.goBackBtn.parent) { game.goBackBtn.parent.removeChild(game.goBackBtn); game.goBackBtn.visible = false; } // Do not show 'Back to Main Menu' button in gameplay game.goBackBtn.visible = false; } // --- SCORE TEXT --- scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF, fontWeight: "bold", stroke: 0x000000, strokeThickness: 8 }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); scoreTxt.visible = true; // --- GO BACK TO MAIN MENU BUTTON (persistent for gameplay) --- game.goBackBtn = new Text2("Back to Main Menu", { size: 90, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 6 }); game.goBackBtn.anchor.set(1, 0); game.goBackBtn.x = 2048 - 40; game.goBackBtn.y = 380; game.goBackBtn.interactive = true; game.goBackBtn.buttonMode = true; game.goBackBtn.visible = false; game.goBackBtn.down = function () { // Remove gameplay UI and go to main menu if (scoreTxt) { scoreTxt.visible = false; } if (game.goBackBtn && game.goBackBtn.parent) { game.goBackBtn.parent.removeChild(game.goBackBtn); } game.goBackBtn.visible = false; showMainMenu(); }; LK.gui.topRight.addChild(game.goBackBtn); // --- NEW UPDATES COMING SOON... --- var sampleText = new Text2("NEW UPDATES COMING SOON...", { size: 100, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 7 }); sampleText.anchor.set(0.5, 0.5); sampleText.x = 2048 / 2; sampleText.y = 2732 / 2; sampleText.interactive = true; sampleText.buttonMode = true; sampleText.down = function () { // Secret action: show a message or perform a hidden action LK.showMessage && LK.showMessage("You found the secret button! 🎉"); }; game.addChild(sampleText); // --- INITIALIZE: show main menu --- showMainMenu(); // Helper: spawn obstacle function spawnObstacle() { // Randomly pick type var type = OBSTACLE_TYPES[Math.floor(Math.random() * OBSTACLE_TYPES.length)]; var obs; if (type === 'spike') { obs = new Spike(); obs.x = 2048 + 100; obs.y = OBSTACLE_Y; } else { obs = new Obstacle(); obs.x = 2048 + 100; obs.y = OBSTACLE_Y; } obstacles.push(obs); game.addChild(obs); lastObstacleX = obs.x; } // Helper: reset game state function resetGame() { // Remove obstacles for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; // Remove wave obstacles if (typeof waveObstacles !== "undefined") { for (var i = 0; i < waveObstacles.length; i++) { waveObstacles[i].destroy && waveObstacles[i].destroy(); } waveObstacles = []; } // Reset wave mode waveMode = false; // Reset player if (player) { player.x = PLAYER_START_X; player.y = player.groundY; player.vy = 0; player.isJumping = false; player.visible = true; // Make player visible again after explosion player.isWave = false; // Always reset wave mode state // Always restore player sprite to correct asset in Dash level (or after wave mode) if (player.childAt && typeof player.childAt === "function") { var oldSprite = player.childAt(0); if (selectedLevel === 4) { if (oldSprite && oldSprite.assetId === "Wave") { player.removeChild(oldSprite); oldSprite = null; } } if (!oldSprite || selectedLevel === 4 && oldSprite.assetId === "Wave") { var newSprite; if (storage.selectedSkin === "skin_red") { newSprite = player.attachAsset('Red', { anchorX: 0.5, anchorY: 1 }); } else if (storage.selectedSkin === "skin_green") { newSprite = player.attachAsset('Green', { anchorX: 0.5, anchorY: 1 }); } else if (storage.selectedSkin === "skin_gold") { newSprite = player.attachAsset('Gold', { anchorX: 0.5, anchorY: 1 }); } else { newSprite = player.attachAsset('player', { anchorX: 0.5, anchorY: 1 }); var skinColors = { "skin_red": 0xe74c3c, "skin_green": 0x2ecc71, "skin_gold": 0xf1c40f }; if (storage.selectedSkin && skinColors[storage.selectedSkin] && storage.selectedSkin !== "skin_red" && storage.selectedSkin !== "skin_green" && storage.selectedSkin !== "skin_gold") { newSprite.color = skinColors[storage.selectedSkin]; } } } } } // Reset score score = 0; if (scoreTxt) { scoreTxt.setText(score); scoreTxt.visible = true; if (!scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } } // Always ensure goBackBtn is in GUI and visible if (game.goBackBtn && !game.goBackBtn.parent) { LK.gui.topRight.addChild(game.goBackBtn); } if (game.goBackBtn) { game.goBackBtn.visible = true; } // Always ensure scoreTxt is in GUI and visible if (scoreTxt && !scoreTxt.parent) { LK.gui.top.addChild(scoreTxt); } if (scoreTxt) { scoreTxt.visible = true; } lastObstacleX = 1200; isGameOver = false; } // Touch/click to jump game.down = function (x, y, obj) { if (menuState !== MENU_STATE.PLAYING) { return; } if (isGameOver) { return; } // Block input during Dash level countdown if (selectedLevel === 4 && game._dashCountdownActive) { return; } if (player.isWave) { // In wave mode, pressing makes you go up player.vy = -38; // Optionally, rotate the wave asset for a little feedback var waveSprite = player.childAt && player.childAt(0); if (waveSprite) { tween(waveSprite, { rotation: waveSprite.rotation + Math.PI * 2 }, { duration: 400, easing: tween.linear }); } return; } else { player.jump(); } }; // Main update loop game.update = function () { if (menuState !== MENU_STATE.PLAYING) { return; } if (isGameOver) { return; } // --- Block update, obstacle spawn, and wave mode entry during Dash level 5s countdown --- if (selectedLevel === 4 && game._dashCountdownActive) { // Only update player position (so countdown looks correct), skip all obstacle logic and wave mode if (player && typeof player.update === "function") player.update(); return; } // --- WAVE MODE TRIGGER (Dash level only) --- // Dash level is index 4, Secret Demon is index 5 if (selectedLevel === 4) { // --- 5s countdown at 20 points --- if (!game._dashCountdownActive && !waveMode && score >= 20) { // Start 5s countdown, block input and update game._dashCountdownActive = true; var countdown = 5; var countdownText = new Text2(countdown + "", { size: 220, fill: 0xFFD700, fontWeight: "bold", stroke: 0x000000, strokeThickness: 12 }); countdownText.anchor.set(0.5, 0.5); countdownText.x = 2048 / 2; countdownText.y = 900; game.addChild(countdownText); var countdownTimer = LK.setInterval(function () { countdown--; if (countdown > 0) { countdownText.setText(countdown + ""); } else { LK.clearInterval(countdownTimer); if (countdownText.parent) countdownText.parent.removeChild(countdownText); game._dashCountdownActive = false; // After countdown, enter wave mode waveMode = true; // Remove all ground obstacles for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; // Move player to center Y for wave mode (start in mid air at y=700) player.y = 700; player.vy = 0; // Change player sprite to wave asset if (player.childAt && typeof player.childAt === "function") { var oldSprite = player.childAt(0); if (oldSprite) { player.removeChild(oldSprite); } // Attach wave asset, anchor center var waveSprite = player.attachAsset('Wave', { anchorX: 0.5, anchorY: 0.5 }); } // Enable wave mode controls player.isWave = true; // Set player.groundY for after wave mode if (typeof player.groundY !== "number") { player.groundY = 2732 - GROUND_HEIGHT; } } }, 1000); // Remove all obstacles and prevent spawning until countdown ends for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; lastObstacleX = 1200 + 700; // Set player to wave mode start position, but do not enable wave mode yet player.x = 2048 / 2; player.y = 1366; player.vy = 0; player.isWave = false; // Block update and input until countdown ends return; } // Block update and input during countdown if (game._dashCountdownActive) { if (player && typeof player.update === "function") player.update(); return; } // End wave mode at 75 points if (waveMode && score >= 75) { waveMode = false; // Remove all wave obstacles for (var i = 0; i < waveObstacles.length; i++) { waveObstacles[i].destroy && waveObstacles[i].destroy(); } waveObstacles = []; // Reset player to ground player.y = player.groundY; player.vy = 0; // Restore player sprite to selected skin if (player.childAt && typeof player.childAt === "function") { var oldSprite = player.childAt(0); if (oldSprite) { player.removeChild(oldSprite); } var newSprite; if (storage.selectedSkin === "skin_red") { newSprite = player.attachAsset('Red', { anchorX: 0.5, anchorY: 1 }); } else if (storage.selectedSkin === "skin_green") { newSprite = player.attachAsset('Green', { anchorX: 0.5, anchorY: 1 }); } else if (storage.selectedSkin === "skin_gold") { newSprite = player.attachAsset('Gold', { anchorX: 0.5, anchorY: 1 }); } else { newSprite = player.attachAsset('player', { anchorX: 0.5, anchorY: 1 }); var skinColors = { "skin_red": 0xe74c3c, "skin_green": 0x2ecc71, "skin_gold": 0xf1c40f }; if (storage.selectedSkin && skinColors[storage.selectedSkin] && storage.selectedSkin !== "skin_red" && storage.selectedSkin !== "skin_green" && storage.selectedSkin !== "skin_gold") { newSprite.color = skinColors[storage.selectedSkin]; } } } // Disable wave mode controls player.isWave = false; } } // --- Secret Demon Level: always wave mode, no countdown, more/faster obstacles, never disables wave mode --- if (selectedLevel === 5) { waveMode = true; player.isWave = true; // Always keep player in wave mode, never disable // Remove all ground obstacles (should be empty, but just in case) for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; // Optionally, increase flying obstacle spawn rate and speed // (handled below in obstacle spawn section) } // Update player player.update(); // --- NORMAL OBSTACLES --- if (!waveMode) { // Update obstacles for (var i = obstacles.length - 1; i >= 0; i--) { var obs = obstacles[i]; obs.update(); // Remove if off screen if (obs.x < -200) { obs.destroy(); obstacles.splice(i, 1); continue; } // Collision detection if (player.intersects(obs)) { // Explode player into small pieces if (player && player.parent) { var numPieces = 12; var centerX = player.x; var centerY = player.y; var pieceSize = 32; for (var p = 0; p < numPieces; p++) { (function (pieceIdx) { var angle = Math.PI * 2 / numPieces * pieceIdx; var px = centerX; var py = centerY - PLAYER_SIZE / 2 + pieceSize / 2; var piece = new Container(); var pieceSprite = piece.attachAsset('player', { anchorX: 0.5, anchorY: 0.5, width: pieceSize, height: pieceSize }); // Use same color as player var playerSpriteRef = player && player.childAt ? player.childAt(0) : null; if (playerSpriteRef && playerSpriteRef.color) { pieceSprite.color = playerSpriteRef.color; } piece.x = px; piece.y = py; game.addChild(piece); // Animate outward with random velocity and fade out var speed = 32 + Math.random() * 32; var dx = Math.cos(angle) * speed; var dy = Math.sin(angle) * speed - 10 + Math.random() * 20; tween(piece, { x: px + dx * 16, y: py + dy * 16, alpha: 0 }, { duration: 700 + Math.random() * 200, easing: tween.easeOutCubic, onComplete: function onComplete() { piece.destroy(); } }); })(p); } // Hide player sprite immediately player.visible = false; } isGameOver = true; // Delay restart until after explosion animation LK.setTimeout(function () { startLevel(selectedLevel); }, 800); return; } } } // --- WAVE MODE OBSTACLES --- if (waveMode) { // Kill player if they touch the ground in Dash wave mode (ground is death) if (selectedLevel === 4 && player.y >= 2732 - GROUND_HEIGHT) { // Explode player into small pieces if (player && player.parent) { var numPieces = 12; var centerX = player.x; var centerY = player.y; var pieceSize = 32; for (var p = 0; p < numPieces; p++) { (function (pieceIdx) { var angle = Math.PI * 2 / numPieces * pieceIdx; var px = centerX; var py = centerY - PLAYER_SIZE / 2 + pieceSize / 2; var piece = new Container(); var pieceSprite = piece.attachAsset('player', { anchorX: 0.5, anchorY: 0.5, width: pieceSize, height: pieceSize }); var playerSpriteRef = player && player.childAt ? player.childAt(0) : null; if (playerSpriteRef && playerSpriteRef.color) { pieceSprite.color = playerSpriteRef.color; } piece.x = px; piece.y = py; game.addChild(piece); var speed = 32 + Math.random() * 32; var dx = Math.cos(angle) * speed; var dy = Math.sin(angle) * speed - 10 + Math.random() * 20; tween(piece, { x: px + dx * 16, y: py + dy * 16, alpha: 0 }, { duration: 700 + Math.random() * 200, easing: tween.easeOutCubic, onComplete: function onComplete() { piece.destroy(); } }); })(p); } player.visible = false; } isGameOver = true; LK.setTimeout(function () { startLevel(selectedLevel); }, 800); return; } // Update flying obstacles for (var i = waveObstacles.length - 1; i >= 0; i--) { var wobs = waveObstacles[i]; wobs.update && wobs.update(); // Remove if off screen if (wobs.x < -200) { wobs.destroy && wobs.destroy(); waveObstacles.splice(i, 1); continue; } // Collision detection if (player.intersects(wobs)) { // Explode player into small pieces if (player && player.parent) { var numPieces = 12; var centerX = player.x; var centerY = player.y; var pieceSize = 32; for (var p = 0; p < numPieces; p++) { (function (pieceIdx) { var angle = Math.PI * 2 / numPieces * pieceIdx; var px = centerX; var py = centerY - PLAYER_SIZE / 2 + pieceSize / 2; var piece = new Container(); var pieceSprite = piece.attachAsset('player', { anchorX: 0.5, anchorY: 0.5, width: pieceSize, height: pieceSize }); var playerSpriteRef = player && player.childAt ? player.childAt(0) : null; if (playerSpriteRef && playerSpriteRef.color) { pieceSprite.color = playerSpriteRef.color; } piece.x = px; piece.y = py; game.addChild(piece); var speed = 32 + Math.random() * 32; var dx = Math.cos(angle) * speed; var dy = Math.sin(angle) * speed - 10 + Math.random() * 20; tween(piece, { x: px + dx * 16, y: py + dy * 16, alpha: 0 }, { duration: 700 + Math.random() * 200, easing: tween.easeOutCubic, onComplete: function onComplete() { piece.destroy(); } }); })(p); } player.visible = false; } isGameOver = true; LK.setTimeout(function () { startLevel(selectedLevel); }, 800); return; } } } // Score: increase as player passes obstacles if (!waveMode) { for (var j = 0; j < obstacles.length; j++) { var o = obstacles[j]; if (!o.passed && o.x + 60 < player.x) { o.passed = true; score += 1; if (scoreTxt) scoreTxt.setText(score); } } } else { // In wave mode, score increases as player passes flying obstacles for (var j = 0; j < waveObstacles.length; j++) { var wo = waveObstacles[j]; if (!wo.passed && wo.x + 60 < player.x) { wo.passed = true; score += 1; if (scoreTxt) scoreTxt.setText(score); } } } // Win condition: pass 10 obstacles (easy), 15 (medium), 20 (hard), 30 (Erin’s atventure continuation), 30 (Dash), 50 (Secret Demon) var winScores = [10, 15, 20, 30, 30, 50]; if (score >= winScores[selectedLevel]) { isGameOver = true; // Mark level as completed for coin claim storage.lastCompletedLevel = selectedLevel; if (!storage.claimed) { storage.claimed = {}; } storage.claimed[selectedLevel] = storage.claimed[selectedLevel] || false; // Award +150 coins if Erin’s atventure continuation (index 3) // (Now handled in claim button, not here) LK.setTimeout(function () { showLevelIntro(); }, 600); return; } // Spawn new obstacles if (!waveMode) { if (obstacles.length === 0 || 2048 - lastObstacleX > OBSTACLE_MIN_GAP + Math.floor(Math.random() * (OBSTACLE_MAX_GAP - OBSTACLE_MIN_GAP))) { spawnObstacle(); } } else if (waveMode && selectedLevel === 4 && score < 75) { // In wave mode, spawn flying spikes/obstacles at random intervals if (waveObstacles.length === 0 || waveObstacles.length < 4 && Math.random() < 0.04) { // Randomly choose between flying spike and flying obstacle var wtype = Math.random() < 0.6 ? 'spike' : 'obstacle'; var wobs; if (wtype === 'spike') { wobs = new WaveSpike(); } else { wobs = new FlyingObstacle(); } wobs.x = 2048 + 100; // Random Y in upper/lower half, but not too close to edge wobs.y = 400 + Math.random() * (2732 - 800); waveObstacles.push(wobs); game.addChild(wobs); } } else if (waveMode && selectedLevel === 5) { // Secret Demon: spawn up to 2 flying obstacles, much lower spawn chance, still SLOWER speed if (waveObstacles.length === 0 || waveObstacles.length < 2 && Math.random() < 0.025) { var wtype = Math.random() < 0.7 ? 'spike' : 'obstacle'; var wobs; if (wtype === 'spike') { wobs = new WaveSpike(); wobs.speed = 18 + Math.random() * 4; // much slower than Dash } else { wobs = new FlyingObstacle(); wobs.speed = 18 + Math.random() * 4; // much slower than Dash } wobs.x = 2048 + 100; // Random Y, but allow more vertical spread wobs.y = 200 + Math.random() * (2732 - 400); waveObstacles.push(wobs); game.addChild(wobs); } } }; // (gameover event handler removed, as game over popup is never shown)
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
coins: 0,
claimed: {}
});
/****
* Classes
****/
// FlyingObstacle class (flying block for wave mode)
var FlyingObstacle = Container.expand(function () {
var self = Container.call(this);
var obsSprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 28;
self.amplitude = 350 + Math.random() * 250;
self.frequency = 0.0015 + Math.random() * 0.001;
self.baseY = 1366 + (Math.random() - 0.5) * 900;
self.phase = Math.random() * Math.PI * 2;
self.update = function () {
self.x -= self.speed;
self.y = self.baseY + Math.cos(self.x * self.frequency + self.phase) * self.amplitude;
};
return self;
});
// Obstacle class (tall block)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsSprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 1
});
self.speed = 22;
self.update = function () {
self.x -= self.speed;
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
// Use image asset for red, green, or gold skin, otherwise use shape asset and tint for other skins
var playerSprite;
if (storage.selectedSkin === "skin_red") {
playerSprite = self.attachAsset('Red', {
anchorX: 0.5,
anchorY: 1
});
} else if (storage.selectedSkin === "skin_green") {
playerSprite = self.attachAsset('Green', {
anchorX: 0.5,
anchorY: 1
});
} else if (storage.selectedSkin === "skin_gold") {
playerSprite = self.attachAsset('Gold', {
anchorX: 0.5,
anchorY: 1
});
} else {
playerSprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 1
});
// Apply skin color if selected and not red, green, or gold
var skinColors = {
"skin_red": 0xe74c3c,
"skin_green": 0x2ecc71,
"skin_gold": 0xf1c40f
};
if (storage.selectedSkin && skinColors[storage.selectedSkin] && storage.selectedSkin !== "skin_red" && storage.selectedSkin !== "skin_green" && storage.selectedSkin !== "skin_gold") {
playerSprite.color = skinColors[storage.selectedSkin];
}
}
// Physics
self.vy = 0;
self.isJumping = false;
self.gravity = 2.2;
self.jumpStrength = -48;
self.groundY = 0; // Set after ground is created
self.isWave = false; // Always initialize wave mode state
// Update method
self.update = function () {
if (self.isWave) {
// Wave mode: apply constant gravity, but allow upward thrust on press
self.vy += 3.2; // slightly higher gravity for wave feel
self.y += self.vy;
// Clamp to screen bounds (not off top or bottom)
if (self.y < 60) {
self.y = 60;
self.vy = 0;
}
if (self.y > 2732 - 60) {
self.y = 2732 - 60;
self.vy = 0;
}
} else {
// Normal mode
self.vy += self.gravity;
self.y += self.vy;
// Ground collision
if (self.y > self.groundY) {
self.y = self.groundY;
self.vy = 0;
self.isJumping = false;
}
}
};
// Jump method
self.jump = function () {
if (!self.isJumping && self.y >= self.groundY) {
self.vy = self.jumpStrength;
self.isJumping = true;
// Start 360-degree rotation tween on playerSprite
tween(playerSprite, {
rotation: playerSprite.rotation + Math.PI * 2
}, {
duration: 400,
easing: tween.linear
});
}
};
return self;
});
// Spike class (obstacle)
var Spike = Container.expand(function () {
var self = Container.call(this);
var spikeSprite = self.attachAsset('spike', {
anchorX: 0.5,
anchorY: 1
});
self.speed = 22; // Moves left
self.update = function () {
self.x -= self.speed;
};
return self;
});
// WaveSpike class (flying spike for wave mode)
var WaveSpike = Container.expand(function () {
var self = Container.call(this);
var spikeSprite = self.attachAsset('spike', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 28;
self.amplitude = 400 + Math.random() * 300; // vertical wave amplitude
self.frequency = 0.002 + Math.random() * 0.0015; // wave frequency
self.baseY = 1366 + (Math.random() - 0.5) * 900; // center Y
self.phase = Math.random() * Math.PI * 2;
self.update = function () {
self.x -= self.speed;
self.y = self.baseY + Math.sin(self.x * self.frequency + self.phase) * self.amplitude;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222831
});
/****
* Game Code
****/
// Player: blue square
// Ground: gray rectangle
// Spike: red triangle (approximate with a tall, thin box for MVP)
// Obstacle: purple rectangle
var LEVELS = [{
name: "First Steps",
difficulty: "Easy",
color: 0x2ecc71,
minGap: 500,
maxGap: 900,
types: ['spike', 'obstacle', 'spike'],
intro: "Welcome to your first steps!"
}, {
name: "Erin's Adventure",
difficulty: "Medium",
color: 0xf1c40f,
minGap: 500,
maxGap: 900,
types: ['spike', 'obstacle', 'spike'],
intro: "A trickier journey awaits."
}, {
name: "Final Steps",
difficulty: "Hard",
color: 0xe74c3c,
minGap: 500,
maxGap: 900,
types: ['spike', 'obstacle', 'spike'],
intro: "Only the brave survive!"
}, {
name: "Erin’s atventure continuation",
difficulty: "Hard",
color: 0x8e44ad,
minGap: 320,
maxGap: 480,
types: ['spike', 'spike', 'obstacle', 'spike', 'spike'],
intro: "The spikes are closer than ever. Time your jumps perfectly!"
}, {
name: "Dash",
difficulty: "Extreme",
color: 0x16a085,
minGap: 220,
maxGap: 350,
types: ['spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike'],
intro: "Dash through a gauntlet of spikes! Can you survive?"
}, {
name: "Secret",
difficulty: "Demon",
color: 0x000000,
minGap: 220,
maxGap: 350,
types: ['spike', 'obstacle', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'obstacle'],
intro: "The secret Demon level. Only the best will survive. Prepare for the hardest challenge!"
}];
// --- MENU STATE ---
var MENU_STATE = {
MAIN: 0,
LEVEL_SELECT: 1,
PLAYING: 2
};
var menuState = MENU_STATE.MAIN;
var selectedLevel = 0;
// --- UI ELEMENTS ---
var menuContainer = new Container();
var levelSelectContainer = new Container();
var introText = null;
// --- GAME CONSTANTS ---
var GROUND_HEIGHT = 120;
var PLAYER_SIZE = 120;
var PLAYER_START_X = 400;
var OBSTACLE_MIN_GAP = 600;
var OBSTACLE_MAX_GAP = 1100;
var OBSTACLE_TYPES = ['spike', 'obstacle'];
var OBSTACLE_Y = 2732 - GROUND_HEIGHT;
// --- GAME STATE ---
var player;
var ground;
var obstacles = [];
var score = 0;
var scoreTxt;
var lastObstacleX = 0;
var isGameOver = false;
// --- WAVE MODE STATE (Dash level only) ---
var waveMode = false; // true if in wave mode
var waveObstacles = []; // flying spikes/obstacles in wave mode
// --- UI HELPERS ---
function clearMenuUI() {
if (menuContainer.parent) {
menuContainer.parent.removeChild(menuContainer);
}
if (levelSelectContainer.parent) {
levelSelectContainer.parent.removeChild(levelSelectContainer);
}
if (introText && introText.parent) {
introText.parent.removeChild(introText);
}
}
// --- MAIN MENU ---
function showMainMenu() {
clearMenuUI();
menuState = MENU_STATE.MAIN;
menuContainer.removeChildren();
// Title
var title = new Text2("Geometry Dash", {
size: 180,
fill: 0xFFFFFF,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 12
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 400;
menuContainer.addChild(title);
// Play button
var playBtn = new Text2("Play", {
size: 140,
fill: 0x2ECC71,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 10
});
playBtn.anchor.set(0.5, 0.5);
playBtn.x = 2048 / 2;
playBtn.y = 1000;
playBtn.interactive = true;
playBtn.buttonMode = true;
playBtn.down = function () {
showLevelSelect();
};
menuContainer.addChild(playBtn);
// Shop button
var shopBtn = new Text2("Shop", {
size: 120,
fill: 0xf1c40f,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
shopBtn.anchor.set(0.5, 0.5);
shopBtn.x = 2048 / 2;
shopBtn.y = 1200;
shopBtn.interactive = true;
shopBtn.buttonMode = true;
shopBtn.down = function () {
showShop();
};
menuContainer.addChild(shopBtn);
// Add to game
game.addChild(menuContainer);
// Remove 'Back to Main Menu' button if present
if (game.goBackBtn && game.goBackBtn.parent) {
game.goBackBtn.parent.removeChild(game.goBackBtn);
game.goBackBtn.visible = false;
}
// Hide score text on main menu
if (scoreTxt) {
scoreTxt.visible = false;
// Always ensure scoreTxt is in GUI
if (!scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
}
// Always ensure goBackBtn is in GUI and hidden
if (game.goBackBtn && !game.goBackBtn.parent) {
LK.gui.topRight.addChild(game.goBackBtn);
}
game.goBackBtn.visible = false;
}
// --- SHOP ---
var shopContainer = new Container();
var SHOP_ITEMS = [{
id: "skin_red",
name: "Red Skin",
price: 30,
color: 0xe74c3c
}, {
id: "skin_green",
name: "Green Skin",
price: 30,
color: 0x2ecc71
}, {
id: "skin_gold",
name: "Gold Skin",
price: 100,
color: 0xf1c40f
}];
function showShop() {
clearMenuUI();
menuState = MENU_STATE.MAIN;
shopContainer.removeChildren();
// Title
var shopTitle = new Text2("Shop", {
size: 150,
fill: 0xFFFFFF,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 10
});
shopTitle.anchor.set(0.5, 0);
shopTitle.x = 2048 / 2;
shopTitle.y = 300;
shopContainer.addChild(shopTitle);
// Coin display
var coins = storage.coins || 0;
var coinTxt = new Text2("Coins: " + coins, {
size: 100,
fill: 0xf1c40f,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
coinTxt.anchor.set(0.5, 0);
coinTxt.x = 2048 / 2;
coinTxt.y = 500;
shopContainer.addChild(coinTxt);
// List items
for (var i = 0; i < SHOP_ITEMS.length; i++) {
(function (idx) {
var item = SHOP_ITEMS[idx];
var owned = storage["owned_" + item.id] === true;
var y = 700 + idx * 220;
var itemTxt = new Text2(item.name, {
size: 100,
fill: "#" + item.color.toString(16).padStart(6, "0"),
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
itemTxt.anchor.set(0, 0.5);
itemTxt.x = 400;
itemTxt.y = y;
shopContainer.addChild(itemTxt);
var priceTxt = new Text2(owned ? "Owned" : item.price + " coins", {
size: 90,
fill: owned ? 0x2ecc71 : 0xffffff,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 6
});
priceTxt.anchor.set(0.5, 0.5);
priceTxt.x = 1200;
priceTxt.y = y;
shopContainer.addChild(priceTxt);
// Buy/select button
var btnTxt = owned ? storage.selectedSkin === item.id ? "Selected" : "Select" : "Buy";
var btn = new Text2(btnTxt, {
size: 90,
fill: owned ? storage.selectedSkin === item.id ? 0x2ecc71 : 0x3498db : 0xf1c40f,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 6
});
btn.anchor.set(0.5, 0.5);
btn.x = 1700;
btn.y = y;
btn.interactive = true;
btn.buttonMode = true;
btn.down = function () {
if (owned) {
// Select skin
storage.selectedSkin = item.id;
// If player exists, update its color immediately
if (player && player.childAt && typeof player.childAt === "function") {
// Remove old sprite
var oldSprite = player.childAt(0);
if (oldSprite) {
player.removeChild(oldSprite);
}
var newSprite;
if (item.id === "skin_red") {
newSprite = player.attachAsset('Red', {
anchorX: 0.5,
anchorY: 1
});
} else if (item.id === "skin_green") {
newSprite = player.attachAsset('Green', {
anchorX: 0.5,
anchorY: 1
});
} else if (item.id === "skin_gold") {
newSprite = player.attachAsset('Gold', {
anchorX: 0.5,
anchorY: 1
});
} else {
newSprite = player.attachAsset('player', {
anchorX: 0.5,
anchorY: 1
});
var skinColors = {
"skin_red": 0xe74c3c,
"skin_green": 0x2ecc71,
"skin_gold": 0xf1c40f
};
if (skinColors[item.id] && item.id !== "skin_red" && item.id !== "skin_green" && item.id !== "skin_gold") {
newSprite.color = skinColors[item.id];
}
}
}
showShop();
} else {
// Try to buy
var coins = storage.coins || 0;
if (coins >= item.price) {
storage.coins = coins - item.price;
storage["owned_" + item.id] = true;
storage.selectedSkin = item.id;
showShop();
} else {
priceTxt.setText("Not enough!");
priceTxt.fill = 0xe74c3c;
}
}
};
shopContainer.addChild(btn);
})(i);
}
// Back button
var backBtn = new Text2("Back", {
size: 90,
fill: 0x888888,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 6
});
backBtn.anchor.set(0.5, 0.5);
backBtn.x = 2048 / 2;
backBtn.y = 700 + SHOP_ITEMS.length * 220 + 100;
backBtn.interactive = true;
backBtn.buttonMode = true;
backBtn.down = function () {
// Remove shop UI before showing main menu
if (shopContainer.parent) {
shopContainer.parent.removeChild(shopContainer);
}
showMainMenu();
};
shopContainer.addChild(backBtn);
game.addChild(shopContainer);
// Always ensure scoreTxt is in GUI and hidden
if (scoreTxt && !scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
scoreTxt.visible = false;
// Always ensure goBackBtn is in GUI and hidden
if (game.goBackBtn && !game.goBackBtn.parent) {
LK.gui.topRight.addChild(game.goBackBtn);
}
game.goBackBtn.visible = false;
}
// --- LEVEL SELECT MENU ---
function showLevelSelect() {
clearMenuUI();
menuState = MENU_STATE.LEVEL_SELECT;
levelSelectContainer.removeChildren();
// Title
var selTitle = new Text2("Select Level", {
size: 150,
fill: 0xFFFFFF,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 10
});
selTitle.anchor.set(0.5, 0);
selTitle.x = 2048 / 2;
selTitle.y = 300;
levelSelectContainer.addChild(selTitle);
// Level buttons
for (var i = 0; i < LEVELS.length; i++) {
(function (idx) {
var lvl = LEVELS[idx];
var btn = new Text2(lvl.name + " (" + lvl.difficulty + ")", {
size: 110,
fill: "#" + lvl.color.toString(16).padStart(6, "0"),
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
btn.anchor.set(0.5, 0.5);
btn.x = 2048 / 2;
btn.y = 600 + idx * 250;
btn.interactive = true;
btn.buttonMode = true;
btn.down = function () {
selectedLevel = idx;
showLevelIntro();
};
levelSelectContainer.addChild(btn);
// Insert "NEW UPDATES COMING SOON..." after Erin’s atventure continuation (index 3)
if (idx === 3) {
var updatesText = new Text2("NEW UPDATES COMING SOON...", {
size: 100,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
updatesText.anchor.set(0.5, 0.5);
updatesText.x = 2048 / 2;
updatesText.y = btn.y + 180;
levelSelectContainer.addChild(updatesText);
}
// Highlight Secret Demon level in red and with a demon icon if desired
if (idx === 5) {
btn.setText(lvl.name + " (" + lvl.difficulty + " 😈)");
btn.fill = "#ff0000";
}
})(i);
}
// Back button
var backBtn = new Text2("Back", {
size: 90,
fill: 0x888888,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 6
});
backBtn.anchor.set(0.5, 0.5);
backBtn.x = 2048 / 2;
backBtn.y = 600 + LEVELS.length * 250 + 100;
backBtn.interactive = true;
backBtn.buttonMode = true;
backBtn.down = function () {
showMainMenu();
};
levelSelectContainer.addChild(backBtn);
game.addChild(levelSelectContainer);
// Remove 'Back to Main Menu' button if present
if (game.goBackBtn && game.goBackBtn.parent) {
game.goBackBtn.parent.removeChild(game.goBackBtn);
game.goBackBtn.visible = false;
}
// Always ensure scoreTxt is in GUI and hidden
if (scoreTxt && !scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
scoreTxt.visible = false;
// Always ensure goBackBtn is in GUI and hidden
if (game.goBackBtn && !game.goBackBtn.parent) {
LK.gui.topRight.addChild(game.goBackBtn);
}
game.goBackBtn.visible = false;
}
// --- LEVEL INTRO ---
function showLevelIntro() {
clearMenuUI();
menuState = MENU_STATE.LEVEL_SELECT;
var lvl = LEVELS[selectedLevel];
// Coin rewards per level
var COIN_REWARDS = [20, 50, 100];
var rewardText = lvl.name + "\nDifficulty: " + lvl.difficulty + "\n\n" + lvl.intro + "\n\nTap to Start";
var showClaim = false;
var coinsToClaim = 0;
if (storage.claimed && storage.claimed[selectedLevel] === false && storage.lastCompletedLevel !== undefined && storage.lastCompletedLevel == selectedLevel) {
// Player just beat this level and hasn't claimed coins yet
showClaim = true;
coinsToClaim = COIN_REWARDS[selectedLevel];
rewardText += "\n\n🎉 Level Complete! 🎉\nClaim +" + coinsToClaim + " coins!";
}
introText = new Text2(rewardText, {
size: 100,
fill: "#" + lvl.color.toString(16).padStart(6, "0"),
align: "center",
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
introText.anchor.set(0.5, 0.5);
introText.x = 2048 / 2;
introText.y = 1200;
introText.interactive = true;
introText.buttonMode = true;
introText.down = function () {
if (showClaim) {
return;
} // Don't start level if claim is available
startLevel(selectedLevel);
};
game.addChild(introText);
// Show claim button if eligible
if (showClaim) {
var claimBtn = new Text2("Claim +" + coinsToClaim + " coins", {
size: 110,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
claimBtn.anchor.set(0.5, 0.5);
claimBtn.x = 2048 / 2;
claimBtn.y = introText.y + 350;
claimBtn.interactive = true;
claimBtn.buttonMode = true;
claimBtn.down = function () {
// Add coins and mark as claimed
if (selectedLevel === 3) {
storage.coins = (storage.coins || 0) + 150;
} else {
storage.coins = (storage.coins || 0) + coinsToClaim;
}
if (!storage.claimed) {
storage.claimed = {};
}
storage.claimed[selectedLevel] = true;
// Remove lastCompletedLevel so claim can't be repeated
delete storage.lastCompletedLevel;
// Update UI
claimBtn.setText("Claimed!");
claimBtn.interactive = false;
claimBtn.buttonMode = false;
};
game.addChild(claimBtn);
}
// Always show 'Back to Main Menu' button on every level intro
var backToMenuBtn = new Text2("Back to Main Menu", {
size: 90,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 6
});
// Anchor to center, place further down under "Tap to Start" and Claim button if present
backToMenuBtn.anchor.set(0.5, 0.5);
// If claimBtn is present, place below it, else below introText
var btnY = introText.y + 250;
// Find claimBtn if present
var claimBtnY = null;
var children = game.children ? game.children.slice() : [];
for (var i = 0; i < children.length; i++) {
if (children[i] && children[i].text && typeof children[i].text === "string" && children[i].text.indexOf("Claim") === 0) {
claimBtnY = children[i].y;
break;
}
}
if (claimBtnY !== null) {
btnY = claimBtnY + 250;
}
// Move button a little to the left (was +700, now +550) and even higher (was btnY - 200, then btnY - 350, then btnY - 500, then btnY - 650, then btnY - 800, then btnY - 950, then btnY - 1100, now btnY - 1250)
backToMenuBtn.x = 2048 / 2 + 550;
backToMenuBtn.y = btnY - 1250;
backToMenuBtn.interactive = true;
backToMenuBtn.buttonMode = true;
backToMenuBtn.visible = true;
backToMenuBtn.down = function () {
// Remove all game children (reset game state)
while (game.children && game.children.length > 0) {
var child = game.children[0];
if (child && child.parent) {
child.parent.removeChild(child);
}
}
// Remove score text from GUI if present
if (scoreTxt && scoreTxt.parent) {
scoreTxt.parent.removeChild(scoreTxt);
}
// Remove goBackBtn from GUI if present
if (game.goBackBtn && game.goBackBtn.parent) {
game.goBackBtn.parent.removeChild(game.goBackBtn);
game.goBackBtn.visible = false;
}
// Reset gameplay state variables
isGameOver = false;
menuState = MENU_STATE.MAIN;
// Show main menu
showMainMenu();
};
game.addChild(backToMenuBtn);
// Remove 'Back to Main Menu' button if present (from gameplay)
if (game.goBackBtn && game.goBackBtn.parent) {
game.goBackBtn.parent.removeChild(game.goBackBtn);
game.goBackBtn.visible = false;
}
// Hide score text on level intro
if (scoreTxt) {
scoreTxt.visible = false;
// Always ensure scoreTxt is in GUI
if (!scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
}
// Always ensure goBackBtn is in GUI and hidden
if (game.goBackBtn && !game.goBackBtn.parent) {
LK.gui.topRight.addChild(game.goBackBtn);
}
game.goBackBtn.visible = false;
}
// --- START LEVEL ---
function startLevel(levelIdx) {
clearMenuUI();
menuState = MENU_STATE.PLAYING;
// Set level params
var lvl = LEVELS[levelIdx];
OBSTACLE_MIN_GAP = lvl.minGap;
OBSTACLE_MAX_GAP = lvl.maxGap;
OBSTACLE_TYPES = lvl.types;
// Set background color to a slightly darker blue for First Steps level
if (levelIdx === 0) {
game.setBackgroundColor(0x2176ae);
// Stop any music before playing Stereo Madness to ensure it starts
LK.stopMusic();
LK.playMusic('stereo_madness');
} else {
game.setBackgroundColor(0x222831);
// Stop Stereo Madness if switching away from First Steps
LK.stopMusic();
}
// Reset game state
resetGame();
// Show score
score = 0;
scoreTxt.setText("0");
scoreTxt.visible = true;
// Show ground and player
if (!ground) {
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 1,
x: 0,
y: 2732
});
game.addChild(ground);
} else if (!ground.parent) {
game.addChild(ground);
}
if (!player) {
player = new Player();
player.groundY = 2732 - GROUND_HEIGHT;
if (levelIdx === 4) {
// Dash level: start in center for wave countdown
player.x = 2048 / 2;
player.y = 1366;
} else if (levelIdx === 5) {
// Secret Demon: start in center of screen, in air, always wave mode
player.x = 2048 / 2;
player.y = 1366; // center vertically
player.vy = 0;
player.isWave = true;
} else {
player.x = PLAYER_START_X;
player.y = player.groundY;
}
game.addChild(player);
} else if (!player.parent) {
game.addChild(player);
if (levelIdx === 4) {
player.x = 2048 / 2;
player.y = 1366;
} else if (levelIdx === 5) {
// Secret Demon: start in center, in air, always wave mode
player.x = 2048 / 2;
player.y = 700;
player.vy = 0;
player.isWave = true;
} else {
player.x = PLAYER_START_X;
player.y = player.groundY;
}
}
// --- Dash level: prepare for 5s countdown at 20 points (do not show countdown yet) ---
if (levelIdx === 4) {
// Set player to center for Dash level start, do not enable wave mode yet
player.x = 2048 / 2;
player.y = 1366;
player.vy = 0;
player.isWave = false;
// Remove all obstacles for clean start
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
lastObstacleX = 1200 + 700;
// --- Dash level: 5s countdown at start ---
game._dashCountdownActive = true;
var dashCountdown = 5;
var dashCountdownText = new Text2(dashCountdown + "", {
size: 220,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 12
});
dashCountdownText.anchor.set(0.5, 0.5);
dashCountdownText.x = 2048 / 2;
dashCountdownText.y = 900;
game.addChild(dashCountdownText);
var dashCountdownTimer = LK.setInterval(function () {
dashCountdown--;
if (dashCountdown > 0) {
dashCountdownText.setText(dashCountdown + "");
} else {
LK.clearInterval(dashCountdownTimer);
if (dashCountdownText.parent) dashCountdownText.parent.removeChild(dashCountdownText);
game._dashCountdownActive = false;
// After countdown, enter wave mode
waveMode = true;
// Remove all ground obstacles
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
// Move player to center Y for wave mode (start in mid air at y=700)
player.y = 700;
player.vy = 0;
// Change player sprite to wave asset
if (player.childAt && typeof player.childAt === "function") {
var oldSprite = player.childAt(0);
if (oldSprite) {
player.removeChild(oldSprite);
}
// Attach wave asset, anchor center
var waveSprite = player.attachAsset('Wave', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Enable wave mode controls
player.isWave = true;
// Set player.groundY for after wave mode
if (typeof player.groundY !== "number") {
player.groundY = 2732 - GROUND_HEIGHT;
}
}
}, 1000);
// Remove all obstacles and prevent spawning until countdown ends
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
lastObstacleX = 1200 + 700;
// Set player to wave mode start position, but do not enable wave mode yet
player.x = 2048 / 2;
player.y = 1366;
player.vy = 0;
player.isWave = false;
// Block update and input until countdown ends
}
// --- Secret Demon: always wave mode, no countdown, clear obstacles, set up wave obstacles ---
if (levelIdx === 5) {
// Remove all obstacles for clean start
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
lastObstacleX = 1200 + 700;
// Remove all wave obstacles
if (typeof waveObstacles !== "undefined") {
for (var i = 0; i < waveObstacles.length; i++) {
waveObstacles[i].destroy && waveObstacles[i].destroy();
}
waveObstacles = [];
}
// Set player to wave mode, attach wave sprite
if (player.childAt && typeof player.childAt === "function") {
var oldSprite = player.childAt(0);
if (oldSprite) {
player.removeChild(oldSprite);
}
// Attach wave asset, anchor center
var waveSprite = player.attachAsset('Wave', {
anchorX: 0.5,
anchorY: 0.5
});
}
player.isWave = true;
// --- Add static ground spikes for Secret Demon level ---
// Place even fewer spikes, more space between them, leaving a small gap at left/right
var spikeW = 100;
var spikeH = 150;
var groundY = 2732 - GROUND_HEIGHT + 2; // +2 to ensure overlap
var startX = 60; // leave a gap at left
var endX = 2048 - 60;
// Increase spacing to every 4 spike widths (fewer spikes)
for (var sx = startX; sx < endX; sx += spikeW * 4) {
var groundSpike = new Spike();
groundSpike.x = sx + spikeW / 2;
groundSpike.y = groundY;
groundSpike.speed = 0; // static
// Mark as ground spike for collision logic if needed
groundSpike.isGroundSpike = true;
obstacles.push(groundSpike);
game.addChild(groundSpike);
}
}
// Always ensure scoreTxt is in GUI and visible
if (scoreTxt && !scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
scoreTxt.visible = true;
// Always ensure goBackBtn is in GUI and visible at gameplay start
if (game.goBackBtn && !game.goBackBtn.parent) {
LK.gui.topRight.addChild(game.goBackBtn);
}
game.goBackBtn.visible = true;
// Initial obstacle
lastObstacleX = 1200 + 700; //{2I} // Add extra distance for a longer start
spawnObstacle();
// Remove 'Back to Main Menu' button if present (should only show after level complete)
if (game.goBackBtn && game.goBackBtn.parent) {
game.goBackBtn.parent.removeChild(game.goBackBtn);
game.goBackBtn.visible = false;
}
// Do not show 'Back to Main Menu' button in gameplay
game.goBackBtn.visible = false;
}
// --- SCORE TEXT ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.visible = true;
// --- GO BACK TO MAIN MENU BUTTON (persistent for gameplay) ---
game.goBackBtn = new Text2("Back to Main Menu", {
size: 90,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 6
});
game.goBackBtn.anchor.set(1, 0);
game.goBackBtn.x = 2048 - 40;
game.goBackBtn.y = 380;
game.goBackBtn.interactive = true;
game.goBackBtn.buttonMode = true;
game.goBackBtn.visible = false;
game.goBackBtn.down = function () {
// Remove gameplay UI and go to main menu
if (scoreTxt) {
scoreTxt.visible = false;
}
if (game.goBackBtn && game.goBackBtn.parent) {
game.goBackBtn.parent.removeChild(game.goBackBtn);
}
game.goBackBtn.visible = false;
showMainMenu();
};
LK.gui.topRight.addChild(game.goBackBtn);
// --- NEW UPDATES COMING SOON... ---
var sampleText = new Text2("NEW UPDATES COMING SOON...", {
size: 100,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
sampleText.anchor.set(0.5, 0.5);
sampleText.x = 2048 / 2;
sampleText.y = 2732 / 2;
sampleText.interactive = true;
sampleText.buttonMode = true;
sampleText.down = function () {
// Secret action: show a message or perform a hidden action
LK.showMessage && LK.showMessage("You found the secret button! 🎉");
};
game.addChild(sampleText);
// --- INITIALIZE: show main menu ---
showMainMenu();
// Helper: spawn obstacle
function spawnObstacle() {
// Randomly pick type
var type = OBSTACLE_TYPES[Math.floor(Math.random() * OBSTACLE_TYPES.length)];
var obs;
if (type === 'spike') {
obs = new Spike();
obs.x = 2048 + 100;
obs.y = OBSTACLE_Y;
} else {
obs = new Obstacle();
obs.x = 2048 + 100;
obs.y = OBSTACLE_Y;
}
obstacles.push(obs);
game.addChild(obs);
lastObstacleX = obs.x;
}
// Helper: reset game state
function resetGame() {
// Remove obstacles
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
// Remove wave obstacles
if (typeof waveObstacles !== "undefined") {
for (var i = 0; i < waveObstacles.length; i++) {
waveObstacles[i].destroy && waveObstacles[i].destroy();
}
waveObstacles = [];
}
// Reset wave mode
waveMode = false;
// Reset player
if (player) {
player.x = PLAYER_START_X;
player.y = player.groundY;
player.vy = 0;
player.isJumping = false;
player.visible = true; // Make player visible again after explosion
player.isWave = false; // Always reset wave mode state
// Always restore player sprite to correct asset in Dash level (or after wave mode)
if (player.childAt && typeof player.childAt === "function") {
var oldSprite = player.childAt(0);
if (selectedLevel === 4) {
if (oldSprite && oldSprite.assetId === "Wave") {
player.removeChild(oldSprite);
oldSprite = null;
}
}
if (!oldSprite || selectedLevel === 4 && oldSprite.assetId === "Wave") {
var newSprite;
if (storage.selectedSkin === "skin_red") {
newSprite = player.attachAsset('Red', {
anchorX: 0.5,
anchorY: 1
});
} else if (storage.selectedSkin === "skin_green") {
newSprite = player.attachAsset('Green', {
anchorX: 0.5,
anchorY: 1
});
} else if (storage.selectedSkin === "skin_gold") {
newSprite = player.attachAsset('Gold', {
anchorX: 0.5,
anchorY: 1
});
} else {
newSprite = player.attachAsset('player', {
anchorX: 0.5,
anchorY: 1
});
var skinColors = {
"skin_red": 0xe74c3c,
"skin_green": 0x2ecc71,
"skin_gold": 0xf1c40f
};
if (storage.selectedSkin && skinColors[storage.selectedSkin] && storage.selectedSkin !== "skin_red" && storage.selectedSkin !== "skin_green" && storage.selectedSkin !== "skin_gold") {
newSprite.color = skinColors[storage.selectedSkin];
}
}
}
}
}
// Reset score
score = 0;
if (scoreTxt) {
scoreTxt.setText(score);
scoreTxt.visible = true;
if (!scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
}
// Always ensure goBackBtn is in GUI and visible
if (game.goBackBtn && !game.goBackBtn.parent) {
LK.gui.topRight.addChild(game.goBackBtn);
}
if (game.goBackBtn) {
game.goBackBtn.visible = true;
}
// Always ensure scoreTxt is in GUI and visible
if (scoreTxt && !scoreTxt.parent) {
LK.gui.top.addChild(scoreTxt);
}
if (scoreTxt) {
scoreTxt.visible = true;
}
lastObstacleX = 1200;
isGameOver = false;
}
// Touch/click to jump
game.down = function (x, y, obj) {
if (menuState !== MENU_STATE.PLAYING) {
return;
}
if (isGameOver) {
return;
}
// Block input during Dash level countdown
if (selectedLevel === 4 && game._dashCountdownActive) {
return;
}
if (player.isWave) {
// In wave mode, pressing makes you go up
player.vy = -38;
// Optionally, rotate the wave asset for a little feedback
var waveSprite = player.childAt && player.childAt(0);
if (waveSprite) {
tween(waveSprite, {
rotation: waveSprite.rotation + Math.PI * 2
}, {
duration: 400,
easing: tween.linear
});
}
return;
} else {
player.jump();
}
};
// Main update loop
game.update = function () {
if (menuState !== MENU_STATE.PLAYING) {
return;
}
if (isGameOver) {
return;
}
// --- Block update, obstacle spawn, and wave mode entry during Dash level 5s countdown ---
if (selectedLevel === 4 && game._dashCountdownActive) {
// Only update player position (so countdown looks correct), skip all obstacle logic and wave mode
if (player && typeof player.update === "function") player.update();
return;
}
// --- WAVE MODE TRIGGER (Dash level only) ---
// Dash level is index 4, Secret Demon is index 5
if (selectedLevel === 4) {
// --- 5s countdown at 20 points ---
if (!game._dashCountdownActive && !waveMode && score >= 20) {
// Start 5s countdown, block input and update
game._dashCountdownActive = true;
var countdown = 5;
var countdownText = new Text2(countdown + "", {
size: 220,
fill: 0xFFD700,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 12
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 900;
game.addChild(countdownText);
var countdownTimer = LK.setInterval(function () {
countdown--;
if (countdown > 0) {
countdownText.setText(countdown + "");
} else {
LK.clearInterval(countdownTimer);
if (countdownText.parent) countdownText.parent.removeChild(countdownText);
game._dashCountdownActive = false;
// After countdown, enter wave mode
waveMode = true;
// Remove all ground obstacles
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
// Move player to center Y for wave mode (start in mid air at y=700)
player.y = 700;
player.vy = 0;
// Change player sprite to wave asset
if (player.childAt && typeof player.childAt === "function") {
var oldSprite = player.childAt(0);
if (oldSprite) {
player.removeChild(oldSprite);
}
// Attach wave asset, anchor center
var waveSprite = player.attachAsset('Wave', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Enable wave mode controls
player.isWave = true;
// Set player.groundY for after wave mode
if (typeof player.groundY !== "number") {
player.groundY = 2732 - GROUND_HEIGHT;
}
}
}, 1000);
// Remove all obstacles and prevent spawning until countdown ends
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
lastObstacleX = 1200 + 700;
// Set player to wave mode start position, but do not enable wave mode yet
player.x = 2048 / 2;
player.y = 1366;
player.vy = 0;
player.isWave = false;
// Block update and input until countdown ends
return;
}
// Block update and input during countdown
if (game._dashCountdownActive) {
if (player && typeof player.update === "function") player.update();
return;
}
// End wave mode at 75 points
if (waveMode && score >= 75) {
waveMode = false;
// Remove all wave obstacles
for (var i = 0; i < waveObstacles.length; i++) {
waveObstacles[i].destroy && waveObstacles[i].destroy();
}
waveObstacles = [];
// Reset player to ground
player.y = player.groundY;
player.vy = 0;
// Restore player sprite to selected skin
if (player.childAt && typeof player.childAt === "function") {
var oldSprite = player.childAt(0);
if (oldSprite) {
player.removeChild(oldSprite);
}
var newSprite;
if (storage.selectedSkin === "skin_red") {
newSprite = player.attachAsset('Red', {
anchorX: 0.5,
anchorY: 1
});
} else if (storage.selectedSkin === "skin_green") {
newSprite = player.attachAsset('Green', {
anchorX: 0.5,
anchorY: 1
});
} else if (storage.selectedSkin === "skin_gold") {
newSprite = player.attachAsset('Gold', {
anchorX: 0.5,
anchorY: 1
});
} else {
newSprite = player.attachAsset('player', {
anchorX: 0.5,
anchorY: 1
});
var skinColors = {
"skin_red": 0xe74c3c,
"skin_green": 0x2ecc71,
"skin_gold": 0xf1c40f
};
if (storage.selectedSkin && skinColors[storage.selectedSkin] && storage.selectedSkin !== "skin_red" && storage.selectedSkin !== "skin_green" && storage.selectedSkin !== "skin_gold") {
newSprite.color = skinColors[storage.selectedSkin];
}
}
}
// Disable wave mode controls
player.isWave = false;
}
}
// --- Secret Demon Level: always wave mode, no countdown, more/faster obstacles, never disables wave mode ---
if (selectedLevel === 5) {
waveMode = true;
player.isWave = true;
// Always keep player in wave mode, never disable
// Remove all ground obstacles (should be empty, but just in case)
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
// Optionally, increase flying obstacle spawn rate and speed
// (handled below in obstacle spawn section)
}
// Update player
player.update();
// --- NORMAL OBSTACLES ---
if (!waveMode) {
// Update obstacles
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.update();
// Remove if off screen
if (obs.x < -200) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision detection
if (player.intersects(obs)) {
// Explode player into small pieces
if (player && player.parent) {
var numPieces = 12;
var centerX = player.x;
var centerY = player.y;
var pieceSize = 32;
for (var p = 0; p < numPieces; p++) {
(function (pieceIdx) {
var angle = Math.PI * 2 / numPieces * pieceIdx;
var px = centerX;
var py = centerY - PLAYER_SIZE / 2 + pieceSize / 2;
var piece = new Container();
var pieceSprite = piece.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
width: pieceSize,
height: pieceSize
});
// Use same color as player
var playerSpriteRef = player && player.childAt ? player.childAt(0) : null;
if (playerSpriteRef && playerSpriteRef.color) {
pieceSprite.color = playerSpriteRef.color;
}
piece.x = px;
piece.y = py;
game.addChild(piece);
// Animate outward with random velocity and fade out
var speed = 32 + Math.random() * 32;
var dx = Math.cos(angle) * speed;
var dy = Math.sin(angle) * speed - 10 + Math.random() * 20;
tween(piece, {
x: px + dx * 16,
y: py + dy * 16,
alpha: 0
}, {
duration: 700 + Math.random() * 200,
easing: tween.easeOutCubic,
onComplete: function onComplete() {
piece.destroy();
}
});
})(p);
}
// Hide player sprite immediately
player.visible = false;
}
isGameOver = true;
// Delay restart until after explosion animation
LK.setTimeout(function () {
startLevel(selectedLevel);
}, 800);
return;
}
}
}
// --- WAVE MODE OBSTACLES ---
if (waveMode) {
// Kill player if they touch the ground in Dash wave mode (ground is death)
if (selectedLevel === 4 && player.y >= 2732 - GROUND_HEIGHT) {
// Explode player into small pieces
if (player && player.parent) {
var numPieces = 12;
var centerX = player.x;
var centerY = player.y;
var pieceSize = 32;
for (var p = 0; p < numPieces; p++) {
(function (pieceIdx) {
var angle = Math.PI * 2 / numPieces * pieceIdx;
var px = centerX;
var py = centerY - PLAYER_SIZE / 2 + pieceSize / 2;
var piece = new Container();
var pieceSprite = piece.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
width: pieceSize,
height: pieceSize
});
var playerSpriteRef = player && player.childAt ? player.childAt(0) : null;
if (playerSpriteRef && playerSpriteRef.color) {
pieceSprite.color = playerSpriteRef.color;
}
piece.x = px;
piece.y = py;
game.addChild(piece);
var speed = 32 + Math.random() * 32;
var dx = Math.cos(angle) * speed;
var dy = Math.sin(angle) * speed - 10 + Math.random() * 20;
tween(piece, {
x: px + dx * 16,
y: py + dy * 16,
alpha: 0
}, {
duration: 700 + Math.random() * 200,
easing: tween.easeOutCubic,
onComplete: function onComplete() {
piece.destroy();
}
});
})(p);
}
player.visible = false;
}
isGameOver = true;
LK.setTimeout(function () {
startLevel(selectedLevel);
}, 800);
return;
}
// Update flying obstacles
for (var i = waveObstacles.length - 1; i >= 0; i--) {
var wobs = waveObstacles[i];
wobs.update && wobs.update();
// Remove if off screen
if (wobs.x < -200) {
wobs.destroy && wobs.destroy();
waveObstacles.splice(i, 1);
continue;
}
// Collision detection
if (player.intersects(wobs)) {
// Explode player into small pieces
if (player && player.parent) {
var numPieces = 12;
var centerX = player.x;
var centerY = player.y;
var pieceSize = 32;
for (var p = 0; p < numPieces; p++) {
(function (pieceIdx) {
var angle = Math.PI * 2 / numPieces * pieceIdx;
var px = centerX;
var py = centerY - PLAYER_SIZE / 2 + pieceSize / 2;
var piece = new Container();
var pieceSprite = piece.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
width: pieceSize,
height: pieceSize
});
var playerSpriteRef = player && player.childAt ? player.childAt(0) : null;
if (playerSpriteRef && playerSpriteRef.color) {
pieceSprite.color = playerSpriteRef.color;
}
piece.x = px;
piece.y = py;
game.addChild(piece);
var speed = 32 + Math.random() * 32;
var dx = Math.cos(angle) * speed;
var dy = Math.sin(angle) * speed - 10 + Math.random() * 20;
tween(piece, {
x: px + dx * 16,
y: py + dy * 16,
alpha: 0
}, {
duration: 700 + Math.random() * 200,
easing: tween.easeOutCubic,
onComplete: function onComplete() {
piece.destroy();
}
});
})(p);
}
player.visible = false;
}
isGameOver = true;
LK.setTimeout(function () {
startLevel(selectedLevel);
}, 800);
return;
}
}
}
// Score: increase as player passes obstacles
if (!waveMode) {
for (var j = 0; j < obstacles.length; j++) {
var o = obstacles[j];
if (!o.passed && o.x + 60 < player.x) {
o.passed = true;
score += 1;
if (scoreTxt) scoreTxt.setText(score);
}
}
} else {
// In wave mode, score increases as player passes flying obstacles
for (var j = 0; j < waveObstacles.length; j++) {
var wo = waveObstacles[j];
if (!wo.passed && wo.x + 60 < player.x) {
wo.passed = true;
score += 1;
if (scoreTxt) scoreTxt.setText(score);
}
}
}
// Win condition: pass 10 obstacles (easy), 15 (medium), 20 (hard), 30 (Erin’s atventure continuation), 30 (Dash), 50 (Secret Demon)
var winScores = [10, 15, 20, 30, 30, 50];
if (score >= winScores[selectedLevel]) {
isGameOver = true;
// Mark level as completed for coin claim
storage.lastCompletedLevel = selectedLevel;
if (!storage.claimed) {
storage.claimed = {};
}
storage.claimed[selectedLevel] = storage.claimed[selectedLevel] || false;
// Award +150 coins if Erin’s atventure continuation (index 3)
// (Now handled in claim button, not here)
LK.setTimeout(function () {
showLevelIntro();
}, 600);
return;
}
// Spawn new obstacles
if (!waveMode) {
if (obstacles.length === 0 || 2048 - lastObstacleX > OBSTACLE_MIN_GAP + Math.floor(Math.random() * (OBSTACLE_MAX_GAP - OBSTACLE_MIN_GAP))) {
spawnObstacle();
}
} else if (waveMode && selectedLevel === 4 && score < 75) {
// In wave mode, spawn flying spikes/obstacles at random intervals
if (waveObstacles.length === 0 || waveObstacles.length < 4 && Math.random() < 0.04) {
// Randomly choose between flying spike and flying obstacle
var wtype = Math.random() < 0.6 ? 'spike' : 'obstacle';
var wobs;
if (wtype === 'spike') {
wobs = new WaveSpike();
} else {
wobs = new FlyingObstacle();
}
wobs.x = 2048 + 100;
// Random Y in upper/lower half, but not too close to edge
wobs.y = 400 + Math.random() * (2732 - 800);
waveObstacles.push(wobs);
game.addChild(wobs);
}
} else if (waveMode && selectedLevel === 5) {
// Secret Demon: spawn up to 2 flying obstacles, much lower spawn chance, still SLOWER speed
if (waveObstacles.length === 0 || waveObstacles.length < 2 && Math.random() < 0.025) {
var wtype = Math.random() < 0.7 ? 'spike' : 'obstacle';
var wobs;
if (wtype === 'spike') {
wobs = new WaveSpike();
wobs.speed = 18 + Math.random() * 4; // much slower than Dash
} else {
wobs = new FlyingObstacle();
wobs.speed = 18 + Math.random() * 4; // much slower than Dash
}
wobs.x = 2048 + 100;
// Random Y, but allow more vertical spread
wobs.y = 200 + Math.random() * (2732 - 400);
waveObstacles.push(wobs);
game.addChild(wobs);
}
}
};
// (gameover event handler removed, as game over popup is never shown)