incanto 0.61.0 → 0.63.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-verify.mjs +134 -55
- package/dist/2d.d.ts +42 -0
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +22 -0
- package/dist/3d.js +5 -5
- package/dist/{create-game-DH7JI5xx.js → create-game-lLeITaZ5.js} +8 -8
- package/dist/{create-game-IX5lEH0P.js → create-game-viBqXUoZ.js} +6 -6
- package/dist/{duplicate-CGqAmK2h.js → duplicate-BOOKmkQ7.js} +1 -1
- package/dist/editor.js +39 -18
- package/dist/{environment-presets-CNxCuhZF.js → environment-presets-QR7_75KJ.js} +5 -3
- package/dist/{gameplay-Dtzd2itW.js → gameplay-CuqoHHUB.js} +153 -16
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +6 -6
- package/dist/{loader-D7jTvDQv.js → loader-zDynoew_.js} +160 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-CBnor8Zf.js → physics-2d-CllJXlic.js} +11 -3
- package/dist/{physics-3d-BTUfUSWO.js → physics-3d-BL_pFJ19.js} +123 -17
- package/dist/react.js +1 -1
- package/dist/{register-DL3izw8j.js → register-1cKM8DEj.js} +60 -9
- package/dist/{register-xuSRyD6b.js → register-Btm7_Emq.js} +158 -2
- package/dist/{replay-D7-yle3s.js → replay-Bhdntkvq.js} +15 -2
- package/dist/{split-screen-5Ban4q4n.js → split-screen-Dx0LvzqS.js} +2 -2
- package/dist/{src-DozXvyZS.js → src-BlV9Sv4m.js} +1 -1
- package/dist/{teardown-B6rwJOyS.js → teardown-BwhkcNt8.js} +1 -1
- package/dist/{test-Dch_7VQD.js → test-Ct1_zf5S.js} +64 -20
- package/dist/test.d.ts +41 -0
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-CF1JL2tR.js → agent8-CLZXBRoM.js} +1 -1
- package/editor/assets/{debug-3QzYhOPA.js → debug-eaSKFAKW.js} +1 -1
- package/editor/assets/{index-CAD2c5ug.js → index-D66AuRwG.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +91 -0
- package/skills/README.md +4 -0
- package/skills/incanto-behaviors-and-scripts.md +37 -0
- package/skills/incanto-building-2d-games.md +20 -1
- package/skills/incanto-building-3d-games.md +1 -1
- package/skills/incanto-game-feel.md +3 -2
- package/skills/incanto-gameplay-behaviors.md +45 -6
- package/skills/incanto-node-reference.md +12 -0
- package/skills/incanto-physics-and-input.md +53 -2
- package/skills/incanto-scene-json-authoring.md +15 -1
- package/skills/incanto-verifying-your-game.md +31 -1
- package/skills/incanto-your-first-game.md +237 -0
- 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/star-survivor/src/game.scene.json +4 -2
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +3 -2
- package/templates-app/tps-3d/verify.ts +38 -0
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-your-first-game
|
|
3
|
+
description: The walkthrough — scaffold, author, verify, hand back. A complete game with a real win and lose in one sitting, the shape of the loop, and the handful of traps that cost every first-time author an hour. Read this FIRST if you have not shipped an Incanto game before.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Your first Incanto game
|
|
7
|
+
|
|
8
|
+
> Shipped inside the `incanto` npm package — this document always matches the
|
|
9
|
+
> installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
|
|
10
|
+
|
|
11
|
+
This is the shortest honest path from nothing to a game with a win and a lose in
|
|
12
|
+
it. Everything below has been built and measured; the timings are real, and so
|
|
13
|
+
are the traps.
|
|
14
|
+
|
|
15
|
+
**You cannot see this game.** No browser, no GPU, no screenshot. That sounds like
|
|
16
|
+
the hard part and it is not — the engine ships the instruments, and §5 is how you
|
|
17
|
+
use them. What actually costs first-time authors an hour is §4, so read it even
|
|
18
|
+
if you skim.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Three minutes to a running game
|
|
23
|
+
|
|
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, …
|
|
27
|
+
cd my-game && bun install
|
|
28
|
+
bun run check && bun run typecheck && bun run verify
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The starter is a **complete game**, green on arrival, and it is meant to be
|
|
32
|
+
reshaped rather than read. Four agents building four different games all started
|
|
33
|
+
here; the two who hand-authored a scene from scratch still scaffolded first, for
|
|
34
|
+
the vite config and the verify harness.
|
|
35
|
+
|
|
36
|
+
Pick by shape, not by subject: `tps-3d` (third-person combat), `platformer-2d`,
|
|
37
|
+
`star-survivor` (top-down survivor), `village-quest-3d` (quest/NPC),
|
|
38
|
+
`beacon-isle-3d` (open world).
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. What you edit
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/game.scene.json ← the game. Nodes, props, connections. This is most of it.
|
|
46
|
+
src/behaviors.ts ← the parts JSON cannot express. Usually very little.
|
|
47
|
+
src/main.ts ← boot. You rarely touch it.
|
|
48
|
+
verify.ts ← the harness that proves it works. You WILL touch it.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The claim that "all structure is JSON" is not marketing: one of the four builds
|
|
52
|
+
shipped a whole 3D world with **zero** gameplay TypeScript, and another needed
|
|
53
|
+
90 lines for one melee swing. Reach for a built-in behavior before you write a
|
|
54
|
+
class — `incanto-gameplay-behaviors.md` is the list, and it is long.
|
|
55
|
+
|
|
56
|
+
Read `incanto-scene-json-authoring.md` before you write JSON. It is the format,
|
|
57
|
+
the node paths, and the connection grammar, and everything else assumes it.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. The spine: a game that can be WON and LOST
|
|
62
|
+
|
|
63
|
+
This is the part worth copying verbatim. It is four nodes and four connections,
|
|
64
|
+
and it is entirely JSON.
|
|
65
|
+
|
|
66
|
+
```jsonc
|
|
67
|
+
{ "name": "Score", "type": "Node3D",
|
|
68
|
+
"script": { "name": "ScoreKeeper", "props": { "scoreToWin": 10, "lives": 3 } } },
|
|
69
|
+
|
|
70
|
+
{ "name": "Flow", "type": "Node3D", "script": { "name": "GameFlow" } },
|
|
71
|
+
|
|
72
|
+
{ "name": "HUD", "type": "HudLayer", "children": [
|
|
73
|
+
{ "name": "Banner", "type": "UiBanner" },
|
|
74
|
+
{ "name": "Hp", "type": "UiBar", "props": { "anchor": "topLeft", "label": "HP" } }
|
|
75
|
+
] }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
"connections": [
|
|
80
|
+
{ "signal": "died", "from": "Player", "to": "Score", "handler": "loseLife" },
|
|
81
|
+
{ "signal": "won", "from": "Score", "to": "Flow", "handler": "win" },
|
|
82
|
+
{ "signal": "lost", "from": "Score", "to": "Flow", "handler": "gameOver" }
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`GameFlow` freezes `engine.timeScale`, shows a sticky banner, and waits for the
|
|
87
|
+
`restart` action. **Give the Flow its own node** — a node holds one behavior and
|
|
88
|
+
your root probably already has the game's director script.
|
|
89
|
+
|
|
90
|
+
A pause menu is the same trick and also zero TypeScript: declare a `pause`
|
|
91
|
+
action, add a `UiPanel` named `PauseMenu` under the HUD, and Escape opens it.
|
|
92
|
+
|
|
93
|
+
### The trap that eats an hour here
|
|
94
|
+
|
|
95
|
+
`died` carries **no arguments**. `ScoreKeeper.addScore(n)` needs one. So the
|
|
96
|
+
obvious way to score a kill —
|
|
97
|
+
|
|
98
|
+
```jsonc
|
|
99
|
+
// WRONG — this is the mistake, not the fix
|
|
100
|
+
{ "signal": "died", "from": "Enemy", "to": "Score", "handler": "addScore" }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
— sets the score to `NaN` on the first kill, and the win condition is
|
|
104
|
+
unreachable forever. The engine reports this the moment the wire fires; do not
|
|
105
|
+
ignore that line. Wire `dealtDamage` from the KILLER instead (it carries the
|
|
106
|
+
amount and the target), which is also clone-safe — a connection on a spawned
|
|
107
|
+
enemy never clones.
|
|
108
|
+
|
|
109
|
+
`won → GameFlow.win` is fine, by contrast, because `win(text = 'YOU WIN')`
|
|
110
|
+
defaults what it is not given. That is the difference, and it is the only one.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 4. Making things hurt (read this one)
|
|
115
|
+
|
|
116
|
+
An enemy that touches you should drain you. Two props decide whether it does,
|
|
117
|
+
and both defaults are the harmless answer.
|
|
118
|
+
|
|
119
|
+
**Contact fires on ENTRY and EXIT, never per frame.** So `oncePerTarget: false`
|
|
120
|
+
means "hurt again on RE-entry" — an enemy that closes and stops deals one hit and
|
|
121
|
+
then nothing at all. `repeatEvery` is the knob that makes a resting overlap keep
|
|
122
|
+
hurting. Six seconds of unbroken contact at `amount: 10`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
oncePerTarget=false repeatEvery=0 → hp 90 ← one hit, then nothing
|
|
126
|
+
oncePerTarget=false repeatEvery=0.5 → hp 0
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
{ "name": "Hit", "type": "Area3D",
|
|
131
|
+
"props": { "collider": { "shape": "sphere", "radius": 1.5 } },
|
|
132
|
+
"script": { "name": "DamageOnContact",
|
|
133
|
+
"props": { "amount": 12, "targetGroup": "player",
|
|
134
|
+
"oncePerTarget": false, "repeatEvery": 0.5 } } }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Pair it with `Health.invulnerableFor` (the real per-frame guard) and keep
|
|
138
|
+
`repeatEvery` at or above it — under 0.6 s it just lands on i-frames.
|
|
139
|
+
|
|
140
|
+
**And they have to be able to REACH you.** A `CharacterBody3D` walks; a chaser
|
|
141
|
+
walks a straight line and cannot go around. `stepHeight` (default 0.35 m) is how
|
|
142
|
+
high a ledge it climbs — raise it for a world with stairs. You will not notice
|
|
143
|
+
this on the player, because `CharacterController3D` rides a hover spring and
|
|
144
|
+
floats over small ledges already.
|
|
145
|
+
|
|
146
|
+
The tell for both is one line of `incanto-playtest` output:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
danger: the player took damage 0 times in 4 runs — nothing here can hurt you
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Read it. It is the truth.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 5. Handing it back
|
|
157
|
+
|
|
158
|
+
Never hand a game back on reasoning. Run the ladder:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
bunx incanto verify # loads · plays · feels · agrees · draws · says
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- **`loads`** — the scene is legal and its assets resolve. Warnings print under
|
|
165
|
+
it with `!`; a scene that "renders black" says so here.
|
|
166
|
+
- **`plays`** — 8 seeded runs. `error`, `fell` and `stuck` are DEFECTS and fail
|
|
167
|
+
the rung; `won`, `lost` and `unfinished` are gameplay. A random bot cannot
|
|
168
|
+
finish a quest, and that is reported as unmeasured, not failed.
|
|
169
|
+
- **`feels`** — the sounds and effects the scene declares, against what actually
|
|
170
|
+
fired. A game whose feedback is wired and never triggered plays perfectly and
|
|
171
|
+
feels dead.
|
|
172
|
+
- **`draws` / `says`** — need a dev server with the page open. Unmeasured is not
|
|
173
|
+
failed.
|
|
174
|
+
|
|
175
|
+
Then the two that answer questions the ladder cannot:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
bunx incanto-playtest src/game.scene.json --behaviors src/behaviors.ts
|
|
179
|
+
bunx incanto-feel src/game.scene.json --behaviors src/behaviors.ts
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`playtest` gives you a difficulty read (`won 4/20, lost 9/20, 9.7 hits per run`)
|
|
183
|
+
that no amount of staring at JSON will. `feel` measures your controls by probing
|
|
184
|
+
them — and prints the **held** jump apex next to the tapped one, because with
|
|
185
|
+
`jumpCutMultiplier` those differ by 4× and the held number is the one your level
|
|
186
|
+
geometry has to match.
|
|
187
|
+
|
|
188
|
+
Full detail: `incanto-verifying-your-game.md`, `incanto-playtesting.md`,
|
|
189
|
+
`incanto-game-feel.md`.
|
|
190
|
+
|
|
191
|
+
### Write the harness, not just the checks
|
|
192
|
+
|
|
193
|
+
`verify.ts` in the starter drives the game with `runScript` and asserts what
|
|
194
|
+
happened. Extend it as you build; it is the only thing that will catch a
|
|
195
|
+
regression you cannot see.
|
|
196
|
+
|
|
197
|
+
**Assert the game's own physics, not your test's.** A harness that emits
|
|
198
|
+
`triggerEnter` by hand proves the handler answers an event and nothing about
|
|
199
|
+
whether the game produces one — that exact mistake hid a starter whose enemies
|
|
200
|
+
could not reach the player. Let the AI chase, and read the health.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 6. The sticky note
|
|
205
|
+
|
|
206
|
+
Things that cost real time, in the order you will meet them.
|
|
207
|
+
|
|
208
|
+
| when | the trap |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| wiring a score | `died` carries nothing; `addScore(n)` wants one → `NaN`. Wire `dealtDamage` from the killer. |
|
|
211
|
+
| enemies feel harmless | `repeatEvery` on the contact hitbox, or one hit is all you get. |
|
|
212
|
+
| enemies never arrive | a chaser cannot climb — `stepHeight`, and it loses to a large downward velocity you apply yourself. |
|
|
213
|
+
| sizing a level | use the **held** jump apex, not the tapped one. |
|
|
214
|
+
| a melee weapon | a body with `Health` on the root and a `Hit` child presents TWO colliders; one swing can deal damage twice. Give the weapon a `targetGroup`. |
|
|
215
|
+
| a HUD you cannot see | `UiText.setText()` fills a slot; the `text` prop keeps the authored line. Use `describeCapture` to read what a widget PAINTS. |
|
|
216
|
+
| framing a scene | `describeFraming(scene)` returns the report object; `framingText(report)` renders it. |
|
|
217
|
+
| spawned enemies | connections on a template do NOT clone. Put the wire on something that is not cloned, or emit from a behavior. |
|
|
218
|
+
| a scene that swaps scenes | `incanto-playtest` drives one scene; a mid-run swap is out of its reach. Script it in `verify.ts` instead. |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 7. Where to go next
|
|
223
|
+
|
|
224
|
+
| you want | read |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| the JSON format itself | `incanto-scene-json-authoring.md` |
|
|
227
|
+
| nodes, props, defaults | `incanto-node-reference.md` (generated — always current) |
|
|
228
|
+
| ready-made game logic | `incanto-gameplay-behaviors.md` |
|
|
229
|
+
| a 3D character that feels right | `incanto-3d-character.md` |
|
|
230
|
+
| terrain, water, trees, sky | `incanto-environment.md` |
|
|
231
|
+
| sound | `incanto-audio.md` |
|
|
232
|
+
| shake, flash, hit-stop, particles | `incanto-game-feel.md` |
|
|
233
|
+
| HUD, menus, inventory | `incanto-hud.md` |
|
|
234
|
+
| proving it works | `incanto-verifying-your-game.md` |
|
|
235
|
+
|
|
236
|
+
And when a game "works" but feels wrong, the answer is almost always in
|
|
237
|
+
`incanto-feel` output you have not run yet.
|
|
@@ -220,7 +220,8 @@
|
|
|
220
220
|
"props": {
|
|
221
221
|
"amount": 8,
|
|
222
222
|
"targetGroup": "player",
|
|
223
|
-
"oncePerTarget": false
|
|
223
|
+
"oncePerTarget": false,
|
|
224
|
+
"repeatEvery": 0.5
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
},
|
|
@@ -332,7 +333,8 @@
|
|
|
332
333
|
"props": {
|
|
333
334
|
"amount": 12,
|
|
334
335
|
"targetGroup": "player",
|
|
335
|
-
"oncePerTarget": false
|
|
336
|
+
"oncePerTarget": false,
|
|
337
|
+
"repeatEvery": 0.5
|
|
336
338
|
}
|
|
337
339
|
}
|
|
338
340
|
},
|
|
@@ -629,7 +629,8 @@
|
|
|
629
629
|
"props": {
|
|
630
630
|
"amount": 12,
|
|
631
631
|
"targetGroup": "player",
|
|
632
|
-
"oncePerTarget": false
|
|
632
|
+
"oncePerTarget": false,
|
|
633
|
+
"repeatEvery": 0.5
|
|
633
634
|
}
|
|
634
635
|
}
|
|
635
636
|
},
|
|
@@ -698,7 +699,7 @@
|
|
|
698
699
|
"name": "ResumeHint",
|
|
699
700
|
"type": "UiText",
|
|
700
701
|
"props": {
|
|
701
|
-
"text": "Esc to resume
|
|
702
|
+
"text": "Esc to resume \u00b7 R to restart",
|
|
702
703
|
"size": 13,
|
|
703
704
|
"color": "#c9d2dd"
|
|
704
705
|
}
|
|
@@ -180,4 +180,42 @@ const ok = (label: string, cond: boolean): void => {
|
|
|
180
180
|
ok('runScript reported no failures (LOSE path)', result.ok);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ---- can an enemy ACTUALLY kill you? ---------------------------------------
|
|
184
|
+
//
|
|
185
|
+
// The LOSE path above emits `triggerEnter` by hand 60 times, which proves
|
|
186
|
+
// DamageOnContact answers an entry event and nothing about whether the game
|
|
187
|
+
// produces those events. It does not: contact fires on ENTRY, so an enemy that
|
|
188
|
+
// closes and rests deals ONE hit unless its hitbox carries `repeatEvery`.
|
|
189
|
+
// Measured on a player parked against an enemy for six seconds, before that
|
|
190
|
+
// prop was added: hp 90. This asserts the drain, from the game's own physics
|
|
191
|
+
// and its own Chase AI — no synthetic signals.
|
|
192
|
+
{
|
|
193
|
+
let hp = 100;
|
|
194
|
+
let hits = 0;
|
|
195
|
+
const result = await runScript(sceneJson, {
|
|
196
|
+
durationMs: 20000,
|
|
197
|
+
seed: 5,
|
|
198
|
+
behaviors,
|
|
199
|
+
steps: [
|
|
200
|
+
{
|
|
201
|
+
atMs: 100,
|
|
202
|
+
label: 'stand still and let the enemies come',
|
|
203
|
+
do: (ctx) => {
|
|
204
|
+
ctx.getNode('/root/Player').on('damaged', () => hits++);
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
atMs: 19000,
|
|
209
|
+
label: 'read the health the contact actually took',
|
|
210
|
+
do: (ctx) => {
|
|
211
|
+
hp = (ctx.getNode('/root/Player').behavior as unknown as { current: number }).current;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
ok(`enemies that reach you keep hurting — ${hits} hits, hp ${hp}`, hits >= 3);
|
|
218
|
+
ok('runScript reported no failures (CONTACT path)', result.ok);
|
|
219
|
+
}
|
|
220
|
+
|
|
183
221
|
console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK — Vanguard plays end to end');
|