doom-osm-godmode 1.2.0

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ <p align="center">
2
+ <img src="assets/header.png" alt="DOOM OSM GODMODE" width="800"/>
3
+ </p>
4
+
5
+ <p align="center">
6
+ <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>
7
+ <a href="https://www.npmjs.com/package/doom-osm-godmode"><img src="https://img.shields.io/npm/v/doom-osm-godmode?color=cc2200" alt="npm"></a>
8
+ <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node 20+">
9
+ <img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero deps">
10
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="MIT"></a>
11
+ </p>
12
+
13
+ ---
14
+
15
+ 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.
16
+
17
+ Zero npm dependencies. Node 20+ only.
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ # run the bundled demo (offline, no network)
23
+ npm run demo
24
+
25
+ # build a real place
26
+ node src/cli.js build-place "Colosseum, Rome, Italy" --radius 450
27
+
28
+ # search without building
29
+ node src/cli.js search "Eiffel Tower, Paris"
30
+
31
+ # build from a config file
32
+ node src/cli.js build-config examples/colosseum.json
33
+ ```
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ git clone https://github.com/eoinjordan/doom-osm-godmode.git
39
+ cd doom-osm-godmode
40
+ npm test # verify everything works
41
+ npm run demo # generate a WAD from the bundled demo site
42
+ ```
43
+
44
+ Or install globally from npm:
45
+
46
+ ```bash
47
+ npx doom-osm-godmode build-place "Colosseum, Rome" --radius 450
48
+ ```
49
+
50
+ No `npm install` needed — there are no dependencies.
51
+
52
+ ## CLI Reference
53
+
54
+ ```
55
+ doom-osm-godmode
56
+
57
+ Commands:
58
+ search <query> Search Nominatim for a place
59
+ build-place <query> [options] Geocode + OSM fetch + WAD export
60
+ build-config <config.json> Build from a JSON config file
61
+ demo [--out output/demo-site] Build the bundled demo WAD
62
+
63
+ Options for build-place:
64
+ --radius <meters> Override bounding box with a fixed radius (default: use Nominatim bbox)
65
+ --map <MAPXX> Map lump name (default: MAP01)
66
+ --max-rooms <count> Cap on rooms generated (default: 12)
67
+ --title <name> Override the map title
68
+ --out <dir> Output directory (default: output/<slugified-title>)
69
+ ```
70
+
71
+ ## Programmatic API
72
+
73
+ ```javascript
74
+ const { buildFromPlace, buildFromConfig, buildDemo } = require('doom-osm-godmode');
75
+
76
+ // from a place name (hits Nominatim + Overpass)
77
+ const result = await buildFromPlace('Sydney Opera House', {
78
+ radiusMeters: 500,
79
+ mapName: 'MAP01',
80
+ maxRooms: 10,
81
+ outputDir: './output/sydney'
82
+ });
83
+
84
+ // from a config file (place query or offline GeoJSON)
85
+ const result = await buildFromConfig('examples/colosseum.json');
86
+
87
+ // bundled demo (no network)
88
+ const result = await buildDemo('./output/demo');
89
+ ```
90
+
91
+ All functions return `{ outputDir, mapPath, featureCount, exportedFeatures }`.
92
+
93
+ ## Output
94
+
95
+ Each build writes a folder containing:
96
+
97
+ | File | Description |
98
+ |------|-------------|
99
+ | `site.json` | Normalized feature set with projected metrics |
100
+ | `layout.json` | Room/corridor grid layout used for WAD generation |
101
+ | `TEXTMAP.udmf` | Human-readable UDMF map geometry |
102
+ | `MAPINFO.txt` | GZDoom map metadata (sky, music, title) |
103
+ | `<MAPXX>.wad` | Packaged PWAD — load this in GZDoom |
104
+
105
+ ## Config Format
106
+
107
+ Place query (network):
108
+
109
+ ```json
110
+ {
111
+ "title": "Colosseum District",
112
+ "mapName": "MAP01",
113
+ "placeQuery": "Colosseum, Rome, Italy",
114
+ "radiusMeters": 450,
115
+ "maxRooms": 10,
116
+ "outputDir": "../output/colosseum"
117
+ }
118
+ ```
119
+
120
+ Offline GeoJSON:
121
+
122
+ ```json
123
+ {
124
+ "title": "Demo Waterfront",
125
+ "mapName": "MAP01",
126
+ "inputGeoJson": "demo-site.geojson",
127
+ "maxRooms": 8,
128
+ "outputDir": "../output/demo-site"
129
+ }
130
+ ```
131
+
132
+ Relative paths in configs resolve from the config file's directory.
133
+
134
+ ## Architecture
135
+
136
+ ```
137
+ place query / GeoJSON
138
+
139
+
140
+ geocode.js ──► Nominatim API
141
+
142
+
143
+ osm.js ────► Overpass API (polygon features)
144
+
145
+
146
+ feature-set.js project to local coords, filter, sort
147
+
148
+
149
+ layout.js grid-based room placement + corridors + theming
150
+
151
+
152
+ udmf.js UDMF vertices, linedefs, sidedefs, sectors, things
153
+
154
+
155
+ wad.js binary PWAD packaging
156
+
157
+
158
+ MAP01.wad ready for GZDoom
159
+ ```
160
+
161
+ ### Key Source Files
162
+
163
+ | File | Purpose |
164
+ |------|---------|
165
+ | `src/cli.js` | CLI entry point and command router |
166
+ | `src/workflow.js` | Build pipeline orchestrator |
167
+ | `src/geocode.js` | Nominatim geocoding |
168
+ | `src/osm.js` | Overpass polygon fetching |
169
+ | `src/feature-set.js` | Feature normalization and projection |
170
+ | `src/geo.js` | Mercator projection, bbox, polygon math |
171
+ | `src/layout.js` | Grid room placement, corridors, theming |
172
+ | `src/udmf.js` | UDMF text map generation |
173
+ | `src/wad.js` | PWAD binary packing |
174
+ | `src/lib/cli.js` | Argument parser |
175
+ | `src/lib/fs.js` | File I/O utilities |
176
+
177
+ ### Room Theming
178
+
179
+ Features are themed by kind:
180
+
181
+ | Kind | Floor | Ceiling | Light | Notes |
182
+ |------|-------|---------|-------|-------|
183
+ | building | stone | flat | 152 | Default indoor |
184
+ | water | nukage | flat | 144 | Floor at -24 |
185
+ | park | grass | sky | 176 | Outdoor feel |
186
+ | road | light stone | flat | 160 | Corridors |
187
+
188
+ ## Testing
189
+
190
+ ```bash
191
+ npm test # smoke tests via node:test
192
+ npm run check # syntax-check all source files
193
+ npm run validate # structural WAD/UDMF validation (builds + verifies integrity)
194
+ ```
195
+
196
+ ## Agent Playtest via doom-mcp
197
+
198
+ 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.
199
+
200
+ ### Quick setup
201
+
202
+ The repo includes a `.mcp.json` that configures doom-mcp to load your demo WAD:
203
+
204
+ ```bash
205
+ # 1. Build a WAD first
206
+ npm run demo
207
+
208
+ # 2. Playtest it (automated agent navigation sequence)
209
+ npm run playtest -- output/demo-site/MAP01.wad
210
+
211
+ # 3. Or playtest a real-place WAD
212
+ node src/cli.js build-place "Colosseum" --radius 450
213
+ npm run playtest -- output/colosseum/MAP01.wad --skill 1 --ticks 300
214
+ ```
215
+
216
+ ### What the playtest validates
217
+
218
+ The automated playtest spawns doom-mcp, loads the WAD, and runs through a navigation sequence:
219
+
220
+ - **Game starts** — doom engine accepts the WAD
221
+ - **Player spawns** — HP > 0 at spawn point
222
+ - **Player can move** — position changes across multiple actions
223
+ - **Player survives** — doesn't die during baby-difficulty walkthrough
224
+
225
+ ### Using doom-mcp interactively
226
+
227
+ For interactive agent playtesting (e.g., in Claude Code or Cursor):
228
+
229
+ ```json
230
+ {
231
+ "mcpServers": {
232
+ "doom": {
233
+ "type": "stdio",
234
+ "command": "npx",
235
+ "args": ["-y", "doom-mcp"],
236
+ "env": {
237
+ "DOOM_WAD_PATH": "/path/to/your/MAP01.wad"
238
+ }
239
+ }
240
+ }
241
+ }
242
+ ```
243
+
244
+ Then tell the agent: *"Play this DOOM level and tell me if it's fun"*
245
+
246
+ ### CI integration
247
+
248
+ The `playtest.yml` workflow runs structural validation on every tag push and can be triggered manually for any place:
249
+
250
+ ```
251
+ Actions → Playtest → Run workflow → Enter place name → Go
252
+ ```
253
+
254
+ ## Playing the WAD
255
+
256
+ ```bash
257
+ # GZDoom (adjust path to your install)
258
+ gzdoom -file output/colosseum/MAP01.wad
259
+ ```
260
+
261
+ The WAD is a PWAD — it layers on top of any IWAD (DOOM2.WAD, FREEDOOM2.WAD, etc.).
262
+
263
+ ## Hand-Crafted Buildings
264
+
265
+ The OSM pipeline creates grid-based rooms. For architecturally accurate buildings (cathedrals, temples, etc.), use a hand-crafted script:
266
+
267
+ ```bash
268
+ # Build the Galway Cathedral demo (cross-shaped plan, dome, catacombs)
269
+ node scripts/build-galway-cathedral.js
270
+
271
+ # Launch in GZDoom
272
+ gzdoom -iwad DOOM2.WAD -file output/galway-cathedral-v3/MAP01.wad +map MAP01
273
+ ```
274
+
275
+ 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.
276
+
277
+ ### Validate any WAD
278
+
279
+ ```bash
280
+ # Structural UDMF diagnostic (checks for duplicate linedefs, etc.)
281
+ node scripts/diagnose-udmf.js output/galway-cathedral-v3/TEXTMAP.udmf
282
+
283
+ # Agent playtest via doom-mcp
284
+ npm run playtest -- output/galway-cathedral-v3/MAP01.wad
285
+ ```
286
+
287
+ ## UDMF Geometry Rules
288
+
289
+ Common issues found when generating DOOM maps from real-world data:
290
+
291
+ | Issue | Symptom | Fix |
292
+ |-------|---------|-----|
293
+ | Duplicate linedefs (same vertex pair) | Flickering/transparent walls | Use a single polygon for complex shapes; deduplicate in `addInnerSector` |
294
+ | Collinear overlapping linedefs | See-through walls, BSP errors | Never let two linedefs share the same 2D line segment |
295
+ | Inner sector outside parent bounds | Rendering void, hall-of-mirrors | Ensure all inner sector vertices are strictly inside the outer polygon |
296
+ | Separate rooms at same crossing | Duplicate shared edges | Use ONE polygon for the union shape (e.g., a cross), not overlapping rectangles |
297
+ | Underground areas sharing x,y with upper | DOOM 2.5D sector conflict | Place underground areas at different x,y coords; connect via teleporter |
298
+ | Invisible walls on two-sided linedefs | Side walls transparent where floor heights match | Set `wrapmidtex = true` + `blocking = true` + `texturemiddle` on the two-sided linedef |
299
+ | Floating mid-textures on promoted linedefs | Choppy half-height walls | Clear `texturemiddle` when promoting one-sided to two-sided; use upper/lower instead |
300
+ | Zero-height pillar sectors (floor = ceiling) | Player stuck, node builder errors | Use Thing-based pillars (type 30) instead of sector-based pillars |
301
+
302
+ ### Repeatable pattern for future OSM meshes
303
+
304
+ 1. Run `node src/cli.js build-place ...` for the grid-room WAD
305
+ 2. Run `node scripts/diagnose-udmf.js output/<name>/TEXTMAP.udmf` to check geometry
306
+ 3. Run `npm run playtest -- output/<name>/MAP01.wad` for engine validation
307
+ 4. If the grid rooms are too abstract, create a hand-crafted script (see `scripts/build-galway-cathedral.js`)
308
+ 5. Always run self-validation before writing the WAD
309
+
310
+ ## Notes
311
+
312
+ - Network commands use public OSM infrastructure (Nominatim, Overpass). Keep requests reasonable.
313
+ - Only polygon features become rooms. Roads and linear features are not yet carved into sectors.
314
+ - The layout is a blockout interpretation — connected rooms reflecting real feature hierarchy, not pixel-perfect street geometry.
315
+
316
+ ## Roadmap
317
+
318
+ - exact polygon-to-sector carving (replace grid rooms with real building outlines)
319
+ - road and canal line buffering into traversable sectors
320
+ - Doom encounter placement (monsters, ammo, weapons)
321
+ - texture themes per location style
322
+ - multi-map episode generation
323
+ - freeform agent-described layouts (no OSM needed)
324
+ - automatic duplicate linedef detection in the OSM pipeline
325
+
326
+ ## Contributing
327
+
328
+ PRs welcome. Run `npm test` and `npm run check` before submitting.
329
+
330
+ ## License
331
+
332
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,8 @@
1
+ {
2
+ "title": "Colosseum District",
3
+ "mapName": "MAP01",
4
+ "placeQuery": "Colosseum, Rome, Italy",
5
+ "radiusMeters": 450,
6
+ "maxRooms": 10,
7
+ "outputDir": "../output/colosseum"
8
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "properties": {
7
+ "name": "Harbor Hall",
8
+ "kind": "building"
9
+ },
10
+ "geometry": {
11
+ "type": "Polygon",
12
+ "coordinates": [
13
+ [
14
+ [12.49205, 41.88985],
15
+ [12.49228, 41.88985],
16
+ [12.49228, 41.89004],
17
+ [12.49205, 41.89004],
18
+ [12.49205, 41.88985]
19
+ ]
20
+ ]
21
+ }
22
+ },
23
+ {
24
+ "type": "Feature",
25
+ "properties": {
26
+ "name": "North Arcade",
27
+ "kind": "building"
28
+ },
29
+ "geometry": {
30
+ "type": "Polygon",
31
+ "coordinates": [
32
+ [
33
+ [12.49234, 41.89012],
34
+ [12.49262, 41.89012],
35
+ [12.49262, 41.89031],
36
+ [12.49234, 41.89031],
37
+ [12.49234, 41.89012]
38
+ ]
39
+ ]
40
+ }
41
+ },
42
+ {
43
+ "type": "Feature",
44
+ "properties": {
45
+ "name": "Garden Court",
46
+ "kind": "park"
47
+ },
48
+ "geometry": {
49
+ "type": "Polygon",
50
+ "coordinates": [
51
+ [
52
+ [12.49200, 41.89019],
53
+ [12.49222, 41.89019],
54
+ [12.49222, 41.89039],
55
+ [12.49200, 41.89039],
56
+ [12.49200, 41.89019]
57
+ ]
58
+ ]
59
+ }
60
+ },
61
+ {
62
+ "type": "Feature",
63
+ "properties": {
64
+ "name": "Reflecting Basin",
65
+ "kind": "water"
66
+ },
67
+ "geometry": {
68
+ "type": "Polygon",
69
+ "coordinates": [
70
+ [
71
+ [12.49240, 41.88978],
72
+ [12.49271, 41.88978],
73
+ [12.49271, 41.88996],
74
+ [12.49240, 41.88996],
75
+ [12.49240, 41.88978]
76
+ ]
77
+ ]
78
+ }
79
+ },
80
+ {
81
+ "type": "Feature",
82
+ "properties": {
83
+ "name": "South Gate",
84
+ "kind": "building"
85
+ },
86
+ "geometry": {
87
+ "type": "Polygon",
88
+ "coordinates": [
89
+ [
90
+ [12.49278, 41.88996],
91
+ [12.49299, 41.88996],
92
+ [12.49299, 41.89015],
93
+ [12.49278, 41.89015],
94
+ [12.49278, 41.88996]
95
+ ]
96
+ ]
97
+ }
98
+ }
99
+ ]
100
+ }
101
+
@@ -0,0 +1,7 @@
1
+ {
2
+ "title": "Demo Waterfront",
3
+ "mapName": "MAP01",
4
+ "inputGeoJson": "demo-site.geojson",
5
+ "maxRooms": 8,
6
+ "outputDir": "../output/demo-site"
7
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "doom-osm-godmode",
3
+ "version": "1.2.0",
4
+ "description": "DOOM OSM GODMODE — Turn any real-world place into a playable GZDoom WAD. Geocode → OSM → UDMF → PWAD. Zero dependencies.",
5
+ "type": "commonjs",
6
+ "main": "src/workflow.js",
7
+ "bin": {
8
+ "doom-osm-godmode": "src/cli.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "examples/",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "check": "node --check src/cli.js && node --check src/feature-set.js && node --check src/geocode.js && node --check src/geo.js && node --check src/layout.js && node --check src/osm.js && node --check src/udmf.js && node --check src/wad.js && node --check src/workflow.js && node --check src/lib/cli.js && node --check src/lib/fs.js",
18
+ "test": "node --test test/*.test.js",
19
+ "demo": "node src/cli.js demo",
20
+ "search": "node src/cli.js search",
21
+ "build:config": "node src/cli.js build-config",
22
+ "build:place": "node src/cli.js build-place",
23
+ "validate": "node --test test/validate-wad.test.js",
24
+ "playtest": "node scripts/playtest-wad.mjs",
25
+ "version": "node -e \"console.log(require('./package.json').version)\"",
26
+ "prepack": "npm run check && npm test"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "keywords": [
32
+ "doom",
33
+ "godmode",
34
+ "iddqd",
35
+ "wad",
36
+ "gzdoom",
37
+ "udmf",
38
+ "openstreetmap",
39
+ "osm",
40
+ "level-generation",
41
+ "procedural-generation",
42
+ "map-generator",
43
+ "mcp"
44
+ ],
45
+ "author": "Eoin",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/eoinjordan/doom-osm-godmode.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/eoinjordan/doom-osm-godmode/issues"
53
+ },
54
+ "homepage": "https://github.com/eoinjordan/doom-osm-godmode#readme"
55
+ }
56
+
package/src/cli.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ const path = require('node:path');
3
+ const { parseArgs, printHelp, readNumberFlag, readStringFlag } = require('./lib/cli.js');
4
+ const { searchPlace } = require('./geocode.js');
5
+ const { buildDemo, buildFromConfig, buildFromPlace } = require('./workflow.js');
6
+
7
+ async function main() {
8
+ const args = parseArgs(process.argv.slice(2));
9
+ const [command, ...rest] = args._;
10
+
11
+ if (!command || command === 'help' || command === '--help') {
12
+ printHelp();
13
+ return;
14
+ }
15
+
16
+ if (command === 'search') {
17
+ const query = rest.join(' ').trim();
18
+ if (!query) {
19
+ throw new Error('search requires a place query.');
20
+ }
21
+
22
+ const results = await searchPlace(query, 5);
23
+ console.log(JSON.stringify(results, null, 2));
24
+ return;
25
+ }
26
+
27
+ if (command === 'build-config') {
28
+ const configPath = rest[0];
29
+ if (!configPath) {
30
+ throw new Error('build-config requires a config file path.');
31
+ }
32
+
33
+ const result = await buildFromConfig(configPath);
34
+ console.log(JSON.stringify(result, null, 2));
35
+ return;
36
+ }
37
+
38
+ if (command === 'build-place') {
39
+ const query = rest.join(' ').trim();
40
+ if (!query) {
41
+ throw new Error('build-place requires a place query.');
42
+ }
43
+
44
+ const result = await buildFromPlace(query, {
45
+ radiusMeters: readNumberFlag(args, 'radius', undefined),
46
+ mapName: readStringFlag(args, 'map', 'MAP01'),
47
+ maxRooms: readNumberFlag(args, 'max-rooms', 12),
48
+ title: readStringFlag(args, 'title', undefined),
49
+ outputDir: args.out ? path.resolve(args.out) : undefined
50
+ });
51
+
52
+ console.log(JSON.stringify(result, null, 2));
53
+ return;
54
+ }
55
+
56
+ if (command === 'demo') {
57
+ const result = await buildDemo(args.out ? path.resolve(args.out) : undefined);
58
+ console.log(JSON.stringify(result, null, 2));
59
+ return;
60
+ }
61
+
62
+ throw new Error(`Unknown command "${command}".`);
63
+ }
64
+
65
+ main().catch((error) => {
66
+ console.error(error.message);
67
+ process.exitCode = 1;
68
+ });