"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.shieldneed){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)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;i0){S.on--; for(let i=0;ist.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;ist.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;ist.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;i18){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;ipdMin){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)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.hp0){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