// The local stats viewer (per-browser match record, viewer + data management. Opened imperatively from the
// title screen / lobby (`openStats()`); is mounted once by main.js like .
//
// The page aggregates over the records of ui/stats.js (one per finished match the local player took part in):
// 总览 – games, wins, win rate, hidden-core reaches / clears, per-difficulty rows
// 策略 – per band: games used / passed (self alive on a winning team) / pass rate (the user's ask)
// 称号 – each of config.titles' six 评语 with its count
// 战斗累计– the self player's summed combat bookkeeping
// 最近对局– the newest RECENT_SHOW records as rows; a row click re-views that match as its settlement
// screen (the record is inverted into an m.result payload for )
// Everything is 本机数据 (localStorage, `sp.pref.stats`): the note under the title says so, and 导出/导入
// (JSON file, merge-by-record-id on the way in) moves it between devices; 清空 resets after a confirm press.
import { useEffect, useRef, useState } from '../../vendor/hooks.module.js';
import { DIFFICULTY_NAMES, PHASE } from '../../../shared/constants.js';
import { html, Icon, MicroLabel, Spinner, Button } from '../ui/components.js';
import { useGameData, BandIcon, Img } from '../ui/gameComponents.js';
import { data, useData } from '../data.js';
import { fmtNum } from '../ui/gameLogic.js';
import { titleIconUrl } from '../ui/assetUrls.js';
import { createStore, useStore, store, emptyMatch } from '../store.js';
import { ResultScreen } from './result.js';
import { toast, toastError } from '../ui/toasts.js';
import {
loadStats, saveStats, emptyStats, importStats, exportStats, aggregateStats, recordToResult, RECENT_SHOW,
} from '../ui/stats.js';
import { t, N_ } from '../../../shared/i18n.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
const statsStore = createStore({ open: false });
/** Open the stats viewer (title screen / lobby entry points). */
export function openStats() {
data.load('config');
data.load('bands');
data.load('assets');
statsStore.set({ open: true });
}
export const closeStats = () => {
// a replayed settlement is synthetic store state — take it away with the viewer
if (store.get().match.result) store.set({ match: emptyMatch() });
statsStore.set({ open: false });
};
/**
* 最近对局行点击: one stored match re-viewed as its settlement screen, INSIDE the stats viewer — the
* record is inverted into an m.result payload (ui/stats.js recordToResult) into store.match.result, and
* StatsHost swaps its panel for the real while a result exists (the settlement's
* 返回大厅 empties match again → back to the list, ready to view the next row). Refused while a match
* is actually live: the overlay is global and clobbering a running match's state would be destructive.
* @param {any} rec
*/
export function openRecordResult(rec) {
const s = store.get();
if ((s.match.public && s.match.public.phase !== PHASE.LOBBY) || s.room?.inMatch) {
toast(t('对局进行中,暂不能回看历史结算'), 'warn');
return;
}
const res = recordToResult(rec);
if (!res) return;
store.set({ match: { ...emptyMatch(), public: { phase: PHASE.RESULT }, result: res } });
}
const pct = (n, d) => (d > 0 ? `${Math.round((n / d) * 100)}%` : '—');
const dateTime = (t) => {
if (!t) return '—';
const d = new Date(t);
const p = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
};
const minsOf = (ms) => (ms > 0 ? Math.round(ms / 60000) : null);
/** One labelled number card. */
function StatCard({ label, micro, value, tone }) {
return html`
`;
}
/**
* Global stats viewer host — mounted once by main.js. Waits only on the files the page reads (config for the
* title list, bands for names/icons, assets for the picture URLs), not all of useGameData's files.
*/
export function StatsHost() {
const { open } = useStore((s) => s, Object.is, statsStore);
const replaying = useStore((s) => !!s.match.result); // a replayed settlement replaces the whole panel
const ready = useData('config', 'bands', 'assets');
const gd = useGameData();
const [stats, setStats] = useState(() => emptyStats());
const boxRef = useRef(null);
const reload = () => setStats(loadStats());
useEffect(() => {
if (!open) return undefined;
setStats(loadStats());
const onKey = (e) => {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); closeStats(); }
};
window.addEventListener('keydown', onKey, true);
const t = setTimeout(() => boxRef.current?.focus?.(), 30);
return () => { window.removeEventListener('keydown', onKey, true); clearTimeout(t); };
}, [open]);
if (!open) return null;
if (replaying) {
// the record re-viewed as its settlement (openRecordResult): the real , full-bleed;
// its 返回大厅 empties match.result and lands back on the list
return html`