/** * StatsView — table view of all stat key-value pairs. * * Groups stats by scope (global, then per-player). Shows computed values * (base + modifiers) and inline roll buttons for stats with roll/table/formula. */ import { Component, For, createMemo, Show } from "solid-js"; import { useJournalStream } from "../stores/journalStream"; import { useJournalCompletions } from "./completions"; import { fullKey, modifierLabel } from "./stat-helpers"; import type { StatDef } from "./completions"; export const StatsView: Component = () => { const stream = useJournalStream(); const comp = useJournalCompletions(); const statDefs = createMemo(() => comp.data.stats); const values = createMemo(() => stream.stats); /** Build a lookup: fullKey -> StatDef */ const defMap = createMemo(() => { const map = new Map(); for (const def of statDefs()) { const fk = fullKey(def, stream.myName); map.set(fk, def); } return map; }); /** Compute the effective value of a stat (base + modifiers) */ function computedValue(key: string): string { const def = defMap().get(key); if (!def) return values()[key] ?? ""; const base = values()[key] ?? def.default ?? ""; const baseNum = parseFloat(base); if (def.type === "number" || def.type === "derived") { // Sum modifiers that target this key (fullKey matching) const modKeys: string[] = []; for (const [fk, d] of defMap()) { if ( d.type === "modifier" && d.target && fullKeyTarget(d.target, d, def) ) { modKeys.push(fk); } } let total = isNaN(baseNum) ? 0 : baseNum; for (const mk of modKeys) { const mv = parseFloat(values()[mk] ?? defMap().get(mk)?.default ?? "0"); if (!isNaN(mv)) total += mv; } return String(total); } return base || "—"; } /** Check if a modifier's target matches a given def (scoped correctly). */ function fullKeyTarget( target: string, modDef: StatDef, targetDef: StatDef, ): boolean { if (modDef.scope === targetDef.scope) { return ( fullKey({ ...modDef, key: target }, stream.myName) === fullKey(targetDef, stream.myName) ); } return false; } /** Group stats by scope */ const groups = createMemo(() => { const result: { scope: string; label: string; entries: { fullKey: string; def: StatDef }[]; }[] = []; const globalEntries: { fullKey: string; def: StatDef }[] = []; const playerEntries: { fullKey: string; def: StatDef }[] = []; for (const def of statDefs()) { const fk = fullKey(def, stream.myName); if (def.scope === "global") { globalEntries.push({ fullKey: fk, def }); } else { playerEntries.push({ fullKey: fk, def }); } } if (globalEntries.length > 0) { result.push({ scope: "global", label: "全局", entries: globalEntries }); } if (playerEntries.length > 0) { result.push({ scope: "player", label: `玩家 (${stream.myName})`, entries: playerEntries, }); } return result; }); return (
0} fallback={

暂无属性定义。在文档中使用 ```yaml role=stat 代码块定义属性。

} > {(group) => (
{group.label}
{({ fullKey: fk, def }) => { const val = () => values()[fk]; const comp = () => computedValue(fk); const hasModifiers = () => { for (const [mfk, md] of defMap()) { if ( md.type === "modifier" && md.target && fullKeyTarget(md.target, md, def) ) { return true; } } return false; }; const canRoll = () => def.roll || def.formula || def.type === "enum" || def.type === "template"; return (
{modifierLabel(def, statDefs())} {def.key} →{def.target}
{def.default ?? "—"} } > {val()} = {comp()}
); }}
)}
); };