Initial commit
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
<!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>Whack-a-Mole</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #8B4513; touch-action: none; font-family: Arial, sans-serif; }
|
||||
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();
|
||||
}
|
||||
|
||||
// Playful carnival melody
|
||||
const melody = [
|
||||
{n: 523, d: 0.2}, {n: 659, d: 0.2}, {n: 784, d: 0.2}, {n: 659, d: 0.2},
|
||||
{n: 523, d: 0.2}, {n: 659, d: 0.2}, {n: 784, d: 0.4},
|
||||
{n: 698, d: 0.2}, {n: 659, d: 0.2}, {n: 587, d: 0.2}, {n: 523, d: 0.4},
|
||||
{n: 587, d: 0.2}, {n: 659, d: 0.2}, {n: 698, d: 0.2}, {n: 784, d: 0.2},
|
||||
{n: 880, d: 0.4}, {n: 784, d: 0.2}, {n: 698, d: 0.2}, {n: 659, d: 0.4}, {n: 0, d: 0.2}
|
||||
];
|
||||
|
||||
const bass = [
|
||||
{n: 131, d: 0.4}, {n: 165, d: 0.4}, {n: 196, d: 0.4}, {n: 165, d: 0.4},
|
||||
{n: 175, d: 0.4}, {n: 147, d: 0.4}, {n: 131, d: 0.4}, {n: 165, d: 0.4},
|
||||
{n: 196, d: 0.4}, {n: 220, d: 0.4}, {n: 196, d: 0.4}, {n: 165, d: 0.4}
|
||||
];
|
||||
|
||||
let melodyIdx = 0, bassIdx = 0, 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 === 'whack') {
|
||||
osc.frequency.setValueAtTime(200, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
||||
osc.type = 'sawtooth';
|
||||
} else if (type === 'pop') {
|
||||
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
||||
osc.type = 'square';
|
||||
} else if (type === 'miss') {
|
||||
osc.frequency.value = 150;
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
|
||||
osc.type = 'triangle';
|
||||
}
|
||||
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, gameArea;
|
||||
|
||||
function resize() {
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
safeTop = H * 0.1;
|
||||
safeBottom = H * 0.9;
|
||||
gameArea = { x: 20, y: safeTop + 20, w: W - 40, h: safeBottom - safeTop - 40 };
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let gameRunning = false;
|
||||
let gameOver = false;
|
||||
let score = 0;
|
||||
let timeLeft = 60;
|
||||
let moleUpTime = 2500;
|
||||
let lastMoleTime = 0;
|
||||
let currentMole = -1;
|
||||
let moleState = 'down'; // down, up, hit
|
||||
let moleY = 0;
|
||||
let holes = [];
|
||||
|
||||
function initHoles() {
|
||||
holes = [];
|
||||
const cols = 3, rows = 3;
|
||||
const holeW = Math.min(gameArea.w / cols - 20, 100);
|
||||
const holeH = holeW * 0.6;
|
||||
const gapX = (gameArea.w - cols * holeW) / (cols + 1);
|
||||
const gapY = (gameArea.h - rows * holeH) / (rows + 1);
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
holes.push({
|
||||
x: gameArea.x + gapX + c * (holeW + gapX),
|
||||
y: gameArea.y + gapY + r * (holeH + gapY),
|
||||
w: holeW,
|
||||
h: holeH
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
score = 0;
|
||||
timeLeft = 60;
|
||||
moleUpTime = 2500;
|
||||
currentMole = -1;
|
||||
moleState = 'down';
|
||||
moleY = 0;
|
||||
initHoles();
|
||||
gameOver = false;
|
||||
gameRunning = true;
|
||||
lastMoleTime = Date.now();
|
||||
startMusic();
|
||||
}
|
||||
|
||||
function showMole() {
|
||||
if (moleState !== 'down') return;
|
||||
currentMole = Math.floor(Math.random() * 9);
|
||||
moleState = 'rising';
|
||||
moleY = 0;
|
||||
playSfx('pop');
|
||||
}
|
||||
|
||||
function handleClick(x, y) {
|
||||
if (gameOver) { init(); return; }
|
||||
if (!gameRunning) return;
|
||||
|
||||
for (let i = 0; i < holes.length; i++) {
|
||||
const h = holes[i];
|
||||
if (x >= h.x && x <= h.x + h.w && y >= h.y && y <= h.y + h.h) {
|
||||
if (i === currentMole && (moleState === 'up' || moleState === 'rising')) {
|
||||
moleState = 'hit';
|
||||
score += 10;
|
||||
playSfx('whack');
|
||||
setTimeout(() => {
|
||||
moleState = 'down';
|
||||
currentMole = -1;
|
||||
}, 300);
|
||||
} else {
|
||||
playSfx('miss');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
initAudio();
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
if (gameOver) init();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('click', e => {
|
||||
initAudio();
|
||||
handleClick(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
initAudio();
|
||||
handleClick(e.touches[0].clientX, e.touches[0].clientY);
|
||||
});
|
||||
|
||||
let lastTime = Date.now();
|
||||
let timerAccum = 0;
|
||||
|
||||
function update() {
|
||||
const now = Date.now();
|
||||
const dt = now - lastTime;
|
||||
lastTime = now;
|
||||
|
||||
if (!gameRunning || gameOver) return;
|
||||
|
||||
// Timer
|
||||
timerAccum += dt;
|
||||
if (timerAccum >= 1000) {
|
||||
timeLeft--;
|
||||
timerAccum = 0;
|
||||
if (timeLeft <= 0) {
|
||||
gameOver = true;
|
||||
gameRunning = false;
|
||||
musicPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Mole animation
|
||||
if (moleState === 'rising') {
|
||||
moleY += 5;
|
||||
if (moleY >= 100) {
|
||||
moleY = 100;
|
||||
moleState = 'up';
|
||||
lastMoleTime = now;
|
||||
}
|
||||
} else if (moleState === 'up') {
|
||||
if (now - lastMoleTime > moleUpTime) {
|
||||
moleState = 'falling';
|
||||
}
|
||||
} else if (moleState === 'falling') {
|
||||
moleY -= 5;
|
||||
if (moleY <= 0) {
|
||||
moleY = 0;
|
||||
moleState = 'down';
|
||||
currentMole = -1;
|
||||
}
|
||||
} else if (moleState === 'down') {
|
||||
if (now - lastMoleTime > 500) {
|
||||
showMole();
|
||||
}
|
||||
}
|
||||
|
||||
// Gradually speed up
|
||||
if (moleUpTime > 1000) moleUpTime -= 0.5;
|
||||
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Background
|
||||
ctx.fillStyle = '#5D4037';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Safe zones
|
||||
ctx.fillStyle = '#4E342E';
|
||||
ctx.fillRect(0, 0, W, safeTop);
|
||||
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${score}`, 20, 35);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`Time: ${timeLeft}s`, W - 20, 35);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('WHACK-A-MOLE', W / 2, 35);
|
||||
|
||||
// Grass background
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.fillRect(gameArea.x, gameArea.y, gameArea.w, gameArea.h);
|
||||
|
||||
// Draw holes
|
||||
holes.forEach((h, i) => {
|
||||
// Hole shadow
|
||||
ctx.fillStyle = '#2E1B0F';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(h.x + h.w / 2, h.y + h.h * 0.7, h.w / 2, h.h / 3, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Mole
|
||||
if (i === currentMole && moleState !== 'down') {
|
||||
const moleH = h.h * 0.8 * (moleY / 100);
|
||||
const moleW = h.w * 0.7;
|
||||
const moleX = h.x + (h.w - moleW) / 2;
|
||||
const moleBottom = h.y + h.h * 0.7;
|
||||
const moleTop = moleBottom - moleH;
|
||||
|
||||
// Mole body
|
||||
ctx.fillStyle = moleState === 'hit' ? '#ff6b6b' : '#8D6E63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(moleX + moleW / 2, moleTop + moleH / 2, moleW / 2, moleH / 2, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Eyes
|
||||
if (moleY > 50) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(moleX + moleW * 0.35, moleTop + moleH * 0.35, 8, 0, Math.PI * 2);
|
||||
ctx.arc(moleX + moleW * 0.65, moleTop + moleH * 0.35, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = moleState === 'hit' ? '#ff0000' : '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(moleX + moleW * 0.35, moleTop + moleH * 0.35, 4, 0, Math.PI * 2);
|
||||
ctx.arc(moleX + moleW * 0.65, moleTop + moleH * 0.35, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Nose
|
||||
ctx.fillStyle = '#E91E63';
|
||||
ctx.beginPath();
|
||||
ctx.arc(moleX + moleW / 2, moleTop + moleH * 0.55, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Hole front (covers mole bottom)
|
||||
ctx.fillStyle = '#3E2723';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(h.x + h.w / 2, h.y + h.h * 0.7, h.w / 2, h.h / 4, 0, 0, Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
if (gameOver) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.8)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 40px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText("TIME'S UP!", W / 2, H / 2 - 40);
|
||||
ctx.font = '28px Arial';
|
||||
ctx.fillText(`Final Score: ${score}`, W / 2, H / 2 + 10);
|
||||
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(W / 2 - 100, H / 2 + 40, 200, 50, 10);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 72);
|
||||
}
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.gameScore = 0;
|
||||
Object.defineProperty(window, 'gameScore', { get: () => score });
|
||||
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