incanto 0.68.0 → 0.70.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/assets/catalog.json +9 -5
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +19 -5
- package/bin/incanto-check.mjs +92 -15
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +128 -5
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +165 -44
- package/bin/incanto.mjs +4 -2
- package/dist/2d.d.ts +285 -36
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +158 -10
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
- package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-DaMxqfNE.d.ts} +33 -14
- package/dist/{behavior-DoFPYrgo.d.ts → behavior-DZExDn9o.d.ts} +809 -44
- package/dist/{create-game-IZIydDwI.js → create-game-Bwvh6q8A.js} +148 -61
- package/dist/{create-game-DbWtVTxD.js → create-game-C7ffQWW7.js} +103 -49
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1066 -864
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-CybQXNqS.js → environment-presets-D6Q5BxeE.js} +299 -46
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-DM1eu_cV.js → gameplay-BfHkuzVb.js} +825 -221
- package/dist/gameplay.d.ts +187 -7
- package/dist/gameplay.js +1 -1
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +156 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-CcB533FR.d.ts → loader-Cff09LMm.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CE0Qvy3V.js} +136 -11
- package/dist/{physics-3d-bG3n70Ky.js → physics-3d-CpH-2gn5.js} +104 -23
- package/dist/{teardown-D2NEmxPB.js → picking-CQJ_PJKh.js} +106 -14
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-3ta-2Xig.js → register-6DYnKZcy.js} +652 -831
- package/dist/{register-ibCjm-wH.js → register-Bkk0wSDB.js} +348 -30
- package/dist/{replay-CYvhVHHN.js → replay-DjAkAzMq.js} +224 -14
- package/dist/{replay-Dvn8aeBd.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-BC4PNtJX.js → save-slots-BXVg148r.js} +4558 -2294
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-DhrSzZIB.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
- package/dist/{split-screen-CYwDkbLF.js → split-screen-PL78oVXP.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-D7RIqXYF.js → src-Cxfiv1Hg.js} +2 -17
- package/dist/test-iHYVUcDK.js +4036 -0
- package/dist/test.d.ts +542 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +54 -3
- package/dist/vite.js +349 -17
- package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-C8aDbGmR.js} +56 -1
- package/editor/assets/agent8-D0MS174y.js +1 -0
- package/editor/assets/{debug-BBhKuBNV.js → debug-BnXkKuYu.js} +2 -2
- package/editor/assets/index-CIu3uc3l.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +7 -16
- package/schemas/scene.schema.json +30 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +25 -3
- package/skills/incanto-audio.md +27 -5
- package/skills/incanto-behaviors-and-scripts.md +83 -6
- package/skills/incanto-building-2d-games.md +106 -8
- package/skills/incanto-building-3d-games.md +118 -6
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +121 -14
- package/skills/incanto-hud.md +128 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +222 -58
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +78 -2
- package/skills/incanto-save-slots.md +188 -6
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +292 -12
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +5 -2
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
- package/templates-app/beacon-isle-3d/src/main.ts +24 -4
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1255 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +143 -625
- package/templates-app/platformer-2d/src/main.ts +35 -13
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +41 -195
- package/templates-app/star-survivor/src/main.ts +28 -7
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +78 -217
- package/templates-app/tps-3d/src/main.ts +39 -17
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
- package/templates-app/village-quest-3d/src/main.ts +24 -4
- package/templates-app/village-quest-3d/src/village.scene.json +199 -838
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-IWIqk0HJ.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-8hoHeRmo.js +0 -2340
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BrrHOjMJ.js +0 -1
- package/editor/assets/index-eVd0BToA.js +0 -10958
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as SceneJson, c as JsonValue, o as JsonKind, s as JsonObject, t as Rng } from "./rng-Bb-IutXB.js";
|
|
2
2
|
|
|
3
3
|
//#region src/core/signal.d.ts
|
|
4
4
|
/**
|
|
@@ -85,6 +85,43 @@ declare class SceneTree {
|
|
|
85
85
|
*/
|
|
86
86
|
private readonly _nameIndex;
|
|
87
87
|
/**
|
|
88
|
+
* group → the ATTACHED nodes in it, maintained the way names are.
|
|
89
|
+
*
|
|
90
|
+
* `getNodesInGroup` was an un-indexed full-tree walk that allocated a fresh
|
|
91
|
+
* array per call, while the sibling name lookup was made O(1) for exactly
|
|
92
|
+
* this reason. The one engine behavior that calls it every frame —
|
|
93
|
+
* `FaceTarget`, the documented tower-defence turret — therefore cost
|
|
94
|
+
* turrets x TOTAL SCENE NODES per frame: a 40-turret map in a 5,000-node
|
|
95
|
+
* world walked 200,000 nodes a frame to find 40 of them, and allocated 40
|
|
96
|
+
* arrays doing it.
|
|
97
|
+
*/
|
|
98
|
+
private readonly _groupIndex;
|
|
99
|
+
/**
|
|
100
|
+
* uid → node, maintained the way names are.
|
|
101
|
+
*
|
|
102
|
+
* `getNodeByUid` was a depth-first walk of the whole tree, and `restoreState`
|
|
103
|
+
* calls it once per saved uid plus once per freed uid — so loading a save
|
|
104
|
+
* was `nodes x saved-keys`. A 3,000-node world with 200 saved behaviors
|
|
105
|
+
* walked 600,000 nodes to hand back 200 values, on the frame the player is
|
|
106
|
+
* already waiting through a level load.
|
|
107
|
+
*/
|
|
108
|
+
private readonly _uidIndex;
|
|
109
|
+
/**
|
|
110
|
+
* Templates a spawner has detached, by the path they were detached FROM.
|
|
111
|
+
*
|
|
112
|
+
* A spawner takes its prefab out of the live tree so it stops ticking. The
|
|
113
|
+
* second spawner pointing at the same template then found nothing and
|
|
114
|
+
* hard-failed the scene:
|
|
115
|
+
*
|
|
116
|
+
* Spawner on '/World/B': "prefab" '../Coin' does not resolve to a template
|
|
117
|
+
*
|
|
118
|
+
* One shared coin between two spawners is an ordinary thing to author, so the
|
|
119
|
+
* tree remembers where each template went. Scene-scoped, like everything
|
|
120
|
+
* else here — a new scene starts with none.
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
readonly _templates: Map<string, Node>;
|
|
124
|
+
/**
|
|
88
125
|
* Bumped whenever ANY node attaches or detaches — cheap "did the tree
|
|
89
126
|
* change shape?" check for per-step body gathers and similar caches.
|
|
90
127
|
*/
|
|
@@ -127,6 +164,14 @@ declare class SceneTree {
|
|
|
127
164
|
get frameId(): number;
|
|
128
165
|
/** @internal Node attach hook. */
|
|
129
166
|
_indexName(node: Node): void;
|
|
167
|
+
/** @internal A uid was assigned or changed on an ATTACHED node. */
|
|
168
|
+
_reindexUid(node: Node, oldUid: string | null): void;
|
|
169
|
+
/** @internal The node carrying this uid, or undefined. */
|
|
170
|
+
_nodeWithUid(uid: string): Node | undefined;
|
|
171
|
+
/** @internal A node joined a group, or arrived already in one. */
|
|
172
|
+
_indexGroup(node: Node, group: string): void;
|
|
173
|
+
/** @internal A node left a group, or left the tree. */
|
|
174
|
+
_unindexGroup(node: Node, group: string): void;
|
|
130
175
|
/**
|
|
131
176
|
* @internal A RENAME: same node, same place, different key in the name index.
|
|
132
177
|
*
|
|
@@ -151,6 +196,14 @@ declare class SceneTree {
|
|
|
151
196
|
/** @internal */
|
|
152
197
|
_setEngine(engine: Engine | null): void;
|
|
153
198
|
setRoot(node: Node): void;
|
|
199
|
+
/**
|
|
200
|
+
* Has the initial load finished?
|
|
201
|
+
*
|
|
202
|
+
* `false` through `setRoot`'s enter/ready pass — where a throw is a load
|
|
203
|
+
* error and must stay one — and `true` for everything after it.
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
_live: boolean;
|
|
154
207
|
update(dt: number): void;
|
|
155
208
|
fixedUpdate(dt: number): void;
|
|
156
209
|
getNodesInGroup(group: string): Node[];
|
|
@@ -180,12 +233,6 @@ interface NodeLifecycle {
|
|
|
180
233
|
update(dt: number): void;
|
|
181
234
|
fixedUpdate(dt: number): void;
|
|
182
235
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Base class of everything in an Incanto scene (Godot's Node model).
|
|
185
|
-
*
|
|
186
|
-
* Pure data + tree structure: no rendering, no DOM, no three.js — renderer
|
|
187
|
-
* adapters subscribe from the outside.
|
|
188
|
-
*/
|
|
189
236
|
declare class Node implements NodeLifecycle {
|
|
190
237
|
static readonly typeName: string;
|
|
191
238
|
/**
|
|
@@ -198,9 +245,49 @@ declare class Node implements NodeLifecycle {
|
|
|
198
245
|
private _name;
|
|
199
246
|
private _parent;
|
|
200
247
|
private readonly _children;
|
|
248
|
+
/**
|
|
249
|
+
* child name → how many children carry it. Built on first use, then
|
|
250
|
+
* MAINTAINED — the whole reason this exists.
|
|
251
|
+
*
|
|
252
|
+
* `uniqueSiblingName` used to build a fresh `Set` of every existing sibling's
|
|
253
|
+
* name on every insert, BEFORE asking whether the requested name was even
|
|
254
|
+
* taken. Filling one parent therefore costs K(K+1)/2 Set operations, and a
|
|
255
|
+
* single attach into a horde of 8,000 cost about thirteen times what
|
|
256
|
+
* simulating that whole tree for a frame costs. A spawner-driven game gets
|
|
257
|
+
* slower the longer it is played, at the moment it is busiest.
|
|
258
|
+
*
|
|
259
|
+
* The three places `_children` changes are the three places this changes:
|
|
260
|
+
* `addChild`, `removeChild` and the `name` setter. `_children` is private to
|
|
261
|
+
* this file, so that list is the whole list.
|
|
262
|
+
*/
|
|
263
|
+
private _childNames;
|
|
264
|
+
/**
|
|
265
|
+
* stem → the next trailing number worth TRYING. A hint, never the truth.
|
|
266
|
+
*
|
|
267
|
+
* The name index alone left a second quadratic in the same function: eight
|
|
268
|
+
* thousand children all called `Enemy` means eight thousand collisions, and
|
|
269
|
+
* `while (taken.has(stem + counter)) counter += 1` restarted at 2 every time.
|
|
270
|
+
* Measured, 4x the children still cost 16.7x the time. Starting the scan
|
|
271
|
+
* where the last one finished makes the whole fill linear; the `while` still
|
|
272
|
+
* runs, so a wrong hint costs a step and never a wrong name.
|
|
273
|
+
*/
|
|
274
|
+
private _nameCounters;
|
|
201
275
|
private readonly _groups;
|
|
202
276
|
private readonly _signals;
|
|
277
|
+
/**
|
|
278
|
+
* Disconnects for connections this node made on OTHER nodes.
|
|
279
|
+
*
|
|
280
|
+
* Null until it makes one — most nodes never do, and an empty array per node
|
|
281
|
+
* is a real cost in a scene with thousands of them.
|
|
282
|
+
* @internal
|
|
283
|
+
*/
|
|
284
|
+
_outgoing: Array<() => void> | null;
|
|
203
285
|
private _declared;
|
|
286
|
+
/**
|
|
287
|
+
* signal → (the listener a caller gave us) → (the quarantining wrapper Signal
|
|
288
|
+
* actually holds). Null until this node has a listener, which most never do.
|
|
289
|
+
*/
|
|
290
|
+
private _wrapped;
|
|
204
291
|
private _tree;
|
|
205
292
|
private _ready;
|
|
206
293
|
/**
|
|
@@ -208,7 +295,9 @@ declare class Node implements NodeLifecycle {
|
|
|
208
295
|
* among siblings) a uid is unique across the whole scene, so scripts and
|
|
209
296
|
* tools can address a node no matter where it moves.
|
|
210
297
|
*/
|
|
211
|
-
|
|
298
|
+
private _uid;
|
|
299
|
+
get uid(): string | null;
|
|
300
|
+
set uid(value: string | null);
|
|
212
301
|
/** Free-form JSON identity for game logic (e.g. `{kind: 'ITEM', value: 10}`). */
|
|
213
302
|
tags: Record<string, unknown>;
|
|
214
303
|
/** Behavior attachment blob from scene JSON (resolved by the loader). */
|
|
@@ -227,6 +316,9 @@ declare class Node implements NodeLifecycle {
|
|
|
227
316
|
get tree(): SceneTree | null;
|
|
228
317
|
/** Whether onReady has run (it runs at most once per instance). */
|
|
229
318
|
get isReady(): boolean;
|
|
319
|
+
/** The sibling-name index, built once per parent and then kept up to date. */
|
|
320
|
+
private childNames;
|
|
321
|
+
private nameCounters;
|
|
230
322
|
addChild<T extends Node>(child: T): T;
|
|
231
323
|
removeChild(child: Node): void;
|
|
232
324
|
reparent(newParent: Node): void;
|
|
@@ -238,8 +330,33 @@ declare class Node implements NodeLifecycle {
|
|
|
238
330
|
/** Every node in THIS subtree named `name` — names repeat, so a list. */
|
|
239
331
|
getNodesByName(name: string): Node[];
|
|
240
332
|
getPath(): string;
|
|
333
|
+
/**
|
|
334
|
+
* The path of the AUTHORED node this one stands for — `getPath()` with every
|
|
335
|
+
* cloned ancestor's auto-renamed name put back (`/Game/Foe37/Fx` →
|
|
336
|
+
* `/Game/Foe/Fx`).
|
|
337
|
+
*
|
|
338
|
+
* "Is this AudioPlayer wired?" is a question about the scene, and a wave of
|
|
339
|
+
* forty enemies is one emitter fired forty times, not forty emitters. The
|
|
340
|
+
* feedback sets ask it, and answering with the live path made a set the
|
|
341
|
+
* engine promised would "grow with the SCENE, not with the run" grow by one
|
|
342
|
+
* entry per spawn, forever, in every shipped game.
|
|
343
|
+
*
|
|
344
|
+
* The event LOGS keep the live path: a log names the node that acted.
|
|
345
|
+
*/
|
|
346
|
+
wiringPath(): string;
|
|
241
347
|
getNode(path: string): Node;
|
|
242
348
|
getNodeOrNull(path: string): Node | null;
|
|
349
|
+
/**
|
|
350
|
+
* `parseNodePath`, but the error says WHERE it was written.
|
|
351
|
+
*
|
|
352
|
+
* Every other scene-load error appends the offending node — `NODE_NOT_FOUND`
|
|
353
|
+
* lists the children that do exist, an unknown type ends `(at '/Level')` —
|
|
354
|
+
* and the six `BAD_NODE_PATH` messages were the only family in the engine
|
|
355
|
+
* that named neither the node nor anything greppable. A bad path in a
|
|
356
|
+
* behavior prop printed `[BAD_NODE_PATH] Node path must not be empty.` and
|
|
357
|
+
* nothing else, so bisecting the scene was the only way to find it.
|
|
358
|
+
*/
|
|
359
|
+
private parseFrom;
|
|
243
360
|
private resolve;
|
|
244
361
|
addToGroup(group: string): void;
|
|
245
362
|
removeFromGroup(group: string): void;
|
|
@@ -254,9 +371,36 @@ declare class Node implements NodeLifecycle {
|
|
|
254
371
|
signal(name: string): Signal<unknown[]>;
|
|
255
372
|
on(signal: string, fn: SignalListener<unknown[]>, opts?: {
|
|
256
373
|
once?: boolean;
|
|
374
|
+
owner?: Node;
|
|
257
375
|
}): () => void;
|
|
258
376
|
off(signal: string, fn: SignalListener<unknown[]>): void;
|
|
259
377
|
/**
|
|
378
|
+
* Wrap a listener so ITS throw stays ITS problem.
|
|
379
|
+
*
|
|
380
|
+
* `Signal.emit` calls listeners directly, so an exception from one unwinds
|
|
381
|
+
* into whoever EMITTED. The node quarantine then blames and disables the
|
|
382
|
+
* emitter's script, and every OTHER listener on that signal is skipped for
|
|
383
|
+
* the rest of the emit. Measured on the same broken handler, two wirings:
|
|
384
|
+
*
|
|
385
|
+
* ```
|
|
386
|
+
* wired with node.on() emitter ticks 1/10 emitter script DISABLED
|
|
387
|
+
* wired with JSON connections emitter ticks 10/10 emitter script fine
|
|
388
|
+
* ```
|
|
389
|
+
*
|
|
390
|
+
* The JSON half has been quarantined since `quarantineWire` shipped, with a
|
|
391
|
+
* comment saying why: *"a behavior is your code; the node is the engine's. A
|
|
392
|
+
* typo in the first must not switch off the second."* `.on()` is the same
|
|
393
|
+
* category — it is the spelling every skill teaches and five shipped examples
|
|
394
|
+
* use, including `controller.on('movementStateChanged', …)`, where blaming
|
|
395
|
+
* the emitter stops the player moving entirely. That is verbatim the defect
|
|
396
|
+
* `quarantineWire` was written to prevent, reached through the other door.
|
|
397
|
+
*
|
|
398
|
+
* The owner node is NOT marked `errored`: its own update is fine, only this
|
|
399
|
+
* wire is off. That is exactly what `quarantineWire` does, and marking it
|
|
400
|
+
* would restore the over-punishment this removes.
|
|
401
|
+
*/
|
|
402
|
+
private quarantined;
|
|
403
|
+
/**
|
|
260
404
|
* How many listeners this node has on `signal` — 0 for one nobody watches.
|
|
261
405
|
*
|
|
262
406
|
* The engine's own leak test reads it, and so can a game: a handler connected
|
|
@@ -272,6 +416,29 @@ declare class Node implements NodeLifecycle {
|
|
|
272
416
|
queueFree(): void;
|
|
273
417
|
/** Immediately detach and tear down this node and its children. */
|
|
274
418
|
free(): void;
|
|
419
|
+
/**
|
|
420
|
+
* Run a structural lifecycle hook, quarantining it the way `update` is.
|
|
421
|
+
*
|
|
422
|
+
* Only `update`/`fixedUpdate` were ever quarantined, so a throw from a
|
|
423
|
+
* RUNTIME-created node's `onReady`/`onEnterTree` unwound into the `update()`
|
|
424
|
+
* of whatever created it. Measured on one Spawner scene, 120 steps:
|
|
425
|
+
*
|
|
426
|
+
* control A spawner alone nodes 20, errors 0
|
|
427
|
+
* control B clone throws in UPDATE nodes 20, errors 19, each line
|
|
428
|
+
* correctly named on the clone
|
|
429
|
+
* A + B clone throws in onReady nodes 2, errors 1,
|
|
430
|
+
* log: "behavior 'Spawner' on /World/Spawner threw in update"
|
|
431
|
+
*
|
|
432
|
+
* The innocent Spawner was quarantined — spawning stopped permanently — and
|
|
433
|
+
* the one headline named the wrong node, the wrong script and the wrong hook.
|
|
434
|
+
* An `onExitTree` throw on a runtime `queueFree` was worse still: it escaped
|
|
435
|
+
* `engine.step()` entirely, with `stats().errors: 0` and an EMPTY log,
|
|
436
|
+
* because the free-queue flush is outside behaviour propagation.
|
|
437
|
+
*
|
|
438
|
+
* During the initial load the throw is re-raised: `loadScene` is where an
|
|
439
|
+
* authoring mistake has to hard-fail, and that is stated design.
|
|
440
|
+
*/
|
|
441
|
+
private _runHook;
|
|
275
442
|
/** @internal */
|
|
276
443
|
_propagateEnterTree(tree: SceneTree): void;
|
|
277
444
|
/** @internal */
|
|
@@ -350,9 +517,24 @@ interface AudioEvent {
|
|
|
350
517
|
name: string;
|
|
351
518
|
/** Node path for a node-driven sound; `engine.music` / `engine.sfx` otherwise. */
|
|
352
519
|
from: string;
|
|
520
|
+
/**
|
|
521
|
+
* The AUTHORED node this stands for — `from` with a clone's auto-renamed
|
|
522
|
+
* ancestors put back (`/Game/Foe37/Snd` → `/Game/Foe/Snd`). Absent = same as
|
|
523
|
+
* `from`. Only `sources()` reads it; the log keeps the live path.
|
|
524
|
+
*/
|
|
525
|
+
wiredAs?: string;
|
|
353
526
|
bus: BusName;
|
|
354
527
|
/** `engine.time` when it fired, in seconds. */
|
|
355
528
|
at: number;
|
|
529
|
+
/**
|
|
530
|
+
* It repeats until stopped, rather than ending on its own.
|
|
531
|
+
*
|
|
532
|
+
* A looping sound is recorded ONCE — re-recording every pass would evict the
|
|
533
|
+
* rest of the log within seconds for a 0.2 s preset — so without this a
|
|
534
|
+
* headless check could not tell "the alarm loops" from "the alarm fired once
|
|
535
|
+
* and stopped". Both read `countOf('alarm') === 1`.
|
|
536
|
+
*/
|
|
537
|
+
loop?: boolean;
|
|
356
538
|
}
|
|
357
539
|
declare class AudioBuses {
|
|
358
540
|
/** Fires whenever any value changes (drives live re-gain of playing sounds). */
|
|
@@ -370,10 +552,25 @@ declare class AudioBuses {
|
|
|
370
552
|
* Bounded (200) — a game running for an hour must not grow a log.
|
|
371
553
|
*/
|
|
372
554
|
private readonly events;
|
|
555
|
+
/**
|
|
556
|
+
* EVERY node that has sounded, ever — a set of paths, not a window.
|
|
557
|
+
*
|
|
558
|
+
* `recent()` is bounded at 200, which is right for a log and wrong for the
|
|
559
|
+
* question "is this AudioPlayer wired?". A game with one sound firing about
|
|
560
|
+
* three times a second overflows the window inside a minute, so a 60-second
|
|
561
|
+
* playtest read the tail and reported correctly-wired feedback as never
|
|
562
|
+
* fired. This grows with the SCENE, not with the run — which is true only
|
|
563
|
+
* because it records the AUTHORED path: a clone's own path is new every
|
|
564
|
+
* spawn, and recording those grew the set by one entry per enemy, forever,
|
|
565
|
+
* in every shipped game. See `Node.wiringPath`.
|
|
566
|
+
*/
|
|
567
|
+
private readonly sourcePaths;
|
|
373
568
|
/** @internal Called by the players; games read `recent()`. */
|
|
374
569
|
record(event: AudioEvent): void;
|
|
375
570
|
/** The last `limit` sounds, oldest first. */
|
|
376
571
|
recent(limit?: number): AudioEvent[];
|
|
572
|
+
/** Every node path that has sounded since the last `clearLog()`. */
|
|
573
|
+
sources(): string[];
|
|
377
574
|
/** How many times a preset/clip/track sounded — the assertion you want. */
|
|
378
575
|
countOf(name: string): number;
|
|
379
576
|
/** Forget what has sounded so far (between scenes, or between assertions). */
|
|
@@ -382,6 +579,7 @@ declare class AudioBuses {
|
|
|
382
579
|
private _sfx;
|
|
383
580
|
private _music;
|
|
384
581
|
private _muted;
|
|
582
|
+
private _suspended;
|
|
385
583
|
get master(): number;
|
|
386
584
|
set master(v: number);
|
|
387
585
|
get sfx(): number;
|
|
@@ -390,6 +588,21 @@ declare class AudioBuses {
|
|
|
390
588
|
set music(v: number);
|
|
391
589
|
get muted(): boolean;
|
|
392
590
|
set muted(v: boolean);
|
|
591
|
+
/**
|
|
592
|
+
* Silence the game WITHOUT it counting as the player muting it.
|
|
593
|
+
*
|
|
594
|
+
* `pause-when-hidden` set `muted = true` when the tab went away, which fires
|
|
595
|
+
* `changed`, which `Settings.bindAudio` persists. Close the tab while it is
|
|
596
|
+
* hidden and `muted: true` is on disk: EVERY later session of that game boots
|
|
597
|
+
* silent, and no shipped starter has an in-game way back.
|
|
598
|
+
*
|
|
599
|
+
* A suspension is the engine's, not the player's. It never touches `muted`,
|
|
600
|
+
* so nothing persists it and unsuspending cannot turn the sound back on for
|
|
601
|
+
* someone who turned it off.
|
|
602
|
+
*/
|
|
603
|
+
suspend(on: boolean): void;
|
|
604
|
+
/** Is the engine holding the sound off (hidden tab), as against the player? */
|
|
605
|
+
get suspended(): boolean;
|
|
393
606
|
/** Final gain for a sound on `bus` with its own `sourceVolume` (all clamped). */
|
|
394
607
|
effectiveVolume(bus: BusName, sourceVolume: number): number;
|
|
395
608
|
/** Clamp + finite-guard a new value; emit only on a real change. */
|
|
@@ -425,6 +638,14 @@ interface MusicTrack {
|
|
|
425
638
|
* Optional so an existing custom backend keeps compiling.
|
|
426
639
|
*/
|
|
427
640
|
playhead?(): number | null;
|
|
641
|
+
/**
|
|
642
|
+
* Advance a track that has no clock of its own, by the engine's real dt.
|
|
643
|
+
*
|
|
644
|
+
* Optional, and the WebAudio backend does not implement it: an element's own
|
|
645
|
+
* `currentTime` is the authority whenever there is one. The silent backend
|
|
646
|
+
* does, so a game charted to a soundtrack has a playhead in the verify VM.
|
|
647
|
+
*/
|
|
648
|
+
tick?(dt: number): void;
|
|
428
649
|
}
|
|
429
650
|
/** Backend that mints {@link MusicTrack}s — WebAudio-backed in the browser. */
|
|
430
651
|
interface MusicBackend {
|
|
@@ -783,10 +1004,24 @@ declare class SfxEngine {
|
|
|
783
1004
|
* The AUDIO clock, in seconds — the one a sound is actually scheduled
|
|
784
1005
|
* against, and the only clock that does not drift relative to what you hear.
|
|
785
1006
|
*
|
|
786
|
-
* `0` headless and before the first sound, since there is no context yet.
|
|
787
1007
|
* Schedule with a lead: `sfx.play(params, 1, { when: sfx.now + 0.08 })`.
|
|
1008
|
+
*
|
|
1009
|
+
* With no AudioContext — headless, and before the first sound — it is the
|
|
1010
|
+
* engine's own real elapsed time instead of a frozen `0`. A constant made the
|
|
1011
|
+
* documented lookahead scheduler queue the notes inside the first 80 ms and
|
|
1012
|
+
* then nothing ever again: measured on a 21-beat metronome in the verify VM,
|
|
1013
|
+
* **1 of 21 beats**, with no error and no warning. The silent clock runs so
|
|
1014
|
+
* the schedule is reachable; nothing sounds either way.
|
|
788
1015
|
*/
|
|
789
1016
|
get now(): number;
|
|
1017
|
+
/** The clock `now` reports when there is no context to ask. */
|
|
1018
|
+
private silentNow;
|
|
1019
|
+
/**
|
|
1020
|
+
* @internal Advance the silent clock — the engine calls this every frame with
|
|
1021
|
+
* the REAL dt, beside `music.tick`, because the audio does not slow down when
|
|
1022
|
+
* the game does.
|
|
1023
|
+
*/
|
|
1024
|
+
_tick(dt: number): void;
|
|
790
1025
|
/**
|
|
791
1026
|
* Play a preset at `gain` (0..1, already bus-multiplied by the caller). Each
|
|
792
1027
|
* call spawns its own source so rapid repeats overlap instead of cutting off.
|
|
@@ -827,6 +1062,12 @@ interface EffectEvent {
|
|
|
827
1062
|
name: string;
|
|
828
1063
|
/** Node path, or `engine` for the screen-wide ones. */
|
|
829
1064
|
from: string;
|
|
1065
|
+
/**
|
|
1066
|
+
* The AUTHORED node this stands for — `from` with a clone's auto-renamed
|
|
1067
|
+
* ancestors put back (`/Game/Foe37/Fx` → `/Game/Foe/Fx`). Absent = same as
|
|
1068
|
+
* `from`. Only `sources()` reads it; the log keeps the live path.
|
|
1069
|
+
*/
|
|
1070
|
+
wiredAs?: string;
|
|
830
1071
|
/** Particles in the burst, shake magnitude, seconds frozen — the amount. */
|
|
831
1072
|
amount: number;
|
|
832
1073
|
/** `engine.time` when it fired, in seconds. */
|
|
@@ -834,8 +1075,19 @@ interface EffectEvent {
|
|
|
834
1075
|
}
|
|
835
1076
|
declare class EffectLog {
|
|
836
1077
|
private readonly events;
|
|
1078
|
+
/**
|
|
1079
|
+
* EVERY node that has fired, ever — a set of paths, not a window. See
|
|
1080
|
+
* `AudioBuses.sources`: 200 entries is a log, and "is this emitter wired?"
|
|
1081
|
+
* needs the whole run.
|
|
1082
|
+
*
|
|
1083
|
+
* AUTHORED paths, so it is bounded by the scene: a wave of forty enemies is
|
|
1084
|
+
* one emitter fired forty times. See `Node.wiringPath`.
|
|
1085
|
+
*/
|
|
1086
|
+
private readonly sourcePaths;
|
|
837
1087
|
/** @internal Called by the effects themselves; games read `recent()`. */
|
|
838
1088
|
record(event: EffectEvent): void;
|
|
1089
|
+
/** Every node path that has fired an effect since the last `clearLog()`. */
|
|
1090
|
+
sources(): string[];
|
|
839
1091
|
/** The last `limit` effects, oldest first. */
|
|
840
1092
|
recent(limit?: number): EffectEvent[];
|
|
841
1093
|
/** How many times an effect with this name fired (`explosion`, `#ff0000`). */
|
|
@@ -847,18 +1099,24 @@ declare class EffectLog {
|
|
|
847
1099
|
}
|
|
848
1100
|
//#endregion
|
|
849
1101
|
//#region src/core/input.d.ts
|
|
850
|
-
/**
|
|
851
|
-
* Declarative input from scene JSON `input{}` — DOM-free logic (feed key state
|
|
852
|
-
* via `handleKey`, or wire a browser with `attachKeyboard(window)`).
|
|
853
|
-
*
|
|
854
|
-
* Convention: vector2 follows the 2D y-down space (up = -y). `justPressed` /
|
|
855
|
-
* `justReleased` are one-frame edges, settled by the Engine each tick.
|
|
856
|
-
*/
|
|
857
1102
|
declare class InputMap {
|
|
858
1103
|
private readonly actions;
|
|
859
1104
|
private readonly down;
|
|
860
1105
|
private readonly pressedEdge;
|
|
861
1106
|
private readonly releasedEdge;
|
|
1107
|
+
private readonly pressedEdgeFixed;
|
|
1108
|
+
private readonly releasedEdgeFixed;
|
|
1109
|
+
/** Which pass is asking. Set by the Engine around each phase. */
|
|
1110
|
+
private phase;
|
|
1111
|
+
/**
|
|
1112
|
+
* Actions something has actually ASKED about since this scene loaded.
|
|
1113
|
+
*
|
|
1114
|
+
* Recorded in the four read methods only — never in `get`/`button`, because
|
|
1115
|
+
* `pressAction` goes through those, and the tool that reports this list is
|
|
1116
|
+
* the same tool that injects presses. Marking an action read because a
|
|
1117
|
+
* playtester pressed it would make the check answer its own question.
|
|
1118
|
+
*/
|
|
1119
|
+
private readonly queried;
|
|
862
1120
|
private detach;
|
|
863
1121
|
/** Load (or extend with) scene-JSON action declarations. Hard-validates shape. */
|
|
864
1122
|
declare(decls: Record<string, unknown>): void;
|
|
@@ -869,7 +1127,9 @@ declare class InputMap {
|
|
|
869
1127
|
* This is what a click-driven game asks for. `pointerDelta()` answers "how far
|
|
870
1128
|
* did the mouse move" — the mouse-look question — and for a whole class of
|
|
871
1129
|
* genres that is the wrong question and the only one the engine could answer.
|
|
872
|
-
* Pair it with `
|
|
1130
|
+
* Pair it with `engine.pickAt(x, y)` for the node under the cursor and
|
|
1131
|
+
* `engine.pointerWorld()` for the place — both reachable from a Behavior,
|
|
1132
|
+
* which `renderer.pick` / `renderer.worldFromScreen` are not.
|
|
873
1133
|
*/
|
|
874
1134
|
pointerPosition(): {
|
|
875
1135
|
x: number;
|
|
@@ -886,6 +1146,13 @@ declare class InputMap {
|
|
|
886
1146
|
private readonly injectedDown;
|
|
887
1147
|
private readonly injectedPressed;
|
|
888
1148
|
private readonly injectedReleased;
|
|
1149
|
+
/** The fixed pass's view of the injected edges — see `pressedEdgeFixed`. */
|
|
1150
|
+
private readonly injectedPressedFixed;
|
|
1151
|
+
private readonly injectedReleasedFixed;
|
|
1152
|
+
private readonly injectedPressedNext;
|
|
1153
|
+
private readonly injectedReleasedNext;
|
|
1154
|
+
/** True between the start of a frame's update pass and `endFrame()`. */
|
|
1155
|
+
private inFrame;
|
|
889
1156
|
private readonly injectedVectors;
|
|
890
1157
|
/**
|
|
891
1158
|
* Press a button ACTION directly — no key codes involved. This is how
|
|
@@ -903,6 +1170,20 @@ declare class InputMap {
|
|
|
903
1170
|
name: string;
|
|
904
1171
|
type: "button" | "vector2";
|
|
905
1172
|
}>;
|
|
1173
|
+
/**
|
|
1174
|
+
* Declared actions NOTHING has read — a control the scene promises and no
|
|
1175
|
+
* code implements.
|
|
1176
|
+
*
|
|
1177
|
+
* `village-quest-3d` declared `"restart": { "keys": ["KeyR"] }` and not one
|
|
1178
|
+
* line read it, so the game's own control list said R restarts and R did
|
|
1179
|
+
* nothing. The scene is where controls are DECLARED; whether anything
|
|
1180
|
+
* consumes one is invisible there, and a `grep` for the name cannot tell an
|
|
1181
|
+
* engine node's read from a dead string.
|
|
1182
|
+
*
|
|
1183
|
+
* The list is advisory, like `behaviorsWithoutSave`: an action polled only in
|
|
1184
|
+
* a state a run never reached is on it honestly. Read it, do not gate on it.
|
|
1185
|
+
*/
|
|
1186
|
+
unreadActions(): string[];
|
|
906
1187
|
pressAction(action: string): void;
|
|
907
1188
|
/** Release an injected button action (yields one justReleased frame). */
|
|
908
1189
|
releaseAction(action: string): void;
|
|
@@ -927,7 +1208,21 @@ declare class InputMap {
|
|
|
927
1208
|
private dy;
|
|
928
1209
|
private wheel;
|
|
929
1210
|
/** Mouse buttons feed the same code space as keys: Mouse0/Mouse1/Mouse2. */
|
|
930
|
-
|
|
1211
|
+
/**
|
|
1212
|
+
* A mouse/pen/finger button.
|
|
1213
|
+
*
|
|
1214
|
+
* `kind` matters on the way UP: a finger that lifts CEASES TO EXIST, while a
|
|
1215
|
+
* cursor stays where you left it. Without it the last-tapped node kept
|
|
1216
|
+
* `hovering: true` for the rest of the session — a permanently highlighted
|
|
1217
|
+
* tile, mole or card on every phone, on a device with no hover at all.
|
|
1218
|
+
*
|
|
1219
|
+
* The position is cleared at `endFrame`, not here: `Clickable` fires on the
|
|
1220
|
+
* release over the node the press started on, so a pointer cleared in the
|
|
1221
|
+
* same frame takes the click with it.
|
|
1222
|
+
*/
|
|
1223
|
+
handleMouseButton(button: number, isDown: boolean, kind?: "mouse" | "touch"): void;
|
|
1224
|
+
/** A finger came up this frame — see `endFrame`. */
|
|
1225
|
+
private touchLifted;
|
|
931
1226
|
/** Accumulate look deltas (movementX/Y under pointer lock, else move deltas). */
|
|
932
1227
|
handlePointerMove(dx: number, dy: number): void;
|
|
933
1228
|
handleWheel(deltaY: number): void;
|
|
@@ -967,11 +1262,7 @@ declare class InputMap {
|
|
|
967
1262
|
* Poll the browser Gamepad API every frame (the first connected pad).
|
|
968
1263
|
* Wire once at boot: `engine.input.attachGamepad(engine)`. Headless no-op.
|
|
969
1264
|
*/
|
|
970
|
-
attachGamepad(
|
|
971
|
-
updated: {
|
|
972
|
-
connect(fn: () => void): () => void;
|
|
973
|
-
};
|
|
974
|
-
}): () => void;
|
|
1265
|
+
attachGamepad(_engine?: unknown): () => void;
|
|
975
1266
|
/**
|
|
976
1267
|
* Pointer input from a mouse, a FINGER or a pen — one code path.
|
|
977
1268
|
*
|
|
@@ -1038,6 +1329,38 @@ declare class InputMap {
|
|
|
1038
1329
|
/** Consume one-frame edges. The Engine calls this at the end of every tick. */
|
|
1039
1330
|
endFrame(): void;
|
|
1040
1331
|
/**
|
|
1332
|
+
* @internal Start a tick: poll the devices that have no events of their own,
|
|
1333
|
+
* then open the frame.
|
|
1334
|
+
*
|
|
1335
|
+
* The gamepad used to be polled from an `engine.updated` handler — AFTER both
|
|
1336
|
+
* passes, and nine lines before `endFrame()` cleared what it had just
|
|
1337
|
+
* produced. So a pad's press edge was created and destroyed inside one tick
|
|
1338
|
+
* and `justPressed('jump')` was never true anywhere in the tree:
|
|
1339
|
+
*
|
|
1340
|
+
* PAD 8 taps -> update() saw justPressed 0, justReleased 0, isPressed 32
|
|
1341
|
+
* KEY 8 taps -> update() saw justPressed 8, justReleased 8
|
|
1342
|
+
*
|
|
1343
|
+
* A controller could hold but could never press: 0.00 px of jump on Pad0
|
|
1344
|
+
* against 74.33 px on Space, and `HudLayer.focusNavigation` — the built-in
|
|
1345
|
+
* controller-menu feature — moved focus zero times. Polling HERE puts a pad
|
|
1346
|
+
* where a keyboard already is: an edge that exists before the frame opens.
|
|
1347
|
+
*/
|
|
1348
|
+
beginFrame(): void;
|
|
1349
|
+
/** Devices with no events of their own, polled at the start of every tick. */
|
|
1350
|
+
private readonly pollers;
|
|
1351
|
+
/**
|
|
1352
|
+
* @internal The fixed pass is starting. Edges it has not seen yet are live.
|
|
1353
|
+
*/
|
|
1354
|
+
beginFixedStep(): void;
|
|
1355
|
+
/**
|
|
1356
|
+
* @internal The fixed pass is done for this tick — it has now seen the edges.
|
|
1357
|
+
*
|
|
1358
|
+
* Called after the FIRST fixed step only, so an edge reaches `fixedUpdate`
|
|
1359
|
+
* exactly once however many steps a tick runs. A tick that runs none leaves
|
|
1360
|
+
* this view untouched and the next one delivers it.
|
|
1361
|
+
*/
|
|
1362
|
+
endFixedStep(): void;
|
|
1363
|
+
/**
|
|
1041
1364
|
* Drop all action declarations and injected action state (physical key
|
|
1042
1365
|
* state is kept). The Engine calls this on setScene so keybinds never
|
|
1043
1366
|
* bleed between scenes.
|
|
@@ -1174,6 +1497,19 @@ interface RayHit {
|
|
|
1174
1497
|
distance: number;
|
|
1175
1498
|
normal: number[];
|
|
1176
1499
|
node: Node | null;
|
|
1500
|
+
/**
|
|
1501
|
+
* WHERE the ray landed, in world units — `origin + dir * distance`.
|
|
1502
|
+
*
|
|
1503
|
+
* The thing every caller actually wants: an impact effect, a decal, a tower
|
|
1504
|
+
* dropped on uneven ground, a waypoint. `incanto-web-integration.md` sends
|
|
1505
|
+
* you here for exactly that case — "`worldFromScreen` lands it on the ground
|
|
1506
|
+
* plane, and `rayFromScreen` hands you the ray when the ground is terrain or
|
|
1507
|
+
* a stack of crates" — and then the hit carried a distance and left the
|
|
1508
|
+
* arithmetic, and the normalization it depends on, to the caller.
|
|
1509
|
+
*
|
|
1510
|
+
* 2 numbers in 2D (pixels), 3 in 3D (metres), like every other position.
|
|
1511
|
+
*/
|
|
1512
|
+
point: number[];
|
|
1177
1513
|
}
|
|
1178
1514
|
/**
|
|
1179
1515
|
* The physics world, as core knows it — the QUERIES a behavior asks, with no
|
|
@@ -1192,6 +1528,10 @@ interface PhysicsQuery {
|
|
|
1192
1528
|
/**
|
|
1193
1529
|
* Fire a ray and report the first thing it hits, or `null`.
|
|
1194
1530
|
*
|
|
1531
|
+
* `dir` may be any length — `target - eye` is the usual spelling — and is
|
|
1532
|
+
* normalized, so `distance` and `maxLen` are both in world units (px / m)
|
|
1533
|
+
* rather than multiples of `dir`. A zero-length direction returns `null`.
|
|
1534
|
+
*
|
|
1195
1535
|
* Sensors never block a ray. Pass `exclude` for the body you are casting from
|
|
1196
1536
|
* — a ray starting inside its own collider hits itself at distance 0.
|
|
1197
1537
|
*/
|
|
@@ -1199,16 +1539,36 @@ interface PhysicsQuery {
|
|
|
1199
1539
|
staticOnly?: boolean;
|
|
1200
1540
|
}): RayHit | null;
|
|
1201
1541
|
/**
|
|
1202
|
-
* A THICK ray
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1542
|
+
* A THICK ray — a sphere sweep in 3D, a circle sweep in 2D.
|
|
1543
|
+
*
|
|
1544
|
+
* Use it where skimming matters: a thin ray that passes 10 cm over a wall
|
|
1545
|
+
* reports "clear" while the wall still fills the frame, and in 2D a shot can
|
|
1546
|
+
* thread a one-pixel gap between two floor tiles. Same filters and the same
|
|
1547
|
+
* direction contract as `castRay`; `distance` is the travel of the CENTRE.
|
|
1548
|
+
*
|
|
1549
|
+
* It was 3D-only, and therefore optional here, until 0.70.
|
|
1205
1550
|
*/
|
|
1206
|
-
castSphere
|
|
1551
|
+
castSphere(origin: readonly number[], dir: readonly number[], radius: number, maxLen: number, exclude?: Node, opts?: {
|
|
1207
1552
|
staticOnly?: boolean;
|
|
1208
1553
|
}): {
|
|
1209
1554
|
distance: number;
|
|
1210
1555
|
node: Node | null;
|
|
1211
1556
|
} | null;
|
|
1557
|
+
/**
|
|
1558
|
+
* How fast this body is actually moving, from the SOLVER.
|
|
1559
|
+
*
|
|
1560
|
+
* 2D answers in px/s (y-down), 3D in m/s (y-up). `linearVelocity` on the node
|
|
1561
|
+
* is written back once per step, so a behavior reading it from inside
|
|
1562
|
+
* `fixedUpdate` — after a collision, before the write-back — sees the value
|
|
1563
|
+
* from BEFORE the impact, which is exactly when a game wants to know how hard
|
|
1564
|
+
* it hit something.
|
|
1565
|
+
*/
|
|
1566
|
+
velocityOf(node: Node): number[];
|
|
1567
|
+
/**
|
|
1568
|
+
* Mass the solver actually uses — collider-derived unless the body overrides
|
|
1569
|
+
* it. Scale a push by this rather than by the authored `mass` prop.
|
|
1570
|
+
*/
|
|
1571
|
+
massOf(node: Node): number;
|
|
1212
1572
|
/** Tear the world down and detach from the engine. */
|
|
1213
1573
|
dispose(): void;
|
|
1214
1574
|
}
|
|
@@ -1230,6 +1590,26 @@ interface SaveStore {
|
|
|
1230
1590
|
/** Wipe THIS namespace only. */
|
|
1231
1591
|
clear(): void;
|
|
1232
1592
|
/**
|
|
1593
|
+
* Every key this namespace holds.
|
|
1594
|
+
*
|
|
1595
|
+
* Optional, so a store a game injected itself keeps compiling. Without it,
|
|
1596
|
+
* the slot layer could only find saves through its own INDEX — and one
|
|
1597
|
+
* corrupt byte in that index made every intact save on the machine
|
|
1598
|
+
* unreachable and then orphaned them for good on the next write. A store
|
|
1599
|
+
* that can be enumerated can be rebuilt.
|
|
1600
|
+
*/
|
|
1601
|
+
keys?(): string[];
|
|
1602
|
+
/**
|
|
1603
|
+
* Keys whose stored text is not JSON any more.
|
|
1604
|
+
*
|
|
1605
|
+
* `parse` swallows a syntax error and answers with the fallback — the
|
|
1606
|
+
* RECOVERY is deliberate ("the fallback beats a crash"), the silence was not.
|
|
1607
|
+
* A truncated slot from a power cut is indistinguishable from a slot that was
|
|
1608
|
+
* never written, and a load menu that shows one fewer save than the player
|
|
1609
|
+
* remembers is the worst thing a save system can do quietly.
|
|
1610
|
+
*/
|
|
1611
|
+
corrupt?(): string[];
|
|
1612
|
+
/**
|
|
1233
1613
|
* Does a write here survive the page closing?
|
|
1234
1614
|
*
|
|
1235
1615
|
* `false` means this store is a Map that lives as long as the tab does — a
|
|
@@ -1288,6 +1668,46 @@ interface RestoreReport {
|
|
|
1288
1668
|
expected: number;
|
|
1289
1669
|
/** Authored nodes the save says the run consumed, and that were freed again. */
|
|
1290
1670
|
freed: number;
|
|
1671
|
+
/**
|
|
1672
|
+
* Behaviors whose `deserialize` THREW, and what it said.
|
|
1673
|
+
*
|
|
1674
|
+
* They are in `skipped` too, because they took nothing — but a save this
|
|
1675
|
+
* build cannot read and a node that has no `deserialize` at all are different
|
|
1676
|
+
* problems, and `skipped` alone said both.
|
|
1677
|
+
*/
|
|
1678
|
+
refused: string[];
|
|
1679
|
+
/**
|
|
1680
|
+
* Behaviors that took their state and then threw out of `announce()`.
|
|
1681
|
+
*
|
|
1682
|
+
* The state IS restored; the SCREEN was not told. That is the difference
|
|
1683
|
+
* between a correct save and a HUD showing zeros over a 1400-coin run, and it
|
|
1684
|
+
* used to be a bare `catch {}` — `restored: 2, expected: 2`, no log, no
|
|
1685
|
+
* error.
|
|
1686
|
+
*/
|
|
1687
|
+
unannounced: string[];
|
|
1688
|
+
/**
|
|
1689
|
+
* Authored nodes THIS tree already consumed that the save says should still
|
|
1690
|
+
* be there — the tell that the scene was PLAYED before the restore.
|
|
1691
|
+
*
|
|
1692
|
+
* `#freed` can take nodes away and nothing puts them back, so restoring into
|
|
1693
|
+
* a live scene (a pause-menu Load, a slot menu, the canonical
|
|
1694
|
+
* death→reload-checkpoint wiring) leaves this run's collectibles deleted and
|
|
1695
|
+
* reports fully green. Measured on 3 gems worth 10 with `scoreToWin: 30`:
|
|
1696
|
+
*
|
|
1697
|
+
* ```
|
|
1698
|
+
* CONTROL restartScene then restore → score 10, gems [Gem2,Gem3] → 30, won TRUE
|
|
1699
|
+
* death wired straight to restore → score 10, gems [Gem3] → 20, won FALSE
|
|
1700
|
+
* ```
|
|
1701
|
+
*
|
|
1702
|
+
* …and the next autosave writes the hybrid back to the slot, so the
|
|
1703
|
+
* unwinnable run survives a page reload. Every other field is identical
|
|
1704
|
+
* between the two, which is why nothing could tell them apart.
|
|
1705
|
+
*
|
|
1706
|
+
* A node that freed ITSELF on a timer between load and restore lands here
|
|
1707
|
+
* too, which is why this is reported rather than thrown: the caller knows
|
|
1708
|
+
* which of its nodes are transient and the engine does not.
|
|
1709
|
+
*/
|
|
1710
|
+
stale: string[];
|
|
1291
1711
|
}
|
|
1292
1712
|
/**
|
|
1293
1713
|
* Every behavior in the tree that has something to say, by uid.
|
|
@@ -1300,24 +1720,14 @@ interface RestoreReport {
|
|
|
1300
1720
|
* cannot be given back. The editor assigns a uid to every node it touches; a
|
|
1301
1721
|
* hand-written scene may not have.
|
|
1302
1722
|
*/
|
|
1303
|
-
declare function captureBehaviors(root: Node, source?: JsonValue): {
|
|
1723
|
+
declare function captureBehaviors(root: Node, source?: JsonValue, authored?: ReadonlySet<string>): {
|
|
1304
1724
|
state: BehaviorState; /** Paths of nodes that had state to save and no uid to save it under. */
|
|
1305
1725
|
unaddressable: string[]; /** Behaviors in this tree that have something to save — addressable or not. */
|
|
1306
1726
|
saveable: number; /** Authored nodes this run consumed that the `#freed` ledger cannot record. */
|
|
1307
|
-
unrecordable: string[];
|
|
1727
|
+
unrecordable: string[]; /** Behaviors whose `serialize()` THREW — their state is not in `state`. */
|
|
1728
|
+
failed: string[];
|
|
1308
1729
|
};
|
|
1309
|
-
|
|
1310
|
-
* Hand each behavior its state back.
|
|
1311
|
-
*
|
|
1312
|
-
* Call this AFTER the scene has loaded and `onReady` has run: `onReady` is where
|
|
1313
|
-
* a behavior sets its starting values, so restoring first would be overwritten
|
|
1314
|
-
* by the fresh start.
|
|
1315
|
-
*
|
|
1316
|
-
* Never throws on a mismatch. A save from an older build of the game will name
|
|
1317
|
-
* uids that no longer exist, and refusing to load it would mean a patch that
|
|
1318
|
-
* moves one node deletes everyone's progress. It reports instead.
|
|
1319
|
-
*/
|
|
1320
|
-
declare function restoreBehaviors(root: Node, state: BehaviorState): RestoreReport;
|
|
1730
|
+
declare function restoreBehaviors(root: Node, state: BehaviorState, authored?: ReadonlySet<string>): RestoreReport;
|
|
1321
1731
|
/**
|
|
1322
1732
|
* The slot layer: a named list of saves over the existing store.
|
|
1323
1733
|
*
|
|
@@ -1336,8 +1746,32 @@ declare class SaveSlots {
|
|
|
1336
1746
|
* be there tomorrow.
|
|
1337
1747
|
*/
|
|
1338
1748
|
get persistent(): boolean;
|
|
1749
|
+
/**
|
|
1750
|
+
* The slot index — rebuilt from the store when it cannot be trusted.
|
|
1751
|
+
*
|
|
1752
|
+
* `store.get(INDEX_KEY, [])` falls back to an EMPTY LIST when the stored text
|
|
1753
|
+
* is not JSON, so one corrupt byte in a derived index emptied the whole load
|
|
1754
|
+
* menu while every save on the machine was still perfectly readable — and the
|
|
1755
|
+
* next `write()` then set the index to `[newId]`, orphaning them for good.
|
|
1756
|
+
* The index is a cache of what the store holds, and the store can be asked.
|
|
1757
|
+
*/
|
|
1758
|
+
private ids;
|
|
1339
1759
|
/** Slot ids that have a save, newest first. */
|
|
1340
1760
|
list(): string[];
|
|
1761
|
+
/**
|
|
1762
|
+
* Slots that EXIST and cannot be read — a truncated write, or a save from a
|
|
1763
|
+
* build that predates this one.
|
|
1764
|
+
*
|
|
1765
|
+
* `read()` answers `null` for both of those and for "no such slot", and
|
|
1766
|
+
* `list()` filters them out, so a save the player made silently disappeared
|
|
1767
|
+
* from the menu. A load screen should render "1 save could not be read"
|
|
1768
|
+
* rather than one fewer row, which is why this is a list and not a warning.
|
|
1769
|
+
*/
|
|
1770
|
+
problems(): Array<{
|
|
1771
|
+
id: string;
|
|
1772
|
+
why: "corrupt" | "unreadable";
|
|
1773
|
+
}>;
|
|
1774
|
+
private isCorrupt;
|
|
1341
1775
|
/** Every slot with its contents — what a load menu actually renders. */
|
|
1342
1776
|
all(): Array<SaveSlot & {
|
|
1343
1777
|
id: string;
|
|
@@ -1416,8 +1850,32 @@ declare class Scene {
|
|
|
1416
1850
|
/** The full source JSON this scene was loaded from — `restartScene` fuel. */
|
|
1417
1851
|
readonly source: SceneJson;
|
|
1418
1852
|
constructor(source: SceneJson, root: Node, tree: SceneTree);
|
|
1853
|
+
/**
|
|
1854
|
+
* Every uid this scene had the moment it finished loading.
|
|
1855
|
+
*
|
|
1856
|
+
* The `#freed` ledger's left-hand side: a uid in here that is not in the live
|
|
1857
|
+
* tree is a node this run consumed. Includes nodes inside expanded
|
|
1858
|
+
* `instance:` sub-scenes, which the scene FILE cannot show.
|
|
1859
|
+
*/
|
|
1860
|
+
readonly authoredUids: ReadonlySet<string>;
|
|
1419
1861
|
/** Lossless export. Note: expanded sub-scene instances serialize as full trees in M1. */
|
|
1420
1862
|
toJSON(): SceneJson;
|
|
1863
|
+
/**
|
|
1864
|
+
* The connections whose BOTH ENDS still exist in the tree being written.
|
|
1865
|
+
*
|
|
1866
|
+
* `toJSON` is what `incanto-play`'s `capture` writes — "your screenshot", the
|
|
1867
|
+
* skills call it — and it wrote every connection the file declared, including
|
|
1868
|
+
* ones pointing at nodes the run had freed. The result was a scene the engine
|
|
1869
|
+
* REFUSES to load:
|
|
1870
|
+
*
|
|
1871
|
+
* DANGLING_CONNECTION: connection to 'Coin' resolves nowhere
|
|
1872
|
+
*
|
|
1873
|
+
* so a captured game with any collectible in it produced a file no tool could
|
|
1874
|
+
* read back. Dropping a wire whose node is gone is what the tree already did
|
|
1875
|
+
* when it freed the node; this makes the written file say the same thing.
|
|
1876
|
+
*/
|
|
1877
|
+
private liveConnections;
|
|
1878
|
+
private resolves;
|
|
1421
1879
|
}
|
|
1422
1880
|
//#endregion
|
|
1423
1881
|
//#region src/core/settings.d.ts
|
|
@@ -1654,6 +2112,24 @@ interface EngineStats {
|
|
|
1654
2112
|
fps: number;
|
|
1655
2113
|
/** Mean frame delta in ms over the same window (headless runs: 0). */
|
|
1656
2114
|
frameMs: number;
|
|
2115
|
+
/**
|
|
2116
|
+
* How long ago the newest frame arrived, in ms. `null` when none has.
|
|
2117
|
+
*
|
|
2118
|
+
* `fps` is a rate over a WINDOW, so it keeps answering after the frames stop
|
|
2119
|
+
* — a hidden tab, a descheduled loop, a page the browser froze. Measured on
|
|
2120
|
+
* an engine that ticked 40 times and then stopped being ticked:
|
|
2121
|
+
*
|
|
2122
|
+
* ```
|
|
2123
|
+
* while ticking : fps 59.9
|
|
2124
|
+
* after 0 more ticks : fps 59.9 <- the last sample is 668 ms old
|
|
2125
|
+
* ```
|
|
2126
|
+
*
|
|
2127
|
+
* Both numbers are true about the window they describe; nothing said WHEN
|
|
2128
|
+
* that window was, so "60 fps" read the same whether it was measured 8 ms ago
|
|
2129
|
+
* or four seconds ago. A reader that cares should compare this against a
|
|
2130
|
+
* frame budget before trusting `fps`.
|
|
2131
|
+
*/
|
|
2132
|
+
lastFrameAgeMs: number | null;
|
|
1657
2133
|
/** Current node count of the active scene tree (no scene: 0). */
|
|
1658
2134
|
nodes: number;
|
|
1659
2135
|
/** Whether the loop is scheduled (start() without stop()/dispose()). */
|
|
@@ -1750,6 +2226,86 @@ declare class Engine {
|
|
|
1750
2226
|
* learns about Rapier or three. From a behavior, prefer `this.physics`.
|
|
1751
2227
|
*/
|
|
1752
2228
|
physics: PhysicsQuery | null;
|
|
2229
|
+
/**
|
|
2230
|
+
* How to build a physics world for a scene that needs one.
|
|
2231
|
+
*
|
|
2232
|
+
* `createGame2D`/`createGame3D` install this; core never learns what Rapier
|
|
2233
|
+
* is. It exists because `physics: 'auto'` can only look at the scene the game
|
|
2234
|
+
* BOOTED with, and the most universal act of finishing a game is putting a
|
|
2235
|
+
* title screen in front of it:
|
|
2236
|
+
*
|
|
2237
|
+
* ```
|
|
2238
|
+
* after boot on the title : engine.physics = null
|
|
2239
|
+
* after goToScene into game: engine.physics = null
|
|
2240
|
+
* player y after 2 s : 0 -> 0
|
|
2241
|
+
* ```
|
|
2242
|
+
*
|
|
2243
|
+
* The game rendered perfectly and was inert — nothing fell, nothing collided,
|
|
2244
|
+
* nothing could be collected or hurt. One scene swap turned a working game
|
|
2245
|
+
* into a diorama, and nothing was logged.
|
|
2246
|
+
*/
|
|
2247
|
+
physicsProvider: ((root: Node) => Promise<void>) | null;
|
|
2248
|
+
private ensuring;
|
|
2249
|
+
/**
|
|
2250
|
+
* Give the current scene a physics world if it needs one and has none.
|
|
2251
|
+
*
|
|
2252
|
+
* Idempotent and safe to call on every swap: the provider decides whether the
|
|
2253
|
+
* incoming tree actually wants physics, and an explicit `physics: false` at
|
|
2254
|
+
* boot installs no provider at all, so opting out stays opted out.
|
|
2255
|
+
*
|
|
2256
|
+
* Asynchronous because the Rapier WASM has to load. The first frames after a
|
|
2257
|
+
* swap therefore run unsimulated — which every AI behaviour already tolerates
|
|
2258
|
+
* (`moveBody` falls back to a direct write when `_physics` is null) and which
|
|
2259
|
+
* is a hitch, where the alternative was a game that never simulated at all.
|
|
2260
|
+
*/
|
|
2261
|
+
ensurePhysics(): Promise<void>;
|
|
2262
|
+
/**
|
|
2263
|
+
* The seed this engine was built with — game logic draws from `rng`, and
|
|
2264
|
+
* anything DECORATIVE derives its own stream from this instead.
|
|
2265
|
+
*
|
|
2266
|
+
* A particle emitter used to draw from `rng`, and it spawns a number of
|
|
2267
|
+
* particles that depends on `dt`. Measured on the same scene and seed, the
|
|
2268
|
+
* same simulated two seconds, comparing the world's own draws by update
|
|
2269
|
+
* index:
|
|
2270
|
+
*
|
|
2271
|
+
* ```
|
|
2272
|
+
* particles off: 60fps and 30fps identical
|
|
2273
|
+
* particles ON : they diverge at update 2
|
|
2274
|
+
* ```
|
|
2275
|
+
*
|
|
2276
|
+
* So a purely visual effect changed the game's random decisions, and the
|
|
2277
|
+
* PLAYER'S FRAME RATE decided them. Derived streams stay reproducible for
|
|
2278
|
+
* replays and screenshots without spending the one the game reads.
|
|
2279
|
+
*/
|
|
2280
|
+
readonly seed: number;
|
|
2281
|
+
/**
|
|
2282
|
+
* How to resolve an `instance:` sub-scene, remembered from the boot load.
|
|
2283
|
+
*
|
|
2284
|
+
* `loadScene(json, { engine, resolveScene })` records it here, so the paths
|
|
2285
|
+
* that RELOAD — `restartScene`, `goToScene`, a game-over restart key — can
|
|
2286
|
+
* pass it back. Without that a game using the documented sub-scenes booted
|
|
2287
|
+
* fine and threw `UNRESOLVED_INSTANCE` on its first restart.
|
|
2288
|
+
*/
|
|
2289
|
+
resolveScene?: (path: string) => SceneJson;
|
|
2290
|
+
/**
|
|
2291
|
+
* How the current scene was LOADED, for the rebuilds that happen later.
|
|
2292
|
+
*
|
|
2293
|
+
* `duplicateNode` — every `Spawner`, every `NetworkSpawner`, every clone a
|
|
2294
|
+
* game makes — rebuilds a subtree from scene JSON long after `loadScene`
|
|
2295
|
+
* returned, and it did so with no options at all. Under
|
|
2296
|
+
* `stubMissingBehaviors` (how EVERY headless check runs a game whose scripts
|
|
2297
|
+
* are TypeScript it cannot import) the first spawn met the real registry and
|
|
2298
|
+
* threw `UNKNOWN_BEHAVIOR` out of `update`, disabling the spawner for the
|
|
2299
|
+
* rest of the run — a wave game reported with no waves and nothing saying
|
|
2300
|
+
* why.
|
|
2301
|
+
*
|
|
2302
|
+
* Recorded here for the same reason as `resolveScene` above: the engine
|
|
2303
|
+
* outlives the call, and the rebuild paths can reach it.
|
|
2304
|
+
*/
|
|
2305
|
+
loadOptions: {
|
|
2306
|
+
stubMissingBehaviors?: boolean;
|
|
2307
|
+
stubAllBehaviors?: boolean;
|
|
2308
|
+
};
|
|
1753
2309
|
/** Seeded randomness for game logic (deterministic when `seed` is set). */
|
|
1754
2310
|
readonly rng: Rng;
|
|
1755
2311
|
/** The engine log channel (debug overlay + headless harness tail this). */
|
|
@@ -1764,6 +2320,60 @@ declare class Engine {
|
|
|
1764
2320
|
* and cleared on dispose; null when nothing renders (headless).
|
|
1765
2321
|
*/
|
|
1766
2322
|
picker: ((x: number, y: number) => Node | null) | null;
|
|
2323
|
+
/**
|
|
2324
|
+
* Screen pixel → a point in the WORLD, when a renderer has offered one.
|
|
2325
|
+
*
|
|
2326
|
+
* `picker` answers "which node"; this answers "which place", and a mouse game
|
|
2327
|
+
* needs both — drag-and-launch, drop a tower on the map, steer a cursor unit,
|
|
2328
|
+
* draw the slingshot band. The conversion has always existed
|
|
2329
|
+
* (`Renderer2D.worldFromScreen` / `Renderer3D.worldFromScreen`) and lived
|
|
2330
|
+
* only on the renderer, so a Behavior — which holds a node and an engine and
|
|
2331
|
+
* nothing else — could not call it. Its two workarounds were both forbidden:
|
|
2332
|
+
* stash the renderer in a module global, or hand-roll the projection the 2D
|
|
2333
|
+
* skill says in so many words not to hand-roll.
|
|
2334
|
+
*
|
|
2335
|
+
* The answer is the scene's own units and shape: `[x, y]` in 2D (design
|
|
2336
|
+
* pixels), `[x, y, z]` on the ground plane in 3D — the same array a node's
|
|
2337
|
+
* `position` is, so it goes straight back into one.
|
|
2338
|
+
*
|
|
2339
|
+
* Instance-scoped, set by the renderer on construction and cleared on
|
|
2340
|
+
* dispose. Headless a harness may install a geometric one; null when nothing
|
|
2341
|
+
* has.
|
|
2342
|
+
*/
|
|
2343
|
+
toWorld: ((x: number, y: number) => number[] | null) | null;
|
|
2344
|
+
/**
|
|
2345
|
+
* A world point → the screen pixel drawing it. The inverse of `toWorld`, for
|
|
2346
|
+
* pinning DOM to a node or placing a marker over one.
|
|
2347
|
+
*
|
|
2348
|
+
* `behind` is true when the point is behind the camera (3D); a 2D renderer
|
|
2349
|
+
* always reports false.
|
|
2350
|
+
*/
|
|
2351
|
+
toScreen: ((world: readonly number[]) => {
|
|
2352
|
+
x: number;
|
|
2353
|
+
y: number;
|
|
2354
|
+
behind: boolean;
|
|
2355
|
+
} | null) | null;
|
|
2356
|
+
/**
|
|
2357
|
+
* Where the cursor is in the world, or null.
|
|
2358
|
+
*
|
|
2359
|
+
* The two-line dance every mouse game writes — read the pointer, convert it —
|
|
2360
|
+
* with the two ways it goes wrong already handled: a pointer nothing has
|
|
2361
|
+
* moved yet is null rather than the origin, and no converter is null rather
|
|
2362
|
+
* than a guess.
|
|
2363
|
+
*/
|
|
2364
|
+
/**
|
|
2365
|
+
* Screen pixel → a ray into the world, when a renderer has offered one.
|
|
2366
|
+
*
|
|
2367
|
+
* `toWorld` lands on a plane; this is the general form, and it is what a shot
|
|
2368
|
+
* aimed at what the cursor is OVER needs: feed it straight to
|
|
2369
|
+
* `engine.physics.castRay(origin, dir, …)`. 3D only — a 2D renderer installs
|
|
2370
|
+
* nothing here, since a 2D world has no depth to cast into.
|
|
2371
|
+
*/
|
|
2372
|
+
screenRay: ((x: number, y: number) => {
|
|
2373
|
+
origin: number[];
|
|
2374
|
+
dir: number[];
|
|
2375
|
+
} | null) | null;
|
|
2376
|
+
pointerWorld(): number[] | null;
|
|
1767
2377
|
/** Frames the engine has stepped — monotonic, and the pick cache's key. */
|
|
1768
2378
|
frameNumber: number;
|
|
1769
2379
|
/** @internal frame number + answer, so N Clickables cost ONE raycast. */
|
|
@@ -1844,7 +2454,24 @@ declare class Engine {
|
|
|
1844
2454
|
* (hit-stop / pause). Scales BOTH variable and fixed updates — physics,
|
|
1845
2455
|
* timers, behaviors all breathe together. See `gameplay` `hitStop()`.
|
|
1846
2456
|
*/
|
|
1847
|
-
timeScale: number;
|
|
2457
|
+
get timeScale(): number;
|
|
2458
|
+
set timeScale(v: number);
|
|
2459
|
+
private _timeScale;
|
|
2460
|
+
private _timeScaleWrites;
|
|
2461
|
+
/**
|
|
2462
|
+
* How many times anyone has written `timeScale` — the clock's ownership tag.
|
|
2463
|
+
*
|
|
2464
|
+
* A temporary freeze cannot tell its own 0 from somebody else's by VALUE.
|
|
2465
|
+
* `hitStop(engine)` then `flow.gameOver()` is the ordinary killing blow, and
|
|
2466
|
+
* both set 0; the freeze then wrote its captured scale back five frames later
|
|
2467
|
+
* and the world started moving again behind a sticky GAME OVER banner —
|
|
2468
|
+
* enemies walking, timers running, the player watching their corpse get shot.
|
|
2469
|
+
*
|
|
2470
|
+
* A counter answers the question the value cannot: *has anyone touched this
|
|
2471
|
+
* since I did?* Cheap (one increment per write, and writes are rare) and it
|
|
2472
|
+
* needs no registry of who holds what.
|
|
2473
|
+
*/
|
|
2474
|
+
get timeScaleWrites(): number;
|
|
1848
2475
|
/**
|
|
1849
2476
|
* The node the dev overlay has selected (renderers draw its bounding box
|
|
1850
2477
|
* as an orange outline in the game view so you can SEE what you picked).
|
|
@@ -1863,6 +2490,29 @@ declare class Engine {
|
|
|
1863
2490
|
/** Elapsed REAL time in seconds since the scene started (ignores timeScale) —
|
|
1864
2491
|
* UI animations that must keep moving during slow-mo/pause read this. */
|
|
1865
2492
|
get unscaledTime(): number;
|
|
2493
|
+
/**
|
|
2494
|
+
* THIS frame's dt in real seconds — the one a paused game still gets.
|
|
2495
|
+
*
|
|
2496
|
+
* `update(dt)` receives the SCALED dt, which is right for anything that is
|
|
2497
|
+
* part of the simulation and wrong for everything that is a presentation of
|
|
2498
|
+
* it. At `timeScale = 0` — which is what `GameFlow.pause()` and, by default,
|
|
2499
|
+
* `gameOver()` set — a scaled countdown does not count:
|
|
2500
|
+
*
|
|
2501
|
+
* a timed UiBanner still up when the player dies never expires, so the
|
|
2502
|
+
* flow's own sticky GAME OVER banner is queued behind it and NEVER
|
|
2503
|
+
* PAINTED — the ending screen shows the last wave toast, forever
|
|
2504
|
+
* a live CameraShake never settles: `t -= dt` is a no-op while the rest of
|
|
2505
|
+
* update() re-rolls a full-magnitude offset every rendered frame, and
|
|
2506
|
+
* with FollowCamera's smoothing that is an unbounded random walk
|
|
2507
|
+
* a remote player's interpolation stops, so everyone else is a statue while
|
|
2508
|
+
* their real positions keep arriving
|
|
2509
|
+
*
|
|
2510
|
+
* `hitStop`, `AudioPlayer` and `music.tick` already use the real clock for
|
|
2511
|
+
* exactly this reason. This is the same clock, per frame, so a countdown can
|
|
2512
|
+
* use it without reading `unscaledTime` twice and subtracting.
|
|
2513
|
+
*/
|
|
2514
|
+
get unscaledDelta(): number;
|
|
2515
|
+
private _unscaledDelta;
|
|
1866
2516
|
constructor(opts?: EngineOptions);
|
|
1867
2517
|
get scene(): Scene | null;
|
|
1868
2518
|
/** Replace the active scene. The previous scene's root is freed. */
|
|
@@ -1903,6 +2553,22 @@ declare class Engine {
|
|
|
1903
2553
|
private reportFrameError;
|
|
1904
2554
|
/** @internal A node quarantined itself; count it for `stats().errors`. */
|
|
1905
2555
|
_nodeErrored(_node: unknown): void;
|
|
2556
|
+
/**
|
|
2557
|
+
* @internal Something OUTSIDE a frame failed — a replication send that
|
|
2558
|
+
* rejected, a transport that refused. Count it and say it once.
|
|
2559
|
+
*
|
|
2560
|
+
* `stats().errors` is what every harness in the ecosystem ends with, and it
|
|
2561
|
+
* only ever counted things that threw INSIDE a frame. A multiplayer client
|
|
2562
|
+
* whose socket died kept running perfectly on its own screen while every
|
|
2563
|
+
* other player watched it frozen, and `errors: 0` with an empty log said the
|
|
2564
|
+
* game was fine. Repeats are counted and not re-printed, for the reason
|
|
2565
|
+
* `reportFrameError` gives: sixty identical traces a second is how you lose
|
|
2566
|
+
* the first one.
|
|
2567
|
+
*/
|
|
2568
|
+
_asyncFailed(what: string, error: unknown): void;
|
|
2569
|
+
private lastAsyncError;
|
|
2570
|
+
/** Re-entrancy guard for `setScene` — an exit hook can now reach the engine. */
|
|
2571
|
+
private swapping;
|
|
1906
2572
|
private errorCount;
|
|
1907
2573
|
private lastFrameError;
|
|
1908
2574
|
/**
|
|
@@ -1926,6 +2592,14 @@ declare class Engine {
|
|
|
1926
2592
|
* core/save-slots.ts for why a save carries state rather than a tree
|
|
1927
2593
|
* snapshot.
|
|
1928
2594
|
*/
|
|
2595
|
+
/**
|
|
2596
|
+
* Behaviors whose `serialize()` threw during the LAST `captureState()`.
|
|
2597
|
+
*
|
|
2598
|
+
* A write path that cannot see part of the run must not overwrite the slot
|
|
2599
|
+
* that still can — `SavePoint.save()` reads this and refuses.
|
|
2600
|
+
*/
|
|
2601
|
+
get lastCaptureFailures(): readonly string[];
|
|
2602
|
+
private _captureFailures;
|
|
1929
2603
|
captureState(): BehaviorState;
|
|
1930
2604
|
/** Give it back, after the scene is loaded and onReady has run. */
|
|
1931
2605
|
restoreState(state: BehaviorState): RestoreReport;
|
|
@@ -2011,6 +2685,14 @@ interface PropRange {
|
|
|
2011
2685
|
/** `min` is a bound the value must EXCEED, not reach (`targetHeight > 0`). */
|
|
2012
2686
|
exclusiveMin?: boolean;
|
|
2013
2687
|
/**
|
|
2688
|
+
* `max` is a bound the value must stay UNDER, not reach.
|
|
2689
|
+
*
|
|
2690
|
+
* `Camera3D.fov` is the case: 180 is a legal number and a degenerate
|
|
2691
|
+
* frustum — the whole world collapses to a point at screen centre and the
|
|
2692
|
+
* framing instrument still calls it on screen.
|
|
2693
|
+
*/
|
|
2694
|
+
exclusiveMax?: boolean;
|
|
2695
|
+
/**
|
|
2014
2696
|
* Values outside the range that mean something anyway.
|
|
2015
2697
|
*
|
|
2016
2698
|
* `metalness: -1` is "keep whatever the GLB authored" — a real value with a
|
|
@@ -2027,6 +2709,23 @@ interface PropDef {
|
|
|
2027
2709
|
* dropdowns and the generated JSON-Schema enum — keep it exhaustive. */
|
|
2028
2710
|
options?: readonly string[];
|
|
2029
2711
|
/**
|
|
2712
|
+
* Every JSON kind this prop accepts, when it accepts more than the default's.
|
|
2713
|
+
* MUST contain the default's own kind — `registerNode` refuses a list that
|
|
2714
|
+
* does not, because that default would be unwritable in a scene.
|
|
2715
|
+
*
|
|
2716
|
+
* A prop typed `boolean | JsonObject` in TypeScript is one kind to the loader:
|
|
2717
|
+
* whatever the default happens to be. `Water3D.underwater` (default `true`)
|
|
2718
|
+
* and `Flowers3D.density` (default `'sparse'`) both wrote a `validateJson`
|
|
2719
|
+
* branch for the other half — checking `underwater.visibility`, budgeting
|
|
2720
|
+
* plants/m² — and neither branch could be reached from a scene, because the
|
|
2721
|
+
* kind gate runs first. Both halves were in the skills; only one loaded.
|
|
2722
|
+
*
|
|
2723
|
+
* When a value's kind is not the default's, the kind gate is all this file
|
|
2724
|
+
* checks: `options`, `range` and the array template describe the DEFAULT's
|
|
2725
|
+
* kind, so the node's own `validateJson` owns the other half's shape.
|
|
2726
|
+
*/
|
|
2727
|
+
kinds?: readonly JsonKind[];
|
|
2728
|
+
/**
|
|
2030
2729
|
* This prop holds a NODE PATH (`%Unique`, `/Absolute/From/Root`, or one
|
|
2031
2730
|
* relative to the node that owns the prop) resolved with `getNodeOrNull`.
|
|
2032
2731
|
*
|
|
@@ -2068,7 +2767,14 @@ interface PropDef {
|
|
|
2068
2767
|
*/
|
|
2069
2768
|
range?: PropRange;
|
|
2070
2769
|
asset?: {
|
|
2071
|
-
/** The `assets{}` entry `type` a `$ref` here must point at. */
|
|
2770
|
+
/** The `assets{}` entry `type` a `$ref` here must point at. */
|
|
2771
|
+
/**
|
|
2772
|
+
* `audio` has no `assets{}` entry type — `AudioPlayer.src` takes a url and
|
|
2773
|
+
* only a url. It is declared anyway so the prop is VISIBLE to everything
|
|
2774
|
+
* that walks asset props (the editor's picker, `incanto-check`'s
|
|
2775
|
+
* missing-file warning), which is the whole point of the spec.
|
|
2776
|
+
*/
|
|
2777
|
+
kind: "texture" | "spritesheet" | "model" | "animation" | "audio";
|
|
2072
2778
|
/**
|
|
2073
2779
|
* A value that is not a `$ref` is an error.
|
|
2074
2780
|
*
|
|
@@ -2077,6 +2783,26 @@ interface PropDef {
|
|
|
2077
2783
|
* GLB (`ModelInstance3D.animation`).
|
|
2078
2784
|
*/
|
|
2079
2785
|
refOnly?: boolean;
|
|
2786
|
+
/**
|
|
2787
|
+
* A `$ref` here is an error — this prop takes a raw URL.
|
|
2788
|
+
*
|
|
2789
|
+
* The 3D texture props declared no `asset` spec at all, so
|
|
2790
|
+
* `validateAssetRefs` never looked at them: `"texture": "$hero"` — the
|
|
2791
|
+
* spelling the 2D sibling's own error message teaches — was certified valid
|
|
2792
|
+
* at author time and then fetched as a relative path, 404ing at runtime
|
|
2793
|
+
* with a sprite that simply never appears.
|
|
2794
|
+
*/
|
|
2795
|
+
urlOnly?: boolean;
|
|
2796
|
+
/**
|
|
2797
|
+
* The prop is an OBJECT whose values are the references, not a string.
|
|
2798
|
+
*
|
|
2799
|
+
* `CharacterController3D.animations` is the zero-TypeScript way to animate
|
|
2800
|
+
* a character — `{ "idle": "$idle", "run": "$run" }` — and every one of
|
|
2801
|
+
* those values is an asset ref that nothing validated. A typo there is a
|
|
2802
|
+
* character that never animates, with no error anywhere, which is the exact
|
|
2803
|
+
* class of silence the hard-error rule exists to prevent.
|
|
2804
|
+
*/
|
|
2805
|
+
mapValues?: boolean;
|
|
2080
2806
|
};
|
|
2081
2807
|
/**
|
|
2082
2808
|
* For an OBJECT-valued prop shaped as a tagged union — `collider`, whose
|
|
@@ -2179,7 +2905,17 @@ declare abstract class Behavior {
|
|
|
2179
2905
|
static signals?: readonly string[];
|
|
2180
2906
|
/** The node this behavior is attached to (assigned by the loader). */
|
|
2181
2907
|
node: Node;
|
|
2182
|
-
/**
|
|
2908
|
+
/**
|
|
2909
|
+
* The running engine, reached through the scene tree.
|
|
2910
|
+
*
|
|
2911
|
+
* The message names the FIX, because there is one and it is one word. Reading
|
|
2912
|
+
* this in `onReady` is the documented pattern — `Wander` draws its first
|
|
2913
|
+
* heading there — and `onReady` runs during `loadScene`, before the tree
|
|
2914
|
+
* belongs to anyone. Every boot path in the engine passes `{ engine }` for
|
|
2915
|
+
* exactly that reason; the two-step boot the skills print did not, so a scene
|
|
2916
|
+
* with one wandering critter in it threw at load and the error said only that
|
|
2917
|
+
* something was not attached.
|
|
2918
|
+
*/
|
|
2183
2919
|
get engine(): Engine;
|
|
2184
2920
|
/** Shortcut for `this.engine.input`. */
|
|
2185
2921
|
get input(): InputMap;
|
|
@@ -2243,6 +2979,35 @@ declare abstract class Behavior {
|
|
|
2243
2979
|
* quietly ignoring a field you renamed is usually the better answer.
|
|
2244
2980
|
*/
|
|
2245
2981
|
deserialize?(data: JsonValue): void;
|
|
2982
|
+
/**
|
|
2983
|
+
* Tell the world what you were just handed — the seventh hook, and the one
|
|
2984
|
+
* that made "Continue" show the right numbers.
|
|
2985
|
+
*
|
|
2986
|
+
* `deserialize` writes fields. A HUD is wired to SIGNALS (`scoreChanged`,
|
|
2987
|
+
* `healthChanged`, `changed`), because that is what `incanto-hud.md` teaches
|
|
2988
|
+
* and what the editor can wire. Nothing emitted them after a restore, so a
|
|
2989
|
+
* Continue boot measured like this:
|
|
2990
|
+
*
|
|
2991
|
+
* ```
|
|
2992
|
+
* HUD score="0" hp=100/100 gems="0"
|
|
2993
|
+
* TRUTH score=1400 hp=38 gems=7
|
|
2994
|
+
* ```
|
|
2995
|
+
*
|
|
2996
|
+
* with `errors: 0`, an empty log, and a restore report of
|
|
2997
|
+
* `{missing:[], skipped:[], restored:2, expected:2}` — every instrument
|
|
2998
|
+
* green over a screen showing a fresh start. Health was the worst of the
|
|
2999
|
+
* three: its setter is private and early-returns when unchanged, so nothing
|
|
3000
|
+
* a game could call would repaint the bar; it read 100 at 38 HP until the
|
|
3001
|
+
* next hit teleported it to 37.
|
|
3002
|
+
*
|
|
3003
|
+
* `restoreBehaviors` calls this once, after every `deserialize` in the pass,
|
|
3004
|
+
* so a handler that reads a sibling sees restored values there too.
|
|
3005
|
+
*
|
|
3006
|
+
* Emit only what DISPLAYS. Not `died`, not `won`, not `levelUp` — a save is
|
|
3007
|
+
* being read, nothing just happened, and re-firing an outcome signal on load
|
|
3008
|
+
* is how a Continue lands straight on the game-over screen.
|
|
3009
|
+
*/
|
|
3010
|
+
announce?(): void;
|
|
2246
3011
|
}
|
|
2247
3012
|
/** Explicit registration — never an import side effect (tree-shaking safety). */
|
|
2248
3013
|
declare function registerBehavior(name: string, ctor: BehaviorCtor, opts?: {
|