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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: incanto-gameplay-behaviors
3
- description: The batteries-included game-logic library (incanto/gameplay) — ready-made Behaviors you wire from scene JSON with ZERO custom code. State & interaction: Health (hit points, i-frames, regen), Lifetime (self-destruct), ScoreKeeper (score/lives/win-lose hub), Pickup (collectibles), Collector (collect-N tally), DamageOnContact (projectiles/hazards), Interactable (press-to-use doors/levers/NPCs), Checkpoint (move where a Respawn puts you back), Turret (shoot the nearest enemy in range). Movement, AI, camera, spawning & juice: FollowCamera (camera tracks a target), Patrol (waypoint paths), Chase (homing enemy AI), Wander (seeded roaming), ZombieAI (wander-then-charge monster AI with proximity aggro), MoveTo (eased position tween), Oscillate (sine float/spin/pulse), Spawner (interval spawning), WaveSpawner (sequenced enemy waves), Projectile (straight-line motion), Buoyancy (float/bob/ride the swell on a Water3D). Use when a game needs health, score, pickups, damage, timers, interaction, enemy movement/AI, camera-follow, tweens, spawning, rafts/boats/floating objects — BEFORE hand-rolling them as custom behaviors.
3
+ description: The batteries-included game-logic library (incanto/gameplay) — ready-made Behaviors you wire from scene JSON with ZERO custom code. State & interaction: Health (hit points, i-frames, regen), Lifetime (self-destruct), ScoreKeeper (score/lives/win-lose hub), Pickup (collectibles), Collector (collect-N tally), DamageOnContact (projectiles/hazards), Interactable (press-to-use doors/levers/NPCs), Checkpoint (move where a Respawn puts you back), Turret (shoot the nearest enemy in range). Movement, AI, camera, spawning & juice: FollowCamera (camera tracks a target), ChaseCamera (behind a vehicle, turning with it), Patrol (waypoint paths), Chase (homing enemy AI), Wander (seeded roaming), ZombieAI (wander-then-charge monster AI with proximity aggro), MoveTo (eased position tween), Oscillate (sine float/spin/pulse), Spawner (interval spawning), WaveSpawner (sequenced enemy waves), Projectile (straight-line motion), Buoyancy (float/bob/ride the swell on a Water3D), Shatter (break a crate into pieces that fly and vanish), Carry (pick up, carry, drop and throw a body), Mount (get on a horse or into a car and drive it; off beside it), Sight (a watcher's eyes — cone, range, line of sight, suspicion; spotted/lost). Use when a game needs health, score, pickups, damage, timers, interaction, enemy movement/AI, camera-follow, tweens, spawning, rafts/boats/floating objects — BEFORE hand-rolling them as custom behaviors.
4
4
  ---
5
5
 
6
6
  # Gameplay behaviors — the built-in logic library
@@ -94,6 +94,43 @@ a connection `to` the root is `/root` (or `.`), never `/root/Game`.
94
94
 
95
95
  ---
96
96
 
97
+ ## Phases
98
+
99
+ A boss that changes at two thirds and again at a third — the last common wiring
100
+ that needed TypeScript.
101
+
102
+ ```json
103
+ { "name": "Boss", "type": "CharacterBody3D", "uid": "n_…",
104
+ "script": { "name": "Health", "props": { "max": 600 } },
105
+ "children": [
106
+ { "name": "Mood", "type": "Node3D", "uid": "n_…",
107
+ "script": { "name": "Phases", "props": { "at": [0.6, 0.3] } } }
108
+ ] }
109
+ ```
110
+ ```json
111
+ { "signal": "phase2", "from": "Boss/Mood", "to": "Adds", "handler": "start" },
112
+ { "signal": "phase2", "from": "Boss/Mood", "to": "HUD/Banner", "handler": "show",
113
+ "args": ["THE CINDERS CATCH", { "color": "#ffb454", "seconds": 1.6 }] },
114
+ { "signal": "phaseChanged", "from": "Boss/Mood", "to": "HUD", "handler": "onPhase" }
115
+ ```
116
+
117
+ | prop | default | what it does |
118
+ |---|---|---|
119
+ | `at` | `[]` | the health FRACTIONS the phases begin at, highest first. `[0.66, 0.33]` is three phases |
120
+ | `health` | `""` | whose health to watch; empty finds the nearest `Health` — this node, its subtree, then its ancestors — so a `Phases` under the boss needs no path |
121
+
122
+ - Signals: `phaseChanged(n)` for a HUD, and `phase2`…`phase5` for the wiring.
123
+ The numbered ones exist because a connection cannot filter on a NUMBER, so
124
+ "phase two starts the adds" has to be a line in the scene file rather than a
125
+ switch in TypeScript.
126
+ - **It never goes backwards.** Healing a boss does not give its phase back, and
127
+ a single huge hit still announces every phase it passed THROUGH — so the
128
+ wiring for phase two runs even when the fight went from full to a tenth in
129
+ one blow.
130
+ - `phase` is the readback, and it survives a save.
131
+ - What a phase DOES is still your game's: `Phases` owns the thresholds and
132
+ nothing else. Past five phases, wire `phaseChanged` and switch on the number.
133
+
97
134
  ## Checkpoint
98
135
 
99
136
  On an `Area3D`/`Area2D`, pointing at a `Respawn`: touch it and the next fall
@@ -101,10 +138,11 @@ puts you back HERE. Documented with `Respawn` in `incanto-behaviors-and-scripts.
101
138
 
102
139
  ## Health
103
140
 
104
- Hit points with regen and post-hit invulnerability (i-frames). The universal
105
- "can be hurt / can die" behavior. Hurt it via `damage(n)` (e.g. from
106
- `DamageOnContact`); heal/kill via `heal(n)` / `kill()`; bring it back with
107
- `revive(hp?)`. Clamps to `0..max`.
141
+ Hit points with regen, post-hit invulnerability (i-frames), and the hit
142
+ REACTION — the shove and the stagger. The universal "can be hurt / can die"
143
+ behavior. Hurt it via `damage(n, from?)` (e.g. from `DamageOnContact`, which
144
+ passes where it hit from); heal/kill via `heal(n)` / `kill()`; bring it back
145
+ with `revive(hp?)`. Clamps to `0..max`.
108
146
 
109
147
  | Prop | Default | Meaning |
110
148
  |---|---|---|
@@ -113,11 +151,77 @@ Hit points with regen and post-hit invulnerability (i-frames). The universal
113
151
  | `invulnerableFor` | `0` | seconds of immunity AFTER a hit (i-frames; 0 = off). Immunity BEFORE one — a dodge roll, a spawn, a cutscene — is `protect(seconds)`: it opens the window now, extends one already open and never shortens it |
114
152
  | `freeOnDeath` | `false` | `queueFree()` this node when it dies (clone-safe) |
115
153
  | `freeParent` | `false` | with `freeOnDeath`, free the PARENT — the enemy, when `Health` rides a child node |
154
+ | `knockback` | `0` | units/s the struck body is shoved AWAY from the hit's source, decaying to nothing over `knockbackSeconds` (so it travels about `knockback × knockbackSeconds / 2`). Needs a source: `damage(n, from)` |
155
+ | `knockbackSeconds` | `0.15` | how long the shove lasts |
156
+ | `knockUp` | `0` | the vertical part of the shove — a hop. 3D `+y` is up; in 2D pass the same positive number, the engine flips it to `−y` |
157
+ | `staggerSeconds` | `0` | how long the body's movers and controllers ignore their inputs after a hit — a brute mid-swing is interrupted, a player loses the stick |
158
+ | `applyKnockback` | `true` | who moves the body. `true`: Health does (a `CharacterController2D/3D` under the body is detected and applies it itself). `false`: YOUR behaviour drives the body every frame and reads the kick — see "Hit reactions" |
116
159
 
117
160
  Signals: `damaged(amount, current)` · `healed(amount, current)` ·
118
- `healthChanged(current, max)` · `died` · `revived(current)`
119
- Methods: `damage(n)` · `heal(n)` · `kill()` · `revive(hp?)` · `protect(seconds)` — state: `current`,
120
- `isDead`
161
+ `healthChanged(current, max)` · `died` · `revived(current)` · `staggerEnded`
162
+ Methods: `damage(n, from?)` · `heal(n)` · `kill()` · `revive(hp?)` · `protect(seconds)` — state: `current`,
163
+ `isDead`, `invulnerableRemaining`, `staggered`, `knocked`, `kickVelocity`, `hitDirection`
164
+
165
+ ### Hit reactions: a hit that MOVES something and STOPS something
166
+
167
+ Five shipped games wrote the same thirty lines: a `KNOCK_X`, a `CONTROL_LOCK`,
168
+ a position nudged along the blow, a `WeakMap` counting flinches. Every one was
169
+ a hit the ENGINE saw and did nothing about — `damage(n)` took the points and
170
+ the body stood exactly where it was. Now the reaction is three props on the
171
+ Health, and the direction comes from the hit itself:
172
+
173
+ ```json
174
+ { "name": "Brute", "type": "CharacterBody3D",
175
+ "script": { "name": "Health",
176
+ "props": { "max": 50, "knockback": 4, "staggerSeconds": 0.4 } },
177
+ "children": [
178
+ { "name": "AI", "type": "Node3D",
179
+ "script": { "name": "Chase", "props": { "target": "%Player", "moveParent": true } } }
180
+ ] }
181
+ ```
182
+
183
+ - **The source is the direction.** `DamageOnContact` and `Turret` pass their
184
+ own world position; your code passes whatever hit — `health.damage(25,
185
+ swordTip)`. The body is shoved AWAY from that point on the ground plane
186
+ (3D x/z, 2D x/y); `damage(n)` with no source takes the points and moves
187
+ nothing, exactly as before. A source ON the body (zero distance) moves
188
+ nothing either — put the hitter's position in, not the victim's.
189
+ - **Stagger holds every built-in mover.** `Chase`, `Patrol`, `PathFollow`,
190
+ `MoveTo`, `Wander`, `ZombieAI` all move through one door, and that door says
191
+ "not now" while `staggered` is true — so the Brute above stops mid-charge for
192
+ 0.4 s and its `Chase` resumes on its own. Your own mover can ask the same
193
+ question: `isStaggered(node)` is not exported, but `findHealth`-style lookup
194
+ is one line — the Health on the node, its children, or its ancestors — and
195
+ `health.staggered` is the answer.
196
+ - **The player's controller honours it too.** `CharacterController2D` and
197
+ `CharacterController3D` drop the stick, the jump and the dash while the
198
+ body's Health is staggered, and write the kick as the body's velocity
199
+ (`knockUp` becomes the hop). `Health` may sit on the body or on a child
200
+ (`Vitals`, when the body's own script slot is taken); the controller finds it
201
+ either way.
202
+ - **Your own controller reads the kick.** A behaviour that writes the body's
203
+ velocity every frame (the platformer's `PlayerController` does) sets
204
+ `"applyKnockback": false` and reads it where it writes velocity — otherwise
205
+ two writers fight over one body:
206
+
207
+ ```ts
208
+ const kick = health.kickVelocity; // [] when not knocked, else decaying
209
+ if (kick.length > 0) vx = kick[0] ?? vx;
210
+ const lift = health.takeLift(); // knockUp, ONCE — then gravity owns it
211
+ if (lift !== 0) vy = lift;
212
+ if (!health.staggered) vx = stick.x * RUN_SPEED;
213
+ ```
214
+
215
+ - **A dynamic `RigidBody3D`/`2D` gets a velocity write; a kinematic body
216
+ slides** (`moveAndSlide`, so a knockback never throws anyone through a
217
+ wall); a plain node is nudged. `revive()` clears a kick and stagger still
218
+ running, so a respawn does not arrive flying backwards from the blow that
219
+ killed it.
220
+ - Sized from the games that measured it: a sword on a brute `knockback 4`
221
+ (0.3 m), a club on the player `knockback 7, knockUp 2.5` (0.5 m back and a
222
+ hop), a bolt on a 2D goblin `knockback 240` px/s (18 px), a goblin on a 2D
223
+ knight `knockback 420, knockbackSeconds 0.22, knockUp 360, staggerSeconds
224
+ 0.22` (30 px against a held stick, 33 px up).
121
225
 
122
226
  **A dead Health STAYS dead until you revive it.** `damage`, `heal` and regen all
123
227
  no-op once it has died — which is what makes a lives system work at all, and
@@ -283,7 +387,9 @@ Signals: `totalChanged(total)` — Methods: `collect(value)` — state: `total`
283
387
 
284
388
  Deals damage to whatever it touches — projectiles, spikes, lava, enemy hitboxes.
285
389
  **Goes on an Area.** On `triggerEnter`, finds the contacted entity's `Health` and
286
- calls `damage(amount)`, then emits `dealtDamage(amount, healthOwnerNode)`.
390
+ calls `damage(amount, from)` `from` being this Area's own world position, so a
391
+ target with `Health.knockback` is shoved away from the hitbox that struck it —
392
+ then emits `dealtDamage(amount, healthOwnerNode)`.
287
393
 
288
394
  | Prop | Default | Meaning |
289
395
  |---|---|---|
@@ -471,6 +577,7 @@ author markers). Emits `reachedPoint(index)` on arrival.
471
577
  | `mode` | `"loop"` | `'loop'` wraps · `'pingpong'` reverses at the ends |
472
578
  | `pauseAt` | `0` | seconds to wait at each reached point |
473
579
  | `moveParent` | `false` | move the PARENT node instead of this one |
580
+ | `requireSight` | `false` | a static-only ray from half a metre over the mover to half a metre over the target must reach it — a wall or a crate between means no target, and the hound waits instead of walking into the wall forever. `chasing` reads the node being chased this frame, or null |
474
581
  | `ground` | `null` | chase along the GROUND — the target's height is not a direction. `null` = yes for a `CharacterBody3D` mover, no for anything else; `true`/`false` overrides |
475
582
  | `facePath` | `""` | node path to TURN toward the direction of travel (e.g. `"../Skin"`) |
476
583
  | `turnSpeed` | `100` | turn rate rad/s; 100 is an instant snap |
@@ -500,6 +607,66 @@ body puts `Patrol` on a CHILD. Without it a patrolling body had to carry
500
607
  `Patrol` itself and could hold nothing else — a patrolling enemy you could not
501
608
  hurt.
502
609
 
610
+ ## ChaseCamera
611
+
612
+ A camera BEHIND something that turns — a car, a boat, a tank, a horse. `FollowCamera`
613
+ keeps a fixed WORLD offset, which is right for a top-down or side view and wrong for a
614
+ vehicle: the offset has to swing round with the target's heading, and lazily, or
615
+ every corner snaps the view. Four examples (a van, a boat, a car, a race car) wrote
616
+ this by hand before it existed. Aim the camera with `Camera3D.lookAt`; this places it.
617
+
618
+ | Prop | Default | Meaning |
619
+ |---|---|---|
620
+ | `target` | `""` | the node to sit behind (required) |
621
+ | `back` | `8` | how far behind, along the target's heading (m or px) |
622
+ | `up` | `3` | how far above it (3D) |
623
+ | `smoothing` | `0.85` | 0..1 retention per 60 Hz frame of the followed heading AND position — 0 rigid, 0.9 lazy; a chase camera switched on across the map flies in instead of snapping (`snap()` places it at once) |
624
+
625
+ ```json
626
+ { "name": "Camera", "type": "Camera3D", "props": { "current": true, "lookAt": "/root/Car" },
627
+ "script": { "name": "ChaseCamera", "props": { "target": "/root/Car", "back": 9, "up": 4 } } }
628
+ ```
629
+
630
+ The heading is read from the target's rotation BASIS, not from `rotation[1]`: a tilted
631
+ hull past 90° of yaw spells its Euler XYZ triple `[180, 180−yaw, 180]`, and a camera that
632
+ read the middle number swung to the wrong side. `heading` (radians) is readable for a
633
+ harness; `enabled: false` parks it (an errand game turns it on when the player boards).
634
+
635
+ ## GroupCamera
636
+
637
+ One camera, several players — the couch co-op camera. `FollowCamera` follows ONE
638
+ target and `ChaseCamera` rides behind ONE character, so a game with two players
639
+ at the same keyboard had nothing that framed a PAIR: whoever the camera was on
640
+ could walk the other player off the screen, and their half of the game went with
641
+ them.
642
+
643
+ ```json
644
+ { "name": "Camera", "type": "Camera3D", "props": { "current": true, "fov": 48 },
645
+ "script": { "name": "GroupCamera",
646
+ "props": { "targets": "player", "offset": [0, 15, 11],
647
+ "minZoom": 0.9, "maxZoom": 2.1, "smoothing": 0.18 } } }
648
+ ```
649
+
650
+ | Prop | Default | Meaning |
651
+ |---|---|---|
652
+ | `targets` | `"player"` | the GROUP whose members are framed — members joining and leaving are picked up as they come |
653
+ | `offset` | `[0, 12, 14]` | where the camera sits relative to the group's centre, at zoom 1 |
654
+ | `minZoom` / `maxZoom` | `1` / `2.5` | how far that offset may shrink and stretch to fit everyone |
655
+ | `padding` | `0.25` | fraction of the view kept clear around the furthest pair |
656
+ | `smoothing` | `0.15` | easing; `0` snaps |
657
+ | `lookAtGroup` | `true` | keep aiming at the centre (a camera that only moves goes cross-eyed) |
658
+
659
+ State: `zoom`, `centre`. Methods: `snap()` — after a teleport or a scene swap.
660
+
661
+ **Take the characters off camera duty.** `CharacterController3D` drives the
662
+ scene's current camera in every view it has, so every player's controller needs
663
+ `"camera": "none"` or they fight this behaviour (and each other) for the same
664
+ camera, every frame. `incanto-check` says so when it sees both.
665
+
666
+ Measured on `examples/coop-3d`: shoulder to shoulder the camera sits 11.8 m up;
667
+ sent to opposite corners it climbs to 29.3 m, and `describeFraming` reports both
668
+ players still on screen.
669
+
503
670
  ## Chase
504
671
 
505
672
  Homing enemy AI: each frame moves toward `target.position` at `speed`, stopping
@@ -643,6 +810,56 @@ is a far worse failure than one running when it was meant to sleep.
643
810
 
644
811
  From TypeScript it is the same switch: `this.getNode('../Hunt').behavior.enable()`.
645
812
 
813
+ ## Sight (eyes for a watcher)
814
+
815
+ Five examples cast their own sightline — a cone, a range, a static-only ray
816
+ from eye to chest — and `Chase` and `Turret` cast none: an enemy chased you
817
+ through a wall and a turret shot you through one. `Sight` is the built-in
818
+ eye, on a child of the watcher's body:
819
+
820
+ ```json
821
+ { "name": "Guard", "type": "CharacterBody3D", "children": [
822
+ { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$base" } },
823
+ { "name": "Eyes", "type": "Node",
824
+ "script": { "name": "Sight", "props": { "range": 12, "coneDeg": 70, "fillSeconds": 1 } } },
825
+ { "name": "Hunt", "type": "Node",
826
+ "script": { "name": "Chase", "props": { "target": "%Player", "enabled": false } } }
827
+ ] }
828
+ ```
829
+ ```jsonc
830
+ { "signal": "spotted", "from": "Guard/Eyes", "to": "Guard/Hunt", "handler": "enable" },
831
+ { "signal": "lost", "from": "Guard/Eyes", "to": "Guard/Hunt", "handler": "disable" }
832
+ ```
833
+
834
+ | Prop | Default | Meaning |
835
+ |---|---|---|
836
+ | `range` | `12` | how far it sees (m) |
837
+ | `coneDeg` | `70` | the FULL cone angle; `360` = all round |
838
+ | `targetGroup` | `player` | only nodes in this group are seen |
839
+ | `facingPath` | `../Skin` | the node whose yaw is the look direction (+Z-forward — the Skin the controller turns); `''` = the body's own rotation |
840
+ | `eyeHeight` / `chestHeight` | `0.6` / `0.3` | where the ray starts above the watcher's position, and where it aims above the target's |
841
+ | `fillSeconds` | `0` | seconds of being seen before `spotted`; `0` = the first frame. Draining back takes as long |
842
+ | `los` | `true` | a STATIC-only ray from eye to chest must reach the target — walls and crates hide you, other guards do not |
843
+
844
+ Signals: `spotted(node)` when `suspicion` reaches 1, `lost(node)` when it
845
+ drains to 0 — once each, an edge, so a connection can flip a state and a
846
+ game can hang an alarm on it. Readbacks any frame: `sees(node)`, `seen()`
847
+ (nearest first), `target` (the nearest), `suspicion` (0..1 — a HUD's
848
+ "detection" bar is this number).
849
+
850
+ In 2D it is the same eye on the x/y plane: the watcher faces along its
851
+ scalar `rotation` (0 = +x, positive clockwise on the y-down screen) or the
852
+ node at `facingPath`, `eyeHeight`/`chestHeight` are ignored, and the ray is
853
+ the 2D adapter's.
854
+
855
+ It runs headless — the ray is the physics adapter's — so a harness can stand
856
+ the player behind a crate and ask `sees()`. `examples/stealth-3d`'s guards
857
+ see with it, and the enemy that changes its mind above is now three
858
+ connections and no code. `Chase` and `Turret` cast the same ray themselves
859
+ with `requireSight: true` — a hound behind a wall waits, a turret holds its
860
+ fire — so a simple enemy needs no `Sight` at all, and a guard with a
861
+ detection meter wants both.
862
+
646
863
  ## Turret
647
864
 
648
865
  The oldest verb in games: the nearest live member of `targetGroup` within
@@ -660,14 +877,67 @@ target and `Projectile` for the bullet, and nothing that decides to shoot.
660
877
  |---|---|---|
661
878
  | `targetGroup` | `"enemy"` | who it shoots — the nearest LIVE member (a dead `Health` is skipped) |
662
879
  | `range` | `6` | reach, in the scene's units |
880
+ | `requireSight` | `false` | a static-only ray from the turret to the target must reach it — no firing through walls |
663
881
  | `damage` | `10` | hitscan damage per shot (`target.behavior.damage(n)`) |
664
882
  | `cooldown` | `0.6` | seconds between shots |
665
883
  | `shelf` + `prefab` | `""` | fire a CLONE instead: a `PrefabShelf` and a template on it; each shot lands at the turret + `muzzle`, its `Projectile` pointed at the target, under `into` (default: the turret's parent). The clone does the hurting then — give it `DamageOnContact` and a `Lifetime` |
884
+ | `muzzle` | `[]` | where clones start, relative to the turret — **in the `facePath` node's frame** when there is one, so the tip of a barrel stays the tip of the barrel as it turns |
666
885
  | `facePath` / `turnSpeed` | `""` / `100` | a barrel to turn toward the target |
886
+ | `arc` | `"flat"` | `"low"` / `"high"` LOB a falling shell — see below |
667
887
 
668
888
  Signals: `acquired(target)` · `fired(target)` · `lost()`. Getters: `target`,
669
889
  `shots`. Dimension-agnostic — a 2D tower is the same node with `[x, y]`.
670
890
 
891
+ ### Lobbing: a shot that has to come DOWN on something
892
+
893
+ A `prefab` with `Projectile.gravity` aimed STRAIGHT at its target is a shell
894
+ that falls out of the sky on the way. Measured on the shipped tank game, whose
895
+ enemy guns fire a 22 m/s shell with `gravity: 9.81`: at 28 m — comfortably
896
+ inside their own 32 m `range` — every shell passed **6.5 m under the tank** and
897
+ kept going down to −1100 m. They could only ever hit at point-blank range, and
898
+ nothing said so.
899
+
900
+ ```json
901
+ { "name": "Gun", "type": "Node3D",
902
+ "script": { "name": "Turret",
903
+ "props": { "targetGroup": "player", "range": 150, "cooldown": 4.5,
904
+ "shelf": "/root/Shelf", "prefab": "Shell", "into": "/root/Shots",
905
+ "facePath": "../Barrel", "muzzle": [0, 0, 2.6], "arc": "low" } } }
906
+ ```
907
+
908
+ - `"low"` is the flat, fast arc; `"high"` is the mortar's — the SAME landing
909
+ point, a much taller flight, and the one that clears a wall in between.
910
+ - The angle is solved from the shell's own `speed` and `gravity` (read off the
911
+ template on the shelf, so the numbers can never disagree), from the MUZZLE,
912
+ which is where the shell actually starts.
913
+ - The `facePath` barrel is ELEVATED to match — `faceTravel` only ever wrote yaw,
914
+ so a lobbing gun used to point flat while its shells climbed.
915
+ - Out of reach it fires at 45°, which is as far as that shell can go: a gun that
916
+ falls short in front of you is honest, a gun that silently holds fire is not.
917
+ - `arc` on a prefab whose `Projectile` has no `gravity` is a hard error — there
918
+ is nothing to arc against, and a silent no-op would be a gun that looks aimed.
919
+
920
+ The same arithmetic is yours to use directly — an aiming line, a grenade toss, a
921
+ catapult, a basketball:
922
+
923
+ ```ts
924
+ import { ballisticAim, ballisticPath, maxRangeAim } from 'incanto/gameplay';
925
+
926
+ const dir = ballisticAim(from, target, speed, 9.81, 'high'); // null = out of reach
927
+ const path = ballisticPath(from, dir ?? aim, speed, 9.81, {
928
+ seconds: 14,
929
+ stop: (at) => (at[1] ?? 0) <= groundHeightAt(at[0] ?? 0, at[2] ?? 0),
930
+ });
931
+ ```
932
+
933
+ **Draw the preview with `ballisticPath`, not with a parabola of your own.**
934
+ `Projectile` integrates step by step (the fall accumulated first), and a
935
+ closed-form curve drifts from it — measured at **ten metres** over an
936
+ eight-second flight, which is the difference between a hit and a crater in the
937
+ wrong field. `ballisticPath` walks exactly what the shell will fly; the beads
938
+ and the crater then land on the same spot (0.1 m apart over 120 m in
939
+ `examples/artillery-3d`).
940
+
671
941
  ## FaceTarget
672
942
 
673
943
  Turn toward the nearest thing in a group and say when you are pointed at it —
@@ -709,8 +979,9 @@ every shop, build mode, upgrade tree and economy game wrote its own wallet.
709
979
  | `amount` | `0` | what you start with |
710
980
  | `max` | `0` | ceiling (0 = none) |
711
981
 
712
- Signals: `changed(amount)` · `earned(gain)` · `spent(cost)` · **`refused(cost)`**
982
+ Signals: `changed(amount)` · `earned(taken)` · `spent(cost)` · **`refused(amount)`**
713
983
  Methods: `spend(cost) -> boolean` · `earn(gain)` · `canAfford(cost)` · `setAmount(n)`
984
+ State: `room` (how much more it can hold; `Infinity` with no ceiling)
714
985
 
715
986
  ```ts
716
987
  if (!wallet.spend(TOWER_COST)) return; // `refused` already fired for the HUD
@@ -721,6 +992,13 @@ placeTower(pad);
721
992
  "you cannot afford that" is one wire to a banner. It saves through `serialize()`
722
993
  like `Health` and `ScoreKeeper`.
723
994
 
995
+ **A CEILING refuses too.** `earn` takes what fits, emits `earned` with what it
996
+ actually TOOK, and emits `refused` with the remainder — so a pickup that will
997
+ not fit can stay on the floor. This clamped silently and said `earned(gain)`
998
+ anyway: measured on a game whose purse holds four, the fifth ember vanished off
999
+ the ground, the HUD read `4 / 4`, and nothing anywhere had the ember. `room` is
1000
+ the question to ask before offering.
1001
+
724
1002
  A NON-POSITIVE amount is ignored, the way `Health.damage`/`heal` have always
725
1003
  ignored one — and a negative is reported, because it is a sign flip in the
726
1004
  caller rather than an offer. `spend(-5)` used to PAY you five (any negative
@@ -829,6 +1107,58 @@ disconnect, or pass `{ owner: someNode }`.)
829
1107
  Same `ZombieSkin` is the place to also FACE the heading and GROUND the body each frame
830
1108
  (see incanto-3d-character.md "+Z-FORWARD rule" and incanto-3d-models.md "Grounding").
831
1109
 
1110
+ ## Mount
1111
+
1112
+ Get on something and drive it; get off beside it. A horse, a car, a boat, a
1113
+ turret seat: the rider's controller lets go, the rider's body PARKS (collider
1114
+ off, hidden, weightless, riding along so everything that targets the rider
1115
+ follows), the rider's `skin` moves onto the steed's `saddle`, and the steed's
1116
+ `drive` — a `CharacterController3D` or a `Vehicle3D` — is enabled. `dismount()`
1117
+ puts everything back beside the steed, on its right, and hands the camera yaw
1118
+ back. Two examples (`errands-3d`'s car, `steed-3d`'s horse) wrote these sixty
1119
+ lines before it existed, and each forgot something the other remembered.
1120
+
1121
+ | Prop | Default | Meaning |
1122
+ |---|---|---|
1123
+ | `rider` | `""` | the player's body (required) |
1124
+ | `steed` | `".."` | the thing ridden — this node's parent |
1125
+ | `drive` | `""` | the steed's controller / vehicle whose `enabled` flips (default: the first child that has one) |
1126
+ | `walk` | `""` | the rider's controller whose `enabled` flips (default: the first child that has one) |
1127
+ | `skin` | `"Skin"` | a child of the rider carried onto the saddle (`""` = nothing moves) |
1128
+ | `saddle` | `""` | where it sits while mounted — a node under the steed (`""` = the steed itself) |
1129
+ | `skinOnSaddle` / `skinOnRider` | `[0,0,0]` | the skin's local position there, and back |
1130
+ | `dismountAt` | `[1.8, 0.3, 0]` | where the rider stands afterwards, in the steed's frame (+x = its right) |
1131
+
1132
+ Methods: `mount()`, `dismount()`, `toggle()` — signals `mounted(steed)`, `dismounted(steed)`;
1133
+ `mounted` reads back.
1134
+
1135
+ ```json
1136
+ { "name": "Horse", "type": "RigidBody3D", "props": { "fixedRotation": true, "friction": 0, "mass": 400,
1137
+ "collider": { "shape": "capsule", "radius": 0.7, "height": 1 } },
1138
+ "children": [
1139
+ { "name": "Ride", "type": "CharacterController3D", "props": { "enabled": false, "maxSpeed": 7, "jumpVelocity": 6.5 } },
1140
+ { "name": "Skin", "type": "Node3D", "children": [ { "name": "Saddle", "type": "Node3D", "props": { "position": [0, 1.7, -0.15] } } ] },
1141
+ { "name": "Whistle", "type": "Node3D", "script": { "name": "Interactable", "props": { "range": 3.2, "prompt": "ride" } } },
1142
+ { "name": "Reins", "type": "Node", "script": { "name": "Mount",
1143
+ "props": { "rider": "/root/Player", "saddle": "../Skin/Saddle", "skinOnRider": [0, -0.85, 0] } } }
1144
+ ] }
1145
+ ```
1146
+ ```json
1147
+ { "signal": "interacted", "from": "Horse/Whistle", "to": "Horse/Reins", "handler": "toggle" }
1148
+ ```
1149
+
1150
+ - **Keep the steed OUT of the Interactable's `actorGroup`.** A horse that is in
1151
+ `player` (so the wolves' `DamageOnContact` can bite it) is its own nearest
1152
+ actor, at distance 0, and its prompt is focused from anywhere. Give the bites
1153
+ their own group (`prey`: the player and the horse) and leave `player` to the
1154
+ walker.
1155
+ - Two `CharacterController3D`s in one scene is what this is; `incanto-check`
1156
+ used to warn that both drive the same camera — it skips one authored
1157
+ `enabled: false` now.
1158
+ - The parked rider rides along every frame, so a `Chase` aimed at the player
1159
+ follows the horse, and a bite that reaches the horse's body hurts the HORSE
1160
+ (its own `Health`), not the parked rider.
1161
+
832
1162
  ## MoveTo
833
1163
 
834
1164
  Eased position tween from the start to a fixed `to` over `duration` seconds —
@@ -842,7 +1172,9 @@ opening doors, sliding platforms, scripted moves. Emits `arrived` once.
842
1172
  | `ease` | `"easeInOut"` | one of `linear` · `easeIn` · `easeOut` · `easeInOut` |
843
1173
  | `startOnSignal` | `false` | defer until `start()` (default: begin at ready) |
844
1174
 
845
- Signals: `arrived` — Methods: `start()`
1175
+ Signals: `arrived` — Methods: `start()`, `finish()` (land on `to` NOW and say
1176
+ `arrived` once — a skipped cutscene must leave the door it opens OPEN; wire
1177
+ `skipped → Door.finish`).
846
1178
 
847
1179
  ```json
848
1180
  { "name": "Platform", "type": "Node2D", "props": { "position": [0, 100] },
@@ -873,6 +1205,168 @@ cuts, a swinging gate blocks. (Before 0.62 it turned only the mesh: a body's
873
1205
  angle was applied once at creation and never followed again, so every spinner
874
1206
  built this way was decoration.)
875
1207
 
1208
+ ## Blast
1209
+
1210
+ Hurt everything NEAR a point — a grenade, a rocket, a barrel, a keg, a boss's
1211
+ ground slam. `DamageOnContact` is a HITBOX (it hurts what touches it) and an
1212
+ explosion is not one, so every explosive game wrote the radius query, the
1213
+ falloff and the direction of the shove itself.
1214
+
1215
+ Put it on a CHILD of the thing that explodes, the way `Shatter` sits under what
1216
+ breaks. A keg with a fuse is then four built-ins and NO TypeScript:
1217
+
1218
+ ```json
1219
+ { "name": "Keg", "type": "Node3D", "groups": ["keg"],
1220
+ "script": { "name": "Lifetime", "props": { "seconds": 2.4 } },
1221
+ "children": [
1222
+ { "name": "Vitals", "type": "Node3D",
1223
+ "script": { "name": "Health",
1224
+ "props": { "max": 1, "freeOnDeath": true, "freeParent": true } } },
1225
+ { "name": "Boom", "type": "Node3D",
1226
+ "script": { "name": "Blast",
1227
+ "props": { "when": "expired,died", "radius": 4.6, "damage": 90,
1228
+ "groups": "crate,enemy,player,keg" } } },
1229
+ { "name": "Bits", "type": "Node3D",
1230
+ "script": { "name": "Shatter", "props": { "when": "expired", "pieces": 10 } } }
1231
+ ] }
1232
+ ```
1233
+
1234
+ | Prop | Default | Meaning |
1235
+ |---|---|---|
1236
+ | `radius` | `4` | how far it reaches, in the scene's units |
1237
+ | `damage` | `40` | damage at the CENTRE |
1238
+ | `falloff` | `"linear"` | fades to nothing at the rim; `"none"` hits everything inside for the full `damage` (what a grid game wants) |
1239
+ | `groups` | `""` | comma-separated groups to hurt; empty hurts EVERYTHING with a `Health` in reach, the player included — an explosion does not check whose side you are on |
1240
+ | `when` | `"died"` | comma-separated signals on the PARENT that set it off (`Lifetime` emits `expired`, `Health` emits `died`); `""` = only `blast()` |
1241
+ | `self` | `false` | whether the carrier's own `Health` is in the blast (a mine that must destroy itself says `true`) |
1242
+ | `throughWalls` | `true` | `false` casts a static-only ray at each victim, so a pillar shields what is behind it |
1243
+
1244
+ Signals: `blasted(count)`. Methods: `blast()` (returns how many it hurt) —
1245
+ state: `spent`.
1246
+
1247
+ - **A victim is measured where its BODY is**, not where its `Health` node sits.
1248
+ A health hangs on a child with no transform of its own, so the blast walks up
1249
+ to the nearest thing with a position. It used to ask every node in the tree
1250
+ "is there a health under you", which the scene ROOT always answers — with the
1251
+ shallowest health in the whole game, measured at the root's own origin, and
1252
+ then marked as already considered. Every scene lost exactly one victim to its
1253
+ own root; measured on a keep game, a firepot at the player's feet reported
1254
+ **0 hits**.
1255
+ - **The damage carries the blast's own position**, so a `Health` with
1256
+ `knockback` is thrown AWAY from the crater without anyone writing a
1257
+ direction. Measured: a keg 1.2 m away throws the player 1.1 m.
1258
+ - **A chain reaction is not a feature.** A keg's own `Health` is one of the
1259
+ things a blast can damage, and `when: "expired,died"` means "my fuse ran out,
1260
+ OR someone else's blast killed me". `examples/bomb-3d` codes no chain.
1261
+ - **It goes off once.** A keg caught in two blasts explodes once, and `spent`
1262
+ says so.
1263
+ - `throughWalls: false` knows two things a line-of-sight test has to know about
1264
+ a BOMB: the floor it is lying on is not a wall (a ray from y=0 on a surface at
1265
+ y=0 reports a hit at zero distance — measured, `0.00 m → /Floor`), and a
1266
+ crate solid enough to walk into is not a wall between the blast and ITSELF.
1267
+
1268
+ ## Shatter
1269
+
1270
+ Break a thing into pieces that fly, tumble and vanish. Every game with a
1271
+ crate wants it to burst — pots, barrels, a wall of boxes, the rock you
1272
+ blast — and `Health` + `freeOnDeath` only made a crate VANISH. Put `Shatter`
1273
+ on a child of the breakable body (the way `Respawn` sits under the player),
1274
+ or on the body itself:
1275
+
1276
+ ```json
1277
+ { "name": "Crate", "type": "RigidBody3D", "groups": ["breakable"],
1278
+ "props": { "mass": 8, "collider": { "shape": "box", "size": [1, 1, 1] } },
1279
+ "script": { "name": "Health", "props": { "max": 1 } },
1280
+ "children": [
1281
+ { "name": "Mesh", "type": "MeshInstance3D",
1282
+ "props": { "material": { "color": "#a0703a", "roughness": 0.9 }, "castShadow": true } },
1283
+ { "name": "Shards", "type": "Node3D", "script": { "name": "Shatter", "props": { "pieces": 8 } } }
1284
+ ] }
1285
+ ```
1286
+
1287
+ | prop | default | what it does |
1288
+ |---|---|---|
1289
+ | `pieces` | `8` | how many fragments — a grid of smaller boxes cut from the body's mesh |
1290
+ | `impulse` | `4` | outward speed, m/s (2D: ×100 px/s) |
1291
+ | `spin` | `6` | random tumble, rad/s |
1292
+ | `lifetime` | `2.5` | seconds the pieces lie there before they are swept up; `0` = they stay |
1293
+ | `when` | `"died"` | the breakable's signal that breaks it — `Health` emits `died`; `""` = only `break()` |
1294
+ | `mesh` | `""` | the `MeshInstance3D` (2D: `ColorRect2D`/`Sprite2D`) whose size and material the pieces copy; `""` = the first one under the body |
1295
+ | `free` | `true` | free the breakable when it breaks |
1296
+
1297
+ - When the body's `died` fires (or `break()` is called), `pieces` fragments of
1298
+ the body's own mesh and material are built where it stood, each its own
1299
+ `RigidBody3D` with a box collider, flung outward with a tumble and carrying
1300
+ a `Lifetime`; the body's collider is switched off and the body freed FIRST,
1301
+ so the pieces are not born inside it. `shattered(body)` fires on the
1302
+ Shatter's node.
1303
+ - 2D bodies burst into `RigidBody2D` + `ColorRect2D` pieces of the rect's
1304
+ colour (or the sprite's `tint`).
1305
+ - Everything is drawn from `engine.rng`, so a seeded run breaks the same way
1306
+ twice — a replay shows the same tumble.
1307
+ - A `StaticBody3D` breaks too (`when: ""` and `break()` from your own trigger —
1308
+ a wall you blast, a floor that gives way).
1309
+ - Composed in `examples/smash-3d` ("Smash Depot": a bat, twelve crates, three
1310
+ barrels that take two hits and bite back, a key in one of the crates).
1311
+
1312
+ ## Carry
1313
+
1314
+ The physics hand: pick a thing up, carry it in front of you, drop it, throw
1315
+ it. "Pick up the box" is the first physics thing a player tries and the first
1316
+ thing an author asks for, and the engine had no route to it — a joint made at
1317
+ runtime hangs the crate's weight on the hover spring and the player sinks; a
1318
+ kinematic swap loses the collisions. Put `Carry` on a child of the player's
1319
+ body (the way `Respawn` sits under it) and declare the actions:
1320
+
1321
+ ```json
1322
+ { "name": "Player", "type": "RigidBody3D", "props": { "…": "…" },
1323
+ "children": [
1324
+ { "name": "Controller", "type": "CharacterController3D" },
1325
+ { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$avatar" } },
1326
+ { "name": "Hands", "type": "Node3D", "script": { "name": "Carry", "props": { "range": 2.2, "maxMass": 30 } } }
1327
+ ] }
1328
+ ```
1329
+ ```json
1330
+ "input": { "grab": { "type": "button", "keys": ["KeyE"] }, "throw": { "type": "button", "keys": ["KeyQ"] } }
1331
+ ```
1332
+
1333
+ | prop | default | what it does |
1334
+ |---|---|---|
1335
+ | `action` | `"grab"` | press: pick up the nearest carryable in reach, or drop what is held |
1336
+ | `throwAction` | `"throw"` | press: throw what is held; `""` = no throwing |
1337
+ | `range` | `2.2` | how far from the hand a thing may be to pick up (m) |
1338
+ | `group` | `"carryable"` | only bodies in this group can be carried (`RigidBody3D` only — a static thing is the world) |
1339
+ | `maxMass` | `30` | heavier than this stays where it is — you can still PUSH it |
1340
+ | `holdDistance` / `holdHeight` | `1.2` / `0.5` | where the held thing rides: ahead of the body's origin, and above it (m) |
1341
+ | `throwSpeed` | `9` | m/s forward, with a little lift |
1342
+ | `gain` | `14` | how stiffly the held thing follows the hand (1/s) |
1343
+ | `inheritMotion` | `1` | how much of the THROWER's own motion goes with the throw (0..1). A throw used to WRITE the velocity, so a docker sprinting at the rail threw exactly as far as one standing at it — and one running backwards threw just as hard forwards. `0` is that old absolute throw, for a game that wants the arcade answer |
1344
+ | `facePath` | `"../Skin"` | the node whose yaw is "ahead" (+Z-forward, as `CharacterController3D` turns the skin); `""` = the body's own rotation |
1345
+
1346
+ - A held body keeps its collider and loses its gravity: it is DRIVEN toward
1347
+ the hand every step (capped at 12 m/s), tumble damped, so a held crate
1348
+ still stops at a wall, pushes a door and rests on whatever you set it on.
1349
+ - Drop it on a shelf and it stays; drop it over a ledge and it falls; a
1350
+ crate that is freed while in hand (shattered) leaves the hand empty.
1351
+ - Signals `grabbed(body)`, `dropped(body)`, `thrown(body)`; readback `held`.
1352
+ - `throwAction: ""` is a game with no throwing, and the engine says nothing
1353
+ about it — an empty action name is an off switch everywhere in this engine,
1354
+ never a typo.
1355
+ - Stack two crates and the character climbs them like any step (the hover
1356
+ spring's `stepHeight`, or a jump) — that is the whole "stack boxes to
1357
+ reach the ledge" puzzle, with no code.
1358
+ - **Pushing is mass.** A thing over `maxMass` can still be shoved by walking
1359
+ into it — but the controller's push scales with the PLAYER's mass, and the
1360
+ default `RigidBody3D` mass is 1 kg, which shoves nothing (measured: a 1 kg
1361
+ porter against a 2 kg crate on a concrete floor, no movement). Give the
1362
+ player `mass: 70` and a walk moves a crate, a run moves a safe on casters.
1363
+ And a static floor's authored `friction` WINS the pair (the pad rule), so a
1364
+ low-friction body on a `friction: 0.9` floor is glued: leave the floor at
1365
+ its default and author the casters on the body.
1366
+ - Composed in `examples/porter-3d` ("Porter": four crates onto the truck, one
1367
+ on a shelf you need a stack to reach, a bell to throw at, a safe you can
1368
+ only push).
1369
+
876
1370
  ## Buoyancy
877
1371
 
878
1372
  Float on water. Hangs on a `RigidBody3D` and asks the water how high it is at
@@ -900,10 +1394,23 @@ wire them to a splash `Particles3D`, a sound, or a "you dropped the cargo" rule.
900
1394
  "script": { "name": "Buoyancy", "props": { "draft": 0.25, "size": [2.4, 1.4], "drift": 0.8 } } }
901
1395
  ```
902
1396
 
1397
+ The other way to float is the PROP: `RigidBody3D.buoyancy` (see "Things
1398
+ that float" in `incanto-environment.md`) is Archimedes on the collider — mass
1399
+ against volume decides, nothing is authored, and there is no `drift`. This
1400
+ behaviour is for a waterline you want to author and a swell you want the
1401
+ body to wander with; the prop is for a body whose weight should decide. One
1402
+ or the other on a body, never both.
1403
+
903
1404
  `drift` is the part players notice: a wave face is a slope, and a floating
904
1405
  thing slides down it, so a raft left alone wanders with the swell instead of
905
- bobbing on the spot. The lift is capped at a few g — water pushes back, it
906
- never launches a body that has been dragged deep.
1406
+ bobbing on the spot. The lift is capped at a few g — water pushes back — and
1407
+ **it can never launch the body**: it may not leave a body rising faster than
1408
+ gravity alone would have carried it up from the waterline, zero at the line
1409
+ itself. A 50-gram bobber cast into a pond used to ride six g up from half a
1410
+ metre under and leave the water at six metres a second, into the air and back,
1411
+ eighty-eight crossings in four seconds; every shipped float had started AT the
1412
+ surface and nothing had ever been thrown in. A cork now goes under, comes up
1413
+ and sits — `examples/fishing-3d` is the cast.
907
1414
 
908
1415
  **The lift cancels gravity AT the waterline**, so the hull holds that line
909
1416
  instead of sinking until a spring happens to match its weight. That is what
@@ -948,7 +1455,10 @@ this.node.getNode('/Game/Towers').addChild(tower);
948
1455
  |---|---|---|
949
1456
  | `prefabs` | `""` | comma-separated child names to take; empty takes them ALL |
950
1457
 
951
- Methods: `make(name)` · `names()`
1458
+ Methods: `make(name)` · `names()` · `template(name)` — the held template itself,
1459
+ to ASK it something (what `Turret.arc` reads a shell's speed and gravity off,
1460
+ rather than re-declaring those numbers where they could disagree). Never change
1461
+ what it hands back: every future clone is a copy of it.
952
1462
 
953
1463
  **Listen before it enters the tree.** A clone placed where a body already is —
954
1464
  an XP orb dropped at the player's feet — is collected on its very first physics
@@ -1133,7 +1643,12 @@ Mid-flight, writing `direction` or `speed` re-bakes immediately (the fall
1133
1643
  `gravity` has accumulated is kept — it is a fact about the world, not about the
1134
1644
  shot). What is deliberately NOT live is `rotation` after that first step: a
1135
1645
  projectile that spins for looks must not have its spin become its trajectory.
1136
- Call **`aim()`** to re-derive from the current rotation on purpose:
1646
+ Call **`aim()`** to re-derive from the current rotation on purpose — and
1647
+ **`aim(direction, carry)`** to add the SHOOTER's velocity: a shell fired from a tank
1648
+ doing 16 m/s left the muzzle at the standing tank's 30 m/s until it did, so a tank
1649
+ could outrun its own shot and a sideways shell landed behind what the barrel pointed
1650
+ at. `carry` is added once, at launch (`examples/tank-3d`'s `Gunner` passes the hull's
1651
+ `linearVelocity`):
1137
1652
 
1138
1653
  ```ts
1139
1654
  bolt.rotation = newHeadingDeg;
@@ -1455,6 +1970,90 @@ can only click is not playable on a controller.
1455
1970
  The pause key is polled in `update()`, which still runs at `timeScale 0` — a
1456
1971
  menu that cannot be closed by the key that opened it is worse than no menu.
1457
1972
 
1973
+ ### The whole shell, without a script
1974
+
1975
+ A title screen and an options panel are the other two screens every shipped game
1976
+ has, and the hard one is BACK: options open from the title AND from the pause
1977
+ menu and have to return to whichever asked. Two more props and seven
1978
+ `connections` rows are the entire thing:
1979
+
1980
+ ```json
1981
+ { "root": { "name": "Game", "type": "Node3D",
1982
+ "script": { "name": "GameFlow", "props": { "titlePanelPath": "/root/HUD/Title" } },
1983
+ "children": [
1984
+ { "name": "HUD", "type": "HudLayer", "children": [
1985
+ { "name": "Title", "type": "UiPanel", "props": { "anchor": "center" }, "children": [
1986
+ { "name": "Play", "type": "UiButton", "props": { "text": "PLAY" } },
1987
+ { "name": "Options", "type": "UiButton", "props": { "text": "OPTIONS" } } ] },
1988
+ { "name": "PauseMenu", "type": "UiPanel", "props": { "visible": false }, "children": [
1989
+ { "name": "Resume", "type": "UiButton", "props": { "text": "RESUME" } },
1990
+ { "name": "Options", "type": "UiButton", "props": { "text": "OPTIONS" } },
1991
+ { "name": "Restart", "type": "UiButton", "props": { "text": "RESTART" } } ] },
1992
+ { "name": "Options", "type": "UiPanel", "props": { "visible": false }, "children": [
1993
+ { "name": "Back", "type": "UiButton", "props": { "text": "BACK" } } ] }
1994
+ ] }
1995
+ ] },
1996
+ "connections": [
1997
+ { "from": "/root/HUD/Title/Play", "signal": "pressed", "to": ".", "handler": "resume" },
1998
+ { "from": "/root/HUD/Title/Options", "signal": "pressed", "to": ".",
1999
+ "handler": "screen", "args": ["/root/HUD/Options"] },
2000
+ { "from": "/root/HUD/PauseMenu/Resume", "signal": "pressed", "to": ".", "handler": "resume" },
2001
+ { "from": "/root/HUD/PauseMenu/Options", "signal": "pressed", "to": ".",
2002
+ "handler": "screen", "args": ["/root/HUD/Options"] },
2003
+ { "from": "/root/HUD/PauseMenu/Restart", "signal": "pressed", "to": ".", "handler": "restart" },
2004
+ { "from": "/root/HUD/Options/Back", "signal": "pressed", "to": ".", "handler": "back" }
2005
+ ] }
2006
+ ```
2007
+
2008
+ | prop / method | what it does |
2009
+ | --- | --- |
2010
+ | `titlePanelPath` | a screen shown at BOOT, holding the world (`""` = start playing) |
2011
+ | `saveSlots` | the `SaveSlots` namespace this game keeps saves in (`""` = no saving) |
2012
+ | `screen(path)` | push a screen over whatever is up; freezes the world if it was running |
2013
+ | `back()` | close the top screen and show **whoever asked** — the title, or the pause menu |
2014
+
2015
+ `back()` with nothing left on the stack and no menu under it RESUMES, so a screen
2016
+ opened during play (an inventory, a map) closes back into the game.
2017
+
2018
+ ### SAVE and CONTINUE, from the same menu
2019
+
2020
+ `SaveSlots`, `captureState` and `restoreState` have been there since 0.62 and no
2021
+ shipped starter used any of them: a scaffolded game could not be closed and come
2022
+ back. The API was never the problem, the SEQUENCE was — write a slot, reload the
2023
+ scene from source, wait for the reload, restore by uid. `restart()` is already
2024
+ the engine's verb for "reload this scene"; these are its two siblings.
2025
+
2026
+ ```json
2027
+ { "from": "/root/HUD/PauseMenu/Save", "signal": "pressed", "to": "/root/Flow", "handler": "save" },
2028
+ { "from": "/root/HUD/PauseMenu/Continue", "signal": "pressed", "to": "/root/Flow", "handler": "continueFrom" }
2029
+ ```
2030
+
2031
+ | method | what it does |
2032
+ | --- | --- |
2033
+ | `save(slot = "1")` | write this scene's behaviour state into the slot; `false` and an error if `saveSlots` is empty |
2034
+ | `hasSave(slot = "1")` | is there something to come back to (a CONTINUE button asks this) |
2035
+ | `continueFrom(slot = "1")` | reload the scene and hand every behaviour its state back |
2036
+
2037
+ **WHEN to save is still yours** — the rule in `incanto-save-slots.md` stands,
2038
+ there is no autosave, and `save` is a handler you wire to whatever signal marks
2039
+ the moment: a checkpoint, a level end, a button.
2040
+
2041
+ **A save belongs to the SCENE that wrote it.** `continueFrom` checks the scene's
2042
+ name and refuses a slot from another one, saying so — routing between levels is
2043
+ the game's, because the engine has no map of them. A multi-scene game loads the
2044
+ right scene first and then restores (see `incanto-save-slots.md`).
2045
+
2046
+ Composed in `examples/beacon-isle-3d` — the template `incanto-new` scaffolds by
2047
+ default — as two buttons in its pause menu.
2048
+
2049
+ **Author every screen that is not up at boot with `"visible": false`.** `GameFlow`
2050
+ owns the visibility of the ones it knows about — the title, the pause menu, and
2051
+ any screen it has been asked to open — and it cannot hide one it has never seen.
2052
+
2053
+ `examples/shell-3d` is the whole shell composed this way: seven rows of JSON, and
2054
+ its behaviour file keeps only the clock, the best time and the two settings the
2055
+ game itself invented.
2056
+
1458
2057
  **Give the Flow its own node.** A node holds one behavior, and your root
1459
2058
  probably already carries the game's director script.
1460
2059
 
@@ -1557,10 +2156,11 @@ parented to a rock: `position` is `[0, 3.02, 0]` and the world point is
1557
2156
  `[13, 14.31, 80.18]` — **82 m apart**.
1558
2157
 
1559
2158
  ```ts
1560
- import { localFromWorld, worldPosition } from 'incanto/gameplay';
2159
+ import { localFromWorld, worldDirection, worldPosition } from 'incanto/gameplay';
1561
2160
 
1562
2161
  const here = worldPosition(crate); // [13, 14.31, 80.18]
1563
2162
  crate.position = localFromWorld(crate, [0, 20, 0]); // put it at world [0,20,0]
2163
+ const forward = worldDirection(barrel, [0, 0, 1]); // which way it POINTS
1564
2164
  ```
1565
2165
 
1566
2166
  Both work headless and in both dimensions — arity comes from the node (2 for a
@@ -1569,10 +2169,15 @@ nothing. Do NOT reach for `_ensureObject3D().matrixWorld`: it is internal, and
1569
2169
  in a headless verify script there is no renderer to parent the object, so it
1570
2170
  hands back the LOCAL position with no warning.
1571
2171
 
1572
- **Translation only.** They sum ancestor positions and ignore ancestor rotation
1573
- and scalethe same contract physics already uses (`worldPosition2D` /
1574
- `worldPosition3D`) and that `applyGroundSnap` assumes. A node under a ROTATING
1575
- parent needs the renderer's matrix, not this.
2172
+ **Rotation composes.** Every ancestor's turn is applied on the way down, the
2173
+ same as the renderer and physics do a muzzle two metres down the +z of a
2174
+ turret that is itself yawed on a hull reports where it is DRAWN. (It sums
2175
+ positions and rotations; scale is still ignored.)
2176
+
2177
+ `worldDirection(node, local)` is the direction half: a local vector turned by
2178
+ the node's own rotation and every ancestor's. `worldDirection(node, [0, 0, 1])`
2179
+ is the +Z-forward convention every 3D skin here follows, and it is what a barrel
2180
+ tip, a hardpoint or a chase camera wants. Two games wrote it by hand first.
1576
2181
 
1577
2182
  ## Persistence (high scores, unlocks, settings)
1578
2183
 
@@ -1614,3 +2219,46 @@ through the crate in the way. `facePath`/`turnSpeed` turn a node (a skin, a
1614
2219
  hull) toward the direction of travel, the way every other mover does — a ship
1615
2220
  on a rail flew every bend sideways without it. Corners never eat frame budget — the
1616
2221
  speed is exact across bends.
2222
+
2223
+ **A rail you place in the editor.** `path` names the node whose CHILDREN are the
2224
+ waypoints, in tree order, in world space — no `setPath` call, no sorting by a
2225
+ tag — and `autoStart: false` holds the rig until `start()`, a handler a
2226
+ connection can name (a cutscene camera waits for its cue). A `path` that
2227
+ resolves to nothing is a load error, not a rig that stands still.
2228
+
2229
+ ```json
2230
+ { "name": "Rail", "type": "Node3D", "children": [
2231
+ { "name": "W0", "type": "Node3D", "props": { "position": [-16, 3.8, 13] } },
2232
+ { "name": "W1", "type": "Node3D", "props": { "position": [-6, 4.6, 8] } },
2233
+ { "name": "W2", "type": "Node3D", "props": { "position": [9, 3.2, -3] } } ] },
2234
+ { "name": "CamRig", "type": "Node3D",
2235
+ "script": { "name": "PathFollow", "props": { "path": "../Rail", "speed": 4.5, "autoStart": false } },
2236
+ "children": [ { "name": "IntroCam", "type": "Camera3D", "props": { "lookAt": "/root/Vault/Door" } } ] }
2237
+ ```
2238
+
2239
+ ## A cutscene
2240
+
2241
+ A cutscene is five things the engine already has, in order — `examples/heist-3d`
2242
+ ("Vault") is the composition, and its `Director` behaviour is the recipe:
2243
+
2244
+ 1. **Take the camera**: `doorCam.makeCurrent()` — or from JSON,
2245
+ `{ "signal": "triggerEnter", "from": "Vault/DoorTrigger", "to": "Vault/DoorCam", "handler": "makeCurrent" }`.
2246
+ Give the door camera and the gameplay camera `blendSeconds: 0.6` and the
2247
+ switch there and back FLIES instead of cutting (`makeCurrent(0.6)` asks once).
2248
+ A rail shot is a `Camera3D` under a `PathFollow` rig with `path` + `autoStart: false`, started on cue.
2249
+ 2. **Let go of the player**: `CharacterController3D.enabled = false` (and zero
2250
+ its velocity); **freeze what could hurt them**: each guard's `Patrol`,
2251
+ `Chase` and `Sight` behaviours `enabled = false`, and its grab `Area3D`
2252
+ `enabled = false` — a switched-off sensor overlaps nothing.
2253
+ 3. **Bars and a line**: `hud.letterbox = 0.11`; `talk.say(...)` with
2254
+ `autoAdvanceSeconds` on the `UiDialogue`, and wait for `dialogueFinished`.
2255
+ 4. **The world changes on camera**: `door.start()` (`MoveTo`) and wait for
2256
+ `arrived`.
2257
+ 5. **Skip**: on the skip action, apply every remaining side effect at once —
2258
+ `door.finish()`, `talk.clear()`, `rig.stop()` — then give everything back:
2259
+ the gameplay camera `makeCurrent()`, the controller and the guards enabled,
2260
+ `letterbox = 0`. A skipped cutscene leaves the world as the whole one would have.
2261
+
2262
+ Offline (`incanto verify`, the editor's ▶ play) the same steps run; a harness
2263
+ reads `playing`, the current camera and the controller's `enabled` to prove
2264
+ the stick moves nobody during the shot and moves the player after it.