# PROCITY — CITY_SPEC (the shared contract) *Every lane reads this first. If a lane needs to change something in here, it changes this file in the same commit and says so loudly in the commit message — this doc is the treaty.* ## What we are building A standalone, procedurally generated, fully walkable Australian shopping town — the Vuntra City trick (every building enterable, interiors generated on demand) applied not to cyberpunk towers but to **record stores, op shops, toy shops, book barns, video rental, pawnbrokers, milk bars, market stalls**. 90s-Australia aesthetic, low-poly + generated texture skins. Content (real item data, lore, economy) plugs in later via the GODVERSE/BaseGod feeds — v1 is the *system*: plan → streets → facades → doors that open → themed interiors → NPCs walking around. ## Engine decision (settled — do not relitigate) **three.js r175, vendored, plain JS ES modules, importmap, zero build step, zero npm.** This is the proven house stack (90sDJsim + thriftgod both ship on it), it runs great on Apple Silicon, and it lets us lift working code (fittings kit, rig stack, dig.js) verbatim. Unreal was considered and parked: nothing in v1 needs it, and the asset pipeline (GLB, web-ok licensing, 3GOD depot) is web-native. Serve with `cd web && python3 -m http.server 8130`. - `three` and `three/addons/` resolve via importmap to `web/vendor/` (copied from 90sDJsim). - No DRACO requirement (house GLBs ship uncompressed/webp-optimized); DRACOLoader is vendored if needed. - No external CDNs at runtime. Everything local or from `https://digalot.fyi/3god` (CORS-enabled). ## Units, axes, determinism - **Metres. +Y up. Ground plane is XZ.** City origin (0,0) = centre of the main square. - **Right-handed three.js defaults.** Buildings face their street; a facade's outward normal points at its street edge. GLB convention: origin at base, facing **−Z**, real-world scale. - **Seeded everything, `Math.random()` is banned in generation code.** `web/js/core/prng.js` is the only randomness source: - `seedFor(citySeed, kind, id)` → stable uint32 (xmur3 hash of `"seed:kind:id"`). - `rng(citySeed, kind, id)` → mulberry32 stream. - Same citySeed ⇒ byte-identical city, forever. This is the thriftgod/90sDJsim house rule and it's what makes the authored-override layer possible later. ## The three-layer architecture ``` LAYER 1 CityPlan (pure data, no THREE) Lane A LAYER 2 Streetscape (chunked 3D realization) Lane B LAYER 3 Interiors (on-demand room scenes) Lane C Citizens (NPCs over layers 2+3) Lane D Assets (skins + GLB depot) Lane E ``` ### Layer 1 — CityPlan (JSON-serializable, generated in <100ms) The whole town as lightweight data, generated up front from `citySeed`, dumpable to JSON. Schema v1 (Lane A owns it; extend, don't break): ```js { version: 1, citySeed: 20261990, // uint32 name: "…", // generated town name size: { w: 1024, d: 1024 }, // metres, city core v1 = 1km² districts: [ { id, kind, cx, cz } ], // kind: 'mainstreet' | 'arcade' | 'backstreets' | 'warehouse' | 'residential' | 'market' streets: { nodes: [ { id, x, z } ], edges: [ { id, a, b, width, kind } ] // kind: 'main' | 'side' | 'lane' | 'arcade' }, blocks: [ { id, district, kind, poly: [[x,z],…] } ], // district = district ID (index into districts[]); kind = that district's kind string, denormalized // onto the block so consumers can theme it without a lookup (Lane A extension, non-breaking). lots: [ { id, block, x, z, w, d, ry, frontEdge, use } ], // use: 'shop' | 'anchor' | 'house' | 'yard' | 'stall' | 'infill' // ry: Y-rotation (radians) so a GLB modelled facing −Z ends up with its facade's OUTWARD normal // pointing at frontEdge. World facing = (−sin ry, −cos ry). Lane B rotates the shell by ry. // frontEdge: id of the street edge this lot fronts (guaranteed to exist and be geometrically adjacent). shops: [ { id, lot, type, name, sign, seed, facadeSkin, storeys, hours: [open, close] } ] // type: see SHOP TYPES below. hours: [open, close] with 0 ≤ open < close ≤ 23. } ``` > **Layer-1 invariants Lane A guarantees** (enforced by `web/js/citygen/selfcheck.js`): fully > deterministic per `citySeed` (with a committed golden fingerprint guarding against drift); every > `frontEdge`/`block`/`district`/`lot` reference resolves; every lot faces its `frontEdge`; **no two > building lots (`shop`/`anchor`/`house`/`stall`) overlap, within OR across blocks**; `chunkIndex` > covers every lot and lists every edge in every chunk its road crosses; strictly JSON round-trippable > (all finite numbers, no stray keys). Chunk key: `cx = floor(x/64)`, `cz = floor(z/64)`. **Chunk size 64m.** Lane A ships a `chunkIndex(plan)` helper: chunk key → { lots, shops, edges } touching it. ### Layer 2 — Streetscape (Lane B) Realizes CityPlan chunks around the player. Hard requirements the two parent games skipped: - **Contiguous geometry** — no teleport seams; you can walk the whole core. - **Chunk streaming** — build/dispose chunk Groups at radius ~2–3; never rebuild the world. - **InstancedMesh from day one** for: building shells, awning boxes, verandah posts, street furniture, trees, streetlights. Canvas signage batched into a **sign atlas texture** per chunk (one CanvasTexture, many UV-mapped planes) — not one canvas per sign. - Facade formula (proven in both games): instanced box shell + front `PlaneGeometry` with a `facade-*.jpg` skin + parametric awning/verandah + sign band + door + window planes. Skins are generated with **blank signboards** — the game overlays the shop name. ### Layer 3 — Interiors (Lane C) Interiors are **not** street geometry. Walking into a doorway triggers `enterShop(shop)` → separate room Group built from `shop.seed` (thriftgod archetypes + the 90sDJsim `fittings.js` parametric kit), themed by `shop.type`. Leaving restores the street exactly where you stood. Street windows show a cheap fake (tinted glass + emissive warm glow at night in v1; interior-mapping shader is a stretch goal). ## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read) > **`web/js/core/registry.js` is the machine-readable source of truth** — import `SHOP_TYPES` for > the exact facade pools, sign hints, interior archetype, fittings, storeys, hours and district > weights. This table mirrors it. *(Lane A amendment 2026-07-14: added a variety facade to > `toy`/`book`/`pawn` and rounded out the fittings mixes — a `counter` to shops that sell over one, > a `freezer` to milkbar, a `glass_case`+`counter` to dept — so this table matches the registry > lanes actually consume. Non-breaking; all facades exist in `web/assets/gen/`.)* | type | facade pool | interior archetype | fittings mix | |---|---|---|---| | `record` | timber-teal, arcade-tile, djsim-record | crates + bins | record bins, crates, counter, listening corner | | `opshop` | weatherboard, fibro-blue, djsim-opshop | racks + shelves | clothes racks, bric-a-brac shelving, book wall, counter | | `toy` | stucco-pink, deco-pastel, boutique | shelves + glass case | cube shelves, display tables, glass case, counter | | `book` | federation, sandstone, redbrick | halls of shelves | bookshelves, spinner racks, armchair, counter | | `video` | stripmall, djsim-video | aisle shelves | VHS shelving, returns slot, poster wall, counter | | `pawn` | besser, grimy, djsim-pawn, corrugated | counter-forward | glass cabinets, wall hooks, barred window, counter | | `milkbar` | djsim-milkbar, brickveneer | counter + freezer | counter, fridge, magazine rack, freezer | | `dept` (anchor) | terrazzo, djsim-dept | grand hall | mixed sections, escalator prop, glass case, counter | | `stall` (market) | facade-market | open stall | trestle tables, crates | ## NPC contract (Lane D) - **Fleet rule (house law):** 2 rigged base meshes + shared clip bank. Canonical rigs: `~/Documents/character_kit/` (female `grandma_game.glb` route, male `hum_character.glb`) plus the 19 peds in `90sDJsim/web/world/models/peds/`. Game-local copies are byte-identical builds — **copy from character_kit / 90sDJsim, never edit locally.** - Distance tiers: near <25m = rigged `SkeletonUtils.clone` + AnimationMixer; mid = billboard impostor (pre-rendered sprite, instanced); far = culled. Walkers path along street-edge footpath lanes from the CityPlan graph. - Licensing: 🟢 web-ok only (Mixamo, CC0/CC-BY, CGTrader RF). **No ActorCore in web builds.** ## Performance budget (M-series laptop, 60fps) - ≤ 300 draw calls in street mode, ≤ 200k tris in a typical view. - `renderer.setPixelRatio(min(devicePixelRatio, 2))`. - Textures: skins ≤ 1024px, sign atlases 2048px, total GPU texture < 512MB. - Chunk build must not hitch: budget 4ms/frame (build incrementally or in idle callbacks). - Adopt 90sDJsim's `GFX_TIERS` idea later if needed; don't gold-plate now. ## Modes & shell (Lane B owns the shell) `MODE ∈ { map, street, interior }` — one renderer, one state machine, same as both parent games. `map` = 2D canvas town directory (Lane A's debug view grows into it). Save/load = localStorage v1. ## File ownership (parallel-safety treaty) | path | owner | |---|---| | `web/js/core/*` (prng, loaders, canvas) | scaffold (frozen — propose changes via CITY_SPEC PR) | | `web/js/core/registry.js` | Lane A | | `web/js/citygen/*`, `web/map.html` | Lane A | | `web/index.html`, `web/js/world/*` | Lane B | | `web/js/interiors/*`, `web/interior_test.html` | Lane C | | `web/js/citizens/*`, `web/citizens_test.html`, `web/models/*` | Lane D | | `pipeline/*`, `web/assets/*` + `web/assets/manifest.json` | Lane E | | `web/package.json` | scaffold/shared — `{"type":"module"}` only, added by Lane A | | `docs/*` | everyone, additively | Each lane also ships its own standalone test page so it can be verified without the others. **Never edit another lane's files.** Integration (Lane F) happens after A–E land. > `web/package.json` (added by Lane A) contains only `{"type":"module"}` so `node` runs the pure > `web/js/**` ES modules directly (e.g. `node web/js/citygen/selfcheck.js`, an acceptance criterion). > Browsers and `python3 -m http.server` ignore it; no deps, no build step. It changes Node's module > interpretation for every lane's `web/js/**` — which is what we want (all lanes can node-test their > pure modules) — so it's flagged here as shared scope, not silently added. ## Infrastructure map - **This mac** — dev box, Blender + Unreal installed, Apple Silicon (no CUDA — all gen is cloud-API or tailnet). - **ultra** `ssh johnking@100.91.239.7` (key auth works, always-on M1 Ultra): MESHGOD Blender scripts (`~/Documents/MESHGOD/scripts/finish_glb.py`, `render_glb.py`, `media_primitive.py`), sorted model library `~/Documents/3D=models/` (shop-fittings / street-furniture / furniture — see its `MANIFEST.md` + `CH_MAPPING.md`), `character_kit/`, `mixamo-fetch/out/` clip bank (34 clips — never re-download an existing clip). Caveats: no GNU coreutils/timeout/setsid, system python 3.9, Homebrew at /opt/homebrew. - **m3ultra** `http://100.89.131.57:11434` — Ollama (`qwen3:235b/32b/8b`, `gemma3:4b` vision, `gemma4`). Free local LLM for name generation, lore, cataloguing. Use it before any paid API. - **3GOD depot** `https://digalot.fyi/3god` — `POST /api/upload?name=.glb`, `GET /a/`, `GET /api/list` (CORS on). The shared GLB CDN all sibling games use. - **MeshGod** `https://digalot.fyi/meshgod` — image→GLB generation (~30¢/solid; check the DealGod canon-3D cache first; media/books use the free parametric skinner). - **Image gen** — house flow is prompt packs (see `~/Documents/FLOW_PROMPTS.md` format) + thriftgod's `gen_assets.py` batch pipeline (OpenRouter `google/gemini-3.1-flash-image`, ~$0.004/img, style-bible prefix, resumable) and 90sDJsim's Cloudflare Flux scripts (creds in `~/Documents/backnforth/.env`). ## Style lock (visual) Low-poly chunky geometry, flat-ish shading, muted 90s Australian palette, warm cinematic light — the 90sDJsim POLY lock — skinned with the thriftgod generated-texture kit (69 skins already in `web/assets/gen/`). Facade prompt template (blank signboards!) is in thriftgod `gen_assets.py:69-86` — reuse it for new skins so everything matches. ## House patterns to keep (both games proved these) - Promise-cached loaders; missing asset ⇒ placeholder box, never a crash. The game must run with zero assets. - `CanvasTexture` text planes for ALL signage/text — no font files. - `SkeletonUtils.clone` for anything skinned; canonicalize `mixamorig\d+` → `mixamorig` so one clip drives every character; strip position tracks from shared clips (`_rotOnly`). - Height-normalize rigs off the head bone; plant feet by min bone Y. - Seeded flat-colour fallbacks under every texture. - A `shot()` fixed-camera screenshot harness for visual regression (90sDJsim `tools/shots.py`).