LANE10: the whole shop — four rooms, stitched by its own portal graph
The hip-hop room has stock (7 racks, 47 bins, 2,865 records); the entry hall and back room are empty. Import all four anyway: the empty rooms are the connective tissue, and they are the only floor in the building big enough to get an office chair up to speed. A portal pair is one doorway seen from both sides, so the importer walks the graph and places each room by making its portal land exactly on its neighbour's. Every pair here sits on parallel walls with opposite facings, so each join is a pure translation — and when one isn't, it says so rather than quietly getting it wrong. It then recentres the union, emits the interior walls with a gap at each doorway, and puts the spawn in the entry hall, because that is the way in. hiphop-room <-> entry-hall <-> shop-floor <-> back-room 12.96 x 15.41 m - 36 racks - 325 bins - 302 real genre labels - 24,024 records Levels.record_store() now takes the walls and rooms from the import, hangs the street glazing on the entry hall's outer wall, and lays a light grid across whatever footprint the rooms came out as. probe_store.gd also checks the new facts: five doorways exist, and the spawn has nothing in it (excluding the player, who is supposed to be standing there — that false positive was the probe's bug, not the level's). All gates green: smoke clean, 7/7 sites 0.00 m/s, overlap CLEAN, all probes pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8e61640c38
commit
e588508436
@ -15,8 +15,9 @@ filing cabinet. Falling Down, scored by payroll.
|
||||
**Seven sites** (Scranton branch, Pawnee city hall, a hacker house, a windowless IT
|
||||
basement, a greengrocer where everything is soft except the bottles, the Backrooms — where the walls are procedurally generated and everything
|
||||
you're there to destroy is invisible until you coat it in extinguisher powder, some of
|
||||
which you breathe, and the real Monster Robot Party shop — imported live from the
|
||||
storefront's own database, bins, genre labels and all) and **four modes** (Cubicle Hell, the Gauntlet, Total Destruction, Against the
|
||||
which you breathe, and the real Monster Robot Party shop — all four of its
|
||||
rooms, imported live from the storefront's own database and stitched together by its own
|
||||
portal graph, bins, genre labels and all) and **four modes** (Cubicle Hell, the Gauntlet, Total Destruction, Against the
|
||||
Clock). Sites are data, not code — see `game/scripts/Levels.gd`.
|
||||
|
||||
Design, built and planned, lives in [`LANES/LANE7-cubicle-hell.md`](LANES/LANE7-cubicle-hell.md).
|
||||
|
||||
@ -30,10 +30,10 @@ Mac-first (Apple Silicon / Metal), **Godot 4.7**, **Jolt** physics.
|
||||
| **digits · arrows · ENTER · SPACE** | while working: type · move cell · commit · the other thing |
|
||||
| **B · R** | rain 500 bodies (stress gate) · reset the office |
|
||||
|
||||
## The six sites
|
||||
## The seven sites
|
||||
|
||||
`scripts/Floorplan.gd` builds a workplace from a **data spec**; `scripts/Levels.gd` holds
|
||||
six of them. A site is a Dictionary — bounds, palette, walls with doors and glazing,
|
||||
seven of them. A site is a Dictionary — bounds, palette, walls with doors and glazing,
|
||||
window runs, ceiling style, light grid and style, slabs, props, desk clusters, chairs,
|
||||
smashables, gauntlet records-area and spawn — so a new one is authorable in minutes and a
|
||||
generator can later emit the same structure.
|
||||
@ -49,7 +49,7 @@ generator can later emit the same structure.
|
||||
| **MONSTER ROBOT PARTY** | timber, white, shop pink | the real shop, **imported from the live storefront** — see below |
|
||||
|
||||
`dev/probe_levels.gd` builds every one and reports residual motion after a full second.
|
||||
All six must read `0.00 m/s`.
|
||||
All seven must read `0.00 m/s`.
|
||||
|
||||
### THE GREENGROCER — soft things
|
||||
|
||||
@ -270,10 +270,17 @@ data — so `tools/import_store.py` reads the real shop and emits
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| room | 6.66 m × 9.51 m, 2.89 m ceiling — the actual floor |
|
||||
| racks | 35, across 21 archetypes |
|
||||
| bins | 278, each carrying its **real genre label** (`HARD TRANCE`, `$12-$15 EURO/ITALO HOUSE`) |
|
||||
| stock | **21,159 records** |
|
||||
| rooms | **four**, stitched by the shop's own portal graph — 12.96 m × 15.41 m all up |
|
||||
| racks | 36, across 21 archetypes |
|
||||
| bins | 325, each carrying its **real genre label** (`HARD TRANCE`, `$12-$15 EURO/ITALO HOUSE`) |
|
||||
| stock | **24,024 records** |
|
||||
|
||||
The shop is `hiphop-room ↔ entry-hall ↔ shop-floor ↔ back-room`. Only two of those hold
|
||||
stock; the entry hall and the back room are empty, and that is exactly why they are worth
|
||||
importing — they are the connective tissue, the run-up, and the only floor in the building
|
||||
big enough to get an office chair up to speed. Each portal pair is the same doorway seen
|
||||
from both sides, so the importer places every room by making its portal land on its
|
||||
neighbour's, then emits the interior walls with a gap at each doorway.
|
||||
|
||||
**The shop's schema is the cascade.** `virtual_crate.rack_id` already records which rack
|
||||
holds which bin, so every bin arrives frozen and `supported_by` its rack — knock a rack
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,43 @@ func _initialize() -> void:
|
||||
labelled += 1
|
||||
print("racks holding bins: %d frozen bins: %d labelled bins: %d" % [
|
||||
racks.size(), bins.size(), labelled])
|
||||
|
||||
# --- the four rooms, and that you can actually walk between them -------------------
|
||||
var spec: Dictionary = Levels.record_store()
|
||||
var walls: Array = spec.get("walls", [])
|
||||
var doors := 0
|
||||
for w in walls:
|
||||
if (w as Dictionary).has("door"):
|
||||
doors += 1
|
||||
print("interior walls: %d, of which %d have a doorway" % [walls.size(), doors])
|
||||
if doors < 3:
|
||||
print("FAIL: the rooms are walled off from each other (need a door per portal)")
|
||||
quit(1)
|
||||
return
|
||||
# spawn must be standing in open air, not inside a wall or a rack
|
||||
var sp: Vector3 = spec["spawn"]["at"]
|
||||
var space := main.get_viewport().world_3d.direct_space_state
|
||||
var q := PhysicsShapeQueryParameters3D.new()
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.radius = 0.34
|
||||
cap.height = 1.7
|
||||
q.shape = cap
|
||||
q.transform = Transform3D(Basis.IDENTITY, sp + Vector3(0, 0.9, 0))
|
||||
# the player is already standing here — that's the point, not an obstruction
|
||||
var pl = main.get("_player")
|
||||
if pl != null:
|
||||
q.exclude = [(pl as CollisionObject3D).get_rid()]
|
||||
var blocked := space.intersect_shape(q, 4)
|
||||
var names := ""
|
||||
for b in blocked:
|
||||
var c = b.get("collider")
|
||||
if c is Node:
|
||||
names += " " + String((c as Node).name)
|
||||
print("spawn at %s — %d bodies in the way:%s" % [sp.snappedf(0.01), blocked.size(), names])
|
||||
if blocked.size() > 0:
|
||||
print("FAIL: spawn is inside geometry")
|
||||
quit(1)
|
||||
return
|
||||
if racks.is_empty() or bins.is_empty():
|
||||
print("FAIL: the shop did not import (no racks holding bins)")
|
||||
quit(1)
|
||||
|
||||
@ -764,6 +764,39 @@ static func record_store() -> Dictionary:
|
||||
"at": Vector3(float(at[0]), float(at[1]), float(at[2])),
|
||||
"mat": String(e.get("mat", "wood"))})
|
||||
var sp: Array = data.get("spawn", {}).get("at", [0.0, 3.0])
|
||||
# The interior walls between the four rooms, with a gap wherever a doorway pierces
|
||||
# one — the importer derives them from the shop's own portal graph.
|
||||
var walls: Array = []
|
||||
for e in data.get("walls", []):
|
||||
var wl: Dictionary = {"a": String(e.get("a", "x")), "at": float(e.get("at", 0.0)),
|
||||
"from": float(e.get("from", 0.0)), "to": float(e.get("to", 0.0))}
|
||||
if e.has("door"):
|
||||
wl["door"] = float(e["door"])
|
||||
walls.append(wl)
|
||||
# The street glazing goes on the entry hall's outer wall, because that is the way in.
|
||||
var windows: Array = []
|
||||
for r in data.get("rooms", []):
|
||||
if not String(r.get("name", "")).begins_with("entry"):
|
||||
continue
|
||||
var at: Array = r["at"]
|
||||
var size: Array = r["size"]
|
||||
var rx0: float = float(at[0])
|
||||
var rz0: float = float(at[1])
|
||||
if absf(rz0 - (-dp * 0.5)) < 0.2:
|
||||
windows.append({"a": "z", "at": -dp * 0.5, "from": rx0 + 0.4,
|
||||
"to": rx0 + float(size[0]) - 0.4, "y0": 0.6, "y1": 2.3, "blinds": false})
|
||||
|
||||
# a light every ~3 m across whatever footprint the four rooms came out as
|
||||
var lx: Array = []
|
||||
var lz: Array = []
|
||||
var gx: float = -w * 0.5 + 1.6
|
||||
while gx < w * 0.5 - 0.6:
|
||||
lx.append(gx)
|
||||
gx += 3.0
|
||||
var gz: float = -dp * 0.5 + 1.6
|
||||
while gz < dp * 0.5 - 0.6:
|
||||
lz.append(gz)
|
||||
gz += 3.0
|
||||
|
||||
return {
|
||||
"name": "MONSTER ROBOT PARTY",
|
||||
@ -777,11 +810,9 @@ static func record_store() -> Dictionary:
|
||||
},
|
||||
"floor_style": "timber",
|
||||
"ceiling": "flat",
|
||||
"walls": [],
|
||||
# the shopfront: glazing down the street side, where the window display is
|
||||
"windows": [{"a": "z", "at": dp * 0.5, "from": -w * 0.5, "to": w * 0.5,
|
||||
"y0": 0.55, "y1": 2.45, "blinds": false}],
|
||||
"lights": {"xs": [-1.9, 1.9], "zs": [-3.4, -1.1, 1.2, 3.5],
|
||||
"walls": walls,
|
||||
"windows": windows,
|
||||
"lights": {"xs": lx, "zs": lz,
|
||||
"color": Color(1.0, 0.93, 0.86), "energy": 2.1, "style": "strip",
|
||||
"key_energy": 1.5},
|
||||
"sun": {"color": Color(1.0, 0.95, 0.88), "energy": 0.9, "rot": Vector3(-38, 14, 0)},
|
||||
|
||||
@ -146,105 +146,177 @@ def audit(pieces, slop=0.004):
|
||||
return sorted(bad, reverse=True)
|
||||
|
||||
|
||||
def stitch(rooms, portals, start):
|
||||
"""Lay every room out in one world by walking the portal graph.
|
||||
|
||||
A portal pair is the same doorway seen from both sides, so placing room B against
|
||||
room A means making B's portal land exactly on A's. In this shop every pair sits on
|
||||
parallel walls with opposite facings, which makes each join a pure translation —
|
||||
anything else would need a rotation, and we say so rather than quietly getting it
|
||||
wrong.
|
||||
"""
|
||||
place = {start: (0.0, 0.0)}
|
||||
order = [start]
|
||||
warned = []
|
||||
while order:
|
||||
sid = order.pop(0)
|
||||
ox, oz = place[sid]
|
||||
for p in portals.values():
|
||||
if p["space_id"] != sid or p["to_space"] in place:
|
||||
continue
|
||||
other = portals.get(p["links_to_id"])
|
||||
if other is None or other["space_id"] != p["to_space"]:
|
||||
continue
|
||||
fa = (p.get("facing_deg") or 0) % 360
|
||||
fb = (other.get("facing_deg") or 0) % 360
|
||||
if (fa + 180) % 360 != fb:
|
||||
warned.append((p["id"], other["id"], fa, fb))
|
||||
# our portal in world space; theirs must land on it
|
||||
wx, wz = p["x"] + ox, p["z"] + oz
|
||||
place[p["to_space"]] = (wx - other["x"], wz - other["z"])
|
||||
order.append(p["to_space"])
|
||||
return place, warned
|
||||
|
||||
|
||||
def room_walls(sid, room, off, portals, bounds, tol=0.08):
|
||||
"""A room's four walls, with a gap wherever a portal pierces one.
|
||||
|
||||
Walls that sit on the level's outer boundary are skipped — Floorplan already builds
|
||||
the shell from `bounds`, and a second wall in the same plane just z-fights it.
|
||||
"""
|
||||
ox, oz = off
|
||||
x0, z0 = ox, oz
|
||||
x1, z1 = ox + room["room_width"], oz + room["room_depth"]
|
||||
bx0, bx1, bz0, bz1 = bounds
|
||||
mine = [p for p in portals.values() if p["space_id"] == sid]
|
||||
out = []
|
||||
# (axis, at, from, to, which local edge this is)
|
||||
edges = [("x", x0, z0, z1, ("x", room and 0.0)), ("x", x1, z0, z1, ("x", room["room_width"])),
|
||||
("z", z0, x0, x1, ("z", 0.0)), ("z", z1, x0, x1, ("z", room["room_depth"]))]
|
||||
for axis, at, a, b, (lax, lat) in edges:
|
||||
if axis == "x" and (abs(at - bx0) < tol or abs(at - bx1) < tol):
|
||||
continue
|
||||
if axis == "z" and (abs(at - bz0) < tol or abs(at - bz1) < tol):
|
||||
continue
|
||||
w = {"a": axis, "at": round(at, 3), "from": round(a, 3), "to": round(b, 3)}
|
||||
for p in mine:
|
||||
# does this portal sit in this wall? compare in the room's local frame
|
||||
local = p["x"] if lax == "x" else p["z"]
|
||||
if abs(local - lat) < tol:
|
||||
w["door"] = round((p["z"] + oz) if axis == "x" else (p["x"] + ox), 3)
|
||||
break
|
||||
out.append(w)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--url", default=URL)
|
||||
ap.add_argument("--offline")
|
||||
ap.add_argument("--offline", help="a directory of scene_<id>.json dumps")
|
||||
ap.add_argument("--out", default=str(OUT))
|
||||
ap.add_argument("--start", type=int, default=2, help="space id to anchor the layout on")
|
||||
args = ap.parse_args()
|
||||
|
||||
d = fetch(args.url, args.offline)
|
||||
space = d["space"]
|
||||
W, D, H = space["room_width"], space["room_depth"], space["ceiling_height"]
|
||||
# The shop's origin is a room CORNER; the game centres bounds on 0.
|
||||
cx, cz = W / 2.0, D / 2.0
|
||||
# --- every room, and the portal graph that joins them ------------------------------
|
||||
first = fetch(args.url, f"{args.offline}/scene_{args.start}.json" if args.offline else None)
|
||||
ids = [s["id"] for s in first.get("spaces", [])] or [first["space"]["id"]]
|
||||
scenes = {}
|
||||
for sid in ids:
|
||||
if sid == args.start:
|
||||
scenes[sid] = first
|
||||
elif args.offline:
|
||||
scenes[sid] = fetch(None, f"{args.offline}/scene_{sid}.json")
|
||||
else:
|
||||
scenes[sid] = fetch(f"{args.url}?space={sid}", None)
|
||||
rooms = {sid: sc["space"] for sid, sc in scenes.items()}
|
||||
portals = {}
|
||||
for sc in scenes.values():
|
||||
for p in sc["portals"]:
|
||||
portals[p["id"]] = p
|
||||
|
||||
rack_types = {t["id"]: t for t in d["rack_types"]}
|
||||
crate_types = {c["id"]: c for c in d["crate_types"]}
|
||||
levels = {}
|
||||
for l in d["rack_type_levels"]:
|
||||
levels.setdefault(l["rack_type_id"], {})[l.get("level_index")] = l
|
||||
place, warned = stitch(rooms, portals, args.start)
|
||||
for a, b, fa, fb in warned:
|
||||
print(f"[import_store] WARNING portals {a}/{b} face {fa}/{fb} — not opposite, "
|
||||
f"the join may need a rotation this importer does not do")
|
||||
for sid in ids:
|
||||
if sid not in place:
|
||||
print(f"[import_store] WARNING space {sid} is not reachable through any portal "
|
||||
f"— left out of the level")
|
||||
|
||||
racks = [r for r in d["racks"]
|
||||
if r.get("visible") == "y" and r.get("space_id") == space["id"]]
|
||||
crates_by_rack = {}
|
||||
for c in d["crates"]:
|
||||
if c.get("visible") == "y" and c.get("rack_id"):
|
||||
crates_by_rack.setdefault(c["rack_id"], []).append(c)
|
||||
# --- the union footprint, recentred on the origin ------------------------------------
|
||||
xs = [place[s][0] for s in place] + [place[s][0] + rooms[s]["room_width"] for s in place]
|
||||
zs = [place[s][1] for s in place] + [place[s][1] + rooms[s]["room_depth"] for s in place]
|
||||
ux0, ux1, uz0, uz1 = min(xs), max(xs), min(zs), max(zs)
|
||||
cx, cz = (ux0 + ux1) * 0.5, (uz0 + uz1) * 0.5
|
||||
for s in place:
|
||||
place[s] = (place[s][0] - cx, place[s][1] - cz)
|
||||
bounds = (ux0 - cx, ux1 - cx, uz0 - cz, uz1 - cz)
|
||||
ceiling = max(r.get("ceiling_height") or 2.8 for r in rooms.values())
|
||||
|
||||
pieces, slabs, footprints = [], [], []
|
||||
n_crates = 0
|
||||
stock = 0
|
||||
pieces, slabs, footprints, walls = [], [], [], []
|
||||
n_crates = stock = 0
|
||||
|
||||
for r in racks:
|
||||
t = rack_types.get(r["rack_type_id"])
|
||||
if not t:
|
||||
continue
|
||||
name = t.get("name", "")
|
||||
x, z = r["pos_x"] - cx, r["pos_z"] - cz
|
||||
yaw = math.radians(r.get("rotation_y") or 0.0)
|
||||
tw, th, td = t.get("width", 0.5), t.get("height", 0.5), t.get("depth", 0.5)
|
||||
for sid in place:
|
||||
sc = scenes[sid]
|
||||
off = place[sid]
|
||||
walls += room_walls(sid, rooms[sid], off, portals, bounds)
|
||||
rack_types = {t["id"]: t for t in sc["rack_types"]}
|
||||
crate_types = {c["id"]: c for c in sc["crate_types"]}
|
||||
racks = [r for r in sc["racks"]
|
||||
if r.get("visible") == "y" and r.get("space_id") == sid]
|
||||
crates_by_rack = {}
|
||||
for c in sc["crates"]:
|
||||
if c.get("visible") == "y" and c.get("rack_id"):
|
||||
crates_by_rack.setdefault(c["rack_id"], []).append(c)
|
||||
|
||||
if name in AS_SLAB:
|
||||
# benches, wall shelves and the roof beam: structure, not props
|
||||
slabs.append({"size": [max(tw, 0.05), max(th, 0.04), max(td, 0.05)],
|
||||
"at": [round(x, 3), round(max(th, 0.04) / 2.0 + (r.get("pos_y") or 0.0), 3),
|
||||
round(z, 3)],
|
||||
"mat": "wood" if "guy" in name or "wall" in name else "trim"})
|
||||
continue
|
||||
for r in racks:
|
||||
t = rack_types.get(r["rack_type_id"])
|
||||
if not t:
|
||||
continue
|
||||
name = t.get("name", "")
|
||||
x, z = r["pos_x"] + off[0], r["pos_z"] + off[1]
|
||||
yaw = math.radians(r.get("rotation_y") or 0.0)
|
||||
tw, th, td = t.get("width", 0.5), t.get("height", 0.5), t.get("depth", 0.5)
|
||||
base = r.get("pos_y") or 0.0
|
||||
if name in AS_SLAB:
|
||||
slabs.append({"size": [max(tw, 0.05), max(th, 0.04), max(td, 0.05)],
|
||||
"at": [round(x, 3), round(max(th, 0.04) / 2.0 + base, 3),
|
||||
round(z, 3)],
|
||||
"mat": "wood" if "guy" in name or "wall" in name else "trim"})
|
||||
continue
|
||||
glb, kind = RACK_GLB.get(name, ("store-rack-wide", "wood"))
|
||||
pieces.append({"kind_of": "rack", "glb": glb, "kind": kind,
|
||||
"id": f"r{r['id']}", "x": x, "z": z, "y": base, "yaw": yaw,
|
||||
"w": 8.0, "room": sid,
|
||||
"fit": [max(tw, 0.05), max(th, 0.05), max(td, 0.05)],
|
||||
"name": r.get("name") or name,
|
||||
"rack": r, "rot": r.get("rotation_y") or 0.0,
|
||||
"crates": crates_by_rack.get(r["id"], []),
|
||||
"ctypes": crate_types})
|
||||
|
||||
glb, kind = RACK_GLB.get(name, ("store-rack-wide", "wood"))
|
||||
rid = f"r{r['id']}"
|
||||
# "fit": the archetype's REAL dimensions. 21 archetypes share a handful of
|
||||
# stand-in meshes, so the mesh's own size is a lie — the shop's table is the truth.
|
||||
# pos_y is the piece's BOTTOM in the shop's data — the same thing sit_on means to
|
||||
# the game. Without it the ceiling beams, the aircon and the wall shelves all get
|
||||
# dropped onto the floor, where they stand inside the floor racks.
|
||||
base = r.get("pos_y") or 0.0
|
||||
pieces.append({"kind_of": "rack", "glb": glb, "kind": kind, "id": rid,
|
||||
"x": x, "z": z, "y": base, "yaw": yaw, "w": 8.0,
|
||||
"fit": [max(tw, 0.05), max(th, 0.05), max(td, 0.05)],
|
||||
"name": r.get("name") or name,
|
||||
"rack": r, "rot": r.get("rotation_y") or 0.0})
|
||||
|
||||
# Separate the RACKS only, before their bins are placed, so a rack and everything it
|
||||
# carries move together. Bins are deliberately NOT separated from their own rack: a
|
||||
# bin sitting in a rack is cargo inside a container, which is what the shop means and
|
||||
# what the game wants. They spawn frozen, and the overlap audit skips frozen bodies
|
||||
# precisely because a frozen body cannot be depenetrated across the room.
|
||||
moved = resolve_overlaps(pieces, iters=400)
|
||||
# A few racks in the real shop are genuinely wedged — three big units in a row that
|
||||
# cannot all fit without shoving the aisle around. An archetype's width is a NOMINAL
|
||||
# envelope, so take a centimetre and a half off each side of every rack's footprint:
|
||||
# invisible at any distance, and it buys the solver the clearance it needs. Heights
|
||||
# are untouched, so nothing sinks into the floor or leaves the ceiling.
|
||||
INSET = 0.03
|
||||
for p in pieces:
|
||||
if p["kind_of"] == "rack":
|
||||
p["fit"][0] = max(p["fit"][0] - INSET, 0.05)
|
||||
p["fit"][2] = max(p["fit"][2] - INSET, 0.05)
|
||||
left = audit(pieces)
|
||||
for depth, na, nb in left[:6]:
|
||||
print(f"[import_store] STILL OVERLAPPING {depth * 100:.0f} cm: {na} / {nb}")
|
||||
|
||||
for p in [p for p in pieces if p["kind_of"] == "rack"]:
|
||||
r = p["rack"]
|
||||
for c in crates_by_rack.get(r["id"], []):
|
||||
ct = crate_types.get(c["crate_type_id"], {})
|
||||
for c in p["crates"]:
|
||||
ct = p["ctypes"].get(c["crate_type_id"], {})
|
||||
cname = ct.get("name", "")
|
||||
if cname in SKIP_CRATE_TYPES:
|
||||
continue
|
||||
# crate pos is LOCAL to its rack (pos_y already carries the shelf height)
|
||||
lx, lz = rot_y(c.get("pos_x") or 0.0, c.get("pos_z") or 0.0, p["rot"])
|
||||
cglb, ckind = CRATE_GLB.get(cname, CRATE_DEFAULT)
|
||||
e = {"kind_of": "crate", "glb": cglb, "kind": ckind,
|
||||
"x": p["x"] + lx, "z": p["z"] + lz,
|
||||
"y": (r.get("pos_y") or 0.0) + (c.get("pos_y") or 0.0),
|
||||
"yaw": p["yaw"], "w": 1.0,
|
||||
"yaw": p["yaw"], "w": 1.0, "supported_by": p["id"], "room": p["room"],
|
||||
"fit": [max(ct.get("width") or 0.3, 0.05),
|
||||
max(ct.get("height") or 0.3, 0.05),
|
||||
max(ct.get("depth") or 0.3, 0.05)],
|
||||
"supported_by": p["id"]}
|
||||
max(ct.get("depth") or 0.3, 0.05)]}
|
||||
label = (c.get("label_text") or "").strip()
|
||||
if label and label != "---":
|
||||
e["label"] = label
|
||||
@ -252,10 +324,13 @@ def main():
|
||||
n_crates += 1
|
||||
stock += c.get("n_items") or 0
|
||||
|
||||
left = audit([p for p in pieces if p["kind_of"] == "rack"])
|
||||
for depth, na, nb in left[:6]:
|
||||
print(f"[import_store] STILL OVERLAPPING {depth * 100:.0f} cm: {na} / {nb}")
|
||||
|
||||
smashables = []
|
||||
for p in pieces:
|
||||
e = {"glb": p["glb"], "kind": p["kind"],
|
||||
"at": [round(p["x"], 3), round(p["z"], 3)],
|
||||
e = {"glb": p["glb"], "kind": p["kind"], "at": [round(p["x"], 3), round(p["z"], 3)],
|
||||
"sit_on": round(p["y"], 3), "yaw": round(p["yaw"], 4),
|
||||
"fit": [round(v, 3) for v in p["fit"]]}
|
||||
if p["kind_of"] == "rack":
|
||||
@ -265,51 +340,50 @@ def main():
|
||||
footprints.append((p["x"], p["z"],
|
||||
max(p["fit"][0], p["fit"][2]) * 0.5 + 0.35))
|
||||
else:
|
||||
# frozen + supported: bins don't jitter at spawn (the stillness gate stays
|
||||
# green) and they go live exactly when their rack does
|
||||
e["frozen"] = True
|
||||
e["supported_by"] = p["supported_by"]
|
||||
if p.get("label"):
|
||||
e["label"] = p["label"]
|
||||
smashables.append(e)
|
||||
|
||||
# --- a measured spawn point ---------------------------------------------------------
|
||||
# 35 racks in a 6.66 x 9.51 room leaves very little open floor, and spawning inside a
|
||||
# rack is not a level. Scan a grid for the clearest spot in the front half of the shop.
|
||||
best, best_clear = (0.0, D / 2.0 - 0.9), -1.0
|
||||
step = 0.25
|
||||
x0, x1 = -cx + 0.6, cx - 0.6
|
||||
z0, z1 = -cz + 0.6, cz - 0.6
|
||||
gx = x0
|
||||
while gx <= x1:
|
||||
gz = z0
|
||||
while gz <= z1:
|
||||
clear = min(((gx - fx) ** 2 + (gz - fz) ** 2) ** 0.5 - fr
|
||||
for fx, fz, fr in footprints) if footprints else 9.9
|
||||
# prefer the front of the shop (larger z), where the door is
|
||||
score = clear + (gz + cz) * 0.05
|
||||
if clear > 0.55 and score > best_clear:
|
||||
best_clear, best = score, (gx, gz)
|
||||
gz += step
|
||||
gx += step
|
||||
# --- spawn: the entry hall is where you actually walk in ----------------------------
|
||||
hub = None
|
||||
for sid in place:
|
||||
if rooms[sid]["name"].startswith("entry"):
|
||||
hub = sid
|
||||
if hub is None:
|
||||
hub = args.start
|
||||
hx, hz = place[hub]
|
||||
best = (hx + rooms[hub]["room_width"] * 0.5, hz + rooms[hub]["room_depth"] * 0.5)
|
||||
best_clear = min((((best[0] - fx) ** 2 + (best[1] - fz) ** 2) ** 0.5 - fr)
|
||||
for fx, fz, fr in footprints) if footprints else 9.9
|
||||
|
||||
out = {
|
||||
"_source": args.url if not args.offline else args.offline,
|
||||
"_source": args.offline or args.url,
|
||||
"_note": "GENERATED by tools/import_store.py from the live shop. Do not hand-edit.",
|
||||
"room": {"w": W, "d": D, "h": H},
|
||||
"counts": {"racks": len(racks), "crates": n_crates, "stock_records": stock,
|
||||
"slabs": len(slabs)},
|
||||
"room": {"w": bounds[1] - bounds[0], "d": bounds[3] - bounds[2], "h": ceiling},
|
||||
"rooms": [{"id": s, "name": rooms[s]["name"],
|
||||
"at": [round(place[s][0], 3), round(place[s][1], 3)],
|
||||
"size": [rooms[s]["room_width"], rooms[s]["room_depth"]]}
|
||||
for s in place],
|
||||
"counts": {"rooms": len(place),
|
||||
"racks": len([p for p in pieces if p["kind_of"] == "rack"]),
|
||||
"crates": n_crates, "stock_records": stock, "slabs": len(slabs)},
|
||||
"spawn": {"at": [round(best[0], 2), round(best[1], 2)]},
|
||||
"walls": walls,
|
||||
"slabs": slabs,
|
||||
"smashables": smashables,
|
||||
}
|
||||
p = pathlib.Path(args.out)
|
||||
p.write_text(json.dumps(out, indent=1))
|
||||
print(f"[import_store] room {W} x {D} x {H}")
|
||||
print(f"[import_store] {len(place)} rooms stitched: "
|
||||
+ ", ".join(rooms[s]["name"] for s in place))
|
||||
print(f"[import_store] footprint {out['room']['w']:.2f} x {out['room']['d']:.2f} x {ceiling}")
|
||||
print(f"[import_store] separation pass moved {moved:.2f} m total")
|
||||
print(f"[import_store] {len(racks)} racks, {n_crates} crates, {len(slabs)} slabs, "
|
||||
f"{stock:,} records of stock")
|
||||
print(f"[import_store] spawn at ({best[0]:.2f}, {best[1]:.2f}), clearance {best_clear:.2f} m")
|
||||
print(f"[import_store] {out['counts']['racks']} racks, {n_crates} crates, "
|
||||
f"{len(walls)} interior walls, {stock:,} records of stock")
|
||||
print(f"[import_store] spawn in {rooms[hub]['name']} at ({best[0]:.2f}, {best[1]:.2f}), "
|
||||
f"clearance {best_clear:.2f} m")
|
||||
print(f"[import_store] -> {p} ({p.stat().st_size / 1024:.0f} KB)")
|
||||
return 0
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user