starraid/star-raid/js/ui.js
JOJO 9edc366cf9 图鉴测试模式+巡飞弹新武器+存档云同步+机库合并+默认无尽
①图鉴隐藏测试模式:连点标题'军'字10次开关,纯内存标志位全部军械可见可演示,不动SAVE.codex(persist/submitRun读它,动了会污染后端),刷新失效
②24件主武器说明文案全部替换(用户提供梗文案);演示页介绍单行显示、去掉类别前缀、删'实战模拟·自动驾驶'提示行
③新主武器'巡飞弹'(loiter):慢攻速追踪导弹(制导引擎新增maxSpd上限5保持中速),命中留火场(zones新kind:fire纯粒子火舌,无圆形区域)+分裂4枚小炸弹快爆;新精灵loms;CODEX_KEYS同步63
④存档云同步:星币/机体/选中机体/永久强化/本地纪录上服务端(player_state表+POST /api/state+vState白名单清洗);login/me/register响应带state;客户端persist()钩子防抖600ms自动上传;服务端整体替换本地防串号,登出全清;老账号首登自动上传本地存档迁移
⑤机体选择/购买从出击准备并入机库强化(buildShips);出击默认无尽模式
服务端已重启生效;/api/state全链路在独立测试库curl验证通过(合法推送/回读/非法拦截清洗/401)
2026-08-06 12:15:17 +08:00

372 lines
18 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:"endless",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();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;
/* 图鉴测试模式隐藏入口连点图鉴标题“军”字10次开关。
纯内存开关——不动 SAVE.codexpersist/结算上报都读它,动了会污染后端),刷新页面即失效 */
let codexTestAll=false,cxClicks=0,cxLastClick=0;
$("cx-secret").onclick=()=>{
const now=Date.now();
if(now-cxLastClick>2000)cxClicks=0; // 间隔超2秒重新计数
cxLastClick=now;
if(++cxClicks<10)return;
cxClicks=0;codexTestAll=!codexTestAll;
SFX.pick();buildCodex();
};
$("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("");restartSameRun();};
$("b-quit").onclick=()=>{bank();G=null;MUS.stop();UI="title";bestLine();show("o-title");};
/* 再来一局:按刚结束那一局自己的 mode/ship/diff 重开(修复 SETUP 漂移导致进错模式,如 rush/闯关死后进了无尽) */
function restartSameRun(){
if(!G){startGame();return;}
if(G.mode==="rush"){startRush(G.rushIdx);return;}
SETUP.mode=G.mode;SETUP.ship=G.ship;SETUP.diff=G.diff;
startGame();
}
$("b-again").onclick=()=>restartSameRun();
$("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 buildShips(){
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();buildShop();}
else SFX.hurt();
return;
}
SETUP.ship=k;persist();buildShips(); // persist 触发云端同步选中机体
};
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;
buildShips();
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(); // 计数永远显示真实收集数,测试模式不影响
$("codex-test").style.display=codexTestAll?"":"none";
$("cx-secret").style.color=codexTestAll?"var(--gold)":""; // 开启后“军”字变金
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]||codexTestAll; // 测试模式:全部可见可演示
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,
codex:Object.keys(SAVE.codex)}); // 注意:不上报 localStorage 历史最高分SAVE.best 是浏览器级共享,会串号)
}
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=ds; // 只显示介绍本身单行不折行nowrap 在 index.html 上)
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();