User prompt
same thing for the range of movement of the platforms starting low to high
User prompt
moving platforms in all levels should have random velocity however in level 2 their velocity in general should be slow and it should increase as levels increase slowly otherwise after level 6 they start moving like crazy we don't want that
User prompt
moving platforms should start from level 2, no moving platforms in the first 50
User prompt
never let any platforms pixels touch the opaque sides of the walls
User prompt
increase the movement speed and range of platforms as levels increment
User prompt
create platforms that disappear after a crack effect if player bounces on them once and start using them starting from the 3rd level and increase number of them as levels increment
User prompt
start moving platforms from the second level and increase the number of them as levels increment
User prompt
increase the hardness as the player passes levels not according to the score
User prompt
make sure that each level has 50 platforms
User prompt
move further 10
User prompt
move it further 200
User prompt
position of tutorial texts are right at where I want but I want the container to start from 300px above the first text in it which is the container, there should be some black space between the titla and beginning of the container
User prompt
add a black box over the position where it says how to play
User prompt
move the latest element you added 600 px left
User prompt
move the overlay 600px left
Code edit (8 edits merged)
Please save this source code
User prompt
add a black background that will cover the hole page during the tutorial
Code edit (1 edits merged)
Please save this source code
Code edit (5 edits merged)
Please save this source code
User prompt
make tutorial text and bg in line with Coins: text in UI
User prompt
add a tutorial to the beggining of the game
User prompt
there is a bug I guess It always exits the screen from the right please clear the current logic and try to implement it from 0 using tween.v1 or any other method you think will work ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
use tween.v1 to achieve this ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
please use it directly in code for the last task I asked ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
it still goes to the right and exists screen from there it should just go to scoreTxt.x ↪💡 Consider importing and using the following plugins: @upit/tween.v1
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Cloud class: background only, moves with camera, not collidable var Cloud = Container.expand(function () { var self = Container.call(this); // Pick a random cloud asset var cloudAssets = ['cloudBlue', 'cloudBright', 'cloudFluffy', 'cloudGray', 'cloudSoft']; var assetId = cloudAssets[Math.floor(Math.random() * cloudAssets.length)]; var assetInfo = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale clouds to a reasonable size for background var targetHeight = 180 + Math.random() * 80; // 180-260px var scale = targetHeight / assetInfo.height; var cloudGfx = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale, alpha: 0.45 + Math.random() * 0.15 // more transparent clouds }); // Set initial position (x, y) and speed self.x = Math.random() * 2048; self.y = Math.random() * 2048; self.speedY = 0; // Will be set by camera movement // Give some clouds a gentle horizontal drift self.driftX = (Math.random() < 0.5 ? -1 : 1) * (0.2 + Math.random() * 0.5); // Clouds are always behind everything self.setToBack = function () { if (self.parent && self.parent.children.indexOf(self) > 0) { self.parent.setChildIndex(self, 0); } }; // No collision, no input self.update = function () { // Track lastX for border collision detection if (typeof self.lastX === "undefined") { self.lastX = self.x; } // Horizontal drift self.x += self.driftX; // Clamp clouds to transparent area and bounce on hitting opaque bg borders var minCloudX = BORDER_LEFT_X; var maxCloudX = BORDER_RIGHT_X; // Bounce off left opaque border if (self.lastX > minCloudX && self.x <= minCloudX) { self.x = minCloudX; self.driftX = Math.abs(self.driftX); // move right } // Clamp left if (self.x < minCloudX) { self.x = minCloudX; self.driftX = Math.abs(self.driftX); // move right } // Bounce off right opaque border if (self.lastX < maxCloudX && self.x >= maxCloudX) { self.x = maxCloudX; self.driftX = -Math.abs(self.driftX); // move left } // Clamp right if (self.x > maxCloudX) { self.x = maxCloudX; self.driftX = -Math.abs(self.driftX); // move left } // Update lastX for next frame self.lastX = self.x; // Vertical movement is handled by camera diff in game.update }; return self; }); // --- Coin class: collectible, always same visual size, shows value popup --- var Coin = Container.expand(function () { var self = Container.call(this); // Coin type: 1, 2, or 3 (for +1, +3, +5) self.coinType = 1; self.value = 1; self.assetId = 'chipCoin1'; // Set asset and value based on type if (typeof arguments[0] === "number") { if (arguments[0] === 1) { self.coinType = 1; self.value = 1; self.assetId = 'chipCoin1'; } else if (arguments[0] === 2) { self.coinType = 2; self.value = 3; self.assetId = 'chipCoin2'; } else if (arguments[0] === 3) { self.coinType = 3; self.value = 5; self.assetId = 'chipCoin3'; } } // Always render coins at the same visual size (height 80px) var targetHeight = 80; var assetInfo = LK.getAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); var scale = targetHeight / assetInfo.height; var coinGfx = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale }); // For collision self.radius = assetInfo.width * scale / 2; // Show value popup when collected self.showValuePopup = function () { var displayValue = self.value * (typeof coinValueMultiplier !== "undefined" ? coinValueMultiplier : 1); var txt = new Text2('+' + displayValue, { size: 90, // Larger for more impact fill: self.value === 1 ? "#ffe066" : self.value === 3 ? "#a78bfa" : "#38bdf8", font: "Impact" }); txt.anchor.set(0.5, 0.5); txt.x = self.x; txt.y = self.y - 60; // Higher above coin txt.scale.set(1.7, 1.7); // Start big txt.alpha = 0.0; // Start invisible game.addChild(txt); // Ensure popup is above player if (txt.parent && player && txt.parent.children.indexOf(txt) < txt.parent.children.indexOf(player)) { txt.parent.setChildIndex(txt, txt.parent.children.length - 1); } // Flash coin for feedback if (self.children && self.children[0]) { var coinGfx = self.children[0]; tween(coinGfx, { tint: 0xffffff }, { duration: 80, onFinish: function onFinish() { tween(coinGfx, { tint: 0xffe066 }, { duration: 120 }); } }); } // Animate popup: fade in, bounce, then float up and fade out tween(txt, { alpha: 1, scaleX: 1.1, scaleY: 1.1 }, { duration: 120, onFinish: function onFinish() { tween(txt, { y: txt.y - 120, scaleX: 0.7, scaleY: 0.7, alpha: 0 }, { duration: 700, onFinish: function onFinish() { txt.destroy(); } }); } }); }; return self; }); // --- PlatformSign class: shows a sign with platforms passed on the last platform of each theme --- var PlatformSign = Container.expand(function () { var self = Container.call(this); // Use a simple box as the sign background var signW = 220, signH = 110; var signBg = self.attachAsset('chipPlatform0', { anchorX: 0.5, anchorY: 1, scaleX: signW / 820, scaleY: signH / 110, color: 0x22223b }); // Text label for platforms passed var signTxt = new Text2('0', { size: 60, fill: "#fff", font: "Impact" }); signTxt.anchor.set(0.5, 0.5); signTxt.x = 0; signTxt.y = -signH / 2; self.addChild(signTxt); // Set the value to display self.setPlatformsPassed = function (val) { signTxt.setText(val + ""); }; return self; }); // PokerChip class: simple collectible poker chip (not draggable, not rotatable) var PokerChip = Container.expand(function () { var self = Container.call(this); // Asset id and color are passed in, default to 'chipCoin1' self.assetId = self.assetId || 'chipCoin1'; var chipAssetInfo = LK.getAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); // Always render poker chips at a consistent visual size (height 80px) var targetHeight = 80; var scale = targetHeight / chipAssetInfo.height; var chip = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale }); // For hit testing self.radius = chipAssetInfo.width * scale / 2; // Show value popup when collected (optional, can be customized) self.value = 1; self.showValuePopup = function () { var txt = new Text2('+1', { size: 90, // Larger for more impact fill: 0xFFE066, font: "Impact" }); txt.anchor.set(0.5, 0.5); txt.x = self.x; txt.y = self.y - 60; txt.scale.set(1.7, 1.7); // Start big txt.alpha = 0.0; // Start invisible game.addChild(txt); // Animate popup: fade in, bounce, then float up and fade out tween(txt, { alpha: 1, scaleX: 1.1, scaleY: 1.1 }, { duration: 120, onFinish: function onFinish() { tween(txt, { y: txt.y - 120, scaleX: 0.7, scaleY: 0.7, alpha: 0 }, { duration: 700, onFinish: function onFinish() { txt.destroy(); } }); } }); }; return self; }); // --- Star class: collectible, animated, optimized for performance --- var Star = Container.expand(function () { var self = Container.call(this); // Use the new collectibleStar asset self.assetId = 'collectibleStar'; self.value = 10; // Stars are worth +10 var assetInfo = LK.getAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); // Always render stars at the same visual size (height 70px) var targetHeight = 70; var scale = targetHeight / assetInfo.height; var starGfx = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale, alpha: 0.92 }); // For collision self.radius = assetInfo.width * scale / 2; // Animate star: slow rotation and pulsing self.update = function () { if (starGfx) { starGfx.rotation += 0.09; var pulse = 0.95 + 0.08 * Math.sin(Date.now() / 180 + self.x); starGfx.scale.x = scale * pulse; starGfx.scale.y = scale * pulse; } }; // Show value popup when collected self.showValuePopup = function () { var displayValue = self.value * (typeof coinValueMultiplier !== "undefined" ? coinValueMultiplier : 1); var txt = new Text2('+' + displayValue, { size: 90, // Larger for more impact fill: 0xFFF7B2, font: "Impact" }); txt.anchor.set(0.5, 0.5); txt.x = self.x; txt.y = self.y - 60; txt.scale.set(1.7, 1.7); // Start big txt.alpha = 0.0; // Start invisible game.addChild(txt); if (txt.parent && player && txt.parent.children.indexOf(txt) < txt.parent.children.indexOf(player)) { txt.parent.setChildIndex(txt, txt.parent.children.length - 1); } // Animate popup: fade in, bounce, then float up and fade out tween(txt, { alpha: 1, scaleX: 1.1, scaleY: 1.1 }, { duration: 120, onFinish: function onFinish() { tween(txt, { y: txt.y - 120, scaleX: 0.7, scaleY: 0.7, alpha: 0 }, { duration: 700, onFinish: function onFinish() { txt.destroy(); } }); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x18181b }); /**** * Game Code ****/ // Collectible star asset (yellow star shape) // Cloud shape assets for customizable cloud looks // --- Icy Tower Constants --- // Additional chip assets for more levels var highScore = storage.highScore || 0; var GAME_W = 2048; var GAME_H = 2732; var PLATFORM_W = 400; // Define new left/right border X values at the intersection of opaque and transparent background var BORDER_LEFT_X = Math.floor(GAME_W / 30); var BORDER_RIGHT_X = GAME_W - Math.floor(GAME_W / 30); var PLATFORM_H = 110; var PLATFORM_SPACING_MIN = 320; var PLATFORM_SPACING_MAX = 440; var PLAYER_W = 170; var PLAYER_H = 170; var GRAVITY = 2.2; var JUMP_VELOCITY = -62; var MOVE_SPEED = 22; var PLATFORM_X_MARGIN = 120; var CAMERA_OFFSET = 900; // How far from bottom the player is kept // --- Cloud background state --- var clouds = []; var NUM_CLOUDS = 8; // Number of clouds to show in background // --- State --- var platforms = []; var coins = []; // All active coins var stars = []; // All active stars var player = null; var vy = 0; var vx = 0; var isJumping = false; var isTouching = false; var touchStartX = 0; // --- Coin value multiplier: increases 10x per theme --- var coinValueMultiplier = 1; // Track all active touches (for multi-touch) var activeTouches = []; var cameraY = 0; var maxHeight = 0; var gameOver = false; game.hasDoubleJumped = false; // --- Combo Logic State --- var comboActive = false; // true if in a combo sequence var comboMultiplier = 1; // 1, 2, 4, 8, 16, ... var comboLastWall = null; // "left" or "right" var comboPlatformsLanded = 0; // platforms landed since last double jump var comboLastPlatform = null; // reference to last platform landed var comboJustDoubleJumped = false; // true for 1 frame after double jump (to track landing) var comboLastScore = 0; // score before last double jump // Track if we are currently emitting stars after a double jump var isEmittingStars = false; var emitStarTimer = 0; var emitStarInterval = 2; // emit a star every 2 frames (about 30 per second) var garavelSounds = ['garavel-1', 'garavel-2', 'garavel-3', 'garavel-4', 'garavel-5']; var garavelSoundIndex = 0; function playNextGaravelSound() { LK.getSound(garavelSounds[garavelSoundIndex]).play(); garavelSoundIndex = (garavelSoundIndex + 1) % garavelSounds.length; } // --- Character --- var DEFAULT_CHARACTER_ASSET_ID = 'chipCharacter'; // Level themes: background color and platform asset per level // Platform color is always high-contrast with background for visibility var LEVEL_THEMES = [{ // 1 Grass bg: 0x18181b, platformAsset: 'chipPlatform1', platformColor: 0xfacc15 }, { // 2 Forest bg: 0x1e293b, platformAsset: 'chipPlatform2', platformColor: 0xf1f5f9 }, { // 3 Mud bg: 0x3b1e1e, platformAsset: 'chipPlatform3', platformColor: 0xffffff }, { // 4 Magic bg: 0x2d1e3b, platformAsset: 'chipPlatform4', platformColor: 0xffe066 }, { // 5 Candy bg: 0x3b2d1e, platformAsset: 'chipPlatform5', platformColor: 0x22223b }, { // 6 Night bg: 0x1e3b2d, platformAsset: 'chipPlatform6', platformColor: 0xf8fafc }, { // 7 Stone bg: 0x3b1e2d, platformAsset: 'chipPlatform7', platformColor: 0x22223b }, { // 8 Ice bg: 0x1e2d3b, platformAsset: 'chipPlatform8', platformColor: 0x22223b }, { // 9 Metal bg: 0x2d3b1e, platformAsset: 'chipPlatform9', platformColor: 0xf8fafc }, { // 10 Neon bg: 0x000000, platformAsset: 'chipPlatform10', platformColor: 0xfacc15 }, { // 11 Special (chipPlatform11) bg: 0x22223b, platformAsset: 'chipPlatform11', platformColor: 0xffffff }]; // Helper to get current theme index based on platformsPassed/level function getThemeIndex(score) { // Every level lasts for 50 platforms var idx = Math.floor(platformsPassed / 50); if (idx < 0) { idx = 0; } if (idx >= LEVEL_THEMES.length) { idx = LEVEL_THEMES.length - 1; } return idx; } // Helper to get current theme object function getCurrentTheme(score) { return LEVEL_THEMES[getThemeIndex(score)]; } // Used for initial platform asset (will be replaced in createPlatform) var platformAsset = LK.getAsset('chipPlatform8', { anchorX: 0.5, anchorY: 0.5, scaleX: PLATFORM_W / 1100, scaleY: PLATFORM_H / 700 }); // --- UI --- // Platform pass counter var platformsPassed = 0; // Track number of platforms created (for UI only, not for scoring) var platformsCreated = 0; // Track total coins collected var coinsCollected = 0; // --- Score & Coins UI --- // Create a container for the UI background and labels var uiContainer = new Container(); // Add a black rectangle background behind the UI (covers full top width, rectangle shape) var uiBgRect = LK.getAsset('uiTopBgRect', { anchorX: 0, anchorY: 0, x: -600, y: 0, scaleX: GAME_W / 100, scaleY: 1 }); uiContainer.addChild(uiBgRect); var uiBgWidth = GAME_W; var uiBgHeight = 110; // "Coins: <number>" label (first, left) var coinsLabelTxt = new Text2('Coins: 0', { size: 70, fill: 0xFFE066, font: "Impact" }); coinsLabelTxt.anchor.set(0, 0.5); // left aligned, vertically centered coinsLabelTxt.x = 60 - 300; // Move 400px to the left coinsLabelTxt.y = uiBgHeight / 2; // "Score:" label (right of coins, with extra spacing) var scoreLabelTxt = new Text2('Score:', { size: 70, fill: "#fff", font: "Impact" }); scoreLabelTxt.anchor.set(0, 0.5); // left aligned, vertically centered scoreLabelTxt.x = 50 + coinsLabelTxt.width; // Keep Score UI in original place scoreLabelTxt.y = uiBgHeight / 2; // Score value (right of label, with spacing) var scoreTxt = new Text2('0', { size: 80, fill: "#fff", font: "Impact" }); scoreTxt.anchor.set(0, 0.5); // left aligned, vertically centered scoreTxt.x = scoreLabelTxt.x + scoreLabelTxt.width + 30; scoreTxt.y = uiBgHeight / 2; // Add all to container in new order: coins, score label, score value uiContainer.addChild(coinsLabelTxt); uiContainer.addChild(scoreLabelTxt); uiContainer.addChild(scoreTxt); // Center the UI container at the top of the screen, shifted 200px left uiContainer.x = -200; uiContainer.y = 0; // Add to GUI overlay (top center) LK.gui.top.addChild(uiContainer); // Proper high score text object for updating best score var highScoreTxt = new Text2('', { size: 60, fill: "#fff", font: "Impact" }); highScoreTxt.anchor.set(1, 0.5); // right aligned, vertically centered highScoreTxt.x = GAME_W - 60; highScoreTxt.y = uiBgHeight / 2; uiContainer.addChild(highScoreTxt); // Listen for game over and update LK score to platformsPassed + coinsCollected LK.on('gameover', function () { // Always use the actual variables for score and coins, not UI text var scoreVal = typeof platformsPassed !== "undefined" ? platformsPassed : 0; var coinsVal = typeof coinsCollected !== "undefined" ? coinsCollected : 0; LK.setScore(scoreVal + coinsVal); }); // --- Helper: create a platform at (x, y) --- function createPlatform(x, y, width) { var plat = new Container(); // Make each new platform a bit harder as level increases // Find the last platform's width if any, otherwise use PLATFORM_W var lastPlat = platforms.length > 0 ? platforms[platforms.length - 1] : null; var prevW = lastPlat && lastPlat.width ? lastPlat.width : PLATFORM_W; var level = getThemeIndex(platformsPassed); // Platform width shrinks with level, min 220, max 1100 // Make width decrease more gradually per level (every 20 platforms) var baseW = 820 - level * 40; if (baseW < 220) { baseW = 220; } var w; // Platform width logic (no checkpoint logic) var platNum = platformsPassed + platforms.length; var themeLength = 50; // Each theme has 50 platforms var w; if (typeof width === "number") { w = width; } else { // Add more variety: sometimes make platforms much narrower or wider var variety = Math.random(); if (variety < 0.12 && platNum > 10) { // 12% chance: very narrow platform (challenge) w = Math.max(160, baseW * 0.45 + Math.random() * 60); } else if (variety > 0.92 && platNum > 10) { // 8% chance: very wide platform (reward) w = Math.min(baseW * 1.5, 1100); } else { // Each new platform is 4% wider than the previous, but capped by baseW for the level w = Math.min(prevW * (1.04 + (Math.random() - 0.5) * 0.08), baseW); } } // Determine theme based on platform index counter for new platforms // Ensure each theme has at most 50 platforms var platformIdx = typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed; var themeIdx = Math.floor(platformIdx / 50); if (themeIdx >= LEVEL_THEMES.length) { themeIdx = LEVEL_THEMES.length - 1; } var theme = LEVEL_THEMES[themeIdx]; var assetId = theme.platformAsset; var platformColor = theme.platformColor; // Get original asset size for aspect ratio var assetInfo = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); var origW = assetInfo.width; var origH = assetInfo.height; var scaleX = w / origW; var scaleY = PLATFORM_H / origH; // To preserve aspect ratio, use the smaller scale var scale = Math.min(scaleX, scaleY); var platGfx = plat.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale, color: platformColor }); plat.x = x; plat.y = y; plat.width = w; plat.height = PLATFORM_H; plat.PLATFORM_H = PLATFORM_H; game.addChild(plat); platforms.push(plat); // --- Add moving platforms for extra fun --- // 15% chance for a moving platform, but not for the first 10 platforms if (platNum > 10 && Math.random() < 0.15) { plat._isMoving = true; plat._moveDir = Math.random() < 0.5 ? -1 : 1; plat._moveRange = 180 + Math.random() * 220; plat._moveSpeed = 2.2 + Math.random() * 1.8; plat._moveOriginX = plat.x; plat.update = function () { // Move back and forth horizontally plat.x = plat._moveOriginX + Math.sin(Date.now() / (420 - plat._moveSpeed * 60)) * plat._moveRange * plat._moveDir; }; } // --- Add platform gaps for challenge (skip platform creation) --- // 10% chance to skip a platform (gap), but never two in a row, and not for the first 10 platforms if (platNum > 10 && Math.random() < 0.10 && lastPlat && !lastPlat._wasGap) { plat._wasGap = true; // Remove this platform immediately to create a gap plat.destroy(); platforms.pop(); return null; } // Increment platformsCreated and update scoreTxt if (typeof platformsCreated === "undefined") { platformsCreated = 0; } platformsCreated += 1; if (typeof scoreTxt !== "undefined" && scoreTxt.setText) { scoreTxt.setText(platformsCreated.toString()); } // --- Add platform number label to every 50th platform only (not all) --- if (platforms.length > 1) { if (typeof createPlatform.platformIndex === "undefined") { createPlatform.platformIndex = 1; } var platformNumber = createPlatform.platformIndex; var themeIdxForPlat = Math.floor((platformNumber - 1) / 50); var themePlatformNumber = (platformNumber - 1) % 50 + 1; // Only allow up to 50 platforms per theme if (themePlatformNumber > 50) { return plat; } createPlatform.platformIndex++; // Only add label if this is a 50th platform (e.g. 50, 100, 150, ...) if (platformNumber % 50 === 0) { // Add a black opaque background behind the platform label, sized to tightly fit the label text var counterTxt = new Text2(platformNumber + "", { size: 18, fill: "#fff", font: "Impact" }); counterTxt.anchor.set(0.5, 0.5); counterTxt.x = 0; counterTxt.y = 0; // Calculate background size to tightly fit the label text (with a small padding) var paddingX = 16; var paddingY = 8; var labelBgW = counterTxt.width + paddingX; var labelBgH = counterTxt.height + paddingY; var scaleX = labelBgW / 100; var scaleY = labelBgH / 110; var labelBg = LK.getAsset('uiTopBgRect', { anchorX: 0.5, anchorY: 0.5, scaleX: scaleX, scaleY: scaleY, alpha: 1 }); labelBg.x = 0; labelBg.y = 0; plat.addChild(labelBg); plat.addChild(counterTxt); } } // --- Coin and Star placement logic --- // Don't place coins or stars on the first platform (player start) if (platforms.length > 1) { // --- Intelligent collectible appearance logic --- // Coins: 80% chance to spawn a coin on a platform, with type based on platform number var coinChance = 0.8; if (Math.random() < coinChance) { // Make higher value coins rarer and more likely on higher platforms var platNum = platformsPassed + platforms.length; var r = Math.random(); var coinType = 1; if (platNum > 200 && r > 0.92) { coinType = 3; } // +5 coin, rare after 200 else if (platNum > 100 && r > 0.85) { coinType = 2; } // +3 coin, more likely after 100 else if (r > 0.95) { coinType = 3; } // +5 coin, very rare early else if (r > 0.8) { coinType = 2; } // +3 coin, uncommon var coin = new Coin(coinType); coin.x = plat.x; coin.y = plat.y - PLATFORM_H / 2 - 40; game.addChild(coin); coins.push(coin); } // Stars: 20% chance to spawn a star, but always spawn on every 10th platform var platNum = platformsPassed + platforms.length; var starChance = 0.2; if (platNum % 10 === 0 || Math.random() < starChance) { var star = new Star(); star.x = plat.x + (Math.random() - 0.5) * (plat.width * 0.4); // randomize a bit star.y = plat.y - PLATFORM_H / 2 - 100; game.addChild(star); stars.push(star); } } // --- Add a sign to the last platform of each theme --- var themeIdxForPlat = getThemeIndex(platformsPassed + platforms.length - 1); var isLastOfTheme = (platformsPassed + platforms.length) % 50 === 0; if (isLastOfTheme) { var sign = new PlatformSign(); sign.x = plat.x; sign.y = plat.y - PLATFORM_H / 2 - 10; sign.setPlatformsPassed(platformsPassed + platforms.length); game.addChild(sign); plat._themeSign = sign; } return plat; } // --- Helper: find a safe X for a new platform, given previous platform --- function getSafePlatformX(prevPlat, width) { // Always keep new platform horizontally reachable from previous var minX = Math.max(PLATFORM_X_MARGIN + width / 2, prevPlat ? prevPlat.x - 400 : PLATFORM_X_MARGIN + width / 2); var maxX = Math.min(GAME_W - PLATFORM_X_MARGIN - width / 2, prevPlat ? prevPlat.x + 400 : GAME_W - PLATFORM_X_MARGIN - width / 2); if (minX > maxX) { minX = maxX = prevPlat ? prevPlat.x : GAME_W / 2; } return minX + Math.random() * (maxX - minX); } // --- Helper: reset game state --- function resetGame() { // Remove old coins for (var i = coins.length - 1; i >= 0; --i) { if (coins[i] && typeof coins[i].destroy === "function") { coins[i].destroy(); } } coins = []; // Remove old stars for (var i = stars.length - 1; i >= 0; --i) { if (stars[i] && typeof stars[i].destroy === "function") { stars[i].destroy(); } } stars = []; // Remove old platforms for (var i = 0; i < platforms.length; ++i) { platforms[i].destroy(); } platforms = []; // Remove player if (player) { player.destroy(); } // Create player player = new Container(); var pGfx = player.attachAsset(typeof selectedCharacterAssetId !== "undefined" ? selectedCharacterAssetId : DEFAULT_CHARACTER_ASSET_ID, { anchorX: 0.5, anchorY: 1, scaleX: PLAYER_W / 640, scaleY: PLAYER_H / 640 }); player.x = GAME_W / 2; player.y = GAME_H - 400; player.width = PLAYER_W; player.height = PLAYER_H; game.addChild(player); vy = 0; vx = 0; isJumping = false; isTouching = false; cameraY = 0; maxHeight = 0; gameOver = false; // --- Reset combo state --- comboActive = false; comboMultiplier = 1; comboLastWall = null; comboPlatformsLanded = 0; comboLastPlatform = null; comboJustDoubleJumped = false; comboLastScore = 0; // Create initial platforms var y = GAME_H - 120; // --- Add platform0 as the very first platform --- var platform0AssetId = 'chipPlatform0'; var platform0AssetInfo = LK.getAsset(platform0AssetId, { anchorX: 0.5, anchorY: 0.5 }); var platform0W = platform0AssetInfo.width; var platform0H = platform0AssetInfo.height; var platform0Scale = PLATFORM_H / platform0H; var platform0 = new Container(); var platform0Gfx = platform0.attachAsset(platform0AssetId, { anchorX: 0.5, anchorY: 0.5, scaleX: platform0Scale, scaleY: platform0Scale }); platform0.x = GAME_W / 2; platform0.y = y; platform0.width = platform0W * platform0Scale; platform0.height = PLATFORM_H; platform0.PLATFORM_H = PLATFORM_H; game.addChild(platform0); platforms.push(platform0); // Place player directly above platform0, so the game starts after landing on this platform player.x = platform0.x; player.y = platform0.y - PLATFORM_H / 2 - PLAYER_H / 2 + 10; // Center player above platform, slightly above // Create initial platforms createPlatform.platformIndex = 1; // Reset platform index counter at game start var level = getThemeIndex(platformsPassed); var EASY_PLATFORM_W = 820 - level * 40; if (EASY_PLATFORM_W < 220) { EASY_PLATFORM_W = 220; } // Increase vertical spacing between platforms for more air time var EASY_PLATFORM_SPACING = 310; var prevPlat = platform0; y -= EASY_PLATFORM_SPACING; for (var i = 1; i < 12; ++i) { var px = getSafePlatformX(prevPlat, EASY_PLATFORM_W); var plat = createPlatform(px, y, EASY_PLATFORM_W); prevPlat = plat; y -= EASY_PLATFORM_SPACING; } // Sort platforms by y platforms.sort(function (a, b) { return a.y - b.y; }); // Score platformsPassed = 0; coinsCollected = 0; // Count all platforms currently on screen as created (including platform0 and the 11 created in the loop) platformsCreated = platforms.length; scoreTxt.setText(platformsCreated.toString()); highScoreTxt.setText('Best: ' + highScore); // Initialize platformsRemoved counter platformsRemoved = 0; // Reset theme and background game.lastThemeIndex = -1; // Use bgTheme asset classes for background var themeIdx = getThemeIndex(platformsPassed); // Map for each theme: [opaqueAsset, transparentAsset] var bgThemeAssets = [['bgThemeGrassOpaque', 'bgThemeGrassTrans'], // 0 Grass ['bgThemeForestOpaque', 'bgThemeForestTrans'], // 1 Forest ['bgThemeMudOpaque', 'bgThemeMudTrans'], // 2 Mud ['bgThemeMagicOpaque', 'bgThemeMagicTrans'], // 3 Magic ['bgThemeCandyOpaque', 'bgThemeCandyTrans'], // 4 Candy ['bgThemeNightOpaque', 'bgThemeNightTrans'], // 5 Night ['bgThemeStoneOpaque', 'bgThemeStoneTrans'], // 6 Stone ['bgThemeIceOpaque', 'bgThemeIceTrans'], // 7 Ice ['bgThemeMetalOpaque', 'bgThemeMetalTrans'], // 8 Metal ['bgThemeNeonOpaque', 'bgThemeNeonTrans'], // 9 Neon ['bgThemeSpecialOpaque', 'bgThemeSpecialTrans'] // 10 Special ]; if (themeIdx < 0) { themeIdx = 0; } if (themeIdx >= bgThemeAssets.length) { themeIdx = bgThemeAssets.length - 1; } // Remove previous background asset if any if (game._bgThemeAsset) { if (typeof game._bgThemeAsset.destroy === "function") { game._bgThemeAsset.destroy(); } game._bgThemeAsset = null; } // Add new background asset as the first child (behind everything) var bgOpaqueAssetId = bgThemeAssets[themeIdx][0]; var bgTransAssetId = bgThemeAssets[themeIdx][1]; // Create a container for the background with three segments: left, center, right var bgThemeContainer = new Container(); var bgOpaqueAssetInfo = LK.getAsset(bgOpaqueAssetId, { anchorX: 0, anchorY: 0 }); var bgTransAssetInfo = LK.getAsset(bgTransAssetId, { anchorX: 0, anchorY: 0 }); var bgWidth = GAME_W; var leftW = Math.floor(bgWidth / 30); var rightW = Math.floor(bgWidth / 30); var centerW = bgWidth - leftW - rightW; // Left 1/30 (opaque) var bgLeft = LK.getAsset(bgOpaqueAssetId, { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: leftW / bgOpaqueAssetInfo.width, scaleY: GAME_H / bgOpaqueAssetInfo.height, alpha: 1 }); bgThemeContainer.addChild(bgLeft); // Center (transparent) var bgCenter = LK.getAsset(bgTransAssetId, { anchorX: 0, anchorY: 0, x: leftW, y: 0, scaleX: centerW / bgTransAssetInfo.width, scaleY: GAME_H / bgTransAssetInfo.height, alpha: 0.3 }); bgThemeContainer.addChild(bgCenter); // Right 1/30 (opaque) var bgRight = LK.getAsset(bgOpaqueAssetId, { anchorX: 0, anchorY: 0, x: leftW + centerW, y: 0, scaleX: rightW / bgOpaqueAssetInfo.width, scaleY: GAME_H / bgOpaqueAssetInfo.height, alpha: 1 }); bgThemeContainer.addChild(bgRight); game.addChild(bgThemeContainer); if (game.children && game.children.length > 1) { game.setChildIndex(bgThemeContainer, 0); } game._bgThemeAsset = bgThemeContainer; // Remove old clouds for (var i = clouds.length - 1; i >= 0; --i) { if (clouds[i] && typeof clouds[i].destroy === "function") { clouds[i].destroy(); } } clouds = []; // Add new clouds to background for (var i = 0; i < NUM_CLOUDS; ++i) { var cloud = new Cloud(); // Distribute vertically in the upper 2/3 of the screen cloud.y = Math.random() * (GAME_H * 0.66); cloud.x = Math.random() * GAME_W; game.addChild(cloud); cloud.setToBack && cloud.setToBack(); clouds.push(cloud); } // (Cloud background reset removed) } // --- Helper: check collision between player and platform --- function playerOnPlatform() { for (var i = 0; i < platforms.length; ++i) { var plat = platforms[i]; // Only check if player is falling if (vy >= 0) { var px = player.x; var py = player.y; var platTop = plat.y - (plat.PLATFORM_H ? plat.PLATFORM_H : PLATFORM_H) / 2; var platLeft = plat.x - plat.width / 2; var platRight = plat.x + plat.width / 2; // Use previous player y for more reliable collision if (typeof player.lastY === "undefined") { player.lastY = py - vy; } // Check if player's feet crossed the platform top this frame if (player.lastY <= platTop && py >= platTop) { // Require player to be horizontally within platform bounds (with a small margin) if (px > platLeft + 10 && px < platRight - 10) { player.lastY = py; // update for next frame return plat; } } } } if (typeof player !== "undefined" && player !== null) { player.lastY = player.y; } return null; } // --- Touch controls: left/right jump --- game.down = function (x, y, obj) { if (gameOver) { return; } // Add this touch to activeTouches if (obj && obj.event && typeof obj.event.identifier !== "undefined") { // Remove if already present (shouldn't happen, but for safety) for (var i = 0; i < activeTouches.length; ++i) { if (activeTouches[i].id === obj.event.identifier) { activeTouches.splice(i, 1); break; } } activeTouches.push({ id: obj.event.identifier, x: x, y: y }); } else { // Fallback for mouse or single touch activeTouches = [{ id: 0, x: x, y: y }]; } isTouching = true; touchStartX = x; // Always use the last finger down for direction, even after double jump var lastTouch = activeTouches[activeTouches.length - 1]; if (lastTouch && lastTouch.x < GAME_W / 2) { vx = -MOVE_SPEED; } else { vx = MOVE_SPEED; } // If player is on ground/platform, jump if (!isJumping && !game.hasDoubleJumped) { vy = JUMP_VELOCITY; isJumping = true; } // No-op: direction change after double jump is handled in move handler }; game.up = function (x, y, obj) { // Remove this touch from activeTouches if (obj && obj.event && typeof obj.event.identifier !== "undefined") { for (var i = 0; i < activeTouches.length; ++i) { if (activeTouches[i].id === obj.event.identifier) { activeTouches.splice(i, 1); break; } } } else { // Fallback for mouse or single touch activeTouches = []; } if (activeTouches.length === 0) { isTouching = false; vx = 0; } else { // Use the new last finger for direction, even after double jump var lastTouch = activeTouches[activeTouches.length - 1]; if (lastTouch && lastTouch.x < GAME_W / 2) { vx = -MOVE_SPEED; } else { vx = MOVE_SPEED; } } }; game.move = function (x, y, obj) { // Update the position of the moving finger in activeTouches if (obj && obj.event && typeof obj.event.identifier !== "undefined") { for (var i = 0; i < activeTouches.length; ++i) { if (activeTouches[i].id === obj.event.identifier) { activeTouches[i].x = x; activeTouches[i].y = y; break; } } } else if (activeTouches.length > 0) { // Fallback for mouse or single touch activeTouches[activeTouches.length - 1].x = x; activeTouches[activeTouches.length - 1].y = y; } // Always use the last finger for direction, even after double jump or wall hit if (activeTouches.length > 0 && !gameOver) { var lastTouch = activeTouches[activeTouches.length - 1]; if (lastTouch && lastTouch.x < GAME_W / 2) { vx = -MOVE_SPEED; } else { vx = MOVE_SPEED; } } else if (!isTouching) { vx = 0; } // No else: vx is always set while touching, even after double jump }; // --- Main update loop --- game.update = function () { if (gameOver) { return; } // Physics if (player) { if (typeof player.lastVy === "undefined") { player.lastVy = vy; } if (typeof player.lastVx === "undefined") { player.lastVx = vx; } } // Smooth velocity interpolation for mobile performance // Use a simple inertia/lerp for vx to avoid abrupt changes var targetVx = vx; if (player) { if (typeof player.smoothVx === "undefined") { player.smoothVx = vx; } // Increase lerp factor for even smoother and more responsive movement player.smoothVx += (targetVx - player.smoothVx) * 0.32; // 0.32 is more responsive and smooth vy += GRAVITY; player.x += player.smoothVx; player.y += vy; player.lastVx = player.smoothVx; } // Clamp player to transparent area and bounce on hitting opaque bg borders if (player && typeof player.x === "number") { // Track lastX for edge detection if (typeof player.lastX === "undefined") { player.lastX = player.x; } var playerLeft = player.x - PLAYER_W / 2; var playerRight = player.x + PLAYER_W / 2; var leftOpaqueX = BORDER_LEFT_X; var rightOpaqueX = BORDER_RIGHT_X; // --- Left wall double jump: trigger as soon as player's leftmost pixel touches rightmost pixel of left opaque bg --- if (player.lastX - PLAYER_W / 2 > leftOpaqueX && playerLeft <= leftOpaqueX) { player.x = leftOpaqueX + PLAYER_W / 2; // Tumble: rotate player quickly if (player && player.children && player.children.length > 0) { var pGfx = player.children[0]; tween(pGfx, { rotation: pGfx.rotation - Math.PI * 2 }, { duration: 400, onFinish: function onFinish() { pGfx.rotation = 0; } }); } // Play shout sound when hitting left border, with cooldown if (typeof game.lastShoutTime === "undefined") { game.lastShoutTime = 0; } var nowWall = Date.now(); if (nowWall - game.lastShoutTime > 500) { game.lastShoutTime = nowWall; } // Double jump when hitting the border, but only if not already double jumped without release if (!game.hasDoubleJumped) { vy = JUMP_VELOCITY * 2; isJumping = true; game.hasDoubleJumped = true; // --- Combo Logic --- var prevScore = platformsPassed; var newScore = platformsPassed; var showCombo = false; var comboText = "x2"; var comboColor = 0xFF3B30; var prevComboMultiplier = comboMultiplier; if (comboActive && comboLastWall === "right" && comboPlatformsLanded === 1) { // Continue combo: increase multiplier comboMultiplier *= 2; showCombo = true; } else { // Start new combo comboMultiplier = 2; showCombo = true; } comboActive = true; comboLastWall = "left"; comboPlatformsLanded = 0; comboLastPlatform = null; comboJustDoubleJumped = true; comboLastScore = platformsPassed; // Apply multiplier prevScore = platformsPassed; newScore = platformsPassed * comboMultiplier; platformsPassed = newScore; // Animate the score value in the UI with a vibrant color flash and scale effect scoreTxt.setText(platformsPassed.toString()); LK.setScore(platformsPassed + coinsCollected); // --- Score UI flash animation --- // Stop any previous tweens on scoreTxt tween.stop(scoreTxt, { tint: true, scaleX: true, scaleY: true, alpha: true }); // Define a sequence of vibrant colors to flash through var scoreFlashColors = [0xFFD600, 0xFF3B30, 0x00FFD0, 0x4cd964, 0x5ac8fa, 0xFF3B30, 0xFFD600]; var flashDuration = 1200; // total duration in ms var flashSteps = scoreFlashColors.length; var flashStepDuration = Math.floor(flashDuration / flashSteps); var originalTint = 0xffffff; var originalScaleX = scoreTxt.scale.x; var originalScaleY = scoreTxt.scale.y; // Animate color and scale in sequence (function animateScoreFlash(step) { if (step >= flashSteps) { // Restore to original tween(scoreTxt, { tint: originalTint, scaleX: originalScaleX, scaleY: originalScaleY }, { duration: 300, easing: tween.cubicOut }); return; } tween(scoreTxt, { tint: scoreFlashColors[step], scaleX: 1.25, scaleY: 1.25 }, { duration: flashStepDuration, easing: tween.cubicInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: originalScaleX, scaleY: originalScaleY }, { duration: flashStepDuration / 2, easing: tween.cubicInOut, onFinish: function onFinish() { animateScoreFlash(step + 1); } }); } }); })(0); // --- Animate the newScoreAfterDoubleJump popup flying to the score UI and settling in --- var prevScoreColor = 0x00FFD0; var xNColor = 0xFF3B30; var newScoreColor = 0xFFD600; var eqColor = 0xffffff; var popupGroup = new Container(); var baseY = GAME_H / 2 - 200; var baseX = GAME_W / 2; var fontSize = 160; var scaleStart = 2.2; var scaleBounce = 1.1; var scaleEnd = 0.7; // Create each part as a Text2 object var prevScoreTxt = new Text2(prevScore + "", { size: fontSize, fill: prevScoreColor, font: "Impact" }); var xNTxt = new Text2("x" + comboMultiplier, { size: fontSize, fill: xNColor, font: "Impact" }); var eqTxt = new Text2("=", { size: fontSize, fill: eqColor, font: "Impact" }); var newScoreTxt = new Text2(newScore + "", { size: fontSize, fill: newScoreColor, font: "Impact" }); // Set anchor to center for all prevScoreTxt.anchor.set(0.5, 0.5); xNTxt.anchor.set(0.5, 0.5); eqTxt.anchor.set(0.5, 0.5); newScoreTxt.anchor.set(0.5, 0.5); // Set initial scale and alpha prevScoreTxt.scale.set(scaleStart, scaleStart); xNTxt.scale.set(scaleStart, scaleStart); eqTxt.scale.set(scaleStart, scaleStart); newScoreTxt.scale.set(scaleStart, scaleStart); prevScoreTxt.alpha = 0.0; xNTxt.alpha = 0.0; eqTxt.alpha = 0.0; newScoreTxt.alpha = 0.0; // Add to group popupGroup.addChild(prevScoreTxt); popupGroup.addChild(xNTxt); popupGroup.addChild(eqTxt); popupGroup.addChild(newScoreTxt); // Position horizontally centered as a group // Calculate total width var spacing = 32; var totalWidth = prevScoreTxt.width + xNTxt.width + eqTxt.width + newScoreTxt.width + spacing * 3; var startX = -totalWidth / 2; prevScoreTxt.x = startX + prevScoreTxt.width / 2; xNTxt.x = prevScoreTxt.x + prevScoreTxt.width / 2 + xNTxt.width / 2 + spacing; eqTxt.x = xNTxt.x + xNTxt.width / 2 + eqTxt.width / 2 + spacing; newScoreTxt.x = eqTxt.x + eqTxt.width / 2 + newScoreTxt.width / 2 + spacing; // All y at 0, group at baseY prevScoreTxt.y = 0; xNTxt.y = 0; eqTxt.y = 0; newScoreTxt.y = 0; popupGroup.x = baseX; popupGroup.y = baseY; game.addChild(popupGroup); // Animate all: pop in, bounce, then float up and fade out, but newScoreTxt will fly to the score UI var popIn = function popIn(txt, delay) { tween(txt, { alpha: 1, scaleX: scaleBounce, scaleY: scaleBounce }, { duration: 180, delay: delay, onFinish: function onFinish() { if (txt === newScoreTxt) { // Animate newScoreTxt flying to the score UI and settling in // Calculate global position of scoreTxt in the UI var globalScorePos = { x: 0, y: 0 }; if (scoreTxt.parent && typeof scoreTxt.parent.toGlobal === "function") { globalScorePos = scoreTxt.parent.toGlobal(scoreTxt.position); } // Convert to local coordinates of the game container var localScorePos = game.toLocal(globalScorePos); // Animate to score UI position, shrink and fade out // Move up and settle at the center of scoreTxt (UI), not to the right tween(txt, { x: localScorePos.x + scoreTxt.width / 2, // center of scoreTxt in game coordinates y: localScorePos.y, // move up to align with score UI scaleX: 0.7, scaleY: 0.7, alpha: 0 }, { duration: 900, easing: tween.cubicInOut, onFinish: function onFinish() { txt.destroy(); } }); } else { // Normal float up and fade out tween(txt, { y: txt.y - 180, scaleX: scaleEnd, scaleY: scaleEnd, alpha: 0 }, { duration: 900, onFinish: function onFinish() { txt.destroy(); } }); } } }); }; // Stagger the pop-in for a fun effect popIn(prevScoreTxt, 0); popIn(xNTxt, 60); popIn(eqTxt, 120); popIn(newScoreTxt, 180); // Destroy the group container after all animations tween(popupGroup, {}, { duration: 1400, onFinish: function onFinish() { if (popupGroup && typeof popupGroup.destroy === "function") popupGroup.destroy(); } }); // Start continuous star emission after double jump isEmittingStars = true; emitStarTimer = 0; // Rainbow arc effect: create a rainbow container behind the player var rainbowArc = new Container(); rainbowArc.x = player.x; rainbowArc.y = player.y - PLAYER_H / 2; rainbowArc._life = 0; rainbowArc._maxLife = 36; rainbowArc._rainbowSegments = []; var rainbowColors = [0xff3b30, 0xff9500, 0xffcc00, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6]; var rainbowRadius = 110; var rainbowWidth = 22; for (var seg = 0; seg < rainbowColors.length; ++seg) { var segAsset = LK.getAsset('collectibleStar', { anchorX: 0.5, anchorY: 0.5, scaleX: rainbowWidth / 90 * (1.1 + seg * 0.08), scaleY: rainbowWidth / 90 * (1.1 + seg * 0.08), tint: rainbowColors[seg], alpha: 0.38 }); // Position in an arc behind the player var arcAngle = Math.PI * (0.5 + 0.7 * (seg / (rainbowColors.length - 1))); segAsset.x = Math.cos(arcAngle) * rainbowRadius; segAsset.y = Math.sin(arcAngle) * rainbowRadius * 0.7; rainbowArc.addChild(segAsset); rainbowArc._rainbowSegments.push(segAsset); } game.addChild(rainbowArc); // Animate rainbow arc fade and scale rainbowArc.update = function () { rainbowArc._life++; var t = rainbowArc._life / rainbowArc._maxLife; rainbowArc.alpha = 0.7 * (1 - t); rainbowArc.scale.x = 1 + 0.25 * t; rainbowArc.scale.y = 1 + 0.25 * t; rainbowArc.x = player.x; rainbowArc.y = player.y - PLAYER_H / 2; if (rainbowArc._life > rainbowArc._maxLife) { if (typeof rainbowArc.destroy === "function") { rainbowArc.destroy(); } } }; stars.push(rainbowArc); // Emit 96-120 animated stars from player after double jump, with color cycling and rainbow effect (left wall) var numEmitStars = 96 + Math.floor(Math.random() * 25); // 96-120 for (var emitIdx = 0; emitIdx < numEmitStars; ++emitIdx) { var emitStar = new Star(); emitStar._emittedAnimation = true; // Mark as animation-only, not collectible emitStar.x = player.x; emitStar.y = player.y - PLAYER_H / 2; // Give each star a random direction and speed var angle = Math.PI * 2 * (emitIdx / numEmitStars) + (Math.random() - 0.5) * 0.5; var speed = 18 + Math.random() * 8; emitStar._vx = Math.cos(angle) * speed; emitStar._vy = Math.sin(angle) * speed - 6; emitStar._life = 0; emitStar._maxLife = 32 + Math.random() * 10; // Color cycling: pick a random start color and cycle through a palette var colorCycle = [0xfff7b2, 0xffe066, 0xffb347, 0xff3b30, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6, 0xffcc00]; var colorIdx = Math.floor(Math.random() * colorCycle.length); // Override update for animation emitStar.update = function (star, colorIdx) { return function () { star.x += star._vx; star.y += star._vy; star._vy += 1.1; // gravity star._vx *= 0.96; // friction star._life++; // Color cycling: change color every 6 frames if (star.children && star.children[0]) { var cycleStep = Math.floor(star._life / 6); var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length]; star.children[0].tint = nextColor; // Fade out star.children[0].alpha = Math.max(0, 0.92 * (1 - star._life / star._maxLife)); } if (star._life > star._maxLife) { if (typeof star.destroy === "function") { star.destroy(); } var idx = stars.indexOf(star); if (idx !== -1) { stars.splice(idx, 1); } } }; }(emitStar, colorIdx); game.addChild(emitStar); stars.push(emitStar); } // Play garavel sound only if double jump is successful playNextGaravelSound(); } } // --- Right wall double jump: trigger as soon as player's rightmost pixel touches leftmost pixel of right opaque bg --- if (player.lastX + PLAYER_W / 2 < rightOpaqueX && playerRight >= rightOpaqueX) { player.x = rightOpaqueX - PLAYER_W / 2; // Tumble: rotate player quickly if (player && player.children && player.children.length > 0) { var pGfx = player.children[0]; tween(pGfx, { rotation: pGfx.rotation + Math.PI * 2 }, { duration: 400, onFinish: function onFinish() { pGfx.rotation = 0; } }); } // Play shout sound when hitting right border, with cooldown if (typeof game.lastShoutTime === "undefined") { game.lastShoutTime = 0; } var nowWall = Date.now(); if (nowWall - game.lastShoutTime > 500) { game.lastShoutTime = nowWall; } // Double jump when hitting the border, but only if not already double jumped without release if (!game.hasDoubleJumped) { vy = JUMP_VELOCITY * 2; isJumping = true; game.hasDoubleJumped = true; // --- Combo Logic --- var prevScore = platformsPassed; var newScore = platformsPassed; var showCombo = false; var comboText = "x2"; var comboColor = 0xFF3B30; var prevComboMultiplier = comboMultiplier; if (comboActive && comboLastWall === "left" && comboPlatformsLanded === 1) { // Continue combo: increase multiplier comboMultiplier *= 2; showCombo = true; } else { // Start new combo comboMultiplier = 2; showCombo = true; } comboActive = true; comboLastWall = "right"; comboPlatformsLanded = 0; comboLastPlatform = null; comboJustDoubleJumped = true; comboLastScore = platformsPassed; // Apply multiplier prevScore = platformsPassed; newScore = platformsPassed * comboMultiplier; platformsPassed = newScore; // Animate the score value in the UI with a vibrant color flash and scale effect scoreTxt.setText(platformsPassed.toString()); LK.setScore(platformsPassed + coinsCollected); // --- Score UI flash animation --- // Stop any previous tweens on scoreTxt tween.stop(scoreTxt, { tint: true, scaleX: true, scaleY: true, alpha: true }); // Define a sequence of vibrant colors to flash through var scoreFlashColors = [0xFFD600, 0xFF3B30, 0x00FFD0, 0x4cd964, 0x5ac8fa, 0xFF3B30, 0xFFD600]; var flashDuration = 1200; // total duration in ms var flashSteps = scoreFlashColors.length; var flashStepDuration = Math.floor(flashDuration / flashSteps); var originalTint = 0xffffff; var originalScaleX = scoreTxt.scale.x; var originalScaleY = scoreTxt.scale.y; // Animate color and scale in sequence (function animateScoreFlash(step) { if (step >= flashSteps) { // Restore to original tween(scoreTxt, { tint: originalTint, scaleX: originalScaleX, scaleY: originalScaleY }, { duration: 300, easing: tween.cubicOut }); return; } tween(scoreTxt, { tint: scoreFlashColors[step], scaleX: 1.25, scaleY: 1.25 }, { duration: flashStepDuration, easing: tween.cubicInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: originalScaleX, scaleY: originalScaleY }, { duration: flashStepDuration / 2, easing: tween.cubicInOut, onFinish: function onFinish() { animateScoreFlash(step + 1); } }); } }); })(0); // --- Animate the newScoreAfterDoubleJump popup flying to the score UI and settling in --- var prevScoreColor = 0x00FFD0; var xNColor = 0xFF3B30; var newScoreColor = 0xFFD600; var eqColor = 0xffffff; var popupGroup = new Container(); var baseY = GAME_H / 2 - 200; var baseX = GAME_W / 2; var fontSize = 160; var scaleStart = 2.2; var scaleBounce = 1.1; var scaleEnd = 0.7; // Create each part as a Text2 object var prevScoreTxt = new Text2(prevScore + "", { size: fontSize, fill: prevScoreColor, font: "Impact" }); var xNTxt = new Text2("x" + comboMultiplier, { size: fontSize, fill: xNColor, font: "Impact" }); var eqTxt = new Text2("=", { size: fontSize, fill: eqColor, font: "Impact" }); var newScoreTxt = new Text2(newScore + "", { size: fontSize, fill: newScoreColor, font: "Impact" }); // Set anchor to center for all prevScoreTxt.anchor.set(0.5, 0.5); xNTxt.anchor.set(0.5, 0.5); eqTxt.anchor.set(0.5, 0.5); newScoreTxt.anchor.set(0.5, 0.5); // Set initial scale and alpha prevScoreTxt.scale.set(scaleStart, scaleStart); xNTxt.scale.set(scaleStart, scaleStart); eqTxt.scale.set(scaleStart, scaleStart); newScoreTxt.scale.set(scaleStart, scaleStart); prevScoreTxt.alpha = 0.0; xNTxt.alpha = 0.0; eqTxt.alpha = 0.0; newScoreTxt.alpha = 0.0; // Add to group popupGroup.addChild(prevScoreTxt); popupGroup.addChild(xNTxt); popupGroup.addChild(eqTxt); popupGroup.addChild(newScoreTxt); // Position horizontally centered as a group // Calculate total width var spacing = 32; var totalWidth = prevScoreTxt.width + xNTxt.width + eqTxt.width + newScoreTxt.width + spacing * 3; var startX = -totalWidth / 2; prevScoreTxt.x = startX + prevScoreTxt.width / 2; xNTxt.x = prevScoreTxt.x + prevScoreTxt.width / 2 + xNTxt.width / 2 + spacing; eqTxt.x = xNTxt.x + xNTxt.width / 2 + eqTxt.width / 2 + spacing; newScoreTxt.x = eqTxt.x + eqTxt.width / 2 + newScoreTxt.width / 2 + spacing; // All y at 0, group at baseY prevScoreTxt.y = 0; xNTxt.y = 0; eqTxt.y = 0; newScoreTxt.y = 0; popupGroup.x = baseX; popupGroup.y = baseY; game.addChild(popupGroup); // Animate all: pop in, bounce, then float up and fade out, but newScoreTxt will fly to the score UI var popIn = function popIn(txt, delay) { tween(txt, { alpha: 1, scaleX: scaleBounce, scaleY: scaleBounce }, { duration: 180, delay: delay, onFinish: function onFinish() { if (txt === newScoreTxt) { // Animate newScoreTxt flying to the score UI and settling in // Calculate global position of scoreTxt in the UI var globalScorePos = { x: 0, y: 0 }; if (scoreTxt.parent && typeof scoreTxt.parent.toGlobal === "function") { globalScorePos = scoreTxt.parent.toGlobal(scoreTxt.position); } // Convert to local coordinates of the game container var localScorePos = game.toLocal(globalScorePos); // Animate to score UI position, shrink and fade out tween(txt, { x: localScorePos.x + scoreTxt.width / 2, y: localScorePos.y, // move up to align with score UI scaleX: 0.7, scaleY: 0.7, alpha: 0 }, { duration: 900, easing: tween.cubicInOut, onFinish: function onFinish() { txt.destroy(); } }); } else { // Normal float up and fade out tween(txt, { y: txt.y - 180, scaleX: scaleEnd, scaleY: scaleEnd, alpha: 0 }, { duration: 900, onFinish: function onFinish() { txt.destroy(); } }); } } }); }; // Stagger the pop-in for a fun effect popIn(prevScoreTxt, 0); popIn(xNTxt, 60); popIn(eqTxt, 120); popIn(newScoreTxt, 180); // Destroy the group container after all animations tween(popupGroup, {}, { duration: 1400, onFinish: function onFinish() { if (popupGroup && typeof popupGroup.destroy === "function") popupGroup.destroy(); } }); // Start continuous star emission after double jump (right wall) isEmittingStars = true; emitStarTimer = 0; // Rainbow arc effect: create a rainbow container behind the player var rainbowArc = new Container(); rainbowArc.x = player.x; rainbowArc.y = player.y - PLAYER_H / 2; rainbowArc._life = 0; rainbowArc._maxLife = 36; rainbowArc._rainbowSegments = []; var rainbowColors = [0xff3b30, 0xff9500, 0xffcc00, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6]; var rainbowRadius = 110; var rainbowWidth = 22; for (var seg = 0; seg < rainbowColors.length; ++seg) { var segAsset = LK.getAsset('collectibleStar', { anchorX: 0.5, anchorY: 0.5, scaleX: rainbowWidth / 90 * (1.1 + seg * 0.08), scaleY: rainbowWidth / 90 * (1.1 + seg * 0.08), tint: rainbowColors[seg], alpha: 0.38 }); // Position in an arc behind the player var arcAngle = Math.PI * (0.5 + 0.7 * (seg / (rainbowColors.length - 1))); segAsset.x = Math.cos(arcAngle) * rainbowRadius; segAsset.y = Math.sin(arcAngle) * rainbowRadius * 0.7; rainbowArc.addChild(segAsset); rainbowArc._rainbowSegments.push(segAsset); } game.addChild(rainbowArc); // Animate rainbow arc fade and scale rainbowArc.update = function () { rainbowArc._life++; var t = rainbowArc._life / rainbowArc._maxLife; rainbowArc.alpha = 0.7 * (1 - t); rainbowArc.scale.x = 1 + 0.25 * t; rainbowArc.scale.y = 1 + 0.25 * t; rainbowArc.x = player.x; rainbowArc.y = player.y - PLAYER_H / 2; if (rainbowArc._life > rainbowArc._maxLife) { if (typeof rainbowArc.destroy === "function") { rainbowArc.destroy(); } } }; stars.push(rainbowArc); // Emit 96-120 animated stars from player after double jump, with color cycling and rainbow effect var numEmitStars = 96 + Math.floor(Math.random() * 25); // 96-120 for (var emitIdx = 0; emitIdx < numEmitStars; ++emitIdx) { var emitStar = new Star(); emitStar._emittedAnimation = true; // Mark as animation-only, not collectible emitStar.x = player.x; emitStar.y = player.y - PLAYER_H / 2; // Give each star a random direction and speed var angle = Math.PI * 2 * (emitIdx / numEmitStars) + (Math.random() - 0.5) * 0.5; var speed = 18 + Math.random() * 8; emitStar._vx = Math.cos(angle) * speed; emitStar._vy = Math.sin(angle) * speed - 6; emitStar._life = 0; emitStar._maxLife = 32 + Math.random() * 10; // Color cycling: pick a random start color and cycle through a palette var colorCycle = [0xfff7b2, 0xffe066, 0xffb347, 0xff3b30, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6, 0xffcc00]; var colorIdx = Math.floor(Math.random() * colorCycle.length); // Override update for animation emitStar.update = function (star, colorIdx) { return function () { star.x += star._vx; star.y += star._vy; star._vy += 1.1; // gravity star._vx *= 0.96; // friction star._life++; // Color cycling: change color every 6 frames if (star.children && star.children[0]) { var cycleStep = Math.floor(star._life / 6); var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length]; star.children[0].tint = nextColor; // Fade out star.children[0].alpha = Math.max(0, 0.92 * (1 - star._life / star._maxLife)); } if (star._life > star._maxLife) { if (typeof star.destroy === "function") { star.destroy(); } var idx = stars.indexOf(star); if (idx !== -1) { stars.splice(idx, 1); } } }; }(emitStar, colorIdx); game.addChild(emitStar); stars.push(emitStar); } // Play garavel sound only if double jump is successful playNextGaravelSound(); } } // --- Clamp player to transparent area (between opaque borders) and bounce if hitting border --- // Clamp left if (player.x - PLAYER_W / 2 < leftOpaqueX) { player.x = leftOpaqueX + PLAYER_W / 2; if (player.smoothVx < 0) { player.smoothVx = -player.smoothVx * 0.7; } // bounce right, lose some speed if (vx < 0) { vx = -vx * 0.7; } } // Clamp right if (player.x + PLAYER_W / 2 > rightOpaqueX) { player.x = rightOpaqueX - PLAYER_W / 2; if (player.smoothVx > 0) { player.smoothVx = -player.smoothVx * 0.7; } // bounce left, lose some speed if (vx > 0) { vx = -vx * 0.7; } } } // Platform collision var plat = playerOnPlatform(); // (Removed logic that made checkpoint platforms appear after passing. Now, checkpoint platforms always come down from the top like other platforms.) // --- Increment platformsPassed when player passes a platform (crosses its top Y going down) --- // Reset scoreTxt logic: count unique platforms passed, not label number if (player && typeof player.lastPlatformY === "undefined") { player.lastPlatformY = null; } if (plat && vy > 0) { // Only increment if this platform hasn't been counted yet if (!plat._countedPassed) { plat._countedPassed = true; // Update internal counter platformsPassed++; } player.y = plat.y - PLATFORM_H / 2; vy = JUMP_VELOCITY; isJumping = false; game.hasDoubleJumped = false; // Reset double jump lock only when landing isEmittingStars = false; // Stop emitting stars when landing // --- Combo landing logic --- if (comboActive) { if (comboJustDoubleJumped) { // First landing after double jump comboPlatformsLanded = 1; comboLastPlatform = plat; comboJustDoubleJumped = false; } else if (comboLastPlatform !== plat) { comboPlatformsLanded += 1; comboLastPlatform = plat; } // If player lands on more than one platform before next double jump, reset combo if (comboPlatformsLanded > 1) { comboActive = false; comboMultiplier = 1; comboLastWall = null; comboPlatformsLanded = 0; comboLastPlatform = null; comboJustDoubleJumped = false; } } // Always play jump animation here for consistency if (player && player.children && player.children.length > 0) { var pGfx = player.children[0]; // Squash down, stretch wide, add a little rotation (no color change) tween(pGfx, { scaleY: 0.7, scaleX: 1.25, rotation: 0.18 }, { duration: 90, onFinish: function onFinish() { // Stretch up, squash in, overshoot a bit for bounce tween(pGfx, { scaleY: 1.18 * PLAYER_H / 320, scaleX: 0.88 * PLAYER_W / 320, rotation: -0.08 }, { duration: 90, onFinish: function onFinish() { // Return to normal tween(pGfx, { scaleY: PLAYER_H / 320, scaleX: PLAYER_W / 320, rotation: 0 }, { duration: 90 }); } }); } }); } } else { isJumping = true; // No shout or fall sound here } // --- Coin collection --- // Use last position to detect passing through coins, not just landing on top if (player && typeof player.lastX === "undefined") { player.lastX = player.x; } if (player && typeof player.lastY === "undefined") { player.lastY = player.y; } for (var j = coins.length - 1; j >= 0; --j) { var coin = coins[j]; // Simple circle collision, check both current and previous position for pass-through var dx = player.x - coin.x; var dy = player.y - PLAYER_H / 2 - coin.y; var dist = Math.sqrt(dx * dx + dy * dy); var lastDx = player.lastX - coin.x; var lastDy = player.lastY - PLAYER_H / 2 - coin.y; var lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy); var collectRadius = (PLAYER_W / 2 + coin.radius) * 0.7; // If player is within collect radius now or passed through it this frame if (dist < collectRadius || lastDist > collectRadius && dist < collectRadius || lastDist < collectRadius && dist < collectRadius) { // Collect coin coin.showValuePopup(); coinsCollected += coin.value * coinValueMultiplier; coins.splice(j, 1); coin.destroy(); } } // --- Star collection --- // Use last position to detect passing through stars, not just landing on top for (var j = stars.length - 1; j >= 0; --j) { var star = stars[j]; var dx = player.x - star.x; var dy = player.y - PLAYER_H / 2 - star.y; var dist = Math.sqrt(dx * dx + dy * dy); var lastDx = player.lastX - star.x; var lastDy = player.lastY - PLAYER_H / 2 - star.y; var lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy); var collectRadius = (PLAYER_W / 2 + star.radius) * 0.7; // Only collect stars that are not "emitted" animation stars (i.e., only collectible platform stars) // We'll mark emitted stars with a flag: star._emittedAnimation === true if ((dist < collectRadius || lastDist > collectRadius && dist < collectRadius || lastDist < collectRadius && dist < collectRadius) && !star._emittedAnimation) { // Collect star star.showValuePopup(); coinsCollected += star.value * coinValueMultiplier; stars.splice(j, 1); star.destroy(); } } // Update lastX/lastY for next frame if (player) { player.lastX = player.x; player.lastY = player.y; } // Camera follows player upward if (player && player.y < GAME_H - CAMERA_OFFSET) { var diff = GAME_H - CAMERA_OFFSET - player.y; cameraY += diff; // Move all platforms and player down by diff for (var i = 0; i < platforms.length; ++i) { platforms[i].y += diff; } // Move all coins down by diff so they stay visually attached to their platform for (var i = 0; i < coins.length; ++i) { coins[i].y += diff; } // Move all stars down by diff so they stay visually attached to their platform for (var i = 0; i < stars.length; ++i) { stars[i].y += diff; } // Move all clouds down by diff, and update their position for (var i = clouds.length - 1; i >= 0; --i) { var cloud = clouds[i]; cloud.y += diff * 0.6; // Parallax: move slower than platforms for depth cloud.update && cloud.update(); // Remove cloud if it goes off bottom, add new one at top if (cloud.y > GAME_H + 200) { cloud.destroy(); clouds.splice(i, 1); // Add new cloud at top var newCloud = new Cloud(); newCloud.x = Math.random() * GAME_W; newCloud.y = -100 - Math.random() * 200; game.addChild(newCloud); newCloud.setToBack && newCloud.setToBack(); clouds.push(newCloud); } } player.y += diff; maxHeight += diff; } // Always keep player in front of platforms if (player && player.parent && player.parent.children && player.parent.children.indexOf(player) !== player.parent.children.length - 1) { player.parent.setChildIndex(player, player.parent.children.length - 1); } // Remove platforms that are off screen, add new ones at top for (var i = platforms.length - 1; i >= 0; --i) { if (platforms[i].y > GAME_H + 100) { // Remove any coins that are visually on this platform (within platform width and just above it) for (var j = coins.length - 1; j >= 0; --j) { var coin = coins[j]; // Coin is considered on this platform if its x is within platform width and y is just above platform if (coin.x >= platforms[i].x - platforms[i].width / 2 && coin.x <= platforms[i].x + platforms[i].width / 2 && Math.abs(coin.y - (platforms[i].y - PLATFORM_H / 2 - 40)) < 60) { coin.destroy(); coins.splice(j, 1); } } // Remove any stars that are visually on this platform (within platform width and just above it) for (var j = stars.length - 1; j >= 0; --j) { var star = stars[j]; if (star.x >= platforms[i].x - platforms[i].width / 2 && star.x <= platforms[i].x + platforms[i].width / 2 && Math.abs(star.y - (platforms[i].y - PLATFORM_H / 2 - 100)) < 80) { star.destroy(); stars.splice(j, 1); } } platforms[i].destroy(); platforms.splice(i, 1); // --- Count platforms removed --- // (No longer updates scoreTxt, which now shows platformsCreated) if (typeof platformsRemoved === "undefined") { platformsRemoved = 0; } platformsRemoved += 1; } } // Add new platforms if needed while (platforms.length < 10) { // Defensive: If platforms array is empty, break to avoid crash if (!platforms.length || !platforms[0] || typeof platforms[0].y === "undefined") { break; } var topY = platforms[0].y; // Make platform width and spacing harder as level increases var level = getThemeIndex(platformsPassed); var EASY_PLATFORM_W = 820 - level * 40; if (EASY_PLATFORM_W < 220) { EASY_PLATFORM_W = 220; } // Increase vertical spacing between platforms for more air time var EASY_PLATFORM_SPACING = 310; var newY = topY - EASY_PLATFORM_SPACING; var prevPlat = platforms[0]; var px = getSafePlatformX(prevPlat, EASY_PLATFORM_W); var plat = createPlatform(px, newY, EASY_PLATFORM_W); platforms.sort(function (a, b) { return a.y - b.y; }); } // Score: always shows platformsPassed scoreTxt.setText(platformsPassed.toString()); // Update coins label to show current coins collected coinsLabelTxt.setText('Coins: ' + coinsCollected); // Update leaderboard score (score = platformsPassed + coinsCollected) LK.setScore(platformsPassed + coinsCollected); // Update the sign on the last platform of each theme to show current platforms passed for (var i = 0; i < platforms.length; ++i) { var plat = platforms[i]; if (plat._themeSign && typeof plat._themeSign.setPlatformsPassed === "function") { plat._themeSign.setPlatformsPassed(platformsPassed); } } // Change background color if level changes, using platform index counter for perfect sync var themeIndex = getThemeIndex(platformsPassed); // Always keep track of lastThemeIndex for perfect sync if (typeof game.lastThemeIndex === "undefined") { game.lastThemeIndex = getThemeIndex(0); } // Only change theme on the exact frame when platformsPassed crosses into a new theme (every 50 platforms passed) if (Math.floor(game.lastThemeIndex) !== Math.floor(themeIndex) && platformsPassed % 50 === 0) { // Increase coin value multiplier by 2x for each theme increment coinValueMultiplier = Math.pow(2, Math.floor(themeIndex)); // Use bgTheme asset classes for background on level change var bgThemeAssets = [['bgThemeGrassOpaque', 'bgThemeGrassTrans'], // 0 Grass ['bgThemeForestOpaque', 'bgThemeForestTrans'], // 1 Forest ['bgThemeMudOpaque', 'bgThemeMudTrans'], // 2 Mud ['bgThemeMagicOpaque', 'bgThemeMagicTrans'], // 3 Magic ['bgThemeCandyOpaque', 'bgThemeCandyTrans'], // 4 Candy ['bgThemeNightOpaque', 'bgThemeNightTrans'], // 5 Night ['bgThemeStoneOpaque', 'bgThemeStoneTrans'], // 6 Stone ['bgThemeIceOpaque', 'bgThemeIceTrans'], // 7 Ice ['bgThemeMetalOpaque', 'bgThemeMetalTrans'], // 8 Metal ['bgThemeNeonOpaque', 'bgThemeNeonTrans'], // 9 Neon ['bgThemeSpecialOpaque', 'bgThemeSpecialTrans'] // 10 Special ]; var idx = themeIndex; if (idx < 0) { idx = 0; } if (idx >= bgThemeAssets.length) { idx = bgThemeAssets.length - 1; } // Remove previous background asset if any if (game._bgThemeAsset) { if (typeof game._bgThemeAsset.destroy === "function") { game._bgThemeAsset.destroy(); } game._bgThemeAsset = null; } // Add new background asset as the first child (behind everything) var bgOpaqueAssetId = bgThemeAssets[idx][0]; var bgTransAssetId = bgThemeAssets[idx][1]; // Create a container for the background with three segments: left, center, right var bgThemeContainer = new Container(); var bgOpaqueAssetInfo = LK.getAsset(bgOpaqueAssetId, { anchorX: 0, anchorY: 0 }); var bgTransAssetInfo = LK.getAsset(bgTransAssetId, { anchorX: 0, anchorY: 0 }); var bgWidth = GAME_W; var leftW = Math.floor(bgWidth / 30); var rightW = Math.floor(bgWidth / 30); var centerW = bgWidth - leftW - rightW; // Left 1/30 (opaque) var bgLeft = LK.getAsset(bgOpaqueAssetId, { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: leftW / bgOpaqueAssetInfo.width, scaleY: GAME_H / bgOpaqueAssetInfo.height, alpha: 1 }); bgThemeContainer.addChild(bgLeft); // Center (transparent) var bgCenter = LK.getAsset(bgTransAssetId, { anchorX: 0, anchorY: 0, x: leftW, y: 0, scaleX: centerW / bgTransAssetInfo.width, scaleY: GAME_H / bgTransAssetInfo.height, alpha: 0.3 }); bgThemeContainer.addChild(bgCenter); // Right 1/30 (opaque) var bgRight = LK.getAsset(bgOpaqueAssetId, { anchorX: 0, anchorY: 0, x: leftW + centerW, y: 0, scaleX: rightW / bgOpaqueAssetInfo.width, scaleY: GAME_H / bgOpaqueAssetInfo.height, alpha: 1 }); bgThemeContainer.addChild(bgRight); game.addChild(bgThemeContainer); if (game.children && game.children.length > 1) { game.setChildIndex(bgThemeContainer, 0); } game._bgThemeAsset = bgThemeContainer; game.lastThemeIndex = themeIndex; } // High score logic (still based on maxHeight climbed) var score = Math.floor(maxHeight / 10); if (score > highScore) { highScore = score; storage.highScore = highScore; highScoreTxt.setText('Best: ' + highScore); } // Game over: fall below screen if (player && player.y > GAME_H + 200) { gameOver = true; // Play a random death sound when it's certain the player is going to die var deathSounds = ['death-1', 'death-2']; var randomIdx = Math.floor(Math.random() * deathSounds.length); var deathSound = LK.getSound(deathSounds[randomIdx]); var played = deathSound.play(); // Always wait 3 seconds before showing game over, regardless of sound LK.setTimeout(function () { LK.showGameOver(); }, 3000); } // Update lastVy for next frame if (player) { player.lastVy = vy; } // Update clouds for horizontal drift (even if camera doesn't move) for (var i = 0; i < clouds.length; ++i) { if (clouds[i] && typeof clouds[i].update === "function") { clouds[i].update(); } } // Emit a star every few frames while isEmittingStars is true if (isEmittingStars && player && typeof player.x === "number" && typeof player.y === "number") { emitStarTimer++; if (emitStarTimer >= emitStarInterval) { emitStarTimer = 0; var emitStar = new Star(); emitStar._emittedAnimation = true; // Mark as animation-only, not collectible emitStar.x = player.x; emitStar.y = player.y - PLAYER_H / 2; // Give each star a random direction and speed var angle = Math.random() * Math.PI * 2; var speed = 14 + Math.random() * 7; emitStar._vx = Math.cos(angle) * speed; emitStar._vy = Math.sin(angle) * speed - 4; emitStar._life = 0; emitStar._maxLife = 28 + Math.random() * 8; // Color cycling: pick a random start color and cycle through a palette var colorCycle = [0xfff7b2, 0xffe066, 0xffb347, 0xff3b30, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6, 0xffcc00]; var colorIdx = Math.floor(Math.random() * colorCycle.length); emitStar.update = function (star, colorIdx) { return function () { star.x += star._vx; star.y += star._vy; star._vy += 1.1; // gravity star._vx *= 0.96; // friction star._life++; // Color cycling: change color every 6 frames if (star.children && star.children[0]) { var cycleStep = Math.floor(star._life / 6); var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length]; star.children[0].tint = nextColor; // Fade out star.children[0].alpha = Math.max(0, 0.92 * (1 - star._life / star._maxLife)); } if (star._life > star._maxLife) { if (typeof star.destroy === "function") { star.destroy(); } var idx = stars.indexOf(star); if (idx !== -1) { stars.splice(idx, 1); } } }; }(emitStar, colorIdx); game.addChild(emitStar); stars.push(emitStar); } } // Update stars for animation for (var i = 0; i < stars.length; ++i) { if (stars[i] && typeof stars[i].update === "function") { stars[i].update(); } } // (Cloud update and spawn logic removed) }; // --- Character selection support --- // List of available character asset IDs var availableCharacters = ['chipCharacter', 'chipCharacter2', 'chipCharacter3', 'chipCharacter4', 'chipCharacter5', 'chipCharacter6']; // Selected character asset ID (default to first) var selectedCharacterAssetId = availableCharacters[0]; // Show character selection menu before starting the game function showCharacterSelectionMenu(availableCharacters, onSelect) { // Remove any previous menu if (typeof showCharacterSelectionMenu._container !== "undefined" && showCharacterSelectionMenu._container) { showCharacterSelectionMenu._container.destroy(); showCharacterSelectionMenu._container = null; } // Play opening music when character selection menu is shown LK.playMusic('opening-music'); var menuContainer = new Container(); showCharacterSelectionMenu._container = menuContainer; // Solid black rectangle overlay that fits the whole screen var overlay = LK.getAsset('uiTopBgRect', { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: GAME_W / 100, scaleY: GAME_H / 110, alpha: 0.7 }); menuContainer.addChild(overlay); // Title var title = new Text2("Select Your Character", { size: 110, fill: "#fff", font: "Impact" }); title.anchor.set(0.5, 0); title.x = GAME_W / 2 - 300; title.y = 140 + 75 + 250; menuContainer.addChild(title); // Layout character icons in two rows of three, centered var iconSize = 320; // doubled from 160 to 320 var iconScale = 1.0; var spacing = 420; // doubled from 210 to 420 var iconsPerRow = 3; var numRows = Math.ceil(availableCharacters.length / iconsPerRow); var totalWidth = (iconsPerRow - 1) * spacing; var startX = GAME_W / 2 - totalWidth / 2 - 300; var startY = 600 + 75 + 250; for (var i = 0; i < availableCharacters.length; ++i) { (function (idx) { var assetId = availableCharacters[idx]; var assetInfo = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale to fit both width and height, preserving aspect ratio var scaleW = iconSize / assetInfo.width; var scaleH = iconSize / assetInfo.height; var scale = Math.min(scaleW, scaleH) * iconScale; var icon = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: scale, scaleY: scale }); var iconContainer = new Container(); var row = Math.floor(idx / iconsPerRow); var col = idx % iconsPerRow; iconContainer.x = startX + col * spacing; iconContainer.y = startY + row * (iconSize + 80); iconContainer.addChild(icon); // Add a border highlight for selection feedback var border = LK.getAsset('uiTopBgRect', { anchorX: 0.5, anchorY: 0.5, scaleX: (iconSize + 32) / 100, scaleY: (iconSize + 32) / 110, alpha: 0.18 }); border.x = 0; border.y = 0; iconContainer.addChild(border); // Touch/click handler iconContainer.down = function (x, y, obj) { // Remove menu if (showCharacterSelectionMenu._container) { showCharacterSelectionMenu._container.destroy(); showCharacterSelectionMenu._container = null; } if (typeof onSelect === "function") { onSelect(assetId); } }; // (Removed character name label below icon) menuContainer.addChild(iconContainer); })(i); } // Add menu to GUI overlay (top left) LK.gui.topLeft.addChild(menuContainer); } // Call our custom character selection menu showCharacterSelectionMenu(availableCharacters, function (chosenId) { if (availableCharacters.indexOf(chosenId) !== -1) { selectedCharacterAssetId = chosenId; } else { selectedCharacterAssetId = availableCharacters[0]; } // Start the game after character is selected resetGame(); // Play a random game theme song (1-4) at a lower volume for a more subtle effect var themeSongs = ['game-theme-song-1', 'game-theme-song-2', 'game-theme-song-3']; var randomThemeIdx = Math.floor(Math.random() * 4); // Ensure 0-3 inclusive LK.playMusic(themeSongs[randomThemeIdx], { volume: 0.10 }); });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Cloud class: background only, moves with camera, not collidable
var Cloud = Container.expand(function () {
var self = Container.call(this);
// Pick a random cloud asset
var cloudAssets = ['cloudBlue', 'cloudBright', 'cloudFluffy', 'cloudGray', 'cloudSoft'];
var assetId = cloudAssets[Math.floor(Math.random() * cloudAssets.length)];
var assetInfo = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale clouds to a reasonable size for background
var targetHeight = 180 + Math.random() * 80; // 180-260px
var scale = targetHeight / assetInfo.height;
var cloudGfx = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale,
alpha: 0.45 + Math.random() * 0.15 // more transparent clouds
});
// Set initial position (x, y) and speed
self.x = Math.random() * 2048;
self.y = Math.random() * 2048;
self.speedY = 0; // Will be set by camera movement
// Give some clouds a gentle horizontal drift
self.driftX = (Math.random() < 0.5 ? -1 : 1) * (0.2 + Math.random() * 0.5);
// Clouds are always behind everything
self.setToBack = function () {
if (self.parent && self.parent.children.indexOf(self) > 0) {
self.parent.setChildIndex(self, 0);
}
};
// No collision, no input
self.update = function () {
// Track lastX for border collision detection
if (typeof self.lastX === "undefined") {
self.lastX = self.x;
}
// Horizontal drift
self.x += self.driftX;
// Clamp clouds to transparent area and bounce on hitting opaque bg borders
var minCloudX = BORDER_LEFT_X;
var maxCloudX = BORDER_RIGHT_X;
// Bounce off left opaque border
if (self.lastX > minCloudX && self.x <= minCloudX) {
self.x = minCloudX;
self.driftX = Math.abs(self.driftX); // move right
}
// Clamp left
if (self.x < minCloudX) {
self.x = minCloudX;
self.driftX = Math.abs(self.driftX); // move right
}
// Bounce off right opaque border
if (self.lastX < maxCloudX && self.x >= maxCloudX) {
self.x = maxCloudX;
self.driftX = -Math.abs(self.driftX); // move left
}
// Clamp right
if (self.x > maxCloudX) {
self.x = maxCloudX;
self.driftX = -Math.abs(self.driftX); // move left
}
// Update lastX for next frame
self.lastX = self.x;
// Vertical movement is handled by camera diff in game.update
};
return self;
});
// --- Coin class: collectible, always same visual size, shows value popup ---
var Coin = Container.expand(function () {
var self = Container.call(this);
// Coin type: 1, 2, or 3 (for +1, +3, +5)
self.coinType = 1;
self.value = 1;
self.assetId = 'chipCoin1';
// Set asset and value based on type
if (typeof arguments[0] === "number") {
if (arguments[0] === 1) {
self.coinType = 1;
self.value = 1;
self.assetId = 'chipCoin1';
} else if (arguments[0] === 2) {
self.coinType = 2;
self.value = 3;
self.assetId = 'chipCoin2';
} else if (arguments[0] === 3) {
self.coinType = 3;
self.value = 5;
self.assetId = 'chipCoin3';
}
}
// Always render coins at the same visual size (height 80px)
var targetHeight = 80;
var assetInfo = LK.getAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5
});
var scale = targetHeight / assetInfo.height;
var coinGfx = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale
});
// For collision
self.radius = assetInfo.width * scale / 2;
// Show value popup when collected
self.showValuePopup = function () {
var displayValue = self.value * (typeof coinValueMultiplier !== "undefined" ? coinValueMultiplier : 1);
var txt = new Text2('+' + displayValue, {
size: 90,
// Larger for more impact
fill: self.value === 1 ? "#ffe066" : self.value === 3 ? "#a78bfa" : "#38bdf8",
font: "Impact"
});
txt.anchor.set(0.5, 0.5);
txt.x = self.x;
txt.y = self.y - 60; // Higher above coin
txt.scale.set(1.7, 1.7); // Start big
txt.alpha = 0.0; // Start invisible
game.addChild(txt);
// Ensure popup is above player
if (txt.parent && player && txt.parent.children.indexOf(txt) < txt.parent.children.indexOf(player)) {
txt.parent.setChildIndex(txt, txt.parent.children.length - 1);
}
// Flash coin for feedback
if (self.children && self.children[0]) {
var coinGfx = self.children[0];
tween(coinGfx, {
tint: 0xffffff
}, {
duration: 80,
onFinish: function onFinish() {
tween(coinGfx, {
tint: 0xffe066
}, {
duration: 120
});
}
});
}
// Animate popup: fade in, bounce, then float up and fade out
tween(txt, {
alpha: 1,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 120,
onFinish: function onFinish() {
tween(txt, {
y: txt.y - 120,
scaleX: 0.7,
scaleY: 0.7,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
txt.destroy();
}
});
}
});
};
return self;
});
// --- PlatformSign class: shows a sign with platforms passed on the last platform of each theme ---
var PlatformSign = Container.expand(function () {
var self = Container.call(this);
// Use a simple box as the sign background
var signW = 220,
signH = 110;
var signBg = self.attachAsset('chipPlatform0', {
anchorX: 0.5,
anchorY: 1,
scaleX: signW / 820,
scaleY: signH / 110,
color: 0x22223b
});
// Text label for platforms passed
var signTxt = new Text2('0', {
size: 60,
fill: "#fff",
font: "Impact"
});
signTxt.anchor.set(0.5, 0.5);
signTxt.x = 0;
signTxt.y = -signH / 2;
self.addChild(signTxt);
// Set the value to display
self.setPlatformsPassed = function (val) {
signTxt.setText(val + "");
};
return self;
});
// PokerChip class: simple collectible poker chip (not draggable, not rotatable)
var PokerChip = Container.expand(function () {
var self = Container.call(this);
// Asset id and color are passed in, default to 'chipCoin1'
self.assetId = self.assetId || 'chipCoin1';
var chipAssetInfo = LK.getAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Always render poker chips at a consistent visual size (height 80px)
var targetHeight = 80;
var scale = targetHeight / chipAssetInfo.height;
var chip = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale
});
// For hit testing
self.radius = chipAssetInfo.width * scale / 2;
// Show value popup when collected (optional, can be customized)
self.value = 1;
self.showValuePopup = function () {
var txt = new Text2('+1', {
size: 90,
// Larger for more impact
fill: 0xFFE066,
font: "Impact"
});
txt.anchor.set(0.5, 0.5);
txt.x = self.x;
txt.y = self.y - 60;
txt.scale.set(1.7, 1.7); // Start big
txt.alpha = 0.0; // Start invisible
game.addChild(txt);
// Animate popup: fade in, bounce, then float up and fade out
tween(txt, {
alpha: 1,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 120,
onFinish: function onFinish() {
tween(txt, {
y: txt.y - 120,
scaleX: 0.7,
scaleY: 0.7,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
txt.destroy();
}
});
}
});
};
return self;
});
// --- Star class: collectible, animated, optimized for performance ---
var Star = Container.expand(function () {
var self = Container.call(this);
// Use the new collectibleStar asset
self.assetId = 'collectibleStar';
self.value = 10; // Stars are worth +10
var assetInfo = LK.getAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Always render stars at the same visual size (height 70px)
var targetHeight = 70;
var scale = targetHeight / assetInfo.height;
var starGfx = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale,
alpha: 0.92
});
// For collision
self.radius = assetInfo.width * scale / 2;
// Animate star: slow rotation and pulsing
self.update = function () {
if (starGfx) {
starGfx.rotation += 0.09;
var pulse = 0.95 + 0.08 * Math.sin(Date.now() / 180 + self.x);
starGfx.scale.x = scale * pulse;
starGfx.scale.y = scale * pulse;
}
};
// Show value popup when collected
self.showValuePopup = function () {
var displayValue = self.value * (typeof coinValueMultiplier !== "undefined" ? coinValueMultiplier : 1);
var txt = new Text2('+' + displayValue, {
size: 90,
// Larger for more impact
fill: 0xFFF7B2,
font: "Impact"
});
txt.anchor.set(0.5, 0.5);
txt.x = self.x;
txt.y = self.y - 60;
txt.scale.set(1.7, 1.7); // Start big
txt.alpha = 0.0; // Start invisible
game.addChild(txt);
if (txt.parent && player && txt.parent.children.indexOf(txt) < txt.parent.children.indexOf(player)) {
txt.parent.setChildIndex(txt, txt.parent.children.length - 1);
}
// Animate popup: fade in, bounce, then float up and fade out
tween(txt, {
alpha: 1,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 120,
onFinish: function onFinish() {
tween(txt, {
y: txt.y - 120,
scaleX: 0.7,
scaleY: 0.7,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
txt.destroy();
}
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x18181b
});
/****
* Game Code
****/
// Collectible star asset (yellow star shape)
// Cloud shape assets for customizable cloud looks
// --- Icy Tower Constants ---
// Additional chip assets for more levels
var highScore = storage.highScore || 0;
var GAME_W = 2048;
var GAME_H = 2732;
var PLATFORM_W = 400;
// Define new left/right border X values at the intersection of opaque and transparent background
var BORDER_LEFT_X = Math.floor(GAME_W / 30);
var BORDER_RIGHT_X = GAME_W - Math.floor(GAME_W / 30);
var PLATFORM_H = 110;
var PLATFORM_SPACING_MIN = 320;
var PLATFORM_SPACING_MAX = 440;
var PLAYER_W = 170;
var PLAYER_H = 170;
var GRAVITY = 2.2;
var JUMP_VELOCITY = -62;
var MOVE_SPEED = 22;
var PLATFORM_X_MARGIN = 120;
var CAMERA_OFFSET = 900; // How far from bottom the player is kept
// --- Cloud background state ---
var clouds = [];
var NUM_CLOUDS = 8; // Number of clouds to show in background
// --- State ---
var platforms = [];
var coins = []; // All active coins
var stars = []; // All active stars
var player = null;
var vy = 0;
var vx = 0;
var isJumping = false;
var isTouching = false;
var touchStartX = 0;
// --- Coin value multiplier: increases 10x per theme ---
var coinValueMultiplier = 1;
// Track all active touches (for multi-touch)
var activeTouches = [];
var cameraY = 0;
var maxHeight = 0;
var gameOver = false;
game.hasDoubleJumped = false;
// --- Combo Logic State ---
var comboActive = false; // true if in a combo sequence
var comboMultiplier = 1; // 1, 2, 4, 8, 16, ...
var comboLastWall = null; // "left" or "right"
var comboPlatformsLanded = 0; // platforms landed since last double jump
var comboLastPlatform = null; // reference to last platform landed
var comboJustDoubleJumped = false; // true for 1 frame after double jump (to track landing)
var comboLastScore = 0; // score before last double jump
// Track if we are currently emitting stars after a double jump
var isEmittingStars = false;
var emitStarTimer = 0;
var emitStarInterval = 2; // emit a star every 2 frames (about 30 per second)
var garavelSounds = ['garavel-1', 'garavel-2', 'garavel-3', 'garavel-4', 'garavel-5'];
var garavelSoundIndex = 0;
function playNextGaravelSound() {
LK.getSound(garavelSounds[garavelSoundIndex]).play();
garavelSoundIndex = (garavelSoundIndex + 1) % garavelSounds.length;
}
// --- Character ---
var DEFAULT_CHARACTER_ASSET_ID = 'chipCharacter';
// Level themes: background color and platform asset per level
// Platform color is always high-contrast with background for visibility
var LEVEL_THEMES = [{
// 1 Grass
bg: 0x18181b,
platformAsset: 'chipPlatform1',
platformColor: 0xfacc15
}, {
// 2 Forest
bg: 0x1e293b,
platformAsset: 'chipPlatform2',
platformColor: 0xf1f5f9
}, {
// 3 Mud
bg: 0x3b1e1e,
platformAsset: 'chipPlatform3',
platformColor: 0xffffff
}, {
// 4 Magic
bg: 0x2d1e3b,
platformAsset: 'chipPlatform4',
platformColor: 0xffe066
}, {
// 5 Candy
bg: 0x3b2d1e,
platformAsset: 'chipPlatform5',
platformColor: 0x22223b
}, {
// 6 Night
bg: 0x1e3b2d,
platformAsset: 'chipPlatform6',
platformColor: 0xf8fafc
}, {
// 7 Stone
bg: 0x3b1e2d,
platformAsset: 'chipPlatform7',
platformColor: 0x22223b
}, {
// 8 Ice
bg: 0x1e2d3b,
platformAsset: 'chipPlatform8',
platformColor: 0x22223b
}, {
// 9 Metal
bg: 0x2d3b1e,
platformAsset: 'chipPlatform9',
platformColor: 0xf8fafc
}, {
// 10 Neon
bg: 0x000000,
platformAsset: 'chipPlatform10',
platformColor: 0xfacc15
}, {
// 11 Special (chipPlatform11)
bg: 0x22223b,
platformAsset: 'chipPlatform11',
platformColor: 0xffffff
}];
// Helper to get current theme index based on platformsPassed/level
function getThemeIndex(score) {
// Every level lasts for 50 platforms
var idx = Math.floor(platformsPassed / 50);
if (idx < 0) {
idx = 0;
}
if (idx >= LEVEL_THEMES.length) {
idx = LEVEL_THEMES.length - 1;
}
return idx;
}
// Helper to get current theme object
function getCurrentTheme(score) {
return LEVEL_THEMES[getThemeIndex(score)];
}
// Used for initial platform asset (will be replaced in createPlatform)
var platformAsset = LK.getAsset('chipPlatform8', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: PLATFORM_W / 1100,
scaleY: PLATFORM_H / 700
});
// --- UI ---
// Platform pass counter
var platformsPassed = 0;
// Track number of platforms created (for UI only, not for scoring)
var platformsCreated = 0;
// Track total coins collected
var coinsCollected = 0;
// --- Score & Coins UI ---
// Create a container for the UI background and labels
var uiContainer = new Container();
// Add a black rectangle background behind the UI (covers full top width, rectangle shape)
var uiBgRect = LK.getAsset('uiTopBgRect', {
anchorX: 0,
anchorY: 0,
x: -600,
y: 0,
scaleX: GAME_W / 100,
scaleY: 1
});
uiContainer.addChild(uiBgRect);
var uiBgWidth = GAME_W;
var uiBgHeight = 110;
// "Coins: <number>" label (first, left)
var coinsLabelTxt = new Text2('Coins: 0', {
size: 70,
fill: 0xFFE066,
font: "Impact"
});
coinsLabelTxt.anchor.set(0, 0.5); // left aligned, vertically centered
coinsLabelTxt.x = 60 - 300; // Move 400px to the left
coinsLabelTxt.y = uiBgHeight / 2;
// "Score:" label (right of coins, with extra spacing)
var scoreLabelTxt = new Text2('Score:', {
size: 70,
fill: "#fff",
font: "Impact"
});
scoreLabelTxt.anchor.set(0, 0.5); // left aligned, vertically centered
scoreLabelTxt.x = 50 + coinsLabelTxt.width; // Keep Score UI in original place
scoreLabelTxt.y = uiBgHeight / 2;
// Score value (right of label, with spacing)
var scoreTxt = new Text2('0', {
size: 80,
fill: "#fff",
font: "Impact"
});
scoreTxt.anchor.set(0, 0.5); // left aligned, vertically centered
scoreTxt.x = scoreLabelTxt.x + scoreLabelTxt.width + 30;
scoreTxt.y = uiBgHeight / 2;
// Add all to container in new order: coins, score label, score value
uiContainer.addChild(coinsLabelTxt);
uiContainer.addChild(scoreLabelTxt);
uiContainer.addChild(scoreTxt);
// Center the UI container at the top of the screen, shifted 200px left
uiContainer.x = -200;
uiContainer.y = 0;
// Add to GUI overlay (top center)
LK.gui.top.addChild(uiContainer);
// Proper high score text object for updating best score
var highScoreTxt = new Text2('', {
size: 60,
fill: "#fff",
font: "Impact"
});
highScoreTxt.anchor.set(1, 0.5); // right aligned, vertically centered
highScoreTxt.x = GAME_W - 60;
highScoreTxt.y = uiBgHeight / 2;
uiContainer.addChild(highScoreTxt);
// Listen for game over and update LK score to platformsPassed + coinsCollected
LK.on('gameover', function () {
// Always use the actual variables for score and coins, not UI text
var scoreVal = typeof platformsPassed !== "undefined" ? platformsPassed : 0;
var coinsVal = typeof coinsCollected !== "undefined" ? coinsCollected : 0;
LK.setScore(scoreVal + coinsVal);
});
// --- Helper: create a platform at (x, y) ---
function createPlatform(x, y, width) {
var plat = new Container();
// Make each new platform a bit harder as level increases
// Find the last platform's width if any, otherwise use PLATFORM_W
var lastPlat = platforms.length > 0 ? platforms[platforms.length - 1] : null;
var prevW = lastPlat && lastPlat.width ? lastPlat.width : PLATFORM_W;
var level = getThemeIndex(platformsPassed);
// Platform width shrinks with level, min 220, max 1100
// Make width decrease more gradually per level (every 20 platforms)
var baseW = 820 - level * 40;
if (baseW < 220) {
baseW = 220;
}
var w;
// Platform width logic (no checkpoint logic)
var platNum = platformsPassed + platforms.length;
var themeLength = 50; // Each theme has 50 platforms
var w;
if (typeof width === "number") {
w = width;
} else {
// Add more variety: sometimes make platforms much narrower or wider
var variety = Math.random();
if (variety < 0.12 && platNum > 10) {
// 12% chance: very narrow platform (challenge)
w = Math.max(160, baseW * 0.45 + Math.random() * 60);
} else if (variety > 0.92 && platNum > 10) {
// 8% chance: very wide platform (reward)
w = Math.min(baseW * 1.5, 1100);
} else {
// Each new platform is 4% wider than the previous, but capped by baseW for the level
w = Math.min(prevW * (1.04 + (Math.random() - 0.5) * 0.08), baseW);
}
}
// Determine theme based on platform index counter for new platforms
// Ensure each theme has at most 50 platforms
var platformIdx = typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed;
var themeIdx = Math.floor(platformIdx / 50);
if (themeIdx >= LEVEL_THEMES.length) {
themeIdx = LEVEL_THEMES.length - 1;
}
var theme = LEVEL_THEMES[themeIdx];
var assetId = theme.platformAsset;
var platformColor = theme.platformColor;
// Get original asset size for aspect ratio
var assetInfo = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
var origW = assetInfo.width;
var origH = assetInfo.height;
var scaleX = w / origW;
var scaleY = PLATFORM_H / origH;
// To preserve aspect ratio, use the smaller scale
var scale = Math.min(scaleX, scaleY);
var platGfx = plat.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale,
color: platformColor
});
plat.x = x;
plat.y = y;
plat.width = w;
plat.height = PLATFORM_H;
plat.PLATFORM_H = PLATFORM_H;
game.addChild(plat);
platforms.push(plat);
// --- Add moving platforms for extra fun ---
// 15% chance for a moving platform, but not for the first 10 platforms
if (platNum > 10 && Math.random() < 0.15) {
plat._isMoving = true;
plat._moveDir = Math.random() < 0.5 ? -1 : 1;
plat._moveRange = 180 + Math.random() * 220;
plat._moveSpeed = 2.2 + Math.random() * 1.8;
plat._moveOriginX = plat.x;
plat.update = function () {
// Move back and forth horizontally
plat.x = plat._moveOriginX + Math.sin(Date.now() / (420 - plat._moveSpeed * 60)) * plat._moveRange * plat._moveDir;
};
}
// --- Add platform gaps for challenge (skip platform creation) ---
// 10% chance to skip a platform (gap), but never two in a row, and not for the first 10 platforms
if (platNum > 10 && Math.random() < 0.10 && lastPlat && !lastPlat._wasGap) {
plat._wasGap = true;
// Remove this platform immediately to create a gap
plat.destroy();
platforms.pop();
return null;
}
// Increment platformsCreated and update scoreTxt
if (typeof platformsCreated === "undefined") {
platformsCreated = 0;
}
platformsCreated += 1;
if (typeof scoreTxt !== "undefined" && scoreTxt.setText) {
scoreTxt.setText(platformsCreated.toString());
}
// --- Add platform number label to every 50th platform only (not all) ---
if (platforms.length > 1) {
if (typeof createPlatform.platformIndex === "undefined") {
createPlatform.platformIndex = 1;
}
var platformNumber = createPlatform.platformIndex;
var themeIdxForPlat = Math.floor((platformNumber - 1) / 50);
var themePlatformNumber = (platformNumber - 1) % 50 + 1;
// Only allow up to 50 platforms per theme
if (themePlatformNumber > 50) {
return plat;
}
createPlatform.platformIndex++;
// Only add label if this is a 50th platform (e.g. 50, 100, 150, ...)
if (platformNumber % 50 === 0) {
// Add a black opaque background behind the platform label, sized to tightly fit the label text
var counterTxt = new Text2(platformNumber + "", {
size: 18,
fill: "#fff",
font: "Impact"
});
counterTxt.anchor.set(0.5, 0.5);
counterTxt.x = 0;
counterTxt.y = 0;
// Calculate background size to tightly fit the label text (with a small padding)
var paddingX = 16;
var paddingY = 8;
var labelBgW = counterTxt.width + paddingX;
var labelBgH = counterTxt.height + paddingY;
var scaleX = labelBgW / 100;
var scaleY = labelBgH / 110;
var labelBg = LK.getAsset('uiTopBgRect', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scaleX,
scaleY: scaleY,
alpha: 1
});
labelBg.x = 0;
labelBg.y = 0;
plat.addChild(labelBg);
plat.addChild(counterTxt);
}
}
// --- Coin and Star placement logic ---
// Don't place coins or stars on the first platform (player start)
if (platforms.length > 1) {
// --- Intelligent collectible appearance logic ---
// Coins: 80% chance to spawn a coin on a platform, with type based on platform number
var coinChance = 0.8;
if (Math.random() < coinChance) {
// Make higher value coins rarer and more likely on higher platforms
var platNum = platformsPassed + platforms.length;
var r = Math.random();
var coinType = 1;
if (platNum > 200 && r > 0.92) {
coinType = 3;
} // +5 coin, rare after 200
else if (platNum > 100 && r > 0.85) {
coinType = 2;
} // +3 coin, more likely after 100
else if (r > 0.95) {
coinType = 3;
} // +5 coin, very rare early
else if (r > 0.8) {
coinType = 2;
} // +3 coin, uncommon
var coin = new Coin(coinType);
coin.x = plat.x;
coin.y = plat.y - PLATFORM_H / 2 - 40;
game.addChild(coin);
coins.push(coin);
}
// Stars: 20% chance to spawn a star, but always spawn on every 10th platform
var platNum = platformsPassed + platforms.length;
var starChance = 0.2;
if (platNum % 10 === 0 || Math.random() < starChance) {
var star = new Star();
star.x = plat.x + (Math.random() - 0.5) * (plat.width * 0.4); // randomize a bit
star.y = plat.y - PLATFORM_H / 2 - 100;
game.addChild(star);
stars.push(star);
}
}
// --- Add a sign to the last platform of each theme ---
var themeIdxForPlat = getThemeIndex(platformsPassed + platforms.length - 1);
var isLastOfTheme = (platformsPassed + platforms.length) % 50 === 0;
if (isLastOfTheme) {
var sign = new PlatformSign();
sign.x = plat.x;
sign.y = plat.y - PLATFORM_H / 2 - 10;
sign.setPlatformsPassed(platformsPassed + platforms.length);
game.addChild(sign);
plat._themeSign = sign;
}
return plat;
}
// --- Helper: find a safe X for a new platform, given previous platform ---
function getSafePlatformX(prevPlat, width) {
// Always keep new platform horizontally reachable from previous
var minX = Math.max(PLATFORM_X_MARGIN + width / 2, prevPlat ? prevPlat.x - 400 : PLATFORM_X_MARGIN + width / 2);
var maxX = Math.min(GAME_W - PLATFORM_X_MARGIN - width / 2, prevPlat ? prevPlat.x + 400 : GAME_W - PLATFORM_X_MARGIN - width / 2);
if (minX > maxX) {
minX = maxX = prevPlat ? prevPlat.x : GAME_W / 2;
}
return minX + Math.random() * (maxX - minX);
}
// --- Helper: reset game state ---
function resetGame() {
// Remove old coins
for (var i = coins.length - 1; i >= 0; --i) {
if (coins[i] && typeof coins[i].destroy === "function") {
coins[i].destroy();
}
}
coins = [];
// Remove old stars
for (var i = stars.length - 1; i >= 0; --i) {
if (stars[i] && typeof stars[i].destroy === "function") {
stars[i].destroy();
}
}
stars = [];
// Remove old platforms
for (var i = 0; i < platforms.length; ++i) {
platforms[i].destroy();
}
platforms = [];
// Remove player
if (player) {
player.destroy();
}
// Create player
player = new Container();
var pGfx = player.attachAsset(typeof selectedCharacterAssetId !== "undefined" ? selectedCharacterAssetId : DEFAULT_CHARACTER_ASSET_ID, {
anchorX: 0.5,
anchorY: 1,
scaleX: PLAYER_W / 640,
scaleY: PLAYER_H / 640
});
player.x = GAME_W / 2;
player.y = GAME_H - 400;
player.width = PLAYER_W;
player.height = PLAYER_H;
game.addChild(player);
vy = 0;
vx = 0;
isJumping = false;
isTouching = false;
cameraY = 0;
maxHeight = 0;
gameOver = false;
// --- Reset combo state ---
comboActive = false;
comboMultiplier = 1;
comboLastWall = null;
comboPlatformsLanded = 0;
comboLastPlatform = null;
comboJustDoubleJumped = false;
comboLastScore = 0;
// Create initial platforms
var y = GAME_H - 120;
// --- Add platform0 as the very first platform ---
var platform0AssetId = 'chipPlatform0';
var platform0AssetInfo = LK.getAsset(platform0AssetId, {
anchorX: 0.5,
anchorY: 0.5
});
var platform0W = platform0AssetInfo.width;
var platform0H = platform0AssetInfo.height;
var platform0Scale = PLATFORM_H / platform0H;
var platform0 = new Container();
var platform0Gfx = platform0.attachAsset(platform0AssetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: platform0Scale,
scaleY: platform0Scale
});
platform0.x = GAME_W / 2;
platform0.y = y;
platform0.width = platform0W * platform0Scale;
platform0.height = PLATFORM_H;
platform0.PLATFORM_H = PLATFORM_H;
game.addChild(platform0);
platforms.push(platform0);
// Place player directly above platform0, so the game starts after landing on this platform
player.x = platform0.x;
player.y = platform0.y - PLATFORM_H / 2 - PLAYER_H / 2 + 10; // Center player above platform, slightly above
// Create initial platforms
createPlatform.platformIndex = 1; // Reset platform index counter at game start
var level = getThemeIndex(platformsPassed);
var EASY_PLATFORM_W = 820 - level * 40;
if (EASY_PLATFORM_W < 220) {
EASY_PLATFORM_W = 220;
}
// Increase vertical spacing between platforms for more air time
var EASY_PLATFORM_SPACING = 310;
var prevPlat = platform0;
y -= EASY_PLATFORM_SPACING;
for (var i = 1; i < 12; ++i) {
var px = getSafePlatformX(prevPlat, EASY_PLATFORM_W);
var plat = createPlatform(px, y, EASY_PLATFORM_W);
prevPlat = plat;
y -= EASY_PLATFORM_SPACING;
}
// Sort platforms by y
platforms.sort(function (a, b) {
return a.y - b.y;
});
// Score
platformsPassed = 0;
coinsCollected = 0;
// Count all platforms currently on screen as created (including platform0 and the 11 created in the loop)
platformsCreated = platforms.length;
scoreTxt.setText(platformsCreated.toString());
highScoreTxt.setText('Best: ' + highScore);
// Initialize platformsRemoved counter
platformsRemoved = 0;
// Reset theme and background
game.lastThemeIndex = -1;
// Use bgTheme asset classes for background
var themeIdx = getThemeIndex(platformsPassed);
// Map for each theme: [opaqueAsset, transparentAsset]
var bgThemeAssets = [['bgThemeGrassOpaque', 'bgThemeGrassTrans'],
// 0 Grass
['bgThemeForestOpaque', 'bgThemeForestTrans'],
// 1 Forest
['bgThemeMudOpaque', 'bgThemeMudTrans'],
// 2 Mud
['bgThemeMagicOpaque', 'bgThemeMagicTrans'],
// 3 Magic
['bgThemeCandyOpaque', 'bgThemeCandyTrans'],
// 4 Candy
['bgThemeNightOpaque', 'bgThemeNightTrans'],
// 5 Night
['bgThemeStoneOpaque', 'bgThemeStoneTrans'],
// 6 Stone
['bgThemeIceOpaque', 'bgThemeIceTrans'],
// 7 Ice
['bgThemeMetalOpaque', 'bgThemeMetalTrans'],
// 8 Metal
['bgThemeNeonOpaque', 'bgThemeNeonTrans'],
// 9 Neon
['bgThemeSpecialOpaque', 'bgThemeSpecialTrans'] // 10 Special
];
if (themeIdx < 0) {
themeIdx = 0;
}
if (themeIdx >= bgThemeAssets.length) {
themeIdx = bgThemeAssets.length - 1;
}
// Remove previous background asset if any
if (game._bgThemeAsset) {
if (typeof game._bgThemeAsset.destroy === "function") {
game._bgThemeAsset.destroy();
}
game._bgThemeAsset = null;
}
// Add new background asset as the first child (behind everything)
var bgOpaqueAssetId = bgThemeAssets[themeIdx][0];
var bgTransAssetId = bgThemeAssets[themeIdx][1];
// Create a container for the background with three segments: left, center, right
var bgThemeContainer = new Container();
var bgOpaqueAssetInfo = LK.getAsset(bgOpaqueAssetId, {
anchorX: 0,
anchorY: 0
});
var bgTransAssetInfo = LK.getAsset(bgTransAssetId, {
anchorX: 0,
anchorY: 0
});
var bgWidth = GAME_W;
var leftW = Math.floor(bgWidth / 30);
var rightW = Math.floor(bgWidth / 30);
var centerW = bgWidth - leftW - rightW;
// Left 1/30 (opaque)
var bgLeft = LK.getAsset(bgOpaqueAssetId, {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: leftW / bgOpaqueAssetInfo.width,
scaleY: GAME_H / bgOpaqueAssetInfo.height,
alpha: 1
});
bgThemeContainer.addChild(bgLeft);
// Center (transparent)
var bgCenter = LK.getAsset(bgTransAssetId, {
anchorX: 0,
anchorY: 0,
x: leftW,
y: 0,
scaleX: centerW / bgTransAssetInfo.width,
scaleY: GAME_H / bgTransAssetInfo.height,
alpha: 0.3
});
bgThemeContainer.addChild(bgCenter);
// Right 1/30 (opaque)
var bgRight = LK.getAsset(bgOpaqueAssetId, {
anchorX: 0,
anchorY: 0,
x: leftW + centerW,
y: 0,
scaleX: rightW / bgOpaqueAssetInfo.width,
scaleY: GAME_H / bgOpaqueAssetInfo.height,
alpha: 1
});
bgThemeContainer.addChild(bgRight);
game.addChild(bgThemeContainer);
if (game.children && game.children.length > 1) {
game.setChildIndex(bgThemeContainer, 0);
}
game._bgThemeAsset = bgThemeContainer;
// Remove old clouds
for (var i = clouds.length - 1; i >= 0; --i) {
if (clouds[i] && typeof clouds[i].destroy === "function") {
clouds[i].destroy();
}
}
clouds = [];
// Add new clouds to background
for (var i = 0; i < NUM_CLOUDS; ++i) {
var cloud = new Cloud();
// Distribute vertically in the upper 2/3 of the screen
cloud.y = Math.random() * (GAME_H * 0.66);
cloud.x = Math.random() * GAME_W;
game.addChild(cloud);
cloud.setToBack && cloud.setToBack();
clouds.push(cloud);
}
// (Cloud background reset removed)
}
// --- Helper: check collision between player and platform ---
function playerOnPlatform() {
for (var i = 0; i < platforms.length; ++i) {
var plat = platforms[i];
// Only check if player is falling
if (vy >= 0) {
var px = player.x;
var py = player.y;
var platTop = plat.y - (plat.PLATFORM_H ? plat.PLATFORM_H : PLATFORM_H) / 2;
var platLeft = plat.x - plat.width / 2;
var platRight = plat.x + plat.width / 2;
// Use previous player y for more reliable collision
if (typeof player.lastY === "undefined") {
player.lastY = py - vy;
}
// Check if player's feet crossed the platform top this frame
if (player.lastY <= platTop && py >= platTop) {
// Require player to be horizontally within platform bounds (with a small margin)
if (px > platLeft + 10 && px < platRight - 10) {
player.lastY = py; // update for next frame
return plat;
}
}
}
}
if (typeof player !== "undefined" && player !== null) {
player.lastY = player.y;
}
return null;
}
// --- Touch controls: left/right jump ---
game.down = function (x, y, obj) {
if (gameOver) {
return;
}
// Add this touch to activeTouches
if (obj && obj.event && typeof obj.event.identifier !== "undefined") {
// Remove if already present (shouldn't happen, but for safety)
for (var i = 0; i < activeTouches.length; ++i) {
if (activeTouches[i].id === obj.event.identifier) {
activeTouches.splice(i, 1);
break;
}
}
activeTouches.push({
id: obj.event.identifier,
x: x,
y: y
});
} else {
// Fallback for mouse or single touch
activeTouches = [{
id: 0,
x: x,
y: y
}];
}
isTouching = true;
touchStartX = x;
// Always use the last finger down for direction, even after double jump
var lastTouch = activeTouches[activeTouches.length - 1];
if (lastTouch && lastTouch.x < GAME_W / 2) {
vx = -MOVE_SPEED;
} else {
vx = MOVE_SPEED;
}
// If player is on ground/platform, jump
if (!isJumping && !game.hasDoubleJumped) {
vy = JUMP_VELOCITY;
isJumping = true;
}
// No-op: direction change after double jump is handled in move handler
};
game.up = function (x, y, obj) {
// Remove this touch from activeTouches
if (obj && obj.event && typeof obj.event.identifier !== "undefined") {
for (var i = 0; i < activeTouches.length; ++i) {
if (activeTouches[i].id === obj.event.identifier) {
activeTouches.splice(i, 1);
break;
}
}
} else {
// Fallback for mouse or single touch
activeTouches = [];
}
if (activeTouches.length === 0) {
isTouching = false;
vx = 0;
} else {
// Use the new last finger for direction, even after double jump
var lastTouch = activeTouches[activeTouches.length - 1];
if (lastTouch && lastTouch.x < GAME_W / 2) {
vx = -MOVE_SPEED;
} else {
vx = MOVE_SPEED;
}
}
};
game.move = function (x, y, obj) {
// Update the position of the moving finger in activeTouches
if (obj && obj.event && typeof obj.event.identifier !== "undefined") {
for (var i = 0; i < activeTouches.length; ++i) {
if (activeTouches[i].id === obj.event.identifier) {
activeTouches[i].x = x;
activeTouches[i].y = y;
break;
}
}
} else if (activeTouches.length > 0) {
// Fallback for mouse or single touch
activeTouches[activeTouches.length - 1].x = x;
activeTouches[activeTouches.length - 1].y = y;
}
// Always use the last finger for direction, even after double jump or wall hit
if (activeTouches.length > 0 && !gameOver) {
var lastTouch = activeTouches[activeTouches.length - 1];
if (lastTouch && lastTouch.x < GAME_W / 2) {
vx = -MOVE_SPEED;
} else {
vx = MOVE_SPEED;
}
} else if (!isTouching) {
vx = 0;
}
// No else: vx is always set while touching, even after double jump
};
// --- Main update loop ---
game.update = function () {
if (gameOver) {
return;
}
// Physics
if (player) {
if (typeof player.lastVy === "undefined") {
player.lastVy = vy;
}
if (typeof player.lastVx === "undefined") {
player.lastVx = vx;
}
}
// Smooth velocity interpolation for mobile performance
// Use a simple inertia/lerp for vx to avoid abrupt changes
var targetVx = vx;
if (player) {
if (typeof player.smoothVx === "undefined") {
player.smoothVx = vx;
}
// Increase lerp factor for even smoother and more responsive movement
player.smoothVx += (targetVx - player.smoothVx) * 0.32; // 0.32 is more responsive and smooth
vy += GRAVITY;
player.x += player.smoothVx;
player.y += vy;
player.lastVx = player.smoothVx;
}
// Clamp player to transparent area and bounce on hitting opaque bg borders
if (player && typeof player.x === "number") {
// Track lastX for edge detection
if (typeof player.lastX === "undefined") {
player.lastX = player.x;
}
var playerLeft = player.x - PLAYER_W / 2;
var playerRight = player.x + PLAYER_W / 2;
var leftOpaqueX = BORDER_LEFT_X;
var rightOpaqueX = BORDER_RIGHT_X;
// --- Left wall double jump: trigger as soon as player's leftmost pixel touches rightmost pixel of left opaque bg ---
if (player.lastX - PLAYER_W / 2 > leftOpaqueX && playerLeft <= leftOpaqueX) {
player.x = leftOpaqueX + PLAYER_W / 2;
// Tumble: rotate player quickly
if (player && player.children && player.children.length > 0) {
var pGfx = player.children[0];
tween(pGfx, {
rotation: pGfx.rotation - Math.PI * 2
}, {
duration: 400,
onFinish: function onFinish() {
pGfx.rotation = 0;
}
});
}
// Play shout sound when hitting left border, with cooldown
if (typeof game.lastShoutTime === "undefined") {
game.lastShoutTime = 0;
}
var nowWall = Date.now();
if (nowWall - game.lastShoutTime > 500) {
game.lastShoutTime = nowWall;
}
// Double jump when hitting the border, but only if not already double jumped without release
if (!game.hasDoubleJumped) {
vy = JUMP_VELOCITY * 2;
isJumping = true;
game.hasDoubleJumped = true;
// --- Combo Logic ---
var prevScore = platformsPassed;
var newScore = platformsPassed;
var showCombo = false;
var comboText = "x2";
var comboColor = 0xFF3B30;
var prevComboMultiplier = comboMultiplier;
if (comboActive && comboLastWall === "right" && comboPlatformsLanded === 1) {
// Continue combo: increase multiplier
comboMultiplier *= 2;
showCombo = true;
} else {
// Start new combo
comboMultiplier = 2;
showCombo = true;
}
comboActive = true;
comboLastWall = "left";
comboPlatformsLanded = 0;
comboLastPlatform = null;
comboJustDoubleJumped = true;
comboLastScore = platformsPassed;
// Apply multiplier
prevScore = platformsPassed;
newScore = platformsPassed * comboMultiplier;
platformsPassed = newScore;
// Animate the score value in the UI with a vibrant color flash and scale effect
scoreTxt.setText(platformsPassed.toString());
LK.setScore(platformsPassed + coinsCollected);
// --- Score UI flash animation ---
// Stop any previous tweens on scoreTxt
tween.stop(scoreTxt, {
tint: true,
scaleX: true,
scaleY: true,
alpha: true
});
// Define a sequence of vibrant colors to flash through
var scoreFlashColors = [0xFFD600, 0xFF3B30, 0x00FFD0, 0x4cd964, 0x5ac8fa, 0xFF3B30, 0xFFD600];
var flashDuration = 1200; // total duration in ms
var flashSteps = scoreFlashColors.length;
var flashStepDuration = Math.floor(flashDuration / flashSteps);
var originalTint = 0xffffff;
var originalScaleX = scoreTxt.scale.x;
var originalScaleY = scoreTxt.scale.y;
// Animate color and scale in sequence
(function animateScoreFlash(step) {
if (step >= flashSteps) {
// Restore to original
tween(scoreTxt, {
tint: originalTint,
scaleX: originalScaleX,
scaleY: originalScaleY
}, {
duration: 300,
easing: tween.cubicOut
});
return;
}
tween(scoreTxt, {
tint: scoreFlashColors[step],
scaleX: 1.25,
scaleY: 1.25
}, {
duration: flashStepDuration,
easing: tween.cubicInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: originalScaleX,
scaleY: originalScaleY
}, {
duration: flashStepDuration / 2,
easing: tween.cubicInOut,
onFinish: function onFinish() {
animateScoreFlash(step + 1);
}
});
}
});
})(0);
// --- Animate the newScoreAfterDoubleJump popup flying to the score UI and settling in ---
var prevScoreColor = 0x00FFD0;
var xNColor = 0xFF3B30;
var newScoreColor = 0xFFD600;
var eqColor = 0xffffff;
var popupGroup = new Container();
var baseY = GAME_H / 2 - 200;
var baseX = GAME_W / 2;
var fontSize = 160;
var scaleStart = 2.2;
var scaleBounce = 1.1;
var scaleEnd = 0.7;
// Create each part as a Text2 object
var prevScoreTxt = new Text2(prevScore + "", {
size: fontSize,
fill: prevScoreColor,
font: "Impact"
});
var xNTxt = new Text2("x" + comboMultiplier, {
size: fontSize,
fill: xNColor,
font: "Impact"
});
var eqTxt = new Text2("=", {
size: fontSize,
fill: eqColor,
font: "Impact"
});
var newScoreTxt = new Text2(newScore + "", {
size: fontSize,
fill: newScoreColor,
font: "Impact"
});
// Set anchor to center for all
prevScoreTxt.anchor.set(0.5, 0.5);
xNTxt.anchor.set(0.5, 0.5);
eqTxt.anchor.set(0.5, 0.5);
newScoreTxt.anchor.set(0.5, 0.5);
// Set initial scale and alpha
prevScoreTxt.scale.set(scaleStart, scaleStart);
xNTxt.scale.set(scaleStart, scaleStart);
eqTxt.scale.set(scaleStart, scaleStart);
newScoreTxt.scale.set(scaleStart, scaleStart);
prevScoreTxt.alpha = 0.0;
xNTxt.alpha = 0.0;
eqTxt.alpha = 0.0;
newScoreTxt.alpha = 0.0;
// Add to group
popupGroup.addChild(prevScoreTxt);
popupGroup.addChild(xNTxt);
popupGroup.addChild(eqTxt);
popupGroup.addChild(newScoreTxt);
// Position horizontally centered as a group
// Calculate total width
var spacing = 32;
var totalWidth = prevScoreTxt.width + xNTxt.width + eqTxt.width + newScoreTxt.width + spacing * 3;
var startX = -totalWidth / 2;
prevScoreTxt.x = startX + prevScoreTxt.width / 2;
xNTxt.x = prevScoreTxt.x + prevScoreTxt.width / 2 + xNTxt.width / 2 + spacing;
eqTxt.x = xNTxt.x + xNTxt.width / 2 + eqTxt.width / 2 + spacing;
newScoreTxt.x = eqTxt.x + eqTxt.width / 2 + newScoreTxt.width / 2 + spacing;
// All y at 0, group at baseY
prevScoreTxt.y = 0;
xNTxt.y = 0;
eqTxt.y = 0;
newScoreTxt.y = 0;
popupGroup.x = baseX;
popupGroup.y = baseY;
game.addChild(popupGroup);
// Animate all: pop in, bounce, then float up and fade out, but newScoreTxt will fly to the score UI
var popIn = function popIn(txt, delay) {
tween(txt, {
alpha: 1,
scaleX: scaleBounce,
scaleY: scaleBounce
}, {
duration: 180,
delay: delay,
onFinish: function onFinish() {
if (txt === newScoreTxt) {
// Animate newScoreTxt flying to the score UI and settling in
// Calculate global position of scoreTxt in the UI
var globalScorePos = {
x: 0,
y: 0
};
if (scoreTxt.parent && typeof scoreTxt.parent.toGlobal === "function") {
globalScorePos = scoreTxt.parent.toGlobal(scoreTxt.position);
}
// Convert to local coordinates of the game container
var localScorePos = game.toLocal(globalScorePos);
// Animate to score UI position, shrink and fade out
// Move up and settle at the center of scoreTxt (UI), not to the right
tween(txt, {
x: localScorePos.x + scoreTxt.width / 2,
// center of scoreTxt in game coordinates
y: localScorePos.y,
// move up to align with score UI
scaleX: 0.7,
scaleY: 0.7,
alpha: 0
}, {
duration: 900,
easing: tween.cubicInOut,
onFinish: function onFinish() {
txt.destroy();
}
});
} else {
// Normal float up and fade out
tween(txt, {
y: txt.y - 180,
scaleX: scaleEnd,
scaleY: scaleEnd,
alpha: 0
}, {
duration: 900,
onFinish: function onFinish() {
txt.destroy();
}
});
}
}
});
};
// Stagger the pop-in for a fun effect
popIn(prevScoreTxt, 0);
popIn(xNTxt, 60);
popIn(eqTxt, 120);
popIn(newScoreTxt, 180);
// Destroy the group container after all animations
tween(popupGroup, {}, {
duration: 1400,
onFinish: function onFinish() {
if (popupGroup && typeof popupGroup.destroy === "function") popupGroup.destroy();
}
});
// Start continuous star emission after double jump
isEmittingStars = true;
emitStarTimer = 0;
// Rainbow arc effect: create a rainbow container behind the player
var rainbowArc = new Container();
rainbowArc.x = player.x;
rainbowArc.y = player.y - PLAYER_H / 2;
rainbowArc._life = 0;
rainbowArc._maxLife = 36;
rainbowArc._rainbowSegments = [];
var rainbowColors = [0xff3b30, 0xff9500, 0xffcc00, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6];
var rainbowRadius = 110;
var rainbowWidth = 22;
for (var seg = 0; seg < rainbowColors.length; ++seg) {
var segAsset = LK.getAsset('collectibleStar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: rainbowWidth / 90 * (1.1 + seg * 0.08),
scaleY: rainbowWidth / 90 * (1.1 + seg * 0.08),
tint: rainbowColors[seg],
alpha: 0.38
});
// Position in an arc behind the player
var arcAngle = Math.PI * (0.5 + 0.7 * (seg / (rainbowColors.length - 1)));
segAsset.x = Math.cos(arcAngle) * rainbowRadius;
segAsset.y = Math.sin(arcAngle) * rainbowRadius * 0.7;
rainbowArc.addChild(segAsset);
rainbowArc._rainbowSegments.push(segAsset);
}
game.addChild(rainbowArc);
// Animate rainbow arc fade and scale
rainbowArc.update = function () {
rainbowArc._life++;
var t = rainbowArc._life / rainbowArc._maxLife;
rainbowArc.alpha = 0.7 * (1 - t);
rainbowArc.scale.x = 1 + 0.25 * t;
rainbowArc.scale.y = 1 + 0.25 * t;
rainbowArc.x = player.x;
rainbowArc.y = player.y - PLAYER_H / 2;
if (rainbowArc._life > rainbowArc._maxLife) {
if (typeof rainbowArc.destroy === "function") {
rainbowArc.destroy();
}
}
};
stars.push(rainbowArc);
// Emit 96-120 animated stars from player after double jump, with color cycling and rainbow effect (left wall)
var numEmitStars = 96 + Math.floor(Math.random() * 25); // 96-120
for (var emitIdx = 0; emitIdx < numEmitStars; ++emitIdx) {
var emitStar = new Star();
emitStar._emittedAnimation = true; // Mark as animation-only, not collectible
emitStar.x = player.x;
emitStar.y = player.y - PLAYER_H / 2;
// Give each star a random direction and speed
var angle = Math.PI * 2 * (emitIdx / numEmitStars) + (Math.random() - 0.5) * 0.5;
var speed = 18 + Math.random() * 8;
emitStar._vx = Math.cos(angle) * speed;
emitStar._vy = Math.sin(angle) * speed - 6;
emitStar._life = 0;
emitStar._maxLife = 32 + Math.random() * 10;
// Color cycling: pick a random start color and cycle through a palette
var colorCycle = [0xfff7b2, 0xffe066, 0xffb347, 0xff3b30, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6, 0xffcc00];
var colorIdx = Math.floor(Math.random() * colorCycle.length);
// Override update for animation
emitStar.update = function (star, colorIdx) {
return function () {
star.x += star._vx;
star.y += star._vy;
star._vy += 1.1; // gravity
star._vx *= 0.96; // friction
star._life++;
// Color cycling: change color every 6 frames
if (star.children && star.children[0]) {
var cycleStep = Math.floor(star._life / 6);
var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length];
star.children[0].tint = nextColor;
// Fade out
star.children[0].alpha = Math.max(0, 0.92 * (1 - star._life / star._maxLife));
}
if (star._life > star._maxLife) {
if (typeof star.destroy === "function") {
star.destroy();
}
var idx = stars.indexOf(star);
if (idx !== -1) {
stars.splice(idx, 1);
}
}
};
}(emitStar, colorIdx);
game.addChild(emitStar);
stars.push(emitStar);
}
// Play garavel sound only if double jump is successful
playNextGaravelSound();
}
}
// --- Right wall double jump: trigger as soon as player's rightmost pixel touches leftmost pixel of right opaque bg ---
if (player.lastX + PLAYER_W / 2 < rightOpaqueX && playerRight >= rightOpaqueX) {
player.x = rightOpaqueX - PLAYER_W / 2;
// Tumble: rotate player quickly
if (player && player.children && player.children.length > 0) {
var pGfx = player.children[0];
tween(pGfx, {
rotation: pGfx.rotation + Math.PI * 2
}, {
duration: 400,
onFinish: function onFinish() {
pGfx.rotation = 0;
}
});
}
// Play shout sound when hitting right border, with cooldown
if (typeof game.lastShoutTime === "undefined") {
game.lastShoutTime = 0;
}
var nowWall = Date.now();
if (nowWall - game.lastShoutTime > 500) {
game.lastShoutTime = nowWall;
}
// Double jump when hitting the border, but only if not already double jumped without release
if (!game.hasDoubleJumped) {
vy = JUMP_VELOCITY * 2;
isJumping = true;
game.hasDoubleJumped = true;
// --- Combo Logic ---
var prevScore = platformsPassed;
var newScore = platformsPassed;
var showCombo = false;
var comboText = "x2";
var comboColor = 0xFF3B30;
var prevComboMultiplier = comboMultiplier;
if (comboActive && comboLastWall === "left" && comboPlatformsLanded === 1) {
// Continue combo: increase multiplier
comboMultiplier *= 2;
showCombo = true;
} else {
// Start new combo
comboMultiplier = 2;
showCombo = true;
}
comboActive = true;
comboLastWall = "right";
comboPlatformsLanded = 0;
comboLastPlatform = null;
comboJustDoubleJumped = true;
comboLastScore = platformsPassed;
// Apply multiplier
prevScore = platformsPassed;
newScore = platformsPassed * comboMultiplier;
platformsPassed = newScore;
// Animate the score value in the UI with a vibrant color flash and scale effect
scoreTxt.setText(platformsPassed.toString());
LK.setScore(platformsPassed + coinsCollected);
// --- Score UI flash animation ---
// Stop any previous tweens on scoreTxt
tween.stop(scoreTxt, {
tint: true,
scaleX: true,
scaleY: true,
alpha: true
});
// Define a sequence of vibrant colors to flash through
var scoreFlashColors = [0xFFD600, 0xFF3B30, 0x00FFD0, 0x4cd964, 0x5ac8fa, 0xFF3B30, 0xFFD600];
var flashDuration = 1200; // total duration in ms
var flashSteps = scoreFlashColors.length;
var flashStepDuration = Math.floor(flashDuration / flashSteps);
var originalTint = 0xffffff;
var originalScaleX = scoreTxt.scale.x;
var originalScaleY = scoreTxt.scale.y;
// Animate color and scale in sequence
(function animateScoreFlash(step) {
if (step >= flashSteps) {
// Restore to original
tween(scoreTxt, {
tint: originalTint,
scaleX: originalScaleX,
scaleY: originalScaleY
}, {
duration: 300,
easing: tween.cubicOut
});
return;
}
tween(scoreTxt, {
tint: scoreFlashColors[step],
scaleX: 1.25,
scaleY: 1.25
}, {
duration: flashStepDuration,
easing: tween.cubicInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: originalScaleX,
scaleY: originalScaleY
}, {
duration: flashStepDuration / 2,
easing: tween.cubicInOut,
onFinish: function onFinish() {
animateScoreFlash(step + 1);
}
});
}
});
})(0);
// --- Animate the newScoreAfterDoubleJump popup flying to the score UI and settling in ---
var prevScoreColor = 0x00FFD0;
var xNColor = 0xFF3B30;
var newScoreColor = 0xFFD600;
var eqColor = 0xffffff;
var popupGroup = new Container();
var baseY = GAME_H / 2 - 200;
var baseX = GAME_W / 2;
var fontSize = 160;
var scaleStart = 2.2;
var scaleBounce = 1.1;
var scaleEnd = 0.7;
// Create each part as a Text2 object
var prevScoreTxt = new Text2(prevScore + "", {
size: fontSize,
fill: prevScoreColor,
font: "Impact"
});
var xNTxt = new Text2("x" + comboMultiplier, {
size: fontSize,
fill: xNColor,
font: "Impact"
});
var eqTxt = new Text2("=", {
size: fontSize,
fill: eqColor,
font: "Impact"
});
var newScoreTxt = new Text2(newScore + "", {
size: fontSize,
fill: newScoreColor,
font: "Impact"
});
// Set anchor to center for all
prevScoreTxt.anchor.set(0.5, 0.5);
xNTxt.anchor.set(0.5, 0.5);
eqTxt.anchor.set(0.5, 0.5);
newScoreTxt.anchor.set(0.5, 0.5);
// Set initial scale and alpha
prevScoreTxt.scale.set(scaleStart, scaleStart);
xNTxt.scale.set(scaleStart, scaleStart);
eqTxt.scale.set(scaleStart, scaleStart);
newScoreTxt.scale.set(scaleStart, scaleStart);
prevScoreTxt.alpha = 0.0;
xNTxt.alpha = 0.0;
eqTxt.alpha = 0.0;
newScoreTxt.alpha = 0.0;
// Add to group
popupGroup.addChild(prevScoreTxt);
popupGroup.addChild(xNTxt);
popupGroup.addChild(eqTxt);
popupGroup.addChild(newScoreTxt);
// Position horizontally centered as a group
// Calculate total width
var spacing = 32;
var totalWidth = prevScoreTxt.width + xNTxt.width + eqTxt.width + newScoreTxt.width + spacing * 3;
var startX = -totalWidth / 2;
prevScoreTxt.x = startX + prevScoreTxt.width / 2;
xNTxt.x = prevScoreTxt.x + prevScoreTxt.width / 2 + xNTxt.width / 2 + spacing;
eqTxt.x = xNTxt.x + xNTxt.width / 2 + eqTxt.width / 2 + spacing;
newScoreTxt.x = eqTxt.x + eqTxt.width / 2 + newScoreTxt.width / 2 + spacing;
// All y at 0, group at baseY
prevScoreTxt.y = 0;
xNTxt.y = 0;
eqTxt.y = 0;
newScoreTxt.y = 0;
popupGroup.x = baseX;
popupGroup.y = baseY;
game.addChild(popupGroup);
// Animate all: pop in, bounce, then float up and fade out, but newScoreTxt will fly to the score UI
var popIn = function popIn(txt, delay) {
tween(txt, {
alpha: 1,
scaleX: scaleBounce,
scaleY: scaleBounce
}, {
duration: 180,
delay: delay,
onFinish: function onFinish() {
if (txt === newScoreTxt) {
// Animate newScoreTxt flying to the score UI and settling in
// Calculate global position of scoreTxt in the UI
var globalScorePos = {
x: 0,
y: 0
};
if (scoreTxt.parent && typeof scoreTxt.parent.toGlobal === "function") {
globalScorePos = scoreTxt.parent.toGlobal(scoreTxt.position);
}
// Convert to local coordinates of the game container
var localScorePos = game.toLocal(globalScorePos);
// Animate to score UI position, shrink and fade out
tween(txt, {
x: localScorePos.x + scoreTxt.width / 2,
y: localScorePos.y,
// move up to align with score UI
scaleX: 0.7,
scaleY: 0.7,
alpha: 0
}, {
duration: 900,
easing: tween.cubicInOut,
onFinish: function onFinish() {
txt.destroy();
}
});
} else {
// Normal float up and fade out
tween(txt, {
y: txt.y - 180,
scaleX: scaleEnd,
scaleY: scaleEnd,
alpha: 0
}, {
duration: 900,
onFinish: function onFinish() {
txt.destroy();
}
});
}
}
});
};
// Stagger the pop-in for a fun effect
popIn(prevScoreTxt, 0);
popIn(xNTxt, 60);
popIn(eqTxt, 120);
popIn(newScoreTxt, 180);
// Destroy the group container after all animations
tween(popupGroup, {}, {
duration: 1400,
onFinish: function onFinish() {
if (popupGroup && typeof popupGroup.destroy === "function") popupGroup.destroy();
}
});
// Start continuous star emission after double jump (right wall)
isEmittingStars = true;
emitStarTimer = 0;
// Rainbow arc effect: create a rainbow container behind the player
var rainbowArc = new Container();
rainbowArc.x = player.x;
rainbowArc.y = player.y - PLAYER_H / 2;
rainbowArc._life = 0;
rainbowArc._maxLife = 36;
rainbowArc._rainbowSegments = [];
var rainbowColors = [0xff3b30, 0xff9500, 0xffcc00, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6];
var rainbowRadius = 110;
var rainbowWidth = 22;
for (var seg = 0; seg < rainbowColors.length; ++seg) {
var segAsset = LK.getAsset('collectibleStar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: rainbowWidth / 90 * (1.1 + seg * 0.08),
scaleY: rainbowWidth / 90 * (1.1 + seg * 0.08),
tint: rainbowColors[seg],
alpha: 0.38
});
// Position in an arc behind the player
var arcAngle = Math.PI * (0.5 + 0.7 * (seg / (rainbowColors.length - 1)));
segAsset.x = Math.cos(arcAngle) * rainbowRadius;
segAsset.y = Math.sin(arcAngle) * rainbowRadius * 0.7;
rainbowArc.addChild(segAsset);
rainbowArc._rainbowSegments.push(segAsset);
}
game.addChild(rainbowArc);
// Animate rainbow arc fade and scale
rainbowArc.update = function () {
rainbowArc._life++;
var t = rainbowArc._life / rainbowArc._maxLife;
rainbowArc.alpha = 0.7 * (1 - t);
rainbowArc.scale.x = 1 + 0.25 * t;
rainbowArc.scale.y = 1 + 0.25 * t;
rainbowArc.x = player.x;
rainbowArc.y = player.y - PLAYER_H / 2;
if (rainbowArc._life > rainbowArc._maxLife) {
if (typeof rainbowArc.destroy === "function") {
rainbowArc.destroy();
}
}
};
stars.push(rainbowArc);
// Emit 96-120 animated stars from player after double jump, with color cycling and rainbow effect
var numEmitStars = 96 + Math.floor(Math.random() * 25); // 96-120
for (var emitIdx = 0; emitIdx < numEmitStars; ++emitIdx) {
var emitStar = new Star();
emitStar._emittedAnimation = true; // Mark as animation-only, not collectible
emitStar.x = player.x;
emitStar.y = player.y - PLAYER_H / 2;
// Give each star a random direction and speed
var angle = Math.PI * 2 * (emitIdx / numEmitStars) + (Math.random() - 0.5) * 0.5;
var speed = 18 + Math.random() * 8;
emitStar._vx = Math.cos(angle) * speed;
emitStar._vy = Math.sin(angle) * speed - 6;
emitStar._life = 0;
emitStar._maxLife = 32 + Math.random() * 10;
// Color cycling: pick a random start color and cycle through a palette
var colorCycle = [0xfff7b2, 0xffe066, 0xffb347, 0xff3b30, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6, 0xffcc00];
var colorIdx = Math.floor(Math.random() * colorCycle.length);
// Override update for animation
emitStar.update = function (star, colorIdx) {
return function () {
star.x += star._vx;
star.y += star._vy;
star._vy += 1.1; // gravity
star._vx *= 0.96; // friction
star._life++;
// Color cycling: change color every 6 frames
if (star.children && star.children[0]) {
var cycleStep = Math.floor(star._life / 6);
var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length];
star.children[0].tint = nextColor;
// Fade out
star.children[0].alpha = Math.max(0, 0.92 * (1 - star._life / star._maxLife));
}
if (star._life > star._maxLife) {
if (typeof star.destroy === "function") {
star.destroy();
}
var idx = stars.indexOf(star);
if (idx !== -1) {
stars.splice(idx, 1);
}
}
};
}(emitStar, colorIdx);
game.addChild(emitStar);
stars.push(emitStar);
}
// Play garavel sound only if double jump is successful
playNextGaravelSound();
}
}
// --- Clamp player to transparent area (between opaque borders) and bounce if hitting border ---
// Clamp left
if (player.x - PLAYER_W / 2 < leftOpaqueX) {
player.x = leftOpaqueX + PLAYER_W / 2;
if (player.smoothVx < 0) {
player.smoothVx = -player.smoothVx * 0.7;
} // bounce right, lose some speed
if (vx < 0) {
vx = -vx * 0.7;
}
}
// Clamp right
if (player.x + PLAYER_W / 2 > rightOpaqueX) {
player.x = rightOpaqueX - PLAYER_W / 2;
if (player.smoothVx > 0) {
player.smoothVx = -player.smoothVx * 0.7;
} // bounce left, lose some speed
if (vx > 0) {
vx = -vx * 0.7;
}
}
}
// Platform collision
var plat = playerOnPlatform();
// (Removed logic that made checkpoint platforms appear after passing. Now, checkpoint platforms always come down from the top like other platforms.)
// --- Increment platformsPassed when player passes a platform (crosses its top Y going down) ---
// Reset scoreTxt logic: count unique platforms passed, not label number
if (player && typeof player.lastPlatformY === "undefined") {
player.lastPlatformY = null;
}
if (plat && vy > 0) {
// Only increment if this platform hasn't been counted yet
if (!plat._countedPassed) {
plat._countedPassed = true;
// Update internal counter
platformsPassed++;
}
player.y = plat.y - PLATFORM_H / 2;
vy = JUMP_VELOCITY;
isJumping = false;
game.hasDoubleJumped = false; // Reset double jump lock only when landing
isEmittingStars = false; // Stop emitting stars when landing
// --- Combo landing logic ---
if (comboActive) {
if (comboJustDoubleJumped) {
// First landing after double jump
comboPlatformsLanded = 1;
comboLastPlatform = plat;
comboJustDoubleJumped = false;
} else if (comboLastPlatform !== plat) {
comboPlatformsLanded += 1;
comboLastPlatform = plat;
}
// If player lands on more than one platform before next double jump, reset combo
if (comboPlatformsLanded > 1) {
comboActive = false;
comboMultiplier = 1;
comboLastWall = null;
comboPlatformsLanded = 0;
comboLastPlatform = null;
comboJustDoubleJumped = false;
}
}
// Always play jump animation here for consistency
if (player && player.children && player.children.length > 0) {
var pGfx = player.children[0];
// Squash down, stretch wide, add a little rotation (no color change)
tween(pGfx, {
scaleY: 0.7,
scaleX: 1.25,
rotation: 0.18
}, {
duration: 90,
onFinish: function onFinish() {
// Stretch up, squash in, overshoot a bit for bounce
tween(pGfx, {
scaleY: 1.18 * PLAYER_H / 320,
scaleX: 0.88 * PLAYER_W / 320,
rotation: -0.08
}, {
duration: 90,
onFinish: function onFinish() {
// Return to normal
tween(pGfx, {
scaleY: PLAYER_H / 320,
scaleX: PLAYER_W / 320,
rotation: 0
}, {
duration: 90
});
}
});
}
});
}
} else {
isJumping = true;
// No shout or fall sound here
}
// --- Coin collection ---
// Use last position to detect passing through coins, not just landing on top
if (player && typeof player.lastX === "undefined") {
player.lastX = player.x;
}
if (player && typeof player.lastY === "undefined") {
player.lastY = player.y;
}
for (var j = coins.length - 1; j >= 0; --j) {
var coin = coins[j];
// Simple circle collision, check both current and previous position for pass-through
var dx = player.x - coin.x;
var dy = player.y - PLAYER_H / 2 - coin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var lastDx = player.lastX - coin.x;
var lastDy = player.lastY - PLAYER_H / 2 - coin.y;
var lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy);
var collectRadius = (PLAYER_W / 2 + coin.radius) * 0.7;
// If player is within collect radius now or passed through it this frame
if (dist < collectRadius || lastDist > collectRadius && dist < collectRadius || lastDist < collectRadius && dist < collectRadius) {
// Collect coin
coin.showValuePopup();
coinsCollected += coin.value * coinValueMultiplier;
coins.splice(j, 1);
coin.destroy();
}
}
// --- Star collection ---
// Use last position to detect passing through stars, not just landing on top
for (var j = stars.length - 1; j >= 0; --j) {
var star = stars[j];
var dx = player.x - star.x;
var dy = player.y - PLAYER_H / 2 - star.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var lastDx = player.lastX - star.x;
var lastDy = player.lastY - PLAYER_H / 2 - star.y;
var lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy);
var collectRadius = (PLAYER_W / 2 + star.radius) * 0.7;
// Only collect stars that are not "emitted" animation stars (i.e., only collectible platform stars)
// We'll mark emitted stars with a flag: star._emittedAnimation === true
if ((dist < collectRadius || lastDist > collectRadius && dist < collectRadius || lastDist < collectRadius && dist < collectRadius) && !star._emittedAnimation) {
// Collect star
star.showValuePopup();
coinsCollected += star.value * coinValueMultiplier;
stars.splice(j, 1);
star.destroy();
}
}
// Update lastX/lastY for next frame
if (player) {
player.lastX = player.x;
player.lastY = player.y;
}
// Camera follows player upward
if (player && player.y < GAME_H - CAMERA_OFFSET) {
var diff = GAME_H - CAMERA_OFFSET - player.y;
cameraY += diff;
// Move all platforms and player down by diff
for (var i = 0; i < platforms.length; ++i) {
platforms[i].y += diff;
}
// Move all coins down by diff so they stay visually attached to their platform
for (var i = 0; i < coins.length; ++i) {
coins[i].y += diff;
}
// Move all stars down by diff so they stay visually attached to their platform
for (var i = 0; i < stars.length; ++i) {
stars[i].y += diff;
}
// Move all clouds down by diff, and update their position
for (var i = clouds.length - 1; i >= 0; --i) {
var cloud = clouds[i];
cloud.y += diff * 0.6; // Parallax: move slower than platforms for depth
cloud.update && cloud.update();
// Remove cloud if it goes off bottom, add new one at top
if (cloud.y > GAME_H + 200) {
cloud.destroy();
clouds.splice(i, 1);
// Add new cloud at top
var newCloud = new Cloud();
newCloud.x = Math.random() * GAME_W;
newCloud.y = -100 - Math.random() * 200;
game.addChild(newCloud);
newCloud.setToBack && newCloud.setToBack();
clouds.push(newCloud);
}
}
player.y += diff;
maxHeight += diff;
}
// Always keep player in front of platforms
if (player && player.parent && player.parent.children && player.parent.children.indexOf(player) !== player.parent.children.length - 1) {
player.parent.setChildIndex(player, player.parent.children.length - 1);
}
// Remove platforms that are off screen, add new ones at top
for (var i = platforms.length - 1; i >= 0; --i) {
if (platforms[i].y > GAME_H + 100) {
// Remove any coins that are visually on this platform (within platform width and just above it)
for (var j = coins.length - 1; j >= 0; --j) {
var coin = coins[j];
// Coin is considered on this platform if its x is within platform width and y is just above platform
if (coin.x >= platforms[i].x - platforms[i].width / 2 && coin.x <= platforms[i].x + platforms[i].width / 2 && Math.abs(coin.y - (platforms[i].y - PLATFORM_H / 2 - 40)) < 60) {
coin.destroy();
coins.splice(j, 1);
}
}
// Remove any stars that are visually on this platform (within platform width and just above it)
for (var j = stars.length - 1; j >= 0; --j) {
var star = stars[j];
if (star.x >= platforms[i].x - platforms[i].width / 2 && star.x <= platforms[i].x + platforms[i].width / 2 && Math.abs(star.y - (platforms[i].y - PLATFORM_H / 2 - 100)) < 80) {
star.destroy();
stars.splice(j, 1);
}
}
platforms[i].destroy();
platforms.splice(i, 1);
// --- Count platforms removed ---
// (No longer updates scoreTxt, which now shows platformsCreated)
if (typeof platformsRemoved === "undefined") {
platformsRemoved = 0;
}
platformsRemoved += 1;
}
}
// Add new platforms if needed
while (platforms.length < 10) {
// Defensive: If platforms array is empty, break to avoid crash
if (!platforms.length || !platforms[0] || typeof platforms[0].y === "undefined") {
break;
}
var topY = platforms[0].y;
// Make platform width and spacing harder as level increases
var level = getThemeIndex(platformsPassed);
var EASY_PLATFORM_W = 820 - level * 40;
if (EASY_PLATFORM_W < 220) {
EASY_PLATFORM_W = 220;
}
// Increase vertical spacing between platforms for more air time
var EASY_PLATFORM_SPACING = 310;
var newY = topY - EASY_PLATFORM_SPACING;
var prevPlat = platforms[0];
var px = getSafePlatformX(prevPlat, EASY_PLATFORM_W);
var plat = createPlatform(px, newY, EASY_PLATFORM_W);
platforms.sort(function (a, b) {
return a.y - b.y;
});
}
// Score: always shows platformsPassed
scoreTxt.setText(platformsPassed.toString());
// Update coins label to show current coins collected
coinsLabelTxt.setText('Coins: ' + coinsCollected);
// Update leaderboard score (score = platformsPassed + coinsCollected)
LK.setScore(platformsPassed + coinsCollected);
// Update the sign on the last platform of each theme to show current platforms passed
for (var i = 0; i < platforms.length; ++i) {
var plat = platforms[i];
if (plat._themeSign && typeof plat._themeSign.setPlatformsPassed === "function") {
plat._themeSign.setPlatformsPassed(platformsPassed);
}
}
// Change background color if level changes, using platform index counter for perfect sync
var themeIndex = getThemeIndex(platformsPassed);
// Always keep track of lastThemeIndex for perfect sync
if (typeof game.lastThemeIndex === "undefined") {
game.lastThemeIndex = getThemeIndex(0);
}
// Only change theme on the exact frame when platformsPassed crosses into a new theme (every 50 platforms passed)
if (Math.floor(game.lastThemeIndex) !== Math.floor(themeIndex) && platformsPassed % 50 === 0) {
// Increase coin value multiplier by 2x for each theme increment
coinValueMultiplier = Math.pow(2, Math.floor(themeIndex));
// Use bgTheme asset classes for background on level change
var bgThemeAssets = [['bgThemeGrassOpaque', 'bgThemeGrassTrans'],
// 0 Grass
['bgThemeForestOpaque', 'bgThemeForestTrans'],
// 1 Forest
['bgThemeMudOpaque', 'bgThemeMudTrans'],
// 2 Mud
['bgThemeMagicOpaque', 'bgThemeMagicTrans'],
// 3 Magic
['bgThemeCandyOpaque', 'bgThemeCandyTrans'],
// 4 Candy
['bgThemeNightOpaque', 'bgThemeNightTrans'],
// 5 Night
['bgThemeStoneOpaque', 'bgThemeStoneTrans'],
// 6 Stone
['bgThemeIceOpaque', 'bgThemeIceTrans'],
// 7 Ice
['bgThemeMetalOpaque', 'bgThemeMetalTrans'],
// 8 Metal
['bgThemeNeonOpaque', 'bgThemeNeonTrans'],
// 9 Neon
['bgThemeSpecialOpaque', 'bgThemeSpecialTrans'] // 10 Special
];
var idx = themeIndex;
if (idx < 0) {
idx = 0;
}
if (idx >= bgThemeAssets.length) {
idx = bgThemeAssets.length - 1;
}
// Remove previous background asset if any
if (game._bgThemeAsset) {
if (typeof game._bgThemeAsset.destroy === "function") {
game._bgThemeAsset.destroy();
}
game._bgThemeAsset = null;
}
// Add new background asset as the first child (behind everything)
var bgOpaqueAssetId = bgThemeAssets[idx][0];
var bgTransAssetId = bgThemeAssets[idx][1];
// Create a container for the background with three segments: left, center, right
var bgThemeContainer = new Container();
var bgOpaqueAssetInfo = LK.getAsset(bgOpaqueAssetId, {
anchorX: 0,
anchorY: 0
});
var bgTransAssetInfo = LK.getAsset(bgTransAssetId, {
anchorX: 0,
anchorY: 0
});
var bgWidth = GAME_W;
var leftW = Math.floor(bgWidth / 30);
var rightW = Math.floor(bgWidth / 30);
var centerW = bgWidth - leftW - rightW;
// Left 1/30 (opaque)
var bgLeft = LK.getAsset(bgOpaqueAssetId, {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: leftW / bgOpaqueAssetInfo.width,
scaleY: GAME_H / bgOpaqueAssetInfo.height,
alpha: 1
});
bgThemeContainer.addChild(bgLeft);
// Center (transparent)
var bgCenter = LK.getAsset(bgTransAssetId, {
anchorX: 0,
anchorY: 0,
x: leftW,
y: 0,
scaleX: centerW / bgTransAssetInfo.width,
scaleY: GAME_H / bgTransAssetInfo.height,
alpha: 0.3
});
bgThemeContainer.addChild(bgCenter);
// Right 1/30 (opaque)
var bgRight = LK.getAsset(bgOpaqueAssetId, {
anchorX: 0,
anchorY: 0,
x: leftW + centerW,
y: 0,
scaleX: rightW / bgOpaqueAssetInfo.width,
scaleY: GAME_H / bgOpaqueAssetInfo.height,
alpha: 1
});
bgThemeContainer.addChild(bgRight);
game.addChild(bgThemeContainer);
if (game.children && game.children.length > 1) {
game.setChildIndex(bgThemeContainer, 0);
}
game._bgThemeAsset = bgThemeContainer;
game.lastThemeIndex = themeIndex;
}
// High score logic (still based on maxHeight climbed)
var score = Math.floor(maxHeight / 10);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Game over: fall below screen
if (player && player.y > GAME_H + 200) {
gameOver = true;
// Play a random death sound when it's certain the player is going to die
var deathSounds = ['death-1', 'death-2'];
var randomIdx = Math.floor(Math.random() * deathSounds.length);
var deathSound = LK.getSound(deathSounds[randomIdx]);
var played = deathSound.play();
// Always wait 3 seconds before showing game over, regardless of sound
LK.setTimeout(function () {
LK.showGameOver();
}, 3000);
}
// Update lastVy for next frame
if (player) {
player.lastVy = vy;
}
// Update clouds for horizontal drift (even if camera doesn't move)
for (var i = 0; i < clouds.length; ++i) {
if (clouds[i] && typeof clouds[i].update === "function") {
clouds[i].update();
}
}
// Emit a star every few frames while isEmittingStars is true
if (isEmittingStars && player && typeof player.x === "number" && typeof player.y === "number") {
emitStarTimer++;
if (emitStarTimer >= emitStarInterval) {
emitStarTimer = 0;
var emitStar = new Star();
emitStar._emittedAnimation = true; // Mark as animation-only, not collectible
emitStar.x = player.x;
emitStar.y = player.y - PLAYER_H / 2;
// Give each star a random direction and speed
var angle = Math.random() * Math.PI * 2;
var speed = 14 + Math.random() * 7;
emitStar._vx = Math.cos(angle) * speed;
emitStar._vy = Math.sin(angle) * speed - 4;
emitStar._life = 0;
emitStar._maxLife = 28 + Math.random() * 8;
// Color cycling: pick a random start color and cycle through a palette
var colorCycle = [0xfff7b2, 0xffe066, 0xffb347, 0xff3b30, 0x4cd964, 0x5ac8fa, 0x007aff, 0x5856d6, 0xffcc00];
var colorIdx = Math.floor(Math.random() * colorCycle.length);
emitStar.update = function (star, colorIdx) {
return function () {
star.x += star._vx;
star.y += star._vy;
star._vy += 1.1; // gravity
star._vx *= 0.96; // friction
star._life++;
// Color cycling: change color every 6 frames
if (star.children && star.children[0]) {
var cycleStep = Math.floor(star._life / 6);
var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length];
star.children[0].tint = nextColor;
// Fade out
star.children[0].alpha = Math.max(0, 0.92 * (1 - star._life / star._maxLife));
}
if (star._life > star._maxLife) {
if (typeof star.destroy === "function") {
star.destroy();
}
var idx = stars.indexOf(star);
if (idx !== -1) {
stars.splice(idx, 1);
}
}
};
}(emitStar, colorIdx);
game.addChild(emitStar);
stars.push(emitStar);
}
}
// Update stars for animation
for (var i = 0; i < stars.length; ++i) {
if (stars[i] && typeof stars[i].update === "function") {
stars[i].update();
}
}
// (Cloud update and spawn logic removed)
};
// --- Character selection support ---
// List of available character asset IDs
var availableCharacters = ['chipCharacter', 'chipCharacter2', 'chipCharacter3', 'chipCharacter4', 'chipCharacter5', 'chipCharacter6'];
// Selected character asset ID (default to first)
var selectedCharacterAssetId = availableCharacters[0];
// Show character selection menu before starting the game
function showCharacterSelectionMenu(availableCharacters, onSelect) {
// Remove any previous menu
if (typeof showCharacterSelectionMenu._container !== "undefined" && showCharacterSelectionMenu._container) {
showCharacterSelectionMenu._container.destroy();
showCharacterSelectionMenu._container = null;
}
// Play opening music when character selection menu is shown
LK.playMusic('opening-music');
var menuContainer = new Container();
showCharacterSelectionMenu._container = menuContainer;
// Solid black rectangle overlay that fits the whole screen
var overlay = LK.getAsset('uiTopBgRect', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: GAME_W / 100,
scaleY: GAME_H / 110,
alpha: 0.7
});
menuContainer.addChild(overlay);
// Title
var title = new Text2("Select Your Character", {
size: 110,
fill: "#fff",
font: "Impact"
});
title.anchor.set(0.5, 0);
title.x = GAME_W / 2 - 300;
title.y = 140 + 75 + 250;
menuContainer.addChild(title);
// Layout character icons in two rows of three, centered
var iconSize = 320; // doubled from 160 to 320
var iconScale = 1.0;
var spacing = 420; // doubled from 210 to 420
var iconsPerRow = 3;
var numRows = Math.ceil(availableCharacters.length / iconsPerRow);
var totalWidth = (iconsPerRow - 1) * spacing;
var startX = GAME_W / 2 - totalWidth / 2 - 300;
var startY = 600 + 75 + 250;
for (var i = 0; i < availableCharacters.length; ++i) {
(function (idx) {
var assetId = availableCharacters[idx];
var assetInfo = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale to fit both width and height, preserving aspect ratio
var scaleW = iconSize / assetInfo.width;
var scaleH = iconSize / assetInfo.height;
var scale = Math.min(scaleW, scaleH) * iconScale;
var icon = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale
});
var iconContainer = new Container();
var row = Math.floor(idx / iconsPerRow);
var col = idx % iconsPerRow;
iconContainer.x = startX + col * spacing;
iconContainer.y = startY + row * (iconSize + 80);
iconContainer.addChild(icon);
// Add a border highlight for selection feedback
var border = LK.getAsset('uiTopBgRect', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: (iconSize + 32) / 100,
scaleY: (iconSize + 32) / 110,
alpha: 0.18
});
border.x = 0;
border.y = 0;
iconContainer.addChild(border);
// Touch/click handler
iconContainer.down = function (x, y, obj) {
// Remove menu
if (showCharacterSelectionMenu._container) {
showCharacterSelectionMenu._container.destroy();
showCharacterSelectionMenu._container = null;
}
if (typeof onSelect === "function") {
onSelect(assetId);
}
};
// (Removed character name label below icon)
menuContainer.addChild(iconContainer);
})(i);
}
// Add menu to GUI overlay (top left)
LK.gui.topLeft.addChild(menuContainer);
}
// Call our custom character selection menu
showCharacterSelectionMenu(availableCharacters, function (chosenId) {
if (availableCharacters.indexOf(chosenId) !== -1) {
selectedCharacterAssetId = chosenId;
} else {
selectedCharacterAssetId = availableCharacters[0];
}
// Start the game after character is selected
resetGame();
// Play a random game theme song (1-4) at a lower volume for a more subtle effect
var themeSongs = ['game-theme-song-1', 'game-theme-song-2', 'game-theme-song-3'];
var randomThemeIdx = Math.floor(Math.random() * 4); // Ensure 0-3 inclusive
LK.playMusic(themeSongs[randomThemeIdx], {
volume: 0.10
});
});
icy tower guy. In-Game asset. 2d. High contrast. No shadows
mario or icy tower like platforms. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
icy tower advanced level platform. In-Game asset. 2d. High contrast. No shadows
diamond. In-Game asset. 2d. High contrast. No shadows
dollar. In-Game asset. 2d. High contrast. No shadows
Design a single floating 2D game platform made of levitating crystal shards, connected by glowing magical runes or light energy. No ice or snow. The platform should feel arcane and unique. No background.. In-Game asset. 2d. High contrast. No shadows
rectangle shape jumping platform for a simple 2D game. In-Game asset. 2d. High contrast. No shadows
super mario facing camera. In-Game asset. 2d. High contrast. No shadows
blue transparent cloud. In-Game asset. 2d. High contrast. No shadows
bright transparent cloud. In-Game asset. 2d. High contrast. No shadows
fluffy transparent cloud. In-Game asset. 2d. High contrast. No shadows
orange transparent cloud. In-Game asset. 2d. High contrast. No shadows
grey transparent cloud. In-Game asset. 2d. High contrast. No shadows
star. In-Game asset. 2d. High contrast. No shadows
icy tower background without platforms, just walls. In-Game asset. 2d. High contrast. No shadows
just a start line without any text. In-Game asset. 2d. High contrast. No shadows
stuart little jumping and raised its arms. In-Game asset. 2d. High contrast. No shadows. facing camera
shout
Sound effect
fall
Sound effect
darara
Music
garavel-1
Sound effect
garavel-2
Sound effect
garavel-3
Sound effect
garavel-4
Sound effect
garavel-5
Sound effect
death-1
Sound effect
death-2
Sound effect
opening-sound
Sound effect
opening-music
Music
game-theme-song-1
Music
game-theme-song-2
Music
game-theme-song-3
Music
game-theme-song-4
Music