obsidian-visualiser/shared/dice.ts

244 lines
8.2 KiB
TypeScript

import type { MainStat } from "~/types/character";
import { MAIN_STATS, mainStatShortTexts } from "./character";
import type { Proxy } from "./reactive";
//[Quantité de dés, Nb de face du dé (1 = pas besoin de roll de dé), explosif]
export type Dice = [number, number | MainStat] | [number, number | MainStat, true];
export type DiceRoll = Dice[];
enum State {
START,
QUANTITY,
D_LETTER,
FACES_NUMBER,
FACES_STAT,
MODIFIER_SIGN,
MODIFIER,
EXPLODING,
END_DICE,
};
export function parseDice(formula: string): DiceRoll
{
const result: DiceRoll = [];
let state = State.START;
let i = 0, length = formula.length;
//Variables utilitaires de la State machine
let buffer = "";
let isExploding = false, isNegative = false, currentQty = 1, currentFaces: number | MainStat = 1;
const reset = () => {
if (state !== State.START && state !== State.END_DICE)
{
const dice: Dice = isExploding ? [currentQty, currentFaces, true] : [currentQty, currentFaces];
if (isNegative)
{
dice[0] = -dice[0];
isNegative = false;
}
result.push(dice);
}
currentQty = 1;
currentFaces = 1;
isExploding = false;
buffer = "";
};
while(i <= length)
{
const char = i < length ? formula[i]?.toLowerCase() ?? '\0' : '\0';
const isDigit = char >= '0' && char <= '9';
const isLetter = char >= 'a' && char <= 'z';
const isEnd = i === length;
switch (state) {
case State.START:
if (isDigit)
{
buffer = char;
state = State.QUANTITY;
}
else if (char === 'd')
{
currentQty = 1;
state = State.D_LETTER;
}
else if (isLetter)
{
buffer = char;
state = State.FACES_STAT;
}
else if (char === '+' || char === '-')
{
isNegative = char === '-';
state = State.MODIFIER_SIGN;
}
else if (!isEnd && char !== ' ' && char !== '\t')
throw new Error(`Caractère inattendu '${char}' à la position ${i}`);
break;
case State.QUANTITY:
if (isDigit)
buffer += char;
else if (char === 'd')
{
currentQty = parseInt(buffer, 10) || 1;
buffer = "";
state = State.D_LETTER;
}
else
{
currentQty = parseInt(buffer, 10);
currentFaces = 1;
reset();
state = State.END_DICE;
i--;
}
break;
case State.D_LETTER:
if (isDigit)
{
buffer = char;
state = State.FACES_NUMBER;
}
else if (isLetter)
{
buffer = 'd' + char;
state = State.FACES_STAT;
}
else
throw new Error(`Attendu nombre ou stat après 'd' à la position ${i}`);
break;
case State.FACES_NUMBER:
if (isDigit)
buffer += char;
else if (char === '!')
{
currentFaces = parseInt(buffer, 10);
state = State.EXPLODING;
}
else
{
currentFaces = parseInt(buffer, 10);
reset();
state = State.END_DICE;
i--;
}
break;
case State.FACES_STAT:
if (isLetter && buffer.length < 3)
buffer += char;
else if (char === '!')
{
const stat = validStat(buffer);
if (!stat)
throw new Error(`Stat invalide '${buffer}' à la position ${i - buffer.length}`);
currentFaces = stat;
state = State.EXPLODING;
}
else
{
const stat = validStat(buffer);
if (!stat)
throw new Error(`Stat invalide '${buffer}' à la position ${i - buffer.length}`);
currentFaces = stat;
reset();
state = State.END_DICE;
i--;
}
break;
case State.EXPLODING:
isExploding = true;
reset();
state = State.END_DICE;
i--;
break;
case State.MODIFIER_SIGN:
if (isDigit)
{
buffer = char;
state = State.MODIFIER;
}
else if (char === 'd')
{
currentQty = 1;
state = State.D_LETTER;
}
else if (isLetter)
{
buffer = char;
state = State.FACES_STAT;
}
else
throw new Error(`Attendu nombre après '${isNegative ? "-" : "+"}' à la position ${i}`);
break;
case State.MODIFIER:
if (isDigit)
buffer += char;
else
{
currentQty = parseInt(buffer, 10);
currentFaces = 1;
if (isNegative) currentQty = -currentQty;
reset();
isNegative = false;
state = State.END_DICE;
i--;
}
break;
case State.END_DICE:
if (char === '+' || char === '-')
{
isNegative = char === '-';
state = State.MODIFIER_SIGN;
}
else if (char === 'd')
{
currentQty = 1;
state = State.D_LETTER;
}
else if (!isEnd && char !== ' ' && char !== '\t')
throw new Error(`Opérateur attendu, trouvé '${char}' à la position ${i}`);
break;
}
i++;
}
return result;
}
export function stringifyRoll(dices: DiceRoll, modifiers?: Proxy<Partial<Record<MainStat, number>>>, explicit?: boolean): string
{
const map = new Map<string, number>();
dices.forEach(e => {
if(e[0] === 0 || e[1] === 0) return;
const target = typeof e[1] === 'string' ? modifiers && e[1] in modifiers ? modifiers[e[1]]! * e[0] : `@${e[1]}` : e[0]; //@ character used to sort the stat dices at the end
const key = typeof e[1] === 'string' ? typeof target === 'string' ? e[1] : '1' : e[2] ? e[1].toString(10) + '!' : e[1].toString(10), value = map.get(key);
if(typeof target === 'string') map.has(target) ? map.set(target, 1) : map.set(target, map.get(target)! + 1);
else value === undefined ? map.set(key, target) : map.set(key, value + target);
});
const stringify = ([face, amount]: [string, number]) => (face.startsWith('@') && MAIN_STATS.includes(face.substring(1) as MainStat)) || face === '1' ? `${amount < 0 ? '-' : '+'}${face.startsWith('@') && MAIN_STATS.includes(face.substring(1) as MainStat) ? mainStatShortTexts[face.substring(1) as MainStat]! : amount}` : `${amount < 0 ? '-' : '+'}${explicit ?? false ? amount : amount !== 1 ? amount : ''}d${face}`;
const text = [...map.entries()].sort((a, b) => b[0].localeCompare(a[0])).reduce((p, v) => p + stringify(v), '');
return text.startsWith('+') ? text.substring(1) : text;
}
function validStat(text: string): MainStat | undefined
{
return Object.entries(mainStatShortTexts).find(e => e[1].toLowerCase() === text.toLowerCase())?.at(0) as MainStat | undefined;
}