incanto 0.63.0 → 0.64.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/incanto-playtest.mjs +14 -5
- package/bin/incanto-verify.mjs +9 -4
- package/dist/2d.js +3 -3
- package/dist/3d.js +4 -4
- package/dist/{create-game-lLeITaZ5.js → create-game-B1KM6dA6.js} +3 -3
- package/dist/{create-game-viBqXUoZ.js → create-game-B746rYbr.js} +3 -3
- package/dist/{environment-presets-QR7_75KJ.js → environment-presets-D2vzw583.js} +1 -1
- package/dist/{gameplay-CuqoHHUB.js → gameplay-CLgFdkh5.js} +32 -1
- package/dist/gameplay.d.ts +23 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.js +2 -2
- package/dist/{physics-2d-CllJXlic.js → physics-2d-BPJcJRRP.js} +4 -4
- package/dist/{physics-3d-BL_pFJ19.js → physics-3d-BOO58xzw.js} +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-1cKM8DEj.js → register-dGsbnJ87.js} +1 -1
- package/dist/{replay-Bhdntkvq.js → replay-DKRmiVjk.js} +40 -0
- package/dist/{src-BlV9Sv4m.js → src-DEWy2qpb.js} +1 -1
- package/dist/{test-Ct1_zf5S.js → test-Ccob3X5i.js} +8 -8
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-CLZXBRoM.js → agent8-Di-UEn5O.js} +1 -1
- package/editor/assets/{debug-eaSKFAKW.js → debug-DWztJ5_y.js} +1 -1
- package/editor/assets/{index-D66AuRwG.js → index-fct4H89G.js} +50 -50
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/incanto-gameplay-behaviors.md +27 -5
- package/skills/incanto-node-reference.md +1 -1
- package/skills/incanto-physics-and-input.md +40 -8
- package/skills/incanto-your-first-game.md +18 -5
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/PROJECT/Requirements.md +3 -1
- package/templates-app/tps-3d/index.html +3 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/behaviors.ts +4 -2
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/grove.scene.json +6 -2
- package/templates-app/village-quest-3d/src/village.scene.json +12 -4
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-fct4H89G.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -90,8 +90,8 @@ a connection `to` the root is `/root` (or `.`), never `/root/Game`.
|
|
|
90
90
|
|
|
91
91
|
Hit points with regen and post-hit invulnerability (i-frames). The universal
|
|
92
92
|
"can be hurt / can die" behavior. Hurt it via `damage(n)` (e.g. from
|
|
93
|
-
`DamageOnContact`); heal/kill via `heal(n)` / `kill()
|
|
94
|
-
|
|
93
|
+
`DamageOnContact`); heal/kill via `heal(n)` / `kill()`; bring it back with
|
|
94
|
+
`revive(hp?)`. Clamps to `0..max`.
|
|
95
95
|
|
|
96
96
|
| Prop | Default | Meaning |
|
|
97
97
|
|---|---|---|
|
|
@@ -101,8 +101,24 @@ dies once.
|
|
|
101
101
|
| `freeOnDeath` | `false` | `queueFree()` this node when it dies (clone-safe) |
|
|
102
102
|
|
|
103
103
|
Signals: `damaged(amount, current)` · `healed(amount, current)` ·
|
|
104
|
-
`healthChanged(current, max)` · `died`
|
|
105
|
-
Methods: `damage(n)` · `heal(n)` · `kill()` — state: `current`,
|
|
104
|
+
`healthChanged(current, max)` · `died` · `revived(current)`
|
|
105
|
+
Methods: `damage(n)` · `heal(n)` · `kill()` · `revive(hp?)` — state: `current`,
|
|
106
|
+
`isDead`
|
|
107
|
+
|
|
108
|
+
**A dead Health STAYS dead until you revive it.** `damage`, `heal` and regen all
|
|
109
|
+
no-op once it has died — which is what makes a lives system work at all, and
|
|
110
|
+
what breaks one that forgets the other half:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "signal": "died", "from": "Player", "to": "Score", "handler": "loseLife" },
|
|
114
|
+
{ "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "revive" }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Without the second wire the player is a walking corpse after the first death —
|
|
118
|
+
full HP bar, immune to every enemy, `died` never firing again, lives frozen, and
|
|
119
|
+
the game quietly unlosable, with every check green. `revive()` comes back at
|
|
120
|
+
`max` (or the hp you pass), re-arms the i-frames, and does nothing at all to
|
|
121
|
+
someone who never died.
|
|
106
122
|
|
|
107
123
|
**`healthChanged` is the one to put on a health BAR.** `damaged`/`healed` lead
|
|
108
124
|
with the DELTA, so wiring either to `UiBar.setValue` paints the damage as the
|
|
@@ -226,7 +242,7 @@ calls `damage(amount)`, then emits `dealtDamage(amount, healthOwnerNode)`.
|
|
|
226
242
|
| Prop | Default | Meaning |
|
|
227
243
|
|---|---|---|
|
|
228
244
|
| `amount` | `10` | HP removed per contact |
|
|
229
|
-
| `targetGroup` | `""` | only damage targets
|
|
245
|
+
| `targetGroup` | `""` | only damage targets in this group — the tag may sit on the BODY or on the node carrying its `Health` (`""` = any) |
|
|
230
246
|
| `oncePerTarget` | `true` | damage each target at most **once, ever** |
|
|
231
247
|
| `repeatEvery` | `0` | seconds between re-hits while still overlapping (`0` = entry only) |
|
|
232
248
|
| `destroySelf` | `false` | `queueFree()` after the first hit |
|
|
@@ -257,6 +273,12 @@ Pair it with `Health.invulnerableFor`, which is the real "don't drain me every
|
|
|
257
273
|
frame" guard: with `invulnerableFor: 0.6`, a `repeatEvery` under 0.6 s just
|
|
258
274
|
lands on i-frames, so 0.5–1 s is the useful range.
|
|
259
275
|
|
|
276
|
+
The tag may sit on either end of the entity, because they are commonly
|
|
277
|
+
different nodes: a body holds one behavior, so a player with its own script
|
|
278
|
+
keeps its `Health` on a child, and the group stays on the body. `incanto check`
|
|
279
|
+
reports a `targetGroup` that nothing in the scene can satisfy — a hazard that is
|
|
280
|
+
wired, validated and disarmed is otherwise invisible.
|
|
281
|
+
|
|
260
282
|
**`targetGroup` — stop enemies killing each other.** With no collision layers in
|
|
261
283
|
v0 (everything overlaps everything), an enemy's contact hitbox would damage any
|
|
262
284
|
`Health` it touches, including other enemies clustered at the spawn point. Gate
|
|
@@ -1438,7 +1438,7 @@ Signals: `flowChanged`
|
|
|
1438
1438
|
| `invulnerableFor` | `0` | number |
|
|
1439
1439
|
| `freeOnDeath` | `false` | boolean |
|
|
1440
1440
|
|
|
1441
|
-
Signals: `damaged(amount, current)` · `healed(amount, current)` · `died` · `healthChanged`
|
|
1441
|
+
Signals: `damaged(amount, current)` · `healed(amount, current)` · `died` · `revived` · `healthChanged`
|
|
1442
1442
|
|
|
1443
1443
|
### `Interactable`
|
|
1444
1444
|
|
|
@@ -428,14 +428,46 @@ From scene JSON, the **`Clickable`** behavior needs no code at all:
|
|
|
428
428
|
| `maxDistance` | `0` | ignore clicks further than this (0 = any) |
|
|
429
429
|
| `enabled` | `true` | stop responding without detaching |
|
|
430
430
|
|
|
431
|
-
Signals: **`clicked`**, **`hovered`**, **`unhovered`** — wire
|
|
432
|
-
`connections` like any other.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
431
|
+
Signals: **`clicked(node)`**, **`hovered(node)`**, **`unhovered(node)`** — wire
|
|
432
|
+
them in `connections` like any other. **Each one carries the node it happened
|
|
433
|
+
on**, so one handler can serve a whole board of tiles:
|
|
434
|
+
|
|
435
|
+
```jsonc
|
|
436
|
+
{ "signal": "clicked", "from": "/Board/Tile3", "to": "/Board", "handler": "onTileClicked" }
|
|
437
|
+
```
|
|
438
|
+
```ts
|
|
439
|
+
onTileClicked(tile: Node) { this.flip(tile); } // which tile, without a wire each
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`clicked` fires on RELEASE over the same node the press started on (a drag that
|
|
443
|
+
ends elsewhere is not a click, the way every button on every platform behaves),
|
|
444
|
+
and a hit on a CHILD counts as a hit on the node — the raycast lands on the
|
|
445
|
+
visual mesh, which is usually a child.
|
|
446
|
+
|
|
447
|
+
### Testing a mouse-driven game headlessly
|
|
448
|
+
|
|
449
|
+
There is no renderer and therefore no raycast, so `engine.picker` is null and
|
|
450
|
+
`Clickable` is inert. That is not a dead end: give the engine a picker of your
|
|
451
|
+
own and drive the cursor.
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
// A picker that answers from the tree instead of a GPU raycast.
|
|
455
|
+
engine.picker = (x, y) => hitTestYourBoard(x, y); // return a Node or null
|
|
456
|
+
|
|
457
|
+
engine.input.setPointerPosition(150, 0); // where the cursor IS
|
|
458
|
+
engine.input.handleMouseButton(0, true); // press …
|
|
459
|
+
engine.step();
|
|
460
|
+
engine.input.handleMouseButton(0, false); // … and release
|
|
461
|
+
engine.step(); // `clicked` fires here
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Note the BUTTON, not an action: `Clickable` reads `mouseJustPressed` directly,
|
|
465
|
+
so `pressAction('click')` drives nothing. Moving the cursor between the press
|
|
466
|
+
and the release correctly produces no click.
|
|
467
|
+
|
|
468
|
+
Without this a mouse game's entire input surface is load-validated and never
|
|
469
|
+
once executed — the scene is legal, every wire resolves, and nothing has ever
|
|
470
|
+
been clicked.
|
|
439
471
|
|
|
440
472
|
## Placement rules
|
|
441
473
|
|
|
@@ -22,12 +22,17 @@ if you skim.
|
|
|
22
22
|
## 1. Three minutes to a running game
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
bunx incanto new --list # what the starters are
|
|
26
|
-
bunx incanto new my-game --template tps-3d # or platformer-2d, star-survivor, …
|
|
25
|
+
bunx incanto@latest new --list # what the starters are
|
|
26
|
+
bunx incanto@latest new my-game --template tps-3d # or platformer-2d, star-survivor, …
|
|
27
27
|
cd my-game && bun install
|
|
28
28
|
bun run check && bun run typecheck && bun run verify
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`@latest` matters: `bunx` caches the CLI, and a cached one scaffolds a project
|
|
32
|
+
pinned to ITS version, so you can quietly get an engine a release behind the
|
|
33
|
+
docs you are reading. Check with `grep incanto package.json` if anything below
|
|
34
|
+
does not match what you see.
|
|
35
|
+
|
|
31
36
|
The starter is a **complete game**, green on arrival, and it is meant to be
|
|
32
37
|
reshaped rather than read. Four agents building four different games all started
|
|
33
38
|
here; the two who hand-authored a scene from scratch still scaffolded first, for
|
|
@@ -77,12 +82,19 @@ and it is entirely JSON.
|
|
|
77
82
|
|
|
78
83
|
```jsonc
|
|
79
84
|
"connections": [
|
|
80
|
-
{ "signal": "died",
|
|
81
|
-
{ "signal": "
|
|
82
|
-
{ "signal": "
|
|
85
|
+
{ "signal": "died", "from": "Player", "to": "Score", "handler": "loseLife" },
|
|
86
|
+
{ "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "revive" },
|
|
87
|
+
{ "signal": "won", "from": "Score", "to": "Flow", "handler": "win" },
|
|
88
|
+
{ "signal": "lost", "from": "Score", "to": "Flow", "handler": "gameOver" }
|
|
83
89
|
]
|
|
84
90
|
```
|
|
85
91
|
|
|
92
|
+
**`lifeLost → revive` is not optional.** A `Health` that has died stays dead:
|
|
93
|
+
`damage`, `heal` and regen all stop, so without that wire the player becomes a
|
|
94
|
+
walking corpse after the first death — full HP bar, immune to everything,
|
|
95
|
+
`died` never firing again, lives frozen, and the game quietly unlosable. It
|
|
96
|
+
looks fine from every check.
|
|
97
|
+
|
|
86
98
|
`GameFlow` freezes `engine.timeScale`, shows a sticky banner, and waits for the
|
|
87
99
|
`restart` action. **Give the Flow its own node** — a node holds one behavior and
|
|
88
100
|
your root probably already has the game's director script.
|
|
@@ -207,6 +219,7 @@ Things that cost real time, in the order you will meet them.
|
|
|
207
219
|
|
|
208
220
|
| when | the trap |
|
|
209
221
|
| --- | --- |
|
|
222
|
+
| a game with LIVES | `died` stops a `Health` for good — wire `lifeLost → revive` or you can spend only one. |
|
|
210
223
|
| wiring a score | `died` carries nothing; `addScore(n)` wants one → `NaN`. Wire `dealtDamage` from the killer. |
|
|
211
224
|
| enemies feel harmless | `repeatEvery` on the contact hitbox, or one hit is all you get. |
|
|
212
225
|
| enemies never arrive | a chaser cannot climb — `stepHeight`, and it loses to a large downward velocity you apply yourself. |
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
strafe-aim rig (always face the camera) would be a custom controller tweak.
|
|
32
32
|
- Mouse look needs POINTER LOCK (click the canvas). A free cursor never spins the
|
|
33
33
|
camera (deltas only accumulate while locked or a button is held).
|
|
34
|
-
- HUD is DOM, not
|
|
34
|
+
- HUD is DOM here by CHOICE, not by necessity: `HudLayer` + `UiText`/`UiBar`
|
|
35
|
+
work in 3D too and are the ones a headless check can read (incanto-hud.md),
|
|
36
|
+
and this scene keeps a `HudLayer` for its banner. The crosshair,
|
|
35
37
|
HUD and banner live in `index.html`; `HudUpdater` fills them (guarded for
|
|
36
38
|
headless). The crosshair marks the camera aim point, not the gun barrel.
|
|
37
39
|
- Zero-asset: arena, soldier and enemies are primitives; SFX are procedural
|
|
@@ -110,7 +110,9 @@
|
|
|
110
110
|
width: 56px;
|
|
111
111
|
height: 56px;
|
|
112
112
|
}
|
|
113
|
-
/*
|
|
113
|
+
/* This game's HUD is plain DOM. `HudLayer` + `UiText`/`UiBar` work in 3D
|
|
114
|
+
too (see incanto-hud.md) and are readable headlessly — the scene has one
|
|
115
|
+
for the banner. Either is fine; DOM here is a choice, not a limit. */
|
|
114
116
|
#hud {
|
|
115
117
|
position: fixed;
|
|
116
118
|
top: 14px;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* snaps the visible character to FACE the aim on each shot so the soldier turns
|
|
12
12
|
* to shoot where you're looking.
|
|
13
13
|
* - `HudUpdater` — pure presentation glue. The gameplay library owns the numbers,
|
|
14
|
-
* not how they're drawn, and
|
|
14
|
+
* not how they're drawn, and this game draws its HUD in plain DOM, so it reads
|
|
15
15
|
* ScoreKeeper.score / Health.current / the wave index / the ammo count each
|
|
16
16
|
* frame into the HTML HUD overlay (index.html) and flips the win/lose banner.
|
|
17
17
|
*/
|
|
@@ -315,7 +315,9 @@ export class Shoot extends Behavior {
|
|
|
315
315
|
|
|
316
316
|
// ---------------------------------------------------------------------------
|
|
317
317
|
// HudUpdater — writes the live numbers into the HTML HUD overlay (index.html).
|
|
318
|
-
//
|
|
318
|
+
// This HUD is plain DOM by choice. `HudLayer` + `UiText`/`UiBar` also work in
|
|
319
|
+
// 3D and are the readable-headlessly option (incanto-hud.md); the scene keeps a
|
|
320
|
+
// HudLayer for its banner. All DOM
|
|
319
321
|
// access is guarded: headless runs (verify, runScript) have no `document`, so
|
|
320
322
|
// every write is a no-op and the gameplay logic is unaffected.
|
|
321
323
|
// ---------------------------------------------------------------------------
|
|
@@ -735,6 +735,10 @@
|
|
|
735
735
|
"preset": "hit",
|
|
736
736
|
"volume": 0.6
|
|
737
737
|
}
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
"name": "Catch",
|
|
741
|
+
"type": "Respawn"
|
|
738
742
|
}
|
|
739
743
|
],
|
|
740
744
|
"script": {
|
|
@@ -801,7 +805,7 @@
|
|
|
801
805
|
"type": "UiText",
|
|
802
806
|
"uid": "n_dg8p7etf1n6lc495",
|
|
803
807
|
"props": {
|
|
804
|
-
"text": "click/F strike
|
|
808
|
+
"text": "click/F strike \u00b7 Shift sprint \u00b7 portal returns home",
|
|
805
809
|
"size": 12,
|
|
806
810
|
"color": "#cccccc",
|
|
807
811
|
"anchor": "bottomRight",
|
|
@@ -830,7 +834,7 @@
|
|
|
830
834
|
"name": "ResumeHint",
|
|
831
835
|
"type": "UiText",
|
|
832
836
|
"props": {
|
|
833
|
-
"text": "Esc to resume
|
|
837
|
+
"text": "Esc to resume \u00b7 R to restart",
|
|
834
838
|
"size": 13,
|
|
835
839
|
"color": "#c9d2dd"
|
|
836
840
|
}
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"en": {
|
|
8
8
|
"quest.talkElder": "Talk to the Elder [E]",
|
|
9
9
|
"ui.playAgain": "Play again",
|
|
10
|
-
"ui.hint": "WASD move
|
|
10
|
+
"ui.hint": "WASD move \u00b7 Shift sprint \u00b7 E talk \u00b7 click/F strike",
|
|
11
11
|
"settings.language": "Language"
|
|
12
12
|
},
|
|
13
13
|
"ko": {
|
|
14
|
-
"quest.talkElder": "
|
|
15
|
-
"ui.playAgain": "
|
|
16
|
-
"settings.language": "
|
|
14
|
+
"quest.talkElder": "\ucd0c\uc7a5\uc5d0\uac8c \ub9d0\uc744 \uac78\uc5b4\ubcf4\uc138\uc694 [E]",
|
|
15
|
+
"ui.playAgain": "\ub2e4\uc2dc \ud558\uae30",
|
|
16
|
+
"settings.language": "\uc5b8\uc5b4"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"environment": {
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"restart": {
|
|
81
81
|
"type": "button",
|
|
82
82
|
"keys": ["KeyR"]
|
|
83
|
+
},
|
|
84
|
+
"pause": {
|
|
85
|
+
"type": "button",
|
|
86
|
+
"keys": ["Escape"]
|
|
83
87
|
}
|
|
84
88
|
},
|
|
85
89
|
"assets": {
|
|
@@ -3430,6 +3434,10 @@
|
|
|
3430
3434
|
"preset": "hit",
|
|
3431
3435
|
"volume": 0.6
|
|
3432
3436
|
}
|
|
3437
|
+
},
|
|
3438
|
+
{
|
|
3439
|
+
"name": "Catch",
|
|
3440
|
+
"type": "Respawn"
|
|
3433
3441
|
}
|
|
3434
3442
|
]
|
|
3435
3443
|
},
|