UNPKG

roleplayer

Version:

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

1,538 lines (1,519 loc) 52.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Actor: () => Actor, Alignment: () => Alignment, Battle: () => Battle, CampaignState: () => CampaignState, CharacterEventTypes: () => CharacterEventTypes, D10: () => D10, D2: () => D2, D20: () => D20, D4: () => D4, D6: () => D6, D8: () => D8, DnDRuleset: () => DnDRuleset, HealthResourceTypeName: () => HealthResourceTypeName, ItemEquipmentType: () => ItemEquipmentType, ItemSlot: () => ItemSlot, ItemType: () => ItemType, LeagueOfDungeoneersRuleset: () => LeagueOfDungeoneersRuleset, MovementSpeedResourceTypeName: () => MovementSpeedResourceTypeName, PrimaryActionResourceTypeName: () => PrimaryActionResourceTypeName, Rarity: () => Rarity, ReactionEventType: () => ReactionEventType, Roleplayer: () => Roleplayer, SecondaryActionResourceTypeName: () => SecondaryActionResourceTypeName, StatusDurationType: () => StatusDurationType, StatusType: () => StatusType, SystemEventType: () => SystemEventType, TargetType: () => TargetType, Version: () => Version, addActionToCharacter: () => addActionToCharacter, addCharacterEquipmentSlot: () => addCharacterEquipmentSlot, addCharacterItem: () => addCharacterItem, addCharacterToCurrentBattle: () => addCharacterToCurrentBattle, characterBattleEnter: () => characterBattleEnter, characterBattleLeave: () => characterBattleLeave, characterDespawn: () => characterDespawn, characterEquipItem: () => characterEquipItem, characterGainExperience: () => characterGainExperience, characterResourceGain: () => characterResourceGain, characterUnEquipItem: () => characterUnEquipItem, createCharacterWithDefaults: () => createCharacterWithDefaults, defaultRoll: () => defaultRoll, endBattle: () => endBattle, endCharacterTurn: () => endCharacterTurn, endRound: () => endRound, generateId: () => generateId, getAbilityModifier: () => getAbilityModifier, getRuleSet: () => getRuleSet, isBattleEvent: () => isBattleEvent, isCharacterEvent: () => isCharacterEvent, mapEffect: () => mapEffect, nextRound: () => nextRound, performCharacterAttack: () => performCharacterAttack, removeCharacterItem: () => removeCharacterItem, setCharacterClasses: () => setCharacterClasses, setCharacterName: () => setCharacterName, setCharacterStats: () => setCharacterStats, spawnCharacterFromTemplate: () => spawnCharacterFromTemplate, startBattle: () => startBattle, startCampaign: () => startCampaign }); module.exports = __toCommonJS(index_exports); // src/core/action/action.ts var TargetType = /* @__PURE__ */ ((TargetType2) => { TargetType2["Self"] = "Self"; TargetType2["Character"] = "Character"; TargetType2["Friendly"] = "Friendly"; TargetType2["Hostile"] = "Hostile"; TargetType2["Environment"] = "Environment"; return TargetType2; })(TargetType || {}); // src/core/action/effect.ts function mapEffect(effect, actionDef, attacker, target, ruleset) { switch (effect.eventType) { case "CharacterResourceLoss": { return instantiateResourceLossEffect(actionDef, effect, attacker, target, ruleset); } case "CharacterStatusGain": { return instantiateStatusGainEffect(actionDef, effect, attacker, target, ruleset); } default: throw new Error("Could not map effect event type"); } } function isCharacterResourceLossEffect(effect) { return effect.eventType === "CharacterResourceLoss"; } function instantiateResourceLossEffect(action, effect, source, target, ruleset) { if (!isCharacterResourceLossEffect(effect)) { throw new Error("Not CharacterResourceLoss effect type"); } return { type: "CharacterResourceLoss" /* CharacterResourceLoss */, amount: ruleset.characterHitDamage(source, action, target, effect), characterId: target.id, resourceTypeId: effect.resourceTypeId, actionId: action.id, sourceId: source.id }; } function isCharacterStatusGain(effect) { return effect.eventType === "CharacterStatusGain"; } function instantiateStatusGainEffect(action, effect, source, target, ruleset) { if (!isCharacterStatusGain(effect)) { throw new Error("Not CharacterStatusGain effect type"); } return { type: "CharacterStatusGain" /* CharacterStatusGain */, characterId: target.id, actionId: action.id, sourceId: source.id, statusId: effect.statusId }; } // src/core/action/status.ts var StatusType = /* @__PURE__ */ ((StatusType2) => { StatusType2["Curse"] = "Curse"; StatusType2["Poison"] = "Poison"; StatusType2["Magic"] = "Magic"; StatusType2["Physical"] = "Physical"; return StatusType2; })(StatusType || {}); var StatusDurationType = /* @__PURE__ */ ((StatusDurationType2) => { StatusDurationType2["Permanent"] = "Permanent"; StatusDurationType2["UntilLongRest"] = "UntilLongRest"; StatusDurationType2["UntilShortRest"] = "UntilShortRest"; StatusDurationType2["NumberOfRounds"] = "NumberOfRounds"; return StatusDurationType2; })(StatusDurationType || {}); // src/lib/generate-id.ts var import_uuid = require("uuid"); function generateId() { return (0, import_uuid.v7)(); } // src/core/events/events.ts var CharacterEventTypes = /* @__PURE__ */ ((CharacterEventTypes2) => { CharacterEventTypes2["CharacterSpawned"] = "CharacterSpawned"; CharacterEventTypes2["CharacterNameSet"] = "CharacterNameSet"; CharacterEventTypes2["CharacterResourceMaxSet"] = "CharacterResourceMaxSet"; CharacterEventTypes2["CharacterResourceGain"] = "CharacterResourceGain"; CharacterEventTypes2["CharacterResourceLoss"] = "CharacterResourceLoss"; CharacterEventTypes2["CharacterStatChange"] = "CharacterStatChange"; CharacterEventTypes2["CharacterExperienceChanged"] = "CharacterExperienceChanged"; CharacterEventTypes2["CharacterExperienceSet"] = "CharacterExperienceSet"; CharacterEventTypes2["CharacterDespawn"] = "CharacterDespawn"; CharacterEventTypes2["CharacterMovement"] = "CharacterMovement"; CharacterEventTypes2["CharacterEndTurn"] = "CharacterEndTurn"; CharacterEventTypes2["CharacterActionGain"] = "CharacterActionGain"; CharacterEventTypes2["CharacterEquipmentSlotGain"] = "CharacterEquipmentSlotGain"; CharacterEventTypes2["CharacterInventoryItemGain"] = "CharacterInventoryItemGain"; CharacterEventTypes2["CharacterInventoryItemLoss"] = "CharacterInventoryItemLoss"; CharacterEventTypes2["CharacterInventoryItemEquip"] = "CharacterInventoryItemEquip"; CharacterEventTypes2["CharacterInventoryItemUnEquip"] = "CharacterInventoryItemUnEquip"; CharacterEventTypes2["CharacterPositionSet"] = "CharacterPositionSet"; CharacterEventTypes2["CharacterStatusGain"] = "CharacterStatusGain"; CharacterEventTypes2["CharacterAttackAttackerHit"] = "CharacterAttackAttackerHit"; CharacterEventTypes2["CharacterAttackAttackerMiss"] = "CharacterAttackAttackerMiss"; CharacterEventTypes2["CharacterAttackDefenderHit"] = "CharacterAttackDefenderHit"; CharacterEventTypes2["CharacterAttackDefenderDodge"] = "CharacterAttackDefenderDodge"; CharacterEventTypes2["CharacterAttackDefenderParry"] = "CharacterAttackDefenderParry"; CharacterEventTypes2["CharacterClassReset"] = "CharacterClassReset"; CharacterEventTypes2["CharacterClassLevelGain"] = "CharacterClassLevelGain"; return CharacterEventTypes2; })(CharacterEventTypes || {}); var SystemEventType = /* @__PURE__ */ ((SystemEventType2) => { SystemEventType2["Unknown"] = "Unknown"; SystemEventType2["CampaignStarted"] = "CampaignStarted"; SystemEventType2["RoundStarted"] = "RoundStarted"; SystemEventType2["RoundEnded"] = "RoundEnded"; SystemEventType2["BattleStarted"] = "BattleStarted"; SystemEventType2["BattleEnded"] = "BattleEnded"; SystemEventType2["CharacterBattleEnter"] = "CharacterBattleEnter"; SystemEventType2["CharacterBattleLeave"] = "CharacterBattleLeave"; return SystemEventType2; })(SystemEventType || {}); function isBattleEvent(event) { return "battleId" in event && event.battleId != null; } // src/core/world/resource.ts var MovementSpeedResourceTypeName = "Movement speed"; var HealthResourceTypeName = "Health"; var PrimaryActionResourceTypeName = "Primary action"; var SecondaryActionResourceTypeName = "Secondary action"; // src/core/actions.ts function startCampaign() { return (dispatch, getState) => { if (getState().campaign.rounds.length > 0) { console.warn("Campaign already started"); return; } const campaignStartEvents = [ { type: "CampaignStarted" /* CampaignStarted */ }, { type: "RoundStarted" /* RoundStarted */, roundId: generateId() } ]; dispatch(...campaignStartEvents); }; } function addCharacterItem(characterId, itemDefinitionId) { return (dispatch, getState) => { const actionGain = { characterId, itemDefinitionId, type: "CharacterInventoryItemGain" /* CharacterInventoryItemGain */, itemInstanceId: generateId() }; dispatch(actionGain); }; } function removeCharacterItem(characterId, characterInventoryItemId) { return (dispatch, getState) => { const actionGain = { characterId, characterInventoryItemId, type: "CharacterInventoryItemLoss" /* CharacterInventoryItemLoss */ }; dispatch(actionGain); }; } function addCharacterEquipmentSlot(characterId, equipmentSlotId) { return (dispatch, getState) => { const equipEvent = { characterId, equipmentSlotId, type: "CharacterEquipmentSlotGain" /* CharacterEquipmentSlotGain */ }; dispatch(equipEvent); }; } function characterUnEquipItem(characterId, equipmentSlotId, itemId) { return (dispatch, getState) => { const equipEvent = { characterId, equipmentSlotId, itemId, type: "CharacterInventoryItemUnEquip" /* CharacterInventoryItemUnEquip */ }; dispatch(equipEvent); }; } function characterEquipItem(characterId, equipmentSlotId, itemId) { return (dispatch, getState) => { const equipEvent = { type: "CharacterInventoryItemEquip" /* CharacterInventoryItemEquip */, characterId, itemId, equipmentSlotId }; dispatch(equipEvent); }; } function addActionToCharacter(characterId, actionId) { return (dispatch, getState) => { const actionGain = { type: "CharacterActionGain" /* CharacterActionGain */, characterId, actionId }; dispatch(actionGain); }; } function setCharacterStats(characterId, stats) { return (dispatch, getState) => { const statsEvents = stats.map((st) => ({ type: "CharacterStatChange" /* CharacterStatChange */, amount: st.amount, characterId, statId: st.statId })); dispatch(...statsEvents); }; } function characterResourceGain(characterId, resourceTypeId, amount) { return (dispatch, getState) => { const event = { type: "CharacterResourceGain" /* CharacterResourceGain */, amount, characterId, resourceTypeId }; dispatch(event); }; } function setCharacterClasses(characterId, classes) { return (dispatch, getState) => { const classResetEvent = { type: "CharacterClassReset" /* CharacterClassReset */, characterId }; const classUpdates = classes.map((c) => ({ type: "CharacterClassLevelGain" /* CharacterClassLevelGain */, characterId, classId: c.classId })); dispatch(...[classResetEvent, ...classUpdates]); }; } function setCharacterName(characterId, name) { return (dispatch, getState) => { const characterUpdate = { type: "CharacterNameSet" /* CharacterNameSet */, characterId, name }; dispatch(characterUpdate); }; } function characterGainExperience(characterId, experience) { return (dispatch, getState) => { const characterUpdate = { type: "CharacterExperienceChanged" /* CharacterExperienceChanged */, characterId, experience }; dispatch(characterUpdate); }; } function addCharacterToCurrentBattle(characterId) { return (dispatch, getState) => { const actor = getState().campaign.characters.find((c) => c.id === characterId); if (!actor) { throw new Error("Actor not found"); } const currentBattle = getState().campaign.getCurrentBattle(); if (!currentBattle) { throw new Error("No current battle"); } const characterBattleEnter2 = [ { type: "CharacterBattleEnter" /* CharacterBattleEnter */, characterId, battleId: currentBattle.id } ]; dispatch(...characterBattleEnter2); }; } function spawnCharacterFromTemplate(templateId) { const action = (dispatch, getState) => { const template = getState().campaign.actorTemplates.find((c) => c.id === templateId); if (!template) { throw new Error("Template character not found"); } const characterId = generateId(); const characterSpawnEvents = [ { type: "CharacterSpawned" /* CharacterSpawned */, characterId, templateCharacterId: templateId } ]; dispatch(...characterSpawnEvents); return characterId; }; return action; } function characterDespawn(actorId) { return (dispatch, getState) => { const characterDespawnEvent = { type: "CharacterDespawn" /* CharacterDespawn */, characterId: actorId }; dispatch(characterDespawnEvent); }; } function characterBattleEnter(actorId, battleId) { return (dispatch, getState) => { const characterBattleEnter2 = { type: "CharacterBattleEnter" /* CharacterBattleEnter */, characterId: actorId, battleId }; dispatch(characterBattleEnter2); }; } function characterBattleLeave(actorId, battleId) { return (dispatch, getState) => { const characterBattleLeave2 = { type: "CharacterBattleLeave" /* CharacterBattleLeave */, characterId: actorId, battleId }; dispatch(characterBattleLeave2); }; } function createCharacterWithDefaults(characterId, name) { return (dispatch, getState) => { const defaultResourcesEvents = getState().ruleset.getCharacterResourceTypes().map((cr) => ({ type: "CharacterResourceMaxSet" /* CharacterResourceMaxSet */, max: cr.defaultMax || 0, characterId, resourceTypeId: cr.id })); const resourcesGainEvents = getState().ruleset.getCharacterResourceTypes().map((cr) => ({ type: "CharacterResourceGain" /* CharacterResourceGain */, amount: cr.defaultMax || 0, characterId, resourceTypeId: cr.id })); const defaultStatsEvents = getState().ruleset.getCharacterStatTypes().map((st) => ({ type: "CharacterStatChange" /* CharacterStatChange */, amount: 0, characterId, statId: st.id })); const defaultEquipmentSlotEvents = getState().ruleset.getCharacterEquipmentSlots().map((es) => ({ type: "CharacterEquipmentSlotGain" /* CharacterEquipmentSlotGain */, characterId, equipmentSlotId: es.id })); const characterSpawnEvents = [ { type: "CharacterSpawned" /* CharacterSpawned */, characterId }, { type: "CharacterNameSet" /* CharacterNameSet */, name, characterId }, { type: "CharacterExperienceSet" /* CharacterExperienceSet */, experience: 0, characterId }, ...defaultEquipmentSlotEvents, ...defaultResourcesEvents, ...defaultStatsEvents, ...resourcesGainEvents ]; dispatch(...characterSpawnEvents); }; } function nextRound(battleId) { return (dispatch, getState) => { const newRoundId = generateId(); const events = [ { type: "RoundStarted" /* RoundStarted */, roundId: newRoundId, battleId } ]; dispatch(...events); return newRoundId; }; } function endRound() { return (dispatch, getState) => { const events = [ { type: "RoundEnded" /* RoundEnded */ } ]; dispatch(...events); }; } function startBattle() { return (dispatch, getState) => { dispatch({ type: "BattleStarted" /* BattleStarted */, battleId: generateId() }); }; } function endBattle(battleId) { return (dispatch, getState) => { dispatch({ type: "BattleEnded" /* BattleEnded */, battleId }); }; } function performCharacterAttack(attacker, actionDef, targets) { return (dispatch, getState) => { const characterAction = attacker.action(actionDef); const characterActionHitRoll = characterAction.rolls.find((r) => r.name === "Hit"); if (!characterActionHitRoll) { throw new Error("Hit roll not defined for character action"); } const healthResource = getState().ruleset.getCharacterResourceTypes().find((rt) => rt.name === HealthResourceTypeName); if (!healthResource) { throw new Error("Health resource not defined in world, cannot perform attack"); } const targetReceiveAttackEvents = targets.flatMap((target) => { if (attacker.tryHit(target)) { return actionDef.appliesEffects.map( (effect) => mapEffect(effect, actionDef, attacker, target, getState().ruleset) ); } return []; }); const characterResourceLoss = actionDef.requiresResources.map((rr) => ({ type: "CharacterResourceLoss" /* CharacterResourceLoss */, characterId: attacker.id, resourceTypeId: rr.resourceTypeId, amount: rr.amount })); return dispatch(...characterResourceLoss, ...targetReceiveAttackEvents); }; } function endCharacterTurn(actor) { return (dispatch, getState) => { const currentBattle = getState().campaign.getCurrentBattle(); if (!currentBattle) { throw new Error("No current battle"); } dispatch({ type: "CharacterEndTurn" /* CharacterEndTurn */, characterId: actor.id, battleId: currentBattle.id }); }; } // src/core/inventory/item.ts var ItemType = /* @__PURE__ */ ((ItemType2) => { ItemType2["Equipment"] = "Equipment"; ItemType2["Consumable"] = "Consumable"; return ItemType2; })(ItemType || {}); var ItemEquipmentType = /* @__PURE__ */ ((ItemEquipmentType2) => { ItemEquipmentType2["None"] = "None"; ItemEquipmentType2["Shield"] = "Shield"; ItemEquipmentType2["OneHandWeapon"] = "OneHandWeapon"; ItemEquipmentType2["TwoHandWeapon"] = "TwoHandWeapon"; ItemEquipmentType2["BodyArmor"] = "BodyArmor"; return ItemEquipmentType2; })(ItemEquipmentType || {}); var ItemSlot = /* @__PURE__ */ ((ItemSlot2) => { ItemSlot2["Inventory"] = "Inventory"; ItemSlot2["MainHand"] = "MainHand"; ItemSlot2["OffHand"] = "OffHand"; ItemSlot2["Head"] = "Head"; ItemSlot2["Feet"] = "Feet"; ItemSlot2["Chest"] = "Chest"; return ItemSlot2; })(ItemSlot || {}); // src/core/actor/character.ts var ReactionEventType = /* @__PURE__ */ ((ReactionEventType2) => { ReactionEventType2["OnHit"] = "OnHit"; ReactionEventType2["OnDodge"] = "OnDodge"; return ReactionEventType2; })(ReactionEventType || {}); function isCharacterEvent(event) { return event.characterId !== void 0; } var Actor = class { id; party; exists; templateCharacterId; characterType; xp; name; race; description; alignment; classes = []; inventory = []; equipment = []; stats = []; actions = []; statuses = []; position; resources = []; reactionsRemaining = []; reactions = []; campaign; unsubscribe; constructor(a) { Object.assign(this, a); this.unsubscribe = a.campaign.roleplayer.subscribe(this.reduce.bind(this)); } resetResources(generation) { this.resources = this.resources.map((resource) => { const resourceGeneration = generation.find((g) => g.resourceTypeId === resource.resourceTypeId); if (!resourceGeneration) { return resource; } return { ...resource, amount: Math.min(resource.max, resource.amount + resourceGeneration.amount) }; }); } action(actionDefinition) { return { rolls: [ { name: "Hit", roll: 20 } ] }; } /** * Get all available actions for the characters, based on equipment, class, spells, etc. * @returns A list of available actions */ getAvailableActions() { const consumables = this.inventory.filter((it) => it.definition.type === "Consumable" /* Consumable */).flatMap((i) => i.definition.actions); const equipmentActions = this.equipment.flatMap((eq) => eq.item).filter((i) => i).flatMap((i) => i.definition.actions); return [...this.actions, ...equipmentActions, ...consumables]; } getDamageAmplify(elementType) { return 1; } getResistance(element, damage) { return 0; } tryHit(target) { return true; } reduce(event) { if (!("characterId" in event) || event.characterId !== this.id) return; switch (event.type) { case "CharacterExperienceChanged" /* CharacterExperienceChanged */: { this.xp += event.experience; break; } case "CharacterExperienceSet" /* CharacterExperienceSet */: { this.xp = event.experience; break; } case "CharacterResourceMaxSet" /* CharacterResourceMaxSet */: { const resource = this.resources.find((r) => r.resourceTypeId === event.resourceTypeId); if (!resource) { this.resources.push({ amount: 0, max: event.max, min: 0, resourceTypeId: event.resourceTypeId }); break; } resource.max = event.max; break; } case "CharacterResourceGain" /* CharacterResourceGain */: { const resource = this.resources.find((r) => r.resourceTypeId === event.resourceTypeId); if (!resource) { throw new Error(`Cannot find character resource type ${event.resourceTypeId}`); } resource.amount += event.amount; break; } case "CharacterResourceLoss" /* CharacterResourceLoss */: { const resource = this.resources.find((r) => r.resourceTypeId === event.resourceTypeId); if (!resource) { throw new Error(`Cannot find character resource type ${event.resourceTypeId}`); } resource.amount -= event.amount; break; } case "CharacterNameSet" /* CharacterNameSet */: { this.name = event.name; break; } case "CharacterStatChange" /* CharacterStatChange */: { const stat = this.stats.find((s) => s.statId === event.statId); if (!stat) { this.stats.push({ statId: event.statId, amount: event.amount }); break; } stat.amount = event.amount; break; } case "CharacterPositionSet" /* CharacterPositionSet */: { if (!event.targetPosition) { throw new Error("Target position not defined for CharacterPositionSet"); } this.position = event.targetPosition; break; } case "CharacterAttackAttackerHit" /* CharacterAttackAttackerHit */: { break; } case "CharacterAttackAttackerMiss" /* CharacterAttackAttackerMiss */: { break; } case "CharacterAttackDefenderHit" /* CharacterAttackDefenderHit */: { break; } case "CharacterAttackDefenderDodge" /* CharacterAttackDefenderDodge */: { const availableReactions = this.reactions.filter((r) => r.eventType === "CharacterAttackDefenderDodge"); const reactionResources = availableReactions.map((r) => ({ reactionId: r.id, targetId: event.attackerId })); this.reactionsRemaining.push(...reactionResources); break; } case "CharacterAttackDefenderParry" /* CharacterAttackDefenderParry */: { break; } case "CharacterClassReset" /* CharacterClassReset */: { this.classes = []; break; } case "CharacterClassLevelGain" /* CharacterClassLevelGain */: { const characterClassLevels = this.classes.length; const characterLevel = 0; if (characterClassLevels >= characterLevel) { throw new Error("Cannot add class levels to character, character level not high enough"); } const clazz = this.campaign.classes.find((c) => c.id === event.classId); if (!clazz) { throw new Error("Class not found"); } const characterClass = this.classes.find((c) => c.classId === event.classId); if (!characterClass) { this.classes.push({ classId: event.classId, level: 1 }); break; } characterClass.level = characterClass.level + 1; break; } case "CharacterStatusGain" /* CharacterStatusGain */: { const status = this.campaign.statuses.find((s) => s.id === event.statusId); if (!status) { throw new Error(`Could not find status with id ${event.statusId} for CharacterStatusGain`); } this.statuses.push(status); break; } case "CharacterInventoryItemLoss" /* CharacterInventoryItemLoss */: { this.equipment = this.equipment.map((eq) => { const equipped = eq.item.id === event.characterInventoryItemId; if (equipped) { eq.item = void 0; } return eq; }); this.inventory = this.inventory.filter((eq) => eq.id === event.characterInventoryItemId); break; } case "CharacterInventoryItemGain" /* CharacterInventoryItemGain */: { const item = this.campaign.itemTemplates.find((eq) => eq.id === event.itemDefinitionId); if (!item) { throw new Error(`Could not find item with id ${event.itemDefinitionId} for CharacterGainItem`); } this.inventory.push({ id: event.itemInstanceId, definition: item }); break; } case "CharacterEquipmentSlotGain" /* CharacterEquipmentSlotGain */: { const characterSlot = this.campaign.ruleset.getCharacterEquipmentSlots().find((slot) => slot.id === event.equipmentSlotId); if (!characterSlot) { throw new Error("Cannot find slot"); } const existing = this.equipment.find((eq) => eq.slotId === event.equipmentSlotId); if (existing) { throw new Error("Character already have equipment slot"); } this.equipment.push({ slotId: characterSlot.id, item: void 0 }); break; } case "CharacterInventoryItemEquip" /* CharacterInventoryItemEquip */: { const characterHasItem = this.inventory.find((eq) => eq.id === event.itemId); if (!characterHasItem) { throw new Error(`Could not find item on character`); } const slot = this.equipment.find((e) => e.slotId === event.equipmentSlotId); if (!slot) { throw new Error(`Could not find slot on event`); } if (slot.item) { throw new Error("Slot already has item"); } slot.item = characterHasItem; break; } case "CharacterInventoryItemUnEquip" /* CharacterInventoryItemUnEquip */: { const slot = this.equipment.find((e) => e.slotId === event.equipmentSlotId); if (!slot) { throw new Error(`Could not find slot on event`); } slot.item = void 0; break; } case "CharacterMovement" /* CharacterMovement */: { const resourceType = this.campaign.ruleset.getCharacterResourceTypes().find((r) => r.name === MovementSpeedResourceTypeName); if (!resourceType) { throw new Error("Movement resource not defined in world"); } const characterMovementResource = this.resources.find((r) => r.resourceTypeId === resourceType.id); if (!characterMovementResource) { throw new Error("Character does not have a defined movement speed resource"); } if (!event.targetPosition) { throw new Error("Target position not defined for CharacterMovement"); } const distanceX = this.position.x + event.targetPosition.x; const distanceY = this.position.y + event.targetPosition.y; const distance = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2)); if (distance > characterMovementResource.amount) { throw new Error("Movement exceeds remaining speed for CharacterMovement"); } characterMovementResource.amount -= distance; this.position.x = event.targetPosition.x; this.position.y = event.targetPosition.y; break; } case "CharacterActionGain" /* CharacterActionGain */: { const action = this.campaign.actions.find((a) => a.id === event.actionId); if (!action) { throw new Error(`Unknown action ${event.actionId}`); } this.actions.push(action); return; } } } dispose() { this.unsubscribe?.(); } }; // src/core/battle/battle.ts var Battle = class { id; name; roleplayer; ruleset; actors = []; actorToAct; actorsThatHaveActed = []; unsubscribe; constructor(b) { Object.assign(this, b); this.ruleset = b.roleplayer.ruleset; this.unsubscribe = b.roleplayer.subscribe(this.reduce.bind(this)); } isBattleOver() { const aliveCharacters = this.actors.filter((actor) => this.ruleset.characterIsDead(actor)); const oneCharacterRemaining = aliveCharacters.length === 1; if (oneCharacterRemaining) { return true; } if (aliveCharacters.length === 0) { return false; } const party = aliveCharacters[0].party; const alliedPartiesRemaining = aliveCharacters.every((actor) => actor.party === party && party !== void 0); return alliedPartiesRemaining; } addBattleActor(actor) { this.actors.push(actor); } getActingOrder() { return this.ruleset.getActingOrder(this.actors); } removeBattleActor(actor) { this.actors = this.actors.filter((a) => a.id !== actor.id); } calculateTargetsCone(position, range, radius, angle) { return []; } calculateTargetsLine(position, range, radius, angle) { return []; } calculateTargetsCircle(position, range, radius, angle) { return []; } performAction(actor, actionDef, targets) { const events = []; for (const target of targets) { const didHit = this.ruleset.characterHit(actor, actionDef, target); if (!didHit) continue; for (const effect of actionDef.appliesEffects) { events.push(mapEffect(effect, actionDef, actor, target, this.ruleset)); } } for (const requiredResource of actionDef.requiresResources) { const availableResource = actor.resources.find((r) => r.resourceTypeId === requiredResource.resourceTypeId); if (requiredResource.amount > availableResource.amount) throw new Error("Not enough resources"); events.push({ type: "CharacterResourceLoss" /* CharacterResourceLoss */, amount: requiredResource.amount, characterId: actor.id, // Target resourceTypeId: requiredResource.resourceTypeId, actionId: actionDef.id, sourceId: actor.id // Actor }); } this.roleplayer.dispatchEvents(...events); return true; } reduce(event) { if (!("battleId" in event) || event.battleId !== this.id) return; switch (event.type) { case "CharacterBattleEnter" /* CharacterBattleEnter */: { const character = this.roleplayer.campaign.characters.find((c) => c.id === event.characterId); if (!character) throw new Error(`Cannot find character with id: ${event.characterId}`); this.addBattleActor(character); break; } case "CharacterBattleLeave" /* CharacterBattleLeave */: { const character = this.roleplayer.campaign.characters.find((c) => c.id === event.characterId); if (!character) throw new Error(`Cannot find character with id: ${event.characterId}`); this.removeBattleActor(character); break; } case "CharacterEndTurn" /* CharacterEndTurn */: { const characterBattle = this.actors.find((actor) => actor.id === event.characterId); if (!characterBattle) { throw new Error("Cannot find battle character"); } this.actorsThatHaveActed.push(characterBattle); this.actorToAct = this.ruleset.getCurrentActorTurn(this); break; } } } dispose() { this.unsubscribe?.(); } }; // src/core/campaign/campaign-state.ts var CampaignState = class { id; ruleset; roleplayer; started = false; battles = []; rounds = []; characters = []; itemTemplates = []; actorTemplates = []; races = []; actions = []; statuses = []; classes = []; constructor(c) { Object.assign(this, c); c.roleplayer.subscribe(this.reduce.bind(this)); } getRoundEvents(round) { return this.roleplayer.events.filter((e) => e.roundId === round.id); } getCharacterRoundEvents(round, characterId) { const roundEvents = this.getRoundEvents(round); return roundEvents.filter((re) => isCharacterEvent(re) && re.characterId === characterId); } getCharacterEvents(characterId) { return this.roleplayer.events.filter((re) => isCharacterEvent(re) && re.characterId === characterId); } getCharacterEligibleTargets(actor, action) { return this.characters; } getCurrentBattle() { return this.battles[this.battles.length - 1]; } characterHasResource(actor, resourceType) { return actor.resources.some((r) => r.resourceTypeId === resourceType && r.amount > 0); } getCurrentRound() { const round = this.rounds.toSorted((a, b) => a.roundNumber - b.roundNumber)[this.rounds.length - 1]; if (!round) { const newRound = { id: generateId(), roundNumber: 0 }; this.rounds.push(newRound); return newRound; } return round; } allCharactersHaveActed(events) { const round = this.rounds[this.rounds.length - 1]; if (!round) { throw new Error("Could not get current round"); } return this.characters.every( (c) => events.some((e) => e.type === "CharacterEndTurn" && e.characterId === c.id && e.roundId === round.id) ); } reduce(event) { switch (event.type) { case "BattleStarted": { this.battles.push( new Battle({ id: event.battleId, name: "Battle", roleplayer: this.roleplayer }) ); break; } case "BattleEnded": { const battleIndex = this.battles.findIndex((b) => b.id === event.battleId); if (battleIndex === -1) throw new Error(`Could not find battle with id: ${event.battleId}`); const [removedBattle] = this.battles.splice(battleIndex, 1); removedBattle?.dispose(); break; } } } }; // src/core/dice/dice.ts var D2 = 2; var D4 = 4; var D6 = 6; var D8 = 8; var D10 = 10; var D20 = 20; var defaultRoll = (dice) => { return 0; }; // src/lib/events/observable.ts var Observable = class { observers = /* @__PURE__ */ new Set(); subscribe(func) { this.observers.add(func); return () => this.unsubscribe(func); } unsubscribe(func) { this.observers.delete(func); } notify(event) { for (const observer of this.observers) { observer(event); } } }; // src/core/roleplayer.ts var Roleplayer = class extends Observable { ruleset; events = []; campaign; logger; constructor(config, initialCampaignConfig) { super(); Object.assign(this, config); this.subscribe(this.reduce.bind(this)); this.campaign = new CampaignState({ ...initialCampaignConfig, roleplayer: this, ruleset: config.ruleset }); } setLogger(logger) { this.logger = logger; } nextSerialNumber() { const sortedEvents = this.events.toSorted((a, b) => a.serialNumber - b.serialNumber); const lastSerialNumber = sortedEvents[sortedEvents.length - 1]?.serialNumber ?? 0; return lastSerialNumber + 1; } dispatchAction(action) { return action(this.dispatchEvents.bind(this), () => this); } dispatchEvents(...events) { for (const event of events) { this.dispatch(event); } } dispatch(event) { const eventSerialNumber = this.nextSerialNumber(); const currentRoundId = event.type === "RoundStarted" /* RoundStarted */ ? event.roundId : this.campaign.getCurrentRound().id; let roleplayerEvent; if (isBattleEvent(event)) { roleplayerEvent = { ...event, id: generateId(), battleId: event.battleId, roundId: currentRoundId, serialNumber: eventSerialNumber }; } else { const currentBattleId = this.campaign.getCurrentBattle()?.id; roleplayerEvent = { ...event, id: generateId(), battleId: currentBattleId, roundId: currentRoundId, serialNumber: eventSerialNumber }; } this.events.push(roleplayerEvent); this.notify(roleplayerEvent); } reduce(event) { switch (event.type) { case "CampaignStarted" /* CampaignStarted */: { if (this.campaign.started) { throw new Error("Campaign already started"); } this.campaign.started = true; break; } case "RoundStarted" /* RoundStarted */: { this.campaign.rounds.push({ id: event.roundId, roundNumber: event.serialNumber }); for (const character of this.campaign.characters) { const characterResourceGeneration = this.ruleset.characterResourceGeneration(character); character.resetResources(characterResourceGeneration); } break; } case "CharacterSpawned" /* CharacterSpawned */: { const templateCharacter = this.campaign.actorTemplates.find((c) => c.id === event.templateCharacterId); const alreadySpawnedTemplateCharacters = this.campaign.characters.filter( (c) => c.templateCharacterId === event.templateCharacterId ); if (!templateCharacter) { this.campaign.characters.push(new Actor({ id: event.characterId, campaign: this.campaign })); } else { this.campaign.characters.push( new Actor({ ...structuredClone(templateCharacter), campaign: this.campaign, id: event.characterId, templateCharacterId: event.templateCharacterId, name: `${templateCharacter.name} #${alreadySpawnedTemplateCharacters.length}` }) ); } break; } case "CharacterDespawn" /* CharacterDespawn */: { const characterIndex = this.campaign.characters.findIndex((c) => c.id === event.characterId); if (characterIndex === -1) throw new Error(`Could not find character with id: ${event.characterId}`); const [removedCharacter] = this.campaign.characters.splice(characterIndex, 1); for (const battle of this.campaign.battles) { battle.actors = battle.actors.filter((actor) => actor.id !== event.characterId); } removedCharacter?.dispose(); break; } default: this.logger?.warn(`Unknown event type ${event.type}`); } } debugEvent(event) { } debugEventProcessing(events) { } }; // src/core/ruleset/ruleset.ts var Alignment = /* @__PURE__ */ ((Alignment2) => { Alignment2["NeutralEvil"] = "NeutralEvil"; Alignment2["LawfulEvil"] = "LawfulEvil"; Alignment2["ChaoticEvil"] = "ChaoticEvil"; Alignment2["NeutralNeutral"] = "NeutralNeutral"; Alignment2["EvilNeutral"] = "EvilNeutral"; Alignment2["GoodNeutral"] = "GoodNeutral"; Alignment2["NeutralGood"] = "NeutralGood"; Alignment2["LawfulGood"] = "LawfulGood"; Alignment2["ChaoticGood"] = "ChaoticGood"; return Alignment2; })(Alignment || {}); // src/core/world/rarity.ts var Rarity = /* @__PURE__ */ ((Rarity2) => { Rarity2["Common"] = "Common"; Rarity2["Uncommon"] = "Uncommon"; Rarity2["Rare"] = "Rare"; Rarity2["Epic"] = "Epic"; Rarity2["Legendary"] = "Legendary"; Rarity2["Artifact"] = "Artifact"; return Rarity2; })(Rarity || {}); // src/data/rulesets/dnd-5th.ts var ArmorClassBase = 10; var MovementSpeedResource = { id: "00000000-0000-0000-0000-000000001000", name: MovementSpeedResourceTypeName, defaultMax: 35 }; var HealthResource = { id: "00000000-0000-0000-0000-000000001001", name: HealthResourceTypeName, defaultMax: 10 }; var PrimaryActionResource = { id: "00000000-0000-0000-0000-000000001002", name: PrimaryActionResourceTypeName, defaultMax: 1 }; var SecondaryActionResource = { id: "00000000-0000-0000-0000-000000001003", name: SecondaryActionResourceTypeName, defaultMax: 1 }; var InitiativeResource = { id: "00000000-0000-0000-0000-000000001006", name: "Initiative" }; var SpellSlot1Resource = { id: "00000000-0000-0000-0000-000000001004", name: "Spell slot 1", defaultMax: 0 }; var SpellSlot2Resource = { id: "00000000-0000-0000-0000-000000001005", name: "Spell slot 2", defaultMax: 0 }; var StrengthStat = { id: "00000000-0000-0000-0000-000000002000", name: "Strength" }; var IntelligenceStat = { id: "00000000-0000-0000-0000-000000002001", name: "Intelligence" }; var WisdomStat = { id: "00000000-0000-0000-0000-000000002002", name: "Wisdom" }; var CharismaStat = { id: "00000000-0000-0000-0000-000000002003", name: "Charisma" }; var DexterityStat = { id: "00000000-0000-0000-0000-000000002004", name: "Dexterity" }; var ConstitutionStat = { id: "00000000-0000-0000-0000-000000002005", name: "Constitution" }; var DefenseStat = { id: "00000000-0000-0000-0000-000000002006", name: "Defense" }; var ArmorClassStat = { id: "00000000-0000-0000-0000-000000001001", name: "Armor class" }; var StatTypes = [ StrengthStat, IntelligenceStat, WisdomStat, CharismaStat, DexterityStat, ConstitutionStat, DefenseStat, ArmorClassStat ]; var ResourceTypes = [ MovementSpeedResource, HealthResource, PrimaryActionResource, SecondaryActionResource, SpellSlot1Resource, SpellSlot2Resource, InitiativeResource ]; var DnDRuleset = class { roll; constructor(roll = defaultRoll) { this.roll = roll; } getActingOrder(actors) { 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) { 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; }); return actorOrder[currentActorIndex + 1] || actorOrder[0]; } getLevelProgression() { 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() { return StatTypes; } getCharacterResourceTypes() { return ResourceTypes; } getCharacterEquipmentSlots() { return [ { id: "00000000-0000-0000-0000-000000003000", name: "Main hand", eligibleEquipmentTypes: ["OneHandWeapon" /* OneHandWeapon */, "Shield" /* Shield */] }, { id: "00000000-0000-0000-0000-000000003001", name: "Off hand", eligibleEquipmentTypes: ["OneHandWeapon" /* OneHandWeapon */, "Shield" /* Shield */] }, { id: "00000000-0000-0000-0000-000000003002", name: "Chest", eligibleEquipmentTypes: ["BodyArmor" /* BodyArmor */] }, { id: "00000000-0000-0000-0000-000000003003", name: "Head", eligibleEquipmentTypes: ["BodyArmor" /* BodyArmor */] } ]; } getClassDefinitions() { return [ { id: "00000000-0000-0000-0000-000000004000", name: "Rogue", levelProgression: [ { actionDefinitionId: "00000000-0000-0000-0000-000000004100", unlockedAtLevel: 1 } ] }, { id: "00000000-0000-0000-0000-000000005000", name: "Bard", levelProgression: [ { actionDefinitionId: "00000000-0000-0000-0000-000000005100", unlockedAtLevel: 1 } ] }, { id: "00000000-0000-0000-0000-000000006000", name: "Wizard", levelProgression: [ { actionDefinitionId: "00000000-0000-0000-0000-000000006100", unlockedAtLevel: 1 } ] } ]; } getElementDefinitions() { 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, attribute) { 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, action, target, effect) { 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, actionDef, defender) { 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, damageType) { return 1; } characterResistanceMultiplier(actor, damageType) { return 1; } characterResistanceAbsolute(actor, damageType) { return 1; } characterResourceGeneration(actor) { return [ { amount: 1, resourceTypeId: PrimaryActionResource.id }, { amount: 35, resourceTypeId: MovementSpeedResource.id } ]; } characterIsDead(actor) { const healthResource = this.getCharacterResourceTypes().find((r) => r.name === HealthResourceTypeName); const characterHealthResource = actor.resources.find((r) => r.resourceTypeId === healthResource.id); return characterHealthResource !== void 0 && characterHealthResource.amount <= 0; } characterBattleActionOrder(actor) { return this.roll("D20+0"); } }; function getAbilityModifier(abilityScore, fallback = -1e3) { const map = { 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; } // src/data/rulesets/league-of-dungeoneers.ts var LeagueOfDungeoneersRuleset = class { getActingOrder(actors) { throw new Error("Method not implemented."); } getCurrentActorTurn(battle) { throw new Error("Method not implemented."); } characterHit(attacker, action, defender) { throw new Error("Method not implemented."); } characterIsDead(actor) { throw new Error("Method not implemented."); } roll(dice) { throw new Error("Method not implemented."); } getLevelProgression() { throw new Error("Method not implemented."); } getCharacterStatTypes() { throw new Error("Method not i