530 lines
17 KiB
JavaScript
530 lines
17 KiB
JavaScript
/**
|
|
* Neon Ping-Pong Game Engine
|
|
* Copyright (c) 2026 Wspólnie - wspolpraca@togethere.cloud
|
|
* All rights reserved. Unauthorized copying, distribution, or modification is prohibited.
|
|
*
|
|
* Główna klasa gry Ping-Pong
|
|
* Obsługuje logikę gry, renderowanie i aktualizacje
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Anti-debugging
|
|
const antiDebug = () => {
|
|
setInterval(() => {
|
|
debugger;
|
|
}, 100);
|
|
};
|
|
|
|
// Uncomment in production:
|
|
// if (window.location.hostname !== 'twoja-domena.pl') antiDebug();
|
|
|
|
class PingPongGame {
|
|
constructor(canvasId) {
|
|
this.canvas = document.getElementById(canvasId);
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
|
this.gameActive = false;
|
|
this.gameMode = null; // 'bot' lub 'online'
|
|
this.difficulty = null; // 'easy', 'medium', 'hard'
|
|
|
|
this.playerScore = 0;
|
|
this.botScore = 0;
|
|
this.playerSets = 0;
|
|
this.botSets = 0;
|
|
this.pointsToWin = 11;
|
|
this.setsToWin = 3;
|
|
this.animationId = null;
|
|
this.pointResetDelayMs = 1000;
|
|
this.setBreakDelayMs = 3000;
|
|
this.pointPauseUntil = 0;
|
|
this.setBreakUntil = 0;
|
|
|
|
// Timer gry
|
|
this.gameStartTime = null;
|
|
this.gameEndTime = null;
|
|
this.gameTime = 0;
|
|
|
|
// Wymiary elementów gry
|
|
this.paddleWidth = 10;
|
|
this.paddleHeight = 100;
|
|
|
|
// Inicjalizacja graczy
|
|
this.player = {
|
|
x: 20,
|
|
y: this.canvas.height / 2 - this.paddleHeight / 2,
|
|
width: this.paddleWidth,
|
|
height: this.paddleHeight,
|
|
speed: 6,
|
|
dy: 0
|
|
};
|
|
|
|
this.bot = {
|
|
x: this.canvas.width - 30,
|
|
y: this.canvas.height / 2 - this.paddleHeight / 2,
|
|
width: this.paddleWidth,
|
|
height: this.paddleHeight,
|
|
speed: 3
|
|
};
|
|
|
|
// Piłka
|
|
this.ball = {
|
|
x: this.canvas.width / 2,
|
|
y: this.canvas.height / 2,
|
|
radius: 8,
|
|
speed: 5,
|
|
dx: 5,
|
|
dy: 3,
|
|
isServe: true
|
|
};
|
|
|
|
// Startowe "serwowanie": piłka leci wolniej, a dopiero po pierwszym odbiciu
|
|
// przechodzi na prędkość wynikającą z poziomu trudności.
|
|
this.serveSpeedMultiplier = 0.75;
|
|
|
|
// Sterowanie
|
|
this.keys = {};
|
|
this.mouseControl = {
|
|
enabled: true,
|
|
y: null
|
|
};
|
|
this.setupControls();
|
|
}
|
|
|
|
setupControls() {
|
|
// Event listenery na window
|
|
const keydownHandler = (e) => {
|
|
this.keys[e.key] = true;
|
|
// Zapobiegaj domyślnemu zachowaniu strzałek (scrollowanie)
|
|
if(['ArrowUp', 'ArrowDown', 'w', 's', 'W', 'S'].includes(e.key)) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
const keyupHandler = (e) => {
|
|
this.keys[e.key] = false;
|
|
};
|
|
|
|
window.addEventListener('keydown', keydownHandler);
|
|
window.addEventListener('keyup', keyupHandler);
|
|
|
|
// Sterowanie myszką: śledź kursor globalnie (również poza canvasem)
|
|
const mouseMoveHandler = (e) => {
|
|
if (!this.mouseControl.enabled) return;
|
|
if (!this.gameActive) return;
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
this.mouseControl.y = e.clientY - rect.top;
|
|
};
|
|
|
|
document.addEventListener('mousemove', mouseMoveHandler);
|
|
|
|
// Zapisz referencje do usunięcia później jeśli potrzeba
|
|
this.keydownHandler = keydownHandler;
|
|
this.keyupHandler = keyupHandler;
|
|
this.mouseMoveHandler = mouseMoveHandler;
|
|
}
|
|
|
|
start(mode, difficulty = 'easy') {
|
|
this.gameMode = mode;
|
|
this.difficulty = difficulty;
|
|
this.gameActive = true;
|
|
this.gameStartTime = Date.now();
|
|
this.gameEndTime = null;
|
|
this.resetGameState();
|
|
if (window.audioManager) {
|
|
window.audioManager.startBgMusic(mode, difficulty);
|
|
}
|
|
this.gameLoop();
|
|
}
|
|
|
|
stop() {
|
|
this.gameActive = false;
|
|
if (this.animationId) {
|
|
cancelAnimationFrame(this.animationId);
|
|
}
|
|
if (window.audioManager) {
|
|
window.audioManager.stopBgMusic();
|
|
}
|
|
}
|
|
|
|
resetGameState() {
|
|
this.playerScore = 0;
|
|
this.botScore = 0;
|
|
this.playerSets = 0;
|
|
this.botSets = 0;
|
|
this.pointPauseUntil = 0;
|
|
this.setBreakUntil = Date.now() + this.setBreakDelayMs;
|
|
|
|
this.player.y = this.canvas.height / 2 - this.paddleHeight / 2;
|
|
this.bot.y = this.canvas.height / 2 - this.paddleHeight / 2;
|
|
|
|
this.resetBall({ frozen: true });
|
|
}
|
|
|
|
resetBall(options = {}) {
|
|
const { frozen = false, direction = null } = options;
|
|
const speedMultiplier = this.serveSpeedMultiplier;
|
|
const serveDirection = (direction === 1 || direction === -1)
|
|
? direction
|
|
: (Math.random() > 0.5 ? 1 : -1);
|
|
this.ball.x = this.canvas.width / 2;
|
|
this.ball.y = this.canvas.height / 2;
|
|
|
|
if (frozen) {
|
|
this.ball.dx = 0;
|
|
this.ball.dy = 0;
|
|
this.ball.isServe = true;
|
|
return;
|
|
}
|
|
|
|
this.ball.dx = serveDirection * (5 * speedMultiplier);
|
|
this.ball.dy = (Math.random() - 0.5) * (6 * speedMultiplier);
|
|
this.ball.isServe = true;
|
|
}
|
|
|
|
getBallSpeedMultiplier() {
|
|
switch (this.difficulty) {
|
|
case 'extreme':
|
|
return 2.5;
|
|
case 'hard':
|
|
return 1.8;
|
|
case 'medium':
|
|
return 1.25;
|
|
case 'easy':
|
|
default:
|
|
return 1.0;
|
|
}
|
|
}
|
|
|
|
promoteBallSpeedAfterServe() {
|
|
if (!this.ball.isServe) return;
|
|
|
|
const targetMultiplier = this.getBallSpeedMultiplier();
|
|
const scale = targetMultiplier / this.serveSpeedMultiplier;
|
|
this.ball.dx *= scale;
|
|
this.ball.dy *= scale;
|
|
this.ball.isServe = false;
|
|
}
|
|
|
|
update() {
|
|
if (!this.gameActive) return;
|
|
|
|
const now = Date.now();
|
|
const isSetBreakPaused = this.setBreakUntil > now;
|
|
if (this.setBreakUntil !== 0) {
|
|
if (!isSetBreakPaused) {
|
|
this.setBreakUntil = 0;
|
|
this.resetBall();
|
|
}
|
|
}
|
|
|
|
const isPointPauseActive = this.pointPauseUntil > now;
|
|
if (this.pointPauseUntil !== 0) {
|
|
if (!isPointPauseActive) {
|
|
this.pointPauseUntil = 0;
|
|
this.resetBall();
|
|
}
|
|
}
|
|
|
|
// Ruch gracza (klawiatura ma priorytet, myszka działa gdy nie trzymasz klawiszy)
|
|
const usingKeyboard = !!(
|
|
this.keys['ArrowUp'] || this.keys['ArrowDown'] ||
|
|
this.keys['w'] || this.keys['W'] ||
|
|
this.keys['s'] || this.keys['S']
|
|
);
|
|
|
|
if (usingKeyboard) {
|
|
if (this.keys['ArrowUp'] || this.keys['w'] || this.keys['W']) {
|
|
this.player.y -= this.player.speed;
|
|
}
|
|
if (this.keys['ArrowDown'] || this.keys['s'] || this.keys['S']) {
|
|
this.player.y += this.player.speed;
|
|
}
|
|
} else if (this.mouseControl.enabled && this.mouseControl.y !== null) {
|
|
const targetY = this.mouseControl.y - this.player.height / 2;
|
|
// Płynne podążanie za kursorem
|
|
const smoothing = 0.35;
|
|
this.player.y += (targetY - this.player.y) * smoothing;
|
|
}
|
|
|
|
// Ograniczenia dla gracza
|
|
if (this.player.y < 0) this.player.y = 0;
|
|
if (this.player.y + this.player.height > this.canvas.height) {
|
|
this.player.y = this.canvas.height - this.player.height;
|
|
}
|
|
|
|
// AI Bota (jeśli tryb bot)
|
|
if (this.gameMode === 'bot' && window.botAI) {
|
|
window.botAI.update(this.bot, this.ball, this.difficulty, this.canvas.height);
|
|
}
|
|
|
|
// Ograniczenia dla bota
|
|
if (this.bot.y < 0) this.bot.y = 0;
|
|
if (this.bot.y + this.bot.height > this.canvas.height) {
|
|
this.bot.y = this.canvas.height - this.bot.height;
|
|
}
|
|
|
|
if (isSetBreakPaused || isPointPauseActive) {
|
|
return;
|
|
}
|
|
|
|
// Ruch piłki
|
|
this.ball.x += this.ball.dx;
|
|
this.ball.y += this.ball.dy;
|
|
|
|
// Kolizja ze ścianami (góra/dół)
|
|
if (this.ball.y - this.ball.radius < 0) {
|
|
this.ball.y = this.ball.radius;
|
|
this.ball.dy = Math.abs(this.ball.dy);
|
|
this.promoteBallSpeedAfterServe();
|
|
if (window.audioManager) {
|
|
window.audioManager.playRandomSound();
|
|
}
|
|
}
|
|
if (this.ball.y + this.ball.radius > this.canvas.height) {
|
|
this.ball.y = this.canvas.height - this.ball.radius;
|
|
this.ball.dy = -Math.abs(this.ball.dy);
|
|
this.promoteBallSpeedAfterServe();
|
|
if (window.audioManager) {
|
|
window.audioManager.playRandomSound();
|
|
}
|
|
}
|
|
|
|
// Kolizja z paletką gracza
|
|
if (this.ball.x - this.ball.radius < this.player.x + this.player.width &&
|
|
this.ball.x + this.ball.radius > this.player.x &&
|
|
this.ball.y > this.player.y &&
|
|
this.ball.y < this.player.y + this.player.height) {
|
|
|
|
this.ball.dx = Math.abs(this.ball.dx);
|
|
const hitPos = (this.ball.y - (this.player.y + this.player.height / 2)) / (this.player.height / 2);
|
|
const currentMultiplier = this.ball.isServe ? this.serveSpeedMultiplier : this.getBallSpeedMultiplier();
|
|
this.ball.dy = hitPos * 8 * currentMultiplier;
|
|
this.promoteBallSpeedAfterServe();
|
|
if (window.audioManager) {
|
|
window.audioManager.playRandomSound();
|
|
}
|
|
}
|
|
|
|
// Kolizja z paletką bota
|
|
if (this.ball.x + this.ball.radius > this.bot.x &&
|
|
this.ball.x - this.ball.radius < this.bot.x + this.bot.width &&
|
|
this.ball.y > this.bot.y &&
|
|
this.ball.y < this.bot.y + this.bot.height) {
|
|
|
|
this.ball.dx = -Math.abs(this.ball.dx);
|
|
const hitPos = (this.ball.y - (this.bot.y + this.bot.height / 2)) / (this.bot.height / 2);
|
|
const currentMultiplier = this.ball.isServe ? this.serveSpeedMultiplier : this.getBallSpeedMultiplier();
|
|
this.ball.dy = hitPos * 8 * currentMultiplier;
|
|
this.promoteBallSpeedAfterServe();
|
|
if (window.audioManager) {
|
|
window.audioManager.playRandomSound();
|
|
}
|
|
}
|
|
|
|
// Punktacja
|
|
if (this.ball.x - this.ball.radius < 0) {
|
|
this.awardPoint('bot');
|
|
}
|
|
|
|
if (this.ball.x + this.ball.radius > this.canvas.width) {
|
|
this.awardPoint('player');
|
|
}
|
|
}
|
|
|
|
awardPoint(side) {
|
|
if (side === 'player') {
|
|
this.playerScore += 1;
|
|
} else {
|
|
this.botScore += 1;
|
|
}
|
|
|
|
this.syncScoreUi();
|
|
|
|
if (this.isSetWon('player')) {
|
|
this.finishSet('player');
|
|
return;
|
|
}
|
|
|
|
if (this.isSetWon('bot')) {
|
|
this.finishSet('bot');
|
|
return;
|
|
}
|
|
|
|
this.pointPauseUntil = Date.now() + this.pointResetDelayMs;
|
|
this.resetBall({ frozen: true });
|
|
}
|
|
|
|
isSetWon(side) {
|
|
const score = side === 'player' ? this.playerScore : this.botScore;
|
|
const opponentScore = side === 'player' ? this.botScore : this.playerScore;
|
|
return score >= this.pointsToWin && (score - opponentScore) >= 2;
|
|
}
|
|
|
|
finishSet(side) {
|
|
if (side === 'player') {
|
|
this.playerSets += 1;
|
|
} else {
|
|
this.botSets += 1;
|
|
}
|
|
|
|
this.syncScoreUi();
|
|
|
|
const matchWon = (side === 'player' ? this.playerSets : this.botSets) >= this.setsToWin;
|
|
if (matchWon) {
|
|
this.gameActive = false;
|
|
this.gameEndTime = Date.now();
|
|
if (window.audioManager) {
|
|
if (side === 'player') {
|
|
window.audioManager.playWinSound();
|
|
} else {
|
|
window.audioManager.playLoseSound();
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (!window.uiManager) return;
|
|
if (side === 'player') {
|
|
window.uiManager.showWinModal(this.getFormattedTime());
|
|
} else {
|
|
window.uiManager.showLoseModal(this.getFormattedTime());
|
|
}
|
|
}, 500);
|
|
return;
|
|
}
|
|
|
|
this.playerScore = 0;
|
|
this.botScore = 0;
|
|
this.pointPauseUntil = 0;
|
|
this.setBreakUntil = Date.now() + this.setBreakDelayMs;
|
|
this.player.y = this.canvas.height / 2 - this.paddleHeight / 2;
|
|
this.bot.y = this.canvas.height / 2 - this.paddleHeight / 2;
|
|
this.resetBall({ frozen: true });
|
|
this.syncScoreUi();
|
|
}
|
|
|
|
syncScoreUi() {
|
|
if (window.uiManager) {
|
|
window.uiManager.updateScore(this.playerScore, this.botScore, this.playerSets, this.botSets);
|
|
}
|
|
}
|
|
|
|
draw() {
|
|
// Tło
|
|
this.ctx.fillStyle = '#0a0a0a';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Siatka
|
|
this.drawNet();
|
|
|
|
// Paletka gracza (niebieska)
|
|
this.drawRect(this.player.x, this.player.y, this.player.width, this.player.height, '#0080ff', '#0080ff');
|
|
|
|
// Paletka bota (czerwona)
|
|
this.drawRect(this.bot.x, this.bot.y, this.bot.width, this.bot.height, '#ff006e', '#ff006e');
|
|
|
|
// Piłka (cyjan)
|
|
this.drawCircle(this.ball.x, this.ball.y, this.ball.radius, '#00fff7', '#00fff7');
|
|
|
|
if (this.setBreakUntil > Date.now()) {
|
|
this.drawSetBreakAnimation();
|
|
}
|
|
}
|
|
|
|
drawSetBreakAnimation() {
|
|
const remainingMs = Math.max(0, this.setBreakUntil - Date.now());
|
|
const secondsLeft = Math.max(1, Math.ceil(remainingMs / 1000));
|
|
const secondProgress = 1 - ((remainingMs % 1000) / 1000);
|
|
const scale = 1 + (secondProgress * 0.22);
|
|
const alpha = 0.45 + (secondProgress * 0.35);
|
|
|
|
this.ctx.fillStyle = 'rgba(5, 10, 20, 0.55)';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
this.ctx.save();
|
|
this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
|
|
this.ctx.scale(scale, scale);
|
|
this.ctx.textAlign = 'center';
|
|
this.ctx.fillStyle = `rgba(0, 255, 247, ${alpha})`;
|
|
this.ctx.shadowBlur = 35;
|
|
this.ctx.shadowColor = '#00fff7';
|
|
this.ctx.font = 'bold 92px Orbitron, Arial, sans-serif';
|
|
this.ctx.fillText(String(secondsLeft), 0, 14);
|
|
this.ctx.restore();
|
|
}
|
|
|
|
drawRect(x, y, w, h, color, glow) {
|
|
this.ctx.fillStyle = color;
|
|
this.ctx.shadowBlur = 20;
|
|
this.ctx.shadowColor = glow;
|
|
this.ctx.fillRect(x, y, w, h);
|
|
this.ctx.shadowBlur = 0;
|
|
}
|
|
|
|
drawCircle(x, y, r, color, glow) {
|
|
this.ctx.fillStyle = color;
|
|
this.ctx.shadowBlur = 30;
|
|
this.ctx.shadowColor = glow;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
this.ctx.shadowBlur = 0;
|
|
}
|
|
|
|
drawNet() {
|
|
this.ctx.strokeStyle = 'rgba(0, 255, 247, 0.3)';
|
|
this.ctx.lineWidth = 2;
|
|
this.ctx.setLineDash([10, 10]);
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.canvas.width / 2, 0);
|
|
this.ctx.lineTo(this.canvas.width / 2, this.canvas.height);
|
|
this.ctx.stroke();
|
|
this.ctx.setLineDash([]);
|
|
}
|
|
|
|
gameLoop() {
|
|
this.update();
|
|
this.draw();
|
|
|
|
// Aktualizuj timer w HTML
|
|
const timerElement = document.getElementById('gameTimer');
|
|
if (timerElement) {
|
|
timerElement.textContent = this.getFormattedTime();
|
|
}
|
|
|
|
if (this.gameActive) {
|
|
this.animationId = requestAnimationFrame(() => this.gameLoop());
|
|
}
|
|
}
|
|
|
|
getScores() {
|
|
return {
|
|
player: this.playerScore,
|
|
bot: this.botScore,
|
|
playerSets: this.playerSets,
|
|
botSets: this.botSets
|
|
};
|
|
}
|
|
|
|
getGameTime() {
|
|
if (!this.gameStartTime) return 0;
|
|
const endTime = this.gameEndTime || Date.now();
|
|
return Math.floor((endTime - this.gameStartTime) / 1000);
|
|
}
|
|
|
|
getFormattedTime() {
|
|
const totalSeconds = this.getGameTime();
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
}
|
|
}
|
|
|
|
// Eksportuj do window
|
|
if (typeof window !== 'undefined') {
|
|
window.PingPongGame = PingPongGame;
|
|
}
|
|
|
|
})(); // End of IIFE
|