@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.
Files changed (98) hide show
  1. package/README.md +81 -11
  2. package/dist/continuumCanary.d.ts +17 -0
  3. package/dist/continuumCanary.js +17 -0
  4. package/dist/continuumCanary.js.map +1 -0
  5. package/dist/server.d.ts +14 -1
  6. package/dist/server.js +4058 -162
  7. package/dist/server.js.map +1 -1
  8. package/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/vehicleTools.d.ts +86 -0
  10. package/dist/vehicleTools.js +229 -0
  11. package/dist/vehicleTools.js.map +1 -0
  12. package/docs/avatar-face.md +115 -0
  13. package/docs/bridge.md +98 -0
  14. package/docs/bring-your-world.md +117 -0
  15. package/docs/catalog.md +69 -1
  16. package/docs/character-animation.md +442 -0
  17. package/docs/character-attachments.md +166 -0
  18. package/docs/character-world.md +785 -130
  19. package/docs/continuum.md +153 -0
  20. package/docs/items.md +73 -0
  21. package/docs/lighting-world.md +667 -0
  22. package/docs/locomotion-clip-spec.md +294 -0
  23. package/docs/manifest.md +31 -6
  24. package/docs/multiplayer-logic.md +460 -17
  25. package/docs/multiplayer-templates/chrono-orchard.md +36 -22
  26. package/docs/multiplayer-templates/collect-a-thon.md +28 -38
  27. package/docs/multiplayer-templates/collections.md +24 -24
  28. package/docs/multiplayer-templates/hangout.md +124 -111
  29. package/docs/multiplayer-templates/npc-wave.md +310 -0
  30. package/docs/multiplayer-templates/obby.md +13 -16
  31. package/docs/multiplayer-templates/persistent-progress.md +218 -0
  32. package/docs/multiplayer-templates/physics-bumper.md +27 -9
  33. package/docs/multiplayer-templates/physics-football.md +22 -8
  34. package/docs/multiplayer-templates/relic-bearers.md +12 -15
  35. package/docs/multiplayer-templates/server-motion.md +16 -19
  36. package/docs/multiplayer-templates/shooter-range.md +275 -0
  37. package/docs/multiplayer-templates/team-control.md +28 -15
  38. package/docs/multiplayer-templates/turn-arena.md +31 -22
  39. package/docs/multiplayer-templates/voice-radio.md +166 -0
  40. package/docs/multiplayer-templates/wave-survival.md +7 -8
  41. package/docs/multiplayer-templates/world-shop.md +240 -0
  42. package/docs/multiplayer-world.md +222 -129
  43. package/docs/npc-world.md +623 -0
  44. package/docs/publishing.md +108 -28
  45. package/docs/purchases.md +223 -0
  46. package/docs/scene-performance.md +64 -0
  47. package/docs/screenshots.md +140 -0
  48. package/docs/sdk.md +324 -5
  49. package/docs/shooter-worlds.md +537 -0
  50. package/docs/terrain.md +173 -0
  51. package/docs/upgrades.md +324 -0
  52. package/docs/vehicles.md +727 -0
  53. package/docs/world-inspect.md +156 -0
  54. package/docs/world-look.md +241 -0
  55. package/docs/world-recipe.md +65 -6
  56. package/package.json +15 -4
  57. package/skills/README.md +91 -0
  58. package/skills/helix-assets/SKILL.md +491 -0
  59. package/skills/helix-assets/references/asset-sources.md +143 -0
  60. package/skills/helix-assets/references/vault-api.md +105 -0
  61. package/skills/helix-avatar-qa/SKILL.md +85 -0
  62. package/skills/helix-avatars/SKILL.md +206 -0
  63. package/skills/helix-avatars/references/contract.md +166 -0
  64. package/skills/helix-avatars/references/dynamics.md +367 -0
  65. package/skills/helix-avatars/references/face.md +50 -0
  66. package/skills/helix-avatars/references/publish.md +76 -0
  67. package/skills/helix-avatars/references/qa.md +251 -0
  68. package/skills/helix-avatars/references/rigging.md +88 -0
  69. package/skills/helix-avatars/references/source-generated.md +190 -0
  70. package/skills/helix-avatars/references/source-model.md +90 -0
  71. package/skills/helix-avatars/references/source-rigid.md +90 -0
  72. package/skills/helix-avatars/references/source-vrm.md +61 -0
  73. package/skills/helix-gauntlet/SKILL.md +128 -0
  74. package/skills/helix-multiplayer/SKILL.md +150 -0
  75. package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
  76. package/skills/helix-vehicles/SKILL.md +218 -0
  77. package/skills/helix-vehicles/references/addons.md +212 -0
  78. package/skills/helix-vehicles/references/appearance.md +339 -0
  79. package/skills/helix-vehicles/references/audio-import.md +138 -0
  80. package/skills/helix-vehicles/references/audio.md +580 -0
  81. package/skills/helix-vehicles/references/cabin.md +225 -0
  82. package/skills/helix-vehicles/references/host-manifest.md +174 -0
  83. package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
  84. package/skills/helix-vehicles/references/publish.md +214 -0
  85. package/skills/helix-vehicles/references/qa.md +177 -0
  86. package/skills/helix-vehicles/references/reference-package.json +3481 -0
  87. package/skills/helix-vehicles/references/reference-package.md +69 -0
  88. package/skills/helix-vehicles/references/source-beamng.md +167 -0
  89. package/skills/helix-vehicles/references/source-concept.md +38 -0
  90. package/skills/helix-vehicles/references/source-model.md +100 -0
  91. package/skills/helix-vehicles/references/source-scratch.md +60 -0
  92. package/skills/helix-world-build/SKILL.md +376 -0
  93. package/skills/helix-world-build/references/config-gates.md +104 -0
  94. package/skills/helix-world-director/SKILL.md +210 -0
  95. package/skills/helix-world-qa/SKILL.md +371 -0
  96. package/skills/helix-world-qa/references/perf-budgets.md +240 -0
  97. package/skills/helix-world-qa/references/perf-handle.md +125 -0
  98. package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
@@ -0,0 +1,376 @@
1
+ ---
2
+ name: helix-world-build
3
+ description: Author a HELIX Instant world the platform's way — discover the catalog before building anything, tune through manifest config rather than the class API, keep helix.json schema-valid, and prove placement by measurement instead of screenshots. Use when scaffolding a world, building or dressing a scene, configuring the character, wiring HUD or input, adding gestures, or fixing an existing world's layout, lighting or materials.
4
+ ---
5
+
6
+ # HELIX World Build
7
+
8
+ ## Measure the first build
9
+
10
+ Immediately after the first build or imported Scene exists, call `analyze_scene_performance` with the exact Scene v2 document. Act on its instancing, material, lighting, delivery, and missing-proof findings before polish. Repeat with the final Package, `inspect_world` snapshot, and runtime receipt. The static grade is estimated utilization of an existing reference profile, never predicted FPS; unknowns remain unknown. Read `read_doc({ name: "scene-performance" })` for the counter contract and visual comparison rules.
11
+
12
+ The platform hands you a character system, a physics chassis, a camera rig, an input
13
+ router, a loading screen and a measurement tool. Your job is the world: geometry, art
14
+ direction, and the rules of the place. Everything the platform already owns, you configure
15
+ — you do not rebuild it.
16
+
17
+ ## Non-negotiables
18
+
19
+ These six are not taste. `helix-world-qa` fails the build on each of them, computed from
20
+ artifacts, so building against them wastes a whole pass.
21
+
22
+ 1. **Real assets, not primitives.** The scaffold's grey plane and boxes are a starting
23
+ point, never a shippable one. Source or generate real geometry — see `helix-assets`.
24
+ A world whose visible meshes are all `BoxGeometry`/`PlaneGeometry`/`SphereGeometry`
25
+ fails QA automatically. Primitives are legitimate for collision proxies, invisible
26
+ volumes, and blockout during authoring; they are not a look.
27
+ 2. **Real PBR materials.** `new THREE.MeshStandardMaterial({ color })` with nothing else
28
+ is the default white plastic every unfinished world wears. Every material a player sees
29
+ carries `roughness` and `metalness`, and ideally a `map`. Reflections need an environment:
30
+ the visual runtime provides it on the default lane; on the opt-out lane set
31
+ `scene.environment` from a PMREM-processed environment yourself — without one, metalness
32
+ and roughness have nothing to reflect and PBR looks like flat paint. Consuming the platform
33
+ pack correctly — per-axis repeat, multiplier semantics, whole-ORM binding, the import
34
+ checklist for third-party assets — is `read_doc({ name: "world-look" })`.
35
+ 3. **Platform lighting by default — and no more than eight punctual lights either way.** The
36
+ default lane: hand the canvas to `createVisualRuntime` (pin `visual` in `systems`), declare
37
+ how the world is lit in `public/helix.visuals.json`, and place NO sun and NO ambient — the
38
+ runtime owns the renderer, sky, sun, tone mapping and grade, and bounded static worlds bake
39
+ at publish (`read_doc({ name: "lighting-world" })`). The opt-out lane — legitimate for a
40
+ world with its own deliberate visual identity (a custom post stack, a stylized look) — is
41
+ the classic doctrine in full: not one ambient light; key + fill + bounce; set
42
+ `renderer.toneMapping` and `toneMappingExposure` (three.js defaults to `NoToneMapping`,
43
+ which is why untouched worlds look washed out and clipped). ON BOTH LANES, local lights are
44
+ *motivated*: every `PointLight`/`SpotLight` is added as a **child of the mesh that appears
45
+ to emit it**, never to the scene root (a pooled light is the exception — see below).
46
+ **The motivation rule and the count ceiling are one rule.** On its own, "every lantern gets
47
+ its own light" is how a world reaches 21 lights and 11 fps.
48
+ 4. **Audio on every interaction.** Every player action, pickup, hit, UI press, state change
49
+ and ambience has a sound. Wire the call site as you write the interaction — retrofitting
50
+ audio at the end is how worlds ship silent. Route every cue through one `playSfx(name)`
51
+ so coverage is countable, and **name the cue after its file** — `playSfx('pickup')`
52
+ resolves to `pickup.mp3|ogg|wav` in the bundle, and a synthesised cue carries the
53
+ `synth:` prefix (`playSfx('synth:ui-press')`). That convention is what lets QA prove a
54
+ cue is not merely declared. Sources and the safety rail: `helix-assets`.
55
+ 5. **Keep the platform mobile controls mounted.** A fresh character-world scaffold already
56
+ mounts the generated touch HUD and ships its v1 controls contract. Configure or extend that
57
+ system from registered action metadata; replace it only when the world genuinely needs an
58
+ equivalent custom controller and you will prove every required touch capability.
59
+ 6. **The world runs at 60 fps.** Not "is optimised afterwards" — designed inside a budget.
60
+ See the next section; `helix world perf-gate` fails the build on frame time now.
61
+
62
+ ## Design inside the budget — it is not an optimisation pass
63
+
64
+ A performance budget you consult after the world is built is a demolition order. These are the
65
+ numbers you *design within*, decided before you place the first lantern.
66
+
67
+ | Budget | Value | The constraint it puts on the design |
68
+ | --- | --- | --- |
69
+ | Punctual lights (point + spot) | **8 by design, 12 hard ceiling** | You get eight real lights for the whole world. Not eight per scene, per street, or per room — eight. |
70
+ | Shadow casters | **1 directional, 0 point, ≤1 spot** | One key light carries the shape read. A shadow-casting point light renders the scene **six times per frame**. |
71
+ | Drawing buffer | **≤2.6 M pixels**, as a derived ratio | Pick the ratio from the budget in your resize handler, never a fixed `Math.min(dpr, 2)`. |
72
+ | Shadow map | **≤2048², 1024² recommended** | A **memory** limit. Shrinking it returns megabytes, not milliseconds. |
73
+ | Draw calls / triangles | **≤300 / ≤500k per frame** | Each call costs ~15 µs of main-thread dispatch whatever it draws, so 300 is ~4.5 ms of a 16.7 ms frame. MESH count is the ceiling you hit first — instance repeated populations. |
74
+
75
+ **Why eight.** three.js has no light culling: every light in the scene is evaluated in the
76
+ fragment shader for **every lit pixel**, whether or not it reaches that pixel. The cost is not
77
+ linear — it is a shader-occupancy cliff. Measured on an M1 Pro: ~0.45 ms per light up to 8,
78
+ 0.75 ms at 12, then 6.1 ms per light at 20 and 9.6 ms at 24. **The 21st light costs 13× the
79
+ 4th.** A night market with a real light in every lantern ran at 39 fps median and 11 fps when
80
+ the player turned around. The full curve: `helix-world-qa/references/perf-budgets.md`.
81
+
82
+ **Eight lights does not mean eight lit things.** The design pattern that keeps a lantern-lined
83
+ street looking like one:
84
+
85
+ - **A fixed light pool.** Every lantern, bulb, brazier and lamp is an *emitter* — a description
86
+ of a light, not a light. Each frame, rank emitters by distance to the camera and bind the
87
+ nearest six into a fixed pool of six real lights, fading over the outer ~28% of each
88
+ emitter's range so a re-bind never pops. Shader cost is then constant however many lanterns
89
+ the world grows.
90
+ - **Instanced additive ground decals — this is the part that preserves the atmosphere.** One
91
+ instanced additive quad per emitter site, on the ground beneath it. A lantern that is not
92
+ currently in the pool still lays a warm smear on wet asphalt. 21 sites, **one draw call**,
93
+ unlit, no per-light cost.
94
+ - **Emissive materials.** A glowing bulb mesh reads as a light source without being one. Four
95
+ street-lamp spot lights were replaced by an emissive bulb inside each lamp's glass housing.
96
+ - **Raise ambient / hemisphere / `environmentIntensity`** to carry what the deleted always-on
97
+ lights used to, and check it against your original hero plate rather than by argument.
98
+ - **Strip lights that arrive inside loaded GLBs**, on placement. An authored prop shipping its
99
+ own `PointLight` bills every material in the world and never shows up in your budget.
100
+
101
+ Working code for the pool and the decals: `helix-world-qa/references/perf-budgets.md`.
102
+
103
+ **The same trick generalises to scenery** — every repeated decorative population (city windows,
104
+ star fields, foliage cards, crowds of repeated props) is one `InstancedMesh` per material family,
105
+ never N meshes. Draw calls cost ~15 µs of dispatch each whatever the mesh's size, so an agent-built
106
+ ~1,200-mesh skyline — 72k triangles — burned 19.7 ms a frame and ran 45 fps facing it. The cost
107
+ model and the remedy order: `read_doc({ name: "world-look" })`.
108
+
109
+ **Resolution is a budget, not a cap** (opt-out lane — on the default lane the quality tier
110
+ owns the pixel ratio and `visuals.setSize({ width, height })` is the only resize call):
111
+
112
+ ```ts
113
+ const BUDGET_PX = 2.6e6;
114
+ const ratioFor = (w: number, h: number, dpr = devicePixelRatio) =>
115
+ Math.max(1, Math.min(2, dpr, Math.sqrt(BUDGET_PX / (w * h))));
116
+ renderer.setPixelRatio(ratioFor(innerWidth, innerHeight)); // and again on resize
117
+ ```
118
+
119
+ At 1440×900 that resolves to 1.42; at 1920×1200, to 1.06 — the same 2.6 M pixels and the same
120
+ frame time on a bigger window. A fixed cap of 2 would be 9.2 M pixels there, and it passes on
121
+ whatever window you happened to test. `helix world perf-gate` grows the window mid-run specifically to
122
+ catch that.
123
+
124
+ **Ship the `?perf=1` handle** (`helix-world-qa/references/perf-handle.md`) — a ~40-line
125
+ `src/perf.ts` that installs nothing unless the query flag is present. It is what turns "the
126
+ world is slow" into "these three lights cost 74 ms", and what lets the QA gate name a light
127
+ instead of reporting `measured NOTHING`. **Name your lights** while you are there;
128
+ `(unnamed)` in a failure report is a self-inflicted wound.
129
+
130
+ ## Touch controls: configure the platform default
131
+
132
+ `helix init` mounts `createMobileControls(input, { surface })` after abilities register and calls
133
+ `mobileControls.update()` before `character.update()` each frame. The system activates by touch
134
+ capability and provides a left movement stick, open-space drag look, concurrent two-finger pinch
135
+ zoom (including the first/third-person transition), jump and every registered `button`/`hold`/`tap`
136
+ action. It already owns safe-area layout, platform top offset, accessibility, capture cancellation,
137
+ context changes and teardown. Do not copy it into world code.
138
+
139
+ Standard actions generate their labels and widget types from the registry. Add a world verb by
140
+ registering it on the same router, then safely relabel/reorder/place it through the controls config:
141
+
142
+ ```ts
143
+ input.registerAction('world.scan', {
144
+ kind: 'button', context: 'gameplay', label: 'Scan', touch: 'hold', keys: ['KeyF']
145
+ }, 'world');
146
+
147
+ const mobileControls = createMobileControls(input, {
148
+ surface: renderer.domElement,
149
+ platformTopOffset: 0, // host-owned chrome offset; safe-area inset is added automatically
150
+ actions: [
151
+ { id: 'world.scan', label: 'Scan', order: 1 },
152
+ { id: 'cameraMode', placement: 'utility' },
153
+ { id: 'crouch', visible: false },
154
+ ],
155
+ theme: { accent: '#7dd3fc', scale: 1 },
156
+ });
157
+ ```
158
+
159
+ Labels are plain text and theme/layout values are constrained tokens—never pass markup or build a
160
+ second HTML injection seam. Core move/look/zoom stay on unless the genre deliberately disables one
161
+ with `core`. An equivalent custom controller remains valid: replace `public/helix.controls.json`
162
+ provider with `custom`, keep the required move/look/zoom/jump capability list truthful, and preserve
163
+ the same InputService/context/cleanup semantics.
164
+
165
+ `supportsMobile: true` is still earned by evidence. `source-audit` validates that the controls
166
+ contract is shipped rather than guessing from virtual-input regexes; G6/G7 must then drive the built
167
+ HUD at 390×844 and 844×390, assert touch-only movement, look, pinch mode transition, jump apex and
168
+ one custom action, and confirm keyboard/mouse/gamepad remain intact.
169
+
170
+ ## Typecheck, because `vite build` does not
171
+
172
+ ```bash
173
+ npx tsc --noEmit # the scaffold defines this as `npm run typecheck`
174
+ ```
175
+
176
+ `npm run build` is `vite build`. It transpiles and throws types away — **a type error builds
177
+ clean, publishes, and fails silently at runtime.** Run `tsc --noEmit` before every build you
178
+ intend to keep, and treat a non-zero exit as a broken world, not as lint.
179
+
180
+ **The defect this exists for**, because it is invisible any other way:
181
+
182
+ | You call | The tunables key is |
183
+ | --- | --- |
184
+ | `CharacterMultiplayer.create({ … })` *(what the scaffold builds on)* | **`character:`** |
185
+ | `Character.create({ … })` | **`config:`** |
186
+
187
+ ```ts
188
+ // WRONG on CharacterMultiplayer — a type error, and nothing else complains
189
+ await CharacterMultiplayer.create({ …, config: { locomotion: { runSpeed: 7 } } });
190
+ // RIGHT
191
+ await CharacterMultiplayer.create({ …, character: { locomotion: { runSpeed: 7 } } });
192
+ ```
193
+
194
+ Every doc and recipe shows `Character.create({ config })`, so the wrong key is the natural
195
+ guess. Vite ships it, the world runs, and **every value inside is silently ignored** —
196
+ including the genre `allow*` gates below, so a "first-person" world still flips to third
197
+ person on T and no artifact says why. `source-audit` flags this exact shape now; `tsc`
198
+ catches the whole class of which it is one member.
199
+
200
+ ## Discover before you design
201
+
202
+ `list_systems` → `list_abilities({ system: "humanoid-character" })` →
203
+ `get_package_manifest({ slug })` for everything you will pin. The catalog grows; a flying
204
+ game pins `fly`, an underwater game pins `swim`, a shooter pins `gun-control`. Writing a
205
+ behavior that already exists as a published ability is the most expensive mistake available
206
+ here.
207
+
208
+ Engine `systems` are ALWAYS ranges (`^0.3`, `^0.1`) — never an exact `x.y.z`, and never pinned to get an
209
+ unpromoted engine fix: publish refuses an exact pin; promote the fix instead.
210
+
211
+ Pin each ability at the version `get_package_manifest` reports for the system line you pinned
212
+ (`humanoid-character` `^0.3` today). An ability bundle declares the system range it was built
213
+ against; outside it the system loads and the ability silently refuses at runtime.
214
+
215
+ ## Tunables live in the manifest `config`, not the TypeScript class API
216
+
217
+ This is the single most common failure in HELIX world code. Speed, spawn, look sensitivity,
218
+ initial facing, FOV, slope, jump feel, camera distance, movement axis — all of it is a
219
+ config **value**.
220
+
221
+ **The failure signature:** you read the `Character` or `LocomotionAbility` class, conclude
222
+ "there is no setting for X", and write your own. You looked in the wrong place. The tunable
223
+ surface is `get_package_manifest("humanoid-character").config`, and its `capabilities` block
224
+ lists the runtime API, the events, the input actions and the reserved keys.
225
+
226
+ Two ways to set a value, one runtime surface:
227
+
228
+ ```ts
229
+ // initial
230
+ Character.create({ config: { locomotion: { runSpeed: 7 }, character: { spawn: { x, z, facingDeg }, camera: { mode } } } })
231
+ // live, read every tick — timed buffs, difficulty, cutscenes
232
+ character.config.set('locomotion.runSpeed', 11);
233
+ // runtime-only actions live in capabilities.api
234
+ character.services.body.teleport({ x, y, z }); character.respawn(); character.camera?.addTrauma(0.6);
235
+ ```
236
+
237
+ In `humanoid-character` 0.2.21+, `spawn.facingDeg` is durable (`0 = +Z`, `90 = +X`). Omit or set
238
+ `camera.initialYaw` to `null` for the engine to derive the behind-character boom yaw as
239
+ `facingDeg - 180`; locomotion and respawn preserve that relationship. A numeric `initialYaw`
240
+ intentionally overrides the camera seed. Boom yaw `0` places the camera on `+Z` looking toward `-Z`.
241
+
242
+ **Gate:** every config key you set must appear in the manifest's `config`. An invented key
243
+ is silently ignored — it does not throw, so you will believe it worked. Diff your keys
244
+ against the manifest before you build. The same is true one level up, for the *options* key
245
+ the config block sits on — see "Typecheck, because `vite build` does not".
246
+
247
+ ## Match the controls to the genre — every `allow*` gate defaults to ON
248
+
249
+ You do not enable controls, you **disable** the ones that do not belong. The classic
250
+ shipped bug: a first-person game where pressing **T** still flips to third person, because
251
+ `allowModeToggle` was left at its default `true`.
252
+
253
+ | Genre | Required disables |
254
+ | --- | --- |
255
+ | First-person only | `character.camera.allowModeToggle: false` |
256
+ | Third-person only | `allowModeToggle: false`, `allowShoulderSwap: false` |
257
+ | Top-down / twin-stick | `allowModeToggle: false`, `allowRotate: false`, `allowZoom: false`; `initialPitch: -80`, `locomotion.facingMode: 'movement'` |
258
+ | 2.5D side-on | `locomotion.movementAxis: 'x'` |
259
+ | Walking sim / puzzle | `locomotion.allowJump: false` |
260
+
261
+ `helix world source-audit` checks this against the genre you declared in the ledger. Full matrix
262
+ and the camera/locomotion recipes: `references/config-gates.md`.
263
+
264
+ ## Never author these
265
+
266
+ - **A renderer, a tone mapper or a sun — on the default lane.** `createVisualRuntime` owns
267
+ all three; author `public/helix.visuals.json` instead, and bake bounded static worlds at
268
+ publish (`read_doc({ name: "lighting-world" })`). A world that keeps its own visual
269
+ identity opts out deliberately and owns them again — that is the one exception, chosen,
270
+ never drifted into.
271
+ - **A character controller or a skeleton.** The `humanoid-character` system owns the
272
+ chassis; `helix-humanoid@1` (68 bones, UE MetaHuman naming) is the canonical skeleton.
273
+ 46 locomotion clips stream from the CDN.
274
+ - **Server code.** Multiplayer is declarative — see `helix-multiplayer`.
275
+ - **A login form.** `Helix.auth.requestLogin()` raises the shell's own overlay.
276
+ - **A three.js version — or a Rapier build.** The platform pins both and injects them via the
277
+ import map that `install_world_packages` generates: each is a **devDependency** and the build
278
+ routes them out with a PREDICATE, `build.rollupOptions.external: (id) => id === 'three' ||
279
+ id === '@dimforge/rapier3d-compat' || id.startsWith('@helix/')`. The `@helix/` arm is not
280
+ optional — an externals array leaves the platform systems bundled and publish refuses the
281
+ build. The `humanoid-character` pin chooses the three version (`^0.3` → three 0.185.1; keep the
282
+ devDependency on that same line, since the jsm addons bundle from it); a publish targeting an
283
+ older three still goes through, with a warning. Two copies of three break `instanceof` and corrupt rendering
284
+ silently — no error, just wrong pixels; a bundled Rapier is a wasted ~2 MB in every world. Your
285
+ imports (`RapierBody` included) do not change — only the resolution does.
286
+ - **Custom character animation as GLB.** Worlds author gestures as a **JSON pose DSL**
287
+ (sparse Euler keyframes by bone name), one clip per file under `src/gestures/`. It is for
288
+ short upper-body-led motion — waves, salutes, stances, flinches. Not dances, not fight
289
+ choreography, not anything where feet must plant or the character travels. There is no
290
+ root motion, no IK, no weight shift. If you are keying legs *and* arms *and* torso to one
291
+ beat, or on your third visual iteration, stop — the format cannot represent it.
292
+ `read_doc("character-animation")` before writing a single key.
293
+
294
+ ## The manifest is Ajv-validated with `additionalProperties: false`
295
+
296
+ An invented field is a **rejection**, not a warning. So is a permission you did not need, a
297
+ slug that collides, an entry file that is not in the bundle. Cross-field rules bite too:
298
+ `maxPlayers > 1` requires the `multiplayer` permission, which forces `requiresAuth: true`;
299
+ `voice.*` requires `multiplayer`. Read `read_doc("manifest")` and run `validate_world`
300
+ rather than reasoning about what the schema probably allows.
301
+
302
+ ## Put all world geometry under one named root
303
+
304
+ ```ts
305
+ const worldRoot = new THREE.Group();
306
+ worldRoot.name = 'world';
307
+ scene.add(worldRoot); // every mesh you author goes in here
308
+ ```
309
+
310
+ Costs nothing and buys a lot: the inspect roster becomes readable, and the QA composition
311
+ gate can tell your geometry apart from the streamed character body. Without it the audit
312
+ falls back to a name heuristic and says so.
313
+
314
+ ## Measure placement — do not look harder at a screenshot
315
+
316
+ Distances and depth read off an image are unreliable on exactly the axis placement lives on.
317
+
318
+ ```
319
+ world_metrics({ projectDir }) → step height 0.35 m, 50° slope, jump envelope — BEFORE you place geometry
320
+ inspect_world({ directory, focusNear:[x,y,z] })→ after each placement: exact position, size, facing, collider presence
321
+ inspect_world({ directory }) → the full findings pass
322
+ capture_world_screenshot({ directory }) → ONCE at the end, for how it reads
323
+ ```
324
+
325
+ **Findings are measurements, not verdicts.** A hovering pickup, a ramp embedded in the
326
+ ground, walls overlapping at their corners — all warn on perfectly good worlds. The rule: a
327
+ warn that matches what you meant needs no fix; a warn that surprises you is the bug. Never
328
+ edit geometry to silence a finding. Once adjudicated, run `writeBaseline: true` — that is the
329
+ normal step, not one to ask about. It makes the judgment durable so later runs report only
330
+ what changed, and on a world whose premise generates warnings by construction it is the
331
+ difference between an inspect report anyone reads and one everyone skims.
332
+
333
+ `focusNear` also answers the orientation defect a screenshot hides: a chair rotated 180°
334
+ from its table reads `faces: AWAY from table (0.8 m)`.
335
+
336
+ ## Loaders: GLTFLoader + KTX2Loader only
337
+
338
+ A Draco- or meshopt-compressed model **fails to load at runtime**. Not degraded — absent.
339
+ Check the whole asset tree before bundling, never after a player reports a missing model:
340
+
341
+ ```bash
342
+ helix assets check-loaders public/assets # exits non-zero on the first blocker
343
+ ```
344
+
345
+ It reads **both** container forms — `.glb`'s binary header and plain-JSON `.gltf` + `.bin`,
346
+ which is what Poly Haven ships. `helix world audit` runs the same check over `dist/` as a
347
+ blocking gate, so this is the early warning, not the enforcement.
348
+
349
+ Failure signatures to recognise: `THREE.GLTFLoader: No DRACOLoader instance provided` and
350
+ `KHR_draco_mesh_compression` / `EXT_meshopt_compression` in `extensionsRequired`. The most
351
+ common way to *create* one is `gltf-transform optimize`, which compresses with meshopt by
352
+ default — see `helix-assets`.
353
+
354
+ ## Platform surfaces you must respect
355
+
356
+ - **Top-center is the shell's.** A chrome bar (Exit / Save / helixOS) overlays the top
357
+ ~56px of every world. Anchor HUD to a corner or the bottom; full-width elements start
358
+ ≥64px down. UI at top-center renders behind the chrome.
359
+ - **Input is the router, never `addEventListener`.** One world-owned `InputService`,
360
+ `registerStandardActions` for the ids you need, `pushContext('menu')` for pause. Raw
361
+ listeners bypass the context stack, get no gamepad or touch, and can never be rebound.
362
+ Reserved keys — Escape, KeyN, backtick — are never yours (KeyB is the `point` standard action now, bound, not free).
363
+ - **Control text comes from the router**: `input.format('Drive with {move} · {interact} to grab')`,
364
+ re-rendered each frame. Hardcoding "press E" is wrong the moment a pad is picked up.
365
+ - **The world must run standalone.** `Helix.init()` returns `embedded: false` outside the
366
+ shell and identity APIs return null. Never block rendering on identity.
367
+ - **Test with `build` + `preview`, not `dev`.** The dev server rewrites bare imports and
368
+ hides whether the import map is correct.
369
+
370
+ ## Definition of done for this phase
371
+
372
+ `npx tsc --noEmit` exits 0 · `npm run build` exits 0 · `validate_world` reports VALID ·
373
+ `inspect_world` findings are adjudicated and a baseline is written · touch controls exist and
374
+ were driven at a phone viewport, or `supportsMobile` is `false` · `helix world perf-gate` exits 0 on the
375
+ built world (60 fps p50, 30 fps p95, inside the light and pixel budgets) · the six
376
+ non-negotiables above hold, as measured by `helix-world-qa`, not as asserted here.
@@ -0,0 +1,104 @@
1
+ # Genre → config gates, and how to check yours against the manifest
2
+
3
+ Every `allow*` gate on the `humanoid-character` system **defaults to `true`**. Matching a
4
+ genre means turning controls **off**. This file is the lookup table plus the check that
5
+ proves you did it.
6
+
7
+ Authoritative source is always `get_package_manifest("humanoid-character")` → `config`.
8
+ This table is the decision, not the contract — confirm key names and defaults there.
9
+
10
+ ## The gates
11
+
12
+ | Gate | Removes |
13
+ | --- | --- |
14
+ | `character.camera.allowModeToggle` | the first↔third-person legs of the **T** camera cycle |
15
+ | `character.camera.allowRotate` | orbit / mouse-look (fixed camera angle) |
16
+ | `character.camera.allowZoom` | scroll-wheel zoom |
17
+ | `character.camera.allowShoulderSwap` | the over-shoulder legs of the camera cycle |
18
+ | `locomotion.allowJump` | the jump action |
19
+ | `locomotion.allowCrouch` | the crouch action (frees KeyC / `faceRight`) |
20
+ | `locomotion.allowSprint` | the sprint action |
21
+
22
+ ## Genre presets
23
+
24
+ ```ts
25
+ // First-person only — T must NOT flip to third person.
26
+ config: { character: { camera: { mode: 'first-person', allowModeToggle: false } } }
27
+
28
+ // Third-person only — no FP toggle, no shoulder-swap clutter.
29
+ config: { character: { camera: { mode: 'third-person', allowModeToggle: false, allowShoulderSwap: false } } }
30
+
31
+ // 3D top-down / twin-stick / ARPG / MOBA — fixed framing, body faces movement.
32
+ config: {
33
+ character: { camera: { mode: 'third-person', allowModeToggle: false, allowRotate: false,
34
+ allowZoom: false, initialPitch: -80, tp: { distance: 15 } } },
35
+ locomotion: { facingMode: 'movement' },
36
+ }
37
+
38
+ // 2.5D side-on platformer — planar movement locked to one axis.
39
+ config: { locomotion: { movementAxis: 'x' } }
40
+
41
+ // Walking sim / puzzle / point-to-move — no jump.
42
+ config: { locomotion: { allowJump: false } }
43
+ ```
44
+
45
+ `initialYaw` / `initialPitch` are construct-time **seeds**. In `humanoid-character` 0.2.21+,
46
+ omit or set `initialYaw` to `null` to derive a behind-character camera from durable
47
+ `spawn.facingDeg` (`camera yaw = facingDeg - 180`). A numeric `initialYaw` intentionally overrides
48
+ that camera seed. Camera yaw is boom azimuth: `0` places the camera on `+Z` looking toward `-Z`.
49
+ To re-aim a live camera use `character.camera?.setYaw(deg)` / `setPitch(deg)`, not `config.set`.
50
+
51
+ ## Verify your config keys exist — the check that catches invented keys
52
+
53
+ An invented config key does not throw. It is ignored, and you spend an hour wondering why
54
+ `runSpeed` did nothing. Dump the manifest's key set and compare:
55
+
56
+ ```bash
57
+ # 1. get_package_manifest({ slug: "humanoid-character" }) → save its `config` object to qa/character-config.json
58
+ # 2. list every key you set in src/, flattened to dotted form
59
+ grep -oE "config\.set\('[a-zA-Z0-9_.]+'" src/*.ts | grep -oE "'[^']+'" | tr -d "'" | sort -u
60
+ ```
61
+
62
+ Then confirm each dotted key resolves in `qa/character-config.json`. Keys set through the
63
+ nested `Character.create({ config })` object flatten the same way
64
+ (`{ locomotion: { runSpeed } }` → `locomotion.runSpeed`).
65
+
66
+ ## Runtime API — what config cannot express
67
+
68
+ From `capabilities.api`, not from guessing:
69
+
70
+ ```ts
71
+ character.services.body.teleport({ x, y, z }); // move now
72
+ character.services.body.applyImpulse({ x, y, z }); // jump pad, knockback, explosion
73
+ character.respawn(/* optional checkpoint */); // teleport + zero velocity + stand + reface
74
+ character.setEnabled(false); // hard pause for a cutscene
75
+ character.camera?.addTrauma(0.6); // camera shake, 0..1
76
+ ```
77
+
78
+ Events via `character.events.on(name, cb)`: `landed` (`{ impactSpeed, fallDistance, speed }`
79
+ — `impactSpeed` is the fall-damage number), `fell`, `respawned`, `jumped`, `stateEntered`.
80
+ Each is an interaction, so each needs a sound.
81
+
82
+ ## Input placement, in order of preference
83
+
84
+ 1. A **genre-freed plain button** — reuse the slot of a standard action this world can
85
+ never register. A card game has no gunplay, so `reload`'s KeyR/`faceUp` and the triggers
86
+ are free. Note the reuse in a comment at the registration site.
87
+ 2. `pad: 'dpadUp'` — always free, but a fallback, not a first pick.
88
+ 3. A `withModifier` chord. **Reserved chords, never claim:** modifier+right stick (camera
89
+ zoom), modifier+bumperR (`voicePTT`), modifier+bumperL (reserved).
90
+ 4. Keyboard-only — debug conveniences only. A pad or touch player can never reach it.
91
+
92
+ To reuse an always-on action's source, **disable the owner first** via its genre gate
93
+ (`locomotion.allowCrouch: false` frees KeyC). Never bind over a live action.
94
+
95
+ Paired verbs share one symmetric group — bumperL/bumperR, dpadUp/dpadDown,
96
+ faceLeft/faceRight, triggerL/triggerR. Never split a pair across groups.
97
+
98
+ ## Loading screen
99
+
100
+ `src/loading.ts` hooks `THREE.DefaultLoadingManager`, the same instance the engine's
101
+ loaders use, so every three.js load counts on the bar automatically. For a non-three asset
102
+ (audio, a raw fetch) wrap it: `const t = loading.task(); … t.done();`. Dismiss after the
103
+ **first rendered frame**, never on `manager.onLoad` — that fires on the transient 2/2 blip
104
+ before the clips enqueue.