/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
// Ball velocity
self.vx = 0;
self.vy = 0;
// For collision, get bounds
self.getBounds = function () {
return {
x: self.x - self.asset.width / 2,
y: self.y - self.asset.height / 2,
width: self.asset.width,
height: self.asset.height
};
};
// Ball update
self.update = function () {
self.x += self.vx;
self.y += self.vy;
// Ball shadow follows ball, offset for depth
if (self.shadow) {
self.shadow.x = self.x + 18;
self.shadow.y = self.y + 24;
self.shadow.scaleX = 1.15;
self.shadow.scaleY = 0.7;
}
};
return self;
});
// Racket class (player or AI)
var Racket = Container.expand(function () {
var self = Container.call(this);
// Default: rectangle
self.racketType = 'racket_rect';
self.asset = null;
// Set racket type and color
self.setType = function (type) {
if (self.asset) {
self.removeChild(self.asset);
}
self.racketType = type;
self.asset = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
// Apply racket attributes
var attr = RACKET_ATTRIBUTES[type] || {
speed: 1.0,
power: 1.0,
control: 1.0,
size: 1.0
};
self.speed = attr.speed;
self.power = attr.power;
self.control = attr.control;
self.size = attr.size;
// Optionally scale racket width for size attribute
if (self.asset && self.size && self.size !== 1.0) {
self.asset.width = self.asset.width * self.size;
}
// For 'rounded' design, tint the box to look different
if (type === 'racket_rounded') {
self.asset.tint = 0x27ae60;
}
// Tint for new racket types
if (type === 'racket_pink') {
self.asset.tint = 0xff69b4;
}
if (type === 'racket_orange') {
self.asset.tint = 0xffa500;
}
if (type === 'racket_purple') {
self.asset.tint = 0x8e44ad;
}
if (type === 'racket_green') {
self.asset.tint = 0x27ae60;
}
if (type === 'racket_red') {
self.asset.tint = 0xe74c3c;
}
};
// Set initial type
self.setType('racket_rect');
// For collision, get bounds
self.getBounds = function () {
return {
x: self.x - self.asset.width / 2,
y: self.y - self.asset.height / 2,
width: self.asset.width,
height: self.asset.height
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x145a32 // Table green
});
/****
* Game Code
****/
// LK.init.sound('score', {volume:0.5});
// LK.init.sound('hit', {volume:0.5});
// Sounds (optional, not used in MVP as per instructions)
// Net
// Table (background, not interactive)
// Ball
// Will tint for rounded look
// Racket shapes (3 designs)
// --- Game constants ---
// New racket designs
var TABLE_WIDTH = 2048;
var TABLE_HEIGHT = 2732;
var NET_Y = TABLE_HEIGHT / 2;
var PLAYER_Y = TABLE_HEIGHT - 200;
var AI_Y = 200;
var RACKET_TYPES = ['racket_rect', 'racket_ellipse', 'racket_rounded', 'racket_pink', 'racket_orange', 'racket_purple', 'racket_green', 'racket_red'];
// Racket attributes for each type
// speed: how fast the racket can move (higher is faster)
// power: how much extra speed is added to the ball on hit (higher is more powerful)
// control: how much spin/curve can be applied (higher is more curve)
// size: multiplier for racket width (1 = normal, <1 = smaller, >1 = larger)
var RACKET_ATTRIBUTES = {
racket_rect: {
speed: 1.0,
power: 1.0,
control: 1.0,
size: 1.0
},
racket_ellipse: {
speed: 1.1,
power: 0.9,
control: 1.2,
size: 1.0
},
racket_rounded: {
speed: 1.0,
power: 1.1,
control: 1.0,
size: 1.1
},
racket_pink: {
speed: 1.2,
power: 0.8,
control: 1.3,
size: 0.95
},
racket_orange: {
speed: 0.9,
power: 1.3,
control: 0.9,
size: 1.05
},
racket_purple: {
speed: 1.0,
power: 1.0,
control: 1.5,
size: 0.9
},
racket_green: {
speed: 1.3,
power: 0.7,
control: 1.1,
size: 0.9
},
racket_red: {
speed: 0.8,
power: 1.5,
control: 0.8,
size: 1.1
}
};
var DIFFICULTIES = [{
name: 'Easy',
aiSpeed: 16,
aiError: 120,
aiReact: 0.5
}, {
name: 'Medium',
aiSpeed: 24,
aiError: 60,
aiReact: 0.7
}, {
name: 'Hard',
aiSpeed: 36,
aiError: 20,
aiReact: 0.9
}, {
name: 'Impossible',
aiSpeed: 52,
aiError: 5,
aiReact: 1.0
}];
// --- Game state ---
var playerScore = 0;
var aiScore = 0;
var maxScore = 7; // First to 7 wins
var selectedRacketType = RACKET_TYPES[0];
var selectedDifficulty = DIFFICULTIES[1]; // Default: Medium
// --- Energy bar state ---
var playerEnergy = 0;
var aiEnergy = 0;
var maxEnergy = 100;
var playerPowerShotReady = false;
var aiPowerShotReady = false;
var playerPowerShotActive = false;
var aiPowerShotActive = false;
// --- UI elements ---
var scoreText = null;
var aiScoreText = null;
var playerScoreText = null;
var infoText = null;
var menuContainer = null;
// --- Game objects ---
var playerRacket = null;
var aiRacket = null;
var ball = null;
var net = null;
var table = null;
// --- Drag state ---
var dragging = false;
// --- AI state ---
var aiTargetX = 0;
var aiReactTimer = 0;
// --- Utility: collision detection (AABB) ---
function rectsIntersect(a, b) {
return !(a.x + a.width < b.x || a.x > b.x + b.width || a.y + a.height < b.y || a.y > b.y + b.height);
}
// --- Utility: clamp ---
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// --- Show menu for racket and difficulty selection ---
function showMenu() {
menuContainer = new Container();
// Title
var title = new Text2('Ping Pong AI', {
size: 140,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0);
title.x = TABLE_WIDTH / 2;
title.y = 120;
menuContainer.addChild(title);
// Decorative underline for title
var titleUnderline = new Text2('─'.repeat(18), {
size: 80,
fill: 0xF1C40F
});
titleUnderline.anchor.set(0.5, 0);
titleUnderline.x = TABLE_WIDTH / 2;
titleUnderline.y = title.y + 120;
menuContainer.addChild(titleUnderline);
// Racket selection
var racketLabel = new Text2('Choose your racket:', {
size: 80,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
racketLabel.anchor.set(0.5, 0);
racketLabel.x = TABLE_WIDTH / 2;
racketLabel.y = titleUnderline.y + 80;
menuContainer.addChild(racketLabel);
var racketButtons = [];
// Layout: 4 rackets per row, multiple rows
var racketsPerRow = 4;
var rowSpacing = 240;
var colSpacing = 420;
var startY = racketLabel.y + 120;
var startX = TABLE_WIDTH / 2 - (racketsPerRow - 1) * colSpacing / 2;
// --- Racket attribute description text ---
var racketAttrDesc = new Text2('', {
size: 64,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
racketAttrDesc.anchor.set(0.5, 0);
racketAttrDesc.x = TABLE_WIDTH / 2;
racketAttrDesc.y = startY + 2 * rowSpacing + 40; // below rackets
menuContainer.addChild(racketAttrDesc);
// Helper to format attribute description
function getRacketAttrDesc(type) {
var attr = RACKET_ATTRIBUTES[type];
if (!attr) return "";
return "Speed: " + attr.speed + " Power: " + attr.power + " Control: " + attr.control + " Size: " + attr.size;
}
// Add a subtle background panel for rackets
var racketPanel = LK.getAsset('table', {
anchorX: 0.5,
anchorY: 0,
x: TABLE_WIDTH / 2,
y: startY - 60,
width: 1800,
height: rowSpacing * 2 + 120,
color: 0x1b3c1a
});
racketPanel.alpha = 0.7;
menuContainer.addChild(racketPanel);
for (var i = 0; i < RACKET_TYPES.length; i++) {
var type = RACKET_TYPES[i];
var row = Math.floor(i / racketsPerRow);
var col = i % racketsPerRow;
var btn = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 0.5,
x: startX + col * colSpacing,
y: startY + row * rowSpacing
});
// Tint for rounded
if (type === 'racket_rounded') btn.tint = 0x27ae60;
if (type === 'racket_pink') btn.tint = 0xff69b4;
if (type === 'racket_orange') btn.tint = 0xffa500;
if (type === 'racket_purple') btn.tint = 0x8e44ad;
if (type === 'racket_green') btn.tint = 0x27ae60;
if (type === 'racket_red') btn.tint = 0xe74c3c;
btn.interactive = true;
btn.buttonMode = true;
// Add a subtle drop shadow effect by duplicating the asset behind
var shadow = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 0.5,
x: btn.x + 10,
y: btn.y + 16
});
shadow.alpha = 0.18;
shadow.tint = 0x000000;
menuContainer.addChild(shadow);
(function (idx, btnObj) {
btnObj.down = function (x, y, obj) {
selectedRacketType = RACKET_TYPES[idx];
// Highlight selection
for (var j = 0; j < racketButtons.length; j++) {
racketButtons[j].alpha = j === idx ? 1 : 0.5;
}
// Update attribute description
racketAttrDesc.setText(getRacketAttrDesc(selectedRacketType));
};
})(i, btn);
if (i === 0) btn.alpha = 1;else btn.alpha = 0.5;
racketButtons.push(btn);
menuContainer.addChild(btn);
}
// Set initial attribute description
racketAttrDesc.setText(getRacketAttrDesc(selectedRacketType));
// Difficulty selection
var diffLabel = new Text2('Select difficulty:', {
size: 80,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
diffLabel.anchor.set(0.5, 0);
diffLabel.x = TABLE_WIDTH / 2;
// Move difficulty selection lower, below racket attribute description
diffLabel.y = startY + 2 * rowSpacing + 180;
menuContainer.addChild(diffLabel);
// Add a subtle background panel for difficulty
var diffPanel = LK.getAsset('table', {
anchorX: 0.5,
anchorY: 0,
x: TABLE_WIDTH / 2,
y: diffLabel.y - 30,
width: 1200,
height: 200,
color: 0x1b3c1a
});
diffPanel.alpha = 0.7;
menuContainer.addChild(diffPanel);
var diffButtons = [];
for (var d = 0; d < DIFFICULTIES.length; d++) {
var diff = DIFFICULTIES[d];
var diffBtn = new Text2(diff.name, {
size: 80,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
diffBtn.anchor.set(0.5, 0.5);
// Spread buttons horizontally, allow for 4 difficulties
diffBtn.x = TABLE_WIDTH / 2 + (d - 1.5) * 350;
diffBtn.y = diffLabel.y + 100;
diffBtn.interactive = true;
diffBtn.buttonMode = true;
(function (idx, btnObj) {
btnObj.down = function (x, y, obj) {
selectedDifficulty = DIFFICULTIES[idx];
for (var j = 0; j < diffButtons.length; j++) {
diffButtons[j].alpha = j === idx ? 1 : 0.5;
}
};
})(d, diffBtn);
// Default to Medium selected
if (d === 1) diffBtn.alpha = 1;else diffBtn.alpha = 0.5;
diffButtons.push(diffBtn);
menuContainer.addChild(diffBtn);
}
// Start button
var startBtn = new Text2('START', {
size: 120,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = TABLE_WIDTH / 2;
startBtn.y = diffLabel.y + 320;
startBtn.interactive = true;
startBtn.buttonMode = true;
// Add a subtle shadow for the start button
var startBtnShadow = new Text2('START', {
size: 120,
fill: 0x000000,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
startBtnShadow.anchor.set(0.5, 0.5);
startBtnShadow.x = startBtn.x + 10;
startBtnShadow.y = startBtn.y + 16;
startBtnShadow.alpha = 0.18;
menuContainer.addChild(startBtnShadow);
startBtn.down = function (x, y, obj) {
// Remove menu and start game
game.removeChild(menuContainer);
menuContainer = null;
startGame();
};
menuContainer.addChild(startBtn);
// Add a small footer
var footer = new Text2('© FRVR Ping Pong Demo', {
size: 48,
fill: 0x888888
});
footer.anchor.set(0.5, 1);
footer.x = TABLE_WIDTH / 2;
footer.y = TABLE_HEIGHT - 60;
menuContainer.addChild(footer);
game.addChild(menuContainer);
}
// --- Start or restart the game ---
function startGame() {
// Reset scores
playerScore = 0;
aiScore = 0;
// Remove previous objects if any
if (table) game.removeChild(table);
if (net) game.removeChild(net);
if (playerRacket) game.removeChild(playerRacket);
if (aiRacket) game.removeChild(aiRacket);
if (ball) game.removeChild(ball);
if (scoreText) LK.gui.top.removeChild(scoreText);
if (aiScoreText) LK.gui.top.removeChild(aiScoreText);
if (playerScoreText) LK.gui.top.removeChild(playerScoreText);
if (infoText) LK.gui.top.removeChild(infoText);
if (typeof playerEnergyBar !== "undefined" && playerEnergyBar) LK.gui.top.removeChild(playerEnergyBar);
if (typeof aiEnergyBar !== "undefined" && aiEnergyBar) LK.gui.top.removeChild(aiEnergyBar);
// Table shadow (subtle, offset, below table)
var tableShadow = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 18,
y: 24,
width: TABLE_WIDTH,
height: TABLE_HEIGHT,
color: 0x000000
});
tableShadow.alpha = 0.10;
game.addChild(tableShadow);
// Table
table = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(table);
// Net shadow (subtle, offset, below net)
var netShadow = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + 10,
y: NET_Y + 16,
width: 2048,
height: 24,
color: 0x000000
});
netShadow.alpha = 0.13;
game.addChild(netShadow);
// Net
net = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2,
y: NET_Y
});
game.addChild(net);
// Add net highlight (thin white line)
var netHighlight = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2,
y: NET_Y - 8,
width: 2048,
height: 6,
color: 0xffffff
});
netHighlight.alpha = 0.22;
game.addChild(netHighlight);
// Player racket shadow
var playerRacketShadow = LK.getAsset(selectedRacketType, {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + 16,
y: PLAYER_Y + 18
});
playerRacketShadow.alpha = 0.16;
playerRacketShadow.tint = 0x000000;
game.addChild(playerRacketShadow);
// Player racket
playerRacket = new Racket();
playerRacket.setType(selectedRacketType);
playerRacket.x = TABLE_WIDTH / 2;
playerRacket.y = PLAYER_Y;
game.addChild(playerRacket);
// AI racket shadow
var aiRacketType = RACKET_TYPES[Math.floor(Math.random() * RACKET_TYPES.length)];
var aiRacketShadow = LK.getAsset(aiRacketType, {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + 16,
y: AI_Y + 18
});
aiRacketShadow.alpha = 0.16;
aiRacketShadow.tint = 0x000000;
game.addChild(aiRacketShadow);
// AI racket
aiRacket = new Racket();
aiRacket.setType(aiRacketType);
aiRacket.x = TABLE_WIDTH / 2;
aiRacket.y = AI_Y;
game.addChild(aiRacket);
// Store shadows for update
playerRacket.shadow = playerRacketShadow;
aiRacket.shadow = aiRacketShadow;
// Ball shadow (created as a separate asset, follows ball)
ball = new Ball();
ball.shadow = LK.getAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
ball.shadow.tint = 0x000000;
ball.shadow.alpha = 0.18;
game.addChild(ball.shadow);
resetBall('player');
game.addChild(ball);
// No entry animation in original gameplay; set positions directly
if (ball.shadow) ball.shadow.y = ball.y + 24;
playerRacket.y = PLAYER_Y;
if (playerRacket.shadow) playerRacket.shadow.y = playerRacket.y + 18;
aiRacket.y = AI_Y;
if (aiRacket.shadow) aiRacket.shadow.y = aiRacket.y + 18;
// Score display
scoreText = new Text2(playerScore + " : " + aiScore, {
size: 120,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
// Player and AI score (smaller, left/right)
playerScoreText = new Text2("You", {
size: 60,
fill: 0xFFFFFF
});
playerScoreText.anchor.set(0, 0);
LK.gui.top.addChild(playerScoreText);
aiScoreText = new Text2("AI", {
size: 60,
fill: 0xFFFFFF
});
aiScoreText.anchor.set(1, 0);
LK.gui.top.addChild(aiScoreText);
// Info text (centered, for serve etc)
infoText = new Text2("", {
size: 80,
fill: 0xF1C40F
});
infoText.anchor.set(0.5, 0);
LK.gui.top.addChild(infoText);
// Energy bar UI
playerEnergy = 0;
aiEnergy = 0;
playerPowerShotReady = false;
aiPowerShotReady = false;
playerPowerShotActive = false;
aiPowerShotActive = false;
// Player energy bar (bottom left, above player label)
playerEnergyBar = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 110,
width: 400,
height: 30,
color: 0x333333
});
playerEnergyBarBar = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 110,
width: 0,
height: 30,
color: 0x3498db
});
LK.gui.top.addChild(playerEnergyBar);
LK.gui.top.addChild(playerEnergyBarBar);
// AI energy bar (top right, above AI label)
aiEnergyBar = LK.getAsset('table', {
anchorX: 1,
anchorY: 0,
x: LK.gui.top.width - 120,
y: 110,
width: 400,
height: 30,
color: 0x333333
});
aiEnergyBarBar = LK.getAsset('table', {
anchorX: 1,
anchorY: 0,
x: LK.gui.top.width - 120,
y: 110,
width: 0,
height: 30,
color: 0x3498db
});
LK.gui.top.addChild(aiEnergyBar);
LK.gui.top.addChild(aiEnergyBarBar);
// Position GUI
scoreText.x = LK.gui.top.width / 2;
scoreText.y = 20;
playerScoreText.x = 120;
playerScoreText.y = 40;
aiScoreText.x = LK.gui.top.width - 120;
aiScoreText.y = 40;
infoText.x = LK.gui.top.width / 2;
infoText.y = 170;
// Reset drag
dragging = false;
// AI state
aiTargetX = aiRacket.x;
aiReactTimer = 0;
// Show serve info
infoText.setText("Tap and drag to move your racket!");
// Start update loop
game.update = gameUpdate;
game.move = gameMove;
game.down = gameDown;
game.up = gameUp;
}
// --- Reset ball to serve (by: 'player' or 'ai') ---
function resetBall(servedBy) {
ball.x = TABLE_WIDTH / 2;
ball.y = servedBy === 'player' ? PLAYER_Y - 80 : AI_Y + 80;
// Ball speed
var angle = servedBy === 'player' ? -Math.PI / 2 + (Math.random() - 0.5) * 0.3 : Math.PI / 2 + (Math.random() - 0.5) * 0.3;
var speed = 20;
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
}
// --- Update score display ---
function updateScoreDisplay() {
scoreText.setText(playerScore + " : " + aiScore);
}
// --- Show info text for a short time ---
function showInfo(msg, duration) {
infoText.setText(msg);
if (duration) {
LK.setTimeout(function () {
infoText.setText("");
}, duration);
}
}
// --- Game update loop ---
function gameUpdate() {
// Ball movement
ball.update();
// Ball wall bounce (left/right)
if (ball.x < ball.asset.width / 2) {
ball.x = ball.asset.width / 2;
ball.vx *= -1;
}
if (ball.x > TABLE_WIDTH - ball.asset.width / 2) {
ball.x = TABLE_WIDTH - ball.asset.width / 2;
ball.vx *= -1;
}
// Ball out of bounds (top/bottom)
if (ball.y < 0) {
// Player scores
playerScore++;
updateScoreDisplay();
// Reset energy and power shot state
playerEnergy = 0;
aiEnergy = 0;
playerPowerShotReady = false;
aiPowerShotReady = false;
playerPowerShotActive = false;
aiPowerShotActive = false;
if (playerEnergyBarBar) playerEnergyBarBar.width = 0;
if (aiEnergyBarBar) aiEnergyBarBar.width = 0;
if (playerScore >= maxScore) {
LK.showYouWin();
return;
}
showInfo("You scored!", 1000);
resetBall('ai');
return;
}
if (ball.y > TABLE_HEIGHT) {
// AI scores
aiScore++;
updateScoreDisplay();
// Give player extra energy when AI scores
playerEnergy += 40;
if (playerEnergy > maxEnergy) {
playerEnergy = maxEnergy;
playerPowerShotReady = true;
}
// Reset energy and power shot state for AI
aiEnergy = 0;
aiPowerShotReady = false;
aiPowerShotActive = false;
// Reset player power shot state if not ready
if (!playerPowerShotReady) {
playerPowerShotActive = false;
playerPowerShotReady = false;
}
if (playerEnergyBarBar) playerEnergyBarBar.width = 400 * (playerEnergy / maxEnergy);
if (aiEnergyBarBar) aiEnergyBarBar.width = 0;
if (aiScore >= maxScore) {
LK.showGameOver();
return;
}
showInfo("AI scored!", 1000);
resetBall('player');
return;
}
// Ball collision with player racket
if (ball.vy > 0) {
var b = ball.getBounds();
var r = playerRacket.getBounds();
if (rectsIntersect(b, r) && ball.y < playerRacket.y) {
// Bounce
ball.vy *= -1;
// Curve effect: If hit near the sides, add curve to vx
var hitOffset = (ball.x - playerRacket.x) / (playerRacket.asset.width / 2); // -1 (left edge) to 1 (right edge)
if (Math.abs(hitOffset) > 0.4) {
// Only curve if hit near sides (outer 20% each side)
// Curve amount scales with offset and racket control
var curveStrength = 10 * hitOffset * (playerRacket.control || 1.0);
ball.vx += curveStrength;
}
// Add power attribute to outgoing ball speed
ball.vx *= playerRacket.power || 1.0;
ball.vy *= playerRacket.power || 1.0;
// Energy gain
if (!playerPowerShotReady) {
playerEnergy += 20;
if (playerEnergy >= maxEnergy) {
playerEnergy = maxEnergy;
playerPowerShotReady = true;
showInfo("Power Shot Ready!", 1000);
}
}
// Power shot activation
if (playerPowerShotReady && dragging) {
playerPowerShotActive = true;
playerPowerShotReady = false;
playerEnergy = 0;
// Power shot: boost ball speed and color
var powerShotSpeed = 40;
var angle = Math.atan2(ball.vy, ball.vx);
ball.vx = Math.cos(angle) * powerShotSpeed;
ball.vy = -Math.abs(Math.sin(angle) * powerShotSpeed);
ball.asset.tint = 0xF1C40F;
showInfo("Power Shot!", 800);
} else {
playerPowerShotActive = false;
ball.asset.tint = 0xffffff;
}
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var maxSpeed = playerPowerShotActive ? 40 : 25;
if (speed > maxSpeed) {
ball.vx *= maxSpeed / speed;
ball.vy *= maxSpeed / speed;
}
}
}
// Ball collision with AI racket
if (ball.vy < 0) {
var b = ball.getBounds();
var r = aiRacket.getBounds();
if (rectsIntersect(b, r) && ball.y > aiRacket.y) {
// Bounce
ball.vy *= -1;
// Curve effect: If hit near the sides, add curve to vx
var hitOffset = (ball.x - aiRacket.x) / (aiRacket.asset.width / 2); // -1 (left edge) to 1 (right edge)
if (Math.abs(hitOffset) > 0.4) {
// Only curve if hit near sides (outer 20% each side)
var curveStrength = 10 * hitOffset * (aiRacket.control || 1.0);
ball.vx += curveStrength;
}
// Add power attribute to outgoing ball speed
ball.vx *= aiRacket.power || 1.0;
ball.vy *= aiRacket.power || 1.0;
// Energy gain
if (!aiPowerShotReady) {
aiEnergy += 20;
if (aiEnergy >= maxEnergy) {
aiEnergy = maxEnergy;
aiPowerShotReady = true;
}
}
// Power shot activation (AI triggers automatically if ready)
if (aiPowerShotReady) {
aiPowerShotActive = true;
aiPowerShotReady = false;
aiEnergy = 0;
// Power shot: boost ball speed and color
var powerShotSpeed = 40;
var angle = Math.atan2(ball.vy, ball.vx);
ball.vx = Math.cos(angle) * powerShotSpeed;
ball.vy = Math.abs(Math.sin(angle) * powerShotSpeed);
ball.asset.tint = 0xF1C40F;
showInfo("AI Power Shot!", 800);
} else {
aiPowerShotActive = false;
if (!playerPowerShotActive) ball.asset.tint = 0xffffff;
}
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var maxSpeed = aiPowerShotActive ? 40 : 25;
if (speed > maxSpeed) {
ball.vx *= maxSpeed / speed;
ball.vy *= maxSpeed / speed;
}
}
}
// AI movement
aiReactTimer++;
if (aiReactTimer > (1 - selectedDifficulty.aiReact) * 30) {
aiReactTimer = 0;
// AI targets ball x, with error
var error = (Math.random() - 0.5) * selectedDifficulty.aiError;
aiTargetX = clamp(ball.x + error, aiRacket.asset.width / 2, TABLE_WIDTH - aiRacket.asset.width / 2);
}
// Move AI racket towards target
if (Math.abs(aiRacket.x - aiTargetX) > 5) {
if (aiRacket.x < aiTargetX) {
aiRacket.x += selectedDifficulty.aiSpeed * (aiRacket.speed || 1.0);
if (aiRacket.x > aiTargetX) aiRacket.x = aiTargetX;
} else {
aiRacket.x -= selectedDifficulty.aiSpeed * (aiRacket.speed || 1.0);
if (aiRacket.x < aiTargetX) aiRacket.x = aiTargetX;
}
// Clamp
aiRacket.x = clamp(aiRacket.x, aiRacket.asset.width / 2, TABLE_WIDTH - aiRacket.asset.width / 2);
}
// Clamp player racket (in case of fast drag)
playerRacket.x = clamp(playerRacket.x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
// Update racket shadows to follow rackets
if (playerRacket.shadow) {
playerRacket.shadow.x = playerRacket.x + 16;
playerRacket.shadow.y = playerRacket.y + 18;
playerRacket.shadow.scaleX = 1.08;
playerRacket.shadow.scaleY = 0.92;
}
if (aiRacket.shadow) {
aiRacket.shadow.x = aiRacket.x + 16;
aiRacket.shadow.y = aiRacket.y + 18;
aiRacket.shadow.scaleX = 1.08;
aiRacket.shadow.scaleY = 0.92;
}
// Update energy bar UI
if (typeof playerEnergyBarBar !== "undefined" && playerEnergyBarBar) {
playerEnergyBarBar.width = 400 * (playerEnergy / maxEnergy);
playerEnergyBarBar.tint = 0x3498db;
}
if (typeof aiEnergyBarBar !== "undefined" && aiEnergyBarBar) {
aiEnergyBarBar.width = 400 * (aiEnergy / maxEnergy);
aiEnergyBarBar.tint = 0x3498db;
}
}
// --- Touch/mouse controls ---
function gameMove(x, y, obj) {
if (dragging) {
// Move player racket horizontally only
// Move with speed attribute (lerp for smoothness)
var targetX = clamp(x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
var moveSpeed = 40 * (playerRacket.speed || 1.0);
if (Math.abs(playerRacket.x - targetX) > moveSpeed) {
if (playerRacket.x < targetX) playerRacket.x += moveSpeed;else playerRacket.x -= moveSpeed;
} else {
playerRacket.x = targetX;
}
}
}
function gameDown(x, y, obj) {
// Only start drag if touch is near player racket
var localY = y;
if (Math.abs(localY - playerRacket.y) < 120) {
dragging = true;
// Move immediately
playerRacket.x = clamp(x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
}
}
function gameUp(x, y, obj) {
dragging = false;
}
// --- Start with menu ---
showMenu();
;
// Allow player to tap energy bar to trigger power shot if ready
function energyBarDown(x, y, obj) {
if (playerPowerShotReady) {
playerPowerShotActive = true;
playerPowerShotReady = false;
playerEnergy = 0;
showInfo("Power Shot!", 800);
}
}
if (typeof playerEnergyBar !== "undefined" && playerEnergyBar) {
playerEnergyBar.down = energyBarDown;
playerEnergyBarBar.down = energyBarDown;
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
// Ball velocity
self.vx = 0;
self.vy = 0;
// For collision, get bounds
self.getBounds = function () {
return {
x: self.x - self.asset.width / 2,
y: self.y - self.asset.height / 2,
width: self.asset.width,
height: self.asset.height
};
};
// Ball update
self.update = function () {
self.x += self.vx;
self.y += self.vy;
// Ball shadow follows ball, offset for depth
if (self.shadow) {
self.shadow.x = self.x + 18;
self.shadow.y = self.y + 24;
self.shadow.scaleX = 1.15;
self.shadow.scaleY = 0.7;
}
};
return self;
});
// Racket class (player or AI)
var Racket = Container.expand(function () {
var self = Container.call(this);
// Default: rectangle
self.racketType = 'racket_rect';
self.asset = null;
// Set racket type and color
self.setType = function (type) {
if (self.asset) {
self.removeChild(self.asset);
}
self.racketType = type;
self.asset = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
// Apply racket attributes
var attr = RACKET_ATTRIBUTES[type] || {
speed: 1.0,
power: 1.0,
control: 1.0,
size: 1.0
};
self.speed = attr.speed;
self.power = attr.power;
self.control = attr.control;
self.size = attr.size;
// Optionally scale racket width for size attribute
if (self.asset && self.size && self.size !== 1.0) {
self.asset.width = self.asset.width * self.size;
}
// For 'rounded' design, tint the box to look different
if (type === 'racket_rounded') {
self.asset.tint = 0x27ae60;
}
// Tint for new racket types
if (type === 'racket_pink') {
self.asset.tint = 0xff69b4;
}
if (type === 'racket_orange') {
self.asset.tint = 0xffa500;
}
if (type === 'racket_purple') {
self.asset.tint = 0x8e44ad;
}
if (type === 'racket_green') {
self.asset.tint = 0x27ae60;
}
if (type === 'racket_red') {
self.asset.tint = 0xe74c3c;
}
};
// Set initial type
self.setType('racket_rect');
// For collision, get bounds
self.getBounds = function () {
return {
x: self.x - self.asset.width / 2,
y: self.y - self.asset.height / 2,
width: self.asset.width,
height: self.asset.height
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x145a32 // Table green
});
/****
* Game Code
****/
// LK.init.sound('score', {volume:0.5});
// LK.init.sound('hit', {volume:0.5});
// Sounds (optional, not used in MVP as per instructions)
// Net
// Table (background, not interactive)
// Ball
// Will tint for rounded look
// Racket shapes (3 designs)
// --- Game constants ---
// New racket designs
var TABLE_WIDTH = 2048;
var TABLE_HEIGHT = 2732;
var NET_Y = TABLE_HEIGHT / 2;
var PLAYER_Y = TABLE_HEIGHT - 200;
var AI_Y = 200;
var RACKET_TYPES = ['racket_rect', 'racket_ellipse', 'racket_rounded', 'racket_pink', 'racket_orange', 'racket_purple', 'racket_green', 'racket_red'];
// Racket attributes for each type
// speed: how fast the racket can move (higher is faster)
// power: how much extra speed is added to the ball on hit (higher is more powerful)
// control: how much spin/curve can be applied (higher is more curve)
// size: multiplier for racket width (1 = normal, <1 = smaller, >1 = larger)
var RACKET_ATTRIBUTES = {
racket_rect: {
speed: 1.0,
power: 1.0,
control: 1.0,
size: 1.0
},
racket_ellipse: {
speed: 1.1,
power: 0.9,
control: 1.2,
size: 1.0
},
racket_rounded: {
speed: 1.0,
power: 1.1,
control: 1.0,
size: 1.1
},
racket_pink: {
speed: 1.2,
power: 0.8,
control: 1.3,
size: 0.95
},
racket_orange: {
speed: 0.9,
power: 1.3,
control: 0.9,
size: 1.05
},
racket_purple: {
speed: 1.0,
power: 1.0,
control: 1.5,
size: 0.9
},
racket_green: {
speed: 1.3,
power: 0.7,
control: 1.1,
size: 0.9
},
racket_red: {
speed: 0.8,
power: 1.5,
control: 0.8,
size: 1.1
}
};
var DIFFICULTIES = [{
name: 'Easy',
aiSpeed: 16,
aiError: 120,
aiReact: 0.5
}, {
name: 'Medium',
aiSpeed: 24,
aiError: 60,
aiReact: 0.7
}, {
name: 'Hard',
aiSpeed: 36,
aiError: 20,
aiReact: 0.9
}, {
name: 'Impossible',
aiSpeed: 52,
aiError: 5,
aiReact: 1.0
}];
// --- Game state ---
var playerScore = 0;
var aiScore = 0;
var maxScore = 7; // First to 7 wins
var selectedRacketType = RACKET_TYPES[0];
var selectedDifficulty = DIFFICULTIES[1]; // Default: Medium
// --- Energy bar state ---
var playerEnergy = 0;
var aiEnergy = 0;
var maxEnergy = 100;
var playerPowerShotReady = false;
var aiPowerShotReady = false;
var playerPowerShotActive = false;
var aiPowerShotActive = false;
// --- UI elements ---
var scoreText = null;
var aiScoreText = null;
var playerScoreText = null;
var infoText = null;
var menuContainer = null;
// --- Game objects ---
var playerRacket = null;
var aiRacket = null;
var ball = null;
var net = null;
var table = null;
// --- Drag state ---
var dragging = false;
// --- AI state ---
var aiTargetX = 0;
var aiReactTimer = 0;
// --- Utility: collision detection (AABB) ---
function rectsIntersect(a, b) {
return !(a.x + a.width < b.x || a.x > b.x + b.width || a.y + a.height < b.y || a.y > b.y + b.height);
}
// --- Utility: clamp ---
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// --- Show menu for racket and difficulty selection ---
function showMenu() {
menuContainer = new Container();
// Title
var title = new Text2('Ping Pong AI', {
size: 140,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0);
title.x = TABLE_WIDTH / 2;
title.y = 120;
menuContainer.addChild(title);
// Decorative underline for title
var titleUnderline = new Text2('─'.repeat(18), {
size: 80,
fill: 0xF1C40F
});
titleUnderline.anchor.set(0.5, 0);
titleUnderline.x = TABLE_WIDTH / 2;
titleUnderline.y = title.y + 120;
menuContainer.addChild(titleUnderline);
// Racket selection
var racketLabel = new Text2('Choose your racket:', {
size: 80,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
racketLabel.anchor.set(0.5, 0);
racketLabel.x = TABLE_WIDTH / 2;
racketLabel.y = titleUnderline.y + 80;
menuContainer.addChild(racketLabel);
var racketButtons = [];
// Layout: 4 rackets per row, multiple rows
var racketsPerRow = 4;
var rowSpacing = 240;
var colSpacing = 420;
var startY = racketLabel.y + 120;
var startX = TABLE_WIDTH / 2 - (racketsPerRow - 1) * colSpacing / 2;
// --- Racket attribute description text ---
var racketAttrDesc = new Text2('', {
size: 64,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
racketAttrDesc.anchor.set(0.5, 0);
racketAttrDesc.x = TABLE_WIDTH / 2;
racketAttrDesc.y = startY + 2 * rowSpacing + 40; // below rackets
menuContainer.addChild(racketAttrDesc);
// Helper to format attribute description
function getRacketAttrDesc(type) {
var attr = RACKET_ATTRIBUTES[type];
if (!attr) return "";
return "Speed: " + attr.speed + " Power: " + attr.power + " Control: " + attr.control + " Size: " + attr.size;
}
// Add a subtle background panel for rackets
var racketPanel = LK.getAsset('table', {
anchorX: 0.5,
anchorY: 0,
x: TABLE_WIDTH / 2,
y: startY - 60,
width: 1800,
height: rowSpacing * 2 + 120,
color: 0x1b3c1a
});
racketPanel.alpha = 0.7;
menuContainer.addChild(racketPanel);
for (var i = 0; i < RACKET_TYPES.length; i++) {
var type = RACKET_TYPES[i];
var row = Math.floor(i / racketsPerRow);
var col = i % racketsPerRow;
var btn = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 0.5,
x: startX + col * colSpacing,
y: startY + row * rowSpacing
});
// Tint for rounded
if (type === 'racket_rounded') btn.tint = 0x27ae60;
if (type === 'racket_pink') btn.tint = 0xff69b4;
if (type === 'racket_orange') btn.tint = 0xffa500;
if (type === 'racket_purple') btn.tint = 0x8e44ad;
if (type === 'racket_green') btn.tint = 0x27ae60;
if (type === 'racket_red') btn.tint = 0xe74c3c;
btn.interactive = true;
btn.buttonMode = true;
// Add a subtle drop shadow effect by duplicating the asset behind
var shadow = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 0.5,
x: btn.x + 10,
y: btn.y + 16
});
shadow.alpha = 0.18;
shadow.tint = 0x000000;
menuContainer.addChild(shadow);
(function (idx, btnObj) {
btnObj.down = function (x, y, obj) {
selectedRacketType = RACKET_TYPES[idx];
// Highlight selection
for (var j = 0; j < racketButtons.length; j++) {
racketButtons[j].alpha = j === idx ? 1 : 0.5;
}
// Update attribute description
racketAttrDesc.setText(getRacketAttrDesc(selectedRacketType));
};
})(i, btn);
if (i === 0) btn.alpha = 1;else btn.alpha = 0.5;
racketButtons.push(btn);
menuContainer.addChild(btn);
}
// Set initial attribute description
racketAttrDesc.setText(getRacketAttrDesc(selectedRacketType));
// Difficulty selection
var diffLabel = new Text2('Select difficulty:', {
size: 80,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
diffLabel.anchor.set(0.5, 0);
diffLabel.x = TABLE_WIDTH / 2;
// Move difficulty selection lower, below racket attribute description
diffLabel.y = startY + 2 * rowSpacing + 180;
menuContainer.addChild(diffLabel);
// Add a subtle background panel for difficulty
var diffPanel = LK.getAsset('table', {
anchorX: 0.5,
anchorY: 0,
x: TABLE_WIDTH / 2,
y: diffLabel.y - 30,
width: 1200,
height: 200,
color: 0x1b3c1a
});
diffPanel.alpha = 0.7;
menuContainer.addChild(diffPanel);
var diffButtons = [];
for (var d = 0; d < DIFFICULTIES.length; d++) {
var diff = DIFFICULTIES[d];
var diffBtn = new Text2(diff.name, {
size: 80,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
diffBtn.anchor.set(0.5, 0.5);
// Spread buttons horizontally, allow for 4 difficulties
diffBtn.x = TABLE_WIDTH / 2 + (d - 1.5) * 350;
diffBtn.y = diffLabel.y + 100;
diffBtn.interactive = true;
diffBtn.buttonMode = true;
(function (idx, btnObj) {
btnObj.down = function (x, y, obj) {
selectedDifficulty = DIFFICULTIES[idx];
for (var j = 0; j < diffButtons.length; j++) {
diffButtons[j].alpha = j === idx ? 1 : 0.5;
}
};
})(d, diffBtn);
// Default to Medium selected
if (d === 1) diffBtn.alpha = 1;else diffBtn.alpha = 0.5;
diffButtons.push(diffBtn);
menuContainer.addChild(diffBtn);
}
// Start button
var startBtn = new Text2('START', {
size: 120,
fill: 0xF1C40F,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = TABLE_WIDTH / 2;
startBtn.y = diffLabel.y + 320;
startBtn.interactive = true;
startBtn.buttonMode = true;
// Add a subtle shadow for the start button
var startBtnShadow = new Text2('START', {
size: 120,
fill: 0x000000,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
startBtnShadow.anchor.set(0.5, 0.5);
startBtnShadow.x = startBtn.x + 10;
startBtnShadow.y = startBtn.y + 16;
startBtnShadow.alpha = 0.18;
menuContainer.addChild(startBtnShadow);
startBtn.down = function (x, y, obj) {
// Remove menu and start game
game.removeChild(menuContainer);
menuContainer = null;
startGame();
};
menuContainer.addChild(startBtn);
// Add a small footer
var footer = new Text2('© FRVR Ping Pong Demo', {
size: 48,
fill: 0x888888
});
footer.anchor.set(0.5, 1);
footer.x = TABLE_WIDTH / 2;
footer.y = TABLE_HEIGHT - 60;
menuContainer.addChild(footer);
game.addChild(menuContainer);
}
// --- Start or restart the game ---
function startGame() {
// Reset scores
playerScore = 0;
aiScore = 0;
// Remove previous objects if any
if (table) game.removeChild(table);
if (net) game.removeChild(net);
if (playerRacket) game.removeChild(playerRacket);
if (aiRacket) game.removeChild(aiRacket);
if (ball) game.removeChild(ball);
if (scoreText) LK.gui.top.removeChild(scoreText);
if (aiScoreText) LK.gui.top.removeChild(aiScoreText);
if (playerScoreText) LK.gui.top.removeChild(playerScoreText);
if (infoText) LK.gui.top.removeChild(infoText);
if (typeof playerEnergyBar !== "undefined" && playerEnergyBar) LK.gui.top.removeChild(playerEnergyBar);
if (typeof aiEnergyBar !== "undefined" && aiEnergyBar) LK.gui.top.removeChild(aiEnergyBar);
// Table shadow (subtle, offset, below table)
var tableShadow = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 18,
y: 24,
width: TABLE_WIDTH,
height: TABLE_HEIGHT,
color: 0x000000
});
tableShadow.alpha = 0.10;
game.addChild(tableShadow);
// Table
table = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(table);
// Net shadow (subtle, offset, below net)
var netShadow = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + 10,
y: NET_Y + 16,
width: 2048,
height: 24,
color: 0x000000
});
netShadow.alpha = 0.13;
game.addChild(netShadow);
// Net
net = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2,
y: NET_Y
});
game.addChild(net);
// Add net highlight (thin white line)
var netHighlight = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2,
y: NET_Y - 8,
width: 2048,
height: 6,
color: 0xffffff
});
netHighlight.alpha = 0.22;
game.addChild(netHighlight);
// Player racket shadow
var playerRacketShadow = LK.getAsset(selectedRacketType, {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + 16,
y: PLAYER_Y + 18
});
playerRacketShadow.alpha = 0.16;
playerRacketShadow.tint = 0x000000;
game.addChild(playerRacketShadow);
// Player racket
playerRacket = new Racket();
playerRacket.setType(selectedRacketType);
playerRacket.x = TABLE_WIDTH / 2;
playerRacket.y = PLAYER_Y;
game.addChild(playerRacket);
// AI racket shadow
var aiRacketType = RACKET_TYPES[Math.floor(Math.random() * RACKET_TYPES.length)];
var aiRacketShadow = LK.getAsset(aiRacketType, {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + 16,
y: AI_Y + 18
});
aiRacketShadow.alpha = 0.16;
aiRacketShadow.tint = 0x000000;
game.addChild(aiRacketShadow);
// AI racket
aiRacket = new Racket();
aiRacket.setType(aiRacketType);
aiRacket.x = TABLE_WIDTH / 2;
aiRacket.y = AI_Y;
game.addChild(aiRacket);
// Store shadows for update
playerRacket.shadow = playerRacketShadow;
aiRacket.shadow = aiRacketShadow;
// Ball shadow (created as a separate asset, follows ball)
ball = new Ball();
ball.shadow = LK.getAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
ball.shadow.tint = 0x000000;
ball.shadow.alpha = 0.18;
game.addChild(ball.shadow);
resetBall('player');
game.addChild(ball);
// No entry animation in original gameplay; set positions directly
if (ball.shadow) ball.shadow.y = ball.y + 24;
playerRacket.y = PLAYER_Y;
if (playerRacket.shadow) playerRacket.shadow.y = playerRacket.y + 18;
aiRacket.y = AI_Y;
if (aiRacket.shadow) aiRacket.shadow.y = aiRacket.y + 18;
// Score display
scoreText = new Text2(playerScore + " : " + aiScore, {
size: 120,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
// Player and AI score (smaller, left/right)
playerScoreText = new Text2("You", {
size: 60,
fill: 0xFFFFFF
});
playerScoreText.anchor.set(0, 0);
LK.gui.top.addChild(playerScoreText);
aiScoreText = new Text2("AI", {
size: 60,
fill: 0xFFFFFF
});
aiScoreText.anchor.set(1, 0);
LK.gui.top.addChild(aiScoreText);
// Info text (centered, for serve etc)
infoText = new Text2("", {
size: 80,
fill: 0xF1C40F
});
infoText.anchor.set(0.5, 0);
LK.gui.top.addChild(infoText);
// Energy bar UI
playerEnergy = 0;
aiEnergy = 0;
playerPowerShotReady = false;
aiPowerShotReady = false;
playerPowerShotActive = false;
aiPowerShotActive = false;
// Player energy bar (bottom left, above player label)
playerEnergyBar = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 110,
width: 400,
height: 30,
color: 0x333333
});
playerEnergyBarBar = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 110,
width: 0,
height: 30,
color: 0x3498db
});
LK.gui.top.addChild(playerEnergyBar);
LK.gui.top.addChild(playerEnergyBarBar);
// AI energy bar (top right, above AI label)
aiEnergyBar = LK.getAsset('table', {
anchorX: 1,
anchorY: 0,
x: LK.gui.top.width - 120,
y: 110,
width: 400,
height: 30,
color: 0x333333
});
aiEnergyBarBar = LK.getAsset('table', {
anchorX: 1,
anchorY: 0,
x: LK.gui.top.width - 120,
y: 110,
width: 0,
height: 30,
color: 0x3498db
});
LK.gui.top.addChild(aiEnergyBar);
LK.gui.top.addChild(aiEnergyBarBar);
// Position GUI
scoreText.x = LK.gui.top.width / 2;
scoreText.y = 20;
playerScoreText.x = 120;
playerScoreText.y = 40;
aiScoreText.x = LK.gui.top.width - 120;
aiScoreText.y = 40;
infoText.x = LK.gui.top.width / 2;
infoText.y = 170;
// Reset drag
dragging = false;
// AI state
aiTargetX = aiRacket.x;
aiReactTimer = 0;
// Show serve info
infoText.setText("Tap and drag to move your racket!");
// Start update loop
game.update = gameUpdate;
game.move = gameMove;
game.down = gameDown;
game.up = gameUp;
}
// --- Reset ball to serve (by: 'player' or 'ai') ---
function resetBall(servedBy) {
ball.x = TABLE_WIDTH / 2;
ball.y = servedBy === 'player' ? PLAYER_Y - 80 : AI_Y + 80;
// Ball speed
var angle = servedBy === 'player' ? -Math.PI / 2 + (Math.random() - 0.5) * 0.3 : Math.PI / 2 + (Math.random() - 0.5) * 0.3;
var speed = 20;
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
}
// --- Update score display ---
function updateScoreDisplay() {
scoreText.setText(playerScore + " : " + aiScore);
}
// --- Show info text for a short time ---
function showInfo(msg, duration) {
infoText.setText(msg);
if (duration) {
LK.setTimeout(function () {
infoText.setText("");
}, duration);
}
}
// --- Game update loop ---
function gameUpdate() {
// Ball movement
ball.update();
// Ball wall bounce (left/right)
if (ball.x < ball.asset.width / 2) {
ball.x = ball.asset.width / 2;
ball.vx *= -1;
}
if (ball.x > TABLE_WIDTH - ball.asset.width / 2) {
ball.x = TABLE_WIDTH - ball.asset.width / 2;
ball.vx *= -1;
}
// Ball out of bounds (top/bottom)
if (ball.y < 0) {
// Player scores
playerScore++;
updateScoreDisplay();
// Reset energy and power shot state
playerEnergy = 0;
aiEnergy = 0;
playerPowerShotReady = false;
aiPowerShotReady = false;
playerPowerShotActive = false;
aiPowerShotActive = false;
if (playerEnergyBarBar) playerEnergyBarBar.width = 0;
if (aiEnergyBarBar) aiEnergyBarBar.width = 0;
if (playerScore >= maxScore) {
LK.showYouWin();
return;
}
showInfo("You scored!", 1000);
resetBall('ai');
return;
}
if (ball.y > TABLE_HEIGHT) {
// AI scores
aiScore++;
updateScoreDisplay();
// Give player extra energy when AI scores
playerEnergy += 40;
if (playerEnergy > maxEnergy) {
playerEnergy = maxEnergy;
playerPowerShotReady = true;
}
// Reset energy and power shot state for AI
aiEnergy = 0;
aiPowerShotReady = false;
aiPowerShotActive = false;
// Reset player power shot state if not ready
if (!playerPowerShotReady) {
playerPowerShotActive = false;
playerPowerShotReady = false;
}
if (playerEnergyBarBar) playerEnergyBarBar.width = 400 * (playerEnergy / maxEnergy);
if (aiEnergyBarBar) aiEnergyBarBar.width = 0;
if (aiScore >= maxScore) {
LK.showGameOver();
return;
}
showInfo("AI scored!", 1000);
resetBall('player');
return;
}
// Ball collision with player racket
if (ball.vy > 0) {
var b = ball.getBounds();
var r = playerRacket.getBounds();
if (rectsIntersect(b, r) && ball.y < playerRacket.y) {
// Bounce
ball.vy *= -1;
// Curve effect: If hit near the sides, add curve to vx
var hitOffset = (ball.x - playerRacket.x) / (playerRacket.asset.width / 2); // -1 (left edge) to 1 (right edge)
if (Math.abs(hitOffset) > 0.4) {
// Only curve if hit near sides (outer 20% each side)
// Curve amount scales with offset and racket control
var curveStrength = 10 * hitOffset * (playerRacket.control || 1.0);
ball.vx += curveStrength;
}
// Add power attribute to outgoing ball speed
ball.vx *= playerRacket.power || 1.0;
ball.vy *= playerRacket.power || 1.0;
// Energy gain
if (!playerPowerShotReady) {
playerEnergy += 20;
if (playerEnergy >= maxEnergy) {
playerEnergy = maxEnergy;
playerPowerShotReady = true;
showInfo("Power Shot Ready!", 1000);
}
}
// Power shot activation
if (playerPowerShotReady && dragging) {
playerPowerShotActive = true;
playerPowerShotReady = false;
playerEnergy = 0;
// Power shot: boost ball speed and color
var powerShotSpeed = 40;
var angle = Math.atan2(ball.vy, ball.vx);
ball.vx = Math.cos(angle) * powerShotSpeed;
ball.vy = -Math.abs(Math.sin(angle) * powerShotSpeed);
ball.asset.tint = 0xF1C40F;
showInfo("Power Shot!", 800);
} else {
playerPowerShotActive = false;
ball.asset.tint = 0xffffff;
}
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var maxSpeed = playerPowerShotActive ? 40 : 25;
if (speed > maxSpeed) {
ball.vx *= maxSpeed / speed;
ball.vy *= maxSpeed / speed;
}
}
}
// Ball collision with AI racket
if (ball.vy < 0) {
var b = ball.getBounds();
var r = aiRacket.getBounds();
if (rectsIntersect(b, r) && ball.y > aiRacket.y) {
// Bounce
ball.vy *= -1;
// Curve effect: If hit near the sides, add curve to vx
var hitOffset = (ball.x - aiRacket.x) / (aiRacket.asset.width / 2); // -1 (left edge) to 1 (right edge)
if (Math.abs(hitOffset) > 0.4) {
// Only curve if hit near sides (outer 20% each side)
var curveStrength = 10 * hitOffset * (aiRacket.control || 1.0);
ball.vx += curveStrength;
}
// Add power attribute to outgoing ball speed
ball.vx *= aiRacket.power || 1.0;
ball.vy *= aiRacket.power || 1.0;
// Energy gain
if (!aiPowerShotReady) {
aiEnergy += 20;
if (aiEnergy >= maxEnergy) {
aiEnergy = maxEnergy;
aiPowerShotReady = true;
}
}
// Power shot activation (AI triggers automatically if ready)
if (aiPowerShotReady) {
aiPowerShotActive = true;
aiPowerShotReady = false;
aiEnergy = 0;
// Power shot: boost ball speed and color
var powerShotSpeed = 40;
var angle = Math.atan2(ball.vy, ball.vx);
ball.vx = Math.cos(angle) * powerShotSpeed;
ball.vy = Math.abs(Math.sin(angle) * powerShotSpeed);
ball.asset.tint = 0xF1C40F;
showInfo("AI Power Shot!", 800);
} else {
aiPowerShotActive = false;
if (!playerPowerShotActive) ball.asset.tint = 0xffffff;
}
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var maxSpeed = aiPowerShotActive ? 40 : 25;
if (speed > maxSpeed) {
ball.vx *= maxSpeed / speed;
ball.vy *= maxSpeed / speed;
}
}
}
// AI movement
aiReactTimer++;
if (aiReactTimer > (1 - selectedDifficulty.aiReact) * 30) {
aiReactTimer = 0;
// AI targets ball x, with error
var error = (Math.random() - 0.5) * selectedDifficulty.aiError;
aiTargetX = clamp(ball.x + error, aiRacket.asset.width / 2, TABLE_WIDTH - aiRacket.asset.width / 2);
}
// Move AI racket towards target
if (Math.abs(aiRacket.x - aiTargetX) > 5) {
if (aiRacket.x < aiTargetX) {
aiRacket.x += selectedDifficulty.aiSpeed * (aiRacket.speed || 1.0);
if (aiRacket.x > aiTargetX) aiRacket.x = aiTargetX;
} else {
aiRacket.x -= selectedDifficulty.aiSpeed * (aiRacket.speed || 1.0);
if (aiRacket.x < aiTargetX) aiRacket.x = aiTargetX;
}
// Clamp
aiRacket.x = clamp(aiRacket.x, aiRacket.asset.width / 2, TABLE_WIDTH - aiRacket.asset.width / 2);
}
// Clamp player racket (in case of fast drag)
playerRacket.x = clamp(playerRacket.x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
// Update racket shadows to follow rackets
if (playerRacket.shadow) {
playerRacket.shadow.x = playerRacket.x + 16;
playerRacket.shadow.y = playerRacket.y + 18;
playerRacket.shadow.scaleX = 1.08;
playerRacket.shadow.scaleY = 0.92;
}
if (aiRacket.shadow) {
aiRacket.shadow.x = aiRacket.x + 16;
aiRacket.shadow.y = aiRacket.y + 18;
aiRacket.shadow.scaleX = 1.08;
aiRacket.shadow.scaleY = 0.92;
}
// Update energy bar UI
if (typeof playerEnergyBarBar !== "undefined" && playerEnergyBarBar) {
playerEnergyBarBar.width = 400 * (playerEnergy / maxEnergy);
playerEnergyBarBar.tint = 0x3498db;
}
if (typeof aiEnergyBarBar !== "undefined" && aiEnergyBarBar) {
aiEnergyBarBar.width = 400 * (aiEnergy / maxEnergy);
aiEnergyBarBar.tint = 0x3498db;
}
}
// --- Touch/mouse controls ---
function gameMove(x, y, obj) {
if (dragging) {
// Move player racket horizontally only
// Move with speed attribute (lerp for smoothness)
var targetX = clamp(x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
var moveSpeed = 40 * (playerRacket.speed || 1.0);
if (Math.abs(playerRacket.x - targetX) > moveSpeed) {
if (playerRacket.x < targetX) playerRacket.x += moveSpeed;else playerRacket.x -= moveSpeed;
} else {
playerRacket.x = targetX;
}
}
}
function gameDown(x, y, obj) {
// Only start drag if touch is near player racket
var localY = y;
if (Math.abs(localY - playerRacket.y) < 120) {
dragging = true;
// Move immediately
playerRacket.x = clamp(x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
}
}
function gameUp(x, y, obj) {
dragging = false;
}
// --- Start with menu ---
showMenu();
;
// Allow player to tap energy bar to trigger power shot if ready
function energyBarDown(x, y, obj) {
if (playerPowerShotReady) {
playerPowerShotActive = true;
playerPowerShotReady = false;
playerEnergy = 0;
showInfo("Power Shot!", 800);
}
}
if (typeof playerEnergyBar !== "undefined" && playerEnergyBar) {
playerEnergyBar.down = energyBarDown;
playerEnergyBarBar.down = energyBarDown;
}