Boss系统重做:多管血条/锁血变身/专属变形动画/签名技能/rush随机装备老虎机

- 8个Boss全部重做:主攻击模式+1~2种随机小攻击+二/三阶段变形动画
- 新增5个雷霆战机参考Boss:太空原种/暗黑突击/激光钢琴/夺权者X/暗黑魔碟
- 多管血条(蓝/红/橙)+锁血转场+入场不掉血(b.y<b.ty-1免疫)
- 签名技能:花弹成长/高机动瞄准弹/21键4模式激光/银钉无人机+巨宽激光压制/锥形弹环+眼激光
- 所有激光发射点跟随Boss移动(bossBeam src机制)
- Boss挑战模式:随机装备3主武Lv3+3副武Lv3+2僚机Lv2,可选老虎机抽奖动画
- rush模式Boss血量x10,击破计时存档
- 武器修正:黎明重炮近炸引信26px/死光伤害x2/椭圆判定bossHit等
This commit is contained in:
JOJO 2026-08-05 20:18:46 +08:00
commit 58c5d9d35b
12 changed files with 2922 additions and 0 deletions

43
README.md Normal file
View File

@ -0,0 +1,43 @@
# 星尘突击队 STAR RAID
像素风 Roguelike 竖版射击游戏。原单文件版本已按功能拆分为多文件结构。
## 运行方式
浏览器直接打开 `index.html` 即可游玩;或启动本地服务器:
```bash
cd star-raid
python3 -m http.server 8000
# 访问 http://localhost:8000/
```
## 文件结构
```
star-raid/
├── index.html # 页面结构:画布 + 各界面覆盖层(标题/机库/图鉴/结算等)
├── css/
│ └── style.css # 全部样式:像素风 UI、覆盖层、卡片、触屏按钮
└── js/
├── config.js # 工具函数、存档读写、平衡数值总表
├── audio.js # 程序化音频:音效合成 + 背景音乐
├── sprites.js # 像素精灵工坊:战机/敌机/弹幕/道具/图标绘制
├── data.js # 静态数据机体、21 主武器、18 副武器、僚机、被动
├── core.js # 全局状态、输入、玩家移动、武器引擎、爆炸特效
├── enemies.js # 敌人图鉴、敌机行为、突变词缀、波次生成、Boss 战
├── combat.js # 战斗结算、子弹更新、碰撞检测、经验与三选一
├── render.js # 背景渲染、Boss 像素绘制、世界渲染、主循环
└── ui.js # 界面流转、机库商店、图鉴、实战模拟演示
```
## 加载顺序说明
JS 模块通过全局作用域共享状态,需按 `index.html` 中的顺序加载:
`config → audio → sprites → data → core → enemies → combat → render → ui`
## 操作
- 移动WASD / 方向键 / 触屏拖动
- 射击:全自动
- SHIFT精密移动 · P暂停 · M音乐开关

118
css/style.css Normal file
View File

@ -0,0 +1,118 @@
:root{
--bg:#05070f; --panel:#0b1226; --panel2:#101a33; --line:#26345f;
--ink:#dfe8ff; --dim:#7f8cb8; --cyan:#41e6ff; --mag:#ff4d9d;
--gold:#ffcf5c; --red:#ff5566; --green:#5cff9d; --purp:#b98cff;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%}
body{
background:var(--bg); color:var(--ink);
font-family:"Press Start 2P","Microsoft YaHei","PingFang SC",monospace;
display:flex; align-items:center; justify-content:center; overflow:hidden;
user-select:none;
}
#wrap{
position:relative; width:min(96vw,calc(96vh * 0.75)); aspect-ratio:3/4;
border:4px solid var(--line); background:#000;
box-shadow:0 0 0 2px #000, 0 0 42px rgba(65,230,255,.12), 0 24px 80px rgba(0,0,0,.7);
}
canvas#game{width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges;background:#05070f;touch-action:none}
#scan{position:absolute;inset:0;pointer-events:none;z-index:40;
background:repeating-linear-gradient(0deg,rgba(0,0,0,.14) 0 1px,transparent 1px 3px);
mix-blend-mode:multiply}
#vig{position:absolute;inset:0;pointer-events:none;z-index:41;
background:radial-gradient(ellipse at center,transparent 55%,rgba(0,0,0,.5) 100%)}
/* ---------- overlays ---------- */
.ovl{position:absolute;inset:0;z-index:50;display:flex;flex-direction:column;align-items:center;justify-content:center;
background:rgba(4,7,16,.88); text-align:center; padding:4%; gap:10px; overflow-y:auto}
.ovl.hide{display:none}
h1.logo{font-size:clamp(20px,5.4vw,44px);color:#fff;letter-spacing:4px;line-height:1.35;
text-shadow:3px 3px 0 var(--mag), -3px -3px 0 var(--cyan), 0 0 24px rgba(65,230,255,.6)}
.logo-cn{font-size:clamp(12px,2.6vw,20px);color:var(--gold);letter-spacing:12px;text-shadow:2px 2px 0 #7a4b00}
.tag{font-size:clamp(7px,1.4vw,10px);color:var(--dim);letter-spacing:2px}
.btn{font:inherit;font-size:clamp(9px,1.8vw,13px);color:var(--ink);background:var(--panel2);
border:3px solid var(--line);padding:12px 22px;cursor:pointer;letter-spacing:3px;
transition:all .12s; min-width:min(64vw,240px)}
.btn:hover{border-color:var(--cyan);color:var(--cyan);transform:translateY(-2px);box-shadow:0 4px 0 rgba(65,230,255,.25)}
.btn:active{transform:translateY(1px)}
.btn.primary{border-color:var(--cyan);color:#031018;background:var(--cyan);font-weight:bold}
.btn.primary:hover{background:#8ff2ff;color:#000}
.btn.gold{border-color:var(--gold);color:var(--gold)}
.btn.gold:hover{background:var(--gold);color:#231600}
.btn.danger{border-color:var(--red);color:var(--red)}
.btn:disabled{opacity:.35;cursor:not-allowed;transform:none}
.row{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
.col{display:flex;flex-direction:column;gap:10px;align-items:center}
.panel{background:var(--panel);border:3px solid var(--line);padding:14px;max-width:92%}
.h2{font-size:clamp(11px,2.2vw,16px);color:var(--cyan);letter-spacing:4px;text-shadow:2px 2px 0 rgba(65,230,255,.25)}
.h3{font-size:clamp(8px,1.6vw,11px);color:var(--dim);letter-spacing:2px;line-height:1.8}
.stat-grid{display:grid;grid-template-columns:auto auto;gap:6px 18px;font-size:clamp(8px,1.6vw,11px);
color:var(--ink);text-align:left;letter-spacing:1px}
.stat-grid b{color:var(--gold);text-align:right}
/* ship / mode select cards */
.pick{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
.pcard{width:min(28vw,132px);background:var(--panel);border:3px solid var(--line);padding:10px 6px;cursor:pointer;transition:all .12s}
.pcard:hover{transform:translateY(-3px)}
.pcard.sel{border-color:var(--cyan);box-shadow:0 0 16px rgba(65,230,255,.35)}
.pcard.lock{opacity:.55}
.pcard canvas{width:52px;height:64px;image-rendering:pixelated}
.pcard .nm{font-size:clamp(8px,1.5vw,11px);color:var(--ink);letter-spacing:2px;margin:6px 0 4px}
.pcard .ds{font-size:clamp(6px,1.2vw,8px);color:var(--dim);line-height:1.7;letter-spacing:1px}
.pcard .cost{font-size:clamp(7px,1.3vw,9px);color:var(--gold);margin-top:5px}
/* rush 老虎机 */
.togrow{font-size:clamp(8px,1.5vw,11px);color:var(--dim);letter-spacing:2px;margin:4px 0 8px}
.togrow input{accent-color:var(--cyan);transform:translateY(1px)}
.slotrow{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:12px 0}
.slotbox{width:min(20vw,86px);background:var(--panel);border:3px solid var(--line);padding:8px 4px}
.slotbox .sc{font-size:clamp(6px,1.2vw,8px);color:var(--cyan);letter-spacing:2px}
.slotbox .sn{font-size:clamp(7px,1.4vw,10px);color:var(--ink);margin-top:6px;min-height:2.6em;display:flex;align-items:center;justify-content:center;line-height:1.3}
.slotbox .sn.hit{color:var(--gold);text-shadow:0 0 8px rgba(255,207,92,.6)}
/* upgrade cards */
#cards{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}
.ucard{width:min(27vw,150px);background:var(--panel);border:3px solid var(--line);padding:12px 8px;cursor:pointer;
transition:all .12s;position:relative}
.ucard:hover{transform:translateY(-4px) scale(1.03)}
.ucard canvas{width:40px;height:40px;image-rendering:pixelated}
.ucard .nm{font-size:clamp(8px,1.6vw,11px);margin:8px 0 4px;letter-spacing:1px}
.ucard .lv{font-size:clamp(6px,1.2vw,8px);color:var(--dim);letter-spacing:2px}
.ucard .ds{font-size:clamp(6px,1.25vw,9px);color:var(--ink);line-height:1.8;margin-top:6px;min-height:3.4em}
.ucard .key{position:absolute;top:4px;left:6px;font-size:8px;color:var(--dim)}
.ucard.r0{border-color:#5f7fb8}.ucard.r0 .nm{color:#9fd8ff}
.ucard.r1{border-color:var(--purp);box-shadow:0 0 12px rgba(185,140,255,.3)}.ucard.r1 .nm{color:var(--purp)}
.ucard.r2{border-color:var(--gold);box-shadow:0 0 16px rgba(255,207,92,.45)}.ucard.r2 .nm{color:var(--gold)}
.rtag{font-size:6px;letter-spacing:2px;color:var(--dim)}
/* hangar */
.shop{display:flex;flex-direction:column;gap:8px;width:min(92%,420px)}
.sitem{display:flex;align-items:center;gap:10px;background:var(--panel);border:3px solid var(--line);padding:8px 10px;text-align:left}
.sitem .ic{width:30px;height:30px;flex:none;image-rendering:pixelated}
.sitem .inf{flex:1}
.sitem .nm{font-size:clamp(8px,1.5vw,10px);letter-spacing:2px}
.sitem .ds{font-size:clamp(6px,1.2vw,8px);color:var(--dim);line-height:1.6;margin-top:3px}
.sitem .pips{font-size:7px;color:var(--cyan);letter-spacing:2px;margin-top:3px}
.sitem button{font:inherit;font-size:clamp(7px,1.3vw,9px);background:var(--panel2);color:var(--gold);
border:2px solid var(--gold);padding:8px 10px;cursor:pointer;flex:none}
.sitem button:disabled{opacity:.35;border-color:var(--line);color:var(--dim)}
.wallet{font-size:clamp(9px,1.8vw,12px);color:var(--gold);letter-spacing:2px}
/* touch buttons */
#tbtns{position:absolute;inset:auto 0 10px 0;z-index:45;display:none;justify-content:space-between;padding:0 12px;pointer-events:none}
#tbtns button{pointer-events:auto;font:inherit;font-size:10px;width:64px;height:64px;border-radius:8px;
background:rgba(16,26,51,.7);border:3px solid var(--line);color:var(--ink);letter-spacing:1px}
#tbtns button:active{background:var(--cyan);color:#000}
#tb-pause{position:absolute;top:6px;right:6px;z-index:46;font:inherit;font-size:11px;width:40px;height:40px;
background:rgba(16,26,51,.7);border:3px solid var(--line);color:var(--ink);letter-spacing:1px}
#tb-pause:active{background:var(--cyan);color:#000}
.blink{animation:blk 1s steps(2) infinite}
@keyframes blk{50%{opacity:.25}}
.help-list{font-size:clamp(7px,1.5vw,10px);color:var(--ink);line-height:2.2;letter-spacing:2px;text-align:left}
.help-list b{color:var(--cyan)}
.small{font-size:clamp(6px,1.2vw,8px);color:var(--dim);letter-spacing:1px;line-height:1.8}
.litem{display:inline-block;background:var(--panel);border:2px solid var(--line);padding:5px 8px;margin:3px;font-size:clamp(7px,1.3vw,9px);cursor:pointer;letter-spacing:1px;color:var(--dim)}
.litem.on{border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 8px rgba(65,230,255,.3)}
.cx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(62px,1fr));gap:6px;width:min(92%,430px)}
.cx-item{background:var(--panel);border:2px solid var(--line);padding:6px 2px;text-align:center;cursor:pointer;transition:all .1s}
.cx-item:hover{transform:translateY(-2px);border-color:var(--cyan)}
.cx-item canvas{width:28px;height:28px;image-rendering:pixelated}
.cx-item .nm{font-size:clamp(6px,1.1vw,8px);margin-top:4px;letter-spacing:1px}
.cx-item.lock{opacity:.35;cursor:default}
.cx-item.lock:hover{transform:none;border-color:var(--line)}
.cx-sec{font-size:clamp(8px,1.5vw,10px);color:var(--cyan);margin:10px 0 4px;letter-spacing:3px}

155
index.html Normal file
View File

@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>星尘突击队 STAR RAID</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="wrap">
<canvas id="game" width="240" height="320"></canvas>
<div id="scan"></div><div id="vig"></div>
<!-- 标题 -->
<div class="ovl" id="o-title">
<div class="tag">PIXEL ROGUELIKE SHMUP</div>
<h1 class="logo">STAR<br>RAID</h1>
<div class="logo-cn">星尘突击队</div>
<div class="h3" id="best-line">BEST -- </div>
<div class="col" style="margin-top:6px">
<button class="btn primary" id="b-go">▶ 出击</button>
<button class="btn gold" id="b-hangar">✦ 机库强化</button>
<button class="btn" id="b-codex">❖ 军械图鉴</button>
<button class="btn danger" id="b-rush">☠ BOSS 挑战</button>
<button class="btn" id="b-help">? 作战手册</button>
</div>
<div class="small" style="margin-top:8px">键盘 WASD/方向键 移动 · 自动射击 · 超频充满自动暴走<br>SHIFT 精密移动 · P 暂停 · M 音乐</div>
</div>
<!-- 出击准备 -->
<div class="ovl hide" id="o-setup">
<div class="h2">— 出击准备 —</div>
<div class="h3">选择模式</div>
<div class="pick" id="mode-pick">
<div class="pcard sel" data-m="stage"><div class="nm" style="margin-top:2px">闯关模式</div><div class="ds">6 大关卡<br>击破母巢核心<br>即获胜利</div></div>
<div class="pcard" data-m="endless"><div class="nm" style="margin-top:2px">无尽模式</div><div class="ds">无限波次<br>突变词缀<br>冲击最高分</div></div>
</div>
<div class="h3">选择机体</div>
<div class="pick" id="ship-pick"></div>
<div class="h3">难度</div>
<div class="row" id="diff-pick">
<button class="btn sel-diff primary" data-d="0" style="min-width:110px;padding:8px 14px">普通</button>
<button class="btn sel-diff danger" data-d="1" style="min-width:110px;padding:8px 14px">困难</button>
</div>
<div class="row" style="margin-top:6px">
<button class="btn primary" id="b-start">开始出击 ▶</button>
<button class="btn" id="b-back1">返回</button>
</div>
</div>
<!-- Boss 挑战 -->
<div class="ovl hide" id="o-rush">
<div class="h2">— BOSS 挑战 —</div>
<div class="h3">随机武装3主武Lv3 · 3副武Lv3 · 2僚机Lv2 · 击破计时</div>
<div class="togrow"><label><input type="checkbox" id="rush-anim"> 老虎机抽奖动画</label></div>
<div class="pick" id="rush-pick"></div>
<button class="btn" id="b-back6" style="margin-top:6px">返回</button>
</div>
<!-- 武装抽取(老虎机) -->
<div class="ovl hide" id="o-slot">
<div class="h2">— 武装抽取 —</div>
<div class="slotrow" id="slot-row"></div>
<div class="h3" id="slot-hint"> </div>
</div>
<!-- 机库 -->
<div class="ovl hide" id="o-hangar">
<div class="h2">— 机库 · 永久强化 —</div>
<div class="wallet">星币 ✦ <span id="wallet">0</span></div>
<div class="shop" id="shop"></div>
<button class="btn" id="b-back2" style="margin-top:6px">返回</button>
</div>
<!-- 帮助 -->
<div class="ovl hide" id="o-help">
<div class="h2">— 作战手册 —</div>
<div class="panel help-list">
<b>移动</b> WASD / 方向键 / 触屏拖动<br>
<b>射击</b> 全自动,无需按键<br>
<b>暴走</b> 超频槽充满后自动触发火力×1.7<br>
<b>武器</b> 主武器无限叠加,抽到即入列<br>
<b>精密</b> SHIFT低速移动并显示判定点<br>
<b>擦弹</b> 贴近敌弹掠过可充能超频<br>
<b>结构值</b> 中弹扣血,归零才损失一条命<br>
<b>判定点</b> 仅机身中心 3 像素!<br>
<b>升级</b> 拾取经验宝石,三选一强化<br>
<b>星币</b> 击坠获得,局外永久强化
</div>
<button class="btn" id="b-back3">返回</button>
</div>
<!-- 升级三选一 -->
<div class="ovl hide" id="o-cards">
<div class="h2 blink">▲ LEVEL UP ▲</div>
<div class="h3">选择一项强化(按 1 / 2 / 3</div>
<div id="cards"></div>
</div>
<!-- 暂停 -->
<div class="ovl hide" id="o-pause">
<div class="h2">‖ 暂停 · 军械配置</div>
<div id="loadout" style="width:100%;max-height:52%;overflow-y:auto"></div>
<div class="col">
<button class="btn primary" id="b-resume">继续战斗</button>
<button class="btn" id="b-restart">重新开始</button>
<button class="btn danger" id="b-quit">放弃任务</button>
</div>
</div>
<!-- 图鉴 -->
<div class="ovl hide" id="o-codex">
<div class="h2">— 军械图鉴 —</div>
<div class="h3">已收集 <span id="codex-n">0</span>/<span id="codex-t">0</span> · 点击可实时演示</div>
<div id="codex" style="overflow-y:auto;max-height:72%;width:100%;display:flex;flex-direction:column;align-items:center"></div>
<button class="btn" id="b-back4">返回</button>
</div>
<!-- 武器演示(全屏实战模拟,透明覆盖层不挡画布) -->
<div class="ovl hide" id="o-demo" style="background:transparent;pointer-events:none;justify-content:space-between;overflow:hidden">
<div style="margin-top:3%">
<div class="h2" id="demo-name">演示</div>
<div class="h3" id="demo-ds" style="max-width:88%;margin:0 auto"></div>
<div class="h3" style="color:var(--dim);margin-top:4px">实战模拟 · 自动驾驶 · 触屏/方向键可接管</div>
</div>
<button class="btn" id="b-back5" style="pointer-events:auto;margin-bottom:4%">返回图鉴</button>
</div>
<!-- 结算 -->
<div class="ovl hide" id="o-over">
<div class="h2" id="over-title">任务失败</div>
<div class="panel"><div class="stat-grid" id="over-stats"></div></div>
<div class="wallet">获得星币 ✦ <span id="over-coins">0</span></div>
<div class="col">
<button class="btn primary" id="b-again">再来一局</button>
<button class="btn" id="b-home">返回基地</button>
</div>
</div>
<button id="tb-pause" style="display:none">| |</button>
</div>
<script src="js/config.js"></script>
<script src="js/audio.js"></script>
<script src="js/sprites.js"></script>
<script src="js/data.js"></script>
<script src="js/core.js"></script>
<script src="js/enemies.js"></script>
<script src="js/combat.js"></script>
<script src="js/render.js"></script>
<script src="js/ui.js"></script>
</body>
</html>

71
js/audio.js Normal file
View File

@ -0,0 +1,71 @@
"use strict";
/* ---------------- 音频:程序化合成 ---------------- */
const AU={ctx:null,master:null,mgain:null,
init(){ if(this.ctx) return;
try{ this.ctx=new (window.AudioContext||window.webkitAudioContext)();
this.master=this.ctx.createGain(); this.master.gain.value=0.5; this.master.connect(this.ctx.destination);
this.mgain=this.ctx.createGain(); this.mgain.gain.value=SAVE.music?0.16:0; this.mgain.connect(this.master);
}catch(e){}
},
now(){return this.ctx?this.ctx.currentTime:0;},
env(g,t,a,d,v){g.gain.setValueAtTime(0,t);g.gain.linearRampToValueAtTime(v,t+a);g.gain.exponentialRampToValueAtTime(0.001,t+a+d);},
tone(f,type,dur,vol=0.2,slide=0){ if(!this.ctx||SAVE.mute)return;
const t=this.now(),o=this.ctx.createOscillator(),g=this.ctx.createGain();
o.type=type;o.frequency.setValueAtTime(f,t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(20,f+slide),t+dur);
this.env(g,t,0.005,dur,vol); o.connect(g);g.connect(this.master); o.start(t);o.stop(t+dur+0.05); },
noise(dur,vol=0.25,hp=400){ if(!this.ctx||SAVE.mute)return;
const t=this.now(),len=Math.floor(this.ctx.sampleRate*dur),buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate),d=buf.getChannelData(0);
for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*(1-i/len);
const s=this.ctx.createBufferSource();s.buffer=buf;
const f=this.ctx.createBiquadFilter();f.type="highpass";f.frequency.value=hp;
const g=this.ctx.createGain();this.env(g,t,0.003,dur,vol);
s.connect(f);f.connect(g);g.connect(this.master);s.start(t); }
};
let _shootT=0,_eshootT=0;
const SFX={
shoot(){const n=performance.now(); if(n-_shootT<45)return; _shootT=n; AU.tone(880,"square",0.05,0.05,-300);},
eshoot(){const n=performance.now();if(n-_eshootT<90)return;_eshootT=n;AU.tone(220,"square",0.06,0.03,-80);},
hit(){AU.noise(0.05,0.12,1200);},
boom(big){AU.noise(big?0.5:0.25,big?0.4:0.22,big?80:200); AU.tone(big?90:140,"sawtooth",big?0.4:0.2,0.18,-60);},
pick(){AU.tone(660,"sine",0.08,0.12,300);},
coin(){AU.tone(990,"square",0.05,0.08);AU.tone(1320,"square",0.08,0.08);},
level(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>AU.tone(f,"square",0.1,0.12),i*70));},
hurt(){AU.tone(160,"sawtooth",0.3,0.3,-100);AU.noise(0.3,0.3,150);},
bomb(){AU.noise(0.8,0.5,60);AU.tone(70,"sawtooth",0.7,0.3,-40);},
od(){[440,660,880,1320].forEach((f,i)=>setTimeout(()=>AU.tone(f,"sawtooth",0.09,0.1),i*50));},
graze(){AU.tone(1800,"sine",0.03,0.04);},
warn(){for(let i=0;i<4;i++)setTimeout(()=>{AU.tone(440,"square",0.15,0.15);setTimeout(()=>AU.tone(349,"square",0.15,0.15),180);},i*400);},
};
/* 简易芯片音序器 */
const MUS={step:0,next:0,bpm:138,playing:false,boss:false,
bass:[45,45,48,45,52,45,48,43], lead:[69,72,76,72,74,72,69,64],
tick(){ if(!AU.ctx||!this.playing||!SAVE.music)return;
const spb=60/(this.bpm*2);
while(this.next<AU.now()+0.12){
const s=this.step%8, t=this.next;
// 贝斯
this.mt(this.N(this.bass[s]+(this.boss?2:0)),"triangle",t,spb*0.9,0.5);
// 琶音
if(s%2===0||this.boss) this.mt(this.N(this.lead[s]+12),"square",t,spb*0.4,0.12);
// 鼓点
if(s%4===0) this.kick(t); this.hat(t+spb/2);
this.next+=spb; this.step++;
}
},
N(n){return 440*Math.pow(2,(n-69)/12);},
mt(f,type,t,d,v){const o=AU.ctx.createOscillator(),g=AU.ctx.createGain();
o.type=type;o.frequency.value=f;g.gain.setValueAtTime(v,t);g.gain.exponentialRampToValueAtTime(0.001,t+d);
o.connect(g);g.connect(AU.mgain);o.start(t);o.stop(t+d+0.02);},
kick(t){const o=AU.ctx.createOscillator(),g=AU.ctx.createGain();
o.frequency.setValueAtTime(150,t);o.frequency.exponentialRampToValueAtTime(40,t+0.1);
g.gain.setValueAtTime(0.6,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.12);
o.connect(g);g.connect(AU.mgain);o.start(t);o.stop(t+0.14);},
hat(t){const len=Math.floor(AU.ctx.sampleRate*0.03),b=AU.ctx.createBuffer(1,len,AU.ctx.sampleRate),d=b.getChannelData(0);
for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*(1-i/len);
const s=AU.ctx.createBufferSource();s.buffer=b;const g=AU.ctx.createGain();
g.gain.setValueAtTime(0.07,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.03);
s.connect(g);g.connect(AU.mgain);s.start(t);},
start(boss){this.playing=true;this.boss=!!boss;this.bpm=boss?152:138;if(AU.ctx)this.next=AU.now()+0.1;},
stop(){this.playing=false;}
};

332
js/combat.js Normal file
View File

@ -0,0 +1,332 @@
"use strict";
/* ================= 战斗结算 ================= */
let _uid=1;
function banner(t,col,time){G.banner={t,col:col||"#41e6ff"};G.bannerT=time||100;}
function addScore(base,x,y){
let s=base*BAL.scoreCombo(G.combo)*(G.mutator==="FRENZY"?1.3:1);
s=Math.round(s);
G.score+=s;
if(base>=400&&x!==undefined)G.texts.push({x,y,t:"+"+s,col:"#ffcf5c",life:50});
}
function addCoins(n){G.coins+=Math.round(n*pCoinMul()*BAL.coinGlobal);}
function spawnPart(x,y,col,n){
for(let i=0;i<(n||1);i++)G.parts.push({x,y,vx:rnd(-2,2),vy:rnd(-2,2),life:rndi(14,30),col,sz:rndi(1,3)});
}
function dropPick(x,y,kind){
G.picks.push({x:clamp(x,8,W-8),y:clamp(y,-20,H-40),kind,vy:0.7,t:0,spr:{gem:"gem",coin:"coin",heart:"heart",bomb:"bombp",odc:"odc"}[kind]});
}
function hurtEnemy(e,dmg,canCrit){
if(!e||e.dead)return;
let crit=false;
if(canCrit&&Math.random()<pCrit()){dmg*=BAL.player.critM;crit=true;}
e.hp-=dmg;e.flash=2;e.hpt=150;
if(crit)G.texts.push({x:e.x,y:e.y-8,t:Math.round(dmg)+"!",col:"#ffe95c",life:30});
spawnPart(e.x,e.y,crit?"#ffe95c":"#fff",1);
// 寄生电浆(副武器):附着水蛭,吸血回复结构
if(canCrit&&G.subLoadout.includes("leech")&&!e.leech&&Math.random()<0.14+0.06*G.subs.leech){
e.leech={t:200,dps:1.2+0.5*G.subs.leech};
for(let i=0;i<3;i++)G.parts.push({x:e.x+rnd(-4,4),y:e.y+rnd(-4,4),vx:0,vy:0,life:14,col:"#b05ce8",sz:1,suck:{x:e.x,y:e.y}});
}
if(e.hp<=0)killEnemy(e);
}
function killEnemy(e){
e.dead=true;G.kills++;
SFX.boom(e.elite||e.r>7);
G.shake=Math.max(G.shake,e.elite?6:2);
const cols=e.elite?["#ffd23d","#fff","#ff9a3d"]:["#ff9a3d","#ff5566","#fff"];
for(let i=0;i<(e.elite?20:8);i++)spawnPart(e.x,e.y,pick(cols),e.elite?3:2);
addScore(e.score,e.x,e.y);
G.combo++;G.comboT=180;
G.od=Math.min(100,G.od+BAL.odGain.kill*(1+0.3*G.u.odcore));
for(let i=0;i<e.gems;i++)dropPick(e.x+rnd(-8,8),e.y+rnd(-6,6),"gem");
if(Math.random()<e.coin){const n=e.elite?rndi(5,8):rndi(1,2);for(let i=0;i<n;i++)dropPick(e.x+rnd(-10,10),e.y,"coin");}
if(e.elite){dropPick(e.x,e.y,"odc");if(Math.random()<0.5)dropPick(e.x+8,e.y,"coin");}
const r=Math.random();
if(!e.neutral){
if(r<0.025&&G.lives<G.stat.maxLives)dropPick(e.x,e.y,"heart");
else if(r<0.085)dropPick(e.x,e.y,"odc");
}
if(e.death==="ring"&&!e.neutral)for(let i=0;i<8;i++)ebul(e.x,e.y,i/8*TAU,1.6,"orbR");
if(e.death==="split")for(let i=0;i<2;i++)spawnEnemy("mini",e.x+(i?8:-8),e.y,false);
}
function hitPlayer(dmg){
const p=G.p;
if(p.inv>0||p.dead>0)return;
if(G.demoSim){spawnPart(p.x,p.y,"#41e6ff",3);SFX.graze();return;} // 演示模式无敌
if(Math.random()<pDodge()){G.texts.push({x:p.x,y:p.y-10,t:"MISS",col:"#9fd8ff",life:40});return;}
if(G.shield>0){G.shield--;G.shieldT=0;p.inv=70;SFX.hurt();G.shake=6;banner("护盾损毁!","#43e8ff",60);return;}
G.hp-=dmg;G.combo=0;
SFX.hurt();G.shake=7;G.flash=8;G.hitstop=5;p.inv=55;
for(let i=0;i<8;i++)spawnPart(p.x,p.y,pick(["#ff5566","#fff","#ff9a3d"]),2);
G.texts.push({x:p.x,y:p.y-10,t:"-"+Math.round(dmg),col:"#ff5566",life:35});
if(G.hp<=0){
G.lives--;G.hp=0;
SFX.boom(true);G.shake=12;G.flash=14;
for(let i=0;i<20;i++)spawnPart(p.x,p.y,pick(["#ff5566","#fff","#ff9a3d"]),3);
if(G.lives<=0){p.dead=99999;endRun(false);}
else{p.dead=70;G.hp=G.maxHp;banner("机体损毁 — 重新部署","#ff5566",80);}
}
}
function shieldCap(){return G.stat.shieldMax+(G.u.shield>0?1:0)+(G.u.shield>=3?1:0);}
/* ================= 子弹更新 ================= */
function explodeOnce(b){if(b.boom)return;b.boom=true;explodeBullet(b);}
function strongestEnemy(x,y,r){ // 斩首巡航弹Boss/精英优先
if(G.boss)return G.boss;
let best=null,bh=-1;const rr=r*r;
for(const e of G.enemies){if(e.dead)continue;
if(dist2(x,y,e.x,e.y)<rr){const v=e.elite?1e6+e.hp:e.hp;if(v>bh){bh=v;best=e;}}}
return best||nearestEnemy(x,y,r);
}
function updateBullets(){
for(const b of G.pb){
// 附着状态:钻头研磨 / 粘着炸弹
if(b.stuck){
const se=b.stuck;
if(se.dead){b.dead=true;if(b.t==="tb")explodeOnce(b);continue;}
b.x=se.x+b.ox;b.y=se.y+b.oy;
if(b.t==="tb"){b.fuse--;if(b.fuse<=0){b.dead=true;explodeOnce(b);}}
continue;
}
if(b.homing||b.softH){const t=b.eliteHunt?strongestEnemy(b.x,b.y,460):nearestEnemy(b.x,b.y,b.softH?400:220);
if(t){const a=Math.atan2(t.y-b.y,t.x-b.x),c=Math.atan2(b.vy,b.vx);
let d=a-c;while(d>Math.PI)d-=TAU;while(d<-Math.PI)d+=TAU;
if(b.softH){ // 星云奥秘:缓慢转向,不加速
const na=c+clamp(d,-0.05,0.05),s=Math.hypot(b.vx,b.vy);
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;
}else{const na=c+clamp(d,-0.16,0.16),s=Math.min(6,Math.hypot(b.vx,b.vy)+0.08);
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;}}}
if(b.back){b.bt--;if(b.bt<=0&&b.vy<0){b.vy*=-1;b.vx*=-0.4;}}
if(b.curve){const c=Math.atan2(b.vy,b.vx)+b.curve,s=Math.hypot(b.vx,b.vy)||0.1;
b.vx=Math.cos(c)*s;b.vy=Math.sin(c)*s;}
if(b.pull)for(const e of G.enemies)if(!e.dead){
const d2=dist2(b.x,b.y,e.x,e.y);
if(d2<b.pull*b.pull&&d2>1){const d=Math.sqrt(d2);e.x+=(b.x-e.x)/d*0.45;e.y+=(b.y-e.y)/d*0.45;}}
// 空爆弹:先向左右平移,再折向直飞,沿途不断爆炸
if(b.t==="am"&&!b.dead){
if(b.phase===1){b.pt++;
if(b.pt>=20){b.phase=2;b.vx=0;b.vy=-4.4;}}
else if(G.time%6===0){
const ex=b.x+rnd(-8,8),ey=b.y+rnd(-4,8);
areaDamage(ex,ey,b.zone||15,b.dmg*0.35);
explodeFx(ex,ey,"airburst",1);
if(G.time%12===0)SFX.boom(false);
}
}
if(b.trailCol&&G.time%2===0)G.parts.push({x:b.x,y:b.y,vx:rnd(-0.2,0.2),vy:0.4,life:12,col:b.trailCol,sz:1});
if(b.t==="nb"&&G.time%4===0)G.parts.push({x:b.x+rnd(-4,4),y:b.y+rnd(-4,4),vx:rnd(-0.3,0.3),vy:rnd(-0.3,0.3),life:14,col:pick(["#b98cff","#e0b3ff"]),sz:1});
// 电弧手里剑:一路前飞不停留,全程周期性放电
if(b.t==="shk"&&!b.dead){
b.pt2=(b.pt2||0)-1;
if(b.pt2<=0){b.pt2=b.pulse||20;
const tg=nearEnemies(b.x,b.y,55,3);
for(const t2 of tg){zapVisual(b.x,b.y,t2.x,t2.y,pick(["#7ce9ff","#ffffff"]));hurtT(t2,b.zapDmg,true);}
if(tg.length)SFX.graze();}
}
if(b.t==="dw"&&!b.dead){ // 黎明重炮近炸引信半径26无需直接命中
let pz=!!nearestEnemy(b.x,b.y,26);
if(!pz&&bossHit(b.x,b.y,26))pz=true;
if(pz){b.dead=true;explodeOnce(b);}}
if(b.t==="c"&&!b.dead&&b.y<H*0.5){ // 月牙斩:进入上半屏后弧线向中线聚拢
const ca=Math.atan2(b.vy,b.vx),ta=Math.atan2(-60-b.y,(W/2)-b.x);
let d4=ta-ca;while(d4>Math.PI)d4-=TAU;while(d4<-Math.PI)d4+=TAU;
const na=ca+clamp(d4,-0.035,0.035),ss=Math.hypot(b.vx,b.vy);
b.vx=Math.cos(na)*ss;b.vy=Math.sin(na)*ss;}
if(!b.back&&b.bt!==undefined){b.bt--;if(b.bt<=0&&!b.dead){b.dead=true;explodeOnce(b);}}
if(b.t==="mt"&&b.ty2!==undefined&&b.y<=b.ty2&&!b.dead){b.dead=true;explodeOnce(b);}
if(b.t==="s"&&!b.dead){ // 分裂弹:飞行一段距离后自动分裂
b.age=(b.age||0)+1;
if(b.age>=(b.splitAt||26)){b.dead=true;
for(let i=0;i<b.split;i++){const a=-Math.PI/2+(i-(b.split-1)/2)*0.35;
pbul(b.x,b.y,Math.cos(a)*4.5,Math.sin(a)*4.5,b.dmg*0.4,{r:2});}
spawnPart(b.x,b.y,"#5cff9d",4);}
}
b.x+=b.vx;b.y+=b.vy;b.life--;
if(b.life<=0&&!b.dead){b.dead=true;if(b.t==="bb"||b.t==="sg"||b.t==="ho"||b.t==="dw"||b.t==="ph"||b.t==="nb")explodeOnce(b);}
if(b.y<-16||b.y>H+16||b.x<-16||b.x>W+16)b.dead=true;
}
// 敌弹:热诱弹偏转(每枚热诱弹只锁定一颗敌弹)+ 时间力场减速
for(const f of G.flares)if(f.lock&&(f.lock.dead||dist2(f.x,f.y,f.lock.x,f.lock.y)>3600))f.lock=null;
for(const b of G.eb){
let esf=1;
for(const z of G.zones)if(z.slow&&dist2(b.x,b.y,z.x,z.y)<z.r*z.r){esf=0.45;break;}
let fl=null,fd=46*46;
for(const f of G.flares){if(f.lock&&f.lock!==b)continue;const d3=dist2(b.x,b.y,f.x,f.y);if(d3<fd){fd=d3;fl=f;}}
if(fl)fl.lock=b;
if(fl){const a=Math.atan2(fl.y-b.y,fl.x-b.x),c=Math.atan2(b.vy,b.vx);
let d4=a-c;while(d4>Math.PI)d4-=TAU;while(d4<-Math.PI)d4+=TAU;
const na=c+clamp(d4,-0.2,0.2),ss=Math.hypot(b.vx,b.vy);
b.vx=Math.cos(na)*ss;b.vy=Math.sin(na)*ss;
if(fd<36){b.dead=true;fl.dead=true;spawnPart(b.x,b.y,"#ffd23d",2);SFX.graze();}}
if(b.grow){b.scale=Math.min(b.maxS||2.6,(b.scale||1)+b.grow);b.r=2.4*b.scale;} // 花弹:越变越大(有上限)
b.x+=b.vx*esf;b.y+=b.vy*esf;
if(b.y<-10||b.y>H+10||b.x<-10||b.x>W+10)b.dead=true;}
// 热诱弹
for(const f of G.flares){f.x+=f.vx||0;f.y+=(f.vy!==undefined?f.vy:0.35);f.life--;
if(G.time%6===0)G.parts.push({x:f.x+rnd(-3,3),y:f.y+rnd(-3,3),vx:0,vy:-0.3,life:10,col:pick(["#ffd23d","#ff9a3d"]),sz:1});}
G.flares=G.flares.filter(f=>f.life>0&&!f.dead&&f.y>-10&&f.y<H+10&&f.x>-10&&f.x<W+10);
G.pb=G.pb.filter(b=>!b.dead);
G.eb=G.eb.filter(b=>!b.dead);
// 区域效果(电浆/引力井)
for(const z of G.zones){z.life--;
if(z.pull)for(const e of G.enemies)if(!e.dead){
const d2=dist2(z.x,z.y,e.x,e.y);
if(d2<(z.r+44)*(z.r+44)&&d2>1){const d=Math.sqrt(d2);e.x+=(z.x-e.x)/d*0.5;e.y+=(z.y-e.y)/d*0.5;}}
if(z.dps&&G.time%6===0){
for(const e of G.enemies)if(!e.dead&&dist2(z.x,z.y,e.x,e.y)<(z.r+e.r)*(z.r+e.r))hurtEnemy(e,z.dps*0.1,false);
if(G.boss&&bossHit(z.x,z.y,z.r))hurtBoss(z.dps*0.1);}
}
G.zones=G.zones.filter(z=>z.life>0);
for(const z of G.zaps)z.life--;
G.zaps=G.zaps.filter(z=>z.life>0);
for(const n of G.novas){n.life--;n.r+=(n.maxr-n.r)*0.18;}
G.novas=G.novas.filter(n=>n.life>0);
// 圣光十字:逐渐变长 + 原地旋转90°周期性结算伤害
for(const hc of G.holies){hc.t++;hc.life--;
if(hc.t%9===0)crossBeams(hc.x,hc.y,hc.len*Math.min(1,hc.t/12),hc.dmg*0.4,(hc.t/36)*Math.PI/2);}
G.holies=G.holies.filter(h=>h.life>0);
// 黎明重炮余震:延迟二次爆炸
for(const sk of G.shocks){sk.t--;if(sk.t<=0&&!sk.done){sk.done=true;
areaDamage(sk.x,sk.y,sk.r,sk.dmg);explodeFx(sk.x,sk.y,"boom",0.7);G.shake=Math.max(G.shake,3);SFX.boom(false);}}
G.shocks=G.shocks.filter(s=>!s.done);
}
/* ================= 碰撞 ================= */
function collide(){
const p=G.p;
// 玩家子弹 vs 敌
for(const b of G.pb){
if(b.dead)continue;
for(const e of G.enemies){
if(e.dead)continue;
const rr=e.r+b.r+1;
if(dist2(b.x,b.y,e.x,e.y)<rr*rr){
// 粘着炸弹:命中即附着,不走普通结算
if(b.t==="tb"&&!b.stuck){
b.stuck=e;b.ox=b.x-e.x;b.oy=b.y-e.y;
SFX.hit();break;
}
if(b.pierce){if(!b.hits)b.hits={};const u2=e.uid||(e.uid=_uid++);
if(b.hits[u2]!==undefined)continue;
b.hits[u2]=1;}
else b.dead=true;
if(b.slow)e.slow=Math.max(e.slow||0,b.slow);
if(b.freeze){e.frozen=Math.max(e.frozen||0,b.freeze);e.slow=0;explodeFx(b.x,b.y,"ice",0.5);}
if(b.poison)e.dot={t:b.poison,dps:Math.max(0.6,b.dmg*0.12)};
if(b.aoe){for(const o of G.enemies)if(!o.dead&&dist2(b.x,b.y,o.x,o.y)<b.aoe*b.aoe)hurtEnemy(o,b.dmg*(o===e?1:0.5),true);
explodeFx(b.x,b.y,"boom",0.45);
if(b.aoe>=20){G.shake=Math.max(G.shake,3);}SFX.hit();}
else hurtEnemy(e,b.dmg,true);
if(b.t==="vl"){ // 电能机炮:命中后电弧连锁跳跃
let vcn=0;const vcm=b.chain||2;
for(const o of G.enemies){if(o.dead||o===e||vcn>=vcm)continue;
if(dist2(e.x,e.y,o.x,o.y)<2304){vcn++;
zapVisual(e.x,e.y,o.x,o.y,pick(["#7ce9ff","#b06cff"]));
hurtEnemy(o,b.dmg*0.5,true);}}
if(vcn)SFX.graze();}
if(b.t==="rg"&&!b.zoneUsed){b.zoneUsed=true;
G.zones.push({x:b.x,y:b.y,r:b.zone,dps:b.dmg*0.09,life:220,col:"#43e8ff",kind:"plasma"});
areaDamage(b.x,b.y,18,b.dmg*0.5);
explodeFx(b.x,b.y,"plasma",0.8);}
SFX.hit();
if(b.dead){if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex")explodeOnce(b);break;}
}
}
if(!b.dead&&G.boss){
const B=G.boss;
if(bossHit(b.x,b.y,b.r)){
if(b.pierce){if(!b.hits)b.hits={};if(!b.hits.boss){b.hits.boss=1;hurtBoss(b.dmg);}}
else{b.dead=true;
if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex")explodeOnce(b);
else hurtBoss(b.dmg);}
SFX.hit();
}
}
}
// 敌弹 vs 玩家(含擦弹)
if(p.dead<=0)for(const b of G.eb){
const d2=dist2(b.x,b.y,p.x,p.y);
const hr=G.stat.hitR+b.r;
if(d2<hr*hr){b.dead=true;hitPlayer(b.dmg);}
else if(!b.grazed&&d2<(BAL.player.grazeR+b.r)*(BAL.player.grazeR+b.r)){
b.grazed=true;G.score+=5;
G.od=Math.min(100,G.od+BAL.odGain.graze*(1+0.3*G.u.odcore));
SFX.graze();spawnPart(p.x+rnd(-6,6),p.y+rnd(-6,6),"#41e6ff",1);
}
}
// Boss光束预警后伤害点-线段距离判定,每束有命中冷却)
for(const bm of G.bbeams){
if(bm.src&&G.boss&&bm.src.b===G.boss){bm.x1=G.boss.x+bm.src.ox;bm.y1=G.boss.y+bm.src.oy;if(bm.src.followX2)bm.x2=bm.x1;} // 发射点跟随Boss炮台
if(bm.tele>0){bm.tele--;continue;}
bm.life--;
if(bm.cd>0)bm.cd--;
if(p.dead<=0&&bm.cd<=0){
const dx=bm.x2-bm.x1,dy=bm.y2-bm.y1,L2=dx*dx+dy*dy||1;
const tt=clamp(((p.x-bm.x1)*dx+(p.y-bm.y1)*dy)/L2,0,1);
const cx=bm.x1+dx*tt,cy=bm.y1+dy*tt,br=bm.w/2+G.stat.hitR;
if(dist2(p.x,p.y,cx,cy)<br*br){bm.cd=30;hitPlayer(bm.dmg);}
}
}
G.bbeams=G.bbeams.filter(bm=>bm.life>0||bm.tele>0);
// 冲撞Boss撞击玩家
if(G.boss&&G.boss.dashSt&&G.boss.dashSt.ph==="go"&&p.dead<=0&&p.inv<=0){
if(bossHit(p.x,p.y,G.stat.hitR+2))hitPlayer(30);
}
// 敌机撞击(反伤棘甲)
if(p.dead<=0&&p.inv<=0)for(const e of G.enemies){
if(e.dead)continue;
const rr=e.r+G.stat.hitR+2;
if(dist2(e.x,e.y,p.x,p.y)<rr*rr){hurtEnemy(e,50,false);
if(G.subLoadout.includes("thorns"))hurtEnemy(e,15*G.subs.thorns*pDmgMul(),true);
hitPlayer(30);break;}
}
// 拾取
for(const k of G.picks){
k.t++;k.y+=k.vy;
const md=pMagnet(),d2=dist2(k.x,k.y,p.x,p.y);
if(p.dead<=0&&d2<md*md){const d=Math.sqrt(d2)||1;k.x+=(p.x-k.x)/d*3.4;k.y+=(p.y-k.y)/d*3.4;}
if(p.dead<=0&&d2<100){
k.dead=true;
if(k.kind==="gem"){addXp(G.mutator==="ARMOR"?1.3:1);SFX.pick();}
else if(k.kind==="coin"){addCoins(1);SFX.coin();}
else if(k.kind==="heart"){if(G.hp<G.maxHp){G.hp=Math.min(G.maxHp,G.hp+35);banner("结构修复 +35","#5cff9d",60);}else if(G.lives<G.stat.maxLives){G.lives++;banner("备用机体 +1","#5cff9d",60);}else addScore(500,p.x,p.y);SFX.pick();}
else if(k.kind==="odc"){G.od=Math.min(100,G.od+25);SFX.pick();}
}
if(k.y>H+10)k.dead=true;
}
G.picks=G.picks.filter(k=>!k.dead);
}
/* ================= 经验与三选一 ================= */
function addXp(n){
if(G.demoSim)return; // 演示模式不升级不抽卡
G.xp+=n;
while(G.xp>=G.xpNeed){G.xp-=G.xpNeed;G.level++;G.xpNeed=BAL.xpNeed(G.level);G.cardQ=(G.cardQ||0)+1;}
if(G.cardQ>0&&UI==="play")offerCards(false);
}
function buildPool(){
const pool=[];
for(const k of MAINS){
if(!G.weapons[k])pool.push({kind:"weapon",key:k,rare:WEAPONS[k].rare});
else if(G.weapons[k]<5)pool.push({kind:"wlvl",key:k,rare:WEAPONS[k].rare});}
for(const k of SUB_KEYS){
if(!G.subs[k])pool.push({kind:"sub",key:k,rare:SUBS[k].rare});
else if(G.subs[k]<SUBS[k].max)pool.push({kind:"slvl",key:k,rare:SUBS[k].rare});}
for(const k of DRONE_KEYS){
if(!G.drones[k])pool.push({kind:"drone",key:k,rare:DRONES[k].rare});
else if(G.drones[k]<DRONES[k].max)pool.push({kind:"dlvl",key:k,rare:DRONES[k].rare});}
for(const k of PAS_KEYS)if(G.u[k]<PASSIVES[k].max)pool.push({kind:"upg",key:k,rare:PASSIVES[k].rare});
return pool;
}
function rollCards(bossRare){
const pool=buildPool(),out=[];
for(let c=0;c<3;c++){
let w=bossRare?[0,68,32]:[BAL.rarity[0],BAL.rarity[1]+G.luck*2,BAL.rarity[2]+G.luck*1.5];
for(let tries=0;tries<8;tries++){
const tot=w[0]+w[1]+w[2];let r=rnd(tot);
const rare=r<w[0]?0:r<w[0]+w[1]?1:2;
const cand=pool.filter(p=>p.rare===rare&&!out.includes(p));
if(cand.length){out.push(pick(cand));break;}
if(tries===7){const rest=pool.filter(p=>!out.includes(p));if(rest.length)out.push(pick(rest));}
}
}
while(out.length<3)out.push({kind:"fill",key:["heal","coins","od"][out.length%3],rare:0});
return out;
}

51
js/config.js Normal file
View File

@ -0,0 +1,51 @@
"use strict";
/* ================================================================
星尘突击队 STAR RAID 像素肉鸽竖版射击
单文件实现像素引擎 / 程序化音频 / 双模式 / 局内外成长
================================================================ */
/* ---------------- 工具 ---------------- */
const $=id=>document.getElementById(id);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a=1,b)=>b===undefined?Math.random()*a:a+Math.random()*(b-a);
const rndi=(a,b)=>Math.floor(rnd(a,b+1));
const pick=arr=>arr[Math.floor(Math.random()*arr.length)];
const TAU=Math.PI*2;
function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;}
const dist2=(ax,ay,bx,by)=>{const dx=ax-bx,dy=ay-by;return dx*dx+dy*dy;};
/* ---------------- 存档 ---------------- */
const SAVE_KEY="starraid_v1";
let SAVE;
function loadSave(){
try{SAVE=JSON.parse(localStorage.getItem(SAVE_KEY))||null;}catch(e){SAVE=null;}
if(!SAVE) SAVE={coins:0, m:{}, ships:["falcon"], best:{s0:0,s1:0,e0:0,e1:0,stage:0}, mute:false, music:true};
SAVE.m=SAVE.m||{}; SAVE.ships=SAVE.ships||["falcon"]; SAVE.best=SAVE.best||{s0:0,s1:0,e0:0,e1:0,stage:0};
SAVE.codex=SAVE.codex||{};
}
function persist(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(SAVE));}catch(e){}}
loadSave();
/* ---------------- ----------------
所有概率与成长曲线集中于此方便调平衡 */
const BAL={
player:{ speed:2.35, lives:3, hp:100, bombs:2, hitR:3, grazeR:14, focusMul:0.5,
fireInt:8, dmg:6, bulletSpd:5.4, critC:0.05, critM:2.5, magnetR:36 },
// 稀有度权重 [普通,稀有,史诗],每点幸运: 稀有+2 史诗+1.5
rarity:[62,28,10],
xpNeed:l=>Math.round(4+l*2.5+Math.pow(l,1.5)), // 升级所需经验下调保证每波≈1级
// 波次缩放n 从 0 起):平缓曲线,匹配玩家成长
hpMul: n=>1+0.10*n+0.012*Math.pow(n,1.5),
spdMul:n=>Math.max(0.6,1-0.015*n), // 血量越高波次,移动越慢
fireMul:n=>Math.min(1.3,1+0.012*n),
eliteC:n=>Math.min(0.16,0.03+n*0.006), // 精英出现率
coinDrop:0.5, // 普通敌掉币率
scoreCombo:k=>Math.min(3,1+k*0.02), // 连击倍率上限 x3
odGain:{kill:2.6,graze:2.2},
coinGlobal:4, // 全局金币倍率
bombDmg:80,
reviveInv:160,
hard:{hp:1.35,fire:1.3,coin:1.5}, // 困难修正
};

605
js/core.js Normal file
View File

@ -0,0 +1,605 @@
"use strict";
/* ================= 全局状态 ================= */
const cv=$("game"),ctx=cv.getContext("2d");
ctx.imageSmoothingEnabled=false;
const W=240,H=320;
let G=null,UI="title";
function newRun(mode,shipKey,diff){
const ship=SHIPS[shipKey],m=ship.mod,meta=SAVE.m;
G={mode,diff,ship:shipKey,stage:1,wave:0,waveT:0,betweenT:60,spawning:[],
score:0,coins:0,kills:0,combo:0,comboT:0,gems:0,level:1,xp:0,xpNeed:BAL.xpNeed(1),
luck:(meta.luck||0),mutator:null,mutList:[],
time:0,boss:null,bossN:0,shake:0,hitstop:0,flash:0,slowmo:0,
weapons:{vulcan:1},loadout:["vulcan"],subs:{},subLoadout:[],drones:{},droneLoadout:[],
wt:{},wst:{},sst:{},dst:{},zones:[],zaps:[],novas:[],flares:[],holies:[],shocks:[],bbeams:[],funnels:[],
u:{fire:0,crit:0,shield:0,nano:0,mag:0,bounty:0,odcore:0,ghost:0},
enemies:[],pb:[],eb:[],parts:[],picks:[],texts:[],lasers:[],
banner:null,bannerT:0,odActive:0,
p:{x:W/2,y:H-46,orbA:0,satA:0,inv:120,focus:false,dead:0}
};
if(m.drone){G.drones.gunner=1;G.droneLoadout=["gunner"];} // 女武神自带僚机
// 机体与局外强化
const b=BAL.player;
G.stat={
spd:b.speed*m.spd, dmgMul:m.dmg,
maxLives:b.lives+m.lives+(meta.armor||0),
critC:b.critC+m.crit, dodge:m.dodge, hitR:b.hitR+m.hitR,
magR:b.magnetR*m.mag*(1+0.15*(meta.mag2||0)), coinMul:1+0.15*(meta.wallet||0),
shieldMax:m.shield, droneBoost:m.droneBoost, odStart:meta.odstart?50:0
};
if(meta.weapon)G.weapons.vulcan=1+meta.weapon; // 机库:初始主武器强化
G.lives=G.stat.maxLives;
G.maxHp=BAL.player.hp+(m.hp||0)+15*(meta.hpup||0); G.hp=G.maxHp;
G.shield=G.stat.shieldMax; G.shieldT=0; G.od=G.stat.odStart;
applyShipMeta();
return G;
}
function applyShipMeta(){
// 局外强化对主武器等级的加成由 newRun 处理,这里预留
}
/* 派生数值 */
function pDmgMul(){let d=G.stat.dmgMul*(G.odActive>0?1.25:1);return d;}
function pFireMul(){let f=1+0.12*G.u.fire; if(G.odActive>0)f*=1.7; return f;}
function pSpd(){return G.stat.spd*(G.p.focus?BAL.player.focusMul:1)*(G.odActive>0?1.2:1);}
function pCrit(){return G.stat.critC+0.08*G.u.crit;}
function pDodge(){return Math.min(0.4,G.stat.dodge+0.1*G.u.ghost);}
function pMagnet(){return G.stat.magR*(1+0.6*G.u.mag);}
function pCoinMul(){let c=G.stat.coinMul*(1+0.3*G.u.bounty); if(G.diff===1)c*=BAL.hard.coin; if(G.mutator==="GOLD")c*=2; return c;}
/* ================= 输入 ================= */
const KEYS={};
const GAMEKEYS=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," ","KeyW","KeyA","KeyS","KeyD","KeyX","KeyC","KeyZ","Shift","KeyP","Escape","KeyM","Digit1","Digit2","Digit3"];
addEventListener("keydown",e=>{
if(GAMEKEYS.includes(e.code))e.preventDefault();
KEYS[e.code]=true;
AU.init(); if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
if(e.code==="KeyM"){SAVE.music=!SAVE.music;if(AU.mgain)AU.mgain.gain.value=SAVE.music?0.16:0;persist();}
if(e.code==="Escape"&&UI==="demoSim"){exitDemoSim();return;}
if(e.code==="KeyP"||e.code==="Escape")togglePause();
if(UI==="cards"&&["Digit1","Digit2","Digit3"].includes(e.code))pickCard(+e.code.slice(-1)-1);
});
addEventListener("keyup",e=>KEYS[e.code]=false);
/* 触屏拖动 */
let touchId=null,tx=0,ty=0,tox=0,toy=0;
cv.addEventListener("pointerdown",e=>{
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
if(touchId!==null)return; touchId=e.pointerId;
const r=cv.getBoundingClientRect();
tx=(e.clientX-r.left)/r.width*W; ty=(e.clientY-r.top)/r.height*H;
if(G&&UI==="play"){tox=G.p.x-tx;toy=G.p.y-ty-14;}
});
cv.addEventListener("pointermove",e=>{
if(e.pointerId!==touchId)return;
const r=cv.getBoundingClientRect();
tx=(e.clientX-r.left)/r.width*W; ty=(e.clientY-r.top)/r.height*H;
});
const endTouch=e=>{if(e.pointerId===touchId)touchId=null;};
cv.addEventListener("pointerup",endTouch);cv.addEventListener("pointercancel",endTouch);
/* ================= 玩家:移动与武器 ================= */
function updatePlayer(){
const p=G.p;
if(p.dead>0){if(G.lives<=0)return;p.dead--;if(p.dead===0){p.x=W/2;p.y=H-46;p.inv=BAL.reviveInv;G.hp=G.maxHp;}return;}
p.focus=!!KEYS["Shift"];
let dx=0,dy=0;
if(KEYS["ArrowLeft"]||KEYS["KeyA"])dx--;
if(KEYS["ArrowRight"]||KEYS["KeyD"])dx++;
if(KEYS["ArrowUp"]||KEYS["KeyW"])dy--;
if(KEYS["ArrowDown"]||KEYS["KeyS"])dy++;
const sp=pSpd();
if(touchId!==null){
const gx=clamp(tx+tox,8,W-8),gy=clamp(ty+toy,20,H-14);
p.x=lerp(p.x,gx,0.4); p.y=lerp(p.y,gy,0.4);
}else{
if(dx&&dy){dx*=0.707;dy*=0.707;}
p.x=clamp(p.x+dx*sp,8,W-8); p.y=clamp(p.y+dy*sp,20,H-14);
if(G.demoSim&&!dx&&!dy){ // 自动驾驶:缓慢追踪最近敌人,玩家输入即接管
const apt=nearestEnemy(p.x,p.y,400);
const ax=apt?apt.x:W/2+Math.sin(G.time*0.008)*70;
p.x=lerp(p.x,clamp(ax,16,W-16),0.028);
p.y=lerp(p.y,H-64+Math.sin(G.time*0.02)*8,0.04);
}
}
if(p.inv>0)p.inv--;
if(G.odActive>0){G.odActive--; if(G.odActive===0)G.od=0;}
// 暴走:超频槽充满自动触发(被动)
if(G.od>=100&&G.odActive<=0&&p.dead<=0){
G.odActive=(6+G.u.odcore)*60;SFX.od();
banner("暴走 OVERDRIVE","#ff9a3d");
}
// 护盾回复
if(G.u.shield>0&&G.shield<shieldCap()){
G.shieldT++; const need=(46-10*(G.u.shield-1))*60;
if(G.shieldT>need){G.shield++;G.shieldT=0;SFX.pick();}
}
fireWeapons();
}
function pbul(x,y,vx,vy,dmg,o){
if(!isFinite(x)||!isFinite(y)||!isFinite(vx)||!isFinite(vy))return; // NaN 防护
G.pb.push(Object.assign({x,y,vx,vy,dmg,r:3,t:"v",life:400,pierce:false,hit:null},o||{}));
}
/* ================= 武器引擎 ================= */
function hurtT(t,dmg,crit){if(G.boss&&t===G.boss)hurtBoss(dmg);else hurtEnemy(t,dmg,crit);}
function nearEnemies(x,y,r,n){
const rr=r*r,c=[];
for(const e of G.enemies)if(!e.dead)c.push(e);
if(G.boss)c.push(G.boss);
c.sort((a,b)=>dist2(x,y,a.x,a.y)-dist2(x,y,b.x,b.y));
const out=[];
for(const e of c){if(out.length>=n)break;if(dist2(x,y,e.x,e.y)<rr)out.push(e);}
return out;
}
function zapVisual(x1,y1,x2,y2,col){
const pts=[[x1,y1]],seg=4;
for(let i=1;i<seg;i++){const t2=i/seg;pts.push([lerp(x1,x2,t2)+rnd(-5,5),lerp(y1,y2,t2)+rnd(-5,5)]);}
pts.push([x2,y2]);
G.zaps.push({pts,life:7,col});
}
function areaDamage(x,y,r,dmg){
for(const e of G.enemies)if(!e.dead&&dist2(x,y,e.x,e.y)<(r+e.r)*(r+e.r))hurtEnemy(e,dmg,true);
if(G.boss&&bossHit(x,y,r))hurtBoss(dmg);
}
function crossBeams(x,y,len,dmg,rot){ // 圣光十字:只结算伤害,绘制在 render 中(支持旋转)
rot=rot||0;
for(let k=0;k<4;k++){
const ca=rot+k*Math.PI/2,dx=Math.cos(ca),dy=Math.sin(ca);
for(const e of G.enemies){if(e.dead)continue;
const t2=clamp((e.x-x)*dx+(e.y-y)*dy,0,len);
if(dist2(e.x,e.y,x+dx*t2,y+dy*t2)<(e.r+3)*(e.r+3))hurtEnemy(e,dmg,true);}
if(G.boss){const t2=clamp((G.boss.x-x)*dx+(G.boss.y-y)*dy,0,len);
if(bossHit(x+dx*t2,y+dy*t2,3))hurtBoss(dmg);}
}
}
/* ================= =================
每种伤害类型唯一主色+形状语言+动画节奏参考原神/明日方舟/Cave系 */
function explodeFx(x,y,type,s){
s=s||1;
switch(type){
case "fire": // 火焰:白闪→火舌上卷→余烬飘落
G.novas.push({x,y,r:2,maxr:9*s,life:7,col:"#fff4d9"});
for(let i=0;i<10;i++)G.parts.push({x:x+rnd(-4,4),y:y+rnd(-3,3),vx:rnd(-0.9,0.9),vy:rnd(-1.8,-0.4),life:rndi(14,26),col:pick(["#ff9a3d","#ff6b3d","#ffd23d"]),sz:rndi(1,3),grav:0.05});
for(let i=0;i<4;i++)G.parts.push({x,y,vx:rnd(-0.4,0.4),vy:rnd(-0.9,-0.3),life:rndi(22,36),col:"#5a5a6a",sz:2});
break;
case "ice": // 冰霜:内吸→冰刺放射→碎片缓落
for(let i=0;i<6;i++)G.parts.push({x:x+rnd(-15,15),y:y+rnd(-15,15),vx:0,vy:0,life:9,col:"#cfeaff",sz:1,suck:{x,y}});
for(let i=0;i<8;i++){const a=i/8*TAU+rnd(-0.2,0.2);
G.parts.push({x,y,vx:Math.cos(a)*2.4,vy:Math.sin(a)*2.4,life:rndi(10,18),col:pick(["#7fd8ff","#ffffff"]),sz:2,spark:true});}
for(let i=0;i<4;i++)G.parts.push({x,y,vx:rnd(-0.8,0.8),vy:rnd(-0.4,0.4),life:rndi(18,30),col:"#7fd8ff",sz:1,grav:0.03});
break;
case "elec": // 雷电:闪白→分叉电弧→残留闪烁
G.novas.push({x,y,r:2,maxr:11*s,life:6,col:"#ffffff"});
for(let i=0;i<3;i++){const a=rnd(0,TAU);zapVisual(x,y,x+Math.cos(a)*rnd(10,20),y+Math.sin(a)*rnd(10,20),pick(["#ffe95c","#43e8ff"]));}
break;
case "boom": // 爆炸:白闪→同心冲击环→碎片+灰烟
G.novas.push({x,y,r:2,maxr:8*s,life:5,col:"#ffffff"});
G.novas.push({x,y,r:4,maxr:22*s,life:14,col:"#ff9a3d"});
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-2.4,2.4),vy:rnd(-2.4,2.4),life:rndi(9,16),col:pick(["#ff9a3d","#ff5566","#fff"]),sz:rndi(1,2),spark:i%2===0});
for(let i=0;i<4;i++)G.parts.push({x,y,vx:rnd(-0.5,0.5),vy:rnd(-0.9,-0.2),life:rndi(20,34),col:"#5a5a6a",sz:2});
break;
case "plasma": // 能量/电浆:青绿光球+电弧
G.novas.push({x,y,r:3,maxr:18*s,life:12,col:"#43e8ff"});
for(let i=0;i<10;i++)G.parts.push({x,y,vx:rnd(-1.7,1.7),vy:rnd(-1.7,1.7),life:rndi(12,24),col:pick(["#43e8ff","#5cff9d","#fff"]),sz:rndi(1,2)});
zapVisual(x,y,x+rnd(-14,14),y+rnd(-14,14),"#43e8ff");
break;
case "dark": // 奇点坍缩:万芒内吸→中心炽闪→三重爆环→紫电撕裂→星芒外喷
for(let i=0;i<26;i++)G.parts.push({x:x+rnd(-44,44),y:y+rnd(-44,44),vx:0,vy:0,life:rndi(8,16),col:pick(["#b05ce8","#7b2fd0","#e0b3ff"]),sz:rndi(1,2),suck:{x,y}});
G.novas.push({x,y,r:2,maxr:15*s,life:7,col:"#ffffff"});
G.novas.push({x,y,r:3,maxr:40*s,life:16,col:"#b05ce8"});
G.novas.push({x,y,r:6,maxr:62*s,life:26,col:"#7b2fd0"});
for(let i=0;i<6;i++){const a=i/6*TAU+rnd(-0.3,0.3);
zapVisual(x,y,x+Math.cos(a)*rnd(24,40)*s,y+Math.sin(a)*rnd(24,40)*s,pick(["#b05ce8","#e0b3ff"]));}
for(let i=0;i<12;i++){const a=rnd(0,TAU);
G.parts.push({x,y,vx:Math.cos(a)*rnd(1.5,3.4),vy:Math.sin(a)*rnd(1.5,3.4),life:rndi(10,20),col:pick(["#fff","#e0b3ff","#b05ce8"]),sz:1,spark:true});}
break;
case "airburst": // 空爆:无冲击环,火花四溅+灰烟翻滚(去圆形区域)
for(let i=0;i<7;i++)G.parts.push({x,y,vx:rnd(-2.2,2.2),vy:rnd(-2.2,2.2),life:rndi(8,14),col:pick(["#ff9a3d","#ffd23d","#fff"]),sz:rndi(1,2),spark:i%2===0});
for(let i=0;i<3;i++)G.parts.push({x,y,vx:rnd(-0.5,0.5),vy:rnd(-0.8,-0.2),life:rndi(16,26),col:"#5a5a6a",sz:2});
break;
case "executor": // 斩首巡航弹:白炽闪→三重冲击环→火舌上卷→钢片四溅→浓烟
G.novas.push({x,y,r:3,maxr:15,life:6,col:"#ffffff"});
G.novas.push({x,y,r:4,maxr:34,life:15,col:"#ff9a3d"});
G.novas.push({x,y,r:7,maxr:54,life:24,col:"#ff5566"});
for(let i=0;i<14;i++){const a=rnd(0,TAU);
G.parts.push({x,y,vx:Math.cos(a)*rnd(1.8,3.6),vy:Math.sin(a)*rnd(1.8,3.6),life:rndi(10,18),col:pick(["#ffd23d","#ff9a3d","#fff"]),sz:rndi(1,2),spark:true});}
for(let i=0;i<8;i++)G.parts.push({x:x+rnd(-4,4),y,vx:rnd(-1.2,1.2),vy:rnd(-2.4,-0.8),life:rndi(16,28),col:pick(["#ff9a3d","#ff6b3d"]),sz:2,grav:0.04});
for(let i=0;i<6;i++)G.parts.push({x,y,vx:rnd(-0.7,0.7),vy:rnd(-1,-0.3),life:rndi(24,40),col:"#5a5a6a",sz:2});
break;
case "holy": // 神圣:金色光环+光粒上飘
G.novas.push({x,y,r:3,maxr:24*s,life:16,col:"#ffcf5c"});
for(let i=0;i<9;i++)G.parts.push({x:x+rnd(-6,6),y:y+rnd(-6,6),vx:rnd(-0.4,0.4),vy:rnd(-1,-0.2),life:rndi(18,32),col:pick(["#ffcf5c","#fff4d9","#fff"]),sz:1});
break;
case "poison": // 毒素:绿泡破裂+黏液溅射
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-1,1),vy:rnd(-1.5,-0.2),life:rndi(14,26),col:pick(["#9dff5c","#5cff9d"]),sz:rndi(1,3),grav:0.06});
break;
case "kinetic": // 动能:金属碎片+火花
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-2.6,2.6),vy:rnd(-2.6,2.6),life:rndi(7,14),col:pick(["#c8d8ff","#8c9ab8","#fff"]),sz:1,spark:true});
break;
case "dawnburst": // 黎明重炮:白炽核→四重热环→爆芯闪光→火星条→烟尘→紫烬
G.novas.push({x,y,r:3,maxr:16*s,life:6,col:"#ffffff"});
G.novas.push({x,y,r:5,maxr:38*s,life:15,col:"#ffc64a"});
G.novas.push({x,y,r:8,maxr:56*s,life:24,col:"#ff4a1f"});
G.novas.push({x,y,r:10,maxr:70*s,life:32,col:"#7a0e0e"});
for(let i=0;i<40;i++){const a=rnd(0,TAU),sp=rnd(1.5,4);
G.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:rndi(18,35),col:pick(["#fff","#fff3d0","#ffc64a","#ff8a2e"]),sz:rndi(1,2),spark:i%3===0});}
for(let i=0;i<18;i++){const a2=rnd(0,TAU),sp2=rnd(2,5);
G.parts.push({x,y,vx:Math.cos(a2)*sp2,vy:Math.sin(a2)*sp2,life:rndi(20,40),col:"#ff8a2e",sz:1,spark:true});}
for(let i=0;i<10;i++)G.parts.push({x:x+rnd(-6,6),y,vx:rnd(-0.6,0.6),vy:rnd(-1.1,-0.4),life:rndi(50,80),col:"#6a4636",sz:2});
for(let i=0;i<6;i++)G.parts.push({x,y,vx:rnd(-0.4,0.4),vy:rnd(-0.8,-0.3),life:rndi(40,70),col:"#c46bff",sz:1});
break;
case "protocol": // 协议重炮:白炽核→三层红黑橙环→橙红径向粒子→黑烟余烬→橙电
G.novas.push({x,y,r:3,maxr:18*s,life:6,col:"#ffffff"});
G.novas.push({x,y,r:5,maxr:34*s,life:14,col:"#ffd54a"});
G.novas.push({x,y,r:8,maxr:52*s,life:22,col:"#ff4a1f"});
for(let i=0;i<30;i++){const a=rnd(0,TAU),sp=rnd(1.6,4.2);
G.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:rndi(14,28),col:pick(["#fff","#ffd54a","#ff5a2e","#26262e"]),sz:rndi(1,2),spark:i%2===0});}
for(let i=0;i<8;i++)G.parts.push({x,y,vx:rnd(-1,1),vy:rnd(-1.6,-0.5),life:rndi(20,34),col:pick(["#26262e","#ff8a2e"]),sz:2,grav:0.05});
for(let i=0;i<4;i++){const a3=rnd(0,TAU);zapVisual(x,y,x+Math.cos(a3)*rnd(18,30),y+Math.sin(a3)*rnd(18,30),"#ff8a2e");}
break;
case "nebulafx": // 星云绽放:紫蓝双环 + 漩涡星尘
G.novas.push({x,y,r:3,maxr:26*s,life:12,col:"#b98cff"});
G.novas.push({x,y,r:6,maxr:40*s,life:20,col:"#5b2bff"});
for(let i=0;i<14;i++)G.parts.push({x,y,vx:rnd(-1.6,1.6),vy:rnd(-1.6,1.6),life:rndi(14,26),col:pick(["#b98cff","#e0b3ff","#7ce9ff","#fff"]),sz:1,spark:i%2===0});
break;
}
}
function explodeBullet(b){
if(b.t==="am"){ // 空爆弹直击:无圈爆炸
areaDamage(b.x,b.y,b.zone||24,b.dmg);
explodeFx(b.x,b.y,"airburst",1.3);
SFX.boom(false);G.shake=Math.max(G.shake,3);
}else if(b.t==="ex"){ // 斩首巡航弹:华丽殉爆
areaDamage(b.x,b.y,b.aoe||26,b.dmg);
explodeFx(b.x,b.y,"executor",1);
SFX.boom(true);G.shake=Math.max(G.shake,8);
}else if(b.t==="tb"||b.t==="tb2"){
areaDamage(b.x,b.y,b.aoe||b.zone||24,b.dmg);
explodeFx(b.x,b.y,"boom",1);
SFX.boom(false);G.shake=Math.max(G.shake,3);
}else if(b.t==="mt"){ // 流星火雨火焰爆炸修复无伤害bug
areaDamage(b.x,b.y,b.aoe||16,b.dmg);
explodeFx(b.x,b.y,"fire",1);G.shake=Math.max(G.shake,2);SFX.boom(false);
}else if(b.t==="bb"){
areaDamage(b.x,b.y,b.aoe,b.dmg);
explodeFx(b.x,b.y,"plasma",1);SFX.boom(false);
}else if(b.t==="sg"){
areaDamage(b.x,b.y,b.aoe,b.dmg);
explodeFx(b.x,b.y,"dark",1.6);
G.shake=Math.max(G.shake,8);SFX.boom(true);
}else if(b.t==="ho"){ // 圣光弹:留下旋转生长的圣光十字
G.holies.push({x:b.x,y:b.y,len:b.len,dmg:b.dmg,t:0,life:36});
explodeFx(b.x,b.y,"holy",0.7);SFX.level();}
else if(b.t==="dw"){ // 黎明重炮:停帧+白闪+巨爆+余震
areaDamage(b.x,b.y,b.aoe||30,b.dmg);
explodeFx(b.x,b.y,"dawnburst",1);
G.hitstop=5;G.shake=Math.max(G.shake,10);
SFX.boom(true);
G.shocks.push({x:b.x,y:b.y,t:22,dmg:b.dmg*0.5,r:(b.aoe||30)*0.7});
}else if(b.t==="ph"){ // 协议重炮:青蓝巨爆
areaDamage(b.x,b.y,b.aoe||26,b.dmg);
explodeFx(b.x,b.y,"protocol",1);
G.shake=Math.max(G.shake,7);SFX.boom(true);
}else if(b.t==="nb"){ // 星云奥秘:绽放群星(星星弹也有伤害)
const ns=b.stars||10;
for(let i=0;i<ns;i++){const a=i/ns*TAU+rnd(-0.15,0.15);
pbul(b.x,b.y,Math.cos(a)*2.6,Math.sin(a)*2.6,b.dmg*0.3,{t:"st",spr:"star4",r:2,life:44});}
explodeFx(b.x,b.y,"nebulafx",1);SFX.boom(false);
}
}
function aimShot(x,y,dmg,spd,spr,slow){
const t=nearestEnemy(x,y,280);let vx=0,vy=-(spd||5);
if(t){const d=Math.hypot(t.x-x,t.y-y)||1;vx=(t.x-x)/d*(spd||5);vy=(t.y-y)/d*(spd||5);}
pbul(x,y,vx,vy,dmg,{r:2,spr,slow});
}
function fireWeapons(){
if(G.boss&&G.boss.transT>0)return; // Boss变形转场全员停火
const p=G.p,fm=pFireMul(),dm=pDmgMul(),wdm=dm*wMul();
/* ---------- 主武器 ---------- */
for(const wk of G.loadout){
const w=WEAPONS[wk],st=w.lvl(G.weapons[wk]),S=G.wst[wk]=G.wst[wk]||{};
G.wt[wk]=(G.wt[wk]||0)-1;
switch(w.beh){
case "streams":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(2,st.int/fm);
for(let i=0;i<st.n;i++){const off=i-(st.n-1)/2;
if(st.skipMid&&Math.abs(off)<=st.skipMid)continue; // 超级霰弹:去掉中央弹丸
const a=-Math.PI/2+off*(st.fan?st.fan*2/Math.max(1,st.n-1):0.09);
pbul(p.x+off*(st.gap||6),p.y-8,Math.cos(a)*st.spd,Math.sin(a)*st.spd,st.dmg*wdm,{r:3,spr:w.spr,poison:st.poison,slow:st.slow});}
SFX.shoot();}
break;
case "flame": // 周期性宽幅喷吐:喷射 burst 帧 → 停歇 cd 帧
if(S.on>0){S.on--;
for(let i=0;i<st.n;i++)pbul(p.x+rnd(-5,5),p.y-10,rnd(-2.1,2.1),-rnd(2.4,3.4),st.dmg*wdm,{t:"f",life:Math.round(st.range/3),pierce:true,r:3});
}else{S.t=(S.t||0)-1;if(S.t<=0){S.on=st.burst;S.t=st.cd;SFX.shoot();}}
break;
case "tesla":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(10,st.int/fm);
const tgts=nearEnemies(p.x,p.y,st.range,st.n);
for(const t of tgts){zapVisual(p.x,p.y-6,t.x,t.y,"#ffe95c");hurtT(t,st.dmg*wdm,true);}
if(tgts.length)SFX.graze();}
break;
case "rail":
S.c=(S.c||0)+1;
if(S.c>st.charge*0.35&&G.time%3===0){const a0=rnd(0,TAU),r0=rnd(10,18);
G.parts.push({x:p.x+Math.cos(a0)*r0,y:p.y-10+Math.sin(a0)*r0,vx:0,vy:0,life:14,col:pick(["#43e8ff","#b98cff","#fff"]),sz:1,suck:{x:p.x,y:p.y-10}});}
if(S.c>=st.charge){S.c=0;
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"rg",r:5,pierce:true,zone:st.zone,trailCol:"#b98cff"});
G.novas.push({x:p.x,y:p.y-10,r:2,maxr:16,life:10,col:"#b98cff"});
G.shake=Math.max(G.shake,5);SFX.boom(false);}
break;
case "deathray":
if(S.beam>0){S.beam--;
const t=nearestEnemy(p.x,p.y,400);
if(t){G.lasers.push({x1:p.x,y1:p.y-8,x2:t.x,y2:t.y,w:st.width,life:2,col:"#ff6b9d"});
if(G.time%2===0)hurtT(t,st.dps*wdm*4,false); // 死光伤害翻倍
if(G.time%3===0)spawnPart(t.x,t.y,pick(["#ff6b9d","#fff"]),1);}
}else{S.c=(S.c||0)+1;
if(S.c>st.charge*0.45&&G.time%2===0){const t2=nearestEnemy(p.x,p.y,400);
if(t2)G.lasers.push({x1:p.x,y1:p.y-8,x2:t2.x,y2:t2.y,w:1,life:2,col:"#7a2a3a"});}
if(S.c>=st.charge){S.c=0;S.beam=st.beam;SFX.od();}}
break;
case "crescent": // 巨型旋刃仅前方±30°两个方向自身旋转贯穿
if(G.wt[wk]<=0){G.wt[wk]=Math.max(20,st.int/fm);
for(const sd of[-1,1]){const ca=-Math.PI/2+sd*Math.PI/6;
pbul(p.x+sd*6,p.y-8,Math.cos(ca)*st.spd,Math.sin(ca)*st.spd,st.dmg*wdm,{t:"c",spr:"cres",pierce:true,r:6,spin:true,scale:1.8});}
SFX.shoot();}
break;
case "fission":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(3,st.int/fm);
pbul(p.x,p.y-8,0,-st.spd,st.dmg*wdm,{t:"s",spr:"fiss",split:st.split,splitAt:st.splitAt||26,r:3});
SFX.shoot();}
break;
case "frost":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(4,st.int/fm);
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"fr",spr:"icelance",pierce:true,r:3,freeze:st.freeze,trailCol:"#7fd8ff"});
SFX.shoot();}
break;
case "homing":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(4,st.int/fm);
for(let i=0;i<st.n;i++)pbul(p.x+(i-(st.n-1)/2)*6,p.y-4,(i-(st.n-1)/2)*0.9,-st.spd,st.dmg*wdm,{t:"m",spr:"msl2",homing:true,r:3,aoe:st.aoe,trailCol:"#e8e8e8"});
SFX.shoot();}
break;
case "bubble":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(6,st.int/fm);
pbul(p.x,p.y-10,rnd(-0.3,0.3),-st.spd,st.dmg*wdm,{t:"bb",spr:"bub",aoe:st.aoe,pierce:true,life:150,r:6});}
break;
case "starfall": // 流星自屏幕底部逆冲而起
if(G.wt[wk]<=0){G.wt[wk]=Math.max(70,st.int/fm);
for(let i=0;i<st.n;i++){const mx=clamp(p.x+rnd(-70,70),10,W-10),my=clamp(p.y-rnd(60,160),20,H-20);
pbul(mx,H+8,rnd(-0.2,0.2),-rnd(4.5,6),st.dmg*wdm,{t:"mt",spr:"met",aoe:st.aoe,ty2:my,r:4,trailCol:"#ff9a3d",rot:true});}
SFX.boom(false);}
break;
case "boom":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(3,st.int/fm);
for(let i=0;i<st.n;i++){const off=i-(st.n-1)/2;
pbul(p.x+off*8,p.y-6,off*0.6,-st.spd,st.dmg*wdm,{t:"b",spr:"boomer",pierce:true,back:true,r:4,bt:39});}
SFX.shoot();}
break;
case "sing":
if(G.wt[wk]<=0){G.wt[wk]=st.int;
pbul(p.x,p.y-10,0,-1.2,st.dmg*wdm,{t:"sg",spr:"sing",pull:st.pull,aoe:st.aoe,life:st.fuse,r:6});}
break;
case "wave":
S.c=(S.c||0)+1;
if(S.c>st.charge*0.35&&G.time%4===0){const a0=rnd(0,TAU),r0=rnd(8,16);
G.parts.push({x:p.x+Math.cos(a0)*r0,y:p.y-10+Math.sin(a0)*r0,vx:0,vy:0,life:12,col:"#43b8ff",sz:1,suck:{x:p.x,y:p.y-10}});}
if(S.c>=st.charge){S.c=0;
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"wv",pierce:true,r:Math.round(st.width/2)+2,trailCol:"#43b8ff"});
G.novas.push({x:p.x,y:p.y-10,r:2,maxr:14,life:8,col:"#43b8ff"});
G.shake=Math.max(G.shake,4);SFX.boom(false);}
break;
case "holy":
if(G.wt[wk]<=0){G.wt[wk]=st.int;
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"ho",spr:"holyo",len:st.len,life:90,r:4});}
break;
case "thunder":
if(G.wt[wk]<=0){G.wt[wk]=Math.max(30,st.int/fm);
for(let i=0;i<st.n;i++){const sx=clamp(p.x+rnd(-70,70),10,W-10),sy=rnd(20,p.y-40);
zapVisual(sx,-4,sx,sy,"#ffe95c");zapVisual(sx+rnd(-10,10),-4,sx,sy,"#43e8ff");
areaDamage(sx,sy,st.aoe,st.dmg*wdm);
explodeFx(sx,sy,"elec",0.7);}
G.shake=Math.max(G.shake,3);SFX.boom(false);}
break;
case "shuriken": // 电弧手里剑:不停留,一路向前飞行放电
if(G.wt[wk]<=0){G.wt[wk]=Math.max(40,st.int/fm);
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"shk",spr:"shuri",pierce:true,r:5,spin:true,life:400,pulse:st.pulse,zapDmg:st.zapDmg*wdm});
SFX.shoot();}
break;
case "volt": // 电能机炮:高频连发,命中连锁电弧(连锁在 collide 结算)
if(G.wt[wk]<=0){G.wt[wk]=Math.max(3,st.int/fm);
for(let i=0;i<st.n;i++){const off=i-(st.n-1)/2;
pbul(p.x+off*st.gap,p.y-8,off*0.5,-st.spd,st.dmg*wdm,{t:"vl",spr:"voltb",r:3,chain:st.chain});}
SFX.shoot();}
break;
case "dawn": // 黎明重炮:长蓄力,一炮巨响
S.c=(S.c||0)+1;
if(S.c>st.charge*0.3&&G.time%3===0){const a0=rnd(0,TAU),r0=rnd(8,20);
G.parts.push({x:p.x+Math.cos(a0)*r0,y:p.y-10+Math.sin(a0)*r0,vx:0,vy:0,life:14,col:pick(["#ffc64a","#ff8a2e","#fff"]),sz:1,suck:{x:p.x,y:p.y-10}});}
if(S.c>=st.charge){S.c=0;
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"dw",spr:"dawnsh",r:5,aoe:st.aoe,trailCol:"#ff8a2e"});
G.novas.push({x:p.x,y:p.y-10,r:2,maxr:18,life:10,col:"#ffc64a"});
G.shake=Math.max(G.shake,5);SFX.boom(false);}
break;
case "protocol": // 协议武装:红橙连发压制 + 周期蓄能重炮
S.t=(S.t||0)-1;
if(S.t<=0){S.t=Math.max(3,st.int/fm);
pbul(p.x-4,p.y-8,0,-st.spd,st.dmg*wdm,{spr:"protb",r:3});
pbul(p.x+4,p.y-8,0,-st.spd,st.dmg*wdm,{spr:"protb",r:3});
SFX.shoot();}
S.hc=(S.hc===undefined?st.heavy:S.hc)-1;
if(S.hc<24&&S.hc>0&&G.time%3===0){const a1=rnd(0,TAU),r1=rnd(6,14);
G.parts.push({x:p.x+Math.cos(a1)*r1,y:p.y-12+Math.sin(a1)*r1,vx:0,vy:0,life:12,col:pick(["#ff5a2e","#ffd54a"]),sz:1,suck:{x:p.x,y:p.y-12}});}
if(S.hc<=0){S.hc=st.heavy;
pbul(p.x,p.y-12,0,-3.6,st.hdmg*wdm,{t:"ph",spr:"protoh",r:6,aoe:st.haoe,trailCol:"#ff5a2e"});
G.novas.push({x:p.x,y:p.y-12,r:2,maxr:14,life:8,col:"#ff5a2e"});
G.shake=Math.max(G.shake,4);SFX.boom(false);}
break;
case "nebula": // 星云奥秘:缓慢软追踪,命中绽放群星
if(G.wt[wk]<=0){G.wt[wk]=Math.max(50,st.int/fm);
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"nb",spr:"neb",r:6,softH:true,stars:st.stars,trailCol:"#b98cff"});
SFX.shoot();}
break;
}
}
/* ---------- 副武器 ---------- */
for(const sk of G.subLoadout){
const sd=SUBS[sk],lv=G.subs[sk],S=G.sst[sk]=G.sst[sk]||{};
switch(sd.beh){
case "broad": S.t=(S.t||0)-1;
if(S.t<=0){S.t=Math.max(40,(76-6*lv)/fm);
const bsp=4.5+0.3*lv;
for(const sd of[-1,1])pbul(p.x+sd*10,p.y-4,sd*bsp*0.707,-bsp*0.707,(14+4*lv)*dm,{spr:"broad",r:3,trailCol:"#c8923e",rot:true});
SFX.shoot();}
break;
case "sweep":
if(S.on>0){S.on--;
const swa=-Math.PI/2+Math.sin(G.time*0.055)*0.55,sx1=p.x,sy1=p.y-8;
const sx2=sx1+Math.cos(swa)*260,sy2=sy1+Math.sin(swa)*260;
G.lasers.push({x1:sx1,y1:sy1,x2:sx2,y2:sy2,w:3,life:2,col:"#43e8ff"});
if(G.time%3===0){const sdx=sx2-sx1,sdy=sy2-sy1,sL2=sdx*sdx+sdy*sdy||1;
for(const e of G.enemies){if(e.dead)continue;
const t2=clamp(((e.x-sx1)*sdx+(e.y-sy1)*sdy)/sL2,0,1);
if(dist2(e.x,e.y,sx1+sdx*t2,sy1+sdy*t2)<(e.r+3)*(e.r+3))hurtEnemy(e,(6+2*lv)*dm*0.34,false);}
if(G.boss){const t2=clamp(((G.boss.x-sx1)*sdx+(G.boss.y-sy1)*sdy)/sL2,0,1);
if(bossHit(sx1+sdx*t2,sy1+sdy*t2,3))hurtBoss((6+2*lv)*dm*0.34);}}}
else{S.t=(S.t||0)-1;if(S.t<=0){S.on=34;S.t=Math.max(90,(170-14*lv)/fm);SFX.od();}}
break;
case "leech": break; // 寄生电浆:被动附着,在 hurtEnemy 中结算
case "orbit":
p.orbA+=0.09;
for(let i=0;i<lv;i++){const a=p.orbA+i*(TAU/lv),bx=p.x+Math.cos(a)*26,by=p.y+Math.sin(a)*26;
for(const e of G.enemies){if(e.dead)continue;
if(dist2(bx,by,e.x,e.y)<(e.r+5)*(e.r+5)&&G.time-(e.bladeT||0)>18){e.bladeT=G.time;hurtEnemy(e,(10+3*lv)*dm,true);}}
if(G.boss&&bossHit(bx,by,5)&&G.time-(G.boss.bladeT||0)>18){G.boss.bladeT=G.time;hurtBoss((10+3*lv)*dm);}}
break;
case "sats": {
const n=1+lv;p.satA=(p.satA||0)+0.07;
for(let i=0;i<n;i++){const a=p.satA+i*(TAU/n),sx=p.x+Math.cos(a)*20,sy=p.y+Math.sin(a)*20;
for(const b of G.eb){if(b.dead)continue;
if(dist2(sx,sy,b.x,b.y)<40){b.dead=true;spawnPart(b.x,b.y,"#43e8ff",2);SFX.graze();}}}
break;}
case "pd": S.t=(S.t||0)-1;
if(S.t<=0){S.t=Math.max(18,Math.round((16-2*lv)/0.3));
let best=null,bd=(60+10*lv)*(60+10*lv);const pdMin=15*15; // 不拦截贴身弹
for(const b of G.eb){if(b.dead)continue;const d=dist2(p.x,p.y,b.x,b.y);if(d<bd&&d>pdMin){bd=d;best=b;}}
if(best){best.dead=true;zapVisual(p.x,p.y-8,best.x,best.y,"#9fd8ff");SFX.hit();}}
break;
case "slowfield": S.t=(S.t||0)-1;
if(S.t<=0){S.t=(330-30*lv)/fm;
G.zones.push({x:p.x,y:p.y-70,r:46+8*lv,slow:true,life:160,col:"#43b8ff"});
SFX.od();}
break;
case "flares": S.t=(S.t||0)-1;
if(S.t<=0){S.t=Math.max(300,Math.round((100-8*lv)*5/fm));
for(let i=0;i<4;i++){const fa=-Math.PI/2+(i-1.5)*0.36;
G.flares.push({x:p.x,y:p.y-6,vx:Math.cos(fa)*0.85,vy:Math.sin(fa)*0.85,life:260});}
SFX.shoot();}
break;
case "seeker": {
if(S.x===undefined){S.x=p.x;S.y=p.y;}
const t=nearestEnemy(S.x,S.y,300);
if(t){const d=Math.hypot(t.x-S.x,t.y-S.y)||1;
if(d>50){S.x+=(t.x-S.x)/d*2.2;S.y+=(t.y-S.y)/d*2.2;}
else{S.x+=Math.cos(G.time*0.05)*1.4;S.y+=Math.sin(G.time*0.05)*1.4;}
S.t=(S.t||0)-1;
if(S.t<=0){S.t=Math.max(16,(32-2*lv)/fm);
const a=Math.atan2(t.y-S.y,t.x-S.x);
pbul(S.x,S.y,Math.cos(a)*5,Math.sin(a)*5,(4+2*lv)*dm,{r:2});}}
else{S.x=lerp(S.x,p.x,0.05);S.y=lerp(S.y,p.y-26,0.05);}
break;}
case "beamdrone": {
const a=G.time*0.03;S.x=p.x+Math.cos(a)*30;S.y=p.y+Math.sin(a)*30;
if(S.on>0){S.on--;
const t=nearestEnemy(S.x,S.y,220);
if(t){G.lasers.push({x1:S.x,y1:S.y,x2:t.x,y2:t.y,w:3,life:2,col:"#ff6b9d"});
if(G.time%3===0)hurtT(t,(6+3*lv)*dm*0.34,false);}}
else{S.t=(S.t||0)-1;if(S.t<=0){S.on=22;S.t=(140-15*lv)/fm;}}
break;}
case "frostnova": S.t=(S.t||0)-1;
if(S.t<=0){S.t=(260-30*lv)/fm;
const r=50+10*lv;
G.novas.push({x:p.x,y:p.y,r:6,maxr:r,life:22,col:"#7fd8ff"});
for(const e of G.enemies)if(!e.dead&&dist2(p.x,p.y,e.x,e.y)<(r+e.r)*(r+e.r)){e.slow=Math.max(e.slow||0,120+30*lv);hurtEnemy(e,(10+5*lv)*dm,true);}
SFX.od();}
break;
case "sticky": S.t=(S.t||0)-1;
if(S.t<=0){S.t=(230-18*lv)/fm;
pbul(p.x,p.y-6,rnd(-0.4,0.4),-2.6,(34+12*lv)*dm,{t:"tb",spr:"sticky",fuse:120,aoe:36,r:4});}
break;
case "magnetar": S.t=(S.t||0)-1;
if(S.t<=0){S.t=(320-25*lv)/fm;
G.novas.push({x:p.x,y:p.y,r:8,maxr:90,life:26,col:"#b05ce8"});
for(const e of G.enemies)if(!e.dead){e.slow=Math.max(e.slow||0,60);hurtEnemy(e,(12+6*lv)*dm,true);}
if(G.boss)hurtBoss((12+6*lv)*dm);
const rr=(70+15*lv)*(70+15*lv);
for(const b of G.eb)if(dist2(p.x,p.y,b.x,b.y)<rr){b.dead=true;spawnPart(b.x,b.y,"#b05ce8",1);}
SFX.boom(true);}
break;
case "executor": S.t=(S.t||0)-1;
if(S.t<=0){S.t=(340-30*lv)/fm;
pbul(p.x,p.y-4,rnd(-0.6,0.6),-2.2,(60+20*lv)*dm,{t:"ex",spr:"cmsl",homing:true,eliteHunt:true,r:4,aoe:26,trailCol:"#ff9a3d"});
SFX.shoot();}
break;
case "airstrike": S.t=(S.t||0)-1; // 副武器:穿透机身,沿途轰炸
if(S.t<=0){S.t=Math.max(70,(160-10*lv)/fm);
for(const sd of[-1,1])
pbul(p.x+sd*8,p.y-2,sd*2.5,0,(16+5*lv)*dm,{t:"am",spr:"amsl",phase:1,pt:0,zone:16,r:3,trailCol:"#9aa4b8",rot:true,pierce:true});
SFX.shoot();}
break;
case "repair": S.t=(S.t||0)-1;
if(S.t<=0){S.t=(30-4*lv)*60;
if(G.hp<G.maxHp){G.hp=Math.min(G.maxHp,G.hp+1);
G.texts.push({x:p.x,y:p.y-12,t:"+1",col:"#5cff9d",life:30});}}
break;
case "thorns": break; // 被动反伤,在碰撞中结算
}
}
/* ---------- 僚机 ---------- */
const dn=G.droneLoadout.length;
for(let i=0;i<dn;i++){
const key=G.droneLoadout[i],dd=DRONES[key],lv=G.drones[key],S=G.dst[key]=G.dst[key]||{};
const a=G.time*0.025+i*(TAU/Math.max(1,dn));
S.x=p.x+Math.cos(a)*24;S.y=p.y+Math.sin(a)*10+6;
S.t=(S.t||0)-1;
const db=G.stat.droneBoost;
switch(dd.beh){
case "aim": if(S.t<=0){S.t=Math.max(18,(34-2*lv)/fm);aimShot(S.x,S.y,(4+2*lv)*dm*db,5);}break;
case "aim2": if(S.t<=0){S.t=Math.max(20,(40-2*lv)/fm);aimShot(S.x-3,S.y,(3+2*lv)*dm*db,5);aimShot(S.x+3,S.y,(3+2*lv)*dm*db,5);}break;
case "snipe": if(S.t<=0){S.t=Math.max(40,(70-4*lv)/fm);aimShot(S.x,S.y,(15+8*lv)*dm*db,7);}break;
case "beam":
if(S.on>0){S.on--;const t=nearestEnemy(S.x,S.y,200);
if(t){G.lasers.push({x1:S.x,y1:S.y,x2:t.x,y2:t.y,w:3,life:2,col:"#ff6b9d"});
if(G.time%3===0)hurtT(t,(5+3*lv)*dm*0.34,false);}}
else if(S.t<=0){S.on=20;S.t=(140-15*lv)/fm;}
break;
case "bomb": if(S.t<=0){S.t=(150-12*lv)/fm;pbul(S.x,S.y,0,-2.5,(12+6*lv)*dm*db,{t:"tb2",spr:"bombp",bt:40,aoe:24,r:3});}break;
case "frostD": if(S.t<=0){S.t=Math.max(24,(44-3*lv)/fm);aimShot(S.x,S.y,(3+2*lv)*dm*db,5,"frost",60+20*lv);}break;
case "teslaD": if(S.t<=0){S.t=Math.max(30,(55-4*lv)/fm);
const t=nearestEnemy(S.x,S.y,85);
if(t){zapVisual(S.x,S.y,t.x,t.y,"#ffe95c");hurtT(t,(6+3*lv)*dm*db,true);}}break;
case "melee": { // 冲撞僚机:加大环绕半径与判定
const a2=G.time*0.07+i*(TAU/Math.max(1,dn));
S.x=p.x+Math.cos(a2)*38;S.y=p.y+Math.sin(a2)*38;
for(const e of G.enemies)if(!e.dead&&dist2(S.x,S.y,e.x,e.y)<(e.r+10)*(e.r+10)&&G.time-(e.ramT||0)>22){e.ramT=G.time;hurtEnemy(e,(8+4*lv)*dm*db,true);}
if(G.boss&&bossHit(S.x,S.y,10)&&G.time-(G.boss.ramT||0)>22){G.boss.ramT=G.time;hurtBoss((8+4*lv)*dm*db);}
break;}
case "heal": if(S.t<=0){S.t=(40-6*lv)*60;
if(G.hp<G.maxHp){G.hp=Math.min(G.maxHp,G.hp+2);G.texts.push({x:p.x,y:p.y-12,t:"+2",col:"#5cff9d",life:30});}}break;
case "block": for(const b of G.eb){if(b.dead)continue;
if(dist2(S.x,S.y,b.x,b.y)<110){b.dead=true;spawnPart(b.x,b.y,"#43e8ff",2);}}break;
case "missile": if(S.t<=0){S.t=(90-8*lv)/fm;pbul(S.x,S.y,rnd(-1,1),-2.5,(10+5*lv)*dm*db,{t:"m",spr:"msl",homing:true,r:3});}break;
case "bladeD": if(S.t<=0){S.t=(80-6*lv)/fm;pbul(S.x,S.y,0,-4,(8+4*lv)*dm*db,{t:"b",spr:"blade",pierce:true,back:true,r:4,bt:34});}break;
case "sweep": // 扫射僚机:周期性一串弹从左到右/从右到左扫过前方扇区
if(S.sw>0){S.sw--;
if(S.sw%2===0){const pr=1-S.sw/14,sa=-Math.PI/2+(pr-0.5)*1.5*(S.dir||1);
pbul(S.x,S.y,Math.cos(sa)*5.2,Math.sin(sa)*5.2,(3+1.5*lv)*dm*db,{r:2,spr:"swep"});}
}else if(S.t<=0){S.dir=S.dir===1?-1:1;S.sw=14;S.t=Math.max(50,(110-8*lv)/fm);}
break;
}
}
}
function nearestEnemy(x,y,maxD){
let best=null,bd=maxD*maxD;
for(const e of G.enemies){if(e.dead)continue;const d=dist2(x,y,e.x,e.y);if(d<bd){bd=d;best=e;}}
if(G.boss){const d=dist2(x,y,G.boss.x,G.boss.y);if(d<bd){best=G.boss;}}
return best;
}

126
js/data.js Normal file
View File

@ -0,0 +1,126 @@
"use strict";
/* ================= 机体(各具特色) ================= */
const SHIPS={
falcon:{name:"猎鹰",spr:"ship0",cost:0,trait:"均衡型 · 无短板",
mod:{spd:1,dmg:1,lives:0,crit:0,dodge:0,hitR:0,mag:1,bombs:0,shield:0,drone:0,droneBoost:1}},
ghost:{name:"幽灵",spr:"ship1",cost:800,trait:"高速 · 闪避+12% · 判定更小",
mod:{spd:1.18,dmg:0.95,lives:-1,crit:0,dodge:0.12,hitR:-0.6,mag:1,bombs:0,shield:0,drone:0,droneBoost:1}},
bastion:{name:"堡垒",spr:"ship2",cost:1500,trait:"重装 · 命+2 · 血量+30 · 自带护盾",
mod:{spd:0.85,dmg:1.1,lives:2,crit:0,dodge:0,hitR:0.5,mag:1.4,bombs:0,shield:1,drone:0,droneBoost:1,hp:30}},
valkyrie:{name:"女武神",spr:"ship3",cost:3000,trait:"自带僚机 · 僚机火力+50%",
mod:{spd:1.05,dmg:1,lives:0,crit:0,dodge:0,hitR:0,mag:1,bombs:0,shield:0,drone:1,droneBoost:1.5}},
asura:{name:"修罗",spr:"ship4",cost:5000,trait:"暴击+15% · 伤害+10% · 命-1",
mod:{spd:1,dmg:1.1,lives:-1,crit:0.15,dodge:0,hitR:0,mag:1,bombs:1,shield:0,drone:0,droneBoost:1}}
};
/* ================= 21 =================
beh: 行为引擎类型 lvl(w): 该等级的战斗参数 */
const WEAPONS={
vulcan:{name:"火神机炮",spr:"trcr",rare:0,beh:"streams",ds:"金色曳光弹雨,可靠的老伙计",
lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:8,spd:5.4,gap:6})},
shotgun:{name:"超级霰弹",spr:"pelt",rare:0,beh:"streams",ds:"弹丸只从两翼倾泻,中央中空",
lvl:w=>({n:5+2*w,dmg:4.2*(1+.18*(w-1)),int:22,spd:5.5,fan:0.5,gap:2,skipMid:1})},
needle:{name:"蜂针刺弹",spr:"ndl",rare:0,beh:"streams",ds:"淬毒细针,命中后持续蚀甲",
lvl:w=>({n:1+Math.floor(w/3),dmg:2.6*(1+.2*(w-1)),int:3,spd:7,gap:4,poison:100})},
flame:{name:"火焰喷射器",icon:"flame",rare:0,beh:"flame",ds:"周期性喷吐的宽幅烈焰,近身湮灭",
lvl:w=>({n:3+Math.floor(w/2),dmg:1.1*(1+.22*(w-1)),range:70+8*w,burst:34+4*w,cd:Math.max(46,74-5*w)})},
shotgun2:{name:"霜冻霰弹",spr:"shard",rare:1,beh:"streams",ds:"冰晶霰弹,大面积冻结减速",
lvl:w=>({n:3+w,dmg:3*(1+.18*(w-1)),int:23,spd:5.2,fan:0.42,gap:2,slow:80+25*w})},
tesla:{name:"电弧发生器",icon:"chain",rare:1,beh:"tesla",ds:"自动电击周围敌人,无处可逃",
lvl:w=>({n:1+Math.floor(w/2),dmg:8*(1+.25*(w-1)),int:44-2*w,range:110})},
railgun:{name:"电磁炮",icon:"rail",rare:2,beh:"rail",ds:"蓄力磁轨贯穿敌阵,命中留下电浆区",
lvl:w=>({dmg:55*(1+.3*(w-1)),charge:120-10*w,spd:11,zone:24+4*w})},
deathray:{name:"死光",icon:"laser",rare:2,beh:"deathray",ds:"蓄力后自动锁敌的死亡射线",
lvl:w=>({dps:0.9*(1+.28*(w-1)),charge:160-12*w,beam:55,width:5+2*w})},
crescent:{name:"月牙斩",spr:"cres",rare:1,beh:"crescent",ds:"巨型旋刃缓慢斩出,贯穿敌阵",
lvl:w=>({dmg:44*(1+.24*(w-1)),int:120,spd:3.4})},
fission:{name:"分裂弹",spr:"fiss",rare:1,beh:"fission",ds:"飞行一段距离后炸裂为散射弹幕",
lvl:w=>({dmg:6*(1+.22*(w-1)),int:20,spd:5.2,split:3+Math.floor(w/2),splitAt:26})},
frost:{name:"冰冻枪",spr:"icelance",rare:1,beh:"frost",ds:"贯穿冰枪,命中将敌人彻底冻结",
lvl:w=>({dmg:6*(1+.2*(w-1)),int:16,spd:6,freeze:70+25*w})},
swarm:{name:"蜂巢导弹",spr:"msl2",rare:1,beh:"homing",ds:"自动追踪的导弹蜂群,命中爆裂",
lvl:w=>({n:1+w,dmg:2.2*(1+.2*(w-1)),int:22,spd:3.2,aoe:10})},
bubble:{name:"重力泡泡",spr:"bub",rare:1,beh:"bubble",ds:"缓慢漂浮的大泡泡,爆裂冲击",
lvl:w=>({dmg:18*(1+.25*(w-1)),int:26,spd:1.4,aoe:30+4*w})},
starfall:{name:"流星火雨",spr:"met",rare:2,beh:"starfall",ds:"流星自脚下逆冲而起,轰炸前方空域",
lvl:w=>({n:3+Math.floor(w/2),dmg:16*(1+.25*(w-1)),int:150-8*w,aoe:16})},
boomerang:{name:"回旋镖",spr:"boomer",rare:0,beh:"boom",ds:"亮蓝回旋刃,远射贯穿后回旋返航",
lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:21,spd:4.8})},
singularity:{name:"奇点炮",spr:"sing",rare:2,beh:"sing",ds:"发射微型黑洞,牵引后剧烈坍缩",
lvl:w=>({dmg:40*(1+.3*(w-1)),int:200,pull:60,aoe:40,fuse:100})},
wavecannon:{name:"波动炮",spr:"waveb",rare:2,beh:"wave",ds:"蓄力斩出贯穿敌阵的剑气弧光",
lvl:w=>({dmg:32*(1+.3*(w-1)),charge:100-8*w,spd:5,width:14+6*w})},
holy:{name:"圣光弹",spr:"holyo",rare:2,beh:"holy",ds:"爆裂为旋转生长的圣光十字",
lvl:w=>({dmg:13*(1+.25*(w-1)),int:65,spd:3,len:16+3*w})},
thundercall:{name:"落雷术",spr:"thnd",rare:1,beh:"thunder",ds:"呼唤天雷轰击前方区域",
lvl:w=>({n:2+Math.floor(w/2),dmg:12*(1+.25*(w-1)),int:90-4*w,aoe:13})},
arcshu:{name:"电弧手里剑",spr:"shuri",rare:2,beh:"shuriken",ds:"旋转雷刃一路放电,贯穿全场",
lvl:w=>({dmg:6*(1+.22*(w-1)),int:95-4*w,spd:2.2,zapDmg:5*(1+.25*(w-1)),pulse:20})},
volt:{name:"电能机炮",spr:"voltb",rare:1,beh:"volt",ds:"高频电能弹,命中后电弧连锁跳跃",
lvl:w=>({n:1+Math.floor(w/3),dmg:3.2*(1+.2*(w-1)),int:5,spd:6.2,gap:5,chain:2+Math.floor(w/2)})},
dawn:{name:"黎明重炮",spr:"dawnsh",rare:2,beh:"dawn",ds:"长蓄力重炮,一炮轰出黎明",
lvl:w=>({dmg:70*(1+.3*(w-1)),charge:170-12*w,spd:5,aoe:30+3*w})},
proto:{name:"协议武装",spr:"protb",rare:2,beh:"protocol",ds:"青蓝连发压制,周期性蓄能重炮",
lvl:w=>({dmg:3*(1+.2*(w-1)),int:6,spd:6.5,heavy:120-8*w,hdmg:36*(1+.28*(w-1)),haoe:26+3*w})},
nebula:{name:"星云奥秘",spr:"neb",rare:2,beh:"nebula",ds:"缓慢追踪的奥术漩涡,命中绽放群星",
lvl:w=>({dmg:12*(1+.25*(w-1)),int:100-5*w,spd:1.6,stars:10+2*w})}
};
const MAINS=Object.keys(WEAPONS);
/* ================= 18 副武器 ================= */
const SUBS={
broad:{name:"侧舷火炮",spr:"broad",rare:0,max:5,beh:"broad",ds:l=>`舷炮斜向前方45°齐射Lv${l+1}`},
sweep:{name:"探照扫射",icon:"laser",rare:1,max:5,beh:"sweep",ds:l=>`往复摆动的扫描光束Lv${l+1}`},
leech:{name:"寄生电浆",icon:"chain",rare:1,max:3,beh:"leech",ds:l=>`命中附着电浆水蛭吸血回复结构Lv${l+1}`},
orbit:{name:"环绕光刃",spr:"blade",rare:1,max:4,beh:"orbit",ds:l=>`光刃+1当前${l+1}把)`},
sats:{name:"卫星屏障",spr:"sat",rare:1,max:3,beh:"sats",ds:l=>`环绕卫星可格挡敌弹Lv${l+1}`},
pd:{name:"点防御炮",spr:"dGun",rare:1,max:4,beh:"pd",ds:l=>`自动拦截来袭敌弹不打贴身弹Lv${l+1}`},
slowfield:{name:"时间力场",spr:"grv",rare:2,max:3,beh:"slowfield",ds:l=>"力场内敌人与敌弹大幅减速Lv"+(l+1)},
flares:{name:"热诱弹舱",spr:"flare",rare:0,max:4,beh:"flares",ds:l=>"扇形抛射4枚缓速热诱弹各诱爆一枚敌弹Lv"+(l+1)},
seeker:{name:"索敌浮游炮",spr:"dGun2",rare:1,max:3,beh:"seeker",ds:l=>"离机索敌的浮游炮Lv"+(l+1)},
beamdrone:{name:"光束浮游炮",spr:"dBeam",rare:2,max:3,beh:"beamdrone",ds:l=>"环绕发射光束的浮游单元Lv"+(l+1)},
frostnova:{name:"冰霜新星",spr:"frost",rare:1,max:3,beh:"frostnova",ds:l=>"周期性冻结周围敌人Lv"+(l+1)},
thorns:{name:"反伤棘甲",icon:"crit",rare:1,max:3,beh:"thorns",ds:l=>"撞击你的敌人遭到反噬Lv"+(l+1)},
sticky:{name:"粘着炸弹",spr:"sticky",rare:0,max:3,beh:"sticky",ds:l=>"粘附敌人的定时高爆炸弹Lv"+(l+1)},
magnetar:{name:"磁暴环",icon:"mag",rare:2,max:3,beh:"magnetar",ds:l=>"周期性电磁脉冲消弹伤敌Lv"+(l+1)},
executor:{name:"斩首巡航弹",spr:"cmsl",rare:2,max:3,beh:"executor",ds:l=>"巡航导弹猎杀最强之敌Lv"+(l+1)},
airstrike:{name:"空爆弹",spr:"amsl",rare:1,max:5,beh:"airstrike",ds:l=>"双弹侧掠折向直上穿透机身沿途轰炸Lv"+(l+1)},
repair:{name:"修复无人机",spr:"dHeal",rare:1,max:3,beh:"repair",ds:l=>"持续修复机体结构Lv"+(l+1)}
};
const SUB_KEYS=Object.keys(SUBS);
/* ================= 12 僚机 ================= */
const DRONES={
gunner:{name:"机枪僚机",spr:"dGun",rare:0,max:3,beh:"aim",ds:l=>"协同瞄准射击Lv"+(l+1)},
gunner2:{name:"双翼机枪手",spr:"dGun2",rare:1,max:3,beh:"aim2",ds:l=>"双管僚机Lv"+(l+1)},
sniperd:{name:"狙击僚机",spr:"dSnip",rare:1,max:3,beh:"snipe",ds:l=>"高伤点射Lv"+(l+1)},
beamer:{name:"光束僚机",spr:"dBeam",rare:2,max:3,beh:"beam",ds:l=>"周期光束Lv"+(l+1)},
bomber:{name:"轰炸僚机",spr:"dBomb",rare:1,max:3,beh:"bomb",ds:l=>"投掷爆炸弹Lv"+(l+1)},
frostd:{name:"冰霜僚机",spr:"dFrost",rare:1,max:3,beh:"frostD",ds:l=>"减速弹Lv"+(l+1)},
teslaD:{name:"电弧僚机",spr:"dTesla",rare:2,max:3,beh:"teslaD",ds:l=>"近身电击Lv"+(l+1)},
rammer:{name:"冲撞僚机",spr:"dRam",rare:1,max:3,beh:"melee",big:1.6,ds:l=>"高速环绕撞击Lv"+(l+1)},
healer:{name:"修复僚机",spr:"dHeal",rare:1,max:3,beh:"heal",ds:l=>"缓慢回复结构值Lv"+(l+1)},
shieldd:{name:"护盾僚机",spr:"dShield",rare:2,max:3,beh:"block",big:1.5,ds:l=>"环绕格挡敌弹Lv"+(l+1)},
missileD:{name:"导弹僚机",spr:"dMsl",rare:1,max:3,beh:"missile",ds:l=>"追踪导弹Lv"+(l+1)},
bladeD:{name:"刃舞僚机",spr:"dBlade",rare:1,max:3,beh:"bladeD",ds:l=>"投掷回旋刃Lv"+(l+1)},
sweeper:{name:"扫射僚机",spr:"dSweep",rare:1,max:3,beh:"sweep",ds:l=>"扇形区域来回扫射火力覆盖Lv"+(l+1)}
};
const DRONE_KEYS=Object.keys(DRONES);
/* ================= 8 被动强化 ================= */
const PASSIVES={
fire:{name:"火控雷达",icon:"fire",rare:0,max:5,ds:()=>"射速 +12%"},
crit:{name:"暴击芯片",icon:"crit",rare:1,max:4,ds:()=>"暴击率 +8%"},
shield:{name:"能量护盾",icon:"shield",rare:1,max:3,ds:l=>l?"护盾充能加快":"获得可格挡的护盾"},
nano:{name:"纳米修复",icon:"nano",rare:1,max:3,ds:()=>"血量上限+25 并回复40"},
mag:{name:"引力核心",icon:"mag",rare:0,max:3,ds:()=>"拾取范围 +60%"},
bounty:{name:"赏金协议",icon:null,spr:"coin",rare:0,max:3,ds:()=>"星币获取 +30%"},
odcore:{name:"超频核心",icon:null,spr:"odc",rare:1,max:3,ds:()=>"暴走充能+30%,时长+1s"},
ghost:{name:"幽灵装甲",icon:"ghost",rare:2,max:2,ds:()=>"闪避率 +10%"}
};
const PAS_KEYS=Object.keys(PASSIVES);
/* ---- ----
装备数量 每把武器伤害系数[1]100% [2]80% [3]60% [4]50% [5]45%
总火力 = 系数×数量敌人血量与数量随总火力提升 */
const WMUL=[0,1,0.8,0.6,0.5,0.45];
function wCount(){return Math.max(1,G.loadout.length);}
function wMul(){return WMUL[clamp(wCount(),1,5)];}
function totWMul(){return wMul()*wCount();}
function enemyScale(){return 1+0.22*(totWMul()-1);} // 血量缩放
function budgetScale(){return 1+0.22*(totWMul()-1);} // 数量缩放

385
js/enemies.js Normal file
View File

@ -0,0 +1,385 @@
"use strict";
/* ================= 敌人图鉴 ================= */
const ENEMIES={
scout:{hp:10,spd:1.15,score:100,r:5,gems:2,coin:.3,beh:"drift",fire:null,cost:1,min:0},
weaver:{hp:16,spd:1.3,score:160,r:6,gems:2,coin:.35,beh:"sine",fire:null,cost:1.4,min:2},
kami:{hp:8,spd:1.5,score:120,r:5,gems:2,coin:.25,beh:"dive",fire:null,death:"ring",cost:1.2,min:3},
splitter:{hp:26,spd:1,score:220,r:7,gems:2,coin:.4,beh:"sine",fire:{t:"aim",cd:145,spd:1.9},death:"split",cost:2,min:4},
mine:{hp:22,spd:.45,score:180,r:6,gems:2,coin:.4,beh:"drift",fire:{t:"ring8",cd:185,spd:1.25},cost:2,min:5},
gunship:{hp:55,spd:.7,score:420,r:8,gems:4,coin:.6,beh:"hover",fire:{t:"burst5",cd:160,spd:1.65},cost:4,min:6},
sniper:{hp:30,spd:.6,score:300,r:6,gems:3,coin:.5,beh:"hover",fire:{t:"snipe",cd:210,spd:3.0},cost:3,min:8},
rock:{hp:30,spd:1,score:40,r:8,gems:0,coin:.1,beh:"spin",fire:null,cost:0,min:0,neutral:true},
mini:{hp:5,spd:1.8,score:60,r:4,gems:1,coin:.15,beh:"dive",fire:null,cost:.5,min:0}
};
function tierN(){ // 难度层数:无尽=波次,闯关=关卡*2+小波(图鉴演示等 wave=0 场景会为负按0计防止hpMul产生NaN
return Math.max(0,G.mode==="endless"?G.wave-1:(G.stage-1)*2+G.wave-1);
}
function spawnEnemy(type,x,y,elite){
const d=ENEMIES[type],n=tierN();
const hpM=BAL.hpMul(n)*(G.diff===1?BAL.hard.hp:1)*(G.mutator==="ARMOR"?1.25:1)*(elite?4:1)*enemyScale();
const sMul=(type==="kami"||type==="mini")?Math.min(1.25,1+0.015*n):BAL.spdMul(n); // 突袭机保持高速,其余随波次减速
const e={type,x,y,hp:d.hp*hpM,maxhp:d.hp*hpM,r:d.r*(elite?1.4:1),spd:d.spd*sMul*(G.mutator==="GOLD"?1.1:1),
score:d.score*(elite?5:1),gems:elite?5:d.gems,coin:elite?1:d.coin,elite:!!elite,
beh:d.beh,fire:d.fire?Object.assign({},d.fire):null,death:d.death||null,neutral:!!d.neutral,
t:0,ph:rnd(0,TAU),ft:rnd(0,60),hy:rnd(50,90),vx:0,vy:0,ax:0,flash:0,dead:false,spr:SPR[(elite?"e_":"")+type]||SPR[type]||SPR.scout};
e.dw=Math.round(e.spr.width*(elite?1.4:1)); e.dh=Math.round(e.spr.height*(elite?1.4:1));
if(elite&&!e.fire)e.fire={t:"aim",cd:140,spd:1.9}; // 精英杂兵获得弱射击能力
if(e.fire)e.fire.cd=Math.round(e.fire.cd/(BAL.fireMul(n)*(G.diff===1?BAL.hard.fire:1)*(G.mutator==="FRENZY"?1.3:1)*(elite?0.7:1)));
e.x0=x;
if(e.beh==="dive"){const a=Math.atan2(G.p.y-y,G.p.x-x);e.vx=Math.cos(a)*e.spd;e.vy=Math.max(0.6,Math.sin(a)*e.spd);}
if(e.beh==="spin"){e.vx=rnd(-0.5,0.5);e.rot=rnd(0,TAU);}
G.enemies.push(e);
}
/* ---- 敌机行为 ---- */
function updateEnemy(e){
e.t++; if(e.flash>0)e.flash--; if(e.hpt>0)e.hpt--;
let spM=1;
if(e.slow>0){e.slow--;spM=0.45;} // 冻结减速
if(e.frozen>0){e.frozen--;spM=0; // 冰冻枪:彻底冻结
if(G.time%8===0)G.parts.push({x:e.x+rnd(-5,5),y:e.y+rnd(-5,5),vx:0,vy:0.2,life:14,col:"#cfeaff",sz:1});}
for(const z of G.zones)if(z.slow&&dist2(e.x,e.y,z.x,z.y)<(z.r+e.r)*(z.r+e.r)){spM*=0.4;break;} // 时间力场
if(e.dot){e.dot.t--;e.hp-=e.dot.dps/60; // 中毒蚀甲
if(e.hp<=0){killEnemy(e);return;}
if(e.dot.t<=0)e.dot=null;}
if(e.leech){e.leech.t--;e.hp-=e.leech.dps/60; // 寄生电浆:吸血
G.hp=Math.min(G.maxHp,G.hp+e.leech.dps*0.3/60);
if(G.time%10===0)G.parts.push({x:e.x,y:e.y,vx:0,vy:0,life:12,col:"#b05ce8",sz:1,suck:{x:G.p.x,y:G.p.y}});
if(e.hp<=0){killEnemy(e);return;}
if(e.leech.t<=0)e.leech=null;}
if(spM>0)switch(e.beh){ // 冻结(spM=0)时完全定身
case "drift": e.y+=e.spd*spM; e.x=clamp(e.x0+Math.sin(e.t*0.025+e.ph)*10,10,W-10); break;
case "sine": e.y+=e.spd*spM; e.x=clamp(e.x0+Math.sin(e.t*0.035+e.ph)*18,10,W-10); break;
case "dive": e.x+=e.vx*spM; e.y+=e.vy*spM; if(e.type==="kami"&&e.t%30===0){const ap=aimPt(),a=Math.atan2(ap.y-e.y,ap.x-e.x);e.vx=lerp(e.vx,Math.cos(a)*e.spd*1.4,0.15);e.vy=lerp(e.vy,Math.sin(a)*e.spd*1.4,0.15);} break;
case "hover": if(e.y<e.hy)e.y+=e.spd*1.6*spM; else e.x=clamp(e.x0+Math.sin(e.t*0.015+e.ph)*8,10,W-10); break;
case "spin": e.y+=e.spd*spM; e.x+=e.vx*spM; e.rot+=0.03; break;
}
// 开火(独立计时器,与摆动相位完全分离)
if(e.fire&&e.y>6&&e.y<H-60){
e.ft+=spM;
if(e.ft>e.fire.cd){e.ft=0;enemyFire(e);}
}
if(e.y>H+20||e.x<-30||e.x>W+30)e.dead=true;
}
function ebul(x,y,a,spd,spr,dmg){
spr=spr||"orbR";
const bb={x,y,vx:Math.cos(a)*spd,vy:Math.sin(a)*spd,r:2.4,spr,halo:BHALO[spr]||"#ff5566",
dmg:Math.round((dmg||16)*(G.diff===1?1.3:1)),grazed:false};
G.eb.push(bb);SFX.eshoot();return bb;
}
function aimPt(){return{x:G.p.x,y:G.p.y};}
function enemyFire(e){
const f=e.fire,ap=aimPt(),a0=Math.atan2(ap.y-e.y,ap.x-e.x);
if(f.t==="aim")ebul(e.x,e.y+4,a0,f.spd,"orbR",16);
else if(f.t==="spread3")for(let i=-1;i<=1;i++)ebul(e.x,e.y+4,a0+i*0.32,f.spd,"orbP",16);
else if(f.t==="burst5")for(let i=-2;i<=2;i++)ebul(e.x,e.y+6,a0+i*0.2,f.spd,"orbO",18);
else if(f.t==="ring8")for(let i=0;i<8;i++)ebul(e.x,e.y,i/8*TAU+e.t*0.01,f.spd,"orbG",18);
else if(f.t==="snipe"){ebul(e.x,e.y+4,a0,f.spd,"orbP",24);ebul(e.x,e.y+4,a0+0.05,f.spd,"orbP",24);ebul(e.x,e.y+4,a0-0.05,f.spd,"orbP",24);}
}
/* ================= 突变词缀 ================= */
const MUTS={
METEOR:{name:"陨石带",ds:"陨石坠落 · 星币+20%",col:"#ff9a3d"},
ELITE:{name:"精英潮",ds:"精英概率大幅提升",col:"#ffd23d"},
SWARM:{name:"虫群",ds:"额外小型机群来袭",col:"#b05ce8"},
FRENZY:{name:"弹雨狂欢",ds:"敌射速+30% · 分数+30%",col:"#ff5566"},
GOLD:{name:"黄金波",ds:"星币×2 · 敌速+10%",col:"#ffcf5c"},
ARMOR:{name:"装甲波",ds:"敌甲+25% · 经验+30%",col:"#8c9ab8"}
};
/* ================= 波次生成 ================= */
function startWave(){
G.wave++; G.waveT=0; G.spawning=[];
const n=tierN();
// 词缀
G.mutator=null;
if(G.mode==="endless"&&G.wave>=3)G.mutator=pick(Object.keys(MUTS));
else if(G.mode==="stage"&&G.stage>=3&&Math.random()<0.5)G.mutator=pick(Object.keys(MUTS));
// Boss 波?
const isBoss = G.mode==="endless" ? G.wave%5===0 : G.wave>(G.stage>=3?4:3);
if(isBoss){startBoss();return;}
// 预算制生成:不开火的杂兵为主(~70%),射手为少数(~30%且有同屏上限)
let budget=(6+n*2.2)*(G.mode==="stage"?1+G.stage*0.12:1)*budgetScale();
const fodder=["scout","weaver","kami"].filter(k=>ENEMIES[k].min<=n);
const shooters=["splitter","mine","gunship","sniper"].filter(k=>ENEMIES[k].min<=n);
const eC=BAL.eliteC(n)*(G.mutator==="ELITE"?4:1);
const shooterCap=2+Math.floor(n/3);
let shooterN=0,delay=30;
while(budget>0){
let type;
if(shooters.length&&shooterN<shooterCap&&Math.random()<0.32){type=pick(shooters);shooterN++;}
else type=pick(fodder.length?fodder:shooters);
const d=ENEMIES[type];
const form=pick(["line","v","col","scatter","pincer"]);
const cnt=Math.min(rndi(2,4),Math.max(1,Math.floor(budget/d.cost)));
const elite=Math.random()<eC;
placeFormation(form,type,cnt,delay,elite);
budget-=d.cost*cnt*(elite?3:1);
delay+=rndi(55,110);
}
if(G.mutator==="SWARM")for(let i=0;i<8;i++)G.spawning.push({type:"mini",x:rnd(15,W-15),y:-10-i*8,delay:60+i*25});
if(G.mutator==="METEOR")for(let i=0;i<6;i++)G.spawning.push({type:"rock",x:rnd(10,W-10),y:-14,delay:40+i*70});
const mn=G.mutator?" · "+MUTS[G.mutator].name:"";
banner((G.mode==="endless"?"WAVE "+G.wave:"第 "+G.stage+"-"+G.wave+" 波")+mn, G.mutator?MUTS[G.mutator].col:"#41e6ff");
}
function placeFormation(form,type,cnt,delay,elite){
const xs=[];
if(form==="line")for(let i=0;i<cnt;i++)xs.push(W*(i+1)/(cnt+1));
else if(form==="v")for(let i=0;i<cnt;i++)xs.push(W/2+(i-(cnt-1)/2)*34);
else if(form==="col")for(let i=0;i<cnt;i++)xs.push(rnd(40,W-40));
else if(form==="pincer")for(let i=0;i<cnt;i++)xs.push(i%2?rnd(15,50):rnd(W-50,W-15));
else for(let i=0;i<cnt;i++)xs.push(rnd(20,W-20));
xs.forEach((x,i)=>G.spawning.push({type,x:clamp(x,12,W-12),y:-12-(form==="col"?i*26:0),delay:delay+i*(form==="v"?12:20),elite:elite&&i===0}));
}
function updateSpawner(){
for(const s of G.spawning){s.delay--;if(s.delay<=0&&!s.done){s.done=true;spawnEnemy(s.type,s.x,s.y,s.elite);}}
G.spawning=G.spawning.filter(s=>!s.done);
if(G.mode==="rush")return; // Boss挑战无波次推进
// 波次结束判定
if(!G.boss&&G.spawning.length===0&&G.enemies.length===0){
G.betweenT--;
if(G.betweenT<=0){G.betweenT=80;
// 波次通关奖励
if(G.wave>0){const bc=G.wave*2;addCoins(bc);
G.texts.push({x:W/2,y:H*0.5,t:"波次奖励 ✦"+Math.round(bc*pCoinMul()),col:"#ffcf5c",life:80});}
startWave();
}
}else G.betweenT=80;
}
/* ================= Boss 战 ================= */
/* 8大Boss1种主攻击模式 + 同时1~2种随机小攻击血量阈值触发二/三阶段变形 */
const BOSSES=[
{name:"巡逻巡洋舰",sub:"PATROL CRUISER",hp:950,col:"#e0393e",r:22,main:"cruiser",minors:["snipe","ring"],phases:2},
{name:"太空原种",sub:"SPACE SPAWN",hp:1350,col:"#ff8fc8",r:20,rr:[20,34],ryy:[26,18],wide:1,main:"spawn",minors:["wiggle","spawnMini","flowerSnipe"],phases:2},
{name:"歼灭者",sub:"ANNIHILATOR",hp:1700,col:"#b05ce8",r:22,main:"annihilate",minors:["aimSpread","spawnMine"],phases:3},
{name:"暗黑突击",sub:"DARK ASSAULT",hp:2100,col:"#ff3355",r:9,rr:[9,10,11],ryy:[5,6,7],main:"assault",minors:["rain","aimSpread"],phases:3},
{name:"激光钢琴",sub:"LASER PIANO",hp:2500,col:"#43e8ff",r:26,rr:[100,100],ryy:[30,30],main:"piano",minors:["topRain","ring"],phases:2,static:1},
{name:"母巢核心",sub:"HIVE CORE",hp:2800,col:"#ff9a3d",r:22,main:"hive",minors:["spawnMini","aimSpread","ring"],phases:3},
{name:"夺权者X",sub:"USURPER X",hp:3700,col:"#e8f4ff",r:24,rr:[52,26],ryy:[34,40],wide:1,main:"usurper",minors:["chain","pillar","aimSpread"],phases:2},
{name:"暗黑魔碟",sub:"DARK SAUCER",hp:4500,col:"#8a5cff",r:24,rr:[30,32,34],main:"saucer",minors:["ringBig","rain"],phases:3}
];
/* Boss命中判定支持椭圆宽扁机型rr=x半径ry=y半径 */
function bossHit(x,y,r){
const B=G.boss;if(!B)return false;
const dx=x-B.x,dy=y-B.y,rr=r||0;
if(B.ry){const rx=B.r+rr,ry2=B.ry+rr;return dx*dx/(rx*rx)+dy*dy/(ry2*ry2)<1;}
const c=B.r+rr;return dx*dx+dy*dy<c*c;
}
function shuffled(n){const a=[];for(let i=0;i<n;i++)a.push(i);for(let i=n-1;i>0;i--){const j=rndi(0,i),t=a[i];a[i]=a[j];a[j]=t;}return a;}
function bossBeam(x1,y1,x2,y2,w,col,tele,dur,dmg,src){ // Boss光束tele帧预警→dur帧伤害src={b,ox,oy,followX2}时发射点跟随Boss
G.bbeams.push({x1,y1,x2,y2,w,col,tele,life:dur,dmg:dmg||20,cd:0,src:src||null});
}
function spawnFunnels(b,n){ // 夺权者X银钉无人机环绕僚机瞄准/散射轮换)
for(let i=0;i<n;i++)
G.funnels.push({x:b.x,y:b.y,sa:i/n*TAU+0.5,t:rndi(0,40),fireT:rndi(50,110)+i*18,mode:i%2?"aim":"scatter",flashT:0});
}
function updateFunnels(){
const b=G.boss;
for(const f of G.funnels){
f.t++;if(f.flashT>0)f.flashT--;
if(b&&!b.dead){f.sa+=0.014;
f.x+=(b.x+Math.cos(f.sa)*46-f.x)*0.09;
f.y+=(b.y+16+Math.sin(f.sa)*20-f.y)*0.09;}
if(b&&b.laserT>0)continue; // 巨宽核心激光期间僚机停火(仍环绕)
f.fireT--;
if(f.fireT<=0){ // 银色高弹速长钉:瞄准单发 / 三发散射 轮换
const ap=aimPt(),a=Math.atan2(ap.y-f.y,ap.x-f.x);
if(f.mode==="aim"){ebul(f.x,f.y,a,4.8,"dartS",15);f.mode="scatter";f.fireT=rndi(64,96);}
else{for(let k=-1;k<=1;k++)ebul(f.x,f.y,a+k*0.24,3.9,"dartS",13);f.mode="aim";f.fireT=rndi(100,140);}
f.flashT=5;}
}
G.funnels=G.funnels.filter(f=>!f.dead);
}
function startBoss(){
const seq=[0,1,2,3,4,6]; // 闯关顺序stage6=夺权者X压轴母巢/魔碟在无尽轮换
const idx=G.mode==="rush"?G.rushIdx:(G.mode==="stage"?seq[Math.min(5,G.stage-1)]:(G.bossN%8));
const cyc=G.mode==="stage"?0:Math.floor(G.bossN/8);
const b=BOSSES[idx],n=tierN();
const hpM=(G.diff===1?BAL.hard.hp:1)*(1+0.35*cyc)*(G.mode==="endless"?1+0.55*G.bossN+0.1*G.wave:1+0.12*(G.stage-1))*(G.mode==="rush"?10:1); // Boss挑战模式血量×10
G.boss={idx,x:W/2,y:-60,ty:64,hp:b.hp*hpM,maxhp:b.hp*hpM,r:(b.rr?b.rr[0]:b.r),ry:(b.ryy?b.ryy[0]:undefined),t:0,phase:0,transT:0,
col:b.col,name:b.name,sub:b.sub,fireM:BAL.fireMul(n)*(G.diff===1?BAL.hard.fire:1),dead:false,
mainT:100,minorT:9999,minorSet:[],rerollT:340,ax:W/2,ay:64};
G.bossN++;SFX.warn();MUS.start(true);
banner("WARNING — "+b.name,"#ff5566",150);
}
function updateBoss(){
const b=G.boss;if(!b)return;
b.t++;
if(b.y<b.ty){b.y+=1.2;b.x=W/2;return;}
if(b.arr===undefined)b.arr=b.t; // 降落结束时刻正弦从0起振杜绝瞬移
const def=BOSSES[b.idx];
// —— 阶段检测与变形(锁血转场:清弹+停火+播动画) ——
const ratio=b.hp/b.maxhp,thr=def.phases===2?[0.5]:[0.66,0.33];
let np=0;for(let i=0;i<thr.length;i++)if(ratio<=thr[i])np=i+1;
if(np>b.phase){b.phase=np;b.transT=70;b.r=(def.rr&&def.rr[np])||b.r;b.ry=(def.ryy&&def.ryy[np])||undefined;
G.eb.length=0;G.bbeams.length=0;b.keys=null;SFX.warn();G.shake=Math.max(G.shake,5);
if(b.idx===6){G.funnels.length=0;if(np===1){spawnFunnels(b,2);b.plT=Math.round(300/b.fireM);}} // 夺权者X二阶段换为2架僚机
banner(b.name+" — 第"+["一","二","三"][np]+"形态","#ff5566",90);}
if(b.transT>0){b.transT--;
if(b.transT===0){explodeFx(b.x,b.y,"dark",1.3);G.shake=Math.max(G.shake,6);
for(let i=0;i<10;i++)ebul(b.x,b.y,i/10*TAU,1.5,"orbP",12);} // 变形完成爆发环弹
updateFunnels();return;} // 变形期间不攻击
// —— 冲撞状态机(暗黑突击) ——
if(b.dashSt){
const ds=b.dashSt;
if(ds.ph==="tele"){ds.t--;
if(ds.t<=0){const ap3=aimPt(),a=Math.atan2(ap3.y-b.y,ap3.x-b.x);
ds.ph="go";ds.t=85;ds.vx=Math.cos(a)*7.2;ds.vy=Math.sin(a)*7.2;SFX.warn();}}
else if(ds.ph==="go"){b.x+=ds.vx;b.y+=ds.vy;ds.t--;
if(G.time%9===0){const a5=Math.atan2(aimPt().y-b.y,aimPt().x-b.x);ebul(b.x,b.y,a5,2.7,"orbR",13);} // 冲撞途中瞄准弹
if(ds.t<=0||b.y>H+26||b.y<-46||b.x<-26||b.x>W+26){ds.ph="back";ds.t=160;}}
else{b.x+=(b.ax-b.x)*0.045;b.y+=(b.ay-b.y)*0.045;ds.t--;
if(ds.t<=0||(Math.abs(b.x-b.ax)<5&&Math.abs(b.y-b.ay)<5)){b.dashSt=null;b.mainT=Math.round(70/b.fireM);}}
updateFunnels();return;
}
// —— 常规移动 ——
if(def.static){b.x+=(W/2-b.x)*0.05;b.y+=(52-b.y)*0.02;}
else if(b.idx===3){b.x=W/2+Math.sin((b.t-b.arr)*0.021)*62;b.y=b.ty+Math.sin((b.t-b.arr)*0.013)*15;} // 暗黑突击:小体型高机动
else b.x=W/2+Math.sin((b.t-b.arr)*0.008)*(def.wide?44:70); // 宽体Boss减小游走幅度防止巨翼出屏
const fm=b.fireM*(1+0.22*b.phase);
if(b.fnT>0)b.fnT--;if(b.plT>0)b.plT--;if(b.eyeT>0)b.eyeT--;if(b.hornT>0)b.hornT--;if(b.laserT>0)b.laserT--; // 帧级技能计时器
const lck=(b.laserT||0)>0; // 巨宽核心激光期间压制其他攻击
// —— 主攻击 ——
b.mainT--;
if(b.mainT<=0&&!lck)bossMain(b,def,fm);
// —— 小攻击定期重抽1~2种 ——
b.rerollT--;
if(b.rerollT<=0&&!lck){b.rerollT=rndi(340,460);
const pool=def.minors,cnt=b.phase>=1?2:1;
b.minorSet=[];for(let i=0;i<cnt&&pool.length;i++)b.minorSet.push(pool[rndi(0,pool.length-1)]);
b.minorT=50;}
b.minorT--;
if(b.minorT<=0&&!lck)for(const mk of b.minorSet)bossMinor(b,mk,fm);
updateFunnels();
}
/* ---- Boss 主攻击模式 ---- */
function bossMain(b,def,fm){
const ap2=aimPt(),a0=Math.atan2(ap2.y-b.y,ap2.x-b.x),ph=b.phase;
switch(def.main){
case "cruiser": // 巡洋舰:扇形主炮
for(let i=-2;i<=2;i++)ebul(b.x+i*12,b.y+16,a0+i*0.13,2.2+0.2*ph,"orbR");
b.mainT=Math.round((ph?52:70)/fm);break;
case "spawn": // 太空原种花弹圆形八刺越变越大P1三波瞄准 / P2腹柱+翼尖花弹
if(ph===0){for(let k=0;k<3;k++)for(let i=-2;i<=2;i++){
const bb=ebul(b.x,b.y+8+k*4,a0+i*0.16+(k-1)*0.09,1.5+k*0.2,"flowr",14);
bb.grow=0.007;bb.maxS=2.8;bb.scale=0.9;}
b.mainT=Math.round(76/fm);
}else{bossBeam(b.x,b.y+10,b.x+clamp(ap2.x-b.x,-50,50)*0.5,H+10,7,"#ff8fc8",30,44,22,{b,ox:0,oy:10});
for(const sd of[-1,1])for(let i=0;i<3;i++){
const bb=ebul(b.x+sd*52,b.y+2,Math.PI/2+sd*(0.35+i*0.2),1.6,"flowr",13);
bb.grow=0.008;bb.maxS=3;bb.scale=0.9;}
b.mainT=Math.round(130/fm);}
break;
case "annihilate": { // 歼灭者:螺旋臂
const arms=2+ph;
for(let arm=0;arm<arms;arm++)for(let i=0;i<2;i++)ebul(b.x,b.y,b.t*0.06+arm*(TAU/arms)+i*0.14,2+0.2*ph,"orbP");
b.mainT=Math.round(10/fm);break;}
case "assault": // 暗黑突击:启动冲撞
b.dashSt={ph:"tele",t:52};b.mainT=9999;break;
case "piano": { // 激光钢琴21键四种模式轮换——顺扫(逼走位)/反扫/乱序/齐射
if(!b.keys)b.keys={n:21,seq:[],i:0,t:40,hot:-1,hotT:0,hotList:null,mode:3};
const ks=b.keys,n=ks.n;
if(ks.hotT>0){ks.hotT--;
if(ks.hotT===0){const list=ks.hotList||[ks.hot];
for(const kh of list){if(kh<0)continue;const kx=-(n-1)*4.75+kh*9.5;
bossBeam(b.x+kx,b.y+18,b.x+kx,H+8,4,"#43e8ff",12,20,18,{b,ox:kx,oy:18,followX2:true});}
ks.hotList=null;ks.hot=-1;SFX.eshoot();}}
ks.t--;
if(ks.t<=0&&ks.hotT<=0){
if(ks.i>=ks.seq.length){ // 布下一局:轮换模式
ks.mode=(ks.mode+1)%4;
if(ks.mode===0){ks.seq=[];for(let i=0;i<n;i++)ks.seq.push(i);}
else if(ks.mode===1){ks.seq=[];for(let i=n-1;i>=0;i--)ks.seq.push(i);}
else if(ks.mode===2){ks.seq=shuffled(n);}
else{ks.seq=[];for(let i=0;i<6+ph*2;i++)ks.seq.push(-1);} // -1=齐射标记
ks.i=0;ks.t=Math.round((ph?36:56)/fm);
}else{const cur=ks.seq[ks.i++];
if(cur===-1){const pool=shuffled(n);ks.hotList=pool.slice(0,4+ph*2);ks.hotT=16;ks.t=Math.round((ph?34:50)/fm);}
else{ks.hot=cur;ks.hotT=7;ks.t=Math.round((ph?3:5)/fm);} // 扫射模式:快速连发
}
}
b.mainT=6;break;}
case "hive": // 母巢核心:环弹+孵化
for(let i=0;i<10+ph*4;i++)ebul(b.x,b.y,i/(10+ph*4)*TAU+b.t*0.03,1.6+0.2*ph,"orbO");
if(b.t%240<3)for(let i=0;i<1+ph;i++)spawnEnemy(ph>=2?"kami":"mini",b.x+rnd(-30,30),b.y+8,false);
b.mainT=Math.round(Math.max(34,70-8*ph)/fm);break;
case "usurper": // 夺权者X银钉无人机P1四架/P2两架僚机P2周期性巨宽核心激光压制其他攻击
if(ph===0){
if(!b.fn0){b.fn0=1;spawnFunnels(b,4);}
for(const sd of[-1,1]){const px2=b.x+sd*55,py2=b.y-4,a6=Math.atan2(ap2.y-py2,ap2.x-px2);
ebul(px2,py2,a6,4.4,"dartS",15);}
b.mainT=Math.round(46/fm);
}else{
if(!b.plT||b.plT<=0){bossBeam(b.x,b.y+20,b.x,H+8,22,"#7ce9ff",46,80,30,{b,ox:0,oy:20,followX2:true});
b.laserT=130;b.plT=Math.round(420/fm);b.mainT=40;SFX.warn();}
else{for(let k=-1;k<=1;k++)ebul(b.x,b.y+30,a0+k*0.14,4.4,"dartS",16);
b.mainT=Math.round(88/fm);}
}
break;
case "saucer": { // 暗黑魔碟:高密度锥形弹环 + P2起双眼激光跟随 + P3犄角炮台
const n2=12+ph*5;
for(let i=0;i<n2;i++){const ea=(b.rotA||0)+i/n2*TAU;
ebul(b.x+Math.cos(ea)*30,b.y+Math.sin(ea)*30,ea,1.9+0.15*ph,"dartP",15);}
b.rotA=(b.rotA||0)+0.22;
b.mainT=Math.round((ph>=2?24:ph===1?30:36)/fm);
if(ph>=1&&(!b.eyeT||b.eyeT<=0)){const ex=ap2.x;
for(const sd of[-1,1])bossBeam(b.x+sd*9,b.y-3,ex+sd*6,H+8,4,"#ff3355",22,26,20,{b,ox:sd*9,oy:-3});
b.eyeT=Math.round(150/fm);}
if(ph>=2&&b.hornT<=0){const sd=(b.hornSide=(b.hornSide||1)*-1);
const hx=b.x+sd*41,hy=b.y-33,ha=Math.atan2(ap2.y-hy,ap2.x-hx);
for(let k=-1;k<=1;k++)ebul(hx,hy,ha+k*0.16,2.8,"orbR",15);
b.hornT=Math.round(46/fm);}
break;}
}
}
/* ---- Boss 小攻击库 ---- */
function bossMinor(b,mk,fm){
const ap2=aimPt(),a0=Math.atan2(ap2.y-b.y,ap2.x-b.x);
switch(mk){
case "snipe": for(let k=0;k<3;k++)ebul(b.x,b.y+10,a0+(k-1)*0.06,3.2,"orbR",14);b.minorT=Math.round(150/fm);break;
case "ring": for(let i=0;i<12;i++)ebul(b.x,b.y,i/12*TAU+b.t*0.02,1.6,"orbG",14);b.minorT=Math.round(190/fm);break;
case "wiggle": for(const sd of[-1,1])for(let i=0;i<4;i++){const bb=ebul(b.x+sd*10,b.y+6,Math.PI/2+sd*0.55-i*sd*0.14,1.5,"flowr",13);bb.grow=0.008;bb.maxS=2.6;bb.scale=0.85;}b.minorT=Math.round(150/fm);break;
case "flowerSnipe": for(let k=0;k<3;k++){const bb=ebul(b.x,b.y+10,a0+(k-1)*0.12,2.6,"flowr",15);bb.grow=0.006;bb.maxS=2.4;bb.scale=0.9;}b.minorT=Math.round(170/fm);break;
case "spawnMini": for(let i=0;i<2;i++)spawnEnemy(pick(["scout","mini"]),b.x+rnd(-26,26),b.y+10,false);b.minorT=Math.round(250/fm);break;
case "spawnMine": spawnEnemy("mine",b.x,b.y+10,false);b.minorT=Math.round(280/fm);break;
case "aimSpread": for(let i=-2;i<=2;i++)ebul(b.x,b.y+12,a0+i*0.19,2.5,"orbO",15);b.minorT=Math.round(140/fm);break;
case "rain": for(let i=0;i<5;i++)ebul(rnd(20,W-20),-6,Math.PI/2+rnd(-0.1,0.1),2.2,"orbO",14);b.minorT=Math.round(125/fm);break;
case "topRain": for(let i=0;i<4;i++)ebul(rnd(30,W-30),-6,Math.PI/2+rnd(-0.15,0.15),1.9,"orbG",13);b.minorT=Math.round(150/fm);break;
case "chain": for(let arm=0;arm<2;arm++)for(let i=0;i<4;i++)ebul(b.x,b.y+8,b.t*0.09+arm*Math.PI+i*0.11,1.7,"orbO",14);b.minorT=Math.round(44/fm);break;
case "pillar": {const px2=rnd(40,W-40);bossBeam(px2,-6,px2,H+8,4,"#b98cff",26,26,18);b.minorT=Math.round(220/fm);break;}
case "eyeBeam": bossBeam(b.x,b.y+6,ap2.x,H+8,4,"#ff3355",22,24,20);b.minorT=Math.round(180/fm);break;
case "ringBig": for(let i=0;i<18;i++)ebul(b.x,b.y,i/18*TAU-b.t*0.03,1.8,"orbP",15);b.minorT=Math.round(230/fm);break;
}
}
function hurtBoss(dmg){
const b=G.boss;if(!b||b.dead||b.transT>0||b.y<b.ty-1)return; // 入场未到位/变形转场期间不掉血
const def=BOSSES[b.idx],thr=def.phases===2?[0.5]:[0.66,0.33];
const floor=b.phase<thr.length?thr[b.phase]:0; // 当前血管的底
b.hp-=dmg;b.flash=2;
if(floor>0&&b.hp<b.maxhp*floor)b.hp=b.maxhp*floor; // 锁血:单管打空即停,等待转场
if(Math.random()<0.3)spawnPart(b.x+rnd(-18,18),b.y+rnd(-10,10),"#fff",2);
if(b.hp<=0){b.dead=true;killBoss();}
}
function killBoss(){
const b=G.boss;
SFX.boom(true);G.shake=16;G.flash=20;
for(let i=0;i<40;i++)spawnPart(b.x+rnd(-24,24),b.y+rnd(-14,14),pick(["#ffcf5c","#ff5566","#fff","#ff9a3d"]),3);
addScore(5000*(1+G.bossN*0.5),b.x,b.y);
addCoins(rndi(30,50));
for(let i=0;i<12;i++)dropPick(b.x+rnd(-30,30),b.y+rnd(-10,10),"gem");
dropPick(b.x-14,b.y,"heart");dropPick(b.x+14,b.y,"odc");
banner("击坠 "+b.name+"","#ffcf5c",120);
G.boss=null;MUS.start(false);
G.eb.length=0;G.bbeams.length=0;G.funnels.length=0;
// Boss 必掉稀有以上三选一(仅当局仍在进行时才弹出;挑战模式不打断)
if(G.mode!=="rush")setTimeout(()=>{if(G&&UI==="play"&&G.lives>0)offerCards(true);},900);
if(G.mode==="rush"){ // Boss挑战记录最快击破返回选择界面
const ct=Math.round((G.time-(G.rushT0||0))/60);
SAVE.rush=SAVE.rush||{};
const best=SAVE.rush[b.idx];
if(!best||ct<best){SAVE.rush[b.idx]=ct;persist();}
setTimeout(()=>{if(!G||G.lives<=0)return;buildRush();UI="rush";show("o-rush");},1500);
}
else if(G.mode==="stage"){
if(G.stage>=6){setTimeout(()=>{if(G&&UI!=="over")endRun(true);},1600);}
else setTimeout(()=>{if(!G||UI==="over"||G.lives<=0)return;G.stage++;G.wave=0;SAVE.best.stage=Math.max(SAVE.best.stage,G.stage-1);persist();banner("STAGE "+G.stage+" — "+STAGE_NAMES[G.stage-1],"#41e6ff",140);startWave();},2400);
}
}
const STAGE_NAMES=["碎石边境","血色星云","钢铁舰队","死寂空间站","虫群巢穴","核心决战"];

509
js/render.js Normal file
View File

@ -0,0 +1,509 @@
"use strict";
/* ================= 背景:星空与星云 ================= */
const STARS=[[],[],[]];
for(let i=0;i<3;i++)for(let j=0;j<26-i*6;j++)STARS[i].push({x:rnd(W),y:rnd(H),s:(0.35+i*0.4),sz:i===2?2:1,col:i===2?pick(["#aab8d8","#8fa8d8","#c0a8c8"]):"#3f4c74"});
const NEB=["#131c3f","#33102a","#0f2a1c","#101a33","#2a1030","#330f0f"];
let nebY=0,planet=null;
function drawBG(dt){
ctx.fillStyle="#05070f";ctx.fillRect(0,0,W,H);
const stage=G?G.stage-1:0,ncol=NEB[clamp(stage,0,5)];
nebY=(nebY+0.08*dt)%160;
for(let k=0;k<2;k++){
const g=ctx.createRadialGradient(W*(k?0.75:0.25),nebY+k*160-160,10,W*(k?0.75:0.25),nebY+k*160-160,130);
g.addColorStop(0,ncol);g.addColorStop(1,"transparent");
ctx.globalAlpha=0.38;ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.globalAlpha=1;
}
for(let i=0;i<3;i++)for(const s of STARS[i]){
s.y+=s.s*dt*(G&&G.odActive>0?2:1);if(s.y>H){s.y=-2;s.x=rnd(W);}
ctx.fillStyle=s.col;ctx.fillRect(s.x,s.y,s.sz,s.sz);
}
if(planet){
planet.y+=0.12*dt;
if(planet.y>H+40){planet=null;}
else{
ctx.globalAlpha=0.5;ctx.fillStyle=planet.col;
ctx.beginPath();ctx.arc(planet.x,planet.y,planet.r,0,TAU);ctx.fill();
ctx.globalAlpha=0.25;ctx.fillStyle="#fff";
ctx.fillRect(planet.x-planet.r,planet.y-2,planet.r*2,1);
ctx.fillRect(planet.x-planet.r+3,planet.y+4,planet.r*2-6,1);
ctx.globalAlpha=1;
}}
else if(Math.random()<0.001)planet={x:rnd(40,W-40),y:-40,r:rndi(14,26),col:pick(["#4a5a8c","#8c4a5a","#4a8c6e"])};
}
/* ================= Boss 程序化像素绘制 ================= */
function px(x,y,w,h,col){ctx.fillStyle=col;ctx.fillRect(Math.round(x),Math.round(y),w,h);}
function poly(pts,col){ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(pts[0],pts[1]);for(let i=2;i<pts.length;i+=2)ctx.lineTo(pts[i],pts[i+1]);ctx.closePath();ctx.fill();}
function circ(x,y,r,col){ctx.fillStyle=col;ctx.beginPath();ctx.arc(x,y,r,0,TAU);ctx.fill();}
function drawBoss(){
const b=G.boss;if(!b)return;
// 变形动画五个新Boss播专属变身mp=进度);其余通用收缩展开 + 白光衰减
let ph2=b.phase,sc=1,ox=0,oy=0,flashA=0,mp=null;
if(b.transT>0){const prog=1-b.transT/70;
ox=rnd(-1.6,1.6);oy=rnd(-1.2,1.2);
if(b.idx===1||b.idx===3||b.idx===4||b.idx===6||b.idx===7){mp=prog;if(prog>0.75)flashA=(1-prog)*1.6;}
else{sc=prog<0.5?1-0.16*(prog*2):0.84+0.16*((prog-0.5)*2);
if(prog<0.5)ph2=b.phase-1;
flashA=prog<0.5?prog*0.8:(1-prog)*0.8;}}
ctx.save();ctx.translate(Math.round(b.x+ox),Math.round(b.y+oy));
if(sc!==1)ctx.scale(sc,sc);
const fl=b.flash>0?"#ffffff":null,t=b.t;
if(b.idx===0){ // 巡逻巡洋舰
px(-22,-10,44,20,"#3a4460");px(-18,-14,36,6,"#5b6b8c");px(-14,10,28,5,"#2a3450");
px(-24,-6,6,12,"#5b6b8c");px(18,-6,6,12,"#5b6b8c");
px(-25,4,4,6,fl||"#e0393e");px(21,4,4,6,fl||"#e0393e");
px(-8,-18,16,8,"#5b6b8c");px(-4,-20,8,4,"#3a4460");
px(-6,-4,12,10,fl||"#ff9a3d");px(-3,-2,6,6,"#fff4d9");
for(let i=-2;i<=2;i++)px(i*8-1,12,2,3,t%40<20?"#ff5566":"#7a1622");
px(-22,-10,44,2,"#8c9ab8");
if(ph2>=1){px(-2,-27,4,8,"#5b6b8c");px(-1,-29,2,3,t%20<10?"#ff5566":"#7a1622");} // 二阶段顶部炮塔
}else if(b.idx===1){ // 太空原种:虫蛹↔蝠鲼(变身:裂皮→变粉→红眼→展翅)
if(mp!==null){if(mp<0.5)drawPupa(t,fl,mp*2);else drawManta(t,fl,(mp-0.5)*2);}
else if(ph2===0)drawPupa(t,fl,0);else drawManta(t,fl,1);
}else if(b.idx===2){ // 歼灭者
for(let i=0;i<6;i++){const a=i/6*TAU+t*0.01;
px(Math.cos(a)*19-3,Math.sin(a)*19-3,6,6,"#5b3a8c");}
px(-16,-12,32,24,"#2a1f4a");px(-12,-16,24,6,"#4a3a7a");px(-12,10,24,6,"#4a3a7a");
px(-20,-4,6,8,"#4a3a7a");px(14,-4,6,8,"#4a3a7a");
px(-6,-6,12,12,fl||"#b05ce8");px(-3,-3,6,6,"#f0d6ff");
for(let i=0;i<3+ph2;i++){const a=t*0.02+i*TAU/(3+ph2);
px(Math.cos(a)*26-2,Math.sin(a)*26-2,4,4,t%30<15?"#b05ce8":"#5c2a80");}
}else if(b.idx===3){ // 暗黑突击小体型高机动翼展角15°/45°/70°随阶段展开
const SW=[15,45,70];
let swp=SW[Math.min(ph2,2)];
if(mp!==null){const s0=SW[Math.max(0,b.phase-1)],s1=SW[Math.min(b.phase,2)];swp=s0+(s1-s0)*mp;}
ctx.scale(0.34,0.34);
drawAssault(t,fl,swp,Math.min(ph2,2),mp);
}else if(b.idx===4){ // 激光钢琴:全屏宽半碟要塞(二阶段裂壳露红核)
drawPiano(b,t,fl,mp!==null?mp:(ph2>=1?1:0));
}else if(b.idx===5){ // 母巢核心
for(let i=0;i<8;i++){const a=i/8*TAU+t*0.005;
px(Math.cos(a)*24-2,Math.sin(a)*24-2,5,5,"#8c5b2a");}
px(-18,-14,36,28,"#4a3410");px(-14,-18,28,8,"#6e4f1a");px(-14,10,28,8,"#6e4f1a");
px(-22,-8,5,16,"#6e4f1a");px(17,-8,5,16,"#6e4f1a");
const pl=8+Math.sin(t*0.1)*2;
px(-pl/2-3,-pl/2-3,pl+6,pl+6,fl||"#ff9a3d");px(-pl/2,-pl/2,pl,pl,"#fff4d9");
px(-2,-22,4,6,"#ffcf5c");px(-2,16,4,6,"#ffcf5c");
}else if(b.idx===6){ // 夺权者X巨型母舰二阶段抛侧舱→拉长钻石机身+僚机)
if(ph2===0||mp!==null){ // P1巨舰主体变身时收缩淡出
const hullA=mp!==null?1-mp:1,hullS=mp!==null?1-mp*0.25:1;
if(hullA>0.02){ctx.save();ctx.scale(hullS,hullS);ctx.globalAlpha=hullA;
poly([0,46,-14,8,-22,-30,-9,-42,9,-42,22,-30,14,8],"#d8e4f0");
poly([0,46,14,8,22,-30,9,-42,0,-42],"#9fb0c6");
poly([-9,-42,-34,-52,-26,-28,-9,-34],"#aab8cc");poly([9,-42,34,-52,26,-28,9,-34],"#aab8cc");
circ(0,-16,8,fl||"#43b8ff");circ(0,-16,4,"#bfefff");
px(-2,-40,4,18,"#43b8ff");px(-2,-4,4,30,"#43b8ff");
ctx.restore();ctx.globalAlpha=1;}}
if(ph2===0||mp!==null){ // 侧舱(变身时抛离飞走)
const jt=mp||0;
if(jt<0.99)for(const sd of[-1,1]){ctx.save();
ctx.translate(sd*(55+jt*90),jt*60);ctx.rotate(sd*jt*0.7);ctx.globalAlpha=1-jt*0.8;
px(-11,-22,22,44,"#aab8cc");px(-11,-22,22,4,"#43b8ff");
px(-8,18,16,6,"#8a9ab8");
for(let i=0;i<3;i++)px(-3,-10+i*8,6,4,fl||"#ff3355");
ctx.restore();ctx.globalAlpha=1;}}
if(ph2>=1){ // 拉长钻石机身+机翼(二阶段显现)
const di=mp!==null?mp:1;
ctx.save();ctx.globalAlpha=di;const ds=0.7+0.3*di;ctx.scale(ds,ds);
poly([0,58,-13,8,-8,-38,0,-48,8,-38,13,8],"#e8f4ff");
poly([0,58,13,8,8,-38,0,-48],"#9fb0c6");
poly([-8,-36,-36,-48,-28,-26,-8,-30],"#c0cddd");poly([8,-36,36,-48,28,-26,8,-30],"#c0cddd");
circ(0,-14,8,fl||"#43b8ff");circ(0,-14,4,"#bfefff");
px(-2,-36,4,18,"#43b8ff");px(-2,0,4,34,"#43b8ff");
ctx.restore();ctx.globalAlpha=1;}
}else{ // 暗黑魔碟:核心+圆环;二阶段圆环前段裂成犄角+赤瞳激光;三阶段爆甲出刺+炮台
circ(0,0,20,"#1a1030");circ(0,0,15,"#12081c");
if(ph2===0&&mp===null){ // 一阶段:完整圆环绕核心缓转
ctx.strokeStyle="#55336e";ctx.lineWidth=13;
ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.stroke();
ctx.strokeStyle="#6e4490";ctx.lineWidth=5;
ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.stroke();
for(let i=0;i<4;i++){const a=i*Math.PI/2+t*0.012;
px(Math.cos(a)*30-2,Math.sin(a)*30-2,4,4,"#c86bff");}
px(-6,-4,12,8,"#2a1a3a");
}else{
if(mp!==null&&b.phase===1){ // 变形中:圆环残留快速淡出
const ra=1-clamp(mp/0.35,0,1);
if(ra>0){ctx.globalAlpha=ra;ctx.strokeStyle="#55336e";ctx.lineWidth=13;
ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.stroke();ctx.globalAlpha=1;}}
const ha=mp!==null?clamp((mp-0.25)/0.6,0,1):1; // 犄角展开度
if(ha>0){ctx.globalAlpha=ha;
for(const sd of[-1,1]){ctx.save();ctx.rotate((1-ha)*sd*1.1);
poly([sd*10,4,sd*30,-26,sd*46,-32,sd*40,-6,sd*20,12],"#55336e");
poly([sd*16,2,sd*32,-20,sd*40,-24,sd*34,-4,sd*20,8],"#6e4490");
if(ph2>=2){ // 三阶段爆甲:犄角外壳小刺+红色炮台
for(let k=0;k<3;k++)poly([sd*(25+k*7),-13-k*6,sd*(29+k*7),-22-k*6,sd*(33+k*7),-12-k*6],"#2a1a3a");
px(sd*41-3,-36,7,6,fl||"#ff3355");}
ctx.restore();}
ctx.globalAlpha=1;}
const eyeA=mp!==null?clamp((mp-0.45)/0.5,0,1):1; // 核心赤瞳
if(eyeA>0){ctx.globalAlpha=eyeA;
px(-11,-5,7,6,fl||"#ff3355");px(4,-5,7,6,fl||"#ff3355");
px(-9,-3,3,2,"#ffdfe5");px(6,-3,3,2,"#ffdfe5");
ctx.globalAlpha=1;}
}
}
if(flashA>0){ctx.globalAlpha=flashA;ctx.strokeStyle="#fff";
ctx.beginPath();ctx.arc(0,0,b.r+6,0,TAU);ctx.stroke();ctx.globalAlpha=1;}
if(b.flash>0)b.flash--;
ctx.restore();
// 冲撞预警线(暗黑突击)
if(b.dashSt&&b.dashSt.ph==="tele"&&G.time%4<2){
ctx.strokeStyle="#ff3355";ctx.globalAlpha=0.6;
if(ctx.setLineDash)ctx.setLineDash([4,3]);
ctx.beginPath();ctx.moveTo(b.x,b.y);ctx.lineTo(G.p.x,G.p.y);ctx.stroke();
if(ctx.setLineDash)ctx.setLineDash([]);ctx.globalAlpha=1;}
// 多管血条:一管蓝/二管红/三管橙,当前管铺在下一管之上,见底锁血转场
const w=160,def2=BOSSES[b.idx],thr=def2.phases===2?[0.5]:[0.66,0.33];
const TUBEC=["#43b8ff","#e0393e","#ff9a3d"];
const ratio=clamp(b.hp/b.maxhp,0,1),bx=(W-w)/2;
px(bx-2,8,w+4,8,"#0d1226");
px(bx,10,w,4,b.phase<thr.length?(TUBEC[b.phase+1]||"#3a1020"):"#3a1020"); // 底:下一管或暗底
const hi=b.phase===0?1:thr[b.phase-1],lo=b.phase<thr.length?thr[b.phase]:0;
const pc=clamp((ratio-lo)/(hi-lo),0,1);
px(bx,10,w*pc,4,TUBEC[b.phase]||b.col); // 当前管
for(const tt of thr)px(bx+Math.round(w*tt)-1,9,2,6,"#0d1226"); // 管界刻痕
if(b.transT>0&&G.time%8<4){ctx.strokeStyle="#fff";ctx.lineWidth=1;ctx.strokeRect(bx-2.5,7.5,w+5,9);}
ctx.font="6px 'Press Start 2P',monospace";ctx.fillStyle="#fff";ctx.textAlign="center";
ctx.fillText(b.transT>0?b.name:(b.name+" "+Math.ceil(pc*100)+"%"),W/2,22);
}
/* ---- Boss 分体绘制助手 ---- */
function drawPupa(t,fl,cr){ // 太空原种·虫蛹cr=裂变粉化进度0~1
ctx.rotate(Math.sin(t*0.05)*0.05);
const body=[0,-40,11,-37,15,-28,13,-14,11,0,8,14,5,26,2,34,0,36,-2,34,-5,26,-8,14,-11,0,-13,-14,-15,-28,-11,-37];
poly(body,"#c9d6e2");
poly([0,-40,11,-37,15,-28,13,-14,11,0,8,14,5,26,2,34,0,36],"#9fb0c2");
if(cr>0){ctx.globalAlpha=cr*0.85;poly(body,"#f0a0c0");ctx.globalAlpha=1;} // 身体变粉
for(let i=0;i<5;i++){const wy=-24+i*11,ww=13-i*1.8;px(-ww,wy,ww*2,2,"#8498ac");}
if(cr>0.25){px(-10,-18,3,14,"#8a2a3a");px(7,-8,3,12,"#8a2a3a");} // 表皮裂开
if(cr>0.55){px(-4,-30,3,10,"#8a2a3a");px(-13,-2,3,9,"#8a2a3a");}
circ(0,-34,10,"#dde7f0");px(-9,-37,18,3,"#9fb0c2");
px(-6,-35,4,4,fl||"#43e8ff");px(2,-35,4,4,fl||"#43e8ff"); // 青眼
if(cr>0.3){ctx.globalAlpha=Math.min(1,(cr-0.3)/0.5); // 眼睛变红
px(-6,-35,4,4,"#ff3355");px(2,-35,4,4,"#ff3355");ctx.globalAlpha=1;}
px(-12,-44,3,7,"#9fb0c2");px(9,-44,3,7,"#9fb0c2");
px(-19,-16,5,12,"#7fd8ff");px(14,-16,5,12,"#7fd8ff");
}
function drawManta(t,fl,un){ // 太空原种·蝠鲼un=展翅进度0~1
const ws=0.15+0.85*un;
ctx.save();ctx.scale(ws,1+Math.sin(t*0.07)*0.04*un); // 翅膀自背后展开
poly([0,-20,-26,-14,-52,-4,-76,6,-58,12,-34,10,-16,16,0,20],"#c86aa8");
poly([0,-20,26,-14,52,-4,76,6,58,12,34,10,16,16,0,20],"#c86aa8");
poly([0,-18,-24,-12,-50,-3,-72,7,-56,9,-30,2,0,-8],"#e89cc4");
poly([0,-18,24,-12,50,-3,72,7,56,9,30,2,0,-8],"#e89cc4");
if(un>0.6)for(const sd of[-1,1])for(let i=0;i<3;i++)px(sd*(44+i*10)-1,6+i*2,2,2,"#7dff8a");
ctx.restore();
ctx.globalAlpha=Math.min(1,un*1.6);
poly([0,28,-7,8,-8,-8,-5,-18,5,-18,8,-8,7,8],"#8a3a70");
circ(0,-18,9,"#e8d5e0");
px(-5,-19,3,4,"#3a1030");px(2,-19,3,4,"#3a1030");
px(-5,-18,3,3,fl||"#ff3355");px(2,-18,3,3,fl||"#ff3355"); // 红瞳
px(-13,-26,3,8,"#7dff8a");px(10,-26,3,8,"#7dff8a");
for(let i=0;i<4;i++)px(-9+i*5,0,3,5,fl||"#ff3355");
ctx.globalAlpha=1;
}
function drawAssault(t,fl,swp,ph,mp){ // 暗黑突击swp=翼展角;炮管/推进器随阶段增多)
const sw=swp*Math.PI/180;
for(const sd of[-1,1]){
const tx=sd*(8+52*Math.sin(sw)),ty=14-52*Math.cos(sw);
poly([sd*4,6,tx,ty,tx-sd*9,ty+11,sd*4,20],ph>=2?"#c01a2e":"#a01224");
poly([tx-sd*2,ty-6,tx+sd*3,ty-14,tx+sd*7,ty-3],"#6e0c1a");}
poly([0,38,-5,6,-3,-26,3,-26,5,6],"#d4233a");
px(-2,8,4,8,fl||"#ffd23d");
const guns=[[16,4],[-16,4],[30,-4],[-30,-4],[42,-10],[-42,-10]];
const gn=ph>=2?6:ph>=1?4:2,pgn=ph===2?4:ph===1?2:0;
for(let i=0;i<gn;i++){let gl=8;if(mp!==null&&i>=pgn)gl=Math.max(1,8*mp); // 新炮管生长
px(guns[i][0]-1.5,guns[i][1],3,gl,"#3a0a10");px(guns[i][0]-1,guns[i][1]+gl-2,2,2,"#ff9a3d");}
const eng=[[7,-26],[-7,-26],[0,-30],[14,-22]];
const en=ph>=2?4:ph>=1?3:2,pen=ph===2?3:ph===1?2:0;
for(let i=0;i<en;i++){let el=5+(t%4<2?2:0);if(mp!==null&&i>=pen)el=Math.max(1,el*mp); // 新推进器
px(eng[i][0]-2,eng[i][1],4,3,"#5a0a14");
px(eng[i][0]-1.5,eng[i][1]-3-el,3,el,t%6<3?"#ff9a3d":"#ffcf5c");}
if(ph>=1){poly([-52,-14,-24,-3,-22,-1,-50,-11],"#ffdfe5");poly([52,-14,24,-3,22,-1,50,-11],"#ffdfe5");}
if(ph>=2){px(-4,-16,8,12,fl||"#ff3355");px(-2,-14,4,8,"#ffdfe5");}
}
function drawPiano(b,t,fl,split){ // 激光钢琴split=裂壳进度0~1
const ks=b.keys,n=ks?ks.n:21;
px(-(n-1)*4.75-5,6,(n-1)*9.5+10,19,"#141821"); // 键槽
for(let i=0;i<n;i++){const kx=-(n-1)*4.75+i*9.5;
const hot=ks&&(ks.hot===i||(ks.hotList&&ks.hotList.indexOf(i)>=0));
px(kx-4,8,8,14,hot?(t%6<3?"#e8fbff":"#43e8ff"):"#d8e4f0");
px(kx-4,20,8,2,hot?"#43e8ff":"#8a9ab8");}
const off=split*30; // 两半穹顶向左右裂开
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*off,0);
const q0=sd<0?Math.PI:Math.PI*1.5,q1=sd<0?Math.PI*1.5:Math.PI*2;
ctx.fillStyle="#3a4460";ctx.beginPath();ctx.ellipse(0,6,118,34,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill();
ctx.fillStyle="#55648a";ctx.beginPath();ctx.ellipse(0,6,92,26,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill();
ctx.fillStyle="#6e7f9e";ctx.beginPath();ctx.ellipse(0,6,60,17,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill();
ctx.restore();}
if(split>0){ctx.globalAlpha=split; // 红色核心
const cr=10+Math.sin(t*0.12)*2;
circ(0,-4,cr+4,"#7a0e1e");circ(0,-4,cr,fl||"#ff3355");circ(0,-4,cr*0.45,"#ffdfe5");
ctx.globalAlpha=1;}
if(split<1){ctx.globalAlpha=1-split; // 一阶段青色核心窗
px(-7,-14,14,9,fl||"#43e8ff");px(-4,-12,8,5,"#bfefff");
ctx.globalAlpha=1;}
px(-10-off,-32,20,10,"#3a4460");px(-6-off,-37,12,5,"#55648a"); // 舰桥随左半壳
if(split<0.4)for(let i=0;i<7;i++){const a=Math.PI+(i+0.5)/7*Math.PI;
px(Math.cos(a)*110-1.5,6+Math.sin(a)*30-1.5,3,3,"#9fb4d8");}
}
/* ---- Boss光束与浮游炮渲染 ---- */
function drawBossFx(){
for(const f of G.funnels){ // 银钉无人机
poly([f.x,f.y-5,f.x+4,f.y+3,f.x-4,f.y+3],"#cfe4ff");
px(f.x-1,f.y-1,2,3,"#43b8ff");
if(f.flashT>0){ctx.globalAlpha=0.6;px(f.x-3,f.y+3,6,3,"#ffffff");ctx.globalAlpha=1;}
}
for(const bm of G.bbeams){
if(bm.tele>0){if(G.time%4<2){ctx.globalAlpha=0.5;ctx.strokeStyle=bm.col;ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(bm.x1,bm.y1);ctx.lineTo(bm.x2,bm.y2);ctx.stroke();ctx.globalAlpha=1;}}
else{ctx.globalCompositeOperation="lighter";
ctx.globalAlpha=clamp(bm.life/10,0,1)*0.85;
ctx.strokeStyle=bm.col;ctx.lineWidth=bm.w;
ctx.beginPath();ctx.moveTo(bm.x1,bm.y1);ctx.lineTo(bm.x2,bm.y2);ctx.stroke();
ctx.strokeStyle="#fff";ctx.lineWidth=Math.max(1,bm.w/3);
ctx.beginPath();ctx.moveTo(bm.x1,bm.y1);ctx.lineTo(bm.x2,bm.y2);ctx.stroke();
ctx.globalAlpha=1;ctx.globalCompositeOperation="source-over";}
}
}
/* ================= 世界渲染 ================= */
function drawSprite(spr,x,y){ctx.drawImage(spr,Math.round(x-spr.width/2),Math.round(y-spr.height/2));}
function drawSeg(x1,y1,x2,y2,w,col,alpha){
const d=Math.hypot(x2-x1,y2-y1)||1,steps=Math.max(1,Math.ceil(d/2));
ctx.globalAlpha=alpha;ctx.fillStyle=col;
for(let i=0;i<=steps;i++){const t2=i/steps;
ctx.fillRect(Math.round(lerp(x1,x2,t2)-w/2),Math.round(lerp(y1,y2,t2)-w/2),w,w);}
ctx.globalAlpha=1;
}
function render(){
ctx.save();
if(G&&G.shake>0){ctx.translate(rnd(-G.shake,G.shake)*0.5,rnd(-G.shake,G.shake)*0.5);}
drawBG(1);
if(G){
// 拾取物
for(const k of G.picks){const bob=Math.sin(k.t*0.1)*1.5;drawSprite(SPR[k.spr],k.x,k.y+bob);}
// 敌机
for(const e of G.enemies){
if(e.dead)continue;
ctx.save();
if(e.beh==="spin"){ctx.translate(e.x,e.y);ctx.rotate(e.rot);ctx.drawImage(e.spr,-e.dw/2,-e.dh/2,e.dw,e.dh);}
else ctx.drawImage(e.spr,e.x-e.dw/2,e.y-e.dh/2,e.dw,e.dh); // 亚像素绘制,移动丝滑
ctx.restore();
// 冻结标记:四角冰晶+雪花粒子(不再用矩形染色)
if(e.slow>0){const cx2=Math.round(e.x-e.dw/2),cy2=Math.round(e.y-e.dh/2);
px(cx2,cy2,2,2,"#7fd8ff");px(cx2+e.dw-2,cy2,2,2,"#7fd8ff");
px(cx2,cy2+e.dh-2,2,2,"#7fd8ff");px(cx2+e.dw-2,cy2+e.dh-2,2,2,"#7fd8ff");
if(G.time%8===0)spawnPart(e.x+rnd(-4,4),e.y-e.dh/2,"#bfeaff",1);}
// 中毒标记:绿色气泡
if(e.dot&&G.time%10===0)spawnPart(e.x+rnd(-3,3),e.y,"#9dff5c",1);
// 彻底冻结:冰壳轮廓+冰凌(描边而非实心,避免误认血条)
if(e.frozen>0){ctx.strokeStyle="#bfeaff";ctx.globalAlpha=0.9;
ctx.strokeRect(Math.round(e.x-e.dw/2)-1.5,Math.round(e.y-e.dh/2)-1.5,e.dw+3,e.dh+3);ctx.globalAlpha=1;
px(e.x-e.dw/2-2,e.y-1,2,2,"#fff");px(e.x+e.dw/2,e.y-3,2,2,"#fff");px(e.x-1,e.y-e.dh/2-3,2,2,"#fff");}
// 寄生电浆标记:紫色水蛙脉动
if(e.leech&&G.time%8<4)px(e.x-2,e.y-2,4,4,"#b05ce8");
if(e.flash>0){ctx.globalAlpha=0.7;ctx.globalCompositeOperation="lighter";ctx.drawImage(e.spr,Math.round(e.x-e.dw/2),Math.round(e.y-e.dh/2),e.dw,e.dh);ctx.globalAlpha=1;ctx.globalCompositeOperation="source-over";}
// 精英标记:头顶金色小皇冠
if(e.elite){const bob=Math.sin(G.time*0.1)*1.5;
px(e.x-4,e.y-e.dh/2-5+bob,2,3,"#ffd23d");px(e.x-1,e.y-e.dh/2-7+bob,2,5,"#ffd23d");px(e.x+2,e.y-e.dh/2-5+bob,2,3,"#ffd23d");}
// 受击迷你血条(真实扣减可见;图鉴演示中常驻显示)
if((e.hpt>0&&e.hp<e.maxhp)||G.demoSim){const w2=e.dw,fr=clamp(e.hp/e.maxhp,0,1);
px(e.x-w2/2,e.y-e.dh/2-3,w2,2,"#3a1020");
px(e.x-w2/2,e.y-e.dh/2-3,w2*fr,2,fr>0.4?"#5cff9d":"#ff5566");}
}
drawBoss();drawBossFx();
// 敌弹(圆形光晕烘焙在精灵内)
for(const b of G.eb){
if(b.scale&&b.scale!==1){const sp=SPR[b.spr]; // 成长弹(花弹)
ctx.save();ctx.translate(Math.round(b.x),Math.round(b.y));ctx.scale(b.scale,b.scale);
ctx.drawImage(sp,-Math.round(sp.width/2),-Math.round(sp.height/2));ctx.restore();}
else drawSprite(SPR[b.spr],b.x,b.y);}
// 玩家子弹
for(const b of G.pb){
if(b.spr){ctx.save();ctx.translate(b.x,b.y);
if(b.spin)ctx.rotate(G.time*0.35);
else if(b.homing||b.rot)ctx.rotate(Math.atan2(b.vy,b.vx)+Math.PI/2);
else if(b.t==="b")ctx.rotate(G.time*0.3);
const bsc=b.scale||1,bspr=SPR[b.spr];
ctx.drawImage(bspr,-bspr.width*bsc/2,-bspr.height*bsc/2,bspr.width*bsc,bspr.height*bsc);ctx.restore();}
else if(b.t==="f"){px(b.x-1.5,b.y-1.5,3,3,pick(["#ff9a3d","#ff5566","#ffd23d"]));}
else if(b.t==="wv"){ // 波动炮:剑气弧光
const rr=b.r;
ctx.globalCompositeOperation="lighter";
ctx.globalAlpha=0.75;ctx.strokeStyle="#43b8ff";ctx.lineWidth=4;
ctx.beginPath();ctx.arc(b.x,b.y+rr*0.55,rr,Math.PI+0.55,TAU-0.55);ctx.stroke();
ctx.globalAlpha=0.9;ctx.strokeStyle="#bfe8ff";ctx.lineWidth=2;
ctx.beginPath();ctx.arc(b.x,b.y+rr*0.55,rr*0.78,Math.PI+0.5,TAU-0.5);ctx.stroke();
ctx.globalAlpha=1;ctx.strokeStyle="#ffffff";ctx.lineWidth=1;
ctx.beginPath();ctx.arc(b.x,b.y+rr*0.55,rr*0.58,Math.PI+0.45,TAU-0.45);ctx.stroke();
ctx.globalCompositeOperation="source-over";}
else if(b.t==="rg"){ctx.globalAlpha=0.3;px(b.x-3.5,b.y-11,7,22,"#b98cff");ctx.globalAlpha=1;
px(b.x-2,b.y-8,4,14,"#b98cff");px(b.x-1,b.y-8,2,14,"#fff");}
else{px(b.x-1,b.y-3,2,5,"#7fd8ff");px(b.x-1,b.y-3,1,3,"#fff");}
}
// 激光束 / 电弧(线段式)
for(const l of G.lasers){
drawSeg(l.x1,l.y1,l.x2,l.y2,l.w,l.col,0.4);
drawSeg(l.x1,l.y1,l.x2,l.y2,Math.max(1,Math.round(l.w*0.4)),"#ffffff",0.9);
l.life--;
}
G.lasers=G.lasers.filter(l=>l.life>0);
for(const z of G.zaps){
for(let i=0;i<z.pts.length-1;i++){
drawSeg(z.pts[i][0],z.pts[i][1],z.pts[i+1][0],z.pts[i+1][1],1,z.col,0.9);
drawSeg(z.pts[i][0],z.pts[i][1],z.pts[i+1][0],z.pts[i+1][1],1,"#ffffff",0.5);}
}
// 区域效果 / 新星环 / 水雷(按类型差异化)
for(const z of G.zones){
if(z.kind==="plasma"){ // 电浆区:去除圆形区域,只保留内部窜动的电弧
if(G.time%4===0)zapVisual(z.x+rnd(-z.r*0.7,z.r*0.7),z.y+rnd(-z.r*0.7,z.r*0.7),z.x+rnd(-z.r*0.7,z.r*0.7),z.y+rnd(-z.r*0.7,z.r*0.7),pick(["#43e8ff","#bfeaff"]));
if(G.time%9===0)G.parts.push({x:z.x+rnd(-z.r/2,z.r/2),y:z.y+rnd(-z.r/2,z.r/2),vx:0,vy:-0.2,life:10,col:"#43e8ff",sz:1});
continue;
}
ctx.globalAlpha=0.16;ctx.fillStyle=z.col;
ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.fill();
ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2;ctx.strokeStyle=z.col;
ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.stroke();ctx.globalAlpha=1;
if(z.slow){ // 时间力场:逆时针旋转刻度
ctx.strokeStyle="#cfeaff";ctx.globalAlpha=0.6;
for(let i=0;i<6;i++){const a=-G.time*0.02+i*TAU/6;
ctx.beginPath();ctx.moveTo(z.x+Math.cos(a)*(z.r-6),z.y+Math.sin(a)*(z.r-6));
ctx.lineTo(z.x+Math.cos(a)*z.r,z.y+Math.sin(a)*z.r);ctx.stroke();}
ctx.globalAlpha=1;
}else if(z.pull){ // 引力:内旋纹
for(let i=0;i<3;i++){const a=G.time*0.08+i*TAU/3;
px(z.x+Math.cos(a)*z.r*0.6-1,z.y+Math.sin(a)*z.r*0.6-1,2,2,"#b05ce8");}}
}
// 热诱弹
for(const f of G.flares)drawSprite(SPR.flare,f.x,f.y);
for(const n of G.novas){
ctx.globalAlpha=clamp(n.life/22,0,1)*0.8;ctx.strokeStyle=n.col;
ctx.beginPath();ctx.arc(n.x,n.y,Math.max(1,n.r),0,TAU);ctx.stroke();ctx.globalAlpha=1;
}
// 圣光十字:逐渐变长 + 原地旋转90°
for(const hc of G.holies){
const hrot=(hc.t/36)*Math.PI/2,hL=hc.len*Math.min(1,hc.t/12),hA=clamp(hc.life/18,0,1);
for(let k=0;k<4;k++){const ang=hrot+k*Math.PI/2,hdx=Math.cos(ang),hdy=Math.sin(ang);
drawSeg(hc.x,hc.y,hc.x+hdx*hL,hc.y+hdy*hL,4,"#ffcf5c",0.4*hA);
drawSeg(hc.x,hc.y,hc.x+hdx*hL,hc.y+hdy*hL,2,"#fff4d9",0.9*hA);}
}
// 僚机 / 光刃 / 卫星 / 浮游单位
if(G.p.dead<=0){
for(const key of G.droneLoadout){const S=G.dst[key];
if(S&&S.x!==undefined){const dspr=SPR[DRONES[key].spr],dsc=DRONES[key].big||1;
if(dsc!==1)ctx.drawImage(dspr,Math.round(S.x-dspr.width*dsc/2),Math.round(S.y-dspr.height*dsc/2),Math.round(dspr.width*dsc),Math.round(dspr.height*dsc));
else drawSprite(dspr,S.x,S.y);}}
if(G.subLoadout.includes("orbit")){const lv=G.subs.orbit;
for(let i=0;i<lv;i++){const a=G.p.orbA+i*(TAU/lv);drawSprite(SPR.blade,G.p.x+Math.cos(a)*26,G.p.y+Math.sin(a)*26);}}
if(G.subLoadout.includes("sats")){const n=1+G.subs.sats;
for(let i=0;i<n;i++){const a=G.p.satA+i*(TAU/n);drawSprite(SPR.sat,G.p.x+Math.cos(a)*20,G.p.y+Math.sin(a)*20);}}
const skS=G.sst.seeker;if(skS&&skS.x!==undefined&&G.subLoadout.includes("seeker"))drawSprite(SPR.dGun2,skS.x,skS.y);
const bdS=G.sst.beamdrone;if(bdS&&bdS.x!==undefined&&G.subLoadout.includes("beamdrone"))drawSprite(SPR.dBeam,bdS.x,bdS.y);
if(G.subLoadout.includes("thorns"))for(let i=0;i<4;i++){const a=G.time*0.04+i*TAU/4;
px(G.p.x+Math.cos(a)*13-1,G.p.y+Math.sin(a)*13-1,3,3,"#c8d8ff");}
}
// 玩家
const p=G.p;
if(p.dead<=0&&!(p.inv>0&&G.time%6<3)){
drawSprite(SPR[SHIPS[G.ship].spr],p.x,p.y);
// 尾焰
const fl=G.time%8<4?3:5;
px(p.x-1,p.y+7,2,fl,"#ff9a3d");px(p.x-0.5,p.y+7,1,fl-1,"#fff4d9");
if(G.odActive>0){ctx.globalAlpha=0.4;px(p.x-8,p.y-4,1,12,"#41e6ff");px(p.x+7,p.y-4,1,12,"#41e6ff");ctx.globalAlpha=1;}
if(G.shield>0){ctx.strokeStyle="#43e8ff";ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2;
ctx.beginPath();ctx.arc(p.x,p.y,11,0,TAU);ctx.stroke();ctx.globalAlpha=1;}
if(p.focus){px(p.x-1,p.y-1,3,3,"#ff3355");px(p.x-0.5,p.y-0.5,2,2,"#fff");
ctx.strokeStyle="rgba(255,51,85,.5)";ctx.beginPath();ctx.arc(p.x,p.y,6,0,TAU);ctx.stroke();}
}
// 粒子与浮字spark 粒子渲染为线段)
for(const q of G.parts){ctx.globalAlpha=clamp(q.life/20,0,1);
if(q.spark){ctx.strokeStyle=q.col;ctx.beginPath();ctx.moveTo(q.x,q.y);ctx.lineTo(q.x-q.vx*2.2,q.y-q.vy*2.2);ctx.stroke();}
else px(q.x,q.y,q.sz,q.sz,q.col);
ctx.globalAlpha=1;}
ctx.textAlign="center";
for(const t of G.texts){ctx.globalAlpha=clamp(t.life/30,0,1);ctx.font="6px 'Press Start 2P',monospace";ctx.fillStyle=t.col;ctx.fillText(t.t,t.x,t.y);ctx.globalAlpha=1;}
if(!G.demoSim)drawHUD();
// 横幅
if(G.bannerT>0){
const a=clamp(G.bannerT/30,0,1);
ctx.globalAlpha=Math.min(1,a);
ctx.font="bold 13px 'Microsoft YaHei',monospace";
ctx.fillStyle=G.banner.col;ctx.textAlign="center";
ctx.fillText(G.banner.t,W/2,H*0.38);
ctx.font="7px 'Press Start 2P',monospace";ctx.fillStyle="#7f8cb8";
if(G.mutator&&G.bannerT>60)ctx.fillText(MUTS[G.mutator].ds,W/2,H*0.38+16);
ctx.globalAlpha=1;
}
if(G.flash>0){ctx.globalAlpha=G.flash/18;ctx.fillStyle="#fff";ctx.fillRect(0,0,W,H);ctx.globalAlpha=1;}
}
ctx.restore();
}
function drawHUD(){
ctx.textAlign="left";ctx.font="7px 'Press Start 2P',monospace";
ctx.fillStyle="#dfe8ff";ctx.fillText("SC "+G.score,5,10);
ctx.fillStyle="#ffcf5c";ctx.textAlign="right";ctx.fillText("✦"+G.coins,W-5,10);
ctx.textAlign="left";
ctx.fillStyle="#7f8cb8";
ctx.fillText(G.mode==="endless"?"WAVE "+G.wave:G.mode==="rush"?"BOSS RUSH":"STAGE "+G.stage+"-"+G.wave,5,20);
if(G.combo>4){ctx.fillStyle="#ff9a3d";ctx.textAlign="right";
ctx.fillText("x"+(1+Math.min(2,G.combo*0.02)).toFixed(1)+" COMBO "+G.combo,W-5,20);ctx.textAlign="left";}
// 结构值血条(绿→红)+ 备用机体 + 炸弹
const hpw=62,pc2=clamp(G.hp/G.maxHp,0,1);
px(4,H-12,hpw+2,6,"#0d1226");
px(5,H-11,hpw,4,"#3a1020");
px(5,H-11,hpw*pc2,4,pc2>0.5?"#5cff9d":pc2>0.25?"#ffcf5c":"#ff5566");
for(let i=0;i<G.stat.maxLives;i++){ctx.globalAlpha=i<G.lives?1:0.2;ctx.drawImage(SPR.heart,5+i*9,H-21);}
ctx.globalAlpha=1;
// 经验条
px(0,H-3,W,3,"#0d1226");px(0,H-3,W*clamp(G.xp/G.xpNeed,0,1),3,"#43e8ff");
ctx.fillStyle="#43e8ff";ctx.font="6px 'Press Start 2P',monospace";
ctx.fillText("LV"+G.level,W-24,H-5);
// 超频条
const odw=54;
px(W-odw-4,H-16,odw,4,"#0d1226");
px(W-odw-4,H-16,odw*clamp(G.od/100,0,1),4,G.odActive>0?"#fff":"#ff9a3d");
if(G.odActive>0){ctx.fillStyle="#ff9a3d";ctx.textAlign="center";ctx.fillText("暴走 OVERDRIVE",W/2,30);ctx.textAlign="left";}
// 主武器装备栏图标
for(let i=0;i<G.loadout.length;i++){
const w=WEAPONS[G.loadout[i]],img=w.icon?ICONS[w.icon]:SPR[w.spr];
if(img)ctx.drawImage(img,W-10-i*9,H-28,7,7);
}
}
/* ================= 主循环 ================= */
function update(){
G.time++;
if(G.hitstop>0){G.hitstop--;return;}
if(G.shake>0)G.shake*=0.85;if(G.shake<0.3)G.shake=0;
if(G.flash>0)G.flash--;
if(G.bannerT>0)G.bannerT--;
if(G.comboT>0){G.comboT--;if(G.comboT===0)G.combo=0;}
updatePlayer();
if(G.demoSim)demoSpawn();else updateSpawner();
for(const e of G.enemies){e.uid=e.uid||_uid++;updateEnemy(e);}
G.enemies=G.enemies.filter(e=>!e.dead);
updateBoss();
updateBullets();
collide();
for(const q of G.parts){
if(q.suck){q.vx+=(q.suck.x-q.x)*0.09;q.vy+=(q.suck.y-q.y)*0.09;}
else{q.vx*=0.95;q.vy*=0.95;}
if(q.grav)q.vy+=q.grav;
q.x+=q.vx;q.y+=q.vy;q.life--;}
G.parts=G.parts.filter(q=>q.life>0);
for(const t of G.texts){t.y-=0.5;t.life--;}
G.texts=G.texts.filter(t=>t.life>0);
}
let last=0,acc=0;
function loop(ts){
requestAnimationFrame(loop);
const dt=Math.min(100,ts-last);last=ts;
MUS.tick();
if(UI==="play"||UI==="demoSim"){
acc+=dt;
let steps=0;
while(acc>=1000/60&&steps<4){update();acc-=1000/60;steps++;}
if(steps===4)acc=0;
}
render();
}
requestAnimationFrame(loop);

187
js/sprites.js Normal file
View File

@ -0,0 +1,187 @@
"use strict";
/* ================= 像素精灵工坊 ================= */
function bake(rows,pal,scale){
const w=Math.max(...rows.map(r=>r.length)),h=rows.length,s=scale||1;
const c=document.createElement("canvas");c.width=w*s;c.height=h*s;
const g=c.getContext("2d");
for(let y=0;y<h;y++){const row=rows[y];
for(let x=0;x<row.length;x++){const ch=row[x];
if(ch==="."||ch===" ")continue;const col=pal[ch];if(!col)continue;
g.fillStyle=col;g.fillRect(x*s,y*s,s,s);}}
return c;
}
const SPR={};
/* ---- 5 架战机(各有独立造型) ---- */
SPR.ship0=bake([
"......K......",".....KWK.....",".....KWK.....",".....KCK.....",
"....KWWWK....","....KRCRK....","...KWRRRWK...","..KWKRDRKWK..",
"..KWRRRRRWK..",".KWKRRRRRKWK.",".KRRKWWWKRRK.","KKRDKWKWKDRKK",
"KD..KWKWK..DK","K...KOKOK...K","....KOKOK...."],
{K:"#14142b",W:"#f2f0e6",C:"#43e8ff",R:"#e0393e",D:"#8f1d2c",O:"#ff9a3d"});
SPR.ship1=bake([
".......K.......","......KPK......",".....KPPPK.....",".....KPCPK.....",
"....KPCCCPK....","...KPPCPPCPK...","..KPPCPPPPCPK..",".KPCKPPPPPKCPK.",
"KPPKPPPPPPPKPPK","KPKPPKPPPPKPPKK","KK.PPKPPPKPP.KK","K..KPKKPKKPK..K",
"...KO..K..OK...","....O.....O...."],
{K:"#12102a",P:"#7b4fd0",C:"#43e8ff",O:"#41e6ff"});
SPR.ship2=bake([
"...KK.....KK...","..KBBK...KBBK..","..KBBK...KBBK..",".KBBBK...KBBBK.",
".KBWBBK.KBBWBK.","KBBBBBK.KBBBBBK","KBCBBBK.KBBBCBK","KBBBBBK.KBBBBBK",
".KBBBKBBBKBBBK.","..KBBBBBBBBBK..","..KBOBBBBBOBK..","...KBBBXBBBK...",
"...KOXXKXXOK...","....KOOOOOK....",".....KK.KK....."],
{K:"#101a12",B:"#3f8f5f",W:"#d8f0e0",C:"#ffd23d",X:"#ff9a3d",O:"#ff9a3d"});
SPR.ship3=bake([
"......K......",".....KVK.....","....KVVVK....","....KVWVK....",
"....KVCVK....","...KVVWCVK...","..KVVKVWKVVK..",".KVKKVVVVVKVK.",
"KVK.KVWCVK.VK","KV..KVVVVVK..VK","K..KVKWKVK..K","...KOKWKWKOK...",
"....KOKKKOK....",".....KOKOK.....","......KO......."],
{K:"#241a08",V:"#e8c25a",W:"#fff6e0",C:"#ff6b9d",O:"#ffb84d"});
SPR.ship4=bake([
".......K.......","......KAK......",".....KAAAK.....",".....KACAK.....",
"....KAACAAK....","...KAACCCAAK...","..KAACAAACAAK..",".KAAKAAAAAKAAK.",
"KAAKAAAAAAAKAAK","KAKAAKAAAKAAKAK","KAAK.KAAAK.KAAK","KA..KAKAKAK..AK",
"K..KOAKAKAOK..K","...KO..K..OK...",".....O...O....."],
{K:"#0d0d14",A:"#3a3f5c",C:"#ff3355",O:"#ff5566"});
/* ---- 敌机(面朝下) ---- */
const EPAL={
scout:{K:"#0d1a1a",T:"#2fa8a0",W:"#c8fff4",D:"#14655f"},
weaver:{K:"#1a0d1a",M:"#c44fd0",W:"#ffd6f4",D:"#6e2a78"},
gunship:{K:"#141410",S:"#8c9a5b",W:"#eef0d0",X:"#ff9a3d",D:"#4a5428"},
kami:{K:"#1a0d0d",R:"#e0393e",Q:"#ffd23d",W:"#ffe9c8"},
mine:{K:"#101418",G:"#7a8a96",W:"#e8f0f8",R:"#ff5566"},
split:{K:"#160d1a",P:"#b05ce8",W:"#f0d6ff",D:"#5c2a80"},
sniper:{K:"#0d141a",S:"#4f7ab0",C:"#43e8ff",W:"#d0ecff"},
rock:{K:"#141210",G:"#7a7268",D:"#4a443c"}
};
SPR.scout=bake(["...KKKK...","..KTTTTK..",".KTTKKTTK.",".KTKTTKTK.","KTTTTTTTTK","KWKTTTTKWK",".K.KTTK.K.","...KDDK...","....KK...."],EPAL.scout);
SPR.weaver=bake(["....KKKK....","..KKMMMMKK..",".KMMKMMKMMK.","KMMKMMMMKMMK","KMKMMMMMMKMK","KMMMMKKMMMMK",".KMMKMMKMMK.","..KM.MM.MK..","...KMMMMK...","....KKKK...."],EPAL.weaver);
SPR.gunship=bake(["......KKKK......",".....KSSSSK.....","....KSSSSSSK....","...KSSKSSKSSK...","..KSSSSSSSSSSK..",".KSKSSSSSSSSKSK.",".KSSKSKSSKSKSSK.","KSSSSSSSSSSSSSSK","KSKSSKSSSSKSSKSK","KSSSKSKSSKSKSSSK",".KSKSSKSSKSSKSK.","..KK.KSXXSK.KK..",".....KSXXSK.....","......KKKK......"],EPAL.gunship);
SPR.kami=bake(["....KK....","...KRRK...","..KRRRRK..","..KRQQRK..",".KRRQQRRK.",".KRQQQQRK.","KRRQQQQRRK","K.RQQQQR.K","..KRRRRK..","...KKKK..."],EPAL.kami);
SPR.mine=bake(["...K..K...","....KK....","..KGGGGK..",".KGGGGGGK.","KKGKGGKGKK","KGGGGGGGGK",".KGGGGGGK.","..KGGGGK..","....KK....","...K..K..."],EPAL.mine);
SPR.split=bake(["....KKKK....","..KKPPPPKK..",".KPPKPPKPPK.","KPPPPPPPPPPK","KPKPPKKPPKPK","KPPPKPPKPPPK",".KPPPPPPPPK.","..KPKPPKPK..","...KPPPPK...","....KKKK...."],EPAL.split);
SPR.mini=bake([".KKKK.","KPPPPK","PKPPKP","KPPPPK",".KPPK.","..KK.."],EPAL.split);
SPR.sniper=bake([".....KK.....","....KSSK....","...KSSSSK...","..KSKSSKSK..","..KSSCCSSK..",".KSSSCCSSSK.","KSKSSCCSSKSK","KSSSKSSKSSSK",".KSK.KK.KSK.","..K..KK..K..","..K.K..K.K..",".....KK....."],EPAL.sniper);
SPR.rock=bake(["....KKKK....","..KKGGGGKK..",".KGGDGGGGGK.","KGGGGDGGGGGK","KGDGGGGGGDGK","KGGGGDDGGGGK","KGGDGGGGGGGK",".KGGGGGGDGK.","..KKGGGGKK..","....KKKK...."],EPAL.rock);
/* 精英变体:保留明暗的金化调色 */
function goldify(pal){
const o={};
for(const k in pal){
if(k==="K"){o[k]="#2a1f00";continue;}
const c=pal[k],r=parseInt(c.slice(1,3),16),g=parseInt(c.slice(3,5),16),b=parseInt(c.slice(5,7),16);
const l=Math.min(1,0.3+(0.3*r+0.6*g+0.1*b)/255*0.85);
const mix=(a,h)=>Math.round(a+(h-a)*l);
o[k]=`rgb(${mix(94,255)},${mix(64,232)},${mix(10,150)})`;
}
return o;
}
SPR.e_scout=bake(["...KKKK...","..KTTTTK..",".KTTKKTTK.",".KTKTTKTK.","KTTTTTTTTK","KWKTTTTKWK",".K.KTTK.K.","...KDDK...","....KK...."],goldify(EPAL.scout));
SPR.e_weaver=bake(["....KKKK....","..KKMMMMKK..",".KMMKMMKMMK.","KMMKMMMMKMMK","KMKMMMMMMKMK","KMMMMKKMMMMK",".KMMKMMKMMK.","..KM.MM.MK..","...KMMMMK...","....KKKK...."],goldify(EPAL.weaver));
SPR.e_gunship=bake(["......KKKK......",".....KSSSSK.....","....KSSSSSSK....","...KSSKSSKSSK...","..KSSSSSSSSSSK..",".KSKSSSSSSSSKSK.",".KSSKSKSSKSKSSK.","KSSSSSSSSSSSSSSK","KSKSSKSSSSKSSKSK","KSSSKSKSSKSKSSSK",".KSKSSKSSKSSKSK.","..KK.KSXXSK.KK..",".....KSXXSK.....","......KKKK......"],goldify(EPAL.gunship));
SPR.e_kami=bake(["....KK....","...KRRK...","..KRRRRK..","..KRQQRK..",".KRRQQRRK.",".KRQQQQRK.","KRRQQQQRRK","K.RQQQQR.K","..KRRRRK..","...KKKK..."],goldify(EPAL.kami));
SPR.e_split=bake(["....KKKK....","..KKPPPPKK..",".KPPKPPKPPK.","KPPPPPPPPPPK","KPKPPKKPPKPK","KPPPKPPKPPPK",".KPPPPPPPPK.","..KPKPPKPK..","...KPPPPK...","....KKKK...."],goldify(EPAL.split));
SPR.e_sniper=bake([".....KK.....","....KSSK....","...KSSSSK...","..KSKSSKSK..","..KSSCCSSK..",".KSSSCCSSSK.","KSKSSCCSSKSK","KSSSKSSKSSSK",".KSK.KK.KSK.","..K..KK..K..","..K.K..K.K..",".....KK....."],goldify(EPAL.sniper));
SPR.e_mine=bake(["...K..K...","....KK....","..KGGGGK..",".KGGGGGGK.","KKGKGGKGKK","KGGGGGGGGK",".KGGGGGGK.","..KGGGGK..","....KK....","...K..K..."],goldify(EPAL.mine));
SPR.e_splitter=bake(["....KKKK....","..KKPPPPKK..",".KPPKPPKPPK.","KPPPPPPPPPPK","KPKPPKKPPKPK","KPPPKPPKPPPK",".KPPPPPPPPK.","..KPKPPKPK..","...KPPPPK...","....KKKK...."],goldify(EPAL.split));
/* ---- 弹幕 / 道具 ---- */
SPR.msl=bake(["..K..",".KWK.","KWWWK","KWKWK",".KKK."],{K:"#14142b",W:"#c8d8ff"});
SPR.blade=bake(["...K...","..KCK..",".KCCCK.","KCCXCCK",".KCCCK.","..KCK..","...K..."],{K:"#0d2a1a",C:"#5cff9d",X:"#eafff0"});
/* 9×9 圆弹H=半透明圆形光晕 C=彩色外壳 W=白芯 */
function orb(c1,c2){
const halo=c2.length===7?c2+"40":c2;
return bake([
"...HHH...",
".HHCCCHH.",
".HCCWCCH.",
"HCCCCCCCH",
"HCWWWWWCH",
"HCCCCCCCH",
".HCCWCCH.",
".HHCCCHH.",
"...HHH..."],{H:halo,W:"#ffffff",C:c2});
}
const BHALO={orbR:"#ff5566",orbP:"#b05ce8",orbG:"#5cff9d",orbO:"#ff9a3d",dartB:"#3a8cd8",flowr:"#ff8fc8",dartS:"#cfe4ff",dartP:"#b98cff"};
SPR.dartB=bake(["KCCCK",".CCC.",".CWC.","..C.."],{K:"#0d2433",C:"#3a8cd8",W:"#cfeaff"}); // 蓝色锥形敌弹Boss弹幕
SPR.flowr=bake([
"....P....",".P..P..P.","..PPPPP..",".PPWWWPP.","PPPWWWPPP",
".PPWWWPP.","..PPPPP..",".P..P..P.","....P...."],
{P:"#ff8fc8",W:"#ffffff"}); // 太空原种:花弹(圆形八刺,会逐渐变大)
SPR.dartS=bake(["SWS","SWS","SWS","SWS","SWS","SWS",".S.",".W."],
{S:"#cfe4ff",W:"#ffffff"}); // 夺权者X银色高弹速长钉
SPR.dartP=bake(["KPPPK",".PPP.",".PWP.","..P.."],{K:"#2a1040",P:"#b98cff",W:"#ead9ff"}); // 暗黑魔碟:紫锥弹
SPR.orbR=orb("#ffe9e9","#ff5566");SPR.orbP=orb("#f4e9ff","#b05ce8");
SPR.orbG=orb("#e9ffe9","#5cff9d");SPR.orbO=orb("#fff4d9","#ff9a3d");
SPR.plasma=orb("#e9f8ff","#43b8ff");
SPR.gem=bake(["..KK..",".KCCK.","KCWWCK","KCCCCK",".KCCK.","..KK.."],{K:"#0d2a33",C:"#43e8ff",W:"#eaffff"});
SPR.coin=bake([".KKKK.","KYWWYK","KWYYWK","KYWYYK","KYYYYK",".KKKK."],{K:"#33240a",Y:"#ffcf5c",W:"#fff4d9"});
SPR.heart=bake([".KK.KK.","KRRKRRK","KRRRRRK",".KRRRK.","..KRK..","...K..."],{K:"#330d14",R:"#ff5566"});
SPR.bombp=bake(["...K...","..KOK..",".KKKKK.","KBBBBBK","KBBWBBK","KBBBBBK",".KKKKK."],{K:"#14142b",B:"#4a5a8c",W:"#dfe8ff",O:"#ff9a3d"});
SPR.odc=bake(["..KK..",".KOOK.","KOWWOK","KOWOOK",".KOOK.","..KK.."],{K:"#33240a",O:"#ff9a3d",W:"#fff4d9"});
/* ---- 新武器弹体 / 僚机精灵 ---- */
SPR.cres=bake(["..KKK...",".KCCCK..","KCCK....","KCK.....","KCK.....","KCCK....",".KCCCK..","..KKK..."],{K:"#0d2a1a",C:"#5cff9d"});
SPR.fiss=bake(["..KK..",".KGGK.","KGWWGK","KGGGGK",".KGGK.","..KK.."],{K:"#0d2a0d",G:"#5cff9d",W:"#eafff0"});
SPR.frost=bake(["..K..",".KCK.","KCWCK","KCCCK",".KCK.","..K.."],{K:"#0d2433",C:"#7fd8ff",W:"#ffffff"});
SPR.sat=bake(["..KKK..",".KCCCK.","KCCWCCK","KCW WC K".replace(" ",""),"KCCWCCK",".KCCCK.","..KKK.."],{K:"#0d2433",C:"#43e8ff",W:"#ffffff"});
SPR.bot=bake(["..K..",".KGK.","KGWGK",".KGK.","..K.."],{K:"#0d2a0d",G:"#5cff9d",W:"#eafff0"});
SPR.ndl=bake([".K.","KCK","KWK","KCK","KCK","KCK",".K."],{K:"#142a0d",C:"#9dff5c",W:"#f0ffe0"});
SPR.bub=bake(["...KKK...",".KKBBBK K".replace(" ",""),"KBBBWBBBK","KBWWWWBBK","KBWWBWBBK","KBBBBBBBK",".KKBBBK K".replace(" ",""),"...KKK..."],{K:"#0d2433",B:"#3a8cd8",W:"#cfeaff"});
SPR.met=bake(["..KK..",".KOOK.","KOOOOK","KOWOOK",".KOOK.","..KK..","...K..."],{K:"#331d0a",O:"#ff9a3d",W:"#ffd23d"});
SPR.boomer=bake(["K...K","KPKPK",".PPP.","..K.."],{K:"#1a0d26",P:"#c86bff"}); // 亮紫回旋镖,清晰可辨
SPR.sing=bake(["..KKK..",".KPPPK.","KPXXXP K".replace(" ",""),"KPXXXP K".replace(" ",""),"KPPPPPK",".KPPPK.","..KKK.."],{K:"#140d1a",P:"#7b2fd0",X:"#0d0514"});
SPR.waveb=bake(["....K....","..K.C.K..","K.C.C.C.K","..K.C.K..","....K...."],{K:"#0d1a33",C:"#43b8ff"});
SPR.holyo=bake(["..KKK..",".KYWYK.","KYW W YK".replace(/ /g,""),"KYW W YK".replace(/ /g,""),".KYWYK.","..KKK.."],{K:"#33240a",Y:"#ffcf5c",W:"#fff4d9"});
SPR.thnd=bake(["..KK",".KWK","KWK.","KWK.",".KWK",".KK.","..K."],{K:"#33240a",W:"#ffe95c"});
SPR.grv=bake(["..KKK..",".K...K.","K..P..K","K.PXP.K" ,"K..P..K",".K...K.","..KKK.."],{K:"#1a0d2a",P:"#b05ce8",X:"#2a0d3a"});
/* ---- 辨识度重设计:新弹体精灵 ---- */
SPR.trcr=bake([".K.","KYK","KYK","KWK","KYK","KYK",".K."],{K:"#33240a",Y:"#ffcf5c",W:"#fff4d9"}); // 火神金色曳光弹
SPR.pelt=bake(["KOOK","OWWO","OWWO","KOOK"],{K:"#331d0a",O:"#ff9a3d",W:"#fff4d9"}); // 霰弹炽热弹丸
SPR.shard=bake(["K..C..","KCCKC.","KCWCKK","KCCCK.","K..C.."],{K:"#0d2433",C:"#7fd8ff",W:"#ffffff"}); // 冰晶霰屑
SPR.icelance=bake(["...K...","..KCK..","..KCK..",".KCWK.",".KCCK.",".KCCK.","..KCK..","..KCK..","...K..."],{K:"#0d2433",C:"#7fd8ff",W:"#ffffff"}); // 贯穿冰枪
SPR.msl2=bake(["..K..",".KYK.","KYGYK","KYGYK",".KGK.","K.K.K"],{K:"#2a240d",Y:"#ffe95c",G:"#8ca86a"}); // 蜂巢小导弹
SPR.amsl=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0763e",W:"#fff4d9"}); // 空爆重型导弹
SPR.cmsl=bake(["...KK...","..KRRK..",".KRRRRK.","KRWRRWRK",".KRRRRK.",".KRRRRK.","..KRRK..",".K.KK.K."],{K:"#1a0d0d",R:"#e0393e",W:"#ffd23d"}); // 斩首巡航导弹
SPR.broad=bake([".KKK.","KBBBK","KBWBK","KBBBK",".KKK."],{K:"#241a08",B:"#c8923e",W:"#fff4d9"}); // 侧舷炮弹
SPR.flare=bake(["..KK..",".KWWK.","KWWWWK","KWWWWK",".KWWK.","..KK.."],{K:"#33240a",W:"#ffd23d"}); // 热诱弹
SPR.sticky=bake([".KKKK.","KGKKGK","KGRRGK","KGRRGK","KGKKGK",".KKKK."],{K:"#142a0d",G:"#5cff9d",R:"#ff5566"}); // 粘着炸弹
/* ---- 新武器弹体精灵(差异化造型) ---- */
SPR.shuri=bake(["...K...","..KCK..",".KCCK..","KCCWCCK","..KCCK.","..KCK..","...K..."],{K:"#0d2433",C:"#41e6ff",W:"#ffffff"}); // 电弧手里剑:青白四芒星
SPR.voltb=bake([".K.","KGK","GWG","KGK","KGK",".K."],{K:"#142a0d",G:"#b8ff3d",W:"#ffffff"}); // 电能弹:荧光绿+白电纹
SPR.dawnsh=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0393e",W:"#fff4d9"}); // 黎明重炮弹:炽红重壳
SPR.protb=bake([".K.","KCK","KWK","KCK","KCK","KCK",".K."],{K:"#140a0a",C:"#ff4a1f",W:"#ffc64a"}); // 协议速射弹:黑红橙
SPR.protoh=orb(null,"#ff5a2e"); // 协议重炮弹:黑壳橙芯大能量球
SPR.neb=orb(null,"#8a5cff"); // 星云奥秘:紫蓝奥术球
SPR.star4=bake(["..K..","..K..","KKBKK","..K..","..K.."],{K:"#e0b3ff",B:"#ffffff"}); // 星云绽放的小星星
/* 12 种僚机 */
SPR.dGun=bake(["...K...","..KWK..",".KWWWK.","KWKWKWK","..KWK.."],{K:"#14142b",W:"#c8d8ff"});
SPR.dGun2=bake(["K..K..K","KKK.KKK",".KWKWK.","..KWK..","...K..."],{K:"#14142b",W:"#9fd8ff"});
SPR.dSnip=bake(["..K..",".KWK.",".KCK.",".KWK.","KWKWK"],{K:"#0d2433",W:"#c8d8ff",C:"#43e8ff"});
SPR.dBeam=bake([".KKKKK.","KCCCCCK","KCW K WCK".replace(/ /g,""),"KCCCCCK",".KKKKK."],{K:"#2a0d1a",C:"#ff6b9d",W:"#fff"});
SPR.dBomb=bake(["..KKK..",".KO O K.".replace(/ /g,""),"KOOOOOK","KOOOWOK",".KKKKK."],{K:"#241a08",O:"#ff9a3d",W:"#fff4d9"});
SPR.dFrost=bake(["...K...","..KCK..",".KCCK.","KCCWCK.",".KCCK.","..KCK..","...K..."],{K:"#0d2433",C:"#7fd8ff",W:"#fff"});
SPR.dTesla=bake(["..KK...",".KWWK..","KWK....",".KWK...","..KWK..","...KK.."],{K:"#33240a",W:"#ffe95c"});
SPR.dRam=bake(["...K...","..KRK..",".KRRRK.","KRRRRRK",".KRRRK.","..KRK..","...K..."],{K:"#1a0d0d",R:"#e0393e"});
SPR.dHeal=bake(["..KKK..",".KGGGK.","KGWGWGK","KGGGGGK",".KGGGK.","..KKK.."],{K:"#0d2a1a",G:"#3f8f5f",W:"#eafff0"});
SPR.dShield=bake([".KKKKK.","KCCBCCK","KCBKBCK","KCCBCCK",".KKKKK."],{K:"#0d2433",C:"#43e8ff",B:"#1a5a8c"});
SPR.dMsl=bake(["..KKK..",".KWWWK.","KWKWKWK",".KWWWK.","..KKK.."],{K:"#14142b",W:"#ffb84d"});
SPR.dBlade=bake(["K..K..K",".K.K.K.","..KKK..",".K.K.K.","K..K..K"],{K:"#0d2a1a",W:"#5cff9d"});
SPR.dSweep=bake(["K.K.K.K","KWKWKWK",".KWWWK.","..KWK..","...K..."],{K:"#1a1206",W:"#ffb03d"}); // 扫射僚机:横排多炮管
SPR.swep=bake([".W.","WCW","WCW",".K."],{K:"#331d0a",W:"#ffd23d",C:"#ff9a3d"}); // 扫射弹:金黄弹丸
/* ---- 升级卡片图标8×8 单色) ---- */
function icon(rows,color){return bake(rows,{X:color});}
const ICONS={
gun:icon(["X..X..X.","X..X..X.","XX.XX.XX","XX.XX.XX",".X.X.X..",".X.X.X..","........","........"],"#7fd8ff"),
spread:icon(["X..X..X.","X..X..X.",".X.X.X..",".X.X.X..","..XXX...","..XXX...","...X....","...X...."],"#ffb84d"),
beam:icon(["...XX...","...XX...","...XX...","...XX...","...XX...","...XX...","...XX...","...XX..."],"#43e8ff"),
pulse:icon(["..X..X..",".X..X..X","X..X..X.",".X..X..X","..X..X..",".X..X..X","X..X..X.",".X..X..X"],"#5cff9d"),
rail:icon(["...XX...","..XXXX..",".XXXXXX.","...XX...","...XX...","...XX...","...XX...","...XX..."],"#b98cff"),
laser:icon(["...X....","...X....","..XXX...",".XXXXX.","..XXX...","...X....","...X....","...X...."],"#ff6b9d"),
chain:icon(["....XX..","...XX...","..XX....","..XXXX..","...XX...","..XX....",".XX.....","XX......"],"#ffe95c"),
fire:icon(["..X.X...",".X.X.X..","X.X.X.X.","..X.X...",".X.X.X..","X.X.X.X.","........","........"],"#ff9a3d"),
crit:icon(["X..XX..X",".X.XX.X.","..XXXX..","XXXXXXXX","XXXXXXXX","..XXXX..",".X.XX.X.","X..XX..X"],"#ff5566"),
ghost:icon(["..XXXX..",".XXXXXX.","XX.XX.XX","XXXXXXXX","XXXXXXXX","X.XXXX.X","..X..X..","........"],"#c8d8ff"),
shield:icon([".XXXXXX.","XXXXXXXX","XXXXXXXX","XXXXXXXX",".XXXXXX.","..XXXX..","...XX...","........"],"#43e8ff"),
nano:icon(["...XX...","...XX...","...XX...","XXXXXXXX","XXXXXXXX","...XX...","...XX...","...XX..."],"#5cff9d"),
eng:icon(["XXXX....","..XXXX..","XXXX....","..XXXXXX","XXXX....","..XXXX..","XXXX....","........"],"#9fd8ff"),
mag:icon([".XX..XX.",".XX..XX.",".XX..XX.",".XX..XX.",".XXXXXX.","..XXXX..","........","........"],"#b98cff"),
drone:icon(["...XX...","..XXXX..",".XX.XXX.","XXXXXXXX","XXXXXXXX",".XX.XX..","..XXXX..","...XX..."],"#ffcf5c"),
flame:icon(["...X....","..XX....",".XX.X...",".XXXX...","XXXXXX..","X.XXX.X.",".XXXX...","..XX...."],"#ff9a3d")
};

340
js/ui.js Normal file
View File

@ -0,0 +1,340 @@
"use strict";
/* ================= 界面流转 ================= */
const OVLS=["o-title","o-setup","o-hangar","o-help","o-cards","o-pause","o-over","o-codex","o-demo","o-rush","o-slot"];
function show(id){for(const o of OVLS)$(o).classList.toggle("hide",o!==id);}
const SETUP={mode:"stage",ship:"falcon",diff:0};
function bestLine(){
const b=SAVE.best;
$("best-line").textContent=`无尽纪录 ${Math.max(b.e0,b.e1)} · 闯关进度 ${b.stage}/6`;
}
$("b-go").onclick=()=>{AU.init();buildSetup();show("o-setup");};
$("b-help").onclick=()=>show("o-help");
$("b-back3").onclick=()=>show("o-title");
$("b-back1").onclick=()=>show("o-title");
$("b-back2").onclick=()=>show("o-title");
$("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");};
$("b-codex").onclick=()=>{AU.init();buildCodex();show("o-codex");};
$("b-rush").onclick=()=>{AU.init();buildRush();show("o-rush");};
$("b-back6").onclick=()=>show("o-title");
$("b-back4").onclick=()=>{bestLine();show("o-title");};
$("b-back5").onclick=exitDemoSim;
$("b-start").onclick=()=>startGame();
if("ontouchstart" in window){const pb=$("tb-pause");pb.style.display="block";pb.onclick=()=>togglePause();}
$("b-resume").onclick=()=>togglePause();
$("b-restart").onclick=()=>{show("");startGame();};
$("b-quit").onclick=()=>{bank();G=null;MUS.stop();UI="title";bestLine();show("o-title");};
$("b-again").onclick=()=>startGame();
$("b-home").onclick=()=>{UI="title";bestLine();show("o-title");};
$("mode-pick").addEventListener("click",e=>{
const c=e.target.closest(".pcard");if(!c)return;
SETUP.mode=c.dataset.m;
for(const el of $("mode-pick").children)el.classList.toggle("sel",el===c);
});
$("diff-pick").addEventListener("click",e=>{
const b=e.target.closest("button");if(!b)return;
SETUP.diff=+b.dataset.d;
for(const el of $("diff-pick").children)el.classList.toggle("primary",el===b);
});
function buildSetup(){
const box=$("ship-pick");box.innerHTML="";
for(const k in SHIPS){
const s=SHIPS[k],owned=SAVE.ships.includes(k);
const d=document.createElement("div");
d.className="pcard"+(owned?"":" lock")+(SETUP.ship===k?" sel":"");
const c=document.createElement("canvas");c.width=52;c.height=64;
const g=c.getContext("2d");g.imageSmoothingEnabled=false;
const spr=SPR[s.spr];g.drawImage(spr,(52-spr.width*3)/2,(64-spr.height*3)/2,spr.width*3,spr.height*3);
d.appendChild(c);
d.insertAdjacentHTML("beforeend",`<div class="nm">${s.name}</div><div class="ds">${s.trait}</div>`+(owned?"":`<div class="cost">✦ ${s.cost} 解锁</div>`));
d.onclick=()=>{
if(!SAVE.ships.includes(k)){
if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildSetup();}
else SFX.hurt();
return;
}
SETUP.ship=k;buildSetup();
};
box.appendChild(d);
}
}
/* ================= 机库商店 ================= */
const META=[
{key:"weapon",name:"主武器校准",ds:"出击时主武器等级 +1",max:2,costs:[300,800]},
{key:"armor",name:"强化装甲",ds:"出击时生命上限 +1",max:2,costs:[400,1000]},
{key:"hpup",name:"结构强化",ds:"出击时结构值 +15",max:2,costs:[250,700]},
{key:"luck",name:"幸运币",ds:"高品质强化概率提升",max:2,costs:[350,900]},
{key:"wallet",name:"收藏家协议",ds:"星币获取 +15%",max:3,costs:[300,800,2000]},
{key:"mag2",name:"引力透镜",ds:"拾取范围 +15%",max:2,costs:[200,600]},
{key:"odstart",name:"超频预载",ds:"出击时超频已充能 50%",max:1,costs:[500]}
];
function buildShop(){
$("wallet").textContent=SAVE.coins;
const box=$("shop");box.innerHTML="";
for(const m of META){
const lv=SAVE.m[m.key]||0,maxed=lv>=m.max,cost=maxed?0:m.costs[lv];
const d=document.createElement("div");d.className="sitem";
d.innerHTML=`<div class="inf"><div class="nm">${m.name}</div><div class="ds">${m.ds}</div>
<div class="pips">${"■".repeat(lv)}${"□".repeat(m.max-lv)}</div></div>
<button ${maxed||SAVE.coins<cost?"disabled":""}>${maxed?"MAX":"✦ "+cost}</button>`;
if(!maxed)d.querySelector("button").onclick=()=>{
if(SAVE.coins<cost)return;
SAVE.coins-=cost;SAVE.m[m.key]=lv+1;persist();SFX.coin();buildShop();
};
box.appendChild(d);
}
}
/* ================= 开局 / 暂停 / 结算 ================= */
function startGame(){
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
newRun(SETUP.mode,SETUP.ship,SETUP.diff);
UI="play";show("");acc=0;
MUS.start(false);
banner(SETUP.mode==="stage"?"STAGE 1 — "+STAGE_NAMES[0]:"无尽模式 — 生存到底!","#41e6ff",130);
startWave();
}
function togglePause(){
if(!G)return;
if(UI==="play"){UI="pause";buildLoadoutUI();show("o-pause");}
else if(UI==="pause"){UI="play";show("");}
}
/* ================= 换装面板 ================= */
function buildLoadoutUI(){
const box=$("loadout");if(!box||!G)return;box.innerHTML="";
const mk=(title,dict,owned,active,max)=>{
const sec=document.createElement("div");
sec.innerHTML=`<div class="h3" style="margin:6px 0 2px">${title}${active.length}/${max})· 点击切换装备</div>`;
const row=document.createElement("div");
for(const k in owned){
const d=document.createElement("span");
const on=active.includes(k);
d.className="litem"+(on?" on":"");
d.textContent=`${dict[k].name} Lv${owned[k]}`;
d.onclick=()=>{
const i=active.indexOf(k);
if(i>=0){if(active===G.loadout&&active.length===1){SFX.hurt();return;}active.splice(i,1);}
else{if(active.length>=max){SFX.hurt();return;}active.push(k);}
SFX.pick();buildLoadoutUI();
};
row.appendChild(d);
}
if(Object.keys(owned).length===0)row.innerHTML='<span class="small">尚未获得</span>';
sec.appendChild(row);box.appendChild(sec);
};
mk("主武器",WEAPONS,G.weapons,G.loadout,5);
mk("副武器",SUBS,G.subs,G.subLoadout,5);
mk("僚机",DRONES,G.drones,G.droneLoadout,3);
}
/* ================= 图鉴 ================= */
function iconCanvas(img,size){
const c=document.createElement("canvas");c.width=c.height=size;
const g=c.getContext("2d");g.imageSmoothingEnabled=false;
const sc=Math.min(size/img.width,size/img.height);
g.drawImage(img,(size-img.width*sc)/2,(size-img.height*sc)/2,img.width*sc,img.height*sc);
return c;
}
function buildCodex(){
const box=$("codex");box.innerHTML="";
const total=MAINS.length+SUB_KEYS.length+DRONE_KEYS.length+PAS_KEYS.length;
$("codex-t").textContent=total;
$("codex-n").textContent=Object.keys(SAVE.codex).length;
const mk=(title,keys,dict,kind)=>{
const h=document.createElement("div");h.className="cx-sec";h.textContent=title;box.appendChild(h);
const grid=document.createElement("div");grid.className="cx-grid";
for(const k of keys){
const def=dict[k];
const d=document.createElement("div");
d.className="cx-item";
const img=def.icon?ICONS[def.icon]:SPR[def.spr];
d.appendChild(iconCanvas(img,28));
d.insertAdjacentHTML("beforeend",`<div class="nm">${def.name}</div>`);
d.onclick=()=>{SFX.pick();startDemoSim(kind,k);};
grid.appendChild(d);
}
box.appendChild(grid);
};
mk("主武器",MAINS,WEAPONS,"m");
mk("副武器",SUB_KEYS,SUBS,"s");
mk("僚机",DRONE_KEYS,DRONES,"d");
mk("被动强化",PAS_KEYS,PASSIVES,"p");
$("codex-n").textContent=total; // 图鉴全量直接展示,无需收集
}
document.addEventListener("visibilitychange",()=>{if(document.hidden&&UI==="play")togglePause();});
function bank(){
if(!G||G.banked)return;G.banked=true;
SAVE.coins+=G.coins;
if(G.mode!=="rush"){const bk=(G.mode==="endless"?"e":"s")+G.diff;
SAVE.best[bk]=Math.max(SAVE.best[bk]||0,G.score);}
persist();
}
/* ---- Boss 挑战模式 ---- */
function buildRush(){
SAVE.rush=SAVE.rush||{};
const ra=$("rush-anim"); // 老虎机开关(持久化)
if(ra){ra.checked=!!SAVE.rushAnim;ra.onchange=()=>{SAVE.rushAnim=ra.checked?1:0;persist();};}
const el=$("rush-pick");el.innerHTML="";
BOSSES.forEach((b,i)=>{
const best=SAVE.rush[i];
const d=document.createElement("div");d.className="pcard";
d.innerHTML=`<div class="nm" style="margin-top:2px;color:${b.col}">${b.name}${best?" ✓":""}</div>
<div class="ds">${b.sub}<br>${best?("最快 "+Math.floor(best/60)+":"+("0"+best%60).slice(-2)):"未击破"}</div>`;
d.onclick=()=>startRush(i);
el.appendChild(d);
});
}
function rollRushBuild(){ // 随机武装3主武Lv3 + 3副武Lv3 + 2僚机Lv2
const sh=a=>{const x=a.slice();for(let i=x.length-1;i>0;i--){const j=rndi(0,i),t2=x[i];x[i]=x[j];x[j]=t2;}return x;};
return {m:sh(Object.keys(WEAPONS)).slice(0,3),s:sh(Object.keys(SUBS)).slice(0,3),d:sh(Object.keys(DRONES)).slice(0,2)};
}
function startRush(i){
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
const build=rollRushBuild();
if(SAVE.rushAnim)playSlot(build,()=>startRushGo(i,build)); // 老虎机动画抽取
else startRushGo(i,build); // 直接随机
}
function startRushGo(i,build){
newRun("rush","falcon",0);
G.weapons={};build.m.forEach(k=>G.weapons[k]=3);G.loadout=build.m.slice();
G.subs={};build.s.forEach(k=>G.subs[k]=3);G.subLoadout=build.s.slice();
G.drones={};build.d.forEach(k=>G.drones[k]=2);G.droneLoadout=build.d.slice();
G.xpNeed=1e9; // 挑战模式无升级打断
G.rushIdx=i;G.rushT0=0;
UI="play";show("");acc=0;
MUS.start(true);
banner("BOSS 挑战 — "+BOSSES[i].name,"#ff5566",130);
G.rushT0=G.time;
startBoss();
}
/* ---- 老虎机抽奖动画 ---- */
let slotTimers=[];
function playSlot(build,cb){
UI="slot";show("o-slot");
const row=$("slot-row");row.innerHTML="";
$("slot-hint").textContent=" ";
const items=[
...build.m.map(k=>({cat:"主武器",pool:WEAPONS,key:k})),
...build.s.map(k=>({cat:"副武器",pool:SUBS,key:k})),
...build.d.map(k=>({cat:"僚机",pool:DRONES,key:k}))];
let done=0;
items.forEach((it,idx)=>{
const d=document.createElement("div");d.className="slotbox";
d.innerHTML=`<div class="sc">${it.cat}</div><div class="sn">???</div>`;
row.appendChild(d);
const sn=d.querySelector(".sn"),names=Object.values(it.pool).map(x=>x.name);
const iv=setInterval(()=>{sn.textContent=names[rndi(0,names.length-1)];},60);
slotTimers.push(iv);
setTimeout(()=>{clearInterval(iv);
sn.textContent=it.pool[it.key].name;sn.classList.add("hit");
SFX.graze();done++;
if(done>=items.length){$("slot-hint").textContent="— 出击 —";setTimeout(cb,700);}
},700+idx*280);
});
}
function endRun(win){
if(UI==="over")return;
bank();MUS.stop();
UI="over";
$("over-title").textContent=win?"★ 任务完成 ★":"任务失败";
$("over-title").style.color=win?"#ffcf5c":"#ff5566";
const mins=Math.floor(G.time/3600),secs=Math.floor(G.time/60)%60;
$("over-stats").innerHTML=
`<span>最终得分</span><b>${G.score}</b>
<span>${G.mode==="endless"?"抵达波次":G.mode==="rush"?"挑战目标":"抵达关卡"}</span><b>${G.mode==="endless"?G.wave:G.mode==="rush"?BOSSES[G.rushIdx].name:G.stage+"-"+G.wave}</b>
<span>击坠数</span><b>${G.kills}</b>
<span>强化等级</span><b>Lv.${G.level}</b>
<span>存活时间</span><b>${mins}${secs}</b>
<span>持有军械</span><b>${Object.keys(G.weapons).length}主/${Object.keys(G.subs).length}/${Object.keys(G.drones).length}</b>`;
$("over-coins").textContent=G.coins;
setTimeout(()=>show("o-over"),win?400:900);
}
/* ================= 三选一卡片 ================= */
function offerCards(bossRare){
if(!G||G.p.dead>0&&!bossRare)return;
G.bossCard=!!bossRare;
const opts=rollCards(bossRare);
G.cardOpts=opts;
const box=$("cards");box.innerHTML="";
opts.forEach((c,i)=>{
let nm,ds,rare,img;
if(c.kind==="wlvl"){const w=WEAPONS[c.key],lv=G.weapons[c.key];nm=w.name;rare=w.rare;ds=`主武器强化 Lv.${lv} → Lv.${lv+1}`;img=w.icon?ICONS[w.icon]:SPR[w.spr];}
else if(c.kind==="weapon"){const w=WEAPONS[c.key];nm="主武器·"+w.name;rare=w.rare;ds=w.ds;img=w.icon?ICONS[w.icon]:SPR[w.spr];}
else if(c.kind==="slvl"){const u=SUBS[c.key],lv=G.subs[c.key];nm=u.name;rare=u.rare;ds=`副武器强化 Lv.${lv} → Lv.${lv+1}`;img=u.icon?ICONS[u.icon]:SPR[u.spr];}
else if(c.kind==="sub"){const u=SUBS[c.key];nm="副武器·"+u.name;rare=u.rare;ds=typeof u.ds==="function"?u.ds(0):u.ds;img=u.icon?ICONS[u.icon]:SPR[u.spr];}
else if(c.kind==="dlvl"){const u=DRONES[c.key],lv=G.drones[c.key];nm=u.name;rare=u.rare;ds=`僚机强化 Lv.${lv} → Lv.${lv+1}`;img=SPR[u.spr];}
else if(c.kind==="drone"){const u=DRONES[c.key];nm="僚机·"+u.name;rare=u.rare;ds=typeof u.ds==="function"?u.ds(0):u.ds;img=SPR[u.spr];}
else if(c.kind==="upg"){const u=PASSIVES[c.key];nm=u.name;rare=u.rare;ds=u.ds(G.u[c.key]);img=u.icon?ICONS[u.icon]:SPR[u.spr];}
else{rare=0;
if(c.key==="heal"){nm="紧急修复";ds="生命 +1";img=SPR.heart;}
else if(c.key==="coins"){nm="星币补给";ds="星币 +50";img=SPR.coin;}
else{nm="超频充能";ds="超频立即充满";img=SPR.odc;}}
const d=document.createElement("div");
d.className="ucard r"+rare;
const cv2=document.createElement("canvas");cv2.width=40;cv2.height=40;
const g=cv2.getContext("2d");g.imageSmoothingEnabled=false;
const sc=Math.min(40/img.width,40/img.height);
g.drawImage(img,(40-img.width*sc)/2,(40-img.height*sc)/2,img.width*sc,img.height*sc);
d.appendChild(cv2);
d.insertAdjacentHTML("beforeend",
`<div class="key">${i+1}</div><div class="nm">${nm}</div>
<div class="rtag">${["普通","稀有","史诗"][rare]}</div><div class="ds">${ds}</div>`);
d.onclick=()=>pickCard(i);
box.appendChild(d);
});
UI="cards";show("o-cards");SFX.level();
}
function pickCard(i){
if(UI!=="cards"||!G.cardOpts||!G.cardOpts[i])return;
const c=G.cardOpts[i];G.cardOpts=null;
if(c.kind==="wlvl")G.weapons[c.key]++;
else if(c.kind==="weapon"){G.weapons[c.key]=1;if(G.loadout.length<5)G.loadout.push(c.key);banner("获得 "+WEAPONS[c.key].name,"#b98cff",80);}
else if(c.kind==="slvl")G.subs[c.key]++;
else if(c.kind==="sub"){G.subs[c.key]=1;if(G.subLoadout.length<5)G.subLoadout.push(c.key);banner("获得 "+SUBS[c.key].name,"#b98cff",80);}
else if(c.kind==="dlvl")G.drones[c.key]++;
else if(c.kind==="drone"){G.drones[c.key]=1;if(G.droneLoadout.length<3)G.droneLoadout.push(c.key);banner("获得 "+DRONES[c.key].name,"#b98cff",80);}
else if(c.kind==="upg"){
G.u[c.key]++;
if(c.key==="nano"){G.maxHp+=25;G.hp=Math.min(G.maxHp,G.hp+40);}
if(c.key==="shield")G.shield=Math.min(shieldCap(),G.shield+1);
}else{
if(c.key==="heal")G.lives=Math.min(G.stat.maxLives,G.lives+1);
if(c.key==="coins")addCoins(50);
if(c.key==="od")G.od=100;
}
if(c.key&&c.kind!=="fill"){SAVE.codex[c.key]=true;persist();} // 图鉴收录
SFX.pick();
if(G.bossCard){G.bossCard=false;UI="play";show("");}
else{G.cardQ=(G.cardQ||1)-1;
if(G.cardQ>0)offerCards(false);
else{UI="play";show("");}}
}
/* ================= =================
复用真实战斗引擎只装备选定军械杂兵+射手+精英持续来袭 */
function startDemoSim(kind,key){
const def=kind==="m"?WEAPONS[key]:kind==="s"?SUBS[key]:kind==="p"?PASSIVES[key]:DRONES[key];
newRun("stage","falcon",0);
G.p.inv=0; // 图鉴演示:取消出生保护闪烁
const lv=3;
G.demoSim={kind,key};
if(kind==="m"){G.weapons={};G.weapons[key]=lv;G.loadout=[key];}
else if(kind==="s"){G.weapons={vulcan:1};G.loadout=["vulcan"];G.subs={};G.subs[key]=lv;G.subLoadout=[key];}
else if(kind==="d"){G.weapons={};G.loadout=[];G.drones={};G.drones[key]=lv;G.droneLoadout=[key];}
else{G.weapons={vulcan:2};G.loadout=["vulcan"];G.u[key]=3;
if(key==="nano"){G.maxHp+=75;G.hp=G.maxHp;}
if(key==="shield")G.shield=shieldCap();}
$("demo-name").textContent=def.name;
const ds=typeof def.ds==="function"?def.ds(lv):def.ds;
$("demo-ds").textContent=(kind==="m"?"主武器 · ":kind==="s"?"副武器 · ":kind==="d"?"僚机 · ":"被动 · ")+ds;
UI="demoSim";show("o-demo");acc=0;
banner(def.name,"#41e6ff",90);
}
function exitDemoSim(){G=null;UI="codex";buildCodex();show("o-codex");}
function demoSpawn(){
const D=G.demoSim;
D.t=(D.t||0)-1;
if(D.t>0||G.enemies.length>=10)return;
D.t=Math.max(30,52-Math.floor(G.time/900)); // 渐进增密
const r=Math.random();
if(r<0.5)spawnEnemy(pick(["scout","scout","weaver","kami"]),rnd(20,W-20),-12,false); // 杂兵为主
else if(r<0.84)spawnEnemy(pick(["gunship","splitter","sniper","mine"]),rnd(20,W-20),-12,false); // 射手少数
else spawnEnemy(pick(["gunship","weaver","splitter"]),rnd(20,W-20),-12,true); // 精英
}
bestLine();