@bitmagic/cli 0.1.64-dev.15 → 0.1.64-dev.17
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.
- package/assets/blender-level/README.md +6 -1
- package/assets/blender-level/bmlevel/core.py +78 -1
- package/assets/blender-level/review.py +106 -11
- package/assets/blender-level/tests/test_core.py +49 -0
- package/dist/ios/collect-urls.d.ts +2 -0
- package/dist/ios/collect-urls.js +2 -2
- package/dist/ios/collect-urls.js.map +1 -1
- package/dist/scaffold/blender-level-skill-files.d.ts +7 -0
- package/dist/scaffold/blender-level-skill-files.js +129 -21
- package/dist/scaffold/blender-level-skill-files.js.map +1 -1
- package/package.json +4 -4
|
@@ -15,10 +15,15 @@ skill's step 1 tells an agent how to copy them into a project's `tools/blender-l
|
|
|
15
15
|
| `bmlevel/` | Blender, or python3 for a dry run | the authoring vocabulary (`__init__.py`), the bpy-free maths and JSON writers (`core.py`), the Blender backend (`scene_bpy.py`) and the recording backend (`scene_null.py`) |
|
|
16
16
|
| `example_level.py` | either | a complete hut-and-courtyard level using every helper; the tests' fixture |
|
|
17
17
|
| `audit.py` | Blender | ray-cast audit of the COLLIDERS: floor continuity, door and route clearance, sealed envelopes, z-fighting candidates |
|
|
18
|
-
| `review.py` | Blender | deterministic review renders (overview, every door, every stair, every room) and a contact sheet |
|
|
18
|
+
| `review.py` | Blender | deterministic review renders (overview, every door, every stair, every room) and a contact sheet; `--shot 'FROM -> TO'` aims a frame anywhere, `--only` renders just those, `--lighting game` swaps the review fill for the engine's interior preset and the level's own lamps |
|
|
19
19
|
| `sync_world.py` | python3 | carries the doors, spawn and `terrain.shape: 'none'` into `src/work/world.json` |
|
|
20
20
|
| `tests/` | python3 | `python3 -m unittest discover -s tests` — no Blender needed; `cli`'s `pnpm test` runs it |
|
|
21
21
|
|
|
22
|
+
`review.py` and `audit.py` are the skill's **inner loop**: with the build they answer every
|
|
23
|
+
geometric and (under `--lighting game`) every lighting question about a level for a few seconds
|
|
24
|
+
and no network, which is what lets the engine handoff — `bitmagic assets add`, `sync_world.py`,
|
|
25
|
+
`bitmagic verify`, `bitmagic judge` — happen once per design pass instead of once per edit.
|
|
26
|
+
|
|
22
27
|
The JSON contract is owned by the engine (`game/src/engine/meshlevel/MeshLevelSchema.ts`,
|
|
23
28
|
documented in `game/agent-docs/mesh-level.md`); `core.py` writes exactly that shape, and the
|
|
24
29
|
engine's validator is the arbiter when the two disagree. The intended end state is a
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"""The bpy-free half of bmlevel: coordinates, records, collider math
|
|
1
|
+
"""The bpy-free half of bmlevel: coordinates, records, collider math, the JSON writers and the
|
|
2
|
+
review cameras — both the standard set (`review_cameras`) and the ad-hoc `--shot` specs
|
|
3
|
+
(`resolve_point`, `custom_shot`) review.py aims from a level JSON.
|
|
2
4
|
|
|
3
5
|
Everything a level script says is in GAME coordinates — metres, Y up, +Z forward, yaw in radians
|
|
4
6
|
about +Y so that an entity's forward at yaw θ is (sin θ, 0, cos θ) (engine/agent-docs/
|
|
@@ -436,3 +438,78 @@ def review_cameras(level: dict, doors: Sequence[dict] = ()) -> list[dict]:
|
|
|
436
438
|
fx, fz = rotate_xz(0.0, 1.0, yaw) if long_axis_z else rotate_xz(1.0, 0.0, yaw)
|
|
437
439
|
shots.append({'name': f"volume-{v['name']}", 'kind': 'persp', 'position': (p[0] + ox, eye_y, p[2] + oz), 'target': (p[0] + ox + fx * 4, eye_y - 0.3, p[2] + oz + fz * 4)})
|
|
438
440
|
return shots
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------- ad-hoc shots
|
|
444
|
+
|
|
445
|
+
EYE_HEIGHT = 1.6
|
|
446
|
+
"""Where a standing player's camera is above the floor — the height every review shot uses."""
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def level_center(level: dict) -> Vec3:
|
|
450
|
+
"""The centre of the collider bounds, at eye height above their floor: what a shot with no
|
|
451
|
+
target looks at."""
|
|
452
|
+
pts: list[Vec3] = []
|
|
453
|
+
for c in level.get('colliders', []):
|
|
454
|
+
if c['shape'] == 'box':
|
|
455
|
+
pts.extend(box_corners(c['position'], c['size'], c.get('yaw', 0.0)))
|
|
456
|
+
else:
|
|
457
|
+
pts.extend(flat_to_points(c['vertices']))
|
|
458
|
+
b = aabb(pts)
|
|
459
|
+
if not b:
|
|
460
|
+
return (0.0, EYE_HEIGHT, 0.0)
|
|
461
|
+
return ((b[0][0] + b[1][0]) / 2, b[0][1] + EYE_HEIGHT, (b[0][2] + b[1][2]) / 2)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def resolve_point(spec: str, level: dict, doors: Sequence[dict] = (), *, as_eye: bool = False) -> Vec3:
|
|
465
|
+
"""A game-space point from one end of a `--shot` spec.
|
|
466
|
+
|
|
467
|
+
`landmark:<name>`, `volume:<name>`, `door:<id>` or a raw `x,y,z`. Landmarks, doors and volumes
|
|
468
|
+
are recorded at the floor, the door's centre and the room's centre respectively, so `as_eye`
|
|
469
|
+
(the FROM end) lifts each to a standing camera and the TO end aims at the thing itself. Raw
|
|
470
|
+
coordinates are used exactly as written, either end — they are the escape hatch.
|
|
471
|
+
"""
|
|
472
|
+
spec = spec.strip()
|
|
473
|
+
kind, _, rest = spec.partition(':')
|
|
474
|
+
kind, rest = kind.strip().lower(), rest.strip()
|
|
475
|
+
if kind == 'landmark':
|
|
476
|
+
for l in level.get('landmarks', []):
|
|
477
|
+
if l['name'] == rest:
|
|
478
|
+
x, y, z = l['position']
|
|
479
|
+
return (x, y + (EYE_HEIGHT if as_eye else 1.0), z)
|
|
480
|
+
known = ', '.join(sorted(l['name'] for l in level.get('landmarks', []))) or '(none)'
|
|
481
|
+
raise ValueError(f'no landmark named {rest!r} — the level has: {known}')
|
|
482
|
+
if kind == 'volume':
|
|
483
|
+
for v in level.get('volumes', []):
|
|
484
|
+
if v['name'] == rest:
|
|
485
|
+
x, y, z = v['position']
|
|
486
|
+
return (x, y - v['size'][1] / 2 + EYE_HEIGHT, z) if as_eye else (x, y, z)
|
|
487
|
+
known = ', '.join(sorted(v['name'] for v in level.get('volumes', []))) or '(none)'
|
|
488
|
+
raise ValueError(f'no volume named {rest!r} — the level has: {known}')
|
|
489
|
+
if kind == 'door':
|
|
490
|
+
for d in doors:
|
|
491
|
+
if d['id'] == rest:
|
|
492
|
+
p = d['position']
|
|
493
|
+
x, y, z = (p['x'], p['y'], p['z']) if isinstance(p, dict) else tuple(p)
|
|
494
|
+
return (x, y - d['height'] / 2 + EYE_HEIGHT, z) if as_eye else (x, y, z)
|
|
495
|
+
known = ', '.join(sorted(d['id'] for d in doors)) or '(none)'
|
|
496
|
+
raise ValueError(f'no door with id {rest!r} — the level has: {known}')
|
|
497
|
+
parts = [p for p in spec.replace(' ', '').split(',') if p]
|
|
498
|
+
if len(parts) == 3:
|
|
499
|
+
try:
|
|
500
|
+
return (float(parts[0]), float(parts[1]), float(parts[2]))
|
|
501
|
+
except ValueError:
|
|
502
|
+
pass
|
|
503
|
+
raise ValueError(f'cannot read shot point {spec!r} — use landmark:<name>, volume:<name>, door:<id> or x,y,z')
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def custom_shot(spec: str, index: int, level: dict, doors: Sequence[dict] = (), fov: float | None = None) -> dict:
|
|
507
|
+
"""One `--shot` argument as a review shot. `'FROM -> TO'`, or `'FROM'` to look at the level's
|
|
508
|
+
centre. `index` numbers the frame (`shot-1`, `shot-2`, …)."""
|
|
509
|
+
frm, arrow, to = spec.partition('->')
|
|
510
|
+
position = resolve_point(frm, level, doors, as_eye=True)
|
|
511
|
+
target = resolve_point(to, level, doors) if arrow else level_center(level)
|
|
512
|
+
shot = {'name': f'shot-{index}', 'kind': 'persp', 'position': position, 'target': target, 'spec': spec.strip()}
|
|
513
|
+
if fov is not None:
|
|
514
|
+
shot['fov'] = fov
|
|
515
|
+
return shot
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
"""Headless review renders of a built level, run inside Blender:
|
|
2
2
|
|
|
3
3
|
"$BLENDER" -b --python tools/blender-level/review.py -- build/level/hut.blend build/level/hut.mesh-level.json build/level/review/
|
|
4
|
+
[--lighting review|game] [--shot 'FROM -> TO']... [--only] [--fov <degrees>]
|
|
4
5
|
|
|
5
6
|
Opens the .blend (named parts intact) and renders a deterministic set of shots from the level
|
|
6
7
|
JSON — `overview` (top-down), `door-<id>-a/b` (both approaches of every door), `stairs-<name>`
|
|
7
8
|
(from the foot), `volume-<name>` (inside every room) — into the output directory, plus
|
|
8
9
|
`contact-sheet.png` with all of them. Read the sheet, then the shots that look wrong.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
`--shot` adds a frame you aim yourself: both ends are `landmark:<name>`, `volume:<name>`,
|
|
12
|
+
`door:<id>` or a raw `x,y,z` in game coordinates, and `'FROM'` alone looks at the level's centre.
|
|
13
|
+
Repeat it; `--only` renders just those and skips the standard set and the sheet, which is the
|
|
14
|
+
one-render look at the thing you just changed.
|
|
15
|
+
|
|
16
|
+
`--lighting review` (the default) mounts a camera fill and a sun so nothing hides in shadow: this
|
|
17
|
+
is a look at the GEOMETRY. `--lighting game` instead runs the engine's `interior` preset and the
|
|
18
|
+
`lamp()`s and `spot()`s the level JSON declares, so the lighting pass can be judged here rather
|
|
19
|
+
than through a `bitmagic verify`. EEVEE is not three.js — trust "too dark" and "too bright", not
|
|
20
|
+
an exact colour.
|
|
12
21
|
"""
|
|
13
22
|
import json
|
|
14
23
|
import math
|
|
@@ -19,14 +28,40 @@ import bpy
|
|
|
19
28
|
from mathutils import Vector
|
|
20
29
|
|
|
21
30
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
22
|
-
from bmlevel.core import review_cameras, to_blender # noqa: E402
|
|
31
|
+
from bmlevel.core import custom_shot, review_cameras, to_blender # noqa: E402
|
|
23
32
|
|
|
24
33
|
argv = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else sys.argv[1:]
|
|
25
|
-
if
|
|
34
|
+
positional = [a for a in argv if not a.startswith('--')]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def flag_value(name: str, default: str | None = None) -> str | None:
|
|
38
|
+
return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else default
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def flag_values(name: str) -> list[str]:
|
|
42
|
+
return [argv[i + 1] for i, a in enumerate(argv) if a == name and i + 1 < len(argv)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# `--shot`, `--lighting` and `--fov` each consume the token after them, which would otherwise read
|
|
46
|
+
# as a positional path.
|
|
47
|
+
consumed = {argv[i + 1] for i, a in enumerate(argv) if a in ('--shot', '--lighting', '--fov') and i + 1 < len(argv)}
|
|
48
|
+
positional = [a for a in positional if a not in consumed]
|
|
49
|
+
if len(positional) < 3:
|
|
26
50
|
print(__doc__)
|
|
27
51
|
sys.exit(2)
|
|
28
|
-
BLEND, LEVEL_PATH, OUT =
|
|
52
|
+
BLEND, LEVEL_PATH, OUT = positional[0], positional[1], positional[2]
|
|
53
|
+
LIGHTING = (flag_value('--lighting', 'review') or 'review').lower()
|
|
54
|
+
if LIGHTING not in ('review', 'game'):
|
|
55
|
+
print(f"--lighting takes 'review' or 'game', not {LIGHTING!r}")
|
|
56
|
+
sys.exit(2)
|
|
57
|
+
SHOT_SPECS = flag_values('--shot')
|
|
58
|
+
ONLY = '--only' in argv
|
|
59
|
+
FOV = float(flag_value('--fov')) if flag_value('--fov') else None
|
|
60
|
+
if ONLY and not SHOT_SPECS:
|
|
61
|
+
print('--only needs at least one --shot')
|
|
62
|
+
sys.exit(2)
|
|
29
63
|
WIDTH, HEIGHT = 960, 540
|
|
64
|
+
DEFAULT_LENS = 24.0
|
|
30
65
|
|
|
31
66
|
level = json.load(open(LEVEL_PATH))
|
|
32
67
|
doors_path = LEVEL_PATH.replace('.mesh-level.json', '.doors.json')
|
|
@@ -47,7 +82,10 @@ if scene.world is None:
|
|
|
47
82
|
scene.world.use_nodes = True
|
|
48
83
|
bg = scene.world.node_tree.nodes['Background']
|
|
49
84
|
bg.inputs[0].default_value = (0.35, 0.4, 0.5, 1)
|
|
50
|
-
|
|
85
|
+
# The engine's `interior` preset holds environment at 0.22 and the skybox at 0.3 against an
|
|
86
|
+
# ambient floor of 0.8 (engine/agent-docs/mesh-level.md); a world background is EEVEE's nearest
|
|
87
|
+
# equivalent to that plus the retargeted HemisphereLight. `review` lights it flat instead.
|
|
88
|
+
bg.inputs[1].default_value = 0.6 if LIGHTING == 'review' else 0.35
|
|
51
89
|
|
|
52
90
|
cam_data = bpy.data.cameras.new('review')
|
|
53
91
|
cam = bpy.data.objects.new('review', cam_data)
|
|
@@ -59,10 +97,57 @@ fill_data.shadow_soft_size = 2
|
|
|
59
97
|
fill = bpy.data.objects.new('review-fill', fill_data)
|
|
60
98
|
scene.collection.objects.link(fill)
|
|
61
99
|
sun_data = bpy.data.lights.new('review-sun', 'SUN')
|
|
62
|
-
|
|
100
|
+
# The engine dims its sun to 8% indoors; `review` keeps a full one so geometry reads everywhere.
|
|
101
|
+
sun_data.energy = 2.5 if LIGHTING == 'review' else 2.5 * 0.08
|
|
63
102
|
sun = bpy.data.objects.new('review-sun', sun_data)
|
|
64
103
|
scene.collection.objects.link(sun)
|
|
65
104
|
sun.rotation_euler = (math.radians(50), math.radians(15), math.radians(30))
|
|
105
|
+
if LIGHTING == 'game':
|
|
106
|
+
fill.hide_render = True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def hex_rgb(value) -> tuple[float, float, float]:
|
|
110
|
+
"""`#rrggbb` (or a three.js colour number) to linear-ish rgb. The level JSON writes hex."""
|
|
111
|
+
if isinstance(value, str):
|
|
112
|
+
h = value.lstrip('#')
|
|
113
|
+
n = int(h, 16) if h else 0xFFFFFF
|
|
114
|
+
else:
|
|
115
|
+
n = int(value)
|
|
116
|
+
return (((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def add_level_lights() -> int:
|
|
120
|
+
"""The JSON's own lamps and spots as real EEVEE lights. three.js intensities are unitless and
|
|
121
|
+
Blender's are watts; 30 W per unit puts a default `lamp(intensity=8)` where the eye expects a
|
|
122
|
+
room light. The engine pools point lights (a room with eight lamps gets four in game) — this
|
|
123
|
+
lights all of them, so read brightness, not count."""
|
|
124
|
+
for light in level.get('lights', []):
|
|
125
|
+
color = hex_rgb(light.get('color', '#ffffff'))
|
|
126
|
+
if light['type'] == 'point':
|
|
127
|
+
data = bpy.data.lights.new(light['name'], 'POINT')
|
|
128
|
+
data.energy = light.get('intensity', 8.0) * 30
|
|
129
|
+
data.shadow_soft_size = 0.25
|
|
130
|
+
obj = bpy.data.objects.new(light['name'], data)
|
|
131
|
+
obj.location = to_blender(light['position'])
|
|
132
|
+
else:
|
|
133
|
+
data = bpy.data.lights.new(light['name'], 'SPOT')
|
|
134
|
+
data.energy = light.get('intensity', 20.0) * 30
|
|
135
|
+
# three.js `angle` is the half-cone; Blender's `spot_size` is the full one.
|
|
136
|
+
data.spot_size = min(light.get('angle', math.pi / 4) * 2, math.pi)
|
|
137
|
+
data.spot_blend = light.get('penumbra', 0.3)
|
|
138
|
+
data.shadow_soft_size = 0.1
|
|
139
|
+
data.use_shadow = bool(light.get('castShadow', True))
|
|
140
|
+
obj = bpy.data.objects.new(light['name'], data)
|
|
141
|
+
pos = Vector(to_blender(light['position']))
|
|
142
|
+
obj.location = pos
|
|
143
|
+
obj.rotation_euler = (Vector(to_blender(light['target'])) - pos).to_track_quat('-Z', 'Y').to_euler()
|
|
144
|
+
data.color = color
|
|
145
|
+
scene.collection.objects.link(obj)
|
|
146
|
+
return len(level.get('lights', []))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if LIGHTING == 'game':
|
|
150
|
+
print(f'LIGHTING game — interior preset, {add_level_lights()} level lights, emissive materials as authored')
|
|
66
151
|
|
|
67
152
|
|
|
68
153
|
def render(shot: dict) -> str:
|
|
@@ -77,8 +162,14 @@ def render(shot: dict) -> str:
|
|
|
77
162
|
fill.hide_render = True
|
|
78
163
|
else:
|
|
79
164
|
cam_data.type = 'PERSP'
|
|
80
|
-
|
|
81
|
-
|
|
165
|
+
fov = shot.get('fov', FOV)
|
|
166
|
+
if fov:
|
|
167
|
+
cam_data.lens_unit = 'FOV'
|
|
168
|
+
cam_data.angle = math.radians(fov)
|
|
169
|
+
else:
|
|
170
|
+
cam_data.lens_unit = 'MILLIMETERS'
|
|
171
|
+
cam_data.lens = DEFAULT_LENS
|
|
172
|
+
fill.hide_render = LIGHTING == 'game'
|
|
82
173
|
fill.location = pos + Vector((0, 0, 0.6))
|
|
83
174
|
path = os.path.join(OUT, f"{shot['name']}.png")
|
|
84
175
|
scene.render.filepath = path
|
|
@@ -86,10 +177,14 @@ def render(shot: dict) -> str:
|
|
|
86
177
|
return path
|
|
87
178
|
|
|
88
179
|
|
|
89
|
-
shots = review_cameras(level, doors)
|
|
180
|
+
shots = [] if ONLY else review_cameras(level, doors)
|
|
181
|
+
shots += [custom_shot(spec, i + 1, level, doors, FOV) for i, spec in enumerate(SHOT_SPECS)]
|
|
90
182
|
paths = [render(s) for s in shots]
|
|
91
183
|
for s, p in zip(shots, paths):
|
|
92
|
-
print(f
|
|
184
|
+
print(f"SHOT {s['name']} -> {p}" + (f" ({s['spec']})" if 'spec' in s else ''))
|
|
185
|
+
|
|
186
|
+
if ONLY:
|
|
187
|
+
sys.exit(0)
|
|
93
188
|
|
|
94
189
|
# Contact sheet: every shot at half size, four per row, labelled by file name in the log.
|
|
95
190
|
import numpy as np # Blender bundles numpy # noqa: E402
|
|
@@ -185,5 +185,54 @@ class SyncWorldTests(unittest.TestCase):
|
|
|
185
185
|
self.assertEqual({d['id'] for d in merged}, {'front', 'gone', 'new'})
|
|
186
186
|
|
|
187
187
|
|
|
188
|
+
class ShotSpecTests(unittest.TestCase):
|
|
189
|
+
"""`--shot` specs: the ad-hoc camera review.py aims from the level JSON."""
|
|
190
|
+
|
|
191
|
+
LEVEL = {
|
|
192
|
+
'colliders': [{'shape': 'box', 'position': [0, 0, 0], 'size': [10, 1, 10], 'yaw': 0}],
|
|
193
|
+
'landmarks': [{'name': 'spawn', 'position': [1, 0.1, 2], 'yaw': 0, 'tags': []}],
|
|
194
|
+
'volumes': [{'name': 'hall', 'position': [0, 1.75, 0], 'size': [12, 3.5, 8], 'yaw': 0, 'tags': []}],
|
|
195
|
+
}
|
|
196
|
+
DOORS = [{'id': 'north', 'position': {'x': 0, 'y': 1.1, 'z': -4}, 'rotationY': 0, 'width': 1.4, 'height': 2.2}]
|
|
197
|
+
|
|
198
|
+
def test_landmark_is_an_eye_at_the_from_end_and_the_thing_at_the_to_end(self):
|
|
199
|
+
self.assertEqual(core.resolve_point('landmark:spawn', self.LEVEL, as_eye=True), (1, 0.1 + core.EYE_HEIGHT, 2))
|
|
200
|
+
self.assertEqual(core.resolve_point('landmark:spawn', self.LEVEL), (1, 1.1, 2))
|
|
201
|
+
|
|
202
|
+
def test_volume_from_is_eye_height_above_its_floor_and_to_is_its_centre(self):
|
|
203
|
+
self.assertEqual(core.resolve_point('volume:hall', self.LEVEL, as_eye=True), (0, 1.75 - 1.75 + core.EYE_HEIGHT, 0))
|
|
204
|
+
self.assertEqual(core.resolve_point('volume:hall', self.LEVEL), (0, 1.75, 0))
|
|
205
|
+
|
|
206
|
+
def test_door_from_is_eye_height_above_its_threshold(self):
|
|
207
|
+
self.assertEqual(core.resolve_point('door:north', self.LEVEL, self.DOORS, as_eye=True), (0, 1.1 - 1.1 + core.EYE_HEIGHT, -4))
|
|
208
|
+
self.assertEqual(core.resolve_point('door:north', self.LEVEL, self.DOORS), (0, 1.1, -4))
|
|
209
|
+
|
|
210
|
+
def test_raw_coordinates_are_used_exactly_at_either_end(self):
|
|
211
|
+
self.assertEqual(core.resolve_point('3, -1.5,7', self.LEVEL, as_eye=True), (3.0, -1.5, 7.0))
|
|
212
|
+
self.assertEqual(core.resolve_point('3,-1.5,7', self.LEVEL), (3.0, -1.5, 7.0))
|
|
213
|
+
|
|
214
|
+
def test_an_unknown_name_lists_the_known_ones(self):
|
|
215
|
+
with self.assertRaises(ValueError) as raised:
|
|
216
|
+
core.resolve_point('landmark:altar', self.LEVEL)
|
|
217
|
+
self.assertIn('spawn', str(raised.exception))
|
|
218
|
+
with self.assertRaises(ValueError):
|
|
219
|
+
core.resolve_point('nonsense', self.LEVEL)
|
|
220
|
+
|
|
221
|
+
def test_a_shot_without_a_target_looks_at_the_level_centre(self):
|
|
222
|
+
shot = core.custom_shot('landmark:spawn', 1, self.LEVEL, self.DOORS)
|
|
223
|
+
self.assertEqual(shot['name'], 'shot-1')
|
|
224
|
+
self.assertEqual(shot['kind'], 'persp')
|
|
225
|
+
self.assertEqual(shot['target'], core.level_center(self.LEVEL))
|
|
226
|
+
self.assertNotIn('fov', shot)
|
|
227
|
+
|
|
228
|
+
def test_a_from_to_shot_carries_both_ends_and_the_fov(self):
|
|
229
|
+
shot = core.custom_shot('landmark:spawn -> volume:hall', 2, self.LEVEL, self.DOORS, fov=70)
|
|
230
|
+
self.assertEqual(shot['name'], 'shot-2')
|
|
231
|
+
self.assertEqual(shot['position'], (1, 0.1 + core.EYE_HEIGHT, 2))
|
|
232
|
+
self.assertEqual(shot['target'], (0, 1.75, 0))
|
|
233
|
+
self.assertEqual(shot['fov'], 70)
|
|
234
|
+
self.assertEqual(shot['spec'], 'landmark:spawn -> volume:hall')
|
|
235
|
+
|
|
236
|
+
|
|
188
237
|
if __name__ == '__main__':
|
|
189
238
|
unittest.main()
|
|
@@ -18,6 +18,8 @@ export declare function scanCompiledCodeUrls(compiledCode: string): Set<string>;
|
|
|
18
18
|
export declare function readCompiledGameCode(root: string): string;
|
|
19
19
|
interface AnimationEntry {
|
|
20
20
|
file: string;
|
|
21
|
+
/** Absolute URL overriding `<base>/<file>` — the capture-backed default locomotion uses it. */
|
|
22
|
+
url?: string;
|
|
21
23
|
motionId: string;
|
|
22
24
|
}
|
|
23
25
|
interface WeaponAnimationEntry {
|
package/dist/ios/collect-urls.js
CHANGED
|
@@ -140,7 +140,7 @@ export function collectAnimationUrls(assets, worldData, compiledCode) {
|
|
|
140
140
|
const cdnLibrary = [
|
|
141
141
|
...assets.coreAnimations.map((entry) => ({
|
|
142
142
|
...entry,
|
|
143
|
-
url: `${assets.animationBaseUrl}/${entry.file}`,
|
|
143
|
+
url: entry.url ?? `${assets.animationBaseUrl}/${entry.file}`,
|
|
144
144
|
})),
|
|
145
145
|
...assets.meleeWeaponAnimations,
|
|
146
146
|
];
|
|
@@ -150,7 +150,7 @@ export function collectAnimationUrls(assets, worldData, compiledCode) {
|
|
|
150
150
|
assets.generatedRangedWeaponAnimations,
|
|
151
151
|
assets.generatedDirectionalAnimations,
|
|
152
152
|
assets.generatedPostureAnimations,
|
|
153
|
-
].flatMap((pack) => pack.map((entry) => ({ ...entry, url: `${assets.generatedAnimationBaseUrl}/${entry.file}` })));
|
|
153
|
+
].flatMap((pack) => pack.map((entry) => ({ ...entry, url: entry.url ?? `${assets.generatedAnimationBaseUrl}/${entry.file}` })));
|
|
154
154
|
const generatedActive = assets.activeLibrary === 'generated';
|
|
155
155
|
const active = generatedActive ? generatedLibrary : cdnLibrary;
|
|
156
156
|
const inactive = generatedActive ? cdnLibrary : generatedLibrary;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collect-urls.js","sourceRoot":"","sources":["../../src/ios/collect-urls.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"collect-urls.js","sourceRoot":"","sources":["../../src/ios/collect-urls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,yFAAyF;AACzF,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAC;IAC/C,eAAe;IACf,cAAc;IACd,sBAAsB;IACtB,oBAAoB;IACpB,cAAc;IACd,gBAAgB;IAChB,oBAAoB;IACpB,WAAW;CACZ,CAAC,CAAC;AAeH,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,GAAgB;IAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,WAAW,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO;IACT,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAClD,OAAO;IACT,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YACnC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,oBAAoB,CAAC,YAAoB;IACvD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,KAAK,CAAC,iCAAiC,CAAC,IAAI,EAAE,EAAE,CAAC;QAChF,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,MAAM,IAAI,GAAG,CAAC,GAAW,EAAQ,EAAE;QACjC,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC;iBAC/B,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,QAAQ,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAClF,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,OAAO,CAAC,CAAC;IACd,OAAO,QAAQ,CAAC;AAClB,CAAC;AA4CD,SAAS,iBAAiB,CAAC,KAAc;IACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,OAAO,CACL,OAAO,MAAM,CAAC,gBAAgB,KAAK,QAAQ;WACxC,OAAO,MAAM,CAAC,yBAAyB,KAAK,QAAQ;WACpD,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ;WACxC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CACxC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAAY;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;IACnF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;QACnE,MAAM,MAAM,GAAI,MAAwC,CAAC,eAAe,CAAC;QACzE,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA2B,EAC3B,SAAyB,EACzB,YAAoB;IAEpB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS;QAC5B,MAAM,CAAC,gBAAgB;QACvB,MAAM,CAAC,yBAAyB;QAChC,MAAM,CAAC,aAAa;QACpB,MAAM,CAAC,kBAAkB;QACzB,MAAM,CAAC,gBAAgB;KACxB,CAAC,CAAC;IAEH,2FAA2F;IAC3F,kDAAkD;IAClD,MAAM,aAAa,GACjB,SAAS,CAAC,gBAAgB,EAAE,kBAAkB,KAAK,KAAK;WACrD,YAAY,CAAC,QAAQ,CAAC,aAAa,CAAC;WACpC,YAAY,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAC/C,IAAI,aAAa,EAAE,CAAC;QAClB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,IAAiB,EAAW,EAAE,CAChD,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC;WACjC,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEnE,+FAA+F;IAC/F,wDAAwD;IACxD,MAAM,UAAU,GAAkB;QAChC,GAAG,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACvC,GAAG,KAAK;YACR,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,gBAAgB,IAAI,KAAK,CAAC,IAAI,EAAE;SAC7D,CAAC,CAAC;QACH,GAAG,MAAM,CAAC,qBAAqB;KAChC,CAAC;IACF,MAAM,gBAAgB,GAAkB;QACtC,MAAM,CAAC,mBAAmB;QAC1B,MAAM,CAAC,8BAA8B;QACrC,MAAM,CAAC,+BAA+B;QACtC,MAAM,CAAC,8BAA8B;QACrC,MAAM,CAAC,0BAA0B;KAClC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,yBAAyB,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAC3G,CAAC;IAEF,MAAM,eAAe,GAAG,MAAM,CAAC,aAAa,KAAK,WAAW,CAAC;IAC7D,MAAM,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC;IAC/D,MAAM,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAEjE,uFAAuF;IACvF,IAAI,aAAa,EAAE,CAAC;QAClB,KAAK,MAAM,IAAI,IAAI,MAAM;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,UAAU,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,2FAA2F;IAC3F,wFAAwF;IACxF,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,SAAS,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACrD,MAAM,OAAO,GAAc,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,SAAS;QAC5D,MAAM,QAAQ,GAAI,MAAkC,CAAC,QAAQ,CAAC;QAC9D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC9C,IAAI,IAAI,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,aAAa,IAAI,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,IAAY;IAC9C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACjD,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9B,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAE7B,MAAM,YAAY,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;IAC9F,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,oBAAoB,CAAC,YAAY,CAAC;QAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAEpE,MAAM,eAAe,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACxD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CACR,yFAAyF;cACrF,uCAAuC,CAC5C,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,MAAM,SAAS,GAAG,oBAAoB,CACpC,eAAe,EACf,CAAC,SAAS,IAAI,EAAE,CAAmB,EACnC,YAAY,CACb,CAAC;QACF,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,IAAI;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChD,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,KAAK;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* and everything else goes to `bitmagic forge`. The CLI cannot tell which model drives it, so the
|
|
17
17
|
* gate is the agent's own call — stated once, plainly, where the planning skill sends it.
|
|
18
18
|
*
|
|
19
|
+
* Steps 4-9 are shaped around two loops, because the first creator to use this took the whole
|
|
20
|
+
* chain — build, upload, verify, judge — round on every edit, and the costs are nothing alike: a
|
|
21
|
+
* Blender build, render and ray audit are seconds and local, while `assets add` uploads, `verify`
|
|
22
|
+
* type-checks and drives real Chrome, and `judge` spends sparks. So the geometry loop is named as
|
|
23
|
+
* the inner one, the engine handoff is gated on it being clean, and `review.py --lighting game`
|
|
24
|
+
* exists so even the lighting pass can stay inside it.
|
|
25
|
+
*
|
|
19
26
|
* Backticks and `${` are escaped for the template literal; `project-files.test.ts` checks the
|
|
20
27
|
* rendered frontmatter and that no escape leaks into the output.
|
|
21
28
|
*/
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* and everything else goes to `bitmagic forge`. The CLI cannot tell which model drives it, so the
|
|
17
17
|
* gate is the agent's own call — stated once, plainly, where the planning skill sends it.
|
|
18
18
|
*
|
|
19
|
+
* Steps 4-9 are shaped around two loops, because the first creator to use this took the whole
|
|
20
|
+
* chain — build, upload, verify, judge — round on every edit, and the costs are nothing alike: a
|
|
21
|
+
* Blender build, render and ray audit are seconds and local, while `assets add` uploads, `verify`
|
|
22
|
+
* type-checks and drives real Chrome, and `judge` spends sparks. So the geometry loop is named as
|
|
23
|
+
* the inner one, the engine handoff is gated on it being clean, and `review.py --lighting game`
|
|
24
|
+
* exists so even the lighting pass can stay inside it.
|
|
25
|
+
*
|
|
19
26
|
* Backticks and `${` are escaped for the template literal; `project-files.test.ts` checks the
|
|
20
27
|
* rendered frontmatter and that no escape leaks into the output.
|
|
21
28
|
*/
|
|
@@ -78,6 +85,10 @@ grep -qx 'build/' .gitignore || echo 'build/' >> .gitignore
|
|
|
78
85
|
\`tools/blender-level/README.md\` lists what each file does. Level scripts go in \`tools/level/\`,
|
|
79
86
|
outputs in \`build/level/\`.
|
|
80
87
|
|
|
88
|
+
Run that install line again on an existing project too. It overwrites, and \`bitmagic upgrade\`
|
|
89
|
+
re-renders this skill but NOT the scripts — so a flag named below that your copy does not
|
|
90
|
+
recognise means the copy is old, not that the skill is wrong.
|
|
91
|
+
|
|
81
92
|
## 2. Plan the place in words, then as a table
|
|
82
93
|
|
|
83
94
|
Write the \`## Art direction\` of \`GAME-DESIGN.md\` first — \`bitmagic judge\` scores against it, and a
|
|
@@ -128,7 +139,34 @@ Rules that are not style:
|
|
|
128
139
|
- **Doors are records, not meshes.** \`door()\` writes a \`DoorDefinition\`; the engine builds and
|
|
129
140
|
animates the leaf. \`jamb=\` adds solid jambs and a header so the wall reads as a doorway.
|
|
130
141
|
|
|
131
|
-
##
|
|
142
|
+
## The two loops
|
|
143
|
+
|
|
144
|
+
Everything below runs in one of two loops, and knowing which one you are in is the difference
|
|
145
|
+
between building a level in a morning and building it in a day.
|
|
146
|
+
|
|
147
|
+
**The Blender loop** — edit \`tools/level/<name>.py\` → build → look → audit. Seconds each, all
|
|
148
|
+
local: nothing uploaded, no browser, no sparks. This is where the level is actually made, and you
|
|
149
|
+
should go round it tens of times.
|
|
150
|
+
|
|
151
|
+
**The engine handoff** — register the GLB → sync world.json → \`check\` → \`verify\` → \`judge\` →
|
|
152
|
+
walk it in \`bitmagic dev\`. \`assets add\` uploads to the CDN, \`verify\` type-checks the project and
|
|
153
|
+
drives a real Chrome through a settle loop, \`judge\` spends sparks and must not be run twice on
|
|
154
|
+
one milestone. Run it at the END of a pass — never after an edit.
|
|
155
|
+
|
|
156
|
+
**The gate: do not hand the level to the engine until the contact sheet has nothing wrong on it
|
|
157
|
+
and both audit modes exit 0.** Every build writes \`build/level/<name>.glb\` already; that is free.
|
|
158
|
+
Publishing it is what costs. A round trip through the engine to discover a hole in a floor is a
|
|
159
|
+
round trip the audit would have made for nothing, and the station lost a day to exactly that.
|
|
160
|
+
|
|
161
|
+
| The level is wrong like this | Found in |
|
|
162
|
+
|---|---|
|
|
163
|
+
| a hole in a floor, a doorway you cannot walk through, a route blocked by a prop, a room that is not sealed | the Blender loop — \`audit.py\`, both modes |
|
|
164
|
+
| z-fighting, a blank wall, a sign that reads backwards, a missing ceiling, stairs ending in air, a wedge of sky beside a junction, the wrong footprint | the Blender loop — the contact sheet |
|
|
165
|
+
| the level washed out or black, an empty published game, doors animating inside walls, no NPCs, the framerate | the engine handoff — nothing in Blender can see these |
|
|
166
|
+
|
|
167
|
+
## The Blender loop
|
|
168
|
+
|
|
169
|
+
### 4. Build
|
|
132
170
|
|
|
133
171
|
\`\`\`
|
|
134
172
|
"$BLENDER" -b --python tools/level/<name>.py
|
|
@@ -140,7 +178,7 @@ line to keep for step 7. Outputs: \`build/level/<name>.glb\` (one draw call per
|
|
|
140
178
|
or geometry error prints a Python traceback naming the helper and the object — fix and rebuild;
|
|
141
179
|
builds take seconds. \`python3 tools/level/<name>.py --dry-run\` gives the counts without Blender.
|
|
142
180
|
|
|
143
|
-
|
|
181
|
+
### 5. Render and look
|
|
144
182
|
|
|
145
183
|
\`\`\`
|
|
146
184
|
"$BLENDER" -b --python tools/blender-level/review.py -- build/level/<name>.blend build/level/<name>.mesh-level.json build/level/review/
|
|
@@ -152,7 +190,23 @@ is missing, stairs that end in the air, a sign that reads backwards, two surface
|
|
|
152
190
|
one another. Fix the script, rebuild, re-render. Do not go on to the audit while the sheet is
|
|
153
191
|
wrong; the audit finds holes, not ugliness.
|
|
154
192
|
|
|
155
|
-
|
|
193
|
+
The fixed set is what you read first. After that the flags turn it into a camera you can aim, so
|
|
194
|
+
a second look costs one render rather than a trip through the engine:
|
|
195
|
+
|
|
196
|
+
\`\`\`
|
|
197
|
+
… review.py -- <blend> <level json> <out dir> --shot 'landmark:spawn -> volume:hall' --only # just this frame
|
|
198
|
+
… review.py -- <blend> <level json> <out dir> --shot 'door:north door -> landmark:altar' --fov 50
|
|
199
|
+
… review.py -- <blend> <level json> <out dir> --lighting game # the lamps and spots you declared
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
\`--shot 'FROM -> TO'\` takes \`landmark:<name>\`, \`volume:<name>\`, \`door:<id>\` or a raw \`x,y,z\`, and
|
|
203
|
+
repeats; \`--only\` renders those instead of the standard set. \`--lighting game\` swaps the review
|
|
204
|
+
fill for the engine's \`interior\` preset and the JSON's own \`lamp()\` / \`spot()\` records, which is
|
|
205
|
+
how you judge the lighting pass without a verify — dark under \`--lighting game\` is dark in the
|
|
206
|
+
game. Keep the default \`--lighting review\` while you are judging geometry: it is lit so that
|
|
207
|
+
nothing can hide.
|
|
208
|
+
|
|
209
|
+
### 6. Audit
|
|
156
210
|
|
|
157
211
|
\`\`\`
|
|
158
212
|
"$BLENDER" -b --python tools/blender-level/audit.py -- build/level/<name>.mesh-level.json # doors open: floors, doorways, routes
|
|
@@ -166,7 +220,16 @@ on the line between two rooms, a step that ends short); a failed \`sealed_envelo
|
|
|
166
220
|
roof or wall. \`coplanar_overlap\` is a warning: two faces on one plane, which flicker. Never
|
|
167
221
|
answer a failure by removing the volume or the link that found it.
|
|
168
222
|
|
|
169
|
-
|
|
223
|
+
A failure sends you back to step 4, not on to step 7. Both modes exiting 0 with a clean contact
|
|
224
|
+
sheet is the whole gate; nothing below can be trusted until you have it.
|
|
225
|
+
|
|
226
|
+
## The engine handoff — once per pass
|
|
227
|
+
|
|
228
|
+
Steps 7 and 8 are the expensive half: an upload, a type-check, a real browser. Do them when the
|
|
229
|
+
Blender loop has nothing left to say, and then do all of them — a partial handoff is what makes
|
|
230
|
+
\`verify\` pass on stale files.
|
|
231
|
+
|
|
232
|
+
### 7. Register the GLB, sync world.json, write the game code
|
|
170
233
|
|
|
171
234
|
\`\`\`
|
|
172
235
|
bitmagic assets add build/level/<name>.glb --name <name> --keep-glb --asset-id level-<name> --height <native height> # the REGISTER line from step 4
|
|
@@ -197,7 +260,7 @@ this.playerController.configureFallRescue({ ...DEFAULT_FALL_RESCUE_OPTIONS, kill
|
|
|
197
260
|
(\`npc.spawn(x, z, y)\` — pass the storey's Y for interiors), \`volumesAt(position)\` for room logic,
|
|
198
261
|
the lighting modes, and \`samples/mesh-level-setup.ts\`.
|
|
199
262
|
|
|
200
|
-
|
|
263
|
+
### 8. Check, verify, walk it
|
|
201
264
|
|
|
202
265
|
\`bitmagic check\`, then \`bitmagic verify\`: the console must show one \`[MeshLevel] loaded …\` line
|
|
203
266
|
with the collider and light counts you expect, and no \`404\` for the GLB. Then walk it in the
|
|
@@ -205,16 +268,24 @@ with the collider and light counts you expect, and no \`404\` for the GLB. Then
|
|
|
205
268
|
proves geometry, not that the place is good to be in. \`bitmagic verify --platform mobile\` too if
|
|
206
269
|
phones matter.
|
|
207
270
|
|
|
271
|
+
All three of step 7 again on every handoff — register with the SAME \`--asset-id\` (the entry is
|
|
272
|
+
replaced), re-run \`sync_world.py\`, re-copy the JSON. Skip one and \`verify\` grades the previous
|
|
273
|
+
build: the collider count in \`[MeshLevel] loaded …\` is how you catch it.
|
|
274
|
+
|
|
208
275
|
## 9. Judge, then iterate in passes
|
|
209
276
|
|
|
210
277
|
\`bitmagic judge\` scores the verify screenshot against your art direction. Improve the level in
|
|
211
|
-
passes
|
|
278
|
+
passes — and a pass is **many turns of the Blender loop followed by one handoff**, not a handoff
|
|
279
|
+
per edit:
|
|
212
280
|
|
|
213
|
-
1. **Structure** — rooms, doors, routes, stairs, the spawn. Audit clean
|
|
281
|
+
1. **Structure** — rooms, doors, routes, stairs, the spawn. Audit clean in both modes; the one
|
|
282
|
+
verify is for walking it in \`bitmagic dev\`, which is the only thing Blender cannot tell you.
|
|
214
283
|
2. **Detail** — cladding, casings, coffers, deck plates, signage, props. All \`decor()\` unless the
|
|
215
|
-
player should bump into it; keep every travel clearance.
|
|
284
|
+
player should bump into it; keep every travel clearance. The sheet and the audit carry this
|
|
285
|
+
pass almost alone; the one verify is to confirm nothing regressed.
|
|
216
286
|
3. **Surfaces and light** — materials, emissive strips, warm and cool lamps, a spot or two with
|
|
217
|
-
shadows.
|
|
287
|
+
shadows. Render with \`--lighting game\` (step 5) until the rooms read, then verify and judge
|
|
288
|
+
once against the real thing.
|
|
218
289
|
|
|
219
290
|
\`references/level-design-passes.md\` says what each pass contains and when to stop.
|
|
220
291
|
|
|
@@ -328,6 +399,10 @@ the art, because decoration is exactly what must not be solid. The renders show
|
|
|
328
399
|
.blend**, so a level can pass the audit and look wrong, or look right and fail. Do both, in that
|
|
329
400
|
order: look first (cheap, catches design errors), audit second (catches the errors you cannot see).
|
|
330
401
|
|
|
402
|
+
Together they are the Blender loop, and they are the whole reason the engine handoff can wait:
|
|
403
|
+
between them they answer every geometric question about the level for the cost of a few seconds
|
|
404
|
+
and no upload. Go round them until neither has anything to say, and only then register the GLB.
|
|
405
|
+
|
|
331
406
|
## The checks
|
|
332
407
|
|
|
333
408
|
| Check | Mode | What it fires | Failure means | Usual fix |
|
|
@@ -368,6 +443,28 @@ no volumes passes every check and holds no claim.
|
|
|
368
443
|
\`volume-<name>\` (inside every room, from one end looking along it). Then \`contact-sheet.png\`,
|
|
369
444
|
four per row in the order the log prints.
|
|
370
445
|
|
|
446
|
+
## Aiming the camera, and the two lightings
|
|
447
|
+
|
|
448
|
+
The standard set is what you read first. After that, ask for what you want to see:
|
|
449
|
+
|
|
450
|
+
| Flag | Does |
|
|
451
|
+
|---|---|
|
|
452
|
+
| \`--shot 'FROM -> TO'\` | one extra frame. Both ends are \`landmark:<name>\`, \`volume:<name>\`, \`door:<id>\` or a raw \`x,y,z\` (game coordinates). Repeats; each frame is named \`shot-1\`, \`shot-2\`, … |
|
|
453
|
+
| \`--shot 'FROM'\` | from that point, looking at the level's centre — enough for "what does it look like from the spawn" |
|
|
454
|
+
| \`--only\` | render just the \`--shot\`s and skip the standard set and the sheet: one render, one look |
|
|
455
|
+
| \`--fov <deg>\` | horizontal field of view for the perspective shots; default 24 mm ≈ 74° |
|
|
456
|
+
| \`--lighting review\` | default. A camera-mounted fill and a sun, so nothing can hide in shadow |
|
|
457
|
+
| \`--lighting game\` | the engine's \`interior\` preset — the sun at 8%, the hemisphere at 0.65, your \`lamp()\`s and \`spot()\`s as real lights, emissive materials glowing at their \`glow\` |
|
|
458
|
+
|
|
459
|
+
Judge geometry under \`--lighting review\`; judge the lighting pass under \`--lighting game\`. The
|
|
460
|
+
second is close enough to the engine that a room which is black there is black in the game, so
|
|
461
|
+
pass 3 no longer needs a verify to find an unlit corner — only to confirm the fixed one.
|
|
462
|
+
|
|
463
|
+
Where \`--lighting game\` and the engine still differ: EEVEE is not three.js, so exposure and
|
|
464
|
+
falloff are approximate, and the engine pools point lights (a room with eight lamps gets four of
|
|
465
|
+
them in game, all eight in the render). Treat "too dark" and "too bright" as real; treat an exact
|
|
466
|
+
colour match as a job for the verify screenshot.
|
|
467
|
+
|
|
371
468
|
What to look for, frame by frame:
|
|
372
469
|
|
|
373
470
|
- **overview** — the footprint matches the plan; nothing floats outside the walls; corridors meet
|
|
@@ -378,24 +475,24 @@ What to look for, frame by frame:
|
|
|
378
475
|
- **stairs** — treads reach the landing; the rail follows the slope; nothing pokes through a step.
|
|
379
476
|
- **volumes** — a ceiling; walls meeting at the corners; props sitting on the floor, not in it.
|
|
380
477
|
|
|
381
|
-
The renderer adds its own fill light so interiors are legible; it shows geometry, not the game's
|
|
382
|
-
lighting. Judge the lighting in \`bitmagic verify\`'s screenshot and in the dev browser.
|
|
383
|
-
|
|
384
478
|
## When a render is right and the game is wrong
|
|
385
479
|
|
|
386
|
-
The two are the same geometry only if the GLB you registered is the one you just built.
|
|
387
|
-
|
|
388
|
-
copy the JSON into \`src/work/\`, and
|
|
389
|
-
|
|
390
|
-
times
|
|
480
|
+
The two are the same geometry only if the GLB you registered is the one you just built. That is
|
|
481
|
+
the whole content of the engine handoff, and it is all-or-nothing: register again (same
|
|
482
|
+
\`--asset-id\`, the entry is replaced), re-run \`sync_world.py\`, copy the JSON into \`src/work/\`, and
|
|
483
|
+
check \`[MeshLevel] loaded\` in the verify console reports the new collider count. A verify that
|
|
484
|
+
passes on stale files is the trap the station fell into three times — and the reason to hand off
|
|
485
|
+
rarely and completely rather than often and in pieces.
|
|
391
486
|
`;
|
|
392
487
|
}
|
|
393
488
|
export function renderBlenderPassesReference() {
|
|
394
489
|
return `# Level design in passes
|
|
395
490
|
|
|
396
491
|
A level that is only walls and floors is walkable and dead. The station that taught this skill
|
|
397
|
-
went from boxes to a place people wanted to screenshot in four passes
|
|
398
|
-
|
|
492
|
+
went from boxes to a place people wanted to screenshot in four passes. A pass is many turns of the
|
|
493
|
+
Blender loop — edit → build → review → audit — ended by one engine handoff; each pass's "Stop
|
|
494
|
+
when" below is the condition for that handoff, not a checkpoint you take on the way. The passes,
|
|
495
|
+
what goes in each, and when to stop.
|
|
399
496
|
|
|
400
497
|
## Pass 1 — structure
|
|
401
498
|
|
|
@@ -441,8 +538,13 @@ clean (detail is the pass that breaks clearances — a casing 20 cm into a doorw
|
|
|
441
538
|
- **The engine's interior preset dims the sun for you**; a level that looks washed out in the game
|
|
442
539
|
after looking fine in the render has an \`exterior\` mode set, or its own sun in \`lightingConfig\`.
|
|
443
540
|
|
|
444
|
-
|
|
445
|
-
|
|
541
|
+
This is the pass where the default review render lies to you — its fill light makes every room
|
|
542
|
+
legible whatever your lamps do. Iterate under \`review.py --lighting game\`, which runs that same
|
|
543
|
+
interior preset and your own \`lamp()\`s and \`spot()\`s, and a dark room shows up dark.
|
|
544
|
+
|
|
545
|
+
Stop when: nothing is unlit or blown out under \`--lighting game\`, then hand off — \`bitmagic judge\`
|
|
546
|
+
no longer names lighting or flat surfaces, and the level reads the same in the verify screenshot
|
|
547
|
+
as in the reference image.
|
|
446
548
|
|
|
447
549
|
## Pass 4 — the reference image
|
|
448
550
|
|
|
@@ -466,6 +568,12 @@ The engine side is \`MeshLevel\` — \`engine/agent-docs/mesh-level.md\` is the
|
|
|
466
568
|
\`engine/agent-docs/samples/mesh-level-setup.ts\` the compile-checked sample. This page is the
|
|
467
569
|
seam: what the build wrote, what each command does with it, and the four ways it goes wrong.
|
|
468
570
|
|
|
571
|
+
**This is the outer loop — run it once per pass, and run all of it.** An upload, a type-check and
|
|
572
|
+
a real browser, against a level the contact sheet and both audit modes have already cleared. Every
|
|
573
|
+
question you can answer in Blender, answer in Blender; what is left here is what only the running
|
|
574
|
+
game knows: how it feels to walk, how it is lit on a phone, and whether the published bundle can
|
|
575
|
+
fetch what it needs.
|
|
576
|
+
|
|
469
577
|
## The files the build wrote
|
|
470
578
|
|
|
471
579
|
| File | Goes | Carries |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blender-level-skill-files.js","sourceRoot":"","sources":["../../src/scaffold/blender-level-skill-files.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"blender-level-skill-files.js","sourceRoot":"","sources":["../../src/scaffold/blender-level-skill-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8QR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,iCAAiC;IAC/C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyFR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2FR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyER,CAAC;AACF,CAAC;AAED,MAAM,UAAU,mCAAmC;IACjD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+GR,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitmagic/cli",
|
|
3
|
-
"version": "0.1.64-dev.
|
|
3
|
+
"version": "0.1.64-dev.17",
|
|
4
4
|
"description": "Build Bitmagic games from your own agent tool",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "Bitmagic Oy",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"playwright-core": "^1.59.1",
|
|
26
26
|
"tar": "^7.4.3",
|
|
27
27
|
"ws": "^8.21.3",
|
|
28
|
-
"@bitmagic/
|
|
29
|
-
"@bitmagic/asset-core": "0.2.19-dev.
|
|
30
|
-
"@bitmagic/
|
|
28
|
+
"@bitmagic/animation-forger": "0.1.19-dev.17",
|
|
29
|
+
"@bitmagic/asset-core": "0.2.19-dev.17",
|
|
30
|
+
"@bitmagic/world-forger": "0.1.24-dev.17"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@eslint/js": "^9.38.0",
|