Initial commit
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
<!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>Geometry Runner</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; 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();
|
||||
}
|
||||
|
||||
// Energetic electronic melody
|
||||
const melody = [
|
||||
{n: 523, d: 0.15}, {n: 523, d: 0.15}, {n: 659, d: 0.15}, {n: 523, d: 0.15},
|
||||
{n: 784, d: 0.3}, {n: 659, d: 0.3},
|
||||
{n: 523, d: 0.15}, {n: 523, d: 0.15}, {n: 659, d: 0.15}, {n: 523, d: 0.15},
|
||||
{n: 698, d: 0.3}, {n: 659, d: 0.3},
|
||||
{n: 587, d: 0.15}, {n: 587, d: 0.15}, {n: 698, d: 0.15}, {n: 587, d: 0.15},
|
||||
{n: 880, d: 0.3}, {n: 784, d: 0.3}, {n: 0, d: 0.15}
|
||||
];
|
||||
|
||||
const bass = [
|
||||
{n: 131, d: 0.3}, {n: 131, d: 0.3}, {n: 165, d: 0.3}, {n: 165, d: 0.3},
|
||||
{n: 175, d: 0.3}, {n: 175, d: 0.3}, {n: 147, d: 0.3}, {n: 147, d: 0.3}
|
||||
];
|
||||
|
||||
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
|
||||
|
||||
function playTone(freq, dur, type = 'square', vol = 0.1) {
|
||||
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(400, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
||||
} else if (type === 'die') {
|
||||
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.4);
|
||||
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
|
||||
osc.type = 'sawtooth';
|
||||
} else if (type === 'coin') {
|
||||
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
|
||||
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.05);
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
||||
}
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + 0.5);
|
||||
}
|
||||
|
||||
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.08);
|
||||
nextMelody = now + note.d;
|
||||
melodyIdx = (melodyIdx + 1) % melody.length;
|
||||
}
|
||||
if (now >= nextBass) {
|
||||
const note = bass[bassIdx];
|
||||
playTone(note.n, note.d, 'triangle', 0.12);
|
||||
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, groundY;
|
||||
|
||||
function resize() {
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
safeTop = H * 0.1;
|
||||
safeBottom = H * 0.9;
|
||||
groundY = safeBottom - 50;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let gameRunning = false;
|
||||
let gameOver = false;
|
||||
let score = 0;
|
||||
let distance = 0;
|
||||
let speed = 3;
|
||||
let spawnTimer = 0;
|
||||
|
||||
const player = { x: 100, y: 0, size: 35, vy: 0, jumping: false, rotation: 0 };
|
||||
const gravity = 0.5;
|
||||
const jumpForce = -12;
|
||||
|
||||
let obstacles = [];
|
||||
let coins = [];
|
||||
let particles = [];
|
||||
let groundOffset = 0;
|
||||
let bgOffset = 0;
|
||||
|
||||
function init() {
|
||||
player.y = groundY - player.size;
|
||||
player.vy = 0;
|
||||
player.jumping = false;
|
||||
player.rotation = 0;
|
||||
obstacles = [];
|
||||
coins = [];
|
||||
particles = [];
|
||||
score = 0;
|
||||
distance = 0;
|
||||
speed = 3;
|
||||
spawnTimer = 0;
|
||||
gameOver = false;
|
||||
gameRunning = true;
|
||||
startMusic();
|
||||
}
|
||||
|
||||
function jump() {
|
||||
if (!player.jumping && gameRunning && !gameOver) {
|
||||
player.vy = jumpForce;
|
||||
player.jumping = true;
|
||||
playSfx('jump');
|
||||
}
|
||||
}
|
||||
|
||||
function spawnObstacle() {
|
||||
const type = Math.random() < 0.7 ? 'spike' : 'block';
|
||||
const h = type === 'spike' ? 35 : 40 + Math.random() * 20;
|
||||
const w = type === 'spike' ? 30 : 35;
|
||||
obstacles.push({
|
||||
x: W + 50,
|
||||
y: groundY - h,
|
||||
w,
|
||||
h,
|
||||
type
|
||||
});
|
||||
|
||||
// Sometimes spawn a coin
|
||||
if (Math.random() < 0.4) {
|
||||
coins.push({
|
||||
x: W + 50 + Math.random() * 100,
|
||||
y: groundY - 80 - Math.random() * 60,
|
||||
size: 20
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addParticles(x, y, color) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const spd = 2 + Math.random() * 3;
|
||||
particles.push({
|
||||
x, y,
|
||||
dx: Math.cos(angle) * spd,
|
||||
dy: Math.sin(angle) * spd,
|
||||
life: 30,
|
||||
color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
initAudio();
|
||||
if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'w') {
|
||||
e.preventDefault();
|
||||
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 update() {
|
||||
if (!gameRunning || gameOver) return;
|
||||
|
||||
// Player physics
|
||||
player.vy += gravity;
|
||||
player.y += player.vy;
|
||||
if (player.y >= groundY - player.size) {
|
||||
player.y = groundY - player.size;
|
||||
player.vy = 0;
|
||||
player.jumping = false;
|
||||
}
|
||||
|
||||
// Rotation while jumping
|
||||
if (player.jumping) {
|
||||
player.rotation += 0.15;
|
||||
} else {
|
||||
player.rotation = 0;
|
||||
}
|
||||
|
||||
// Spawn obstacles
|
||||
spawnTimer++;
|
||||
const spawnInterval = Math.max(80, 180 - distance / 50);
|
||||
if (spawnTimer >= spawnInterval) {
|
||||
spawnObstacle();
|
||||
spawnTimer = 0;
|
||||
}
|
||||
|
||||
// Move obstacles
|
||||
for (let i = obstacles.length - 1; i >= 0; i--) {
|
||||
obstacles[i].x -= speed;
|
||||
if (obstacles[i].x + obstacles[i].w < 0) {
|
||||
obstacles.splice(i, 1);
|
||||
score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Move coins
|
||||
for (let i = coins.length - 1; i >= 0; i--) {
|
||||
coins[i].x -= speed;
|
||||
if (coins[i].x + coins[i].size < 0) {
|
||||
coins.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Collision with obstacles (generous hitbox)
|
||||
for (const obs of obstacles) {
|
||||
const padding = 10;
|
||||
if (player.x + player.size - padding > obs.x + padding &&
|
||||
player.x + padding < obs.x + obs.w - padding &&
|
||||
player.y + player.size - padding > obs.y + padding) {
|
||||
gameOver = true;
|
||||
gameRunning = false;
|
||||
musicPlaying = false;
|
||||
addParticles(player.x + player.size / 2, player.y + player.size / 2, '#ff6b6b');
|
||||
playSfx('die');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect coins
|
||||
for (let i = coins.length - 1; i >= 0; i--) {
|
||||
const c = coins[i];
|
||||
const dx = (player.x + player.size / 2) - c.x;
|
||||
const dy = (player.y + player.size / 2) - c.y;
|
||||
if (Math.sqrt(dx * dx + dy * dy) < player.size / 2 + c.size / 2) {
|
||||
coins.splice(i, 1);
|
||||
score += 50;
|
||||
addParticles(c.x, c.y, '#ffd700');
|
||||
playSfx('coin');
|
||||
}
|
||||
}
|
||||
|
||||
// Update particles
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i];
|
||||
p.x += p.dx;
|
||||
p.y += p.dy;
|
||||
p.dy += 0.2;
|
||||
p.life--;
|
||||
if (p.life <= 0) particles.splice(i, 1);
|
||||
}
|
||||
|
||||
// Update distance and speed
|
||||
distance++;
|
||||
if (speed < 8) speed += 0.001;
|
||||
|
||||
// Background scroll
|
||||
groundOffset = (groundOffset + speed) % 40;
|
||||
bgOffset = (bgOffset + speed * 0.3) % 200;
|
||||
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Background gradient
|
||||
const grad = ctx.createLinearGradient(0, safeTop, 0, groundY);
|
||||
grad.addColorStop(0, '#1a1a2e');
|
||||
grad.addColorStop(1, '#16213e');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Safe zones
|
||||
ctx.fillStyle = '#0f0f1a';
|
||||
ctx.fillRect(0, 0, W, safeTop);
|
||||
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
||||
|
||||
// Background shapes
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = ((i * 200) - bgOffset) % W + W;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, groundY - 100);
|
||||
ctx.lineTo(x + 80, groundY);
|
||||
ctx.lineTo(x - 80, groundY);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Ground
|
||||
ctx.fillStyle = '#4ade80';
|
||||
ctx.fillRect(0, groundY, W, safeBottom - groundY);
|
||||
|
||||
// Ground pattern
|
||||
ctx.fillStyle = '#22c55e';
|
||||
for (let x = -groundOffset; x < W; x += 40) {
|
||||
ctx.fillRect(x, groundY, 2, 10);
|
||||
}
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 22px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${score}`, 20, 35);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`Distance: ${Math.floor(distance / 10)}m`, W - 20, 35);
|
||||
|
||||
// Particles
|
||||
for (const p of particles) {
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.globalAlpha = p.life / 30;
|
||||
ctx.fillRect(p.x - 3, p.y - 3, 6, 6);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Coins
|
||||
ctx.fillStyle = '#ffd700';
|
||||
for (const c of coins) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x, c.y, c.size / 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffed4a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x - 3, c.y - 3, c.size / 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffd700';
|
||||
}
|
||||
|
||||
// Obstacles
|
||||
for (const obs of obstacles) {
|
||||
if (obs.type === 'spike') {
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(obs.x + obs.w / 2, obs.y);
|
||||
ctx.lineTo(obs.x + obs.w, obs.y + obs.h);
|
||||
ctx.lineTo(obs.x, obs.y + obs.h);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.fillRect(obs.x, obs.y, obs.w, obs.h);
|
||||
}
|
||||
}
|
||||
|
||||
// Player
|
||||
ctx.save();
|
||||
ctx.translate(player.x + player.size / 2, player.y + player.size / 2);
|
||||
ctx.rotate(player.rotation);
|
||||
ctx.fillStyle = '#00d4ff';
|
||||
ctx.shadowColor = '#00d4ff';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.fillRect(-player.size / 2, -player.size / 2, player.size, player.size);
|
||||
ctx.shadowBlur = 0;
|
||||
// Eyes
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(-player.size / 4, -player.size / 4, 8, 8);
|
||||
ctx.fillRect(player.size / 8, -player.size / 4, 8, 8);
|
||||
ctx.restore();
|
||||
|
||||
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('GAME OVER', W / 2, H / 2 - 50);
|
||||
ctx.font = '24px Arial';
|
||||
ctx.fillText(`Score: ${score}`, W / 2, H / 2);
|
||||
ctx.fillText(`Distance: ${Math.floor(distance / 10)}m`, W / 2, H / 2 + 35);
|
||||
|
||||
ctx.fillStyle = '#00d4ff';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(W / 2 - 100, H / 2 + 60, 200, 50, 10);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 92);
|
||||
}
|
||||
|
||||
// Controls hint
|
||||
if (!gameOver) {
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Tap or Space to jump', W / 2, safeBottom + 25);
|
||||
}
|
||||
}
|
||||
|
||||
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