/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var AimLine = Container.expand(function () {
var self = Container.call(this);
var line = self.attachAsset('aimLine', {
anchorX: 0.5,
anchorY: 1.0
});
self.alpha = 0.5;
self.visible = false;
return self;
});
var Board = Container.expand(function () {
var self = Container.call(this);
// Board base (outer)
var boardBase = self.attachAsset('boardBase', {
anchorX: 0.5,
anchorY: 0.5
});
// Board inner area
var boardInner = self.attachAsset('boardInner', {
anchorX: 0.5,
anchorY: 0.5
});
self.boardInner = boardInner; // Ensure boardInner is accessible
self.width = boardBase.width;
self.height = boardBase.height;
self.innerWidth = boardInner.width;
self.innerHeight = boardInner.height;
// Create pockets in corners
self.pockets = [];
var pocketPositions = [
// Top left
{
x: -boardBase.width / 2 + 80,
y: -boardBase.height / 2 + 80
},
// Top right
{
x: boardBase.width / 2 - 80,
y: -boardBase.height / 2 + 80
},
// Bottom left
{
x: -boardBase.width / 2 + 80,
y: boardBase.height / 2 - 80
},
// Bottom right
{
x: boardBase.width / 2 - 80,
y: boardBase.height / 2 - 80
}];
for (var i = 0; i < pocketPositions.length; i++) {
var pocket = new Pocket();
pocket.x = pocketPositions[i].x;
pocket.y = pocketPositions[i].y;
self.addChild(pocket);
self.pockets.push(pocket);
// Add collider for each pocket
var pocketCollider = new PocketCollider(pocket.x, pocket.y, pocket.radius);
self.addChild(pocketCollider);
}
// Add red base assets to each side of the board
var baseWidth = 20;
var baseHeight = boardInner.height - 300; // Further reduced height to ensure it doesn't overlap with pockets
// Top base
var topBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: boardInner.width - 280,
// Reduced width by 80
height: baseWidth + 10 //{D} // Increased height by 10
});
topBase.x = 0;
topBase.y = -boardInner.height / 2 + baseWidth / 2 + 60; // Adjust space to be exactly in the middle of the striker's place
self.addChild(topBase);
// Bottom base
var bottomBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: boardInner.width - 280,
// Reduced width by 80
height: baseWidth + 20
});
bottomBase.x = 0;
bottomBase.y = boardInner.height / 2 - baseWidth / 2 - 60; // Adjust space to be exactly in the middle of the striker's place
self.addChild(bottomBase);
// Left base
var leftBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: baseWidth + 20,
//{K} // Increased width by 20
height: baseHeight - 30
});
leftBase.x = -self.boardInner.width / 2 + baseWidth / 2 + 60; // Adjust space to be exactly in the middle of the striker's place
leftBase.y = 0;
self.addChild(leftBase);
// Right base
var rightBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: baseWidth + 20,
//{M} // Increased width by 20
height: baseHeight - 30
});
rightBase.x = self.boardInner.width / 2 - baseWidth / 2 - 60; // Adjust space to be exactly in the middle of the striker's place
rightBase.y = 0;
self.addChild(rightBase);
return self;
});
var Coin = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'white';
self.value = self.type === 'red' ? 50 : 10;
var assetId = self.type + 'Coin';
var coinGraphic = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.friction = 0.95;
self.radius = coinGraphic.width / 2;
self.mass = self.type === 'red' ? 1.1 : 1;
self.active = true;
self.update = function () {
if (!self.active) {
return;
}
// Initialize last known positions if undefined
if (self.lastX === undefined) {
self.lastX = self.x;
}
if (self.lastY === undefined) {
self.lastY = self.y;
}
// Apply velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Ensure coin stays within the board inner boundaries
var innerLeft = board.x - board.innerWidth / 2 + self.radius;
var innerRight = board.x + board.innerWidth / 2 - self.radius;
var innerTop = board.y - board.innerHeight / 2 + self.radius;
var innerBottom = board.y + board.innerHeight / 2 - self.radius;
if (self.x < innerLeft) {
self.x = innerLeft;
self.velocityX *= -0.9; // Bounce with slight energy loss
} else if (self.x > innerRight) {
self.x = innerRight;
self.velocityX *= -0.9;
}
if (self.y < innerTop) {
self.y = innerTop;
self.velocityY *= -0.9;
} else if (self.y > innerBottom) {
self.y = innerBottom;
self.velocityY *= -0.9;
}
// Update last known positions
self.lastX = self.x;
self.lastY = self.y;
// Apply friction
self.velocityX *= self.friction;
self.velocityY *= self.friction;
// Stop small movements
if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) {
self.velocityX = 0;
self.velocityY = 0;
}
};
self.applyForce = function (forceX, forceY) {
self.velocityX += forceX / self.mass;
self.velocityY += forceY / self.mass;
};
return self;
});
var Pocket = Container.expand(function () {
var self = Container.call(this);
var pocketGraphic = self.attachAsset('pocket', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = pocketGraphic.width / 2;
return self;
});
var PocketCollider = Container.expand(function (x, y, radius) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.radius = radius;
self.intersects = function (piece) {
var dx = piece.x - self.x;
var dy = piece.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance < self.radius + piece.radius;
};
return self;
});
var PowerMeter = Container.expand(function () {
var self = Container.call(this);
// Background
var bg = self.attachAsset('powerBG', {
anchorX: 0.5,
anchorY: 0.5
});
// Foreground (power indicator)
var meter = self.attachAsset('powerMeter', {
anchorX: 0.5,
anchorY: 1.0,
y: bg.height / 2
});
self.meter = meter;
self.setLevel = function (level) {
// level should be between 0 and 1
var clampedLevel = Math.max(0, Math.min(1, level));
self.meter.scaleY = clampedLevel;
};
self.visible = false;
return self;
});
var Striker = Container.expand(function () {
var self = Container.call(this);
var strikerGraphic = self.attachAsset('striker', {
anchorX: 0.5,
anchorY: 0.5
});
// Override properties for striker
self.mass = 1.5;
self.friction = 0.975;
self.value = 0;
self.radius = strikerGraphic.width / 2;
self.restitution = 1.1; // Increase restitution for a faster bounce back effect
self.immovable = false; // Ensure striker is not immovable
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
// Initialize last known positions if undefined
if (self.lastX === undefined) {
self.lastX = self.x;
}
if (self.lastY === undefined) {
self.lastY = self.y;
}
if (self.lastWasIntersecting === undefined) {
self.lastWasIntersecting = false;
}
// Apply velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Update last known positions
self.lastX = self.x;
self.lastY = self.y;
// Initialize last known intersection state if undefined
if (self.lastWasIntersecting === undefined) {
self.lastWasIntersecting = false;
}
// Check for collision with any coin
var currentIntersecting = false;
for (var i = 0; i < coins.length; i++) {
if (self.intersects(coins[i])) {
currentIntersecting = true;
// Handle collision response
checkPieceCollision(self, coins[i]);
break;
}
}
// Update last known intersection state
self.lastWasIntersecting = currentIntersecting;
// Ensure striker stays within the board inner boundaries
var innerLeft = board.x - board.innerWidth / 2 + self.radius;
var innerRight = board.x + board.innerWidth / 2 - self.radius;
var innerTop = board.y - board.innerHeight / 2 + self.radius;
var innerBottom = board.y + board.innerHeight / 2 - self.radius;
if (self.x < innerLeft) {
self.x = innerLeft;
self.velocityX *= -self.restitution;
} else if (self.x > innerRight) {
self.x = innerRight;
self.velocityX *= -self.restitution;
}
if (self.y < innerTop) {
self.y = innerTop;
self.velocityY *= -self.restitution;
} else if (self.y > innerBottom) {
self.y = innerBottom;
self.velocityY *= -self.restitution;
}
// Apply friction
self.velocityX *= self.friction;
self.velocityY *= self.friction;
// Stop small movements
if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) {
self.velocityX = 0;
self.velocityY = 0;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xFFA500
});
/****
* Game Code
****/
// Utility function to check if an object intersects with any object in a given array
// Game state variables
function applyDirectForceToStriker(forceX, forceY) {
striker.velocityX += forceX / striker.mass;
striker.velocityY += forceY / striker.mass;
// Ensure striker is active to apply force
striker.active = true;
}
function intersectsAny(object, array) {
for (var i = 0; i < array.length; i++) {
if (object.intersects(array[i])) {
return true;
}
}
return false;
}
var gameState = 'aiming'; // aiming, power, shooting, waiting
var redCoinPocketed = false; // Track if the red coin is pocketed
var board;
var striker;
var coins = [];
var aimLine;
var powerMeter;
var score = 0;
var highScore = storage.highScore || 0;
var isMoving = false;
var powerLevel = 0;
var powerDirection = 1;
var powerSpeed = 0.02;
// Text elements
var scoreTxt = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreTxt);
var highScoreTxt = new Text2('Best: ' + highScore, {
size: 50,
fill: 0xFFFFFF
});
highScoreTxt.anchor.set(1, 1);
LK.gui.bottomRight.addChild(highScoreTxt);
var gameRulesTxt = new Text2('Pocket all coins to win!\nWhite/Black: 10pts\nRed: 50pts', {
size: 40,
fill: 0xFFFFFF,
align: 'left'
});
gameRulesTxt.anchor.set(0, 1);
LK.gui.bottomLeft.addChild(gameRulesTxt);
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.3,
duration: 1000
}
});
// Initialize board
function initializeGame() {
// Create the board
board = new Board();
board.x = 2048 / 2;
board.y = 2732 / 2;
game.addChild(board);
// Create the striker
striker = new Striker();
striker.x = board.x;
striker.y = board.y + board.height / 2 - 200;
game.addChild(striker);
// Input handling for dragging the striker
game.down = function (x, y) {
if (gameState === 'aiming') {
aimStriker(x, y);
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
};
game.up = function () {
if (gameState === 'power') {
shootStriker(powerLevel);
}
};
// Input handling for dragging the striker
game.down = function (x, y) {
if (gameState === 'aiming') {
aimStriker(x, y);
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
};
game.up = function () {
if (gameState === 'power') {
shootStriker(powerLevel);
}
};
// Create aiming line
aimLine = new AimLine();
game.addChild(aimLine);
// Create power meter
powerMeter = new PowerMeter();
powerMeter.x = 100;
powerMeter.y = 2732 / 2;
game.addChild(powerMeter);
// Create coins
createCoins();
// Reset game state
gameState = 'aiming';
score = 0;
updateScore();
}
function createCoins() {
// Clear existing coins
for (var i = 0; i < coins.length; i++) {
if (coins[i].parent) {
coins[i].parent.removeChild(coins[i]);
}
}
coins = [];
// Create a formation of coins in the center
var centerX = board.x;
var centerY = board.y;
var coinRadius = 40;
var spacing = coinRadius * 2.1;
// Create coins in a circular arrangement
var coinPositions = [
// Center
{
x: 0,
y: 0,
type: 'red'
},
// Inner circle (6 coins)
{
x: 0,
y: -spacing,
type: 'white'
}, {
x: -spacing * 0.866,
y: -spacing * 0.5,
type: 'black'
}, {
x: -spacing * 0.866,
y: spacing * 0.5,
type: 'white'
}, {
x: 0,
y: spacing,
type: 'black'
}, {
x: spacing * 0.866,
y: spacing * 0.5,
type: 'white'
}, {
x: spacing * 0.866,
y: -spacing * 0.5,
type: 'black'
},
// Outer circle (12 coins)
{
x: 0,
y: -spacing * 2,
type: 'black'
}, {
x: -spacing,
y: -spacing * 1.732,
type: 'white'
}, {
x: -spacing * 1.732,
y: -spacing,
type: 'black'
}, {
x: -spacing * 2,
y: 0,
type: 'white'
}, {
x: -spacing * 1.732,
y: spacing,
type: 'black'
}, {
x: -spacing,
y: spacing * 1.732,
type: 'white'
}, {
x: 0,
y: spacing * 2,
type: 'black'
}, {
x: spacing,
y: spacing * 1.732,
type: 'white'
}, {
x: spacing * 1.732,
y: spacing,
type: 'black'
}, {
x: spacing * 2,
y: 0,
type: 'white'
}, {
x: spacing * 1.732,
y: -spacing,
type: 'black'
}, {
x: spacing,
y: -spacing * 1.732,
type: 'white'
}];
for (var i = 0; i < coinPositions.length; i++) {
var pos = coinPositions[i];
var coin = new Coin(pos.type);
coin.x = centerX + pos.x;
coin.y = centerY + pos.y;
game.addChild(coin);
coins.push(coin);
}
}
function updateScore() {
scoreTxt.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
}
function checkGameOver() {
if (coins.length === 0) {
// All coins are pocketed, player wins
LK.showYouWin();
}
}
function aimStriker(x, y) {
var dx = x - striker.x;
var dy = y - striker.y;
var maxDragDistance = 200; // Maximum drag distance
var dragDistance = Math.sqrt(dx * dx + dy * dy);
if (dragDistance > maxDragDistance) {
var scale = maxDragDistance / dragDistance;
dx *= scale;
dy *= scale;
}
var angle = Math.atan2(dy, dx);
// Rotate aim line to point in the direction
aimLine.rotation = angle - Math.PI / 2;
aimLine.x = striker.x;
aimLine.y = striker.y;
aimLine.visible = true;
}
function shootStriker(power) {
// Convert power (0-1) to velocity
var maxVelocity = 100;
var velocity = power * maxVelocity;
// Calculate direction from aim line angle
var angle = aimLine.rotation + Math.PI / 2;
applyDirectForceToStriker(-Math.cos(angle) * velocity, -Math.sin(angle) * velocity);
// Hide aim line and power meter
aimLine.visible = false;
powerMeter.visible = false;
// Play striker release sound
LK.getSound('strikerHit').play();
// Change state
gameState = 'shooting';
// Reset striker position after shot
LK.setTimeout(function () {
striker.x = board.x;
striker.y = board.y + board.height / 2 - 200;
striker.velocityX = 0; // Reset velocity to ensure it stops moving
striker.velocityY = 0; // Reset velocity to ensure it stops moving
}, 1000);
}
function handleCollisions() {
// Collect all game pieces
var allPieces = [striker].concat(coins);
// Check collisions between all pieces
for (var i = 0; i < allPieces.length; i++) {
var pieceA = allPieces[i];
if (!pieceA.active) {
continue;
}
// Board edge collisions
checkBoardCollision(pieceA);
// Pocket collisions
checkPocketCollision(pieceA);
// Piece to piece collisions
for (var j = i + 1; j < allPieces.length; j++) {
var pieceB = allPieces[j];
if (!pieceB.active) {
continue;
}
checkPieceCollision(pieceA, pieceB);
}
// Striker to coin collision
if (pieceA === striker) {
for (var k = 0; k < coins.length; k++) {
var coin = coins[k];
if (coin.active) {
checkPieceCollision(pieceA, coin);
}
}
}
}
}
function checkBoardCollision(piece) {
var boardLeft = board.x - board.width / 2;
var boardRight = board.x + board.width / 2;
var boardTop = board.y - board.height / 2;
var boardBottom = board.y + board.height / 2;
// Adjust for piece radius
var leftEdge = boardLeft + piece.radius;
var rightEdge = boardRight - piece.radius;
var topEdge = boardTop + piece.radius;
var bottomEdge = boardBottom - piece.radius;
// Check horizontal collision
if (piece.x < leftEdge) {
piece.x = leftEdge;
piece.velocityX *= -0.9; // Bounce with slight energy loss
} else if (piece.x > rightEdge) {
piece.x = rightEdge;
piece.velocityX *= -0.9;
}
// Check vertical collision
if (piece.y < topEdge) {
piece.y = topEdge;
piece.velocityY *= -0.9;
} else if (piece.y > bottomEdge) {
piece.y = bottomEdge;
piece.velocityY *= -0.9;
}
}
function checkPocketCollision(piece) {
for (var i = 0; i < board.pockets.length; i++) {
var pocket = board.pockets[i];
var dx = piece.x - pocket.x;
var dy = piece.y - pocket.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If the piece is in the pocket
if (pocket.intersects(piece)) {
// Striker went in
if (piece === striker) {
// Reset striker position
piece.velocityX = 0;
piece.velocityY = 0;
piece.x = board.x;
piece.y = board.y + board.height / 2 - 200;
// Play pocket sound
LK.getSound('pocket').play();
} else {
// A coin went in
piece.active = false;
piece.visible = false; // Hide the coin
// Add score
score += piece.value;
updateScore();
// Check if the red coin is pocketed
if (piece.type === 'red') {
redCoinPocketed = true;
} else if (redCoinPocketed && (piece.type === 'white' || piece.type === 'black')) {
// If a white/black coin is pocketed after the red coin
redCoinPocketed = false; // Reset the flag
} else if (redCoinPocketed) {
// If no white/black coin is pocketed after the red coin
redCoinPocketed = false; // Reset the flag
// Return the red coin to the center
var redCoin = coins.find(function (c) {
return c.type === 'red';
});
if (redCoin) {
redCoin.active = true;
redCoin.visible = true;
redCoin.x = board.x;
redCoin.y = board.y;
}
}
// Remove from coins array
var index = coins.indexOf(piece);
if (index !== -1) {
coins.splice(index, 1);
}
// Play pocket sound
LK.getSound('pocket').play();
}
// Flash effect for pocket
LK.effects.flashObject(pocket, 0xFFFFFF, 300);
}
}
}
function checkPieceCollision(pieceA, pieceB) {
var dx = pieceB.x - pieceA.x;
var dy = pieceB.y - pieceA.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = pieceA.radius + pieceB.radius;
// If there's a collision
if (distance < minDistance) {
// Update last known intersection state
pieceA.lastWasIntersecting = true;
pieceB.lastWasIntersecting = true;
// Play hit sound only if striker is involved in the collision
if ((pieceA === striker || pieceB === striker) && (Math.abs(pieceA.velocityX) > 1 || Math.abs(pieceA.velocityY) > 1 || Math.abs(pieceB.velocityX) > 1 || Math.abs(pieceB.velocityY) > 1)) {
LK.getSound('hit').play();
}
// Normal vector of collision
var nx = dx / distance;
var ny = dy / distance;
// Tangent vector of collision
var tx = -ny;
var ty = nx;
// Correcting overlap
var overlap = minDistance - distance;
var correction = overlap * 0.5;
pieceA.x -= nx * correction;
pieceA.y -= ny * correction;
pieceB.x += nx * correction;
pieceB.y += ny * correction;
// Relative velocity in normal direction
var vRelativeX = pieceB.velocityX - pieceA.velocityX;
var vRelativeY = pieceB.velocityY - pieceA.velocityY;
// Normal velocity component
var vn = vRelativeX * nx + vRelativeY * ny;
// Don't do anything if pieces are moving away from each other
if (vn > 0) {
return;
}
// Elasticity coefficient
var e = 0.9; // Set elasticity for a realistic bounce back effect
// Simplified momentum and energy equations
var j = -(1 + e) * vn / (1 / pieceA.mass + 1 / pieceB.mass);
// Apply impulse
var jnx = j * nx;
var jny = j * ny;
pieceA.velocityX -= jnx / pieceA.mass;
pieceA.velocityY -= jny / pieceA.mass;
pieceB.velocityX += jnx / pieceB.mass;
pieceB.velocityY += jny / pieceB.mass;
// Transfer momentum from striker to coin
if (pieceA === striker || pieceB === striker) {
var coin = pieceA === striker ? pieceB : pieceA;
coin.applyForce(jnx, jny);
// Ensure the coin moves by setting it active
coin.active = true;
// Play hit sound when striker hits a coin
LK.getSound('hit').play();
// Update striker's last known intersection state
striker.lastWasIntersecting = true;
// Apply restitution to simulate realistic bounce
var restitution = 0.9;
coin.velocityX *= restitution;
coin.velocityY *= restitution;
// Apply additional friction to reduce speed after collision
coin.velocityX *= 0.9;
coin.velocityY *= 0.9;
}
}
}
function isAnyPieceMoving() {
if (Math.abs(striker.velocityX) > 0.1 || Math.abs(striker.velocityY) > 0.1) {
return true;
}
for (var i = 0; i < coins.length; i++) {
if (Math.abs(coins[i].velocityX) > 0.1 || Math.abs(coins[i].velocityY) > 0.1) {
return true;
}
}
return false;
}
// Input handlers
game.down = function (x, y) {
if (gameState === 'aiming') {
aimStriker(x, y);
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
};
game.move = function (x, y) {
if (gameState === 'aiming' || gameState === 'power') {
aimStriker(x, y);
if (gameState === 'aiming') {
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
} else if (gameState === 'power') {
powerLevel += powerDirection * powerSpeed;
if (powerLevel >= 1) {
powerLevel = 1;
powerDirection = -1;
} else if (powerLevel <= 0) {
powerLevel = 0;
powerDirection = 1;
}
powerMeter.setLevel(powerLevel);
}
};
game.up = function () {
if (gameState === 'power') {
shootStriker(powerLevel);
}
};
// Game update loop
game.update = function () {
switch (gameState) {
case 'aiming':
// Update power meter while aiming
powerLevel += powerDirection * powerSpeed;
if (powerLevel >= 1) {
powerLevel = 1;
powerDirection = -1;
} else if (powerLevel <= 0) {
powerLevel = 0;
powerDirection = 1;
}
powerMeter.setLevel(powerLevel);
break;
case 'power':
// Update power meter
powerLevel += powerDirection * powerSpeed;
if (powerLevel >= 1) {
powerLevel = 1;
powerDirection = -1;
} else if (powerLevel <= 0) {
powerLevel = 0;
powerDirection = 1;
}
powerMeter.setLevel(powerLevel);
break;
case 'shooting':
// Update all piece physics
striker.update();
for (var i = 0; i < coins.length; i++) {
coins[i].update();
}
// Check all collisions
handleCollisions();
// Check if all pieces have stopped moving
var wasMoving = isMoving;
isMoving = isAnyPieceMoving();
// Transition from moving to stopped
if (wasMoving && !isMoving) {
gameState = 'waiting';
redCoinPocketed = false; // Reset the flag when all pieces stop moving
// Set timeout before allowing next shot
LK.setTimeout(function () {
gameState = 'aiming';
checkGameOver();
}, 1000);
}
break;
case 'waiting':
// Waiting for timeout before next turn
break;
}
};
// Initialize the game
initializeGame(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var AimLine = Container.expand(function () {
var self = Container.call(this);
var line = self.attachAsset('aimLine', {
anchorX: 0.5,
anchorY: 1.0
});
self.alpha = 0.5;
self.visible = false;
return self;
});
var Board = Container.expand(function () {
var self = Container.call(this);
// Board base (outer)
var boardBase = self.attachAsset('boardBase', {
anchorX: 0.5,
anchorY: 0.5
});
// Board inner area
var boardInner = self.attachAsset('boardInner', {
anchorX: 0.5,
anchorY: 0.5
});
self.boardInner = boardInner; // Ensure boardInner is accessible
self.width = boardBase.width;
self.height = boardBase.height;
self.innerWidth = boardInner.width;
self.innerHeight = boardInner.height;
// Create pockets in corners
self.pockets = [];
var pocketPositions = [
// Top left
{
x: -boardBase.width / 2 + 80,
y: -boardBase.height / 2 + 80
},
// Top right
{
x: boardBase.width / 2 - 80,
y: -boardBase.height / 2 + 80
},
// Bottom left
{
x: -boardBase.width / 2 + 80,
y: boardBase.height / 2 - 80
},
// Bottom right
{
x: boardBase.width / 2 - 80,
y: boardBase.height / 2 - 80
}];
for (var i = 0; i < pocketPositions.length; i++) {
var pocket = new Pocket();
pocket.x = pocketPositions[i].x;
pocket.y = pocketPositions[i].y;
self.addChild(pocket);
self.pockets.push(pocket);
// Add collider for each pocket
var pocketCollider = new PocketCollider(pocket.x, pocket.y, pocket.radius);
self.addChild(pocketCollider);
}
// Add red base assets to each side of the board
var baseWidth = 20;
var baseHeight = boardInner.height - 300; // Further reduced height to ensure it doesn't overlap with pockets
// Top base
var topBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: boardInner.width - 280,
// Reduced width by 80
height: baseWidth + 10 //{D} // Increased height by 10
});
topBase.x = 0;
topBase.y = -boardInner.height / 2 + baseWidth / 2 + 60; // Adjust space to be exactly in the middle of the striker's place
self.addChild(topBase);
// Bottom base
var bottomBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: boardInner.width - 280,
// Reduced width by 80
height: baseWidth + 20
});
bottomBase.x = 0;
bottomBase.y = boardInner.height / 2 - baseWidth / 2 - 60; // Adjust space to be exactly in the middle of the striker's place
self.addChild(bottomBase);
// Left base
var leftBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: baseWidth + 20,
//{K} // Increased width by 20
height: baseHeight - 30
});
leftBase.x = -self.boardInner.width / 2 + baseWidth / 2 + 60; // Adjust space to be exactly in the middle of the striker's place
leftBase.y = 0;
self.addChild(leftBase);
// Right base
var rightBase = self.attachAsset('redBase', {
anchorX: 0.5,
anchorY: 0.5,
width: baseWidth + 20,
//{M} // Increased width by 20
height: baseHeight - 30
});
rightBase.x = self.boardInner.width / 2 - baseWidth / 2 - 60; // Adjust space to be exactly in the middle of the striker's place
rightBase.y = 0;
self.addChild(rightBase);
return self;
});
var Coin = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'white';
self.value = self.type === 'red' ? 50 : 10;
var assetId = self.type + 'Coin';
var coinGraphic = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.friction = 0.95;
self.radius = coinGraphic.width / 2;
self.mass = self.type === 'red' ? 1.1 : 1;
self.active = true;
self.update = function () {
if (!self.active) {
return;
}
// Initialize last known positions if undefined
if (self.lastX === undefined) {
self.lastX = self.x;
}
if (self.lastY === undefined) {
self.lastY = self.y;
}
// Apply velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Ensure coin stays within the board inner boundaries
var innerLeft = board.x - board.innerWidth / 2 + self.radius;
var innerRight = board.x + board.innerWidth / 2 - self.radius;
var innerTop = board.y - board.innerHeight / 2 + self.radius;
var innerBottom = board.y + board.innerHeight / 2 - self.radius;
if (self.x < innerLeft) {
self.x = innerLeft;
self.velocityX *= -0.9; // Bounce with slight energy loss
} else if (self.x > innerRight) {
self.x = innerRight;
self.velocityX *= -0.9;
}
if (self.y < innerTop) {
self.y = innerTop;
self.velocityY *= -0.9;
} else if (self.y > innerBottom) {
self.y = innerBottom;
self.velocityY *= -0.9;
}
// Update last known positions
self.lastX = self.x;
self.lastY = self.y;
// Apply friction
self.velocityX *= self.friction;
self.velocityY *= self.friction;
// Stop small movements
if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) {
self.velocityX = 0;
self.velocityY = 0;
}
};
self.applyForce = function (forceX, forceY) {
self.velocityX += forceX / self.mass;
self.velocityY += forceY / self.mass;
};
return self;
});
var Pocket = Container.expand(function () {
var self = Container.call(this);
var pocketGraphic = self.attachAsset('pocket', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = pocketGraphic.width / 2;
return self;
});
var PocketCollider = Container.expand(function (x, y, radius) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.radius = radius;
self.intersects = function (piece) {
var dx = piece.x - self.x;
var dy = piece.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance < self.radius + piece.radius;
};
return self;
});
var PowerMeter = Container.expand(function () {
var self = Container.call(this);
// Background
var bg = self.attachAsset('powerBG', {
anchorX: 0.5,
anchorY: 0.5
});
// Foreground (power indicator)
var meter = self.attachAsset('powerMeter', {
anchorX: 0.5,
anchorY: 1.0,
y: bg.height / 2
});
self.meter = meter;
self.setLevel = function (level) {
// level should be between 0 and 1
var clampedLevel = Math.max(0, Math.min(1, level));
self.meter.scaleY = clampedLevel;
};
self.visible = false;
return self;
});
var Striker = Container.expand(function () {
var self = Container.call(this);
var strikerGraphic = self.attachAsset('striker', {
anchorX: 0.5,
anchorY: 0.5
});
// Override properties for striker
self.mass = 1.5;
self.friction = 0.975;
self.value = 0;
self.radius = strikerGraphic.width / 2;
self.restitution = 1.1; // Increase restitution for a faster bounce back effect
self.immovable = false; // Ensure striker is not immovable
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
// Initialize last known positions if undefined
if (self.lastX === undefined) {
self.lastX = self.x;
}
if (self.lastY === undefined) {
self.lastY = self.y;
}
if (self.lastWasIntersecting === undefined) {
self.lastWasIntersecting = false;
}
// Apply velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Update last known positions
self.lastX = self.x;
self.lastY = self.y;
// Initialize last known intersection state if undefined
if (self.lastWasIntersecting === undefined) {
self.lastWasIntersecting = false;
}
// Check for collision with any coin
var currentIntersecting = false;
for (var i = 0; i < coins.length; i++) {
if (self.intersects(coins[i])) {
currentIntersecting = true;
// Handle collision response
checkPieceCollision(self, coins[i]);
break;
}
}
// Update last known intersection state
self.lastWasIntersecting = currentIntersecting;
// Ensure striker stays within the board inner boundaries
var innerLeft = board.x - board.innerWidth / 2 + self.radius;
var innerRight = board.x + board.innerWidth / 2 - self.radius;
var innerTop = board.y - board.innerHeight / 2 + self.radius;
var innerBottom = board.y + board.innerHeight / 2 - self.radius;
if (self.x < innerLeft) {
self.x = innerLeft;
self.velocityX *= -self.restitution;
} else if (self.x > innerRight) {
self.x = innerRight;
self.velocityX *= -self.restitution;
}
if (self.y < innerTop) {
self.y = innerTop;
self.velocityY *= -self.restitution;
} else if (self.y > innerBottom) {
self.y = innerBottom;
self.velocityY *= -self.restitution;
}
// Apply friction
self.velocityX *= self.friction;
self.velocityY *= self.friction;
// Stop small movements
if (Math.abs(self.velocityX) < 0.1 && Math.abs(self.velocityY) < 0.1) {
self.velocityX = 0;
self.velocityY = 0;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xFFA500
});
/****
* Game Code
****/
// Utility function to check if an object intersects with any object in a given array
// Game state variables
function applyDirectForceToStriker(forceX, forceY) {
striker.velocityX += forceX / striker.mass;
striker.velocityY += forceY / striker.mass;
// Ensure striker is active to apply force
striker.active = true;
}
function intersectsAny(object, array) {
for (var i = 0; i < array.length; i++) {
if (object.intersects(array[i])) {
return true;
}
}
return false;
}
var gameState = 'aiming'; // aiming, power, shooting, waiting
var redCoinPocketed = false; // Track if the red coin is pocketed
var board;
var striker;
var coins = [];
var aimLine;
var powerMeter;
var score = 0;
var highScore = storage.highScore || 0;
var isMoving = false;
var powerLevel = 0;
var powerDirection = 1;
var powerSpeed = 0.02;
// Text elements
var scoreTxt = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreTxt);
var highScoreTxt = new Text2('Best: ' + highScore, {
size: 50,
fill: 0xFFFFFF
});
highScoreTxt.anchor.set(1, 1);
LK.gui.bottomRight.addChild(highScoreTxt);
var gameRulesTxt = new Text2('Pocket all coins to win!\nWhite/Black: 10pts\nRed: 50pts', {
size: 40,
fill: 0xFFFFFF,
align: 'left'
});
gameRulesTxt.anchor.set(0, 1);
LK.gui.bottomLeft.addChild(gameRulesTxt);
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.3,
duration: 1000
}
});
// Initialize board
function initializeGame() {
// Create the board
board = new Board();
board.x = 2048 / 2;
board.y = 2732 / 2;
game.addChild(board);
// Create the striker
striker = new Striker();
striker.x = board.x;
striker.y = board.y + board.height / 2 - 200;
game.addChild(striker);
// Input handling for dragging the striker
game.down = function (x, y) {
if (gameState === 'aiming') {
aimStriker(x, y);
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
};
game.up = function () {
if (gameState === 'power') {
shootStriker(powerLevel);
}
};
// Input handling for dragging the striker
game.down = function (x, y) {
if (gameState === 'aiming') {
aimStriker(x, y);
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
};
game.up = function () {
if (gameState === 'power') {
shootStriker(powerLevel);
}
};
// Create aiming line
aimLine = new AimLine();
game.addChild(aimLine);
// Create power meter
powerMeter = new PowerMeter();
powerMeter.x = 100;
powerMeter.y = 2732 / 2;
game.addChild(powerMeter);
// Create coins
createCoins();
// Reset game state
gameState = 'aiming';
score = 0;
updateScore();
}
function createCoins() {
// Clear existing coins
for (var i = 0; i < coins.length; i++) {
if (coins[i].parent) {
coins[i].parent.removeChild(coins[i]);
}
}
coins = [];
// Create a formation of coins in the center
var centerX = board.x;
var centerY = board.y;
var coinRadius = 40;
var spacing = coinRadius * 2.1;
// Create coins in a circular arrangement
var coinPositions = [
// Center
{
x: 0,
y: 0,
type: 'red'
},
// Inner circle (6 coins)
{
x: 0,
y: -spacing,
type: 'white'
}, {
x: -spacing * 0.866,
y: -spacing * 0.5,
type: 'black'
}, {
x: -spacing * 0.866,
y: spacing * 0.5,
type: 'white'
}, {
x: 0,
y: spacing,
type: 'black'
}, {
x: spacing * 0.866,
y: spacing * 0.5,
type: 'white'
}, {
x: spacing * 0.866,
y: -spacing * 0.5,
type: 'black'
},
// Outer circle (12 coins)
{
x: 0,
y: -spacing * 2,
type: 'black'
}, {
x: -spacing,
y: -spacing * 1.732,
type: 'white'
}, {
x: -spacing * 1.732,
y: -spacing,
type: 'black'
}, {
x: -spacing * 2,
y: 0,
type: 'white'
}, {
x: -spacing * 1.732,
y: spacing,
type: 'black'
}, {
x: -spacing,
y: spacing * 1.732,
type: 'white'
}, {
x: 0,
y: spacing * 2,
type: 'black'
}, {
x: spacing,
y: spacing * 1.732,
type: 'white'
}, {
x: spacing * 1.732,
y: spacing,
type: 'black'
}, {
x: spacing * 2,
y: 0,
type: 'white'
}, {
x: spacing * 1.732,
y: -spacing,
type: 'black'
}, {
x: spacing,
y: -spacing * 1.732,
type: 'white'
}];
for (var i = 0; i < coinPositions.length; i++) {
var pos = coinPositions[i];
var coin = new Coin(pos.type);
coin.x = centerX + pos.x;
coin.y = centerY + pos.y;
game.addChild(coin);
coins.push(coin);
}
}
function updateScore() {
scoreTxt.setText('Score: ' + score);
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
}
function checkGameOver() {
if (coins.length === 0) {
// All coins are pocketed, player wins
LK.showYouWin();
}
}
function aimStriker(x, y) {
var dx = x - striker.x;
var dy = y - striker.y;
var maxDragDistance = 200; // Maximum drag distance
var dragDistance = Math.sqrt(dx * dx + dy * dy);
if (dragDistance > maxDragDistance) {
var scale = maxDragDistance / dragDistance;
dx *= scale;
dy *= scale;
}
var angle = Math.atan2(dy, dx);
// Rotate aim line to point in the direction
aimLine.rotation = angle - Math.PI / 2;
aimLine.x = striker.x;
aimLine.y = striker.y;
aimLine.visible = true;
}
function shootStriker(power) {
// Convert power (0-1) to velocity
var maxVelocity = 100;
var velocity = power * maxVelocity;
// Calculate direction from aim line angle
var angle = aimLine.rotation + Math.PI / 2;
applyDirectForceToStriker(-Math.cos(angle) * velocity, -Math.sin(angle) * velocity);
// Hide aim line and power meter
aimLine.visible = false;
powerMeter.visible = false;
// Play striker release sound
LK.getSound('strikerHit').play();
// Change state
gameState = 'shooting';
// Reset striker position after shot
LK.setTimeout(function () {
striker.x = board.x;
striker.y = board.y + board.height / 2 - 200;
striker.velocityX = 0; // Reset velocity to ensure it stops moving
striker.velocityY = 0; // Reset velocity to ensure it stops moving
}, 1000);
}
function handleCollisions() {
// Collect all game pieces
var allPieces = [striker].concat(coins);
// Check collisions between all pieces
for (var i = 0; i < allPieces.length; i++) {
var pieceA = allPieces[i];
if (!pieceA.active) {
continue;
}
// Board edge collisions
checkBoardCollision(pieceA);
// Pocket collisions
checkPocketCollision(pieceA);
// Piece to piece collisions
for (var j = i + 1; j < allPieces.length; j++) {
var pieceB = allPieces[j];
if (!pieceB.active) {
continue;
}
checkPieceCollision(pieceA, pieceB);
}
// Striker to coin collision
if (pieceA === striker) {
for (var k = 0; k < coins.length; k++) {
var coin = coins[k];
if (coin.active) {
checkPieceCollision(pieceA, coin);
}
}
}
}
}
function checkBoardCollision(piece) {
var boardLeft = board.x - board.width / 2;
var boardRight = board.x + board.width / 2;
var boardTop = board.y - board.height / 2;
var boardBottom = board.y + board.height / 2;
// Adjust for piece radius
var leftEdge = boardLeft + piece.radius;
var rightEdge = boardRight - piece.radius;
var topEdge = boardTop + piece.radius;
var bottomEdge = boardBottom - piece.radius;
// Check horizontal collision
if (piece.x < leftEdge) {
piece.x = leftEdge;
piece.velocityX *= -0.9; // Bounce with slight energy loss
} else if (piece.x > rightEdge) {
piece.x = rightEdge;
piece.velocityX *= -0.9;
}
// Check vertical collision
if (piece.y < topEdge) {
piece.y = topEdge;
piece.velocityY *= -0.9;
} else if (piece.y > bottomEdge) {
piece.y = bottomEdge;
piece.velocityY *= -0.9;
}
}
function checkPocketCollision(piece) {
for (var i = 0; i < board.pockets.length; i++) {
var pocket = board.pockets[i];
var dx = piece.x - pocket.x;
var dy = piece.y - pocket.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If the piece is in the pocket
if (pocket.intersects(piece)) {
// Striker went in
if (piece === striker) {
// Reset striker position
piece.velocityX = 0;
piece.velocityY = 0;
piece.x = board.x;
piece.y = board.y + board.height / 2 - 200;
// Play pocket sound
LK.getSound('pocket').play();
} else {
// A coin went in
piece.active = false;
piece.visible = false; // Hide the coin
// Add score
score += piece.value;
updateScore();
// Check if the red coin is pocketed
if (piece.type === 'red') {
redCoinPocketed = true;
} else if (redCoinPocketed && (piece.type === 'white' || piece.type === 'black')) {
// If a white/black coin is pocketed after the red coin
redCoinPocketed = false; // Reset the flag
} else if (redCoinPocketed) {
// If no white/black coin is pocketed after the red coin
redCoinPocketed = false; // Reset the flag
// Return the red coin to the center
var redCoin = coins.find(function (c) {
return c.type === 'red';
});
if (redCoin) {
redCoin.active = true;
redCoin.visible = true;
redCoin.x = board.x;
redCoin.y = board.y;
}
}
// Remove from coins array
var index = coins.indexOf(piece);
if (index !== -1) {
coins.splice(index, 1);
}
// Play pocket sound
LK.getSound('pocket').play();
}
// Flash effect for pocket
LK.effects.flashObject(pocket, 0xFFFFFF, 300);
}
}
}
function checkPieceCollision(pieceA, pieceB) {
var dx = pieceB.x - pieceA.x;
var dy = pieceB.y - pieceA.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = pieceA.radius + pieceB.radius;
// If there's a collision
if (distance < minDistance) {
// Update last known intersection state
pieceA.lastWasIntersecting = true;
pieceB.lastWasIntersecting = true;
// Play hit sound only if striker is involved in the collision
if ((pieceA === striker || pieceB === striker) && (Math.abs(pieceA.velocityX) > 1 || Math.abs(pieceA.velocityY) > 1 || Math.abs(pieceB.velocityX) > 1 || Math.abs(pieceB.velocityY) > 1)) {
LK.getSound('hit').play();
}
// Normal vector of collision
var nx = dx / distance;
var ny = dy / distance;
// Tangent vector of collision
var tx = -ny;
var ty = nx;
// Correcting overlap
var overlap = minDistance - distance;
var correction = overlap * 0.5;
pieceA.x -= nx * correction;
pieceA.y -= ny * correction;
pieceB.x += nx * correction;
pieceB.y += ny * correction;
// Relative velocity in normal direction
var vRelativeX = pieceB.velocityX - pieceA.velocityX;
var vRelativeY = pieceB.velocityY - pieceA.velocityY;
// Normal velocity component
var vn = vRelativeX * nx + vRelativeY * ny;
// Don't do anything if pieces are moving away from each other
if (vn > 0) {
return;
}
// Elasticity coefficient
var e = 0.9; // Set elasticity for a realistic bounce back effect
// Simplified momentum and energy equations
var j = -(1 + e) * vn / (1 / pieceA.mass + 1 / pieceB.mass);
// Apply impulse
var jnx = j * nx;
var jny = j * ny;
pieceA.velocityX -= jnx / pieceA.mass;
pieceA.velocityY -= jny / pieceA.mass;
pieceB.velocityX += jnx / pieceB.mass;
pieceB.velocityY += jny / pieceB.mass;
// Transfer momentum from striker to coin
if (pieceA === striker || pieceB === striker) {
var coin = pieceA === striker ? pieceB : pieceA;
coin.applyForce(jnx, jny);
// Ensure the coin moves by setting it active
coin.active = true;
// Play hit sound when striker hits a coin
LK.getSound('hit').play();
// Update striker's last known intersection state
striker.lastWasIntersecting = true;
// Apply restitution to simulate realistic bounce
var restitution = 0.9;
coin.velocityX *= restitution;
coin.velocityY *= restitution;
// Apply additional friction to reduce speed after collision
coin.velocityX *= 0.9;
coin.velocityY *= 0.9;
}
}
}
function isAnyPieceMoving() {
if (Math.abs(striker.velocityX) > 0.1 || Math.abs(striker.velocityY) > 0.1) {
return true;
}
for (var i = 0; i < coins.length; i++) {
if (Math.abs(coins[i].velocityX) > 0.1 || Math.abs(coins[i].velocityY) > 0.1) {
return true;
}
}
return false;
}
// Input handlers
game.down = function (x, y) {
if (gameState === 'aiming') {
aimStriker(x, y);
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
};
game.move = function (x, y) {
if (gameState === 'aiming' || gameState === 'power') {
aimStriker(x, y);
if (gameState === 'aiming') {
gameState = 'power';
powerLevel = 0;
powerDirection = 1;
powerMeter.setLevel(powerLevel);
powerMeter.visible = true;
}
} else if (gameState === 'power') {
powerLevel += powerDirection * powerSpeed;
if (powerLevel >= 1) {
powerLevel = 1;
powerDirection = -1;
} else if (powerLevel <= 0) {
powerLevel = 0;
powerDirection = 1;
}
powerMeter.setLevel(powerLevel);
}
};
game.up = function () {
if (gameState === 'power') {
shootStriker(powerLevel);
}
};
// Game update loop
game.update = function () {
switch (gameState) {
case 'aiming':
// Update power meter while aiming
powerLevel += powerDirection * powerSpeed;
if (powerLevel >= 1) {
powerLevel = 1;
powerDirection = -1;
} else if (powerLevel <= 0) {
powerLevel = 0;
powerDirection = 1;
}
powerMeter.setLevel(powerLevel);
break;
case 'power':
// Update power meter
powerLevel += powerDirection * powerSpeed;
if (powerLevel >= 1) {
powerLevel = 1;
powerDirection = -1;
} else if (powerLevel <= 0) {
powerLevel = 0;
powerDirection = 1;
}
powerMeter.setLevel(powerLevel);
break;
case 'shooting':
// Update all piece physics
striker.update();
for (var i = 0; i < coins.length; i++) {
coins[i].update();
}
// Check all collisions
handleCollisions();
// Check if all pieces have stopped moving
var wasMoving = isMoving;
isMoving = isAnyPieceMoving();
// Transition from moving to stopped
if (wasMoving && !isMoving) {
gameState = 'waiting';
redCoinPocketed = false; // Reset the flag when all pieces stop moving
// Set timeout before allowing next shot
LK.setTimeout(function () {
gameState = 'aiming';
checkGameOver();
}, 1000);
}
break;
case 'waiting':
// Waiting for timeout before next turn
break;
}
};
// Initialize the game
initializeGame();
Is a top-down circular image, ideally 60–70 pixels in diameter. Has a realistic or slightly stylized design (classic carrom striker look). Has a white outer ring, with either a red, blue, or black inner design.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
I’m creating a 2D Carrom game and need a high-quality top-down Carrom board asset. Please generate. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows