吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 538|回复: 22
上一主题 下一主题
收起左侧

[学习记录] 纯qwen3.8写的坦克大战游戏

  [复制链接]
跳转到指定楼层
楼主
fssq121 发表于 2026-7-23 09:57 回帖奖励
使用qwen3.8-max-preview写的坦克大战游戏,纯单文件html,适配手机端和电脑端。
复制存为html文件,浏览器打开即可游玩。
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>坦克大战</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
  html,body { width:100%; height:100%; overflow:hidden; background:#0a0a0a;
    font-family:'Courier New',Courier,monospace; user-select:none; -webkit-user-select:none; touch-action:none; }
  #app { width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; }
  #canvas-wrap { position:relative; line-height:0; flex:none; }
  canvas { display:block; image-rendering:pixelated; image-rendering:crisp-edges; background:#000; border:2px solid #333; }

  #overlay { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:rgba(0,0,0,.72); line-height:1.6; }
  #overlay.hidden { display:none; }
  .screen { display:none; flex-direction:column; align-items:center; text-align:center; color:#ddd;
    padding:12px; width:100%; max-height:100%; overflow:auto; }
  .screen.active { display:flex; }
  .title { font-size:clamp(28px,7vmin,52px); color:#e8c422; text-shadow:3px 3px 0 #7a3010;
    letter-spacing:8px; font-weight:bold; }
  .subtitle { color:#888; font-size:clamp(11px,2.5vmin,15px); letter-spacing:4px; margin:4px 0 14px; }
  .instructions { font-size:clamp(11px,2.4vmin,14px); color:#aaa; margin-bottom:16px; }
  .instructions p { margin:3px 0; }
  .btn { font-family:inherit; font-size:clamp(15px,3vmin,19px); font-weight:bold; color:#fff;
    background:#b02020; border:3px solid #e8c422; border-radius:8px; padding:10px 34px;
    cursor:pointer; letter-spacing:3px; box-shadow:0 4px 0 #601010; }
  .btn:active { transform:translateY(3px); box-shadow:0 1px 0 #601010; }
  .best { margin-top:14px; color:#ffd040; font-size:13px; }
  .title2 { font-size:clamp(24px,6vmin,40px); font-weight:bold; color:#fff; margin-bottom:12px; letter-spacing:4px; }
  .title2.red { color:#e84040; }

  #controls { display:none; width:100%; max-width:560px; padding:10px 18px 14px;
    align-items:center; justify-content:space-between; flex:none; }
  #controls.show { display:flex; }
  #dpad { position:relative; width:156px; height:156px; flex:none; }
  .dbtn { position:absolute; width:52px; height:52px; background:rgba(255,255,255,.10);
    border:2px solid rgba(255,255,255,.28); border-radius:12px; color:#ccc; font-size:18px;
    display:flex; align-items:center; justify-content:center; }
  .dbtn.on { background:rgba(232,196,34,.35); }
  #d-up{left:52px;top:0} #d-down{left:52px;top:104px} #d-left{left:0;top:52px} #d-right{left:104px;top:52px}
  #firebtn { width:86px; height:86px; border-radius:50%; background:rgba(200,40,40,.55);
    border:3px solid rgba(255,120,120,.6); color:#fff; font-size:17px; font-weight:bold;
    display:flex; align-items:center; justify-content:center; letter-spacing:2px; flex:none; }
  #firebtn.on { background:rgba(255,80,80,.8); }
  [url=home.php?mod=space&uid=945662]@media[/url] (orientation:landscape) {
    #controls.show { position:fixed; left:0; right:0; bottom:0; max-width:none;
      justify-content:space-between; padding:0 16px 12px; pointer-events:none; }
    #dpad,#firebtn { pointer-events:auto; }
  }

  #floatbtns { position:fixed; top:6px; right:6px; z-index:20; display:flex; gap:6px; }
  #floatbtns button { width:38px; height:38px; border-radius:50%; border:2px solid #555;
    background:rgba(30,30,30,.8); color:#fff; font-size:16px; cursor:pointer; }
</style>
</head>
<body>
<div id="app">
  <div id="canvas-wrap">
    <canvas id="game"></canvas>
    <div id="overlay">
      <div class="screen" id="menuScreen">
        <div class="title">坦 克 大 战</div>
        <div class="subtitle">BATTLE CITY</div>
        <div class="instructions">
          <p>&#127918; 电脑:方向键 / WASD 移动,空格键射击</p>
          <p>&#128241; 手机:左下方向键移动,右下按钮射击</p>
          <p>&#127919; 消灭全部敌方坦克,守护底部老鹰基地!</p>
          <p>&#128138; 打红色闪烁坦克掉落道具  |  P 键暂停</p>
        </div>
        <button class="btn" id="startBtn">开 始 游 戏</button>
        <div class="best">最高分:<span id="bestScore">0</span></div>
      </div>
      <div class="screen" id="pauseScreen">
        <div class="title2">已 暂 停</div>
        <button class="btn" id="resumeBtn">继 续</button>
      </div>
      <div class="screen" id="overScreen">
        <div class="title2 red">游 戏 结 束</div>
        <p style="margin-bottom:8px">得分:<span id="finalScore" style="color:#ffd040">0</span></p>
        <p id="newRecord" style="color:#7af07a;margin-bottom:8px">&#127942; 新纪录!</p>
        <button class="btn" id="restartBtn">再 来 一 局</button>
      </div>
    </div>
  </div>

  <div id="controls">
    <div id="dpad">
      <div class="dbtn" id="d-up" data-dir="up">▲</div>
      <div class="dbtn" id="d-left" data-dir="left">&#9664;</div>
      <div class="dbtn" id="d-right" data-dir="right">&#9654;</div>
      <div class="dbtn" id="d-down" data-dir="down">▼</div>
    </div>
    <div id="firebtn">射击</div>
  </div>
</div>

<div id="floatbtns">
  <button id="pauseBtn">&#9208;</button>
  <button id="muteBtn">&#128266;</button>
</div>

<script>
'use strict';
/* ================= 基础常量 ================= */
const $ = id => document.getElementById(id);
const canvas = $('game'), ctx = canvas.getContext('2d');
const COLS = 13, ROWS = 13, CELL = 48;
const FIELD = COLS * CELL;
const PANEL_W = 88;
const CVW = FIELD + PANEL_W, CVH = FIELD;
const TS = 42;
const DX = [0,1,0,-1], DY = [-1,0,1,0];
const T_EMPTY=0, T_BRICK=1, T_STEEL=2, T_WATER=3, T_TREE=4, T_ICE=5;
const QUADS = [[0,0,1],[1,0,2],[0,1,4],[1,1,8]];
const MAX_ENEMIES = 4, TOTAL_ENEMIES = 20, SPAWN_TIME = 1.0, PU_SIZE = 36;
const SPAWN_CELLS = [[0,0],[6,0],[12,0]];
const FORTRESS = [[11,5],[11,6],[11,7],[12,5],[12,7]];
const ALIGN_SPEED = 320;   // 侧向收敛速度(像素/秒)

/* ================= 关卡地图 ================= */
const MAPS = [
  [".............",".BB..B.B..BB.",".BB..B.B..BB.",".............","..B.BB.BB.B..",
   "..B.BB.BB.B..",".............",".BB..S.S..BB.",".BB..S.S..BB.",".....B.B.....",
   ".BB..B.B..BB.",".....BBB.....",".....BEB....."],
  [".............",".T..BB.BB..T.",".T..BB.BB..T.","..W.......W..","..W..BBB..W..",
   ".....B.B.....",".BB..B.B..BB.",".BB.......BB.",".....SSS.....",".T...B.B...T.",
   ".T...B.B...T.",".....BBB.....",".....BEB....."],
  [".............",".S.BB.B.BB.S.",".S.BB.B.BB.S.",".............","..BB.S.B.SB..",
   "..BB.....BB..","..S..BBB..S..",".....B.B.....",".BB..B.B..BB.",".BB..S.S..BB.",
   ".............",".....BBB.....",".....BEB....."],
  [".............",".B...B.B...B.",".B.II...II.B.","....I...I....",".BB.I.B.I.BB.",
   "....I...I....",".B.II...II.B.",".B...B.B...B.",".............","..BBB...BBB..",
   ".............",".....BBB.....",".....BEB....."],
  [".............",".BB.S...S.BB.",".BB.S...S.BB.",".....T.T.....",".BB..T.T..BB.",
   ".....B.B.....",".SS..B.B..SS.",".....B.B.....",".BB..T.T..BB.",".....T.T.....",
   ".BB.S...S.BB.",".....BBB.....",".....BEB....."]
];

/* ================= 坦克数据 ================= */
const STATS = {
  basic:{speed:55, hp:1, score:100, bulletSpeed:170, fireRate:0.8, power:1},
  fast: {speed:105,hp:1, score:200, bulletSpeed:170, fireRate:0.9, power:1},
  power:{speed:70, hp:1, score:300, bulletSpeed:290, fireRate:1.5, power:2},
  armor:{speed:50, hp:4, score:400, bulletSpeed:170, fireRate:1.0, power:1}
};
const PALS = {
  player:{body:'#e0c422',dark:'#8a7814',light:'#fff27a',track:'#6b6b52',trackDark:'#3f3f2e'},
  basic: {body:'#c9c9c9',dark:'#7a7a7a',light:'#f2f2f2',track:'#5a5a5a',trackDark:'#333333'},
  fast:  {body:'#5ab0e8',dark:'#2a6a9a',light:'#a8dcff',track:'#4a5a6a',trackDark:'#2a343e'},
  power: {body:'#5ac85a',dark:'#2a7a2a',light:'#a8f0a8',track:'#4a6a4a',trackDark:'#2a3e2a'},
  armor: {body:'#9aa878',dark:'#5a6a3a',light:'#d8e8b0',track:'#5a5a4a',trackDark:'#34342a'},
  bonus: {body:'#e85050',dark:'#8a2020',light:'#ffa0a0',track:'#6a4a4a',trackDark:'#3e2a2a'}
};
const ARMOR_COLORS = {4:'#9aa878',3:'#d8c858',2:'#d89848',1:'#d86048'};

/* ================= 游戏状态 ================= */
let grid=[], player=null, enemies=[], bullets=[], explosions=[], powerups=[];
let queue=[], spawnTimer=0, spawnIdx=0, spawnCount=0;
let state='menu', level=1, score=0, lives=3;
let shieldTimer=0, freezeTimer=0, shovelTimer=0;
let bannerText='', bannerT=0, bannerColor='#ffdc40';
let gameOverT=0, clearT=0, eagleAlive=true, animTime=0, muted=false;
let best = 0;
try { best = +localStorage.getItem('tankBest') || 0; } catch(e){}

/* ================= 音效 ================= */
let actx = null;
function initAudio(){
  if (!actx) { try { actx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){} }
  if (actx && actx.state === 'suspended') actx.resume();
}
function tone(freq, dur, type, vol, slideTo, delay){
  if (!actx || muted) return;
  type = type||'square'; vol = vol||0.15; delay = delay||0;
  const t0 = actx.currentTime + delay;
  const o = actx.createOscillator(), g = actx.createGain();
  o.type = type; o.frequency.setValueAtTime(freq, t0);
  if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t0+dur);
  g.gain.setValueAtTime(vol, t0);
  g.gain.exponentialRampToValueAtTime(0.001, t0+dur);
  o.connect(g); g.connect(actx.destination);
  o.start(t0); o.stop(t0+dur+0.03);
}
function noise(dur, vol){
  if (!actx || muted) return;
  const len = (actx.sampleRate*dur)|0;
  const buf = actx.createBuffer(1, len, actx.sampleRate);
  const d = buf.getChannelData(0);
  for (let i=0;i<len;i++) d[i] = (Math.random()*2-1)*(1-i/len);
  const src = actx.createBufferSource(); src.buffer = buf;
  const g = actx.createGain(); g.gain.value = vol||0.3;
  src.connect(g); g.connect(actx.destination); src.start();
}
const SFX = {
  shoot(){ tone(900,.07,'square',.08,300); },
  hit(){ tone(300,.06,'square',.12,150); noise(.04,.08); },
  boom(){ noise(.35,.35); tone(150,.3,'sawtooth',.15,50); },
  bigboom(){ noise(.6,.45); tone(90,.55,'sawtooth',.2,35); },
  power(){ [523,659,784,1046].forEach((f,i)=>tone(f,.09,'square',.12,null,i*.08)); },
  spawn(){ tone(250,.2,'square',.07,500); },
  start(){ [392,523,659,784].forEach((f,i)=>tone(f,.12,'square',.12,null,i*.13)); },
  gameover(){ tone(330,.3,'sawtooth',.16,165); tone(165,.5,'sawtooth',.16,82,.32); }
};

/* ================= 工具函数 ================= */
function rectsOverlap(x1,y1,w1,h1,x2,y2,w2,h2){
  return x1 < x2+w2 && x1+w1 > x2 && y1 < y2+h2 && y1+h1 > y2;
}
function snapAxis(v){
  return Math.round((v - (CELL-TS)/2)/CELL)*CELL + (CELL-TS)/2;
}
function addExplosion(x,y,big){ explosions.push({x,y,big,t:0,dur:big?0.5:0.28}); }
function setBanner(text,color,dur){ bannerText=text; bannerColor=color||'#ffdc40'; bannerT=dur||1.6; }

/* ================= 关卡加载 ================= */
function loadMap(idx){
  const m = MAPS[idx % MAPS.length];
  grid = [];
  for (let r=0;r<ROWS;r++){
    const row = [];
    const line = (m[r]||'').padEnd(COLS,'.');
    for (let c=0;c<COLS;c++){
      const ch = line[c];
      if (ch==='B') row.push({t:T_BRICK,q:15});
      else if (ch==='S') row.push({t:T_STEEL,q:0});
      else if (ch==='W') row.push({t:T_WATER,q:0});
      else if (ch==='T') row.push({t:T_TREE,q:0});
      else if (ch==='I') row.push({t:T_ICE,q:0});
      else row.push({t:T_EMPTY,q:0});
    }
    grid.push(row);
  }
  eagleAlive = true;
}
function setFortress(type){
  for (const [r,c] of FORTRESS) grid[r][c] = {t:type, q:type===T_BRICK?15:0};
}
function buildQueue(lv){
  const w = {
    basic: Math.max(8, 55-(lv-1)*9),
    fast: 22+(lv-1)*2,
    power: 14+(lv-1)*3,
    armor: 6+(lv-1)*4
  };
  const total = w.basic+w.fast+w.power+w.armor;
  const q = [];
  for (let i=0;i<TOTAL_ENEMIES;i++){
    const r = Math.random()*total; let type;
    if (r < w.basic) type='basic';
    else if (r < w.basic+w.fast) type='fast';
    else if (r < w.basic+w.fast+w.power) type='power';
    else type='armor';
    q.push(type);
  }
  return q;
}
function makePlayer(){
  return { x:4*CELL+(CELL-TS)/2, y:12*CELL+(CELL-TS)/2, dir:0, speed:95,
    alive:true, spawning:false, spawnT:0, level:1, cool:0, anim:0, respawnT:0, flash:0 };
}
function startLevel(){
  loadMap(level-1);
  enemies=[]; bullets=[]; explosions=[]; powerups=[];
  queue = buildQueue(level);
  spawnTimer = 0.5; spawnIdx = 0; spawnCount = 0;
  player = makePlayer();
  shieldTimer = 3; freezeTimer = 0; shovelTimer = 0; gameOverT = 0;
  setBanner('第 '+level+' 关', '#ffdc40', 2);
  state = 'playing';
}
function startGame(){
  initAudio();
  score = 0; lives = 3; level = 1;
  startLevel();
  showScreen(null);
  SFX.start();
}

/* ================= 碰撞检测 ================= */
function cellBlocksTank(nx,ny,r,c){
  const cell = grid[r][c];
  if (cell.t === T_STEEL || cell.t === T_WATER) return true;
  if (cell.t === T_BRICK && cell.q){
    const h = CELL/2, qx = c*CELL, qy = r*CELL;
    for (const [dx,dy,bit] of QUADS){
      if (cell.q & bit){
        if (rectsOverlap(nx,ny,TS,TS, qx+dx*h, qy+dy*h, h, h)) return true;
      }
    }
  }
  return false;
}
function canMove(tank,nx,ny){
  if (nx<0 || ny<0 || nx+TS>FIELD || ny+TS>FIELD) return false;
  const c0 = Math.floor(nx/CELL), c1 = Math.floor((nx+TS-0.001)/CELL);
  const r0 = Math.floor(ny/CELL), r1 = Math.floor((ny+TS-0.001)/CELL);
  for (let r=r0;r<=r1;r++) for (let c=c0;c<=c1;c++)
    if (cellBlocksTank(nx,ny,r,c)) return false;
  const ex = 6*CELL, ey = 12*CELL;
  if (rectsOverlap(nx,ny,TS,TS,ex,ey,CELL,CELL)) return false;
  if (player && player.alive && !player.spawning && tank !== player)
    if (rectsOverlap(nx,ny,TS,TS,player.x,player.y,TS,TS)) return false;
  for (const e of enemies){
    if (e===tank || e.dead || e.spawning) continue;
    if (rectsOverlap(nx,ny,TS,TS,e.x,e.y,TS,TS)) return false;
  }
  return true;
}

/* ====== 侧向收敛:每帧朝最近网格中心逐像素靠拢,只减偏移、绝不引入偏移 ====== */
function alignPerp(tank, dt){
  const horiz = (tank.dir === 1 || tank.dir === 3);   // 水平运动 → 收敛 y
  const cur = horiz ? tank.y : tank.x;
  const t  = snapAxis(cur);
  const d  = t - cur;
  if (Math.abs(d) < 0.01) return;
  const sgn = Math.sign(d);
  const maxStep = Math.min(Math.abs(d), ALIGN_SPEED * dt);
  let best = 0;
  for (let s = 1; s <= Math.ceil(maxStep); s++){
    const off = sgn * Math.min(s, maxStep);
    const nx = horiz ? tank.x : cur + off;
    const ny = horiz ? cur + off : tank.y;
    if (canMove(tank, nx, ny)) best = off; else break;   // 撞墙即停,绝不越过
  }
  if (best !== 0){ if (horiz) tank.y = cur + best; else tank.x = cur + best; }
}

/* ====== 移动:先收敛侧向,再纯轴向前进(前进绝不动侧向坐标) ====== */
function moveTank(tank, dt){
  alignPerp(tank, dt);
  const dist = tank.speed * dt;
  let nx = tank.x, ny = tank.y;
  if (tank.dir===0) ny -= dist;
  else if (tank.dir===1) nx += dist;
  else if (tank.dir===2) ny += dist;
  else nx -= dist;
  if (canMove(tank, nx, ny)){ tank.x = nx; tank.y = ny; return true; }
  return false;
}

/* ================= 玩家 ================= */
function updatePlayer(dt){
  if (!player.alive){
    player.respawnT -= dt;
    if (player.respawnT <= 0 && lives >= 0) respawnPlayer();
    return;
  }
  if (player.spawning){
    player.spawnT -= dt;
    if (player.spawnT <= 0) player.spawning = false;
    return;
  }
  player.cool -= dt;
  const inp = getInput();
  let dir = -1;
  if (inp.up) dir=0; else if (inp.right) dir=1; else if (inp.down) dir=2; else if (inp.left) dir=3;

  if (dir >= 0){
    player.dir = dir;                       // 转向即时生效,无停顿状态
    if (moveTank(player, dt)) player.anim += player.speed*dt;
  }

  if (inp.fire && player.cool <= 0){
    const maxB = player.level >= 2 ? 2 : 1;
    const cnt = bullets.filter(b=>b.isPlayer && !b.dead).length;
    if (cnt < maxB){ spawnBullet(player,true); player.cool = player.level>=2 ? 0.22 : 0.32; }
  }
}
function respawnPlayer(){
  const sx = 4*CELL+(CELL-TS)/2, sy = 12*CELL+(CELL-TS)/2;
  for (const e of enemies)
    if (!e.dead && rectsOverlap(sx,sy,TS,TS,e.x,e.y,TS,TS)){ player.respawnT = 0.5; return; }
  Object.assign(player,{x:sx,y:sy,dir:0,alive:true,spawning:true,spawnT:0.8,level:1,cool:0});
  shieldTimer = 3;
}
function hitPlayer(){
  addExplosion(player.x+TS/2, player.y+TS/2, true);
  SFX.boom();
  if (navigator.vibrate) navigator.vibrate([60,40,60]);
  lives--;
  player.alive = false;
  player.respawnT = 1.2;
  if (lives < 0) gameOverT = 1.6;
}

/* ================= 敌人 ================= */
function chooseDirection(e){
  const targets = [];
  if (player.alive) targets.push({x:player.x,y:player.y});
  targets.push({x:6*CELL,y:12*CELL});
  const t = targets[(Math.random()*targets.length)|0];
  const prefer = Math.abs(t.x-e.x) > Math.abs(t.y-e.y) ? (t.x>e.x?1:3) : (t.y>e.y?2:0);
  e.dir = (Math.random() < 0.45) ? prefer : (Math.random()*4)|0;
}
function updateSpawning(dt){
  if (queue.length === 0) return;
  if (enemies.filter(e=>!e.dead).length >= MAX_ENEMIES) return;
  spawnTimer -= dt;
  if (spawnTimer > 0) return;
  const [sc,sr] = SPAWN_CELLS[spawnIdx % 3];
  const sx = sc*CELL+(CELL-TS)/2, sy = sr*CELL+(CELL-TS)/2;
  let blocked = player.alive && rectsOverlap(sx,sy,TS,TS,player.x,player.y,TS,TS);
  for (const e of enemies) if (!e.dead && rectsOverlap(sx,sy,TS,TS,e.x,e.y,TS,TS)) blocked = true;
  if (blocked){ spawnTimer = 0.5; spawnIdx++; return; }
  const type = queue.shift(), st = STATS[type];
  enemies.push({
    x:sx, y:sy, dir:2, type,
    speed: st.speed*(1+Math.min(0.3,(level-1)*0.03)),
    hp:st.hp, score:st.score, bulletSpeed:st.bulletSpeed,
    power:st.power, fireRate:st.fireRate,
    bonus: spawnCount % 4 === 3,
    spawning:true, spawnT:SPAWN_TIME,
    aiT:Math.random()*1.5, cool:1+Math.random(),
    anim:0, flash:0, bullet:null, dead:false
  });
  spawnIdx++; spawnCount++;
  spawnTimer = Math.max(1.6, 3.2-level*0.15);
  SFX.spawn();
}
function updateEnemies(dt){
  for (const e of enemies){
    if (e.dead) continue;
    if (e.flash > 0) e.flash -= dt;
    if (e.spawning){ e.spawnT -= dt; if (e.spawnT<=0) e.spawning=false; continue; }
    e.cool -= dt;
    if (freezeTimer > 0) continue;
    e.aiT -= dt;
    if (e.aiT <= 0){ chooseDirection(e); e.aiT = 0.8+Math.random()*2; }

    if (moveTank(e, dt)) e.anim += e.speed*dt;
    else { chooseDirection(e); e.aiT = 0.6+Math.random()*1.6; }   // 撞墙才换向

    if (e.cool <= 0 && (!e.bullet || e.bullet.dead)){
      if (Math.random() < dt*e.fireRate*1.3){
        spawnBullet(e,false);
        e.cool = 0.5+Math.random()*0.8;
      }
    }
  }
  enemies = enemies.filter(e=>!e.dead);
}
function killEnemy(e){
  e.dead = true;
  addExplosion(e.x+TS/2, e.y+TS/2, true);
  SFX.boom();
  score += e.score;
  if (navigator.vibrate) navigator.vibrate(30);
  if (e.bonus) spawnPowerup();
}

/* ================= 子弹 ================= */
function spawnBullet(tank,isPlayer){
  bullets.push({
    x: tank.x+TS/2 + DX[tank.dir]*(TS/2+4),
    y: tank.y+TS/2 + DY[tank.dir]*(TS/2+4),
    dir: tank.dir,
    speed: isPlayer ? (player.level>=3?380:320) : tank.bulletSpeed,
    isPlayer,
    power: isPlayer ? (player.level>=3?2:1) : (tank.power||1),
    dead:false, owner:tank
  });
  if (!isPlayer) tank.bullet = bullets[bullets.length-1];
  SFX.shoot();
}
function bulletHitsBrick(b,r,c){
  const cell = grid[r][c];
  const h = CELL/2, qx = c*CELL, qy = r*CELL;
  let hit = false;
  for (const [dx,dy,bit] of QUADS){
    if ((cell.q & bit) && rectsOverlap(b.x-5,b.y-5,10,10, qx+dx*h,qy+dy*h,h,h)){
      cell.q &= ~bit; hit = true;
    }
  }
  return hit;
}
function destroyEagle(){
  eagleAlive = false;
  addExplosion(6*CELL+CELL/2, 12*CELL+CELL/2, true);
  SFX.bigboom();
  if (navigator.vibrate) navigator.vibrate([100,60,100,60,100]);
  gameOverT = 1.8;
}
function updateBullets(dt){
  for (const b of bullets){
    if (b.dead) continue;
    b.x += DX[b.dir]*b.speed*dt;
    b.y += DY[b.dir]*b.speed*dt;
    if (b.x<0 || b.y<0 || b.x>FIELD || b.y>FIELD){
      b.dead = true; addExplosion(b.x,b.y,false); SFX.hit(); continue;
    }
    const bx=b.x-5, by=b.y-5;
    let c0=Math.floor(bx/CELL), c1=Math.floor((bx+9.99)/CELL);
    let r0=Math.floor(by/CELL), r1=Math.floor((by+9.99)/CELL);
    if (c0<0) c0=0; if (c1>=COLS) c1=COLS-1;     // 命中盒越界时钳制到合法网格
    if (r0<0) r0=0; if (r1>=ROWS) r1=ROWS-1;
    let hitWall = false;
    for (let r=r0; r<=r1 && !hitWall; r++) for (let c=c0; c<=c1; c++){
      const cell = grid[r][c];
      if (cell.t===T_BRICK && cell.q){
        if (bulletHitsBrick(b,r,c)) hitWall = true;
      } else if (cell.t===T_STEEL){
        if (rectsOverlap(bx,by,10,10,c*CELL,r*CELL,CELL,CELL)){
          if (b.power>=2){ cell.t=T_EMPTY; addExplosion(c*CELL+CELL/2,r*CELL+CELL/2,false); }
          hitWall = true;
        }
      }
    }
    if (hitWall){ b.dead=true; addExplosion(b.x,b.y,false); SFX.hit(); continue; }
    if (eagleAlive && rectsOverlap(bx,by,10,10,6*CELL,12*CELL,CELL,CELL)){
      b.dead = true; destroyEagle(); continue;
    }
    if (b.isPlayer){
      for (const e of enemies){
        if (e.spawning || e.dead) continue;
        if (rectsOverlap(bx,by,10,10,e.x,e.y,TS,TS)){
          b.dead = true; e.hp--; e.flash = 0.15;
          if (e.hp<=0) killEnemy(e); else SFX.hit();
          break;
        }
      }
    } else if (player.alive && !player.spawning &&
               rectsOverlap(bx,by,10,10,player.x,player.y,TS,TS)){
      b.dead = true;
      if (shieldTimer <= 0) hitPlayer(); else SFX.hit();
    }
  }
  for (const a of bullets) for (const b of bullets){
    if (a.isPlayer !== b.isPlayer && !a.dead && !b.dead &&
        Math.abs(a.x-b.x)<10 && Math.abs(a.y-b.y)<10){ a.dead = b.dead = true; }
  }
  bullets = bullets.filter(b=>!b.dead);
}

/* ================= 道具 ================= */
function spawnPowerup(){
  const types = ['star','shield','bomb','timer','shovel','life'];
  const type = types[(Math.random()*types.length)|0];
  for (let tries=0; tries<200; tries++){
    const r = (Math.random()*ROWS)|0, c = (Math.random()*COLS)|0;
    if (grid[r][c].t !== T_EMPTY) continue;
    if (r===12 && c>=5 && c<=7) continue;
    if (r===0 && (c===0||c===6||c===12)) continue;
    powerups = [{type, x:c*CELL+6, y:r*CELL+6, t:16}];
    return;
  }
}
function applyPowerup(p){
  SFX.power();
  if (navigator.vibrate) navigator.vibrate(40);
  switch(p.type){
    case 'star':
      if (player.level < 3) player.level++;
      setBanner('火力升级!', '#ffdc40'); break;
    case 'shield': shieldTimer = 12; setBanner('无敌护盾!', '#7ac8ff'); break;
    case 'bomb':
      for (const e of enemies) if (!e.dead && !e.spawning) killEnemy(e);
      setBanner('全灭!', '#ff8080'); break;
    case 'timer': freezeTimer = 10; setBanner('敌人冻结!', '#7ac8ff'); break;
    case 'shovel':
      setFortress(T_STEEL); shovelTimer = 16; setBanner('基地加固!', '#d0d0d0'); break;
    case 'life': lives++; setBanner('生命 +1!', '#7af07a'); break;
  }
}
function updatePowerups(dt){
  for (const p of powerups){
    p.t -= dt;
    if (player.alive && !player.spawning &&
        rectsOverlap(player.x,player.y,TS,TS,p.x,p.y,PU_SIZE,PU_SIZE)){
      applyPowerup(p); p.t = 0;
    }
  }
  powerups = powerups.filter(p=>p.t>0);
}

/* ================= 主更新 ================= */
function updateEffects(dt){
  for (const e of explosions) e.t += dt;
  explosions = explosions.filter(e=>e.t<e.dur);
  if (bannerT > 0) bannerT -= dt;
}
function update(dt){
  animTime += dt;
  if (bannerT > 0) bannerT -= dt;
  if (gameOverT > 0){
    gameOverT -= dt;
    updateEffects(dt);
    if (gameOverT <= 0) gameOver();
    return;
  }
  if (shieldTimer > 0) shieldTimer -= dt;
  if (freezeTimer > 0) freezeTimer -= dt;
  if (shovelTimer > 0){ shovelTimer -= dt; if (shovelTimer<=0) setFortress(T_BRICK); }
  updatePlayer(dt);
  updateSpawning(dt);
  updateEnemies(dt);
  updateBullets(dt);
  updatePowerups(dt);
  updateEffects(dt);
  if (eagleAlive && queue.length===0 && enemies.length===0){
    state = 'clear'; clearT = 3; score += 500; SFX.power();
  }
}
function gameOver(){
  state = 'over';
  const isRecord = score > best;
  if (isRecord){ best = score; try{ localStorage.setItem('tankBest', best); }catch(e){} }
  $('finalScore').textContent = score;
  $('newRecord').style.display = isRecord ? 'block' : 'none';
  showScreen('overScreen');
  SFX.gameover();
}

/* ================= 绘制 ================= */
function drawTankShape(cx,cy,dir,size,pal,frame){
  ctx.save();
  ctx.translate(cx,cy);
  ctx.rotate(dir*Math.PI/2);
  const s=size, h=s/2, tw=Math.max(4,s*0.2);
  ctx.fillStyle = pal.track;
  ctx.fillRect(-h,-h,tw,s); ctx.fillRect(h-tw,-h,tw,s);
  ctx.fillStyle = pal.trackDark;
  const step = s/5;
  let off = frame % step; if (off<0) off += step;
  for (let y=-h+off-step; y<h; y+=step){
    ctx.fillRect(-h,y,tw,step*0.45); ctx.fillRect(h-tw,y,tw,step*0.45);
  }
  ctx.fillStyle = pal.dark;
  ctx.fillRect(-h+tw-1,-h*0.8,s-tw*2+2,s*0.85);
  ctx.fillStyle = pal.body;
  ctx.fillRect(-h+tw,-h*0.75,s-tw*2,s*0.78);
  ctx.fillStyle = pal.light;
  ctx.fillRect(-h+tw,-h*0.75,s-tw*2,s*0.1);
  ctx.fillStyle = pal.dark;
  ctx.fillRect(-s*0.07,-h-1,s*0.14,h*0.9);
  ctx.fillStyle = pal.body;
  ctx.fillRect(-s*0.045,-h,s*0.09,h*0.85);
  ctx.fillStyle = pal.body;
  ctx.beginPath(); ctx.arc(0,s*0.05,s*0.24,0,Math.PI*2); ctx.fill();
  ctx.strokeStyle = pal.dark; ctx.lineWidth = 1.5;
  ctx.beginPath(); ctx.arc(0,s*0.05,s*0.24,0,Math.PI*2); ctx.stroke();
  ctx.fillStyle = pal.light;
  ctx.beginPath(); ctx.arc(-s*0.06,0,s*0.09,0,Math.PI*2); ctx.fill();
  ctx.restore();
}
function drawSpawnAnim(cx,cy,t){
  const ph = Math.floor(t*10)%3;
  const r = TS/2*[0.5,0.8,1.05][ph];
  ctx.strokeStyle = ph===1 ? '#6af' : '#fff';
  ctx.lineWidth = 3;
  ctx.beginPath();
  for (let i=0;i<8;i++){
    const ang = i*Math.PI/4 + Math.PI/8;
    const rr = i%2===0 ? r : r*0.5;
    const x = cx+Math.cos(ang)*rr, y = cy+Math.sin(ang)*rr;
    i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
  }
  ctx.closePath(); ctx.stroke();
}
function drawTank(t,isPlayer){
  if (t.spawning){ drawSpawnAnim(t.x+TS/2,t.y+TS/2,t.spawnT); return; }
  let pal;
  if (isPlayer) pal = PALS.player;
  else {
    pal = (t.bonus && Math.floor(animTime*6)%2===0) ? PALS.bonus : PALS[t.type];
    if (t.type==='armor' && t.hp<4) pal = Object.assign({},pal,{body:ARMOR_COLORS[t.hp]});
  }
  drawTankShape(t.x+TS/2, t.y+TS/2, t.dir, TS, pal, t.anim/6);
  if (t.flash > 0){
    ctx.fillStyle = 'rgba(255,255,255,0.7)';
    ctx.fillRect(t.x,t.y,TS,TS);
  }
}
function drawBrickQuad(x,y){
  const h = CELL/2;
  ctx.fillStyle = '#5a2808'; ctx.fillRect(x,y,h,h);
  ctx.fillStyle = '#b85a18';
  const rh = 6;
  for (let i=0;i<4;i++){
    const by = y+i*rh;
    if (i%2===0){
      ctx.fillRect(x+0.5,by+0.5,11,rh-1);
      ctx.fillRect(x+12.5,by+0.5,11,rh-1);
    } else {
      ctx.fillRect(x+0.5,by+0.5,5,rh-1);
      ctx.fillRect(x+6.5,by+0.5,11,rh-1);
      ctx.fillRect(x+18.5,by+0.5,5,rh-1);
    }
  }
}
function drawSteel(x,y){
  const s=CELL, h=s/2;
  ctx.fillStyle = '#7a7a7a'; ctx.fillRect(x,y,s,s);
  for (const [dx,dy] of [[0,0],[1,0],[0,1],[1,1]]){
    const bx=x+dx*h, by=y+dy*h;
    ctx.fillStyle = '#e0e0e0';
    ctx.fillRect(bx+1,by+1,h-2,3); ctx.fillRect(bx+1,by+1,3,h-2);
    ctx.fillStyle = '#4a4a4a';
    ctx.fillRect(bx+1,by+h-4,h-2,3); ctx.fillRect(bx+h-4,by+1,3,h-2);
    ctx.fillStyle = '#a8a8a8';
    ctx.fillRect(bx+5,by+5,h-10,h-10);
  }
}
function drawWater(x,y){
  ctx.fillStyle = '#1a3a9a'; ctx.fillRect(x,y,CELL,CELL);
  ctx.fillStyle = '#3a6ae0';
  const frame = Math.floor(animTime*2)%2;
  for (let i=0;i<4;i++){
    const wy = y+4+i*12+(frame?3:0);
    for (let j=0;j<3;j++){
      const wx = x+2+j*15+((i+frame)%2?6:0);
      ctx.fillRect(wx,wy,8,3);
    }
  }
}
function drawIce(x,y){
  ctx.fillStyle = '#b8d8f0'; ctx.fillRect(x,y,CELL,CELL);
  ctx.fillStyle = '#e8f6ff';
  ctx.fillRect(x+4,y+6,16,4); ctx.fillRect(x+24,y+20,18,4); ctx.fillRect(x+8,y+34,14,4);
  ctx.strokeStyle = '#8ab8d8'; ctx.strokeRect(x+0.5,y+0.5,CELL-1,CELL-1);
}
function drawTree(x,y){
  const colors = ['#0e7a1e','#1a9a2e','#0a5a14'];
  for (let dy=0;dy<3;dy++) for (let dx=0;dx<3;dx++){
    ctx.fillStyle = colors[(dx+dy)%3];
    ctx.beginPath();
    ctx.arc(x+6+dx*18, y+6+dy*18, 8, 0, Math.PI*2);
    ctx.fill();
  }
}
function drawTiles(trees){
  const h = CELL/2;
  for (let r=0;r<ROWS;r++) for (let c=0;c<COLS;c++){
    const cell = grid[r][c], x=c*CELL, y=r*CELL;
    if (trees){ if (cell.t===T_TREE) drawTree(x,y); continue; }
    if (cell.t===T_BRICK){
      for (const [dx,dy,bit] of QUADS) if (cell.q & bit) drawBrickQuad(x+dx*h,y+dy*h);
    }
    else if (cell.t===T_STEEL) drawSteel(x,y);
    else if (cell.t===T_WATER) drawWater(x,y);
    else if (cell.t===T_ICE) drawIce(x,y);
  }
}
function drawEagle(){
  const x=6*CELL, y=12*CELL;
  if (!eagleAlive){
    ctx.fillStyle = '#4a4a4a';
    ctx.beginPath();
    ctx.moveTo(x+4,y+44); ctx.lineTo(x+14,y+26); ctx.lineTo(x+24,y+38);
    ctx.lineTo(x+32,y+22); ctx.lineTo(x+44,y+44); ctx.closePath(); ctx.fill();
    ctx.fillStyle = '#6a6a6a'; ctx.fillRect(x+6,y+40,36,6);
    return;
  }
  ctx.fillStyle = '#9a9a9a'; ctx.fillRect(x+6,y+38,36,8);
  ctx.fillStyle = '#c0c0c0'; ctx.fillRect(x+10,y+34,28,5);
  ctx.fillStyle = '#4a3018';
  ctx.beginPath(); ctx.moveTo(x+24,y+18); ctx.lineTo(x+3,y+6); ctx.lineTo(x+10,y+26); ctx.closePath(); ctx.fill();
  ctx.beginPath(); ctx.moveTo(x+24,y+18); ctx.lineTo(x+45,y+6); ctx.lineTo(x+38,y+26); ctx.closePath(); ctx.fill();
  ctx.fillStyle = '#6a4a22';
  ctx.beginPath(); ctx.ellipse(x+24,y+24,7,11,0,0,Math.PI*2); ctx.fill();
  ctx.fillStyle = '#e8e8e8';
  ctx.beginPath(); ctx.arc(x+24,y+12,4.5,0,Math.PI*2); ctx.fill();
  ctx.fillStyle = '#e8a020';
  ctx.beginPath(); ctx.moveTo(x+24,y+11); ctx.lineTo(x+30,y+13); ctx.lineTo(x+24,y+15); ctx.closePath(); ctx.fill();
  ctx.fillStyle = '#000'; ctx.fillRect(x+23,y+10,2,2);
}
function drawPowerupIcon(type,cx,cy,color){
  ctx.fillStyle = color; ctx.strokeStyle = color; ctx.lineWidth = 2;
  switch(type){
    case 'star':
      ctx.beginPath();
      for (let i=0;i<10;i++){
        const ang = -Math.PI/2+i*Math.PI/5, r = i%2===0?10:4.5;
        const x = cx+Math.cos(ang)*r, y = cy+Math.sin(ang)*r;
        i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
      }
      ctx.closePath(); ctx.fill(); break;
    case 'shield':
      ctx.beginPath();
      ctx.moveTo(cx-8,cy-9); ctx.lineTo(cx+8,cy-9); ctx.lineTo(cx+8,cy+1);
      ctx.lineTo(cx,cy+10); ctx.lineTo(cx-8,cy+1); ctx.closePath(); ctx.fill(); break;
    case 'bomb':
      ctx.beginPath(); ctx.arc(cx,cy+2,7,0,Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.moveTo(cx+3,cy-4); ctx.lineTo(cx+7,cy-9); ctx.stroke();
      ctx.fillStyle = '#ffd040';
      ctx.beginPath(); ctx.arc(cx+8,cy-10,2.5,0,Math.PI*2); ctx.fill(); break;
    case 'timer':
      ctx.beginPath(); ctx.arc(cx,cy+1,8,0,Math.PI*2); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(cx,cy+1); ctx.lineTo(cx,cy-4);
      ctx.moveTo(cx,cy+1); ctx.lineTo(cx+4,cy+1); ctx.stroke();
      ctx.fillRect(cx-3,cy-11,6,3); break;
    case 'shovel':
      ctx.fillRect(cx-1.5,cy-11,3,9);
      ctx.beginPath();
      ctx.moveTo(cx-6,cy-2); ctx.lineTo(cx+6,cy-2); ctx.lineTo(cx+5,cy+5);
      ctx.lineTo(cx,cy+10); ctx.lineTo(cx-5,cy+5); ctx.closePath(); ctx.fill(); break;
    case 'life':
      ctx.beginPath();
      ctx.moveTo(cx,cy+7);
      ctx.bezierCurveTo(cx-11,cy-1,cx-7,cy-10,cx,cy-4);
      ctx.bezierCurveTo(cx+7,cy-10,cx+11,cy-1,cx,cy+7);
      ctx.fill(); break;
  }
}
function drawPowerups(){
  for (const p of powerups){
    if (p.t<3 && Math.floor(animTime*8)%2===0) continue;
    const flash = Math.floor(animTime*5)%2===0;
    ctx.fillStyle = flash ? '#c8c8c8' : '#d03030';
    ctx.fillRect(p.x,p.y,PU_SIZE,PU_SIZE);
    ctx.strokeStyle = '#303030'; ctx.lineWidth = 2;
    ctx.strokeRect(p.x+1,p.y+1,PU_SIZE-2,PU_SIZE-2);
    drawPowerupIcon(p.type, p.x+PU_SIZE/2, p.y+PU_SIZE/2, flash?'#c02020':'#fff');
  }
}
function drawBullets(){
  for (const b of bullets){
    ctx.fillStyle = b.isPlayer ? '#ffe860' : '#f0f0f0';
    ctx.fillRect(b.x-3,b.y-3,6,6);
    ctx.fillStyle = b.isPlayer ? '#c89020' : '#a0a0a0';
    ctx.fillRect(b.x-1.5,b.y-1.5,3,3);
  }
}
function circle(x,y,r){ ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill(); }
function drawExplosions(){
  for (const e of explosions){
    const p = e.t/e.dur;
    const grow = p<0.4 ? p/0.4 : 1;
    const fade = p>0.7 ? 1-(p-0.7)/0.3 : 1;
    const R = (e.big?24:12)*grow;
    ctx.globalAlpha = fade;
    ctx.fillStyle = '#f05020'; circle(e.x,e.y,R);
    ctx.fillStyle = '#f8a030'; circle(e.x,e.y,R*0.7);
    ctx.fillStyle = '#fff0a0'; circle(e.x,e.y,R*0.4);
    ctx.globalAlpha = 1;
  }
}
function drawShield(){
  const cx = player.x+TS/2, cy = player.y+TS/2;
  const blink = shieldTimer<3 && Math.floor(animTime*10)%2===0;
  ctx.strokeStyle = blink ? '#fff' : '#6af';
  ctx.lineWidth = 2.5;
  ctx.beginPath(); ctx.arc(cx,cy,TS/2+4+Math.sin(animTime*10)*1.5,0,Math.PI*2); ctx.stroke();
  ctx.strokeStyle = 'rgba(120,180,255,0.5)';
  ctx.beginPath(); ctx.arc(cx,cy,TS/2+8,0,Math.PI*2); ctx.stroke();
}
function drawBanner(){
  let text=null, color='#ffdc40';
  if (bannerT > 0){ text = bannerText; color = bannerColor; }
  else if (state === 'clear'){ text = '关卡通过!+500'; color = '#7af07a'; }
  if (!text) return;
  ctx.fillStyle = 'rgba(0,0,0,0.85)';
  ctx.fillRect(0, FIELD/2-34, FIELD, 68);
  ctx.fillStyle = color;
  ctx.font = 'bold 30px "Courier New",monospace';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(text, FIELD/2, FIELD/2);
  ctx.textBaseline = 'alphabetic';
}
function drawMiniTank(x,y,color){
  ctx.fillStyle = color;
  ctx.fillRect(x,y,4,13); ctx.fillRect(x+10,y,4,13);
  ctx.fillRect(x+4,y+3,6,8); ctx.fillRect(x+6,y-2,2,6);
}
function drawFlag(x,y){
  ctx.fillStyle = '#d8d8d8'; ctx.fillRect(x,y,2,16);
  ctx.fillStyle = '#e03030';
  ctx.beginPath(); ctx.moveTo(x+2,y); ctx.lineTo(x+14,y+4); ctx.lineTo(x+2,y+8);
  ctx.closePath(); ctx.fill();
}
function drawPanel(){
  const px = FIELD;
  ctx.fillStyle = '#5f5f5f'; ctx.fillRect(px,0,PANEL_W,CVH);
  ctx.fillStyle = '#4a4a4a'; ctx.fillRect(px,0,2,CVH);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 13px "Courier New",monospace';
  ctx.fillText('SCORE', px+PANEL_W/2, 26);
  ctx.font = 'bold 15px "Courier New",monospace';
  ctx.fillText(String(score).padStart(6,'0'), px+PANEL_W/2, 46);
  ctx.fillStyle = '#ffd040';
  ctx.font = 'bold 11px "Courier New",monospace';
  ctx.fillText('BEST '+String(best).padStart(6,'0'), px+PANEL_W/2, 66);
  ctx.fillStyle = '#3f3f3f'; ctx.fillRect(px+10,78,PANEL_W-20,2);
  ctx.fillStyle = '#fff'; ctx.font = 'bold 12px sans-serif';
  ctx.fillText('敌 军', px+PANEL_W/2, 98);
  const rem = queue.length + enemies.filter(e=>!e.dead).length;
  for (let i=0;i<rem;i++) drawMiniTank(px+22+(i%2)*26, 108+((i/2)|0)*17, '#111');
  ctx.fillStyle = '#3f3f3f'; ctx.fillRect(px+10,288,PANEL_W-20,2);
  ctx.fillStyle = '#fff'; ctx.font = 'bold 12px sans-serif';
  ctx.fillText('玩 家', px+PANEL_W/2, 308);
  drawMiniTank(px+22,318,'#e0c422');
  ctx.textAlign = 'left';
  ctx.fillStyle = '#fff'; ctx.font = 'bold 14px "Courier New",monospace';
  ctx.fillText('×'+Math.max(0,lives), px+44, 331);
  drawFlag(px+24,346);
  ctx.fillText(String(level), px+44, 360);
  ctx.fillStyle = '#3f3f3f'; ctx.fillRect(px+10,376,PANEL_W-20,2);
  const legends = [['star','升级火力'],['shield','无敌护盾'],['bomb','全灭敌人'],
                   ['timer','冻结敌人'],['shovel','加固基地'],['life','生命+1']];
  ctx.font = '11px sans-serif';
  legends.forEach((lg,i)=>{
    const ly = 396+i*34;
    drawPowerupIcon(lg[0], px+24, ly+6, '#ffd040');
    ctx.fillStyle = '#e8e8e8'; ctx.textAlign = 'left';
    ctx.fillText(lg[1], px+42, ly+10);
  });
}
function render(){
  ctx.fillStyle = '#1a1a1a'; ctx.fillRect(0,0,CVW,CVH);
  ctx.fillStyle = '#000'; ctx.fillRect(0,0,FIELD,FIELD);
  drawTiles(false);
  drawEagle();
  drawPowerups();
  if (player && player.alive) drawTank(player,true);
  for (const e of enemies) drawTank(e,false);
  drawBullets();
  drawTiles(true);
  drawExplosions();
  if (shieldTimer>0 && player && player.alive) drawShield();
  drawBanner();
  drawPanel();
}

/* ================= 输入 ================= */
const keyInput = {up:false,down:false,left:false,right:false,fire:false};
const touchInput = {up:false,down:false,left:false,right:false,fire:false};
function getInput(){
  return {
    up: keyInput.up||touchInput.up, down: keyInput.down||touchInput.down,
    left: keyInput.left||touchInput.left, right: keyInput.right||touchInput.right,
    fire: keyInput.fire||touchInput.fire
  };
}
const KEYMAP = {ArrowUp:'up',KeyW:'up',ArrowDown:'down',KeyS:'down',
  ArrowLeft:'left',KeyA:'left',ArrowRight:'right',KeyD:'right',Space:'fire',KeyJ:'fire'};
window.addEventListener('keydown', e=>{
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) e.preventDefault();
  initAudio();
  if (e.code === 'Enter'){
    if (state==='menu' || state==='over') startGame();
    else if (state==='paused') togglePause();
    return;
  }
  if (e.code==='KeyP' || e.code==='Escape'){
    if (state==='playing' || state==='paused') togglePause();
    return;
  }
  const k = KEYMAP[e.code];
  if (k) keyInput[k] = true;
});
window.addEventListener('keyup', e=>{
  const k = KEYMAP[e.code];
  if (k) keyInput[k] = false;
});
const dpad = $('dpad');
function dpadHandle(touches){
  touchInput.up = touchInput.down = touchInput.left = touchInput.right = false;
  for (const t of touches){
    const el = document.elementFromPoint(t.clientX, t.clientY);
    if (el && el.dataset && el.dataset.dir){
      touchInput[el.dataset.dir] = true;
      el.classList.add('on');
    }
  }
  for (const el of document.querySelectorAll('.dbtn')){
    let on = false;
    for (const t of touches){
      const p = document.elementFromPoint(t.clientX,t.clientY);
      if (p === el) on = true;
    }
    el.classList.toggle('on', on);
  }
}
['touchstart','touchmove','touchend','touchcancel'].forEach(ev=>{
  dpad.addEventListener(ev, e=>{ e.preventDefault(); initAudio(); dpadHandle(e.touches); }, {passive:false});
});
const fireBtn = $('firebtn');
fireBtn.addEventListener('touchstart', e=>{ e.preventDefault(); initAudio(); touchInput.fire=true; fireBtn.classList.add('on'); }, {passive:false});
['touchend','touchcancel'].forEach(ev=>fireBtn.addEventListener(ev, e=>{ e.preventDefault(); touchInput.fire=false; fireBtn.classList.remove('on'); }, {passive:false}));

document.addEventListener('touchmove', e=>e.preventDefault(), {passive:false});
document.addEventListener('contextmenu', e=>e.preventDefault());
document.addEventListener('visibilitychange', ()=>{ if (document.hidden && state==='playing') togglePause(); });

/* ================= UI ================= */
function showScreen(id){
  const ov = $('overlay');
  if (!id){ ov.classList.add('hidden'); return; }
  ov.classList.remove('hidden');
  for (const s of ['menuScreen','pauseScreen','overScreen'])
    $(s).classList.toggle('active', s===id);
}
function togglePause(){
  if (state==='playing'){ state='paused'; showScreen('pauseScreen'); }
  else if (state==='paused'){ state='playing'; showScreen(null); }
}
$('startBtn').addEventListener('click', startGame);
$('restartBtn').addEventListener('click', startGame);
$('resumeBtn').addEventListener('click', togglePause);
$('pauseBtn').addEventListener('click', ()=>{ if (state==='playing'||state==='paused') togglePause(); });
$('muteBtn').addEventListener('click', ()=>{ muted=!muted; $('muteBtn').textContent = muted?'&#128263;':''; });

/* ================= 自适应尺寸 ================= */
const isTouch = matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window;
if (isTouch) $('controls').classList.add('show');
function resize(){
  const landscapeOverlay = isTouch && window.innerWidth > window.innerHeight;
  const ctrlH = (isTouch && !landscapeOverlay) ? $('controls').offsetHeight+8 : 0;
  const availW = window.innerWidth-4;
  const availH = window.innerHeight-ctrlH-4;
  const scale = Math.min(availW/CVW, availH/CVH);
  const w = Math.max(220, Math.floor(CVW*scale));
  const h = Math.floor(w*CVH/CVW);
  canvas.style.width = w+'px'; canvas.style.height = h+'px';
  $('canvas-wrap').style.width = w+'px'; $('canvas-wrap').style.height = h+'px';
}
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', ()=>setTimeout(resize,100));

/* ================= 启动 ================= */
(function init(){
  const dpr = Math.min(2, window.devicePixelRatio||1);
  canvas.width = CVW*dpr; canvas.height = CVH*dpr;
  ctx.setTransform(dpr,0,0,dpr,0,0);
  loadMap(0);
  player = makePlayer();
  $('bestScore').textContent = best;
  $('newRecord').style.display = 'none';
  showScreen('menuScreen');
  resize();
  let last = performance.now();
  function loop(now){
    const dt = Math.min(0.05, (now-last)/1000);
    last = now;
    try {
      if (state === 'playing') update(dt);
      else if (state === 'clear'){
        animTime += dt; clearT -= dt; updateEffects(dt);
        if (clearT <= 0){ level++; startLevel(); }
      }
      render();
    } catch(err){
      console.error('game loop error:', err);   // 记录错误但不断开 rAF 链
    }
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

免费评分

参与人数 1热心值 +1 收起 理由
JustFI987 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
demonsea 发表于 2026-7-23 12:39
玩了两局键盘操作不熟练留着有空慢慢玩
3#
chlong 发表于 2026-7-23 12:55
4#
wuming4 发表于 2026-7-23 13:14
5#
gaoxing01 发表于 2026-7-23 13:35
刚才试玩了下,真好玩
6#
Tomyi 发表于 2026-7-23 13:37
厉害了,手搓红白机游戏哇
7#
Cann 发表于 2026-7-23 13:43
正好上班摸鱼                        
8#
zhoustar2020 发表于 2026-7-23 13:43
高手  支持
9#
JustFI987 发表于 2026-7-23 13:43
试了一下,还挺好玩的,厉害厉害
10#
柒渊网络 发表于 2026-7-23 13:50
这个不错,本地就能轻松畅玩了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-24 22:37

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表