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.
- package/dist/2d.d.ts +3 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +3 -3
- package/dist/3d.js +5 -5
- package/dist/{audio-player-D5GJgb_x.d.ts → audio-player-BrEHwbK1.d.ts} +1 -1
- package/dist/{behavior-DsgayMsH.d.ts → behavior-CmMuQ6CL.d.ts} +109 -0
- package/dist/{create-game-B1KM6dA6.js → create-game-DIyh5Zy9.js} +6 -6
- package/dist/{create-game-B746rYbr.js → create-game-W2xzTJhs.js} +6 -6
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-BOOKmkQ7.js → duplicate-CqSAtdrh.js} +1 -1
- package/dist/{environment-presets-D2vzw583.js → environment-presets-BvHs6HEk.js} +3 -3
- package/dist/{gameplay-CLgFdkh5.js → gameplay-D-H986A4.js} +107 -11
- package/dist/gameplay.d.ts +17 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/{loader-DolLJWJn.d.ts → loader-CTB441z4.d.ts} +1 -1
- package/dist/{loader-zDynoew_.js → loader-lQDCwNag.js} +59 -3
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-BPJcJRRP.js → physics-2d-CmnunaR2.js} +4 -2
- package/dist/{physics-3d-BOO58xzw.js → physics-3d-DULB1lD1.js} +5 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-Btm7_Emq.js → register-CgeR4ckF.js} +15 -1
- package/dist/{register-dGsbnJ87.js → register-DO5_Qp0Q.js} +3 -3
- package/dist/{replay-DKRmiVjk.js → replay-CEw2xAhG.js} +2 -2
- package/dist/{replay-C5x2vPF5.d.ts → replay-CThvwkcQ.d.ts} +1 -1
- package/dist/{split-screen-D7OopelJ.d.ts → split-screen-DxZzC1nm.d.ts} +2 -2
- package/dist/{split-screen-Dx0LvzqS.js → split-screen-gb0c3xdb.js} +2 -2
- package/dist/{src-DEWy2qpb.js → src-DYffzHbd.js} +1 -1
- package/dist/{teardown-BwhkcNt8.js → teardown-BordHL9o.js} +1 -1
- package/dist/{test-Ccob3X5i.js → test-CXZuXs8k.js} +13 -13
- package/dist/test.d.ts +4 -4
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-Di-UEn5O.js → agent8-DO5qzIOD.js} +1 -1
- package/editor/assets/{debug-DWztJ5_y.js → debug-DntG9CVD.js} +1 -1
- package/editor/assets/{index-fct4H89G.js → index-CqEUVaPN.js} +72 -72
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/incanto-3d-character.md +11 -1
- package/skills/incanto-behaviors-and-scripts.md +21 -0
- package/skills/incanto-gameplay-behaviors.md +92 -3
- package/skills/incanto-physics-and-input.md +15 -1
- package/templates-app/beacon-isle-3d/coverage.json +9 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/coverage.json +5 -0
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/coverage.json +5 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/coverage.json +5 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +7 -1
- package/templates-app/village-quest-3d/coverage.json +9 -0
- 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-
|
|
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
|
@@ -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
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
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):
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"script": {
|
|
97
97
|
"name": "ScoreKeeper",
|
|
98
98
|
"props": {
|
|
99
|
-
"lives":
|
|
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",
|