激光钢琴重设计:美术大改=4层同心装甲环带穹顶要塞+像素琴键(黑键纹样)+6小炮塔+左右大炮塔(左紫右蓝)+雷达+发光纹路蓄力蔓延+裂壳露红色管线网红核(内层滑更远阶梯裂口);攻击小改=新增第5模式滑奏(顺扫立刻反扫)/P2乱序双键和音/topRain改炮塔轮流开火/ring删改pedal踏板重音慢速大弹。绘制迁入新文件js/boss_art.js。仅语法检查通过,未运行验证

This commit is contained in:
JOJO 2026-08-06 03:25:51 +08:00
parent a7da7e1031
commit b776149ff0
4 changed files with 97 additions and 45 deletions

View File

@ -132,6 +132,7 @@
<script src="js/enemies.js"></script>
<script src="js/combat.js"></script>
<script src="js/render.js"></script>
<script src="js/boss_art.js"></script>
<script src="js/ui.js"></script>
</body>
</html>

78
star-raid/js/boss_art.js Normal file
View File

@ -0,0 +1,78 @@
"use strict";
/* ================= Boss =================
激光钢琴 drawPiano / 夺权者X drawUsurper / 暗黑魔碟 drawSaucer
设计标准见 .astrion/memory/boss_design_standard.md
大剪影+3~4层配色递进+多层结构+受光逻辑+发光焦点+微动效变形=同一几何体连续演化 */
/* ---- split=0~1 ----
4层同心装甲环带+像素琴键炮口带变形=环带沿中线阶梯滑开内层滑更远露红色管线网+红核 */
function drawPiano(b,t,fl,split){
const ks=b.keys,n=ks?ks.n:21;
// —— 红色管线网(壳下常存,裂壳才见;锯齿线非圆) ——
if(split>0){ctx.globalAlpha=split*0.85;
for(let k=0;k<5;k++){const lx=-40+k*20;
px(lx,-14,1,10,"#ff3355");px(lx,-5,4,1,"#ff3355");px(lx+3,-4,1,8,"#ff3355");}
ctx.globalAlpha=1;}
// —— 红色核心(裂壳显现,搏动;收在穹顶基线之上) ——
if(split>0){ctx.globalAlpha=split;
const cr=8+Math.sin(t*0.12)*1.5;
circ(0,-8,cr+4,"#7a0e1e");circ(0,-8,cr,fl||"#ff3355");circ(0,-8,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;}
// —— 穹顶4层装甲环带沿中线剖半内层滑得更远=阶梯裂口) ——
const BANDS=[[118,34,"#10151f"],[98,28,"#1b2434"],[74,21,"#33436a"],[50,14,"#55648a"]];
for(let bi=0;bi<4;bi++){const off=split*(16+bi*6),rx=BANDS[bi][0],ry=BANDS[bi][1];
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=BANDS[bi][2];ctx.beginPath();ctx.ellipse(0,6,rx,ry,0,q0,q1);ctx.lineTo(0,6);ctx.closePath();ctx.fill();
ctx.restore();}}
// —— 外环窗火+壳沿铆钉(随最外半壳滑开) ——
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*split*16,0);
for(let k=0;k<4;k++)px(sd*(20+k*22)-2,-2,4,3,t%40<20?"#3d5a73":"#24304a"); // 窗火明暗
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();}
// —— 6座穹顶小炮塔topRain炮位与enemies.js同源x=-75+ti*30P2随第3环带滑开28 ——
{const off2=split*28;
for(let ti=0;ti<6;ti++){const sd2=ti<3?-1:1,tx=-75+ti*30+sd2*off2;
circ(tx,-4,3.5,"#33436a");circ(tx-1,-5,1.5,"#55648a");
px(tx-1,-2,2,5,b.turrT>0&&b.turrI===ti?"#e8fbff":"#1b2434");}}
// —— 左右大炮塔踏板重音炮位左紫右蓝P2随最外环带滑开16 ——
{const off0=split*16;
for(const sd of[-1,1]){const px2=sd*(112+off0);
circ(px2,2,5.5,"#33436a");circ(px2-1.5,0,2.5,"#55648a");
px(px2-1.5,4,3,8,"#1b2434");
px(px2-1,10,2,3,b.pedT>0?"#ffffff":(sd<0?"#b98cff":"#43e8ff"));}}
// —— 舰桥+雷达(随半壳滑开) ——
for(const sd of[-1,1]){ctx.save();ctx.translate(sd*split*30,0);
px(sd<0?-10:0,-32,10,10,"#1b2434");
px(sd<0?-6:0,-37,6,5,"#33436a");
ctx.restore();}
ctx.save();ctx.translate(-split*30,0); // 雷达随左半壳
circ(0,-34,4,"#33436a");
drawSeg(0,-34,Math.cos(t*0.05)*8,-34+Math.sin(t*0.05)*8,1,"#43e8ff",0.55); // 雷达扫面
px(-1,-38,2,2,t%30<15?"#ff3355":"#7a1622");
ctx.restore();
// —— 发光纹路(暗纹常存,蓄力时向键带点亮蔓延) ——
{const pr=(ks&&ks.hotT>0&&ks.hotT0)?1-ks.hotT/ks.hotT0:0;
for(let k=0;k<5;k++){const tx2=-60+k*30;
px(tx2,-8,1,12,"#134152");
if(pr>0){ctx.globalAlpha=pr*0.7;px(tx2,-8,1,12,"#43e8ff");ctx.globalAlpha=1;}}}
// —— 琴键炮口带21键像素钢琴白键+黑键+键缝;键位公式 -(n-1)*5+k*10 与enemies.js同步 ——
px(-(n-1)*5-5,6,(n-1)*10+10,19,"#0a0e16"); // 键槽
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(i<n-1&&[0,1,3,4,5].indexOf(i%7)>=0)px(kx+3,8,4,7,"#101318");} // 黑键(钢琴纹样)
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;}}
}

View File

@ -192,7 +192,7 @@ const BOSSES=[
{name:"太空原种",sub:"SPACE SPAWN",hp:1350,col:"#ff8fc8",r:20,rr:[20,34],ryy:[26,18],wide:1,main:"spawn",minors:["wiggle","spikeSnipe","sporeArc","tentBeam","mawSnipe","wingRain"],phases:2},
{name:"歼灭者",sub:"ANNIHILATOR",hp:1700,col:"#b05ce8",r:22,main:"annihilate",minors:["aimSpread","spawnMine"],phases:3},
{name:"暗黑突击",sub:"DARK ASSAULT",hp:2100,col:"#ff3355",r:9,rr:[9,10,11],ryy:[5,6,7],main:"assault",minors:["rain","aimSpread"],phases:3},
{name:"激光钢琴",sub:"LASER PIANO",hp:2500,col:"#43e8ff",r:26,rr:[100,100],ryy:[30,30],main:"piano",minors:["topRain","ring"],phases:2,static:1},
{name:"激光钢琴",sub:"LASER PIANO",hp:2500,col:"#43e8ff",r:26,rr:[100,100],ryy:[30,30],main:"piano",minors:["topRain","pedal"],phases:2,static:1},
{name:"母巢核心",sub:"HIVE CORE",hp:2800,col:"#ff9a3d",r:22,main:"hive",minors:["spawnMini","aimSpread","ring"],phases:3},
{name:"夺权者X",sub:"USURPER X",hp:3700,col:"#e8f4ff",r:24,rr:[52,26],ryy:[34,40],wide:1,main:"usurper",minors:["chain","pillar","aimSpread"],phases:2},
{name:"暗黑魔碟",sub:"DARK SAUCER",hp:4500,col:"#8a5cff",r:24,rr:[30,32,34],main:"saucer",minors:["ringBig","coneFan","crossEye","dartSpiral"],phases:3},
@ -302,7 +302,7 @@ function updateBoss(){
else{b.x=W/2+Math.sin(b.mt*0.013)*(def.wide?44:70);b.y=b.ty+Math.sin(b.mt*0.011)*(def.wide?8:12);} // 通用游走提速+上下浮动(钢琴站桩除外)
b.mt++;
const fm=b.fireM*(1+0.22*b.phase);
if(b.fnT>0)b.fnT--;if(b.plT>0)b.plT--;if(b.eyeT>0)b.eyeT--;if(b.hornT>0)b.hornT--;if(b.laserT>0)b.laserT--;if(b.eyeGlow>0)b.eyeGlow--;if(b.rainT>0)b.rainT--; // 帧级技能计时器
if(b.fnT>0)b.fnT--;if(b.plT>0)b.plT--;if(b.eyeT>0)b.eyeT--;if(b.hornT>0)b.hornT--;if(b.laserT>0)b.laserT--;if(b.eyeGlow>0)b.eyeGlow--;if(b.rainT>0)b.rainT--;if(b.turrT>0)b.turrT--;if(b.pedT>0)b.pedT--; // 帧级技能计时器
if(b.idx===1&&b.rainT>0&&b.rainT<94&&b.rainT%3===0){ // 原种红雨前摇16帧双翼高频抖动后翼展范围内红光点随机洒落
const span=46*(0.22+0.78*(b.phase>=1?1:0.62));
ebul(b.x+rnd(-span,span),b.y+rnd(-10,2),Math.PI/2+rnd(-0.08,0.08),rnd(2.2,3.4),"orbR",12);}
@ -385,17 +385,18 @@ function bossMain(b,def,fm){
ks.hotList=null;ks.hot=-1;SFX.eshoot();}}
ks.t--;
if(ks.t<=0&&ks.hotT<=0){
if(ks.i>=ks.seq.length){ // 布下一局:轮换模式
ks.mode=(ks.mode+1)%4;
if(ks.i>=ks.seq.length){ // 布下一局:轮换模式5种
ks.mode=(ks.mode+1)%5;
if(ks.mode===0){ks.seq=[];for(let i=0;i<n-3;i++)ks.seq.push(i);} // 顺扫止于18号最右3键永远安全
else if(ks.mode===1){ks.seq=[];for(let i=n-1;i>=3;i--)ks.seq.push(i);} // 反扫止于3号最左3键永远安全
else if(ks.mode===2){ks.seq=shuffled(n);}
else{ks.seq=[];for(let i=0;i<6+ph*2;i++)ks.seq.push(-1);} // -1=齐射标记
else if(ks.mode===3){ks.seq=[];for(let i=0;i<6+ph*2;i++)ks.seq.push(-1);} // -1=齐射标记
else{ks.seq=[];for(let i=0;i<n-3;i++)ks.seq.push(i);for(let i=n-4;i>=0;i--)ks.seq.push(i);} // 滑奏:顺扫到尾立刻反扫回来
ks.i=0;ks.t=Math.round((ph?14:22)/fm);
}else{const cur=ks.seq[ks.i++];
if(cur===-1){const pool=shuffled(n);ks.hotList=pool.slice(0,4+ph*2);ks.hotT=9;ks.hotT0=9;ks.t=Math.round((ph?21:30)/fm);} // 齐射慢50%
else if(ks.mode<=1){ks.hot=cur;ks.hotT=1;ks.hotT0=1;ks.t=Math.round((ph?2:3)/fm);} // 顺/反扫前摇快50%
else{ks.hot=cur;ks.hotT=3;ks.hotT0=3;ks.t=Math.round((ph?3:5)/fm);} // 乱序:慢50%
else if(ks.mode<=1||ks.mode===4){ks.hot=cur;ks.hotT=1;ks.hotT0=1;ks.t=Math.round((ph?2:3)/fm);} // 顺/反扫/滑奏前摇快50%
else{if(ph>=1){ks.hotList=[cur,ks.i<ks.seq.length?ks.seq[ks.i++]:cur];ks.hot=-1;}else{ks.hot=cur;}ks.hotT=3;ks.hotT0=3;ks.t=Math.round((ph?3:5)/fm);} // 乱序:P2一次点2键和音
}
}
b.mainT=4;break;}
@ -466,7 +467,16 @@ function bossMinor(b,mk,fm){
case "spawnMine": spawnEnemy("mine",b.x,b.y+10,false);b.minorT=Math.round(280/fm);break;
case "aimSpread": for(let i=-2;i<=2;i++)ebul(b.x,b.y+12,a0+i*0.19,2.5,"orbO",15);b.minorT=Math.round(140/fm);break;
case "rain": for(let i=0;i<5;i++)ebul(rnd(20,W-20),-6,Math.PI/2+rnd(-0.1,0.1),2.2,"orbO",14);b.minorT=Math.round(125/fm);break;
case "topRain": for(let i=0;i<4;i++)ebul(rnd(30,W-30),-6,Math.PI/2+rnd(-0.15,0.15),1.9,"orbG",13);b.minorT=Math.round(150/fm);break;
case "topRain": { // 钢琴6座穹顶小炮塔轮流开火炮位与drawPiano同源P2随环带滑开28
const off2=b.phase>=1?28:0,ti=rndi(0,5),tx=b.x+(-75+ti*30)+(ti<3?-1:1)*off2;
b.turrI=ti;b.turrT=8;
for(let i=0;i<3;i++)ebul(tx+rnd(-3,3),b.y-2,Math.PI/2+rnd(-0.12,0.12),2.0,"orbG",13);
b.minorT=Math.round(150/fm);break;}
case "pedal": { // 钢琴踏板重音——左右大炮塔慢速大弹左紫右蓝P2随外环滑开16炮位与drawPiano同源
const off0=b.phase>=1?16:0;
for(const sd of[-1,1]){const bb=ebul(b.x+sd*(112+off0),b.y+10,Math.PI/2+sd*0.08,1.1,sd<0?"orbP":"dartB",18);
bb.scale=2.4;bb.r=6;}
b.pedT=8;b.minorT=Math.round(280/fm);break;}
case "chain": for(let arm=0;arm<2;arm++)for(let i=0;i<4;i++)ebul(b.x,b.y+8,b.t*0.09+arm*Math.PI+i*0.11,1.7,"orbO",14);b.minorT=Math.round(44/fm);break;
case "pillar": {const px2=rnd(40,W-40);bossBeam(px2,-6,px2,H+8,4,"#b98cff",26,26,18);b.minorT=Math.round(220/fm);break;}
case "eyeBeam": bossBeam(b.x,b.y+6,ap2.x,H+8,4,"#ff3355",22,24,20);b.minorT=Math.round(180/fm);break;

View File

@ -337,43 +337,6 @@ function drawAssault(t,fl,swp,ph,mp){ // 暗黑突击swp=翼展角;炮管/
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");