The pack
cellDungeon
function cellDungeon(): Pack<{ cellDungeon: typeof kit; }>
Compose the cell map under game.cellDungeon.
Helpers
createCellDungeon
function createCellDungeon(config: CellDungeonConfig): CellDungeon
Make a map from rows and a legend. Throws, naming the problem, on one it cannot read.
yawOf
const yawOf = (dx: number, dz: number): number=> …
The engine's yaw for a direction across the map: the inverse of the kernel's forward.
Classes
CellDungeon
class CellDungeon
A map of square cells for first-person and dungeon games: walls with a picture per cell, sliding doors (some locked
by key) that open when used and close by themselves, walls that slide away when pushed to reveal secrets, and
switches, and lamps, torches and glows a presenter can light the rooms with. It answers the questions such games ask
every step: can I stand here (with sliding collision), what does a ray hit and where on its face, can these two
points see each other, which cells does a sound reach through open doors, and what is the way from here to there.
Coordinates are in cells: cell (x, z) spans x..x+1 and z..z+1, rows run south as z grows. Get one from
createCellDungeon.
readonly width: number— Width in cells.readonly height: number— Height in cells.constructor(config: CellDungeonConfig)get config(): CellDungeonConfig— A copy of the config.get state(): CellDungeonState— A copy of the state: everything a save needs.get secretsFound(): number— Secret walls pushed so far.get secretCount(): number— How many secret walls the map had.things(): CellThing[]— What the map places: starts, enemies, items, by the legend'sthingnames.lights(): CellLight[]— The map's lights as they are now, with their defaults filled in and whether each is lit.switchLights(tag: string, on: boolean): number— Switch every light taggedtagon or off (a switch's action, a trap, a power cut). Returns how many have that tag.kindAt(x: number, z: number): CellKind— The kind of a cell; off the map is a wall.textureAt(x: number, z: number): string— The picture on a wall, door or switch cell ('' on floor).inside(x: number, z: number): boolean— Whether a cell is on the map.door(x: number, z: number): CellDoor | undefined— A door's state, or undefined when the cell has none.doors(): CellDoor[]— Every door.pushing(): CellPushWall[]— The walls sliding now.switchUsed(x: number, z: number): boolean— Whether a switch has been used.solidAt(x: number, z: number): boolean— Whether a body may stand in a cell: not a wall, a secret, a switch, a closed door (one under 0.9 open) or a sliding wall.collide(x: number, z: number, dx: number, dz: number, radius = 0.3): { x: number; z: number }— Move a round body ofradiuscells (0.05 to 0.49) fromx, zbydx, dz, sliding along walls: each axis is tried on its own and refused when the body would overlap a solid cell it does not overlap already. Returns where it ends up.ray(x: number, z: number, yaw: number, max = 64): CellRayHit | null— Cast a ray fromx, zalong the engine's yawyaw(0 faces +z) for up tomaxcells (default 64), stepping cell by cell (a digital differential analyser). Doors are hit on their mid-plane where their panel still covers; open floor and sliding walls are passed (a sliding wall's own cell blocks). Returns the hit or null.sees(ax: number, az: number, bx: number, bz: number): boolean— Whether pointbcan be seen from pointa: no wall, closed door panel or sliding wall between them.hears(x: number, z: number, steps: number): Set<string>— The cells a sound made in cellx, zreaches withinstepscells (1 to 256): a flood through open floor and doors that are at least partly open, as sounds travel between rooms in the classic corridor shooters.path(fromX: number, fromZ: number, toX: number, toZ: number, doors: 'open' | 'unlocked' | 'any' = 'unlocked'): { x: number; z: number }[] | null— The shortest way between two cells through floor and doors, as cells after the first, or null.doorschooses which doors count as a way through:'open'only open ones,'unlocked'(default) any a body without keys could open, or'any'.use(x: number, z: number, yaw: number, keys: readonly string[] = [], reach = 1): CellUse— Use whatever is in front of a body atx, zfacingyaw, withinreachcells (default 1): open a door (whenkeysholds its key), push a secret wall away from the user, or press a switch. Changes the map; the result says what happened.enemyWorld(): CellEnemyWorld— The map asfps-enemiesneeds it: pass it tocreateHorde(config, map.enemyWorld()).openDoor(x: number, z: number): boolean— Open a door from anywhere (an enemy walking through, a script); a locked door opens too. False when there is none.tick(dt: number, occupied: (x: number, z: number) => boolean = () => false): CellEvent[]— Run doors and sliding walls on bydtseconds (0 to 10).occupied(x, z)says whether any part of a body is in a cell, so a door does not close on it (it waits and tries again).restore(state: CellDungeonState): void— Put back a saved state of this map. Throws on one that does not fit.
Types
CellDef
interface CellDef
What a map character means.
kind: CellKind—'floor','wall','door'(slides open),'secret'(a wall that slides away when pushed) or'switch'(a wall that does something when used).texture?: string— The wall or door picture's name, for the presenter; default'wall'('door'on doors).key?: string— Door: the key it needs, by name; default none (anyone opens it).action?: string— Switch: what using it does, by name ('exit'ends a floor in the shooter rules); default'exit'.thing?: string— Floor: a thing standing on this cell (a start, an enemy, an item), by name.facing?: CellFacing— Floor: which way the thing faces; default'n'.
CellDoor
interface CellDoor
A door's state.
x: number— Cell column.z: number— Cell row.axis: 'x' | 'z'— The axis the door panel spans and slides along:'x'in a north–south corridor,'z'in an east–west one.open: number— 0 closed to 1 open.phase: 'closed' | 'opening' | 'open' | 'closing'—'closed','opening','open'or'closing'.hold: number— Seconds left fully open before it closes.key: string | null— The key it needs, or null.texture: string— Its picture.
CellDungeonConfig
interface CellDungeonConfig
A map written as rows of characters with a legend.
rows: string[]— The map, north row first, one character per cell, 1 to 128 rows of 1 to 128 characters (short rows are padded with walls).legend: Record<string, CellDef>— What each character means. A character missing from the legend is an error; a space is a wall.doorSpeed?: number— How fast a door slides, in door widths per second, 0.1 to 20; default 1.5.doorHold?: number— Seconds a door stays fully open before it closes, 0 to 600; default 4. 0 keeps doors open.secretSpeed?: number— How fast a pushed wall slides, cells per second, 0.1 to 20; default 1.secretSlide?: number— How many cells a pushed wall slides at most, 1 to 8; default 2.things?: CellThingDef[]— Things placed by position rather than by legend character: the easier way to place enemies and items, and what a map editor writes. Each stands on a floor cell. Default none.lights?: CellLightDef[]— Lamps, torches and glows: points of light a presenter turns into real lights (withlighting-3d, say). The map keeps each one on or off (switchLights); it draws nothing. Default none.
CellDungeonState
interface CellDungeonState
Everything that changes; a save holds exactly this.
cells: number[]— The cells, row-major: index intokinds.textures: string[]— Wall and door pictures, row-major ('' on open floor).doors: Record<string, CellDoor>— Doors by"x,z".pushing: CellPushWall[]— Walls sliding now.used: string[]— Switches used, by"x,z".secretsFound: number— Secret walls pushed.lamps: boolean[]— Each map light: lit or not, in the config's order.
CellEnemyWorld
interface CellEnemyWorld
The map as enemies see it, for fps-enemies' createHorde(config, map.enemyWorld()): sight, sliding moves,
routes through doors, hearing, rays, doors to open and cell centres to patrol between. Floors are at height 0.
sees(ax: number, az: number, bx: number, bz: number): boolean— An unbroken line between two points.move(x: number, z: number, dx: number, dz: number, body: { radius: number }): { x: number; z: number; y: number }— Move a round body, sliding along walls.route(fx: number, fz: number, tx: number, tz: number): { x: number; z: number; door?: string }[] | null— The way between two points as cell centres, a door cell naming its door ("x,z").hearing(x: number, z: number, radius: number): (x: number, z: number) => boolean— Who hears a noise that carriesradiuscells through open floor and open doors.wallDistance(x: number, z: number, yaw: number, max: number): number | null— How far a ray goes before a wall or closed door, or null withinmax.doorShut(door: string): boolean— Whether the door"x,z"blocks the way.openDoor(door: string): boolean— Open the door"x,z"; true when it was closed.floorAt(x: number, z: number): number— 0: the floor is flat.settle(x: number, z: number): { x: number; z: number }— The centre of the cell a point is in.
CellEvent
type CellEvent = | { type: 'door-opened'; x: number; z: number } | { type: 'door-closing'; x: number; z: number } | { type: 'door-closed'; x: number; z: number } | { type: 'door-blocked'; x: number; z: number } | { type: 'wall-moved'; x: number; z: number } | { type: 'wall-stopped'; x: number; z: number }
What a step changed.
CellFacing
type CellFacing = 'n' | 'e' | 's' | 'w'
A compass facing for things placed on the map: north is −z (up the rows), south +z, east +x, west −x.
CellKind
type CellKind = 'floor' | 'wall' | 'door' | 'secret' | 'switch'
What a cell of the map is.
CellLight
interface CellLight
A map light with its defaults filled in and whether it is lit now, as lights() lists them.
x: number— Position in cells.z: number— Position in cells, along the rows.y: number— Height above the floor, in cells.color: string— Colour, CSS.intensity: number— Brightness.radius: number— Reach, in cells.flicker: number— Flicker, 0 to 1.kind: CellLightKind— What it looks like.tag: string | null— Its group name, or null.on: boolean— Whether it is lit now.index: number— Its place in the config'slights.
CellLightDef
interface CellLightDef
A light placed on the map: plain data, as a map editor writes it.
x: number— Position in cells, any fraction (the centre of cell 3 is 3.5). It must stand over a floor or door cell.z: number— Position in cells, along the rows.y?: number— Height above the floor in cells (a wall is as tall as a cell is wide), 0 to 16; default 0.8, just under a one-cell ceiling.color?: string— Colour, CSS; default'#ffffff'.intensity?: number— Brightness, 0 to 1000, in inverse-square units at cell scale: a lamp lighting a room five cells across wants about 1 to 3; default 2.radius?: number— How far it reaches, in cells, 0.1 to 128; default 4. Point lights cast no shadows, so keep it to the room.flicker?: number— Flicker, 0 (steady) to 1 (a guttering torch); default 0.kind?: CellLightKind— What it looks like; default'lamp'.tag?: string— A group name, forswitchLights.on?: boolean— Lit at the start; default true.
CellLightKind
type CellLightKind = 'lamp' | 'torch' | 'glow'
What a light looks like, for the presenter: a 'lamp' fitting, a 'torch' flame, or a 'glow' with no visible source.
CellPushWall
interface CellPushWall
A wall sliding after a push, and where it is.
x: number— The cell it started this step in (the one it blocks).z: number— The cell row it started this step in.dx: number— Direction it slides, one of −1, 0, 1 on each axis.dz: number— Direction along rows.progress: number— 0 to 1 of the way into the next cell.left: number— Cells still to slide after this one.texture: string— Its picture.
CellRayHit
interface CellRayHit
Where a ray stopped.
dist: number— Distance along the ray, in cells.x: number— The cell hit.z: number— The cell row hit.side: 'x' | 'z'—'x'when it crossed a vertical (x) boundary,'z'a horizontal one; a door is hit on its mid-plane.u: number— 0 to 1 across the face where it hit, for texturing.kind: CellKind— What it hit.texture: string— The face's picture.hitX: number— Where it hit, in cell units.hitZ: number— Where it hit, in cell units.
CellThing
interface CellThing
A thing the map places.
thing: string— Its name in the legend.x: number— Cell column.z: number— Cell row.yaw: number— The facing as the engine's yaw, radians: 0 faces +z (south), and turning right adds (π/2 faces −x, west).
CellThingDef
interface CellThingDef
A thing placed on the map by position.
thing: string— What it is, by name: a start, an enemy kind, an item.x: number— Cell column.z: number— Cell row.facing?: CellFacing— Which way it faces; default'n'.
CellUse
type CellUse = | { type: 'door'; x: number; z: number; opening: boolean } | { type: 'locked'; x: number; z: number; key: string } | { type: 'secret'; x: number; z: number; dx: number; dz: number } | { type: 'switch'; x: number; z: number; action: string } | { type: 'nothing' }
What using the wall in front did.


