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 +364 -341
- package/package.json +1 -1
- package/scripts/build-galway-cathedral.js +99 -118
package/README.md
CHANGED
|
@@ -1,341 +1,364 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
<img src="
|
|
5
|
-
<img src="
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<p align="center">
|
|
11
|
-
<img src="assets/header.png" alt="DOOM OSM GODMODE" width="800"/>
|
|
12
|
-
</p>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
|
191
|
-
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
#
|
|
215
|
-
npm run
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
#
|
|
281
|
-
gzdoom -
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
The
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
|
303
|
-
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
##
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
@@ -317,53 +317,51 @@ function buildCathedral() {
|
|
|
317
317
|
const b = new UDMFBuilder();
|
|
318
318
|
|
|
319
319
|
// ── Dimensions (map units) ──
|
|
320
|
-
const NAVE_HW =
|
|
321
|
-
const NAVE_LEN =
|
|
322
|
-
const CHANCEL_LEN =
|
|
323
|
-
const TRANS_HW =
|
|
324
|
-
const TRANS_LEN =
|
|
325
|
-
const DOME_R =
|
|
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 =
|
|
327
|
+
const TOWER_SIZE = 256; // entrance tower footprint
|
|
328
328
|
|
|
329
329
|
// Heights
|
|
330
330
|
const FLOOR = 0;
|
|
331
|
-
const NAVE_CEIL =
|
|
332
|
-
const DOME_CEIL =
|
|
333
|
-
const SPIRE_CEIL =
|
|
334
|
-
const TOWER_CEIL =
|
|
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 =
|
|
339
|
-
const
|
|
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 = '
|
|
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 = '
|
|
350
|
+
const COPPER_W = 'GRAY7';
|
|
352
351
|
const TILE_F = 'FLOOR0_1';
|
|
353
|
-
const
|
|
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 =
|
|
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:
|
|
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.
|
|
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 PLAZA — front forecourt with broad approach
|
|
571
575
|
// ════════════════════════════════════════════════════════════
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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:
|
|
584
|
-
floorTex:
|
|
585
|
-
},
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
[
|
|
608
|
-
[
|
|
609
|
-
[
|
|
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:
|
|
613
|
-
floorTex: '
|
|
614
|
-
},
|
|
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 <
|
|
639
|
-
const ty = -NAVE_LEN +
|
|
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
|
-
//
|
|
661
|
-
for (let
|
|
662
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
b.addThing(
|
|
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: '
|
|
718
|
-
features: ['cross polygon', 'dome', 'spire', 'twin towers',
|
|
719
|
-
|
|
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(' +
|
|
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:');
|