🎮ArcadeLab

LETHAL 2D - 致命公司2D版

by SolarWolf30
2221 lines74.6 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LETHAL 2D - 致命公司2D版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #0a0a0a;
  color: #fff;
  font-family: 'Courier New', monospace;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  user-select: none;
}
#gameContainer {
  position: relative;
  width: 100vw;
  height: 100vh;
}
canvas {
  display: block;
  background: #000;
  cursor: crosshair;
}
#ui {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 10;
}
#topBar {
  position: absolute;
  top: 10px; left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 20px;
  background: rgba(0,0,0,0.7);
  padding: 8px 20px;
  border: 1px solid #444;
  border-radius: 4px;
  font-size: 14px;
}
#topBar span { color: #ffd700; }
#inventory {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 6px;
}
.slot {
  width: 56px; height: 56px;
  background: rgba(0,0,0,0.75);
  border: 2px solid #555;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  font-size: 11px;
  position: relative;
  color: #ccc;
}
.slot.active { border-color: #ffd700; box-shadow: 0 0 10px rgba(255,215,0,0.5); }
.slot .key { position: absolute; top: 2px; left: 4px; font-size: 10px; color: #888; }
.slot .item-icon { font-size: 22px; }
.slot .item-name { font-size: 9px; text-align: center; margin-top: 2px; }
.slot.tool { border-color: #4488ff; }
.slot.tool .item-icon { color: #66aaff; }
#healthBar {
  position: absolute;
  bottom: 90px;
  left: 50%;
  transform: translateX(-50%);
  width: 200px;
  height: 16px;
  background: rgba(0,0,0,0.7);
  border: 1px solid #666;
  border-radius: 3px;
  overflow: hidden;
}
#healthFill {
  height: 100%;
  background: linear-gradient(90deg, #ff4444, #ff6666);
  transition: width 0.3s;
}
#flashlightBar {
  position: absolute;
  bottom: 115px;
  left: 50%;
  transform: translateX(-50%);
  width: 150px;
  height: 10px;
  background: rgba(0,0,0,0.7);
  border: 1px solid #666;
  border-radius: 3px;
  overflow: hidden;
}
#flashlightFill {
  height: 100%;
  background: linear-gradient(90deg, #ffff88, #ffdd44);
  transition: width 0.2s;
}
#message {
  position: absolute;
  top: 80px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0,0,0,0.8);
  padding: 10px 20px;
  border-radius: 4px;
  font-size: 14px;
  display: none;
  text-align: center;
  max-width: 60%;
}
#shopUI, #shipInvUI, #menuUI, #gameOverUI {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: rgba(0,0,0,0.92);
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 100;
  pointer-events: auto;
}
.panel {
  background: #1a1a1a;
  border: 2px solid #444;
  border-radius: 8px;
  padding: 30px;
  max-width: 700px;
  width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}
.panel h2 { color: #ffd700; margin-bottom: 15px; text-align: center; }
.exit-btn {
  position: absolute;
  top: 15px;
  right: 15px;
  background: #c0392b;
  color: #fff;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-family: inherit;
  font-size: 14px;
}
.exit-btn:hover { background: #e74c3c; }
.shop-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  margin: 15px 0;
}
.shop-item {
  background: #222;
  border: 1px solid #555;
  border-radius: 4px;
  padding: 10px;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s;
}
.shop-item:hover { border-color: #ffd700; background: #2a2a2a; }
.shop-item .icon { font-size: 28px; }
.shop-item .name { font-size: 12px; margin: 5px 0; }
.shop-item .price { color: #ffd700; font-size: 13px; font-weight: bold; }
.shop-item.owned { opacity: 0.5; cursor: not-allowed; }
.page-controls {
  display: flex;
  justify-content: center;
  gap: 15px;
  margin-top: 10px;
}
.page-controls button {
  background: #333;
  color: #fff;
  border: 1px solid #666;
  padding: 6px 14px;
  border-radius: 4px;
  cursor: pointer;
  font-family: inherit;
}
.page-controls button:hover { background: #444; }
.page-controls button:disabled { opacity: 0.3; cursor: not-allowed; }
.ship-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  margin: 15px 0;
}
.ship-slot {
  width: 70px; height: 70px;
  background: #222;
  border: 2px solid #555;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  font-size: 11px;
}
.ship-slot:hover { border-color: #ffd700; }
.ship-slot .icon { font-size: 24px; }
.menu-btn {
  display: block;
  width: 250px;
  margin: 10px auto;
  padding: 14px;
  background: #2c3e50;
  color: #fff;
  border: 2px solid #3498db;
  border-radius: 6px;
  cursor: pointer;
  font-family: inherit;
  font-size: 16px;
  transition: all 0.2s;
}
.menu-btn:hover { background: #34495e; border-color: #5dade2; }
.menu-title {
  font-size: 48px;
  color: #ffd700;
  text-align: center;
  margin-bottom: 10px;
  text-shadow: 0 0 20px rgba(255,215,0,0.5);
  letter-spacing: 4px;
}
.menu-sub { text-align: center; color: #888; margin-bottom: 30px; font-size: 14px; }
.controls-info {
  background: rgba(0,0,0,0.5);
  padding: 15px;
  border-radius: 6px;
  margin-top: 20px;
  font-size: 12px;
  color: #aaa;
  line-height: 1.8;
}
.controls-info b { color: #ffd700; }
#hint {
  position: absolute;
  bottom: 160px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0,0,0,0.7);
  padding: 6px 14px;
  border-radius: 4px;
  font-size: 12px;
  color: #aaa;
  display: none;
}
#radioPlaying {
  position: absolute;
  top: 50px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(100,0,100,0.8);
  padding: 8px 20px;
  border-radius: 4px;
  font-size: 13px;
  display: none;
  animation: pulse 1s infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.6; }
}
</style>
</head>
<body>
<div id="gameContainer">
  <canvas id="game"></canvas>
  <div id="ui">
    <div id="topBar">
      <div>任务金额: <span id="taskAmount">$0</span></div>
      <div>已收集: <span id="collectedAmount">$0</span></div>
      <div>时间: <span id="timer">30:00</span></div>
      <div>地图: <span id="mapName">-</span></div>
    </div>
    <div id="healthBar"><div id="healthFill" style="width:100%"></div></div>
    <div id="flashlightBar"><div id="flashlightFill" style="width:100%"></div></div>
    <div id="inventory"></div>
    <div id="hint"></div>
    <div id="message"></div>
    <div id="radioPlaying">📻 BL STUDIO HARDTEKK (Slowed) 播放中...</div>
  </div>

  <div id="menuUI">
    <div class="menu-title">LETHAL 2D</div>
    <div class="menu-sub">致命公司 2D 版 — 进入大门,捡取物资,活着回来</div>
    <button class="menu-btn" onclick="Game.startGame()">开始游戏</button>
    <button class="menu-btn" onclick="Game.showControls()">操作说明</button>
    <div id="controlsDetail" class="controls-info" style="display:none;max-width:500px;margin:20px auto;">
      <b>WASD</b> - 移动 &nbsp; <b>鼠标</b> - 转视野<br>
      <b>1-4</b> - 切换物品栏 &nbsp; <b>5</b> - 切换小包<br>
      <b>F</b> - 开关手电筒 &nbsp; <b>E</b> - 拾取/购买<br>
      <b>Q</b> - 上交物资 &nbsp; <b>T</b> - 丢弃物品<br>
      <b>O</b> - 飞船储物 &nbsp; <b>Y</b> - 上车/出发<br>
      <b>空格</b> - 使用道具(收音机/枪) &nbsp; <b>ESC</b> - 暂停
    </div>
  </div>

  <div id="shopUI">
    <div class="panel" style="position:relative;">
      <button class="exit-btn" onclick="Game.closeShop()">退出商店</button>
      <h2>🛒 公司商店</h2>
      <div style="text-align:center;color:#ffd700;margin-bottom:10px;">余额: $<span id="shopMoney">0</span></div>
      <div class="shop-grid" id="shopGrid"></div>
      <div class="page-controls" id="pageControls" style="display:none;">
        <button onclick="Game.shopPage(-1)" id="prevPage">上一页</button>
        <span id="pageNum">1/1</span>
        <button onclick="Game.shopPage(1)" id="nextPage">下一页</button>
      </div>
      <div style="text-align:center;margin-top:15px;font-size:12px;color:#888;">
        走到车子旁按 <b style="color:#ffd700;">Y</b> 上车出发
      </div>
    </div>
  </div>

  <div id="shipInvUI">
    <div class="panel" style="position:relative;">
      <button class="exit-btn" onclick="Game.closeShipInv()">关闭</button>
      <h2>🚀 飞船储物柜</h2>
      <div style="text-align:center;color:#888;font-size:12px;margin-bottom:10px;">道具永久保存,点击取出</div>
      <div class="ship-grid" id="shipGrid"></div>
    </div>
  </div>

  <div id="gameOverUI">
    <div class="panel" style="text-align:center;">
      <h2 id="gameOverTitle">任务失败</h2>
      <p id="gameOverMsg" style="margin:15px 0;color:#aaa;"></p>
      <p style="color:#ffd700;">本局收集: $<span id="finalCollected">0</span></p>
      <button class="menu-btn" onclick="Game.backToMenu()">返回主菜单</button>
    </div>
  </div>
</div>

<script>
// 全局错误捕获
window.addEventListener('error', function(e) {
  console.error('JS错误:', e.message, e.filename, e.lineno);
  var div = document.createElement('div');
  div.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ff0000;color:#fff;padding:10px;z-index:99999;font-size:12px;';
  div.textContent = 'ERROR: ' + e.message + ' (' + e.lineno + ')';
  document.body.appendChild(div);
});
// ==================== 常量定义 ====================
const TILE = 32;
const MAP_W = 80;
const MAP_H = 80;
const VIEW_RADIUS = 3;       // 不开手电时可见3步(包括自身)
const MOUSE_VIEW = 20;        // 开手电时鼠标方向可见20步
const MOUSE_VIEW_DARK = 5;    // 不开手电时鼠标方向最多5步
const GAME_TIME = 30 * 60;    // 30分钟
const SHIP_REFILL_DIST = 3;   // 离飞船3步充电

// 地图瓦片类型
const T_FLOOR = 0, T_WALL = 1, T_GATE = 2, T_OUTSIDE = 3, T_BUILDING = 4;

// ==================== 音效系统 ====================
const Audio = {
  ctx: null,
  init() {
    if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)();
  },
  play(type) {
    if (!this.ctx) return;
    const ctx = this.ctx;
    const now = ctx.currentTime;
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.connect(g); g.connect(ctx.destination);
    
    switch(type) {
      case 'pickup':
        o.frequency.setValueAtTime(600, now);
        o.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
        g.gain.setValueAtTime(0.2, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
        o.start(now); o.stop(now + 0.15);
        break;
      case 'drop':
        o.frequency.setValueAtTime(400, now);
        o.frequency.exponentialRampToValueAtTime(150, now + 0.15);
        g.gain.setValueAtTime(0.15, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
        o.start(now); o.stop(now + 0.2);
        break;
      case 'sell':
        o.frequency.setValueAtTime(800, now);
        o.frequency.setValueAtTime(1000, now + 0.08);
        o.frequency.setValueAtTime(1400, now + 0.16);
        g.gain.setValueAtTime(0.2, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
        o.start(now); o.stop(now + 0.3);
        break;
      case 'hurt':
        o.type = 'sawtooth';
        o.frequency.setValueAtTime(200, now);
        o.frequency.exponentialRampToValueAtTime(80, now + 0.2);
        g.gain.setValueAtTime(0.25, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
        o.start(now); o.stop(now + 0.25);
        break;
      case 'monster':
        o.type = 'square';
        o.frequency.setValueAtTime(100 + Math.random()*50, now);
        g.gain.setValueAtTime(0.1, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
        o.start(now); o.stop(now + 0.3);
        break;
      case 'flashlight':
        o.frequency.setValueAtTime(1500, now);
        g.gain.setValueAtTime(0.08, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
        o.start(now); o.stop(now + 0.05);
        break;
      case 'buy':
        o.frequency.setValueAtTime(500, now);
        o.frequency.setValueAtTime(700, now + 0.06);
        o.frequency.setValueAtTime(900, now + 0.12);
        g.gain.setValueAtTime(0.15, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
        o.start(now); o.stop(now + 0.25);
        break;
      case 'gunshot':
        o.type = 'sawtooth';
        o.frequency.setValueAtTime(150, now);
        o.frequency.exponentialRampToValueAtTime(50, now + 0.1);
        g.gain.setValueAtTime(0.3, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
        o.start(now); o.stop(now + 0.15);
        break;
      case 'radio':
        // 收音机开启音效
        o.type = 'sine';
        o.frequency.setValueAtTime(440, now);
        g.gain.setValueAtTime(0.15, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
        o.start(now); o.stop(now + 0.3);
        break;
      case 'door':
        o.type = 'triangle';
        o.frequency.setValueAtTime(200, now);
        o.frequency.linearRampToValueAtTime(400, now + 0.3);
        g.gain.setValueAtTime(0.15, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
        o.start(now); o.stop(now + 0.4);
        break;
      case 'warning':
        o.type = 'square';
        o.frequency.setValueAtTime(440, now);
        o.frequency.setValueAtTime(880, now + 0.15);
        g.gain.setValueAtTime(0.12, now);
        g.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
        o.start(now); o.stop(now + 0.4);
        break;
    }
  },
  // 收音机音乐 - BL STUDIO HARDTEKK (Slowed) 风格模拟
  radioMusic: {
    playing: false,
    nodes: [],
    startTime: 0,
    start() {
      if (this.playing || !Audio.ctx) return;
      this.playing = true;
      const ctx = Audio.ctx;
      this.startTime = ctx.currentTime;
      
      // 硬派科技舞曲风格 -  slowed down
      const bpm = 100; // slowed
      const beatLen = 60 / bpm;
      
      // 低音鼓
      const kickInterval = setInterval(() => {
        if (!this.playing) { clearInterval(kickInterval); return; }
        const t = ctx.currentTime;
        const o = ctx.createOscillator();
        const g = ctx.createGain();
        o.type = 'sine';
        o.frequency.setValueAtTime(120, t);
        o.frequency.exponentialRampToValueAtTime(40, t + 0.15);
        g.gain.setValueAtTime(0.4, t);
        g.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
        o.connect(g); g.connect(ctx.destination);
        o.start(t); o.stop(t + 0.2);
      }, beatLen * 1000);
      
      // 贝斯线
      const bassNotes = [55, 55, 73, 55, 65, 55, 82, 73];
      let bassIdx = 0;
      const bassInterval = setInterval(() => {
        if (!this.playing) { clearInterval(bassInterval); return; }
        const t = ctx.currentTime;
        const freq = bassNotes[bassIdx % bassNotes.length];
        bassIdx++;
        const o = ctx.createOscillator();
        const g = ctx.createGain();
        o.type = 'sawtooth';
        o.frequency.setValueAtTime(freq, t);
        g.gain.setValueAtTime(0.12, t);
        g.gain.exponentialRampToValueAtTime(0.01, t + beatLen * 0.9);
        const filter = ctx.createBiquadFilter();
        filter.type = 'lowpass';
        filter.frequency.value = 300;
        o.connect(filter); filter.connect(g); g.connect(ctx.destination);
        o.start(t); o.stop(t + beatLen);
      }, beatLen * 1000);
      
      // 高音镲
      const hatInterval = setInterval(() => {
        if (!this.playing) { clearInterval(hatInterval); return; }
        const t = ctx.currentTime;
        const bufferSize = ctx.sampleRate * 0.05;
        const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
        const noise = ctx.createBufferSource();
        noise.buffer = buffer;
        const g = ctx.createGain();
        const filter = ctx.createBiquadFilter();
        filter.type = 'highpass';
        filter.frequency.value = 6000;
        g.gain.setValueAtTime(0.08, t);
        g.gain.exponentialRampToValueAtTime(0.01, t + 0.04);
        noise.connect(filter); filter.connect(g); g.connect(ctx.destination);
        noise.start(t); noise.stop(t + 0.05);
      }, (beatLen / 2) * 1000);
      
      this.nodes = [kickInterval, bassInterval, hatInterval];
      document.getElementById('radioPlaying').style.display = 'block';
    },
    stop() {
      this.playing = false;
      this.nodes.forEach(n => clearInterval(n));
      this.nodes = [];
      document.getElementById('radioPlaying').style.display = 'none';
    }
  }
};

// ==================== 物品定义 ====================
const ITEMS = {
  // ==================== 物资(垃圾)- 致命公司全系列 ====================
  junk_apparatus: { name:'装置', icon:'⚙️', price:65, type:'junk', color:'#aaa' },
  junk_bell: { name:'大钟', icon:'🔔', price:60, type:'junk', color:'#ddaa44' },
  junk_bigbolt: { name:'大螺栓', icon:'🔩', price:30, type:'junk', color:'#888' },
  junk_bottle: { name:'瓶子', icon:'🍾', price:35, type:'junk', color:'#88cc88' },
  junk_brush: { name:'刷子', icon:'🪥', price:25, type:'junk', color:'#dd88aa' },
  junk_candy: { name:'糖果', icon:'🍬', price:20, type:'junk', color:'#ff88cc' },
  junk_cashregister: { name:'收银机', icon:'🧾', price:90, type:'junk', color:'#66cc66' },
  junk_chemicaljug: { name:'化学壶', icon:'⚗️', price:50, type:'junk', color:'#88ff88' },
  junk_clownhorn: { name:'小丑喇叭', icon:'📯', price:50, type:'junk', color:'#ff8866' },
  junk_clock: { name:'时钟', icon:'🕰️', price:45, type:'junk', color:'#aa9966' },
  junk_cod: { name:'鳕鱼', icon:'🐟', price:52, type:'junk', color:'#88bbdd' },
  junk_comedymask: { name:'喜剧面具', icon:'🎭', price:40, type:'junk', color:'#ffdd66' },
  junk_cookiemold: { name:'饼干模具', icon:'🍪', price:35, type:'junk', color:'#cc9966' },
  junk_copperpipe: { name:'铜管', icon:'🥫', price:40, type:'junk', color:'#dd8844' },
  junk_dustpan: { name:'簸箕', icon:'🧹', price:28, type:'junk', color:'#cc8844' },
  junk_eggbeater: { name:'打蛋器', icon:'🥚', price:22, type:'junk', color:'#eeeecc' },
  junk_fancylamp: { name:'花式台灯', icon:'💡', price:45, type:'junk', color:'#ffee88' },
  junk_gift: { name:'礼物盒', icon:'🎁', price:55, type:'junk', color:'#ff6688' },
  junk_goldbar: { name:'金条', icon:'🪙', price:120, type:'junk', color:'#ffd700' },
  junk_goldcup: { name:'金杯', icon:'🏆', price:100, type:'junk', color:'#ffdd00' },
  junk_hairdryer: { name:'吹风机', icon:'💨', price:30, type:'junk', color:'#dd6688' },
  junk_jar: { name:'腌菜罐', icon:'🫙', price:25, type:'junk', color:'#aabbcc' },
  junk_kitchencounter: { name:'厨房台面', icon:'🍳', price:60, type:'junk', color:'#bb9977' },
  junk_largeaxle: { name:'大轴', icon:'⚙️', price:55, type:'junk', color:'#777' },
  junk_laserpointer: { name:'激光笔', icon:'🔴', price:35, type:'junk', color:'#ff4444' },
  junk_magic7ball: { name:'魔法7球', icon:'🔮', price:40, type:'junk', color:'#9944dd' },
  junk_magnifyingglass: { name:'放大镜', icon:'🔍', price:30, type:'junk', color:'#aaccdd' },
  junk_metalsheet: { name:'金属板', icon:'🪟', price:45, type:'junk', color:'#999' },
  junk_mug: { name:'杯子', icon:'☕', price:22, type:'junk', color:'#dd9966' },
  junk_oldphone: { name:'旧电话', icon:'📞', price:38, type:'junk', color:'#666688' },
  junk_painting: { name:'画', icon:'🖼️', price:50, type:'junk', color:'#cc8844' },
  junk_perfume: { name:'香水瓶', icon:'🧴', price:42, type:'junk', color:'#ddaaff' },
  junk_pill: { name:'药瓶', icon:'💊', price:30, type:'junk', color:'#ff88aa' },
  junk_plasticfish: { name:'塑料鱼', icon:'🐠', price:28, type:'junk', color:'#ffaa44' },
  junk_plate: { name:'盘子', icon:'🍽️', price:20, type:'junk', color:'#cccccc' },
  junk_remote: { name:'遥控器', icon:'📱', price:38, type:'junk', color:'#666688' },
  junk_ring: { name:'戒指', icon:'💍', price:75, type:'junk', color:'#ddeeff' },
  junk_robottoy: { name:'机器人玩具', icon:'🤖', price:40, type:'junk', color:'#88aacc' },
  junk_rubberduck: { name:'橡皮鸭', icon:'🦆', price:25, type:'junk', color:'#ffdd44' },
  junk_sodacan: { name:'汽水罐', icon:'🥤', price:18, type:'junk', color:'#ff6644' },
  junk_steeringwheel: { name:'方向盘', icon:'☸️', price:58, type:'junk', color:'#666666' },
  junk_stopsign: { name:'停车标志', icon:'🛑', price:45, type:'junk', color:'#ff4444' },
  junk_tatteredcape: { name:'破旧斗篷', icon:'🧣', price:35, type:'junk', color:'#884466' },
  junk_teeth: { name:'牙齿', icon:'🦷', price:20, type:'junk', color:'#eeeeee' },
  junk_tragedymask: { name:'悲剧面具', icon:'🎭', price:40, type:'junk', color:'#6666aa' },
  junk_toycube: { name:'玩具方块', icon:'🧊', price:22, type:'junk', color:'#88ccff' },
  junk_vase: { name:'花瓶', icon:'🏺', price:48, type:'junk', color:'#cc99cc' },
  junk_whoopiecushion: { name:'放屁垫', icon:'💨', price:20, type:'junk', color:'#cc6688' },
  junk_yieldsign: { name:'让行标志', icon:'⚠️', price:40, type:'junk', color:'#ffaa00' },
  junk_zeddog: { name:'Zedd狗', icon:'🐕', price:35, type:'junk', color:'#aa7755' },
  
  // ==================== 道具(商店购买,蓝色,不被清空)====================
  tool_flashlight: { name:'手电筒', icon:'🔦', price:0, type:'tool', color:'#4488ff', desc:'基础照明,电量1分钟' },
  tool_superflash: { name:'超强手电', icon:'🔦', price:500, type:'tool', color:'#44aaff', desc:'强光照明,电量3分钟' },
  tool_radio: { name:'收音机', icon:'📻', price:150, type:'tool', color:'#4488ff', desc:'停止所有怪物1分钟' },
  tool_gun: { name:'手枪', icon:'🔫', price:1000, type:'tool', color:'#4488ff', desc:'武器,需要子弹' },
  tool_bullet: { name:'子弹', icon:'🔸', price:100, type:'tool', color:'#4488ff', desc:'手枪弹药x1' },
  tool_backpack: { name:'小包', icon:'🎒', price:300, type:'tool', color:'#4488ff', desc:'+1物品栏,按5切换' },
  tool_shovel: { name:'铲子', icon:'🪏', price:300, type:'tool', color:'#4488ff', desc:'近战武器,空格攻击' },
  tool_boombox: { name:'音箱', icon:'📻', price:120, type:'tool', color:'#4488ff', desc:'播放音乐吸引怪物' },
  tool_stungrenade: { name:'眩晕手榴弹', icon:'💣', price:250, type:'tool', color:'#4488ff', desc:'空格投掷,眩晕怪物' },
  tool_zapgun: { name:'电击枪', icon:'⚡', price:400, type:'tool', color:'#4488ff', desc:'电击麻痹怪物' },
  tool_jetpack: { name:'喷气背包', icon:'🚀', price:1000, type:'tool', color:'#4488ff', desc:'提升移动速度' },
  tool_extensionladder: { name:'伸缩梯', icon:'🪜', price:500, type:'tool', color:'#4488ff', desc:'探索道具' },
  tool_radarbooster: { name:'雷达增强器', icon:'📡', price:300, type:'tool', color:'#4488ff', desc:'显示物资位置' },
  tool_spraypaint: { name:'喷漆', icon:'🎨', price:200, type:'tool', color:'#4488ff', desc:'标记路径' },
  tool_walkietalkie: { name:'对讲机', icon:'📟', price:200, type:'tool', color:'#4488ff', desc:'通讯道具' },
  tool_hazardsuit: { name:'防护服', icon:'🦺', price:300, type:'tool', color:'#4488ff', desc:'减少受到的伤害' },
  tool_lockpicker: { name:'开锁器', icon:'🔑', price:200, type:'tool', color:'#4488ff', desc:'开锁道具' },
  tool_tzpinhalant: { name:'TZP吸入剂', icon:'💨', price:300, type:'tool', color:'#4488ff', desc:'临时提升速度' },
};

// 商店商品列表(超过8个自动分页)
const SHOP_ITEMS = [
  'tool_superflash','tool_radio','tool_gun','tool_bullet',
  'tool_backpack','tool_shovel','tool_boombox','tool_stungrenade',
  'tool_zapgun','tool_jetpack','tool_extensionladder','tool_radarbooster',
  'tool_spraypaint','tool_walkietalkie','tool_hazardsuit','tool_lockpicker',
  'tool_tzpinhalant'
];

// ==================== 15种怪物定义(模仿致命公司)====================
const MONSTERS = [
  { id:'circuit_bee', name:'电路蜜蜂', icon:'🐝', hp:20, dmg:8, speed:2.2, color:'#ffcc00', behavior:'swarm', desc:'群体追击' },
  { id:'centipede', name:'巨型蜈蚣', icon:'🐛', hp:40, dmg:15, speed:1.5, color:'#884422', behavior:'ambush', desc:'伏击掉落' },
  { id:'coilhead', name:'线圈头', icon:'🗿', hp:60, dmg:25, speed:3.0, color:'#aa8855', behavior:'stare', desc:'你看它不动' },
  { id:'eyeless_dog', name:'瞎眼狗', icon:'🐕', hp:80, dmg:30, speed:2.8, color:'#555555', behavior:'sound', desc:'听声追击' },
  { id:'forest_keeper', name:'森林守护者', icon:'🌳', hp:120, dmg:40, speed:1.8, color:'#2d5a27', behavior:'patrol', desc:'高大巡逻' },
  { id:'giant', name:'巨人', icon:'👹', hp:150, dmg:50, speed:1.2, color:'#8b4513', behavior:'chase', desc:'缓慢但致命' },
  { id:'hoarding_bug', name:'地穴蝙蝠', icon:'🦇', hp:15, dmg:5, speed:3.5, color:'#4a235a', behavior:'steal', desc:'偷物资' },
  { id:'jester', name:'小丑盒', icon:'🤡', hp:70, dmg:35, speed:2.0, color:'#ff4444', behavior:'windup', desc:'蓄力后狂暴' },
  { id:'masked', name:'面具人', icon:'🎭', hp:50, dmg:20, speed:2.5, color:'#ffffff', behavior:'mimic', desc:'模仿玩家' },
  { id:'maneater', name:'泥怪', icon:'🟤', hp:90, dmg:28, speed:1.0, color:'#6b4423', behavior:'ground', desc:'地底突袭' },
  { id:'earth_leviathan', name:'沙虫', icon:'🪱', hp:200, dmg:60, speed:0.8, color:'#c0a080', behavior:'underground', desc:'地下吞噬' },
  { id:'spore_lizard', name:'孢子蜥蜴', icon:'🦎', hp:35, dmg:12, speed:2.0, color:'#556b2f', behavior:'spore', desc:'喷孢子减速' },
  { id:'bunker_spider', name:'地堡蜘蛛', icon:'🕷️', hp:45, dmg:18, speed:2.3, color:'#2f2f2f', behavior:'web', desc:'结网陷阱' },
  { id:'thumper', name:'冲刺虫', icon:'🦗', hp:30, dmg:16, speed:4.0, color:'#cd853f', behavior:'dash', desc:'高速冲刺' },
  { id:'ghost_girl', name:'幽灵女孩', icon:'👻', hp:999, dmg:999, speed:1.5, color:'#e6e6fa', behavior:'haunt', desc:'无法杀死,猎杀' },
];

// ==================== 地图名称 ====================
const MAP_NAMES = ['220-资产','47-决断','9-丁尼生','21-奥芬','12-黑森林'];

// ==================== 游戏状态 ====================
window.Game = {
  state: 'menu', // menu, playing, shop, shipinv, gameover
  canvas: null,
  ctx: null,
  mouse: { x: 0, y: 0, worldX: 0, worldY: 0, angle: 0 },
  keys: {},
  map: null,
  mapName: '',
  player: null,
  monsters: [],
  items: [],        // 地图上的物品
  droppedItems: [], // 丢弃的物品
  shipPos: { x: 0, y: 0 },
  gatePos: { x: 0, y: 0 },
  insideArea: { x: 0, y: 0, w: 0, h: 0 },
  taskAmount: 0,
  collectedAmount: 0,
  totalMoney: 0,
  timeLeft: GAME_TIME,
  firstGame: true,
  shopPage: 0,
  shipInventory: [null,null,null,null,null], // 5格永久存储
  radioActive: false,
  radioTimer: 0,
  camera: { x: 0, y: 0 },
  lightCanvas: null,
  lightCtx: null,
  lastTime: 0,
  gameOverReason: '',
  
  init() {
    this.canvas = document.getElementById('game');
    this.ctx = this.canvas.getContext('2d');
    this.resize();
    window.addEventListener('resize', () => this.resize());
    
    // 光照离屏canvas
    this.lightCanvas = document.createElement('canvas');
    this.lightCtx = this.lightCanvas.getContext('2d');
    
    // 输入
    window.addEventListener('keydown', e => {
      this.keys[e.key.toLowerCase()] = true;
      // Escape关闭弹窗
      if (e.key === 'Escape') {
        if (this.state === 'shop') this.closeShop();
        else if (this.state === 'shipinv') this.closeShipInv();
        return;
      }
      // 菜单状态下按Enter开始游戏
      if (this.state === 'menu' && e.key === 'Enter') {
        this.startGame();
        return;
      }
      this.handleKey(e.key.toLowerCase());
      if (['w','a','s','d',' ','e','q','t','f','o','y','1','2','3','4','5'].includes(e.key.toLowerCase())) {
        e.preventDefault();
      }
    });
    window.addEventListener('keyup', e => { this.keys[e.key.toLowerCase()] = false; });
    
    this.canvas.addEventListener('mousemove', e => {
      const rect = this.canvas.getBoundingClientRect();
      this.mouse.x = e.clientX - rect.left;
      this.mouse.y = e.clientY - rect.top;
    });
    
    this.canvas.addEventListener('click', () => {
      Audio.init();
    });
    
    // 显示菜单
    document.getElementById('menuUI').style.display = 'flex';
    requestAnimationFrame(t => this.loop(t));
  },
  
  resize() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
  },
  
  showControls() {
    const el = document.getElementById('controlsDetail');
    el.style.display = el.style.display === 'none' ? 'block' : 'none';
  },
  
  // ==================== 开始游戏 ====================
  startGame() {
    Audio.init();
    document.getElementById('menuUI').style.display = 'none';
    document.getElementById('gameOverUI').style.display = 'none';
    
    // 随机选择地图
    const mapIdx = Math.floor(Math.random() * MAP_NAMES.length);
    this.mapName = MAP_NAMES[mapIdx];
    document.getElementById('mapName').textContent = this.mapName;
    
    this.generateMap(mapIdx);
    this.spawnPlayer();
    this.spawnMonsters();
    this.spawnJunk();
    
    this.state = 'playing';
    this.timeLeft = GAME_TIME;
    this.collectedAmount = 0;
    this.radioActive = false;
    this.radioTimer = 0;
    Audio.radioMusic.stop();
    
    // 设置任务金额(不大于地图刷新的垃圾总值)
    const totalJunkValue = this.items.filter(i => i.type === 'junk').reduce((s,i) => s + i.price, 0);
    this.taskAmount = Math.min(Math.floor(totalJunkValue * (0.4 + Math.random() * 0.3)), totalJunkValue);
    document.getElementById('taskAmount').textContent = '$' + this.taskAmount;
    
    this.updateUI();
    this.showMessage('欢迎来到 ' + this.mapName + '!进入大门收集物资,30分钟内撤离。', 4000);
  },
  
  // ==================== 地图生成 ====================
  generateMap(mapIdx) {
    // 初始化地图为外墙
    this.map = [];
    for (let y = 0; y < MAP_H; y++) {
      this.map[y] = [];
      for (let x = 0; x < MAP_W; x++) {
        if (x === 0 || y === 0 || x === MAP_W-1 || y === MAP_H-1) {
          this.map[y][x] = T_WALL;
        } else {
          this.map[y][x] = T_OUTSIDE;
        }
      }
    }
    
    // 内部区域(大门内)- 居中偏上
    const inX = 15, inY = 10, inW = 50, inH = 45;
    this.insideArea = { x: inX, y: inY, w: inW, h: inH };
    
    // 填充内部为地板
    for (let y = inY; y < inY + inH; y++) {
      for (let x = inX; x < inX + inW; x++) {
        this.map[y][x] = T_FLOOR;
      }
    }
    
    // 内部墙壁(建筑结构)
    const seed = mapIdx * 12345 + 67890;
    let rng = seed;
    const rand = () => { rng = (rng * 1103515245 + 12345) & 0x7fffffff; return rng / 0x7fffffff; };
    
    // 生成房间墙壁
    const numRooms = 8 + mapIdx * 2;
    for (let i = 0; i < numRooms; i++) {
      const rx = inX + 3 + Math.floor(rand() * (inW - 8));
      const ry = inY + 3 + Math.floor(rand() * (inH - 8));
      const rw = 4 + Math.floor(rand() * 6);
      const rh = 3 + Math.floor(rand() * 5);
      // 画房间墙(留门)
      for (let x = rx; x < rx + rw && x < inX + inW - 1; x++) {
        if (this.map[ry] && this.map[ry][x] === T_FLOOR) this.map[ry][x] = T_WALL;
        if (this.map[ry+rh-1] && this.map[ry+rh-1][x] === T_FLOOR) this.map[ry+rh-1][x] = T_WALL;
      }
      for (let y = ry; y < ry + rh && y < inY + inH - 1; y++) {
        if (this.map[y] && this.map[y][rx] === T_FLOOR) this.map[y][rx] = T_WALL;
        if (this.map[y] && this.map[y][rx+rw-1] === T_FLOOR) this.map[y][rx+rw-1] = T_WALL;
      }
      // 留门
      const doorSide = Math.floor(rand() * 4);
      if (doorSide === 0 && this.map[ry]) this.map[ry][rx + Math.floor(rw/2)] = T_FLOOR;
      if (doorSide === 1 && this.map[ry+rh-1]) this.map[ry+rh-1][rx + Math.floor(rw/2)] = T_FLOOR;
      if (doorSide === 2 && this.map[ry]) this.map[ry + Math.floor(rh/2)][rx] = T_FLOOR;
      if (doorSide === 3 && this.map[ry]) this.map[ry + Math.floor(rh/2)][rx+rw-1] = T_FLOOR;
    }
    
    // 随机障碍/柱子
    for (let i = 0; i < 30; i++) {
      const ox = inX + 2 + Math.floor(rand() * (inW - 4));
      const oy = inY + 2 + Math.floor(rand() * (inH - 4));
      if (this.map[oy] && this.map[oy][ox] === T_FLOOR) {
        this.map[oy][ox] = T_BUILDING;
      }
    }
    
    // 大门位置(内部区域底部中央)
    const gateX = inX + Math.floor(inW / 2);
    const gateY = inY + inH;
    this.gatePos = { x: gateX, y: gateY };
    
    // 在底部墙上开大门
    for (let x = gateX - 1; x <= gateX + 1; x++) {
      if (this.map[gateY] && x >= 0 && x < MAP_W) {
        this.map[gateY][x] = T_GATE;
      }
    }
    
    // 飞船位置 - 大门外,检查空旷
    this.shipPos = this.findShipSpawn(gateX, gateY);
    
    // 验证所有物资可达(在spawnJunk中检查)
  },
  
  findShipSpawn(gateX, gateY) {
    // 在大门下方找空旷位置
    for (let attempt = 0; attempt < 100; attempt++) {
      const sx = gateX + Math.floor((Math.random() - 0.5) * 10);
      const sy = gateY + 5 + Math.floor(Math.random() * 8);
      if (sx < 2 || sx >= MAP_W-2 || sy < 2 || sy >= MAP_H-2) continue;
      
      // 检查3x3区域是否空旷(无建筑、无墙)
      let clear = true;
      for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -1; dx <= 1; dx++) {
          const tile = this.map[sy+dy]?.[sx+dx];
          if (tile === T_WALL || tile === T_BUILDING || tile === undefined) {
            clear = false;
            break;
          }
        }
        if (!clear) break;
      }
      if (!clear) continue;
      
      // 检查能否到达大门(简单直线检查)
      let canReachGate = true;
      for (let y = Math.min(sy, gateY); y <= Math.max(sy, gateY); y++) {
        const tile = this.map[y]?.[sx];
        if (tile === T_WALL || tile === T_BUILDING) { canReachGate = false; break; }
      }
      if (!canReachGate) continue;
      
      // 检查能否上交物资(飞船位置本身可达)
      return { x: sx, y: sy };
    }
    // 兜底:大门正下方
    return { x: gateX, y: gateY + 6 };
  },
  
  // ==================== 生成玩家 ====================
  spawnPlayer() {
    this.player = {
      x: this.shipPos.x * TILE + TILE/2,
      y: this.shipPos.y * TILE + TILE/2,
      speed: 3,
      health: 100,
      maxHealth: 100,
      inventory: [null, null, null, null], // 4格
      backpackSlot: null, // 小包额外1格
      hasBackpack: false,
      activeSlot: 0,
      flashlightOn: false,
      flashlightPower: 60, // 秒
      maxFlashlightPower: 60,
      hasSuperFlash: false,
      hasGun: false,
      bullets: 0,
      facing: 0, // 角度
      invulnTimer: 0,
    };
    
    // 第一局送手电筒
    if (this.firstGame) {
      this.player.inventory[0] = { ...ITEMS.tool_flashlight };
      this.firstGame = false;
    }
  },
  
  // ==================== 生成怪物 ====================
  spawnMonsters() {
    this.monsters = [];
    const inArea = this.insideArea;
    const numMonsters = 2 + Math.floor(Math.random() * 3); // 2-4个
    const minDistBetween = 20; // 怪物之间最小距离(格),确保分散很远
    
    for (let i = 0; i < numMonsters; i++) {
      const mDef = MONSTERS[Math.floor(Math.random() * MONSTERS.length)];
      let mx, my, attempts = 0;
      let tooClose;
      do {
        mx = inArea.x + 2 + Math.floor(Math.random() * (inArea.w - 4));
        my = inArea.y + 2 + Math.floor(Math.random() * (inArea.h - 4));
        attempts++;
        // 检查是否离其他怪物太近
        tooClose = false;
        for (const m of this.monsters) {
          if (this.dist(mx, my, m.x/TILE, m.y/TILE) < minDistBetween) {
            tooClose = true;
            break;
          }
        }
      } while ((this.map[my]?.[mx] !== T_FLOOR || this.dist(mx,my,this.gatePos.x,this.gatePos.y) < 10 || tooClose) && attempts < 100);
      
      if (attempts >= 100) continue;
      
      this.monsters.push({
        ...mDef,
        x: mx * TILE + TILE/2,
        y: my * TILE + TILE/2,
        hp: mDef.hp * 2, // 血量x2
        maxHp: mDef.hp * 2,
        dmg: mDef.dmg * 2, // 伤害x2
        vx: 0, vy: 0,
        state: 'idle',
        stateTimer: 0,
        targetX: mx * TILE + TILE/2,
        targetY: my * TILE + TILE/2,
        windup: 0,
        stunned: 0,
      });
    }
  },
  
  // ==================== 生成物资 ====================
  spawnJunk() {
    this.items = [];
    this.droppedItems = [];
    const inArea = this.insideArea;
    const junkKeys = Object.keys(ITEMS).filter(k => ITEMS[k].type === 'junk');
    const numJunk = 25 + Math.floor(Math.random() * 10);
    
    for (let i = 0; i < numJunk; i++) {
      const key = junkKeys[Math.floor(Math.random() * junkKeys.length)];
      let jx, jy, attempts = 0;
      do {
        jx = inArea.x + 1 + Math.floor(Math.random() * (inArea.w - 2));
        jy = inArea.y + 1 + Math.floor(Math.random() * (inArea.h - 2));
        attempts++;
      } while ((this.map[jy]?.[jx] !== T_FLOOR) && attempts < 50);
      
      if (attempts >= 50) continue;
      
      // 检查物资是否可达(从大门出发BFS简化检查)
      if (!this.isReachable(this.gatePos.x, this.gatePos.y, jx, jy)) continue;
      
      this.items.push({
        ...ITEMS[key],
        id: key + '_' + i,
        x: jx * TILE + TILE/2,
        y: jy * TILE + TILE/2,
        tileX: jx,
        tileY: jy,
        picked: false,
      });
    }
  },
  
  // 简单可达性检查(BFS)
  isReachable(sx, sy, tx, ty) {
    const visited = new Set();
    const queue = [[sx, sy]];
    visited.add(sx + ',' + sy);
    let count = 0;
    while (queue.length > 0 && count < 5000) {
      const [x, y] = queue.shift();
      if (x === tx && y === ty) return true;
      const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
      for (const [dx, dy] of dirs) {
        const nx = x + dx, ny = y + dy;
        const key = nx + ',' + ny;
        if (visited.has(key)) continue;
        const tile = this.map[ny]?.[nx];
        if (tile === T_WALL || tile === T_BUILDING || tile === undefined) continue;
        visited.add(key);
        queue.push([nx, ny]);
      }
      count++;
    }
    return false;
  },
  
  // ==================== 主循环 ====================
  loop(timestamp) {
    const dt = Math.min((timestamp - this.lastTime) / 1000, 0.1);
    this.lastTime = timestamp;
    
    if (this.state === 'playing') {
      this.update(dt);
    }
    this.render();
    requestAnimationFrame(t => this.loop(t));
  },
  
  // ==================== 更新 ====================
  update(dt) {
    // 计时器
    this.timeLeft -= dt;
    if (this.timeLeft <= 0) {
      this.gameOver('时间耗尽,未能撤离!');
      return;
    }
    
    // 玩家移动
    this.updatePlayer(dt);
    
    // 怪物更新
    if (!this.radioActive) {
      this.updateMonsters(dt);
    } else {
      this.radioTimer -= dt;
      if (this.radioTimer <= 0) {
        this.radioActive = false;
        Audio.radioMusic.stop();
        this.showMessage('收音机停止,怪物恢复活动!', 2500);
      }
    }
    
    // 手电筒电量
    if (this.player.flashlightOn && this.player.flashlightPower > 0) {
      this.player.flashlightPower -= dt;
      if (this.player.flashlightPower <= 0) {
        this.player.flashlightPower = 0;
        this.player.flashlightOn = false;
        Audio.play('warning');
        this.showMessage('手电筒电量耗尽!', 2000);
      }
    }
    
    // 离飞船3步内充电
    const distToShip = this.dist(this.player.x/TILE, this.player.y/TILE, this.shipPos.x, this.shipPos.y);
    if (distToShip <= SHIP_REFILL_DIST) {
      this.player.flashlightPower = this.player.maxFlashlightPower;
    }
    
    // 无敌时间
    if (this.player.invulnTimer > 0) this.player.invulnTimer -= dt;
    
    // 更新鼠标世界坐标和角度
    this.mouse.worldX = this.mouse.x + this.camera.x;
    this.mouse.worldY = this.mouse.y + this.camera.y;
    this.mouse.angle = Math.atan2(this.mouse.worldY - this.player.y, this.mouse.worldX - this.player.x);
    this.player.facing = this.mouse.angle;
    
    // 摄像机跟随(玩家居中)
    this.camera.x = this.player.x - this.canvas.width / 2;
    this.camera.y = this.player.y - this.canvas.height / 2;
    
    // UI更新
    this.updateUI();
    this.updateHint();
  },
  
  updatePlayer(dt) {
    const p = this.player;
    let dx = 0, dy = 0;
    if (this.keys['w']) dy -= 1;
    if (this.keys['s']) dy += 1;
    if (this.keys['a']) dx -= 1;
    if (this.keys['d']) dx += 1;
    
    if (dx !== 0 || dy !== 0) {
      const len = Math.sqrt(dx*dx + dy*dy);
      dx /= len; dy /= len;
      const newX = p.x + dx * p.speed * 60 * dt;
      const newY = p.y + dy * p.speed * 60 * dt;
      
      // 碰撞检测
      if (!this.isWall(newX, p.y)) p.x = newX;
      if (!this.isWall(p.x, newY)) p.y = newY;
      
      // 地图边界
      p.x = Math.max(TILE, Math.min(MAP_W * TILE - TILE, p.x));
      p.y = Math.max(TILE, Math.min(MAP_H * TILE - TILE, p.y));
    }
  },
  
  isWall(px, py) {
    const tx = Math.floor(px / TILE);
    const ty = Math.floor(py / TILE);
    const tile = this.map[ty]?.[tx];
    return tile === T_WALL || tile === T_BUILDING;
  },
  
  // ==================== 怪物AI ====================
  updateMonsters(dt) {
    const p = this.player;
    const playerTileX = Math.floor(p.x / TILE);
    const playerTileY = Math.floor(p.y / TILE);
    const playerInside = this.isInside(playerTileX, playerTileY);
    
    for (const m of this.monsters) {
      if (m.stunned > 0) { m.stunned -= dt; continue; }
      
      const mTileX = Math.floor(m.x / TILE);
      const mTileY = Math.floor(m.y / TILE);
      const dist = this.dist(m.x, m.y, p.x, p.y) / TILE;
      
      // 怪物只在大门内活动
      if (!this.isInside(mTileX, mTileY)) {
        // 被引到外面了,往回走
        const cx = this.insideArea.x + this.insideArea.w/2;
        const cy = this.insideArea.y + this.insideArea.h/2;
        this.moveMonster(m, cx * TILE, cy * TILE, dt);
        continue;
      }
      
      // 只有玩家在大门内时才追击
      if (!playerInside) {
        // 巡逻
        if (Math.random() < 0.01) {
          m.targetX = (this.insideArea.x + Math.random() * this.insideArea.w) * TILE;
          m.targetY = (this.insideArea.y + Math.random() * this.insideArea.h) * TILE;
        }
        this.moveMonster(m, m.targetX, m.targetY, dt);
        continue;
      }
      
      // 各种行为
      switch(m.behavior) {
        case 'swarm': // 蜜蜂 - 群体追击
        case 'chase': // 巨人 - 直接追击
        case 'dash': // 冲刺虫
          if (dist < 15) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'stare': // 线圈头 - 玩家看着就不动
          const angleToMonster = Math.atan2(m.y - p.y, m.x - p.x);
          const angleDiff = Math.abs(this.normAngle(angleToMonster - p.facing));
          if (angleDiff < 0.5 && dist < 12) {
            // 玩家看着它,不动
          } else {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'sound': // 瞎眼狗 - 听声
          if (dist < 20) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'patrol': // 森林守护者 - 巡逻,靠近追击
          if (dist < 10) {
            this.moveMonster(m, p.x, p.y, dt);
          } else {
            if (Math.random() < 0.008) {
              m.targetX = (this.insideArea.x + Math.random() * this.insideArea.w) * TILE;
              m.targetY = (this.insideArea.y + Math.random() * this.insideArea.h) * TILE;
            }
            this.moveMonster(m, m.targetX, m.targetY, dt);
          }
          break;
          
        case 'steal': // 地穴蝙蝠 - 偷物资
          if (dist < 8) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'windup': // 小丑盒 - 蓄力后狂暴
          if (dist < 12) {
            m.windup += dt;
            if (m.windup > 3) {
              this.moveMonster(m, p.x, p.y, dt, m.speed * 2);
            }
          } else {
            m.windup = Math.max(0, m.windup - dt);
          }
          break;
          
        case 'mimic': // 面具人 - 模仿
          if (dist < 15) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'ground': // 泥怪
        case 'underground': // 沙虫
          if (dist < 10) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'spore': // 孢子蜥蜴
          if (dist < 12) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'web': // 蜘蛛
          if (dist < 14) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
          
        case 'haunt': // 幽灵女孩 - 持续追击
          this.moveMonster(m, p.x, p.y, dt);
          break;
          
        case 'ambush': // 蜈蚣
          if (dist < 8) {
            this.moveMonster(m, p.x, p.y, dt);
          }
          break;
      }
      
      // 攻击玩家
      if (dist < 1.2 && p.invulnTimer <= 0) {
        p.health -= m.dmg;
        p.invulnTimer = 0.8;
        Audio.play('hurt');
        if (p.health <= 0) {
          this.gameOver('你被 ' + m.name + ' 杀死了!');
          return;
        }
      }
    }
  },
  
  moveMonster(m, tx, ty, dt, speedMult = 1) {
    const dx = tx - m.x;
    const dy = ty - m.y;
    const d = Math.sqrt(dx*dx + dy*dy);
    if (d < 2) return;
    const nx = m.x + (dx/d) * m.speed * speedMult * 60 * dt;
    const ny = m.y + (dy/d) * m.speed * speedMult * 60 * dt;
    if (!this.isWall(nx, m.y)) m.x = nx;
    if (!this.isWall(m.x, ny)) m.y = ny;
  },
  
  isInside(tx, ty) {
    const a = this.insideArea;
    return tx >= a.x && tx < a.x + a.w && ty >= a.y && ty < a.y + a.h;
  },
  
  normAngle(a) {
    while (a > Math.PI) a -= Math.PI * 2;
    while (a < -Math.PI) a += Math.PI * 2;
    return a;
  },
  
  dist(x1, y1, x2, y2) {
    return Math.sqrt((x2-x1)**2 + (y2-y1)**2);
  },
  
  // ==================== 键盘处理 ====================
  handleKey(key) {
    if (this.state !== 'playing') return;
    const p = this.player;
    
    // 物品栏切换
    if (key >= '1' && key <= '4') {
      p.activeSlot = parseInt(key) - 1;
      Audio.play('pickup');
    }
    if (key === '5' && p.hasBackpack) {
      p.activeSlot = 4; // 小包槽
      Audio.play('pickup');
    }
    
    // 手电筒
    if (key === 'f') {
      const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
      if (item && (item.name === '手电筒' || item.name === '超强手电')) {
        if (p.flashlightPower > 0) {
          p.flashlightOn = !p.flashlightOn;
          Audio.play('flashlight');
        }
      }
    }
    
    // 拾取
    if (key === 'e') {
      this.pickupNearby();
    }
    
    // 上交物资
    if (key === 'q') {
      this.sellJunk();
    }
    
    // 丢弃
    if (key === 't') {
      this.dropItem();
    }
    
    // 飞船储物
    if (key === 'o') {
      const d = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
      if (d < 3) {
        this.openShipInv();
      } else {
        this.showMessage('需要靠近飞船才能打开储物柜', 1500);
      }
    }
    
    // 上车/撤离
    if (key === 'y') {
      const d = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
      if (d < 3) {
        this.extract();
      } else {
        this.showMessage('需要靠近飞船才能上车撤离', 1500);
      }
    }
    
    // 使用道具(空格)
    if (key === ' ') {
      this.useActiveItem();
    }
  },
  
  // ==================== 拾取 ====================
  pickupNearby() {
    const p = this.player;
    const px = Math.floor(p.x / TILE);
    const py = Math.floor(p.y / TILE);
    
    // 检查附近物资
    for (const item of this.items) {
      if (item.picked) continue;
      const d = this.dist(p.x, p.y, item.x, item.y) / TILE;
      if (d < 1.5) {
        // 找空槽
        const slot = this.findEmptySlot();
        if (slot === -1) {
          this.showMessage('物品栏已满!', 1500);
          return;
        }
        if (slot === 4) {
          p.backpackSlot = { ...item };
        } else {
          p.inventory[slot] = { ...item };
        }
        item.picked = true;
        Audio.play('pickup');
        this.showMessage('拾取: ' + item.name + ' ($' + item.price + ')', 1500);
        return;
      }
    }
    
    // 检查丢弃的物品
    for (let i = this.droppedItems.length - 1; i >= 0; i--) {
      const item = this.droppedItems[i];
      const d = this.dist(p.x, p.y, item.x, item.y) / TILE;
      if (d < 1.5) {
        const slot = this.findEmptySlot();
        if (slot === -1) {
          this.showMessage('物品栏已满!', 1500);
          return;
        }
        if (slot === 4) {
          p.backpackSlot = { ...item };
        } else {
          p.inventory[slot] = { ...item };
        }
        this.droppedItems.splice(i, 1);
        Audio.play('pickup');
        this.showMessage('拾取: ' + item.name, 1500);
        return;
      }
    }
  },
  
  findEmptySlot() {
    const p = this.player;
    for (let i = 0; i < 4; i++) {
      if (!p.inventory[i]) return i;
    }
    if (p.hasBackpack && !p.backpackSlot) return 4;
    return -1;
  },
  
  // ==================== 上交物资 ====================
  sellJunk() {
    const p = this.player;
    // 检查是否在飞船旁
    const d = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
    if (d > 3) {
      this.showMessage('需要在飞船旁才能上交物资!', 1500);
      return;
    }
    
    let sold = 0;
    for (let i = 0; i < 4; i++) {
      if (p.inventory[i] && p.inventory[i].type === 'junk') {
        sold += p.inventory[i].price;
        p.inventory[i] = null;
      }
    }
    if (p.backpackSlot && p.backpackSlot.type === 'junk') {
      sold += p.backpackSlot.price;
      p.backpackSlot = null;
    }
    
    if (sold > 0) {
      this.collectedAmount += sold;
      this.totalMoney += sold;
      Audio.play('sell');
      this.showMessage('上交物资获得 $' + sold + '!总计: $' + this.collectedAmount, 2000);
    } else {
      this.showMessage('没有可上交的物资', 1500);
    }
  },
  
  // ==================== 丢弃 ====================
  dropItem() {
    const p = this.player;
    const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
    if (!item) {
      this.showMessage('该槽位为空', 1000);
      return;
    }
    
    // 收音机使用后直接丢地上(在useActiveItem中处理)
    this.droppedItems.push({
      ...item,
      x: p.x + Math.cos(p.facing) * 30,
      y: p.y + Math.sin(p.facing) * 30,
    });
    
    if (p.activeSlot === 4) {
      p.backpackSlot = null;
    } else {
      p.inventory[p.activeSlot] = null;
    }
    Audio.play('drop');
    this.showMessage('丢弃: ' + item.name, 1200);
  },
  
  // ==================== 使用道具 ====================
  useActiveItem() {
    const p = this.player;
    const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
    if (!item) return;
    
    if (item.name === '收音机') {
      // 不能在门外使用
      const px = Math.floor(p.x / TILE);
      const py = Math.floor(p.y / TILE);
      if (!this.isInside(px, py)) {
        this.showMessage('收音机不能在大门外使用!', 2000);
        return;
      }
      // 使用后直接丢到地上
      this.radioActive = true;
      this.radioTimer = 60; // 1分钟
      Audio.radioMusic.start();
      Audio.play('radio');
      
      // 丢到地上
      this.droppedItems.push({
        ...item,
        x: p.x,
        y: p.y,
        radioTimer: 60,
      });
      
      if (p.activeSlot === 4) {
        p.backpackSlot = null;
      } else {
        p.inventory[p.activeSlot] = null;
      }
      
      this.showMessage('📻 收音机已启动!所有怪物停止活动1分钟', 3000);
    }
    
    if (item.name === '手枪') {
      if (p.bullets <= 0) {
        this.showMessage('没有子弹!', 1500);
        return;
      }
      p.bullets--;
      Audio.play('gunshot');
      // 射击 - 检查鼠标方向上的怪物
      const range = 8 * TILE;
      for (const m of this.monsters) {
        const d = this.dist(p.x, p.y, m.x, m.y);
        if (d > range) continue;
        const angle = Math.atan2(m.y - p.y, m.x - p.x);
        const diff = Math.abs(this.normAngle(angle - p.facing));
        if (diff < 0.3) {
          m.hp -= 50;
          m.stunned = 0.5;
          if (m.hp <= 0) {
            this.monsters = this.monsters.filter(x => x !== m);
            this.showMessage('击杀: ' + m.name, 1500);
          }
          break;
        }
      }
    }
  },
  
  // ==================== 撤离 ====================
  extract() {
    // 上交所有物资
    this.sellJunk();
    
    if (this.collectedAmount >= this.taskAmount) {
      this.showMessage('任务完成!撤离成功,前往商店...', 2000);
    } else {
      this.showMessage('未完成任务金额,但仍可撤离。前往商店...', 2000);
    }
    
    setTimeout(() => {
      this.openShop();
    }, 1500);
  },
  
  // ==================== 商店 ====================
  openShop() {
    this.state = 'shop';
    document.getElementById('shopUI').style.display = 'flex';
    document.getElementById('shopMoney').textContent = this.totalMoney;
    this.shopPage = 0;
    this.renderShop();
  },
  
  closeShop() {
    document.getElementById('shopUI').style.display = 'none';
    this.state = 'playing';
    // 回到飞船位置
    this.player.x = this.shipPos.x * TILE + TILE/2;
    this.player.y = this.shipPos.y * TILE + TILE/2;
    this.showMessage('按 Y 上车出发下一张地图', 2500);
  },
  
  renderShop() {
    const grid = document.getElementById('shopGrid');
    const perPage = 8; // 2排 x 4列
    const totalPages = Math.ceil(SHOP_ITEMS.length / perPage);
    const start = this.shopPage * perPage;
    const items = SHOP_ITEMS.slice(start, start + perPage);
    
    grid.innerHTML = '';
    for (const key of items) {
      const item = ITEMS[key];
      const div = document.createElement('div');
      div.className = 'shop-item';
      div.innerHTML = `<div class="icon">${item.icon}</div><div class="name">${item.name}</div><div class="price">$${item.price}</div>`;
      div.onclick = () => this.buyItem(key);
      grid.appendChild(div);
    }
    
    // 分页控制
    const pageControls = document.getElementById('pageControls');
    if (totalPages > 1) {
      pageControls.style.display = 'flex';
      document.getElementById('pageNum').textContent = (this.shopPage + 1) + '/' + totalPages;
      document.getElementById('prevPage').disabled = this.shopPage === 0;
      document.getElementById('nextPage').disabled = this.shopPage >= totalPages - 1;
    } else {
      pageControls.style.display = 'none';
    }
  },
  
  shopPage(dir) {
    const perPage = 8;
    const totalPages = Math.ceil(SHOP_ITEMS.length / perPage);
    this.shopPage = Math.max(0, Math.min(totalPages - 1, this.shopPage + dir));
    this.renderShop();
  },
  
  buyItem(key) {
    const item = ITEMS[key];
    if (this.totalMoney < item.price) {
      this.showMessage('余额不足!', 1500);
      return;
    }
    
    // 特殊处理
    if (key === 'tool_backpack') {
      if (this.player.hasBackpack) {
        this.showMessage('已经有小包了', 1500);
        return;
      }
      this.player.hasBackpack = true;
      this.totalMoney -= item.price;
      Audio.play('buy');
      this.showMessage('购买: 小包!按5切换小包槽位', 2000);
    } else if (key === 'tool_bullet') {
      this.player.bullets++;
      this.totalMoney -= item.price;
      Audio.play('buy');
      this.showMessage('购买子弹!当前: ' + this.player.bullets + '发', 1500);
    } else if (key === 'tool_gun') {
      if (this.player.hasGun) {
        this.showMessage('已经有手枪了', 1500);
        return;
      }
      const slot = this.findEmptySlot();
      if (slot === -1) {
        this.showMessage('物品栏已满,无法购买!', 1500);
        return;
      }
      this.player.hasGun = true;
      if (slot === 4) this.player.backpackSlot = { ...item };
      else this.player.inventory[slot] = { ...item };
      this.totalMoney -= item.price;
      Audio.play('buy');
      this.showMessage('购买: 手枪!空格射击', 2000);
    } else if (key === 'tool_superflash') {
      const slot = this.findEmptySlot();
      if (slot === -1) {
        this.showMessage('物品栏已满,无法购买!', 1500);
        return;
      }
      if (slot === 4) this.player.backpackSlot = { ...item };
      else this.player.inventory[slot] = { ...item };
      this.player.hasSuperFlash = true;
      this.player.maxFlashlightPower = 180; // 3分钟
      this.player.flashlightPower = 180;
      this.totalMoney -= item.price;
      Audio.play('buy');
      this.showMessage('购买: 超强手电!电量3分钟', 2000);
    } else if (key === 'tool_radio') {
      const slot = this.findEmptySlot();
      if (slot === -1) {
        this.showMessage('物品栏已满,无法购买!', 1500);
        return;
      }
      if (slot === 4) this.player.backpackSlot = { ...item };
      else this.player.inventory[slot] = { ...item };
      this.totalMoney -= item.price;
      Audio.play('buy');
      this.showMessage('购买: 收音机!空格使用', 2000);
    }
    
    document.getElementById('shopMoney').textContent = this.totalMoney;
    this.renderShop();
  },
  
  // ==================== 飞船储物 ====================
  openShipInv() {
    this.state = 'shipinv';
    document.getElementById('shipInvUI').style.display = 'flex';
    this.renderShipInv();
  },
  
  closeShipInv() {
    document.getElementById('shipInvUI').style.display = 'none';
    this.state = 'playing';
  },
  
  renderShipInv() {
    const grid = document.getElementById('shipGrid');
    grid.innerHTML = '';
    for (let i = 0; i < 5; i++) {
      const item = this.shipInventory[i];
      const div = document.createElement('div');
      div.className = 'ship-slot';
      if (item) {
        div.innerHTML = `<div class="icon">${item.icon}</div><div style="font-size:10px">${item.name}</div>`;
        div.onclick = () => this.takeFromShip(i);
      } else {
        div.innerHTML = '<div style="color:#555;font-size:10px">空</div>';
        div.onclick = () => this.storeToShip(i);
      }
      grid.appendChild(div);
    }
  },
  
  storeToShip(slotIdx) {
    const p = this.player;
    const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
    if (!item) {
      this.showMessage('当前槽位为空', 1200);
      return;
    }
    if (item.type === 'junk') {
      this.showMessage('物资不能存入飞船,需要上交', 1500);
      return;
    }
    this.shipInventory[slotIdx] = { ...item };
    if (p.activeSlot === 4) p.backpackSlot = null;
    else p.inventory[p.activeSlot] = null;
    Audio.play('pickup');
    this.renderShipInv();
    this.showMessage('已存入飞船: ' + item.name, 1500);
  },
  
  takeFromShip(slotIdx) {
    const p = this.player;
    const item = this.shipInventory[slotIdx];
    if (!item) return;
    const slot = this.findEmptySlot();
    if (slot === -1) {
      this.showMessage('物品栏已满!', 1500);
      return;
    }
    if (slot === 4) p.backpackSlot = { ...item };
    else p.inventory[slot] = { ...item };
    this.shipInventory[slotIdx] = null;
    Audio.play('pickup');
    this.renderShipInv();
    this.showMessage('取出: ' + item.name, 1500);
  },
  
  // ==================== 游戏结束 ====================
  gameOver(reason) {
    this.state = 'gameover';
    this.gameOverReason = reason;
    Audio.radioMusic.stop();
    document.getElementById('gameOverTitle').textContent = '任务失败';
    document.getElementById('gameOverMsg').textContent = reason;
    document.getElementById('finalCollected').textContent = this.collectedAmount;
    document.getElementById('gameOverUI').style.display = 'flex';
  },
  
  backToMenu() {
    document.getElementById('gameOverUI').style.display = 'none';
    document.getElementById('menuUI').style.display = 'flex';
    this.state = 'menu';
    this.totalMoney = 0;
    this.firstGame = true;
    this.shipInventory = [null,null,null,null,null];
  },
  
  // ==================== UI更新 ====================
  updateUI() {
    const p = this.player;
    document.getElementById('collectedAmount').textContent = '$' + this.collectedAmount;
    
    const mins = Math.floor(this.timeLeft / 60);
    const secs = Math.floor(this.timeLeft % 60);
    document.getElementById('timer').textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
    if (this.timeLeft < 60) {
      document.getElementById('timer').style.color = '#ff4444';
    }
    
    // 血量
    document.getElementById('healthFill').style.width = (p.health / p.maxHealth * 100) + '%';
    
    // 手电筒电量
    document.getElementById('flashlightFill').style.width = (p.flashlightPower / p.maxFlashlightPower * 100) + '%';
    
    // 物品栏
    const inv = document.getElementById('inventory');
    inv.innerHTML = '';
    for (let i = 0; i < 4; i++) {
      const item = p.inventory[i];
      const slot = document.createElement('div');
      slot.className = 'slot' + (p.activeSlot === i ? ' active' : '') + (item && item.type === 'tool' ? ' tool' : '');
      slot.innerHTML = `<span class="key">${i+1}</span>` +
        (item ? `<div class="item-icon">${item.icon}</div><div class="item-name">${item.name}</div>` : '');
      inv.appendChild(slot);
    }
    // 小包槽
    if (p.hasBackpack) {
      const item = p.backpackSlot;
      const slot = document.createElement('div');
      slot.className = 'slot' + (p.activeSlot === 4 ? ' active' : '') + (item && item.type === 'tool' ? ' tool' : '');
      slot.innerHTML = `<span class="key">5</span>` +
        (item ? `<div class="item-icon">${item.icon}</div><div class="item-name">${item.name}</div>` : '<div style="font-size:9px;color:#666">小包</div>');
      inv.appendChild(slot);
    }
    // 子弹显示
    if (p.hasGun) {
      const bulletSlot = document.createElement('div');
      bulletSlot.className = 'slot tool';
      bulletSlot.innerHTML = `<div class="item-icon">🔸</div><div class="item-name">x${p.bullets}</div>`;
      inv.appendChild(bulletSlot);
    }
  },
  
  updateHint() {
    const p = this.player;
    const hint = document.getElementById('hint');
    const px = Math.floor(p.x / TILE);
    const py = Math.floor(p.y / TILE);
    const distToShip = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
    const distToGate = this.dist(p.x, p.y, this.gatePos.x * TILE, this.gatePos.y * TILE) / TILE;
    
    // 检查附近物资
    let nearItem = false;
    for (const item of this.items) {
      if (!item.picked && this.dist(p.x, p.y, item.x, item.y) / TILE < 1.5) {
        nearItem = true; break;
      }
    }
    for (const item of this.droppedItems) {
      if (this.dist(p.x, p.y, item.x, item.y) / TILE < 1.5) {
        nearItem = true; break;
      }
    }
    
    if (nearItem) {
      hint.textContent = '按 E 拾取';
      hint.style.display = 'block';
    } else if (distToShip < 3) {
      hint.textContent = '按 Q 上交物资 | 按 O 飞船储物 | 按 Y 上车撤离';
      hint.style.display = 'block';
    } else if (distToGate < 3 && !this.isInside(px, py)) {
      hint.textContent = '进入大门 - 内部有怪物!';
      hint.style.display = 'block';
    } else {
      hint.style.display = 'none';
    }
  },
  
  showMessage(text, duration = 2000) {
    const msg = document.getElementById('message');
    msg.textContent = text;
    msg.style.display = 'block';
    clearTimeout(this._msgTimer);
    this._msgTimer = setTimeout(() => { msg.style.display = 'none'; }, duration);
  },
  
  // ==================== 渲染 ====================
  render() {
    const ctx = this.ctx;
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
    
    if (this.state === 'menu' || this.state === 'gameover') return;
    
    ctx.save();
    ctx.translate(-this.camera.x, -this.camera.y);
    
    // 渲染地图
    this.renderMap();
    
    // 渲染物资
    this.renderItems();
    
    // 渲染飞船
    this.renderShip();
    
    // 渲染大门
    this.renderGate();
    
    // 渲染怪物(只有可见的)
    this.renderMonsters();
    
    // 渲染玩家
    this.renderPlayer();
    
    ctx.restore();
    
    // 光照/黑雾覆盖
    this.renderLighting();
  },
  
  renderMap() {
    const ctx = this.ctx;
    const startX = Math.max(0, Math.floor(this.camera.x / TILE));
    const startY = Math.max(0, Math.floor(this.camera.y / TILE));
    const endX = Math.min(MAP_W, Math.ceil((this.camera.x + this.canvas.width) / TILE) + 1);
    const endY = Math.min(MAP_H, Math.ceil((this.camera.y + this.canvas.height) / TILE) + 1);
    
    for (let y = startY; y < endY; y++) {
      for (let x = startX; x < endX; x++) {
        const tile = this.map[y]?.[x];
        const px = x * TILE, py = y * TILE;
        
        if (tile === T_WALL) {
          ctx.fillStyle = '#3a3a3a';
          ctx.fillRect(px, py, TILE, TILE);
          ctx.fillStyle = '#2a2a2a';
          ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
        } else if (tile === T_BUILDING) {
          ctx.fillStyle = '#4a3a2a';
          ctx.fillRect(px, py, TILE, TILE);
          ctx.fillStyle = '#3a2a1a';
          ctx.fillRect(px + 3, py + 3, TILE - 6, TILE - 6);
        } else if (tile === T_GATE) {
          ctx.fillStyle = '#664422';
          ctx.fillRect(px, py, TILE, TILE);
          ctx.fillStyle = '#886633';
          ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
        } else if (tile === T_FLOOR) {
          // 内部地板
          ctx.fillStyle = '#1a1a1f';
          ctx.fillRect(px, py, TILE, TILE);
          if ((x + y) % 2 === 0) {
            ctx.fillStyle = '#1e1e24';
            ctx.fillRect(px, py, TILE, TILE);
          }
        } else {
          // 外部地面
          ctx.fillStyle = '#1a2a1a';
          ctx.fillRect(px, py, TILE, TILE);
          if ((x + y) % 3 === 0) {
            ctx.fillStyle = '#1d2d1d';
            ctx.fillRect(px, py, TILE, TILE);
          }
        }
      }
    }
    
    // 画内部区域边界线
    const a = this.insideArea;
    ctx.strokeStyle = '#444466';
    ctx.lineWidth = 2;
    ctx.strokeRect(a.x * TILE, a.y * TILE, a.w * TILE, a.h * TILE);
  },
  
  renderItems() {
    const ctx = this.ctx;
    for (const item of this.items) {
      if (item.picked) continue;
      ctx.font = '20px serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      // 发光效果
      ctx.shadowColor = item.color || '#ffd700';
      ctx.shadowBlur = 8;
      ctx.fillText(item.icon, item.x, item.y);
      ctx.shadowBlur = 0;
    }
    // 丢弃的物品
    for (const item of this.droppedItems) {
      ctx.font = '20px serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.shadowColor = item.type === 'tool' ? '#4488ff' : '#ffd700';
      ctx.shadowBlur = 8;
      ctx.fillText(item.icon, item.x, item.y);
      ctx.shadowBlur = 0;
    }
  },
  
  renderShip() {
    const ctx = this.ctx;
    const sx = this.shipPos.x * TILE;
    const sy = this.shipPos.y * TILE;
    
    // 飞船主体
    ctx.fillStyle = '#555566';
    ctx.fillRect(sx - TILE, sy - TILE/2, TILE * 3, TILE * 2);
    ctx.fillStyle = '#666677';
    ctx.fillRect(sx - TILE + 4, sy - TILE/2 + 4, TILE * 3 - 8, TILE * 2 - 8);
    
    // 窗户
    ctx.fillStyle = '#88ccff';
    ctx.fillRect(sx - 8, sy - 4, 16, 10);
    
    // 文字
    ctx.fillStyle = '#ffd700';
    ctx.font = 'bold 10px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('飞船', sx + TILE/2, sy + TILE + 8);
    
    // 3步充电范围指示
    ctx.strokeStyle = 'rgba(255,221,68,0.2)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.arc(sx + TILE/2, sy + TILE/2, SHIP_REFILL_DIST * TILE, 0, Math.PI * 2);
    ctx.stroke();
  },
  
  renderGate() {
    const ctx = this.ctx;
    const gx = this.gatePos.x * TILE;
    const gy = this.gatePos.y * TILE;
    
    ctx.fillStyle = '#884422';
    ctx.font = 'bold 14px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('⬇ 大门 ⬇', gx + TILE/2, gy - 8);
    
    // 大门指示
    ctx.strokeStyle = '#aa6633';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(gx - TILE, gy + TILE);
    ctx.lineTo(gx + TILE * 2, gy + TILE);
    ctx.stroke();
  },
  
  renderMonsters() {
    const ctx = this.ctx;
    for (const m of this.monsters) {
      // 只渲染在视野范围内的
      const d = this.dist(m.x, m.y, this.player.x, this.player.y) / TILE;
      if (d > MOUSE_VIEW + 5) continue;
      
      // 怪物身体
      ctx.fillStyle = m.color;
      ctx.beginPath();
      ctx.arc(m.x, m.y, 14, 0, Math.PI * 2);
      ctx.fill();
      
      // 图标
      ctx.font = '18px serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(m.icon, m.x, m.y);
      
      // 血条
      if (m.hp < m.maxHp) {
        ctx.fillStyle = '#333';
        ctx.fillRect(m.x - 15, m.y - 22, 30, 4);
        ctx.fillStyle = '#ff4444';
        ctx.fillRect(m.x - 15, m.y - 22, 30 * (m.hp / m.maxHp), 4);
      }
      
      // 小丑盒蓄力指示
      if (m.behavior === 'windup' && m.windup > 0) {
        ctx.fillStyle = '#ff0000';
        ctx.font = '10px monospace';
        ctx.fillText('⚠️', m.x, m.y - 28);
      }
    }
  },
  
  renderPlayer() {
    const ctx = this.ctx;
    const p = this.player;
    
    // 无敌闪烁
    if (p.invulnTimer > 0 && Math.floor(p.invulnTimer * 10) % 2 === 0) {
      ctx.globalAlpha = 0.5;
    }
    
    // 身体
    ctx.fillStyle = '#4488ff';
    ctx.beginPath();
    ctx.arc(p.x, p.y, 12, 0, Math.PI * 2);
    ctx.fill();
    
    // 朝向指示
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(p.x, p.y);
    ctx.lineTo(p.x + Math.cos(p.facing) * 18, p.y + Math.sin(p.facing) * 18);
    ctx.stroke();
    
    // 玩家图标
    ctx.font = '14px serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('🧑', p.x, p.y);
    
    ctx.globalAlpha = 1;
  },
  
  // ==================== 光照系统 ====================
  renderLighting() {
    const ctx = this.ctx;
    const lctx = this.lightCtx;
    
    // 设置光照canvas大小
    if (this.lightCanvas.width !== this.canvas.width || this.lightCanvas.height !== this.canvas.height) {
      this.lightCanvas.width = this.canvas.width;
      this.lightCanvas.height = this.canvas.height;
    }
    
    // 全黑
    lctx.fillStyle = 'rgba(0,0,0,0.97)';
    lctx.fillRect(0, 0, this.lightCanvas.width, this.lightCanvas.height);
    
    const p = this.player;
    const cx = this.canvas.width / 2; // 玩家在屏幕中央
    const cy = this.canvas.height / 2;
    
    // 基础视野(3步圆形,柔和边缘)
    const baseGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, VIEW_RADIUS * TILE);
    baseGrad.addColorStop(0, 'rgba(0,0,0,0)');
    baseGrad.addColorStop(0.7, 'rgba(0,0,0,0.3)');
    baseGrad.addColorStop(1, 'rgba(0,0,0,0.97)');
    lctx.globalCompositeOperation = 'destination-out';
    lctx.fillStyle = baseGrad;
    lctx.beginPath();
    lctx.arc(cx, cy, VIEW_RADIUS * TILE, 0, Math.PI * 2);
    lctx.fill();
    
    // 鼠标方向视野(开手电20步,不开手电最多5步)
    const mouseAngle = Math.atan2(this.mouse.y - cy, this.mouse.x - cx);
    const spreadAngle = 0.6; // 扇形张角
    const mouseViewDist = (p.flashlightOn && p.flashlightPower > 0) ? MOUSE_VIEW : MOUSE_VIEW_DARK;
    
    lctx.beginPath();
    lctx.moveTo(cx, cy);
    lctx.arc(cx, cy, mouseViewDist * TILE, mouseAngle - spreadAngle, mouseAngle + spreadAngle);
    lctx.closePath();
    const mouseGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, mouseViewDist * TILE);
    mouseGrad.addColorStop(0, 'rgba(0,0,0,0.8)');
    mouseGrad.addColorStop(0.5, 'rgba(0,0,0,0.5)');
    mouseGrad.addColorStop(1, 'rgba(0,0,0,0)');
    lctx.fillStyle = mouseGrad;
    lctx.fill();
    
    // 手电筒 - 扩散式照明,撞墙停止
    if (p.flashlightOn && p.flashlightPower > 0) {
      this.renderFlashlight(lctx, cx, cy, p.facing);
    }
    
    lctx.globalCompositeOperation = 'source-over';
    
    // 将光照层画到主canvas
    ctx.drawImage(this.lightCanvas, 0, 0);
  },
  
  renderFlashlight(lctx, cx, cy, angle) {
    const p = this.player;
    const maxDist = (p.hasSuperFlash ? 15 : 12) * TILE;
    const steps = 40; // 光线数量
    const spread = 0.8; // 总扩散角度
    
    // 画扩散光线(边缘线,隐藏),然后填充
    const rays = [];
    for (let i = 0; i <= steps; i++) {
      const rayAngle = angle - spread/2 + (spread * i / steps);
      // 光线传播,撞墙停止
      let dist = maxDist;
      const stepSize = 8;
      for (let d = 0; d < maxDist; d += stepSize) {
        const wx = p.x + Math.cos(rayAngle) * d;
        const wy = p.y + Math.sin(rayAngle) * d;
        if (this.isWall(wx, wy)) {
          dist = d;
          break;
        }
      }
      // 离得越远扩散越大(通过增加光线间距实现)
      rays.push({ angle: rayAngle, dist: dist });
    }
    
    // 填充光锥
    lctx.globalCompositeOperation = 'destination-out';
    lctx.beginPath();
    lctx.moveTo(cx, cy);
    for (const ray of rays) {
      // 转换到屏幕坐标
      const sx = cx + Math.cos(ray.angle) * ray.dist;
      const sy = cy + Math.sin(ray.angle) * ray.dist;
      lctx.lineTo(sx, sy);
    }
    lctx.closePath();
    
    // 渐变填充 - 中心亮,边缘暗
    const lightGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, maxDist);
    lightGrad.addColorStop(0, 'rgba(0,0,0,1)');
    lightGrad.addColorStop(0.4, 'rgba(0,0,0,0.9)');
    lightGrad.addColorStop(0.8, 'rgba(0,0,0,0.5)');
    lightGrad.addColorStop(1, 'rgba(0,0,0,0)');
    lctx.fillStyle = lightGrad;
    lctx.fill();
    
    // 暖色叠加
    lctx.globalCompositeOperation = 'source-over';
    lctx.beginPath();
    lctx.moveTo(cx, cy);
    for (const ray of rays) {
      const sx = cx + Math.cos(ray.angle) * ray.dist;
      const sy = cy + Math.sin(ray.angle) * ray.dist;
      lctx.lineTo(sx, sy);
    }
    lctx.closePath();
    const warmGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, maxDist);
    warmGrad.addColorStop(0, 'rgba(255,240,180,0.15)');
    warmGrad.addColorStop(0.5, 'rgba(255,220,120,0.08)');
    warmGrad.addColorStop(1, 'rgba(255,200,80,0)');
    lctx.fillStyle = warmGrad;
    lctx.fill();
  },
};

// 启动
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => Game.init());
} else {
  Game.init();
}
</script>
</body>
</html>

Game Source: LETHAL 2D - 致命公司2D版

Creator: SolarWolf30

Libraries: none

Complexity: complex (2221 lines, 74.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: lethal-2d-2d-solarwolf30" to link back to the original. Then publish at arcadelab.ai/publish.