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.
- package/bin/incanto-check.mjs +114 -42
- package/bin/incanto-verify.mjs +9 -2
- package/dist/2d.d.ts +29 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +137 -15
- package/dist/3d.js +6 -6
- package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
- package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
- package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
- package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
- package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/editor.js +965 -893
- package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
- package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
- package/dist/gameplay.d.ts +141 -7
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -6
- package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
- package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
- package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
- package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
- package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
- package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
- package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
- package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
- package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
- package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
- package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
- package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
- package/dist/test.d.ts +56 -5
- package/dist/test.js +2 -2
- package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
- package/dist/vite.js +3 -3
- package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
- package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
- package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +634 -10
- package/skills/incanto-3d-character.md +113 -5
- package/skills/incanto-assets.md +9 -3
- package/skills/incanto-behaviors-and-scripts.md +26 -2
- package/skills/incanto-building-2d-games.md +9 -0
- package/skills/incanto-building-3d-games.md +45 -0
- package/skills/incanto-editor.md +6 -1
- package/skills/incanto-environment.md +7 -2
- package/skills/incanto-game-feel.md +16 -11
- package/skills/incanto-gameplay-behaviors.md +179 -16
- package/skills/incanto-node-reference.md +124 -45
- package/skills/incanto-physics-and-input.md +67 -5
- package/skills/incanto-scene-json-authoring.md +9 -3
- package/skills/incanto-verifying-your-game.md +19 -2
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
- package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
- package/templates-app/beacon-isle-3d/verify.ts +144 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +126 -0
- package/templates-app/platformer-2d/verify.ts +54 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/PROJECT/Status.md +40 -1
- package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/behaviors.ts +9 -0
- package/templates-app/tps-3d/src/cover.scene.json +26 -0
- package/templates-app/tps-3d/src/game.scene.json +331 -75
- package/templates-app/tps-3d/src/main.ts +14 -0
- package/templates-app/tps-3d/src/pillar.scene.json +25 -0
- package/templates-app/tps-3d/verify.ts +582 -3
- 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
|
-
`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
485
|
-
being moved is a `CharacterBody2D`/`3D`, `Chase`
|
|
486
|
-
`moveAndSlide()`, so it collides and slides like any
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
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.
|