Files
ttrpg-tools/src/components/md-dice.tsx
T
hypercross 2f29f8774d feat: add spark table completion and rolling support
Implement "spark tables" functionality, which allows users to roll
dice against markdown tables to retrieve specific values.

- Add `sparkTablesSource` to scan markdown files for tables starting
  with a dice notation (e.g., d6, d20).
- Implement `/spark` command in the journal to resolve and roll
  spark tables.
- Add a new message type `spark` with a dedicated UI component to
  render the results.
- Update completions API to include spark table metadata.
2026-07-07 19:02:17 +08:00

101 lines
2.9 KiB
TypeScript

import { customElement, noShadowDOM } from "solid-element";
import { createSignal, onMount } from "solid-js";
import { rollFormula } from "./md-commander/hooks";
export interface DiceProps {
key?: string;
}
// 从 URL 参数获取骰子结果
function getDiceResultFromUrl(key: string): number | null {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
const value = params.get(`dice-${key}`);
return value ? parseInt(value) : null;
}
function setDiceResultToUrl(key: string, value: number | null) {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
if (value === null) {
params.delete(`dice-${key}`);
} else {
params.set(`dice-${key}`, value.toString());
}
const newUrl = `${window.location.pathname}${params.toString() ? `?${params.toString()}` : ""}${window.location.hash}`;
window.history.pushState({}, "", newUrl);
}
customElement("md-dice", { key: "" }, (props, { element }) => {
noShadowDOM();
const [result, setResult] = createSignal<number | null>(null);
const [isRolled, setIsRolled] = createSignal(false);
const [rollDetail, setRollDetail] = createSignal<string>("");
// 从 element 的 textContent 获取骰子公式
const formula = element?.textContent?.trim() || "";
// 隐藏原始文本内容
if (element) {
element.textContent = "";
}
// 使用的 key(如果没有提供则使用生成的 ID)
const effectiveKey = () => props.key;
onMount(() => {
// 初始化时检查 URL 参数
if (effectiveKey()) {
const urlResult = getDiceResultFromUrl(effectiveKey());
if (urlResult !== null) {
setResult(urlResult);
setIsRolled(true);
}
}
});
const handleRoll = (e: MouseEvent) => {
e.preventDefault();
// 点击骰子图标:总是重 roll
const rollResult = rollFormula(formula);
setResult(rollResult.result.total);
setRollDetail(rollResult.result.plainDetail);
setIsRolled(true);
if (effectiveKey()) {
setDiceResultToUrl(effectiveKey(), rollResult.result.total);
}
};
const handleTextClick = (e: MouseEvent) => {
e.preventDefault();
// 点击文本:总是重置为公式
setResult(null);
setIsRolled(false);
setRollDetail("");
if (effectiveKey()) {
setDiceResultToUrl(effectiveKey(), null);
}
};
const displayText = () => (isRolled() ? `${result()}` : formula);
return (
<span class="inline-flex items-center px-1">
<span
onClick={handleTextClick}
class="text-blue-600 hover:text-blue-800 cursor-pointer"
title={rollDetail() || "掷骰子"}
>
🎲
</span>
<a
onClick={handleRoll}
class="cursor-pointer text-gray-700 hover:text-gray-900"
title={isRolled() ? "点击重置为公式" : formula}
>
{displayText()}
</a>
</span>
);
});