"use strict"; /* ================= 背景:星空与星云 ================= */ const STARS=[[],[],[]]; for(let i=0;i<3;i++)for(let j=0;j<26-i*6;j++)STARS[i].push({x:rnd(W),y:rnd(H),s:(0.35+i*0.4),sz:i===2?2:1,col:i===2?pick(["#aab8d8","#8fa8d8","#c0a8c8"]):"#3f4c74"}); const NEB=["#131c3f","#33102a","#0f2a1c","#101a33","#2a1030","#330f0f"]; let nebY=0,planet=null; function drawBG(dt){ ctx.fillStyle="#05070f";ctx.fillRect(0,0,W,H); const stage=G?G.stage-1:0,ncol=NEB[clamp(stage,0,5)]; nebY=(nebY+0.08*dt)%160; for(let k=0;k<2;k++){ const g=ctx.createRadialGradient(W*(k?0.75:0.25),nebY+k*160-160,10,W*(k?0.75:0.25),nebY+k*160-160,130); g.addColorStop(0,ncol);g.addColorStop(1,"transparent"); ctx.globalAlpha=0.38;ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.globalAlpha=1; } for(let i=0;i<3;i++)for(const s of STARS[i]){ s.y+=s.s*dt*(G&&G.odActive>0?2:1);if(s.y>H){s.y=-2;s.x=rnd(W);} ctx.fillStyle=s.col;ctx.fillRect(s.x,s.y,s.sz,s.sz); } if(planet){ planet.y+=0.12*dt; if(planet.y>H+40){planet=null;} else{ ctx.globalAlpha=0.5;ctx.fillStyle=planet.col; ctx.beginPath();ctx.arc(planet.x,planet.y,planet.r,0,TAU);ctx.fill(); ctx.globalAlpha=0.25;ctx.fillStyle="#fff"; ctx.fillRect(planet.x-planet.r,planet.y-2,planet.r*2,1); ctx.fillRect(planet.x-planet.r+3,planet.y+4,planet.r*2-6,1); ctx.globalAlpha=1; }} else if(Math.random()<0.001)planet={x:rnd(40,W-40),y:-40,r:rndi(14,26),col:pick(["#4a5a8c","#8c4a5a","#4a8c6e"])}; } /* ================= Boss 程序化像素绘制 ================= */ function px(x,y,w,h,col){ctx.fillStyle=col;ctx.fillRect(Math.round(x),Math.round(y),w,h);} function poly(pts,col){ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(pts[0],pts[1]);for(let i=2;i0){const prog=1-b.transT/70; ox=rnd(-1.6,1.6);oy=rnd(-1.2,1.2); if(b.idx===1||b.idx===3||b.idx===4||b.idx===6||b.idx===7||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){ // 夺权者X:P1三层阶梯甲板母舰(巨大)→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=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.phase0&&G.time%8<4){ctx.strokeStyle="#fff";ctx.lineWidth=1;ctx.strokeRect(bx-2.5,7.5,w+5,9);} ctx.font="6px 'Press Start 2P',monospace";ctx.fillStyle="#fff";ctx.textAlign="center"; ctx.fillText(b.transT>0?b.name:(b.name+" "+Math.ceil(pc*100)+"%"),W/2,22); } /* ---- Boss 分体绘制助手 ---- */ function 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){ // 大激光:fill实心上半圆+fillRect实心矩形,中间加白圆匹配激光内芯 const pR=bm.w*0.5; ctx.fillStyle=bm.col; ctx.beginPath();ctx.arc(bm.x1,bm.y1+pR,pR,0,Math.PI,true);ctx.fill(); // 上半圆 ctx.fillRect(bm.x1-pR,bm.y1+pR,bm.w,bm.y2-(bm.y1+pR)); // 主矩形 ctx.fillStyle="#fff"; ctx.beginPath();ctx.arc(bm.x1,bm.y1+pR/2,pR/3,0,TAU);ctx.fill(); // 半圆中间白圆,直径=内芯宽 ctx.fillRect(bm.x1-pR/3,bm.y1+pR,pR*2/3,bm.y2-(bm.y1+pR));} // 主矩形内芯 else{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 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.hp0.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;i0&&G.time%6<3)){ drawSprite(SPR[SHIPS[G.ship].spr],p.x,p.y); // 尾焰 const fl=G.time%8<4?3:5; px(p.x-1,p.y+7,2,fl,"#ff9a3d");px(p.x-0.5,p.y+7,1,fl-1,"#fff4d9"); if(G.odActive>0){ctx.globalAlpha=0.4;px(p.x-8,p.y-4,1,12,"#41e6ff");px(p.x+7,p.y-4,1,12,"#41e6ff");ctx.globalAlpha=1;} if(G.shield>0){ctx.strokeStyle="#43e8ff";ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2; ctx.beginPath();ctx.arc(p.x,p.y,11,0,TAU);ctx.stroke();ctx.globalAlpha=1;} if(p.focus){px(p.x-1,p.y-1,3,3,"#ff3355");px(p.x-0.5,p.y-0.5,2,2,"#fff"); ctx.strokeStyle="rgba(255,51,85,.5)";ctx.beginPath();ctx.arc(p.x,p.y,6,0,TAU);ctx.stroke();} } // 粒子与浮字(spark 粒子渲染为线段) for(const q of G.parts){ctx.globalAlpha=clamp(q.life/20,0,1); if(q.spark){ctx.strokeStyle=q.col;ctx.beginPath();ctx.moveTo(q.x,q.y);ctx.lineTo(q.x-q.vx*2.2,q.y-q.vy*2.2);ctx.stroke();} else px(q.x,q.y,q.sz,q.sz,q.col); ctx.globalAlpha=1;} ctx.textAlign="center"; for(const t of G.texts){ctx.globalAlpha=clamp(t.life/30,0,1);ctx.font="6px 'Press Start 2P',monospace";ctx.fillStyle=t.col;ctx.fillText(t.t,t.x,t.y);ctx.globalAlpha=1;} if(!G.demoSim)drawHUD(); // 横幅 if(G.bannerT>0){ const a=clamp(G.bannerT/30,0,1); ctx.globalAlpha=Math.min(1,a); ctx.font="bold 13px 'Microsoft YaHei',monospace"; ctx.fillStyle=G.banner.col;ctx.textAlign="center"; ctx.fillText(G.banner.t,W/2,H*0.38); ctx.font="7px 'Press Start 2P',monospace";ctx.fillStyle="#7f8cb8"; if(G.mutator&&G.bannerT>60)ctx.fillText(MUTS[G.mutator].ds,W/2,H*0.38+16); ctx.globalAlpha=1; } if(G.flash>0){ctx.globalAlpha=G.flash/18;ctx.fillStyle="#fff";ctx.fillRect(0,0,W,H);ctx.globalAlpha=1;} } ctx.restore(); } function drawHUD(){ ctx.textAlign="left";ctx.font="7px 'Press Start 2P',monospace"; ctx.fillStyle="#dfe8ff";ctx.fillText("SC "+G.score,5,10); ctx.fillStyle="#ffcf5c";ctx.textAlign="right";ctx.fillText("✦"+G.coins,W-5,10); ctx.textAlign="left"; ctx.fillStyle="#7f8cb8"; ctx.fillText(G.mode==="endless"?"WAVE "+G.wave:G.mode==="rush"?"BOSS RUSH":"STAGE "+G.stage+"-"+G.wave,5,20); if(G.combo>4){ctx.fillStyle="#ff9a3d";ctx.textAlign="right"; ctx.fillText("x"+(1+Math.min(2,G.combo*0.02)).toFixed(1)+" COMBO "+G.combo,W-5,20);ctx.textAlign="left";} // 结构值血条(绿→红)+ 备用机体 + 炸弹 const hpw=62,pc2=clamp(G.hp/G.maxHp,0,1); px(4,H-12,hpw+2,6,"#0d1226"); px(5,H-11,hpw,4,"#3a1020"); px(5,H-11,hpw*pc2,4,pc2>0.5?"#5cff9d":pc2>0.25?"#ffcf5c":"#ff5566"); for(let i=0;i0?"#fff":"#ff9a3d"); if(G.odActive>0){ctx.fillStyle="#ff9a3d";ctx.textAlign="center";ctx.fillText("暴走 OVERDRIVE",W/2,30);ctx.textAlign="left";} // 主武器装备栏图标 for(let i=0;i0){G.hitstop--;return;} if(G.shake>0)G.shake*=0.85;if(G.shake<0.3)G.shake=0; if(G.flash>0)G.flash--; if(G.bannerT>0)G.bannerT--; if(G.comboT>0){G.comboT--;if(G.comboT===0)G.combo=0;} updatePlayer(); if(G.demoSim)demoSpawn();else updateSpawner(); for(const e of G.enemies){e.uid=e.uid||_uid++;updateEnemy(e);} G.enemies=G.enemies.filter(e=>!e.dead); updateBoss(); updateBullets(); collide(); for(const q of G.parts){ if(q.suck){q.vx+=(q.suck.x-q.x)*0.09;q.vy+=(q.suck.y-q.y)*0.09;} else{q.vx*=0.95;q.vy*=0.95;} if(q.grav)q.vy+=q.grav; q.x+=q.vx;q.y+=q.vy;q.life--;} G.parts=G.parts.filter(q=>q.life>0); for(const t of G.texts){t.y-=0.5;t.life--;} G.texts=G.texts.filter(t=>t.life>0); } let last=0,acc=0; function loop(ts){ requestAnimationFrame(loop); const dt=Math.min(100,ts-last);last=ts; MUS.tick(); // 主页/基地背景=实战模拟(自动驾驶随机武装打怪);离开即销毁,永不计分 if((UI==="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);