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
|
@@ -33,14 +33,22 @@ wiring, and composition are all in the file. TS is for *behavior only* (attached
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
- `format` MUST be `1`. `type` MUST be `"scene"`. `name` non-empty.
|
|
36
|
-
- `dimension` is optional: `"2d"` or `"3d"` only.
|
|
36
|
+
- `dimension` is optional: `"2d"` or `"3d"` only. Left out, it is INFERRED from
|
|
37
|
+
the node types in the tree (anything ending `3D` makes the scene 3D) — write
|
|
38
|
+
it anyway, because it is what the environment validator, the audit's
|
|
39
|
+
no-camera/no-light warnings and `physics: "auto"` all read.
|
|
37
40
|
- `fragment` (optional, default `false`) says this file is a PIECE of another
|
|
38
41
|
scene — a spawner's prefab, a sub-scene something instances — and is never
|
|
39
42
|
rendered on its own. It changes nothing at runtime; it tells the checker to
|
|
40
|
-
stop asking a prefab where its camera and its sun are
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
stop asking a prefab where its camera and its sun are, and to stop answering
|
|
44
|
+
the two questions only the HOST can answer: a node path that leaves the file
|
|
45
|
+
(`/root/Bolts` is the fragment's own root only while the fragment is alone)
|
|
46
|
+
and a `DamageOnContact.targetGroup` whose victims live in the host scene.
|
|
47
|
+
Those two are still NAMED, with a reworded verdict — a typo is most likely in
|
|
48
|
+
exactly this file, written away from the scene that gives it meaning. Set it
|
|
49
|
+
on any scene you load from TypeScript and hand to a spawner. (A scene another
|
|
50
|
+
scene embeds with `instance` needs no key — `incanto-check` walks the project
|
|
51
|
+
and can see that for itself.)
|
|
44
52
|
- `viewport` (optional) makes scene JSON own responsive layout: author the world
|
|
45
53
|
in fixed `design` pixels (`[width, height]`, positive numbers) and the renderer
|
|
46
54
|
maps them onto any canvas size. `fit`: `"expand"` (design rect always fully
|
|
@@ -96,12 +104,24 @@ Rules the engine enforces with **hard errors** (it never warns silently):
|
|
|
96
104
|
3. Prop keys and value kinds are validated against the type's schema:
|
|
97
105
|
unknown key → `UNKNOWN_PROP` (message lists valid keys); wrong JSON kind
|
|
98
106
|
(e.g. string where the default is boolean) → `PROP_TYPE_MISMATCH`.
|
|
107
|
+
A few props accept TWO kinds — `Flowers3D.density` takes a preset name or
|
|
108
|
+
plants per m², `Water3D.underwater` takes `true`/`false` or a settings
|
|
109
|
+
object — and `incanto-node-reference.md` names both in its Kind column
|
|
110
|
+
(`one of: lush sparse none, or number`). The error names them too, so a
|
|
111
|
+
value the loader refuses tells you every shape it would have taken.
|
|
99
112
|
4. Sibling names must be unique. Colliding names are auto-renamed by incrementing a
|
|
100
113
|
trailing number (`Enemy` → `Enemy2` → `Enemy3`) — **write unique names yourself** so your
|
|
101
114
|
connection paths stay valid (connections resolve after renaming).
|
|
102
115
|
5. Node names must not contain `/` or `%` and must be non-empty.
|
|
103
116
|
6. `groups` are string tags for queries (`getNodesInGroup`, connection filters).
|
|
104
117
|
`tags` is free-form JSON for game-logic identity (`{"kind": "ITEM", "value": 10}`).
|
|
118
|
+
**`groups` is a LIST and `tags` is an OBJECT**, and the loader now says so:
|
|
119
|
+
`"groups": "player"` used to load clean and spread the string one CHARACTER
|
|
120
|
+
at a time (`["p","l","a","y","e","r"]`), so `player` was in no group and
|
|
121
|
+
every query and `filter: {group: "player"}` silently matched nothing. Every
|
|
122
|
+
node key is checked for its KIND now — `props`/`overrides` an object,
|
|
123
|
+
`children` a list, `uid` a non-empty string from `newUid()` — as are the
|
|
124
|
+
header's `constants`, `multiplayer` (objects) and `connections` (a list).
|
|
105
125
|
7. `script` resolves to a registered Behavior at load (`incanto-behaviors-and-scripts.md`);
|
|
106
126
|
`network` drives multiplayer replication (`incanto-multiplayer.md`). Both round-trip
|
|
107
127
|
losslessly through export.
|
|
@@ -321,10 +341,21 @@ that do exist at the failing spot).
|
|
|
321
341
|
never recovers.
|
|
322
342
|
```
|
|
323
343
|
|
|
324
|
-
`died → ScoreKeeper.addScore` is the natural way to score a kill and it
|
|
325
|
-
score to `NaN` on the first one, with the win condition then
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
`died → ScoreKeeper.addScore` is the natural way to score a kill and it used to
|
|
345
|
+
set the score to `NaN` on the first one, with the win condition then
|
|
346
|
+
permanently out of reach. The numeric handlers REFUSE a non-number now, so the
|
|
347
|
+
same wire fails loudly instead — and so does the one the arity check cannot
|
|
348
|
+
see: `clicked → addScore` hands the NODE, which is one argument into one
|
|
349
|
+
parameter, and `score + node` is a STRING that grows forever. A handler that DEFAULTS what it is not given is correct and stays silent:
|
|
350
|
+
`won → GameFlow.win` works, because `win(text = 'YOU WIN')` needs nothing.
|
|
351
|
+
|
|
352
|
+
Writing your OWN handler, the same rule applies to you: an optional parameter
|
|
353
|
+
is a **default**, not a `?`. `hurt(amount: number, from?: Node)` counts as
|
|
354
|
+
needing 2, because `?` is a TypeScript annotation that does not survive to
|
|
355
|
+
runtime — write `from: Node | undefined = undefined` and the wire is judged on
|
|
356
|
+
what it actually needs. Every engine method a wire can name has been corrected
|
|
357
|
+
to this; before that, four of five documented HUD wires were reported as
|
|
358
|
+
broken while working perfectly. On **both**, it is
|
|
328
359
|
`AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
|
|
329
360
|
and nothing would say so. A core node answers to 62 public methods before any
|
|
330
361
|
adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
|
|
@@ -333,7 +364,11 @@ that do exist at the failing spot).
|
|
|
333
364
|
- Unresolvable `from`/`to` → `DANGLING_CONNECTION` at load. Renaming a node breaks its
|
|
334
365
|
connections **loudly** — update paths in the same edit.
|
|
335
366
|
- `filter` gates firing on the first emitted argument: it must be a node in `filter.group`
|
|
336
|
-
and/or match every `filter.tag` entry.
|
|
367
|
+
and/or match every `filter.tag` entry. **A filter's keys are `group` and `tag`, and a
|
|
368
|
+
connection's are `[signal, from, to, handler, once, filter]`** — anything else is a load
|
|
369
|
+
error, because a filter the matcher cannot read (`{"gruop": "player"}`, or the bare
|
|
370
|
+
string `"player"`) passes EVERYTHING, which is the opposite of what a filter is for, and
|
|
371
|
+
`"once": "no"` is truthy so it fires exactly once. With `once: true`, the connection is consumed only
|
|
337
372
|
when the filter matches.
|
|
338
373
|
|
|
339
374
|
## Sub-scene composition
|
|
@@ -345,7 +380,20 @@ that do exist at the failing spot).
|
|
|
345
380
|
- `instance` embeds another scene's tree; `overrides` deep-merge onto the sub-scene root's
|
|
346
381
|
props. At the instancing site, `groups` and `children` COMPOSE (union/append), while
|
|
347
382
|
`tags`/`script`/`network` REPLACE the sub-scene root's values when declared (omitted = kept).
|
|
348
|
-
- The sub-scene's own `connections` are wired inside its subtree automatically
|
|
383
|
+
- The sub-scene's own `connections` are wired inside its subtree automatically,
|
|
384
|
+
and so are its `assets`, `input`, `strings`, `constants` and `orderGroups` —
|
|
385
|
+
**a prefab brings everything it declares**. (Its `orderGroups` used to come
|
|
386
|
+
through as NAMES only, so `"orderGroup": "loot"` validated and the band's
|
|
387
|
+
number was dropped: the sprite sorted in the default band and the scene loaded
|
|
388
|
+
clean.) A `coin.scene.json` with its own
|
|
389
|
+
spritesheet, or a `player.scene.json` with its own `move`/`jump` bindings, is
|
|
390
|
+
a complete, reusable thing.
|
|
391
|
+
- The HOST wins any asset key, action name or string it declares itself, so a
|
|
392
|
+
level can re-point a prefab's art or rebind its controls without editing the
|
|
393
|
+
prefab. Two INSTANCED scenes declaring one asset key with different urls — or
|
|
394
|
+
one action name with different keys — is a hard error naming both:
|
|
395
|
+
first-writer-wins would hand the second prefab the first one's art in silence.
|
|
396
|
+
The same key with the same value is just two prefabs agreeing.
|
|
349
397
|
- There is **no** scene inheritance — composition only.
|
|
350
398
|
- Current limitation: exporting expands instances into full trees (the `instance` reference is
|
|
351
399
|
not preserved on export yet).
|
|
@@ -373,6 +421,12 @@ that do exist at the failing spot).
|
|
|
373
421
|
| `DUPLICATE_BEHAVIOR` | two classes registered under one behavior name | rename one, or `{ replace: true }` for hot reload |
|
|
374
422
|
| `TREE_VIOLATION` | invalid name, re-parenting without detach, cycles, double root | follow the rule in the message |
|
|
375
423
|
|
|
424
|
+
Every one of these lists what WOULD have worked, and names the nearest
|
|
425
|
+
candidate when there is one — `Unknown node type 'Label2D'. Did you mean
|
|
426
|
+
"Label"?`, `Unknown prop 'size' on 'Label'. Did you mean "fontSize"?`. A name
|
|
427
|
+
that resembles nothing gets the list alone, because a suggestion that is not the
|
|
428
|
+
answer is worse than none.
|
|
429
|
+
|
|
376
430
|
Scene-load and registry errors carry structured `details` (`path`, `uid`, `nodeType`,
|
|
377
431
|
`prop`, `signal`, `validOptions`) mirroring the prose — prefer those over regexing the
|
|
378
432
|
message (a few runtime errors still carry prose only). Scene-load
|
|
@@ -384,7 +438,10 @@ errors append the offending node's path: `… (at '/Level/Enemies/Slime3')`.
|
|
|
384
438
|
import { loadScene, registerCoreNodes, duplicateNode } from 'incanto';
|
|
385
439
|
|
|
386
440
|
registerCoreNodes(); // explicit — never an import side effect
|
|
387
|
-
|
|
441
|
+
// `{ engine }` on a manual boot: `onReady` runs during the LOAD, so a behaviour
|
|
442
|
+
// that reads `this.engine` or `this.rng` there throws without it. `createGame2D`
|
|
443
|
+
// /`3D` and `runScript` pass it for you.
|
|
444
|
+
const scene = loadScene(json, { engine, resolveScene }); // throws IncantoError on any problem
|
|
388
445
|
scene.root.getNode('Player').emit('hit', 10);
|
|
389
446
|
const copy = duplicateNode(scene.root.getNode('Coin'));
|
|
390
447
|
const exported = scene.toJSON(); // lossless (delta-only props)
|
|
@@ -34,7 +34,7 @@ $ bunx incanto verify # finds your scene AND your behaviou
|
|
|
34
34
|
✓ loads — the scene is legal and its assets resolve
|
|
35
35
|
! /Game/Chest: script Chest — target '%Ke' matches nothing
|
|
36
36
|
? plays — 8 runs played without reaching a win (4 lost, 4 ran out the clock)
|
|
37
|
-
|
|
37
|
+
? feels — 5 of 7 fired — silent: /Game/Boss/Roar, /Game/Boss/Boom
|
|
38
38
|
· agrees — not run — this scene has no `multiplayer` header
|
|
39
39
|
? draws — the dev server is running on :5173, but no page answered
|
|
40
40
|
|
|
@@ -106,8 +106,17 @@ and a run without `--behaviors` reports that instead: neither is a broken game.
|
|
|
106
106
|
`feels` is the sound-and-effects rung: it lists what the scene DECLARES against
|
|
107
107
|
what actually fired during the playtest. A game whose feedback is wired but
|
|
108
108
|
never triggered plays perfectly and feels dead, and every other rung calls it
|
|
109
|
-
healthy. Nothing firing at all is a
|
|
110
|
-
|
|
109
|
+
healthy. Nothing firing at all is a FAILURE. Some nodes staying silent is
|
|
110
|
+
`unmeasured` (`?`) with the names listed — a silent emitter is either a broken
|
|
111
|
+
wire or a moment random play never reached, and the two look identical from
|
|
112
|
+
here. Reach it in a scripted run and this rung can tell you which. It ticks
|
|
113
|
+
(`✓`) only when every declared emitter fired.
|
|
114
|
+
|
|
115
|
+
`says` reads the running page's own log. Errors and failed assets fail it;
|
|
116
|
+
WARNINGS are carried under it with a `!` and it still passes — a camera whose
|
|
117
|
+
`follow` points at a node with no position, a `DayNight` with no sky to move, a
|
|
118
|
+
body with no collider. Those leave a game running and wrong, and they are the
|
|
119
|
+
lines worth reading before anything else on this list.
|
|
111
120
|
|
|
112
121
|
Exit 1 only when a rung actually failed. `--behaviors src/behaviors.ts` matters:
|
|
113
122
|
without it your game logic is stubbed and only the structure is played.
|
|
@@ -116,6 +125,16 @@ The sections below are the same rungs one at a time, for when you need the detai
|
|
|
116
125
|
|
|
117
126
|
## 1. After EVERY scene edit: `incanto-check`
|
|
118
127
|
|
|
128
|
+
```bash
|
|
129
|
+
bunx incanto-check # structure, assets, semantics
|
|
130
|
+
bunx incanto-check --behaviors src/behaviors.ts --strict-behaviors
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`--strict-behaviors` makes a `script.name` that resolves nowhere a FAILURE
|
|
134
|
+
instead of a stub — and it needs `--behaviors` to know what you registered.
|
|
135
|
+
Without it, on any project with a behaviour of its own, it could only ever fail:
|
|
136
|
+
`Unknown behavior 'Hunter' … Did you forget registerBehavior?`
|
|
137
|
+
|
|
119
138
|
```bash
|
|
120
139
|
bunx incanto-check # validates every *.scene.json under cwd
|
|
121
140
|
bunx incanto-check --json # machine-readable (code + details.path/prop)
|
|
@@ -211,6 +230,22 @@ console.log(result.describe()); // run report + final scene, one line per
|
|
|
211
230
|
if (!result.ok) process.exit(1); // failures carry atMs + label + message
|
|
212
231
|
```
|
|
213
232
|
|
|
233
|
+
**An assert fails on `false`, on a non-empty STRING, or on a throw.** The
|
|
234
|
+
string is used as the message, which is how a harness explains itself:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
assert: (ctx) => {
|
|
238
|
+
const lives = (ctx.getNode('Score').behavior as never as { lives: number }).lives;
|
|
239
|
+
if (lives <= 0) return `the keep fell — ${lives} lives`; // the failure, in words
|
|
240
|
+
return null; // nothing to say
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`true`, `null`, `undefined` and `''` are passes. Anything else — a number, an
|
|
245
|
+
object — is recorded as a failure naming the contract, because a check that
|
|
246
|
+
cannot fail is not a check. (It used to read only `false` and a throw, so every
|
|
247
|
+
assert in the shape above passed by not being `false`.)
|
|
248
|
+
|
|
214
249
|
Run it with `bunx tsx verify.ts` (or inside a vitest test). Physics enables
|
|
215
250
|
automatically from the scene's `dimension`. `result.snapshots` holds periodic
|
|
216
251
|
captures; `result.logs` holds everything behaviors logged via `this.log`.
|
|
@@ -222,6 +257,84 @@ into the process-global registries (hot-replacing same names). In a test
|
|
|
222
257
|
suite, isolate with `clearRegistry()`/`clearBehaviors()` between files if
|
|
223
258
|
you register conflicting classes.
|
|
224
259
|
|
|
260
|
+
### Clicking, for a game the mouse plays
|
|
261
|
+
|
|
262
|
+
`press`/`release`/`vector`/`key` are all ACTIONS, and `Clickable` reads the mouse
|
|
263
|
+
BUTTON — so a match-3, a tower defense, a card game, a point-and-click, an RTS or
|
|
264
|
+
a flip puzzle needs one more verb:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
steps: [
|
|
268
|
+
{ atMs: 100, click: 'Board/Tile3' }, // by node path — the usual way
|
|
269
|
+
{ atMs: 400, click: [300, 100] }, // or a raw point, in the scene's units
|
|
270
|
+
]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A click is a press and a release on two DIFFERENT frames — `clicked` fires on
|
|
274
|
+
release over the node the press started on — and that is handled for you. So is
|
|
275
|
+
the picker: headless there is no renderer and therefore no raycast, so a
|
|
276
|
+
**geometric** one is installed for the run, answering from the same world boxes
|
|
277
|
+
`framing()` measures. A game that installs a picker of its own keeps it — its
|
|
278
|
+
answer always wins.
|
|
279
|
+
|
|
280
|
+
2D coordinates are world pixels (the space every 2D `position` in the file is
|
|
281
|
+
already written in); 3D casts a ray from the scene's current camera. A `click`
|
|
282
|
+
naming a path that resolves to nothing is a FAILURE, not a click into space.
|
|
283
|
+
|
|
284
|
+
`geometricPicker(scene)` from `incanto/test` is the same thing on its own, for a
|
|
285
|
+
harness that wants to ask "what is under this point" directly.
|
|
286
|
+
|
|
287
|
+
**A `click` naming a HUD widget PRESSES it** — `click: 'Hud/Shop/Buy'` is the
|
|
288
|
+
whole verb for a menu, a shop, a settings screen, an idle game or a card game.
|
|
289
|
+
There is no cursor involved: a widget lives in the DOM overlay, not the world,
|
|
290
|
+
so the click goes to what a MOUSE does to that widget — a button presses, a
|
|
291
|
+
toggle flips, a select advances, and a `UiDialogue` ADVANCES THE LINE without
|
|
292
|
+
answering a waiting choice, exactly as clicking the box does in a browser
|
|
293
|
+
(`advance()` refuses to skip choices). To answer one, do what a player does and
|
|
294
|
+
press the choice, or call `talk.choose(i)` from a `do:` step. Enter and the
|
|
295
|
+
pad's A are a different verb and DO answer.
|
|
296
|
+
A widget that is hidden — or that sits under a hidden `UiPanel` or a hidden
|
|
297
|
+
`HudLayer` — is a FAILURE naming it, because a player could not have pressed it
|
|
298
|
+
either.
|
|
299
|
+
|
|
300
|
+
**A drag needs the two edges apart**, and `click` puts them at one point:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
steps: [
|
|
304
|
+
{ atMs: 100, at: [400, 300], mouse: 'down' }, // press, here
|
|
305
|
+
{ atMs: 200, at: [300, 380] }, // and move while held
|
|
306
|
+
{ atMs: 300, mouse: 'up' }, // let go, there
|
|
307
|
+
]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`at` on its own is a HOVER — the state a game shows for a cursor that has not
|
|
311
|
+
clicked yet. `button` picks a different one (0 left · 1 middle · 2 right) for
|
|
312
|
+
`mouse` and for `click` alike. Written with `click`, a drag-and-launch game
|
|
313
|
+
launches with a vector of `from - to` where both are the same point: **it fires
|
|
314
|
+
every shot and every shot goes nowhere**, and the run reports fine.
|
|
315
|
+
|
|
316
|
+
The aiming side is on the engine, not the renderer: a behaviour reads
|
|
317
|
+
`this.engine.pointerWorld()` (and `toWorld`/`toScreen`), and a `click` or `at`
|
|
318
|
+
step installs a geometric one for the run alongside the picker.
|
|
319
|
+
|
|
320
|
+
### Asking the game a question
|
|
321
|
+
|
|
322
|
+
An assert needs to READ something, and the readable thing is often neither a
|
|
323
|
+
prop nor a method. Every node type's **`Reads:`** line in
|
|
324
|
+
`incanto-node-reference.md` lists its read-only accessors — the ones that exist
|
|
325
|
+
for exactly this:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
(ctx.getNode('Hud/Banner') as never as { showing: string }).showing // '' when idle
|
|
329
|
+
(ctx.getNode('Talk') as never as { active: boolean }).active // mid-dialogue?
|
|
330
|
+
(ctx.getNode('Gun').behavior as never as { liveCount: number }).liveCount
|
|
331
|
+
(ctx.getNode('Player').behavior as never as { isDead: boolean }).isDead
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`banner.text` does not exist and reads as `undefined`, which an assert will
|
|
335
|
+
happily compare against and pass. Look the accessor up rather than guessing a
|
|
336
|
+
prop name.
|
|
337
|
+
|
|
225
338
|
## 4. Read the scene like a screenshot
|
|
226
339
|
|
|
227
340
|
```ts
|
|
@@ -290,19 +403,27 @@ from geometry alone — no renderer, no canvas, no browser:
|
|
|
290
403
|
|
|
291
404
|
```
|
|
292
405
|
> framing
|
|
293
|
-
camera /World/Player/Cam at [0, 2, 8] looking [0, -0.2, -0.97] fov 60
|
|
406
|
+
camera /World/Player/Cam at [0, 2, 8] looking [0, -0.2, -0.97] up [0, 1, 0] fov 60
|
|
294
407
|
lit by: /World/Sun (DirectionalLight3D), environment.sky
|
|
295
|
-
3 on screen, 1 off screen, 2 behind the camera
|
|
408
|
+
3 on screen, 1 off screen, 2 behind the camera, 1 hidden (visible: false)
|
|
296
409
|
onScreen /World/Island (MeshInstance3D) [0, 0, 0] screen [0.02, -0.11] 8.2m
|
|
297
410
|
behind /World/Chest (ModelInstance3D) [0, 0, 40] 32m
|
|
298
411
|
offscreen /World/Gem (MeshInstance3D) [80, 0, 0] screen [4.4, 0] 80.1m
|
|
412
|
+
hidden /World/Boss (ModelInstance3D) [0, 0, -12] 20.1m
|
|
299
413
|
overlap /World/Platform ∩ /World/Gem
|
|
300
414
|
```
|
|
301
415
|
|
|
302
|
-
Read it for the
|
|
416
|
+
Read it for the five visual failures that look identical from a node list:
|
|
303
417
|
|
|
304
418
|
- **`behind`** — the thing is fine, the camera has its back to it. Indistinguishable
|
|
305
419
|
from "my model did not load" until you can see this line.
|
|
420
|
+
- **`hidden`** — `visible: false` on the node or on any ancestor. The renderer skips
|
|
421
|
+
the whole subtree, so it is NOT in view; a screen drawing nothing used to report
|
|
422
|
+
every one of them as in view. `NOTHING IS DRAWN` when that is all of them.
|
|
423
|
+
- **`up`** — which way is up on screen. Roll changes NOTHING else, so a camera rolled
|
|
424
|
+
180° prints the same `at` and the same `looking` as an upright one; the report
|
|
425
|
+
says `is UPSIDE DOWN` when `up.y` goes negative (a top-down camera measures
|
|
426
|
+
`up.y = 0` and is left alone).
|
|
306
427
|
- **`lit by: NOTHING`** — the scene renders black. No error, no log, nothing wrong
|
|
307
428
|
with any node.
|
|
308
429
|
- **`overlap`** — two declared colliders intersect: the gem is inside the platform.
|
|
@@ -312,6 +433,13 @@ Also `incanto-play`'s `framing` command, and `describeFraming(scene)` /
|
|
|
312
433
|
as on screen when its `size` box (or collider) is, not only its origin — so a
|
|
313
434
|
200 m island whose origin sits off to the left is correctly `onScreen`.
|
|
314
435
|
|
|
436
|
+
**HUD widgets are counted separately** (`report.widgets`), because asking
|
|
437
|
+
whether the camera can see one is a category error — and because leaving them
|
|
438
|
+
out meant a shop screen made entirely of widgets and an empty scene file both
|
|
439
|
+
read `0/0 on screen`. A scene with no drawable node AND no widget now says
|
|
440
|
+
`nothing on screen`, and the gate treats it as broken: a scene that draws
|
|
441
|
+
nothing looks exactly like one that failed to load.
|
|
442
|
+
|
|
315
443
|
**A 2D scene gets a 2D report**, in pixels: the `viewport.design` window centred
|
|
316
444
|
on the current `Camera2D` and CLAMPED by its `limits`, which is the view the
|
|
317
445
|
renderer actually draws. Nothing is ever `behind` a 2D camera and nothing is
|
|
@@ -324,7 +452,7 @@ the instant a frame composites, so `canvas.toDataURL()` and every screenshot
|
|
|
324
452
|
built on it come back BLANK without it:
|
|
325
453
|
|
|
326
454
|
```ts
|
|
327
|
-
createGame3D({ ..., preserveDrawingBuffer: true });
|
|
455
|
+
createGame3D({ ..., preserveDrawingBuffer: true }); // createGame2D too
|
|
328
456
|
// or in the scene: "environment": { "rendering": { "preserveDrawingBuffer": true } }
|
|
329
457
|
```
|
|
330
458
|
|
|
@@ -510,6 +638,44 @@ The same words `incanto-play` takes — `press`/`release ACTION`, `vector ACTION
|
|
|
510
638
|
newlines. `--do-file` reads a longer plan from a file. One vocabulary for the
|
|
511
639
|
headless gamepad and the live one.
|
|
512
640
|
|
|
641
|
+
**Pointing at things**, for the games that are played that way:
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
$ bunx incanto-frame --do "at %Mole; click; step 300" --out whacked.png
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`at X Y` puts the cursor on a canvas pixel and `at <node path>` puts it wherever
|
|
648
|
+
that node is ON SCREEN — the page knows, and you cannot compute it from out
|
|
649
|
+
here. **A `click` before anything has aimed is refused**, because it would land
|
|
650
|
+
wherever the cursor was left, and a plan whose `at` failed would otherwise
|
|
651
|
+
report the aim false and the click true. Headless, `click` also answers with
|
|
652
|
+
what it HIT (`"hit": "/Game/Field/Hole11/Mole"`, or `null`) — the reason to aim
|
|
653
|
+
is to hit something, and from outside the process there is no way to ask. **`incanto-play` takes the same two words**, headless, installing a
|
|
654
|
+
geometric picker for them (there is no renderer to raycast with):
|
|
655
|
+
|
|
656
|
+
```
|
|
657
|
+
$ printf 'step 700\nat Field/Hole12/Mole\nclick\ndescribe\nquit\n' \
|
|
658
|
+
| bunx incanto-play src/game.scene.json --behaviors src/behaviors.ts
|
|
659
|
+
{"ok":true,"cmd":"at","at":[560,291]}
|
|
660
|
+
{"ok":true,"cmd":"click","t":733}
|
|
661
|
+
… /Game/Hud/Score UiText text="Moles 1 / 12"
|
|
662
|
+
``` `click` (optionally `left`/`middle`/`right`) presses and releases where the
|
|
663
|
+
cursor is, with a frame in between so the game gets to show that it is being
|
|
664
|
+
pressed.
|
|
665
|
+
|
|
666
|
+
**A drag is `mouse` — press, move, let go**, because `click` is both edges at
|
|
667
|
+
one point and a slingshot, a selection box, a swipe and an item carried between
|
|
668
|
+
inventory slots are all "press here, move there, let go":
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
$ bunx incanto-frame --do "at 400 300; mouse left down; at 300 380; mouse left up; step 400"
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`mouse` takes the same three words `click` does, and the same `0|1|2` the
|
|
675
|
+
`incanto-play` REPL takes. The language used to have only `pointer DX DY`, which is a delta —
|
|
676
|
+
the mouse-LOOK question — so an agent driving its own match-3, tower defense,
|
|
677
|
+
card game or point-and-click could reach no state at all.
|
|
678
|
+
|
|
513
679
|
`step` waits on the REAL clock: this is the running game, with its own loop, its
|
|
514
680
|
own physics and its own animation, and stepping it by hand would fight all three.
|
|
515
681
|
|
|
@@ -637,8 +803,9 @@ the path, then **▶ play ▸ ⤢ run as the game** and the page is the game aga
|
|
|
637
803
|
running your edit. `game.openEditor()` is the same switch from code.
|
|
638
804
|
|
|
639
805
|
It follows `debug` (so a build with the overlay has it) and loads lazily, so a
|
|
640
|
-
game that never opens it never downloads it. `editor: false` opts out
|
|
641
|
-
`editor: { save }` adds the editor's save
|
|
806
|
+
game that never opens it never downloads it. `editor: false` opts out at
|
|
807
|
+
RUNTIME — the button is absent — and `editor: { save }` adds the editor's save
|
|
808
|
+
button, wired to your own writer:
|
|
642
809
|
|
|
643
810
|
```ts
|
|
644
811
|
createGame3D({
|
|
@@ -660,6 +827,89 @@ overlay is a lazily-imported chunk — it stays out of the main bundle (no size
|
|
|
660
827
|
cost), but it still ships in the build, so that runtime `debug` flag is the only
|
|
661
828
|
thing gating it: keep it `false` (or dev-gated) in anything you publish.
|
|
662
829
|
|
|
830
|
+
## 9. Verify the thing you SHIP, not just the thing you run
|
|
831
|
+
|
|
832
|
+
Everything above measures a dev server. The artifact a player gets is
|
|
833
|
+
`bun run build` — a bundled, minified `dist/` with different asset paths and a
|
|
834
|
+
different module graph — and the boot path is the only code a BUILD adds over a
|
|
835
|
+
SCENE. Two of the nastiest defects this engine has had lived exactly there: a
|
|
836
|
+
`main.ts` whose scene swap left the game with no physics, and a boot that threw
|
|
837
|
+
behind a loading overlay that never came down.
|
|
838
|
+
|
|
839
|
+
`incanto-frame` and `incanto-logs` ask a dev server, and the dev server asks the
|
|
840
|
+
page over vite's HMR channel. A build has no HMR, so they used to answer:
|
|
841
|
+
|
|
842
|
+
```
|
|
843
|
+
? draws — no incanto dev server found.
|
|
844
|
+
? says — no incanto dev server found.
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
**`incanto-serve` closes that.** It serves your build as plain static files —
|
|
848
|
+
no bundler, no transform, byte-for-byte what you deploy — and adds only the
|
|
849
|
+
`/__incanto/*` endpoints the CLIs already speak, plus one inline script injected
|
|
850
|
+
into `index.html` at serve time. Nothing is written to disk, so what you measure
|
|
851
|
+
is what you ship.
|
|
852
|
+
|
|
853
|
+
```bash
|
|
854
|
+
bun run build # the artifact
|
|
855
|
+
bunx incanto-serve # ./dist on :4180 (or: incanto-serve build --port 5050)
|
|
856
|
+
# open http://127.0.0.1:4180/ , then:
|
|
857
|
+
bunx incanto-verify # draws and says now measure the BUILT game
|
|
858
|
+
bunx incanto-logs # what the built page is complaining about
|
|
859
|
+
bunx incanto-frame --out shipped.png
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
**Ship-check list**, in the order things actually break:
|
|
863
|
+
|
|
864
|
+
1. **`bun run build` at all.** Dev-only code (`import.meta.env.DEV`, a plugin
|
|
865
|
+
that is `apply: 'serve'`) can compile and then do nothing.
|
|
866
|
+
2. **Open the built page.** A blank screen with a console error is the common
|
|
867
|
+
failure; `incanto-logs` reads it without you opening devtools.
|
|
868
|
+
3. **`incanto-verify` against `incanto-serve`.** `draws` catches an asset base
|
|
869
|
+
path that survived dev and broke under a subdirectory deploy; `says` catches
|
|
870
|
+
the 404s.
|
|
871
|
+
4. **A phone-sized window.** See `incanto-web-integration.md` — a HUD that is
|
|
872
|
+
legible at 1280×800 can be 6 px tall at 390×844.
|
|
873
|
+
5. **A second load.** Saves, settings and unlocks come back; a private window
|
|
874
|
+
answers `slots.persistent === false` (see `incanto-save-slots.md`).
|
|
875
|
+
|
|
876
|
+
The one thing `incanto-serve` is not is a production server: it is a local tool
|
|
877
|
+
for looking at a build, on `127.0.0.1` by default.
|
|
878
|
+
|
|
879
|
+
### The editor is in your build, even in a 2D game
|
|
880
|
+
|
|
881
|
+
`editor: false` is a runtime option and **cannot remove bytes from a bundle**.
|
|
882
|
+
The editor chunk is lazy — a game that never opens it never downloads it — but
|
|
883
|
+
it is in `dist/`, and it imports `incanto/3d`, which imports rapier3d. Measured
|
|
884
|
+
on a pristine `platformer-2d` scaffold:
|
|
885
|
+
|
|
886
|
+
```
|
|
887
|
+
2236503 assets/rapier-*.js rapier3d ← a 2D game can never execute this
|
|
888
|
+
1692278 assets/rapier-*.js rapier2d ← the one it uses
|
|
889
|
+
471186 assets/3d-*.js the 3D adapter
|
|
890
|
+
─────────
|
|
891
|
+
5979588 total
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
A build-time decision needs a build-time lever:
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
import { incantoScenes, incantoTrim } from 'incanto/vite';
|
|
898
|
+
export default defineConfig({
|
|
899
|
+
plugins: [incantoScenes(), incantoTrim({ editor: false })],
|
|
900
|
+
});
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
```
|
|
904
|
+
5979588 → 2769972 bytes 53.7% smaller, same game
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Build only: the dev server keeps the editor, so `☰ debug ▸ ✎ edit this scene`
|
|
908
|
+
works the whole time you are making the game and is simply absent from what you
|
|
909
|
+
ship. Opening it in a trimmed build says so in one sentence rather than failing
|
|
910
|
+
obscurely. Reach for this when deploy size matters — a CDN, a jam upload, an
|
|
911
|
+
agent8 zip — and leave it off otherwise.
|
|
912
|
+
|
|
663
913
|
## 8. Debugging signals
|
|
664
914
|
|
|
665
915
|
- `this.log.info(...)` in behaviors — shows up in `runScript().logs` (and the
|
|
@@ -685,6 +935,32 @@ rows until the orange box lands on the thing you're hunting.
|
|
|
685
935
|
inside a `static` subtree, HUD widgets outside a HudLayer. Programmatic:
|
|
686
936
|
`auditScene(json)` from `incanto` or `incanto/test` returns the warnings.
|
|
687
937
|
|
|
938
|
+
**Including a HUD line that names the wrong key.** `tps-3d`'s pause menu read
|
|
939
|
+
`Esc to resume · R to restart` for exactly three hours after `restart` was moved
|
|
940
|
+
off `KeyR` — the binding moved and the sentence about it did not, which is the
|
|
941
|
+
whole failure mode of any fact written down twice. The check is narrow on
|
|
942
|
+
purpose: it fires only when a line names both a key it recognises AND a word
|
|
943
|
+
that is a DECLARED ACTION in the same scene, so "Esc to resume" says nothing
|
|
944
|
+
(there is no `resume` action) and prose is left alone.
|
|
945
|
+
|
|
946
|
+
**Including one key bound to two actions.** `tps-3d` shipped `reload` and
|
|
947
|
+
`restart` both on `KeyR`: pressing R reloaded, and `restart` — which nothing
|
|
948
|
+
read — looked broken. Two actions on one key are legal, sometimes deliberate,
|
|
949
|
+
so this warns rather than refuses; the sibling question ("does anything read
|
|
950
|
+
this action at all?") is `input.unreadActions()`, in `incanto-playtesting.md`.
|
|
951
|
+
|
|
952
|
+
**Including a `Clickable` nobody listens to.** It produces `clicked`, `hovered`
|
|
953
|
+
and `unhovered` and nothing else, so attaching it and wiring none of them costs
|
|
954
|
+
a pick every frame and can never do anything — the likeliest mistake a mouse
|
|
955
|
+
game makes, and one with no error anywhere. A board of sixteen unwired tiles
|
|
956
|
+
used to print `1/1 scene(s) valid`.
|
|
957
|
+
|
|
958
|
+
```
|
|
959
|
+
warn: Game/A: Clickable produces nothing but 'clicked'/'hovered'/'unhovered',
|
|
960
|
+
and no connection listens to any of them — so it can never do anything.
|
|
961
|
+
Wire one, or listen in code with `node.on('clicked', …)`.
|
|
962
|
+
```
|
|
963
|
+
|
|
688
964
|
**Including a node path that points at nothing.** `Chase.target`,
|
|
689
965
|
`Camera2D.follow`, `Spawner.prefab`, `Joint3D.target`, `skinPath`, `terrain` —
|
|
690
966
|
these hold a path to another node, and a wrong one is SILENT: a connection that
|
|
@@ -699,8 +975,12 @@ warn: World/Cam: follow — '/Level/Player' starts at 'Level', but the scene
|
|
|
699
975
|
```
|
|
700
976
|
|
|
701
977
|
An empty value is never reported — `""` is the default of most of these and
|
|
702
|
-
means "not set"
|
|
703
|
-
|
|
978
|
+
means "not set" (and `getNodeOrNull("")` returns null, so the guard you would
|
|
979
|
+
write for it is unnecessary). A behavior of YOUR OWN is reported too, once you
|
|
980
|
+
hand the checker the file that registers it: `incanto-check src --behaviors
|
|
981
|
+
src/behaviors.ts`. Without the flag it has no schema for your behaviours and
|
|
982
|
+
cannot know which of their props are paths — so pass it, the way every
|
|
983
|
+
scaffolded project's `check` script does.
|
|
704
984
|
|
|
705
985
|
## Deterministic replay (record once, regression-test forever)
|
|
706
986
|
|
|
@@ -717,7 +997,7 @@ save.set('replays/level1', rec.stop());
|
|
|
717
997
|
|
|
718
998
|
// in a headless test:
|
|
719
999
|
const engine = new Engine({ seed: SAME_SEED });
|
|
720
|
-
engine.setScene(loadScene(sceneJson));
|
|
1000
|
+
engine.setScene(loadScene(sceneJson, { engine }));
|
|
721
1001
|
replay(engine, recording, { onTick(i) { /* mid-run asserts */ } });
|
|
722
1002
|
expect(player.position).toEqual(expected); // exact, not approximate
|
|
723
1003
|
```
|
|
@@ -83,6 +83,34 @@ null when the ray misses (looking at the sky). `pick(sx, sy)` gives you the NODE
|
|
|
83
83
|
under a pixel — a different question, and the only one 3D could answer before
|
|
84
84
|
0.66.
|
|
85
85
|
|
|
86
|
+
**Inside a Behavior there is no renderer**, so the same four questions are on
|
|
87
|
+
the engine, installed by whichever renderer is running and cleared when it is
|
|
88
|
+
disposed:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
this.engine.pointerWorld() // where the cursor is → number[] | null
|
|
92
|
+
this.engine.toWorld(sx, sy) // any screen point → [x, y] | [x, y, z]
|
|
93
|
+
this.engine.toScreen(node.position) // → { x, y, behind } | null
|
|
94
|
+
this.engine.screenRay(sx, sy) // 3D only → { origin, dir } for castRay
|
|
95
|
+
this.engine.pickAt(sx, sy) // which node (cached per frame)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The answer is the scene's own shape — `[x, y]` in 2D, `[x, y, z]` on the ground
|
|
99
|
+
in 3D — so it goes straight back into a `position`. All are null with no
|
|
100
|
+
renderer; a `runScript` `click` step (and `incanto-play`'s `at`) installs
|
|
101
|
+
geometric `toWorld`/`toScreen`/`screenRay` for the run, so a game that AIMS is
|
|
102
|
+
testable headless and not only one that clicks.
|
|
103
|
+
|
|
104
|
+
**On uneven ground, cast the ray** — `toWorld` lands on a plane, which is the
|
|
105
|
+
wrong answer under a hill or a crate:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const at = this.engine.input.pointerPosition();
|
|
109
|
+
const ray = at && this.engine.screenRay?.(at.x, at.y);
|
|
110
|
+
const hit = ray && this.engine.physics?.castRay(ray.origin, ray.dir, 100);
|
|
111
|
+
if (hit) place(hit.point); // WHERE it landed, not how far
|
|
112
|
+
```
|
|
113
|
+
|
|
86
114
|
Perf note: `useNodeProp` deep-compares per frame — subscribe to LEAF values
|
|
87
115
|
(`'UI/Score', 'text'`), not big objects like a whole `animations` map.
|
|
88
116
|
|
|
@@ -40,7 +40,9 @@ the vite config and the verify harness.
|
|
|
40
40
|
|
|
41
41
|
Pick by shape, not by subject: `tps-3d` (third-person combat), `platformer-2d`,
|
|
42
42
|
`star-survivor` (top-down survivor), `village-quest-3d` (quest/NPC),
|
|
43
|
-
`beacon-isle-3d` (open world)
|
|
43
|
+
`beacon-isle-3d` (open world), `molehill-2d` (**played with the MOUSE** — no
|
|
44
|
+
character, no keyboard: the shape a match-3, a tower defense, a card game, a
|
|
45
|
+
point-and-click or an RTS starts from).
|
|
44
46
|
|
|
45
47
|
---
|
|
46
48
|
|
|
@@ -242,7 +244,8 @@ Things that cost real time, in the order you will meet them.
|
|
|
242
244
|
| a 3D character that feels right | `incanto-3d-character.md` |
|
|
243
245
|
| terrain, water, trees, sky | `incanto-environment.md` |
|
|
244
246
|
| sound | `incanto-audio.md` |
|
|
245
|
-
| shake, flash, hit-stop, particles | `incanto-
|
|
247
|
+
| shake, flash, hit-stop, particles | `incanto-gameplay-behaviors.md` (`CameraShake`, `screenFlash`, `hitStop`, `Particles2D/3D`) |
|
|
248
|
+
| is the feel RIGHT? measure it | `incanto-game-feel.md` |
|
|
246
249
|
| HUD, menus, inventory | `incanto-hud.md` |
|
|
247
250
|
| proving it works | `incanto-verifying-your-game.md` |
|
|
248
251
|
|