feat: expanded dice
This commit is contained in:
parent
34d78df810
commit
cc22b89226
|
|
@ -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<number | null>(null);
|
||||
const [isRolled, setIsRolled] = createSignal(false);
|
||||
const [rollDetail, setRollDetail] = createSignal<string>('');
|
||||
const [diceId] = createSignal(generateDiceId());
|
||||
|
||||
// 从 element 的 textContent 获取骰子公式
|
||||
const formula = element?.textContent?.trim() || '';
|
||||
|
||||
const handleClick = () => {
|
||||
if (isRolled()) {
|
||||
// 重置为公式
|
||||
setResult(null);
|
||||
setIsRolled(false);
|
||||
} else {
|
||||
// 掷骰子
|
||||
const rollResult = rollDice(formula);
|
||||
setResult(rollResult);
|
||||
// 使用的 key(如果没有提供则使用生成的 ID)
|
||||
const effectiveKey = () => props.key || diceId();
|
||||
|
||||
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 (
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<a
|
||||
href={props.key && isRolled() ? `?dice-${props.key}=${result()}` : '#'}
|
||||
href={effectiveKey() && isRolled() ? `?dice-${effectiveKey()}=${result()}` : '#'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
handleRoll();
|
||||
}}
|
||||
class="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer"
|
||||
class="text-blue-600 hover:text-blue-800 cursor-pointer"
|
||||
title={rollDetail() || '掷骰子'}
|
||||
>
|
||||
<span>🎲</span>
|
||||
<span>{displayText()}</span>
|
||||
🎲
|
||||
</a>
|
||||
<span
|
||||
onClick={handleTextClick}
|
||||
class={isRolled() ? 'cursor-pointer text-gray-700 hover:text-gray-900' : 'text-gray-600'}
|
||||
title={isRolled() ? '点击重置为公式' : formula}
|
||||
>
|
||||
{displayText()}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue