340 lines
17 KiB
JavaScript
340 lines
17 KiB
JavaScript
"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;} // 花弹:越变越大(有上限)
|
||
if(b.rift){if(b.rift>0){b.rift--;b.vx=0;b.vy=0; // 亚空间裂隙:悬停预瞄
|
||
if(b.rift<=0){const ra2=Math.atan2(G.p.y-b.y,G.p.x-b.x);b.vx=Math.cos(ra2)*3.9;b.vy=Math.sin(ra2)*3.9;b.spr="dartS";b.scale=1;b.r=2.5;}}} // 倒计时尽化长钉射出
|
||
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.vy)z.y+=z.vy; // 酸雾下飘
|
||
if(z.cd>0)z.cd--;
|
||
if(z.kind==="gas"&&G.p.dead<=0&&G.p.inv<=0&&dist2(G.p.x,G.p.y,z.x,z.y)<(z.r+G.stat.hitR)*(z.r+G.stat.hitR)&&!(z.cd>0)){z.cd=45;hitPlayer(12);} // 酸雾持续伤害
|
||
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){let ox=bm.src.ox||0,oy=bm.src.oy||0;
|
||
bm.x1=G.boss.x+ox;bm.y1=G.boss.y+oy;if(bm.src.followX2)bm.x2=bm.x1;
|
||
if(bm.src.sweep)bm.x2=bm.x1+Math.sin(G.time*0.014)*(bm.src.amp||55);} // 黑石要塞:亚空间主炮缓慢扫动
|
||
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;
|
||
}
|