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
User prompt
fix newScoreAfterDoubleJump popup flying to the score UI and settling in. it now goes right and exits the screen though it should move up and settle in the position of scoreTxt.x
User prompt
Make score value in ui flash colors, change slower and with a better animation, maybe the newScoreAfterDoubleJump animation may fly there and settle or sth ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
we need to implement a new logic. this will change a lot in the game. After making a double jump if player lands on one platform only and after bouncing one time only if he makes a double jump from the opposite wall then his score should be quadrupled and if he lands on one platform only again and makes another jump from the opposite wall his score should be 16x and so on. This is a combo logic which allows player to increase the score multiplier as he makes a combo in the game
User prompt
didn't work it shows html code in game like
User prompt
let newScoreAfterDoubleJump, currentScore and x2 all be in different vibrant colors
User prompt
Instead of writing SCORE DOUBLED write currentScore x2 = newScoreAfterDoubleJump
User prompt
also after the double jump let the player know that s/he has doubled his score in a more fancy evident and fun way
User prompt
make values of coins collected more evident and visible to the player with a better animation
Code edit (1 edits merged)
Please save this source code
User prompt
move Score: text and its value 70px left, don't touch Coins: or the UI bg
User prompt
Clamp player and clouds to transparent area, bounce on hitting opaque bg borders Clamp clouds to transparent area, bounce on hitting opaque bg borders
User prompt
player should make a double jump as soon as the rightmost pixel of it touches the leftmost pixel of the opaque bg on the right side wall double jump. for the left side double jump, player should make when the leftmost pixel of it touches the rightmost pixel of opaque bg on the left side wall
User prompt
make left side double jump animation same with right side (currently more stars are emitted after the right side double jump)
User prompt
apply the same logic for the right wall double jump as well and emit more stars
User prompt
Let the character continue emitting stars after a double jump until he lands on a platform
User prompt
let the character to continue emitting stars after double jump until he lands on a platform
User prompt
whatever you just changed did not make the character jump up to UI scoreboard, he still can't pass half screen during the double jump animation, but it caused the character to fly like 150 platforms at once limit it to like 15
User prompt
character should jump up until the UI scoreboard during double jump
/**** * 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, y: localScorePos.y, 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 // Move up and settle at the center of scoreTxt (UI), not to the right 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); } // --- Tutorial Overlay --- // Show a tutorial overlay before character selection function showTutorialOverlay(onFinish) { // Remove any previous overlay if (typeof showTutorialOverlay._container !== "undefined" && showTutorialOverlay._container) { showTutorialOverlay._container.destroy(); showTutorialOverlay._container = null; } var tutorialContainer = new Container(); showTutorialOverlay._container = tutorialContainer; // Use the same UI background rectangle as the Coins: UI, but sized to fit the tutorial content var tutorialBgWidth = 1200; var tutorialBgHeight = 700; var overlay = LK.getAsset('uiTopBgRect', { anchorX: 0.5, anchorY: 0, x: 120, y: 120, scaleX: tutorialBgWidth / 100, scaleY: tutorialBgHeight / 110, alpha: 0.92 }); tutorialContainer.addChild(overlay); // Tutorial Title var title = new Text2("How to Play", { size: 110, fill: "#fff", font: "Impact" }); title.anchor.set(0, 0.5); title.x = overlay.x - tutorialBgWidth / 2 + 60; title.y = overlay.y + 60; tutorialContainer.addChild(title); // Tutorial Steps var stepY = title.y + 110; var stepSpacing = 90; var step1 = new Text2("• Tap left/right to jump in that direction", { size: 70, fill: 0xFFE066, font: "Impact" }); step1.anchor.set(0, 0.5); step1.x = overlay.x - tutorialBgWidth / 2 + 60; step1.y = stepY; tutorialContainer.addChild(step1); var step2 = new Text2("• Bounce off the walls for a DOUBLE JUMP!", { size: 70, fill: 0xA78BFA, font: "Impact" }); step2.anchor.set(0, 0.5); step2.x = overlay.x - tutorialBgWidth / 2 + 60; step2.y = step1.y + stepSpacing; tutorialContainer.addChild(step2); var step3 = new Text2("• Collect coins and stars for points", { size: 70, fill: 0x38BDF8, font: "Impact" }); step3.anchor.set(0, 0.5); step3.x = overlay.x - tutorialBgWidth / 2 + 60; step3.y = step2.y + stepSpacing; tutorialContainer.addChild(step3); var step4 = new Text2("• Land on platforms to keep going higher!", { size: 70, fill: "#fff", font: "Impact" }); step4.anchor.set(0, 0.5); step4.x = overlay.x - tutorialBgWidth / 2 + 60; step4.y = step3.y + stepSpacing; tutorialContainer.addChild(step4); // "Tap to continue" prompt var continueTxt = new Text2("Tap anywhere to continue", { size: 60, fill: "#fff", font: "Impact" }); continueTxt.anchor.set(1, 1); continueTxt.x = overlay.x + tutorialBgWidth / 2 - 60; continueTxt.y = overlay.y + tutorialBgHeight - 40; continueTxt.alpha = 0.0; tutorialContainer.addChild(continueTxt); // Fade in the continue text tween(continueTxt, { alpha: 1 }, { duration: 900 }); // Add to GUI overlay (top center) LK.gui.top.addChild(tutorialContainer); // Dismiss on any tap/click tutorialContainer.down = function (x, y, obj) { if (showTutorialOverlay._container) { showTutorialOverlay._container.destroy(); showTutorialOverlay._container = null; } if (typeof onFinish === "function") { onFinish(); } }; } // Show tutorial first, then character selection showTutorialOverlay(function () { // 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 }); }); });
===================================================================
--- original.js
+++ change.js
@@ -1399,9 +1399,11 @@
// Destroy the group container after all animations
tween(popupGroup, {}, {
duration: 1400,
onFinish: function onFinish() {
- if (popupGroup && typeof popupGroup.destroy === "function") popupGroup.destroy();
+ if (popupGroup && typeof popupGroup.destroy === "function") {
+ popupGroup.destroy();
+ }
}
});
// Start continuous star emission after double jump
isEmittingStars = true;
@@ -1740,9 +1742,11 @@
// Destroy the group container after all animations
tween(popupGroup, {}, {
duration: 1400,
onFinish: function onFinish() {
- if (popupGroup && typeof popupGroup.destroy === "function") popupGroup.destroy();
+ if (popupGroup && typeof popupGroup.destroy === "function") {
+ popupGroup.destroy();
+ }
}
});
// Start continuous star emission after double jump (right wall)
isEmittingStars = true;
@@ -2404,9 +2408,9 @@
var tutorialBgHeight = 700;
var overlay = LK.getAsset('uiTopBgRect', {
anchorX: 0.5,
anchorY: 0,
- x: GAME_W / 2,
+ x: 120,
y: 120,
scaleX: tutorialBgWidth / 100,
scaleY: tutorialBgHeight / 110,
alpha: 0.92
@@ -2485,9 +2489,11 @@
if (showTutorialOverlay._container) {
showTutorialOverlay._container.destroy();
showTutorialOverlay._container = null;
}
- if (typeof onFinish === "function") onFinish();
+ if (typeof onFinish === "function") {
+ onFinish();
+ }
};
}
// Show tutorial first, then character selection
showTutorialOverlay(function () {
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