✨Feature: add level 1 content including room templates, encounter tables, and room generation logic
- Introduced contentHelpers for table and room template lookups. - Created level1Rooms.ts with various room templates for level 1. - Added level1Tables.ts containing encounter tables for animals, martial, dogs, people, fungal, guards, workers, and room types. - Implemented room generation functions in rooms.ts to create rooms based on templates and tables. - Added tests for room generation logic in rooms.test.ts to ensure correct functionality.
This commit is contained in:
198
src/rules/rooms.ts
Normal file
198
src/rules/rooms.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
findRoomTemplateById,
|
||||
findRoomTemplateForLookup,
|
||||
findTableByCode,
|
||||
} from "@/data/contentHelpers";
|
||||
import type { ContentPack, ExitType, RoomClass } from "@/types/content";
|
||||
import type { 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) {
|
||||
return {
|
||||
level,
|
||||
themeName: level === 1 ? "The Entry" : undefined,
|
||||
rooms: {},
|
||||
discoveredRoomOrder: [],
|
||||
secretDoorUsed: false,
|
||||
exhaustedExitSearch: false,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user