/****
* 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
****/
// Obstacle: purple rectangle
// Spike: red triangle (approximate with a tall, thin box for MVP)
// Ground: gray rectangle
// Player: blue square
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!"
}, {
name: "THE REAL SECRET",
difficulty: "EXTREME DEMON",
color: 0x8B0000,
minGap: 180,
maxGap: 280,
types: ['spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike'],
intro: "THE REAL SECRET revealed! This is the ultimate EXTREME DEMON challenge. Only legends survive here!"
}];
// --- 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);
// NEW UPDATES COMING SOON text
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 = 1400;
menuContainer.addChild(updatesText);
// THE REAL SECRET button
var realSecretBtn = new Text2("THE REAL SECRET", {
size: 120,
fill: 0x8B0000,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
realSecretBtn.anchor.set(0.5, 0.5);
realSecretBtn.x = 2048 / 2;
realSecretBtn.y = 1550;
realSecretBtn.interactive = true;
realSecretBtn.buttonMode = true;
realSecretBtn.down = function () {
// Check if player has 200 coins to access THE REAL SECRET
var coins = storage.coins || 0;
if (coins >= 200) {
// Start THE REAL SECRET level (index 6)
selectedLevel = 6;
showLevelIntro();
} else {
// Show insufficient coins message
var insufficientText = new Text2("Need 200 coins to access THE REAL SECRET!\nYou have: " + coins, {
size: 100,
fill: 0xff0000,
align: "center",
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
insufficientText.anchor.set(0.5, 0.5);
insufficientText.x = 2048 / 2;
insufficientText.y = 1800;
menuContainer.addChild(insufficientText);
// Remove message after 3 seconds
LK.setTimeout(function () {
if (insufficientText.parent) {
insufficientText.parent.removeChild(insufficientText);
}
}, 3000);
}
};
menuContainer.addChild(realSecretBtn);
// 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);
}
// THE REAL SECRET button
var realSecretBtn = new Text2("THE REAL SECRET", {
size: 120,
fill: 0x8B0000,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
realSecretBtn.anchor.set(0.5, 0.5);
realSecretBtn.x = 2048 / 2;
realSecretBtn.y = 600 + LEVELS.length * 250 + 50;
realSecretBtn.interactive = true;
realSecretBtn.buttonMode = true;
realSecretBtn.down = function () {
// Check if player has 200 coins to access THE REAL SECRET
var coins = storage.coins || 0;
if (coins >= 200) {
// Start THE REAL SECRET level (index 6)
selectedLevel = 6;
showLevelIntro();
} else {
// Show insufficient coins message
var insufficientText = new Text2("Need 200 coins to access THE REAL SECRET!\nYou have: " + coins, {
size: 100,
fill: 0xff0000,
align: "center",
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
insufficientText.anchor.set(0.5, 0.5);
insufficientText.x = 2048 / 2;
insufficientText.y = 1800;
levelSelectContainer.addChild(insufficientText);
// Remove message after 3 seconds
LK.setTimeout(function () {
if (insufficientText.parent) {
insufficientText.parent.removeChild(insufficientText);
}
}, 3000);
}
};
levelSelectContainer.addChild(realSecretBtn);
// 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 + 200;
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, 150, 100, 200, 300];
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
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 if (levelIdx === 6) {
// THE REAL SECRET: start in center, in air, always wave mode, even more extreme
player.x = 2048 / 2;
player.y = 1366;
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);
}
}
// --- THE REAL SECRET Level: always wave mode, more extreme than Secret Demon ---
if (levelIdx === 6) {
// 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 even fewer static ground spikes for THE REAL SECRET level
var spikeW = 100;
var spikeH = 150;
var groundY = 2732 - GROUND_HEIGHT + 2;
var startX = 80; // more gap at left
var endX = 2048 - 80;
// Much more spacing - every 5 spike widths (extremely few spikes)
for (var sx = startX; sx < endX; sx += spikeW * 5) {
var groundSpike = new Spike();
groundSpike.x = sx + spikeW / 2;
groundSpike.y = groundY;
groundSpike.speed = 0; // static
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)
}
// --- THE REAL SECRET Level: always wave mode, even more extreme than Secret Demon ---
if (selectedLevel === 6) {
waveMode = true;
player.isWave = true;
// Always keep player in wave mode, never disable
// Remove all ground obstacles
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
}
// 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), 100 (THE REAL SECRET)
var winScores = [10, 15, 20, 30, 30, 50, 100];
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);
}
} else if (waveMode && selectedLevel === 6) {
// THE REAL SECRET: spawn up to 3 flying obstacles, higher spawn chance, EXTREME speed
if (waveObstacles.length === 0 || waveObstacles.length < 3 && Math.random() < 0.035) {
var wtype = Math.random() < 0.8 ? 'spike' : 'obstacle';
var wobs;
if (wtype === 'spike') {
wobs = new WaveSpike();
wobs.speed = 35 + Math.random() * 8; // EXTREME speed - faster than Dash
} else {
wobs = new FlyingObstacle();
wobs.speed = 35 + Math.random() * 8; // EXTREME speed - faster than Dash
}
wobs.x = 2048 + 100;
// Random Y with full vertical range for maximum difficulty
wobs.y = 100 + Math.random() * (2732 - 200);
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
****/
// Obstacle: purple rectangle
// Spike: red triangle (approximate with a tall, thin box for MVP)
// Ground: gray rectangle
// Player: blue square
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!"
}, {
name: "THE REAL SECRET",
difficulty: "EXTREME DEMON",
color: 0x8B0000,
minGap: 180,
maxGap: 280,
types: ['spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike', 'obstacle', 'spike', 'spike', 'spike'],
intro: "THE REAL SECRET revealed! This is the ultimate EXTREME DEMON challenge. Only legends survive here!"
}];
// --- 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);
// NEW UPDATES COMING SOON text
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 = 1400;
menuContainer.addChild(updatesText);
// THE REAL SECRET button
var realSecretBtn = new Text2("THE REAL SECRET", {
size: 120,
fill: 0x8B0000,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
realSecretBtn.anchor.set(0.5, 0.5);
realSecretBtn.x = 2048 / 2;
realSecretBtn.y = 1550;
realSecretBtn.interactive = true;
realSecretBtn.buttonMode = true;
realSecretBtn.down = function () {
// Check if player has 200 coins to access THE REAL SECRET
var coins = storage.coins || 0;
if (coins >= 200) {
// Start THE REAL SECRET level (index 6)
selectedLevel = 6;
showLevelIntro();
} else {
// Show insufficient coins message
var insufficientText = new Text2("Need 200 coins to access THE REAL SECRET!\nYou have: " + coins, {
size: 100,
fill: 0xff0000,
align: "center",
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
insufficientText.anchor.set(0.5, 0.5);
insufficientText.x = 2048 / 2;
insufficientText.y = 1800;
menuContainer.addChild(insufficientText);
// Remove message after 3 seconds
LK.setTimeout(function () {
if (insufficientText.parent) {
insufficientText.parent.removeChild(insufficientText);
}
}, 3000);
}
};
menuContainer.addChild(realSecretBtn);
// 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);
}
// THE REAL SECRET button
var realSecretBtn = new Text2("THE REAL SECRET", {
size: 120,
fill: 0x8B0000,
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 8
});
realSecretBtn.anchor.set(0.5, 0.5);
realSecretBtn.x = 2048 / 2;
realSecretBtn.y = 600 + LEVELS.length * 250 + 50;
realSecretBtn.interactive = true;
realSecretBtn.buttonMode = true;
realSecretBtn.down = function () {
// Check if player has 200 coins to access THE REAL SECRET
var coins = storage.coins || 0;
if (coins >= 200) {
// Start THE REAL SECRET level (index 6)
selectedLevel = 6;
showLevelIntro();
} else {
// Show insufficient coins message
var insufficientText = new Text2("Need 200 coins to access THE REAL SECRET!\nYou have: " + coins, {
size: 100,
fill: 0xff0000,
align: "center",
fontWeight: "bold",
stroke: 0x000000,
strokeThickness: 7
});
insufficientText.anchor.set(0.5, 0.5);
insufficientText.x = 2048 / 2;
insufficientText.y = 1800;
levelSelectContainer.addChild(insufficientText);
// Remove message after 3 seconds
LK.setTimeout(function () {
if (insufficientText.parent) {
insufficientText.parent.removeChild(insufficientText);
}
}, 3000);
}
};
levelSelectContainer.addChild(realSecretBtn);
// 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 + 200;
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, 150, 100, 200, 300];
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
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 if (levelIdx === 6) {
// THE REAL SECRET: start in center, in air, always wave mode, even more extreme
player.x = 2048 / 2;
player.y = 1366;
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);
}
}
// --- THE REAL SECRET Level: always wave mode, more extreme than Secret Demon ---
if (levelIdx === 6) {
// 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 even fewer static ground spikes for THE REAL SECRET level
var spikeW = 100;
var spikeH = 150;
var groundY = 2732 - GROUND_HEIGHT + 2;
var startX = 80; // more gap at left
var endX = 2048 - 80;
// Much more spacing - every 5 spike widths (extremely few spikes)
for (var sx = startX; sx < endX; sx += spikeW * 5) {
var groundSpike = new Spike();
groundSpike.x = sx + spikeW / 2;
groundSpike.y = groundY;
groundSpike.speed = 0; // static
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)
}
// --- THE REAL SECRET Level: always wave mode, even more extreme than Secret Demon ---
if (selectedLevel === 6) {
waveMode = true;
player.isWave = true;
// Always keep player in wave mode, never disable
// Remove all ground obstacles
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
}
// 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), 100 (THE REAL SECRET)
var winScores = [10, 15, 20, 30, 30, 50, 100];
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);
}
} else if (waveMode && selectedLevel === 6) {
// THE REAL SECRET: spawn up to 3 flying obstacles, higher spawn chance, EXTREME speed
if (waveObstacles.length === 0 || waveObstacles.length < 3 && Math.random() < 0.035) {
var wtype = Math.random() < 0.8 ? 'spike' : 'obstacle';
var wobs;
if (wtype === 'spike') {
wobs = new WaveSpike();
wobs.speed = 35 + Math.random() * 8; // EXTREME speed - faster than Dash
} else {
wobs = new FlyingObstacle();
wobs.speed = 35 + Math.random() * 8; // EXTREME speed - faster than Dash
}
wobs.x = 2048 + 100;
// Random Y with full vertical range for maximum difficulty
wobs.y = 100 + Math.random() * (2732 - 200);
waveObstacles.push(wobs);
game.addChild(wobs);
}
}
};
// (gameover event handler removed, as game over popup is never shown)