实现账号绑定、持久登录与跨设备对局恢复
CI / test (ubuntu-latest, node 22) (push) Failing after 33m10s
CI / test (ubuntu-latest, node 24) (push) Failing after 34m41s
CI / tooling (lint, imports, typecheck) (push) Failing after 36m11s
CI / test (windows-latest, node 22) (push) Canceled after 0s
CI / test (windows-latest, node 24) (push) Canceled after 0s

This commit is contained in:
herryyun committed 2026-10-14 23:22:50 +08:00
1 parent 488db49f11
commit 7c25c89992
62 files changed
+6855 -1005

No files matched your search

+12 -9
View File
@@ -8,13 +8,13 @@
// otherwise → Lobby
// Deep link `?room=CODE`: remembered at boot, auto-joined once the player has entered and the
// session is online (after a short grace period in case the server restores a room on resume).
// Reloading a tab that already passed the title re-enters automatically (sessionStorage flag) and
// Reloading a tab that already passed the title re-enters after checking its current role binding and
// resumes the server session with the saved token; stale room/match state is dropped if the
// server does not re-push it within RESTORE_GRACE_MS after `welcome`. Boot waits for
// `identity.init()` (which token this tab may use without stealing another live tab's session)
// before the first connect; a page restored from the back/forward cache reloads.
// A `welcome` with a NEW playerId (the server restarted / the session expired) while a room or match was on screen:
// toast 「服务器会话已重置,上一局模拟已结束」 (store.js sessionResetNotice) and return to the lobby with nothing stale.
// drop stale match state (store.js sessionResetNotice); an unauthenticated transport returns to the title for checked entry.
//
// Shared modules are imported relatively ('../../shared/…' resolves to /shared/… in the browser).
// Multi-device support (ui/device.js + css/devices.css): feature classes on <html>, no page zoom, safe areas, rotation
@@ -41,6 +41,7 @@ import { store, useStore, emptyMatch, selectRoute, sessionResetNotice, isSpectat
import { data } from './data.js';
import { GAME_FILES } from './ui/gameComponents.js';
import { TitleScreen, sanitizeName } from './screens/title.js';
import { acceptAccountWelcome, restoreBoundEntry } from './skland/gate.js';
import { LobbyScreen, rememberRoom, parseRoomParam } from './screens/lobby.js';
import { RoomScreen } from './screens/room.js';
import { GameScreen } from './screens/game.js';
@@ -145,6 +146,7 @@ function backToLobby() {
}
function onWelcome(msg) {
const accountAccepted = acceptAccountWelcome(msg);
identity.saveToken(msg.token);
identity.rememberMatch(null);
const prev = store.get();
@@ -156,8 +158,8 @@ function onWelcome(msg) {
if (prevId != null && prevId !== msg.playerId) {
// A brand-new server session (the server restarted — crashed / killed, so no room.closed arrived — or this session
// expired on it): whatever we showed before is gone — back to the lobby cleanly and say why.
const notice = sessionResetNotice(prev, msg.playerId);
// expired on it): clear the old snapshot. A login-only transport stays at the title until checked account entry.
const notice = accountAccepted ? sessionResetNotice(prev, msg.playerId) : null;
backToLobby();
if (notice) toast(t(notice), 'warn', { ttl: 7000 });
} else if (prev.room || prev.match.public) {
@@ -303,7 +305,7 @@ function App() {
return html`<div class="app-root">
<div class="app-bg" aria-hidden="true"></div>
${error ? html`<${ScreenCrashed} error=${error} reset=${resetError} />` : html`<${Screen} key=${route} />`}
<${ConnectionBanner} />
<${ConnectionBanner} continueSession=${() => restoreBoundEntry(sanitizeName)} />
<${ToastHost} />
<${UiHosts} />
<${GuideHost} />
@@ -360,7 +362,7 @@ async function boot() {
const entered = identity.wasEntered() && !!savedName;
store.set((s) => ({
me: { ...s.me, name: savedName },
session: { entered },
session: { entered: false },
ui: { ...s.ui, pendingJoin },
}));
@@ -378,9 +380,10 @@ async function boot() {
// Optional local-client art manifest (emotes, tutorial pages, official UI sprites; DESIGN §13).
data.load('local').catch(() => {});
const connectWhenReady = identityReady.then(() => {
if (entered) net.setName(savedName);
else net.connect();
const connectWhenReady = identityReady.then((token) => {
if (entered && token) return restoreBoundEntry(sanitizeName, { automatic: true });
identity.setEntered(false);
net.connect();
});
// the language (and its UI translations) before the first render: no Chinese flash for an English player
const langReady = initLang().catch((err) => console.warn('[app] language setup failed', err));