starraid/js/render.js
JOJO 3ea4bc5a73 黑石要塞重做v2:俯视五芒装甲星堡,四层石色递进+臂间小堡+同心堡垒+窗火
- 造型:五芒装甲臂(双瓣外壳+板面层次+棱线高光+臂尖绿晶)+臂间小堡次剪影+同心八角堡垒+堡沿窗火闪烁+四瓣穹顶覆盖亚空间核心;去掉俯视图不该有的尖塔
- 攻击:单臂光矛依序绕堡充能与全体臂尖黑石碎片幕交替(40帧/轮);新增shardB碎片弹(深色绿芯、按飞行方向偏转);P2亚空间主炮不变
- 二阶段:装甲瓣沿中线微开+穹顶对角滑开,露出壳下常存绿光
2026-08-05 23:05:02 +08:00

592 lines
34 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||b.idx===8||b.idx===9||b.idx===10){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 if(b.idx===8){ // 兽人战斗月亮:锯齿岩月+发光兽人图腾;二阶段岩缝熔光、舷炮伸出(连续生长)
const op=mp!==null?mp:(ph2>=1?1:0);
poly([0,-34,20,-28,32,-12,34,8,24,26,8,34,-12,32,-28,22,-34,2,-30,-18,-14,-30],"#6a5a48"); // 岩体
poly([0,-34,20,-28,32,-12,34,8,24,26,8,34,0,32],"#52452f"); // 背光面
for(let i=0;i<5;i++){const px8=-27+i*11,py8=-24+(i%2)*11;px(px8,py8,9,7,"#8a8578");px(px8+3,py8+2,2,2,"#3a3430");} // 铆接铁板
px(-31,-8,6,10,"#3a3430");px(25,-8,6,10,"#3a3430"); // 侧舷炮座(常存)
const bl8=4+op*9; // 炮管自炮座连续伸出
px(-30,-4,4,bl8,"#241f1a");px(26,-4,4,bl8,"#241f1a");
px(-30,-4+bl8,4,2,fl||"#ff7a2a");px(26,-4+bl8,4,2,fl||"#ff7a2a"); // 炮口
ctx.globalAlpha=0.55+0.45*op; // 兽人图腾脸:发光随阶段增强
px(-15,-8,9,8,fl||"#ff7a2a");px(6,-8,9,8,fl||"#ff7a2a"); // 怒目
poly([-16,10,-11,16,-6,10,-1,16,4,10,9,16,14,10,16,14,10,20,0,23,-10,20,-16,14],fl||"#ffb02a"); // 獠牙巨口
ctx.globalAlpha=1;
if(op>0){const cl=3+op*9; // 岩缝开裂生长(缝内透熔光)
ctx.globalAlpha=0.35+0.45*op;
px(-22,-26,3,cl,fl||"#ff5a2a");px(8,-16,3,cl*0.8,fl||"#ff5a2a");px(-6,0,3,cl*0.6,fl||"#ff5a2a");
ctx.globalAlpha=1;}
}else if(b.idx===9){ // 黑石要塞:五芒黑石装甲星堡(俯视),缓慢旋转;二阶段外壳裂瓣微开露亚空间绿光
const op=mp!==null?mp:(ph2>=1?1:0);
ctx.save();ctx.rotate(t*0.0032); // 整体缓慢旋转(光矛臂尖同步跟随)
for(let k=0;k<5;k++){ctx.save();ctx.rotate(k*TAU/5);
px(-2,-26,4,24,"#2fae6a"); // 臂内亚空间光芯(壳下常存,裂开后露出)
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*op*2.5,0); // 装甲外壳两瓣沿中线微开
poly([0,-32,sd*6,-20,sd*12,6,sd*6,12,0,10],"#161d2c");
poly([0,-28,sd*4,-18,sd*8,4,sd*4,8,0,6],"#232f47"); // 板面层次
px(sd<0?-8:5,-12,3,9,"#3d4f73"); // 棱线高光
ctx.restore();}
px(-1.5,-34,3,4,fl||"#5cff9d"); // 臂尖绿晶
ctx.restore();}
for(let k=0;k<5;k++){ctx.save();ctx.rotate((k+0.5)*TAU/5); // 臂间小堡(次层剪影)
poly([0,-13,5,-4,-5,-4],"#0f1622");
ctx.restore();}
poly([0,-17,12,-12,17,0,12,12,0,17,-12,12,-17,0,-12,-12],"#161d2c"); // 中央八角堡垒·外环
poly([0,-14,10,-10,14,0,10,10,0,14,-10,10,-14,0,-10,-10],"#232f47"); // 内环
for(let i=0;i<8;i++){const a2=i*TAU/8+TAU/16;px(Math.cos(a2)*14-1,Math.sin(a2)*14-1,2,2,t%40<20?"#3d4f73":"#24304a");} // 堡沿窗火
circ(0,0,7,"#1f6a44");circ(0,0,4.5,fl||"#5cff9d");circ(0,0,2,"#d6ffe8"); // 亚空间核心(穹顶之下常存)
for(let k=0;k<4;k++){ctx.save();ctx.rotate(k*Math.PI/2+Math.PI/4);ctx.translate(0,-op*4); // 四瓣穹顶对角滑开
poly([0,0,-5,-2,0,-9,5,-2],"#1c2434");
ctx.restore();}
ctx.restore();
}else if(b.idx===10){ // 泰伦母舰:生物质长舰+前触手;二阶段背甲滑开露荧光器官、触手伸长
const op=mp!==null?mp:(ph2>=1?1:0);
const tl=13+op*11; // 触手连续伸长
for(const sd of[-1,0,1]){const sw=Math.sin(t*0.06+sd*2)*3; // 触手缓摆(常存)
poly([sd*10-3,12,sd*10+3,12,sd*13+sw+1.5,12+tl,sd*13+sw-1.5,12+tl],"#8a4a6a");}
poly([0,-30,16,-22,22,-4,18,16,8,24,-8,24,-18,16,-22,-4,-16,-22],"#c8b49a"); // 生物质舰体
poly([0,-30,16,-22,22,-4,18,16,8,24,0,24],"#a8947a"); // 受光面
for(let i=0;i<4;i++){const ry2=0+i*5,ww=16-i*2.5;px(-ww,ry2,ww*2,2,"#9a8a72");} // 腹肋节理
circ(0,-8,5,"#7dff8a");circ(0,-18,3.5,"#7dff8a"); // 荧光生物器官(甲壳之下常存,裂开才露出)
circ(0,-8,2,"#e8ffd0");circ(0,-18,1.5,"#e8ffd0");
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*op*7,0); // 背甲两瓣滑开(同一几何体平移)
poly([0,-30,sd*16,-22,sd*20,-6,sd*14,4,0,-2],"#5a3a6a");
poly([0,-30,sd*16,-22,sd*12,-16,0,-22],"#7a5a8a");
ctx.restore();}
px(-22,-8,4,7,"#3a2a1a");px(18,-8,4,7,"#3a2a1a"); // 孢子囊
px(-21,-9,2,2,t%20<10?"#c8ff5c":"#7dff8a");px(19,-9,2,2,t%20<10?"#c8ff5c":"#7dff8a"); // 囊口荧光
px(-4,-27,3,4,fl||"#c8ff5c"); // 艏部感知器
}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"||b.spr==="shardB"){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);