"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")); }, };