diff --git a/src/components/dice.tsx b/src/components/dice.tsx index d5fff62..30ae0ce 100644 --- a/src/components/dice.tsx +++ b/src/components/dice.tsx @@ -1,66 +1,201 @@ import { customElement, noShadowDOM } from 'solid-element'; -import { createSignal } from 'solid-js'; +import { createSignal, onMount } from 'solid-js'; -function rollDice(formula: string): number { - // 解析骰子公式,例如:2d6+d8 - const parts = formula.split('+').map(p => p.trim()); +interface RollResult { + total: number; + rolls: number[]; + detail: string; +} + +interface DiceOperator { + pattern: RegExp; + handler: (match: RegExpMatchArray, rolls: number[]) => { value: number; rolls: number[]; detail: string }; +} + +function rollDice(formula: string): RollResult { + const allRolls: number[] = []; let total = 0; + const details: string[] = []; - for (const part of parts) { - const match = part.match(/^(\d+)?d(\d+)$/i); + // 解析骰子公式,支持 + 和 - 运算符 + // 先按 + 和 - 分割,但保留运算符 + const tokens = formula.split(/([+-])/).map(t => t.trim()).filter(t => t.length > 0); + + let sign = 1; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (token === '+') { + sign = 1; + continue; + } + if (token === '-') { + sign = -1; + continue; + } + + // 处理 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; + + 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); + + 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]); + } + + const match = processedToken.match(/^(\d+)?d(\d+)$/i); if (match) { const count = parseInt(match[1] || '1'); const sides = parseInt(match[2]); - for (let i = 0; i < count; i++) { - total += Math.floor(Math.random() * sides) + 1; + const rolls: number[] = []; + + for (let j = 0; j < count; j++) { + rolls.push(Math.floor(Math.random() * sides) + 1); } + + let processedRolls = [...rolls]; + let detail = `${count}d${sides}`; + + // 处理 keep/drop 运算符 + if (keepHigh || keepLow || dropHigh || dropLow) { + const sorted = [...processedRolls].sort((a, b) => a - b); + + 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}`; + } + } + + const partTotal = processedRolls.reduce((a, b) => a + b, 0); + total += sign * partTotal; + allRolls.push(...processedRolls); + details.push(`${detail}=[${processedRolls.join(',')}]${sign < 0 ? ' (负)' : ''}`); } else { // 处理固定数字 - const num = parseInt(part); + const num = parseInt(token); if (!isNaN(num)) { - total += num; + total += sign * num; + details.push(`${num}${sign < 0 ? ' (负)' : ''}`); } } } - return total; + return { + total, + rolls: allRolls, + detail: details.join(' + ') + }; +} + +// 生成唯一 ID +let diceCounter = 0; +function generateDiceId(): string { + return `dice-${Date.now()}-${diceCounter++}`; +} + +// 从 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; } customElement('md-dice', { key: '' }, (props, { element }) => { noShadowDOM(); const [result, setResult] = createSignal(null); const [isRolled, setIsRolled] = createSignal(false); - + const [rollDetail, setRollDetail] = createSignal(''); + const [diceId] = createSignal(generateDiceId()); + // 从 element 的 textContent 获取骰子公式 const formula = element?.textContent?.trim() || ''; + + // 使用的 key(如果没有提供则使用生成的 ID) + const effectiveKey = () => props.key || diceId(); - const handleClick = () => { - if (isRolled()) { - // 重置为公式 - setResult(null); - setIsRolled(false); - } else { - // 掷骰子 - const rollResult = rollDice(formula); - setResult(rollResult); - setIsRolled(true); + onMount(() => { + // 初始化时检查 URL 参数 + if (effectiveKey()) { + const urlResult = getDiceResultFromUrl(effectiveKey()); + if (urlResult !== null) { + setResult(urlResult); + setIsRolled(true); + } } + }); + + const handleRoll = () => { + // 点击骰子图标:总是重 roll + const rollResult = rollDice(formula); + setResult(rollResult.total); + setRollDetail(rollResult.detail); + setIsRolled(true); + }; + + const handleTextClick = () => { + // 点击文本:总是重置为公式 + setResult(null); + setIsRolled(false); + setRollDetail(''); }; const displayText = () => (isRolled() ? `${result()}` : formula); return ( - { - e.preventDefault(); - handleClick(); - }} - class="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer" - > - 🎲 - {displayText()} - + + { + e.preventDefault(); + handleRoll(); + }} + class="text-blue-600 hover:text-blue-800 cursor-pointer" + title={rollDetail() || '掷骰子'} + > + 🎲 + + + {displayText()} + + ); });