244 lines
8.2 KiB
TypeScript
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;
|
|
} |