starraid/star-raid/js/combat.js
JOJO 3ffb51098b 新增海盗、植物两大种族敌人各15种
- 海盗族15种:5杂兵+6小兵+4精英,锈红/金/骨白配色
- 植物族15种:5杂兵+6小兵+4精英,叶绿/深绿/花粉粉配色
- 各族子弹配色严格≤3色:海盗锈钉/炮弹/燃烧瓶,植物荆棘/种子/孢子
- 敌表新增 race/tier 标签,波次生成按层级池抽取
- 更新 CODEX_KEYS 与客户端图鉴总数
2026-08-06 21:20:39 +08:00

353 lines
18 KiB
JavaScript
Raw Permalink 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";
/* ================= 战斗结算 ================= */
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(b.maxSpd||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){
const tg2=nearestEnemy(b.x,b.y,130);
if(tg2){const a=Math.atan2(tg2.y-b.y,tg2.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,-(b.deflect||0.04),b.deflect||0.04),s=Math.hypot(b.vx,b.vy);
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;}
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==="bg"&&!b.dead){ // 蜜蜂手雷:近炸引信
const tg=nearestEnemy(b.x,b.y,38);
if(tg){b.dead=true;explodeOnce(b);}
else{b.fuse=(b.fuse||0)-1;if(b.fuse<=0){b.dead=true;explodeOnce(b);}}
}
if(b.t==="pmol"&&!b.dead){ // 海盗燃烧瓶:抛物线,落地/触敌爆炸并留火场
b.vy+=b.grav||0.08;
if(b.y>=H-10||b.fuse<=0){b.dead=true;explodeOnce(b);}
}
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&&z.y<H+40); // 酸雾飘出屏幕才清理
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;b.phc=(b.phc||0)+1;
if(b.hitsMax&&b.phc>=b.hitsMax){b.dead=true;}}
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"||b.t==="lm"||b.t==="lb")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);b.phc=(b.phc||0)+1;if(b.hitsMax&&b.phc>=b.hitsMax)b.dead=true;}}
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"||b.t==="lm"||b.t==="lb")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.swPh||0))*(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){healPlayer(35);}
else if(G.lives<G.stat.maxLives){G.lives++;banner("备用机体 +1","#5cff9d",60);SFX.pick();}
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;
}