Phase 1: player character depth

- Inventory + currency + encumbrance vs carrying capacity
- Class resources with correct short/long/daily rest recovery (applyRest)
- Spellcasting: slots (+pact), known/prepared spells, derived DC + attack
- Defenses: 5e death saves/inspiration/exhaustion, pf2e dying/wounded/hero points
- Derived weapon attacks + passive perception via extended RulesSystem
- Dexie v2 migration backfills new fields; debounced save flushes on pagehide
- 9 new unit tests, new character-depth e2e; all green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:30:34 +02:00
parent 1a9e5e2c18
commit 9ecd817bc6
20 changed files with 1069 additions and 25 deletions
+31
View File
@@ -3,6 +3,7 @@ import type {
CharacterRulesInput,
DerivedSkill,
ProficiencyRank,
RestOption,
RulesSystem,
} from '../types';
import { ABILITY_KEYS } from '../types';
@@ -11,9 +12,15 @@ import {
ABILITY_LABELS,
abilityModifier,
defaultAbilityScores,
formatDamage,
} from '../abilities';
import { DND5E_SKILLS } from './skills';
const DND5E_RESTS: readonly RestOption[] = [
{ id: 'short', label: 'Short Rest', restoresHp: false, restoresSlots: false, restoresPact: true, recovers: ['short'] },
{ id: 'long', label: 'Long Rest', restoresHp: true, restoresSlots: true, restoresPact: true, recovers: ['short', 'long'], reduceExhaustion: true },
];
/** Proficiency bonus by character level (PHB table): +2 at 1, +6 at 17. */
export function proficiencyBonus(level: number): number {
const lvl = Math.max(1, Math.min(20, Math.floor(level) || 1));
@@ -79,6 +86,30 @@ export const dnd5e: RulesSystem = {
spellSaveDc(input, ability) {
return 8 + proficiencyBonus(input.level) + abilityModifier(input.abilities[ability]);
},
spellAttackBonus(input, ability) {
return proficiencyBonus(input.level) + abilityModifier(input.abilities[ability]);
},
passivePerception(input) {
const rank = input.skillRanks?.['perception'] ?? 'untrained';
return 10 + abilityModifier(input.abilities.wis) + proficiencyBonus(input.level) * profMultiplier(rank);
},
weaponAttack(input, weapon) {
const abilityMod = abilityModifier(input.abilities[weapon.ability]);
const item = weapon.itemBonus ?? 0;
const toHit = abilityMod + proficiencyBonus(input.level) * profMultiplier(weapon.rank) + item;
const dmgMod = (weapon.addAbilityToDamage !== false ? abilityMod : 0) + item;
return { toHit, damage: formatDamage(weapon.damageDice, dmgMod) };
},
carryingCapacity(input) {
const str = input.abilities.str;
return { encumbered: str * 5, max: str * 15, unit: 'lb' };
},
restOptions: DND5E_RESTS,
};
export { ABILITY_ABBR };