/**** * 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