网友作品 #000224 · 提交于 2026/09/13
艾泽拉斯防线:网页塔防小游戏


1 / 2
作品简介
「艾泽拉斯防线」是一款联盟题材的网页塔防游戏,采用纯前端技术(HTML5 Canvas + 原生 JavaScript)实现,浏览器打开页面即可游玩,无需安装任何环境。
游戏包含艾尔文森林、西部荒野、暴风城下三大战役关卡,每关 10 波敌人,难度逐关递增,通关进度自动保存。玩家扮演联盟指挥官,在地图上建造防御塔、指挥圣骑士英雄移动作战,守住通往暴风城的大门,防止敌人突破要塞。
顶栏实时显示波次、金币、要塞生命与击杀数,配合波次横幅、浮动提示等反馈,整体为暗色联盟主题的完整游戏界面。
核心亮点
- 三大战役关卡:艾尔文森林、西部荒野、暴风城下依次解锁,每关 10 波敌人,通关进度存入本地,可随时清除重开
- 四种防御塔:瞭望箭塔(低造价快速单体)、奥术法师塔(高单体魔法伤害、专克护甲)、矮人火炮台(范围溅射、清理群怪)、冰霜符文塔(范围减速控制),支持升级与拆除,拆塔返还 70% 金币
- 圣骑士英雄系统:可左键指派移动、右键移动并取消建造,Q/W/E 三个主动技能,击杀获取经验升级(最高 6 级),阵亡后 12 秒复活;英雄靠近防御塔可提升其攻速
- 三种伤害类型:物理受护甲减免、魔法受抗性减免、神圣无视一切减免,敌人分亡灵、豺狼人、部落等阵营
- 完整对局控制:x1/x2/x3 倍速切换、空格暂停、音效开关、按 N 提前召唤下一波并按剩余秒数奖励金币
- 数据面板齐全:波次进度、金币、要塞生命、击杀数实时统计,开局界面展示关卡信息、起始金币与敌方阵容
技术与工具
- 技术方案:纯前端 HTML5 Canvas 渲染 + 原生 JavaScript,代码按 config / utils / defs / levels / entities / renderer / ui / game / main 等 11 个文件模块化组织,地图采用 25×15 网格
- 主循环设计:1/60 秒固定步长更新,支持 1~3 倍速;数值常量集中在一个配置文件,便于扩展关卡时统一调节
- 开发方式:零基础借助 AI 编程类工具(Trae/Code Buddy 等)完成,只需向 AI 描述需求与想法,即可获得开发流程提示并产出完整项目
适用场景
- 休闲娱乐:打开网页即可体验一局完整的塔防对局
- 编程教学案例:可作为展示「AI 辅助开发」流程的示例项目,说明零基础也能借助 AI 工具完成一个结构完整的网页游戏
原始提交内容(点开查看,可复制全文去问 AI)
Source Code Collection Project: wow-tower-defense
File Index
File Lang Description
1 index.html html Page skeleton and UI containers 2 css/style.css css Dark alliance theme styles 3 js/config.js js Global constants 4 js/utils.js js Math utils, Path, Store 5 js/defs.js js Tower / enemy / hero / skill data 6 js/levels.js js 3 campaign levels 7 js/entities.js js Enemy, Tower, Projectile, Hero, Effect 8 js/renderer.js js Canvas rendering 9 js/ui.js js DOM panels and overlays 10 js/game.js js Main loop, waves, input, sfx 11 js/main.js js Bootstrap entry index.html <!DOCTYPE html> <html lang=“zh-CN”> <head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1”> <title>艾泽拉斯防线 · 联盟塔防</title> <link rel=“stylesheet” href=“css/style.css”> </head> <body> <div id=“app”>
<header id=“topbar”> <div class=“brand”> <span class=“brand-mark”>⚔</span> <div class=“brand-text”> <div class=“brand-title”>艾泽拉斯防线</div> <div class=“brand-sub” id=“levelName”>准备出征</div> </div> </div>
<div class="stats">
<div class="stat">
<span class="stat-icon wave">☉</span>
<div><div class="stat-label">波次</div><div class="stat-value" id="statWave">0 / 10</div></div>
</div>
<div class="stat">
<span class="stat-icon gold">◆</span>
<div><div class="stat-label">金币</div><div class="stat-value gold-text" id="statGold">0</div></div>
</div>
<div class="stat">
<span class="stat-icon life">♜</span>
<div><div class="stat-label">要塞生命</div><div class="stat-value life-text" id="statLives">20</div></div>
</div>
<div class="stat">
<span class="stat-icon kill">☠</span>
<div><div class="stat-label">击杀</div><div class="stat-value" id="statKills">0</div></div>
</div>
</div>
<div class="controls">
<button id="btnSpeed" class="btn" title="切换游戏速度">速度 x1</button>
<button id="btnPause" class="btn" title="暂停 / 继续(空格)">暂停</button>
<button id="btnMute" class="btn" title="音效开关">♪ 开</button>
<button id="btnMenu" class="btn ghost" title="返回主菜单">菜单</button>
</div>
</header>
<main id=“stage”> <div id=“canvasWrap”> <canvas id=“game” width=“1000” height=“600”></canvas> <div id=“toast” class=“toast”></div> <div id=“waveBanner” class=“wave-banner”></div> </div>
<aside id="sidebar">
<section class="panel">
<h3>防御工事 <span class="hint">按 1-4 快捷选择</span></h3>
<div id="shop" class="shop"></div>
</section>
<section class="panel" id="towerPanel">
<h3>建筑详情</h3>
<div id="towerInfo" class="tower-info empty">在地图上选择一座防御塔</div>
<div class="row">
<button id="btnUpgrade" class="btn primary" disabled>升级 (U)</button>
<button id="btnSell" class="btn danger" disabled>拆除 (S)</button>
</div>
</section>
<section class="panel">
<h3>作战提示</h3>
<ul class="tips">
<li><b>左键</b> 空地 = 建造 / 移动英雄</li>
<li><b>右键</b> = 移动英雄并取消建造</li>
<li><b>Q / W / E</b> 释放圣骑士技能</li>
<li><b>空格</b> 暂停,<b>N</b> 提前召唤下一波</li>
<li>英雄靠近防御塔可提升其攻速</li>
</ul>
</section>
</aside>
</main>
<footer id=“bottombar”> <div id=“heroPanel” class=“hero-panel”></div> <div id=“skillbar” class=“skillbar”></div> <div id=“wavePanel” class=“wave-panel”> <button id=“btnNextWave” class=“btn primary big”>开始第 1 波</button> <div id=“prepTimer” class=“prep-timer”></div> </div> </footer> </div>
<div id=“overlay” class=“overlay”> <div id=“overlayContent” class=“overlay-content”></div> </div>
<script src=“js/config.js”></script> <script src=“js/utils.js”></script> <script src=“js/defs.js”></script> <script src=“js/levels.js”></script> <script src=“js/entities.js”></script> <script src=“js/renderer.js”></script> <script src=“js/ui.js”></script> <script src=“js/game.js”></script> <script src=“js/main.js”></script> </body> </html> css/style.css /* 艾泽拉斯防线 · 联盟塔防 — 样式表 */
:root { –bg-0: #0b1020; –bg-1: #131a30; –bg-2: #1b2440; –panel: rgba(18, 25, 46, 0.92); –line: #2f3c63; –line-soft: #263251; –text: #e8eaf3; –text-dim: #98a2c0; –gold: #f0c060; –gold-dim: #a98432; –blue: #4a8fe0; –green: #6fcf72; –red: #e05a4f; –purple: #a06fe0; –cyan: #5fd6f5; }
- { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: radial-gradient(1200px 700px at 50% -10%, rgba(40, 58, 110, .55) 0%, transparent 62%), linear-gradient(160deg, #0a0f1e 0%, #131a30 60%, #0a0f1e 100%); background-attachment: fixed; color: var(–text); font-family: “Segoe UI”, “PingFang SC”, “Microsoft YaHei”, “Noto Sans SC”, sans-serif; min-height: 100vh; }
body { overflow-x: hidden; }
#app { max-width: 1340px; margin: 0 auto; padding: 12px 16px 20px; }
/* ––––– 顶部栏 ––––– */ #topbar { display: flex; align-items: center; gap: 18px; padding: 10px 16px; background: linear-gradient(180deg, rgba(30, 40, 70, 0.95), rgba(16, 22, 42, 0.95)); border: 1px solid var(–line); border-radius: 10px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.06); flex-wrap: wrap; }
.brand { display: flex; align-items: center; gap: 10px; min-width: 190px; } .brand-mark { font-size: 26px; color: var(–gold); text-shadow: 0 0 12px rgba(240, 192, 96, 0.55); } .brand-title { font-size: 17px; font-weight: 700; letter-spacing: 1px; color: var(–gold); } .brand-sub { font-size: 12px; color: var(–text-dim); }
.stats { display: flex; gap: 22px; flex: 1; flex-wrap: wrap; } .stat { display: flex; align-items: center; gap: 8px; } .stat-icon { font-size: 18px; line-height: 1; } .stat-icon.wave { color: var(–cyan); } .stat-icon.gold { color: var(–gold); } .stat-icon.life { color: var(–red); } .stat-icon.kill { color: var(–text-dim); } .stat-label { font-size: 11px; color: var(–text-dim); } .stat-value { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; } .gold-text { color: var(–gold); } .life-text { color: #ff8a7a; }
.controls { display: flex; gap: 8px; }
/* ––––– 按钮 ––––– */ .btn { background: linear-gradient(180deg, #2a3559 0%, #1d2745 100%); color: var(–text); border: 1px solid var(–line); border-radius: 6px; padding: 7px 13px; font-size: 13px; font-family: inherit; cursor: pointer; transition: all .15s ease; white-space: nowrap; } .btn:hover:not(:disabled) { border-color: var(–gold-dim); background: #2c3a63; transform: translateY(-1px); } .btn:active:not(:disabled) { transform: translateY(0); } .btn:disabled { opacity: .4; cursor: not-allowed; } .btn.primary { background: linear-gradient(180deg, #c8963a, #8f6420); border-color: #e0b558; color: #221703; font-weight: 700; } .btn.primary:hover:not(:disabled) { background: linear-gradient(180deg, #e0ad4a, #a5762a); } .btn.danger { background: linear-gradient(180deg, #6a2a26, #4a1c19); border-color: #8f3a33; } .btn.danger:hover:not(:disabled) { background: #7d3630; } .btn.ghost { background: transparent; } .btn.big { padding: 11px 22px; font-size: 15px; }
/* ––––– 主舞台 ––––– */ #stage { display: flex; gap: 14px; margin-top: 12px; align-items: flex-start; }
#canvasWrap { position: relative; flex: 1; min-width: 0; }
#game { display: block; width: 100%; height: auto; max-width: 1000px; background: #22331f; border: 1px solid var(–line); border-radius: 10px; box-shadow: 0 10px 32px rgba(0, 0, 0, .55); cursor: crosshair; }
/* 浮动提示 */ .toast { position: absolute; left: 50%; top: 14px; transform: translateX(-50%) translateY(-12px); background: rgba(10, 14, 28, .92); border: 1px solid var(–line); color: var(–text); padding: 8px 16px; border-radius: 6px; font-size: 13px; opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; } .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .toast.warn { border-color: var(–red); color: #ffb3ab; } .toast.good { border-color: var(–green); color: #b6f0b8; }
.wave-banner { position: absolute; left: 50%; top: 42%; transform: translate(-50%, -50%) scale(.85); font-size: 42px; font-weight: 800; letter-spacing: 6px; color: var(–gold); text-shadow: 0 0 26px rgba(240, 192, 96, .7), 0 4px 10px #000; opacity: 0; pointer-events: none; transition: opacity .3s, transform .5s; } .wave-banner.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
/* ––––– 侧栏 ––––– */ #sidebar { width: 296px; flex: 0 0 296px; display: flex; flex-direction: column; gap: 12px; }
.panel { background: var(–panel); border: 1px solid var(–line); border-radius: 10px; padding: 12px 14px; } .panel h3 { margin: 0 0 10px; font-size: 13px; letter-spacing: 1px; color: var(–gold); border-bottom: 1px solid var(–line-soft); padding-bottom: 7px; display: flex; justify-content: space-between; align-items: baseline; } .panel h3 .hint { font-size: 10px; color: var(–text-dim); font-weight: 400; letter-spacing: 0; }
.shop { display: flex; flex-direction: column; gap: 7px; }
.shop-card { display: flex; gap: 10px; align-items: center; padding: 8px 10px; border: 1px solid var(–line-soft); border-radius: 8px; background: rgba(10, 15, 30, .5); cursor: pointer; transition: all .14s ease; } .shop-card:hover { border-color: var(–gold-dim); background: rgba(30, 42, 74, .7); } .shop-card.active { border-color: var(–gold); background: rgba(60, 46, 18, .75); box-shadow: 0 0 0 1px rgba(240, 192, 96, .35) inset; } .shop-card.poor { opacity: .55; }
.sc-icon { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 7px; display: flex; align-items: center; justify-content: center; font-size: 19px; border: 1px solid rgba(255, 255, 255, .16); } .sc-body { flex: 1; min-width: 0; } .sc-name { font-size: 13px; font-weight: 600; display: flex; justify-content: space-between; gap: 6px; } .sc-cost { color: var(–gold); font-variant-numeric: tabular-nums; } .sc-desc { font-size: 10.5px; color: var(–text-dim); margin-top: 2px; line-height: 1.35; } .sc-key { width: 18px; height: 18px; flex: 0 0 18px; border: 1px solid var(–line); border-radius: 4px; font-size: 10px; display: flex; align-items: center; justify-content: center; color: var(–text-dim); }
.tower-info.empty { color: var(–text-dim); font-size: 12px; text-align: center; padding: 8px 0; } .ti-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .ti-name { font-size: 14px; font-weight: 700; } .ti-lv { font-size: 11px; color: var(–gold); border: 1px solid var(–gold-dim); border-radius: 4px; padding: 1px 6px; } .ti-row { display: flex; justify-content: space-between; font-size: 12px; padding: 3px 0; border-bottom: 1px dashed var(–line-soft); } .ti-row span:first-child { color: var(–text-dim); } .ti-row .up { color: var(–green); margin-left: 6px; } .ti-total { font-size: 11px; color: var(–text-dim); margin-top: 7px; }
.row { display: flex; gap: 8px; margin-top: 10px; } .row .btn { flex: 1; }
.tips { margin: 0; padding-left: 16px; font-size: 11.5px; color: var(–text-dim); line-height: 1.75; } .tips b { color: var(–text); }
/* ––––– 底部栏 ––––– */ #bottombar { display: flex; align-items: center; gap: 16px; margin-top: 12px; padding: 10px 16px; background: var(–panel); border: 1px solid var(–line); border-radius: 10px; flex-wrap: wrap; }
.hero-panel { display: flex; align-items: center; gap: 11px; min-width: 268px; } .hp-avatar { width: 46px; height: 46px; flex: 0 0 46px; border-radius: 8px; background: linear-gradient(160deg, #d8b456, #8f6a1e); border: 1px solid #f0cf82; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #2a1c02; } .hp-avatar.down { filter: grayscale(1) brightness(.55); } .hp-info { flex: 1; min-width: 0; } .hp-top { display: flex; align-items: baseline; gap: 7px; } .hp-name { font-size: 13px; font-weight: 700; } .hp-lv { font-size: 11px; color: var(–gold); border: 1px solid var(–gold-dim); border-radius: 4px; padding: 0 5px; }
.bar { position: relative; height: 11px; background: rgba(0, 0, 0, .5); border: 1px solid var(–line-soft); border-radius: 3px; overflow: hidden; margin-top: 3px; } .bar > i { display: block; height: 100%; width: 0; transition: width .18s linear; } .bar.hp > i { background: linear-gradient(90deg, #4fbf5a, #8ee08f); } .bar.mp > i { background: linear-gradient(90deg, #3f7fd8, #77b6f5); } .bar.xp > i { background: linear-gradient(90deg, #a06fe0, #d0a8f5); } .bar .bar-text { position: absolute; inset: 0; font-size: 9px; line-height: 9px; text-align: center; color: #fff; text-shadow: 0 0 3px #000; } .bar.small { height: 8px; } .bar.small .bar-text { line-height: 7px; font-size: 8px; }
.skillbar { display: flex; gap: 9px; } .skill { position: relative; width: 52px; height: 52px; border: 1px solid var(–line); border-radius: 8px; background: rgba(10, 15, 30, .7); display: flex; align-items: center; justify-content: center; font-size: 22px; cursor: pointer; overflow: hidden; transition: all .14s; } .skill:hover:not(.locked) { border-color: var(–gold); transform: translateY(-2px); } .skill.locked { opacity: .38; cursor: not-allowed; } .skill.nomana .skill-icon { filter: grayscale(1) brightness(.6); } .skill.ready { box-shadow: 0 0 0 1px rgba(240, 192, 96, .5), 0 0 14px rgba(240, 192, 96, .28); } .skill.active { box-shadow: 0 0 0 2px var(–gold), 0 0 18px rgba(240, 192, 96, .6); } .skill-key { position: absolute; top: 1px; left: 3px; font-size: 10px; color: var(–gold); font-weight: 700; } .skill-cost { position: absolute; bottom: 1px; right: 3px; font-size: 9px; color: #7fb2f0; } .skill-cd { position: absolute; inset: 0; background: rgba(4, 8, 18, .78); display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; color: #cfe0ff; } .skill-lv { position: absolute; bottom: 1px; left: 3px; font-size: 9px; color: var(–text-dim); }
.wave-panel { margin-left: auto; text-align: center; } .prep-timer { font-size: 11.5px; color: var(–text-dim); margin-top: 5px; }
/* ––––– 遮罩层 ––––– */ .overlay { position: fixed; inset: 0; background: radial-gradient(circle at 50% 40%, rgba(18, 26, 50, .95), rgba(4, 7, 16, .98)); display: flex; align-items: center; justify-content: center; z-index: 50; padding: 24px; overflow-y: auto; } .overlay.hidden { display: none; }
.overlay-content { max-width: 880px; width: 100%; background: linear-gradient(180deg, rgba(28, 37, 68, .96), rgba(14, 20, 38, .96)); border: 1px solid var(–line); border-radius: 14px; padding: 28px 32px; box-shadow: 0 24px 60px rgba(0, 0, 0, .7); text-align: center; }
.oc-title { font-size: 32px; font-weight: 800; letter-spacing: 3px; color: var(–gold); text-shadow: 0 0 24px rgba(240, 192, 96, .45); margin-bottom: 6px; } .oc-sub { color: var(–text-dim); font-size: 13.5px; margin-bottom: 22px; line-height: 1.7; } .oc-actions { display: flex; gap: 12px; justify-content: center; margin-top: 22px; flex-wrap: wrap; }
.level-grid { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; } .level-card { width: 236px; border: 1px solid var(–line); border-radius: 10px; padding: 14px; background: rgba(10, 15, 30, .6); cursor: pointer; text-align: left; transition: all .16s; } .level-card:hover:not(.locked) { border-color: var(–gold); transform: translateY(-3px); box-shadow: 0 8px 22px rgba(0, 0, 0, .5); } .level-card.locked { opacity: .45; cursor: not-allowed; } .level-card.cleared { border-color: rgba(111, 207, 114, .5); } .lc-banner { height: 74px; border-radius: 7px; margin-bottom: 10px; display: flex; align-items: center; justify-content: center; font-size: 30px; color: rgba(255, 255, 255, .9); text-shadow: 0 2px 8px rgba(0, 0, 0, .8); border: 1px solid rgba(255, 255, 255, .12); } .lc-index { font-size: 10.5px; color: var(–text-dim); letter-spacing: 1px; } .lc-name { font-size: 15px; font-weight: 700; margin: 2px 0 6px; } .lc-desc { font-size: 11.5px; color: var(–text-dim); line-height: 1.6; min-height: 56px; } .lc-foot { display: flex; justify-content: space-between; font-size: 11px; color: var(–gold); margin-top: 8px; } .lc-badge { color: var(–green); }
.brief-row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin: 16px 0; } .brief-chip { border: 1px solid var(–line-soft); border-radius: 8px; padding: 9px 15px; background: rgba(10, 15, 30, .55); min-width: 116px; } .bc-label { font-size: 10.5px; color: var(–text-dim); } .bc-value { font-size: 17px; font-weight: 700; color: var(–gold); margin-top: 2px; }
.enemy-list { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 8px; } .enemy-chip { display: flex; align-items: center; gap: 7px; border: 1px solid var(–line-soft); border-radius: 20px; padding: 5px 12px 5px 6px; background: rgba(10, 15, 30, .55); font-size: 12px; } .enemy-dot { width: 16px; height: 16px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, .25); }
.result-stats { display: flex; gap: 26px; justify-content: center; margin: 18px 0 4px; flex-wrap: wrap; } .rs-item { min-width: 92px; } .rs-value { font-size: 24px; font-weight: 800; color: var(–gold); } .rs-label { font-size: 11px; color: var(–text-dim); }
.hidden { display: none !important; }
@media (max-width: 1180px) { #stage { flex-direction: column; } #sidebar { width: 100%; flex: 1 1 auto; flex-direction: row; flex-wrap: wrap; } #sidebar .panel { flex: 1 1 260px; } } js/config.js ‘use strict’;
/**
- 全局常量:地图尺寸、节奏参数、伤害类型等。
- 所有数值集中在此,便于后续扩展关卡时统一调节。 */ const CFG = { // 地图 CELL: 40, GRID_W: 25, GRID_H: 15, CANVAS_W: 1000, CANVAS_H: 600,
// 经济 SELL_RATIO: 0.7, // 拆塔返还比例 PREP_TIME: 14, // 每波之间的备战秒数 EARLY_BONUS_PER_SEC: 2, // 提前召唤每剩余 1 秒奖励金币
// 英雄 HERO_RESPAWN: 12, // 英雄阵亡后复活倒计时 HERO_MAX_LEVEL: 6, HERO_AGGRO_RANGE: 1.9, // 敌人主动攻击英雄的距离(格) HERO_COMMAND_AURA: 3, // 英雄提升防御塔攻速的光环半径(格) HERO_XP_SHARE_RANGE: 999, // 击杀获取经验的范围(格,999=全图)
// 主循环 MAX_DT: 0.05, FIXED_STEP: 1 / 60, SPEEDS: [1, 2, 3],
STORAGE_KEY: ‘azeroth_td_progress_v1’, };
/** 伤害类型:物理受护甲减免,魔法受抗性减免,神圣无视一切减免 */ const DMG = { PHYSICAL: ‘physical’, MAGIC: ‘magic’, HOLY: ‘holy’, };
/** 阵营配色(用于渲染与 UI) */ const FACTION = { ally: { main: ‘#4a8fe0’, light: ‘#7fb6f5’, dark: ‘#1f3f6e’, name: ‘联盟’ }, undead: { main: ‘#8f6fd0’, light: ‘#c2a4f0’, dark: ‘#3a2b58’, name: ‘亡灵’ }, gnoll: { main: ‘#a98442’, light: ‘#d8b177’, dark: ‘#4a3a1c’, name: ‘豺狼人’ }, horde: { main: ‘#6aa84f’, light: ‘#9ed684’, dark: ‘#2c4a22’, name: ‘部落’ }, }; js/utils.js ‘use strict’;
/* ==================== 数学与通用工具 ==================== */
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); } function lerp(a, b, t) { return a + (b - a) * t; } function dist(ax, ay, bx, by) { return Math.hypot(bx - ax, by - ay); } function dist2(ax, ay, bx, by) { const dx = bx - ax, dy = by - ay; return dx * dx + dy * dy; } function rand(a, b) { return a + Math.random() * (b - a); } function randInt(a, b) { return Math.floor(rand(a, b + 1)); } function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function toRad(deg) { return deg * Math.PI / 180; }
/** 角度插值,避免绕圈 */ function angleLerp(from, to, t) { let d = ((to - from + Math.PI) % (Math.PI * 2)) - Math.PI; if (d < -Math.PI) d += Math.PI * 2; return from + d * t; }
/** 保留一位小数的紧凑数字显示 */ function fmtNum(n) { n = Math.round(n); if (Math.abs(n) >= 10000) return (n / 1000).toFixed(1) + ‘k’; return String(n); }
/** 确定性伪随机(用于地图装饰,保证每次进入同一关卡地形一致) */ function mulberry32(seed) { let a = seed >>> 0; return function () {