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.
- package/bin/incanto-multiplay.mjs +52 -4
- package/dist/2d.d.ts +83 -7
- package/dist/2d.js +4 -3
- package/dist/3d.d.ts +861 -167
- package/dist/3d.js +6 -6
- package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
- package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
- package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
- package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
- package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
- package/dist/editor.js +1600 -1529
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
- package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
- package/dist/gameplay.d.ts +573 -31
- package/dist/gameplay.js +3 -2
- package/dist/index.d.ts +150 -6
- package/dist/index.js +6 -6
- package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
- package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
- package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
- package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
- package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
- package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
- package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
- package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
- package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
- package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
- package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
- package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
- package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
- package/dist/test.d.ts +90 -12
- package/dist/test.js +2 -2
- package/dist/vite.js +34 -5
- package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
- package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
- package/editor/assets/index-Db74LZY-.js +11579 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +1639 -280
- package/skills/incanto-3d-character.md +383 -20
- package/skills/incanto-audio.md +24 -2
- package/skills/incanto-building-2d-games.md +21 -2
- package/skills/incanto-building-3d-games.md +38 -3
- package/skills/incanto-editor.md +29 -8
- package/skills/incanto-environment.md +257 -14
- package/skills/incanto-game-feel.md +60 -1
- package/skills/incanto-gameplay-behaviors.md +668 -20
- package/skills/incanto-hud.md +116 -0
- package/skills/incanto-multiplayer.md +141 -2
- package/skills/incanto-node-reference.md +398 -16
- package/skills/incanto-performance.md +32 -0
- package/skills/incanto-physics-and-input.md +241 -13
- package/skills/incanto-playtesting.md +2 -2
- package/skills/incanto-save-slots.md +61 -2
- package/skills/incanto-scene-json-authoring.md +13 -0
- package/skills/incanto-verifying-your-game.md +86 -3
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
- package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/molehill-2d/src/game.scene.json +129 -2
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +40 -26
- package/templates-app/platformer-2d/src/game.scene.json +144 -2
- package/templates-app/platformer-2d/verify.ts +18 -1
- package/templates-app/star-survivor/PROJECT/Status.md +7 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/behaviors.ts +9 -8
- package/templates-app/star-survivor/src/game.scene.json +142 -4
- package/templates-app/star-survivor/verify.ts +13 -0
- package/templates-app/tps-3d/PROJECT/Status.md +6 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +71 -10
- package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
- package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
- package/templates-app/village-quest-3d/src/village.scene.json +126 -2
- package/templates-app/village-quest-3d/verify.ts +52 -2
- 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
|
|
105
|
-
"can be hurt / can die"
|
|
106
|
-
|
|
107
|
-
`
|
|
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)
|
|
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(
|
|
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
|
|
906
|
-
never
|
|
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
|
-
**
|
|
1573
|
-
and
|
|
1574
|
-
|
|
1575
|
-
|
|
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.
|