incanto 0.72.0 → 0.74.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 (84) hide show
  1. package/bin/incanto-check.mjs +114 -42
  2. package/bin/incanto-verify.mjs +9 -2
  3. package/dist/2d.d.ts +29 -4
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +137 -15
  6. package/dist/3d.js +6 -6
  7. package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
  8. package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
  9. package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
  10. package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
  11. package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
  12. package/dist/debug.d.ts +1 -1
  13. package/dist/debug.js +1 -1
  14. package/dist/editor.js +965 -893
  15. package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
  16. package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
  17. package/dist/gameplay.d.ts +141 -7
  18. package/dist/gameplay.js +2 -2
  19. package/dist/index.d.ts +6 -4
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
  25. package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
  26. package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
  30. package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
  31. package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
  32. package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
  33. package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
  34. package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
  35. package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
  36. package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
  37. package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
  38. package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
  39. package/dist/test.d.ts +56 -5
  40. package/dist/test.js +2 -2
  41. package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
  44. package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
  45. package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +634 -10
  49. package/skills/incanto-3d-character.md +113 -5
  50. package/skills/incanto-assets.md +9 -3
  51. package/skills/incanto-behaviors-and-scripts.md +26 -2
  52. package/skills/incanto-building-2d-games.md +9 -0
  53. package/skills/incanto-building-3d-games.md +45 -0
  54. package/skills/incanto-editor.md +6 -1
  55. package/skills/incanto-environment.md +7 -2
  56. package/skills/incanto-game-feel.md +16 -11
  57. package/skills/incanto-gameplay-behaviors.md +179 -16
  58. package/skills/incanto-node-reference.md +124 -45
  59. package/skills/incanto-physics-and-input.md +67 -5
  60. package/skills/incanto-scene-json-authoring.md +9 -3
  61. package/skills/incanto-verifying-your-game.md +19 -2
  62. package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
  63. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
  64. package/templates-app/beacon-isle-3d/package.json +1 -1
  65. package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
  66. package/templates-app/beacon-isle-3d/verify.ts +144 -0
  67. package/templates-app/molehill-2d/package.json +1 -1
  68. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  69. package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
  70. package/templates-app/platformer-2d/package.json +1 -1
  71. package/templates-app/platformer-2d/src/game.scene.json +126 -0
  72. package/templates-app/platformer-2d/verify.ts +54 -0
  73. package/templates-app/star-survivor/package.json +1 -1
  74. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  75. package/templates-app/tps-3d/PROJECT/Status.md +40 -1
  76. package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
  77. package/templates-app/tps-3d/package.json +1 -1
  78. package/templates-app/tps-3d/src/behaviors.ts +9 -0
  79. package/templates-app/tps-3d/src/cover.scene.json +26 -0
  80. package/templates-app/tps-3d/src/game.scene.json +331 -75
  81. package/templates-app/tps-3d/src/main.ts +14 -0
  82. package/templates-app/tps-3d/src/pillar.scene.json +25 -0
  83. package/templates-app/tps-3d/verify.ts +582 -3
  84. package/templates-app/village-quest-3d/package.json +1 -1
@@ -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). 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), 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.
4
4
  ---
5
5
 
6
6
  # Gameplay behaviors — the built-in logic library
@@ -79,7 +79,7 @@ scaffolded project's `check` script passes it, and why a hand-written one that
79
79
  does not loses the check silently. `auditScene` from `incanto/test` is the same
80
80
  audit from inside your harness, once your behaviours are registered.
81
81
 
82
- `Chase.target` and `FollowCamera.target` are `required`: an EMPTY one
82
+ `FollowCamera.target` is `required` (and `Chase` needs `target` or `targetGroup`): an EMPTY one
83
83
  is a load error naming the node, not a surprise in the browser.
84
84
 
85
85
  **Never write `/root/<RootName>/...`.** If your root node is named `Game`, the
@@ -94,6 +94,11 @@ a connection `to` the root is `/root` (or `.`), never `/root/Game`.
94
94
 
95
95
  ---
96
96
 
97
+ ## Checkpoint
98
+
99
+ On an `Area3D`/`Area2D`, pointing at a `Respawn`: touch it and the next fall
100
+ puts you back HERE. Documented with `Respawn` in `incanto-behaviors-and-scripts.md`.
101
+
97
102
  ## Health
98
103
 
99
104
  Hit points with regen and post-hit invulnerability (i-frames). The universal
@@ -105,13 +110,13 @@ Hit points with regen and post-hit invulnerability (i-frames). The universal
105
110
  |---|---|---|
106
111
  | `max` | `100` | maximum HP; `current` starts here |
107
112
  | `regenPerSec` | `0` | HP regained per second (0 = off) |
108
- | `invulnerableFor` | `0` | seconds of immunity after a hit (i-frames; 0 = off) |
113
+ | `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 |
109
114
  | `freeOnDeath` | `false` | `queueFree()` this node when it dies (clone-safe) |
110
115
  | `freeParent` | `false` | with `freeOnDeath`, free the PARENT — the enemy, when `Health` rides a child node |
111
116
 
112
117
  Signals: `damaged(amount, current)` · `healed(amount, current)` ·
113
118
  `healthChanged(current, max)` · `died` · `revived(current)`
114
- Methods: `damage(n)` · `heal(n)` · `kill()` · `revive(hp?)` — state: `current`,
119
+ Methods: `damage(n)` · `heal(n)` · `kill()` · `revive(hp?)` · `protect(seconds)` — state: `current`,
115
120
  `isDead`
116
121
 
117
122
  **A dead Health STAYS dead until you revive it.** `damage`, `heal` and regen all
@@ -179,6 +184,28 @@ Signals: `expired` — Methods: `startTimer()`
179
184
  "script": { "name": "Lifetime", "props": { "seconds": 2 } } }
180
185
  ```
181
186
 
187
+ **A fuse wired to a trigger on the floor is lit by the floor.** The natural
188
+ `startOnSignal` composition — a mine whose child `Area3D` is wired
189
+ `triggerEnter → startTimer` — sat on tps-3d's arena floor with a 1 m sphere at
190
+ y 0.5, and `triggerEnter` fired on frame 1 from `/Game/Arena/Floor`
191
+ (a `StaticBody3D`): the 1.5 s fuse expired at frame 91 with nobody near it.
192
+ Areas report the static world too (see `overlapping()` in
193
+ `incanto-physics-and-input.md`). The fix is the connection's own `filter`,
194
+ which gates on the body that entered:
195
+
196
+ ```json
197
+ { "signal": "triggerEnter", "from": "Arena/Mine/Trigger", "to": "Arena/Mine",
198
+ "handler": "startTimer", "filter": { "group": "player" } }
199
+ ```
200
+
201
+ The sensor still SEES the floor; the connection no longer fires on it, and the
202
+ player (in group `player`) lights the fuse as before — measured: 0 expiries in
203
+ 200 frames with nobody near, 1 within 90 frames of the player stepping in. The
204
+ same rule `Pickup.collectorGroup` and `DamageOnContact.targetGroup` carry on
205
+ their props, for a bare connection. (An earlier version of this paragraph said
206
+ a connection cannot filter by group and told you to lift the trigger. It can,
207
+ and you do not have to.)
208
+
182
209
  ## ScoreKeeper
183
210
 
184
211
  The game's **state hub** — score, lives, win/lose. Put it on the Root/Game node
@@ -195,7 +222,7 @@ Signals: `scoreChanged(score)` · `won` · `lost` · `lifeLost(lives)` ·
195
222
  `livesChanged(lives)` — the EVENT and the COUNT: `lifeLost` fires when a life is
196
223
  spent (flash, respawn), `livesChanged` also fires when a save is restored, so a
197
224
  lives counter belongs on that one.
198
- Methods: `addScore(n)` · `setScore(n)` · `loseLife()`
225
+ Methods: `addScore(n)` · `setScore(n)` · `loseLife()` — state: `score`, `lives`, and read-only `won` / `lost` (the signals fire once; the flags stay true, which is what a HUD banner and a harness want to ASK)
199
226
 
200
227
  ```json
201
228
  { "name": "Game", "type": "Node",
@@ -262,7 +289,7 @@ calls `damage(amount)`, then emits `dealtDamage(amount, healthOwnerNode)`.
262
289
  |---|---|---|
263
290
  | `amount` | `10` | HP removed per contact |
264
291
  | `targetGroup` | `""` | only damage targets in this group — the tag may sit on the BODY or on the node carrying its `Health` (`""` = any) |
265
- | `oncePerTarget` | `true` | damage each target at most **once, ever** |
292
+ | `oncePerTarget` | `true` | damage each target at most once **per overlap** — leave and come back (or disarm the hitbox and arm it again) and it lands again |
266
293
  | `repeatEvery` | `0` | seconds between re-hits while still overlapping (`0` = entry only) |
267
294
  | `destroySelf` | `false` | `queueFree()` after the first hit |
268
295
 
@@ -282,6 +309,32 @@ oncePerTarget=true repeatEvery=0.5 → hp 0
282
309
  oncePerTarget=true repeatEvery=0 → hp 90
283
310
  ```
284
311
 
312
+ ### A melee swing: arm the hitbox for the active frames
313
+
314
+ A sword is an `Area3D` with `DamageOnContact` that is **disarmed** (`enabled:
315
+ false`) and armed only while the swing can hurt — the engine's own
316
+ `enabled` on the Area, flipped from the attack behaviour:
317
+
318
+ ```ts
319
+ // in the attack behaviour's update(), `t` seconds into the swing
320
+ edge.enabled = t >= 0.18 && t <= 0.42; // the active frames of the clip
321
+ ```
322
+
323
+ Arming over a foe you are already touching IS a hit (the engine synthesises the
324
+ `triggerEnter`), and disarming ends the contact, so `oncePerTarget` stays at its
325
+ default and every swing lands once per foe — a sword built from the tarball hit
326
+ each brute exactly once per GAME before this was so. Two more things the build
327
+ paid for: **the hitbox rides the BODY, not the skin** — physics composes
328
+ ancestor offsets only, and the skin is what the controller turns, so an `Area3D`
329
+ under `Skin` sat behind the player at every yaw (`incanto-check` now warns) —
330
+ and it is **aimed from the skin's yaw** every frame, a +Z-forward model facing
331
+ `(sin yaw, 0, cos yaw)`:
332
+
333
+ ```ts
334
+ const yaw = (skin.rotation[1] * Math.PI) / 180;
335
+ edge.position = [Math.sin(yaw) * 0.9, 0.3, Math.cos(yaw) * 0.9];
336
+ ```
337
+
285
338
  `repeatEvery` is the only prop that makes a resting overlap keep hurting, and it
286
339
  overrides `oncePerTarget` — lava pools, poison clouds, an enemy standing on you.
287
340
  Set it on **every enemy contact hitbox**; without it a game plays as though the
@@ -340,8 +393,24 @@ in-range actor). Declare the input action in the scene `input{}`.
340
393
  | `action` | `"interact"` | input action (button) that triggers it |
341
394
  | `range` | `2` | max distance an actor may be |
342
395
  | `actorGroup` | `"player"` | only nodes in this group can interact |
396
+ | `prompt` | `""` | what the actor sees on it — `Open drawer`; read by whoever draws the "[E] …" line |
397
+ | `focus` | `"any"` | which one answers when SEVERAL are in range: `any` (all of them) · `nearest` (the closest) · `facing` (of those inside `coneDeg` of the current camera's forward, the one nearest the CENTRE of the view — a crosshair, pitch included — so the note on the desk and the drawer under it are two looks, and in third person the plot the farmer looks at beats the plot under the farmer's feet; with no `Camera3D`, same as `nearest`) |
398
+ | `coneDeg` | `40` | full width of the `facing` cone — a generous crosshair |
399
+
400
+ Signals: `interacted(actor)` · `focused(actor)` · `unfocused(actor)`. Getter:
401
+ `focusedBy` — the actor it would answer right now, or null.
343
402
 
344
- Signals: `interacted(actor)`
403
+ **Three things within two metres and one press opened, lit and read all of
404
+ them** — an escape room built from the tarball. Each Interactable answered the
405
+ press on its own; nothing asked which one the actor MEANT. `focus: "facing"`
406
+ is the first-person answer, `"nearest"` the top-down one, and the prompt line
407
+ every adventure game draws is the focused one's `prompt`:
408
+
409
+ ```ts
410
+ // HudUpdater.update — the "[E] Open drawer" line
411
+ const focused = interactables.find((i) => i.focusedBy);
412
+ promptText.text = focused ? `[E] ${focused.prompt}` : '';
413
+ ```
345
414
 
346
415
  ```json
347
416
  { "input": { "interact": { "type": "button", "keys": ["KeyE"] } },
@@ -402,6 +471,7 @@ author markers). Emits `reachedPoint(index)` on arrival.
402
471
  | `mode` | `"loop"` | `'loop'` wraps · `'pingpong'` reverses at the ends |
403
472
  | `pauseAt` | `0` | seconds to wait at each reached point |
404
473
  | `moveParent` | `false` | move the PARENT node instead of this one |
474
+ | `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 |
405
475
  | `facePath` | `""` | node path to TURN toward the direction of travel (e.g. `"../Skin"`) |
406
476
  | `turnSpeed` | `100` | turn rate rad/s; 100 is an instant snap |
407
477
 
@@ -438,11 +508,14 @@ within `stopRange` (melee reach). `reachedTarget` fires once on entering range
438
508
 
439
509
  | Prop | Default | Meaning |
440
510
  |---|---|---|
441
- | `target` | `""` | node path to chase, resolved from THIS node (required) |
511
+ | `target` | `""` | node path to chase, resolved from THIS node this or `targetGroup` |
512
+ | `targetGroup` | `""` | the OTHER way to name a target: the NEAREST member of this group, re-picked every `retargetEvery` s (`0.25`). A guard in an RTS, a co-op, a survival game with allies chases "whichever of the six is closest" — `target: "%Player"` was written for one hero. No member in reach: it waits |
513
+ | `retargetEvery` | `0.25` | seconds between nearest-of-group picks (`0` = every frame) |
442
514
  | `speed` | `60` | units per second |
443
515
  | `stopRange` | `0` | stop (and emit reachedTarget) within this distance |
444
516
  | `loseRange` | `0` | give up beyond this distance (0 = never lose) |
445
517
  | `moveParent` | `false` | move the PARENT node instead of this one |
518
+ | `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 |
446
519
  | `facePath` | `""` | node path to TURN toward the direction of travel (e.g. `"../Skin"`) |
447
520
  | `turnSpeed` | `100` | turn rate rad/s; 100 is an instant snap |
448
521
 
@@ -462,6 +535,19 @@ whole entity toward the target. (Distance is measured from the moved parent.)
462
535
  "script": { "name": "Chase", "props": { "target": "/root/Player", "speed": 120, "stopRange": 20 } } }
463
536
  ```
464
537
 
538
+ **`ground` — a walker, not a flyer.** A chaser used to move straight at
539
+ `target.position` in every axis, so a player on a ledge was met by enemies
540
+ pushing diagonally up whatever face was in the way — a 70° cliff included —
541
+ and `slopeLimitDeg` never got a say. A `CharacterBody3D` mover is a walker by
542
+ construction, so it now chases along the ground by default: the target's
543
+ height is ignored, the KCC's `slopeLimitDeg` and `stepHeight` decide what it
544
+ climbs (tps-3d's enemies declare `stepHeight: 0.7` and `slopeLimitDeg: 50`, and
545
+ the arena's harness walks one up a 40° hill and into a 70° cliff), and it
546
+ FALLS off what it walks off — scene gravity, accumulated while `isOnFloor()`
547
+ says no. A flying enemy on a kinematic capsule says `ground: false`. In 2D it
548
+ is opt-in (`ground: true` for a platformer's ground chaser), because a top-down
549
+ game chases in y on purpose.
550
+
465
551
  **`facePath` — an NPC that does not run backwards.** `CharacterController3D`
466
552
  turns the PLAYER's skin toward its travel; for an NPC nothing did, so every 3D
467
553
  game hand-wrote the same `yaw = atan2(dx, dz)` — against a formula
@@ -481,12 +567,22 @@ It is opt-in (empty default turns nothing), it is the exact math and the exact
481
567
  `rotation` (`atan2(dy, dx)`) — right for top-down, so a PLATFORMER that faces
482
568
  with `flipX` should leave `facePath` empty.
483
569
 
484
- **A character body chases through the level, not through walls.** When the node
485
- being moved is a `CharacterBody2D`/`3D`, `Chase` (and `Patrol`) move it with
486
- `moveAndSlide()`, so it collides and slides like any character. A plain
570
+ **A character body chases through the level, not through walls and it IS one
571
+ to the player.** When the node being moved is a `CharacterBody2D`/`3D`, `Chase`
572
+ (and `Patrol`) move it with `moveAndSlide()`, so it collides and slides like any
573
+ character. A kinematic body is immovable to a `RigidBody3D` player: a chaser
574
+ coming head-on down a 6 m ramp held the player at the foot of it through six
575
+ hits and two deaths, and an approach offset by 1.8 m went through with one.
576
+ Give a choke point a way round, or the chaser is a door. A plain
487
577
  `Node2D`/`Node3D` — the example above — has no collider and keeps the direct
488
578
  write, which is also what you want for a ghost or a flying marker.
489
579
 
580
+ **A horde ignores itself.** A hundred chasers that are all `CharacterBody3D`
581
+ cost a second a step once they crowd the player — every `moveAndSlide` shape-casts
582
+ against every neighbour. Give a swarm's template `collideWithCharacters: false`
583
+ (measured on `examples/survivor-3d`: 120 husks, 1008 ms → 2.0 ms a step); they
584
+ overlap each other and still hit the walls.
585
+
490
586
  This is straight-line homing, not pathfinding: a body can now be STOPPED by
491
587
  geometry it used to walk through. When enemies must find their way around a
492
588
  level, that is what `buildTerrainNav` + `findPath` + `PathFollow` are for (see
@@ -547,6 +643,31 @@ is a far worse failure than one running when it was meant to sleep.
547
643
 
548
644
  From TypeScript it is the same switch: `this.getNode('../Hunt').behavior.enable()`.
549
645
 
646
+ ## Turret
647
+
648
+ The oldest verb in games: the nearest live member of `targetGroup` within
649
+ `range`, hit for `damage` every `cooldown`. A defense tower, a ship's gun, a
650
+ sentry, a trooper who fights what he meets. Two games in a row wrote these
651
+ forty lines by hand before it existed — the engine had `Chase` for going TO a
652
+ target and `Projectile` for the bullet, and nothing that decides to shoot.
653
+
654
+ ```json
655
+ { "name": "Gun", "type": "Node3D",
656
+ "script": { "name": "Turret", "props": { "range": 7, "damage": 12, "cooldown": 0.5, "facePath": "../Barrel" } } }
657
+ ```
658
+
659
+ | Prop | Default | Meaning |
660
+ |---|---|---|
661
+ | `targetGroup` | `"enemy"` | who it shoots — the nearest LIVE member (a dead `Health` is skipped) |
662
+ | `range` | `6` | reach, in the scene's units |
663
+ | `damage` | `10` | hitscan damage per shot (`target.behavior.damage(n)`) |
664
+ | `cooldown` | `0.6` | seconds between shots |
665
+ | `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` |
666
+ | `facePath` / `turnSpeed` | `""` / `100` | a barrel to turn toward the target |
667
+
668
+ Signals: `acquired(target)` · `fired(target)` · `lost()`. Getters: `target`,
669
+ `shots`. Dimension-agnostic — a 2D tower is the same node with `[x, y]`.
670
+
550
671
  ## FaceTarget
551
672
 
552
673
  Turn toward the nearest thing in a group and say when you are pointed at it —
@@ -829,6 +950,12 @@ this.node.getNode('/Game/Towers').addChild(tower);
829
950
 
830
951
  Methods: `make(name)` · `names()`
831
952
 
953
+ **Listen before it enters the tree.** A clone placed where a body already is —
954
+ an XP orb dropped at the player's feet — is collected on its very first physics
955
+ step; a listener attached a frame later (a director scanning a container in
956
+ `update`) misses it. `make()` returns the node detached: wire `collected`,
957
+ `died`, whatever you need, THEN `addChild`.
958
+
832
959
  - **Its children never enter the tree.** Claimed in `onEnterTree` (parent-first,
833
960
  so a template carrying its own shelf is taken intact), so nothing has to be
834
961
  `visible: false` and nothing has to be `"enabled": false`. A template left in
@@ -850,6 +977,14 @@ clone the editor uses (scripts and children come along; the clone gets a fresh
850
977
  identity). `max` caps LIVE instances (drops as spawned children free, so it
851
978
  refills); `total` caps LIFETIME spawns then emits `finished`.
852
979
 
980
+ **One template per spawner, and do not move a spawner that has spawned.** The
981
+ clones are the spawner's CHILDREN, so a gate that was moved to a new edge before
982
+ each spawn dragged every husk it had ever made along with it (measured: they
983
+ fell through the world). Four gates sharing one template lost it to the first —
984
+ the detach below takes it out of the tree, and the other three found nothing.
985
+ Give each spawner its own template child and let a director call `spawn()` on
986
+ whichever gate it likes.
987
+
853
988
  **The template never ticks.** On first resolution the Spawner DETACHES the
854
989
  template node from the live tree (holding it only as a clone source), so its
855
990
  behaviors never run and it never renders — a `visible:false` template no longer
@@ -865,6 +1000,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
865
1000
  | `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
866
1001
  | `autoStart` | `true` | begin ticking at ready |
867
1002
  | `total` | `0` | total LIFETIME spawns (0 = infinite) |
1003
+ | `into` | `""` | node path of the CONTAINER clones are added to (`""` = under the spawner). Clones are placed at the spawner's world position plus `at`, and stay put when the spawner moves — the way to let a gate wander |
868
1004
 
869
1005
  Signals: `spawned(node)` · `finished` — Methods: `spawn()` · `start()` · `stop()`
870
1006
 
@@ -966,7 +1102,15 @@ the heading on the projectile itself.
966
1102
  |---|---|---|
967
1103
  | `speed` | `300` | units per second along `direction` — **live**, rescales in flight |
968
1104
  | `direction` | `null` | a vector `[x,y(,z)]` OR `'forward'` (`null` = forward) — **live** |
969
- | `gravity` | `0` | constant +y (downward, 2D y-down) pull for arcing shots |
1105
+ | `gravity` | `0` | constant DOWNWARD acceleration for arcing shots, in the scene's own convention (+y in 2D, y in 3D): `9.81` lobs a 3D shell, `980` a 2D one |
1106
+
1107
+ **Down is the scene's down.** It used to add to +y whatever the scene — the 2D
1108
+ convention — so `"gravity": 9.81` in a y-up 3D scene was a shell that CLIMBED
1109
+ (measured on tps-3d's mortar, speed 8, direction `[0.8, 0.6, 0]`: y 19.4 m after
1110
+ 1.5 s and still rising at 59.8 m when its `Lifetime` freed it). A positive value
1111
+ now pulls down in both dimensions, the way `Chase.ground` signs its fall: the same
1112
+ mortar peaks at 2.1 m at 0.47 s and is under the floor by 1.5 s, the parabola a
1113
+ lob at 4.8 m/s up should draw.
970
1114
 
971
1115
  ### Aiming a bullet you just spawned
972
1116
 
@@ -1239,6 +1383,7 @@ smashTheCrystal();
1239
1383
  session.step(200);
1240
1384
 
1241
1385
  session.engine.effects.countOf('explosion'); // 1
1386
+ session.engine.effects.countKind('shake'); // by KIND — a camera shake has no name
1242
1387
  session.engine.effects.countFrom('/Game/Crystal/Boom'); // 1
1243
1388
  session.engine.effects.recent();
1244
1389
  // [{ kind: 'burst', name: 'explosion', from: '/Game/Crystal/Boom', amount: 60, at: 1.2 }]
@@ -1316,8 +1461,18 @@ probably already carries the game's director script.
1316
1461
 
1317
1462
  Terminal states ignore further transitions; the restart action (or
1318
1463
  `flow.restart()`) reloads the scene from its source JSON — fresh nodes,
1319
- physics, input. Listen to the `flowChanged(state)` signal for custom UI.
1320
- `restartScene(engine)` is exported standalone.
1464
+ physics, input. Listen to the `flowChanged(state)` signal for custom UI
1465
+ `state` is one of `"playing" | "paused" | "gameover" | "won"` (a LOSS is
1466
+ `"gameover"`, not "lost"; the signal the ScoreKeeper emits is `lost`, the
1467
+ state the flow lands in is `gameover`). `restartScene(engine)` is exported
1468
+ standalone.
1469
+
1470
+ **A game that has ended does not advance.** `freezeOnEnd` (default `true`)
1471
+ sets the engine's `timeScale` to 0 on `won`/`gameover`, so every `engine.step()`
1472
+ after that — a harness's `do:` steps included — moves nothing and reports
1473
+ nothing; a page of zeros after a win is the game being over, not the
1474
+ controller being broken. Read `flow.state`, or set `freezeOnEnd: false` for a
1475
+ harness that keeps measuring past the end.
1321
1476
 
1322
1477
  **Any scene swap thaws.** The freeze lives on the engine, not on the flow, so
1323
1478
  leaving a game-over screen by your own route — `engine.setScene(loadScene(next, { engine }))`
@@ -1378,7 +1533,9 @@ runtime, which it keeps looking for.
1378
1533
  Signals: `dayPhaseChanged('dawn'|'day'|'dusk'|'night')` on transitions — hook
1379
1534
  spawners, music, or monster aggression to it. The `hour` field (0-24) is
1380
1535
  writable: jump to a time of day in one assignment. For one-off look changes
1381
- use `setEnvironment3D(engine, patch)` from `incanto/3d` directly.
1536
+ use `setEnvironment3D(engine, patch)` from `incanto/3d` directly. A written `hour`
1537
+ announces its new phase on the NEXT update (a bed that jumps to dawn fires
1538
+ `dayPhaseChanged('dawn')` one frame later); only the sky patch is throttled.
1382
1539
 
1383
1540
  ## Boot loading overlay
1384
1541
 
@@ -1449,5 +1606,11 @@ npc.on('arrived', () => attack());
1449
1606
  default, corner-cutting forbidden; `{ diagonal: false }` for 4-way).
1450
1607
  `PathFollow` (registered behavior) walks waypoints at constant `speed`
1451
1608
  (units/sec — px in 2D, meters in 3D), emits `waypointReached(i)` /
1452
- `arrived`, and `loop: true` patrols. Corners never eat frame budget the
1609
+ `arrived`, and `loop: true` patrols. `moveParent: true` walks the PARENT
1610
+ the AI-on-a-child pattern `Chase`/`Patrol`/`Wander` share, so a unit keeps
1611
+ its `Health` on the body and its path on a child — and the move lands through
1612
+ `moveBody`, so a kinematic unit walks its path with its collisions rather than
1613
+ through the crate in the way. `facePath`/`turnSpeed` turn a node (a skin, a
1614
+ hull) toward the direction of travel, the way every other mover does — a ship
1615
+ on a rail flew every bend sideways without it. Corners never eat frame budget — the
1453
1616
  speed is exact across bends.