/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ var AimArrow = Container.expand(function () { var self = Container.call(this); // Create arrow components var arrowLine = self.attachAsset('aimLine', { anchorX: 0.5, anchorY: 1.0, width: 10, height: 150 }); // Set initial visibility self.visible = false; // Update arrow position and rotation based on angle self.updateArrow = function (startX, startY, angle) { self.x = startX; self.y = startY; self.rotation = angle; self.visible = true; }; self.hide = function () { self.visible = false; }; return self; }); var BowlingPin = Container.expand(function () { var self = Container.call(this); var pinGraphics = self.attachAsset('bowlingPin', { anchorX: 0.5, anchorY: 0.5 }); self.isKnockedDown = false; self.velocityX = 0; self.velocityY = 0; self.friction = 0.95; self.lastX = 0; self.lastY = 0; self.row = 0; // Store which row this pin belongs to self.mass = 1 + Math.random() * 0.2; // Slight mass variation for more realistic physics self.collisionEnergy = 0; // Track collision energy for better pin-to-pin interactions self.rotationVelocity = 0; // Track rotation velocity separately self.knockDown = function () { if (!self.isKnockedDown) { self.isKnockedDown = true; // Calculate direction based on the impact var directionX = Math.random() - 0.5; var directionY = Math.random() * 0.3 + 0.7; // Mostly forward, slight randomness // More realistic physics - calculate velocity based on position, mass, etc. self.velocityX = directionX * (5 + Math.random() * 3); self.velocityY = directionY * (5 + Math.random() * 3); self.rotationVelocity = (Math.random() - 0.5) * 0.2; // Add collision energy for pin-to-pin interactions self.collisionEnergy = 10; LK.getSound('pinHit').play(); return true; } return false; }; self.applyImpact = function (impactX, impactY, energy) { // Apply an impact force from another object (pin or penguin) // Direction based on impact position var dirX = self.x - impactX; var dirY = self.y - impactY; // Normalize var length = Math.sqrt(dirX * dirX + dirY * dirY); if (length > 0) { dirX /= length; dirY /= length; } // Apply velocity based on energy and direction var forceScale = energy / self.mass; self.velocityX += dirX * forceScale; self.velocityY += dirY * forceScale; // Add some rotation based on impact offset var rotationImpact = (Math.random() - 0.5) * 0.1 * forceScale; self.rotationVelocity += rotationImpact; // Mark as knocked down if (!self.isKnockedDown) { self.isKnockedDown = true; self.collisionEnergy = energy * 0.8; // Transfer most of the energy LK.getSound('pinHit').play(); return true; } return false; }; self.update = function () { // Store last position for collision detection self.lastX = self.x; self.lastY = self.y; if (self.isKnockedDown) { // Apply velocity self.x += self.velocityX; self.y += self.velocityY; // Apply friction self.velocityX *= self.friction; self.velocityY *= self.friction; // Apply rotation pinGraphics.rotation += self.rotationVelocity; self.rotationVelocity *= 0.98; // Dampen rotation // Gradually reduce collision energy self.collisionEnergy *= 0.95; // More realistic falling behavior with physics-based motion // Pins slow down more when they're almost stopped if (Math.abs(self.velocityX) < 0.5 && Math.abs(self.velocityY) < 0.5) { self.velocityX *= 0.9; self.velocityY *= 0.9; } // Slower fade out for more realistic pin falling if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) { pinGraphics.alpha *= 0.99; // Slower fade when stopped } else { pinGraphics.alpha *= 0.995; // Normal fade when moving } // Remove from game when almost invisible if (pinGraphics.alpha < 0.1) { self.visible = false; } } }; self.reset = function () { self.isKnockedDown = false; self.velocityX = 0; self.velocityY = 0; self.rotationVelocity = 0; self.collisionEnergy = 0; pinGraphics.rotation = 0; pinGraphics.alpha = 1; self.visible = true; }; return self; }); var Penguin = Container.expand(function () { var self = Container.call(this); var penguinGraphics = self.attachAsset('penguin', { anchorX: 0.5, anchorY: 0.5 }); // Physics properties self.velocityX = 0; self.velocityY = 0; self.friction = 0.985; // Further decreased value for even slower sliding self.active = false; self.hasCollided = false; self.burnCount = 0; // Track how many times penguin has hit the obstacle self.isBurned = false; // Track if penguin is burned self.pullDistance = 0; // Store the pull distance for power calculation self.launch = function (angle, power, dragSpeed) { // Use the drag speed to determine penguin velocity // dragSpeed is calculated based on how quickly the player dragged and released var speedMultiplier = power / MAX_POWER; // Base power from pull distance // Use dragSpeed as a multiplier to the base power calculation var baseSpeed = 18; // Minimum speed needed to move (reduced from 25) var reachPinsSpeed = 30; // Speed needed to reach pins (reduced from 40) var maxSpeed = baseSpeed + (reachPinsSpeed - baseSpeed) * speedMultiplier; // Multiply by dragSpeed factor for more dynamic speed control maxSpeed *= dragSpeed || 0.8; self.velocityX = Math.cos(angle) * maxSpeed; self.velocityY = Math.sin(angle) * maxSpeed; // Special case for throwing straight - make sure it reaches pins at max power if (Math.abs(Math.cos(angle)) < 0.2 && Math.sin(angle) < -0.8 && speedMultiplier > 0.9) { // Adjust vertical speed to ensure it reaches the pins self.velocityY = -reachPinsSpeed * (dragSpeed || 0.8); } self.active = true; self.hasCollided = false; LK.getSound('slide').play(); }; self.update = function () { if (!self.active) return; // Apply velocity self.x += self.velocityX; self.y += self.velocityY; // Apply friction // Ice physics - lower friction when moving fast, gradually increases as slowing down var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY); var dynamicFriction = speed > 10 ? 0.988 : speed > 5 ? 0.982 : 0.975; // More graduated and further reduced friction for slower ice physics self.velocityX *= dynamicFriction; self.velocityY *= dynamicFriction; // More realistic penguin rotation while sliding var rotationFactor = speed > 5 ? 0.005 : 0.01; // Less rotation at high speeds penguinGraphics.rotation += self.velocityX * rotationFactor; // Stop if velocity is very small if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) { self.active = false; self.velocityX = 0; self.velocityY = 0; penguinGraphics.rotation = 0; // Reset rotation when stopped } // Frame boundary checks with proper reflection vectors // Left boundary if (self.x < 0) { self.x = 0; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed // If X velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityX) < 0.5) { self.velocityX = 0; // Perfect 90 degrees means no horizontal movement self.velocityY = self.velocityY > 0 ? speed : -speed; // Maintain vertical direction } // Award wall bounce bonus if active and hasn't collided yet if (self.active && !self.hasCollided) { self.wallBounceCount = self.wallBounceCount || 0; self.wallBounceCount++; } } // Right boundary else if (self.x > 2048) { self.x = 2048; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed // If X velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityX) < 0.5) { self.velocityX = 0; // Perfect 90 degrees means no horizontal movement self.velocityY = self.velocityY > 0 ? speed : -speed; // Maintain vertical direction } } // Top boundary if (self.y < 0) { self.y = 0; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed // If Y velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityY) < 0.5) { self.velocityY = 0; // Perfect 90 degrees means no vertical movement self.velocityX = self.velocityX > 0 ? speed : -speed; // Maintain horizontal direction } } // Bottom boundary if (self.y > 2732) { self.y = 2732; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed // If Y velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityY) < 0.5) { self.velocityY = 0; // Perfect 90 degrees means no vertical movement self.velocityX = self.velocityX > 0 ? speed : -speed; // Maintain horizontal direction } } // Bowling lane boundary checks (light blue area) // Calculate bowling lane boundaries var laneLeftBoundary = bowlingLane.x - bowlingLane.width / 2; var laneRightBoundary = bowlingLane.x + bowlingLane.width / 2; var laneTopBoundary = bowlingLane.y; var laneBottomBoundary = bowlingLane.y + bowlingLane.height; // Check if penguin is outside the lane but not outside the game boundaries // Left lane boundary if (self.x < laneLeftBoundary && self.x > 0) { self.x = laneLeftBoundary; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed // If X velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityX) < 0.5) { self.velocityX = 0; self.velocityY = self.velocityY > 0 ? speed : -speed; } } // Right lane boundary else if (self.x > laneRightBoundary && self.x < 2048) { self.x = laneRightBoundary; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed // If X velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityX) < 0.5) { self.velocityX = 0; self.velocityY = self.velocityY > 0 ? speed : -speed; } } // Top lane boundary if (self.y < laneTopBoundary && self.y > 0) { self.y = laneTopBoundary; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed // If Y velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityY) < 0.5) { self.velocityY = 0; self.velocityX = self.velocityX > 0 ? speed : -speed; } } // Bottom lane boundary if (self.y > laneBottomBoundary && self.y < 2732) { self.y = laneBottomBoundary; // Calculate reflection vector for a 90-degree bounce with reduced speed var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8; self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed // If Y velocity is very small, set to zero for perfect 90-degree bounce if (Math.abs(self.velocityY) < 0.5) { self.velocityY = 0; self.velocityX = self.velocityX > 0 ? speed : -speed; } } }; self.reset = function () { self.x = launchArea.x; self.y = launchArea.y - 250; // Move penguin further up the lane self.velocityX = 0; self.velocityY = 0; self.active = false; self.hasCollided = false; self.pullDistance = 0; // Reset pull distance self.wallBounceCount = 0; // Reset wall bounce counter self.dragSpeed = 0; // Reset drag speed self.lastMoveTime = 0; // Reset time tracking self.lastMoveX = 0; // Reset position tracking self.lastMoveY = 0; // Reset position tracking penguinGraphics.rotation = 0; self.resetBurnState(); }; // Add function to reset burn state self.resetBurnState = function () { self.burnCount = 0; self.isBurned = false; // Restore original penguin image penguinGraphics.texture = LK.getAsset('penguin', {}).texture; }; // Add function to set penguin burned state self.setBurned = function () { self.isBurned = true; // Remove the old penguin graphics self.removeChild(penguinGraphics); // Create and add the burned penguin graphics with same anchor points penguinGraphics = self.attachAsset('burntPenguin', { anchorX: 0.5, anchorY: 0.5 }); // Flash red to show penguin is burned LK.effects.flashObject(self, 0xff0000, 1000); // Stop penguin movement self.velocityX = 0; self.velocityY = 0; self.active = false; }; return self; }); var PowerMeter = Container.expand(function () { var self = Container.call(this); var meterBG = self.attachAsset('powerMeterBG', { anchorX: 0.5, anchorY: 0.5 }); var meter = self.attachAsset('powerMeter', { anchorX: 0.5, anchorY: 1.0, height: 0 // Start with no power }); var maxPower = 300; self.power = 0; self.increasing = true; self.update = function () { if (self.visible) { if (self.increasing) { self.power += 5; if (self.power >= maxPower) { self.power = maxPower; self.increasing = false; } } else { self.power -= 5; if (self.power <= 0) { self.power = 0; self.increasing = true; } } // Update meter height based on power meter.height = self.power; meter.y = meterBG.y + meterBG.height / 2 - meter.height / 2; } }; self.getPowerRatio = function () { return self.power / maxPower; }; self.reset = function () { self.power = 0; self.increasing = true; meter.height = 0; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xC7E6F7 }); /**** * Game Code ****/ // Game constants var MAX_POWER = 15; // Reduced from 20 to make penguin slower var PIN_ROWS = 4; var FRAMES = 10; var PINS_PER_FRAME = 10; // Game state variables var currentFrame = 1; var pinsKnockedDown = 0; var totalScore = 0; var aiming = false; var powering = false; var gameState = "aiming"; // States: aiming, powering, sliding, scoring, gameover var aimAngle = -Math.PI / 2; // Start aiming straight up var consecutiveFailedStrikes = 0; // Track consecutive failures to get a strike var strikeInLastFrame = false; // Track if the player got a strike in the last frame var obstacle = null; // Reference to obstacle object // Create background var background = game.addChild(LK.getAsset('background', { anchorX: 0, anchorY: 0 })); // Create bowling lane var bowlingLane = game.addChild(LK.getAsset('bowlingLane', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 100 })); // Create launch area var launchArea = game.addChild(LK.getAsset('launchArea', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2400 })); // Create the aiming line var aimLine = game.addChild(LK.getAsset('aimLine', { anchorX: 0.5, anchorY: 1.0, x: launchArea.x, y: launchArea.y, visible: false })); // Arrow removed as requested // Create the power meter var powerMeter = game.addChild(new PowerMeter()); powerMeter.x = 1900; powerMeter.y = 1400; powerMeter.visible = false; // Create the penguin var penguin = game.addChild(new Penguin()); penguin.x = launchArea.x; penguin.y = launchArea.y - 250; // Move penguin further up the lane // Create bowling pins var pins = []; setupPins(); // Create UI elements var frameText = new Text2('Frame: 1/10', { size: 70, fill: 0x000000 }); frameText.anchor.set(0.5, 0); // Position frame text under the penguin's position frameText.y = penguin.y + 150; LK.gui.center.addChild(frameText); var scoreText = new Text2('Score: 0', { size: 70, fill: 0x000000 }); scoreText.anchor.set(0.5, 0); scoreText.y = 80; LK.gui.top.addChild(scoreText); var instructionText = new Text2('Tap and drag to pull penguin back, release to launch', { size: 50, fill: 0x000000 }); instructionText.anchor.set(0.5, 0); instructionText.y = 180; LK.gui.top.addChild(instructionText); // Helper functions function setupPins() { // Clear any existing pins for (var i = 0; i < pins.length; i++) { pins[i].destroy(); } pins = []; // Create pin layout in traditional bowling triangle formation // Ensure pins are exactly centered on the lane var startX = 2048 / 2; // Center of screen var startY = 400; // Position from top var pinSpacingX = 120; // Fixed horizontal spacing regardless of strikes var pinSpacingY = 120; // Fixed vertical spacing regardless of strikes // Standard bowling pin layout (4-3-2-1 triangle) // First row (back) - 4 pins for (var i = 0; i < 4; i++) { var pin = new BowlingPin(); pin.x = startX + (i - 1.5) * pinSpacingX; // Perfectly centered pin.y = startY; pin.row = 1; // Back row pin.lastX = pin.x; pin.lastY = pin.y; pins.push(pin); game.addChild(pin); } // Second row - 3 pins for (var i = 0; i < 3; i++) { var pin = new BowlingPin(); pin.x = startX + (i - 1) * pinSpacingX; // Centered relative to first row pin.y = startY + pinSpacingY; pin.row = 2; // Second row pin.lastX = pin.x; pin.lastY = pin.y; pins.push(pin); game.addChild(pin); } // Third row - 2 pins for (var i = 0; i < 2; i++) { var pin = new BowlingPin(); pin.x = startX + (i - 0.5) * pinSpacingX; // Centered relative to second row pin.y = startY + 2 * pinSpacingY; pin.row = 3; // Third row pin.lastX = pin.x; pin.lastY = pin.y; pins.push(pin); game.addChild(pin); } // Fourth row (front) - 1 pin var pin = new BowlingPin(); pin.x = startX; // Center pin pin.y = startY + 3 * pinSpacingY; pin.row = 4; // Front row pin.lastX = pin.x; pin.lastY = pin.y; pins.push(pin); game.addChild(pin); pinsKnockedDown = 0; // Reset penguin burn state for new frame penguin.resetBurnState(); // Remove any existing obstacle if (typeof obstacle !== 'undefined' && obstacle) { obstacle.destroy(); obstacle = null; } // Add obstacle after strike if (currentFrame > 1 && strikeInLastFrame) { // Create obstacle based on a random position (left or right) var obstaclePosition = Math.random() < 0.5 ? "left" : "right"; obstacle = game.addChild(LK.getAsset('horizontalline', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 50, x: startX, y: 1200 })); // If obstacle is on the left side, move it to the left half of the lane if (obstaclePosition === "left") { obstacle.x = startX - 300; } else { obstacle.x = startX + 300; } // Display instruction about the obstacle instructionText.setText("Obstacle added! Find a way around it to hit the pins!"); } } function updateAimLine() { aimLine.rotation = aimAngle; // Ensure the aim line points from the penguin position aimLine.x = launchArea.x; aimLine.y = launchArea.y; } function launchPenguin() { // Calculate power based on the pull distance var powerRatio = Math.min(penguin.pullDistance / 300, 1); // Normalize based on max drag distance var power = powerRatio * MAX_POWER; // Launch penguin from current position toward the launch area var launchAngle = aimAngle + Math.PI; // Reverse the angle to launch toward the pins // Calculate final drag speed, normalize between 0.4 and 1.5 (reduced from 0.5-2.0) var dragSpeedFactor = 0.8; // Default if no drag speed calculated (reduced from 1.0) if (penguin.dragSpeed) { // Cap the drag speed between 0.4 (slower) and 1.5 (faster) dragSpeedFactor = Math.max(0.4, Math.min(1.5, penguin.dragSpeed / 6)); } // Launch with power and drag speed penguin.launch(launchAngle, power, dragSpeedFactor); gameState = "sliding"; powerMeter.visible = false; // Update instruction text to include drag speed information var speedMsg = dragSpeedFactor > 1.0 ? " Fast release!" : dragSpeedFactor < 0.8 ? " Slow release!" : ""; var difficultyMsg = pinSpacingMultiplier > 1.0 ? " - Difficulty: " + Math.round((pinSpacingMultiplier - 1) * 100) + "%" : ""; if (powerRatio >= 0.9) { instructionText.setText("Full power!" + speedMsg + " Watch the penguin slide!" + difficultyMsg); } else { instructionText.setText("Watch the penguin slide! (Power: " + Math.round(powerRatio * 100) + "%)" + speedMsg + difficultyMsg); } } function checkCollisions() { if (!penguin.active || penguin.hasCollided) return; var newKnockdowns = 0; // Calculate penguin velocity magnitude for physics-based knockdown var penguinSpeed = Math.sqrt(penguin.velocityX * penguin.velocityX + penguin.velocityY * penguin.velocityY); var penguinDirection = Math.atan2(penguin.velocityY, penguin.velocityX); // Check penguin collision with ANY pin based on realistic physics for (var i = 0; i < pins.length; i++) { var pin = pins[i]; if (!pin.isKnockedDown && penguin.intersects(pin)) { // Calculate direction of impact from penguin to pin var impactAngle = Math.atan2(pin.y - penguin.y, pin.x - penguin.x); // Calculate angle difference to determine if hitting from front, side, etc. var angleDiff = Math.abs((impactAngle - penguinDirection + Math.PI) % (2 * Math.PI) - Math.PI); // More realistic physics: energy transfer based on angle of impact and penguin speed var energyTransfer = penguinSpeed * (1 - angleDiff / Math.PI); // Knock down pin with physics impact if (pin.applyImpact(penguin.x, penguin.y, energyTransfer)) { newKnockdowns++; pinsKnockedDown++; // Update score totalScore += 1; updateScoreDisplay(); } } } // Now check for pin-to-pin collisions between ALL pins with realistic physics for (var i = 0; i < pins.length; i++) { var pin1 = pins[i]; if (pin1.isKnockedDown && pin1.collisionEnergy > 1) { // This pin is moving with enough energy to cause collisions for (var j = 0; j < pins.length; j++) { var pin2 = pins[j]; if (i !== j && pin1.intersects(pin2)) { // Calculate impact dynamics var impactDirection = Math.atan2(pin2.y - pin1.y, pin2.x - pin1.x); var impactSpeed = Math.sqrt(pin1.velocityX * pin1.velocityX + pin1.velocityY * pin1.velocityY); // If pin2 is not knocked down, apply impact if (!pin2.isKnockedDown) { if (pin2.applyImpact(pin1.x, pin1.y, pin1.collisionEnergy * 0.7)) { pinsKnockedDown++; totalScore += 1; updateScoreDisplay(); } } // Both pins are moving - apply realistic collision physics else { // Calculate new velocities based on conservation of momentum var velocity1 = Math.sqrt(pin1.velocityX * pin1.velocityX + pin1.velocityY * pin1.velocityY); var velocity2 = Math.sqrt(pin2.velocityX * pin2.velocityX + pin2.velocityY * pin2.velocityY); if (velocity1 > 0.5 || velocity2 > 0.5) { // Simple elastic collision - exchange some momentum var tempVX = pin1.velocityX * 0.5; var tempVY = pin1.velocityY * 0.5; pin1.velocityX = pin1.velocityX * 0.5 + pin2.velocityX * 0.5; pin1.velocityY = pin1.velocityY * 0.5 + pin2.velocityY * 0.5; pin2.velocityX = tempVX * 0.5 + pin2.velocityX * 0.5; pin2.velocityY = tempVY * 0.5 + pin2.velocityY * 0.5; // Add some random scatter for realism pin1.velocityX += (Math.random() - 0.5) * 0.5; pin1.velocityY += (Math.random() - 0.5) * 0.5; pin2.velocityX += (Math.random() - 0.5) * 0.5; pin2.velocityY += (Math.random() - 0.5) * 0.5; // Update rotation velocities pin1.rotationVelocity += (Math.random() - 0.5) * 0.1; pin2.rotationVelocity += (Math.random() - 0.5) * 0.1; } } } } } } // Handle collision with obstacle if present - make it completely impassable like a solid wall if (typeof obstacle !== 'undefined' && obstacle && penguin.intersects(obstacle)) { // Store previous position before intersection occurred var prevX = penguin.lastX || penguin.x; var prevY = penguin.lastY || penguin.y; // Determine collision direction by comparing previous position to obstacle var hitFromLeft = prevX < obstacle.x - obstacle.width / 4; var hitFromRight = prevX > obstacle.x + obstacle.width / 4; var hitFromTop = prevY < obstacle.y - obstacle.height / 4; var hitFromBottom = prevY > obstacle.y + obstacle.height / 4; // Calculate bounce velocity with energy loss var bounceMultiplier = 0.7; // 30% energy loss on bounce // Horizontal collision (from left or right) if (hitFromLeft || hitFromRight) { // Position correction - move outside obstacle penguin.x = hitFromLeft ? obstacle.x - obstacle.width / 2 - penguin.width / 2 - 5 : // 5px extra buffer obstacle.x + obstacle.width / 2 + penguin.width / 2 + 5; // Reverse X velocity with energy loss penguin.velocityX = -penguin.velocityX * bounceMultiplier; } // Vertical collision (from top or bottom) if (hitFromTop || hitFromBottom) { // Position correction - move outside obstacle penguin.y = hitFromTop ? obstacle.y - obstacle.height / 2 - penguin.height / 2 - 5 : // 5px extra buffer obstacle.y + obstacle.height / 2 + penguin.height / 2 + 5; // Reverse Y velocity with energy loss penguin.velocityY = -penguin.velocityY * bounceMultiplier; } // Ensure minimum bounce velocity var minBounceVelocity = 2.0; if (Math.abs(penguin.velocityX) < minBounceVelocity && (hitFromLeft || hitFromRight)) { penguin.velocityX = hitFromLeft ? -minBounceVelocity : minBounceVelocity; } if (Math.abs(penguin.velocityY) < minBounceVelocity && (hitFromTop || hitFromBottom)) { penguin.velocityY = hitFromTop ? -minBounceVelocity : minBounceVelocity; } // Play sound effect for the bounce LK.getSound('pinHit').play(); // Track obstacle collisions for burn mechanic penguin.burnCount++; // When penguin hits the obstacle, immediately set it to burned penguin.setBurned(); // Play burn sound effect LK.getSound('burn').play(); instructionText.setText("Penguin got burned after hitting the obstacle!"); // Game over after a short delay LK.setTimeout(function () { LK.showGameOver(); }, 2000); // Flash screen red to indicate burn LK.effects.flashScreen(0xff0000, 1000); } if (newKnockdowns > 0) { penguin.hasCollided = true; // Play strike sound if all pins are knocked down if (pinsKnockedDown === PINS_PER_FRAME) { LK.getSound('strike').play(); LK.effects.flashScreen(0xFFFFFF, 500); instructionText.setText("STRIKE!"); } } } function updateScoreDisplay() { scoreText.setText("Score: " + totalScore); // Update high score if needed if (totalScore > storage.highScore) { storage.highScore = totalScore; } } function nextFrame() { // Check if player got a strike var isStrike = pinsKnockedDown === PINS_PER_FRAME; // Store if we had a strike in the last frame to use in setupPins strikeInLastFrame = isStrike; // Update consecutive fails counter if (isStrike) { consecutiveFailedStrikes = 0; instructionText.setText("STRIKE! Next frame will have an obstacle!"); // Make penguin slower after each strike MAX_POWER = Math.max(5, MAX_POWER - 1); // Decrease power but ensure minimum of 5 // If there's an existing obstacle, make it wider for the next frame if (typeof obstacle !== 'undefined' && obstacle) { obstacle.width += 100; // Increase obstacle width by 100px } } else { consecutiveFailedStrikes++; // Check for game over condition (2 consecutive failed strikes) if (consecutiveFailedStrikes >= 2) { gameState = "gameover"; instructionText.setText("Game Over! 2 consecutive misses! Final Score: " + totalScore); // Show game over screen after a brief delay LK.setTimeout(function () { LK.showGameOver(); }, 2000); return; } } currentFrame++; if (currentFrame > FRAMES) { // Game over gameState = "gameover"; instructionText.setText("Game Over! Final Score: " + totalScore); // Show game over screen after a brief delay LK.setTimeout(function () { LK.showGameOver(); }, 2000); } else { // Setup for next frame frameText.setText("Frame: " + currentFrame + "/10"); // Reset pins regardless of strike setupPins(); penguin.reset(); gameState = "aiming"; // Update instruction based on presence of obstacle if (typeof obstacle !== 'undefined' && obstacle) { instructionText.setText("Tap and drag backward to aim, find a path around the obstacle!"); } else { instructionText.setText("Tap and drag backward to aim, release to launch"); } } } function checkGameStateTransition() { if (gameState === "sliding") { // Check if penguin has stopped if (!penguin.active) { // Add wall bounce bonus if any if (penguin.wallBounceCount && penguin.wallBounceCount > 0) { var bounceBonus = penguin.wallBounceCount * 2; totalScore += bounceBonus; instructionText.setText("Nice trick shot! +" + bounceBonus + " bonus points!"); updateScoreDisplay(); } // Wait a bit for pins to settle and then move to next frame LK.setTimeout(function () { nextFrame(); }, 2000); gameState = "scoring"; } } } // Event handlers game.down = function (x, y, obj) { if (gameState === "aiming") { aiming = true; aimLine.visible = false; // Store original penguin position penguin.originalX = penguin.x; penguin.originalY = penguin.y; } }; game.move = function (x, y, obj) { if (aiming) { // Store the current time and position for calculating drag speed var currentTime = Date.now(); if (!penguin.lastMoveTime) { penguin.lastMoveTime = currentTime; penguin.lastMoveX = x; penguin.lastMoveY = y; } // Calculate aim angle from penguin to opposite of pull direction var dx = launchArea.x - x; var dy = launchArea.y - y; // Calculate the distance of the pull for power var distance = Math.sqrt(dx * dx + dy * dy); // Limit the maximum drag distance var maxDrag = 300; distance = Math.min(distance, maxDrag); // Calculate the angle aimAngle = Math.atan2(dy, dx); // Move the penguin to the drag position penguin.x = launchArea.x - Math.cos(aimAngle) * distance; penguin.y = launchArea.y - Math.sin(aimAngle) * distance; // Store the pull distance for calculating power later penguin.pullDistance = distance; // Calculate drag speed based on pointer movement var timeDiff = currentTime - penguin.lastMoveTime; if (timeDiff > 0) { var moveDistX = x - penguin.lastMoveX; var moveDistY = y - penguin.lastMoveY; var moveDist = Math.sqrt(moveDistX * moveDistX + moveDistY * moveDistY); penguin.dragSpeed = moveDist / timeDiff * 10; // Scale factor to make it reasonable // Update last values for next calculation penguin.lastMoveTime = currentTime; penguin.lastMoveX = x; penguin.lastMoveY = y; } } }; game.up = function (x, y, obj) { if (aiming) { aiming = false; // Calculate final drag speed on release var currentTime = Date.now(); if (penguin.lastMoveTime && currentTime - penguin.lastMoveTime < 200) { // If the player released quickly after the last move, use that as speed indicator var timeDiff = currentTime - penguin.lastMoveTime; if (timeDiff > 0) { var moveDistX = x - penguin.lastMoveX; var moveDistY = y - penguin.lastMoveY; var moveDist = Math.sqrt(moveDistX * moveDistX + moveDistY * moveDistY); // Final release speed calculation var releaseSpeed = moveDist / timeDiff * 15; // Scale factor for release speed // Combine with ongoing drag speed for a weighted average penguin.dragSpeed = (penguin.dragSpeed + releaseSpeed) / 2; } } // Only launch if penguin was actually pulled back if (penguin.pullDistance > 10) { // Launch immediately after release launchPenguin(); } else { // Reset penguin position if not pulled back enough penguin.x = penguin.originalX; penguin.y = penguin.originalY; penguin.dragSpeed = 0; // Reset drag speed if not launching } } }; // Game update loop game.update = function () { if (gameState === "sliding") { penguin.update(); // Update pin physics for (var i = 0; i < pins.length; i++) { pins[i].update(); } checkCollisions(); // Check for pin-to-pin collisions even after penguin has finished moving if (penguin.hasCollided) { for (var i = 0; i < pins.length; i++) { var pin1 = pins[i]; if (pin1.isKnockedDown && pin1.visible && (Math.abs(pin1.velocityX) > 0.5 || Math.abs(pin1.velocityY) > 0.5)) { for (var j = 0; j < pins.length; j++) { var pin2 = pins[j]; if (i !== j && !pin2.isKnockedDown && pin2.visible && pin1.intersects(pin2)) { if (pin2.knockDown()) { pinsKnockedDown++; totalScore++; updateScoreDisplay(); } } } } } } checkGameStateTransition(); } }; // Initialize variables and start the game consecutiveFailedStrikes = 0; pinSpacingMultiplier = 1.0; // Show initial instructions instructionText.setText("Tap and drag backward to aim, release to launch. 2 misses = Game Over!"); // Start background music LK.playMusic('gameMusic'); ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var AimArrow = Container.expand(function () {
var self = Container.call(this);
// Create arrow components
var arrowLine = self.attachAsset('aimLine', {
anchorX: 0.5,
anchorY: 1.0,
width: 10,
height: 150
});
// Set initial visibility
self.visible = false;
// Update arrow position and rotation based on angle
self.updateArrow = function (startX, startY, angle) {
self.x = startX;
self.y = startY;
self.rotation = angle;
self.visible = true;
};
self.hide = function () {
self.visible = false;
};
return self;
});
var BowlingPin = Container.expand(function () {
var self = Container.call(this);
var pinGraphics = self.attachAsset('bowlingPin', {
anchorX: 0.5,
anchorY: 0.5
});
self.isKnockedDown = false;
self.velocityX = 0;
self.velocityY = 0;
self.friction = 0.95;
self.lastX = 0;
self.lastY = 0;
self.row = 0; // Store which row this pin belongs to
self.mass = 1 + Math.random() * 0.2; // Slight mass variation for more realistic physics
self.collisionEnergy = 0; // Track collision energy for better pin-to-pin interactions
self.rotationVelocity = 0; // Track rotation velocity separately
self.knockDown = function () {
if (!self.isKnockedDown) {
self.isKnockedDown = true;
// Calculate direction based on the impact
var directionX = Math.random() - 0.5;
var directionY = Math.random() * 0.3 + 0.7; // Mostly forward, slight randomness
// More realistic physics - calculate velocity based on position, mass, etc.
self.velocityX = directionX * (5 + Math.random() * 3);
self.velocityY = directionY * (5 + Math.random() * 3);
self.rotationVelocity = (Math.random() - 0.5) * 0.2;
// Add collision energy for pin-to-pin interactions
self.collisionEnergy = 10;
LK.getSound('pinHit').play();
return true;
}
return false;
};
self.applyImpact = function (impactX, impactY, energy) {
// Apply an impact force from another object (pin or penguin)
// Direction based on impact position
var dirX = self.x - impactX;
var dirY = self.y - impactY;
// Normalize
var length = Math.sqrt(dirX * dirX + dirY * dirY);
if (length > 0) {
dirX /= length;
dirY /= length;
}
// Apply velocity based on energy and direction
var forceScale = energy / self.mass;
self.velocityX += dirX * forceScale;
self.velocityY += dirY * forceScale;
// Add some rotation based on impact offset
var rotationImpact = (Math.random() - 0.5) * 0.1 * forceScale;
self.rotationVelocity += rotationImpact;
// Mark as knocked down
if (!self.isKnockedDown) {
self.isKnockedDown = true;
self.collisionEnergy = energy * 0.8; // Transfer most of the energy
LK.getSound('pinHit').play();
return true;
}
return false;
};
self.update = function () {
// Store last position for collision detection
self.lastX = self.x;
self.lastY = self.y;
if (self.isKnockedDown) {
// Apply velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Apply friction
self.velocityX *= self.friction;
self.velocityY *= self.friction;
// Apply rotation
pinGraphics.rotation += self.rotationVelocity;
self.rotationVelocity *= 0.98; // Dampen rotation
// Gradually reduce collision energy
self.collisionEnergy *= 0.95;
// More realistic falling behavior with physics-based motion
// Pins slow down more when they're almost stopped
if (Math.abs(self.velocityX) < 0.5 && Math.abs(self.velocityY) < 0.5) {
self.velocityX *= 0.9;
self.velocityY *= 0.9;
}
// Slower fade out for more realistic pin falling
if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) {
pinGraphics.alpha *= 0.99; // Slower fade when stopped
} else {
pinGraphics.alpha *= 0.995; // Normal fade when moving
}
// Remove from game when almost invisible
if (pinGraphics.alpha < 0.1) {
self.visible = false;
}
}
};
self.reset = function () {
self.isKnockedDown = false;
self.velocityX = 0;
self.velocityY = 0;
self.rotationVelocity = 0;
self.collisionEnergy = 0;
pinGraphics.rotation = 0;
pinGraphics.alpha = 1;
self.visible = true;
};
return self;
});
var Penguin = Container.expand(function () {
var self = Container.call(this);
var penguinGraphics = self.attachAsset('penguin', {
anchorX: 0.5,
anchorY: 0.5
});
// Physics properties
self.velocityX = 0;
self.velocityY = 0;
self.friction = 0.985; // Further decreased value for even slower sliding
self.active = false;
self.hasCollided = false;
self.burnCount = 0; // Track how many times penguin has hit the obstacle
self.isBurned = false; // Track if penguin is burned
self.pullDistance = 0; // Store the pull distance for power calculation
self.launch = function (angle, power, dragSpeed) {
// Use the drag speed to determine penguin velocity
// dragSpeed is calculated based on how quickly the player dragged and released
var speedMultiplier = power / MAX_POWER; // Base power from pull distance
// Use dragSpeed as a multiplier to the base power calculation
var baseSpeed = 18; // Minimum speed needed to move (reduced from 25)
var reachPinsSpeed = 30; // Speed needed to reach pins (reduced from 40)
var maxSpeed = baseSpeed + (reachPinsSpeed - baseSpeed) * speedMultiplier;
// Multiply by dragSpeed factor for more dynamic speed control
maxSpeed *= dragSpeed || 0.8;
self.velocityX = Math.cos(angle) * maxSpeed;
self.velocityY = Math.sin(angle) * maxSpeed;
// Special case for throwing straight - make sure it reaches pins at max power
if (Math.abs(Math.cos(angle)) < 0.2 && Math.sin(angle) < -0.8 && speedMultiplier > 0.9) {
// Adjust vertical speed to ensure it reaches the pins
self.velocityY = -reachPinsSpeed * (dragSpeed || 0.8);
}
self.active = true;
self.hasCollided = false;
LK.getSound('slide').play();
};
self.update = function () {
if (!self.active) return;
// Apply velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Apply friction
// Ice physics - lower friction when moving fast, gradually increases as slowing down
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
var dynamicFriction = speed > 10 ? 0.988 : speed > 5 ? 0.982 : 0.975; // More graduated and further reduced friction for slower ice physics
self.velocityX *= dynamicFriction;
self.velocityY *= dynamicFriction;
// More realistic penguin rotation while sliding
var rotationFactor = speed > 5 ? 0.005 : 0.01; // Less rotation at high speeds
penguinGraphics.rotation += self.velocityX * rotationFactor;
// Stop if velocity is very small
if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) {
self.active = false;
self.velocityX = 0;
self.velocityY = 0;
penguinGraphics.rotation = 0; // Reset rotation when stopped
}
// Frame boundary checks with proper reflection vectors
// Left boundary
if (self.x < 0) {
self.x = 0;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed
// If X velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityX) < 0.5) {
self.velocityX = 0; // Perfect 90 degrees means no horizontal movement
self.velocityY = self.velocityY > 0 ? speed : -speed; // Maintain vertical direction
}
// Award wall bounce bonus if active and hasn't collided yet
if (self.active && !self.hasCollided) {
self.wallBounceCount = self.wallBounceCount || 0;
self.wallBounceCount++;
}
}
// Right boundary
else if (self.x > 2048) {
self.x = 2048;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed
// If X velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityX) < 0.5) {
self.velocityX = 0; // Perfect 90 degrees means no horizontal movement
self.velocityY = self.velocityY > 0 ? speed : -speed; // Maintain vertical direction
}
}
// Top boundary
if (self.y < 0) {
self.y = 0;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed
// If Y velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityY) < 0.5) {
self.velocityY = 0; // Perfect 90 degrees means no vertical movement
self.velocityX = self.velocityX > 0 ? speed : -speed; // Maintain horizontal direction
}
}
// Bottom boundary
if (self.y > 2732) {
self.y = 2732;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed
// If Y velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityY) < 0.5) {
self.velocityY = 0; // Perfect 90 degrees means no vertical movement
self.velocityX = self.velocityX > 0 ? speed : -speed; // Maintain horizontal direction
}
}
// Bowling lane boundary checks (light blue area)
// Calculate bowling lane boundaries
var laneLeftBoundary = bowlingLane.x - bowlingLane.width / 2;
var laneRightBoundary = bowlingLane.x + bowlingLane.width / 2;
var laneTopBoundary = bowlingLane.y;
var laneBottomBoundary = bowlingLane.y + bowlingLane.height;
// Check if penguin is outside the lane but not outside the game boundaries
// Left lane boundary
if (self.x < laneLeftBoundary && self.x > 0) {
self.x = laneLeftBoundary;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed
// If X velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityX) < 0.5) {
self.velocityX = 0;
self.velocityY = self.velocityY > 0 ? speed : -speed;
}
}
// Right lane boundary
else if (self.x > laneRightBoundary && self.x < 2048) {
self.x = laneRightBoundary;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityX = -self.velocityX * 0.8; // Reverse X direction with reduced speed
// If X velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityX) < 0.5) {
self.velocityX = 0;
self.velocityY = self.velocityY > 0 ? speed : -speed;
}
}
// Top lane boundary
if (self.y < laneTopBoundary && self.y > 0) {
self.y = laneTopBoundary;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed
// If Y velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityY) < 0.5) {
self.velocityY = 0;
self.velocityX = self.velocityX > 0 ? speed : -speed;
}
}
// Bottom lane boundary
if (self.y > laneBottomBoundary && self.y < 2732) {
self.y = laneBottomBoundary;
// Calculate reflection vector for a 90-degree bounce with reduced speed
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY) * 0.8;
self.velocityY = -self.velocityY * 0.8; // Reverse Y direction with reduced speed
// If Y velocity is very small, set to zero for perfect 90-degree bounce
if (Math.abs(self.velocityY) < 0.5) {
self.velocityY = 0;
self.velocityX = self.velocityX > 0 ? speed : -speed;
}
}
};
self.reset = function () {
self.x = launchArea.x;
self.y = launchArea.y - 250; // Move penguin further up the lane
self.velocityX = 0;
self.velocityY = 0;
self.active = false;
self.hasCollided = false;
self.pullDistance = 0; // Reset pull distance
self.wallBounceCount = 0; // Reset wall bounce counter
self.dragSpeed = 0; // Reset drag speed
self.lastMoveTime = 0; // Reset time tracking
self.lastMoveX = 0; // Reset position tracking
self.lastMoveY = 0; // Reset position tracking
penguinGraphics.rotation = 0;
self.resetBurnState();
};
// Add function to reset burn state
self.resetBurnState = function () {
self.burnCount = 0;
self.isBurned = false;
// Restore original penguin image
penguinGraphics.texture = LK.getAsset('penguin', {}).texture;
};
// Add function to set penguin burned state
self.setBurned = function () {
self.isBurned = true;
// Remove the old penguin graphics
self.removeChild(penguinGraphics);
// Create and add the burned penguin graphics with same anchor points
penguinGraphics = self.attachAsset('burntPenguin', {
anchorX: 0.5,
anchorY: 0.5
});
// Flash red to show penguin is burned
LK.effects.flashObject(self, 0xff0000, 1000);
// Stop penguin movement
self.velocityX = 0;
self.velocityY = 0;
self.active = false;
};
return self;
});
var PowerMeter = Container.expand(function () {
var self = Container.call(this);
var meterBG = self.attachAsset('powerMeterBG', {
anchorX: 0.5,
anchorY: 0.5
});
var meter = self.attachAsset('powerMeter', {
anchorX: 0.5,
anchorY: 1.0,
height: 0 // Start with no power
});
var maxPower = 300;
self.power = 0;
self.increasing = true;
self.update = function () {
if (self.visible) {
if (self.increasing) {
self.power += 5;
if (self.power >= maxPower) {
self.power = maxPower;
self.increasing = false;
}
} else {
self.power -= 5;
if (self.power <= 0) {
self.power = 0;
self.increasing = true;
}
}
// Update meter height based on power
meter.height = self.power;
meter.y = meterBG.y + meterBG.height / 2 - meter.height / 2;
}
};
self.getPowerRatio = function () {
return self.power / maxPower;
};
self.reset = function () {
self.power = 0;
self.increasing = true;
meter.height = 0;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xC7E6F7
});
/****
* Game Code
****/
// Game constants
var MAX_POWER = 15; // Reduced from 20 to make penguin slower
var PIN_ROWS = 4;
var FRAMES = 10;
var PINS_PER_FRAME = 10;
// Game state variables
var currentFrame = 1;
var pinsKnockedDown = 0;
var totalScore = 0;
var aiming = false;
var powering = false;
var gameState = "aiming"; // States: aiming, powering, sliding, scoring, gameover
var aimAngle = -Math.PI / 2; // Start aiming straight up
var consecutiveFailedStrikes = 0; // Track consecutive failures to get a strike
var strikeInLastFrame = false; // Track if the player got a strike in the last frame
var obstacle = null; // Reference to obstacle object
// Create background
var background = game.addChild(LK.getAsset('background', {
anchorX: 0,
anchorY: 0
}));
// Create bowling lane
var bowlingLane = game.addChild(LK.getAsset('bowlingLane', {
anchorX: 0.5,
anchorY: 0,
x: 2048 / 2,
y: 100
}));
// Create launch area
var launchArea = game.addChild(LK.getAsset('launchArea', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2400
}));
// Create the aiming line
var aimLine = game.addChild(LK.getAsset('aimLine', {
anchorX: 0.5,
anchorY: 1.0,
x: launchArea.x,
y: launchArea.y,
visible: false
}));
// Arrow removed as requested
// Create the power meter
var powerMeter = game.addChild(new PowerMeter());
powerMeter.x = 1900;
powerMeter.y = 1400;
powerMeter.visible = false;
// Create the penguin
var penguin = game.addChild(new Penguin());
penguin.x = launchArea.x;
penguin.y = launchArea.y - 250; // Move penguin further up the lane
// Create bowling pins
var pins = [];
setupPins();
// Create UI elements
var frameText = new Text2('Frame: 1/10', {
size: 70,
fill: 0x000000
});
frameText.anchor.set(0.5, 0);
// Position frame text under the penguin's position
frameText.y = penguin.y + 150;
LK.gui.center.addChild(frameText);
var scoreText = new Text2('Score: 0', {
size: 70,
fill: 0x000000
});
scoreText.anchor.set(0.5, 0);
scoreText.y = 80;
LK.gui.top.addChild(scoreText);
var instructionText = new Text2('Tap and drag to pull penguin back, release to launch', {
size: 50,
fill: 0x000000
});
instructionText.anchor.set(0.5, 0);
instructionText.y = 180;
LK.gui.top.addChild(instructionText);
// Helper functions
function setupPins() {
// Clear any existing pins
for (var i = 0; i < pins.length; i++) {
pins[i].destroy();
}
pins = [];
// Create pin layout in traditional bowling triangle formation
// Ensure pins are exactly centered on the lane
var startX = 2048 / 2; // Center of screen
var startY = 400; // Position from top
var pinSpacingX = 120; // Fixed horizontal spacing regardless of strikes
var pinSpacingY = 120; // Fixed vertical spacing regardless of strikes
// Standard bowling pin layout (4-3-2-1 triangle)
// First row (back) - 4 pins
for (var i = 0; i < 4; i++) {
var pin = new BowlingPin();
pin.x = startX + (i - 1.5) * pinSpacingX; // Perfectly centered
pin.y = startY;
pin.row = 1; // Back row
pin.lastX = pin.x;
pin.lastY = pin.y;
pins.push(pin);
game.addChild(pin);
}
// Second row - 3 pins
for (var i = 0; i < 3; i++) {
var pin = new BowlingPin();
pin.x = startX + (i - 1) * pinSpacingX; // Centered relative to first row
pin.y = startY + pinSpacingY;
pin.row = 2; // Second row
pin.lastX = pin.x;
pin.lastY = pin.y;
pins.push(pin);
game.addChild(pin);
}
// Third row - 2 pins
for (var i = 0; i < 2; i++) {
var pin = new BowlingPin();
pin.x = startX + (i - 0.5) * pinSpacingX; // Centered relative to second row
pin.y = startY + 2 * pinSpacingY;
pin.row = 3; // Third row
pin.lastX = pin.x;
pin.lastY = pin.y;
pins.push(pin);
game.addChild(pin);
}
// Fourth row (front) - 1 pin
var pin = new BowlingPin();
pin.x = startX; // Center pin
pin.y = startY + 3 * pinSpacingY;
pin.row = 4; // Front row
pin.lastX = pin.x;
pin.lastY = pin.y;
pins.push(pin);
game.addChild(pin);
pinsKnockedDown = 0;
// Reset penguin burn state for new frame
penguin.resetBurnState();
// Remove any existing obstacle
if (typeof obstacle !== 'undefined' && obstacle) {
obstacle.destroy();
obstacle = null;
}
// Add obstacle after strike
if (currentFrame > 1 && strikeInLastFrame) {
// Create obstacle based on a random position (left or right)
var obstaclePosition = Math.random() < 0.5 ? "left" : "right";
obstacle = game.addChild(LK.getAsset('horizontalline', {
anchorX: 0.5,
anchorY: 0.5,
width: 500,
height: 50,
x: startX,
y: 1200
}));
// If obstacle is on the left side, move it to the left half of the lane
if (obstaclePosition === "left") {
obstacle.x = startX - 300;
} else {
obstacle.x = startX + 300;
}
// Display instruction about the obstacle
instructionText.setText("Obstacle added! Find a way around it to hit the pins!");
}
}
function updateAimLine() {
aimLine.rotation = aimAngle;
// Ensure the aim line points from the penguin position
aimLine.x = launchArea.x;
aimLine.y = launchArea.y;
}
function launchPenguin() {
// Calculate power based on the pull distance
var powerRatio = Math.min(penguin.pullDistance / 300, 1); // Normalize based on max drag distance
var power = powerRatio * MAX_POWER;
// Launch penguin from current position toward the launch area
var launchAngle = aimAngle + Math.PI; // Reverse the angle to launch toward the pins
// Calculate final drag speed, normalize between 0.4 and 1.5 (reduced from 0.5-2.0)
var dragSpeedFactor = 0.8; // Default if no drag speed calculated (reduced from 1.0)
if (penguin.dragSpeed) {
// Cap the drag speed between 0.4 (slower) and 1.5 (faster)
dragSpeedFactor = Math.max(0.4, Math.min(1.5, penguin.dragSpeed / 6));
}
// Launch with power and drag speed
penguin.launch(launchAngle, power, dragSpeedFactor);
gameState = "sliding";
powerMeter.visible = false;
// Update instruction text to include drag speed information
var speedMsg = dragSpeedFactor > 1.0 ? " Fast release!" : dragSpeedFactor < 0.8 ? " Slow release!" : "";
var difficultyMsg = pinSpacingMultiplier > 1.0 ? " - Difficulty: " + Math.round((pinSpacingMultiplier - 1) * 100) + "%" : "";
if (powerRatio >= 0.9) {
instructionText.setText("Full power!" + speedMsg + " Watch the penguin slide!" + difficultyMsg);
} else {
instructionText.setText("Watch the penguin slide! (Power: " + Math.round(powerRatio * 100) + "%)" + speedMsg + difficultyMsg);
}
}
function checkCollisions() {
if (!penguin.active || penguin.hasCollided) return;
var newKnockdowns = 0;
// Calculate penguin velocity magnitude for physics-based knockdown
var penguinSpeed = Math.sqrt(penguin.velocityX * penguin.velocityX + penguin.velocityY * penguin.velocityY);
var penguinDirection = Math.atan2(penguin.velocityY, penguin.velocityX);
// Check penguin collision with ANY pin based on realistic physics
for (var i = 0; i < pins.length; i++) {
var pin = pins[i];
if (!pin.isKnockedDown && penguin.intersects(pin)) {
// Calculate direction of impact from penguin to pin
var impactAngle = Math.atan2(pin.y - penguin.y, pin.x - penguin.x);
// Calculate angle difference to determine if hitting from front, side, etc.
var angleDiff = Math.abs((impactAngle - penguinDirection + Math.PI) % (2 * Math.PI) - Math.PI);
// More realistic physics: energy transfer based on angle of impact and penguin speed
var energyTransfer = penguinSpeed * (1 - angleDiff / Math.PI);
// Knock down pin with physics impact
if (pin.applyImpact(penguin.x, penguin.y, energyTransfer)) {
newKnockdowns++;
pinsKnockedDown++;
// Update score
totalScore += 1;
updateScoreDisplay();
}
}
}
// Now check for pin-to-pin collisions between ALL pins with realistic physics
for (var i = 0; i < pins.length; i++) {
var pin1 = pins[i];
if (pin1.isKnockedDown && pin1.collisionEnergy > 1) {
// This pin is moving with enough energy to cause collisions
for (var j = 0; j < pins.length; j++) {
var pin2 = pins[j];
if (i !== j && pin1.intersects(pin2)) {
// Calculate impact dynamics
var impactDirection = Math.atan2(pin2.y - pin1.y, pin2.x - pin1.x);
var impactSpeed = Math.sqrt(pin1.velocityX * pin1.velocityX + pin1.velocityY * pin1.velocityY);
// If pin2 is not knocked down, apply impact
if (!pin2.isKnockedDown) {
if (pin2.applyImpact(pin1.x, pin1.y, pin1.collisionEnergy * 0.7)) {
pinsKnockedDown++;
totalScore += 1;
updateScoreDisplay();
}
}
// Both pins are moving - apply realistic collision physics
else {
// Calculate new velocities based on conservation of momentum
var velocity1 = Math.sqrt(pin1.velocityX * pin1.velocityX + pin1.velocityY * pin1.velocityY);
var velocity2 = Math.sqrt(pin2.velocityX * pin2.velocityX + pin2.velocityY * pin2.velocityY);
if (velocity1 > 0.5 || velocity2 > 0.5) {
// Simple elastic collision - exchange some momentum
var tempVX = pin1.velocityX * 0.5;
var tempVY = pin1.velocityY * 0.5;
pin1.velocityX = pin1.velocityX * 0.5 + pin2.velocityX * 0.5;
pin1.velocityY = pin1.velocityY * 0.5 + pin2.velocityY * 0.5;
pin2.velocityX = tempVX * 0.5 + pin2.velocityX * 0.5;
pin2.velocityY = tempVY * 0.5 + pin2.velocityY * 0.5;
// Add some random scatter for realism
pin1.velocityX += (Math.random() - 0.5) * 0.5;
pin1.velocityY += (Math.random() - 0.5) * 0.5;
pin2.velocityX += (Math.random() - 0.5) * 0.5;
pin2.velocityY += (Math.random() - 0.5) * 0.5;
// Update rotation velocities
pin1.rotationVelocity += (Math.random() - 0.5) * 0.1;
pin2.rotationVelocity += (Math.random() - 0.5) * 0.1;
}
}
}
}
}
}
// Handle collision with obstacle if present - make it completely impassable like a solid wall
if (typeof obstacle !== 'undefined' && obstacle && penguin.intersects(obstacle)) {
// Store previous position before intersection occurred
var prevX = penguin.lastX || penguin.x;
var prevY = penguin.lastY || penguin.y;
// Determine collision direction by comparing previous position to obstacle
var hitFromLeft = prevX < obstacle.x - obstacle.width / 4;
var hitFromRight = prevX > obstacle.x + obstacle.width / 4;
var hitFromTop = prevY < obstacle.y - obstacle.height / 4;
var hitFromBottom = prevY > obstacle.y + obstacle.height / 4;
// Calculate bounce velocity with energy loss
var bounceMultiplier = 0.7; // 30% energy loss on bounce
// Horizontal collision (from left or right)
if (hitFromLeft || hitFromRight) {
// Position correction - move outside obstacle
penguin.x = hitFromLeft ? obstacle.x - obstacle.width / 2 - penguin.width / 2 - 5 :
// 5px extra buffer
obstacle.x + obstacle.width / 2 + penguin.width / 2 + 5;
// Reverse X velocity with energy loss
penguin.velocityX = -penguin.velocityX * bounceMultiplier;
}
// Vertical collision (from top or bottom)
if (hitFromTop || hitFromBottom) {
// Position correction - move outside obstacle
penguin.y = hitFromTop ? obstacle.y - obstacle.height / 2 - penguin.height / 2 - 5 :
// 5px extra buffer
obstacle.y + obstacle.height / 2 + penguin.height / 2 + 5;
// Reverse Y velocity with energy loss
penguin.velocityY = -penguin.velocityY * bounceMultiplier;
}
// Ensure minimum bounce velocity
var minBounceVelocity = 2.0;
if (Math.abs(penguin.velocityX) < minBounceVelocity && (hitFromLeft || hitFromRight)) {
penguin.velocityX = hitFromLeft ? -minBounceVelocity : minBounceVelocity;
}
if (Math.abs(penguin.velocityY) < minBounceVelocity && (hitFromTop || hitFromBottom)) {
penguin.velocityY = hitFromTop ? -minBounceVelocity : minBounceVelocity;
}
// Play sound effect for the bounce
LK.getSound('pinHit').play();
// Track obstacle collisions for burn mechanic
penguin.burnCount++;
// When penguin hits the obstacle, immediately set it to burned
penguin.setBurned();
// Play burn sound effect
LK.getSound('burn').play();
instructionText.setText("Penguin got burned after hitting the obstacle!");
// Game over after a short delay
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
// Flash screen red to indicate burn
LK.effects.flashScreen(0xff0000, 1000);
}
if (newKnockdowns > 0) {
penguin.hasCollided = true;
// Play strike sound if all pins are knocked down
if (pinsKnockedDown === PINS_PER_FRAME) {
LK.getSound('strike').play();
LK.effects.flashScreen(0xFFFFFF, 500);
instructionText.setText("STRIKE!");
}
}
}
function updateScoreDisplay() {
scoreText.setText("Score: " + totalScore);
// Update high score if needed
if (totalScore > storage.highScore) {
storage.highScore = totalScore;
}
}
function nextFrame() {
// Check if player got a strike
var isStrike = pinsKnockedDown === PINS_PER_FRAME;
// Store if we had a strike in the last frame to use in setupPins
strikeInLastFrame = isStrike;
// Update consecutive fails counter
if (isStrike) {
consecutiveFailedStrikes = 0;
instructionText.setText("STRIKE! Next frame will have an obstacle!");
// Make penguin slower after each strike
MAX_POWER = Math.max(5, MAX_POWER - 1); // Decrease power but ensure minimum of 5
// If there's an existing obstacle, make it wider for the next frame
if (typeof obstacle !== 'undefined' && obstacle) {
obstacle.width += 100; // Increase obstacle width by 100px
}
} else {
consecutiveFailedStrikes++;
// Check for game over condition (2 consecutive failed strikes)
if (consecutiveFailedStrikes >= 2) {
gameState = "gameover";
instructionText.setText("Game Over! 2 consecutive misses! Final Score: " + totalScore);
// Show game over screen after a brief delay
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
return;
}
}
currentFrame++;
if (currentFrame > FRAMES) {
// Game over
gameState = "gameover";
instructionText.setText("Game Over! Final Score: " + totalScore);
// Show game over screen after a brief delay
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
} else {
// Setup for next frame
frameText.setText("Frame: " + currentFrame + "/10");
// Reset pins regardless of strike
setupPins();
penguin.reset();
gameState = "aiming";
// Update instruction based on presence of obstacle
if (typeof obstacle !== 'undefined' && obstacle) {
instructionText.setText("Tap and drag backward to aim, find a path around the obstacle!");
} else {
instructionText.setText("Tap and drag backward to aim, release to launch");
}
}
}
function checkGameStateTransition() {
if (gameState === "sliding") {
// Check if penguin has stopped
if (!penguin.active) {
// Add wall bounce bonus if any
if (penguin.wallBounceCount && penguin.wallBounceCount > 0) {
var bounceBonus = penguin.wallBounceCount * 2;
totalScore += bounceBonus;
instructionText.setText("Nice trick shot! +" + bounceBonus + " bonus points!");
updateScoreDisplay();
}
// Wait a bit for pins to settle and then move to next frame
LK.setTimeout(function () {
nextFrame();
}, 2000);
gameState = "scoring";
}
}
}
// Event handlers
game.down = function (x, y, obj) {
if (gameState === "aiming") {
aiming = true;
aimLine.visible = false;
// Store original penguin position
penguin.originalX = penguin.x;
penguin.originalY = penguin.y;
}
};
game.move = function (x, y, obj) {
if (aiming) {
// Store the current time and position for calculating drag speed
var currentTime = Date.now();
if (!penguin.lastMoveTime) {
penguin.lastMoveTime = currentTime;
penguin.lastMoveX = x;
penguin.lastMoveY = y;
}
// Calculate aim angle from penguin to opposite of pull direction
var dx = launchArea.x - x;
var dy = launchArea.y - y;
// Calculate the distance of the pull for power
var distance = Math.sqrt(dx * dx + dy * dy);
// Limit the maximum drag distance
var maxDrag = 300;
distance = Math.min(distance, maxDrag);
// Calculate the angle
aimAngle = Math.atan2(dy, dx);
// Move the penguin to the drag position
penguin.x = launchArea.x - Math.cos(aimAngle) * distance;
penguin.y = launchArea.y - Math.sin(aimAngle) * distance;
// Store the pull distance for calculating power later
penguin.pullDistance = distance;
// Calculate drag speed based on pointer movement
var timeDiff = currentTime - penguin.lastMoveTime;
if (timeDiff > 0) {
var moveDistX = x - penguin.lastMoveX;
var moveDistY = y - penguin.lastMoveY;
var moveDist = Math.sqrt(moveDistX * moveDistX + moveDistY * moveDistY);
penguin.dragSpeed = moveDist / timeDiff * 10; // Scale factor to make it reasonable
// Update last values for next calculation
penguin.lastMoveTime = currentTime;
penguin.lastMoveX = x;
penguin.lastMoveY = y;
}
}
};
game.up = function (x, y, obj) {
if (aiming) {
aiming = false;
// Calculate final drag speed on release
var currentTime = Date.now();
if (penguin.lastMoveTime && currentTime - penguin.lastMoveTime < 200) {
// If the player released quickly after the last move, use that as speed indicator
var timeDiff = currentTime - penguin.lastMoveTime;
if (timeDiff > 0) {
var moveDistX = x - penguin.lastMoveX;
var moveDistY = y - penguin.lastMoveY;
var moveDist = Math.sqrt(moveDistX * moveDistX + moveDistY * moveDistY);
// Final release speed calculation
var releaseSpeed = moveDist / timeDiff * 15; // Scale factor for release speed
// Combine with ongoing drag speed for a weighted average
penguin.dragSpeed = (penguin.dragSpeed + releaseSpeed) / 2;
}
}
// Only launch if penguin was actually pulled back
if (penguin.pullDistance > 10) {
// Launch immediately after release
launchPenguin();
} else {
// Reset penguin position if not pulled back enough
penguin.x = penguin.originalX;
penguin.y = penguin.originalY;
penguin.dragSpeed = 0; // Reset drag speed if not launching
}
}
};
// Game update loop
game.update = function () {
if (gameState === "sliding") {
penguin.update();
// Update pin physics
for (var i = 0; i < pins.length; i++) {
pins[i].update();
}
checkCollisions();
// Check for pin-to-pin collisions even after penguin has finished moving
if (penguin.hasCollided) {
for (var i = 0; i < pins.length; i++) {
var pin1 = pins[i];
if (pin1.isKnockedDown && pin1.visible && (Math.abs(pin1.velocityX) > 0.5 || Math.abs(pin1.velocityY) > 0.5)) {
for (var j = 0; j < pins.length; j++) {
var pin2 = pins[j];
if (i !== j && !pin2.isKnockedDown && pin2.visible && pin1.intersects(pin2)) {
if (pin2.knockDown()) {
pinsKnockedDown++;
totalScore++;
updateScoreDisplay();
}
}
}
}
}
}
checkGameStateTransition();
}
};
// Initialize variables and start the game
consecutiveFailedStrikes = 0;
pinSpacingMultiplier = 1.0;
// Show initial instructions
instructionText.setText("Tap and drag backward to aim, release to launch. 2 misses = Game Over!");
// Start background music
LK.playMusic('gameMusic');
;
A cartoon-style penguin lying flat on its belly, facing forward with its body stretched out. In-Game asset. 2d. High contrast. No shadows
Bowling pin. In-Game asset. 2d. High contrast. No shadows
iglo. In-Game asset. 2d. High contrast. No shadows
Icy surface. In-Game asset. 2d. High contrast. No shadows
Snow gently falling from the sky in a peaceful winter scene. The snowflakes are soft and light, creating a calm atmosphere. The snow is falling in large, delicate flakes, covering the icy surface and creating a serene, magical ambiance.". In-Game asset. 2d. High contrast. No shadows
horizontal fire. In-Game asset. 2d. High contrast. No shadows
pişmiş tavuk. In-Game asset. 2d. High contrast. No shadows