import { findRoomTemplateById } from "@/data/contentHelpers"; import type { ContentPack } from "@/types/content"; import type { DungeonLevelState, RoomState } from "@/types/state"; import type { LogEntry } from "@/types/rules"; import type { DiceRoller } from "./dice"; import { resolveRoomEncounter } from "./encounters"; import type { TableLookupResult } from "./tables"; export type EnterRoomOptions = { content: ContentPack; levelState: DungeonLevelState; roomId: string; roller?: DiceRoller; at?: string; }; export type EnterRoomResult = { levelState: DungeonLevelState; room: RoomState; logEntries: LogEntry[]; lookup?: TableLookupResult; firstEntry: boolean; encounterResolved: boolean; }; function cloneRoom(room: RoomState): RoomState { return { ...room, position: { ...room.position }, dimensions: { ...room.dimensions }, exits: room.exits.map((exit) => ({ ...exit })), discovery: { ...room.discovery }, encounter: room.encounter ? { ...room.encounter, creatureIds: [...room.encounter.creatureIds], creatureNames: room.encounter.creatureNames ? [...room.encounter.creatureNames] : undefined, } : undefined, objects: room.objects.map((object) => ({ ...object, effects: object.effects ? [...object.effects] : undefined, })), notes: [...room.notes], flags: [...room.flags], }; } function cloneLevel(levelState: DungeonLevelState): DungeonLevelState { return { ...levelState, rooms: Object.fromEntries( Object.entries(levelState.rooms).map(([roomId, room]) => [roomId, cloneRoom(room)]), ), discoveredRoomOrder: [...levelState.discoveredRoomOrder], }; } function getRoomLabel(content: ContentPack, room: RoomState) { if (room.templateId) { return findRoomTemplateById(content, room.templateId).title; } return room.notes[0] ?? room.id; } function createLogEntry( id: string, at: string, type: LogEntry["type"], text: string, relatedIds: string[], ): LogEntry { return { id, at, type, text, relatedIds, }; } function formatRollText(lookup: TableLookupResult) { const total = lookup.roll.modifiedTotal ?? lookup.roll.total; const rolledValues = lookup.roll.rolls.join(", "); return `Rolled ${lookup.roll.diceKind} [${rolledValues}] on ${lookup.tableId} for ${total}: ${lookup.entry.label}.`; } export function enterRoom(options: EnterRoomOptions): EnterRoomResult { const levelState = cloneLevel(options.levelState); const room = levelState.rooms[options.roomId]; if (!room) { throw new Error(`Unknown room id: ${options.roomId}`); } const at = options.at ?? new Date().toISOString(); const roomLabel = getRoomLabel(options.content, room); const firstEntry = !room.discovery.entered; room.discovery.entered = true; const resolution = resolveRoomEncounter(options.content, room, options.roller); const nextRoom = resolution.room; const encounterResolved = Boolean(resolution.encounter); if (resolution.encounter && resolution.encounter.creatureIds.length === 0) { nextRoom.discovery.cleared = true; } levelState.rooms[nextRoom.id] = nextRoom; const entryVerb = firstEntry ? "Entered" : "Re-entered"; const logEntries: LogEntry[] = [ createLogEntry( `${nextRoom.id}.entry.${firstEntry ? "first" : "repeat"}`, at, "room", `${entryVerb} ${roomLabel}.`, [nextRoom.id], ), ]; if (resolution.lookup) { logEntries.push( createLogEntry( `${nextRoom.id}.entry.roll`, at, "roll", formatRollText(resolution.lookup), [nextRoom.id, resolution.lookup.tableId], ), ); } if (resolution.encounter) { const encounterText = resolution.encounter.creatureIds.length === 0 ? `${roomLabel} is quiet.` : `Encounter in ${roomLabel}: ${resolution.encounter.resultLabel ?? "Unknown threat"}.`; logEntries.push( createLogEntry( `${nextRoom.id}.entry.encounter`, at, "room", encounterText, [nextRoom.id, resolution.encounter.id], ), ); } return { levelState, room: nextRoom, logEntries, lookup: resolution.lookup, firstEntry, encounterResolved, }; }