#!/usr/bin/env python3 """Generate web/assets/manifest.json — the single contract Lanes B/C/D read. Mechanical parts (skin listing, fitting footprints) are derived from the filesystem + the normalizer's _normalize_results.json; the curated parts (which facade suits which shop type) are the maps below, seeded from CITY_SPEC's registry table. Re-run any time skins or normalized GLBs change: python3 pipeline/build_manifest.py Everything here is OPTIONAL at runtime — the game runs asset-free (CITY_SPEC house law). The manifest just tells the other lanes what nicer assets exist and how big they are. """ import json, os, glob ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) GEN = os.path.join(ROOT, "web", "assets", "gen") RESULTS = os.path.join(ROOT, "pipeline", "_normalize_results.json") # library GLBs PROPS_RESULTS = os.path.join(ROOT, "pipeline", "_props_results.json") # round-3 hero props OUT = os.path.join(ROOT, "web", "assets", "manifest.json") DEPOT = "https://digalot.fyi/3god" # ── depot-reuse: already-published GLBs we point at without a local normalize. The round-3 reuse # check found vintage-cash-register.glb live on 3GOD (from thriftgod_hero) — reuse it for the # universal till instead of re-generating. Manual footprint/height (no local file to measure); # no local thumb, so it isn't staged for ?localdepot (falls back to primitive there). ── DEPOT_REUSE = { "cash_register": {"file": "vintage-cash-register.glb", "footprint": [0.35, 0.28], "height": 0.35}, } # ── curated: facade skin → shop types it suits ───────────────────────────────────────── # MIRRORS Lane A's web/js/core/registry.js SHOP_TYPES[*].facades (the authoritative render-time # pool) so there is ONE consistent mapping, plus additive-only coverage: cashtheists→pawn (a pawn # facade registry leaves unreferenced), corrugated→stall (registry's stall pool has only `market`; # this is stall's 2nd skin — flagged for Lane A), djsim-pub→general. FACADE_TYPES = { "timber-teal": ["record"], "arcade-tile": ["record"], "djsim-record": ["record"], "weatherboard": ["opshop"], "fibro-blue": ["opshop"], "djsim-opshop": ["opshop"], "stucco-pink": ["toy"], "deco-pastel": ["toy"], "boutique": ["toy"], "federation": ["book"], "sandstone": ["book"], "redbrick": ["book"], "stripmall": ["video"], "djsim-video": ["video"], "besser": ["pawn"], "grimy": ["pawn"], "djsim-pawn": ["pawn"], "corrugated": ["pawn", "stall"], "cashtheists": ["pawn"], "djsim-milkbar": ["milkbar"], "brickveneer": ["milkbar"], "terrazzo": ["dept"], "djsim-dept": ["dept"], "market": ["stall"], "djsim-pub": ["general"], # ── Step-4 gap-fill fronts, generated on-device (MODELBEAST flux2-klein-4b, free) ────── # docs/shots/laneE/facades_gen_local.png. Additive to registry.js: each fattens a thin # pool so no type leans on one skin (video 2→4, milkbar 2→5, dept 2→4, stall 1→3). "toy-brights": ["toy"], "toy-lolly": ["toy"], "book-barn": ["book"], "book-scholarly": ["book"], "video-neon": ["video"], "video-suburban": ["video"], "milkbar-corner": ["milkbar"], "milkbar-fibro": ["milkbar"], "milkbar-deco": ["milkbar"], "dept-anchor": ["dept"], "dept-emporium": ["dept"], "market-fruit": ["stall"], "market-edge": ["stall"], "warehouse-roller": ["pawn", "general"], "warehouse-tin": ["pawn", "general"], "arcade-entry": ["general"], # arcade portal, not a shop — Lane B places by key # residential fronts for use:'house' lots (incl. the milkbar corner-shop's street) "res-terrace": ["house"], "res-weatherboard": ["house"], "res-fibro": ["house"], "res-brickveneer": ["house"], } # facades that already carry BAKED sign text (mostly the 90sDJsim Flux ones) — Lane B should NOT # overlay a shop name on these; the blank-signboard ones are the clean overlay targets. Verified # by eye against docs/shots/laneE/facades_style_anchor.png. BAKED_SIGN = {"djsim-milkbar", "djsim-video", "djsim-pub", "market", "grimy"} # ── curated: ground skin → where it belongs ───────────────────────────────────────────── GROUND_USE = { "asphalt1": "road", "asphalt2": "road", "djsim-road": "road", "footpath1": "footpath", "footpath2": "footpath", "djsim-footpath": "footpath", "grass": "yard", "brickpave": "plaza", "gravel": "verge", "reddust": "outback", } # ── curated: interior floor / surface split of the tex-* skins ─────────────────────────── FLOOR_TEX = ["carpet-swirl", "carpet-mustard", "carpet-greygreen", "lino-check", "lino-cork", "boards-polished"] SURFACE_TEX = { # interior wall / counter surfaces → biased shop types (hint only) "wood-counter": ["record", "milkbar", "pawn"], "boutique-wall": ["toy", "opshop"], "pegboard": ["pawn", "video"], "brick": ["general"], "render": ["general"], "grimy-wall": ["pawn"], "velvet-curtain": ["toy", "record"], } AWNING_TEX = ["awning-red", "awning-green", "awning-blue"] def rel(p): return "gen/" + os.path.basename(p) def build(): have = {os.path.splitext(os.path.basename(f))[0] for f in glob.glob(GEN + "/*.jpg")} def f(cat, key): name = f"{cat}-{key}" return f"gen/{name}.jpg" if name in have else None # ---- facades ---- facade = {} for key, types in FACADE_TYPES.items(): file = f("facade", key) if file: facade[key] = {"file": file, "types": types, "signboard": "baked" if key in BAKED_SIGN else "blank"} # any facade-*.jpg not in the curated map → catalogue it so nothing is orphaned. # `-side` skins are corner-lot SIDE walls (windowless): face:"side" + empty types so no # shop-front pool ever selects one — Lane B picks them by the facade--side.jpg name. for name in sorted(have): if not name.startswith("facade-") or name[len("facade-"):] in facade: continue key = name[len("facade-"):] is_side = key.endswith("-side") entry = {"file": "gen/" + name + ".jpg", "types": [] if is_side else ["general"], "signboard": "baked" if key in BAKED_SIGN else "blank"} if is_side: entry["face"] = "side" facade[key] = entry # ---- skies (array; not type-specific) ---- sky = [{"id": n[len("sky-"):], "file": "gen/" + n + ".jpg"} for n in sorted(have) if n.startswith("sky-")] # ---- grounds (object keyed by id, with a use tag) ---- ground = {} for name in sorted(have): if name.startswith("ground-"): key = name[len("ground-"):] ground[key] = {"file": "gen/" + name + ".jpg", "use": GROUND_USE.get(key, "road")} # ---- interior wallpapers (array) ---- wall = [{"id": n[len("wall-"):], "file": "gen/" + n + ".jpg"} for n in sorted(have) if n.startswith("wall-")] # ---- interior floors + surfaces + street awnings (from tex-*) ---- floor = [{"id": k, "file": f("tex", k)} for k in FLOOR_TEX if f("tex", k)] surface = [{"id": k, "file": f("tex", k), "types": t} for k, t in SURFACE_TEX.items() if f("tex", k)] awning = [{"id": k, "file": f("tex", k)} for k in AWNING_TEX if f("tex", k)] skins = { "facade": facade, "sky": sky, "ground": ground, "wall": wall, "interior": {"floor": floor, "surface": surface}, "awning": awning, } # ---- fittings / furniture from the normalizer results (library + round-3 hero props) ---- fittings, furniture = {}, {} # cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md DROP = {"procity_fit_cube_shelf_01.glb"} for results_file in (RESULTS, PROPS_RESULTS): if not os.path.exists(results_file): continue for r in json.load(open(results_file)).get("results", []): if r["file"] in DROP: continue fid = r["file"].replace("procity_fit_", "").replace("procity_street_", "").replace("_01.glb", "") entry = { "file": r["file"], "footprint": r["footprint"], # [w, d] metres, on the ground "height": r["size_m"][1], # metres "thumb": "thumbs/" + r["file"].replace(".glb", ".png"), } (furniture if r["file"].startswith("procity_street_") else fittings)[fid] = entry # depot-reuse assets (no local normalize) — e.g. the reused vintage cash register for fid, meta in DEPOT_REUSE.items(): fittings.setdefault(fid, dict(meta)) # registry.js semantic fitting id → our physical asset id (only where a GLB exists; the rest # stay primitives — see MESHGOD_BATCH.md). Lets a consumer that iterates registry SHOP_TYPES.fittings # resolve to a GLB. Lane C's glb.js already maps its own kinds straight to the physical ids below. fitting_aliases = { # registry 'counter' → the ~2m counter-with-till (counter_till), NOT the 4m balcao 'counter' "record_bins": "record_crate", "crates": "box_crate", "counter": "counter_till", "clothes_racks": "clothes_rack", "bricabrac_shelving": "wire_shelf", "book_wall": "bookshelf", "cube_shelves": "cube_shelf_wide", "display_tables": "coffee_table", "bookshelves": "bookshelf", "vhs_shelving": "wire_shelf", "trestle_tables": "work_table", # round-5 fal/MODELBEAST hero props → their registry semantic ids "glass_case": "glass_case", "glass_cabinets": "glass_case", "magazine_rack": "magazine_rack", "fridge": "drinks_fridge", "listening_corner": "listening_booth", } fitting_aliases = {k: v for k, v in fitting_aliases.items() if v in fittings} manifest = { "version": 1, "depot": DEPOT, "conventions": { "glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco", "facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic " "facing-correction (per-asset yaw is available in batch.json) — Lane B/C rotate " "directional props (counter, food_cart) to taste when placing", "facade_blank_signboard": "facades tagged signboard:'blank' ship a blank sign panel — " "Lane B overlays the shop name there. signboard:'baked' facades " "(some djsim-* Flux skins) already have sign text: use as-is, don't overlay", "facade_face": "facade entries without a 'face' key are front elevations (shop or " "house fronts); face:'side' entries are windowless corner-lot side " "walls with empty types — never select one as a shop front", "corner_lots": "corner lots expose a side wall: dedicated facade--side.jpg skins " "now exist for brick/render/weatherboard (face:'side') — Lane B maps " "these to the exposed side face; other types reuse the front skin", "fallbacks": "every asset is optional — missing file => primitive/flat-colour, never a crash", "load": "fittings/furniture load from depot as depot:; skins are local gen/", }, "skins": skins, "fittings": fittings, "furniture": furniture, "fittingAliases": fitting_aliases, } with open(OUT, "w") as fh: json.dump(manifest, fh, indent=2) print(f"wrote {OUT}") print(f" facades {len(facade)} skies {len(sky)} grounds {len(ground)} " f"wallpapers {len(wall)} floors {len(floor)} surfaces {len(surface)} awnings {len(awning)}") print(f" fittings {len(fittings)} furniture {len(furniture)}") if __name__ == "__main__": build()