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:
Monster Robot Party 2026-08-04 02:48:39 +10:00
parent 8e61640c38
commit e588508436
6 changed files with 1987 additions and 748 deletions

View File

@ -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).

View File

@ -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

View File

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

View File

@ -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)},

View File

@ -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