/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Bowling Ball class var BowlingBall = Container.expand(function () { var self = Container.call(this); var shadow = self.attachAsset('shadow', { anchorX: 0.5, anchorY: 0.5, y: 80 }); shadow.alpha = 0.25; var ballAsset = self.attachAsset('bowlingBall', { anchorX: 0.5, anchorY: 0.5 }); self.radius = ballAsset.width / 2; self.vx = 0; self.vy = 0; self.isRolling = false; self.update = function () { if (self.isRolling) { self.x += self.vx; self.y += self.vy; // Use per-ball friction for more realistic deceleration var friction = typeof self.friction === "number" ? self.friction : 0.995; self.vx *= friction; self.vy *= friction; // Ball rotation for effect ballAsset.rotation += 0.15 * (self.vx + self.vy); // Stop if slow if (Math.abs(self.vx) < 1 && Math.abs(self.vy) < 1) { self.isRolling = false; self.vx = 0; self.vy = 0; } } }; self.reset = function (x, y) { self.x = x; self.y = y; self.vx = 0; self.vy = 0; self.isRolling = false; self.friction = 0.995; // Reset to default friction ballAsset.rotation = 0; }; return self; }); // Pin class var Pin = Container.expand(function () { var self = Container.call(this); var pinAsset = self.attachAsset('pin', { anchorX: 0.5, anchorY: 1 }); self.isStanding = true; self.knockDown = function () { if (!self.isStanding) { return; } self.isStanding = false; // Animate pin falling tween(self, { rotation: Math.PI / 2, y: self.y + 60 }, { duration: 400, easing: tween.cubicOut }); // Fade out after a short delay tween(self, { alpha: 0 }, { duration: 300, delay: 400, onFinish: function onFinish() { self.visible = false; } }); }; self.reset = function () { self.isStanding = true; self.rotation = 0; self.alpha = 1; self.visible = true; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // Sound for ball roll // Shadow for ball // Lane // Pin // Bowling Ball // Lane setup // Top-down lane setup var lane = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(lane); // Pin setup (top-down, triangle formation at the top of the lane) var pins = []; var pinRows = 4; // 4 rows: 1,2,3,4 = 10 pins var pinSpacingX = 110; var pinSpacingY = 110; var laneTopY = lane.y - lane.height / 2 + 180; var laneCenterX = lane.x; var pinStartY = laneTopY + 60; var pinStartX = lane.x; var pinLayout = [[0], [-1, 1], [-2, 0, 2], [-3, -1, 1, 3]]; for (var row = 0; row < pinRows; row++) { for (var col = 0; col < pinLayout[row].length; col++) { var pin = new Pin(); pin.x = laneCenterX + pinLayout[row][col] * pinSpacingX; pin.y = pinStartY + row * pinSpacingY; pins.push(pin); game.addChild(pin); } } // Ball setup (starts at the bottom of the lane, centered) var ballStartX = lane.x; var ballStartY = lane.y + lane.height / 2 - 120; var ball = new BowlingBall(); ball.reset(ballStartX, ballStartY); game.addChild(ball); // Game state var throwsPerFrame = 2; var currentThrow = 1; var frame = 1; var maxFrames = 10; var pinsDownThisFrame = 0; var isAiming = false; var aimStart = null; var aimEnd = null; var isBallRolling = false; var gameOver = false; // 2 Player state var playerCount = 2; var currentPlayer = 1; // 1 or 2 var playerScores = [0, 0]; // [player1, player2] // GUI var scoreTxt = new Text2('Player 1: 0 Player 2: 0', { size: 90, fill: "#fff" }); scoreTxt.setText('Player 1: 0 Player 2: 0'); // ensure initial text scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var frameTxt = new Text2('Frame: 1/10', { size: 60, fill: "#fff" }); frameTxt.anchor.set(0.5, 0); LK.gui.top.addChild(frameTxt); frameTxt.y = 110; var throwTxt = new Text2('Throw: 1/2', { size: 60, fill: "#fff" }); throwTxt.anchor.set(0.5, 0); LK.gui.top.addChild(throwTxt); throwTxt.y = 180; var playerTxt = new Text2('Player 1', { size: 60, fill: 0xFF2222 }); playerTxt.anchor.set(0.5, 0); LK.gui.top.addChild(playerTxt); playerTxt.y = 250; // Helper: reset pins function resetPins() { for (var i = 0; i < pins.length; i++) { pins[i].reset(); } } // Track which pins are down across frames var pinsStandingState = []; function savePinsStandingState() { pinsStandingState = []; for (var i = 0; i < pins.length; i++) { pinsStandingState.push(pins[i].isStanding); } } function restorePinsStandingState() { for (var i = 0; i < pins.length; i++) { if (pinsStandingState[i]) { pins[i].reset(); } else { pins[i].isStanding = false; pins[i].visible = false; pins[i].alpha = 0; } } } // Helper: count standing pins function countStandingPins() { var count = 0; for (var i = 0; i < pins.length; i++) { if (pins[i].isStanding) { count++; } } return count; } // Helper: get pin bounding box function getPinBounds(pin) { return { x: pin.x - 30, y: pin.y - 90, width: 60, height: 180 }; } // Helper: ball-pin collision function checkBallPinCollision(ball, pin) { if (!pin.isStanding) { return false; } var dx = ball.x - pin.x; var dy = ball.y - pin.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < ball.radius + 50) { return true; } return false; } // Helper: knock down pins hit by ball function handleBallPinCollisions() { var hit = false; for (var i = 0; i < pins.length; i++) { if (checkBallPinCollision(ball, pins[i])) { pins[i].knockDown(); hit = true; } } if (hit) { LK.getSound('hit').play(); } } // Helper: check if all pins are down function allPinsDown() { for (var i = 0; i < pins.length; i++) { if (pins[i].isStanding) { return false; } } return true; } // Helper: update GUI function updateGUI() { scoreTxt.setText('Player 1: ' + playerScores[0] + ' Player 2: ' + playerScores[1]); frameTxt.setText('Frame: ' + frame + '/' + maxFrames); throwTxt.setText('Throw: ' + currentThrow + '/' + throwsPerFrame); playerTxt.setText('Player ' + currentPlayer); if (currentPlayer === 2) { if (playerTxt.style) { playerTxt.style.fill = "#2277FF"; } } else { if (playerTxt.style) { playerTxt.style.fill = "#FF2222"; } } } // Helper: next throw or frame function nextThrowOrFrame() { var pinsDown = 10 - countStandingPins(); pinsDownThisFrame += pinsDown; playerScores[currentPlayer - 1] += pinsDown; LK.setScore(playerScores[0] + playerScores[1]); updateGUI(); var advanceFrame = false; var advancePlayer = false; if (allPinsDown()) { // Strike or spare LK.getSound('strike').play(); LK.effects.flashObject(scoreTxt, 0xffd700, 800); advanceFrame = true; advancePlayer = true; } else if (currentThrow < throwsPerFrame) { // Next throw in same frame, same player currentThrow++; advancePlayer = false; } else { // End of frame for this player advanceFrame = true; advancePlayer = true; } if (advancePlayer) { // Switch player currentPlayer++; if (currentPlayer > playerCount) { currentPlayer = 1; if (advanceFrame) { frame++; } } pinsDownThisFrame = 0; currentThrow = 1; } // End game check if (frame > maxFrames) { // Determine winner var winnerIdx = 0; if (playerScores[1] > playerScores[0]) { winnerIdx = 1; } // Remove any previous winner text if (typeof winnerTxt !== "undefined" && winnerTxt && winnerTxt.parent) { winnerTxt.parent.removeChild(winnerTxt); } // Show winner name winnerTxt = new Text2('Kazanan: Player ' + (winnerIdx + 1), { size: 120, fill: 0x0B3EE3 }); winnerTxt.anchor.set(0.5, 0.5); winnerTxt.x = 2048 / 2; winnerTxt.y = 2732 / 2; game.addChild(winnerTxt); // Fireworks effect at winner text position (custom implementation) (function fireworksEffect(cx, cy, opts) { opts = opts || {}; var count = opts.count || 12; var duration = opts.duration || 1800; var colors = opts.colors || [0xffd700, 0xff2222, 0x2277ff, 0x00ff00]; for (var i = 0; i < count; i++) { (function (i) { var angle = Math.PI * 2 * (i / count); var dist = 320 + Math.random() * 120; var color = colors[i % colors.length]; var dot = new Container(); var circ = dot.attachAsset('shadow', { anchorX: 0.5, anchorY: 0.5, width: 32, height: 32 }); circ.tint = color; circ.alpha = 0.85; dot.x = cx; dot.y = cy; dot.alpha = 1; game.addChild(dot); tween(dot, { x: cx + Math.cos(angle) * dist, y: cy + Math.sin(angle) * dist, alpha: 0 }, { duration: duration, easing: tween.cubicOut, onFinish: function onFinish() { if (dot.parent) { dot.parent.removeChild(dot); } } }); })(i); } })(winnerTxt.x, winnerTxt.y, { count: 12, duration: 1800, colors: [0xffd700, 0xff2222, 0x2277ff, 0x00ff00] }); gameOver = true; return; } // Only reset/reposition pins on frames 3, 6, 9, 1 if (frame === 1 || frame === 3 || frame === 6 || frame === 9) { resetPins(); savePinsStandingState(); } else { restorePinsStandingState(); savePinsStandingState(); } // Reset ball ball.reset(ballStartX, ballStartY); isBallRolling = false; isAiming = false; aimStart = null; aimEnd = null; updateGUI(); } // Touch/drag controls var dragStart = null; var dragEnd = null; var dragActive = false; function isOnBall(x, y) { var dx = x - ball.x; var dy = y - ball.y; return dx * dx + dy * dy < ball.radius * ball.radius * 1.2; } game.down = function (x, y, obj) { if (gameOver) { return; } if (isBallRolling) { return; } // Only allow drag if touch is on ball and ball is at start if (isOnBall(x, y) && !isBallRolling) { dragStart = { x: x, y: y }; dragActive = true; isAiming = true; } }; game.move = function (x, y, obj) { if (gameOver) { return; } if (!dragActive) { return; } // Show aiming line (not implemented visually) dragEnd = { x: x, y: y }; }; game.up = function (x, y, obj) { if (gameOver) { return; } if (!dragActive) { return; } dragActive = false; isAiming = false; dragEnd = { x: x, y: y }; // Calculate swipe vector (top-down: swipe up to roll ball up the lane) var dx = dragEnd.x - dragStart.x; var dy = dragEnd.y - dragStart.y; // Only allow upward swipes (from bottom to top) if (dy > -40) { return; } // Set ball velocity based on swipe, with more realistic physics var swipeDist = Math.sqrt(dx * dx + dy * dy); var maxSwipe = 1200; var minSwipe = 200; var cappedSwipe = Math.max(minSwipe, Math.min(swipeDist, maxSwipe)); if (swipeDist === 0) { return; } // Angle of swipe var angle = Math.atan2(dy, dx); // Ball speed scales with swipe, but capped for realism (reduced for slower ball) var baseSpeed = 24 + (cappedSwipe - minSwipe) * 0.045; // 24..~70 // Add a little curve effect for side swipes (reduced) var curve = Math.sin(angle) * (cappedSwipe / maxSwipe) * 4; // vx: mostly from dx, but curve adds a little side movement (reduced scaling) ball.vx = dx / swipeDist * baseSpeed * 0.5 + curve; // vy: mostly from dy, always negative (up the lane) (reduced scaling) ball.vy = dy / swipeDist * baseSpeed * 0.7; // Clamp vy to be negative (upwards only) if (ball.vy > -5) { ball.vy = -5; } if (ball.vy < -70) { ball.vy = -70; } // Friction is now proportional to initial speed (simulate heavier ball at high speed) ball.friction = 0.992 + 0.003 * (1 - cappedSwipe / maxSwipe); // 0.992..0.995 ball.isRolling = true; isBallRolling = true; LK.getSound('roll').play(); }; // Game update loop game.update = function () { if (gameOver) { return; } ball.update(); // Ball stays in lane (top-down) var laneLeft = lane.x - lane.width / 2 + ball.radius; var laneRight = lane.x + lane.width / 2 - ball.radius; var laneTop = lane.y - lane.height / 2 + ball.radius; var laneBottom = lane.y + lane.height / 2 - ball.radius; if (ball.x < laneLeft) { ball.x = laneLeft; ball.vx *= -0.5; } if (ball.x > laneRight) { ball.x = laneRight; ball.vx *= -0.5; } if (ball.y < laneTop) { ball.y = laneTop; ball.vy *= -0.5; } if (ball.y > laneBottom) { ball.y = laneBottom; ball.vy *= -0.5; } // Ball reaches pins if (ball.isRolling) { handleBallPinCollisions(); // Ball out of lane (top, i.e. reaches pins area) if (ball.y < laneTop + 60) { ball.isRolling = false; isBallRolling = false; LK.setTimeout(nextThrowOrFrame, 700); } // Ball stops moving if (!ball.isRolling) { LK.setTimeout(nextThrowOrFrame, 700); } } }; // Initial GUI update updateGUI(); resetPins(); savePinsStandingState();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bowling Ball class
var BowlingBall = Container.expand(function () {
var self = Container.call(this);
var shadow = self.attachAsset('shadow', {
anchorX: 0.5,
anchorY: 0.5,
y: 80
});
shadow.alpha = 0.25;
var ballAsset = self.attachAsset('bowlingBall', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = ballAsset.width / 2;
self.vx = 0;
self.vy = 0;
self.isRolling = false;
self.update = function () {
if (self.isRolling) {
self.x += self.vx;
self.y += self.vy;
// Use per-ball friction for more realistic deceleration
var friction = typeof self.friction === "number" ? self.friction : 0.995;
self.vx *= friction;
self.vy *= friction;
// Ball rotation for effect
ballAsset.rotation += 0.15 * (self.vx + self.vy);
// Stop if slow
if (Math.abs(self.vx) < 1 && Math.abs(self.vy) < 1) {
self.isRolling = false;
self.vx = 0;
self.vy = 0;
}
}
};
self.reset = function (x, y) {
self.x = x;
self.y = y;
self.vx = 0;
self.vy = 0;
self.isRolling = false;
self.friction = 0.995; // Reset to default friction
ballAsset.rotation = 0;
};
return self;
});
// Pin class
var Pin = Container.expand(function () {
var self = Container.call(this);
var pinAsset = self.attachAsset('pin', {
anchorX: 0.5,
anchorY: 1
});
self.isStanding = true;
self.knockDown = function () {
if (!self.isStanding) {
return;
}
self.isStanding = false;
// Animate pin falling
tween(self, {
rotation: Math.PI / 2,
y: self.y + 60
}, {
duration: 400,
easing: tween.cubicOut
});
// Fade out after a short delay
tween(self, {
alpha: 0
}, {
duration: 300,
delay: 400,
onFinish: function onFinish() {
self.visible = false;
}
});
};
self.reset = function () {
self.isStanding = true;
self.rotation = 0;
self.alpha = 1;
self.visible = true;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Sound for ball roll
// Shadow for ball
// Lane
// Pin
// Bowling Ball
// Lane setup
// Top-down lane setup
var lane = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(lane);
// Pin setup (top-down, triangle formation at the top of the lane)
var pins = [];
var pinRows = 4; // 4 rows: 1,2,3,4 = 10 pins
var pinSpacingX = 110;
var pinSpacingY = 110;
var laneTopY = lane.y - lane.height / 2 + 180;
var laneCenterX = lane.x;
var pinStartY = laneTopY + 60;
var pinStartX = lane.x;
var pinLayout = [[0], [-1, 1], [-2, 0, 2], [-3, -1, 1, 3]];
for (var row = 0; row < pinRows; row++) {
for (var col = 0; col < pinLayout[row].length; col++) {
var pin = new Pin();
pin.x = laneCenterX + pinLayout[row][col] * pinSpacingX;
pin.y = pinStartY + row * pinSpacingY;
pins.push(pin);
game.addChild(pin);
}
}
// Ball setup (starts at the bottom of the lane, centered)
var ballStartX = lane.x;
var ballStartY = lane.y + lane.height / 2 - 120;
var ball = new BowlingBall();
ball.reset(ballStartX, ballStartY);
game.addChild(ball);
// Game state
var throwsPerFrame = 2;
var currentThrow = 1;
var frame = 1;
var maxFrames = 10;
var pinsDownThisFrame = 0;
var isAiming = false;
var aimStart = null;
var aimEnd = null;
var isBallRolling = false;
var gameOver = false;
// 2 Player state
var playerCount = 2;
var currentPlayer = 1; // 1 or 2
var playerScores = [0, 0]; // [player1, player2]
// GUI
var scoreTxt = new Text2('Player 1: 0 Player 2: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.setText('Player 1: 0 Player 2: 0'); // ensure initial text
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var frameTxt = new Text2('Frame: 1/10', {
size: 60,
fill: "#fff"
});
frameTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(frameTxt);
frameTxt.y = 110;
var throwTxt = new Text2('Throw: 1/2', {
size: 60,
fill: "#fff"
});
throwTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(throwTxt);
throwTxt.y = 180;
var playerTxt = new Text2('Player 1', {
size: 60,
fill: 0xFF2222
});
playerTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(playerTxt);
playerTxt.y = 250;
// Helper: reset pins
function resetPins() {
for (var i = 0; i < pins.length; i++) {
pins[i].reset();
}
}
// Track which pins are down across frames
var pinsStandingState = [];
function savePinsStandingState() {
pinsStandingState = [];
for (var i = 0; i < pins.length; i++) {
pinsStandingState.push(pins[i].isStanding);
}
}
function restorePinsStandingState() {
for (var i = 0; i < pins.length; i++) {
if (pinsStandingState[i]) {
pins[i].reset();
} else {
pins[i].isStanding = false;
pins[i].visible = false;
pins[i].alpha = 0;
}
}
}
// Helper: count standing pins
function countStandingPins() {
var count = 0;
for (var i = 0; i < pins.length; i++) {
if (pins[i].isStanding) {
count++;
}
}
return count;
}
// Helper: get pin bounding box
function getPinBounds(pin) {
return {
x: pin.x - 30,
y: pin.y - 90,
width: 60,
height: 180
};
}
// Helper: ball-pin collision
function checkBallPinCollision(ball, pin) {
if (!pin.isStanding) {
return false;
}
var dx = ball.x - pin.x;
var dy = ball.y - pin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius + 50) {
return true;
}
return false;
}
// Helper: knock down pins hit by ball
function handleBallPinCollisions() {
var hit = false;
for (var i = 0; i < pins.length; i++) {
if (checkBallPinCollision(ball, pins[i])) {
pins[i].knockDown();
hit = true;
}
}
if (hit) {
LK.getSound('hit').play();
}
}
// Helper: check if all pins are down
function allPinsDown() {
for (var i = 0; i < pins.length; i++) {
if (pins[i].isStanding) {
return false;
}
}
return true;
}
// Helper: update GUI
function updateGUI() {
scoreTxt.setText('Player 1: ' + playerScores[0] + ' Player 2: ' + playerScores[1]);
frameTxt.setText('Frame: ' + frame + '/' + maxFrames);
throwTxt.setText('Throw: ' + currentThrow + '/' + throwsPerFrame);
playerTxt.setText('Player ' + currentPlayer);
if (currentPlayer === 2) {
if (playerTxt.style) {
playerTxt.style.fill = "#2277FF";
}
} else {
if (playerTxt.style) {
playerTxt.style.fill = "#FF2222";
}
}
}
// Helper: next throw or frame
function nextThrowOrFrame() {
var pinsDown = 10 - countStandingPins();
pinsDownThisFrame += pinsDown;
playerScores[currentPlayer - 1] += pinsDown;
LK.setScore(playerScores[0] + playerScores[1]);
updateGUI();
var advanceFrame = false;
var advancePlayer = false;
if (allPinsDown()) {
// Strike or spare
LK.getSound('strike').play();
LK.effects.flashObject(scoreTxt, 0xffd700, 800);
advanceFrame = true;
advancePlayer = true;
} else if (currentThrow < throwsPerFrame) {
// Next throw in same frame, same player
currentThrow++;
advancePlayer = false;
} else {
// End of frame for this player
advanceFrame = true;
advancePlayer = true;
}
if (advancePlayer) {
// Switch player
currentPlayer++;
if (currentPlayer > playerCount) {
currentPlayer = 1;
if (advanceFrame) {
frame++;
}
}
pinsDownThisFrame = 0;
currentThrow = 1;
}
// End game check
if (frame > maxFrames) {
// Determine winner
var winnerIdx = 0;
if (playerScores[1] > playerScores[0]) {
winnerIdx = 1;
}
// Remove any previous winner text
if (typeof winnerTxt !== "undefined" && winnerTxt && winnerTxt.parent) {
winnerTxt.parent.removeChild(winnerTxt);
}
// Show winner name
winnerTxt = new Text2('Kazanan: Player ' + (winnerIdx + 1), {
size: 120,
fill: 0x0B3EE3
});
winnerTxt.anchor.set(0.5, 0.5);
winnerTxt.x = 2048 / 2;
winnerTxt.y = 2732 / 2;
game.addChild(winnerTxt);
// Fireworks effect at winner text position (custom implementation)
(function fireworksEffect(cx, cy, opts) {
opts = opts || {};
var count = opts.count || 12;
var duration = opts.duration || 1800;
var colors = opts.colors || [0xffd700, 0xff2222, 0x2277ff, 0x00ff00];
for (var i = 0; i < count; i++) {
(function (i) {
var angle = Math.PI * 2 * (i / count);
var dist = 320 + Math.random() * 120;
var color = colors[i % colors.length];
var dot = new Container();
var circ = dot.attachAsset('shadow', {
anchorX: 0.5,
anchorY: 0.5,
width: 32,
height: 32
});
circ.tint = color;
circ.alpha = 0.85;
dot.x = cx;
dot.y = cy;
dot.alpha = 1;
game.addChild(dot);
tween(dot, {
x: cx + Math.cos(angle) * dist,
y: cy + Math.sin(angle) * dist,
alpha: 0
}, {
duration: duration,
easing: tween.cubicOut,
onFinish: function onFinish() {
if (dot.parent) {
dot.parent.removeChild(dot);
}
}
});
})(i);
}
})(winnerTxt.x, winnerTxt.y, {
count: 12,
duration: 1800,
colors: [0xffd700, 0xff2222, 0x2277ff, 0x00ff00]
});
gameOver = true;
return;
}
// Only reset/reposition pins on frames 3, 6, 9, 1
if (frame === 1 || frame === 3 || frame === 6 || frame === 9) {
resetPins();
savePinsStandingState();
} else {
restorePinsStandingState();
savePinsStandingState();
}
// Reset ball
ball.reset(ballStartX, ballStartY);
isBallRolling = false;
isAiming = false;
aimStart = null;
aimEnd = null;
updateGUI();
}
// Touch/drag controls
var dragStart = null;
var dragEnd = null;
var dragActive = false;
function isOnBall(x, y) {
var dx = x - ball.x;
var dy = y - ball.y;
return dx * dx + dy * dy < ball.radius * ball.radius * 1.2;
}
game.down = function (x, y, obj) {
if (gameOver) {
return;
}
if (isBallRolling) {
return;
}
// Only allow drag if touch is on ball and ball is at start
if (isOnBall(x, y) && !isBallRolling) {
dragStart = {
x: x,
y: y
};
dragActive = true;
isAiming = true;
}
};
game.move = function (x, y, obj) {
if (gameOver) {
return;
}
if (!dragActive) {
return;
}
// Show aiming line (not implemented visually)
dragEnd = {
x: x,
y: y
};
};
game.up = function (x, y, obj) {
if (gameOver) {
return;
}
if (!dragActive) {
return;
}
dragActive = false;
isAiming = false;
dragEnd = {
x: x,
y: y
};
// Calculate swipe vector (top-down: swipe up to roll ball up the lane)
var dx = dragEnd.x - dragStart.x;
var dy = dragEnd.y - dragStart.y;
// Only allow upward swipes (from bottom to top)
if (dy > -40) {
return;
}
// Set ball velocity based on swipe, with more realistic physics
var swipeDist = Math.sqrt(dx * dx + dy * dy);
var maxSwipe = 1200;
var minSwipe = 200;
var cappedSwipe = Math.max(minSwipe, Math.min(swipeDist, maxSwipe));
if (swipeDist === 0) {
return;
}
// Angle of swipe
var angle = Math.atan2(dy, dx);
// Ball speed scales with swipe, but capped for realism (reduced for slower ball)
var baseSpeed = 24 + (cappedSwipe - minSwipe) * 0.045; // 24..~70
// Add a little curve effect for side swipes (reduced)
var curve = Math.sin(angle) * (cappedSwipe / maxSwipe) * 4;
// vx: mostly from dx, but curve adds a little side movement (reduced scaling)
ball.vx = dx / swipeDist * baseSpeed * 0.5 + curve;
// vy: mostly from dy, always negative (up the lane) (reduced scaling)
ball.vy = dy / swipeDist * baseSpeed * 0.7;
// Clamp vy to be negative (upwards only)
if (ball.vy > -5) {
ball.vy = -5;
}
if (ball.vy < -70) {
ball.vy = -70;
}
// Friction is now proportional to initial speed (simulate heavier ball at high speed)
ball.friction = 0.992 + 0.003 * (1 - cappedSwipe / maxSwipe); // 0.992..0.995
ball.isRolling = true;
isBallRolling = true;
LK.getSound('roll').play();
};
// Game update loop
game.update = function () {
if (gameOver) {
return;
}
ball.update();
// Ball stays in lane (top-down)
var laneLeft = lane.x - lane.width / 2 + ball.radius;
var laneRight = lane.x + lane.width / 2 - ball.radius;
var laneTop = lane.y - lane.height / 2 + ball.radius;
var laneBottom = lane.y + lane.height / 2 - ball.radius;
if (ball.x < laneLeft) {
ball.x = laneLeft;
ball.vx *= -0.5;
}
if (ball.x > laneRight) {
ball.x = laneRight;
ball.vx *= -0.5;
}
if (ball.y < laneTop) {
ball.y = laneTop;
ball.vy *= -0.5;
}
if (ball.y > laneBottom) {
ball.y = laneBottom;
ball.vy *= -0.5;
}
// Ball reaches pins
if (ball.isRolling) {
handleBallPinCollisions();
// Ball out of lane (top, i.e. reaches pins area)
if (ball.y < laneTop + 60) {
ball.isRolling = false;
isBallRolling = false;
LK.setTimeout(nextThrowOrFrame, 700);
}
// Ball stops moving
if (!ball.isRolling) {
LK.setTimeout(nextThrowOrFrame, 700);
}
}
};
// Initial GUI update
updateGUI();
resetPins();
savePinsStandingState();