化学塔防

学校办的信息学科活动,大概意思就是用 AI 开发,制作网站游戏,体现数理化学科或者学校特色。

在五一以及后几个晚自习完成了。Workbuddy 还是很强的。

美术效果几乎没有,机制也没认真想。创意和其他几个作品相比差多了。总之看下来糖糖的。

一起制作的机房同学

wwwtc10 gggzy01 XNN AluWalker

游戏在学校网站上,here。但是教练说服务器只会租一个月,所以可能之后就看不到了?

代码

<!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; }
html, body { width:100%; height:100%; overflow:hidden; }
body {
  font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background: #111; display: flex; justify-content: center; align-items: center;
}
.game-box {
  position: relative; width:100%; height:100%;
  background: #FFF; overflow: hidden; display: flex; flex-direction: column;
}
.top-bar {
  position: relative; z-index:10;
  display: flex; align-items: center; gap: 16px;
  padding: 8px 16px; border-bottom: 0.5px solid #DDD;
  font-size: 13px; color: #555; background: #FFF;
}
.top-bar .title { font-weight: 600; color: #2C3E50; font-size: 15px; }
.top-bar .spacer { flex: 1; }
.canvas-wrap {
  flex: 1; min-height: 0; overflow: hidden;
  display: flex; justify-content: center; align-items: center;
  background: #1a1a1a;
}
#gameCanvas { display: block; cursor: crosshair; vertical-align: middle; }
.bottom-bar {
  position: relative; z-index:10;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; border-top: 0.5px solid #DDD;
  background: #FAFAFA; min-height: 56px; flex-wrap: wrap;
}
.tower-btn {
  padding: 5px 10px; border: 1px solid #CCC; border-radius: 8px;
  background: #FFF; cursor: pointer; font-size: 12px;
  display: flex; gap: 4px; align-items: center; transition: all 0.1s;
}
.tower-btn:hover { background: #F0F0F0; }
.tower-btn.selected { background: #3498DB; color: #FFF; border-color: #2980B9; }
.tower-btn .cost { color: #E67E22; font-weight: 600; }
.tower-btn.selected .cost { color: #FFF; }
#speedBtn { margin-left: auto; padding: 4px 12px; cursor: pointer; }
/* ========= 介绍界面 ========= */
#introScreen {
  position: fixed; inset: 0;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  z-index: 999; font-family: inherit; color: #FFF;
  overflow-y: auto; padding: 30px 20px;
}
#introScreen h1 { font-size: 28px; margin-bottom: 20px; letter-spacing: 2px; }
#introContent {
  max-width: 620px; background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 16px;
  padding: 28px 32px; line-height: 1.9; font-size: 14px;
}
#introContent h2 { font-size: 16px; color: #F6AD55; margin: 16px 0 8px; }
#introContent h2:first-child { margin-top: 0; }
#introContent table { width: 100%; border-collapse: collapse; margin: 8px 0 12px; font-size: 13px; }
#introContent th { background: rgba(255,255,255,0.1); padding: 6px 10px; text-align: left; }
#introContent td { padding: 5px 10px; border-bottom: 1px solid rgba(255,255,255,0.06); }
#introContent ul { margin: 6px 0 10px 0; padding-left: 20px; }
#introContent li { margin-bottom: 4px; }
#introStartBtn {
  margin-top: 24px; padding: 12px 40px;
  background: #3498DB; color: #FFF;
  border: none; border-radius: 30px;
  font-size: 16px; font-weight: bold;
  cursor: pointer; transition: background 0.2s, transform 0.1s;
  letter-spacing: 2px;
}
#introStartBtn:hover { background: #2980B9; transform: translateY(-2px); }

/* ========= 选关界面 ========= */
#startScreen {
  position: fixed; inset: 0;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  z-index: 999; font-family: inherit; color: #FFF;
  overflow-y: auto; padding: 20px 0;
}
#startScreen h1 { font-size: 26px; margin-bottom: 16px; letter-spacing: 2px; }
#difficultyBar {
  display: flex; gap: 12px; margin-bottom: 24px; align-items: center;
}
#difficultyBar span { font-size: 13px; opacity: 0.6; }
.diff-btn {
  padding: 7px 22px; border-radius: 20px; border: 1.5px solid #555;
  background: transparent; color: #AAA; cursor: pointer;
  font-size: 13px; letter-spacing: 1px; transition: all 0.15s;
}
.diff-btn:hover { border-color: #F6AD55; color: #F6AD55; }
.diff-btn.active-easy  { background: #2a7a4b; border-color: #2ecc71; color: #2ecc71; font-weight: 600; }
.diff-btn.active-normal{ background: #2c4880; border-color: #3498db; color: #74b9ff; font-weight: 600; }
#levelSelect { display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; }
.lvl-btn {
  width: 120px; height: 90px;
  border-radius: 12px; border: 2px solid #555;
  background: #2d3748; color: #FFF;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  cursor: pointer; font-size: 14px; transition: 0.2s;
  gap: 4px;
}
.lvl-btn:hover:not(.locked) { border-color: #F6AD55; background: #4a5568; transform: translateY(-2px); }
.lvl-btn.locked { opacity: 0.4; cursor: not-allowed; }

#msgBox {
  display: none; position: fixed; top: 50px; left: 50%; transform: translateX(-50%);
  background: rgba(231,76,60,0.95); color: #FFF; padding: 6px 20px; border-radius: 6px; font-size: 13px; z-index: 999;
}
/* ========= 跳关浮层 ========= */
#skipOverlay {
  display: none; position: fixed; inset: 0;
  background: rgba(0,0,0,0.55); z-index: 1000;
  align-items: center; justify-content: center;
}
#skipOverlay.open { display: flex; }
#skipPanel {
  background: #1e2535; border-radius: 16px;
  padding: 24px 28px; min-width: 320px;
  box-shadow: 0 8px 40px rgba(0,0,0,0.6);
  color: #FFF; font-family: inherit;
}
#skipPanel h3 { font-size: 16px; margin-bottom: 16px; color: #F6AD55; letter-spacing: 1px; }
#skipLevelList { display: flex; flex-direction: column; gap: 8px; }
.skip-lvl-btn {
  padding: 10px 16px; border-radius: 8px;
  border: 1px solid #3d4f6b; background: #2d3748;
  color: #FFF; cursor: pointer; font-size: 13px;
  display: flex; align-items: center; gap: 10px;
  transition: background 0.15s, border-color 0.15s;
  text-align: left;
}
.skip-lvl-btn:hover { background: #3a4a60; border-color: #F6AD55; }
.skip-lvl-btn .tag {
  font-size: 11px; padding: 2px 7px; border-radius: 4px;
  background: rgba(246,173,85,0.15); color: #F6AD55;
  font-weight: 600; min-width: 34px; text-align: center;
}
.skip-lvl-btn.endless .tag { background: rgba(78,205,196,0.15); color: #4ECDC4; }
#skipCloseBtn {
  margin-top: 16px; width: 100%; padding: 8px;
  border-radius: 8px; border: 1px solid #555;
  background: transparent; color: #AAA;
  cursor: pointer; font-size: 13px; transition: background 0.15s;
}
#skipCloseBtn:hover { background: #2d3748; color: #FFF; }
</style>
</head>
<body>

<!-- 介绍界面 -->
<div id="introScreen">
  <h1>🧪 化学反应塔防</h1>
  <div id="introContent">
    <h2>游戏简介</h2>
    <p>一款将高中化学融入策略塔防的游戏。在这里,你不需要大炮和箭塔——你需要的是<strong>盐酸、高锰酸钾、硝酸钡</strong>,以及一颗懂化学的大脑。</p>
    <p>游戏共设 5 个关卡,涵盖高中化学的核心知识模块,并附有挑战无上限的<strong>无尽双线模式</strong>:</p>
    <table>
      <tr><th>关卡</th><th>主题</th><th>涉及知识</th></tr>
      <tr><td>1-1</td><td>酸碱中和</td><td>HCl、NaOH 与酸碱反应</td></tr>
      <tr><td>1-2</td><td>沉淀与置换</td><td>Fe 的置换反应、BaSO₄ 沉淀</td></tr>
      <tr><td>1-3</td><td>氧化还原</td><td>KMnO₄ 氧化 SO₂、活性炭吸附</td></tr>
      <tr><td>2-1</td><td>烯烃加成与醇的氧化</td><td>Br₂ 加成、KMnO₄ 氧化醇类</td></tr>
      <tr><td>2-2</td><td>羧酸与酯的水解</td><td>NaOH 中和羧酸、酯的碱性水解</td></tr>
      <tr><td>无尽</td><td>双线综合挑战</td><td>全部化学塔混合运用</td></tr>
    </table>
    <h2>游戏玩法</h2>
    <p>在敌人(有害化学物质)沿路径涌向终点之前,将对应的<strong>化学试剂塔</strong>放置在路旁,利用化学反应将它们消灭。每一次成功的反应都会在屏幕上弹出真实的<strong>化学方程式</strong>,让你在游戏中不知不觉地巩固知识。</p>
    <ul>
      <li>💰 <strong>击败敌人获得金币</strong>,用于购买和升级塔</li>
      <li>⚗️ <strong>每种塔只能攻击特定化学物质</strong>,需根据敌人类型合理布局</li>
      <li>🔄 <strong>部分敌人消灭后会变形</strong>(如酯水解为醇),需二次处理</li>
      <li>❤️ <strong>敌人到达终点扣除生命值</strong>,生命归零即告失败</li>
    </ul>
    <p style="font-size:12px;color:rgba(255,255,255,0.45);margin-top:8px;">为了提高可玩性,使特定的塔攻击特定的敌人,一些反应在游戏中不进行。如高锰酸钾与烯烃反应,氢氧化钠与铜离子。</p>
  </div>
  <button id="introStartBtn" onclick="enterLevelSelect()">开始游戏 →</button>
</div>

<div id="startScreen">
  <h1>🧪 化学反应塔防</h1>
  <div id="difficultyBar">
    <span>难度:</span>
    <button class="diff-btn active-normal" id="diffEasyBtn"  onclick="setDifficulty('easy')">✦ 简单</button>
    <button class="diff-btn active-normal" id="diffNormalBtn" onclick="setDifficulty('normal')">● 普通</button>
  </div>
  <div id="levelSelect"></div>
</div>

<div class="game-box" id="gameBox" style="display:none">
  <div class="top-bar">
    <span class="title">🧪 化学反应塔防</span>
    <span id="levelInfo">第1关:酸碱中和</span>
    <span class="spacer"></span>
    <span id="goldInfo">💰 ¥300</span>
    <span id="livesInfo">❤️ 100</span>
    <span id="waveInfo">波次: 0/8</span>
    <button id="speedBtn" onclick="toggleSpeed()">▶ 正常</button>
    <button id="skipJumpBtn" onclick="openSkipOverlay()" style="padding:4px 12px;cursor:pointer;background:#2d3748;color:#F6AD55;border:1px solid #4a5568;border-radius:4px;font-size:12px;">跳关 ☰</button>
  </div>
  <div class="canvas-wrap">
    <canvas id="gameCanvas" width="600" height="420"></canvas>
  </div>
  <div class="bottom-bar" id="bottomBar"></div>
</div>
<div id="msgBox"></div>

<!-- 跳关浮层 -->
<div id="skipOverlay" onclick="if(event.target===this)closeSkipOverlay()">
  <div id="skipPanel">
    <h3>⚗️ 跳转关卡</h3>
    <div id="skipLevelList"></div>
    <button id="skipCloseBtn" onclick="closeSkipOverlay()">取消</button>
  </div>
</div>

<script>
// ============================================================
//   化学反应塔防 — 完整可运行版
//   路径点全部对齐 40px 格子中心
//   格子中心坐标:x = 20 + c*40, y = 20 + r*40
// ============================================================

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

// ========== 全屏缩放逻辑(高清 + 跨浏览器兼容) ==========
const LOGICAL_W = 600, LOGICAL_H = 420;
const W = LOGICAL_W, H = LOGICAL_H;
let dpr = Math.min(window.devicePixelRatio || 1, 2);
let physW = 0, physH = 0;

function resizeCanvas() {
  const ww = window.innerWidth, wh = window.innerHeight;
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  physW = ww * dpr;
  physH = wh * dpr;
  canvas.width  = physW;
  canvas.height = physH;
  canvas.style.width  = ww + 'px';
  canvas.style.height = wh + 'px';
  const sx = physW / LOGICAL_W, sy = physH / LOGICAL_H;
  ctx.setTransform(sx, 0, 0, sy, 0, 0);
}
window.addEventListener('load', function() {
  resizeCanvas();
  window.addEventListener('resize', function() { resizeCanvas(); });
});

// 屏幕坐标 → 游戏逻辑坐标
function screenToGame(sx, sy) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: (sx - rect.left) / rect.width  * LOGICAL_W,
    y: (sy - rect.top)  / rect.height * LOGICAL_H
  };
}

// ========== 路径定义(全部对齐 40px 格子中心)==========
// 格子中心公式:cx = 20 + c*40, cy = 20 + r*40
// c = 列号 0..14, r = 行号 0..9
// 路宽逻辑:怪物沿路径点移动,道路格子由 buildGrid 动态计算

const PATH1 = [
  // 从左侧屏幕外进入,沿 r=2 行向右,下到 r=7,再上到 r=1,从右侧离开
  {x:-20, y:100},   // 左侧入口(r=2)
  {x:100, y:100},   // (c=2, r=2)
  {x:100, y:300},   // 下到 (c=2, r=7)
  {x:340, y:300},   // 右到 (c=8, r=7)
  {x:340, y:60},    // 上到 (c=8, r=1)
  {x:580, y:60},    // 右到 (c=14, r=1)
  {x:620, y:60}     // 右侧出口
];

const PATH2A = [
  // 第2关主线:从左侧进入,下到 r=5,右到 c=11,下到 r=8,离开
  {x:-20, y:100},   // (c=-1, r=2)
  {x:220, y:100},   // (c=5, r=2)
  {x:220, y:220},   // 下到 (c=5, r=5)
  {x:460, y:220},   // 右到 (c=11, r=5)
  {x:460, y:340},   // 下到 (c=11, r=8)
  {x:620, y:340}    // 右侧出口
];

const PATH2B = [
  // 第2关支线:从右侧进入,左到 c=11,下到汇合点
  {x:620, y:60},    // 右侧入口(r=1)
  {x:460, y:60},    // 左到 (c=11, r=1)
  {x:460, y:220}    // 下到汇合点 (c=11, r=5),与 PATH2A 共享
];

const PATH3 = [
  // 第3关:从左侧进入,下到 r=8,右到 c=8,上到 r=1,右到 c=13,下到 r=8,离开
  {x:-20, y:60},    // (c=-1, r=1)
  {x:140, y:60},    // (c=3, r=1)
  {x:140, y:340},   // 下到 (c=3, r=8)
  {x:340, y:340},   // 右到 (c=8, r=8)
  {x:340, y:60},    // 上到 (c=8, r=1)
  {x:540, y:60},    // 右到 (c=13, r=1)
  {x:540, y:340},   // 下到 (c=13, r=8)
  {x:620, y:340}    // 右侧出口
];

// ========= 有机反应路径 =========
const PATH4 = [  // 2-1 关 S 型路径(烯烃加成与醇的氧化)
  {x:-20,y:220},{x:100,y:220},{x:100,y:140},{x:220,y:140},
  {x:220,y:300},{x:340,y:300},{x:340,y:180},{x:460,y:180},
  {x:460,y:260},{x:580,y:260}
];
const PATH5 = [  // 2-2 关 S 型路径(羧酸与酯的水解)
  {x:20,y:100},{x:140,y:100},{x:140,y:260},{x:260,y:260},
  {x:260,y:140},{x:380,y:140},{x:380,y:300},{x:500,y:300},
  {x:500,y:180},{x:580,y:180}
];

const PATH6A = [  // 无尽模式上线 (r=2~4区域)
  {x:-20,y:100},{x:20,y:100},{x:140,y:100},{x:140,y:180},{x:340,y:180},
  {x:340,y:100},{x:460,y:100},{x:460,y:180},{x:580,y:180},{x:620,y:180}
];
const PATH6B = [  // 无尽模式下线 (r=5~7区域)
  {x:-20,y:300},{x:20,y:300},{x:340,y:300},{x:340,y:220},{x:460,y:220},
  {x:460,y:300},{x:580,y:300},{x:620,y:300}
];

function getPathByKey(level, key) {
  if (level === 1) return PATH1;
  if (level === 2) return key === '2B' ? PATH2B : PATH2A;
  if (level === 3) return PATH3;
  if (level === 4) return PATH4;
  if (level === 5) return PATH5;
  if (level === 6) return key === '6B' ? PATH6B : PATH6A;
}

// 验证路径点是否对齐格子中心(调试用)
function debugCheckPath(name, path) {
  for (const p of path) {
    const cx = Math.round((p.x - 20) / 40);
    const cy = Math.round((p.y - 20) / 40);
    const expectedX = 20 + cx * 40;
    const expectedY = 20 + cy * 40;
    if (Math.abs(p.x - expectedX) > 1 || Math.abs(p.y - expectedY) > 1) {
      console.warn('路径未对齐格子中心: ' + name + ' 点(' + p.x + ',' + p.y + ') 最近格子中心(' + expectedX + ',' + expectedY + ')');
    }
  }
}
debugCheckPath('PATH1', PATH1);
debugCheckPath('PATH2A', PATH2A);
debugCheckPath('PATH2B', PATH2B);
debugCheckPath('PATH3', PATH3);

// ========== 方格系统 ==========
const CELL = 40;
const COLS = 15; // 0..14
const ROWS = 10; // 0..9
const _grid = [];       // _grid[r][c] = true(路)/false(空地)
let _roadCells = [];   // 有序道路格子中心 [{x, y}],按路径遍历顺序

// 通过格子中心坐标反算行列
function cellOf(px, py) {
  return {
    c: Math.round((px - 20) / CELL),
    r: Math.round((py - 20) / CELL)
  };
}

// 有序遍历一条路径经过的格子,生成按路径顺序的格子中心列表
// 自动裁剪超出网格范围的格子
function tracePathCells(path) {
  const cells = [];  // 按路径顺序的 {c, r}
  for (let i = 0; i < path.length - 1; i++) {
    const a = path[i], b = path[i + 1];
    const aCell = cellOf(a.x, a.y);
    const bCell = cellOf(b.x, b.y);
    if (a.y === b.y) {
      // 水平段:固定行 r,按方向遍历列
      const r = aCell.r;
      const step = aCell.c <= bCell.c ? 1 : -1;
      const startC = aCell.c;
      const endC = bCell.c;
      let c = startC;
      while (step > 0 ? c <= endC : c >= endC) {
        // 只保留网格内的格子
        if (c >= 0 && c < COLS && r >= 0 && r < ROWS) {
          if (cells.length === 0 || cells[cells.length-1].r !== r || cells[cells.length-1].c !== c) {
            cells.push({r, c});
          }
        }
        c += step;
      }
    } else {
      // 竖直段:固定列 c,按方向遍历行
      const c = aCell.c;
      const step = aCell.r <= bCell.r ? 1 : -1;
      const startR = aCell.r;
      const endR = bCell.r;
      let r = startR;
      while (step > 0 ? r <= endR : r >= endR) {
        // 只保留网格内的格子
        if (c >= 0 && c < COLS && r >= 0 && r < ROWS) {
          if (cells.length === 0 || cells[cells.length-1].r !== r || cells[cells.length-1].c !== c) {
            cells.push({r, c});
          }
        }
        r += step;
      }
    }
  }
  return cells;
}

function buildGrid(level) {
  // 初始化 grid
  for (let r = 0; r < ROWS; r++) {
    _grid[r] = [];
    for (let c = 0; c < COLS; c++) _grid[r][c] = false;
  }
  // 收集所有路径的有序格子
  const paths = level===1 ? [PATH1] : level===2 ? [PATH2A,PATH2B] : level===3 ? [PATH3] : level===4 ? [PATH4] : level===5 ? [PATH5] : [PATH6A,PATH6B];
  const seen = new Set();
  _roadCells = [];
  for (const p of paths) {
    const cells = tracePathCells(p);
    for (const cell of cells) {
      const key = cell.r * COLS + cell.c;
      if (!seen.has(key)) {
        seen.add(key);
        _roadCells.push({ x: 20 + cell.c * CELL, y: 20 + cell.r * CELL });
      }
      if (cell.r >= 0 && cell.r < ROWS && cell.c >= 0 && cell.c < COLS) {
        _grid[cell.r][cell.c] = true;
      }
    }
  }
}

// 绘制格子底板(先绘道路格子深色效果,再绘空地)
function drawGridBackground() {
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      const x = c * CELL, y = r * CELL;
      if (_grid[r][c]) {
        // 道路格子:深灰底色 + 细微纹理
        ctx.fillStyle = '#4A5568';
        ctx.fillRect(x + 1, y + 1, CELL - 2, CELL - 2);
        // 格子内纹理线(模拟道路质感)
        ctx.strokeStyle = '#5D6D7E';
        ctx.lineWidth = 0.5;
        ctx.beginPath();
        ctx.moveTo(x + CELL - 2, y + 4);
        ctx.lineTo(x + CELL - 2, y + CELL - 4);
        ctx.moveTo(x + 4, y + CELL - 2);
        ctx.lineTo(x + CELL - 4, y + CELL - 2);
        ctx.stroke();
      } else {
        // 空地格子:暖白色
        ctx.fillStyle = '#F7FAFC';
        ctx.fillRect(x, y, CELL, CELL);
      }
    }
  }
  // 绘制细微的格子分隔线(所有格子)
  ctx.strokeStyle = '#E2E8F0';
  ctx.lineWidth = 0.5;
  for (let r = 0; r <= ROWS; r++) {
    ctx.beginPath();
    ctx.moveTo(0, r * CELL);
    ctx.lineTo(COLS * CELL, r * CELL);
    ctx.stroke();
  }
  for (let c = 0; c <= COLS; c++) {
    ctx.beginPath();
    ctx.moveTo(c * CELL, 0);
    ctx.lineTo(c * CELL, ROWS * CELL);
    ctx.stroke();
  }
}

// 按路径分别绘制虚线,避免多路径时连成一条折线
function drawPath(level) {
  const paths = level===1 ? [PATH1] : level===2 ? [PATH2A,PATH2B] : level===3 ? [PATH3] : level===4 ? [PATH4] : level===5 ? [PATH5] : [PATH6A,PATH6B];
  ctx.save();
  ctx.strokeStyle = '#FFFFFF';
  ctx.lineWidth = 2;
  ctx.setLineDash([8, 8]);
  ctx.lineCap = 'round';
  for (const path of paths) {
    const cells = tracePathCells(path);
    const pts = cells
      .filter(cell => cell.c >= 0 && cell.c < COLS && cell.r >= 0 && cell.r < ROWS)
      .map(cell => ({ x: 20 + cell.c * CELL, y: 20 + cell.r * CELL }));
    if (pts.length < 2) continue;
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++) {
      ctx.lineTo(pts[i].x, pts[i].y);
    }
    ctx.stroke();
  }
  ctx.restore();
}

// 鼠标所在可用格子
function getHoverCell(x, y) {
  const c = Math.floor(x / CELL);
  const r = Math.floor(y / CELL);
  if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return null;
  if (_grid[r][c]) return null;
  return { cx: 20 + c*CELL, cy: 20 + r*CELL, r, c };
}

// 格子是否被塔占用
function cellOccupied(cx, cy) {
  for (const t of gs.towers) {
    if (Math.hypot(t.x - cx, t.y - cy) < CELL * 0.8) return true;
  }
  return false;
}

// ========== 塔定义 ==========
const TDEF = {
  hcl:   {name:'盐酸塔',   formula:'HCl',   cost:100, damage:25, range:100, rate:1200, color:'#FF6B6B',
           targets:['nh3','naoh_enemy'],
           targetsEq:{
             nh3:'NH₃ + HCl → NH₄Cl',
             naoh_enemy:'HCl + NaOH → NaCl + H₂O'
           }},
  naoh:  {name:'氢氧化钠塔',formula:'NaOH', cost:150, damage:30, range:110, rate:1000, color:'#4ECDC4',
           targets:['h2so4','ch3cooh','c2h5cooh'],
           targetsEq:{
             h2so4:'H₂SO₄ + 2NaOH → Na₂SO₄ + 2H₂O',
             ch3cooh:'CH₃COOH + NaOH → CH₃COONa + H₂O',
             c2h5cooh:'C₂H₅COOH + NaOH → C₂H₅COONa + H₂O'
           }},
  fe:    {name:'铁塔',     formula:'Fe',    cost:150, damage:40, range:90,  rate:1400, color:'#95A5A6',
           targets:['cu2','ag'], eq:'Fe + CuSO₄ → FeSO₄ + Cu'},
  bano32:{name:'硝酸钡塔', formula:'Ba(NO₃)₂', cost:160, damage:0,  range:100, rate:1500, color:'#E67E22',
           targets:['so4','h2so4'], eq:'Ba²⁺ + SO₄²⁻ → BaSO₄↓', solidify:200},
  kmno4: {name:'高锰酸钾塔',formula:'KMnO₄',cost:180, damage:30, range:115, rate:1000, color:'#8E44AD',
           targets:['so2','c2h5oh','c3h7oh','ch3oh'],
           targetsEq:{
             so2:'2KMnO₄ + 5SO₂ + 2H₂O → K₂SO₄ + 2MnSO₄ + 2H₂SO₄',
             c2h5oh:'C₂H₅OH + [O] → CH₃CHO + H₂O',
             c3h7oh:'C₃H₇OH + [O] → C₃H₆O + H₂O',
             ch3oh:'CH₃OH + [O] → HCHO + H₂O'
           }},
  carbon:{name:'活性炭塔', formula:'C',     cost:90,  damage:20, range:80,  rate:1500, color:'#2C3E50',
           targets:['co'], eq:'C(活性) 吸附有害气体'},

// ---- 有机反应塔(2-1、2-2 关)----
  br2_add:    {name:'溴水加成塔',formula:'Br₂',       cost:150,damage:22,range:120,rate:1000,color:'#FF4500',
             targets:['c2h4','c3h6'],               eq:'C₂H₄ + Br₂ → C₂H₄Br₂'},
  naoh_hydro: {name:'氢氧化钠水解塔',formula:'NaOH/H₂O',cost:220,damage:35,range:125,rate:850,color:'#B0E0E6',
             targets:['ch3cooc2h5','c2h5cooch3'],
             eq:'CH₃COOC₂H₅ + NaOH → CH₃COONa + C₂H₅OH'}
};

// ========== 敌人定义 ==========
const EDEF = {
  nh3:       {name:'氨气',     formula:'NH₃',   hp:80,  speed:2.0, reward:50,  dmg:1, color:'#2ECC71'},
  h2so4:     {name:'硫酸',     formula:'H₂SO₄', hp:120, speed:1.2, reward:80,  dmg:2, color:'#E74C3C'},
  naoh_enemy: {name:'氢氧化钠',formula:'NaOH',  hp:120, speed:1.2, reward:80,  dmg:2, color:'#1ABC9C'},
  so2:       {name:'二氧化硫',formula:'SO₂',   hp:100, speed:1.5, reward:60,  dmg:1, color:'#F1C40F'},
  cu2:       {name:'铜离子',   formula:'Cu²⁺',  hp:150, speed:1.0, reward:100, dmg:2, color:'#E67E22'},
  co:        {name:'一氧化碳',formula:'CO',    hp:100, speed:1.8, reward:70,  dmg:1, color:'#34495E'},
  ag:        {name:'银离子',   formula:'Ag⁺',   hp:130, speed:1.0, reward:90,  dmg:2, color:'#BDC3C7'},
  so4:       {name:'硫酸根',   formula:'SO₄²⁻', hp:500, speed:0.8, reward:120, dmg:3, color:'#8E44AD'},
// ---- 有机反应敌人(2-1、2-2 关)----
  c2h4:      {name:'乙烯',   formula:'C₂H₄',      hp:60, speed:2.0, reward:40, dmg:2, color:'#ADD8E6'},
  c3h6:      {name:'丙烯',   formula:'C₃H₆',      hp:80, speed:1.8, reward:50, dmg:2, color:'#90EE90'},
  c2h5oh:    {name:'乙醇',   formula:'C₂H₅OH',    hp:100,speed:1.9, reward:45, dmg:2, color:'#FFFFE0'},
  c3h7oh:    {name:'丙醇',   formula:'C₃H₇OH',    hp:120,speed:1.7, reward:55, dmg:3, color:'#E6E6FA'},
  ch3cooh:   {name:'乙酸',   formula:'CH₃COOH',   hp:130,speed:1.7, reward:50, dmg:3, color:'#FFD700'},
  c2h5cooh:  {name:'丙酸',   formula:'C₂H₅COOH',  hp:160,speed:1.6, reward:55, dmg:3, color:'#FFA500'},
  ch3cooc2h5:{name:'乙酸乙酯',formula:'CH₃COOC₂H₅',hp:180,speed:1.5, reward:60, dmg:3, color:'#FF69B4',onDeathTransform:'c2h5oh'},
  c2h5cooch3:{name:'丙酸甲酯',formula:'C₂H₅COOCH₃',hp:200,speed:1.4, reward:65, dmg:4, color:'#FF1493',onDeathTransform:'ch3oh'},
  ch3oh:     {name:'甲醇',   formula:'CH₃OH',     hp:80, speed:2.1, reward:35, dmg:2, color:'#E0F7FF'}
};

// ========== 关卡波次 ==========
function getWaves(level) {
  if (level === 1) return [
    {e:[{t:'nh3',c:5,i:800}]},
    {e:[{t:'nh3',c:8,i:700}]},
    {e:[{t:'nh3',c:6,i:600}]},
    {e:[{t:'nh3',c:4,i:800},{t:'h2so4',c:2,i:1200}]},
    {e:[{t:'h2so4',c:4,i:1000}]},
    {e:[{t:'nh3',c:8,i:600},{t:'h2so4',c:3,i:1000}]},
    {e:[{t:'h2so4',c:6,i:900}]},
    {e:[{t:'nh3',c:10,i:500},{t:'h2so4',c:5,i:800}]}
  ];
  if (level === 2) return [
    {e:[{t:'cu2',c:5,i:900}]},
    {e:[{t:'cu2',c:4,i:800},{t:'cu2',c:4,i:800,p:'2B'}]},
    {e:[{t:'cu2',c:8,i:600}]},
    {e:[{t:'cu2',c:5,i:700},{t:'ag',c:3,i:1000,p:'2B'}]},
    {e:[{t:'ag',c:6,i:800,p:'2B'},{t:'ag',c:6,i:800}]},
    {e:[{t:'so4',c:3,i:1200}]},
    {e:[{t:'so4',c:4,i:1000},{t:'so4',c:4,i:1000,p:'2B'}]},
    {e:[{t:'cu2',c:6,i:600},{t:'ag',c:4,i:800,p:'2B'},{t:'so4',c:3,i:1200}]},
    {e:[{t:'so4',c:6,i:900,p:'2B'},{t:'so4',c:6,i:900}]},
    {e:[{t:'cu2',c:8,i:500},{t:'ag',c:5,i:700,p:'2B'},{t:'so4',c:5,i:1000}]}
  ];
  if (level === 3) return [
    {e:[{t:'so2',c:6,i:700}]},
    {e:[{t:'so2',c:10,i:600}]},
    {e:[{t:'so2',c:12,i:500}]},
    {e:[{t:'so2',c:5,i:600},{t:'co',c:4,i:800}]},
    {e:[{t:'co',c:8,i:500}]},
    {e:[{t:'co',c:6,i:700},{t:'co',c:6,i:700}]},
    {e:[{t:'so2',c:8,i:500},{t:'co',c:5,i:600}]},
    {e:[{t:'so2',c:10,i:400},{t:'co',c:10,i:400}]},
    {e:[{t:'co',c:8,i:500},{t:'so2',c:8,i:500}]},
    {e:[{t:'co',c:12,i:350}]},
    {e:[{t:'so2',c:10,i:400},{t:'co',c:8,i:500},{t:'co',c:10,i:400}]}
  ];
  if (level === 4) return [
    {e:[{t:'c2h4',c:5,i:2000}]},
    {e:[{t:'c2h4',c:4,i:1800},{t:'c3h6',c:3,i:1800}]},
    {e:[{t:'c3h6',c:3,i:1600},{t:'c2h5oh',c:3,i:1600}]},
    {e:[{t:'c2h5oh',c:4,i:1500},{t:'c3h7oh',c:2,i:1500}]},
    {e:[{t:'c2h4',c:3,i:1400},{t:'c3h6',c:3,i:1400},{t:'c2h5oh',c:3,i:1400}]},
    {e:[{t:'c3h6',c:4,i:1300},{t:'c3h7oh',c:4,i:1300}]},
    {e:[{t:'c2h4',c:5,i:1200},{t:'c3h7oh',c:3,i:1200}]},
    {e:[{t:'c2h5oh',c:4,i:1100},{t:'c3h7oh',c:4,i:1100}]},
    {e:[{t:'c2h4',c:5,i:1000},{t:'c3h6',c:5,i:1000},{t:'c2h5oh',c:5,i:1000},{t:'c3h7oh',c:5,i:1000}]},
    {e:[{t:'c2h4',c:3,i:900},{t:'c3h6',c:3,i:900},{t:'c2h5oh',c:2,i:900},{t:'c3h7oh',c:2,i:900}]}
  ];
  if (level === 5) return [
    {e:[{t:'ch3cooh',c:5,i:2000}]},
    {e:[{t:'ch3cooh',c:4,i:1800},{t:'c2h5cooh',c:3,i:1800}]},
    {e:[{t:'c2h5cooh',c:3,i:1600},{t:'ch3cooc2h5',c:3,i:1600}]},
    {e:[{t:'ch3cooc2h5',c:4,i:1500},{t:'c2h5cooch3',c:2,i:1500}]},
    {e:[{t:'ch3cooh',c:3,i:1400},{t:'c2h5cooh',c:3,i:1400},{t:'ch3cooc2h5',c:2,i:1400},{t:'c2h5cooch3',c:2,i:1400}]},
    {e:[{t:'c2h5cooh',c:4,i:1300},{t:'c2h5cooch3',c:4,i:1300}]},
    {e:[{t:'ch3cooh',c:5,i:1200},{t:'ch3cooc2h5',c:5,i:1200}]},
    {e:[{t:'ch3cooh',c:4,i:1100},{t:'c2h5cooh',c:4,i:1100},{t:'ch3cooc2h5',c:3,i:1100},{t:'c2h5cooch3',c:3,i:1100}]},
    {e:[{t:'ch3cooh',c:4,i:1000},{t:'c2h5cooh',c:4,i:1000},{t:'ch3cooc2h5',c:4,i:1000},{t:'c2h5cooch3',c:4,i:1000}]},
    {e:[{t:'ch3cooh',c:3,i:900},{t:'c2h5cooh',c:3,i:900},{t:'ch3cooc2h5',c:3,i:900},{t:'c2h5cooch3',c:3,i:900}]}
  ];
  return [];
}

function getAvailableTowers(level) {
  if (level === 1) return ['hcl','naoh'];
  if (level === 2) return ['fe','bano32'];
  if (level === 3) return ['kmno4','carbon'];
  if (level === 4) return ['br2_add','kmno4'];
  if (level === 5) return ['naoh','naoh_hydro','kmno4'];
  if (level === 6) return ['hcl','naoh','fe','bano32','kmno4','carbon','br2_add','naoh_hydro'];
  return [];
}
function getLevelName(level) {
  return ['','1-1:酸碱中和','1-2:沉淀与置换','1-3:氧化还原',
          '2-1:烯烃加成与醇的氧化','2-2:羧酸与酯的水解'][level] || '';
}

// ========== 难度系统 ==========
let currentDifficulty = 'normal'; // 'easy' | 'normal'

const DIFFICULTY_SCALE = {
  easy: {
    towerCost:   0.7,   // 塔费用 × 0.7
    towerDmg:    1.4,   // 塔伤害 × 1.4(含 solidify)
    towerRange:  1.15,  // 射程 × 1.15
    towerRate:   0.8,   // CD × 0.8(攻速更快)
    enemyHp:     0.6,   // 敌人血量 × 0.6
    enemySpeed:  0.75,  // 敌人移速 × 0.75
    enemyDmg:    0.5,   // 到终点扣血 × 0.5(最低 1)
    initGoldAdd: 150,   // 初始金币额外加
    initLives:   30     // 初始生命
  },
  normal: {
    towerCost:   1,
    towerDmg:    1,
    towerRange:  1,
    towerRate:   1,
    enemyHp:     1,
    enemySpeed:  1,
    enemyDmg:    1,
    initGoldAdd: 0,
    initLives:   20
  }
};

function getTowerCost(type) {
  return Math.round(TDEF[type].cost * DIFFICULTY_SCALE[currentDifficulty].towerCost);
}

function setDifficulty(d) {
  currentDifficulty = d;
  document.getElementById('diffEasyBtn').className   = 'diff-btn' + (d === 'easy'   ? ' active-easy'   : '');
  document.getElementById('diffNormalBtn').className = 'diff-btn' + (d === 'normal' ? ' active-normal' : '');
}

// ========== 游戏状态 ==========
const gs = {
  level:1, gold:300, lives:100,
  towers:[], enemies:[], waveIndex:0, totalWaves:0, waves:[],
  spawnQueue:[], spawning:false, waveTimer:0,
  gameOver:false, victory:false, paused:false, speed:1,
  reactionsThisLevel:[], waitingForFirstWave:true
};
let selectedTowerType = null;
let selectedPlacedTower = null;
let mouseX = 0, mouseY = 0;
let msgTimer = null;

// ========== 工具函数 ==========
function dist(x1,y1,x2,y2) { return Math.sqrt((x1-x2)**2 + (y1-y2)**2); }

function isOnPath(x, y, level) {
  const paths = level===1 ? [PATH1] : level===2 ? [PATH2A,PATH2B] : level===3 ? [PATH3] : level===4 ? [PATH4] : level===5 ? [PATH5] : [PATH6A,PATH6B];
  for (const p of paths) {
    for (const pt of p) { if (dist(x,y,pt.x,pt.y) < 25) return true; }
  }
  return false;
}

// ========== 敌人逻辑 ==========
function spawnEnemy(type, level, pathKey) {
  const def = EDEF[type]; if (!def) return null;
  const sc = DIFFICULTY_SCALE[currentDifficulty];
  const hp = Math.round(def.hp * sc.enemyHp);
  const path = getPathByKey(level, pathKey);
  return {
    type, name:def.name, formula:def.formula,
    hp, maxHp:hp,
    speed:  def.speed * sc.enemySpeed,
    reward: def.reward,
    dmg:    Math.max(1, Math.round(def.dmg * sc.enemyDmg)),
    color:  def.color,
    path, pathIndex:0, x:path[0].x, y:path[0].y,
    alive:true, reachingEnd:false, hitFlash:0, floatingText:null
  };
}

function updateOneEnemy(e) {
  if (!e.alive) return 'dead';
  if (e.reachingEnd) return 'reached';
  const target = e.path[e.pathIndex];
  if (!target) { e.alive = false; e.reachingEnd = true; return 'reached'; }

  // PATH2B 支线敌人到达汇合点 (460, 220) 后,继续沿 PATH2A 方向前进
  if (e.path === PATH2B && e.pathIndex === e.path.length - 1
      && Math.abs(e.x - 460) < 4 && Math.abs(e.y - 220) < 4) {
    // 创建新数组,不修改 PATH2B 原数组
    e.path = [...PATH2B, {x:460, y:340}, {x:620, y:340}];
  }

  const dx = target.x - e.x, dy = target.y - e.y;
  const d = Math.sqrt(dx*dx + dy*dy);
  if (d < 2) { e.pathIndex++; }
  else { e.x += (dx/d) * e.speed; e.y += (dy/d) * e.speed; }
  if (e.hitFlash > 0) e.hitFlash--;
  if (e.floatingText) {
    e.floatingText.alpha -= 0.02;
    e.floatingText.yOffset -= 0.5;
    if (e.floatingText.alpha <= 0) e.floatingText = null;
  }
  return 'ok';
}

// ========== 塔逻辑 ==========
function createTower(type, x, y) {
  const def = TDEF[type]; if (!def) return null;
  const sc = DIFFICULTY_SCALE[currentDifficulty];
  return {
    type, x, y, level:0,
    damage:       Math.round(def.damage  * sc.towerDmg),
    range:        Math.round(def.range   * sc.towerRange),
    rate:         Math.round(def.rate    * sc.towerRate),
    lastFire:0, color: def.color,
    firing: false, fireAnimTime: 0, solidify: !!def.solidify,
    _solidifyVal: def.solidify ? Math.round(def.solidify * sc.towerDmg) : 0,
    formula: def.formula, name: def.name, eq: def.eq
  };
}

function towerCanAttack(tower, enemyType) {
  const def = TDEF[tower.type];
  return def && def.targets.includes(enemyType);
}

function towerDoAttack(tower, enemy) {
  const now = Date.now();
  if (now - tower.lastFire < tower.rate) return null;
  if (dist(tower.x, tower.y, enemy.x, enemy.y) > tower.range) return null;
  if (!towerCanAttack(tower, enemy.type)) return null;
  const def = TDEF[tower.type];
  tower.lastFire = now;
  tower.firing = true;
  tower.fireAnimTime = 15;
  // 获取反应式:优先使用targetsEq映射,否则使用默认eq
  const eq = (def.targetsEq && def.targetsEq[enemy.type]) ? def.targetsEq[enemy.type] : def.eq;
  if (def.solidify) {
    const dmg = tower._solidifyVal * (tower.level + 1);
    enemy.hp -= dmg;
    enemy.hitFlash = 8;
    enemy.floatingText = {text: eq + ' -' + dmg, alpha:1.0, yOffset:0};
  } else {
    enemy.hp -= tower.damage;
    enemy.hitFlash = 8;
    enemy.floatingText = {text: eq, alpha:1.0, yOffset:0};
  }
  if (enemy.hp <= 0) enemy.alive = false;
  return eq;
}

// ========== 高端塔绘制函数 ==========
function drawTowerShape(t) {
  const r = 16 + t.level * 4;
  ctx.save();
  // 1) 选中时的范围环(保留原逻辑)
  if (selectedPlacedTower === t) {
    ctx.beginPath(); ctx.arc(t.x, t.y, t.range, 0, Math.PI*2);
    ctx.strokeStyle = t.color + '60'; ctx.lineWidth = 1; ctx.stroke();
    ctx.fillStyle = t.color + '15'; ctx.fill();
  }
  // 2) 底座:六边形
  ctx.beginPath();
  for (let i = 0; i < 6; i++) {
    const a = Math.PI/6 + Math.PI*2*i/6;
    const method = i === 0 ? 'moveTo' : 'lineTo';
    ctx[method](t.x + r*Math.cos(a), t.y + r*Math.sin(a));
  }
  ctx.closePath();
  ctx.fillStyle = t.color; ctx.fill();
  ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5; ctx.stroke();

  // 3) 类型特征标识(化学式文字)
  ctx.fillStyle = '#FFF'; ctx.font = (r<18 ? 'bold 8px' : 'bold 9px') + ' sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(t.formula, t.x, t.y);

  // 4) 等级指示:弧形能量条(0级=120°,1级=240°,2级=360°)
  if (t.level >= 0) {
    const totalDeg = (t.level + 1) * 120; // 0级→120°, 1级→240°, 2级→360°
    const totalAngle = totalDeg * Math.PI / 180;
    // 弧线居中显示(12点钟方向为对称轴)
    const startAngle = -Math.PI / 2 - totalAngle / 2;
    const endAngle = startAngle + totalAngle;
    ctx.beginPath();
    ctx.arc(t.x, t.y, r + 5, startAngle, endAngle);
    ctx.strokeStyle = '#F39C12';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.stroke();

    // 端点高光
    if (t.level < 2) {
      // 0级和1级:在弧线末端画一个小圆点
      const tipX = t.x + (r + 5) * Math.cos(endAngle);
      const tipY = t.y + (r + 5) * Math.sin(endAngle);
      ctx.beginPath();
      ctx.arc(tipX, tipY, 2.5, 0, Math.PI * 2);
      ctx.fillStyle = '#F39C12';
      ctx.fill();
    } else {
      // 2级(满级):外圈高光环
      ctx.beginPath();
      ctx.arc(t.x, t.y, r + 7, 0, Math.PI * 2);
      ctx.strokeStyle = '#F39C1240';
      ctx.lineWidth = 1.5;
      ctx.stroke();
    }
  }
  ctx.restore();
}

function drawTower(t) {
  drawTowerShape(t);
  // 攻击动画:从塔中心到敌人的激光线段
  if (t.firing) {
    const p = 1 - t.fireAnimTime/15;
    const r = 16 + t.level * 4; // 局部变量,供下方使用
    // 找到最近的目标敌人
    let best = null, bestD = Infinity;
    for (const e of gs.enemies) {
      if (!e.alive) continue;
      if (!towerCanAttack(t, e.type)) continue;
      const d = dist(t.x, t.y, e.x, e.y);
      if (d < t.range && d < bestD) { bestD = d; best = e; }
    }
    ctx.save();
    if (best) {
      // 有目标:画激光
      ctx.strokeStyle = t.color + Math.floor((1-p)*180).toString(16).padStart(2,'0');
      ctx.lineWidth = 2 + (1-p)*2;
      ctx.beginPath(); ctx.moveTo(t.x, t.y); ctx.lineTo(best.x, best.y); ctx.stroke();
      // 命中闪光
      ctx.beginPath(); ctx.arc(best.x, best.y, 4 + (1-p)*8, 0, Math.PI*2);
      ctx.fillStyle = t.color + '80'; ctx.fill();
    } else {
      // 没有目标时的扩散环
      ctx.beginPath(); ctx.arc(t.x, t.y, r + p*(t.range), 0, Math.PI*2);
      ctx.strokeStyle = t.color + Math.floor((1-p)*80).toString(16).padStart(2,'0');
      ctx.lineWidth = 2; ctx.stroke();
    }
    ctx.restore();
  }
}

// ========== 高端敌人绘制函数 ==========
function drawEnemyShape(e) {
  const s = 14;
  ctx.save();
  ctx.translate(e.x, e.y);

  // 受击闪烁:黄色缩放
  if (e.hitFlash > 0) {
    const shake = 1 - e.hitFlash / 8;
    ctx.scale(1 + shake * 0.25, 1 + shake * 0.25);
    ctx.fillStyle = '#FFD700';
  } else {
    ctx.fillStyle = e.color;
  }

  // 统一三角形
  ctx.beginPath();
  ctx.moveTo(0, -s);
  ctx.lineTo(s, s * 0.7);
  ctx.lineTo(-s, s * 0.7);
  ctx.closePath();
  ctx.fill();
  ctx.strokeStyle = 'rgba(0,0,0,0.3)';
  ctx.lineWidth = 1.2;
  ctx.stroke();

  // 中心高光
  ctx.fillStyle = 'rgba(255,255,255,0.15)';
  ctx.beginPath();
  ctx.arc(0, -s * 0.15, s * 0.35, 0, Math.PI * 2);
  ctx.fill();

  ctx.restore();

  // 化学式居中于三角形
  ctx.save();
  ctx.fillStyle = '#FFF';
  ctx.font = 'bold 9px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.shadowColor = 'rgba(0,0,0,0.5)';
  ctx.shadowBlur = 2;
  ctx.fillText(e.formula, e.x, e.y + 2);
  ctx.restore();
}

function drawEnemy(e) {
  if (!e.alive) return;
  drawEnemyShape(e);

  // 高端血条:加外框 + 渐变颜色
  const bw = 30, bh = 4, bx = e.x - bw/2, by = e.y - 12 - 10;
  // 外框
  ctx.fillStyle = '#00000040'; ctx.fillRect(bx-0.5, by-0.5, bw+1, bh+1);
  ctx.fillStyle = '#DDD'; ctx.fillRect(bx, by, bw, bh);
  // 颜色渐变(分段绘制)
  const hpRatio = e.hp / e.maxHp;
  let barColor;
  if (hpRatio > 0.5) barColor = '#2ECC71';
  else if (hpRatio > 0.25) barColor = '#F39C12';
  else barColor = '#E74C3C';
  // 血量光效
  ctx.fillStyle = barColor; ctx.fillRect(bx, by, bw * hpRatio, bh);
  // 血条内高光
  ctx.fillStyle = 'rgba(255,255,255,0.35)';
  ctx.fillRect(bx, by, bw * hpRatio, bh * 0.5);

  // 浮动文字(反应式)
  if (e.floatingText) {
    ctx.save(); ctx.globalAlpha = e.floatingText.alpha;
    ctx.fillStyle = '#E74C3C'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText(e.floatingText.text, e.x, e.y + 12 + 28 + e.floatingText.yOffset);
    ctx.restore();
  }
}

// ========== 波次管理 ==========
function loadWave(idx) {
  if (idx >= gs.waves.length) return;
  const wave = gs.waves[idx];
  gs.spawnQueue = [];
  for (const g of wave.e) {
    for (let i=0; i<g.c; i++) {
      gs.spawnQueue.push({type: g.t, pathKey: g.p || '1', delay: i * g.i, spawned: false});
    }
  }
  gs.spawnQueue.sort((a,b) => a.delay - b.delay);
  gs.spawning = true;
  gs.waveTimer = 0;
  gs.waitingForFirstWave = false;
}

// ========== 无尽模式波次生成 ==========
function getEndlessWave(n) {
  // n: 波次序号(从0开始)
  // 敌人类型池按波次解锁
  const pool1 = ['nh3','h2so4'];                     // n<3
  const pool2 = [...pool1,'cu2','ag'];                // n<6
  const pool3 = [...pool2,'so2','co'];                // n<10
  const pool4 = [...pool3,'c2h4','c3h6','c2h5oh','c3h7oh']; // n<15
  const allPool = [...pool4,'so4','ch3cooh','c2h5cooh','ch3cooc2h5','c2h5cooch3','ch3oh'];
  let pool;
  if (n < 3) pool = pool1;
  else if (n < 6) pool = pool2;
  else if (n < 10) pool = pool3;
  else if (n < 15) pool = pool4;
  else pool = allPool;
  // 每波敌人数递增
  const countA = 6 + Math.floor(n / 3) * 2;
  const countB = 4 + Math.floor(n / 3) * 2;
  // 每10波加入so4
  const boss = (n > 0 && n % 10 === 0) ? [{t:'so4',c:1,i:0}] : [];
  // 随机构建本波敌人列表
  const pickType = () => pool[Math.floor(Math.random() * pool.length)];
  const enemiesA = [];
  const enemiesB = [];
  for (let i = 0; i < countA; i++) enemiesA.push({t:pickType(), p:'6A'});
  for (let i = 0; i < countB; i++) enemiesB.push({t:pickType(), p:'6B'});
  // 合并,加入boss
  const list = [...enemiesA.map(e=>({t:e.t,c:1,i:Math.floor(Math.random()*1500),p:e.p})),
              ...enemiesB.map(e=>({t:e.t,c:1,i:Math.floor(Math.random()*1500),p:e.p})),
              ...boss.map(e=>({...e,c:1,i:500,p:Math.random()>0.5?'6A':'6B'}))];
  // 合并同类同路径的敌人
  const merged = [];
  const keyMap = {};
  for (const e of list) {
    const key = e.t + '|' + (e.p||'6A');
    if (keyMap[key]) { keyMap[key].c++; }
    else { keyMap[key] = {t:e.t,c:1,i:e.i,p:e.p}; merged.push(keyMap[key]); }
  }
  return { e: merged };
}

function loadEndlessWave(idx) {
  const wave = getEndlessWave(idx);
  gs.spawnQueue = [];
  for (const g of wave.e) {
    for (let i = 0; i < g.c; i++) {
      gs.spawnQueue.push({type:g.t, pathKey:g.p||'6A', delay:i*g.i, spawned:false});
    }
  }
  gs.spawnQueue.sort((a,b)=>a.delay-b.delay);
  gs.spawning = true;
  gs.waveTimer = 0;
  gs.waitingForFirstWave = false;
}

function initEndless() {
  buildGrid(6);
  gs.level = 6;
  gs.isEndless = true;
  gs.gold  = 600 + DIFFICULTY_SCALE[currentDifficulty].initGoldAdd;
  gs.lives = DIFFICULTY_SCALE[currentDifficulty].initLives === 20 ? 100 : 150;
  gs.towers = [];
  gs.enemies = [];
  gs.gameOver = false;
  gs.victory = false;
  gs.waveIndex = 0;
  gs.spawning = false;
  gs.spawnQueue = [];
  gs.waitingForFirstWave = true;
  selectedTowerType = null;
  selectedPlacedTower = null;
  renderBottomBar();
  updateInfoBar();
  showMsg('无尽模式:双线挑战!');
  setTimeout(()=>loadEndlessWave(0), 1500);
}

// ========== 主更新 ==========
function update(dt) {
  if (gs.gameOver || gs.victory) return;

  // 生成敌人
  if (gs.spawning && gs.spawnQueue.length > 0) {
    gs.waveTimer += dt * gs.speed * 1000;
    let allDone = true;
    for (const item of gs.spawnQueue) {
      if (!item.spawned && gs.waveTimer >= item.delay) {
        const e = spawnEnemy(item.type, gs.level, item.pathKey);
        if (e) gs.enemies.push(e);
        item.spawned = true;
      }
      if (!item.spawned) allDone = false;
    }
    if (allDone) { gs.spawning = false; gs.spawnQueue = []; }
  }

  // 更新敌人
  for (let i=gs.enemies.length-1; i>=0; i--) {
    const e = gs.enemies[i];
    const status = updateOneEnemy(e);
    if (status === 'dead') {
      const def = EDEF[e.type];
      if (def.onDeathTransform) {
        // 酯类HP归零时转化为对应醇
        const newType = def.onDeathTransform;
        const newDef = EDEF[newType];
        e.type    = newType;
        e.name    = newDef.name;
        e.formula = newDef.formula;
        e.hp      = newDef.hp;
        e.maxHp   = newDef.hp;
        e.speed   = newDef.speed;
        e.reward  = newDef.reward;
        e.dmg     = newDef.dmg;
        e.color   = newDef.color;
        e.alive   = true;
        e.hitFlash = 10;
        e.floatingText = {text:'→ '+newDef.name, alpha:1.0, yOffset:0};
        showMsg('🔄 '+newDef.name+' 生成!');
      } else {
        gs.gold += def.reward;
        gs.enemies.splice(i, 1);
      }
    } else if (status === 'reached') {
      gs.lives -= EDEF[e.type].dmg;
      gs.enemies.splice(i, 1);
      if (gs.lives <= 0) { gs.lives = 0; gs.gameOver = true; return; }
    }
  }

  // 检查波次完成
  if (!gs.spawning && gs.enemies.length === 0 && !gs.waitingForFirstWave) {
    if (gs.isEndless) {
      // 无尽模式:无限推进
      gs.waveIndex++;
      gs.waitingForFirstWave = true;
      setTimeout(() => loadEndlessWave(gs.waveIndex), 1500);
    } else {
      gs.waveIndex++;
      if (gs.waveIndex >= gs.totalWaves) {
        // 所有波次已完成,触发胜利
        gs.victory = true;
        setTimeout(showSummary, 800);
      } else {
        // 加载下一波
        gs.waitingForFirstWave = true;
        setTimeout(() => loadWave(gs.waveIndex), 2000);
      }
    }
  }

  // 塔攻击
  const now = Date.now();
  for (const t of gs.towers) {
    if (t.firing) { t.fireAnimTime--; if (t.fireAnimTime <= 0) t.firing = false; }
    if (t.firing) continue;
    for (const e of gs.enemies) {
      if (!e.alive) continue;
      const eq = towerDoAttack(t, e);
      if (eq) { gs.reactionsThisLevel.push(eq); break; }
    }
  }
}

// ========== 主渲染 ==========
function draw() {
  ctx.fillStyle = '#FFF'; ctx.fillRect(0, 0, W, H);
  // 选关界面时 _roadCells 未初始化,跳过游戏画面绘制
  if (_roadCells.length === 0) return;
  drawGridBackground();
  drawPath(gs.level);
  for (const t of gs.towers) drawTower(t);
  for (const e of gs.enemies) drawEnemy(e);

  // 放置模式:显示格子边框
  if (selectedTowerType && mouseY < 400) {
    const def = TDEF[selectedTowerType];
    const hover = getHoverCell(mouseX, mouseY);
    ctx.save();
    for (let r = 0; r < ROWS; r++) {
      for (let c = 0; c < COLS; c++) {
        if (_grid[r][c]) continue;
        const x = c * CELL, y = r * CELL;
        const cellCX = 20 + c*CELL, cellCY = 20 + r*CELL;
        const occupied = cellOccupied(cellCX, cellCY);
        if (cellCX === hover?.cx && cellCY === hover?.cy) {
          ctx.fillStyle = '#3498DB40';
          ctx.fillRect(x, y, CELL, CELL);
          ctx.strokeStyle = '#FFFFFF';
          ctx.lineWidth = 2; ctx.setLineDash([]);
          ctx.strokeRect(x + 1, y + 1, CELL - 2, CELL - 2);
          ctx.beginPath(); ctx.arc(cellCX, cellCY, def.range, 0, Math.PI*2);
          ctx.strokeStyle = def.color + '60'; ctx.lineWidth = 1.5;
          ctx.setLineDash([4, 4]); ctx.stroke();
          ctx.beginPath(); ctx.arc(cellCX, cellCY, 14, 0, Math.PI*2);
          ctx.fillStyle = def.color + '70'; ctx.fill();
          ctx.fillStyle = '#FFF'; ctx.font = 'bold 8px sans-serif';
          ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
          ctx.fillText(def.formula, cellCX, cellCY);
        } else if (occupied) {
          ctx.strokeStyle = '#CCC'; ctx.lineWidth = 1;
          ctx.setLineDash([3, 3]);
          ctx.strokeRect(x + 1, y + 1, CELL - 2, CELL - 2);
        } else {
          ctx.strokeStyle = '#B0D4F1'; ctx.lineWidth = 1;
          ctx.setLineDash([4, 4]);
          ctx.strokeRect(x + 1, y + 1, CELL - 2, CELL - 2);
        }
      }
    }
    ctx.restore();
  }

  // 选中塔的操作按钮
  if (selectedPlacedTower && selectedPlacedTower.y < 400) {
    const t = selectedPlacedTower;
    if (t.level < 2) {
      const upCost = getTowerCost(t.type) + (t.level === 0 ? 80 : 160);
      if (gs.gold >= upCost) {
        ctx.fillStyle = '#27AE60'; ctx.fillRect(t.x-40, t.y-50, 80, 18);
        ctx.fillStyle = '#FFF'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center';
        ctx.fillText('升级 ¥' + upCost, t.x, t.y-37);
        t._btnUp = {x:t.x-40, y:t.y-50, w:80, h:18};
      }
    }
    const sellVal = Math.floor(getTowerCost(t.type) * 0.6);
    ctx.fillStyle = '#E74C3C'; ctx.fillRect(t.x+45, t.y-50, 60, 18);
    ctx.fillStyle = '#FFF'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText('出售 ¥' + sellVal, t.x+75, t.y-37);
    t._btnSell = {x:t.x+45, y:t.y-50, w:60, h:18};
  }

  // 游戏结束 / 胜利
  if (gs.gameOver) {
    ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#E74C3C'; ctx.font = 'bold 32px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText('游戏结束', W/2, H/2);
    ctx.fillStyle = '#FFF'; ctx.font = '14px sans-serif';
    ctx.fillText('点击任意位置重新开始', W/2, H/2+30);
  }
  if (gs.victory && !gs.gameOver) {
    ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#27AE60'; ctx.font = 'bold 32px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText('关卡完成!', W/2, H/2);
  }
}

// ========== UI 更新 ==========
function updateInfoBar() {
  document.getElementById('levelInfo').textContent = getLevelName(gs.level);
  document.getElementById('goldInfo').textContent = '💰 ¥' + gs.gold;
  document.getElementById('livesInfo').textContent = '❤️ ' + gs.lives;
  document.getElementById('waveInfo').textContent = '波次: ' + Math.min(gs.waveIndex, gs.totalWaves) + '/' + gs.totalWaves;
  // 实时同步底部防御塔按钮的可购买状态
  const bar = document.getElementById('bottomBar');
  if (bar) {
    const btns = bar.querySelectorAll('.tower-btn');
    const available = getAvailableTowers(gs.level);
      btns.forEach((btn, i) => {
        const tt = available[i];
        if (!tt) return;
        const cost = getTowerCost(tt);
        btn.style.opacity = gs.gold >= cost ? '1' : '0.4';
      });
  }
}

function showMsg(text) {
  const box = document.getElementById('msgBox');
  box.textContent = text; box.style.display = 'block';
  clearTimeout(msgTimer); msgTimer = setTimeout(() => { box.style.display = 'none'; }, 2000);
}

function renderBottomBar() {
  const bar = document.getElementById('bottomBar');
  const available = getAvailableTowers(gs.level);
  let html = '';
  for (const tt of available) {
    const def = TDEF[tt];
    const cost = getTowerCost(tt);
    const sel = selectedTowerType === tt ? 'selected' : '';
    const afford = gs.gold >= cost;
    html += '<button class="tower-btn ' + sel + '" style="opacity:' + (afford?1:0.4) + ';border-color:' + def.color + '" onclick="selectTower(\'' + tt + '\')">'
          + '<span style="color:' + def.color + ';font-weight:bold;">' + def.formula + '</span>'
          + '<span class="cost"> ¥' + cost + '</span></button>';
  }
  bar.innerHTML = html;
}

window.selectTower = function(type) {
  if (gs.gold < getTowerCost(type)) { showMsg('金币不足!'); return; }
  selectedTowerType = (selectedTowerType === type) ? null : type;
  selectedPlacedTower = null;
  renderBottomBar();
};

function showSummary() {
  const eqs = [...new Set(gs.reactionsThisLevel)];
  let msg;
  if (gs.isEndless) {
    msg = '🎉 无尽模式结束!\n\n到达第 ' + (gs.waveIndex) + ' 波\n触发的反应:\n';
  } else {
    msg = '🎉 第' + gs.level + '关完成!\n\n触发的反应:\n';
  }
  if (eqs.length === 0) msg += '(无)';
  else msg += eqs.map(e => '• ' + e).join('\n');
  if (!gs.isEndless) unlockLevel(gs.level);
  alert(msg);
  document.getElementById('startScreen').style.display = 'flex';
  document.getElementById('gameBox').style.display = 'none';
  renderLevelButtons();
}

// ========== 选关界面逻辑 ==========
function getUnlocked() {
  return Math.max(1, parseInt(localStorage.getItem('chemtd_unlocked')) || 1);
}
function unlockLevel(n) {
  const cur = getUnlocked();
  if (n + 1 > cur) localStorage.setItem('chemtd_unlocked', n + 1);
}
function startLevel(n) {
  document.getElementById('startScreen').style.display = 'none';
  document.getElementById('gameBox').style.display = 'flex';
  initLevel(n);
}
function startEndless() {
  document.getElementById('startScreen').style.display = 'none';
  document.getElementById('gameBox').style.display = 'flex';
  initEndless();
}
function renderLevelButtons() {
  const sel = document.getElementById('levelSelect');
  sel.innerHTML = '';
  const unlocked = getUnlocked();
  const levels = [
    { key:1, label:'1-1\u3000酸碱中和' },
    { key:2, label:'1-2\u3000沉淀与置换' },
    { key:3, label:'1-3\u3000氧化还原' },
    { key:4, label:'2-1\u3000烯烃加成与醇的氧化' },
    { key:5, label:'2-2\u3000羧酸与酯的水解' },
    { key:6, label:'🌟\u3000双线无尽挑战' }
  ];
  for (const lv of levels) {
    const btn = document.createElement('div');
    btn.className = 'lvl-btn';
    const parts = lv.label.split(' ');
    btn.innerHTML = '<div style="font-size:11px;opacity:0.7;">' + parts[0] + '</div><div>' + parts[1] + '</div>';
    if (lv.key <= 5 && lv.key <= unlocked) {
      btn.onclick = function() { startLevel(lv.key); };
    } else if (lv.key === 6) {
      btn.onclick = function() { startEndless(); };
    } else {
      btn.classList.add('locked');
    }
    sel.appendChild(btn);
  }
}

// ========== 初始化关卡 ==========
function initLevel(level) {
  gs.isEndless = false;
  buildGrid(level);
  gs.level = level;
  gs.gold  = 300 + (level-1) * 100 + DIFFICULTY_SCALE[currentDifficulty].initGoldAdd;
  gs.lives = DIFFICULTY_SCALE[currentDifficulty].initLives;
  gs.towers = []; gs.enemies = [];
  gs.reactionsThisLevel = [];
  gs.gameOver = false; gs.victory = false;
  gs.waveIndex = 0; gs.spawning = false; gs.spawnQueue = [];
  gs.waves = getWaves(level);
  gs.totalWaves = gs.waves.length;
  gs.waitingForFirstWave = true;
  selectedTowerType = null; selectedPlacedTower = null;
  renderBottomBar();
  updateInfoBar();
  showMsg('第' + level + '关:' + getLevelName(level));
  setTimeout(() => loadWave(0), 1500);
}

// ========== 鼠标 / 点击 ==========
canvas.addEventListener('click', function(e) {
  const { x, y } = screenToGame(e.clientX, e.clientY);
  if (gs.gameOver) { location.reload(); return; }

  // 升级/出售按钮
  if (selectedPlacedTower) {
    const t = selectedPlacedTower;
    if (t._btnUp && x>=t._btnUp.x && x<=t._btnUp.x+t._btnUp.w && y>=t._btnUp.y && y<=t._btnUp.y+t._btnUp.h) {
      const upCost = getTowerCost(t.type) + (t.level === 0 ? 80 : 160);
      if (gs.gold >= upCost && t.level < 2) {
        gs.gold -= upCost; t.level++;
        t.damage = TDEF[t.type].damage + t.level * 15;
        t.range = TDEF[t.type].range + t.level * 20;
        t.rate = TDEF[t.type].rate - t.level * 200;
        t._btnUp = null;
      }
      return;
    }
    if (t._btnSell && x>=t._btnSell.x && x<=t._btnSell.x+t._btnSell.w && y>=t._btnSell.y && y<=t._btnSell.y+t._btnSell.h) {
      gs.gold += Math.floor(TDEF[t.type].cost * 0.6);
      gs.towers = gs.towers.filter(tt => tt !== t);
      selectedPlacedTower = null;
      return;
    }
  }

  // 放置塔(吸附到方格中心)
  if (selectedTowerType) {
    const hover = getHoverCell(mouseX, mouseY);
    if (!hover) {
      if (mouseY < 400) showMsg('请将塔放置在方格内!');
      return;
    }
    if (cellOccupied(hover.cx, hover.cy)) {
      showMsg('该方格已有塔!'); return;
    }
    const t = createTower(selectedTowerType, hover.cx, hover.cy);
    if (t) { gs.towers.push(t); gs.gold -= getTowerCost(selectedTowerType); }
    selectedTowerType = null;
    renderBottomBar();
    return;
  }

  // 选中已放置的塔
  if (!selectedTowerType) {
    let found = null;
    for (const t of gs.towers) {
      if (dist(x,y,t.x,t.y) < 20) { found = t; break; }
    }
    selectedPlacedTower = (selectedPlacedTower === found) ? null : found;
  }
});

canvas.addEventListener('mousemove', function(e) {
  const { x, y } = screenToGame(e.clientX, e.clientY);
  mouseX = x; mouseY = y;
});

document.addEventListener('keydown', function(e) {
  if (e.code === 'Space') { gs.paused = !gs.paused; e.preventDefault(); }
});

window.toggleSpeed = function() {
  gs.speed = gs.speed === 1 ? 2 : 1;
  document.getElementById('speedBtn').textContent = gs.speed === 1 ? '▶ 正常' : '▶▶ 快进';
};

// ========== 游戏主循环 ==========
let lastTime = 0;
function gameLoop(timestamp) {
  const dt = lastTime ? Math.min((timestamp - lastTime) / 1000, 0.05) : 0.016;
  lastTime = timestamp;
  if (!gs.paused) update(dt);
  updateInfoBar();
  draw();
  requestAnimationFrame(gameLoop);
}

// ========== 启动 ==========
document.getElementById('startScreen').style.display = 'none';
renderLevelButtons();
setDifficulty('normal'); // 初始化难度按钮高亮
requestAnimationFrame(gameLoop);

// ========== 介绍界面 ==========
function enterLevelSelect() {
  document.getElementById('introScreen').style.display = 'none';
  document.getElementById('startScreen').style.display = 'flex';
}

// ========== 跳关系统 ==========
const SKIP_LEVELS = [
  { key:1, tag:'1-1', label:'酸碱中和',         desc:'HCl · NaOH' },
  { key:2, tag:'1-2', label:'沉淀与置换',        desc:'Fe · Ba(NO₃)₂' },
  { key:3, tag:'1-3', label:'氧化还原',          desc:'KMnO₄ · C' },
  { key:4, tag:'2-1', label:'烯烃加成与醇的氧化', desc:'Br₂ · KMnO₄' },
  { key:5, tag:'2-2', label:'羧酸与酯的水解',    desc:'NaOH · NaOH/H₂O' },
  { key:6, tag:'∞',  label:'双线无尽挑战',       desc:'全塔混合', endless:true }
];

function openSkipOverlay() {
  const list = document.getElementById('skipLevelList');
  list.innerHTML = '';
  for (const lv of SKIP_LEVELS) {
    const btn = document.createElement('button');
    btn.className = 'skip-lvl-btn' + (lv.endless ? ' endless' : '');
    btn.innerHTML =
      '<span class="tag">' + lv.tag + '</span>' +
      '<span style="flex:1">' + lv.label + '</span>' +
      '<span style="font-size:11px;opacity:0.5;">' + lv.desc + '</span>';
    btn.onclick = function() {
      closeSkipOverlay();
      // 确保游戏界面显示
      document.getElementById('startScreen').style.display = 'none';
      document.getElementById('introScreen').style.display = 'none';
      document.getElementById('gameBox').style.display = 'flex';
      if (lv.endless) {
        initEndless();
      } else {
        initLevel(lv.key);
      }
    };
    list.appendChild(btn);
  }
  document.getElementById('skipOverlay').classList.add('open');
}

function closeSkipOverlay() {
  document.getElementById('skipOverlay').classList.remove('open');
}
</script>
</body>
</html>
posted @ 2026-05-30 15:16  Natho_nA  阅读(11)  评论(0)    收藏  举报