<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Dino Runner</title>
<style>
body{margin:0;background:#f7f7f7;display:flex;align-items:center;justify-content:center;height:100vh;font-family:"Courier New",monospace}
canvas{background:#f7f7f7;image-rendering:pixelated}
</style>
</head>
<body>
<canvas id="game" width="600" height="200"></canvas>
<script>
const cv=document.getElementById('game'),ctx=cv.getContext('2d');
const W=600,H=200,GROUND=170;
const GRAVITY=0.6,JUMP_V=10.25;
let state='idle',speed=6,score=0,hiScore=+localStorage.getItem('dinoHi')||0;
let obstacles=[],clouds=[],groundTicks=[],frame=0,night=false,nightAlpha=0;
const dino={x:40,y:GROUND,w:44,h:47,vy:0,ducking:false,dead:false};
// ---- 输入 ----
const keys={};
addEventListener('keydown',e=>{
if(e.code==='Space'||e.code==='ArrowUp'||e.code==='ArrowDown')e.preventDefault();
if(state==='idle'&&(e.code==='Space'||e.code==='ArrowUp'))start();
if(state==='over'&&e.code==='Space')start();
keys[e.code]=true;
});
addEventListener('keyup',e=>keys[e.code]=false);
cv.addEventListener('pointerdown',()=>{ if(state!=='run')start(); else jump(); });
function start(){
state='run';speed=6;score=0;frame=0;night=false;nightAlpha=0;
obstacles=[];clouds=[];groundTicks=[];
for(let i=0;i<20;i++)groundTicks.push({x:Math.random()*W,y:GROUND+8+Math.random()*10,w:6+Math.random()*12});
dino.y=GROUND;dino.vy=0;dino.ducking=false;dino.dead=false;
dino.h=47;dino.w=44;
}
function jump(){
if(dino.y>=GROUND&&!dino.dead){dino.vy=-JUMP_V;dino.ducking=false;dino.h=47;dino.w=44;}
}
function beep(){
try{const a=new AudioContext(),o=a.createOscillator(),g=a.createGain();
o.connect(g);g.connect(a.destination);o.frequency.value=880;
g.gain.setValueAtTime(.15,a.currentTime);g.gain.exponentialRampToValueAtTime(.001,a.currentTime+.15);
o.start();o.stop(a.currentTime+.15);}catch(e){}
}
// ---- 障碍生成 ----
let nextSpawn=60;
function spawn(){
const r=Math.random();
if(score>150&&r<0.25){ // 翼龙
const heights=[GROUND-62,GROUND-32,GROUND-4];
obstacles.push({type:'bird',x:W+30,y:heights[Math.floor(Math.random()*heights.length)],w:42,h:30,flap:0});
}else{
const big=Math.random()<0.5,n=1+Math.floor(Math.random()*3);
const cw=big?25:17,ch=big?50:35;
obstacles.push({type:'cactus',x:W+30,y:GROUND-ch,w:cw*n,h:ch,cw,n,big});
}
nextSpawn=40+Math.random()*60+(300/speed);
}
// ---- 更新 ----
function update(){
frame++;
speed=Math.min(13,6+score/1500*3);
score+=speed*0.05;
if(Math.floor(score)%100===0&&Math.floor(score)!==Math.floor(score-speed*0.05))beep();
// 昼夜切换(每700分)
const target=(Math.floor(score/700)%2===1)?1:0;
nightAlpha+=(target-nightAlpha)*0.02;
// 恐龙
if(keys.Space||keys.ArrowUp)jump();
dino.ducking=keys.ArrowDown&&dino.y>=GROUND;
dino.h=dino.ducking?26:47; dino.w=dino.ducking?58:44;
dino.vy+=keys.ArrowDown&&dino.y<GROUND?GRAVITY*2.5:GRAVITY;
dino.y+=dino.vy;
if(dino.y>GROUND){dino.y=GROUND;dino.vy=0;}
// 地面
groundTicks.forEach(g=>{g.x-=speed;if(g.x<-20){g.x=W;g.y=GROUND+8+Math.random()*10;}});
// 云
if(frame%150===0)clouds.push({x:W,y:20+Math.random()*60});
clouds.forEach(c=>c.x-=speed*0.3);
clouds=clouds.filter(c=>c.x>-60);
// 障碍
nextSpawn-=speed;
if(nextSpawn<=0)spawn();
obstacles.forEach(o=>{o.x-=speed;if(o.type==='bird')o.flap++;});
obstacles=obstacles.filter(o=>o.x>-100);
// 碰撞(缩小判定框)
const pad=6;
const dx=dino.x+pad,dy=dino.y,dw=dino.w-pad*2,dh=dino.h-pad;
for(const o of obstacles){
let ow=o.w;
if(o.type==='bird')ow=36;
if(dx<o.x+ow&&dx+dw>o.x&&dy<o.y+o.h&&dy+dh>o.y){
state='over';dino.dead=true;
hiScore=Math.max(hiScore,Math.floor(score));
localStorage.setItem('dinoHi',hiScore);
}
}
}
// ---- 绘制 ----
function drawDino(){
ctx.save();ctx.translate(dino.x,dino.y);
const legFrame=Math.floor(frame/6)%2;
if(dino.ducking){
ctx.fillRect(0,8,58,18); // 身体(趴下)
ctx.fillRect(44,2,16,12); // 头
ctx.fillStyle='#fff';ctx.fillRect(52,4,3,3);ctx.fillStyle='#535353';
ctx.fillRect(60,8,8,4); // 嘴
if(state==='run')ctx.fillRect(legFrame?8:20,26,6,6);else ctx.fillRect(8,26,6,6),ctx.fillRect(20,26,6,6);
}else{
ctx.fillRect(8,0,26,30); // 身体
ctx.fillRect(20,-18,22,18); // 头
ctx.fillStyle='#fff';ctx.fillRect(32,-12,3,3);ctx.fillStyle='#535353';
ctx.fillRect(40,-12,8,5); // 嘴
ctx.fillRect(0,10,10,14); // 尾巴
ctx.fillRect(14,30,6,4);ctx.fillRect(24,30,6,4);
if(state==='run'&&dino.y>=GROUND)
ctx.fillRect(legFrame?14:24,34,6,13);
else{ctx.fillRect(14,34,6,13);ctx.fillRect(24,34,6,13);}
}
if(dino.dead){ // X眼
ctx.fillStyle='#f7f7f7';ctx.fillRect(30,-14,7,7);
ctx.fillStyle='#535353';
ctx.fillRect(30,-14,2,2);ctx.fillRect(35,-14,2,2);ctx.fillRect(32,-12,2,2);
ctx.fillRect(30,-9,2,2);ctx.fillRect(35,-9,2,2);
}
ctx.restore();
}
function drawCactus(o){
for(let i=0;i<o.n;i++){
const x=o.x+i*o.cw;
ctx.fillRect(x+o.cw/2-3,o.y,6,o.h); // 主干
ctx.fillRect(x,o.y+o.h*0.3,4,o.h*0.3);ctx.fillRect(x,o.y+o.h*0.3,o.cw*0.4,4);
ctx.fillRect(x+o.cw-4,o.y+o.h*0.15,4,o.h*0.3);ctx.fillRect(x+o.cw*0.6,o.y+o.h*0.15,o.cw*0.4,4);
}
}
function drawBird(o){
const up=Math.floor(o.flap/12)%2===0;
ctx.save();ctx.translate(o.x,o.y);
ctx.fillRect(6,12,30,8); // 身体
ctx.fillRect(36,8,10,6);ctx.fillRect(44,10,6,3); // 头嘴
if(up)ctx.fillRect(14,0,6,12);else ctx.fillRect(14,20,6,12); // 翅膀
ctx.restore();
}
function draw(){
// 昼夜
ctx.fillStyle=`rgb(${247-nightAlpha*197},${247-nightAlpha*197},${247-nightAlpha*167})`;
ctx.fillRect(0,0,W,H);
ctx.fillStyle=`rgb(${83+nightAlpha*140},${83+nightAlpha*140},${83+nightAlpha*140})`;
// 云
clouds.forEach(c=>{ctx.fillRect(c.x,c.y,40,10);ctx.fillRect(c.x+8,c.y-6,24,8);});
// 地面线
ctx.fillRect(0,GROUND+47,W,2);
groundTicks.forEach(g=>ctx.fillRect(g.x,g.y,g.w,2));
// 障碍
obstacles.forEach(o=>o.type==='cactus'?drawCactus(o):drawBird(o));
drawDino();
// 分数
const s=String(Math.floor(score)).padStart(5,'0');
ctx.font='bold 16px "Courier New"';ctx.textAlign='right';
ctx.fillText('HI '+String(hiScore).padStart(5,'0')+' '+s,W-15,28);
if(state==='idle'){ctx.textAlign='center';ctx.fillText('按空格开始',W/2,90);}
if(state==='over'){ctx.textAlign='center';ctx.font='bold 22px "Courier New"';
ctx.fillText('G A M E O V E R',W/2,80);
ctx.font='14px "Courier New"';ctx.fillText('按空格重新开始',W/2,105);}
ctx.textAlign='left';
}
(function loop(){
if(state==='run')update();
draw();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
index.html
index.html