/**** * 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