diff --git a/src/cli/completions/variable-system.test.ts b/src/cli/completions/variable-system.test.ts index 4ba5a4a..60e7666 100644 --- a/src/cli/completions/variable-system.test.ts +++ b/src/cli/completions/variable-system.test.ts @@ -690,16 +690,16 @@ describe("var-reactivity", () => { // --------------------------------------------------------------------------- describe("parseInput", () => { - test("parses /roll command", () => { - const result = parseInput("/roll 3d6+5"); + test("parses /roll command with spark table slug", () => { + const result = parseInput("/roll my-table"); expect(result.type).toBe("roll"); - expect(result.payload).toEqual({ notation: "3d6+5", label: "3d6+5" }); + expect(result.payload).toEqual({ arg: "my-table" }); }); - test("parses /spark command", () => { - const result = parseInput("/spark my-table"); - expect(result.type).toBe("spark"); - expect(result.payload).toEqual({ key: "my-table" }); + test("parses /roll command with dice notation", () => { + const result = parseInput("/roll 3d6+5"); + expect(result.type).toBe("roll"); + expect(result.payload).toEqual({ arg: "3d6+5" }); }); test("parses /link command", () => { @@ -743,7 +743,7 @@ describe("parseInput", () => { test("returns error for empty /roll", () => { const result = parseInput("/roll "); - expect(result.error).toBe("需要骰子表达式"); + expect(result.error).toBe("需要骰子表达式或种子表键名"); }); test("returns error for empty /set", () => { diff --git a/src/components/RevealManager.tsx b/src/components/RevealManager.tsx index 490cdd3..0f0af4c 100644 --- a/src/components/RevealManager.tsx +++ b/src/components/RevealManager.tsx @@ -126,7 +126,7 @@ export const RevealManager: Component = () => { rect: sparkTable.getBoundingClientRect(), action: () => setActionPrefill({ - command: "/spark", + command: "/roll", text: combinedSlug, }), title: "Roll spark table", diff --git a/src/components/journal-doc-data.ts b/src/components/journal-doc-data.ts index 29e0e02..6cbd567 100644 --- a/src/components/journal-doc-data.ts +++ b/src/components/journal-doc-data.ts @@ -45,7 +45,6 @@ import journalGmRaw from "../doc-entries/journal-gm.md"; import journalPlayerRaw from "../doc-entries/journal-player.md"; import journalSetRaw from "../doc-entries/journal-set.md"; import journalRollRaw from "../doc-entries/journal-roll.md"; -import journalSparkRaw from "../doc-entries/journal-spark.md"; import journalLinkRaw from "../doc-entries/journal-link.md"; const rawDocuments: string[] = [ @@ -53,7 +52,6 @@ const rawDocuments: string[] = [ journalPlayerRaw, journalSetRaw, journalRollRaw, - journalSparkRaw, journalLinkRaw, ]; diff --git a/src/components/journal/JournalInput.tsx b/src/components/journal/JournalInput.tsx index ee2fb67..25b1cef 100644 --- a/src/components/journal/JournalInput.tsx +++ b/src/components/journal/JournalInput.tsx @@ -265,7 +265,7 @@ export const JournalInput: Component = () => { onKeyDown={handleKeyDown} placeholder={ isGm() - ? "输入消息,或使用 /roll、/spark、/link、/set 命令..." + ? "输入消息,或使用 /roll、/link、/set 命令..." : "输入消息,或使用 /set 命令..." } rows={2} diff --git a/src/components/journal/command-completions.ts b/src/components/journal/command-completions.ts index 3804ae1..0c05f85 100644 --- a/src/components/journal/command-completions.ts +++ b/src/components/journal/command-completions.ts @@ -21,7 +21,6 @@ export function buildCompletions( const commands = [ { label: "/roll", kind: "command" as const, insertText: "/roll " }, - { label: "/spark", kind: "command" as const, insertText: "/spark " }, { label: "/link", kind: "command" as const, insertText: "/link " }, { label: "/set", kind: "command" as const, insertText: "/set " }, ]; @@ -40,46 +39,52 @@ export function buildCompletions( : [{ label: "未知命令", kind: "no-results", insertText: "" }]; } - // After /roll — show dice suggestions + // After /roll — show dice AND spark table suggestions if (raw.startsWith("/roll ")) { const prefix = raw.slice("/roll ".length).toLowerCase(); - const matches = data.dice + + const diceMatches = data.dice .filter( (d) => d.notation.toLowerCase().includes(prefix) || d.label.toLowerCase().includes(prefix), ) - .slice(0, 8); - if (matches.length === 0) { - return [{ label: "未找到骰子", kind: "no-results", insertText: "" }]; - } - return matches.map((d) => ({ - label: d.notation, - kind: "value" as const, - insertText: "/roll " + d.notation, - })); - } + .slice(0, 4); - // After /spark — show spark table suggestions - if (raw.startsWith("/spark ")) { - const prefix = raw.slice("/spark ".length).toLowerCase(); - const matches = data.sparkTables + const sparkMatches = data.sparkTables .filter( (s) => s.slug.toLowerCase().includes(prefix) || s.label.toLowerCase().includes(prefix), ) - .slice(0, 8); - if (matches.length === 0) { - return [{ label: "未找到种子表", kind: "no-results", insertText: "" }]; + .slice(0, 4); + + const items: CompletionItem[] = []; + + for (const d of diceMatches) { + items.push({ + label: `🎲 ${d.notation}`, + kind: "value", + insertText: "/roll " + d.notation, + }); } - return matches.map((s) => ({ - label: `${s.slug} (${s.notation})`, - kind: "value" as const, - insertText: `/spark ${s.slug}`, - })); + + for (const s of sparkMatches) { + items.push({ + label: `✨ ${s.slug} (${s.notation})`, + kind: "value", + insertText: `/roll ${s.slug}`, + }); + } + + if (items.length === 0) { + return [{ label: "输入骰子表达式", kind: "no-results", insertText: "" }]; + } + return items; } + + // After /link — show article and heading suggestions if (raw.startsWith("/link ")) { const prefix = raw.slice("/link ".length).toLowerCase(); diff --git a/src/components/journal/command-dispatcher.ts b/src/components/journal/command-dispatcher.ts index a849e8d..2740841 100644 --- a/src/components/journal/command-dispatcher.ts +++ b/src/components/journal/command-dispatcher.ts @@ -87,30 +87,57 @@ export async function dispatchCommand( // GM: all commands if (parsed.type === "roll") { - const p = resolveRollPayload( - parsed.payload as { notation: string; label?: string }, - ); + const arg = (parsed.payload as { arg: string }).arg; + + // Try spark table first: if arg matches a known spark table slug, roll it + const match = ctx.sparkTables.find((s) => s.slug === arg); + if (match) { + try { + const csvPath = match.csvPath ?? ""; + const remix = match.remix ?? false; + const p = await resolveSparkPayload({ key: arg, csvPath, remix }); + const result = sendMessage("spark", p); + return finish(unwrap(result)); + } catch (e) { + return finish({ + ok: false, + error: e instanceof Error ? e.message : "种子表掷骰失败", + }); + } + } + + // Variable expression (e.g. "2d6 + $mod" or "$hp / 2") + if (arg.includes("$")) { + try { + const ev = evaluateExpression(arg, { + lookup: (name: string) => getCombined("$" + name, ctx.variables), + }); + const payload = { + notation: arg, + label: arg, + result: { + total: ev.value, + detail: "", + plainDetail: "", + pools: [] as { rolls: number[]; subtotal: number }[], + }, + }; + const result = sendMessage("roll", payload); + return finish(unwrap(result)); + } catch (e) { + return finish({ + ok: false, + error: e instanceof Error ? e.message : "表达式求值失败", + }); + } + } + + // Otherwise, treat as a dice expression + const p = resolveRollPayload({ notation: arg, label: arg }); const result = sendMessage("roll", p); return finish(unwrap(result)); } - if (parsed.type === "spark") { - try { - const key = (parsed.payload as { key: string }).key; - const match = ctx.sparkTables.find((s) => s.slug === key); - const csvPath = match?.csvPath ?? ""; - const remix = match?.remix ?? false; - const p = await resolveSparkPayload({ key, csvPath, remix }); - const result = sendMessage("spark", p); - return finish(unwrap(result)); - } catch (e) { - return finish({ - ok: false, - error: e instanceof Error ? e.message : "种子表掷骰失败", - }); - } - } - if (parsed.type === "set" || parsed.type === "rolltag") { return finish(dispatchSet(parsed.payload as Record, ctx)); } diff --git a/src/components/journal/command-parser.ts b/src/components/journal/command-parser.ts index e6e9de5..ccb5926 100644 --- a/src/components/journal/command-parser.ts +++ b/src/components/journal/command-parser.ts @@ -12,23 +12,17 @@ export interface CompletionItem { } export interface ParsedInput { - type: "chat" | "roll" | "spark" | "link" | "set" | "rolltag"; + type: "chat" | "roll" | "link" | "set" | "rolltag"; payload: Record; error?: string; } export function parseInput(raw: string): ParsedInput { if (raw.startsWith("/roll ")) { - const notation = raw.slice("/roll ".length).trim(); - if (!notation) - return { type: "roll", payload: {}, error: "需要骰子表达式" }; - return { type: "roll", payload: { notation, label: notation } }; - } - - if (raw.startsWith("/spark ")) { - const key = raw.slice("/spark ".length).trim(); - if (!key) return { type: "spark", payload: {}, error: "需要种子表键名" }; - return { type: "spark", payload: { key } }; + const arg = raw.slice("/roll ".length).trim(); + if (!arg) + return { type: "roll", payload: {}, error: "需要骰子表达式或种子表键名" }; + return { type: "roll", payload: { arg } }; } if (raw.startsWith("/link ")) { @@ -69,7 +63,7 @@ export function parseInput(raw: string): ParsedInput { return { type: "set", payload: { key, expr } }; } - if (raw === "/roll" || raw === "/spark" || raw === "/link" || raw === "/set") { + if (raw === "/roll" || raw === "/link" || raw === "/set") { return { type: "chat", payload: {}, error: "请补全命令" }; } diff --git a/src/doc-entries/journal-roll.md b/src/doc-entries/journal-roll.md index aca6ca1..06f4e19 100644 --- a/src/doc-entries/journal-roll.md +++ b/src/doc-entries/journal-roll.md @@ -1,27 +1,32 @@ --- tag: journal-roll icon: 🎲 -title: 掷骰命令 -description: 在 Journal 中发送掷骰结果,支持标准骰子表达式。 -syntax: '/roll 骰子表达式' +title: 掷骰与种子表命令 +description: 在 Journal 中掷骰或随机生成种子表内容。优先匹配种子表,否则作为骰子表达式处理。 +syntax: '/roll 骰子表达式 | 种子表键名' props: - - name: 骰子表达式 + - name: 参数 type: string - desc: 标准骰子表达式,如 3d6、2d8kh1、d20+5 + desc: 骰子表达式(如 3d6、2d8kh1、d20+5)或已定义的种子表 slug --- ## 概述 -`/roll` 命令用于在 Journal 流中掷骰并发布结果。结果会同步到所有连接的客户端。 +`/roll` 命令用于掷骰或随机抽取种子表,并将结果同步到所有客户端。 + +- 如果参数匹配已定义的**种子表** slug,则抽取并发布对应种子表的结果。 +- 否则,作为**骰子表达式**求值并发布掷骰结果。 ## 语法 ``` -/roll 骰子表达式 +/roll 骰子表达式 | 种子表键名 ``` ## 示例 +### 掷骰 + ``` /roll d20 /roll 3d6 @@ -30,6 +35,14 @@ props: /roll 4d6k3 ``` +### 种子表 + +``` +/roll npc +/roll encounter +/roll loot +``` + ## 支持的骰子表达式 | 表达式 | 说明 | @@ -40,8 +53,12 @@ props: | `1d20+5` | 1 个 20 面骰加 5 | | `4d6k3` | 4 个 6 面骰保留最高 3 个 | +## 种子表定义 + +种子表在文档中通过 `:spark[CSV路径]` 指令或 spark 形状的 markdown 表格定义,CSV 文件的第一列表头为骰子公式(如 d6、d20),后续列为数据列。 + ## 权限 - **GM**:可使用 - **玩家**:不可使用 -- **观察者**:不可使用 \ No newline at end of file +- **观察者**:不可使用