incanto 0.74.0 → 0.76.0

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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -134,12 +134,23 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
134
134
  node in the scene; the handler is a dropdown of real methods when the target
135
135
  has no script, and free text when it does (those handlers live in your
136
136
  TypeScript, which the editor never loads). An end that no longer resolves is
137
- marked red with the reason. For what a row cannot express — `filter` and `once`
138
- — the scene header still has `connections` as raw JSON. (It used to say "bound
139
- args" as well: a connection has never had them. Its keys are exactly
140
- `[signal, from, to, handler, once, filter]`, and since they became a closed
141
- set an invented one is a load error rather than a key that quietly does
142
- nothing.)
137
+ marked red with the reason. For what a row cannot express — `filter`, `once`
138
+ and `args` — the scene header still has `connections` as raw JSON. A
139
+ connection's keys are exactly `[signal, from, to, handler, once, filter,
140
+ args]`, a closed set, so an invented one is a load error rather than a key
141
+ that quietly does nothing. **`args` survives editing but is not shown**: a row
142
+ whose wire carries bound arguments reads as `phase2 → HUD/Banner.show` with no
143
+ hint of the words it says.
144
+
145
+ **Re-pointing a target keeps your handler.** A handler the new target does not
146
+ have is replaced (the row would otherwise stay green here and hard-fail at
147
+ load), and the row remembers what it started with — so changing your mind and
148
+ putting the target back puts the handler back. Before that, a round trip left
149
+ `phase2 → HUD/Banner.show` as `HUD/Banner.childNames`: the replacement took
150
+ the target's first offer, and the offers included the tree plumbing every node
151
+ inherits. They do not any more — the node base class is read the way the
152
+ behavior base class always was, so `queueFree` stays and `childNames`,
153
+ `wiringPath` and the rest are gone.
143
154
  **Validation now runs WITH connections**: the editor used to strip them before
144
155
  loading, so it reported green for a scene the game hard-fails to open.
145
156
  - **Viewport with DIRECT MANIPULATION** — a mode toolbar (top left) + the
@@ -378,8 +389,10 @@ transport instead. With neither, the buttons are simply absent.
378
389
 
379
390
  From that game, the ☰ debug menu's **✎ edit this scene** switches straight back
380
391
  to the editor — same window, no reload, and the edit camera picks up the pose
381
- the player was looking from. **✕ exit** in the editor returns to the game
382
- without playing. The pair is one loop you can ride all session:
392
+ the player was looking from. **✕ exit** returns to the game WITH the scene as
393
+ the editor has it you were editing it, and half the time you saved it, so
394
+ handing back the json the page booted with would show you your own edit
395
+ disappearing on the way out. The pair is one loop you can ride all session:
383
396
 
384
397
  ```
385
398
  game --(☰ debug ▸ ✎ edit this scene)--> editor
@@ -392,6 +405,14 @@ editing wherever the simulation happened to be. Games hosting the editor
392
405
  themselves (`createGame3D({ editor: … })`) get a **save** button only when they
393
406
  pass `editor.save` — otherwise edits live as long as the session does.
394
407
 
408
+ **Saving keeps you in the editor.** It used to throw you out of it: the save
409
+ writes the scene through `incantoScenes()`'s `PUT /api/scene`, vite saw its own
410
+ watched file change, and a JSON module imported by `main.ts` cannot be
411
+ hot-swapped — so vite full-page-RELOADED, the page came back as the game, and
412
+ the selection, the camera and the panel you were working in were gone. The
413
+ plugin now recognises a write it just made and lets the page be. A hand edit in
414
+ a text editor is news the page does not have, and still reloads.
415
+
395
416
  **The handle you booted with does not survive the round trip.** ▶ builds a NEW
396
417
  game, so after one ✎→▶ the `game` your app is holding refers to something that
397
418
  no longer exists. It is inert rather than destructive — a stale `dispose()` and
@@ -509,6 +509,7 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
509
509
  | `leafFadeEnd` | `0` | leaf LOD — cards fully collapsed (zero overdraw) by here; tune ≈ the scene `fog.far` so the thinning hides in fog (load-time check: `> leafFadeStart`) |
510
510
  | `leafShadows` | `true` | `false` keeps trunk/branch shadows but drops the expensive alpha-cutout LEAF shadow pass — big FPS in dense forests |
511
511
  | `drape` | `null` | AUTO by default: each scattered instance roots at the ground height under it (instead of the node's single Y) whenever the scene has a Terrain3D — a `count > 1` grove on rolling/carved terrain never floats or buries. `true` forces, `false` opts out |
512
+ | `avoidWater` | `true` | keep trunks out of standing and running water — a valley a rill runs through has a grove on its BANKS. A wet draw is re-rolled (up to eight times) rather than dropped, so a grove keeps the `count` you asked for even over a lake. `false` is the mangrove |
512
513
  | `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
513
514
 
514
515
  > **Tree LOD / forest FPS.** Leaf overdraw + the leaf shadow pass dominate a
@@ -517,6 +518,14 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
517
518
  > leaf cards (branch silhouette stays), and `leafShadows: false` to drop the leaf
518
519
  > shadow pass. Both default off → existing scenes are unchanged.
519
520
 
521
+ **`grove.trunks()`** answers where every trunk of this grove stands, in world
522
+ coordinates — recomputed from the seed, so it works headless where there is no
523
+ mesh at all. It is how a harness asks whether the drape happened:
524
+ `Math.abs(y - terrain.heightAt(x, z)) < 0.01` for every trunk. (Until it
525
+ existed, nothing could: `Tree3D` read the tri-state `drape` as a boolean, so
526
+ AUTO — the documented default, and what this table has said since 0.14 — meant
527
+ FLAT, and no check in the repo could see it.)
528
+
520
529
  > **Floating trees?** Since 0.14 groves drape automatically whenever the scene
521
530
  > has a Terrain3D — each instance roots on the ground under it. The drape is a
522
531
  > pure heightAt lookup — no rng draw, so the seed/grove layout is
@@ -524,6 +533,19 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
524
533
  > single-Y placement.
525
534
 
526
535
  ## Terrain3D
536
+
537
+ **Putting things on it.** `snapToGround: true` (a `Node3D` prop) sets a node's Y
538
+ from the terrain under it at load — and on a node with a COLLIDER it places the
539
+ collider's FOOT on the ground, not the origin. That is what every hand-written
540
+ lift in this repo already computed (`beacon-isle-3d`'s walker says `0.95` for a
541
+ capsule of radius 0.4 and height 1.1). It matters more than tidiness: a body
542
+ whose origin sits on the surface starts half INSIDE the heightfield, and a
543
+ penetrating body is ejected the nearest way out — which is down, through the
544
+ world, forever. A number still means "place the origin and lift by this".
545
+
546
+ And a terrain is not SOLID on its own: put it under a `StaticBody3D` with
547
+ `collider: { shape: "heightfield" }`, which pulls the terrain's own grid.
548
+
527
549
  Procedural heightfield terrain with biome texture splatting (in `incanto/3d`,
528
550
  registered by `registerNodes3D`), ported from the agent8 starter terrain:
529
551
  seeded simplex octaves displace a plane grid ONCE on the CPU, and a patched
@@ -637,9 +659,25 @@ is radians of `atan(|∇h|)`. Default textures stream from the live agent8 CDN
637
659
  | `layers` | `[]` | custom splat layers (theme `custom` only) |
638
660
  | `textureBase` | agent8 CDN | base URL: `<base>/<name>.png` + `<base>/<name>_normal.png` |
639
661
  | `basins` | `[]` | lake/pond bowls — `[{ x, z, radius, depth }]` (centered meters) carved into the surface (smooth, 0-slope rim); **negative `depth` RAISES a smooth dome mound instead** (a hill, an islet); `heightAt`, the collider AND draped grass all see them |
640
- | `channels` | `[]` | river/road TRENCHES carved along polylines — `[{ path, width, depth, taper? }]` (the line counterpart of `basins`). This is what gives a `River3D` a bed to sit in; `heightAt`, the collider and draped vegetation all see it |
662
+ | `channels` | `[]` | river/road TRENCHES carved along polylines — `[{ path, width, depth, taper? }]` (the line counterpart of `basins`). Points are `[x, z]` **or `[x, y, z]`** (the y ignored), the same as `River3D.path`, so one array feeds both. This is what gives a `River3D` a bed to sit in; `heightAt`, the collider and draped vegetation all see it |
641
663
  | `wetline` | `null` | WET SAND band — `{ y, band? }` darkens + glosses the splat in a noisy, breathing band just above height `y` (a waterline): the trace of the last swash runup. `band` (default `1.1` m) caps how high above `y` reads wet. `null` = off |
642
664
 
665
+ **A `Terrain3D` DRAWS a landscape; it is SOLID only under a body.** Colliders are
666
+ props on bodies in this engine, never implicit, so a bare terrain is scenery the
667
+ player falls through — while it renders perfectly, `heightAt` answers, and
668
+ `snapToGround` puts the whole scene on it. Wrap it:
669
+
670
+ ```json
671
+ { "name": "Floor", "type": "StaticBody3D", "props": { "collider": { "shape": "heightfield" } },
672
+ "children": [ { "name": "Ground", "type": "Terrain3D", "props": { "size": [180, 180] } } ] }
673
+ ```
674
+
675
+ `auditScene` (and so `incanto-check`) says so now, naming the node — a game
676
+ spent an hour falling through the world before it did. It only speaks when
677
+ NOTHING in the scene is solid: a decorative terrain beside a solid one (a lagoon
678
+ bed under the waves, a far ridge) is ordinary.
679
+
680
+
643
681
  **Wet sand at a shoreline.** Set `wetline.y` to the SAME world height as the
644
682
  adjoining `Water3D` surface and the beach keeps a damp, glossy apron that
645
683
  breathes with the swash — sell any beach/lakeshore in one prop:
@@ -721,6 +759,114 @@ every rim cliff top and inside the sand band, and auto-fits `maxHeight`
721
759
  down for seeds whose rim is too tall. Hand-author only when you need a
722
760
  non-default composition.
723
761
 
762
+ ## Weather3D — rain and snow that follow the camera
763
+
764
+ The scene had a sky, a fog, a cloud deck and a sun, and no weather: rain was
765
+ forty lines of `Particles3D` tuning an author had to know, plus a script to
766
+ keep the emitter over the camera. One node now:
767
+
768
+ ```json
769
+ { "name": "Rain", "type": "Weather3D",
770
+ "props": { "kind": "rain", "intensity": 0.7, "wind": [2, 0] } }
771
+ ```
772
+
773
+ | prop | default | what it does |
774
+ |---|---|---|
775
+ | `kind` | `"rain"` | `rain` — fast, straight, a shower · `snow` — slow, drifting flakes |
776
+ | `intensity` | `0.6` | how much of it, 0..1; scales the rate LIVE (a storm that builds, a shower that passes); `0` = a dry sky |
777
+ | `radius` | `20` | half-width (m) of the box the drops are born in, around what is followed |
778
+ | `height` | `12` | how far above it they are born (m) |
779
+ | `wind` | `[0, 0]` | a steady drift, m/s, on the ground plane `[x, z]` — every drop is born with it |
780
+ | `follow` | `""` | what to follow; `""` = the current `Camera3D` |
781
+
782
+ - It is a `Particles3D`: every particle prop is still there to override (a
783
+ colour, a size, `maxParticles`), but `kind` sets them all and the drops
784
+ fall in WORLD space, so what fell stays where it fell when the camera
785
+ moves on. Headless it simulates like any emitter (`aliveCount`), so a
786
+ harness can assert that it rains.
787
+ - **A raindrop is a STREAK.** The particle system draws soft round dots, which
788
+ is right for smoke and sparks and reads as a dirty lens for rain: measured in
789
+ a browser at `intensity: 1`, 2,600 dots the size of dust and nothing that
790
+ looked like weather. Rain now draws with its own stamp — a bright vertical
791
+ line with soft ends — and the drop is 55 px long (0.55 m) instead of 10.
792
+ Snow keeps the flake.
793
+ - **The sun goes BEHIND the camera in rain.** Lit from ahead the streaks wash
794
+ into a bright sky and vanish; from behind they read against it. The rest of a
795
+ storm is the atmosphere block, written live as the weather builds:
796
+
797
+ ```ts
798
+ env.sky = { type: 'atmosphere', elevationDeg: mix(30, 13), azimuthDeg: 10,
799
+ turbidity: mix(5, 14), rayleigh: mix(1.8, 0.65) };
800
+ env.ambient = { color: '#9fb0cc', intensity: mix(0.62, 0.3) };
801
+ fog.far = mix(120, 34);
802
+ ```
803
+
804
+ Every one of those is picked up per frame, so a storm is a behaviour writing
805
+ numbers — there is no weather state machine to learn.
806
+ - **`intensity` is what a game holds on to.** Nothing else about the weather is
807
+ gameplay: whether the rain MEANS anything is the game's own wiring. In
808
+ `examples/moor-3d` it drives the fog, the sky and how far a hound's `Sight`
809
+ reaches — the same hound closes 4.1 m on the player in the clear and 0.0 m in
810
+ the downpour.
811
+ - Composed in `examples/rooftops-3d` (a shower over the run) and
812
+ `examples/moor-3d` (a storm that IS the game).
813
+
814
+ **It OWNS the emitter it inherits.** `Weather3D` extends `Particles3D` and tunes
815
+ the whole thing from `kind` and `intensity` on the first frame — `rate`,
816
+ `speed`, `lifetime`, `drift`, `emitBox`, the colours, the sizes, the blend. Those
817
+ props are inherited, so the loader takes them and the editor offers them, and
818
+ they are DISCARDED. `kind`, `intensity`, `radius`, `height`, `wind` and `follow`
819
+ are what steer it; `auditScene` names any of the others you write.
820
+
821
+ Two props `Weather3D` needed became general: **`emitBox`** on
822
+ `Particles2D/3D` (half-extents of a box the particles are born in — a
823
+ cloud, a dusty field, sparks along a bench; `[0,0,0]` = a point) and
824
+ **`drift`** (a steady velocity every particle is born with — wind on smoke,
825
+ a current in water).
826
+
827
+ ## Flock3D — birds and fish
828
+
829
+ Forty-eight examples and not one flock — no gulls over the harbor, no fish
830
+ under the boat — because a flock was a behaviour per bird and a body per
831
+ bird, and nobody paid that. `Flock3D` is an `InstancedMesh3D` that flies its
832
+ own instances:
833
+
834
+ ```json
835
+ { "name": "Gulls", "type": "Flock3D",
836
+ "props": { "position": [0, 14, 0], "count": 40, "radius": 30, "height": 6,
837
+ "mesh": "box", "size": [0.5, 0.08, 0.3],
838
+ "material": { "color": "#f4f4f4" }, "follow": "/root/Boat" } }
839
+ ```
840
+
841
+ | prop | default | what it does |
842
+ |---|---|---|
843
+ | `count` | `30` | how many; live — a change reseeds |
844
+ | `radius` / `height` | `20` / `8` | the bound: an ellipsoid this wide and this tall around the centre, which they turn back inside of. A flat one (`height` 1.5 at y −2) keeps fish under the surface |
845
+ | `speed` | `6` | every bird's pace, m/s — always, so a flock never stalls |
846
+ | `turn` | `2.5` | the sharpest turn, rad/s; lower is lazier, gull-like |
847
+ | `separation` | `1.5` | closer than this and two birds push apart |
848
+ | `sight` | `5` | how far a bird sees neighbours for the next two |
849
+ | `alignment` / `cohesion` | `1` / `1` | match the neighbours' heading; drift toward their centre. `0` switches one off |
850
+ | `follow` | `''` | a node the centre follows, offset by this node's `position` — gulls 14 m over the boat, wherever it sails |
851
+
852
+ Plus everything `InstancedMesh3D` has (`mesh`, `size`, `material`, shadows);
853
+ `transforms` is driven for you and `collider` is refused — birds are not
854
+ solid. Each instance faces +z along its flight, so a mesh longer in z reads
855
+ as a body with a beak. One draw call; forty birds cost forty-squared distance
856
+ checks a frame, so keep `count` in the dozens, not the thousands.
857
+
858
+ Seeded from `engine.rng`: a seeded run flies the same flight. **`boids()`**
859
+ answers headless — `{x, y, z, vx, vy, vz}` per bird in world metres — so a
860
+ harness can ask that the fish stayed under the water and the gulls over it
861
+ (`examples/harbor-3d` does).
862
+
863
+ **It owns `transforms`.** `Flock3D` extends `InstancedMesh3D` and rewrites them
864
+ every frame from the boids, so authoring a row there does nothing — `count`,
865
+ `radius`, `height`, `speed`, `turn`, `separation`, `sight`, `alignment`,
866
+ `cohesion` and `follow` are what steer it, and `auditScene` says so if you write
867
+ the other. `boids()` answers where every one of them is, in world metres and
868
+ m/s, which is how a harness asks whether they kept station.
869
+
724
870
  ## Water3D
725
871
 
726
872
  ### The player's graphics setting reaches this node
@@ -761,15 +907,29 @@ A commercial-quality water surface (in `incanto/3d`, registered by
761
907
  `examples/water-*-3d` templates stage one each (cove pier, resort pool,
762
908
  mirror-lake dock, walled garden pond). The default `quality: 'fancy'` is a
763
909
  real water shader:
764
- 9-iteration simplex-FBM wave displacement with analytic normals, optional
910
+ 9-iteration simplex-FBM wave displacement with analytic normals — or, with
911
+ `wind` > 0, a real JONSWAP SPECTRUM inverse-FFT'd on the GPU every frame (the
912
+ ocean preset: 65 536 waves, each at its own deep-water speed, choppy crests,
913
+ whitecaps where the surface folds, and a per-pixel slope texture whose mips
914
+ hand a far pixel the slope VARIANCE it integrates — the roughness of a distant
915
+ sea, measured rather than guessed) — plus optional
765
916
  DIRECTIONAL TRAVELING SWELL trains + open-water whitecaps (`swell` /
766
917
  `whitecaps` — the open-ocean look; see the prop table), a DISPERSION-TIMED
767
918
  micro-ripple spectrum — four octaves (λ 6 m → 0.38 m), each advected at its
768
919
  REAL deep-water phase speed so short ripples flicker ~2×/s (slow fine ripple
769
920
  is what makes water read as syrup), the fine octaves interference-PAIRED so
770
- the surface churns instead of sliding, each octave distance-faded sun
771
- glitter straight off that spectrum (facets align for only a flash natural
772
- grain, weighted toward the sun's azimuth path and grazing views), per-channel
921
+ the surface churns instead of sliding, each octave distance-faded, plus a
922
+ 16 cm "spark" octave inside arm's reach the SUN as a real GGX microfacet
923
+ highlight off that spectrum (near the camera every resolved facet
924
+ that faces the sun flashes, HDR so the bloom pass reads it; with distance
925
+ the roughness grows and the same lobe widens into the continuous sun path a
926
+ far sea shows — no azimuth gate, no hand-shaped powers), a SEA-STATE
927
+ fresnel (the far, wind-roughened field reflects the sky at the ~0.5-0.6 a
928
+ rough mirror really does at grazing angles, so the ocean stays a darker band
929
+ under its sky instead of whiting out into the horizon haze; calm near water
930
+ is untouched), light THROUGH the crest (with `swell`: looking toward the sun
931
+ the thin crests glow translucent turquoise — lit from inside, the one cue
932
+ that says water rather than painted lumps), per-channel
773
933
  Beer's-law depth `absorption` (red dies first — turquoise shallows fade
774
934
  WIDE into deep teal-blue, like a real sea), CRYSTAL shallows (inside the
775
935
  first ~1.5 m of water the refracted bottom dominates, ripple-distorted —
@@ -814,24 +974,28 @@ hue reads grey.
814
974
  | `reflection` | `true` | live CubeCamera reflections (fancy only; needs a renderer) |
815
975
  | `reflectionInterval` | `1000` | ms between CubeCamera reflection re-renders |
816
976
  | `mirror` | `false` | TRUE planar reflection (see below) — the only way the dock, the boat and the player appear IN the water. One extra scene render per frame |
977
+ | `ssr` | `true` | screen-space reflections (fancy only): the scene ABOVE the waterline is rendered once at half res and every water pixel marches its reflected ray through that image — a rock, a pier, a wader, a boat mirrored on the water with NO planar pass. What the screen cannot see (behind the camera, past its edges) falls back to the sky cube, so a mirror-calm lake still wants `mirror: true`; with `mirror` on this is skipped as redundant, and a tier that forbids the mirror pass forbids this too (same cost class: one more half-res scene submission). `false` keeps the cube-only sky reflection |
817
978
  | `mirrorInterval` | `33` | ms between planar-mirror re-renders. A reflection seen through a rippling surface does not need 60 Hz; 33 halves the pass's cost. 0 = every frame |
818
979
  | `swellSteepness` | `0.35` | GERSTNER pinch on the swell: 0 = height-only trains, 1 = maximum trochoid (crests narrow to a peak, troughs broaden). It also decides where whitecaps break — the surface's Jacobian collapses on a folding crest, and that is where foam belongs |
819
980
  | `foam` | `true` | noise-broken shoreline foam band + crest foam near shores |
820
981
  | `interaction` | `true` | character interaction: bodies (CharacterBody3D/RigidBody3D) inside the XZ footprint whose FEET dip below the surface count as in-water — so a WADING character (origin above water, feet below, sized from its collider) splashes, not only a fully-submerged one. Entry/exit emit `entered`/`exited` + a splash ripple; striding through water trails a WAKE (a ripple every ~0.45 m); floating bodies bob gently. Ripples drive both shaders' `uRipples[8]` (fancy) and CPU-displace the mesh (`simple`) |
821
982
  | `splash` | `true` | built-in splash WHITEWATER (needs `interaction`): the shaders paint expanding **surface foam** where bodies interact — a **"풍덩"** froth bloom when a body plunges in (bigger the faster it falls) and a **foam trail ("물살")** behind a body wading/running through. Real frothy whitewater on the surface, not flying droplets; works on both `fancy` and `simple` quality. `false` keeps the ripples/waves but paints no foam. No wiring needed; for an extra custom burst add your own `Particles3D` on the `entered` signal |
822
983
  | `sunDirection` | `[0.5, 0.8, 0.3]` | TOWARD the sun, for the specular glint — match the scene's key light (non-zero, load-time check) |
823
- | `sunColor` | `"#fff5d6"` | glint/sheen tint |
824
- | `sunIntensity` | `1` | glint/sheen strength (≥ 0; 0 = off) |
825
- | `detailStrength` | `0.26` | animated detail-normal strength (≥ 0; 0 = vertex normals only) |
984
+ | `sunColor` | `"#fff5d6"` | sun highlight tint |
985
+ | `sunIntensity` | `1` | sun highlight strength (≥ 0; 0 = off). The highlight is a GGX lobe over the detail normal and runs HDR on purpose — the sun's reflection is the brightest thing on any water and is what `environment.bloom` catches; 0.7-1.25 is the presets' range |
986
+ | `detailStrength` | `0.26` | animated detail-normal strength (≥ 0; 0 = vertex normals only). Under it runs a LIVE ripple FFT (a second 6 m spectrum patch, 4.7 cm texels, inverse-transformed every frame): the wind ripple every fancy surface wears — real churn with a mip chain for distance instead of scrolling noise — normalized so its rms slope is 0.8 × this value; three short procedural octaves stay on at reduced weight for the sub-pixel facets that flash (a filtered texture alone hands the sun one soft lobe). Without float render targets the noise octaves carry it all, as before |
826
987
  | `absorption` | `0.15` | Beer's-law constant per meter of water depth (≥ 0; lower = clearer/wider turquoise band; red absorbs ~3× faster than blue under the hood) |
827
988
  | `refraction` | `true` | screen-space refraction of the submerged scene |
828
- | `preset` | `"custom"` | **THE one-choice water type** (0.16.0): `ocean` \| `pool` \| `lake` \| `pond` bundles every wave/color/clarity/caustics knob into a hand-tuned sea state. A preset value applies ONLY where a prop still sits at its schema default — pick the type, then override any detail prop and your value wins. ocean = traveling swell + whitecaps + wide turquoise→deep absorption · pool = near-flat crystal water + strong above-water caustics · lake = calm high-reflectivity mirror, green-teal murk · pond = still, mossy, opaque quickly. `custom` (default) changes nothing |
989
+ | `preset` | `"custom"` | **THE one-choice water type** (0.16.0): `ocean` \| `pool` \| `lake` \| `pond` bundles every wave/color/clarity/caustics knob into a hand-tuned sea state. A preset value applies ONLY where a prop still sits at its schema default — pick the type, then override any detail prop and your value wins. ocean = traveling swell + whitecaps + wide turquoise→deep absorption · pool = near-flat crystal water + strong above-water caustics · lake = calm high-reflectivity mirror, green-teal murk, short low wind chop (a 9 m set at 3 cm — a lake has no swell, and the 24 m one it used to ship was the single strongest source of the 꿀렁꿀렁 gel read) · pond = still, mossy, opaque quickly. `custom` (default) changes nothing |
829
990
  | `shoreWaves` | `0` | **traveling shore BREAKERS** 0–1 (fancy + depth pre-pass, 0.17.0): foam fronts that roll down the depth gradient and break toward every beach — fronts follow the shore's depth contours automatically (bays curve them, points wrap them, zero wiring). Sharp collapsing face + long noisy wash, alive in the 0.1–3.2 m shoaling band. Ocean preset ships 0.7 |
830
991
  | `causticsAbove` | `0` | above-water CAUSTICS intensity 0–3 (fancy + `refraction` only): the dancing light web on the submerged bottom seen THROUGH the surface — the pool / tropical-shallows look. Reconstructed on the real refracted bottom position, strongest through the first meters of water, distance-faded. Presets set it (pool 1.0); override freely |
831
992
  | `swell` | `0` | **THE open-ocean dial** (fancy only, 0.15.0): directional traveling swell amplitude in meters. Three peaked wave trains (primary + two crossing sets) march across the surface at real deep-water speeds; crests light up toward the peak color and wave FACES get sun-lit/shadow shading, so the swell reads at any distance. 0.15 = lively lake chop, 0.4 = coastal sea, 0.7 = heavy weather. `0` (default) keeps the legacy calm look byte-identical |
832
993
  | `swellDirectionDeg` | `0` | compass direction the swell TRAVELS toward (0 = +z, 90 = +x) |
833
994
  | `swellWavelength` | `30` | primary swell wavelength, meters (≥ 2, load-time check); the crossing trains derive from it |
834
- | `whitecaps` | `0` | open-water whitecap foam on the tallest crests, 0–1 (fancy only; load-time check). Pairs with `swell` — the caps ride its crest lines and fade with distance so the horizon stays clean |
995
+ | `whitecaps` | `0` | open-water whitecap foam on the tallest crests, 0–1 (fancy only; load-time check). Pairs with `swell` — the caps ride its crest lines and fade with distance so the horizon stays clean. With `wind` on, the caps are born where the FFT sea's surface actually FOLDS (its Jacobian) and fade over seconds |
996
+ | `bed` | `""` | node path to the Terrain3D under this water (fancy only). With it the swell KNOWS the beach: once the water is shallower than a quarter wavelength a wave RISES by Green's law (depth^-1/4, capped 2×) and its crest peaks; at 0.78× the depth it BREAKS — the height is capped there and the crest turns to torn whitewater — and the FFT sea shoals and breaks the same way. Spray sprites burst on the 0.3–0.9 m break line when the `shoreWaves` foam fronts pass (needs `shoreWaves` > 0; a few hundred camera-facing puffs, one draw). Sampled once into a 256² depth grid over the terrain's footprint; beyond it the water is assumed 12 m deep. The ocean template points it at its island |
997
+ | `wind` | `0` | **THE realistic-sea dial** (fancy only; m/s; ≥ 0, load-time check). Above 0 the surface stops being a noise field and becomes a JONSWAP SPECTRUM — thousands of waves at every wavelength and heading, each moving at the speed deep water gives it — inverse-FFT'd on the GPU every frame (256², ~37 tiny passes) into a 200 m tiling patch with real crest shapes, choppy horizontal drag and fold-born whitecaps. Blows toward `swellDirectionDeg`. 4 = a light breeze (20 cm ripple), 6.5 = the ocean preset's fresh breeze (Hs ≈ 1.4 m), 12 = rough. `waveHeight` is inert while wind > 0 (the FBM is replaced, not added); `swell` still rides underneath as the long-period set a 200 m patch cannot hold. `heightAt` / buoyancy / waders sum the strongest 2048 modes of the SAME spectrum on the CPU (~8% of the rms height, ~45 µs a call). Needs float render targets (every WebGL2 desktop, iOS 15+) — without them the analytic waves stay and the CPU twin follows |
998
+ | `choppiness` | `0.85` | horizontal drag of the FFT sea toward its crests (≥ 0): 0 = pure height field, ~0.9 = narrow crests over broad troughs, past ~1.2 the surface folds and foams everywhere |
835
999
  | `underwater` | `true` | submerged-camera look: ONLY when the camera EYE is below this surface (inside its XZ footprint) — wading with the camera above the water never tints the view — the scene switches to short-range underwater fog + tint, the sky is hidden, and animated **caustics** (depth-aware, projected onto the submerged floor/props) play. `false` disables it; an object `{ color?, visibility?, caustics? }` overrides the murk hue / view distance (m, default 22) / caustics. `caustics` is `true` (default) \| `false` \| `{ color?, intensity?, scale?, speed? }`. The murk color defaults to a darkened shade of `color`. Works for both qualities (the caustics composite runs only while the camera is genuinely underwater) |
836
1000
 
837
1001
  Performance notes (fancy): beyond drawing the surface, each rendered frame
@@ -882,6 +1046,26 @@ plane and samples it per fragment, so those things appear in the water:
882
1046
  - Geometry below the waterline is clipped out of the mirror pass, and the whole
883
1047
  pass is skipped while the camera is submerged.
884
1048
 
1049
+ ### Asking how deep something is — `depthAt(x, y, z)`
1050
+
1051
+ ```ts
1052
+ const pool = game.scene.root.getNode('Pool') as Water3D;
1053
+ const under = pool.depthAt(...player.position); // metres below the surface, 0 above
1054
+ if (under > 0.5) breath -= dt;
1055
+ ```
1056
+
1057
+ Metres BELOW this water's surface, `0` above it — and `0` outside the pool's
1058
+ footprint, because a point that is not over the water is not in it. That last
1059
+ part is the whole reason it exists: `heightAt(x, z)` answers the surface height
1060
+ EVERYWHERE, so the obvious spelling of "am I under water"
1061
+ (`y < water.heightAt(x, z)`) calls a player standing in a cellar a hundred
1062
+ metres inland submerged. The wave is included, so a swimmer bobbing under a
1063
+ trough and under a crest get different answers, which is what they feel.
1064
+
1065
+ A breath meter, muffled audio, a "you surfaced" banner and an enemy that only
1066
+ strikes below are all this one call. With several pools, ask the one the player
1067
+ is over — or take the largest answer.
1068
+
885
1069
  ### Under the surface
886
1070
 
887
1071
  Swimming down is a different optical world, and the engine renders it as one:
@@ -1069,8 +1253,67 @@ emitter to the body first — the signal hands you the node:
1069
1253
  that emitter `"worldSpace": true`, or the second splash drags the first one to
1070
1254
  the new spot.)
1071
1255
 
1256
+ ### Things that float — `RigidBody3D.buoyancy`
1257
+
1258
+ Two ways to float a body, and the difference is who decides the waterline.
1259
+ The **`Buoyancy` behaviour** (`incanto-gameplay-behaviors.md`) is the one
1260
+ you AUTHOR: a `draft`, a hull `size`, a `stiffness`, and `drift` — a wave
1261
+ face is a slope and a raft slides down it — which is what a raft, a cast
1262
+ bobber or a boat with a tuned feel wants (`beacon-isle-3d`, `fishing-3d`,
1263
+ `water-ocean-3d`). The **`buoyancy` prop** below is the one PHYSICS decides:
1264
+ Archimedes on the collider, so `mass` against its volume says whether it
1265
+ floats and how deep — a crate that sinks when it is heavy, a hull that sits
1266
+ lower with cargo, an anchor that goes down by the same rule as the crates
1267
+ that do not. Use one, never both on one body.
1268
+
1269
+ ```json
1270
+ { "name": "Crate", "type": "RigidBody3D",
1271
+ "props": { "position": [4, 2, 0], "mass": 150, "buoyancy": 1,
1272
+ "collider": { "shape": "box", "size": [1, 1, 1] } } }
1273
+ ```
1274
+
1275
+ - **`buoyancy: 1` is water's own upthrust** — the submerged part of the
1276
+ collider's volume at 1000 kg/m³ — so `mass` against the collider decides
1277
+ everything: a 1 m³ crate at 200 kg floats a fifth under (its centre 0.3 m
1278
+ over the surface), at 600 kg it sits deeper, at 3000 kg it sinks to the bed.
1279
+ A boat is a box hull at a third to a half of its volume in kg: `[2.2, 0.8,
1280
+ 4.4]` at 3000 kg sits 0.3 m in. `0` (the default) is air. Under 1 lifts
1281
+ less for the same mass; nothing needs it, and mass is the honest knob.
1282
+ - **It rides the waves.** The upthrust reads `heightAt` under eight sample
1283
+ points across the collider, so the surface it floats on is the one the
1284
+ player sees — and a tilted hull's low corners push harder, which is what
1285
+ rights it. A CUBE a fifth under has a metacentric height of 1.7 cm, all but
1286
+ neutral, so it rolls easily; a wide flat hull rights itself firmly. Author
1287
+ crates light (150 kg for 1 m³) and hulls wide.
1288
+ - **It settles in about one bob whatever it weighs**: the vertical motion is
1289
+ damped to 0.7 of critical for the hull's own spring (a crate dropped from
1290
+ 3 m bounced out of the water and back for half a minute before that).
1291
+ - **`Water3D.drag`** (1.5) holds travel and spin back, per second, in full
1292
+ once a quarter of the hull is under: a pushed crate coasts a few lengths and
1293
+ stops; `0` is ice, `6` is syrup. Spin loses three times that — a hull turning
1294
+ drags its whole wetted side.
1295
+ - The shape is what Rapier built: a box, sphere, capsule or cylinder by its
1296
+ own volume; an `auto` fit as the box it made; a hull or mesh as the box
1297
+ around its vertices. The swimmer's body (a `CharacterController3D` under
1298
+ it) is never lifted here — the controller's swim spring owns that one.
1299
+ - Any footprint works: a body beside the lake, over dry ground, is not lifted;
1300
+ one over the water is, from the moment its lowest sample dips under.
1301
+
1302
+ Thrust and steering are yours — `applyImpulse` along the hull's +z and a
1303
+ written `angularVelocity[1]` for the rudder are all `examples/harbor-3d`
1304
+ needs to drive a boat, push six crates into a pen and lose one to the rocks.
1305
+
1072
1306
  ## River3D — running water
1073
1307
 
1308
+ **A river with no `path` is not a river.** `path` defaults to `[]` and one point
1309
+ is a hard load error, so the shape that is checked is the one nobody writes: with
1310
+ NO points the node builds no channel — it draws nothing, carves nothing and
1311
+ pushes nothing, whatever `width`, `flowSpeed` and `flowForce` say. It warns once
1312
+ on its first frame now, rather than being an inert node in a scene that audits
1313
+ clean. Give it at least two `[x, z]` control points, source → mouth, in the
1314
+ node's own local frame.
1315
+
1316
+
1074
1317
  `Water3D` is a *surface*; `River3D` is a *current*. A river is authored as a
1075
1318
  line, not a shape: give it a centerline `path` and a width, and the terrain
1076
1319
  underneath decides everything else.
@@ -1087,21 +1330,21 @@ underneath decides everything else.
1087
1330
 
1088
1331
  | prop | default | meaning |
1089
1332
  |---|---|---|
1090
- | `path` | `[]` | centerline `[[x, z], …]` in NODE-LOCAL meters, Catmull-Rom smoothed (2+ points) |
1333
+ | `path` | `[]` | centerline `[[x, z], …]` in NODE-LOCAL meters, Catmull-Rom smoothed (2+ points). **`[x, y, z]` is accepted too** and the y is ignored — a course has no height of its own, and every other world coordinate in a 3D scene is three numbers, so one array can feed both this and `Terrain3D.channels` |
1091
1334
  | `width` | `6` | channel width in meters |
1092
1335
  | `widths` | `[]` | width profile lerped source→mouth (e.g. `[2.5, 5, 9]`); empty = constant `width` |
1093
1336
  | `depth` | `0.8` | water column at the centerline — also the scale foam thresholds read as "shallow" |
1094
1337
  | `flowSpeed` | `1.6` | reference current in m/s (the mean; reaches speed up and slow down) |
1095
- | `colors` | `{}` | `{ shallow, deep, sky, horizon, bank }` — the body ramp plus what the surface mirrors |
1338
+ | `colors` | `{}` | `{ shallow, deep, sky, horizon, bank }` — the body ramp plus what the surface mirrors. In a live renderer the river keeps its OWN sky cube (the world beyond its banks, refreshed every 2 s, near-clipped so the banks never smear across it) and mirrors THAT through a sea-state fresnel; `sky`/`horizon` are then the headless fallback and `bank` is still what the lowest rays meet. A 6 m ripple FFT rides the current in the ribbon's own frame under `ripples` |
1096
1339
  | `absorption` | `0.5` | how fast the water hides its bed (Beer's law) |
1097
1340
  | `opacity` | `0.85` | upper bound on the body's opacity |
1098
1341
  | `foam` | `1` | whitewater dial — 0 = a glassy canal, 2 = raging |
1099
- | `ripples` | `1` | surface-detail dial (ripple relief + glitter) |
1342
+ | `ripples` | `1` | surface-detail dial (ripple relief, the fine near-field wavelets, and how much sun the water throws) |
1100
1343
  | `terrain` | `""` | drape target path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
1101
1344
  | `carve` | `true` | cut the bed. The river trenches its own channel into the terrain it drapes on — see below |
1102
1345
  | `flowForce` | `1` | how hard the current sweeps bodies downstream (0 = visual only) |
1103
1346
  | `spray` | `1` | mist at the foot of every drop the course turns out to contain — the node finds them and hangs a Particles3D plume on each (0 = off) |
1104
- | `sunDirection` / `sunColor` / `sunIntensity` | sky / `#fff6e0` / `1` | glint (the environment sun wins while `sunDirection` is default) |
1347
+ | `sunDirection` / `sunColor` / `sunIntensity` | sky / `#fff6e0` / `1` | the sun highlight — a GGX lobe over the wavelet normal, HDR so the bloom pass reads it (the environment sun wins while `sunDirection` is default) |
1105
1348
 
1106
1349
  **What the node derives for you**, all from `path` + the ground:
1107
1350
 
@@ -42,7 +42,8 @@ script was dying reads as a healthy character, so read those lines first.
42
42
  holds `move` for five seconds and takes the 90th percentile of the run, because
43
43
  the maximum belongs to whatever hit the character last. Measured on the shipped
44
44
  `platformer-2d`, whose `RUN_SPEED` is 250: it reported `top speed 300 u/s` —
45
- that is `KNOCK_X`, from an enemy the probe walked into and `to full speed
45
+ that is the knockback (then a hand-written `KNOCK_X`, now the player Health's
46
+ `knockback`), from an enemy the probe walked into — and `to full speed
46
47
  2367 ms`, which was the walk to the enemy, on a character that reaches its speed
47
48
  in ONE frame. Four numbers, all about a character being attacked. When the peak
48
49
  and the sustained speed disagree the report says so:
@@ -127,6 +128,64 @@ When nothing moves it says so instead of printing a zero:
127
128
  OFF — you asked about that action, so a silent answer about a different one
128
129
  would be worse than no answer.
129
130
 
131
+ ## It presses your title screen first
132
+
133
+ A game with a title screen boots PAUSED — `GameFlow.pause()` from the shell's
134
+ `onReady` is what holds the world behind the menu — and a probe that drives a
135
+ stopped world measures nothing. Three of this repo's own examples ship that
136
+ shell, and so does every game scaffolded from one.
137
+
138
+ So before measuring, the probe presses what a player can reach: the HUD's
139
+ `focusables()` (which already excludes hidden panels and disabled buttons),
140
+ buttons labelled PLAY / START / RESUME first, until `engine.timeScale` stops
141
+ being 0. Nothing is pressed in a game that was already running.
142
+
143
+ When even that does not start it, the report says so instead of printing zeros:
144
+
145
+ ```
146
+ the clock never ran: this game was PAUSED for the whole measurement
147
+ (`engine.timeScale` 0), and it stayed that way after pressing every button on
148
+ screen. …
149
+
150
+ coyote window not measured — the clock was stopped (the scene declares 120 ms)
151
+ jump buffer not measured — the clock was stopped (the scene declares 150 ms)
152
+ ```
153
+
154
+ **A window nobody could measure is not a window of zero.** Before this, a shipped
155
+ starter that declares `coyoteSeconds: 0.12` reported `coyote window 0 ms` and
156
+ advised its author to set `coyoteSeconds: 0.12`. The advice below only ever fires
157
+ on a number that was actually taken.
158
+
159
+ It also sits out an opening **cutscene**. That is the other way a game holds the
160
+ player still, and it looks nothing like a pause: the clock runs, the world moves,
161
+ and only `controller.enabled` is false while a camera flies around. The probe
162
+ waits up to fifteen seconds for control and says so if it never comes.
163
+
164
+ ## The windows are searched by JUMPING
165
+
166
+ `coyote window` and `jump buffer` are measured by lifting the body and pressing —
167
+ so a character that never leaves the ground fails every step of that search, and
168
+ that failure came back as `0`. Three shipped examples printed
169
+ `jump apex — (nothing left the ground)` and then `coyote window 0 ms` with advice
170
+ attached, one of them declaring `coyoteSeconds: 0.12` in the same file.
171
+
172
+ When nothing jumps in any of the four probes (standing, held, walking,
173
+ sprinting), both windows read `not measured — nothing ever jumped` and the report
174
+ says the thing worth saying instead:
175
+
176
+ ```
177
+ note: this scene declares 'jump' and NOTHING left the ground in any
178
+ of the four jump probes — standing, held, walking, sprinting. Check that the
179
+ controller reads 'jump', that its `jumpVelocity` is not 0, and that it is `enabled`.
180
+ ```
181
+
182
+ **In a two-player scene, the player it drives and the buttons it presses are the
183
+ same character's.** They were not: the props were read from whichever controller
184
+ a stack-walk reached first, which is the LAST one in the file, so a local co-op
185
+ game's player one was sent player two's jump button and reported a jump apex of
186
+ nothing. Move survived it — the move probe has a fallback SEARCH — and jump has
187
+ none, which is why the whole jump half of that report was dashes.
188
+
130
189
  ## It reports, it does not grade
131
190
 
132
191
  There are no PASS/FAIL bands here on purpose. A twitch platformer and a survival