starraid/js/enemies.js
JOJO 58c5d9d35b 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等
2026-08-05 20:18:46 +08:00

386 lines
23 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}
};
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=["碎石边境","血色星云","钢铁舰队","死寂空间站","虫群巢穴","核心决战"];