User prompt
The round position should be underneath the penguin.
User prompt
Change the penguin's position to a slightly forward location, and make sure it returns to the same spot at the end of the round.
User prompt
The platform where the penguin is placed should be a bit further ahead, so I can easily drag the penguin with the mouse.
User prompt
penguenin olduğu konumu biraz daha ileri al
User prompt
The penguin and the arrow should work inversely — when you click and drag the penguin backwards, the arrow should point in the direction the penguin will travel. The farther you pull it back, the faster it should go. As soon as you release it, the penguin should start sliding toward the pins. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
A directional arrow should appear while dragging the penguin, showing the direction it will move once released.
User prompt
A directional arrow should appear while dragging the penguin, showing the direction it will move once released.
User prompt
The pins are still positioned off to the side. Move them to the center of the screen and make sure they are aligned with the penguin.
User prompt
Align the pins with the penguin.
User prompt
You need to arrange the pins in the center, just like in a real bowling game.
User prompt
labutları üçgen bir şekilde ekranın tam ortasına getir
User prompt
labutlar pengueni saldığımda penguenin çarpması gereken objeler ve tam ortaya hizalaman gerekiyor
User prompt
labutların ortada olması gerekiyor ve tam ortadan labutlara geçen bir yol olması gerekiyor
Code edit (1 edits merged)
Please save this source code
User prompt
Penguin Bowling: Ice Slide Strike
Initial prompt
I want to make a bowling game where, instead of a bowling ball, you launch a penguin. The ground is made of ice, and the penguin slides across it.
/**** * 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