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') {
// Blue balls: solids 2,4,6 (numbers 2,4,6), stripes 10,12,14 (numbers 10,12,14)
var blueNumbers = [2, 4, 6, 10, 12, 14];
var textColor = blueNumbers.indexOf(self.number) !== -1 ? 0x3333ff : 0x000000;
var numText = new Text2('' + self.number, {
size: 32,
fill: textColor
});
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') {
// Blue balls: solids 2,4,6 (numbers 2,4,6), stripes 10,12,14 (numbers 10,12,14)
var blueNumbers = [2, 4, 6, 10, 12, 14];
var textColor = blueNumbers.indexOf(number) !== -1 ? 0x3333ff : 0x000000;
var numText = new Text2('' + number, {
size: 32,
fill: textColor
});
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
self.vx *= 0.985;
self.vy *= 0.985;
// Stop if very slow
if (Math.abs(self.vx) < 0.05) self.vx = 0;
if (Math.abs(self.vy) < 0.05) 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
****/
// Table and layout constants
// Table
// Pockets
// Balls
// Cue stick
// Ball numbers (text overlays)
var tableW = 1800,
tableH = 900,
border = 50;
var tableX = (2048 - tableW) / 2,
tableY = (2732 - tableH) / 2;
var pocketR = 40;
// 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);
// 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;
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 (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 (!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 (!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
cueBall.vx = Math.cos(angle) * (power / 10);
cueBall.vy = Math.sin(angle) * (power / 10);
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);
}
}
// 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;
} else if (!groupBallPocketed && playerGroup[currentPlayer]) {
// No group ball pocketed: switch turn
currentPlayer = currentPlayer === 1 ? 2 : 1;
statusTxt.setText('Player ' + currentPlayer + "'s turn");
} 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]);
}
}
}; ===================================================================
--- original.js
+++ change.js
@@ -24,11 +24,14 @@
anchorY: 0.5
});
// Add number overlay (except cue ball)
if (self.ballType !== 'cue') {
+ // Blue balls: solids 2,4,6 (numbers 2,4,6), stripes 10,12,14 (numbers 10,12,14)
+ var blueNumbers = [2, 4, 6, 10, 12, 14];
+ var textColor = blueNumbers.indexOf(self.number) !== -1 ? 0x3333ff : 0x000000;
var numText = new Text2('' + self.number, {
size: 32,
- fill: 0x000000
+ fill: textColor
});
numText.anchor.set(0.5, 0.5);
numText.x = 0;
numText.y = 0;
@@ -48,11 +51,14 @@
anchorX: 0.5,
anchorY: 0.5
});
if (type !== 'cue') {
+ // Blue balls: solids 2,4,6 (numbers 2,4,6), stripes 10,12,14 (numbers 10,12,14)
+ var blueNumbers = [2, 4, 6, 10, 12, 14];
+ var textColor = blueNumbers.indexOf(number) !== -1 ? 0x3333ff : 0x000000;
var numText = new Text2('' + number, {
size: 32,
- fill: 0x000000
+ fill: textColor
});
numText.anchor.set(0.5, 0.5);
numText.x = 0;
numText.y = 0;
@@ -105,16 +111,14 @@
/****
* Game Code
****/
-// Blue for stripes 9-15
-// Red for solids 1-7
-// Ball numbers (text overlays)
-// Cue stick
-// Balls
-// Pockets
-// Table
// Table and layout constants
+// Table
+// Pockets
+// Balls
+// Cue stick
+// Ball numbers (text overlays)
var tableW = 1800,
tableH = 900,
border = 50;
var tableX = (2048 - tableW) / 2,
@@ -165,12 +169,8 @@
// Helper: create and place a ball
function createBall(type, number, x, y) {
var b = new Ball();
b.setType(type, number);
- // If blue ball (stripe) and number is 1-7, override number text to 1-7
- if (type === 'stripe' && number >= 1 && number <= 7 && b.numText) {
- b.numText.setText('' + number);
- }
b.x = x;
b.y = y;
balls.push(b);
game.addChild(b);
@@ -184,80 +184,51 @@
var rackRows = [[0], [-1, 1], [-2, 0, 2], [-3, -1, 1, 3], [-4, -2, 0, 2, 4]];
var rackBalls = [{
type: 'solid',
number: 1
-},
-// Red 1
-{
+}, {
+ type: 'stripe',
+ number: 9
+}, {
type: 'solid',
number: 2
-},
-// Red 2
-{
+}, {
+ type: 'stripe',
+ number: 10
+}, {
type: 'solid',
number: 3
-},
-// Red 3
-{
+}, {
+ type: 'stripe',
+ number: 11
+}, {
type: 'solid',
number: 4
-},
-// Red 4
-{
+}, {
+ type: 'stripe',
+ number: 12
+}, {
type: 'solid',
number: 5
-},
-// Red 5
-{
+}, {
+ type: 'stripe',
+ number: 13
+}, {
type: 'solid',
number: 6
-},
-// Red 6
-{
+}, {
+ type: 'stripe',
+ number: 14
+}, {
type: 'solid',
number: 7
-},
-// Red 7
-{
+}, {
type: 'eight',
number: 8
-},
-// Black 8
-{
+}, {
type: 'stripe',
- number: 9
-},
-// Blue 9
-{
- type: 'stripe',
- number: 10
-},
-// Blue 10
-{
- type: 'stripe',
- number: 11
-},
-// Blue 11
-{
- type: 'stripe',
- number: 12
-},
-// Blue 12
-{
- type: 'stripe',
- number: 13
-},
-// Blue 13
-{
- type: 'stripe',
- number: 14
-},
-// Blue 14
-{
- type: 'stripe',
number: 15
-} // Blue 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;