@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,156 @@
1
+ # HELIX Instant — World Inspection (layout as data)
2
+
3
+ Screenshots answer "does this read as a plaza"; they measurably cannot answer "is the bench 0.4 m in the
4
+ air" — distances and depth read off an image are unreliable on exactly the axis placement lives on.
5
+ `inspect_world` gives you the world's geometry as data instead: one headless boot returns a serialized
6
+ snapshot of the scene — world positions, extents, oriented boxes, visibility, physics colliders — and every
7
+ placement question is answered from that snapshot with numbers.
8
+
9
+ **The loop: measure with `inspect_world` until the numbers are right, then `capture_world_screenshot` once
10
+ before handing to a human.** Do not answer placement questions by looking harder at a render.
11
+
12
+ ## 1. The one-line integration
13
+
14
+ One import and one call, a cheap no-op during normal play — it only activates when the page is loaded with
15
+ `?helixInspect=1`, which `inspect_world` (and the `helix world inspect` CLI command) do for you:
16
+
17
+ ```ts
18
+ import { world } from '@helix/engine-core/inspect';
19
+ // ...at the END of your world's async boot, after the scene is fully built:
20
+ world.ready({ scene, body, spawn: SPAWN });
21
+ ```
22
+
23
+ `scaffold_world` already wires this in `src/main.ts`. `scene` is required; `body` (your `RapierBody`) enables
24
+ the collider report — without it the snapshot still covers the render side and says so honestly
25
+ (`sources.colliders: "absent"`); `spawn` is optional context. Timing is forgiving: the snapshot walks the
26
+ live scene when taken, so registering early still captures everything built afterwards.
27
+
28
+ Needs `engine-core >= 0.1.2` (and `humanoid-character >= 0.2.13` for the collider report).
29
+
30
+ ## 2. What a report contains
31
+
32
+ - **anomalies** (the default section) — placement findings, each with the measured numbers, the method that
33
+ produced it, and a confidence: floating / sunk geometry (vs the character's 0.35 m step height),
34
+ intersecting geometry (true oriented-box overlap, not loose bounding boxes), collider-vs-visual divergence
35
+ — including the classic half-extent/full-size slip, where a collider is exactly 2× its visual because full
36
+ sizes were passed where half-extents belong — orphan colliders (invisible walls) and un-collided visuals,
37
+ mirrored or degenerate transforms, and stray far-away coordinates (a `1e6` typo).
38
+ - **roster** — the grouped object inventory: 60 identical trees collapse to one row with a count, size,
39
+ triangle count, and spread. Ask for it when you need to know what is actually in the scene and where.
40
+ - **colliders** — the physics roster: every collider with shape, extents, and position. The only way to SEE
41
+ collision geometry — colliders never render, so a screenshot cannot debug an invisible wall.
42
+ - **zones** — the declared `multiplayer.json` trigger volumes, each with its extents, the geometry it touches,
43
+ and its **rest surfaces** (surfaces a standing player could occupy while inside it). Zones are doubly
44
+ invisible — they never render AND they are usually off-camera — so this is the only check that catches a
45
+ kill band the pit floor misses, a checkpoint volume floating in empty space, or a zone centered on a stray
46
+ coordinate. Zone findings (`zone-empty`, `zone-fly-through`, `zone-out-of-bounds`, spawn-inside-zone) join
47
+ the anomalies section automatically whenever the bundle carries a `multiplayer.json`. The same
48
+ intent rule applies: a tall abyss band under a floorless arena is a design pattern, not a bug — the report
49
+ says which reading applies.
50
+
51
+ Request sections via `report: ["anomalies", "roster", "colliders"]`; `json: true` returns the raw
52
+ `{ snapshot, analysis }` for programmatic follow-up on exact numbers.
53
+
54
+ ## 3. Findings are measurements, not verdicts
55
+
56
+ Every finding is a true statement about geometry — whether it is a PROBLEM depends on what you meant.
57
+ Perfectly good worlds keep warnings on purpose:
58
+
59
+ - a collectible gem hovering 1.2 m above the ground → an intended float, not a floating bug;
60
+ - a tilted ramp whose base dips below the ground plane → an intended embed, not sunk;
61
+ - arena walls overlapping at their ends → corner joints, standard construction (uniform-depth overlaps are
62
+ collapsed into one summary line for exactly this reason).
63
+
64
+ **The rule: a warn that matches what you meant needs no fix; a warn that surprises you is the bug.** Never
65
+ edit geometry just to silence a finding — that trades real design for a quiet report. What the checks are
66
+ FOR is the surprise: the bench you believed was grounded but floats, the platform whose collider is twice
67
+ its size, the wall segment left at the origin.
68
+
69
+ Two mechanisms keep a dense hand-built world readable instead of drowning the one real defect:
70
+
71
+ - **Run collapse (automatic).** Three or more same-parent siblings with the same finding against the same
72
+ support and uniform gaps fold into one line — `33 children of Group#12 floating 1.75–1.96 m above
73
+ Mesh#38 … adjudicate as one`. A member that breaks the run's gap band keeps its own line, because in a
74
+ hanging run the bulb 2 m below its siblings IS the finding. Composite-model internals collapse per model
75
+ the same way.
76
+ - **The baseline (your adjudication, made durable).** After checking the findings against intent, pass
77
+ `writeBaseline: true` once — every finding is accepted into `inspect.baseline.json` beside the project.
78
+ From then on inspect reports only what CHANGED: new findings, plus accepted entries that no longer occur
79
+ (prune those with a fresh `writeBaseline`). Entries key on rule + world-space anchor — never on paths —
80
+ so they survive rebuilds. Suggest the write to the user before doing it: accepting findings is a
81
+ judgement, not a reflex. `showAccepted: true` renders the hidden ones when you need the full picture.
82
+
83
+ Object paths in reports are **canonicalized** — auto-named `Type#N` segments are renumbered in content
84
+ order (by each subtree's centroid), not scene-traversal order, so the same object keeps the same path
85
+ across runs even when async loads shift traversal. Coordinates remain the strongest cross-run handle.
86
+
87
+ ## 4. What this cannot tell you
88
+
89
+ Whether the scene READS as intended: materials, lighting mood, color, animation, art direction, atmosphere.
90
+ That is the closing `capture_world_screenshot` check. The snapshot also reads bounding boxes, not exact
91
+ surfaces — non-box shapes are over-approximated, and each finding's stated method and confidence say so.
92
+
93
+ ## 5. Checking ONE object — the focus lens
94
+
95
+ After placing or moving a single object, you do not need the whole report — focus the lens on the spot you
96
+ just wrote into the code:
97
+
98
+ ```
99
+ inspect_world({ directory, focusNear: [10, 0.5, 3] })
100
+ ```
101
+
102
+ You get back exactly what is at that coordinate: each matching object's geometry/color, exact position, size,
103
+ world AABB, its FACING — yaw plus the world direction its +Z forward points, and horizontal
104
+ `TOWARD / AWAY from / side-on to` relations against the nearest objects (floors are skipped — you stand on
105
+ them, you do not face them). A chair rotated 180° from its table reads `faces: AWAY from table (0.8 m)` —
106
+ orientation defects are invisible in a position/AABB listing and easy to miss in a screenshot. Also reported:
107
+ whether any collider touches it ("walk-through unless intended" when none does), and only the
108
+ findings that involve it — with the world's total finding count stated so nothing is silently hidden. If
109
+ nothing matches, the three nearest objects are listed with distances, which answers both failure modes at once
110
+ (the object landed somewhere else vs it never made it into the scene). Analysis always runs whole-world —
111
+ correctness is relational — the lens only scopes the report.
112
+
113
+ No naming is required: you know the coordinates because you wrote them. Unnamed meshes report as `Mesh#N`;
114
+ imported GLB assets keep their authoring-tool node names for free. Optionally set `mesh.name = 'bench'` on a
115
+ few landmark objects if you want findings and `focus: "bench"` to read by name — but do not name everything;
116
+ it is authoring noise the tools do not need.
117
+
118
+ ## 6. Sizing geometry BEFORE you place it — `world_metrics`
119
+
120
+ `world_metrics({ projectDir })` returns the traversal/movement reference from the world's own installed
121
+ character system: capsule height/radius, the 0.35 m step height (taller blocks; shorter auto-steps), the 50°
122
+ walkable slope limit, movement speeds, and the derived jump envelope — air time and the maximum flat-ground
123
+ gap at run and sprint speed — plus corridor and ceiling clearances. Read it instead of guessing whether a gap
124
+ is jumpable or a doorway fits; values are the system defaults a world starts from (worlds may override via
125
+ config). The CLI pair is `helix world metrics -C <projectDir>`.
126
+
127
+ ## 7. Comparing before and after an edit
128
+
129
+ `inspect_world({ json: true })` returns the raw snapshot; the CLI pair `helix world inspect <dist> --out
130
+ a.json` … `helix world diff a.json b.json` reports what moved (per-axis deltas), what appeared/disappeared,
131
+ and which findings appeared or cleared — the regression check for a layout edit.
132
+
133
+ ## 8. Timing and determinism
134
+
135
+ The snapshot is taken after a settle delay (default 750 ms past world-ready). A world whose props animate
136
+ into place may need more: pass `settleMs`. Objects that animate forever (a spinning pickup) snapshot at
137
+ whatever pose they hold that frame — expect their numbers to differ between runs.
138
+
139
+ ## 9. Adding the helper to an existing world
140
+
141
+ Almost every world scaffolded before the inspect helper existed is missing the `world.ready` call — and a
142
+ fix-up session on such a world cannot measure anything until it lands. `check_for_updates({ projectDir })`
143
+ reports the wiring state up front ("engine-core is pinned but src never calls world.ready"), which is cheaper
144
+ than discovering it through an inspect timeout. The upgrade is small and additive:
145
+
146
+ 1. `"engine-core": "^0.1.2"` in `public/helix.json` → `systems` (alongside `humanoid-character`), then
147
+ `install_world_packages` (or the CLI's `helix install`).
148
+ 2. If `vite.config.ts` predates the alias, add this line to `resolve.alias` BEFORE the root
149
+ `@helix/engine-core` entry — subpath aliases must precede the root alias, or rollup's prefix
150
+ substitution mangles the import into an ENOENT:
151
+
152
+ ```ts
153
+ '@helix/engine-core/inspect': fileURLToPath(new URL('./public/helix_modules/engine-core/inspect/index.js', import.meta.url)),
154
+ ```
155
+
156
+ 3. The two lines from §1 in `src/main.ts`, then rebuild (`npm run build`) and `inspect_world` as usual.
@@ -0,0 +1,241 @@
1
+ # The look of a world — from raw assets to good
2
+
3
+ The QA gates can tell you a world fails to look right (`materials.default-plastic`, `composition.primitive-dominant`,
4
+ `lighting.unmotivated`). This page is how to make it look right: the process the platform maintainers converged on
5
+ while calibrating real production scenes (Amazon's Bistro, Crytek and Intel Sponza) against the visual runtime — and
6
+ the mistakes they made first, so you do not repeat them. Every number below was measured on those scenes.
7
+
8
+ Territory: lighting itself — the runtime, `helix.visuals.json`, baked vs dynamic, quality tiers — is
9
+ `read_doc({ name: "lighting-world" })`. Where assets COME FROM (the sourcing ladder, budgets, loaders) is the
10
+ `helix-assets` skill; the six build non-negotiables are `helix-world-build`; what blocks a ship is `helix-world-qa`.
11
+ This page owns what sits between: **asset truth, material consumption, texture hygiene, scene assembly, composition,
12
+ and judging your own captures.** The platform doctrine is *measure first, look last* — this page is the LOOK half.
13
+
14
+ ## Fix defects before taste
15
+
16
+ Raw assets are usually **broken**, not merely unstyled. Bistro — a professionally authored, industry-standard
17
+ showcase scene — shipped to the calibration bench with the wrong scale, all-metal materials, dead normal maps and
18
+ mip-less textures. Styling on top of defects teaches you wrong numbers everywhere downstream: you crank a light to
19
+ fight metalness, then crank the grade to fight the light. The maintainers' rule, verbatim: **a knob that does nothing
20
+ or behaves nonlinearly is a defect signal, not a tuning challenge.** Find the defect; do not compensate.
21
+
22
+ So the work has a fixed order, and taste comes last:
23
+
24
+ ```
25
+ 1. ASSET TRUTH — the import checklist below, per asset, before anything is styled
26
+ 2. GEOMETRY + SUN — measure the space, then aim the light at it
27
+ 3. MATERIALS — consumed correctly (per-axis repeat, multiplier semantics, whole ORM)
28
+ 4. GRADE, LAST — and inside the grade: power (contrast) first, saturation second, slope last
29
+ 5. VERIFY — captures from more than one camera; on a cycling world, more than one hour
30
+ ```
31
+
32
+ Iterate with one knob at a time, and re-look after each change. And the meta-rule that has paid for itself more than
33
+ any other: **measure whether a knob reaches the frame — do not trust a comment, including your own.** Take a capture,
34
+ change the value, take another; if nothing moved, you found a defect (or the knob is not yours — see the
35
+ lighting-world DO/DON'T table).
36
+
37
+ ## Asset truth — the import checklist
38
+
39
+ Run this on every imported GLB or third-party asset before styling. Each row is a defect that shipped in a
40
+ professional scene and survived until someone measured.
41
+
42
+ 1. **Scale, proven — never assumed.** The platform is metric and the avatar is a real ~1.8 m human; shadow bias, fog
43
+ density and the probe volume are all metre-derived, so a mis-scaled world silently degrades all of them. Bistro's
44
+ exterior shipped **1.6× oversized** — proven by a shared liquor-bottle mesh measuring 0.580 m in one sub-scene and
45
+ 0.364 m in another. Prove scale with a known-size object: a bottle, a table (~0.75 m), a doorway (~2.1 m).
46
+ 2. **`metallicFactor` present.** glTF's default for a MISSING `metallicFactor` is **1.0** — full metal, no diffuse
47
+ term at all. All 132 of Bistro's materials omitted it: cobbles, plaster, foliage and fabric arrived as mirrors
48
+ with their base colour re-read as specular tint, and — because diffuse GI lights only diffuse surfaces — the
49
+ probe field lit **literally nothing**: the frame rendered byte-identical with the GI removed. If an imported
50
+ world looks dark, oddly tinted, and GI seems to do nothing, check metalness FIRST. Set `metalness: 0` on
51
+ everything that is not actually metal.
52
+ 3. **Roughness varies.** One global roughness constant across a whole asset (Bistro: 0.5527864 on all 132 materials)
53
+ is a conversion artifact, not authoring. Expect a roughness map or per-material values.
54
+ 4. **Normal maps carry a real blue channel.** Bistro's normal maps were two-channel with blue zeroed — the decoded
55
+ normal pointed INTO the surface, so up-facing geometry refused sun. Symptom: surface detail lit from the wrong
56
+ direction, or normal maps that seem to darken. The cheap convention test (DirectX vs OpenGL green): flip
57
+ `material.normalScale.y` and see whether the detail snaps right.
58
+ 5. **Colorspaces are per texture.** Albedo/emissive = sRGB; normal, ORM, roughness, anything data = linear. A linear
59
+ albedo reads washed out; an sRGB normal map reads lumpy. Platform materials arrive correctly tagged; hand-wired
60
+ textures are where this goes wrong.
61
+ 6. **Alpha mode is a per-material decision.** Near-binary foliage alpha → `alphaTest` (MASK); liquids and glass stay
62
+ BLEND — Bistro's wine and beer forced to cutout render as garbage. And do not assume foliage means alpha at all:
63
+ Intel's ivy is opaque modelled geometry with no alpha channel. Blended foliage on thousands of triangles will
64
+ sort-artifact; that is what MASK is for.
65
+ 7. **No stray lights, no double-counting.** Imported GLBs carry lights you did not ask for (Intel's Sponza ships 24)
66
+ — strip them on load. Never hide one with `visible = false`: a glTF light node may PARENT its fixture's meshes,
67
+ and an invisible node's children stop rendering with it. Remove the light from the scene or layer-mask it. And
68
+ never represent one light twice — an emissive mesh is not a light source (it glows, it does not illuminate), and
69
+ a hand-placed "sun" on a runtime-lit world double-lights the whole frame.
70
+
71
+ ## Geometry first, then the sun
72
+
73
+ Aim the light at the space you actually built — measured, not eyeballed. A courtyard, atrium, alley or roofed street
74
+ has an **elevation floor**: a sun below it never reaches the ground, and the transition is a cliff, not a slider.
75
+ Sponza's courtyard measures 16 × 5.6 m at 14.4 m deep — a well; direct sun reaches its pavement only above ~61°
76
+ elevation, and the first auto-derived sun left **0.14 % of the frame lit**, with the whole scene running on blue sky
77
+ ambient. That is the "flat, dim, bluish" signature: not a grade problem, an aim problem.
78
+
79
+ On a runtime-lit world you aim with two fields in `helix.visuals.json`: `noonAzimuthDeg` (the one knob that aligns
80
+ the sun path to YOUR geometry — set it first) and `hours`. Sanity-check against the shadows you want: pick the
81
+ façade the sun should rake, verify the interior you care about actually receives light at your chosen hour, and
82
+ re-check at a second hour if the world cycles. If an interior must read sunlit, the opening it receives sun through
83
+ is part of your geometry design, not something the grade can fake later. And if the aim is right but a bounded
84
+ interior still reads washed and flat, the defect is the sun-to-ambient RATIO, not the aim and not the grade — the
85
+ fix is the transport knobs, in lighting-world's "Calibrating a high-contrast interior" recipe. The same recipe
86
+ covers the opposite complaint: an interior that reads too DARK once its bake is honest is short of BOUNCES, not of
87
+ exposure — `probes.bounces` 2–3 and `sky.irradianceScale` are the dials, and `exposure.key` / `exposure.postGain`
88
+ develop the result afterwards.
89
+
90
+ Two scene-side facts complete the picture:
91
+
92
+ - **Scene-side dynamic range is half the job.** The tone mapper cannot manufacture contrast the render does not
93
+ contain: a scene where grass, cobble and concrete all sit near the same albedo, with no deep shade anywhere,
94
+ measured ~1 stop of usable range against Bistro's 3.5–4. Vary your albedos and build real occluded volumes —
95
+ doorways, overhangs, interiors — or the world will read flat under any grade.
96
+ - **Sealed interiors.** A single-sided or paper-thin wall lets sky light leak into shaded space (the bake samples
97
+ through backfaces): the signature is a blue-white wash on surfaces that should be in shadow. Give interior shells
98
+ thickness or double-sided walls, keep large geometry off exact probe-grid planes, and expect thin cards and
99
+ single-quad walls to need deliberate shadow treatment (`material.shadowSide`, or `castShadow = false` on a shell
100
+ that would otherwise fill the shadow map with its own far face). An interior that will BAKE also has to fit the
101
+ probe volume to its room (`probes: { center, size }` in `helix.visuals.json`): placement and extent are authorable,
102
+ the probe budget is not — which is why a very large map, spreading the same probes thinner, ships `dynamic`
103
+ instead. The numbers, and the ceiling-wash defect this prevents, are in `read_doc({ name: "lighting-world" })`.
104
+
105
+ ## Platform materials — consuming them correctly
106
+
107
+ A real HELIX agent once built a world on the platform material pack and it still looked wrong. The audit found the
108
+ textures were fine — the agent had re-implemented a material library by hand, got ~80 % right, and the subtle 20 %
109
+ ruined it: tiling written as if UVs were metres, scalar overrides that silently discarded the roughness/metalness
110
+ maps, the AO channel never bound. Every rule below exists because of that world or one like it.
111
+
112
+ **The taught path** — the visual system ships a material library that gets all of it right by construction.
113
+ Setup, once per world: add `"visual": "^0.1"` to the `systems` block in `helix.json`, then run
114
+ `install_world_packages` with `update: true` (terminal: `helix install --update`) — a newly added pin is refused
115
+ until the lock is re-resolved.
116
+
117
+ ```ts
118
+ import { loadMaterialLibrary } from '@helix/visual';
119
+
120
+ // assetBaseUrl = the pack base: `assetBaseUrl` in use_material's output (`helix assets material <id>`).
121
+ const materials = await loadMaterialLibrary(assetBaseUrl, { renderer });
122
+
123
+ // A 20 x 12 m floor: apply() measures the mesh (and its world scale) and derives per-axis repeat
124
+ // from the material's physical tile size — no repeat arithmetic in world code, ever.
125
+ // Boxes, planes, spheres, cylinders and capsules are measured exactly.
126
+ const floor = new THREE.Mesh(new THREE.PlaneGeometry(20, 12));
127
+ materials.apply(floor, 'cobblestone');
128
+
129
+ // Imported geometry with unknown UVs: state the metres its 0..1 UV span covers. Without `size`, apply()
130
+ // can only estimate from the bounding box, which stretches anything whose UVs are not laid out that way.
131
+ materials.apply(importedWall, 'painted-wood', { size: [8, 3] });
132
+
133
+ // Hand the library's readiness to the visual runtime (the assetsReady option in the boot snippet)
134
+ // so no lighting is computed from half-loaded textures.
135
+ await materials.ready();
136
+ ```
137
+
138
+ Worlds not on the visual system load the same catalog through the SDK — `Helix.materials.load(assetBaseUrl)`, then
139
+ `library.resolve(id)` for absolute map URLs and authored defaults — and wire the maps themselves. If you do that,
140
+ these are the rules the hand-rolled library broke:
141
+
142
+ | DON'T | DO | Why |
143
+ |---|---|---|
144
+ | `texture.repeat.set(s, s)` with s derived from "tiles per metre" | derive repeat PER AXIS from world size: `repeat = faceMetres / tileMetres` for each axis | stock geometry UVs span 0..1 across the whole face — they are not metres. The square set() smeared ~45 % of ONE tile across a 34 m slab, and ignores face aspect, stretching every non-square face |
145
+ | pass `roughness: 0.8` and drop the map (or null it "to be safe") | keep the maps bound; scalars are three.js MULTIPLIERS over them — with real maps bound, set the scalars to 1 | nulling the map throws the authored data away; and a catalog value authored as an absolute double-attenuates when multiplied into the map |
146
+ | bind only the albedo | bind the whole ORM triple — one texture into `aoMap`, `roughnessMap` AND `metalnessMap` | the pack's ORM is R=occlusion, G=roughness, B=metalness; binding one channel wastes the other two (the r185 rule "aoMap needs uv2" is dead — every map samples through `.channel`) |
147
+ | grab Vault `kind=texture` results as world textures | the material pack is the FIRST rung (`list_materials` → `use_material`) | Vault loose textures are flat single images, several with baked-in lighting — a diffuse photo applied as `map` on a default material is exactly "supposed to be PBR but does not look PBR" |
148
+ | request 2K everywhere | 2K on hero surfaces the camera gets close to; the default elsewhere | resolution is a budget; distant and background surfaces cannot show the difference |
149
+
150
+ The pack's authored values (`roughness`, `metalness`, `normalScale`, the physical tile size, glass/water parameters)
151
+ are calibrated as a set — trust them first, and tune only after the checklist above is clean.
152
+
153
+ ## Texture hygiene
154
+
155
+ The single highest-leverage image-quality fact on the platform: **mips + anisotropy measured +5.06 dB / +0.153 SSIM
156
+ — about three times the benefit of the entire anti-aliasing ladder.** Shimmer, moiré and texture crawl are texture
157
+ defects; no AA setting fixes them. Check these before ever blaming the quality tier:
158
+
159
+ - **Mips must live inside the KTX2 container.** three cannot generate mipmaps for compressed textures — a mip-less
160
+ KTX2 shimmers at grazing angles and reads full-resolution at any distance, forever. 622 of Bistro's 637 textures
161
+ shipped mip-less. Vault uploads of mip-less KTX2 are now rejected at publish (a hard error), but **world bundles
162
+ only WARN** at `helix validate` — a shimmering file ships legally, so read your warnings.
163
+ - **Never hand-encode KTX2 into a bundle.** The platform importers pass the mip flag, cap sizes and pad correctly;
164
+ hand-encoded files are how mip-less textures happen. Feed the pipeline source images (PNG/JPG/WebP) instead.
165
+ - **Never a non-mipmap `minFilter`.** With `minFilter: LinearFilter` the driver ignores anisotropy entirely —
166
+ anisotropy 1, 8 and 16 render byte-identical. Inside a glTF the sampler wins over the loader, so an exporter can
167
+ commit this silently; `helix validate` warns on it.
168
+ - **Alpha-tested foliage needs coverage-preserving alpha mips**, or leaves thin out and vanish with distance —
169
+ a separate defect from cutout edge-crawl, and `alphaToCoverage` does not fix it.
170
+ - **Old Vault assets predating the mip gate still shimmer** — they are grandfathered, not fixed. Re-publishing the
171
+ asset through the pipeline is the fix.
172
+
173
+ On runtime-lit worlds, anisotropy itself is the runtime's job (set per quality tier at construction) — your job is
174
+ not to break it with the two DON'Ts above.
175
+
176
+ ## Composition — building a frame worth capturing
177
+
178
+ This is the territory the QA scorecard grades (rows 3–5) and nothing else teaches:
179
+
180
+ - **Layer the space: foreground, midground, background.** A flat plane with objects scattered at one distance reads
181
+ as a diorama. Give the camera something near, something at play distance, and something far (a skyline, a wall
182
+ with depth cues, terrain) — and include **scale cues** the player reads instinctively: doorways, railings,
183
+ furniture, lamps, all at real metric sizes against the ~1.8 m avatar.
184
+ - **Repeated scenery ships instanced — mesh count is the ceiling, not triangles.** Every draw call costs
185
+ roughly constant main-thread dispatch (~15 µs, measured) however small the mesh: the platform's ≤300-call
186
+ budget is already ~4.5 ms of a 16.7 ms frame, and every extra hundred calls is another ~1.5 ms gone before
187
+ a pixel shades. An agent-built world shipped its skyline as ~1,200 individual meshes — every lit tower
188
+ window and water glint its own object — for only 72k triangles and 19.7 ms of draw dispatch: **45 fps
189
+ facing the windows, 60 looking away.** So anything you place in a loop (windows, glints, star fields,
190
+ foliage cards, crowds of repeated props) is ONE `InstancedMesh` per material family, per-instance colour
191
+ for variety — the same frame, a handful of draws. When frames are slow, **count draw calls first** (the
192
+ `?perf=1` handle and `helix world perf-gate` both report them) and remedy in order: instance repeated
193
+ families, merge static same-material geometry, and only then reach for resolution or quality levers. The
194
+ cost only shows when the camera FACES the population, so captures pointed elsewhere read clean — which is
195
+ why this is authored up front, not discovered at QA.
196
+ - **Material ROLES, not per-object one-offs.** Good scenes reuse a small palette by role — one floor family, one
197
+ wall family, a trim material on edges and openings, an accent — with wear where hands and feet actually go. A
198
+ world where every object carries its own unrelated material reads as noise; the scorecard's "cohesive material
199
+ language" is this, and reuse is also what keeps the texture budget honest.
200
+ - **Contrast is authored.** Deep shade, bright openings, albedo variation between families (see "scene-side dynamic
201
+ range" above). If every surface is mid-grey, no lighting or grading rescues it.
202
+ - **Practical lights follow the budget patterns** — the emissive-mesh-plus-real-light pairing, the layer-masked
203
+ night rig, and the eight-light ceiling are in the `helix-world-build` skill and the lighting-world night note;
204
+ composition-wise the rule is that light placement is motivated by fixtures the player can see.
205
+
206
+ ## Judging your own captures
207
+
208
+ `capture_world_screenshot` is the closing check — after the numbers (placement via `inspect_world`, traversal via
209
+ `world_metrics`) are already right. When you look, know what a capture can and cannot answer, and iterate honestly:
210
+
211
+ - **One well-chosen angle beats a burst.** Frame the world at active-play framing (what a player actually sees),
212
+ not a turntable beauty angle. Check the mobile viewport too — composition that reads at desktop can collapse at
213
+ 390 px wide.
214
+ - **Change one knob, capture before/after, compare.** A look opinion formed across two captures with three changes
215
+ between them attributes improvement to the wrong knob.
216
+ - **What to look for**, in order: Is anything obviously BROKEN (black surfaces = missing maps or all-metal; lit
217
+ interior walls that should be dark = leaks; wrong-scale props against the avatar)? Does the frame have depth
218
+ layers and deep shade? Do materials read as their substance at play distance? Does light direction match the
219
+ fixtures and sky the player can see? Only then: mood, grade, art direction.
220
+ - **References have limited authority.** A photograph settles *relative* falloff and material behaviour, never
221
+ absolute brightness (your exposure is auto-derived — bias or pin it through lighting-world's `exposure` block,
222
+ not by matching a photo's levels); an engine-demo screenshot settles sun side and gross framing
223
+ and is often a LOW quality bar, not an aspiration; a render made without GI answers framing only — never brightness
224
+ or colour. Do not tune your world to match a reference's property that the reference cannot testify about.
225
+
226
+ ## The hard boundary — no fake light transport
227
+
228
+ The platform's ground rule, and it is craft, not just a gate: **light in a HELIX world is real transport.** No fill
229
+ lights standing in for GI — the runtime provides real indirect light (sky, probes, bounces); a hand-placed fill
230
+ "because the corner is dark" fights it and reads wrong the moment the sun moves. (The sanctioned dials for the
231
+ GLOBAL balance are the transport knobs — `sun.intensityScale`, `sky.irradianceScale`, `environment.intensity`,
232
+ `shadows.opacity` and, for indirect light specifically, `probes.bounces` in lighting-world — plus `exposure.key` /
233
+ `exposure.postGain` to develop the frame. Never an extra light: a second bounce is something the bake can compute,
234
+ and a fill light is a guess at it.) No fog, darkness, bloom or particle
235
+ soup hiding missing geometry — the scorecard fails it on sight, and it always reads as what it is. If a corner is
236
+ too dark, the fixes are real: an opening that admits light, a practical fixture with a real light, a lighter albedo,
237
+ or accepting that some corners are dark because that is what makes the lit parts read.
238
+
239
+ Where to go deeper: `read_doc({ name: "lighting-world" })` for everything the light rig itself does;
240
+ `list_materials` / `use_material` for the palette; the `helix-assets` skill for sourcing anything you do not have;
241
+ the `helix-world-qa` skill for proving the result.
@@ -1,5 +1,7 @@
1
1
  # HELIX Instant — World Project Recipe
2
2
 
3
+ After the first build or Scene import, call `analyze_scene_performance` before visual polish. It returns estimated reference-budget utilization and concrete fixes while leaving unknown runtime costs unknown. Repeat it with the final Scene, Package, inspection snapshot, and performance receipt; `read_doc({ name: "scene-performance" })` defines every counter and the matched fullscreen visual QA.
4
+
3
5
  Follow this recipe exactly to generate a **publishable HELIX Instant world**. A world is a static web app (Vite-built) that runs sandboxed in the HELIX portal and talks to the platform only through `@hypersoniclabs/helix-sdk`.
4
6
 
5
7
  ## Project layout
@@ -27,11 +29,11 @@ my-world/
27
29
  "build": "vite build"
28
30
  },
29
31
  "dependencies": {
30
- "@hypersoniclabs/helix-sdk": "{{SDK_DEP_SPEC}}",
31
- "three": "^0.172.0"
32
+ "@hypersoniclabs/helix-sdk": "{{SDK_DEP_SPEC}}"
32
33
  },
33
34
  "devDependencies": {
34
- "@types/three": "^0.172.0",
35
+ "@types/three": "^0.185.1",
36
+ "three": "^0.185.1",
35
37
  "typescript": "~5.7.3",
36
38
  "vite": "^6.0.0"
37
39
  }
@@ -40,6 +42,8 @@ my-world/
40
42
 
41
43
  Three.js is the usual choice but any web stack works — the only hard requirements are the manifest, the SDK, and a static `dist/` output.
42
44
 
45
+ `three` belongs under `devDependencies`, not `dependencies`: nothing installs a world's npm tree at runtime. Either Vite bundles three into `dist/` (a bare scene like this one) or the platform hosts it and `helix install` import-maps the bare specifier (any world that pins a system — see the character recipe). Keep it on the platform's line, **0.185.1**: a world that pins a system and targets an older three still publishes, but `helix validate`/`publish` warn and name the upgrade path.
46
+
43
47
  ## vite.config.ts — `base: './'` is REQUIRED
44
48
 
45
49
  ```ts
@@ -52,6 +56,13 @@ export default defineConfig({
52
56
  });
53
57
  ```
54
58
 
59
+ **And never write a root-absolute URL for anything inside your own bundle.** `base: './'` only fixes the
60
+ URLs *Vite* emits. Any path you write by hand — `fetch('/data.json')`, `new Audio('/theme.mp3')`,
61
+ `loader.load('/models/ship.glb')` — still resolves against the SITE root, which is not where a published
62
+ world lives (`…/instant-worlds/<id>/<build>/`). It 404s and usually throws, while working perfectly in
63
+ local preview. Resolve against the document instead: `new URL('models/ship.glb', document.baseURI).href`.
64
+ `verify-subpath` (below) is what proves you got them all.
65
+
55
66
  ## public/helix.json — the manifest
56
67
 
57
68
  ```json
@@ -91,6 +102,10 @@ Rules that commonly bite:
91
102
  </html>
92
103
  ```
93
104
 
105
+ **On-screen UI (HUD).** The player shell overlays a chrome bar across the **top-center** of every world (~top 56px:
106
+ Exit / Save Progress / helixOS — user-hideable, but shown by default). Any HUD / DOM overlay you add must stay OUT of
107
+ that strip: anchor it to a corner (top-left/right) or the bottom, never top-center, or it renders behind the chrome.
108
+
94
109
  ## src/main.ts — SDK integration pattern
95
110
 
96
111
  ```ts
@@ -129,9 +144,53 @@ npm install
129
144
  npm run build # → dist/ (contains helix.json, index.html, assets/)
130
145
  ```
131
146
 
132
- Then use the MCP tools, in order:
147
+ Then, in order:
133
148
  1. `validate_world` with the **absolute path to `dist/`** — fix every reported problem.
134
- 2. `whoami` — confirm a creator login exists (if not, the human must run `helix login`).
135
- 3. `publish_world` with the same dist path — returns the public play URL.
149
+ 2. `npx -y {{CLI_PKG}}@latest verify-subpath dist` — **REQUIRED.** A published world is served from
150
+ `…/instant-worlds/<id>/<build>/`, never the origin root, but `npm run preview`, `validate_world` and
151
+ `inspect_world` all serve `dist/` from a root. So they all pass a bundle whose root-absolute URL 404s
152
+ and throws in production. This gate serves the same bytes from a nested path and fails on a throw, a
153
+ 404, or a blank render — it is the only local check that sees the difference.
154
+ 3. `whoami` — confirm a creator login exists (if not, the human must run `helix login`).
155
+ 4. `publish_world` with the same dist path — returns the public play URL.
136
156
 
137
157
  Budget: ≤ 200 files, ≤ 25 MB per file, ≤ 50 MB total. Allowed types include html/js/css/json/wasm, images (png/jpg/webp/svg/ktx2), models (glb/gltf/bin), audio (mp3/ogg/wav), fonts.
158
+
159
+ ## Verify placement by measurement — see the world-inspect doc
160
+
161
+ To check WHERE things actually landed, do not squint at screenshots — `inspect_world` on `dist/` returns the
162
+ scene as data (positions, extents, placement findings), and `focusNear: [x, y, z]` checks just the object you
163
+ placed at that coordinate. Works on a bare scene too (no character system needed — the collider report simply
164
+ says it has nothing to read). Full workflow:
165
+
166
+ ```
167
+ read_doc("world-inspect")
168
+ ```
169
+
170
+ ## In development: scene-streamed worlds — opt-in, only when the creator asks for one
171
+
172
+ The `scene` scaffold kind is a platform feature still in development. Do NOT pick it for "a scene", "a bare scene" or
173
+ "a world with no character" — those are the plain recipe above, or a `character` / `multiplayer` scaffold whose
174
+ geometry you place yourself. Use it ONLY when the creator explicitly asks for a world whose environment is a streamed HELIX Scene
175
+ v2 document, or names a Scene document / Scene Package to stream — and say so in your plan before scaffolding.
176
+
177
+ A **scene world** is the opposite trade from the recipe above: instead of placing geometry in `main.ts`, the world's
178
+ environment is a sealed **HELIX Scene v2** document that the platform's `scene` system streams and the `visual` system
179
+ lights. `scaffold_world({ kind: "scene" })` — `helix init <dir> --kind scene` — writes one: a starter document
180
+ compiled offline into `public/scene/mall/` next to its `pin.json`, that same pin in `public/helix.json`'s `scene`
181
+ field, a `public/helix.visuals.json` lighting contract, a `public/.helixignore` that keeps `scene/` out of the
182
+ uploaded bundle, and a `src/main.ts` that boots in the one order that works: document → visual runtime → scene world
183
+ → character facade. There is no floor, no props and no lights in that file — geometry, materials, authored lights,
184
+ spawn and collision all come from the document. Underneath it is a multiplayer world (presence + proximity voice), so
185
+ the multiplayer recipe still applies on top.
186
+
187
+ Two things differ at publish time: `install_world_packages` must resolve the extra `scene` and `visual` system pins,
188
+ and a published world resolves its environment from the `scene` pin against the backend — so the **document has to be
189
+ published before the world** (`helix world scene-source publish`). Locally, `?scene=<folder>` picks between the
190
+ documents under `public/scene/`.
191
+
192
+ **Terrain** — open ground for a scene world — is the one environment piece you author as DATA rather than place:
193
+ `scaffold_world({ kind: "scene", terrain: "alpine" })` generates a 1 km x 1 km heightfield into the scaffolded
194
+ document, and `terrain_generate` / `terrain_import` / `terrain_inspect` author, adopt and measure it afterwards. It
195
+ is a scene-world feature only, it stays on dynamic lighting (never bake a terrain world), and the folder must be
196
+ inspected before publishing. The whole recipe, the six presets and the caps: `read_doc({ name: "terrain" })`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersoniclabs/helix-mcp",
3
- "version": "0.2.5",
3
+ "version": "0.2.12",
4
4
  "description": "HELIX Instant MCP server — gives AI coding agents the platform contract: world recipes, manifest schema, validation, catalog discovery, and publishing.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/server.js",
@@ -9,15 +9,26 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "docs"
12
+ "docs",
13
+ "scaffold",
14
+ "skills"
13
15
  ],
14
16
  "scripts": {
15
17
  "build": "tsc -p tsconfig.build.json",
16
18
  "lint": "eslint \"src/**/*.ts\"",
17
- "test": "node scripts/smoke.mjs"
19
+ "test": "npm run build && node scripts/smoke.mjs && node scripts/legacy-publish-negative-smoke.mjs && node scripts/continuum-canary-smoke.mjs && node scripts/continuum-pull-smoke.mjs && node scripts/item-distribution-smoke.mjs && node scripts/item-thumbnail-smoke.mjs && node scripts/builds-smoke.mjs && node scripts/looks-smoke.mjs && node scripts/device-smoke.mjs && node scripts/engine-pin-smoke.mjs && node scripts/publish-source-smoke.mjs && node scripts/codex-smoke.mjs --schema-only",
20
+ "test:codex": "npm run build && node scripts/smoke.mjs && node scripts/publish-source-smoke.mjs && node scripts/codex-smoke.mjs",
21
+ "test:package": "npm run build && node scripts/package-smoke.mjs",
22
+ "verify": "npm run test:codex && node scripts/package-smoke.mjs",
23
+ "e2e": "node scripts/e2e-inspect.mjs",
24
+ "test:vault-live": "node scripts/test-vault-live.mjs",
25
+ "test:looks-live": "npm run build && node scripts/looks-live-smoke.mjs"
26
+ },
27
+ "helix": {
28
+ "minCliVersion": "0.1.23-helix3.433"
18
29
  },
19
30
  "dependencies": {
20
- "@hypersoniclabs/helix-cli": "^0.1.7",
31
+ "@hypersoniclabs/helix-cli": "^0.1.23",
21
32
  "@modelcontextprotocol/sdk": "^1.12.0",
22
33
  "zod": "^3.24.0"
23
34
  },