吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1501|回复: 40
上一主题 下一主题
收起左侧

[其他原创] 坦克大战本地版

[复制链接]
跳转到指定楼层
楼主
Summer000 发表于 2026-7-3 15:06 回帖奖励
本帖最后由 Summer000 于 2026-7-3 16:02 编辑

又做了一款吧本地坦克大战,不建议使用edge、IE浏览器,开始按钮会失效。
玩法
方向键 / WASD 控制坦克移动
空格键 射击
P 暂停 / 继续
[HTML] 纯文本查看 复制代码
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>坦克大战</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #000;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Press Start 2P', 'Courier New', monospace;
    overflow: hidden;
    color: #fff;
  }
  #gameWrapper {
    display: flex;
    gap: 20px;
    align-items: flex-start;
  }
  #gameCanvas {
    border: 3px solid #555;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }
  #sidebar {
    width: 160px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 10px;
  }
  .info-block { text-align: center; }
  .info-block .label {
    font-size: 10px;
    color: #888;
    margin-bottom: 6px;
    letter-spacing: 2px;
  }
  .info-block .value {
    font-size: 22px;
    color: #ff0;
    text-shadow: 0 0 8px rgba(255,255,0,0.5);
  }
  .enemy-icons {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    justify-content: center;
  }
  .enemy-icon {
    width: 16px; height: 16px;
    background: #e44;
    border-radius: 2px;
  }
  .enemy-icon.dead { background: #333; }
  #startScreen, #gameOverScreen, #winScreen {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.92);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 100;
    gap: 30px;
  }
  #startScreen h1 {
    font-size: 36px;
    color: #ff0;
    text-shadow: 0 0 20px rgba(255,255,0,0.6);
    letter-spacing: 6px;
  }
  #startScreen .subtitle {
    font-size: 12px;
    color: #aaa;
    letter-spacing: 3px;
  }
  .btn {
    padding: 14px 40px;
    font-size: 14px;
    font-family: inherit;
    background: transparent;
    color: #ff0;
    border: 2px solid #ff0;
    cursor: pointer;
    letter-spacing: 3px;
    transition: all 0.2s;
  }
  .btn:hover {
    background: #ff0;
    color: #000;
  }
  #gameOverScreen h1 { font-size: 32px; color: #e44; letter-spacing: 4px; }
  #winScreen h1 { font-size: 28px; color: #4f4; letter-spacing: 4px; }
  .score-display { font-size: 16px; color: #ff0; }
  .controls-hint {
    font-size: 10px;
    color: #666;
    line-height: 2;
    text-align: center;
  }
  .hidden { display: none !important; }
  #pauseOverlay {
    position: fixed;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    font-size: 28px;
    color: #ff0;
    letter-spacing: 6px;
    z-index: 90;
    text-shadow: 0 0 20px rgba(255,255,0,0.6);
  }
  
  /* Mobile Controls */
  #mobileControls {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 20px;
    z-index: 80;
    pointer-events: none;
  }
  
  #mobileControls.active {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
  }
  
  .dpad {
    position: relative;
    width: 140px;
    height: 140px;
    pointer-events: auto;
  }
  
  .dpad-btn {
    position: absolute;
    width: 46px;
    height: 46px;
    background: rgba(255, 255, 0, 0.3);
    border: 2px solid rgba(255, 255, 0, 0.6);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    color: #ff0;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
  
  .dpad-btn:active, .dpad-btn.pressed {
    background: rgba(255, 255, 0, 0.6);
  }
  
  .dpad-up { top: 0; left: 50%; transform: translateX(-50%); }
  .dpad-down { bottom: 0; left: 50%; transform: translateX(-50%); }
  .dpad-left { left: 0; top: 50%; transform: translateY(-50%); }
  .dpad-right { right: 0; top: 50%; transform: translateY(-50%); }
  
  .fire-btn {
    width: 80px;
    height: 80px;
    background: rgba(255, 68, 68, 0.4);
    border: 3px solid rgba(255, 68, 68, 0.8);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #fff;
    font-weight: bold;
    pointer-events: auto;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
  
  .fire-btn:active, .fire-btn.pressed {
    background: rgba(255, 68, 68, 0.7);
  }
  
  .pause-btn {
    width: 44px;
    height: 44px;
    background: rgba(255, 255, 0, 0.3);
    border: 2px solid rgba(255, 255, 0, 0.6);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    color: #ff0;
    font-weight: bold;
    pointer-events: auto;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
  
  .pause-btn:active, .pause-btn.pressed {
    background: rgba(255, 255, 0, 0.6);
  }
  
  /* Responsive layout */
  @media (max-width: 768px), (max-height: 600px) {
    #gameWrapper {
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }
    
    #sidebar {
      flex-direction: row;
      width: auto;
      gap: 20px;
      padding: 10px;
      flex-wrap: wrap;
      justify-content: center;
    }
    
    #gameCanvas {
      max-width: 90vw;
      max-height: 50vh;
      width: auto;
      height: auto;
    }
    
    #mobileControls {
      padding: 10px 20px 30px;
    }
    
    .dpad {
      width: 120px;
      height: 120px;
    }
    
    .dpad-btn {
      width: 40px;
      height: 40px;
      font-size: 16px;
    }
    
    .fire-btn {
      width: 70px;
      height: 70px;
      font-size: 12px;
    }
    
    .controls-hint {
      display: none;
    }
  }
  
  @media (max-width: 400px) {
    .dpad {
      width: 100px;
      height: 100px;
    }
    
    .dpad-btn {
      width: 34px;
      height: 34px;
      font-size: 14px;
    }
    
    .fire-btn {
      width: 60px;
      height: 60px;
    }
  }
</style>
</head>
<body tabindex="0">

<div id="startScreen">
  <h1>坦克大战</h1>
  <div class="subtitle">BATTLE CITY</div>
  <button class="btn">开始游戏</button>
  <div class="controls-hint" id="desktopHint">
    方向键 / WASD 移动<br>
    空格键 射击<br>
    P 暂停
  </div>
  <div class="controls-hint" id="mobileHint" style="display:none;">
    左侧方向键移动<br>
    右侧 FIRE 射击<br>
    II 按钮暂停
  </div>
</div>

<div id="gameOverScreen" class="hidden">
  <h1>游戏结束</h1>
  <div class="score-display">得分: <span id="finalScore">0</span></div>
  <button class="btn">再来一局</button>
</div>

<div id="winScreen" class="hidden">
  <h1>胜利!</h1>
  <div class="score-display">得分: <span id="winScore">0</span></div>
  <button class="btn">下一关</button>
</div>

<div id="pauseOverlay" class="hidden">暂停</div>

<div id="gameWrapper">
  <canvas id="gameCanvas" width="520" height="520"></canvas>
  <div id="sidebar">
    <div class="info-block">
      <div class="label">关卡</div>
      <div class="value" id="levelDisplay">1</div>
    </div>
    <div class="info-block">
      <div class="label">得分</div>
      <div class="value" id="scoreDisplay">0</div>
    </div>
    <div class="info-block">
      <div class="label">生命</div>
      <div class="value" id="livesDisplay">3</div>
    </div>
    <div class="info-block">
      <div class="label">剩余敌人</div>
      <div class="enemy-icons" id="enemyIcons"></div>
    </div>
    <div class="controls-hint">
      方向键/WASD 移动<br>
      空格 射击<br>
      P 暂停
    </div>
  </div>
</div>

<!-- Mobile Controls -->
<div id="mobileControls">
  <div class="dpad">
    <div class="dpad-btn dpad-up" data-dir="up">▲</div>
    <div class="dpad-btn dpad-down" data-dir="down">▼</div>
    <div class="dpad-btn dpad-left" data-dir="left">&#9664;</div>
    <div class="dpad-btn dpad-right" data-dir="right">&#9654;</div>
  </div>
  <div style="display:flex;flex-direction:column;gap:12px;align-items:center;pointer-events:auto;">
    <div class="pause-btn" id="pauseBtn">II</div>
    <div class="fire-btn" id="fireBtn">FIRE</div>
  </div>
</div>

<script>
// ==================== CONSTANTS ====================
const TILE = 20;
const COLS = 26;
const ROWS = 26;
const W = COLS * TILE;
const H = ROWS * TILE;
const HALF = TILE / 2;

const DIR = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3 };
const DX = [0, 1, 0, -1];
const DY = [-1, 0, 1, 0];

const TANK_SIZE = TILE * 2 - 2;
const BULLET_SIZE = 4;
const PLAYER_SPEED = 1.8;
const ENEMY_SPEED = 1.0;
const BULLET_SPEED = 3.5;
const ENEMY_BULLET_SPEED = 2.8;
const ENEMY_SHOOT_INTERVAL = 90;
const SPAWN_INTERVAL = 180;
const MAX_ENEMIES_ON_FIELD = 4;

const TILE_EMPTY = 0;
const TILE_BRICK = 1;
const TILE_STEEL = 2;
const TILE_WATER = 3;
const TILE_TREE = 4;
const TILE_BASE = 5;
const TILE_BASE_DEAD = 6;

const COLORS = {
  bg: '#000',
  brick: '#b35900',
  brickLight: '#cc7a33',
  steel: '#aaa',
  steelLight: '#ccc',
  water: '#2244aa',
  waterLight: '#3366cc',
  tree: '#226622',
  treeLight: '#33aa33',
  base: '#ff0',
  baseDead: '#666',
  playerBody: '#ff0',
  playerTurret: '#cc0',
  playerTrack: '#aa0',
  enemyBody: '#e44',
  enemyTurret: '#c22',
  enemyTrack: '#a22',
  enemyFastBody: '#e8e',
  enemyFastTurret: '#c6c',
  enemyArmorBody: '#4ae',
  enemyArmorTurret: '#28c',
  bullet: '#fff',
  explosion: ['#ff0', '#fa0', '#f60', '#f00', '#800']
};

// ==================== LEVELS ====================
const LEVELS = [
  // Level 1
  [
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011002200110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00110011001100110011001100",
    "00000000001100000000000000",
    "00000000001100000000000000",
    "11001100000000001100110000",
    "22001100000000001100002200",
    "00000000001100000000000000",
    "00000000001100000000000000",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00000000001111000000000000",
    "00000000001551000000000000",
    "00000000001551000000000000",
  ],
  // Level 2
  [
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00110022001100110022001100",
    "00110022001100110022001100",
    "00110000001100110000001100",
    "00110000001100110000001100",
    "00000011220000221100000000",
    "00000011220000221100000000",
    "00220000001100000000220000",
    "00220000001100000000220000",
    "00001100110000110011000000",
    "00001100110000110011000000",
    "11000022000000002200001100",
    "11000022000000002200001100",
    "00001100110000110011000000",
    "00001100110000110011000000",
    "00220000001100000000220000",
    "00220000001100000000220000",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000000000000000000000000",
    "00000000001111000000000000",
    "00000000001551000000000000",
    "00000000001551000000000000",
  ],
  // Level 3
  [
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00110011003300110011001100",
    "00110011003300110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000022001100220000001100",
    "00000022001100220000001100",
    "22110000002200000011002200",
    "22110000002200000011002200",
    "00001133000000331100000000",
    "00001133000000331100000000",
    "00110000221100220000110000",
    "00110000221100220000110000",
    "00001133000000331100000000",
    "00001133000000331100000000",
    "22110000002200000011002200",
    "22110000002200000011002200",
    "00000022001100220000001100",
    "00000022001100220000001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000000000000000000000000",
    "00000000001111000000000000",
    "00000000001551000000000000",
    "00000000001551000000000000",
  ],
];

// ==================== GAME STATE ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

let gameState = 'start'; // start, playing, paused, gameover, win
let level = 0;
let score = 0;
let lives = 3;
let map = [];
let player = null;
let enemies = [];
let bullets = [];
let explosions = [];
let powerUps = [];
let enemiesRemaining = 0;
let enemiesSpawned = 0;
let totalEnemies = 20;
let spawnTimer = 0;
let frameCount = 0;
let baseAlive = true;
let spawnPoints = [];
let keys = {};
let playerInvincible = 0;
let freezeTimer = 0;

// ==================== INPUT ====================
function handleKeyDown(e) {
  keys[e.code] = true;
  // Also map to key for compatibility
  if (e.key) keys[e.key] = true;
  
  if (e.key === 'p' || e.key === 'P') {
    if (gameState === 'playing') {
      gameState = 'paused';
      document.getElementById('pauseOverlay').classList.remove('hidden');
    } else if (gameState === 'paused') {
      gameState = 'playing';
      document.getElementById('pauseOverlay').classList.add('hidden');
    }
  }
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) {
    e.preventDefault();
  }
}

function handleKeyUp(e) {
  keys[e.code] = false;
  if (e.key) keys[e.key] = false;
}

document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);

// Ensure focus for keyboard input in iframe environments
window.addEventListener('load', () => { document.body.focus(); });
document.addEventListener('click', () => { document.body.focus(); });
document.addEventListener('touchstart', () => { document.body.focus(); });

// ==================== MOBILE CONTROLS ====================
const mobileControls = document.getElementById('mobileControls');
const dpadBtns = document.querySelectorAll('.dpad-btn');
const fireBtn = document.getElementById('fireBtn');

// Detect touch device and show controls
function checkTouchDevice() {
  if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
    mobileControls.classList.add('active');
    document.getElementById('desktopHint').style.display = 'none';
    document.getElementById('mobileHint').style.display = 'block';
  }
}
checkTouchDevice();

// D-pad touch handling
const dirMap = { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
const codeMap = { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };

dpadBtns.forEach(btn => {
  const dir = btn.dataset.dir;
  
  btn.addEventListener('touchstart', (e) => {
    e.preventDefault();
    btn.classList.add('pressed');
    keys[dirMap[dir]] = true;
    keys[codeMap[dir]] = true;
  });
  
  btn.addEventListener('touchend', (e) => {
    e.preventDefault();
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
  
  btn.addEventListener('touchcancel', (e) => {
    e.preventDefault();
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
  
  // Mouse fallback for testing
  btn.addEventListener('mousedown', (e) => {
    e.preventDefault();
    btn.classList.add('pressed');
    keys[dirMap[dir]] = true;
    keys[codeMap[dir]] = true;
  });
  
  btn.addEventListener('mouseup', (e) => {
    e.preventDefault();
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
  
  btn.addEventListener('mouseleave', (e) => {
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
});

// Fire button touch handling
fireBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  fireBtn.classList.add('pressed');
  keys[' '] = true;
  keys['Space'] = true;
});

fireBtn.addEventListener('touchend', (e) => {
  e.preventDefault();
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});

fireBtn.addEventListener('touchcancel', (e) => {
  e.preventDefault();
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});

// Mouse fallback for fire button
fireBtn.addEventListener('mousedown', (e) => {
  e.preventDefault();
  fireBtn.classList.add('pressed');
  keys[' '] = true;
  keys['Space'] = true;
});

fireBtn.addEventListener('mouseup', (e) => {
  e.preventDefault();
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});

fireBtn.addEventListener('mouseleave', (e) => {
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});

// Pause button
const pauseBtn = document.getElementById('pauseBtn');
function togglePause() {
  if (gameState === 'playing') {
    gameState = 'paused';
    document.getElementById('pauseOverlay').classList.remove('hidden');
  } else if (gameState === 'paused') {
    gameState = 'playing';
    document.getElementById('pauseOverlay').classList.add('hidden');
  }
}

pauseBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  pauseBtn.classList.add('pressed');
  togglePause();
});
pauseBtn.addEventListener('touchend', (e) => {
  e.preventDefault();
  pauseBtn.classList.remove('pressed');
});
pauseBtn.addEventListener('click', (e) => {
  e.preventDefault();
  togglePause();
});

// ==================== MAP ====================
function loadMap(lvl) {
  const data = LEVELS[lvl % LEVELS.length];
  map = [];
  for (let r = 0; r < ROWS; r++) {
    map[r] = [];
    for (let c = 0; c < COLS; c++) {
      map[r][c] = parseInt(data[r][c]);
    }
  }
}

function drawTile(r, c) {
  const x = c * TILE;
  const y = r * TILE;
  const t = map[r][c];

  if (t === TILE_BRICK) {
    ctx.fillStyle = COLORS.brick;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = COLORS.brickLight;
    ctx.fillRect(x, y, TILE/2, TILE/2);
    ctx.fillRect(x + TILE/2, y + TILE/2, TILE/2, TILE/2);
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 0.5;
    ctx.strokeRect(x, y, TILE, TILE);
  } else if (t === TILE_STEEL) {
    ctx.fillStyle = COLORS.steel;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = COLORS.steelLight;
    ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
    ctx.fillStyle = COLORS.steel;
    ctx.fillRect(x + 5, y + 5, TILE - 10, TILE - 10);
  } else if (t === TILE_WATER) {
    const wave = Math.sin(frameCount * 0.05 + c + r) * 0.3 + 0.5;
    ctx.fillStyle = wave > 0.5 ? COLORS.water : COLORS.waterLight;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = wave > 0.5 ? COLORS.waterLight : COLORS.water;
    for (let i = 0; i < 3; i++) {
      const wy = y + 4 + i * 6 + Math.sin(frameCount * 0.08 + c * 0.5) * 2;
      ctx.fillRect(x, wy, TILE, 2);
    }
  } else if (t === TILE_BASE) {
    ctx.fillStyle = COLORS.base;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = '#000';
    ctx.fillRect(x + 4, y + 4, TILE - 8, TILE - 8);
    ctx.fillStyle = COLORS.base;
    ctx.fillRect(x + 7, y + 7, TILE - 14, TILE - 14);
  } else if (t === TILE_BASE_DEAD) {
    ctx.fillStyle = COLORS.baseDead;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = '#444';
    ctx.fillRect(x + 3, y + 3, TILE - 6, TILE - 6);
  }
}

function drawTree(r, c) {
  if (map[r][c] !== TILE_TREE) return;
  const x = c * TILE;
  const y = r * TILE;
  ctx.fillStyle = COLORS.tree;
  ctx.fillRect(x, y, TILE, TILE);
  ctx.fillStyle = COLORS.treeLight;
  ctx.fillRect(x + 2, y + 2, 6, 6);
  ctx.fillRect(x + 10, y + 8, 6, 6);
  ctx.fillRect(x + 4, y + 12, 6, 6);
}

// ==================== TANK ====================
class Tank {
  constructor(x, y, dir, isPlayer, type) {
    this.x = x;
    this.y = y;
    this.dir = dir;
    this.isPlayer = isPlayer;
    this.type = type || 'normal'; // normal, fast, armor
    this.speed = isPlayer ? PLAYER_SPEED : (type === 'fast' ? ENEMY_SPEED * 1.6 : ENEMY_SPEED);
    this.hp = isPlayer ? 1 : (type === 'armor' ? 3 : 1);
    this.maxHp = this.hp;
    this.shootCooldown = 0;
    this.alive = true;
    this.moveTimer = 0;
    this.aiDir = dir;
    this.aiDirTimer = 0;
    this.flashTimer = 0;
    this.spawnAnim = 30;
    this.size = TANK_SIZE;
  }

  get cx() { return this.x + this.size / 2; }
  get cy() { return this.y + this.size / 2; }

  update() {
    if (this.spawnAnim > 0) { this.spawnAnim--; return; }
    if (this.shootCooldown > 0) this.shootCooldown--;
    if (this.flashTimer > 0) this.flashTimer--;

    if (this.isPlayer) {
      this.updatePlayer();
    } else {
      if (freezeTimer <= 0) this.updateAI();
    }
  }

  updatePlayer() {
    let moving = false;
    let newDir = this.dir;

    if (keys['ArrowUp'] || keys['KeyW'] || keys['w'] || keys['W']) { newDir = DIR.UP; moving = true; }
    else if (keys['ArrowDown'] || keys['KeyS'] || keys['s'] || keys['S']) { newDir = DIR.DOWN; moving = true; }
    else if (keys['ArrowLeft'] || keys['KeyA'] || keys['a'] || keys['A']) { newDir = DIR.LEFT; moving = true; }
    else if (keys['ArrowRight'] || keys['KeyD'] || keys['d'] || keys['D']) { newDir = DIR.RIGHT; moving = true; }

    this.dir = newDir;
    if (moving) this.move(this.dir);

    if (keys[' '] || keys['Space']) this.shoot();
  }

  updateAI() {
    this.aiDirTimer--;
    if (this.aiDirTimer <= 0) {
      // Change direction periodically or when blocked
      const dirs = [DIR.UP, DIR.RIGHT, DIR.DOWN, DIR.LEFT];
      // Bias towards player direction
      if (player && player.alive && Math.random() < 0.3) {
        const dx = player.cx - this.cx;
        const dy = player.cy - this.cy;
        if (Math.abs(dx) > Math.abs(dy)) {
          this.aiDir = dx > 0 ? DIR.RIGHT : DIR.LEFT;
        } else {
          this.aiDir = dy > 0 ? DIR.DOWN : DIR.UP;
        }
      } else {
        this.aiDir = dirs[Math.floor(Math.random() * 4)];
      }
      this.aiDirTimer = 60 + Math.floor(Math.random() * 120);
    }

    this.dir = this.aiDir;
    const moved = this.move(this.dir);
    if (!moved) {
      this.aiDirTimer = 0; // change direction next frame
    }

    // Shooting
    if (Math.random() < 0.02 || (this.shootCooldown <= 0 && Math.random() < 0.05)) {
      this.shoot();
    }
  }

  move(dir) {
    const nx = this.x + DX[dir] * this.speed;
    const ny = this.y + DY[dir] * this.speed;

    // Boundary check
    if (nx < 0 || ny < 0 || nx + this.size > W || ny + this.size > H) return false;

    // Tile collision
    if (this.checkTileCollision(nx, ny)) return false;

    // Tank-tank collision
    if (this.checkTankCollision(nx, ny)) return false;

    this.x = nx;
    this.y = ny;
    return true;
  }

  checkTileCollision(nx, ny) {
    const margin = 1;
    const left = Math.floor((nx + margin) / TILE);
    const top = Math.floor((ny + margin) / TILE);
    const right = Math.floor((nx + this.size - 1 - margin) / TILE);
    const bottom = Math.floor((ny + this.size - 1 - margin) / TILE);

    for (let r = top; r <= bottom; r++) {
      for (let c = left; c <= right; c++) {
        if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return true;
        const t = map[r][c];
        if (t === TILE_BRICK || t === TILE_STEEL || t === TILE_WATER || t === TILE_BASE || t === TILE_BASE_DEAD) {
          return true;
        }
      }
    }
    return false;
  }

  checkTankCollision(nx, ny) {
    const allTanks = [player, ...enemies].filter(t => t && t.alive && t !== this && t.spawnAnim <= 0);
    for (const other of allTanks) {
      if (nx < other.x + other.size && nx + this.size > other.x &&
          ny < other.y + other.size && ny + this.size > other.y) {
        return true;
      }
    }
    return false;
  }

  shoot() {
    if (this.shootCooldown > 0) return;
    // Limit bullets per tank
    const myBullets = bullets.filter(b => b.owner === this);
    if (this.isPlayer && myBullets.length >= 2) return;
    if (!this.isPlayer && myBullets.length >= 1) return;

    const bx = this.cx - BULLET_SIZE / 2 + DX[this.dir] * (this.size / 2);
    const by = this.cy - BULLET_SIZE / 2 + DY[this.dir] * (this.size / 2);
    const speed = this.isPlayer ? BULLET_SPEED : ENEMY_BULLET_SPEED;

    bullets.push(new Bullet(bx, by, this.dir, this.isPlayer, this, speed));
    this.shootCooldown = this.isPlayer ? 15 : ENEMY_SHOOT_INTERVAL;
  }

  draw() {
    if (!this.alive) return;

    // Spawn animation
    if (this.spawnAnim > 0) {
      const flash = this.spawnAnim % 8 < 4;
      ctx.strokeStyle = flash ? '#fff' : '#ff0';
      ctx.lineWidth = 2;
      const s = this.size * (1 - this.spawnAnim / 30) * 0.8;
      ctx.strokeRect(this.cx - s/2, this.cy - s/2, s, s);
      return;
    }

    ctx.save();
    ctx.translate(this.cx, this.cy);
    ctx.rotate(this.dir * Math.PI / 2);

    const s = this.size;
    const hs = s / 2;

    // Flash when hit
    if (this.flashTimer > 0 && this.flashTimer % 4 < 2) {
      ctx.globalAlpha = 0.5;
    }

    // Body colors
    let bodyColor, turretColor, trackColor;
    if (this.isPlayer) {
      bodyColor = COLORS.playerBody;
      turretColor = COLORS.playerTurret;
      trackColor = COLORS.playerTrack;
    } else if (this.type === 'fast') {
      bodyColor = COLORS.enemyFastBody;
      turretColor = COLORS.enemyFastTurret;
      trackColor = COLORS.enemyFastBody;
    } else if (this.type === 'armor') {
      bodyColor = this.hp > 1 ? COLORS.enemyArmorBody : COLORS.enemyBody;
      turretColor = this.hp > 1 ? COLORS.enemyArmorTurret : COLORS.enemyTurret;
      trackColor = COLORS.enemyArmorBody;
    } else {
      bodyColor = COLORS.enemyBody;
      turretColor = COLORS.enemyTurret;
      trackColor = COLORS.enemyTrack;
    }

    // Tracks
    ctx.fillStyle = trackColor;
    ctx.fillRect(-hs, -hs, s * 0.22, s);
    ctx.fillRect(hs - s * 0.22, -hs, s * 0.22, s);
    // Track details
    ctx.fillStyle = '#000';
    for (let i = 0; i < 5; i++) {
      const ty = -hs + 3 + i * (s / 5);
      ctx.fillRect(-hs + 1, ty, s * 0.2, 2);
      ctx.fillRect(hs - s * 0.22 + 1, ty, s * 0.2, 2);
    }

    // Body
    ctx.fillStyle = bodyColor;
    ctx.fillRect(-hs + s * 0.22, -hs + 2, s * 0.56, s - 4);

    // Turret base
    ctx.fillStyle = turretColor;
    ctx.fillRect(-s * 0.18, -s * 0.18, s * 0.36, s * 0.36);

    // Barrel
    ctx.fillStyle = turretColor;
    ctx.fillRect(-2, -hs - 2, 4, hs);

    // Invincibility shield
    if (this.isPlayer && playerInvincible > 0) {
      ctx.strokeStyle = playerInvincible % 6 < 3 ? '#0ff' : '#fff';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(0, 0, hs + 2, 0, Math.PI * 2);
      ctx.stroke();
    }

    ctx.restore();
  }

  hit() {
    this.hp--;
    this.flashTimer = 15;
    if (this.hp <= 0) {
      this.alive = false;
      addExplosion(this.cx, this.cy, true);
      if (!this.isPlayer) {
        score += this.type === 'armor' ? 300 : this.type === 'fast' ? 200 : 100;
        updateUI();
      }
    }
  }
}

// ==================== BULLET ====================
class Bullet {
  constructor(x, y, dir, isPlayerBullet, owner, speed) {
    this.x = x;
    this.y = y;
    this.dir = dir;
    this.isPlayerBullet = isPlayerBullet;
    this.owner = owner;
    this.speed = speed;
    this.alive = true;
    this.size = BULLET_SIZE;
  }

  update() {
    this.x += DX[this.dir] * this.speed;
    this.y += DY[this.dir] * this.speed;

    // Boundary
    if (this.x < 0 || this.y < 0 || this.x > W || this.y > H) {
      this.alive = false;
      addExplosion(this.x, this.y, false);
      return;
    }

    // Tile collision
    this.checkTileHit();
    if (!this.alive) return;

    // Tank collision
    this.checkTankHit();
  }

  checkTileHit() {
    const cx = this.x + this.size / 2;
    const cy = this.y + this.size / 2;
    const c = Math.floor(cx / TILE);
    const r = Math.floor(cy / TILE);

    // Check surrounding tiles
    for (let dr = -1; dr <= 1; dr++) {
      for (let dc = -1; dc <= 1; dc++) {
        const tr = r + dr;
        const tc = c + dc;
        if (tr < 0 || tr >= ROWS || tc < 0 || tc >= COLS) continue;

        const tx = tc * TILE;
        const ty = tr * TILE;
        if (this.x + this.size <= tx || this.x >= tx + TILE ||
            this.y + this.size <= ty || this.y >= ty + TILE) continue;

        const t = map[tr][tc];
        if (t === TILE_BRICK) {
          map[tr][tc] = TILE_EMPTY;
          this.alive = false;
          addExplosion(cx, cy, false);
          return;
        } else if (t === TILE_STEEL) {
          if (this.isPlayerBullet) {
            // Player bullets can't destroy steel (unless power-up)
          }
          this.alive = false;
          addExplosion(cx, cy, false);
          return;
        } else if (t === TILE_BASE) {
          map[tr][tc] = TILE_BASE_DEAD;
          baseAlive = false;
          this.alive = false;
          addExplosion(tx + TILE/2, ty + TILE/2, true);
          return;
        }
      }
    }
  }

  checkTankHit() {
    if (this.isPlayerBullet) {
      for (const enemy of enemies) {
        if (!enemy.alive || enemy.spawnAnim > 0) continue;
        if (this.x < enemy.x + enemy.size && this.x + this.size > enemy.x &&
            this.y < enemy.y + enemy.size && this.y + this.size > enemy.y) {
          this.alive = false;
          enemy.hit();
          return;
        }
      }
    } else {
      if (player && player.alive && player.spawnAnim <= 0) {
        if (this.x < player.x + player.size && this.x + this.size > player.x &&
            this.y < player.y + player.size && this.y + this.size > player.y) {
          this.alive = false;
          if (playerInvincible <= 0) {
            player.hit();
            if (!player.alive) {
              lives--;
              updateUI();
              if (lives > 0) {
                setTimeout(() => respawnPlayer(), 1500);
              }
            }
          }
          return;
        }
      }
    }

    // Bullet-bullet collision
    for (const other of bullets) {
      if (other === this || !other.alive) continue;
      if (this.isPlayerBullet !== other.isPlayerBullet) {
        if (this.x < other.x + other.size && this.x + this.size > other.x &&
            this.y < other.y + other.size && this.y + this.size > other.y) {
          this.alive = false;
          other.alive = false;
          return;
        }
      }
    }
  }

  draw() {
    if (!this.alive) return;
    ctx.fillStyle = COLORS.bullet;
    ctx.shadowColor = '#ff0';
    ctx.shadowBlur = 4;
    ctx.fillRect(this.x, this.y, this.size, this.size);
    ctx.shadowBlur = 0;
  }
}

// ==================== EXPLOSION ====================
class Explosion {
  constructor(x, y, big) {
    this.x = x;
    this.y = y;
    this.big = big;
    this.frame = 0;
    this.maxFrame = big ? 20 : 12;
    this.alive = true;
  }

  update() {
    this.frame++;
    if (this.frame >= this.maxFrame) this.alive = false;
  }

  draw() {
    if (!this.alive) return;
    const progress = this.frame / this.maxFrame;
    const maxR = this.big ? 30 : 15;
    let r;
    if (progress < 0.4) {
      r = maxR * (progress / 0.4);
    } else {
      r = maxR * (1 - (progress - 0.4) / 0.6);
    }

    const colorIdx = Math.min(Math.floor(progress * COLORS.explosion.length), COLORS.explosion.length - 1);
    ctx.fillStyle = COLORS.explosion[colorIdx];
    ctx.globalAlpha = 1 - progress * 0.5;
    ctx.beginPath();
    ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
    ctx.fill();

    if (this.big && progress < 0.5) {
      ctx.fillStyle = COLORS.explosion[0];
      ctx.beginPath();
      ctx.arc(this.x, this.y, r * 0.5, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }
}

function addExplosion(x, y, big) {
  explosions.push(new Explosion(x, y, big));
}

// ==================== POWER-UP ====================
class PowerUp {
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type; // 'star', 'shield', 'bomb', 'life'
    this.alive = true;
    this.timer = 600; // 10 seconds
    this.size = TILE * 2;
  }

  update() {
    this.timer--;
    if (this.timer <= 0) this.alive = false;

    if (player && player.alive) {
      if (this.x < player.x + player.size && this.x + this.size > player.x &&
          this.y < player.y + player.size && this.y + this.size > player.y) {
        this.alive = false;
        this.apply();
      }
    }
  }

  apply() {
    switch (this.type) {
      case 'shield':
        playerInvincible = 300;
        break;
      case 'bomb':
        enemies.forEach(e => {
          if (e.alive) {
            e.alive = false;
            addExplosion(e.cx, e.cy, true);
            score += 50;
          }
        });
        break;
      case 'life':
        lives++;
        break;
      case 'star':
        player.speed = PLAYER_SPEED * 1.3;
        break;
    }
    score += 500;
    updateUI();
  }

  draw() {
    if (!this.alive) return;
    if (this.timer % 20 < 10 && this.timer < 120) return; // blink when about to expire

    const x = this.x;
    const y = this.y;
    const s = this.size;

    ctx.fillStyle = '#222';
    ctx.fillRect(x, y, s, s);
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 1;
    ctx.strokeRect(x, y, s, s);

    ctx.font = '16px monospace';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    switch (this.type) {
      case 'shield':
        ctx.fillStyle = '#0ff';
        ctx.fillText('S', x + s/2, y + s/2);
        break;
      case 'bomb':
        ctx.fillStyle = '#f44';
        ctx.fillText('B', x + s/2, y + s/2);
        break;
      case 'life':
        ctx.fillStyle = '#4f4';
        ctx.fillText('+', x + s/2, y + s/2);
        break;
      case 'star':
        ctx.fillStyle = '#ff0';
        ctx.fillText('*', x + s/2, y + s/2);
        break;
    }
  }
}

// ==================== GAME LOGIC ====================
function spawnEnemy() {
  if (enemiesSpawned >= totalEnemies) return;
  if (enemies.filter(e => e.alive).length >= MAX_ENEMIES_ON_FIELD) return;

  const sp = spawnPoints[enemiesSpawned % spawnPoints.length];
  // Check if spawn point is clear
  const allTanks = [player, ...enemies].filter(t => t && t.alive);
  for (const t of allTanks) {
    if (sp.x < t.x + t.size && sp.x + TANK_SIZE > t.x &&
        sp.y < t.y + t.size && sp.y + TANK_SIZE > t.y) {
      return; // spawn point blocked
    }
  }

  const types = ['normal', 'normal', 'fast', 'armor'];
  const typeIdx = Math.min(Math.floor(enemiesSpawned / 5), types.length - 1);
  const type = types[Math.floor(Math.random() * (typeIdx + 1))];

  const enemy = new Tank(sp.x, sp.y, DIR.DOWN, false, type);
  enemies.push(enemy);
  enemiesSpawned++;
  updateUI();
}

function respawnPlayer() {
  if (gameState !== 'playing') return;
  player = new Tank(4 * TILE, 24 * TILE, DIR.UP, true);
  playerInvincible = 120;
}

function startGame() {
  document.getElementById('startScreen').classList.add('hidden');
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('winScreen').classList.add('hidden');

  level = 0;
  score = 0;
  lives = 3;
  initLevel();
  gameState = 'playing';
  document.body.focus();
}

function nextLevel() {
  document.getElementById('winScreen').classList.add('hidden');
  level++;
  initLevel();
  gameState = 'playing';
  document.body.focus();
}

function initLevel() {
  loadMap(level);
  bullets = [];
  explosions = [];
  powerUps = [];
  enemies = [];
  enemiesSpawned = 0;
  totalEnemies = 20;
  enemiesRemaining = totalEnemies;
  spawnTimer = 0;
  baseAlive = true;
  playerInvincible = 120;
  freezeTimer = 0;

  spawnPoints = [
    { x: 0, y: 0 },
    { x: 12 * TILE, y: 0 },
    { x: 24 * TILE, y: 0 }
  ];

  player = new Tank(4 * TILE, 24 * TILE, DIR.UP, true);
  updateUI();
}

function updateUI() {
  document.getElementById('levelDisplay').textContent = level + 1;
  document.getElementById('scoreDisplay').textContent = score;
  document.getElementById('livesDisplay').textContent = lives;

  const remaining = totalEnemies - enemiesSpawned + enemies.filter(e => e.alive).length;
  const icons = document.getElementById('enemyIcons');
  icons.innerHTML = '';
  for (let i = 0; i < totalEnemies; i++) {
    const div = document.createElement('div');
    div.className = 'enemy-icon' + (i >= remaining ? ' dead' : '');
    icons.appendChild(div);
  }
}

function checkWinLose() {
  if (!baseAlive) {
    gameState = 'gameover';
    document.getElementById('finalScore').textContent = score;
    document.getElementById('gameOverScreen').classList.remove('hidden');
    return;
  }

  if (lives <= 0 && (!player || !player.alive)) {
    gameState = 'gameover';
    document.getElementById('finalScore').textContent = score;
    document.getElementById('gameOverScreen').classList.remove('hidden');
    return;
  }

  const allDead = enemies.every(e => !e.alive);
  if (enemiesSpawned >= totalEnemies && allDead) {
    gameState = 'win';
    document.getElementById('winScore').textContent = score;
    document.getElementById('winScreen').classList.remove('hidden');
  }
}

// ==================== MAIN LOOP ====================
function update() {
  if (gameState !== 'playing') return;

  frameCount++;

  // Spawn enemies
  spawnTimer++;
  if (spawnTimer >= SPAWN_INTERVAL) {
    spawnTimer = 0;
    spawnEnemy();
  }

  // Update timers
  if (playerInvincible > 0) playerInvincible--;
  if (freezeTimer > 0) freezeTimer--;

  // Update player
  if (player && player.alive) player.update();

  // Update enemies
  enemies.forEach(e => { if (e.alive) e.update(); });

  // Update bullets
  bullets.forEach(b => { if (b.alive) b.update(); });
  bullets = bullets.filter(b => b.alive);

  // Update explosions
  explosions.forEach(e => e.update());
  explosions = explosions.filter(e => e.alive);

  // Update power-ups
  powerUps.forEach(p => { if (p.alive) p.update(); });
  powerUps = powerUps.filter(p => p.alive);

  // Random power-up drop
  if (frameCount % 600 === 0 && Math.random() < 0.5) {
    const types = ['shield', 'bomb', 'life', 'star'];
    const type = types[Math.floor(Math.random() * types.length)];
    const px = Math.floor(Math.random() * (COLS - 2)) * TILE;
    const py = (4 + Math.floor(Math.random() * 16)) * TILE;
    powerUps.push(new PowerUp(px, py, type));
  }

  checkWinLose();
}

function draw() {
  // Background
  ctx.fillStyle = COLORS.bg;
  ctx.fillRect(0, 0, W, H);

  // Don't draw map if not initialized
  if (!map || map.length === 0) return;

  // Draw map (bottom layer - no trees)
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      if (map[r][c] !== TILE_EMPTY && map[r][c] !== TILE_TREE) {
        drawTile(r, c);
      }
    }
  }

  // Draw power-ups
  powerUps.forEach(p => p.draw());

  // Draw tanks
  if (player && player.alive) player.draw();
  enemies.forEach(e => { if (e.alive) e.draw(); });

  // Draw bullets
  bullets.forEach(b => { if (b.alive) b.draw(); });

  // Draw trees (top layer - covers tanks)
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      drawTree(r, c);
    }
  }

  // Draw explosions (topmost)
  explosions.forEach(e => e.draw());

  // Debug: show active keys
  const activeKeys = Object.entries(keys).filter(([k, v]) => v).map(([k]) => k);
  if (activeKeys.length > 0) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(2, H - 22, 200, 20);
    ctx.fillStyle = '#0f0';
    ctx.font = '10px monospace';
    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.fillText('Keys: ' + activeKeys.join(', '), 6, H - 12);
  }
}

function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

// Start the loop
gameLoop();
</script>
</body>
</html>

85515377b1824572bfb6e5ec92f366c8.png (20.14 KB, 下载次数: 2)

85515377b1824572bfb6e5ec92f366c8.png

免费评分

参与人数 4吾爱币 +3 热心值 +4 收起 理由
抱薪风雪雾 + 1 + 1 谢谢@Thanks!
Lcp1027 + 1 + 1 谢谢@Thanks!
hjsm + 1 + 1 能不能更新一下,把大家测试完的BUG修复以后,再发一个新版本出来
zxq1010117 + 1 谢谢@Thanks!

查看全部评分

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

推荐
l12345678qaz 发表于 2026-7-3 17:13
不能玩,根据原代码,修改后可以玩。

[HTML] 纯文本查看 复制代码
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>坦克大战</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #000;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Press Start 2P', 'Courier New', monospace;
    overflow: hidden;
    color: #fff;
  }
  #gameWrapper {
    display: flex;
    gap: 20px;
    align-items: flex-start;
  }
  #gameCanvas {
    border: 3px solid #555;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }
  #sidebar {
    width: 160px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 10px;
  }
  .info-block { text-align: center; }
  .info-block .label {
    font-size: 10px;
    color: #888;
    margin-bottom: 6px;
    letter-spacing: 2px;
  }
  .info-block .value {
    font-size: 22px;
    color: #ff0;
    text-shadow: 0 0 8px rgba(255,255,0,0.5);
  }
  .enemy-icons {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    justify-content: center;
  }
  .enemy-icon {
    width: 16px; height: 16px;
    background: #e44;
    border-radius: 2px;
  }
  .enemy-icon.dead { background: #333; }
  #startScreen, #gameOverScreen, #winScreen {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.92);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 100;
    gap: 30px;
  }
  #startScreen h1 {
    font-size: 36px;
    color: #ff0;
    text-shadow: 0 0 20px rgba(255,255,0,0.6);
    letter-spacing: 6px;
  }
  #startScreen .subtitle {
    font-size: 12px;
    color: #aaa;
    letter-spacing: 3px;
  }
  .btn {
    padding: 14px 40px;
    font-size: 14px;
    font-family: inherit;
    background: transparent;
    color: #ff0;
    border: 2px solid #ff0;
    cursor: pointer;
    letter-spacing: 3px;
    transition: all 0.2s;
  }
  .btn:hover {
    background: #ff0;
    color: #000;
  }
  #gameOverScreen h1 { font-size: 32px; color: #e44; letter-spacing: 4px; }
  #winScreen h1 { font-size: 28px; color: #4f4; letter-spacing: 4px; }
  .score-display { font-size: 16px; color: #ff0; }
  .controls-hint {
    font-size: 10px;
    color: #666;
    line-height: 2;
    text-align: center;
  }
  .hidden { display: none !important; }
  #pauseOverlay {
    position: fixed;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    font-size: 28px;
    color: #ff0;
    letter-spacing: 6px;
    z-index: 90;
    text-shadow: 0 0 20px rgba(255,255,0,0.6);
  }
   
  /* Mobile Controls */
  #mobileControls {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 20px;
    z-index: 80;
    pointer-events: none;
  }
   
  #mobileControls.active {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
  }
   
  .dpad {
    position: relative;
    width: 140px;
    height: 140px;
    pointer-events: auto;
  }
   
  .dpad-btn {
    position: absolute;
    width: 46px;
    height: 46px;
    background: rgba(255, 255, 0, 0.3);
    border: 2px solid rgba(255, 255, 0, 0.6);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    color: #ff0;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
   
  .dpad-btn:active, .dpad-btn.pressed {
    background: rgba(255, 255, 0, 0.6);
  }
   
  .dpad-up { top: 0; left: 50%; transform: translateX(-50%); }
  .dpad-down { bottom: 0; left: 50%; transform: translateX(-50%); }
  .dpad-left { left: 0; top: 50%; transform: translateY(-50%); }
  .dpad-right { right: 0; top: 50%; transform: translateY(-50%); }
   
  .fire-btn {
    width: 80px;
    height: 80px;
    background: rgba(255, 68, 68, 0.4);
    border: 3px solid rgba(255, 68, 68, 0.8);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #fff;
    font-weight: bold;
    pointer-events: auto;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
   
  .fire-btn:active, .fire-btn.pressed {
    background: rgba(255, 68, 68, 0.7);
  }
   
  .pause-btn {
    width: 44px;
    height: 44px;
    background: rgba(255, 255, 0, 0.3);
    border: 2px solid rgba(255, 255, 0, 0.6);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    color: #ff0;
    font-weight: bold;
    pointer-events: auto;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
   
  .pause-btn:active, .pause-btn.pressed {
    background: rgba(255, 255, 0, 0.6);
  }
   
  /* Responsive layout */
  [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 768px), (max-height: 600px) {
    #gameWrapper {
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }
     
    #sidebar {
      flex-direction: row;
      width: auto;
      gap: 20px;
      padding: 10px;
      flex-wrap: wrap;
      justify-content: center;
    }
     
    #gameCanvas {
      max-width: 90vw;
      max-height: 50vh;
      width: auto;
      height: auto;
    }
     
    #mobileControls {
      padding: 10px 20px 30px;
    }
     
    .dpad {
      width: 120px;
      height: 120px;
    }
     
    .dpad-btn {
      width: 40px;
      height: 40px;
      font-size: 16px;
    }
     
    .fire-btn {
      width: 70px;
      height: 70px;
      font-size: 12px;
    }
     
    .controls-hint {
      display: none;
    }
  }
   
  @media (max-width: 400px) {
    .dpad {
      width: 100px;
      height: 100px;
    }
     
    .dpad-btn {
      width: 34px;
      height: 34px;
      font-size: 14px;
    }
     
    .fire-btn {
      width: 60px;
      height: 60px;
    }
  }
</style>
</head>
<body tabindex="0">
 
<div id="startScreen">
  <h1>坦克大战</h1>
  <div class="subtitle">BATTLE CITY</div>
  <button class="btn">开始游戏</button>
  <div class="controls-hint" id="desktopHint">
    方向键 / WASD 移动<br>
    空格键 射击<br>
    P 暂停
  </div>
  <div class="controls-hint" id="mobileHint" style="display:none;">
    左侧方向键移动<br>
    右侧 FIRE 射击<br>
    II 按钮暂停
  </div>
</div>
 
<div id="gameOverScreen" class="hidden">
  <h1>游戏结束</h1>
  <div class="score-display">得分: <span id="finalScore">0</span></div>
  <button class="btn">再来一局</button>
</div>
 
<div id="winScreen" class="hidden">
  <h1>胜利!</h1>
  <div class="score-display">得分: <span id="winScore">0</span></div>
  <button class="btn">下一关</button>
</div>
 
<div id="pauseOverlay" class="hidden">暂停</div>
 
<div id="gameWrapper">
  <canvas id="gameCanvas" width="520" height="520"></canvas>
  <div id="sidebar">
    <div class="info-block">
      <div class="label">关卡</div>
      <div class="value" id="levelDisplay">1</div>
    </div>
    <div class="info-block">
      <div class="label">得分</div>
      <div class="value" id="scoreDisplay">0</div>
    </div>
    <div class="info-block">
      <div class="label">生命</div>
      <div class="value" id="livesDisplay">3</div>
    </div>
    <div class="info-block">
      <div class="label">剩余敌人</div>
      <div class="enemy-icons" id="enemyIcons"></div>
    </div>
    <div class="controls-hint">
      方向键/WASD 移动<br>
      空格 射击<br>
      P 暂停
    </div>
  </div>
</div>
 
<!-- Mobile Controls -->
<div id="mobileControls">
  <div class="dpad">
    <div class="dpad-btn dpad-up" data-dir="up">▲</div>
    <div class="dpad-btn dpad-down" data-dir="down">▼</div>
    <div class="dpad-btn dpad-left" data-dir="left">&#9664;</div>
    <div class="dpad-btn dpad-right" data-dir="right">&#9654;</div>
  </div>
  <div style="display:flex;flex-direction:column;gap:12px;align-items:center;pointer-events:auto;">
    <div class="pause-btn" id="pauseBtn">II</div>
    <div class="fire-btn" id="fireBtn">FIRE</div>
  </div>
</div>
 
<script>
// ==================== CONSTANTS ====================
const TILE = 20;
const COLS = 26;
const ROWS = 26;
const W = COLS * TILE;
const H = ROWS * TILE;
const HALF = TILE / 2;
 
const DIR = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3 };
const DX = [0, 1, 0, -1];
const DY = [-1, 0, 1, 0];
 
const TANK_SIZE = TILE * 2 - 2;
const BULLET_SIZE = 4;
const PLAYER_SPEED = 1.8;
const ENEMY_SPEED = 1.0;
const BULLET_SPEED = 3.5;
const ENEMY_BULLET_SPEED = 2.8;
const ENEMY_SHOOT_INTERVAL = 90;
const SPAWN_INTERVAL = 180;
const MAX_ENEMIES_ON_FIELD = 4;
 
const TILE_EMPTY = 0;
const TILE_BRICK = 1;
const TILE_STEEL = 2;
const TILE_WATER = 3;
const TILE_TREE = 4;
const TILE_BASE = 5;
const TILE_BASE_DEAD = 6;
 
const COLORS = {
  bg: '#000',
  brick: '#b35900',
  brickLight: '#cc7a33',
  steel: '#aaa',
  steelLight: '#ccc',
  water: '#2244aa',
  waterLight: '#3366cc',
  tree: '#226622',
  treeLight: '#33aa33',
  base: '#ff0',
  baseDead: '#666',
  playerBody: '#ff0',
  playerTurret: '#cc0',
  playerTrack: '#aa0',
  enemyBody: '#e44',
  enemyTurret: '#c22',
  enemyTrack: '#a22',
  enemyFastBody: '#e8e',
  enemyFastTurret: '#c6c',
  enemyArmorBody: '#4ae',
  enemyArmorTurret: '#28c',
  bullet: '#fff',
  explosion: ['#ff0', '#fa0', '#f60', '#f00', '#800']
};
 
// ==================== LEVELS ====================
const LEVELS = [
  // Level 1
  [
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011002200110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00110011001100110011001100",
    "00000000001100000000000000",
    "00000000001100000000000000",
    "11001100000000001100110000",
    "22001100000000001100002200",
    "00000000001100000000000000",
    "00000000001100000000000000",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00000000001111000000000000",
    "00000000001551000000000000",
    "00000000001551000000000000",
  ],
  // Level 2
  [
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00110022001100110022001100",
    "00110022001100110022001100",
    "00110000001100110000001100",
    "00110000001100110000001100",
    "00000011220000221100000000",
    "00000011220000221100000000",
    "00220000001100000000220000",
    "00220000001100000000220000",
    "00001100110000110011000000",
    "00001100110000110011000000",
    "11000022000000002200001100",
    "11000022000000002200001100",
    "00001100110000110011000000",
    "00001100110000110011000000",
    "00220000001100000000220000",
    "00220000001100000000220000",
    "00110011001100110011001100",
    "00110011001100110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000000000000000000000000",
    "00000000001111000000000000",
    "00000000001551000000000000",
    "00000000001551000000000000",
  ],
  // Level 3
  [
    "00000000000000000000000000",
    "00000000000000000000000000",
    "00110011003300110011001100",
    "00110011003300110011001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000022001100220000001100",
    "00000022001100220000001100",
    "22110000002200000011002200",
    "22110000002200000011002200",
    "00001133000000331100000000",
    "00001133000000331100000000",
    "00110000221100220000110000",
    "00110000221100220000110000",
    "00001133000000331100000000",
    "00001133000000331100000000",
    "22110000002200000011002200",
    "22110000002200000011002200",
    "00000022001100220000001100",
    "00000022001100220000001100",
    "00110011000000110011001100",
    "00110011000000110011001100",
    "00000000000000000000000000",
    "00000000001111000000000000",
    "00000000001551000000000000",
    "00000000001551000000000000",
  ],
];
 
// ==================== GAME STATE ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
 
let gameState = 'start'; // start, playing, paused, gameover, win
let level = 0;
let score = 0;
let lives = 3;
let map = [];
let player = null;
let enemies = [];
let bullets = [];
let explosions = [];
let powerUps = [];
let enemiesRemaining = 0;
let enemiesSpawned = 0;
let totalEnemies = 20;
let spawnTimer = 0;
let frameCount = 0;
let baseAlive = true;
let spawnPoints = [];
let keys = {};
let playerInvincible = 0;
let freezeTimer = 0;
 
// ==================== INPUT ====================
function handleKeyDown(e) {
  keys[e.code] = true;
  // Also map to key for compatibility
  if (e.key) keys[e.key] = true;
   
  if (e.key === 'p' || e.key === 'P') {
    if (gameState === 'playing') {
      gameState = 'paused';
      document.getElementById('pauseOverlay').classList.remove('hidden');
    } else if (gameState === 'paused') {
      gameState = 'playing';
      document.getElementById('pauseOverlay').classList.add('hidden');
    }
  }
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) {
    e.preventDefault();
  }
}
 
function handleKeyUp(e) {
  keys[e.code] = false;
  if (e.key) keys[e.key] = false;
}
 
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
 
// Ensure focus for keyboard input in iframe environments
window.addEventListener('load', () => { document.body.focus(); });
document.addEventListener('click', () => { document.body.focus(); });
document.addEventListener('touchstart', () => { document.body.focus(); });
 
// ==================== MOBILE CONTROLS ====================
const mobileControls = document.getElementById('mobileControls');
const dpadBtns = document.querySelectorAll('.dpad-btn');
const fireBtn = document.getElementById('fireBtn');
 
// Detect touch device and show controls
function checkTouchDevice() {
  if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
    mobileControls.classList.add('active');
    document.getElementById('desktopHint').style.display = 'none';
    document.getElementById('mobileHint').style.display = 'block';
  }
}
checkTouchDevice();
 
// D-pad touch handling
const dirMap = { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
const codeMap = { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
 
dpadBtns.forEach(btn => {
  const dir = btn.dataset.dir;
   
  btn.addEventListener('touchstart', (e) => {
    e.preventDefault();
    btn.classList.add('pressed');
    keys[dirMap[dir]] = true;
    keys[codeMap[dir]] = true;
  });
   
  btn.addEventListener('touchend', (e) => {
    e.preventDefault();
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
   
  btn.addEventListener('touchcancel', (e) => {
    e.preventDefault();
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
   
  // Mouse fallback for testing
  btn.addEventListener('mousedown', (e) => {
    e.preventDefault();
    btn.classList.add('pressed');
    keys[dirMap[dir]] = true;
    keys[codeMap[dir]] = true;
  });
   
  btn.addEventListener('mouseup', (e) => {
    e.preventDefault();
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
   
  btn.addEventListener('mouseleave', (e) => {
    btn.classList.remove('pressed');
    keys[dirMap[dir]] = false;
    keys[codeMap[dir]] = false;
  });
});
 
// Fire button touch handling
fireBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  fireBtn.classList.add('pressed');
  keys[' '] = true;
  keys['Space'] = true;
});
 
fireBtn.addEventListener('touchend', (e) => {
  e.preventDefault();
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});
 
fireBtn.addEventListener('touchcancel', (e) => {
  e.preventDefault();
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});
 
// Mouse fallback for fire button
fireBtn.addEventListener('mousedown', (e) => {
  e.preventDefault();
  fireBtn.classList.add('pressed');
  keys[' '] = true;
  keys['Space'] = true;
});
 
fireBtn.addEventListener('mouseup', (e) => {
  e.preventDefault();
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});
 
fireBtn.addEventListener('mouseleave', (e) => {
  fireBtn.classList.remove('pressed');
  keys[' '] = false;
  keys['Space'] = false;
});
 
// Pause button
const pauseBtn = document.getElementById('pauseBtn');
function togglePause() {
  if (gameState === 'playing') {
    gameState = 'paused';
    document.getElementById('pauseOverlay').classList.remove('hidden');
  } else if (gameState === 'paused') {
    gameState = 'playing';
    document.getElementById('pauseOverlay').classList.add('hidden');
  }
}
 
pauseBtn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  pauseBtn.classList.add('pressed');
  togglePause();
});
pauseBtn.addEventListener('touchend', (e) => {
  e.preventDefault();
  pauseBtn.classList.remove('pressed');
});
pauseBtn.addEventListener('click', (e) => {
  e.preventDefault();
  togglePause();
});
 
// ==================== MAP ====================
function loadMap(lvl) {
  const data = LEVELS[lvl % LEVELS.length];
  map = [];
  for (let r = 0; r < ROWS; r++) {
    map[r] = [];
    for (let c = 0; c < COLS; c++) {
      map[r][c] = parseInt(data[r][c]);
    }
  }
}
 
function drawTile(r, c) {
  const x = c * TILE;
  const y = r * TILE;
  const t = map[r][c];
 
  if (t === TILE_BRICK) {
    ctx.fillStyle = COLORS.brick;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = COLORS.brickLight;
    ctx.fillRect(x, y, TILE/2, TILE/2);
    ctx.fillRect(x + TILE/2, y + TILE/2, TILE/2, TILE/2);
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 0.5;
    ctx.strokeRect(x, y, TILE, TILE);
  } else if (t === TILE_STEEL) {
    ctx.fillStyle = COLORS.steel;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = COLORS.steelLight;
    ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
    ctx.fillStyle = COLORS.steel;
    ctx.fillRect(x + 5, y + 5, TILE - 10, TILE - 10);
  } else if (t === TILE_WATER) {
    const wave = Math.sin(frameCount * 0.05 + c + r) * 0.3 + 0.5;
    ctx.fillStyle = wave > 0.5 ? COLORS.water : COLORS.waterLight;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = wave > 0.5 ? COLORS.waterLight : COLORS.water;
    for (let i = 0; i < 3; i++) {
      const wy = y + 4 + i * 6 + Math.sin(frameCount * 0.08 + c * 0.5) * 2;
      ctx.fillRect(x, wy, TILE, 2);
    }
  } else if (t === TILE_BASE) {
    ctx.fillStyle = COLORS.base;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = '#000';
    ctx.fillRect(x + 4, y + 4, TILE - 8, TILE - 8);
    ctx.fillStyle = COLORS.base;
    ctx.fillRect(x + 7, y + 7, TILE - 14, TILE - 14);
  } else if (t === TILE_BASE_DEAD) {
    ctx.fillStyle = COLORS.baseDead;
    ctx.fillRect(x, y, TILE, TILE);
    ctx.fillStyle = '#444';
    ctx.fillRect(x + 3, y + 3, TILE - 6, TILE - 6);
  }
}
 
function drawTree(r, c) {
  if (map[r][c] !== TILE_TREE) return;
  const x = c * TILE;
  const y = r * TILE;
  ctx.fillStyle = COLORS.tree;
  ctx.fillRect(x, y, TILE, TILE);
  ctx.fillStyle = COLORS.treeLight;
  ctx.fillRect(x + 2, y + 2, 6, 6);
  ctx.fillRect(x + 10, y + 8, 6, 6);
  ctx.fillRect(x + 4, y + 12, 6, 6);
}
 
// ==================== TANK ====================
class Tank {
  constructor(x, y, dir, isPlayer, type) {
    this.x = x;
    this.y = y;
    this.dir = dir;
    this.isPlayer = isPlayer;
    this.type = type || 'normal'; // normal, fast, armor
    this.speed = isPlayer ? PLAYER_SPEED : (type === 'fast' ? ENEMY_SPEED * 1.6 : ENEMY_SPEED);
    this.hp = isPlayer ? 1 : (type === 'armor' ? 3 : 1);
    this.maxHp = this.hp;
    this.shootCooldown = 0;
    this.alive = true;
    this.moveTimer = 0;
    this.aiDir = dir;
    this.aiDirTimer = 0;
    this.flashTimer = 0;
    this.spawnAnim = 30;
    this.size = TANK_SIZE;
  }
 
  get cx() { return this.x + this.size / 2; }
  get cy() { return this.y + this.size / 2; }
 
  update() {
    if (this.spawnAnim > 0) { this.spawnAnim--; return; }
    if (this.shootCooldown > 0) this.shootCooldown--;
    if (this.flashTimer > 0) this.flashTimer--;
 
    if (this.isPlayer) {
      this.updatePlayer();
    } else {
      if (freezeTimer <= 0) this.updateAI();
    }
  }
 
  updatePlayer() {
    let moving = false;
    let newDir = this.dir;
 
    if (keys['ArrowUp'] || keys['KeyW'] || keys['w'] || keys['W']) { newDir = DIR.UP; moving = true; }
    else if (keys['ArrowDown'] || keys['KeyS'] || keys['s'] || keys['S']) { newDir = DIR.DOWN; moving = true; }
    else if (keys['ArrowLeft'] || keys['KeyA'] || keys['a'] || keys['A']) { newDir = DIR.LEFT; moving = true; }
    else if (keys['ArrowRight'] || keys['KeyD'] || keys['d'] || keys['D']) { newDir = DIR.RIGHT; moving = true; }
 
    this.dir = newDir;
    if (moving) this.move(this.dir);
 
    if (keys[' '] || keys['Space']) this.shoot();
  }
 
  updateAI() {
    this.aiDirTimer--;
    if (this.aiDirTimer <= 0) {
      // Change direction periodically or when blocked
      const dirs = [DIR.UP, DIR.RIGHT, DIR.DOWN, DIR.LEFT];
      // Bias towards player direction
      if (player && player.alive && Math.random() < 0.3) {
        const dx = player.cx - this.cx;
        const dy = player.cy - this.cy;
        if (Math.abs(dx) > Math.abs(dy)) {
          this.aiDir = dx > 0 ? DIR.RIGHT : DIR.LEFT;
        } else {
          this.aiDir = dy > 0 ? DIR.DOWN : DIR.UP;
        }
      } else {
        this.aiDir = dirs[Math.floor(Math.random() * 4)];
      }
      this.aiDirTimer = 60 + Math.floor(Math.random() * 120);
    }
 
    this.dir = this.aiDir;
    const moved = this.move(this.dir);
    if (!moved) {
      this.aiDirTimer = 0; // change direction next frame
    }
 
    // Shooting
    if (Math.random() < 0.02 || (this.shootCooldown <= 0 && Math.random() < 0.05)) {
      this.shoot();
    }
  }
 
  move(dir) {
    const nx = this.x + DX[dir] * this.speed;
    const ny = this.y + DY[dir] * this.speed;
 
    // Boundary check
    if (nx < 0 || ny < 0 || nx + this.size > W || ny + this.size > H) return false;
 
    // Tile collision
    if (this.checkTileCollision(nx, ny)) return false;
 
    // Tank-tank collision
    if (this.checkTankCollision(nx, ny)) return false;
 
    this.x = nx;
    this.y = ny;
    return true;
  }
 
  checkTileCollision(nx, ny) {
    const margin = 1;
    const left = Math.floor((nx + margin) / TILE);
    const top = Math.floor((ny + margin) / TILE);
    const right = Math.floor((nx + this.size - 1 - margin) / TILE);
    const bottom = Math.floor((ny + this.size - 1 - margin) / TILE);
 
    for (let r = top; r <= bottom; r++) {
      for (let c = left; c <= right; c++) {
        if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return true;
        const t = map[r][c];
        if (t === TILE_BRICK || t === TILE_STEEL || t === TILE_WATER || t === TILE_BASE || t === TILE_BASE_DEAD) {
          return true;
        }
      }
    }
    return false;
  }
 
  checkTankCollision(nx, ny) {
    const allTanks = [player, ...enemies].filter(t => t && t.alive && t !== this && t.spawnAnim <= 0);
    for (const other of allTanks) {
      if (nx < other.x + other.size && nx + this.size > other.x &&
          ny < other.y + other.size && ny + this.size > other.y) {
        return true;
      }
    }
    return false;
  }
 
  shoot() {
    if (this.shootCooldown > 0) return;
    // Limit bullets per tank
    const myBullets = bullets.filter(b => b.owner === this);
    if (this.isPlayer && myBullets.length >= 2) return;
    if (!this.isPlayer && myBullets.length >= 1) return;
 
    const bx = this.cx - BULLET_SIZE / 2 + DX[this.dir] * (this.size / 2);
    const by = this.cy - BULLET_SIZE / 2 + DY[this.dir] * (this.size / 2);
    const speed = this.isPlayer ? BULLET_SPEED : ENEMY_BULLET_SPEED;
 
    bullets.push(new Bullet(bx, by, this.dir, this.isPlayer, this, speed));
    this.shootCooldown = this.isPlayer ? 15 : ENEMY_SHOOT_INTERVAL;
  }
 
  draw() {
    if (!this.alive) return;
 
    // Spawn animation
    if (this.spawnAnim > 0) {
      const flash = this.spawnAnim % 8 < 4;
      ctx.strokeStyle = flash ? '#fff' : '#ff0';
      ctx.lineWidth = 2;
      const s = this.size * (1 - this.spawnAnim / 30) * 0.8;
      ctx.strokeRect(this.cx - s/2, this.cy - s/2, s, s);
      return;
    }
 
    ctx.save();
    ctx.translate(this.cx, this.cy);
    ctx.rotate(this.dir * Math.PI / 2);
 
    const s = this.size;
    const hs = s / 2;
 
    // Flash when hit
    if (this.flashTimer > 0 && this.flashTimer % 4 < 2) {
      ctx.globalAlpha = 0.5;
    }
 
    // Body colors
    let bodyColor, turretColor, trackColor;
    if (this.isPlayer) {
      bodyColor = COLORS.playerBody;
      turretColor = COLORS.playerTurret;
      trackColor = COLORS.playerTrack;
    } else if (this.type === 'fast') {
      bodyColor = COLORS.enemyFastBody;
      turretColor = COLORS.enemyFastTurret;
      trackColor = COLORS.enemyFastBody;
    } else if (this.type === 'armor') {
      bodyColor = this.hp > 1 ? COLORS.enemyArmorBody : COLORS.enemyBody;
      turretColor = this.hp > 1 ? COLORS.enemyArmorTurret : COLORS.enemyTurret;
      trackColor = COLORS.enemyArmorBody;
    } else {
      bodyColor = COLORS.enemyBody;
      turretColor = COLORS.enemyTurret;
      trackColor = COLORS.enemyTrack;
    }
 
    // Tracks
    ctx.fillStyle = trackColor;
    ctx.fillRect(-hs, -hs, s * 0.22, s);
    ctx.fillRect(hs - s * 0.22, -hs, s * 0.22, s);
    // Track details
    ctx.fillStyle = '#000';
    for (let i = 0; i < 5; i++) {
      const ty = -hs + 3 + i * (s / 5);
      ctx.fillRect(-hs + 1, ty, s * 0.2, 2);
      ctx.fillRect(hs - s * 0.22 + 1, ty, s * 0.2, 2);
    }
 
    // Body
    ctx.fillStyle = bodyColor;
    ctx.fillRect(-hs + s * 0.22, -hs + 2, s * 0.56, s - 4);
 
    // Turret base
    ctx.fillStyle = turretColor;
    ctx.fillRect(-s * 0.18, -s * 0.18, s * 0.36, s * 0.36);
 
    // Barrel
    ctx.fillStyle = turretColor;
    ctx.fillRect(-2, -hs - 2, 4, hs);
 
    // Invincibility shield
    if (this.isPlayer && playerInvincible > 0) {
      ctx.strokeStyle = playerInvincible % 6 < 3 ? '#0ff' : '#fff';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(0, 0, hs + 2, 0, Math.PI * 2);
      ctx.stroke();
    }
 
    ctx.restore();
  }
 
  hit() {
    this.hp--;
    this.flashTimer = 15;
    if (this.hp <= 0) {
      this.alive = false;
      addExplosion(this.cx, this.cy, true);
      if (!this.isPlayer) {
        score += this.type === 'armor' ? 300 : this.type === 'fast' ? 200 : 100;
        updateUI();
      }
    }
  }
}
 
// ==================== BULLET ====================
class Bullet {
  constructor(x, y, dir, isPlayerBullet, owner, speed) {
    this.x = x;
    this.y = y;
    this.dir = dir;
    this.isPlayerBullet = isPlayerBullet;
    this.owner = owner;
    this.speed = speed;
    this.alive = true;
    this.size = BULLET_SIZE;
  }
 
  update() {
    this.x += DX[this.dir] * this.speed;
    this.y += DY[this.dir] * this.speed;
 
    // Boundary
    if (this.x < 0 || this.y < 0 || this.x > W || this.y > H) {
      this.alive = false;
      addExplosion(this.x, this.y, false);
      return;
    }
 
    // Tile collision
    this.checkTileHit();
    if (!this.alive) return;
 
    // Tank collision
    this.checkTankHit();
  }
 
  checkTileHit() {
    const cx = this.x + this.size / 2;
    const cy = this.y + this.size / 2;
    const c = Math.floor(cx / TILE);
    const r = Math.floor(cy / TILE);
 
    // Check surrounding tiles
    for (let dr = -1; dr <= 1; dr++) {
      for (let dc = -1; dc <= 1; dc++) {
        const tr = r + dr;
        const tc = c + dc;
        if (tr < 0 || tr >= ROWS || tc < 0 || tc >= COLS) continue;
 
        const tx = tc * TILE;
        const ty = tr * TILE;
        if (this.x + this.size <= tx || this.x >= tx + TILE ||
            this.y + this.size <= ty || this.y >= ty + TILE) continue;
 
        const t = map[tr][tc];
        if (t === TILE_BRICK) {
          map[tr][tc] = TILE_EMPTY;
          this.alive = false;
          addExplosion(cx, cy, false);
          return;
        } else if (t === TILE_STEEL) {
          if (this.isPlayerBullet) {
            // Player bullets can't destroy steel (unless power-up)
          }
          this.alive = false;
          addExplosion(cx, cy, false);
          return;
        } else if (t === TILE_BASE) {
          map[tr][tc] = TILE_BASE_DEAD;
          baseAlive = false;
          this.alive = false;
          addExplosion(tx + TILE/2, ty + TILE/2, true);
          return;
        }
      }
    }
  }
 
  checkTankHit() {
    if (this.isPlayerBullet) {
      for (const enemy of enemies) {
        if (!enemy.alive || enemy.spawnAnim > 0) continue;
        if (this.x < enemy.x + enemy.size && this.x + this.size > enemy.x &&
            this.y < enemy.y + enemy.size && this.y + this.size > enemy.y) {
          this.alive = false;
          enemy.hit();
          return;
        }
      }
    } else {
      if (player && player.alive && player.spawnAnim <= 0) {
        if (this.x < player.x + player.size && this.x + this.size > player.x &&
            this.y < player.y + player.size && this.y + this.size > player.y) {
          this.alive = false;
          if (playerInvincible <= 0) {
            player.hit();
            if (!player.alive) {
              lives--;
              updateUI();
              if (lives > 0) {
                setTimeout(() => respawnPlayer(), 1500);
              }
            }
          }
          return;
        }
      }
    }
 
    // Bullet-bullet collision
    for (const other of bullets) {
      if (other === this || !other.alive) continue;
      if (this.isPlayerBullet !== other.isPlayerBullet) {
        if (this.x < other.x + other.size && this.x + this.size > other.x &&
            this.y < other.y + other.size && this.y + this.size > other.y) {
          this.alive = false;
          other.alive = false;
          return;
        }
      }
    }
  }
 
  draw() {
    if (!this.alive) return;
    ctx.fillStyle = COLORS.bullet;
    ctx.shadowColor = '#ff0';
    ctx.shadowBlur = 4;
    ctx.fillRect(this.x, this.y, this.size, this.size);
    ctx.shadowBlur = 0;
  }
}
 
// ==================== EXPLOSION ====================
class Explosion {
  constructor(x, y, big) {
    this.x = x;
    this.y = y;
    this.big = big;
    this.frame = 0;
    this.maxFrame = big ? 20 : 12;
    this.alive = true;
  }

  update() {
    this.frame++;
    if (this.frame >= this.maxFrame) this.alive = false;
  }

  draw() {
    if (!this.alive) return;
    const progress = this.frame / this.maxFrame;
    const radius = this.big ? 15 * (1 - progress) : 10 * (1 - progress);
    const colorIdx = Math.floor(progress * COLORS.explosion.length);
    ctx.fillStyle = COLORS.explosion[colorIdx] || COLORS.explosion[COLORS.explosion.length - 1];
    ctx.beginPath();
    ctx.arc(this.x, this.y, radius, 0, Math.PI * 2);
    ctx.fill();
  }
}

// ==================== GAME FUNCTIONS ====================
function addExplosion(x, y, big) {
  explosions.push(new Explosion(x, y, big));
}

function respawnPlayer() {
  if (lives <= 0) return;
  player = new Tank(12 * TILE, 24 * TILE, DIR.UP, true);
  playerInvincible = 120;
}

function updateUI() {
  document.getElementById('levelDisplay').textContent = level + 1;
  document.getElementById('scoreDisplay').textContent = score;
  document.getElementById('livesDisplay').textContent = lives;
  
  const enemyIcons = document.getElementById('enemyIcons');
  enemyIcons.innerHTML = '';
  for (let i = 0; i < totalEnemies; i++) {
    const icon = document.createElement('div');
    icon.className = 'enemy-icon';
    if (i < enemiesSpawned) icon.classList.add('dead');
    enemyIcons.appendChild(icon);
  }
  
  document.getElementById('finalScore').textContent = score;
  document.getElementById('winScore').textContent = score;
}

function initLevel() {
  loadMap(level);
  enemies = [];
  bullets = [];
  explosions = [];
  powerUps = [];
  enemiesRemaining = totalEnemies;
  enemiesSpawned = 0;
  spawnTimer = 0;
  baseAlive = true;
  spawnPoints = [
    { x: 0, y: 0 },
    { x: 12 * TILE, y: 0 },
    { x: 24 * TILE, y: 0 }
  ];
  
  // Reset base tiles
  for (let r = ROWS - 2; r < ROWS; r++) {
    for (let c = 11; c <= 14; c++) {
      if (map[r][c] === TILE_BASE_DEAD) map[r][c] = TILE_BASE;
    }
  }
  
  // Reset player
  if (player && player.alive) {
    player.x = 12 * TILE;
    player.y = 24 * TILE;
    player.dir = DIR.UP;
    player.spawnAnim = 30;
  } else {
    player = new Tank(12 * TILE, 24 * TILE, DIR.UP, true);
  }
  
  playerInvincible = 60;
  updateUI();
}

function spawnEnemy() {
  if (enemies.length >= MAX_ENEMIES_ON_FIELD || enemiesSpawned >= totalEnemies) return;
  
  const spawnPoint = spawnPoints[Math.floor(Math.random() * spawnPoints.length)];
  const types = ['normal', 'fast', 'armor'];
  const type = Math.random() < 0.3 ? 'armor' : (Math.random() < 0.4 ? 'fast' : 'normal');
  const enemy = new Tank(spawnPoint.x, spawnPoint.y, DIR.DOWN, false, type);
  enemies.push(enemy);
  enemiesSpawned++;
  updateUI();
}

function checkWinCondition() {
  if (enemiesRemaining <= 0) {
    gameState = 'win';
    document.getElementById('winScreen').classList.remove('hidden');
    document.getElementById('winScore').textContent = score;
  }
}

function startGame() {
  level = 0;
  score = 0;
  lives = 3;
  initLevel();
  gameState = 'playing';
  document.getElementById('startScreen').classList.add('hidden');
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('winScreen').classList.add('hidden');
}

function restartGame() {
  gameState = 'start';
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('winScreen').classList.add('hidden');
  document.getElementById('startScreen').classList.remove('hidden');
}

function nextLevel() {
  level++;
  document.getElementById('winScreen').classList.add('hidden');
  initLevel();
  gameState = 'playing';
}

// ==================== GAME LOOP ====================
function gameLoop() {
  frameCount++;
  
  if (gameState === 'playing') {
    // Update timers
    if (playerInvincible > 0) playerInvincible--;
    if (freezeTimer > 0) freezeTimer--;
    
    // Spawn enemies
    spawnTimer++;
    if (spawnTimer >= SPAWN_INTERVAL && enemiesSpawned < totalEnemies) {
      spawnTimer = 0;
      spawnEnemy();
    }
    
    // Update player
    if (player && player.alive) {
      player.update();
    }
    
    // Update enemies
    enemies = enemies.filter(enemy => enemy.alive);
    enemies.forEach(enemy => enemy.update());
    
    // Update bullets
    bullets = bullets.filter(bullet => bullet.alive);
    bullets.forEach(bullet => bullet.update());
    
    // Update explosions
    explosions = explosions.filter(explosion => explosion.alive);
    explosions.forEach(explosion => explosion.update());
    
    // Update enemy remaining count
    enemiesRemaining = totalEnemies - enemiesSpawned + enemies.length;
    
    // Check win/lose conditions
    if (enemiesRemaining <= 0) {
      checkWinCondition();
    }
    if (lives <= 0 || !baseAlive) {
      gameState = 'gameover';
      document.getElementById('gameOverScreen').classList.remove('hidden');
      document.getElementById('finalScore').textContent = score;
    }
  }
  
  // Draw everything
  ctx.clearRect(0, 0, W, H);
  
  // Draw map
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      drawTile(r, c);
    }
  }
  
  // Draw trees (above everything)
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      if (map[r][c] === TILE_TREE) {
        drawTree(r, c);
      }
    }
  }
  
  // Draw tanks
  enemies.forEach(enemy => enemy.draw());
  if (player && player.alive) player.draw();
  
  // Draw bullets
  bullets.forEach(bullet => bullet.draw());
  
  // Draw explosions
  explosions.forEach(explosion => explosion.draw());
  
  // Update UI
  updateUI();
  
  requestAnimationFrame(gameLoop);
}

// ==================== INITIALIZATION ====================
function init() {
  // Set canvas size
  canvas.width = W;
  canvas.height = H;
  
  // Add button event listeners
  const startBtn = document.querySelector('#startScreen .btn');
  const restartBtn = document.querySelector('#gameOverScreen .btn');
  const nextLevelBtn = document.querySelector('#winScreen .btn');
  
  startBtn.addEventListener('click', startGame);
  restartBtn.addEventListener('click', startGame);
  nextLevelBtn.addEventListener('nextLevel', nextLevel);
  
  // Start game loop
  requestAnimationFrame(gameLoop);
}

// Start the game when page loads
window.addEventListener('load', init);
 
// ==================== POWER-UP ====================
class PowerUp {
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type; // 'star', 'shield', 'bomb', 'life'
    this.alive = true;
    this.timer = 600; // 10 seconds
    this.size = TILE * 2;
  }
 
  update() {
    this.timer--;
    if (this.timer <= 0) this.alive = false;
 
    if (player && player.alive) {
      if (this.x < player.x + player.size && this.x + this.size > player.x &&
          this.y < player.y + player.size && this.y + this.size > player.y) {
        this.alive = false;
        this.apply();
      }
    }
  }
 
  apply() {
    switch (this.type) {
      case 'shield':
        playerInvincible = 300;
        break;
      case 'bomb':
        enemies.forEach(e => {
          if (e.alive) {
            e.alive = false;
            addExplosion(e.cx, e.cy, true);
            score += 50;
          }
        });
        break;
      case 'life':
        lives++;
        break;
      case 'star':
        player.speed = PLAYER_SPEED * 1.3;
        break;
    }
    score += 500;
    updateUI();
  }
 
  draw() {
    if (!this.alive) return;
    if (this.timer % 20 < 10 && this.timer < 120) return; // blink when about to expire
 
    const x = this.x;
    const y = this.y;
    const s = this.size;
 
    ctx.fillStyle = '#222';
    ctx.fillRect(x, y, s, s);
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 1;
    ctx.strokeRect(x, y, s, s);
 
    ctx.font = '16px monospace';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
 
    switch (this.type) {
      case 'shield':
        ctx.fillStyle = '#0ff';
        ctx.fillText('S', x + s/2, y + s/2);
        break;
      case 'bomb':
        ctx.fillStyle = '#f44';
        ctx.fillText('B', x + s/2, y + s/2);
        break;
      case 'life':
        ctx.fillStyle = '#4f4';
        ctx.fillText('+', x + s/2, y + s/2);
        break;
      case 'star':
        ctx.fillStyle = '#ff0';
        ctx.fillText('*', x + s/2, y + s/2);
        break;
    }
  }
}
 
// ==================== GAME LOGIC ====================
function spawnEnemy() {
  if (enemiesSpawned >= totalEnemies) return;
  if (enemies.filter(e => e.alive).length >= MAX_ENEMIES_ON_FIELD) return;
 
  const sp = spawnPoints[enemiesSpawned % spawnPoints.length];
  // Check if spawn point is clear
  const allTanks = [player, ...enemies].filter(t => t && t.alive);
  for (const t of allTanks) {
    if (sp.x < t.x + t.size && sp.x + TANK_SIZE > t.x &&
        sp.y < t.y + t.size && sp.y + TANK_SIZE > t.y) {
      return; // spawn point blocked
    }
  }
 
  const types = ['normal', 'normal', 'fast', 'armor'];
  const typeIdx = Math.min(Math.floor(enemiesSpawned / 5), types.length - 1);
  const type = types[Math.floor(Math.random() * (typeIdx + 1))];
 
  const enemy = new Tank(sp.x, sp.y, DIR.DOWN, false, type);
  enemies.push(enemy);
  enemiesSpawned++;
  updateUI();
}
 
function respawnPlayer() {
  if (gameState !== 'playing') return;
  player = new Tank(4 * TILE, 24 * TILE, DIR.UP, true);
  playerInvincible = 120;
}
 
function startGame() {
  document.getElementById('startScreen').classList.add('hidden');
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('winScreen').classList.add('hidden');
 
  level = 0;
  score = 0;
  lives = 3;
  initLevel();
  gameState = 'playing';
  document.body.focus();
}
 
function nextLevel() {
  document.getElementById('winScreen').classList.add('hidden');
  level++;
  initLevel();
  gameState = 'playing';
  document.body.focus();
}
 
function initLevel() {
  loadMap(level);
  bullets = [];
  explosions = [];
  powerUps = [];
  enemies = [];
  enemiesSpawned = 0;
  totalEnemies = 20;
  enemiesRemaining = totalEnemies;
  spawnTimer = 0;
  baseAlive = true;
  playerInvincible = 120;
  freezeTimer = 0;
 
  spawnPoints = [
    { x: 0, y: 0 },
    { x: 12 * TILE, y: 0 },
    { x: 24 * TILE, y: 0 }
  ];
 
  player = new Tank(4 * TILE, 24 * TILE, DIR.UP, true);
  updateUI();
}
 
function updateUI() {
  document.getElementById('levelDisplay').textContent = level + 1;
  document.getElementById('scoreDisplay').textContent = score;
  document.getElementById('livesDisplay').textContent = lives;
 
  const remaining = totalEnemies - enemiesSpawned + enemies.filter(e => e.alive).length;
  const icons = document.getElementById('enemyIcons');
  icons.innerHTML = '';
  for (let i = 0; i < totalEnemies; i++) {
    const div = document.createElement('div');
    div.className = 'enemy-icon' + (i >= remaining ? ' dead' : '');
    icons.appendChild(div);
  }
}
 
function checkWinLose() {
  if (!baseAlive) {
    gameState = 'gameover';
    document.getElementById('finalScore').textContent = score;
    document.getElementById('gameOverScreen').classList.remove('hidden');
    return;
  }
 
  if (lives <= 0 && (!player || !player.alive)) {
    gameState = 'gameover';
    document.getElementById('finalScore').textContent = score;
    document.getElementById('gameOverScreen').classList.remove('hidden');
    return;
  }
 
  const allDead = enemies.every(e => !e.alive);
  if (enemiesSpawned >= totalEnemies && allDead) {
    gameState = 'win';
    document.getElementById('winScore').textContent = score;
    document.getElementById('winScreen').classList.remove('hidden');
  }
}
 
// ==================== MAIN LOOP ====================
function update() {
  if (gameState !== 'playing') return;
 
  frameCount++;
 
  // Spawn enemies
  spawnTimer++;
  if (spawnTimer >= SPAWN_INTERVAL) {
    spawnTimer = 0;
    spawnEnemy();
  }
 
  // Update timers
  if (playerInvincible > 0) playerInvincible--;
  if (freezeTimer > 0) freezeTimer--;
 
  // Update player
  if (player && player.alive) player.update();
 
  // Update enemies
  enemies.forEach(e => { if (e.alive) e.update(); });
 
  // Update bullets
  bullets.forEach(b => { if (b.alive) b.update(); });
  bullets = bullets.filter(b => b.alive);
 
  // Update explosions
  explosions.forEach(e => e.update());
  explosions = explosions.filter(e => e.alive);
 
  // Update power-ups
  powerUps.forEach(p => { if (p.alive) p.update(); });
  powerUps = powerUps.filter(p => p.alive);
 
  // Random power-up drop
  if (frameCount % 600 === 0 && Math.random() < 0.5) {
    const types = ['shield', 'bomb', 'life', 'star'];
    const type = types[Math.floor(Math.random() * types.length)];
    const px = Math.floor(Math.random() * (COLS - 2)) * TILE;
    const py = (4 + Math.floor(Math.random() * 16)) * TILE;
    powerUps.push(new PowerUp(px, py, type));
  }
 
  checkWinLose();
}
 
function draw() {
  // Background
  ctx.fillStyle = COLORS.bg;
  ctx.fillRect(0, 0, W, H);
 
  // Don't draw map if not initialized
  if (!map || map.length === 0) return;
 
  // Draw map (bottom layer - no trees)
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      if (map[r][c] !== TILE_EMPTY && map[r][c] !== TILE_TREE) {
        drawTile(r, c);
      }
    }
  }
 
  // Draw power-ups
  powerUps.forEach(p => p.draw());
 
  // Draw tanks
  if (player && player.alive) player.draw();
  enemies.forEach(e => { if (e.alive) e.draw(); });
 
  // Draw bullets
  bullets.forEach(b => { if (b.alive) b.draw(); });
 
  // Draw trees (top layer - covers tanks)
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      drawTree(r, c);
    }
  }
 
  // Draw explosions (topmost)
  explosions.forEach(e => e.draw());
 
  // Debug: show active keys
  const activeKeys = Object.entries(keys).filter(([k, v]) => v).map(([k]) => k);
  if (activeKeys.length > 0) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(2, H - 22, 200, 20);
    ctx.fillStyle = '#0f0';
    ctx.font = '10px monospace';
    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.fillText('Keys: ' + activeKeys.join(', '), 6, H - 12);
  }
}
 
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}
 
// Start the loop
gameLoop();
</script>
</body>
</html>

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
luogx + 1 + 1 我很赞同!
xuet + 1 + 1 我很赞同!

查看全部评分

推荐
Lcp1027 发表于 2026-7-3 16:07
根据对您提供的 HTML 文件进行解析,发现游戏无法在本地正常启动的核心原因是:开始游戏、重新开始、下一关等按钮均未绑定点击事件。您点击“开始游戏”按钮时,页面不会执行任何操作,因为 #startScreen 内的按钮没有 onclick 属性,JavaScript 中也没有通过 addEventListener 为其绑定 startGame 函数。
&#129513; 问题定位在您的代码中,以下三个关键按钮缺少事件监听器:
按钮位置
应触发的函数

开始画面 “开始游戏”startGame()
游戏结束 “再来一局”startGame() 或 resetGame()
胜利画面 “下一关”nextLevel()
虽然 startGame、nextLevel 等函数已定义,但没有任何地方调用它们,因此游戏永远停留在初始画面。
&#128736;&#65039; 修复方法在 JavaScript 代码的末尾(gameLoop() 调用之前或之后均可)添加以下事件绑定代码:
javascript// ==================== 绑定按钮事件 ====================document.querySelector('#startScreen .btn').addEventListener('click', startGame);document.querySelector('#gameOverScreen .btn').addEventListener('click', startGame);document.querySelector('#winScreen .btn').addEventListener('click', nextLevel);

&#9989; 完整修改示例(在现有代码末尾追加)找到您的 </script> 标签之前,在 gameLoop() 调用之后(或之前)添加:
javascript// 按钮事件绑定document.querySelector('#startScreen .btn').addEventListener('click', startGame);document.querySelector('#gameOverScreen .btn').addEventListener('click', startGame);document.querySelector('#winScreen .btn').addEventListener('click', nextLevel);

沙发
Mzhang2008 发表于 2026-7-3 15:12
3#
SriChen 发表于 2026-7-3 15:16
怎么玩呢
4#
xiaoguips123 发表于 2026-7-3 15:19
点开始不生效
5#
SDQDSJW2000 发表于 2026-7-3 15:21
经典怀旧啊!支持一下!
6#
Flanders 发表于 2026-7-3 15:21
最怀念烟山版90 TANK 吃3把手枪可以打草,还能吃船水上行走如在雪地上丝滑
7#
l12345678qaz 发表于 2026-7-3 15:30
不能玩的
8#
gy123621 发表于 2026-7-3 15:38
按钮点击无效
9#
 楼主| Summer000 发表于 2026-7-3 15:42 |楼主

保存代码,格式html,直接运行
10#
 楼主| Summer000 发表于 2026-7-3 15:42 |楼主

我本地试着好着呢
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-7-4 07:29

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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