Initial commit
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Pong</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; touch-action: none; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game"></canvas>
|
||||
<script>
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
let audioCtx = null;
|
||||
let musicEnabled = true;
|
||||
let sfxEnabled = true;
|
||||
let musicPlaying = false;
|
||||
|
||||
function initAudio() {
|
||||
if (!audioCtx) audioCtx = new AudioContext();
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
}
|
||||
|
||||
// Retro arcade melody
|
||||
const melody = [
|
||||
{n: 392, d: 0.2}, {n: 440, d: 0.2}, {n: 494, d: 0.2}, {n: 523, d: 0.4},
|
||||
{n: 494, d: 0.2}, {n: 440, d: 0.2}, {n: 392, d: 0.4},
|
||||
{n: 330, d: 0.2}, {n: 349, d: 0.2}, {n: 392, d: 0.2}, {n: 440, d: 0.4},
|
||||
{n: 392, d: 0.2}, {n: 349, d: 0.2}, {n: 330, d: 0.4},
|
||||
{n: 294, d: 0.2}, {n: 330, d: 0.2}, {n: 349, d: 0.2}, {n: 392, d: 0.4},
|
||||
{n: 440, d: 0.2}, {n: 494, d: 0.2}, {n: 523, d: 0.4}, {n: 0, d: 0.4}
|
||||
];
|
||||
|
||||
const bass = [
|
||||
{n: 98, d: 0.4}, {n: 98, d: 0.4}, {n: 110, d: 0.4}, {n: 110, d: 0.4},
|
||||
{n: 82, d: 0.4}, {n: 82, d: 0.4}, {n: 98, d: 0.4}, {n: 98, d: 0.4},
|
||||
{n: 73, d: 0.4}, {n: 73, d: 0.4}, {n: 82, d: 0.4}, {n: 82, d: 0.4},
|
||||
{n: 98, d: 0.4}, {n: 110, d: 0.4}, {n: 131, d: 0.8}
|
||||
];
|
||||
|
||||
let melodyIdx = 0, bassIdx = 0;
|
||||
let nextMelody = 0, nextBass = 0;
|
||||
|
||||
function playTone(freq, dur, type = 'square', vol = 0.12) {
|
||||
if (!audioCtx || !musicEnabled || freq === 0) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + dur);
|
||||
}
|
||||
|
||||
function playSfx(type) {
|
||||
if (!audioCtx || !sfxEnabled) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
if (type === 'hit') {
|
||||
osc.frequency.value = 440;
|
||||
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
||||
} else if (type === 'score') {
|
||||
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
||||
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
|
||||
} else if (type === 'wall') {
|
||||
osc.frequency.value = 220;
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.05);
|
||||
}
|
||||
osc.type = 'square';
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + 0.3);
|
||||
}
|
||||
|
||||
function updateMusic() {
|
||||
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
||||
const now = audioCtx.currentTime;
|
||||
if (now >= nextMelody) {
|
||||
const note = melody[melodyIdx];
|
||||
playTone(note.n, note.d, 'square', 0.1);
|
||||
nextMelody = now + note.d;
|
||||
melodyIdx = (melodyIdx + 1) % melody.length;
|
||||
}
|
||||
if (now >= nextBass) {
|
||||
const note = bass[bassIdx];
|
||||
playTone(note.n, note.d, 'triangle', 0.18);
|
||||
nextBass = now + note.d;
|
||||
bassIdx = (bassIdx + 1) % bass.length;
|
||||
}
|
||||
}
|
||||
|
||||
function startMusic() {
|
||||
if (!musicPlaying && musicEnabled) {
|
||||
initAudio();
|
||||
musicPlaying = true;
|
||||
nextMelody = audioCtx.currentTime;
|
||||
nextBass = audioCtx.currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W, H, safeTop, safeBottom;
|
||||
|
||||
function resize() {
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
safeTop = H * 0.1;
|
||||
safeBottom = H * 0.9;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let gameRunning = false;
|
||||
let gameOver = false;
|
||||
let playerScore = 0;
|
||||
let cpuScore = 0;
|
||||
const winScore = 7;
|
||||
|
||||
const paddleW = 15;
|
||||
const paddleH = 100;
|
||||
const ballSize = 12;
|
||||
let ballSpeed = 3;
|
||||
|
||||
const player = { y: 0, speed: 8 };
|
||||
const cpu = { y: 0, speed: 2, mistakeChance: 0.35 };
|
||||
const ball = { x: 0, y: 0, dx: 0, dy: 0 };
|
||||
|
||||
let targetY = H / 2;
|
||||
let keys = { up: false, down: false };
|
||||
|
||||
function resetBall(towardPlayer = true) {
|
||||
ball.x = W / 2;
|
||||
ball.y = H / 2;
|
||||
const angle = (Math.random() - 0.5) * Math.PI / 3;
|
||||
ball.dx = (towardPlayer ? -1 : 1) * Math.cos(angle) * ballSpeed;
|
||||
ball.dy = Math.sin(angle) * ballSpeed;
|
||||
}
|
||||
|
||||
function init() {
|
||||
playerScore = 0;
|
||||
cpuScore = 0;
|
||||
ballSpeed = 3;
|
||||
cpu.speed = 2;
|
||||
cpu.mistakeChance = 0.35;
|
||||
player.y = H / 2 - paddleH / 2;
|
||||
cpu.y = H / 2 - paddleH / 2;
|
||||
resetBall(true);
|
||||
gameOver = false;
|
||||
gameRunning = true;
|
||||
startMusic();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'ArrowUp' || e.key === 'w') keys.up = true;
|
||||
if (e.key === 'ArrowDown' || e.key === 's') keys.down = true;
|
||||
if ((e.key === ' ' || e.key === 'Enter') && gameOver) init();
|
||||
initAudio();
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.key === 'ArrowUp' || e.key === 'w') keys.up = false;
|
||||
if (e.key === 'ArrowDown' || e.key === 's') keys.down = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', e => { targetY = e.clientY; });
|
||||
canvas.addEventListener('click', () => { initAudio(); if (gameOver) init(); });
|
||||
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
initAudio();
|
||||
if (gameOver) init();
|
||||
targetY = e.touches[0].clientY;
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
targetY = e.touches[0].clientY;
|
||||
});
|
||||
|
||||
function update() {
|
||||
if (!gameRunning || gameOver) return;
|
||||
|
||||
// Player movement
|
||||
if (keys.up) player.y -= player.speed;
|
||||
if (keys.down) player.y += player.speed;
|
||||
player.y += (targetY - player.y - paddleH / 2) * 0.1;
|
||||
player.y = Math.max(safeTop, Math.min(safeBottom - paddleH, player.y));
|
||||
|
||||
// CPU movement (makes mistakes)
|
||||
const cpuTarget = ball.y - paddleH / 2;
|
||||
let dir = Math.sign(cpuTarget - cpu.y);
|
||||
if (Math.random() < cpu.mistakeChance) dir = -dir;
|
||||
if (Math.abs(cpuTarget - cpu.y) > 15) cpu.y += dir * cpu.speed;
|
||||
cpu.y = Math.max(safeTop, Math.min(safeBottom - paddleH, cpu.y));
|
||||
|
||||
// Ball movement
|
||||
ball.x += ball.dx;
|
||||
ball.y += ball.dy;
|
||||
|
||||
// Top/bottom walls
|
||||
if (ball.y - ballSize < safeTop || ball.y + ballSize > safeBottom) {
|
||||
ball.dy = -ball.dy;
|
||||
ball.y = Math.max(safeTop + ballSize, Math.min(safeBottom - ballSize, ball.y));
|
||||
playSfx('wall');
|
||||
}
|
||||
|
||||
// Player paddle
|
||||
if (ball.dx < 0 && ball.x - ballSize <= 30 + paddleW &&
|
||||
ball.x > 30 && ball.y >= player.y - 10 && ball.y <= player.y + paddleH + 10) {
|
||||
ball.dx = Math.abs(ball.dx) * 1.02;
|
||||
ball.dy += ((ball.y - player.y - paddleH / 2) / paddleH) * 3;
|
||||
playSfx('hit');
|
||||
}
|
||||
|
||||
// CPU paddle
|
||||
if (ball.dx > 0 && ball.x + ballSize >= W - 30 - paddleW &&
|
||||
ball.x < W - 30 && ball.y >= cpu.y - 10 && ball.y <= cpu.y + paddleH + 10) {
|
||||
ball.dx = -Math.abs(ball.dx) * 1.02;
|
||||
ball.dy += ((ball.y - cpu.y - paddleH / 2) / paddleH) * 3;
|
||||
playSfx('hit');
|
||||
}
|
||||
|
||||
// Scoring
|
||||
if (ball.x < 0) {
|
||||
cpuScore++;
|
||||
playSfx('score');
|
||||
if (cpuScore >= winScore) { gameOver = true; gameRunning = false; }
|
||||
else resetBall(true);
|
||||
}
|
||||
if (ball.x > W) {
|
||||
playerScore++;
|
||||
playSfx('score');
|
||||
if (cpu.mistakeChance > 0.15) cpu.mistakeChance -= 0.03;
|
||||
if (cpu.speed < 3.5) cpu.speed += 0.15;
|
||||
if (playerScore >= winScore) { gameOver = true; gameRunning = false; }
|
||||
else resetBall(false);
|
||||
}
|
||||
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Safe zones
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(0, 0, W, safeTop);
|
||||
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
||||
|
||||
// Center line
|
||||
ctx.setLineDash([15, 15]);
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(W / 2, safeTop);
|
||||
ctx.lineTo(W / 2, safeBottom);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Scores
|
||||
ctx.fillStyle = '#0f0';
|
||||
ctx.font = 'bold 48px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(playerScore, W / 4, safeTop - 15);
|
||||
ctx.fillStyle = '#f00';
|
||||
ctx.fillText(cpuScore, W * 3 / 4, safeTop - 15);
|
||||
|
||||
// Labels
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = '#0f0';
|
||||
ctx.fillText('YOU', W / 4, 20);
|
||||
ctx.fillStyle = '#f00';
|
||||
ctx.fillText('CPU', W * 3 / 4, 20);
|
||||
|
||||
// Paddles
|
||||
ctx.fillStyle = '#0f0';
|
||||
ctx.shadowColor = '#0f0';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.fillRect(30, player.y, paddleW, paddleH);
|
||||
ctx.fillStyle = '#f00';
|
||||
ctx.shadowColor = '#f00';
|
||||
ctx.fillRect(W - 30 - paddleW, cpu.y, paddleW, paddleH);
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Ball
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.shadowColor = '#fff';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(ball.x, ball.y, ballSize, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
if (gameOver) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = playerScore >= winScore ? '#0f0' : '#f00';
|
||||
ctx.font = 'bold 40px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(playerScore >= winScore ? 'YOU WIN!' : 'CPU WINS', W / 2, H / 2 - 30);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '24px Arial';
|
||||
ctx.fillText(`${playerScore} - ${cpuScore}`, W / 2, H / 2 + 20);
|
||||
|
||||
ctx.fillStyle = '#0f0';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(W / 2 - 100, H / 2 + 50, 200, 50, 10);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 82);
|
||||
}
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.gameScore = 0;
|
||||
Object.defineProperty(window, 'gameScore', { get: () => playerScore * 100 });
|
||||
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
|
||||
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
|
||||
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
||||
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
||||
|
||||
init();
|
||||
gameLoop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user