<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Platform Jumper</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: linear-gradient(to bottom, #7ec0ee, #cceeff);
font-family: Arial, sans-serif;
touch-action: none;
}
#gameCanvas {
display: block;
margin: 0 auto;
background: linear-gradient(to bottom, #87CEEB, #e0f7ff);
touch-action: none;
}
#ui {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
font-size: 22px;
color: #333;
font-weight: bold;
text-shadow: 1px 1px 0 #fff;
pointer-events: none;
}
#msg {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
color: #b00;
font-weight: bold;
text-shadow: 2px 2px 0 #fff;
display: none;
text-align: center;
}
</style>
</head>
<body>
<div id="ui">Score: 0</div>
<div id="msg">Game Over!<br><span style="font-size:20px">Press SPACE to restart</span></div>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const ui = document.getElementById('ui');
const msg = document.getElementById('msg');
let keys = {};
document.addEventListener('keydown', e => {
if (['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key)) e.preventDefault();
keys[e.key] = true;
if (e.key === ' ' && gameOver) resetGame();
});
document.addEventListener('keyup', e => {
keys[e.key] = false;
});
let player, platforms, score, gameOver, cameraY;
let level, levelFlash;
let hats, hasHat, hatTimer;
let rockets, hasRocket, rocketTimer;
let enemies, lives;
function resetGame() {
player = {
x: W/2 - 15,
y: H - 100,
w: 30,
h: 30,
vx: 0,
vy: 0,
color: '#ff6b6b'
};
platforms = [];
platforms.push({x: W/2 - 40, y: H - 60, w: 80, h: 12, spring: false});
let y = H - 60;
while (y > -H) {
y -= 60 + Math.random()*30;
let x = Math.random() * (W - 80);
let isSpring = Math.random() < 0.18;
platforms.push({x: x, y: y, w: 80, h: 12, spring: isSpring});
}
score = 0;
gameOver = false;
cameraY = 0;
level = 1;
levelFlash = 0;
hats = [];
hasHat = false;
hatTimer = 0;
rockets = [];
hasRocket = false;
rocketTimer = 0;
enemies = [];
lives = 3;
msg.style.display = 'none';
}
function update() {
if (gameOver) return;
if (keys['ArrowLeft']) player.vx = -5;
else if (keys['ArrowRight']) player.vx = 5;
else player.vx = 0;
if (keys['ArrowUp'] && player.onGround) {
player.vy = -13;
player.onGround = false;
}
player.vy += 0.5;
player.x += player.vx;
player.y += player.vy;
if (player.x + player.w < 0) player.x = W;
if (player.x > W) player.x = -player.w;
player.onGround = false;
if (hasHat) {
hatTimer--;
player.vy = -8;
if (hatTimer <= 0) hasHat = false;
}
if (hasRocket) {
rocketTimer--;
player.vy = -16;
if (rocketTimer <= 0) hasRocket = false;
}
for (let p of platforms) {
if (player.vy > 0 &&
player.x + player.w > p.x &&
player.x < p.x + p.w &&
player.y + player.h > p.y &&
player.y + player.h < p.y + p.h + player.vy + 1) {
if (p.spring) {
player.vy = -22;
player.onGround = false;
} else {
player.y = p.y - player.h;
player.vy = 0;
player.onGround = true;
}
}
}
if (Math.random() < 0.08 && hats.length < 4) {
let choices = platforms.filter(p => p.y < player.y - 40 && !p.hasItem);
if (choices.length) {
let p = choices[Math.floor(Math.random()*choices.length)];
p.hasItem = true;
hats.push({x: p.x + p.w/2 - 13, y: p.y - 24, w: 26, h: 20, homeP: p});
}
}
for (let h of hats) {
if (!hasHat &&
player.x + player.w > h.x &&
player.x < h.x + h.w &&
player.y + player.h > h.y &&
player.y < h.y + h.h) {
hasHat = true;
hatTimer = 120;
h.collected = true;
}
}
hats = hats.filter(h => !h.collected && h.y < H + 200);
if (Math.random() < 0.05 && rockets.length < 2) {
let choices = platforms.filter(p => p.y < player.y - 40 && !p.hasItem);
if (choices.length) {
let p = choices[Math.floor(Math.random()*choices.length)];
p.hasItem = true;
rockets.push({x: p.x + p.w/2 - 11, y: p.y - 36, w: 22, h: 34, homeP: p});
}
}
for (let r of rockets) {
if (!hasRocket &&
player.x + player.w > r.x &&
player.x < r.x + r.w &&
player.y + player.h > r.y &&
player.y < r.y + r.h) {
hasRocket = true;
rocketTimer = 90;
r.collected = true;
}
}
rockets = rockets.filter(r => !r.collected && r.y < H + 200);
if (player.y < H/2) {
let diff = H/2 - player.y;
player.y = H/2;
for (let p of platforms) p.y += diff;
for (let en of enemies) { en.y += diff; en.baseY += diff; }
score += Math.floor(diff);
let newLevel = Math.floor(score / 500) + 1;
if (newLevel > level) {
level = newLevel;
levelFlash = 90;
}
}
platforms = platforms.filter(p => p.y < H + 20);
while (platforms.length < 10) {
let topY = Math.min(...platforms.map(p => p.y));
let gapBoost = Math.min(level * 2, 20);
let x = Math.random() * (W - 80);
let y = topY - (60 + Math.random()*30 + gapBoost);
let isSpring = Math.random() < 0.18;
platforms.push({x: x, y: y, w: 80, h: 12, spring: isSpring, hasItem: false});
}
for (let h of hats) if (h.homeP) h.y = h.homeP.y - 24;
for (let r of rockets) if (r.homeP) r.y = r.homeP.y - 36;
if (player.y > H + 40) {
gameOver = true;
msg.style.display = 'block';
}
if (Math.random() < 0.012 && enemies.length < 1 + Math.min(level, 3)) {
let ex = Math.random() * (W - 40);
let ey = player.y - H/2 - Math.random()*300 - 100;
enemies.push({x: ex, y: ey, w: 34, h: 26, dir: Math.random() < 0.5 ? -1 : 1, baseY: ey, phase: Math.random()*Math.PI*2, hit: false});
}
for (let en of enemies) {
en.x += en.dir * 2;
if (en.x < 0 || en.x > W - en.w) en.dir *= -1;
en.phase += 0.05;
en.y = en.baseY + Math.sin(en.phase) * 15;
if (!en.hit && !hasRocket &&
player.x + player.w > en.x &&
player.x < en.x + en.w &&
player.y + player.h > en.y &&
player.y < en.y + en.h) {
en.hit = true;
lives--;
player.vy = -10;
if (lives <= 0) {
gameOver = true;
msg.style.display = 'block';
}
}
}
enemies = enemies.filter(en => !en.hit && en.y < H + 200 && en.y > -3000);
ui.textContent = 'Score: ' + score + ' Level ' + level + ' (' + getTheme().name + ') Hearts: ' + '❤️'.repeat(Math.max(lives,0));
if (levelFlash > 0) levelFlash--;
}
function getTheme() {
let t = Math.floor((level - 1) / 3) % 5;
if (t === 0) return { sky1: '#87CEEB', sky2: '#e0f7ff', dot: 'rgba(255,255,255,0.6)', platTop: '#5a3921', platSide: '#8bc34a', name: 'Sky' };
if (t === 1) return { sky1: '#b3e5fc', sky2: '#ffffff', dot: 'rgba(255,255,255,0.9)', platTop: '#7fa8c9', platSide: '#e0f7ff', name: 'Winter', snow: true };
if (t === 2) return { sky1: '#0288d1', sky2: '#4fc3f7', dot: 'rgba(255,255,255,0.35)', platTop: '#01579b', platSide: '#4dd0e1', name: 'Ocean', bubbles: true };
if (t === 3) return { sky1: '#4a0e0e', sky2: '#ff7043', dot: 'rgba(255,180,0,0.5)', platTop: '#3e2723', platSide: '#ff5722', name: 'Lava', lava: true };
return { sky1: '#0d0d2b', sky2: '#241b4a', dot: 'rgba(255,255,255,0.8)', platTop: '#37474f', platSide: '#7e57c2', name: 'Night', stars: true };
}
function draw() {
let theme = getTheme();
let grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, theme.sky1);
grad.addColorStop(1, theme.sky2);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = theme.dot;
for (let i = 0; i < 5; i++) {
ctx.beginPath();
ctx.arc(60 + i*70, 80 + (i%2)*200, theme.snow ? 8 : (theme.bubbles ? 12 : 25), 0, Math.PI*2);
ctx.fill();
}
if (theme.snow) {
ctx.fillStyle = 'white';
for (let i = 0; i < 30; i++) {
let sx = (i * 53 + (Date.now()/20)) % W;
let sy = (i * 97 + Date.now()/10) % H;
ctx.beginPath();
ctx.arc(sx, sy, 3, 0, Math.PI*2);
ctx.fill();
}
}
if (theme.bubbles) {
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 2;
for (let i = 0; i < 12; i++) {
let bx = (i * 71 + 20) % W;
let by = H - ((i * 130 + Date.now()/8) % (H + 100));
ctx.beginPath();
ctx.arc(bx, by, 6 + (i%3)*3, 0, Math.PI*2);
ctx.stroke();
}
}
if (theme.lava) {
ctx.fillStyle = 'rgba(255,140,0,0.5)';
for (let i = 0; i < 8; i++) {
let fx = (i * 61 + Date.now()/15) % W;
let fy = H - ((i * 90) % H);
ctx.beginPath();
ctx.arc(fx, fy, 5, 0, Math.PI*2);
ctx.fill();
}
}
if (theme.stars) {
ctx.fillStyle = 'white';
for (let i = 0; i < 40; i++) {
let sx = (i * 37) % W;
let sy = (i * 83) % H;
let tw = 1 + Math.sin(Date.now()/300 + i) * 0.8;
ctx.beginPath();
ctx.arc(sx, sy, tw, 0, Math.PI*2);
ctx.fill();
}
}
for (let p of platforms) {
if (p.spring) {
ctx.fillStyle = '#a0522d';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#ffd700';
ctx.fillRect(p.x, p.y, p.w, 5);
ctx.strokeStyle = '#fff59d';
ctx.lineWidth = 3;
ctx.beginPath();
for (let i = 0; i < 3; i++) {
let sx = p.x + 15 + i*25;
ctx.moveTo(sx, p.y);
ctx.lineTo(sx-5, p.y-6);
ctx.lineTo(sx+5, p.y-12);
ctx.lineTo(sx, p.y-18);
}
ctx.stroke();
} else {
ctx.fillStyle = theme.platTop;
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = theme.platSide;
ctx.fillRect(p.x, p.y, p.w, 5);
}
}
for (let r of rockets) {
ctx.save();
ctx.translate(r.x + r.w/2, r.y + r.h/2);
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.moveTo(0, -r.h/2);
ctx.lineTo(r.w/2, r.h/4);
ctx.lineTo(-r.w/2, r.h/4);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#90caf9';
ctx.beginPath();
ctx.arc(0, -2, 5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ff9800';
ctx.beginPath();
ctx.moveTo(-6, r.h/4);
ctx.lineTo(0, r.h/4 + 12 + Math.random()*6);
ctx.lineTo(6, r.h/4);
ctx.closePath();
ctx.fill();
ctx.restore();
}
for (let en of enemies) {
ctx.save();
ctx.translate(en.x + en.w/2, en.y + en.h/2);
ctx.fillStyle = '#8e24aa';
ctx.beginPath();
ctx.ellipse(0, 0, en.w/2, en.h/2, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(-7, -3, 5, 0, Math.PI*2);
ctx.arc(7, -3, 5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(-7 + en.dir*2, -3, 2, 0, Math.PI*2);
ctx.arc(7 + en.dir*2, -3, 2, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#5e1370';
ctx.lineWidth = 3;
let flap = Math.sin(Date.now()/80) * 6;
ctx.beginPath();
ctx.moveTo(-en.w/2, 0);
ctx.lineTo(-en.w/2 - 10, -6 + flap);
ctx.moveTo(en.w/2, 0);
ctx.lineTo(en.w/2 + 10, -6 - flap);
ctx.stroke();
ctx.restore();
}
for (let h of hats) {
ctx.save();
ctx.translate(h.x + h.w/2, h.y + h.h/2);
ctx.fillStyle = '#e91e63';
ctx.beginPath();
ctx.arc(0, 4, 13, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#333';
ctx.fillRect(-16, 4, 32, 4);
let spin = (Date.now()/40) % (Math.PI*2);
ctx.strokeStyle = '#ffca28';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-14*Math.cos(spin), -6-14*Math.sin(spin));
ctx.lineTo(14*Math.cos(spin), -6+14*Math.sin(spin));
ctx.stroke();
ctx.restore();
}
if (levelFlash > 0) {
ctx.save();
let t = 90 - levelFlash;
let pop = t < 15 ? (t/15) : 1;
let bounce = Math.sin(t/6) * 4;
ctx.globalAlpha = Math.min(1, levelFlash/30);
ctx.translate(W/2, H/2 - 100 + bounce);
ctx.scale(pop, pop);
ctx.font = 'bold 46px Arial';
ctx.textAlign = 'center';
ctx.strokeStyle = '#333';
ctx.lineWidth = 4;
ctx.strokeText('LEVEL UP!', 0, -20);
ctx.fillStyle = '#ffca28';
ctx.fillText('LEVEL UP!', 0, -20);
ctx.font = 'bold 34px Arial';
ctx.strokeText('Level ' + level, 0, 25);
ctx.fillStyle = '#fff';
ctx.fillText('Level ' + level, 0, 25);
ctx.restore();
}
ctx.save();
ctx.translate(player.x + player.w/2, player.y + player.h/2);
let wiggle = Math.sin(Date.now()/80) * (player.onGround ? 0.1 : 0.25);
ctx.rotate(wiggle);
if (hasRocket) {
ctx.fillStyle = '#ff9800';
ctx.beginPath();
ctx.moveTo(-6, player.h/2);
ctx.lineTo(0, player.h/2 + 14 + Math.random()*8);
ctx.lineTo(6, player.h/2);
ctx.closePath();
ctx.fill();
}
if (hasHat) {
let spin = (Date.now()/30) % (Math.PI*2);
ctx.fillStyle = '#e91e63';
ctx.beginPath();
ctx.arc(0, -player.h/2, 13, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#333';
ctx.fillRect(-16, -player.h/2, 32, 4);
ctx.strokeStyle = '#ffca28';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-14*Math.cos(spin), -player.h/2-6-14*Math.sin(spin));
ctx.lineTo(14*Math.cos(spin), -player.h/2-6+14*Math.sin(spin));
ctx.stroke();
}
ctx.fillStyle = player.color;
ctx.beginPath();
ctx.ellipse(0, 0, player.w/2, player.h/2, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(-6, -4, 5, 0, Math.PI*2);
ctx.arc(6, -4, 5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(-6, -4, 2, 0, Math.PI*2);
ctx.arc(6, -4, 2, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(0, 2, 6, 0, Math.PI);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.strokeStyle = player.color;
ctx.lineWidth = 4;
let armWiggle = Math.sin(Date.now()/60) * 15;
ctx.beginPath();
ctx.moveTo(-player.w/2, 0);
ctx.lineTo(-player.w/2 - 10, -10 + armWiggle);
ctx.moveTo(player.w/2, 0);
ctx.lineTo(player.w/2 + 10, -10 - armWiggle);
ctx.stroke();
ctx.restore();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
resetGame();
loop();
</script>
</body>
</html>