/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Ball = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('ballShape', { anchorX: 0.5, anchorY: 0.5 }); self.width = graphics.width; self.height = graphics.height; self.vx = 0; self.vy = 0; self.initialSpeed = 15; // Adjusted initial speed self.currentSpeedFactor = 1.0; self.maxSpeedFactor = 2.8; self.serve = function (directionY) { self.currentSpeedFactor = 1.0; var angle = Math.random() * Math.PI / 2.5 - Math.PI / 5; var speed = self.initialSpeed * self.currentSpeedFactor; self.vx = speed * Math.sin(angle); self.vy = speed * Math.cos(angle) * directionY; }; self.update = function () { self.x += self.vx; self.y += self.vy; if (self.x <= self.width / 2 || self.x >= gameWidth - self.width / 2) { self.vx *= -1.01; self.x = Math.max(self.width / 2, Math.min(self.x, gameWidth - self.width / 2)); LK.getSound('wallBounceSound').play({ volume: 0.7 }); } }; self.bounce = function (paddle) { self.vy *= -1; var hitPos = (self.x - paddle.x) / (paddle.getWidth() / 2); self.vx = self.vx * 0.5 + hitPos * self.initialSpeed * 1.2; self.currentSpeedFactor = Math.min(self.maxSpeedFactor, self.currentSpeedFactor * 1.04); var baseSpeed = self.initialSpeed * self.currentSpeedFactor; var maxHorizontalRatio = 1.4; self.vx = Math.max(-baseSpeed * maxHorizontalRatio, Math.min(self.vx, baseSpeed * maxHorizontalRatio)); var currentMagnitude = Math.sqrt(self.vx * self.vx + self.vy * self.vy); if (currentMagnitude > 0) { var speedRatio = baseSpeed / currentMagnitude; self.vx *= speedRatio; self.vy *= speedRatio; } else { var angle = Math.random() * Math.PI / 2.5 - Math.PI / 5; self.vx = baseSpeed * Math.sin(angle); self.vy = baseSpeed * Math.cos(angle) * (self.vy > 0 ? -1 : 1); } self.y += self.vy > 0 ? 5 : -5; if (paddle === playerPaddle) { LK.getSound('hitSoundPlayer').play(); } else { LK.getSound('hitSoundAI').play(); } }; return self; }); var Paddle = Container.expand(function (isAI) { var self = Container.call(this); var graphics = self.attachAsset('paddleShape', { anchorX: 0.5, anchorY: 0.5 }); var PADDLE_WIDTH = graphics.width; var PADDLE_HEIGHT = graphics.height; var PADDLE_COLOR = graphics.color; // Initial color from asset self.isAI = isAI; self.originalWidth = PADDLE_WIDTH; self.originalHeight = PADDLE_HEIGHT; self.originalColor = PADDLE_COLOR; // Store the initial white color self.width = PADDLE_WIDTH; self.height = PADDLE_HEIGHT; self.hasShield = false; self.shieldTimer = null; self.sizeChangeTimer = null; self.shieldVisual = null; self.isRed = false; // Track red debuff state self.reset = function (keepShield) { tween.stop(graphics, { width: true, tint: true }); tween(graphics, { width: self.originalWidth }, { duration: 200, easing: tween.easeOut }); self.width = self.originalWidth; self.isRed = false; // Reset red state FIRST // Determine target tint var targetTint = self.originalColor; // Default to original white if (keepShield && self.hasShield) { targetTint = 0x88CCFF; // Light Blue for shield } // Set tint DIRECTLY - NO TWEEN graphics.tint = targetTint; // Handle shield removal if necessary (this might change tint back to white if !isRed) if (!keepShield) { self.removeShield(); } // Clear related timer if (self.sizeChangeTimer) { LK.clearTimeout(self.sizeChangeTimer); self.sizeChangeTimer = null; } // Update shield visual visibility based on the CURRENT state of hasShield if (self.shieldVisual) { self.shieldVisual.visible = self.hasShield; } }; self.removeShield = function () { self.hasShield = false; if (self.shieldTimer) { LK.clearTimeout(self.shieldTimer); self.shieldTimer = null; } // Manage shield visual if (self.shieldVisual) { // Only modify visual if it exists if (self.shieldVisual.parent) { // Check if attached before removing self.removeChild(self.shieldVisual); // Keep self.shieldVisual reference but ensure it's hidden if re-added later? // For now, let's assume removeChild is sufficient. Nulling might be better if bugs persist. // self.shieldVisual = null; // Let's not nullify, just remove from display list } // Ensure it's marked invisible even if not attached (belt-and-suspenders) self.shieldVisual.visible = false; } //{Z} // Line ID approx // Reset tint ONLY if NOT currently under the red debuff if (!self.isRed) { graphics.tint = self.originalColor; } }; self.updateAI = function (ballX, ballY, ballVY) { // AI reacts *only* when ball is moving towards it (negative vy) if (self.isAI && ballVY < 0) { var targetX = ballX; var aiSpeed = 18; // Increased AI speed significantly var deadZone = self.width * 0.03; // Very small dead zone // More direct following by reducing interpolation factor or directly setting position // Option 1: Less interpolation (smoother than direct set) var moveAmount = (targetX - self.x) * 0.35; // Faster reaction interpolation factor self.x += Math.max(-aiSpeed, Math.min(aiSpeed, moveAmount)); // Option 2: Near-direct set (can look jittery) // var diff = targetX - self.x; // self.x += Math.max(-aiSpeed, Math.min(aiSpeed, diff)); } self.clampPosition(); }; self.clampPosition = function () { var halfWidth = self.width / 2; self.x = Math.max(halfWidth, Math.min(self.x, gameWidth - halfWidth)); }; self.applySizeChange = function (factor, duration, color) { // Clear any *pending* reset from a previous size change if (self.sizeChangeTimer) { LK.clearTimeout(self.sizeChangeTimer); self.sizeChangeTimer = null; // Clear timer reference } // Stop ongoing width/tint tweens that might interfere tween.stop(graphics, { width: true, tint: true }); //{1c} approx // Apply new width via tween var targetWidth = self.originalWidth * factor; tween(graphics, { width: targetWidth }, { duration: 300, easing: tween.easeOut }); //{1h} approx self.width = targetWidth; // Update logical width immediately // Handle color / red debuff state and associated visuals if (color === 0xFF0000) { // RED Debuff graphics.tint = 0xFF0000; self.isRed = true; if (self.shieldVisual) { self.shieldVisual.visible = false; // Hide shield during red debuff } } else { // Blue Buff or Neutral (size change only) // Only change tint if NOT currently RED if (!self.isRed) { // Set tint based on whether shield is active graphics.tint = self.hasShield ? 0x88CCFF : self.originalColor; if (self.shieldVisual) { // Ensure shield visibility matches shield state self.shieldVisual.visible = self.hasShield; } } else {//{1p} approx // If currently RED, tint remains RED, shield remains hidden. Do nothing here. } } // Update shield visual size to match new paddle size IF shield is active and visual exists if (self.hasShield && self.shieldVisual) { self.shieldVisual.width = self.width + 10; self.shieldVisual.height = self.height + 10; // Visibility is handled above based on isRed state } // Set timer to reset the size/color effects self.sizeChangeTimer = LK.setTimeout(function () { var wasShielded = self.hasShield; // Check shield status *before* reset potentially changes it // Call reset, passing whether a shield was active. // reset() handles setting isRed=false, width tween, tint, and shield visibility based on wasShielded. self.reset(wasShielded); // Simplified logic: Trust reset() to restore the correct visual state. // The original shield timer (if it existed and hasn't fired) will handle shield expiration. // No need for complex shield duration restoration here, which was potentially buggy. self.sizeChangeTimer = null; }, duration); }; self.applyShield = function (duration) { // Clear any existing shield first to prevent timer duplication/conflicts // Note: removeShield() already handles timer clearing and tint reset (if not red) self.removeShield(); //{1x} // Line ID approx self.hasShield = true; if (!self.shieldVisual) { // Create shield visual if it doesn't exist self.shieldVisual = LK.getAsset('shieldShape', { anchorX: 0.5, anchorY: 0.5 }); //{1z} approx self.addChild(self.shieldVisual); //{1A} approx } // Ensure visual matches current paddle size (might be affected by powerups) self.shieldVisual.width = self.width + 10; // Use current self.width self.shieldVisual.height = self.height + 10; // Use current self.height self.shieldVisual.visible = true; // Apply blue tint ONLY if NOT currently under the red debuff if (!self.isRed) { graphics.tint = 0x88CCFF; // Set to light blue } // Set timer to remove the shield effect self.shieldTimer = LK.setTimeout(function () { // Double-check if shield is still active before removing, // it might have been removed by other means (e.g., red powerup reset) if (self.hasShield) { self.removeShield(); } }, duration); }; self.clearAllTimers = function () { self.removeShield(); if (self.sizeChangeTimer) { LK.clearTimeout(self.sizeChangeTimer); self.sizeChangeTimer = null; } self.isRed = false; }; self.getWidth = function () { return self.width; }; self.getHeight = function () { return self.height; }; return self; }); var PowerUp = Container.expand(function (type) { var self = Container.call(this); self.type = type; var assetName = type === 'blue' ? 'powerUpBlueShape' : 'powerUpRedShape'; var graphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.width = graphics.width; self.height = graphics.height; // Set tint before starting the tween graphics.tint = type === 'blue' ? 0x4488FF : 0xFF4444; tween(graphics, { scaleX: 1.1, scaleY: 1.1, tint: type === 'blue' ? 0x4488FF : 0xFF4444 }, { duration: 700, easing: tween.easeInOut, repeat: -1, yoyo: true }); self.update = function () {}; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x050510 }); /**** * Game Code ****/ // Sound Assets (Replace IDs) // Cyan shield var gameWidth = 2048; var gameHeight = 2732; var playerPaddle; var aiPaddle; var ball; var powerUps = []; var lastPaddleHitter = null; var gameState = 'intro'; var powerUpSpawnTimer = null; var introTimeout1 = null; var introTimeout2 = null; var playerScore = 0; var aiScore = 0; var winScore = 7; var background = LK.getAsset('backgroundRect', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); game.addChild(background); var centerLineXStart = 50; var centerLineXEnd = gameWidth - 50; var segmentWidth = 40; var gapWidth = 25; // Slightly larger gap var currentX = centerLineXStart; while (currentX < centerLineXEnd) { var segment = LK.getAsset('centerLineSegment', { anchorX: 0.5, anchorY: 0.5 }); segment.width = segmentWidth; segment.x = currentX + segmentWidth / 2; segment.y = gameHeight / 2; game.addChild(segment); currentX += segmentWidth + gapWidth; } var scoreTextPlayer = new Text2("0", { size: 100, fill: 0xFFFFFF, align: 'center', stroke: 0x000000, strokeThickness: 4 }); scoreTextPlayer.anchor.set(0.5, 1); scoreTextPlayer.x = gameWidth / 2; scoreTextPlayer.y = gameHeight - 30; game.addChild(scoreTextPlayer); var scoreTextAI = new Text2("0", { size: 100, fill: 0xFFFFFF, align: 'center', stroke: 0x000000, strokeThickness: 4 }); scoreTextAI.anchor.set(0.5, 0); scoreTextAI.x = gameWidth / 2; scoreTextAI.y = 30; game.addChild(scoreTextAI); var introText1 = new Text2('Paddle Clash 3', { size: 200, fill: 0xFFFFFF, align: 'center', stroke: 0x000000, strokeThickness: 8, fontWeight: 'bold' }); introText1.anchor.set(0.5, 0.5); introText1.x = gameWidth / 2; introText1.y = gameHeight / 2 - 100; introText1.visible = false; game.addChild(introText1); var introText2 = new Text2('Glaud', { size: 180, fill: 0xFFA500, align: 'center', stroke: 0x000000, strokeThickness: 7, fontWeight: 'bold' }); introText2.anchor.set(0.5, 0.5); introText2.x = gameWidth / 2; introText2.y = gameHeight / 2 + 100; introText2.visible = false; game.addChild(introText2); var gameOverText = new Text2('', { size: 150, fill: 0xFFFFFF, align: 'center', stroke: 0x000000, strokeThickness: 6 }); gameOverText.anchor.set(0.5, 0.5); gameOverText.x = gameWidth / 2; gameOverText.y = gameHeight / 2 - 100; gameOverText.visible = false; game.addChild(gameOverText); var restartText = new Text2('Click to Restart', { size: 90, fill: 0xDDDDDD, align: 'center' }); restartText.anchor.set(0.5, 0.5); restartText.x = gameWidth / 2; restartText.y = gameHeight / 2 + 100; restartText.visible = false; restartText.interactive = true; restartText.down = function () { if (gameState === 'gameOver') { startIntroSequence(); LK.getSound('restartSound').play(); } }; game.addChild(restartText); function resetGame() { if (playerPaddle && playerPaddle.parent) { game.removeChild(playerPaddle); } if (aiPaddle && aiPaddle.parent) { game.removeChild(aiPaddle); } if (ball && ball.parent) { game.removeChild(ball); } powerUps.forEach(function (p) { if (p && p.parent) { game.removeChild(p); } }); powerUps = []; if (powerUpSpawnTimer) { LK.clearInterval(powerUpSpawnTimer); } if (introTimeout1) { LK.clearTimeout(introTimeout1); } if (introTimeout2) { LK.clearTimeout(introTimeout2); } playerScore = 0; aiScore = 0; scoreTextPlayer.setText("0"); scoreTextAI.setText("0"); introText1.visible = false; introText2.visible = false; gameOverText.visible = false; restartText.visible = false; lastPaddleHitter = null; playerPaddle = null; aiPaddle = null; ball = null; } function startGame() { resetGame(); gameState = 'playing'; playerPaddle = new Paddle(false); playerPaddle.x = gameWidth / 2; playerPaddle.y = gameHeight - 70; game.addChild(playerPaddle); aiPaddle = new Paddle(true); aiPaddle.x = gameWidth / 2; aiPaddle.y = 70; game.addChild(aiPaddle); ball = new Ball(); game.addChild(ball); resetBall(Math.random() < 0.5 ? -1 : 1); LK.setTimeout(spawnPowerUp, 4000 + Math.random() * 4000); powerUpSpawnTimer = LK.setInterval(spawnPowerUp, 10000 + Math.random() * 6000); scoreTextPlayer.visible = true; scoreTextAI.visible = true; LK.playMusic('gameMusic'); } function resetBall(directionY) { if (ball) { ball.x = gameWidth / 2; ball.y = gameHeight / 2; ball.serve(directionY); } } function spawnPowerUp() { if (gameState !== 'playing' || powerUps.length > 0) { return; } var type = Math.random() < 0.6 ? 'blue' : 'red'; var powerUp = new PowerUp(type); powerUp.x = gameWidth * (0.2 + Math.random() * 0.6); // Spawn in middle 60% horizontally powerUp.y = gameHeight * (0.3 + Math.random() * 0.4); // Spawn in middle 40% vertically game.addChild(powerUp); powerUps.push(powerUp); LK.getSound('powerupSpawnSound').play({ volume: 0.8 }); } game.move = function (x, y, obj) { if (gameState === 'playing' && playerPaddle) { var gamePos = game.toLocal({ x: x, y: y }); playerPaddle.x = gamePos.x; playerPaddle.clampPosition(); } }; game.down = function (x, y, obj) { if (gameState === 'gameOver' && restartText.visible) { startIntroSequence(); LK.getSound('restartSound').play(); } }; game.update = function () { if (gameState !== 'playing') { return; } if (!ball || !playerPaddle || !aiPaddle) { console.error("Game elements missing!"); endGame(false); return; } ball.update(); aiPaddle.updateAI(ball.x, ball.y, ball.vy); for (var i = powerUps.length - 1; i >= 0; i--) { var p = powerUps[i]; if (!p || p.destroyed || !p.parent) { powerUps.splice(i, 1); continue; } p.update(); if (ball.intersects(p)) { var targetPaddle = lastPaddleHitter; if (targetPaddle) { if (p.type === 'blue') { LK.getSound('powerupGoodSound').play(); if (Math.random() < 0.5) { targetPaddle.applyShield(5000); } else { targetPaddle.applySizeChange(1.5, 5000, null); } } else { LK.getSound('powerupBadSound').play(); targetPaddle.applySizeChange(0.6, 3000, 0xFF0000); // Shrink + RED } } if (p.parent) { game.removeChild(p); } p.destroy(); powerUps.splice(i, 1); } } var ballHitPaddle = false; if (ball.intersects(playerPaddle)) { if (playerPaddle.hasShield) { LK.getSound('shieldBlockSound').play(); playerPaddle.removeShield(); ball.vy = -Math.abs(ball.vy) * 1.1 - 5; ball.vx += (Math.random() - 0.5) * 6; ball.y = playerPaddle.y - playerPaddle.getHeight() / 2 - ball.height / 2 - 3; lastPaddleHitter = null; } else { ball.bounce(playerPaddle); lastPaddleHitter = playerPaddle; ballHitPaddle = true; } } else if (ball.intersects(aiPaddle)) { if (aiPaddle.hasShield) { LK.getSound('shieldBlockSound').play(); aiPaddle.removeShield(); ball.vy = Math.abs(ball.vy) * 1.1 + 5; ball.vx += (Math.random() - 0.5) * 6; ball.y = aiPaddle.y + aiPaddle.getHeight() / 2 + ball.height / 2 + 3; lastPaddleHitter = null; } else { ball.bounce(aiPaddle); lastPaddleHitter = aiPaddle; ballHitPaddle = true; } } if (ballHitPaddle) { var nudgeAttempts = 0; while (ball.intersects(playerPaddle) && playerPaddle && nudgeAttempts < 10) { ball.y += ball.vy > 0 ? 1 : -1; nudgeAttempts++; } nudgeAttempts = 0; while (ball.intersects(aiPaddle) && aiPaddle && nudgeAttempts < 10) { ball.y += ball.vy > 0 ? 1 : -1; nudgeAttempts++; } } if (ball.y < -ball.height) { playerScore++; scoreTextPlayer.setText(playerScore); LK.getSound('scoreSoundPlayer').play(); lastPaddleHitter = null; if (playerScore >= winScore) { endGame(true); } else { resetBall(1); } } else if (ball.y > gameHeight + ball.height) { aiScore++; scoreTextAI.setText(aiScore); LK.getSound('scoreSoundAI').play(); lastPaddleHitter = null; if (aiScore >= winScore) { endGame(false); } else { resetBall(-1); } } }; function endGame(playerWon) { gameState = 'gameOver'; LK.stopMusic('gameMusic'); if (powerUpSpawnTimer) { LK.clearInterval(powerUpSpawnTimer); } powerUps.forEach(function (p) { if (p && p.parent) { game.removeChild(p); } p.destroy(); }); powerUps = []; if (playerPaddle) { playerPaddle.clearAllTimers(); } if (aiPaddle) { aiPaddle.clearAllTimers(); } gameOverText.setText(playerWon ? "YOU WIN!" : "COMPUTER WINS!"); gameOverText.visible = true; restartText.visible = true; if (ball && ball.parent) { game.removeChild(ball); } if (playerPaddle && playerPaddle.parent) { game.removeChild(playerPaddle); } if (aiPaddle && aiPaddle.parent) { game.removeChild(aiPaddle); } scoreTextPlayer.visible = false; scoreTextAI.visible = false; if (playerWon) { LK.getSound('gameOverSoundWin').play(); } else { LK.getSound('gameOverSoundLose').play(); } } function startIntroSequence() { resetGame(); gameState = 'intro'; scoreTextPlayer.visible = false; scoreTextAI.visible = false; introText1.alpha = 0; introText1.visible = true; tween(introText1, { alpha: 1 }, { duration: 500 }); LK.getSound('introSound1').play(); introTimeout1 = LK.setTimeout(function () { tween(introText1, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { introText1.visible = false; } }); introText2.alpha = 0; introText2.visible = true; tween(introText2, { alpha: 1 }, { duration: 500 }); LK.getSound('introSound2').play(); introTimeout2 = LK.setTimeout(function () { tween(introText2, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { introText2.visible = false; startGame(); } }); }, 2000); }, 3000); } startIntroSequence();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('ballShape', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = graphics.width;
self.height = graphics.height;
self.vx = 0;
self.vy = 0;
self.initialSpeed = 15; // Adjusted initial speed
self.currentSpeedFactor = 1.0;
self.maxSpeedFactor = 2.8;
self.serve = function (directionY) {
self.currentSpeedFactor = 1.0;
var angle = Math.random() * Math.PI / 2.5 - Math.PI / 5;
var speed = self.initialSpeed * self.currentSpeedFactor;
self.vx = speed * Math.sin(angle);
self.vy = speed * Math.cos(angle) * directionY;
};
self.update = function () {
self.x += self.vx;
self.y += self.vy;
if (self.x <= self.width / 2 || self.x >= gameWidth - self.width / 2) {
self.vx *= -1.01;
self.x = Math.max(self.width / 2, Math.min(self.x, gameWidth - self.width / 2));
LK.getSound('wallBounceSound').play({
volume: 0.7
});
}
};
self.bounce = function (paddle) {
self.vy *= -1;
var hitPos = (self.x - paddle.x) / (paddle.getWidth() / 2);
self.vx = self.vx * 0.5 + hitPos * self.initialSpeed * 1.2;
self.currentSpeedFactor = Math.min(self.maxSpeedFactor, self.currentSpeedFactor * 1.04);
var baseSpeed = self.initialSpeed * self.currentSpeedFactor;
var maxHorizontalRatio = 1.4;
self.vx = Math.max(-baseSpeed * maxHorizontalRatio, Math.min(self.vx, baseSpeed * maxHorizontalRatio));
var currentMagnitude = Math.sqrt(self.vx * self.vx + self.vy * self.vy);
if (currentMagnitude > 0) {
var speedRatio = baseSpeed / currentMagnitude;
self.vx *= speedRatio;
self.vy *= speedRatio;
} else {
var angle = Math.random() * Math.PI / 2.5 - Math.PI / 5;
self.vx = baseSpeed * Math.sin(angle);
self.vy = baseSpeed * Math.cos(angle) * (self.vy > 0 ? -1 : 1);
}
self.y += self.vy > 0 ? 5 : -5;
if (paddle === playerPaddle) {
LK.getSound('hitSoundPlayer').play();
} else {
LK.getSound('hitSoundAI').play();
}
};
return self;
});
var Paddle = Container.expand(function (isAI) {
var self = Container.call(this);
var graphics = self.attachAsset('paddleShape', {
anchorX: 0.5,
anchorY: 0.5
});
var PADDLE_WIDTH = graphics.width;
var PADDLE_HEIGHT = graphics.height;
var PADDLE_COLOR = graphics.color; // Initial color from asset
self.isAI = isAI;
self.originalWidth = PADDLE_WIDTH;
self.originalHeight = PADDLE_HEIGHT;
self.originalColor = PADDLE_COLOR; // Store the initial white color
self.width = PADDLE_WIDTH;
self.height = PADDLE_HEIGHT;
self.hasShield = false;
self.shieldTimer = null;
self.sizeChangeTimer = null;
self.shieldVisual = null;
self.isRed = false; // Track red debuff state
self.reset = function (keepShield) {
tween.stop(graphics, {
width: true,
tint: true
});
tween(graphics, {
width: self.originalWidth
}, {
duration: 200,
easing: tween.easeOut
});
self.width = self.originalWidth;
self.isRed = false; // Reset red state FIRST
// Determine target tint
var targetTint = self.originalColor; // Default to original white
if (keepShield && self.hasShield) {
targetTint = 0x88CCFF; // Light Blue for shield
}
// Set tint DIRECTLY - NO TWEEN
graphics.tint = targetTint;
// Handle shield removal if necessary (this might change tint back to white if !isRed)
if (!keepShield) {
self.removeShield();
}
// Clear related timer
if (self.sizeChangeTimer) {
LK.clearTimeout(self.sizeChangeTimer);
self.sizeChangeTimer = null;
}
// Update shield visual visibility based on the CURRENT state of hasShield
if (self.shieldVisual) {
self.shieldVisual.visible = self.hasShield;
}
};
self.removeShield = function () {
self.hasShield = false;
if (self.shieldTimer) {
LK.clearTimeout(self.shieldTimer);
self.shieldTimer = null;
}
// Manage shield visual
if (self.shieldVisual) {
// Only modify visual if it exists
if (self.shieldVisual.parent) {
// Check if attached before removing
self.removeChild(self.shieldVisual);
// Keep self.shieldVisual reference but ensure it's hidden if re-added later?
// For now, let's assume removeChild is sufficient. Nulling might be better if bugs persist.
// self.shieldVisual = null; // Let's not nullify, just remove from display list
}
// Ensure it's marked invisible even if not attached (belt-and-suspenders)
self.shieldVisual.visible = false;
} //{Z} // Line ID approx
// Reset tint ONLY if NOT currently under the red debuff
if (!self.isRed) {
graphics.tint = self.originalColor;
}
};
self.updateAI = function (ballX, ballY, ballVY) {
// AI reacts *only* when ball is moving towards it (negative vy)
if (self.isAI && ballVY < 0) {
var targetX = ballX;
var aiSpeed = 18; // Increased AI speed significantly
var deadZone = self.width * 0.03; // Very small dead zone
// More direct following by reducing interpolation factor or directly setting position
// Option 1: Less interpolation (smoother than direct set)
var moveAmount = (targetX - self.x) * 0.35; // Faster reaction interpolation factor
self.x += Math.max(-aiSpeed, Math.min(aiSpeed, moveAmount));
// Option 2: Near-direct set (can look jittery)
// var diff = targetX - self.x;
// self.x += Math.max(-aiSpeed, Math.min(aiSpeed, diff));
}
self.clampPosition();
};
self.clampPosition = function () {
var halfWidth = self.width / 2;
self.x = Math.max(halfWidth, Math.min(self.x, gameWidth - halfWidth));
};
self.applySizeChange = function (factor, duration, color) {
// Clear any *pending* reset from a previous size change
if (self.sizeChangeTimer) {
LK.clearTimeout(self.sizeChangeTimer);
self.sizeChangeTimer = null; // Clear timer reference
}
// Stop ongoing width/tint tweens that might interfere
tween.stop(graphics, {
width: true,
tint: true
}); //{1c} approx
// Apply new width via tween
var targetWidth = self.originalWidth * factor;
tween(graphics, {
width: targetWidth
}, {
duration: 300,
easing: tween.easeOut
}); //{1h} approx
self.width = targetWidth; // Update logical width immediately
// Handle color / red debuff state and associated visuals
if (color === 0xFF0000) {
// RED Debuff
graphics.tint = 0xFF0000;
self.isRed = true;
if (self.shieldVisual) {
self.shieldVisual.visible = false; // Hide shield during red debuff
}
} else {
// Blue Buff or Neutral (size change only)
// Only change tint if NOT currently RED
if (!self.isRed) {
// Set tint based on whether shield is active
graphics.tint = self.hasShield ? 0x88CCFF : self.originalColor;
if (self.shieldVisual) {
// Ensure shield visibility matches shield state
self.shieldVisual.visible = self.hasShield;
}
} else {//{1p} approx
// If currently RED, tint remains RED, shield remains hidden. Do nothing here.
}
}
// Update shield visual size to match new paddle size IF shield is active and visual exists
if (self.hasShield && self.shieldVisual) {
self.shieldVisual.width = self.width + 10;
self.shieldVisual.height = self.height + 10;
// Visibility is handled above based on isRed state
}
// Set timer to reset the size/color effects
self.sizeChangeTimer = LK.setTimeout(function () {
var wasShielded = self.hasShield; // Check shield status *before* reset potentially changes it
// Call reset, passing whether a shield was active.
// reset() handles setting isRed=false, width tween, tint, and shield visibility based on wasShielded.
self.reset(wasShielded);
// Simplified logic: Trust reset() to restore the correct visual state.
// The original shield timer (if it existed and hasn't fired) will handle shield expiration.
// No need for complex shield duration restoration here, which was potentially buggy.
self.sizeChangeTimer = null;
}, duration);
};
self.applyShield = function (duration) {
// Clear any existing shield first to prevent timer duplication/conflicts
// Note: removeShield() already handles timer clearing and tint reset (if not red)
self.removeShield(); //{1x} // Line ID approx
self.hasShield = true;
if (!self.shieldVisual) {
// Create shield visual if it doesn't exist
self.shieldVisual = LK.getAsset('shieldShape', {
anchorX: 0.5,
anchorY: 0.5
}); //{1z} approx
self.addChild(self.shieldVisual); //{1A} approx
}
// Ensure visual matches current paddle size (might be affected by powerups)
self.shieldVisual.width = self.width + 10; // Use current self.width
self.shieldVisual.height = self.height + 10; // Use current self.height
self.shieldVisual.visible = true;
// Apply blue tint ONLY if NOT currently under the red debuff
if (!self.isRed) {
graphics.tint = 0x88CCFF; // Set to light blue
}
// Set timer to remove the shield effect
self.shieldTimer = LK.setTimeout(function () {
// Double-check if shield is still active before removing,
// it might have been removed by other means (e.g., red powerup reset)
if (self.hasShield) {
self.removeShield();
}
}, duration);
};
self.clearAllTimers = function () {
self.removeShield();
if (self.sizeChangeTimer) {
LK.clearTimeout(self.sizeChangeTimer);
self.sizeChangeTimer = null;
}
self.isRed = false;
};
self.getWidth = function () {
return self.width;
};
self.getHeight = function () {
return self.height;
};
return self;
});
var PowerUp = Container.expand(function (type) {
var self = Container.call(this);
self.type = type;
var assetName = type === 'blue' ? 'powerUpBlueShape' : 'powerUpRedShape';
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = graphics.width;
self.height = graphics.height;
// Set tint before starting the tween
graphics.tint = type === 'blue' ? 0x4488FF : 0xFF4444;
tween(graphics, {
scaleX: 1.1,
scaleY: 1.1,
tint: type === 'blue' ? 0x4488FF : 0xFF4444
}, {
duration: 700,
easing: tween.easeInOut,
repeat: -1,
yoyo: true
});
self.update = function () {};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x050510
});
/****
* Game Code
****/
// Sound Assets (Replace IDs)
// Cyan shield
var gameWidth = 2048;
var gameHeight = 2732;
var playerPaddle;
var aiPaddle;
var ball;
var powerUps = [];
var lastPaddleHitter = null;
var gameState = 'intro';
var powerUpSpawnTimer = null;
var introTimeout1 = null;
var introTimeout2 = null;
var playerScore = 0;
var aiScore = 0;
var winScore = 7;
var background = LK.getAsset('backgroundRect', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(background);
var centerLineXStart = 50;
var centerLineXEnd = gameWidth - 50;
var segmentWidth = 40;
var gapWidth = 25; // Slightly larger gap
var currentX = centerLineXStart;
while (currentX < centerLineXEnd) {
var segment = LK.getAsset('centerLineSegment', {
anchorX: 0.5,
anchorY: 0.5
});
segment.width = segmentWidth;
segment.x = currentX + segmentWidth / 2;
segment.y = gameHeight / 2;
game.addChild(segment);
currentX += segmentWidth + gapWidth;
}
var scoreTextPlayer = new Text2("0", {
size: 100,
fill: 0xFFFFFF,
align: 'center',
stroke: 0x000000,
strokeThickness: 4
});
scoreTextPlayer.anchor.set(0.5, 1);
scoreTextPlayer.x = gameWidth / 2;
scoreTextPlayer.y = gameHeight - 30;
game.addChild(scoreTextPlayer);
var scoreTextAI = new Text2("0", {
size: 100,
fill: 0xFFFFFF,
align: 'center',
stroke: 0x000000,
strokeThickness: 4
});
scoreTextAI.anchor.set(0.5, 0);
scoreTextAI.x = gameWidth / 2;
scoreTextAI.y = 30;
game.addChild(scoreTextAI);
var introText1 = new Text2('Paddle Clash 3', {
size: 200,
fill: 0xFFFFFF,
align: 'center',
stroke: 0x000000,
strokeThickness: 8,
fontWeight: 'bold'
});
introText1.anchor.set(0.5, 0.5);
introText1.x = gameWidth / 2;
introText1.y = gameHeight / 2 - 100;
introText1.visible = false;
game.addChild(introText1);
var introText2 = new Text2('Glaud', {
size: 180,
fill: 0xFFA500,
align: 'center',
stroke: 0x000000,
strokeThickness: 7,
fontWeight: 'bold'
});
introText2.anchor.set(0.5, 0.5);
introText2.x = gameWidth / 2;
introText2.y = gameHeight / 2 + 100;
introText2.visible = false;
game.addChild(introText2);
var gameOverText = new Text2('', {
size: 150,
fill: 0xFFFFFF,
align: 'center',
stroke: 0x000000,
strokeThickness: 6
});
gameOverText.anchor.set(0.5, 0.5);
gameOverText.x = gameWidth / 2;
gameOverText.y = gameHeight / 2 - 100;
gameOverText.visible = false;
game.addChild(gameOverText);
var restartText = new Text2('Click to Restart', {
size: 90,
fill: 0xDDDDDD,
align: 'center'
});
restartText.anchor.set(0.5, 0.5);
restartText.x = gameWidth / 2;
restartText.y = gameHeight / 2 + 100;
restartText.visible = false;
restartText.interactive = true;
restartText.down = function () {
if (gameState === 'gameOver') {
startIntroSequence();
LK.getSound('restartSound').play();
}
};
game.addChild(restartText);
function resetGame() {
if (playerPaddle && playerPaddle.parent) {
game.removeChild(playerPaddle);
}
if (aiPaddle && aiPaddle.parent) {
game.removeChild(aiPaddle);
}
if (ball && ball.parent) {
game.removeChild(ball);
}
powerUps.forEach(function (p) {
if (p && p.parent) {
game.removeChild(p);
}
});
powerUps = [];
if (powerUpSpawnTimer) {
LK.clearInterval(powerUpSpawnTimer);
}
if (introTimeout1) {
LK.clearTimeout(introTimeout1);
}
if (introTimeout2) {
LK.clearTimeout(introTimeout2);
}
playerScore = 0;
aiScore = 0;
scoreTextPlayer.setText("0");
scoreTextAI.setText("0");
introText1.visible = false;
introText2.visible = false;
gameOverText.visible = false;
restartText.visible = false;
lastPaddleHitter = null;
playerPaddle = null;
aiPaddle = null;
ball = null;
}
function startGame() {
resetGame();
gameState = 'playing';
playerPaddle = new Paddle(false);
playerPaddle.x = gameWidth / 2;
playerPaddle.y = gameHeight - 70;
game.addChild(playerPaddle);
aiPaddle = new Paddle(true);
aiPaddle.x = gameWidth / 2;
aiPaddle.y = 70;
game.addChild(aiPaddle);
ball = new Ball();
game.addChild(ball);
resetBall(Math.random() < 0.5 ? -1 : 1);
LK.setTimeout(spawnPowerUp, 4000 + Math.random() * 4000);
powerUpSpawnTimer = LK.setInterval(spawnPowerUp, 10000 + Math.random() * 6000);
scoreTextPlayer.visible = true;
scoreTextAI.visible = true;
LK.playMusic('gameMusic');
}
function resetBall(directionY) {
if (ball) {
ball.x = gameWidth / 2;
ball.y = gameHeight / 2;
ball.serve(directionY);
}
}
function spawnPowerUp() {
if (gameState !== 'playing' || powerUps.length > 0) {
return;
}
var type = Math.random() < 0.6 ? 'blue' : 'red';
var powerUp = new PowerUp(type);
powerUp.x = gameWidth * (0.2 + Math.random() * 0.6); // Spawn in middle 60% horizontally
powerUp.y = gameHeight * (0.3 + Math.random() * 0.4); // Spawn in middle 40% vertically
game.addChild(powerUp);
powerUps.push(powerUp);
LK.getSound('powerupSpawnSound').play({
volume: 0.8
});
}
game.move = function (x, y, obj) {
if (gameState === 'playing' && playerPaddle) {
var gamePos = game.toLocal({
x: x,
y: y
});
playerPaddle.x = gamePos.x;
playerPaddle.clampPosition();
}
};
game.down = function (x, y, obj) {
if (gameState === 'gameOver' && restartText.visible) {
startIntroSequence();
LK.getSound('restartSound').play();
}
};
game.update = function () {
if (gameState !== 'playing') {
return;
}
if (!ball || !playerPaddle || !aiPaddle) {
console.error("Game elements missing!");
endGame(false);
return;
}
ball.update();
aiPaddle.updateAI(ball.x, ball.y, ball.vy);
for (var i = powerUps.length - 1; i >= 0; i--) {
var p = powerUps[i];
if (!p || p.destroyed || !p.parent) {
powerUps.splice(i, 1);
continue;
}
p.update();
if (ball.intersects(p)) {
var targetPaddle = lastPaddleHitter;
if (targetPaddle) {
if (p.type === 'blue') {
LK.getSound('powerupGoodSound').play();
if (Math.random() < 0.5) {
targetPaddle.applyShield(5000);
} else {
targetPaddle.applySizeChange(1.5, 5000, null);
}
} else {
LK.getSound('powerupBadSound').play();
targetPaddle.applySizeChange(0.6, 3000, 0xFF0000); // Shrink + RED
}
}
if (p.parent) {
game.removeChild(p);
}
p.destroy();
powerUps.splice(i, 1);
}
}
var ballHitPaddle = false;
if (ball.intersects(playerPaddle)) {
if (playerPaddle.hasShield) {
LK.getSound('shieldBlockSound').play();
playerPaddle.removeShield();
ball.vy = -Math.abs(ball.vy) * 1.1 - 5;
ball.vx += (Math.random() - 0.5) * 6;
ball.y = playerPaddle.y - playerPaddle.getHeight() / 2 - ball.height / 2 - 3;
lastPaddleHitter = null;
} else {
ball.bounce(playerPaddle);
lastPaddleHitter = playerPaddle;
ballHitPaddle = true;
}
} else if (ball.intersects(aiPaddle)) {
if (aiPaddle.hasShield) {
LK.getSound('shieldBlockSound').play();
aiPaddle.removeShield();
ball.vy = Math.abs(ball.vy) * 1.1 + 5;
ball.vx += (Math.random() - 0.5) * 6;
ball.y = aiPaddle.y + aiPaddle.getHeight() / 2 + ball.height / 2 + 3;
lastPaddleHitter = null;
} else {
ball.bounce(aiPaddle);
lastPaddleHitter = aiPaddle;
ballHitPaddle = true;
}
}
if (ballHitPaddle) {
var nudgeAttempts = 0;
while (ball.intersects(playerPaddle) && playerPaddle && nudgeAttempts < 10) {
ball.y += ball.vy > 0 ? 1 : -1;
nudgeAttempts++;
}
nudgeAttempts = 0;
while (ball.intersects(aiPaddle) && aiPaddle && nudgeAttempts < 10) {
ball.y += ball.vy > 0 ? 1 : -1;
nudgeAttempts++;
}
}
if (ball.y < -ball.height) {
playerScore++;
scoreTextPlayer.setText(playerScore);
LK.getSound('scoreSoundPlayer').play();
lastPaddleHitter = null;
if (playerScore >= winScore) {
endGame(true);
} else {
resetBall(1);
}
} else if (ball.y > gameHeight + ball.height) {
aiScore++;
scoreTextAI.setText(aiScore);
LK.getSound('scoreSoundAI').play();
lastPaddleHitter = null;
if (aiScore >= winScore) {
endGame(false);
} else {
resetBall(-1);
}
}
};
function endGame(playerWon) {
gameState = 'gameOver';
LK.stopMusic('gameMusic');
if (powerUpSpawnTimer) {
LK.clearInterval(powerUpSpawnTimer);
}
powerUps.forEach(function (p) {
if (p && p.parent) {
game.removeChild(p);
}
p.destroy();
});
powerUps = [];
if (playerPaddle) {
playerPaddle.clearAllTimers();
}
if (aiPaddle) {
aiPaddle.clearAllTimers();
}
gameOverText.setText(playerWon ? "YOU WIN!" : "COMPUTER WINS!");
gameOverText.visible = true;
restartText.visible = true;
if (ball && ball.parent) {
game.removeChild(ball);
}
if (playerPaddle && playerPaddle.parent) {
game.removeChild(playerPaddle);
}
if (aiPaddle && aiPaddle.parent) {
game.removeChild(aiPaddle);
}
scoreTextPlayer.visible = false;
scoreTextAI.visible = false;
if (playerWon) {
LK.getSound('gameOverSoundWin').play();
} else {
LK.getSound('gameOverSoundLose').play();
}
}
function startIntroSequence() {
resetGame();
gameState = 'intro';
scoreTextPlayer.visible = false;
scoreTextAI.visible = false;
introText1.alpha = 0;
introText1.visible = true;
tween(introText1, {
alpha: 1
}, {
duration: 500
});
LK.getSound('introSound1').play();
introTimeout1 = LK.setTimeout(function () {
tween(introText1, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
introText1.visible = false;
}
});
introText2.alpha = 0;
introText2.visible = true;
tween(introText2, {
alpha: 1
}, {
duration: 500
});
LK.getSound('introSound2').play();
introTimeout2 = LK.setTimeout(function () {
tween(introText2, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
introText2.visible = false;
startGame();
}
});
}, 2000);
}, 3000);
}
startIntroSequence();
wallBounceSound
Sound effect
hitSoundPlayer
Sound effect
hitSoundAI
Sound effect
restartSound
Sound effect
powerupSpawnSound
Sound effect
gameMusic
Music
powerupGoodSound
Sound effect
powerupBadSound
Sound effect
shieldBlockSound
Sound effect
scoreSoundPlayer
Sound effect
scoreSoundAI
Sound effect
gameOverSoundWin
Sound effect
gameOverSoundLose
Sound effect
introSound1
Sound effect
introSound2
Sound effect