Files
2D6-Dungeon/src/rules/rooms.ts

199 lines
4.8 KiB
TypeScript

import {
findRoomTemplateById,
findRoomTemplateForLookup,
findTableByCode,
} from "@/data/contentHelpers";
import type { ContentPack, ExitType, RoomClass } from "@/types/content";
import type { DungeonLevelState, RoomExitState, RoomState } from "@/types/state";
import { lookupTable, type TableLookupResult } from "./tables";
import type { DiceRoller } from "./dice";
export type RoomGenerationOptions = {
content: ContentPack;
roomId: string;
level: number;
roomTemplateId?: string;
roomTableCode?: string;
roomClass?: RoomClass;
position?: {
x: number;
y: number;
};
roller?: DiceRoller;
};
export type RoomGenerationResult = {
room: RoomState;
lookup?: TableLookupResult;
templateSource: "direct-template" | "table-lookup";
};
const DEFAULT_ROOM_DIMENSIONS: Record<RoomClass, { width: number; height: number }> = {
start: { width: 4, height: 4 },
normal: { width: 4, height: 4 },
small: { width: 2, height: 3 },
large: { width: 6, height: 6 },
special: { width: 4, height: 4 },
stairs: { width: 4, height: 4 },
};
const DEFAULT_DIRECTIONS = ["north", "east", "south", "west"] as const;
function inferExitType(exitHint?: string): ExitType {
const normalized = exitHint?.toLowerCase() ?? "";
if (normalized.includes("reinforced")) {
return "locked";
}
if (normalized.includes("curtain")) {
return "open";
}
if (normalized.includes("archway")) {
return "open";
}
if (normalized.includes("wooden")) {
return "door";
}
return "door";
}
function inferExitCount(roomClass: RoomClass, exitHint?: string) {
const normalized = exitHint?.toLowerCase() ?? "";
if (normalized.includes("random")) {
return roomClass === "large" ? 3 : 2;
}
if (normalized.includes("archway")) {
return roomClass === "large" ? 3 : 2;
}
if (roomClass === "small") {
return 1;
}
if (roomClass === "large") {
return 3;
}
return 2;
}
function createExits(
roomId: string,
roomClass: RoomClass,
exitHint?: string,
): RoomExitState[] {
const exitCount = inferExitCount(roomClass, exitHint);
const exitType = inferExitType(exitHint);
return DEFAULT_DIRECTIONS.slice(0, exitCount).map((direction, index) => ({
id: `${roomId}.exit.${index + 1}`,
direction,
exitType,
discovered: roomClass === "start",
traversable: true,
}));
}
export function createRoomStateFromTemplate(
content: ContentPack,
roomId: string,
level: number,
roomTemplateId: string,
position = { x: 0, y: 0 },
): RoomState {
const template = findRoomTemplateById(content, roomTemplateId);
const dimensions = template.dimensions ?? DEFAULT_ROOM_DIMENSIONS[template.roomClass];
const exits =
template.exits?.map((exit, index) => ({
id: `${roomId}.exit.${index + 1}`,
direction: exit.direction ?? DEFAULT_DIRECTIONS[index] ?? "north",
exitType: exit.exitType,
discovered: template.roomClass === "start",
traversable: exit.exitType !== "locked",
destinationLevel: exit.destinationLevel,
})) ?? createExits(roomId, template.roomClass, template.exitHint);
return {
id: roomId,
level,
templateId: template.id,
position,
dimensions,
roomClass: template.roomClass,
exits,
discovery: {
generated: true,
entered: template.roomClass === "start",
cleared: false,
searched: false,
},
encounter: undefined,
objects: [],
notes: [template.text ?? template.title].filter(Boolean),
flags: [
`table:${template.tableCode}`,
`entry:${template.tableEntryKey}`,
...(template.unique ? ["unique"] : []),
...template.tags,
],
};
}
export function generateRoomFromTable(
options: RoomGenerationOptions,
): RoomGenerationResult {
if (!options.roomTableCode) {
throw new Error("Room table code is required for table-based room generation.");
}
const table = findTableByCode(options.content, options.roomTableCode);
const lookup = lookupTable(table, { roller: options.roller });
const template = findRoomTemplateForLookup(options.content, lookup);
const room = createRoomStateFromTemplate(
options.content,
options.roomId,
options.level,
template.id,
options.position,
);
return {
room,
lookup,
templateSource: "table-lookup",
};
}
export function generateLevel1StartRoom(
content: ContentPack,
roomId = "room.level1.start",
): RoomGenerationResult {
const room = createRoomStateFromTemplate(content, roomId, 1, "room.level1.entry", {
x: 0,
y: 0,
});
return {
room,
templateSource: "direct-template",
};
}
export function createLevelShell(level: number): DungeonLevelState {
return {
level,
themeName: level === 1 ? "The Entry" : undefined,
rooms: {},
discoveredRoomOrder: [],
secretDoorUsed: false,
exhaustedExitSearch: false,
};
}