/****
* 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();