User prompt
start the multiplier from 1.5x and increase 1.5x after every combo
User prompt
change the score logic. current score is the amount of seconds player is in the game and should increase in accordance, multiplier logic should stay the same
User prompt
values of collectibles should increase every level by 1.2x rounded up
User prompt
use ceiling function while rounding the coin values
User prompt
Start multiplying by 1.2 and increase it in every combo by 0.2 not every 2 combos
User prompt
increase the opaque walls' width by 100%
User prompt
add coin value incrementation logic back in compliance with the score, I mean I would expect both to increase with a similar pace, this might change for the playing style of course but we should set the most optimal setup to the player in an intelligent way so that both scores increase with a similar pace. also I don't want any fractional coin values coin values should be integers
User prompt
I didn't like the percentage logic let's change it back to multipliers but start from 1.2x and increase 0.2x every two combo for example
User prompt
Also doubling the score each time after a combo makes very high scores reachable so easy we should find a more intelligent scoring logic, both for coins and score
User prompt
simplify the score animation to show only the multiplier no the current score and the newscore
User prompt
It is far from pretty still covers the whole game area its width should be as much as the player width not more
User prompt
make the label prettier like it shouldn't cover the whole game area it should be a small part of the platform with a background so that it is readable. Use readable colors with a good font. make it a good one you got me
User prompt
on every 50th platform there should be an animation like newScoreAfterDoubleJump showing the platform number and this animation should stay with the platform until it disappears from the screen
User prompt
delete the labels on these and pop up an animation like the score animation while passing them showing the number that is currently on the label
User prompt
instead of every 50th platform show a new rectangle shaped object that have the same asset with the other platforms in the theme but as wide as the game area
User prompt
every 50th platform have a label, make these one guaranteed to be in the center of the game area and adjust their width to be guaranteed to be game area wide
User prompt
make every 50th platform position in the middle, not moving and have width from left opaque wall to the right opaque wall
User prompt
only green orange and red stars are emitted what happened to the colors I asked
User prompt
add purple pink and yellow stars as well to the star emission animation
User prompt
make color of stars more flashy make them more visible but not increase the size maybe the number
User prompt
resume star emission until the combo finishes
User prompt
they should have a crack animation and after player bounces on them 1 time they should disappear immediately
User prompt
they should appear in level 3 so after 100th platform
User prompt
I never encountered any cracking platform they should start appearing after level 3
User prompt
make the platform spread more random they're like a stairway right now spawn them at random horizontal positions
/**** * 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; // 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 isWidePlatform = (platNum + 1) % 50 === 0; // every 50th platform (platforms are 0-indexed in array, so +1) if (isWidePlatform) { // Make the platform as wide as the transparent area between the opaque walls 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; if (isWidePlatform) { // Center the platform exactly in the middle of the transparent area plat.x = (BORDER_LEFT_X + BORDER_RIGHT_X) / 2; } else { 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 (!isWidePlatform && 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 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 // 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 / 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 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 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, 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; } } } // 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(); 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 // 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) { // 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; // 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); // "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
@@ -584,9 +584,13 @@
// 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") {
+ var isWidePlatform = (platNum + 1) % 50 === 0; // every 50th platform (platforms are 0-indexed in array, so +1)
+ if (isWidePlatform) {
+ // Make the platform as wide as the transparent area between the opaque walls
+ 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();
@@ -640,9 +644,14 @@
// 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));
+ if (isWidePlatform) {
+ // Center the platform exactly in the middle of the transparent area
+ plat.x = (BORDER_LEFT_X + BORDER_RIGHT_X) / 2;
+ } else {
+ plat.x = Math.max(minPlatX, Math.min(x, maxPlatX));
+ }
plat.y = y;
plat.width = w;
plat.height = PLATFORM_H;
plat.PLATFORM_H = PLATFORM_H;
@@ -657,9 +666,9 @@
// 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) {
+ if (!isWidePlatform && 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
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