①图鉴隐藏测试模式:连点标题'军'字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)
109 lines
4.1 KiB
JavaScript
109 lines
4.1 KiB
JavaScript
"use strict";
|
||
/* ================= 网络层:服务端 API 客户端 ================= */
|
||
const NET = {
|
||
user: null, // {username,email}
|
||
stats: null, // {bossKills,endlessHigh,runs}
|
||
codexTotal: 63,
|
||
online: false,
|
||
|
||
async api(path, method, body) {
|
||
let res;
|
||
try {
|
||
res = await fetch(path, {
|
||
method: method || "GET",
|
||
credentials: "same-origin",
|
||
headers: body ? { "Content-Type": "application/json" } : undefined,
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
} catch (e) {
|
||
return { ok: false, status: 0, data: { error: "无法连接服务器" } };
|
||
}
|
||
let data = {};
|
||
try { data = await res.json(); } catch (e) { /* 空响应 */ }
|
||
return { ok: res.ok, status: res.status, data };
|
||
},
|
||
|
||
/* 启动:尝试恢复会话。true=已登录(未登录时服务端返回 200 {user:null}) */
|
||
async boot() {
|
||
const r = await this.api("/api/me", "GET");
|
||
if (r.ok && r.data.user) { this._apply(r.data); return true; }
|
||
return false;
|
||
},
|
||
|
||
async login(id, password) {
|
||
const r = await this.api("/api/login", "POST", { id, password });
|
||
if (r.ok) { this._apply(r.data); return null; }
|
||
return r.data.error || "登陆失败";
|
||
},
|
||
|
||
async register(username, email, password) {
|
||
const r = await this.api("/api/register", "POST", { username, email, password });
|
||
if (r.ok) { this._apply(r.data); return null; }
|
||
return r.data.error || "注册失败";
|
||
},
|
||
|
||
async logout() {
|
||
await this.api("/api/logout", "POST");
|
||
this.user = null; this.stats = null; this.online = false;
|
||
/* 防同机下一账号继承:图鉴/星币/机体/强化/纪录全部清空(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();
|
||
},
|
||
|
||
/* 应用服务端返回的用户数据:图鉴以服务端为准【替换】本地
|
||
(不能并集合并——localStorage 是浏览器级共享,同机多账号会互相污染,已踩坑) */
|
||
_apply(d) {
|
||
this.user = d.user;
|
||
this.stats = d.stats;
|
||
this.codexTotal = d.codexTotal || 63;
|
||
this.online = true;
|
||
SAVE.codex = {};
|
||
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();
|
||
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):成功后回写最新统计 */
|
||
submitRun(payload) {
|
||
if (!this.user) return;
|
||
this.api("/api/run", "POST", payload).then((r) => {
|
||
if (r.ok) {
|
||
this.stats = r.data.stats;
|
||
if (Array.isArray(r.data.codex)) {
|
||
for (const k of r.data.codex) SAVE.codex[k] = true;
|
||
persist();
|
||
}
|
||
}
|
||
}).catch(() => { /* 网络失败静默:本地存档仍在 */ });
|
||
},
|
||
};
|