@bitmagic/cli 0.1.64-dev.2 → 0.1.64-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,548 @@
1
+ /**
2
+ * The `building-levels-in-blender` skill: a hand-designed low-poly world written as a Python script
3
+ * against the shipped `bmlevel` library, built by Blender into one GLB plus the collider / door /
4
+ * light / landmark JSON, audited with ray casts, reviewed from rendered stills, and loaded by the
5
+ * engine's `MeshLevel`. Distilled from a creator's space-station game whose agent installed Blender
6
+ * and hand-rolled every part of this — the build vocabulary, the audits, the review renders, the
7
+ * level loader — and hit the two failures the skill now names up front (a GLB fetched from a local
8
+ * path that 404s after publish; NPC spawns routed into an absent voxel grid).
9
+ *
10
+ * Same contract as `unity-skill-files.ts`: pure functions returning file contents, registered in
11
+ * `SHIPPED_SKILLS` (project.ts). The Python and Blender scripts ship in the package's
12
+ * `assets/blender-level/` (see `files` in package.json) rather than being rendered into every
13
+ * project; the skill's step 1 copies them into `tools/blender-level/`.
14
+ *
15
+ * The skill's first section is the gate: the path is for frontier models with Blender available,
16
+ * and everything else goes to `bitmagic forge`. The CLI cannot tell which model drives it, so the
17
+ * gate is the agent's own call — stated once, plainly, where the planning skill sends it.
18
+ *
19
+ * Backticks and `${` are escaped for the template literal; `project-files.test.ts` checks the
20
+ * rendered frontmatter and that no escape leaks into the output.
21
+ */
22
+ export function renderBlenderLevelsSkill() {
23
+ return `---
24
+ name: building-levels-in-blender
25
+ description: Build a hand-designed low-poly 3D world — an interior, a space station, a house and its garden, a stylised set — in Blender from a short Python script instead of voxel terrain, and load it through the engine's MeshLevel: one GLB plus the collider, door, light and landmark JSON, audited with ray casts and reviewed from rendered stills before the game ever runs it. Use it when a creator asks for "low poly", "stylised 3D", a "hand-built level", an enclosed interior, "not voxels", or "in Blender" — and when such a level has walls you fall through, doors that do not open, z-fighting seams, or shows an empty world after publish.
26
+ ---
27
+
28
+ # Building a level in Blender
29
+
30
+ A mesh level is one GLB that IS the whole world, plus a JSON that says what the art cannot: which
31
+ surfaces are solid, where the rooms, doors, lights and named points are. You write the level as a
32
+ Python script against \`bmlevel\` (game coordinates: metres, Y up, +Z forward), Blender builds it,
33
+ \`audit.py\` fires thousands of rays at the colliders, \`review.py\` renders every door and room, and
34
+ the engine's \`MeshLevel\` loads the result in six lines (\`engine/agent-docs/mesh-level.md\`). The
35
+ level that taught this skill was a five-module space station; every rule below was paid for there.
36
+
37
+ ## 0. Is this path yours?
38
+
39
+ Take it only when BOTH hold:
40
+
41
+ - **You are a frontier model** — Claude Fable / Opus 5, GPT-6 class. Building a level part by part
42
+ in code, reading a contact sheet and diagnosing a failed ray audit is sustained spatial reasoning
43
+ over hundreds of lines; a smaller model produces a plausible script and a level with holes in it.
44
+ If you are not sure you are one, you are not.
45
+ - **Blender is installed or installable here.** Run the probe below. If it cannot be installed
46
+ (no admin rights, a sandbox), stop.
47
+
48
+ Otherwise use \`bitmagic forge\` (AGENTS.md, "Forging a whole level") — it builds a real level from a
49
+ prompt with no tooling beyond Chrome. The forge is also the right answer for natural terrain of
50
+ any kind; this path is for places that are designed, not grown.
51
+
52
+ \`\`\`
53
+ python3 tools/blender-level/find_blender.py --check # prints "Blender 4.x at <path>", or how to install it
54
+ export BLENDER="$(python3 tools/blender-level/find_blender.py)"
55
+ \`\`\`
56
+
57
+ Install if needed: macOS \`brew install --cask blender\`, Debian/Ubuntu \`sudo apt install blender\`,
58
+ Windows \`winget install BlenderFoundation.Blender\`. Blender 4.2 or newer.
59
+
60
+ ## 1. Install the library into the project
61
+
62
+ The scripts ship inside the Bitmagic CLI package. Copy them once:
63
+
64
+ \`\`\`
65
+ CLI_ROOT="$(cd "$(dirname "$(readlink -f "$(which bitmagic)")")/.." && pwd)"
66
+ mkdir -p tools && cp -R "$CLI_ROOT"/assets/blender-level tools/ && mkdir -p tools/level build
67
+ grep -qx 'build/' .gitignore || echo 'build/' >> .gitignore
68
+ \`\`\`
69
+
70
+ \`tools/blender-level/README.md\` lists what each file does. Level scripts go in \`tools/level/\`,
71
+ outputs in \`build/level/\`.
72
+
73
+ ## 2. Plan the place in words, then as a table
74
+
75
+ Write the \`## Art direction\` of \`GAME-DESIGN.md\` first — \`bitmagic judge\` scores against it, and a
76
+ reference image (\`bitmagic reference\`) keeps every pass aimed at the same look. Then list, before
77
+ any code: every **room** with its size in metres, every **door** with which two rooms it joins,
78
+ every **route** a player must be able to walk, the **spawn**, and the named points the game needs
79
+ (extraction pad, guard posts, an altar). That table becomes \`volume()\`, \`door()\`, \`link()\`,
80
+ \`spawn()\` and \`landmark()\` calls — and the audit checks exactly those, so a room you did not
81
+ declare is a room nothing checks.
82
+
83
+ ## 3. Write \`tools/level/<name>.py\`
84
+
85
+ Start from \`tools/blender-level/example_level.py\` — a two-room hut, porch, stairs and courtyard
86
+ using every helper. The vocabulary:
87
+
88
+ \`\`\`python
89
+ import os, sys
90
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'blender-level'))
91
+ from bmlevel import *
92
+
93
+ mat('plaster', (0.82, 0.78, 0.7)) # rgb 0..1; glass* renders transparent, emissive* glows
94
+ with context('hall', origin=(0, 0, 0), yaw=0): # a named part with its own local frame; nests
95
+ slab('floor', (0, 0), (12, 8), 'plaster') # top surface at y=0
96
+ wall('north', (-6, -4), (6, -4), 3.5, 'plaster', opening={'at': 6, 'width': 1.6, 'height': 2.4})
97
+ door('north door', (0, 0, -4), yaw=0, width=1.6, height=2.4, jamb='timber')
98
+ stairs('up', (4, 0, 0), rise=3, run=5, material='stone', rail='brass')
99
+ decor('cornice', (0, 3.3, -3.85), (12, 0.2, 0.3), 'plaster') # never collides
100
+ prism('bay', [(-2, 4), (2, 4), (0, 6)], 0, 3.5, 'plaster') # convex outline, extruded
101
+ text('sign', 'HALL', (0, 2.9, -4.2), 'brass', size=0.4, both_faces=True)
102
+ lamp('centre', (0, 3.2, 0), '#ffd9a0', intensity=10, distance=14)
103
+ volume('hall', (0, 1.75, 0), (12, 3.5, 8), tags=['indoors'])
104
+ landmark('altar', (0, 0.1, 5), tags=['objective'])
105
+ spawn((0, 0.1, 2), yaw=3.14159)
106
+ link('hall', 'yard', door='north door')
107
+ export('build/level', 'temple')
108
+ \`\`\`
109
+
110
+ Rules that are not style:
111
+
112
+ - **Everything solid by default.** \`box()\`, \`wall()\`, \`slab()\`, \`prism()\` and \`stairs()\` become
113
+ colliders; \`decor()\` and \`text()\` never do. Cladding, trim, lamps, fasteners and rugs are
114
+ \`decor()\` — a collider on a 5 mm strip is a step the player trips on and the audit flags.
115
+ - **Name everything, uniquely.** Names are the \`.blend\` outliner and the audit's error messages.
116
+ - **Overlap, never abut.** Two slabs meeting edge to edge leave a seam a ray, and a capsule, slips
117
+ through; overlap them by 5 cm. Two faces on the same plane z-fight; offset by 5 cm or make the
118
+ outer one deeper (the door header the library builds is 4 cm deeper than its jambs for this).
119
+ - **Convex prisms only** — a concave outline raises; split it. Mitred junctions are two prisms.
120
+ - **Doors are records, not meshes.** \`door()\` writes a \`DoorDefinition\`; the engine builds and
121
+ animates the leaf. \`jamb=\` adds solid jambs and a header so the wall reads as a doorway.
122
+
123
+ ## 4. Build
124
+
125
+ \`\`\`
126
+ "$BLENDER" -b --python tools/level/<name>.py
127
+ \`\`\`
128
+
129
+ It prints \`LEVEL … colliders … doors … volumes …\`, \`BOUNDS\`, and a \`REGISTER bitmagic assets add …\`
130
+ line to keep for step 7. Outputs: \`build/level/<name>.glb\` (one draw call per material),
131
+ \`<name>.blend\` (named, editable parts), \`<name>.mesh-level.json\`, \`<name>.doors.json\`. A syntax
132
+ or geometry error prints a Python traceback naming the helper and the object — fix and rebuild;
133
+ builds take seconds. \`python3 tools/level/<name>.py --dry-run\` gives the counts without Blender.
134
+
135
+ ## 5. Render and look
136
+
137
+ \`\`\`
138
+ "$BLENDER" -b --python tools/blender-level/review.py -- build/level/<name>.blend build/level/<name>.mesh-level.json build/level/review/
139
+ \`\`\`
140
+
141
+ Read \`build/level/review/contact-sheet.png\` (the log says which frame is which), then any single
142
+ frame that looks wrong. You are looking for: a door with no wall around it, a room whose ceiling
143
+ is missing, stairs that end in the air, a sign that reads backwards, two surfaces flickering into
144
+ one another. Fix the script, rebuild, re-render. Do not go on to the audit while the sheet is
145
+ wrong; the audit finds holes, not ugliness.
146
+
147
+ ## 6. Audit
148
+
149
+ \`\`\`
150
+ "$BLENDER" -b --python tools/blender-level/audit.py -- build/level/<name>.mesh-level.json # doors open: floors, doorways, routes
151
+ "$BLENDER" -b --python tools/blender-level/audit.py -- build/level/<name>.mesh-level.json --doors closed # envelopes sealed with the leaves in
152
+ \`\`\`
153
+
154
+ Every check prints its ray count and its failures with coordinates; exit 1 means the level is
155
+ not done. A failed \`floor_continuity\` is a hole a player falls through; a failed \`door_clearance\`
156
+ or \`link_clearance\` is a doorway or route the player cannot walk (a jamb in the opening, a table
157
+ on the line between two rooms, a step that ends short); a failed \`sealed_envelope\` is a gap in a
158
+ roof or wall. \`coplanar_overlap\` is a warning: two faces on one plane, which flicker. Never
159
+ answer a failure by removing the volume or the link that found it.
160
+
161
+ ## 7. Register the GLB, sync world.json, write the game code
162
+
163
+ \`\`\`
164
+ bitmagic assets add build/level/<name>.glb --name <name> --keep-glb --asset-id level-<name> --height <native height> # the REGISTER line from step 4
165
+ python3 tools/blender-level/sync_world.py build/level/<name>.doors.json build/level/<name>.mesh-level.json src/work/world.json
166
+ cp build/level/<name>.mesh-level.json src/work/
167
+ \`\`\`
168
+
169
+ \`--keep-glb\` keeps it a polygon mesh and puts a CDN URL in \`assets[]\`, which is the only thing a
170
+ published bundle can load — a GLB fetched from \`src/work/\` works in \`bitmagic dev\` and shows an
171
+ empty world after \`bitmagic publish\`. \`--height\` is the level's real height: the entry's default
172
+ is 2 m. \`sync_world.py\` upserts the doors into \`worldProfileData.doors[]\` (creator-set keys and
173
+ locks survive), sets the spawn, and sets \`terrain.shape\` to \`none\` so no voxel ground is built
174
+ under the level. Then, in \`src/work/Game.ts\`, after the player controller exists:
175
+
176
+ \`\`\`ts
177
+ import { MeshLevel, DEFAULT_MESH_LEVEL_OPTIONS } from 'engine/meshlevel/MeshLevel.js';
178
+ import { DEFAULT_FALL_RESCUE_OPTIONS } from 'engine/FallRescue.js';
179
+ import levelJson from './<name>.mesh-level.json';
180
+
181
+ const level = new MeshLevel(this.engine, { glb: { assetId: 'level-<name>' }, level: { data: levelJson } }, DEFAULT_MESH_LEVEL_OPTIONS);
182
+ const loaded = await level.load();
183
+ const spawn = level.landmark('spawn');
184
+ this.playerController.teleportTo(spawn.position[0], spawn.position[1], spawn.position[2], spawn.yaw);
185
+ this.playerController.configureFallRescue({ ...DEFAULT_FALL_RESCUE_OPTIONS, killPlaneY: loaded.bounds.minY - 50 });
186
+ \`\`\`
187
+
188
+ \`engine/agent-docs/mesh-level.md\` has the rest: \`landmarksTagged('guard')\` for NPC posts
189
+ (\`npc.spawn(x, z, y)\` — pass the storey's Y for interiors), \`volumesAt(position)\` for room logic,
190
+ the lighting modes, and \`samples/mesh-level-setup.ts\`.
191
+
192
+ ## 8. Check, verify, walk it
193
+
194
+ \`bitmagic check\`, then \`bitmagic verify\`: the console must show one \`[MeshLevel] loaded …\` line
195
+ with the collider and light counts you expect, and no \`404\` for the GLB. Then walk it in the
196
+ \`bitmagic dev\` browser — through every door, up every stair, into every room — because an audit
197
+ proves geometry, not that the place is good to be in. \`bitmagic verify --platform mobile\` too if
198
+ phones matter.
199
+
200
+ ## 9. Judge, then iterate in passes
201
+
202
+ \`bitmagic judge\` scores the verify screenshot against your art direction. Improve the level in
203
+ passes, each one a full loop (edit script → build → review → audit → sync → verify):
204
+
205
+ 1. **Structure** — rooms, doors, routes, stairs, the spawn. Audit clean, walkable.
206
+ 2. **Detail** — cladding, casings, coffers, deck plates, signage, props. All \`decor()\` unless the
207
+ player should bump into it; keep every travel clearance.
208
+ 3. **Surfaces and light** — materials, emissive strips, warm and cool lamps, a spot or two with
209
+ shadows. Look at it in the real game, not only in the render.
210
+
211
+ \`references/level-design-passes.md\` says what each pass contains and when to stop.
212
+
213
+ ## Where the deep material is
214
+
215
+ | Read when… | File |
216
+ |---|---|
217
+ | a piece is mirrored, rotated, at the wrong height, or the \`.blend\` no longer matches the GLB | \`references/blender-conventions.md\` |
218
+ | an audit check fails and the fix is not obvious, or a render looks wrong | \`references/audits-and-review.md\` |
219
+ | the level is walkable but plain, and you want it to look like the reference | \`references/level-design-passes.md\` |
220
+ | registering, syncing doors, spawning NPCs, lighting, publishing, or a level that is empty in the published game | \`references/engine-handoff.md\` |
221
+ `;
222
+ }
223
+ export function renderBlenderConventionsReference() {
224
+ return `# Blender conventions the library relies on
225
+
226
+ Every rule here is one the space-station level broke first and paid for in a rebuild.
227
+
228
+ ## Coordinates: you write game space, the library flips once
229
+
230
+ | You write | Meaning |
231
+ |---|---|
232
+ | positions \`(x, y, z)\` | metres, **Y up, +Z forward**, in the current \`context()\` frame |
233
+ | \`yaw\` | radians about +Y; an entity's forward at yaw θ is \`(sin θ, 0, cos θ)\` (\`engine/agent-docs/coordinate-system.md\`) |
234
+ | \`size\` | full extents \`(width, height, depth)\` — the engine halves them |
235
+
236
+ Blender is Z up. \`bmlevel\` places a game point at Blender \`(x, -z, y)\`. That map is a 90° rotation
237
+ about X (determinant +1), not a mirror, so a yaw about game +Y is the **same angle** about Blender
238
+ +Z, and \`export_yup=True\` on the glTF exporter undoes the placement exactly: what the script said
239
+ is what the engine reads. The collider JSON is written in game coordinates untouched, which is why
240
+ art and physics line up without a single correction on the TypeScript side.
241
+
242
+ Do not "fix" a piece that looks mirrored by negating an axis in the script. Something else is
243
+ wrong — usually yaw applied to the wrong frame, or a \`context(yaw=…)\` you forgot you were inside.
244
+
245
+ ## The .blend is named parts; the GLB is one mesh per material
246
+
247
+ \`export()\` saves the \`.blend\` **before** it joins anything, so every \`box()\` you named is a separate
248
+ object in a collection named after its \`context()\`. Then it duplicates every mesh, joins the
249
+ copies per material and exports only those — one draw call per material, the GLB the engine wants.
250
+ Consequences:
251
+
252
+ - Edit the script, not the \`.blend\`. A hand edit in Blender is lost on the next build. The \`.blend\`
253
+ is for LOOKING: open it when a render shows something you cannot place from the script.
254
+ - Keep the material count small (six to twelve). Each is a draw call; a material per object is a
255
+ level that renders at 20 fps on a phone.
256
+ - A material name is a contract with the engine: \`glass*\` renders as a thin transparent pane and
257
+ casts no shadow, \`emissive*\` is guaranteed to glow (\`glow=\` sets the exported emission strength;
258
+ the engine's bloom picks it up). Name a window material \`glass_pane\`, a light strip \`emissive_strip\`.
259
+ - \`alpha < 1\` exports blended transparency; the engine only treats it as glass when the name says so.
260
+
261
+ ## Text without fonts
262
+
263
+ \`text()\` uses Blender's bundled font, extrudes it and converts it to a mesh, so signage needs no
264
+ font file and survives the per-material join. Text reads from local −Z at yaw 0, i.e. it faces a
265
+ player approaching along +Z. \`both_faces=True\` adds a mirrored copy for the other approach — do
266
+ that for any sign on a header seen from both sides; a sign that reads backwards from one side is
267
+ the single most common review-sheet finding.
268
+
269
+ ## Solids, decoration, and the two failures in between
270
+
271
+ - \`box\`, \`wall\`, \`slab\`, \`prism\`, \`stairs\` are solid: a collider record each. \`decor\`, \`text\` are
272
+ not. \`glazing()\` is both — a non-solid pane you can see through and a solid collider you cannot
273
+ walk through.
274
+ - A decoration that should have been solid: the player walks through a pillar. A solid that should
275
+ have been decoration: a 5 mm deck plate becomes a step, \`link_clearance\` fails, and walking
276
+ stutters. The default is solid because the second failure is loud and the first is not.
277
+ - \`prism()\` colliders are convex hulls. A concave outline raises rather than silently becoming its
278
+ hull (which would fill the concavity with invisible wall). Split it: an L is two rectangles, a
279
+ mitred corridor end is a triangle plus a rectangle.
280
+ - \`stairs()\` makes visual treads and ONE ramp hull, so walking up is smooth; \`treads_solid=True\`
281
+ when the player must be able to stand on an individual step (a dropped item, an NPC post).
282
+
283
+ ## Overlap, offset, depth: the three z-fighting rules
284
+
285
+ 1. **Overlap, never abut.** Two slabs meeting edge to edge leave a hairline a ray falls through
286
+ and a capsule catches on. Overlap by 5 cm. The audit's \`floor_continuity\` finds the seam.
287
+ 2. **Offset coplanar faces by 5 cm.** A cladding panel flush with the wall it decorates flickers.
288
+ Push it out 5 cm (or in 5 cm, as an inset). \`coplanar_overlap\` warns about the ones on solids.
289
+ 3. **Give layered parts distinct depths.** Casing 0.30 m, header 0.34 m, trim 0.26 m: no two
290
+ layers share a face plane. The door helper does this for its own jambs and header.
291
+
292
+ Bevels (\`bevel=\`) catch light on edges and read as "made", not "boxed"; 1–5 cm on furniture and
293
+ casings, none on walls.
294
+
295
+ ## Mitred junctions
296
+
297
+ Where a corridor meets a room at an angle, a square corridor end leaves a wedge of open air beside
298
+ the doorway and a rectangular wall cutout leaves a slot above it. Build both ends on the same
299
+ mitre plane: the corridor floor, walls and roof as \`prism()\`s whose end edge lies on the angled
300
+ plane, and the room wall as two \`prism()\`s flanking the same plane. The station's ten junctions were
301
+ rebuilt this way after a player reported "gaps and pillars" that the centre-line audit had not
302
+ caught — which is why \`door_clearance\` now samples the full width of every opening.
303
+
304
+ ## What the exporter does, and why each flag
305
+
306
+ | Flag | Why |
307
+ |---|---|
308
+ | \`export_format='GLB'\` | one binary file for \`bitmagic assets add\` |
309
+ | \`use_selection=True\` | only the per-material joins, not the named parts |
310
+ | \`export_apply=True\` | bakes object transforms so the engine gets world-space vertices |
311
+ | \`export_yup=True\` | the coordinate round trip above |
312
+ | \`export_lights=False\`, \`export_cameras=False\` | lights come from the JSON, cameras are review-only |
313
+ `;
314
+ }
315
+ export function renderBlenderAuditsReference() {
316
+ return `# Audits and review renders
317
+
318
+ The audit fires rays at the **colliders in the JSON** — the physics the engine will build — never at
319
+ the art, because decoration is exactly what must not be solid. The renders show the **art in the
320
+ .blend**, so a level can pass the audit and look wrong, or look right and fail. Do both, in that
321
+ order: look first (cheap, catches design errors), audit second (catches the errors you cannot see).
322
+
323
+ ## The checks
324
+
325
+ | Check | Mode | What it fires | Failure means | Usual fix |
326
+ |---|---|---|---|---|
327
+ | \`floor_continuity\` | open, closed | a 0.5 m grid over every \`volume()\`, one ray down from 1 m above its floor, must hit within 1.5 m | a hole in the floor, a slab that ends short, a volume declared over open air | overlap the slabs by 5 cm; move the volume to the floor's real extent; if the volume is a stairwell, lower it or split it per storey |
328
+ | \`door_clearance\` | open | rays across every \`door()\` opening at three heights and every 25 cm of its width | something solid in the doorway: a jamb built into the opening, a lintel too low, a wall segment that did not stop at the opening | check the \`opening={'at': …}\` distance from the wall's START; check the door's \`yaw\` matches the wall's direction; check \`height\` against the lintel |
329
+ | \`link_clearance\` | open | a player capsule (radius 0.4 m, 0.5 m and 1.5 m high) marched every 0.5 m from volume A's centre to the door to volume B's centre, plus a floor probe under it | a route the player cannot walk: furniture on the line, a step that ends short of the floor, a doorway narrower than 0.8 m, no floor between the rooms | move the prop off the walk line; overlap the ramp with the landing; widen the door; add the missing floor |
330
+ | \`sealed_envelope\` | closed | from a 1 m grid inside every roofed volume: one ray up and four rays outward, each must hit within the volume plus 1 m | a gap in a roof or wall, a corner where two walls do not meet, a doorway with no door record (nothing to seal it) | extend the wall through the corner; add the door(); mark an open-sided space \`roofed=False\` |
331
+ | \`coplanar_overlap\` | both (warning) | pairs of unrotated box colliders sharing a face plane, same facing, overlapping extents | two visible faces flicker into each other | offset by 5 cm or change one depth |
332
+
333
+ "Mode" is \`--doors\` open (default) or \`closed\`: the leaves from \`<name>.doors.json\` are added as
334
+ solids in closed mode, which is the only mode in which a room can be sealed. Run both.
335
+
336
+ Every failure prints its first example with coordinates (game space) and \`--json out.json\` writes
337
+ all of them. Locate a coordinate by asking which \`context()\` it falls in and subtracting that
338
+ context's origin.
339
+
340
+ ## Reading a failure you do not believe
341
+
342
+ - **A ray at exactly a slab's edge fails.** Rays that graze an edge miss. The fix is the same as
343
+ for a real seam — overlap the slabs — because the player's capsule grazes edges the same way.
344
+ - **\`link_clearance\` fails at the door waypoint.** The march probes for the floor from 0.5 m above
345
+ waist height at the door; a threshold higher than the door's floor point, or a step directly
346
+ under the header, trips it. Put the door's \`pos\` at the floor of its threshold.
347
+ - **\`sealed_envelope\` fails along a whole wall.** The wall does not reach the roof: \`wall(height=…)\`
348
+ is measured from \`base\`, and a ceiling slab's top is at \`y\` with its underside at \`y - thickness\`,
349
+ so a 3 m wall under a slab whose top is at 3.2 m and thickness 0.2 m just touches — overlap them.
350
+ - **\`floor_continuity\` fails everywhere in one volume.** The volume's \`pos\` is its CENTRE, not its
351
+ floor; \`(x, height/2, z)\` for a room whose floor is at y = 0.
352
+
353
+ Never make a failure go away by deleting the \`volume()\` or \`link()\` that found it. A level with
354
+ no volumes passes every check and holds no claim.
355
+
356
+ ## The review set
357
+
358
+ \`review.py\` renders, from the JSON: \`overview\` (top-down, the whole extent), \`door-<id>-a\` and \`-b\`
359
+ (4 m back from each side of every door, eye height), \`stairs-<name>\` (from the foot, looking up),
360
+ \`volume-<name>\` (inside every room, from one end looking along it). Then \`contact-sheet.png\`,
361
+ four per row in the order the log prints.
362
+
363
+ What to look for, frame by frame:
364
+
365
+ - **overview** — the footprint matches the plan; nothing floats outside the walls; corridors meet
366
+ rooms with no wedge of sky beside the doorway.
367
+ - **doors** — a wall on both sides and a header above; the sign readable from THIS side; the floor
368
+ continuous through the opening; the far room visible through it (a black doorway is a room with
369
+ no light AND no fill — usually a missing volume light).
370
+ - **stairs** — treads reach the landing; the rail follows the slope; nothing pokes through a step.
371
+ - **volumes** — a ceiling; walls meeting at the corners; props sitting on the floor, not in it.
372
+
373
+ The renderer adds its own fill light so interiors are legible; it shows geometry, not the game's
374
+ lighting. Judge the lighting in \`bitmagic verify\`'s screenshot and in the dev browser.
375
+
376
+ ## When a render is right and the game is wrong
377
+
378
+ The two are the same geometry only if the GLB you registered is the one you just built. After
379
+ every rebuild: register again (same \`--asset-id\`, the entry is replaced), re-run \`sync_world.py\`,
380
+ copy the JSON into \`src/work/\`, and check \`[MeshLevel] loaded\` in the verify console reports the
381
+ new collider count. A verify that passes on stale files is the trap the station fell into three
382
+ times.
383
+ `;
384
+ }
385
+ export function renderBlenderPassesReference() {
386
+ return `# Level design in passes
387
+
388
+ A level that is only walls and floors is walkable and dead. The station that taught this skill
389
+ went from boxes to a place people wanted to screenshot in four passes, each a full loop of edit →
390
+ build → review → audit → sync → verify. The passes, what goes in each, and when to stop.
391
+
392
+ ## Pass 1 — structure
393
+
394
+ Rooms, corridors, doors, stairs, the spawn, every route. Nothing decorative. Stop when: the audit
395
+ is clean in both modes, every door opens onto a floor, every stair reaches a landing, and you have
396
+ walked every \`link()\` in the dev browser. Sizes that read well: rooms 6–12 m across, 3–4 m high;
397
+ corridors 3–4 m wide (a 2 m corridor is a pipe); doors 1.4–2 m wide and 2.2–2.6 m high; treads
398
+ 0.28–0.32 m deep and 0.16–0.18 m high. Bigger than a house, smaller than a hangar.
399
+
400
+ ## Pass 2 — detail
401
+
402
+ Everything the eye reads as "built". All \`decor()\` unless the player should bump into it.
403
+
404
+ - **Layered casings around every door**: an outer casing 5 cm proud of the wall, an inner recessed
405
+ band, a crown over the header. Three depths, three widths.
406
+ - **Cladding bands** on long walls: panels inset 5 cm every 4–6 m with a thin vertical trim between
407
+ them, a service channel at waist height, a coffer or beam line at the ceiling. A blank wall longer
408
+ than 6 m is the second most common judge finding.
409
+ - **Deck plates**: tile the floor slab with plates 4 × 4 m, 1.2 cm high, 6 cm apart, an occasional
410
+ plate in the alternate material; fasteners at the corners; a shallow grate every few plates.
411
+ - **Ceiling coffers** or beams on a 3–4 m rhythm, with a recessed light housing in each.
412
+ - **Guidance**: a floor strip in the room's accent colour running the route between doors; signs
413
+ on every door header, both faces; a module or room name on the wall you see on entering.
414
+ - **Furniture at the walls, not on the line.** The audit will tell you when a prop blocks a route,
415
+ but the rule is simpler: props go against walls and in corners; the middle of a room is for
416
+ walking and fighting.
417
+ - **Rails on every stair and gallery edge** (\`rail=\`), posts every third step.
418
+
419
+ Stop when: the review sheet shows no blank wall and no floating detail, and the audit is still
420
+ clean (detail is the pass that breaks clearances — a casing 20 cm into a doorway is a failed
421
+ \`door_clearance\`).
422
+
423
+ ## Pass 3 — surfaces and light
424
+
425
+ - **Materials**: six to twelve. A warm neutral for walls, a dark deck, one trim metal, one accent
426
+ per room, one glass, one emissive. Roughness 0.4–0.7 on walls, 0.2–0.35 on deck plates so they
427
+ catch light; metal only on rails and fasteners.
428
+ - **Lights**: one warm \`lamp()\` per room near its centre at ceiling height minus 0.5 m, cool fill in
429
+ corridors, \`emissive_*\` strips where the eye should travel (door frames, guidance, consoles). One
430
+ or two \`spot()\`s with shadows over the places that matter (the spawn, the objective). Point lights
431
+ share a small pool in the engine — a room with eight lamps gets four of them; put the count into
432
+ emissive materials instead.
433
+ - **The engine's interior preset dims the sun for you**; a level that looks washed out in the game
434
+ after looking fine in the render has an \`exterior\` mode set, or its own sun in \`lightingConfig\`.
435
+
436
+ Stop when: \`bitmagic judge\` no longer names lighting or flat surfaces, and the level reads the same
437
+ in the verify screenshot as in the reference image.
438
+
439
+ ## Pass 4 — the reference image
440
+
441
+ Only now: put the reference image beside the review sheet and the verify screenshot and list the
442
+ five biggest differences. Fix those five, in order. Then stop — a level is done when the next
443
+ change would be for you, not the player.
444
+
445
+ ## What not to do
446
+
447
+ - Do not rebuild the whole script for one room; add a \`context()\` or edit one.
448
+ - Do not make decoration solid "to be safe". Make it \`decor()\` and keep the clearances.
449
+ - Do not add a light per lamp housing. Add the housing as \`emissive_*\` decor and one real light per room.
450
+ - Do not put the whole level in one \`context()\`. One per room or module: the \`.blend\` outliner and
451
+ the audit's coordinates both read in room-local metres.
452
+ `;
453
+ }
454
+ export function renderBlenderEngineHandoffReference() {
455
+ return `# Handing the level to the engine
456
+
457
+ The engine side is \`MeshLevel\` — \`engine/agent-docs/mesh-level.md\` is the full reference and
458
+ \`engine/agent-docs/samples/mesh-level-setup.ts\` the compile-checked sample. This page is the
459
+ seam: what the build wrote, what each command does with it, and the four ways it goes wrong.
460
+
461
+ ## The files the build wrote
462
+
463
+ | File | Goes | Carries |
464
+ |---|---|---|
465
+ | \`build/level/<name>.glb\` | \`bitmagic assets add … --keep-glb\` → CDN, an \`assets[]\` entry with id \`level-<name>\` | the art, one mesh per material |
466
+ | \`build/level/<name>.mesh-level.json\` | \`cp\` into \`src/work/\`, static \`import\` in \`Game.ts\` | colliders, volumes, landmarks, lights, \`extras.links\` |
467
+ | \`build/level/<name>.doors.json\` | \`sync_world.py\` → \`worldProfileData.doors[]\` | \`DoorDefinition\` records the engine animates |
468
+ | \`build/level/<name>.blend\` | nowhere; open it to look | the named parts |
469
+
470
+ ## Registering: \`--keep-glb\` and \`--height\`
471
+
472
+ \`\`\`
473
+ bitmagic assets add build/level/<name>.glb --name <name> --keep-glb --asset-id level-<name> --height <native height>
474
+ \`\`\`
475
+
476
+ - Without \`--keep-glb\` the file is voxelized into a \`.vxl\` and the level becomes a blocky prop.
477
+ - The entry's \`targetHeight\` defaults to 2 m. \`MeshLevel\` does not scale by it, but anything else
478
+ that reads the asset (the Creator's asset panel, a placement) would squash a 40 m station into a
479
+ 2 m box. The build's \`REGISTER\` line has the real number.
480
+ - Re-registering with the same \`--asset-id\` replaces the entry; the game code does not change.
481
+ - **A GLB that is not an asset is not in the published game.** \`bitmagic publish\` bundles one HTML
482
+ file whose assets are absolute CDN URLs; a path under \`src/work/\` or \`build/\` resolves in
483
+ \`bitmagic dev\` and in \`bitmagic verify\` and returns 404 from the published page, which then shows
484
+ the sky and a falling player. \`MeshLevel\` warns when handed a local \`{ url }\`; use \`{ assetId }\`.
485
+
486
+ ## Syncing: doors, spawn, terrain
487
+
488
+ \`\`\`
489
+ python3 tools/blender-level/sync_world.py build/level/<name>.doors.json build/level/<name>.mesh-level.json src/work/world.json
490
+ \`\`\`
491
+
492
+ - **Doors** are upserted by id. The build owns \`position\`, \`rotationY\`, \`width\`, \`height\`,
493
+ \`thickness\`, \`animation\`; a \`kind: 'locked'\`, \`keyId\`, \`assetId\`, \`color\` or \`autoOpenRadius\` set
494
+ in world.json by the creator (or by you, for a key-and-lock design — \`engine/agent-docs/doors-and-locks.md\`)
495
+ survives a rebuild. Doors the build no longer has are listed, never deleted.
496
+ - **Spawn**: the \`spawn\` landmark becomes \`playerSpawnPosition\` and \`playerSpawnRotation\`. The game
497
+ code teleports there too (below) so the two cannot disagree.
498
+ - **Terrain**: \`terrain.shape\` becomes \`'none'\` — no voxel ground, no invisible safety plane. That
499
+ is the mode \`MeshLevel\` needs; it also means a level with no floor under the spawn is a player
500
+ falling for ever, which \`configureFallRescue\` turns into a respawn.
501
+
502
+ Run it after EVERY rebuild; a door moved in the script and not synced is a leaf animating in a wall.
503
+
504
+ ## The game code
505
+
506
+ \`\`\`ts
507
+ import { MeshLevel, DEFAULT_MESH_LEVEL_OPTIONS } from 'engine/meshlevel/MeshLevel.js';
508
+ import { DEFAULT_FALL_RESCUE_OPTIONS } from 'engine/FallRescue.js';
509
+ import levelJson from './<name>.mesh-level.json';
510
+
511
+ // After setupPlayerController(), before any NPC spawns.
512
+ this.level = new MeshLevel(this.engine, { glb: { assetId: 'level-<name>' }, level: { data: levelJson } }, DEFAULT_MESH_LEVEL_OPTIONS);
513
+ const loaded = await this.level.load();
514
+ const spawn = this.level.landmark('spawn');
515
+ this.playerController.teleportTo(spawn.position[0], spawn.position[1], spawn.position[2], spawn.yaw);
516
+ this.playerController.configureFallRescue({ ...DEFAULT_FALL_RESCUE_OPTIONS, killPlaneY: loaded.bounds.minY - 50 });
517
+
518
+ const guards = this.engine.registerNpc('guards', new NpcIdleBehavior(), { autoRespawn: false }); // any INpcBehavior — engine/agent-docs/npc-system.md
519
+ for (const post of this.level.landmarksTagged('guard')) {
520
+ await guards.spawn(post.position[0], post.position[2], post.position[1]); // (x, z, y): Y picks the storey
521
+ }
522
+ \`\`\`
523
+
524
+ - \`landmark(name)\` throws with the known names when one is missing; a typo cannot spawn the boss
525
+ at the origin. \`landmarksTagged(tag)\` for posts, \`volumesAt(position)\` for "which room am I in".
526
+ - NPC spawns on a mesh level resolve the floor with raycasts (there is no voxel grid). Pass the
527
+ storey's Y for interior spawns, or an upstairs room resolves to the roof above it.
528
+ - \`DEFAULT_MESH_LEVEL_OPTIONS.lighting.mode\` is \`interior\`: the sun is dimmed through
529
+ \`lightingConfig\`, the template's hemisphere light reused, JSON point lights pooled, the first two
530
+ shadow-casting spots cached. An outdoor level (the courtyard game) wants
531
+ \`{ ...DEFAULT_MESH_LEVEL_OPTIONS, lighting: { ...DEFAULT_MESH_LEVEL_LIGHTING, mode: 'exterior' } }\`.
532
+ - \`dispose()\` removes bodies, lights and the scene, and restores the lighting config and the spawn
533
+ finder it replaced — call it before loading another level.
534
+
535
+ ## Verify, and the four ways it fails silently
536
+
537
+ \`bitmagic verify\` must print \`[MeshLevel] loaded "<cdn url>" — N colliders …\` with the count the
538
+ build printed. Then:
539
+
540
+ 1. **Verify passes, published game is empty** — the GLB was a local path (above).
541
+ 2. **Verify passes, doors animate inside walls** — \`sync_world.py\` was not re-run after a rebuild.
542
+ 3. **Verify passes, zero NPCs** — spawns given without the storey's Y on a multi-floor level, or
543
+ posts declared inside a wall; the console shows the finder returning null for each.
544
+ 4. **Verify passes, the level is bright white** — \`lighting.mode\` is \`exterior\`, or \`lightingConfig\`
545
+ in world.json sets a sun the preset then merges with. The preset only lowers what it names.
546
+ `;
547
+ }
548
+ //# sourceMappingURL=blender-level-skill-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blender-level-skill-files.js","sourceRoot":"","sources":["../../src/scaffold/blender-level-skill-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsMR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,iCAAiC;IAC/C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyFR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmER,CAAC;AACF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkER,CAAC;AACF,CAAC;AAED,MAAM,UAAU,mCAAmC;IACjD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2FR,CAAC;AACF,CAAC"}
@@ -749,7 +749,7 @@ If you are unsure whether one is already running, \`bitmagic reload\` says so an
749
749
  itself: \`planning-a-game/\` (what to build first), \`finding-engine-apis/\` (engine docs),
750
750
  \`generating-assets/\`, \`working-with-the-creator/\`, \`theming-the-hud/\`,
751
751
  \`publishing-a-game/\`, \`judging-quality/\`, \`checking-usage/\`, \`making-a-trailer/\`,
752
- \`converting-unity-levels/\`.${mirrorClause}
752
+ \`converting-unity-levels/\`, \`building-levels-in-blender/\`.${mirrorClause}
753
753
  \`upgrade\` rewrites every copy, so edits to them are lost; skills you add yourself are never
754
754
  touched.
755
755
  - \`.claude/settings.json\` — **Claude Code only, and yours**: seeded with one Stop hook that runs
@@ -1051,7 +1051,9 @@ terrain, props, spawn points — into \`src/work/world.json\`. \`bitmagic levels
1051
1051
  | set-start | remove\` manages them afterwards (free, local); \`set-start\` is how you change which
1052
1052
  level the game boots into, and it moves the legacy mirror fields with it. Add \`--city\`, \`--dungeon\`,
1053
1053
  \`--platformer\` or \`--freeform\` (at most one) for a different kind of place; omit them all for
1054
- natural terrain.
1054
+ natural terrain. A place designed part by part — an interior, a station, a stylised set — can
1055
+ instead be built in Blender from a script: \`${PRIMARY_SKILLS_DIR}/building-levels-in-blender/SKILL.md\`,
1056
+ whose first section says when that path is yours (a frontier model, Blender installable).
1055
1057
 
1056
1058
  Four things worth knowing:
1057
1059
 
@@ -1062,8 +1064,7 @@ Four things worth knowing:
1062
1064
  voxelizing is the engine's own code. The command builds and serves the project itself, so you
1063
1065
  do not need \`bitmagic dev\` running and it will not collide with one that is.
1064
1066
  3. **It rewrites large parts of \`world.json\`** (assets, environmentObjects, the level registry,
1065
- spawn points), so re-read the file afterwards if you were editing it. It also drops a
1066
- \`forged-heightfield.json\` beside it, which later placements use to sit objects on the terrain.
1067
+ spawn points), so re-read the file afterwards if you were editing it.
1067
1068
  4. **Resume rather than re-run.** The command prints a job id, \`world.json\` only ever changes on
1068
1069
  the final step, and every failure says whether it did and whether
1069
1070
  \`bitmagic forge --resume <jobId>\` is worth it — which is twenty minutes you do not spend again.
@@ -1152,6 +1153,7 @@ the engine, already wired** — and hand-building a worse version of it is the m
1152
1153
  | Animals, snakes, fish, birds, dragons | Procedural creature systems | \`animal-instructions.md\`, \`snake-instructions.md\` |
1153
1154
  | Score, timers, health bars, game-over and win screens | HUD + end-game systems | \`HUD_ELEMENTS.md\`, \`end-game.md\` |
1154
1155
  | A start screen — cover image, title, Play-button label | Start-screen system | \`START_SCREEN.md\` |
1156
+ | An opening cutscene or any full-screen video | \`engine.playVideo()\` fullscreen overlay | \`video-cutscenes.md\` |
1155
1157
  | Collectibles, pickups, checkpoints, markers | Dedicated systems | \`collectible-objects.md\`, \`markers-system.md\` |
1156
1158
  | Doors, locked doors and their keys | \`worldProfileData.doors[]\` + \`keyItems[]\` | \`doors-and-locks.md\` |
1157
1159
  | Moving platforms, elevators, crushers, conveyors, spinners | \`mechanisms[]\` in world.json | \`moving-platforms.md\` |
@@ -1207,7 +1209,7 @@ player to move) either builds no player controller (the \`no-character\` templat
1207
1209
  - Controls: every action as desktop key + phone button, bound in one call
1208
1210
  - Engine systems: one line each, naming the doc — \`engine/agent-docs/…\`
1209
1211
  - Assets to generate: name them. A skybox and the cover usually earn their place in slice 1.
1210
- - Level: procedural baseline terrain, unless the terrain IS the mechanic
1212
+ - Level: procedural baseline terrain, unless the terrain IS the mechanic (forge — or Blender, if that skill's gate passes)
1211
1213
 
1212
1214
  ## Deferred — the backlog
1213
1215
  Numbered, roughly best-first. Everything the design implies that slice 1 does not do.
@@ -1244,7 +1246,11 @@ ${PLANNING_SKILL_REFERENCE_STEP4}4. \`bitmagic cover\` — **now**, not later. I
1244
1246
  so it sets how long the human waits for slice 1. It belongs in slice 1 for a game that cannot be
1245
1247
  played without its shape — skiing, racing on a real circuit, open-world driving, an enclosed dungeon
1246
1248
  interior. For everything else the baseline's procedural voxel terrain is a fine slice-1 world and the
1247
- forge is backlog item 1, run once the loop is proven.
1249
+ forge is backlog item 1, run once the loop is proven. A designed place — an interior, a station, a
1250
+ stylised set the creator has a picture of — has a second route, building it in Blender from a
1251
+ script (\`${PRIMARY_SKILLS_DIR}/building-levels-in-blender/SKILL.md\`); its first section is a gate
1252
+ on the model and on Blender being available, and it belongs in slice 1 only under the same rule as
1253
+ the forge.
1248
1254
 
1249
1255
  ## Step 5 — hand it back
1250
1256
 
@@ -1339,6 +1345,7 @@ tier 2.
1339
1345
  | HUD elements — bars, scores, counters, timers | \`HUD_ELEMENTS.md\` |
1340
1346
  | HUD visual style and themes | \`HUD_STYLE_GUIDE.md\`, \`HUD_THEMES.md\` |
1341
1347
  | Start screen — cover image, title, Play-button label | \`START_SCREEN.md\` |
1348
+ | An opening cutscene or any full-screen video — \`engine.playVideo\` | \`video-cutscenes.md\` |
1342
1349
  | Collectibles and interactables | \`collectible-objects.md\` |
1343
1350
  | Placed scenery / \`environmentObjects\` — gameplay near props, harvesting, occupancy | \`environment-objects.md\` |
1344
1351
  | Doors, locked gates, keycards | \`doors-and-locks.md\` |