579 lines
17 KiB
HTML
579 lines
17 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>Platformer</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #87ceeb; 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();
|
|
}
|
|
|
|
// Cheerful platformer melody
|
|
const melody = [
|
|
{n: 523, d: 0.2}, {n: 587, d: 0.2}, {n: 659, d: 0.2}, {n: 784, d: 0.4},
|
|
{n: 659, d: 0.2}, {n: 523, d: 0.2}, {n: 587, d: 0.4},
|
|
{n: 440, d: 0.2}, {n: 494, d: 0.2}, {n: 523, d: 0.2}, {n: 659, d: 0.4},
|
|
{n: 587, d: 0.2}, {n: 523, d: 0.2}, {n: 494, 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: 110, d: 0.4}, {n: 131, d: 0.4}, {n: 147, d: 0.4}, {n: 131, d: 0.4}
|
|
];
|
|
|
|
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(300, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.15);
|
|
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
|
} else if (type === 'coin') {
|
|
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
|
|
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.08);
|
|
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
|
} else if (type === 'win') {
|
|
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
|
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.15);
|
|
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.3);
|
|
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.45);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
|
|
} else if (type === 'die') {
|
|
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.4);
|
|
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
|
|
osc.type = 'sawtooth';
|
|
}
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.7);
|
|
}
|
|
|
|
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;
|
|
|
|
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 levelComplete = false;
|
|
let score = 0;
|
|
let level = 1;
|
|
|
|
const player = { x: 50, y: 0, w: 30, h: 40, vx: 0, vy: 0, onGround: false, facingRight: true };
|
|
const gravity = 0.4;
|
|
const jumpForce = -10;
|
|
const moveSpeed = 4;
|
|
|
|
let platforms = [];
|
|
let coins = [];
|
|
let enemies = [];
|
|
let flag = null;
|
|
let cameraX = 0;
|
|
|
|
function generateLevel() {
|
|
platforms = [];
|
|
coins = [];
|
|
enemies = [];
|
|
|
|
const groundY = safeBottom - 40;
|
|
|
|
// Ground
|
|
platforms.push({ x: 0, y: groundY, w: 2000 + level * 500, h: 50 });
|
|
|
|
// Generate platforms
|
|
let lastX = 100;
|
|
const levelLength = 1500 + level * 300;
|
|
|
|
while (lastX < levelLength) {
|
|
const gap = 80 + Math.random() * 100;
|
|
const height = groundY - 80 - Math.random() * 150;
|
|
const width = 80 + Math.random() * 100;
|
|
|
|
platforms.push({ x: lastX + gap, y: height, w: width, h: 20 });
|
|
|
|
// Add coin on platform
|
|
if (Math.random() < 0.7) {
|
|
coins.push({ x: lastX + gap + width / 2, y: height - 30, collected: false });
|
|
}
|
|
|
|
// Add enemy on some platforms (not at start)
|
|
if (Math.random() < 0.3 && lastX > 300) {
|
|
enemies.push({
|
|
x: lastX + gap + 20,
|
|
y: height - 30,
|
|
w: 30,
|
|
h: 30,
|
|
startX: lastX + gap + 10,
|
|
endX: lastX + gap + width - 40,
|
|
dir: 1,
|
|
speed: 1 + level * 0.2
|
|
});
|
|
}
|
|
|
|
lastX += gap + width;
|
|
}
|
|
|
|
// Flag at end
|
|
flag = { x: levelLength, y: groundY - 100, w: 40, h: 100 };
|
|
|
|
// Reset player
|
|
player.x = 50;
|
|
player.y = groundY - player.h - 10;
|
|
player.vx = 0;
|
|
player.vy = 0;
|
|
player.onGround = true;
|
|
cameraX = 0;
|
|
}
|
|
|
|
function init() {
|
|
score = 0;
|
|
level = 1;
|
|
gameOver = false;
|
|
levelComplete = false;
|
|
gameRunning = true;
|
|
generateLevel();
|
|
startMusic();
|
|
}
|
|
|
|
function nextLevel() {
|
|
level++;
|
|
levelComplete = false;
|
|
generateLevel();
|
|
}
|
|
|
|
let keys = { left: false, right: false, jump: false };
|
|
|
|
document.addEventListener('keydown', e => {
|
|
initAudio();
|
|
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
|
|
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
|
|
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === ' ') {
|
|
keys.jump = true;
|
|
e.preventDefault();
|
|
}
|
|
if ((e.key === ' ' || e.key === 'Enter') && (gameOver || levelComplete)) {
|
|
if (levelComplete) nextLevel();
|
|
else init();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', e => {
|
|
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
|
|
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
|
|
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === ' ') keys.jump = false;
|
|
});
|
|
|
|
canvas.addEventListener('touchstart', e => {
|
|
e.preventDefault();
|
|
initAudio();
|
|
if (gameOver || levelComplete) {
|
|
if (levelComplete) nextLevel();
|
|
else init();
|
|
return;
|
|
}
|
|
const x = e.touches[0].clientX;
|
|
if (x < W / 3) keys.left = true;
|
|
else if (x > W * 2 / 3) keys.right = true;
|
|
else keys.jump = true;
|
|
});
|
|
|
|
canvas.addEventListener('touchend', e => {
|
|
keys.left = false;
|
|
keys.right = false;
|
|
keys.jump = false;
|
|
});
|
|
|
|
canvas.addEventListener('click', e => {
|
|
initAudio();
|
|
if (gameOver || levelComplete) {
|
|
if (levelComplete) nextLevel();
|
|
else init();
|
|
}
|
|
});
|
|
|
|
function update() {
|
|
if (!gameRunning || gameOver || levelComplete) return;
|
|
|
|
// Horizontal movement
|
|
if (keys.left) {
|
|
player.vx = -moveSpeed;
|
|
player.facingRight = false;
|
|
} else if (keys.right) {
|
|
player.vx = moveSpeed;
|
|
player.facingRight = true;
|
|
} else {
|
|
player.vx *= 0.8;
|
|
}
|
|
|
|
// Jump
|
|
if (keys.jump && player.onGround) {
|
|
player.vy = jumpForce;
|
|
player.onGround = false;
|
|
playSfx('jump');
|
|
}
|
|
|
|
// Gravity
|
|
player.vy += gravity;
|
|
|
|
// Move horizontally
|
|
player.x += player.vx;
|
|
if (player.x < 0) player.x = 0;
|
|
|
|
// Horizontal collision
|
|
for (const p of platforms) {
|
|
if (player.x + player.w > p.x - cameraX && player.x < p.x - cameraX + p.w &&
|
|
player.y + player.h > p.y && player.y < p.y + p.h) {
|
|
if (player.vx > 0) player.x = p.x - cameraX - player.w;
|
|
else if (player.vx < 0) player.x = p.x - cameraX + p.w;
|
|
}
|
|
}
|
|
|
|
// Move vertically
|
|
player.y += player.vy;
|
|
player.onGround = false;
|
|
|
|
// Vertical collision
|
|
for (const p of platforms) {
|
|
if (player.x + player.w > p.x - cameraX && player.x < p.x - cameraX + p.w &&
|
|
player.y + player.h > p.y && player.y < p.y + p.h) {
|
|
if (player.vy > 0) {
|
|
player.y = p.y - player.h;
|
|
player.vy = 0;
|
|
player.onGround = true;
|
|
} else if (player.vy < 0) {
|
|
player.y = p.y + p.h;
|
|
player.vy = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall off screen
|
|
if (player.y > H) {
|
|
gameOver = true;
|
|
gameRunning = false;
|
|
musicPlaying = false;
|
|
playSfx('die');
|
|
return;
|
|
}
|
|
|
|
// Collect coins
|
|
for (const c of coins) {
|
|
if (!c.collected) {
|
|
const cx = c.x - cameraX;
|
|
const cy = c.y;
|
|
if (player.x + player.w > cx - 15 && player.x < cx + 15 &&
|
|
player.y + player.h > cy - 15 && player.y < cy + 15) {
|
|
c.collected = true;
|
|
score += 100;
|
|
playSfx('coin');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update enemies
|
|
for (const e of enemies) {
|
|
e.x += e.dir * e.speed;
|
|
if (e.x < e.startX || e.x > e.endX) {
|
|
e.dir *= -1;
|
|
}
|
|
|
|
// Enemy collision
|
|
const ex = e.x - cameraX;
|
|
if (player.x + player.w - 5 > ex && player.x + 5 < ex + e.w &&
|
|
player.y + player.h > e.y && player.y < e.y + e.h) {
|
|
// Check if stomping
|
|
if (player.vy > 0 && player.y + player.h < e.y + e.h / 2) {
|
|
e.y = 9999; // Remove enemy
|
|
player.vy = -8;
|
|
score += 50;
|
|
playSfx('jump');
|
|
} else {
|
|
gameOver = true;
|
|
gameRunning = false;
|
|
musicPlaying = false;
|
|
playSfx('die');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check flag
|
|
if (flag) {
|
|
const fx = flag.x - cameraX;
|
|
if (player.x + player.w > fx && player.x < fx + flag.w) {
|
|
levelComplete = true;
|
|
score += 500;
|
|
playSfx('win');
|
|
}
|
|
}
|
|
|
|
// Camera follow
|
|
const targetCameraX = player.x + player.w / 2 + cameraX - W / 3;
|
|
cameraX += (targetCameraX - cameraX) * 0.1;
|
|
if (cameraX < 0) cameraX = 0;
|
|
|
|
updateMusic();
|
|
}
|
|
|
|
function draw() {
|
|
// Sky
|
|
ctx.fillStyle = '#87ceeb';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Safe zones
|
|
ctx.fillStyle = '#6bb9f0';
|
|
ctx.fillRect(0, 0, W, safeTop);
|
|
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
|
|
|
// Clouds
|
|
ctx.fillStyle = '#fff';
|
|
for (let i = 0; i < 5; i++) {
|
|
const x = ((i * 300 - cameraX * 0.2) % (W + 200)) - 100;
|
|
const y = safeTop + 30 + (i * 50) % 80;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 25, 0, Math.PI * 2);
|
|
ctx.arc(x + 30, y - 10, 30, 0, Math.PI * 2);
|
|
ctx.arc(x + 60, y, 25, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Header
|
|
ctx.fillStyle = '#333';
|
|
ctx.font = 'bold 22px Arial';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(`Score: ${score}`, 20, 35);
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(`Level ${level}`, W / 2, 35);
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(`Coins: ${coins.filter(c => c.collected).length}/${coins.length}`, W - 20, 35);
|
|
|
|
// Platforms
|
|
for (const p of platforms) {
|
|
const x = p.x - cameraX;
|
|
if (x + p.w > 0 && x < W) {
|
|
if (p.h > 30) {
|
|
// Ground
|
|
ctx.fillStyle = '#8B4513';
|
|
ctx.fillRect(x, p.y, p.w, p.h);
|
|
ctx.fillStyle = '#228B22';
|
|
ctx.fillRect(x, p.y, p.w, 15);
|
|
} else {
|
|
// Platform
|
|
ctx.fillStyle = '#8B4513';
|
|
ctx.fillRect(x, p.y, p.w, p.h);
|
|
ctx.fillStyle = '#654321';
|
|
ctx.fillRect(x, p.y, p.w, 5);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Coins
|
|
ctx.fillStyle = '#ffd700';
|
|
for (const c of coins) {
|
|
if (!c.collected) {
|
|
const x = c.x - cameraX;
|
|
if (x > -20 && x < W + 20) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, c.y, 12, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#ffed4a';
|
|
ctx.beginPath();
|
|
ctx.arc(x - 3, c.y - 3, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#ffd700';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enemies
|
|
ctx.fillStyle = '#e74c3c';
|
|
for (const e of enemies) {
|
|
const x = e.x - cameraX;
|
|
if (x > -50 && x < W + 50 && e.y < H) {
|
|
ctx.fillRect(x, e.y, e.w, e.h);
|
|
// Eyes
|
|
ctx.fillStyle = '#fff';
|
|
ctx.beginPath();
|
|
ctx.arc(x + 8, e.y + 10, 5, 0, Math.PI * 2);
|
|
ctx.arc(x + e.w - 8, e.y + 10, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#000';
|
|
ctx.beginPath();
|
|
ctx.arc(x + 8 + e.dir * 2, e.y + 10, 2, 0, Math.PI * 2);
|
|
ctx.arc(x + e.w - 8 + e.dir * 2, e.y + 10, 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#e74c3c';
|
|
}
|
|
}
|
|
|
|
// Flag
|
|
if (flag) {
|
|
const fx = flag.x - cameraX;
|
|
if (fx > -50 && fx < W + 50) {
|
|
ctx.fillStyle = '#8B4513';
|
|
ctx.fillRect(fx + 5, flag.y, 8, flag.h);
|
|
ctx.fillStyle = '#2ecc71';
|
|
ctx.beginPath();
|
|
ctx.moveTo(fx + 13, flag.y);
|
|
ctx.lineTo(fx + 50, flag.y + 25);
|
|
ctx.lineTo(fx + 13, flag.y + 50);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Player
|
|
ctx.fillStyle = '#3498db';
|
|
ctx.fillRect(player.x, player.y, player.w, player.h);
|
|
// Eyes
|
|
ctx.fillStyle = '#fff';
|
|
const eyeOffset = player.facingRight ? 5 : -5;
|
|
ctx.beginPath();
|
|
ctx.arc(player.x + player.w / 2 + eyeOffset, player.y + 12, 6, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#000';
|
|
ctx.beginPath();
|
|
ctx.arc(player.x + player.w / 2 + eyeOffset + (player.facingRight ? 2 : -2), player.y + 12, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Touch controls hint
|
|
ctx.fillStyle = '#666';
|
|
ctx.font = '14px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('Left side: Move Left | Center: Jump | Right side: Move Right', W / 2, safeBottom + 25);
|
|
|
|
if (levelComplete) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
ctx.fillStyle = '#2ecc71';
|
|
ctx.font = 'bold 40px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('LEVEL COMPLETE!', W / 2, H / 2 - 50);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = '24px Arial';
|
|
ctx.fillText(`Score: ${score}`, W / 2, H / 2);
|
|
|
|
ctx.fillStyle = '#2ecc71';
|
|
ctx.beginPath();
|
|
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 18px Arial';
|
|
ctx.fillText('NEXT LEVEL', W / 2, H / 2 + 62);
|
|
}
|
|
|
|
if (gameOver) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
ctx.fillStyle = '#e74c3c';
|
|
ctx.font = 'bold 40px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('GAME OVER', W / 2, H / 2 - 50);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = '24px Arial';
|
|
ctx.fillText(`Final Score: ${score}`, W / 2, H / 2);
|
|
|
|
ctx.fillStyle = '#3498db';
|
|
ctx.beginPath();
|
|
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 18px Arial';
|
|
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 62);
|
|
}
|
|
}
|
|
|
|
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 && !levelComplete) { gameRunning = true; startMusic(); } };
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
|
|
init();
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html>
|