UNPKG

roleplayer

Version:

A library for building table top role playing game worlds, and managing campaigns in those worlds

381 lines (330 loc) 10.2 kB
import { Battle, HealthResourceTypeName, MovementSpeedResourceTypeName, PrimaryActionResourceTypeName, SecondaryActionResourceTypeName, type ResourceDefinition, type ResourceGeneration, } from "../.."; import type { ActionDefinition } from "../../core/action/action"; import type { CharacterResourceLossEffect } from "../../core/action/effect"; import type { Actor } from "../../core/actor/character"; import { defaultRoll, type Roll } from "../../core/dice/dice"; import { ItemEquipmentType, type EquipmentSlotDefinition } from "../../core/inventory/item"; import type { CharacterStatType as CharacterAttributeType, Clazz, ElementDefinition, LevelProgression, Ruleset, } from "../../core/ruleset/ruleset"; import { generateId } from "../../lib/generate-id"; const ArmorClassBase = 10; const MovementSpeedResource = { id: "00000000-0000-0000-0000-000000001000" as const, name: MovementSpeedResourceTypeName, defaultMax: 35, }; const HealthResource = { id: "00000000-0000-0000-0000-000000001001" as const, name: HealthResourceTypeName, defaultMax: 10, }; const PrimaryActionResource = { id: "00000000-0000-0000-0000-000000001002" as const, name: PrimaryActionResourceTypeName, defaultMax: 1, }; const SecondaryActionResource = { id: "00000000-0000-0000-0000-000000001003" as const, name: SecondaryActionResourceTypeName, defaultMax: 1, }; const InitiativeResource = { id: "00000000-0000-0000-0000-000000001006" as const, name: "Initiative", }; const SpellSlot1Resource = { id: "00000000-0000-0000-0000-000000001004" as const, name: "Spell slot 1", defaultMax: 0 }; const SpellSlot2Resource = { id: "00000000-0000-0000-0000-000000001005" as const, name: "Spell slot 2", defaultMax: 0 }; const StrengthStat = { id: "00000000-0000-0000-0000-000000002000" as const, name: "Strength", }; const IntelligenceStat = { id: "00000000-0000-0000-0000-000000002001" as const, name: "Intelligence", }; const WisdomStat = { id: "00000000-0000-0000-0000-000000002002" as const, name: "Wisdom", }; const CharismaStat = { id: "00000000-0000-0000-0000-000000002003" as const, name: "Charisma", }; const DexterityStat = { id: "00000000-0000-0000-0000-000000002004" as const, name: "Dexterity", }; const ConstitutionStat = { id: "00000000-0000-0000-0000-000000002005" as const, name: "Constitution", }; const DefenseStat = { id: "00000000-0000-0000-0000-000000002006" as const, name: "Defense", }; const ArmorClassStat = { id: "00000000-0000-0000-0000-000000001001" as const, name: "Armor class", }; const StatTypes: CharacterAttributeType[] = [ StrengthStat, IntelligenceStat, WisdomStat, CharismaStat, DexterityStat, ConstitutionStat, DefenseStat, ArmorClassStat, ]; const ResourceTypes: ResourceDefinition[] = [ MovementSpeedResource, HealthResource, PrimaryActionResource, SecondaryActionResource, SpellSlot1Resource, SpellSlot2Resource, InitiativeResource, ]; export class DnDRuleset implements Ruleset { roll: Roll; constructor(roll: Roll = defaultRoll) { this.roll = roll; } getActingOrder(actors: Actor[]): Actor[] { return actors.toSorted((a, b) => { const initiativeA = a.resources.find((s) => s.resourceTypeId === InitiativeResource.id); const initiativeB = b.resources.find((s) => s.resourceTypeId === InitiativeResource.id); if (!initiativeA) { throw new Error(`Character ${a.name} is missing initiative resource`); } if (!initiativeB) { throw new Error(`Character ${b.name} is missing initiative resource`); } return initiativeB.amount - initiativeA.amount; }); } getCurrentActorTurn(battle: Battle): Actor | undefined { const actorOrder = this.getActingOrder(battle.actors); const noCurrentActor = !battle.actorToAct; if (noCurrentActor) { return actorOrder[0]; } const currentActorIndex = actorOrder.findIndex((a) => { return a.id === battle.actorToAct!.id; }); // If we are at the end of the list, return the first actor return actorOrder[currentActorIndex + 1] || actorOrder[0]; } getLevelProgression(): LevelProgression[] { return [ { requiredXp: 0, id: generateId(), unlocksLevel: 1 }, { requiredXp: 50, id: generateId(), unlocksLevel: 2 }, { requiredXp: 100, id: generateId(), unlocksLevel: 3 }, { requiredXp: 200, id: generateId(), unlocksLevel: 4 }, { requiredXp: 400, id: generateId(), unlocksLevel: 5 }, ]; } getCharacterStatTypes(): CharacterAttributeType[] { return StatTypes; } getCharacterResourceTypes(): ResourceDefinition[] { return ResourceTypes; } getCharacterEquipmentSlots(): EquipmentSlotDefinition[] { return [ { id: "00000000-0000-0000-0000-000000003000" as const, name: "Main hand", eligibleEquipmentTypes: [ItemEquipmentType.OneHandWeapon, ItemEquipmentType.Shield], }, { id: "00000000-0000-0000-0000-000000003001" as const, name: "Off hand", eligibleEquipmentTypes: [ItemEquipmentType.OneHandWeapon, ItemEquipmentType.Shield], }, { id: "00000000-0000-0000-0000-000000003002" as const, name: "Chest", eligibleEquipmentTypes: [ItemEquipmentType.BodyArmor], }, { id: "00000000-0000-0000-0000-000000003003" as const, name: "Head", eligibleEquipmentTypes: [ItemEquipmentType.BodyArmor], }, ]; } getClassDefinitions(): Clazz[] { return [ { id: "00000000-0000-0000-0000-000000004000" as const, name: "Rogue", levelProgression: [ { actionDefinitionId: "00000000-0000-0000-0000-000000004100" as const, unlockedAtLevel: 1, }, ], }, { id: "00000000-0000-0000-0000-000000005000" as const, name: "Bard", levelProgression: [ { actionDefinitionId: "00000000-0000-0000-0000-000000005100" as const, unlockedAtLevel: 1, }, ], }, { id: "00000000-0000-0000-0000-000000006000" as const, name: "Wizard", levelProgression: [ { actionDefinitionId: "00000000-0000-0000-0000-000000006100" as const, unlockedAtLevel: 1, }, ], }, ]; } getElementDefinitions(): ElementDefinition[] { return [ { id: "00000000-0000-0000-0000-000000010000", name: "Cold", }, { id: "00000000-0000-0000-0000-000000020000", name: "Physical", }, { id: "00000000-0000-0000-0000-000000030000", name: "Fire", }, { id: "00000000-0000-0000-0000-000000040000", name: "Radiant", }, { id: "00000000-0000-0000-0000-000000050000", name: "Necrotic", }, { id: "00000000-0000-0000-0000-000000060000", name: "Thunder", }, ]; } characterAttributeTotal(character: Actor, attribute: CharacterAttributeType) { switch (attribute.id) { case ArmorClassStat.id: { const dexterityStat = character.stats.find((s) => s.statId == DexterityStat.id); if (!dexterityStat) { throw new Error("Character does not have a dexterity stat"); } return ArmorClassBase + getAbilityModifier(dexterityStat.amount); } } } characterHitDamage(source: Actor, action: ActionDefinition, target: Actor, effect: CharacterResourceLossEffect) { const elementTypes = this.getElementDefinitions(); const elementType = elementTypes.find((e) => e.id === effect.elementTypeId); if (!elementType) { throw new Error("Element type not found"); } const sourceDamageRoll = this.roll(effect.roll); const sourceDamage = sourceDamageRoll * source.getDamageAmplify(elementType); const targetDamageReduction = target.getResistance(elementType, sourceDamage); const totalDamage = sourceDamage - targetDamageReduction; return totalDamage; } characterHit(attacker: Actor, actionDef: ActionDefinition, defender: Actor) { const attackerHit = attacker.stats.find((s) => s.statId === "character-stats-hit"); if (!attackerHit) throw new Error("Character hit not found"); const defenderArmorClass = defender.stats.find((s) => s.statId === "character-armor-class"); if (!defenderArmorClass) throw new Error("Character hit not found"); return defaultRoll("D20+0") + attackerHit.amount > defenderArmorClass.amount; } characterElementDamageMultiplier(actor: Actor, damageType: ElementDefinition): number { return 1; } characterResistanceMultiplier(actor: Actor, damageType: ElementDefinition): number { return 1; } characterResistanceAbsolute(actor: Actor, damageType: ElementDefinition): number { return 1; } characterResourceGeneration(actor: Actor): ResourceGeneration[] { return [ { amount: 1, resourceTypeId: PrimaryActionResource.id, }, { amount: 35, resourceTypeId: MovementSpeedResource.id, }, ]; } characterIsDead(actor: Actor): boolean { const healthResource = this.getCharacterResourceTypes().find((r) => r.name === HealthResourceTypeName)!; const characterHealthResource = actor.resources.find((r) => r.resourceTypeId === healthResource.id); return characterHealthResource !== undefined && characterHealthResource!.amount <= 0; } characterBattleActionOrder(actor: Actor): number { return this.roll("D20+0"); } } export function getAbilityModifier(abilityScore: number, fallback = -1000) { const map: { [index: number]: number } = { 1: -5, 2: -4, 3: -4, 4: -3, 5: -3, 6: -2, 7: -2, 8: -1, 9: -1, 10: 0, 11: 0, 12: 1, 13: 1, 14: 2, 15: 2, 16: 3, 17: 3, 18: 4, 19: 4, 20: 5, 21: 5, 22: 6, 23: 6, 24: 7, 25: 7, 26: 8, 27: 8, 28: 9, 29: 9, 30: 10, }; const score = map[abilityScore]; if (!score) { return fallback; } return score; }