图鉴测试模式+巡飞弹新武器+存档云同步+机库合并+默认无尽
①图鉴隐藏测试模式:连点标题'军'字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)
This commit is contained in:
parent
e178bc9f1b
commit
9edc366cf9
@ -99,5 +99,5 @@ star-raid/
|
|||||||
- 结算上报:`submitRun()`(ui.js)在 endRun 与 rush 胜利(enemies.js killBoss)两处调用;`G.submitted` 防重
|
- 结算上报:`submitRun()`(ui.js)在 endRun 与 rush 胜利(enemies.js killBoss)两处调用;`G.submitted` 防重
|
||||||
- 图鉴:服务端 `codex_unlocks` 为准,登陆时与本地 SAVE.codex 并集合并;未收集=剪影+???不可演示
|
- 图鉴:服务端 `codex_unlocks` 为准,登陆时与本地 SAVE.codex 并集合并;未收集=剪影+???不可演示
|
||||||
- 背景规则:render() 用 battleUI() 门控;**主页/基地背景=实战模拟**(home.js startHomeSim:随机3主Lv3+3副Lv3+2僚Lv2,G.homeSim 标记,复用 demoSim 自动驾驶刷怪,AU.simMute 静音,不伤/不计分/不上报,主循环看门狗自动重建);其余空闲=星空。离场必须 G=null(b-home/b-quit/buildRush 已处理)
|
- 背景规则:render() 用 battleUI() 门控;**主页/基地背景=实战模拟**(home.js startHomeSim:随机3主Lv3+3副Lv3+2僚Lv2,G.homeSim 标记,复用 demoSim 自动驾驶刷怪,AU.simMute 静音,不伤/不计分/不上报,主循环看门狗自动重建);其余空闲=星空。离场必须 G=null(b-home/b-quit/buildRush 已处理)
|
||||||
- 新增装备时必须同步 `server/api.js` 的 CODEX_KEYS 白名单(现 24+17+13+8=62)
|
- 新增装备时必须同步 `server/api.js` 的 CODEX_KEYS 白名单(现 25+17+13+8=63)
|
||||||
- 星币/机库/机体仍存 localStorage 不同步(v1 边界,见 DEPLOY.md)
|
- 星币/机体/选中机体/永久强化/本地纪录已上服务端(POST /api/state → player_state 表,服务端权威整体替换,登出清空;客户端 persist() 钩子防抖600ms上传)
|
||||||
|
|||||||
@ -53,7 +53,7 @@ server {
|
|||||||
|
|
||||||
- 邮箱仅作唯一标识,**无验证邮件、无找回密码**(需 SMTP 服务,留 v2)
|
- 邮箱仅作唯一标识,**无验证邮件、无找回密码**(需 SMTP 服务,留 v2)
|
||||||
- **反作弊仅为基础校验**(数值范围/限流):纯客户端游戏无法杜绝作弊,有志者改内存照样能伪造高分。如需严肃竞技,要加服务端回放校验(大工程)
|
- **反作弊仅为基础校验**(数值范围/限流):纯客户端游戏无法杜绝作弊,有志者改内存照样能伪造高分。如需严肃竞技,要加服务端回放校验(大工程)
|
||||||
- 星币/机库强化/机体解锁仍存浏览器 localStorage,**不随账号同步**(跨设备冲突解决复杂,留 v2)
|
- 星币/机体/永久强化/本地纪录已上服务端(`POST /api/state`,`player_state` 表,2026-08-06):登陆时服务端存档整体替换本地;最后登录的设备覆盖云端(无冲突合并);音乐/音效开关等设置仍存 localStorage 不同步
|
||||||
- 排行榜 = 全服单榜,无分页/赛季
|
- 排行榜 = 全服单榜,无分页/赛季
|
||||||
|
|
||||||
## 数据维护
|
## 数据维护
|
||||||
|
|||||||
@ -83,11 +83,9 @@
|
|||||||
<div class="h2">— 出击准备 —</div>
|
<div class="h2">— 出击准备 —</div>
|
||||||
<div class="h3">选择模式</div>
|
<div class="h3">选择模式</div>
|
||||||
<div class="pick" id="mode-pick">
|
<div class="pick" id="mode-pick">
|
||||||
<div class="pcard sel" data-m="stage"><div class="nm" style="margin-top:2px">闯关模式</div><div class="ds">6 大关卡<br>击破母巢核心<br>即获胜利</div></div>
|
<div class="pcard" data-m="stage"><div class="nm" style="margin-top:2px">闯关模式</div><div class="ds">6 大关卡<br>击破母巢核心<br>即获胜利</div></div>
|
||||||
<div class="pcard" data-m="endless"><div class="nm" style="margin-top:2px">无尽模式</div><div class="ds">无限波次<br>突变词缀<br>冲击最高分</div></div>
|
<div class="pcard sel" data-m="endless"><div class="nm" style="margin-top:2px">无尽模式</div><div class="ds">无限波次<br>突变词缀<br>冲击最高分</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h3">选择机体</div>
|
|
||||||
<div class="pick" id="ship-pick"></div>
|
|
||||||
<div class="h3">难度</div>
|
<div class="h3">难度</div>
|
||||||
<div class="row" id="diff-pick">
|
<div class="row" id="diff-pick">
|
||||||
<button class="btn sel-diff primary" data-d="0" style="min-width:110px;padding:8px 14px">普通</button>
|
<button class="btn sel-diff primary" data-d="0" style="min-width:110px;padding:8px 14px">普通</button>
|
||||||
@ -117,9 +115,12 @@
|
|||||||
|
|
||||||
<!-- 机库 -->
|
<!-- 机库 -->
|
||||||
<div class="ovl hide" id="o-hangar">
|
<div class="ovl hide" id="o-hangar">
|
||||||
<div class="h2">— 机库 · 永久强化 —</div>
|
<div class="h2">— 机库 · 强化 —</div>
|
||||||
<div class="wallet">星币 ✦ <span id="wallet">0</span></div>
|
<div class="wallet">星币 ✦ <span id="wallet">0</span></div>
|
||||||
<div class="shop" id="shop"></div>
|
<div class="h3">机体(选择 / 解锁)</div>
|
||||||
|
<div class="pick" id="ship-pick"></div>
|
||||||
|
<div class="h3">永久强化</div>
|
||||||
|
<div class="shop" id="shop" style="overflow-y:auto;max-height:42%"></div>
|
||||||
<button class="btn" id="b-back2" style="margin-top:6px">返回</button>
|
<button class="btn" id="b-back2" style="margin-top:6px">返回</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -144,8 +145,8 @@
|
|||||||
|
|
||||||
<!-- 图鉴 -->
|
<!-- 图鉴 -->
|
||||||
<div class="ovl hide" id="o-codex">
|
<div class="ovl hide" id="o-codex">
|
||||||
<div class="h2">— 军械图鉴 —</div>
|
<div class="h2">— <span id="cx-secret" style="user-select:none">军</span>械图鉴 —</div>
|
||||||
<div class="h3">已收集 <span id="codex-n">0</span>/<span id="codex-t">0</span> · 点击可实时演示</div>
|
<div class="h3">已收集 <span id="codex-n">0</span>/<span id="codex-t">0</span> · 点击可实时演示<span id="codex-test" title="临时解锁全部军械查看/演示,不写入后端,刷新页面即失效" style="display:none;color:var(--gold)"> · ⚠ 测试模式</span></div>
|
||||||
<div id="codex" style="overflow-y:auto;max-height:72%;width:100%;display:flex;flex-direction:column;align-items:center"></div>
|
<div id="codex" style="overflow-y:auto;max-height:72%;width:100%;display:flex;flex-direction:column;align-items:center"></div>
|
||||||
<button class="btn" id="b-back4">返回</button>
|
<button class="btn" id="b-back4">返回</button>
|
||||||
</div>
|
</div>
|
||||||
@ -154,8 +155,7 @@
|
|||||||
<div class="ovl hide" id="o-demo" style="background:transparent;pointer-events:none;justify-content:space-between;overflow:hidden">
|
<div class="ovl hide" id="o-demo" style="background:transparent;pointer-events:none;justify-content:space-between;overflow:hidden">
|
||||||
<div style="margin-top:3%">
|
<div style="margin-top:3%">
|
||||||
<div class="h2" id="demo-name">演示</div>
|
<div class="h2" id="demo-name">演示</div>
|
||||||
<div class="h3" id="demo-ds" style="max-width:88%;margin:0 auto"></div>
|
<div class="h3" id="demo-ds" style="margin:0 auto;white-space:nowrap"></div>
|
||||||
<div class="h3" style="color:var(--dim);margin-top:4px">实战模拟 · 自动驾驶 · 触屏/方向键可接管</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" id="b-back5" style="pointer-events:auto;margin-bottom:4%">返回图鉴</button>
|
<button class="btn" id="b-back5" style="pointer-events:auto;margin-bottom:4%">返回图鉴</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -93,7 +93,7 @@ function updateBullets(){
|
|||||||
if(b.softH){ // 星云奥秘:缓慢转向,不加速
|
if(b.softH){ // 星云奥秘:缓慢转向,不加速
|
||||||
const na=c+clamp(d,-0.05,0.05),s=Math.hypot(b.vx,b.vy);
|
const na=c+clamp(d,-0.05,0.05),s=Math.hypot(b.vx,b.vy);
|
||||||
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;
|
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;
|
||||||
}else{const na=c+clamp(d,-0.16,0.16),s=Math.min(6,Math.hypot(b.vx,b.vy)+0.08);
|
}else{const na=c+clamp(d,-0.16,0.16),s=Math.min(b.maxSpd||6,Math.hypot(b.vx,b.vy)+0.08);
|
||||||
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;}}}
|
b.vx=Math.cos(na)*s;b.vy=Math.sin(na)*s;}}}
|
||||||
if(b.back){b.bt--;if(b.bt<=0&&b.vy<0){b.vy*=-1;b.vx*=-0.4;}}
|
if(b.back){b.bt--;if(b.bt<=0&&b.vy<0){b.vy*=-1;b.vx*=-0.4;}}
|
||||||
if(b.curve){const c=Math.atan2(b.vy,b.vx)+b.curve,s=Math.hypot(b.vx,b.vy)||0.1;
|
if(b.curve){const c=Math.atan2(b.vy,b.vx)+b.curve,s=Math.hypot(b.vx,b.vy)||0.1;
|
||||||
@ -232,7 +232,7 @@ function collide(){
|
|||||||
areaDamage(b.x,b.y,18,b.dmg*0.5);
|
areaDamage(b.x,b.y,18,b.dmg*0.5);
|
||||||
explodeFx(b.x,b.y,"plasma",0.8);}
|
explodeFx(b.x,b.y,"plasma",0.8);}
|
||||||
SFX.hit();
|
SFX.hit();
|
||||||
if(b.dead){if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex")explodeOnce(b);break;}
|
if(b.dead){if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex"||b.t==="lm"||b.t==="lb")explodeOnce(b);break;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!b.dead&&G.boss){
|
if(!b.dead&&G.boss){
|
||||||
@ -240,7 +240,7 @@ function collide(){
|
|||||||
if(bossHit(b.x,b.y,b.r)){
|
if(bossHit(b.x,b.y,b.r)){
|
||||||
if(b.pierce){if(!b.hits)b.hits={};if(!b.hits.boss){b.hits.boss=1;hurtBoss(b.dmg);}}
|
if(b.pierce){if(!b.hits)b.hits={};if(!b.hits.boss){b.hits.boss=1;hurtBoss(b.dmg);}}
|
||||||
else{b.dead=true;
|
else{b.dead=true;
|
||||||
if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex")explodeOnce(b);
|
if(b.t==="sg"||b.t==="ho"||b.t==="bb"||b.t==="am"||b.t==="tb"||b.t==="tb2"||b.t==="dw"||b.t==="ph"||b.t==="nb"||b.t==="ex"||b.t==="lm"||b.t==="lb")explodeOnce(b);
|
||||||
else hurtBoss(b.dmg);}
|
else hurtBoss(b.dmg);}
|
||||||
SFX.hit();
|
SFX.hit();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,8 @@ function loadSave(){
|
|||||||
SAVE.m=SAVE.m||{}; SAVE.ships=SAVE.ships||["falcon"]; SAVE.best=SAVE.best||{s0:0,s1:0,e0:0,e1:0,stage:0};
|
SAVE.m=SAVE.m||{}; SAVE.ships=SAVE.ships||["falcon"]; SAVE.best=SAVE.best||{s0:0,s1:0,e0:0,e1:0,stage:0};
|
||||||
SAVE.codex=SAVE.codex||{};
|
SAVE.codex=SAVE.codex||{};
|
||||||
}
|
}
|
||||||
function persist(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(SAVE));}catch(e){}}
|
function persist(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(SAVE));}catch(e){}
|
||||||
|
if(typeof NET!=="undefined")NET.pushState();} // 登录态下云端同步(NET 内防抖;未登录自动跳过)
|
||||||
loadSave();
|
loadSave();
|
||||||
|
|
||||||
/* ---------------- 平衡数值总表 ----------------
|
/* ---------------- 平衡数值总表 ----------------
|
||||||
|
|||||||
@ -251,6 +251,16 @@ function explodeBullet(b){
|
|||||||
areaDamage(b.x,b.y,b.aoe||26,b.dmg);
|
areaDamage(b.x,b.y,b.aoe||26,b.dmg);
|
||||||
explodeFx(b.x,b.y,"executor",1);
|
explodeFx(b.x,b.y,"executor",1);
|
||||||
SFX.boom(true);G.shake=Math.max(G.shake,8);
|
SFX.boom(true);G.shake=Math.max(G.shake,8);
|
||||||
|
}else if(b.t==="lm"){ // 巡飞弹:原地火场(kind:fire)+ 分裂4枚小炸弹很快起爆
|
||||||
|
areaDamage(b.x,b.y,b.zone||20,b.dmg);
|
||||||
|
G.zones.push({x:b.x,y:b.y,r:b.zone||20,dps:b.dmg*0.12,life:b.fire||95,col:"#ff8a2e",kind:"fire"});
|
||||||
|
for(let i=0;i<4;i++){const la=-Math.PI/2+(i-1.5)*0.85+rnd(-0.2,0.2);
|
||||||
|
pbul(b.x,b.y,Math.cos(la)*2.8,Math.sin(la)*2.8,b.dmg*0.35,{t:"lb",r:2,bt:12+rndi(0,6),aoe:12,trailCol:"#ff9a3d"});}
|
||||||
|
explodeFx(b.x,b.y,"fire",1.2);
|
||||||
|
SFX.boom(false);G.shake=Math.max(G.shake,3);
|
||||||
|
}else if(b.t==="lb"){ // 巡飞弹小炸弹:小范围快爆
|
||||||
|
areaDamage(b.x,b.y,b.aoe||12,b.dmg);
|
||||||
|
explodeFx(b.x,b.y,"fire",0.6);SFX.boom(false);
|
||||||
}else if(b.t==="tb"||b.t==="tb2"){
|
}else if(b.t==="tb"||b.t==="tb2"){
|
||||||
areaDamage(b.x,b.y,b.aoe||b.zone||24,b.dmg);
|
areaDamage(b.x,b.y,b.aoe||b.zone||24,b.dmg);
|
||||||
explodeFx(b.x,b.y,"boom",1);
|
explodeFx(b.x,b.y,"boom",1);
|
||||||
@ -358,6 +368,11 @@ function fireWeapons(){
|
|||||||
for(let i=0;i<st.n;i++)pbul(p.x+(i-(st.n-1)/2)*6,p.y-4,(i-(st.n-1)/2)*0.9,-st.spd,st.dmg*wdm,{t:"m",spr:"msl2",homing:true,r:3,aoe:st.aoe,trailCol:"#e8e8e8"});
|
for(let i=0;i<st.n;i++)pbul(p.x+(i-(st.n-1)/2)*6,p.y-4,(i-(st.n-1)/2)*0.9,-st.spd,st.dmg*wdm,{t:"m",spr:"msl2",homing:true,r:3,aoe:st.aoe,trailCol:"#e8e8e8"});
|
||||||
SFX.shoot();}
|
SFX.shoot();}
|
||||||
break;
|
break;
|
||||||
|
case "loiter": // 巡飞弹:慢攻速+追踪(中速上限5),命中留火场+分裂4枚快爆小炸弹
|
||||||
|
if(G.wt[wk]<=0){G.wt[wk]=Math.max(30,st.int/fm);
|
||||||
|
pbul(p.x,p.y-10,0,-st.spd,st.dmg*wdm,{t:"lm",spr:"loms",r:4,zone:st.zone,fire:st.fire,homing:true,maxSpd:5,trailCol:"#ff8a2e"});
|
||||||
|
SFX.shoot();}
|
||||||
|
break;
|
||||||
case "bubble":
|
case "bubble":
|
||||||
if(G.wt[wk]<=0){G.wt[wk]=Math.max(6,st.int/fm);
|
if(G.wt[wk]<=0){G.wt[wk]=Math.max(6,st.int/fm);
|
||||||
pbul(p.x,p.y-10,rnd(-0.3,0.3),-st.spd,st.dmg*wdm,{t:"bb",spr:"bub",aoe:st.aoe,pierce:true,life:150,r:6});}
|
pbul(p.x,p.y-10,rnd(-0.3,0.3),-st.spd,st.dmg*wdm,{t:"bb",spr:"bub",aoe:st.aoe,pierce:true,life:150,r:6});}
|
||||||
|
|||||||
@ -12,57 +12,59 @@ const SHIPS={
|
|||||||
asura:{name:"修罗",spr:"ship4",cost:5000,trait:"暴击+15% · 伤害+10% · 命-1",
|
asura:{name:"修罗",spr:"ship4",cost:5000,trait:"暴击+15% · 伤害+10% · 命-1",
|
||||||
mod:{spd:1,dmg:1.1,lives:-1,crit:0.15,dodge:0,hitR:0,mag:1,bombs:1,shield:0,drone:0,droneBoost:1}}
|
mod:{spd:1,dmg:1.1,lives:-1,crit:0.15,dodge:0,hitR:0,mag:1,bombs:1,shield:0,drone:0,droneBoost:1}}
|
||||||
};
|
};
|
||||||
/* ================= 军械库:21 主武器 =================
|
/* ================= 军械库:25 主武器 =================
|
||||||
beh: 行为引擎类型 lvl(w): 该等级的战斗参数 */
|
beh: 行为引擎类型 lvl(w): 该等级的战斗参数 */
|
||||||
const WEAPONS={
|
const WEAPONS={
|
||||||
vulcan:{name:"火神机炮",spr:"trcr",rare:0,beh:"streams",ds:"金色曳光弹雨,可靠的老伙计",
|
vulcan:{name:"火神机炮",spr:"trcr",rare:0,beh:"streams",ds:"!?神神?!",
|
||||||
lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:8,spd:5.4,gap:6})},
|
lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:8,spd:5.4,gap:6})},
|
||||||
shotgun:{name:"超级霰弹",spr:"pelt",rare:0,beh:"streams",ds:"弹丸只从两翼倾泻,中央中空",
|
shotgun:{name:"超级霰弹",spr:"pelt",rare:0,beh:"streams",ds:"“友谊颗粒”",
|
||||||
lvl:w=>({n:5+2*w,dmg:4.2*(1+.18*(w-1)),int:22,spd:5.5,fan:0.5,gap:2,skipMid:1})},
|
lvl:w=>({n:5+2*w,dmg:4.2*(1+.18*(w-1)),int:22,spd:5.5,fan:0.5,gap:2,skipMid:1})},
|
||||||
needle:{name:"蜂针刺弹",spr:"ndl",rare:0,beh:"streams",ds:"淬毒细针,命中后持续蚀甲",
|
needle:{name:"蜂针刺弹",spr:"ndl",rare:0,beh:"streams",ds:"这可是吐了剧毒的飞刀",
|
||||||
lvl:w=>({n:1+Math.floor(w/3),dmg:2.6*(1+.2*(w-1)),int:3,spd:7,gap:4,poison:100})},
|
lvl:w=>({n:1+Math.floor(w/3),dmg:2.6*(1+.2*(w-1)),int:3,spd:7,gap:4,poison:100})},
|
||||||
flame:{name:"火焰喷射器",icon:"flame",rare:0,beh:"flame",ds:"周期性喷吐的宽幅烈焰,近身湮灭",
|
flame:{name:"火焰喷射器",icon:"flame",rare:0,beh:"flame",ds:"让火焰净化一切",
|
||||||
lvl:w=>({n:3+Math.floor(w/2),dmg:1.1*(1+.22*(w-1)),range:70+8*w,burst:34+4*w,cd:Math.max(46,74-5*w)})},
|
lvl:w=>({n:3+Math.floor(w/2),dmg:1.1*(1+.22*(w-1)),range:70+8*w,burst:34+4*w,cd:Math.max(46,74-5*w)})},
|
||||||
shotgun2:{name:"霜冻霰弹",spr:"shard",rare:1,beh:"streams",ds:"冰晶霰弹,大面积冻结减速",
|
shotgun2:{name:"霜冻霰弹",spr:"shard",rare:1,beh:"streams",ds:"冷冷冷",
|
||||||
lvl:w=>({n:3+w,dmg:3*(1+.18*(w-1)),int:23,spd:5.2,fan:0.42,gap:2,slow:80+25*w})},
|
lvl:w=>({n:3+w,dmg:3*(1+.18*(w-1)),int:23,spd:5.2,fan:0.42,gap:2,slow:80+25*w})},
|
||||||
tesla:{name:"电弧发生器",icon:"chain",rare:1,beh:"tesla",ds:"自动电击周围敌人,无处可逃",
|
tesla:{name:"电弧发生器",icon:"chain",rare:1,beh:"tesla",ds:"?!电电!?",
|
||||||
lvl:w=>({n:1+Math.floor(w/2),dmg:8*(1+.25*(w-1)),int:44-2*w,range:110})},
|
lvl:w=>({n:1+Math.floor(w/2),dmg:8*(1+.25*(w-1)),int:44-2*w,range:110})},
|
||||||
railgun:{name:"电磁炮",icon:"rail",rare:2,beh:"rail",ds:"蓄力磁轨贯穿敌阵,命中留下电浆区",
|
railgun:{name:"电磁炮",icon:"rail",rare:2,beh:"rail",ds:"带轮子的垃圾桶",
|
||||||
lvl:w=>({dmg:55*(1+.3*(w-1)),charge:120-10*w,spd:11,zone:24+4*w})},
|
lvl:w=>({dmg:55*(1+.3*(w-1)),charge:120-10*w,spd:11,zone:24+4*w})},
|
||||||
deathray:{name:"死光",icon:"laser",rare:2,beh:"deathray",ds:"蓄力后自动锁敌的死亡射线",
|
deathray:{name:"死光",icon:"laser",rare:2,beh:"deathray",ds:"记得带镜子",
|
||||||
lvl:w=>({dps:0.9*(1+.28*(w-1)),charge:160-12*w,beam:55,width:5+2*w})},
|
lvl:w=>({dps:0.9*(1+.28*(w-1)),charge:160-12*w,beam:55,width:5+2*w})},
|
||||||
crescent:{name:"月牙斩",spr:"cres",rare:1,beh:"crescent",ds:"巨型旋刃缓慢斩出,贯穿敌阵",
|
crescent:{name:"月牙斩",spr:"cres",rare:1,beh:"crescent",ds:"青豆?",
|
||||||
lvl:w=>({dmg:44*(1+.24*(w-1)),int:120,spd:3.4})},
|
lvl:w=>({dmg:44*(1+.24*(w-1)),int:120,spd:3.4})},
|
||||||
fission:{name:"分裂弹",spr:"fiss",rare:1,beh:"fission",ds:"飞行一段距离后炸裂为散射弹幕",
|
fission:{name:"分裂弹",spr:"fiss",rare:1,beh:"fission",ds:"四阶村长",
|
||||||
lvl:w=>({dmg:6*(1+.22*(w-1)),int:20,spd:5.2,split:3+Math.floor(w/2),splitAt:26})},
|
lvl:w=>({dmg:6*(1+.22*(w-1)),int:20,spd:5.2,split:3+Math.floor(w/2),splitAt:26})},
|
||||||
frost:{name:"冰冻枪",spr:"icelance",rare:1,beh:"frost",ds:"贯穿冰枪,命中将敌人彻底冻结",
|
frost:{name:"冰冻枪",spr:"icelance",rare:1,beh:"frost",ds:"冷冷冷冷",
|
||||||
lvl:w=>({dmg:6*(1+.2*(w-1)),int:16,spd:6,freeze:70+25*w})},
|
lvl:w=>({dmg:6*(1+.2*(w-1)),int:16,spd:6,freeze:70+25*w})},
|
||||||
swarm:{name:"蜂巢导弹",spr:"msl2",rare:1,beh:"homing",ds:"自动追踪的导弹蜂群,命中爆裂",
|
swarm:{name:"蜂巢导弹",spr:"msl2",rare:1,beh:"homing",ds:"不可用于对付肉山",
|
||||||
lvl:w=>({n:1+w,dmg:2.2*(1+.2*(w-1)),int:22,spd:3.2,aoe:10})},
|
lvl:w=>({n:1+w,dmg:2.2*(1+.2*(w-1)),int:22,spd:3.2,aoe:10})},
|
||||||
bubble:{name:"重力泡泡",spr:"bub",rare:1,beh:"bubble",ds:"缓慢漂浮的大泡泡,爆裂冲击",
|
bubble:{name:"重力泡泡",spr:"bub",rare:1,beh:"bubble",ds:"天呐!是海绵宝宝",
|
||||||
lvl:w=>({dmg:18*(1+.25*(w-1)),int:26,spd:1.4,aoe:30+4*w})},
|
lvl:w=>({dmg:18*(1+.25*(w-1)),int:26,spd:1.4,aoe:30+4*w})},
|
||||||
starfall:{name:"流星火雨",spr:"met",rare:2,beh:"starfall",ds:"流星自脚下逆冲而起,轰炸前方空域",
|
starfall:{name:"流星火雨",spr:"met",rare:2,beh:"starfall",ds:"天火",
|
||||||
lvl:w=>({n:3+Math.floor(w/2),dmg:16*(1+.25*(w-1)),int:150-8*w,aoe:16})},
|
lvl:w=>({n:3+Math.floor(w/2),dmg:16*(1+.25*(w-1)),int:150-8*w,aoe:16})},
|
||||||
boomerang:{name:"回旋镖",spr:"boomer",rare:0,beh:"boom",ds:"亮蓝回旋刃,远射贯穿后回旋返航",
|
boomerang:{name:"回旋镖",spr:"boomer",rare:0,beh:"boom",ds:"但愿不会打到自己",
|
||||||
lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:21,spd:4.8})},
|
lvl:w=>({n:1+Math.floor(w/2),dmg:7*(1+.22*(w-1)),int:21,spd:4.8})},
|
||||||
singularity:{name:"奇点炮",spr:"sing",rare:2,beh:"sing",ds:"发射微型黑洞,牵引后剧烈坍缩",
|
singularity:{name:"奇点炮",spr:"sing",rare:2,beh:"sing",ds:"雷工蛋?",
|
||||||
lvl:w=>({dmg:40*(1+.3*(w-1)),int:200,pull:88,aoe:62,fuse:160})}, // 牵引/爆炸范围与射程增强
|
lvl:w=>({dmg:40*(1+.3*(w-1)),int:200,pull:88,aoe:62,fuse:160})}, // 牵引/爆炸范围与射程增强
|
||||||
wavecannon:{name:"波动炮",spr:"waveb",rare:2,beh:"wave",ds:"蓄力斩出贯穿敌阵的剑气弧光",
|
wavecannon:{name:"波动炮",spr:"waveb",rare:2,beh:"wave",ds:"炼狱无双波动炮",
|
||||||
lvl:w=>({dmg:32*(1+.3*(w-1)),charge:100-8*w,spd:5,width:14+6*w})},
|
lvl:w=>({dmg:32*(1+.3*(w-1)),charge:100-8*w,spd:5,width:14+6*w})},
|
||||||
holy:{name:"圣光弹",spr:"holyo",rare:2,beh:"holy",ds:"爆裂为旋转生长的圣光十字",
|
holy:{name:"圣光弹",spr:"holyo",rare:2,beh:"holy",ds:"哇哦",
|
||||||
lvl:w=>({dmg:13*(1+.25*(w-1)),int:65,spd:3,len:16+3*w})},
|
lvl:w=>({dmg:13*(1+.25*(w-1)),int:65,spd:3,len:16+3*w})},
|
||||||
thundercall:{name:"落雷术",spr:"thnd",rare:1,beh:"thunder",ds:"呼唤天雷轰击前方区域",
|
thundercall:{name:"落雷术",spr:"thnd",rare:1,beh:"thunder",ds:"何方道友在此渡劫",
|
||||||
lvl:w=>({n:2+Math.floor(w/2),dmg:12*(1+.25*(w-1)),int:90-4*w,aoe:13})},
|
lvl:w=>({n:2+Math.floor(w/2),dmg:12*(1+.25*(w-1)),int:90-4*w,aoe:13})},
|
||||||
arcshu:{name:"电弧手里剑",spr:"shuri",rare:2,beh:"shuriken",ds:"旋转雷刃一路放电,贯穿全场",
|
arcshu:{name:"电弧手里剑",spr:"shuri",rare:2,beh:"shuriken",ds:"这个很熟悉了吧",
|
||||||
lvl:w=>({dmg:6*(1+.22*(w-1)),int:95-4*w,spd:2.2,zapDmg:5*(1+.25*(w-1)),pulse:7})}, // 电弧释放速度×3
|
lvl:w=>({dmg:6*(1+.22*(w-1)),int:95-4*w,spd:2.2,zapDmg:5*(1+.25*(w-1)),pulse:7})}, // 电弧释放速度×3
|
||||||
volt:{name:"电能机炮",spr:"voltb",rare:1,beh:"volt",ds:"高频电能弹,命中后电弧连锁跳跃",
|
volt:{name:"电能机炮",spr:"voltb",rare:1,beh:"volt",ds:"疑似电大",
|
||||||
lvl:w=>({n:1+Math.floor(w/3),dmg:3.2*(1+.2*(w-1)),int:5,spd:6.2,gap:5,chain:2+Math.floor(w/2)})},
|
lvl:w=>({n:1+Math.floor(w/3),dmg:3.2*(1+.2*(w-1)),int:5,spd:6.2,gap:5,chain:2+Math.floor(w/2)})},
|
||||||
dawn:{name:"黎明重炮",spr:"dawnsh",rare:2,beh:"dawn",ds:"长蓄力重炮,一炮轰出黎明",
|
dawn:{name:"黎明重炮",spr:"dawnsh",rare:2,beh:"dawn",ds:"祖宗发射器",
|
||||||
lvl:w=>({dmg:70*(1+.3*(w-1)),charge:170-12*w,spd:5,aoe:30+3*w})},
|
lvl:w=>({dmg:70*(1+.3*(w-1)),charge:170-12*w,spd:5,aoe:30+3*w})},
|
||||||
proto:{name:"协议武装",spr:"protb",rare:2,beh:"protocol",ds:"青蓝连发压制,周期性蓄能重炮",
|
proto:{name:"协议武装",spr:"protb",rare:2,beh:"protocol",ds:"搜索,歼灭",
|
||||||
lvl:w=>({dmg:3*(1+.2*(w-1)),int:6,spd:6.5,heavy:120-8*w,hdmg:36*(1+.28*(w-1)),haoe:26+3*w})},
|
lvl:w=>({dmg:3*(1+.2*(w-1)),int:6,spd:6.5,heavy:120-8*w,hdmg:36*(1+.28*(w-1)),haoe:26+3*w})},
|
||||||
nebula:{name:"星云奥秘",spr:"neb",rare:2,beh:"nebula",ds:"缓慢追踪的奥术漩涡,命中绽放群星",
|
nebula:{name:"星云奥秘",spr:"neb",rare:2,beh:"nebula",ds:"字面意思",
|
||||||
lvl:w=>({dmg:12*(1+.25*(w-1)),int:100-5*w,spd:1.6,stars:10+2*w})}
|
lvl:w=>({dmg:12*(1+.25*(w-1)),int:100-5*w,spd:1.6,stars:10+2*w})},
|
||||||
|
loiter:{name:"巡飞弹",spr:"loms",rare:1,beh:"loiter",ds:"堵桥来",
|
||||||
|
lvl:w=>({dmg:22*(1+.25*(w-1)),int:68-5*w,spd:4.2,zone:20+3*w,fire:95+15*w})}
|
||||||
};
|
};
|
||||||
const MAINS=Object.keys(WEAPONS);
|
const MAINS=Object.keys(WEAPONS);
|
||||||
/* ================= 18 副武器 ================= */
|
/* ================= 18 副武器 ================= */
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
const NET = {
|
const NET = {
|
||||||
user: null, // {username,email}
|
user: null, // {username,email}
|
||||||
stats: null, // {bossKills,endlessHigh,runs}
|
stats: null, // {bossKills,endlessHigh,runs}
|
||||||
codexTotal: 62,
|
codexTotal: 63,
|
||||||
online: false,
|
online: false,
|
||||||
|
|
||||||
async api(path, method, body) {
|
async api(path, method, body) {
|
||||||
@ -45,7 +45,12 @@ const NET = {
|
|||||||
async logout() {
|
async logout() {
|
||||||
await this.api("/api/logout", "POST");
|
await this.api("/api/logout", "POST");
|
||||||
this.user = null; this.stats = null; this.online = false;
|
this.user = null; this.stats = null; this.online = false;
|
||||||
SAVE.codex = {}; persist(); // 防同机下一账号继承图鉴
|
/* 防同机下一账号继承:图鉴/星币/机体/强化/纪录全部清空(user 已置空,persist 不会触发上传) */
|
||||||
|
SAVE.codex = {};
|
||||||
|
SAVE.coins = 0; SAVE.ships = ["falcon"]; SAVE.m = {};
|
||||||
|
SAVE.best = { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }; SAVE.rush = {};
|
||||||
|
if (typeof SETUP !== "undefined") SETUP.ship = "falcon";
|
||||||
|
persist();
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 应用服务端返回的用户数据:图鉴以服务端为准【替换】本地
|
/* 应用服务端返回的用户数据:图鉴以服务端为准【替换】本地
|
||||||
@ -53,11 +58,38 @@ const NET = {
|
|||||||
_apply(d) {
|
_apply(d) {
|
||||||
this.user = d.user;
|
this.user = d.user;
|
||||||
this.stats = d.stats;
|
this.stats = d.stats;
|
||||||
this.codexTotal = d.codexTotal || 62;
|
this.codexTotal = d.codexTotal || 63;
|
||||||
this.online = true;
|
this.online = true;
|
||||||
SAVE.codex = {};
|
SAVE.codex = {};
|
||||||
if (Array.isArray(d.codex)) for (const k of d.codex) SAVE.codex[k] = true;
|
if (Array.isArray(d.codex)) for (const k of d.codex) SAVE.codex[k] = true;
|
||||||
|
/* 服务端存档权威:整体替换本地(防串号,与图鉴同理);替换期间禁止回传 */
|
||||||
|
this._mutePush = true;
|
||||||
|
if (d.state) {
|
||||||
|
SAVE.coins = d.state.coins;
|
||||||
|
SAVE.ships = d.state.ships.slice();
|
||||||
|
SAVE.m = Object.assign({}, d.state.m);
|
||||||
|
SAVE.best = Object.assign({ s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }, d.state.best);
|
||||||
|
SAVE.rush = Object.assign({}, d.state.rush);
|
||||||
|
if (d.state.ship && SAVE.ships.includes(d.state.ship) && typeof SETUP !== "undefined") SETUP.ship = d.state.ship;
|
||||||
|
}
|
||||||
persist();
|
persist();
|
||||||
|
this._mutePush = false;
|
||||||
|
if (!d.state) this.pushState(); // 服务端无存档(老账号首登):把本地存档上传初始化
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 存档云同步(防抖 600ms):星币/机体/选中机体/永久强化/本地纪录。任何 persist 都会触发 */
|
||||||
|
_stateT: null,
|
||||||
|
pushState() {
|
||||||
|
if (!this.user || this._mutePush) return;
|
||||||
|
clearTimeout(this._stateT);
|
||||||
|
this._stateT = setTimeout(() => {
|
||||||
|
if (!this.user) return;
|
||||||
|
this.api("/api/state", "POST", { state: {
|
||||||
|
coins: SAVE.coins, ships: SAVE.ships,
|
||||||
|
ship: typeof SETUP !== "undefined" ? SETUP.ship : "falcon",
|
||||||
|
m: SAVE.m, best: SAVE.best, rush: SAVE.rush || {},
|
||||||
|
} }).catch(() => { /* 网络失败静默:本地存档仍在 */ });
|
||||||
|
}, 600);
|
||||||
},
|
},
|
||||||
|
|
||||||
/* 结算上报(fire-and-forget):成功后回写最新统计 */
|
/* 结算上报(fire-and-forget):成功后回写最新统计 */
|
||||||
|
|||||||
@ -329,6 +329,11 @@ function render(){
|
|||||||
if(G.time%9===0)G.parts.push({x:z.x+rnd(-z.r/2,z.r/2),y:z.y+rnd(-z.r/2,z.r/2),vx:0,vy:-0.2,life:10,col:"#43e8ff",sz:1});
|
if(G.time%9===0)G.parts.push({x:z.x+rnd(-z.r/2,z.r/2),y:z.y+rnd(-z.r/2,z.r/2),vx:0,vy:-0.2,life:10,col:"#43e8ff",sz:1});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if(z.kind==="fire"){ // 巡飞弹火场:纯粒子火舌上卷+黑烟(无圆形区域)
|
||||||
|
if(G.time%3===0)for(let i=0;i<2;i++)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.7,y:z.y+rnd(-1,1)*z.r*0.5,vx:rnd(-0.25,0.25),vy:rnd(-1.3,-0.5),life:rndi(12,22),col:pick(["#ff8a2e","#ffc64a","#ff4a1f"]),sz:rndi(1,2)});
|
||||||
|
if(G.time%8===0)G.parts.push({x:z.x+rnd(-1,1)*z.r*0.5,y:z.y-z.r*0.3,vx:rnd(-0.15,0.15),vy:rnd(-0.7,-0.3),life:rndi(24,38),col:"#5a5a6a",sz:2});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ctx.globalAlpha=0.16;ctx.fillStyle=z.col;
|
ctx.globalAlpha=0.16;ctx.fillStyle=z.col;
|
||||||
ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.fill();
|
ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,TAU);ctx.fill();
|
||||||
ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2;ctx.strokeStyle=z.col;
|
ctx.globalAlpha=0.5+Math.sin(G.time*0.1)*0.2;ctx.strokeStyle=z.col;
|
||||||
|
|||||||
@ -148,6 +148,7 @@ SPR.icelance=bake(["...K...","..KCK..","..KCK..",".KCWK.",".KCCK.",".KCCK.","..K
|
|||||||
SPR.msl2=bake(["..K..",".KYK.","KYGYK","KYGYK",".KGK.","K.K.K"],{K:"#2a240d",Y:"#ffe95c",G:"#8ca86a"}); // 蜂巢小导弹
|
SPR.msl2=bake(["..K..",".KYK.","KYGYK","KYGYK",".KGK.","K.K.K"],{K:"#2a240d",Y:"#ffe95c",G:"#8ca86a"}); // 蜂巢小导弹
|
||||||
SPR.amsl=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0763e",W:"#fff4d9"}); // 空爆重型导弹
|
SPR.amsl=bake(["...K...","..KRK..",".KRRRK.","KRWRKRK",".KRRRK.",".KRRRK.","K.KRK.K"],{K:"#241a08",R:"#e0763e",W:"#fff4d9"}); // 空爆重型导弹
|
||||||
SPR.cmsl=bake(["...KK...","..KRRK..",".KRRRRK.","KRWRRWRK",".KRRRRK.",".KRRRRK.","..KRRK..",".K.KK.K."],{K:"#1a0d0d",R:"#e0393e",W:"#ffd23d"}); // 斩首巡航导弹
|
SPR.cmsl=bake(["...KK...","..KRRK..",".KRRRRK.","KRWRRWRK",".KRRRRK.",".KRRRRK.","..KRRK..",".K.KK.K."],{K:"#1a0d0d",R:"#e0393e",W:"#ffd23d"}); // 斩首巡航导弹
|
||||||
|
SPR.loms=bake(["...W...","..KWK..",".KOOOK.","KKOOOKK","..KOK..","..KOK..","...K..."],{K:"#26160d",W:"#fff4d9",O:"#ff8a2e"}); // 巡飞弹:白尖橙体+翼横杆(与msl2/cmsl/amsl区分)
|
||||||
SPR.broad=bake([".KKK.","KBBBK","KBWBK","KBBBK",".KKK."],{K:"#241a08",B:"#c8923e",W:"#fff4d9"}); // 侧舷炮弹
|
SPR.broad=bake([".KKK.","KBBBK","KBWBK","KBBBK",".KKK."],{K:"#241a08",B:"#c8923e",W:"#fff4d9"}); // 侧舷炮弹
|
||||||
SPR.flare=bake(["..KK..",".KWWK.","KWWWWK","KWWWWK",".KWWK.","..KK.."],{K:"#33240a",W:"#ffd23d"}); // 热诱弹
|
SPR.flare=bake(["..KK..",".KWWK.","KWWWWK","KWWWWK",".KWWK.","..KK.."],{K:"#33240a",W:"#ffd23d"}); // 热诱弹
|
||||||
SPR.sticky=bake([".KKKK.","KGKKGK","KGRRGK","KGRRGK","KGKKGK",".KKKK."],{K:"#142a0d",G:"#5cff9d",R:"#ff5566"}); // 粘着炸弹
|
SPR.sticky=bake([".KKKK.","KGKKGK","KGRRGK","KGRRGK","KGKKGK",".KKKK."],{K:"#142a0d",G:"#5cff9d",R:"#ff5566"}); // 粘着炸弹
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
/* ================= 界面流转 ================= */
|
/* ================= 界面流转 ================= */
|
||||||
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"];
|
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);}
|
function show(id){for(const o of OVLS)$(o).classList.toggle("hide",o!==id);}
|
||||||
const SETUP={mode:"stage",ship:"falcon",diff:0};
|
const SETUP={mode:"endless",ship:"falcon",diff:0}; // 出击默认无尽模式
|
||||||
function bestLine(){
|
function bestLine(){
|
||||||
const b=SAVE.best;
|
const b=SAVE.best;
|
||||||
$("best-line").textContent=`无尽纪录 ${Math.max(b.e0,b.e1)} · 闯关进度 ${b.stage}/6`;
|
$("best-line").textContent=`无尽纪录 ${Math.max(b.e0,b.e1)} · 闯关进度 ${b.stage}/6`;
|
||||||
}
|
}
|
||||||
$("b-go").onclick=()=>{AU.init();buildSetup();show("o-setup");};
|
$("b-go").onclick=()=>{AU.init();show("o-setup");};
|
||||||
$("b-back1").onclick=()=>show("o-title");
|
$("b-back1").onclick=()=>show("o-title");
|
||||||
$("b-back2").onclick=()=>show("o-title");
|
$("b-back2").onclick=()=>show("o-title");
|
||||||
$("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");};
|
$("b-hangar").onclick=()=>{AU.init();buildShop();show("o-hangar");};
|
||||||
@ -16,6 +16,17 @@ $("b-rush").onclick=()=>{AU.init();buildRush();show("o-rush");};
|
|||||||
$("b-back6").onclick=()=>{UI="title";show("o-title");};
|
$("b-back6").onclick=()=>{UI="title";show("o-title");};
|
||||||
$("b-back4").onclick=()=>{UI="title";bestLine();show("o-title");};
|
$("b-back4").onclick=()=>{UI="title";bestLine();show("o-title");};
|
||||||
$("b-back5").onclick=exitDemoSim;
|
$("b-back5").onclick=exitDemoSim;
|
||||||
|
/* 图鉴测试模式(隐藏入口):连点图鉴标题“军”字10次开关。
|
||||||
|
纯内存开关——不动 SAVE.codex(persist/结算上报都读它,动了会污染后端),刷新页面即失效 */
|
||||||
|
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();
|
$("b-start").onclick=()=>startGame();
|
||||||
if("ontouchstart" in window){const pb=$("tb-pause");pb.style.display="block";pb.onclick=()=>togglePause();}
|
if("ontouchstart" in window){const pb=$("tb-pause");pb.style.display="block";pb.onclick=()=>togglePause();}
|
||||||
$("b-resume").onclick=()=>togglePause();
|
$("b-resume").onclick=()=>togglePause();
|
||||||
@ -40,7 +51,8 @@ $("diff-pick").addEventListener("click",e=>{
|
|||||||
SETUP.diff=+b.dataset.d;
|
SETUP.diff=+b.dataset.d;
|
||||||
for(const el of $("diff-pick").children)el.classList.toggle("primary",el===b);
|
for(const el of $("diff-pick").children)el.classList.toggle("primary",el===b);
|
||||||
});
|
});
|
||||||
function buildSetup(){
|
/* 机体选择/购买(机库内):购买后整店刷新(星币/按钮态变化),选择只刷新机体卡 */
|
||||||
|
function buildShips(){
|
||||||
const box=$("ship-pick");box.innerHTML="";
|
const box=$("ship-pick");box.innerHTML="";
|
||||||
for(const k in SHIPS){
|
for(const k in SHIPS){
|
||||||
const s=SHIPS[k],owned=SAVE.ships.includes(k);
|
const s=SHIPS[k],owned=SAVE.ships.includes(k);
|
||||||
@ -53,11 +65,11 @@ function buildSetup(){
|
|||||||
d.insertAdjacentHTML("beforeend",`<div class="nm">${s.name}</div><div class="ds">${s.trait}</div>`+(owned?"":`<div class="cost">✦ ${s.cost} 解锁</div>`));
|
d.insertAdjacentHTML("beforeend",`<div class="nm">${s.name}</div><div class="ds">${s.trait}</div>`+(owned?"":`<div class="cost">✦ ${s.cost} 解锁</div>`));
|
||||||
d.onclick=()=>{
|
d.onclick=()=>{
|
||||||
if(!SAVE.ships.includes(k)){
|
if(!SAVE.ships.includes(k)){
|
||||||
if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildSetup();}
|
if(SAVE.coins>=s.cost){SAVE.coins-=s.cost;SAVE.ships.push(k);persist();SFX.level();buildShop();}
|
||||||
else SFX.hurt();
|
else SFX.hurt();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SETUP.ship=k;buildSetup();
|
SETUP.ship=k;persist();buildShips(); // persist 触发云端同步选中机体
|
||||||
};
|
};
|
||||||
box.appendChild(d);
|
box.appendChild(d);
|
||||||
}
|
}
|
||||||
@ -74,6 +86,7 @@ const META=[
|
|||||||
];
|
];
|
||||||
function buildShop(){
|
function buildShop(){
|
||||||
$("wallet").textContent=SAVE.coins;
|
$("wallet").textContent=SAVE.coins;
|
||||||
|
buildShips();
|
||||||
const box=$("shop");box.innerHTML="";
|
const box=$("shop");box.innerHTML="";
|
||||||
for(const m of META){
|
for(const m of META){
|
||||||
const lv=SAVE.m[m.key]||0,maxed=lv>=m.max,cost=maxed?0:m.costs[lv];
|
const lv=SAVE.m[m.key]||0,maxed=lv>=m.max,cost=maxed?0:m.costs[lv];
|
||||||
@ -142,12 +155,14 @@ function buildCodex(){
|
|||||||
const box=$("codex");box.innerHTML="";
|
const box=$("codex");box.innerHTML="";
|
||||||
const total=MAINS.length+SUB_KEYS.length+DRONE_KEYS.length+PAS_KEYS.length;
|
const total=MAINS.length+SUB_KEYS.length+DRONE_KEYS.length+PAS_KEYS.length;
|
||||||
$("codex-t").textContent=total;
|
$("codex-t").textContent=total;
|
||||||
$("codex-n").textContent=codexCount();
|
$("codex-n").textContent=codexCount(); // 计数永远显示真实收集数,测试模式不影响
|
||||||
|
$("codex-test").style.display=codexTestAll?"":"none";
|
||||||
|
$("cx-secret").style.color=codexTestAll?"var(--gold)":""; // 开启后“军”字变金
|
||||||
const mk=(title,keys,dict,kind)=>{
|
const mk=(title,keys,dict,kind)=>{
|
||||||
const h=document.createElement("div");h.className="cx-sec";h.textContent=title;box.appendChild(h);
|
const h=document.createElement("div");h.className="cx-sec";h.textContent=title;box.appendChild(h);
|
||||||
const grid=document.createElement("div");grid.className="cx-grid";
|
const grid=document.createElement("div");grid.className="cx-grid";
|
||||||
for(const k of keys){
|
for(const k of keys){
|
||||||
const def=dict[k],owned=!!SAVE.codex[k];
|
const def=dict[k],owned=!!SAVE.codex[k]||codexTestAll; // 测试模式:全部可见可演示
|
||||||
const d=document.createElement("div");
|
const d=document.createElement("div");
|
||||||
d.className="cx-item"+(owned?"":" lock"); // 未收集:剪影+???,不可演示
|
d.className="cx-item"+(owned?"":" lock"); // 未收集:剪影+???,不可演示
|
||||||
const img=def.icon?ICONS[def.icon]:SPR[def.spr];
|
const img=def.icon?ICONS[def.icon]:SPR[def.spr];
|
||||||
@ -338,7 +353,7 @@ function startDemoSim(kind,key){
|
|||||||
if(key==="shield")G.shield=shieldCap();}
|
if(key==="shield")G.shield=shieldCap();}
|
||||||
$("demo-name").textContent=def.name;
|
$("demo-name").textContent=def.name;
|
||||||
const ds=typeof def.ds==="function"?def.ds(lv):def.ds;
|
const ds=typeof def.ds==="function"?def.ds(lv):def.ds;
|
||||||
$("demo-ds").textContent=(kind==="m"?"主武器 · ":kind==="s"?"副武器 · ":kind==="d"?"僚机 · ":"被动 · ")+ds;
|
$("demo-ds").textContent=ds; // 只显示介绍本身,单行不折行(nowrap 在 index.html 上)
|
||||||
UI="demoSim";show("o-demo");acc=0;
|
UI="demoSim";show("o-demo");acc=0;
|
||||||
banner(def.name,"#41e6ff",90);
|
banner(def.name,"#41e6ff",90);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const { vUsername, vEmail, vPassword, vInt, rateLimit, clientIp } = require("./u
|
|||||||
const CODEX_KEYS = new Set([
|
const CODEX_KEYS = new Set([
|
||||||
"vulcan", "shotgun", "needle", "flame", "shotgun2", "tesla", "railgun", "deathray",
|
"vulcan", "shotgun", "needle", "flame", "shotgun2", "tesla", "railgun", "deathray",
|
||||||
"crescent", "fission", "frost", "swarm", "bubble", "starfall", "boomerang", "singularity",
|
"crescent", "fission", "frost", "swarm", "bubble", "starfall", "boomerang", "singularity",
|
||||||
"wavecannon", "holy", "thundercall", "arcshu", "volt", "dawn", "proto", "nebula",
|
"wavecannon", "holy", "thundercall", "arcshu", "volt", "dawn", "proto", "nebula", "loiter",
|
||||||
"broad", "sweep", "leech", "orbit", "sats", "pd", "slowfield", "flares", "seeker",
|
"broad", "sweep", "leech", "orbit", "sats", "pd", "slowfield", "flares", "seeker",
|
||||||
"beamdrone", "frostnova", "thorns", "sticky", "magnetar", "executor", "airstrike", "repair",
|
"beamdrone", "frostnova", "thorns", "sticky", "magnetar", "executor", "airstrike", "repair",
|
||||||
"gunner", "gunner2", "sniperd", "beamer", "bomber", "frostd", "teslaD", "rammer",
|
"gunner", "gunner2", "sniperd", "beamer", "bomber", "frostd", "teslaD", "rammer",
|
||||||
@ -18,6 +18,10 @@ const CODEX_KEYS = new Set([
|
|||||||
]);
|
]);
|
||||||
const CODEX_TOTAL = CODEX_KEYS.size;
|
const CODEX_TOTAL = CODEX_KEYS.size;
|
||||||
const RUN_MODES = new Set(["stage", "endless", "rush"]);
|
const RUN_MODES = new Set(["stage", "endless", "rush"]);
|
||||||
|
/* 存档白名单(与 js/data.js SHIPS、js/ui.js META 保持同步) */
|
||||||
|
const SHIP_KEYS = new Set(["falcon", "ghost", "bastion", "valkyrie"]);
|
||||||
|
const META_MAX = { weapon: 2, armor: 2, hpup: 2, luck: 2, wallet: 3, mag2: 2, odstart: 1 };
|
||||||
|
const DEFAULT_STATE = { coins: 0, ships: ["falcon"], ship: "falcon", m: {}, best: { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 }, rush: {} };
|
||||||
|
|
||||||
const qInsUser = db.prepare("INSERT INTO users(username,email,pass_hash,created_at) VALUES(?,?,?,?)");
|
const qInsUser = db.prepare("INSERT INTO users(username,email,pass_hash,created_at) VALUES(?,?,?,?)");
|
||||||
const qInsStats = db.prepare("INSERT OR IGNORE INTO user_stats(user_id) VALUES(?)");
|
const qInsStats = db.prepare("INSERT OR IGNORE INTO user_stats(user_id) VALUES(?)");
|
||||||
@ -33,6 +37,9 @@ const qUpdStats = db.prepare(`INSERT INTO user_stats(user_id,boss_kills,endless_
|
|||||||
endless_high=MAX(endless_high,excluded.endless_high),
|
endless_high=MAX(endless_high,excluded.endless_high),
|
||||||
runs=runs+1,
|
runs=runs+1,
|
||||||
updated_at=excluded.updated_at`);
|
updated_at=excluded.updated_at`);
|
||||||
|
const qGetState = db.prepare("SELECT state FROM player_state WHERE user_id=?");
|
||||||
|
const qPutState = db.prepare(`INSERT INTO player_state(user_id,state,updated_at) VALUES(?,?,?)
|
||||||
|
ON CONFLICT(user_id) DO UPDATE SET state=excluded.state, updated_at=excluded.updated_at`);
|
||||||
|
|
||||||
function statsOf(userId) {
|
function statsOf(userId) {
|
||||||
const s = qStats.get(userId) || { boss_kills: 0, endless_high: 0, runs: 0 };
|
const s = qStats.get(userId) || { boss_kills: 0, endless_high: 0, runs: 0 };
|
||||||
@ -41,6 +48,31 @@ function statsOf(userId) {
|
|||||||
function codexOf(userId) {
|
function codexOf(userId) {
|
||||||
return qCodex.all(userId).map((r) => r.entry_key);
|
return qCodex.all(userId).map((r) => r.entry_key);
|
||||||
}
|
}
|
||||||
|
function stateOf(userId) {
|
||||||
|
const r = qGetState.get(userId);
|
||||||
|
if (!r) return null; // null=从未同步(老账号首次登陆:客户端用本地存档初始化上传)
|
||||||
|
try { return JSON.parse(r.state); } catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 存档白名单校验:只留合法字段并 clamp,任何字段非法则整体拒收 */
|
||||||
|
function vState(s) {
|
||||||
|
if (!s || typeof s !== "object" || Array.isArray(s)) return null;
|
||||||
|
if (!vInt(s.coins, 0, 1e7)) return null;
|
||||||
|
if (!Array.isArray(s.ships)) return null;
|
||||||
|
const ships = [...new Set(s.ships.filter((k) => SHIP_KEYS.has(k)))].slice(0, 8);
|
||||||
|
if (!ships.includes("falcon")) ships.unshift("falcon");
|
||||||
|
const ship = SHIP_KEYS.has(s.ship) ? s.ship : "falcon";
|
||||||
|
const m = {};
|
||||||
|
if (s.m && typeof s.m === "object") for (const k of Object.keys(META_MAX))
|
||||||
|
if (vInt(s.m[k], 0, META_MAX[k])) m[k] = s.m[k];
|
||||||
|
const best = { s0: 0, s1: 0, e0: 0, e1: 0, stage: 0 };
|
||||||
|
if (s.best && typeof s.best === "object") for (const k of Object.keys(best))
|
||||||
|
if (vInt(s.best[k], 0, 1e8)) best[k] = s.best[k];
|
||||||
|
const rush = {};
|
||||||
|
if (s.rush && typeof s.rush === "object") for (const k of Object.keys(s.rush).slice(0, 32))
|
||||||
|
if (/^\d{1,2}$/.test(k) && vInt(s.rush[k], 0, 8640000)) rush[k] = s.rush[k];
|
||||||
|
return { coins: s.coins, ships, ship, m, best, rush };
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- POST /api/register ---------- */
|
/* ---------- POST /api/register ---------- */
|
||||||
function register(req, res, ctx, body) {
|
function register(req, res, ctx, body) {
|
||||||
@ -56,7 +88,7 @@ function register(req, res, ctx, body) {
|
|||||||
qInsStats.run(uid);
|
qInsStats.run(uid);
|
||||||
const token = auth.createSession(db, uid);
|
const token = auth.createSession(db, uid);
|
||||||
ctx.setCookie = auth.sessionCookie(token, ctx.secure);
|
ctx.setCookie = auth.sessionCookie(token, ctx.secure);
|
||||||
return ctx.ok({ user: { username, email: email.trim().toLowerCase() }, stats: statsOf(uid), codex: [], codexTotal: CODEX_TOTAL });
|
return ctx.ok({ user: { username, email: email.trim().toLowerCase() }, stats: statsOf(uid), codex: [], codexTotal: CODEX_TOTAL, state: DEFAULT_STATE });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- POST /api/login ---------- */
|
/* ---------- POST /api/login ---------- */
|
||||||
@ -72,7 +104,7 @@ function login(req, res, ctx, body) {
|
|||||||
qInsStats.run(u.id);
|
qInsStats.run(u.id);
|
||||||
const token = auth.createSession(db, u.id);
|
const token = auth.createSession(db, u.id);
|
||||||
ctx.setCookie = auth.sessionCookie(token, ctx.secure);
|
ctx.setCookie = auth.sessionCookie(token, ctx.secure);
|
||||||
return ctx.ok({ user: { username: u.username, email: u.email }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL });
|
return ctx.ok({ user: { username: u.username, email: u.email }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL, state: stateOf(u.id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- POST /api/logout ---------- */
|
/* ---------- POST /api/logout ---------- */
|
||||||
@ -86,7 +118,7 @@ function logout(req, res, ctx) {
|
|||||||
function me(req, res, ctx) {
|
function me(req, res, ctx) {
|
||||||
const u = ctx.user;
|
const u = ctx.user;
|
||||||
if (!u) return ctx.ok({ user: null });
|
if (!u) return ctx.ok({ user: null });
|
||||||
return ctx.ok({ user: { username: u.username, email: u.email, createdAt: u.created_at }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL });
|
return ctx.ok({ user: { username: u.username, email: u.email, createdAt: u.created_at }, stats: statsOf(u.id), codex: codexOf(u.id), codexTotal: CODEX_TOTAL, state: stateOf(u.id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- GET /api/leaderboard?sort=endless|boss|codex ---------- */
|
/* ---------- GET /api/leaderboard?sort=endless|boss|codex ---------- */
|
||||||
@ -131,6 +163,16 @@ function run(req, res, ctx, body) {
|
|||||||
return ctx.ok({ stats: statsOf(uid), codex: codexOf(uid), codexTotal: CODEX_TOTAL });
|
return ctx.ok({ stats: statsOf(uid), codex: codexOf(uid), codexTotal: CODEX_TOTAL });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- POST /api/state ----------
|
||||||
|
存档云同步:星币/机体/选中机体/永久强化/本地纪录(防抖由客户端负责) */
|
||||||
|
function saveState(req, res, ctx, body) {
|
||||||
|
if (!rateLimit("state:" + ctx.user.id, 60, 5 * 60000)) return ctx.fail(429, "同步太频繁");
|
||||||
|
const st = vState(body && body.state);
|
||||||
|
if (!st) return ctx.fail(400, "非法存档数据");
|
||||||
|
qPutState.run(ctx.user.id, JSON.stringify(st), Date.now());
|
||||||
|
return ctx.ok({ saved: true });
|
||||||
|
}
|
||||||
|
|
||||||
const ROUTES = {
|
const ROUTES = {
|
||||||
"POST /api/register": { fn: register, auth: false },
|
"POST /api/register": { fn: register, auth: false },
|
||||||
"POST /api/login": { fn: login, auth: false },
|
"POST /api/login": { fn: login, auth: false },
|
||||||
@ -138,6 +180,7 @@ const ROUTES = {
|
|||||||
"GET /api/me": { fn: me, auth: false },
|
"GET /api/me": { fn: me, auth: false },
|
||||||
"GET /api/leaderboard": { fn: leaderboard, auth: true },
|
"GET /api/leaderboard": { fn: leaderboard, auth: true },
|
||||||
"POST /api/run": { fn: run, auth: true },
|
"POST /api/run": { fn: run, auth: true },
|
||||||
|
"POST /api/state": { fn: saveState, auth: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { ROUTES, CODEX_TOTAL };
|
module.exports = { ROUTES, CODEX_TOTAL };
|
||||||
|
|||||||
@ -55,6 +55,11 @@ CREATE TABLE IF NOT EXISTS runs(
|
|||||||
created_at INTEGER NOT NULL
|
created_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_runs_user ON runs(user_id);
|
CREATE INDEX IF NOT EXISTS idx_runs_user ON runs(user_id);
|
||||||
|
CREATE TABLE IF NOT EXISTS player_state(
|
||||||
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
/* 定期清理过期 session */
|
/* 定期清理过期 session */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user