User prompt
chain must be move
User prompt
🧠 Path-Following Chain Behavior Ensure the chain of balls follows a clearly defined path that winds across the screen. The path should be a list of (x, y) points forming a spiral, zigzag or maze-like trajectory (e.g., a predefined array called track[]). Each ball in the chain must have a trackPosition value (e.g., 0 to 3000) which increments over time based on game speed. The ball's x and y coordinates must be updated every frame to match the corresponding position on the path: js Kopyala Düzenle ball.x = track[Math.floor(ball.trackPosition)].x; ball.y = track[Math.floor(ball.trackPosition)].y; The entire chain must appear to move smoothly as if traveling through a maze, heading toward a visible goal (hole). When the last ball reaches the end of the track, the player loses. The path can be visualized with faded "track segment" dots to debug or show progress. 🎮 Ekstra Tavsiye: Path Oluştururken Aşağıdaki gibi path oluşturabilirsin: js Kopyala Düzenle // Spiral track for (let i = 0; i < 1000; i++) { let angle = i * 0.05; let radius = 200 + i * 0.5; let x = centerX + Math.cos(angle) * radius; let y = centerY + Math.sin(angle) * radius; track.push({ x, y }); } Ya da karmaşık bir labirent için manuel [{x:..., y:...}, ...] dizisi oluşturabilirsin., ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
A spiral or curved path where a chain of colored balls moves continuously toward a hole. If the chain reaches the hole, the player loses. A rotatable shooter at the bottom center that fires balls toward the chain. The shooter shows a "current ball" inside the barrel, and a clearly separated "next ball preview" somewhere else on the screen (top-right corner recommended). Clicking/tapping fires the current ball toward the mouse/touch direction. Fired balls stick into the chain where they hit and trigger matches if 3+ same colors align. Matching balls disappear with an animation and sound, and the chain retracts slightly. Add power-up balls (5% chance): explosive, freeze, rapid-fire. 🎯 UI Clarity: Next ball should be visually separated and labeled. Current ball should have a glowing border or scale effect. Use distinct UI spacing to avoid confusion between the two. 🔫 Shooter Visuals: Add a visible cannon barrel (muzzle) that rotates toward the mouse. The barrel should be part of the shooter and stretch/scale when firing. 🐛 Bug Prevention: Ensure the ball chain actually moves. Fired balls must always insert into the chain properly. The shooter must not change ball colors on click. 🧪 Extras: Glowing effects on power-up balls. Subtle screen shake or flash on power activation. Score counter, level display, and lose/win screens. Fully responsive to touch and mouse. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Zuma Ball Chain Blaster
Initial prompt
📌 Objective: Develop a fully interactive, polished Zuma-style arcade game from the ground up. The gameplay, visuals, user interface, mechanics, and physics should mimic the classic Zuma Deluxe experience as closely as possible. The game must feel fluid, reactive, and visually engaging. 🎮 GAME TYPE Genre: Arcade / Puzzle View: 2D Top-down Platform: Mobile + Desktop browser compatible Resolution: 1080x1920 (portrait mode) Input: Mouse or Touch (tap to shoot) 🧠 CORE GAMEPLAY MECHANICS 1. Ball Chain (The Marble Train) A sequence of colored balls moves smoothly along a curved or spiral path (track). The ball chain continuously progresses toward a “hole” (goal). If any ball reaches the hole, the game is lost. The chain progresses at a constant speed (e.g., 2 pixels/frame), with possible variations as levels increase. 2. Ball Shooter A rotating shooter is placed at the bottom center of the screen. It can rotate 360° and always aims toward the player’s cursor or touch location. On click/tap, it fires the current ball in the aimed direction. The next ball to shoot is previewed in the UI (beside or above the shooter). If a power-up ball is active, it replaces the current color. 3. Ball Insertion & Matching When a fired ball hits the chain, it must be inserted between two balls. The inserted ball triggers a match if 3 or more consecutive balls of the same color are connected. Matching balls are destroyed, and the chain slides backward slightly to close the gap. If no match is made, the ball stays in place and the chain continues moving forward. 4. Ball Colors Use a set of 5 to 6 colors: Red, Blue, Green, Yellow, Purple, Orange. The game randomly generates ball colors for the chain and for shooting. Only colors present in the chain should be used for shooting. 💥 POWER-UP BALLS (5% CHANCE PER BALL) Explosive Ball (💣): Explodes on impact and destroys nearby 4–6 balls (regardless of color). Triggered on contact or when part of a match. Plays explosion animation and sound. Freeze Ball (❄️): Stops the entire chain movement for 3 seconds. Chain resumes after time expires. Applies icy tint to chain balls during freeze effect. Rapid Fire Ball (⚡): Activates rapid fire mode for 5 seconds. Cooldown between shots is reduced significantly. Applies glowing tint to the shooter during this time. 🖼️ VISUAL COMPONENTS & UI 1. Balls Shape: Circle (80px diameter) Style: Bright solid colors, slightly glossy, glowing border if it's a power-up Animation: Smooth scaling on impact, glow pulse for power-ups 2. Shooter Centered at bottom of screen Rotatable gun/turret with visible barrel Displays: Current ball (inside shooter) Next ball (as a floating preview nearby) 3. Path / Track Designed as a curved or spiral track Consists of coordinate points (x, y) that define the full path Visual “track markers” (e.g., small faded segments every 4–5 points) for clarity 4. Hole (Goal Point) Located at the end of the path Appears as a black circular “hole” that absorbs balls Losing animation when ball enters 5. User Interface (UI) Top-right: Score counter Top-left: Level indicator Bottom-right: Preview of next ball Optional pause and mute buttons ⚙️ PHYSICS & TIMING Ball speed in chain: 2 px/frame (can increase over time) Fired ball speed: 12 px/frame Collision detection threshold: ~40–45px (center-to-center) Ball spacing in chain: ~45px Chain update interval: every game frame Inserted ball spacing should maintain consistent alignment with the chain 🔁 GAME LOOP OVERVIEW The ball chain moves automatically along the track. The player rotates the shooter toward the target direction. Player clicks/taps to shoot. The shot ball moves forward and checks for collisions: If it hits the chain, it is inserted between two balls. If a group of 3+ same-color balls is formed, they disappear. If a match is made, score is awarded, and the chain pulls back slightly. If no match is made, the ball remains inserted, and the chain keeps moving. If the chain reaches the hole, the game ends. If the entire chain is cleared, the player wins and moves to the next level. ✅ WIN / LOSE CONDITIONS Win: All balls in the chain are cleared before reaching the hole. Lose: Any ball reaches the hole. 🎁 ADDITIONAL FEATURES (Optional, but recommended) Combo system (consecutive matches = bonus score) Level system with increasing difficulty: Faster chain speed Longer tracks More colors introduced Score-based power-up unlocks Visual and sound feedback for every match or power activation Game over & level complete screen with transitions Support for high scores or leaderboard 🚫 BUGS TO AVOID (Explicitly Clarify) Do not allow shooter to change the current ball color by clicking — no manual switching! Fired balls must always stick into the chain if collision occurs. Do not let balls pass through the chain or disappear. Make sure collision and insertion always happen smoothly and visibly. Ensure the shooter cannot fire during cooldown or rapid-fire cooldown gaps. ✨ POLISH REQUIREMENTS Smooth animations (tween scale, fade, glow) Sound effects: Shoot, match, explosion, freeze, rapid-fire Subtle screen shake or flash for power-ups Responsive and mobile-friendly touch support
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Ball = Container.expand(function (color) { var self = Container.call(this); // Safety check to prevent black balls if (color === 'black') { color = 'red'; // Default to red if black is passed } self.color = color; self.ballColors = ['red', 'blue', 'green', 'yellow', 'pink']; self.isSpecial = false; self.specialType = null; // 'explosive', 'freeze', 'rapid', 'white_freeze', 'black_destroyer' var ballGraphics = self.attachAsset('ball_' + color, { anchorX: 0.5, anchorY: 0.5 }); // Add continuous rotation animation to the ball var rotateSpeed = 0.02 + Math.random() * 0.03; // Random rotation speed between 0.02 and 0.05 var rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction var _rotateBall = function rotateBall() { if (ballGraphics && ballGraphics.parent) { ballGraphics.rotation += rotateSpeed * rotationDirection; LK.setTimeout(_rotateBall, 16); // 60fps rotation } }; // Start rotation animation _rotateBall(); // Check for special ball types based on color if (color === 'white') { self.isSpecial = true; self.specialType = 'white_freeze'; ballGraphics.alpha = 0.9; // Stop rotation for white balls ballGraphics.rotation = 0; var _whiteRotateStop2 = function _whiteRotateStop() { if (ballGraphics && ballGraphics.parent) { ballGraphics.rotation = 0; LK.setTimeout(_whiteRotateStop2, 16); } }; _whiteRotateStop2(); } else if (color === 'fire') { self.isSpecial = true; self.specialType = 'fire_blast'; ballGraphics.alpha = 0.9; // Apply lighter tint to make it look like a bright meteor ballGraphics.tint = 0xFFBB44; // Bright golden-orange like a meteor } else { // 5% chance for special balls on regular colors if (Math.random() < 0.05) { self.isSpecial = true; var specials = ['explosive', 'freeze', 'rapid']; self.specialType = specials[Math.floor(Math.random() * specials.length)]; ballGraphics.alpha = 0.9; // Add glow effect for special balls if (self.specialType === 'explosive') { ballGraphics.tint = 0xFF4500; } else if (self.specialType === 'freeze') { ballGraphics.tint = 0x00FFFF; } else if (self.specialType === 'rapid') { ballGraphics.tint = 0xFFFF00; } } } self.trackPosition = 0; self.trackX = 0; self.trackY = 0; self.isMoving = false; return self; }); var ChainBall = Ball.expand(function (color) { var self = Ball.call(this, color); self.chainIndex = 0; self.targetX = 0; self.targetY = 0; // Stop the rotation animation for chain balls var ballGraphics = self.children[0]; // Get the ball graphics if (ballGraphics) { ballGraphics.rotation = 0; // Reset rotation to 0 } self.update = function () { if (self.isMoving) { // Smooth movement towards target position var dx = self.targetX - self.x; var dy = self.targetY - self.y; self.x += dx * 0.3; self.y += dy * 0.3; if (Math.abs(dx) < 1 && Math.abs(dy) < 1) { self.isMoving = false; self.x = self.targetX; self.y = self.targetY; } } // Keep chain balls from rotating by maintaining rotation at 0 if (ballGraphics) { ballGraphics.rotation = 0; } // Ensure consistent size for all chain balls self.scaleX = 1.15; self.scaleY = 1.15; }; return self; }); var Shooter = Container.expand(function () { var self = Container.call(this); // Simple shooter image asset only var shooterGraphics = self.attachAsset('shooter', { anchorX: 0.5, anchorY: 0.5 }); self.angle = 0; self.currentBall = null; self.nextBall = null; self.rapidFire = false; self.rapidFireTimer = 0; self.shootCooldown = 0; self.loadBall = function () { if (self.currentBall) { self.currentBall.destroy(); } self.currentBall = self.nextBall; if (self.currentBall) { self.currentBall.x = 0; self.currentBall.y = -20; // Move ball closer to barrel self.currentBall.scaleX = 1.4375; // 43.75% larger (25% + 15%) self.currentBall.scaleY = 1.4375; // 43.75% larger (25% + 15%) self.addChild(self.currentBall); } // Generate next ball var colors = ['red', 'blue', 'green', 'yellow', 'pink']; var randomColor; // Check if it's time for a special ball (every 10 shots) if (shotCounter > 0 && shotCounter % 10 === 0) { // Randomly choose between white freeze ball or fire blast ball if (Math.random() < 0.5) { randomColor = 'white'; // White freeze ball } else { randomColor = 'fire'; // Fire blast ball } } else { randomColor = colors[Math.floor(Math.random() * colors.length)]; } // Safety check to ensure we never create black balls if (randomColor === 'black' || !randomColor) { randomColor = colors[Math.floor(Math.random() * colors.length)]; // Default to valid colors } self.nextBall = new Ball(randomColor); }; self.aimAt = function (x, y) { var dx = x - self.x; var dy = y - self.y; self.angle = Math.atan2(dy, dx); self.rotation = self.angle; }; self.shoot = function () { if (self.shootCooldown > 0 || !self.currentBall) return null; var ball = self.currentBall; self.removeChild(ball); // Maintain consistent ball scale when firing ball.scaleX = 1.15; ball.scaleY = 1.15; // Set ball velocity var speed = 8; ball.vx = Math.cos(self.angle) * speed; ball.vy = Math.sin(self.angle) * speed; ball.x = self.x; ball.y = self.y; // Add rocket trail effect to every shot ball ball.rocketTrail = []; ball.createRocketTrail = function () { // Create rocket trail effect behind the ball - different for white balls if (LK.ticks % 2 === 0) { // Create trail every 2 frames var trailEffect = LK.getAsset('fire_effect', { anchorX: 0.5, anchorY: 0.5 }); // Position trail behind the ball trailEffect.x = ball.x - ball.vx * 0.8; trailEffect.y = ball.y - ball.vy * 0.8; trailEffect.scaleX = 0.6; trailEffect.scaleY = 0.6; trailEffect.alpha = 0.8; // White balls get pure white trail, others get orange if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') { trailEffect.tint = 0xFFFFFF; // Pure white for white balls trailEffect.scaleX = 0.7; // Slightly larger trailEffect.scaleY = 0.7; trailEffect.alpha = 0.9; // Brighter } else { trailEffect.tint = 0xFF8844; // Orange rocket flame color for regular balls } // Add trail behind the ball in z-order var ballIndex = game.getChildIndex(ball); game.addChildAt(trailEffect, ballIndex); // Store trail reference ball.rocketTrail.push(trailEffect); // Animate trail - fade out and scale down tween(trailEffect, { scaleX: 0.2, scaleY: 0.2, alpha: 0 }, { duration: ball.color === 'white' ? 400 : 300, easing: tween.easeOut, onFinish: function onFinish() { if (trailEffect && trailEffect.parent) { trailEffect.destroy(); } // Remove from trail array var index = ball.rocketTrail.indexOf(trailEffect); if (index > -1) { ball.rocketTrail.splice(index, 1); } } }); } }; // Increment shot counter shotCounter++; self.loadBall(); // Create optimized fire effect only if under limit and not in rapid fire if (activeFireEffects.length < maxFireEffects && !self.rapidFire) { var fireEffect = getFireEffect(); game.addChild(fireEffect); activeFireEffects.push(fireEffect); // Position fire effect at the exact tip of the 200x200 shooter barrel - moved upward var barrelEndX = self.x + Math.cos(self.angle) * 100; var barrelEndY = self.y + Math.sin(self.angle) * 100 - 30; // Moved 30 pixels upward fireEffect.x = barrelEndX; fireEffect.y = barrelEndY; fireEffect.rotation = self.angle + Math.PI / 2; // Rotate 90 degrees to the right // Animate fire effect - expand and fade out quickly tween(fireEffect, { scaleX: 1.2, scaleY: 1.2, alpha: 0 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { returnFireEffect(fireEffect); } }); } // Set cooldown to 1.5 seconds (90 frames at 60fps) self.shootCooldown = 90; // Play shoot sound immediately - different sound for white balls try { if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') { LK.getSound('white_shoot').play(); // Use dedicated white ball shooting sound } else { LK.getSound('shoot').play(); } } catch (e) { console.log('Sound play error:', e); } return ball; }; self.update = function () { if (self.shootCooldown > 0) { self.shootCooldown--; } if (self.rapidFire) { self.rapidFireTimer--; if (self.rapidFireTimer <= 0) { self.rapidFire = false; // Remove glow effect tween.stop(self, { tint: true }); self.tint = 0xFFFFFF; } else { // Maintain glow effect if (self.tint === 0xFFFFFF) { tween(self, { tint: 0xFFFF88 }, { duration: 200, easing: tween.easeInOut }); } } } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a0a1a }); /**** * Game Code ****/ // Fire effect pool management function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function getFireEffect() { var fireEffect; if (fireEffectPool.length > 0) { fireEffect = fireEffectPool.pop(); fireEffect.alpha = 0.9; fireEffect.scaleX = 0.8; fireEffect.scaleY = 0.8; tween.stop(fireEffect); // Stop any ongoing animations } else { fireEffect = LK.getAsset('fire_effect', { anchorX: 0.5, anchorY: 0.5 }); } return fireEffect; } function returnFireEffect(fireEffect) { if (fireEffect && fireEffect.parent) { fireEffect.parent.removeChild(fireEffect); } var index = activeFireEffects.indexOf(fireEffect); if (index > -1) { activeFireEffects.splice(index, 1); } // Reset fire effect properties to prevent animation conflicts fireEffect.alpha = 0.9; fireEffect.scaleX = 0.8; fireEffect.scaleY = 0.8; fireEffect.rotation = 0; tween.stop(fireEffect); // Stop any ongoing animations if (fireEffectPool.length < maxPoolSize) { // Limit pool size to prevent memory buildup fireEffectPool.push(fireEffect); } } // Create dynamic space background with shooting stars function createSpaceBackground() { // Create complex starfield with realistic star colors and sizes (no colored squares) var starColors = [0xFFFFFF, 0xFFE4B5, 0xFFB347, 0x87CEEB, 0xF0F8FF, 0xFFF8DC]; var starSizes = [0.5, 0.8, 1.2, 1.8, 2.5, 3.2]; var holeX = 1024; // Black hole position var holeY = 1200; for (var layer = 0; layer < 5; layer++) { var numStars = layer === 0 ? 200 : layer === 1 ? 150 : layer === 2 ? 100 : layer === 3 ? 60 : 30; var baseAlpha = layer === 0 ? 0.2 : layer === 1 ? 0.4 : layer === 2 ? 0.6 : layer === 3 ? 0.8 : 1.0; for (var i = 0; i < numStars; i++) { var starSize = starSizes[Math.floor(Math.random() * starSizes.length)]; var star = game.addChild(LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5, scaleX: starSize * 0.1, scaleY: starSize * 0.1 })); star.x = Math.random() * 2048; star.y = Math.random() * 2732; star.alpha = baseAlpha + Math.random() * 0.3; star.tint = starColors[Math.floor(Math.random() * starColors.length)]; // Calculate distance from black hole for vortex effect var dx = star.x - holeX; var dy = star.y - holeY; var distanceFromHole = Math.sqrt(dx * dx + dy * dy); // Store original position for vortex animation star.originalX = star.x; star.originalY = star.y; star.vortexSpeed = 0.3 + Math.random() * 0.5; // Individual vortex speed star.vortexRadius = distanceFromHole; star.vortexAngle = Math.atan2(dy, dx); // Add realistic twinkling based on star size and distance if (layer >= 2 && Math.random() < 0.4) { var twinkleDelay = Math.random() * 3000; LK.setTimeout(function () { var _twinkleEffect = function twinkleEffect() { if (star && star.parent) { tween(star, { alpha: star.alpha * 0.2 }, { duration: 1000 + Math.random() * 800, easing: tween.easeInOut, onFinish: function onFinish() { if (star && star.parent) { tween(star, { alpha: baseAlpha + Math.random() * 0.3 }, { duration: 1000 + Math.random() * 800, easing: tween.easeInOut, onFinish: _twinkleEffect }); } } }); } }; _twinkleEffect(); }, twinkleDelay); } } } // Create space debris being pulled into black hole for (var i = 0; i < 20; i++) { var debris = game.addChild(LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8 + Math.random() * 1.2, scaleY: 0.8 + Math.random() * 1.2 })); // Start debris at random positions around the edge var angle = Math.random() * Math.PI * 2; var radius = 800 + Math.random() * 600; debris.x = holeX + Math.cos(angle) * radius; debris.y = holeY + Math.sin(angle) * radius; debris.alpha = 0.4 + Math.random() * 0.4; debris.tint = 0x666666; // Gray space debris color debris.rotation = Math.random() * Math.PI * 2; debris.vortexSpeed = 0.8 + Math.random() * 0.4; debris.originalRadius = radius; debris.vortexAngle = angle; // Animate debris spiraling into black hole var animateDebris = function animateDebris(debrisObj) { var _spiralToBlackHole = function spiralToBlackHole() { if (debrisObj && debrisObj.parent) { // Spiral inward while rotating around black hole debrisObj.vortexAngle += debrisObj.vortexSpeed * 0.02; debrisObj.originalRadius -= debrisObj.vortexSpeed * 0.8; // Spiral inward debrisObj.rotation += 0.05; // Rotate the debris itself // Update position debrisObj.x = holeX + Math.cos(debrisObj.vortexAngle) * debrisObj.originalRadius; debrisObj.y = holeY + Math.sin(debrisObj.vortexAngle) * debrisObj.originalRadius; // Fade out as it gets closer to black hole var fadeProgress = 1 - debrisObj.originalRadius / 1400; debrisObj.alpha = 0.4 * (1 - fadeProgress); debrisObj.scaleX *= 0.999; // Gradually shrink debrisObj.scaleY *= 0.999; // Reset debris when it gets too close to black hole if (debrisObj.originalRadius < 50 || debrisObj.alpha < 0.1) { // Respawn at edge var newAngle = Math.random() * Math.PI * 2; var newRadius = 800 + Math.random() * 600; debrisObj.x = holeX + Math.cos(newAngle) * newRadius; debrisObj.y = holeY + Math.sin(newAngle) * newRadius; debrisObj.originalRadius = newRadius; debrisObj.vortexAngle = newAngle; debrisObj.alpha = 0.4 + Math.random() * 0.4; debrisObj.scaleX = 0.8 + Math.random() * 1.2; debrisObj.scaleY = 0.8 + Math.random() * 1.2; } LK.setTimeout(_spiralToBlackHole, 16); // 60fps animation } }; _spiralToBlackHole(); }; animateDebris(debris); } // Cosmic dust clouds removed - only keep the game hole // Nebula formations removed - only keep the game hole } // Initialize space background createSpaceBackground(); // Create shooting star function function createShootingStar() { var shootingStar = game.addChild(LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.3 })); // Random starting position from top or sides var startSide = Math.floor(Math.random() * 3); // 0 = top, 1 = left, 2 = right if (startSide === 0) { shootingStar.x = Math.random() * 2048; shootingStar.y = -50; } else if (startSide === 1) { shootingStar.x = -50; shootingStar.y = Math.random() * 1500; } else { shootingStar.x = 2098; shootingStar.y = Math.random() * 1500; } // Set shooting star properties shootingStar.alpha = 0.8; shootingStar.tint = 0xFFFFFF; shootingStar.rotation = Math.random() * Math.PI * 2; // Create trail effect with multiple small stars var trailStars = []; for (var i = 0; i < 5; i++) { var trailStar = game.addChild(LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.25 - i * 0.035, scaleY: 0.12 - i * 0.018 })); trailStar.x = shootingStar.x; trailStar.y = shootingStar.y; trailStar.alpha = 0.6 - i * 0.1; trailStar.tint = 0xFFFFFF; trailStar.rotation = shootingStar.rotation; trailStars.push(trailStar); } // Calculate movement direction var targetX = Math.random() * 2048; var targetY = 2732 + 200; var deltaX = targetX - shootingStar.x; var deltaY = targetY - shootingStar.y; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); var speed = 15 + Math.random() * 10; var vx = deltaX / distance * speed; var vy = deltaY / distance * speed; // Animate shooting star var _moveShootingStar = function moveShootingStar() { if (shootingStar && shootingStar.parent) { shootingStar.x += vx; shootingStar.y += vy; // Update trail positions for (var i = trailStars.length - 1; i > 0; i--) { if (trailStars[i] && trailStars[i].parent) { trailStars[i].x = trailStars[i - 1].x; trailStars[i].y = trailStars[i - 1].y; } } if (trailStars[0] && trailStars[0].parent) { trailStars[0].x = shootingStar.x; trailStars[0].y = shootingStar.y; } // Check if off screen if (shootingStar.x < -100 || shootingStar.x > 2148 || shootingStar.y > 2832) { shootingStar.destroy(); for (var i = 0; i < trailStars.length; i++) { if (trailStars[i] && trailStars[i].parent) { trailStars[i].destroy(); } } return; } LK.setTimeout(_moveShootingStar, 16); } }; _moveShootingStar(); } // Start shooting star timer (every 3 seconds for more frequency) var shootingStarTimer = LK.setInterval(createShootingStar, 3000); // Create continuous vortex effect for background stars function animateBackgroundVortex() { var holeX = 1024; var holeY = 1200; var _updateVortex = function updateVortex() { // Vortex animation disabled - stars now move directly toward hole LK.setTimeout(_updateVortex, 33); // Keep timer running but do nothing }; _updateVortex(); } // Start background vortex animation animateBackgroundVortex(); // Enhanced star movement toward hole with spiral pattern function animateStarsTowardHole() { var holeX = 1024; // Black hole position var holeY = 1200; var _updateStarMovement = function updateStarMovement() { // Update all background stars with direct vacuum pull toward hole for (var i = 0; i < game.children.length; i++) { var child = game.children[i]; // Check if this is a background star (has original position properties) if (child && child.originalX !== undefined && child.originalY !== undefined) { // Calculate direction toward black hole var dx = holeX - child.x; var dy = holeY - child.y; var distance = Math.sqrt(dx * dx + dy * dy); // Stars move much faster as they get closer to the hole (vacuum effect) var baseSpeed = 2.5 + (1 - Math.min(distance / 600, 1)) * 4; // Direct movement toward hole - no spiral, straight vacuum pull if (distance > 50) { child.x += dx / distance * baseSpeed; child.y += dy / distance * baseSpeed; } // When star gets very close to hole, respawn it at edge if (distance < 80) { // Respawn at random edge position var edge = Math.floor(Math.random() * 4); if (edge === 0) { // Top edge child.x = Math.random() * 2048; child.y = -50; } else if (edge === 1) { // Right edge child.x = 2098; child.y = Math.random() * 2732; } else if (edge === 2) { // Bottom edge child.x = Math.random() * 2048; child.y = 2782; } else { // Left edge child.x = -50; child.y = Math.random() * 2732; } // Update original position for new spawn child.originalX = child.x; child.originalY = child.y; } } } LK.setTimeout(_updateStarMovement, 16); // 60fps animation }; _updateStarMovement(); } // Start enhanced star movement animation animateStarsTowardHole(); // Create Superman flying across background function createFlyingSuperman() { var superman = game.addChild(LK.getAsset('superman', { anchorX: 0.5, anchorY: 0.5 })); // Random starting position from left edge superman.x = -200; superman.y = 200 + Math.random() * 1500; // Random height superman.alpha = 0.8; superman.scaleX = 1.0; superman.scaleY = 1.0; // Random flight path - either straight across or slight curve var targetX = 2248; // Off-screen right var targetY = superman.y + (Math.random() - 0.5) * 400; // Slight vertical variation var flightDuration = 3000 + Math.random() * 2000; // 3-5 seconds flight time // Schedule laser attack when Superman is in middle of screen var laserAttackTime = flightDuration * 0.4; // Attack when 40% across screen LK.setTimeout(function () { if (superman && superman.parent && chain.length > 0) { // Get 2 random balls from chain to destroy - with safety checks var ballsToDestroy = []; var maxBalls = Math.min(2, chain.length); // Get random indices for balls to destroy with better validation var usedIndices = []; var attempts = 0; for (var i = 0; i < maxBalls && attempts < 10; i++) { var randomIndex; do { randomIndex = Math.floor(Math.random() * chain.length); attempts++; } while (usedIndices.indexOf(randomIndex) !== -1 && attempts < 10); if (attempts < 10 && chain[randomIndex] && chain[randomIndex].parent) { usedIndices.push(randomIndex); ballsToDestroy.push(chain[randomIndex]); } } // Play Superman laser sound try { LK.getSound('superman_laser').play(); } catch (e) { console.log('Superman laser sound play error:', e); } // Create laser beams from Superman's eyes to each target ball for (var i = 0; i < ballsToDestroy.length; i++) { var targetBall = ballsToDestroy[i]; // Create laser beam effect using dedicated laser beam asset var laserBeam = LK.getAsset('laser_beam', { anchorX: 0, anchorY: 0.5, scaleX: 1.0, scaleY: 1.0 }); laserBeam.tint = 0xFF0000; // Red laser color laserBeam.alpha = 0.9; // Store reference to Superman and target for continuous tracking laserBeam.superman = superman; laserBeam.targetBall = targetBall; // Position laser at Superman's eye level var eyeX = superman.x + 20; // Adjusted offset for eye position var eyeY = superman.y - 15; // Adjusted for more accurate eye position laserBeam.x = eyeX; laserBeam.y = eyeY; // Calculate angle and length to target ball var dx = targetBall.x - eyeX; var dy = targetBall.y - eyeY; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); laserBeam.rotation = angle; laserBeam.scaleX = distance / 25; // Scale to reach target // Add update function to continuously follow Superman laserBeam.update = function () { if (this.superman && this.superman.parent && this.targetBall && this.targetBall.parent) { // Update laser position to Superman's current eye position var currentEyeX = this.superman.x + 20; // Adjusted offset for eye position var currentEyeY = this.superman.y - 15; // Adjusted for more accurate eye position this.x = currentEyeX; this.y = currentEyeY; // Recalculate angle and distance to target var dx = this.targetBall.x - currentEyeX; var dy = this.targetBall.y - currentEyeY; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); this.rotation = angle; this.scaleX = distance / 25; } }; game.addChild(laserBeam); // Use closure to properly capture laserBeam reference for each iteration (function (currentLaser) { // Animate laser beam appearance and disappearance tween(currentLaser, { alpha: 1.0, scaleY: 0.8 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(currentLaser, { alpha: 0, scaleY: 0.2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { if (currentLaser && currentLaser.parent) { currentLaser.parent.removeChild(currentLaser); currentLaser.destroy(); } } }); } }); })(laserBeam); // Destroy the target ball with explosion effect LK.setTimeout(function () { // Validate ball still exists and is in chain before destroying if (targetBall && targetBall.parent && chain.indexOf(targetBall) > -1) { // Create explosion effect at ball position LK.effects.flashObject(targetBall, 0xFF0000, 300); // Remove ball from chain with proper index validation var ballIndex = chain.indexOf(targetBall); if (ballIndex > -1 && ballIndex < chain.length) { // Store track position for gap closure var removedBallTrackPosition = targetBall.trackPosition; // Remove ball from chain chain.splice(ballIndex, 1); targetBall.destroy(); // Close gap by moving all balls after the removed ball forward var trackSpacing = 7.0; for (var k = ballIndex; k < chain.length; k++) { if (chain[k] && typeof chain[k].trackPosition === 'number') { chain[k].trackPosition -= trackSpacing; positionBallOnTrack(chain[k], chain[k].trackPosition); } } } // Check for new matches after ball removal with validation LK.setTimeout(function () { if (chain && chain.length > 0) { checkMatches(); } }, 150); // Slightly longer delay for proper positioning } }, 250); // Slightly longer delay to ensure laser is visible } } }, laserAttackTime); // Play Superman flying sound try { LK.getSound('superman_flying').play(); } catch (e) { console.log('Superman flying sound play error:', e); } // Animate Superman flying across screen tween(superman, { x: targetX, y: targetY, alpha: 0.9 }, { duration: flightDuration, easing: tween.easeInOut, onFinish: function onFinish() { superman.destroy(); } }); // Add cape fluttering effect var _capeFlutter = function capeFlutter() { if (superman && superman.parent) { tween(superman, { scaleX: 1.05 + Math.random() * 0.1, scaleY: 0.98 + Math.random() * 0.04 }, { duration: 200 + Math.random() * 100, easing: tween.easeInOut, onFinish: function onFinish() { if (superman && superman.parent) { tween(superman, { scaleX: 0.95 + Math.random() * 0.1, scaleY: 1.02 + Math.random() * 0.04 }, { duration: 200 + Math.random() * 100, easing: tween.easeInOut, onFinish: _capeFlutter }); } } }); } }; _capeFlutter(); } // Start Superman timer (random intervals between 8-15 seconds) function scheduleNextSuperman() { var nextInterval = 8000 + Math.random() * 7000; // 8-15 seconds LK.setTimeout(function () { createFlyingSuperman(); scheduleNextSuperman(); // Schedule next Superman }, nextInterval); } // Start the Superman scheduling scheduleNextSuperman(); // Start Villain background appearance scheduling (separate from villain actions) function scheduleNextVillainAppearance() { var nextInterval = 12000 + Math.random() * 8000; // 12-20 seconds LK.setTimeout(function () { createBackgroundVillain(); scheduleNextVillainAppearance(); // Schedule next appearance }, nextInterval); } // Create background villain that just flies across without adding balls function createBackgroundVillain() { var backgroundVillain = game.addChild(LK.getAsset('villain', { anchorX: 0.5, anchorY: 0.5 })); // Add surfboard underneath background villain var backgroundSurfboard = game.addChild(LK.getAsset('surfboard', { anchorX: 0.5, anchorY: 0.5 })); // Random starting position from right edge backgroundVillain.x = 2248; backgroundVillain.y = 300 + Math.random() * 1200; // Random height across more of screen backgroundVillain.scaleX = 1.0; // Full size for clear visibility backgroundVillain.scaleY = 1.0; // Position surfboard under background villain backgroundSurfboard.x = backgroundVillain.x; backgroundSurfboard.y = backgroundVillain.y + 80; // Position below villain backgroundSurfboard.scaleX = 1.0; backgroundSurfboard.scaleY = 1.0; // Store reference to surfboard in background villain for movement backgroundVillain.surfboard = backgroundSurfboard; // No tint for clearer appearance // Flight path across screen (right to left) var targetX = -200; // Off-screen left var targetY = backgroundVillain.y + (Math.random() - 0.5) * 300; var flightDuration = 4000 + Math.random() * 2000; // 4-6 seconds // Schedule ball addition for background villain as well var ballAdditionTime = flightDuration * 0.4; // Add balls when 40% across screen LK.setTimeout(function () { if (backgroundVillain && backgroundVillain.parent) { // Add 3 random colored balls to the back of the chain (behind the last ball) var ballColors = ['yellow', 'blue', 'red', 'yellow']; // Random colors including yellow, blue, red, and yellow var ballsToAdd = 3; var trackSpacing = 7.0; // Standard spacing between balls // Find the last ball position in the chain to determine where to add new balls behind it var lastBallPosition = 0; if (chain.length > 0) { lastBallPosition = chain[chain.length - 1].trackPosition; } else { lastBallPosition = 0; // Start at beginning of track if no balls } // Add 3 strategically colored balls behind the chain (toward the back/start of track) var lastChainColors = []; // Track colors of last few balls in chain for safe selection if (chain.length > 0) { // Get colors of the last 2 balls in the chain for (var c = Math.max(0, chain.length - 2); c < chain.length; c++) { lastChainColors.push(chain[c].color); } } // Sometimes change shooter's current ball color (30% chance) if (Math.random() < 0.3 && shooter && shooter.currentBall) { var allColors = ['red', 'blue', 'green', 'yellow', 'pink']; var newShooterColor = allColors[Math.floor(Math.random() * allColors.length)]; // Remove current ball and create new one with different color if (shooter.currentBall.parent) { shooter.currentBall.parent.removeChild(shooter.currentBall); } shooter.currentBall.destroy(); // Create new ball with different color shooter.currentBall = new Ball(newShooterColor); shooter.currentBall.x = 0; shooter.currentBall.y = -20; shooter.currentBall.scaleX = 1.4375; shooter.currentBall.scaleY = 1.4375; shooter.addChild(shooter.currentBall); // Flash effect to show change LK.effects.flashObject(shooter.currentBall, 0x8800FF, 500); // Display "Ball Changed by GOBLIN" notification var ballChangedText = new Text2('Ball Changed by GOBLIN', { size: 80, fill: 0x8800FF }); ballChangedText.anchor.set(0.5, 0.5); ballChangedText.x = 1024; ballChangedText.y = 900; ballChangedText.alpha = 0; ballChangedText.scaleX = 0.5; ballChangedText.scaleY = 0.5; game.addChild(ballChangedText); // Animate the notification text tween(ballChangedText, { alpha: 1.0, scaleX: 1.2, scaleY: 1.2 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(ballChangedText, { alpha: 0, scaleX: 0.8, scaleY: 0.8, y: ballChangedText.y - 100 }, { duration: 600, easing: tween.easeIn, onFinish: function onFinish() { ballChangedText.destroy(); } }); } }); } for (var i = 0; i < ballsToAdd; i++) { var randomColor = getVillainDisruptiveColor(lastChainColors); // Update lastChainColors for next ball selection lastChainColors.push(randomColor); if (lastChainColors.length > 2) { lastChainColors.shift(); // Keep only last 2 colors } var newBall = game.addChild(new ChainBall(randomColor)); newBall.chainIndex = chain.length; // Position new balls behind the last ball (negative offset from last position) newBall.trackPosition = lastBallPosition + trackSpacing * (i + 1); // Create throwing effect from villain's hand to ball position var villainHandX = backgroundVillain.x - 30; // Villain's hand position (left hand) var villainHandY = backgroundVillain.y + 10; // Slightly below center for hand position // Start ball at villain's hand newBall.x = villainHandX; newBall.y = villainHandY; newBall.scaleX = 0.3; // Start smaller newBall.scaleY = 0.3; newBall.alpha = 0.8; // Position target on track positionBallOnTrack(newBall, newBall.trackPosition); var targetBallX = newBall.targetX; var targetBallY = newBall.targetY; // Animate ball flying from villain's hand to chain position with arc var midX = (villainHandX + targetBallX) / 2; var midY = Math.min(villainHandY, targetBallY) - 100; // Arc peak above both points // First part of arc - villain hand to peak tween(newBall, { x: midX, y: midY, scaleX: 0.6, scaleY: 0.6, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Second part of arc - peak to final position tween(newBall, { x: targetBallX, y: targetBallY, scaleX: 1.15, // Final size scaleY: 1.15 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { // Position ball on track properly after animation positionBallOnTrack(newBall, newBall.trackPosition); // Flash effect when ball reaches chain LK.effects.flashObject(newBall, 0x8800FF, 300); } }); } }); // Add to chain array chain.push(newBall); } // Play villain laser sound for the ball addition try { LK.getSound('villain_laser').play(); } catch (e) { console.log('Background villain laser sound play error:', e); } } }, ballAdditionTime); // Always play villain flying sound with better error handling try { var villainFlyingSound = LK.getSound('villain_flying'); if (villainFlyingSound && typeof villainFlyingSound.play === 'function') { villainFlyingSound.play(); } } catch (e) { console.log('Background villain flying sound play error:', e); } // Animate villain flying across screen tween(backgroundVillain, { x: targetX, y: targetY }, { duration: flightDuration, easing: tween.easeInOut, onFinish: function onFinish() { backgroundVillain.destroy(); if (backgroundVillain.surfboard && backgroundVillain.surfboard.parent) { backgroundVillain.surfboard.destroy(); } } }); // Animate surfboard to follow background villain if (backgroundVillain.surfboard) { tween(backgroundVillain.surfboard, { x: targetX, y: targetY + 80 }, { duration: flightDuration, easing: tween.easeInOut }); } // Add subtle cape fluttering var _backgroundCapeFlutter = function backgroundCapeFlutter() { if (backgroundVillain && backgroundVillain.parent) { tween(backgroundVillain, { scaleX: 0.85 + Math.random() * 0.1, scaleY: 0.78 + Math.random() * 0.04 }, { duration: 250 + Math.random() * 100, easing: tween.easeInOut, onFinish: function onFinish() { if (backgroundVillain && backgroundVillain.parent) { tween(backgroundVillain, { scaleX: 0.75 + Math.random() * 0.1, scaleY: 0.82 + Math.random() * 0.04 }, { duration: 250 + Math.random() * 100, easing: tween.easeInOut, onFinish: _backgroundCapeFlutter }); } // Update surfboard position continuously if (backgroundVillain.surfboard && backgroundVillain.surfboard.parent) { backgroundVillain.surfboard.x = backgroundVillain.x; backgroundVillain.surfboard.y = backgroundVillain.y + 80; } } }); } }; _backgroundCapeFlutter(); } // Start the villain background appearance scheduling scheduleNextVillainAppearance(); // Create Villain flying across background function createFlyingVillain() { var villain = game.addChild(LK.getAsset('villain', { anchorX: 0.5, anchorY: 0.5 })); // Add surfboard underneath villain var surfboard = game.addChild(LK.getAsset('surfboard', { anchorX: 0.5, anchorY: 0.5 })); // Random starting position from right edge (opposite of Superman) villain.x = 2248; villain.y = 200 + Math.random() * 1500; // Random height villain.scaleX = 1.0; villain.scaleY = 1.0; // Position surfboard under villain surfboard.x = villain.x; surfboard.y = villain.y + 80; // Position below villain surfboard.scaleX = 1.0; surfboard.scaleY = 1.0; // Store reference to surfboard in villain for movement villain.surfboard = surfboard; // No tint for clearer appearance // Random flight path - either straight across or slight curve (flying left) var targetX = -200; // Off-screen left var targetY = villain.y + (Math.random() - 0.5) * 400; // Slight vertical variation var flightDuration = 3000 + Math.random() * 2000; // 3-5 seconds flight time // Schedule ball addition when Villain is in middle of screen var ballAdditionTime = flightDuration * 0.4; // Add balls when 40% across screen LK.setTimeout(function () { if (villain && villain.parent) { // Add 3 random colored balls to the back of the chain (behind the last ball) var ballColors = ['yellow', 'blue', 'red', 'yellow']; // Random colors including yellow, blue, red, and yellow var ballsToAdd = 3; var trackSpacing = 7.0; // Standard spacing between balls // Find the last ball position in the chain to determine where to add new balls behind it var lastBallPosition = 0; if (chain.length > 0) { lastBallPosition = chain[chain.length - 1].trackPosition; } else { lastBallPosition = 0; // Start at beginning of track if no balls } // Add 3 strategically colored balls behind the chain (toward the back/start of track) var lastChainColors = []; // Track colors of last few balls in chain for safe selection if (chain.length > 0) { // Get colors of the last 2 balls in the chain for (var c = Math.max(0, chain.length - 2); c < chain.length; c++) { lastChainColors.push(chain[c].color); } } // Sometimes change shooter's current ball color (30% chance) if (Math.random() < 0.3 && shooter && shooter.currentBall) { var allColors = ['red', 'blue', 'green', 'yellow', 'pink']; var newShooterColor = allColors[Math.floor(Math.random() * allColors.length)]; // Remove current ball and create new one with different color if (shooter.currentBall.parent) { shooter.currentBall.parent.removeChild(shooter.currentBall); } shooter.currentBall.destroy(); // Create new ball with different color shooter.currentBall = new Ball(newShooterColor); shooter.currentBall.x = 0; shooter.currentBall.y = -20; shooter.currentBall.scaleX = 1.4375; shooter.currentBall.scaleY = 1.4375; shooter.addChild(shooter.currentBall); // Flash effect to show change LK.effects.flashObject(shooter.currentBall, 0x8800FF, 500); // Display "Ball Changed by GOBLIN" notification var ballChangedText = new Text2('Ball Changed by GOBLIN', { size: 80, fill: 0x8800FF }); ballChangedText.anchor.set(0.5, 0.5); ballChangedText.x = 1024; ballChangedText.y = 900; ballChangedText.alpha = 0; ballChangedText.scaleX = 0.5; ballChangedText.scaleY = 0.5; game.addChild(ballChangedText); // Animate the notification text tween(ballChangedText, { alpha: 1.0, scaleX: 1.2, scaleY: 1.2 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(ballChangedText, { alpha: 0, scaleX: 0.8, scaleY: 0.8, y: ballChangedText.y - 100 }, { duration: 600, easing: tween.easeIn, onFinish: function onFinish() { ballChangedText.destroy(); } }); } }); } for (var i = 0; i < ballsToAdd; i++) { var randomColor = getVillainDisruptiveColor(lastChainColors); // Update lastChainColors for next ball selection lastChainColors.push(randomColor); if (lastChainColors.length > 2) { lastChainColors.shift(); // Keep only last 2 colors } var newBall = game.addChild(new ChainBall(randomColor)); newBall.chainIndex = chain.length; // Position new balls behind the last ball (negative offset from last position) newBall.trackPosition = lastBallPosition + trackSpacing * (i + 1); // Create throwing effect from villain's hand to ball position var villainHandX = villain.x - 30; // Villain's hand position (left hand) var villainHandY = villain.y + 10; // Slightly below center for hand position // Start ball at villain's hand newBall.x = villainHandX; newBall.y = villainHandY; newBall.scaleX = 0.3; // Start smaller newBall.scaleY = 0.3; newBall.alpha = 0.8; // Position target on track positionBallOnTrack(newBall, newBall.trackPosition); var targetX = newBall.targetX; var targetY = newBall.targetY; // Animate ball flying from villain's hand to chain position with arc var midX = (villainHandX + targetX) / 2; var midY = Math.min(villainHandY, targetY) - 100; // Arc peak above both points // First part of arc - villain hand to peak tween(newBall, { x: midX, y: midY, scaleX: 0.6, scaleY: 0.6, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Second part of arc - peak to final position tween(newBall, { x: targetX, y: targetY, scaleX: 1.15, // Final size scaleY: 1.15 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { // Position ball on track properly after animation positionBallOnTrack(newBall, newBall.trackPosition); // Flash effect when ball reaches chain LK.effects.flashObject(newBall, 0x8800FF, 300); } }); } }); // Add to chain array chain.push(newBall); } // Play villain laser sound for the ball addition try { LK.getSound('villain_laser').play(); } catch (e) { console.log('Villain laser sound play error:', e); } // Create visual effect showing villain adding balls to chain var addBallText = new Text2('VILLAIN ADDS BALLS!', { size: 80, fill: 0x8800FF }); addBallText.anchor.set(0.5, 0.5); addBallText.x = villain.x; addBallText.y = villain.y - 80; addBallText.alpha = 0; addBallText.scaleX = 0.5; addBallText.scaleY = 0.5; game.addChild(addBallText); // Animate warning text tween(addBallText, { alpha: 1.0, scaleX: 1.2, scaleY: 1.2 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(addBallText, { alpha: 0, scaleX: 0.8, scaleY: 0.8, y: addBallText.y - 50 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { addBallText.destroy(); } }); } }); } }, ballAdditionTime); // Play Villain flying sound with delay to prevent overlap LK.setTimeout(function () { try { var villainFlyingSound = LK.getSound('villain_flying'); if (villainFlyingSound && typeof villainFlyingSound.play === 'function') { villainFlyingSound.play(); } } catch (e) { console.log('Villain flying sound play error:', e); } }, 100); // Animate Villain flying across screen (right to left) tween(villain, { x: targetX, y: targetY }, { duration: flightDuration, easing: tween.easeInOut, onFinish: function onFinish() { villain.destroy(); if (villain.surfboard && villain.surfboard.parent) { villain.surfboard.destroy(); } } }); // Animate surfboard to follow villain if (villain.surfboard) { tween(villain.surfboard, { x: targetX, y: targetY + 80 }, { duration: flightDuration, easing: tween.easeInOut }); } // Add cape fluttering effect (similar to Superman but with villain styling) var _villainCapeFlutter = function villainCapeFlutter() { if (villain && villain.parent) { tween(villain, { scaleX: 1.05 + Math.random() * 0.1, scaleY: 0.98 + Math.random() * 0.04 }, { duration: 200 + Math.random() * 100, easing: tween.easeInOut, onFinish: function onFinish() { if (villain && villain.parent) { tween(villain, { scaleX: 0.95 + Math.random() * 0.1, scaleY: 1.02 + Math.random() * 0.04 }, { duration: 200 + Math.random() * 100, easing: tween.easeInOut, onFinish: _villainCapeFlutter }); } } }); // Update surfboard position continuously if (villain.surfboard && villain.surfboard.parent) { villain.surfboard.x = villain.x; villain.surfboard.y = villain.y + 80; } } }; _villainCapeFlutter(); } // Game variables var chain = []; var flyingBalls = []; var shooter; var crosshair; var trackPoints = []; var chainSpeed = 0.004; // Increased initial chain speed for faster start var normalChainSpeed = 0.004; // Store normal speed - increased from 0.0003 var fastChainSpeed = 0.002; // Fast chain speed - slower than before var veryFastChainSpeed = 0.006; // Very fast chain speed - slower than before var speedLevel = 0; // 0 = normal, 1 = fast, 2 = very fast var chainFrozen = false; var freezeTimer = 0; var freezeCountdownDisplay = null; var level = 1; var gameWon = false; var gameLost = false; // Score multiplier system var scoreMultipliers = [1.0, 1.5, 2.0]; // normal, fast, very fast var pointsDisplays = []; // Array to track active point displays var shotCounter = 0; // Counter to track shots fired // Consecutive scoring system var consecutiveScores = 0; // Track consecutive scores var lastScoreTime = 0; // Track when last score happened var celebrationMessages = ['AWESOME!', 'AMAZING!', 'FANTASTIC!', 'INCREDIBLE!', 'SPECTACULAR!']; // Fire effect optimization var fireEffectPool = []; // Pool of reusable fire effects var activeFireEffects = []; // Currently active fire effects var maxFireEffects = 2; // Reduced maximum concurrent fire effects var maxPoolSize = 3; // Limit pool size to prevent memory buildup // Create vortex track path function createTrackPath() { trackPoints = []; var centerX = 1024; var centerY = 1366; var holeX = 1024; var holeY = 1200; // Hole position - closer to shooter var segments = 1500; // Longer track for better gameplay var targetSpacing = 7.0; // Target distance between consecutive points var currentDistance = 0; // Generate initial spiral path points (more densely packed) var tempPoints = []; var densityMultiplier = 3; // Create 3x more points initially for smoother curves for (var i = 0; i < segments * densityMultiplier; i++) { var progress = i / (segments * densityMultiplier); // Single spiral angle - creates one continuous swirl towards center var spiralAngle = progress * Math.PI * 6; // 3 full rotations for longer swirling path // Radius decreases linearly for smooth inward movement var baseRadius = 1000 * (1 - progress); // Linear decrease for steady approach to center // Add gentle wave motion for swirling effect var waveMotion = Math.sin(progress * Math.PI * 12) * (60 * (1 - progress)); // Gentle swirling motion var currentRadius = baseRadius + waveMotion; // Ensure minimum radius for playability at center currentRadius = Math.max(currentRadius, 40); // Calculate position on spiral path - interpolate towards hole position var spiralX = centerX + Math.cos(spiralAngle) * currentRadius; var spiralY = centerY + Math.sin(spiralAngle) * currentRadius; // Gradually move towards hole position in final segment var holeProgress = Math.max(0, (progress - 0.9) / 0.1); // Last 10% of track moves to hole var x = spiralX + (holeX - spiralX) * holeProgress; var y = spiralY + (holeY - spiralY) * holeProgress; // Ensure the path stays within screen bounds with padding x = Math.max(150, Math.min(1898, x)); y = Math.max(300, Math.min(2600, y)); tempPoints.push({ x: x, y: y }); } // Now resample the path to ensure equal spacing trackPoints.push(tempPoints[0]); // Add first point currentDistance = 0; for (var i = 1; i < tempPoints.length; i++) { var prevPoint = tempPoints[i - 1]; var currPoint = tempPoints[i]; var segmentLength = Math.sqrt(Math.pow(currPoint.x - prevPoint.x, 2) + Math.pow(currPoint.y - prevPoint.y, 2)); currentDistance += segmentLength; // Add point when we've traveled the target spacing distance if (currentDistance >= targetSpacing) { // Interpolate to get exact position at target spacing var excess = currentDistance - targetSpacing; var ratio = excess / segmentLength; var adjustedX = currPoint.x - (currPoint.x - prevPoint.x) * ratio; var adjustedY = currPoint.y - (currPoint.y - prevPoint.y) * ratio; trackPoints.push({ x: adjustedX, y: adjustedY }); currentDistance = excess; // Carry over the excess distance } } // Create visual track markers with vortex effect for (var i = 0; i < trackPoints.length; i += 6) { var trackMarker = game.addChild(LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5 })); trackMarker.x = trackPoints[i].x; trackMarker.y = trackPoints[i].y; trackMarker.alpha = 0.3 + i / trackPoints.length * 0.3; // Fade in towards center // Scale markers smaller as they approach center for vortex effect var scaleProgress = i / trackPoints.length; trackMarker.scaleX = 0.5 + (1 - scaleProgress) * 0.5; trackMarker.scaleY = 0.5 + (1 - scaleProgress) * 0.5; } } // Create hole at end of track var hole = game.addChild(LK.getAsset('hole', { anchorX: 0.5, anchorY: 0.5 })); // Create shooter shooter = game.addChild(new Shooter()); shooter.x = 1024; // Center of screen horizontally (2048/2 = 1024) shooter.y = 1366; // Position near the center hole // Initialize first balls shooter.loadBall(); shooter.loadBall(); // Create crosshair crosshair = game.addChild(LK.getAsset('aim', { anchorX: 0.5, anchorY: 0.5 })); crosshair.x = 1024; // Start at center crosshair.y = 1000; crosshair.alpha = 0.8; crosshair.scaleX = 1.2; crosshair.scaleY = 1.2; // Villain strategic color selection - uses all colors and aims to disrupt same-color groups function getVillainDisruptiveColor(lastChainColors) { var allColors = ['red', 'blue', 'green', 'yellow', 'pink']; // All available colors var chainColors = []; // Track colors in current chain for analysis // Analyze chain colors to find same-color groups that can be disrupted for (var i = 0; i < chain.length; i++) { chainColors.push(chain[i].color); } // Find the most common colors in chain to target for disruption var colorCounts = {}; for (var i = 0; i < chainColors.length; i++) { var color = chainColors[i]; colorCounts[color] = (colorCounts[color] || 0) + 1; } // Look for consecutive same-color groups of 2 that villain can disrupt var disruptiveColors = []; for (var i = 0; i < chainColors.length - 1; i++) { if (chainColors[i] === chainColors[i + 1]) { // Found a pair of same colors - add different colors as disruptive options for (var c = 0; c < allColors.length; c++) { if (allColors[c] !== chainColors[i] && disruptiveColors.indexOf(allColors[c]) === -1) { disruptiveColors.push(allColors[c]); } } } } // If no specific disruption needed, avoid creating 3+ consecutive with last added colors var safeColors = []; for (var c = 0; c < allColors.length; c++) { var testColor = allColors[c]; var wouldCreate3 = false; // Check if this color would create 3 consecutive with last chain colors if (lastChainColors.length >= 2 && lastChainColors[lastChainColors.length - 1] === testColor && lastChainColors[lastChainColors.length - 2] === testColor) { wouldCreate3 = true; } if (!wouldCreate3) { safeColors.push(testColor); } } // Prefer disruptive colors if available, otherwise use safe colors var finalColors = disruptiveColors.length > 0 ? disruptiveColors : safeColors; // If no valid colors (shouldn't happen), use any color except the most recent if (finalColors.length === 0) { for (var c = 0; c < allColors.length; c++) { if (lastChainColors.length === 0 || allColors[c] !== lastChainColors[lastChainColors.length - 1]) { finalColors.push(allColors[c]); } } } // Return random color from final selection return finalColors[Math.floor(Math.random() * finalColors.length)]; } // Helper function to get safe color that won't create 3+ consecutive function getSafeColor(colors, lastColors) { var availableColors = []; // Find colors that won't create 3 consecutive for (var c = 0; c < colors.length; c++) { var testColor = colors[c]; var wouldCreate3 = false; // Check if this color would create 3 consecutive if (lastColors.length >= 2 && lastColors[lastColors.length - 1] === testColor && lastColors[lastColors.length - 2] === testColor) { wouldCreate3 = true; } if (!wouldCreate3) { availableColors.push(testColor); } } // If all colors would create 3 consecutive (shouldn't happen), return any color except the last one if (availableColors.length === 0) { for (var c = 0; c < colors.length; c++) { if (lastColors.length === 0 || colors[c] !== lastColors[lastColors.length - 1]) { availableColors.push(colors[c]); } } } // Return random color from available safe colors return availableColors[Math.floor(Math.random() * availableColors.length)]; } // Create initial chain function createChain() { chain = []; var chainLength = 15 + level * 5; var colors = ['red', 'blue', 'green', 'yellow', 'pink']; var ballSpacing = 69; // Space between balls - balls touch each other (increased for bigger balls) var trackSpacing = 6.0; // Increased track position spacing to prevent overlap var lastColors = []; // Track last few colors to prevent 3+ consecutive // Ensure better color distribution by cycling through colors for (var i = 0; i < chainLength; i++) { var color; // Use safe color selection to prevent 3+ consecutive if (i < 2) { // For first two balls, use any color if (i % 8 < 5) { // First 5 of every 8 balls use sequential colors var colorIndex = i % colors.length; color = colors[colorIndex]; } else { // Last 3 of every 8 balls use random colors var colorIndex = Math.floor(Math.random() * colors.length); color = colors[colorIndex]; } } else { // For subsequent balls, ensure no 3+ consecutive color = getSafeColor(colors, lastColors); } // Safety check to ensure we never create black balls if (color === 'black' || !color) { color = colors[i % colors.length]; // Default to cycling through valid colors } var ball = game.addChild(new ChainBall(color)); ball.chainIndex = i; // Initialize track position with consistent spacing for vortex ball.trackPosition = i * 7.0; // Use consistent spacing to prevent overlap // Position ball immediately on track positionBallOnTrack(ball, ball.trackPosition); chain.push(ball); // Update lastColors array (keep only last 2 colors for checking) lastColors.push(color); if (lastColors.length > 2) { lastColors.shift(); // Remove oldest color, keep only last 2 } } } // Position ball on track function positionBallOnTrack(ball, trackPosition) { if (trackPosition >= trackPoints.length || trackPoints.length === 0) { if (trackPoints.length > 0) { ball.x = trackPoints[trackPoints.length - 1].x; ball.y = trackPoints[trackPoints.length - 1].y; } else { ball.x = 1024; ball.y = 1366; } return; } var pointIndex = Math.floor(Math.max(0, trackPosition)); var nextIndex = Math.min(pointIndex + 1, trackPoints.length - 1); var t = trackPosition - pointIndex; var point1 = trackPoints[pointIndex]; var point2 = trackPoints[nextIndex]; // Add safety check for undefined points if (!point1 || !point2) { ball.x = 1024; ball.y = 1366; ball.isMoving = false; return; } // Calculate position with anti-overlap adjustment near the hole var baseX = point1.x + (point2.x - point1.x) * t; var baseY = point1.y + (point2.y - point1.y) * t; // Apply spacing adjustment to prevent overlap near hole var progressToHole = trackPosition / trackPoints.length; if (progressToHole > 0.8) { // Near the hole (last 20% of track) // Add slight offset based on ball's chain position to prevent stacking var offsetAngle = ball.chainIndex * 0.3; // Small angular offset per ball var offsetRadius = 15 * (progressToHole - 0.8) * 5; // Increase offset near hole baseX += Math.cos(offsetAngle) * offsetRadius; baseY += Math.sin(offsetAngle) * offsetRadius; } ball.targetX = baseX; ball.targetY = baseY; ball.isMoving = true; } // Update chain positions function updateChain() { if (chainFrozen) { freezeTimer--; // Update freeze countdown display if (freezeCountdownDisplay) { var secondsLeft = Math.ceil(freezeTimer / 60); freezeCountdownDisplay.setText('FROZEN: ' + secondsLeft); // Add pulsing effect each second if (freezeTimer % 60 === 0) { tween(freezeCountdownDisplay, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { if (freezeCountdownDisplay) { tween(freezeCountdownDisplay, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200, easing: tween.easeIn }); } } }); } } if (freezeTimer <= 0) { chainFrozen = false; // Remove countdown display if (freezeCountdownDisplay) { // Remove white overlay if (freezeCountdownDisplay.whiteOverlay && freezeCountdownDisplay.whiteOverlay.parent) { freezeCountdownDisplay.whiteOverlay.destroy(); } freezeCountdownDisplay.destroy(); freezeCountdownDisplay = null; } // Remove ice tint from all chain balls for (var i = 0; i < chain.length; i++) { var chainBall = chain[i]; tween(chainBall, { tint: 0xFFFFFF }, { duration: 300 }); } } return; } // Detect gaps in the chain by checking distances between consecutive balls var hasGap = false; var gapStartIndex = -1; var gapEndIndex = -1; var trackSpacing = 7.0; // Standard spacing between balls var maxAllowedGap = trackSpacing * 2; // Gap threshold for (var i = 0; i < chain.length - 1; i++) { var currentBall = chain[i]; var nextBall = chain[i + 1]; var gapDistance = nextBall.trackPosition - currentBall.trackPosition; if (gapDistance > maxAllowedGap) { hasGap = true; gapStartIndex = i; gapEndIndex = i + 1; break; } } if (hasGap) { // Gap detected - back part waits completely, front part moves forward to close gap for (var i = 0; i < chain.length; i++) { var ball = chain[i]; if (i <= gapStartIndex) { // Front part (before gap) - move forward normally to close gap ball.trackPosition += chainSpeed; // Check if reached hole (vortex center) if (ball.trackPosition >= trackPoints.length - 1) { if (!gameLost) { gameLost = true; // Play gameover sound with error handling try { var gameoverSound = LK.getSound('gameover'); if (gameoverSound && typeof gameoverSound.play === 'function') { gameoverSound.play(); } } catch (e) { console.log('Gameover sound play error:', e); } LK.showGameOver(); } return; } } else { // Back part (after gap) - wait completely, don't move at all // Don't move the back part at all } } } else { // No gap - move all balls forward normally together for (var i = 0; i < chain.length; i++) { var ball = chain[i]; // Move chain forward in vortex pattern ball.trackPosition += chainSpeed; // Check if reached hole (vortex center) if (ball.trackPosition >= trackPoints.length - 1) { if (!gameLost) { gameLost = true; // Play gameover sound with error handling try { var gameoverSound = LK.getSound('gameover'); if (gameoverSound && typeof gameoverSound.play === 'function') { gameoverSound.play(); } } catch (e) { console.log('Gameover sound play error:', e); } LK.showGameOver(); } return; } } } // Maintain equal spacing between balls for vortex movement with collision avoidance for (var i = 0; i < chain.length; i++) { var ball = chain[i]; // Ensure proper spacing from previous ball if (i > 0) { var previousBall = chain[i - 1]; var expectedPosition = previousBall.trackPosition + trackSpacing; // Only enforce forward spacing when gap is closing or normal movement var currentGap = ball.trackPosition - previousBall.trackPosition; if (currentGap < trackSpacing * 0.8) { // Gap is almost closed, enforce proper spacing ball.trackPosition = Math.max(ball.trackPosition, expectedPosition); } else if (currentGap > maxAllowedGap) { // Large gap detected, allow back part to move forward freely // Don't enforce spacing yet } else { // Normal spacing enforcement ball.trackPosition = Math.max(ball.trackPosition, expectedPosition); } } positionBallOnTrack(ball, ball.trackPosition); } // Additional collision avoidance pass to prevent balls from overlapping for (var i = 0; i < chain.length; i++) { var ball = chain[i]; for (var j = i + 1; j < chain.length; j++) { var otherBall = chain[j]; var dx = ball.x - otherBall.x; var dy = ball.y - otherBall.y; var distance = Math.sqrt(dx * dx + dy * dy); var minDistance = 69; // Minimum distance between ball centers (increased for 35% larger balls) if (distance < minDistance && distance > 0) { // Calculate separation needed var separation = minDistance - distance; var separationX = dx / distance * separation * 0.5; var separationY = dy / distance * separation * 0.5; // Move balls apart by adjusting their track positions var trackDelta = separation / trackSpacing; if (i > 0) { // Move current ball back ball.trackPosition -= trackDelta * 0.5; // Reposition on track positionBallOnTrack(ball, ball.trackPosition); } if (j < chain.length - 1) { // Move other ball forward otherBall.trackPosition += trackDelta * 0.5; // Reposition on track positionBallOnTrack(otherBall, otherBall.trackPosition); } } } } } // Check for matches function checkMatches() { var matches = []; var currentColor = null; var currentMatch = []; // First, ensure all balls are positioned correctly on the track for (var i = 0; i < chain.length; i++) { var ball = chain[i]; positionBallOnTrack(ball, ball.trackPosition); } // Check for consecutive balls of the same color for (var i = 0; i < chain.length; i++) { var ball = chain[i]; if (ball.color === currentColor) { currentMatch.push(ball); } else { if (currentMatch.length >= 3) { matches.push(currentMatch.slice()); } currentColor = ball.color; currentMatch = [ball]; } } // Check last group if (currentMatch.length >= 3) { matches.push(currentMatch.slice()); } // Remove matches for (var m = 0; m < matches.length; m++) { var match = matches[m]; var baseScore = match.length * 10; var multiplier = scoreMultipliers[speedLevel]; var score = Math.floor(baseScore * multiplier); // Calculate display position (center of the match) var displayX = 0; var displayY = 0; for (var b = 0; b < match.length; b++) { displayX += match[b].x; displayY += match[b].y; } displayX /= match.length; displayY /= match.length; // Display flashing points displayPoints(score, displayX, displayY); // Check for special balls for (var b = 0; b < match.length; b++) { var ball = match[b]; if (ball.isSpecial) { handleSpecialBall(ball); } } // Store the first index of the match to know where the gap starts var firstMatchIndex = chain.indexOf(match[0]); var trackSpacing = 7.0; // Increased spacing to prevent overlap var gapSize = match.length * trackSpacing; // Calculate gap size based on track spacing // Remove matched balls for (var b = 0; b < match.length; b++) { var ball = match[b]; var index = chain.indexOf(ball); if (index > -1) { chain.splice(index, 1); ball.destroy(); } } // Close the gap by moving all balls after the removed section forward for (var i = firstMatchIndex; i < chain.length; i++) { var ball = chain[i]; // Add safety check for ball and trackPosition existence if (!ball || typeof ball.trackPosition !== 'number' || isNaN(ball.trackPosition)) { // Skip invalid balls or initialize trackPosition if (ball) { ball.trackPosition = i * 7.0; // Initialize with index-based positioning } continue; } // Move the ball's track position forward to close the gap ball.trackPosition -= gapSize; } // Re-space all balls to maintain equal intervals after gap closing for (var i = 1; i < chain.length; i++) { var previousBall = chain[i - 1]; var currentBall = chain[i]; var expectedPosition = previousBall.trackPosition + trackSpacing; // Enforce minimum spacing to prevent overlap currentBall.trackPosition = Math.max(currentBall.trackPosition, expectedPosition); // Position the ball immediately to detect matches faster positionBallOnTrack(currentBall, currentBall.trackPosition); // Animate the ball to its new position smoothly tween(currentBall, { x: currentBall.targetX, y: currentBall.targetY }, { duration: 200, // Faster animation for quicker match detection easing: tween.easeOut }); } LK.setScore(LK.getScore() + score); // Update score display immediately scoreTxt.setText('SCORE: ' + LK.getScore()); // Add enhanced scale up/down effect and flash score text in RGB colors with longer duration tween(scoreTxt, { scaleX: 1.6, scaleY: 1.6, tint: 0xFF0000 // Red }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: 1.4, scaleY: 1.4, tint: 0x00FF00 // Green }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: 1.5, scaleY: 1.5, tint: 0x0000FF // Blue }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: 1.3, scaleY: 1.3, tint: 0xFFFF00 // Yellow }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: 1.2, scaleY: 1.2, tint: 0xFF00FF // Magenta }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: 1.1, scaleY: 1.1, tint: 0x00FFFF // Cyan }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt, { scaleX: 1.0, scaleY: 1.0, tint: 0xFFFFFF // Back to white }, { duration: 300, easing: tween.easeIn }); } }); } }); } }); } }); } }); } }); LK.getSound('match').play(); // Track consecutive scoring var currentTime = LK.ticks; if (currentTime - lastScoreTime < 180) { // Within 3 seconds (180 frames at 60fps) consecutiveScores++; } else { consecutiveScores = 1; // Reset to 1 for this score } lastScoreTime = currentTime; // Show celebration for consecutive scores (2 or more) if (consecutiveScores >= 2) { displayCelebration(); } // Pull chain back slightly when balls are removed chainSpeed = Math.max(0.5, chainSpeed - 0.1); } // Always check for new matches after gap closure, regardless of previous matches // This ensures balls disappear when they touch each other after gap closure if (matches.length > 0) { // After removing matches and closing gaps, check for new matches that may have formed // Use a timeout to allow animations to settle before checking LK.setTimeout(function () { checkMatches(); }, 50); // Reduced timeout for faster response } // Always perform an additional check for consecutive same-color balls after any gap closure // This ensures that balls of the same color that become adjacent are immediately detected var foundConsecutiveColors = false; for (var i = 0; i < chain.length - 1; i++) { var currentBall = chain[i]; var nextBall = chain[i + 1]; if (currentBall.color === nextBall.color) { // Found consecutive same-color balls, check for a group of 3 or more var consecutiveCount = 1; var groupStart = i; // Count consecutive balls of the same color starting from current position for (var j = i + 1; j < chain.length && chain[j].color === currentBall.color; j++) { consecutiveCount++; } // If we have 3 or more consecutive balls of the same color, mark for immediate removal if (consecutiveCount >= 3) { foundConsecutiveColors = true; break; } } } // If consecutive same-color balls were found, immediately check matches again if (foundConsecutiveColors) { LK.setTimeout(function () { checkMatches(); }, 10); // Very short timeout for immediate response } // Check win condition if (chain.length === 0) { gameWon = true; level++; LK.showYouWin(); } } // Display celebration message for consecutive scores function displayCelebration() { var message = celebrationMessages[Math.min(consecutiveScores - 2, celebrationMessages.length - 1)]; var celebrationText = new Text2(message, { size: 100, fill: 0xFFD700 // Gold color }); celebrationText.anchor.set(0.5, 0.5); celebrationText.x = 1024; // Center of screen celebrationText.y = 800; // Middle area celebrationText.alpha = 0; celebrationText.scaleX = 0.5; celebrationText.scaleY = 0.5; celebrationText.rotation = -0.2; game.addChild(celebrationText); // Animate celebration text with dramatic entrance tween(celebrationText, { alpha: 1.0, scaleX: 1.5, scaleY: 1.5, rotation: 0.1 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { // Hold for a moment then fade out tween(celebrationText, { alpha: 0, scaleX: 0.8, scaleY: 0.8, y: celebrationText.y - 100 }, { duration: 600, easing: tween.easeIn, onFinish: function onFinish() { celebrationText.destroy(); } }); } }); // Play celebration sound try { LK.getSound('celebration').play(); } catch (e) { console.log('Celebration sound play error:', e); } } // Display flashing points function displayPoints(points, x, y) { var pointsText = new Text2(('+' + points).toUpperCase(), { size: 80, fill: 0xFFFF00, font: "'Courier New Bold', 'Monaco Bold', 'Menlo Bold', 'Consolas Bold', 'DejaVu Sans Mono Bold', 'Lucida Console Bold', monospace" }); pointsText.anchor.set(0.5, 0.5); pointsText.x = x; pointsText.y = y; pointsText.alpha = 1.0; pointsText.scaleX = 0.5; pointsText.scaleY = 0.5; game.addChild(pointsText); pointsDisplays.push(pointsText); // Add RGB flashing effect for 0.5 seconds tween(pointsText, { tint: 0xFF0000 // Red }, { duration: 125, easing: tween.easeInOut, onFinish: function onFinish() { tween(pointsText, { tint: 0x00FF00 // Green }, { duration: 125, easing: tween.easeInOut, onFinish: function onFinish() { tween(pointsText, { tint: 0x0000FF // Blue }, { duration: 125, easing: tween.easeInOut, onFinish: function onFinish() { tween(pointsText, { tint: 0xFFFF00 // Back to yellow }, { duration: 125, easing: tween.easeInOut }); } }); } }); } }); // Get score text position for flying animation var scoreTextPos = scoreTxt.parent.toGlobal(scoreTxt.position); var gameScorePos = game.toLocal(scoreTextPos); // Animate the points display tween(pointsText, { scaleX: 1.2, scaleY: 1.2, y: y - 100 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { // First fade and fly towards score tween(pointsText, { alpha: 0.8, scaleX: 0.6, scaleY: 0.6, x: gameScorePos.x, y: gameScorePos.y }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { // Final fade out at score position tween(pointsText, { alpha: 0, scaleX: 0.3, scaleY: 0.3 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { pointsText.destroy(); var index = pointsDisplays.indexOf(pointsText); if (index > -1) { pointsDisplays.splice(index, 1); } } }); } }); } }); } // Handle special ball effects function handleSpecialBall(ball) { if (ball.specialType === 'explosive') { var _shakeEffect = function shakeEffect() { if (shakeTimer < shakeDuration) { game.x = originalX + (Math.random() - 0.5) * shakeIntensity; game.y = originalY + (Math.random() - 0.5) * shakeIntensity; shakeTimer += 16; LK.setTimeout(_shakeEffect, 16); } else { game.x = originalX; game.y = originalY; } }; // Remove nearby balls var explosionRadius = 3; var ballIndex = chain.indexOf(ball); var toRemove = []; for (var i = Math.max(0, ballIndex - explosionRadius); i < Math.min(chain.length, ballIndex + explosionRadius + 1); i++) { if (chain[i] !== ball) { toRemove.push(chain[i]); } } for (var i = 0; i < toRemove.length; i++) { var index = chain.indexOf(toRemove[i]); if (index > -1) { chain.splice(index, 1); toRemove[i].destroy(); } } LK.effects.flashScreen(0xff8000, 300); // Add screen shake effect var originalX = game.x; var originalY = game.y; var shakeIntensity = 10; var shakeDuration = 300; var shakeTimer = 0; _shakeEffect(); } else if (ball.specialType === 'freeze') { chainFrozen = true; freezeTimer = 180; // 3 seconds at 60fps LK.effects.flashScreen(0x00ffff, 500); // Play frozen sound try { LK.getSound('frozen').play(); } catch (e) { console.log('Frozen sound play error:', e); } // Add ice tint to all chain balls for (var i = 0; i < chain.length; i++) { var chainBall = chain[i]; tween(chainBall, { tint: 0x88DDFF }, { duration: 300 }); } } else if (ball.specialType === 'rapid') { shooter.rapidFire = true; shooter.rapidFireTimer = 300; // 5 seconds at 60fps LK.effects.flashScreen(0xffff00, 200); } else if (ball.specialType === 'white_freeze') { chainFrozen = true; freezeTimer = 120; // 2 seconds at 60fps LK.effects.flashScreen(0xffffff, 500); // Play frozen sound try { LK.getSound('frozen').play(); } catch (e) { console.log('Frozen sound play error:', e); } // Create freeze countdown display first if (freezeCountdownDisplay) { freezeCountdownDisplay.destroy(); } freezeCountdownDisplay = new Text2('FROZEN: 2', { size: 80, fill: 0x00FFFF }); // Create transparent white overlay for entire freeze duration var whiteOverlay = LK.getAsset('centerCircle', { anchorX: 0, anchorY: 0, scaleX: 25, scaleY: 35 }); whiteOverlay.x = 0; whiteOverlay.y = 0; whiteOverlay.alpha = 0.3; whiteOverlay.tint = 0xFFFFFF; game.addChild(whiteOverlay); // Store reference to remove later freezeCountdownDisplay.whiteOverlay = whiteOverlay; freezeCountdownDisplay.anchor.set(0.5, 1); freezeCountdownDisplay.y = -50; LK.gui.bottom.addChild(freezeCountdownDisplay); // Add pulsing animation to the countdown display tween(freezeCountdownDisplay, { scaleX: 1.2, scaleY: 1.2 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { tween(freezeCountdownDisplay, { scaleX: 1.0, scaleY: 1.0 }, { duration: 300, easing: tween.easeInOut }); } }); // Add ice tint to all chain balls for (var i = 0; i < chain.length; i++) { var chainBall = chain[i]; tween(chainBall, { tint: 0x88DDFF }, { duration: 300 }); } } else if (ball.specialType === 'fire_blast') { // Fire blast effect - explode 3 balls on each side var ballIndex = chain.indexOf(ball); var blastRadius = 3; var toRemove = []; // Get 3 balls to the left and 3 balls to the right for (var i = Math.max(0, ballIndex - blastRadius); i < Math.min(chain.length, ballIndex + blastRadius + 1); i++) { if (chain[i] !== ball) { toRemove.push(chain[i]); } } // Store the first index of the blast to know where the gap starts var firstBlastIndex = Math.max(0, ballIndex - blastRadius); var trackSpacing = 7.0; // Track spacing to prevent overlap var gapSize = toRemove.length * trackSpacing; // Calculate gap size based on track spacing // Remove blasted balls for (var i = 0; i < toRemove.length; i++) { var index = chain.indexOf(toRemove[i]); if (index > -1) { chain.splice(index, 1); toRemove[i].destroy(); } } // Close the gap by moving all balls after the removed section forward for (var i = firstBlastIndex; i < chain.length; i++) { var ball = chain[i]; // Add comprehensive safety check for ball existence and trackPosition if (!ball || _typeof(ball) !== 'object') { // Skip invalid ball entries continue; } // Ensure trackPosition exists and is a valid number if (typeof ball.trackPosition !== 'number' || isNaN(ball.trackPosition)) { // Initialize trackPosition if undefined or invalid ball.trackPosition = i * 7.0; // Use index-based positioning } // Ensure gapSize is valid before using it if (typeof gapSize !== 'number' || isNaN(gapSize)) { gapSize = toRemove.length * 7.0; // Recalculate if invalid } // Now safely access trackPosition - additional check before modification if (typeof ball.trackPosition === 'number' && !isNaN(ball.trackPosition) && typeof gapSize === 'number' && !isNaN(gapSize)) { // Move the ball's track position forward to close the gap ball.trackPosition -= gapSize; } } // Comprehensive re-spacing to ensure no gaps remain // Start from the beginning and ensure each ball has proper spacing for (var i = 0; i < chain.length; i++) { var ball = chain[i]; // Safety check for ball existence if (!ball || _typeof(ball) !== 'object') { continue; } // Ensure trackPosition exists and is valid for current ball if (typeof ball.trackPosition !== 'number' || isNaN(ball.trackPosition)) { ball.trackPosition = i * 7.0; // Initialize with index-based positioning } if (i === 0) { // First ball keeps its current position as reference continue; } var previousBall = chain[i - 1]; // Safety check for previous ball if (!previousBall || _typeof(previousBall) !== 'object') { // Initialize trackPosition if invalid ball.trackPosition = i * 7.0; continue; } // Ensure previous ball has valid trackPosition if (typeof previousBall.trackPosition !== 'number' || isNaN(previousBall.trackPosition)) { previousBall.trackPosition = (i - 1) * 7.0; } var expectedPosition = previousBall.trackPosition + trackSpacing; // Always enforce proper spacing, moving balls forward to close any gaps ball.trackPosition = expectedPosition; // Position the ball immediately to detect matches faster positionBallOnTrack(ball, ball.trackPosition); // Animate the ball to its new position smoothly tween(ball, { x: ball.targetX, y: ball.targetY }, { duration: 200, // Faster animation for quicker match detection easing: tween.easeOut }); } // Immediately check for any consecutive same-color balls after repositioning var foundImmediateMatch = false; for (var i = 0; i < chain.length - 2; i++) { if (chain[i].color === chain[i + 1].color && chain[i + 1].color === chain[i + 2].color) { foundImmediateMatch = true; break; } } if (foundImmediateMatch) { // Trigger immediate match check LK.setTimeout(function () { checkMatches(); }, 5); // Very fast response for fire blast } // Create blast explosion effect LK.effects.flashScreen(0xFF4500, 500); // Add screen shake effect var originalX = game.x; var originalY = game.y; var shakeIntensity = 15; var shakeDuration = 400; var shakeTimer = 0; var _shakeEffect = function shakeEffect() { if (shakeTimer < shakeDuration) { game.x = originalX + (Math.random() - 0.5) * shakeIntensity; game.y = originalY + (Math.random() - 0.5) * shakeIntensity; shakeTimer += 16; LK.setTimeout(_shakeEffect, 16); } else { game.x = originalX; game.y = originalY; } }; _shakeEffect(); // Create blast text effect with swinging animation var blastText = new Text2('BoOoOMm !', { size: 120, fill: 0xFF4500 }); blastText.anchor.set(0.5, 0.5); blastText.x = ball.x; blastText.y = ball.y; blastText.scaleX = 0.5; blastText.scaleY = 0.5; blastText.rotation = -0.3; // Start with slight rotation game.addChild(blastText); // Animate blast text with swinging motion tween(blastText, { scaleX: 2.0, scaleY: 2.0, alpha: 0.8, rotation: 0.3 // Swing to the other side }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(blastText, { alpha: 0, scaleX: 1.5, scaleY: 1.5, rotation: -0.2 // Swing back slightly }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { blastText.destroy(); } }); } }); // Check for new matches after gap closure LK.setTimeout(function () { checkMatches(); }, 250); // Allow time for animations to settle } LK.getSound('powerup').play(); } // Insert ball into chain function insertBallIntoChain(ball, insertIndex) { var chainBall = new ChainBall(ball.color); chainBall.isSpecial = ball.isSpecial; chainBall.specialType = ball.specialType; chainBall.x = ball.x; chainBall.y = ball.y; chainBall.scaleX = 0.1; chainBall.scaleY = 0.1; game.addChild(chainBall); // Set consistent scale immediately chainBall.scaleX = 1.15; chainBall.scaleY = 1.15; // Animate ball insertion with consistent final size tween(chainBall, { scaleX: 1.15, scaleY: 1.15 }, { duration: 200, easing: tween.easeOut }); chain.splice(insertIndex, 0, chainBall); // Update chain indices for (var i = 0; i < chain.length; i++) { chain[i].chainIndex = i; } checkMatches(); } // Initialize game createTrackPath(); createChain(); // Start gameplay music LK.playMusic('Gameplay'); // Position hole at the end of the track - near the shooter hole.x = 1024; hole.y = 1200; // Position hole closer to shooter // Add breathing and rotation effects to the hole var _holeBreathingEffect = function holeBreathingEffect() { if (hole && hole.parent) { // Breathing effect - scale up and down tween(hole, { scaleX: 1.2, scaleY: 1.2 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { if (hole && hole.parent) { tween(hole, { scaleX: 1.0, scaleY: 1.0 }, { duration: 1500, easing: tween.easeInOut, onFinish: _holeBreathingEffect }); } } }); } }; // Start breathing effect _holeBreathingEffect(); // Continuous rotation effect var _holeRotationEffect = function holeRotationEffect() { if (hole && hole.parent) { hole.rotation += 0.02; // Rotate around its own axis LK.setTimeout(_holeRotationEffect, 16); // 60fps rotation } }; // Start rotation effect _holeRotationEffect(); // Score display - will be moved to bottom later var scoreTxt = new Text2('SCORE: 0', { size: 60, fill: 0xFFFFFF, font: "'Courier New Bold', 'Monaco Bold', 'Menlo Bold', 'Consolas Bold', 'DejaVu Sans Mono Bold', 'Lucida Console Bold', monospace" }); scoreTxt.anchor.set(0.5, 0); // Chain speed display - will be moved to bottom later var chainSpeedTxt = new Text2('Speed: 0.00', { size: 40, fill: 0xCCCCCC }); chainSpeedTxt.anchor.set(0.5, 0); // Add spaceship HUD status indicators // Chain status indicator var chainStatusTxt = new Text2('', { size: 30, fill: 0x00FF88 }); chainStatusTxt.anchor.set(0, 0); chainStatusTxt.x = 20; chainStatusTxt.y = 100; LK.gui.left.addChild(chainStatusTxt); // Weapon status indicator var weaponStatusTxt = new Text2('WEAPON: READY', { size: 30, fill: 0x00FF88 }); weaponStatusTxt.anchor.set(1, 1); weaponStatusTxt.x = -30; weaponStatusTxt.y = -110; LK.gui.bottom.addChild(weaponStatusTxt); // System status indicators var systemStatus1 = new Text2('', { size: 25, fill: 0x88CCFF }); systemStatus1.anchor.set(0, 0); systemStatus1.x = 20; systemStatus1.y = 150; LK.gui.left.addChild(systemStatus1); var systemStatus2 = new Text2('', { size: 25, fill: 0x88CCFF }); systemStatus2.anchor.set(1, 0); systemStatus2.x = -20; systemStatus2.y = 150; LK.gui.right.addChild(systemStatus2); // Targeting system indicator var targetingTxt = new Text2('TARGET: LOCKED', { size: 25, fill: 0xFFAA00 }); targetingTxt.anchor.set(0, 1); targetingTxt.x = 30; targetingTxt.y = -110; LK.gui.bottom.addChild(targetingTxt); // Navigation indicator var navTxt = new Text2('', { size: 25, fill: 0xFFAA00 }); navTxt.anchor.set(1, 0); navTxt.x = -20; navTxt.y = 200; LK.gui.right.addChild(navTxt); // Next ball preview var nextBallTxt = new Text2('NEXT BALL', { size: 30, fill: 0xFFFFFF }); nextBallTxt.anchor.set(0, 1); nextBallTxt.x = 30; nextBallTxt.y = -110; LK.gui.bottomLeft.addChild(nextBallTxt); // Add corner decorative elements for spaceship feel // Top corner indicators var topCornerIndicator1 = new Text2('◊', { size: 35, fill: 0x00AAFF }); topCornerIndicator1.anchor.set(0, 0); topCornerIndicator1.x = 180; topCornerIndicator1.y = 15; LK.gui.topLeft.addChild(topCornerIndicator1); var topCornerIndicator2 = new Text2('◊', { size: 35, fill: 0x00AAFF }); topCornerIndicator2.anchor.set(1, 0); topCornerIndicator2.x = -50; topCornerIndicator2.y = 15; LK.gui.topRight.addChild(topCornerIndicator2); // Bottom corner indicators var bottomCornerIndicator1 = new Text2('▲', { size: 25, fill: 0x00AAFF }); bottomCornerIndicator1.anchor.set(0, 1); bottomCornerIndicator1.x = 50; bottomCornerIndicator1.y = -15; LK.gui.bottomLeft.addChild(bottomCornerIndicator1); var bottomCornerIndicator2 = new Text2('▲', { size: 25, fill: 0x00AAFF }); bottomCornerIndicator2.anchor.set(1, 1); bottomCornerIndicator2.x = -50; bottomCornerIndicator2.y = -15; LK.gui.bottomRight.addChild(bottomCornerIndicator2); // Add pulsing animation to corner indicators var pulseCornerIndicators = function pulseCornerIndicators() { var indicators = [topCornerIndicator1, topCornerIndicator2, bottomCornerIndicator1, bottomCornerIndicator2]; for (var i = 0; i < indicators.length; i++) { if (indicators[i] && indicators[i].parent) { tween(indicators[i], { alpha: 0.3 }, { duration: 800 + i * 200, easing: tween.easeInOut, onFinish: function () { if (this && this.parent) { tween(this, { alpha: 1.0 }, { duration: 800, easing: tween.easeInOut }); } }.bind(indicators[i]) }); } } }; // Start corner indicator animation LK.setTimeout(pulseCornerIndicators, 1000); LK.setInterval(pulseCornerIndicators, 3000); // Create modern spaceship UI system // Add corner accent panels for spaceship aesthetics // Add animated scanner lines for spaceship feel var scannerLineLeft = LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.3, scaleY: 15 }); scannerLineLeft.tint = 0x00FFAA; scannerLineLeft.alpha = 0.8; scannerLineLeft.x = -80; scannerLineLeft.y = 0; LK.gui.left.addChild(scannerLineLeft); var scannerLineRight = LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.3, scaleY: 15 }); scannerLineRight.tint = 0x00FFAA; scannerLineRight.alpha = 0.8; scannerLineRight.x = 80; scannerLineRight.y = 0; LK.gui.right.addChild(scannerLineRight); // Animate scanner lines var _animateScannerLines = function animateScannerLines() { if (scannerLineLeft && scannerLineLeft.parent) { tween(scannerLineLeft, { alpha: 0.3 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (scannerLineLeft && scannerLineLeft.parent) { tween(scannerLineLeft, { alpha: 0.8 }, { duration: 1000, easing: tween.easeInOut, onFinish: _animateScannerLines }); } } }); } if (scannerLineRight && scannerLineRight.parent) { tween(scannerLineRight, { alpha: 0.3 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { if (scannerLineRight && scannerLineRight.parent) { tween(scannerLineRight, { alpha: 0.8 }, { duration: 1200, easing: tween.easeInOut }); } } }); } }; _animateScannerLines(); // Add header image asset at the top var gameHeader = LK.getAsset('game_header', { anchorX: 0.5, anchorY: 0 }); gameHeader.x = 0; gameHeader.y = 50; LK.gui.top.addChild(gameHeader); // Add modern score display to bottom panel scoreTxt.anchor.set(0.5, 1); scoreTxt.y = -180; // Moved higher up scoreTxt.size = 70; LK.gui.bottom.addChild(scoreTxt); // Add modern speed display to bottom panel chainSpeedTxt.anchor.set(0.5, 1); chainSpeedTxt.y = -35; chainSpeedTxt.size = 35; chainSpeedTxt.fill = 0xAADDFF; // Light blue color LK.gui.bottom.addChild(chainSpeedTxt); // Add remaining balls counter display next to speed text var remainingBallsTxt = new Text2('BALLS: 0', { size: 35, fill: 0xAADDFF // Light blue color matching speed text }); remainingBallsTxt.anchor.set(0, 1); remainingBallsTxt.x = 160; // Position further to the right of speed text remainingBallsTxt.y = -35; LK.gui.bottom.addChild(remainingBallsTxt); // Create difficulty label text var difficultyLabel = new Text2('SET THE DIFFICULT', { size: 30, fill: 0xCCCCCC }); difficultyLabel.anchor.set(1, 1); difficultyLabel.x = -30; difficultyLabel.y = -110; LK.gui.bottomRight.addChild(difficultyLabel); // Create accelerate button - repositioned for modern UI var accelerateBtn = new Text2('EASY', { size: 45, fill: 0x00FF00 }); accelerateBtn.anchor.set(1, 1); accelerateBtn.x = -30; accelerateBtn.y = -25; LK.gui.bottomRight.addChild(accelerateBtn); // Button press handler accelerateBtn.down = function (x, y, obj) { speedLevel = (speedLevel + 1) % 3; // Cycle through 0, 1, 2 if (speedLevel === 0) { accelerateBtn.setText('EASY'); accelerateBtn.fill = 0x00FF00; // Green } else if (speedLevel === 1) { accelerateBtn.setText('NORMAL'); accelerateBtn.fill = 0xFFFFFF; // White } else { accelerateBtn.setText('HARD'); accelerateBtn.fill = 0xFF0000; // Red } }; // Game input game.down = function (x, y, obj) { // Update crosshair position and aim crosshair.x = x; crosshair.y = y; shooter.aimAt(x, y); // Add crosshair click effect tween(crosshair, { scaleX: 1.5, scaleY: 1.5, alpha: 1.0 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(crosshair, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 200, easing: tween.easeIn }); } }); var ball = shooter.shoot(); if (ball) { flyingBalls.push(ball); game.addChild(ball); } }; // Mouse move handler to make shooter follow mouse and move crosshair game.move = function (x, y, obj) { // Update crosshair position crosshair.x = x; crosshair.y = y; // Aim shooter at crosshair position shooter.aimAt(x, y); }; // Main game loop game.update = function () { if (gameWon || gameLost) { // Clean up fire effects when game ends for (var i = 0; i < activeFireEffects.length; i++) { if (activeFireEffects[i] && activeFireEffects[i].parent) { tween.stop(activeFireEffects[i]); // Stop animations activeFireEffects[i].parent.removeChild(activeFireEffects[i]); } } activeFireEffects = []; // Clean up flying balls for (var i = 0; i < flyingBalls.length; i++) { flyingBalls[i].destroy(); } flyingBalls = []; return; } // Update chain updateChain(); // Update flying balls - limit processing if too many var maxFlyingBalls = 5; // Prevent too many balls in flight if (flyingBalls.length > maxFlyingBalls) { // Remove oldest balls if too many in flight var oldestBall = flyingBalls.shift(); oldestBall.destroy(); } for (var i = flyingBalls.length - 1; i >= 0; i--) { var ball = flyingBalls[i]; ball.x += ball.vx; ball.y += ball.vy; // Create rocket trail for every shot ball except white balls if (ball.createRocketTrail && !(ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze')) { ball.createRocketTrail(); } // Create trail effects for special balls - added behind the ball so ball renders on top if (ball.color === 'fire' && ball.isSpecial && ball.specialType === 'fire_blast') { // Play fire flying sound continuously while ball is moving if (LK.ticks % 20 === 0) { // Play sound every 20 frames (about 3 times per second) try { LK.getSound('fire_flying').play(); } catch (e) { console.log('Fire flying sound play error:', e); } } // Create fire trail effect - more frequent and larger if (LK.ticks % 2 === 0) { // Create multiple trails for more dramatic effect for (var trailIndex = 0; trailIndex < 3; trailIndex++) { var fireTrail = LK.getAsset('fire_effect', { anchorX: 0.5, anchorY: 0.5 }); // Create trails at different positions for spread effect var offsetX = (Math.random() - 0.5) * 40; var offsetY = (Math.random() - 0.5) * 40; fireTrail.x = ball.x - ball.vx * (0.3 + trailIndex * 0.2) + offsetX; fireTrail.y = ball.y - ball.vy * (0.3 + trailIndex * 0.2) + offsetY; fireTrail.scaleX = 0.8 + Math.random() * 0.6; // Much larger scale fireTrail.scaleY = 0.8 + Math.random() * 0.6; fireTrail.alpha = 0.9 - trailIndex * 0.2; fireTrail.tint = 0xFF6A00; // Brighter orange flame color for meteor effect // Add trail behind the ball in z-order var ballIndex = game.getChildIndex(ball); game.addChildAt(fireTrail, ballIndex); // Animate trail - fade out and scale down tween(fireTrail, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 400 + trailIndex * 100, easing: tween.easeOut, onFinish: function onFinish() { if (fireTrail && fireTrail.parent) { fireTrail.destroy(); } } }); } } } else if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') { // Create white rocket trail effect behind white balls - unique white trail if (LK.ticks % 2 === 0) { // Create distinctive white rocket trail effect var rocketTrail = LK.getAsset('fire_effect', { anchorX: 0.5, anchorY: 0.5 }); // Position trail behind the ball rocketTrail.x = ball.x - ball.vx * 0.8; rocketTrail.y = ball.y - ball.vy * 0.8; rocketTrail.scaleX = 0.7; // Slightly larger for white effect rocketTrail.scaleY = 0.7; rocketTrail.alpha = 0.9; // Higher alpha for more visible white effect rocketTrail.tint = 0xFFFFFF; // Pure white rocket flame color // Add trail behind the ball in z-order var ballIndex = game.getChildIndex(ball); game.addChildAt(rocketTrail, ballIndex); // Animate trail - distinctive white fade effect tween(rocketTrail, { scaleX: 0.3, scaleY: 0.3, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (rocketTrail && rocketTrail.parent) { rocketTrail.destroy(); } } }); } // Also create white snowflake particles for additional effect if (LK.ticks % 3 === 0) { // Create multiple white snowflakes for snow effect - less frequent for (var snowIndex = 0; snowIndex < 4; snowIndex++) { var snowflake = LK.getAsset('track', { anchorX: 0.5, anchorY: 0.5 }); // Create small white particles at different positions behind the ball var offsetX = (Math.random() - 0.5) * 50; var offsetY = (Math.random() - 0.5) * 50; snowflake.x = ball.x - ball.vx * (0.4 + snowIndex * 0.15) + offsetX; snowflake.y = ball.y - ball.vy * (0.4 + snowIndex * 0.15) + offsetY; snowflake.scaleX = 0.12 + Math.random() * 0.08; // Small varied snowflake particles snowflake.scaleY = 0.12 + Math.random() * 0.08; snowflake.alpha = 0.95 - snowIndex * 0.12; snowflake.tint = 0xFFFFFF; // Pure white snow color snowflake.rotation = Math.random() * Math.PI * 2; // Random rotation // Add snowflake behind the ball in z-order var ballIndex2 = game.getChildIndex(ball); game.addChildAt(snowflake, ballIndex2); // Animate snowflake - gentle drift and fade like floating snow particles tween(snowflake, { scaleX: 0.03, scaleY: 0.03, alpha: 0, x: snowflake.x + (Math.random() - 0.5) * 40, y: snowflake.y + Math.random() * 50 + 30, // Gentle drift motion like snow rotation: snowflake.rotation + (Math.random() - 0.5) * Math.PI * 1.5 }, { duration: 900 + snowIndex * 120, easing: tween.easeOut, onFinish: function onFinish() { if (snowflake && snowflake.parent) { snowflake.destroy(); } } }); } } } // Check collision with chain - optimized detection var inserted = false; var collisionIndex = -1; var ballRadius = 34.5; // Approximate ball radius (increased for 35% larger balls) // Only check balls within reasonable distance for (var j = 0; j < chain.length; j++) { var chainBall = chain[j]; // Quick distance check before expensive intersects call var dx = ball.x - chainBall.x; var dy = ball.y - chainBall.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < ballRadius * 2 && ball.intersects(chainBall)) { collisionIndex = j; break; } } // If collision detected, handle special balls or insert into chain if (collisionIndex !== -1) { // Check if the flying ball is a white ball - trigger freeze effect and disappear if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') { handleSpecialBall(ball); // Clean up rocket trail effects if (ball.rocketTrail) { for (var t = 0; t < ball.rocketTrail.length; t++) { if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) { ball.rocketTrail[t].destroy(); } } ball.rocketTrail = []; } ball.destroy(); flyingBalls.splice(i, 1); inserted = true; } else if (ball.color === 'fire' && ball.isSpecial && ball.specialType === 'fire_blast') { // Fire ball - trigger blast effect on the first ball it hits var hitBall = chain[collisionIndex]; // Temporarily set the hit ball as the fire ball for the blast effect hitBall.isSpecial = true; hitBall.specialType = 'fire_blast'; handleSpecialBall(hitBall); // Clean up rocket trail effects if (ball.rocketTrail) { for (var t = 0; t < ball.rocketTrail.length; t++) { if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) { ball.rocketTrail[t].destroy(); } } ball.rocketTrail = []; } ball.destroy(); flyingBalls.splice(i, 1); inserted = true; } else { // Regular ball - insert into chain with collision avoidance var insertIndex = collisionIndex + 1; var trackSpacing = 7.0; // Increased spacing to prevent overlap // Create more space by pushing balls further back var pushBackDistance = trackSpacing * 1.5; // Extra space to prevent overlap // Adjust track positions for all balls after insertion point for (var k = insertIndex; k < chain.length; k++) { chain[k].trackPosition += pushBackDistance; // Move balls back to make space } insertBallIntoChain(ball, insertIndex); // Set the inserted ball's track position with proper spacing if (insertIndex < chain.length) { chain[insertIndex].trackPosition = chain[collisionIndex].trackPosition + trackSpacing; positionBallOnTrack(chain[insertIndex], chain[insertIndex].trackPosition); } // Re-space all balls after insertion to maintain equal intervals with extra spacing for (var k = insertIndex + 1; k < chain.length; k++) { var expectedPosition = chain[k - 1].trackPosition + trackSpacing; // Enforce minimum spacing to prevent overlap with buffer chain[k].trackPosition = Math.max(chain[k].trackPosition, expectedPosition + 1.0); } // Additional pass to ensure no overlapping after insertion for (var k = 0; k < chain.length - 1; k++) { var currentBall = chain[k]; var nextBall = chain[k + 1]; var dx = currentBall.x - nextBall.x; var dy = currentBall.y - nextBall.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 69) { // Push next ball further back nextBall.trackPosition += (69 - distance) / trackSpacing; positionBallOnTrack(nextBall, nextBall.trackPosition); } } // Clean up rocket trail effects if (ball.rocketTrail) { for (var t = 0; t < ball.rocketTrail.length; t++) { if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) { ball.rocketTrail[t].destroy(); } } ball.rocketTrail = []; } ball.destroy(); flyingBalls.splice(i, 1); inserted = true; } } // Remove if off screen if (!inserted && (ball.x < -100 || ball.x > 2148 || ball.y < -100 || ball.y > 2832)) { // Deduct 10 points for missing the chain - always allow negative scores var currentScore = LK.getScore(); LK.setScore(currentScore - 10); // Display -10 points at ball position var minusPointsText = new Text2('-10', { size: 60, fill: 0xFF4444 }); minusPointsText.anchor.set(0.5, 0.5); minusPointsText.x = ball.x; minusPointsText.y = ball.y; minusPointsText.alpha = 1.0; minusPointsText.scaleX = 0.5; minusPointsText.scaleY = 0.5; game.addChild(minusPointsText); // Animate the minus points display tween(minusPointsText, { scaleX: 1.2, scaleY: 1.2, y: ball.y - 80, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { minusPointsText.destroy(); } }); // Clean up rocket trail effects if (ball.rocketTrail) { for (var t = 0; t < ball.rocketTrail.length; t++) { if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) { ball.rocketTrail[t].destroy(); } } ball.rocketTrail = []; } ball.destroy(); flyingBalls.splice(i, 1); } } // Update shooter shooter.update(); // Update UI scoreTxt.setText('SCORE: ' + LK.getScore()); chainSpeedTxt.setText('Speed: ' + (chainSpeed * 1000).toFixed(2)); remainingBallsTxt.setText('BALLS: ' + chain.length); // Keep crosshair on top of everything else if (crosshair && crosshair.parent) { game.addChild(crosshair); // Moves crosshair to front by re-adding it } // Update spaceship HUD status based on game state if (chainStatusTxt) { if (chainFrozen) { chainStatusTxt.setText(''); chainStatusTxt.fill = 0x00DDFF; } else if (chain.length < 5) { chainStatusTxt.setText(''); chainStatusTxt.fill = 0xFF4444; } else { chainStatusTxt.setText(''); chainStatusTxt.fill = 0x00FF88; } } if (weaponStatusTxt) { if (shooter.rapidFire) { weaponStatusTxt.setText('WEAPON: RAPID'); weaponStatusTxt.fill = 0xFFFF00; } else if (shooter.shootCooldown > 0) { var reloadSeconds = (shooter.shootCooldown / 60).toFixed(1); weaponStatusTxt.setText('WEAPON: RELOAD ' + reloadSeconds + 's'); weaponStatusTxt.fill = 0xFFAA00; } else { weaponStatusTxt.setText('WEAPON: READY'); weaponStatusTxt.fill = 0x00FF88; } } if (systemStatus1) { var powerLevel = Math.max(0, 100 - speedLevel * 25); systemStatus1.setText(''); if (powerLevel > 50) { systemStatus1.fill = 0x88CCFF; } else { systemStatus1.fill = 0xFFAA00; } } if (targetingTxt) { if (flyingBalls.length > 0) { targetingTxt.setText('TARGET: FIRING'); targetingTxt.fill = 0xFF4444; } else { targetingTxt.setText('TARGET: LOCKED'); targetingTxt.fill = 0xFFAA00; } } // Show next ball preview with larger size and higher position if (shooter.nextBall) { if (shooter.nextBall.parent) { shooter.nextBall.parent.removeChild(shooter.nextBall); } shooter.nextBall.x = 70; // Moved further right from 50 to 70 shooter.nextBall.y = -60; shooter.nextBall.scaleX = 1.2; // Increased from 0.8 to 1.2 for more prominence shooter.nextBall.scaleY = 1.2; // Increased from 0.8 to 1.2 for more prominence LK.gui.bottomLeft.addChild(shooter.nextBall); } // Increase chain speed over time (slower increase) normalChainSpeed += 0.00003; // Update other speeds to maintain ratios fastChainSpeed = normalChainSpeed * 6.67; // Maintain same ratio as before veryFastChainSpeed = normalChainSpeed * 20; // Maintain same ratio as before if (speedLevel === 0) { chainSpeed = normalChainSpeed; } else if (speedLevel === 1) { chainSpeed = fastChainSpeed; } else { chainSpeed = veryFastChainSpeed; } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Ball = Container.expand(function (color) {
var self = Container.call(this);
// Safety check to prevent black balls
if (color === 'black') {
color = 'red'; // Default to red if black is passed
}
self.color = color;
self.ballColors = ['red', 'blue', 'green', 'yellow', 'pink'];
self.isSpecial = false;
self.specialType = null; // 'explosive', 'freeze', 'rapid', 'white_freeze', 'black_destroyer'
var ballGraphics = self.attachAsset('ball_' + color, {
anchorX: 0.5,
anchorY: 0.5
});
// Add continuous rotation animation to the ball
var rotateSpeed = 0.02 + Math.random() * 0.03; // Random rotation speed between 0.02 and 0.05
var rotationDirection = Math.random() < 0.5 ? 1 : -1; // Random direction
var _rotateBall = function rotateBall() {
if (ballGraphics && ballGraphics.parent) {
ballGraphics.rotation += rotateSpeed * rotationDirection;
LK.setTimeout(_rotateBall, 16); // 60fps rotation
}
};
// Start rotation animation
_rotateBall();
// Check for special ball types based on color
if (color === 'white') {
self.isSpecial = true;
self.specialType = 'white_freeze';
ballGraphics.alpha = 0.9;
// Stop rotation for white balls
ballGraphics.rotation = 0;
var _whiteRotateStop2 = function _whiteRotateStop() {
if (ballGraphics && ballGraphics.parent) {
ballGraphics.rotation = 0;
LK.setTimeout(_whiteRotateStop2, 16);
}
};
_whiteRotateStop2();
} else if (color === 'fire') {
self.isSpecial = true;
self.specialType = 'fire_blast';
ballGraphics.alpha = 0.9;
// Apply lighter tint to make it look like a bright meteor
ballGraphics.tint = 0xFFBB44; // Bright golden-orange like a meteor
} else {
// 5% chance for special balls on regular colors
if (Math.random() < 0.05) {
self.isSpecial = true;
var specials = ['explosive', 'freeze', 'rapid'];
self.specialType = specials[Math.floor(Math.random() * specials.length)];
ballGraphics.alpha = 0.9;
// Add glow effect for special balls
if (self.specialType === 'explosive') {
ballGraphics.tint = 0xFF4500;
} else if (self.specialType === 'freeze') {
ballGraphics.tint = 0x00FFFF;
} else if (self.specialType === 'rapid') {
ballGraphics.tint = 0xFFFF00;
}
}
}
self.trackPosition = 0;
self.trackX = 0;
self.trackY = 0;
self.isMoving = false;
return self;
});
var ChainBall = Ball.expand(function (color) {
var self = Ball.call(this, color);
self.chainIndex = 0;
self.targetX = 0;
self.targetY = 0;
// Stop the rotation animation for chain balls
var ballGraphics = self.children[0]; // Get the ball graphics
if (ballGraphics) {
ballGraphics.rotation = 0; // Reset rotation to 0
}
self.update = function () {
if (self.isMoving) {
// Smooth movement towards target position
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
self.x += dx * 0.3;
self.y += dy * 0.3;
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
self.isMoving = false;
self.x = self.targetX;
self.y = self.targetY;
}
}
// Keep chain balls from rotating by maintaining rotation at 0
if (ballGraphics) {
ballGraphics.rotation = 0;
}
// Ensure consistent size for all chain balls
self.scaleX = 1.15;
self.scaleY = 1.15;
};
return self;
});
var Shooter = Container.expand(function () {
var self = Container.call(this);
// Simple shooter image asset only
var shooterGraphics = self.attachAsset('shooter', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = 0;
self.currentBall = null;
self.nextBall = null;
self.rapidFire = false;
self.rapidFireTimer = 0;
self.shootCooldown = 0;
self.loadBall = function () {
if (self.currentBall) {
self.currentBall.destroy();
}
self.currentBall = self.nextBall;
if (self.currentBall) {
self.currentBall.x = 0;
self.currentBall.y = -20; // Move ball closer to barrel
self.currentBall.scaleX = 1.4375; // 43.75% larger (25% + 15%)
self.currentBall.scaleY = 1.4375; // 43.75% larger (25% + 15%)
self.addChild(self.currentBall);
}
// Generate next ball
var colors = ['red', 'blue', 'green', 'yellow', 'pink'];
var randomColor;
// Check if it's time for a special ball (every 10 shots)
if (shotCounter > 0 && shotCounter % 10 === 0) {
// Randomly choose between white freeze ball or fire blast ball
if (Math.random() < 0.5) {
randomColor = 'white'; // White freeze ball
} else {
randomColor = 'fire'; // Fire blast ball
}
} else {
randomColor = colors[Math.floor(Math.random() * colors.length)];
}
// Safety check to ensure we never create black balls
if (randomColor === 'black' || !randomColor) {
randomColor = colors[Math.floor(Math.random() * colors.length)]; // Default to valid colors
}
self.nextBall = new Ball(randomColor);
};
self.aimAt = function (x, y) {
var dx = x - self.x;
var dy = y - self.y;
self.angle = Math.atan2(dy, dx);
self.rotation = self.angle;
};
self.shoot = function () {
if (self.shootCooldown > 0 || !self.currentBall) return null;
var ball = self.currentBall;
self.removeChild(ball);
// Maintain consistent ball scale when firing
ball.scaleX = 1.15;
ball.scaleY = 1.15;
// Set ball velocity
var speed = 8;
ball.vx = Math.cos(self.angle) * speed;
ball.vy = Math.sin(self.angle) * speed;
ball.x = self.x;
ball.y = self.y;
// Add rocket trail effect to every shot ball
ball.rocketTrail = [];
ball.createRocketTrail = function () {
// Create rocket trail effect behind the ball - different for white balls
if (LK.ticks % 2 === 0) {
// Create trail every 2 frames
var trailEffect = LK.getAsset('fire_effect', {
anchorX: 0.5,
anchorY: 0.5
});
// Position trail behind the ball
trailEffect.x = ball.x - ball.vx * 0.8;
trailEffect.y = ball.y - ball.vy * 0.8;
trailEffect.scaleX = 0.6;
trailEffect.scaleY = 0.6;
trailEffect.alpha = 0.8;
// White balls get pure white trail, others get orange
if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') {
trailEffect.tint = 0xFFFFFF; // Pure white for white balls
trailEffect.scaleX = 0.7; // Slightly larger
trailEffect.scaleY = 0.7;
trailEffect.alpha = 0.9; // Brighter
} else {
trailEffect.tint = 0xFF8844; // Orange rocket flame color for regular balls
}
// Add trail behind the ball in z-order
var ballIndex = game.getChildIndex(ball);
game.addChildAt(trailEffect, ballIndex);
// Store trail reference
ball.rocketTrail.push(trailEffect);
// Animate trail - fade out and scale down
tween(trailEffect, {
scaleX: 0.2,
scaleY: 0.2,
alpha: 0
}, {
duration: ball.color === 'white' ? 400 : 300,
easing: tween.easeOut,
onFinish: function onFinish() {
if (trailEffect && trailEffect.parent) {
trailEffect.destroy();
}
// Remove from trail array
var index = ball.rocketTrail.indexOf(trailEffect);
if (index > -1) {
ball.rocketTrail.splice(index, 1);
}
}
});
}
};
// Increment shot counter
shotCounter++;
self.loadBall();
// Create optimized fire effect only if under limit and not in rapid fire
if (activeFireEffects.length < maxFireEffects && !self.rapidFire) {
var fireEffect = getFireEffect();
game.addChild(fireEffect);
activeFireEffects.push(fireEffect);
// Position fire effect at the exact tip of the 200x200 shooter barrel - moved upward
var barrelEndX = self.x + Math.cos(self.angle) * 100;
var barrelEndY = self.y + Math.sin(self.angle) * 100 - 30; // Moved 30 pixels upward
fireEffect.x = barrelEndX;
fireEffect.y = barrelEndY;
fireEffect.rotation = self.angle + Math.PI / 2; // Rotate 90 degrees to the right
// Animate fire effect - expand and fade out quickly
tween(fireEffect, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
returnFireEffect(fireEffect);
}
});
}
// Set cooldown to 1.5 seconds (90 frames at 60fps)
self.shootCooldown = 90;
// Play shoot sound immediately - different sound for white balls
try {
if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') {
LK.getSound('white_shoot').play(); // Use dedicated white ball shooting sound
} else {
LK.getSound('shoot').play();
}
} catch (e) {
console.log('Sound play error:', e);
}
return ball;
};
self.update = function () {
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
if (self.rapidFire) {
self.rapidFireTimer--;
if (self.rapidFireTimer <= 0) {
self.rapidFire = false;
// Remove glow effect
tween.stop(self, {
tint: true
});
self.tint = 0xFFFFFF;
} else {
// Maintain glow effect
if (self.tint === 0xFFFFFF) {
tween(self, {
tint: 0xFFFF88
}, {
duration: 200,
easing: tween.easeInOut
});
}
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0a0a1a
});
/****
* Game Code
****/
// Fire effect pool management
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function getFireEffect() {
var fireEffect;
if (fireEffectPool.length > 0) {
fireEffect = fireEffectPool.pop();
fireEffect.alpha = 0.9;
fireEffect.scaleX = 0.8;
fireEffect.scaleY = 0.8;
tween.stop(fireEffect); // Stop any ongoing animations
} else {
fireEffect = LK.getAsset('fire_effect', {
anchorX: 0.5,
anchorY: 0.5
});
}
return fireEffect;
}
function returnFireEffect(fireEffect) {
if (fireEffect && fireEffect.parent) {
fireEffect.parent.removeChild(fireEffect);
}
var index = activeFireEffects.indexOf(fireEffect);
if (index > -1) {
activeFireEffects.splice(index, 1);
}
// Reset fire effect properties to prevent animation conflicts
fireEffect.alpha = 0.9;
fireEffect.scaleX = 0.8;
fireEffect.scaleY = 0.8;
fireEffect.rotation = 0;
tween.stop(fireEffect); // Stop any ongoing animations
if (fireEffectPool.length < maxPoolSize) {
// Limit pool size to prevent memory buildup
fireEffectPool.push(fireEffect);
}
}
// Create dynamic space background with shooting stars
function createSpaceBackground() {
// Create complex starfield with realistic star colors and sizes (no colored squares)
var starColors = [0xFFFFFF, 0xFFE4B5, 0xFFB347, 0x87CEEB, 0xF0F8FF, 0xFFF8DC];
var starSizes = [0.5, 0.8, 1.2, 1.8, 2.5, 3.2];
var holeX = 1024; // Black hole position
var holeY = 1200;
for (var layer = 0; layer < 5; layer++) {
var numStars = layer === 0 ? 200 : layer === 1 ? 150 : layer === 2 ? 100 : layer === 3 ? 60 : 30;
var baseAlpha = layer === 0 ? 0.2 : layer === 1 ? 0.4 : layer === 2 ? 0.6 : layer === 3 ? 0.8 : 1.0;
for (var i = 0; i < numStars; i++) {
var starSize = starSizes[Math.floor(Math.random() * starSizes.length)];
var star = game.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: starSize * 0.1,
scaleY: starSize * 0.1
}));
star.x = Math.random() * 2048;
star.y = Math.random() * 2732;
star.alpha = baseAlpha + Math.random() * 0.3;
star.tint = starColors[Math.floor(Math.random() * starColors.length)];
// Calculate distance from black hole for vortex effect
var dx = star.x - holeX;
var dy = star.y - holeY;
var distanceFromHole = Math.sqrt(dx * dx + dy * dy);
// Store original position for vortex animation
star.originalX = star.x;
star.originalY = star.y;
star.vortexSpeed = 0.3 + Math.random() * 0.5; // Individual vortex speed
star.vortexRadius = distanceFromHole;
star.vortexAngle = Math.atan2(dy, dx);
// Add realistic twinkling based on star size and distance
if (layer >= 2 && Math.random() < 0.4) {
var twinkleDelay = Math.random() * 3000;
LK.setTimeout(function () {
var _twinkleEffect = function twinkleEffect() {
if (star && star.parent) {
tween(star, {
alpha: star.alpha * 0.2
}, {
duration: 1000 + Math.random() * 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (star && star.parent) {
tween(star, {
alpha: baseAlpha + Math.random() * 0.3
}, {
duration: 1000 + Math.random() * 800,
easing: tween.easeInOut,
onFinish: _twinkleEffect
});
}
}
});
}
};
_twinkleEffect();
}, twinkleDelay);
}
}
}
// Create space debris being pulled into black hole
for (var i = 0; i < 20; i++) {
var debris = game.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8 + Math.random() * 1.2,
scaleY: 0.8 + Math.random() * 1.2
}));
// Start debris at random positions around the edge
var angle = Math.random() * Math.PI * 2;
var radius = 800 + Math.random() * 600;
debris.x = holeX + Math.cos(angle) * radius;
debris.y = holeY + Math.sin(angle) * radius;
debris.alpha = 0.4 + Math.random() * 0.4;
debris.tint = 0x666666; // Gray space debris color
debris.rotation = Math.random() * Math.PI * 2;
debris.vortexSpeed = 0.8 + Math.random() * 0.4;
debris.originalRadius = radius;
debris.vortexAngle = angle;
// Animate debris spiraling into black hole
var animateDebris = function animateDebris(debrisObj) {
var _spiralToBlackHole = function spiralToBlackHole() {
if (debrisObj && debrisObj.parent) {
// Spiral inward while rotating around black hole
debrisObj.vortexAngle += debrisObj.vortexSpeed * 0.02;
debrisObj.originalRadius -= debrisObj.vortexSpeed * 0.8; // Spiral inward
debrisObj.rotation += 0.05; // Rotate the debris itself
// Update position
debrisObj.x = holeX + Math.cos(debrisObj.vortexAngle) * debrisObj.originalRadius;
debrisObj.y = holeY + Math.sin(debrisObj.vortexAngle) * debrisObj.originalRadius;
// Fade out as it gets closer to black hole
var fadeProgress = 1 - debrisObj.originalRadius / 1400;
debrisObj.alpha = 0.4 * (1 - fadeProgress);
debrisObj.scaleX *= 0.999; // Gradually shrink
debrisObj.scaleY *= 0.999;
// Reset debris when it gets too close to black hole
if (debrisObj.originalRadius < 50 || debrisObj.alpha < 0.1) {
// Respawn at edge
var newAngle = Math.random() * Math.PI * 2;
var newRadius = 800 + Math.random() * 600;
debrisObj.x = holeX + Math.cos(newAngle) * newRadius;
debrisObj.y = holeY + Math.sin(newAngle) * newRadius;
debrisObj.originalRadius = newRadius;
debrisObj.vortexAngle = newAngle;
debrisObj.alpha = 0.4 + Math.random() * 0.4;
debrisObj.scaleX = 0.8 + Math.random() * 1.2;
debrisObj.scaleY = 0.8 + Math.random() * 1.2;
}
LK.setTimeout(_spiralToBlackHole, 16); // 60fps animation
}
};
_spiralToBlackHole();
};
animateDebris(debris);
}
// Cosmic dust clouds removed - only keep the game hole
// Nebula formations removed - only keep the game hole
}
// Initialize space background
createSpaceBackground();
// Create shooting star function
function createShootingStar() {
var shootingStar = game.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.3
}));
// Random starting position from top or sides
var startSide = Math.floor(Math.random() * 3); // 0 = top, 1 = left, 2 = right
if (startSide === 0) {
shootingStar.x = Math.random() * 2048;
shootingStar.y = -50;
} else if (startSide === 1) {
shootingStar.x = -50;
shootingStar.y = Math.random() * 1500;
} else {
shootingStar.x = 2098;
shootingStar.y = Math.random() * 1500;
}
// Set shooting star properties
shootingStar.alpha = 0.8;
shootingStar.tint = 0xFFFFFF;
shootingStar.rotation = Math.random() * Math.PI * 2;
// Create trail effect with multiple small stars
var trailStars = [];
for (var i = 0; i < 5; i++) {
var trailStar = game.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25 - i * 0.035,
scaleY: 0.12 - i * 0.018
}));
trailStar.x = shootingStar.x;
trailStar.y = shootingStar.y;
trailStar.alpha = 0.6 - i * 0.1;
trailStar.tint = 0xFFFFFF;
trailStar.rotation = shootingStar.rotation;
trailStars.push(trailStar);
}
// Calculate movement direction
var targetX = Math.random() * 2048;
var targetY = 2732 + 200;
var deltaX = targetX - shootingStar.x;
var deltaY = targetY - shootingStar.y;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var speed = 15 + Math.random() * 10;
var vx = deltaX / distance * speed;
var vy = deltaY / distance * speed;
// Animate shooting star
var _moveShootingStar = function moveShootingStar() {
if (shootingStar && shootingStar.parent) {
shootingStar.x += vx;
shootingStar.y += vy;
// Update trail positions
for (var i = trailStars.length - 1; i > 0; i--) {
if (trailStars[i] && trailStars[i].parent) {
trailStars[i].x = trailStars[i - 1].x;
trailStars[i].y = trailStars[i - 1].y;
}
}
if (trailStars[0] && trailStars[0].parent) {
trailStars[0].x = shootingStar.x;
trailStars[0].y = shootingStar.y;
}
// Check if off screen
if (shootingStar.x < -100 || shootingStar.x > 2148 || shootingStar.y > 2832) {
shootingStar.destroy();
for (var i = 0; i < trailStars.length; i++) {
if (trailStars[i] && trailStars[i].parent) {
trailStars[i].destroy();
}
}
return;
}
LK.setTimeout(_moveShootingStar, 16);
}
};
_moveShootingStar();
}
// Start shooting star timer (every 3 seconds for more frequency)
var shootingStarTimer = LK.setInterval(createShootingStar, 3000);
// Create continuous vortex effect for background stars
function animateBackgroundVortex() {
var holeX = 1024;
var holeY = 1200;
var _updateVortex = function updateVortex() {
// Vortex animation disabled - stars now move directly toward hole
LK.setTimeout(_updateVortex, 33); // Keep timer running but do nothing
};
_updateVortex();
}
// Start background vortex animation
animateBackgroundVortex();
// Enhanced star movement toward hole with spiral pattern
function animateStarsTowardHole() {
var holeX = 1024; // Black hole position
var holeY = 1200;
var _updateStarMovement = function updateStarMovement() {
// Update all background stars with direct vacuum pull toward hole
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
// Check if this is a background star (has original position properties)
if (child && child.originalX !== undefined && child.originalY !== undefined) {
// Calculate direction toward black hole
var dx = holeX - child.x;
var dy = holeY - child.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Stars move much faster as they get closer to the hole (vacuum effect)
var baseSpeed = 2.5 + (1 - Math.min(distance / 600, 1)) * 4;
// Direct movement toward hole - no spiral, straight vacuum pull
if (distance > 50) {
child.x += dx / distance * baseSpeed;
child.y += dy / distance * baseSpeed;
}
// When star gets very close to hole, respawn it at edge
if (distance < 80) {
// Respawn at random edge position
var edge = Math.floor(Math.random() * 4);
if (edge === 0) {
// Top edge
child.x = Math.random() * 2048;
child.y = -50;
} else if (edge === 1) {
// Right edge
child.x = 2098;
child.y = Math.random() * 2732;
} else if (edge === 2) {
// Bottom edge
child.x = Math.random() * 2048;
child.y = 2782;
} else {
// Left edge
child.x = -50;
child.y = Math.random() * 2732;
}
// Update original position for new spawn
child.originalX = child.x;
child.originalY = child.y;
}
}
}
LK.setTimeout(_updateStarMovement, 16); // 60fps animation
};
_updateStarMovement();
}
// Start enhanced star movement animation
animateStarsTowardHole();
// Create Superman flying across background
function createFlyingSuperman() {
var superman = game.addChild(LK.getAsset('superman', {
anchorX: 0.5,
anchorY: 0.5
}));
// Random starting position from left edge
superman.x = -200;
superman.y = 200 + Math.random() * 1500; // Random height
superman.alpha = 0.8;
superman.scaleX = 1.0;
superman.scaleY = 1.0;
// Random flight path - either straight across or slight curve
var targetX = 2248; // Off-screen right
var targetY = superman.y + (Math.random() - 0.5) * 400; // Slight vertical variation
var flightDuration = 3000 + Math.random() * 2000; // 3-5 seconds flight time
// Schedule laser attack when Superman is in middle of screen
var laserAttackTime = flightDuration * 0.4; // Attack when 40% across screen
LK.setTimeout(function () {
if (superman && superman.parent && chain.length > 0) {
// Get 2 random balls from chain to destroy - with safety checks
var ballsToDestroy = [];
var maxBalls = Math.min(2, chain.length);
// Get random indices for balls to destroy with better validation
var usedIndices = [];
var attempts = 0;
for (var i = 0; i < maxBalls && attempts < 10; i++) {
var randomIndex;
do {
randomIndex = Math.floor(Math.random() * chain.length);
attempts++;
} while (usedIndices.indexOf(randomIndex) !== -1 && attempts < 10);
if (attempts < 10 && chain[randomIndex] && chain[randomIndex].parent) {
usedIndices.push(randomIndex);
ballsToDestroy.push(chain[randomIndex]);
}
}
// Play Superman laser sound
try {
LK.getSound('superman_laser').play();
} catch (e) {
console.log('Superman laser sound play error:', e);
}
// Create laser beams from Superman's eyes to each target ball
for (var i = 0; i < ballsToDestroy.length; i++) {
var targetBall = ballsToDestroy[i];
// Create laser beam effect using dedicated laser beam asset
var laserBeam = LK.getAsset('laser_beam', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.0,
scaleY: 1.0
});
laserBeam.tint = 0xFF0000; // Red laser color
laserBeam.alpha = 0.9;
// Store reference to Superman and target for continuous tracking
laserBeam.superman = superman;
laserBeam.targetBall = targetBall;
// Position laser at Superman's eye level
var eyeX = superman.x + 20; // Adjusted offset for eye position
var eyeY = superman.y - 15; // Adjusted for more accurate eye position
laserBeam.x = eyeX;
laserBeam.y = eyeY;
// Calculate angle and length to target ball
var dx = targetBall.x - eyeX;
var dy = targetBall.y - eyeY;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
laserBeam.rotation = angle;
laserBeam.scaleX = distance / 25; // Scale to reach target
// Add update function to continuously follow Superman
laserBeam.update = function () {
if (this.superman && this.superman.parent && this.targetBall && this.targetBall.parent) {
// Update laser position to Superman's current eye position
var currentEyeX = this.superman.x + 20; // Adjusted offset for eye position
var currentEyeY = this.superman.y - 15; // Adjusted for more accurate eye position
this.x = currentEyeX;
this.y = currentEyeY;
// Recalculate angle and distance to target
var dx = this.targetBall.x - currentEyeX;
var dy = this.targetBall.y - currentEyeY;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
this.rotation = angle;
this.scaleX = distance / 25;
}
};
game.addChild(laserBeam);
// Use closure to properly capture laserBeam reference for each iteration
(function (currentLaser) {
// Animate laser beam appearance and disappearance
tween(currentLaser, {
alpha: 1.0,
scaleY: 0.8
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(currentLaser, {
alpha: 0,
scaleY: 0.2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
if (currentLaser && currentLaser.parent) {
currentLaser.parent.removeChild(currentLaser);
currentLaser.destroy();
}
}
});
}
});
})(laserBeam);
// Destroy the target ball with explosion effect
LK.setTimeout(function () {
// Validate ball still exists and is in chain before destroying
if (targetBall && targetBall.parent && chain.indexOf(targetBall) > -1) {
// Create explosion effect at ball position
LK.effects.flashObject(targetBall, 0xFF0000, 300);
// Remove ball from chain with proper index validation
var ballIndex = chain.indexOf(targetBall);
if (ballIndex > -1 && ballIndex < chain.length) {
// Store track position for gap closure
var removedBallTrackPosition = targetBall.trackPosition;
// Remove ball from chain
chain.splice(ballIndex, 1);
targetBall.destroy();
// Close gap by moving all balls after the removed ball forward
var trackSpacing = 7.0;
for (var k = ballIndex; k < chain.length; k++) {
if (chain[k] && typeof chain[k].trackPosition === 'number') {
chain[k].trackPosition -= trackSpacing;
positionBallOnTrack(chain[k], chain[k].trackPosition);
}
}
}
// Check for new matches after ball removal with validation
LK.setTimeout(function () {
if (chain && chain.length > 0) {
checkMatches();
}
}, 150); // Slightly longer delay for proper positioning
}
}, 250); // Slightly longer delay to ensure laser is visible
}
}
}, laserAttackTime);
// Play Superman flying sound
try {
LK.getSound('superman_flying').play();
} catch (e) {
console.log('Superman flying sound play error:', e);
}
// Animate Superman flying across screen
tween(superman, {
x: targetX,
y: targetY,
alpha: 0.9
}, {
duration: flightDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
superman.destroy();
}
});
// Add cape fluttering effect
var _capeFlutter = function capeFlutter() {
if (superman && superman.parent) {
tween(superman, {
scaleX: 1.05 + Math.random() * 0.1,
scaleY: 0.98 + Math.random() * 0.04
}, {
duration: 200 + Math.random() * 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (superman && superman.parent) {
tween(superman, {
scaleX: 0.95 + Math.random() * 0.1,
scaleY: 1.02 + Math.random() * 0.04
}, {
duration: 200 + Math.random() * 100,
easing: tween.easeInOut,
onFinish: _capeFlutter
});
}
}
});
}
};
_capeFlutter();
}
// Start Superman timer (random intervals between 8-15 seconds)
function scheduleNextSuperman() {
var nextInterval = 8000 + Math.random() * 7000; // 8-15 seconds
LK.setTimeout(function () {
createFlyingSuperman();
scheduleNextSuperman(); // Schedule next Superman
}, nextInterval);
}
// Start the Superman scheduling
scheduleNextSuperman();
// Start Villain background appearance scheduling (separate from villain actions)
function scheduleNextVillainAppearance() {
var nextInterval = 12000 + Math.random() * 8000; // 12-20 seconds
LK.setTimeout(function () {
createBackgroundVillain();
scheduleNextVillainAppearance(); // Schedule next appearance
}, nextInterval);
}
// Create background villain that just flies across without adding balls
function createBackgroundVillain() {
var backgroundVillain = game.addChild(LK.getAsset('villain', {
anchorX: 0.5,
anchorY: 0.5
}));
// Add surfboard underneath background villain
var backgroundSurfboard = game.addChild(LK.getAsset('surfboard', {
anchorX: 0.5,
anchorY: 0.5
}));
// Random starting position from right edge
backgroundVillain.x = 2248;
backgroundVillain.y = 300 + Math.random() * 1200; // Random height across more of screen
backgroundVillain.scaleX = 1.0; // Full size for clear visibility
backgroundVillain.scaleY = 1.0;
// Position surfboard under background villain
backgroundSurfboard.x = backgroundVillain.x;
backgroundSurfboard.y = backgroundVillain.y + 80; // Position below villain
backgroundSurfboard.scaleX = 1.0;
backgroundSurfboard.scaleY = 1.0;
// Store reference to surfboard in background villain for movement
backgroundVillain.surfboard = backgroundSurfboard;
// No tint for clearer appearance
// Flight path across screen (right to left)
var targetX = -200; // Off-screen left
var targetY = backgroundVillain.y + (Math.random() - 0.5) * 300;
var flightDuration = 4000 + Math.random() * 2000; // 4-6 seconds
// Schedule ball addition for background villain as well
var ballAdditionTime = flightDuration * 0.4; // Add balls when 40% across screen
LK.setTimeout(function () {
if (backgroundVillain && backgroundVillain.parent) {
// Add 3 random colored balls to the back of the chain (behind the last ball)
var ballColors = ['yellow', 'blue', 'red', 'yellow']; // Random colors including yellow, blue, red, and yellow
var ballsToAdd = 3;
var trackSpacing = 7.0; // Standard spacing between balls
// Find the last ball position in the chain to determine where to add new balls behind it
var lastBallPosition = 0;
if (chain.length > 0) {
lastBallPosition = chain[chain.length - 1].trackPosition;
} else {
lastBallPosition = 0; // Start at beginning of track if no balls
}
// Add 3 strategically colored balls behind the chain (toward the back/start of track)
var lastChainColors = []; // Track colors of last few balls in chain for safe selection
if (chain.length > 0) {
// Get colors of the last 2 balls in the chain
for (var c = Math.max(0, chain.length - 2); c < chain.length; c++) {
lastChainColors.push(chain[c].color);
}
}
// Sometimes change shooter's current ball color (30% chance)
if (Math.random() < 0.3 && shooter && shooter.currentBall) {
var allColors = ['red', 'blue', 'green', 'yellow', 'pink'];
var newShooterColor = allColors[Math.floor(Math.random() * allColors.length)];
// Remove current ball and create new one with different color
if (shooter.currentBall.parent) {
shooter.currentBall.parent.removeChild(shooter.currentBall);
}
shooter.currentBall.destroy();
// Create new ball with different color
shooter.currentBall = new Ball(newShooterColor);
shooter.currentBall.x = 0;
shooter.currentBall.y = -20;
shooter.currentBall.scaleX = 1.4375;
shooter.currentBall.scaleY = 1.4375;
shooter.addChild(shooter.currentBall);
// Flash effect to show change
LK.effects.flashObject(shooter.currentBall, 0x8800FF, 500);
// Display "Ball Changed by GOBLIN" notification
var ballChangedText = new Text2('Ball Changed by GOBLIN', {
size: 80,
fill: 0x8800FF
});
ballChangedText.anchor.set(0.5, 0.5);
ballChangedText.x = 1024;
ballChangedText.y = 900;
ballChangedText.alpha = 0;
ballChangedText.scaleX = 0.5;
ballChangedText.scaleY = 0.5;
game.addChild(ballChangedText);
// Animate the notification text
tween(ballChangedText, {
alpha: 1.0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(ballChangedText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8,
y: ballChangedText.y - 100
}, {
duration: 600,
easing: tween.easeIn,
onFinish: function onFinish() {
ballChangedText.destroy();
}
});
}
});
}
for (var i = 0; i < ballsToAdd; i++) {
var randomColor = getVillainDisruptiveColor(lastChainColors);
// Update lastChainColors for next ball selection
lastChainColors.push(randomColor);
if (lastChainColors.length > 2) {
lastChainColors.shift(); // Keep only last 2 colors
}
var newBall = game.addChild(new ChainBall(randomColor));
newBall.chainIndex = chain.length;
// Position new balls behind the last ball (negative offset from last position)
newBall.trackPosition = lastBallPosition + trackSpacing * (i + 1);
// Create throwing effect from villain's hand to ball position
var villainHandX = backgroundVillain.x - 30; // Villain's hand position (left hand)
var villainHandY = backgroundVillain.y + 10; // Slightly below center for hand position
// Start ball at villain's hand
newBall.x = villainHandX;
newBall.y = villainHandY;
newBall.scaleX = 0.3; // Start smaller
newBall.scaleY = 0.3;
newBall.alpha = 0.8;
// Position target on track
positionBallOnTrack(newBall, newBall.trackPosition);
var targetBallX = newBall.targetX;
var targetBallY = newBall.targetY;
// Animate ball flying from villain's hand to chain position with arc
var midX = (villainHandX + targetBallX) / 2;
var midY = Math.min(villainHandY, targetBallY) - 100; // Arc peak above both points
// First part of arc - villain hand to peak
tween(newBall, {
x: midX,
y: midY,
scaleX: 0.6,
scaleY: 0.6,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second part of arc - peak to final position
tween(newBall, {
x: targetBallX,
y: targetBallY,
scaleX: 1.15,
// Final size
scaleY: 1.15
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
// Position ball on track properly after animation
positionBallOnTrack(newBall, newBall.trackPosition);
// Flash effect when ball reaches chain
LK.effects.flashObject(newBall, 0x8800FF, 300);
}
});
}
});
// Add to chain array
chain.push(newBall);
}
// Play villain laser sound for the ball addition
try {
LK.getSound('villain_laser').play();
} catch (e) {
console.log('Background villain laser sound play error:', e);
}
}
}, ballAdditionTime);
// Always play villain flying sound with better error handling
try {
var villainFlyingSound = LK.getSound('villain_flying');
if (villainFlyingSound && typeof villainFlyingSound.play === 'function') {
villainFlyingSound.play();
}
} catch (e) {
console.log('Background villain flying sound play error:', e);
}
// Animate villain flying across screen
tween(backgroundVillain, {
x: targetX,
y: targetY
}, {
duration: flightDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
backgroundVillain.destroy();
if (backgroundVillain.surfboard && backgroundVillain.surfboard.parent) {
backgroundVillain.surfboard.destroy();
}
}
});
// Animate surfboard to follow background villain
if (backgroundVillain.surfboard) {
tween(backgroundVillain.surfboard, {
x: targetX,
y: targetY + 80
}, {
duration: flightDuration,
easing: tween.easeInOut
});
}
// Add subtle cape fluttering
var _backgroundCapeFlutter = function backgroundCapeFlutter() {
if (backgroundVillain && backgroundVillain.parent) {
tween(backgroundVillain, {
scaleX: 0.85 + Math.random() * 0.1,
scaleY: 0.78 + Math.random() * 0.04
}, {
duration: 250 + Math.random() * 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (backgroundVillain && backgroundVillain.parent) {
tween(backgroundVillain, {
scaleX: 0.75 + Math.random() * 0.1,
scaleY: 0.82 + Math.random() * 0.04
}, {
duration: 250 + Math.random() * 100,
easing: tween.easeInOut,
onFinish: _backgroundCapeFlutter
});
}
// Update surfboard position continuously
if (backgroundVillain.surfboard && backgroundVillain.surfboard.parent) {
backgroundVillain.surfboard.x = backgroundVillain.x;
backgroundVillain.surfboard.y = backgroundVillain.y + 80;
}
}
});
}
};
_backgroundCapeFlutter();
}
// Start the villain background appearance scheduling
scheduleNextVillainAppearance();
// Create Villain flying across background
function createFlyingVillain() {
var villain = game.addChild(LK.getAsset('villain', {
anchorX: 0.5,
anchorY: 0.5
}));
// Add surfboard underneath villain
var surfboard = game.addChild(LK.getAsset('surfboard', {
anchorX: 0.5,
anchorY: 0.5
}));
// Random starting position from right edge (opposite of Superman)
villain.x = 2248;
villain.y = 200 + Math.random() * 1500; // Random height
villain.scaleX = 1.0;
villain.scaleY = 1.0;
// Position surfboard under villain
surfboard.x = villain.x;
surfboard.y = villain.y + 80; // Position below villain
surfboard.scaleX = 1.0;
surfboard.scaleY = 1.0;
// Store reference to surfboard in villain for movement
villain.surfboard = surfboard;
// No tint for clearer appearance
// Random flight path - either straight across or slight curve (flying left)
var targetX = -200; // Off-screen left
var targetY = villain.y + (Math.random() - 0.5) * 400; // Slight vertical variation
var flightDuration = 3000 + Math.random() * 2000; // 3-5 seconds flight time
// Schedule ball addition when Villain is in middle of screen
var ballAdditionTime = flightDuration * 0.4; // Add balls when 40% across screen
LK.setTimeout(function () {
if (villain && villain.parent) {
// Add 3 random colored balls to the back of the chain (behind the last ball)
var ballColors = ['yellow', 'blue', 'red', 'yellow']; // Random colors including yellow, blue, red, and yellow
var ballsToAdd = 3;
var trackSpacing = 7.0; // Standard spacing between balls
// Find the last ball position in the chain to determine where to add new balls behind it
var lastBallPosition = 0;
if (chain.length > 0) {
lastBallPosition = chain[chain.length - 1].trackPosition;
} else {
lastBallPosition = 0; // Start at beginning of track if no balls
}
// Add 3 strategically colored balls behind the chain (toward the back/start of track)
var lastChainColors = []; // Track colors of last few balls in chain for safe selection
if (chain.length > 0) {
// Get colors of the last 2 balls in the chain
for (var c = Math.max(0, chain.length - 2); c < chain.length; c++) {
lastChainColors.push(chain[c].color);
}
}
// Sometimes change shooter's current ball color (30% chance)
if (Math.random() < 0.3 && shooter && shooter.currentBall) {
var allColors = ['red', 'blue', 'green', 'yellow', 'pink'];
var newShooterColor = allColors[Math.floor(Math.random() * allColors.length)];
// Remove current ball and create new one with different color
if (shooter.currentBall.parent) {
shooter.currentBall.parent.removeChild(shooter.currentBall);
}
shooter.currentBall.destroy();
// Create new ball with different color
shooter.currentBall = new Ball(newShooterColor);
shooter.currentBall.x = 0;
shooter.currentBall.y = -20;
shooter.currentBall.scaleX = 1.4375;
shooter.currentBall.scaleY = 1.4375;
shooter.addChild(shooter.currentBall);
// Flash effect to show change
LK.effects.flashObject(shooter.currentBall, 0x8800FF, 500);
// Display "Ball Changed by GOBLIN" notification
var ballChangedText = new Text2('Ball Changed by GOBLIN', {
size: 80,
fill: 0x8800FF
});
ballChangedText.anchor.set(0.5, 0.5);
ballChangedText.x = 1024;
ballChangedText.y = 900;
ballChangedText.alpha = 0;
ballChangedText.scaleX = 0.5;
ballChangedText.scaleY = 0.5;
game.addChild(ballChangedText);
// Animate the notification text
tween(ballChangedText, {
alpha: 1.0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(ballChangedText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8,
y: ballChangedText.y - 100
}, {
duration: 600,
easing: tween.easeIn,
onFinish: function onFinish() {
ballChangedText.destroy();
}
});
}
});
}
for (var i = 0; i < ballsToAdd; i++) {
var randomColor = getVillainDisruptiveColor(lastChainColors);
// Update lastChainColors for next ball selection
lastChainColors.push(randomColor);
if (lastChainColors.length > 2) {
lastChainColors.shift(); // Keep only last 2 colors
}
var newBall = game.addChild(new ChainBall(randomColor));
newBall.chainIndex = chain.length;
// Position new balls behind the last ball (negative offset from last position)
newBall.trackPosition = lastBallPosition + trackSpacing * (i + 1);
// Create throwing effect from villain's hand to ball position
var villainHandX = villain.x - 30; // Villain's hand position (left hand)
var villainHandY = villain.y + 10; // Slightly below center for hand position
// Start ball at villain's hand
newBall.x = villainHandX;
newBall.y = villainHandY;
newBall.scaleX = 0.3; // Start smaller
newBall.scaleY = 0.3;
newBall.alpha = 0.8;
// Position target on track
positionBallOnTrack(newBall, newBall.trackPosition);
var targetX = newBall.targetX;
var targetY = newBall.targetY;
// Animate ball flying from villain's hand to chain position with arc
var midX = (villainHandX + targetX) / 2;
var midY = Math.min(villainHandY, targetY) - 100; // Arc peak above both points
// First part of arc - villain hand to peak
tween(newBall, {
x: midX,
y: midY,
scaleX: 0.6,
scaleY: 0.6,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second part of arc - peak to final position
tween(newBall, {
x: targetX,
y: targetY,
scaleX: 1.15,
// Final size
scaleY: 1.15
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
// Position ball on track properly after animation
positionBallOnTrack(newBall, newBall.trackPosition);
// Flash effect when ball reaches chain
LK.effects.flashObject(newBall, 0x8800FF, 300);
}
});
}
});
// Add to chain array
chain.push(newBall);
}
// Play villain laser sound for the ball addition
try {
LK.getSound('villain_laser').play();
} catch (e) {
console.log('Villain laser sound play error:', e);
}
// Create visual effect showing villain adding balls to chain
var addBallText = new Text2('VILLAIN ADDS BALLS!', {
size: 80,
fill: 0x8800FF
});
addBallText.anchor.set(0.5, 0.5);
addBallText.x = villain.x;
addBallText.y = villain.y - 80;
addBallText.alpha = 0;
addBallText.scaleX = 0.5;
addBallText.scaleY = 0.5;
game.addChild(addBallText);
// Animate warning text
tween(addBallText, {
alpha: 1.0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(addBallText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8,
y: addBallText.y - 50
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
addBallText.destroy();
}
});
}
});
}
}, ballAdditionTime);
// Play Villain flying sound with delay to prevent overlap
LK.setTimeout(function () {
try {
var villainFlyingSound = LK.getSound('villain_flying');
if (villainFlyingSound && typeof villainFlyingSound.play === 'function') {
villainFlyingSound.play();
}
} catch (e) {
console.log('Villain flying sound play error:', e);
}
}, 100);
// Animate Villain flying across screen (right to left)
tween(villain, {
x: targetX,
y: targetY
}, {
duration: flightDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
villain.destroy();
if (villain.surfboard && villain.surfboard.parent) {
villain.surfboard.destroy();
}
}
});
// Animate surfboard to follow villain
if (villain.surfboard) {
tween(villain.surfboard, {
x: targetX,
y: targetY + 80
}, {
duration: flightDuration,
easing: tween.easeInOut
});
}
// Add cape fluttering effect (similar to Superman but with villain styling)
var _villainCapeFlutter = function villainCapeFlutter() {
if (villain && villain.parent) {
tween(villain, {
scaleX: 1.05 + Math.random() * 0.1,
scaleY: 0.98 + Math.random() * 0.04
}, {
duration: 200 + Math.random() * 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (villain && villain.parent) {
tween(villain, {
scaleX: 0.95 + Math.random() * 0.1,
scaleY: 1.02 + Math.random() * 0.04
}, {
duration: 200 + Math.random() * 100,
easing: tween.easeInOut,
onFinish: _villainCapeFlutter
});
}
}
});
// Update surfboard position continuously
if (villain.surfboard && villain.surfboard.parent) {
villain.surfboard.x = villain.x;
villain.surfboard.y = villain.y + 80;
}
}
};
_villainCapeFlutter();
}
// Game variables
var chain = [];
var flyingBalls = [];
var shooter;
var crosshair;
var trackPoints = [];
var chainSpeed = 0.004; // Increased initial chain speed for faster start
var normalChainSpeed = 0.004; // Store normal speed - increased from 0.0003
var fastChainSpeed = 0.002; // Fast chain speed - slower than before
var veryFastChainSpeed = 0.006; // Very fast chain speed - slower than before
var speedLevel = 0; // 0 = normal, 1 = fast, 2 = very fast
var chainFrozen = false;
var freezeTimer = 0;
var freezeCountdownDisplay = null;
var level = 1;
var gameWon = false;
var gameLost = false;
// Score multiplier system
var scoreMultipliers = [1.0, 1.5, 2.0]; // normal, fast, very fast
var pointsDisplays = []; // Array to track active point displays
var shotCounter = 0; // Counter to track shots fired
// Consecutive scoring system
var consecutiveScores = 0; // Track consecutive scores
var lastScoreTime = 0; // Track when last score happened
var celebrationMessages = ['AWESOME!', 'AMAZING!', 'FANTASTIC!', 'INCREDIBLE!', 'SPECTACULAR!'];
// Fire effect optimization
var fireEffectPool = []; // Pool of reusable fire effects
var activeFireEffects = []; // Currently active fire effects
var maxFireEffects = 2; // Reduced maximum concurrent fire effects
var maxPoolSize = 3; // Limit pool size to prevent memory buildup
// Create vortex track path
function createTrackPath() {
trackPoints = [];
var centerX = 1024;
var centerY = 1366;
var holeX = 1024;
var holeY = 1200; // Hole position - closer to shooter
var segments = 1500; // Longer track for better gameplay
var targetSpacing = 7.0; // Target distance between consecutive points
var currentDistance = 0;
// Generate initial spiral path points (more densely packed)
var tempPoints = [];
var densityMultiplier = 3; // Create 3x more points initially for smoother curves
for (var i = 0; i < segments * densityMultiplier; i++) {
var progress = i / (segments * densityMultiplier);
// Single spiral angle - creates one continuous swirl towards center
var spiralAngle = progress * Math.PI * 6; // 3 full rotations for longer swirling path
// Radius decreases linearly for smooth inward movement
var baseRadius = 1000 * (1 - progress); // Linear decrease for steady approach to center
// Add gentle wave motion for swirling effect
var waveMotion = Math.sin(progress * Math.PI * 12) * (60 * (1 - progress)); // Gentle swirling motion
var currentRadius = baseRadius + waveMotion;
// Ensure minimum radius for playability at center
currentRadius = Math.max(currentRadius, 40);
// Calculate position on spiral path - interpolate towards hole position
var spiralX = centerX + Math.cos(spiralAngle) * currentRadius;
var spiralY = centerY + Math.sin(spiralAngle) * currentRadius;
// Gradually move towards hole position in final segment
var holeProgress = Math.max(0, (progress - 0.9) / 0.1); // Last 10% of track moves to hole
var x = spiralX + (holeX - spiralX) * holeProgress;
var y = spiralY + (holeY - spiralY) * holeProgress;
// Ensure the path stays within screen bounds with padding
x = Math.max(150, Math.min(1898, x));
y = Math.max(300, Math.min(2600, y));
tempPoints.push({
x: x,
y: y
});
}
// Now resample the path to ensure equal spacing
trackPoints.push(tempPoints[0]); // Add first point
currentDistance = 0;
for (var i = 1; i < tempPoints.length; i++) {
var prevPoint = tempPoints[i - 1];
var currPoint = tempPoints[i];
var segmentLength = Math.sqrt(Math.pow(currPoint.x - prevPoint.x, 2) + Math.pow(currPoint.y - prevPoint.y, 2));
currentDistance += segmentLength;
// Add point when we've traveled the target spacing distance
if (currentDistance >= targetSpacing) {
// Interpolate to get exact position at target spacing
var excess = currentDistance - targetSpacing;
var ratio = excess / segmentLength;
var adjustedX = currPoint.x - (currPoint.x - prevPoint.x) * ratio;
var adjustedY = currPoint.y - (currPoint.y - prevPoint.y) * ratio;
trackPoints.push({
x: adjustedX,
y: adjustedY
});
currentDistance = excess; // Carry over the excess distance
}
}
// Create visual track markers with vortex effect
for (var i = 0; i < trackPoints.length; i += 6) {
var trackMarker = game.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5
}));
trackMarker.x = trackPoints[i].x;
trackMarker.y = trackPoints[i].y;
trackMarker.alpha = 0.3 + i / trackPoints.length * 0.3; // Fade in towards center
// Scale markers smaller as they approach center for vortex effect
var scaleProgress = i / trackPoints.length;
trackMarker.scaleX = 0.5 + (1 - scaleProgress) * 0.5;
trackMarker.scaleY = 0.5 + (1 - scaleProgress) * 0.5;
}
}
// Create hole at end of track
var hole = game.addChild(LK.getAsset('hole', {
anchorX: 0.5,
anchorY: 0.5
}));
// Create shooter
shooter = game.addChild(new Shooter());
shooter.x = 1024; // Center of screen horizontally (2048/2 = 1024)
shooter.y = 1366; // Position near the center hole
// Initialize first balls
shooter.loadBall();
shooter.loadBall();
// Create crosshair
crosshair = game.addChild(LK.getAsset('aim', {
anchorX: 0.5,
anchorY: 0.5
}));
crosshair.x = 1024; // Start at center
crosshair.y = 1000;
crosshair.alpha = 0.8;
crosshair.scaleX = 1.2;
crosshair.scaleY = 1.2;
// Villain strategic color selection - uses all colors and aims to disrupt same-color groups
function getVillainDisruptiveColor(lastChainColors) {
var allColors = ['red', 'blue', 'green', 'yellow', 'pink']; // All available colors
var chainColors = []; // Track colors in current chain for analysis
// Analyze chain colors to find same-color groups that can be disrupted
for (var i = 0; i < chain.length; i++) {
chainColors.push(chain[i].color);
}
// Find the most common colors in chain to target for disruption
var colorCounts = {};
for (var i = 0; i < chainColors.length; i++) {
var color = chainColors[i];
colorCounts[color] = (colorCounts[color] || 0) + 1;
}
// Look for consecutive same-color groups of 2 that villain can disrupt
var disruptiveColors = [];
for (var i = 0; i < chainColors.length - 1; i++) {
if (chainColors[i] === chainColors[i + 1]) {
// Found a pair of same colors - add different colors as disruptive options
for (var c = 0; c < allColors.length; c++) {
if (allColors[c] !== chainColors[i] && disruptiveColors.indexOf(allColors[c]) === -1) {
disruptiveColors.push(allColors[c]);
}
}
}
}
// If no specific disruption needed, avoid creating 3+ consecutive with last added colors
var safeColors = [];
for (var c = 0; c < allColors.length; c++) {
var testColor = allColors[c];
var wouldCreate3 = false;
// Check if this color would create 3 consecutive with last chain colors
if (lastChainColors.length >= 2 && lastChainColors[lastChainColors.length - 1] === testColor && lastChainColors[lastChainColors.length - 2] === testColor) {
wouldCreate3 = true;
}
if (!wouldCreate3) {
safeColors.push(testColor);
}
}
// Prefer disruptive colors if available, otherwise use safe colors
var finalColors = disruptiveColors.length > 0 ? disruptiveColors : safeColors;
// If no valid colors (shouldn't happen), use any color except the most recent
if (finalColors.length === 0) {
for (var c = 0; c < allColors.length; c++) {
if (lastChainColors.length === 0 || allColors[c] !== lastChainColors[lastChainColors.length - 1]) {
finalColors.push(allColors[c]);
}
}
}
// Return random color from final selection
return finalColors[Math.floor(Math.random() * finalColors.length)];
}
// Helper function to get safe color that won't create 3+ consecutive
function getSafeColor(colors, lastColors) {
var availableColors = [];
// Find colors that won't create 3 consecutive
for (var c = 0; c < colors.length; c++) {
var testColor = colors[c];
var wouldCreate3 = false;
// Check if this color would create 3 consecutive
if (lastColors.length >= 2 && lastColors[lastColors.length - 1] === testColor && lastColors[lastColors.length - 2] === testColor) {
wouldCreate3 = true;
}
if (!wouldCreate3) {
availableColors.push(testColor);
}
}
// If all colors would create 3 consecutive (shouldn't happen), return any color except the last one
if (availableColors.length === 0) {
for (var c = 0; c < colors.length; c++) {
if (lastColors.length === 0 || colors[c] !== lastColors[lastColors.length - 1]) {
availableColors.push(colors[c]);
}
}
}
// Return random color from available safe colors
return availableColors[Math.floor(Math.random() * availableColors.length)];
}
// Create initial chain
function createChain() {
chain = [];
var chainLength = 15 + level * 5;
var colors = ['red', 'blue', 'green', 'yellow', 'pink'];
var ballSpacing = 69; // Space between balls - balls touch each other (increased for bigger balls)
var trackSpacing = 6.0; // Increased track position spacing to prevent overlap
var lastColors = []; // Track last few colors to prevent 3+ consecutive
// Ensure better color distribution by cycling through colors
for (var i = 0; i < chainLength; i++) {
var color;
// Use safe color selection to prevent 3+ consecutive
if (i < 2) {
// For first two balls, use any color
if (i % 8 < 5) {
// First 5 of every 8 balls use sequential colors
var colorIndex = i % colors.length;
color = colors[colorIndex];
} else {
// Last 3 of every 8 balls use random colors
var colorIndex = Math.floor(Math.random() * colors.length);
color = colors[colorIndex];
}
} else {
// For subsequent balls, ensure no 3+ consecutive
color = getSafeColor(colors, lastColors);
}
// Safety check to ensure we never create black balls
if (color === 'black' || !color) {
color = colors[i % colors.length]; // Default to cycling through valid colors
}
var ball = game.addChild(new ChainBall(color));
ball.chainIndex = i;
// Initialize track position with consistent spacing for vortex
ball.trackPosition = i * 7.0; // Use consistent spacing to prevent overlap
// Position ball immediately on track
positionBallOnTrack(ball, ball.trackPosition);
chain.push(ball);
// Update lastColors array (keep only last 2 colors for checking)
lastColors.push(color);
if (lastColors.length > 2) {
lastColors.shift(); // Remove oldest color, keep only last 2
}
}
}
// Position ball on track
function positionBallOnTrack(ball, trackPosition) {
if (trackPosition >= trackPoints.length || trackPoints.length === 0) {
if (trackPoints.length > 0) {
ball.x = trackPoints[trackPoints.length - 1].x;
ball.y = trackPoints[trackPoints.length - 1].y;
} else {
ball.x = 1024;
ball.y = 1366;
}
return;
}
var pointIndex = Math.floor(Math.max(0, trackPosition));
var nextIndex = Math.min(pointIndex + 1, trackPoints.length - 1);
var t = trackPosition - pointIndex;
var point1 = trackPoints[pointIndex];
var point2 = trackPoints[nextIndex];
// Add safety check for undefined points
if (!point1 || !point2) {
ball.x = 1024;
ball.y = 1366;
ball.isMoving = false;
return;
}
// Calculate position with anti-overlap adjustment near the hole
var baseX = point1.x + (point2.x - point1.x) * t;
var baseY = point1.y + (point2.y - point1.y) * t;
// Apply spacing adjustment to prevent overlap near hole
var progressToHole = trackPosition / trackPoints.length;
if (progressToHole > 0.8) {
// Near the hole (last 20% of track)
// Add slight offset based on ball's chain position to prevent stacking
var offsetAngle = ball.chainIndex * 0.3; // Small angular offset per ball
var offsetRadius = 15 * (progressToHole - 0.8) * 5; // Increase offset near hole
baseX += Math.cos(offsetAngle) * offsetRadius;
baseY += Math.sin(offsetAngle) * offsetRadius;
}
ball.targetX = baseX;
ball.targetY = baseY;
ball.isMoving = true;
}
// Update chain positions
function updateChain() {
if (chainFrozen) {
freezeTimer--;
// Update freeze countdown display
if (freezeCountdownDisplay) {
var secondsLeft = Math.ceil(freezeTimer / 60);
freezeCountdownDisplay.setText('FROZEN: ' + secondsLeft);
// Add pulsing effect each second
if (freezeTimer % 60 === 0) {
tween(freezeCountdownDisplay, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
if (freezeCountdownDisplay) {
tween(freezeCountdownDisplay, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
}
if (freezeTimer <= 0) {
chainFrozen = false;
// Remove countdown display
if (freezeCountdownDisplay) {
// Remove white overlay
if (freezeCountdownDisplay.whiteOverlay && freezeCountdownDisplay.whiteOverlay.parent) {
freezeCountdownDisplay.whiteOverlay.destroy();
}
freezeCountdownDisplay.destroy();
freezeCountdownDisplay = null;
}
// Remove ice tint from all chain balls
for (var i = 0; i < chain.length; i++) {
var chainBall = chain[i];
tween(chainBall, {
tint: 0xFFFFFF
}, {
duration: 300
});
}
}
return;
}
// Detect gaps in the chain by checking distances between consecutive balls
var hasGap = false;
var gapStartIndex = -1;
var gapEndIndex = -1;
var trackSpacing = 7.0; // Standard spacing between balls
var maxAllowedGap = trackSpacing * 2; // Gap threshold
for (var i = 0; i < chain.length - 1; i++) {
var currentBall = chain[i];
var nextBall = chain[i + 1];
var gapDistance = nextBall.trackPosition - currentBall.trackPosition;
if (gapDistance > maxAllowedGap) {
hasGap = true;
gapStartIndex = i;
gapEndIndex = i + 1;
break;
}
}
if (hasGap) {
// Gap detected - back part waits completely, front part moves forward to close gap
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
if (i <= gapStartIndex) {
// Front part (before gap) - move forward normally to close gap
ball.trackPosition += chainSpeed;
// Check if reached hole (vortex center)
if (ball.trackPosition >= trackPoints.length - 1) {
if (!gameLost) {
gameLost = true;
// Play gameover sound with error handling
try {
var gameoverSound = LK.getSound('gameover');
if (gameoverSound && typeof gameoverSound.play === 'function') {
gameoverSound.play();
}
} catch (e) {
console.log('Gameover sound play error:', e);
}
LK.showGameOver();
}
return;
}
} else {
// Back part (after gap) - wait completely, don't move at all
// Don't move the back part at all
}
}
} else {
// No gap - move all balls forward normally together
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
// Move chain forward in vortex pattern
ball.trackPosition += chainSpeed;
// Check if reached hole (vortex center)
if (ball.trackPosition >= trackPoints.length - 1) {
if (!gameLost) {
gameLost = true;
// Play gameover sound with error handling
try {
var gameoverSound = LK.getSound('gameover');
if (gameoverSound && typeof gameoverSound.play === 'function') {
gameoverSound.play();
}
} catch (e) {
console.log('Gameover sound play error:', e);
}
LK.showGameOver();
}
return;
}
}
}
// Maintain equal spacing between balls for vortex movement with collision avoidance
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
// Ensure proper spacing from previous ball
if (i > 0) {
var previousBall = chain[i - 1];
var expectedPosition = previousBall.trackPosition + trackSpacing;
// Only enforce forward spacing when gap is closing or normal movement
var currentGap = ball.trackPosition - previousBall.trackPosition;
if (currentGap < trackSpacing * 0.8) {
// Gap is almost closed, enforce proper spacing
ball.trackPosition = Math.max(ball.trackPosition, expectedPosition);
} else if (currentGap > maxAllowedGap) {
// Large gap detected, allow back part to move forward freely
// Don't enforce spacing yet
} else {
// Normal spacing enforcement
ball.trackPosition = Math.max(ball.trackPosition, expectedPosition);
}
}
positionBallOnTrack(ball, ball.trackPosition);
}
// Additional collision avoidance pass to prevent balls from overlapping
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
for (var j = i + 1; j < chain.length; j++) {
var otherBall = chain[j];
var dx = ball.x - otherBall.x;
var dy = ball.y - otherBall.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = 69; // Minimum distance between ball centers (increased for 35% larger balls)
if (distance < minDistance && distance > 0) {
// Calculate separation needed
var separation = minDistance - distance;
var separationX = dx / distance * separation * 0.5;
var separationY = dy / distance * separation * 0.5;
// Move balls apart by adjusting their track positions
var trackDelta = separation / trackSpacing;
if (i > 0) {
// Move current ball back
ball.trackPosition -= trackDelta * 0.5;
// Reposition on track
positionBallOnTrack(ball, ball.trackPosition);
}
if (j < chain.length - 1) {
// Move other ball forward
otherBall.trackPosition += trackDelta * 0.5;
// Reposition on track
positionBallOnTrack(otherBall, otherBall.trackPosition);
}
}
}
}
}
// Check for matches
function checkMatches() {
var matches = [];
var currentColor = null;
var currentMatch = [];
// First, ensure all balls are positioned correctly on the track
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
positionBallOnTrack(ball, ball.trackPosition);
}
// Check for consecutive balls of the same color
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
if (ball.color === currentColor) {
currentMatch.push(ball);
} else {
if (currentMatch.length >= 3) {
matches.push(currentMatch.slice());
}
currentColor = ball.color;
currentMatch = [ball];
}
}
// Check last group
if (currentMatch.length >= 3) {
matches.push(currentMatch.slice());
}
// Remove matches
for (var m = 0; m < matches.length; m++) {
var match = matches[m];
var baseScore = match.length * 10;
var multiplier = scoreMultipliers[speedLevel];
var score = Math.floor(baseScore * multiplier);
// Calculate display position (center of the match)
var displayX = 0;
var displayY = 0;
for (var b = 0; b < match.length; b++) {
displayX += match[b].x;
displayY += match[b].y;
}
displayX /= match.length;
displayY /= match.length;
// Display flashing points
displayPoints(score, displayX, displayY);
// Check for special balls
for (var b = 0; b < match.length; b++) {
var ball = match[b];
if (ball.isSpecial) {
handleSpecialBall(ball);
}
}
// Store the first index of the match to know where the gap starts
var firstMatchIndex = chain.indexOf(match[0]);
var trackSpacing = 7.0; // Increased spacing to prevent overlap
var gapSize = match.length * trackSpacing; // Calculate gap size based on track spacing
// Remove matched balls
for (var b = 0; b < match.length; b++) {
var ball = match[b];
var index = chain.indexOf(ball);
if (index > -1) {
chain.splice(index, 1);
ball.destroy();
}
}
// Close the gap by moving all balls after the removed section forward
for (var i = firstMatchIndex; i < chain.length; i++) {
var ball = chain[i];
// Add safety check for ball and trackPosition existence
if (!ball || typeof ball.trackPosition !== 'number' || isNaN(ball.trackPosition)) {
// Skip invalid balls or initialize trackPosition
if (ball) {
ball.trackPosition = i * 7.0; // Initialize with index-based positioning
}
continue;
}
// Move the ball's track position forward to close the gap
ball.trackPosition -= gapSize;
}
// Re-space all balls to maintain equal intervals after gap closing
for (var i = 1; i < chain.length; i++) {
var previousBall = chain[i - 1];
var currentBall = chain[i];
var expectedPosition = previousBall.trackPosition + trackSpacing;
// Enforce minimum spacing to prevent overlap
currentBall.trackPosition = Math.max(currentBall.trackPosition, expectedPosition);
// Position the ball immediately to detect matches faster
positionBallOnTrack(currentBall, currentBall.trackPosition);
// Animate the ball to its new position smoothly
tween(currentBall, {
x: currentBall.targetX,
y: currentBall.targetY
}, {
duration: 200,
// Faster animation for quicker match detection
easing: tween.easeOut
});
}
LK.setScore(LK.getScore() + score);
// Update score display immediately
scoreTxt.setText('SCORE: ' + LK.getScore());
// Add enhanced scale up/down effect and flash score text in RGB colors with longer duration
tween(scoreTxt, {
scaleX: 1.6,
scaleY: 1.6,
tint: 0xFF0000 // Red
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: 1.4,
scaleY: 1.4,
tint: 0x00FF00 // Green
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: 1.5,
scaleY: 1.5,
tint: 0x0000FF // Blue
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFFF00 // Yellow
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: 1.2,
scaleY: 1.2,
tint: 0xFF00FF // Magenta
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: 1.1,
scaleY: 1.1,
tint: 0x00FFFF // Cyan
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF // Back to white
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
});
}
});
}
});
}
});
}
});
LK.getSound('match').play();
// Track consecutive scoring
var currentTime = LK.ticks;
if (currentTime - lastScoreTime < 180) {
// Within 3 seconds (180 frames at 60fps)
consecutiveScores++;
} else {
consecutiveScores = 1; // Reset to 1 for this score
}
lastScoreTime = currentTime;
// Show celebration for consecutive scores (2 or more)
if (consecutiveScores >= 2) {
displayCelebration();
}
// Pull chain back slightly when balls are removed
chainSpeed = Math.max(0.5, chainSpeed - 0.1);
}
// Always check for new matches after gap closure, regardless of previous matches
// This ensures balls disappear when they touch each other after gap closure
if (matches.length > 0) {
// After removing matches and closing gaps, check for new matches that may have formed
// Use a timeout to allow animations to settle before checking
LK.setTimeout(function () {
checkMatches();
}, 50); // Reduced timeout for faster response
}
// Always perform an additional check for consecutive same-color balls after any gap closure
// This ensures that balls of the same color that become adjacent are immediately detected
var foundConsecutiveColors = false;
for (var i = 0; i < chain.length - 1; i++) {
var currentBall = chain[i];
var nextBall = chain[i + 1];
if (currentBall.color === nextBall.color) {
// Found consecutive same-color balls, check for a group of 3 or more
var consecutiveCount = 1;
var groupStart = i;
// Count consecutive balls of the same color starting from current position
for (var j = i + 1; j < chain.length && chain[j].color === currentBall.color; j++) {
consecutiveCount++;
}
// If we have 3 or more consecutive balls of the same color, mark for immediate removal
if (consecutiveCount >= 3) {
foundConsecutiveColors = true;
break;
}
}
}
// If consecutive same-color balls were found, immediately check matches again
if (foundConsecutiveColors) {
LK.setTimeout(function () {
checkMatches();
}, 10); // Very short timeout for immediate response
}
// Check win condition
if (chain.length === 0) {
gameWon = true;
level++;
LK.showYouWin();
}
}
// Display celebration message for consecutive scores
function displayCelebration() {
var message = celebrationMessages[Math.min(consecutiveScores - 2, celebrationMessages.length - 1)];
var celebrationText = new Text2(message, {
size: 100,
fill: 0xFFD700 // Gold color
});
celebrationText.anchor.set(0.5, 0.5);
celebrationText.x = 1024; // Center of screen
celebrationText.y = 800; // Middle area
celebrationText.alpha = 0;
celebrationText.scaleX = 0.5;
celebrationText.scaleY = 0.5;
celebrationText.rotation = -0.2;
game.addChild(celebrationText);
// Animate celebration text with dramatic entrance
tween(celebrationText, {
alpha: 1.0,
scaleX: 1.5,
scaleY: 1.5,
rotation: 0.1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
// Hold for a moment then fade out
tween(celebrationText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8,
y: celebrationText.y - 100
}, {
duration: 600,
easing: tween.easeIn,
onFinish: function onFinish() {
celebrationText.destroy();
}
});
}
});
// Play celebration sound
try {
LK.getSound('celebration').play();
} catch (e) {
console.log('Celebration sound play error:', e);
}
}
// Display flashing points
function displayPoints(points, x, y) {
var pointsText = new Text2(('+' + points).toUpperCase(), {
size: 80,
fill: 0xFFFF00,
font: "'Courier New Bold', 'Monaco Bold', 'Menlo Bold', 'Consolas Bold', 'DejaVu Sans Mono Bold', 'Lucida Console Bold', monospace"
});
pointsText.anchor.set(0.5, 0.5);
pointsText.x = x;
pointsText.y = y;
pointsText.alpha = 1.0;
pointsText.scaleX = 0.5;
pointsText.scaleY = 0.5;
game.addChild(pointsText);
pointsDisplays.push(pointsText);
// Add RGB flashing effect for 0.5 seconds
tween(pointsText, {
tint: 0xFF0000 // Red
}, {
duration: 125,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(pointsText, {
tint: 0x00FF00 // Green
}, {
duration: 125,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(pointsText, {
tint: 0x0000FF // Blue
}, {
duration: 125,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(pointsText, {
tint: 0xFFFF00 // Back to yellow
}, {
duration: 125,
easing: tween.easeInOut
});
}
});
}
});
}
});
// Get score text position for flying animation
var scoreTextPos = scoreTxt.parent.toGlobal(scoreTxt.position);
var gameScorePos = game.toLocal(scoreTextPos);
// Animate the points display
tween(pointsText, {
scaleX: 1.2,
scaleY: 1.2,
y: y - 100
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
// First fade and fly towards score
tween(pointsText, {
alpha: 0.8,
scaleX: 0.6,
scaleY: 0.6,
x: gameScorePos.x,
y: gameScorePos.y
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Final fade out at score position
tween(pointsText, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
pointsText.destroy();
var index = pointsDisplays.indexOf(pointsText);
if (index > -1) {
pointsDisplays.splice(index, 1);
}
}
});
}
});
}
});
}
// Handle special ball effects
function handleSpecialBall(ball) {
if (ball.specialType === 'explosive') {
var _shakeEffect = function shakeEffect() {
if (shakeTimer < shakeDuration) {
game.x = originalX + (Math.random() - 0.5) * shakeIntensity;
game.y = originalY + (Math.random() - 0.5) * shakeIntensity;
shakeTimer += 16;
LK.setTimeout(_shakeEffect, 16);
} else {
game.x = originalX;
game.y = originalY;
}
};
// Remove nearby balls
var explosionRadius = 3;
var ballIndex = chain.indexOf(ball);
var toRemove = [];
for (var i = Math.max(0, ballIndex - explosionRadius); i < Math.min(chain.length, ballIndex + explosionRadius + 1); i++) {
if (chain[i] !== ball) {
toRemove.push(chain[i]);
}
}
for (var i = 0; i < toRemove.length; i++) {
var index = chain.indexOf(toRemove[i]);
if (index > -1) {
chain.splice(index, 1);
toRemove[i].destroy();
}
}
LK.effects.flashScreen(0xff8000, 300);
// Add screen shake effect
var originalX = game.x;
var originalY = game.y;
var shakeIntensity = 10;
var shakeDuration = 300;
var shakeTimer = 0;
_shakeEffect();
} else if (ball.specialType === 'freeze') {
chainFrozen = true;
freezeTimer = 180; // 3 seconds at 60fps
LK.effects.flashScreen(0x00ffff, 500);
// Play frozen sound
try {
LK.getSound('frozen').play();
} catch (e) {
console.log('Frozen sound play error:', e);
}
// Add ice tint to all chain balls
for (var i = 0; i < chain.length; i++) {
var chainBall = chain[i];
tween(chainBall, {
tint: 0x88DDFF
}, {
duration: 300
});
}
} else if (ball.specialType === 'rapid') {
shooter.rapidFire = true;
shooter.rapidFireTimer = 300; // 5 seconds at 60fps
LK.effects.flashScreen(0xffff00, 200);
} else if (ball.specialType === 'white_freeze') {
chainFrozen = true;
freezeTimer = 120; // 2 seconds at 60fps
LK.effects.flashScreen(0xffffff, 500);
// Play frozen sound
try {
LK.getSound('frozen').play();
} catch (e) {
console.log('Frozen sound play error:', e);
}
// Create freeze countdown display first
if (freezeCountdownDisplay) {
freezeCountdownDisplay.destroy();
}
freezeCountdownDisplay = new Text2('FROZEN: 2', {
size: 80,
fill: 0x00FFFF
});
// Create transparent white overlay for entire freeze duration
var whiteOverlay = LK.getAsset('centerCircle', {
anchorX: 0,
anchorY: 0,
scaleX: 25,
scaleY: 35
});
whiteOverlay.x = 0;
whiteOverlay.y = 0;
whiteOverlay.alpha = 0.3;
whiteOverlay.tint = 0xFFFFFF;
game.addChild(whiteOverlay);
// Store reference to remove later
freezeCountdownDisplay.whiteOverlay = whiteOverlay;
freezeCountdownDisplay.anchor.set(0.5, 1);
freezeCountdownDisplay.y = -50;
LK.gui.bottom.addChild(freezeCountdownDisplay);
// Add pulsing animation to the countdown display
tween(freezeCountdownDisplay, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(freezeCountdownDisplay, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
// Add ice tint to all chain balls
for (var i = 0; i < chain.length; i++) {
var chainBall = chain[i];
tween(chainBall, {
tint: 0x88DDFF
}, {
duration: 300
});
}
} else if (ball.specialType === 'fire_blast') {
// Fire blast effect - explode 3 balls on each side
var ballIndex = chain.indexOf(ball);
var blastRadius = 3;
var toRemove = [];
// Get 3 balls to the left and 3 balls to the right
for (var i = Math.max(0, ballIndex - blastRadius); i < Math.min(chain.length, ballIndex + blastRadius + 1); i++) {
if (chain[i] !== ball) {
toRemove.push(chain[i]);
}
}
// Store the first index of the blast to know where the gap starts
var firstBlastIndex = Math.max(0, ballIndex - blastRadius);
var trackSpacing = 7.0; // Track spacing to prevent overlap
var gapSize = toRemove.length * trackSpacing; // Calculate gap size based on track spacing
// Remove blasted balls
for (var i = 0; i < toRemove.length; i++) {
var index = chain.indexOf(toRemove[i]);
if (index > -1) {
chain.splice(index, 1);
toRemove[i].destroy();
}
}
// Close the gap by moving all balls after the removed section forward
for (var i = firstBlastIndex; i < chain.length; i++) {
var ball = chain[i];
// Add comprehensive safety check for ball existence and trackPosition
if (!ball || _typeof(ball) !== 'object') {
// Skip invalid ball entries
continue;
}
// Ensure trackPosition exists and is a valid number
if (typeof ball.trackPosition !== 'number' || isNaN(ball.trackPosition)) {
// Initialize trackPosition if undefined or invalid
ball.trackPosition = i * 7.0; // Use index-based positioning
}
// Ensure gapSize is valid before using it
if (typeof gapSize !== 'number' || isNaN(gapSize)) {
gapSize = toRemove.length * 7.0; // Recalculate if invalid
}
// Now safely access trackPosition - additional check before modification
if (typeof ball.trackPosition === 'number' && !isNaN(ball.trackPosition) && typeof gapSize === 'number' && !isNaN(gapSize)) {
// Move the ball's track position forward to close the gap
ball.trackPosition -= gapSize;
}
}
// Comprehensive re-spacing to ensure no gaps remain
// Start from the beginning and ensure each ball has proper spacing
for (var i = 0; i < chain.length; i++) {
var ball = chain[i];
// Safety check for ball existence
if (!ball || _typeof(ball) !== 'object') {
continue;
}
// Ensure trackPosition exists and is valid for current ball
if (typeof ball.trackPosition !== 'number' || isNaN(ball.trackPosition)) {
ball.trackPosition = i * 7.0; // Initialize with index-based positioning
}
if (i === 0) {
// First ball keeps its current position as reference
continue;
}
var previousBall = chain[i - 1];
// Safety check for previous ball
if (!previousBall || _typeof(previousBall) !== 'object') {
// Initialize trackPosition if invalid
ball.trackPosition = i * 7.0;
continue;
}
// Ensure previous ball has valid trackPosition
if (typeof previousBall.trackPosition !== 'number' || isNaN(previousBall.trackPosition)) {
previousBall.trackPosition = (i - 1) * 7.0;
}
var expectedPosition = previousBall.trackPosition + trackSpacing;
// Always enforce proper spacing, moving balls forward to close any gaps
ball.trackPosition = expectedPosition;
// Position the ball immediately to detect matches faster
positionBallOnTrack(ball, ball.trackPosition);
// Animate the ball to its new position smoothly
tween(ball, {
x: ball.targetX,
y: ball.targetY
}, {
duration: 200,
// Faster animation for quicker match detection
easing: tween.easeOut
});
}
// Immediately check for any consecutive same-color balls after repositioning
var foundImmediateMatch = false;
for (var i = 0; i < chain.length - 2; i++) {
if (chain[i].color === chain[i + 1].color && chain[i + 1].color === chain[i + 2].color) {
foundImmediateMatch = true;
break;
}
}
if (foundImmediateMatch) {
// Trigger immediate match check
LK.setTimeout(function () {
checkMatches();
}, 5); // Very fast response for fire blast
}
// Create blast explosion effect
LK.effects.flashScreen(0xFF4500, 500);
// Add screen shake effect
var originalX = game.x;
var originalY = game.y;
var shakeIntensity = 15;
var shakeDuration = 400;
var shakeTimer = 0;
var _shakeEffect = function shakeEffect() {
if (shakeTimer < shakeDuration) {
game.x = originalX + (Math.random() - 0.5) * shakeIntensity;
game.y = originalY + (Math.random() - 0.5) * shakeIntensity;
shakeTimer += 16;
LK.setTimeout(_shakeEffect, 16);
} else {
game.x = originalX;
game.y = originalY;
}
};
_shakeEffect();
// Create blast text effect with swinging animation
var blastText = new Text2('BoOoOMm !', {
size: 120,
fill: 0xFF4500
});
blastText.anchor.set(0.5, 0.5);
blastText.x = ball.x;
blastText.y = ball.y;
blastText.scaleX = 0.5;
blastText.scaleY = 0.5;
blastText.rotation = -0.3; // Start with slight rotation
game.addChild(blastText);
// Animate blast text with swinging motion
tween(blastText, {
scaleX: 2.0,
scaleY: 2.0,
alpha: 0.8,
rotation: 0.3 // Swing to the other side
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(blastText, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
rotation: -0.2 // Swing back slightly
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
blastText.destroy();
}
});
}
});
// Check for new matches after gap closure
LK.setTimeout(function () {
checkMatches();
}, 250); // Allow time for animations to settle
}
LK.getSound('powerup').play();
}
// Insert ball into chain
function insertBallIntoChain(ball, insertIndex) {
var chainBall = new ChainBall(ball.color);
chainBall.isSpecial = ball.isSpecial;
chainBall.specialType = ball.specialType;
chainBall.x = ball.x;
chainBall.y = ball.y;
chainBall.scaleX = 0.1;
chainBall.scaleY = 0.1;
game.addChild(chainBall);
// Set consistent scale immediately
chainBall.scaleX = 1.15;
chainBall.scaleY = 1.15;
// Animate ball insertion with consistent final size
tween(chainBall, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 200,
easing: tween.easeOut
});
chain.splice(insertIndex, 0, chainBall);
// Update chain indices
for (var i = 0; i < chain.length; i++) {
chain[i].chainIndex = i;
}
checkMatches();
}
// Initialize game
createTrackPath();
createChain();
// Start gameplay music
LK.playMusic('Gameplay');
// Position hole at the end of the track - near the shooter
hole.x = 1024;
hole.y = 1200; // Position hole closer to shooter
// Add breathing and rotation effects to the hole
var _holeBreathingEffect = function holeBreathingEffect() {
if (hole && hole.parent) {
// Breathing effect - scale up and down
tween(hole, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (hole && hole.parent) {
tween(hole, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: _holeBreathingEffect
});
}
}
});
}
};
// Start breathing effect
_holeBreathingEffect();
// Continuous rotation effect
var _holeRotationEffect = function holeRotationEffect() {
if (hole && hole.parent) {
hole.rotation += 0.02; // Rotate around its own axis
LK.setTimeout(_holeRotationEffect, 16); // 60fps rotation
}
};
// Start rotation effect
_holeRotationEffect();
// Score display - will be moved to bottom later
var scoreTxt = new Text2('SCORE: 0', {
size: 60,
fill: 0xFFFFFF,
font: "'Courier New Bold', 'Monaco Bold', 'Menlo Bold', 'Consolas Bold', 'DejaVu Sans Mono Bold', 'Lucida Console Bold', monospace"
});
scoreTxt.anchor.set(0.5, 0);
// Chain speed display - will be moved to bottom later
var chainSpeedTxt = new Text2('Speed: 0.00', {
size: 40,
fill: 0xCCCCCC
});
chainSpeedTxt.anchor.set(0.5, 0);
// Add spaceship HUD status indicators
// Chain status indicator
var chainStatusTxt = new Text2('', {
size: 30,
fill: 0x00FF88
});
chainStatusTxt.anchor.set(0, 0);
chainStatusTxt.x = 20;
chainStatusTxt.y = 100;
LK.gui.left.addChild(chainStatusTxt);
// Weapon status indicator
var weaponStatusTxt = new Text2('WEAPON: READY', {
size: 30,
fill: 0x00FF88
});
weaponStatusTxt.anchor.set(1, 1);
weaponStatusTxt.x = -30;
weaponStatusTxt.y = -110;
LK.gui.bottom.addChild(weaponStatusTxt);
// System status indicators
var systemStatus1 = new Text2('', {
size: 25,
fill: 0x88CCFF
});
systemStatus1.anchor.set(0, 0);
systemStatus1.x = 20;
systemStatus1.y = 150;
LK.gui.left.addChild(systemStatus1);
var systemStatus2 = new Text2('', {
size: 25,
fill: 0x88CCFF
});
systemStatus2.anchor.set(1, 0);
systemStatus2.x = -20;
systemStatus2.y = 150;
LK.gui.right.addChild(systemStatus2);
// Targeting system indicator
var targetingTxt = new Text2('TARGET: LOCKED', {
size: 25,
fill: 0xFFAA00
});
targetingTxt.anchor.set(0, 1);
targetingTxt.x = 30;
targetingTxt.y = -110;
LK.gui.bottom.addChild(targetingTxt);
// Navigation indicator
var navTxt = new Text2('', {
size: 25,
fill: 0xFFAA00
});
navTxt.anchor.set(1, 0);
navTxt.x = -20;
navTxt.y = 200;
LK.gui.right.addChild(navTxt);
// Next ball preview
var nextBallTxt = new Text2('NEXT BALL', {
size: 30,
fill: 0xFFFFFF
});
nextBallTxt.anchor.set(0, 1);
nextBallTxt.x = 30;
nextBallTxt.y = -110;
LK.gui.bottomLeft.addChild(nextBallTxt);
// Add corner decorative elements for spaceship feel
// Top corner indicators
var topCornerIndicator1 = new Text2('◊', {
size: 35,
fill: 0x00AAFF
});
topCornerIndicator1.anchor.set(0, 0);
topCornerIndicator1.x = 180;
topCornerIndicator1.y = 15;
LK.gui.topLeft.addChild(topCornerIndicator1);
var topCornerIndicator2 = new Text2('◊', {
size: 35,
fill: 0x00AAFF
});
topCornerIndicator2.anchor.set(1, 0);
topCornerIndicator2.x = -50;
topCornerIndicator2.y = 15;
LK.gui.topRight.addChild(topCornerIndicator2);
// Bottom corner indicators
var bottomCornerIndicator1 = new Text2('▲', {
size: 25,
fill: 0x00AAFF
});
bottomCornerIndicator1.anchor.set(0, 1);
bottomCornerIndicator1.x = 50;
bottomCornerIndicator1.y = -15;
LK.gui.bottomLeft.addChild(bottomCornerIndicator1);
var bottomCornerIndicator2 = new Text2('▲', {
size: 25,
fill: 0x00AAFF
});
bottomCornerIndicator2.anchor.set(1, 1);
bottomCornerIndicator2.x = -50;
bottomCornerIndicator2.y = -15;
LK.gui.bottomRight.addChild(bottomCornerIndicator2);
// Add pulsing animation to corner indicators
var pulseCornerIndicators = function pulseCornerIndicators() {
var indicators = [topCornerIndicator1, topCornerIndicator2, bottomCornerIndicator1, bottomCornerIndicator2];
for (var i = 0; i < indicators.length; i++) {
if (indicators[i] && indicators[i].parent) {
tween(indicators[i], {
alpha: 0.3
}, {
duration: 800 + i * 200,
easing: tween.easeInOut,
onFinish: function () {
if (this && this.parent) {
tween(this, {
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
}.bind(indicators[i])
});
}
}
};
// Start corner indicator animation
LK.setTimeout(pulseCornerIndicators, 1000);
LK.setInterval(pulseCornerIndicators, 3000);
// Create modern spaceship UI system
// Add corner accent panels for spaceship aesthetics
// Add animated scanner lines for spaceship feel
var scannerLineLeft = LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 15
});
scannerLineLeft.tint = 0x00FFAA;
scannerLineLeft.alpha = 0.8;
scannerLineLeft.x = -80;
scannerLineLeft.y = 0;
LK.gui.left.addChild(scannerLineLeft);
var scannerLineRight = LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 15
});
scannerLineRight.tint = 0x00FFAA;
scannerLineRight.alpha = 0.8;
scannerLineRight.x = 80;
scannerLineRight.y = 0;
LK.gui.right.addChild(scannerLineRight);
// Animate scanner lines
var _animateScannerLines = function animateScannerLines() {
if (scannerLineLeft && scannerLineLeft.parent) {
tween(scannerLineLeft, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (scannerLineLeft && scannerLineLeft.parent) {
tween(scannerLineLeft, {
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: _animateScannerLines
});
}
}
});
}
if (scannerLineRight && scannerLineRight.parent) {
tween(scannerLineRight, {
alpha: 0.3
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (scannerLineRight && scannerLineRight.parent) {
tween(scannerLineRight, {
alpha: 0.8
}, {
duration: 1200,
easing: tween.easeInOut
});
}
}
});
}
};
_animateScannerLines();
// Add header image asset at the top
var gameHeader = LK.getAsset('game_header', {
anchorX: 0.5,
anchorY: 0
});
gameHeader.x = 0;
gameHeader.y = 50;
LK.gui.top.addChild(gameHeader);
// Add modern score display to bottom panel
scoreTxt.anchor.set(0.5, 1);
scoreTxt.y = -180; // Moved higher up
scoreTxt.size = 70;
LK.gui.bottom.addChild(scoreTxt);
// Add modern speed display to bottom panel
chainSpeedTxt.anchor.set(0.5, 1);
chainSpeedTxt.y = -35;
chainSpeedTxt.size = 35;
chainSpeedTxt.fill = 0xAADDFF; // Light blue color
LK.gui.bottom.addChild(chainSpeedTxt);
// Add remaining balls counter display next to speed text
var remainingBallsTxt = new Text2('BALLS: 0', {
size: 35,
fill: 0xAADDFF // Light blue color matching speed text
});
remainingBallsTxt.anchor.set(0, 1);
remainingBallsTxt.x = 160; // Position further to the right of speed text
remainingBallsTxt.y = -35;
LK.gui.bottom.addChild(remainingBallsTxt);
// Create difficulty label text
var difficultyLabel = new Text2('SET THE DIFFICULT', {
size: 30,
fill: 0xCCCCCC
});
difficultyLabel.anchor.set(1, 1);
difficultyLabel.x = -30;
difficultyLabel.y = -110;
LK.gui.bottomRight.addChild(difficultyLabel);
// Create accelerate button - repositioned for modern UI
var accelerateBtn = new Text2('EASY', {
size: 45,
fill: 0x00FF00
});
accelerateBtn.anchor.set(1, 1);
accelerateBtn.x = -30;
accelerateBtn.y = -25;
LK.gui.bottomRight.addChild(accelerateBtn);
// Button press handler
accelerateBtn.down = function (x, y, obj) {
speedLevel = (speedLevel + 1) % 3; // Cycle through 0, 1, 2
if (speedLevel === 0) {
accelerateBtn.setText('EASY');
accelerateBtn.fill = 0x00FF00; // Green
} else if (speedLevel === 1) {
accelerateBtn.setText('NORMAL');
accelerateBtn.fill = 0xFFFFFF; // White
} else {
accelerateBtn.setText('HARD');
accelerateBtn.fill = 0xFF0000; // Red
}
};
// Game input
game.down = function (x, y, obj) {
// Update crosshair position and aim
crosshair.x = x;
crosshair.y = y;
shooter.aimAt(x, y);
// Add crosshair click effect
tween(crosshair, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(crosshair, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeIn
});
}
});
var ball = shooter.shoot();
if (ball) {
flyingBalls.push(ball);
game.addChild(ball);
}
};
// Mouse move handler to make shooter follow mouse and move crosshair
game.move = function (x, y, obj) {
// Update crosshair position
crosshair.x = x;
crosshair.y = y;
// Aim shooter at crosshair position
shooter.aimAt(x, y);
};
// Main game loop
game.update = function () {
if (gameWon || gameLost) {
// Clean up fire effects when game ends
for (var i = 0; i < activeFireEffects.length; i++) {
if (activeFireEffects[i] && activeFireEffects[i].parent) {
tween.stop(activeFireEffects[i]); // Stop animations
activeFireEffects[i].parent.removeChild(activeFireEffects[i]);
}
}
activeFireEffects = [];
// Clean up flying balls
for (var i = 0; i < flyingBalls.length; i++) {
flyingBalls[i].destroy();
}
flyingBalls = [];
return;
}
// Update chain
updateChain();
// Update flying balls - limit processing if too many
var maxFlyingBalls = 5; // Prevent too many balls in flight
if (flyingBalls.length > maxFlyingBalls) {
// Remove oldest balls if too many in flight
var oldestBall = flyingBalls.shift();
oldestBall.destroy();
}
for (var i = flyingBalls.length - 1; i >= 0; i--) {
var ball = flyingBalls[i];
ball.x += ball.vx;
ball.y += ball.vy;
// Create rocket trail for every shot ball except white balls
if (ball.createRocketTrail && !(ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze')) {
ball.createRocketTrail();
}
// Create trail effects for special balls - added behind the ball so ball renders on top
if (ball.color === 'fire' && ball.isSpecial && ball.specialType === 'fire_blast') {
// Play fire flying sound continuously while ball is moving
if (LK.ticks % 20 === 0) {
// Play sound every 20 frames (about 3 times per second)
try {
LK.getSound('fire_flying').play();
} catch (e) {
console.log('Fire flying sound play error:', e);
}
}
// Create fire trail effect - more frequent and larger
if (LK.ticks % 2 === 0) {
// Create multiple trails for more dramatic effect
for (var trailIndex = 0; trailIndex < 3; trailIndex++) {
var fireTrail = LK.getAsset('fire_effect', {
anchorX: 0.5,
anchorY: 0.5
});
// Create trails at different positions for spread effect
var offsetX = (Math.random() - 0.5) * 40;
var offsetY = (Math.random() - 0.5) * 40;
fireTrail.x = ball.x - ball.vx * (0.3 + trailIndex * 0.2) + offsetX;
fireTrail.y = ball.y - ball.vy * (0.3 + trailIndex * 0.2) + offsetY;
fireTrail.scaleX = 0.8 + Math.random() * 0.6; // Much larger scale
fireTrail.scaleY = 0.8 + Math.random() * 0.6;
fireTrail.alpha = 0.9 - trailIndex * 0.2;
fireTrail.tint = 0xFF6A00; // Brighter orange flame color for meteor effect
// Add trail behind the ball in z-order
var ballIndex = game.getChildIndex(ball);
game.addChildAt(fireTrail, ballIndex);
// Animate trail - fade out and scale down
tween(fireTrail, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 400 + trailIndex * 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fireTrail && fireTrail.parent) {
fireTrail.destroy();
}
}
});
}
}
} else if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') {
// Create white rocket trail effect behind white balls - unique white trail
if (LK.ticks % 2 === 0) {
// Create distinctive white rocket trail effect
var rocketTrail = LK.getAsset('fire_effect', {
anchorX: 0.5,
anchorY: 0.5
});
// Position trail behind the ball
rocketTrail.x = ball.x - ball.vx * 0.8;
rocketTrail.y = ball.y - ball.vy * 0.8;
rocketTrail.scaleX = 0.7; // Slightly larger for white effect
rocketTrail.scaleY = 0.7;
rocketTrail.alpha = 0.9; // Higher alpha for more visible white effect
rocketTrail.tint = 0xFFFFFF; // Pure white rocket flame color
// Add trail behind the ball in z-order
var ballIndex = game.getChildIndex(ball);
game.addChildAt(rocketTrail, ballIndex);
// Animate trail - distinctive white fade effect
tween(rocketTrail, {
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
if (rocketTrail && rocketTrail.parent) {
rocketTrail.destroy();
}
}
});
}
// Also create white snowflake particles for additional effect
if (LK.ticks % 3 === 0) {
// Create multiple white snowflakes for snow effect - less frequent
for (var snowIndex = 0; snowIndex < 4; snowIndex++) {
var snowflake = LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5
});
// Create small white particles at different positions behind the ball
var offsetX = (Math.random() - 0.5) * 50;
var offsetY = (Math.random() - 0.5) * 50;
snowflake.x = ball.x - ball.vx * (0.4 + snowIndex * 0.15) + offsetX;
snowflake.y = ball.y - ball.vy * (0.4 + snowIndex * 0.15) + offsetY;
snowflake.scaleX = 0.12 + Math.random() * 0.08; // Small varied snowflake particles
snowflake.scaleY = 0.12 + Math.random() * 0.08;
snowflake.alpha = 0.95 - snowIndex * 0.12;
snowflake.tint = 0xFFFFFF; // Pure white snow color
snowflake.rotation = Math.random() * Math.PI * 2; // Random rotation
// Add snowflake behind the ball in z-order
var ballIndex2 = game.getChildIndex(ball);
game.addChildAt(snowflake, ballIndex2);
// Animate snowflake - gentle drift and fade like floating snow particles
tween(snowflake, {
scaleX: 0.03,
scaleY: 0.03,
alpha: 0,
x: snowflake.x + (Math.random() - 0.5) * 40,
y: snowflake.y + Math.random() * 50 + 30,
// Gentle drift motion like snow
rotation: snowflake.rotation + (Math.random() - 0.5) * Math.PI * 1.5
}, {
duration: 900 + snowIndex * 120,
easing: tween.easeOut,
onFinish: function onFinish() {
if (snowflake && snowflake.parent) {
snowflake.destroy();
}
}
});
}
}
}
// Check collision with chain - optimized detection
var inserted = false;
var collisionIndex = -1;
var ballRadius = 34.5; // Approximate ball radius (increased for 35% larger balls)
// Only check balls within reasonable distance
for (var j = 0; j < chain.length; j++) {
var chainBall = chain[j];
// Quick distance check before expensive intersects call
var dx = ball.x - chainBall.x;
var dy = ball.y - chainBall.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ballRadius * 2 && ball.intersects(chainBall)) {
collisionIndex = j;
break;
}
}
// If collision detected, handle special balls or insert into chain
if (collisionIndex !== -1) {
// Check if the flying ball is a white ball - trigger freeze effect and disappear
if (ball.color === 'white' && ball.isSpecial && ball.specialType === 'white_freeze') {
handleSpecialBall(ball);
// Clean up rocket trail effects
if (ball.rocketTrail) {
for (var t = 0; t < ball.rocketTrail.length; t++) {
if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) {
ball.rocketTrail[t].destroy();
}
}
ball.rocketTrail = [];
}
ball.destroy();
flyingBalls.splice(i, 1);
inserted = true;
} else if (ball.color === 'fire' && ball.isSpecial && ball.specialType === 'fire_blast') {
// Fire ball - trigger blast effect on the first ball it hits
var hitBall = chain[collisionIndex];
// Temporarily set the hit ball as the fire ball for the blast effect
hitBall.isSpecial = true;
hitBall.specialType = 'fire_blast';
handleSpecialBall(hitBall);
// Clean up rocket trail effects
if (ball.rocketTrail) {
for (var t = 0; t < ball.rocketTrail.length; t++) {
if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) {
ball.rocketTrail[t].destroy();
}
}
ball.rocketTrail = [];
}
ball.destroy();
flyingBalls.splice(i, 1);
inserted = true;
} else {
// Regular ball - insert into chain with collision avoidance
var insertIndex = collisionIndex + 1;
var trackSpacing = 7.0; // Increased spacing to prevent overlap
// Create more space by pushing balls further back
var pushBackDistance = trackSpacing * 1.5; // Extra space to prevent overlap
// Adjust track positions for all balls after insertion point
for (var k = insertIndex; k < chain.length; k++) {
chain[k].trackPosition += pushBackDistance; // Move balls back to make space
}
insertBallIntoChain(ball, insertIndex);
// Set the inserted ball's track position with proper spacing
if (insertIndex < chain.length) {
chain[insertIndex].trackPosition = chain[collisionIndex].trackPosition + trackSpacing;
positionBallOnTrack(chain[insertIndex], chain[insertIndex].trackPosition);
}
// Re-space all balls after insertion to maintain equal intervals with extra spacing
for (var k = insertIndex + 1; k < chain.length; k++) {
var expectedPosition = chain[k - 1].trackPosition + trackSpacing;
// Enforce minimum spacing to prevent overlap with buffer
chain[k].trackPosition = Math.max(chain[k].trackPosition, expectedPosition + 1.0);
}
// Additional pass to ensure no overlapping after insertion
for (var k = 0; k < chain.length - 1; k++) {
var currentBall = chain[k];
var nextBall = chain[k + 1];
var dx = currentBall.x - nextBall.x;
var dy = currentBall.y - nextBall.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 69) {
// Push next ball further back
nextBall.trackPosition += (69 - distance) / trackSpacing;
positionBallOnTrack(nextBall, nextBall.trackPosition);
}
}
// Clean up rocket trail effects
if (ball.rocketTrail) {
for (var t = 0; t < ball.rocketTrail.length; t++) {
if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) {
ball.rocketTrail[t].destroy();
}
}
ball.rocketTrail = [];
}
ball.destroy();
flyingBalls.splice(i, 1);
inserted = true;
}
}
// Remove if off screen
if (!inserted && (ball.x < -100 || ball.x > 2148 || ball.y < -100 || ball.y > 2832)) {
// Deduct 10 points for missing the chain - always allow negative scores
var currentScore = LK.getScore();
LK.setScore(currentScore - 10);
// Display -10 points at ball position
var minusPointsText = new Text2('-10', {
size: 60,
fill: 0xFF4444
});
minusPointsText.anchor.set(0.5, 0.5);
minusPointsText.x = ball.x;
minusPointsText.y = ball.y;
minusPointsText.alpha = 1.0;
minusPointsText.scaleX = 0.5;
minusPointsText.scaleY = 0.5;
game.addChild(minusPointsText);
// Animate the minus points display
tween(minusPointsText, {
scaleX: 1.2,
scaleY: 1.2,
y: ball.y - 80,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
minusPointsText.destroy();
}
});
// Clean up rocket trail effects
if (ball.rocketTrail) {
for (var t = 0; t < ball.rocketTrail.length; t++) {
if (ball.rocketTrail[t] && ball.rocketTrail[t].parent) {
ball.rocketTrail[t].destroy();
}
}
ball.rocketTrail = [];
}
ball.destroy();
flyingBalls.splice(i, 1);
}
}
// Update shooter
shooter.update();
// Update UI
scoreTxt.setText('SCORE: ' + LK.getScore());
chainSpeedTxt.setText('Speed: ' + (chainSpeed * 1000).toFixed(2));
remainingBallsTxt.setText('BALLS: ' + chain.length);
// Keep crosshair on top of everything else
if (crosshair && crosshair.parent) {
game.addChild(crosshair); // Moves crosshair to front by re-adding it
}
// Update spaceship HUD status based on game state
if (chainStatusTxt) {
if (chainFrozen) {
chainStatusTxt.setText('');
chainStatusTxt.fill = 0x00DDFF;
} else if (chain.length < 5) {
chainStatusTxt.setText('');
chainStatusTxt.fill = 0xFF4444;
} else {
chainStatusTxt.setText('');
chainStatusTxt.fill = 0x00FF88;
}
}
if (weaponStatusTxt) {
if (shooter.rapidFire) {
weaponStatusTxt.setText('WEAPON: RAPID');
weaponStatusTxt.fill = 0xFFFF00;
} else if (shooter.shootCooldown > 0) {
var reloadSeconds = (shooter.shootCooldown / 60).toFixed(1);
weaponStatusTxt.setText('WEAPON: RELOAD ' + reloadSeconds + 's');
weaponStatusTxt.fill = 0xFFAA00;
} else {
weaponStatusTxt.setText('WEAPON: READY');
weaponStatusTxt.fill = 0x00FF88;
}
}
if (systemStatus1) {
var powerLevel = Math.max(0, 100 - speedLevel * 25);
systemStatus1.setText('');
if (powerLevel > 50) {
systemStatus1.fill = 0x88CCFF;
} else {
systemStatus1.fill = 0xFFAA00;
}
}
if (targetingTxt) {
if (flyingBalls.length > 0) {
targetingTxt.setText('TARGET: FIRING');
targetingTxt.fill = 0xFF4444;
} else {
targetingTxt.setText('TARGET: LOCKED');
targetingTxt.fill = 0xFFAA00;
}
}
// Show next ball preview with larger size and higher position
if (shooter.nextBall) {
if (shooter.nextBall.parent) {
shooter.nextBall.parent.removeChild(shooter.nextBall);
}
shooter.nextBall.x = 70; // Moved further right from 50 to 70
shooter.nextBall.y = -60;
shooter.nextBall.scaleX = 1.2; // Increased from 0.8 to 1.2 for more prominence
shooter.nextBall.scaleY = 1.2; // Increased from 0.8 to 1.2 for more prominence
LK.gui.bottomLeft.addChild(shooter.nextBall);
}
// Increase chain speed over time (slower increase)
normalChainSpeed += 0.00003;
// Update other speeds to maintain ratios
fastChainSpeed = normalChainSpeed * 6.67; // Maintain same ratio as before
veryFastChainSpeed = normalChainSpeed * 20; // Maintain same ratio as before
if (speedLevel === 0) {
chainSpeed = normalChainSpeed;
} else if (speedLevel === 1) {
chainSpeed = fastChainSpeed;
} else {
chainSpeed = veryFastChainSpeed;
}
};
8 ball billard with fire. In-Game asset. 2d. High contrast. No shadows
green neon ball. In-Game asset. 2d. High contrast. No shadows
blach hole gif. In-Game asset. 2d. High contrast. No shadows
space shooter cannon. In-Game asset. 2d. High contrast. No shadows
fire effect. In-Game asset. 2d. High contrast. No shadows
space track point. In-Game asset. 2d. High contrast. No shadows
aim . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
ice. In-Game asset. 2d. High contrast. No shadows
flying superman
laser beam. In-Game asset. 2d. High contrast. No shadows
green goblin. In-Game asset. 2d. High contrast. No shadows
rocket. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
shoot
Sound effect
match
Sound effect
powerup
Sound effect
Gameplay
Music
gameover
Sound effect
frozen
Sound effect
celebration
Sound effect
white_shoot
Sound effect
fire_flying
Sound effect
superman_flying
Sound effect
superman_laser
Sound effect
villain_flying
Sound effect
villain_laser
Sound effect