User prompt
Change the texts on the blue balls to 1,2,3,4,5,6,7 respectively
User prompt
For blue balls, write numbers from 1 to 7 on the balls.
User prompt
Separate the red and blue balls into groups according to their colors and write 1 to 7 on the red balls and do the same for the blue ones.
User prompt
Make the straight line that shows the direction the ball will go gray and slightly transparent
User prompt
Make the line that shows the direction the ball will go gray and slightly transparent
Code edit (1 edits merged)
Please save this source code
User prompt
Classic Billiards
Initial prompt
I want a game of billiards, let it be a classic billiard
/**** * 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]);
}
}
};