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
JavaScript
"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