import type { AbilityKey, CharacterRulesInput, DerivedSkill, ProficiencyRank, RestOption, RulesSystem, } from '../types'; import { ABILITY_KEYS } from '../types'; import { ABILITY_ABBR, 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)); return 2 + Math.floor((lvl - 1) / 4); } /** * In 5e a skill/save is either proficient or not, with "expert" meaning * expertise (double proficiency). We map PF2e-style ranks onto that. */ function profMultiplier(rank: ProficiencyRank): number { if (rank === 'untrained') return 0; if (rank === 'trained') return 1; return 2; // expert/master/legendary => expertise } export const dnd5e: RulesSystem = { id: '5e', label: 'D&D 5e', abilities: ABILITY_KEYS, abilityLabels: ABILITY_LABELS, skills: DND5E_SKILLS, abilityModifier, defaultAbilityScores, proficiencyValue(level, rank) { return proficiencyBonus(level) * profMultiplier(rank); }, initiativeModifier({ abilities }) { return abilityModifier(abilities.dex); }, skillModifiers(input: CharacterRulesInput): DerivedSkill[] { const pb = proficiencyBonus(input.level); return DND5E_SKILLS.map((s) => { const rank = input.skillRanks?.[s.key] ?? 'untrained'; return { key: s.key, label: s.label, ability: s.ability, modifier: abilityModifier(input.abilities[s.ability]) + pb * profMultiplier(rank), }; }); }, saveModifiers(input): Record { const pb = proficiencyBonus(input.level); const out = {} as Record; for (const a of ABILITY_KEYS) { const rank = input.saveRanks?.[a] ?? 'untrained'; out[a] = abilityModifier(input.abilities[a]) + (rank !== 'untrained' ? pb : 0); } return out; }, baseArmorClass(input) { // Unarmored AC. Heavy armor etc. is represented as armorBonus on top. return 10 + abilityModifier(input.abilities.dex) + (input.armorBonus ?? 0); }, 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 };