User prompt
there is a problem with the platformsPassed calculation, it shows a much higher value than expected, it should just show how many platforms were passed by the player no matter if they were bounced on or not, as soon as player's head reaches the min y of a platform that platform is passed. this should be the logic
User prompt
place it on top of the multiplier in the animation don't let them overlap
User prompt
show + platformspassed and multiplier in multiplier animation
User prompt
don't multiply the current score by the multiplier, multiply platform number by the multiplier and add to the current score
User prompt
don't multiply the current score by the multiplier, multiply floor number by the multiplier and add to the current score
User prompt
let the multiplier animation leave the screen from the to, right now it leaves from right
User prompt
Reduce alpha to make cloud more transparent
User prompt
I see that clouds always spawn from the same x where they should be spawning from random x positions. Also, when the player moves so fast in the frame after a double jump, for example all the clothes spawn at the same time which makes an area full of clothes and the rest with none. Please fix this.
User prompt
I see that clouds only spawn on the left side of the game area. I want them to be spawned from anywhere.
User prompt
Opaque areas should act like borders for clouds as well just like they do for the player and the platforms use a very similar logic please
User prompt
Yuvarlaklıkları azaltın, biraz daha transparent yapın ve platformlar ve oyuncuların yuvarlaklıkları opak yuvarlaklıklara ulaşamayacak.
User prompt
Make cloud spawn much more intelligent and also clamp them to the game area.
User prompt
Tutoriyalardan 5B'yi çıkart.
User prompt
I meant remove 5B from the tutorial not 5A
User prompt
Tutorielden 5a'yı çıkart.
User prompt
The last bullet point you added should be in two lines.
User prompt
add another bullet point to the tutorial and explain the combo logic briefly of course like the other points
User prompt
fibonacci addtiion logic should start from the second theme in the first theme player should get the base values
User prompt
initial values for coins should be 1 dollars should be 2 diamond should be 3 and stars should be 5, they should then increase each theme with the Fibonacci, in the second theme values should be 2,3,4,6 respectively
User prompt
addition should be with the fibonacci logic
User prompt
don't use coin value multiplier just add numbers to the values addition not multiplication
User prompt
collectible values get big so quick they should only increase +1,+1,+2,+3 each round according to the Fibonacci sequence
User prompt
show the multipliers as integers not fractions
User prompt
make first combo multiplier 2x then increment by 1 every 2 combos and limit it to 5
User prompt
double values of coins each round
/**** * 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, using cloud's actual width var cloudWidth = 0; if (self.children && self.children[0] && typeof self.children[0].width === "number") { cloudWidth = self.children[0].width; } else if (typeof cloudGfx !== "undefined" && cloudGfx.width) { cloudWidth = cloudGfx.width; } else { cloudWidth = 300; // fallback default } var halfCloudW = cloudWidth / 2; var minCloudX = BORDER_LEFT_X + halfCloudW; var maxCloudX = BORDER_RIGHT_X - halfCloudW; // 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 coin, dollar, diamond) self.coinType = 1; self.value = COLLECTIBLE_BASE_VALUES.coin; self.assetId = 'chipCoin1'; // Set asset and value based on type if (typeof arguments[0] === "number") { if (arguments[0] === 1) { self.coinType = 1; self.value = COLLECTIBLE_BASE_VALUES.coin; self.assetId = 'chipCoin1'; } else if (arguments[0] === 2) { self.coinType = 2; self.value = COLLECTIBLE_BASE_VALUES.dollar; self.assetId = 'chipCoin2'; } else if (arguments[0] === 3) { self.coinType = 3; self.value = COLLECTIBLE_BASE_VALUES.diamond; 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 () { // Show value as base + Fibonacci addition for current theme var coinThemeIdx = typeof getThemeIndex === "function" ? getThemeIndex(typeof platformsPassed !== "undefined" ? platformsPassed : 0) : 0; var displayValue = getCollectibleValue(self.coinType === 1 ? "coin" : self.coinType === 2 ? "dollar" : "diamond", coinThemeIdx); var txt = new Text2('+' + displayValue, { size: 90, // Larger for more impact fill: self.coinType === 1 ? "#ffe066" : self.coinType === 2 ? "#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 = COLLECTIBLE_BASE_VALUES.star; // Stars are worth +5 initially 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 () { // Show value as base + Fibonacci addition for current theme var starThemeIdx = typeof getThemeIndex === "function" ? getThemeIndex(typeof platformsPassed !== "undefined" ? platformsPassed : 0) : 0; var displayValue = getCollectibleValue("star", starThemeIdx); 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 ****/ // Additional chip assets for more levels // --- Icy Tower Constants --- // Cloud shape assets for customizable cloud looks // Collectible star asset (yellow star shape) 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 / 15); var BORDER_RIGHT_X = GAME_W - Math.floor(GAME_W / 15); 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; // --- Fibonacci addition logic for coin, dollar, diamond, and star values per theme --- var fibonacciAdditions = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]; // enough for all themes // Initial values for collectibles var COLLECTIBLE_BASE_VALUES = { coin: 1, dollar: 2, diamond: 3, star: 5 }; // Returns the Fibonacci addition for a given theme index (0-based) function getFibonacciAdditionForTheme(themeIdx) { if (themeIdx < 0) return 1; if (themeIdx >= fibonacciAdditions.length) return fibonacciAdditions[fibonacciAdditions.length - 1]; return fibonacciAdditions[themeIdx]; } // Returns the collectible value for a given type and theme index function getCollectibleValue(type, themeIdx) { var base = COLLECTIBLE_BASE_VALUES[type] || 1; // For the first theme (themeIdx === 0), use only the base value (no Fibonacci addition) if (themeIdx === 0) { return base; } // For second theme and beyond, add Fibonacci addition var fibo = getFibonacciAdditionForTheme(themeIdx); return base + fibo; } var coinValueMultiplier = 1; // Kept for compatibility, but always 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; // Determine level by number of levels passed, not by score var level = Math.floor((typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed) / 50); if (level < 0) level = 0; if (level >= LEVEL_THEMES.length) level = LEVEL_THEMES.length - 1; // 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; var platformIdx = typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed; if ((platformIdx + 1) % 50 === 0) { // Every 50th platform: make a wide platform spanning the game area (between borders) w = BORDER_RIGHT_X - BORDER_LEFT_X; } else if (typeof width === "number") { w = width; } else { // Add more variety: sometimes make platforms much narrower or wider var variety = Math.random(); // Only allow cracking platforms after level 3 (platform 101+) var allowCracking = platNum > 100; 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); } // If cracking platform logic exists elsewhere, ensure it is only enabled if allowCracking is true. } // Determine theme based on platform index counter for new platforms // Ensure each theme has exactly 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; } // Cap platforms per theme to 50 var themePlatformNumber = platformIdx % 50 + 1; if (themePlatformNumber > 50) { return null; } 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 }); // Clamp platform X so no platform pixels touch the opaque sides of the walls var platformHalfWidth = w / 2; var minPlatX = BORDER_LEFT_X + platformHalfWidth; var maxPlatX = BORDER_RIGHT_X - platformHalfWidth; plat.x = Math.max(minPlatX, Math.min(x, maxPlatX)); 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 --- // From level 2 (platforms 51+) onwards, start moving platforms and increase their number as levels increment var movingPlatformLevel = 2; // Start moving platforms from level 2 (platform 51+) var currentLevel = Math.floor((typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed) / 50); var movingChance = 0.15; // Default for level 1 if (currentLevel >= movingPlatformLevel - 1) { // Increase moving platform chance by 7% per level after level 2, up to 70% movingChance = Math.min(0.15 + 0.07 * (currentLevel - (movingPlatformLevel - 1)), 0.7); } // Only allow moving platforms if platNum > 50 (i.e., after the first 50 platforms) if (platNum > 50 && Math.random() < movingChance) { plat._isMoving = true; plat._moveDir = Math.random() < 0.5 ? -1 : 1; // --- Random velocity logic with level scaling --- // At level 2, keep speeds slow and tight // As level increases, allow higher max speed, but cap so it never gets crazy // Level 2: speed 1.2-2.2, Level 3: 1.5-2.7, Level 4: 1.8-3.2, ... Level 6+: 2.2-3.8, never above 4.2 var minSpeed = 1.2 + Math.min(currentLevel, 6) * 0.3; var maxSpeed = 2.2 + Math.min(currentLevel, 6) * 0.4; if (currentLevel < 2) { minSpeed = 1.2; maxSpeed = 2.2; } if (currentLevel > 6) { minSpeed = 2.2; maxSpeed = 4.2; } plat._moveSpeed = minSpeed + Math.random() * (maxSpeed - minSpeed); // --- Random movement range logic with level scaling --- // At level 2, keep range small and tight // As level increases, allow higher max range, but cap so it never gets crazy // Level 2: range 120-180, Level 3: 150-240, Level 4: 180-320, ... Level 6+: 220-420, never above 600 var minRange = 120 + Math.min(currentLevel, 6) * 30; var maxRange = 180 + Math.min(currentLevel, 6) * 40; if (currentLevel < 2) { minRange = 120; maxRange = 180; } if (currentLevel > 6) { minRange = 220; maxRange = 420; } plat._moveRange = minRange + Math.random() * (maxRange - minRange); plat._moveRange = Math.min(plat._moveRange, 600); plat._moveOriginX = plat.x; plat.update = function () { // Move back and forth horizontally var platformHalfWidth = plat.width / 2; var minWallX = BORDER_LEFT_X + platformHalfWidth; var maxWallX = BORDER_RIGHT_X - platformHalfWidth; var moveRange = plat._moveRange; // Clamp moveOriginX so the full range stays within the allowed area if (plat._moveOriginX - moveRange < minWallX) { plat._moveOriginX = minWallX + moveRange; } if (plat._moveOriginX + moveRange > maxWallX) { plat._moveOriginX = maxWallX - moveRange; } var newX = plat._moveOriginX + Math.sin(Date.now() / (420 - plat._moveSpeed * 60)) * moveRange * plat._moveDir; // Clamp newX so no platform pixels touch the opaque wall plat.x = Math.max(minWallX, Math.min(newX, maxWallX)); }; } // --- 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 show popup animation if this is a 50th platform (e.g. 50, 100, 150, ...) if (platformNumber % 50 === 0) { // Score-style popup animation at the platform's position var popupValue = platformNumber + ""; var popupTxt = new Text2(popupValue, { size: 120, fill: "#fff", font: "Impact" }); popupTxt.anchor.set(0.5, 0.5); popupTxt.x = plat.x; popupTxt.y = plat.y - PLATFORM_H / 2 - 60; popupTxt.scale.set(1.8, 1.8); // Start big popupTxt.alpha = 0.0; // Start invisible game.addChild(popupTxt); // Animate popup: fade in, bounce, then float up and fade out tween(popupTxt, { alpha: 1, scaleX: 1.2, scaleY: 1.2 }, { duration: 180, onFinish: function onFinish() { tween(popupTxt, { y: popupTxt.y - 160, scaleX: 0.7, scaleY: 0.7, alpha: 0 }, { duration: 900, onFinish: function onFinish() { popupTxt.destroy(); } }); } }); } } // --- 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 // Clamp so no platform pixels touch the opaque sides of the walls var platformHalfWidth = width / 2; var minWallX = BORDER_LEFT_X + platformHalfWidth; var maxWallX = BORDER_RIGHT_X - platformHalfWidth; // Make platform spread more random: allow full random X in safe area, not just near previous platform var minX = minWallX; var maxX = maxWallX; 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 }); // Clamp platform0.x so no platform pixels touch the opaque wall sides var platform0HalfWidth = platform0W * platform0Scale / 2; var minWallX0 = BORDER_LEFT_X + platform0HalfWidth; var maxWallX0 = BORDER_RIGHT_X - platform0HalfWidth; platform0.x = Math.max(minWallX0, Math.min(GAME_W / 2, maxWallX0)); 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 // Determine level by number of levels passed, not by score var level = Math.floor((typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed) / 50); if (level < 0) level = 0; if (level >= LEVEL_THEMES.length) level = LEVEL_THEMES.length - 1; 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 / 15); var rightW = Math.floor(bgWidth / 15); 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(); // --- Intelligent spawn: --- // Distribute vertically in the upper 2/3 of the screen, but stagger so clouds are not bunched var verticalBand = GAME_H * 0.66 / NUM_CLOUDS; cloud.y = verticalBand * i + Math.random() * verticalBand * 0.7; // Clamp cloud X to only spawn in the transparent area, and stagger horizontally // Get cloud width after scaling var assetId = cloud.children && cloud.children[0] && cloud.children[0].assetId ? cloud.children[0].assetId : 'cloudBlue'; var assetInfo = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); var scale = cloud.children && cloud.children[0] && cloud.children[0].scale && cloud.children[0].scale.y ? cloud.children[0].scale.y : 1; var cloudW = assetInfo.width * scale; var minCloudX = BORDER_LEFT_X + cloudW / 2; var maxCloudX = BORDER_RIGHT_X - cloudW / 2; // Stagger clouds horizontally, evenly spaced, but with some random offset var horizontalBand = (maxCloudX - minCloudX) / NUM_CLOUDS; cloud.x = minCloudX + horizontalBand * i + Math.random() * horizontalBand * 0.7; // Clamp to area if (cloud.x < minCloudX) cloud.x = minCloudX; if (cloud.x > maxCloudX) cloud.x = maxCloudX; 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 comboColor = 0xFF3B30; var prevComboCount = typeof comboCount !== "undefined" ? comboCount : 0; if (typeof comboCount === "undefined") comboCount = 0; if (comboActive && comboLastWall === "right" && comboPlatformsLanded === 1) { // Continue combo: increase comboCount comboCount += 1; showCombo = true; } else { // Start new combo comboCount = 1; showCombo = true; } comboActive = true; comboLastWall = "left"; comboPlatformsLanded = 0; comboLastPlatform = null; comboJustDoubleJumped = true; comboLastScore = platformsPassed; // New combo multiplier logic: starts at 2x, increments by 1 every 2 combos, capped at 5x var comboMultiplier = 2 + Math.floor((comboCount - 1) / 2); // 2, 2, 3, 3, 4, 4, 5, 5, ... if (comboMultiplier > 5) comboMultiplier = 5; // Cap at 5x var comboBonus = Math.floor(platformsPassed * (comboMultiplier - 1)); if (comboBonus < 1) comboBonus = 1; newScore = platformsPassed + comboBonus; 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 combo multiplier popup (only xN) flying to the score UI and settling in --- var xNColor = 0xFF3B30; 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; // Only show the multiplier as integer (e.g. 'x2', 'x3', etc.) var comboMultiplierText = "x" + comboMultiplier.toFixed(0); var xNTxt = new Text2(comboMultiplierText, { size: fontSize, fill: xNColor, font: "Impact" }); xNTxt.anchor.set(0.5, 0.5); xNTxt.scale.set(scaleStart, scaleStart); xNTxt.alpha = 0.0; xNTxt.x = 0; xNTxt.y = 0; popupGroup.addChild(xNTxt); popupGroup.x = baseX; popupGroup.y = baseY; game.addChild(popupGroup); // Animate pop in, bounce, then fly to score UI and fade out tween(xNTxt, { alpha: 1, scaleX: scaleBounce, scaleY: scaleBounce }, { duration: 180, onFinish: function onFinish() { // Animate xNTxt flying to the score UI and settling in var globalScorePos = { x: 0, y: 0 }; if (scoreTxt.parent && typeof scoreTxt.parent.toGlobal === "function") { globalScorePos = scoreTxt.parent.toGlobal(scoreTxt.position); } var localScorePos = game.toLocal(globalScorePos); tween(xNTxt, { x: localScorePos.x + scoreTxt.width / 2, y: localScorePos.y, scaleX: scaleEnd, scaleY: scaleEnd, alpha: 0 }, { duration: 900, easing: tween.cubicInOut, onFinish: function onFinish() { xNTxt.destroy(); } }); } }); // 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 144-180 animated stars from player after double jump, with more vibrant color cycling and rainbow effect (left wall) var numEmitStars = 144 + Math.floor(Math.random() * 37); // 144-180 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; // More vibrant color cycle for stars, now including purple, pink, and yellow var colorCycle = [0xfff7b2, // light yellow 0xffe066, // yellow 0xffb347, // orange 0xff3b30, // red 0x4cd964, // green 0x5ac8fa, // blue 0x007aff, // deep blue 0x5856d6, // purple 0xffcc00, // bright yellow 0xff00ff, // magenta (pink) 0xff6fff, // light pink 0xfff700, // yellow 0xff0099, // hot pink 0xcc33ff, // purple 0xffea00, // yellow 0x00fff7, // cyan 0x00ff00, // green 0x00ffcc, // teal 0xff1744, // pink-red 0x00e676, // green 0xe040fb, // purple (added) 0xf06292, // pink (added) 0xffeb3b, // yellow (added) 0xba68c8, // purple (added) 0xffc107, // yellow (added) 0xec407a, // pink (added) 0xd500f9 // purple (added) ]; 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 4 frames for extra flashiness if (star.children && star.children[0]) { var cycleStep = Math.floor(star._life / 4); var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length]; star.children[0].tint = nextColor; // Fade out star.children[0].alpha = Math.max(0, 0.98 * (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 comboColor = 0xFF3B30; var prevComboCount = typeof comboCount !== "undefined" ? comboCount : 0; if (typeof comboCount === "undefined") comboCount = 0; if (comboActive && comboLastWall === "left" && comboPlatformsLanded === 1) { // Continue combo: increase comboCount comboCount += 1; showCombo = true; } else { // Start new combo comboCount = 1; showCombo = true; } comboActive = true; comboLastWall = "right"; comboPlatformsLanded = 0; comboLastPlatform = null; comboJustDoubleJumped = true; comboLastScore = platformsPassed; // New combo multiplier logic: starts at 2x, increments by 1 every 2 combos, capped at 5x var comboMultiplier = 2 + Math.floor((comboCount - 1) / 2); // 2, 2, 3, 3, 4, 4, 5, 5, ... if (comboMultiplier > 5) comboMultiplier = 5; // Cap at 5x var comboBonus = Math.floor(platformsPassed * (comboMultiplier - 1)); if (comboBonus < 1) comboBonus = 1; newScore = platformsPassed + comboBonus; 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 combo multiplier popup (only xN) flying to the score UI and settling in --- var xNColor = 0xFF3B30; 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; // Only show the multiplier as integer (e.g. 'x2', 'x3', etc.) var comboMultiplierText = "x" + comboMultiplier.toFixed(0); var xNTxt = new Text2(comboMultiplierText, { size: fontSize, fill: xNColor, font: "Impact" }); xNTxt.anchor.set(0.5, 0.5); xNTxt.scale.set(scaleStart, scaleStart); xNTxt.alpha = 0.0; xNTxt.x = 0; xNTxt.y = 0; popupGroup.addChild(xNTxt); popupGroup.x = baseX; popupGroup.y = baseY; game.addChild(popupGroup); // Animate pop in, bounce, then fly to score UI and fade out tween(xNTxt, { alpha: 1, scaleX: scaleBounce, scaleY: scaleBounce }, { duration: 180, onFinish: function onFinish() { // Animate xNTxt flying to the score UI and settling in var globalScorePos = { x: 0, y: 0 }; if (scoreTxt.parent && typeof scoreTxt.parent.toGlobal === "function") { globalScorePos = scoreTxt.parent.toGlobal(scoreTxt.position); } var localScorePos = game.toLocal(globalScorePos); tween(xNTxt, { x: localScorePos.x + scoreTxt.width / 2, y: localScorePos.y, scaleX: scaleEnd, scaleY: scaleEnd, alpha: 0 }, { duration: 900, easing: tween.cubicInOut, onFinish: function onFinish() { xNTxt.destroy(); } }); } }); // 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, now including purple, pink, and yellow var colorCycle = [0xfff7b2, // light yellow 0xffe066, // yellow 0xffb347, // orange 0xff3b30, // red 0x4cd964, // green 0x5ac8fa, // blue 0x007aff, // deep blue 0x5856d6, // purple 0xffcc00, // bright yellow 0xff00ff, // magenta (pink) 0xff6fff, // light pink 0xfff700, // yellow 0xff0099, // hot pink 0xcc33ff, // purple 0xffea00, // yellow 0x00fff7, // cyan 0x00ff00, // green 0x00ffcc, // teal 0xff1744, // pink-red 0x00e676, // green 0xe040fb, // purple (added) 0xf06292, // pink (added) 0xffeb3b, // yellow (added) 0xba68c8, // purple (added) 0xffc107, // yellow (added) 0xec407a, // pink (added) 0xd500f9 // purple (added) ]; 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; } } } // --- Coin value addition logic: always 1, no multiplier or combo --- // No theme or combo multiplier, always use base coin value coinValueMultiplier = 1; // 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 // Do not stop emitting stars on landing; only stop when combo ends // --- 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; isEmittingStars = false; // Stop emitting stars when combo ends } } // 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(); // Determine theme index for coin addition var coinThemeIdx = getThemeIndex(platformsPassed); var coinTypeName = coin.coinType === 1 ? "coin" : coin.coinType === 2 ? "dollar" : "diamond"; var coinCollectValue = getCollectibleValue(coinTypeName, coinThemeIdx); coinsCollected += coinCollectValue; 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(); // Determine theme index for star addition var starThemeIdx = getThemeIndex(platformsPassed); var starCollectValue = getCollectibleValue("star", starThemeIdx); coinsCollected += starCollectValue; 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, intelligently spaced and clamped var newCloud = new Cloud(); // Distribute new cloud's X so clouds are not bunched, and clamp to transparent area var assetId = newCloud.children && newCloud.children[0] && newCloud.children[0].assetId ? newCloud.children[0].assetId : 'cloudBlue'; var assetInfo = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); var scale = newCloud.children && newCloud.children[0] && newCloud.children[0].scale && newCloud.children[0].scale.y ? newCloud.children[0].scale.y : 1; var cloudW = assetInfo.width * scale; var minCloudX = BORDER_LEFT_X + cloudW / 2; var maxCloudX = BORDER_RIGHT_X - cloudW / 2; // Try to find a horizontal gap between existing clouds var bestX = minCloudX + Math.random() * (maxCloudX - minCloudX); var maxDist = 0; for (var c = 0; c < clouds.length; ++c) { var cx = clouds[c].x; var dist = Math.abs(cx - bestX); if (dist > maxDist) { maxDist = dist; } } // If there are enough clouds, try to avoid overlap by offsetting if (clouds.length > 1) { var bestGap = 0; var bestGapX = bestX; for (var c = 0; c < clouds.length; ++c) { var cx = clouds[c].x; var nextCx = clouds[(c + 1) % clouds.length].x; var gap = Math.abs(nextCx - cx); if (gap > bestGap) { bestGap = gap; bestGapX = (cx + nextCx) / 2; } } bestX = bestGapX; } // Clamp to area if (bestX < minCloudX) bestX = minCloudX; if (bestX > maxCloudX) bestX = maxCloudX; newCloud.x = bestX; // Place new cloud just above the visible area, with some random offset 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 // Determine level by number of levels passed, not by score var level = Math.floor((typeof createPlatform.platformIndex === "number" ? createPlatform.platformIndex - 1 : platformsPassed) / 50); if (level < 0) level = 0; if (level >= LEVEL_THEMES.length) level = LEVEL_THEMES.length - 1; 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) { // No coin value multiplier update on theme change, always use addition coinValueMultiplier = 1; // 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 / 15); var rightW = Math.floor(bgWidth / 15); 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; // Emit 2 stars per interval for extra flashiness for (var flashStarIdx = 0; flashStarIdx < 2; ++flashStarIdx) { 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; // More vibrant color cycle for stars, now including purple, pink, and yellow var colorCycle = [0xfff7b2, // light yellow 0xffe066, // yellow 0xffb347, // orange 0xff3b30, // red 0x4cd964, // green 0x5ac8fa, // blue 0x007aff, // deep blue 0x5856d6, // purple 0xffcc00, // bright yellow 0xff00ff, // magenta (pink) 0xff6fff, // light pink 0xfff700, // yellow 0xff0099, // hot pink 0xcc33ff, // purple 0xffea00, // yellow 0x00fff7, // cyan 0x00ff00, // green 0x00ffcc, // teal 0xff1744, // pink-red 0x00e676, // green 0xe040fb, // purple (added) 0xf06292, // pink (added) 0xffeb3b, // yellow (added) 0xba68c8, // purple (added) 0xffc107, // yellow (added) 0xec407a, // pink (added) 0xd500f9 // purple (added) ]; 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 4 frames for extra flashiness if (star.children && star.children[0]) { var cycleStep = Math.floor(star._life / 4); var nextColor = colorCycle[(colorIdx + cycleStep) % colorCycle.length]; star.children[0].tint = nextColor; // Fade out star.children[0].alpha = Math.max(0, 0.98 * (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 = 1700; var tutorialBgHeight = 7000; var tutorialYOffset = 510; // Amount of black space above the title (moved further up by 10px) var overlay = LK.getAsset('uiTopBgRect', { anchorX: 0.5, anchorY: 0, x: 120, y: 620 - tutorialYOffset, scaleX: tutorialBgWidth / 100, scaleY: tutorialBgHeight / 110, alpha: 1 }); 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 + tutorialYOffset + 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); var step5a = new Text2("• Bounce left/right walls in a row for a COMBO!", { size: 70, fill: 0xFFD600, font: "Impact" }); step5a.anchor.set(0, 0.5); step5a.x = overlay.x - tutorialBgWidth / 2 + 60; step5a.y = step4.y + stepSpacing; tutorialContainer.addChild(step5a); // "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
@@ -46,11 +46,20 @@
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;
+ // Clamp clouds to transparent area and bounce on hitting opaque bg borders, using cloud's actual width
+ var cloudWidth = 0;
+ if (self.children && self.children[0] && typeof self.children[0].width === "number") {
+ cloudWidth = self.children[0].width;
+ } else if (typeof cloudGfx !== "undefined" && cloudGfx.width) {
+ cloudWidth = cloudGfx.width;
+ } else {
+ cloudWidth = 300; // fallback default
+ }
+ var halfCloudW = cloudWidth / 2;
+ var minCloudX = BORDER_LEFT_X + halfCloudW;
+ var maxCloudX = BORDER_RIGHT_X - halfCloudW;
// Bounce off left opaque border
if (self.lastX > minCloudX && self.x <= minCloudX) {
self.x = minCloudX;
self.driftX = Math.abs(self.driftX); // move right
@@ -348,12 +357,12 @@
/****
* Game Code
****/
-// Collectible star asset (yellow star shape)
-// Cloud shape assets for customizable cloud looks
-// --- Icy Tower Constants ---
// Additional chip assets for more levels
+// --- Icy Tower Constants ---
+// Cloud shape assets for customizable cloud looks
+// Collectible star asset (yellow star shape)
var highScore = storage.highScore || 0;
var GAME_W = 2048;
var GAME_H = 2732;
var PLATFORM_W = 400;
@@ -667,12 +676,9 @@
anchorX: 0.5,
anchorY: 0.5,
scaleX: scale,
scaleY: scale,
- color: platformColor,
- alpha: 0.78,
- // More transparent platforms
- roundness: 0.18 // Less round, not fully round
+ color: platformColor
});
// Clamp platform X so no platform pixels touch the opaque sides of the walls
var platformHalfWidth = w / 2;
var minPlatX = BORDER_LEFT_X + platformHalfWidth;
@@ -913,12 +919,9 @@
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,
- alpha: 0.82,
- // More transparent player
- roundness: 0.22 // Less round, not fully round
+ scaleY: PLAYER_H / 640
});
player.x = GAME_W / 2;
player.y = GAME_H - 400;
player.width = PLAYER_W;
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