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.
- package/bin/incanto-check.mjs +58 -4
- package/dist/2d.d.ts +22 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +5 -5
- package/dist/3d.js +5 -4
- package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
- package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
- package/dist/{create-game-CHDLDQsQ.js → create-game-DH7JI5xx.js} +76 -11
- package/dist/{create-game-BiW8Men_.js → create-game-IX5lEH0P.js} +21 -294
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/{duplicate-DJQd44CD.js → duplicate-CGqAmK2h.js} +1 -1
- package/dist/editor.js +2 -1
- package/dist/{environment-presets-DRAz5EV9.js → environment-presets-CNxCuhZF.js} +48 -13
- package/dist/{gameplay-BBEjPFsR.js → gameplay-Dtzd2itW.js} +53 -11
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.js +7 -7
- package/dist/{loader-D8n7TU8W.js → loader-D7jTvDQv.js} +797 -2
- package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
- package/dist/{physics-2d-BaRSRrrZ.js → physics-2d-CBnor8Zf.js} +2 -2
- package/dist/{physics-3d-CYxjh-HW.js → physics-3d-BTUfUSWO.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-BpFcgdcL.js → register-DL3izw8j.js} +46 -9
- package/dist/{register-CDrAQqPp.js → register-xuSRyD6b.js} +119 -21
- package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
- package/dist/{replay-O-yAGM76.d.ts → replay-C5x2vPF5.d.ts} +16 -3
- package/dist/{replay-CEPyQtF_.js → replay-D7-yle3s.js} +56 -205
- package/dist/{split-screen-DDMZutQ6.js → split-screen-5Ban4q4n.js} +3 -3
- package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
- package/dist/{src-CY21B462.js → src-DozXvyZS.js} +1 -1
- package/dist/{teardown-RApWnM1G.js → teardown-B6rwJOyS.js} +1 -1
- package/dist/{test-DHYuFyAu.js → test-Dch_7VQD.js} +19 -14
- package/dist/test.d.ts +24 -4
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-BoRGtVxK.js → agent8-CF1JL2tR.js} +1 -1
- package/editor/assets/{debug-CzdyCg75.js → debug-3QzYhOPA.js} +1 -1
- package/editor/assets/{index-VesuVEhe.js → index-CAD2c5ug.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +4 -0
- package/skills/incanto-3d-models.md +1 -1
- package/skills/incanto-assets.md +10 -1
- package/skills/incanto-audio.md +21 -13
- package/skills/incanto-behaviors-and-scripts.md +7 -3
- package/skills/incanto-building-2d-games.md +12 -3
- package/skills/incanto-editor.md +1 -1
- package/skills/incanto-gameplay-behaviors.md +24 -1
- package/skills/incanto-hud.md +5 -3
- package/skills/incanto-localization.md +31 -0
- package/skills/incanto-node-reference.md +1 -0
- package/skills/incanto-scene-json-authoring.md +6 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
- 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-
|
|
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
|
@@ -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` (
|
|
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 —
|
package/skills/incanto-assets.md
CHANGED
|
@@ -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
|
|
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
|
package/skills/incanto-audio.md
CHANGED
|
@@ -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`**
|
|
142
|
-
non-looping `src` clip ends
|
|
143
|
-
|
|
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 (
|
|
182
|
+
## 2b. Spatial (positional) audio
|
|
179
183
|
|
|
180
|
-
Set **`spatial: true`** on an `AudioPlayer`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
The
|
|
184
|
-
moving the emitter or the camera updates the sound live. Works
|
|
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
|
|
218
|
-
>
|
|
219
|
-
>
|
|
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
|
|
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
|
|
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.
|
|
260
|
-
|
|
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 }));
|
package/skills/incanto-editor.md
CHANGED
|
@@ -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.
|
package/skills/incanto-hud.md
CHANGED
|
@@ -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
|
|
229
|
-
icon inside it, which is the normal case
|
|
230
|
-
|
|
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.
|
|
@@ -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`.
|
|
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 |
|
|
@@ -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 };
|