incanto 0.28.0 → 0.29.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 (58) hide show
  1. package/README.md +44 -0
  2. package/bin/incanto-editor.mjs +118 -1
  3. package/dist/2d.d.ts +38 -5
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +319 -15
  6. package/dist/3d.js +5 -5
  7. package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
  8. package/dist/{create-game-BRgWpNsa.js → create-game-CNKXGfpr.js} +270 -47
  9. package/dist/{create-game-71TJjW1T.js → create-game-CbuLWorm.js} +53 -14
  10. package/dist/debug.d.ts +51 -6
  11. package/dist/debug.js +242 -19
  12. package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
  13. package/dist/editor-switch-CAKlJMIY.js +161 -0
  14. package/dist/editor.d.ts +49 -0
  15. package/dist/editor.js +8439 -0
  16. package/dist/{gameplay-DEG-TP7D.js → gameplay-L05WgWd1.js} +207 -37
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +22 -3
  20. package/dist/index.js +2 -2
  21. package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
  22. package/dist/net.d.ts +1 -1
  23. package/dist/net.js +2 -2
  24. package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
  25. package/dist/{physics-2d-DiVFFlH3.js → physics-2d-CCVTrKOd.js} +62 -2
  26. package/dist/{physics-3d--y5clE2j.js → physics-3d-BZZLtwJu.js} +439 -8
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
  30. package/dist/{register-6R75AC7-.js → register-C44aSduO.js} +5550 -5073
  31. package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
  32. package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
  33. package/dist/test.d.ts +35 -3
  34. package/dist/test.js +104 -10
  35. package/dist/vite.d.ts +53 -1
  36. package/dist/vite.js +94 -2
  37. package/editor/assets/{agent8-CojUfCXN.js → agent8-DEVkEa3d.js} +1 -1
  38. package/editor/assets/debug-zGAtpDF0.js +2 -0
  39. package/editor/assets/index-Bx4UtWYY.js +10586 -0
  40. package/editor/index.html +3 -157
  41. package/package.json +3 -2
  42. package/schemas/scene.schema.json +102 -4
  43. package/skills/incanto-3d-models.md +38 -0
  44. package/skills/incanto-assets.md +13 -0
  45. package/skills/incanto-building-3d-games.md +83 -6
  46. package/skills/incanto-editor.md +151 -0
  47. package/skills/incanto-environment.md +65 -1
  48. package/skills/incanto-node-reference.md +33 -1
  49. package/skills/incanto-physics-and-input.md +45 -6
  50. package/skills/incanto-verifying-your-game.md +51 -2
  51. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
  52. package/templates-app/beacon-isle-3d/package.json +1 -1
  53. package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
  54. package/templates-app/tps-3d/package.json +1 -1
  55. package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
  56. package/templates-app/village-quest-3d/package.json +1 -1
  57. package/editor/assets/index-D6RQgROR.js +0 -8330
  58. package/editor/assets/index-D8QvwvOm.css +0 -1
@@ -97,10 +97,19 @@ never be one node each. One instanced node draws them all at once:
97
97
  ```
98
98
 
99
99
  - Each row: `[x, y, z, yawDeg?, scale?]` — dense enough to GENERATE hundreds
100
- (pair with `terrain.heightAt` for y).
100
+ (pair with `snapToGround` or `terrain.heightAt` for y).
101
101
  - Same `mesh`/`size`/`material` surface as MeshInstance3D. Replace the whole
102
- `transforms` array to update. No per-instance colliders — add a few
103
- StaticBody3D boxes only where gameplay needs blocking.
102
+ `transforms` array to update.
103
+ - **`collider` makes the whole field SOLID**: `"collider": {}` gives every row
104
+ its own static collider SHAPED LIKE THE MESH — a faceted `gem` stone collides
105
+ as that stone's own hull, not as a ball tucked inside it, so you can't clip
106
+ through the corners that stick out. Sized from `size` × that row's scale and
107
+ turned by its yaw. All of them hang off one fixed body, so a fifty-stone
108
+ scatter costs fifty shapes and no per-frame work. A boulder field the player
109
+ walks straight through is the tell that a world is scenery rather than a
110
+ place.
111
+ `{ "shape": "sphere", "radius": r }` / `{ "shape": "box", "size": [x,y,z] }`
112
+ opt out into a cheaper approximation when a rough blocker is all you want.
104
113
 
105
114
  ### `LoftMesh3D` (smooth swept hulls — vehicle bodies, canopies)
106
115
 
@@ -367,14 +376,82 @@ show it as the background (otherwise `background` color applies). Standard
367
376
  materials need this OR explicit lights to be visible. An explicit HDRI wins
368
377
  over the `sky`'s image-based ambience.
369
378
 
370
- ## Preloading with a progress bar
379
+ ## NEVER compute a Y against the terrain by hand
380
+
381
+ This is the single most repeated mistake in scene authoring, and it is not a
382
+ careless one: the number is right when you write it and wrong the moment
383
+ anything reshapes the ground under it — a river carving its bed, a basin, a new
384
+ seed, a generator that mirrors a carve slightly differently. Boulders in
385
+ mid-air, a player buried to the waist, bridge posts hanging over a creek: all
386
+ the same bug.
387
+
388
+ **Say what you mean instead.** Every `Node3D` takes `snapToGround`:
389
+
390
+ ```json
391
+ { "name": "Boulder", "type": "MeshInstance3D",
392
+ "props": { "mesh": "gem", "size": [1.2, 0.8, 1.1],
393
+ "position": [12, 0, -30], // y is IGNORED
394
+ "snapToGround": 0.4 } } // half its height → it rests on the ground
395
+ ```
396
+
397
+ - `true` — put the node's origin on the ground.
398
+ - a number — the same, lifted that many metres (a prop whose origin is at its
399
+ middle wants half its height).
400
+ - `false` — "this hangs in the air ON PURPOSE" (a bridge deck, a lantern, a
401
+ cloud). The grounding check below skips these, so mark them.
402
+ - omitted — `y` is used exactly as authored.
403
+
404
+ Resolved at load, in the same frame the terrain is built, and it honours an
405
+ ancestor's yaw, so props under a turned rig land where they look.
406
+
407
+ **And check it.** `findFloatingProps` from `incanto/test` reports every prop
408
+ whose underside is off the ground, instanced fields row by row:
409
+
410
+ ```ts
411
+ import { findFloatingProps } from 'incanto/test';
412
+ const off = findFloatingProps(gameJson);
413
+ ok(`every prop meets the ground (${off.length})`, off.length === 0);
414
+ for (const o of off) console.error(` ${o.path}: ${o.gap > 0 ? 'floats' : 'buried'} ${o.gap} m`);
415
+ ```
416
+
417
+ Put it in your `verify.ts` next to `auditScene`. It costs nothing and it is the
418
+ difference between shipping a floating bridge and not.
419
+
420
+ ## The loading screen has to cover the WHOLE boot
421
+
422
+ Downloading the assets is the first half. Building a dressed scene and
423
+ compiling its GPU programs is the second, and on a big outdoor scene the second
424
+ is the longer one (measured on the creek template: 1.5 s of assets, then **7 s
425
+ of shader compile** — 49 programs). A bar that reaches 100% on the last
426
+ downloaded byte and then leaves the player looking at a blank canvas is worse
427
+ than no bar at all.
428
+
429
+ So run ONE bar across both, with `createGame3D`'s `onProgress`:
371
430
 
372
431
  ```ts
373
432
  import { assetUrls, preloadUrls } from 'incanto';
433
+
434
+ const progress = (fraction: number, label: string) => {
435
+ bar.style.width = `${Math.round(fraction * 100)}%`;
436
+ text.textContent = `${Math.round(fraction * 100)}% · ${label}`;
437
+ };
374
438
  await preloadUrls(assetUrls(sceneJson.assets), (done, total) =>
375
- bar.style.width = `${(done / total) * 100}%`);
439
+ progress((done / total) * 0.5, 'assets'));
440
+ const game = await createGame3D({
441
+ canvas, scene: sceneJson,
442
+ onProgress: (f, label) => progress(0.5 + f * 0.5, label),
443
+ });
444
+ document.querySelector('#loading')?.remove(); // now it is really ready
376
445
  ```
377
- Warms the HTTP cache before the engine starts; failures warn, never block.
446
+
447
+ `onProgress` reports `scene` → `physics` → `renderer` → `shaders` → `ready`,
448
+ and yields a frame at each step so the bar actually repaints (a callback fired
449
+ inside one synchronous block updates nothing a player can see). `createGame3D`
450
+ also draws its FIRST FRAME before it resolves, so the compile lands while your
451
+ screen is still up rather than after you remove it.
452
+
453
+ Preloading warms the HTTP cache before the engine starts; failures warn, never
454
+ block.
378
455
 
379
456
  ## Particles
380
457
 
@@ -110,6 +110,157 @@ Save activates only when the working scene differs from the original
110
110
  (`Ctrl/Cmd+S`); undo is `Ctrl/Cmd+Z`. Saving writes pretty-printed JSON to
111
111
  `--output` (default: the input file).
112
112
 
113
+ ## The agent8 asset library (📚)
114
+
115
+ Every field that takes a resource carries a **📚** button:
116
+
117
+ | Field | Writes |
118
+ |---|---|
119
+ | `ModelInstance3D.model` | scene asset (`model`) + the `$ref` |
120
+ | `ModelInstance3D.animation` | scene asset (`animation`) + the `$ref` — or type an embedded clip name |
121
+ | `Sprite2D.texture`, `AnimatedSprite2D.sheet` | scene asset (`texture` / `spritesheet`) + the `$ref` |
122
+ | `TileMap2D.texture` | scene asset (`texture`) + the `$ref` |
123
+ | `Sprite3D.texture`, `AnimatedSprite3D.sheet` | the URL (3D sprites take raw URLs) |
124
+ | material `map` / `normalMap`, `Tree3D.leafTexture`, `AudioPlayer.src` | the URL |
125
+ | ASSETS + form, and any asset's `url` | the URL (+ a suggested key) |
126
+
127
+ It opens the agent8 catalog that opens the agent8 catalog: shelves down the
128
+ left (**Everything** / 2D · sprite characters / 3D · characters, monsters,
129
+ objects, vehicles, weapons, polyhaven, textures), keyword search, and a detail
130
+ pane with the description, tags, format/size/mime and the URL. **Everything** is
131
+ the tagless shelf — the one that searches the whole catalog and the only way to
132
+ reach assets the tag tree does not name (sounds, one-off props).
133
+
134
+ The preview is the real thing: an image renders as an image, a **GLB is
135
+ rendered by the engine itself** (turntable, drag to spin, `targetHeight: 1` so
136
+ everything frames the same) — the same loader the game will use, so what you
137
+ see is what the scene gets.
138
+
139
+ Picking does the whole job, not half of it:
140
+
141
+ - a **URL field** (material map, leaf texture, audio src) gets the URL;
142
+ - a **`$assetKey` field** (`ModelInstance3D.model`, sprite texture/sheet) gets a
143
+ scene `assets{}` entry AND the ref that resolves to it — one action, no
144
+ separate "declare the asset" step. An entry with the same url is reused, so
145
+ browsing to the model you already have does not duplicate it;
146
+ - the ASSETS **+** form fills the url and suggests the key from the file name.
147
+
148
+ A sheet lands as a `spritesheet`, not a plain `texture` — the engine hard-fails
149
+ on the latter in an `AnimatedSprite2D.sheet` — with `frameWidth`/`frameHeight`
150
+ taken from the catalog's metadata when it carries them, and LEFT BLANK when it
151
+ does not (an invented frame size renders wrong art in silence; a blank field
152
+ asks you the question).
153
+
154
+ **Sprite configs are the good pick.** Many sprite characters ship a sibling
155
+ `<name>.json` (frame grid + named frame ranges) and the card wears an
156
+ `animated` badge. Picking one declares the sheet asset with its frame size AND
157
+ writes the node's `animations` + `autoplay` — one click gives you a sprite that
158
+ actually animates, instead of a still frame and a homework assignment.
159
+
160
+ What is on the shelves depends on the account behind your token. The public
161
+ catalog at the time of writing:
162
+
163
+ ```
164
+ 3d 5,325 characters 392 · objects 1,992 · textures 2,538 · polyhaven 2,723
165
+ animations 24 · shaders 14 · vehicles 12 · weapons 6 · monsters 6
166
+ 2d 436 projects 361 (ui, characters, sounds) · sprite_characters 54
167
+ building 9 · minecraft 7 · characterportraits 5
168
+ ```
169
+
170
+ Tags are lowercase — the catalog's own convention, and the query is
171
+ case-sensitive. If a shelf you expect is missing it is missing upstream: browse
172
+ from **Everything** and the tree shows what is actually there.
173
+
174
+ A field that knows where its art lives opens there — a model at
175
+ `3d / characters`, a clip at `3d / animations`, a material map at
176
+ `3d / textures`, a sprite sheet at `2d / sprite_characters` — and the rest start
177
+ at the root. It is a hint, not a cage: the breadcrumb walks anywhere. Either way
178
+ the kind filter applies, so a "model" field never offers you a .png.
179
+
180
+ **Access.** The catalog is per-account and its CORS list has no localhost in it,
181
+ so the page cannot call it directly — `incanto-editor` proxies it at
182
+ `GET /api/library`. Give the server a Verse8 token:
183
+
184
+ ```bash
185
+ bunx incanto-editor --token <v8 access token>
186
+ INCANTO_V8_TOKEN=<token> bunx incanto-editor # same thing
187
+ ```
188
+
189
+ With no token the 📚 panel asks for one and keeps it in the browser
190
+ (`localStorage`), which is the right answer for a shared machine — the token is
191
+ sent only to your own editor server, never written to disk by it, and forwarded
192
+ upstream as a bearer. `INCANTO_LIBRARY_URL` points the proxy somewhere else
193
+ (a mirror, or a stub in tests).
194
+
195
+ A game hosting the editor (the live play ⇄ edit switch) has no catalog of its
196
+ own — but its dev server can serve the same proxy, and then the 📚 works there
197
+ too:
198
+
199
+ ```ts
200
+ // vite.config.ts
201
+ import { incantoLibrary, incantoScenes } from 'incanto/vite';
202
+ export default defineConfig({ plugins: [incantoScenes(), incantoLibrary()] });
203
+ ```
204
+ ```ts
205
+ // main.ts
206
+ createGame3D({
207
+ ...,
208
+ debug: import.meta.env.VITE_INCANTO_DEBUG === '1',
209
+ editor: { library: true }, // "my dev server serves /api/library"
210
+ });
211
+ ```
212
+
213
+ `incantoLibrary()` is `apply: 'serve'` — dev only, nothing reaches a build, and
214
+ the token stays in the terminal that started the server (`INCANTO_V8_TOKEN`, or
215
+ `incantoLibrary({ token })`). `editor: { library: fn }` plugs in your own
216
+ transport instead. With neither, the buttons are simply absent.
217
+
218
+ ## Play, and the switch into the running game
219
+
220
+ **▶ play** means two different things and the editor asks which:
221
+
222
+ - **▶ preview here** — the scene simulates inside the editor viewport with the
223
+ game's TypeScript behaviors STRIPPED. Engine-native motion only (physics,
224
+ animation, water) — a quick look, not the game.
225
+ - **⤢ run as the game** — the page BECOMES the game: `createGame3D` boots the
226
+ working scene with your behaviors, your loading screen and the debug overlay.
227
+ This is the real thing, and it carries the edits you have not saved.
228
+
229
+ From that game, the ☰ debug menu's **✎ edit this scene** switches straight back
230
+ to the editor — same window, no reload, and the edit camera picks up the pose
231
+ the player was looking from. **✕ exit** in the editor returns to the game
232
+ without playing. The pair is one loop you can ride all session:
233
+
234
+ ```
235
+ game --(☰ debug ▸ ✎ edit this scene)--> editor
236
+ editor --(▶ play ▸ ⤢ run as the game)--> game
237
+ ```
238
+
239
+ What crosses is the AUTHORED scene, never the stepped tree: a running scene has
240
+ been moved by physics and spawned into by behaviors, and editing that would mean
241
+ editing wherever the simulation happened to be. Games hosting the editor
242
+ themselves (`createGame3D({ editor: … })`) get a **save** button only when they
243
+ pass `editor.save` — otherwise edits live as long as the session does.
244
+
245
+ ## Animating a model from the inspector
246
+
247
+ Select a `ModelInstance3D` and everything about what it is DOING sits in one
248
+ **animation** card: the clip, whether it loops, how fast, and where in the clip
249
+ it starts. The clip list is the model's own embedded clips plus the scene's
250
+ animation assets, and 📚 pulls a clip-only GLB from the agent8 library
251
+ (`3d / animations`) — picking one declares the asset and writes the ref.
252
+
253
+ - **speed** −3…3. `1` is as authored, `0` freezes the pose, negative plays
254
+ backwards. It retimes the clip already playing, so dragging it is smooth.
255
+ - **animationOffset** seconds into the clip. Changing it re-seeks live, which
256
+ makes it a scrubber as well as a stagger control — give copies different
257
+ offsets so a row of torches stops flickering in lockstep.
258
+ - The **upper-body layer** (a second clip on the upper body only) is folded away
259
+ until you use it.
260
+
261
+ The edit viewport keeps ambient visuals moving, so all of this animates while
262
+ you are editing — you see the change, not a still frame.
263
+
113
264
  ## Painting TileMap2D levels
114
265
 
115
266
  Select a `TileMap2D` node and the inspector grows a **🖌 paint tiles** tool:
@@ -520,6 +520,64 @@ automatic, no props.
520
520
  { "name": "Terrain", "type": "Terrain3D",
521
521
  "props": { "size": [200, 200], "maxHeight": 6, "seed": 1, "theme": "island" } }
522
522
  ] }
523
+ **`reflectivity` (0..1, null = the preset's own)** dials how much sky the
524
+ surface throws back at grazing angles. Turn it DOWN under a bright or overcast
525
+ sky: that sky's radiance runs far past 1, so a high ceiling paints the water
526
+ white — and a Water3D doing that beside a River3D (which paints an AUTHORED sky
527
+ palette and never blows out) reads as two unrelated materials meeting at a line.
528
+ The river template's plunge pool runs 0.06 for exactly that reason.
529
+
530
+ ### Condition a traced course before a river runs it
531
+
532
+ ```ts
533
+ import { smoothCourse, traceDownhillPath } from 'incanto/3d';
534
+ const path = smoothCourse(traceDownhillPath(heightAt, opts), widestWidth / 2);
535
+ ```
536
+
537
+ **Do this every time you feed `traceDownhillPath` into a River3D.** A steepest-
538
+ descent trace zigzags at the terrain's noise scale and can hook back within
539
+ metres of a reach it already ran. A ribbon on that course cannot be well formed:
540
+ tight corners fold its inner edge, and a hook stacks two sheets of water on the
541
+ same ground — which renders as translucent sheets of paper pasted over one
542
+ another, with no rendering fix available. `smoothCourse` relaxes the bends to
543
+ what the width can take, drops sub-width noise, and CUTS the course where it
544
+ folds back (a creek does not flow over its own reach; ending is the better
545
+ answer).
546
+
547
+ ### A river's course sets its maximum width
548
+
549
+ A `River3D` surface is a RIBBON swept along `path`, and a swept ribbon cannot be
550
+ wider than the bend it is taking — past that, its inner edge folds through
551
+ itself. The engine holds the INNER bank inside the local turn radius (the outer
552
+ bank keeps every metre the ground gives it, which is what a real bend looks
553
+ like: point bar inside, cut bank outside).
554
+
555
+ The failure this prevents is worth recognising, because it is the one visual bug
556
+ this node keeps producing: **bare channel floor beside the water — the creek
557
+ appears to stop in the middle of its own bed.** It means the ground is wetter
558
+ than the course can carry. River3D says so at load:
559
+
560
+ ```
561
+ [incanto] River3D 'Creek': the course is too tight for the water it holds —
562
+ 41 of 608 stations leave bare channel beside the ribbon (worst at 210 m: ground
563
+ wet to 11.3 m from the centre, a bend that can only carry 2.1 m). Fix by EASING
564
+ THE BENDS in `path` (fewer, straighter points), NARROWING `widths` there, or
565
+ placing standing water (a Water3D) if that reach is really a pool.
566
+ ```
567
+
568
+ Three rules that keep a river out of this entirely:
569
+
570
+ 1. **Widths ≤ ⅓ of the tightest bend.** A `traceDownhillPath` result zig-zags;
571
+ thin it or raise its `step` before handing it to a wide river.
572
+ 2. **A wide slow reach is still the river.** Don't put a Water3D pool at the
573
+ foot of a River3D — they are different shaders with different sky and foam
574
+ models, they cannot be tuned to match, and they meet along a visible seam.
575
+ Open the `widths` profile instead (`[2.4, 4.5, 7, 9]` is a spring→pool run).
576
+ 3. **Verify it, don't eyeball it.** `findRiverCoverageGaps(rings)` (exported from
577
+ `incanto/3d`) reports the reaches where the ribbon falls short, and a probe a
578
+ metre outside each bank — ground still BELOW the waterline out there — counts
579
+ bare channel headlessly. The river template gates on both.
580
+
523
581
  That snippet is the canonical PHYSICS recipe: the `heightfield` collider has
524
582
  no params of its own — it pulls the height grid from its Terrain3D CHILD, so
525
583
  visuals and collision can never drift apart. It is static-only (load-time
@@ -819,6 +877,12 @@ the scene root a cutout has nothing to stand in for, and the loader says so.
819
877
  | `size` | `[2, 1, 1]` | box extent in meters, centered on the node — which PART of the parent holds water back |
820
878
  | `enabled` | `true` | live toggle — a scuttled hull can start letting the sea in |
821
879
 
880
+ **A plunge pool is a `Water3D`, not a river.** A `River3D` is a current: a
881
+ ribbon fitted to the ground it runs over. Run one across a carved basin and it
882
+ lays a strip of water over the bowl with its edges hanging in mid-air — end the
883
+ creek at the lip and put a pool in the basin instead. Size a square surface to
884
+ `side = radius · √2` so its corners land on the rim rather than over the bank.
885
+
822
886
  **A cutout cannot punch a hole in open water**, because nothing is ever cut out
823
887
  of the water. The mask is the parent's own silhouette, so water can only be
824
888
  hidden behind geometry the player is already looking at. Overshoot the box and
@@ -979,7 +1043,7 @@ underneath decides everything else.
979
1043
  | `terrain` | `''` | drape target path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
980
1044
  | `carve` | `true` | cut the bed. The river trenches its own channel into the terrain it drapes on — see below |
981
1045
  | `flowForce` | `1` | how hard the current sweeps bodies downstream (0 = visual only) |
982
- | `spray` | `1` | mist at the foot of every waterfall the course turns out to contain — the node finds the drops and hangs a Particles3D plume on each (0 = off) |
1046
+ | `spray` | `1` | mist at the foot of every drop the course turns out to contain — the node finds them and hangs a Particles3D plume on each (0 = off) |
983
1047
  | `sunDirection` / `sunColor` / `sunIntensity` | sky / `#fff6e0` / `1` | glint (the environment sun wins while `sunDirection` is default) |
984
1048
 
985
1049
  **What the node derives for you**, all from `path` + the ground:
@@ -49,6 +49,7 @@ Signals: `animationFinished(name)`
49
49
  | `visible` | `true` | boolean |
50
50
  | `renderOrder` | `0` | number |
51
51
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
52
+ | `snapToGround` | `null` | null |
52
53
  | `texture` | `""` | string |
53
54
  | `size` | `[1,1]` | array |
54
55
  | `billboard` | `"y"` | one of: `y` `full` `none` |
@@ -94,6 +95,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
94
95
  | `visible` | `true` | boolean |
95
96
  | `renderOrder` | `0` | number |
96
97
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
98
+ | `snapToGround` | `null` | null |
97
99
  | `collider` | `{}` | object |
98
100
 
99
101
  Signals: `triggerEnter(other)` · `triggerExit(other)`
@@ -128,6 +130,7 @@ Signals: `finished`
128
130
  | `visible` | `true` | boolean |
129
131
  | `renderOrder` | `0` | number |
130
132
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
133
+ | `snapToGround` | `null` | null |
131
134
  | `mode` | `"screen"` | one of: `screen` `y` `none` |
132
135
 
133
136
  ## `BoneAttachment3D` — `incanto/3d`
@@ -141,6 +144,7 @@ Signals: `finished`
141
144
  | `visible` | `true` | boolean |
142
145
  | `renderOrder` | `0` | number |
143
146
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
147
+ | `snapToGround` | `null` | null |
144
148
  | `target` | `""` | string |
145
149
  | `bone` | `""` | string |
146
150
 
@@ -155,6 +159,7 @@ Signals: `finished`
155
159
  | `visible` | `true` | boolean |
156
160
  | `renderOrder` | `0` | number |
157
161
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
162
+ | `snapToGround` | `null` | null |
158
163
  | `target` | `""` | string |
159
164
  | `bone` | `"Head"` | string |
160
165
  | `lookAt` | `""` | string |
@@ -190,6 +195,7 @@ Signals: `finished`
190
195
  | `visible` | `true` | boolean |
191
196
  | `renderOrder` | `0` | number |
192
197
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
198
+ | `snapToGround` | `null` | null |
193
199
  | `fov` | `60` | number |
194
200
  | `near` | `0.1` | number |
195
201
  | `far` | `1000` | number |
@@ -224,9 +230,9 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
224
230
  | `visible` | `true` | boolean |
225
231
  | `renderOrder` | `0` | number |
226
232
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
233
+ | `snapToGround` | `true` | boolean |
227
234
  | `collider` | `{}` | object |
228
235
  | `velocity` | `[0,0,0]` | array |
229
- | `snapToGround` | `true` | boolean |
230
236
  | `slopeLimitDeg` | `45` | number |
231
237
 
232
238
  Signals: `triggerEnter(other)` · `triggerExit(other)`
@@ -252,6 +258,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
252
258
  | `visible` | `true` | boolean |
253
259
  | `renderOrder` | `0` | number |
254
260
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
261
+ | `snapToGround` | `null` | null |
255
262
  | `view` | `"free"` | one of: `free` `firstPerson` `quarter` `side` |
256
263
  | `camDistance` | `4` | number |
257
264
  | `eyeHeight` | `0.64` | number |
@@ -304,6 +311,7 @@ Signals: `movementStateChanged(state)`
304
311
  | `visible` | `true` | boolean |
305
312
  | `renderOrder` | `0` | number |
306
313
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
314
+ | `snapToGround` | `null` | null |
307
315
  | `color` | `"#ffffff"` | string |
308
316
  | `intensity` | `1` | number |
309
317
  | `castShadow` | `false` | boolean |
@@ -322,6 +330,7 @@ Signals: `movementStateChanged(state)`
322
330
  | `visible` | `true` | boolean |
323
331
  | `renderOrder` | `0` | number |
324
332
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
333
+ | `snapToGround` | `null` | null |
325
334
  | `density` | `"sparse"` | one of: `lush` `sparse` `none` |
326
335
  | `area` | `[20,20]` | array |
327
336
  | `seed` | `1` | number |
@@ -345,6 +354,7 @@ Signals: `movementStateChanged(state)`
345
354
  | `visible` | `true` | boolean |
346
355
  | `renderOrder` | `2` | number |
347
356
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
357
+ | `snapToGround` | `null` | null |
348
358
  | `kind` | `"grass"` | one of: `grass` `flowers` `reeds` |
349
359
  | `style` | `"mesh"` | one of: `blades` `simple` `mesh` `tufts` |
350
360
  | `tuftStyle` | `"grass"` | one of: `grass` `fern` |
@@ -385,12 +395,14 @@ Signals: `movementStateChanged(state)`
385
395
  | `visible` | `true` | boolean |
386
396
  | `renderOrder` | `0` | number |
387
397
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
398
+ | `snapToGround` | `null` | null |
388
399
  | `mesh` | `"box"` | one of: `box` `sphere` `capsule` `plane` `cylinder` `gem` |
389
400
  | `size` | `[1,1,1]` | array |
390
401
  | `material` | `{}` | object |
391
402
  | `transforms` | `[]` | array |
392
403
  | `castShadow` | `false` | boolean |
393
404
  | `receiveShadow` | `false` | boolean |
405
+ | `collider` | `null` | null |
394
406
 
395
407
  ## `Joint2D` — `incanto/2d`
396
408
 
@@ -422,6 +434,7 @@ Signals: `movementStateChanged(state)`
422
434
  | `visible` | `true` | boolean |
423
435
  | `renderOrder` | `0` | number |
424
436
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
437
+ | `snapToGround` | `null` | null |
425
438
  | `type` | `"spherical"` | one of: `fixed` `spherical` `rope` `spring` |
426
439
  | `target` | `""` | string |
427
440
  | `anchor` | `[0,0,0]` | array |
@@ -458,6 +471,7 @@ Signals: `movementStateChanged(state)`
458
471
  | `visible` | `true` | boolean |
459
472
  | `renderOrder` | `0` | number |
460
473
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
474
+ | `snapToGround` | `null` | null |
461
475
  | `sections` | `[]` | array |
462
476
  | `slices` | `24` | number |
463
477
  | `smooth` | `4` | number |
@@ -476,6 +490,7 @@ Signals: `movementStateChanged(state)`
476
490
  | `visible` | `true` | boolean |
477
491
  | `renderOrder` | `0` | number |
478
492
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
493
+ | `snapToGround` | `null` | null |
479
494
  | `mesh` | `"box"` | one of: `box` `sphere` `capsule` `plane` `cylinder` `gem` |
480
495
  | `size` | `[1,1,1]` | array |
481
496
  | `material` | `{}` | object |
@@ -493,11 +508,14 @@ Signals: `movementStateChanged(state)`
493
508
  | `visible` | `true` | boolean |
494
509
  | `renderOrder` | `0` | number |
495
510
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
511
+ | `snapToGround` | `null` | null |
496
512
  | `model` | `""` | string |
497
513
  | `animation` | `""` | string |
498
514
  | `targetHeight` | `0` | number |
499
515
  | `castShadow` | `false` | boolean |
500
516
  | `animationLoop` | `true` | boolean |
517
+ | `animationSpeed` | `1` | number |
518
+ | `animationOffset` | `0` | number |
501
519
  | `animationUpper` | `""` | string |
502
520
  | `animationUpperLoop` | `false` | boolean |
503
521
  | `upperBodyRoot` | `"Spine"` | string |
@@ -545,6 +563,7 @@ _No props (structural fields only)._
545
563
  | `visible` | `true` | boolean |
546
564
  | `renderOrder` | `0` | number |
547
565
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
566
+ | `snapToGround` | `null` | null |
548
567
 
549
568
  ## `OmniLight3D` — `incanto/3d`
550
569
 
@@ -557,6 +576,7 @@ _No props (structural fields only)._
557
576
  | `visible` | `true` | boolean |
558
577
  | `renderOrder` | `0` | number |
559
578
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
579
+ | `snapToGround` | `null` | null |
560
580
  | `color` | `"#ffffff"` | string |
561
581
  | `intensity` | `1` | number |
562
582
  | `range` | `0` | number |
@@ -606,6 +626,7 @@ Signals: `finished`
606
626
  | `visible` | `true` | boolean |
607
627
  | `renderOrder` | `0` | number |
608
628
  | `orderGroup` | `"effects"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
629
+ | `snapToGround` | `null` | null |
609
630
  | `preset` | `"custom"` | one of: `custom` `fire` `smoke` `sparks` `fireworks` `explosion` `flash` `lightning` `rain` `snow` `magic` |
610
631
  | `emitting` | `true` | boolean |
611
632
  | `rate` | `40` | number |
@@ -662,6 +683,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
662
683
  | `visible` | `true` | boolean |
663
684
  | `renderOrder` | `0` | number |
664
685
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
686
+ | `snapToGround` | `null` | null |
665
687
  | `collider` | `{}` | object |
666
688
  | `mass` | `1` | number |
667
689
  | `gravityScale` | `1` | number |
@@ -683,6 +705,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
683
705
  | `visible` | `true` | boolean |
684
706
  | `renderOrder` | `0` | number |
685
707
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
708
+ | `snapToGround` | `null` | null |
686
709
  | `path` | `[]` | array |
687
710
  | `width` | `6` | number |
688
711
  | `widths` | `[]` | array |
@@ -730,6 +753,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
730
753
  | `visible` | `true` | boolean |
731
754
  | `renderOrder` | `0` | number |
732
755
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
756
+ | `snapToGround` | `null` | null |
733
757
  | `texture` | `""` | string |
734
758
  | `size` | `[1,1]` | array |
735
759
  | `billboard` | `"y"` | one of: `y` `full` `none` |
@@ -768,6 +792,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
768
792
  | `visible` | `true` | boolean |
769
793
  | `renderOrder` | `0` | number |
770
794
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
795
+ | `snapToGround` | `null` | null |
771
796
  | `collider` | `{}` | object |
772
797
 
773
798
  Signals: `triggerEnter(other)` · `triggerExit(other)`
@@ -783,6 +808,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
783
808
  | `visible` | `true` | boolean |
784
809
  | `renderOrder` | `0` | number |
785
810
  | `orderGroup` | `"terrain"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
811
+ | `snapToGround` | `null` | null |
786
812
  | `size` | `[200,200]` | array |
787
813
  | `maxHeight` | `28` | number |
788
814
  | `seed` | `1` | number |
@@ -838,6 +864,7 @@ Signals: `timeout`
838
864
  | `visible` | `true` | boolean |
839
865
  | `renderOrder` | `0` | number |
840
866
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
867
+ | `snapToGround` | `null` | null |
841
868
  | `width` | `0.3` | number |
842
869
  | `color` | `"#ffffff"` | string |
843
870
  | `opacity` | `0.8` | number |
@@ -857,6 +884,7 @@ Signals: `timeout`
857
884
  | `visible` | `true` | boolean |
858
885
  | `renderOrder` | `0` | number |
859
886
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
887
+ | `snapToGround` | `null` | null |
860
888
  | `tier` | `"medium"` | one of: `simple` `medium` `high` |
861
889
  | `type` | `"conifer"` | one of: `conifer` `broadleaf` `dead` `bush` |
862
890
  | `seed` | `1` | number |
@@ -952,6 +980,7 @@ Signals: `lineShown` · `choiceMade` · `dialogueFinished`
952
980
  | `visible` | `true` | boolean |
953
981
  | `renderOrder` | `0` | number |
954
982
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
983
+ | `snapToGround` | `null` | null |
955
984
  | `voxels` | `[]` | array |
956
985
 
957
986
  Signals: `blocksChanged`
@@ -967,6 +996,7 @@ Signals: `blocksChanged`
967
996
  | `visible` | `true` | boolean |
968
997
  | `renderOrder` | `1` | number |
969
998
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
999
+ | `snapToGround` | `null` | null |
970
1000
  | `size` | `[40,40]` | array |
971
1001
  | `color` | `"#2a6fbe"` | string |
972
1002
  | `opacity` | `0.8` | number |
@@ -980,6 +1010,7 @@ Signals: `blocksChanged`
980
1010
  | `mirrorInterval` | `33` | number |
981
1011
  | `swellSteepness` | `0.35` | number |
982
1012
  | `foam` | `true` | boolean |
1013
+ | `reflectivity` | `null` | null |
983
1014
  | `interaction` | `true` | boolean |
984
1015
  | `splash` | `true` | boolean |
985
1016
  | `sunDirection` | `[0.5,0.8,0.3]` | array |
@@ -1010,6 +1041,7 @@ Signals: `entered(body)` · `exited(body)`
1010
1041
  | `visible` | `true` | boolean |
1011
1042
  | `renderOrder` | `0` | number |
1012
1043
  | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
1044
+ | `snapToGround` | `null` | null |
1013
1045
  | `size` | `[2,1,1]` | array |
1014
1046
  | `enabled` | `true` | boolean |
1015
1047
 
@@ -30,11 +30,43 @@ independent; enable the one matching your scene.
30
30
 
31
31
  2D collider shapes: `{"shape":"rect","size":[w,h]}` · `{"shape":"circle","radius":r}` ·
32
32
  `{"shape":"capsule","radius":r,"height":h}`.
33
- 3D: `{"shape":"box","size":[x,y,z]}` · `{"shape":"sphere","radius":r}` · `{"shape":"capsule",...}` ·
34
- `{"shape":"trimesh","vertices":[...],"indices":[...]}` · `{"shape":"heightfield"}` (STATIC-only;
35
- reads the grid from a `Terrain3D` child — see the incanto-environment skill).
33
+ 3D: `{"shape":"auto"}` · `{"shape":"box","size":[x,y,z]}` · `{"shape":"sphere","radius":r}` ·
34
+ `{"shape":"capsule",...}` · `{"shape":"trimesh","vertices":[...],"indices":[...]}` ·
35
+ `{"shape":"heightfield"}` (STATIC-only; reads the grid from a `Terrain3D` child — see the
36
+ incanto-environment skill).
36
37
  Malformed colliders are hard `BAD_FORMAT` errors at enable time.
37
38
 
39
+ **`{"shape":"auto"}` is the one to reach for on props.** It shapes the collider like the
40
+ `MeshInstance3D` the body CARRIES — box→cuboid, sphere→ball, cylinder/capsule→their own
41
+ shapes, `gem`→a convex hull of its real vertices — so a blocker can never disagree with what
42
+ the player sees. Hand-typed extents drift: a box "around" a bridge ends up wider than the
43
+ bridge and you stop in mid-air; a sphere "for" a stone ends up inside it and you clip through
44
+ the corners. Give the body its mesh as a CHILD and let the shape follow:
45
+
46
+ ```json
47
+ { "name": "DeckBody", "type": "StaticBody3D",
48
+ "props": { "position": [0, 3.2, -8], "rotation": [0, 34, 0], "collider": { "shape": "auto" } },
49
+ "children": [ { "name": "Deck", "type": "MeshInstance3D",
50
+ "props": { "mesh": "box", "size": [14, 0.22, 2.2] } } ] }
51
+ ```
52
+
53
+ Bodies ignore an ANCESTOR's rotation, so a yawed structure puts each body at top level with
54
+ its own `rotation` — the child mesh then turns with it, visual and collider together.
55
+
56
+ **`auto` works on a `ModelInstance3D` (GLB/glTF/VRM) too** — the shape comes from the model's
57
+ real triangles, baked through their own transforms, so an offset or `targetHeight`-fitted model
58
+ collides where it is DRAWN:
59
+
60
+ - on a **static** body (`StaticBody3D`, `Area3D`) → a triangle mesh: the model keeps its holes,
61
+ so you walk THROUGH an archway instead of into it. Level art can be dense; a collider over
62
+ ~20k triangles logs a note suggesting a low-poly collision model.
63
+ - on a **moving** body (`RigidBody3D`, `CharacterBody3D`) → the convex hull. Rapier's triangle
64
+ meshes are hollow shells; a dynamic one falls through the world.
65
+
66
+ The body simply does not exist until the model has downloaded — nothing is invented in the
67
+ meantime — and it appears the step the asset lands. A body is ONE shape, so `auto` fits one
68
+ mesh or one model and warns when the body carries more.
69
+
38
70
  - **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider`.
39
71
  - **`RigidBody2D/3D`** — simulated. Props: `collider`, `mass 1`, `gravityScale 1`,
40
72
  `fixedRotation false`, `friction 0.5`, `restitution 0`, `linearVelocity` (write to launch,
@@ -200,9 +232,16 @@ change scan was removed for performance).
200
232
  ## Debug drawing
201
233
 
202
234
  `physics.debugDraw = true` (the instance `enablePhysics2D/3D` returns) renders
203
- every collider as light-green wireframe lines IN THE GAME VIEW — Rapier's own
204
- debug geometry, so what you see is exactly what the solver sees. Default OFF;
205
- toggle it live whenever physics feels wrong.
235
+ every collider as light-green wireframe lines IN THE GAME VIEW — the shape data
236
+ the solver holds, so what you see is what it collides with. Default OFF; toggle
237
+ it live whenever physics feels wrong.
238
+
239
+ `physics.debugScope = node` narrows those lines to ONE node: its own colliders,
240
+ its descendants', or the nearest body above it when it owns none (so a visual
241
+ mesh child shows the body it belongs to). Null again for the whole world. Without
242
+ it, a map with terrain is a wall of grid lines and the shape you came to check is
243
+ somewhere inside it. The debug overlay's **Colliders** menu item cycles
244
+ off → all → selected over exactly these two fields.
206
245
 
207
246
  ## Placement rules
208
247