commit 58c5d9d35b62851d484d469c6e9fd3f1eec91764 Author: JOJO <1498581755@qq.com> Date: Wed Aug 5 20:18:46 2026 +0800 Boss系统重做:多管血条/锁血变身/专属变形动画/签名技能/rush随机装备老虎机 - 8个Boss全部重做:主攻击模式+1~2种随机小攻击+二/三阶段变形动画 - 新增5个雷霆战机参考Boss:太空原种/暗黑突击/激光钢琴/夺权者X/暗黑魔碟 - 多管血条(蓝/红/橙)+锁血转场+入场不掉血(b.y + + + + +星尘突击队 STAR RAID + + + + + +
+ +
+ + +
+
PIXEL ROGUELIKE SHMUP
+

STAR
RAID

+
星尘突击队
+
BEST --
+
+ + + + + +
+
键盘 WASD/方向键 移动 · 自动射击 · 超频充满自动暴走
SHIFT 精密移动 · P 暂停 · M 音乐
+
+ + +
+
— 出击准备 —
+
选择模式
+
+
闯关模式
6 大关卡
击破母巢核心
即获胜利
+
无尽模式
无限波次
突变词缀
冲击最高分
+
+
选择机体
+
+
难度
+
+ + +
+
+ + +
+
+ + +
+
— BOSS 挑战 —
+
随机武装:3主武Lv3 · 3副武Lv3 · 2僚机Lv2 · 击破计时
+
+
+ +
+ + +
+
— 武装抽取 —
+
+
 
+
+ + +
+
— 机库 · 永久强化 —
+
星币 ✦ 0
+
+ +
+ + +
+
— 作战手册 —
+
+ 移动 WASD / 方向键 / 触屏拖动
+ 射击 全自动,无需按键
+ 暴走 超频槽充满后自动触发,火力×1.7
+ 武器 主武器无限叠加,抽到即入列
+ 精密 SHIFT:低速移动并显示判定点
+ 擦弹 贴近敌弹掠过可充能超频
+ 结构值 中弹扣血,归零才损失一条命
+ 判定点 仅机身中心 3 像素!
+ 升级 拾取经验宝石,三选一强化
+ 星币 击坠获得,局外永久强化 +
+ +
+ + +
+ +
选择一项强化(按 1 / 2 / 3)
+
+
+ + +
+
‖ 暂停 · 军械配置
+
+
+ + + +
+
+ + +
+
— 军械图鉴 —
+
已收集 0/0 · 点击可实时演示
+
+ +
+ + +
+
+
演示
+
+
实战模拟 · 自动驾驶 · 触屏/方向键可接管
+
+ +
+ + +
+
任务失败
+
+
获得星币 ✦ 0
+
+ + +
+
+ + +
+ + + + + + + + + + + + diff --git a/js/audio.js b/js/audio.js new file mode 100644 index 0000000..809cc64 --- /dev/null +++ b/js/audio.js @@ -0,0 +1,71 @@ +"use strict"; +/* ---------------- 音频:程序化合成 ---------------- */ +const AU={ctx:null,master:null,mgain:null, + init(){ if(this.ctx) return; + try{ this.ctx=new (window.AudioContext||window.webkitAudioContext)(); + this.master=this.ctx.createGain(); this.master.gain.value=0.5; this.master.connect(this.ctx.destination); + this.mgain=this.ctx.createGain(); this.mgain.gain.value=SAVE.music?0.16:0; this.mgain.connect(this.master); + }catch(e){} + }, + now(){return this.ctx?this.ctx.currentTime:0;}, + env(g,t,a,d,v){g.gain.setValueAtTime(0,t);g.gain.linearRampToValueAtTime(v,t+a);g.gain.exponentialRampToValueAtTime(0.001,t+a+d);}, + tone(f,type,dur,vol=0.2,slide=0){ if(!this.ctx||SAVE.mute)return; + const t=this.now(),o=this.ctx.createOscillator(),g=this.ctx.createGain(); + o.type=type;o.frequency.setValueAtTime(f,t); + if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(20,f+slide),t+dur); + this.env(g,t,0.005,dur,vol); o.connect(g);g.connect(this.master); o.start(t);o.stop(t+dur+0.05); }, + noise(dur,vol=0.25,hp=400){ if(!this.ctx||SAVE.mute)return; + const t=this.now(),len=Math.floor(this.ctx.sampleRate*dur),buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate),d=buf.getChannelData(0); + for(let i=0;isetTimeout(()=>AU.tone(f,"square",0.1,0.12),i*70));}, + hurt(){AU.tone(160,"sawtooth",0.3,0.3,-100);AU.noise(0.3,0.3,150);}, + bomb(){AU.noise(0.8,0.5,60);AU.tone(70,"sawtooth",0.7,0.3,-40);}, + od(){[440,660,880,1320].forEach((f,i)=>setTimeout(()=>AU.tone(f,"sawtooth",0.09,0.1),i*50));}, + graze(){AU.tone(1800,"sine",0.03,0.04);}, + warn(){for(let i=0;i<4;i++)setTimeout(()=>{AU.tone(440,"square",0.15,0.15);setTimeout(()=>AU.tone(349,"square",0.15,0.15),180);},i*400);}, +}; +/* 简易芯片音序器 */ +const MUS={step:0,next:0,bpm:138,playing:false,boss:false, + bass:[45,45,48,45,52,45,48,43], lead:[69,72,76,72,74,72,69,64], + tick(){ if(!AU.ctx||!this.playing||!SAVE.music)return; + const spb=60/(this.bpm*2); + while(this.next=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()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;i0||p.dead>0)return; + if(G.demoSim){spawnPart(p.x,p.y,"#41e6ff",3);SFX.graze();return;} // 演示模式无敌 + if(Math.random()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)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(d21){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.yMath.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;iH+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)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;} // 花弹:越变越大(有上限) + 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-10&&f.x!b.dead); + G.eb=G.eb.filter(b=>!b.dead); + // 区域效果(电浆/引力井) + for(const z of G.zones){z.life--; + 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)=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(d20){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)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)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]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; +} diff --git a/js/config.js b/js/config.js new file mode 100644 index 0000000..5bfd1b5 --- /dev/null +++ b/js/config.js @@ -0,0 +1,51 @@ +"use strict"; +/* ================================================================ + 星尘突击队 STAR RAID —— 像素肉鸽竖版射击 + 单文件实现:像素引擎 / 程序化音频 / 双模式 / 局内外成长 + ================================================================ */ + +/* ---------------- 工具 ---------------- */ +const $=id=>document.getElementById(id); +const clamp=(v,a,b)=>vb?b:v; +const lerp=(a,b,t)=>a+(b-a)*t; +const rnd=(a=1,b)=>b===undefined?Math.random()*a:a+Math.random()*(b-a); +const rndi=(a,b)=>Math.floor(rnd(a,b+1)); +const pick=arr=>arr[Math.floor(Math.random()*arr.length)]; +const TAU=Math.PI*2; +function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;} +const dist2=(ax,ay,bx,by)=>{const dx=ax-bx,dy=ay-by;return dx*dx+dy*dy;}; + +/* ---------------- 存档 ---------------- */ +const SAVE_KEY="starraid_v1"; +let SAVE; +function loadSave(){ + try{SAVE=JSON.parse(localStorage.getItem(SAVE_KEY))||null;}catch(e){SAVE=null;} + if(!SAVE) SAVE={coins:0, m:{}, ships:["falcon"], best:{s0:0,s1:0,e0:0,e1:0,stage:0}, mute:false, music:true}; + SAVE.m=SAVE.m||{}; SAVE.ships=SAVE.ships||["falcon"]; SAVE.best=SAVE.best||{s0:0,s1:0,e0:0,e1:0,stage:0}; + SAVE.codex=SAVE.codex||{}; +} +function persist(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(SAVE));}catch(e){}} +loadSave(); + +/* ---------------- 平衡数值总表 ---------------- + 所有概率与成长曲线集中于此,方便调平衡 */ +const BAL={ + player:{ speed:2.35, lives:3, hp:100, bombs:2, hitR:3, grazeR:14, focusMul:0.5, + fireInt:8, dmg:6, bulletSpd:5.4, critC:0.05, critM:2.5, magnetR:36 }, + // 稀有度权重 [普通,稀有,史诗],每点幸运: 稀有+2 史诗+1.5 + rarity:[62,28,10], + xpNeed:l=>Math.round(4+l*2.5+Math.pow(l,1.5)), // 升级所需经验(下调,保证每波≈1级) + // 波次缩放(n 从 0 起):平缓曲线,匹配玩家成长 + hpMul: n=>1+0.10*n+0.012*Math.pow(n,1.5), + spdMul:n=>Math.max(0.6,1-0.015*n), // 血量越高波次,移动越慢 + fireMul:n=>Math.min(1.3,1+0.012*n), + eliteC:n=>Math.min(0.16,0.03+n*0.006), // 精英出现率 + coinDrop:0.5, // 普通敌掉币率 + scoreCombo:k=>Math.min(3,1+k*0.02), // 连击倍率上限 x3 + odGain:{kill:2.6,graze:2.2}, + coinGlobal:4, // 全局金币倍率 + bombDmg:80, + reviveInv:160, + hard:{hp:1.35,fire:1.3,coin:1.5}, // 困难修正 +}; + diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..f6b2d9c --- /dev/null +++ b/js/core.js @@ -0,0 +1,605 @@ +"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({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:8,spd:5.4,gap:6})}, + shotgun:{name:"超级霰弹",spr:"pelt",rare:0,beh:"streams",ds:"弹丸只从两翼倾泻,中央中空", + lvl:w=>({n:5+2*w,dmg:4.2*(1+.18*(w-1)),int:22,spd:5.5,fan:0.5,gap:2,skipMid:1})}, + needle:{name:"蜂针刺弹",spr:"ndl",rare:0,beh:"streams",ds:"淬毒细针,命中后持续蚀甲", + lvl:w=>({n:1+Math.floor(w/3),dmg:2.6*(1+.2*(w-1)),int:3,spd:7,gap:4,poison:100})}, + flame:{name:"火焰喷射器",icon:"flame",rare:0,beh:"flame",ds:"周期性喷吐的宽幅烈焰,近身湮灭", + lvl:w=>({n:3+Math.floor(w/2),dmg:1.1*(1+.22*(w-1)),range:70+8*w,burst:34+4*w,cd:Math.max(46,74-5*w)})}, + shotgun2:{name:"霜冻霰弹",spr:"shard",rare:1,beh:"streams",ds:"冰晶霰弹,大面积冻结减速", + lvl:w=>({n:3+w,dmg:3*(1+.18*(w-1)),int:23,spd:5.2,fan:0.42,gap:2,slow:80+25*w})}, + tesla:{name:"电弧发生器",icon:"chain",rare:1,beh:"tesla",ds:"自动电击周围敌人,无处可逃", + lvl:w=>({n:1+Math.floor(w/2),dmg:8*(1+.25*(w-1)),int:44-2*w,range:110})}, + railgun:{name:"电磁炮",icon:"rail",rare:2,beh:"rail",ds:"蓄力磁轨贯穿敌阵,命中留下电浆区", + lvl:w=>({dmg:55*(1+.3*(w-1)),charge:120-10*w,spd:11,zone:24+4*w})}, + deathray:{name:"死光",icon:"laser",rare:2,beh:"deathray",ds:"蓄力后自动锁敌的死亡射线", + lvl:w=>({dps:0.9*(1+.28*(w-1)),charge:160-12*w,beam:55,width:5+2*w})}, + crescent:{name:"月牙斩",spr:"cres",rare:1,beh:"crescent",ds:"巨型旋刃缓慢斩出,贯穿敌阵", + lvl:w=>({dmg:44*(1+.24*(w-1)),int:120,spd:3.4})}, + fission:{name:"分裂弹",spr:"fiss",rare:1,beh:"fission",ds:"飞行一段距离后炸裂为散射弹幕", + lvl:w=>({dmg:6*(1+.22*(w-1)),int:20,spd:5.2,split:3+Math.floor(w/2),splitAt:26})}, + frost:{name:"冰冻枪",spr:"icelance",rare:1,beh:"frost",ds:"贯穿冰枪,命中将敌人彻底冻结", + lvl:w=>({dmg:6*(1+.2*(w-1)),int:16,spd:6,freeze:70+25*w})}, + swarm:{name:"蜂巢导弹",spr:"msl2",rare:1,beh:"homing",ds:"自动追踪的导弹蜂群,命中爆裂", + lvl:w=>({n:1+w,dmg:2.2*(1+.2*(w-1)),int:22,spd:3.2,aoe:10})}, + bubble:{name:"重力泡泡",spr:"bub",rare:1,beh:"bubble",ds:"缓慢漂浮的大泡泡,爆裂冲击", + lvl:w=>({dmg:18*(1+.25*(w-1)),int:26,spd:1.4,aoe:30+4*w})}, + starfall:{name:"流星火雨",spr:"met",rare:2,beh:"starfall",ds:"流星自脚下逆冲而起,轰炸前方空域", + lvl:w=>({n:3+Math.floor(w/2),dmg:16*(1+.25*(w-1)),int:150-8*w,aoe:16})}, + boomerang:{name:"回旋镖",spr:"boomer",rare:0,beh:"boom",ds:"亮蓝回旋刃,远射贯穿后回旋返航", + lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:21,spd:4.8})}, + singularity:{name:"奇点炮",spr:"sing",rare:2,beh:"sing",ds:"发射微型黑洞,牵引后剧烈坍缩", + lvl:w=>({dmg:40*(1+.3*(w-1)),int:200,pull:60,aoe:40,fuse:100})}, + wavecannon:{name:"波动炮",spr:"waveb",rare:2,beh:"wave",ds:"蓄力斩出贯穿敌阵的剑气弧光", + lvl:w=>({dmg:32*(1+.3*(w-1)),charge:100-8*w,spd:5,width:14+6*w})}, + holy:{name:"圣光弹",spr:"holyo",rare:2,beh:"holy",ds:"爆裂为旋转生长的圣光十字", + lvl:w=>({dmg:13*(1+.25*(w-1)),int:65,spd:3,len:16+3*w})}, + thundercall:{name:"落雷术",spr:"thnd",rare:1,beh:"thunder",ds:"呼唤天雷轰击前方区域", + lvl:w=>({n:2+Math.floor(w/2),dmg:12*(1+.25*(w-1)),int:90-4*w,aoe:13})}, + arcshu:{name:"电弧手里剑",spr:"shuri",rare:2,beh:"shuriken",ds:"旋转雷刃一路放电,贯穿全场", + lvl:w=>({dmg:6*(1+.22*(w-1)),int:95-4*w,spd:2.2,zapDmg:5*(1+.25*(w-1)),pulse:20})}, + volt:{name:"电能机炮",spr:"voltb",rare:1,beh:"volt",ds:"高频电能弹,命中后电弧连锁跳跃", + lvl:w=>({n:1+Math.floor(w/3),dmg:3.2*(1+.2*(w-1)),int:5,spd:6.2,gap:5,chain:2+Math.floor(w/2)})}, + dawn:{name:"黎明重炮",spr:"dawnsh",rare:2,beh:"dawn",ds:"长蓄力重炮,一炮轰出黎明", + lvl:w=>({dmg:70*(1+.3*(w-1)),charge:170-12*w,spd:5,aoe:30+3*w})}, + proto:{name:"协议武装",spr:"protb",rare:2,beh:"protocol",ds:"青蓝连发压制,周期性蓄能重炮", + lvl:w=>({dmg:3*(1+.2*(w-1)),int:6,spd:6.5,heavy:120-8*w,hdmg:36*(1+.28*(w-1)),haoe:26+3*w})}, + nebula:{name:"星云奥秘",spr:"neb",rare:2,beh:"nebula",ds:"缓慢追踪的奥术漩涡,命中绽放群星", + lvl:w=>({dmg:12*(1+.25*(w-1)),int:100-5*w,spd:1.6,stars:10+2*w})} +}; +const MAINS=Object.keys(WEAPONS); +/* ================= 18 副武器 ================= */ +const SUBS={ + broad:{name:"侧舷火炮",spr:"broad",rare:0,max:5,beh:"broad",ds:l=>`舷炮斜向前方45°齐射,Lv${l+1}`}, + sweep:{name:"探照扫射",icon:"laser",rare:1,max:5,beh:"sweep",ds:l=>`往复摆动的扫描光束,Lv${l+1}`}, + leech:{name:"寄生电浆",icon:"chain",rare:1,max:3,beh:"leech",ds:l=>`命中附着电浆水蛭,吸血回复结构,Lv${l+1}`}, + orbit:{name:"环绕光刃",spr:"blade",rare:1,max:4,beh:"orbit",ds:l=>`光刃+1(当前${l+1}把)`}, + sats:{name:"卫星屏障",spr:"sat",rare:1,max:3,beh:"sats",ds:l=>`环绕卫星可格挡敌弹,Lv${l+1}`}, + pd:{name:"点防御炮",spr:"dGun",rare:1,max:4,beh:"pd",ds:l=>`自动拦截来袭敌弹(不打贴身弹),Lv${l+1}`}, + slowfield:{name:"时间力场",spr:"grv",rare:2,max:3,beh:"slowfield",ds:l=>"力场内敌人与敌弹大幅减速,Lv"+(l+1)}, + flares:{name:"热诱弹舱",spr:"flare",rare:0,max:4,beh:"flares",ds:l=>"扇形抛射4枚缓速热诱弹,各诱爆一枚敌弹,Lv"+(l+1)}, + seeker:{name:"索敌浮游炮",spr:"dGun2",rare:1,max:3,beh:"seeker",ds:l=>"离机索敌的浮游炮,Lv"+(l+1)}, + beamdrone:{name:"光束浮游炮",spr:"dBeam",rare:2,max:3,beh:"beamdrone",ds:l=>"环绕发射光束的浮游单元,Lv"+(l+1)}, + frostnova:{name:"冰霜新星",spr:"frost",rare:1,max:3,beh:"frostnova",ds:l=>"周期性冻结周围敌人,Lv"+(l+1)}, + thorns:{name:"反伤棘甲",icon:"crit",rare:1,max:3,beh:"thorns",ds:l=>"撞击你的敌人遭到反噬,Lv"+(l+1)}, + sticky:{name:"粘着炸弹",spr:"sticky",rare:0,max:3,beh:"sticky",ds:l=>"粘附敌人的定时高爆炸弹,Lv"+(l+1)}, + magnetar:{name:"磁暴环",icon:"mag",rare:2,max:3,beh:"magnetar",ds:l=>"周期性电磁脉冲,消弹伤敌,Lv"+(l+1)}, + executor:{name:"斩首巡航弹",spr:"cmsl",rare:2,max:3,beh:"executor",ds:l=>"巡航导弹猎杀最强之敌,Lv"+(l+1)}, + airstrike:{name:"空爆弹",spr:"amsl",rare:1,max:5,beh:"airstrike",ds:l=>"双弹侧掠折向直上,穿透机身沿途轰炸,Lv"+(l+1)}, + repair:{name:"修复无人机",spr:"dHeal",rare:1,max:3,beh:"repair",ds:l=>"持续修复机体结构,Lv"+(l+1)} +}; +const SUB_KEYS=Object.keys(SUBS); +/* ================= 12 僚机 ================= */ +const DRONES={ + gunner:{name:"机枪僚机",spr:"dGun",rare:0,max:3,beh:"aim",ds:l=>"协同瞄准射击,Lv"+(l+1)}, + gunner2:{name:"双翼机枪手",spr:"dGun2",rare:1,max:3,beh:"aim2",ds:l=>"双管僚机,Lv"+(l+1)}, + sniperd:{name:"狙击僚机",spr:"dSnip",rare:1,max:3,beh:"snipe",ds:l=>"高伤点射,Lv"+(l+1)}, + beamer:{name:"光束僚机",spr:"dBeam",rare:2,max:3,beh:"beam",ds:l=>"周期光束,Lv"+(l+1)}, + bomber:{name:"轰炸僚机",spr:"dBomb",rare:1,max:3,beh:"bomb",ds:l=>"投掷爆炸弹,Lv"+(l+1)}, + frostd:{name:"冰霜僚机",spr:"dFrost",rare:1,max:3,beh:"frostD",ds:l=>"减速弹,Lv"+(l+1)}, + teslaD:{name:"电弧僚机",spr:"dTesla",rare:2,max:3,beh:"teslaD",ds:l=>"近身电击,Lv"+(l+1)}, + rammer:{name:"冲撞僚机",spr:"dRam",rare:1,max:3,beh:"melee",big:1.6,ds:l=>"高速环绕撞击,Lv"+(l+1)}, + healer:{name:"修复僚机",spr:"dHeal",rare:1,max:3,beh:"heal",ds:l=>"缓慢回复结构值,Lv"+(l+1)}, + shieldd:{name:"护盾僚机",spr:"dShield",rare:2,max:3,beh:"block",big:1.5,ds:l=>"环绕格挡敌弹,Lv"+(l+1)}, + missileD:{name:"导弹僚机",spr:"dMsl",rare:1,max:3,beh:"missile",ds:l=>"追踪导弹,Lv"+(l+1)}, + bladeD:{name:"刃舞僚机",spr:"dBlade",rare:1,max:3,beh:"bladeD",ds:l=>"投掷回旋刃,Lv"+(l+1)}, + sweeper:{name:"扫射僚机",spr:"dSweep",rare:1,max:3,beh:"sweep",ds:l=>"扇形区域来回扫射,火力覆盖,Lv"+(l+1)} +}; +const DRONE_KEYS=Object.keys(DRONES); +/* ================= 8 被动强化 ================= */ +const PASSIVES={ + fire:{name:"火控雷达",icon:"fire",rare:0,max:5,ds:()=>"射速 +12%"}, + crit:{name:"暴击芯片",icon:"crit",rare:1,max:4,ds:()=>"暴击率 +8%"}, + shield:{name:"能量护盾",icon:"shield",rare:1,max:3,ds:l=>l?"护盾充能加快":"获得可格挡的护盾"}, + nano:{name:"纳米修复",icon:"nano",rare:1,max:3,ds:()=>"血量上限+25 并回复40"}, + mag:{name:"引力核心",icon:"mag",rare:0,max:3,ds:()=>"拾取范围 +60%"}, + bounty:{name:"赏金协议",icon:null,spr:"coin",rare:0,max:3,ds:()=>"星币获取 +30%"}, + odcore:{name:"超频核心",icon:null,spr:"odc",rare:1,max:3,ds:()=>"暴走充能+30%,时长+1s"}, + ghost:{name:"幽灵装甲",icon:"ghost",rare:2,max:2,ds:()=>"闪避率 +10%"} +}; +const PAS_KEYS=Object.keys(PASSIVES); +/* ---- 主武器火力衰减与敌人动态缩放 ---- + 装备数量 → 每把武器伤害系数:[1]100% [2]80% [3]60% [4]50% [5]45% + 总火力 = 系数×数量;敌人血量与数量随总火力提升 */ +const WMUL=[0,1,0.8,0.6,0.5,0.45]; +function wCount(){return Math.max(1,G.loadout.length);} +function wMul(){return WMUL[clamp(wCount(),1,5)];} +function totWMul(){return wMul()*wCount();} +function enemyScale(){return 1+0.22*(totWMul()-1);} // 血量缩放 +function budgetScale(){return 1+0.22*(totWMul()-1);} // 数量缩放 diff --git a/js/enemies.js b/js/enemies.js new file mode 100644 index 0000000..aaed3e9 --- /dev/null +++ b/js/enemies.js @@ -0,0 +1,385 @@ +"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.y6&&e.ye.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&&shooterNG.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大Boss:1种主攻击模式 + 同时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*dy0;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;i0)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.yb.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;i0){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=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=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.y0&&b.hp{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{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=["碎石边境","血色星云","钢铁舰队","死寂空间站","虫群巢穴","核心决战"]; diff --git a/js/render.js b/js/render.js new file mode 100644 index 0000000..de76ae0 --- /dev/null +++ b/js/render.js @@ -0,0 +1,509 @@ +"use strict"; +/* ================= 背景:星空与星云 ================= */ +const STARS=[[],[],[]]; +for(let i=0;i<3;i++)for(let j=0;j<26-i*6;j++)STARS[i].push({x:rnd(W),y:rnd(H),s:(0.35+i*0.4),sz:i===2?2:1,col:i===2?pick(["#aab8d8","#8fa8d8","#c0a8c8"]):"#3f4c74"}); +const NEB=["#131c3f","#33102a","#0f2a1c","#101a33","#2a1030","#330f0f"]; +let nebY=0,planet=null; +function drawBG(dt){ + ctx.fillStyle="#05070f";ctx.fillRect(0,0,W,H); + const stage=G?G.stage-1:0,ncol=NEB[clamp(stage,0,5)]; + nebY=(nebY+0.08*dt)%160; + for(let k=0;k<2;k++){ + const g=ctx.createRadialGradient(W*(k?0.75:0.25),nebY+k*160-160,10,W*(k?0.75:0.25),nebY+k*160-160,130); + g.addColorStop(0,ncol);g.addColorStop(1,"transparent"); + ctx.globalAlpha=0.38;ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.globalAlpha=1; + } + for(let i=0;i<3;i++)for(const s of STARS[i]){ + s.y+=s.s*dt*(G&&G.odActive>0?2:1);if(s.y>H){s.y=-2;s.x=rnd(W);} + ctx.fillStyle=s.col;ctx.fillRect(s.x,s.y,s.sz,s.sz); + } + if(planet){ + planet.y+=0.12*dt; + if(planet.y>H+40){planet=null;} + else{ + ctx.globalAlpha=0.5;ctx.fillStyle=planet.col; + ctx.beginPath();ctx.arc(planet.x,planet.y,planet.r,0,TAU);ctx.fill(); + ctx.globalAlpha=0.25;ctx.fillStyle="#fff"; + ctx.fillRect(planet.x-planet.r,planet.y-2,planet.r*2,1); + ctx.fillRect(planet.x-planet.r+3,planet.y+4,planet.r*2-6,1); + ctx.globalAlpha=1; + }} + else if(Math.random()<0.001)planet={x:rnd(40,W-40),y:-40,r:rndi(14,26),col:pick(["#4a5a8c","#8c4a5a","#4a8c6e"])}; +} +/* ================= Boss 程序化像素绘制 ================= */ +function px(x,y,w,h,col){ctx.fillStyle=col;ctx.fillRect(Math.round(x),Math.round(y),w,h);} +function poly(pts,col){ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(pts[0],pts[1]);for(let i=2;i0){const prog=1-b.transT/70; + ox=rnd(-1.6,1.6);oy=rnd(-1.2,1.2); + if(b.idx===1||b.idx===3||b.idx===4||b.idx===6||b.idx===7){mp=prog;if(prog>0.75)flashA=(1-prog)*1.6;} + else{sc=prog<0.5?1-0.16*(prog*2):0.84+0.16*((prog-0.5)*2); + if(prog<0.5)ph2=b.phase-1; + flashA=prog<0.5?prog*0.8:(1-prog)*0.8;}} + ctx.save();ctx.translate(Math.round(b.x+ox),Math.round(b.y+oy)); + if(sc!==1)ctx.scale(sc,sc); + const fl=b.flash>0?"#ffffff":null,t=b.t; + if(b.idx===0){ // 巡逻巡洋舰 + px(-22,-10,44,20,"#3a4460");px(-18,-14,36,6,"#5b6b8c");px(-14,10,28,5,"#2a3450"); + px(-24,-6,6,12,"#5b6b8c");px(18,-6,6,12,"#5b6b8c"); + px(-25,4,4,6,fl||"#e0393e");px(21,4,4,6,fl||"#e0393e"); + px(-8,-18,16,8,"#5b6b8c");px(-4,-20,8,4,"#3a4460"); + px(-6,-4,12,10,fl||"#ff9a3d");px(-3,-2,6,6,"#fff4d9"); + for(let i=-2;i<=2;i++)px(i*8-1,12,2,3,t%40<20?"#ff5566":"#7a1622"); + px(-22,-10,44,2,"#8c9ab8"); + if(ph2>=1){px(-2,-27,4,8,"#5b6b8c");px(-1,-29,2,3,t%20<10?"#ff5566":"#7a1622");} // 二阶段顶部炮塔 + }else if(b.idx===1){ // 太空原种:虫蛹↔蝠鲼(变身:裂皮→变粉→红眼→展翅) + if(mp!==null){if(mp<0.5)drawPupa(t,fl,mp*2);else drawManta(t,fl,(mp-0.5)*2);} + else if(ph2===0)drawPupa(t,fl,0);else drawManta(t,fl,1); + }else if(b.idx===2){ // 歼灭者 + for(let i=0;i<6;i++){const a=i/6*TAU+t*0.01; + px(Math.cos(a)*19-3,Math.sin(a)*19-3,6,6,"#5b3a8c");} + px(-16,-12,32,24,"#2a1f4a");px(-12,-16,24,6,"#4a3a7a");px(-12,10,24,6,"#4a3a7a"); + px(-20,-4,6,8,"#4a3a7a");px(14,-4,6,8,"#4a3a7a"); + px(-6,-6,12,12,fl||"#b05ce8");px(-3,-3,6,6,"#f0d6ff"); + for(let i=0;i<3+ph2;i++){const a=t*0.02+i*TAU/(3+ph2); + px(Math.cos(a)*26-2,Math.sin(a)*26-2,4,4,t%30<15?"#b05ce8":"#5c2a80");} + }else if(b.idx===3){ // 暗黑突击:小体型高机动,翼展角15°/45°/70°随阶段展开 + const SW=[15,45,70]; + let swp=SW[Math.min(ph2,2)]; + if(mp!==null){const s0=SW[Math.max(0,b.phase-1)],s1=SW[Math.min(b.phase,2)];swp=s0+(s1-s0)*mp;} + ctx.scale(0.34,0.34); + drawAssault(t,fl,swp,Math.min(ph2,2),mp); + }else if(b.idx===4){ // 激光钢琴:全屏宽半碟要塞(二阶段裂壳露红核) + drawPiano(b,t,fl,mp!==null?mp:(ph2>=1?1:0)); + }else if(b.idx===5){ // 母巢核心 + for(let i=0;i<8;i++){const a=i/8*TAU+t*0.005; + px(Math.cos(a)*24-2,Math.sin(a)*24-2,5,5,"#8c5b2a");} + px(-18,-14,36,28,"#4a3410");px(-14,-18,28,8,"#6e4f1a");px(-14,10,28,8,"#6e4f1a"); + px(-22,-8,5,16,"#6e4f1a");px(17,-8,5,16,"#6e4f1a"); + const pl=8+Math.sin(t*0.1)*2; + px(-pl/2-3,-pl/2-3,pl+6,pl+6,fl||"#ff9a3d");px(-pl/2,-pl/2,pl,pl,"#fff4d9"); + px(-2,-22,4,6,"#ffcf5c");px(-2,16,4,6,"#ffcf5c"); + }else if(b.idx===6){ // 夺权者X:巨型母舰(二阶段抛侧舱→拉长钻石机身+僚机) + if(ph2===0||mp!==null){ // P1巨舰主体(变身时收缩淡出) + const hullA=mp!==null?1-mp:1,hullS=mp!==null?1-mp*0.25:1; + if(hullA>0.02){ctx.save();ctx.scale(hullS,hullS);ctx.globalAlpha=hullA; + poly([0,46,-14,8,-22,-30,-9,-42,9,-42,22,-30,14,8],"#d8e4f0"); + poly([0,46,14,8,22,-30,9,-42,0,-42],"#9fb0c6"); + poly([-9,-42,-34,-52,-26,-28,-9,-34],"#aab8cc");poly([9,-42,34,-52,26,-28,9,-34],"#aab8cc"); + circ(0,-16,8,fl||"#43b8ff");circ(0,-16,4,"#bfefff"); + px(-2,-40,4,18,"#43b8ff");px(-2,-4,4,30,"#43b8ff"); + ctx.restore();ctx.globalAlpha=1;}} + if(ph2===0||mp!==null){ // 侧舱(变身时抛离飞走) + const jt=mp||0; + if(jt<0.99)for(const sd of[-1,1]){ctx.save(); + ctx.translate(sd*(55+jt*90),jt*60);ctx.rotate(sd*jt*0.7);ctx.globalAlpha=1-jt*0.8; + px(-11,-22,22,44,"#aab8cc");px(-11,-22,22,4,"#43b8ff"); + px(-8,18,16,6,"#8a9ab8"); + for(let i=0;i<3;i++)px(-3,-10+i*8,6,4,fl||"#ff3355"); + ctx.restore();ctx.globalAlpha=1;}} + if(ph2>=1){ // 拉长钻石机身+机翼(二阶段显现) + const di=mp!==null?mp:1; + ctx.save();ctx.globalAlpha=di;const ds=0.7+0.3*di;ctx.scale(ds,ds); + poly([0,58,-13,8,-8,-38,0,-48,8,-38,13,8],"#e8f4ff"); + poly([0,58,13,8,8,-38,0,-48],"#9fb0c6"); + poly([-8,-36,-36,-48,-28,-26,-8,-30],"#c0cddd");poly([8,-36,36,-48,28,-26,8,-30],"#c0cddd"); + circ(0,-14,8,fl||"#43b8ff");circ(0,-14,4,"#bfefff"); + px(-2,-36,4,18,"#43b8ff");px(-2,0,4,34,"#43b8ff"); + ctx.restore();ctx.globalAlpha=1;} + }else{ // 暗黑魔碟:核心+圆环;二阶段圆环前段裂成犄角+赤瞳激光;三阶段爆甲出刺+炮台 + circ(0,0,20,"#1a1030");circ(0,0,15,"#12081c"); + if(ph2===0&&mp===null){ // 一阶段:完整圆环绕核心缓转 + ctx.strokeStyle="#55336e";ctx.lineWidth=13; + ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.stroke(); + ctx.strokeStyle="#6e4490";ctx.lineWidth=5; + ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.stroke(); + for(let i=0;i<4;i++){const a=i*Math.PI/2+t*0.012; + px(Math.cos(a)*30-2,Math.sin(a)*30-2,4,4,"#c86bff");} + px(-6,-4,12,8,"#2a1a3a"); + }else{ + if(mp!==null&&b.phase===1){ // 变形中:圆环残留快速淡出 + const ra=1-clamp(mp/0.35,0,1); + if(ra>0){ctx.globalAlpha=ra;ctx.strokeStyle="#55336e";ctx.lineWidth=13; + ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.stroke();ctx.globalAlpha=1;}} + const ha=mp!==null?clamp((mp-0.25)/0.6,0,1):1; // 犄角展开度 + if(ha>0){ctx.globalAlpha=ha; + for(const sd of[-1,1]){ctx.save();ctx.rotate((1-ha)*sd*1.1); + poly([sd*10,4,sd*30,-26,sd*46,-32,sd*40,-6,sd*20,12],"#55336e"); + poly([sd*16,2,sd*32,-20,sd*40,-24,sd*34,-4,sd*20,8],"#6e4490"); + if(ph2>=2){ // 三阶段爆甲:犄角外壳小刺+红色炮台 + for(let k=0;k<3;k++)poly([sd*(25+k*7),-13-k*6,sd*(29+k*7),-22-k*6,sd*(33+k*7),-12-k*6],"#2a1a3a"); + px(sd*41-3,-36,7,6,fl||"#ff3355");} + ctx.restore();} + ctx.globalAlpha=1;} + const eyeA=mp!==null?clamp((mp-0.45)/0.5,0,1):1; // 核心赤瞳 + if(eyeA>0){ctx.globalAlpha=eyeA; + px(-11,-5,7,6,fl||"#ff3355");px(4,-5,7,6,fl||"#ff3355"); + px(-9,-3,3,2,"#ffdfe5");px(6,-3,3,2,"#ffdfe5"); + ctx.globalAlpha=1;} + } + } + if(flashA>0){ctx.globalAlpha=flashA;ctx.strokeStyle="#fff"; + ctx.beginPath();ctx.arc(0,0,b.r+6,0,TAU);ctx.stroke();ctx.globalAlpha=1;} + if(b.flash>0)b.flash--; + ctx.restore(); + // 冲撞预警线(暗黑突击) + if(b.dashSt&&b.dashSt.ph==="tele"&&G.time%4<2){ + ctx.strokeStyle="#ff3355";ctx.globalAlpha=0.6; + if(ctx.setLineDash)ctx.setLineDash([4,3]); + ctx.beginPath();ctx.moveTo(b.x,b.y);ctx.lineTo(G.p.x,G.p.y);ctx.stroke(); + if(ctx.setLineDash)ctx.setLineDash([]);ctx.globalAlpha=1;} + // 多管血条:一管蓝/二管红/三管橙,当前管铺在下一管之上,见底锁血转场 + const w=160,def2=BOSSES[b.idx],thr=def2.phases===2?[0.5]:[0.66,0.33]; + const TUBEC=["#43b8ff","#e0393e","#ff9a3d"]; + const ratio=clamp(b.hp/b.maxhp,0,1),bx=(W-w)/2; + px(bx-2,8,w+4,8,"#0d1226"); + px(bx,10,w,4,b.phase0&&G.time%8<4){ctx.strokeStyle="#fff";ctx.lineWidth=1;ctx.strokeRect(bx-2.5,7.5,w+5,9);} + ctx.font="6px 'Press Start 2P',monospace";ctx.fillStyle="#fff";ctx.textAlign="center"; + ctx.fillText(b.transT>0?b.name:(b.name+" "+Math.ceil(pc*100)+"%"),W/2,22); +} +/* ---- Boss 分体绘制助手 ---- */ +function drawPupa(t,fl,cr){ // 太空原种·虫蛹(cr=裂变粉化进度0~1) + ctx.rotate(Math.sin(t*0.05)*0.05); + const body=[0,-40,11,-37,15,-28,13,-14,11,0,8,14,5,26,2,34,0,36,-2,34,-5,26,-8,14,-11,0,-13,-14,-15,-28,-11,-37]; + poly(body,"#c9d6e2"); + poly([0,-40,11,-37,15,-28,13,-14,11,0,8,14,5,26,2,34,0,36],"#9fb0c2"); + if(cr>0){ctx.globalAlpha=cr*0.85;poly(body,"#f0a0c0");ctx.globalAlpha=1;} // 身体变粉 + for(let i=0;i<5;i++){const wy=-24+i*11,ww=13-i*1.8;px(-ww,wy,ww*2,2,"#8498ac");} + if(cr>0.25){px(-10,-18,3,14,"#8a2a3a");px(7,-8,3,12,"#8a2a3a");} // 表皮裂开 + if(cr>0.55){px(-4,-30,3,10,"#8a2a3a");px(-13,-2,3,9,"#8a2a3a");} + circ(0,-34,10,"#dde7f0");px(-9,-37,18,3,"#9fb0c2"); + px(-6,-35,4,4,fl||"#43e8ff");px(2,-35,4,4,fl||"#43e8ff"); // 青眼 + if(cr>0.3){ctx.globalAlpha=Math.min(1,(cr-0.3)/0.5); // 眼睛变红 + px(-6,-35,4,4,"#ff3355");px(2,-35,4,4,"#ff3355");ctx.globalAlpha=1;} + px(-12,-44,3,7,"#9fb0c2");px(9,-44,3,7,"#9fb0c2"); + px(-19,-16,5,12,"#7fd8ff");px(14,-16,5,12,"#7fd8ff"); +} +function drawManta(t,fl,un){ // 太空原种·蝠鲼(un=展翅进度0~1) + const ws=0.15+0.85*un; + ctx.save();ctx.scale(ws,1+Math.sin(t*0.07)*0.04*un); // 翅膀自背后展开 + poly([0,-20,-26,-14,-52,-4,-76,6,-58,12,-34,10,-16,16,0,20],"#c86aa8"); + poly([0,-20,26,-14,52,-4,76,6,58,12,34,10,16,16,0,20],"#c86aa8"); + poly([0,-18,-24,-12,-50,-3,-72,7,-56,9,-30,2,0,-8],"#e89cc4"); + poly([0,-18,24,-12,50,-3,72,7,56,9,30,2,0,-8],"#e89cc4"); + if(un>0.6)for(const sd of[-1,1])for(let i=0;i<3;i++)px(sd*(44+i*10)-1,6+i*2,2,2,"#7dff8a"); + ctx.restore(); + ctx.globalAlpha=Math.min(1,un*1.6); + poly([0,28,-7,8,-8,-8,-5,-18,5,-18,8,-8,7,8],"#8a3a70"); + circ(0,-18,9,"#e8d5e0"); + px(-5,-19,3,4,"#3a1030");px(2,-19,3,4,"#3a1030"); + px(-5,-18,3,3,fl||"#ff3355");px(2,-18,3,3,fl||"#ff3355"); // 红瞳 + px(-13,-26,3,8,"#7dff8a");px(10,-26,3,8,"#7dff8a"); + for(let i=0;i<4;i++)px(-9+i*5,0,3,5,fl||"#ff3355"); + ctx.globalAlpha=1; +} +function drawAssault(t,fl,swp,ph,mp){ // 暗黑突击(swp=翼展角;炮管/推进器随阶段增多) + const sw=swp*Math.PI/180; + for(const sd of[-1,1]){ + const tx=sd*(8+52*Math.sin(sw)),ty=14-52*Math.cos(sw); + poly([sd*4,6,tx,ty,tx-sd*9,ty+11,sd*4,20],ph>=2?"#c01a2e":"#a01224"); + poly([tx-sd*2,ty-6,tx+sd*3,ty-14,tx+sd*7,ty-3],"#6e0c1a");} + poly([0,38,-5,6,-3,-26,3,-26,5,6],"#d4233a"); + px(-2,8,4,8,fl||"#ffd23d"); + const guns=[[16,4],[-16,4],[30,-4],[-30,-4],[42,-10],[-42,-10]]; + const gn=ph>=2?6:ph>=1?4:2,pgn=ph===2?4:ph===1?2:0; + for(let i=0;i=pgn)gl=Math.max(1,8*mp); // 新炮管生长 + px(guns[i][0]-1.5,guns[i][1],3,gl,"#3a0a10");px(guns[i][0]-1,guns[i][1]+gl-2,2,2,"#ff9a3d");} + const eng=[[7,-26],[-7,-26],[0,-30],[14,-22]]; + const en=ph>=2?4:ph>=1?3:2,pen=ph===2?3:ph===1?2:0; + for(let i=0;i=pen)el=Math.max(1,el*mp); // 新推进器 + px(eng[i][0]-2,eng[i][1],4,3,"#5a0a14"); + px(eng[i][0]-1.5,eng[i][1]-3-el,3,el,t%6<3?"#ff9a3d":"#ffcf5c");} + if(ph>=1){poly([-52,-14,-24,-3,-22,-1,-50,-11],"#ffdfe5");poly([52,-14,24,-3,22,-1,50,-11],"#ffdfe5");} + if(ph>=2){px(-4,-16,8,12,fl||"#ff3355");px(-2,-14,4,8,"#ffdfe5");} +} +function drawPiano(b,t,fl,split){ // 激光钢琴(split=裂壳进度0~1) + const ks=b.keys,n=ks?ks.n:21; + px(-(n-1)*4.75-5,6,(n-1)*9.5+10,19,"#141821"); // 键槽 + for(let i=0;i=0)); + px(kx-4,8,8,14,hot?(t%6<3?"#e8fbff":"#43e8ff"):"#d8e4f0"); + px(kx-4,20,8,2,hot?"#43e8ff":"#8a9ab8");} + const off=split*30; // 两半穹顶向左右裂开 + for(const sd of[-1,1]){ctx.save();ctx.translate(sd*off,0); + const q0=sd<0?Math.PI:Math.PI*1.5,q1=sd<0?Math.PI*1.5:Math.PI*2; + ctx.fillStyle="#3a4460";ctx.beginPath();ctx.ellipse(0,6,118,34,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill(); + ctx.fillStyle="#55648a";ctx.beginPath();ctx.ellipse(0,6,92,26,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill(); + ctx.fillStyle="#6e7f9e";ctx.beginPath();ctx.ellipse(0,6,60,17,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill(); + ctx.restore();} + if(split>0){ctx.globalAlpha=split; // 红色核心 + const cr=10+Math.sin(t*0.12)*2; + circ(0,-4,cr+4,"#7a0e1e");circ(0,-4,cr,fl||"#ff3355");circ(0,-4,cr*0.45,"#ffdfe5"); + ctx.globalAlpha=1;} + if(split<1){ctx.globalAlpha=1-split; // 一阶段青色核心窗 + px(-7,-14,14,9,fl||"#43e8ff");px(-4,-12,8,5,"#bfefff"); + ctx.globalAlpha=1;} + px(-10-off,-32,20,10,"#3a4460");px(-6-off,-37,12,5,"#55648a"); // 舰桥随左半壳 + if(split<0.4)for(let i=0;i<7;i++){const a=Math.PI+(i+0.5)/7*Math.PI; + px(Math.cos(a)*110-1.5,6+Math.sin(a)*30-1.5,3,3,"#9fb4d8");} +} +/* ---- Boss光束与浮游炮渲染 ---- */ +function drawBossFx(){ + for(const f of G.funnels){ // 银钉无人机 + poly([f.x,f.y-5,f.x+4,f.y+3,f.x-4,f.y+3],"#cfe4ff"); + px(f.x-1,f.y-1,2,3,"#43b8ff"); + if(f.flashT>0){ctx.globalAlpha=0.6;px(f.x-3,f.y+3,6,3,"#ffffff");ctx.globalAlpha=1;} + } + for(const bm of G.bbeams){ + if(bm.tele>0){if(G.time%4<2){ctx.globalAlpha=0.5;ctx.strokeStyle=bm.col;ctx.lineWidth=1; + ctx.beginPath();ctx.moveTo(bm.x1,bm.y1);ctx.lineTo(bm.x2,bm.y2);ctx.stroke();ctx.globalAlpha=1;}} + else{ctx.globalCompositeOperation="lighter"; + ctx.globalAlpha=clamp(bm.life/10,0,1)*0.85; + ctx.strokeStyle=bm.col;ctx.lineWidth=bm.w; + ctx.beginPath();ctx.moveTo(bm.x1,bm.y1);ctx.lineTo(bm.x2,bm.y2);ctx.stroke(); + ctx.strokeStyle="#fff";ctx.lineWidth=Math.max(1,bm.w/3); + ctx.beginPath();ctx.moveTo(bm.x1,bm.y1);ctx.lineTo(bm.x2,bm.y2);ctx.stroke(); + ctx.globalAlpha=1;ctx.globalCompositeOperation="source-over";} + } +} +/* ================= 世界渲染 ================= */ +function drawSprite(spr,x,y){ctx.drawImage(spr,Math.round(x-spr.width/2),Math.round(y-spr.height/2));} +function drawSeg(x1,y1,x2,y2,w,col,alpha){ + const d=Math.hypot(x2-x1,y2-y1)||1,steps=Math.max(1,Math.ceil(d/2)); + ctx.globalAlpha=alpha;ctx.fillStyle=col; + for(let i=0;i<=steps;i++){const t2=i/steps; + ctx.fillRect(Math.round(lerp(x1,x2,t2)-w/2),Math.round(lerp(y1,y2,t2)-w/2),w,w);} + ctx.globalAlpha=1; +} +function render(){ + ctx.save(); + if(G&&G.shake>0){ctx.translate(rnd(-G.shake,G.shake)*0.5,rnd(-G.shake,G.shake)*0.5);} + drawBG(1); + if(G){ + // 拾取物 + for(const k of G.picks){const bob=Math.sin(k.t*0.1)*1.5;drawSprite(SPR[k.spr],k.x,k.y+bob);} + // 敌机 + for(const e of G.enemies){ + if(e.dead)continue; + ctx.save(); + if(e.beh==="spin"){ctx.translate(e.x,e.y);ctx.rotate(e.rot);ctx.drawImage(e.spr,-e.dw/2,-e.dh/2,e.dw,e.dh);} + else ctx.drawImage(e.spr,e.x-e.dw/2,e.y-e.dh/2,e.dw,e.dh); // 亚像素绘制,移动丝滑 + ctx.restore(); + // 冻结标记:四角冰晶+雪花粒子(不再用矩形染色) + if(e.slow>0){const cx2=Math.round(e.x-e.dw/2),cy2=Math.round(e.y-e.dh/2); + px(cx2,cy2,2,2,"#7fd8ff");px(cx2+e.dw-2,cy2,2,2,"#7fd8ff"); + px(cx2,cy2+e.dh-2,2,2,"#7fd8ff");px(cx2+e.dw-2,cy2+e.dh-2,2,2,"#7fd8ff"); + if(G.time%8===0)spawnPart(e.x+rnd(-4,4),e.y-e.dh/2,"#bfeaff",1);} + // 中毒标记:绿色气泡 + if(e.dot&&G.time%10===0)spawnPart(e.x+rnd(-3,3),e.y,"#9dff5c",1); + // 彻底冻结:冰壳轮廓+冰凌(描边而非实心,避免误认血条) + if(e.frozen>0){ctx.strokeStyle="#bfeaff";ctx.globalAlpha=0.9; + ctx.strokeRect(Math.round(e.x-e.dw/2)-1.5,Math.round(e.y-e.dh/2)-1.5,e.dw+3,e.dh+3);ctx.globalAlpha=1; + px(e.x-e.dw/2-2,e.y-1,2,2,"#fff");px(e.x+e.dw/2,e.y-3,2,2,"#fff");px(e.x-1,e.y-e.dh/2-3,2,2,"#fff");} + // 寄生电浆标记:紫色水蛙脉动 + if(e.leech&&G.time%8<4)px(e.x-2,e.y-2,4,4,"#b05ce8"); + if(e.flash>0){ctx.globalAlpha=0.7;ctx.globalCompositeOperation="lighter";ctx.drawImage(e.spr,Math.round(e.x-e.dw/2),Math.round(e.y-e.dh/2),e.dw,e.dh);ctx.globalAlpha=1;ctx.globalCompositeOperation="source-over";} + // 精英标记:头顶金色小皇冠 + if(e.elite){const bob=Math.sin(G.time*0.1)*1.5; + px(e.x-4,e.y-e.dh/2-5+bob,2,3,"#ffd23d");px(e.x-1,e.y-e.dh/2-7+bob,2,5,"#ffd23d");px(e.x+2,e.y-e.dh/2-5+bob,2,3,"#ffd23d");} + // 受击迷你血条(真实扣减可见;图鉴演示中常驻显示) + if((e.hpt>0&&e.hp0.4?"#5cff9d":"#ff5566");} + } + drawBoss();drawBossFx(); + // 敌弹(圆形光晕烘焙在精灵内) + for(const b of G.eb){ + if(b.scale&&b.scale!==1){const sp=SPR[b.spr]; // 成长弹(花弹) + ctx.save();ctx.translate(Math.round(b.x),Math.round(b.y));ctx.scale(b.scale,b.scale); + ctx.drawImage(sp,-Math.round(sp.width/2),-Math.round(sp.height/2));ctx.restore();} + else drawSprite(SPR[b.spr],b.x,b.y);} + // 玩家子弹 + for(const b of G.pb){ + if(b.spr){ctx.save();ctx.translate(b.x,b.y); + if(b.spin)ctx.rotate(G.time*0.35); + else if(b.homing||b.rot)ctx.rotate(Math.atan2(b.vy,b.vx)+Math.PI/2); + else if(b.t==="b")ctx.rotate(G.time*0.3); + const bsc=b.scale||1,bspr=SPR[b.spr]; + ctx.drawImage(bspr,-bspr.width*bsc/2,-bspr.height*bsc/2,bspr.width*bsc,bspr.height*bsc);ctx.restore();} + else if(b.t==="f"){px(b.x-1.5,b.y-1.5,3,3,pick(["#ff9a3d","#ff5566","#ffd23d"]));} + else if(b.t==="wv"){ // 波动炮:剑气弧光 + const rr=b.r; + ctx.globalCompositeOperation="lighter"; + ctx.globalAlpha=0.75;ctx.strokeStyle="#43b8ff";ctx.lineWidth=4; + ctx.beginPath();ctx.arc(b.x,b.y+rr*0.55,rr,Math.PI+0.55,TAU-0.55);ctx.stroke(); + ctx.globalAlpha=0.9;ctx.strokeStyle="#bfe8ff";ctx.lineWidth=2; + ctx.beginPath();ctx.arc(b.x,b.y+rr*0.55,rr*0.78,Math.PI+0.5,TAU-0.5);ctx.stroke(); + ctx.globalAlpha=1;ctx.strokeStyle="#ffffff";ctx.lineWidth=1; + ctx.beginPath();ctx.arc(b.x,b.y+rr*0.55,rr*0.58,Math.PI+0.45,TAU-0.45);ctx.stroke(); + ctx.globalCompositeOperation="source-over";} + else if(b.t==="rg"){ctx.globalAlpha=0.3;px(b.x-3.5,b.y-11,7,22,"#b98cff");ctx.globalAlpha=1; + px(b.x-2,b.y-8,4,14,"#b98cff");px(b.x-1,b.y-8,2,14,"#fff");} + else{px(b.x-1,b.y-3,2,5,"#7fd8ff");px(b.x-1,b.y-3,1,3,"#fff");} + } + // 激光束 / 电弧(线段式) + for(const l of G.lasers){ + drawSeg(l.x1,l.y1,l.x2,l.y2,l.w,l.col,0.4); + drawSeg(l.x1,l.y1,l.x2,l.y2,Math.max(1,Math.round(l.w*0.4)),"#ffffff",0.9); + l.life--; + } + G.lasers=G.lasers.filter(l=>l.life>0); + for(const z of G.zaps){ + for(let i=0;i0&&G.time%6<3)){ + drawSprite(SPR[SHIPS[G.ship].spr],p.x,p.y); + // 尾焰 + const fl=G.time%8<4?3:5; + px(p.x-1,p.y+7,2,fl,"#ff9a3d");px(p.x-0.5,p.y+7,1,fl-1,"#fff4d9"); + if(G.odActive>0){ctx.globalAlpha=0.4;px(p.x-8,p.y-4,1,12,"#41e6ff");px(p.x+7,p.y-4,1,12,"#41e6ff");ctx.globalAlpha=1;} + if(G.shield>0){ctx.strokeStyle="#43e8ff";ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2; + ctx.beginPath();ctx.arc(p.x,p.y,11,0,TAU);ctx.stroke();ctx.globalAlpha=1;} + if(p.focus){px(p.x-1,p.y-1,3,3,"#ff3355");px(p.x-0.5,p.y-0.5,2,2,"#fff"); + ctx.strokeStyle="rgba(255,51,85,.5)";ctx.beginPath();ctx.arc(p.x,p.y,6,0,TAU);ctx.stroke();} + } + // 粒子与浮字(spark 粒子渲染为线段) + for(const q of G.parts){ctx.globalAlpha=clamp(q.life/20,0,1); + if(q.spark){ctx.strokeStyle=q.col;ctx.beginPath();ctx.moveTo(q.x,q.y);ctx.lineTo(q.x-q.vx*2.2,q.y-q.vy*2.2);ctx.stroke();} + else px(q.x,q.y,q.sz,q.sz,q.col); + ctx.globalAlpha=1;} + ctx.textAlign="center"; + for(const t of G.texts){ctx.globalAlpha=clamp(t.life/30,0,1);ctx.font="6px 'Press Start 2P',monospace";ctx.fillStyle=t.col;ctx.fillText(t.t,t.x,t.y);ctx.globalAlpha=1;} + if(!G.demoSim)drawHUD(); + // 横幅 + if(G.bannerT>0){ + const a=clamp(G.bannerT/30,0,1); + ctx.globalAlpha=Math.min(1,a); + ctx.font="bold 13px 'Microsoft YaHei',monospace"; + ctx.fillStyle=G.banner.col;ctx.textAlign="center"; + ctx.fillText(G.banner.t,W/2,H*0.38); + ctx.font="7px 'Press Start 2P',monospace";ctx.fillStyle="#7f8cb8"; + if(G.mutator&&G.bannerT>60)ctx.fillText(MUTS[G.mutator].ds,W/2,H*0.38+16); + ctx.globalAlpha=1; + } + if(G.flash>0){ctx.globalAlpha=G.flash/18;ctx.fillStyle="#fff";ctx.fillRect(0,0,W,H);ctx.globalAlpha=1;} + } + ctx.restore(); +} +function drawHUD(){ + ctx.textAlign="left";ctx.font="7px 'Press Start 2P',monospace"; + ctx.fillStyle="#dfe8ff";ctx.fillText("SC "+G.score,5,10); + ctx.fillStyle="#ffcf5c";ctx.textAlign="right";ctx.fillText("✦"+G.coins,W-5,10); + ctx.textAlign="left"; + ctx.fillStyle="#7f8cb8"; + ctx.fillText(G.mode==="endless"?"WAVE "+G.wave:G.mode==="rush"?"BOSS RUSH":"STAGE "+G.stage+"-"+G.wave,5,20); + if(G.combo>4){ctx.fillStyle="#ff9a3d";ctx.textAlign="right"; + ctx.fillText("x"+(1+Math.min(2,G.combo*0.02)).toFixed(1)+" COMBO "+G.combo,W-5,20);ctx.textAlign="left";} + // 结构值血条(绿→红)+ 备用机体 + 炸弹 + const hpw=62,pc2=clamp(G.hp/G.maxHp,0,1); + px(4,H-12,hpw+2,6,"#0d1226"); + px(5,H-11,hpw,4,"#3a1020"); + px(5,H-11,hpw*pc2,4,pc2>0.5?"#5cff9d":pc2>0.25?"#ffcf5c":"#ff5566"); + for(let i=0;i0?"#fff":"#ff9a3d"); + if(G.odActive>0){ctx.fillStyle="#ff9a3d";ctx.textAlign="center";ctx.fillText("暴走 OVERDRIVE",W/2,30);ctx.textAlign="left";} + // 主武器装备栏图标 + for(let i=0;i0){G.hitstop--;return;} + if(G.shake>0)G.shake*=0.85;if(G.shake<0.3)G.shake=0; + if(G.flash>0)G.flash--; + if(G.bannerT>0)G.bannerT--; + if(G.comboT>0){G.comboT--;if(G.comboT===0)G.combo=0;} + updatePlayer(); + if(G.demoSim)demoSpawn();else updateSpawner(); + for(const e of G.enemies){e.uid=e.uid||_uid++;updateEnemy(e);} + G.enemies=G.enemies.filter(e=>!e.dead); + updateBoss(); + updateBullets(); + collide(); + for(const q of G.parts){ + if(q.suck){q.vx+=(q.suck.x-q.x)*0.09;q.vy+=(q.suck.y-q.y)*0.09;} + else{q.vx*=0.95;q.vy*=0.95;} + if(q.grav)q.vy+=q.grav; + q.x+=q.vx;q.y+=q.vy;q.life--;} + G.parts=G.parts.filter(q=>q.life>0); + for(const t of G.texts){t.y-=0.5;t.life--;} + G.texts=G.texts.filter(t=>t.life>0); +} +let last=0,acc=0; +function loop(ts){ + requestAnimationFrame(loop); + const dt=Math.min(100,ts-last);last=ts; + MUS.tick(); + if(UI==="play"||UI==="demoSim"){ + acc+=dt; + let steps=0; + while(acc>=1000/60&&steps<4){update();acc-=1000/60;steps++;} + if(steps===4)acc=0; + } + render(); +} +requestAnimationFrame(loop); diff --git a/js/sprites.js b/js/sprites.js new file mode 100644 index 0000000..7730f34 --- /dev/null +++ b/js/sprites.js @@ -0,0 +1,187 @@ +"use strict"; +/* ================= 像素精灵工坊 ================= */ +function bake(rows,pal,scale){ + const w=Math.max(...rows.map(r=>r.length)),h=rows.length,s=scale||1; + const c=document.createElement("canvas");c.width=w*s;c.height=h*s; + const g=c.getContext("2d"); + for(let y=0;yMath.round(a+(h-a)*l); + o[k]=`rgb(${mix(94,255)},${mix(64,232)},${mix(10,150)})`; + } + return o; +} +SPR.e_scout=bake(["...KKKK...","..KTTTTK..",".KTTKKTTK.",".KTKTTKTK.","KTTTTTTTTK","KWKTTTTKWK",".K.KTTK.K.","...KDDK...","....KK...."],goldify(EPAL.scout)); +SPR.e_weaver=bake(["....KKKK....","..KKMMMMKK..",".KMMKMMKMMK.","KMMKMMMMKMMK","KMKMMMMMMKMK","KMMMMKKMMMMK",".KMMKMMKMMK.","..KM.MM.MK..","...KMMMMK...","....KKKK...."],goldify(EPAL.weaver)); +SPR.e_gunship=bake(["......KKKK......",".....KSSSSK.....","....KSSSSSSK....","...KSSKSSKSSK...","..KSSSSSSSSSSK..",".KSKSSSSSSSSKSK.",".KSSKSKSSKSKSSK.","KSSSSSSSSSSSSSSK","KSKSSKSSSSKSSKSK","KSSSKSKSSKSKSSSK",".KSKSSKSSKSSKSK.","..KK.KSXXSK.KK..",".....KSXXSK.....","......KKKK......"],goldify(EPAL.gunship)); +SPR.e_kami=bake(["....KK....","...KRRK...","..KRRRRK..","..KRQQRK..",".KRRQQRRK.",".KRQQQQRK.","KRRQQQQRRK","K.RQQQQR.K","..KRRRRK..","...KKKK..."],goldify(EPAL.kami)); +SPR.e_split=bake(["....KKKK....","..KKPPPPKK..",".KPPKPPKPPK.","KPPPPPPPPPPK","KPKPPKKPPKPK","KPPPKPPKPPPK",".KPPPPPPPPK.","..KPKPPKPK..","...KPPPPK...","....KKKK...."],goldify(EPAL.split)); +SPR.e_sniper=bake([".....KK.....","....KSSK....","...KSSSSK...","..KSKSSKSK..","..KSSCCSSK..",".KSSSCCSSSK.","KSKSSCCSSKSK","KSSSKSSKSSSK",".KSK.KK.KSK.","..K..KK..K..","..K.K..K.K..",".....KK....."],goldify(EPAL.sniper)); +SPR.e_mine=bake(["...K..K...","....KK....","..KGGGGK..",".KGGGGGGK.","KKGKGGKGKK","KGGGGGGGGK",".KGGGGGGK.","..KGGGGK..","....KK....","...K..K..."],goldify(EPAL.mine)); +SPR.e_splitter=bake(["....KKKK....","..KKPPPPKK..",".KPPKPPKPPK.","KPPPPPPPPPPK","KPKPPKKPPKPK","KPPPKPPKPPPK",".KPPPPPPPPK.","..KPKPPKPK..","...KPPPPK...","....KKKK...."],goldify(EPAL.split)); +/* ---- 弹幕 / 道具 ---- */ +SPR.msl=bake(["..K..",".KWK.","KWWWK","KWKWK",".KKK."],{K:"#14142b",W:"#c8d8ff"}); +SPR.blade=bake(["...K...","..KCK..",".KCCCK.","KCCXCCK",".KCCCK.","..KCK..","...K..."],{K:"#0d2a1a",C:"#5cff9d",X:"#eafff0"}); +/* 9×9 圆弹:H=半透明圆形光晕 C=彩色外壳 W=白芯 */ +function orb(c1,c2){ + const halo=c2.length===7?c2+"40":c2; + return bake([ + "...HHH...", + ".HHCCCHH.", + ".HCCWCCH.", + "HCCCCCCCH", + "HCWWWWWCH", + "HCCCCCCCH", + ".HCCWCCH.", + ".HHCCCHH.", + "...HHH..."],{H:halo,W:"#ffffff",C:c2}); +} +const BHALO={orbR:"#ff5566",orbP:"#b05ce8",orbG:"#5cff9d",orbO:"#ff9a3d",dartB:"#3a8cd8",flowr:"#ff8fc8",dartS:"#cfe4ff",dartP:"#b98cff"}; +SPR.dartB=bake(["KCCCK",".CCC.",".CWC.","..C.."],{K:"#0d2433",C:"#3a8cd8",W:"#cfeaff"}); // 蓝色锥形敌弹(Boss弹幕) +SPR.flowr=bake([ +"....P....",".P..P..P.","..PPPPP..",".PPWWWPP.","PPPWWWPPP", +".PPWWWPP.","..PPPPP..",".P..P..P.","....P...."], +{P:"#ff8fc8",W:"#ffffff"}); // 太空原种:花弹(圆形八刺,会逐渐变大) +SPR.dartS=bake(["SWS","SWS","SWS","SWS","SWS","SWS",".S.",".W."], +{S:"#cfe4ff",W:"#ffffff"}); // 夺权者X:银色高弹速长钉 +SPR.dartP=bake(["KPPPK",".PPP.",".PWP.","..P.."],{K:"#2a1040",P:"#b98cff",W:"#ead9ff"}); // 暗黑魔碟:紫锥弹 +SPR.orbR=orb("#ffe9e9","#ff5566");SPR.orbP=orb("#f4e9ff","#b05ce8"); +SPR.orbG=orb("#e9ffe9","#5cff9d");SPR.orbO=orb("#fff4d9","#ff9a3d"); +SPR.plasma=orb("#e9f8ff","#43b8ff"); +SPR.gem=bake(["..KK..",".KCCK.","KCWWCK","KCCCCK",".KCCK.","..KK.."],{K:"#0d2a33",C:"#43e8ff",W:"#eaffff"}); +SPR.coin=bake([".KKKK.","KYWWYK","KWYYWK","KYWYYK","KYYYYK",".KKKK."],{K:"#33240a",Y:"#ffcf5c",W:"#fff4d9"}); +SPR.heart=bake([".KK.KK.","KRRKRRK","KRRRRRK",".KRRRK.","..KRK..","...K..."],{K:"#330d14",R:"#ff5566"}); +SPR.bombp=bake(["...K...","..KOK..",".KKKKK.","KBBBBBK","KBBWBBK","KBBBBBK",".KKKKK."],{K:"#14142b",B:"#4a5a8c",W:"#dfe8ff",O:"#ff9a3d"}); +SPR.odc=bake(["..KK..",".KOOK.","KOWWOK","KOWOOK",".KOOK.","..KK.."],{K:"#33240a",O:"#ff9a3d",W:"#fff4d9"}); +/* ---- 新武器弹体 / 僚机精灵 ---- */ +SPR.cres=bake(["..KKK...",".KCCCK..","KCCK....","KCK.....","KCK.....","KCCK....",".KCCCK..","..KKK..."],{K:"#0d2a1a",C:"#5cff9d"}); +SPR.fiss=bake(["..KK..",".KGGK.","KGWWGK","KGGGGK",".KGGK.","..KK.."],{K:"#0d2a0d",G:"#5cff9d",W:"#eafff0"}); +SPR.frost=bake(["..K..",".KCK.","KCWCK","KCCCK",".KCK.","..K.."],{K:"#0d2433",C:"#7fd8ff",W:"#ffffff"}); +SPR.sat=bake(["..KKK..",".KCCCK.","KCCWCCK","KCW WC K".replace(" ",""),"KCCWCCK",".KCCCK.","..KKK.."],{K:"#0d2433",C:"#43e8ff",W:"#ffffff"}); +SPR.bot=bake(["..K..",".KGK.","KGWGK",".KGK.","..K.."],{K:"#0d2a0d",G:"#5cff9d",W:"#eafff0"}); +SPR.ndl=bake([".K.","KCK","KWK","KCK","KCK","KCK",".K."],{K:"#142a0d",C:"#9dff5c",W:"#f0ffe0"}); +SPR.bub=bake(["...KKK...",".KKBBBK K".replace(" ",""),"KBBBWBBBK","KBWWWWBBK","KBWWBWBBK","KBBBBBBBK",".KKBBBK K".replace(" ",""),"...KKK..."],{K:"#0d2433",B:"#3a8cd8",W:"#cfeaff"}); +SPR.met=bake(["..KK..",".KOOK.","KOOOOK","KOWOOK",".KOOK.","..KK..","...K..."],{K:"#331d0a",O:"#ff9a3d",W:"#ffd23d"}); +SPR.boomer=bake(["K...K","KPKPK",".PPP.","..K.."],{K:"#1a0d26",P:"#c86bff"}); // 亮紫回旋镖,清晰可辨 +SPR.sing=bake(["..KKK..",".KPPPK.","KPXXXP K".replace(" ",""),"KPXXXP K".replace(" ",""),"KPPPPPK",".KPPPK.","..KKK.."],{K:"#140d1a",P:"#7b2fd0",X:"#0d0514"}); +SPR.waveb=bake(["....K....","..K.C.K..","K.C.C.C.K","..K.C.K..","....K...."],{K:"#0d1a33",C:"#43b8ff"}); +SPR.holyo=bake(["..KKK..",".KYWYK.","KYW W YK".replace(/ /g,""),"KYW W YK".replace(/ /g,""),".KYWYK.","..KKK.."],{K:"#33240a",Y:"#ffcf5c",W:"#fff4d9"}); +SPR.thnd=bake(["..KK",".KWK","KWK.","KWK.",".KWK",".KK.","..K."],{K:"#33240a",W:"#ffe95c"}); +SPR.grv=bake(["..KKK..",".K...K.","K..P..K","K.PXP.K" ,"K..P..K",".K...K.","..KKK.."],{K:"#1a0d2a",P:"#b05ce8",X:"#2a0d3a"}); +/* ---- 辨识度重设计:新弹体精灵 ---- */ +SPR.trcr=bake([".K.","KYK","KYK","KWK","KYK","KYK",".K."],{K:"#33240a",Y:"#ffcf5c",W:"#fff4d9"}); // 火神金色曳光弹 +SPR.pelt=bake(["KOOK","OWWO","OWWO","KOOK"],{K:"#331d0a",O:"#ff9a3d",W:"#fff4d9"}); // 霰弹炽热弹丸 +SPR.shard=bake(["K..C..","KCCKC.","KCWCKK","KCCCK.","K..C.."],{K:"#0d2433",C:"#7fd8ff",W:"#ffffff"}); // 冰晶霰屑 +SPR.icelance=bake(["...K...","..KCK..","..KCK..",".KCWK.",".KCCK.",".KCCK.","..KCK..","..KCK..","...K..."],{K:"#0d2433",C:"#7fd8ff",W:"#ffffff"}); // 贯穿冰枪 +SPR.msl2=bake(["..K..",".KYK.","KYGYK","KYGYK",".KGK.","K.K.K"],{K:"#2a240d",Y:"#ffe95c",G:"#8ca86a"}); // 蜂巢小导弹 +SPR.amsl=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0763e",W:"#fff4d9"}); // 空爆重型导弹 +SPR.cmsl=bake(["...KK...","..KRRK..",".KRRRRK.","KRWRRWRK",".KRRRRK.",".KRRRRK.","..KRRK..",".K.KK.K."],{K:"#1a0d0d",R:"#e0393e",W:"#ffd23d"}); // 斩首巡航导弹 +SPR.broad=bake([".KKK.","KBBBK","KBWBK","KBBBK",".KKK."],{K:"#241a08",B:"#c8923e",W:"#fff4d9"}); // 侧舷炮弹 +SPR.flare=bake(["..KK..",".KWWK.","KWWWWK","KWWWWK",".KWWK.","..KK.."],{K:"#33240a",W:"#ffd23d"}); // 热诱弹 +SPR.sticky=bake([".KKKK.","KGKKGK","KGRRGK","KGRRGK","KGKKGK",".KKKK."],{K:"#142a0d",G:"#5cff9d",R:"#ff5566"}); // 粘着炸弹 +/* ---- 新武器弹体精灵(差异化造型) ---- */ +SPR.shuri=bake(["...K...","..KCK..",".KCCK..","KCCWCCK","..KCCK.","..KCK..","...K..."],{K:"#0d2433",C:"#41e6ff",W:"#ffffff"}); // 电弧手里剑:青白四芒星 +SPR.voltb=bake([".K.","KGK","GWG","KGK","KGK",".K."],{K:"#142a0d",G:"#b8ff3d",W:"#ffffff"}); // 电能弹:荧光绿+白电纹 +SPR.dawnsh=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0393e",W:"#fff4d9"}); // 黎明重炮弹:炽红重壳 +SPR.protb=bake([".K.","KCK","KWK","KCK","KCK","KCK",".K."],{K:"#140a0a",C:"#ff4a1f",W:"#ffc64a"}); // 协议速射弹:黑红橙 +SPR.protoh=orb(null,"#ff5a2e"); // 协议重炮弹:黑壳橙芯大能量球 +SPR.neb=orb(null,"#8a5cff"); // 星云奥秘:紫蓝奥术球 +SPR.star4=bake(["..K..","..K..","KKBKK","..K..","..K.."],{K:"#e0b3ff",B:"#ffffff"}); // 星云绽放的小星星 +/* 12 种僚机 */ +SPR.dGun=bake(["...K...","..KWK..",".KWWWK.","KWKWKWK","..KWK.."],{K:"#14142b",W:"#c8d8ff"}); +SPR.dGun2=bake(["K..K..K","KKK.KKK",".KWKWK.","..KWK..","...K..."],{K:"#14142b",W:"#9fd8ff"}); +SPR.dSnip=bake(["..K..",".KWK.",".KCK.",".KWK.","KWKWK"],{K:"#0d2433",W:"#c8d8ff",C:"#43e8ff"}); +SPR.dBeam=bake([".KKKKK.","KCCCCCK","KCW K WCK".replace(/ /g,""),"KCCCCCK",".KKKKK."],{K:"#2a0d1a",C:"#ff6b9d",W:"#fff"}); +SPR.dBomb=bake(["..KKK..",".KO O K.".replace(/ /g,""),"KOOOOOK","KOOOWOK",".KKKKK."],{K:"#241a08",O:"#ff9a3d",W:"#fff4d9"}); +SPR.dFrost=bake(["...K...","..KCK..",".KCCK.","KCCWCK.",".KCCK.","..KCK..","...K..."],{K:"#0d2433",C:"#7fd8ff",W:"#fff"}); +SPR.dTesla=bake(["..KK...",".KWWK..","KWK....",".KWK...","..KWK..","...KK.."],{K:"#33240a",W:"#ffe95c"}); +SPR.dRam=bake(["...K...","..KRK..",".KRRRK.","KRRRRRK",".KRRRK.","..KRK..","...K..."],{K:"#1a0d0d",R:"#e0393e"}); +SPR.dHeal=bake(["..KKK..",".KGGGK.","KGWGWGK","KGGGGGK",".KGGGK.","..KKK.."],{K:"#0d2a1a",G:"#3f8f5f",W:"#eafff0"}); +SPR.dShield=bake([".KKKKK.","KCCBCCK","KCBKBCK","KCCBCCK",".KKKKK."],{K:"#0d2433",C:"#43e8ff",B:"#1a5a8c"}); +SPR.dMsl=bake(["..KKK..",".KWWWK.","KWKWKWK",".KWWWK.","..KKK.."],{K:"#14142b",W:"#ffb84d"}); +SPR.dBlade=bake(["K..K..K",".K.K.K.","..KKK..",".K.K.K.","K..K..K"],{K:"#0d2a1a",W:"#5cff9d"}); +SPR.dSweep=bake(["K.K.K.K","KWKWKWK",".KWWWK.","..KWK..","...K..."],{K:"#1a1206",W:"#ffb03d"}); // 扫射僚机:横排多炮管 +SPR.swep=bake([".W.","WCW","WCW",".K."],{K:"#331d0a",W:"#ffd23d",C:"#ff9a3d"}); // 扫射弹:金黄弹丸 +/* ---- 升级卡片图标(8×8 单色) ---- */ +function icon(rows,color){return bake(rows,{X:color});} +const ICONS={ + gun:icon(["X..X..X.","X..X..X.","XX.XX.XX","XX.XX.XX",".X.X.X..",".X.X.X..","........","........"],"#7fd8ff"), + spread:icon(["X..X..X.","X..X..X.",".X.X.X..",".X.X.X..","..XXX...","..XXX...","...X....","...X...."],"#ffb84d"), + beam:icon(["...XX...","...XX...","...XX...","...XX...","...XX...","...XX...","...XX...","...XX..."],"#43e8ff"), + pulse:icon(["..X..X..",".X..X..X","X..X..X.",".X..X..X","..X..X..",".X..X..X","X..X..X.",".X..X..X"],"#5cff9d"), + rail:icon(["...XX...","..XXXX..",".XXXXXX.","...XX...","...XX...","...XX...","...XX...","...XX..."],"#b98cff"), + laser:icon(["...X....","...X....","..XXX...",".XXXXX.","..XXX...","...X....","...X....","...X...."],"#ff6b9d"), + chain:icon(["....XX..","...XX...","..XX....","..XXXX..","...XX...","..XX....",".XX.....","XX......"],"#ffe95c"), + fire:icon(["..X.X...",".X.X.X..","X.X.X.X.","..X.X...",".X.X.X..","X.X.X.X.","........","........"],"#ff9a3d"), + crit:icon(["X..XX..X",".X.XX.X.","..XXXX..","XXXXXXXX","XXXXXXXX","..XXXX..",".X.XX.X.","X..XX..X"],"#ff5566"), + ghost:icon(["..XXXX..",".XXXXXX.","XX.XX.XX","XXXXXXXX","XXXXXXXX","X.XXXX.X","..X..X..","........"],"#c8d8ff"), + shield:icon([".XXXXXX.","XXXXXXXX","XXXXXXXX","XXXXXXXX",".XXXXXX.","..XXXX..","...XX...","........"],"#43e8ff"), + nano:icon(["...XX...","...XX...","...XX...","XXXXXXXX","XXXXXXXX","...XX...","...XX...","...XX..."],"#5cff9d"), + eng:icon(["XXXX....","..XXXX..","XXXX....","..XXXXXX","XXXX....","..XXXX..","XXXX....","........"],"#9fd8ff"), + mag:icon([".XX..XX.",".XX..XX.",".XX..XX.",".XX..XX.",".XXXXXX.","..XXXX..","........","........"],"#b98cff"), + drone:icon(["...XX...","..XXXX..",".XX.XXX.","XXXXXXXX","XXXXXXXX",".XX.XX..","..XXXX..","...XX..."],"#ffcf5c"), + flame:icon(["...X....","..XX....",".XX.X...",".XXXX...","XXXXXX..","X.XXX.X.",".XXXX...","..XX...."],"#ff9a3d") +}; diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..947fb80 --- /dev/null +++ b/js/ui.js @@ -0,0 +1,340 @@ +"use strict"; +/* ================= 界面流转 ================= */ +const OVLS=["o-title","o-setup","o-hangar","o-help","o-cards","o-pause","o-over","o-codex","o-demo","o-rush","o-slot"]; +function show(id){for(const o of OVLS)$(o).classList.toggle("hide",o!==id);} +const SETUP={mode:"stage",ship:"falcon",diff:0}; +function bestLine(){ + const b=SAVE.best; + $("best-line").textContent=`无尽纪录 ${Math.max(b.e0,b.e1)} · 闯关进度 ${b.stage}/6`; +} +$("b-go").onclick=()=>{AU.init();buildSetup();show("o-setup");}; +$("b-help").onclick=()=>show("o-help"); +$("b-back3").onclick=()=>show("o-title"); +$("b-back1").onclick=()=>show("o-title"); +$("b-back2").onclick=()=>show("o-title"); +$("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");}; +$("b-codex").onclick=()=>{AU.init();buildCodex();show("o-codex");}; +$("b-rush").onclick=()=>{AU.init();buildRush();show("o-rush");}; +$("b-back6").onclick=()=>show("o-title"); +$("b-back4").onclick=()=>{bestLine();show("o-title");}; +$("b-back5").onclick=exitDemoSim; +$("b-start").onclick=()=>startGame(); +if("ontouchstart" in window){const pb=$("tb-pause");pb.style.display="block";pb.onclick=()=>togglePause();} +$("b-resume").onclick=()=>togglePause(); +$("b-restart").onclick=()=>{show("");startGame();}; +$("b-quit").onclick=()=>{bank();G=null;MUS.stop();UI="title";bestLine();show("o-title");}; +$("b-again").onclick=()=>startGame(); +$("b-home").onclick=()=>{UI="title";bestLine();show("o-title");}; +$("mode-pick").addEventListener("click",e=>{ + const c=e.target.closest(".pcard");if(!c)return; + SETUP.mode=c.dataset.m; + for(const el of $("mode-pick").children)el.classList.toggle("sel",el===c); +}); +$("diff-pick").addEventListener("click",e=>{ + const b=e.target.closest("button");if(!b)return; + SETUP.diff=+b.dataset.d; + for(const el of $("diff-pick").children)el.classList.toggle("primary",el===b); +}); +function buildSetup(){ + const box=$("ship-pick");box.innerHTML=""; + for(const k in SHIPS){ + const s=SHIPS[k],owned=SAVE.ships.includes(k); + const d=document.createElement("div"); + d.className="pcard"+(owned?"":" lock")+(SETUP.ship===k?" sel":""); + const c=document.createElement("canvas");c.width=52;c.height=64; + const g=c.getContext("2d");g.imageSmoothingEnabled=false; + const spr=SPR[s.spr];g.drawImage(spr,(52-spr.width*3)/2,(64-spr.height*3)/2,spr.width*3,spr.height*3); + d.appendChild(c); + d.insertAdjacentHTML("beforeend",`
${s.name}
${s.trait}
`+(owned?"":`
✦ ${s.cost} 解锁
`)); + d.onclick=()=>{ + if(!SAVE.ships.includes(k)){ + if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildSetup();} + else SFX.hurt(); + return; + } + SETUP.ship=k;buildSetup(); + }; + box.appendChild(d); + } +} +/* ================= 机库商店 ================= */ +const META=[ + {key:"weapon",name:"主武器校准",ds:"出击时主武器等级 +1",max:2,costs:[300,800]}, + {key:"armor",name:"强化装甲",ds:"出击时生命上限 +1",max:2,costs:[400,1000]}, + {key:"hpup",name:"结构强化",ds:"出击时结构值 +15",max:2,costs:[250,700]}, + {key:"luck",name:"幸运币",ds:"高品质强化概率提升",max:2,costs:[350,900]}, + {key:"wallet",name:"收藏家协议",ds:"星币获取 +15%",max:3,costs:[300,800,2000]}, + {key:"mag2",name:"引力透镜",ds:"拾取范围 +15%",max:2,costs:[200,600]}, + {key:"odstart",name:"超频预载",ds:"出击时超频已充能 50%",max:1,costs:[500]} +]; +function buildShop(){ + $("wallet").textContent=SAVE.coins; + const box=$("shop");box.innerHTML=""; + for(const m of META){ + const lv=SAVE.m[m.key]||0,maxed=lv>=m.max,cost=maxed?0:m.costs[lv]; + const d=document.createElement("div");d.className="sitem"; + d.innerHTML=`
${m.name}
${m.ds}
+
${"■".repeat(lv)}${"□".repeat(m.max-lv)}
+ `; + if(!maxed)d.querySelector("button").onclick=()=>{ + if(SAVE.coins{ + const sec=document.createElement("div"); + sec.innerHTML=`
${title}(${active.length}/${max})· 点击切换装备
`; + const row=document.createElement("div"); + for(const k in owned){ + const d=document.createElement("span"); + const on=active.includes(k); + d.className="litem"+(on?" on":""); + d.textContent=`${dict[k].name} Lv${owned[k]}`; + d.onclick=()=>{ + const i=active.indexOf(k); + if(i>=0){if(active===G.loadout&&active.length===1){SFX.hurt();return;}active.splice(i,1);} + else{if(active.length>=max){SFX.hurt();return;}active.push(k);} + SFX.pick();buildLoadoutUI(); + }; + row.appendChild(d); + } + if(Object.keys(owned).length===0)row.innerHTML='尚未获得'; + sec.appendChild(row);box.appendChild(sec); + }; + mk("主武器",WEAPONS,G.weapons,G.loadout,5); + mk("副武器",SUBS,G.subs,G.subLoadout,5); + mk("僚机",DRONES,G.drones,G.droneLoadout,3); +} +/* ================= 图鉴 ================= */ +function iconCanvas(img,size){ + const c=document.createElement("canvas");c.width=c.height=size; + const g=c.getContext("2d");g.imageSmoothingEnabled=false; + const sc=Math.min(size/img.width,size/img.height); + g.drawImage(img,(size-img.width*sc)/2,(size-img.height*sc)/2,img.width*sc,img.height*sc); + return c; +} +function buildCodex(){ + const box=$("codex");box.innerHTML=""; + const total=MAINS.length+SUB_KEYS.length+DRONE_KEYS.length+PAS_KEYS.length; + $("codex-t").textContent=total; + $("codex-n").textContent=Object.keys(SAVE.codex).length; + const mk=(title,keys,dict,kind)=>{ + const h=document.createElement("div");h.className="cx-sec";h.textContent=title;box.appendChild(h); + const grid=document.createElement("div");grid.className="cx-grid"; + for(const k of keys){ + const def=dict[k]; + const d=document.createElement("div"); + d.className="cx-item"; + const img=def.icon?ICONS[def.icon]:SPR[def.spr]; + d.appendChild(iconCanvas(img,28)); + d.insertAdjacentHTML("beforeend",`
${def.name}
`); + d.onclick=()=>{SFX.pick();startDemoSim(kind,k);}; + grid.appendChild(d); + } + box.appendChild(grid); + }; + mk("主武器",MAINS,WEAPONS,"m"); + mk("副武器",SUB_KEYS,SUBS,"s"); + mk("僚机",DRONE_KEYS,DRONES,"d"); + mk("被动强化",PAS_KEYS,PASSIVES,"p"); + $("codex-n").textContent=total; // 图鉴全量直接展示,无需收集 +} +document.addEventListener("visibilitychange",()=>{if(document.hidden&&UI==="play")togglePause();}); +function bank(){ + if(!G||G.banked)return;G.banked=true; + SAVE.coins+=G.coins; + if(G.mode!=="rush"){const bk=(G.mode==="endless"?"e":"s")+G.diff; + SAVE.best[bk]=Math.max(SAVE.best[bk]||0,G.score);} + persist(); +} +/* ---- Boss 挑战模式 ---- */ +function buildRush(){ + SAVE.rush=SAVE.rush||{}; + const ra=$("rush-anim"); // 老虎机开关(持久化) + if(ra){ra.checked=!!SAVE.rushAnim;ra.onchange=()=>{SAVE.rushAnim=ra.checked?1:0;persist();};} + const el=$("rush-pick");el.innerHTML=""; + BOSSES.forEach((b,i)=>{ + const best=SAVE.rush[i]; + const d=document.createElement("div");d.className="pcard"; + d.innerHTML=`
${b.name}${best?" ✓":""}
+
${b.sub}
${best?("最快 "+Math.floor(best/60)+":"+("0"+best%60).slice(-2)):"未击破"}
`; + d.onclick=()=>startRush(i); + el.appendChild(d); + }); +} +function rollRushBuild(){ // 随机武装:3主武Lv3 + 3副武Lv3 + 2僚机Lv2 + const sh=a=>{const x=a.slice();for(let i=x.length-1;i>0;i--){const j=rndi(0,i),t2=x[i];x[i]=x[j];x[j]=t2;}return x;}; + return {m:sh(Object.keys(WEAPONS)).slice(0,3),s:sh(Object.keys(SUBS)).slice(0,3),d:sh(Object.keys(DRONES)).slice(0,2)}; +} +function startRush(i){ + AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume(); + const build=rollRushBuild(); + if(SAVE.rushAnim)playSlot(build,()=>startRushGo(i,build)); // 老虎机动画抽取 + else startRushGo(i,build); // 直接随机 +} +function startRushGo(i,build){ + newRun("rush","falcon",0); + G.weapons={};build.m.forEach(k=>G.weapons[k]=3);G.loadout=build.m.slice(); + G.subs={};build.s.forEach(k=>G.subs[k]=3);G.subLoadout=build.s.slice(); + G.drones={};build.d.forEach(k=>G.drones[k]=2);G.droneLoadout=build.d.slice(); + G.xpNeed=1e9; // 挑战模式无升级打断 + G.rushIdx=i;G.rushT0=0; + UI="play";show("");acc=0; + MUS.start(true); + banner("BOSS 挑战 — "+BOSSES[i].name,"#ff5566",130); + G.rushT0=G.time; + startBoss(); +} +/* ---- 老虎机抽奖动画 ---- */ +let slotTimers=[]; +function playSlot(build,cb){ + UI="slot";show("o-slot"); + const row=$("slot-row");row.innerHTML=""; + $("slot-hint").textContent=" "; + const items=[ + ...build.m.map(k=>({cat:"主武器",pool:WEAPONS,key:k})), + ...build.s.map(k=>({cat:"副武器",pool:SUBS,key:k})), + ...build.d.map(k=>({cat:"僚机",pool:DRONES,key:k}))]; + let done=0; + items.forEach((it,idx)=>{ + const d=document.createElement("div");d.className="slotbox"; + d.innerHTML=`
${it.cat}
???
`; + row.appendChild(d); + const sn=d.querySelector(".sn"),names=Object.values(it.pool).map(x=>x.name); + const iv=setInterval(()=>{sn.textContent=names[rndi(0,names.length-1)];},60); + slotTimers.push(iv); + setTimeout(()=>{clearInterval(iv); + sn.textContent=it.pool[it.key].name;sn.classList.add("hit"); + SFX.graze();done++; + if(done>=items.length){$("slot-hint").textContent="— 出击 —";setTimeout(cb,700);} + },700+idx*280); + }); +} +function endRun(win){ + if(UI==="over")return; + bank();MUS.stop(); + UI="over"; + $("over-title").textContent=win?"★ 任务完成 ★":"任务失败"; + $("over-title").style.color=win?"#ffcf5c":"#ff5566"; + const mins=Math.floor(G.time/3600),secs=Math.floor(G.time/60)%60; + $("over-stats").innerHTML= + `最终得分${G.score} + ${G.mode==="endless"?"抵达波次":G.mode==="rush"?"挑战目标":"抵达关卡"}${G.mode==="endless"?G.wave:G.mode==="rush"?BOSSES[G.rushIdx].name:G.stage+"-"+G.wave} + 击坠数${G.kills} + 强化等级Lv.${G.level} + 存活时间${mins}分${secs}秒 + 持有军械${Object.keys(G.weapons).length}主/${Object.keys(G.subs).length}副/${Object.keys(G.drones).length}机`; + $("over-coins").textContent=G.coins; + setTimeout(()=>show("o-over"),win?400:900); +} +/* ================= 三选一卡片 ================= */ +function offerCards(bossRare){ + if(!G||G.p.dead>0&&!bossRare)return; + G.bossCard=!!bossRare; + const opts=rollCards(bossRare); + G.cardOpts=opts; + const box=$("cards");box.innerHTML=""; + opts.forEach((c,i)=>{ + let nm,ds,rare,img; + if(c.kind==="wlvl"){const w=WEAPONS[c.key],lv=G.weapons[c.key];nm=w.name;rare=w.rare;ds=`主武器强化 Lv.${lv} → Lv.${lv+1}`;img=w.icon?ICONS[w.icon]:SPR[w.spr];} + else if(c.kind==="weapon"){const w=WEAPONS[c.key];nm="主武器·"+w.name;rare=w.rare;ds=w.ds;img=w.icon?ICONS[w.icon]:SPR[w.spr];} + else if(c.kind==="slvl"){const u=SUBS[c.key],lv=G.subs[c.key];nm=u.name;rare=u.rare;ds=`副武器强化 Lv.${lv} → Lv.${lv+1}`;img=u.icon?ICONS[u.icon]:SPR[u.spr];} + else if(c.kind==="sub"){const u=SUBS[c.key];nm="副武器·"+u.name;rare=u.rare;ds=typeof u.ds==="function"?u.ds(0):u.ds;img=u.icon?ICONS[u.icon]:SPR[u.spr];} + else if(c.kind==="dlvl"){const u=DRONES[c.key],lv=G.drones[c.key];nm=u.name;rare=u.rare;ds=`僚机强化 Lv.${lv} → Lv.${lv+1}`;img=SPR[u.spr];} + else if(c.kind==="drone"){const u=DRONES[c.key];nm="僚机·"+u.name;rare=u.rare;ds=typeof u.ds==="function"?u.ds(0):u.ds;img=SPR[u.spr];} + else if(c.kind==="upg"){const u=PASSIVES[c.key];nm=u.name;rare=u.rare;ds=u.ds(G.u[c.key]);img=u.icon?ICONS[u.icon]:SPR[u.spr];} + else{rare=0; + if(c.key==="heal"){nm="紧急修复";ds="生命 +1";img=SPR.heart;} + else if(c.key==="coins"){nm="星币补给";ds="星币 +50";img=SPR.coin;} + else{nm="超频充能";ds="超频立即充满";img=SPR.odc;}} + const d=document.createElement("div"); + d.className="ucard r"+rare; + const cv2=document.createElement("canvas");cv2.width=40;cv2.height=40; + const g=cv2.getContext("2d");g.imageSmoothingEnabled=false; + const sc=Math.min(40/img.width,40/img.height); + g.drawImage(img,(40-img.width*sc)/2,(40-img.height*sc)/2,img.width*sc,img.height*sc); + d.appendChild(cv2); + d.insertAdjacentHTML("beforeend", + `
${i+1}
${nm}
+
${["普通","稀有","史诗"][rare]}
${ds}
`); + d.onclick=()=>pickCard(i); + box.appendChild(d); + }); + UI="cards";show("o-cards");SFX.level(); +} +function pickCard(i){ + if(UI!=="cards"||!G.cardOpts||!G.cardOpts[i])return; + const c=G.cardOpts[i];G.cardOpts=null; + if(c.kind==="wlvl")G.weapons[c.key]++; + else if(c.kind==="weapon"){G.weapons[c.key]=1;if(G.loadout.length<5)G.loadout.push(c.key);banner("获得 "+WEAPONS[c.key].name,"#b98cff",80);} + else if(c.kind==="slvl")G.subs[c.key]++; + else if(c.kind==="sub"){G.subs[c.key]=1;if(G.subLoadout.length<5)G.subLoadout.push(c.key);banner("获得 "+SUBS[c.key].name,"#b98cff",80);} + else if(c.kind==="dlvl")G.drones[c.key]++; + else if(c.kind==="drone"){G.drones[c.key]=1;if(G.droneLoadout.length<3)G.droneLoadout.push(c.key);banner("获得 "+DRONES[c.key].name,"#b98cff",80);} + else if(c.kind==="upg"){ + G.u[c.key]++; + if(c.key==="nano"){G.maxHp+=25;G.hp=Math.min(G.maxHp,G.hp+40);} + if(c.key==="shield")G.shield=Math.min(shieldCap(),G.shield+1); + }else{ + if(c.key==="heal")G.lives=Math.min(G.stat.maxLives,G.lives+1); + if(c.key==="coins")addCoins(50); + if(c.key==="od")G.od=100; + } + if(c.key&&c.kind!=="fill"){SAVE.codex[c.key]=true;persist();} // 图鉴收录 + SFX.pick(); + if(G.bossCard){G.bossCard=false;UI="play";show("");} + else{G.cardQ=(G.cardQ||1)-1; + if(G.cardQ>0)offerCards(false); + else{UI="play";show("");}} +} +/* ================= 图鉴:全屏实战模拟 ================= + 复用真实战斗引擎:只装备选定军械,杂兵+射手+精英持续来袭 */ +function startDemoSim(kind,key){ + const def=kind==="m"?WEAPONS[key]:kind==="s"?SUBS[key]:kind==="p"?PASSIVES[key]:DRONES[key]; + newRun("stage","falcon",0); + G.p.inv=0; // 图鉴演示:取消出生保护闪烁 + const lv=3; + G.demoSim={kind,key}; + if(kind==="m"){G.weapons={};G.weapons[key]=lv;G.loadout=[key];} + else if(kind==="s"){G.weapons={vulcan:1};G.loadout=["vulcan"];G.subs={};G.subs[key]=lv;G.subLoadout=[key];} + else if(kind==="d"){G.weapons={};G.loadout=[];G.drones={};G.drones[key]=lv;G.droneLoadout=[key];} + else{G.weapons={vulcan:2};G.loadout=["vulcan"];G.u[key]=3; + if(key==="nano"){G.maxHp+=75;G.hp=G.maxHp;} + if(key==="shield")G.shield=shieldCap();} + $("demo-name").textContent=def.name; + const ds=typeof def.ds==="function"?def.ds(lv):def.ds; + $("demo-ds").textContent=(kind==="m"?"主武器 · ":kind==="s"?"副武器 · ":kind==="d"?"僚机 · ":"被动 · ")+ds; + UI="demoSim";show("o-demo");acc=0; + banner(def.name,"#41e6ff",90); +} +function exitDemoSim(){G=null;UI="codex";buildCodex();show("o-codex");} +function demoSpawn(){ + const D=G.demoSim; + D.t=(D.t||0)-1; + if(D.t>0||G.enemies.length>=10)return; + D.t=Math.max(30,52-Math.floor(G.time/900)); // 渐进增密 + const r=Math.random(); + if(r<0.5)spawnEnemy(pick(["scout","scout","weaver","kami"]),rnd(20,W-20),-12,false); // 杂兵为主 + else if(r<0.84)spawnEnemy(pick(["gunship","splitter","sniper","mine"]),rnd(20,W-20),-12,false); // 射手少数 + else spawnEnemy(pick(["gunship","weaver","splitter"]),rnd(20,W-20),-12,true); // 精英 +} +bestLine();