173 lines
4.9 KiB
TypeScript
173 lines
4.9 KiB
TypeScript
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,
|
|
};
|
|
}
|