/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0, aiDifficulty: 0.7 }); /**** * Classes ****/ var Ball = Container.expand(function () { var self = Container.call(this); var ballGraphics = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); self.radius = ballGraphics.width / 2; self.velocity = { x: 0, y: 0 }; self.baseSpeed = 12; self.speedMultiplier = 1.0; self.maxSpeedMultiplier = 2.5; self.reset = function () { self.x = 2048 / 2; self.y = 2732 / 2; self.speedMultiplier = 1.0; // Random initial direction var angle = Math.random() * Math.PI / 2 - Math.PI / 4; if (Math.random() < 0.5) { angle += Math.PI; } self.velocity.x = Math.cos(angle) * self.baseSpeed; self.velocity.y = Math.sin(angle) * self.baseSpeed; // Make sure horizontal speed is significant if (Math.abs(self.velocity.x) < self.baseSpeed * 0.6) { self.velocity.x = (self.velocity.x > 0 ? 1 : -1) * self.baseSpeed * 0.6; } }; self.update = function () { // Move ball based on velocity self.x += self.velocity.x * self.speedMultiplier; self.y += self.velocity.y * self.speedMultiplier; // Collision with top and bottom walls if (self.y < self.radius) { self.y = self.radius; self.velocity.y *= -1; LK.getSound('hit').play(); } else if (self.y > 2732 - self.radius) { self.y = 2732 - self.radius; self.velocity.y *= -1; LK.getSound('hit').play(); } }; self.hitPaddle = function (paddle) { // Normalize collision point (-1 to 1) from paddle center var relativeY = (self.y - paddle.y) / (paddle.height / 2); // Adjust angle based on where the ball hit the paddle var angle = relativeY * Math.PI / 3; // max ±60 degrees // Reverse x direction and apply new angle self.velocity.x = -Math.sign(self.velocity.x) * Math.cos(angle) * self.baseSpeed; self.velocity.y = Math.sin(angle) * self.baseSpeed; // Increase speed with each hit, up to a maximum self.speedMultiplier = Math.min(self.maxSpeedMultiplier, self.speedMultiplier + 0.05); // Play hit sound LK.getSound('hit').play(); }; return self; }); var Paddle = Container.expand(function () { var self = Container.call(this); var paddleGraphics = self.attachAsset('paddle', { anchorX: 0.5, anchorY: 0.5 }); self.width = paddleGraphics.width; self.height = paddleGraphics.height; self.speed = 0; self.target = 0; self.isAI = false; self.aiReactionSpeed = 0.7; // Default AI difficulty level (0.1-0.9) // AI paddle movement logic self.updateAI = function (ball, dt) { if (!self.isAI) { return; } // Only update target if ball is moving toward this paddle if (self.x < 1024 && ball.velocity.x < 0 || self.x > 1024 && ball.velocity.x > 0) { // Predict where the ball will be var distanceX = Math.abs(self.x - ball.x); var timeToReach = distanceX / Math.abs(ball.velocity.x); var futureY = ball.y + ball.velocity.y * timeToReach; // Keep the future position in bounds futureY = Math.max(self.height / 2, Math.min(2732 - self.height / 2, futureY)); // Add some imperfection based on AI difficulty var errorFactor = (1 - self.aiReactionSpeed) * 300; futureY += Math.random() * errorFactor * 2 - errorFactor; // Gradually move toward the predicted position self.target = futureY; } }; self.update = function (dt) { // Gradually move toward target position (whether controlled by player or AI) if (self.target !== null) { self.speed = (self.target - self.y) * 0.2; // Cap movement speed var maxSpeed = 40; self.speed = Math.max(-maxSpeed, Math.min(maxSpeed, self.speed)); self.y += self.speed; // Keep paddle in bounds self.y = Math.max(self.height / 2, Math.min(2732 - self.height / 2, self.y)); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Game state var gameStarted = false; var gameMode = "single"; // "single" or "multi" var scoreLeft = 0; var scoreRight = 0; var maxScore = 11; var lastFrameTime = Date.now(); // Create paddles var leftPaddle = game.addChild(new Paddle()); leftPaddle.x = 100; leftPaddle.y = 2732 / 2; var rightPaddle = game.addChild(new Paddle()); rightPaddle.x = 2048 - 100; rightPaddle.y = 2732 / 2; rightPaddle.isAI = true; rightPaddle.aiReactionSpeed = storage.aiDifficulty; // Create ball var ball = game.addChild(new Ball()); ball.reset(); // Create center line var centerLineContainer = game.addChild(new Container()); centerLineContainer.x = 2048 / 2; for (var y = 15; y < 2732; y += 60) { var lineSegment = centerLineContainer.attachAsset('centerLine', { anchorX: 0.5, anchorY: 0.5, y: y }); } // Score text var scoreLeftText = new Text2(scoreLeft.toString(), { size: 150, fill: 0xFFFFFF }); scoreLeftText.anchor.set(1, 0); scoreLeftText.x = 2048 / 2 - 50; scoreLeftText.y = 50; LK.gui.addChild(scoreLeftText); var scoreRightText = new Text2(scoreRight.toString(), { size: 150, fill: 0xFFFFFF }); scoreRightText.anchor.set(0, 0); scoreRightText.x = 2048 / 2 + 50; scoreRightText.y = 50; LK.gui.addChild(scoreRightText); // Game mode button var singlePlayerButton = new Text2("1P", { size: 80, fill: gameMode === "single" ? "#ffffff" : "#888888" }); singlePlayerButton.anchor.set(1, 0); singlePlayerButton.x = 2048 / 2 - 20; singlePlayerButton.y = 220; LK.gui.addChild(singlePlayerButton); var multiPlayerButton = new Text2("2P", { size: 80, fill: gameMode === "multi" ? "#ffffff" : "#888888" }); multiPlayerButton.anchor.set(0, 0); multiPlayerButton.x = 2048 / 2 + 20; multiPlayerButton.y = 220; LK.gui.addChild(multiPlayerButton); // Start text var startText = new Text2("TAP TO START", { size: 100, fill: 0xFFFFFF }); startText.anchor.set(0.5, 0.5); startText.x = 2048 / 2; startText.y = 2732 / 2; LK.gui.addChild(startText); // Store which paddle the user is currently controlling var activePaddle = null; // Update scores function updateScore() { scoreLeftText.setText(scoreLeft.toString()); scoreRightText.setText(scoreRight.toString()); } // Check for win condition function checkWinCondition() { if (scoreLeft >= maxScore) { if (gameMode === "single") { // Update high score if playing single player if (scoreLeft > storage.highScore) { storage.highScore = scoreLeft; } } LK.showYouWin(); } else if (scoreRight >= maxScore) { if (gameMode === "multi") { LK.showYouWin(); // In multiplayer, right player winning is still a win } else { LK.showGameOver(); } } } // Handle touch events game.down = function (x, y, obj) { if (!gameStarted) { gameStarted = true; startText.visible = false; LK.playMusic('gameMusic'); return; } // Check if game mode buttons were clicked if (!gameStarted && y < 300) { var centerX = 2048 / 2; if (x < centerX) { gameMode = "single"; rightPaddle.isAI = true; singlePlayerButton.style.fill = "#ffffff"; multiPlayerButton.style.fill = "#888888"; } else { gameMode = "multi"; rightPaddle.isAI = false; singlePlayerButton.style.fill = "#888888"; multiPlayerButton.style.fill = "#ffffff"; } return; } // Determine which paddle to control based on screen side if (gameMode === "multi") { if (x < 2048 / 2) { activePaddle = leftPaddle; } else { activePaddle = rightPaddle; } } else { // In single player, only control left paddle activePaddle = leftPaddle; } if (activePaddle) { activePaddle.target = y; } }; game.move = function (x, y, obj) { if (activePaddle) { activePaddle.target = y; } }; game.up = function (x, y, obj) { // Keep tracking the paddle's target position // This allows for smooth movement even after lifting finger }; // Main game update loop game.update = function () { // Calculate delta time for smoother animations var currentTime = Date.now(); var dt = (currentTime - lastFrameTime) / 16.67; // Normalize to 60fps lastFrameTime = currentTime; // Don't update game logic if not started if (!gameStarted) { return; } // Update AI rightPaddle.updateAI(ball, dt); // Update paddles leftPaddle.update(dt); rightPaddle.update(dt); // Update ball ball.update(); // Check for collisions with paddles if (ball.velocity.x < 0 && ball.x - ball.radius <= leftPaddle.x + leftPaddle.width / 2 && ball.x - ball.radius > leftPaddle.x - leftPaddle.width / 2 && ball.y >= leftPaddle.y - leftPaddle.height / 2 && ball.y <= leftPaddle.y + leftPaddle.height / 2) { // Position ball at paddle edge to prevent sticking ball.x = leftPaddle.x + leftPaddle.width / 2 + ball.radius; ball.hitPaddle(leftPaddle); } else if (ball.velocity.x > 0 && ball.x + ball.radius >= rightPaddle.x - rightPaddle.width / 2 && ball.x + ball.radius < rightPaddle.x + rightPaddle.width / 2 && ball.y >= rightPaddle.y - rightPaddle.height / 2 && ball.y <= rightPaddle.y + rightPaddle.height / 2) { // Position ball at paddle edge to prevent sticking ball.x = rightPaddle.x - rightPaddle.width / 2 - ball.radius; ball.hitPaddle(rightPaddle); } // Check for scoring if (ball.x < -ball.radius) { // Right scores scoreRight++; updateScore(); LK.getSound('score').play(); ball.reset(); checkWinCondition(); } else if (ball.x > 2048 + ball.radius) { // Left scores scoreLeft++; updateScore(); LK.getSound('score').play(); ball.reset(); checkWinCondition(); } // Flash effect for center line if ball crosses it if (ball.x < 1024 && ball.velocity.x > 0 && ball.x + ball.velocity.x >= 1024 || ball.x > 1024 && ball.velocity.x < 0 && ball.x + ball.velocity.x <= 1024) { tween(centerLineContainer, { alpha: 0.3 }, { duration: 100, onFinish: function onFinish() { tween(centerLineContainer, { alpha: 1 }, { duration: 100 }); } }); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0,
aiDifficulty: 0.7
});
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGraphics = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = ballGraphics.width / 2;
self.velocity = {
x: 0,
y: 0
};
self.baseSpeed = 12;
self.speedMultiplier = 1.0;
self.maxSpeedMultiplier = 2.5;
self.reset = function () {
self.x = 2048 / 2;
self.y = 2732 / 2;
self.speedMultiplier = 1.0;
// Random initial direction
var angle = Math.random() * Math.PI / 2 - Math.PI / 4;
if (Math.random() < 0.5) {
angle += Math.PI;
}
self.velocity.x = Math.cos(angle) * self.baseSpeed;
self.velocity.y = Math.sin(angle) * self.baseSpeed;
// Make sure horizontal speed is significant
if (Math.abs(self.velocity.x) < self.baseSpeed * 0.6) {
self.velocity.x = (self.velocity.x > 0 ? 1 : -1) * self.baseSpeed * 0.6;
}
};
self.update = function () {
// Move ball based on velocity
self.x += self.velocity.x * self.speedMultiplier;
self.y += self.velocity.y * self.speedMultiplier;
// Collision with top and bottom walls
if (self.y < self.radius) {
self.y = self.radius;
self.velocity.y *= -1;
LK.getSound('hit').play();
} else if (self.y > 2732 - self.radius) {
self.y = 2732 - self.radius;
self.velocity.y *= -1;
LK.getSound('hit').play();
}
};
self.hitPaddle = function (paddle) {
// Normalize collision point (-1 to 1) from paddle center
var relativeY = (self.y - paddle.y) / (paddle.height / 2);
// Adjust angle based on where the ball hit the paddle
var angle = relativeY * Math.PI / 3; // max ±60 degrees
// Reverse x direction and apply new angle
self.velocity.x = -Math.sign(self.velocity.x) * Math.cos(angle) * self.baseSpeed;
self.velocity.y = Math.sin(angle) * self.baseSpeed;
// Increase speed with each hit, up to a maximum
self.speedMultiplier = Math.min(self.maxSpeedMultiplier, self.speedMultiplier + 0.05);
// Play hit sound
LK.getSound('hit').play();
};
return self;
});
var Paddle = Container.expand(function () {
var self = Container.call(this);
var paddleGraphics = self.attachAsset('paddle', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = paddleGraphics.width;
self.height = paddleGraphics.height;
self.speed = 0;
self.target = 0;
self.isAI = false;
self.aiReactionSpeed = 0.7; // Default AI difficulty level (0.1-0.9)
// AI paddle movement logic
self.updateAI = function (ball, dt) {
if (!self.isAI) {
return;
}
// Only update target if ball is moving toward this paddle
if (self.x < 1024 && ball.velocity.x < 0 || self.x > 1024 && ball.velocity.x > 0) {
// Predict where the ball will be
var distanceX = Math.abs(self.x - ball.x);
var timeToReach = distanceX / Math.abs(ball.velocity.x);
var futureY = ball.y + ball.velocity.y * timeToReach;
// Keep the future position in bounds
futureY = Math.max(self.height / 2, Math.min(2732 - self.height / 2, futureY));
// Add some imperfection based on AI difficulty
var errorFactor = (1 - self.aiReactionSpeed) * 300;
futureY += Math.random() * errorFactor * 2 - errorFactor;
// Gradually move toward the predicted position
self.target = futureY;
}
};
self.update = function (dt) {
// Gradually move toward target position (whether controlled by player or AI)
if (self.target !== null) {
self.speed = (self.target - self.y) * 0.2;
// Cap movement speed
var maxSpeed = 40;
self.speed = Math.max(-maxSpeed, Math.min(maxSpeed, self.speed));
self.y += self.speed;
// Keep paddle in bounds
self.y = Math.max(self.height / 2, Math.min(2732 - self.height / 2, self.y));
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game state
var gameStarted = false;
var gameMode = "single"; // "single" or "multi"
var scoreLeft = 0;
var scoreRight = 0;
var maxScore = 11;
var lastFrameTime = Date.now();
// Create paddles
var leftPaddle = game.addChild(new Paddle());
leftPaddle.x = 100;
leftPaddle.y = 2732 / 2;
var rightPaddle = game.addChild(new Paddle());
rightPaddle.x = 2048 - 100;
rightPaddle.y = 2732 / 2;
rightPaddle.isAI = true;
rightPaddle.aiReactionSpeed = storage.aiDifficulty;
// Create ball
var ball = game.addChild(new Ball());
ball.reset();
// Create center line
var centerLineContainer = game.addChild(new Container());
centerLineContainer.x = 2048 / 2;
for (var y = 15; y < 2732; y += 60) {
var lineSegment = centerLineContainer.attachAsset('centerLine', {
anchorX: 0.5,
anchorY: 0.5,
y: y
});
}
// Score text
var scoreLeftText = new Text2(scoreLeft.toString(), {
size: 150,
fill: 0xFFFFFF
});
scoreLeftText.anchor.set(1, 0);
scoreLeftText.x = 2048 / 2 - 50;
scoreLeftText.y = 50;
LK.gui.addChild(scoreLeftText);
var scoreRightText = new Text2(scoreRight.toString(), {
size: 150,
fill: 0xFFFFFF
});
scoreRightText.anchor.set(0, 0);
scoreRightText.x = 2048 / 2 + 50;
scoreRightText.y = 50;
LK.gui.addChild(scoreRightText);
// Game mode button
var singlePlayerButton = new Text2("1P", {
size: 80,
fill: gameMode === "single" ? "#ffffff" : "#888888"
});
singlePlayerButton.anchor.set(1, 0);
singlePlayerButton.x = 2048 / 2 - 20;
singlePlayerButton.y = 220;
LK.gui.addChild(singlePlayerButton);
var multiPlayerButton = new Text2("2P", {
size: 80,
fill: gameMode === "multi" ? "#ffffff" : "#888888"
});
multiPlayerButton.anchor.set(0, 0);
multiPlayerButton.x = 2048 / 2 + 20;
multiPlayerButton.y = 220;
LK.gui.addChild(multiPlayerButton);
// Start text
var startText = new Text2("TAP TO START", {
size: 100,
fill: 0xFFFFFF
});
startText.anchor.set(0.5, 0.5);
startText.x = 2048 / 2;
startText.y = 2732 / 2;
LK.gui.addChild(startText);
// Store which paddle the user is currently controlling
var activePaddle = null;
// Update scores
function updateScore() {
scoreLeftText.setText(scoreLeft.toString());
scoreRightText.setText(scoreRight.toString());
}
// Check for win condition
function checkWinCondition() {
if (scoreLeft >= maxScore) {
if (gameMode === "single") {
// Update high score if playing single player
if (scoreLeft > storage.highScore) {
storage.highScore = scoreLeft;
}
}
LK.showYouWin();
} else if (scoreRight >= maxScore) {
if (gameMode === "multi") {
LK.showYouWin(); // In multiplayer, right player winning is still a win
} else {
LK.showGameOver();
}
}
}
// Handle touch events
game.down = function (x, y, obj) {
if (!gameStarted) {
gameStarted = true;
startText.visible = false;
LK.playMusic('gameMusic');
return;
}
// Check if game mode buttons were clicked
if (!gameStarted && y < 300) {
var centerX = 2048 / 2;
if (x < centerX) {
gameMode = "single";
rightPaddle.isAI = true;
singlePlayerButton.style.fill = "#ffffff";
multiPlayerButton.style.fill = "#888888";
} else {
gameMode = "multi";
rightPaddle.isAI = false;
singlePlayerButton.style.fill = "#888888";
multiPlayerButton.style.fill = "#ffffff";
}
return;
}
// Determine which paddle to control based on screen side
if (gameMode === "multi") {
if (x < 2048 / 2) {
activePaddle = leftPaddle;
} else {
activePaddle = rightPaddle;
}
} else {
// In single player, only control left paddle
activePaddle = leftPaddle;
}
if (activePaddle) {
activePaddle.target = y;
}
};
game.move = function (x, y, obj) {
if (activePaddle) {
activePaddle.target = y;
}
};
game.up = function (x, y, obj) {
// Keep tracking the paddle's target position
// This allows for smooth movement even after lifting finger
};
// Main game update loop
game.update = function () {
// Calculate delta time for smoother animations
var currentTime = Date.now();
var dt = (currentTime - lastFrameTime) / 16.67; // Normalize to 60fps
lastFrameTime = currentTime;
// Don't update game logic if not started
if (!gameStarted) {
return;
}
// Update AI
rightPaddle.updateAI(ball, dt);
// Update paddles
leftPaddle.update(dt);
rightPaddle.update(dt);
// Update ball
ball.update();
// Check for collisions with paddles
if (ball.velocity.x < 0 && ball.x - ball.radius <= leftPaddle.x + leftPaddle.width / 2 && ball.x - ball.radius > leftPaddle.x - leftPaddle.width / 2 && ball.y >= leftPaddle.y - leftPaddle.height / 2 && ball.y <= leftPaddle.y + leftPaddle.height / 2) {
// Position ball at paddle edge to prevent sticking
ball.x = leftPaddle.x + leftPaddle.width / 2 + ball.radius;
ball.hitPaddle(leftPaddle);
} else if (ball.velocity.x > 0 && ball.x + ball.radius >= rightPaddle.x - rightPaddle.width / 2 && ball.x + ball.radius < rightPaddle.x + rightPaddle.width / 2 && ball.y >= rightPaddle.y - rightPaddle.height / 2 && ball.y <= rightPaddle.y + rightPaddle.height / 2) {
// Position ball at paddle edge to prevent sticking
ball.x = rightPaddle.x - rightPaddle.width / 2 - ball.radius;
ball.hitPaddle(rightPaddle);
}
// Check for scoring
if (ball.x < -ball.radius) {
// Right scores
scoreRight++;
updateScore();
LK.getSound('score').play();
ball.reset();
checkWinCondition();
} else if (ball.x > 2048 + ball.radius) {
// Left scores
scoreLeft++;
updateScore();
LK.getSound('score').play();
ball.reset();
checkWinCondition();
}
// Flash effect for center line if ball crosses it
if (ball.x < 1024 && ball.velocity.x > 0 && ball.x + ball.velocity.x >= 1024 || ball.x > 1024 && ball.velocity.x < 0 && ball.x + ball.velocity.x <= 1024) {
tween(centerLineContainer, {
alpha: 0.3
}, {
duration: 100,
onFinish: function onFinish() {
tween(centerLineContainer, {
alpha: 1
}, {
duration: 100
});
}
});
}
};