HP formula stored on participant, loads in edit modal

hpFormula field persisted on participant doc (makeParticipant + builder).
Add handler stores formula. Edit modal loads participant.hpFormula, reroll
button sets maxHp field (no save until save). Edit submit persists formula.
Formula only for monster/npc types.
This commit is contained in:
david raistrick
2026-07-07 17:24:36 -04:00
parent 2dfa155469
commit 6f11edab94
4 changed files with 169 additions and 17 deletions
+33 -4
View File
@@ -28,6 +28,22 @@ const generateId = () =>
const rollD20 = () => Math.floor(Math.random() * 20) + 1;
// Parse HP formula like "2d6+9" or "3d8-2" or "1d20". Roll, return total.
// Returns null on invalid input. Whitespace/case tolerant.
function rollHpFormula(str) {
if (!str || typeof str !== 'string') return null;
const m = str.trim().toLowerCase().match(/^(\d+)d(\d+)\s*([+-]\s*\d+)?$/);
if (!m) return null;
const count = parseInt(m[1], 10);
const sides = parseInt(m[2], 10);
if (count === 0 || sides === 0) return null;
let bonus = 0;
if (m[3]) bonus = parseInt(m[3].replace(/\s/g, ''), 10);
let total = 0;
for (let i = 0; i < count; i++) total += Math.floor(Math.random() * sides) + 1;
return total + bonus;
}
const formatInitMod = (mod) => {
if (mod === undefined || mod === null) return 'N/A';
return mod >= 0 ? `+${mod}` : `${mod}`;
@@ -344,6 +360,7 @@ function makeParticipant(opts) {
status: opts.status || (opts.currentHp === 0 ? 'dying' : 'conscious'),
deathSaveSuccesses: opts.deathSaveSuccesses || 0,
deathSaveFailures: opts.deathSaveFailures || 0,
hpFormula: opts.hpFormula || null,
};
}
@@ -365,11 +382,22 @@ function buildCharacterParticipant(character) {
};
}
function buildMonsterParticipant({ name, maxHp, initMod, asNpc }) {
function buildMonsterParticipant({ name, maxHp, initMod, asNpc, hpFormula }) {
const initiativeRoll = rollD20();
const modifier = initMod !== undefined ? initMod : MONSTER_DEFAULT_INIT_MOD;
const finalInitiative = initiativeRoll + modifier;
const hp = maxHp || DEFAULT_MAX_HP;
// maxHp wins if both set; else roll formula; else default.
let hp;
let hpRoll = null;
if (maxHp) {
hp = maxHp;
} else if (hpFormula) {
const rolled = rollHpFormula(hpFormula);
hp = rolled || DEFAULT_MAX_HP;
hpRoll = rolled;
} else {
hp = DEFAULT_MAX_HP;
}
return {
participant: makeParticipant({
name,
@@ -377,8 +405,9 @@ function buildMonsterParticipant({ name, maxHp, initMod, asNpc }) {
initiative: finalInitiative,
maxHp: hp,
currentHp: hp,
hpFormula: hpFormula || null,
}),
roll: { roll: initiativeRoll, mod: modifier, total: finalInitiative },
roll: { roll: initiativeRoll, mod: modifier, total: finalInitiative, hpRoll },
};
}
@@ -1044,7 +1073,7 @@ async function toggleHidePlayerHp(currentValue, ctx) {
module.exports = {
DEFAULT_MAX_HP, DEFAULT_INIT_MOD, MONSTER_DEFAULT_INIT_MOD,
generateId, rollD20, formatInitMod,
generateId, rollD20, rollHpFormula, formatInitMod,
sortParticipantsByInitiative, syncTurnOrder, computeTurnOrderAfterRemoval,
snapshotOf, buildEntry, expandUndo,
makeParticipant, buildCharacterParticipant, buildMonsterParticipant,