2026-02-26 09:08:05 +08:00
|
|
|
|
import { customElement, noShadowDOM } from "solid-element";
|
|
|
|
|
|
import { createSignal, onMount } from "solid-js";
|
2026-02-26 00:17:23 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
interface RollResult {
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
rolls: number[];
|
|
|
|
|
|
detail: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rollDice(formula: string): RollResult {
|
|
|
|
|
|
const allRolls: number[] = [];
|
2026-02-26 00:17:23 +08:00
|
|
|
|
let total = 0;
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const details: string[] = [];
|
2026-02-26 00:17:23 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
// 解析骰子公式,支持 + 和 - 运算符
|
|
|
|
|
|
// 先按 + 和 - 分割,但保留运算符
|
2026-02-26 09:08:05 +08:00
|
|
|
|
const tokens = formula
|
|
|
|
|
|
.split(/([+-])/)
|
|
|
|
|
|
.map((t) => t.trim())
|
|
|
|
|
|
.filter((t) => t.length > 0);
|
|
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
let sign = 1;
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
|
|
|
|
const token = tokens[i];
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
|
|
|
|
|
if (token === "+") {
|
2026-02-26 09:01:47 +08:00
|
|
|
|
sign = 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
if (token === "-") {
|
2026-02-26 09:01:47 +08:00
|
|
|
|
sign = -1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
// 处理 kh/kl/dh/dl 运算符
|
|
|
|
|
|
let processedToken = token;
|
|
|
|
|
|
let keepHigh = false;
|
|
|
|
|
|
let keepLow = false;
|
|
|
|
|
|
let dropHigh = false;
|
|
|
|
|
|
let dropLow = false;
|
|
|
|
|
|
let keepCount = 0;
|
|
|
|
|
|
let dropCount = 0;
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const khMatch = token.match(/^(.+?)kh(\d+)$/i);
|
|
|
|
|
|
const klMatch = token.match(/^(.+?)kl(\d+)$/i);
|
|
|
|
|
|
const dhMatch = token.match(/^(.+?)dh(\d+)$/i);
|
|
|
|
|
|
const dlMatch = token.match(/^(.+?)dl(\d+)$/i);
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
if (khMatch) {
|
|
|
|
|
|
processedToken = khMatch[1];
|
|
|
|
|
|
keepHigh = true;
|
|
|
|
|
|
keepCount = parseInt(khMatch[2]);
|
|
|
|
|
|
} else if (klMatch) {
|
|
|
|
|
|
processedToken = klMatch[1];
|
|
|
|
|
|
keepLow = true;
|
|
|
|
|
|
keepCount = parseInt(klMatch[2]);
|
|
|
|
|
|
} else if (dhMatch) {
|
|
|
|
|
|
processedToken = dhMatch[1];
|
|
|
|
|
|
dropHigh = true;
|
|
|
|
|
|
dropCount = parseInt(dhMatch[2]);
|
|
|
|
|
|
} else if (dlMatch) {
|
|
|
|
|
|
processedToken = dlMatch[1];
|
|
|
|
|
|
dropLow = true;
|
|
|
|
|
|
dropCount = parseInt(dlMatch[2]);
|
|
|
|
|
|
}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const match = processedToken.match(/^(\d+)?d(\d+)$/i);
|
2026-02-26 00:17:23 +08:00
|
|
|
|
if (match) {
|
2026-02-26 09:08:05 +08:00
|
|
|
|
const count = parseInt(match[1] || "1");
|
2026-02-26 00:17:23 +08:00
|
|
|
|
const sides = parseInt(match[2]);
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const rolls: number[] = [];
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
for (let j = 0; j < count; j++) {
|
|
|
|
|
|
rolls.push(Math.floor(Math.random() * sides) + 1);
|
|
|
|
|
|
}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
let processedRolls = [...rolls];
|
|
|
|
|
|
let detail = `${count}d${sides}`;
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
// 处理 keep/drop 运算符
|
|
|
|
|
|
if (keepHigh || keepLow || dropHigh || dropLow) {
|
|
|
|
|
|
const sorted = [...processedRolls].sort((a, b) => a - b);
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
if (dropHigh && dropCount > 0) {
|
|
|
|
|
|
processedRolls = sorted.slice(0, sorted.length - dropCount);
|
|
|
|
|
|
detail += `d${dropCount}h`;
|
|
|
|
|
|
} else if (dropLow && dropCount > 0) {
|
|
|
|
|
|
processedRolls = sorted.slice(dropCount);
|
|
|
|
|
|
detail += `d${dropCount}l`;
|
|
|
|
|
|
} else if (keepHigh && keepCount > 0) {
|
|
|
|
|
|
processedRolls = sorted.slice(-keepCount);
|
|
|
|
|
|
detail += `kh${keepCount}`;
|
|
|
|
|
|
} else if (keepLow && keepCount > 0) {
|
|
|
|
|
|
processedRolls = sorted.slice(0, keepCount);
|
|
|
|
|
|
detail += `kl${keepCount}`;
|
|
|
|
|
|
}
|
2026-02-26 00:17:23 +08:00
|
|
|
|
}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const partTotal = processedRolls.reduce((a, b) => a + b, 0);
|
|
|
|
|
|
total += sign * partTotal;
|
|
|
|
|
|
allRolls.push(...processedRolls);
|
2026-02-26 09:08:05 +08:00
|
|
|
|
details.push(
|
|
|
|
|
|
`${detail}=[${processedRolls.join(",")}]${sign < 0 ? " (负)" : ""}`,
|
|
|
|
|
|
);
|
2026-02-26 00:17:23 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 处理固定数字
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const num = parseInt(token);
|
2026-02-26 00:17:23 +08:00
|
|
|
|
if (!isNaN(num)) {
|
2026-02-26 09:01:47 +08:00
|
|
|
|
total += sign * num;
|
2026-02-26 09:08:05 +08:00
|
|
|
|
details.push(`${num}${sign < 0 ? " (负)" : ""}`);
|
2026-02-26 00:17:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
return {
|
|
|
|
|
|
total,
|
|
|
|
|
|
rolls: allRolls,
|
2026-02-26 09:08:05 +08:00
|
|
|
|
detail: details.join(" + "),
|
2026-02-26 09:01:47 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从 URL 参数获取骰子结果
|
|
|
|
|
|
function getDiceResultFromUrl(key: string): number | null {
|
2026-02-26 09:08:05 +08:00
|
|
|
|
if (typeof window === "undefined") return null;
|
2026-02-26 09:01:47 +08:00
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const value = params.get(`dice-${key}`);
|
|
|
|
|
|
return value ? parseInt(value) : null;
|
2026-02-26 00:17:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 09:18:05 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
|
|
|
|
|
|
customElement("md-dice", { key: "" }, (props, { element }) => {
|
2026-02-26 00:47:26 +08:00
|
|
|
|
noShadowDOM();
|
2026-02-26 00:17:23 +08:00
|
|
|
|
const [result, setResult] = createSignal<number | null>(null);
|
|
|
|
|
|
const [isRolled, setIsRolled] = createSignal(false);
|
2026-02-26 09:08:05 +08:00
|
|
|
|
const [rollDetail, setRollDetail] = createSignal<string>("");
|
|
|
|
|
|
|
2026-02-26 00:47:26 +08:00
|
|
|
|
// 从 element 的 textContent 获取骰子公式
|
2026-02-26 09:08:05 +08:00
|
|
|
|
const formula = element?.textContent?.trim() || "";
|
|
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
// 使用的 key(如果没有提供则使用生成的 ID)
|
2026-02-26 09:08:05 +08:00
|
|
|
|
const effectiveKey = () => props.key;
|
2026-02-26 00:47:26 +08:00
|
|
|
|
|
2026-02-26 09:01:47 +08:00
|
|
|
|
onMount(() => {
|
|
|
|
|
|
// 初始化时检查 URL 参数
|
|
|
|
|
|
if (effectiveKey()) {
|
|
|
|
|
|
const urlResult = getDiceResultFromUrl(effectiveKey());
|
|
|
|
|
|
if (urlResult !== null) {
|
|
|
|
|
|
setResult(urlResult);
|
|
|
|
|
|
setIsRolled(true);
|
|
|
|
|
|
}
|
2026-02-26 00:17:23 +08:00
|
|
|
|
}
|
2026-02-26 09:01:47 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const handleRoll = () => {
|
|
|
|
|
|
// 点击骰子图标:总是重 roll
|
|
|
|
|
|
const rollResult = rollDice(formula);
|
|
|
|
|
|
setResult(rollResult.total);
|
|
|
|
|
|
setRollDetail(rollResult.detail);
|
|
|
|
|
|
setIsRolled(true);
|
2026-02-26 09:18:05 +08:00
|
|
|
|
if (effectiveKey()) {
|
|
|
|
|
|
setDiceResultToUrl(effectiveKey(), rollResult.total);
|
|
|
|
|
|
}
|
2026-02-26 09:01:47 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTextClick = () => {
|
|
|
|
|
|
// 点击文本:总是重置为公式
|
|
|
|
|
|
setResult(null);
|
|
|
|
|
|
setIsRolled(false);
|
2026-02-26 09:08:05 +08:00
|
|
|
|
setRollDetail("");
|
2026-02-26 09:18:05 +08:00
|
|
|
|
if (effectiveKey()) {
|
|
|
|
|
|
setDiceResultToUrl(effectiveKey(), null);
|
|
|
|
|
|
}
|
2026-02-26 00:17:23 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-26 00:47:26 +08:00
|
|
|
|
const displayText = () => (isRolled() ? `${result()}` : formula);
|
2026-02-26 00:17:23 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-26 09:01:47 +08:00
|
|
|
|
<span class="inline-flex items-center gap-1">
|
|
|
|
|
|
<a
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
handleRoll();
|
|
|
|
|
|
}}
|
|
|
|
|
|
class="text-blue-600 hover:text-blue-800 cursor-pointer"
|
2026-02-26 09:08:05 +08:00
|
|
|
|
title={rollDetail() || "掷骰子"}
|
2026-02-26 09:01:47 +08:00
|
|
|
|
>
|
|
|
|
|
|
🎲
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<span
|
|
|
|
|
|
onClick={handleTextClick}
|
2026-02-26 09:08:05 +08:00
|
|
|
|
class={
|
|
|
|
|
|
isRolled()
|
|
|
|
|
|
? "cursor-pointer text-gray-700 hover:text-gray-900"
|
|
|
|
|
|
: "text-gray-600"
|
|
|
|
|
|
}
|
|
|
|
|
|
title={isRolled() ? "点击重置为公式" : formula}
|
2026-02-26 09:01:47 +08:00
|
|
|
|
>
|
|
|
|
|
|
{displayText()}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
2026-02-26 00:17:23 +08:00
|
|
|
|
);
|
2026-02-26 00:47:26 +08:00
|
|
|
|
});
|