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>>, explicit?: boolean): string { const map = new Map(); 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; }