@hypersoniclabs/helix-mcp 0.2.5 → 0.2.12
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/README.md +81 -11
- package/dist/continuumCanary.d.ts +17 -0
- package/dist/continuumCanary.js +17 -0
- package/dist/continuumCanary.js.map +1 -0
- package/dist/server.d.ts +14 -1
- package/dist/server.js +4058 -162
- package/dist/server.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/vehicleTools.d.ts +86 -0
- package/dist/vehicleTools.js +229 -0
- package/dist/vehicleTools.js.map +1 -0
- package/docs/avatar-face.md +115 -0
- package/docs/bridge.md +98 -0
- package/docs/bring-your-world.md +117 -0
- package/docs/catalog.md +69 -1
- package/docs/character-animation.md +442 -0
- package/docs/character-attachments.md +166 -0
- package/docs/character-world.md +785 -130
- package/docs/continuum.md +153 -0
- package/docs/items.md +73 -0
- package/docs/lighting-world.md +667 -0
- package/docs/locomotion-clip-spec.md +294 -0
- package/docs/manifest.md +31 -6
- package/docs/multiplayer-logic.md +460 -17
- package/docs/multiplayer-templates/chrono-orchard.md +36 -22
- package/docs/multiplayer-templates/collect-a-thon.md +28 -38
- package/docs/multiplayer-templates/collections.md +24 -24
- package/docs/multiplayer-templates/hangout.md +124 -111
- package/docs/multiplayer-templates/npc-wave.md +310 -0
- package/docs/multiplayer-templates/obby.md +13 -16
- package/docs/multiplayer-templates/persistent-progress.md +218 -0
- package/docs/multiplayer-templates/physics-bumper.md +27 -9
- package/docs/multiplayer-templates/physics-football.md +22 -8
- package/docs/multiplayer-templates/relic-bearers.md +12 -15
- package/docs/multiplayer-templates/server-motion.md +16 -19
- package/docs/multiplayer-templates/shooter-range.md +275 -0
- package/docs/multiplayer-templates/team-control.md +28 -15
- package/docs/multiplayer-templates/turn-arena.md +31 -22
- package/docs/multiplayer-templates/voice-radio.md +166 -0
- package/docs/multiplayer-templates/wave-survival.md +7 -8
- package/docs/multiplayer-templates/world-shop.md +240 -0
- package/docs/multiplayer-world.md +222 -129
- package/docs/npc-world.md +623 -0
- package/docs/publishing.md +108 -28
- package/docs/purchases.md +223 -0
- package/docs/scene-performance.md +64 -0
- package/docs/screenshots.md +140 -0
- package/docs/sdk.md +324 -5
- package/docs/shooter-worlds.md +537 -0
- package/docs/terrain.md +173 -0
- package/docs/upgrades.md +324 -0
- package/docs/vehicles.md +727 -0
- package/docs/world-inspect.md +156 -0
- package/docs/world-look.md +241 -0
- package/docs/world-recipe.md +65 -6
- package/package.json +15 -4
- package/skills/README.md +91 -0
- package/skills/helix-assets/SKILL.md +491 -0
- package/skills/helix-assets/references/asset-sources.md +143 -0
- package/skills/helix-assets/references/vault-api.md +105 -0
- package/skills/helix-avatar-qa/SKILL.md +85 -0
- package/skills/helix-avatars/SKILL.md +206 -0
- package/skills/helix-avatars/references/contract.md +166 -0
- package/skills/helix-avatars/references/dynamics.md +367 -0
- package/skills/helix-avatars/references/face.md +50 -0
- package/skills/helix-avatars/references/publish.md +76 -0
- package/skills/helix-avatars/references/qa.md +251 -0
- package/skills/helix-avatars/references/rigging.md +88 -0
- package/skills/helix-avatars/references/source-generated.md +190 -0
- package/skills/helix-avatars/references/source-model.md +90 -0
- package/skills/helix-avatars/references/source-rigid.md +90 -0
- package/skills/helix-avatars/references/source-vrm.md +61 -0
- package/skills/helix-gauntlet/SKILL.md +128 -0
- package/skills/helix-multiplayer/SKILL.md +150 -0
- package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
- package/skills/helix-vehicles/SKILL.md +218 -0
- package/skills/helix-vehicles/references/addons.md +212 -0
- package/skills/helix-vehicles/references/appearance.md +339 -0
- package/skills/helix-vehicles/references/audio-import.md +138 -0
- package/skills/helix-vehicles/references/audio.md +580 -0
- package/skills/helix-vehicles/references/cabin.md +225 -0
- package/skills/helix-vehicles/references/host-manifest.md +174 -0
- package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
- package/skills/helix-vehicles/references/publish.md +214 -0
- package/skills/helix-vehicles/references/qa.md +177 -0
- package/skills/helix-vehicles/references/reference-package.json +3481 -0
- package/skills/helix-vehicles/references/reference-package.md +69 -0
- package/skills/helix-vehicles/references/source-beamng.md +167 -0
- package/skills/helix-vehicles/references/source-concept.md +38 -0
- package/skills/helix-vehicles/references/source-model.md +100 -0
- package/skills/helix-vehicles/references/source-scratch.md +60 -0
- package/skills/helix-world-build/SKILL.md +376 -0
- package/skills/helix-world-build/references/config-gates.md +104 -0
- package/skills/helix-world-director/SKILL.md +210 -0
- package/skills/helix-world-qa/SKILL.md +371 -0
- package/skills/helix-world-qa/references/perf-budgets.md +240 -0
- package/skills/helix-world-qa/references/perf-handle.md +125 -0
- package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Source: rigid bodies (robots, CAD and URDF, scanned or segmented kits, armour)
|
|
2
|
+
|
|
3
|
+
Lanes:
|
|
4
|
+
- **Unitree G1**: a URDF with CAD STL meshes.
|
|
5
|
+
- **C-3PO**: a geometry-only scan in 242 welded parts, ripped from a web viewer.
|
|
6
|
+
- **T-800**: hard-surface plates on a skinned source.
|
|
7
|
+
|
|
8
|
+
A rigid avatar uses no dynamics. Its quality comes from the pivots, the pose and the materials.
|
|
9
|
+
|
|
10
|
+
## Bind 1:1 to bones
|
|
11
|
+
|
|
12
|
+
Every part gets weight 1.0 on **exactly one** `helix-humanoid@1` bone:
|
|
13
|
+
|
|
14
|
+
| Parts | Bone |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Pelvis shell | `pelvis` |
|
|
17
|
+
| Torso and chest plates | `spine_05` |
|
|
18
|
+
| Head | `head` |
|
|
19
|
+
| Shoulder bell and upper arm | `upperarm_*` |
|
|
20
|
+
| Forearm | `lowerarm_*` |
|
|
21
|
+
| Hand and fingers | `hand_*`, unless you build finger joints |
|
|
22
|
+
| Thigh, knee drum and shin, foot | `thigh_*`, `calf_*`, `foot_*` |
|
|
23
|
+
|
|
24
|
+
- **Put the joints at the mechanical pivots.** On a URDF, these are the joint origins.
|
|
25
|
+
- Author the bones with canonical names and pass an identity `--map`.
|
|
26
|
+
- Exceptions:
|
|
27
|
+
- **Pistons** that span two bones are split along the rod, and each half binds to its own bone, so
|
|
28
|
+
they telescope.
|
|
29
|
+
- **Cables, wires and hoses** keep a two-bone blend: pelvis to `spine_05` by height, for example.
|
|
30
|
+
|
|
31
|
+
**Segmenting a welded scan.** Assign each connected component to the nearest bone capsule, using the
|
|
32
|
+
source's own joint positions. Keep an override file for components the heuristic gets wrong
|
|
33
|
+
(drop / forearm / piston), and read `JOINTS_0` back per LOD afterwards.
|
|
34
|
+
|
|
35
|
+
## Pose to the canonical A-pose
|
|
36
|
+
|
|
37
|
+
Source poses vary: URDF zero pose, hands on hips, a T-pose. Re-pose **per segment**:
|
|
38
|
+
1. Map each segment's measured frame onto its canonical frame. For a limb, use the bone direction plus
|
|
39
|
+
the hinge plane. For a hand, the finger direction plus the palm normal. For a leg, the leg
|
|
40
|
+
direction plus the foot heading.
|
|
41
|
+
2. Find the pelvis, torso and head from a mirror-plane symmetry search.
|
|
42
|
+
|
|
43
|
+
The importer's rest aim then comes out as identity, and the shared clips play as designed. Unitree G1
|
|
44
|
+
solved its rest pose by optimisation: arms rotated +8° and thighs splayed 3.1° to match the canonical
|
|
45
|
+
A-pose.
|
|
46
|
+
|
|
47
|
+
## Size, and the short-body gate
|
|
48
|
+
|
|
49
|
+
- Scale to the real height. Do not stretch to human height: Unitree G1 is 1.32 m.
|
|
50
|
+
- **Under 1.35 m, locomotion retargets only if (pelvis − foot) / 0.877 < 0.65.** Measure that ratio
|
|
51
|
+
before you publish.
|
|
52
|
+
- G1's natural ratio was 0.74, so the clips ran unscaled and the feet sank 2–5.7 cm.
|
|
53
|
+
- Placing the pelvis joint 7.5 cm lower, at 0.60 m (ratio 0.62), fixed it.
|
|
54
|
+
- The mesh did not move. Only the pivot did.
|
|
55
|
+
|
|
56
|
+
## Materials without textures
|
|
57
|
+
|
|
58
|
+
A CAD or scan source with no textures gets **one palette atlas material**. Rules map faces to
|
|
59
|
+
colours, with dark variants in occluded crevices:
|
|
60
|
+
- C-3PO's palette was 4 × 4 at 256 px;
|
|
61
|
+
- Unitree G1's atlas was 256 px, holding base, metal/rough and emissive.
|
|
62
|
+
|
|
63
|
+
The cost is one material: 6 draws across three LODs, with a few MB of file.
|
|
64
|
+
|
|
65
|
+
Do not attempt a unique-UV normal bake on thousands of small parts. C-3PO's attempt produced about
|
|
66
|
+
8,000 islands at 9–31 % coverage and noisy normals. Transfer normals from the high-poly mesh onto the
|
|
67
|
+
decimated one instead.
|
|
68
|
+
|
|
69
|
+
## Geometry
|
|
70
|
+
|
|
71
|
+
- Weld and smooth normals at about 40°.
|
|
72
|
+
- Ray-test and cull hidden faces outside the moving limbs (about 16k on C-3PO).
|
|
73
|
+
- Decimate per segment toward a shared triangle total.
|
|
74
|
+
- Replace brackets that decimate badly with convex hulls.
|
|
75
|
+
|
|
76
|
+
Budgets that shipped:
|
|
77
|
+
|
|
78
|
+
| Avatar | Triangles | Draws | File |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| Unitree G1 | 34,998 / 14,531 / 4,944 | 6 | 4.49 MB |
|
|
81
|
+
| C-3PO | 38,999 / 13,993 / 5,646 | 6 | 3.39 MB |
|
|
82
|
+
|
|
83
|
+
## Taste
|
|
84
|
+
|
|
85
|
+
- A 5 % head scale-up read as stocky.
|
|
86
|
+
- Pale cream eyes and pale gold read as washed out.
|
|
87
|
+
- Judge against real photographs, using the same viewer for each candidate. For Unitree G1, the
|
|
88
|
+
URDF's own render served as the baseline.
|
|
89
|
+
|
|
90
|
+
The critic plateaued at about 6/10 on these lanes. Say which exit you took.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Source: a VRM (`.vrm`, VRoid and VRM 0.x / 1.0)
|
|
2
|
+
|
|
3
|
+
VRM is the only source that arrives with a humanoid map, spring bones and a face. **Never hand-roll a
|
|
4
|
+
VRM conversion.** The bridge does it in-process:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
bridge_import({ source: "/abs/avatar.vrm", type: "character", canonical: "/abs/base.web.glb", output: "/abs/out-vrm" })
|
|
8
|
+
# CLI: helix bridge import avatar.vrm --type character --canonical base.web.glb -o out-vrm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`canonical` is **required, with no default**. It is the base body `*.web.glb` that the canonical
|
|
12
|
+
skeleton is fitted from: the stock `body-f-default.web.glb` or `body-m-default.web.glb` from a
|
|
13
|
+
throwaway character world's asset pack (`qa` step 3). The base you fit to is part of the result, so
|
|
14
|
+
record which one you used.
|
|
15
|
+
|
|
16
|
+
The output is `<slug>.glb` plus `vrm-conversion-receipt.json`. There is no `*.avatar-import.json` for
|
|
17
|
+
this route.
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
| Area | Result |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Skeleton | Read from the file's own humanoid map, handling the thumb-name difference between VRM 0.x and 1.0. It is flipped to +Z forward and conformed to `helix-humanoid@1`. Spring-bone joints survive as auxiliary joints. |
|
|
24
|
+
| Dynamics | SpringBone becomes `helix/dynamics@1`, with real unit conversion, on every scene. A chain survives only if its root survived the conform **and** something in its subtree is weighted. Set `VRM_DEBUG_CHAINS=1` to print kept and dropped chains. |
|
|
25
|
+
| Colliders | Used colliders are kept and renamed onto canonical bones. VRM 1.0 sphere and capsule shapes import. VRM 0.x is sphere only. |
|
|
26
|
+
| Face | Expressions become `helix/avatar-face@1`, capability `vowel5`, with both blinks and the expression presets. **No `jawOpen` is declared**, on purpose. Undeclared blend shapes are pruned; a VRoid face drops from 57 shapes to 13. |
|
|
27
|
+
| First-person hide | Written automatically: meshes with at least 80 % head weight. |
|
|
28
|
+
| Budget | **A single scene: no LOD chain.** It decimates to 15,000 triangles, textures to 512 px, and atlases to 8 materials or fewer. |
|
|
29
|
+
|
|
30
|
+
## Audit the conversion before you publish
|
|
31
|
+
|
|
32
|
+
1. **Read the receipt's warnings**: dropped collider shapes, clamped stiffness or damping, guessed
|
|
33
|
+
importance.
|
|
34
|
+
2. **Check every `center` chain.**
|
|
35
|
+
- VRoid writes a `center` on most spring groups by default. The CLI approximates it as
|
|
36
|
+
`inertia: 0.6`.
|
|
37
|
+
- On short chains (ears, bangs, accessories), 0.6 lets them swing 90°–120° at a sprint.
|
|
38
|
+
- Retune them to the short-chain values in `dynamics`: inertia about 0.97, stiffness 1.0, damping
|
|
39
|
+
0.5, a 22° cone.
|
|
40
|
+
3. **Per-joint variation is lost.** Only each spring's first joint values are used. Where the source
|
|
41
|
+
tapered stiffness along a strand, write a curve (`[root, tip]`).
|
|
42
|
+
4. **Heavily dragged chains all clamp to critical damping** and stop looking different from each
|
|
43
|
+
other. If the hair reads as one stiff mass, lower damping on the long strands.
|
|
44
|
+
5. **Look at it running** (see `qa`). VRM tuning assumed three-vrm's frame-rate-dependent integrator,
|
|
45
|
+
and HELIX's solver is exact. The same numbers can read differently.
|
|
46
|
+
|
|
47
|
+
Edit only the `helixDynamics` JSON in place, on every scene. The catears lane's approach was to
|
|
48
|
+
rewrite just the GLB JSON chunk and copy the binary chunk verbatim. Never re-import to change tuning.
|
|
49
|
+
|
|
50
|
+
## Limits of this route
|
|
51
|
+
|
|
52
|
+
- One LOD at 15k triangles. That is far under the 60k budget, but it means no near-field detail.
|
|
53
|
+
- A VRoid model exported as **GLB** instead of VRM loses all of this. It imports through
|
|
54
|
+
`import_character` with `face: none`, unless you pass a `faceMap`, and with no dynamics.
|
|
55
|
+
- Get the `.vrm` if one exists.
|
|
56
|
+
- If not, write the `faceMap` (`read_doc({ name: "avatar-face" })`) and author dynamics per
|
|
57
|
+
`dynamics`.
|
|
58
|
+
- Cutting a **wearable** from a VRM (`keepMaterials`) uses the same converter at a smaller budget
|
|
59
|
+
(8k triangles, 4 materials). Chains with no garment geometry under them are dropped.
|
|
60
|
+
- A published package-backed VRM base cannot have its mesh replaced. The pin-correction route admits
|
|
61
|
+
stiffness and damping changes only. Tune before the first publish.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: helix-gauntlet
|
|
3
|
+
description: MANDATORY before any HELIX item is published, given a new version, listed, repriced into sale or reported done — vehicles, add-ons, avatars, wearables, props and worlds. Pick an external bar (photos and specs of the real thing), render the candidate in the real runtime from a fixed inspection set, look at every render against the bar, get a fresh blind critic's verdict where you can spawn subagents, fix and re-render, and ship only on a pass. Also for re-checking an item that is already listed when the gates change or you update it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HELIX Gauntlet
|
|
7
|
+
|
|
8
|
+
Every gate in the other skills measures something. This one makes you **look**, against something real.
|
|
9
|
+
|
|
10
|
+
**Why it exists.** A Ferrari F40 passed every measured gate, a submitted visual-QA run and an owner-proxy
|
|
11
|
+
ACCEPT, and went on sale. Its owner then opened it and found the driver lying almost flat, both hands off
|
|
12
|
+
the steering wheel, the fingers twitching, and the passenger folded with the knees at the chest. The
|
|
13
|
+
proxy had written "reclined, knees up" in its notes and called it non-blocking; nobody looked at the
|
|
14
|
+
hands. A Porsche 993 shipped with its wheels tucked into the arches because QA read numbers and never
|
|
15
|
+
looked at the car. Both defects were obvious in one picture. Neither picture was looked at against the
|
|
16
|
+
real car.
|
|
17
|
+
|
|
18
|
+
## The rule
|
|
19
|
+
|
|
20
|
+
- **No publish, new version, listing or "done" without a PASS from this loop.** Not your belief that it
|
|
21
|
+
passes: the artifacts below, written to the ledger.
|
|
22
|
+
- **A defect you can see is a FAIL even when every number passes.** There is no "minor" for posture,
|
|
23
|
+
hands, fingers, clipping, floating, missing or wrong-coloured parts. "Reclined", "knees up", "hands off
|
|
24
|
+
the rim", "fingers moving" are FAIL reasons, never notes.
|
|
25
|
+
- **Every executable assert is blocking.** A tool exit 1 is a FAIL; fix it before you spend a critic.
|
|
26
|
+
- **Only the real runtime counts.** Judge renders from the HELIX runtime (`preview_vehicle`, visual QA),
|
|
27
|
+
never Blender viewport shots, a GLB viewer, or the source code.
|
|
28
|
+
|
|
29
|
+
## The loop
|
|
30
|
+
|
|
31
|
+
Keep `gauntlet/` beside the item's source: `bar/`, `round-1/`, `round-2/`…, and `LEDGER.md`.
|
|
32
|
+
|
|
33
|
+
1. **Pick the bar, once.** The bar is the real thing: reference photos of the exact car / character /
|
|
34
|
+
object at the inspection poses (front, both 3/4 views, both sides, rear, top, cockpit and interior,
|
|
35
|
+
wheel, lamp and detail close-ups) plus its published specs. Save them in `gauntlet/bar/` with
|
|
36
|
+
`INDEX.md` naming each photo's pose and source. A fictional item's bar is its concept art plus a named
|
|
37
|
+
real product of the same kind ("the Ferrari F40's cabin" for a fictional supercar's cabin). Never a
|
|
38
|
+
rubric you wrote. Freeze it: every round is judged against the same photos.
|
|
39
|
+
2. **Render the fixed inspection set in the real runtime** (the set per kind is below). Same cameras every
|
|
40
|
+
round — the tool owns them; never hand-pick flattering angles.
|
|
41
|
+
3. **Gate on the executable asserts first.** Every blocking assert the tool prints must pass. Fix
|
|
42
|
+
failures now; a critic on a car that fails its asserts is wasted.
|
|
43
|
+
4. **Look — yourself — at every sheet.** Open each sheet the tool lists, in order, beside the matching
|
|
44
|
+
bar photo. Each sheet carries a LOOK question; answer it in `LEDGER.md` per sheet: `PASS` or `FAIL`
|
|
45
|
+
plus every defect you see. Do not skip a sheet because its numbers passed.
|
|
46
|
+
5. **Get a blind verdict.** If your client can spawn subagents, spawn a FRESH one that never saw the build
|
|
47
|
+
and give it only: the sheets at the size the tool wrote them (copied into a neutral folder, renamed
|
|
48
|
+
`sheet-01.jpg`…, nothing in a name or path that says which item, round or author), the bar photos,
|
|
49
|
+
and the critic brief below. A sheet the critic says it cannot judge (too dark, hidden, too small) is
|
|
50
|
+
a FAIL of the inspection, not a pass: fix the render, not the verdict. If you
|
|
51
|
+
cannot spawn one, re-read only the brief and the images and judge again from cold, and say in the
|
|
52
|
+
report that the critic was not independent.
|
|
53
|
+
6. **Fix every defect the asserts, your look and the critic name; re-render; re-judge.** Keep the best
|
|
54
|
+
render set (the ratchet): a round replaces it only when it wins on every sheet it touches.
|
|
55
|
+
**Two rounds per approach, then diagnose**: name each remaining defect tunable or structural, and for a
|
|
56
|
+
structural one ask what the bar has that this approach cannot produce, and whether you can reuse the
|
|
57
|
+
bar's own machinery (a real licensed mesh of the car, the platform's seat rig and H-points, the
|
|
58
|
+
engine's own fold) instead of imitating it. Change approach; do not tune a third round.
|
|
59
|
+
7. **Exit, and say which exit.** `PASS`: every blocking assert passes, every sheet PASS in your look, and
|
|
60
|
+
the critic picks `MATCHES` on every sheet with an overall score ≥ 8. Anything else is not a pass: a
|
|
61
|
+
round or budget limit is a **brake** — do not publish or list; report the item blocked with its top
|
|
62
|
+
unresolved defect and the sheet that shows it.
|
|
63
|
+
|
|
64
|
+
## Updating or re-checking a listed item
|
|
65
|
+
|
|
66
|
+
A listed item is never re-judged when a gate is added, so a car on sale can be running on a record that
|
|
67
|
+
predates the gate that would fail it. **Before and after you touch a listed car, run
|
|
68
|
+
`check_listed_items({ itemIds: [<id>], rerun: true })`.** STALE (a current gate never ran) or FAIL means
|
|
69
|
+
the car is on sale broken: run this loop on it, stage a new version of the SAME item, pass hosted visual
|
|
70
|
+
QA on that version, and until then delist it (`delist_item`). With no ids it re-checks every listed
|
|
71
|
+
vehicle.
|
|
72
|
+
|
|
73
|
+
## The vehicle inspection set (fixed; `preview_vehicle` renders all of it)
|
|
74
|
+
|
|
75
|
+
`preview_vehicle({ target: <dir> })` for a candidate, `preview_vehicle({ item: <id> })` for a published
|
|
76
|
+
car. Occupants and the inspection set are ON by default; `occupants: false` / `inspection: false` are for
|
|
77
|
+
a quick iteration only and are never a gate result. It writes `<out>/inspection/index.json` and these
|
|
78
|
+
sheets (≈1280 px JPEGs, one LOOK question each):
|
|
79
|
+
|
|
80
|
+
| Sheet | What is in it |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `exterior-day` | 8 angles: front, front-left 3/4, left, rear-left 3/4, rear, rear-right 3/4, right, front-right 3/4 |
|
|
83
|
+
| `exterior-night` | the same 8 at night, low beam on |
|
|
84
|
+
| `lamps-night` | every lamp function lit: high beam, fog, brake, reverse, each indicator front and rear, hazards |
|
|
85
|
+
| `wheels` | each wheel close-up, low and square-on |
|
|
86
|
+
| `panels-open` | every door and lid at full open |
|
|
87
|
+
| `seat-<seat>` | **every seat occupied**, each from first person, through the side glass, over the shoulder, through the windscreen |
|
|
88
|
+
| `seat-<seat>-detail` | the same occupant: cutaway, head to roof, hips, feet, door open |
|
|
89
|
+
| `driver-steering` | the driver steering left / centre / right: hands view and first person |
|
|
90
|
+
| `stillness` | three frames 0.3 s apart of every occupant's hands |
|
|
91
|
+
| add-ons | every add-on fitted: `preview_vehicle({ target or item, addon: [<dir>] })` per part, its contact sheet |
|
|
92
|
+
|
|
93
|
+
What a PASS looks like, per occupant: torso at a normal driving recline (not lying back), thighs about
|
|
94
|
+
level (knees not at the chest), head clear of the roof and not wedged in the seat wings, feet on the
|
|
95
|
+
pedals or floor, nothing through the bodywork; the driver's hands **on the rim at about 9 and 3 and
|
|
96
|
+
turning with it** at both locks; fingers and hands identical across the stillness frames (any twitch or
|
|
97
|
+
"grasping" is a FAIL). The blocking asserts behind those: `vehicle.occupants.torso_angle`, `knees_up`,
|
|
98
|
+
`head_in_wings`, `head_clearance`, `hips_on_seat`, `feet_on_floor`, `penetration`,
|
|
99
|
+
`hands_on_wheel_steering`, `hands_follow_wheel`, `hands_at_nine_and_three`, `fingers_still`,
|
|
100
|
+
`hands_still`, `seat_rig_warnings` (the seat rig logged a rim out of reach and parked a hand in the lap), and `preview.inspection.complete` (a sheet with a frame missing is not an inspection).
|
|
101
|
+
When an occupant assert fails, fix the cabin (seat H-point, cushion, footwell, wheel position and reach:
|
|
102
|
+
`read_skill({ name: "helix-vehicles", reference: "cabin" })`), never the threshold.
|
|
103
|
+
|
|
104
|
+
## Other kinds
|
|
105
|
+
|
|
106
|
+
The loop is the same; the inspection set comes from the kind's own tools.
|
|
107
|
+
|
|
108
|
+
- **Avatars** (`helix-avatars`, `helix-avatar-qa`): bar = the reference sheet or photos of the character
|
|
109
|
+
at front, side, back and 3/4; inspection = the visual-QA turnaround, the walk frames and the
|
|
110
|
+
Marketplace preview walk, on both base bodies where relevant.
|
|
111
|
+
- **Wearables, props and other items** (`helix-assets`): bar = product photos at matched angles;
|
|
112
|
+
inspection = the `visual_qa_item` frames (fitted on the body or placed in the scene).
|
|
113
|
+
- **Worlds** (`helix-world-qa`): bar = the named reference art or a comparable published world;
|
|
114
|
+
inspection = the fixed-camera screenshots and the mobile frames from the QA pass, plus frame time.
|
|
115
|
+
|
|
116
|
+
## Critic brief (give it verbatim with the sheets and the bar photos)
|
|
117
|
+
|
|
118
|
+
> You are judging renders of a product against photos of the real thing. You did not make it and you do
|
|
119
|
+
> not know who did. For EACH sheet, in order: (1) first write `MATCHES` or `DOES NOT MATCH` the reference
|
|
120
|
+
> — decide before you write anything else; (2) list every visible defect: wrong shape or proportion,
|
|
121
|
+
> missing or extra parts, wrong colour or material, anything floating, clipping, see-through, black or
|
|
122
|
+
> glowing where it should not; for any seated person, posture (reclined, knees up, head jammed against
|
|
123
|
+
> the roof or wedged in the seat wings), hands not on the steering wheel or not turning with it, fingers
|
|
124
|
+
> that change between frames, feet not on the floor or pedals; (3) score the sheet 0–10, where any
|
|
125
|
+
> posture, hands or fingers defect caps the score at 3. Then give an overall score 0–10 and the three
|
|
126
|
+
> defects that matter most, each with the sheet that shows it. Be harsh: you are protecting a buyer.
|
|
127
|
+
|
|
128
|
+
Record the critic's picks, defects and scores per round in `LEDGER.md`, and keep its raw answer.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: helix-multiplayer
|
|
3
|
+
description: Design HELIX multiplayer against what the declarative rule DSL can actually express — and recognise early when a mechanic falls outside it, before it is half-built. Use for any shared-space or shared-game world, for converting a single-player world to multiplayer, and whenever a proposed mechanic involves hit detection, pathfinding, hidden information, persistence or physics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HELIX Multiplayer
|
|
7
|
+
|
|
8
|
+
Multiplayer on HELIX is **declarative and server-authoritative**. You declare state and
|
|
9
|
+
`when`/`if`/`then` rules as data in `helix.json`; a platform-owned room interprets them at a
|
|
10
|
+
fixed 20 Hz. **Your world ships no server code.** There is no server file to write, and no
|
|
11
|
+
place to put one.
|
|
12
|
+
|
|
13
|
+
That is a genuine constraint, not a temporary gap. This skill exists so you find the edge
|
|
14
|
+
of the DSL during design, not after you have built three-quarters of a mechanic it cannot
|
|
15
|
+
express.
|
|
16
|
+
|
|
17
|
+
## Read the contract, do not reconstruct it
|
|
18
|
+
|
|
19
|
+
`read_doc("multiplayer-world")` is the hub. `read_doc("multiplayer-logic")` is the full
|
|
20
|
+
grammar — 17 rule events, 25 effects, the expression language, the caps. `list_templates`
|
|
21
|
+
+ `read_template` give you complete worked worlds. **Every verb, op and event you write is
|
|
22
|
+
validated at publish; one that is not in the grammar is rejected with a did-you-mean.**
|
|
23
|
+
Do not author the block from memory.
|
|
24
|
+
|
|
25
|
+
This skill covers what those docs deliberately do not: the boundary, and what to do at it.
|
|
26
|
+
|
|
27
|
+
## The capability boundary
|
|
28
|
+
|
|
29
|
+
### Expressible today
|
|
30
|
+
Shared numbers/strings/booleans/vec3/refs/lists/counterMaps · per-player and per-room state
|
|
31
|
+
· zones (enter / exit / inside, static or entity-attached) · timers, keyed or room-scoped ·
|
|
32
|
+
a phase state machine with late-join policy · server-authoritative entity motion
|
|
33
|
+
(`linear`/`orbit`/`seek`/`waypoints`) · client-hosted entities with ownership transfer ·
|
|
34
|
+
networked physics (dynamic rigid bodies, authority-transfer or dual-sim) · turn order and
|
|
35
|
+
elimination · teams · server→client broadcasts and client→server validated actions ·
|
|
36
|
+
proximity or global voice with channels · `playerContact` at a radius — the tag primitive.
|
|
37
|
+
|
|
38
|
+
### Not expressible — and the DSL will not tell you nicely
|
|
39
|
+
| You want | Why it cannot be declared | What to do instead |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| "in front of me", a facing cone, a direction vector | There is **no vec3 arithmetic.** Vec3 exists only as a literal, `distance`, and `randomPoint`. No dot, cross, normalise, add or subtract. | Reduce to a distance test (`playerContact`, a zone), or resolve client-side and send a declared `action` the rules validate. |
|
|
42
|
+
| Raycast, hitscan, line of sight, cover | No raycast primitive exists at any layer of the DSL. | Client detects, sends `action`; the rule validates distance + cooldown + phase before applying. Accepts that the client is trusted about *aim*, never about *outcome*. |
|
|
43
|
+
| Navmesh pathfinding, obstacle avoidance, "decide" AI | `seek` is a straight line. Navigating *around* things is not in the grammar. | `authority: "owner"` entity, simulated client-side. Spoofable by design — never let it write another player's state. |
|
|
44
|
+
| Progress that survives the session — currency, inventory, unlocks | Room state is per-instance and dies with the room. | `Helix.dataStore` (get/set/delete/list). **No multi-key transaction** — design so no two keys must move together. |
|
|
45
|
+
| Fog of war, hidden roles, a private hand, secret objectives | **Every value in room state is broadcast to every client.** A "hidden" playerVar is readable in devtools by every player in the room. There is no per-player-private state. | Redesign to open information, or keep the secret client-side and accept it is unenforceable. Do not ship a social-deduction game on this and call the secret secure. |
|
|
46
|
+
| Server-authoritative hit resolution | Follows from no raycast + no vec3 math. The server cannot independently confirm a hit. | The `claimHit` pattern: a declared action carrying the target ref, validated in the rule for distance, cooldown and phase, then writing through `{ref: action.args.target, var: "health"}`. Cheat-*resistant*, not cheat-proof. Say which you have. |
|
|
47
|
+
|
|
48
|
+
Full construct-by-construct map with the escape hatch for each:
|
|
49
|
+
`references/dsl-capability-map.md`.
|
|
50
|
+
|
|
51
|
+
## The three escape hatches, in order of preference
|
|
52
|
+
|
|
53
|
+
1. **A declared `action`.** Client sends intent, the server validates args and applies the
|
|
54
|
+
consequence in a rule. **This is the cheat-resistant path and the correct default for
|
|
55
|
+
anything that affects another player.**
|
|
56
|
+
2. **An `authority: "owner"` entity.** A client simulates and uploads; the server
|
|
57
|
+
sanity-checks `maxSpeed` and relays. The escape hatch for motion the server cannot
|
|
58
|
+
compute. Spoofable — never use it as an authority over other players.
|
|
59
|
+
3. **Redesign the mechanic.** Usually the right answer for hidden information and for
|
|
60
|
+
precise hit resolution. Cheaper than a half-working version of a game the platform
|
|
61
|
+
cannot host.
|
|
62
|
+
|
|
63
|
+
## The cross-player write firewall — a publish-time error, not a runtime one
|
|
64
|
+
|
|
65
|
+
Publish **statically blocks** owner-entity logic from any cross-player write **or read**:
|
|
66
|
+
no `set`/`add`/`teleport`/`respawn`/`destroyEntity`/collection mutation/ownership
|
|
67
|
+
verb/keyed timer/`broadcast` aimed at another member, and no dereferencing a client-supplied
|
|
68
|
+
ref to *read* another member's var. This fails the publish, not a code review. Design around
|
|
69
|
+
it up front.
|
|
70
|
+
|
|
71
|
+
## The #1 conversion bug — client-side teleport
|
|
72
|
+
|
|
73
|
+
A client that moves its own body across the map trips the room's movement gate. The gate
|
|
74
|
+
holds the seat's last plausible position and **never self-heals**: you keep playing locally,
|
|
75
|
+
and every other player sees you frozen or vanished at the old spot. **Solo testing cannot
|
|
76
|
+
surface this.**
|
|
77
|
+
|
|
78
|
+
Every kill-plane, checkpoint, round reset, portal, out-of-bounds handler and "play again"
|
|
79
|
+
flow becomes a server `respawn`/`teleport` effect. Round resets are one rule
|
|
80
|
+
(`forEachPlayer` → `respawn`), never a broadcast each client answers by teleporting itself
|
|
81
|
+
— that is one copy of the bug per player.
|
|
82
|
+
|
|
83
|
+
**Artifact check.** Every local transform write must be inside a single-player branch:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
grep -nE "\.(teleport|respawn)\(" src/*.ts src/**/*.ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each hit is either inside `if (!mp.room) { … }` or it is a bug. `source-audit.mjs` runs this
|
|
90
|
+
and fails on an unguarded hit in a world whose manifest declares `multiplayer`.
|
|
91
|
+
|
|
92
|
+
## Verify with two clients — a delta assertion, not a final-state check
|
|
93
|
+
|
|
94
|
+
Authority bugs are invisible solo, and a final-state check passes on a disconnected client.
|
|
95
|
+
Assert the **change**, in the *other* window:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// A and B are two browser contexts on the built preview, both joined.
|
|
99
|
+
const before = await B.evaluate(() => window.__helixMpProbe.remote(0).position.z);
|
|
100
|
+
await A.evaluate(() => window.__helixMpProbe.requestRespawn()); // triggers the SERVER rule
|
|
101
|
+
await expect.poll(() => B.evaluate(() => window.__helixMpProbe.remote(0).position.z))
|
|
102
|
+
.toBeGreaterThan(before + 0.5); // B saw A move. Magnitude, not truthiness.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Expose `window.__helixMpProbe` from world code behind a query param, the same pattern
|
|
106
|
+
`world.ready()` and `screenshots.register()` use — a no-op during normal play. Without a
|
|
107
|
+
probe you are asserting on pixels, which cannot distinguish "moved" from "repainted".
|
|
108
|
+
|
|
109
|
+
**The gate:** the artifact is the test's exit code plus both screenshots, written to
|
|
110
|
+
`qa/mp-two-client/`. A passing description of a two-window test is not the test.
|
|
111
|
+
|
|
112
|
+
## Voice is on by default
|
|
113
|
+
|
|
114
|
+
Every multiplayer world ships voice unless it has a reason not to. `scaffold_world` already
|
|
115
|
+
declares `voice.proximity` and writes the fail-soft join. A deliberately silent world opts
|
|
116
|
+
out by removing **both** the permission and the block.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
if (mp.room) {
|
|
120
|
+
try { if (await Helix.voice.join()) mp.attachVoice(Helix.voice, manifest.multiplayer.voice); }
|
|
121
|
+
catch (err) { console.info('voice unavailable:', err); } // never blocks play
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Tuning lives in the manifest `multiplayer.voice` block, not in code. Declare
|
|
126
|
+
`"spatial": true` when *presence* is the product — social spaces, horror, hide-and-seek,
|
|
127
|
+
stealth. Skip it for announcer-shaped or competitive-callout voice. Channels are
|
|
128
|
+
**exclusive**: a player is in the ambient space or in exactly one channel. Mic behavior
|
|
129
|
+
(mode, device, volumes, mutes) belongs to the player via the platform tablet — **a world
|
|
130
|
+
never renders mic UI.**
|
|
131
|
+
|
|
132
|
+
## Rules that bite
|
|
133
|
+
|
|
134
|
+
- **It must work solo.** Guests, standalone and failed joins all still play. The facade
|
|
135
|
+
sets up your local player unconditionally and layers multiplayer on only when a join
|
|
136
|
+
succeeds. Gate any "needs 2 players" logic explicitly.
|
|
137
|
+
- **Read state through the typed accessors, fresh each frame** — `room.vars.num('x')`,
|
|
138
|
+
`room.me.str('team')`, `room.player(id)`, `room.phase()`. Colyseus maps are mutated in
|
|
139
|
+
place: copy values you cache, never alias. Guard against the pre-first-patch empty state.
|
|
140
|
+
- **Skip your own `sessionId`** when spawning remotes.
|
|
141
|
+
- **Mirror `maxSpeed`** between the DSL and the `EntityScene` options — the reconcile clamp
|
|
142
|
+
reads the client copy.
|
|
143
|
+
- **`maxReplicas` is a real budget** (each replica is a full character); keep it equal to
|
|
144
|
+
`maxPlayers`. 2–8 is the sweet spot. `uploadHz: 20` caps `maxPlayers` at 12; the platform
|
|
145
|
+
caps concurrent players per room at 24 regardless.
|
|
146
|
+
- **Never call `room.uploadEntity` directly** — register through `EntityScene`.
|
|
147
|
+
- **Gestures replicate by default.** Trigger them from input handlers, never from
|
|
148
|
+
replicated-state observers, or every client fires its own copy.
|
|
149
|
+
- **The per-tick evaluation budget is static.** A too-large `forEach` fan-out or nested
|
|
150
|
+
`aggregate`/`nearest*` fails publish before it ever runs. Simplify or spread across ticks.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# The DSL capability map
|
|
2
|
+
|
|
3
|
+
A design-time lookup: can the declarative rule DSL express this, and if not, which escape
|
|
4
|
+
hatch does it take. The authoritative grammar is `read_doc("multiplayer-logic")` — this file
|
|
5
|
+
is the judgment layer on top of it.
|
|
6
|
+
|
|
7
|
+
Read the middle column as the *mechanism*, not as a rule of thumb. An agent that knows why
|
|
8
|
+
a thing is impossible handles the case this table does not list.
|
|
9
|
+
|
|
10
|
+
## Expression language — what you can compute in an `if`
|
|
11
|
+
|
|
12
|
+
| Available | Notes |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `+ - * /` | **numbers only** |
|
|
15
|
+
| `== != < <= >= >` | `==`/`!=` on same-typed scalars (not vec3); ordering ops require numbers |
|
|
16
|
+
| `and` / `or` / `not` | |
|
|
17
|
+
| `distance(a, b)` | the **only** vec3 operation that produces a number |
|
|
18
|
+
| `randomPoint(min, max)` | the only op that produces a vec3 |
|
|
19
|
+
| `playerCount` · `random` · `now` · `timeInState` | leaf generators |
|
|
20
|
+
| `aggregate` (`count`/`sum`/`min`/`max`/`avg`/`argmax`/`argmin`) over players, a zone, or an entity kind | with an optional `where` filter |
|
|
21
|
+
| `timerRemaining` | |
|
|
22
|
+
| `listLength` · `listAt` · `count` · `listCount` · `listIndexOf` | collections |
|
|
23
|
+
| `controlledBy` · `hostLoad` · `sameRef` | ownership |
|
|
24
|
+
|
|
25
|
+
**Not available at any depth:** vec3 addition/subtraction, dot, cross, normalise, length of
|
|
26
|
+
a difference, angle between, min/max of two scalars, string concatenation, modulo, sqrt,
|
|
27
|
+
trigonometry, nested collections, free-form JSON.
|
|
28
|
+
|
|
29
|
+
The consequence that catches people: **you cannot compute a direction.** Every mechanic
|
|
30
|
+
phrased as "in front of", "facing", "behind", "within a cone", "line of sight" reduces to a
|
|
31
|
+
distance test or leaves the DSL entirely.
|
|
32
|
+
|
|
33
|
+
## Mechanic → verdict
|
|
34
|
+
|
|
35
|
+
| Mechanic | Verdict | Route |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Score, lives, teams, rounds | ✅ | `roomVars` / `playerVars` + rules |
|
|
38
|
+
| Pickups, collectibles | ✅ | `spawnEntity` (server authority) + zone or `playerContact` |
|
|
39
|
+
| Capture points, king of the hill | ✅ | zone `zoneInside` + `aggregate` over `zone:<id>` |
|
|
40
|
+
| Checkpoints, obby, race | ✅ | ordered zones + a per-player checkpoint var + `respawn` |
|
|
41
|
+
| Patrolling NPC, moving platform, homing pickup | ✅ | entity `motion: waypoints`/`seek`/`orbit`, `authority: "server"` |
|
|
42
|
+
| Timed waves, day cycle, crop growth | ✅ | timers + `{op:"now"}` + phases |
|
|
43
|
+
| Turn-based / board game | ✅ | `states` + `advanceTurn` + `eliminate` |
|
|
44
|
+
| Cards, hand, inventory (in-session) | ✅ | `list` of flat records + `forEachInList` |
|
|
45
|
+
| Ball, puck, bumper cars | ✅ | `physics` on an `authority:"owner"` kind; `claimOnContact` for one contested body, `dualSimOnContact` for player-driven bodies |
|
|
46
|
+
| Tag / touch-based elimination | ✅ | `playerContact` at a radius |
|
|
47
|
+
| Walkie-talkies, private calls, team radio | ✅ | `multiplayer.voice.channels` + `Helix.voice.setChannel` |
|
|
48
|
+
| Enemy that paths around a wall | ❌ no pathfinding | `authority:"owner"` entity, client-simulated |
|
|
49
|
+
| Shooting with server-confirmed hits | ❌ no raycast, no vec3 math | declared `action` + rule-side distance/cooldown/phase validation. Cheat-**resistant**. |
|
|
50
|
+
| Melee arc / cone attack | ❌ no facing math | `playerContact` radius, or a client-detected `action` |
|
|
51
|
+
| Fog of war, hidden roles, secret hand | ❌ all state is broadcast | redesign to open information, or keep it client-side and say plainly that it is unenforceable |
|
|
52
|
+
| Saved currency, progression, unlocks | ❌ room state is per-instance | `Helix.dataStore` — **no multi-key transaction** |
|
|
53
|
+
| Persistent world edits between sessions | ❌ | `Helix.dataStore`, reloaded on join |
|
|
54
|
+
| Anti-cheat on client-simulated entities | ❌ by design | the server validates `maxSpeed` plausibility and nothing more |
|
|
55
|
+
|
|
56
|
+
## Why "hidden information" is the hardest no
|
|
57
|
+
|
|
58
|
+
Room state syncs to every connected client. A `playerVars` entry named `role` with value
|
|
59
|
+
`"traitor"` arrives in every player's browser and is one devtools expand away. There is no
|
|
60
|
+
per-recipient filtering in the broadcast path and no private channel for state.
|
|
61
|
+
|
|
62
|
+
`broadcast` with `to: <ref>` or `to: {team: "..."}` targets a **message**, not state — so a
|
|
63
|
+
one-shot secret *reveal* can be directed. That is enough for "you are the traitor, told
|
|
64
|
+
once at round start" **only if** nothing in synced state ever encodes it afterwards. The
|
|
65
|
+
moment a rule writes the role into a var to make a later rule work, it is public. Design
|
|
66
|
+
accordingly, and never describe such a world as having secure hidden roles.
|
|
67
|
+
|
|
68
|
+
## Entity authority — declare `motion` first
|
|
69
|
+
|
|
70
|
+
- `authority: "server"` (default) — deterministic, cheat-proof, never freezes. Reach for it
|
|
71
|
+
first. Covers straight-line `seek`, `waypoints`, `orbit`, `linear`, `static`.
|
|
72
|
+
- `authority: "owner"` — a client simulates and uploads; requires `maxSpeed`. The escape
|
|
73
|
+
hatch, spoofable by design.
|
|
74
|
+
- `shared: true` + `ownerLifecycle: "hostMigrate"` — game-owned, host re-elected on leave.
|
|
75
|
+
This is how a swarm spreads its simulation across clients.
|
|
76
|
+
|
|
77
|
+
Physics cross-field rules that publish enforces: a `physics` block **requires**
|
|
78
|
+
`authority: "owner"`; `claimOnContact` ⇒ `transferPolicy: "takeover"`; `dualSimOnContact` ⇒
|
|
79
|
+
`transferPolicy: "fixed"`; `collidesWith` names declared kinds. Cap: 8 physics kinds.
|
|
80
|
+
|
|
81
|
+
## Caps — publish rejects an over-cap config with a precise message
|
|
82
|
+
|
|
83
|
+
`roomVars`/`playerVars` 384 · `rules` 768 · effects per `then` 16 · `if` depth 8 / 64 nodes ·
|
|
84
|
+
entity kinds 32 · entities per kind 256 · entity vars 64 · zones 256 · timers 192 · phases 64 ·
|
|
85
|
+
events 256 · actions 256 · list `maxLen` 256 · counterMap keys 64 · record fields 8 · string
|
|
86
|
+
`maxLen` 1024 · physics kinds 8 · enum values 64 · cascade depth 16 · `tickNodeBudget`
|
|
87
|
+
100 000 (computed statically from rules × loop fan-out × cascade depth — the declaration caps
|
|
88
|
+
were raised, this budget was NOT, and it binds long before any declaration ceiling).
|
|
89
|
+
|
|
90
|
+
Nearing a cap is a design signal, not a tuning problem: a mechanic that wants hundreds of
|
|
91
|
+
rules probably wants a phase machine, a collection, or a `forEach` instead.
|
|
92
|
+
|
|
93
|
+
## Ordering semantics you will trip over
|
|
94
|
+
|
|
95
|
+
Rules evaluate in **declared order**; effects within a `then` apply in order; later rules
|
|
96
|
+
see earlier rules' writes within the same tick. Effects that fire secondary events
|
|
97
|
+
(`transitionTo`, `spawnEntity`, `destroyEntity`, ownership verbs) queue and drain
|
|
98
|
+
breadth-first **after** the primary rules. `varReached` is edge-checked once at the **end**
|
|
99
|
+
of the rule phase and is not part of the cascade. Publish rejects cascades that could cycle.
|
|
100
|
+
|
|
101
|
+
## Zone box sizing — the slip that costs an afternoon
|
|
102
|
+
|
|
103
|
+
A `zones[]` box takes `size` as **full dimensions**. A `physics.shape` box takes
|
|
104
|
+
`halfExtents`. They look identical and differ by 2×. `inspect_world` reads the declared
|
|
105
|
+
zones from the bundle and checks each against real geometry — the only way to catch a kill
|
|
106
|
+
band the pit floor misses or a checkpoint volume floating in empty space, because zones
|
|
107
|
+
never render.
|