diff --git a/src/components/journal/JournalInput.tsx b/src/components/journal/JournalInput.tsx index d477244..172243b 100644 --- a/src/components/journal/JournalInput.tsx +++ b/src/components/journal/JournalInput.tsx @@ -15,7 +15,7 @@ import { actionPrefill, setActionPrefill } from "../stores/reveal"; import { useJournalCompletions, ensureCompletions } from "./completions"; import { resolveRollPayload } from "./types/roll"; import { resolveSparkPayload } from "./types/spark"; -import { resolveStatRoll, canModifyStat } from "./stat-helpers"; +import { resolveStatRoll, canModifyStat, fullKey } from "./stat-helpers"; import { parseInput } from "./command-parser"; import type { CompletionItem } from "./command-parser"; import { buildCompletions } from "./command-completions"; @@ -147,13 +147,18 @@ export const JournalInput: Component = () => { const p = payload as { action?: string; key?: string; value?: string }; if (p.action === "roll" && p.key) { - const resolved = resolveStatRoll(p.key, comp.data.stats, stream.stats); + const resolved = resolveStatRoll( + p.key, + comp.data.stats, + stream.stats, + stream.myName, + ); if (resolved.error) { setError(resolved.error); } else { const result = sendMessage("stat", { action: "set", - key: p.key, + key: resolved.fullKey, value: resolved.value, }); const r = unwrap(result); @@ -167,16 +172,38 @@ export const JournalInput: Component = () => { return; } - if (!canModifyStat(stream.myRole, stream.myName, p.key)) { + // Resolve bare key to full key for set/del + const fk = resolveKey(p.key, comp.data.stats, stream.myName); + + if (!canModifyStat(stream.myRole, stream.myName, fk, comp.data.stats)) { setError(`无权修改属性: ${p.key}`); return; } - const result = sendMessage("stat", payload); + const result = sendMessage("stat", { + action: p.action, + key: fk, + value: p.value, + }); const r = unwrap(result); finish(r.ok, r.err); } + /** Resolve a bare or full key to the actual runtime key. */ + function resolveKey( + inputKey: string, + statDefs: typeof comp.data.stats, + playerName: string, + ): string { + // Exact match + if (statDefs.some((d) => fullKey(d, playerName) === inputKey)) + return inputKey; + // Bare key → full key + const def = statDefs.find((d) => d.key === inputKey); + if (def) return fullKey(def, playerName); + return inputKey; + } + /** Clear text + error on success, or set error on failure. */ function finish(success: boolean, err?: string) { if (success) { diff --git a/src/components/journal/StatsView.tsx b/src/components/journal/StatsView.tsx index a84b72e..ccf88ad 100644 --- a/src/components/journal/StatsView.tsx +++ b/src/components/journal/StatsView.tsx @@ -1,30 +1,29 @@ /** * StatsView — table view of all stat key-value pairs * - * Groups stats by owner (global, then per-player). Shows computed values + * 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 } from "./stat-helpers"; import type { StatDef } from "./completions"; export const StatsView: Component = () => { const stream = useJournalStream(); const comp = useJournalCompletions(); - /** All stat definitions from YAML */ const statDefs = createMemo(() => comp.data.stats); - - /** Current runtime values */ const values = createMemo(() => stream.stats); - /** Build a lookup: key -> StatDef */ + /** Build a lookup: fullKey -> StatDef */ const defMap = createMemo(() => { const map = new Map(); for (const def of statDefs()) { - map.set(def.key, def); + const fk = fullKey(def, stream.myName); + map.set(fk, def); } return map; }); @@ -38,10 +37,17 @@ export const StatsView: Component = () => { const baseNum = parseFloat(base); if (def.type === "number" || def.type === "derived") { - // Sum modifiers that target this key - const modKeys = statDefs() - .filter((d) => d.type === "modifier" && d.target === key) - .map((d) => d.key); + // 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"); @@ -53,29 +59,50 @@ export const StatsView: Component = () => { return base || "—"; } - /** Group stats by owner */ + /** 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: { owner: string; label: string; keys: string[] }[] = []; - const globalKeys: string[] = []; - const playerMap = new Map(); + 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 colonIdx = def.key.indexOf(":"); - if (colonIdx === -1) { - globalKeys.push(def.key); + const fk = fullKey(def, stream.myName); + if (def.scope === "global") { + globalEntries.push({ fullKey: fk, def }); } else { - const owner = def.key.slice(0, colonIdx); - if (!playerMap.has(owner)) playerMap.set(owner, []); - playerMap.get(owner)!.push(def.key); + playerEntries.push({ fullKey: fk, def }); } } - if (globalKeys.length > 0) { - result.push({ owner: "", label: "全局", keys: globalKeys }); + if (globalEntries.length > 0) { + result.push({ scope: "global", label: "全局", entries: globalEntries }); } - for (const [owner, keys] of playerMap) { - result.push({ owner, label: owner, keys }); + if (playerEntries.length > 0) { + result.push({ + scope: "player", + label: `玩家 (${stream.myName})`, + entries: playerEntries, + }); } return result; @@ -100,42 +127,47 @@ export const StatsView: Component = () => {
- - {(key) => { - const def = () => defMap().get(key); - const val = () => values()[key]; - const comp = () => computedValue(key); - const hasModifiers = () => - statDefs().some( - (d) => d.type === "modifier" && d.target === key, - ); + + {({ 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.roll || def.formula || def.type === "enum"; return (
- {/* Label + type badge */}
- {def()?.label ?? key} + {def.label} - {key} + {def.key} - + - →{def()?.target} + →{def.target}
- {/* Value */}
- {def()?.default ?? "—"} + {def.default ?? "—"} } > @@ -144,11 +176,10 @@ export const StatsView: Component = () => { - {/* Computed value (shown when different from base) */} @@ -156,13 +187,11 @@ export const StatsView: Component = () => { - {/* Roll button */}