import { findCreatureById, findItemById, findTableByCode } from "@/data/contentHelpers"; import type { ContentPack } from "@/types/content"; import type { CombatState, InventoryEntry, InventoryState } from "@/types/state"; import type { LogEntry } from "@/types/rules"; import type { DiceRoller } from "./dice"; import { lookupTable } from "./tables"; export type ResolveCombatLootOptions = { content: ContentPack; combat: CombatState; inventory: InventoryState; roller?: DiceRoller; at: string; }; export type ResolveCombatLootResult = { inventory: InventoryState; itemsAwarded: InventoryEntry[]; goldAwarded: number; logEntries: LogEntry[]; }; function cloneInventory(inventory: InventoryState): InventoryState { return { ...inventory, carried: inventory.carried.map((entry) => ({ ...entry })), equipped: inventory.equipped.map((entry) => ({ ...entry })), stored: inventory.stored.map((entry) => ({ ...entry })), currency: { ...inventory.currency }, lightSources: inventory.lightSources.map((entry) => ({ ...entry })), }; } function createInventoryEntry(definitionId: string, quantity = 1): InventoryEntry { return { definitionId, quantity, }; } function mergeInventoryEntry( content: ContentPack, entries: InventoryEntry[], definitionId: string, quantity: number, ) { const item = findItemById(content, definitionId); if (item.stackable) { const existing = entries.find((entry) => entry.definitionId === definitionId); if (existing) { existing.quantity += quantity; return; } } entries.push(createInventoryEntry(definitionId, quantity)); } function createLootLog( id: string, at: string, text: string, relatedIds: string[], ): LogEntry { return { id, at, type: "loot", text, relatedIds, }; } function summarizeLoot( goldAwarded: number, itemsAwarded: InventoryEntry[], content: ContentPack, ) { const parts: string[] = []; if (goldAwarded > 0) { parts.push(`${goldAwarded} gold`); } for (const item of itemsAwarded) { const itemName = findItemById(content, item.definitionId).name; parts.push(item.quantity > 1 ? `${item.quantity}x ${itemName}` : itemName); } return parts.length > 0 ? parts.join(", ") : "nothing useful"; } export function resolveCombatLoot( options: ResolveCombatLootOptions, ): ResolveCombatLootResult { const inventory = cloneInventory(options.inventory); const itemsAwarded: InventoryEntry[] = []; const logEntries: LogEntry[] = []; let goldAwarded = 0; const defeatedCreatures = options.combat.enemies.filter( (enemy) => enemy.hpCurrent === 0 && enemy.sourceDefinitionId, ); for (const enemy of defeatedCreatures) { const creature = findCreatureById(options.content, enemy.sourceDefinitionId!); const lootTableCodes = creature.lootTableCodes ?? []; for (const tableCode of lootTableCodes) { const table = findTableByCode(options.content, tableCode); const lookup = lookupTable(table, { roller: options.roller }); const rollTotal = lookup.roll.modifiedTotal ?? lookup.roll.total; logEntries.push({ id: `${options.combat.id}.${creature.id}.${tableCode}.roll`, at: options.at, type: "roll", text: `Rolled loot ${lookup.roll.diceKind} [${lookup.roll.rolls.join(", ")}] on ${tableCode} for ${rollTotal}: ${lookup.entry.label}.`, relatedIds: [options.combat.id, creature.id, tableCode], }); let creatureGold = 0; const creatureItems: InventoryEntry[] = []; for (const effect of lookup.entry.effects ?? []) { if (effect.type === "gain-gold" && effect.amount) { creatureGold += effect.amount; } if (effect.type === "add-item" && effect.referenceId) { mergeInventoryEntry(options.content, inventory.carried, effect.referenceId, effect.amount ?? 1); mergeInventoryEntry(options.content, itemsAwarded, effect.referenceId, effect.amount ?? 1); mergeInventoryEntry(options.content, creatureItems, effect.referenceId, effect.amount ?? 1); } } for (const reference of lookup.entry.references ?? []) { if (reference.type !== "item") { continue; } mergeInventoryEntry(options.content, inventory.carried, reference.id, 1); mergeInventoryEntry(options.content, itemsAwarded, reference.id, 1); mergeInventoryEntry(options.content, creatureItems, reference.id, 1); } if (creatureGold > 0) { inventory.currency.gold += creatureGold; goldAwarded += creatureGold; } logEntries.push( createLootLog( `${options.combat.id}.${creature.id}.${tableCode}.loot`, options.at, `${creature.name} yielded ${summarizeLoot(creatureGold, creatureItems, options.content)}.`, [options.combat.id, creature.id, tableCode], ), ); } } return { inventory, itemsAwarded, goldAwarded, logEntries, }; }