togethere.cloud/public_html/disciplines/ping-pong/js/game.js

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