/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Ball class for all balls (cue, solids, stripes, 8) var Ball = Container.expand(function () { var self = Container.call(this); // Ball properties self.radius = 30; // 60px diameter self.vx = 0; self.vy = 0; self.inPlay = true; // false if pocketed self.ballType = 'cue'; // 'cue', 'solid', 'stripe', 'eight' self.number = 0; // 0 for cue, 8 for eight, 1-7 solid, 9-15 stripe // Attach correct asset var assetId = 'cueBall'; if (self.ballType === 'eight') assetId = 'eightBall';else if (self.ballType === 'solid') assetId = 'solidBall';else if (self.ballType === 'stripe') assetId = 'stripeBall'; var ballSprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Add number overlay (except cue ball) if (self.ballType !== 'cue') { var numText = new Text2('' + self.number, { size: 32, fill: 0x000000 }); numText.anchor.set(0.5, 0.5); numText.x = 0; numText.y = 0; self.addChild(numText); self.numText = numText; } // Update asset if type/number changes self.setType = function (type, number) { self.ballType = type; self.number = number; // Remove old children while (self.children.length) self.removeChild(self.children[0]); // Attach new asset var assetId = 'cueBall'; if (type === 'eight') assetId = 'eightBall';else if (type === 'solid') assetId = 'solidBall';else if (type === 'stripe') assetId = 'stripeBall'; ballSprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); if (type !== 'cue') { var numText = new Text2('' + number, { size: 32, fill: 0x000000 }); numText.anchor.set(0.5, 0.5); numText.x = 0; numText.y = 0; self.addChild(numText); self.numText = numText; } }; // Ball physics update self.update = function () { if (!self.inPlay) return; // Move self.x += self.vx; self.y += self.vy; // Friction (easier: balls roll further) self.vx *= 0.992; self.vy *= 0.992; // Stop if very slow (easier: balls keep moving at lower speeds) if (Math.abs(self.vx) < 0.025) self.vx = 0; if (Math.abs(self.vy) < 0.025) self.vy = 0; }; // Pocketed self.pocket = function () { self.inPlay = false; self.visible = false; self.vx = 0; self.vy = 0; }; return self; }); // Cue stick class var CueStick = Container.expand(function () { var self = Container.call(this); var cueSprite = self.attachAsset('cueStick', { anchorX: 0.05, anchorY: 0.5 }); self.visible = false; self.length = cueSprite.width; self.angle = 0; self.power = 0; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x003300 }); /**** * Game Code ****/ // rules button asset // Ball numbers (text overlays) // Cue stick // Balls // Pockets // Table // Table and layout constants var tableW = 1800, tableH = 900, border = 50; var tableX = (2048 - tableW) / 2, tableY = (2732 - tableH) / 2; var pocketR = 52; // easier: larger pockets // Table border var tableBorder = LK.getAsset('tableBorder', { anchorX: 0, anchorY: 0, x: tableX - border, y: tableY - border }); game.addChild(tableBorder); // Table felt var table = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: tableX, y: tableY }); game.addChild(table); // Pockets (6) var pockets = []; var pocketPos = [[tableX, tableY], // TL [tableX + tableW / 2, tableY], // TM [tableX + tableW, tableY], // TR [tableX, tableY + tableH], // BL [tableX + tableW / 2, tableY + tableH], // BM [tableX + tableW, tableY + tableH] // BR ]; for (var i = 0; i < 6; i++) { var p = LK.getAsset('pocket', { anchorX: 0.5, anchorY: 0.5, x: pocketPos[i][0], y: pocketPos[i][1] }); game.addChild(p); pockets.push(p); } // Balls var balls = []; // Helper: create and place a ball function createBall(type, number, x, y) { var b = new Ball(); b.setType(type, number); b.x = x; b.y = y; balls.push(b); game.addChild(b); return b; } // Place cue ball var cueBall = createBall('cue', 0, tableX + tableW * 0.25, tableY + tableH / 2); // Place rack (triangle) - 8 at center, 1 at apex, randomize solids/stripes var rackX = tableX + tableW * 0.75, rackY = tableY + tableH / 2; var rackRows = [[0], [-1, 1], [-2, 0, 2], [-3, -1, 1, 3], [-4, -2, 0, 2, 4]]; var rackBalls = [{ type: 'solid', number: 1 }, { type: 'stripe', number: 9 }, { type: 'solid', number: 2 }, { type: 'stripe', number: 10 }, { type: 'solid', number: 3 }, { type: 'stripe', number: 11 }, { type: 'solid', number: 4 }, { type: 'stripe', number: 12 }, { type: 'solid', number: 5 }, { type: 'stripe', number: 13 }, { type: 'solid', number: 6 }, { type: 'stripe', number: 14 }, { type: 'solid', number: 7 }, { type: 'eight', number: 8 }, { type: 'stripe', number: 15 }]; // Shuffle rackBalls except 8-ball (must be center) function shuffleRack(arr) { for (var i = arr.length - 1; i > 0; i--) { if (arr[i].type === 'eight') continue; var j = Math.floor(Math.random() * (i + 1)); if (arr[j].type === 'eight') continue; var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } shuffleRack(rackBalls); // Place balls in triangle var rackIdx = 0; for (var row = 0; row < rackRows.length; row++) { for (var col = 0; col < rackRows[row].length; col++) { var dx = row * 60 * Math.cos(Math.PI / 6); var dy = rackRows[row][col] * 62; var bx = rackX + dx; var by = rackY + dy; // 8-ball must be center of 3rd row var ballData; if (row === 2 && col === 1) { for (var k = 0; k < rackBalls.length; k++) { if (rackBalls[k].type === 'eight') { ballData = rackBalls[k]; break; } } } else { // Find next non-8-ball for (var k = 0; k < rackBalls.length; k++) { if (rackBalls[k].type !== 'eight' && !rackBalls[k].used) { ballData = rackBalls[k]; rackBalls[k].used = true; break; } } } createBall(ballData.type, ballData.number, bx, by); rackIdx++; } } // Cue stick var cueStick = new CueStick(); game.addChild(cueStick); // Game state var isAiming = false; var aimStart = { x: 0, y: 0 }; var aimEnd = { x: 0, y: 0 }; var shotInProgress = false; var currentPlayer = 1; // 1 or 2 (future: multiplayer) var playerGroup = { 1: null, 2: null }; // 'solid' or 'stripe' var turnFoul = false; var firstContact = null; var ballsPocketedThisTurn = []; var gameOver = false; // GUI: Turn/Status var statusTxt = new Text2('Player 1: Aim & Shoot', { size: 80, fill: 0xFFFFFF }); statusTxt.anchor.set(0.5, 0); LK.gui.top.addChild(statusTxt); // GUI: Player group info at bottom var groupTxt = new Text2('', { size: 48, fill: 0xFFFFFF }); groupTxt.anchor.set(0.5, 1); LK.gui.bottom.addChild(groupTxt); // --- Rules Button and Popup --- // Create a square button at the top right var rulesBtn = new Text2('?', { size: 80, fill: "#fff", align: "center" }); rulesBtn.anchor.set(1, 0); // top right rulesBtn.x = 0; // will be positioned by gui rulesBtn.y = 0; rulesBtn.bg = 0x333333; rulesBtn.width = 120; rulesBtn.height = 120; rulesBtn.interactive = true; rulesBtn.buttonMode = true; LK.gui.topRight.addChild(rulesBtn); // --- To Assets Button --- // Add a 'to assets' button to the top right of the screen var toAssetsBtn = LK.getAsset('toAssetsBtn', { anchorX: 1, anchorY: 0, x: 130, // offset to the left of rulesBtn y: 0 }); toAssetsBtn.interactive = true; toAssetsBtn.buttonMode = true; LK.gui.topRight.addChild(toAssetsBtn); // Rules popup container (hidden by default) var rulesPopup = new Container(); rulesPopup.visible = false; rulesPopup.zIndex = 1000; // ensure on top // Semi-transparent background var popupBg = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732, tint: 0x000000, alpha: 0.7 }); rulesPopup.addChild(popupBg); // Rules text (Turkish and English, toggleable) var rulesTextTurkish = "8 Top Bilardo Kuralları:\n\n" + "1. Oyuncular sırayla ıstaka topunu kullanarak kendi grubundaki (düz veya çizgili) topları cebe sokmaya çalışır.\n" + "2. Tüm toplarını sokan oyuncu, siyah 8 topunu doğru cebe sokarsa oyunu kazanır.\n" + "3. Rakibin topunu veya 8 topunu erken sokmak fauldür.\n" + "4. Faul durumunda rakip serbest atış hakkı kazanır.\n" + "5. Oyun sırası, faul veya top sokulamayan turda değişir.\n\n" + "İyi oyunlar!"; var rulesTextEnglish = "8-Ball Billiards Rules:\n\n" + "1. Players take turns using the cue ball to pocket their group of balls (solids or stripes).\n" + "2. The player who pockets all their balls and then legally pockets the black 8-ball wins the game.\n" + "3. Pocketing the opponent's ball or the 8-ball early is a foul.\n" + "4. After a foul, the opponent gets ball-in-hand.\n" + "5. Turn changes after a foul or if no ball is pocketed.\n\n" + "Good luck!"; var currentRulesLang = "tr"; // "tr" for Turkish, "en" for English var rulesText = new Text2(rulesTextTurkish, { size: 60, fill: "#fff", align: "center", wordWrap: true, wordWrapWidth: 1600 }); rulesText.anchor.set(0.5, 0); rulesText.x = 2048 / 2; rulesText.y = 400; rulesPopup.addChild(rulesText); // English button var englishBtn = new Text2('English', { size: 48, fill: "#fff", align: "center" }); englishBtn.anchor.set(1, 0); englishBtn.x = 2048 / 2 + 800; englishBtn.y = 400 - 80; englishBtn.bg = 0x333333; englishBtn.width = 220; englishBtn.height = 90; englishBtn.interactive = true; englishBtn.buttonMode = true; rulesPopup.addChild(englishBtn); // Close button var closeBtn = new Text2('Kapat', { size: 64, fill: "#fff", align: "center" }); closeBtn.anchor.set(0.5, 0.5); closeBtn.x = 2048 / 2; closeBtn.y = 400 + rulesText.height + 100; closeBtn.bg = 0x333333; closeBtn.width = 400; closeBtn.height = 120; closeBtn.interactive = true; closeBtn.buttonMode = true; rulesPopup.addChild(closeBtn); // Add popup to game (so it overlays everything) game.addChild(rulesPopup); // Button event: show popup // Track if rules popup is open and game is paused var rulesPopupOpen = false; rulesBtn.down = function (x, y, obj) { rulesPopup.visible = true; rulesPopupOpen = true; // Always show Turkish by default currentRulesLang = "tr"; rulesText.setText(rulesTextTurkish); closeBtn.setText("Kapat"); englishBtn.setText("English"); }; // English button event: toggle language englishBtn.down = function (x, y, obj) { if (currentRulesLang === "tr") { currentRulesLang = "en"; rulesText.setText(rulesTextEnglish); closeBtn.setText("Close"); englishBtn.setText("Türkçe"); } else { currentRulesLang = "tr"; rulesText.setText(rulesTextTurkish); closeBtn.setText("Kapat"); englishBtn.setText("English"); } // Adjust closeBtn position in case rulesText height changes closeBtn.y = 400 + rulesText.height + 100; }; // Close event: hide popup closeBtn.down = function (x, y, obj) { rulesPopup.visible = false; rulesPopupOpen = false; // LK.resumeGame(); // Removed, LK handles resume for overlays }; // Helper: update group info text function updateGroupTxt() { // Helper to find the lowest-numbered in-play ball of a group function nextBallColor(group) { if (!group) return 'Unassigned'; var minNum = 100; for (var i = 0; i < balls.length; i++) { if (!balls[i].inPlay) continue; if (balls[i].ballType === group && balls[i].number < minNum) { minNum = balls[i].number; } } if (minNum === 100) { // No balls left, so next is 8-ball return 'Black 8'; } return (group === 'solid' ? 'Red ' : 'Blue ') + minNum; } var p1 = playerGroup[1] ? nextBallColor(playerGroup[1]) : 'Unassigned'; var p2 = playerGroup[2] ? nextBallColor(playerGroup[2]) : 'Unassigned'; groupTxt.setText('Player 1: ' + p1 + ' | Player 2: ' + p2); } updateGroupTxt(); // Helper: check if all balls stopped function allBallsStopped() { for (var i = 0; i < balls.length; i++) { if (!balls[i].inPlay) continue; if (Math.abs(balls[i].vx) > 0.05 || Math.abs(balls[i].vy) > 0.05) return false; } return true; } // Helper: distance between two points function dist(ax, ay, bx, by) { var dx = ax - bx, dy = ay - by; return Math.sqrt(dx * dx + dy * dy); } // Helper: ball-ball collision function resolveBallCollision(b1, b2) { var dx = b2.x - b1.x, dy = b2.y - b1.y; var d = Math.sqrt(dx * dx + dy * dy); if (d === 0 || d > b1.radius * 2) return false; // Overlap var overlap = b1.radius * 2 - d; var nx = dx / d, ny = dy / d; // Separate balls b1.x -= nx * overlap / 2; b1.y -= ny * overlap / 2; b2.x += nx * overlap / 2; b2.y += ny * overlap / 2; // Relative velocity var dvx = b2.vx - b1.vx, dvy = b2.vy - b1.vy; var dot = dvx * nx + dvy * ny; if (dot > 0) return false; // Elastic collision var impulse = dot; b1.vx += nx * impulse; b1.vy += ny * impulse; b2.vx -= nx * impulse; b2.vy -= ny * impulse; // Play collision sound LK.getSound('ballHit').play(); return true; } // Helper: ball-table collision function resolveTableCollision(ball) { if (!ball.inPlay) return; var minX = tableX + ball.radius, maxX = tableX + tableW - ball.radius; var minY = tableY + ball.radius, maxY = tableY + tableH - ball.radius; if (ball.x < minX) { ball.x = minX; ball.vx = -ball.vx * 0.9; } if (ball.x > maxX) { ball.x = maxX; ball.vx = -ball.vx * 0.9; } if (ball.y < minY) { ball.y = minY; ball.vy = -ball.vy * 0.9; } if (ball.y > maxY) { ball.y = maxY; ball.vy = -ball.vy * 0.9; } } // Helper: ball-pocket collision function checkPocket(ball) { if (!ball.inPlay) return false; for (var i = 0; i < pockets.length; i++) { var px = pockets[i].x, py = pockets[i].y; if (dist(ball.x, ball.y, px, py) < pocketR) { ball.pocket(); return true; } } return false; } // Helper: assign group after first legal pocket function assignGroup(player, type) { if (playerGroup[1] === null && playerGroup[2] === null && (type === 'solid' || type === 'stripe')) { playerGroup[player] = type; playerGroup[player === 1 ? 2 : 1] = type === 'solid' ? 'stripe' : 'solid'; } } // Helper: check win/lose function checkGameEnd() { // If 8-ball pocketed var eight = null; for (var i = 0; i < balls.length; i++) { if (balls[i].ballType === 'eight') eight = balls[i]; } if (!eight.inPlay) { // Did player clear all their group? var groupType = playerGroup[currentPlayer]; var groupCleared = true; for (var i = 0; i < balls.length; i++) { if (balls[i].ballType === groupType && balls[i].inPlay) groupCleared = false; } if (groupCleared && !turnFoul) { statusTxt.setText('Player ' + currentPlayer + ' wins!'); LK.showYouWin(); } else { statusTxt.setText('Player ' + currentPlayer + ' loses!'); LK.showGameOver(); } gameOver = true; } } // Helper: reset cue ball after scratch function resetCueBall() { cueBall.x = tableX + tableW * 0.25; cueBall.y = tableY + tableH / 2; cueBall.vx = 0; cueBall.vy = 0; cueBall.inPlay = true; cueBall.visible = true; } // Touch controls game.down = function (x, y, obj) { if (rulesPopupOpen) return; if (gameOver) return; if (!allBallsStopped()) return; // Only allow aiming if touch is on table and cue ball is in play if (!cueBall.inPlay) return; if (x < tableX || x > tableX + tableW || y < tableY || y > tableY + tableH) return; // Only allow aiming if touch is not on a ball (except cue ball) for (var i = 0; i < balls.length; i++) { if (balls[i] !== cueBall && balls[i].inPlay && dist(x, y, balls[i].x, balls[i].y) < balls[i].radius) return; } isAiming = true; aimStart.x = x; aimStart.y = y; aimEnd.x = x; aimEnd.y = y; cueStick.visible = true; }; game.move = function (x, y, obj) { if (rulesPopupOpen) return; if (!isAiming) return; aimEnd.x = x; aimEnd.y = y; // Update cue stick position/angle var dx = cueBall.x - aimEnd.x; var dy = cueBall.y - aimEnd.y; var angle = Math.atan2(dy, dx); cueStick.x = cueBall.x; cueStick.y = cueBall.y; cueStick.rotation = angle; // Power: distance from cue ball to drag point, max 300 var power = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 300); cueStick.scale.x = 1 + power / 600; cueStick.power = power; }; game.up = function (x, y, obj) { if (rulesPopupOpen) return; if (!isAiming) return; isAiming = false; cueStick.visible = false; if (gameOver) return; if (!allBallsStopped()) return; // Calculate shot var dx = cueBall.x - aimEnd.x; var dy = cueBall.y - aimEnd.y; var power = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 300); if (power < 10) return; // too weak var angle = Math.atan2(dy, dx); // Apply velocity to cue ball (easier: more power) cueBall.vx = Math.cos(angle) * (power / 8.5); cueBall.vy = Math.sin(angle) * (power / 8.5); shotInProgress = true; firstContact = null; ballsPocketedThisTurn = []; turnFoul = false; }; // Main update loop game.update = function () { if (gameOver) return; // Physics: update balls for (var i = 0; i < balls.length; i++) { balls[i].update(); } // Ball-ball collisions for (var i = 0; i < balls.length; i++) { for (var j = i + 1; j < balls.length; j++) { if (!balls[i].inPlay || !balls[j].inPlay) continue; var collided = resolveBallCollision(balls[i], balls[j]); // First contact detection (cue ball) if (shotInProgress && !firstContact) { if (balls[i] === cueBall && balls[j].inPlay && balls[j].ballType !== 'cue' || balls[j] === cueBall && balls[i].inPlay && balls[i].ballType !== 'cue') { firstContact = balls[i] === cueBall ? balls[j] : balls[i]; } } } } // Ball-table collisions for (var i = 0; i < balls.length; i++) { resolveTableCollision(balls[i]); } // Ball-pocket collisions for (var i = 0; i < balls.length; i++) { if (!balls[i].inPlay) continue; if (checkPocket(balls[i])) { ballsPocketedThisTurn.push(balls[i]); // If cue ball pocketed if (balls[i] === cueBall) { turnFoul = true; } } } // If shot in progress, check if all balls stopped if (shotInProgress && allBallsStopped()) { shotInProgress = false; // Rules: assign group if not yet assigned for (var i = 0; i < ballsPocketedThisTurn.length; i++) { var b = ballsPocketedThisTurn[i]; if (playerGroup[currentPlayer] === null && (b.ballType === 'solid' || b.ballType === 'stripe')) { assignGroup(currentPlayer, b.ballType); updateGroupTxt(); } } // Foul: cue ball pocketed or no group ball hit first if (turnFoul || playerGroup[currentPlayer] && (!firstContact || firstContact.ballType !== playerGroup[currentPlayer])) { turnFoul = true; } // End turn if foul or no group ball pocketed var groupBallPocketed = false; for (var i = 0; i < ballsPocketedThisTurn.length; i++) { if (playerGroup[currentPlayer] && ballsPocketedThisTurn[i].ballType === playerGroup[currentPlayer]) { groupBallPocketed = true; } } // 8-ball pocketed: check win/lose checkGameEnd(); if (gameOver) return; // Foul: give ball in hand (reset cue ball) if (turnFoul) { resetCueBall(); statusTxt.setText('Foul! Player ' + (currentPlayer === 1 ? 2 : 1) + "'s turn"); currentPlayer = currentPlayer === 1 ? 2 : 1; updateGroupTxt(); } else if (!groupBallPocketed && playerGroup[currentPlayer]) { // No group ball pocketed: switch turn currentPlayer = currentPlayer === 1 ? 2 : 1; statusTxt.setText('Player ' + currentPlayer + "'s turn"); updateGroupTxt(); } else { // Continue turn statusTxt.setText('Player ' + currentPlayer + ': Aim & Shoot'); } ballsPocketedThisTurn = []; firstContact = null; turnFoul = false; } // Update cue stick position if aiming if (isAiming) { var dx = cueBall.x - aimEnd.x; var dy = cueBall.y - aimEnd.y; var angle = Math.atan2(dy, dx); cueStick.x = cueBall.x; cueStick.y = cueBall.y; cueStick.rotation = angle; var power = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 300); cueStick.scale.x = 1 + power / 600; cueStick.power = power; // Draw aiming line (gray, slightly transparent) if (!game.aimLine) { game.aimLine = new Container(); game.addChild(game.aimLine); } var aimLine = game.aimLine; while (aimLine.children.length) aimLine.removeChild(aimLine.children[0]); var lineLen = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 400); var steps = Math.floor(lineLen / 16); for (var i = 1; i <= steps; i++) { var px = cueBall.x - dx / lineLen * (i * 16); var py = cueBall.y - dy / lineLen * (i * 16); var dot = LK.getAsset('cueBall', { anchorX: 0.5, anchorY: 0.5, x: px, y: py, scaleX: 0.18, scaleY: 0.18, tint: 0x888888, // gray alpha: 0.38 // slightly transparent }); aimLine.addChild(dot); } } else { if (game.aimLine) { while (game.aimLine.children.length) game.aimLine.removeChild(game.aimLine.children[0]); } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class for all balls (cue, solids, stripes, 8)
var Ball = Container.expand(function () {
var self = Container.call(this);
// Ball properties
self.radius = 30; // 60px diameter
self.vx = 0;
self.vy = 0;
self.inPlay = true; // false if pocketed
self.ballType = 'cue'; // 'cue', 'solid', 'stripe', 'eight'
self.number = 0; // 0 for cue, 8 for eight, 1-7 solid, 9-15 stripe
// Attach correct asset
var assetId = 'cueBall';
if (self.ballType === 'eight') assetId = 'eightBall';else if (self.ballType === 'solid') assetId = 'solidBall';else if (self.ballType === 'stripe') assetId = 'stripeBall';
var ballSprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Add number overlay (except cue ball)
if (self.ballType !== 'cue') {
var numText = new Text2('' + self.number, {
size: 32,
fill: 0x000000
});
numText.anchor.set(0.5, 0.5);
numText.x = 0;
numText.y = 0;
self.addChild(numText);
self.numText = numText;
}
// Update asset if type/number changes
self.setType = function (type, number) {
self.ballType = type;
self.number = number;
// Remove old children
while (self.children.length) self.removeChild(self.children[0]);
// Attach new asset
var assetId = 'cueBall';
if (type === 'eight') assetId = 'eightBall';else if (type === 'solid') assetId = 'solidBall';else if (type === 'stripe') assetId = 'stripeBall';
ballSprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
if (type !== 'cue') {
var numText = new Text2('' + number, {
size: 32,
fill: 0x000000
});
numText.anchor.set(0.5, 0.5);
numText.x = 0;
numText.y = 0;
self.addChild(numText);
self.numText = numText;
}
};
// Ball physics update
self.update = function () {
if (!self.inPlay) return;
// Move
self.x += self.vx;
self.y += self.vy;
// Friction (easier: balls roll further)
self.vx *= 0.992;
self.vy *= 0.992;
// Stop if very slow (easier: balls keep moving at lower speeds)
if (Math.abs(self.vx) < 0.025) self.vx = 0;
if (Math.abs(self.vy) < 0.025) self.vy = 0;
};
// Pocketed
self.pocket = function () {
self.inPlay = false;
self.visible = false;
self.vx = 0;
self.vy = 0;
};
return self;
});
// Cue stick class
var CueStick = Container.expand(function () {
var self = Container.call(this);
var cueSprite = self.attachAsset('cueStick', {
anchorX: 0.05,
anchorY: 0.5
});
self.visible = false;
self.length = cueSprite.width;
self.angle = 0;
self.power = 0;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x003300
});
/****
* Game Code
****/
// rules button asset
// Ball numbers (text overlays)
// Cue stick
// Balls
// Pockets
// Table
// Table and layout constants
var tableW = 1800,
tableH = 900,
border = 50;
var tableX = (2048 - tableW) / 2,
tableY = (2732 - tableH) / 2;
var pocketR = 52; // easier: larger pockets
// Table border
var tableBorder = LK.getAsset('tableBorder', {
anchorX: 0,
anchorY: 0,
x: tableX - border,
y: tableY - border
});
game.addChild(tableBorder);
// Table felt
var table = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: tableX,
y: tableY
});
game.addChild(table);
// Pockets (6)
var pockets = [];
var pocketPos = [[tableX, tableY],
// TL
[tableX + tableW / 2, tableY],
// TM
[tableX + tableW, tableY],
// TR
[tableX, tableY + tableH],
// BL
[tableX + tableW / 2, tableY + tableH],
// BM
[tableX + tableW, tableY + tableH] // BR
];
for (var i = 0; i < 6; i++) {
var p = LK.getAsset('pocket', {
anchorX: 0.5,
anchorY: 0.5,
x: pocketPos[i][0],
y: pocketPos[i][1]
});
game.addChild(p);
pockets.push(p);
}
// Balls
var balls = [];
// Helper: create and place a ball
function createBall(type, number, x, y) {
var b = new Ball();
b.setType(type, number);
b.x = x;
b.y = y;
balls.push(b);
game.addChild(b);
return b;
}
// Place cue ball
var cueBall = createBall('cue', 0, tableX + tableW * 0.25, tableY + tableH / 2);
// Place rack (triangle) - 8 at center, 1 at apex, randomize solids/stripes
var rackX = tableX + tableW * 0.75,
rackY = tableY + tableH / 2;
var rackRows = [[0], [-1, 1], [-2, 0, 2], [-3, -1, 1, 3], [-4, -2, 0, 2, 4]];
var rackBalls = [{
type: 'solid',
number: 1
}, {
type: 'stripe',
number: 9
}, {
type: 'solid',
number: 2
}, {
type: 'stripe',
number: 10
}, {
type: 'solid',
number: 3
}, {
type: 'stripe',
number: 11
}, {
type: 'solid',
number: 4
}, {
type: 'stripe',
number: 12
}, {
type: 'solid',
number: 5
}, {
type: 'stripe',
number: 13
}, {
type: 'solid',
number: 6
}, {
type: 'stripe',
number: 14
}, {
type: 'solid',
number: 7
}, {
type: 'eight',
number: 8
}, {
type: 'stripe',
number: 15
}];
// Shuffle rackBalls except 8-ball (must be center)
function shuffleRack(arr) {
for (var i = arr.length - 1; i > 0; i--) {
if (arr[i].type === 'eight') continue;
var j = Math.floor(Math.random() * (i + 1));
if (arr[j].type === 'eight') continue;
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
shuffleRack(rackBalls);
// Place balls in triangle
var rackIdx = 0;
for (var row = 0; row < rackRows.length; row++) {
for (var col = 0; col < rackRows[row].length; col++) {
var dx = row * 60 * Math.cos(Math.PI / 6);
var dy = rackRows[row][col] * 62;
var bx = rackX + dx;
var by = rackY + dy;
// 8-ball must be center of 3rd row
var ballData;
if (row === 2 && col === 1) {
for (var k = 0; k < rackBalls.length; k++) {
if (rackBalls[k].type === 'eight') {
ballData = rackBalls[k];
break;
}
}
} else {
// Find next non-8-ball
for (var k = 0; k < rackBalls.length; k++) {
if (rackBalls[k].type !== 'eight' && !rackBalls[k].used) {
ballData = rackBalls[k];
rackBalls[k].used = true;
break;
}
}
}
createBall(ballData.type, ballData.number, bx, by);
rackIdx++;
}
}
// Cue stick
var cueStick = new CueStick();
game.addChild(cueStick);
// Game state
var isAiming = false;
var aimStart = {
x: 0,
y: 0
};
var aimEnd = {
x: 0,
y: 0
};
var shotInProgress = false;
var currentPlayer = 1; // 1 or 2 (future: multiplayer)
var playerGroup = {
1: null,
2: null
}; // 'solid' or 'stripe'
var turnFoul = false;
var firstContact = null;
var ballsPocketedThisTurn = [];
var gameOver = false;
// GUI: Turn/Status
var statusTxt = new Text2('Player 1: Aim & Shoot', {
size: 80,
fill: 0xFFFFFF
});
statusTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(statusTxt);
// GUI: Player group info at bottom
var groupTxt = new Text2('', {
size: 48,
fill: 0xFFFFFF
});
groupTxt.anchor.set(0.5, 1);
LK.gui.bottom.addChild(groupTxt);
// --- Rules Button and Popup ---
// Create a square button at the top right
var rulesBtn = new Text2('?', {
size: 80,
fill: "#fff",
align: "center"
});
rulesBtn.anchor.set(1, 0); // top right
rulesBtn.x = 0; // will be positioned by gui
rulesBtn.y = 0;
rulesBtn.bg = 0x333333;
rulesBtn.width = 120;
rulesBtn.height = 120;
rulesBtn.interactive = true;
rulesBtn.buttonMode = true;
LK.gui.topRight.addChild(rulesBtn);
// --- To Assets Button ---
// Add a 'to assets' button to the top right of the screen
var toAssetsBtn = LK.getAsset('toAssetsBtn', {
anchorX: 1,
anchorY: 0,
x: 130,
// offset to the left of rulesBtn
y: 0
});
toAssetsBtn.interactive = true;
toAssetsBtn.buttonMode = true;
LK.gui.topRight.addChild(toAssetsBtn);
// Rules popup container (hidden by default)
var rulesPopup = new Container();
rulesPopup.visible = false;
rulesPopup.zIndex = 1000; // ensure on top
// Semi-transparent background
var popupBg = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732,
tint: 0x000000,
alpha: 0.7
});
rulesPopup.addChild(popupBg);
// Rules text (Turkish and English, toggleable)
var rulesTextTurkish = "8 Top Bilardo Kuralları:\n\n" + "1. Oyuncular sırayla ıstaka topunu kullanarak kendi grubundaki (düz veya çizgili) topları cebe sokmaya çalışır.\n" + "2. Tüm toplarını sokan oyuncu, siyah 8 topunu doğru cebe sokarsa oyunu kazanır.\n" + "3. Rakibin topunu veya 8 topunu erken sokmak fauldür.\n" + "4. Faul durumunda rakip serbest atış hakkı kazanır.\n" + "5. Oyun sırası, faul veya top sokulamayan turda değişir.\n\n" + "İyi oyunlar!";
var rulesTextEnglish = "8-Ball Billiards Rules:\n\n" + "1. Players take turns using the cue ball to pocket their group of balls (solids or stripes).\n" + "2. The player who pockets all their balls and then legally pockets the black 8-ball wins the game.\n" + "3. Pocketing the opponent's ball or the 8-ball early is a foul.\n" + "4. After a foul, the opponent gets ball-in-hand.\n" + "5. Turn changes after a foul or if no ball is pocketed.\n\n" + "Good luck!";
var currentRulesLang = "tr"; // "tr" for Turkish, "en" for English
var rulesText = new Text2(rulesTextTurkish, {
size: 60,
fill: "#fff",
align: "center",
wordWrap: true,
wordWrapWidth: 1600
});
rulesText.anchor.set(0.5, 0);
rulesText.x = 2048 / 2;
rulesText.y = 400;
rulesPopup.addChild(rulesText);
// English button
var englishBtn = new Text2('English', {
size: 48,
fill: "#fff",
align: "center"
});
englishBtn.anchor.set(1, 0);
englishBtn.x = 2048 / 2 + 800;
englishBtn.y = 400 - 80;
englishBtn.bg = 0x333333;
englishBtn.width = 220;
englishBtn.height = 90;
englishBtn.interactive = true;
englishBtn.buttonMode = true;
rulesPopup.addChild(englishBtn);
// Close button
var closeBtn = new Text2('Kapat', {
size: 64,
fill: "#fff",
align: "center"
});
closeBtn.anchor.set(0.5, 0.5);
closeBtn.x = 2048 / 2;
closeBtn.y = 400 + rulesText.height + 100;
closeBtn.bg = 0x333333;
closeBtn.width = 400;
closeBtn.height = 120;
closeBtn.interactive = true;
closeBtn.buttonMode = true;
rulesPopup.addChild(closeBtn);
// Add popup to game (so it overlays everything)
game.addChild(rulesPopup);
// Button event: show popup
// Track if rules popup is open and game is paused
var rulesPopupOpen = false;
rulesBtn.down = function (x, y, obj) {
rulesPopup.visible = true;
rulesPopupOpen = true;
// Always show Turkish by default
currentRulesLang = "tr";
rulesText.setText(rulesTextTurkish);
closeBtn.setText("Kapat");
englishBtn.setText("English");
};
// English button event: toggle language
englishBtn.down = function (x, y, obj) {
if (currentRulesLang === "tr") {
currentRulesLang = "en";
rulesText.setText(rulesTextEnglish);
closeBtn.setText("Close");
englishBtn.setText("Türkçe");
} else {
currentRulesLang = "tr";
rulesText.setText(rulesTextTurkish);
closeBtn.setText("Kapat");
englishBtn.setText("English");
}
// Adjust closeBtn position in case rulesText height changes
closeBtn.y = 400 + rulesText.height + 100;
};
// Close event: hide popup
closeBtn.down = function (x, y, obj) {
rulesPopup.visible = false;
rulesPopupOpen = false;
// LK.resumeGame(); // Removed, LK handles resume for overlays
};
// Helper: update group info text
function updateGroupTxt() {
// Helper to find the lowest-numbered in-play ball of a group
function nextBallColor(group) {
if (!group) return 'Unassigned';
var minNum = 100;
for (var i = 0; i < balls.length; i++) {
if (!balls[i].inPlay) continue;
if (balls[i].ballType === group && balls[i].number < minNum) {
minNum = balls[i].number;
}
}
if (minNum === 100) {
// No balls left, so next is 8-ball
return 'Black 8';
}
return (group === 'solid' ? 'Red ' : 'Blue ') + minNum;
}
var p1 = playerGroup[1] ? nextBallColor(playerGroup[1]) : 'Unassigned';
var p2 = playerGroup[2] ? nextBallColor(playerGroup[2]) : 'Unassigned';
groupTxt.setText('Player 1: ' + p1 + ' | Player 2: ' + p2);
}
updateGroupTxt();
// Helper: check if all balls stopped
function allBallsStopped() {
for (var i = 0; i < balls.length; i++) {
if (!balls[i].inPlay) continue;
if (Math.abs(balls[i].vx) > 0.05 || Math.abs(balls[i].vy) > 0.05) return false;
}
return true;
}
// Helper: distance between two points
function dist(ax, ay, bx, by) {
var dx = ax - bx,
dy = ay - by;
return Math.sqrt(dx * dx + dy * dy);
}
// Helper: ball-ball collision
function resolveBallCollision(b1, b2) {
var dx = b2.x - b1.x,
dy = b2.y - b1.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d === 0 || d > b1.radius * 2) return false;
// Overlap
var overlap = b1.radius * 2 - d;
var nx = dx / d,
ny = dy / d;
// Separate balls
b1.x -= nx * overlap / 2;
b1.y -= ny * overlap / 2;
b2.x += nx * overlap / 2;
b2.y += ny * overlap / 2;
// Relative velocity
var dvx = b2.vx - b1.vx,
dvy = b2.vy - b1.vy;
var dot = dvx * nx + dvy * ny;
if (dot > 0) return false;
// Elastic collision
var impulse = dot;
b1.vx += nx * impulse;
b1.vy += ny * impulse;
b2.vx -= nx * impulse;
b2.vy -= ny * impulse;
// Play collision sound
LK.getSound('ballHit').play();
return true;
}
// Helper: ball-table collision
function resolveTableCollision(ball) {
if (!ball.inPlay) return;
var minX = tableX + ball.radius,
maxX = tableX + tableW - ball.radius;
var minY = tableY + ball.radius,
maxY = tableY + tableH - ball.radius;
if (ball.x < minX) {
ball.x = minX;
ball.vx = -ball.vx * 0.9;
}
if (ball.x > maxX) {
ball.x = maxX;
ball.vx = -ball.vx * 0.9;
}
if (ball.y < minY) {
ball.y = minY;
ball.vy = -ball.vy * 0.9;
}
if (ball.y > maxY) {
ball.y = maxY;
ball.vy = -ball.vy * 0.9;
}
}
// Helper: ball-pocket collision
function checkPocket(ball) {
if (!ball.inPlay) return false;
for (var i = 0; i < pockets.length; i++) {
var px = pockets[i].x,
py = pockets[i].y;
if (dist(ball.x, ball.y, px, py) < pocketR) {
ball.pocket();
return true;
}
}
return false;
}
// Helper: assign group after first legal pocket
function assignGroup(player, type) {
if (playerGroup[1] === null && playerGroup[2] === null && (type === 'solid' || type === 'stripe')) {
playerGroup[player] = type;
playerGroup[player === 1 ? 2 : 1] = type === 'solid' ? 'stripe' : 'solid';
}
}
// Helper: check win/lose
function checkGameEnd() {
// If 8-ball pocketed
var eight = null;
for (var i = 0; i < balls.length; i++) {
if (balls[i].ballType === 'eight') eight = balls[i];
}
if (!eight.inPlay) {
// Did player clear all their group?
var groupType = playerGroup[currentPlayer];
var groupCleared = true;
for (var i = 0; i < balls.length; i++) {
if (balls[i].ballType === groupType && balls[i].inPlay) groupCleared = false;
}
if (groupCleared && !turnFoul) {
statusTxt.setText('Player ' + currentPlayer + ' wins!');
LK.showYouWin();
} else {
statusTxt.setText('Player ' + currentPlayer + ' loses!');
LK.showGameOver();
}
gameOver = true;
}
}
// Helper: reset cue ball after scratch
function resetCueBall() {
cueBall.x = tableX + tableW * 0.25;
cueBall.y = tableY + tableH / 2;
cueBall.vx = 0;
cueBall.vy = 0;
cueBall.inPlay = true;
cueBall.visible = true;
}
// Touch controls
game.down = function (x, y, obj) {
if (rulesPopupOpen) return;
if (gameOver) return;
if (!allBallsStopped()) return;
// Only allow aiming if touch is on table and cue ball is in play
if (!cueBall.inPlay) return;
if (x < tableX || x > tableX + tableW || y < tableY || y > tableY + tableH) return;
// Only allow aiming if touch is not on a ball (except cue ball)
for (var i = 0; i < balls.length; i++) {
if (balls[i] !== cueBall && balls[i].inPlay && dist(x, y, balls[i].x, balls[i].y) < balls[i].radius) return;
}
isAiming = true;
aimStart.x = x;
aimStart.y = y;
aimEnd.x = x;
aimEnd.y = y;
cueStick.visible = true;
};
game.move = function (x, y, obj) {
if (rulesPopupOpen) return;
if (!isAiming) return;
aimEnd.x = x;
aimEnd.y = y;
// Update cue stick position/angle
var dx = cueBall.x - aimEnd.x;
var dy = cueBall.y - aimEnd.y;
var angle = Math.atan2(dy, dx);
cueStick.x = cueBall.x;
cueStick.y = cueBall.y;
cueStick.rotation = angle;
// Power: distance from cue ball to drag point, max 300
var power = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 300);
cueStick.scale.x = 1 + power / 600;
cueStick.power = power;
};
game.up = function (x, y, obj) {
if (rulesPopupOpen) return;
if (!isAiming) return;
isAiming = false;
cueStick.visible = false;
if (gameOver) return;
if (!allBallsStopped()) return;
// Calculate shot
var dx = cueBall.x - aimEnd.x;
var dy = cueBall.y - aimEnd.y;
var power = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 300);
if (power < 10) return; // too weak
var angle = Math.atan2(dy, dx);
// Apply velocity to cue ball (easier: more power)
cueBall.vx = Math.cos(angle) * (power / 8.5);
cueBall.vy = Math.sin(angle) * (power / 8.5);
shotInProgress = true;
firstContact = null;
ballsPocketedThisTurn = [];
turnFoul = false;
};
// Main update loop
game.update = function () {
if (gameOver) return;
// Physics: update balls
for (var i = 0; i < balls.length; i++) {
balls[i].update();
}
// Ball-ball collisions
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
if (!balls[i].inPlay || !balls[j].inPlay) continue;
var collided = resolveBallCollision(balls[i], balls[j]);
// First contact detection (cue ball)
if (shotInProgress && !firstContact) {
if (balls[i] === cueBall && balls[j].inPlay && balls[j].ballType !== 'cue' || balls[j] === cueBall && balls[i].inPlay && balls[i].ballType !== 'cue') {
firstContact = balls[i] === cueBall ? balls[j] : balls[i];
}
}
}
}
// Ball-table collisions
for (var i = 0; i < balls.length; i++) {
resolveTableCollision(balls[i]);
}
// Ball-pocket collisions
for (var i = 0; i < balls.length; i++) {
if (!balls[i].inPlay) continue;
if (checkPocket(balls[i])) {
ballsPocketedThisTurn.push(balls[i]);
// If cue ball pocketed
if (balls[i] === cueBall) {
turnFoul = true;
}
}
}
// If shot in progress, check if all balls stopped
if (shotInProgress && allBallsStopped()) {
shotInProgress = false;
// Rules: assign group if not yet assigned
for (var i = 0; i < ballsPocketedThisTurn.length; i++) {
var b = ballsPocketedThisTurn[i];
if (playerGroup[currentPlayer] === null && (b.ballType === 'solid' || b.ballType === 'stripe')) {
assignGroup(currentPlayer, b.ballType);
updateGroupTxt();
}
}
// Foul: cue ball pocketed or no group ball hit first
if (turnFoul || playerGroup[currentPlayer] && (!firstContact || firstContact.ballType !== playerGroup[currentPlayer])) {
turnFoul = true;
}
// End turn if foul or no group ball pocketed
var groupBallPocketed = false;
for (var i = 0; i < ballsPocketedThisTurn.length; i++) {
if (playerGroup[currentPlayer] && ballsPocketedThisTurn[i].ballType === playerGroup[currentPlayer]) {
groupBallPocketed = true;
}
}
// 8-ball pocketed: check win/lose
checkGameEnd();
if (gameOver) return;
// Foul: give ball in hand (reset cue ball)
if (turnFoul) {
resetCueBall();
statusTxt.setText('Foul! Player ' + (currentPlayer === 1 ? 2 : 1) + "'s turn");
currentPlayer = currentPlayer === 1 ? 2 : 1;
updateGroupTxt();
} else if (!groupBallPocketed && playerGroup[currentPlayer]) {
// No group ball pocketed: switch turn
currentPlayer = currentPlayer === 1 ? 2 : 1;
statusTxt.setText('Player ' + currentPlayer + "'s turn");
updateGroupTxt();
} else {
// Continue turn
statusTxt.setText('Player ' + currentPlayer + ': Aim & Shoot');
}
ballsPocketedThisTurn = [];
firstContact = null;
turnFoul = false;
}
// Update cue stick position if aiming
if (isAiming) {
var dx = cueBall.x - aimEnd.x;
var dy = cueBall.y - aimEnd.y;
var angle = Math.atan2(dy, dx);
cueStick.x = cueBall.x;
cueStick.y = cueBall.y;
cueStick.rotation = angle;
var power = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 300);
cueStick.scale.x = 1 + power / 600;
cueStick.power = power;
// Draw aiming line (gray, slightly transparent)
if (!game.aimLine) {
game.aimLine = new Container();
game.addChild(game.aimLine);
}
var aimLine = game.aimLine;
while (aimLine.children.length) aimLine.removeChild(aimLine.children[0]);
var lineLen = Math.min(dist(cueBall.x, cueBall.y, aimEnd.x, aimEnd.y), 400);
var steps = Math.floor(lineLen / 16);
for (var i = 1; i <= steps; i++) {
var px = cueBall.x - dx / lineLen * (i * 16);
var py = cueBall.y - dy / lineLen * (i * 16);
var dot = LK.getAsset('cueBall', {
anchorX: 0.5,
anchorY: 0.5,
x: px,
y: py,
scaleX: 0.18,
scaleY: 0.18,
tint: 0x888888,
// gray
alpha: 0.38 // slightly transparent
});
aimLine.addChild(dot);
}
} else {
if (game.aimLine) {
while (game.aimLine.children.length) game.aimLine.removeChild(game.aimLine.children[0]);
}
}
};