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