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
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-CIu3uc3l.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incanto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"game-engine",
|
|
@@ -54,8 +54,10 @@
|
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsdown",
|
|
56
56
|
"dev": "tsdown --watch",
|
|
57
|
-
"typecheck": "tsc --noEmit",
|
|
58
|
-
"test": "vitest run --project unit --root ../.."
|
|
57
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
|
|
58
|
+
"test": "vitest run --project unit --root ../..",
|
|
59
|
+
"prepack": "node scripts/strip-dev-deps.mjs",
|
|
60
|
+
"postpack": "node scripts/strip-dev-deps.mjs --restore"
|
|
59
61
|
},
|
|
60
62
|
"peerDependencies": {
|
|
61
63
|
"@agent8/gameserver": ">=1.10.0",
|
|
@@ -70,18 +72,6 @@
|
|
|
70
72
|
"optional": true
|
|
71
73
|
}
|
|
72
74
|
},
|
|
73
|
-
"devDependencies": {
|
|
74
|
-
"@agent8/gameserver": "1.10.2",
|
|
75
|
-
"@types/react": "^19.2.17",
|
|
76
|
-
"@types/react-dom": "^19.2.3",
|
|
77
|
-
"@types/three": "catalog:",
|
|
78
|
-
"react": "^19.2.7",
|
|
79
|
-
"react-dom": "^19.2.7",
|
|
80
|
-
"three": "catalog:",
|
|
81
|
-
"tsdown": "0.22.2",
|
|
82
|
-
"typescript": "catalog:",
|
|
83
|
-
"vitest": "catalog:"
|
|
84
|
-
},
|
|
85
75
|
"dependencies": {
|
|
86
76
|
"@dimforge/rapier2d-compat": "0.19.3",
|
|
87
77
|
"@dimforge/rapier3d-compat": "0.19.3",
|
|
@@ -102,6 +92,7 @@
|
|
|
102
92
|
"incanto-new": "bin/incanto-new.mjs",
|
|
103
93
|
"incanto-frame": "bin/incanto-frame.mjs",
|
|
104
94
|
"incanto-verify": "./bin/incanto-verify.mjs",
|
|
105
|
-
"incanto-logs": "./bin/incanto-logs.mjs"
|
|
95
|
+
"incanto-logs": "./bin/incanto-logs.mjs",
|
|
96
|
+
"incanto-serve": "./bin/incanto-serve.mjs"
|
|
106
97
|
}
|
|
107
98
|
}
|
|
@@ -1771,6 +1771,11 @@
|
|
|
1771
1771
|
"current": {
|
|
1772
1772
|
"type": "boolean",
|
|
1773
1773
|
"default": false
|
|
1774
|
+
},
|
|
1775
|
+
"lookAt": {
|
|
1776
|
+
"type": "string",
|
|
1777
|
+
"description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
|
|
1778
|
+
"default": ""
|
|
1774
1779
|
}
|
|
1775
1780
|
},
|
|
1776
1781
|
"additionalProperties": false
|
|
@@ -1903,6 +1908,10 @@
|
|
|
1903
1908
|
"slopeLimitDeg": {
|
|
1904
1909
|
"type": "number",
|
|
1905
1910
|
"default": 45
|
|
1911
|
+
},
|
|
1912
|
+
"stepHeight": {
|
|
1913
|
+
"type": "number",
|
|
1914
|
+
"default": 35
|
|
1906
1915
|
}
|
|
1907
1916
|
},
|
|
1908
1917
|
"additionalProperties": false
|
|
@@ -2800,8 +2809,15 @@
|
|
|
2800
2809
|
"default": null
|
|
2801
2810
|
},
|
|
2802
2811
|
"density": {
|
|
2803
|
-
"
|
|
2804
|
-
|
|
2812
|
+
"anyOf": [
|
|
2813
|
+
{
|
|
2814
|
+
"type": "string",
|
|
2815
|
+
"enum": ["lush", "sparse", "none"]
|
|
2816
|
+
},
|
|
2817
|
+
{
|
|
2818
|
+
"type": "number"
|
|
2819
|
+
}
|
|
2820
|
+
],
|
|
2805
2821
|
"default": "sparse"
|
|
2806
2822
|
},
|
|
2807
2823
|
"area": {
|
|
@@ -5532,6 +5548,10 @@
|
|
|
5532
5548
|
"minItems": 2,
|
|
5533
5549
|
"maxItems": 2,
|
|
5534
5550
|
"default": [0, 0]
|
|
5551
|
+
},
|
|
5552
|
+
"angularVelocity": {
|
|
5553
|
+
"type": "number",
|
|
5554
|
+
"default": 0
|
|
5535
5555
|
}
|
|
5536
5556
|
},
|
|
5537
5557
|
"additionalProperties": false
|
|
@@ -9442,7 +9462,14 @@
|
|
|
9442
9462
|
"default": true
|
|
9443
9463
|
},
|
|
9444
9464
|
"underwater": {
|
|
9445
|
-
"
|
|
9465
|
+
"anyOf": [
|
|
9466
|
+
{
|
|
9467
|
+
"type": "boolean"
|
|
9468
|
+
},
|
|
9469
|
+
{
|
|
9470
|
+
"type": "object"
|
|
9471
|
+
}
|
|
9472
|
+
],
|
|
9446
9473
|
"default": true
|
|
9447
9474
|
},
|
|
9448
9475
|
"swell": {
|
|
@@ -252,7 +252,16 @@ That is the EXACT formula `CharacterController3D` uses for move-facing
|
|
|
252
252
|
an enemy that idles facing you): use the direction from self TO the target —
|
|
253
253
|
`dx = target.position[0] - self.position[0]`, `dz = target.position[2] - self.position[2]`,
|
|
254
254
|
then `rotation[1] = atan2(dx, dz)*RAD2DEG`. Movement-facing is just this with the
|
|
255
|
-
target being "where I'm walking." There is ONE facing formula
|
|
255
|
+
target being "where I'm walking." There is ONE facing formula for anything with a
|
|
256
|
+
SKIN; only the direction differs.
|
|
257
|
+
|
|
258
|
+
**NOT a camera.** A `Camera3D` looks down its own local **−Z**, and — more to the
|
|
259
|
+
point — it has an UP that a rod does not. This formula aims a camera perfectly and
|
|
260
|
+
rolls it: over 72 headings, dot 1.000 in all 72 and **upside down in 36**. No
|
|
261
|
+
two-angle recipe can fix it (Euler XYZ makes `[pitch, yaw, 0]` = Rx·Ry, while a
|
|
262
|
+
level camera is Ry·Rx, so its third angle is non-zero). Use
|
|
263
|
+
`Camera3D.lookAt: "%Target"` — a node path, re-aimed every frame with world up.
|
|
264
|
+
See `incanto-building-3d-games.md`.
|
|
256
265
|
|
|
257
266
|
- **The 180° is NOT a facing formula.** It is only the AT-REST MOUNT — the character
|
|
258
267
|
starts turned away from the behind-the-shoulder camera until it first moves. Adding
|
|
@@ -354,6 +363,10 @@ const rx = Math.atan2(-Dy, Dz);
|
|
|
354
363
|
t.rotation = [rx*RAD2DEG, ry*RAD2DEG, 0];
|
|
355
364
|
```
|
|
356
365
|
|
|
366
|
+
(This is a ROD: a stretched box has no up, so two angles are enough for it and
|
|
367
|
+
`rodAxis · (target − muzzle) ≈ 1` is a complete check. A camera is not a rod —
|
|
368
|
+
that same check scores 1.000 on an upside-down one. See the Camera3D note above.)
|
|
369
|
+
|
|
357
370
|
The "obvious" `[-atan2(dy, hypot(dx,dz)), atan2(dx,dz), 0]` is only correct for a LEVEL
|
|
358
371
|
shot — for XYZ order the yaw denominator must be `hypot(Dy,Dz)`, not the ground `hypot(dx,
|
|
359
372
|
dz)`. With the wrong form the rod skews off-axis when you aim up/down, so its near end
|
|
@@ -76,6 +76,18 @@ meta (name/authors) + humanoid bone count. Decision rules:
|
|
|
76
76
|
(a center.y of half the height usually means feet at origin: good).
|
|
77
77
|
- **animation names** are exact strings for the `animation` prop.
|
|
78
78
|
|
|
79
|
+
**At runtime the node itself will tell you**, which is what a harness or a
|
|
80
|
+
behaviour picking a random idle needs:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
model.availableAnimations(); // embedded clip names PLUS the scene's $animation assets
|
|
84
|
+
model.boneNames(); // every bone, for BoneAttachment3D's `bone`
|
|
85
|
+
model.findBone('RightHand'); // the Object3D, matching mixamorig prefixes too
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`availableAnimations` is the one to check before setting `animation` from data:
|
|
89
|
+
a clip name that is not in the list plays nothing and says nothing.
|
|
90
|
+
|
|
79
91
|
## Scene JSON
|
|
80
92
|
|
|
81
93
|
```json
|
package/skills/incanto-assets.md
CHANGED
|
@@ -21,13 +21,27 @@ playable character — model, locomotion clips, body, controller, skin and input
|
|
|
21
21
|
## 1. Built-in assets (in the package)
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
bunx incanto-assets list #
|
|
24
|
+
bunx incanto-assets list # name · kind · GRID · what it is
|
|
25
25
|
bunx incanto-assets info medieval-knight # description + animation names
|
|
26
26
|
bunx incanto-assets copy medieval-knight --out public/assets
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`list --json` prints every entry. Each entry carries a **`url`** — the drop-in
|
|
30
|
-
reference you put in scene JSON so the asset LOADS (the contract, see §1b)
|
|
30
|
+
reference you put in scene JSON so the asset LOADS (the contract, see §1b) —
|
|
31
|
+
and, where the art has a grid, the numbers a node needs:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
2dbasic character [animated] 111×83 2dbasic sprite sheet image…
|
|
35
|
+
minecraft-tiles tile 16×16 (25 tiles) Minecraft-themed tiles…
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`frameWidth`/`frameHeight` are what `AnimatedSprite2D.frameWidth` and
|
|
39
|
+
`TileMap2D.tileSize` want; `columns` and `tiles` tell you the highest index a
|
|
40
|
+
`legend` may name (25 tiles means 0–24, and asking for 99 draws clamp-streaks —
|
|
41
|
+
the engine reports it, but the catalog is where you get the number). **The sizes
|
|
42
|
+
live in these fields and nowhere else** — a frame size in the description was a
|
|
43
|
+
second copy, and it drifted: `2dbasic` said 192×192 for art whose real grid is
|
|
44
|
+
111×83.
|
|
31
45
|
|
|
32
46
|
### Categories — the whole built-in set
|
|
33
47
|
|
|
@@ -55,7 +69,7 @@ Every catalog entry has a `url` that is directly usable; there are two classes:
|
|
|
55
69
|
// Tree3D — a bare node already loads oak leaves from the agent8 CDN; override
|
|
56
70
|
// leafTexture only to swap in a different built-in cutout (e.g. ash)
|
|
57
71
|
{ "type": "Tree3D", "props": { "type": "broadleaf",
|
|
58
|
-
"leafTexture": "https://agent8-games.verse8.io/assets/3D/default/textures/vegetation/ash_color.png" } }
|
|
72
|
+
"leafTexture": "https://agent8-games.verse8.io/assets/3D/default/textures/vegetation/ash_color.png" } },
|
|
59
73
|
|
|
60
74
|
// MeshInstance3D material — built-in ground/bark color as a map (+ a normal map)
|
|
61
75
|
{ "type": "MeshInstance3D", "props": { "material": {
|
|
@@ -224,3 +238,11 @@ node keeps its own texture cache. In **2D** the same question is `renderer.asset
|
|
|
224
238
|
(`$ref`, url and reason per failed entry), and the scene EDITOR reads it: a
|
|
225
239
|
failed asset is red in the explorer with the url in its tooltip and the
|
|
226
240
|
consequence in its inspector.
|
|
241
|
+
|
|
242
|
+
**And a texture INSIDE a model.** A `.gltf` + `.bin` + a missing `textures/`
|
|
243
|
+
folder — the ordinary shape of a Blender "glTF Separate" export or a Sketchfab
|
|
244
|
+
download — used to be the one silent case: GLTFLoader swallows a sub-resource
|
|
245
|
+
failure, so the model resolved as a SUCCESS (`status: 'ready'`, `error: null`,
|
|
246
|
+
empty `assetErrors()`, `stats().errors` 0) and the character simply rendered
|
|
247
|
+
flat white. A half-loaded model was indistinguishable from a whole one. Now the
|
|
248
|
+
missing texture is in `assetErrors()` by its URL, like every other one.
|
package/skills/incanto-audio.md
CHANGED
|
@@ -414,17 +414,26 @@ let cursor = 0; // next un-queued note
|
|
|
414
414
|
engine.updated.connect(() => {
|
|
415
415
|
const horizon = engine.sfx.now + LEAD;
|
|
416
416
|
while (cursor < chart.length && startedAt + chart[cursor].atSec <= horizon) {
|
|
417
|
-
hit.
|
|
417
|
+
hit.playAt(startedAt + chart[cursor].atSec); // NOT play() — see below
|
|
418
418
|
cursor++;
|
|
419
419
|
}
|
|
420
420
|
});
|
|
421
421
|
```
|
|
422
422
|
|
|
423
423
|
- **`engine.sfx.now`** — seconds on the audio clock, the one the sound is
|
|
424
|
-
actually placed on.
|
|
425
|
-
|
|
424
|
+
actually placed on. With no AudioContext — headless, and before the first
|
|
425
|
+
sound — it is the engine's own elapsed real time, so the loop above queues the
|
|
426
|
+
whole chart in the verify VM exactly as it does in a browser. (It was a frozen
|
|
427
|
+
`0`, which made that loop queue the first 80 ms of notes and then nothing ever
|
|
428
|
+
again: 1 of 21 beats, with no error anywhere.)
|
|
429
|
+
- **`AudioPlayer.playAt(when)`** and **`engine.sfx.play(params, gain, { when })`** —
|
|
426
430
|
PRESETS only. A `src` clip goes through an `<audio>` element, which has no
|
|
427
|
-
scheduling clock
|
|
431
|
+
scheduling clock, so `playAt` on one plays immediately.
|
|
432
|
+
- **`play()` takes no arguments, deliberately.** It is the method scenes wire
|
|
433
|
+
signals to, and a signal hands its handler whatever it carries — `collected`
|
|
434
|
+
leads with a number, so `collected → play` would have become `play(10)` and
|
|
435
|
+
scheduled the pickup sound at absolute audio-clock second 10. Scheduling has
|
|
436
|
+
its own name.
|
|
428
437
|
- A time already past plays immediately (Web Audio's own rule), so a scheduler
|
|
429
438
|
that ran late is late, not silent.
|
|
430
439
|
- Everything else still applies: the bus gain, `engine.audio.recent()`, and
|
|
@@ -443,7 +452,9 @@ const songSeconds = engine.music.playhead; // null = nothing playing, or no cl
|
|
|
443
452
|
```
|
|
444
453
|
|
|
445
454
|
It is the raw element time, so a looping track wraps to 0 each pass — a loop
|
|
446
|
-
boundary to chart against, not a fault.
|
|
455
|
+
boundary to chart against, not a fault. Headless there is no element and no
|
|
456
|
+
duration to wrap at, so it counts up from 0 for as long as the track plays —
|
|
457
|
+
enough for a harness to prove the chart advanced.
|
|
447
458
|
|
|
448
459
|
## Decision guide
|
|
449
460
|
|
|
@@ -479,6 +490,17 @@ session.engine.audio.recent();
|
|
|
479
490
|
its volume comes from. `countOf(name)` is the assertion you usually want;
|
|
480
491
|
`clearLog()` resets between steps.
|
|
481
492
|
|
|
493
|
+
**A LOOPING sound is recorded once, and its entry carries `loop: true`.**
|
|
494
|
+
Re-recording every pass would evict the rest of the log within seconds for a
|
|
495
|
+
0.2 s preset, so the count stays 1 — which used to make "the alarm loops"
|
|
496
|
+
indistinguishable from "the alarm fired once and stopped". Assert the flag when
|
|
497
|
+
that is the difference you care about:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const alarm = session.engine.audio.recent().find((e) => e.name === 'alarm');
|
|
501
|
+
expect(alarm?.loop).toBe(true);
|
|
502
|
+
```
|
|
503
|
+
|
|
482
504
|
This covers every path: `AudioPlayer.play()` on both the procedural and the
|
|
483
505
|
`src` route, `engine.music.play`/`crossfadeTo`, and `engine.sfx.startVoice`. It
|
|
484
506
|
records the INTENT to play — that the wiring fired — not that a speaker moved;
|
|
@@ -50,10 +50,21 @@ silently breaks the link (`getNodeOrNull` returns null, nothing throws).
|
|
|
50
50
|
```ts
|
|
51
51
|
static readonly props = {
|
|
52
52
|
target: { default: '', nodePath: true }, // fed to getNode/getNodeOrNull
|
|
53
|
+
home: { default: '', nodePath: true, required: true }, // and you MUST set it
|
|
53
54
|
damage: { default: 10 },
|
|
54
55
|
};
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
`""` is the "not set" value of a node-path prop, and `getNodeOrNull('')` returns
|
|
59
|
+
`null` for it — so `const t = this.node.getNodeOrNull(this.target)` is safe to
|
|
60
|
+
write without a guard.
|
|
61
|
+
|
|
62
|
+
**`required: true` when the prop is not optional.** It turns "left empty" into a
|
|
63
|
+
LOAD error naming the prop and the node (`'script:Archer' needs a "home" — it is
|
|
64
|
+
empty. (at '/Level/Enemies/Slime3')`) instead of a behavior that silently does
|
|
65
|
+
nothing all game. It costs one word and it is the best message the engine has
|
|
66
|
+
for an unset prop.
|
|
67
|
+
|
|
57
68
|
## The three-artifact contract (keep them in sync!)
|
|
58
69
|
|
|
59
70
|
1. scene JSON `"script": {"name": "CoinCounter"}`
|
|
@@ -75,10 +86,47 @@ under an existing name (re-registering a different class without it → `DUPLICA
|
|
|
75
86
|
`createGame2D/3D` — they pass `{ engine }` to `loadScene`, attaching it BEFORE
|
|
76
87
|
`onReady` fires. Under a manual boot it throws `TREE_VIOLATION` until
|
|
77
88
|
`engine.setScene(scene)`: prefer `createGame` (or `loadScene(json, { engine })`),
|
|
78
|
-
or defer engine-dependent work to the first `update`.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`update`/`fixedUpdate`, never `onReady`.
|
|
89
|
+
or defer engine-dependent work to the first `update`. Caveats even with the
|
|
90
|
+
engine attached: `engine.scene` is still null (setScene runs after), and the
|
|
91
|
+
scene's `input` actions are not declared yet — query input from
|
|
92
|
+
`update`/`fixedUpdate`, never `onReady`. The scene's `strings` and its
|
|
93
|
+
`connections[]` ARE both in place by then, so `engine.t(...)` resolves and a
|
|
94
|
+
signal you emit from `onReady` reaches its JSON handler.
|
|
95
|
+
|
|
96
|
+
## When your script throws
|
|
97
|
+
|
|
98
|
+
A throw is CONTAINED, not fatal — but only after the scene has loaded, and the
|
|
99
|
+
two halves are different on purpose:
|
|
100
|
+
|
|
101
|
+
- **at load** (`loadScene`, including every `onReady` in the initial pass) a
|
|
102
|
+
throw is a hard failure. That is where an authoring mistake belongs: you want
|
|
103
|
+
to hear about it before the game runs, not to play a game with a piece
|
|
104
|
+
missing.
|
|
105
|
+
- **at runtime** — a spawned prefab, a clone, a node a behaviour adds — a throw
|
|
106
|
+
from `onEnterTree`, `onReady`, `onExitTree`, `update` or `fixedUpdate`
|
|
107
|
+
quarantines THAT script and nothing else. The node keeps its state, the rest
|
|
108
|
+
of the scene keeps running, and `engine.log` names the node, the script and
|
|
109
|
+
the hook.
|
|
110
|
+
|
|
111
|
+
The runtime half used to cover only `update`/`fixedUpdate`, so a spawned
|
|
112
|
+
prefab's `onReady` bug unwound into the `update()` of whatever spawned it: the
|
|
113
|
+
innocent Spawner was quarantined, spawning stopped permanently, and the log
|
|
114
|
+
named the wrong node.
|
|
115
|
+
|
|
116
|
+
**A signal listener is quarantined the same way** — the one that throws is
|
|
117
|
+
disconnected, everything else keeps running, and the log names the node that
|
|
118
|
+
OWNS the listener rather than the one that emitted:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
[incanto] a 'scoreChanged' listener owned by behavior 'Hud' on /World/Hud threw
|
|
122
|
+
— THIS LISTENER is now off; /World/Score, which emitted it, and the rest of the
|
|
123
|
+
scene keep running.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Both spellings, `node.on(...)` and a JSON `connections` wire. Only the JSON
|
|
127
|
+
half used to be covered, so a cosmetic HUD handler with a typo in it disabled
|
|
128
|
+
the SCRIPT OF THE NODE THAT EMITTED — on the five shipped examples that do
|
|
129
|
+
`controller.on('movementStateChanged', …)`, that stops the player moving.
|
|
82
130
|
- `this.rng` — seeded engine randomness: `next()` [0,1), `range(min,max)`,
|
|
83
131
|
`int(min,max)` (inclusive), `pick(arr)`. **Never `Math.random()` in game logic** —
|
|
84
132
|
with `new Engine({ seed: 42 })` a run replays identically (scripted verification).
|
|
@@ -115,19 +163,46 @@ What behavior code actually calls at runtime — all instance methods, no global
|
|
|
115
163
|
| `parent.addChild(node)` | attach a DETACHED node (already-parented → `TREE_VIOLATION`); sibling name collisions auto-rename (`Enemy` → `Enemy2`) |
|
|
116
164
|
| `duplicateNode(node)` | deep-clone via serialize→rebuild — returns a **detached** node; attach it explicitly with `addChild` |
|
|
117
165
|
| `node.queueFree()` | deferred destruction — flushed at the END of the current update pass (queued during a flush = freed next pass) |
|
|
118
|
-
| `node.free()` | immediate detach + teardown (children first
|
|
166
|
+
| `node.free()` | immediate detach + teardown (children first; the signals it owns AND the ones it subscribed to elsewhere are both disconnected — see below) |
|
|
119
167
|
| `root.getNodesByName('Enemy')` | EVERY node with that name in the subtree, document order |
|
|
120
168
|
| `node.getNodeOrNull(path)` | like `getNode` but `null` instead of `NODE_NOT_FOUND` |
|
|
121
169
|
| `node.getNode('%Player')` | unique-name lookup across the whole tree (≥2 matches → `DUPLICATE_UNIQUE_NAME`) |
|
|
170
|
+
| `node.findChild('Skin')` | the first descendant with that name, or `null` — `findChild(name, false)` searches direct children only |
|
|
171
|
+
| `parent.removeChild(node)` | DETACH without tearing down: the node keeps its children and its props, and is yours to `addChild` somewhere else. Nothing frees it — a detached node nobody re-attaches is a leak |
|
|
172
|
+
| `node.reparent(newParent)` | detach-and-attach in one call, which is the safe order (`addChild` on a still-parented node is a `TREE_VIOLATION`) |
|
|
173
|
+
| `node.getPath()` | its absolute path, `/Level/Enemies/Slime3` — what every error message and report prints, and what to log when a lookup surprises you |
|
|
174
|
+
| `other.isInGroup('enemy')` | **the question a trigger asks** — the first line of nearly every `triggerEnter` handler. `node.groups` is a SET, not the array the JSON writes it as, so `groups.includes(…)` is a TypeError |
|
|
175
|
+
| `node.addToGroup('enemies')` / `node.removeFromGroup('enemies')` | tag at RUNTIME; `groups` in the JSON is the same set, declared |
|
|
122
176
|
| `this.node.tree?.getNodesInGroup('enemies')` | group query (also `tree.callGroup(group, method, ...args)`) |
|
|
123
177
|
| `engine.stop()` / `engine.start()` | pause / resume the loop — stop resets the clock and accumulator, so no banked sim time leaks into the resume |
|
|
124
178
|
| `engine.step()` | advance exactly ONE fixed step + one update (both dt = the fixed step) — the unit of time for headless tests |
|
|
125
179
|
| `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 |
|
|
126
180
|
| `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 |
|
|
181
|
+
| `node.off(signal, fn)` | disconnect ONE listener you connected with `on` — pass the same function reference. (`free()` disconnects everything a node owns, so this is for a listener that must stop while the node lives on) |
|
|
182
|
+
| `node.signal('died')` | the `Signal` object itself, for `connect`/`disconnect` by hand; undeclared names throw, like `emit` |
|
|
183
|
+
| `node.listenerCount('died')` | how many are listening — a test's way to prove a wire was made, or dropped |
|
|
127
184
|
| `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()` |
|
|
128
185
|
|
|
129
186
|
The recurring traps: `duplicateNode` does NOT insert the clone anywhere — a
|
|
130
|
-
"spawner that does nothing" usually forgot `addChild`.
|
|
187
|
+
"spawner that does nothing" usually forgot `addChild`.
|
|
188
|
+
|
|
189
|
+
**The template you clone FROM is a live node.** Its behaviors run, its timers
|
|
190
|
+
tick, its turret shoots — `visible: false` hides a node, it does not switch it
|
|
191
|
+
off. `Spawner` and `WaveSpawner` sidestep this by DETACHING their `prefab` at
|
|
192
|
+
ready; a shelf of templates you clone yourself is still in the tree, so author
|
|
193
|
+
their scripts `"enabled": false` and wake the clone:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const tower = duplicateNode(this.node.getNode('/Game/Prefabs/Tower'));
|
|
197
|
+
tower.behavior?.enable(); // the template stays asleep, this one works
|
|
198
|
+
this.node.getNode('/Game/Towers').addChild(tower);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Without it an invisible tower at the origin defends your map, and the game looks
|
|
202
|
+
fine. **And look the template up by PATH, not `%Name`**: a clone keeps its
|
|
203
|
+
template's name, so `%Tower` is unambiguous exactly until the first one is
|
|
204
|
+
placed, and then it throws `DUPLICATE_UNIQUE_NAME` from inside your build
|
|
205
|
+
handler — a game that works once. `engine.tick(16)` does
|
|
131
206
|
not mean "advance 16ms": tick wants wall-clock timestamps, so scripted loops
|
|
132
207
|
should call `engine.step()` instead. And `queueFree` inside `update` is always
|
|
133
208
|
safe — the node keeps existing until the pass ends.
|
|
@@ -209,6 +284,8 @@ no TypeScript, and all five are clean.
|
|
|
209
284
|
{ "name": "Spawner", "type": "Timer", "props": { "waitTime": 2, "autostart": true } }
|
|
210
285
|
```
|
|
211
286
|
Emits `timeout` every `waitTime` s (`oneShot` for once). API: `start(time?)`, `stop()`, `running`.
|
|
287
|
+
A `waitTime` of 0 is a load error: the update guard stops a timer with no period
|
|
288
|
+
on its first frame, so it would never fire and never say so.
|
|
212
289
|
|
|
213
290
|
## CharacterController2D (JSON-only playable characters)
|
|
214
291
|
|
|
@@ -15,6 +15,7 @@ Prerequisite: `incanto-scene-json-authoring.md` (this directory) for the file fo
|
|
|
15
15
|
```bash
|
|
16
16
|
bunx incanto new my-game --template platformer-2d # tilemap, jump feel, follow cam, coins
|
|
17
17
|
bunx incanto new my-game --template star-survivor # endless waves, auto-attack, upgrades
|
|
18
|
+
bunx incanto new my-game --template molehill-2d # played with the MOUSE — no character at all
|
|
18
19
|
bunx incanto new --list # every starter, 3D and 2D
|
|
19
20
|
```
|
|
20
21
|
|
|
@@ -95,6 +96,10 @@ stop hand-placing dozens of ColorRect walls:
|
|
|
95
96
|
- `.` / space = empty; digits `0-9` = atlas tile index directly; any OTHER char
|
|
96
97
|
must be in `legend` — unknown chars **hard-fail at load** (fix the map, not
|
|
97
98
|
the runtime).
|
|
99
|
+
- **A legend value is an INDEX, not a description.** Every other tilemap format
|
|
100
|
+
puts solidity in the tile definition, so `"legend": { "#": { "solid": true } }`
|
|
101
|
+
is the natural guess — and it used to load clean and produce a level with no
|
|
102
|
+
floor. It is a hard error now, naming the `solid` prop that was meant.
|
|
98
103
|
- `solid` tiles are merged into a handful of `StaticBody2D` rect colliders
|
|
99
104
|
(greedy rectangles), so a 100×50 level costs a few bodies, not thousands.
|
|
100
105
|
- Cell (0,0) hangs its TOP-LEFT on the node's origin; position the node to
|
|
@@ -103,7 +108,25 @@ stop hand-placing dozens of ColorRect walls:
|
|
|
103
108
|
- No texture yet? Colliders still work — pair with ColorRect2D placeholders or
|
|
104
109
|
just leave it invisible while you block out the level.
|
|
105
110
|
- Change the map at runtime by REPLACING `cells` (mutations are not watched):
|
|
106
|
-
`map.cells = [...rows]` — geometry and colliders rebuild next frame.
|
|
111
|
+
`map.cells = [...rows]` — geometry and colliders rebuild next frame. For ONE
|
|
112
|
+
cell use `setTile` below, which does that correctly.
|
|
113
|
+
|
|
114
|
+
**Pointing at tiles** — the whole input of a tile game, and four calls:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const cell = map.cellAt(...this.engine.pointerWorld() ?? []); // [cx, cy] | null
|
|
118
|
+
if (cell && map.tileAt(...cell) === 1) {
|
|
119
|
+
map.setTile(cell[0], cell[1], '.'); // dig it out (a CHARACTER)
|
|
120
|
+
marker.position = map.worldAt(...cell); // the cell's CENTRE
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`cellAt` is null off the grid, `tileAt` is `-1` for an empty cell or one that is
|
|
125
|
+
not there, and `worldAt` gives the cell's **centre** — the half-tile a
|
|
126
|
+
hand-rolled version forgets. All four measure from the node's WORLD position
|
|
127
|
+
(ancestors composed), which is the other thing hand-rolling gets wrong. An
|
|
128
|
+
ancestor that SCALES or rotates the map moves the picture and not the answer,
|
|
129
|
+
and `incanto-check` says so.
|
|
107
130
|
|
|
108
131
|
### `Sprite2D`
|
|
109
132
|
| Prop | Default | Notes |
|
|
@@ -212,6 +235,42 @@ offsets that survive any canvas size: a `Label` at `[-16, 16]` under a
|
|
|
212
235
|
`top-right` layer hugs the corner everywhere. Misspelled anchors fail at load
|
|
213
236
|
listing the valid set. With a viewport design, UI coordinates are design px.
|
|
214
237
|
|
|
238
|
+
**Two spellings, and the engine means it.** `UILayer` writes them with hyphens
|
|
239
|
+
(`top-left`); the `HudLayer` widgets write them in camel case (`topLeft`). Nine
|
|
240
|
+
of the same anchors, two vocabularies, and neither accepts the other's — a
|
|
241
|
+
`UiText` with `"anchor": "top-left"` is a load error, and so is a `UILayer` with
|
|
242
|
+
`"topLeft"`. Both errors print their own valid set, so you are one edit from
|
|
243
|
+
right; this note is so you know why.
|
|
244
|
+
|
|
245
|
+
### A `UILayer` still SCALES. On a phone, use `HudLayer`
|
|
246
|
+
|
|
247
|
+
`UILayer` ignores the camera; it does not ignore the viewport. Its contents are
|
|
248
|
+
design pixels multiplied by the same scale as the world, and on a portrait phone
|
|
249
|
+
that scale is small:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
1280×800 scale 1.333 a fontSize 15 Label paints at 20 device px
|
|
253
|
+
390×844 scale 0.406 the same Label paints at 6.1 device px
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Six device pixels is an unreadable smear, and it is the flagship 2D template's
|
|
257
|
+
control hint before this was fixed. The vertical framing goes the same way:
|
|
258
|
+
`design: [960, 540]` under `fit: "expand"` shows a portrait phone **2077 world
|
|
259
|
+
px** of height, so the play surface is a band with acres of empty sky.
|
|
260
|
+
|
|
261
|
+
**`HudLayer` and the `Ui*` widgets are DOM at their declared CSS pixels** and are
|
|
262
|
+
immune — which is why the engine's own volume sliders and touch controls stayed
|
|
263
|
+
legible in the same capture that reduced the template's HUD to a grey line. Use
|
|
264
|
+
them for anything a player has to READ:
|
|
265
|
+
|
|
266
|
+
| | |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `Label` under `UILayer` | part of the picture — damage numbers, world-anchored callouts, art |
|
|
269
|
+
| `UiText` under `HudLayer` | part of the interface — score, hearts, hints, menus |
|
|
270
|
+
|
|
271
|
+
See `incanto-hud.md`. It costs nothing on desktop and it is the difference
|
|
272
|
+
between a readable and an unusable phone build.
|
|
273
|
+
|
|
215
274
|
## Patterns
|
|
216
275
|
|
|
217
276
|
- Group pickups/enemies (`"groups": ["coins"]`) and query `scene.tree.getNodesInGroup('coins')`.
|
|
@@ -245,13 +304,33 @@ listing the valid set. With a viewport design, UI coordinates are design px.
|
|
|
245
304
|
touched. Its inverse, `renderer.screenFromWorld(wx, wy)`, pins DOM to the
|
|
246
305
|
world (see incanto-web-integration).
|
|
247
306
|
|
|
307
|
+
**From a Behavior, ask the ENGINE** — `this.engine` is all a behavior has, and
|
|
308
|
+
the renderer is not on it:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const at = this.engine.pointerWorld(); // world px, or null
|
|
312
|
+
if (at) this.node.position = at; // it IS a position array
|
|
313
|
+
this.engine.toWorld(sx, sy); // any screen point
|
|
314
|
+
this.engine.toScreen(this.node.position); // → { x, y, behind }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The renderer installs these, exactly as it installs `engine.picker`: `pickAt`
|
|
318
|
+
answers WHICH NODE the cursor is on, `pointerWorld` answers WHERE it is, and a
|
|
319
|
+
drag-and-launch needs the second one. Both are null with no renderer, and a
|
|
320
|
+
`runScript` `click` step (or `incanto-play`'s `at`) installs a geometric
|
|
321
|
+
version for the run — so aiming is testable headless, not only clicking.
|
|
322
|
+
|
|
248
323
|
## Game flow recipes
|
|
249
324
|
|
|
250
325
|
- **Scene transition** (level 2, title → game):
|
|
251
326
|
```ts
|
|
252
327
|
import { loadScene } from 'incanto';
|
|
253
|
-
game.engine.setScene(loadScene(level2Json));
|
|
328
|
+
game.engine.setScene(loadScene(level2Json, { engine: game.engine }));
|
|
254
329
|
```
|
|
330
|
+
Pass `{ engine }`: `onReady` runs during the LOAD, so a behaviour that reads
|
|
331
|
+
`this.engine` or `this.rng` there — `Wander` draws its first heading in it —
|
|
332
|
+
throws `TREE_VIOLATION` without it.
|
|
333
|
+
|
|
255
334
|
`setScene` frees the old root, clears + redeclares the input map from the new
|
|
256
335
|
scene's `input{}`, and emits `sceneChanged` — `createGame2D`'s touch overlay
|
|
257
336
|
rebuilds itself on that signal, the PHYSICS world registers the new scene's
|
|
@@ -266,7 +345,7 @@ listing the valid set. With a viewport design, UI coordinates are design px.
|
|
|
266
345
|
last one is why the game-over recipe below is safe — a swap out of a frozen
|
|
267
346
|
screen boots the next level running, not frozen.
|
|
268
347
|
- **Game over / restart**: swap to a fresh load of the SAME JSON —
|
|
269
|
-
`engine.setScene(loadScene(levelJson))`. `loadScene` treats the JSON as
|
|
348
|
+
`engine.setScene(loadScene(levelJson, { engine }))`. `loadScene` treats the JSON as
|
|
270
349
|
read-only (everything it keeps is cloned), so reloading the same imported
|
|
271
350
|
object yields a clean run; only clone (`structuredClone(levelJson)`) first if
|
|
272
351
|
your own code mutated that object. (`createGame`'s `scene` option already
|
|
@@ -294,7 +373,7 @@ constructor).
|
|
|
294
373
|
## Particles (effects in one node)
|
|
295
374
|
|
|
296
375
|
```json
|
|
297
|
-
{ "name": "Torch", "type": "Particles2D", "props": { "preset": "fire" } }
|
|
376
|
+
{ "name": "Torch", "type": "Particles2D", "props": { "preset": "fire" } },
|
|
298
377
|
{ "name": "Boom", "type": "Particles2D",
|
|
299
378
|
"props": { "preset": "explosion", "rate": 0, "burst": 60 } }
|
|
300
379
|
```
|
|
@@ -394,7 +473,7 @@ anything a 540 px window can show.
|
|
|
394
473
|
|
|
395
474
|
```
|
|
396
475
|
camera /Game/Camera centred [480, 274] showing 960×540px
|
|
397
|
-
11 in view, 2 outside it
|
|
476
|
+
11 in view, 2 outside it, 1 hidden (visible: false)
|
|
398
477
|
onScreen /Game/Level (TileMap2D) [0, 0] screen [0.333, -0.007] 160px
|
|
399
478
|
onScreen /Game/Gems/Gem1/Icon (Sprite2D) [512, 428] screen [0.067, 0.57] 157px
|
|
400
479
|
offscreen /Game/Goblins/Goblin2/Skin (AnimatedSprite2D) [1002, 512] … 574px
|
|
@@ -405,14 +484,26 @@ camera /Game/Camera centred [480, 274] showing 960×540px
|
|
|
405
484
|
edge". A node counts as in view when its BOX is — a `ColorRect2D`'s `size`, a
|
|
406
485
|
`TileMap2D`'s whole grid (which hangs from its TOP-LEFT, not its centre), or a
|
|
407
486
|
collider. A bare `Sprite2D` declares no size, so read `offscreen` there as "its
|
|
408
|
-
ORIGIN is outside the view".
|
|
409
|
-
|
|
410
|
-
|
|
487
|
+
ORIGIN is outside the view". `hidden` is `visible: false` on the node or on any
|
|
488
|
+
ancestor — counted apart from `offscreen` because the fix differs, and printed as
|
|
489
|
+
`NOTHING IS DRAWN` when it is every drawable in the scene. Nothing is ever
|
|
490
|
+
"behind" a 2D camera and nothing is lit, so neither is reported; `overlap` is
|
|
491
|
+
real interpenetration, not resting on a platform. HUD widgets are screen-space
|
|
492
|
+
and are left out entirely.
|
|
411
493
|
|
|
412
494
|
Then read `stats().errors` (something threw and got skipped) and `assetErrors()`
|
|
413
495
|
(a texture 404'd). All of these are silent failures otherwise — the screen just
|
|
414
496
|
looks wrong, or empty, and nothing throws.
|
|
415
497
|
|
|
498
|
+
**A game with no jump key is fine.** `jumpAction`, `dashAction` and even
|
|
499
|
+
`moveAction` may name an action the scene never declares — a top-down RPG has
|
|
500
|
+
nothing to jump over — and the character simply never does that thing. Until
|
|
501
|
+
this was fixed the controller THREW every frame, the node was quarantined, and
|
|
502
|
+
the character could not move either: a top-down game that declared only `move`
|
|
503
|
+
reported `✗ error in 1/1` and stood still, with the reason in a log nobody
|
|
504
|
+
prints. An action you TYPED that does not exist is still reported, once, because
|
|
505
|
+
that is a typo and not an omission.
|
|
506
|
+
|
|
416
507
|
## Platformer game feel (CharacterController2D)
|
|
417
508
|
|
|
418
509
|
A jump that only fires while `isOnFloor()` is true feels BROKEN, and players do
|
|
@@ -462,6 +553,13 @@ entry, which is what the states you have no artwork for should be:
|
|
|
462
553
|
}
|
|
463
554
|
```
|
|
464
555
|
|
|
556
|
+
**The whole map is checked when the scene loads**, not when a clip first plays:
|
|
557
|
+
a clip is `{frames, fps, loop}` and nothing else, so `frame`, `frameRate`,
|
|
558
|
+
`speed` and `repeat` — all real fields in the sheet formats people convert from
|
|
559
|
+
— are load errors naming the field you meant. Only the aliases used to be
|
|
560
|
+
checked, so a misspelled `frames` was a hard error the first time the character
|
|
561
|
+
walked, twenty minutes in.
|
|
562
|
+
|
|
465
563
|
`incanto-assets copy` and `spriteFromLibraryMeta` already print these aliases,
|
|
466
564
|
so pasting their output next to the wiring above works as-is; point any of them
|
|
467
565
|
somewhere better when you have the art. Aliases may chain (`fall` → `jump` →
|