Built overnight 6-7 Nov 2026 in about two hours. Live (once the container grant is approved): https://dm-maps.dev.tetrix.dev. Files: /home/admin/engram/work/dm-maps/site/ on DEV02 (index.html links every prototype; a README per area). Everything runs in the browser; no image model was called and nothing was sent anywhere. The project checkout, containers and database were not touched.
The idea throughout: the code generates D&D-logical layouts as data; renderers only draw that data. The same JSON drives the engine's rules (movement, cover, line of sight, light), a 2D canvas, an image-model pass and a 3D scene.
World maps
Noise heightmap, parchment. Simplex fbm with ridges, rivers by flow accumulation from mountains to sea, lakes from filled basins, Whittaker biomes, cities scored by coast/river/flat/fertile, A* roads that merge, bridges and fords. world/noise/Same world, painted atlas. Hillshaded biome colours; every style reads the same stored heightfield. world/noise/?style=paintedSame world, hex. Majority biome per hex; a view, not hex game data. world/noise/?style=hexAI constraints demo. "Mountain range in the north" adds a ridge band; "port city in the south-east" boosts coastal sites there. world/noise/?mountains=north&port=seVoronoi polygons (Azgaar / O'Leary). Own Delaunay, Lloyd relaxation, rivers and roads on the polygon graph. Works, more code for a similar result. world/voronoi/Wave function collapse. Six tiles with adjacency rules: locally consistent, no continents or mountain chains. The wrong tool for a world; useful for local maps. world/wfc/Noise world in 3D. three.js terrain from the same heightfield. world/noise/?style=3d
Data: a world_map root whose representation_data holds the heightfield (elevation and biome as base64 bytes, rivers, crossings), child locations (cities, towns, ruins, dungeons, natural features) as stubs with map_x/map_y, and location_edges (road, trail, river, sea) with travel_cost in hours and curve polylines. Sample: world/noise/sample.json.
Local / tactical maps (5-ft squares)
BSP dungeon, cover overlay. Room roles from the room graph: entrance lowest, lair deepest, a locked treasury as a dead end beside it, guard post next to the entrance. Props by role; torches on walls. Shaded: what the selected square cannot see; yellow/orange: half / three-quarters cover. local/?gen=bsp&cover=1Cave, light and darkness. Cellular automata; the enemy camp sits in the most open spot far from the entry; a pool (deep centre, shallow edge), stalagmites for cover, glowing mushrooms for dim light. local/?gen=cave&light=1Tavern floorplan. Common room, bar with stools and casks, kitchen with back door, locked storeroom with cellar stairs, fireplace, tables with aisles and candles; a street outside. local/?gen=tavernVillage. Roads meet at a square with a well; a stream with a ford; houses next to the road with doors facing it; smithy and chapel; trees in clusters. local/?gen=villageImage-model pass (mocked). Control images from the data (segmentation, line art), a stand-in for the painted result, the backdrop with grid and tokens on top, and the exact ControlNet request. Nothing was sent. local/?style=pipeline3D from the same data: tavern. Walls, floors, doors, props by rule height, real lights from the light sources, Poly Haven CC0 barrels/crates/lanterns, the Quaternius CC0 robot as party token A. local/3d.html?gen=tavern3D: BSP dungeon. Same seed as the 2D dungeon. local/3d.html?gen=bsp3D: village in daylight. Sun shadows, trees, stream. local/3d.html?gen=village
Data (TacticalMap v0): grid_w, grid_h, square_ft, ambient_light, terrain (legend with walk/cost/sight/cover per character, one row string per line of squares), rooms with links (the room graph), doors with state, props whose type keys a rules table (blocks, cover, height, light), lights, spawns. 5-11 KB per map, generated in 10-40 ms. Fits locations.terrain + representation_data + ambient_light; spawns seed combatants.x/y.
Placement rules: a blocking prop is placed only if every walkable square stays reachable (flood fill); squares just inside a door stay empty; no prop beside a door; light sources only in occupied rooms; cover objects in open areas, away from spawn points.
Inventory items
Inventory viewer. 7 Poly Haven CC0 models next to 5 items built in code from parameters (longsword, two potions, scroll, amulet); thumbnails rendered in the browser at load; the item row as JSON. items/A procedural potion. One builder with params (shape, liquid colour, fill) covers every potion. items/?item=potion-greater
Characters
Three routes side by side. (a) Quaternius CC0 rigged robot with 14 animations, expression morphs, class tint, a round VTT token and a figure on a 5-ft square; (b) AI portrait, mocked, with the exact prompt and request; (c) the pose rendered into beauty, depth, normal and edge control images for a ControlNet call, mocked. characters/
Recommendations
World: the noise heightmap (approach 1), run in the browser and saved as JSON into locations + location_edges; parchment style as the default look; the AI names and describes, and steers through constraints. Voronoi is a fine alternative; WFC is not for worlds.
Local: adopt the TacticalMap v0 data shape now and write the engine's line of sight, cover and light against it; port the generators to PHP after phase-1; the generator does geometry, the AI adds meaning. Render 2D first; an image-model backdrop later; 3D only if the game goes 3D.
Items: not urgent. When it comes: procedural builders for potions, scrolls, rings and simple weapons, one stylised CC0 pack (Quaternius or Kenney) for the rest, baked PNG thumbnails.
Characters: AI portraits once an image model is chosen, tokens cropped from them, the first portrait as reference for consistency; 3D bases only alongside 3D maps.
Decisions for you
1 · World map · Should Dungeon Maister generate its world maps (terrain, rivers, places, roads) with the noise-heightmap generator?
A generator makes a heightmap with biomes, rivers, cities, roads and natural features, and writes them as a world_map location with child stubs (map_x/map_y) and location_edges with travel_cost in hours. The AI then names and describes what it placed, and can steer it with constraints such as "mountains in the north, a port in the south-east".
Whether
Yes, adopt the noise generator
When it helps: Worlds get believable geography and real travel times for overland play with no drawing; the same data can be drawn as parchment, painted, hex or 3D.
When it hurts: It adds about 1,000 lines of generator and renderer code to maintain, and the AI no longer has full freedom to put places anywhere.
Edge case: A campaign that starts in one city and never travels gets little from it; a stub-only world is enough there.
No, keep worlds narrative (AI places markers by hand)
When it helps: Nothing to build; the AI keeps full freedom.
When it hurts: There is no map to show, and travel costs are guesses that do not match the geography.
Edge case: Players who want a map to look at will ask for one anyway.
Not now
When it helps: Lets phase-1 and session play settle first; the prototype stays here.
When it hurts: Worlds created in the meantime need a backfill later (the generator can wrap existing places, with some work).
Edge case: The current test campaign's places would have to be fitted onto a generated map afterwards.
Recommendation: Yes, after phase-1 is merged, because the world_map model (node + path, travel_cost on edges) was already designed for exactly this output and the generator fills it with no further art.
How
A. Run in the browser, post JSON
How it works: The JS from world/ becomes a page in the app; "save" posts the locations tree to a Laravel endpoint that writes locations and location_edges in one transaction. About 1-2 evenings.
When it helps: No port; the world-builder page shows seeds and constraints, Jasper or the AI picks one.
When it hurts: An AI working unattended cannot run it without a browser.
Edge case: Large maps make a 100-150 KB JSON per world; fine in jsonb.
B. Port to PHP
How it works: Port noise, flow, biomes, placement and A* to an Engine/World service; the browser only renders the stored data.
When it helps: The AI world-builder can call it as a tool server-side, with no browser.
When it hurts: 2-3 evenings, and two copies of the logic if the browser preview stays.
Edge case: PHP is slower for A* and flow on 240x160; still well under a second.
C. Node sidecar
How it works: A small Node service (or a script the PHP container calls) runs the generator and returns JSON.
When it helps: One JS codebase serves both browser and server.
When it hurts: One more container to run and keep up to date.
Edge case: The AI Bridge already runs Node, so it could host it.
Recommendation: A first, B only once the AI must make worlds unattended, because A gets a playable map fastest and keeps one copy of the code.
2 · Combat maps · Should tactical locations get a generated layout (rooms, doors, props, light, cover) instead of the planned plain 20x20 grid?
On first promotion to tactical, a generator (dungeon, cave, building, village) writes the layout into locations.terrain: terrain rows, doors, props with rules (blocks, cover, light), plus the room graph in representation_data. The engine computes range, line of sight, cover and light from it; renderers only draw it.
Whether
Yes, generate layouts
When it helps: Fights have cover, doors, chokepoints and light, so tactics matter (the campaign setting "treat the fight as a puzzle of space" needs this).
When it hurts: Engine range/LoS/cover must read terrain, and generators need tuning per place type; procedural layouts can feel samey.
Edge case: An improvised place ("a rope bridge over a chasm") fits no generator; it needs a hand-written fallback or AI-placed props on an empty grid.
No, keep the plain grid
When it helps: Simplest; the engine stays pure geometry.
When it hurts: Positioning rarely matters, and cover/light rules stay unused.
Edge case: Theatre-of-the-mind players may not miss it.
Not now
When it helps: Finish the combat engine on the plain grid first.
When it hurts: The terrain format then gets decided later, after the engine assumed an empty grid.
Edge case: Saved encounters on plain grids stay valid either way.
Recommendation: Yes, but agree the data shape (TacticalMap v0 in local/README.md) now even if the generators come later, because the engine's LoS and cover code should be written against it from the start.
How
A. Generators in PHP in the engine
How it works: Port local/gen.js (about 600 lines) to app/Engine/World; write terrain and representation_data on promote; spawns seed combatants.
When it helps: Deterministic, testable with the engine; the AI calls create_location and gets a layout.
When it hurts: 2-3 evenings to port, plus tuning.
Edge case: Seeds must be stored so a layout can be regenerated identically.
B. AI places everything on an empty grid
How it works: The AI emits rooms and props as JSON; the engine validates reachability and door clearance (the same rules as the generator) and rejects bad layouts.
When it helps: Any place the AI can describe, no generator code.
When it hurts: LLMs are bad at grid geometry (the architecture doc says so); blocked doors and unreachable squares need a validator anyway.
Edge case: Small, special set pieces work well this way.
C. Both: generator for the shape, AI for meaning
How it works: The generator makes the layout from a type and parameters the AI chose; the AI annotates rooms and props.
When it helps: Geometry stays correct; the AI names rooms, hides the trap, picks what the chest holds.
When it hurts: Two parts to build.
Edge case: The AI may want a room the generator did not make; it then edits the JSON within the validator's rules.
Recommendation: C, starting with the PHP generators (A) and adding AI annotation, because geometry is exactly what the doc says the engine owns.
3 · Map looks · Should combat maps get more than a flat 2D drawing, and if so which way?
The same layout data can be drawn as plain 2D (canvas), as a painted backdrop made by an image model with the grid and tokens drawn on top, or as a 3D scene in three.js. The rules never depend on the picture.
Whether
Yes, beyond plain 2D
When it helps: The game feels richer; the maps are a big part of the D&D experience.
When it hurts: Costs time (and money for an image model) before the game itself is finished.
Edge case: On a phone, 3D and large painted images are heavy.
No, plain 2D only
When it helps: Cheapest; readable; fast on every device.
When it hurts: It looks like a generator made it.
Edge case: The ProFantasy symbol sets could lift plain 2D without any model, if the licence allows.
Not now
When it helps: Ship 2D first; decide after playing with it.
When it hurts: None really; both upgrades sit on top of the same data.
Edge case: -
Recommendation: Not now: ship the 2D VTT style first, because both upgrades read the same data and can be added later without changing anything stored.
How
A. Image-model backdrop on the 2D map
How it works: Render a segmentation map and line art from the data, send them with a prompt to SDXL/Flux + ControlNet, store the image per location, and draw the grid on top. The page shows the exact request.
When it helps: Painted, unique maps; the grid, tokens and fog stay exact because they are drawn from data.
When it hurts: Costs per map, and the model may paint a door or pillar where the data has none.
Edge case: A layout change needs a new image; key the cache on a hash of the layout.
B. 3D scene from the data
How it works: three.js builds walls, floors, doors and props from the JSON (as in local/3d.html), with a consistent CC0 kit (Kenney or Quaternius) instead of primitives.
When it helps: Lighting, line of sight and height read naturally; one asset kit covers every map.
When it hurts: The most work (assets, camera, performance, readability of the grid), 4+ evenings.
Edge case: Mobile GPUs and old laptops struggle with many lights.
Recommendation: A when you want prettier maps, because it keeps the 2D game and adds art per location; B only if the game becomes a 3D game.
4 · Image model · Should Dungeon Maister use a paid image model (portraits, map backdrops), and which one?
Portraits and painted map backdrops need an image model. None is configured on DEV02 (no GPU), so tonight's pages only show the request they would send. This needs a provider, a key in the project's own secrets, and a spending cap.
Whether
Yes
When it helps: Best-looking portraits and maps for the least work.
When it hurts: Every image costs money and goes to a third party; consistency of a character between images is weak.
Edge case: Other users of the app later would spend your money unless capped per user.
No
When it helps: No cost, no third party.
When it hurts: Characters stay as 3D bases or bought tokens.
Edge case: The ProFantasy tokens could cover NPCs, if the licence allows.
Not now
When it helps: Nothing in the game needs it yet.
When it hurts: -
Edge case: -
Recommendation: Not now; decide when portraits come up in play, because nothing blocks on it and prices move.
How
A. OpenAI gpt-image-1
How it works: One HTTP call from a queued job; store the PNG; crop a token from it.
When it helps: Simplest API, good portraits, can edit with a reference image for consistency.
When it hurts: No ControlNet, so a map backdrop will not line up with the squares.
Edge case: Prices from memory (about $0.01-0.17 per image by quality); verify.
B. Hosted SDXL/Flux with ControlNet (Replicate, fal)
How it works: Send the control images from the data plus a prompt; get an aligned image back.
When it helps: Does both portraits and grid-aligned map backdrops; cheap per image.
When it hurts: More moving parts (workflows, model versions, licences such as Flux dev's non-commercial terms).
Edge case: Cold starts can take tens of seconds.
C. Own GPU with ComfyUI
How it works: ComfyUI workflow on a GPU box reached over Tailscale.
When it helps: No per-image cost; full control.
When it hurts: A GPU machine to buy or rent (about $0.5-2 an hour rented) and keep up.
Edge case: Only worth it at volume.
Recommendation: B if map backdrops matter, A if only portraits, because only ControlNet keeps a painted map aligned with the grid.
5 · Items · Should items get 3D models and thumbnails?
Each item row gets a model reference (a CC0 library asset, or a builder in code with parameters such as colour and shape) and a baked PNG thumbnail; the 3D viewer opens when an item is selected.
Whether
Yes
When it helps: Inventory looks like a game; the AI can create new items as data (builder + params) with no art.
When it hurts: Builders per kind of item take an hour or two each; assets are 1-4 MB.
Edge case: Unique artifacts look generic unless someone makes them a model.
No, text and icons only
When it helps: Nothing to build.
When it hurts: Inventory stays a list.
Edge case: -
Not now
When it helps: Inventory UI is not the bottleneck.
When it hurts: -
Edge case: -
Recommendation: Not now, then yes with procedural builders plus one stylised CC0 pack and baked thumbnails, because the realistic Poly Haven look clashes with everything else.
6 · Characters · Which route should character art take first?
Three routes were compared: (a) a rigged CC0 3D base posed and rendered (also gives tokens), (b) an AI-generated portrait, (c) the 3D pose fed into an image model as control images.
Whether
(a) 3D base
When it helps: Free, instant, consistent; tokens and grid figures come with it.
When it hurts: Looks like a game asset; variety needs a modular kit.
Edge case: The CC0 robot here is a stand-in; humanoid bases per race are needed.
(b) AI portrait
When it helps: Best art for the least work.
When it hurts: Costs per image; the same character drifts between images.
Edge case: Keep the first portrait as the reference for later ones.
(c) 3D pose → image model
When it helps: Controlled pose and framing with real art.
When it hurts: Most plumbing; needs ControlNet (card 4, way B or C).
Edge case: Only worth it for many poses per character.
Recommendation: (b) when an image model is chosen, with tokens cropped from the portrait; (a) only if maps go 3D, because portraits are what players look at most.
Two small ones
Approve the grant "declare container dm-maps-static" (Approvals page) to put the site online at dm-maps.dev.tetrix.dev; until then this file and the folder are the way to look. Recommendation: approve; it is a read-only nginx container like bell-static, removable in one command.
Ask ProFantasy (management@profantasy.com) whether the Map Making bundle art may be used by an AI tool and by other users of the app? Recommendation: yes, before any of it goes into the game; it decides whether the Token Treasury can be the 2D token source.
What is honest to know
Image-model routes are mocks: DEV02 has no GPU and no image model of this project; other projects' keys were deliberately not used. Prices in the pages are rough and from memory; verify before deciding.
Screenshots come from software WebGL in a headless browser; on a normal GPU the 3D pages run in real time.
Simplified rules: movement counts diagonals as 5 ft (5e default) but does not yet stop squeezing past wall corners, cover is a simplified DMG line rule, light radius uses grid distance.
CC0 assets: 7 Poly Haven models (LICENSE.txt in each folder) and RobotExpressive by Quaternius (README.md). The ProFantasy bundle was not used: it is not uploaded and its licence for AI use is unconfirmed.