- Spell slots. Every class that casts has a slot tracker, worked out from class and level: the full-caster table for bards, clerics, druids, sorcerers and wizards, the half-caster table for paladins and rangers (from level 2), and a warlock's few slots of one level. It is in the Spellcasting card on the sheet and in the character panel, and the party overview has a column for it. A player presses Use to spend a slot; only the DM gives one back. - Rests. The DM's "Rest…" button, beside the character list and on the party overview, rests any of the party at once. A short rest shows which Hit Dice each character can roll and heals from what they rolled (typed in, or rolled by the site), and gives a warlock their slots. A long rest puts back HP, every spell slot and half the level in Hit Dice, and clears temp HP. - Hit Dice are counted: one per level, shown on the sheet, the panel and the party overview. - Level Up. "Level Up…" on a sheet takes a character up one level with the whole checklist: the new max HP and a full heal, the class's new special abilities for levels 2 to 10, a Boon where the level grants one (Toughened raises Extra AC, Hardy gives temp HP now and after each long rest), fresh spell slots and new spells, and the free upgrade of one standard weapon. - A level typed into the Level field still writes down what the class gains on the way up, and leaves a line where a Boon is owed. - The docs cover all of it, with a generated list of what each class gains at each level. The Hollowcrown seed's note no longer says to track spell slots on paper. - New migration: spent spell slots and Hit Dice on a character. - All packages at version 1.14.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
4.3 KiB
JavaScript
85 lines
4.3 KiB
JavaScript
// Resting (the Hollowcrown book's Rules Cheat Sheet, "Resting"). Each function
|
|
// takes a finished character and its sheet and returns the columns to write
|
|
// plus an account of what happened, for the DM to read. Pure: no DB calls.
|
|
//
|
|
// Short rest (an hour): spend Hit Dice to heal, each one the class's die plus
|
|
// the Constitution modifier. A warlock's spell slots come back.
|
|
// Long rest (a night): all HP, all spell slots, Hit Dice up to half the level
|
|
// (at least one), and temp HP is gone, except what the Hardy Boon gives.
|
|
|
|
const { changeHp, lifeState, NO_DEATH_SAVES } = require('./hp');
|
|
const { hardyTempHp } = require('../game/levelUp');
|
|
|
|
// Once a day on a short rest a wizard or a druid (from level 2) gets some
|
|
// spent slots back. Which ones is the player's choice, so the DM puts them
|
|
// back on the sheet; this is the reminder, or null when it does not apply.
|
|
function recoveryNote(character, sheet) {
|
|
const feature = character.characterClass === 'WIZARD' ? 'Arcane Recovery' : character.characterClass === 'DRUID' && character.level >= 2 ? 'Natural Recovery' : null;
|
|
if (!feature || !sheet.spellSlots || !sheet.spellSlots.levels.some((slot) => slot.used > 0)) return null;
|
|
const budget = Math.ceil(character.level / 2);
|
|
return `${feature}: once per day, ${character.name} can regain spent slots adding up to ${budget} slot level${budget === 1 ? '' : 's'} (none of 6th level or higher). Put them back on the sheet.`;
|
|
}
|
|
|
|
// A problem with the Hit Dice asked for, or null. `rolled` is the total the
|
|
// dice showed, for dice rolled at the table.
|
|
function hitDiceProblem(character, sheet, dice, rolled) {
|
|
if (dice === 0) return null;
|
|
const { size, remaining } = sheet.hitDice;
|
|
if (lifeState(character) === 'DYING') return `${character.name} is dying: steady or heal them before they spend Hit Dice.`;
|
|
if (dice > remaining) return `${character.name} has ${remaining} Hit ${remaining === 1 ? 'Die' : 'Dice'} left, not ${dice}.`;
|
|
if (rolled != null && (rolled < dice || rolled > dice * size)) {
|
|
return `${character.name}: ${dice}d${size} adds up to between ${dice} and ${dice * size}, not ${rolled}.`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// `rolls` is what each Hit Die showed, or `rolled` their total when they were
|
|
// rolled at the table. The Constitution modifier is added once per die, and
|
|
// the rest never takes HP away.
|
|
function shortRest(character, sheet, { dice = 0, rolls = null, rolled = null } = {}) {
|
|
const total = dice === 0 ? 0 : rolls ? rolls.reduce((sum, roll) => sum + roll, 0) : rolled;
|
|
const conBonus = dice * sheet.abilities.CON.modifier;
|
|
const healed = Math.max(0, total + conBonus);
|
|
const pact = Boolean(sheet.spellSlots) && sheet.spellSlots.restoresOn === 'SHORT';
|
|
const slotsRestored = pact && sheet.spellSlots.levels.some((slot) => slot.used > 0);
|
|
|
|
const data = {
|
|
...(healed > 0 ? changeHp(character, character.currentHp + healed, sheet.hp.max) : {}),
|
|
hitDiceUsed: sheet.hitDice.used + dice,
|
|
...(pact ? { spellSlotsUsed: [] } : {}),
|
|
};
|
|
const hpAfter = data.currentHp ?? character.currentHp;
|
|
return {
|
|
data,
|
|
result: {
|
|
hpBefore: character.currentHp,
|
|
hpAfter,
|
|
maxHp: sheet.hp.max,
|
|
tempHp: character.tempHp,
|
|
hitDice: { size: sheet.hitDice.size, spent: dice, rolls, total, conBonus, healed: hpAfter - character.currentHp, left: sheet.hitDice.remaining - dice, of: sheet.hitDice.count, regained: 0 },
|
|
slotsRestored,
|
|
notes: [recoveryNote(character, sheet)].filter(Boolean),
|
|
},
|
|
};
|
|
}
|
|
|
|
function longRest(character, sheet) {
|
|
const regained = Math.min(sheet.hitDice.used, Math.max(1, Math.floor(character.level / 2)));
|
|
const tempHp = hardyTempHp(character.specialAbilities);
|
|
const data = { currentHp: sheet.hp.max, tempHp, ...NO_DEATH_SAVES, hitDiceUsed: sheet.hitDice.used - regained, spellSlotsUsed: [] };
|
|
return {
|
|
data,
|
|
result: {
|
|
hpBefore: character.currentHp,
|
|
hpAfter: sheet.hp.max,
|
|
maxHp: sheet.hp.max,
|
|
tempHp,
|
|
hitDice: { size: sheet.hitDice.size, spent: 0, rolls: null, total: 0, conBonus: 0, healed: 0, left: sheet.hitDice.remaining + regained, of: sheet.hitDice.count, regained },
|
|
slotsRestored: Boolean(sheet.spellSlots) && sheet.spellSlots.levels.some((slot) => slot.used > 0),
|
|
notes: tempHp > 0 ? [`Hardy: ${tempHp} Temp HP for the day.`] : [],
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { hitDiceProblem, shortRest, longRest };
|