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
|
@@ -324,3 +324,55 @@ rendered a five-pass frame at 3.3 ms where `low` rendered one pass at 1.24 ms
|
|
|
324
324
|
A tier is also REVERSIBLE now: it is applied against the scene FILE rather than
|
|
325
325
|
against whatever the last tier left behind, so `low` → `high` restores exactly
|
|
326
326
|
what you authored instead of leaving it stripped.
|
|
327
|
+
|
|
328
|
+
## How big a game actually is, and the chunk named after the wrong thing
|
|
329
|
+
|
|
330
|
+
Measured on all six templates, scaffolded from the packed tarball and built:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
eager on disk
|
|
334
|
+
molehill-2d 967 KB 5.8 MB
|
|
335
|
+
platformer-2d 981 KB 5.9 MB
|
|
336
|
+
star-survivor 962 KB 5.9 MB
|
|
337
|
+
beacon-isle-3d 1414 KB 5.8 MB
|
|
338
|
+
tps-3d 1376 KB 5.8 MB
|
|
339
|
+
village-quest-3d 1469 KB 5.8 MB
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**`dist/` is six megabytes and a player downloads one.** The rest is lazy: the
|
|
343
|
+
Rapier wasm builds (2D and 3D, ~4.5 MB together) load only when a scene has
|
|
344
|
+
physics bodies, and the editor chunk only if someone opens it. Read the
|
|
345
|
+
`<script>` and `<link rel=modulepreload>` tags in `dist/index.html` for what
|
|
346
|
+
actually loads — `du -sh dist` answers a question nobody asked.
|
|
347
|
+
|
|
348
|
+
**The vendor chunk is named after whichever module the bundler happened to pick,
|
|
349
|
+
and it is not the one you think.** In every one of those builds the ~790 KB
|
|
350
|
+
chunk is called `quiet-rapier-*.js` and contains **three.js** — no Rapier at
|
|
351
|
+
all. A size audit that reads names concludes the physics engine is eager in a
|
|
352
|
+
mouse game with no bodies in it. Read the contents:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
grep -c WebGLRenderer dist/assets/quiet-rapier-*.js # three.js lives here
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Name them yourself if it matters to you — it changes nothing about what loads,
|
|
359
|
+
only what the file is called:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
export default defineConfig({
|
|
363
|
+
build: {
|
|
364
|
+
rollupOptions: {
|
|
365
|
+
output: {
|
|
366
|
+
manualChunks: (id) =>
|
|
367
|
+
id.includes('node_modules/three') ? 'three'
|
|
368
|
+
: id.includes('@dimforge/rapier') ? 'rapier'
|
|
369
|
+
: undefined,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
three is eager in a 2D game too, and that is not a bug: `Renderer2D` draws
|
|
377
|
+
through the same WebGL renderer as the 3D one — shared-PlaneGeometry meshes and
|
|
378
|
+
an orthographic camera. It is the price of one renderer instead of two.
|
|
@@ -80,14 +80,22 @@ mesh or one model and warns when the body carries more.
|
|
|
80
80
|
- **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider`.
|
|
81
81
|
- **`RigidBody2D/3D`** — simulated. Props: `collider`, `mass 1`, `gravityScale 1`,
|
|
82
82
|
`fixedRotation false`, `friction 0.5`, `restitution 0`, `linearVelocity` (write to launch,
|
|
83
|
-
read back every step).
|
|
83
|
+
read back every step; `velocity` is an alias for it). 3D also has
|
|
84
|
+
`angularVelocity`. Every one of them is LIVE — write it mid-run and the solver
|
|
85
|
+
picks it up on the next step.
|
|
84
86
|
- **`Area2D/3D`** — sensor. Emits `triggerEnter(other)` / `triggerExit(other)`. Never blocks
|
|
85
87
|
movement. Solid bodies emit the same signals on real contact (one mental model).
|
|
86
88
|
Areas overlapping OTHER Areas fire too (e.g. a weapon-hitbox Area over an
|
|
87
89
|
enemy-hitbox Area) — neither side needs to be a Body.
|
|
88
90
|
- **`CharacterBody2D/3D`** — kinematic character (Rapier KCC). Props: `collider`
|
|
89
|
-
(capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45
|
|
90
|
-
|
|
91
|
+
(capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`,
|
|
92
|
+
`stepHeight` (3D `0.35` m / 2D `35` px — see below).
|
|
93
|
+
API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`,
|
|
94
|
+
**`isOnWall()`** and **`isOnCeiling()`**. Which wall is answered in the shape
|
|
95
|
+
each dimension has: 2D's `wallSide()` is `-1` left / `+1` right, 3D's
|
|
96
|
+
`wallNormal()` is the wall's normal pointing away from it — the direction a
|
|
97
|
+
wall jump pushes. Without them a wall jump or a wall slide is not awkward to
|
|
98
|
+
write, it is impossible: there is nothing to ask.
|
|
91
99
|
|
|
92
100
|
### `enabled` — a collider that is off for now
|
|
93
101
|
|
|
@@ -105,6 +113,13 @@ sword.enabled = true; // the active window of the swing
|
|
|
105
113
|
sword.enabled = false; // …and done
|
|
106
114
|
```
|
|
107
115
|
|
|
116
|
+
**A collider's keys are CLOSED**: `shape`, `size`, `radius`, `height`, `offset`
|
|
117
|
+
and (2D) `oneWay` — plus `vertices`/`indices` for a 3D trimesh. Anything else is
|
|
118
|
+
a load error naming the nearest one, because
|
|
119
|
+
`{"shape": "circle", "radius": 8, "offest": [0, -24]}` used to load clean with
|
|
120
|
+
its hitbox 24 px from where it was meant to be. `friction`, `restitution` and
|
|
121
|
+
mass are props of the BODY, not of its collider.
|
|
122
|
+
|
|
108
123
|
**Do not arm a hitbox by swapping `collider` in and out.** Replacing the
|
|
109
124
|
collider prop tears the rigid body down and builds a new one — twice per swing —
|
|
110
125
|
and a scene authored `"collider": {}` warns `has no collider — physics skips it`
|
|
@@ -169,9 +184,19 @@ Space/arrows; pass `'none'` to opt out). Keys typed into INPUT/TEXTAREA/SELECT/
|
|
|
169
184
|
contenteditable are ignored entirely, so DOM UI overlays keep working.
|
|
170
185
|
`engine.input.dispose()` detaches every attached source (keyboard and pointer).
|
|
171
186
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
**One press is ONE edge**, in `update` and in `fixedUpdate` alike — the fixed pass has
|
|
188
|
+
its own edge view, drained after its first step, so a dropped frame carrying five steps
|
|
189
|
+
delivers the edge once and a 144 Hz tick that runs no fixed step at all does not lose it.
|
|
190
|
+
Same for `justReleased`. (Until 0.63 this warned that the edge repeated in every fixed
|
|
191
|
+
step of a slow frame; it has not since the fixed pass got its own edge sets, and nothing
|
|
192
|
+
noticed for two releases.)
|
|
193
|
+
|
|
194
|
+
⚠️ What IS worth knowing: **a press injected from inside the frame lands on the NEXT
|
|
195
|
+
tick.** `pressAction`/`releaseAction` called from a `fixedUpdate`, an `update` or an
|
|
196
|
+
`engine.updated` handler — replay drivers, AI input, touch overlays — is invisible for
|
|
197
|
+
the rest of that tick and arrives at the next one, because an edge created after the
|
|
198
|
+
pass that reads it would be created and destroyed without anything seeing it. The HELD
|
|
199
|
+
state (`isPressed`) is immediate either way.
|
|
175
200
|
|
|
176
201
|
## Touch controls (mobile web)
|
|
177
202
|
|
|
@@ -307,11 +332,25 @@ body.velocity = [2, 0, 0]; // or set the velocity outright
|
|
|
307
332
|
|
|
308
333
|
`applyImpulse` is what the character controller itself runs on. It is an
|
|
309
334
|
IMPULSE, not a force: it changes velocity once, so a continuous push is one call
|
|
310
|
-
per frame from `update(dt)` scaled by `dt`.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
per frame from `update(dt)` scaled by `dt`. In 2D the impulse is in px·kg/s,
|
|
336
|
+
y-down, exactly like `linearVelocity`.
|
|
337
|
+
|
|
338
|
+
Readable/writable body state, in BOTH adapters: `velocity` (= `linearVelocity`),
|
|
339
|
+
`angularVelocity`, `mass`, `gravityScale`, `friction`, `restitution`. Read the
|
|
340
|
+
spin to see how fast a lever is swinging; write it to launch a spinning body.
|
|
341
|
+
3D's is a 3-vector about the world axes, 2D's is the scalar rad/s about z with
|
|
342
|
+
the same sign as `rotation` (clockwise, because 2D is y-down). There is no
|
|
343
|
+
torque call in either. Writes land on the next step — these are live props, not
|
|
344
|
+
load-time constants.
|
|
345
|
+
|
|
346
|
+
`fixedRotation` is authored, not live: it locks the body's rotation when the
|
|
347
|
+
body is CREATED. Changing it mid-run does nothing, and a locked body's
|
|
348
|
+
`angularVelocity` is zero rather than whatever was authored.
|
|
349
|
+
|
|
350
|
+
> Four of these were once false, silently: `applyImpulse` existed only in
|
|
351
|
+
> 3D (a TypeError in 2D), `body.velocity = […]` wrote a stray field nothing read
|
|
352
|
+
> in both, `mass`/`friction`/`restitution` were read once at creation in both,
|
|
353
|
+
> and `gravityScale` was live in 3D and dead in 2D.
|
|
315
354
|
|
|
316
355
|
## What is inside this Area right now?
|
|
317
356
|
|
|
@@ -354,7 +393,12 @@ question you usually mean.
|
|
|
354
393
|
only** — never their contact with the rest of the world. Defaulting a spring
|
|
355
394
|
off drops a barrel roped to the floor straight through it, which is how this
|
|
356
395
|
table was arrived at.
|
|
357
|
-
3.
|
|
396
|
+
3. **A joint's two ends must be two DIFFERENT bodies.** It is a child of one and
|
|
397
|
+
points at the other, so `"target": ".."` — which reads like naming the body
|
|
398
|
+
it belongs to — is now a load-time error: Rapier accepts a body jointed to
|
|
399
|
+
itself and constrains nothing, and the rope fell exactly as if it were not
|
|
400
|
+
there.
|
|
401
|
+
4. **`anchor`/`targetAnchor` are local offsets from each body's ORIGIN**,
|
|
358
402
|
resolved against the bodies' positions at load. If the two disagree the solver
|
|
359
403
|
snaps the body into place on the first frame, and a 4 cm typo is silent. Author
|
|
360
404
|
them with arithmetic (generate the scene) rather than by hand.
|
|
@@ -380,19 +424,41 @@ Both runtimes expose the same query (2D in PIXELS y-down, 3D in meters):
|
|
|
380
424
|
|
|
381
425
|
```ts
|
|
382
426
|
const hit = physics.castRay(origin, dir, maxLen, excludeBody?, { staticOnly?: true });
|
|
427
|
+
// hit: { distance, normal, node, point } — `point` is WHERE it landed, in the
|
|
428
|
+
// scene's own units. `dir` may be any length; the ray normalizes it, so
|
|
429
|
+
// `origin + dir * distance` by hand is wrong unless you normalize first.
|
|
383
430
|
// → { distance, normal, node } | null (sensors never block rays)
|
|
384
431
|
```
|
|
385
432
|
|
|
433
|
+
Both also answer two questions about a body that the node props cannot:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
physics.velocityOf(body); // px/s (2D, y-down) or m/s (3D) — from the SOLVER
|
|
437
|
+
physics.massOf(body); // what the solver settled on, collider-derived
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`linearVelocity` on the node is written back once per step, AFTER the solve — so
|
|
441
|
+
a behavior reading it from inside `fixedUpdate`, which is where a collision
|
|
442
|
+
response belongs, sees the value from BEFORE the impact. That is exactly the
|
|
443
|
+
moment a game wants to know how hard it hit something. Scale a push by `massOf`
|
|
444
|
+
rather than by the authored `mass` prop, which the solver may not be using.
|
|
445
|
+
|
|
446
|
+
`dir` may be ANY length — `target - eye` is the usual spelling and is metres
|
|
447
|
+
long. It is normalized for you, so `distance` and `maxLen` are always plain
|
|
448
|
+
metres (2D: pixels), never multiples of the vector you passed. A zero-length
|
|
449
|
+
direction points nowhere and returns `null`.
|
|
450
|
+
|
|
386
451
|
Exclude the shooter's own body when casting from inside it — a ray that starts
|
|
387
452
|
inside its own collider hits itself at distance 0, which reads as "blocked" and
|
|
388
453
|
is the reason a vision cone can come back permanently blind.
|
|
389
454
|
|
|
390
|
-
|
|
391
|
-
(the camera boom, ledge feelers
|
|
455
|
+
Both also have a THICK ray — a sphere sweep in 3D, a circle sweep in 2D — for
|
|
456
|
+
probes where skimming matters (the camera boom, ledge feelers, a shot that must
|
|
457
|
+
not thread a one-pixel gap between two floor tiles):
|
|
392
458
|
|
|
393
459
|
```ts
|
|
394
460
|
const hit = physics.castSphere(origin, dir, radius, maxLen, excludeBody?, { staticOnly?: true });
|
|
395
|
-
// → { distance, node } | null (distance = travel of the
|
|
461
|
+
// → { distance, node } | null (distance = travel of the CENTRE)
|
|
396
462
|
```
|
|
397
463
|
|
|
398
464
|
## Gamepad
|
|
@@ -440,12 +506,15 @@ the tree says, without your scene falling over while you look at it. Use it for
|
|
|
440
506
|
any "show me the collision geometry" tool. Do NOT use it to freeze a running
|
|
441
507
|
game — that is `engine.timeScale = 0`, which keeps the world coherent.
|
|
442
508
|
|
|
443
|
-
## A walking body climbs a step (`stepHeight`, 3D)
|
|
509
|
+
## A walking body climbs a step (`stepHeight`, 2D **and** 3D)
|
|
444
510
|
|
|
445
|
-
`CharacterBody3D.stepHeight` (default `0.35` m
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
511
|
+
`CharacterBody3D.stepHeight` (default `0.35` m) and `CharacterBody2D.stepHeight`
|
|
512
|
+
(default `35` px — the same 0.35 m at the 2D world's scale of 100 px = 1 m,
|
|
513
|
+
which is why gravity defaults to 980) are how high a ledge the body walks UP
|
|
514
|
+
without jumping. `0` = off. It is Rapier's autostep, and **Rapier does not
|
|
515
|
+
autostep unless it is asked** — before 0.63 nothing asked in 3D, and for longer
|
|
516
|
+
still nothing asked in 2D at all, where a lip ONE PIXEL high stopped a walking body
|
|
517
|
+
dead.
|
|
449
518
|
|
|
450
519
|
You will not see this on the player: `CharacterController3D` rides a hover spring
|
|
451
520
|
rather than the character controller, so it floats over small ledges already.
|
|
@@ -522,6 +591,16 @@ game.pick(x, y); // the node under that pixel, or null
|
|
|
522
591
|
A stretched canvas is handled: `pointerPosition()` scales client → canvas pixels,
|
|
523
592
|
so the coordinates are the ones `pick()` wants.
|
|
524
593
|
|
|
594
|
+
**The pointer has to be attached, and now attaches itself.** `createGame2D`/
|
|
595
|
+
`createGame3D` turn it on when the scene contains a `Clickable` — the same
|
|
596
|
+
`'auto'` shape `physics` has always had — and they ask again on every scene
|
|
597
|
+
change, because a title screen you click into a level has no `Clickable` in it
|
|
598
|
+
(a `UiButton` is DOM) and the level is full of them. Pass `pointer: true` explicitly when a
|
|
599
|
+
behaviour of YOUR OWN reads `pointerPosition()`; the boot cannot see that. A
|
|
600
|
+
`Clickable` that finds no pointer (or no picker) for two seconds says so by
|
|
601
|
+
name instead of sitting there inert, which is what it used to do: no error, a
|
|
602
|
+
clean `incanto-check`, a clean audit, and a board that did nothing when clicked.
|
|
603
|
+
|
|
525
604
|
From scene JSON, the **`Clickable`** behavior needs no code at all:
|
|
526
605
|
|
|
527
606
|
```json
|
|
@@ -531,7 +610,7 @@ From scene JSON, the **`Clickable`** behavior needs no code at all:
|
|
|
531
610
|
| prop | default | |
|
|
532
611
|
| --- | --- | --- |
|
|
533
612
|
| `button` | `0` | 0 left · 1 middle · 2 right |
|
|
534
|
-
| `maxDistance` | `0` | ignore clicks further than this (0 = any) |
|
|
613
|
+
| `maxDistance` | `0` | ignore clicks further than this **from the current camera** (0 = any) |
|
|
535
614
|
| `enabled` | `true` | stop responding without detaching |
|
|
536
615
|
|
|
537
616
|
Signals: **`clicked(node)`**, **`hovered(node)`**, **`unhovered(node)`** — wire
|
|
@@ -550,11 +629,31 @@ ends elsewhere is not a click, the way every button on every platform behaves),
|
|
|
550
629
|
and a hit on a CHILD counts as a hit on the node — the raycast lands on the
|
|
551
630
|
visual mesh, which is usually a child.
|
|
552
631
|
|
|
632
|
+
**A finger that lifts stops hovering.** `hovered`/`unhovered` follow the cursor,
|
|
633
|
+
and a mouse cursor stays where you left it — but a finger ceases to exist, so a
|
|
634
|
+
touch release clears the pointer at the end of that frame (after the click has
|
|
635
|
+
fired, not with it). Without that the last-tapped node stayed `hovering: true`
|
|
636
|
+
for the rest of the session: a permanently highlighted tile, mole or card on
|
|
637
|
+
every phone. Do not build a hover-only affordance — on touch there is no hover
|
|
638
|
+
before the tap, only after it, and only for a frame.
|
|
639
|
+
|
|
640
|
+
**`visible: false` is not clickable.** A pixel the frame does not draw has
|
|
641
|
+
nothing under it, so hiding a node (or any ancestor of it) takes it out of the
|
|
642
|
+
pick — which is how you disable a button, a dialogue choice or a mole down its
|
|
643
|
+
hole without detaching anything. Before 0.71 three's raycaster reported hidden
|
|
644
|
+
geometry and `pick()` handed it straight to `Clickable`.
|
|
645
|
+
|
|
553
646
|
### Testing a mouse-driven game headlessly
|
|
554
647
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
648
|
+
**Use `runScript`'s `click` step** — `{ atMs: 100, click: 'Board/Tile3' }` — which
|
|
649
|
+
installs a geometric picker, points the cursor and spreads the press and release
|
|
650
|
+
across the two frames `Clickable` needs. `incanto-playtest` clicks too, and its
|
|
651
|
+
report says how many clicks it landed. See `incanto-verifying-your-game.md`.
|
|
652
|
+
|
|
653
|
+
The raw recipe below is what that does, and is still what you want when the
|
|
654
|
+
cursor has to be somewhere a node is not. There is no renderer and therefore no
|
|
655
|
+
raycast, so `engine.picker` is null and `Clickable` is inert. That is not a dead
|
|
656
|
+
end: give the engine a picker of your own and drive the cursor.
|
|
558
657
|
|
|
559
658
|
```ts
|
|
560
659
|
// A picker that answers from the tree instead of a GPU raycast.
|
|
@@ -42,6 +42,24 @@ find a door, not enough to solve a puzzle.
|
|
|
42
42
|
No renderer is involved, so twenty minutes of simulated play takes a second or
|
|
43
43
|
two.
|
|
44
44
|
|
|
45
|
+
**Two buttons it will stop pressing.** The bot drives every declared button
|
|
46
|
+
action, then notices what a player would notice. If pressing one STOPS THE
|
|
47
|
+
CLOCK, that is a pause menu, and a run spent inside one measures nothing — it
|
|
48
|
+
lets go and never presses that button again. If pressing one replaces the scene
|
|
49
|
+
with THAT SAME SCENE, that is a restart, and it does not just waste the run, it
|
|
50
|
+
ends it: the bot is bound to the tree it started in. One run of the set pays for
|
|
51
|
+
that lesson and the rest play the game. Both are reported (`the bot stopped
|
|
52
|
+
pressing …`), both are OBSERVED rather than read off the words "pause" and
|
|
53
|
+
"restart" — your game may call them anything, or nothing — and neither is a
|
|
54
|
+
defect in your game.
|
|
55
|
+
|
|
56
|
+
The second one matters more than it sounds: mid-run retry is ordinary in puzzle,
|
|
57
|
+
racing and tower-defense games, and `GameFlow` restarts only from a terminal
|
|
58
|
+
state, so the games that reload mid-play are the ones that call `restartScene`
|
|
59
|
+
themselves. Every one of them used to report `8 runs played without reaching a
|
|
60
|
+
win (8 changed scene)` — and each run died a second in, which took the emitters,
|
|
61
|
+
the hazards and the destinations down with it.
|
|
62
|
+
|
|
45
63
|
## What it reports, and where each answer comes from
|
|
46
64
|
|
|
47
65
|
| line | source |
|
|
@@ -51,12 +69,36 @@ two.
|
|
|
51
69
|
| `fell` | the player left the world past `--fall-below` (default: 50 m under the spawn in 3D, 1000 px under it in 2D) |
|
|
52
70
|
| `error` | `stats().errors` went above zero — a behavior threw |
|
|
53
71
|
| `stuck` | the clock ran out AND the player never got more than 3 m (96 px) from its spawn — it is wedged, or nothing moves it |
|
|
54
|
-
| `swapped` | the game changed scene. The bot binds player, destinations and the win/lose oracle ONCE, from the tree it started in, so nothing after a `goToScene` is measured — the run stops there and says so. It is not a defect, and the destinations it never got to are reported as unmeasured rather than missed. Judge a multi-scene game with a scripted `runScript`. |
|
|
72
|
+
| `swapped` | the game changed scene. The bot binds player, destinations and the win/lose oracle ONCE, from the tree it started in, so nothing after a `goToScene` is measured — the run stops there and says so. It is not a defect, and the destinations it never got to are reported as unmeasured rather than missed. Judge a multi-scene game with a scripted `runScript`. The bot's own KEYS are re-read on the swap: it used to hold the first scene's buttons into the second, and letting go of one the new scene never declared threw the whole playtest away. |
|
|
55
73
|
| `unfinished` | the clock ran out on a player that was getting around. Not a defect: a win that needs a SEQUENCE (talk to the NPC, then fetch, then return) is out of reach of a random walker, forever |
|
|
56
74
|
| `never reached` | destinations the bot never came within `--reach-radius` of (default 2 m in 3D, 32 px in 2D) |
|
|
57
75
|
| `never fired` | signals a `connections[]` entry listens to that never happened |
|
|
76
|
+
| `declared and never read` | actions in the scene's `input{}` that NOTHING asked about, in any run — a key the control list promises and no code implements. Measured as a read, not a press, so the bot pressing it does not clear it. Two shipped starters had one: `tps-3d` declared `restart` (on `KeyR`, already the reload key) and `MISSION FAILED` was a dead end you left with F5. An action polled only on a screen no run reached lands here honestly — read it, do not gate on it |
|
|
58
77
|
| `danger` | how many times the player's `Health` emitted `damaged` — and when that is ZERO, WHICH zero it is: a scene with no hazard wired at all is a walkabout and worth shouting about; a scene full of them that never landed one is a report about the BOT, which a real player would outrun less easily. Hazards are counted from the scene FILE, because a `Spawner` detaches its prefab at ready and a wave game's hazards are not in the tree yet. |
|
|
59
78
|
|
|
79
|
+
**The bot presses the HUD.** Every focusable widget a player can currently see —
|
|
80
|
+
`UiButton`, `UiToggle`, `UiSelect`, `UiSlider` — gets pressed occasionally,
|
|
81
|
+
through the same entry point Enter and the pad's A use. That is the whole
|
|
82
|
+
interface of a menu, a shop, a settings screen or an idle game, and it was on
|
|
83
|
+
none of the lists the bot searched: one built from the tarball reported
|
|
84
|
+
`nothing here is drivable … nothing to point at` and `✗ stuck in 2/2` about a
|
|
85
|
+
game that plays perfectly. A widget under a hidden panel is not pressed, because
|
|
86
|
+
a player could not press it either. `report.runs[].presses` counts them.
|
|
87
|
+
|
|
88
|
+
**The bot drags between SLOTS.** Every `draggable` widget gets picked up now and
|
|
89
|
+
then and dropped on a `dropTarget` that is not its own parent, through
|
|
90
|
+
`dropOnto` — the same entry point a harness uses, with the same rules as the
|
|
91
|
+
pointer path. An inventory is the one screen the HUD skill says a click cannot
|
|
92
|
+
express, and it was the one screen the bot could not touch: a potion bench whose
|
|
93
|
+
every verb is a drag reported `nothing here is drivable` and `✗ stuck in 6/6`,
|
|
94
|
+
and now reports `324 slot drag(s)` and which of its wires never fired.
|
|
95
|
+
`report.runs[].widgetDrags` counts them.
|
|
96
|
+
|
|
97
|
+
**The line names every channel it used**, not the first one that fired: a game
|
|
98
|
+
played by dragging AND by keys reports both counts, because an either/or ladder
|
|
99
|
+
printed the drags and said nothing about three hundred key presses on the same
|
|
100
|
+
run.
|
|
101
|
+
|
|
60
102
|
**The bot can use the MOUSE.** If your scene has `Clickable` nodes, it presses
|
|
61
103
|
and releases over them at random — through the real `Clickable`, the real
|
|
62
104
|
`mouseJustPressed` edge and your real connection, with a picker that answers
|
|
@@ -70,6 +112,12 @@ A random clicker still cannot finish a game that needs a SEQUENCE — select a
|
|
|
70
112
|
trooper, then a tile, then end the turn — and that is reported as unmeasured,
|
|
71
113
|
not failed.
|
|
72
114
|
|
|
115
|
+
**A wire on a HUD widget is visible now too.** The whole screen-space subtree was
|
|
116
|
+
skipped when the run collected signals — right for "never reached", which is
|
|
117
|
+
about walking, and it took every `pressed`, `changed` and `dropped` wire with
|
|
118
|
+
it, so the line that exists to say "this wire does nothing" could never say it
|
|
119
|
+
about a HUD.
|
|
120
|
+
|
|
73
121
|
**Nothing here is a guess about your design.** The scene already declares what
|
|
74
122
|
winning is; this only listens.
|
|
75
123
|
|
|
@@ -102,10 +150,54 @@ geometry says you cannot. Check the jump height, the gap, the collider.
|
|
|
102
150
|
**`never fired`** means you drew a wire that does nothing. Either the signal is
|
|
103
151
|
unreachable or the connection is wrong.
|
|
104
152
|
|
|
153
|
+
**`silent`** names a declared emitter — an `AudioPlayer`, a particle node — that
|
|
154
|
+
never went off in any run. Emitters on a SPAWNER's prefab count: the template is
|
|
155
|
+
lifted out of the tree at load, so the check reads the scene FILE as well as the
|
|
156
|
+
loaded tree, and a wave of forty enemies is reported as its one authored
|
|
157
|
+
emitter, not forty.
|
|
158
|
+
|
|
159
|
+
**`made and still held`** counts the clones alive when the run ended, grouped by
|
|
160
|
+
the TEMPLATE they came from, with what the count was at the halfway mark:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
· made and still held: 12 × /Game/Prefabs/Bolt (3 at halfway),
|
|
164
|
+
6 × /Game/Prefabs/Tower (3 at halfway)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Six towers from three is a player who kept building; twelve projectiles from
|
|
168
|
+
three is a projectile that never learned to stop. **Furniture holds steady; a
|
|
169
|
+
leak climbs** — and nothing in the engine can tell them apart, so both are
|
|
170
|
+
printed and the judgement is yours. It exists because a shipped tower defense
|
|
171
|
+
kept every bolt that MISSED alive forever (its `Lifetime` was asleep on the
|
|
172
|
+
prefab shelf) and no instrument said a word: the playtest was green, the audit
|
|
173
|
+
was green, the harness was green, every rung of the ladder was green.
|
|
174
|
+
|
|
105
175
|
**`nothing here can hurt you`** is a design report, not an error. Sometimes it
|
|
106
176
|
is exactly right (a walking simulator). Usually it means the hazards are not
|
|
107
177
|
wired up.
|
|
108
178
|
|
|
179
|
+
**`!` lines** are what the ENGINE warned about during the runs — a camera whose
|
|
180
|
+
`follow` resolves to a node with no position, a `DayNight` with no sky, a body
|
|
181
|
+
with no collider. They are not failures and they do not change the exit code;
|
|
182
|
+
they are the things that leave a game running and wrong, and they are usually
|
|
183
|
+
the first thing to read. One line per distinct warning, however many times it
|
|
184
|
+
fired.
|
|
185
|
+
|
|
186
|
+
**`error`** prints the throw under it — the behavior, the node and the
|
|
187
|
+
exception, in the engine's own words:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
✗ error in 8/8
|
|
191
|
+
behavior 'Boom' on /Game/Player/Sword (Node2D) threw in update — THE
|
|
192
|
+
SCRIPT is now skipped … Error: the sword script is broken
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A script that throws in `update` is quarantined (the node keeps running, its
|
|
196
|
+
script does not), so everything downstream of it simply stops happening and
|
|
197
|
+
every other line of the report describes the silence rather than the cause.
|
|
198
|
+
Read this one first. `incanto-verify` carries the same lines under its `plays`
|
|
199
|
+
rung.
|
|
200
|
+
|
|
109
201
|
**`stuck` in every run** means the player is not going anywhere: wedged in
|
|
110
202
|
geometry, spawned inside a collider, or missing the input map that moves it.
|
|
111
203
|
That is a real bug and worth chasing.
|
|
@@ -156,6 +248,18 @@ real thing:
|
|
|
156
248
|
bunx incanto-playtest src/game.scene.json --behaviors src/behaviors.ts
|
|
157
249
|
```
|
|
158
250
|
|
|
251
|
+
**More than one file.** Behaviours split as a game grows — bosses in `boss.ts`,
|
|
252
|
+
the scene table in `scenes.ts` — so comma-separate them or repeat the flag,
|
|
253
|
+
rather than writing a barrel that re-exports everything for the CLI's benefit:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
bunx incanto-playtest src/game.scene.json --behaviors src/behaviors.ts,src/boss.ts
|
|
257
|
+
bunx incanto-verify src/game.scene.json --behaviors src/behaviors.ts --behaviors src/boss.ts
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Every bin that takes `--behaviors` accepts both forms: `check`, `playtest`,
|
|
261
|
+
`verify`, `feel`, `play`, `multiplay`.
|
|
262
|
+
|
|
159
263
|
## From TypeScript
|
|
160
264
|
|
|
161
265
|
```ts
|
|
@@ -166,7 +270,13 @@ console.log(playtestText(report));
|
|
|
166
270
|
```
|
|
167
271
|
|
|
168
272
|
`report.runs` carries per-run outcome, timing, reached paths, fired signals,
|
|
169
|
-
damage and the replay.
|
|
273
|
+
damage and the replay. `report.inertActions` is the dead-control list above.
|
|
274
|
+
|
|
275
|
+
The same question, without a playtest — after any run, scripted or hand-driven:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
game.engine.input.unreadActions(); // ['restart'] — declared, nothing read it
|
|
279
|
+
```
|
|
170
280
|
|
|
171
281
|
## Pairs with the feel report
|
|
172
282
|
|