starraid/js/render.js
JOJO 45017fbd3f 原种/魔碟变形重写:同一几何体连续演化,杜绝部件凭空出现
- 太空原种:蛹体保持同一多边形,翅膀折叠藏于身后逐渐展开,裂纹逐条生长,眼青->红、侧囊青->绿渐变(删除drawPupa/drawManta双套形体切换)
- 暗黑魔碟:圆环=左右两个半环,变形为两半环裂口扩张各自收缩成犄角;眼窝常存赤瞳点亮;三阶段爆甲=壳板带轨迹旋飞飞离+铆钉连续伸长成刺+炮台基座伸出炮管
2026-08-05 20:45:37 +08:00

512 lines
29 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*(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{ // 暗黑魔碟:核心+圆环;变形=圆环前后裂开、两半各自收缩成犄角(同一几何体连续演化);爆甲=壳板飞离、铆钉伸长成刺
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 sB=Math.PI/2-open*0.55,sT=Math.PI/2-open*0.35; // 半环跨幅:前端裂口扩得更快
for(const sd of[-1,1]){
const a0=sd<0?Math.PI-sB:-sT,a1=sd<0?Math.PI+sT:sB; // 左半环[π-sB,π+sT] 右半环[-sT,sB]
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();
for(const ea of[a0,a1]){ctx.save(); // 裂口断茬(随弧端移动)
ctx.translate(Math.cos(ea)*R,Math.sin(ea)*R);ctx.rotate(ea+Math.PI/2);
px(-6,-8,12,4,"#2a1a3a");px(-3,-11,6,3,"#2a1a3a");ctx.restore();}
for(let k=0;k<3;k++){const aa=a0+(0.32+k*0.18)*(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.32+k*0.36)*(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");}
}
}
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.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)*4.75-5,6,(n-1)*9.5+10,19,"#141821"); // 键槽
for(let i=0;i<n;i++){const kx=-(n-1)*4.75+i*9.5;
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");}
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.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");}
}
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;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);