199 lines
4.8 KiB
TypeScript
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,
|
|
};
|
|
}
|