Files
2D6-Dungeon/src/rules/character.ts
Keith Solomon 6bf48df74c Feature: add character creation logic and tests
- Implement character creation functions to handle adventurer setup.
- Add validation for adventurer name and selection of starting items.
- Introduce new items in sample content pack: Flint and Steel, Pouch, Wax Sealing Kit, and Backpack.
- Create tests for character creation functionality to ensure valid options and error handling.

Feature: implement dice rolling mechanics and tests

- Add functions for rolling various dice types (d3, d6, 2d6, d66) with validation.
- Implement modifier application with clamping for roll results.
- Create tests to verify correct behavior of dice rolling functions.

Feature: add table lookup functionality and tests

- Implement table lookup and resolution logic for defined tables.
- Support for modified ranges and fallback to nearest entries.
- Create tests to ensure correct table entry resolution based on roll results.
2026-03-15 11:59:50 -05:00

129 lines
3.4 KiB
TypeScript

import type { ContentPack } from "@/types/content";
import type { AdventurerState, InventoryEntry } from "@/types/state";
export type CharacterCreationChoiceSet = {
name: string;
weaponId: string;
armourId: string;
scrollId: string;
};
const DEFAULT_STARTING_ITEM_IDS = [
"item.flint-and-steel",
"item.lantern",
"item.ration",
"item.pouch",
"item.wax-sealing-kit",
"item.backpack",
];
function requireDefinition<T extends { id: string }>(
entries: T[],
id: string,
label: string,
) {
const entry = entries.find((candidate) => candidate.id === id);
if (!entry) {
throw new Error(`Unknown ${label} id: ${id}`);
}
return entry;
}
function makeInventoryEntry(definitionId: string, quantity = 1): InventoryEntry {
return {
definitionId,
quantity,
};
}
export function getCharacterCreationOptions(content: ContentPack) {
return {
weapons: content.weapons.filter((weapon) => weapon.startingOption),
armour: content.armour.filter((armour) => armour.startingOption),
scrolls: content.scrolls.filter((scroll) => scroll.startingOption),
};
}
export function createStartingAdventurer(
content: ContentPack,
choices: CharacterCreationChoiceSet,
): AdventurerState {
const trimmedName = choices.name.trim();
if (trimmedName.length === 0) {
throw new Error("Adventurer name is required.");
}
const selectedWeapon = requireDefinition(content.weapons, choices.weaponId, "weapon");
const selectedArmour = requireDefinition(content.armour, choices.armourId, "armour");
const selectedScroll = requireDefinition(content.scrolls, choices.scrollId, "scroll");
if (!selectedWeapon.startingOption) {
throw new Error(`Weapon ${selectedWeapon.id} is not a legal starting option.`);
}
if (!selectedArmour.startingOption) {
throw new Error(`Armour ${selectedArmour.id} is not a legal starting option.`);
}
if (!selectedScroll.startingOption) {
throw new Error(`Scroll ${selectedScroll.id} is not a legal starting option.`);
}
const allowedManoeuvreIds = selectedWeapon.allowedManoeuvreIds;
if (allowedManoeuvreIds.length === 0) {
throw new Error(`Weapon ${selectedWeapon.id} does not define starting manoeuvres.`);
}
DEFAULT_STARTING_ITEM_IDS.forEach((itemId) => requireDefinition(content.items, itemId, "item"));
requireDefinition(content.potions, "potion.healing", "potion");
return {
id: "adventurer.new",
name: trimmedName,
level: 1,
xp: 0,
hp: {
current: 10,
max: 10,
base: 10,
},
stats: {
shift: 2,
discipline: 1,
precision: 0,
},
weaponId: selectedWeapon.id,
manoeuvreIds: allowedManoeuvreIds,
armourId: selectedArmour.id,
favour: {},
statuses: [],
inventory: {
carried: [
makeInventoryEntry("item.flint-and-steel"),
makeInventoryEntry("item.lantern"),
makeInventoryEntry("item.ration", 3),
makeInventoryEntry("item.pouch"),
makeInventoryEntry("item.wax-sealing-kit"),
makeInventoryEntry("item.backpack"),
makeInventoryEntry(selectedScroll.id),
makeInventoryEntry("potion.healing"),
],
equipped: [
makeInventoryEntry(selectedWeapon.id),
makeInventoryEntry(selectedArmour.id),
],
stored: [],
currency: {
gold: 0,
},
rationCount: 3,
lightSources: [makeInventoryEntry("item.lantern")],
},
progressionFlags: [],
};
}