incanto 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/bin/incanto-check.mjs +58 -4
  2. package/dist/2d.d.ts +22 -3
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +5 -5
  5. package/dist/3d.js +5 -4
  6. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  7. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  8. package/dist/{create-game-CHDLDQsQ.js → create-game-DH7JI5xx.js} +76 -11
  9. package/dist/{create-game-BiW8Men_.js → create-game-IX5lEH0P.js} +21 -294
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/debug.js +1 -1
  12. package/dist/{duplicate-DJQd44CD.js → duplicate-CGqAmK2h.js} +1 -1
  13. package/dist/editor.js +2 -1
  14. package/dist/{environment-presets-DRAz5EV9.js → environment-presets-CNxCuhZF.js} +48 -13
  15. package/dist/{gameplay-BBEjPFsR.js → gameplay-Dtzd2itW.js} +53 -11
  16. package/dist/gameplay.d.ts +1 -1
  17. package/dist/gameplay.js +1 -1
  18. package/dist/index.d.ts +23 -6
  19. package/dist/index.js +7 -7
  20. package/dist/{loader-D8n7TU8W.js → loader-D7jTvDQv.js} +797 -2
  21. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +1 -1
  24. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  25. package/dist/{physics-2d-BaRSRrrZ.js → physics-2d-CBnor8Zf.js} +2 -2
  26. package/dist/{physics-3d-CYxjh-HW.js → physics-3d-BTUfUSWO.js} +3 -3
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-BpFcgdcL.js → register-DL3izw8j.js} +46 -9
  30. package/dist/{register-CDrAQqPp.js → register-xuSRyD6b.js} +119 -21
  31. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  32. package/dist/{replay-O-yAGM76.d.ts → replay-C5x2vPF5.d.ts} +16 -3
  33. package/dist/{replay-CEPyQtF_.js → replay-D7-yle3s.js} +56 -205
  34. package/dist/{split-screen-DDMZutQ6.js → split-screen-5Ban4q4n.js} +3 -3
  35. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  36. package/dist/{src-CY21B462.js → src-DozXvyZS.js} +1 -1
  37. package/dist/{teardown-RApWnM1G.js → teardown-B6rwJOyS.js} +1 -1
  38. package/dist/{test-DHYuFyAu.js → test-Dch_7VQD.js} +19 -14
  39. package/dist/test.d.ts +24 -4
  40. package/dist/test.js +2 -2
  41. package/dist/vite.js +2 -2
  42. package/editor/assets/{agent8-BoRGtVxK.js → agent8-CF1JL2tR.js} +1 -1
  43. package/editor/assets/{debug-CzdyCg75.js → debug-3QzYhOPA.js} +1 -1
  44. package/editor/assets/{index-VesuVEhe.js → index-CAD2c5ug.js} +92 -92
  45. package/editor/index.html +1 -1
  46. package/package.json +1 -1
  47. package/schemas/scene.schema.json +4 -0
  48. package/skills/incanto-3d-models.md +1 -1
  49. package/skills/incanto-assets.md +10 -1
  50. package/skills/incanto-audio.md +21 -13
  51. package/skills/incanto-behaviors-and-scripts.md +7 -3
  52. package/skills/incanto-building-2d-games.md +12 -3
  53. package/skills/incanto-editor.md +1 -1
  54. package/skills/incanto-gameplay-behaviors.md +24 -1
  55. package/skills/incanto-hud.md +5 -3
  56. package/skills/incanto-localization.md +31 -0
  57. package/skills/incanto-node-reference.md +1 -0
  58. package/skills/incanto-scene-json-authoring.md +6 -1
  59. package/templates-app/beacon-isle-3d/package.json +1 -1
  60. package/templates-app/platformer-2d/package.json +1 -1
  61. package/templates-app/star-survivor/package.json +1 -1
  62. package/templates-app/tps-3d/package.json +1 -1
  63. package/templates-app/village-quest-3d/package.json +1 -1
  64. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-VesuVEhe.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-CAD2c5ug.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.59.0",
3
+ "version": "0.61.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -3696,6 +3696,10 @@
3696
3696
  "align": {
3697
3697
  "type": "string",
3698
3698
  "default": "left"
3699
+ },
3700
+ "opacity": {
3701
+ "type": "number",
3702
+ "default": 1
3699
3703
  }
3700
3704
  },
3701
3705
  "additionalProperties": false
@@ -94,7 +94,7 @@ meta (name/authors) + humanoid bone count. Decision rules:
94
94
 
95
95
  `ModelInstance3D` props: `model` (`$key` or a direct URL), `targetHeight` (>0 uniformly
96
96
  scales the model to stand that many units tall — composes with the node `scale`, reactive
97
- at runtime; see "Sizing" below), `animation` (see below), `tint` (hex; `""`=off — multiplies into
97
+ at runtime; see "Sizing" below), `animation` (see below), `tint` (any CSS colour — `"#5f8f4a"`, `"chartreuse"`; `""`=off — multiplies into
98
98
  every material to RESKIN one shared GLB into many variants, e.g. base human → green
99
99
  zombie; clones materials per instance so other instances are untouched),
100
100
  `metalness`/`roughness` (0..1 overrides applied to EVERY material; `-1`=keep authored —
@@ -86,7 +86,16 @@ Every catalog entry has a `url` that is directly usable; there are two classes:
86
86
  `copy` does this for you and prints the ready-to-paste JSON — for animated
87
87
  sheets it includes the full `animations` map (idle/move/attack…) derived from
88
88
  the sheet metadata. `Sprite2D.texture` / `AnimatedSprite2D.sheet` take a
89
- `"$assetKey"` ref (the engine hard-fails on a raw URL).
89
+ `"$assetKey"` ref, and the engine hard-fails at LOAD on anything else — a raw
90
+ URL, a bare catalog id, or a `$key` the scene does not declare:
91
+
92
+ ```
93
+ [UNKNOWN_ASSET] 'Coin.texture' names asset '$coinn', which the scene does not
94
+ declare. Declared: [coin, gem]. (at '/Room/Coin')
95
+ ```
96
+
97
+ (It genuinely did not, until 0.61: the check lived in the renderer, so only a
98
+ browser ever reached it and a typo'd ref simply drew nothing.)
90
99
 
91
100
  The scene **composer (incanto-editor)** wires all of this for you: the inspector
92
101
  shows an asset PICKER on every texture/sheet/map prop — browse the built-ins by
@@ -138,9 +138,13 @@ motor.stop(1.2); // …or a slow custom fade
138
138
  } }
139
139
  ```
140
140
 
141
- Methods: **`play()`**, **`stop()`**. Signal: **`finished`** (fires when a
142
- non-looping `src` clip ends — connect it to clean up or chain sounds). Procedural
143
- presets are fire-and-forget one-shots: they do not emit `finished`.
141
+ Methods: **`play()`**, **`stop()`**. Signal: **`finished`** — connect it to clean
142
+ up or chain sounds. It fires when a non-looping `src` clip ends AND when a
143
+ procedural preset does: a preset's length is `attack + sustain + decay`, known
144
+ exactly, so `player.playing` is true for that long and `finished` arrives at the
145
+ end. It is measured on the unscaled clock, because a paused game still hears the
146
+ tail of the hit that paused it, and it fires headlessly too — the length is
147
+ arithmetic, not a device. `stop()` is not a finish and does not emit.
144
148
 
145
149
  ### Autoplay + the browser gesture-unlock
146
150
 
@@ -175,14 +179,14 @@ and models that 404'd:
175
179
 
176
180
  ---
177
181
 
178
- ## 2b. Spatial (3D positional) audio
182
+ ## 2b. Spatial (positional) audio
179
183
 
180
- Set **`spatial: true`** on an `AudioPlayer` in a **3D scene** and the sound pans
181
- (left/right) and attenuates by **distance** — the emitter is the node's world
182
- position, the **listener is the active `Camera3D`** (the one marked `current`).
183
- The 3D adapter feeds the emitter + listener pose to the panner every frame, so
184
- moving the emitter or the camera updates the sound live. Works for BOTH paths
185
- (procedural `preset` and a `src` file).
184
+ Set **`spatial: true`** on an `AudioPlayer` and the sound pans (left/right) and
185
+ attenuates by **distance** — the emitter is the node's world position, the
186
+ **listener is the active camera** (`Camera3D` or `Camera2D`, the one marked
187
+ `current`). The adapter feeds the emitter + listener pose to the panner every
188
+ frame, so moving the emitter or the camera updates the sound live. Works in both
189
+ dimensions and for BOTH paths (procedural `preset` and a `src` file).
186
190
 
187
191
  ```jsonc
188
192
  // Attach under a moving Node3D — it emits from THAT node's world position.
@@ -214,9 +218,13 @@ listener driven by the camera's world position + orientation. The pure
214
218
  distance-gain + pan-sign math (`spatialGain`, `spatialPan`, exported) is what the
215
219
  non-WebAudio paths use, so headless gameplay is unaffected.
216
220
 
217
- > **2D scenes:** spatial is currently **ignored** (no listener is fed) — a 2D
218
- > sound plays non-positionally regardless of `spatial`. Pan a 2D sound yourself
219
- > from the camera-relative x-offset if you need it. Spatial is a 3D feature.
221
+ > **2D scenes** work the same way: the listener is the active `Camera2D` (its
222
+ > world position is the view centre, and its own rotation turns the stereo image
223
+ > with it). The one difference is the UNIT — a 2D scene measures in PIXELS,
224
+ > and `refDistance: 1` / `maxDistance: 50` are metre-shaped defaults that put a
225
+ > sound at its quietest about one sprite away. Set them to your level's scale
226
+ > (`refDistance: 100`, `maxDistance: 900` is a reasonable start); `incanto check`
227
+ > warns when it sees `spatial: true` in a 2D scene with the defaults left alone.
220
228
 
221
229
  > **Headless / verify VM:** spatial is a no-op like all audio — `play()` won't
222
230
  > throw and your gameplay logic is unaffected.
@@ -102,7 +102,7 @@ What behavior code actually calls at runtime — all instance methods, no global
102
102
  | `engine.stop()` / `engine.start()` | pause / resume the loop — stop resets the clock and accumulator, so no banked sim time leaks into the resume |
103
103
  | `engine.step()` | advance exactly ONE fixed step + one update (both dt = the fixed step) — the unit of time for headless tests |
104
104
  | `engine.tick(timestampMs)` | manual frame advance — takes an **absolute** ms timestamp (rAF-style), NOT a dt; the first call after (re)start only primes the clock |
105
- | `engine.setScene(scene)` | swap scenes: the previous root is freed, the input map is cleared and redeclared from the new scene's `input{}`, then `sceneChanged` fires |
105
+ | `engine.setScene(scene)` | swap scenes: the previous root is freed, the input map is cleared and redeclared from the new scene's `input{}`, the clock resets (`time`, `unscaledTime`, and **`timeScale` back to 1** — a level restarted out of a frozen game-over must not boot frozen), then `sceneChanged` fires |
106
106
  | `engine.stats()` | live perf counters `{ fps, frameMs, nodes, running }` — fps/frameMs average the last ~60 REAL `tick` frames (headless `step()` runs report 0), nodes is the current tree size. GPU counters (triangles/draw calls) live on `renderer.stats()` / the merged `game.stats()` |
107
107
 
108
108
  The recurring traps: `duplicateNode` does NOT insert the clone anywhere — a
@@ -138,7 +138,9 @@ declared by their classes. Escape hatch for runtime one-offs: `node.declareSigna
138
138
  ]
139
139
  ```
140
140
  - `handler` must exist on the target NODE or its BEHAVIOR — hard `UNKNOWN_HANDLER`
141
- otherwise (validated at load; node methods take precedence on invocation).
141
+ otherwise, and hard `AMBIGUOUS_HANDLER` when BOTH have it (node methods take
142
+ precedence on invocation, so the script's would never run). Prefix handlers
143
+ with `on` and the question never comes up.
142
144
  - Handlers receive the EMITTED args only (e.g. the other body for `triggerEnter`) — not
143
145
  the emitting node. For per-emitter logic, attach a small behavior to the emitter itself
144
146
  (see the `Pickup` pattern: a coin's own `triggerEnter` → its own `onTaken` → `queueFree`).
@@ -166,7 +168,9 @@ Child of a `CharacterBody2D` (hard error otherwise):
166
168
  - Defaults: `mode 'platformer'`, `maxSpeed 220`, `jumpHeight 64`, `moveAction 'move'`,
167
169
  `jumpAction 'jump'`. Timer defaults: `waitTime 1`, `oneShot false`, `autostart false`.
168
170
  - ⚠️ Connection handler names must not collide with Node API methods (`emit`, `update`,
169
- `on`, `queueFree`, …) — the NODE method wins silently. Prefix handlers with `on`.
171
+ `on`, `queueFree`, …) — the NODE method wins. That is `AMBIGUOUS_HANDLER` at load
172
+ now, not a silent wrong call; `incanto check` warns on the same pair from the
173
+ file alone. Prefix handlers with `on`.
170
174
 
171
175
  Reference: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) — engine nodes + one small PlayerControl behavior
172
176
  (`CoinCounter`, `Pickup`) wired entirely through JSON connections. Verified in Chromium.
@@ -173,7 +173,9 @@ ALWAYS confirm in the browser (`bun run dev`) that the feet touch the surface.
173
173
  Camera `position` is the view CENTER. With no camera, the view shows (0,0)–(w,h).
174
174
 
175
175
  ### `Label`
176
- `text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'`.
176
+ `text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'`,
177
+ `opacity 1` (0..1, like `Sprite2D`/`ColorRect2D` — a `Label` can fade now, which is
178
+ what makes `FloatAway` work on one).
177
179
  CanvasTexture-rendered; mutate `label.text` freely (re-rasterizes only on change).
178
180
 
179
181
  ### `UILayer`
@@ -250,14 +252,21 @@ listing the valid set. With a viewport design, UI coordinates are design px.
250
252
  The input map is CLEARED by the swap, which matters for a headless drive: an
251
253
  injected `setActionVector` does not carry into the next level — set it again
252
254
  after the transition, the same way a player's held key is re-read.
255
+
256
+ So is the CLOCK: `time`, `unscaledTime` and `timeScale` all start over. That
257
+ last one is why the game-over recipe below is safe — a swap out of a frozen
258
+ screen boots the next level running, not frozen.
253
259
  - **Game over / restart**: swap to a fresh load of the SAME JSON —
254
260
  `engine.setScene(loadScene(levelJson))`. `loadScene` treats the JSON as
255
261
  read-only (everything it keeps is cloned), so reloading the same imported
256
262
  object yields a clean run; only clone (`structuredClone(levelJson)`) first if
257
263
  your own code mutated that object. (`createGame`'s `scene` option already
258
264
  structuredClones internally.)
259
- - **Pause / resume**: `engine.stop()` halts the loop (clock and accumulator
260
- reset — no banked time on resume); `engine.start()` resumes.
265
+ - **Pause / resume**: `engine.timeScale = 0` is the pause you want — the loop
266
+ keeps running, so menus, banners and the key that closes them stay live (this
267
+ is what `GameFlow.pause()` does). `engine.stop()` halts the loop entirely
268
+ (clock and accumulator reset — no banked time on resume); `engine.start()`
269
+ resumes. Either way a `setScene` puts `timeScale` back to 1.
261
270
  - **Save / load** (a behavior, localStorage):
262
271
  ```ts
263
272
  localStorage.setItem('save', JSON.stringify({ score: this.score }));
@@ -104,7 +104,7 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
104
104
  changed. A prop is rewritten only if its type declares `nodePath: true`, so a
105
105
  text prop that merely reads "Player" is never touched. `+` adds a child of any registered
106
106
  type, `✕` deletes the selection. Selecting the ⚙ scene row edits the header
107
- (dimension, gravity with real inputs, environment/input/assets/multiplayer).
107
+ (dimension, gravity with real inputs, environment/input/assets/multiplayer/strings).
108
108
  - **CONNECTIONS on the selected node** — the inspector's last section shows what
109
109
  the node **emits** and what it **receives**, each row being signal → target →
110
110
  handler with a ✕ to remove and `+ connect` to add. Signals come from the node
@@ -315,6 +315,15 @@ toward `target.position + offset`. Works in 2D and 3D.
315
315
  "script": { "name": "FollowCamera", "props": { "target": "/root/Player", "smoothing": 0.9 } } }
316
316
  ```
317
317
 
318
+ **Not together with `CharacterController3D`.** That controller drives the
319
+ scene's current `Camera3D` itself, in every one of its views — so a
320
+ `FollowCamera` on the same camera is a second writer, and the two fight for the
321
+ position every frame. Measured on a scene with both, target at (10, 0, 0): the
322
+ camera settled at (6.59, 0.22, 1.36), which is neither answer, with no error
323
+ anywhere. Use the controller's own `view` / `camDistance` for a third-person
324
+ camera, and keep `FollowCamera` for the cameras nothing else drives.
325
+ `incanto check` reports the pair.
326
+
318
327
  ## Patrol
319
328
 
320
329
  Walk a node along fixed waypoints at constant `speed` — guards, moving platforms.
@@ -822,7 +831,9 @@ import { CameraShake, Cooldown, hitStop, screenFlash } from 'incanto/gameplay';
822
831
  - **hitStop(engine, seconds?)** — freezes `engine.timeScale` for REAL
823
832
  seconds then restores; stacked calls extend. Sells melee impacts. It measures
824
833
  those seconds on `engine.unscaledTime`, so it thaws in a headless session
825
- exactly as it does in a browser.
834
+ exactly as it does in a browser, and a `setScene` under a live freeze drops it
835
+ (the swap resets that clock, and the killing blow — hit-stop, then restart —
836
+ is exactly when the two meet).
826
837
  - **engine.timeScale** — 1 realtime, 0.5 slow motion, 0 pause: scales
827
838
  variable AND fixed updates together (physics, timers, behaviors) — and it
828
839
  means the same thing headless, so a pause menu and a slow-motion finish can be
@@ -848,6 +859,13 @@ puff, a 2D `Label`. It reads the start position ONCE, so the ease does not
848
859
  compound, and it frees the node a frame after it is invisible rather than on the
849
860
  frame it gets there (which reads as a flicker).
850
861
 
862
+ **`opacity` is not optional.** A node without one still rises and still frees
863
+ itself, and skips the fade entirely — the number pops out at full brightness,
864
+ which is the thing this behavior exists to prevent. It says so at ready, naming
865
+ the node, rather than leaving you to spot a missing fade by eye. (The 2D `Label`
866
+ in the list above is the reason: it had no `opacity` prop until 0.57, so the one
867
+ 2D node the doc named was the one it could not fade.)
868
+
851
869
  ## Did the effect fire?
852
870
 
853
871
  Sound has `engine.audio`; vision has **`engine.effects`**, and the question is
@@ -933,6 +951,11 @@ Terminal states ignore further transitions; the restart action (or
933
951
  physics, input. Listen to the `flowChanged(state)` signal for custom UI.
934
952
  `restartScene(engine)` is exported standalone.
935
953
 
954
+ **Any scene swap thaws.** The freeze lives on the engine, not on the flow, so
955
+ leaving a game-over screen by your own route — `engine.setScene(loadScene(next))`
956
+ from a menu button — puts `timeScale` back to 1 as surely as `flow.restart()`
957
+ does. You cannot restart into a frozen level.
958
+
936
959
  Multi-scene games: `flow.goToScene(nextSceneJson, { fadeSeconds: 0.4 })`
937
960
  fades to black, swaps, fades back (headless = instant). Title → level →
938
961
  next level is three JSON files and this one call.
@@ -225,9 +225,11 @@ slot, the slot takes it. **Who owns the item is your game's business** — these
225
225
  report the GESTURE, not a model, so an inventory that stacks, swaps or refuses is
226
226
  your `connections` and a behavior, not a prop nobody could have guessed.
227
227
 
228
- A drop on a CHILD of a slot counts as a drop on the slot (the cursor lands on the
229
- icon inside it, which is the normal case), a drop on nothing cancels, and a
230
- widget never drops onto itself.
228
+ A drop on a CHILD of a slot counts as a drop on the slot — the cursor lands on
229
+ the icon inside it, which is the normal case, and the icon is itself a widget.
230
+ A drop on nothing cancels, and so does putting a thing back into the slot it
231
+ came from: that is not a move, and firing `dropped` for it would make every
232
+ mis-grab look like a transfer.
231
233
 
232
234
 
233
235
 
@@ -99,6 +99,37 @@ banner.show(this.engine.t('hud.wave', { n: this.wave }));
99
99
  slot; a scene-JSON prop has no params to fill it from. A slot with no matching
100
100
  param is left alone rather than blanked.
101
101
 
102
+ ### Authoring the table
103
+
104
+ The scene header's **advanced** section has a `strings` field — the same
105
+ raw-JSON editor `input` and `multiplayer` use. Until 0.61 it was the one of
106
+ those three the panel did not show, so a localized game could be read by the
107
+ editor and never authored in it.
108
+
109
+ ## Checking a translation without a browser
110
+
111
+ No headless check could see a translated string: a capture printed
112
+ `text="@t:menu.start"`, byte-identical in every language, so a green report
113
+ proved nothing. Two things fix that.
114
+
115
+ `runScript` takes a **`locale`** — the whole run happens in that language:
116
+
117
+ ```ts
118
+ const ko = await runScript(sceneJson, { durationMs: 2000, locale: 'ko' });
119
+ ```
120
+
121
+ And a capture records what a widget actually **paints**, beside the prop that
122
+ produced it:
123
+
124
+ ```
125
+ /Root/Hud/Start UiText paints="시작" text="@t:menu.start"
126
+ ```
127
+
128
+ `paints=` appears only when the words differ from the prop, so a game that
129
+ localizes nothing sees no extra noise. Put a `locale` run in your `verify.ts`
130
+ next to the English one and a broken translation fails the harness instead of
131
+ waiting for someone to open the page.
132
+
102
133
  ## The language picker
103
134
 
104
135
  One node. Do not build your own.
@@ -482,6 +482,7 @@ Signals: `movementStateChanged(state)`
482
482
  | `color` | `"#ffffff"` | string |
483
483
  | `font` | `"monospace"` | string |
484
484
  | `align` | `"left"` | string |
485
+ | `opacity` | `1` | number |
485
486
 
486
487
  ## `Label3D` — `incanto`
487
488
 
@@ -299,7 +299,11 @@ that do exist at the failing spot).
299
299
  - `signal` must be DECLARED on the from node (the class's or its behavior's
300
300
  `static signals`) — validated at load → `UNKNOWN_SIGNAL` otherwise.
301
301
  - `handler` must be a method on the target node OR its behavior (script) — both are
302
- validated hard at load. Otherwise → `UNKNOWN_HANDLER`. Script names themselves must be
302
+ validated hard at load. Otherwise → `UNKNOWN_HANDLER`. On **both**, it is
303
+ `AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
304
+ and nothing would say so. A core node answers to 62 public methods before any
305
+ adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
306
+ is why the collision is easy to write — rename the script's method. Script names themselves must be
303
307
  registered (`registerBehavior`) before `loadScene` → `UNKNOWN_BEHAVIOR` otherwise.
304
308
  - Unresolvable `from`/`to` → `DANGLING_CONNECTION` at load. Renaming a node breaks its
305
309
  connections **loudly** — update paths in the same edit.
@@ -336,6 +340,7 @@ that do exist at the failing spot).
336
340
  | `DUPLICATE_UNIQUE_NAME` | `%Name` matches ≥2 nodes | rename one, or use an explicit path |
337
341
  | `DANGLING_CONNECTION` | connection `from`/`to` unresolvable | fix the path after renames |
338
342
  | `UNKNOWN_HANDLER` | handler missing on the node AND its behavior | fix the method name |
343
+ | `AMBIGUOUS_HANDLER` | handler is a method on the node AND on its behavior — the node's wins silently | rename the behavior's method |
339
344
  | `UNKNOWN_SIGNAL` | connection (or emit/on) names a signal the from node never declares | use a declared signal, or declare it (`static signals` / `declareSignal`) |
340
345
  | `UNKNOWN_BEHAVIOR` | `script.name` not registered | `registerBehavior(name, Class)` before `loadScene` |
341
346
  | `UNRESOLVED_INSTANCE` | no resolver, unknown path, or instance cycle | provide/fix `resolveScene`, break the cycle |
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.59.0",
17
+ "incanto": "^0.61.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.59.0",
14
+ "incanto": "^0.61.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.59.0",
14
+ "incanto": "^0.61.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.59.0",
16
+ "incanto": "^0.61.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.59.0",
16
+ "incanto": "^0.61.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -1,77 +0,0 @@
1
- import { a as Rng } from "./schema-CFeioQRE.js";
2
-
3
- //#region src/core/particle-sim.d.ts
4
- /**
5
- * Deterministic particle pool — pure math, renderer-agnostic, headless-
6
- * testable. 2D uses the xy plane (y-down, directionDeg 0 = +x, -90 = up);
7
- * 3D feeds the same sim with a z spread.
8
- */
9
- interface ParticleSimConfig {
10
- /** Particles per second (0 = burst-only). */
11
- rate: number;
12
- /** Seconds, [min, max]. */
13
- lifetime: [number, number];
14
- /** Initial speed, [min, max] (units/sec — px in 2D, meters in 3D). */
15
- speed: [number, number];
16
- /** Emission direction center, degrees (0 = +x, -90 = up in y-down 2D). */
17
- directionDeg: number;
18
- /** Cone width, degrees (360 = all directions). */
19
- spreadDeg: number;
20
- /** Constant acceleration (y-down in 2D). */
21
- gravity: [number, number, number];
22
- /** Exponential velocity damping per second (0 = none). */
23
- drag: number;
24
- maxParticles: number;
25
- /** Spread emission into the z axis too (3D). */
26
- spreadZ?: boolean;
27
- }
28
- interface ParticleView {
29
- x: number;
30
- y: number;
31
- z: number;
32
- vx: number;
33
- vy: number;
34
- vz: number;
35
- /** Seconds alive. */
36
- age: number;
37
- /** Total lifetime in seconds. */
38
- life: number;
39
- /** age/life in [0, 1] — drives size/color/alpha ramps. */
40
- t: number;
41
- /** Per-particle random in [0, 1) — stable for the particle's lifetime. */
42
- seed: number;
43
- }
44
- declare class ParticleSim {
45
- private readonly config;
46
- private readonly rng;
47
- private readonly data;
48
- private alive;
49
- private spawnAccumulator;
50
- private everSpawned;
51
- constructor(config: ParticleSimConfig, rng: Rng);
52
- /**
53
- * Shift every live particle — how world-space emission is done.
54
- *
55
- * The pool is in the emitter's LOCAL space, so a moving emitter drags its
56
- * whole plume with it: dust glued to a running player instead of left behind,
57
- * and the documented "move the emitter and replay" recipe for a one-shot
58
- * teleporting the previous explosion across the level. Counter-translating by
59
- * the emitter's own motion each frame leaves the particles where they were
60
- * born, exactly, and costs one pass over the live ones.
61
- */
62
- translateAll(dx: number, dy: number, dz: number): void;
63
- /** Change the emission rate LIVE (particles/sec; 0 pauses emission) — lets an
64
- * emitter toggle on/off at runtime (drift smoke, throttle flames). */
65
- setRate(rate: number): void;
66
- get count(): number;
67
- /** True when nothing is alive and at least one particle has ever spawned. */
68
- get done(): boolean;
69
- /** Spawn n particles immediately (fireworks, explosions, flashes). */
70
- burst(n: number): void;
71
- update(dt: number): void;
72
- forEach(fn: (p: ParticleView) => void): void;
73
- private spawn;
74
- private kill;
75
- }
76
- //#endregion
77
- export { ParticleSimConfig as n, ParticleView as r, ParticleSim as t };