"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("");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 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",`
${s.name}
${s.trait}
`+(owned?"":`✦ ${s.cost} 解锁
`));
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=`${m.name}
${m.ds}
${"■".repeat(lv)}${"□".repeat(m.max-lv)}
`;
if(!maxed)d.querySelector("button").onclick=()=>{
if(SAVE.coins{
const sec=document.createElement("div");
sec.innerHTML=`${title}(${active.length}/${max})· 点击切换装备
`;
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='尚未获得';
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",`${owned?def.name:"???"}
`);
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=`${b.name}${best?" ✓":""}
${b.sub}
${best?("最快 "+Math.floor(best/60)+":"+("0"+best%60).slice(-2)):"未击破"}
`;
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=`${it.cat}
???
`;
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=
`最终得分${G.score}
${G.mode==="endless"?"抵达波次":G.mode==="rush"?"挑战目标":"抵达关卡"}${G.mode==="endless"?G.wave:G.mode==="rush"?BOSSES[G.rushIdx].name:G.stage+"-"+G.wave}
击坠数${G.kills}
强化等级Lv.${G.level}
存活时间${mins}分${secs}秒
持有军械${Object.keys(G.weapons).length}主/${Object.keys(G.subs).length}副/${Object.keys(G.drones).length}机`;
$("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",
`${i+1}
${nm}
${["普通","稀有","史诗"][rare]}
${ds}
`);
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();