User prompt
Ping Pong Rhythm Smash
Initial prompt
Ping Pong Game Description for Upit AI: Create a simple 2D ping pong (table tennis) game with the following features: Game Setup: A 2D ping pong table with two paddles (player and opponent) and a ball. Player Controls: The player controls the paddle using left and right arrow keys (or optionally mouse). The opponent paddle moves automatically, tracking the ball’s horizontal position. Ball Mechanics: The ball moves continuously and bounces off the paddles and table edges. When the ball hits a paddle, it changes direction and speeds up slightly. The ball also slightly changes direction and speed in sync with music beats (for example, every second). This creates a “dancing ball” effect tied to the rhythm. Music Synchronization: The ball’s speed and direction react to music beats or rhythm, making the gameplay rhythmic and dynamic. Scoring System: Keep track of the player’s and opponent’s scores. Scores update when the ball passes beyond a paddle. Visuals and UI: Display the current scores on screen. Simple but clear 2D graphics for the paddles, ball, and table.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Ball class var Ball = Container.expand(function () { var self = Container.call(this); var ballSprite = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); // Ball velocity self.vx = 0; self.vy = 0; // Ball speed (pixels per tick) self.speed = 18; // Used for rhythm sync self.baseSpeed = 18; self.rhythmBoost = 0; // Used to prevent multiple scoring on one pass self.lastScored = false; // Ball update self.update = function () { self.x += self.vx * (self.speed + self.rhythmBoost); self.y += self.vy * (self.speed + self.rhythmBoost); }; // Reset ball to center, random direction self.reset = function (direction) { self.x = 2048 / 2; self.y = 2732 / 2; // Start ball slow, then accelerate after 1 second self.speed = 4; self.rhythmBoost = 0; LK.setTimeout(function () { // Only accelerate if this ball is still in play and hasn't been reset again if (self && typeof self.speed !== "undefined" && self.speed === 4) { self.speed = self.baseSpeed; } }, 1000); // --- Fakeout logic --- // direction: 1 = to player, -1 = to AI // Sometimes (30% chance) the ball will "fake" left or right before going the real direction // Sometimes (20% chance) the ball will "fake" up or down before going the real direction // Rarely (5% chance), the ball will go to a completely random direction var rareRandomChance = 0.05; var fakeoutLRChance = 0.3; var fakeoutUDChance = 0.2; var doRareRandom = Math.random() < rareRandomChance; var doFakeoutLR = !doRareRandom && Math.random() < fakeoutLRChance; var doFakeoutUD = !doRareRandom && !doFakeoutLR && Math.random() < fakeoutUDChance; // Only one fakeout at a time var fakeoutDuration = 220; // ms var realAngle, fakeAngle; if (doRareRandom) { // Ball goes in a completely random direction (but not straight up/down) var angle = Math.random() * Math.PI * 2; // Avoid perfectly vertical if (Math.abs(Math.cos(angle)) < 0.2) { angle += 0.3; } self.vx = Math.sin(angle); self.vy = Math.cos(angle); } else if (doFakeoutLR) { // Left/right fakeout (original logic) var goLeft = Math.random() < 0.5; realAngle = (goLeft ? -0.35 : 0.35) + (direction === 1 ? Math.PI / 2 : -Math.PI / 2); fakeAngle = (goLeft ? 0.35 : -0.35) + (direction === 1 ? Math.PI / 2 : -Math.PI / 2); self.vx = Math.sin(fakeAngle); self.vy = Math.cos(fakeAngle) * direction; LK.setTimeout(function () { if (self && typeof self.vx !== "undefined" && typeof self.vy !== "undefined") { self.vx = Math.sin(realAngle); self.vy = Math.cos(realAngle) * direction; } }, fakeoutDuration); } else if (doFakeoutUD) { // Up/down fakeout // Ball will appear to go up (or down) for a moment, then go the real direction var goUpFirst = Math.random() < 0.5; // Real angle: normal random angle toward the correct side realAngle = Math.random() * 0.5 - 0.25 + (direction === 1 ? Math.PI / 2 : -Math.PI / 2); // Fake angle: sharply up or down, but not toward the goal if (goUpFirst) { // Fake up: angle close to 0 (straight up) fakeAngle = -Math.PI / 2 + (Math.random() - 0.5) * 0.2; } else { // Fake down: angle close to PI (straight down) fakeAngle = Math.PI / 2 + (Math.random() - 0.5) * 0.2; } self.vx = Math.sin(fakeAngle); self.vy = Math.cos(fakeAngle) * (goUpFirst ? -1 : 1); LK.setTimeout(function () { if (self && typeof self.vx !== "undefined" && typeof self.vy !== "undefined") { self.vx = Math.sin(realAngle); self.vy = Math.cos(realAngle) * direction; } }, fakeoutDuration); } else { // Normal random angle var angle = Math.random() * 0.5 - 0.25 + (direction === 1 ? Math.PI / 2 : -Math.PI / 2); self.vx = Math.sin(angle); self.vy = Math.cos(angle) * direction; } self.lastScored = false; }; return self; }); // LongNote class: 5 musical notes in a fixed, centered, circular formation, spinning as one, with vertical and horizontal bars attached var LongNote = Container.expand(function () { var self = Container.call(this); var noteColor = 0x00e6e6; var noteCount = 5; var radius = 180; // distance from center to each note var noteScale = 2.5; self.noteSprites = []; // Place 5 notes in a circle, evenly spaced for (var i = 0; i < noteCount; i++) { var angle = Math.PI * 2 / noteCount * i; var x = Math.cos(angle) * radius; var y = Math.sin(angle) * radius; var note = self.attachAsset('musical_note', { anchorX: 0.5, anchorY: 0.5, tint: noteColor, scaleX: noteScale, scaleY: noteScale, x: x, y: y }); self.noteSprites.push(note); } // Add vertical and horizontal bars (long rectangles) attached to the center // Vertical bar var barColor = 0x00e6e6; var barThickness = 38; var barLength = radius * 2 + 110 * noteScale * 0.7; // slightly shorter than full diameter self.verticalBar = self.attachAsset('net', { anchorX: 0.5, anchorY: 0.5, width: barThickness, height: barLength, tint: barColor, x: 0, y: 0 }); // Horizontal bar self.horizontalBar = self.attachAsset('net', { anchorX: 0.5, anchorY: 0.5, width: barLength, height: barThickness, tint: barColor, x: 0, y: 0 }); // Set bounding box for collision (encompass all notes and bars) self.width = barLength; self.height = barLength; self.rotationTween = null; self.isRotating = false; self.duration = 1800; // 30s at 60fps self.timer = 0; self.hit = false; // Center in the middle of the table self.x = 2048 / 2; self.y = 2732 / 2; // Play 'sesar' sound immediately when LongNote appears (only once, not on update) if (typeof LK.getSound === "function" && LK.getSound('sesar')) { LK.getSound('sesar').play(); self.sesarPlayed = true; } // Mute background music when LongNote appears if (typeof LK.stopMusic === "function") { LK.stopMusic(); } // Start continuous rotation for 30s self.startRotation = function () { if (self.isRotating) return; self.isRotating = true; // Use tween to rotate 360deg (2*PI) every 2s, repeat for 30s var _doSpin = function doSpin() { if (!self.isRotating) return; self.rotation = 0; self.rotationTween = tween(self, { rotation: Math.PI * 2 }, { duration: 2000, easing: tween.linear, onFinish: _doSpin }); }; _doSpin(); }; self.stopRotation = function () { self.isRotating = false; if (self.rotationTween) { tween.stop(self, { rotation: true }); self.rotationTween = null; } self.rotation = 0; }; self.update = function () { self.timer++; if (!self.isRotating) self.startRotation(); // Remove after 30s if (self.timer > self.duration) { self.stopRotation(); // Unmute/return background music when LongNote disappears if (typeof LK.playMusic === "function") { LK.playMusic('Backround_Music'); } if (self.parent) self.parent.removeChild(self); if (typeof longNoteInstance !== "undefined") longNoteInstance = null; } // If already hit, do not check collision again if (self.hit) return; // Defensive: check ball exists // Check collision with the whole LongNote (including bars) if (typeof ball !== "undefined" && ball && rectsIntersect(self, ball)) { self.hit = true; // Reverse ball direction ball.vx *= -1; ball.vy *= -1; // Do not play 'sesar' sound again on hit (only play on creation) // Continue rotating for the rest of 30s (do not remove) } }; return self; }); // Paddle class var Paddle = Container.expand(function () { var self = Container.call(this); // Set in init self.isPlayer = false; // Attach asset var paddleSprite = self.attachAsset('paddle_player', { anchorX: 0.5, anchorY: 0.5 }); // Set color for AI self.setAI = function () { paddleSprite.destroy(); self.attachAsset('paddle_ai', { anchorX: 0.5, anchorY: 0.5 }); self.isPlayer = false; }; // Set color for player self.setPlayer = function () { paddleSprite.destroy(); self.attachAsset('paddle_player', { anchorX: 0.5, anchorY: 0.5 }); self.isPlayer = true; }; // Clamp paddle inside table self.clamp = function () { var halfW = self.width / 2; if (self.x < halfW) { self.x = halfW; } if (self.x > 2048 - halfW) { self.x = 2048 - halfW; } }; return self; }); // PowerUp class var PowerUp = Container.expand(function () { var self = Container.call(this); // Types: 'big_paddle', 'multi_ball', 'slow_ball', 'speed_ball', 'blur_screen', 'invert_screen', 'reverse_ball' self.type = 'big_paddle'; self.active = false; // Randomly pick a type if not set if (arguments.length > 0 && typeof arguments[0] === "string") { self.type = arguments[0]; } else { var types = ['big_paddle', 'multi_ball', 'slow_ball', 'speed_ball', 'blur_screen', 'invert_screen', 'reverse_ball']; self.type = types[Math.floor(Math.random() * types.length)]; } // Attach musical note asset for all power-ups // Use updated musical_note color scheme for each power-up type var noteColor = 0x00e6e6; // default: cyan blue for musical notes if (self.type === 'big_paddle') { noteColor = 0x99ff99; // green (EASY) } if (self.type === 'multi_ball') { noteColor = 0xffcc33; // orange-yellow (new for multi_ball) } if (self.type === 'slow_ball') { noteColor = 0x6699ff; // blue (new for slow_ball) } if (self.type === 'speed_ball') { noteColor = 0xff3366; // pink-red (new for speed_ball) } if (self.type === 'blur_screen') { noteColor = 0xbebebe; // gray (blur_screen) } if (self.type === 'invert_screen') { noteColor = 0xbb66ff; // purple (invert_screen) } if (self.type === 'reverse_ball') { noteColor = 0xffff00; // yellow (reverse_ball) } // Use 'musical_note' asset (ellipse with note color, or a dedicated note asset if available) var powerupSprite = self.attachAsset('musical_note', { anchorX: 0.5, anchorY: 0.5, tint: noteColor, scaleX: 2.0, scaleY: 2.0 }); // Set size for collision self.width = powerupSprite.width * 2.0; self.height = powerupSprite.height * 2.0; // PowerUp update (spin and move to random places) self.floatDir = 1; self.floatTimer = 0; self.targetX = self.x; self.targetY = self.y; self.moveTimer = 0; self.rotationSpeed = 0.07 + Math.random() * 0.07; // random spin speed self.update = function () { self.floatTimer++; // Spin the note if (powerupSprite && typeof powerupSprite.rotation === "number") { powerupSprite.rotation += self.rotationSpeed; } // Move toward target position var moveSpeed = 1.2; // slow movement if (typeof self.targetX === "number" && typeof self.targetY === "number") { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > moveSpeed) { self.x += dx / dist * moveSpeed; self.y += dy / dist * moveSpeed; } else { self.x = self.targetX; self.y = self.targetY; } } // Every 120-240 frames, pick a new random target position self.moveTimer++; if (self.moveTimer > 120 + Math.floor(Math.random() * 120)) { self.moveTimer = 0; // Stay within table bounds var margin = 120; self.targetX = margin + Math.random() * (2048 - 2 * margin); self.targetY = 2732 / 2 - 400 + Math.random() * 800; } // Float up/down for a little extra effect self.y += Math.sin(self.floatTimer / 20) * 0.8; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x228B22 // Table green }); /**** * Game Code ****/ // --- Language Selection System --- // Localization dictionary var LANGUAGES = { en: { title: "PING PONG RHYTHM SMASH", easy: "EASY", normal: "NORMAL", hard: "HARD", howToPlay: "HOW TO PLAY", powerUps: "Power-Ups", close: "CLOSE", "return": "RETURN", powerUpsTitle: "Power-Ups", // Power-up descriptions big_paddle: "BIG PADDLE\nBoth paddles grow much larger for 7 seconds. Making it easier to hit the ball.", multi_ball: "MULTI BALL\nA second ball appears for 7 seconds. Score with either ball to earn points!", slow_ball: "SLOW BALL\nThe ball moves at half speed for 7 seconds. Giving you more time to react.", speed_ball: "SPEED BALL\nThe ball moves much faster for 7 seconds. Can you keep up?", blur_screen: "BLUR SCREEN\nThe whole game blurs for 7 seconds. Try to play through the haze!", invert_screen: "INVERT SCREEN\nThe entire game flips upside down for 7 seconds. Can you play inverted?", reverse_ball: "REVERSE BALL\nThe ball instantly reverses direction! Surprise your opponent.", howToPlayText: "How to Play\n\n- Drag your paddle left/right to hit the ball.\n- The ball moves in sync with the music beat. Watch for the rhythm boosts!\n- Score points by getting the ball past the AI paddle at the top.\n- First to 7 points wins the match.\n- Collect musical note power-ups for wild effects: big paddles, multi-ball, speed changes, and more!\n- Sometimes, a spinning Long Note appears. It bounces the ball back and changes the music—hit it only once per appearance!\n- The AI learns from where you hit the ball on your paddle. Mix up your shots to stay unpredictable!\n- Choose your difficulty: higher levels make the AI faster and smarter.\n- Good luck, and keep the beat!", language: "Language", english: "English", turkish: "Türkçe" }, tr: { title: "RİTİM PİNG PONG", easy: "KOLAY", normal: "ORTA", hard: "ZOR", howToPlay: "NASIL OYNANIR", powerUps: "Güçlendiriciler", close: "KAPAT", "return": "GERİ DÖN", powerUpsTitle: "Güçlendiriciler", big_paddle: "BÜYÜK RAKET\nHer iki raket de 7 saniyeliğine büyür. Topa vurmak daha kolay!", multi_ball: "ÇOKLU TOP\n7 saniyeliğine ikinci bir top çıkar. Her iki topla da puan kazanabilirsin!", slow_ball: "YAVAŞ TOP\nTop 7 saniyeliğine yarı hızda hareket eder. Tepki vermek için daha fazla zaman!", speed_ball: "HIZLI TOP\nTop 7 saniyeliğine çok daha hızlı hareket eder. Yetişebilecek misin?", blur_screen: "BULANIK EKRAN\nOyun 7 saniyeliğine bulanıklaşır. Sis içinde oynamayı dene!", invert_screen: "TERS EKRAN\nOyun 7 saniyeliğine baş aşağı döner. Ters oynayabilir misin?", reverse_ball: "TERS TOP\nTop anında yön değiştirir! Rakibini şaşırt.", howToPlayText: "Nasıl Oynanır\n\n- Raketini sola/sağa sürükleyerek topa vur.\n- Top, müzik ritmiyle uyumlu hareket eder. Ritim güçlendirmelerine dikkat et!\n- Topu AI raketinin arkasına geçirerek puan kazan.\n- 7 puana ilk ulaşan maçı kazanır.\n- Müzikal nota güçlendiricilerini topla: büyük raket, çoklu top, hız değişimi ve daha fazlası!\n- Bazen dönen Uzun Nota çıkar. Topu geri sektirir ve müziği değiştirir—her çıkışında sadece bir kez vur!\n- AI, topa raketinin neresinden vurduğunu öğrenir. Vuruşlarını çeşitlendir!\n- Zorluk seç: daha yüksek seviyelerde AI daha hızlı ve akıllı olur.\n- Bol şans, ritmi yakala!", language: "Dil", english: "İngilizce", turkish: "Türkçe" } }; var currentLang = "en"; // Default language // Helper to get localized string function t(key) { if (LANGUAGES[currentLang] && LANGUAGES[currentLang][key]) return LANGUAGES[currentLang][key]; if (LANGUAGES["en"][key]) return LANGUAGES["en"][key]; return key; } // --- Main Menu and Difficulty Selection --- var menuContainer = new Container(); var menuBg = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0, tint: 0x1a1a40 // New background color for main menu (deep blue/purple) }); menuContainer.addChild(menuBg); var titleText = new Text2(t('title'), { size: 120, fill: 0xffffff }); titleText.anchor.set(0.5, 0); titleText.x = 2048 / 2; titleText.y = 320; menuContainer.addChild(titleText); var easyBtn = new Text2(t('easy'), { size: 100, fill: 0x99ff99 }); easyBtn.anchor.set(0.5, 0.5); easyBtn.x = 2048 / 2; easyBtn.y = 800; menuContainer.addChild(easyBtn); var normalBtn = new Text2(t('normal'), { size: 100, fill: 0xffff99 }); normalBtn.anchor.set(0.5, 0.5); normalBtn.x = 2048 / 2; normalBtn.y = 1050; menuContainer.addChild(normalBtn); var hardBtn = new Text2(t('hard'), { size: 100, fill: 0xff9999 }); hardBtn.anchor.set(0.5, 0.5); hardBtn.x = 2048 / 2; hardBtn.y = 1300; menuContainer.addChild(hardBtn); // Add a "How to Play" button var howToPlayBtn = new Text2(t('howToPlay'), { size: 90, fill: 0xffffff }); howToPlayBtn.anchor.set(0.5, 0.5); howToPlayBtn.x = 2048 / 2; howToPlayBtn.y = 1550; menuContainer.addChild(howToPlayBtn); // Add a "Power-Ups" button var powerUpsBtn = new Text2(t('powerUps'), { size: 90, fill: 0xffff00 }); powerUpsBtn.anchor.set(0.5, 0.5); powerUpsBtn.x = 2048 / 2; powerUpsBtn.y = 1700; menuContainer.addChild(powerUpsBtn); // --- Language Button --- // Language button (bottom center) var languageBtn = new Text2(t('language'), { size: 80, fill: 0xffffff }); languageBtn.anchor.set(0.5, 0.5); languageBtn.x = 2048 / 2; languageBtn.y = 2100; menuContainer.addChild(languageBtn); // Language selection overlay var languageOverlay = null; languageBtn.down = function (x, y, obj) { if (languageOverlay && languageOverlay.parent) { languageOverlay.parent.removeChild(languageOverlay); } languageOverlay = new Container(); // Hide menu text when overlay is open for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) { child.visible = false; } } // Semi-transparent background var bg = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0, tint: 0x1a1a40 }); bg.alpha = 0.92; languageOverlay.addChild(bg); // British flag button var ukFlag = LK.getAsset('musical_note', { anchorX: 0.5, anchorY: 0.5, tint: 0x2345a0, // blue scaleX: 2.2, scaleY: 2.2, x: 2048 / 2 - 260, y: 1100 }); languageOverlay.addChild(ukFlag); var enText = new Text2(t('english'), { size: 90, fill: 0xffffff }); enText.anchor.set(0, 0.5); enText.x = 2048 / 2 - 180; enText.y = 1100; languageOverlay.addChild(enText); // Turkish flag button var trFlag = LK.getAsset('musical_note', { anchorX: 0.5, anchorY: 0.5, tint: 0xe30a17, // red scaleX: 2.2, scaleY: 2.2, x: 2048 / 2 - 260, y: 1300 }); languageOverlay.addChild(trFlag); var trText = new Text2(t('turkish'), { size: 90, fill: 0xffffff }); trText.anchor.set(0, 0.5); trText.x = 2048 / 2 - 180; trText.y = 1300; languageOverlay.addChild(trText); // Button handlers ukFlag.down = function () { setLanguage('en'); if (languageOverlay && languageOverlay.parent) languageOverlay.parent.removeChild(languageOverlay); // Restore menu for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) child.visible = true; } }; enText.down = ukFlag.down; trFlag.down = function () { setLanguage('tr'); if (languageOverlay && languageOverlay.parent) languageOverlay.parent.removeChild(languageOverlay); // Restore menu for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) child.visible = true; } }; trText.down = trFlag.down; // Close button var closeBtn = new Text2(t('close'), { size: 90, fill: 0xffff00 }); closeBtn.anchor.set(0.5, 0.5); closeBtn.x = 2048 / 2; closeBtn.y = 1700; closeBtn.down = function () { if (languageOverlay && languageOverlay.parent) languageOverlay.parent.removeChild(languageOverlay); for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) child.visible = true; } }; languageOverlay.addChild(closeBtn); game.addChild(languageOverlay); }; // --- Language switching logic --- // Update all static UI text to match currentLang function setLanguage(lang) { if (lang !== "en" && lang !== "tr") lang = "en"; currentLang = lang; // Main menu titleText.setText(t('title')); easyBtn.setText(t('easy')); normalBtn.setText(t('normal')); hardBtn.setText(t('hard')); howToPlayBtn.setText(t('howToPlay')); powerUpsBtn.setText(t('powerUps')); languageBtn.setText(t('language')); returnMenuBtn.setText(t('return')); // If overlays are open, update their text as well if (howToPlayOverlay && howToPlayOverlay.parent) { // Remove and re-open to update text howToPlayOverlay.parent.removeChild(howToPlayOverlay); howToPlayBtn.down(); } if (powerUpsOverlay && powerUpsOverlay.parent) { powerUpsOverlay.parent.removeChild(powerUpsOverlay); powerUpsBtn.down(); } } // --- END Language Selection System --- // Power-Ups overlay (shown when Power-Ups button is clicked) var powerUpsOverlay = null; powerUpsBtn.down = function (x, y, obj) { if (powerUpsOverlay && powerUpsOverlay.parent) { powerUpsOverlay.parent.removeChild(powerUpsOverlay); } powerUpsOverlay = new Container(); // Hide menu text when overlay is open for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) { child.visible = false; } } // Semi-transparent background, match main menu color (deep blue/purple) var bg = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0, tint: 0x1a1a40 // match main menu background color }); bg.alpha = 0.92; powerUpsOverlay.addChild(bg); // Title var title = new Text2(t('powerUpsTitle'), { size: 90, fill: 0xffff00 }); title.anchor.set(0.5, 0); title.x = 2048 / 2; title.y = 220; powerUpsOverlay.addChild(title); // Show all power-ups as colored musical notes with description var powerupY = title.y + title.height + 60; var powerupSpacing = 220; // more vertical space between power-ups var powerupTypes = [{ type: "big_paddle", color: 0x99ff99 }, { type: "multi_ball", color: 0xffcc33 }, { type: "slow_ball", color: 0x6699ff }, { type: "speed_ball", color: 0xff3366 }, { type: "blur_screen", color: 0xbebebe }, { type: "invert_screen", color: 0xab66aa }, { type: "reverse_ball", color: 0xffff00 }]; // Move all musical notes and their texts to the left, stack notes, enlarge text, and add spacing var stackX = 180; // further left margin for notes var textX = stackX + 140; // text starts to the right of the stacked notes for (var i = 0; i < powerupTypes.length; i++) { var p = powerupTypes[i]; // Note icon (stacked vertically, left-aligned) var noteIcon = LK.getAsset('musical_note', { anchorX: 0.5, anchorY: 0.5, tint: p.color, scaleX: 1.7, scaleY: 1.7, x: stackX, y: powerupY + i * powerupSpacing }); powerUpsOverlay.addChild(noteIcon); // Description text (centered vertically with note, left-aligned to right of note) var descText = new Text2(t(p.type), { size: 48, fill: p.color }); descText.anchor.set(0, 0.5); // left align text, center vertically descText.x = textX; descText.y = powerupY + i * powerupSpacing; powerUpsOverlay.addChild(descText); } // Scroll overlay so the last power-up text is at the bottom of the screen if (powerupTypes.length > 0) { var lastDescY = powerupY + (powerupTypes.length - 1) * powerupSpacing + 70; var lastDescText = new Text2(t(powerupTypes[powerupTypes.length - 1].type), { size: 70 }); var bottomLineY = lastDescY + lastDescText.height / 2; var screenHeight = LK.height ? LK.height : 2732; var scrollTo = bottomLineY - screenHeight + 120; // 120px margin from bottom if (scrollTo < 0) { scrollTo = 0; } powerUpsOverlay.y = -scrollTo; } // Close button var closeBtn = new Text2(t('close'), { size: 90, fill: 0xffff00 }); closeBtn.anchor.set(0.5, 0.5); closeBtn.x = 2048 / 2; closeBtn.y = 2000; closeBtn.down = function (x, y, obj) { if (powerUpsOverlay && powerUpsOverlay.parent) { powerUpsOverlay.parent.removeChild(powerUpsOverlay); } // Restore menu text visibility when overlay is closed for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) { child.visible = true; } } }; powerUpsOverlay.addChild(closeBtn); game.addChild(powerUpsOverlay); }; // Add a "Return to Main Menu" button (hidden by default, shown on stop) var returnMenuBtn = new Text2(t('return'), { size: 90, fill: 0xffff00 }); returnMenuBtn.anchor.set(0.5, 0.5); returnMenuBtn.x = 2048 / 2; returnMenuBtn.y = 2200; returnMenuBtn.visible = false; // Removed MAIN MENU text and button as requested // Show main menu function function showMainMenu() { // Remove all children except menuBg, titleText, and buttons while (menuContainer.children.length > 0) { menuContainer.removeChild(menuContainer.children[0]); } menuContainer.addChild(menuBg); menuContainer.addChild(titleText); menuContainer.addChild(easyBtn); menuContainer.addChild(normalBtn); menuContainer.addChild(hardBtn); menuContainer.addChild(howToPlayBtn); menuContainer.addChild(returnMenuBtn); returnMenuBtn.visible = false; game.addChild(menuContainer); // Hide pause button when in menu if (pauseBtn && pauseBtn.parent) { pauseBtn.visible = false; } // Start background music in main menu LK.playMusic('Backround_Music'); } // How to Play overlay var howToPlayOverlay = null; howToPlayBtn.down = function (x, y, obj) { if (howToPlayOverlay && howToPlayOverlay.parent) { howToPlayOverlay.parent.removeChild(howToPlayOverlay); } howToPlayOverlay = new Container(); // Hide menu text when overlay is open for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) { child.visible = false; } } // Semi-transparent background, match main menu color (deep blue/purple) var bg = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0, tint: 0x1a1a40 // match main menu background color }); bg.alpha = 0.92; howToPlayOverlay.addChild(bg); // Instructions text // Enlarge text until it reaches the screen borders (target width: 100% of 2048px) var howToPlayString = t('howToPlayText'); var maxWidth = 2048 * 1.0; // 100% of screen width var minSize = 24; var maxSize = 400; // allow even larger var testSize = minSize; var howText = null; while (testSize < maxSize) { var testText = new Text2(howToPlayString, { size: testSize, fill: 0xffffff }); testText.anchor.set(0.5, 0); testText.x = 2048 / 2; if (testText.width > maxWidth) { break; } howText = testText; testSize += 4; // increase faster for larger text } if (!howText) { // fallback howText = new Text2(howToPlayString, { size: minSize, fill: 0xffffff }); howText.anchor.set(0.5, 0); howText.x = 2048 / 2; } howText.x = 2048 / 2; // Set anchor to top-center so text expands downward from y=400 howText.anchor.set(0.5, 0); howText.y = 400; // This ensures the text grows downward from y=400, not upward // The anchorY = 0 ensures the text grows downward from y=400, not upward howToPlayOverlay.addChild(howText); // Close button var closeBtn = new Text2(t('close'), { size: 90, fill: 0xffff00 }); closeBtn.anchor.set(0.5, 0.5); closeBtn.x = 2048 / 2; closeBtn.y = 2000; closeBtn.down = function (x, y, obj) { if (howToPlayOverlay && howToPlayOverlay.parent) { howToPlayOverlay.parent.removeChild(howToPlayOverlay); } // Restore menu text visibility when overlay is closed for (var i = 0; i < menuContainer.children.length; i++) { var child = menuContainer.children[i]; if (child !== menuBg) { child.visible = true; } } }; howToPlayOverlay.addChild(closeBtn); game.addChild(howToPlayOverlay); }; // Listen for stop event to show return to menu button game.onStop = function () { // Show the return to menu button returnMenuBtn.visible = true; // Hide pause button when game ends if (pauseBtn && pauseBtn.parent) { pauseBtn.visible = false; } // (Do not stop background music here to allow music in all modes) // Make sure menu is visible if (!menuContainer.parent) { game.addChild(menuContainer); } }; // Return to menu button handler returnMenuBtn.down = function (x, y, obj) { // Hide the button and show the main menu returnMenuBtn.visible = false; showMainMenu(); }; var selectedDifficulty = null; var aiDifficulty = "normal"; // default function startGameWithDifficulty(diff) { selectedDifficulty = diff; if (diff === "easy") { aiDifficulty = "easy"; } else if (diff === "normal") { aiDifficulty = "normal"; } else { aiDifficulty = "hard"; } menuContainer.destroy(); // Only start the game (and thus pause logic) after menu is gone initGame(); } easyBtn.down = function (x, y, obj) { startGameWithDifficulty("easy"); }; normalBtn.down = function (x, y, obj) { startGameWithDifficulty("normal"); }; hardBtn.down = function (x, y, obj) { startGameWithDifficulty("hard"); }; game.addChild(menuContainer); // --- Game variables (initialized in initGame) --- var tableBg, net, playerPaddle, aiPaddle, ball, playerScore, aiScore, scoreText; var rhythmInterval, rhythmTimer, lastRhythmTick, rhythmBoostAmount, rhythmBoostDuration; var dragging; // --- Pause button (global for menu/game access) --- var pauseBtn = null; // --- AI learning: memory of where player hits the ball on the paddle --- // We'll store counts for 'top', 'middle', 'bottom' hits var aiPlayerHitMemory = { top: 1, // Start with 1 to avoid divide-by-zero middle: 1, bottom: 1 }; var aiPlayerHitMemoryTotal = 3; // sum of all above // Helper to classify hit location: returns 'top', 'middle', or 'bottom' function classifyPlayerPaddleHit(ballY, paddleY, paddleHeight) { var rel = (ballY - (paddleY - paddleHeight / 2)) / paddleHeight; if (rel < 0.33) return 'top'; if (rel > 0.66) return 'bottom'; return 'middle'; } // --- Power-up variables (global scope) --- var powerUps = []; var powerUpActive = false; var powerUpTimer = 0; var powerUpEffectTimer = 0; var powerUpEffectType = null; // --- LongNote instance (only one at a time) --- var longNoteInstance = null; // --- Power-up attribute text (global for removal timeout) var powerUpAttributeText = undefined; // --- New power-up effect globals --- var blurOverlay = null; var blurText = null; var spinScreenActive = false; var spinScreenTimer = 0; var spinOverlay = null; // --- Game initialization function --- function initGame() { // Table background tableBg = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); game.addChild(tableBg); // Net net = LK.getAsset('net', { anchorX: 0, anchorY: 0.5, x: 0, y: 2732 / 2 }); game.addChild(net); // Player paddle playerPaddle = new Paddle(); playerPaddle.setPlayer(); playerPaddle.x = 2048 / 2; playerPaddle.y = 2732 - 180; game.addChild(playerPaddle); // AI paddle aiPaddle = new Paddle(); aiPaddle.setAI(); aiPaddle.x = 2048 / 2; aiPaddle.y = 180; game.addChild(aiPaddle); // Ball ball = new Ball(); // Set ball speed based on selectedDifficulty if (selectedDifficulty === "easy") { ball.baseSpeed = 10; ball.speed = 10; } else if (selectedDifficulty === "hard") { ball.baseSpeed = 26; ball.speed = 26; } else { // normal ball.baseSpeed = 18; ball.speed = 18; } ball.reset(Math.random() < 0.5 ? 1 : -1); game.addChild(ball); // Score playerScore = 0; aiScore = 0; // Score display scoreText = new Text2('0 : 0', { size: 120, fill: 0xFFFFFF }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); // Rhythm variables rhythmInterval = 600; // ms per beat (100 BPM) rhythmTimer = 0; lastRhythmTick = 0; rhythmBoostAmount = 8; rhythmBoostDuration = 180; // ms // Power-up variables powerUps = []; powerUpActive = false; powerUpTimer = 0; powerUpEffectTimer = 0; powerUpEffectType = null; // Remove any power-up effects from previous game if (typeof playerPaddle !== "undefined" && playerPaddle) { playerPaddle.scale.x = 1; playerPaddle.scale.y = 1; } if (typeof extraBall !== "undefined" && extraBall) { extraBall.destroy(); extraBall = null; } if (typeof ball !== "undefined" && ball) { ball.speed = ball.baseSpeed; } // Start music LK.playMusic('Backround_Music'); // Dragging dragging = false; // Create Pause button if not already created if (!pauseBtn) { pauseBtn = new Text2('PAUSE', { size: 90, fill: 0xffffff }); pauseBtn.anchor.set(0.5, 0.5); pauseBtn.x = 2048 - 180; pauseBtn.y = 180; pauseBtn.visible = true; pauseBtn.down = function (x, y, obj) { LK.pauseGame(); }; } // Show pause button in GUI (top right, but not in top left 100x100) if (!pauseBtn.parent) { LK.gui.topRight.addChild(pauseBtn); } pauseBtn.visible = true; } // Move handler (player paddle) function handleMove(x, y, obj) { if (dragging) { // Clamp to table var newX = x; var halfW = playerPaddle.width / 2; if (newX < halfW) { newX = halfW; } if (newX > 2048 - halfW) { newX = 2048 - halfW; } playerPaddle.x = newX; } } game.move = handleMove; game.down = function (x, y, obj) { // Only start drag if touch is on/near player paddle var localY = y; if (typeof playerPaddle !== "undefined" && playerPaddle && typeof playerPaddle.y !== "undefined" && typeof playerPaddle.height !== "undefined" && localY > playerPaddle.y - playerPaddle.height / 2 - 80) { dragging = true; handleMove(x, y, obj); } }; game.up = function (x, y, obj) { dragging = false; }; // Helper: rectangle collision function rectsIntersect(a, b) { // Defensive: check all required properties exist and are numbers if (!a || !b || typeof a.x !== "number" || typeof a.y !== "number" || typeof a.width !== "number" || typeof a.height !== "number" || typeof b.x !== "number" || typeof b.y !== "number" || typeof b.width !== "number" || typeof b.height !== "number") { return false; } return !(a.x + a.width / 2 < b.x - b.width / 2 || a.x - a.width / 2 > b.x + b.width / 2 || a.y + a.height / 2 < b.y - b.height / 2 || a.y - a.height / 2 > b.y + b.height / 2); } // Update score display function updateScore() { if (typeof scoreText !== "undefined" && scoreText && typeof scoreText.setText === "function") { scoreText.setText(playerScore + ' : ' + aiScore); } } // Game update game.update = function () { // Rhythm sync: every rhythmInterval ms, boost ball speed and randomize direction slightly var now = LK.ticks * 1000 / 60; if (now - lastRhythmTick > rhythmInterval) { lastRhythmTick = now; // Boost ball.rhythmBoost = rhythmBoostAmount; // Add a small random angle to ball direction var angle = Math.atan2(ball.vy, ball.vx); var delta = (Math.random() - 0.5) * 0.3; // -0.15 to 0.15 radians angle += delta; var mag = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); ball.vx = Math.cos(angle) * (mag > 0 ? 1 : 1); ball.vy = Math.sin(angle) * (mag > 0 ? 1 : 1); // Animate ball (flash) LK.effects.flashObject(ball, 0xffff00, 120); } // Remove boost after duration if (typeof ball !== "undefined" && ball && ball.rhythmBoost > 0 && now - lastRhythmTick > rhythmBoostDuration) { ball.rhythmBoost = 0; } // --- Power-up and LongNote spawn logic --- // Only spawn if not already active and not too many on field // Prevent power-ups from spawning in the main menu (before game starts) // Allow up to 2 power-ups on the field at once if (typeof tableBg !== "undefined" && tableBg && typeof powerUps !== "undefined" && !powerUpActive && powerUps.length < 2) { if (typeof powerUpTimer === "undefined") { powerUpTimer = 0; } powerUpTimer++; // Try to spawn every 6-10 seconds if (powerUpTimer > 360 + Math.floor(Math.random() * 240)) { powerUpTimer = 0; // Reduce chance of LongNote spawning: only spawn if random() < 0.25 (25% chance) if ((typeof longNoteInstance === "undefined" || !longNoteInstance) && Math.random() < 0.25) { longNoteInstance = new LongNote(); longNoteInstance.x = 2048 / 2; longNoteInstance.y = 2732 / 2; game.addChild(longNoteInstance); } else { // If longNoteInstance exists, only spawn regular powerups var types = ['big_paddle', 'multi_ball', 'slow_ball', 'speed_ball', 'blur_screen', 'blur_screen', 'blur_screen', // Increase blur_screen weight 'invert_screen']; var pType = types[Math.floor(Math.random() * types.length)]; var p = new PowerUp(pType); // Place in random X, mid Y p.x = 200 + Math.random() * (2048 - 400); p.y = 2732 / 2 + (Math.random() - 0.5) * 400; powerUps.push(p); game.addChild(p); } } } else if (typeof powerUpTimer !== "undefined") { powerUpTimer = 0; } // --- LongNote update and cleanup --- if (typeof longNoteInstance !== "undefined" && longNoteInstance && typeof longNoteInstance.update === "function") { longNoteInstance.update(); // Remove reference if destroyed if (!longNoteInstance || !longNoteInstance.parent) { longNoteInstance = null; } // Ball collision with LongNote: always rebound, never pass through if (typeof ball !== "undefined" && ball && rectsIntersect(longNoteInstance, ball)) { // Calculate the angle from the center of the LongNote to the ball var dx = ball.x - longNoteInstance.x; var dy = ball.y - longNoteInstance.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist === 0) dist = 1; // Prevent division by zero // Normalized direction var nx = dx / dist; var ny = dy / dist; // Project ball velocity onto normal and reflect var dot = ball.vx * nx + ball.vy * ny; // Only reflect if ball is moving toward the LongNote if (dot < 0) { // Reflect velocity ball.vx = ball.vx - 2 * dot * nx; ball.vy = ball.vy - 2 * dot * ny; // Move ball just outside the LongNote to prevent sticking var overlap = longNoteInstance.width / 2 + ball.width / 2 - dist; if (overlap > 0) { ball.x += nx * overlap; ball.y += ny * overlap; } // Optional: flash the ball for feedback LK.effects.flashObject(ball, 0x00e6e6, 120); // Do NOT play 'sesar' sound here; only play on LongNote creation } } // Extra ball collision with LongNote (if present) if (typeof extraBall !== "undefined" && extraBall && rectsIntersect(longNoteInstance, extraBall)) { var dx2 = extraBall.x - longNoteInstance.x; var dy2 = extraBall.y - longNoteInstance.y; var dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); if (dist2 === 0) dist2 = 1; var nx2 = dx2 / dist2; var ny2 = dy2 / dist2; var dot2 = extraBall.vx * nx2 + extraBall.vy * ny2; if (dot2 < 0) { extraBall.vx = extraBall.vx - 2 * dot2 * nx2; extraBall.vy = extraBall.vy - 2 * dot2 * ny2; var overlap2 = longNoteInstance.width / 2 + extraBall.width / 2 - dist2; if (overlap2 > 0) { extraBall.x += nx2 * overlap2; extraBall.y += ny2 * overlap2; } LK.effects.flashObject(extraBall, 0x00e6e6, 120); // Do NOT play 'sesar' sound here; only play on LongNote creation } } } // --- Power-up update and collision --- var _loop = function _loop() { p = powerUps[i]; if (typeof p.update === "function") { p.update(); } // If player paddle or ball collides with powerup if (rectsIntersect(playerPaddle, p) || typeof ball !== "undefined" && ball && rectsIntersect(ball, p)) { // Play power-up collect sound ONCE when ball hits powerup if (typeof ball !== "undefined" && ball && rectsIntersect(ball, p)) { if (typeof LK.getSound === "function" && LK.getSound('powerup_collect')) { LK.getSound('powerup_collect').play(); } // Remove any previous attribute texts if (typeof powerUpAttributeText !== "undefined" && powerUpAttributeText && powerUpAttributeText.parent) { powerUpAttributeText.parent.removeChild(powerUpAttributeText); } // Show only the duration of the musical note power-up (the one just hit) if (p.type === "big_paddle" || p.type === "multi_ball" || p.type === "slow_ball" || p.type === "speed_ball" || p.type === "blur_screen" || p.type === "invert_screen" || p.type === "reverse_ball") { var durationText = "7s"; var txt = new Text2(durationText, { size: 90, fill: 0xffff00 }); txt.anchor.set(0.5, 0.5); txt.x = 2048 / 2; txt.y = 2732 / 2; LK.gui.center.addChild(txt); powerUpAttributeText = { attrTexts: [txt] }; // store for removal LK.setTimeout(function () { if (powerUpAttributeText && powerUpAttributeText.attrTexts) { for (var i = 0; i < powerUpAttributeText.attrTexts.length; i++) { var t = powerUpAttributeText.attrTexts[i]; if (t && t.parent) { t.parent.removeChild(t); } } powerUpAttributeText = undefined; } }, 1200); } } // Only allow one power-up effect at a time if (!powerUpActive) { // Helper: random color generator var _getRandomColor = function _getRandomColor() { // Avoid too dark colors for visibility var min = 0x222222, max = 0xFFFFFF; var c = Math.floor(Math.random() * (max - min)) + min; // Ensure it's not too close to black if ((c & 0xFF) < 0x22 && (c >> 8 & 0xFF) < 0x22 && (c >> 16 & 0xFF) < 0x22) { c += 0x222222; } return c; }; // Store original tints to restore later var storeOriginalTint = function storeOriginalTint(obj, key) { if (!obj) { return; } if (typeof obj.tint !== "undefined" && typeof originalTints[key] === "undefined") { originalTints[key] = obj.tint; } }; // Remove any previous effect before applying new one // Reset paddle size playerPaddle.scale.x = 1; playerPaddle.scale.y = 1; if (typeof aiPaddle !== "undefined" && aiPaddle) { aiPaddle.scale.x = 1; aiPaddle.scale.y = 1; } // Remove extra ball if present if (typeof extraBall !== "undefined" && extraBall) { extraBall.destroy(); extraBall = null; } // Reset ball speed if it was slowed if (typeof ball !== "undefined" && ball) { ball.speed = ball.baseSpeed; } // Now apply new effect powerUpActive = true; powerUpEffectType = p.type; powerUpEffectTimer = 0; // Remove color randomization effect for all game elements (no-op) // Remove from field p.destroy(); powerUps.splice(i, 1); // Visual feedback for both paddles LK.effects.flashObject(playerPaddle, 0xffff99, 200); LK.effects.flashObject(aiPaddle, 0xffff99, 200); // Apply effect to both player and AI if (p.type === 'big_paddle') { // Enlarge both paddles playerPaddle.scale.x = 1.7; playerPaddle.scale.y = 1.2; if (typeof aiPaddle !== "undefined" && aiPaddle) { aiPaddle.scale.x = 1.7; aiPaddle.scale.y = 1.2; } } else if (p.type === 'multi_ball') { // Add a second ball (if not already present) if (typeof extraBall === "undefined" || !extraBall) { extraBall = new Ball(); extraBall.x = ball.x; extraBall.y = ball.y; extraBall.vx = -ball.vx; extraBall.vy = ball.vy; // Set extraBall speed based on selectedDifficulty if (selectedDifficulty === "easy") { extraBall.baseSpeed = 10; extraBall.speed = 10; } else if (selectedDifficulty === "hard") { extraBall.baseSpeed = 26; extraBall.speed = 26; } else { // normal extraBall.baseSpeed = 18; extraBall.speed = 18; } extraBall.rhythmBoost = ball.rhythmBoost; extraBall.lastScored = false; game.addChild(extraBall); } } else if (p.type === 'slow_ball') { // Slow down ball if (typeof ball !== "undefined" && ball) { ball.speed = Math.max(6, ball.speed * 0.5); } } else if (p.type === 'speed_ball') { // Speed up ball if (typeof ball !== "undefined" && ball) { ball.speed = Math.min(ball.baseSpeed * 2.2, 48); } if (typeof extraBall !== "undefined" && extraBall) { extraBall.speed = Math.min(extraBall.baseSpeed * 2.2, 48); } } else if (p.type === 'blur_screen') { // Blur effect: overlay a semi-transparent blurred rectangle if (typeof blurOverlay === "undefined" || !blurOverlay) { blurOverlay = LK.getAsset('table', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); blurOverlay.alpha = 0.92; // Increased blur strength (was 0.7) blurOverlay.tint = 0xbebebe; // #bebebe blurOverlay.tint = 0xbebebe; // #bebebe blurOverlay.width = 2048; blurOverlay.height = 2732; // Add a "blur" text for fun blurText = new Text2("BLUR!", { size: 200, fill: 0xffffff }); blurText.anchor.set(0.5, 0.5); blurText.x = 2048 / 2; blurText.y = 2732 / 2; blurOverlay.addChild(blurText); game.addChild(blurOverlay); } } else if (p.type === 'invert_screen') { // Invert screen: flip the game vertically (upside down) for 7 seconds spinScreenActive = true; spinScreenTimer = 0; if (typeof spinOverlay === "undefined" || !spinOverlay) { spinOverlay = new Container(); } // Instantly flip the game vertically by setting scale.y to -1 and anchor to center if (typeof game.scale !== "undefined") { game.pivot.x = 2048 / 2; game.pivot.y = 2732 / 2; game.position.x = 2048 / 2; game.position.y = 2732 / 2; game.scale.y = -1; } } else if (p.type === 'reverse_ball') { // Reverse the ball's direction instantly if (typeof ball !== "undefined" && ball) { ball.vx *= -1; ball.vy *= -1; // Optional: flash the ball for feedback LK.effects.flashObject(ball, 0x00e6e6, 300); } // Also reverse extraBall if present if (typeof extraBall !== "undefined" && extraBall) { extraBall.vx *= -1; extraBall.vy *= -1; LK.effects.flashObject(extraBall, 0x00e6e6, 300); } } } else { // If effect is already active, just remove the powerup from the field p.destroy(); powerUps.splice(i, 1); } } }, p, attrText, ci; for (var i = powerUps.length - 1; i >= 0; i--) { _loop(); } // Color randomization effect update removed // --- Power-up effect duration --- if (powerUpActive) { powerUpEffectTimer++; // Effect lasts 7 seconds if (powerUpEffectTimer > 420) { // Remove all effects and reset state if (typeof playerPaddle !== "undefined" && playerPaddle) { playerPaddle.scale.x = 1; playerPaddle.scale.y = 1; } if (typeof extraBall !== "undefined" && extraBall) { extraBall.destroy(); extraBall = null; } if (typeof ball !== "undefined" && ball) { ball.speed = ball.baseSpeed; } // Remove blur overlay if present if (typeof blurOverlay !== "undefined" && blurOverlay && blurOverlay.parent) { blurOverlay.parent.removeChild(blurOverlay); blurOverlay = null; blurText = null; } // Remove invert_screen effect (reset vertical flip) if (typeof spinScreenActive !== "undefined" && spinScreenActive) { spinScreenActive = false; spinScreenTimer = 0; if (typeof game.scale !== "undefined") { game.scale.y = 1; game.pivot.x = 0; game.pivot.y = 0; game.position.x = 0; game.position.y = 0; } } // Clean up any other power-up objects on the field for (var i = powerUps.length - 1; i >= 0; i--) { if (powerUps[i]) { powerUps[i].destroy(); } } powerUps = []; powerUpActive = false; powerUpEffectType = null; powerUpEffectTimer = 0; } } // Ball update if (typeof ball !== "undefined" && ball && typeof ball.update === "function") { ball.update(); } // Extra ball update (if present) if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.update === "function") { extraBall.update(); // Ball collision with left/right walls if (extraBall.x < extraBall.width / 2) { extraBall.x = extraBall.width / 2; extraBall.vx *= -1; } if (extraBall.x > 2048 - extraBall.width / 2) { extraBall.x = 2048 - extraBall.width / 2; extraBall.vx *= -1; } } // Ball collision with left/right walls if (typeof ball !== "undefined" && ball && typeof ball.x !== "undefined" && typeof ball.width !== "undefined") { if (ball.x < ball.width / 2) { ball.x = ball.width / 2; ball.vx *= -1; } if (ball.x > 2048 - ball.width / 2) { ball.x = 2048 - ball.width / 2; ball.vx *= -1; } } // Ball collision with player paddle if (typeof ball !== "undefined" && ball && typeof ball.vy !== "undefined" && ball.vy > 0 && rectsIntersect(ball, playerPaddle)) { ball.y = playerPaddle.y - playerPaddle.height / 2 - ball.height / 2; ball.vy *= -1; // Play p1 sound when ball hits player paddle if (typeof LK.getSound === "function" && LK.getSound('p1')) { LK.getSound('p1').play(); } // --- AI learning: record where player hit the ball on the paddle --- if (typeof aiPlayerHitMemory !== "undefined" && typeof classifyPlayerPaddleHit === "function") { var hitLoc = classifyPlayerPaddleHit(ball.y + ball.height / 2, playerPaddle.y, playerPaddle.height); if (aiPlayerHitMemory[hitLoc] !== undefined) { aiPlayerHitMemory[hitLoc]++; aiPlayerHitMemoryTotal++; } } // Add a bit of angle based on where it hit the paddle var offset = (ball.x - playerPaddle.x) / (playerPaddle.width / 2); var angle = offset * 0.5; // up to ~30deg var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); var newAngle = Math.atan2(-ball.vy, ball.vx) + angle; ball.vx = Math.cos(newAngle); ball.vy = -Math.abs(Math.sin(newAngle)); // Increase speed ball.speed += 1.2; // Flash paddle LK.effects.flashObject(playerPaddle, 0x99ccff, 120); } // Ball collision with AI paddle if (typeof ball !== "undefined" && ball && typeof ball.vy !== "undefined" && ball.vy < 0 && rectsIntersect(ball, aiPaddle)) { ball.y = aiPaddle.y + aiPaddle.height / 2 + ball.height / 2; ball.vy *= -1; // Play pAI sound when ball hits AI paddle if (typeof LK.getSound === "function" && LK.getSound('pAI')) { LK.getSound('pAI').play(); } // Add a bit of angle based on where it hit the paddle var offset = (ball.x - aiPaddle.x) / (aiPaddle.width / 2); var angle = offset * 0.5; var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); var newAngle = Math.atan2(-ball.vy, ball.vx) + angle; ball.vx = Math.cos(newAngle); ball.vy = Math.abs(Math.sin(newAngle)); // Increase speed ball.speed += 1.2; // Flash paddle LK.effects.flashObject(aiPaddle, 0xff99bb, 120); } // Ball out of bounds (top/bottom) if (typeof ball !== "undefined" && ball && typeof ball.y !== "undefined" && typeof ball.height !== "undefined" && !ball.lastScored && ball.y < -ball.height / 2) { // Player scores playerScore += 1; updateScore(); ball.lastScored = true; LK.effects.flashScreen(0x3399ff, 400); if (playerScore >= 7) { if (typeof LK.getSound === "function" && LK.getSound('p1won')) { LK.getSound('p1won').play(); } LK.showYouWin(); return; } ball.reset(-1); } if (typeof ball !== "undefined" && ball && typeof ball.y !== "undefined" && typeof ball.height !== "undefined" && !ball.lastScored && ball.y > 2732 + ball.height / 2) { // AI scores aiScore += 1; updateScore(); ball.lastScored = true; LK.effects.flashScreen(0xff3366, 400); if (aiScore >= 7) { LK.showGameOver(); return; } ball.reset(1); } // Extra ball scoring if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.y !== "undefined" && typeof extraBall.height !== "undefined" && !extraBall.lastScored) { if (extraBall.y < -extraBall.height / 2) { // Player scores playerScore += 1; updateScore(); extraBall.lastScored = true; LK.effects.flashScreen(0x3399ff, 400); if (playerScore >= 7) { if (typeof LK.getSound === "function" && LK.getSound('p1won')) { LK.getSound('p1won').play(); } LK.showYouWin(); return; } extraBall.reset(-1); } else if (extraBall.y > 2732 + extraBall.height / 2) { // AI scores aiScore += 1; updateScore(); extraBall.lastScored = true; LK.effects.flashScreen(0xff3366, 400); if (aiScore >= 7) { LK.showGameOver(); return; } extraBall.reset(1); } } // Extra ball paddle collision if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.vy !== "undefined" && extraBall.vy > 0 && rectsIntersect(extraBall, playerPaddle)) { extraBall.y = playerPaddle.y - playerPaddle.height / 2 - extraBall.height / 2; extraBall.vy *= -1; var offset = (extraBall.x - playerPaddle.x) / (playerPaddle.width / 2); var angle = offset * 0.5; var speed = Math.sqrt(extraBall.vx * extraBall.vx + extraBall.vy * extraBall.vy); var newAngle = Math.atan2(-extraBall.vy, extraBall.vx) + angle; extraBall.vx = Math.cos(newAngle); extraBall.vy = -Math.abs(Math.sin(newAngle)); extraBall.speed += 1.2; LK.effects.flashObject(playerPaddle, 0x99ccff, 120); } if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.vy !== "undefined" && extraBall.vy < 0 && rectsIntersect(extraBall, aiPaddle)) { extraBall.y = aiPaddle.y + aiPaddle.height / 2 + extraBall.height / 2; extraBall.vy *= -1; var offset = (extraBall.x - aiPaddle.x) / (aiPaddle.width / 2); var angle = offset * 0.5; var speed = Math.sqrt(extraBall.vx * extraBall.vx + extraBall.vy * extraBall.vy); var newAngle = Math.atan2(-extraBall.vy, extraBall.vx) + angle; extraBall.vx = Math.cos(newAngle); extraBall.vy = Math.abs(Math.sin(newAngle)); extraBall.speed += 1.2; LK.effects.flashObject(aiPaddle, 0xff99bb, 120); } // --- AI paddle movement: difficulty-based --- if (typeof aiMoveMode === "undefined") { var aiMoveMode = "random"; // "random" or "track" var aiMoveTimer = 0; var aiMoveTargetX = typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined" ? aiPaddle.x : 2048 / 2; var aiMoveDir = 0; } // Difficulty parameters var aiTrackChance, aiRandomOffset, aiSpeedBase, aiSpeedMax, aiStandStillChance, aiReactDelay; if (typeof aiDifficulty === "undefined") { aiDifficulty = "normal"; } if (aiDifficulty === "easy") { aiTrackChance = 0.10; aiRandomOffset = 700; aiSpeedBase = 9; aiSpeedMax = 18; aiStandStillChance = 0.40; aiReactDelay = 110; } else if (aiDifficulty === "hard") { aiTrackChance = 0.6; aiRandomOffset = 180; aiSpeedBase = 24; aiSpeedMax = 44; aiStandStillChance = 0.10; aiReactDelay = 40; } else { // normal aiTrackChance = 0.20; aiRandomOffset = 500; aiSpeedBase = 15; aiSpeedMax = 28; aiStandStillChance = 0.25; aiReactDelay = 80; } aiMoveTimer--; if (aiMoveTimer <= 0) { // Switch mode every 0.5-2 seconds depending on difficulty if (Math.random() < aiTrackChance) { aiMoveMode = "track"; if (typeof ball !== "undefined" && ball && typeof ball.x !== "undefined") { // --- AI learning: bias target X based on where player tends to hit the ball --- var bias = 0; if (typeof aiPlayerHitMemory !== "undefined" && aiPlayerHitMemoryTotal > 0) { // Calculate weighted bias: top = -1, middle = 0, bottom = +1 var topW = aiPlayerHitMemory.top / aiPlayerHitMemoryTotal; var midW = aiPlayerHitMemory.middle / aiPlayerHitMemoryTotal; var botW = aiPlayerHitMemory.bottom / aiPlayerHitMemoryTotal; // If player hits top more, AI should move more toward left (anticipate higher bounce) // If player hits bottom more, AI should move more toward right (anticipate lower bounce) // We'll bias by up to 180px left/right bias = (-topW + botW) * 180; } aiMoveTargetX = ball.x + (Math.random() - 0.5) * aiRandomOffset + bias; } else { aiMoveTargetX = 2048 / 2; } aiMoveTimer = aiReactDelay + Math.floor(Math.random() * aiReactDelay); } else { aiMoveMode = "random"; if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.width !== "undefined") { aiMoveTargetX = Math.random() * (2048 - aiPaddle.width) + aiPaddle.width / 2; } else { aiMoveTargetX = 2048 / 2; } aiMoveTimer = aiReactDelay + Math.floor(Math.random() * (aiReactDelay + 30)); } // Randomly sometimes just stand still if (Math.random() < aiStandStillChance) { if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined") { aiMoveTargetX = aiPaddle.x; } else { aiMoveTargetX = 2048 / 2; } } } var aiSpeed = aiSpeedBase; if (typeof ball !== "undefined" && ball && typeof ball.speed !== "undefined") { aiSpeed += Math.min(ball.speed * 1.2, aiSpeedMax); } if (aiMoveMode === "track") { if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined" && typeof aiMoveTargetX !== "undefined" && Math.abs(aiPaddle.x - aiMoveTargetX) > 8) { if (aiPaddle.x < aiMoveTargetX) { aiPaddle.x += aiSpeed; if (aiPaddle.x > aiMoveTargetX) { aiPaddle.x = aiMoveTargetX; } } else { aiPaddle.x -= aiSpeed; if (aiPaddle.x < aiMoveTargetX) { aiPaddle.x = aiMoveTargetX; } } aiPaddle.clamp(); } } else { // random mode: move toward random target if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined" && typeof aiMoveTargetX !== "undefined" && Math.abs(aiPaddle.x - aiMoveTargetX) > 8) { if (aiPaddle.x < aiMoveTargetX) { aiPaddle.x += aiSpeed * 0.7; if (aiPaddle.x > aiMoveTargetX) { aiPaddle.x = aiMoveTargetX; } } else { aiPaddle.x -= aiSpeed * 0.7; if (aiPaddle.x < aiMoveTargetX) { aiPaddle.x = aiMoveTargetX; } } aiPaddle.clamp(); } } // (Gun and bullet logic removed) // --- Spin screen effect update --- // No continuous spin for screen rotation boost; handled by effect application and removal }; // Initial score updateScore();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballSprite = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
// Ball velocity
self.vx = 0;
self.vy = 0;
// Ball speed (pixels per tick)
self.speed = 18;
// Used for rhythm sync
self.baseSpeed = 18;
self.rhythmBoost = 0;
// Used to prevent multiple scoring on one pass
self.lastScored = false;
// Ball update
self.update = function () {
self.x += self.vx * (self.speed + self.rhythmBoost);
self.y += self.vy * (self.speed + self.rhythmBoost);
};
// Reset ball to center, random direction
self.reset = function (direction) {
self.x = 2048 / 2;
self.y = 2732 / 2;
// Start ball slow, then accelerate after 1 second
self.speed = 4;
self.rhythmBoost = 0;
LK.setTimeout(function () {
// Only accelerate if this ball is still in play and hasn't been reset again
if (self && typeof self.speed !== "undefined" && self.speed === 4) {
self.speed = self.baseSpeed;
}
}, 1000);
// --- Fakeout logic ---
// direction: 1 = to player, -1 = to AI
// Sometimes (30% chance) the ball will "fake" left or right before going the real direction
// Sometimes (20% chance) the ball will "fake" up or down before going the real direction
// Rarely (5% chance), the ball will go to a completely random direction
var rareRandomChance = 0.05;
var fakeoutLRChance = 0.3;
var fakeoutUDChance = 0.2;
var doRareRandom = Math.random() < rareRandomChance;
var doFakeoutLR = !doRareRandom && Math.random() < fakeoutLRChance;
var doFakeoutUD = !doRareRandom && !doFakeoutLR && Math.random() < fakeoutUDChance; // Only one fakeout at a time
var fakeoutDuration = 220; // ms
var realAngle, fakeAngle;
if (doRareRandom) {
// Ball goes in a completely random direction (but not straight up/down)
var angle = Math.random() * Math.PI * 2;
// Avoid perfectly vertical
if (Math.abs(Math.cos(angle)) < 0.2) {
angle += 0.3;
}
self.vx = Math.sin(angle);
self.vy = Math.cos(angle);
} else if (doFakeoutLR) {
// Left/right fakeout (original logic)
var goLeft = Math.random() < 0.5;
realAngle = (goLeft ? -0.35 : 0.35) + (direction === 1 ? Math.PI / 2 : -Math.PI / 2);
fakeAngle = (goLeft ? 0.35 : -0.35) + (direction === 1 ? Math.PI / 2 : -Math.PI / 2);
self.vx = Math.sin(fakeAngle);
self.vy = Math.cos(fakeAngle) * direction;
LK.setTimeout(function () {
if (self && typeof self.vx !== "undefined" && typeof self.vy !== "undefined") {
self.vx = Math.sin(realAngle);
self.vy = Math.cos(realAngle) * direction;
}
}, fakeoutDuration);
} else if (doFakeoutUD) {
// Up/down fakeout
// Ball will appear to go up (or down) for a moment, then go the real direction
var goUpFirst = Math.random() < 0.5;
// Real angle: normal random angle toward the correct side
realAngle = Math.random() * 0.5 - 0.25 + (direction === 1 ? Math.PI / 2 : -Math.PI / 2);
// Fake angle: sharply up or down, but not toward the goal
if (goUpFirst) {
// Fake up: angle close to 0 (straight up)
fakeAngle = -Math.PI / 2 + (Math.random() - 0.5) * 0.2;
} else {
// Fake down: angle close to PI (straight down)
fakeAngle = Math.PI / 2 + (Math.random() - 0.5) * 0.2;
}
self.vx = Math.sin(fakeAngle);
self.vy = Math.cos(fakeAngle) * (goUpFirst ? -1 : 1);
LK.setTimeout(function () {
if (self && typeof self.vx !== "undefined" && typeof self.vy !== "undefined") {
self.vx = Math.sin(realAngle);
self.vy = Math.cos(realAngle) * direction;
}
}, fakeoutDuration);
} else {
// Normal random angle
var angle = Math.random() * 0.5 - 0.25 + (direction === 1 ? Math.PI / 2 : -Math.PI / 2);
self.vx = Math.sin(angle);
self.vy = Math.cos(angle) * direction;
}
self.lastScored = false;
};
return self;
});
// LongNote class: 5 musical notes in a fixed, centered, circular formation, spinning as one, with vertical and horizontal bars attached
var LongNote = Container.expand(function () {
var self = Container.call(this);
var noteColor = 0x00e6e6;
var noteCount = 5;
var radius = 180; // distance from center to each note
var noteScale = 2.5;
self.noteSprites = [];
// Place 5 notes in a circle, evenly spaced
for (var i = 0; i < noteCount; i++) {
var angle = Math.PI * 2 / noteCount * i;
var x = Math.cos(angle) * radius;
var y = Math.sin(angle) * radius;
var note = self.attachAsset('musical_note', {
anchorX: 0.5,
anchorY: 0.5,
tint: noteColor,
scaleX: noteScale,
scaleY: noteScale,
x: x,
y: y
});
self.noteSprites.push(note);
}
// Add vertical and horizontal bars (long rectangles) attached to the center
// Vertical bar
var barColor = 0x00e6e6;
var barThickness = 38;
var barLength = radius * 2 + 110 * noteScale * 0.7; // slightly shorter than full diameter
self.verticalBar = self.attachAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
width: barThickness,
height: barLength,
tint: barColor,
x: 0,
y: 0
});
// Horizontal bar
self.horizontalBar = self.attachAsset('net', {
anchorX: 0.5,
anchorY: 0.5,
width: barLength,
height: barThickness,
tint: barColor,
x: 0,
y: 0
});
// Set bounding box for collision (encompass all notes and bars)
self.width = barLength;
self.height = barLength;
self.rotationTween = null;
self.isRotating = false;
self.duration = 1800; // 30s at 60fps
self.timer = 0;
self.hit = false;
// Center in the middle of the table
self.x = 2048 / 2;
self.y = 2732 / 2;
// Play 'sesar' sound immediately when LongNote appears (only once, not on update)
if (typeof LK.getSound === "function" && LK.getSound('sesar')) {
LK.getSound('sesar').play();
self.sesarPlayed = true;
}
// Mute background music when LongNote appears
if (typeof LK.stopMusic === "function") {
LK.stopMusic();
}
// Start continuous rotation for 30s
self.startRotation = function () {
if (self.isRotating) return;
self.isRotating = true;
// Use tween to rotate 360deg (2*PI) every 2s, repeat for 30s
var _doSpin = function doSpin() {
if (!self.isRotating) return;
self.rotation = 0;
self.rotationTween = tween(self, {
rotation: Math.PI * 2
}, {
duration: 2000,
easing: tween.linear,
onFinish: _doSpin
});
};
_doSpin();
};
self.stopRotation = function () {
self.isRotating = false;
if (self.rotationTween) {
tween.stop(self, {
rotation: true
});
self.rotationTween = null;
}
self.rotation = 0;
};
self.update = function () {
self.timer++;
if (!self.isRotating) self.startRotation();
// Remove after 30s
if (self.timer > self.duration) {
self.stopRotation();
// Unmute/return background music when LongNote disappears
if (typeof LK.playMusic === "function") {
LK.playMusic('Backround_Music');
}
if (self.parent) self.parent.removeChild(self);
if (typeof longNoteInstance !== "undefined") longNoteInstance = null;
}
// If already hit, do not check collision again
if (self.hit) return;
// Defensive: check ball exists
// Check collision with the whole LongNote (including bars)
if (typeof ball !== "undefined" && ball && rectsIntersect(self, ball)) {
self.hit = true;
// Reverse ball direction
ball.vx *= -1;
ball.vy *= -1;
// Do not play 'sesar' sound again on hit (only play on creation)
// Continue rotating for the rest of 30s (do not remove)
}
};
return self;
});
// Paddle class
var Paddle = Container.expand(function () {
var self = Container.call(this);
// Set in init
self.isPlayer = false;
// Attach asset
var paddleSprite = self.attachAsset('paddle_player', {
anchorX: 0.5,
anchorY: 0.5
});
// Set color for AI
self.setAI = function () {
paddleSprite.destroy();
self.attachAsset('paddle_ai', {
anchorX: 0.5,
anchorY: 0.5
});
self.isPlayer = false;
};
// Set color for player
self.setPlayer = function () {
paddleSprite.destroy();
self.attachAsset('paddle_player', {
anchorX: 0.5,
anchorY: 0.5
});
self.isPlayer = true;
};
// Clamp paddle inside table
self.clamp = function () {
var halfW = self.width / 2;
if (self.x < halfW) {
self.x = halfW;
}
if (self.x > 2048 - halfW) {
self.x = 2048 - halfW;
}
};
return self;
});
// PowerUp class
var PowerUp = Container.expand(function () {
var self = Container.call(this);
// Types: 'big_paddle', 'multi_ball', 'slow_ball', 'speed_ball', 'blur_screen', 'invert_screen', 'reverse_ball'
self.type = 'big_paddle';
self.active = false;
// Randomly pick a type if not set
if (arguments.length > 0 && typeof arguments[0] === "string") {
self.type = arguments[0];
} else {
var types = ['big_paddle', 'multi_ball', 'slow_ball', 'speed_ball', 'blur_screen', 'invert_screen', 'reverse_ball'];
self.type = types[Math.floor(Math.random() * types.length)];
}
// Attach musical note asset for all power-ups
// Use updated musical_note color scheme for each power-up type
var noteColor = 0x00e6e6; // default: cyan blue for musical notes
if (self.type === 'big_paddle') {
noteColor = 0x99ff99; // green (EASY)
}
if (self.type === 'multi_ball') {
noteColor = 0xffcc33; // orange-yellow (new for multi_ball)
}
if (self.type === 'slow_ball') {
noteColor = 0x6699ff; // blue (new for slow_ball)
}
if (self.type === 'speed_ball') {
noteColor = 0xff3366; // pink-red (new for speed_ball)
}
if (self.type === 'blur_screen') {
noteColor = 0xbebebe; // gray (blur_screen)
}
if (self.type === 'invert_screen') {
noteColor = 0xbb66ff; // purple (invert_screen)
}
if (self.type === 'reverse_ball') {
noteColor = 0xffff00; // yellow (reverse_ball)
}
// Use 'musical_note' asset (ellipse with note color, or a dedicated note asset if available)
var powerupSprite = self.attachAsset('musical_note', {
anchorX: 0.5,
anchorY: 0.5,
tint: noteColor,
scaleX: 2.0,
scaleY: 2.0
});
// Set size for collision
self.width = powerupSprite.width * 2.0;
self.height = powerupSprite.height * 2.0;
// PowerUp update (spin and move to random places)
self.floatDir = 1;
self.floatTimer = 0;
self.targetX = self.x;
self.targetY = self.y;
self.moveTimer = 0;
self.rotationSpeed = 0.07 + Math.random() * 0.07; // random spin speed
self.update = function () {
self.floatTimer++;
// Spin the note
if (powerupSprite && typeof powerupSprite.rotation === "number") {
powerupSprite.rotation += self.rotationSpeed;
}
// Move toward target position
var moveSpeed = 1.2; // slow movement
if (typeof self.targetX === "number" && typeof self.targetY === "number") {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > moveSpeed) {
self.x += dx / dist * moveSpeed;
self.y += dy / dist * moveSpeed;
} else {
self.x = self.targetX;
self.y = self.targetY;
}
}
// Every 120-240 frames, pick a new random target position
self.moveTimer++;
if (self.moveTimer > 120 + Math.floor(Math.random() * 120)) {
self.moveTimer = 0;
// Stay within table bounds
var margin = 120;
self.targetX = margin + Math.random() * (2048 - 2 * margin);
self.targetY = 2732 / 2 - 400 + Math.random() * 800;
}
// Float up/down for a little extra effect
self.y += Math.sin(self.floatTimer / 20) * 0.8;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x228B22 // Table green
});
/****
* Game Code
****/
// --- Language Selection System ---
// Localization dictionary
var LANGUAGES = {
en: {
title: "PING PONG RHYTHM SMASH",
easy: "EASY",
normal: "NORMAL",
hard: "HARD",
howToPlay: "HOW TO PLAY",
powerUps: "Power-Ups",
close: "CLOSE",
"return": "RETURN",
powerUpsTitle: "Power-Ups",
// Power-up descriptions
big_paddle: "BIG PADDLE\nBoth paddles grow much larger for 7 seconds. Making it easier to hit the ball.",
multi_ball: "MULTI BALL\nA second ball appears for 7 seconds. Score with either ball to earn points!",
slow_ball: "SLOW BALL\nThe ball moves at half speed for 7 seconds. Giving you more time to react.",
speed_ball: "SPEED BALL\nThe ball moves much faster for 7 seconds. Can you keep up?",
blur_screen: "BLUR SCREEN\nThe whole game blurs for 7 seconds. Try to play through the haze!",
invert_screen: "INVERT SCREEN\nThe entire game flips upside down for 7 seconds. Can you play inverted?",
reverse_ball: "REVERSE BALL\nThe ball instantly reverses direction! Surprise your opponent.",
howToPlayText: "How to Play\n\n- Drag your paddle left/right to hit the ball.\n- The ball moves in sync with the music beat. Watch for the rhythm boosts!\n- Score points by getting the ball past the AI paddle at the top.\n- First to 7 points wins the match.\n- Collect musical note power-ups for wild effects: big paddles, multi-ball, speed changes, and more!\n- Sometimes, a spinning Long Note appears. It bounces the ball back and changes the music—hit it only once per appearance!\n- The AI learns from where you hit the ball on your paddle. Mix up your shots to stay unpredictable!\n- Choose your difficulty: higher levels make the AI faster and smarter.\n- Good luck, and keep the beat!",
language: "Language",
english: "English",
turkish: "Türkçe"
},
tr: {
title: "RİTİM PİNG PONG",
easy: "KOLAY",
normal: "ORTA",
hard: "ZOR",
howToPlay: "NASIL OYNANIR",
powerUps: "Güçlendiriciler",
close: "KAPAT",
"return": "GERİ DÖN",
powerUpsTitle: "Güçlendiriciler",
big_paddle: "BÜYÜK RAKET\nHer iki raket de 7 saniyeliğine büyür. Topa vurmak daha kolay!",
multi_ball: "ÇOKLU TOP\n7 saniyeliğine ikinci bir top çıkar. Her iki topla da puan kazanabilirsin!",
slow_ball: "YAVAŞ TOP\nTop 7 saniyeliğine yarı hızda hareket eder. Tepki vermek için daha fazla zaman!",
speed_ball: "HIZLI TOP\nTop 7 saniyeliğine çok daha hızlı hareket eder. Yetişebilecek misin?",
blur_screen: "BULANIK EKRAN\nOyun 7 saniyeliğine bulanıklaşır. Sis içinde oynamayı dene!",
invert_screen: "TERS EKRAN\nOyun 7 saniyeliğine baş aşağı döner. Ters oynayabilir misin?",
reverse_ball: "TERS TOP\nTop anında yön değiştirir! Rakibini şaşırt.",
howToPlayText: "Nasıl Oynanır\n\n- Raketini sola/sağa sürükleyerek topa vur.\n- Top, müzik ritmiyle uyumlu hareket eder. Ritim güçlendirmelerine dikkat et!\n- Topu AI raketinin arkasına geçirerek puan kazan.\n- 7 puana ilk ulaşan maçı kazanır.\n- Müzikal nota güçlendiricilerini topla: büyük raket, çoklu top, hız değişimi ve daha fazlası!\n- Bazen dönen Uzun Nota çıkar. Topu geri sektirir ve müziği değiştirir—her çıkışında sadece bir kez vur!\n- AI, topa raketinin neresinden vurduğunu öğrenir. Vuruşlarını çeşitlendir!\n- Zorluk seç: daha yüksek seviyelerde AI daha hızlı ve akıllı olur.\n- Bol şans, ritmi yakala!",
language: "Dil",
english: "İngilizce",
turkish: "Türkçe"
}
};
var currentLang = "en"; // Default language
// Helper to get localized string
function t(key) {
if (LANGUAGES[currentLang] && LANGUAGES[currentLang][key]) return LANGUAGES[currentLang][key];
if (LANGUAGES["en"][key]) return LANGUAGES["en"][key];
return key;
}
// --- Main Menu and Difficulty Selection ---
var menuContainer = new Container();
var menuBg = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
tint: 0x1a1a40 // New background color for main menu (deep blue/purple)
});
menuContainer.addChild(menuBg);
var titleText = new Text2(t('title'), {
size: 120,
fill: 0xffffff
});
titleText.anchor.set(0.5, 0);
titleText.x = 2048 / 2;
titleText.y = 320;
menuContainer.addChild(titleText);
var easyBtn = new Text2(t('easy'), {
size: 100,
fill: 0x99ff99
});
easyBtn.anchor.set(0.5, 0.5);
easyBtn.x = 2048 / 2;
easyBtn.y = 800;
menuContainer.addChild(easyBtn);
var normalBtn = new Text2(t('normal'), {
size: 100,
fill: 0xffff99
});
normalBtn.anchor.set(0.5, 0.5);
normalBtn.x = 2048 / 2;
normalBtn.y = 1050;
menuContainer.addChild(normalBtn);
var hardBtn = new Text2(t('hard'), {
size: 100,
fill: 0xff9999
});
hardBtn.anchor.set(0.5, 0.5);
hardBtn.x = 2048 / 2;
hardBtn.y = 1300;
menuContainer.addChild(hardBtn);
// Add a "How to Play" button
var howToPlayBtn = new Text2(t('howToPlay'), {
size: 90,
fill: 0xffffff
});
howToPlayBtn.anchor.set(0.5, 0.5);
howToPlayBtn.x = 2048 / 2;
howToPlayBtn.y = 1550;
menuContainer.addChild(howToPlayBtn);
// Add a "Power-Ups" button
var powerUpsBtn = new Text2(t('powerUps'), {
size: 90,
fill: 0xffff00
});
powerUpsBtn.anchor.set(0.5, 0.5);
powerUpsBtn.x = 2048 / 2;
powerUpsBtn.y = 1700;
menuContainer.addChild(powerUpsBtn);
// --- Language Button ---
// Language button (bottom center)
var languageBtn = new Text2(t('language'), {
size: 80,
fill: 0xffffff
});
languageBtn.anchor.set(0.5, 0.5);
languageBtn.x = 2048 / 2;
languageBtn.y = 2100;
menuContainer.addChild(languageBtn);
// Language selection overlay
var languageOverlay = null;
languageBtn.down = function (x, y, obj) {
if (languageOverlay && languageOverlay.parent) {
languageOverlay.parent.removeChild(languageOverlay);
}
languageOverlay = new Container();
// Hide menu text when overlay is open
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) {
child.visible = false;
}
}
// Semi-transparent background
var bg = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
tint: 0x1a1a40
});
bg.alpha = 0.92;
languageOverlay.addChild(bg);
// British flag button
var ukFlag = LK.getAsset('musical_note', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x2345a0,
// blue
scaleX: 2.2,
scaleY: 2.2,
x: 2048 / 2 - 260,
y: 1100
});
languageOverlay.addChild(ukFlag);
var enText = new Text2(t('english'), {
size: 90,
fill: 0xffffff
});
enText.anchor.set(0, 0.5);
enText.x = 2048 / 2 - 180;
enText.y = 1100;
languageOverlay.addChild(enText);
// Turkish flag button
var trFlag = LK.getAsset('musical_note', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xe30a17,
// red
scaleX: 2.2,
scaleY: 2.2,
x: 2048 / 2 - 260,
y: 1300
});
languageOverlay.addChild(trFlag);
var trText = new Text2(t('turkish'), {
size: 90,
fill: 0xffffff
});
trText.anchor.set(0, 0.5);
trText.x = 2048 / 2 - 180;
trText.y = 1300;
languageOverlay.addChild(trText);
// Button handlers
ukFlag.down = function () {
setLanguage('en');
if (languageOverlay && languageOverlay.parent) languageOverlay.parent.removeChild(languageOverlay);
// Restore menu
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) child.visible = true;
}
};
enText.down = ukFlag.down;
trFlag.down = function () {
setLanguage('tr');
if (languageOverlay && languageOverlay.parent) languageOverlay.parent.removeChild(languageOverlay);
// Restore menu
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) child.visible = true;
}
};
trText.down = trFlag.down;
// Close button
var closeBtn = new Text2(t('close'), {
size: 90,
fill: 0xffff00
});
closeBtn.anchor.set(0.5, 0.5);
closeBtn.x = 2048 / 2;
closeBtn.y = 1700;
closeBtn.down = function () {
if (languageOverlay && languageOverlay.parent) languageOverlay.parent.removeChild(languageOverlay);
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) child.visible = true;
}
};
languageOverlay.addChild(closeBtn);
game.addChild(languageOverlay);
};
// --- Language switching logic ---
// Update all static UI text to match currentLang
function setLanguage(lang) {
if (lang !== "en" && lang !== "tr") lang = "en";
currentLang = lang;
// Main menu
titleText.setText(t('title'));
easyBtn.setText(t('easy'));
normalBtn.setText(t('normal'));
hardBtn.setText(t('hard'));
howToPlayBtn.setText(t('howToPlay'));
powerUpsBtn.setText(t('powerUps'));
languageBtn.setText(t('language'));
returnMenuBtn.setText(t('return'));
// If overlays are open, update their text as well
if (howToPlayOverlay && howToPlayOverlay.parent) {
// Remove and re-open to update text
howToPlayOverlay.parent.removeChild(howToPlayOverlay);
howToPlayBtn.down();
}
if (powerUpsOverlay && powerUpsOverlay.parent) {
powerUpsOverlay.parent.removeChild(powerUpsOverlay);
powerUpsBtn.down();
}
}
// --- END Language Selection System ---
// Power-Ups overlay (shown when Power-Ups button is clicked)
var powerUpsOverlay = null;
powerUpsBtn.down = function (x, y, obj) {
if (powerUpsOverlay && powerUpsOverlay.parent) {
powerUpsOverlay.parent.removeChild(powerUpsOverlay);
}
powerUpsOverlay = new Container();
// Hide menu text when overlay is open
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) {
child.visible = false;
}
}
// Semi-transparent background, match main menu color (deep blue/purple)
var bg = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
tint: 0x1a1a40 // match main menu background color
});
bg.alpha = 0.92;
powerUpsOverlay.addChild(bg);
// Title
var title = new Text2(t('powerUpsTitle'), {
size: 90,
fill: 0xffff00
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 220;
powerUpsOverlay.addChild(title);
// Show all power-ups as colored musical notes with description
var powerupY = title.y + title.height + 60;
var powerupSpacing = 220; // more vertical space between power-ups
var powerupTypes = [{
type: "big_paddle",
color: 0x99ff99
}, {
type: "multi_ball",
color: 0xffcc33
}, {
type: "slow_ball",
color: 0x6699ff
}, {
type: "speed_ball",
color: 0xff3366
}, {
type: "blur_screen",
color: 0xbebebe
}, {
type: "invert_screen",
color: 0xab66aa
}, {
type: "reverse_ball",
color: 0xffff00
}];
// Move all musical notes and their texts to the left, stack notes, enlarge text, and add spacing
var stackX = 180; // further left margin for notes
var textX = stackX + 140; // text starts to the right of the stacked notes
for (var i = 0; i < powerupTypes.length; i++) {
var p = powerupTypes[i];
// Note icon (stacked vertically, left-aligned)
var noteIcon = LK.getAsset('musical_note', {
anchorX: 0.5,
anchorY: 0.5,
tint: p.color,
scaleX: 1.7,
scaleY: 1.7,
x: stackX,
y: powerupY + i * powerupSpacing
});
powerUpsOverlay.addChild(noteIcon);
// Description text (centered vertically with note, left-aligned to right of note)
var descText = new Text2(t(p.type), {
size: 48,
fill: p.color
});
descText.anchor.set(0, 0.5); // left align text, center vertically
descText.x = textX;
descText.y = powerupY + i * powerupSpacing;
powerUpsOverlay.addChild(descText);
}
// Scroll overlay so the last power-up text is at the bottom of the screen
if (powerupTypes.length > 0) {
var lastDescY = powerupY + (powerupTypes.length - 1) * powerupSpacing + 70;
var lastDescText = new Text2(t(powerupTypes[powerupTypes.length - 1].type), {
size: 70
});
var bottomLineY = lastDescY + lastDescText.height / 2;
var screenHeight = LK.height ? LK.height : 2732;
var scrollTo = bottomLineY - screenHeight + 120; // 120px margin from bottom
if (scrollTo < 0) {
scrollTo = 0;
}
powerUpsOverlay.y = -scrollTo;
}
// Close button
var closeBtn = new Text2(t('close'), {
size: 90,
fill: 0xffff00
});
closeBtn.anchor.set(0.5, 0.5);
closeBtn.x = 2048 / 2;
closeBtn.y = 2000;
closeBtn.down = function (x, y, obj) {
if (powerUpsOverlay && powerUpsOverlay.parent) {
powerUpsOverlay.parent.removeChild(powerUpsOverlay);
}
// Restore menu text visibility when overlay is closed
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) {
child.visible = true;
}
}
};
powerUpsOverlay.addChild(closeBtn);
game.addChild(powerUpsOverlay);
};
// Add a "Return to Main Menu" button (hidden by default, shown on stop)
var returnMenuBtn = new Text2(t('return'), {
size: 90,
fill: 0xffff00
});
returnMenuBtn.anchor.set(0.5, 0.5);
returnMenuBtn.x = 2048 / 2;
returnMenuBtn.y = 2200;
returnMenuBtn.visible = false;
// Removed MAIN MENU text and button as requested
// Show main menu function
function showMainMenu() {
// Remove all children except menuBg, titleText, and buttons
while (menuContainer.children.length > 0) {
menuContainer.removeChild(menuContainer.children[0]);
}
menuContainer.addChild(menuBg);
menuContainer.addChild(titleText);
menuContainer.addChild(easyBtn);
menuContainer.addChild(normalBtn);
menuContainer.addChild(hardBtn);
menuContainer.addChild(howToPlayBtn);
menuContainer.addChild(returnMenuBtn);
returnMenuBtn.visible = false;
game.addChild(menuContainer);
// Hide pause button when in menu
if (pauseBtn && pauseBtn.parent) {
pauseBtn.visible = false;
}
// Start background music in main menu
LK.playMusic('Backround_Music');
}
// How to Play overlay
var howToPlayOverlay = null;
howToPlayBtn.down = function (x, y, obj) {
if (howToPlayOverlay && howToPlayOverlay.parent) {
howToPlayOverlay.parent.removeChild(howToPlayOverlay);
}
howToPlayOverlay = new Container();
// Hide menu text when overlay is open
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) {
child.visible = false;
}
}
// Semi-transparent background, match main menu color (deep blue/purple)
var bg = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
tint: 0x1a1a40 // match main menu background color
});
bg.alpha = 0.92;
howToPlayOverlay.addChild(bg);
// Instructions text
// Enlarge text until it reaches the screen borders (target width: 100% of 2048px)
var howToPlayString = t('howToPlayText');
var maxWidth = 2048 * 1.0; // 100% of screen width
var minSize = 24;
var maxSize = 400; // allow even larger
var testSize = minSize;
var howText = null;
while (testSize < maxSize) {
var testText = new Text2(howToPlayString, {
size: testSize,
fill: 0xffffff
});
testText.anchor.set(0.5, 0);
testText.x = 2048 / 2;
if (testText.width > maxWidth) {
break;
}
howText = testText;
testSize += 4; // increase faster for larger text
}
if (!howText) {
// fallback
howText = new Text2(howToPlayString, {
size: minSize,
fill: 0xffffff
});
howText.anchor.set(0.5, 0);
howText.x = 2048 / 2;
}
howText.x = 2048 / 2;
// Set anchor to top-center so text expands downward from y=400
howText.anchor.set(0.5, 0);
howText.y = 400; // This ensures the text grows downward from y=400, not upward
// The anchorY = 0 ensures the text grows downward from y=400, not upward
howToPlayOverlay.addChild(howText);
// Close button
var closeBtn = new Text2(t('close'), {
size: 90,
fill: 0xffff00
});
closeBtn.anchor.set(0.5, 0.5);
closeBtn.x = 2048 / 2;
closeBtn.y = 2000;
closeBtn.down = function (x, y, obj) {
if (howToPlayOverlay && howToPlayOverlay.parent) {
howToPlayOverlay.parent.removeChild(howToPlayOverlay);
}
// Restore menu text visibility when overlay is closed
for (var i = 0; i < menuContainer.children.length; i++) {
var child = menuContainer.children[i];
if (child !== menuBg) {
child.visible = true;
}
}
};
howToPlayOverlay.addChild(closeBtn);
game.addChild(howToPlayOverlay);
};
// Listen for stop event to show return to menu button
game.onStop = function () {
// Show the return to menu button
returnMenuBtn.visible = true;
// Hide pause button when game ends
if (pauseBtn && pauseBtn.parent) {
pauseBtn.visible = false;
}
// (Do not stop background music here to allow music in all modes)
// Make sure menu is visible
if (!menuContainer.parent) {
game.addChild(menuContainer);
}
};
// Return to menu button handler
returnMenuBtn.down = function (x, y, obj) {
// Hide the button and show the main menu
returnMenuBtn.visible = false;
showMainMenu();
};
var selectedDifficulty = null;
var aiDifficulty = "normal"; // default
function startGameWithDifficulty(diff) {
selectedDifficulty = diff;
if (diff === "easy") {
aiDifficulty = "easy";
} else if (diff === "normal") {
aiDifficulty = "normal";
} else {
aiDifficulty = "hard";
}
menuContainer.destroy();
// Only start the game (and thus pause logic) after menu is gone
initGame();
}
easyBtn.down = function (x, y, obj) {
startGameWithDifficulty("easy");
};
normalBtn.down = function (x, y, obj) {
startGameWithDifficulty("normal");
};
hardBtn.down = function (x, y, obj) {
startGameWithDifficulty("hard");
};
game.addChild(menuContainer);
// --- Game variables (initialized in initGame) ---
var tableBg, net, playerPaddle, aiPaddle, ball, playerScore, aiScore, scoreText;
var rhythmInterval, rhythmTimer, lastRhythmTick, rhythmBoostAmount, rhythmBoostDuration;
var dragging;
// --- Pause button (global for menu/game access) ---
var pauseBtn = null;
// --- AI learning: memory of where player hits the ball on the paddle ---
// We'll store counts for 'top', 'middle', 'bottom' hits
var aiPlayerHitMemory = {
top: 1,
// Start with 1 to avoid divide-by-zero
middle: 1,
bottom: 1
};
var aiPlayerHitMemoryTotal = 3; // sum of all above
// Helper to classify hit location: returns 'top', 'middle', or 'bottom'
function classifyPlayerPaddleHit(ballY, paddleY, paddleHeight) {
var rel = (ballY - (paddleY - paddleHeight / 2)) / paddleHeight;
if (rel < 0.33) return 'top';
if (rel > 0.66) return 'bottom';
return 'middle';
}
// --- Power-up variables (global scope) ---
var powerUps = [];
var powerUpActive = false;
var powerUpTimer = 0;
var powerUpEffectTimer = 0;
var powerUpEffectType = null;
// --- LongNote instance (only one at a time) ---
var longNoteInstance = null;
// --- Power-up attribute text (global for removal timeout)
var powerUpAttributeText = undefined;
// --- New power-up effect globals ---
var blurOverlay = null;
var blurText = null;
var spinScreenActive = false;
var spinScreenTimer = 0;
var spinOverlay = null;
// --- Game initialization function ---
function initGame() {
// Table background
tableBg = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(tableBg);
// Net
net = LK.getAsset('net', {
anchorX: 0,
anchorY: 0.5,
x: 0,
y: 2732 / 2
});
game.addChild(net);
// Player paddle
playerPaddle = new Paddle();
playerPaddle.setPlayer();
playerPaddle.x = 2048 / 2;
playerPaddle.y = 2732 - 180;
game.addChild(playerPaddle);
// AI paddle
aiPaddle = new Paddle();
aiPaddle.setAI();
aiPaddle.x = 2048 / 2;
aiPaddle.y = 180;
game.addChild(aiPaddle);
// Ball
ball = new Ball();
// Set ball speed based on selectedDifficulty
if (selectedDifficulty === "easy") {
ball.baseSpeed = 10;
ball.speed = 10;
} else if (selectedDifficulty === "hard") {
ball.baseSpeed = 26;
ball.speed = 26;
} else {
// normal
ball.baseSpeed = 18;
ball.speed = 18;
}
ball.reset(Math.random() < 0.5 ? 1 : -1);
game.addChild(ball);
// Score
playerScore = 0;
aiScore = 0;
// Score display
scoreText = new Text2('0 : 0', {
size: 120,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
// Rhythm variables
rhythmInterval = 600; // ms per beat (100 BPM)
rhythmTimer = 0;
lastRhythmTick = 0;
rhythmBoostAmount = 8;
rhythmBoostDuration = 180; // ms
// Power-up variables
powerUps = [];
powerUpActive = false;
powerUpTimer = 0;
powerUpEffectTimer = 0;
powerUpEffectType = null;
// Remove any power-up effects from previous game
if (typeof playerPaddle !== "undefined" && playerPaddle) {
playerPaddle.scale.x = 1;
playerPaddle.scale.y = 1;
}
if (typeof extraBall !== "undefined" && extraBall) {
extraBall.destroy();
extraBall = null;
}
if (typeof ball !== "undefined" && ball) {
ball.speed = ball.baseSpeed;
}
// Start music
LK.playMusic('Backround_Music');
// Dragging
dragging = false;
// Create Pause button if not already created
if (!pauseBtn) {
pauseBtn = new Text2('PAUSE', {
size: 90,
fill: 0xffffff
});
pauseBtn.anchor.set(0.5, 0.5);
pauseBtn.x = 2048 - 180;
pauseBtn.y = 180;
pauseBtn.visible = true;
pauseBtn.down = function (x, y, obj) {
LK.pauseGame();
};
}
// Show pause button in GUI (top right, but not in top left 100x100)
if (!pauseBtn.parent) {
LK.gui.topRight.addChild(pauseBtn);
}
pauseBtn.visible = true;
}
// Move handler (player paddle)
function handleMove(x, y, obj) {
if (dragging) {
// Clamp to table
var newX = x;
var halfW = playerPaddle.width / 2;
if (newX < halfW) {
newX = halfW;
}
if (newX > 2048 - halfW) {
newX = 2048 - halfW;
}
playerPaddle.x = newX;
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only start drag if touch is on/near player paddle
var localY = y;
if (typeof playerPaddle !== "undefined" && playerPaddle && typeof playerPaddle.y !== "undefined" && typeof playerPaddle.height !== "undefined" && localY > playerPaddle.y - playerPaddle.height / 2 - 80) {
dragging = true;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragging = false;
};
// Helper: rectangle collision
function rectsIntersect(a, b) {
// Defensive: check all required properties exist and are numbers
if (!a || !b || typeof a.x !== "number" || typeof a.y !== "number" || typeof a.width !== "number" || typeof a.height !== "number" || typeof b.x !== "number" || typeof b.y !== "number" || typeof b.width !== "number" || typeof b.height !== "number") {
return false;
}
return !(a.x + a.width / 2 < b.x - b.width / 2 || a.x - a.width / 2 > b.x + b.width / 2 || a.y + a.height / 2 < b.y - b.height / 2 || a.y - a.height / 2 > b.y + b.height / 2);
}
// Update score display
function updateScore() {
if (typeof scoreText !== "undefined" && scoreText && typeof scoreText.setText === "function") {
scoreText.setText(playerScore + ' : ' + aiScore);
}
}
// Game update
game.update = function () {
// Rhythm sync: every rhythmInterval ms, boost ball speed and randomize direction slightly
var now = LK.ticks * 1000 / 60;
if (now - lastRhythmTick > rhythmInterval) {
lastRhythmTick = now;
// Boost
ball.rhythmBoost = rhythmBoostAmount;
// Add a small random angle to ball direction
var angle = Math.atan2(ball.vy, ball.vx);
var delta = (Math.random() - 0.5) * 0.3; // -0.15 to 0.15 radians
angle += delta;
var mag = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.cos(angle) * (mag > 0 ? 1 : 1);
ball.vy = Math.sin(angle) * (mag > 0 ? 1 : 1);
// Animate ball (flash)
LK.effects.flashObject(ball, 0xffff00, 120);
}
// Remove boost after duration
if (typeof ball !== "undefined" && ball && ball.rhythmBoost > 0 && now - lastRhythmTick > rhythmBoostDuration) {
ball.rhythmBoost = 0;
}
// --- Power-up and LongNote spawn logic ---
// Only spawn if not already active and not too many on field
// Prevent power-ups from spawning in the main menu (before game starts)
// Allow up to 2 power-ups on the field at once
if (typeof tableBg !== "undefined" && tableBg && typeof powerUps !== "undefined" && !powerUpActive && powerUps.length < 2) {
if (typeof powerUpTimer === "undefined") {
powerUpTimer = 0;
}
powerUpTimer++;
// Try to spawn every 6-10 seconds
if (powerUpTimer > 360 + Math.floor(Math.random() * 240)) {
powerUpTimer = 0;
// Reduce chance of LongNote spawning: only spawn if random() < 0.25 (25% chance)
if ((typeof longNoteInstance === "undefined" || !longNoteInstance) && Math.random() < 0.25) {
longNoteInstance = new LongNote();
longNoteInstance.x = 2048 / 2;
longNoteInstance.y = 2732 / 2;
game.addChild(longNoteInstance);
} else {
// If longNoteInstance exists, only spawn regular powerups
var types = ['big_paddle', 'multi_ball', 'slow_ball', 'speed_ball', 'blur_screen', 'blur_screen', 'blur_screen',
// Increase blur_screen weight
'invert_screen'];
var pType = types[Math.floor(Math.random() * types.length)];
var p = new PowerUp(pType);
// Place in random X, mid Y
p.x = 200 + Math.random() * (2048 - 400);
p.y = 2732 / 2 + (Math.random() - 0.5) * 400;
powerUps.push(p);
game.addChild(p);
}
}
} else if (typeof powerUpTimer !== "undefined") {
powerUpTimer = 0;
}
// --- LongNote update and cleanup ---
if (typeof longNoteInstance !== "undefined" && longNoteInstance && typeof longNoteInstance.update === "function") {
longNoteInstance.update();
// Remove reference if destroyed
if (!longNoteInstance || !longNoteInstance.parent) {
longNoteInstance = null;
}
// Ball collision with LongNote: always rebound, never pass through
if (typeof ball !== "undefined" && ball && rectsIntersect(longNoteInstance, ball)) {
// Calculate the angle from the center of the LongNote to the ball
var dx = ball.x - longNoteInstance.x;
var dy = ball.y - longNoteInstance.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) dist = 1; // Prevent division by zero
// Normalized direction
var nx = dx / dist;
var ny = dy / dist;
// Project ball velocity onto normal and reflect
var dot = ball.vx * nx + ball.vy * ny;
// Only reflect if ball is moving toward the LongNote
if (dot < 0) {
// Reflect velocity
ball.vx = ball.vx - 2 * dot * nx;
ball.vy = ball.vy - 2 * dot * ny;
// Move ball just outside the LongNote to prevent sticking
var overlap = longNoteInstance.width / 2 + ball.width / 2 - dist;
if (overlap > 0) {
ball.x += nx * overlap;
ball.y += ny * overlap;
}
// Optional: flash the ball for feedback
LK.effects.flashObject(ball, 0x00e6e6, 120);
// Do NOT play 'sesar' sound here; only play on LongNote creation
}
}
// Extra ball collision with LongNote (if present)
if (typeof extraBall !== "undefined" && extraBall && rectsIntersect(longNoteInstance, extraBall)) {
var dx2 = extraBall.x - longNoteInstance.x;
var dy2 = extraBall.y - longNoteInstance.y;
var dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (dist2 === 0) dist2 = 1;
var nx2 = dx2 / dist2;
var ny2 = dy2 / dist2;
var dot2 = extraBall.vx * nx2 + extraBall.vy * ny2;
if (dot2 < 0) {
extraBall.vx = extraBall.vx - 2 * dot2 * nx2;
extraBall.vy = extraBall.vy - 2 * dot2 * ny2;
var overlap2 = longNoteInstance.width / 2 + extraBall.width / 2 - dist2;
if (overlap2 > 0) {
extraBall.x += nx2 * overlap2;
extraBall.y += ny2 * overlap2;
}
LK.effects.flashObject(extraBall, 0x00e6e6, 120);
// Do NOT play 'sesar' sound here; only play on LongNote creation
}
}
}
// --- Power-up update and collision ---
var _loop = function _loop() {
p = powerUps[i];
if (typeof p.update === "function") {
p.update();
}
// If player paddle or ball collides with powerup
if (rectsIntersect(playerPaddle, p) || typeof ball !== "undefined" && ball && rectsIntersect(ball, p)) {
// Play power-up collect sound ONCE when ball hits powerup
if (typeof ball !== "undefined" && ball && rectsIntersect(ball, p)) {
if (typeof LK.getSound === "function" && LK.getSound('powerup_collect')) {
LK.getSound('powerup_collect').play();
}
// Remove any previous attribute texts
if (typeof powerUpAttributeText !== "undefined" && powerUpAttributeText && powerUpAttributeText.parent) {
powerUpAttributeText.parent.removeChild(powerUpAttributeText);
}
// Show only the duration of the musical note power-up (the one just hit)
if (p.type === "big_paddle" || p.type === "multi_ball" || p.type === "slow_ball" || p.type === "speed_ball" || p.type === "blur_screen" || p.type === "invert_screen" || p.type === "reverse_ball") {
var durationText = "7s";
var txt = new Text2(durationText, {
size: 90,
fill: 0xffff00
});
txt.anchor.set(0.5, 0.5);
txt.x = 2048 / 2;
txt.y = 2732 / 2;
LK.gui.center.addChild(txt);
powerUpAttributeText = {
attrTexts: [txt]
}; // store for removal
LK.setTimeout(function () {
if (powerUpAttributeText && powerUpAttributeText.attrTexts) {
for (var i = 0; i < powerUpAttributeText.attrTexts.length; i++) {
var t = powerUpAttributeText.attrTexts[i];
if (t && t.parent) {
t.parent.removeChild(t);
}
}
powerUpAttributeText = undefined;
}
}, 1200);
}
}
// Only allow one power-up effect at a time
if (!powerUpActive) {
// Helper: random color generator
var _getRandomColor = function _getRandomColor() {
// Avoid too dark colors for visibility
var min = 0x222222,
max = 0xFFFFFF;
var c = Math.floor(Math.random() * (max - min)) + min;
// Ensure it's not too close to black
if ((c & 0xFF) < 0x22 && (c >> 8 & 0xFF) < 0x22 && (c >> 16 & 0xFF) < 0x22) {
c += 0x222222;
}
return c;
}; // Store original tints to restore later
var storeOriginalTint = function storeOriginalTint(obj, key) {
if (!obj) {
return;
}
if (typeof obj.tint !== "undefined" && typeof originalTints[key] === "undefined") {
originalTints[key] = obj.tint;
}
}; // Remove any previous effect before applying new one
// Reset paddle size
playerPaddle.scale.x = 1;
playerPaddle.scale.y = 1;
if (typeof aiPaddle !== "undefined" && aiPaddle) {
aiPaddle.scale.x = 1;
aiPaddle.scale.y = 1;
}
// Remove extra ball if present
if (typeof extraBall !== "undefined" && extraBall) {
extraBall.destroy();
extraBall = null;
}
// Reset ball speed if it was slowed
if (typeof ball !== "undefined" && ball) {
ball.speed = ball.baseSpeed;
}
// Now apply new effect
powerUpActive = true;
powerUpEffectType = p.type;
powerUpEffectTimer = 0;
// Remove color randomization effect for all game elements (no-op)
// Remove from field
p.destroy();
powerUps.splice(i, 1);
// Visual feedback for both paddles
LK.effects.flashObject(playerPaddle, 0xffff99, 200);
LK.effects.flashObject(aiPaddle, 0xffff99, 200);
// Apply effect to both player and AI
if (p.type === 'big_paddle') {
// Enlarge both paddles
playerPaddle.scale.x = 1.7;
playerPaddle.scale.y = 1.2;
if (typeof aiPaddle !== "undefined" && aiPaddle) {
aiPaddle.scale.x = 1.7;
aiPaddle.scale.y = 1.2;
}
} else if (p.type === 'multi_ball') {
// Add a second ball (if not already present)
if (typeof extraBall === "undefined" || !extraBall) {
extraBall = new Ball();
extraBall.x = ball.x;
extraBall.y = ball.y;
extraBall.vx = -ball.vx;
extraBall.vy = ball.vy;
// Set extraBall speed based on selectedDifficulty
if (selectedDifficulty === "easy") {
extraBall.baseSpeed = 10;
extraBall.speed = 10;
} else if (selectedDifficulty === "hard") {
extraBall.baseSpeed = 26;
extraBall.speed = 26;
} else {
// normal
extraBall.baseSpeed = 18;
extraBall.speed = 18;
}
extraBall.rhythmBoost = ball.rhythmBoost;
extraBall.lastScored = false;
game.addChild(extraBall);
}
} else if (p.type === 'slow_ball') {
// Slow down ball
if (typeof ball !== "undefined" && ball) {
ball.speed = Math.max(6, ball.speed * 0.5);
}
} else if (p.type === 'speed_ball') {
// Speed up ball
if (typeof ball !== "undefined" && ball) {
ball.speed = Math.min(ball.baseSpeed * 2.2, 48);
}
if (typeof extraBall !== "undefined" && extraBall) {
extraBall.speed = Math.min(extraBall.baseSpeed * 2.2, 48);
}
} else if (p.type === 'blur_screen') {
// Blur effect: overlay a semi-transparent blurred rectangle
if (typeof blurOverlay === "undefined" || !blurOverlay) {
blurOverlay = LK.getAsset('table', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
blurOverlay.alpha = 0.92; // Increased blur strength (was 0.7)
blurOverlay.tint = 0xbebebe; // #bebebe
blurOverlay.tint = 0xbebebe; // #bebebe
blurOverlay.width = 2048;
blurOverlay.height = 2732;
// Add a "blur" text for fun
blurText = new Text2("BLUR!", {
size: 200,
fill: 0xffffff
});
blurText.anchor.set(0.5, 0.5);
blurText.x = 2048 / 2;
blurText.y = 2732 / 2;
blurOverlay.addChild(blurText);
game.addChild(blurOverlay);
}
} else if (p.type === 'invert_screen') {
// Invert screen: flip the game vertically (upside down) for 7 seconds
spinScreenActive = true;
spinScreenTimer = 0;
if (typeof spinOverlay === "undefined" || !spinOverlay) {
spinOverlay = new Container();
}
// Instantly flip the game vertically by setting scale.y to -1 and anchor to center
if (typeof game.scale !== "undefined") {
game.pivot.x = 2048 / 2;
game.pivot.y = 2732 / 2;
game.position.x = 2048 / 2;
game.position.y = 2732 / 2;
game.scale.y = -1;
}
} else if (p.type === 'reverse_ball') {
// Reverse the ball's direction instantly
if (typeof ball !== "undefined" && ball) {
ball.vx *= -1;
ball.vy *= -1;
// Optional: flash the ball for feedback
LK.effects.flashObject(ball, 0x00e6e6, 300);
}
// Also reverse extraBall if present
if (typeof extraBall !== "undefined" && extraBall) {
extraBall.vx *= -1;
extraBall.vy *= -1;
LK.effects.flashObject(extraBall, 0x00e6e6, 300);
}
}
} else {
// If effect is already active, just remove the powerup from the field
p.destroy();
powerUps.splice(i, 1);
}
}
},
p,
attrText,
ci;
for (var i = powerUps.length - 1; i >= 0; i--) {
_loop();
}
// Color randomization effect update removed
// --- Power-up effect duration ---
if (powerUpActive) {
powerUpEffectTimer++;
// Effect lasts 7 seconds
if (powerUpEffectTimer > 420) {
// Remove all effects and reset state
if (typeof playerPaddle !== "undefined" && playerPaddle) {
playerPaddle.scale.x = 1;
playerPaddle.scale.y = 1;
}
if (typeof extraBall !== "undefined" && extraBall) {
extraBall.destroy();
extraBall = null;
}
if (typeof ball !== "undefined" && ball) {
ball.speed = ball.baseSpeed;
}
// Remove blur overlay if present
if (typeof blurOverlay !== "undefined" && blurOverlay && blurOverlay.parent) {
blurOverlay.parent.removeChild(blurOverlay);
blurOverlay = null;
blurText = null;
}
// Remove invert_screen effect (reset vertical flip)
if (typeof spinScreenActive !== "undefined" && spinScreenActive) {
spinScreenActive = false;
spinScreenTimer = 0;
if (typeof game.scale !== "undefined") {
game.scale.y = 1;
game.pivot.x = 0;
game.pivot.y = 0;
game.position.x = 0;
game.position.y = 0;
}
}
// Clean up any other power-up objects on the field
for (var i = powerUps.length - 1; i >= 0; i--) {
if (powerUps[i]) {
powerUps[i].destroy();
}
}
powerUps = [];
powerUpActive = false;
powerUpEffectType = null;
powerUpEffectTimer = 0;
}
}
// Ball update
if (typeof ball !== "undefined" && ball && typeof ball.update === "function") {
ball.update();
}
// Extra ball update (if present)
if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.update === "function") {
extraBall.update();
// Ball collision with left/right walls
if (extraBall.x < extraBall.width / 2) {
extraBall.x = extraBall.width / 2;
extraBall.vx *= -1;
}
if (extraBall.x > 2048 - extraBall.width / 2) {
extraBall.x = 2048 - extraBall.width / 2;
extraBall.vx *= -1;
}
}
// Ball collision with left/right walls
if (typeof ball !== "undefined" && ball && typeof ball.x !== "undefined" && typeof ball.width !== "undefined") {
if (ball.x < ball.width / 2) {
ball.x = ball.width / 2;
ball.vx *= -1;
}
if (ball.x > 2048 - ball.width / 2) {
ball.x = 2048 - ball.width / 2;
ball.vx *= -1;
}
}
// Ball collision with player paddle
if (typeof ball !== "undefined" && ball && typeof ball.vy !== "undefined" && ball.vy > 0 && rectsIntersect(ball, playerPaddle)) {
ball.y = playerPaddle.y - playerPaddle.height / 2 - ball.height / 2;
ball.vy *= -1;
// Play p1 sound when ball hits player paddle
if (typeof LK.getSound === "function" && LK.getSound('p1')) {
LK.getSound('p1').play();
}
// --- AI learning: record where player hit the ball on the paddle ---
if (typeof aiPlayerHitMemory !== "undefined" && typeof classifyPlayerPaddleHit === "function") {
var hitLoc = classifyPlayerPaddleHit(ball.y + ball.height / 2, playerPaddle.y, playerPaddle.height);
if (aiPlayerHitMemory[hitLoc] !== undefined) {
aiPlayerHitMemory[hitLoc]++;
aiPlayerHitMemoryTotal++;
}
}
// Add a bit of angle based on where it hit the paddle
var offset = (ball.x - playerPaddle.x) / (playerPaddle.width / 2);
var angle = offset * 0.5; // up to ~30deg
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var newAngle = Math.atan2(-ball.vy, ball.vx) + angle;
ball.vx = Math.cos(newAngle);
ball.vy = -Math.abs(Math.sin(newAngle));
// Increase speed
ball.speed += 1.2;
// Flash paddle
LK.effects.flashObject(playerPaddle, 0x99ccff, 120);
}
// Ball collision with AI paddle
if (typeof ball !== "undefined" && ball && typeof ball.vy !== "undefined" && ball.vy < 0 && rectsIntersect(ball, aiPaddle)) {
ball.y = aiPaddle.y + aiPaddle.height / 2 + ball.height / 2;
ball.vy *= -1;
// Play pAI sound when ball hits AI paddle
if (typeof LK.getSound === "function" && LK.getSound('pAI')) {
LK.getSound('pAI').play();
}
// Add a bit of angle based on where it hit the paddle
var offset = (ball.x - aiPaddle.x) / (aiPaddle.width / 2);
var angle = offset * 0.5;
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var newAngle = Math.atan2(-ball.vy, ball.vx) + angle;
ball.vx = Math.cos(newAngle);
ball.vy = Math.abs(Math.sin(newAngle));
// Increase speed
ball.speed += 1.2;
// Flash paddle
LK.effects.flashObject(aiPaddle, 0xff99bb, 120);
}
// Ball out of bounds (top/bottom)
if (typeof ball !== "undefined" && ball && typeof ball.y !== "undefined" && typeof ball.height !== "undefined" && !ball.lastScored && ball.y < -ball.height / 2) {
// Player scores
playerScore += 1;
updateScore();
ball.lastScored = true;
LK.effects.flashScreen(0x3399ff, 400);
if (playerScore >= 7) {
if (typeof LK.getSound === "function" && LK.getSound('p1won')) {
LK.getSound('p1won').play();
}
LK.showYouWin();
return;
}
ball.reset(-1);
}
if (typeof ball !== "undefined" && ball && typeof ball.y !== "undefined" && typeof ball.height !== "undefined" && !ball.lastScored && ball.y > 2732 + ball.height / 2) {
// AI scores
aiScore += 1;
updateScore();
ball.lastScored = true;
LK.effects.flashScreen(0xff3366, 400);
if (aiScore >= 7) {
LK.showGameOver();
return;
}
ball.reset(1);
}
// Extra ball scoring
if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.y !== "undefined" && typeof extraBall.height !== "undefined" && !extraBall.lastScored) {
if (extraBall.y < -extraBall.height / 2) {
// Player scores
playerScore += 1;
updateScore();
extraBall.lastScored = true;
LK.effects.flashScreen(0x3399ff, 400);
if (playerScore >= 7) {
if (typeof LK.getSound === "function" && LK.getSound('p1won')) {
LK.getSound('p1won').play();
}
LK.showYouWin();
return;
}
extraBall.reset(-1);
} else if (extraBall.y > 2732 + extraBall.height / 2) {
// AI scores
aiScore += 1;
updateScore();
extraBall.lastScored = true;
LK.effects.flashScreen(0xff3366, 400);
if (aiScore >= 7) {
LK.showGameOver();
return;
}
extraBall.reset(1);
}
}
// Extra ball paddle collision
if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.vy !== "undefined" && extraBall.vy > 0 && rectsIntersect(extraBall, playerPaddle)) {
extraBall.y = playerPaddle.y - playerPaddle.height / 2 - extraBall.height / 2;
extraBall.vy *= -1;
var offset = (extraBall.x - playerPaddle.x) / (playerPaddle.width / 2);
var angle = offset * 0.5;
var speed = Math.sqrt(extraBall.vx * extraBall.vx + extraBall.vy * extraBall.vy);
var newAngle = Math.atan2(-extraBall.vy, extraBall.vx) + angle;
extraBall.vx = Math.cos(newAngle);
extraBall.vy = -Math.abs(Math.sin(newAngle));
extraBall.speed += 1.2;
LK.effects.flashObject(playerPaddle, 0x99ccff, 120);
}
if (typeof extraBall !== "undefined" && extraBall && typeof extraBall.vy !== "undefined" && extraBall.vy < 0 && rectsIntersect(extraBall, aiPaddle)) {
extraBall.y = aiPaddle.y + aiPaddle.height / 2 + extraBall.height / 2;
extraBall.vy *= -1;
var offset = (extraBall.x - aiPaddle.x) / (aiPaddle.width / 2);
var angle = offset * 0.5;
var speed = Math.sqrt(extraBall.vx * extraBall.vx + extraBall.vy * extraBall.vy);
var newAngle = Math.atan2(-extraBall.vy, extraBall.vx) + angle;
extraBall.vx = Math.cos(newAngle);
extraBall.vy = Math.abs(Math.sin(newAngle));
extraBall.speed += 1.2;
LK.effects.flashObject(aiPaddle, 0xff99bb, 120);
}
// --- AI paddle movement: difficulty-based ---
if (typeof aiMoveMode === "undefined") {
var aiMoveMode = "random"; // "random" or "track"
var aiMoveTimer = 0;
var aiMoveTargetX = typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined" ? aiPaddle.x : 2048 / 2;
var aiMoveDir = 0;
}
// Difficulty parameters
var aiTrackChance, aiRandomOffset, aiSpeedBase, aiSpeedMax, aiStandStillChance, aiReactDelay;
if (typeof aiDifficulty === "undefined") {
aiDifficulty = "normal";
}
if (aiDifficulty === "easy") {
aiTrackChance = 0.10;
aiRandomOffset = 700;
aiSpeedBase = 9;
aiSpeedMax = 18;
aiStandStillChance = 0.40;
aiReactDelay = 110;
} else if (aiDifficulty === "hard") {
aiTrackChance = 0.6;
aiRandomOffset = 180;
aiSpeedBase = 24;
aiSpeedMax = 44;
aiStandStillChance = 0.10;
aiReactDelay = 40;
} else {
// normal
aiTrackChance = 0.20;
aiRandomOffset = 500;
aiSpeedBase = 15;
aiSpeedMax = 28;
aiStandStillChance = 0.25;
aiReactDelay = 80;
}
aiMoveTimer--;
if (aiMoveTimer <= 0) {
// Switch mode every 0.5-2 seconds depending on difficulty
if (Math.random() < aiTrackChance) {
aiMoveMode = "track";
if (typeof ball !== "undefined" && ball && typeof ball.x !== "undefined") {
// --- AI learning: bias target X based on where player tends to hit the ball ---
var bias = 0;
if (typeof aiPlayerHitMemory !== "undefined" && aiPlayerHitMemoryTotal > 0) {
// Calculate weighted bias: top = -1, middle = 0, bottom = +1
var topW = aiPlayerHitMemory.top / aiPlayerHitMemoryTotal;
var midW = aiPlayerHitMemory.middle / aiPlayerHitMemoryTotal;
var botW = aiPlayerHitMemory.bottom / aiPlayerHitMemoryTotal;
// If player hits top more, AI should move more toward left (anticipate higher bounce)
// If player hits bottom more, AI should move more toward right (anticipate lower bounce)
// We'll bias by up to 180px left/right
bias = (-topW + botW) * 180;
}
aiMoveTargetX = ball.x + (Math.random() - 0.5) * aiRandomOffset + bias;
} else {
aiMoveTargetX = 2048 / 2;
}
aiMoveTimer = aiReactDelay + Math.floor(Math.random() * aiReactDelay);
} else {
aiMoveMode = "random";
if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.width !== "undefined") {
aiMoveTargetX = Math.random() * (2048 - aiPaddle.width) + aiPaddle.width / 2;
} else {
aiMoveTargetX = 2048 / 2;
}
aiMoveTimer = aiReactDelay + Math.floor(Math.random() * (aiReactDelay + 30));
}
// Randomly sometimes just stand still
if (Math.random() < aiStandStillChance) {
if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined") {
aiMoveTargetX = aiPaddle.x;
} else {
aiMoveTargetX = 2048 / 2;
}
}
}
var aiSpeed = aiSpeedBase;
if (typeof ball !== "undefined" && ball && typeof ball.speed !== "undefined") {
aiSpeed += Math.min(ball.speed * 1.2, aiSpeedMax);
}
if (aiMoveMode === "track") {
if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined" && typeof aiMoveTargetX !== "undefined" && Math.abs(aiPaddle.x - aiMoveTargetX) > 8) {
if (aiPaddle.x < aiMoveTargetX) {
aiPaddle.x += aiSpeed;
if (aiPaddle.x > aiMoveTargetX) {
aiPaddle.x = aiMoveTargetX;
}
} else {
aiPaddle.x -= aiSpeed;
if (aiPaddle.x < aiMoveTargetX) {
aiPaddle.x = aiMoveTargetX;
}
}
aiPaddle.clamp();
}
} else {
// random mode: move toward random target
if (typeof aiPaddle !== "undefined" && aiPaddle && typeof aiPaddle.x !== "undefined" && typeof aiMoveTargetX !== "undefined" && Math.abs(aiPaddle.x - aiMoveTargetX) > 8) {
if (aiPaddle.x < aiMoveTargetX) {
aiPaddle.x += aiSpeed * 0.7;
if (aiPaddle.x > aiMoveTargetX) {
aiPaddle.x = aiMoveTargetX;
}
} else {
aiPaddle.x -= aiSpeed * 0.7;
if (aiPaddle.x < aiMoveTargetX) {
aiPaddle.x = aiMoveTargetX;
}
}
aiPaddle.clamp();
}
}
// (Gun and bullet logic removed)
// --- Spin screen effect update ---
// No continuous spin for screen rotation boost; handled by effect application and removal
};
// Initial score
updateScore();