feat: implement scoped stat system with player/global support

Introduces a scoping mechanism for statistics, allowing properties
to be defined as either "player" (prefixed with player name) or
"global".

- Adds `scope` property to StatDef
- Implements key resolution to handle bare keys vs full keys
- Updates `canModifyStat` to enforce player-specific permissions
- Enhances `makeStatLookup` to resolve scoped references in formulas
- Adds documentation for the new stat system
This commit is contained in:
2026-07-09 09:41:16 +08:00
parent 44e4e15f3f
commit 138c089514
5 changed files with 400 additions and 77 deletions
+32 -5
View File
@@ -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) {