5.0 KiB
D&D 5e Rules Reference — Implementation Spec
This is the canonical math the backend must implement. Build it as a single shared
calculation module (e.g. backend/src/game/calculations.ts) that is the only
place these formulas live — every API route and the character builder call into it
rather than re-deriving numbers. This mirrors the logic already built and verified
in the "DnD Character Sheet Calculator.xlsx" workbook delivered earlier in this
project; treat that workbook as the reference implementation if anything below is
ambiguous.
All rules are 5e (2014 core rules).
Ability Modifier
modifier = floor((score - 10) / 2)
Use Math.floor, not integer truncation — they differ for negative results
(e.g. score 7 → (7-10)/2 = -1.5 → floor = -2, not -1).
Proficiency Bonus
proficiencyBonus = 2 + floor((level - 1) / 4)
Level 1–4 → +2, 5–8 → +3, 9–12 → +4, 13–16 → +5, 17–20 → +6.
Final Ability Score
finalScore = rolledScore + speciesBonus(ability, species)
speciesBonus comes from the Species Ability Score Increase table below. See
seed-data/species.json.
Saving Throws
saveBonus = abilityModifier + (proficient ? proficiencyBonus : 0)
Proficiency per ability comes from the Class Quick Facts table (seed-data/classes.json).
Skills
skillBonus = governingAbilityModifier + (proficient ? proficiencyBonus : 0)
Governing ability per skill is fixed — see seed-data/skills.json. Proficiency is
a per-character, per-skill boolean the player/DM sets during character building
(CharacterSkillProficiency in the Prisma schema).
Passive Perception:
passivePerception = 10 + perceptionSkillBonus
Armor Class
armorClass = 10 + dexModifier + armorBonus
armorBonus is a flat number the player enters representing whatever armor/shield
they have equipped (this matches the workbook's "Armor Bonus (from armor worn)"
input cell). Do not try to model armor types as a lookup table unless you want to
extend scope later — a plain integer field on Character is enough for v1.
Initiative
initiative = dexModifier
Speed
Comes from the species table (30 ft standard; 25 ft for Gnome, Dwarf, Halfling).
Store as speed on the species seed data; characters may override it
(speedOverride on Character) for future feature support (e.g. barbarian speed
boosts) without a schema change.
Hit Points
Hit Die size comes from class (seed-data/classes.json).
hpMax(level) =
if level === 1:
hitDieMax + conModifier
else:
hitDieMax + conModifier
+ (level - 1) * (floor(hitDieSize / 2) + 1 + conModifier)
Important: level 1 always uses the maximum possible roll of the Hit Die, not
the average. Every level after 1 uses the fixed "average" value
floor(hitDieSize / 2) + 1 (e.g. d10 → 6, d6 → 4, d12 → 7) plus the CON modifier
again. This was a bug caught and fixed while building the Excel calculator —
do not regress it.
hitDice available to spend on a short rest = character level (one die of the
class's Hit Die size per level).
Spellcasting (only for classes with a Spellcasting Ability ≠ "None")
spellSaveDC = 8 + proficiencyBonus + spellcastingAbilityModifier
spellAttackBonus = proficiencyBonus + spellcastingAbilityModifier
Spellcasting ability per class is in seed-data/classes.json (Intelligence,
Wisdom, or Charisma). Classes with "None" have no spellcasting stats — the UI
should hide those fields entirely rather than show them as zero/blank.
Species Ability Score Increases & Speed
See seed-data/species.json for the full table (10 species). Note: Half-Elf gets
+2 Charisma fixed, plus +1 to two other abilities of the player's choice — the
seed data defaults that flexible pair to STR/INT, but the character builder UI
must let the player pick which two abilities get the +1 during Half-Elf
character creation (don't hardcode the default in the UI).
Class Quick Facts
See seed-data/classes.json for the full table (12 classes): Hit Die, Primary
Ability, the two Saving Throw proficiencies, and Spellcasting Ability.
Skill → Governing Ability
See seed-data/skills.json (18 skills).
"Quick Stats" View (combat-context panel)
When the frontend shows the contextual quick-stats panel during an attack action, surface exactly these derived values (all computed via the module above, never stored redundantly):
- Attack bonus for the equipped/selected weapon:
proficiencyBonus (if proficient) + relevantAbilityModifier(STR normally, DEX for finesse/ranged weapons — seeseed-data/weapons.json'sattackAbilityfield, which is"STR","DEX", or"FINESSE"meaning "use whichever of STR/DEX is higher") - Damage: weapon's damage dice + relevant ability modifier
- Current HP / Max HP / Temp HP
- AC
- Any special abilities/features flagged on the character (free-text list is fine for v1 — a
specialAbilitiestext/JSON field onCharacteror a simple related table)
This is a read view assembled from existing data — it does not need its own schema table.