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