starraid/js/render.js
JOJO 1d4e8597bb 激光钢琴手感修正:降速/扫射安全区/蓄力前摇/键距统一/舰桥随壳裂开
- 降速:mainT2->4、单键hotT1->2/ks.t(1/1)->(2/3)、齐射hotT4->6、模式间隔(8/14)->(14/22),密度609->约135束/10s
- 顺扫止于18号键/反扫止于3号键:尽头3键永不发射,玩家总有躲藏区
- 前摇动画:即将发射的琴键下方画圆形蓄力圈(随hotT进度放大变亮)
- 键距9.5->10整数化:消除取整导致的'两个一组'视觉;光束kx计算同步
- 舰桥从中线剖开两半各随半壳平移(原来整体跟左壳跑);壳沿铆钉同步分半
2026-08-05 21:38:23 +08:00

535 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
/* ================= 背景:星空与星云 ================= */
const 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;i<pts.length;i+=2)ctx.lineTo(pts[i],pts[i+1]);ctx.closePath();ctx.fill();}
function circ(x,y,r,col){ctx.fillStyle=col;ctx.beginPath();ctx.arc(x,y,r,0,TAU);ctx.fill();}
function drawBoss(){
const b=G.boss;if(!b)return;
// 变形动画五个新Boss播专属变身mp=进度);其余通用收缩展开 + 白光衰减
let ph2=b.phase,sc=1,ox=0,oy=0,flashA=0,mp=null;
if(b.transT>0){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){ // 太空原种:同一躯体连续变形——裂皮→粉化→红眼→身后展翅(无部件凭空出现)
drawProgenitor(t,fl,mp!==null?mp:(ph2===0?0: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*(37+jt*95),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{ // 暗黑魔碟:核心+圆环;变形=圆环前后裂开、两半各自收缩成犄角(同一几何体连续演化);爆甲=壳板飞离、铆钉伸长成刺
let open,fo,eyeG;
if(b.phase===0){open=0;fo=0;eyeG=0;}
else if(b.phase===1){open=mp!==null?mp:1;fo=0;eyeG=mp!==null?mp:1;}
else{open=1;fo=mp!==null?mp:1;eyeG=1;}
const R=30+open*3; // 外径微扩
circ(0,0,20,"#1a1030");circ(0,0,15,"#12081c");
px(-11,-5,7,6,"#241430");px(4,-5,7,6,"#241430"); // 眼窝常存
if(eyeG>0){ctx.globalAlpha=eyeG; // 赤瞳点亮(非凭空出现)
px(-10,-4,5,4,fl||"#ff3355");px(5,-4,5,4,fl||"#ff3355");
px(-9,-3,2,1,"#ffdfe5");px(6,-3,2,1,"#ffdfe5");
ctx.globalAlpha=1;}
const rot=open*0.55; // 外壳下边分开,以顶部为轴外旋,下端打开
for(const sd of[-1,1]){ctx.save();
ctx.translate(0,-R);ctx.rotate(-sd*rot);ctx.translate(0,R); // 绕顶端轴旋转:下端向外打开
const a0=sd<0?Math.PI/2:-Math.PI/2,a1=sd<0?Math.PI*1.5:Math.PI/2; // 左半环/右半环
ctx.strokeStyle="#55336e";ctx.lineWidth=13;
ctx.beginPath();ctx.arc(0,0,R,a0,a1);ctx.stroke();
ctx.strokeStyle="#6e4490";ctx.lineWidth=5;
ctx.beginPath();ctx.arc(0,0,R,a0,a1);ctx.stroke();
const hL=3+open*26; // 半环下端伸长变尖:圆环下半部分延续为朝下的锥刺(同一几何体)
poly([-6.5,R,6.5,R,2.5,R+hL*0.55,0,R+hL,-2.5,R+hL*0.55],"#55336e");
poly([-3.5,R,3.5,R,1.2,R+hL*0.5,0,R+hL*0.8,-1.2,R+hL*0.5],"#6e4490");
for(let k=0;k<3;k++){const aa=a0+(0.35+k*0.2)*(a1-a0);
const rx=Math.cos(aa),ry=Math.sin(aa),tx2=-ry,ty2=rx;
const bl=2+9*fo; // 铆钉→尖刺连续伸长
poly([rx*(R+5)+tx2*2.5,ry*(R+5)+ty2*2.5, rx*(R+5)-tx2*2.5,ry*(R+5)-ty2*2.5, rx*(R+5+bl),ry*(R+5+bl)],fo>0.6?"#c86bff":"#2a1a3a");
if(fo<0.99){ctx.save(); // 外壳板:爆甲时旋转飞离(有运动轨迹,非消失)
ctx.translate(rx*(R+9+fo*22)+tx2*fo*sd*9,ry*(R+9+fo*22)+ty2*fo*sd*9);
ctx.rotate(aa+Math.PI/2+fo*sd*1.7);ctx.globalAlpha=1-fo;
px(-5,-3,10,6,"#55336e");px(-5,-3,10,2,"#6e4490");
ctx.restore();ctx.globalAlpha=1;}}
{const ta=a0+0.5*(a1-a0),bl2=2+6*fo; // 红色炮台:基座常存,爆甲后炮管伸出点亮
ctx.save();ctx.translate(Math.cos(ta)*(R+2),Math.sin(ta)*(R+2));ctx.rotate(ta+Math.PI/2);
px(-2.5,-3,5,4,"#2a1a3a");
px(-1.5,-3-bl2,3,bl2,fo>0.4?(fl||"#ff3355"):"#3a2248");
ctx.restore();}
for(let k=0;k<2;k++){const aa2=a0+(0.35+k*0.3)*(a1-a0)+Math.sin(t*0.012+k*2)*0.05; // 环面光点
px(Math.cos(aa2)*R-2,Math.sin(aa2)*R-2,4,4,"#c86bff");}
ctx.restore();}
circ(0,-R,4,"#6e4490"); // 顶部铰链节
}
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.phase<thr.length?(TUBEC[b.phase+1]||"#3a1020"):"#3a1020"); // 底:下一管或暗底
const hi=b.phase===0?1:thr[b.phase-1],lo=b.phase<thr.length?thr[b.phase]:0;
const pc=clamp((ratio-lo)/(hi-lo),0,1);
px(bx,10,w*pc,4,TUBEC[b.phase]||b.col); // 当前管
for(const tt of thr)px(bx+Math.round(w*tt)-1,9,2,6,"#0d1226"); // 管界刻痕
if(b.transT>0&&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 drawProgenitor(t,fl,tf){ // 太空原种同一躯体连续演化tf 0=虫蛹→1=完全体),无部件凭空出现
ctx.rotate(Math.sin(t*0.05)*0.05);
const wE=Math.min(1,tf*1.2); // 展翅进度
// —— 翅膀:折叠时收在躯体后方偏上(被身体完全遮住),自背后向两侧展开 ——
ctx.save();
ctx.translate(0,-12);
ctx.scale(0.05+0.95*wE,(0.55+0.45*wE)*(1+Math.sin(t*0.07)*0.04*wE));
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");
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();
// —— 躯体:蛹壳保持同一多边形,粉色血肉晕染全身 ——
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(tf>0){ctx.globalAlpha=tf*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");}
const crks=[[-10,-18,14,0.12],[7,-8,12,0.28],[-4,-30,10,0.44],[-13,-2,9,0.58]]; // 裂纹逐条生长(延长,非新增)
for(const ck of crks){const cl=Math.max(0,Math.min(1,(tf-ck[3])/0.3))*ck[2];
if(cl>0.5)px(ck[0],ck[1],3,cl,"#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(tf>0.3){ctx.globalAlpha=Math.min(1,(tf-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"); // 侧囊:青→荧绿渐变
if(tf>0.5){ctx.globalAlpha=(tf-0.5)*2;px(-19,-16,5,12,"#7dff8a");px(14,-16,5,12,"#7dff8a");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<gn;i++){let gl=8;if(mp!==null&&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<en;i++){let el=5+(t%4<2?2:0);if(mp!==null&&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)*5-5,6,(n-1)*10+10,19,"#141821"); // 键槽键距10统一不再因非整数取整成对
for(let i=0;i<n;i++){const kx=-(n-1)*5+i*10;
const hot=ks&&(ks.hot===i||(ks.hotList&&ks.hotList.indexOf(i)>=0));
px(kx-4,8,8,14,hot?(t%6<3?"#e8fbff":"#43e8ff"):"#d8e4f0");
px(kx-4,20,8,2,hot?"#43e8ff":"#8a9ab8");}
if(ks&&ks.hotT>0&&ks.hotT0){const pr=1-ks.hotT/ks.hotT0; // 蓄力前摇:即将发射的键下方圆形蓄力
const list=ks.hotList||(ks.hot>=0?[ks.hot]:[]);
for(const kh of list){const kx=-(n-1)*5+kh*10;
ctx.globalAlpha=0.3+0.55*pr;ctx.strokeStyle="#43e8ff";ctx.lineWidth=1.5;
ctx.beginPath();ctx.arc(kx,28,2+3.5*pr,0,TAU);ctx.stroke();
ctx.globalAlpha=0.22*pr;ctx.fillStyle="#43e8ff";
ctx.beginPath();ctx.arc(kx,28,2+3.5*pr,0,TAU);ctx.fill();
ctx.globalAlpha=1;}}
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;}
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*off,0); // 舰桥从中线剖开,各随各的半壳
px(sd<0?-10:0,-32,10,10,"#3a4460");
px(sd<0?-6:0,-37,6,5,"#55648a");
for(let i=0;i<7;i++){const a=Math.PI+(i+0.5)/7*Math.PI; // 壳沿铆钉也随半壳
if((sd<0)!==(a<Math.PI*1.5))continue;
px(Math.cos(a)*110-1.5,6+Math.sin(a)*30-1.5,3,3,"#9fb4d8");}
ctx.restore();}
}
/* ---- Boss光束与浮游炮渲染 ---- */
function drawFunnel(f){ // 蓝白长钉无人机(单架)
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;}
}
function drawFunnelsBack(){ // 转到Boss上半部背后的无人机先画被机体遮挡
for(const f of G.funnels)if(Math.sin(f.sa)<-0.15)drawFunnel(f);
}
function drawBossFx(){
for(const f of G.funnels)if(Math.sin(f.sa)>=-0.15)drawFunnel(f); // 下半部(身前)后画
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.hp<e.maxhp)||G.demoSim){const w2=e.dw,fr=clamp(e.hp/e.maxhp,0,1);
px(e.x-w2/2,e.y-e.dh/2-3,w2,2,"#3a1020");
px(e.x-w2/2,e.y-e.dh/2-3,w2*fr,2,fr>0.4?"#5cff9d":"#ff5566");}
}
drawFunnelsBack();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 if(b.spr==="dartS"||b.spr==="dartB"||b.spr==="dartP"){const sp=SPR[b.spr]; // 长条弹:按飞行方向偏转
ctx.save();ctx.translate(Math.round(b.x),Math.round(b.y));
ctx.rotate(Math.atan2(b.vy,b.vx)-Math.PI/2);
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;i<z.pts.length-1;i++){
drawSeg(z.pts[i][0],z.pts[i][1],z.pts[i+1][0],z.pts[i+1][1],1,z.col,0.9);
drawSeg(z.pts[i][0],z.pts[i][1],z.pts[i+1][0],z.pts[i+1][1],1,"#ffffff",0.5);}
}
// 区域效果 / 新星环 / 水雷(按类型差异化)
for(const z of G.zones){
if(z.kind==="plasma"){ // 电浆区:去除圆形区域,只保留内部窜动的电弧
if(G.time%4===0)zapVisual(z.x+rnd(-z.r*0.7,z.r*0.7),z.y+rnd(-z.r*0.7,z.r*0.7),z.x+rnd(-z.r*0.7,z.r*0.7),z.y+rnd(-z.r*0.7,z.r*0.7),pick(["#43e8ff","#bfeaff"]));
if(G.time%9===0)G.parts.push({x:z.x+rnd(-z.r/2,z.r/2),y:z.y+rnd(-z.r/2,z.r/2),vx:0,vy:-0.2,life:10,col:"#43e8ff",sz:1});
continue;
}
ctx.globalAlpha=0.16;ctx.fillStyle=z.col;
ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.fill();
ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2;ctx.strokeStyle=z.col;
ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.stroke();ctx.globalAlpha=1;
if(z.slow){ // 时间力场:逆时针旋转刻度
ctx.strokeStyle="#cfeaff";ctx.globalAlpha=0.6;
for(let i=0;i<6;i++){const a=-G.time*0.02+i*TAU/6;
ctx.beginPath();ctx.moveTo(z.x+Math.cos(a)*(z.r-6),z.y+Math.sin(a)*(z.r-6));
ctx.lineTo(z.x+Math.cos(a)*z.r,z.y+Math.sin(a)*z.r);ctx.stroke();}
ctx.globalAlpha=1;
}else if(z.pull){ // 引力:内旋纹
for(let i=0;i<3;i++){const a=G.time*0.08+i*TAU/3;
px(z.x+Math.cos(a)*z.r*0.6-1,z.y+Math.sin(a)*z.r*0.6-1,2,2,"#b05ce8");}}
}
// 热诱弹
for(const f of G.flares)drawSprite(SPR.flare,f.x,f.y);
for(const n of G.novas){
ctx.globalAlpha=clamp(n.life/22,0,1)*0.8;ctx.strokeStyle=n.col;
ctx.beginPath();ctx.arc(n.x,n.y,Math.max(1,n.r),0,TAU);ctx.stroke();ctx.globalAlpha=1;
}
// 圣光十字:逐渐变长 + 原地旋转90°
for(const hc of G.holies){
const hrot=(hc.t/36)*Math.PI/2,hL=hc.len*Math.min(1,hc.t/12),hA=clamp(hc.life/18,0,1);
for(let k=0;k<4;k++){const ang=hrot+k*Math.PI/2,hdx=Math.cos(ang),hdy=Math.sin(ang);
drawSeg(hc.x,hc.y,hc.x+hdx*hL,hc.y+hdy*hL,4,"#ffcf5c",0.4*hA);
drawSeg(hc.x,hc.y,hc.x+hdx*hL,hc.y+hdy*hL,2,"#fff4d9",0.9*hA);}
}
// 僚机 / 光刃 / 卫星 / 浮游单位
if(G.p.dead<=0){
for(const key of G.droneLoadout){const S=G.dst[key];
if(S&&S.x!==undefined){const dspr=SPR[DRONES[key].spr],dsc=DRONES[key].big||1;
if(dsc!==1)ctx.drawImage(dspr,Math.round(S.x-dspr.width*dsc/2),Math.round(S.y-dspr.height*dsc/2),Math.round(dspr.width*dsc),Math.round(dspr.height*dsc));
else drawSprite(dspr,S.x,S.y);}}
if(G.subLoadout.includes("orbit")){const lv=G.subs.orbit;
for(let i=0;i<lv;i++){const a=G.p.orbA+i*(TAU/lv);drawSprite(SPR.blade,G.p.x+Math.cos(a)*26,G.p.y+Math.sin(a)*26);}}
if(G.subLoadout.includes("sats")){const n=1+G.subs.sats;
for(let i=0;i<n;i++){const a=G.p.satA+i*(TAU/n);drawSprite(SPR.sat,G.p.x+Math.cos(a)*20,G.p.y+Math.sin(a)*20);}}
const skS=G.sst.seeker;if(skS&&skS.x!==undefined&&G.subLoadout.includes("seeker"))drawSprite(SPR.dGun2,skS.x,skS.y);
const bdS=G.sst.beamdrone;if(bdS&&bdS.x!==undefined&&G.subLoadout.includes("beamdrone"))drawSprite(SPR.dBeam,bdS.x,bdS.y);
if(G.subLoadout.includes("thorns"))for(let i=0;i<4;i++){const a=G.time*0.04+i*TAU/4;
px(G.p.x+Math.cos(a)*13-1,G.p.y+Math.sin(a)*13-1,3,3,"#c8d8ff");}
}
// 玩家
const p=G.p;
if(p.dead<=0&&!(p.inv>0&&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;i<G.stat.maxLives;i++){ctx.globalAlpha=i<G.lives?1:0.2;ctx.drawImage(SPR.heart,5+i*9,H-21);}
ctx.globalAlpha=1;
// 经验条
px(0,H-3,W,3,"#0d1226");px(0,H-3,W*clamp(G.xp/G.xpNeed,0,1),3,"#43e8ff");
ctx.fillStyle="#43e8ff";ctx.font="6px 'Press Start 2P',monospace";
ctx.fillText("LV"+G.level,W-24,H-5);
// 超频条
const odw=54;
px(W-odw-4,H-16,odw,4,"#0d1226");
px(W-odw-4,H-16,odw*clamp(G.od/100,0,1),4,G.odActive>0?"#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;i<G.loadout.length;i++){
const w=WEAPONS[G.loadout[i]],img=w.icon?ICONS[w.icon]:SPR[w.spr];
if(img)ctx.drawImage(img,W-10-i*9,H-28,7,7);
}
}
/* ================= 主循环 ================= */
function update(){
G.time++;
if(G.hitstop>0){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);