incanto 0.64.0 → 0.65.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.
Files changed (56) hide show
  1. package/dist/2d.d.ts +3 -3
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +3 -3
  4. package/dist/3d.js +5 -5
  5. package/dist/{audio-player-D5GJgb_x.d.ts → audio-player-BrEHwbK1.d.ts} +1 -1
  6. package/dist/{behavior-DsgayMsH.d.ts → behavior-CmMuQ6CL.d.ts} +109 -0
  7. package/dist/{create-game-B1KM6dA6.js → create-game-DIyh5Zy9.js} +6 -6
  8. package/dist/{create-game-B746rYbr.js → create-game-W2xzTJhs.js} +6 -6
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-BOOKmkQ7.js → duplicate-CqSAtdrh.js} +1 -1
  11. package/dist/{environment-presets-D2vzw583.js → environment-presets-BvHs6HEk.js} +3 -3
  12. package/dist/{gameplay-CLgFdkh5.js → gameplay-D-H986A4.js} +107 -11
  13. package/dist/gameplay.d.ts +17 -1
  14. package/dist/gameplay.js +1 -1
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +5 -5
  17. package/dist/{loader-DolLJWJn.d.ts → loader-CTB441z4.d.ts} +1 -1
  18. package/dist/{loader-zDynoew_.js → loader-lQDCwNag.js} +59 -3
  19. package/dist/net.d.ts +2 -2
  20. package/dist/net.js +1 -1
  21. package/dist/{physics-2d-BPJcJRRP.js → physics-2d-CmnunaR2.js} +4 -2
  22. package/dist/{physics-3d-BOO58xzw.js → physics-3d-DULB1lD1.js} +5 -3
  23. package/dist/react.d.ts +1 -1
  24. package/dist/react.js +1 -1
  25. package/dist/{register-Btm7_Emq.js → register-CgeR4ckF.js} +15 -1
  26. package/dist/{register-dGsbnJ87.js → register-DO5_Qp0Q.js} +3 -3
  27. package/dist/{replay-DKRmiVjk.js → replay-CEw2xAhG.js} +2 -2
  28. package/dist/{replay-C5x2vPF5.d.ts → replay-CThvwkcQ.d.ts} +1 -1
  29. package/dist/{split-screen-D7OopelJ.d.ts → split-screen-DxZzC1nm.d.ts} +2 -2
  30. package/dist/{split-screen-Dx0LvzqS.js → split-screen-gb0c3xdb.js} +2 -2
  31. package/dist/{src-DEWy2qpb.js → src-DYffzHbd.js} +1 -1
  32. package/dist/{teardown-BwhkcNt8.js → teardown-BordHL9o.js} +1 -1
  33. package/dist/{test-Ccob3X5i.js → test-CXZuXs8k.js} +13 -13
  34. package/dist/test.d.ts +4 -4
  35. package/dist/test.js +2 -2
  36. package/dist/vite.js +2 -2
  37. package/editor/assets/{agent8-Di-UEn5O.js → agent8-DO5qzIOD.js} +1 -1
  38. package/editor/assets/{debug-DWztJ5_y.js → debug-DntG9CVD.js} +1 -1
  39. package/editor/assets/{index-fct4H89G.js → index-CqEUVaPN.js} +72 -72
  40. package/editor/index.html +1 -1
  41. package/package.json +1 -1
  42. package/skills/incanto-3d-character.md +11 -1
  43. package/skills/incanto-behaviors-and-scripts.md +21 -0
  44. package/skills/incanto-gameplay-behaviors.md +92 -3
  45. package/skills/incanto-physics-and-input.md +15 -1
  46. package/templates-app/beacon-isle-3d/coverage.json +9 -0
  47. package/templates-app/beacon-isle-3d/package.json +1 -1
  48. package/templates-app/platformer-2d/coverage.json +5 -0
  49. package/templates-app/platformer-2d/package.json +1 -1
  50. package/templates-app/star-survivor/coverage.json +5 -0
  51. package/templates-app/star-survivor/package.json +1 -1
  52. package/templates-app/tps-3d/coverage.json +5 -0
  53. package/templates-app/tps-3d/package.json +1 -1
  54. package/templates-app/tps-3d/src/game.scene.json +7 -1
  55. package/templates-app/village-quest-3d/coverage.json +9 -0
  56. package/templates-app/village-quest-3d/package.json +1 -1
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-fct4H89G.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-CqEUVaPN.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.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -269,7 +269,17 @@ target being "where I'm walking." There is ONE facing formula; only the directio
269
269
  its FACE, not its back. (Headless, no three.js: with the yaw you set, the model's
270
270
  forward is `(sin(yaw), cos(yaw))`; dot it with the desired unit dir — `< 0` = wrong way.)
271
271
 
272
- **NON-player models (enemies/NPCs) face + animate via the SAME rules, by hand.**
272
+ **NON-player models (enemies/NPCs) face by the SAME rule but do not hand-write
273
+ it if a built-in is moving them.** `Patrol` and `Chase` take `facePath` (+
274
+ `turnSpeed`), which applies exactly the formula above to the skin you name:
275
+
276
+ ```jsonc
277
+ { "name": "Hunt", "type": "Node3D",
278
+ "script": { "name": "Chase",
279
+ "props": { "target": "/root/Player", "moveParent": true, "facePath": "../Skin" } } }
280
+ ```
281
+
282
+ Only a model driven by YOUR OWN behavior needs the formula written out.
273
283
  `CharacterController3D` automates this for the player; a custom-driven model does it
274
284
  itself — face with the formula above, and drive `skin.animation` from your AI state (see
275
285
  incanto-gameplay-behaviors.md "Driving a model's animation from a custom AI"). Ground a
@@ -84,6 +84,27 @@ under an existing name (re-registering a different class without it → `DUPLICA
84
84
  with `new Engine({ seed: 42 })` a run replays identically (scripted verification).
85
85
  - `this.log` — the engine log channel (`debug/info/warn/error(...parts)`; debug overlay
86
86
  and headless harnesses tail it via `entries()` / the live `added` signal)
87
+ - `this.physics` — the physics world, for the queries AI asks:
88
+ `castRay(origin, dir, maxLen, exclude?)` for line of sight, `castSphere(...)`
89
+ (3D) for a probe that must not skim past a wall. `null` in a game with no
90
+ physics. Pass `this.node` as `exclude` when casting from your own body.
91
+ See `incanto-physics-and-input.md`.
92
+ - `this.enabled` — whether this behavior's `update`/`fixedUpdate` run, with
93
+ `enable()` / `disable()` to flip it. Every behavior has it, it is authorable
94
+ as a `script` prop, and because they are METHODS a connection can call them:
95
+
96
+ ```jsonc
97
+ { "signal": "spotted", "from": "Eyes", "to": "Hunt", "handler": "enable" }
98
+ ```
99
+
100
+ That is what makes an enemy STATE MACHINE expressible in JSON — a node carries
101
+ one behavior, so each state gets a child node and only one of them ticks. Full
102
+ recipe: "An enemy that changes its mind" in `incanto-gameplay-behaviors.md`.
103
+
104
+ Asleep is not detached: props, accumulated state, signal connections and
105
+ `serialize()` all survive; only the per-frame hooks pause. The lifecycle hooks
106
+ are never gated, so a behavior authored `enabled: false` is fully initialized
107
+ and simply idle.
87
108
 
88
109
  ## Runtime API (the imperative half)
89
110
 
@@ -382,6 +382,9 @@ author markers). Emits `reachedPoint(index)` on arrival.
382
382
  | `loop` | `true` | wrap to the first point after the last (loop mode) |
383
383
  | `mode` | `"loop"` | `'loop'` wraps · `'pingpong'` reverses at the ends |
384
384
  | `pauseAt` | `0` | seconds to wait at each reached point |
385
+ | `moveParent` | `false` | move the PARENT node instead of this one |
386
+ | `facePath` | `""` | node path to TURN toward the direction of travel (e.g. `"../Skin"`) |
387
+ | `turnSpeed` | `100` | turn rate rad/s; 100 is an instant snap |
385
388
 
386
389
  Signals: `reachedPoint(index)`
387
390
 
@@ -390,6 +393,12 @@ Signals: `reachedPoint(index)`
390
393
  "script": { "name": "Patrol", "props": { "points": [[100, 0], [300, 0]], "speed": 80, "mode": "pingpong" } } }
391
394
  ```
392
395
 
396
+ `moveParent` is the same prop `Chase` has, and it exists for the same reason: a
397
+ node carries ONE behavior, so a guard that must patrol AND hold `Health` on its
398
+ body puts `Patrol` on a CHILD. Without it a patrolling body had to carry
399
+ `Patrol` itself and could hold nothing else — a patrolling enemy you could not
400
+ hurt.
401
+
393
402
  ## Chase
394
403
 
395
404
  Homing enemy AI: each frame moves toward `target.position` at `speed`, stopping
@@ -403,6 +412,8 @@ within `stopRange` (melee reach). `reachedTarget` fires once on entering range
403
412
  | `stopRange` | `0` | stop (and emit reachedTarget) within this distance |
404
413
  | `loseRange` | `0` | give up beyond this distance (0 = never lose) |
405
414
  | `moveParent` | `false` | move the PARENT node instead of this one |
415
+ | `facePath` | `""` | node path to TURN toward the direction of travel (e.g. `"../Skin"`) |
416
+ | `turnSpeed` | `100` | turn rate rad/s; 100 is an instant snap |
406
417
 
407
418
  Signals: `reachedTarget` · `lostTarget`
408
419
 
@@ -420,6 +431,25 @@ whole entity toward the target. (Distance is measured from the moved parent.)
420
431
  "script": { "name": "Chase", "props": { "target": "/root/Player", "speed": 120, "stopRange": 20 } } }
421
432
  ```
422
433
 
434
+ **`facePath` — an NPC that does not run backwards.** `CharacterController3D`
435
+ turns the PLAYER's skin toward its travel; for an NPC nothing did, so every 3D
436
+ game hand-wrote the same `yaw = atan2(dx, dz)` — against a formula
437
+ `incanto-3d-character.md` simultaneously teaches and calls "the #1 recurring
438
+ bug", because the wrong version shows you the enemy's BACK while it charges at
439
+ you. Point `facePath` at the skin and the engine writes the right one:
440
+
441
+ ```jsonc
442
+ { "name": "Hunt", "type": "Node3D",
443
+ "script": { "name": "Chase",
444
+ "props": { "target": "/root/Player", "moveParent": true, "facePath": "../Skin" } } }
445
+ ```
446
+
447
+ It is opt-in (empty default turns nothing), it is the exact math and the exact
448
+ `turnSpeed` units the controller uses, and `bunx incanto-feel` verifies it:
449
+ `facing: /Guard/Skin faces its travel (dot 1.00)`. In 2D it writes the scalar
450
+ `rotation` (`atan2(dy, dx)`) — right for top-down, so a PLATFORMER that faces
451
+ with `flipX` should leave `facePath` empty.
452
+
423
453
  **A character body chases through the level, not through walls.** When the node
424
454
  being moved is a `CharacterBody2D`/`3D`, `Chase` (and `Patrol`) move it with
425
455
  `moveAndSlide()`, so it collides and slides like any character. A plain
@@ -431,6 +461,61 @@ geometry it used to walk through. When enemies must find their way around a
431
461
  level, that is what `buildTerrainNav` + `findPath` + `PathFollow` are for (see
432
462
  `incanto-building-3d-games.md`).
433
463
 
464
+ ## An enemy that changes its mind (`enabled`)
465
+
466
+ An enemy that patrols until it spots you and then chases is the most ordinary AI
467
+ there is, and it used to be inexpressible: a node carries ONE behavior, and
468
+ nothing could stop one. `ZombieAI` exists because of that — it is `Wander` and
469
+ `Chase` FUSED into a single class, which solves exactly one pair and no others.
470
+
471
+ **Every behavior has `enabled`** (default `true`), plus `enable()` and
472
+ `disable()` methods. They are methods, so a CONNECTION can call them — which is
473
+ the whole point. Give the guard one child per state, sleep the ones that are not
474
+ running, and the state machine is scene JSON:
475
+
476
+ ```jsonc
477
+ { "name": "Guard", "type": "CharacterBody3D", "groups": ["enemy"],
478
+ "script": { "name": "Health", "props": { "max": 40 } },
479
+ "children": [
480
+ { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$guard" } },
481
+
482
+ { "name": "Walk", "type": "Node3D",
483
+ "script": { "name": "Patrol",
484
+ "props": { "points": ["%PostA", "%PostB"], "speed": 2,
485
+ "moveParent": true, "facePath": "../Skin" } } },
486
+
487
+ { "name": "Hunt", "type": "Node3D",
488
+ "script": { "name": "Chase",
489
+ "props": { "target": "/root/Player", "speed": 5, "stopRange": 1.4,
490
+ "moveParent": true, "facePath": "../Skin", "enabled": false } } }
491
+ ] }
492
+ ```
493
+ ```jsonc
494
+ "connections": [
495
+ { "signal": "spotted", "from": "Guard/Eyes", "to": "Guard/Walk", "handler": "disable" },
496
+ { "signal": "spotted", "from": "Guard/Eyes", "to": "Guard/Hunt", "handler": "enable" },
497
+ { "signal": "lostTarget", "from": "Guard/Hunt", "to": "Guard/Hunt", "handler": "disable" },
498
+ { "signal": "lostTarget", "from": "Guard/Hunt", "to": "Guard/Walk", "handler": "enable" }
499
+ ]
500
+ ```
501
+
502
+ That is a two-state guard, with health, that walks a route, turns to face where
503
+ it is going, gives up when you break away, and goes back to its patrol — in
504
+ zero TypeScript.
505
+
506
+ **Asleep is not detached.** A disabled behavior keeps its props and everything
507
+ it had accumulated, stays reachable as `node.behavior`, keeps its signal
508
+ connections, and still saves through `serialize()`. Only `update`/`fixedUpdate`
509
+ pause. The lifecycle hooks are never gated: they are structural, they fire once,
510
+ and a behavior that skipped its own `onReady` would wake up uninitialized —
511
+ so a behavior authored `enabled: false` is fully set up and simply idle.
512
+
513
+ **OFF is explicit.** The tick is gated on `enabled !== false`, so a hand-attached
514
+ object that never heard of the prop still runs. A behavior silently not running
515
+ is a far worse failure than one running when it was meant to sleep.
516
+
517
+ From TypeScript it is the same switch: `this.getNode('../Hunt').behavior.enable()`.
518
+
434
519
  ## Wander
435
520
 
436
521
  Seeded random roaming inside a circle around the spawn point — idle critters,
@@ -452,9 +537,13 @@ run (replayable, test-stable).
452
537
 
453
538
  The staple monster AI in ONE behavior: SHAMBLE around on its own, then LOCK ON and
454
539
  CHARGE `aggroTarget` (the player) once it wanders within `aggroRange` — giving up
455
- again past `deAggroRange` (hysteresis, no boundary flicker). Use this instead of
456
- `Wander`+`Chase` (a node carries ONE behavior, so they can't co-exist) whenever you
457
- want enemies that idle/roam until the player gets close, then rush in.
540
+ again past `deAggroRange` (hysteresis, no boundary flicker). Use this whenever you want enemies that idle/roam until the player gets close,
541
+ then rush in it is one behavior, so it needs no wiring at all.
542
+
543
+ For any OTHER pair (patrol-then-chase, guard-then-flee, the state machine your
544
+ game actually wants), compose them yourself with `enabled` — see
545
+ "An enemy that changes its mind" below. This behavior is the fused
546
+ `Wander`+`Chase` from when that was the only way.
458
547
 
459
548
  | Prop | Default | Meaning |
460
549
  |---|---|---|
@@ -295,6 +295,18 @@ Chains work (pendulums, bridges: each link a body + joint to the previous).
295
295
 
296
296
  ## Raycasts
297
297
 
298
+ **Where `physics` comes from.** From a `Behavior` — where AI lives, and where
299
+ line of sight is decided — it is `this.physics`. At the boot site it is
300
+ `game.physics`, and anywhere you hold the engine it is `engine.physics`. All
301
+ three are the same object; it is `null` in a game with no physics bodies, so
302
+ `?.` and treat a missing world as "nothing in the way", or don't.
303
+
304
+ ```ts
305
+ // inside a Behavior
306
+ const hit = this.physics?.castRay(eye, dir, range, this.node);
307
+ const blocked = hit != null && hit.node !== player;
308
+ ```
309
+
298
310
  Both runtimes expose the same query (2D in PIXELS y-down, 3D in meters):
299
311
 
300
312
  ```ts
@@ -302,7 +314,9 @@ const hit = physics.castRay(origin, dir, maxLen, excludeBody?, { staticOnly?: tr
302
314
  // → { distance, normal, node } | null (sensors never block rays)
303
315
  ```
304
316
 
305
- Exclude the shooter's own body when casting from inside it.
317
+ Exclude the shooter's own body when casting from inside it — a ray that starts
318
+ inside its own collider hits itself at distance 0, which reads as "blocked" and
319
+ is the reason a vision cone can come back permanently blind.
306
320
 
307
321
  3D also has a THICK ray — a sphere sweep — for probes where skimming matters
308
322
  (the camera boom, ledge feelers):
@@ -0,0 +1,9 @@
1
+ {
2
+ "hurt": {
3
+ "onto": "%Shade1_1"
4
+ },
5
+ "lose": {
6
+ "skip": "no fail state by design \u2014 `onPlayerDown` shows a banner and reloads the level after 1.6s"
7
+ },
8
+ "seconds": 20
9
+ }
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.64.0",
17
+ "incanto": "^0.65.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -0,0 +1,5 @@
1
+ {
2
+ "hurt": { "onto": "%Spikes1" },
3
+ "lose": true,
4
+ "seconds": 20
5
+ }
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.64.0",
14
+ "incanto": "^0.65.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -0,0 +1,5 @@
1
+ {
2
+ "hurt": "afk",
3
+ "lose": true,
4
+ "seconds": 30
5
+ }
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.64.0",
14
+ "incanto": "^0.65.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -0,0 +1,5 @@
1
+ {
2
+ "hurt": "afk",
3
+ "lose": true,
4
+ "seconds": 30
5
+ }
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.64.0",
16
+ "incanto": "^0.65.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -96,7 +96,7 @@
96
96
  "script": {
97
97
  "name": "ScoreKeeper",
98
98
  "props": {
99
- "lives": 1
99
+ "lives": 3
100
100
  }
101
101
  },
102
102
  "children": [
@@ -718,6 +718,12 @@
718
718
  "to": "/root",
719
719
  "handler": "loseLife"
720
720
  },
721
+ {
722
+ "signal": "lifeLost",
723
+ "from": "/root",
724
+ "to": "Player",
725
+ "handler": "revive"
726
+ },
721
727
  {
722
728
  "signal": "damaged",
723
729
  "from": "Player",
@@ -0,0 +1,9 @@
1
+ {
2
+ "hurt": {
3
+ "onto": "%Wolf1"
4
+ },
5
+ "lose": {
6
+ "skip": "no fail state by design \u2014 `onPlayerDown` shows a banner and reloads the level after 1.6s"
7
+ },
8
+ "seconds": 20
9
+ }
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.64.0",
16
+ "incanto": "^0.65.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {