Chrome 断网才能玩的小恐龙,被做成了单文件 HTML:不用断网、不用装任何东西,保存成 .html 双击就能玩。零外部依赖,一个文件不到 30KB。

玩法

  • 跳跃:空格 / ↑ / 鼠标点击
  • 下蹲:↓(空中按下会快速落地,躲低空鸟用)
  • 手机:点屏幕上半跳、下半蹲
  • 最高分:存 localStorage,关了浏览器也在

跑 700 分昼夜会切换一次,撞到障碍物结束,死了点一下重来。

实现

  • Canvas + requestAnimationFrame 游戏循环
  • 恐龙、仙人掌、飞鸟全是字符串矩阵定义的像素精灵,fillRect 逐像素画
  • 音效用 Web Audio API 现场合成(振荡器 + 噪声),没有音频文件
  • 速度随分数递增,封顶 13

完整代码

保存为 dino.html,双击运行:

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chrome Dino Game</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#f7f7f7;display:flex;justify-content:center;align-items:center;height:100vh;overflow:hidden;font-family:'Courier New',monospace}
canvas{image-rendering:pixelated;image-rendering:crisp-edges;max-width:100vw;max-height:100vh}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(function(){
"use strict";
 
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
 
// --- Constants ---
const W=800,H=200;
const GROUND_Y=160;
const GRAVITY=0.6;
const JUMP_FORCE=-10;
const INITIAL_SPEED=6;
const MAX_SPEED=13;
const SPEED_INCREMENT=0.001;
const PIXEL=2;
 
canvas.width=W;
canvas.height=H;
 
// --- Pixel Sprite System ---
function drawSprite(sprite,x,y,color,scale){
  scale=scale||PIXEL;
  ctx.fillStyle=color;
  for(let row=0;row<sprite.length;row++){
    for(let col=0;col<sprite[row].length;col++){
      if(sprite[row][col]==='1'){
        ctx.fillRect(x+col*scale,y+row*scale,scale,scale);
      }
    }
  }
}
 
// --- Sprite Data ---
// T-Rex standing/running
const DINO_RUN1=[
"0000000011111100",
"0000000111111110",
"0000000110111111",
"0000000111111110",
"0000000111111100",
"0000000111110000",
"0000000111111000",
"1000001111111000",
"1000011111111000",
"1100111111110000",
"1111111111100000",
"0111111111100000",
"0011111111000000",
"0001111110000000",
"0000111100000000",
"0000110110000000",
"0000110011000000",
"0000100001000000",
];
 
const DINO_RUN2=[
"0000000011111100",
"0000000111111110",
"0000000110111111",
"0000000111111110",
"0000000111111100",
"0000000111110000",
"0000000111111000",
"1000001111111000",
"1000011111111000",
"1100111111110000",
"1111111111100000",
"0111111111100000",
"0011111111000000",
"0001111110000000",
"0000111100000000",
"0000011011000000",
"0000110001000000",
"0000010000000000",
];
 
const DINO_JUMP=[
"0000000011111100",
"0000000111111110",
"0000000110111111",
"0000000111111110",
"0000000111111100",
"0000000111110000",
"0000000111111000",
"1000001111111000",
"1000011111111000",
"1100111111110000",
"1111111111100000",
"0111111111100000",
"0011111111000000",
"0001111110000000",
"0000110110000000",
"0000110011000000",
"0000000000000000",
"0000000000000000",
];
 
const DINO_DUCK1=[
"0000000000000000000001111100",
"0000000000000000000011111110",
"0000000000000000000110111111",
"0000000000000000000111111110",
"0000000000000000000111111100",
"0000000000000000000111110000",
"1111110000111111111111100000",
"1111111001111111111110000000",
"1111111111111111111100000000",
"0111111111111111110000000000",
"0001111111111110000000000000",
"0000011111001100000000000000",
"0000011000001100000000000000",
"0000010000000000000000000000",
];
 
const DINO_DUCK2=[
"0000000000000000000001111100",
"0000000000000000000011111110",
"0000000000000000000110111111",
"0000000000000000000111111110",
"0000000000000000000111111100",
"0000000000000000000111110000",
"1111110000111111111111100000",
"1111111001111111111110000000",
"1111111111111111111100000000",
"0111111111111111110000000000",
"0001111111111110000000000000",
"0000001100011100000000000000",
"0000011000011000000000000000",
"0000000000001000000000000000",
];
 
const DINO_DEAD=[
"0000000011111100",
"0000000111111110",
"0000000110111111",
"0000000111111110",
"0000000111111100",
"0000000111110000",
"0000000111111000",
"1000001111111000",
"1000011111111000",
"1100111111110000",
"1111111111100000",
"0111111111100000",
"0011111111000000",
"0001111110000000",
"0000110110000000",
"0000110011000000",
"0000100001000000",
"0000000000000000",
];
 
// Eye patch for dead dino (X eyes)
const DINO_EYE_DEAD=[
"101",
"010",
"101",
];
 
// Cactus small
const CACTUS_SMALL=[
"000010000",
"000010000",
"000010000",
"100010010",
"100010010",
"100010010",
"100010010",
"100110010",
"100110010",
"101110010",
"101110010",
"111110110",
"111110110",
"011110110",
"001110100",
"000110000",
"000110000",
"000110000",
"000110000",
"000110000",
];
 
// Cactus large
const CACTUS_LARGE=[
"0000010000000",
"0000010000000",
"0000010000000",
"0000010000000",
"1000010000010",
"1000010000010",
"1000010000010",
"1000010000010",
"1000010000010",
"1000010001010",
"1000010001010",
"1000010101010",
"1000010101010",
"1010010101010",
"1010010101010",
"1010010101010",
"1011010101110",
"1011010101110",
"1111011101110",
"1111011101110",
"0111011101100",
"0111011101100",
"0011011101000",
"0001011001000",
"0001001000000",
"0001001000000",
"0001001000000",
"0001001000000",
"0001001000000",
"0001001000000",
];
 
// Bird (Pterodactyl) frame 1 - wings up
const BIRD1=[
"000000000000000000100000",
"000000000000000001100000",
"000000000000000011100000",
"000000000000000111000000",
"000000000000001110000000",
"000000000000011100000000",
"000000000000111000000000",
"000000000001110000000000",
"111111111111100000000000",
"111111111111111111111110",
"011111111111111111111100",
"000111111111111111111000",
"000001111111111111100000",
"000000011111111100000000",
"000000000111110000000000",
"000000000001000000000000",
];
 
// Bird frame 2 - wings level
const BIRD2=[
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"111111111111100000000000",
"111111111111111111111110",
"011111111111111111111100",
"000111111111111111111000",
"000001111111111111100000",
"000000011111111100000000",
"000000000111110000000000",
"000000000001000000000000",
];
 
// Bird frame 3 - wings down
const BIRD3=[
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"000000000000000000000000",
"111111111111100000000000",
"111111111111111111111110",
"011111111111111111111100",
"000111111111111111111000",
"000001111111111111100000",
"000000011111111100000000",
"000000000111110000000000",
"000000000111000000000000",
"000000001110000000000000",
"000000011100000000000000",
"000000111000000000000000",
"000001110000000000000000",
];
 
// Cloud
const CLOUD=[
"00000111000000000",
"00001111100000000",
"00011111111001100",
"00111111111111110",
"01111111111111111",
"11111111111111111",
"00111111111111110",
];
 
// Game Over text pixels (simplified block letters)
const GAMEOVER_G=[
"1111",
"1000",
"1011",
"1001",
"1111",
];
const GAMEOVER_A=[
"0110",
"1001",
"1111",
"1001",
"1001",
];
const GAMEOVER_M=[
"10001",
"11011",
"10101",
"10001",
"10001",
];
const GAMEOVER_E=[
"1111",
"1000",
"1110",
"1000",
"1111",
];
const GAMEOVER_O=[
"0110",
"1001",
"1001",
"1001",
"0110",
];
const GAMEOVER_V=[
"10001",
"10001",
"01010",
"01010",
"00100",
];
const GAMEOVER_R=[
"1110",
"1001",
"1110",
"1010",
"1001",
];
 
// Restart icon
const RESTART=[
"001111100",
"011000110",
"110000011",
"100000001",
"100000000",
"100000000",
"110000011",
"011000110",
"001111100",
];
 
// --- Audio Engine (Web Audio API) ---
let audioCtx=null;
function getAudioCtx(){
  if(!audioCtx) audioCtx=new(window.AudioContext||window.webkitAudioContext)();
  return audioCtx;
}
 
function playJumpSound(){
  try{
    const ac=getAudioCtx();
    const osc=ac.createOscillator();
    const gain=ac.createGain();
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.type='square';
    osc.frequency.setValueAtTime(600,ac.currentTime);
    osc.frequency.exponentialRampToValueAtTime(900,ac.currentTime+0.05);
    osc.frequency.exponentialRampToValueAtTime(500,ac.currentTime+0.1);
    gain.gain.setValueAtTime(0.15,ac.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01,ac.currentTime+0.15);
    osc.start(ac.currentTime);
    osc.stop(ac.currentTime+0.15);
  }catch(e){}
}
 
function playScoreSound(){
  try{
    const ac=getAudioCtx();
    const osc=ac.createOscillator();
    const gain=ac.createGain();
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.type='square';
    osc.frequency.setValueAtTime(500,ac.currentTime);
    osc.frequency.exponentialRampToValueAtTime(700,ac.currentTime+0.05);
    gain.gain.setValueAtTime(0.1,ac.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01,ac.currentTime+0.1);
    osc.start(ac.currentTime);
    osc.stop(ac.currentTime+0.1);
    // second tone
    const osc2=ac.createOscillator();
    const gain2=ac.createGain();
    osc2.connect(gain2);
    gain2.connect(ac.destination);
    osc2.type='square';
    osc2.frequency.setValueAtTime(700,ac.currentTime+0.08);
    osc2.frequency.exponentialRampToValueAtTime(900,ac.currentTime+0.13);
    gain2.gain.setValueAtTime(0.1,ac.currentTime+0.08);
    gain2.gain.exponentialRampToValueAtTime(0.01,ac.currentTime+0.2);
    osc2.start(ac.currentTime+0.08);
    osc2.stop(ac.currentTime+0.2);
  }catch(e){}
}
 
function playHitSound(){
  try{
    const ac=getAudioCtx();
    // noise burst
    const bufferSize=ac.sampleRate*0.15;
    const buffer=ac.createBuffer(1,bufferSize,ac.sampleRate);
    const data=buffer.getChannelData(0);
    for(let i=0;i<bufferSize;i++){
      data[i]=(Math.random()*2-1)*Math.pow(1-i/bufferSize,2);
    }
    const src=ac.createBufferSource();
    src.buffer=buffer;
    const gain=ac.createGain();
    gain.gain.setValueAtTime(0.3,ac.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01,ac.currentTime+0.15);
    src.connect(gain);
    gain.connect(ac.destination);
    src.start(ac.currentTime);
    // low thud
    const osc=ac.createOscillator();
    const g2=ac.createGain();
    osc.connect(g2);
    g2.connect(ac.destination);
    osc.type='sine';
    osc.frequency.setValueAtTime(150,ac.currentTime);
    osc.frequency.exponentialRampToValueAtTime(50,ac.currentTime+0.2);
    g2.gain.setValueAtTime(0.3,ac.currentTime);
    g2.gain.exponentialRampToValueAtTime(0.01,ac.currentTime+0.2);
    osc.start(ac.currentTime);
    osc.stop(ac.currentTime+0.2);
  }catch(e){}
}
 
// --- Game State ---
let gameState='idle'; // idle, running, dead
let speed=INITIAL_SPEED;
let score=0;
let highScore=parseInt(localStorage.getItem('dinoHighScore'))||0;
let frameCount=0;
let nightMode=false;
let nightAlpha=0; // 0=day, 1=full night
let nightTarget=0;
let flashTimer=0;
 
// --- Dino ---
const dino={
  x:50,
  y:GROUND_Y,
  w:16*PIXEL,
  h:18*PIXEL,
  vy:0,
  jumping:false,
  ducking:false,
  animFrame:0,
  animTimer:0,
  reset(){
    this.y=GROUND_Y;
    this.vy=0;
    this.jumping=false;
    this.ducking=false;
    this.animFrame=0;
    this.animTimer=0;
  },
  jump(){
    if(!this.jumping){
      this.vy=JUMP_FORCE;
      this.jumping=true;
      this.ducking=false;
      playJumpSound();
    }
  },
  duck(active){
    if(this.jumping){
      if(active) this.vy=Math.max(this.vy,4); // fast fall
      return;
    }
    this.ducking=active;
  },
  update(){
    if(this.jumping){
      this.vy+=GRAVITY;
      this.y+=this.vy;
      if(this.y>=GROUND_Y){
        this.y=GROUND_Y;
        this.vy=0;
        this.jumping=false;
      }
    }
    this.animTimer++;
    if(this.animTimer>6){
      this.animTimer=0;
      this.animFrame=1-this.animFrame;
    }
    // update hitbox
    if(this.ducking){
      this.w=26*PIXEL;
      this.h=14*PIXEL;
    }else{
      this.w=16*PIXEL;
      this.h=18*PIXEL;
    }
  },
  getHitbox(){
    if(this.ducking){
      return{x:this.x+4,y:this.y-14*PIXEL+4,w:this.w-8,h:14*PIXEL-6};
    }
    return{x:this.x+4,y:this.y-18*PIXEL+4,w:this.w-8,h:18*PIXEL-6};
  },
  draw(color){
    const fg=color||getFgColor();
    if(gameState==='dead'){
      drawSprite(DINO_DEAD,this.x,this.y-18*PIXEL,fg);
      // draw X eyes
      ctx.fillStyle=nightMode&&nightAlpha>0.5?'#333':'#fff';
      const eyeX=this.x+8*PIXEL;
      const eyeY=this.y-18*PIXEL+3*PIXEL;
      ctx.fillRect(eyeX,eyeY,PIXEL,PIXEL);
      ctx.fillRect(eyeX+2*PIXEL,eyeY,PIXEL,PIXEL);
      ctx.fillRect(eyeX+PIXEL,eyeY+PIXEL,PIXEL,PIXEL);
      ctx.fillRect(eyeX,eyeY+2*PIXEL,PIXEL,PIXEL);
      ctx.fillRect(eyeX+2*PIXEL,eyeY+2*PIXEL,PIXEL,PIXEL);
      return;
    }
    if(this.ducking){
      const sprite=this.animFrame===0?DINO_DUCK1:DINO_DUCK2;
      drawSprite(sprite,this.x,this.y-14*PIXEL,fg);
    }else if(this.jumping){
      drawSprite(DINO_JUMP,this.x,this.y-18*PIXEL,fg);
    }else{
      const sprite=this.animFrame===0?DINO_RUN1:DINO_RUN2;
      drawSprite(sprite,this.x,this.y-18*PIXEL,fg);
    }
  }
};
 
// --- Obstacles ---
let obstacles=[];
let obstacleTimer=0;
const MIN_OBSTACLE_GAP=50;
 
function spawnObstacle(){
  const r=Math.random();
  let obs;
  if(score>300&&r<0.25){
    // bird
    const heights=[GROUND_Y-15*PIXEL,GROUND_Y-30*PIXEL,GROUND_Y-50*PIXEL];
    const birdY=heights[Math.floor(Math.random()*heights.length)];
    obs={type:'bird',x:W+10,y:birdY,w:24*PIXEL,h:16*PIXEL,animFrame:0,animTimer:0};
  }else if(r<0.6){
    obs={type:'cactus_small',x:W+10,y:GROUND_Y-20*PIXEL+2,w:9*PIXEL,h:20*PIXEL-2};
  }else{
    obs={type:'cactus_large',x:W+10,y:GROUND_Y-30*PIXEL+2,w:13*PIXEL,h:30*PIXEL-2};
  }
  obstacles.push(obs);
}
 
function updateObstacles(){
  obstacleTimer++;
  const minGap=Math.max(MIN_OBSTACLE_GAP,Math.floor(100-speed*4));
  if(obstacleTimer>minGap+Math.random()*40){
    spawnObstacle();
    obstacleTimer=0;
  }
  for(let i=obstacles.length-1;i>=0;i--){
    obstacles[i].x-=speed;
    if(obstacles[i].type==='bird'){
      obstacles[i].animTimer++;
      if(obstacles[i].animTimer>10){
        obstacles[i].animTimer=0;
        obstacles[i].animFrame=(obstacles[i].animFrame+1)%3;
      }
    }
    if(obstacles[i].x<-100){
      obstacles.splice(i,1);
    }
  }
}
 
function drawObstacles(){
  const fg=getFgColor();
  for(const obs of obstacles){
    if(obs.type==='cactus_small'){
      drawSprite(CACTUS_SMALL,obs.x,obs.y,fg);
    }else if(obs.type==='cactus_large'){
      drawSprite(CACTUS_LARGE,obs.x,obs.y,fg);
    }else if(obs.type==='bird'){
      const sprites=[BIRD1,BIRD2,BIRD3];
      drawSprite(sprites[obs.animFrame],obs.x,obs.y,fg);
    }
  }
}
 
function getObstacleHitbox(obs){
  if(obs.type==='bird'){
    return{x:obs.x+4,y:obs.y+4,w:obs.w-8,h:obs.h-8};
  }
  return{x:obs.x+2,y:obs.y+2,w:obs.w-4,h:obs.h-2};
}
 
// --- Ground ---
let groundOffset=0;
const groundBumps=[];
for(let i=0;i<200;i++){
  groundBumps.push({x:Math.random()*W*2,h:Math.random()*3+1,w:Math.random()*4+1});
}
 
function drawGround(){
  const fg=getFgColor();
  ctx.fillStyle=fg;
  // main ground line
  ctx.fillRect(0,GROUND_Y+2,W,1);
  // ground texture
  for(const bump of groundBumps){
    const bx=((bump.x-groundOffset)%(W*2)+W*2)%(W*2)-W*0.5;
    if(bx>-10&&bx<W+10){
      ctx.fillRect(bx,GROUND_Y+4+Math.random()*2,bump.w,1);
    }
  }
  groundOffset+=speed;
}
 
// --- Clouds ---
let clouds=[];
let cloudTimer=0;
 
function spawnCloud(){
  clouds.push({x:W+10,y:20+Math.random()*60,speed:0.5+Math.random()*0.5});
}
 
function updateClouds(){
  cloudTimer++;
  if(cloudTimer>100+Math.random()*100){
    spawnCloud();
    cloudTimer=0;
  }
  for(let i=clouds.length-1;i>=0;i--){
    clouds[i].x-=clouds[i].speed;
    if(clouds[i].x<-40){
      clouds.splice(i,1);
    }
  }
}
 
function drawClouds(){
  const col=nightMode&&nightAlpha>0.5?'rgba(200,200,200,0.3)':'rgba(200,200,200,0.8)';
  for(const c of clouds){
    drawSprite(CLOUD,c.x,c.y,col,2);
  }
}
 
// --- Stars (night) ---
const stars=[];
for(let i=0;i<20;i++){
  stars.push({x:Math.random()*W,y:Math.random()*100,size:Math.random()>0.5?2:1,blink:Math.random()*100});
}
 
function drawStars(){
  if(nightAlpha<0.3)return;
  const alpha=Math.min(1,(nightAlpha-0.3)/0.7);
  ctx.fillStyle=`rgba(255,255,255,${alpha*0.8})`;
  for(const s of stars){
    const blink=Math.sin((frameCount+s.blink)*0.05)>0;
    if(blink||s.size===2){
      ctx.fillRect(s.x,s.y,s.size,s.size);
    }
  }
}
 
// --- Moon ---
function drawMoon(){
  if(nightAlpha<0.3)return;
  const alpha=Math.min(1,(nightAlpha-0.3)/0.7);
  ctx.fillStyle=`rgba(255,255,240,${alpha})`;
  // simple circle
  const mx=W-80,my=30,mr=15;
  ctx.beginPath();
  ctx.arc(mx,my,mr,0,Math.PI*2);
  ctx.fill();
  // crater
  ctx.fillStyle=`rgba(200,200,180,${alpha*0.5})`;
  ctx.beginPath();
  ctx.arc(mx-4,my-3,3,0,Math.PI*2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(mx+5,my+4,2,0,Math.PI*2);
  ctx.fill();
}
 
// --- Collision Detection ---
function checkCollision(){
  const dHB=dino.getHitbox();
  for(const obs of obstacles){
    const oHB=getObstacleHitbox(obs);
    if(dHB.x<oHB.x+oHB.w&&dHB.x+dHB.w>oHB.x&&dHB.y<oHB.y+oHB.h&&dHB.y+dHB.h>oHB.y){
      return true;
    }
  }
  return false;
}
 
// --- Score ---
function drawScore(){
  const fg=getFgColor();
  ctx.fillStyle=fg;
  ctx.font='bold 13px "Courier New",monospace';
  ctx.textAlign='right';
  const scoreStr=String(Math.floor(score)).padStart(5,'0');
  if(highScore>0){
    const hiStr='HI '+String(highScore).padStart(5,'0')+'  '+scoreStr;
    ctx.fillText(hiStr,W-15,22);
  }else{
    ctx.fillText(scoreStr,W-15,22);
  }
}
 
// --- Flash effect on score milestone ---
function drawFlash(){
  if(flashTimer>0){
    ctx.fillStyle=`rgba(255,255,255,${flashTimer/20})`;
    ctx.fillRect(0,0,W,H);
    flashTimer--;
  }
}
 
// --- Night/Day ---
function getBgColor(){
  if(nightAlpha<=0)return'#f7f7f7';
  const r=Math.round(247*(1-nightAlpha)+50*nightAlpha);
  const g=Math.round(247*(1-nightAlpha)+50*nightAlpha);
  const b=Math.round(247*(1-nightAlpha)+70*nightAlpha);
  return`rgb(${r},${g},${b})`;
}
 
function getFgColor(){
  if(nightAlpha<=0)return'#535353';
  const r=Math.round(83*(1-nightAlpha)+240*nightAlpha);
  const g=Math.round(83*(1-nightAlpha)+240*nightAlpha);
  const b=Math.round(83*(1-nightAlpha)+240*nightAlpha);
  return`rgb(${r},${g},${b})`;
}
 
function updateNightMode(){
  // switch every 700 points
  const phase=Math.floor(score/700);
  nightTarget=phase%2===1?1:0;
  if(nightAlpha<nightTarget)nightAlpha=Math.min(nightTarget,nightAlpha+0.005);
  if(nightAlpha>nightTarget)nightAlpha=Math.max(nightTarget,nightAlpha-0.005);
  nightMode=nightAlpha>0.5;
}
 
// --- Game Over / Restart ---
function drawGameOver(){
  const fg=getFgColor();
  // "GAME OVER" text using pixel letters
  const letters=[GAMEOVER_G,GAMEOVER_A,GAMEOVER_M,GAMEOVER_E,null,GAMEOVER_O,GAMEOVER_V,GAMEOVER_E,GAMEOVER_R];
  let startX=W/2-80;
  const startY=60;
  ctx.fillStyle=fg;
  for(const letter of letters){
    if(letter===null){startX+=8;continue;}
    for(let r=0;r<letter.length;r++){
      for(let c=0;c<letter[r].length;c++){
        if(letter[r][c]==='1'){
          ctx.fillRect(startX+c*3,startY+r*3,3,3);
        }
      }
    }
    startX+=(letter[0].length+1)*3;
  }
  // restart button
  const rx=W/2-10,ry=100;
  ctx.fillStyle=fg;
  // circular arrow icon
  ctx.beginPath();
  ctx.arc(rx+9,ry+9,8,0,Math.PI*2);
  ctx.lineWidth=2;
  ctx.strokeStyle=fg;
  ctx.stroke();
  // arrow head
  ctx.beginPath();
  ctx.moveTo(rx+14,ry+3);
  ctx.lineTo(rx+18,ry+5);
  ctx.lineTo(rx+14,ry+7);
  ctx.fillStyle=fg;
  ctx.fill();
  // clear inside
  ctx.fillStyle=getBgColor();
  ctx.beginPath();
  ctx.arc(rx+9,ry+9,6,0,Math.PI*2);
  ctx.fill();
  // inner arrow
  ctx.fillStyle=fg;
  ctx.beginPath();
  ctx.moveTo(rx+5,ry+4);
  ctx.lineTo(rx+13,ry+4);
  ctx.lineTo(rx+13,ry+7);
  ctx.lineTo(rx+5,ry+7);
  ctx.fill();
}
 
// --- Start Screen ---
function drawStartScreen(){
  const fg=getFgColor();
  ctx.fillStyle=fg;
  ctx.font='bold 14px "Courier New",monospace';
  ctx.textAlign='center';
  ctx.fillText('Press SPACE or Tap to Start',W/2,H/2-10);
  // draw static dino
  drawSprite(DINO_RUN1,dino.x,dino.y-18*PIXEL,fg);
}
 
// --- Input ---
const keys={};
document.addEventListener('keydown',function(e){
  if(e.code==='Space'||e.code==='ArrowUp'){
    e.preventDefault();
    keys.jump=true;
    handleInput();
  }
  if(e.code==='ArrowDown'){
    e.preventDefault();
    keys.duck=true;
    if(gameState==='running')dino.duck(true);
  }
});
document.addEventListener('keyup',function(e){
  if(e.code==='Space'||e.code==='ArrowUp'){
    keys.jump=false;
  }
  if(e.code==='ArrowDown'){
    keys.duck=false;
    if(gameState==='running')dino.duck(false);
  }
});
 
// Touch support
canvas.addEventListener('touchstart',function(e){
  e.preventDefault();
  const touch=e.touches[0];
  const rect=canvas.getBoundingClientRect();
  const y=touch.clientY-rect.top;
  const relY=y/rect.height;
  if(relY>0.6){
    // bottom area = duck
    if(gameState==='running')dino.duck(true);
  }else{
    handleInput();
  }
});
canvas.addEventListener('touchend',function(e){
  dino.duck(false);
});
 
// Mouse click
canvas.addEventListener('click',function(e){
  handleInput();
});
 
function handleInput(){
  if(gameState==='idle'){
    startGame();
  }else if(gameState==='running'){
    dino.jump();
  }else if(gameState==='dead'){
    resetGame();
  }
}
 
// --- Game Loop ---
function startGame(){
  gameState='running';
  score=0;
  speed=INITIAL_SPEED;
  obstacles=[];
  obstacleTimer=0;
  frameCount=0;
  nightAlpha=0;
  nightTarget=0;
  flashTimer=0;
  dino.reset();
}
 
function resetGame(){
  startGame();
}
 
function die(){
  gameState='dead';
  playHitSound();
  const finalScore=Math.floor(score);
  if(finalScore>highScore){
    highScore=finalScore;
    localStorage.setItem('dinoHighScore',String(highScore));
  }
}
 
let lastTime=0;
function gameLoop(timestamp){
  const dt=timestamp-lastTime;
  lastTime=timestamp;
 
  // Clear
  ctx.fillStyle=getBgColor();
  ctx.fillRect(0,0,W,H);
 
  frameCount++;
 
  if(gameState==='idle'){
    drawGround();
    drawClouds();
    drawStartScreen();
    requestAnimationFrame(gameLoop);
    return;
  }
 
  if(gameState==='running'){
    // Update
    speed=Math.min(MAX_SPEED,INITIAL_SPEED+score*0.002);
    score+=speed*0.02;
 
    // Score milestone sound
    if(Math.floor(score)%100===0&&Math.floor(score)>0&&Math.floor(score)!==Math.floor(score-speed*0.02)){
      playScoreSound();
      flashTimer=15;
    }
 
    updateNightMode();
    dino.update();
    updateObstacles();
    updateClouds();
 
    if(checkCollision()){
      die();
    }
  }
 
  // Draw
  drawStars();
  drawMoon();
  drawClouds();
  drawGround();
  drawObstacles();
  dino.draw();
  drawScore();
  drawFlash();
 
  if(gameState==='dead'){
    drawGameOver();
  }
 
  requestAnimationFrame(gameLoop);
}
 
// --- Scale canvas for display ---
function resizeCanvas(){
  const maxW=window.innerWidth*0.95;
  const maxH=window.innerHeight*0.9;
  const scale=Math.min(maxW/W,maxH/H);
  canvas.style.width=Math.floor(W*scale)+'px';
  canvas.style.height=Math.floor(H*scale)+'px';
}
window.addEventListener('resize',resizeCanvas);
resizeCanvas();
 
// --- Start ---
requestAnimationFrame(gameLoop);
 
})();
</script>
</body>
</html>