starraid/star-raid/js/enemies.js
JOJO ab403f4cc7 后端化:账号系统+排行榜+图鉴收集同步+飞行员大厅(零依赖Node+SQLite)
- server/:node:http+node:sqlite 零依赖后端(users/sessions/user_stats/codex_unlocks/runs 五表)
- 安全:scrypt密码哈希/服务端session/全prepared statement/输入白名单/限流/CSP等安全头/静态白名单防穿越
- 前端:o-auth登陆注册 + o-home飞行员大厅(排行榜Top100三排序+图鉴展柜背景:随机3主3副2僚机+刷怪巡游)
- 图鉴改为收集点亮制(未收集=剪影+???),服务端codex_unlocks与本地并集同步
- 修复:回标题/大厅后背景卡在上场残影——render按battleUI门控+离场G=null;空闲=星空
- 结算上报:endRun/rush胜利两处submitRun,Boss击杀计数入排行榜
- playwright全流程验证通过(注册/登陆/游戏/结算/图鉴/安全注入/限流);游戏视觉未验收(用户亲自)
2026-08-06 04:20:25 +08:00

571 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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},
gaunt:{hp:26,spd:1.45,score:150,r:6,gems:1,coin:.2,beh:"sidle",fire:null,cost:1.5,min:99}, // 泰伦刀虫(母舰专属)
sporeT:{hp:14,spd:.5,score:120,r:6,gems:1,coin:.15,beh:"drift",fire:null,death:"ring",cost:1,min:99}, // 孢子囊:慢飘,死亡酸环
ripper:{hp:6,spd:2.2,score:50,r:3.5,gems:1,coin:.1,beh:"dive",fire:null,cost:.4,min:99}, // 撕裂虫:小快速
boomer:{hp:8,spd:2.0,score:130,r:5,gems:1,coin:.15,beh:"boom",fire:null,cost:1,min:99}, // 爆虫:冲锋自爆
spitter:{hp:20,spd:.8,score:170,r:6,gems:2,coin:.25,beh:"hover",fire:{t:"acid3",cd:150,spd:1.7},cost:1.6,min:99}, // 吐酸虫:远程散弹
spinegun:{hp:22,spd:.75,score:180,r:6,gems:2,coin:.25,beh:"hover",fire:{t:"spike",cd:170,spd:3.2},cost:1.7,min:99} // 棘枪虫:骨钉直线
};
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);
}
/* ---- 泰伦触手扫击角度制扇形动作demo验证版移植渲染与判定同源 ---- */
function tentPose(tn,side,tB){ // 返回{ang(0=正下,side向为正),stretch,glow}
const idle=Math.sin(tB*0.06)*0.07*side;
if(tn.warn>0){const f=1-tn.warn/50,e=f*f; // 前摇:向扫击反方向后拉蓄力
return{ang:idle*(1-f)+side*0.73*e+Math.sin(tB*0.9)*0.012*e,stretch:1+e*0.15,glow:f};}
if(tn.act>17){const f=(45-tn.act)/28,e=f*f*f; // 扫击加速挥出右下→左下60°扇形伸长+50%
return{ang:side*(0.73-e*1.31),stretch:1.15+e*1.25,glow:1};}
if(tn.act>0){return{ang:side*(-0.58)+Math.sin(tB*1.2)*0.05*(tn.act/17),stretch:2.4,glow:1};} // 顿挫
const f=1-tn.rec/40,e=1-(1-f)*(1-f); // 收回
return{ang:side*(-0.58)+(idle-side*(-0.58))*e,stretch:2.4-1.4*e,glow:1-e};}
function tentSegs(b,side,opF){ // 触手7节局部坐标相对Boss中心逐节延迟形成鞭弧
const pts=[];opF=opF===undefined?1:opF;
for(let i=0;i<7;i++){
let ang=Math.sin((b.t-i*2.2)*0.06)*0.07*side,stretch=1,glow=0;
if(b.tent&&b.tent.side===side){const s=tentPose(b.tent,side,b.t-i*2.2);ang=s.ang;stretch=s.stretch;glow=s.glow;} // 仅扫击侧动作,另一侧保持待机摆动
const f=i/6,L=40*stretch*f*opF;
pts.push({x:side*9+Math.sin(ang)*L,y:24+Math.cos(ang)*L,glow});
}
return pts;}
/* ---- 敌机行为 ---- */
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;
case "sidle": if(e.sidleT===undefined)e.sidleT=30; // 泰伦:从母舰体内向两侧排出,随后扑向玩家
if(e.sidleT>0){e.sidleT--;e.x+=e.vx*spM;e.y+=e.vy*spM;}
else{const ap3=aimPt(),a3=Math.atan2(ap3.y-e.y,ap3.x-e.x);
e.vx=lerp(e.vx,Math.cos(a3)*e.spd,0.06);e.vy=lerp(e.vy,Math.sin(a3)*e.spd,0.06);
e.x+=e.vx*spM;e.y+=e.vy*spM;} break;
case "boom": { // 泰伦爆虫:追踪冲锋,近身自爆(酸液飞溅)
const ap4=aimPt(),a4=Math.atan2(ap4.y-e.y,ap4.x-e.x);
e.vx=lerp(e.vx,Math.cos(a4)*e.spd*1.35,0.08);e.vy=lerp(e.vy,Math.sin(a4)*e.spd*1.35,0.08);
e.x+=e.vx*spM;e.y+=e.vy*spM;
if(G.time%6===0)G.parts.push({x:e.x,y:e.y,vx:0,vy:-0.2,life:8,col:"#a8ff5c",sz:1}); // 酸液拖尾
if(dist2(e.x,e.y,G.p.x,G.p.y)<17*17){e.dead=true;
for(let i2=0;i2<8;i2++)G.parts.push({x:e.x,y:e.y,vx:Math.cos(i2*TAU/8)*rnd(0.8,1.7),vy:Math.sin(i2*TAU/8)*rnd(0.8,1.7),life:16,col:"#a8ff5c",sz:2});
SFX.hit();
if(dist2(e.x,e.y,G.p.x,G.p.y)<26*26&&G.p.dead<=0)hitPlayer(20);}
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);}
else if(f.t==="acid3")for(let i=-1;i<=1;i++)ebul(e.x,e.y+4,a0+i*0.3,f.spd,"orbG",14); // 泰伦:酸液散弹
else if(f.t==="spike")ebul(e.x,e.y+4,a0,f.spd,"spikeB",16); // 泰伦:骨钉直线
}
/* ================= 突变词缀 ================= */
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","spikeSnipe","sporeArc","tentBeam","mawSnipe","wingRain"],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:5000,col:"#43e8ff",r:26,rr:[100,100],ryy:[30,30],main:"piano",minors:["topRain","pedal"],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:[58,26],ryy:[34,40],wide:1,main:"usurper",minors:["deckVolley","pillar","aimSpread"],phases:2},
{name:"暗黑魔碟",sub:"DARK SAUCER",hp:4500,col:"#8a5cff",r:24,rr:[30,32,34],main:"saucer",minors:["ringBig","coneFan","crossEye","dartSpiral"],phases:3},
{name:"黑石要塞",sub:"BLACKSTONE",hp:3400,col:"#5cff9d",r:30,rr:[34,37],ryy:[26,28],wide:1,main:"blackstone",minors:["deckGuns","crackRift","starLine"],phases:2},
{name:"泰伦虫族母舰",sub:"TYRANID HIVESHIP",hp:3200,col:"#7dff8a",r:26,rr:[26,29,32],ryy:[36,38,40],main:"hivespawn",minors:["sporeBloom","tentSweep","acidGas"],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,slot:i,flashT:0});
}
function updateFunnels(){
const b=G.boss,n=G.funnels.length;
if(b&&!b.dead&&n){
if(b.formT===undefined)b.formT=rndi(340,520);
if(b.form){b.form.t++;if(b.form.t>170)b.form=null;} // 阵型演出40帧入位→4轮齐射→归位
else if((b.laserT||0)<=0&&--b.formT<=0){ // 巨宽激光期间不排阵
b.form={pat:rndi(0,2),t:0};
G.funnels.forEach((f,i)=>{f.slot=i;});
b.formT=rndi(480,660);
}
}
for(const f of G.funnels){
f.t++;if(f.flashT>0)f.flashT--;
if(b&&!b.dead){
f.sa+=0.014;
const ringX=b.x+Math.cos(f.sa)*46,ringY=b.y+16+Math.sin(f.sa)*20;
if(b.form){ // 脱离环绕摆阵型:横队垂直向下 / 两翼外张扇形 / 集火瞄准
const fm2=b.form,i=f.slot||0,nn=Math.max(1,n-1);
let tx,ty,fa;
if(fm2.pat===0){tx=b.x+(i-nn/2)*26;ty=b.y-30;fa=Math.PI/2;}
else if(fm2.pat===1){const sd=i%2?1:-1;tx=b.x+sd*(36+Math.floor(i/2)*16);ty=b.y+6+Math.floor(i/2)*8;fa=Math.PI/2+sd*0.55;}
else{tx=b.x+(i-nn/2)*18;ty=b.y+26;fa=null;}
if(fm2.t<150){f.x+=(tx-f.x)*0.11;f.y+=(ty-f.y)*0.11;}
else{f.x+=(ringX-f.x)*0.09;f.y+=(ringY-f.y)*0.09;} // 归位回环绕
if((b.laserT||0)>0)continue;
if(fm2.t>=40&&fm2.t<140&&fm2.t%26===0){
if(fm2.pat===2){const ap4=aimPt();bossBeam(f.x,f.y,ap4.x,ap4.y,2.5,"#7ce9ff",18,14,10);} // 集火阵型改细光束
else{const a2=fa===null?Math.atan2(aimPt().y-f.y,aimPt().x-f.x):fa;
ebul(f.x,f.y,a2,4.6,"dartS",14);}f.flashT=5;}
}else{
f.x+=(ringX-f.x)*0.09;f.y+=(ringY-f.y)*0.09;
if((b.laserT||0)>0)continue; // 巨宽核心激光期间停火(仍环绕)
f.fireT--;
if(f.fireT<=0){const a=Math.atan2(aimPt().y-f.y,aimPt().x-f.x);
ebul(f.x,f.y,a,4.8,"dartS",15);f.fireT=rndi(70,120);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%10));
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"?3:1); // Boss挑战模式血量倍数调平衡改这里
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,mt:0,landed:false};
if(b.main==="piano")G.boss.mainT=24; // 激光钢琴:缩短开局空窗
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.landed){if(b.y<b.ty){b.y+=1.2;b.x=W/2;return;}b.landed=true;} // 入场只跑一次:到位后锁定,防正弦浮沉跌回触发重置
// b.mt移动时钟只在常规移动时推进——冲撞/变身期间冻结,恢复时位置连续不瞬移
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;b.form=null;if(np===1){spawnFunnels(b,4);b.plT=Math.round(300/b.fireM);}} // 夺权者X二阶段增至4架僚机
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(105/b.fireM);}} // 冲撞间隔+50%
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.mt*0.034)*62;b.y=b.ty+Math.sin(b.mt*0.021)*18;} // 暗黑突击:小体型高机动(空闲大幅提速)
else if(b.idx===6){const usp=b.phase>=1?0.028:0.008,amp=b.phase>=1?70:44;
b.x=clamp(b.x+amp*usp*Math.cos(b.mt*usp),30,W-30); // 速度积分定位:换阶段参数突变也不瞬移
b.yA=(b.yA||8)+((b.phase>=1?12:8)-(b.yA||8))*0.05;b.y=b.ty+Math.sin(b.mt*0.011)*b.yA;} // 夺权者XP1巨大笨重→P2紧凑灵活
else{b.x=W/2+Math.sin(b.mt*0.013)*(def.wide?44:70);b.y=b.ty+Math.sin(b.mt*0.011)*(def.wide?8:12);} // 通用游走提速+上下浮动(钢琴站桩除外)
b.mt++;
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--;if(b.eyeGlow>0)b.eyeGlow--;if(b.rainT>0)b.rainT--;if(b.turrT>0)b.turrT--;if(b.pedT>0)b.pedT--; // 帧级技能计时器
if(b.idx===1&&b.rainT>0&&b.rainT<94&&b.rainT%3===0){ // 原种红雨前摇16帧双翼高频抖动后翼展范围内红光点随机洒落
const span=46*(0.22+0.78*(b.phase>=1?1:0.62));
ebul(b.x+rnd(-span,span),b.y+rnd(-10,2),Math.PI/2+rnd(-0.08,0.08),rnd(2.2,3.4),"orbR",12);}
if(b.idx===6&&b.deckT>0){b.deckT--; // 夺权者X甲板齐射——炮口逐个点亮前摇26帧后6发垂直弹雨
if(b.deckT===0)for(const sd of[-1,1])for(const gx of[50,56,62])ebul(b.x+sd*gx,b.y+8,Math.PI/2,2.6,"dartB",14);}
if(b.idx===8){ // 黑石要塞亚空间主炮序列纹路充能52帧→巨柱扫动80帧
if(!b.plT||b.plT<=0)b.plT=Math.round((b.phase>=1?380:460)/fm);
else{
if(b.plT===142){b.laserT=142;b.warpChg=0.001;SFX.warn();}
if(b.plT<=142&&b.plT>90)b.warpChg=Math.min(1,(142-b.plT)/52);
if(b.plT===90){b.warpChg=0;bossBeam(b.x,b.y+6,b.x,H+8,b.phase>=1?24:20,"#5cff9d",10,80,30,{b,ox:0,oy:6,followX2:true,sweep:1,amp:b.phase>=1?70:50});}
}
}
if(b.idx===9){ // 泰伦触手扫击9秒一次左右随机 + 玩家靠近立即触发对应侧
if(b.phase>=1&&!b.tent){const p=G.p;
if(p.dead<=0&&Math.abs(p.x-b.x)<52&&p.y>b.y&&p.y<b.y+95){
b.tent={side:p.x<b.x?-1:1,warn:50,act:0,rec:0,trail:null};SFX.warn();}}
if(b.tent){const tn=b.tent; // 前摇→横扫→顿挫→收回;触手即碰撞体
if(tn.warn>0){tn.warn--;if(tn.warn<=0){tn.act=45;tn.hitDone=false;}}
else if(tn.act>0){tn.act--;
if(!tn.hitDone){ // 扫击+顿挫全期:玩家与触手各节线段做点-线段碰撞
const p=G.p;
if(p.dead<=0){const pts=tentSegs(b,tn.side),lx=p.x-b.x,ly=p.y-b.y;
for(let i=1;i<7;i++){const f=i/6,sr=3.2*(1-f*0.45)+G.stat.hitR+1;
const ax=pts[i-1].x,ay=pts[i-1].y,dx=pts[i].x-ax,dy=pts[i].y-ay,L2=dx*dx+dy*dy||1;
const tt=clamp(((lx-ax)*dx+(ly-ay)*dy)/L2,0,1),cx=ax+dx*tt,cy=ay+dy*tt;
if(dist2(lx,ly,cx,cy)<sr*sr){tn.hitDone=true;hitPlayer(28);break;}}}
}
if(tn.act<=0)tn.rec=40;
}
else if(tn.rec>0){tn.rec--;if(tn.rec<=0)b.tent=null;}
}
}
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.idx===3&&b.minorT>0)b.minorT--; // 暗黑突击:空闲攻击频率翻倍
if(b.minorT<=0&&!lck&&!(b.rainT>0))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": { // 太空原种:花弹从腹末生殖腔喷出(发射点=生殖腔摆动与drawProgenitor同源公式P1三波起手偏左 / P2生殖腔血光柱+翼尖花弹
if(b.rainT>0){b.mainT=24;break;} // 红雨期间花弹全停(不然躲不了)
if(ph===0){const swx=Math.sin(b.t*0.055+2.8)*4.8;
for(let k=0;k<3;k++)for(let i=-2;i<=2;i++){
const bb=ebul(b.x+swx,b.y+34,a0+i*0.16+(k-1)*0.09+[0.2,0.1,0][k],1.5+k*0.2,"flowr",14);
bb.grow=0.007;bb.maxS=2.8;bb.scale=0.9;}
b.mainT=Math.round(84/fm);
}else{bossBeam(b.x,b.y+40,b.x+clamp(ap2.x-b.x,-50,50)*0.5,H+10,9,"#ff5c8a",30,44,22,{b,ox:0,oy:40});
for(const sd of[-1,1])for(let i=0;i<3;i++){
const bb=ebul(b.x+sd*50,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.ax=b.x;b.ay=b.y;b.mainT=9999;break;
case "piano": { // 激光钢琴21键四种模式轮换——顺扫(逼走位)/反扫/乱序/齐射
if(!b.keys)b.keys={n:21,seq:[],i:0,t:2,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)*5+kh*10; // 与drawPiano键位一致整数键距10
bossBeam(b.x+kx,b.y+18,b.x+kx,H+8,4,"#43e8ff",7,22,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){ // 布下一局轮换模式5种
ks.mode=(ks.mode+1)%5;
if(ks.mode===0){ks.seq=[];for(let i=0;i<n-3;i++)ks.seq.push(i);} // 顺扫止于18号最右3键永远安全
else if(ks.mode===1){ks.seq=[];for(let i=n-1;i>=3;i--)ks.seq.push(i);} // 反扫止于3号最左3键永远安全
else if(ks.mode===2){ks.seq=shuffled(n);}
else if(ks.mode===3){ks.seq=[];for(let i=0;i<6+ph*2;i++)ks.seq.push(-1);} // -1=齐射标记
else{ks.seq=[];for(let i=0;i<n-3;i++)ks.seq.push(i);for(let i=n-4;i>=0;i--)ks.seq.push(i);} // 滑奏:顺扫到尾立刻反扫回来
ks.i=0;ks.t=Math.round((ph?14:22)/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=9;ks.hotT0=9;ks.t=Math.round((ph?21:30)/fm);} // 齐射慢50%
else if(ks.mode<=1||ks.mode===4){ks.hot=cur;ks.hotT=1;ks.hotT0=1;ks.t=Math.round((ph?2:3)/fm);} // 顺/反扫/滑奏前摇快50%
else{if(ph>=1){ks.hotList=[cur,ks.i<ks.seq.length?ks.seq[ks.i++]:cur];ks.hot=-1;}else{ks.hot=cur;}ks.hotT=3;ks.hotT0=3;ks.t=Math.round((ph?3:5)/fm);} // 乱序P2一次点2键和音
}
}
b.mainT=4;break;}
case "blackstone": { // 黑石要塞五重旋转花瓣弹幕往复旋摆签名亚空间主炮见updateBoss帧级序列
const base=Math.sin(b.t*0.008)*1.1;
for(let k=0;k<5;k++)ebul(b.x,b.y,k*TAU/5+base,1.55,"orbG",13);
if(ph>=1)for(let k=0;k<5;k++)ebul(b.x,b.y,k*TAU/5-base+TAU/10,1.55,"orbG",13); // P2双层反向编织
b.mainT=Math.round((ph>=1?38:46)/fm);break;}
case "hivespawn": { // 泰伦母舰从体内向两侧排出虫群无弹幕P2虫群血量变厚P3嘴吐骨钉扇
b.wvC=(b.wvC||0)+1;
const big=b.wvC%4===0,n1=big?7+ph*2:3+ph;
if(b.wvC%2===0)for(let k=-2;k<=2;k++)ebul(b.x,b.y+26,Math.PI/2+k*0.22,0.95,"orbG",10); // 间歇喷孢子(慢速散弹,威胁不大)
for(let i=0;i<n1;i++){const sd=i%2?1:-1,tp=pick(["gaunt","gaunt","ripper","sporeT"]);
spawnEnemy(tp,b.x+sd*30,b.y+rnd(-6,14),false);
const e=G.enemies[G.enemies.length-1];
if(tp==="sporeT"){e.x0=e.x;} // 孢子囊缓飘
else if(tp==="boomer"){e.vx=sd*rnd(0.6,1);e.vy=rnd(0.2,0.4);} // 爆虫:侧弹后直接冲锋
else if(tp!=="spitter"&&tp!=="spinegun"){e.vx=sd*rnd(0.7,1.1);e.vy=rnd(0.1,0.35);e.sidleT=rndi(26,42);e.beh="sidle";} // 近战虫侧排后扑击
if(ph>=1){e.hp*=1.5;e.maxhp*=1.5;} // P2起虫群血量变厚
spawnPart(b.x+sd*24,b.y+8,"#a8ff5c",2);}
b.mainT=Math.round((big?210:125)/fm);
if(ph>=2){for(let k=-2;k<=2;k++)ebul(b.x,b.y+30,Math.PI/2+k*0.14,2.6,"spikeB",14); // 嘴吐骨钉
b.mainT=Math.round((big?230:150)/fm);}
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,2);}
for(const sd of[-1,1]){const px2=b.x+sd*50,py2=b.y+8,a6=Math.atan2(ap2.y-py2,ap2.x-px2);
ebul(px2,py2,a6,4.4,"dartS",15);} // 发射点=最下甲板炮口与drawUsurper同源
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);}
if(ph>=1){const n3=6+ph*3; // P2起内层反向橙弹环双层反向编织致敬参考图紫+橙)
for(let i=0;i<n3;i++){const ea2=-(b.rotA||0)+i/n3*TAU+TAU/(n3*2);
ebul(b.x+Math.cos(ea2)*22,b.y+Math.sin(ea2)*22,ea2,1.5+0.15*ph,"orbO",13);}}
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 "spikeSnipe": for(let k=0;k<3;k++)ebul(b.x,b.y-16,a0+(k-1)*0.12,3.4,"spikeB",15);b.minorT=Math.round(170/fm);break; // 原种:大颚骨钉三连狙(白色骨钉高速直线,非花弹)
case "wingRain": b.rainT=110;b.minorT=Math.round(300/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": { // 钢琴6座穹顶小炮塔轮流开火炮位与drawPiano同源P2随环带滑开28
const off2=b.phase>=1?28:0,ti=rndi(0,5),tx=b.x+(-75+ti*30)+(ti<3?-1:1)*off2;
b.turrI=ti;b.turrT=8;
for(let i=0;i<3;i++)ebul(tx+rnd(-3,3),b.y-2,Math.PI/2+rnd(-0.12,0.12),2.0,"orbG",13);
b.minorT=Math.round(150/fm);break;}
case "pedal": { // 钢琴踏板重音——左右大炮塔慢速大弹左紫右蓝P2随外环滑开16炮位与drawPiano同源
const off0=b.phase>=1?16:0;
for(const sd of[-1,1]){const bb=ebul(b.x+sd*(112+off0),b.y+10,Math.PI/2+sd*0.08,1.1,sd<0?"orbP":"dartB",18);
bb.scale=2.4;bb.r=6;}
b.pedT=8;b.minorT=Math.round(280/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 "deckVolley": if(b.phase>=1){b.minorT=80;break;}b.deckT=26;b.minorT=Math.round(320/fm);break; // 夺权者X甲板齐射P1限定——最下甲板炮口逐个点亮前摇→6发垂直弹雨
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,"orbO",15);b.minorT=Math.round(230/fm);break; // 橙环(与签名紫锥拉开)
case "sporeArc": for(let i=0;i<7;i++)ebul(b.x,b.y+14,Math.PI/2+(i-3)*0.26,1.2,"orbG",13);b.minorT=Math.round(210/fm);break; // 原种:慢速孢子幕(绿,与花弹差异化)
case "tentBeam": { // 原种双触手鞭击光束——从头侧管囊伸出锚定相位从垂直向下逐帧向外鞭展sweep连续判定扫到哪判定到哪
const ph0=-G.time*0.014;
for(const sd of[-1,1])bossBeam(b.x+sd*15,b.y-16,b.x+sd*15,H+8,3,"#ff5c8a",26,74,16,{b,ox:sd*15,oy:-16,sweep:1,amp:sd*42,swPh:ph0});
b.minorT=Math.round(280/fm);break;}
case "mawSnipe": { // 原种:颚部四联狙击——双大颚+头侧管囊齐射快速红点弹发射点与drawProgenitor同源击发后大颚+感器缝闪白余辉)
const MP=[[-5,-22],[5,-22],[-15,-16],[15,-16]];
for(let e=0;e<4;e++){const ex2=b.x+MP[e][0],ey2=b.y+MP[e][1];
const ea=Math.atan2(ap2.y-ey2,ap2.x-ex2);
ebul(ex2,ey2,ea+(e-1.5)*0.04,3.6,"orbR",14);}
b.eyeGlow=20;b.minorT=Math.round(200/fm);break;}
case "coneFan": for(let i=-2;i<=2;i++)ebul(b.x,b.y+8,a0+i*0.15,2.9,"dartP",14);b.minorT=Math.round(150/fm);break; // 魔碟:瞄准锥扇
case "crossEye": for(const sd of[-1,1])bossBeam(b.x+sd*9,b.y-3,ap2.x-sd*26,H+8,3.5,"#ff3355",22,24,18,{b,ox:sd*9,oy:-3});b.minorT=Math.round(230/fm);break; // 魔碟交叉赤瞳斜线X
case "dartSpiral": for(let arm=0;arm<3;arm++)ebul(b.x,b.y,b.t*0.11+arm*TAU/3,2.0,"dartP",13);b.minorT=Math.round(46/fm);break; // 魔碟:三臂锥弹螺旋
case "deckGuns": { // 黑石:甲板炮群——五臂侧舷齐射交叉火力网
const rt=b.t*0.0032;
for(let k=0;k<5;k++){const a=rt+k*TAU/5,gx=b.x+Math.sin(a)*22,gy=b.y-Math.cos(a)*22;
for(const sd of[-1,1])ebul(gx+Math.cos(a)*sd*8,gy+Math.sin(a)*sd*8,Math.PI/2+Math.sin(a)*0.7+sd*0.3,1.5,"orbG",12);}
b.minorT=Math.round(225/fm);break;}
case "crackRift": // 黑石:亚空间裂隙——现身预瞄,延迟变长钉射出
for(let i=0;i<3+b.phase*2;i++){const rb=ebul(clamp(ap2.x+rnd(-70,70),16,W-16),clamp(ap2.y+rnd(-60,30),H*0.32,H-30),0,0,"orbG",14);
rb.rift=rndi(26,44);rb.scale=1.5;rb.r=4;}
b.minorT=Math.round(255/fm);break;
case "starLine": { // 黑石:星芒脉冲——十向高速弹线,交替偏相半格
b.stC=(b.stC||0)+1;const off=(b.stC%2)*(TAU/20);
for(let k=0;k<10;k++)ebul(b.x,b.y,k*TAU/10+off,3.1,"orbG",13);
b.minorT=Math.round(215/fm);break;}
case "sporeBloom": // 泰伦:孢子囊投放(慢飘碍走位,死亡酸液环爆)
for(let i=0;i<2+b.phase;i++)spawnEnemy("sporeT",b.x+rnd(-20,20),b.y+rnd(2,16),false);
b.minorT=Math.round(280/fm);break;
case "tentSweep": // 泰伦触手扫击P2+随机一侧触手前摇后拉蓄力→60°扇形挥鞭
if(b.phase<1){b.minorT=80;break;}
if(!b.tent){b.tent={side:pick([-1,1]),warn:50,act:0,rec:0,trail:null};SFX.warn();}
b.minorT=Math.round(540/fm);break; // 自动扫击9秒一次
case "acidGas": // 泰伦酸蚀气体P3触手末端释放毒雾向下飘
if(b.phase<2){b.minorT=80;break;}
for(const sd of[-1,1])G.zones.push({x:b.x+sd*16,y:b.y+30,r:19,vy:0.75,life:520,col:"#a8ff5c",kind:"gas"});
b.minorT=Math.round(240/fm);break;
}
}
function hurtBoss(dmg){
const b=G.boss;if(!b||b.dead||b.transT>0||!b.landed)return; // 入场未到位/变形转场期间不掉血landed锁防站桩Boss爬升后永远满足 y<ty 而免疫)
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;
G.bossKills=(G.bossKills||0)+1; // 账号累计击坠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();}
if(typeof submitRun==="function")submitRun(true); // rush胜利也上报击坠计入排行榜
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=["碎石边境","血色星云","钢铁舰队","死寂空间站","虫群巢穴","核心决战"];