doom-osm-godmode 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,341 +1,364 @@
1
- ## Screenshots
2
-
3
- <p align="center">
4
- <img src="assets/screens/outside.jpg" alt="Map outside view" width="400"/>
5
- <img src="assets/screens/inside.jpg" alt="Map inside view" width="400"/>
6
- </p>
7
-
8
- <p align="center"><i>Note: Some maps may show minor visual bugs or wall clipping issues. These are known and will be improved in future releases.</i></p>
9
-
10
- <p align="center">
11
- <img src="assets/header.png" alt="DOOM OSM GODMODE" width="800"/>
12
- </p>
13
-
14
- <p align="center">
15
- <a href="https://github.com/eoinjordan/doom-osm-godmode/actions"><img src="https://github.com/eoinjordan/doom-osm-godmode/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
- <a href="https://www.npmjs.com/package/doom-osm-godmode"><img src="https://img.shields.io/npm/v/doom-osm-godmode?color=brightgreen" alt="npm"></a>
17
- <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node 20+">
18
- <img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero deps">
19
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="MIT"></a>
20
- </p>
21
-
22
- ---
23
-
24
- Give it a place name, a GeoJSON file, or a config — it geocodes through Nominatim, pulls polygon features from OpenStreetMap via Overpass, projects them into a connected room-and-corridor layout, and writes a PWAD in UDMF format ready for GZDoom.
25
-
26
- Zero npm dependencies. Node 20+ only.
27
-
28
- ## Quick Start
29
-
30
- ```bash
31
- # run the bundled demo (offline, no network)
32
- npm run demo
33
-
34
- # build a real place
35
- node src/cli.js build-place "Colosseum, Rome, Italy" --radius 450
36
-
37
- # search without building
38
- node src/cli.js search "Eiffel Tower, Paris"
39
-
40
- # build from a config file
41
- node src/cli.js build-config examples/colosseum.json
42
- ```
43
-
44
- ## Installation
45
-
46
- ```bash
47
- git clone https://github.com/eoinjordan/doom-osm-godmode.git
48
- cd doom-osm-godmode
49
- npm test # verify everything works
50
- npm run demo # generate a WAD from the bundled demo site
51
- ```
52
-
53
- Or install globally from npm:
54
-
55
- ```bash
56
- npx doom-osm-godmode build-place "Colosseum, Rome" --radius 450
57
- ```
58
-
59
- No `npm install` needed — there are no dependencies.
60
-
61
- ## CLI Reference
62
-
63
- ```
64
- doom-osm-godmode
65
-
66
- Commands:
67
- search <query> Search Nominatim for a place
68
- build-place <query> [options] Geocode + OSM fetch + WAD export
69
- build-config <config.json> Build from a JSON config file
70
- demo [--out output/demo-site] Build the bundled demo WAD
71
-
72
- Options for build-place:
73
- --radius <meters> Override bounding box with a fixed radius (default: use Nominatim bbox)
74
- --map <MAPXX> Map lump name (default: MAP01)
75
- --max-rooms <count> Cap on rooms generated (default: 12)
76
- --title <name> Override the map title
77
- --out <dir> Output directory (default: output/<slugified-title>)
78
- ```
79
-
80
- ## Programmatic API
81
-
82
- ```javascript
83
- const { buildFromPlace, buildFromConfig, buildDemo } = require('doom-osm-godmode');
84
-
85
- // from a place name (hits Nominatim + Overpass)
86
- const result = await buildFromPlace('Sydney Opera House', {
87
- radiusMeters: 500,
88
- mapName: 'MAP01',
89
- maxRooms: 10,
90
- outputDir: './output/sydney'
91
- });
92
-
93
- // from a config file (place query or offline GeoJSON)
94
- const result = await buildFromConfig('examples/colosseum.json');
95
-
96
- // bundled demo (no network)
97
- const result = await buildDemo('./output/demo');
98
- ```
99
-
100
- All functions return `{ outputDir, mapPath, featureCount, exportedFeatures }`.
101
-
102
- ## Output
103
-
104
- Each build writes a folder containing:
105
-
106
- | File | Description |
107
- |------|-------------|
108
- | `site.json` | Normalized feature set with projected metrics |
109
- | `layout.json` | Room/corridor grid layout used for WAD generation |
110
- | `TEXTMAP.udmf` | Human-readable UDMF map geometry |
111
- | `MAPINFO.txt` | GZDoom map metadata (sky, music, title) |
112
- | `<MAPXX>.wad` | Packaged PWAD — load this in GZDoom |
113
-
114
- ## Config Format
115
-
116
- Place query (network):
117
-
118
- ```json
119
- {
120
- "title": "Colosseum District",
121
- "mapName": "MAP01",
122
- "placeQuery": "Colosseum, Rome, Italy",
123
- "radiusMeters": 450,
124
- "maxRooms": 10,
125
- "outputDir": "../output/colosseum"
126
- }
127
- ```
128
-
129
- Offline GeoJSON:
130
-
131
- ```json
132
- {
133
- "title": "Demo Waterfront",
134
- "mapName": "MAP01",
135
- "inputGeoJson": "demo-site.geojson",
136
- "maxRooms": 8,
137
- "outputDir": "../output/demo-site"
138
- }
139
- ```
140
-
141
- Relative paths in configs resolve from the config file's directory.
142
-
143
- ## Architecture
144
-
145
- ```
146
- place query / GeoJSON
147
-
148
-
149
- geocode.js ──► Nominatim API
150
-
151
-
152
- osm.js ────► Overpass API (polygon features)
153
-
154
-
155
- feature-set.js project to local coords, filter, sort
156
-
157
-
158
- layout.js grid-based room placement + corridors + theming
159
-
160
-
161
- udmf.js UDMF vertices, linedefs, sidedefs, sectors, things
162
-
163
-
164
- wad.js binary PWAD packaging
165
-
166
-
167
- MAP01.wad ready for GZDoom
168
- ```
169
-
170
- ### Key Source Files
171
-
172
- | File | Purpose |
173
- |------|---------|
174
- | `src/cli.js` | CLI entry point and command router |
175
- | `src/workflow.js` | Build pipeline orchestrator |
176
- | `src/geocode.js` | Nominatim geocoding |
177
- | `src/osm.js` | Overpass polygon fetching |
178
- | `src/feature-set.js` | Feature normalization and projection |
179
- | `src/geo.js` | Mercator projection, bbox, polygon math |
180
- | `src/layout.js` | Grid room placement, corridors, theming |
181
- | `src/udmf.js` | UDMF text map generation |
182
- | `src/wad.js` | PWAD binary packing |
183
- | `src/lib/cli.js` | Argument parser |
184
- | `src/lib/fs.js` | File I/O utilities |
185
-
186
- ### Room Theming
187
-
188
- Features are themed by kind:
189
-
190
- | Kind | Floor | Ceiling | Light | Notes |
191
- |------|-------|---------|-------|-------|
192
- | building | stone | flat | 152 | Default indoor |
193
- | water | nukage | flat | 144 | Floor at -24 |
194
- | park | grass | sky | 176 | Outdoor feel |
195
- | road | light stone | flat | 160 | Corridors |
196
-
197
- ## Testing
198
-
199
- ```bash
200
- npm test # smoke tests via node:test
201
- npm run check # syntax-check all source files
202
- npm run validate # structural WAD/UDMF validation (builds + verifies integrity)
203
- ```
204
-
205
- ## Agent Playtest via doom-mcp
206
-
207
- Generated WADs can be playtested by an AI agent using [doom-mcp](https://github.com/gunnargrosch/doom-mcp) — a Rust MCP server that embeds the real DOOM engine.
208
-
209
- ### Quick setup
210
-
211
- The repo includes a `.mcp.json` that configures doom-mcp to load your demo WAD:
212
-
213
- ```bash
214
- # 1. Build a WAD first
215
- npm run demo
216
-
217
- # 2. Playtest it (automated agent navigation sequence)
218
- npm run playtest -- output/demo-site/MAP01.wad
219
-
220
- # 3. Or playtest a real-place WAD
221
- node src/cli.js build-place "Colosseum" --radius 450
222
- npm run playtest -- output/colosseum/MAP01.wad --skill 1 --ticks 300
223
- ```
224
-
225
- ### What the playtest validates
226
-
227
- The automated playtest spawns doom-mcp, loads the WAD, and runs through a navigation sequence:
228
-
229
- - **Game starts** — doom engine accepts the WAD
230
- - **Player spawns** — HP > 0 at spawn point
231
- - **Player can move** position changes across multiple actions
232
- - **Player survives** doesn't die during baby-difficulty walkthrough
233
-
234
- ### Using doom-mcp interactively
235
-
236
- For interactive agent playtesting (e.g., in Claude Code or Cursor):
237
-
238
- ```json
239
- {
240
- "mcpServers": {
241
- "doom": {
242
- "type": "stdio",
243
- "command": "npx",
244
- "args": ["-y", "doom-mcp"],
245
- "env": {
246
- "DOOM_WAD_PATH": "/path/to/your/MAP01.wad"
247
- }
248
- }
249
- }
250
- }
251
- ```
252
-
253
- Then tell the agent: *"Play this DOOM level and tell me if it's fun"*
254
-
255
- ### CI integration
256
-
257
- The `playtest.yml` workflow runs structural validation on every tag push and can be triggered manually for any place:
258
-
259
- ```
260
- Actions → Playtest → Run workflow → Enter place name → Go
261
- ```
262
-
263
- ## Playing the WAD
264
-
265
- ```bash
266
- # GZDoom (adjust path to your install)
267
- gzdoom -file output/colosseum/MAP01.wad
268
- ```
269
-
270
- The WAD is a PWAD — it layers on top of any IWAD (DOOM2.WAD, FREEDOOM2.WAD, etc.).
271
-
272
- ## Hand-Crafted Buildings
273
-
274
- The OSM pipeline creates grid-based rooms. For architecturally accurate buildings (cathedrals, temples, etc.), use a hand-crafted script:
275
-
276
- ```bash
277
- # Build the Galway Cathedral demo (cross-shaped plan, dome, catacombs)
278
- node scripts/build-galway-cathedral.js
279
-
280
- # Launch in GZDoom
281
- gzdoom -iwad DOOM2.WAD -file output/galway-cathedral-v3/MAP01.wad +map MAP01
282
- ```
283
-
284
- The cathedral builder demonstrates the `UDMFBuilder` class with polygon-based sector creation, self-validation, and teleporter-linked sub-areas. Use it as a template for new hand-crafted maps.
285
-
286
- ### Validate any WAD
287
-
288
- ```bash
289
- # Structural UDMF diagnostic (checks for duplicate linedefs, etc.)
290
- node scripts/diagnose-udmf.js output/galway-cathedral-v3/TEXTMAP.udmf
291
-
292
- # Agent playtest via doom-mcp
293
- npm run playtest -- output/galway-cathedral-v3/MAP01.wad
294
- ```
295
-
296
- ## UDMF Geometry Rules
297
-
298
- Common issues found when generating DOOM maps from real-world data:
299
-
300
- | Issue | Symptom | Fix |
301
- |-------|---------|-----|
302
- | Duplicate linedefs (same vertex pair) | Flickering/transparent walls | Use a single polygon for complex shapes; deduplicate in `addInnerSector` |
303
- | Collinear overlapping linedefs | See-through walls, BSP errors | Never let two linedefs share the same 2D line segment |
304
- | Inner sector outside parent bounds | Rendering void, hall-of-mirrors | Ensure all inner sector vertices are strictly inside the outer polygon |
305
- | Separate rooms at same crossing | Duplicate shared edges | Use ONE polygon for the union shape (e.g., a cross), not overlapping rectangles |
306
- | Underground areas sharing x,y with upper | DOOM 2.5D sector conflict | Place underground areas at different x,y coords; connect via teleporter |
307
- | Invisible walls on two-sided linedefs | Side walls transparent where floor heights match | Set `wrapmidtex = true` + `blocking = true` + `texturemiddle` on the two-sided linedef |
308
- | Floating mid-textures on promoted linedefs | Choppy half-height walls | Clear `texturemiddle` when promoting one-sided to two-sided; use upper/lower instead |
309
- | Zero-height pillar sectors (floor = ceiling) | Player stuck, node builder errors | Use Thing-based pillars (type 30) instead of sector-based pillars |
310
-
311
- ### Repeatable pattern for future OSM meshes
312
-
313
- 1. Run `node src/cli.js build-place ...` for the grid-room WAD
314
- 2. Run `node scripts/diagnose-udmf.js output/<name>/TEXTMAP.udmf` to check geometry
315
- 3. Run `npm run playtest -- output/<name>/MAP01.wad` for engine validation
316
- 4. If the grid rooms are too abstract, create a hand-crafted script (see `scripts/build-galway-cathedral.js`)
317
- 5. Always run self-validation before writing the WAD
318
-
319
- ## Notes
320
-
321
- - Network commands use public OSM infrastructure (Nominatim, Overpass). Keep requests reasonable.
322
- - Only polygon features become rooms. Roads and linear features are not yet carved into sectors.
323
- - The layout is a blockout interpretation — connected rooms reflecting real feature hierarchy, not pixel-perfect street geometry.
324
-
325
- ## Roadmap
326
-
327
- - exact polygon-to-sector carving (replace grid rooms with real building outlines)
328
- - road and canal line buffering into traversable sectors
329
- - Doom encounter placement (monsters, ammo, weapons)
330
- - texture themes per location style
331
- - multi-map episode generation
332
- - freeform agent-described layouts (no OSM needed)
333
- - automatic duplicate linedef detection in the OSM pipeline
334
-
335
- ## Contributing
336
-
337
- PRs welcome. Run `npm test` and `npm run check` before submitting.
338
-
339
- ## License
340
-
341
- MIT — see [LICENSE](LICENSE)
1
+ <p align="center">
2
+ <a href="https://github.com/eoinjordan/doom-osm-godmode/releases"><img src="https://img.shields.io/github/v/tag/eoinjordan/doom-osm-godmode?sort=semver&label=release&color=brightgreen" alt="release tag"></a>
3
+ <a href="https://www.npmjs.com/package/doom-osm-godmode"><img src="https://img.shields.io/npm/v/doom-osm-godmode?color=brightgreen" alt="npm"></a>
4
+ <a href="https://github.com/eoinjordan/doom-osm-godmode/actions"><img src="https://github.com/eoinjordan/doom-osm-godmode/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
5
+ <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node 20+">
6
+ <img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero deps">
7
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="MIT"></a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <img src="assets/header.png" alt="DOOM OSM GODMODE" width="800"/>
12
+ </p>
13
+
14
+ ---
15
+
16
+ Give it a place name, a GeoJSON file, or a config — it geocodes through Nominatim, pulls polygon features from OpenStreetMap via Overpass, projects them into a connected room-and-corridor layout, and writes a PWAD in UDMF format ready for GZDoom.
17
+
18
+ Zero npm dependencies. Node 20+ only.
19
+
20
+ ## Quick Start
21
+
22
+ ```bash
23
+ # run the bundled demo (offline, no network)
24
+ npm run demo
25
+
26
+ # build a real place
27
+ node src/cli.js build-place "Colosseum, Rome, Italy" --radius 450
28
+ node src/cli.js build-place "Eiffel Tower, Paris" --radius 450
29
+
30
+ # hand-crafted architecturally accurate building
31
+ node scripts/build-galway-cathedral.js
32
+
33
+ # search without building
34
+ node src/cli.js search "Eiffel Tower, Paris"
35
+
36
+ # build from a config file
37
+ node src/cli.js build-config examples/colosseum.json
38
+ ```
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ git clone https://github.com/eoinjordan/doom-osm-godmode.git
44
+ cd doom-osm-godmode
45
+ npm test # verify everything works
46
+ npm run demo # generate a WAD from the bundled demo site
47
+ ```
48
+
49
+ Or install globally from npm:
50
+
51
+ ```bash
52
+ npx doom-osm-godmode build-place "Colosseum, Rome" --radius 450
53
+ ```
54
+
55
+ No `npm install` needed — there are no dependencies.
56
+
57
+
58
+ Start the map on GZDOOM or Ultimate on Windows:
59
+
60
+ ```
61
+ "C:\Games\GZDoom\gzdoom.exe" -iwad "C:\Program Files (x86)\Steam\steamapps\common\Ultimate Doom\base\doom2\DOOM2.WAD" -file "C:\Users\Eoin\git\doom-osm-wad\output\galway-cathedral-v3\MAP01.wad" +map MAP01
62
+ ```
63
+
64
+ <img width="899" height="689" alt="image" src="https://github.com/user-attachments/assets/894904fe-5e10-4226-ad60-72ff50e7b9ce" />
65
+
66
+ ## Screenshots
67
+
68
+ <p align="center">
69
+ <img src="https://raw.githubusercontent.com/eoinjordan/doom-osm-godmode/main/assets/screens/outside.jpg" alt="Galway Cathedral outside" width="400"/>
70
+ <img src="https://raw.githubusercontent.com/eoinjordan/doom-osm-godmode/main/assets/screens/inside.jpg" alt="Galway Cathedral inside" width="400"/>
71
+ </p>
72
+
73
+ <p align="center"><em>Galway Cathedral hand-crafted map. Some visual clipping bugs are known and will be addressed in future releases.</em></p>
74
+
75
+ ## CLI Reference
76
+
77
+ ```
78
+ doom-osm-godmode
79
+
80
+ Commands:
81
+ search <query> Search Nominatim for a place
82
+ build-place <query> [options] Geocode + OSM fetch + WAD export
83
+ build-config <config.json> Build from a JSON config file
84
+ demo [--out output/demo-site] Build the bundled demo WAD
85
+
86
+ Options for build-place:
87
+ --radius <meters> Override bounding box with a fixed radius (default: use Nominatim bbox)
88
+ --map <MAPXX> Map lump name (default: MAP01)
89
+ --max-rooms <count> Cap on rooms generated (default: 12)
90
+ --title <name> Override the map title
91
+ --out <dir> Output directory (default: output/<slugified-title>)
92
+ ```
93
+
94
+ ## Programmatic API
95
+
96
+ ```javascript
97
+ const { buildFromPlace, buildFromConfig, buildDemo } = require('doom-osm-godmode');
98
+
99
+ // from a place name (hits Nominatim + Overpass)
100
+ const result = await buildFromPlace('Sydney Opera House', {
101
+ radiusMeters: 500,
102
+ mapName: 'MAP01',
103
+ maxRooms: 10,
104
+ outputDir: './output/sydney'
105
+ });
106
+
107
+ // from a config file (place query or offline GeoJSON)
108
+ const result = await buildFromConfig('examples/colosseum.json');
109
+
110
+ // bundled demo (no network)
111
+ const result = await buildDemo('./output/demo');
112
+ ```
113
+
114
+ All functions return `{ outputDir, mapPath, featureCount, exportedFeatures }`.
115
+
116
+ ## Output
117
+
118
+ Each build writes a folder containing:
119
+
120
+ | File | Description |
121
+ |------|-------------|
122
+ | `site.json` | Normalized feature set with projected metrics |
123
+ | `layout.json` | Room/corridor grid layout used for WAD generation |
124
+ | `TEXTMAP.udmf` | Human-readable UDMF map geometry |
125
+ | `MAPINFO.txt` | GZDoom map metadata (sky, music, title) |
126
+ | `<MAPXX>.wad` | Packaged PWAD — load this in GZDoom |
127
+
128
+ ## Config Format
129
+
130
+ Place query (network):
131
+
132
+ ```json
133
+ {
134
+ "title": "Colosseum District",
135
+ "mapName": "MAP01",
136
+ "placeQuery": "Colosseum, Rome, Italy",
137
+ "radiusMeters": 450,
138
+ "maxRooms": 10,
139
+ "outputDir": "../output/colosseum"
140
+ }
141
+ ```
142
+
143
+ Offline GeoJSON:
144
+
145
+ ```json
146
+ {
147
+ "title": "Demo Waterfront",
148
+ "mapName": "MAP01",
149
+ "inputGeoJson": "demo-site.geojson",
150
+ "maxRooms": 8,
151
+ "outputDir": "../output/demo-site"
152
+ }
153
+ ```
154
+
155
+ Relative paths in configs resolve from the config file's directory.
156
+
157
+ ## Architecture
158
+
159
+ ```
160
+ place query / GeoJSON
161
+
162
+
163
+ geocode.js ──► Nominatim API
164
+
165
+
166
+ osm.js ────► Overpass API (polygon features)
167
+
168
+
169
+ feature-set.js project to local coords, filter, sort
170
+
171
+
172
+ layout.js grid-based room placement + corridors + theming
173
+
174
+
175
+ udmf.js UDMF vertices, linedefs, sidedefs, sectors, things
176
+
177
+
178
+ wad.js binary PWAD packaging
179
+
180
+
181
+ MAP01.wad ready for GZDoom
182
+ ```
183
+
184
+ ### Key Source Files
185
+
186
+ | File | Purpose |
187
+ |------|---------|
188
+ | `src/cli.js` | CLI entry point and command router |
189
+ | `src/workflow.js` | Build pipeline orchestrator |
190
+ | `src/geocode.js` | Nominatim geocoding |
191
+ | `src/osm.js` | Overpass polygon fetching |
192
+ | `src/feature-set.js` | Feature normalization and projection |
193
+ | `src/geo.js` | Mercator projection, bbox, polygon math |
194
+ | `src/layout.js` | Grid room placement, corridors, theming |
195
+ | `src/udmf.js` | UDMF text map generation |
196
+ | `src/wad.js` | PWAD binary packing |
197
+ | `src/lib/cli.js` | Argument parser |
198
+ | `src/lib/fs.js` | File I/O utilities |
199
+
200
+ ### Room Theming
201
+
202
+ Features are themed by kind:
203
+
204
+ | Kind | Floor | Ceiling | Light | Notes |
205
+ |------|-------|---------|-------|-------|
206
+ | building | stone | flat | 152 | Default indoor |
207
+ | water | nukage | flat | 144 | Floor at -24 |
208
+ | park | grass | sky | 176 | Outdoor feel |
209
+ | road | light stone | flat | 160 | Corridors |
210
+
211
+ ## Testing
212
+
213
+ ```bash
214
+ npm test # smoke tests via node:test
215
+ npm run check # syntax-check all source files
216
+ npm run validate # structural WAD/UDMF validation (builds + verifies integrity)
217
+ ```
218
+
219
+ ## Agent Playtest via doom-mcp
220
+
221
+ Generated WADs can be playtested by an AI agent using [doom-mcp](https://github.com/gunnargrosch/doom-mcp) a Rust MCP server that embeds the real DOOM engine.
222
+
223
+ ### Quick setup
224
+
225
+ The repo includes a `.mcp.json` that configures doom-mcp to load your demo WAD:
226
+
227
+ ```bash
228
+ # 1. Build a WAD first
229
+ npm run demo
230
+
231
+ # 2. Playtest it (automated agent navigation sequence)
232
+ npm run playtest -- output/demo-site/MAP01.wad
233
+
234
+ # 3. Or playtest a real-place WAD
235
+ node src/cli.js build-place "Colosseum" --radius 450
236
+ npm run playtest -- output/colosseum/MAP01.wad --skill 1 --ticks 300
237
+ ```
238
+
239
+ ### What the playtest validates
240
+
241
+ The automated playtest spawns doom-mcp, loads the WAD, and runs through a navigation sequence:
242
+
243
+ - **Game starts** — doom engine accepts the WAD
244
+ - **Player spawns** — HP > 0 at spawn point
245
+ - **Player can move** — position changes across multiple actions
246
+ - **Player survives** — doesn't die during baby-difficulty walkthrough
247
+
248
+ ### Using doom-mcp interactively
249
+
250
+ For interactive agent playtesting (e.g., in Claude Code or Cursor):
251
+
252
+ ```json
253
+ {
254
+ "mcpServers": {
255
+ "doom": {
256
+ "type": "stdio",
257
+ "command": "npx",
258
+ "args": ["-y", "doom-mcp"],
259
+ "env": {
260
+ "DOOM_WAD_PATH": "/path/to/your/MAP01.wad"
261
+ }
262
+ }
263
+ }
264
+ }
265
+ ```
266
+
267
+ Then tell the agent: *"Play this DOOM level and tell me if it's fun"*
268
+
269
+ ### CI integration
270
+
271
+ The `playtest.yml` workflow runs structural validation on every tag push and can be triggered manually for any place:
272
+
273
+ ```
274
+ Actions Playtest Run workflow Enter place name Go
275
+ ```
276
+
277
+ ## Playing the WAD
278
+
279
+ ```bash
280
+ # GZDoom (adjust path to your install)
281
+ gzdoom -file output/colosseum/MAP01.wad
282
+ ```
283
+
284
+ The WAD is a PWAD it layers on top of any IWAD (DOOM2.WAD, FREEDOOM2.WAD, etc.).
285
+
286
+ ## Hand-Crafted Buildings
287
+
288
+ The OSM pipeline creates grid-based rooms. For architecturally accurate buildings (cathedrals, temples, etc.), use a hand-crafted script:
289
+
290
+ ```bash
291
+ # Build the Galway Cathedral demo (cross-shaped plan, green copper dome, grand nave)
292
+ node scripts/build-galway-cathedral.js
293
+
294
+ # Launch in GZDoom
295
+ gzdoom -iwad DOOM2.WAD -file output/galway-cathedral-v3/MAP01.wad +map MAP01
296
+ ```
297
+
298
+ The cathedral builder demonstrates the `UDMFBuilder` class with polygon-based sector creation, grand interior aisle, raised chancel, self-validation, and exterior forecourt. Use it as a template for new hand-crafted maps.
299
+
300
+ **Example maps included:**
301
+
302
+ | Map | Type | Script / Command |
303
+ |-----|------|------------------|
304
+ | Galway Cathedral | Hand-crafted | `node scripts/build-galway-cathedral.js` |
305
+ | Colosseum, Rome | OSM / live | `node src/cli.js build-place "Colosseum, Rome, Italy" --radius 450` |
306
+ | Eiffel Tower, Paris | OSM / live | `node src/cli.js build-place "Eiffel Tower, Paris" --radius 450` |
307
+ | Demo Waterfront | Offline GeoJSON | `npm run demo` |
308
+
309
+ ### Validate any WAD
310
+
311
+ ```bash
312
+ # Structural UDMF diagnostic (checks for duplicate linedefs, etc.)
313
+ node scripts/diagnose-udmf.js output/galway-cathedral-v3/TEXTMAP.udmf
314
+
315
+ # Agent playtest via doom-mcp
316
+ npm run playtest -- output/galway-cathedral-v3/MAP01.wad
317
+ ```
318
+
319
+ ## UDMF Geometry Rules
320
+
321
+ Common issues found when generating DOOM maps from real-world data:
322
+
323
+ | Issue | Symptom | Fix |
324
+ |-------|---------|-----|
325
+ | Duplicate linedefs (same vertex pair) | Flickering/transparent walls | Use a single polygon for complex shapes; deduplicate in `addInnerSector` |
326
+ | Collinear overlapping linedefs | See-through walls, BSP errors | Never let two linedefs share the same 2D line segment |
327
+ | Inner sector outside parent bounds | Rendering void, hall-of-mirrors | Ensure all inner sector vertices are strictly inside the outer polygon |
328
+ | Separate rooms at same crossing | Duplicate shared edges | Use ONE polygon for the union shape (e.g., a cross), not overlapping rectangles |
329
+ | Underground areas sharing x,y with upper | DOOM 2.5D sector conflict | Place underground areas at different x,y coords; connect via teleporter |
330
+ | Invisible walls on two-sided linedefs | Side walls transparent where floor heights match | Set `wrapmidtex = true` + `blocking = true` + `texturemiddle` on the two-sided linedef |
331
+ | Floating mid-textures on promoted linedefs | Choppy half-height walls | Clear `texturemiddle` when promoting one-sided to two-sided; use upper/lower instead |
332
+ | Zero-height pillar sectors (floor = ceiling) | Player stuck, node builder errors | Use Thing-based pillars (type 30) instead of sector-based pillars |
333
+
334
+ ### Repeatable pattern for future OSM meshes
335
+
336
+ 1. Run `node src/cli.js build-place ...` for the grid-room WAD
337
+ 2. Run `node scripts/diagnose-udmf.js output/<name>/TEXTMAP.udmf` to check geometry
338
+ 3. Run `npm run playtest -- output/<name>/MAP01.wad` for engine validation
339
+ 4. If the grid rooms are too abstract, create a hand-crafted script (see `scripts/build-galway-cathedral.js`)
340
+ 5. Always run self-validation before writing the WAD
341
+
342
+ ## Notes
343
+
344
+ - Network commands use public OSM infrastructure (Nominatim, Overpass). Keep requests reasonable.
345
+ - Only polygon features become rooms. Roads and linear features are not yet carved into sectors.
346
+ - The layout is a blockout interpretation — connected rooms reflecting real feature hierarchy, not pixel-perfect street geometry.
347
+
348
+ ## Roadmap
349
+
350
+ - exact polygon-to-sector carving (replace grid rooms with real building outlines)
351
+ - road and canal line buffering into traversable sectors
352
+ - Doom encounter placement (monsters, ammo, weapons)
353
+ - texture themes per location style
354
+ - multi-map episode generation
355
+ - freeform agent-described layouts (no OSM needed)
356
+ - automatic duplicate linedef detection in the OSM pipeline
357
+
358
+ ## Contributing
359
+
360
+ PRs welcome. Run `npm test` and `npm run check` before submitting.
361
+
362
+ ## License
363
+
364
+ MIT — see [LICENSE](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doom-osm-godmode",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "DOOM OSM GODMODE — Turn any real-world place into a playable GZDoom WAD. Geocode → OSM → UDMF → PWAD. Zero dependencies.",
5
5
  "type": "commonjs",
6
6
  "main": "src/workflow.js",
@@ -317,53 +317,51 @@ function buildCathedral() {
317
317
  const b = new UDMFBuilder();
318
318
 
319
319
  // ── Dimensions (map units) ──
320
- const NAVE_HW = 192; // nave half-width
321
- const NAVE_LEN = 1024; // nave length south of crossing
322
- const CHANCEL_LEN = 640; // chancel length north of crossing
323
- const TRANS_HW = 192; // transept half-width (same as nave)
324
- const TRANS_LEN = 512; // transept arm length each side
325
- const DOME_R = 256; // dome radius
320
+ const NAVE_HW = 256; // nave half-width
321
+ const NAVE_LEN = 1408; // nave length south of crossing
322
+ const CHANCEL_LEN = 896; // chancel length north of crossing
323
+ const TRANS_HW = 256; // transept half-width (same as nave)
324
+ const TRANS_LEN = 704; // transept arm length each side
325
+ const DOME_R = 320; // dome radius
326
326
  const SPIRE_R = 48; // spire radius
327
- const TOWER_SIZE = 192; // entrance tower footprint
327
+ const TOWER_SIZE = 256; // entrance tower footprint
328
328
 
329
329
  // Heights
330
330
  const FLOOR = 0;
331
- const NAVE_CEIL = 256;
332
- const DOME_CEIL = 384;
333
- const SPIRE_CEIL = 512;
334
- const TOWER_CEIL = 320;
331
+ const NAVE_CEIL = 384;
332
+ const DOME_CEIL = 544;
333
+ const SPIRE_CEIL = 672;
334
+ const TOWER_CEIL = 448;
335
335
  // Sky ceiling should match the tallest directly-adjacent non-sky sector.
336
336
  // F_SKY1 renders as sky regardless of height, so the numeric value
337
337
  // only affects upper-texture gap sizing. Matching eliminates artifacts.
338
- const CARPARK_CEIL = 320;
339
- const CATA_FLOOR = -192;
340
- const CATA_CEIL = -16;
338
+ const CARPARK_CEIL = 448;
339
+ const CARPARK_FLOOR = -16;
341
340
 
342
341
  // Textures
343
342
  const STONE = 'STONE2';
344
- const BIGSTONE = 'BIGBRIK1';
343
+ const BIGSTONE = 'STONE3';
345
344
  const MARBLE_F = 'FLAT1';
346
345
  const CEIL_INT = 'CEIL1_1';
347
346
  const SKY = 'F_SKY1';
348
347
  const FLAT_GREY = 'FLAT5_4';
349
348
  const DOME_TEX = 'CEIL5_2';
350
349
  const METAL = 'METAL';
351
- const COPPER_W = 'BROWNGRN';
350
+ const COPPER_W = 'GRAY7';
352
351
  const TILE_F = 'FLOOR0_1';
353
- const CATA_WALL = 'STONE3';
354
- const CATA_FLOOR_T = 'FLOOR7_1';
352
+ const AISLE_F = 'FLOOR4_8';
355
353
 
356
354
  // ════════════════════════════════════════════════════════════
357
355
  // 1. CARPARK (outer boundary, sky ceiling)
358
356
  // ════════════════════════════════════════════════════════════
359
- const CP = 3200;
357
+ const CP = 4608;
360
358
  const carparkSector = b.addPolySector([
361
359
  [-CP / 2, -CP / 2],
362
360
  [CP / 2, -CP / 2],
363
361
  [CP / 2, CP / 2],
364
362
  [-CP / 2, CP / 2]
365
363
  ], {
366
- floorH: FLOOR, ceilH: CARPARK_CEIL,
364
+ floorH: CARPARK_FLOOR, ceilH: CARPARK_CEIL,
367
365
  floorTex: FLAT_GREY, ceilTex: SKY, light: 192
368
366
  }, STONE);
369
367
 
@@ -407,6 +405,38 @@ function buildCathedral() {
407
405
  }, carparkSector, BIGSTONE);
408
406
  const crossLdEnd = b.linedefs.length;
409
407
 
408
+ // Center aisle to give the nave a more ceremonial, grand feel.
409
+ b.addInnerSector([
410
+ [-72, -NAVE_LEN + 48],
411
+ [72, -NAVE_LEN + 48],
412
+ [72, CHANCEL_LEN - 80],
413
+ [-72, CHANCEL_LEN - 80]
414
+ ], {
415
+ floorH: FLOOR + 4, ceilH: NAVE_CEIL,
416
+ floorTex: AISLE_F, ceilTex: CEIL_INT, light: 188
417
+ }, crossSector, BIGSTONE);
418
+
419
+ // Side aisles for depth and visual layering inside the nave.
420
+ b.addInnerSector([
421
+ [-NAVE_HW + 24, -NAVE_LEN + 96],
422
+ [-NAVE_HW + 116, -NAVE_LEN + 96],
423
+ [-NAVE_HW + 116, CHANCEL_LEN - 128],
424
+ [-NAVE_HW + 24, CHANCEL_LEN - 128]
425
+ ], {
426
+ floorH: FLOOR + 2, ceilH: NAVE_CEIL,
427
+ floorTex: TILE_F, ceilTex: CEIL_INT, light: 174
428
+ }, crossSector, BIGSTONE);
429
+
430
+ b.addInnerSector([
431
+ [NAVE_HW - 116, -NAVE_LEN + 96],
432
+ [NAVE_HW - 24, -NAVE_LEN + 96],
433
+ [NAVE_HW - 24, CHANCEL_LEN - 128],
434
+ [NAVE_HW - 116, CHANCEL_LEN - 128]
435
+ ], {
436
+ floorH: FLOOR + 2, ceilH: NAVE_CEIL,
437
+ floorTex: TILE_F, ceilTex: CEIL_INT, light: 174
438
+ }, crossSector, BIGSTONE);
439
+
410
440
  // Make cross walls SOLID with wrapmidtex.
411
441
  // In DOOM, two-sided linedefs between same-floor sectors are transparent.
412
442
  // wrapmidtex tiles the mid-texture to fill the full height = opaque wall.
@@ -541,87 +571,27 @@ function buildCathedral() {
541
571
  }
542
572
 
543
573
  // ════════════════════════════════════════════════════════════
544
- // 9. TELEPORTER PADin east transept, sends to catacombs
545
- // ════════════════════════════════════════════════════════════
546
- const TELE_TID_DOWN = 1;
547
- const TELE_TID_UP = 2;
548
-
549
- const telePadSector = b.addInnerSector([
550
- [NAVE_HW + TRANS_LEN - 128, -64],
551
- [NAVE_HW + TRANS_LEN - 32, -64],
552
- [NAVE_HW + TRANS_LEN - 32, 64],
553
- [NAVE_HW + TRANS_LEN - 128, 64]
554
- ], {
555
- floorH: FLOOR + 8, ceilH: NAVE_CEIL,
556
- floorTex: 'GATE4', ceilTex: CEIL_INT, light: 255
557
- }, crossSector, METAL);
558
-
559
- // Teleport linedef across the middle of the pad (walk-over)
560
- b.addSpecialLinedef(
561
- [NAVE_HW + TRANS_LEN - 80, -64],
562
- [NAVE_HW + TRANS_LEN - 80, 64],
563
- telePadSector, telePadSector,
564
- 70, // Teleport
565
- [TELE_TID_DOWN, 0, 1],
566
- '-'
567
- );
568
-
569
- // ════════════════════════════════════════════════════════════
570
- // 10. CATACOMBS — separate area east of cathedral (x=1200+)
574
+ // 9. EXTERIOR PLAZAfront forecourt with broad approach
571
575
  // ════════════════════════════════════════════════════════════
572
- const CATA_X = 1200;
573
- const CATA_Y = 0;
574
- const CATA_W = 768;
575
- const CATA_H = 512;
576
-
577
- const cataSector = b.addPolySector([
578
- [CATA_X - CATA_W / 2, CATA_Y - CATA_H / 2],
579
- [CATA_X + CATA_W / 2, CATA_Y - CATA_H / 2],
580
- [CATA_X + CATA_W / 2, CATA_Y + CATA_H / 2],
581
- [CATA_X - CATA_W / 2, CATA_Y + CATA_H / 2]
576
+ b.addInnerSector([
577
+ [-512, -NAVE_LEN - 320],
578
+ [512, -NAVE_LEN - 320],
579
+ [512, -NAVE_LEN - 64],
580
+ [-512, -NAVE_LEN - 64]
582
581
  ], {
583
- floorH: CATA_FLOOR, ceilH: CATA_CEIL,
584
- floorTex: CATA_FLOOR_T, ceilTex: 'CEIL3_3', light: 96
585
- }, CATA_WALL);
586
-
587
- // Catacomb side chambers (4 alcoves)
588
- const chW = 192, chH = 128;
589
- for (let i = -1; i <= 1; i += 2) {
590
- for (let j = -1; j <= 1; j += 2) {
591
- const cx = CATA_X + i * 200;
592
- const cy = CATA_Y + j * 128;
593
- b.addInnerSector([
594
- [cx - chW / 2, cy - chH / 2],
595
- [cx + chW / 2, cy - chH / 2],
596
- [cx + chW / 2, cy + chH / 2],
597
- [cx - chW / 2, cy + chH / 2]
598
- ], {
599
- floorH: CATA_FLOOR, ceilH: CATA_CEIL,
600
- floorTex: CATA_FLOOR_T, ceilTex: 'CEIL3_3', light: 80
601
- }, cataSector, CATA_WALL);
602
- }
603
- }
582
+ floorH: CARPARK_FLOOR + 8, ceilH: CARPARK_CEIL,
583
+ floorTex: 'FLOOR5_1', ceilTex: SKY, light: 200
584
+ }, carparkSector, STONE);
604
585
 
605
- // Return teleporter pad in catacombs
606
- const cataReturnSector = b.addInnerSector([
607
- [CATA_X - 48, CATA_Y + CATA_H / 2 - 96],
608
- [CATA_X + 48, CATA_Y + CATA_H / 2 - 96],
609
- [CATA_X + 48, CATA_Y + CATA_H / 2 - 32],
610
- [CATA_X - 48, CATA_Y + CATA_H / 2 - 32]
586
+ b.addInnerSector([
587
+ [-300, -NAVE_LEN - 64],
588
+ [300, -NAVE_LEN - 64],
589
+ [300, -NAVE_LEN],
590
+ [-300, -NAVE_LEN]
611
591
  ], {
612
- floorH: CATA_FLOOR + 8, ceilH: CATA_CEIL,
613
- floorTex: 'GATE4', ceilTex: 'CEIL3_3', light: 255
614
- }, cataSector, METAL);
615
-
616
- // Return teleport linedef
617
- b.addSpecialLinedef(
618
- [CATA_X, CATA_Y + CATA_H / 2 - 96],
619
- [CATA_X, CATA_Y + CATA_H / 2 - 32],
620
- cataReturnSector, cataReturnSector,
621
- 70,
622
- [TELE_TID_UP, 0, 1],
623
- '-'
624
- );
592
+ floorH: FLOOR - 4, ceilH: CARPARK_CEIL,
593
+ floorTex: 'MFLR8_2', ceilTex: SKY, light: 208
594
+ }, carparkSector, STONE);
625
595
 
626
596
  // ════════════════════════════════════════════════════════════
627
597
  // THINGS
@@ -630,17 +600,20 @@ function buildCathedral() {
630
600
  // Player start — in nave, facing north toward altar
631
601
  b.addThing(0, -NAVE_LEN + 256, 1, 90);
632
602
 
633
- // Teleport destinations
634
- b.addThing(CATA_X, CATA_Y, 14, 90, { tid: TELE_TID_DOWN });
635
- b.addThing(NAVE_HW + TRANS_LEN - 160, 0, 14, 270, { tid: TELE_TID_UP });
636
-
637
603
  // Candelabras in nave
638
- for (let i = 0; i < 5; i++) {
639
- const ty = -NAVE_LEN + 200 + i * 160;
604
+ for (let i = 0; i < 9; i++) {
605
+ const ty = -NAVE_LEN + 220 + i * 190;
640
606
  b.addThing(-NAVE_HW + 64, ty, 35, 0);
641
607
  b.addThing(NAVE_HW - 64, ty, 35, 0);
642
608
  }
643
609
 
610
+ // Mid-aisle lights for the grand interior rhythm.
611
+ for (let i = 0; i < 7; i++) {
612
+ const ty = -NAVE_LEN + 300 + i * 220;
613
+ b.addThing(-40, ty, 44, 0);
614
+ b.addThing(40, ty, 44, 0);
615
+ }
616
+
644
617
  // Torches in transepts
645
618
  b.addThing(-NAVE_HW - TRANS_LEN + 64, 0, 46, 0);
646
619
  b.addThing(NAVE_HW + TRANS_LEN - 160, 0, 46, 0);
@@ -657,19 +630,26 @@ function buildCathedral() {
657
630
  b.addThing(-NAVE_HW - 32, -NAVE_LEN - 32, 46, 0);
658
631
  b.addThing(NAVE_HW + 32, -NAVE_LEN - 32, 46, 0);
659
632
 
660
- // Catacomb torches
661
- for (let i = -1; i <= 1; i += 2) {
662
- for (let j = -1; j <= 1; j += 2) {
663
- b.addThing(CATA_X + i * 200, CATA_Y + j * 128, 44, 0);
664
- }
633
+ // Plaza lights and markers for exterior style.
634
+ for (let x = -420; x <= 420; x += 140) {
635
+ b.addThing(x, -NAVE_LEN - 220, 46, 0);
665
636
  }
666
637
 
667
- // Monsters in catacombs
668
- b.addThing(CATA_X - 200, CATA_Y - 128, 3001, 0);
669
- b.addThing(CATA_X + 200, CATA_Y + 128, 3001, 0);
670
- b.addThing(CATA_X - 200, CATA_Y + 128, 3002, 0);
671
- b.addThing(CATA_X + 200, CATA_Y - 128, 3002, 0);
672
- b.addThing(CATA_X, CATA_Y, 3003, 0);
638
+ for (let x = -560; x <= 560; x += 280) {
639
+ b.addThing(x, -NAVE_LEN - 460, 35, 0);
640
+ }
641
+
642
+ // Interior encounters and cast.
643
+ b.addThing(-120, -360, 3001, 0);
644
+ b.addThing(120, -280, 3001, 0);
645
+ b.addThing(-180, 140, 3002, 0);
646
+ b.addThing(180, 180, 3002, 0);
647
+ b.addThing(0, CHANCEL_LEN - 210, 3003, 180);
648
+
649
+ // Exterior encounters around the forecourt.
650
+ b.addThing(-340, -NAVE_LEN - 280, 3001, 90);
651
+ b.addThing(340, -NAVE_LEN - 280, 3001, 270);
652
+ b.addThing(0, -NAVE_LEN - 500, 3002, 90);
673
653
 
674
654
  // Weapons and items
675
655
  b.addThing(0, -NAVE_LEN + 64, 2001, 0); // shotgun at entrance
@@ -714,9 +694,9 @@ function main() {
714
694
  title: 'Galway Cathedral',
715
695
  version: 3,
716
696
  mapName: 'MAP01',
717
- description: 'Cross-shaped limestone cathedral with green copper dome, twin towers, teleporter catacombs, and carpark',
718
- features: ['cross polygon', 'dome', 'spire', 'twin towers', 'teleporter catacombs',
719
- 'nave pillars', 'raised chancel', 'altar', 'carpark']
697
+ description: 'Grand cross-shaped limestone cathedral with expanded nave, twin towers, ceremonial aisle, and exterior forecourt',
698
+ features: ['cross polygon', 'dome', 'spire', 'twin towers',
699
+ 'grand aisle', 'side aisles', 'nave pillars', 'raised chancel', 'altar', 'forecourt']
720
700
  }, null, 2);
721
701
 
722
702
  const wadBuffer = createWadBuffer({
@@ -747,7 +727,8 @@ function main() {
747
727
  console.log(' + Nave pillars');
748
728
  console.log(' + Raised chancel + altar');
749
729
  console.log(' + Flat grey carpark exterior');
750
- console.log(' + Catacombs (teleporter from east transept)');
730
+ console.log(' + Grand nave aisle + side aisles');
731
+ console.log(' + Exterior forecourt with approach lights');
751
732
  console.log(' + Self-validation pass');
752
733
 
753
734
  console.log('\n Launch:');