incanto 0.74.0 → 0.75.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 +743 -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-BAiA-FjP.js → create-game-DVUhHs2B.js} +79 -10
  10. package/dist/{create-game-ssvGvP7W.js → create-game-DnwhJ5nW.js} +83 -7
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-O8mxpgRX.d.ts} +1 -1
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-BP77Dl0K.js} +2005 -60
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-D-JuGura.js} +9309 -6595
  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-x3bCrQf0.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-Bz2VPQ6D.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-Buita5T9.js} +2 -2
  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-CjociOtt.js → register-B768Qedj.js} +596 -2
  31. package/dist/{register-DRQyZKGU.js → register-DxAbTO2T.js} +191 -430
  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-DLKVc6Hj.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-HlVUeCSn.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-BXlYM9rv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-S42XGP4Y.js} +1 -1
  45. package/editor/assets/index-DgrgUKno.js +11046 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1630 -299
  49. package/skills/incanto-3d-character.md +358 -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 +17 -6
  54. package/skills/incanto-environment.md +227 -2
  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 +393 -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 +151 -2
  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
@@ -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
@@ -882,6 +1028,26 @@ plane and samples it per fragment, so those things appear in the water:
882
1028
  - Geometry below the waterline is clipped out of the mirror pass, and the whole
883
1029
  pass is skipped while the camera is submerged.
884
1030
 
1031
+ ### Asking how deep something is — `depthAt(x, y, z)`
1032
+
1033
+ ```ts
1034
+ const pool = game.scene.root.getNode('Pool') as Water3D;
1035
+ const under = pool.depthAt(...player.position); // metres below the surface, 0 above
1036
+ if (under > 0.5) breath -= dt;
1037
+ ```
1038
+
1039
+ Metres BELOW this water's surface, `0` above it — and `0` outside the pool's
1040
+ footprint, because a point that is not over the water is not in it. That last
1041
+ part is the whole reason it exists: `heightAt(x, z)` answers the surface height
1042
+ EVERYWHERE, so the obvious spelling of "am I under water"
1043
+ (`y < water.heightAt(x, z)`) calls a player standing in a cellar a hundred
1044
+ metres inland submerged. The wave is included, so a swimmer bobbing under a
1045
+ trough and under a crest get different answers, which is what they feel.
1046
+
1047
+ A breath meter, muffled audio, a "you surfaced" banner and an enemy that only
1048
+ strikes below are all this one call. With several pools, ask the one the player
1049
+ is over — or take the largest answer.
1050
+
885
1051
  ### Under the surface
886
1052
 
887
1053
  Swimming down is a different optical world, and the engine renders it as one:
@@ -1069,8 +1235,67 @@ emitter to the body first — the signal hands you the node:
1069
1235
  that emitter `"worldSpace": true`, or the second splash drags the first one to
1070
1236
  the new spot.)
1071
1237
 
1238
+ ### Things that float — `RigidBody3D.buoyancy`
1239
+
1240
+ Two ways to float a body, and the difference is who decides the waterline.
1241
+ The **`Buoyancy` behaviour** (`incanto-gameplay-behaviors.md`) is the one
1242
+ you AUTHOR: a `draft`, a hull `size`, a `stiffness`, and `drift` — a wave
1243
+ face is a slope and a raft slides down it — which is what a raft, a cast
1244
+ bobber or a boat with a tuned feel wants (`beacon-isle-3d`, `fishing-3d`,
1245
+ `water-ocean-3d`). The **`buoyancy` prop** below is the one PHYSICS decides:
1246
+ Archimedes on the collider, so `mass` against its volume says whether it
1247
+ floats and how deep — a crate that sinks when it is heavy, a hull that sits
1248
+ lower with cargo, an anchor that goes down by the same rule as the crates
1249
+ that do not. Use one, never both on one body.
1250
+
1251
+ ```json
1252
+ { "name": "Crate", "type": "RigidBody3D",
1253
+ "props": { "position": [4, 2, 0], "mass": 150, "buoyancy": 1,
1254
+ "collider": { "shape": "box", "size": [1, 1, 1] } } }
1255
+ ```
1256
+
1257
+ - **`buoyancy: 1` is water's own upthrust** — the submerged part of the
1258
+ collider's volume at 1000 kg/m³ — so `mass` against the collider decides
1259
+ everything: a 1 m³ crate at 200 kg floats a fifth under (its centre 0.3 m
1260
+ over the surface), at 600 kg it sits deeper, at 3000 kg it sinks to the bed.
1261
+ A boat is a box hull at a third to a half of its volume in kg: `[2.2, 0.8,
1262
+ 4.4]` at 3000 kg sits 0.3 m in. `0` (the default) is air. Under 1 lifts
1263
+ less for the same mass; nothing needs it, and mass is the honest knob.
1264
+ - **It rides the waves.** The upthrust reads `heightAt` under eight sample
1265
+ points across the collider, so the surface it floats on is the one the
1266
+ player sees — and a tilted hull's low corners push harder, which is what
1267
+ rights it. A CUBE a fifth under has a metacentric height of 1.7 cm, all but
1268
+ neutral, so it rolls easily; a wide flat hull rights itself firmly. Author
1269
+ crates light (150 kg for 1 m³) and hulls wide.
1270
+ - **It settles in about one bob whatever it weighs**: the vertical motion is
1271
+ damped to 0.7 of critical for the hull's own spring (a crate dropped from
1272
+ 3 m bounced out of the water and back for half a minute before that).
1273
+ - **`Water3D.drag`** (1.5) holds travel and spin back, per second, in full
1274
+ once a quarter of the hull is under: a pushed crate coasts a few lengths and
1275
+ stops; `0` is ice, `6` is syrup. Spin loses three times that — a hull turning
1276
+ drags its whole wetted side.
1277
+ - The shape is what Rapier built: a box, sphere, capsule or cylinder by its
1278
+ own volume; an `auto` fit as the box it made; a hull or mesh as the box
1279
+ around its vertices. The swimmer's body (a `CharacterController3D` under
1280
+ it) is never lifted here — the controller's swim spring owns that one.
1281
+ - Any footprint works: a body beside the lake, over dry ground, is not lifted;
1282
+ one over the water is, from the moment its lowest sample dips under.
1283
+
1284
+ Thrust and steering are yours — `applyImpulse` along the hull's +z and a
1285
+ written `angularVelocity[1]` for the rudder are all `examples/harbor-3d`
1286
+ needs to drive a boat, push six crates into a pen and lose one to the rocks.
1287
+
1072
1288
  ## River3D — running water
1073
1289
 
1290
+ **A river with no `path` is not a river.** `path` defaults to `[]` and one point
1291
+ is a hard load error, so the shape that is checked is the one nobody writes: with
1292
+ NO points the node builds no channel — it draws nothing, carves nothing and
1293
+ pushes nothing, whatever `width`, `flowSpeed` and `flowForce` say. It warns once
1294
+ on its first frame now, rather than being an inert node in a scene that audits
1295
+ clean. Give it at least two `[x, z]` control points, source → mouth, in the
1296
+ node's own local frame.
1297
+
1298
+
1074
1299
  `Water3D` is a *surface*; `River3D` is a *current*. A river is authored as a
1075
1300
  line, not a shape: give it a centerline `path` and a width, and the terrain
1076
1301
  underneath decides everything else.
@@ -1087,7 +1312,7 @@ underneath decides everything else.
1087
1312
 
1088
1313
  | prop | default | meaning |
1089
1314
  |---|---|---|
1090
- | `path` | `[]` | centerline `[[x, z], …]` in NODE-LOCAL meters, Catmull-Rom smoothed (2+ points) |
1315
+ | `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
1316
  | `width` | `6` | channel width in meters |
1092
1317
  | `widths` | `[]` | width profile lerped source→mouth (e.g. `[2.5, 5, 9]`); empty = constant `width` |
1093
1318
  | `depth` | `0.8` | water column at the centerline — also the scale foam thresholds read as "shallow" |
@@ -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