/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Box class var Box = Container.expand(function () { var self = Container.call(this); var boxGraphics = self.attachAsset('box', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, // Increase the scale to enlarge the bounding box scaleY: 1.2 }); self.speed = 5; self.update = function () { self.y += self.speed; if (self.y > 2732) { self.destroy(); } }; }); // Box1 class var Box1 = Container.expand(function () { var self = Container.call(this); var boxGraphics = self.attachAsset('box1', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, // Increase the scale to enlarge the bounding box scaleY: 1.2 }); self.speed = 5; self.update = function () { self.y += self.speed; if (self.y > 2732) { self.destroy(); } }; }); // Box2 class var Box2 = Container.expand(function () { var self = Container.call(this); var boxGraphics = self.attachAsset('box2', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, // Increase the scale to enlarge the bounding box scaleY: 1.2 }); self.speed = 7; self.update = function () { self.y += self.speed; if (self.y > 2732) { self.destroy(); } }; }); // Box3 class var Box3 = Container.expand(function () { var self = Container.call(this); var boxGraphics = self.attachAsset('box3', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, // Increase the scale to enlarge the bounding box scaleY: 1.2 }); self.speed = 9; self.update = function () { self.y += self.speed; if (self.y > 2732) { self.destroy(); } }; }); // The assets will be automatically created and loaded by the LK engine. // Bubble class var Bubble = Container.expand(function () { var self = Container.call(this); var bubbleGraphics = self.attachAsset('bubble', { anchorX: 0.5, anchorY: 0.5, alpha: 1 // Start fully visible instead of transparent }); // Generate random color for the bubble var randomColor = Math.random() * 0xFFFFFF; bubbleGraphics.tint = randomColor; // Allow for different bubble sizes (original size = 1.0) self.size = 1.0; // Update bubble size based on the size property bubbleGraphics.scale.set(self.size); // Physics constants for more consistent behavior self.gravity = 0.2; // Gravity constant self.bounceFactor = 0.7; // Energy loss on bounce (30% loss) self.friction = 0.98; // Air/ground friction self.speed = 3; // Vertical speed (significantly reduced initial speed) // Track last position for collision detection self.lastY = 0; self.lastIntersecting = false; // Method to split the bubble into two smaller ones self.split = function () { // Only split if bubble is not too small // Note: We're keeping the same threshold (0.3) to determine when bubbles stop splitting if (self.size <= 0.3) { // Track that this is a small bubble that can't be split anymore console.log("Bubble too small to split, size=" + self.size); return false; } // Create two new smaller bubbles var halfSize = self.size * 0.5; console.log("Splitting bubble: original size=" + self.size + ", new size=" + halfSize); // Create left bubble var leftBubble = new Bubble(); leftBubble.x = self.x; leftBubble.y = self.y; leftBubble.size = halfSize; leftBubble.speedX = -Math.abs(self.speedX) * 1.2; // Move left, slightly faster leftBubble.speed = -5 - Math.random() * 3; // Jump up leftBubble.lastY = leftBubble.y; leftBubble.lastIntersecting = false; if (leftBubble.children && leftBubble.children.length > 0) { leftBubble.children[0].scale.set(halfSize); // Generate a new random color for split bubble leftBubble.children[0].tint = Math.random() * 0xFFFFFF; } // Create right bubble var rightBubble = new Bubble(); rightBubble.x = self.x; rightBubble.y = self.y; rightBubble.size = halfSize; rightBubble.speedX = Math.abs(self.speedX) * 1.2; // Move right, slightly faster rightBubble.speed = -5 - Math.random() * 3; // Jump up rightBubble.lastY = rightBubble.y; rightBubble.lastIntersecting = false; if (rightBubble.children && rightBubble.children.length > 0) { rightBubble.children[0].scale.set(halfSize); // Generate a new random color for split bubble rightBubble.children[0].tint = Math.random() * 0xFFFFFF; } // Add both bubbles to the game game.addChild(leftBubble); game.addChild(rightBubble); return true; }; self.update = function () { // Track last position before moving self.lastY = self.y; self.lastIntersecting = self.intersects(player); self.speed += 0.2; // Further reduced gravity acceleration from 0.3 to 0.2 self.y += self.speed; self.x += self.speedX; // Update horizontal position based on speedX // Bounce off the left and right margins with a small boost to ensure movement if (self.x <= 100 && self.speedX < 0 || self.x >= 1948 && self.speedX > 0) { self.speedX *= -1.1; // Reverse horizontal direction with a slight boost to ensure movement } // Bounce off the ground (bottom of the screen) if (self.y >= 2732 - 100) { // Account for bubble size/radius self.y = 2732 - 100; // Reset position to prevent sinking below ground // Calculate bounce height based on bubble size // Larger bubbles bounce higher (500-800px), smaller bubbles bounce lower (300-500px) var bounceHeight = 400 + self.size * 1000; // Size 1.0 = 800px, Size 0.3 = 450px // Calculate velocity needed to reach the desired height var sizeBasedVelocity = -Math.sqrt(2 * self.gravity * bounceHeight); var naturalBounce = -self.speed * 0.7; // Original dampening logic (70% of original speed) // Use the stronger of the two values to ensure minimum bounce height self.speed = Math.min(naturalBounce, sizeBasedVelocity); // Add a small random horizontal impulse on bounce for more natural movement self.speedX += (Math.random() - 0.5) * 2; // Add a slight color flash when bouncing (if not a rainbow bubble) if (!self.isRainbow && self.children && self.children.length > 0) { var bubbleGraphics = self.children[0]; // Store original tint var originalTint = bubbleGraphics.tint; // Slightly lighten the bubble on bounce for visual feedback var brighterTint = 0xFFFFFF; // Flash to brighter color tween(bubbleGraphics, { tint: brighterTint }, { duration: 100, onFinish: function onFinish() { // Return to original tint tween(bubbleGraphics, { tint: originalTint }, { duration: 300 }); } }); } } // Removed tint application when reaching a certain Y position // Bubble class update method where player collision is detected if (!self.lastIntersecting && self.intersects(player)) { // Make the player blink three times with rainbow colors when hit by a bubble if (player.children && player.children.length > 0 && !player.isDying) { var rainbowTint = function rainbowTint() { // Convert HSL to RGB (simplified version) rainbowHue = (rainbowHue + 20) % 360; // Faster color change // Convert hue (0-360) to RGB (0-255) var h = rainbowHue / 60; var c = 1; // Chroma var x = c * (1 - Math.abs(h % 2 - 1)); var r, g, b; if (h < 1) { r = c; g = x; b = 0; } else if (h < 2) { r = x; g = c; b = 0; } else if (h < 3) { r = 0; g = c; b = x; } else if (h < 4) { r = 0; g = x; b = c; } else if (h < 5) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } // Convert to RGB format return (Math.floor(r * 255) << 16) + (Math.floor(g * 255) << 8) + Math.floor(b * 255); }; // First blink (1/3) var playerGraphics = player.children[0]; // Save original tint var originalTint = playerGraphics.tint || 0xFFFFFF; // Create rainbow animation function for player blinking var rainbowHue = 0; playerGraphics.tint = rainbowTint(); // Initial rainbow color tween(playerGraphics, { alpha: 0 }, { duration: 100, onFinish: function onFinish() { playerGraphics.tint = rainbowTint(); // New rainbow color tween(playerGraphics, { alpha: 1 }, { duration: 100, onFinish: function onFinish() { // Second blink (2/3) playerGraphics.tint = rainbowTint(); // New rainbow color tween(playerGraphics, { alpha: 0 }, { duration: 100, onFinish: function onFinish() { playerGraphics.tint = rainbowTint(); // New rainbow color tween(playerGraphics, { alpha: 1 }, { duration: 100, onFinish: function onFinish() { // Third blink (3/3) playerGraphics.tint = rainbowTint(); // New rainbow color tween(playerGraphics, { alpha: 0 }, { duration: 100, onFinish: function onFinish() { playerGraphics.tint = rainbowTint(); // Final rainbow color tween(playerGraphics, { alpha: 1 }, { duration: 100, onFinish: function onFinish() { // Reset tint to original color playerGraphics.tint = originalTint; } }); } }); } }); } }); } }); } }); } // Instead of destroying the bubble, try to split it var wasSplit = self.split(); // Always destroy the original bubble after splitting or if too small to split self.destroy(); lives -= 1; // Remove a heart icon when a life is lost if (hearts.length > lives) { var heartToRemove = hearts.pop(); if (heartToRemove) { tween(heartToRemove.scale, { x: 0, y: 0 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { heartToRemove.destroy(); } }); } } if (lives < 0) { // Trigger player death animation before showing game over if (player && !player.isDying) { player.die(); // Delay game over screen until animation completes LK.setTimeout(function () { LK.showGameOver(); }, 2000); // Wait for death animation to complete } else { LK.showGameOver(); } } } }; }); // Explosion class var Explosion = Container.expand(function () { var self = Container.call(this); var explosionGraphics = self.attachAsset('explosion', { anchorX: 0.5, anchorY: 0.5 }); tween(explosionGraphics, { scaleX: explosionGraphics.scaleX + 1, scaleY: explosionGraphics.scaleY + 1 }, { duration: 1000, easing: tween.bounceOut, onFinish: function onFinish() { self.destroy(); } }); self.update = function () { // The explosion will disappear after a while if (self.alpha > 0) { self.alpha -= 0.02; } else { self.destroy(); } }; }); // Harpoon class var Harpoon = Container.expand(function () { var self = Container.call(this); var harpoonGraphics = self.attachAsset('harpoon', { anchorX: 0.5, anchorY: 0.5 }); self.speed = -20; self.originalSpeed = -20; self.maxDistance = 2732 * 0.85; // Maximum distance - 85% of screen height self.startY = 0; // Will store the starting Y position self.trails = []; // Apply a quick acceleration tween to give a nice launch effect tween(self, { speed: self.speed * 1.2 // Accelerate to 1.2x speed }, { duration: 200, easing: tween.easeOut }); // Function to check if point is on line (trail) self.isPointOnLine = function (point, bubbleSize) { // The trail is a vertical line from the harpoon down to the player // Get bubble size factor (if not provided, default to 1) var sizeFactor = bubbleSize || 1.0; // Special handling for the smallest bubbles - much more generous if (sizeFactor <= 0.25) { // For very small bubbles, use a very large fixed tolerance var tolerance = 100; // Extremely generous for the tiniest bubbles // More lenient y-range check for tiny bubbles var minY = self.y - 100; // Allow collision even slightly above the harpoon var maxY = 2732 + 100; // Allow collision even slightly below the bottom // Very generous x-range check for tiny bubbles return Math.abs(point.x - self.x) < tolerance && point.y >= minY && point.y <= maxY; } // For all other bubble sizes, use scaled tolerance var baseWidth = 50; // Base tolerance for normal bubbles var tolerance; if (sizeFactor <= 0.3) { tolerance = 150; // Still very generous for small bubbles } else if (sizeFactor < 1.0) { // Scale tolerance inversely with bubble size tolerance = baseWidth * (2 - sizeFactor); } else { tolerance = baseWidth; // Normal tolerance for regular bubbles } // Check if point is on main harpoon or any trail segment var yCheck = point.y >= self.y && point.y <= 2732; // First check against harpoon itself if (Math.abs(point.x - self.x) < tolerance && yCheck) { return true; } // Then check against each trail segment for (var i = 0; i < self.trails.length; i++) { var trail = self.trails[i]; if (Math.abs(point.x - self.x) < tolerance && point.y >= trail.y - trail.height / 2 && point.y <= trail.y + trail.height / 2) { return true; } } return false; }; self.update = function () { self.y += self.speed; // Store initial position on first update if (self.startY === 0) { self.startY = self.y; } // Calculate total trail distance var totalDistance = self.startY - self.y; // Create trail segments for every 100 pixels var segmentCount = Math.floor(totalDistance / 100); var currentTrailCount = self.trails.length; // Create new trail segments if needed if (segmentCount > currentTrailCount) { for (var i = currentTrailCount; i < segmentCount; i++) { var trail = game.addChild(new Trail()); trail.x = self.x; trail.y = self.y + 50 + i * 100; trail.tint = 0x3843ab; trail.width = 20; trail.height = 70; self.trails.push(trail); } } // Update position of all trail segments for (var i = 0; i < self.trails.length; i++) { var trail = self.trails[i]; trail.y = self.y + 50 + i * 100; } // Destroy when reaching max distance or going off-screen if (self.y < 0 || self.startY - self.y > self.maxDistance) { self.destroy(); self.trails.forEach(function (trail) { trail.destroy(); }); } }; }); // Player class var Player = Container.expand(function () { var self = Container.call(this); var playerGraphics = self.attachAsset('player', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 5; self.isDying = false; self.deathSpeed = 0; self.deathRotation = 0; self.lastX = 0; // Track the last X position to detect direction of movement self.update = function () { // Handle death animation if player is dying if (self.isDying) { // Accelerate the fall self.deathSpeed += 0.5; self.y += self.deathSpeed; // Flip the player while falling (rotate vertically) self.deathRotation += 0.05; self.rotation = self.deathRotation; // Add slight horizontal movement for more natural fall self.x += Math.sin(self.deathRotation * 2) * 3; // Slowly make the player more transparent if (self.alpha > 0.1) { self.alpha -= 0.01; } // Check if player has fallen off the screen if (self.y > 2732 + 500) { self.isDying = false; self.destroy(); } return; } // Normal movement when not dying if (self.direction && self.direction === 'left') { self.x -= self.speed; } else if (self.direction === 'right') { self.x += self.speed; } // Check if player has moved since last update and in which direction if (self.x !== self.lastX && !self.isDying) { // If moving right (x increasing), make sure scale is positive (not mirrored) if (self.x > self.lastX && self.scaleX < 0) { self.scaleX *= -1; // Flip to face right } // If moving left (x decreasing), make sure scale is negative (mirrored) else if (self.x < self.lastX && self.scaleX > 0) { self.scaleX *= -1; // Flip to face left } } // Update lastX for next frame comparison self.lastX = self.x; }; self.die = function () { if (self.isDying) { return; } // Prevent multiple death animations self.isDying = true; self.deathSpeed = 5; // Play death sound (if available) if (LK.getSound('explosion')) { LK.getSound('explosion').play(); } }; self.shoot = function () { if (self.isDying) { return; } // Cannot shoot while dying var harpoon = new Harpoon(); harpoon.x = player.x; harpoon.y = player.y; // Trails will be created automatically in the harpoon update method game.addChild(harpoon); LK.getSound('crossbow').play(); }; }); // PowerUpText class var PowerUpText = Container.expand(function () { var self = Container.call(this); var textGraphics = self.attachAsset('PowerUpText', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { self.y -= 2; if (self.y < 0) { self.destroy(); } }; }); // Trail class var Trail = Container.expand(function () { var self = Container.call(this); var trailGraphics = self.attachAsset('line', { anchorX: 0.5, anchorY: 0.5, width: 18, alpha: 0.9, // Slightly transparent scaleY: 0.15 // Default scale for segment }); // Custom setter for the height property Object.defineProperty(self, 'height', { get: function get() { return trailGraphics.scaleY * 100; // Convert scale to height }, set: function set(value) { trailGraphics.scaleY = value / 100; // Convert height to scale } }); self.update = function () { // Trail segments are now controlled by the harpoon // They don't move independently if (self.y > 2732) { self.destroy(); } }; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xFFFFFFFF // Init game with black background }); /**** * Game Code ****/ var background = game.attachAsset('Landscape', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); game.setChildIndex(background, 0); var player = new Player(); player.x = 2048 / 2; player.y = 2732 - 180; player.lastX = player.x; // Initialize lastX to current position game.addChild(player); // Add player after trail to ensure correct rendering order game.move = function (x, y, obj) { if (player && !player.isDying) { // Track the last position before moving (done in player.update) player.x = x; // Flipping is now handled in player.update based on movement direction } }; var score = 0; var lives = 3; var lastDifficultyLevel = 0; // Track the last level for showing difficulty announcements // Create score background var scoreBackground = new Container(); var scoreBgGraphics = scoreBackground.attachAsset('scoreBg', { anchorX: 0.5, anchorY: 0.1, scaleX: 5, scaleY: 5, alpha: 1 }); // Create difficulty indicator var difficultyIndicator = new Text2('Difficulty: Easy', { size: 24, fill: 0x00FF00, font: "'Comic Sans MS', cursive, sans-serif" }); difficultyIndicator.anchor.set(0, 0.5); difficultyIndicator.x = 20; difficultyIndicator.y = 50; LK.gui.topLeft.addChild(difficultyIndicator); // Function to update difficulty indicator based on score function updateDifficultyIndicator() { var level = Math.floor(score / 10); var difficultyText = 'Difficulty: '; var textColor = 0x00FF00; if (level === 0) { difficultyText += 'Very Easy'; textColor = 0x00FF00; } else if (level === 1) { difficultyText += 'Easy'; textColor = 0x66FF00; } else if (level === 2) { difficultyText += 'Medium'; textColor = 0xFFFF00; } else if (level === 3) { difficultyText += 'Challenging'; textColor = 0xFF9900; } else if (level === 4) { difficultyText += 'Hard'; textColor = 0xFF6600; } else { difficultyText += 'Expert'; textColor = 0xFF0000; } difficultyIndicator.setText(difficultyText); difficultyIndicator.fill = textColor; } scoreBackground.addChild(scoreBgGraphics); scoreBackground.x = 0; scoreBackground.y = 0; LK.gui.top.addChild(scoreBackground); // Create score text var scoreTxt = new Text2('Bubbles popped: 0', { size: 30, fill: 0x007bff, font: "'Comic Sans MS', cursive, sans-serif" }); scoreTxt.anchor.set(0.5, -0.1); scoreBackground.addChild(scoreTxt); // Function to show difficulty level change announcement function showDifficultyAnnouncement(level) { var difficultyText = ''; var textColor = 0xFFFFFF; switch (level) { case 1: difficultyText = "Warming Up!"; textColor = 0x00FF00; break; case 2: difficultyText = "Getting Started!"; textColor = 0xFFFF00; break; case 3: difficultyText = "Picking Up Pace!"; textColor = 0xFF9900; break; case 4: difficultyText = "More Bubbles!"; textColor = 0xFF6600; break; case 5: difficultyText = "Getting Challenging!"; textColor = 0xFF3300; break; default: if (level > 5) { difficultyText = "Level " + (level - 5) + "!"; textColor = 0xFF0000; } } if (difficultyText) { // Create text for announcement var announcement = new Text2(difficultyText, { size: 100, fill: textColor, font: "'Comic Sans MS', cursive, sans-serif" }); announcement.anchor.set(0.5, 0.5); announcement.x = 2048 / 2; announcement.y = 2732 / 2; announcement.alpha = 0; game.addChild(announcement); // Fade in tween(announcement, { alpha: 1 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Hold for a moment LK.setTimeout(function () { // Fade out tween(announcement, { alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { announcement.destroy(); } }); }, 1500); } }); // Play a sound for level up LK.getSound('powerup').play(); } } var hearts = []; for (var i = 0; i < lives; i++) { var heart = LK.getAsset('heart', { anchorX: 0.5, anchorY: 0.5, x: -1 * (i + 1) * 50, // Position hearts with some spacing y: 50 }); LK.gui.topRight.addChild(heart); hearts.push(heart); } var lastShot = -999; game.down = function (x, y, obj) { if (LK.ticks - lastShot > 10 && player && !player.isDying) { player.shoot(); lastShot = LK.ticks; } }; // Start the music 'chinese' upon starting the game LK.playMusic('arcade'); game.update = function () { // Create boxes array before using it var boxes = game.children.filter(function (child) { return child instanceof Box || child instanceof Box1 || child instanceof Box2 || child instanceof Box3; }); // Dynamic bubble spawn rate based on score // Base spawn interval is much slower at start (300 ticks) and only gradually decreases based on score // This makes the start of the game much easier var baseInterval = 300; // Start with very slow spawn rate (5 seconds between bubbles) var minInterval = 60; // Minimum spawn interval is now higher (1 second) var reductionPerPoint = 2; // Slower progression - only reduce by 2 ticks per point var intervalReductionThreshold = 10; // Only start making game harder after scoring 10 points // Calculate spawn interval with much gentler progression var scoreBasedReduction = Math.max(0, score - intervalReductionThreshold) * reductionPerPoint; var spawnInterval = Math.max(minInterval, baseInterval - scoreBasedReduction); // Only spawn a bubble if current tick matches spawn interval if (LK.ticks % spawnInterval === 0) { var newBubble = new Bubble(); newBubble.x = Math.random() * 2048; newBubble.y = 0; // Reduced horizontal speed for more predictable trajectories var minSpeed = 3; // Reduced from 5 var maxSpeed = 6; // Reduced from 8 var randomSpeed = minSpeed + Math.random() * (maxSpeed - minSpeed); newBubble.speedX = Math.random() < 0.5 ? -randomSpeed : randomSpeed; // Give bubbles a smaller initial vertical speed for gentler physics newBubble.speed = 1 + Math.random() * 2; // Reduced from 2 + random * 2 // Set bounce count to track how many times the bubble has bounced newBubble.bounceCount = 0; newBubble.maxBounces = 3 + Math.floor(Math.random() * 3); // Initialize tracking properties for the bubble newBubble.lastY = newBubble.y; newBubble.lastIntersecting = false; // For special bubbles (every 10th bubble), create a rainbow effect if (score > 30 && Math.random() < 0.1) { // Set the initial tint var hue = 0; // Store the original tint for reference newBubble.originalTint = newBubble.children[0].tint; // Flag this bubble as a special rainbow bubble newBubble.isRainbow = true; // Start the rainbow animation newBubble.rainbowTween = function () { // Convert HSL to RGB (simplified version) hue = (hue + 1) % 360; // Convert hue (0-360) to RGB (0-255) var h = hue / 60; var c = 1; // Chroma var x = c * (1 - Math.abs(h % 2 - 1)); var r, g, b; if (h < 1) { r = c; g = x; b = 0; } else if (h < 2) { r = x; g = c; b = 0; } else if (h < 3) { r = 0; g = c; b = x; } else if (h < 4) { r = 0; g = x; b = c; } else if (h < 5) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } // Convert to RGB format var rgb = (Math.floor(r * 255) << 16) + (Math.floor(g * 255) << 8) + Math.floor(b * 255); // Apply the color if (newBubble.children && newBubble.children.length > 0) { newBubble.children[0].tint = rgb; } // Continue the animation next frame newBubble.rainbowTimeout = LK.setTimeout(newBubble.rainbowTween, 50); }; // Start the rainbow animation newBubble.rainbowTween(); } game.addChild(newBubble); // Log difficulty progression for debugging if (score > 0 && score % 10 === 0 && LK.ticks % spawnInterval === 0) { console.log("Score: " + score + ", New spawn interval: " + spawnInterval + " ms (" + (spawnInterval / 60).toFixed(1) + " seconds between bubbles)"); } } // Make sure to initialize all arrays at the beginning var bubbles = game.children.filter(function (child) { return child instanceof Bubble; }); // boxes array is now initialized at the beginning of update function // to prevent 'Cannot read properties of undefined' error var harpoons = game.children.filter(function (child) { return child instanceof Harpoon; }); for (var i = 0; i < bubbles.length; i++) { var bubble = bubbles[i]; // Collision handling is now done in the Bubble class update method for (var j = 0; j < harpoons.length; j++) { var harpoon = harpoons[j]; // Check if bubble collides with harpoon OR with harpoon's trail var hitsHarpoon = bubble.intersects(harpoon); var hitsTrail = harpoon.isPointOnLine({ x: bubble.x, y: bubble.y }, bubble.size); // For very small bubbles, do additional detection with shifted positions to improve hit detection if (!hitsTrail && bubble.size <= 0.25) { // Try multiple points in a wider pattern around the bubble for tiny bubbles // Use more test points with larger offsets var offset = 20; // Increase test point spread var testPoints = [ // Square pattern around center { x: bubble.x - offset, y: bubble.y }, { x: bubble.x + offset, y: bubble.y }, { x: bubble.x, y: bubble.y - offset }, { x: bubble.x, y: bubble.y + offset }, // Diagonal test points { x: bubble.x - offset, y: bubble.y - offset }, { x: bubble.x + offset, y: bubble.y - offset }, { x: bubble.x - offset, y: bubble.y + offset }, { x: bubble.x + offset, y: bubble.y + offset }, // Even wider points for extreme edge cases { x: bubble.x - offset * 1.5, y: bubble.y }, { x: bubble.x + offset * 1.5, y: bubble.y }]; // Check each test point for (var p = 0; p < testPoints.length; p++) { if (harpoon.isPointOnLine(testPoints[p], bubble.size)) { hitsTrail = true; // Log when a tiny bubble is hit console.log("Small bubble hit by trail: size=" + bubble.size + ", x=" + bubble.x + ", harpoon.x=" + harpoon.x + ", point=" + testPoints[p].x + "," + testPoints[p].y); break; } } } if (hitsHarpoon || hitsTrail) { // Clean up rainbow animation if this is a rainbow bubble if (bubble.isRainbow && bubble.rainbowTimeout) { LK.clearTimeout(bubble.rainbowTimeout); bubble.rainbowTimeout = null; } // Destroy small bubbles immediately when hit by the trail if (bubble.size <= 0.25) { // Show a small explosion effect var explosion = new Explosion(); explosion.x = bubble.x; explosion.y = bubble.y; // explosion.scale.set(bubble.size); game.addChild(explosion); // Play explosion sound LK.getSound('explosion').play(); // Show Explosion asset for tiny bubbles (instead of Boom! text) var explosion = new Explosion(); explosion.x = bubble.x; explosion.y = bubble.y; // Adjust explosion size based on bubble size explosion.scale.set(bubble.size * 1.5); // Slightly larger than bubble for visual effect game.addChild(explosion); // Update score score += 1; scoreTxt.setText("Bubbles popped: " + score.toString()); // Destroy bubble and harpoon bubble.destroy(); bubbles.splice(i, 1); // Check if harpoon.trail exists before destroying it if (harpoon.trails) { harpoon.trails.forEach(function (trail) { trail.destroy(); }); } else if (harpoon.trail) { harpoon.trail.destroy(); } harpoon.destroy(); harpoons.splice(j, 1); continue; // Skip further checks for this bubble } // Log small bubble collisions for debugging if (bubble.size <= 0.3) { // Check for trail hits on small bubbles, store the hit location for verification if (hitsTrail && !hitsHarpoon) { // Create temporary visual indicator at hit point (for debug) var debugHitSpot = LK.getAsset('bubble', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.1, scaleY: 0.1, alpha: 0.8, tint: 0xFF0000 // Red to make it visible }); debugHitSpot.x = bubble.x; debugHitSpot.y = bubble.y; game.addChild(debugHitSpot); // Remove the debug hit spot after a short duration LK.setTimeout(function () { debugHitSpot.destroy(); }, 300); // Add log message for each trail segment if (harpoon.trails) { for (var t = 0; t < harpoon.trails.length; t++) { var trail = harpoon.trails[t]; console.log("Trail segment " + t + ": x=" + trail.x + ", y=" + trail.y + ", height=" + trail.height); } } } } // Store current position and data for explosion var bubbleX = bubble.x; var bubbleY = bubble.y; var bubbleSize = bubble.size || 1.0; var bubbleSpeedX = bubble.speedX; // Clean up rainbow animation if this is a rainbow bubble if (bubble.isRainbow && bubble.rainbowTimeout) { LK.clearTimeout(bubble.rainbowTimeout); bubble.rainbowTimeout = null; } // Try to split the bubble var wasSplit = false; if (bubbleSize > 0.3) { // Call split method to create two smaller bubbles wasSplit = bubble.split(); var boxes = game.children.filter(function (child) { return child instanceof Box || child instanceof Box1 || child instanceof Box2 || child instanceof Box3; }); // Remove the original bubble bubble.destroy(); // Play explosion sound LK.getSound('explosion').play(); // Remove the harpoon and all its trail segments if (harpoon.trails) { harpoon.trails.forEach(function (trail) { trail.destroy(); }); } harpoon.destroy(); bubbles.splice(i, 1); // Remove bubble from the array // Update score and UI score += 1; scoreTxt.setText("Bubbles popped: " + score.toString()); // Check if we've reached a new difficulty level (every 10 points) var currentDifficultyLevel = Math.floor(score / 10); if (currentDifficultyLevel > lastDifficultyLevel) { lastDifficultyLevel = currentDifficultyLevel; // Show difficulty level announcement showDifficultyAnnouncement(currentDifficultyLevel); // Update the difficulty indicator updateDifficultyIndicator(); } tween(scoreTxt.scale, { x: 2, y: 2 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt.scale, { x: 1, y: 1 }, { duration: 500, easing: tween.easeInOut }); } }); harpoons.splice(j, 1); // Remove harpoon from the array // Create an explosion at the intersection point var explosion = new Explosion(); explosion.x = bubbleX; explosion.y = bubbleY; game.addChild(explosion); // Create a box at the intersection point with a 10% probability, but only if bubble wasn't split // and if there are no other boxes in the game if (!wasSplit && !game.children.some(function (child) { return child instanceof Box || child instanceof Box1 || child instanceof Box2 || child instanceof Box3; })) { if (Math.random() < 1) { var boxType = Math.floor(Math.random() * 4); var box; switch (boxType) { case 0: box = new Box(); break; case 1: box = new Box1(); break; case 2: box = new Box2(); break; case 3: box = new Box3(); break; } box.x = bubbleX; box.y = bubbleY; game.addChild(box); } } break; // Break out of the loop since we're deleting the bubble } } } for (var k = 0; k < boxes.length; k++) { var box = boxes[k]; for (var l = 0; l < harpoons.length; l++) { var harpoon = harpoons[l]; // Check if box collides with harpoon OR with harpoon's trail var hitsHarpoon = box.intersects(harpoon); var hitsTrail = harpoon.isPointOnLine({ x: box.x, y: box.y }, 1.0); // Using size 1.0 for boxes to ensure consistent behavior if (hitsHarpoon || hitsTrail) { box.destroy(); LK.getSound('powerup').play(); // Destroy all trail segments if (harpoon.trails) { harpoon.trails.forEach(function (trail) { trail.destroy(); }); } harpoon.destroy(); // Display toast text based on the type of box destroyed var toastText = new Text2('', { size: 250, fill: 0xFFC0CB, font: "'Comic Sans MS', cursive, sans-serif" }); var toastTextBg = new Text2('', { size: 255, fill: 0xFF00AA, font: "'Comic Sans MS', cursive, sans-serif" }); toastText.anchor.set(0.5, 0.5); toastText.x = 2048 / 2; toastText.y = 2732 / 2; toastTextBg.anchor.set(0.5, 0.5); toastTextBg.x = 2048 / 2; toastTextBg.y = 2732 / 2; game.addChild(toastText); game.addChild(toastTextBg); if (box instanceof Box1) { toastText.setText("Smash!"); toastTextBg.setText("Smash!"); } else if (box instanceof Box2) { toastText.setText("Destruction!"); toastTextBg.setText("Destruction!"); } else if (box instanceof Box) { toastText.setText("Less madness!"); toastTextBg.setText("Less madness!"); } else if (box instanceof Box3) { toastText.setText("Life up!"); toastTextBg.setText("Life up!"); } // Tween the toast text to fade out and destroy after 2 seconds tween(toastText, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { toastText.destroy(); } }); tween(toastTextBg, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { toastTextBg.destroy(); } }); boxes.splice(k, 1); harpoons.splice(l, 1); // Check if the destroyed box is an instance of Box1 if (box instanceof Box1) { // Create six additional harpoons and trails for (var i = 1; i <= 3; i++) { var leftHarpoon = new Harpoon(); leftHarpoon.x = player.x - i * 150; leftHarpoon.y = player.y; game.addChild(leftHarpoon); var rightHarpoon = new Harpoon(); rightHarpoon.x = player.x + i * 150; rightHarpoon.y = player.y; game.addChild(rightHarpoon); // Set a timeout to remove the additional harpoons after 5 seconds LK.setTimeout(function (lh, rh) { if (lh.trails) { lh.trails.forEach(function (trail) { trail.destroy(); }); } lh.destroy(); if (rh.trails) { rh.trails.forEach(function (trail) { trail.destroy(); }); } rh.destroy(); }.bind(null, leftHarpoon, rightHarpoon), 5000); } } // Check if the destroyed box is an instance of Box and reduce bubble speed if (box instanceof Box) { var bubbles = game.children.filter(function (child) { return child instanceof Bubble; }); bubbles.forEach(function (bubble) { bubble.speed /= 2; }); LK.setTimeout(function () { bubbles.forEach(function (bubble) { bubble.speed *= 2; }); }, 5000); } // Check if the destroyed box is an instance of Box3 and lives are less than 3 if (box instanceof Box3 && lives < 3) { lives += 1; var heart = LK.getAsset('heart', { anchorX: 0.5, anchorY: 0.5, x: -1 * lives * 50, y: 50 }); LK.gui.topRight.addChild(heart); hearts.push(heart); } // Check if the destroyed box is an instance of Box2 if (box instanceof Box2) { var bubbles = game.children.filter(function (child) { return child instanceof Bubble; }); var bubblesDestroyed = bubbles.length; bubbles.forEach(function (bubble) { bubble.destroy(); }); score += bubblesDestroyed; scoreTxt.setText("Balloons popped: " + score.toString()); tween(scoreTxt.scale, { x: 2, y: 2 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(scoreTxt.scale, { x: 1, y: 1 }, { duration: 500, easing: tween.easeInOut }); } }); } break; } } } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Box class
var Box = Container.expand(function () {
var self = Container.call(this);
var boxGraphics = self.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
// Increase the scale to enlarge the bounding box
scaleY: 1.2
});
self.speed = 5;
self.update = function () {
self.y += self.speed;
if (self.y > 2732) {
self.destroy();
}
};
});
// Box1 class
var Box1 = Container.expand(function () {
var self = Container.call(this);
var boxGraphics = self.attachAsset('box1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
// Increase the scale to enlarge the bounding box
scaleY: 1.2
});
self.speed = 5;
self.update = function () {
self.y += self.speed;
if (self.y > 2732) {
self.destroy();
}
};
});
// Box2 class
var Box2 = Container.expand(function () {
var self = Container.call(this);
var boxGraphics = self.attachAsset('box2', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
// Increase the scale to enlarge the bounding box
scaleY: 1.2
});
self.speed = 7;
self.update = function () {
self.y += self.speed;
if (self.y > 2732) {
self.destroy();
}
};
});
// Box3 class
var Box3 = Container.expand(function () {
var self = Container.call(this);
var boxGraphics = self.attachAsset('box3', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
// Increase the scale to enlarge the bounding box
scaleY: 1.2
});
self.speed = 9;
self.update = function () {
self.y += self.speed;
if (self.y > 2732) {
self.destroy();
}
};
});
// The assets will be automatically created and loaded by the LK engine.
// Bubble class
var Bubble = Container.expand(function () {
var self = Container.call(this);
var bubbleGraphics = self.attachAsset('bubble', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1 // Start fully visible instead of transparent
});
// Generate random color for the bubble
var randomColor = Math.random() * 0xFFFFFF;
bubbleGraphics.tint = randomColor;
// Allow for different bubble sizes (original size = 1.0)
self.size = 1.0;
// Update bubble size based on the size property
bubbleGraphics.scale.set(self.size);
// Physics constants for more consistent behavior
self.gravity = 0.2; // Gravity constant
self.bounceFactor = 0.7; // Energy loss on bounce (30% loss)
self.friction = 0.98; // Air/ground friction
self.speed = 3; // Vertical speed (significantly reduced initial speed)
// Track last position for collision detection
self.lastY = 0;
self.lastIntersecting = false;
// Method to split the bubble into two smaller ones
self.split = function () {
// Only split if bubble is not too small
// Note: We're keeping the same threshold (0.3) to determine when bubbles stop splitting
if (self.size <= 0.3) {
// Track that this is a small bubble that can't be split anymore
console.log("Bubble too small to split, size=" + self.size);
return false;
}
// Create two new smaller bubbles
var halfSize = self.size * 0.5;
console.log("Splitting bubble: original size=" + self.size + ", new size=" + halfSize);
// Create left bubble
var leftBubble = new Bubble();
leftBubble.x = self.x;
leftBubble.y = self.y;
leftBubble.size = halfSize;
leftBubble.speedX = -Math.abs(self.speedX) * 1.2; // Move left, slightly faster
leftBubble.speed = -5 - Math.random() * 3; // Jump up
leftBubble.lastY = leftBubble.y;
leftBubble.lastIntersecting = false;
if (leftBubble.children && leftBubble.children.length > 0) {
leftBubble.children[0].scale.set(halfSize);
// Generate a new random color for split bubble
leftBubble.children[0].tint = Math.random() * 0xFFFFFF;
}
// Create right bubble
var rightBubble = new Bubble();
rightBubble.x = self.x;
rightBubble.y = self.y;
rightBubble.size = halfSize;
rightBubble.speedX = Math.abs(self.speedX) * 1.2; // Move right, slightly faster
rightBubble.speed = -5 - Math.random() * 3; // Jump up
rightBubble.lastY = rightBubble.y;
rightBubble.lastIntersecting = false;
if (rightBubble.children && rightBubble.children.length > 0) {
rightBubble.children[0].scale.set(halfSize);
// Generate a new random color for split bubble
rightBubble.children[0].tint = Math.random() * 0xFFFFFF;
}
// Add both bubbles to the game
game.addChild(leftBubble);
game.addChild(rightBubble);
return true;
};
self.update = function () {
// Track last position before moving
self.lastY = self.y;
self.lastIntersecting = self.intersects(player);
self.speed += 0.2; // Further reduced gravity acceleration from 0.3 to 0.2
self.y += self.speed;
self.x += self.speedX; // Update horizontal position based on speedX
// Bounce off the left and right margins with a small boost to ensure movement
if (self.x <= 100 && self.speedX < 0 || self.x >= 1948 && self.speedX > 0) {
self.speedX *= -1.1; // Reverse horizontal direction with a slight boost to ensure movement
}
// Bounce off the ground (bottom of the screen)
if (self.y >= 2732 - 100) {
// Account for bubble size/radius
self.y = 2732 - 100; // Reset position to prevent sinking below ground
// Calculate bounce height based on bubble size
// Larger bubbles bounce higher (500-800px), smaller bubbles bounce lower (300-500px)
var bounceHeight = 400 + self.size * 1000; // Size 1.0 = 800px, Size 0.3 = 450px
// Calculate velocity needed to reach the desired height
var sizeBasedVelocity = -Math.sqrt(2 * self.gravity * bounceHeight);
var naturalBounce = -self.speed * 0.7; // Original dampening logic (70% of original speed)
// Use the stronger of the two values to ensure minimum bounce height
self.speed = Math.min(naturalBounce, sizeBasedVelocity);
// Add a small random horizontal impulse on bounce for more natural movement
self.speedX += (Math.random() - 0.5) * 2;
// Add a slight color flash when bouncing (if not a rainbow bubble)
if (!self.isRainbow && self.children && self.children.length > 0) {
var bubbleGraphics = self.children[0];
// Store original tint
var originalTint = bubbleGraphics.tint;
// Slightly lighten the bubble on bounce for visual feedback
var brighterTint = 0xFFFFFF;
// Flash to brighter color
tween(bubbleGraphics, {
tint: brighterTint
}, {
duration: 100,
onFinish: function onFinish() {
// Return to original tint
tween(bubbleGraphics, {
tint: originalTint
}, {
duration: 300
});
}
});
}
}
// Removed tint application when reaching a certain Y position
// Bubble class update method where player collision is detected
if (!self.lastIntersecting && self.intersects(player)) {
// Make the player blink three times with rainbow colors when hit by a bubble
if (player.children && player.children.length > 0 && !player.isDying) {
var rainbowTint = function rainbowTint() {
// Convert HSL to RGB (simplified version)
rainbowHue = (rainbowHue + 20) % 360; // Faster color change
// Convert hue (0-360) to RGB (0-255)
var h = rainbowHue / 60;
var c = 1; // Chroma
var x = c * (1 - Math.abs(h % 2 - 1));
var r, g, b;
if (h < 1) {
r = c;
g = x;
b = 0;
} else if (h < 2) {
r = x;
g = c;
b = 0;
} else if (h < 3) {
r = 0;
g = c;
b = x;
} else if (h < 4) {
r = 0;
g = x;
b = c;
} else if (h < 5) {
r = x;
g = 0;
b = c;
} else {
r = c;
g = 0;
b = x;
}
// Convert to RGB format
return (Math.floor(r * 255) << 16) + (Math.floor(g * 255) << 8) + Math.floor(b * 255);
}; // First blink (1/3)
var playerGraphics = player.children[0];
// Save original tint
var originalTint = playerGraphics.tint || 0xFFFFFF;
// Create rainbow animation function for player blinking
var rainbowHue = 0;
playerGraphics.tint = rainbowTint(); // Initial rainbow color
tween(playerGraphics, {
alpha: 0
}, {
duration: 100,
onFinish: function onFinish() {
playerGraphics.tint = rainbowTint(); // New rainbow color
tween(playerGraphics, {
alpha: 1
}, {
duration: 100,
onFinish: function onFinish() {
// Second blink (2/3)
playerGraphics.tint = rainbowTint(); // New rainbow color
tween(playerGraphics, {
alpha: 0
}, {
duration: 100,
onFinish: function onFinish() {
playerGraphics.tint = rainbowTint(); // New rainbow color
tween(playerGraphics, {
alpha: 1
}, {
duration: 100,
onFinish: function onFinish() {
// Third blink (3/3)
playerGraphics.tint = rainbowTint(); // New rainbow color
tween(playerGraphics, {
alpha: 0
}, {
duration: 100,
onFinish: function onFinish() {
playerGraphics.tint = rainbowTint(); // Final rainbow color
tween(playerGraphics, {
alpha: 1
}, {
duration: 100,
onFinish: function onFinish() {
// Reset tint to original color
playerGraphics.tint = originalTint;
}
});
}
});
}
});
}
});
}
});
}
});
}
// Instead of destroying the bubble, try to split it
var wasSplit = self.split();
// Always destroy the original bubble after splitting or if too small to split
self.destroy();
lives -= 1;
// Remove a heart icon when a life is lost
if (hearts.length > lives) {
var heartToRemove = hearts.pop();
if (heartToRemove) {
tween(heartToRemove.scale, {
x: 0,
y: 0
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
heartToRemove.destroy();
}
});
}
}
if (lives < 0) {
// Trigger player death animation before showing game over
if (player && !player.isDying) {
player.die();
// Delay game over screen until animation completes
LK.setTimeout(function () {
LK.showGameOver();
}, 2000); // Wait for death animation to complete
} else {
LK.showGameOver();
}
}
}
};
});
// Explosion class
var Explosion = Container.expand(function () {
var self = Container.call(this);
var explosionGraphics = self.attachAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5
});
tween(explosionGraphics, {
scaleX: explosionGraphics.scaleX + 1,
scaleY: explosionGraphics.scaleY + 1
}, {
duration: 1000,
easing: tween.bounceOut,
onFinish: function onFinish() {
self.destroy();
}
});
self.update = function () {
// The explosion will disappear after a while
if (self.alpha > 0) {
self.alpha -= 0.02;
} else {
self.destroy();
}
};
});
// Harpoon class
var Harpoon = Container.expand(function () {
var self = Container.call(this);
var harpoonGraphics = self.attachAsset('harpoon', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = -20;
self.originalSpeed = -20;
self.maxDistance = 2732 * 0.85; // Maximum distance - 85% of screen height
self.startY = 0; // Will store the starting Y position
self.trails = [];
// Apply a quick acceleration tween to give a nice launch effect
tween(self, {
speed: self.speed * 1.2 // Accelerate to 1.2x speed
}, {
duration: 200,
easing: tween.easeOut
});
// Function to check if point is on line (trail)
self.isPointOnLine = function (point, bubbleSize) {
// The trail is a vertical line from the harpoon down to the player
// Get bubble size factor (if not provided, default to 1)
var sizeFactor = bubbleSize || 1.0;
// Special handling for the smallest bubbles - much more generous
if (sizeFactor <= 0.25) {
// For very small bubbles, use a very large fixed tolerance
var tolerance = 100; // Extremely generous for the tiniest bubbles
// More lenient y-range check for tiny bubbles
var minY = self.y - 100; // Allow collision even slightly above the harpoon
var maxY = 2732 + 100; // Allow collision even slightly below the bottom
// Very generous x-range check for tiny bubbles
return Math.abs(point.x - self.x) < tolerance && point.y >= minY && point.y <= maxY;
}
// For all other bubble sizes, use scaled tolerance
var baseWidth = 50; // Base tolerance for normal bubbles
var tolerance;
if (sizeFactor <= 0.3) {
tolerance = 150; // Still very generous for small bubbles
} else if (sizeFactor < 1.0) {
// Scale tolerance inversely with bubble size
tolerance = baseWidth * (2 - sizeFactor);
} else {
tolerance = baseWidth; // Normal tolerance for regular bubbles
}
// Check if point is on main harpoon or any trail segment
var yCheck = point.y >= self.y && point.y <= 2732;
// First check against harpoon itself
if (Math.abs(point.x - self.x) < tolerance && yCheck) {
return true;
}
// Then check against each trail segment
for (var i = 0; i < self.trails.length; i++) {
var trail = self.trails[i];
if (Math.abs(point.x - self.x) < tolerance && point.y >= trail.y - trail.height / 2 && point.y <= trail.y + trail.height / 2) {
return true;
}
}
return false;
};
self.update = function () {
self.y += self.speed;
// Store initial position on first update
if (self.startY === 0) {
self.startY = self.y;
}
// Calculate total trail distance
var totalDistance = self.startY - self.y;
// Create trail segments for every 100 pixels
var segmentCount = Math.floor(totalDistance / 100);
var currentTrailCount = self.trails.length;
// Create new trail segments if needed
if (segmentCount > currentTrailCount) {
for (var i = currentTrailCount; i < segmentCount; i++) {
var trail = game.addChild(new Trail());
trail.x = self.x;
trail.y = self.y + 50 + i * 100;
trail.tint = 0x3843ab;
trail.width = 20;
trail.height = 70;
self.trails.push(trail);
}
}
// Update position of all trail segments
for (var i = 0; i < self.trails.length; i++) {
var trail = self.trails[i];
trail.y = self.y + 50 + i * 100;
}
// Destroy when reaching max distance or going off-screen
if (self.y < 0 || self.startY - self.y > self.maxDistance) {
self.destroy();
self.trails.forEach(function (trail) {
trail.destroy();
});
}
};
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 5;
self.isDying = false;
self.deathSpeed = 0;
self.deathRotation = 0;
self.lastX = 0; // Track the last X position to detect direction of movement
self.update = function () {
// Handle death animation if player is dying
if (self.isDying) {
// Accelerate the fall
self.deathSpeed += 0.5;
self.y += self.deathSpeed;
// Flip the player while falling (rotate vertically)
self.deathRotation += 0.05;
self.rotation = self.deathRotation;
// Add slight horizontal movement for more natural fall
self.x += Math.sin(self.deathRotation * 2) * 3;
// Slowly make the player more transparent
if (self.alpha > 0.1) {
self.alpha -= 0.01;
}
// Check if player has fallen off the screen
if (self.y > 2732 + 500) {
self.isDying = false;
self.destroy();
}
return;
}
// Normal movement when not dying
if (self.direction && self.direction === 'left') {
self.x -= self.speed;
} else if (self.direction === 'right') {
self.x += self.speed;
}
// Check if player has moved since last update and in which direction
if (self.x !== self.lastX && !self.isDying) {
// If moving right (x increasing), make sure scale is positive (not mirrored)
if (self.x > self.lastX && self.scaleX < 0) {
self.scaleX *= -1; // Flip to face right
}
// If moving left (x decreasing), make sure scale is negative (mirrored)
else if (self.x < self.lastX && self.scaleX > 0) {
self.scaleX *= -1; // Flip to face left
}
}
// Update lastX for next frame comparison
self.lastX = self.x;
};
self.die = function () {
if (self.isDying) {
return;
} // Prevent multiple death animations
self.isDying = true;
self.deathSpeed = 5;
// Play death sound (if available)
if (LK.getSound('explosion')) {
LK.getSound('explosion').play();
}
};
self.shoot = function () {
if (self.isDying) {
return;
} // Cannot shoot while dying
var harpoon = new Harpoon();
harpoon.x = player.x;
harpoon.y = player.y;
// Trails will be created automatically in the harpoon update method
game.addChild(harpoon);
LK.getSound('crossbow').play();
};
});
// PowerUpText class
var PowerUpText = Container.expand(function () {
var self = Container.call(this);
var textGraphics = self.attachAsset('PowerUpText', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.y -= 2;
if (self.y < 0) {
self.destroy();
}
};
});
// Trail class
var Trail = Container.expand(function () {
var self = Container.call(this);
var trailGraphics = self.attachAsset('line', {
anchorX: 0.5,
anchorY: 0.5,
width: 18,
alpha: 0.9,
// Slightly transparent
scaleY: 0.15 // Default scale for segment
});
// Custom setter for the height property
Object.defineProperty(self, 'height', {
get: function get() {
return trailGraphics.scaleY * 100; // Convert scale to height
},
set: function set(value) {
trailGraphics.scaleY = value / 100; // Convert height to scale
}
});
self.update = function () {
// Trail segments are now controlled by the harpoon
// They don't move independently
if (self.y > 2732) {
self.destroy();
}
};
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xFFFFFFFF // Init game with black background
});
/****
* Game Code
****/
var background = game.attachAsset('Landscape', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.setChildIndex(background, 0);
var player = new Player();
player.x = 2048 / 2;
player.y = 2732 - 180;
player.lastX = player.x; // Initialize lastX to current position
game.addChild(player); // Add player after trail to ensure correct rendering order
game.move = function (x, y, obj) {
if (player && !player.isDying) {
// Track the last position before moving (done in player.update)
player.x = x;
// Flipping is now handled in player.update based on movement direction
}
};
var score = 0;
var lives = 3;
var lastDifficultyLevel = 0; // Track the last level for showing difficulty announcements
// Create score background
var scoreBackground = new Container();
var scoreBgGraphics = scoreBackground.attachAsset('scoreBg', {
anchorX: 0.5,
anchorY: 0.1,
scaleX: 5,
scaleY: 5,
alpha: 1
});
// Create difficulty indicator
var difficultyIndicator = new Text2('Difficulty: Easy', {
size: 24,
fill: 0x00FF00,
font: "'Comic Sans MS', cursive, sans-serif"
});
difficultyIndicator.anchor.set(0, 0.5);
difficultyIndicator.x = 20;
difficultyIndicator.y = 50;
LK.gui.topLeft.addChild(difficultyIndicator);
// Function to update difficulty indicator based on score
function updateDifficultyIndicator() {
var level = Math.floor(score / 10);
var difficultyText = 'Difficulty: ';
var textColor = 0x00FF00;
if (level === 0) {
difficultyText += 'Very Easy';
textColor = 0x00FF00;
} else if (level === 1) {
difficultyText += 'Easy';
textColor = 0x66FF00;
} else if (level === 2) {
difficultyText += 'Medium';
textColor = 0xFFFF00;
} else if (level === 3) {
difficultyText += 'Challenging';
textColor = 0xFF9900;
} else if (level === 4) {
difficultyText += 'Hard';
textColor = 0xFF6600;
} else {
difficultyText += 'Expert';
textColor = 0xFF0000;
}
difficultyIndicator.setText(difficultyText);
difficultyIndicator.fill = textColor;
}
scoreBackground.addChild(scoreBgGraphics);
scoreBackground.x = 0;
scoreBackground.y = 0;
LK.gui.top.addChild(scoreBackground);
// Create score text
var scoreTxt = new Text2('Bubbles popped: 0', {
size: 30,
fill: 0x007bff,
font: "'Comic Sans MS', cursive, sans-serif"
});
scoreTxt.anchor.set(0.5, -0.1);
scoreBackground.addChild(scoreTxt);
// Function to show difficulty level change announcement
function showDifficultyAnnouncement(level) {
var difficultyText = '';
var textColor = 0xFFFFFF;
switch (level) {
case 1:
difficultyText = "Warming Up!";
textColor = 0x00FF00;
break;
case 2:
difficultyText = "Getting Started!";
textColor = 0xFFFF00;
break;
case 3:
difficultyText = "Picking Up Pace!";
textColor = 0xFF9900;
break;
case 4:
difficultyText = "More Bubbles!";
textColor = 0xFF6600;
break;
case 5:
difficultyText = "Getting Challenging!";
textColor = 0xFF3300;
break;
default:
if (level > 5) {
difficultyText = "Level " + (level - 5) + "!";
textColor = 0xFF0000;
}
}
if (difficultyText) {
// Create text for announcement
var announcement = new Text2(difficultyText, {
size: 100,
fill: textColor,
font: "'Comic Sans MS', cursive, sans-serif"
});
announcement.anchor.set(0.5, 0.5);
announcement.x = 2048 / 2;
announcement.y = 2732 / 2;
announcement.alpha = 0;
game.addChild(announcement);
// Fade in
tween(announcement, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Hold for a moment
LK.setTimeout(function () {
// Fade out
tween(announcement, {
alpha: 0
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
announcement.destroy();
}
});
}, 1500);
}
});
// Play a sound for level up
LK.getSound('powerup').play();
}
}
var hearts = [];
for (var i = 0; i < lives; i++) {
var heart = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
x: -1 * (i + 1) * 50,
// Position hearts with some spacing
y: 50
});
LK.gui.topRight.addChild(heart);
hearts.push(heart);
}
var lastShot = -999;
game.down = function (x, y, obj) {
if (LK.ticks - lastShot > 10 && player && !player.isDying) {
player.shoot();
lastShot = LK.ticks;
}
};
// Start the music 'chinese' upon starting the game
LK.playMusic('arcade');
game.update = function () {
// Create boxes array before using it
var boxes = game.children.filter(function (child) {
return child instanceof Box || child instanceof Box1 || child instanceof Box2 || child instanceof Box3;
});
// Dynamic bubble spawn rate based on score
// Base spawn interval is much slower at start (300 ticks) and only gradually decreases based on score
// This makes the start of the game much easier
var baseInterval = 300; // Start with very slow spawn rate (5 seconds between bubbles)
var minInterval = 60; // Minimum spawn interval is now higher (1 second)
var reductionPerPoint = 2; // Slower progression - only reduce by 2 ticks per point
var intervalReductionThreshold = 10; // Only start making game harder after scoring 10 points
// Calculate spawn interval with much gentler progression
var scoreBasedReduction = Math.max(0, score - intervalReductionThreshold) * reductionPerPoint;
var spawnInterval = Math.max(minInterval, baseInterval - scoreBasedReduction);
// Only spawn a bubble if current tick matches spawn interval
if (LK.ticks % spawnInterval === 0) {
var newBubble = new Bubble();
newBubble.x = Math.random() * 2048;
newBubble.y = 0;
// Reduced horizontal speed for more predictable trajectories
var minSpeed = 3; // Reduced from 5
var maxSpeed = 6; // Reduced from 8
var randomSpeed = minSpeed + Math.random() * (maxSpeed - minSpeed);
newBubble.speedX = Math.random() < 0.5 ? -randomSpeed : randomSpeed;
// Give bubbles a smaller initial vertical speed for gentler physics
newBubble.speed = 1 + Math.random() * 2; // Reduced from 2 + random * 2
// Set bounce count to track how many times the bubble has bounced
newBubble.bounceCount = 0;
newBubble.maxBounces = 3 + Math.floor(Math.random() * 3);
// Initialize tracking properties for the bubble
newBubble.lastY = newBubble.y;
newBubble.lastIntersecting = false;
// For special bubbles (every 10th bubble), create a rainbow effect
if (score > 30 && Math.random() < 0.1) {
// Set the initial tint
var hue = 0;
// Store the original tint for reference
newBubble.originalTint = newBubble.children[0].tint;
// Flag this bubble as a special rainbow bubble
newBubble.isRainbow = true;
// Start the rainbow animation
newBubble.rainbowTween = function () {
// Convert HSL to RGB (simplified version)
hue = (hue + 1) % 360;
// Convert hue (0-360) to RGB (0-255)
var h = hue / 60;
var c = 1; // Chroma
var x = c * (1 - Math.abs(h % 2 - 1));
var r, g, b;
if (h < 1) {
r = c;
g = x;
b = 0;
} else if (h < 2) {
r = x;
g = c;
b = 0;
} else if (h < 3) {
r = 0;
g = c;
b = x;
} else if (h < 4) {
r = 0;
g = x;
b = c;
} else if (h < 5) {
r = x;
g = 0;
b = c;
} else {
r = c;
g = 0;
b = x;
}
// Convert to RGB format
var rgb = (Math.floor(r * 255) << 16) + (Math.floor(g * 255) << 8) + Math.floor(b * 255);
// Apply the color
if (newBubble.children && newBubble.children.length > 0) {
newBubble.children[0].tint = rgb;
}
// Continue the animation next frame
newBubble.rainbowTimeout = LK.setTimeout(newBubble.rainbowTween, 50);
};
// Start the rainbow animation
newBubble.rainbowTween();
}
game.addChild(newBubble);
// Log difficulty progression for debugging
if (score > 0 && score % 10 === 0 && LK.ticks % spawnInterval === 0) {
console.log("Score: " + score + ", New spawn interval: " + spawnInterval + " ms (" + (spawnInterval / 60).toFixed(1) + " seconds between bubbles)");
}
}
// Make sure to initialize all arrays at the beginning
var bubbles = game.children.filter(function (child) {
return child instanceof Bubble;
});
// boxes array is now initialized at the beginning of update function
// to prevent 'Cannot read properties of undefined' error
var harpoons = game.children.filter(function (child) {
return child instanceof Harpoon;
});
for (var i = 0; i < bubbles.length; i++) {
var bubble = bubbles[i];
// Collision handling is now done in the Bubble class update method
for (var j = 0; j < harpoons.length; j++) {
var harpoon = harpoons[j];
// Check if bubble collides with harpoon OR with harpoon's trail
var hitsHarpoon = bubble.intersects(harpoon);
var hitsTrail = harpoon.isPointOnLine({
x: bubble.x,
y: bubble.y
}, bubble.size);
// For very small bubbles, do additional detection with shifted positions to improve hit detection
if (!hitsTrail && bubble.size <= 0.25) {
// Try multiple points in a wider pattern around the bubble for tiny bubbles
// Use more test points with larger offsets
var offset = 20; // Increase test point spread
var testPoints = [
// Square pattern around center
{
x: bubble.x - offset,
y: bubble.y
}, {
x: bubble.x + offset,
y: bubble.y
}, {
x: bubble.x,
y: bubble.y - offset
}, {
x: bubble.x,
y: bubble.y + offset
},
// Diagonal test points
{
x: bubble.x - offset,
y: bubble.y - offset
}, {
x: bubble.x + offset,
y: bubble.y - offset
}, {
x: bubble.x - offset,
y: bubble.y + offset
}, {
x: bubble.x + offset,
y: bubble.y + offset
},
// Even wider points for extreme edge cases
{
x: bubble.x - offset * 1.5,
y: bubble.y
}, {
x: bubble.x + offset * 1.5,
y: bubble.y
}];
// Check each test point
for (var p = 0; p < testPoints.length; p++) {
if (harpoon.isPointOnLine(testPoints[p], bubble.size)) {
hitsTrail = true;
// Log when a tiny bubble is hit
console.log("Small bubble hit by trail: size=" + bubble.size + ", x=" + bubble.x + ", harpoon.x=" + harpoon.x + ", point=" + testPoints[p].x + "," + testPoints[p].y);
break;
}
}
}
if (hitsHarpoon || hitsTrail) {
// Clean up rainbow animation if this is a rainbow bubble
if (bubble.isRainbow && bubble.rainbowTimeout) {
LK.clearTimeout(bubble.rainbowTimeout);
bubble.rainbowTimeout = null;
}
// Destroy small bubbles immediately when hit by the trail
if (bubble.size <= 0.25) {
// Show a small explosion effect
var explosion = new Explosion();
explosion.x = bubble.x;
explosion.y = bubble.y;
// explosion.scale.set(bubble.size);
game.addChild(explosion);
// Play explosion sound
LK.getSound('explosion').play();
// Show Explosion asset for tiny bubbles (instead of Boom! text)
var explosion = new Explosion();
explosion.x = bubble.x;
explosion.y = bubble.y;
// Adjust explosion size based on bubble size
explosion.scale.set(bubble.size * 1.5); // Slightly larger than bubble for visual effect
game.addChild(explosion);
// Update score
score += 1;
scoreTxt.setText("Bubbles popped: " + score.toString());
// Destroy bubble and harpoon
bubble.destroy();
bubbles.splice(i, 1);
// Check if harpoon.trail exists before destroying it
if (harpoon.trails) {
harpoon.trails.forEach(function (trail) {
trail.destroy();
});
} else if (harpoon.trail) {
harpoon.trail.destroy();
}
harpoon.destroy();
harpoons.splice(j, 1);
continue; // Skip further checks for this bubble
}
// Log small bubble collisions for debugging
if (bubble.size <= 0.3) {
// Check for trail hits on small bubbles, store the hit location for verification
if (hitsTrail && !hitsHarpoon) {
// Create temporary visual indicator at hit point (for debug)
var debugHitSpot = LK.getAsset('bubble', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1,
scaleY: 0.1,
alpha: 0.8,
tint: 0xFF0000 // Red to make it visible
});
debugHitSpot.x = bubble.x;
debugHitSpot.y = bubble.y;
game.addChild(debugHitSpot);
// Remove the debug hit spot after a short duration
LK.setTimeout(function () {
debugHitSpot.destroy();
}, 300);
// Add log message for each trail segment
if (harpoon.trails) {
for (var t = 0; t < harpoon.trails.length; t++) {
var trail = harpoon.trails[t];
console.log("Trail segment " + t + ": x=" + trail.x + ", y=" + trail.y + ", height=" + trail.height);
}
}
}
}
// Store current position and data for explosion
var bubbleX = bubble.x;
var bubbleY = bubble.y;
var bubbleSize = bubble.size || 1.0;
var bubbleSpeedX = bubble.speedX;
// Clean up rainbow animation if this is a rainbow bubble
if (bubble.isRainbow && bubble.rainbowTimeout) {
LK.clearTimeout(bubble.rainbowTimeout);
bubble.rainbowTimeout = null;
}
// Try to split the bubble
var wasSplit = false;
if (bubbleSize > 0.3) {
// Call split method to create two smaller bubbles
wasSplit = bubble.split();
var boxes = game.children.filter(function (child) {
return child instanceof Box || child instanceof Box1 || child instanceof Box2 || child instanceof Box3;
});
// Remove the original bubble
bubble.destroy();
// Play explosion sound
LK.getSound('explosion').play();
// Remove the harpoon and all its trail segments
if (harpoon.trails) {
harpoon.trails.forEach(function (trail) {
trail.destroy();
});
}
harpoon.destroy();
bubbles.splice(i, 1); // Remove bubble from the array
// Update score and UI
score += 1;
scoreTxt.setText("Bubbles popped: " + score.toString());
// Check if we've reached a new difficulty level (every 10 points)
var currentDifficultyLevel = Math.floor(score / 10);
if (currentDifficultyLevel > lastDifficultyLevel) {
lastDifficultyLevel = currentDifficultyLevel;
// Show difficulty level announcement
showDifficultyAnnouncement(currentDifficultyLevel);
// Update the difficulty indicator
updateDifficultyIndicator();
}
tween(scoreTxt.scale, {
x: 2,
y: 2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt.scale, {
x: 1,
y: 1
}, {
duration: 500,
easing: tween.easeInOut
});
}
});
harpoons.splice(j, 1); // Remove harpoon from the array
// Create an explosion at the intersection point
var explosion = new Explosion();
explosion.x = bubbleX;
explosion.y = bubbleY;
game.addChild(explosion);
// Create a box at the intersection point with a 10% probability, but only if bubble wasn't split
// and if there are no other boxes in the game
if (!wasSplit && !game.children.some(function (child) {
return child instanceof Box || child instanceof Box1 || child instanceof Box2 || child instanceof Box3;
})) {
if (Math.random() < 1) {
var boxType = Math.floor(Math.random() * 4);
var box;
switch (boxType) {
case 0:
box = new Box();
break;
case 1:
box = new Box1();
break;
case 2:
box = new Box2();
break;
case 3:
box = new Box3();
break;
}
box.x = bubbleX;
box.y = bubbleY;
game.addChild(box);
}
}
break; // Break out of the loop since we're deleting the bubble
}
}
}
for (var k = 0; k < boxes.length; k++) {
var box = boxes[k];
for (var l = 0; l < harpoons.length; l++) {
var harpoon = harpoons[l];
// Check if box collides with harpoon OR with harpoon's trail
var hitsHarpoon = box.intersects(harpoon);
var hitsTrail = harpoon.isPointOnLine({
x: box.x,
y: box.y
}, 1.0); // Using size 1.0 for boxes to ensure consistent behavior
if (hitsHarpoon || hitsTrail) {
box.destroy();
LK.getSound('powerup').play();
// Destroy all trail segments
if (harpoon.trails) {
harpoon.trails.forEach(function (trail) {
trail.destroy();
});
}
harpoon.destroy();
// Display toast text based on the type of box destroyed
var toastText = new Text2('', {
size: 250,
fill: 0xFFC0CB,
font: "'Comic Sans MS', cursive, sans-serif"
});
var toastTextBg = new Text2('', {
size: 255,
fill: 0xFF00AA,
font: "'Comic Sans MS', cursive, sans-serif"
});
toastText.anchor.set(0.5, 0.5);
toastText.x = 2048 / 2;
toastText.y = 2732 / 2;
toastTextBg.anchor.set(0.5, 0.5);
toastTextBg.x = 2048 / 2;
toastTextBg.y = 2732 / 2;
game.addChild(toastText);
game.addChild(toastTextBg);
if (box instanceof Box1) {
toastText.setText("Smash!");
toastTextBg.setText("Smash!");
} else if (box instanceof Box2) {
toastText.setText("Destruction!");
toastTextBg.setText("Destruction!");
} else if (box instanceof Box) {
toastText.setText("Less madness!");
toastTextBg.setText("Less madness!");
} else if (box instanceof Box3) {
toastText.setText("Life up!");
toastTextBg.setText("Life up!");
}
// Tween the toast text to fade out and destroy after 2 seconds
tween(toastText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
toastText.destroy();
}
});
tween(toastTextBg, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
toastTextBg.destroy();
}
});
boxes.splice(k, 1);
harpoons.splice(l, 1);
// Check if the destroyed box is an instance of Box1
if (box instanceof Box1) {
// Create six additional harpoons and trails
for (var i = 1; i <= 3; i++) {
var leftHarpoon = new Harpoon();
leftHarpoon.x = player.x - i * 150;
leftHarpoon.y = player.y;
game.addChild(leftHarpoon);
var rightHarpoon = new Harpoon();
rightHarpoon.x = player.x + i * 150;
rightHarpoon.y = player.y;
game.addChild(rightHarpoon);
// Set a timeout to remove the additional harpoons after 5 seconds
LK.setTimeout(function (lh, rh) {
if (lh.trails) {
lh.trails.forEach(function (trail) {
trail.destroy();
});
}
lh.destroy();
if (rh.trails) {
rh.trails.forEach(function (trail) {
trail.destroy();
});
}
rh.destroy();
}.bind(null, leftHarpoon, rightHarpoon), 5000);
}
}
// Check if the destroyed box is an instance of Box and reduce bubble speed
if (box instanceof Box) {
var bubbles = game.children.filter(function (child) {
return child instanceof Bubble;
});
bubbles.forEach(function (bubble) {
bubble.speed /= 2;
});
LK.setTimeout(function () {
bubbles.forEach(function (bubble) {
bubble.speed *= 2;
});
}, 5000);
}
// Check if the destroyed box is an instance of Box3 and lives are less than 3
if (box instanceof Box3 && lives < 3) {
lives += 1;
var heart = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
x: -1 * lives * 50,
y: 50
});
LK.gui.topRight.addChild(heart);
hearts.push(heart);
}
// Check if the destroyed box is an instance of Box2
if (box instanceof Box2) {
var bubbles = game.children.filter(function (child) {
return child instanceof Bubble;
});
var bubblesDestroyed = bubbles.length;
bubbles.forEach(function (bubble) {
bubble.destroy();
});
score += bubblesDestroyed;
scoreTxt.setText("Balloons popped: " + score.toString());
tween(scoreTxt.scale, {
x: 2,
y: 2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(scoreTxt.scale, {
x: 1,
y: 1
}, {
duration: 500,
easing: tween.easeInOut
});
}
});
}
break;
}
}
}
}
};
a green cross, icon, pixel style. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
a sand clock pixel style.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
a bomb, pixel style. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
a pixel harpoon, vertical and looking up, retro like in pang games.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
A banner to show a message, pixel art. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
two harpoons looking up, retro, pixel. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
A Mount fuji background with a big rainbow crossing from side to side in the sky, pixel style, colourful. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows