incanto 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- 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 +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- 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 +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.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-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.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-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- 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 +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- 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 +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- 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 +1205 -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/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- 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 +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- 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 +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- 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 +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.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-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- package/editor/assets/index-CZIRr5EL.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-DK9xMGpW.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.71.0",
|
|
4
4
|
"description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"game-engine",
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
"build": "tsdown",
|
|
56
56
|
"dev": "tsdown --watch",
|
|
57
57
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
|
|
58
|
-
"test": "vitest run --project unit --root ../.."
|
|
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",
|
|
@@ -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
|
|
@@ -6737,6 +6757,10 @@
|
|
|
6737
6757
|
"type": "object",
|
|
6738
6758
|
"default": {}
|
|
6739
6759
|
},
|
|
6760
|
+
"colors": {
|
|
6761
|
+
"type": "object",
|
|
6762
|
+
"default": {}
|
|
6763
|
+
},
|
|
6740
6764
|
"solid": {
|
|
6741
6765
|
"type": "array",
|
|
6742
6766
|
"default": []
|
|
@@ -9442,7 +9466,14 @@
|
|
|
9442
9466
|
"default": true
|
|
9443
9467
|
},
|
|
9444
9468
|
"underwater": {
|
|
9445
|
-
"
|
|
9469
|
+
"anyOf": [
|
|
9470
|
+
{
|
|
9471
|
+
"type": "boolean"
|
|
9472
|
+
},
|
|
9473
|
+
{
|
|
9474
|
+
"type": "object"
|
|
9475
|
+
}
|
|
9476
|
+
],
|
|
9446
9477
|
"default": true
|
|
9447
9478
|
},
|
|
9448
9479
|
"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
|
@@ -69,7 +69,7 @@ Every catalog entry has a `url` that is directly usable; there are two classes:
|
|
|
69
69
|
// Tree3D — a bare node already loads oak leaves from the agent8 CDN; override
|
|
70
70
|
// leafTexture only to swap in a different built-in cutout (e.g. ash)
|
|
71
71
|
{ "type": "Tree3D", "props": { "type": "broadleaf",
|
|
72
|
-
"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" } },
|
|
73
73
|
|
|
74
74
|
// MeshInstance3D material — built-in ground/bark color as a map (+ a normal map)
|
|
75
75
|
{ "type": "MeshInstance3D", "props": { "material": {
|
|
@@ -238,3 +238,11 @@ node keeps its own texture cache. In **2D** the same question is `renderer.asset
|
|
|
238
238
|
(`$ref`, url and reason per failed entry), and the scene EDITOR reads it: a
|
|
239
239
|
failed asset is red in the explorer with the url in its tooltip and the
|
|
240
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
|
@@ -421,7 +421,11 @@ engine.updated.connect(() => {
|
|
|
421
421
|
```
|
|
422
422
|
|
|
423
423
|
- **`engine.sfx.now`** — seconds on the audio clock, the one the sound is
|
|
424
|
-
actually placed on.
|
|
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.)
|
|
425
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
431
|
scheduling clock, so `playAt` on one plays immediately.
|
|
@@ -448,7 +452,9 @@ const songSeconds = engine.music.playhead; // null = nothing playing, or no cl
|
|
|
448
452
|
```
|
|
449
453
|
|
|
450
454
|
It is the raw element time, so a looping track wraps to 0 each pass — a loop
|
|
451
|
-
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.
|
|
452
458
|
|
|
453
459
|
## Decision guide
|
|
454
460
|
|
|
@@ -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,67 @@ 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
|
-
| `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 |
|
|
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. It fires with **null** at `dispose()` too — a handler that reads `scene.root` has to check, or teardown says so and continues without it |
|
|
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, its `autoplay` audio plays — `visible: false` hides a
|
|
191
|
+
node, it does not switch it off. `Spawner` and `WaveSpawner` sidestep this by
|
|
192
|
+
DETACHING their `prefab` at enter. For a shelf of templates you clone yourself,
|
|
193
|
+
put `PrefabShelf` on the shelf node and it does the same for all of them:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{ "name": "Prefabs", "type": "Node2D", "script": { "name": "PrefabShelf" },
|
|
197
|
+
"children": [ { "name": "Tower", "type": "Node2D", "children": [] } ] }
|
|
198
|
+
```
|
|
199
|
+
```ts
|
|
200
|
+
const shelf = this.node.getNode('/Game/Prefabs').behavior as PrefabShelf;
|
|
201
|
+
const tower = shelf.make('Tower'); // detached, awake all the way down
|
|
202
|
+
tower.position = at; // set it up BEFORE it readies
|
|
203
|
+
this.node.getNode('/Game/Towers').addChild(tower);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Its children never ENTER the tree, so nothing has to be hidden and nothing has
|
|
207
|
+
to be asleep. `make()` returns the clone DETACHED, like `duplicateNode` — not a
|
|
208
|
+
formality: `onReady` fires on attach, and a behavior that banks its node's
|
|
209
|
+
position there (`FloatAway`) would bank the wrong one. `names()` lists what the
|
|
210
|
+
shelf holds, and asking for anything else fails saying so.
|
|
211
|
+
|
|
212
|
+
**Why not just author the templates `"enabled": false`?** That was the old
|
|
213
|
+
advice and it half-works: `enabled` is a per-behavior pause switch, so
|
|
214
|
+
`clone.behavior?.enable()` wakes ONE node — while the shape this same skill
|
|
215
|
+
teaches (one behavior per node, so a prefab's parts live on children) leaves
|
|
216
|
+
every child asleep, silently and for good. Measured on a shipped tower defense:
|
|
217
|
+
the bolt's root `Projectile` was woken and its sibling `Lifetime` was not, so
|
|
218
|
+
every bolt that MISSED flew forever — five still in the tree at 69 seconds. And
|
|
219
|
+
`enabled` never silenced the template's `autoplay` audio at all, which is how
|
|
220
|
+
that game's `feels` rung read `every one of the 4 emitters fired` while two of
|
|
221
|
+
the four were templates going off on the shelf. `enabled: false` is for a state
|
|
222
|
+
a node deliberately starts in (a `Chase` waiting on `spotted`), not for hiding a
|
|
223
|
+
template from the tree. **And look the template up by PATH, not `%Name`**: a clone keeps its
|
|
224
|
+
template's name, so `%Tower` is unambiguous exactly until the first one is
|
|
225
|
+
placed, and then it throws `DUPLICATE_UNIQUE_NAME` from inside your build
|
|
226
|
+
handler — a game that works once. `engine.tick(16)` does
|
|
131
227
|
not mean "advance 16ms": tick wants wall-clock timestamps, so scripted loops
|
|
132
228
|
should call `engine.step()` instead. And `queueFree` inside `update` is always
|
|
133
229
|
safe — the node keeps existing until the pass ends.
|
|
@@ -209,6 +305,8 @@ no TypeScript, and all five are clean.
|
|
|
209
305
|
{ "name": "Spawner", "type": "Timer", "props": { "waitTime": 2, "autostart": true } }
|
|
210
306
|
```
|
|
211
307
|
Emits `timeout` every `waitTime` s (`oneShot` for once). API: `start(time?)`, `stop()`, `running`.
|
|
308
|
+
A `waitTime` of 0 is a load error: the update guard stops a timer with no period
|
|
309
|
+
on its first frame, so it would never fire and never say so.
|
|
212
310
|
|
|
213
311
|
## CharacterController2D (JSON-only playable characters)
|
|
214
312
|
|
|
@@ -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,15 +96,55 @@ 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
|
|
101
106
|
place the level. `columns: 0` (default) derives atlas columns from the
|
|
102
107
|
texture width.
|
|
103
|
-
- No
|
|
104
|
-
|
|
108
|
+
- **No art yet? Paint with `colors`.** A char → `#rrggbb` map draws that cell as
|
|
109
|
+
a flat quad, in the same single draw call, with no atlas at all:
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
{ "name": "Walls", "type": "TileMap2D", "props": {
|
|
113
|
+
"tileSize": 56,
|
|
114
|
+
"cells": ["########", "#......#", "########"],
|
|
115
|
+
"colors": { "#": "#3a4356" },
|
|
116
|
+
"solid": ["#"]
|
|
117
|
+
} }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Exclusive with `texture` (one material, one draw call — a half-textured level
|
|
121
|
+
that loads clean is exactly the silence this engine fails loudly about), and
|
|
122
|
+
`solid` takes the CHAR you painted as well as an atlas index, because that is
|
|
123
|
+
the thing an author knows. Everything else is unchanged: `cellAt`, `tileAt`,
|
|
124
|
+
`setTile`, `worldAt` and the merged colliders work the same. It exists because
|
|
125
|
+
a sokoban built from the published package came out as 24 hand-placed
|
|
126
|
+
`ColorRect2D` walls — the one node that answers every question a grid game
|
|
127
|
+
asks could not be SEEN.
|
|
105
128
|
- Change the map at runtime by REPLACING `cells` (mutations are not watched):
|
|
106
|
-
`map.cells = [...rows]` — geometry and colliders rebuild next frame.
|
|
129
|
+
`map.cells = [...rows]` — geometry and colliders rebuild next frame. For ONE
|
|
130
|
+
cell use `setTile` below, which does that correctly.
|
|
131
|
+
|
|
132
|
+
**Pointing at tiles** — the whole input of a tile game, and four calls:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const cell = map.cellAt(...this.engine.pointerWorld() ?? []); // [cx, cy] | null
|
|
136
|
+
if (cell && map.tileAt(...cell) === 1) {
|
|
137
|
+
map.setTile(cell[0], cell[1], '.'); // dig it out (a CHARACTER)
|
|
138
|
+
marker.position = map.worldAt(...cell); // the cell's CENTRE
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`cellAt` is null off the grid, `tileAt` is `-1` for an empty cell or one that is
|
|
143
|
+
not there, and `worldAt` gives the cell's **centre** — the half-tile a
|
|
144
|
+
hand-rolled version forgets. All four measure from the node's WORLD position
|
|
145
|
+
(ancestors composed), which is the other thing hand-rolling gets wrong. An
|
|
146
|
+
ancestor that SCALES or rotates the map moves the picture and not the answer,
|
|
147
|
+
and `incanto-check` says so.
|
|
107
148
|
|
|
108
149
|
### `Sprite2D`
|
|
109
150
|
| Prop | Default | Notes |
|
|
@@ -212,6 +253,13 @@ offsets that survive any canvas size: a `Label` at `[-16, 16]` under a
|
|
|
212
253
|
`top-right` layer hugs the corner everywhere. Misspelled anchors fail at load
|
|
213
254
|
listing the valid set. With a viewport design, UI coordinates are design px.
|
|
214
255
|
|
|
256
|
+
**Two spellings, and the engine means it.** `UILayer` writes them with hyphens
|
|
257
|
+
(`top-left`); the `HudLayer` widgets write them in camel case (`topLeft`). Nine
|
|
258
|
+
of the same anchors, two vocabularies, and neither accepts the other's — a
|
|
259
|
+
`UiText` with `"anchor": "top-left"` is a load error, and so is a `UILayer` with
|
|
260
|
+
`"topLeft"`. Both errors print their own valid set, so you are one edit from
|
|
261
|
+
right; this note is so you know why.
|
|
262
|
+
|
|
215
263
|
### A `UILayer` still SCALES. On a phone, use `HudLayer`
|
|
216
264
|
|
|
217
265
|
`UILayer` ignores the camera; it does not ignore the viewport. Its contents are
|
|
@@ -274,13 +322,33 @@ between a readable and an unusable phone build.
|
|
|
274
322
|
touched. Its inverse, `renderer.screenFromWorld(wx, wy)`, pins DOM to the
|
|
275
323
|
world (see incanto-web-integration).
|
|
276
324
|
|
|
325
|
+
**From a Behavior, ask the ENGINE** — `this.engine` is all a behavior has, and
|
|
326
|
+
the renderer is not on it:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const at = this.engine.pointerWorld(); // world px, or null
|
|
330
|
+
if (at) this.node.position = at; // it IS a position array
|
|
331
|
+
this.engine.toWorld(sx, sy); // any screen point
|
|
332
|
+
this.engine.toScreen(this.node.position); // → { x, y, behind }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The renderer installs these, exactly as it installs `engine.picker`: `pickAt`
|
|
336
|
+
answers WHICH NODE the cursor is on, `pointerWorld` answers WHERE it is, and a
|
|
337
|
+
drag-and-launch needs the second one. Both are null with no renderer, and a
|
|
338
|
+
`runScript` `click` step (or `incanto-play`'s `at`) installs a geometric
|
|
339
|
+
version for the run — so aiming is testable headless, not only clicking.
|
|
340
|
+
|
|
277
341
|
## Game flow recipes
|
|
278
342
|
|
|
279
343
|
- **Scene transition** (level 2, title → game):
|
|
280
344
|
```ts
|
|
281
345
|
import { loadScene } from 'incanto';
|
|
282
|
-
game.engine.setScene(loadScene(level2Json));
|
|
346
|
+
game.engine.setScene(loadScene(level2Json, { engine: game.engine }));
|
|
283
347
|
```
|
|
348
|
+
Pass `{ engine }`: `onReady` runs during the LOAD, so a behaviour that reads
|
|
349
|
+
`this.engine` or `this.rng` there — `Wander` draws its first heading in it —
|
|
350
|
+
throws `TREE_VIOLATION` without it.
|
|
351
|
+
|
|
284
352
|
`setScene` frees the old root, clears + redeclares the input map from the new
|
|
285
353
|
scene's `input{}`, and emits `sceneChanged` — `createGame2D`'s touch overlay
|
|
286
354
|
rebuilds itself on that signal, the PHYSICS world registers the new scene's
|
|
@@ -295,7 +363,7 @@ between a readable and an unusable phone build.
|
|
|
295
363
|
last one is why the game-over recipe below is safe — a swap out of a frozen
|
|
296
364
|
screen boots the next level running, not frozen.
|
|
297
365
|
- **Game over / restart**: swap to a fresh load of the SAME JSON —
|
|
298
|
-
`engine.setScene(loadScene(levelJson))`. `loadScene` treats the JSON as
|
|
366
|
+
`engine.setScene(loadScene(levelJson, { engine }))`. `loadScene` treats the JSON as
|
|
299
367
|
read-only (everything it keeps is cloned), so reloading the same imported
|
|
300
368
|
object yields a clean run; only clone (`structuredClone(levelJson)`) first if
|
|
301
369
|
your own code mutated that object. (`createGame`'s `scene` option already
|
|
@@ -323,7 +391,7 @@ constructor).
|
|
|
323
391
|
## Particles (effects in one node)
|
|
324
392
|
|
|
325
393
|
```json
|
|
326
|
-
{ "name": "Torch", "type": "Particles2D", "props": { "preset": "fire" } }
|
|
394
|
+
{ "name": "Torch", "type": "Particles2D", "props": { "preset": "fire" } },
|
|
327
395
|
{ "name": "Boom", "type": "Particles2D",
|
|
328
396
|
"props": { "preset": "explosion", "rate": 0, "burst": 60 } }
|
|
329
397
|
```
|
|
@@ -423,7 +491,7 @@ anything a 540 px window can show.
|
|
|
423
491
|
|
|
424
492
|
```
|
|
425
493
|
camera /Game/Camera centred [480, 274] showing 960×540px
|
|
426
|
-
11 in view, 2 outside it
|
|
494
|
+
11 in view, 2 outside it, 1 hidden (visible: false)
|
|
427
495
|
onScreen /Game/Level (TileMap2D) [0, 0] screen [0.333, -0.007] 160px
|
|
428
496
|
onScreen /Game/Gems/Gem1/Icon (Sprite2D) [512, 428] screen [0.067, 0.57] 157px
|
|
429
497
|
offscreen /Game/Goblins/Goblin2/Skin (AnimatedSprite2D) [1002, 512] … 574px
|
|
@@ -434,14 +502,26 @@ camera /Game/Camera centred [480, 274] showing 960×540px
|
|
|
434
502
|
edge". A node counts as in view when its BOX is — a `ColorRect2D`'s `size`, a
|
|
435
503
|
`TileMap2D`'s whole grid (which hangs from its TOP-LEFT, not its centre), or a
|
|
436
504
|
collider. A bare `Sprite2D` declares no size, so read `offscreen` there as "its
|
|
437
|
-
ORIGIN is outside the view".
|
|
438
|
-
|
|
439
|
-
|
|
505
|
+
ORIGIN is outside the view". `hidden` is `visible: false` on the node or on any
|
|
506
|
+
ancestor — counted apart from `offscreen` because the fix differs, and printed as
|
|
507
|
+
`NOTHING IS DRAWN` when it is every drawable in the scene. Nothing is ever
|
|
508
|
+
"behind" a 2D camera and nothing is lit, so neither is reported; `overlap` is
|
|
509
|
+
real interpenetration, not resting on a platform. HUD widgets are screen-space
|
|
510
|
+
and are left out entirely.
|
|
440
511
|
|
|
441
512
|
Then read `stats().errors` (something threw and got skipped) and `assetErrors()`
|
|
442
513
|
(a texture 404'd). All of these are silent failures otherwise — the screen just
|
|
443
514
|
looks wrong, or empty, and nothing throws.
|
|
444
515
|
|
|
516
|
+
**A game with no jump key is fine.** `jumpAction`, `dashAction` and even
|
|
517
|
+
`moveAction` may name an action the scene never declares — a top-down RPG has
|
|
518
|
+
nothing to jump over — and the character simply never does that thing. Until
|
|
519
|
+
this was fixed the controller THREW every frame, the node was quarantined, and
|
|
520
|
+
the character could not move either: a top-down game that declared only `move`
|
|
521
|
+
reported `✗ error in 1/1` and stood still, with the reason in a log nobody
|
|
522
|
+
prints. An action you TYPED that does not exist is still reported, once, because
|
|
523
|
+
that is a typo and not an omission.
|
|
524
|
+
|
|
445
525
|
## Platformer game feel (CharacterController2D)
|
|
446
526
|
|
|
447
527
|
A jump that only fires while `isOnFloor()` is true feels BROKEN, and players do
|
|
@@ -491,6 +571,13 @@ entry, which is what the states you have no artwork for should be:
|
|
|
491
571
|
}
|
|
492
572
|
```
|
|
493
573
|
|
|
574
|
+
**The whole map is checked when the scene loads**, not when a clip first plays:
|
|
575
|
+
a clip is `{frames, fps, loop}` and nothing else, so `frame`, `frameRate`,
|
|
576
|
+
`speed` and `repeat` — all real fields in the sheet formats people convert from
|
|
577
|
+
— are load errors naming the field you meant. Only the aliases used to be
|
|
578
|
+
checked, so a misspelled `frames` was a hard error the first time the character
|
|
579
|
+
walked, twenty minutes in.
|
|
580
|
+
|
|
494
581
|
`incanto-assets copy` and `spriteFromLibraryMeta` already print these aliases,
|
|
495
582
|
so pasting their output next to the wiring above works as-is; point any of them
|
|
496
583
|
somewhere better when you have the art. Aliases may chain (`fall` → `jump` →
|