362 lines
11 KiB
HTML
362 lines
11 KiB
HTML
<!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>Dino Runner</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #f7f7f7; 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();
|
|
}
|
|
|
|
// Happy running melody
|
|
const melody = [
|
|
{n: 523, d: 0.15}, {n: 587, d: 0.15}, {n: 659, d: 0.15}, {n: 698, d: 0.15},
|
|
{n: 784, d: 0.3}, {n: 659, d: 0.15}, {n: 523, d: 0.15},
|
|
{n: 587, d: 0.15}, {n: 659, d: 0.15}, {n: 587, d: 0.15}, {n: 523, d: 0.3},
|
|
{n: 494, d: 0.15}, {n: 523, d: 0.15}, {n: 587, d: 0.15}, {n: 659, d: 0.15},
|
|
{n: 698, d: 0.3}, {n: 784, d: 0.3}, {n: 880, d: 0.3}, {n: 784, d: 0.15},
|
|
{n: 698, d: 0.15}, {n: 659, d: 0.3}, {n: 0, d: 0.3}
|
|
];
|
|
|
|
const bass = [
|
|
{n: 131, d: 0.3}, {n: 165, d: 0.3}, {n: 196, d: 0.3}, {n: 165, d: 0.3},
|
|
{n: 147, d: 0.3}, {n: 175, d: 0.3}, {n: 196, d: 0.3}, {n: 220, d: 0.3},
|
|
{n: 131, d: 0.3}, {n: 165, d: 0.3}, {n: 196, d: 0.6}
|
|
];
|
|
|
|
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 === 'jump') {
|
|
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.15);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
|
} else if (type === 'die') {
|
|
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.5);
|
|
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
|
|
} else if (type === 'point') {
|
|
osc.frequency.value = 880;
|
|
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
|
}
|
|
osc.type = 'square';
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.6);
|
|
}
|
|
|
|
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, groundY, safeTop;
|
|
|
|
function resize() {
|
|
W = window.innerWidth;
|
|
H = window.innerHeight;
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
safeTop = H * 0.1;
|
|
groundY = H * 0.75;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
let gameRunning = false;
|
|
let gameOver = false;
|
|
let score = 0;
|
|
let highScore = 0;
|
|
let gameSpeed = 2.5;
|
|
let spawnTimer = 0;
|
|
let spawnInterval = 200;
|
|
|
|
const dino = { x: 80, y: 0, w: 50, h: 60, vy: 0, jumping: false };
|
|
const gravity = 0.5;
|
|
const jumpForce = -13;
|
|
let obstacles = [];
|
|
let clouds = [];
|
|
let groundOffset = 0;
|
|
|
|
function init() {
|
|
score = 0;
|
|
gameSpeed = 2.5;
|
|
spawnInterval = 200;
|
|
spawnTimer = 0;
|
|
dino.y = groundY - dino.h;
|
|
dino.vy = 0;
|
|
dino.jumping = false;
|
|
obstacles = [];
|
|
clouds = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
clouds.push({ x: Math.random() * W, y: safeTop + 30 + Math.random() * 80, w: 60 + Math.random() * 40 });
|
|
}
|
|
gameOver = false;
|
|
gameRunning = true;
|
|
startMusic();
|
|
}
|
|
|
|
function jump() {
|
|
if (!dino.jumping && gameRunning && !gameOver) {
|
|
dino.vy = jumpForce;
|
|
dino.jumping = true;
|
|
playSfx('jump');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === ' ' || e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
initAudio();
|
|
if (gameOver) init();
|
|
else jump();
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('click', () => { initAudio(); if (gameOver) init(); else jump(); });
|
|
canvas.addEventListener('touchstart', e => { e.preventDefault(); initAudio(); if (gameOver) init(); else jump(); });
|
|
|
|
function spawnObstacle() {
|
|
const type = Math.random() < 0.7 ? 'cactus' : 'bird';
|
|
const h = type === 'cactus' ? 40 + Math.random() * 25 : 30;
|
|
const y = type === 'bird' ? groundY - 80 - Math.random() * 60 : groundY - h;
|
|
obstacles.push({ x: W + 50, y, w: 30, h, type });
|
|
}
|
|
|
|
function update() {
|
|
if (!gameRunning || gameOver) return;
|
|
|
|
// Dino physics
|
|
dino.vy += gravity;
|
|
dino.y += dino.vy;
|
|
if (dino.y >= groundY - dino.h) {
|
|
dino.y = groundY - dino.h;
|
|
dino.vy = 0;
|
|
dino.jumping = false;
|
|
}
|
|
|
|
// Spawn obstacles
|
|
spawnTimer++;
|
|
if (spawnTimer >= spawnInterval) {
|
|
spawnObstacle();
|
|
spawnTimer = 0;
|
|
spawnInterval = Math.max(80, 200 - score / 3);
|
|
}
|
|
|
|
// Move obstacles
|
|
for (let i = obstacles.length - 1; i >= 0; i--) {
|
|
obstacles[i].x -= gameSpeed;
|
|
if (obstacles[i].x + obstacles[i].w < 0) {
|
|
obstacles.splice(i, 1);
|
|
score++;
|
|
playSfx('point');
|
|
}
|
|
}
|
|
|
|
// Move clouds
|
|
clouds.forEach(c => {
|
|
c.x -= gameSpeed * 0.3;
|
|
if (c.x + c.w < 0) c.x = W + c.w;
|
|
});
|
|
|
|
// Ground scroll
|
|
groundOffset = (groundOffset + gameSpeed) % 20;
|
|
|
|
// Collision (generous hitbox)
|
|
for (let obs of obstacles) {
|
|
const padding = 12;
|
|
if (dino.x + dino.w - padding > obs.x + padding &&
|
|
dino.x + padding < obs.x + obs.w - padding &&
|
|
dino.y + dino.h - padding > obs.y + padding) {
|
|
gameOver = true;
|
|
gameRunning = false;
|
|
musicPlaying = false;
|
|
if (score > highScore) highScore = score;
|
|
playSfx('die');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Increase speed gradually
|
|
if (gameSpeed < 10) gameSpeed += 0.001;
|
|
|
|
updateMusic();
|
|
}
|
|
|
|
function draw() {
|
|
// Sky
|
|
ctx.fillStyle = '#87CEEB';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Safe zone
|
|
ctx.fillStyle = '#6BB9F0';
|
|
ctx.fillRect(0, 0, W, safeTop);
|
|
|
|
// Score
|
|
ctx.fillStyle = '#333';
|
|
ctx.font = 'bold 24px Arial';
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(`Score: ${score}`, W - 20, 35);
|
|
ctx.font = '16px Arial';
|
|
ctx.fillText(`Best: ${highScore}`, W - 20, 55);
|
|
|
|
// Clouds
|
|
ctx.fillStyle = '#fff';
|
|
clouds.forEach(c => {
|
|
ctx.beginPath();
|
|
ctx.arc(c.x, c.y, 20, 0, Math.PI * 2);
|
|
ctx.arc(c.x + 25, c.y - 10, 25, 0, Math.PI * 2);
|
|
ctx.arc(c.x + 50, c.y, 20, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
|
|
// Ground
|
|
ctx.fillStyle = '#8B4513';
|
|
ctx.fillRect(0, groundY, W, H - groundY);
|
|
ctx.fillStyle = '#228B22';
|
|
ctx.fillRect(0, groundY - 10, W, 15);
|
|
|
|
// Ground detail
|
|
ctx.fillStyle = '#654321';
|
|
for (let x = -groundOffset; x < W; x += 20) {
|
|
ctx.fillRect(x, groundY + 5, 2, 10);
|
|
}
|
|
|
|
// Obstacles
|
|
obstacles.forEach(obs => {
|
|
if (obs.type === 'cactus') {
|
|
ctx.fillStyle = '#228B22';
|
|
ctx.fillRect(obs.x, obs.y, obs.w, obs.h);
|
|
ctx.fillRect(obs.x - 8, obs.y + 10, 8, 15);
|
|
ctx.fillRect(obs.x + obs.w, obs.y + 15, 8, 12);
|
|
} else {
|
|
ctx.fillStyle = '#8B4513';
|
|
ctx.beginPath();
|
|
ctx.ellipse(obs.x + obs.w / 2, obs.y + obs.h / 2, obs.w / 2, obs.h / 2, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#FFA500';
|
|
ctx.beginPath();
|
|
ctx.moveTo(obs.x, obs.y + obs.h / 2);
|
|
ctx.lineTo(obs.x - 10, obs.y + obs.h / 2 - 5);
|
|
ctx.lineTo(obs.x - 10, obs.y + obs.h / 2 + 5);
|
|
ctx.fill();
|
|
}
|
|
});
|
|
|
|
// Dino
|
|
ctx.fillStyle = '#2ECC71';
|
|
ctx.fillRect(dino.x, dino.y, dino.w, dino.h);
|
|
// Eye
|
|
ctx.fillStyle = '#fff';
|
|
ctx.beginPath();
|
|
ctx.arc(dino.x + dino.w - 12, dino.y + 15, 8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#000';
|
|
ctx.beginPath();
|
|
ctx.arc(dino.x + dino.w - 10, dino.y + 15, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// Legs
|
|
const legOffset = dino.jumping ? 0 : Math.sin(Date.now() / 80) * 5;
|
|
ctx.fillStyle = '#2ECC71';
|
|
ctx.fillRect(dino.x + 8, dino.y + dino.h, 10, 12 + legOffset);
|
|
ctx.fillRect(dino.x + dino.w - 18, dino.y + dino.h, 10, 12 - legOffset);
|
|
|
|
if (gameOver) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 40px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('GAME OVER', W / 2, H / 2 - 40);
|
|
ctx.font = '28px Arial';
|
|
ctx.fillText(`Score: ${score}`, W / 2, H / 2 + 10);
|
|
|
|
ctx.fillStyle = '#2ECC71';
|
|
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>
|