# Local / tactical maps (5-ft squares) Open `index.html` (2D) and `3d.html` (3D). Both run the same generators (`gen.js`) with the same seed, so `?gen=tavern&seed=7` is the same tavern in both. ## How it works `gen.js` has four generators. Each writes one data object, **TacticalMap** (`schema: dm.tactical.v0`). Nothing in it is about pixels: ``` { grid_w, grid_h, square_ft: 5, ambient_light: "dark"|"dim"|"bright", terrain: { legend: { "#": {name:"wall", walk:false, sight:true, cover:"total"}, ".": {...}, ":": {name:"rubble", walk:true, cost:2}, ... }, rows: ["#####+####", "#........#", ...] }, // one char per square rooms: [{id:"r3", kind:"lair", x,y,w,h, links:["r2"], depth:4}], // the room graph (site_map) doors: [{x,y, orient:"ns"|"ew", state:"open"|"closed"|"locked"|"secret", room}], props: [{id, type:"pillar", x,y,w,h, rot, room}], // type keys into a rules table lights: [{prop, x,y, bright_ft, dim_ft}], // derived from props spawns: {party:[{x,y}], enemies:[{x,y}]} } ``` A prop's type maps to rules in `PROPS`: does it block movement, what cover it gives (half, three-quarters, total), its height, its light (bright/dim radius in feet), whether it is difficult terrain. The engine needs only these rules. Every renderer (2D, image model, 3D) reads the same object. | Generator | Method | Placement rules | |---|---|---| | BSP dungeon | Splits the space recursively and puts one room per leaf. Corridors join each room to its nearest earlier room (a tree), plus 2 extra links so there are loops. | Rooms get roles from the graph. The entrance is the lowest room, the lair is the deepest by room count, the treasury is a dead end next to the lair behind a locked door, the guard post is next to the entrance. Props come from the role: crates against walls in storage, an altar with braziers in the shrine, a throne and 4 pillars in the lair. Torches go on walls of occupied rooms. | | Cave | Cellular automata: 46% random fill, 5 smoothing passes, keep the largest region. | The camp goes in the most open spot that is far from the entry by walking. A pool is deep in the middle and shallow at the edge. Stalagmites only stand in open floor, away from the camp and the entry. Glowing mushrooms grow by the water. | | Tavern | A building on a street with a fixed room programme. | The front door faces the street and the storeroom is locked. The bar stands in front of the kitchen with stools, casks behind it and a free row for the barkeep. Tables sit on a lattice with aisles, and each has chairs and a candle. The fireplace is on an outside wall. | | Village | Two roads meet at a square with a well. A stream crosses them. | Houses stand next to a road with their door facing it, and never touch each other. The smithy and the chapel come first. Trees grow in clusters away from the roads, and lampposts stand along them. | Hard rules for every placement: a blocking prop is only placed if every walkable square stays reachable (a flood-fill check). The squares just inside a door stay empty, and no prop goes next to a door. The rule overlays in the 2D page are what the engine would compute from the data: - cover from a selected square (line of sight, plus the best obstacle on the line; a simplified version of the DMG rule); - movement within 30 ft (breadth-first search, diagonals cost 5 ft as in the 5e default; squeezing past wall corners is not checked yet); - light and darkness from the light sources (bright/dim radius, blocked by walls), plus the scene's ambient light. ## Render routes **2D**: a canvas draws straight from the data, in four styles: flat VTT, old-school blue, line art, and control map. It is cheap and exact, and every rule overlay is a few lines of code. It looks like a generator made it. **2D + image model** (the "image-model pass" style). This is a mock: no image model is configured or cheap on this machine, so nothing was sent anywhere. The pipeline would be: 1. Render two control images from the data: a flat semantic-colour segmentation map, and line art of walls and props. 2. Send both with a prompt built from the room kinds to SDXL or Flux with ControlNet (ComfyUI locally, or a hosted API such as Replicate or fal). The exact request is shown on the page. 3. Store the painted image as the location's backdrop, keyed by a hash of `terrain.rows` and `props`, so it is regenerated only when the layout changes. 4. In play, the painted image is only a backdrop. The grid, tokens, fog and cover overlays are still drawn from the data on top, so the rules never depend on what the model painted. Risks: the model may paint a door or a pillar where the data has none. ControlNet keeps this small but not at zero, and the rules still follow the data. Costs, rough and to be verified: a hosted SDXL/Flux image runs about EUR 0.01-0.05. A local GPU costs nothing per image but needs a GPU box, which DEV02 does not have. The ProFantasy symbol sets could replace or seed the 2D props, but their licence for AI use and use by others is unconfirmed (see memory). **3D** (`3d.html`, three.js): builds the scene from the same JSON: - instanced floor tiles per square and wall blocks only where a wall touches floor; - water sunk below the floor; - door leaves set by their state; - one primitive placeholder per prop type, whose height comes from the rules table, so cover is readable at a glance; - a point light per light source (the 10 strongest; the rest only glow); - tokens, where party token A is the Quaternius CC0 robot (rigged, playing its idle animation). "CC0 high-quality props" swaps chests, crates, barrels and wall lanterns for Poly Haven CC0 models. That shows that one prop type can be replaced by a real asset without touching the generator. Roofs are left off so you can see into buildings. In software WebGL (the screenshot container) a frame is slow; on a normal GPU it is real-time. ## Good and bad - **Good:** one data shape serves the engine, 2D, the image model and 3D. Placement is plain, testable rules. Generation takes about 10-40 ms. The JSON is 5-11 KB per map, which fits `locations.terrain` and `representation_data` easily. - **Bad:** the layouts are recognisably procedural, especially BSP. The tavern and village programmes are hand-written per building type. The village has no plots, fences or gardens yet. The cover calculation is simplified. - **3D:** it is striking that placeholders work at all. Making it look good means a consistent asset kit (Kenney or Quaternius dungeon kits, CC0), wall pieces instead of cubes, and per-room lighting. It costs more in time than 2D. It also brings camera, mobile performance and the question "can players still read the grid". ## What it costs to make real - Port `gen.js` to PHP: about 600 lines, 2-3 evenings. Or keep it in JS and run it in the browser or a Node sidecar, then post the JSON. - Add a `TacticalMap` value object, and write `terrain` and `representation_data` on promotion to `tactical`. This replaces the planned "dumb 20×20 seed". 1 evening. - Let the engine's range/line-of-sight/cover read `terrain.rows` and the props' rules instead of an empty grid: 1-2 evenings (the primitives are already planned). - Let the AI pick the generator and parameters ("a cramped smugglers' cellar with a hidden exit"), name the rooms and hide things in them; the generator does geometry, the AI does meaning: 1-2 evenings. - 2D renderer in the app (canvas or SVG): 1-2 evenings. The image-model pass: 1-2 evenings once an image provider is chosen. 3D in the app: 4+ evenings plus an asset kit. ## How it fits the project - `locations.representation = 'tactical'`, with `grid_w` and `grid_h` set from the map. - `locations.terrain` holds `terrain` (legend and rows), `doors`, `props` and `lights`: the persistent layout. It is promote-only, so a tavern brawl reuses the same room next time. - `locations.representation_data` holds `rooms` with their `links`: the site_map room graph. The same graph can seed child locations (one `locations` row per room) when the AI needs to talk about "the shrine". - `locations.ambient_light` holds `ambient_light`. The visibility model (Option B in architecture.md) gets lit zones from `lights`; this is exactly the light-source extension the doc calls forward-compatible. - `spawns` would seed `combatants.x` and `combatants.y` when an encounter starts. `move_token` then moves them over squares the rules allow.