Rooms and Regions

Rooms are the locations players explore. Regions are logical groupings of related rooms organized as single files.

Creating Rooms

import { WorldModel, EntityType, RoomTrait, IdentityTrait } from '@sharpee/world-model';
const kitchen = world.createEntity('kitchen', EntityType.ROOM);
kitchen.add(new RoomTrait({ exits: {}, isDark: false }));
kitchen.add(new IdentityTrait({
name: 'Kitchen',
description: 'A small kitchen with copper pots hanging from the ceiling.',
properName: true,
article: 'the',
}));

Every room needs two traits:

  • RoomTrait — exits, darkness, outdoor status
  • IdentityTrait — name, description, aliases

Room Properties

PropertyTypeDescription
isDarkbooleanRequires light source to see
isOutdoorsbooleanOutdoor location
exitsobjectDirection-to-destination mapping

Dark Rooms

const cellar = world.createEntity('cellar', EntityType.ROOM);
cellar.add(new RoomTrait({ exits: {}, isDark: true }));
cellar.add(new IdentityTrait({
name: 'Cellar',
description: 'A damp cellar with stone walls.',
properName: true,
article: 'the',
}));

Players need a light source (like a lantern with LightSourceTrait and isOn: true) to see in dark rooms.

Connecting Rooms

Connections are set directly on the RoomTrait.exits property:

import { Direction } from '@sharpee/world-model';
// Direct assignment
kitchen.get(RoomTrait)!.exits[Direction.NORTH] = { destination: diningRoom.id };
diningRoom.get(RoomTrait)!.exits[Direction.SOUTH] = { destination: kitchen.id };

Helper Function

For convenience, define a setExits helper:

import { DirectionType } from '@sharpee/world-model';
function setExits(room: IFEntity, exits: Partial<Record<DirectionType, string>>): void {
const trait = room.get(RoomTrait);
if (trait) {
for (const [dir, dest] of Object.entries(exits)) {
trait.exits[dir as DirectionType] = { destination: dest! };
}
}
}
// Usage
setExits(kitchen, {
[Direction.NORTH]: diningRoom.id,
[Direction.DOWN]: cellar.id,
[Direction.OUT]: garden.id,
});

Available Directions

Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST
Direction.UP, Direction.DOWN, Direction.IN, Direction.OUT
Direction.NORTHEAST, Direction.NORTHWEST
Direction.SOUTHEAST, Direction.SOUTHWEST

Exits Through Doors

Connect rooms through a door that can be opened/closed:

import { DoorTrait, OpenableTrait } from '@sharpee/world-model';
const door = world.createEntity('oak-door', EntityType.DOOR);
door.add(new IdentityTrait({
name: 'oak door',
description: 'A heavy oak door with iron bands.',
}));
door.add(new OpenableTrait({ isOpen: false }));
door.add(new DoorTrait());
world.moveEntity(door.id, hallway.id);
// Connect through the door
hallway.get(RoomTrait)!.exits[Direction.NORTH] = {
destination: study.id,
via: door.id, // Must open door first
};

One-Way Exits

// Cliff edge — can go down but not back up
setExits(cliffTop, { [Direction.DOWN]: ravine.id });
// Don't add UP exit from ravine

Organizing into Regions

For any story beyond a handful of rooms, organize by region. Each region is a single file containing all rooms, objects, and internal connections for that area:

src/regions/
├── village.ts # Village square, tavern, shop
├── forest.ts # Paths, clearing, grove
├── dungeon.ts # Underground rooms and items
└── castle.ts # Castle rooms and treasures

Region Pattern

src/regions/forest.ts
import { WorldModel, IFEntity, EntityType, RoomTrait, IdentityTrait, Direction } from '@sharpee/world-model';
export interface ForestRooms {
clearing: IFEntity;
path: IFEntity;
grove: IFEntity;
}
export function createForest(world: WorldModel): ForestRooms {
// Create rooms
const clearing = world.createEntity('clearing', EntityType.ROOM);
clearing.add(new RoomTrait({ exits: {}, isDark: false, isOutdoors: true }));
clearing.add(new IdentityTrait({
name: 'Forest Clearing',
description: 'Sunlight filters through the canopy above.',
properName: true,
article: 'the',
}));
const path = world.createEntity('forest-path', EntityType.ROOM);
path.add(new RoomTrait({ exits: {}, isDark: false, isOutdoors: true }));
path.add(new IdentityTrait({
name: 'Forest Path',
description: 'A winding path through dense trees.',
properName: true,
article: 'the',
}));
const grove = world.createEntity('grove', EntityType.ROOM);
grove.add(new RoomTrait({ exits: {}, isDark: false, isOutdoors: true }));
grove.add(new IdentityTrait({
name: 'Sacred Grove',
description: 'Ancient oaks form a natural cathedral.',
properName: true,
article: 'the',
}));
// Internal connections
clearing.get(RoomTrait)!.exits[Direction.EAST] = { destination: path.id };
path.get(RoomTrait)!.exits[Direction.WEST] = { destination: clearing.id };
path.get(RoomTrait)!.exits[Direction.NORTH] = { destination: grove.id };
grove.get(RoomTrait)!.exits[Direction.SOUTH] = { destination: path.id };
// Objects in this region
const mushroom = world.createEntity('mushroom', EntityType.ITEM);
mushroom.add(new IdentityTrait({
name: 'red mushroom',
description: 'A bright red mushroom with white spots.',
}));
world.moveEntity(mushroom.id, clearing.id);
return { clearing, path, grove };
}

Connecting Regions

Wire regions together in your story’s initializeWorld:

initializeWorld(world: WorldModel): void {
const forest = createForest(world);
const castle = createCastle(world);
// Cross-region connections
forest.path.get(RoomTrait)!.exits[Direction.NORTH] = { destination: castle.gate.id };
castle.gate.get(RoomTrait)!.exits[Direction.SOUTH] = { destination: forest.path.id };
const player = world.getPlayer();
world.moveEntity(player.id, forest.clearing.id);
}

Best Practices

  1. Use meaningful IDs: 'forest-clearing' not 'room1'
  2. One region file per area: Keep rooms, objects, and connections together
  3. Return typed interfaces: ForestRooms makes cross-region wiring type-safe
  4. Test navigation: Create transcript tests for critical paths
  5. Consider light: Dark rooms add puzzle opportunities