starraid/star-raid/js/render.js
JOJO b8df012c4d 钢琴Boss大激光半圆:改用和激光线一致的描边风格
- 去掉fill()实心填充,改用stroke()弧线描边
- 粗主色弧线(lineWidth=bm.w)+细白边弧线(lineWidth=bm.w/3),和激光线绘制方式完全一致
- 半圆与激光都是描边发光风格,纹理自然统一
2026-08-06 14:59:52 +08:00

502 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;
// 暗黑突击冲撞红色残影(二三阶段冲刺时记录位置,旧→新淡出;与机体同轮廓同线条)
if(b.idx===3&&b.dashTrail&&b.dashTrail.length){
const SW=[15,45,70],swp=SW[Math.min(b.phase,2)];
for(let i=0;i<b.dashTrail.length;i++){const tp=b.dashTrail[i];
ctx.save();ctx.translate(Math.round(tp.x),Math.round(tp.y));ctx.scale(0.34,0.34);
ctx.globalAlpha=(i+1)/b.dashTrail.length*0.3;
drawAssaultGhost(swp,Math.min(b.phase,2));
ctx.restore();ctx.globalAlpha=1;}
}
// 变形动画五个新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){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(b,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){ // 夺权者XP1三层阶梯甲板母舰巨大→P2抛甲板露紧凑钻石机身灵活
drawUsurper(b,t,fl,ph2,mp);
}else if(b.idx===8){ // 黑石要塞:厚重箭头臂+五棱金字塔核心(俯视五芒星堡,缓慢旋转);纹路充能=主炮前摇
const op=mp!==null?mp:(ph2>=1?1:0);
const chg=Math.max(b.warpChg||0,ph2>=1?0.55:0); // P2纹路常亮半格主炮充能满亮
ctx.save();ctx.rotate(t*0.0032); // 整体缓慢旋转
for(let k=0;k<5;k++){ctx.save();ctx.rotate(k*TAU/5); // —— 五支厚重箭头臂 ——
px(-2,-30,4,22,"#173d2a"); // 壳下亚空间光芯(常存,裂开才见)
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*op*2,0); // 二阶段臂板沿中线微开
poly([0,-50,sd*16,-32,sd*9,-26,sd*6,-6,0,-6],"#10151f"); // 下层臂板:宽肩后掠箭头剪影
poly([0,-47,sd*12.5,-31,sd*7,-25,sd*4.5,-8,0,-8],"#1b2434"); // 上层臂板(厚度层次)
poly([0,-47,sd*12.5,-31,sd*7,-25,0,-30],sd<0?"#33436a":"#232f47"); // 箭头斜面受光(左亮右暗)
px(sd*2-1,-22,2,12,"#0a0e16"); // 板缝
ctx.restore();}
px(-1,-26,2,18,"#123324"); // 中央能量纹路(暗纹常存)
if(chg>0){const vl=18*chg;px(-1,-8-vl,2,vl,"#5cff9d");} // 亮纹从臂根向箭头蔓延(充能前摇)
px(-1.5,-50,3,3,fl||"#5cff9d"); // 箭头尖绿晶
ctx.restore();}
for(let k=0;k<5;k++){ctx.save();ctx.rotate((k+0.5)*TAU/5); // 臂间小堡
poly([0,-15,6.5,-5,-6.5,-5],"#141b29");px(-1,-11,2,2,"#1f6a44");
ctx.restore();}
poly([0,-20,12,-16,19,-6,19,6,12,16,0,20,-12,16,-19,6,-19,-6,-12,-16],"#10151f"); // 外环堡垒·下层
poly([0,-17,10,-13,16,-5,16,5,10,13,0,17,-10,13,-16,5,-16,-5,-10,-13],"#1b2434"); // 外环堡垒·上层
for(let i=0;i<10;i++){const a2=i*TAU/10;px(Math.sin(a2)*17.5-1,-Math.cos(a2)*17.5-1,2,2,t%40<20?"#3d4f73":"#24304a");} // 环上窗火
for(let k=0;k<5;k++){const a1=k*TAU/5-TAU/20,a2=k*TAU/5+TAU/20; // 五棱金字塔(俯视棱面交替受光)
ctx.fillStyle=k%2?"#33436a":"#1b2434";
ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(Math.sin(a1)*13,-Math.cos(a1)*13);ctx.lineTo(Math.sin(a2)*13,-Math.cos(a2)*13);ctx.closePath();ctx.fill();}
if(op>0){ctx.strokeStyle=`rgba(92,255,157,${op*0.7})`;ctx.lineWidth=1; // 二阶段棱缝透绿
for(let k=0;k<5;k++){const a=k*TAU/5+TAU/20;ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(Math.sin(a)*13,-Math.cos(a)*13);ctx.stroke();}}
if(chg>0)circ(0,0,5+chg*3.5,`rgba(92,255,157,${chg*0.3})`); // 充能光晕
circ(0,0,4.5,"#0a2418");circ(0,0,3,fl||"#5cff9d");circ(0,0,1.3,"#d6ffe8"); // 亚空间核心
ctx.restore();
}else if(b.idx===9){ // 泰伦母舰锥形生物舰头下尾上P2头背甲壳裂开露眼+伸出触手P3触手间展开环齿嘴
let op2,op3; // 变形期 mp=0→1 连续驱动phase在变形开始即+1故mp优先于ph2
if(ph2===0){op2=0;op3=0;} // P1甲壳闭合
else if(ph2===1){op2=mp!==null?mp:1;op3=0;} // P1→P2变形甲壳滑开+触手伸出+眼渐显
else{op2=1;op3=mp!==null?mp:1;} // P2→P3变形甲壳翻开+嘴展开+触手加粗
ctx.save();ctx.scale(1,1+Math.sin(t*0.05)*0.015); // 呼吸起伏
for(const sd of[-1,0,1]){const sw=Math.sin(t*0.055+sd*1.8)*3; // 尾须缓摆
poly([sd*8-2.5,-36,sd*8+2.5,-36,sd*11+sw,-52,sd*9+sw-1.5,-52],"#3d2f4a");
px(sd*11+sw-1,-54,2,3,"#e8dcc0");}
poly([0,-44,18,-36,24,-18,20,2,12,18,6,32,0,38,-6,32,-12,18,-20,2,-24,-18,-18,-36],"#241a30"); // 锥形舰体·底
poly([0,-42,15,-34,20,-18,17,0,10,16,5,30,0,35,-5,30,-10,16,-17,0,-20,-18,-15,-34],"#3d2f4a"); // 中层甲壳
poly([0,-40,6,-30,7,-12,5,4,3,16,0,22,-3,16,-5,4,-7,-12,-6,-30],"#1a1426"); // 背脊中线甲
for(let i=0;i<3;i++){const yy=-38+i*7,ww=15-i*3; // 橄榄甲片叠瓦
poly([0,yy-4,ww,yy+1,ww-3,yy+5,-(ww-3),yy+5,-ww,yy+1],i%2?"#5a6a32":"#4a5628");}
for(const sd of[-1,1])for(let i=0;i<3;i++){const yy=-30+i*12,xx=sd*(20-i*3); // 侧棘(骨白)
poly([xx,yy,xx+sd*7,yy+3,xx,yy+6],"#e8dcc0");}
for(const sd of[-1,1]){poly([sd*4,8,sd*13,4,sd*11,18,sd*6,26],"#b87a6a"); // 两侧粉肉
for(let i=0;i<3;i++)px(sd*(7+i*2)-1,9+i*5,2,4,"#8a5a4a");} // 肋纹
ctx.globalAlpha=0.35+0.3*Math.sin(t*0.09);px(-1,-34,2,44,"#7dff8a");ctx.globalAlpha=1; // 脉络搏动
if(op2>0){ctx.globalAlpha=op2; // 壳下组织:粉肉+大眼瞳孔追玩家P3变橙红
circ(0,20,8,"#b87a6a");
const ex=clamp((G.p.x-b.x)*0.02,-1.5,1.5),ey=clamp((G.p.y-b.y)*0.015,-1,2);
circ(0,20,5.5,ph2>=2?"#e85c3a":"#b8e85c");
ctx.fillStyle="#0d0d14";ctx.fillRect(ex-1,17+ey,2.5,6);
circ(ex-1.5,18.5+ey,0.8,"#e8ffd0");
ctx.globalAlpha=1;}
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*(op2*6+op3*5),-op3*3); // 头背甲壳两瓣P2滑开/P3翻开
poly([0,20,sd*9,14,sd*8,28,sd*3,36,0,34],"#241a30");
poly([0,22,sd*6,17,sd*5,27,sd*2,32,0,31],"#3d2f4a");
ctx.restore();}
if(op2>0){ // 触手2长角度制扇形摆荡与判定同源+4小
for(const sd of[-1,1]){
const pts=tentSegs(b,sd,op2),gl=pts[3].glow||0,sweeping=b.tent&&b.tent.side===sd;
if(sweeping&&b.tent.act>17){b.tent.trail=b.tent.trail||[];b.tent.trail.push(pts.map(p=>({x:p.x,y:p.y})));if(b.tent.trail.length>10)b.tent.trail.shift();} // 扫击记录残影
else if(sweeping&&b.tent.trail&&b.tent.trail.length)b.tent.trail.shift();
if(sweeping&&b.tent.trail){const tr=b.tent.trail;
for(let hi=0;hi<tr.length;hi++){const a=(hi+1)/tr.length*0.2;ctx.globalAlpha=a;
for(let i=1;i<7;i++){const f=i/6;circ(tr[hi][i].x,tr[hi][i].y,3.2*(1-f*0.45)*0.85,"#a8ff5c");}ctx.globalAlpha=1;}}
for(let i=6;i>=0;i--){const p=pts[i],f=i/6,r=Math.max(0.5,3.2*(1-f*0.45)*(1+op3*0.3));
if(gl>0.15){ctx.globalAlpha=gl*0.28;circ(p.x,p.y,r+2,"#a8ff5c");ctx.globalAlpha=1;}
circ(p.x,p.y,r,i%2?"#7a4a6a":"#5a3a5a");}
const p1=pts[5],p2=pts[6],ta=Math.atan2(p2.y-p1.y,p2.x-p1.x); // 骨白刀尖:朝向跟随末端切线
ctx.save();ctx.translate(p2.x,p2.y);ctx.rotate(ta-Math.PI/2);
poly([-2.5,-1.5,2.5,-1.5,0,6],"#e8dcc0");
ctx.restore();
}
if(b.tent&&b.tent.warn>0&&G.time%3===0){const pts2=tentSegs(b,b.tent.side,op2); // 前摇聚能粒子
G.parts.push({x:b.x+pts2[4].x+rnd(-24,24),y:b.y+pts2[4].y+rnd(-14,24),vx:0,vy:0,life:18,col:"#a8ff5c",sz:1,suck:{x:b.x+pts2[4].x,y:b.y+pts2[4].y}});}
for(const sd of[-1,1]){const tentS=(x0,y0,len,wd,phs)=>{const sw2=Math.sin(t*0.06+phs)*3; // 4条小触手
for(let i2=0;i2<3;i2++){const f=i2/3;circ(x0+sw2*f*f*2,y0+len*f,wd*(1-f*0.4)*(1+op3*0.3),i2%2?"#5a3a5a":"#7a4a6a");}};
tentS(sd*15*op2,16,12*op2,1.8,sd*2.9+1);tentS(sd*4*op2,32,14*op2,1.6,sd*4.1+2);}
}
if(op3>0){const mw=9*op3; // 环齿嘴P3展开利齿缓慢旋动
circ(0,30,mw,"#4a1a1a");circ(0,30,mw*0.55,"#2a0d0d");
for(let i=0;i<8;i++){const a=i*TAU/8+t*0.02,tx3=Math.cos(a)*mw*0.82,ty3=Math.sin(a)*mw*0.82;
poly([tx3-1.5,30+ty3,tx3+1.5,30+ty3,tx3*0.6,30+ty3*0.6],"#e8dcc0");}}
ctx.restore();
}else{ // 暗黑魔碟8扇区缝合装甲碟+红能量窗裂纹+紫红竖瞳魔眼变形沿用mp优先
drawSaucer(b,t,fl,ph2,mp);
}
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); // 当前管(整管连续)
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 drawFunnel(f){ // 蓝白长钉无人机(单架,带光环)
ctx.strokeStyle="rgba(127,216,255,0.7)";ctx.lineWidth=1;
ctx.beginPath();ctx.ellipse(f.x,f.y-1,6,2.5,0,0,TAU);ctx.stroke(); // 光环
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;
// 大激光起点扩口半圆:弧线描边风格与激光线完全一致(粗主色弧+细白边弧)
if(bm.dmg>=90000){const pR=bm.w*0.5;
ctx.strokeStyle=bm.col;ctx.lineWidth=bm.w;
ctx.beginPath();ctx.arc(bm.x1,bm.y1+pR,pR,Math.PI,0);ctx.stroke();
ctx.strokeStyle="#fff";ctx.lineWidth=Math.max(1,bm.w/3);
ctx.beginPath();ctx.arc(bm.x1,bm.y1+pR,pR,Math.PI,0);ctx.stroke();}
ctx.strokeStyle=bm.col;ctx.lineWidth=bm.w;
ctx.beginPath();ctx.moveTo(bm.x1,bm.y1+bm.w);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+bm.w);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 battleUI(){return UI==="play"||UI==="demoSim"||UI==="pause"||UI==="cards"||UI==="over"||UI==="slot"||!!(G&&G.homeSim&&(UI==="home"||UI==="title"));}
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&&battleUI()){
// 拾取物
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);
if(b.rift>0)ctx.globalAlpha=0.5+0.4*Math.sin(G.time*0.35); // 裂隙弹预警闪烁
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"||b.spr==="spikeB"){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==="gas"){ // 酸雾:纯粒子雾团(无圆形区域),飘过留下渐散酸雾轨迹
if(G.time%2===0)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.6,y:z.y+rnd(-1,1)*z.r*0.5,vx:rnd(-0.15,0.15),vy:rnd(0.1,0.4),life:rndi(26,40),col:pick(["#7dff8a","#a8ff5c","#4a8a3a"]),sz:rndi(2,4)});
if(G.time%5===0)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.85,y:z.y+rnd(-1,1)*z.r*0.7,vx:rnd(-0.3,0.3),vy:rnd(-0.1,0.3),life:rndi(14,22),col:"#e8ffd0",sz:1});
continue;
}
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;
}
if(z.kind==="fire"){ // 巡飞弹火场:纯粒子火舌上卷+黑烟(无圆形区域)
if(G.time%3===0)for(let i=0;i<2;i++)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.7,y:z.y+rnd(-1,1)*z.r*0.5,vx:rnd(-0.25,0.25),vy:rnd(-1.3,-0.5),life:rndi(12,22),col:pick(["#ff8a2e","#ffc64a","#ff4a1f"]),sz:rndi(1,2)});
if(G.time%8===0)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.5,y:z.y-z.r*0.3,vx:rnd(-0.15,0.15),vy:rnd(-0.7,-0.3),life:rndi(24,38),col:"#5a5a6a",sz:2});
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==="home"||UI==="title")&&(!G||!G.homeSim)&&typeof startHomeSim==="function")startHomeSim();
AU.simMute=!!(G&&G.homeSim); // 背景模拟战静音
if(UI==="play"||UI==="demoSim"||(G&&G.homeSim&&(UI==="home"||UI==="title"))){
acc+=dt;
let steps=0;
while(acc>=1000/60&&steps<4){update();acc-=1000/60;steps++;}
if(steps===4)acc=0;
}
render();
}
requestAnimationFrame(loop);