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