/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGraphics = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10;
self.dx = 0;
self.dy = 0;
self.radius = ballGraphics.width / 2;
self.active = false;
self.lastX = 0;
self.lastY = 0;
self.lastCollidedWith = {}; // Track collisions with paddles
self.reset = function () {
// Clear any existing launch timer first to prevent ghost balls
if (self.launchTimer) {
LK.clearTimeout(self.launchTimer);
self.launchTimer = null;
}
// Reset position to center of table
self.x = tableX;
self.y = tableY;
// Make sure to set lastX and lastY to the same values to prevent false collisions
self.lastX = self.x;
self.lastY = self.y;
// Reset speed and direction vectors to 0
self.dx = 0;
self.dy = 0;
// Explicitly deactivate ball
self.active = false;
// Clear collision tracking completely
self.lastCollidedWith = {};
// Make sure this ball is the only active ball in the game
if (game && game.children) {
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child !== self && child instanceof Ball) {
// Remove any other balls from the game
game.removeChild(child);
}
}
}
// Schedule ball to start after a delay
self.launchTimer = LK.setTimeout(function () {
// Only launch if this ball is still in the game
if (self.parent) {
self.launch();
}
self.launchTimer = null;
}, 1000);
};
self.launch = function () {
// Only launch if ball is not active and is still attached to game to prevent multiple launches
if (self.active || !self.parent) {
return;
}
// Launch ball at a random angle toward player or ai
var angle = Math.random() * Math.PI / 4 - Math.PI / 8;
// Determine direction (left or right) - use exact Math.PI value
if (Math.random() < 0.5) {
angle += Math.PI;
}
// Set reasonable initial velocity
self.dx = Math.cos(angle) * self.speed;
self.dy = Math.sin(angle) * self.speed;
// Ensure lastX/lastY match current position before activating
self.lastX = self.x;
self.lastY = self.y;
// Clear collision tracking completely before activating
self.lastCollidedWith = {};
// Activate the ball
self.active = true;
// Cancel any pending launch timers to prevent duplicates
if (self.launchTimer) {
LK.clearTimeout(self.launchTimer);
self.launchTimer = null;
}
};
self.update = function () {
// Verify this ball is valid and active
if (!self.active || !self.parent) {
return;
}
// Verify this is the only active ball
if (game && game.children) {
var activeBalls = 0;
var thisBallFound = false;
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child instanceof Ball) {
activeBalls++;
if (child === self) {
thisBallFound = true;
}
}
}
// If this ball isn't found or there are multiple balls, reset
if (!thisBallFound || activeBalls > 1) {
// Remove extra balls and reset this one
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child instanceof Ball && child !== self) {
game.removeChild(child);
}
}
// If this is a ghost ball, remove it
if (!thisBallFound) {
return; // Exit update cycle for ghost ball
}
}
}
// Track last position for proper collision detection
self.lastX = self.x;
self.lastY = self.y;
// Limit speed to prevent teleportation issues
var maxSpeed = 25;
if (Math.abs(self.dx) > maxSpeed) {
self.dx = self.dx > 0 ? maxSpeed : -maxSpeed;
}
if (Math.abs(self.dy) > maxSpeed) {
self.dy = self.dy > 0 ? maxSpeed : -maxSpeed;
}
// Move ball
self.x += self.dx;
self.y += self.dy;
// Table boundaries
var tableTop = tableY - tableHeight / 2 + self.radius;
var tableBottom = tableY + tableHeight / 2 - self.radius;
// Bounce off top and bottom
if (self.y < tableTop) {
self.y = tableTop;
self.dy = -self.dy;
// Always use Tralalero sound
LK.getSound('hitTrala').play();
} else if (self.y > tableBottom) {
self.y = tableBottom;
self.dy = -self.dy;
// Always use Tralalero sound
LK.getSound('hitTrala').play();
}
// Check for paddle collisions first (returns true if collision happened)
var hitPlayerPaddle = checkPaddleCollision(self, playerPaddle);
var hitAiPaddle = checkPaddleCollision(self, aiPaddle);
// Only check for scoring if no paddle was hit this frame
if (!hitPlayerPaddle && !hitAiPaddle) {
// Check for scoring (left and right boundaries)
var tableLeft = tableX - tableWidth / 2;
var tableRight = tableX + tableWidth / 2;
// Track boundary crossing for accurate scoring
// Check if ball has just crossed the left boundary
if (self.lastX >= tableLeft - self.radius && self.x < tableLeft - self.radius) {
// AI scores - only if ball is active (wasn't already scored)
if (self.active) {
self.active = false;
aiScore++;
updateScoreDisplay();
LK.getSound('score').play();
checkGameOver();
self.reset();
}
}
// Check if ball has just crossed the right boundary
else if (self.lastX <= tableRight + self.radius && self.x > tableRight + self.radius) {
// Player scores - only if ball is active (wasn't already scored)
if (self.active) {
self.active = false;
playerScore++;
updateScoreDisplay();
LK.getSound('score').play();
checkGameOver();
self.reset();
}
}
}
};
return self;
});
var Character = Container.expand(function (name, description, imageAsset) {
var self = Container.call(this);
// Create the character box with image, name and description
var characterBox = new CharacterBox(imageAsset, name, description);
self.addChild(characterBox);
// Store properties
self.name = name || "Unknown Character";
self.description = description || "No description available";
self.imageAsset = imageAsset;
// Add bounce animation
self.startY = 0; // Initialize with 0, will be updated in first bounce
LK.setInterval(function () {
self.startY = self.y; // Update current position before animation
tween(self, {
y: self.startY - 15
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
y: self.startY
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}, 2000);
// Method to set selected state
self.setSelected = function (isSelected) {
characterBox.setSelected(isSelected);
};
// Handle click/tap
self.down = function (x, y, obj) {
selectCharacter(self);
};
return self;
});
var CharacterBox = Container.expand(function (imageAsset, name, description) {
var self = Container.call(this);
// Create box background
var characterBox = self.attachAsset('characterBox', {
anchorX: 0.5,
anchorY: 0.5
});
// Assign a unique color based on the character name's length
var colorOptions = [0xf4d03f, 0x3498db, 0xe74c3c, 0x2ecc71, 0x9b59b6, 0x1abc9c];
var colorIndex = name ? name.length % colorOptions.length : 0;
characterBox.tint = colorOptions[colorIndex];
// Store properties
self.characterName = name || "Unknown Character";
self.characterDescription = description || "No description available";
self.imageAsset = imageAsset;
// Add character image if specified
if (imageAsset) {
var characterImage = LK.getAsset(imageAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
characterImage.y = -10; // Position image in the center of the box
self.addChild(characterImage);
}
// Create empty container for description (kept to maintain layout structure)
var descContainer = new Container();
descContainer.y = -characterBox.height / 2 + 80;
self.addChild(descContainer);
// Method to set selected state
self.setSelected = function (isSelected) {
if (isSelected) {
characterBox.tint = 0xff0000;
} else {
// Get the original color from initialization
var colorOptions = [0xf4d03f, 0x3498db, 0xe74c3c, 0x2ecc71, 0x9b59b6, 0x1abc9c];
var colorIndex = self.characterName ? self.characterName.length % colorOptions.length : 0;
characterBox.tint = colorOptions[colorIndex]; // Restore original color
}
};
// Handle click/tap
self.down = function (x, y, obj) {
selectCharacter(self);
};
return self;
});
var CharacterSelectScreen = Container.expand(function () {
var self = Container.call(this);
var titleText = new Text2("CHOOSE YOUR WARRIOR", {
size: 100,
fill: 0xFFFF00
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 300;
self.addChild(titleText);
var characterContainers = [];
var selectedCharacterIndex = -1;
self.createCharacters = function () {
// Calculate spacing based on number of characters
var totalWidth = 2048;
var spacing = 600; // Increased spacing from 350 to 600
var startX = totalWidth / 2 - (characters.length - 1) * spacing / 2;
var startY = 2732 / 2 - 200;
for (var i = 0; i < characters.length; i++) {
var characterContainer = new Container();
// Position characters horizontally next to each other
characterContainer.x = startX + i * spacing;
characterContainer.y = startY;
// Create character with image, name and description
var character = new Character(characters[i].name, characters[i].description, characters[i].imageAsset);
character.index = i;
character.difficultyLevel = characters[i].difficulty;
// Store character in container for reference
characterContainer.character = character;
// Add the character to the container
characterContainer.addChild(character);
// Implement setSelected method on the container
characterContainer.setSelected = function (isSelected) {
this.character.setSelected(isSelected);
};
characterContainer.down = function (x, y, obj) {
selectCharacter(this.character);
};
characterContainers.push(characterContainer);
self.addChild(characterContainer);
}
};
self.selectCharacter = function (character) {
// Deselect previous character
if (selectedCharacterIndex >= 0) {
characterContainers[selectedCharacterIndex].setSelected(false);
}
// Select new character
selectedCharacterIndex = character.index;
character.setSelected(true);
// Animate the selected character
tween(character, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(character, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
// Return selected character
return characters[selectedCharacterIndex];
};
return self;
});
var Paddle = Container.expand(function () {
var self = Container.call(this);
var paddleGraphics = self.attachAsset('paddle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
self.targetY = null;
self.isAI = false;
self.aiDifficulty = 0.5; // Default difficulty
// Give each paddle a unique ID for collision tracking
self.id = "paddle_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
self.update = function () {
if (self.isAI && ball) {
// AI paddle movement - follows the ball with some delay based on difficulty
// Higher difficulty means better tracking
if (Math.random() < self.aiDifficulty) {
self.targetY = ball.y;
}
if (self.targetY !== null) {
// Move toward target position with speed adjusted by difficulty
var moveSpeed = self.speed * self.aiDifficulty;
if (Math.abs(self.y - self.targetY) > moveSpeed) {
if (self.y < self.targetY) {
self.y += moveSpeed;
} else {
self.y -= moveSpeed;
}
} else {
self.y = self.targetY;
}
}
}
// Keep paddle within table bounds
var halfPaddleHeight = paddleGraphics.height / 2;
var tableTop = tableY - tableHeight / 2 + halfPaddleHeight;
var tableBottom = tableY + tableHeight / 2 - halfPaddleHeight;
if (self.y < tableTop) {
self.y = tableTop;
}
if (self.y > tableBottom) {
self.y = tableBottom;
}
};
return self;
});
var PaddleWithImage = Paddle.expand(function (characterImageAsset) {
var self = Paddle.call(this);
// Add character image on top of paddle
if (characterImageAsset) {
// Create a container for character image
var imageContainer = new Container();
self.addChild(imageContainer);
// Get the paddle's dimensions
var paddleWidth = 30; // Width of paddle
var paddleHeight = 150; // Height of paddle
// Add character image
var characterImage = LK.getAsset(characterImageAsset, {
anchorX: 0.5,
anchorY: 0.5
});
// Now that we have the image, set its scale to cover the paddle
if (characterImage && characterImage.width && characterImage.height) {
// Make character image much larger but keep paddle collision size
var imageWidth = paddleWidth * 8; // Make image 8x wider for better visibility
var imageHeight = paddleHeight * 3; // Triple the height of the image
characterImage.scale.x = imageWidth / characterImage.width;
characterImage.scale.y = imageHeight / characterImage.height;
}
// Position the image to be visible on paddle
characterImage.y = 0;
imageContainer.addChild(characterImage);
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a5276
});
/****
* Game Code
****/
// Game constants
var tableWidth = 1600;
var tableHeight = 1200;
var tableX = 2048 / 2;
var tableY = 2732 / 2;
// Game state variables
var gameState = "character_select"; // "character_select", "playing", "game_over"
var playerScore = 0;
var aiScore = 0;
var currentOpponent = 0;
var selectedCharacter = null;
var lastDragY = 0;
// Character data
var characters = [{
name: "Bombardiro Crocodilo",
description: "Bomber plane crocodile",
difficulty: 0.5,
imageAsset: 'bombardiro',
hitSound: 'hitBomba'
}, {
name: "Tung Tung Sahur",
description: "Wooden figure with a bat",
difficulty: 0.7,
imageAsset: 'tungsahur',
hitSound: 'hitTung'
}];
// Game elements
var table;
var net;
var playerPaddle;
var aiPaddle;
var ball;
var characterSelection = [];
var characterSelectScreen;
var playerScoreText;
var aiScoreText;
var statusText;
// Initialize the game
function initGame() {
// Play background music
LK.playMusic('gameMusic');
// Always reset status text to avoid overlap
if (statusText) {
game.removeChild(statusText);
statusText = null;
}
if (gameState === "character_select") {
// Set a different background color for character select screen
game.setBackgroundColor(0x6c3483); // Purple background for character select
// Create and display character selection screen
characterSelectScreen = new CharacterSelectScreen();
game.addChild(characterSelectScreen);
characterSelectScreen.createCharacters();
} else {
// Set game background color
game.setBackgroundColor(0x1a5276); // Blue background for gameplay
// Create table
table = LK.getAsset('table', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX,
y: tableY
});
game.addChild(table);
// Create net
net = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX,
y: tableY
});
game.addChild(net);
// Create score displays
var playerScoreBoard = LK.getAsset('scoreBoard', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX - tableWidth / 4,
y: tableY - tableHeight / 2 - 80
});
game.addChild(playerScoreBoard);
var aiScoreBoard = LK.getAsset('scoreBoard', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX + tableWidth / 4,
y: tableY - tableHeight / 2 - 80
});
game.addChild(aiScoreBoard);
playerScoreText = new Text2("0", {
size: 60,
fill: 0xFFFFFF
});
playerScoreText.anchor.set(0.5, 0.5);
playerScoreText.x = tableX - tableWidth / 4;
playerScoreText.y = tableY - tableHeight / 2 - 80;
game.addChild(playerScoreText);
aiScoreText = new Text2("0", {
size: 60,
fill: 0xFFFFFF
});
aiScoreText.anchor.set(0.5, 0.5);
aiScoreText.x = tableX + tableWidth / 4;
aiScoreText.y = tableY - tableHeight / 2 - 80;
game.addChild(aiScoreText);
// Don't create status text at initialization since we're in character select mode
}
}
function selectCharacter(character) {
// Don't select the current opponent character
if (character.index === currentOpponent) {
// Flash the character to indicate it can't be selected
tween(character, {
alpha: 0.4
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(character, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
return; // Don't proceed with selection
}
// If we already have a selected character, prevent selecting the same character
if (selectedCharacter && character.index === selectedCharacter.index) {
// Flash the character to indicate it can't be selected
tween(character, {
alpha: 0.4
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(character, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
return; // Don't proceed with selection
}
// Use the CharacterSelectScreen to handle selection
var selectedChar = characterSelectScreen.selectCharacter(character);
selectedCharacter = character;
// Make sure currentOpponent is properly set before checking it
if (currentOpponent === undefined || currentOpponent === null) {
currentOpponent = 0;
}
// Always set the opponent to Tralalero Tralalai (using index 0)
currentOpponent = 0;
// Start the game after a delay with animation
tween(character, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
LK.setTimeout(function () {
startGame();
}, 800);
}
});
}
function startGame() {
// Remove character selection screen with fade out
if (characterSelectScreen) {
tween(characterSelectScreen, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
game.removeChild(characterSelectScreen);
characterSelectScreen = null;
// Switch game state
gameState = "playing";
// Reinitialize the game with gameplay elements
game.setBackgroundColor(0x1a5276); // Blue background for gameplay
initGame();
// Setup game elements
playerScore = 0;
aiScore = 0;
currentOpponent = 0;
// Remove any existing balls to prevent ghost balls
if (game.children) {
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child instanceof Ball) {
game.removeChild(child);
}
}
}
// Remove any existing paddles to prevent duplicates
if (game.children) {
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child instanceof Paddle) {
game.removeChild(child);
}
}
}
// Create paddles with character images
playerPaddle = new PaddleWithImage(selectedCharacter.imageAsset);
playerPaddle.x = tableX - tableWidth / 2 + 50;
playerPaddle.y = tableY;
game.addChild(playerPaddle);
aiPaddle = new PaddleWithImage('tralalero');
aiPaddle.x = tableX + tableWidth / 2 - 50;
aiPaddle.y = tableY;
aiPaddle.isAI = true;
// Set AI difficulty based on Tralalero character
aiPaddle.aiDifficulty = 0.4;
console.log("AI Difficulty: " + aiPaddle.aiDifficulty);
game.addChild(aiPaddle);
// Always remove existing status text to prevent duplicates
if (statusText) {
game.removeChild(statusText);
}
// Create new status text
statusText = new Text2("VS Tralalero Tralalai", {
size: 80,
fill: 0xFFFFFF
});
statusText.anchor.set(0.5, 0.5);
statusText.x = tableX;
statusText.y = 200;
game.addChild(statusText);
// Make sure the status text is visible
statusText.alpha = 1;
// Create ball
ball = new Ball();
game.addChild(ball);
// Reset the ball after adding to game to ensure proper initialization
ball.reset();
updateScoreDisplay();
}
});
}
}
function updateScoreDisplay() {
playerScoreText.setText(playerScore.toString());
aiScoreText.setText(aiScore.toString());
}
function checkGameOver() {
if (playerScore >= 5) {
// Player wins this round
// Player has beaten the opponent
gameState = "game_over";
// Show victory
LK.setTimeout(function () {
if (statusText) {
statusText.setText("You are the Brainrot Pong Champion!");
}
}, 100);
LK.getSound('win').play();
LK.showYouWin();
} else if (aiScore >= 5) {
// AI wins
gameState = "game_over";
// Wait for game over screen before showing text
LK.setTimeout(function () {
if (statusText) {
statusText.setText("Game Over!");
}
}, 100);
LK.showGameOver();
}
}
function checkPaddleCollision(ball, paddle) {
if (!ball.active) {
return false;
} // Return false if ball not active
// Get actual paddle dimensions from the image if it exists
var paddleWidth;
var paddleHeight;
// Check if this is a PaddleWithImage with a character image
if (paddle.children && paddle.children.length > 0) {
// First child is the original paddle image, second might be image container
var imageContainer = paddle.children[1];
if (imageContainer && imageContainer.children && imageContainer.children.length > 0) {
var characterImage = imageContainer.children[0];
if (characterImage) {
paddleWidth = characterImage.width * characterImage.scale.x;
paddleHeight = characterImage.height * characterImage.scale.y;
}
}
}
// Fallback to default paddle dimensions if we couldn't find the image
if (!paddleWidth || !paddleHeight) {
paddleWidth = 30; // Default paddle width
paddleHeight = 150; // Default paddle height
}
// Define rectangular collision box for paddle
var paddleLeft = paddle.x - paddleWidth / 2;
var paddleRight = paddle.x + paddleWidth / 2;
var paddleTop = paddle.y - paddleHeight / 2;
var paddleBottom = paddle.y + paddleHeight / 2;
// Calculate ball position including radius for more accurate detection
var ballLeft = ball.x - ball.radius;
var ballRight = ball.x + ball.radius;
var ballTop = ball.y - ball.radius;
var ballBottom = ball.y + ball.radius;
// Calculate last ball position for trajectory analysis
var lastBallLeft = ball.lastX - ball.radius;
var lastBallRight = ball.lastX + ball.radius;
var lastBallTop = ball.lastY - ball.radius;
var lastBallBottom = ball.lastY + ball.radius;
// Check if ball is within paddle bounds
var isIntersecting = ballRight > paddleLeft && ballLeft < paddleRight && ballBottom > paddleTop && ballTop < paddleBottom;
// Check for fast-moving ball that might have teleported through paddle
// Check both horizontal and vertical trajectories for accurate detection
var crossedLeftToRight = lastBallRight <= paddleLeft && ballRight > paddleLeft && !(lastBallBottom < paddleTop || lastBallTop > paddleBottom) && !(ballBottom < paddleTop || ballTop > paddleBottom);
var crossedRightToLeft = lastBallLeft >= paddleRight && ballLeft < paddleRight && !(lastBallBottom < paddleTop || lastBallTop > paddleBottom) && !(ballBottom < paddleTop || ballTop > paddleBottom);
// Only process collision if ball is moving toward the paddle
var movingTowardLeftPaddle = ball.dx < 0 && paddle === playerPaddle;
var movingTowardRightPaddle = ball.dx > 0 && paddle === aiPaddle;
// Check if this paddle was already hit in this movement
var alreadyCollidedWithThisPaddle = ball.lastCollidedWith[paddle.id];
// Apply collision only on first contact, not for continuous contact
var justHitPaddle = (isIntersecting || crossedLeftToRight || crossedRightToLeft) && (movingTowardLeftPaddle && paddle === playerPaddle || movingTowardRightPaddle && paddle === aiPaddle) && !alreadyCollidedWithThisPaddle;
if (justHitPaddle) {
// Mark this paddle as having been collided with
ball.lastCollidedWith[paddle.id] = true;
// For left paddle (player)
if (paddle === playerPaddle) {
// Ball hit paddle from right
ball.x = paddleRight + ball.radius;
ball.dx = Math.abs(ball.dx);
// Adjust angle based on where ball hit the paddle
var relativeIntersectY = paddle.y - ball.y;
var normalizedRelativeIntersectionY = relativeIntersectY / (paddleHeight / 2);
var bounceAngle = normalizedRelativeIntersectionY * (Math.PI / 4); // 45 degrees max angle
// Adjust ball direction based on where it hit the paddle
var speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = Math.cos(bounceAngle) * speed;
ball.dy = -Math.sin(bounceAngle) * speed;
// Increase speed slightly with each hit but cap it
ball.dx = Math.min(Math.abs(ball.dx * 1.05), 25) * (ball.dx < 0 ? -1 : 1);
ball.dy = Math.min(Math.abs(ball.dy * 1.05), 25) * (ball.dy < 0 ? -1 : 1);
// Use player's selected character's hit sound if available, otherwise default hit sound
if (selectedCharacter && selectedCharacter.index !== undefined && characters[selectedCharacter.index].hitSound) {
LK.getSound(characters[selectedCharacter.index].hitSound).play();
} else {
LK.getSound('hit').play();
}
// For right paddle (AI)
} else if (paddle === aiPaddle) {
// Ball hit paddle from left
ball.x = paddleLeft - ball.radius;
ball.dx = -Math.abs(ball.dx);
// Adjust angle based on where ball hit the paddle
var relativeIntersectY = paddle.y - ball.y;
var normalizedRelativeIntersectionY = relativeIntersectY / (paddleHeight / 2);
var bounceAngle = normalizedRelativeIntersectionY * (Math.PI / 4); // 45 degrees max angle
// Adjust ball direction based on where it hit the paddle
var speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = -Math.cos(bounceAngle) * speed;
ball.dy = -Math.sin(bounceAngle) * speed;
// Increase speed slightly with each hit but cap it
ball.dx = Math.min(Math.abs(ball.dx * 1.05), 25) * (ball.dx < 0 ? -1 : 1);
ball.dy = Math.min(Math.abs(ball.dy * 1.05), 25) * (ball.dy < 0 ? -1 : 1);
// Always use Tralalero sound for AI paddle
LK.getSound('hitTrala').play();
}
// Return true to indicate collision happened
return true;
} else if (!justHitPaddle && !isIntersecting) {
// Clear collision flag when ball is no longer intersecting with this paddle
if (ball.lastCollidedWith[paddle.id]) {
ball.lastCollidedWith[paddle.id] = false;
}
}
// Return false if no collision happened
return false;
}
// Input handling
game.down = function (x, y, obj) {
if (gameState === "playing") {
lastDragY = y;
}
};
game.move = function (x, y, obj) {
if (gameState === "playing" && playerPaddle) {
var deltaY = y - lastDragY;
playerPaddle.y += deltaY;
lastDragY = y;
}
};
game.up = function (x, y, obj) {
// Release any dragging
};
// Game update function
game.update = function () {
if (gameState === "playing") {
if (playerPaddle) {
playerPaddle.update();
}
if (aiPaddle) {
aiPaddle.update();
}
if (ball) {
ball.update();
}
}
};
// Initialize the game
initGame(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGraphics = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10;
self.dx = 0;
self.dy = 0;
self.radius = ballGraphics.width / 2;
self.active = false;
self.lastX = 0;
self.lastY = 0;
self.lastCollidedWith = {}; // Track collisions with paddles
self.reset = function () {
// Clear any existing launch timer first to prevent ghost balls
if (self.launchTimer) {
LK.clearTimeout(self.launchTimer);
self.launchTimer = null;
}
// Reset position to center of table
self.x = tableX;
self.y = tableY;
// Make sure to set lastX and lastY to the same values to prevent false collisions
self.lastX = self.x;
self.lastY = self.y;
// Reset speed and direction vectors to 0
self.dx = 0;
self.dy = 0;
// Explicitly deactivate ball
self.active = false;
// Clear collision tracking completely
self.lastCollidedWith = {};
// Make sure this ball is the only active ball in the game
if (game && game.children) {
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child !== self && child instanceof Ball) {
// Remove any other balls from the game
game.removeChild(child);
}
}
}
// Schedule ball to start after a delay
self.launchTimer = LK.setTimeout(function () {
// Only launch if this ball is still in the game
if (self.parent) {
self.launch();
}
self.launchTimer = null;
}, 1000);
};
self.launch = function () {
// Only launch if ball is not active and is still attached to game to prevent multiple launches
if (self.active || !self.parent) {
return;
}
// Launch ball at a random angle toward player or ai
var angle = Math.random() * Math.PI / 4 - Math.PI / 8;
// Determine direction (left or right) - use exact Math.PI value
if (Math.random() < 0.5) {
angle += Math.PI;
}
// Set reasonable initial velocity
self.dx = Math.cos(angle) * self.speed;
self.dy = Math.sin(angle) * self.speed;
// Ensure lastX/lastY match current position before activating
self.lastX = self.x;
self.lastY = self.y;
// Clear collision tracking completely before activating
self.lastCollidedWith = {};
// Activate the ball
self.active = true;
// Cancel any pending launch timers to prevent duplicates
if (self.launchTimer) {
LK.clearTimeout(self.launchTimer);
self.launchTimer = null;
}
};
self.update = function () {
// Verify this ball is valid and active
if (!self.active || !self.parent) {
return;
}
// Verify this is the only active ball
if (game && game.children) {
var activeBalls = 0;
var thisBallFound = false;
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child instanceof Ball) {
activeBalls++;
if (child === self) {
thisBallFound = true;
}
}
}
// If this ball isn't found or there are multiple balls, reset
if (!thisBallFound || activeBalls > 1) {
// Remove extra balls and reset this one
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child instanceof Ball && child !== self) {
game.removeChild(child);
}
}
// If this is a ghost ball, remove it
if (!thisBallFound) {
return; // Exit update cycle for ghost ball
}
}
}
// Track last position for proper collision detection
self.lastX = self.x;
self.lastY = self.y;
// Limit speed to prevent teleportation issues
var maxSpeed = 25;
if (Math.abs(self.dx) > maxSpeed) {
self.dx = self.dx > 0 ? maxSpeed : -maxSpeed;
}
if (Math.abs(self.dy) > maxSpeed) {
self.dy = self.dy > 0 ? maxSpeed : -maxSpeed;
}
// Move ball
self.x += self.dx;
self.y += self.dy;
// Table boundaries
var tableTop = tableY - tableHeight / 2 + self.radius;
var tableBottom = tableY + tableHeight / 2 - self.radius;
// Bounce off top and bottom
if (self.y < tableTop) {
self.y = tableTop;
self.dy = -self.dy;
// Always use Tralalero sound
LK.getSound('hitTrala').play();
} else if (self.y > tableBottom) {
self.y = tableBottom;
self.dy = -self.dy;
// Always use Tralalero sound
LK.getSound('hitTrala').play();
}
// Check for paddle collisions first (returns true if collision happened)
var hitPlayerPaddle = checkPaddleCollision(self, playerPaddle);
var hitAiPaddle = checkPaddleCollision(self, aiPaddle);
// Only check for scoring if no paddle was hit this frame
if (!hitPlayerPaddle && !hitAiPaddle) {
// Check for scoring (left and right boundaries)
var tableLeft = tableX - tableWidth / 2;
var tableRight = tableX + tableWidth / 2;
// Track boundary crossing for accurate scoring
// Check if ball has just crossed the left boundary
if (self.lastX >= tableLeft - self.radius && self.x < tableLeft - self.radius) {
// AI scores - only if ball is active (wasn't already scored)
if (self.active) {
self.active = false;
aiScore++;
updateScoreDisplay();
LK.getSound('score').play();
checkGameOver();
self.reset();
}
}
// Check if ball has just crossed the right boundary
else if (self.lastX <= tableRight + self.radius && self.x > tableRight + self.radius) {
// Player scores - only if ball is active (wasn't already scored)
if (self.active) {
self.active = false;
playerScore++;
updateScoreDisplay();
LK.getSound('score').play();
checkGameOver();
self.reset();
}
}
}
};
return self;
});
var Character = Container.expand(function (name, description, imageAsset) {
var self = Container.call(this);
// Create the character box with image, name and description
var characterBox = new CharacterBox(imageAsset, name, description);
self.addChild(characterBox);
// Store properties
self.name = name || "Unknown Character";
self.description = description || "No description available";
self.imageAsset = imageAsset;
// Add bounce animation
self.startY = 0; // Initialize with 0, will be updated in first bounce
LK.setInterval(function () {
self.startY = self.y; // Update current position before animation
tween(self, {
y: self.startY - 15
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
y: self.startY
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}, 2000);
// Method to set selected state
self.setSelected = function (isSelected) {
characterBox.setSelected(isSelected);
};
// Handle click/tap
self.down = function (x, y, obj) {
selectCharacter(self);
};
return self;
});
var CharacterBox = Container.expand(function (imageAsset, name, description) {
var self = Container.call(this);
// Create box background
var characterBox = self.attachAsset('characterBox', {
anchorX: 0.5,
anchorY: 0.5
});
// Assign a unique color based on the character name's length
var colorOptions = [0xf4d03f, 0x3498db, 0xe74c3c, 0x2ecc71, 0x9b59b6, 0x1abc9c];
var colorIndex = name ? name.length % colorOptions.length : 0;
characterBox.tint = colorOptions[colorIndex];
// Store properties
self.characterName = name || "Unknown Character";
self.characterDescription = description || "No description available";
self.imageAsset = imageAsset;
// Add character image if specified
if (imageAsset) {
var characterImage = LK.getAsset(imageAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
characterImage.y = -10; // Position image in the center of the box
self.addChild(characterImage);
}
// Create empty container for description (kept to maintain layout structure)
var descContainer = new Container();
descContainer.y = -characterBox.height / 2 + 80;
self.addChild(descContainer);
// Method to set selected state
self.setSelected = function (isSelected) {
if (isSelected) {
characterBox.tint = 0xff0000;
} else {
// Get the original color from initialization
var colorOptions = [0xf4d03f, 0x3498db, 0xe74c3c, 0x2ecc71, 0x9b59b6, 0x1abc9c];
var colorIndex = self.characterName ? self.characterName.length % colorOptions.length : 0;
characterBox.tint = colorOptions[colorIndex]; // Restore original color
}
};
// Handle click/tap
self.down = function (x, y, obj) {
selectCharacter(self);
};
return self;
});
var CharacterSelectScreen = Container.expand(function () {
var self = Container.call(this);
var titleText = new Text2("CHOOSE YOUR WARRIOR", {
size: 100,
fill: 0xFFFF00
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 300;
self.addChild(titleText);
var characterContainers = [];
var selectedCharacterIndex = -1;
self.createCharacters = function () {
// Calculate spacing based on number of characters
var totalWidth = 2048;
var spacing = 600; // Increased spacing from 350 to 600
var startX = totalWidth / 2 - (characters.length - 1) * spacing / 2;
var startY = 2732 / 2 - 200;
for (var i = 0; i < characters.length; i++) {
var characterContainer = new Container();
// Position characters horizontally next to each other
characterContainer.x = startX + i * spacing;
characterContainer.y = startY;
// Create character with image, name and description
var character = new Character(characters[i].name, characters[i].description, characters[i].imageAsset);
character.index = i;
character.difficultyLevel = characters[i].difficulty;
// Store character in container for reference
characterContainer.character = character;
// Add the character to the container
characterContainer.addChild(character);
// Implement setSelected method on the container
characterContainer.setSelected = function (isSelected) {
this.character.setSelected(isSelected);
};
characterContainer.down = function (x, y, obj) {
selectCharacter(this.character);
};
characterContainers.push(characterContainer);
self.addChild(characterContainer);
}
};
self.selectCharacter = function (character) {
// Deselect previous character
if (selectedCharacterIndex >= 0) {
characterContainers[selectedCharacterIndex].setSelected(false);
}
// Select new character
selectedCharacterIndex = character.index;
character.setSelected(true);
// Animate the selected character
tween(character, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(character, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
// Return selected character
return characters[selectedCharacterIndex];
};
return self;
});
var Paddle = Container.expand(function () {
var self = Container.call(this);
var paddleGraphics = self.attachAsset('paddle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
self.targetY = null;
self.isAI = false;
self.aiDifficulty = 0.5; // Default difficulty
// Give each paddle a unique ID for collision tracking
self.id = "paddle_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
self.update = function () {
if (self.isAI && ball) {
// AI paddle movement - follows the ball with some delay based on difficulty
// Higher difficulty means better tracking
if (Math.random() < self.aiDifficulty) {
self.targetY = ball.y;
}
if (self.targetY !== null) {
// Move toward target position with speed adjusted by difficulty
var moveSpeed = self.speed * self.aiDifficulty;
if (Math.abs(self.y - self.targetY) > moveSpeed) {
if (self.y < self.targetY) {
self.y += moveSpeed;
} else {
self.y -= moveSpeed;
}
} else {
self.y = self.targetY;
}
}
}
// Keep paddle within table bounds
var halfPaddleHeight = paddleGraphics.height / 2;
var tableTop = tableY - tableHeight / 2 + halfPaddleHeight;
var tableBottom = tableY + tableHeight / 2 - halfPaddleHeight;
if (self.y < tableTop) {
self.y = tableTop;
}
if (self.y > tableBottom) {
self.y = tableBottom;
}
};
return self;
});
var PaddleWithImage = Paddle.expand(function (characterImageAsset) {
var self = Paddle.call(this);
// Add character image on top of paddle
if (characterImageAsset) {
// Create a container for character image
var imageContainer = new Container();
self.addChild(imageContainer);
// Get the paddle's dimensions
var paddleWidth = 30; // Width of paddle
var paddleHeight = 150; // Height of paddle
// Add character image
var characterImage = LK.getAsset(characterImageAsset, {
anchorX: 0.5,
anchorY: 0.5
});
// Now that we have the image, set its scale to cover the paddle
if (characterImage && characterImage.width && characterImage.height) {
// Make character image much larger but keep paddle collision size
var imageWidth = paddleWidth * 8; // Make image 8x wider for better visibility
var imageHeight = paddleHeight * 3; // Triple the height of the image
characterImage.scale.x = imageWidth / characterImage.width;
characterImage.scale.y = imageHeight / characterImage.height;
}
// Position the image to be visible on paddle
characterImage.y = 0;
imageContainer.addChild(characterImage);
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a5276
});
/****
* Game Code
****/
// Game constants
var tableWidth = 1600;
var tableHeight = 1200;
var tableX = 2048 / 2;
var tableY = 2732 / 2;
// Game state variables
var gameState = "character_select"; // "character_select", "playing", "game_over"
var playerScore = 0;
var aiScore = 0;
var currentOpponent = 0;
var selectedCharacter = null;
var lastDragY = 0;
// Character data
var characters = [{
name: "Bombardiro Crocodilo",
description: "Bomber plane crocodile",
difficulty: 0.5,
imageAsset: 'bombardiro',
hitSound: 'hitBomba'
}, {
name: "Tung Tung Sahur",
description: "Wooden figure with a bat",
difficulty: 0.7,
imageAsset: 'tungsahur',
hitSound: 'hitTung'
}];
// Game elements
var table;
var net;
var playerPaddle;
var aiPaddle;
var ball;
var characterSelection = [];
var characterSelectScreen;
var playerScoreText;
var aiScoreText;
var statusText;
// Initialize the game
function initGame() {
// Play background music
LK.playMusic('gameMusic');
// Always reset status text to avoid overlap
if (statusText) {
game.removeChild(statusText);
statusText = null;
}
if (gameState === "character_select") {
// Set a different background color for character select screen
game.setBackgroundColor(0x6c3483); // Purple background for character select
// Create and display character selection screen
characterSelectScreen = new CharacterSelectScreen();
game.addChild(characterSelectScreen);
characterSelectScreen.createCharacters();
} else {
// Set game background color
game.setBackgroundColor(0x1a5276); // Blue background for gameplay
// Create table
table = LK.getAsset('table', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX,
y: tableY
});
game.addChild(table);
// Create net
net = LK.getAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX,
y: tableY
});
game.addChild(net);
// Create score displays
var playerScoreBoard = LK.getAsset('scoreBoard', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX - tableWidth / 4,
y: tableY - tableHeight / 2 - 80
});
game.addChild(playerScoreBoard);
var aiScoreBoard = LK.getAsset('scoreBoard', {
anchorX: 0.5,
anchorY: 0.5,
x: tableX + tableWidth / 4,
y: tableY - tableHeight / 2 - 80
});
game.addChild(aiScoreBoard);
playerScoreText = new Text2("0", {
size: 60,
fill: 0xFFFFFF
});
playerScoreText.anchor.set(0.5, 0.5);
playerScoreText.x = tableX - tableWidth / 4;
playerScoreText.y = tableY - tableHeight / 2 - 80;
game.addChild(playerScoreText);
aiScoreText = new Text2("0", {
size: 60,
fill: 0xFFFFFF
});
aiScoreText.anchor.set(0.5, 0.5);
aiScoreText.x = tableX + tableWidth / 4;
aiScoreText.y = tableY - tableHeight / 2 - 80;
game.addChild(aiScoreText);
// Don't create status text at initialization since we're in character select mode
}
}
function selectCharacter(character) {
// Don't select the current opponent character
if (character.index === currentOpponent) {
// Flash the character to indicate it can't be selected
tween(character, {
alpha: 0.4
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(character, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
return; // Don't proceed with selection
}
// If we already have a selected character, prevent selecting the same character
if (selectedCharacter && character.index === selectedCharacter.index) {
// Flash the character to indicate it can't be selected
tween(character, {
alpha: 0.4
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(character, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
return; // Don't proceed with selection
}
// Use the CharacterSelectScreen to handle selection
var selectedChar = characterSelectScreen.selectCharacter(character);
selectedCharacter = character;
// Make sure currentOpponent is properly set before checking it
if (currentOpponent === undefined || currentOpponent === null) {
currentOpponent = 0;
}
// Always set the opponent to Tralalero Tralalai (using index 0)
currentOpponent = 0;
// Start the game after a delay with animation
tween(character, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
LK.setTimeout(function () {
startGame();
}, 800);
}
});
}
function startGame() {
// Remove character selection screen with fade out
if (characterSelectScreen) {
tween(characterSelectScreen, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
game.removeChild(characterSelectScreen);
characterSelectScreen = null;
// Switch game state
gameState = "playing";
// Reinitialize the game with gameplay elements
game.setBackgroundColor(0x1a5276); // Blue background for gameplay
initGame();
// Setup game elements
playerScore = 0;
aiScore = 0;
currentOpponent = 0;
// Remove any existing balls to prevent ghost balls
if (game.children) {
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child instanceof Ball) {
game.removeChild(child);
}
}
}
// Remove any existing paddles to prevent duplicates
if (game.children) {
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child instanceof Paddle) {
game.removeChild(child);
}
}
}
// Create paddles with character images
playerPaddle = new PaddleWithImage(selectedCharacter.imageAsset);
playerPaddle.x = tableX - tableWidth / 2 + 50;
playerPaddle.y = tableY;
game.addChild(playerPaddle);
aiPaddle = new PaddleWithImage('tralalero');
aiPaddle.x = tableX + tableWidth / 2 - 50;
aiPaddle.y = tableY;
aiPaddle.isAI = true;
// Set AI difficulty based on Tralalero character
aiPaddle.aiDifficulty = 0.4;
console.log("AI Difficulty: " + aiPaddle.aiDifficulty);
game.addChild(aiPaddle);
// Always remove existing status text to prevent duplicates
if (statusText) {
game.removeChild(statusText);
}
// Create new status text
statusText = new Text2("VS Tralalero Tralalai", {
size: 80,
fill: 0xFFFFFF
});
statusText.anchor.set(0.5, 0.5);
statusText.x = tableX;
statusText.y = 200;
game.addChild(statusText);
// Make sure the status text is visible
statusText.alpha = 1;
// Create ball
ball = new Ball();
game.addChild(ball);
// Reset the ball after adding to game to ensure proper initialization
ball.reset();
updateScoreDisplay();
}
});
}
}
function updateScoreDisplay() {
playerScoreText.setText(playerScore.toString());
aiScoreText.setText(aiScore.toString());
}
function checkGameOver() {
if (playerScore >= 5) {
// Player wins this round
// Player has beaten the opponent
gameState = "game_over";
// Show victory
LK.setTimeout(function () {
if (statusText) {
statusText.setText("You are the Brainrot Pong Champion!");
}
}, 100);
LK.getSound('win').play();
LK.showYouWin();
} else if (aiScore >= 5) {
// AI wins
gameState = "game_over";
// Wait for game over screen before showing text
LK.setTimeout(function () {
if (statusText) {
statusText.setText("Game Over!");
}
}, 100);
LK.showGameOver();
}
}
function checkPaddleCollision(ball, paddle) {
if (!ball.active) {
return false;
} // Return false if ball not active
// Get actual paddle dimensions from the image if it exists
var paddleWidth;
var paddleHeight;
// Check if this is a PaddleWithImage with a character image
if (paddle.children && paddle.children.length > 0) {
// First child is the original paddle image, second might be image container
var imageContainer = paddle.children[1];
if (imageContainer && imageContainer.children && imageContainer.children.length > 0) {
var characterImage = imageContainer.children[0];
if (characterImage) {
paddleWidth = characterImage.width * characterImage.scale.x;
paddleHeight = characterImage.height * characterImage.scale.y;
}
}
}
// Fallback to default paddle dimensions if we couldn't find the image
if (!paddleWidth || !paddleHeight) {
paddleWidth = 30; // Default paddle width
paddleHeight = 150; // Default paddle height
}
// Define rectangular collision box for paddle
var paddleLeft = paddle.x - paddleWidth / 2;
var paddleRight = paddle.x + paddleWidth / 2;
var paddleTop = paddle.y - paddleHeight / 2;
var paddleBottom = paddle.y + paddleHeight / 2;
// Calculate ball position including radius for more accurate detection
var ballLeft = ball.x - ball.radius;
var ballRight = ball.x + ball.radius;
var ballTop = ball.y - ball.radius;
var ballBottom = ball.y + ball.radius;
// Calculate last ball position for trajectory analysis
var lastBallLeft = ball.lastX - ball.radius;
var lastBallRight = ball.lastX + ball.radius;
var lastBallTop = ball.lastY - ball.radius;
var lastBallBottom = ball.lastY + ball.radius;
// Check if ball is within paddle bounds
var isIntersecting = ballRight > paddleLeft && ballLeft < paddleRight && ballBottom > paddleTop && ballTop < paddleBottom;
// Check for fast-moving ball that might have teleported through paddle
// Check both horizontal and vertical trajectories for accurate detection
var crossedLeftToRight = lastBallRight <= paddleLeft && ballRight > paddleLeft && !(lastBallBottom < paddleTop || lastBallTop > paddleBottom) && !(ballBottom < paddleTop || ballTop > paddleBottom);
var crossedRightToLeft = lastBallLeft >= paddleRight && ballLeft < paddleRight && !(lastBallBottom < paddleTop || lastBallTop > paddleBottom) && !(ballBottom < paddleTop || ballTop > paddleBottom);
// Only process collision if ball is moving toward the paddle
var movingTowardLeftPaddle = ball.dx < 0 && paddle === playerPaddle;
var movingTowardRightPaddle = ball.dx > 0 && paddle === aiPaddle;
// Check if this paddle was already hit in this movement
var alreadyCollidedWithThisPaddle = ball.lastCollidedWith[paddle.id];
// Apply collision only on first contact, not for continuous contact
var justHitPaddle = (isIntersecting || crossedLeftToRight || crossedRightToLeft) && (movingTowardLeftPaddle && paddle === playerPaddle || movingTowardRightPaddle && paddle === aiPaddle) && !alreadyCollidedWithThisPaddle;
if (justHitPaddle) {
// Mark this paddle as having been collided with
ball.lastCollidedWith[paddle.id] = true;
// For left paddle (player)
if (paddle === playerPaddle) {
// Ball hit paddle from right
ball.x = paddleRight + ball.radius;
ball.dx = Math.abs(ball.dx);
// Adjust angle based on where ball hit the paddle
var relativeIntersectY = paddle.y - ball.y;
var normalizedRelativeIntersectionY = relativeIntersectY / (paddleHeight / 2);
var bounceAngle = normalizedRelativeIntersectionY * (Math.PI / 4); // 45 degrees max angle
// Adjust ball direction based on where it hit the paddle
var speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = Math.cos(bounceAngle) * speed;
ball.dy = -Math.sin(bounceAngle) * speed;
// Increase speed slightly with each hit but cap it
ball.dx = Math.min(Math.abs(ball.dx * 1.05), 25) * (ball.dx < 0 ? -1 : 1);
ball.dy = Math.min(Math.abs(ball.dy * 1.05), 25) * (ball.dy < 0 ? -1 : 1);
// Use player's selected character's hit sound if available, otherwise default hit sound
if (selectedCharacter && selectedCharacter.index !== undefined && characters[selectedCharacter.index].hitSound) {
LK.getSound(characters[selectedCharacter.index].hitSound).play();
} else {
LK.getSound('hit').play();
}
// For right paddle (AI)
} else if (paddle === aiPaddle) {
// Ball hit paddle from left
ball.x = paddleLeft - ball.radius;
ball.dx = -Math.abs(ball.dx);
// Adjust angle based on where ball hit the paddle
var relativeIntersectY = paddle.y - ball.y;
var normalizedRelativeIntersectionY = relativeIntersectY / (paddleHeight / 2);
var bounceAngle = normalizedRelativeIntersectionY * (Math.PI / 4); // 45 degrees max angle
// Adjust ball direction based on where it hit the paddle
var speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
ball.dx = -Math.cos(bounceAngle) * speed;
ball.dy = -Math.sin(bounceAngle) * speed;
// Increase speed slightly with each hit but cap it
ball.dx = Math.min(Math.abs(ball.dx * 1.05), 25) * (ball.dx < 0 ? -1 : 1);
ball.dy = Math.min(Math.abs(ball.dy * 1.05), 25) * (ball.dy < 0 ? -1 : 1);
// Always use Tralalero sound for AI paddle
LK.getSound('hitTrala').play();
}
// Return true to indicate collision happened
return true;
} else if (!justHitPaddle && !isIntersecting) {
// Clear collision flag when ball is no longer intersecting with this paddle
if (ball.lastCollidedWith[paddle.id]) {
ball.lastCollidedWith[paddle.id] = false;
}
}
// Return false if no collision happened
return false;
}
// Input handling
game.down = function (x, y, obj) {
if (gameState === "playing") {
lastDragY = y;
}
};
game.move = function (x, y, obj) {
if (gameState === "playing" && playerPaddle) {
var deltaY = y - lastDragY;
playerPaddle.y += deltaY;
lastDragY = y;
}
};
game.up = function (x, y, obj) {
// Release any dragging
};
// Game update function
game.update = function () {
if (gameState === "playing") {
if (playerPaddle) {
playerPaddle.update();
}
if (aiPaddle) {
aiPaddle.update();
}
if (ball) {
ball.update();
}
}
};
// Initialize the game
initGame();
shark with nikes on the beach. In-Game asset. 2d. High contrast. No shadows
bomber plane crocodile. In-Game asset. 2d. High contrast. No shadows
anthropomorphic wooden figure holding a baseball bat. In-Game asset. 2d. High contrast. No shadows
ping pong table only from the top with a face on it. In-Game asset. 2d. High contrast. No shadows
ball with face. In-Game asset. 2d. High contrast. No shadows