PROCITY/docs/CITY_SPEC.md
2026-07-14 10:46:40 +10:00

10 KiB
Raw Blame History

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):

{
  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, poly: [[x,z],] } ],
  lots:   [ { id, block, x, z, w, d, ry, frontEdge, use } ],
      // use: 'shop' | 'anchor' | 'house' | 'yard' | 'stall' | 'infill'
  shops:  [ { id, lot, type, name, sign, seed, facadeSkin, storeys, hours: [open, close] } ]
      // type: see SHOP TYPES below
}

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 ~23; 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)

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
toy stucco-pink, deco-pastel shelves + glass case cube shelves, display tables, glass case
book federation, sandstone halls of shelves bookshelves, spinner racks, armchair
video stripmall, djsim-video aisle shelves VHS shelving, returns slot, poster wall
pawn besser, grimy, djsim-pawn counter-forward glass cabinets, wall hooks, barred window
milkbar djsim-milkbar, brickveneer counter + freezer counter, fridge, magazine rack
dept (anchor) terrazzo, djsim-dept grand hall mixed sections, escalator prop
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
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 AE land.

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/3godPOST /api/upload?name=<f>.glb, GET /a/<f>, 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).