starraid/star-raid/js/net.js
JOJO 3ffb51098b 新增海盗、植物两大种族敌人各15种
- 海盗族15种:5杂兵+6小兵+4精英,锈红/金/骨白配色
- 植物族15种:5杂兵+6小兵+4精英,叶绿/深绿/花粉粉配色
- 各族子弹配色严格≤3色:海盗锈钉/炮弹/燃烧瓶,植物荆棘/种子/孢子
- 敌表新增 race/tier 标签,波次生成按层级池抽取
- 更新 CODEX_KEYS 与客户端图鉴总数
2026-08-06 21:20:39 +08:00

148 lines
6.0 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: 60,
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 = {};
SAVE.mute = false; SAVE.music = true; // 音频开关也防串号,恢复默认
if (typeof SETUP !== "undefined") SETUP.ship = "falcon";
persist();
if (typeof updateMuteBtn === "function") updateMuteBtn();
if (typeof AU !== "undefined") AU.applyMute();
},
/* 应用服务端返回的用户数据:图鉴以服务端为准【替换】本地
不能并集合并——localStorage 是浏览器级共享,同机多账号会互相污染,已踩坑) */
_apply(d) {
this.user = d.user;
this.stats = d.stats;
this.codexTotal = d.codexTotal || 60;
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);
SAVE.mute = d.state.mute === true; // 音频开关跟随账号(缺省不静音)
SAVE.music = d.state.music !== false; // 缺省音乐开
if (d.state.ship && SAVE.ships.includes(d.state.ship) && typeof SETUP !== "undefined") SETUP.ship = d.state.ship;
}
persist();
this._mutePush = false;
if (typeof updateMuteBtn === "function") updateMuteBtn(); // 刷新基地静音按钮图标
if (typeof AU !== "undefined") AU.applyMute();
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 || {},
mute: !!SAVE.mute, music: SAVE.music !== false,
} }).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"));
},
};