refactor: merge /spark command into /roll
Consolidate the `/spark` command into `/roll` to simplify the CLI. The `/roll` command now handles both dice notation and spark table slugs. - Update `parseInput` to treat `/roll` arguments as a generic string. - Update `dispatchCommand` to check for spark table matches before evaluating dice notation or variable expressions. - Update command completions to show both dice and spark table suggestions under `/roll`. - Update documentation and UI placeholders to reflect the change.
This commit is contained in:
parent
c4038a5213
commit
d74ba69fdf
|
|
@ -690,16 +690,16 @@ describe("var-reactivity", () => {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe("parseInput", () => {
|
describe("parseInput", () => {
|
||||||
test("parses /roll command", () => {
|
test("parses /roll command with spark table slug", () => {
|
||||||
const result = parseInput("/roll 3d6+5");
|
const result = parseInput("/roll my-table");
|
||||||
expect(result.type).toBe("roll");
|
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", () => {
|
test("parses /roll command with dice notation", () => {
|
||||||
const result = parseInput("/spark my-table");
|
const result = parseInput("/roll 3d6+5");
|
||||||
expect(result.type).toBe("spark");
|
expect(result.type).toBe("roll");
|
||||||
expect(result.payload).toEqual({ key: "my-table" });
|
expect(result.payload).toEqual({ arg: "3d6+5" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parses /link command", () => {
|
test("parses /link command", () => {
|
||||||
|
|
@ -743,7 +743,7 @@ describe("parseInput", () => {
|
||||||
|
|
||||||
test("returns error for empty /roll", () => {
|
test("returns error for empty /roll", () => {
|
||||||
const result = parseInput("/roll ");
|
const result = parseInput("/roll ");
|
||||||
expect(result.error).toBe("需要骰子表达式");
|
expect(result.error).toBe("需要骰子表达式或种子表键名");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns error for empty /set", () => {
|
test("returns error for empty /set", () => {
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export const RevealManager: Component = () => {
|
||||||
rect: sparkTable.getBoundingClientRect(),
|
rect: sparkTable.getBoundingClientRect(),
|
||||||
action: () =>
|
action: () =>
|
||||||
setActionPrefill({
|
setActionPrefill({
|
||||||
command: "/spark",
|
command: "/roll",
|
||||||
text: combinedSlug,
|
text: combinedSlug,
|
||||||
}),
|
}),
|
||||||
title: "Roll spark table",
|
title: "Roll spark table",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ import journalGmRaw from "../doc-entries/journal-gm.md";
|
||||||
import journalPlayerRaw from "../doc-entries/journal-player.md";
|
import journalPlayerRaw from "../doc-entries/journal-player.md";
|
||||||
import journalSetRaw from "../doc-entries/journal-set.md";
|
import journalSetRaw from "../doc-entries/journal-set.md";
|
||||||
import journalRollRaw from "../doc-entries/journal-roll.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";
|
import journalLinkRaw from "../doc-entries/journal-link.md";
|
||||||
|
|
||||||
const rawDocuments: string[] = [
|
const rawDocuments: string[] = [
|
||||||
|
|
@ -53,7 +52,6 @@ const rawDocuments: string[] = [
|
||||||
journalPlayerRaw,
|
journalPlayerRaw,
|
||||||
journalSetRaw,
|
journalSetRaw,
|
||||||
journalRollRaw,
|
journalRollRaw,
|
||||||
journalSparkRaw,
|
|
||||||
journalLinkRaw,
|
journalLinkRaw,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ export const JournalInput: Component = () => {
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={
|
placeholder={
|
||||||
isGm()
|
isGm()
|
||||||
? "输入消息,或使用 /roll、/spark、/link、/set 命令..."
|
? "输入消息,或使用 /roll、/link、/set 命令..."
|
||||||
: "输入消息,或使用 /set 命令..."
|
: "输入消息,或使用 /set 命令..."
|
||||||
}
|
}
|
||||||
rows={2}
|
rows={2}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ export function buildCompletions(
|
||||||
|
|
||||||
const commands = [
|
const commands = [
|
||||||
{ label: "/roll", kind: "command" as const, insertText: "/roll " },
|
{ 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: "/link", kind: "command" as const, insertText: "/link " },
|
||||||
{ label: "/set", kind: "command" as const, insertText: "/set " },
|
{ label: "/set", kind: "command" as const, insertText: "/set " },
|
||||||
];
|
];
|
||||||
|
|
@ -40,46 +39,52 @@ export function buildCompletions(
|
||||||
: [{ label: "未知命令", kind: "no-results", insertText: "" }];
|
: [{ label: "未知命令", kind: "no-results", insertText: "" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// After /roll — show dice suggestions
|
// After /roll — show dice AND spark table suggestions
|
||||||
if (raw.startsWith("/roll ")) {
|
if (raw.startsWith("/roll ")) {
|
||||||
const prefix = raw.slice("/roll ".length).toLowerCase();
|
const prefix = raw.slice("/roll ".length).toLowerCase();
|
||||||
const matches = data.dice
|
|
||||||
|
const diceMatches = data.dice
|
||||||
.filter(
|
.filter(
|
||||||
(d) =>
|
(d) =>
|
||||||
d.notation.toLowerCase().includes(prefix) ||
|
d.notation.toLowerCase().includes(prefix) ||
|
||||||
d.label.toLowerCase().includes(prefix),
|
d.label.toLowerCase().includes(prefix),
|
||||||
)
|
)
|
||||||
.slice(0, 8);
|
.slice(0, 4);
|
||||||
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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// After /spark — show spark table suggestions
|
const sparkMatches = data.sparkTables
|
||||||
if (raw.startsWith("/spark ")) {
|
|
||||||
const prefix = raw.slice("/spark ".length).toLowerCase();
|
|
||||||
const matches = data.sparkTables
|
|
||||||
.filter(
|
.filter(
|
||||||
(s) =>
|
(s) =>
|
||||||
s.slug.toLowerCase().includes(prefix) ||
|
s.slug.toLowerCase().includes(prefix) ||
|
||||||
s.label.toLowerCase().includes(prefix),
|
s.label.toLowerCase().includes(prefix),
|
||||||
)
|
)
|
||||||
.slice(0, 8);
|
.slice(0, 4);
|
||||||
if (matches.length === 0) {
|
|
||||||
return [{ label: "未找到种子表", kind: "no-results", insertText: "" }];
|
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})`,
|
for (const s of sparkMatches) {
|
||||||
kind: "value" as const,
|
items.push({
|
||||||
insertText: `/spark ${s.slug}`,
|
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
|
// After /link — show article and heading suggestions
|
||||||
if (raw.startsWith("/link ")) {
|
if (raw.startsWith("/link ")) {
|
||||||
const prefix = raw.slice("/link ".length).toLowerCase();
|
const prefix = raw.slice("/link ".length).toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -87,30 +87,57 @@ export async function dispatchCommand(
|
||||||
|
|
||||||
// GM: all commands
|
// GM: all commands
|
||||||
if (parsed.type === "roll") {
|
if (parsed.type === "roll") {
|
||||||
const p = resolveRollPayload(
|
const arg = (parsed.payload as { arg: string }).arg;
|
||||||
parsed.payload as { notation: string; label?: string },
|
|
||||||
);
|
// 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);
|
const result = sendMessage("roll", p);
|
||||||
return finish(unwrap(result));
|
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") {
|
if (parsed.type === "set" || parsed.type === "rolltag") {
|
||||||
return finish(dispatchSet(parsed.payload as Record<string, unknown>, ctx));
|
return finish(dispatchSet(parsed.payload as Record<string, unknown>, ctx));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,17 @@ export interface CompletionItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParsedInput {
|
export interface ParsedInput {
|
||||||
type: "chat" | "roll" | "spark" | "link" | "set" | "rolltag";
|
type: "chat" | "roll" | "link" | "set" | "rolltag";
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseInput(raw: string): ParsedInput {
|
export function parseInput(raw: string): ParsedInput {
|
||||||
if (raw.startsWith("/roll ")) {
|
if (raw.startsWith("/roll ")) {
|
||||||
const notation = raw.slice("/roll ".length).trim();
|
const arg = raw.slice("/roll ".length).trim();
|
||||||
if (!notation)
|
if (!arg)
|
||||||
return { type: "roll", payload: {}, error: "需要骰子表达式" };
|
return { type: "roll", payload: {}, error: "需要骰子表达式或种子表键名" };
|
||||||
return { type: "roll", payload: { notation, label: notation } };
|
return { type: "roll", payload: { arg } };
|
||||||
}
|
|
||||||
|
|
||||||
if (raw.startsWith("/spark ")) {
|
|
||||||
const key = raw.slice("/spark ".length).trim();
|
|
||||||
if (!key) return { type: "spark", payload: {}, error: "需要种子表键名" };
|
|
||||||
return { type: "spark", payload: { key } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw.startsWith("/link ")) {
|
if (raw.startsWith("/link ")) {
|
||||||
|
|
@ -69,7 +63,7 @@ export function parseInput(raw: string): ParsedInput {
|
||||||
return { type: "set", payload: { key, expr } };
|
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: "请补全命令" };
|
return { type: "chat", payload: {}, error: "请补全命令" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,32 @@
|
||||||
---
|
---
|
||||||
tag: journal-roll
|
tag: journal-roll
|
||||||
icon: 🎲
|
icon: 🎲
|
||||||
title: 掷骰命令
|
title: 掷骰与种子表命令
|
||||||
description: 在 Journal 中发送掷骰结果,支持标准骰子表达式。
|
description: 在 Journal 中掷骰或随机生成种子表内容。优先匹配种子表,否则作为骰子表达式处理。
|
||||||
syntax: '/roll 骰子表达式'
|
syntax: '/roll 骰子表达式 | 种子表键名'
|
||||||
props:
|
props:
|
||||||
- name: 骰子表达式
|
- name: 参数
|
||||||
type: string
|
type: string
|
||||||
desc: 标准骰子表达式,如 3d6、2d8kh1、d20+5
|
desc: 骰子表达式(如 3d6、2d8kh1、d20+5)或已定义的种子表 slug
|
||||||
---
|
---
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
`/roll` 命令用于在 Journal 流中掷骰并发布结果。结果会同步到所有连接的客户端。
|
`/roll` 命令用于掷骰或随机抽取种子表,并将结果同步到所有客户端。
|
||||||
|
|
||||||
|
- 如果参数匹配已定义的**种子表** slug,则抽取并发布对应种子表的结果。
|
||||||
|
- 否则,作为**骰子表达式**求值并发布掷骰结果。
|
||||||
|
|
||||||
## 语法
|
## 语法
|
||||||
|
|
||||||
```
|
```
|
||||||
/roll 骰子表达式
|
/roll 骰子表达式 | 种子表键名
|
||||||
```
|
```
|
||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
|
|
||||||
|
### 掷骰
|
||||||
|
|
||||||
```
|
```
|
||||||
/roll d20
|
/roll d20
|
||||||
/roll 3d6
|
/roll 3d6
|
||||||
|
|
@ -30,6 +35,14 @@ props:
|
||||||
/roll 4d6k3
|
/roll 4d6k3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 种子表
|
||||||
|
|
||||||
|
```
|
||||||
|
/roll npc
|
||||||
|
/roll encounter
|
||||||
|
/roll loot
|
||||||
|
```
|
||||||
|
|
||||||
## 支持的骰子表达式
|
## 支持的骰子表达式
|
||||||
|
|
||||||
| 表达式 | 说明 |
|
| 表达式 | 说明 |
|
||||||
|
|
@ -40,6 +53,10 @@ props:
|
||||||
| `1d20+5` | 1 个 20 面骰加 5 |
|
| `1d20+5` | 1 个 20 面骰加 5 |
|
||||||
| `4d6k3` | 4 个 6 面骰保留最高 3 个 |
|
| `4d6k3` | 4 个 6 面骰保留最高 3 个 |
|
||||||
|
|
||||||
|
## 种子表定义
|
||||||
|
|
||||||
|
种子表在文档中通过 `:spark[CSV路径]` 指令或 spark 形状的 markdown 表格定义,CSV 文件的第一列表头为骰子公式(如 d6、d20),后续列为数据列。
|
||||||
|
|
||||||
## 权限
|
## 权限
|
||||||
|
|
||||||
- **GM**:可使用
|
- **GM**:可使用
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue