incanto 0.32.0 → 0.34.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 +59 -2
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +8 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-D_jMpFh8.d.ts → behavior-CtPScDMo.d.ts} +149 -1
- package/dist/{create-game-bKHgHcsZ.js → create-game-CFNqoqOX.js} +13 -6
- package/dist/{create-game-gXI7PYl0.js → create-game-QTfghzwM.js} +11 -5
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-KRPtUtzl.js → duplicate-DlOvknDO.js} +1 -1
- package/dist/editor.js +35 -0
- package/dist/{environment-presets--DigHNg4.js → environment-presets-B6WiUsaO.js} +2 -2
- package/dist/{gameplay-BftxM_It.js → gameplay-BxFtmfSe.js} +90 -2
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +175 -23
- package/dist/index.js +5 -58
- package/dist/{loader-BlaRQGaA.js → loader-BwR0DxeM.js} +66 -0
- package/dist/{loader-BYBrqTxP.d.ts → loader-DmXfj6Uv.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{pathfinding-BwhqPD3i.d.ts → pathfinding-DRCe89SI.d.ts} +1 -1
- package/dist/{physics-2d-D9wquBvK.js → physics-2d-BuyAwsW5.js} +12 -2
- package/dist/{physics-3d-CnPygVGo.js → physics-3d-C8Kn_Nyb.js} +55 -9
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-tkR_8tWg.js → register-BLPC10Mj.js} +145 -8
- package/dist/{register-Dzkd6-os.js → register-G3edsjJ2.js} +752 -16
- package/dist/{register-CUY284Is.js → register-OodmuUMK.js} +2 -2
- package/dist/{test-WwRIlXsK.js → test-C4B5znap.js} +10 -10
- package/dist/test.d.ts +2 -2
- package/dist/test.js +1 -1
- package/dist/vite.js +1 -1
- package/editor/assets/{agent8-C5k1nTCH.js → agent8-BTODHtdM.js} +1 -1
- package/editor/assets/{debug-BT_0mjk-.js → debug-CJEz4EwC.js} +1 -1
- package/editor/assets/{index-gfyrByWw.js → index-BYCso8Bf.js} +90 -90
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +667 -11
- package/skills/incanto-audio.md +43 -0
- package/skills/incanto-building-2d-games.md +36 -0
- package/skills/incanto-hud.md +97 -0
- package/skills/incanto-node-reference.md +131 -3
- package/skills/incanto-physics-and-input.md +50 -0
- package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +40 -19
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/docs/project-3d-rules.md +41 -19
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +40 -19
- package/templates-app/village-quest-3d/package.json +1 -1
package/skills/incanto-audio.md
CHANGED
|
@@ -330,3 +330,46 @@ this.tree.engine.music.crossfadeTo('incanto/assets/audio/boss.mp3', 3);
|
|
|
330
330
|
- **Global volume / mute / settings** → `engine.audio.master/sfx/music/muted`.
|
|
331
331
|
- **Verifying headlessly** → audio is a no-op in the VM; assert `play()` doesn't
|
|
332
332
|
throw and check your gameplay/score logic instead (see incanto-verifying).
|
|
333
|
+
|
|
334
|
+
## Settings that survive a reload (`engine.settings`)
|
|
335
|
+
|
|
336
|
+
Every player's volume used to reset on every page load: `createSaveStore` was
|
|
337
|
+
good and had zero callers, and eight examples set `engine.audio.music = 0.5` at
|
|
338
|
+
boot without reading a saved value.
|
|
339
|
+
|
|
340
|
+
`createGame2D/3D` now binds them, so **a slider that writes `engine.audio.music`
|
|
341
|
+
has already saved it** — no extra call, which is the promise this skill was
|
|
342
|
+
already making.
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
engine.settings.get('quality'); // 'low' | 'medium' | 'high'
|
|
346
|
+
engine.settings.set('sensitivity', 1.4);
|
|
347
|
+
engine.settings.all(); // the whole options screen at once
|
|
348
|
+
engine.settings.reset();
|
|
349
|
+
engine.settings.changed.connect((k) => …); // k is the key that changed
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Stored keys: `master`, `sfx`, `music`, `muted`, `quality`, `sensitivity`,
|
|
353
|
+
`invertY`, `reduceMotion`. Setting a value that has not changed does NOT emit,
|
|
354
|
+
and a corrupt/hand-edited `quality` falls back to the default instead of
|
|
355
|
+
bricking the game.
|
|
356
|
+
|
|
357
|
+
### Quality tiers
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { qualityEnvironment, readDeviceHints, suggestQuality, setEnvironment3D } from 'incanto';
|
|
361
|
+
|
|
362
|
+
const tier = engine.settings.get('quality') ?? suggestQuality(readDeviceHints());
|
|
363
|
+
setEnvironment3D(engine, qualityEnvironment(tier));
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`qualityEnvironment(tier)` is an **environment patch**, not a second rendering
|
|
367
|
+
pipeline: `setEnvironment3D` already applies one live and validated. `low` turns
|
|
368
|
+
off shadows/bloom/post/clouds and pins pixelRatio to 1; `medium` keeps shadows
|
|
369
|
+
but makes them static (measured ~42% of a dense-forest frame); `high` is
|
|
370
|
+
everything at device pixel ratio.
|
|
371
|
+
|
|
372
|
+
`suggestQuality(readDeviceHints())` picks a STARTING point from cores, memory and
|
|
373
|
+
a coarse pointer — the options menu is the real answer, and a `UiSelect` bound to
|
|
374
|
+
`quality` is the whole options row (see `incanto-hud.md`).
|
|
375
|
+
|
|
@@ -293,3 +293,39 @@ behind it), what lights the scene, and which colliders intersect. Then read
|
|
|
293
293
|
`stats().errors` (something threw and got skipped) and `assetErrors()` (a model
|
|
294
294
|
404'd). All four are silent failures otherwise — the screen just looks wrong,
|
|
295
295
|
or empty, and nothing throws.
|
|
296
|
+
|
|
297
|
+
## Platformer game feel (CharacterController2D)
|
|
298
|
+
|
|
299
|
+
A jump that only fires while `isOnFloor()` is true feels BROKEN, and players do
|
|
300
|
+
not report it as "the coyote time is missing" — they report the game as
|
|
301
|
+
unresponsive. These are the constants `examples/platformer-2d` hand-rolls in a
|
|
302
|
+
541-line behavior, as props. **Every one defaults to off**, so an existing scene
|
|
303
|
+
behaves exactly as before.
|
|
304
|
+
|
|
305
|
+
| prop | default | what it buys |
|
|
306
|
+
| --- | --- | --- |
|
|
307
|
+
| `coyoteSeconds` | `0` | jump for this long AFTER walking off a ledge — the single biggest one |
|
|
308
|
+
| `jumpBufferSeconds` | `0` | a press just BEFORE landing still fires on touchdown |
|
|
309
|
+
| `jumpCutMultiplier` | `1` | release early → rise less (variable jump height) |
|
|
310
|
+
| `maxJumps` | `1` | `2` = double jump. Walking off a ledge spends the ground jump |
|
|
311
|
+
| `dashSpeed` / `dashSeconds` / `dashAction` | `0` / `0.15` / `dash` | a straight-line dash |
|
|
312
|
+
| `wallSlideSpeed` | `0` | capped fall while against a wall |
|
|
313
|
+
| `wallJumpImpulse` | `[0, 0]` | `[away, up]` push off a wall |
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
{ "name": "Controller", "type": "CharacterController2D",
|
|
317
|
+
"props": { "coyoteSeconds": 0.12, "jumpBufferSeconds": 0.15,
|
|
318
|
+
"jumpCutMultiplier": 0.45, "maxJumps": 2,
|
|
319
|
+
"wallSlideSpeed": 60, "wallJumpImpulse": [260, 380] } }
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**`movementStateChanged(state)`** fires on every change — `idle` · `run` ·
|
|
323
|
+
`jump` · `fall` · `wallSlide` · `dash` — so an `AnimatedSprite2D` follows the
|
|
324
|
+
character from scene JSON instead of a behavior polling velocities. The 3D
|
|
325
|
+
sibling has had this signal since it shipped.
|
|
326
|
+
|
|
327
|
+
Wall moves need to know there IS a wall: `CharacterBody2D` now answers
|
|
328
|
+
`isOnWall()`, `wallSide()` (-1 left / +1 right) and `isOnCeiling()` as well as
|
|
329
|
+
`isOnFloor()`. Before, the floor was the only surface a character could report,
|
|
330
|
+
which is why a wall jump was not awkward to write but impossible.
|
|
331
|
+
|
package/skills/incanto-hud.md
CHANGED
|
@@ -80,3 +80,100 @@ Typewriter reveal at `charsPerSecond` (0 = instant); clicking the box
|
|
|
80
80
|
reveals the line then advances; choice lines render buttons and wait for
|
|
81
81
|
`choose(i)`. Buttons opt into pointer events — the rest of the HUD stays
|
|
82
82
|
click-through.
|
|
83
|
+
|
|
84
|
+
## Menus, options and inventories (UiPanel + the value widgets)
|
|
85
|
+
|
|
86
|
+
The HUD widgets can SAY things. These arrange them and take a value back — which
|
|
87
|
+
is what a title screen, pause menu, options panel, shop and inventory grid are.
|
|
88
|
+
Before them every one of those was hand-rolled DOM, which drops the whole screen
|
|
89
|
+
out of scene JSON, out of the editor, out of `incanto-check` and out of every
|
|
90
|
+
headless check you have.
|
|
91
|
+
|
|
92
|
+
**`UiPanel` is the box.** Widgets normally mount into the HudLayer's anchor slot
|
|
93
|
+
no matter how the tree is shaped; anything under a panel mounts into the PANEL,
|
|
94
|
+
so structure in the tree becomes structure on screen. Panels nest.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "name": "Pause", "type": "UiPanel",
|
|
98
|
+
"props": { "anchor": "center", "layout": "column", "gap": 12, "padding": 20 },
|
|
99
|
+
"children": [
|
|
100
|
+
{ "name": "Title", "type": "UiText", "props": { "text": "PAUSED", "size": 28 } },
|
|
101
|
+
{ "name": "Volume", "type": "UiSlider", "props": { "label": "volume", "value": 0.8 } },
|
|
102
|
+
{ "name": "Invert", "type": "UiToggle", "props": { "label": "invert Y" } },
|
|
103
|
+
{ "name": "Quality", "type": "UiSelect", "props": { "options": "low,medium,high", "value": "high" } },
|
|
104
|
+
{ "name": "Resume", "type": "UiButton", "props": { "text": "Resume" } }
|
|
105
|
+
] }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| node | props | signal |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `UiPanel` | `layout` (column/row/**grid**), `columns`, `gap`, `padding`, `background`, `radius`, `width`, `height`, `border` | — |
|
|
111
|
+
| `UiImage` | `src` (url or `$assetKey`), `width`, `height`, `fit`, `tint`, `opacity` | — |
|
|
112
|
+
| `UiSlider` | `label`, `value`, `min`, `max`, `step`, `width`, `color` | `changed(value)` |
|
|
113
|
+
| `UiToggle` | `label`, `value` | `changed(bool)` |
|
|
114
|
+
| `UiSelect` | `label`, `options` (`"low,medium,high"`), `value` | `changed(value)` |
|
|
115
|
+
|
|
116
|
+
**An inventory is a grid panel**: `"layout": "grid", "columns": 5`, one child per
|
|
117
|
+
slot, each a small `UiPanel` holding a `UiImage` (`tint` greys out what you
|
|
118
|
+
cannot afford) — and `Clickable` is on the world node, not the widget; for a
|
|
119
|
+
widget use `UiButton`'s `pressed`.
|
|
120
|
+
|
|
121
|
+
Setting `.value` from a behavior updates the control and does **NOT** re-emit
|
|
122
|
+
`changed` — restoring a saved setting must not fire the handler that saved it.
|
|
123
|
+
|
|
124
|
+
## Playable on a controller (focus navigation)
|
|
125
|
+
|
|
126
|
+
A menu you can only click is not playable on a gamepad, and "add controller
|
|
127
|
+
support" was not something a JSON scene could express at all.
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{ "name": "Hud", "type": "HudLayer", "props": { "focusNavigation": true }, "children": [ … ] }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Arrow keys / d-pad move the focus between the **focusable** widgets under that
|
|
134
|
+
layer, `Enter` / `A` activates, and the focused one wears a ring.
|
|
135
|
+
`UiButton`/`UiSlider`/`UiToggle`/`UiSelect` are focusable by default;
|
|
136
|
+
`UiText`/`UiBar`/`UiImage`/`UiPanel` are not, so arrowing never lands on a label.
|
|
137
|
+
|
|
138
|
+
What activation MEANS is per widget: a button presses, a toggle flips (left/right
|
|
139
|
+
sets it explicitly, which reads better on a pad), a select walks its list, a
|
|
140
|
+
slider nudges by one `step`.
|
|
141
|
+
|
|
142
|
+
**It is OFF by default**, deliberately: a game whose HUD happens to contain a
|
|
143
|
+
button must not lose its arrow keys the moment one exists. Turn it on for the
|
|
144
|
+
screens that ARE menus and off again when play resumes —
|
|
145
|
+
`hud.focusNavigation = false`. `hud.focus(widget)` sets the starting item, since
|
|
146
|
+
opening a menu should land somewhere rather than nowhere.
|
|
147
|
+
|
|
148
|
+
The gamepad codes are the ones the engine already produces from a pad
|
|
149
|
+
(`Pad12`–`Pad15` d-pad, `Pad0` = A), so nothing extra is declared.
|
|
150
|
+
|
|
151
|
+
## Dragging things between slots (inventory)
|
|
152
|
+
|
|
153
|
+
An inventory is the one screen where "click it" is not enough, and every game
|
|
154
|
+
that wanted one dropped out of scene JSON to hand-roll pointer handlers. Two
|
|
155
|
+
props and four signals, on the widgets you already have — no new node type:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{ "name": "SlotA", "type": "UiPanel", "props": { "dropTarget": true }, "children": [
|
|
159
|
+
{ "name": "Potion", "type": "UiImage", "props": { "src": "$potion", "draggable": true } }
|
|
160
|
+
] },
|
|
161
|
+
{ "name": "SlotB", "type": "UiPanel", "props": { "dropTarget": true } }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| signal | on | args |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `dragStarted` | the dragged widget | itself |
|
|
167
|
+
| `droppedOn` | the dragged widget | the target it landed on |
|
|
168
|
+
| `dropped` | the drop target | the widget that landed |
|
|
169
|
+
| `dragCancelled` | the dragged widget | itself (landed on nothing) |
|
|
170
|
+
|
|
171
|
+
Both ends are told, because both usually have work to do: the item leaves its old
|
|
172
|
+
slot, the slot takes it. **Who owns the item is your game's business** — these
|
|
173
|
+
report the GESTURE, not a model, so an inventory that stacks, swaps or refuses is
|
|
174
|
+
your `connections` and a behavior, not a prop nobody could have guessed.
|
|
175
|
+
|
|
176
|
+
A drop on a CHILD of a slot counts as a drop on the slot (the cursor lands on the
|
|
177
|
+
icon inside it, which is the normal case), a drop on nothing cancels, and a
|
|
178
|
+
widget never drops onto itself.
|
|
179
|
+
|
|
@@ -246,6 +246,17 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
|
|
|
246
246
|
| `jumpHeight` | `64` | number |
|
|
247
247
|
| `moveAction` | `"move"` | string |
|
|
248
248
|
| `jumpAction` | `"jump"` | string |
|
|
249
|
+
| `coyoteSeconds` | `0` | number |
|
|
250
|
+
| `jumpBufferSeconds` | `0` | number |
|
|
251
|
+
| `jumpCutMultiplier` | `1` | number |
|
|
252
|
+
| `maxJumps` | `1` | number |
|
|
253
|
+
| `dashSpeed` | `0` | number |
|
|
254
|
+
| `dashSeconds` | `0.15` | number |
|
|
255
|
+
| `dashAction` | `"dash"` | string |
|
|
256
|
+
| `wallSlideSpeed` | `0` | number |
|
|
257
|
+
| `wallJumpImpulse` | `[0,0]` | array |
|
|
258
|
+
|
|
259
|
+
Signals: `movementStateChanged(state)`
|
|
249
260
|
|
|
250
261
|
## `CharacterController3D` — `incanto/3d`
|
|
251
262
|
|
|
@@ -383,6 +394,7 @@ Signals: `movementStateChanged(state)`
|
|
|
383
394
|
|---|---|---|
|
|
384
395
|
| `zIndex` | `100` | number |
|
|
385
396
|
| `visible` | `true` | boolean |
|
|
397
|
+
| `focusNavigation` | `false` | boolean |
|
|
386
398
|
|
|
387
399
|
## `InstancedMesh3D` — `incanto/3d`
|
|
388
400
|
|
|
@@ -912,10 +924,13 @@ Signals: `timeout`
|
|
|
912
924
|
|---|---|---|
|
|
913
925
|
| `anchor` | `"center"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
914
926
|
| `visible` | `true` | boolean |
|
|
927
|
+
| `focusable` | `false` | boolean |
|
|
928
|
+
| `draggable` | `false` | boolean |
|
|
929
|
+
| `dropTarget` | `false` | boolean |
|
|
915
930
|
| `size` | `42` | number |
|
|
916
931
|
| `seconds` | `2` | number |
|
|
917
932
|
|
|
918
|
-
Signals: `bannerShown`
|
|
933
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `bannerShown`
|
|
919
934
|
|
|
920
935
|
## `UiBar` — `incanto`
|
|
921
936
|
|
|
@@ -923,6 +938,9 @@ Signals: `bannerShown`
|
|
|
923
938
|
|---|---|---|
|
|
924
939
|
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
925
940
|
| `visible` | `true` | boolean |
|
|
941
|
+
| `focusable` | `false` | boolean |
|
|
942
|
+
| `draggable` | `false` | boolean |
|
|
943
|
+
| `dropTarget` | `false` | boolean |
|
|
926
944
|
| `value` | `100` | number |
|
|
927
945
|
| `max` | `100` | number |
|
|
928
946
|
| `width` | `180` | number |
|
|
@@ -933,19 +951,24 @@ Signals: `bannerShown`
|
|
|
933
951
|
| `background` | `"rgba(0,0,0,0.5)"` | string |
|
|
934
952
|
| `label` | `""` | string |
|
|
935
953
|
|
|
954
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
|
|
955
|
+
|
|
936
956
|
## `UiButton` — `incanto`
|
|
937
957
|
|
|
938
958
|
| Prop | Default | Kind |
|
|
939
959
|
|---|---|---|
|
|
940
960
|
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
941
961
|
| `visible` | `true` | boolean |
|
|
962
|
+
| `focusable` | `true` | boolean |
|
|
963
|
+
| `draggable` | `false` | boolean |
|
|
964
|
+
| `dropTarget` | `false` | boolean |
|
|
942
965
|
| `text` | `"OK"` | string |
|
|
943
966
|
| `size` | `16` | number |
|
|
944
967
|
| `color` | `"#ffffff"` | string |
|
|
945
968
|
| `background` | `"rgba(255,255,255,0.14)"` | string |
|
|
946
969
|
| `disabled` | `false` | boolean |
|
|
947
970
|
|
|
948
|
-
Signals: `pressed`
|
|
971
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `pressed`
|
|
949
972
|
|
|
950
973
|
## `UiDialogue` — `incanto`
|
|
951
974
|
|
|
@@ -953,10 +976,86 @@ Signals: `pressed`
|
|
|
953
976
|
|---|---|---|
|
|
954
977
|
| `anchor` | `"bottom"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
955
978
|
| `visible` | `true` | boolean |
|
|
979
|
+
| `focusable` | `false` | boolean |
|
|
980
|
+
| `draggable` | `false` | boolean |
|
|
981
|
+
| `dropTarget` | `false` | boolean |
|
|
956
982
|
| `charsPerSecond` | `40` | number |
|
|
957
983
|
| `width` | `520` | number |
|
|
958
984
|
|
|
959
|
-
Signals: `lineShown` · `choiceMade` · `dialogueFinished`
|
|
985
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `lineShown` · `choiceMade` · `dialogueFinished`
|
|
986
|
+
|
|
987
|
+
## `UiImage` — `incanto`
|
|
988
|
+
|
|
989
|
+
| Prop | Default | Kind |
|
|
990
|
+
|---|---|---|
|
|
991
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
992
|
+
| `visible` | `true` | boolean |
|
|
993
|
+
| `focusable` | `false` | boolean |
|
|
994
|
+
| `draggable` | `false` | boolean |
|
|
995
|
+
| `dropTarget` | `false` | boolean |
|
|
996
|
+
| `src` | `""` | string |
|
|
997
|
+
| `width` | `48` | number |
|
|
998
|
+
| `height` | `48` | number |
|
|
999
|
+
| `fit` | `"contain"` | one of: `contain` `cover` `fill` |
|
|
1000
|
+
| `tint` | `""` | string |
|
|
1001
|
+
| `opacity` | `1` | number |
|
|
1002
|
+
|
|
1003
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
|
|
1004
|
+
|
|
1005
|
+
## `UiPanel` — `incanto`
|
|
1006
|
+
|
|
1007
|
+
| Prop | Default | Kind |
|
|
1008
|
+
|---|---|---|
|
|
1009
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
1010
|
+
| `visible` | `true` | boolean |
|
|
1011
|
+
| `focusable` | `false` | boolean |
|
|
1012
|
+
| `draggable` | `false` | boolean |
|
|
1013
|
+
| `dropTarget` | `false` | boolean |
|
|
1014
|
+
| `layout` | `"column"` | one of: `column` `row` `grid` |
|
|
1015
|
+
| `columns` | `4` | number |
|
|
1016
|
+
| `gap` | `8` | number |
|
|
1017
|
+
| `padding` | `12` | number |
|
|
1018
|
+
| `background` | `"rgba(0,0,0,0.55)"` | string |
|
|
1019
|
+
| `radius` | `10` | number |
|
|
1020
|
+
| `width` | `0` | number |
|
|
1021
|
+
| `height` | `0` | number |
|
|
1022
|
+
| `border` | `""` | string |
|
|
1023
|
+
|
|
1024
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
|
|
1025
|
+
|
|
1026
|
+
## `UiSelect` — `incanto`
|
|
1027
|
+
|
|
1028
|
+
| Prop | Default | Kind |
|
|
1029
|
+
|---|---|---|
|
|
1030
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
1031
|
+
| `visible` | `true` | boolean |
|
|
1032
|
+
| `focusable` | `true` | boolean |
|
|
1033
|
+
| `draggable` | `false` | boolean |
|
|
1034
|
+
| `dropTarget` | `false` | boolean |
|
|
1035
|
+
| `label` | `""` | string |
|
|
1036
|
+
| `options` | `""` | string |
|
|
1037
|
+
| `value` | `""` | string |
|
|
1038
|
+
|
|
1039
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1040
|
+
|
|
1041
|
+
## `UiSlider` — `incanto`
|
|
1042
|
+
|
|
1043
|
+
| Prop | Default | Kind |
|
|
1044
|
+
|---|---|---|
|
|
1045
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
1046
|
+
| `visible` | `true` | boolean |
|
|
1047
|
+
| `focusable` | `true` | boolean |
|
|
1048
|
+
| `draggable` | `false` | boolean |
|
|
1049
|
+
| `dropTarget` | `false` | boolean |
|
|
1050
|
+
| `label` | `""` | string |
|
|
1051
|
+
| `value` | `0.5` | number |
|
|
1052
|
+
| `min` | `0` | number |
|
|
1053
|
+
| `max` | `1` | number |
|
|
1054
|
+
| `step` | `0.01` | number |
|
|
1055
|
+
| `width` | `180` | number |
|
|
1056
|
+
| `color` | `"#6ee7dc"` | string |
|
|
1057
|
+
|
|
1058
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
960
1059
|
|
|
961
1060
|
## `UiText` — `incanto`
|
|
962
1061
|
|
|
@@ -964,11 +1063,30 @@ Signals: `lineShown` · `choiceMade` · `dialogueFinished`
|
|
|
964
1063
|
|---|---|---|
|
|
965
1064
|
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
966
1065
|
| `visible` | `true` | boolean |
|
|
1066
|
+
| `focusable` | `false` | boolean |
|
|
1067
|
+
| `draggable` | `false` | boolean |
|
|
1068
|
+
| `dropTarget` | `false` | boolean |
|
|
967
1069
|
| `text` | `""` | string |
|
|
968
1070
|
| `size` | `16` | number |
|
|
969
1071
|
| `color` | `"#ffffff"` | string |
|
|
970
1072
|
| `shadow` | `true` | boolean |
|
|
971
1073
|
|
|
1074
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
|
|
1075
|
+
|
|
1076
|
+
## `UiToggle` — `incanto`
|
|
1077
|
+
|
|
1078
|
+
| Prop | Default | Kind |
|
|
1079
|
+
|---|---|---|
|
|
1080
|
+
| `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
|
|
1081
|
+
| `visible` | `true` | boolean |
|
|
1082
|
+
| `focusable` | `true` | boolean |
|
|
1083
|
+
| `draggable` | `false` | boolean |
|
|
1084
|
+
| `dropTarget` | `false` | boolean |
|
|
1085
|
+
| `label` | `""` | string |
|
|
1086
|
+
| `value` | `false` | boolean |
|
|
1087
|
+
|
|
1088
|
+
Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
|
|
1089
|
+
|
|
972
1090
|
## `VoxelGrid3D` — `incanto/3d`
|
|
973
1091
|
|
|
974
1092
|
| Prop | Default | Kind |
|
|
@@ -1084,6 +1202,16 @@ Signals: `submerged` · `surfaced`
|
|
|
1084
1202
|
|
|
1085
1203
|
Signals: `reachedTarget` · `lostTarget`
|
|
1086
1204
|
|
|
1205
|
+
### `Clickable`
|
|
1206
|
+
|
|
1207
|
+
| Prop | Default | Kind |
|
|
1208
|
+
|---|---|---|
|
|
1209
|
+
| `button` | `0` | number |
|
|
1210
|
+
| `maxDistance` | `0` | number |
|
|
1211
|
+
| `enabled` | `true` | boolean |
|
|
1212
|
+
|
|
1213
|
+
Signals: `clicked` · `hovered` · `unhovered`
|
|
1214
|
+
|
|
1087
1215
|
### `Collector`
|
|
1088
1216
|
|
|
1089
1217
|
| Prop | Default | Kind |
|
|
@@ -258,6 +258,56 @@ the tree says, without your scene falling over while you look at it. Use it for
|
|
|
258
258
|
any "show me the collision geometry" tool. Do NOT use it to freeze a running
|
|
259
259
|
game — that is `engine.timeScale = 0`, which keeps the world coherent.
|
|
260
260
|
|
|
261
|
+
## Moving platforms carry their riders (2D **and** 3D)
|
|
262
|
+
|
|
263
|
+
A `CharacterBody2D`/`CharacterBody3D` standing on a body that moves is dragged
|
|
264
|
+
along by however far that body moved — elevators, patrol platforms, conveyors,
|
|
265
|
+
rotating discs. It is what makes a floating-island game work.
|
|
266
|
+
|
|
267
|
+
Author it the obvious way: move the platform's `position` (a behavior, a
|
|
268
|
+
`PathFollow`, an `Oscillate`). Nothing else to declare.
|
|
269
|
+
|
|
270
|
+
The carry stops the moment the character is no longer grounded on it, so walking
|
|
271
|
+
off an edge or jumping is not "sticky", and it is vertical as well as horizontal
|
|
272
|
+
— an elevator lifts you.
|
|
273
|
+
|
|
274
|
+
## The mouse as gameplay input (click, hover)
|
|
275
|
+
|
|
276
|
+
`pointerDelta()` answers "how far did the mouse move" — the mouse-look question.
|
|
277
|
+
For match-3, tower defense, card games, point-and-click and RTS that is the
|
|
278
|
+
wrong question, and it used to be the only one the engine could answer.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
engine.input.pointerPosition(); // { x, y } in CANVAS pixels, or null
|
|
282
|
+
engine.input.mousePressed(0); // 0 left · 1 middle · 2 right
|
|
283
|
+
engine.input.mouseJustPressed(0); // and mouseJustReleased(0)
|
|
284
|
+
game.pick(x, y); // the node under that pixel, or null
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
A stretched canvas is handled: `pointerPosition()` scales client → canvas pixels,
|
|
288
|
+
so the coordinates are the ones `pick()` wants.
|
|
289
|
+
|
|
290
|
+
From scene JSON, the **`Clickable`** behavior needs no code at all:
|
|
291
|
+
|
|
292
|
+
```json
|
|
293
|
+
{ "name": "Tile", "type": "MeshInstance3D", "script": { "name": "Clickable" } }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| prop | default | |
|
|
297
|
+
| --- | --- | --- |
|
|
298
|
+
| `button` | `0` | 0 left · 1 middle · 2 right |
|
|
299
|
+
| `maxDistance` | `0` | ignore clicks further than this (0 = any) |
|
|
300
|
+
| `enabled` | `true` | stop responding without detaching |
|
|
301
|
+
|
|
302
|
+
Signals: **`clicked`**, **`hovered`**, **`unhovered`** — wire them in
|
|
303
|
+
`connections` like any other. `clicked` fires on RELEASE over the same node the
|
|
304
|
+
press started on (a drag that ends elsewhere is not a click, the way every
|
|
305
|
+
button on every platform behaves), and a hit on a CHILD counts as a hit on the
|
|
306
|
+
node — the raycast lands on the visual mesh, which is usually a child.
|
|
307
|
+
|
|
308
|
+
Headless there is no renderer and therefore no raycast: `Clickable` is inert
|
|
309
|
+
rather than wrong, and `engine.picker` is null.
|
|
310
|
+
|
|
261
311
|
## Placement rules
|
|
262
312
|
|
|
263
313
|
`CharacterController2D` MUST be a direct child of a `CharacterBody2D` — the
|
|
@@ -5,15 +5,17 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
|
|
|
5
5
|
manual, version-pinned to what is installed. Start with
|
|
6
6
|
`incanto-building-3d-games.md`, `incanto-3d-character.md`,
|
|
7
7
|
`incanto-gameplay-behaviors.md`, `incanto-environment.md` and
|
|
8
|
-
`incanto-
|
|
8
|
+
`incanto-hud.md` — this template composes them —
|
|
9
|
+
and FINISH with `incanto-verifying-your-game.md` (rule 11).
|
|
9
10
|
2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and
|
|
10
|
-
replication all live in `src
|
|
11
|
+
replication all live in `src/*.scene.json`. Add nodes there (or with
|
|
11
12
|
`npx incanto-editor`), NOT by constructing them ad-hoc in code.
|
|
12
13
|
3. 🧠 LOGIC IS BEHAVIORS: gameplay code = small TypeScript Behavior classes,
|
|
13
14
|
registered with `registerBehavior('Name', Class)` and attached in JSON via
|
|
14
15
|
`"script": { "name": "Name" }`. Reach for a BUILT-IN gameplay behavior
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
before writing your own — this template already uses
|
|
17
|
+
Health, DamageOnContact, Interactable, PathFollow, DayNight, and its only custom behaviors are
|
|
18
|
+
`IsleDirector` and `SwordStrike`.
|
|
17
19
|
4. 🔑 UIDS ARE GENERATED: every node uid comes from
|
|
18
20
|
`import { newUid } from 'incanto'`. NEVER invent readable uid strings.
|
|
19
21
|
5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference
|
|
@@ -23,21 +25,40 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
|
|
|
23
25
|
(Euler XYZ). Negative gravity falls.
|
|
24
26
|
7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "box"|"sphere"|"capsule", … }`
|
|
25
27
|
on a body node — never child shape nodes. Wrong shapes hard-fail at load.
|
|
26
|
-
8. 🖥️ THE HUD IS
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
8. 🖥️ THE HUD IS NODES: this template's HUD lives in the scene as a
|
|
29
|
+
`HudLayer` with `UiText`/`UiBar` children — the engine builds the DOM for
|
|
30
|
+
them and positions them by anchor. Edit the HUD in `*.scene.json` like any
|
|
31
|
+
other node. (A DOM-in-`index.html` HUD is the other valid shape, and what
|
|
32
|
+
`tps-3d` does; both are supported — do not mix them in one screen.)
|
|
33
|
+
9. 🐛 DEBUG WHEN UNSURE: `VITE_INCANTO_DEBUG=1 bun run dev` adds the ☰ debug
|
|
34
|
+
menu — Explorer/Inspector (click a node, see and edit its live props), Logs,
|
|
35
|
+
Stats and Colliders (every physics shape as a wireframe, drawn by the same
|
|
36
|
+
Rapier the game runs). There is NO `VITE_INCANTO_DEBUG=1` URL toggle: it was
|
|
37
|
+
removed as deploy-unsafe. Never ship the env flag on.
|
|
33
38
|
10. 🎬 DELTA DISCIPLINE: only write props that differ from defaults — the
|
|
34
39
|
loader treats unknown/garbage props as hard errors, which is your friend.
|
|
35
|
-
11. ✅ VERIFY LIKE A USER
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
11. ✅ VERIFY LIKE A USER, THEN READ THE FOUR SIGNALS. `bun run check` and
|
|
41
|
+
`bun run verify` are the start, not the end — you cannot SEE this game, and
|
|
42
|
+
each of these answers a failure that otherwise looks like nothing at all:
|
|
43
|
+
|
|
44
|
+
| read | it is not clean when |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `bunx incanto-check` | the scene will not load, or renders black (no light) |
|
|
47
|
+
| `stats().errors` | something threw and was skipped to keep the game alive |
|
|
48
|
+
| `assetErrors()` | a model or texture 404'd — the thing is simply not there |
|
|
49
|
+
| `framing` | the camera points the wrong way, or nothing is lit |
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
printf 'step 500\nframing\nquit\n' | bunx incanto-play src/game.scene.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`framing` reports what the camera SEES — on screen / off screen / behind it
|
|
56
|
+
— plus what lights the scene and which colliders intersect. `behind` looks
|
|
57
|
+
exactly like "my model did not load"; nothing else can tell you apart.
|
|
58
|
+
Then `bun run dev` and actually PLAY what you changed.
|
|
59
|
+
12. 🧯 A THROWN ERROR NO LONGER STOPS THE GAME: a behavior that throws is
|
|
60
|
+
reported once (node, script, phase, file:line) and SKIPPED — your script
|
|
61
|
+
stops, the node it is attached to keeps running, the game plays on. So a
|
|
62
|
+
game that looks fine can still be broken: read `stats().errors`, and
|
|
63
|
+
`engine.resumeErroredNodes()` after you fix it.
|
|
43
64
|
</userRequest>
|
|
@@ -5,15 +5,17 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
|
|
|
5
5
|
manual, version-pinned to what is installed. Start with
|
|
6
6
|
`incanto-building-3d-games.md`, `incanto-3d-character.md`,
|
|
7
7
|
`incanto-gameplay-behaviors.md`, `incanto-environment.md` and
|
|
8
|
-
`incanto-audio.md` — this template composes
|
|
8
|
+
`incanto-audio.md` — this template composes them —
|
|
9
|
+
and FINISH with `incanto-verifying-your-game.md` (rule 11).
|
|
9
10
|
2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and
|
|
10
|
-
replication all live in `src
|
|
11
|
+
replication all live in `src/*.scene.json`. Add nodes there (or with
|
|
11
12
|
`npx incanto-editor`), NOT by constructing them ad-hoc in code.
|
|
12
13
|
3. 🧠 LOGIC IS BEHAVIORS: gameplay code = small TypeScript Behavior classes,
|
|
13
14
|
registered with `registerBehavior('Name', Class)` and attached in JSON via
|
|
14
15
|
`"script": { "name": "Name" }`. Reach for a BUILT-IN gameplay behavior
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
before writing your own — this template already uses
|
|
17
|
+
Health, DamageOnContact, Chase, WaveSpawner, ScoreKeeper, and its only custom behaviors are
|
|
18
|
+
`Shoot` and `HudUpdater`.
|
|
17
19
|
4. 🔑 UIDS ARE GENERATED: every node uid comes from
|
|
18
20
|
`import { newUid } from 'incanto'`. NEVER invent readable uid strings.
|
|
19
21
|
5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference
|
|
@@ -23,21 +25,41 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
|
|
|
23
25
|
(Euler XYZ). Negative gravity falls.
|
|
24
26
|
7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "box"|"sphere"|"capsule", … }`
|
|
25
27
|
on a body node — never child shape nodes. Wrong shapes hard-fail at load.
|
|
26
|
-
8. 🖥️ THE HUD IS DOM
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
8. 🖥️ THE HUD IS DOM HERE: this template's HUD, crosshair and banner are
|
|
29
|
+
plain HTML in `index.html`, and `HudUpdater` writes the live numbers into
|
|
30
|
+
them (guarded, so headless runs are a no-op). That is a CHOICE, not a
|
|
31
|
+
limit — `HudLayer` + `UiText`/`UiBar` nodes work in 3D scenes too (see
|
|
32
|
+
`incanto-hud.md`, and the `beacon-isle-3d` template). Do not mix them in
|
|
33
|
+
one screen.
|
|
34
|
+
9. 🐛 DEBUG WHEN UNSURE: `VITE_INCANTO_DEBUG=1 bun run dev` adds the ☰ debug
|
|
35
|
+
menu — Explorer/Inspector (click a node, see and edit its live props), Logs,
|
|
36
|
+
Stats and Colliders (every physics shape as a wireframe, drawn by the same
|
|
37
|
+
Rapier the game runs). There is NO `VITE_INCANTO_DEBUG=1` URL toggle: it was
|
|
38
|
+
removed as deploy-unsafe. Never ship the env flag on.
|
|
33
39
|
10. 🎬 DELTA DISCIPLINE: only write props that differ from defaults — the
|
|
34
40
|
loader treats unknown/garbage props as hard errors, which is your friend.
|
|
35
|
-
11. ✅ VERIFY LIKE A USER
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
11. ✅ VERIFY LIKE A USER, THEN READ THE FOUR SIGNALS. `bun run check` and
|
|
42
|
+
`bun run verify` are the start, not the end — you cannot SEE this game, and
|
|
43
|
+
each of these answers a failure that otherwise looks like nothing at all:
|
|
44
|
+
|
|
45
|
+
| read | it is not clean when |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `bunx incanto-check` | the scene will not load, or renders black (no light) |
|
|
48
|
+
| `stats().errors` | something threw and was skipped to keep the game alive |
|
|
49
|
+
| `assetErrors()` | a model or texture 404'd — the thing is simply not there |
|
|
50
|
+
| `framing` | the camera points the wrong way, or nothing is lit |
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
printf 'step 500\nframing\nquit\n' | bunx incanto-play src/game.scene.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`framing` reports what the camera SEES — on screen / off screen / behind it
|
|
57
|
+
— plus what lights the scene and which colliders intersect. `behind` looks
|
|
58
|
+
exactly like "my model did not load"; nothing else can tell you apart.
|
|
59
|
+
Then `bun run dev` and actually PLAY what you changed.
|
|
60
|
+
12. 🧯 A THROWN ERROR NO LONGER STOPS THE GAME: a behavior that throws is
|
|
61
|
+
reported once (node, script, phase, file:line) and SKIPPED — your script
|
|
62
|
+
stops, the node it is attached to keeps running, the game plays on. So a
|
|
63
|
+
game that looks fine can still be broken: read `stats().errors`, and
|
|
64
|
+
`engine.resumeErroredNodes()` after you fix it.
|
|
43
65
|
</userRequest>
|