猎豹跳跳
by ElectricPenguin69433 lines18.6 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>猎豹跳跳</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; overflow: hidden; touch-action: none; }
canvas { display: block; margin: 0 auto; background: #16213e; }
#info {
position: fixed; top: 10px; left: 50%; transform: translateX(-50%);
color: #eee; font-family: monospace; font-size: 14px; text-align: center;
pointer-events: none; z-index: 10; text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
#gameover {
position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
color: #fff; font-family: monospace; font-size: 24px; text-align: center;
display: none; z-index: 20;
}
#gameover button {
margin-top: 15px; padding: 10px 30px; font-size: 18px;
background: #e94560; color: #fff; border: none; border-radius: 8px; cursor: pointer;
}
</style>
</head>
<body>
<div id="info">
<div>分数: <span id="score">0</span> | Combo: <span id="combo">x1</span> | 跳跃: <span id="jumps">0</span>/50</div>
<div style="font-size:12px;opacity:0.7;">空格/鼠标/触屏 蓄力跳跃</div>
</div>
<div id="gameover">
<div id="go-text">游戏结束</div>
<div id="go-score" style="font-size:18px;margin:10px 0;"></div>
<button onclick="restart()">再来一次</button>
</div>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
let audioCtx;
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
function playTone(freq, dur, type='sine', vol=0.3) {
if (!audioCtx) 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.001, audioCtx.currentTime + dur);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); osc.stop(audioCtx.currentTime + dur);
}
function sfxJump() { playTone(600, 0.15, 'sine', 0.2); playTone(800, 0.1, 'sine', 0.15); }
function sfxPerfect() { playTone(1200, 0.1, 'bell', 0.3); playTone(1600, 0.15, 'bell', 0.2); }
function sfxGood() { playTone(800, 0.1, 'square', 0.2); }
function sfxMiss() { playTone(200, 0.4, 'sawtooth', 0.3); playTone(150, 0.5, 'sawtooth', 0.2); }
function sfxSpring() { playTone(400, 0.1, 'square', 0.3); playTone(800, 0.15, 'square', 0.2); }
function sfxBossHit() { playTone(300, 0.2, 'sawtooth', 0.4); playTone(200, 0.3, 'square', 0.3); }
function sfxBossDead() { playTone(150, 0.8, 'sawtooth', 0.5); playTone(100, 1.0, 'square', 0.4); }
let score = 0, combo = 1, comboCount = 0, totalJumps = 0;
let gameRunning = true, inBoss = false, bossPhase = 1, bossHP = 5, bossMaxHP = 5;
let blocks = [], particles = [], floatingTexts = [], bullets = [];
const leopard = {
x: 0, y: 0, vy: 0, vx: 0, w: 30, h: 20,
charging: false, chargeTime: 0, jumping: false,
squash: 1, stretch: 1, eyeBlink: 0, frozen: 0,
onBlock: null, landed: false, rollAnim: 0,
draw() {
ctx.save();
ctx.translate(this.x, this.y - this.h/2);
let sy = this.squash, sx = this.stretch;
if (this.charging) { sy = 1 + this.chargeTime*0.3; sx = 1 - this.chargeTime*0.1; }
ctx.scale(sx, sy);
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath(); ctx.ellipse(0, this.h/2+2, 15, 5, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#e67e22';
roundRect(-15, -12, 30, 18, 8); ctx.fill();
ctx.fillStyle = '#2c3e50';
ctx.beginPath(); ctx.arc(-5,-5,2,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(3,-2,1.5,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(-8,2,1.5,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#e67e22';
ctx.beginPath(); ctx.arc(12, -6, 10, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.moveTo(8,-14); ctx.lineTo(12,-18); ctx.lineTo(16,-13); ctx.fill();
if (this.eyeBlink > 0) {
ctx.strokeStyle = '#2c3e50'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(14,-8); ctx.lineTo(18,-8); ctx.stroke();
} else {
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(15,-8,3,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#2c3e50'; ctx.beginPath(); ctx.arc(16,-8,1.5,0,Math.PI*2); ctx.fill();
}
ctx.strokeStyle = '#e67e22'; ctx.lineWidth = 4; ctx.lineCap = 'round';
ctx.beginPath();
let tailWag = Math.sin(Date.now()*0.005)*5;
ctx.moveTo(-12, 0); ctx.quadraticCurveTo(-20, -5+tailWag, -18, -12+tailWag);
ctx.stroke();
if (this.frozen > 0) { ctx.strokeStyle = '#74b9ff'; ctx.lineWidth = 2; ctx.strokeRect(-16, -14, 34, 26); }
ctx.restore();
},
update() {
if (!gameRunning) return;
if (this.charging) this.chargeTime = Math.min(this.chargeTime + 0.016, 1.2);
if (this.jumping) {
this.vy += 0.6; this.y += this.vy; this.x += this.vx;
if (inBoss) {
if (this.vy > 0 && this.y >= boss.y - 60 && this.y <= boss.y - 50 && this.x > boss.x - 80 && this.x < boss.x + 80) {
this.landOnBoss();
}
if (this.y > H + 50) this.die();
} else {
for (let b of blocks) {
if (this.vy > 0 && this.y >= b.y - b.h/2 - this.h/2 && this.y <= b.y - b.h/2 + 5 && this.x > b.x - b.w/2 && this.x < b.x + b.w/2) {
this.land(b); break;
}
}
if (this.y > H + 50) this.die();
}
}
if (this.onBlock && this.onBlock.type === 'crack' && this.onBlock.crackTimer > 0) {
this.onBlock.crackTimer -= 0.016;
if (this.onBlock.crackTimer <= 0) this.die();
}
if (this.frozen > 0 && this.landed) { this.frozen--; this.landed = false; }
if (Math.random() < 0.01) this.eyeBlink = 10;
if (this.eyeBlink > 0) this.eyeBlink--;
},
land(block) {
this.jumping = false; this.y = block.y - block.h/2 - this.h/2; this.vy = 0; this.vx = 0;
this.onBlock = block; this.landed = true;
if (block.type === 'spring') {
sfxSpring(); this.jumping = true; this.vy = -14; this.vx = 0;
comboCount = 0; combo = 1; updateUI(); return;
}
if (block.type === 'ice') this.frozen = 2;
if (block.type === 'crack' && block.crackTimer <= 0) block.crackTimer = 1.0;
let dist = Math.abs(this.x - block.x);
let halfW = block.w / 2;
if (dist <= halfW * 0.3) {
sfxPerfect(); comboCount++; combo = Math.min(1 + comboCount * 0.2, 2.0);
let pts = Math.round(100 * combo); score += pts;
floatingTexts.push({x:this.x, y:this.y-40, text:'PERFECT! +'+pts, color:'#f1c40f', life:60});
spawnParticles(this.x, this.y-10, '#f1c40f', 8);
} else if (dist <= halfW * 0.7) {
sfxGood(); comboCount = 0; combo = 1; score += 50;
floatingTexts.push({x:this.x, y:this.y-40, text:'GOOD +50', color:'#95a5a6', life:60});
} else { sfxMiss(); this.die(); return; }
totalJumps++; updateUI();
if (totalJumps > 0 && totalJumps % 50 === 0) startBoss();
},
landOnBoss() {
this.jumping = false; this.y = boss.y - 60 - this.h/2; this.vy = 0; this.vx = 0;
// 判定是否踩中核心
let dist = Math.abs(this.x - boss.x);
if (dist < 25) {
// 命中核心
sfxBossHit(); bossHP--; comboCount++; combo = Math.min(1 + comboCount * 0.2, 2.0);
score += 200; floatingTexts.push({x:this.x, y:this.y-40, text:'BOSS HIT! +200', color:'#e74c3c', life:60});
spawnParticles(this.x, this.y-10, '#e74c3c', 15);
if (bossHP <= 0) { bossDefeat(); return; }
} else {
sfxGood(); comboCount = 0; combo = 1; score += 50;
floatingTexts.push({x:this.x, y:this.y-40, text:'GOOD +50', color:'#95a5a6', life:60});
}
updateUI();
},
die() {
gameRunning = false; sfxMiss();
spawnParticles(this.x, this.y, '#e74c3c', 20);
document.getElementById('go-score').innerText = '得分: ' + score + ' | 最高Combo: x' + combo.toFixed(1);
document.getElementById('gameover').style.display = 'block';
},
jump() {
if (!gameRunning || this.jumping) return;
initAudio();
let power = this.chargeTime / 1.2;
if (power < 0.05) power = 0.05;
if (inBoss) {
let dx = boss.x - this.x;
let speed = power * 18;
this.vx = dx > 0 ? speed * 0.3 : -speed * 0.3;
this.vy = -speed * 0.8;
this.jumping = true; this.charging = false; this.chargeTime = 0;
sfxJump();
return;
}
let nextBlock = null;
for (let b of blocks) { if (b.y < this.y - 20) { nextBlock = b; break; } }
if (!nextBlock) return;
let dx = nextBlock.x - this.x, dy = nextBlock.y - this.y;
let dist = Math.sqrt(dx*dx+dy*dy);
let speed = power * 18;
if (this.frozen > 0) speed *= 0.6;
this.vx = (dx/dist)*speed; this.vy = -speed*0.8;
this.jumping = true; this.charging = false; this.chargeTime = 0;
this.onBlock = null; sfxJump();
blocks = blocks.filter(b => b.y >= this.y - 100);
generateAhead(nextBlock);
},
startCharge() {
if (!gameRunning || this.jumping) return;
initAudio(); this.charging = true; this.chargeTime = 0;
}
};
const boss = {
x: 0, y: 0, w: 200, h: 120, moveDir: 1, moveTimer: 0, shootTimer: 0,
draw() {
ctx.save();
// Boss 身体
ctx.fillStyle = '#8e44ad';
roundRect(this.x - this.w/2, this.y - this.h, this.w, this.h, 12); ctx.fill();
// 眼睛
ctx.fillStyle = '#e74c3c';
ctx.beginPath(); ctx.arc(this.x - 30, this.y - this.h + 30, 12, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(this.x + 30, this.y - this.h + 30, 12, 0, Math.PI*2); ctx.fill();
// 核心(金色)
ctx.fillStyle = '#f1c40f';
ctx.beginPath(); ctx.arc(this.x, this.y - 60, 18, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(this.x, this.y - 60, 22, 0, Math.PI*2); ctx.stroke();
// 背部方块
let bx = this.x - 60;
roundRect(bx, this.y - 80, 40, 15, 3); ctx.fillStyle='#3498db'; ctx.fill();
roundRect(this.x + 20, this.y - 80, 40, 15, 3); ctx.fill();
// HP条
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(this.x - 60, this.y - this.h - 20, 120, 8);
ctx.fillStyle = '#e74c3c'; ctx.fillRect(this.x - 60, this.y - this.h - 20, 120 * (bossHP/bossMaxHP), 8);
ctx.restore();
},
update() {
if (!inBoss) return;
if (bossPhase === 2) {
this.x += this.moveDir * 1.5;
if (this.x > W - 120 || this.x < 120) this.moveDir *= -1;
} else if (bossPhase === 3) {
this.x += this.moveDir * 3;
if (this.x > W - 120 || this.x < 120) this.moveDir *= -1;
this.shootTimer -= 0.016;
if (this.shootTimer <= 0) {
this.shootTimer = 2.0;
bullets.push({x: this.x, y: this.y - 60, vy: 5, w: 12, h: 12});
}
}
}
};
function startBoss() {
inBoss = true; bossPhase = 1; bossHP = bossMaxHP;
boss.x = W / 2; boss.y = H * 0.6;
blocks = []; bullets = [];
leopard.jumping = false; leopard.x = W/2; leopard.y = boss.y - 60 - leopard.h/2;
leopard.vx = 0; leopard.vy = 0; leopard.charging = false;
floatingTexts.push({x:W/2, y:H/2, text:'BOSS 出现!', color:'#e74c3c', life:90});
}
function bossDefeat() {
sfxBossDead();
inBoss = false; bossPhase = 1;
floatingTexts.push({x:W/2, y:H/2, text:'BOSS 击败!', color:'#f1c40f', life:90});
score += 500;
// 从Boss位置恢复无尽
let startY = H * 0.75;
let startX = W / 2;
blocks.push(makeBlock(startX, startY, 'normal'));
leopard.x = startX; leopard.y = startY - 30;
leopard.jumping = false; leopard.vy = 0; leopard.vx = 0;
leopard.onBlock = blocks[0];
generateAhead(blocks[0]);
updateUI();
}
function makeBlock(x, y, type) {
let w = 80 + Math.random() * 30;
return {
x, y, w, h: 20, type, crackTimer: 0,
moveOffset: 0, moveDir: Math.random()>0.5?1:-1,
moveSpeed: 0.8+Math.random()*0.5, moveRange: 40+Math.random()*30, startX: x,
draw() {
let drawX = this.x;
if (this.type === 'move') {
this.moveOffset += this.moveSpeed * this.moveDir;
if (Math.abs(this.moveOffset) > this.moveRange) this.moveDir *= -1;
drawX = this.startX + this.moveOffset; this.x = drawX;
}
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(drawX - this.w/2 + 3, this.y - this.h/2 + 3, this.w, this.h);
switch(this.type) {
case 'normal':
if (!this._color) { let h=Math.random()*360; this._color = `hsl(${h},65%,60%)`; }
ctx.fillStyle = this._color; roundRect(drawX-this.w/2, this.y-this.h/2, this.w, this.h, 4); ctx.fill(); break;
case 'spring':
ctx.fillStyle = '#f39c12'; roundRect(drawX-this.w/2, this.y-this.h/2, this.w, this.h, 4); ctx.fill();
ctx.fillStyle = '#f1c40f'; ctx.fillRect(drawX-10, this.y-this.h/2-8, 20, 8); ctx.fillRect(drawX-6, this.y-this.h/2-12, 12, 4); break;
case 'crack':
ctx.fillStyle = '#95a5a6'; roundRect(drawX-this.w/2, this.y-this.h/2, this.w, this.h, 4); ctx.fill();
ctx.strokeStyle = '#7f8c8d'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(drawX-20, this.y); ctx.lineTo(drawX-5, this.y-5); ctx.lineTo(drawX+10, this.y+3); ctx.stroke();
if (this.crackTimer > 0) { ctx.fillStyle='rgba(231,76,60,0.4)'; ctx.fillRect(drawX-this.w/2, this.y-this.h/2, this.w, this.h); } break;
case 'ice':
ctx.fillStyle = 'rgba(116,185,255,0.8)'; roundRect(drawX-this.w/2, this.y-this.h/2, this.w, this.h, 4); ctx.fill(); break;
case 'move':
ctx.fillStyle = '#3498db'; roundRect(drawX-this.w/2, this.y-this.h/2, this.w, this.h, 4); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fillRect(drawX-this.w/2, this.y+this.h/2-4, this.w, 4); break;
}
}
};
}
let blockCount = 0;
function generateAhead(fromBlock) {
let lastY = fromBlock ? fromBlock.y : H * 0.7;
let lastX = fromBlock ? fromBlock.x : W / 2;
while (blocks.length < 8) {
let gap = 100 + Math.random() * 60;
let newY = lastY - gap;
let newX = lastX + (Math.random() - 0.5) * 240;
newX = Math.max(80, Math.min(W - 80, newX));
let r = Math.random(), type = 'normal';
if (blockCount > 5) {
if (r < 0.1) type = 'spring'; else if (r < 0.2) type = 'crack';
else if (r < 0.3) type = 'move'; else if (r < 0.35) type = 'ice';
}
if (blocks.length > 0 && blocks[blocks.length-1].type === 'spring') type = 'normal';
blocks.push(makeBlock(newX, newY, type));
lastY = newY; lastX = newX; blockCount++;
}
}
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({ x, y, vx: (Math.random()-0.5)*8, vy: (Math.random()-0.5)*8, life: 30+Math.random()*20, color, size: 2+Math.random()*4 });
}
}
function updateUI() {
document.getElementById('score').innerText = score;
document.getElementById('combo').innerText = 'x' + combo.toFixed(1);
document.getElementById('jumps').innerText = totalJumps % 50;
}
function roundRect(x, y, w, h, r) {
ctx.beginPath(); ctx.moveTo(x+r,y); ctx.lineTo(x+w-r,y); ctx.quadraticCurveTo(x+w,y,x+w,y+r);
ctx.lineTo(x+w,y+h-r); ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
ctx.lineTo(x+r,y+h); ctx.quadraticCurveTo(x,y+h,x,y+h-r);
ctx.lineTo(x,y+r); ctx.quadraticCurveTo(x,y,x+r,y); ctx.closePath();
}
let chargeStarted = false;
function onChargeStart(e) { e.preventDefault(); if (!chargeStarted) { leopard.startCharge(); chargeStarted = true; } }
function onChargeEnd(e) { e.preventDefault(); if (chargeStarted) { leopard.jump(); chargeStarted = false; } }
document.addEventListener('keydown', e => { if (e.code === 'Space') onChargeStart(e); });
document.addEventListener('keyup', e => { if (e.code === 'Space') onChargeEnd(e); });
canvas.addEventListener('mousedown', onChargeStart);
canvas.addEventListener('mouseup', onChargeEnd);
canvas.addEventListener('touchstart', onChargeStart, {passive:false});
canvas.addEventListener('touchend', onChargeEnd, {passive:false});
function restart() {
score = 0; combo = 1; comboCount = 0; totalJumps = 0;
gameRunning = true; inBoss = false; bossPhase = 1; bossHP = 5;
blocks = []; particles = []; floatingTexts = []; bullets = []; blockCount = 0;
leopard.frozen = 0; leopard.onBlock = null; leopard.charging = false; leopard.jumping = false;
document.getElementById('gameover').style.display = 'none'; updateUI(); init();
}
function init() {
let startY = H * 0.75, startX = W / 2;
blocks.push(makeBlock(startX, startY, 'normal'));
leopard.x = startX; leopard.y = startY - 30; leopard.onBlock = blocks[0];
generateAhead(blocks[0]);
}
function gameLoop() {
ctx.clearRect(0, 0, W, H);
let grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0f3460'); grad.addColorStop(1, '#16213e');
ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
for (let i = 0; i < 30; i++) ctx.fillRect((i*137.5)%W, (i*97.3)%H, 2, 2);
if (inBoss) {
boss.update(); boss.draw();
// 弹幕
for (let i = bullets.length-1; i >= 0; i--) {
let b = bullets[i]; b.y += b.vy;
ctx.fillStyle = '#e74c3c'; ctx.fillRect(b.x-b.w/2, b.y, b.w, b.h);
if (b.y > H) bullets.splice(i, 1);
if (Math.abs(b.x - leopard.x) < 20 && Math.abs(b.y - leopard.y) < 20) { leopard.die(); }
}
} else {
for (let b of blocks) b.draw();
}
leopard.update(); leopard.draw();
for (let i = particles.length-1; i >= 0; i--) {
let p = particles[i]; p.x += p.vx; p.y += p.vy; p.life--;
ctx.fillStyle = p.color; ctx.globalAlpha = p.life/50; ctx.fillRect(p.x, p.y, p.size, p.size); ctx.globalAlpha = 1;
if (p.life <= 0) particles.splice(i, 1);
}
for (let i = floatingTexts.length-1; i >= 0; i--) {
let ft = floatingTexts[i]; ft.y -= 1; ft.life--;
ctx.fillStyle = ft.color; ctx.globalAlpha = ft.life/60; ctx.font = 'bold 18px monospace'; ctx.textAlign = 'center';
ctx.fillText(ft.text, ft.x, ft.y); ctx.globalAlpha = 1;
if (ft.life <= 0) floatingTexts.splice(i, 1);
}
if (leopard.charging) {
let barW = 200, barH = 12, bx = W/2-barW/2, by = H-40;
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(bx, by, barW, barH);
let fillW = (leopard.chargeTime/1.2)*barW;
let barColor = fillW > barW*0.8 ? '#e74c3c' : fillW > barW*0.5 ? '#f1c40f' : '#2ecc71';
ctx.fillStyle = barColor; ctx.fillRect(bx, by, fillW, barH);
}
requestAnimationFrame(gameLoop);
}
init(); gameLoop();
</script>
</body>
</html>Game Source: 猎豹跳跳
Creator: ElectricPenguin69
Libraries: none
Complexity: complex (433 lines, 18.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-electricpenguin69" to link back to the original. Then publish at arcadelab.ai/publish.