/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var AIPlayer = Container.expand(function () { var self = Container.call(this); self.difficulty = 0.7; // AI skill level (0.0 to 1.0) self.isThinking = false; self.thinkingTime = 1500; // Time AI takes to "think" // Find best shot for AI self.findBestShot = function () { var playerGroup = currentPlayer === 1 ? player1Group : player2Group; var targetBalls = []; // Find target balls based on player group for (var i = 0; i < balls.length; i++) { var ball = balls[i]; if (ball.isPotted || ball.type === 'cue') continue; if (!playerGroup) { // No groups assigned yet, target any numbered ball if (ball.type !== 'eight') { targetBalls.push(ball); } } else if (ball.type === playerGroup) { targetBalls.push(ball); } else if (ball.type === 'eight' && allGroupBallsPotted(playerGroup)) { targetBalls.push(ball); } } if (targetBalls.length === 0) return null; // Find best target ball (closest to a pocket) var bestTarget = null; var bestScore = -1; for (var i = 0; i < targetBalls.length; i++) { var ball = targetBalls[i]; var score = self.evaluateBallShot(ball); if (score > bestScore) { bestScore = score; bestTarget = ball; } } return bestTarget; }; // Evaluate how good a shot on a specific ball would be self.evaluateBallShot = function (targetBall) { var bestPocketScore = 0; for (var i = 0; i < pockets.length; i++) { var pocket = pockets[i]; var distanceToPocket = Math.sqrt(Math.pow(targetBall.x - pocket.x, 2) + Math.pow(targetBall.y - pocket.y, 2)); // Check if there's a clear path from cue ball to target ball var cueToBallClear = self.isPathClear(cueBall.x, cueBall.y, targetBall.x, targetBall.y); // Check if there's a clear path from target ball to pocket var ballToPocketClear = self.isPathClear(targetBall.x, targetBall.y, pocket.x, pocket.y); if (cueToBallClear && ballToPocketClear) { var score = (1000 - distanceToPocket) / 1000; if (score > bestPocketScore) { bestPocketScore = score; } } } return bestPocketScore; }; // Check if path between two points is clear of other balls self.isPathClear = function (x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 10) return true; var steps = Math.floor(distance / 20); var stepX = dx / steps; var stepY = dy / steps; for (var step = 1; step < steps; step++) { var checkX = x1 + stepX * step; var checkY = y1 + stepY * step; for (var i = 0; i < balls.length; i++) { var ball = balls[i]; if (ball.isPotted || ball === cueBall) continue; var ballDist = Math.sqrt(Math.pow(checkX - ball.x, 2) + Math.pow(checkY - ball.y, 2)); if (ballDist < ball.radius + 10) { return false; } } } return true; }; // Calculate shot angle and power for target ball self.calculateShot = function (targetBall) { if (!targetBall) return null; var bestShot = null; var bestScore = -1; for (var i = 0; i < pockets.length; i++) { var pocket = pockets[i]; // Calculate angle from target ball to pocket var ballToPocketDx = pocket.x - targetBall.x; var ballToPocketDy = pocket.y - targetBall.y; var ballToPocketAngle = Math.atan2(ballToPocketDy, ballToPocketDx); // Calculate where cue ball should hit target ball var hitDistance = targetBall.radius + cueBall.radius + 5; var hitX = targetBall.x - Math.cos(ballToPocketAngle) * hitDistance; var hitY = targetBall.y - Math.sin(ballToPocketAngle) * hitDistance; // Calculate shot from cue ball to hit point var cueToDx = hitX - cueBall.x; var cueToDy = hitY - cueBall.y; var cueToDistance = Math.sqrt(cueToDx * cueToDx + cueToDy * cueToDy); if (cueToDistance > 50 && self.isPathClear(cueBall.x, cueBall.y, hitX, hitY)) { var shotAngle = Math.atan2(cueToDy, cueToDx); var power = Math.min(cueToDistance / 30, 20); // Add some randomness based on difficulty var angleError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3; var powerError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3; shotAngle += angleError; power += powerError; var score = self.evaluateBallShot(targetBall); if (score > bestScore) { bestScore = score; bestShot = { angle: shotAngle, power: Math.max(5, Math.min(25, power)), targetBall: targetBall, pocket: pocket }; } } } return bestShot; }; // Execute AI turn self.takeTurn = function () { if (currentPlayer === 1 || ballsMoving || gameWon) return; self.isThinking = true; gameStatusText.setText('AI shooting...'); var targetBall = self.findBestShot(); var shot = self.calculateShot(targetBall); if (shot) { // Calculate force components var forceX = Math.cos(shot.angle) * shot.power; var forceY = Math.sin(shot.angle) * shot.power; // Execute shot cueBall.hit(forceX, forceY); turnEnded = true; } else { // No good shot found, take a random shot var randomAngle = Math.random() * Math.PI * 2; var randomPower = 5 + Math.random() * 10; var forceX = Math.cos(randomAngle) * randomPower; var forceY = Math.sin(randomAngle) * randomPower; cueBall.hit(forceX, forceY); turnEnded = true; } self.isThinking = false; }; return self; }); var AimingSystem = Container.expand(function () { var self = Container.call(this); self.isAiming = false; self.startX = 0; self.startY = 0; self.endX = 0; self.endY = 0; self.power = 0; self.maxPower = 25; var aimLine = self.attachAsset('aimLine', { anchorX: 0, anchorY: 0.5 }); var powerBarBg = self.attachAsset('powerBar', { anchorX: 0.5, anchorY: 0.5 }); var powerBarFill = self.attachAsset('powerFill', { anchorX: 0, anchorY: 0.5 }); aimLine.visible = false; powerBarBg.visible = false; powerBarFill.visible = false; powerBarBg.x = 1024; powerBarBg.y = 2500; powerBarFill.x = 1024 - 150; powerBarFill.y = 2500; self.startAiming = function (x, y) { self.isAiming = true; self.startX = x; self.startY = y; aimLine.visible = true; powerBarBg.visible = true; powerBarFill.visible = true; }; self.updateAiming = function (x, y) { if (!self.isAiming) return; self.endX = x; self.endY = y; var dx = self.endX - self.startX; var dy = self.endY - self.startY; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); // Update aim line aimLine.x = self.startX; aimLine.y = self.startY; aimLine.rotation = angle; aimLine.width = Math.min(distance, 200); // Update power self.power = Math.min(distance / 20, self.maxPower); powerBarFill.width = self.power / self.maxPower * 300; if (self.power < 5) { powerBarFill.tint = 0x00FF00; } else if (self.power < 10) { powerBarFill.tint = 0xFFFF00; } else { powerBarFill.tint = 0xFF0000; } }; self.shoot = function () { if (!self.isAiming) return { forceX: 0, forceY: 0 }; var dx = self.endX - self.startX; var dy = self.endY - self.startY; var distance = Math.sqrt(dx * dx + dy * dy); var forceX = dx / distance * self.power * 1.0; var forceY = dy / distance * self.power * 1.0; self.stopAiming(); return { forceX: forceX, forceY: forceY }; }; self.stopAiming = function () { self.isAiming = false; aimLine.visible = false; powerBarBg.visible = false; powerBarFill.visible = false; }; return self; }); var Ball = Container.expand(function (type, ballNumber) { var self = Container.call(this); self.type = type || 'solid'; self.number = ballNumber || 1; self.velocityX = 0; self.velocityY = 0; self.radius = 40; self.friction = 0.98; self.isPotted = false; var ballGraphic; var numberText; if (type === 'cue') { ballGraphic = self.attachAsset('cueBall', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'eight') { ballGraphic = self.attachAsset('eightBall', { anchorX: 0.5, anchorY: 0.5 }); // Add number 8 to the 8-ball numberText = new Text2('8', { size: 54, fill: 0xFFFFFF }); numberText.anchor.set(0.5, 0.5); self.addChild(numberText); } else { // Use specific colored balls based on number var assetName = 'ball' + ballNumber; ballGraphic = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // Add number text to all numbered balls numberText = new Text2(ballNumber.toString(), { size: 48, fill: type === 'stripe' ? 0x000000 : 0xFFFFFF }); numberText.anchor.set(0.5, 0.5); self.addChild(numberText); } self.update = function () { if (self.isPotted) return; // Apply physics self.x += self.velocityX; self.y += self.velocityY; // Apply friction - make colored balls more slippery var frictionValue = self.type === 'cue' ? self.friction : 0.995; // Much lower friction for colored balls to make them slippery self.velocityX *= frictionValue; self.velocityY *= frictionValue; // Stop very slow movement if (Math.abs(self.velocityX) < 0.1) self.velocityX = 0; if (Math.abs(self.velocityY) < 0.1) self.velocityY = 0; // Table bounds collision var tableLeft = 124 + self.radius; var tableRight = 1924 - self.radius; var tableTop = 916 + self.radius; var tableBottom = 1816 - self.radius; if (self.x <= tableLeft || self.x >= tableRight) { self.velocityX *= -0.8; self.x = Math.max(tableLeft, Math.min(tableRight, self.x)); LK.getSound('railBounce').play(); } if (self.y <= tableTop || self.y >= tableBottom) { self.velocityY *= -0.8; self.y = Math.max(tableTop, Math.min(tableBottom, self.y)); LK.getSound('railBounce').play(); } }; self.hit = function (forceX, forceY) { // Reduce white ball (cue ball) force multiplier to decrease its power var forceMultiplier = self.type === 'cue' ? 1.5 : 1.0; self.velocityX += forceX * forceMultiplier; self.velocityY += forceY * forceMultiplier; LK.getSound('ballHit').play(); }; self.isMoving = function () { return Math.abs(self.velocityX) > 0.1 || Math.abs(self.velocityY) > 0.1; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1a4c2b }); /**** * Game Code ****/ // Game state variables // Colorful solid balls (1-7) // Gold // Blue // Red // Purple // Orange // Green // Brown // Colorful stripe balls (9-15) // Gold stripe // Blue stripe // Red stripe // Purple stripe // Orange stripe // Green stripe // Brown stripe var currentPlayer = 1; var player1Group = null; // 'solid' or 'stripe' var player2Group = null; var gameStarted = false; var ballsMoving = false; var turnEnded = false; var gameWon = false; var correctBallPotted = false; // Track if correct ball was potted this turn var difficultySelected = true; var selectedDifficulty = 'medium'; // Game objects var balls = []; var pockets = []; var cueBall = null; var aimingSystem = null; var aiPlayer = null; // UI elements var playerTurnText = new Text2('Player Turn', { size: 60, fill: 0xFFFFFF }); playerTurnText.anchor.set(0.5, 0); var gameStatusText = new Text2('Touch and drag from cue ball to aim', { size: 40, fill: 0xFFFF00 }); gameStatusText.anchor.set(0.5, 0); LK.gui.top.addChild(playerTurnText); LK.gui.bottom.addChild(gameStatusText); // Create table var tableRail = game.addChild(LK.getAsset('tableRail', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366 })); var poolTable = game.addChild(LK.getAsset('poolTable', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366 })); // Create pockets var pocketPositions = [{ x: 134, y: 926 }, // top left { x: 1024, y: 916 }, // top middle { x: 1914, y: 926 }, // top right { x: 134, y: 1806 }, // bottom left { x: 1024, y: 1816 }, // bottom middle { x: 1914, y: 1806 } // bottom right ]; for (var i = 0; i < pocketPositions.length; i++) { var pocket = game.addChild(LK.getAsset('pocket', { anchorX: 0.5, anchorY: 0.5, x: pocketPositions[i].x, y: pocketPositions[i].y })); pockets.push(pocket); } // Create aiming system aimingSystem = game.addChild(new AimingSystem()); // Initialize balls function setupBalls() { // Cue ball cueBall = game.addChild(new Ball('cue', 0)); cueBall.x = 1024 - 400; cueBall.y = 1366; balls.push(cueBall); // Ball rack positions (triangle formation) var rackX = 1024 + 300; var rackY = 1366; var ballSpacing = 82; var ballTypes = ['solid', 'stripe', 'solid', 'stripe', 'eight', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid']; var ballNumbers = [1, 9, 2, 10, 8, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7]; var ballIndex = 0; // Create triangle rack for (var row = 0; row < 5; row++) { for (var col = 0; col <= row; col++) { if (ballIndex < 15) { var ball = game.addChild(new Ball(ballTypes[ballIndex], ballNumbers[ballIndex])); ball.x = rackX + row * ballSpacing * 0.866; ball.y = rackY - row * ballSpacing * 0.5 + col * ballSpacing; balls.push(ball); ballIndex++; } } } } // Ball collision detection function checkBallCollisions() { for (var i = 0; i < balls.length; i++) { for (var j = i + 1; j < balls.length; j++) { var ball1 = balls[i]; var ball2 = balls[j]; if (ball1.isPotted || ball2.isPotted) continue; var dx = ball2.x - ball1.x; var dy = ball2.y - ball1.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < ball1.radius + ball2.radius) { // Collision detected var angle = Math.atan2(dy, dx); var sin = Math.sin(angle); var cos = Math.cos(angle); // Separate balls var overlap = ball1.radius + ball2.radius - distance; ball1.x -= cos * overlap * 0.5; ball1.y -= sin * overlap * 0.5; ball2.x += cos * overlap * 0.5; ball2.y += sin * overlap * 0.5; // Exchange velocities (simplified) var tempVx = ball1.velocityX; var tempVy = ball1.velocityY; // Apply original force transfer values var ball1Force = ball1.type === 'cue' ? 1.0 : 1.0; var ball2Force = ball2.type === 'cue' ? 1.0 : 1.0; ball1.velocityX = ball2.velocityX * ball1Force; ball1.velocityY = ball2.velocityY * ball1Force; ball2.velocityX = tempVx * ball2Force; ball2.velocityY = tempVy * ball2Force; LK.getSound('ballHit').play(); } } } } // Check pocket collisions function checkPocketCollisions() { for (var i = 0; i < balls.length; i++) { var ball = balls[i]; if (ball.isPotted) continue; for (var j = 0; j < pockets.length; j++) { var pocket = pockets[j]; var dx = ball.x - pocket.x; var dy = ball.y - pocket.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 60) { // Ball potted ball.isPotted = true; ball.visible = false; ball.velocityX = 0; ball.velocityY = 0; LK.getSound('ballPocket').play(); handleBallPotted(ball); break; } } } } // Handle ball potted logic function handleBallPotted(ball) { if (ball.type === 'cue') { // Scratch - end turn gameStatusText.setText('Scratch! Turn ends.'); resetCueBall(); } else if (ball.type === 'eight') { // 8-ball potted var playerGroup = currentPlayer === 1 ? player1Group : player2Group; if (playerGroup && allGroupBallsPotted(playerGroup)) { // Win condition gameStatusText.setText('Player ' + currentPlayer + ' Wins!'); gameWon = true; LK.setScore(LK.getScore() + 100); LK.setTimeout(function () { LK.showYouWin(); }, 2000); } else { // 8-ball potted too early - lose var winner = currentPlayer === 1 ? 2 : 1; gameStatusText.setText('Player ' + winner + ' Wins! 8-ball potted early.'); gameWon = true; LK.setTimeout(function () { LK.showGameOver(); }, 2000); } } else { // Regular ball potted if (!player1Group && !player2Group) { // First ball potted determines groups if (currentPlayer === 1) { player1Group = ball.type; player2Group = ball.type === 'solid' ? 'stripe' : 'solid'; } else { player2Group = ball.type; player1Group = ball.type === 'solid' ? 'stripe' : 'solid'; } } var playerGroup = currentPlayer === 1 ? player1Group : player2Group; if (ball.type === playerGroup) { // Correct ball potted - continue turn gameStatusText.setText('Good shot! Continue.'); correctBallPotted = true; // Player gets another shot } else { // Wrong ball potted - end turn gameStatusText.setText('Wrong ball! Turn ends.'); } } } // Check if all group balls are potted function allGroupBallsPotted(group) { for (var i = 0; i < balls.length; i++) { var ball = balls[i]; if (ball.type === group && !ball.isPotted) { return false; } } return true; } // Reset cue ball after scratch function resetCueBall() { cueBall.x = 1024 - 400; cueBall.y = 1366; cueBall.isPotted = false; cueBall.visible = true; cueBall.velocityX = 0; cueBall.velocityY = 0; } // Switch turns function switchTurn() { currentPlayer = currentPlayer === 1 ? 2 : 1; if (currentPlayer === 1) { playerTurnText.setText('Player Turn'); gameStatusText.setText('Touch and drag from cue ball to aim'); } else { playerTurnText.setText('AI Turn'); gameStatusText.setText('AI is preparing...'); } turnEnded = false; correctBallPotted = false; // Reset flag for new turn } // Check if all balls stopped moving function checkBallsMoving() { ballsMoving = false; for (var i = 0; i < balls.length; i++) { if (balls[i].isMoving()) { ballsMoving = true; break; } } if (!ballsMoving && turnEnded) { // Turn is complete, check if should switch player if (!correctBallPotted) { // No correct ball potted, switch to next player switchTurn(); } else { // Correct ball was potted, same player continues turnEnded = false; correctBallPotted = false; // Reset flag for next shot } } } // Touch handling var isDragging = false; var dragStartX = 0; var dragStartY = 0; game.down = function (x, y, obj) { if (gameWon || ballsMoving || currentPlayer !== 1) return; // Check if touching near cue ball var dx = x - cueBall.x; var dy = y - cueBall.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 90) { isDragging = true; dragStartX = x; dragStartY = y; aimingSystem.startAiming(cueBall.x, cueBall.y); } }; game.move = function (x, y, obj) { if (isDragging && !ballsMoving && currentPlayer === 1) { aimingSystem.updateAiming(x, y); } }; game.up = function (x, y, obj) { if (isDragging && !ballsMoving && currentPlayer === 1) { var force = aimingSystem.shoot(); cueBall.hit(force.forceX, force.forceY); isDragging = false; turnEnded = true; gameStatusText.setText('Balls in motion...'); } }; // Initialize game immediately setupBalls(); aiPlayer = game.addChild(new AIPlayer()); aiPlayer.difficulty = 1.0; // Maximum difficulty - perfect AI gameStarted = true; // Main game loop game.update = function () { if (!difficultySelected || !gameStarted || gameWon) return; // Update all balls for (var i = 0; i < balls.length; i++) { balls[i].update(); } // Check collisions checkBallCollisions(); checkPocketCollisions(); // Check if balls are still moving checkBallsMoving(); // Trigger AI turn if it's AI's turn and balls are not moving if (currentPlayer === 2 && !ballsMoving && !turnEnded && !aiPlayer.isThinking) { aiPlayer.takeTurn(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var AIPlayer = Container.expand(function () {
var self = Container.call(this);
self.difficulty = 0.7; // AI skill level (0.0 to 1.0)
self.isThinking = false;
self.thinkingTime = 1500; // Time AI takes to "think"
// Find best shot for AI
self.findBestShot = function () {
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
var targetBalls = [];
// Find target balls based on player group
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted || ball.type === 'cue') continue;
if (!playerGroup) {
// No groups assigned yet, target any numbered ball
if (ball.type !== 'eight') {
targetBalls.push(ball);
}
} else if (ball.type === playerGroup) {
targetBalls.push(ball);
} else if (ball.type === 'eight' && allGroupBallsPotted(playerGroup)) {
targetBalls.push(ball);
}
}
if (targetBalls.length === 0) return null;
// Find best target ball (closest to a pocket)
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < targetBalls.length; i++) {
var ball = targetBalls[i];
var score = self.evaluateBallShot(ball);
if (score > bestScore) {
bestScore = score;
bestTarget = ball;
}
}
return bestTarget;
};
// Evaluate how good a shot on a specific ball would be
self.evaluateBallShot = function (targetBall) {
var bestPocketScore = 0;
for (var i = 0; i < pockets.length; i++) {
var pocket = pockets[i];
var distanceToPocket = Math.sqrt(Math.pow(targetBall.x - pocket.x, 2) + Math.pow(targetBall.y - pocket.y, 2));
// Check if there's a clear path from cue ball to target ball
var cueToBallClear = self.isPathClear(cueBall.x, cueBall.y, targetBall.x, targetBall.y);
// Check if there's a clear path from target ball to pocket
var ballToPocketClear = self.isPathClear(targetBall.x, targetBall.y, pocket.x, pocket.y);
if (cueToBallClear && ballToPocketClear) {
var score = (1000 - distanceToPocket) / 1000;
if (score > bestPocketScore) {
bestPocketScore = score;
}
}
}
return bestPocketScore;
};
// Check if path between two points is clear of other balls
self.isPathClear = function (x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) return true;
var steps = Math.floor(distance / 20);
var stepX = dx / steps;
var stepY = dy / steps;
for (var step = 1; step < steps; step++) {
var checkX = x1 + stepX * step;
var checkY = y1 + stepY * step;
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted || ball === cueBall) continue;
var ballDist = Math.sqrt(Math.pow(checkX - ball.x, 2) + Math.pow(checkY - ball.y, 2));
if (ballDist < ball.radius + 10) {
return false;
}
}
}
return true;
};
// Calculate shot angle and power for target ball
self.calculateShot = function (targetBall) {
if (!targetBall) return null;
var bestShot = null;
var bestScore = -1;
for (var i = 0; i < pockets.length; i++) {
var pocket = pockets[i];
// Calculate angle from target ball to pocket
var ballToPocketDx = pocket.x - targetBall.x;
var ballToPocketDy = pocket.y - targetBall.y;
var ballToPocketAngle = Math.atan2(ballToPocketDy, ballToPocketDx);
// Calculate where cue ball should hit target ball
var hitDistance = targetBall.radius + cueBall.radius + 5;
var hitX = targetBall.x - Math.cos(ballToPocketAngle) * hitDistance;
var hitY = targetBall.y - Math.sin(ballToPocketAngle) * hitDistance;
// Calculate shot from cue ball to hit point
var cueToDx = hitX - cueBall.x;
var cueToDy = hitY - cueBall.y;
var cueToDistance = Math.sqrt(cueToDx * cueToDx + cueToDy * cueToDy);
if (cueToDistance > 50 && self.isPathClear(cueBall.x, cueBall.y, hitX, hitY)) {
var shotAngle = Math.atan2(cueToDy, cueToDx);
var power = Math.min(cueToDistance / 30, 20);
// Add some randomness based on difficulty
var angleError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3;
var powerError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3;
shotAngle += angleError;
power += powerError;
var score = self.evaluateBallShot(targetBall);
if (score > bestScore) {
bestScore = score;
bestShot = {
angle: shotAngle,
power: Math.max(5, Math.min(25, power)),
targetBall: targetBall,
pocket: pocket
};
}
}
}
return bestShot;
};
// Execute AI turn
self.takeTurn = function () {
if (currentPlayer === 1 || ballsMoving || gameWon) return;
self.isThinking = true;
gameStatusText.setText('AI shooting...');
var targetBall = self.findBestShot();
var shot = self.calculateShot(targetBall);
if (shot) {
// Calculate force components
var forceX = Math.cos(shot.angle) * shot.power;
var forceY = Math.sin(shot.angle) * shot.power;
// Execute shot
cueBall.hit(forceX, forceY);
turnEnded = true;
} else {
// No good shot found, take a random shot
var randomAngle = Math.random() * Math.PI * 2;
var randomPower = 5 + Math.random() * 10;
var forceX = Math.cos(randomAngle) * randomPower;
var forceY = Math.sin(randomAngle) * randomPower;
cueBall.hit(forceX, forceY);
turnEnded = true;
}
self.isThinking = false;
};
return self;
});
var AimingSystem = Container.expand(function () {
var self = Container.call(this);
self.isAiming = false;
self.startX = 0;
self.startY = 0;
self.endX = 0;
self.endY = 0;
self.power = 0;
self.maxPower = 25;
var aimLine = self.attachAsset('aimLine', {
anchorX: 0,
anchorY: 0.5
});
var powerBarBg = self.attachAsset('powerBar', {
anchorX: 0.5,
anchorY: 0.5
});
var powerBarFill = self.attachAsset('powerFill', {
anchorX: 0,
anchorY: 0.5
});
aimLine.visible = false;
powerBarBg.visible = false;
powerBarFill.visible = false;
powerBarBg.x = 1024;
powerBarBg.y = 2500;
powerBarFill.x = 1024 - 150;
powerBarFill.y = 2500;
self.startAiming = function (x, y) {
self.isAiming = true;
self.startX = x;
self.startY = y;
aimLine.visible = true;
powerBarBg.visible = true;
powerBarFill.visible = true;
};
self.updateAiming = function (x, y) {
if (!self.isAiming) return;
self.endX = x;
self.endY = y;
var dx = self.endX - self.startX;
var dy = self.endY - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
// Update aim line
aimLine.x = self.startX;
aimLine.y = self.startY;
aimLine.rotation = angle;
aimLine.width = Math.min(distance, 200);
// Update power
self.power = Math.min(distance / 20, self.maxPower);
powerBarFill.width = self.power / self.maxPower * 300;
if (self.power < 5) {
powerBarFill.tint = 0x00FF00;
} else if (self.power < 10) {
powerBarFill.tint = 0xFFFF00;
} else {
powerBarFill.tint = 0xFF0000;
}
};
self.shoot = function () {
if (!self.isAiming) return {
forceX: 0,
forceY: 0
};
var dx = self.endX - self.startX;
var dy = self.endY - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
var forceX = dx / distance * self.power * 1.0;
var forceY = dy / distance * self.power * 1.0;
self.stopAiming();
return {
forceX: forceX,
forceY: forceY
};
};
self.stopAiming = function () {
self.isAiming = false;
aimLine.visible = false;
powerBarBg.visible = false;
powerBarFill.visible = false;
};
return self;
});
var Ball = Container.expand(function (type, ballNumber) {
var self = Container.call(this);
self.type = type || 'solid';
self.number = ballNumber || 1;
self.velocityX = 0;
self.velocityY = 0;
self.radius = 40;
self.friction = 0.98;
self.isPotted = false;
var ballGraphic;
var numberText;
if (type === 'cue') {
ballGraphic = self.attachAsset('cueBall', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'eight') {
ballGraphic = self.attachAsset('eightBall', {
anchorX: 0.5,
anchorY: 0.5
});
// Add number 8 to the 8-ball
numberText = new Text2('8', {
size: 54,
fill: 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
self.addChild(numberText);
} else {
// Use specific colored balls based on number
var assetName = 'ball' + ballNumber;
ballGraphic = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add number text to all numbered balls
numberText = new Text2(ballNumber.toString(), {
size: 48,
fill: type === 'stripe' ? 0x000000 : 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
self.addChild(numberText);
}
self.update = function () {
if (self.isPotted) return;
// Apply physics
self.x += self.velocityX;
self.y += self.velocityY;
// Apply friction - make colored balls more slippery
var frictionValue = self.type === 'cue' ? self.friction : 0.995; // Much lower friction for colored balls to make them slippery
self.velocityX *= frictionValue;
self.velocityY *= frictionValue;
// Stop very slow movement
if (Math.abs(self.velocityX) < 0.1) self.velocityX = 0;
if (Math.abs(self.velocityY) < 0.1) self.velocityY = 0;
// Table bounds collision
var tableLeft = 124 + self.radius;
var tableRight = 1924 - self.radius;
var tableTop = 916 + self.radius;
var tableBottom = 1816 - self.radius;
if (self.x <= tableLeft || self.x >= tableRight) {
self.velocityX *= -0.8;
self.x = Math.max(tableLeft, Math.min(tableRight, self.x));
LK.getSound('railBounce').play();
}
if (self.y <= tableTop || self.y >= tableBottom) {
self.velocityY *= -0.8;
self.y = Math.max(tableTop, Math.min(tableBottom, self.y));
LK.getSound('railBounce').play();
}
};
self.hit = function (forceX, forceY) {
// Reduce white ball (cue ball) force multiplier to decrease its power
var forceMultiplier = self.type === 'cue' ? 1.5 : 1.0;
self.velocityX += forceX * forceMultiplier;
self.velocityY += forceY * forceMultiplier;
LK.getSound('ballHit').play();
};
self.isMoving = function () {
return Math.abs(self.velocityX) > 0.1 || Math.abs(self.velocityY) > 0.1;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a4c2b
});
/****
* Game Code
****/
// Game state variables
// Colorful solid balls (1-7)
// Gold
// Blue
// Red
// Purple
// Orange
// Green
// Brown
// Colorful stripe balls (9-15)
// Gold stripe
// Blue stripe
// Red stripe
// Purple stripe
// Orange stripe
// Green stripe
// Brown stripe
var currentPlayer = 1;
var player1Group = null; // 'solid' or 'stripe'
var player2Group = null;
var gameStarted = false;
var ballsMoving = false;
var turnEnded = false;
var gameWon = false;
var correctBallPotted = false; // Track if correct ball was potted this turn
var difficultySelected = true;
var selectedDifficulty = 'medium';
// Game objects
var balls = [];
var pockets = [];
var cueBall = null;
var aimingSystem = null;
var aiPlayer = null;
// UI elements
var playerTurnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
playerTurnText.anchor.set(0.5, 0);
var gameStatusText = new Text2('Touch and drag from cue ball to aim', {
size: 40,
fill: 0xFFFF00
});
gameStatusText.anchor.set(0.5, 0);
LK.gui.top.addChild(playerTurnText);
LK.gui.bottom.addChild(gameStatusText);
// Create table
var tableRail = game.addChild(LK.getAsset('tableRail', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
var poolTable = game.addChild(LK.getAsset('poolTable', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Create pockets
var pocketPositions = [{
x: 134,
y: 926
},
// top left
{
x: 1024,
y: 916
},
// top middle
{
x: 1914,
y: 926
},
// top right
{
x: 134,
y: 1806
},
// bottom left
{
x: 1024,
y: 1816
},
// bottom middle
{
x: 1914,
y: 1806
} // bottom right
];
for (var i = 0; i < pocketPositions.length; i++) {
var pocket = game.addChild(LK.getAsset('pocket', {
anchorX: 0.5,
anchorY: 0.5,
x: pocketPositions[i].x,
y: pocketPositions[i].y
}));
pockets.push(pocket);
}
// Create aiming system
aimingSystem = game.addChild(new AimingSystem());
// Initialize balls
function setupBalls() {
// Cue ball
cueBall = game.addChild(new Ball('cue', 0));
cueBall.x = 1024 - 400;
cueBall.y = 1366;
balls.push(cueBall);
// Ball rack positions (triangle formation)
var rackX = 1024 + 300;
var rackY = 1366;
var ballSpacing = 82;
var ballTypes = ['solid', 'stripe', 'solid', 'stripe', 'eight', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid'];
var ballNumbers = [1, 9, 2, 10, 8, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7];
var ballIndex = 0;
// Create triangle rack
for (var row = 0; row < 5; row++) {
for (var col = 0; col <= row; col++) {
if (ballIndex < 15) {
var ball = game.addChild(new Ball(ballTypes[ballIndex], ballNumbers[ballIndex]));
ball.x = rackX + row * ballSpacing * 0.866;
ball.y = rackY - row * ballSpacing * 0.5 + col * ballSpacing;
balls.push(ball);
ballIndex++;
}
}
}
}
// Ball collision detection
function checkBallCollisions() {
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
var ball1 = balls[i];
var ball2 = balls[j];
if (ball1.isPotted || ball2.isPotted) continue;
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ball1.radius + ball2.radius) {
// Collision detected
var angle = Math.atan2(dy, dx);
var sin = Math.sin(angle);
var cos = Math.cos(angle);
// Separate balls
var overlap = ball1.radius + ball2.radius - distance;
ball1.x -= cos * overlap * 0.5;
ball1.y -= sin * overlap * 0.5;
ball2.x += cos * overlap * 0.5;
ball2.y += sin * overlap * 0.5;
// Exchange velocities (simplified)
var tempVx = ball1.velocityX;
var tempVy = ball1.velocityY;
// Apply original force transfer values
var ball1Force = ball1.type === 'cue' ? 1.0 : 1.0;
var ball2Force = ball2.type === 'cue' ? 1.0 : 1.0;
ball1.velocityX = ball2.velocityX * ball1Force;
ball1.velocityY = ball2.velocityY * ball1Force;
ball2.velocityX = tempVx * ball2Force;
ball2.velocityY = tempVy * ball2Force;
LK.getSound('ballHit').play();
}
}
}
}
// Check pocket collisions
function checkPocketCollisions() {
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted) continue;
for (var j = 0; j < pockets.length; j++) {
var pocket = pockets[j];
var dx = ball.x - pocket.x;
var dy = ball.y - pocket.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60) {
// Ball potted
ball.isPotted = true;
ball.visible = false;
ball.velocityX = 0;
ball.velocityY = 0;
LK.getSound('ballPocket').play();
handleBallPotted(ball);
break;
}
}
}
}
// Handle ball potted logic
function handleBallPotted(ball) {
if (ball.type === 'cue') {
// Scratch - end turn
gameStatusText.setText('Scratch! Turn ends.');
resetCueBall();
} else if (ball.type === 'eight') {
// 8-ball potted
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
if (playerGroup && allGroupBallsPotted(playerGroup)) {
// Win condition
gameStatusText.setText('Player ' + currentPlayer + ' Wins!');
gameWon = true;
LK.setScore(LK.getScore() + 100);
LK.setTimeout(function () {
LK.showYouWin();
}, 2000);
} else {
// 8-ball potted too early - lose
var winner = currentPlayer === 1 ? 2 : 1;
gameStatusText.setText('Player ' + winner + ' Wins! 8-ball potted early.');
gameWon = true;
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
} else {
// Regular ball potted
if (!player1Group && !player2Group) {
// First ball potted determines groups
if (currentPlayer === 1) {
player1Group = ball.type;
player2Group = ball.type === 'solid' ? 'stripe' : 'solid';
} else {
player2Group = ball.type;
player1Group = ball.type === 'solid' ? 'stripe' : 'solid';
}
}
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
if (ball.type === playerGroup) {
// Correct ball potted - continue turn
gameStatusText.setText('Good shot! Continue.');
correctBallPotted = true; // Player gets another shot
} else {
// Wrong ball potted - end turn
gameStatusText.setText('Wrong ball! Turn ends.');
}
}
}
// Check if all group balls are potted
function allGroupBallsPotted(group) {
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.type === group && !ball.isPotted) {
return false;
}
}
return true;
}
// Reset cue ball after scratch
function resetCueBall() {
cueBall.x = 1024 - 400;
cueBall.y = 1366;
cueBall.isPotted = false;
cueBall.visible = true;
cueBall.velocityX = 0;
cueBall.velocityY = 0;
}
// Switch turns
function switchTurn() {
currentPlayer = currentPlayer === 1 ? 2 : 1;
if (currentPlayer === 1) {
playerTurnText.setText('Player Turn');
gameStatusText.setText('Touch and drag from cue ball to aim');
} else {
playerTurnText.setText('AI Turn');
gameStatusText.setText('AI is preparing...');
}
turnEnded = false;
correctBallPotted = false; // Reset flag for new turn
}
// Check if all balls stopped moving
function checkBallsMoving() {
ballsMoving = false;
for (var i = 0; i < balls.length; i++) {
if (balls[i].isMoving()) {
ballsMoving = true;
break;
}
}
if (!ballsMoving && turnEnded) {
// Turn is complete, check if should switch player
if (!correctBallPotted) {
// No correct ball potted, switch to next player
switchTurn();
} else {
// Correct ball was potted, same player continues
turnEnded = false;
correctBallPotted = false; // Reset flag for next shot
}
}
}
// Touch handling
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
game.down = function (x, y, obj) {
if (gameWon || ballsMoving || currentPlayer !== 1) return;
// Check if touching near cue ball
var dx = x - cueBall.x;
var dy = y - cueBall.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 90) {
isDragging = true;
dragStartX = x;
dragStartY = y;
aimingSystem.startAiming(cueBall.x, cueBall.y);
}
};
game.move = function (x, y, obj) {
if (isDragging && !ballsMoving && currentPlayer === 1) {
aimingSystem.updateAiming(x, y);
}
};
game.up = function (x, y, obj) {
if (isDragging && !ballsMoving && currentPlayer === 1) {
var force = aimingSystem.shoot();
cueBall.hit(force.forceX, force.forceY);
isDragging = false;
turnEnded = true;
gameStatusText.setText('Balls in motion...');
}
};
// Initialize game immediately
setupBalls();
aiPlayer = game.addChild(new AIPlayer());
aiPlayer.difficulty = 1.0; // Maximum difficulty - perfect AI
gameStarted = true;
// Main game loop
game.update = function () {
if (!difficultySelected || !gameStarted || gameWon) return;
// Update all balls
for (var i = 0; i < balls.length; i++) {
balls[i].update();
}
// Check collisions
checkBallCollisions();
checkPocketCollisions();
// Check if balls are still moving
checkBallsMoving();
// Trigger AI turn if it's AI's turn and balls are not moving
if (currentPlayer === 2 && !ballsMoving && !turnEnded && !aiPlayer.isThinking) {
aiPlayer.takeTurn();
}
};