starraid/star-raid/js/ui.js

351 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 OVLS=["o-auth","o-home","o-title","o-setup","o-hangar","o-cards","o-pause","o-over","o-codex","o-demo","o-rush","o-slot"];
function show(id){for(const o of OVLS)$(o).classList.toggle("hide",o!==id);}
const SETUP={mode:"stage",ship:"falcon",diff:0};
function bestLine(){
const b=SAVE.best;
$("best-line").textContent=`无尽纪录 ${Math.max(b.e0,b.e1)} · 闯关进度 ${b.stage}/6`;
}
$("b-go").onclick=()=>{AU.init();buildSetup();show("o-setup");};
$("b-back1").onclick=()=>show("o-title");
$("b-back2").onclick=()=>show("o-title");
$("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");};
$("b-codex").onclick=()=>{AU.init();buildCodex();show("o-codex");};
$("b-rush").onclick=()=>{AU.init();buildRush();show("o-rush");};
$("b-back6").onclick=()=>{UI="title";show("o-title");};
$("b-back4").onclick=()=>{UI="title";bestLine();show("o-title");};
$("b-back5").onclick=exitDemoSim;
$("b-start").onclick=()=>startGame();
if("ontouchstart" in window){const pb=$("tb-pause");pb.style.display="block";pb.onclick=()=>togglePause();}
$("b-resume").onclick=()=>togglePause();
$("b-restart").onclick=()=>{show("");if(G&&G.mode==="rush")startRush(G.rushIdx);else startGame();}; // Boss挑战重开仍在挑战模式
$("b-quit").onclick=()=>{bank();G=null;MUS.stop();UI="title";bestLine();show("o-title");};
$("b-again").onclick=()=>startGame();
$("b-home").onclick=()=>{G=null;UI="title";bestLine();show("o-title");};
$("mode-pick").addEventListener("click",e=>{
const c=e.target.closest(".pcard");if(!c)return;
SETUP.mode=c.dataset.m;
for(const el of $("mode-pick").children)el.classList.toggle("sel",el===c);
});
$("diff-pick").addEventListener("click",e=>{
const b=e.target.closest("button");if(!b)return;
SETUP.diff=+b.dataset.d;
for(const el of $("diff-pick").children)el.classList.toggle("primary",el===b);
});
function buildSetup(){
const box=$("ship-pick");box.innerHTML="";
for(const k in SHIPS){
const s=SHIPS[k],owned=SAVE.ships.includes(k);
const d=document.createElement("div");
d.className="pcard"+(owned?"":" lock")+(SETUP.ship===k?" sel":"");
const c=document.createElement("canvas");c.width=52;c.height=64;
const g=c.getContext("2d");g.imageSmoothingEnabled=false;
const spr=SPR[s.spr];g.drawImage(spr,(52-spr.width*3)/2,(64-spr.height*3)/2,spr.width*3,spr.height*3);
d.appendChild(c);
d.insertAdjacentHTML("beforeend",`<div class="nm">${s.name}</div><div class="ds">${s.trait}</div>`+(owned?"":`<div class="cost">✦ ${s.cost} 解锁</div>`));
d.onclick=()=>{
if(!SAVE.ships.includes(k)){
if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildSetup();}
else SFX.hurt();
return;
}
SETUP.ship=k;buildSetup();
};
box.appendChild(d);
}
}
/* ================= 机库商店 ================= */
const META=[
{key:"weapon",name:"主武器校准",ds:"出击时主武器等级 +1",max:2,costs:[300,800]},
{key:"armor",name:"强化装甲",ds:"出击时生命上限 +1",max:2,costs:[400,1000]},
{key:"hpup",name:"结构强化",ds:"出击时结构值 +15",max:2,costs:[250,700]},
{key:"luck",name:"幸运币",ds:"高品质强化概率提升",max:2,costs:[350,900]},
{key:"wallet",name:"收藏家协议",ds:"星币获取 +15%",max:3,costs:[300,800,2000]},
{key:"mag2",name:"引力透镜",ds:"拾取范围 +15%",max:2,costs:[200,600]},
{key:"odstart",name:"超频预载",ds:"出击时超频已充能 50%",max:1,costs:[500]}
];
function buildShop(){
$("wallet").textContent=SAVE.coins;
const box=$("shop");box.innerHTML="";
for(const m of META){
const lv=SAVE.m[m.key]||0,maxed=lv>=m.max,cost=maxed?0:m.costs[lv];
const d=document.createElement("div");d.className="sitem";
d.innerHTML=`<div class="inf"><div class="nm">${m.name}</div><div class="ds">${m.ds}</div>
<div class="pips">${"■".repeat(lv)}${"□".repeat(m.max-lv)}</div></div>
<button ${maxed||SAVE.coins<cost?"disabled":""}>${maxed?"MAX":"✦ "+cost}</button>`;
if(!maxed)d.querySelector("button").onclick=()=>{
if(SAVE.coins<cost)return;
SAVE.coins-=cost;SAVE.m[m.key]=lv+1;persist();SFX.coin();buildShop();
};
box.appendChild(d);
}
}
/* ================= 开局 / 暂停 / 结算 ================= */
function startGame(){
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
newRun(SETUP.mode,SETUP.ship,SETUP.diff);
UI="play";show("");acc=0;
MUS.start(false);
banner(SETUP.mode==="stage"?"STAGE 1 — "+STAGE_NAMES[0]:"无尽模式 — 生存到底!","#41e6ff",130);
startWave();
}
function togglePause(){
if(!G)return;
if(UI==="play"){UI="pause";buildLoadoutUI();show("o-pause");}
else if(UI==="pause"){UI="play";show("");}
}
/* ================= 换装面板 ================= */
function buildLoadoutUI(){
const box=$("loadout");if(!box||!G)return;box.innerHTML="";
const mk=(title,dict,owned,active,max)=>{
const sec=document.createElement("div");
sec.innerHTML=`<div class="h3" style="margin:6px 0 2px">${title}${active.length}/${max})· 点击切换装备</div>`;
const row=document.createElement("div");
for(const k in owned){
const d=document.createElement("span");
const on=active.includes(k);
d.className="litem"+(on?" on":"");
d.textContent=`${dict[k].name} Lv${owned[k]}`;
d.onclick=()=>{
const i=active.indexOf(k);
if(i>=0){if(active===G.loadout&&active.length===1){SFX.hurt();return;}active.splice(i,1);}
else{if(active.length>=max){SFX.hurt();return;}active.push(k);}
SFX.pick();buildLoadoutUI();
};
row.appendChild(d);
}
if(Object.keys(owned).length===0)row.innerHTML='<span class="small">尚未获得</span>';
sec.appendChild(row);box.appendChild(sec);
};
mk("主武器",WEAPONS,G.weapons,G.loadout,5);
mk("副武器",SUBS,G.subs,G.subLoadout,5);
mk("僚机",DRONES,G.drones,G.droneLoadout,3);
}
/* ================= 图鉴 ================= */
function iconCanvas(img,size,sil){
const c=document.createElement("canvas");c.width=c.height=size;
const g=c.getContext("2d");g.imageSmoothingEnabled=false;
const sc=Math.min(size/img.width,size/img.height);
g.drawImage(img,(size-img.width*sc)/2,(size-img.height*sc)/2,img.width*sc,img.height*sc);
if(sil){g.globalCompositeOperation="source-atop";g.fillStyle="#232f52";g.fillRect(0,0,size,size);g.globalCompositeOperation="source-over";}
return c;
}
function buildCodex(){
const box=$("codex");box.innerHTML="";
const total=MAINS.length+SUB_KEYS.length+DRONE_KEYS.length+PAS_KEYS.length;
$("codex-t").textContent=total;
$("codex-n").textContent=codexCount();
const mk=(title,keys,dict,kind)=>{
const h=document.createElement("div");h.className="cx-sec";h.textContent=title;box.appendChild(h);
const grid=document.createElement("div");grid.className="cx-grid";
for(const k of keys){
const def=dict[k],owned=!!SAVE.codex[k];
const d=document.createElement("div");
d.className="cx-item"+(owned?"":" lock"); // 未收集:剪影+???,不可演示
const img=def.icon?ICONS[def.icon]:SPR[def.spr];
d.appendChild(iconCanvas(img,28,!owned));
d.insertAdjacentHTML("beforeend",`<div class="nm">${owned?def.name:"???"}</div>`);
if(owned)d.onclick=()=>{SFX.pick();startDemoSim(kind,k);};
grid.appendChild(d);
}
box.appendChild(grid);
};
mk("主武器",MAINS,WEAPONS,"m");
mk("副武器",SUB_KEYS,SUBS,"s");
mk("僚机",DRONE_KEYS,DRONES,"d");
mk("被动强化",PAS_KEYS,PASSIVES,"p");
}
document.addEventListener("visibilitychange",()=>{if(document.hidden&&UI==="play")togglePause();});
function bank(){
if(!G||G.banked)return;G.banked=true;
SAVE.coins+=G.coins;
if(G.mode!=="rush"){const bk=(G.mode==="endless"?"e":"s")+G.diff;
SAVE.best[bk]=Math.max(SAVE.best[bk]||0,G.score);}
persist();
}
/* ---- Boss 挑战模式 ---- */
function buildRush(){
G=null; // 清掉上一局残留rush胜利后G仍在防背景卡残影
SAVE.rush=SAVE.rush||{};
const ra=$("rush-anim"); // 老虎机开关(持久化)
if(ra){ra.checked=!!SAVE.rushAnim;ra.onchange=()=>{SAVE.rushAnim=ra.checked?1:0;persist();};}
const el=$("rush-pick");el.innerHTML="";
BOSSES.forEach((b,i)=>{
const best=SAVE.rush[i];
const d=document.createElement("div");d.className="pcard";
d.innerHTML=`<div class="nm" style="margin-top:2px;color:${b.col}">${b.name}${best?" ✓":""}</div>
<div class="ds">${b.sub}<br>${best?("最快 "+Math.floor(best/60)+":"+("0"+best%60).slice(-2)):"未击破"}</div>`;
d.onclick=()=>startRush(i);
el.appendChild(d);
});
}
function rollRushBuild(){ // 随机武装3主武Lv3 + 3副武Lv3 + 2僚机Lv2
const sh=a=>{const x=a.slice();for(let i=x.length-1;i>0;i--){const j=rndi(0,i),t2=x[i];x[i]=x[j];x[j]=t2;}return x;};
return {m:sh(Object.keys(WEAPONS)).slice(0,3),s:sh(Object.keys(SUBS)).slice(0,3),d:sh(Object.keys(DRONES)).slice(0,2)};
}
function startRush(i){
AU.init();if(AU.ctx&&AU.ctx.state==="suspended")AU.ctx.resume();
const build=rollRushBuild();
if(SAVE.rushAnim)playSlot(build,()=>startRushGo(i,build)); // 老虎机动画抽取
else startRushGo(i,build); // 直接随机
}
function startRushGo(i,build){
newRun("rush","falcon",0);
G.weapons={};build.m.forEach(k=>G.weapons[k]=3);G.loadout=build.m.slice();
G.subs={};build.s.forEach(k=>G.subs[k]=3);G.subLoadout=build.s.slice();
G.drones={};build.d.forEach(k=>G.drones[k]=2);G.droneLoadout=build.d.slice();
G.xpNeed=1e9; // 挑战模式无升级打断
G.rushIdx=i;G.rushT0=0;
UI="play";show("");acc=0;
MUS.start(true);
banner("BOSS 挑战 — "+BOSSES[i].name,"#ff5566",130);
G.rushT0=G.time;
startBoss();
}
/* ---- 老虎机抽奖动画 ---- */
let slotTimers=[];
function playSlot(build,cb){
UI="slot";show("o-slot");
const row=$("slot-row");row.innerHTML="";
$("slot-hint").textContent=" ";
const items=[
...build.m.map(k=>({cat:"主武器",pool:WEAPONS,key:k})),
...build.s.map(k=>({cat:"副武器",pool:SUBS,key:k})),
...build.d.map(k=>({cat:"僚机",pool:DRONES,key:k}))];
let done=0;
items.forEach((it,idx)=>{
const d=document.createElement("div");d.className="slotbox";
d.innerHTML=`<div class="sc">${it.cat}</div><div class="sn">???</div>`;
row.appendChild(d);
const sn=d.querySelector(".sn"),names=Object.values(it.pool).map(x=>x.name);
const iv=setInterval(()=>{sn.textContent=names[rndi(0,names.length-1)];},60);
slotTimers.push(iv);
setTimeout(()=>{clearInterval(iv);
sn.textContent=it.pool[it.key].name;sn.classList.add("hit");
SFX.graze();done++;
if(done>=items.length){$("slot-hint").textContent="— 出击 —";setTimeout(cb,700);}
},700+idx*280);
});
}
/* 结算上报服务端每局一次含rush胜利图鉴演示除外 */
function submitRun(win){
if(!G||G.submitted||G.demoSim)return;
G.submitted=true;
if(typeof NET==="undefined"||!NET.user)return;
NET.submitRun({mode:G.mode,diff:G.diff,score:G.score,wave:G.wave,stage:G.stage,
timeSec:Math.round(G.time/60),bossKills:G.bossKills||0,win:!!win,
bestEndless:Math.max(SAVE.best.e0||0,SAVE.best.e1||0),
codex:Object.keys(SAVE.codex)});
}
function endRun(win){
if(UI==="over")return;
bank();MUS.stop();
submitRun(win);
UI="over";
$("over-title").textContent=win?"★ 任务完成 ★":"任务失败";
$("over-title").style.color=win?"#ffcf5c":"#ff5566";
const mins=Math.floor(G.time/3600),secs=Math.floor(G.time/60)%60;
$("over-stats").innerHTML=
`<span>最终得分</span><b>${G.score}</b>
<span>${G.mode==="endless"?"抵达波次":G.mode==="rush"?"挑战目标":"抵达关卡"}</span><b>${G.mode==="endless"?G.wave:G.mode==="rush"?BOSSES[G.rushIdx].name:G.stage+"-"+G.wave}</b>
<span>击坠数</span><b>${G.kills}</b>
<span>强化等级</span><b>Lv.${G.level}</b>
<span>存活时间</span><b>${mins}${secs}秒</b>
<span>持有军械</span><b>${Object.keys(G.weapons).length}主/${Object.keys(G.subs).length}副/${Object.keys(G.drones).length}机</b>`;
$("over-coins").textContent=G.coins;
setTimeout(()=>show("o-over"),win?400:900);
}
/* ================= 三选一卡片 ================= */
function offerCards(bossRare){
if(!G||G.p.dead>0&&!bossRare)return;
G.bossCard=!!bossRare;
const opts=rollCards(bossRare);
G.cardOpts=opts;
const box=$("cards");box.innerHTML="";
opts.forEach((c,i)=>{
let nm,ds,rare,img;
if(c.kind==="wlvl"){const w=WEAPONS[c.key],lv=G.weapons[c.key];nm=w.name;rare=w.rare;ds=`主武器强化 Lv.${lv} → Lv.${lv+1}`;img=w.icon?ICONS[w.icon]:SPR[w.spr];}
else if(c.kind==="weapon"){const w=WEAPONS[c.key];nm="主武器·"+w.name;rare=w.rare;ds=w.ds;img=w.icon?ICONS[w.icon]:SPR[w.spr];}
else if(c.kind==="slvl"){const u=SUBS[c.key],lv=G.subs[c.key];nm=u.name;rare=u.rare;ds=`副武器强化 Lv.${lv} → Lv.${lv+1}`;img=u.icon?ICONS[u.icon]:SPR[u.spr];}
else if(c.kind==="sub"){const u=SUBS[c.key];nm="副武器·"+u.name;rare=u.rare;ds=typeof u.ds==="function"?u.ds(0):u.ds;img=u.icon?ICONS[u.icon]:SPR[u.spr];}
else if(c.kind==="dlvl"){const u=DRONES[c.key],lv=G.drones[c.key];nm=u.name;rare=u.rare;ds=`僚机强化 Lv.${lv} → Lv.${lv+1}`;img=SPR[u.spr];}
else if(c.kind==="drone"){const u=DRONES[c.key];nm="僚机·"+u.name;rare=u.rare;ds=typeof u.ds==="function"?u.ds(0):u.ds;img=SPR[u.spr];}
else if(c.kind==="upg"){const u=PASSIVES[c.key];nm=u.name;rare=u.rare;ds=u.ds(G.u[c.key]);img=u.icon?ICONS[u.icon]:SPR[u.spr];}
else{rare=0;
if(c.key==="heal"){nm="紧急修复";ds="生命 +1";img=SPR.heart;}
else if(c.key==="coins"){nm="星币补给";ds="星币 +50";img=SPR.coin;}
else{nm="超频充能";ds="超频立即充满";img=SPR.odc;}}
const d=document.createElement("div");
d.className="ucard r"+rare;
const cv2=document.createElement("canvas");cv2.width=40;cv2.height=40;
const g=cv2.getContext("2d");g.imageSmoothingEnabled=false;
const sc=Math.min(40/img.width,40/img.height);
g.drawImage(img,(40-img.width*sc)/2,(40-img.height*sc)/2,img.width*sc,img.height*sc);
d.appendChild(cv2);
d.insertAdjacentHTML("beforeend",
`<div class="key">${i+1}</div><div class="nm">${nm}</div>
<div class="rtag">${["普通","稀有","史诗"][rare]}</div><div class="ds">${ds}</div>`);
d.onclick=()=>pickCard(i);
box.appendChild(d);
});
UI="cards";show("o-cards");SFX.level();
}
function pickCard(i){
if(UI!=="cards"||!G.cardOpts||!G.cardOpts[i])return;
const c=G.cardOpts[i];G.cardOpts=null;
if(c.kind==="wlvl")G.weapons[c.key]++;
else if(c.kind==="weapon"){G.weapons[c.key]=1;if(G.loadout.length<5)G.loadout.push(c.key);banner("获得 "+WEAPONS[c.key].name,"#b98cff",80);}
else if(c.kind==="slvl")G.subs[c.key]++;
else if(c.kind==="sub"){G.subs[c.key]=1;if(G.subLoadout.length<5)G.subLoadout.push(c.key);banner("获得 "+SUBS[c.key].name,"#b98cff",80);}
else if(c.kind==="dlvl")G.drones[c.key]++;
else if(c.kind==="drone"){G.drones[c.key]=1;if(G.droneLoadout.length<3)G.droneLoadout.push(c.key);banner("获得 "+DRONES[c.key].name,"#b98cff",80);}
else if(c.kind==="upg"){
G.u[c.key]++;
if(c.key==="nano"){G.maxHp+=25;G.hp=Math.min(G.maxHp,G.hp+40);}
if(c.key==="shield")G.shield=Math.min(shieldCap(),G.shield+1);
}else{
if(c.key==="heal")G.lives=Math.min(G.stat.maxLives,G.lives+1);
if(c.key==="coins")addCoins(50);
if(c.key==="od")G.od=100;
}
if(c.key&&c.kind!=="fill"){SAVE.codex[c.key]=true;persist();} // 图鉴收录
SFX.pick();
if(G.bossCard){G.bossCard=false;UI="play";show("");}
else{G.cardQ=(G.cardQ||1)-1;
if(G.cardQ>0)offerCards(false);
else{UI="play";show("");}}
}
/* ================= 图鉴:全屏实战模拟 =================
复用真实战斗引擎:只装备选定军械,杂兵+射手+精英持续来袭 */
function startDemoSim(kind,key){
const def=kind==="m"?WEAPONS[key]:kind==="s"?SUBS[key]:kind==="p"?PASSIVES[key]:DRONES[key];
newRun("stage","falcon",0);
G.p.inv=0; // 图鉴演示:取消出生保护闪烁
const lv=3;
G.demoSim={kind,key};
if(kind==="m"){G.weapons={};G.weapons[key]=lv;G.loadout=[key];}
else if(kind==="s"){G.weapons={vulcan:1};G.loadout=["vulcan"];G.subs={};G.subs[key]=lv;G.subLoadout=[key];}
else if(kind==="d"){G.weapons={};G.loadout=[];G.drones={};G.drones[key]=lv;G.droneLoadout=[key];}
else{G.weapons={vulcan:2};G.loadout=["vulcan"];G.u[key]=3;
if(key==="nano"){G.maxHp+=75;G.hp=G.maxHp;}
if(key==="shield")G.shield=shieldCap();}
$("demo-name").textContent=def.name;
const ds=typeof def.ds==="function"?def.ds(lv):def.ds;
$("demo-ds").textContent=(kind==="m"?"主武器 · ":kind==="s"?"副武器 · ":kind==="d"?"僚机 · ":"被动 · ")+ds;
UI="demoSim";show("o-demo");acc=0;
banner(def.name,"#41e6ff",90);
}
function exitDemoSim(){G=null;UI="codex";buildCodex();show("o-codex");}
function demoSpawn(){
const D=G.demoSim;
D.t=(D.t||0)-1;
if(D.t>0||G.enemies.length>=10)return;
D.t=Math.max(30,52-Math.floor(G.time/900)); // 渐进增密
const r=Math.random();
if(r<0.5)spawnEnemy(pick(["scout","scout","weaver","kami"]),rnd(20,W-20),-12,false); // 杂兵为主
else if(r<0.84)spawnEnemy(pick(["gunship","splitter","sniper","mine"]),rnd(20,W-20),-12,false); // 射手少数
else spawnEnemy(pick(["gunship","weaver","splitter"]),rnd(20,W-20),-12,true); // 精英
}
bestLine();