# How to make a co-op dungeon crawler with procedural rooms in TypeScript

> Start the CRYPTWICK template with one command: a four-player co-op crawl through a crypt grown from a seed, with rooms that wake as you approach, flow-field pathing round walls, reliquary upgrades and permadeath. Change the seed, the rooms and the encounters in src/game.ts.

Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under `@netforge` with source at https://github.com/netnetdev/net-forge.

Source: https://forge.netnet.capital/docs/guides/dungeon-crawler-typescript  
Last updated: 2026-10-23. Checked against @netforge/engine 0.2.0.

Run `npm create netforge@latest my-crypt -- --template dungeon --yes` and you have a co-op dungeon crawler: a crypt of ten rooms grown from a seed, up to four players going down room by room, enemies that walk round walls through the doors, an upgrade at every cleared room and a boss in the deepest one. The floor plan is one line of TypeScript, `dungeon({ seed, rooms, ... })`, and the room logic is a system you can read and change.

```sh
npm create netforge@latest my-crypt -- --template dungeon --yes
cd my-crypt
npm run play
```

`--template crawler` is accepted as the same template.

## What you get

| Part | Where | Notes |
|---|---|---|
| Procedural floor plan | `dungeon({...})` | Rooms on a grid joined by corridors, from a seed. The same seed gives the same crypt on the server, every client and the playtest; nothing is downloaded. |
| Walls and pathing | engine-sim | Walls stop players, enemies and bolts. Enemies behind a wall path round through the doors on a flow field, big bodies included. |
| The crawl | `system("crawl", ...)` in `src/game.ts` | Rooms wake as the party nears them, encounters by depth, a quiet timer, reliquaries and respawns on clear. |
| Permadeath | `players.downed` | No free get-up; if the whole party is down at once the descent is over. |
| Fog of war and x-ray walls | template client | Rooms nobody entered stay dark; a wall never hides your character. |
| Server cost | measured | Four bots for four minutes clear 219 enemies across 8 rooms at 0.40 ms per tick on average, p99 0.77 ms (Apple M2 Pro, Node 22). |

## 1. Start it

```sh
npm create netforge@latest my-crypt -- --template dungeon --yes
cd my-crypt
npm run play -- --players 2
```

## 2. Grow a different crypt

```ts title=src/game.ts
import { dungeon } from "@netforge/engine";

const CRYPT = dungeon({ seed: 7, rooms: 14, cell: 28, size: [16, 22], corridor: 6, loops: 1 });

const deepest = CRYPT.boss;
const firstRoom = CRYPT.roomAt(0, 0);
export { CRYPT, deepest, firstRoom };
```

- `seed` is any whole number; a different seed is a different floor plan.
- `rooms` is 3 to 20, the start and boss rooms included. About 10 makes a 6 to 10 minute descent, about 40 seconds a room.
- `cell` is the room spacing in metres (16 to 32), `size` the smallest and largest room side, `corridor` the corridor width (6 lets a boss through), and `loops` the extra doors that make loops (0 is a pure tree).

Pass `field: CRYPT.field` to `game({...})`. `CRYPT.boss` is the deepest room, `CRYPT.rooms` gives each room's centre, half sizes, depth and links, and `CRYPT.roomAt(x, z)` the room holding a point (or -1 in a corridor), so your systems can reason about rooms.

## 3. Change what each depth holds

The crawl system reads an `ENCOUNTERS` table: what a woken room sends, by its depth, for one player. Each extra player adds 35%.

```ts title=src/game.ts
const ENCOUNTERS: readonly (readonly (readonly [string, number])[])[] = [
  [["bone_rattler", 7], ["crypt_bat", 5]],
  [["bone_rattler", 9], ["bone_archer", 3], ["grave_shambler", 2]],
  [["grave_shambler", 3], ["crypt_bat", 9], ["wax_wisp", 4], ["bone_archer", 3]],
];
export { ENCOUNTERS };
```

Add a new enemy with `prefab(...)`, put it in a row of `ENCOUNTERS` and in the `DEAD` list, so a room that still has one standing is not cleared:

```ts title=src/game.ts
import { prefab } from "@netforge/engine";

prefab("ghoul", { model: "style.lowpoly.zombie", health: 30, speed: 3.2, radius: 0.4, damage: 8, ai: "chase", drops: ["wick_small"] });
prefab("bone_archer", { model: "style.lowpoly.skeleton", health: 24, speed: 2.8, ai: "ranged", attack: { damage: 9, range: 10, speed: 7 }, drops: ["wick_small"] });
```

A ranged enemy shows an aim line before it fires and needs a clear line, so walls are cover.

## 4. Test it

```sh
npm test
```

The playtest sends four bots down the crypt and checks that rooms clear, players level and earn rules fire, with no softlock. A crypt whose boss room cannot be reached, or a room that never clears, shows up here.

## How it works

The crawler runs on the same machinery as the survivor-like: auto-firing weapons, level-up cards, co-op downs and revives, bosses with telegraphed attacks. The server steps it at 30 Hz and is authoritative; your character is predicted in the browser. Walls are boxes in the Rapier world for players, and enemies, bolts and spawns are resolved against them, with a flow field (a wider grid for big bodies) guiding enemies round to the doors. A woken room's enemies come in inside that room but off every player's screen. See [Simulation and ECS](https://forge.netnet.capital/docs/concepts/simulation-ecs.md) and the [dungeon crawler template page](https://forge.netnet.capital/docs/templates/dungeon-crawler.md).

## Alternatives

- **[Phaser](https://docs.phaser.io/)** is a free, open-source 2D framework with WebGL and Canvas rendering. Pick it for a 2D tile-based crawler where you write the generation and the loop yourself.
- **[Godot](https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_web.html)** has a scene editor and tile tools and exports to the web as WebAssembly with WebGL 2. Pick it when you want to hand-design rooms in an editor.

Pick Net Forge when you want the co-op 3D crawl with procedural rooms, pathing and netcode already working.

## FAQ

### Is the dungeon generated on the server or the client?

Both, from the same spec. `dungeon({ seed, ... })` is deterministic, so the server, every client and the playtest grow the same crypt without sending it over the network.

### How do enemies find players behind walls?

They follow a flow field computed over the floor plan, which leads them round to the doors. Big enemies use a wider grid so they do not get stuck in corridors.

### Can I turn permadeath off?

Yes. Set `players.downed` to allow a free get-up (`selfRevives: 1`) or a respawn time in seconds.

### How long is a run?

About 40 seconds a room. Ten rooms make a 6 to 10 minute descent.

### Can I hand-design the rooms instead of generating them?

The template grows the crypt from `dungeon(...)`. A game can also declare its own `field` with `walls` and `floors` as boxes, which is how the party template builds its single playroom.
