starraid/star-raid/js/net.js
JOJO 9d048a2aa9 无尽模式断点续玩:进度云端存档,中途退出可续玩,新开需确认结算旧档
- 服务端:run_saves 表(每账号1栏位)+ GET/POST/DELETE /api/run_save,vRunSave 白名单校验
- 波次检查点:startWave 拍快照上传;中途放弃任务不结算(星币/分数留快照);死亡删档
- 出击页卡片显示未完成对局并一键续玩(该波从头重打,bossN 不偏移)
- 新开无尽(不分难度)弹窗确认→旧档按当前进度结算(星币入账+上报排行榜)再开新局
- NET._rsQ 串行队列防删建竞态;顺带修复 SHIP_KEYS 漏 asura 致修罗云同步丢失
2026-08-06 12:50:37 +08:00

140 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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";
/* ================= 网络层:服务端 API 客户端 ================= */
const NET = {
user: null, // {username,email}
stats: null, // {bossKills,endlessHigh,runs}
codexTotal: 63,
online: false,
runSave: undefined, // 无尽断点存档缓存undefined=未拉取null=无档object=有档
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;
this.runSave = undefined; // 防同机下一账号继承云端断点存档
/* 防同机下一账号继承:图鉴/星币/机体/强化/纪录全部清空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(() => { /* 网络失败静默:本地存档仍在 */ });
},
/* ---- 无尽模式断点存档(波次检查点) ----
所有 run_save 操作串行入队:防 删除→新建 乱序覆盖(死亡后立刻开新局会竞态) */
_rsQ: Promise.resolve(),
_rsEnqueue(op) {
const p = this._rsQ.then(op).catch(() => ({ ok: false, data: {} }));
this._rsQ = p;
return p;
},
/* 拉取云端断点存档(同时刷新缓存) */
fetchRunSave() {
if (!this.user) { this.runSave = null; return Promise.resolve(null); }
return this._rsEnqueue(() => this.api("/api/run_save", "GET")).then((r) => {
this.runSave = r.ok && r.data ? (r.data.save || null) : null;
return this.runSave;
});
},
/* 上传断点快照fire-and-forget仅限无尽模式波次开始 */
saveRunSnapshot(snap) {
if (!this.user) return;
this.runSave = snap;
this._rsEnqueue(() => this.api("/api/run_save", "POST", { save: snap }));
},
/* 删除云端断点存档(结算/死亡后;返回 Promise 供需要时等待) */
deleteRunSave() {
if (!this.user) return Promise.resolve();
this.runSave = null;
return this._rsEnqueue(() => this.api("/api/run_save", "DELETE"));
},
};