/****
* 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;
};
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
});
// For 'rounded' design, tint the box to look different
if (type === 'racket_rounded') {
self.asset.tint = 0x27ae60;
}
};
// 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 ---
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'];
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
}];
// --- 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
// --- 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: 120,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.x = TABLE_WIDTH / 2;
title.y = 200;
menuContainer.addChild(title);
// Racket selection
var racketLabel = new Text2('Choose your racket:', {
size: 70,
fill: 0xFFFFFF
});
racketLabel.anchor.set(0.5, 0);
racketLabel.x = TABLE_WIDTH / 2;
racketLabel.y = 400;
menuContainer.addChild(racketLabel);
var racketButtons = [];
for (var i = 0; i < RACKET_TYPES.length; i++) {
var type = RACKET_TYPES[i];
var btn = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2 + (i - 1) * 400,
y: 600
});
// Tint for rounded
if (type === 'racket_rounded') btn.tint = 0x27ae60;
btn.interactive = true;
btn.buttonMode = true;
(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;
}
};
})(i, btn);
if (i === 0) btn.alpha = 1;else btn.alpha = 0.5;
racketButtons.push(btn);
menuContainer.addChild(btn);
}
// Difficulty selection
var diffLabel = new Text2('Select difficulty:', {
size: 70,
fill: 0xFFFFFF
});
diffLabel.anchor.set(0.5, 0);
diffLabel.x = TABLE_WIDTH / 2;
diffLabel.y = 800;
menuContainer.addChild(diffLabel);
var diffButtons = [];
for (var d = 0; d < DIFFICULTIES.length; d++) {
var diff = DIFFICULTIES[d];
var diffBtn = new Text2(diff.name, {
size: 70,
fill: 0xFFFFFF
});
diffBtn.anchor.set(0.5, 0.5);
diffBtn.x = TABLE_WIDTH / 2 + (d - 1) * 350;
diffBtn.y = 950;
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);
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: 100,
fill: 0xF1C40F
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = TABLE_WIDTH / 2;
startBtn.y = 1200;
startBtn.interactive = true;
startBtn.buttonMode = true;
startBtn.down = function (x, y, obj) {
// Remove menu and start game
game.removeChild(menuContainer);
menuContainer = null;
startGame();
};
menuContainer.addChild(startBtn);
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);
// Table
table = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(table);
// Net
net = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: TABLE_WIDTH / 2,
y: NET_Y
});
game.addChild(net);
// Player racket
playerRacket = new Racket();
playerRacket.setType(selectedRacketType);
playerRacket.x = TABLE_WIDTH / 2;
playerRacket.y = PLAYER_Y;
game.addChild(playerRacket);
// AI racket
aiRacket = new Racket();
aiRacket.setType(selectedRacketType);
aiRacket.x = TABLE_WIDTH / 2;
aiRacket.y = AI_Y;
game.addChild(aiRacket);
// Ball
ball = new Ball();
resetBall('player');
game.addChild(ball);
// 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);
// 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();
if (playerScore >= maxScore) {
LK.showYouWin();
return;
}
showInfo("You scored!", 1000);
resetBall('ai');
return;
}
if (ball.y > TABLE_HEIGHT) {
// AI scores
aiScore++;
updateScoreDisplay();
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.y = r.y - ball.asset.height / 2;
ball.vy *= -1;
// Add spin based on where hit
var dx = (ball.x - playerRacket.x) / (playerRacket.asset.width / 2);
ball.vx += dx * 10;
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 25) {
ball.vx *= 25 / speed;
ball.vy *= 25 / 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.y = r.y + r.height + ball.asset.height / 2;
ball.vy *= -1;
// Add spin based on where hit
var dx = (ball.x - aiRacket.x) / (aiRacket.asset.width / 2);
ball.vx += dx * 10;
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 25) {
ball.vx *= 25 / speed;
ball.vy *= 25 / 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;
if (aiRacket.x > aiTargetX) aiRacket.x = aiTargetX;
} else {
aiRacket.x -= selectedDifficulty.aiSpeed;
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);
}
// --- Touch/mouse controls ---
function gameMove(x, y, obj) {
if (dragging) {
// Move player racket horizontally only
playerRacket.x = clamp(x, playerRacket.asset.width / 2, TABLE_WIDTH - playerRacket.asset.width / 2);
}
}
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(); ===================================================================
--- original.js
+++ change.js
@@ -339,9 +339,9 @@
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 = 32;
+ var speed = 20;
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
}
// --- Update score display ---
@@ -407,11 +407,11 @@
var dx = (ball.x - playerRacket.x) / (playerRacket.asset.width / 2);
ball.vx += dx * 10;
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
- if (speed > 40) {
- ball.vx *= 40 / speed;
- ball.vy *= 40 / speed;
+ if (speed > 25) {
+ ball.vx *= 25 / speed;
+ ball.vy *= 25 / speed;
}
}
}
// Ball collision with AI racket
@@ -426,11 +426,11 @@
var dx = (ball.x - aiRacket.x) / (aiRacket.asset.width / 2);
ball.vx += dx * 10;
// Clamp speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
- if (speed > 40) {
- ball.vx *= 40 / speed;
- ball.vy *= 40 / speed;
+ if (speed > 25) {
+ ball.vx *= 25 / speed;
+ ball.vy *= 25 / speed;
}
}
}
// AI movement