incanto 0.4.2 → 0.5.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 (43) hide show
  1. package/dist/2d.d.ts +74 -6
  2. package/dist/2d.js +4 -4
  3. package/dist/3d.d.ts +174 -16
  4. package/dist/3d.js +4 -4
  5. package/dist/{audio-player-DqUR3XFs.d.ts → audio-player-DkBqRTs4.d.ts} +1 -1
  6. package/dist/{behavior-BAQq7HGM.d.ts → behavior-CWhW3oa6.d.ts} +54 -0
  7. package/dist/{create-game-CZHROKcT.js → create-game-BFESHv1X.js} +37 -25
  8. package/dist/{create-game-CMS7DiPi.js → create-game-D8UvwXme.js} +231 -8
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DP2WPYom.js → duplicate-BEvGBtb_.js} +1 -1
  11. package/dist/{gameplay-Ccruc3Wd.js → gameplay-CDFgSG6z.js} +293 -24
  12. package/dist/gameplay.d.ts +113 -2
  13. package/dist/gameplay.js +2 -2
  14. package/dist/index.d.ts +139 -5
  15. package/dist/index.js +59 -6
  16. package/dist/{loader-CGs_G-r0.js → loader-B4OEXDZ8.js} +60 -0
  17. package/dist/{loader-Mo0KghCv.d.ts → loader-D2u0fVW2.d.ts} +1 -1
  18. package/dist/net.d.ts +1 -1
  19. package/dist/net.js +1 -1
  20. package/dist/{particle-sim-DYuSUxvK.js → particle-sim-CFkILGwh.js} +84 -3
  21. package/dist/{particle-sim-CbN4YUuH.d.ts → particle-sim-CwJ5rI_P.d.ts} +3 -0
  22. package/dist/{physics-2d-KuMWPTf6.js → physics-2d-KXn1N1jF.js} +95 -10
  23. package/dist/{physics-3d-CXOBOUKG.js → physics-3d-C58oQzTX.js} +121 -43
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
  27. package/dist/{register-DPEV9_9t.js → register-D71C4rDC.js} +76 -5
  28. package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
  29. package/dist/{register-02aSywx2.js → register-DvPHJdVj.js} +390 -59
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-DUVZGcuO.js → agent8-DruKhR22.js} +1 -1
  33. package/editor/assets/index-D2qufqUo.js +7417 -0
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +847 -133
  37. package/skills/incanto-3d-models.md +24 -1
  38. package/skills/incanto-building-3d-games.md +35 -3
  39. package/skills/incanto-gameplay-behaviors.md +60 -0
  40. package/skills/incanto-hud.md +58 -0
  41. package/skills/incanto-node-reference.md +113 -0
  42. package/skills/incanto-physics-and-input.md +47 -0
  43. package/editor/assets/index-DsM2Yp9F.js +0 -7365
@@ -53,7 +53,30 @@ scales so the bounding box stands that many units tall — composes with the nod
53
53
  reactive at runtime), `animation` (see below), `tint` (hex; `""`=off — multiplies into
54
54
  every material to RESKIN one shared GLB into many variants, e.g. base human → green
55
55
  zombie; clones materials per instance so other instances are untouched),
56
- `castShadow`/`receiveShadow`, plus the usual Node3D transform.
56
+ `metalness`/`roughness` (0..1 overrides applied to EVERY material; `-1`=keep authored —
57
+ low-poly kits usually ship fully rough, so `metalness: 0.6, roughness: 0.25` turns flat
58
+ paint into a glinting clear-coat under the scene's sky IBL; per-instance clones like
59
+ tint), `castShadow`/`receiveShadow`, plus the usual Node3D transform.
60
+
61
+ ## Animating named sub-nodes (wheels, turrets, doors) — `poseNode`
62
+
63
+ Many GLBs ship articulated parts as NAMED nodes (`bunx incanto-model <file>` lists them —
64
+ e.g. the Kenney vehicles have `wheel-front-left` … `wheel-back-right`). From a behavior,
65
+ rotate one per frame with `poseNode(name, [rxDeg, ryDeg, rzDeg])` (local Euler XYZ):
66
+
67
+ ```ts
68
+ // spin the wheels by travel + steer the fronts (Kenney car GLB)
69
+ const model = car.getNodesByName('CarModel')[0] as ModelInstance3D;
70
+ this.spin += this.speed * dt * 60; // degrees of roll
71
+ for (const w of ['wheel-back-left', 'wheel-back-right'])
72
+ model.poseNode(w, [this.spin, 0, 0]);
73
+ for (const w of ['wheel-front-left', 'wheel-front-right'])
74
+ model.poseNode(w, [this.spin, steerDeg, 0]);
75
+ ```
76
+
77
+ Poses are queued if the model hasn't finished loading, re-applied every sync, and
78
+ unknown names are ignored (no throw). This is intent-level articulation — for full
79
+ skeletal animation use `animation` clips instead.
57
80
 
58
81
  **`targetHeight` + skinned rigs (the implausible-scale warning).** Mixamo-style humanoid
59
82
  rigs hide their true size behind a ~0.01 armature scale + bone-driven vertex scaling, so
@@ -62,9 +62,34 @@ All 3D nodes extend `Node3D` and therefore have the transform props:
62
62
  |---|---|---|
63
63
  | `mesh` | `"box"` | `box \| sphere \| capsule \| plane \| cylinder \| gem` — `gem` is a faceted crystal (icosahedron); with `material.flatShading: true` + low `roughness` it reads as a sparkling jewel (spin/tumble it for the sparkle) |
64
64
  | `size` | `[1,1,1]` | box: extents · sphere/gem: radius = x · capsule: radius = x, height = y · plane: x·z ground (laid FLAT on XZ) · cylinder: radius = x, height = y |
65
- | `material` | `{}` | `{color: '#hex', metalness: 0..1, roughness: 0..1, opacity: 0..1, wireframe, flatShading, depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat: [u,v]}` — the object IS the material state: omitted keys reset to defaults (`#ffffff`, 0, 1, 1, false, false, depthTest/Write true). `opacity < 1` turns on transparency; `flatShading` gives faceted per-face glints (the gem sparkle). `depthTest: false` (+ a high `renderOrder`) makes a flat `plane`/`cylinder` an ALWAYS-ON-TOP ground decal — AoE telegraphs, selection/spawn rings — that never z-fights with or is hidden by bumpy terrain (`depthWrite: false` keeps it from occluding later effects). `map`/`normalMap` are texture URLs (lazy-loaded, headless-safe; map samples sRGB, normalMap linear), tiled `repeat` times across each face — box/plane UVs span 0..1 per face, so for worldspace density use `repeat: [length/tile, height/tile]` (e.g. a 6×2.5 m brick wall at one tile per 2 m → `[3, 1.25]`). `color` tints the map (near-white keeps the texture's own color). NB: under ACES a very bright `emissiveIntensity` blows out to white — keep it modest (~1–2) + a saturated color for a vivid GLOWING look. Unknown keys, malformed `repeat`, or `repeat` without a map hard-fail at load |
65
+ | `material` | `{}` | `{color: '#hex', metalness: 0..1, roughness: 0..1, opacity: 0..1, clearcoat: 0..1, clearcoatRoughness: 0..1, envMapIntensity: >=0, wireframe, flatShading, depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat: [u,v]}` — the object IS the material state: omitted keys reset to defaults (`#ffffff`, 0, 1, 1, false, false, depthTest/Write true). `opacity < 1` turns on transparency; `flatShading` gives faceted per-face glints (the gem sparkle). `depthTest: false` (+ a high `renderOrder`) makes a flat `plane`/`cylinder` an ALWAYS-ON-TOP ground decal — AoE telegraphs, selection/spawn rings — that never z-fights with or is hidden by bumpy terrain (`depthWrite: false` keeps it from occluding later effects). `map`/`normalMap` are texture URLs (lazy-loaded, headless-safe; map samples sRGB, normalMap linear), tiled `repeat` times across each face — box/plane UVs span 0..1 per face, so for worldspace density use `repeat: [length/tile, height/tile]` (e.g. a 6×2.5 m brick wall at one tile per 2 m → `[3, 1.25]`). `color` tints the map (near-white keeps the texture's own color). CAR PAINT: `clearcoat: 1` adds a glossy lacquer layer with its own reflection over a metallic base (`clearcoatRoughness` ~0.05-0.1 = mirror lacquer) — THE premium vehicle-paint look; `envMapIntensity` scales how strongly the surface mirrors the scene's sky/HDRI (glass windows want ~2, matte surfaces <1; needs a `sky`/`hdri` environment to reflect — at night with no sky there is nothing to mirror). NB: under ACES a very bright `emissiveIntensity` blows out to white — keep it modest (~1–2) + a saturated color for a vivid GLOWING look. Unknown keys, malformed `repeat`, or `repeat` without a map hard-fail at load |
66
66
  | `castShadow` / `receiveShadow` | `false` | |
67
67
 
68
+ ### `LoftMesh3D` (smooth swept hulls — vehicle bodies, canopies)
69
+
70
+ A SMOOTH curved surface swept through cross-sections — what box/capsule assemblies can
71
+ never give you. Superellipse sections are Catmull-Rom-interpolated along Z with per-vertex
72
+ normals, so highlights and sky reflections SWEEP across it like sheet metal. Use it for car
73
+ bodies, glass canopies, boat hulls, fuselages, submarines.
74
+
75
+ | Prop | Default | Notes |
76
+ |---|---|---|
77
+ | `sections` | `[]` | ≥2 stations, ascending `z`: `{z, width, height, y, corner?}` — `width`/`height` are the full cross-section extents, `y` is the section CENTRE height, `corner` 0 = boxy slab … 1 = full ellipse (default 0.7; the lower half always stays flatter — vehicle floors are flat). Malformed sections hard-fail at load |
78
+ | `slices` | `24` | vertices around each ring (8..64) |
79
+ | `smooth` | `4` | interpolated rings per section gap (1..16) |
80
+ | `material` | `{}` | same object as MeshInstance3D **minus textures** (`map`/`normalMap`/`repeat` hard-fail — a loft has no UVs) |
81
+ | `castShadow` / `receiveShadow` | `false` | |
82
+
83
+ Supercar recipe (hand-tuned against McLaren F1 proportions — see `examples/racing-3d`):
84
+ one hull loft (low rounded nose ~`corner 0.9` → wide fender-crown stations at the axles →
85
+ coke-bottle waist pinch → tall rear haunches → squared kamm tail `corner ~0.25`), plus a
86
+ second small loft as the GLASS bubble canopy (`metalness 1, roughness 0.03,
87
+ envMapIntensity ~1.7`), plus a dark interior loft inside it, with rectilinear
88
+ `MeshInstance3D` details (splitter, wing, light bars, mirrors) on top. Paint that stays
89
+ saturated in sunlight: `metalness ~0.45, roughness ~0.35, clearcoat 1,
90
+ clearcoatRoughness 0.06, envMapIntensity ~0.9` — high metalness washes the base colour
91
+ out into the sky reflection.
92
+
68
93
  ### `Sprite3D` / `AnimatedSprite3D` (2D billboard sprites — the 2.5D look)
69
94
  A 2D image drawn as a camera-facing quad inside the 3D world (Octopath / Don't
70
95
  Starve / classic MapleStory in a 3D scene). Unlit — the art shows as-is. The
@@ -75,7 +100,7 @@ billboard spin. `Sprite3D`:
75
100
  |---|---|---|
76
101
  | `texture` | `""` | image URL (sampled sRGB, tinted by `tint`); empty = hidden |
77
102
  | `size` | `[1,1]` | quad `[width, height]` in world METERS (not pixels) |
78
- | `billboard` | `"y"` | `"y"` upright — yaws to face the camera, stays vertical when you look down (characters, props) · `"full"` always faces the camera (items, FX) · `"none"` fixed to the node's own rotation |
103
+ | `billboard` | `"y"` | `"y"` upright — screen-aligned yaw: every sprite shares the camera heading, so off-center sprites never ROLL under a pitched camera (characters, props) · `"full"` always faces the camera (items, FX) · `"none"` fixed to the node's own rotation |
79
104
  | `anchor` | `[0.5,0]` | which point of the sprite sits at the node origin (0..1). `[0.5,0]` = bottom-center → the FEET stand on the ground; `[0.5,0.5]` = center |
80
105
  | `tint` | `"#ffffff"` | multiplies the texture (near-white keeps original colors) |
81
106
  | `opacity` | `1` | `< 1` turns on alpha blending |
@@ -117,7 +142,7 @@ number is a canvas-rendered texture on a child `Sprite3D`.)
117
142
 
118
143
  | Prop | Default | Notes |
119
144
  |---|---|---|
120
- | `mode` | `"screen"` | `"screen"` copies the camera's orientation wholesale (screen-aligned) — a child rectangle projects as an **upright rectangle anywhere on screen** · `"y"` stays vertical and only yaws toward the camera (standing content) · `"none"` billboarding off — the authored rotation applies again |
145
+ | `mode` | `"screen"` | `"screen"` copies the camera's orientation wholesale (screen-aligned) — a child rectangle projects as an **upright rectangle anywhere on screen** · `"y"` stays vertical with a screen-aligned yaw no off-center roll (standing content) · `"none"` billboarding off — the authored rotation applies again |
121
146
 
122
147
  - Use `"screen"` for flat UI shapes (bars, plates). A yaw-only look-at — the
123
148
  tempting hand-rolled version — visibly TILTS off-center shapes when the camera
@@ -251,6 +276,13 @@ demo into a commercial-looking outdoor scene:
251
276
  one extra fullscreen pass per frame — keep `Water3D` at `quality:"simple"`
252
277
  (zero re-renders) when pairing with clouds to hold 60fps. Absent = zero cost.
253
278
  Ideal for flight/sky scenes; see `examples/dogfight-3d`.
279
+ - `bloom`: a glow post pass — bright pixels bleed light, so emissive
280
+ materials (tail-lights, neon, lava, muzzle flashes) and sun glints actually
281
+ GLOW. `{ threshold?, strength? }`: `threshold` 0..1 post-tonemap brightness
282
+ above which pixels bloom (default 0.75 — keeps it to lights/glints; lower =
283
+ dreamier), `strength` how strongly the blurred glow is added back (default
284
+ 0.85; ~1.2 = neon night). One extra half-res blur pass per frame — cheap.
285
+ Caustics/clouds composites take precedence (pipelines don't stack yet).
254
286
  - `shadows`: `true` | `false` | `{ "mapSize": 1024|2048, "radius": n }`.
255
287
  Truthy makes the main DirectionalLight cast (PCF soft, terrain + trees +
256
288
  meshes cast/receive, mesh grass RECEIVES only). One static ortho box (±75 m
@@ -705,3 +705,63 @@ signal into a built-in's method through a `connection`).
705
705
  Generated, always-current prop/signal tables for every behavior live in
706
706
  `incanto-node-reference.md` (the "Gameplay behaviors" section).
707
707
  ```
708
+
709
+ ## Game feel ("juice")
710
+
711
+ ```ts
712
+ import { CameraShake, Cooldown, hitStop, screenFlash } from 'incanto/gameplay';
713
+ ```
714
+
715
+ - **Cooldown** — fire-rate gate, replaces hand-rolled clock math:
716
+ `private gun = new Cooldown(0.2);` then per update `this.gun.tick(dt);
717
+ if (firing && this.gun.tryUse()) shoot();`. `progress` (0..1) drives a
718
+ UiBar reload indicator; `reset()` for reload pickups.
719
+ - **Camera shake** — `FollowCamera` has it built in:
720
+ `(cam.behavior as FollowCamera).shake(8)` (2D pixels; use ~0.2 in 3D
721
+ meters). For cameras WITHOUT a follow script attach the standalone
722
+ `CameraShake` behavior — it composes with any other position writer and
723
+ returns the camera exactly to base.
724
+ - **screenFlash(color?, opacity?, seconds?)** — full-screen damage/pickup
725
+ flash (DOM, headless no-op).
726
+ - **hitStop(engine, seconds?)** — freezes `engine.timeScale` for REAL
727
+ seconds then restores; stacked calls extend. Sells melee impacts.
728
+ - **engine.timeScale** — 1 realtime, 0.5 slow motion, 0 pause: scales
729
+ variable AND fixed updates together (physics, timers, behaviors).
730
+
731
+ ## Game flow (win / lose / restart)
732
+
733
+ Attach `GameFlow` to the scene root (`"script": { "name": "GameFlow" }`),
734
+ declare a `restart` input action, and put a `%Banner` UiBanner in your HUD:
735
+
736
+ ```ts
737
+ const flow = this.node.getRoot().behavior as GameFlow;
738
+ flow.gameOver('YOU DIED'); // freezes engine.timeScale, sticky banner, waits for restart
739
+ flow.win('AREA CLEAR');
740
+ flow.pause(); flow.resume();
741
+ ```
742
+
743
+ Terminal states ignore further transitions; the restart action (or
744
+ `flow.restart()`) reloads the scene from its source JSON — fresh nodes,
745
+ physics, input. Listen to the `flowChanged(state)` signal for custom UI.
746
+ `restartScene(engine)` is exported standalone.
747
+
748
+ ## Boot loading overlay
749
+
750
+ ```ts
751
+ import { preloadSceneAssets } from 'incanto';
752
+ await preloadSceneAssets(sceneJson.assets); // black screen + bar + %, auto-removes
753
+ ```
754
+
755
+ Replaces the ~125-line hand-rolled loading screen; `createLoadingOverlay()`
756
+ gives you the raw overlay when you preload something else.
757
+
758
+ ## Persistence (high scores, unlocks, settings)
759
+
760
+ ```ts
761
+ import { createSaveStore } from 'incanto';
762
+ const save = createSaveStore('my-game'); // namespaced localStorage
763
+ save.set('highScore', Math.max(score, save.get('highScore', 0)));
764
+ ```
765
+
766
+ JSON round-tripped; headless/private-mode falls back to in-memory (never
767
+ throws). `clear()` wipes only your namespace.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: incanto-hud
3
+ description: Screen-space HUD widgets (HudLayer + UiText / UiBar / UiBanner) — health bars, score text, wave banners declared in scene JSON, no hand-rolled DOM or CSS. Use for any in-game UI overlay in 2D or 3D games.
4
+ ---
5
+
6
+ # HUD widgets
7
+
8
+ Declare your HUD in the scene JSON — no `index.html` markup, no CSS, no
9
+ `document.querySelector` in behaviors:
10
+
11
+ ```json
12
+ {
13
+ "name": "HUD", "type": "HudLayer",
14
+ "children": [
15
+ { "name": "Health", "type": "UiBar",
16
+ "props": { "anchor": "topLeft", "value": 100, "max": 100, "label": "HP" } },
17
+ { "name": "Score", "type": "UiText",
18
+ "props": { "anchor": "topRight", "text": "Score: 0", "size": 20 } },
19
+ { "name": "Banner", "type": "UiBanner" }
20
+ ]
21
+ }
22
+ ```
23
+
24
+ `HudLayer` is a fixed, pointer-transparent DOM overlay above the canvas — it
25
+ works identically over the 2D and 3D renderers and is a silent no-op in
26
+ headless tests (core nodes, no three.js). Widgets pick one of 9 anchors:
27
+ `topLeft top topRight left center right bottomLeft bottom bottomRight`.
28
+ Widgets stacked on the same anchor form a column.
29
+
30
+ ## Driving widgets from behaviors
31
+
32
+ Plain node access — same as everything else:
33
+
34
+ ```ts
35
+ import type { UiBanner, UiBar, UiText } from 'incanto';
36
+
37
+ const hp = this.node.getNode('%Health') as UiBar;
38
+ hp.value = health.current; // fill animates; turns red below 30%
39
+
40
+ (this.node.getNode('%Score') as UiText).text = `Score: ${score}`;
41
+
42
+ const banner = this.node.getNode('%Banner') as UiBanner;
43
+ banner.show('WAVE 2', { color: '#f87171', seconds: 2 }); // queued, fades
44
+ banner.show('YOU DIED', { color: '#ef4444', seconds: 0 }); // sticky until next show()
45
+ ```
46
+
47
+ ## Widget reference
48
+
49
+ - **UiText** — `text`, `size` (px), `color`, `shadow` (readability outline).
50
+ - **UiBar** — `value`/`max` (ratio clamped 0..1), `width`/`height`, `color`,
51
+ `lowColor` + `lowThreshold` (default 0.3), `background`, `label` caption.
52
+ - **UiBanner** — `show(text, {color, seconds})` queues center-screen
53
+ announcements with fade in/out; `seconds: 0` = sticky; `clear()` empties
54
+ the queue; signal `bannerShown(text)`. Props: `size` (font px), `seconds`
55
+ (default duration).
56
+
57
+ Pair with the `Health` / `ScoreKeeper` gameplay behaviors: listen to their
58
+ signals and write the widget props — that is the whole HUD wiring.
@@ -304,6 +304,47 @@ Signals: `movementStateChanged(state)`
304
304
  | `drape` | `false` | boolean |
305
305
  | `terrain` | `""` | string |
306
306
 
307
+ ## `HudLayer` — `incanto`
308
+
309
+ | Prop | Default | Kind |
310
+ |---|---|---|
311
+ | `zIndex` | `100` | number |
312
+ | `visible` | `true` | boolean |
313
+
314
+ ## `Joint2D` — `incanto/2d`
315
+
316
+ | Prop | Default | Kind |
317
+ |---|---|---|
318
+ | `position` | `[0,0]` | array |
319
+ | `rotation` | `0` | number |
320
+ | `scale` | `[1,1]` | array |
321
+ | `renderOrder` | `0` | number |
322
+ | `visible` | `true` | boolean |
323
+ | `type` | `"revolute"` | one of: `fixed` `revolute` `rope` `spring` |
324
+ | `target` | `""` | string |
325
+ | `anchor` | `[0,0]` | array |
326
+ | `targetAnchor` | `[0,0]` | array |
327
+ | `length` | `0` | number |
328
+ | `stiffness` | `50` | number |
329
+ | `damping` | `5` | number |
330
+
331
+ ## `Joint3D` — `incanto/3d`
332
+
333
+ | Prop | Default | Kind |
334
+ |---|---|---|
335
+ | `position` | `[0,0,0]` | array |
336
+ | `rotation` | `[0,0,0]` | array |
337
+ | `scale` | `[1,1,1]` | array |
338
+ | `visible` | `true` | boolean |
339
+ | `renderOrder` | `0` | number |
340
+ | `type` | `"spherical"` | one of: `fixed` `spherical` `rope` `spring` |
341
+ | `target` | `""` | string |
342
+ | `anchor` | `[0,0,0]` | array |
343
+ | `targetAnchor` | `[0,0,0]` | array |
344
+ | `length` | `0` | number |
345
+ | `stiffness` | `50` | number |
346
+ | `damping` | `5` | number |
347
+
307
348
  ## `Label` — `incanto/2d`
308
349
 
309
350
  | Prop | Default | Kind |
@@ -319,6 +360,22 @@ Signals: `movementStateChanged(state)`
319
360
  | `font` | `"monospace"` | string |
320
361
  | `align` | `"left"` | string |
321
362
 
363
+ ## `LoftMesh3D` — `incanto/3d`
364
+
365
+ | Prop | Default | Kind |
366
+ |---|---|---|
367
+ | `position` | `[0,0,0]` | array |
368
+ | `rotation` | `[0,0,0]` | array |
369
+ | `scale` | `[1,1,1]` | array |
370
+ | `visible` | `true` | boolean |
371
+ | `renderOrder` | `0` | number |
372
+ | `sections` | `[]` | array |
373
+ | `slices` | `24` | number |
374
+ | `smooth` | `4` | number |
375
+ | `material` | `{}` | object |
376
+ | `castShadow` | `false` | boolean |
377
+ | `receiveShadow` | `false` | boolean |
378
+
322
379
  ## `MeshInstance3D` — `incanto/3d`
323
380
 
324
381
  | Prop | Default | Kind |
@@ -350,6 +407,8 @@ Signals: `movementStateChanged(state)`
350
407
  | `animationLoop` | `true` | boolean |
351
408
  | `receiveShadow` | `false` | boolean |
352
409
  | `tint` | `""` | string |
410
+ | `metalness` | `-1` | number |
411
+ | `roughness` | `-1` | number |
353
412
 
354
413
  Signals: `animationFinished(name)`
355
414
 
@@ -628,6 +687,44 @@ Signals: `timeout`
628
687
  |---|---|---|
629
688
  | `anchor` | `"top-left"` | one of: `top-left` `top` `top-right` `left` `center` `right` `bottom-left` `bottom` `bottom-right` |
630
689
 
690
+ ## `UiBanner` — `incanto`
691
+
692
+ | Prop | Default | Kind |
693
+ |---|---|---|
694
+ | `anchor` | `"center"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
695
+ | `visible` | `true` | boolean |
696
+ | `size` | `42` | number |
697
+ | `seconds` | `2` | number |
698
+
699
+ Signals: `bannerShown`
700
+
701
+ ## `UiBar` — `incanto`
702
+
703
+ | Prop | Default | Kind |
704
+ |---|---|---|
705
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
706
+ | `visible` | `true` | boolean |
707
+ | `value` | `100` | number |
708
+ | `max` | `100` | number |
709
+ | `width` | `180` | number |
710
+ | `height` | `14` | number |
711
+ | `color` | `"#4ade80"` | string |
712
+ | `lowColor` | `"#ef4444"` | string |
713
+ | `lowThreshold` | `0.3` | number |
714
+ | `background` | `"rgba(0,0,0,0.5)"` | string |
715
+ | `label` | `""` | string |
716
+
717
+ ## `UiText` — `incanto`
718
+
719
+ | Prop | Default | Kind |
720
+ |---|---|---|
721
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
722
+ | `visible` | `true` | boolean |
723
+ | `text` | `""` | string |
724
+ | `size` | `16` | number |
725
+ | `color` | `"#ffffff"` | string |
726
+ | `shadow` | `true` | boolean |
727
+
631
728
  ## `VoxelGrid3D` — `incanto/3d`
632
729
 
633
730
  | Prop | Default | Kind |
@@ -678,6 +775,12 @@ Ready-made `Behavior`s auto-registered by `createGame2D`/`createGame3D` (opt out
678
775
  with `gameplay: false`). Attach via `"script": {"name": "...", "props": {...}}` and
679
776
  wire their signals through scene `connections`. Full guide: `incanto-gameplay-behaviors.md`.
680
777
 
778
+ ### `CameraShake`
779
+
780
+ | Prop | Default | Kind |
781
+ |---|---|---|
782
+ | `falloff` | `0.28` | number |
783
+
681
784
  ### `Chase`
682
785
 
683
786
  | Prop | Default | Kind |
@@ -718,6 +821,16 @@ Signals: `dealtDamage(amount, target)`
718
821
  | `smoothing` | `0` | number |
719
822
  | `deadzone` | `0` | number |
720
823
 
824
+ ### `GameFlow`
825
+
826
+ | Prop | Default | Kind |
827
+ |---|---|---|
828
+ | `restartAction` | `"restart"` | string |
829
+ | `freezeOnEnd` | `true` | boolean |
830
+ | `bannerPath` | `"%Banner"` | string |
831
+
832
+ Signals: `flowChanged`
833
+
721
834
  ### `Health`
722
835
 
723
836
  | Prop | Default | Kind |
@@ -133,6 +133,53 @@ Injected state combines with key state (vectors clamped to unit length).
133
133
  - Reference: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) — gravity, jump, attack lockout, custom
134
134
  `Player` node type. Verified in Chromium end-to-end.
135
135
 
136
+ ## Joints (Joint2D / Joint3D)
137
+
138
+ Link two bodies: put the joint node as a CHILD of body A, point `target` at
139
+ body B (any node path — `%Name` is typical):
140
+
141
+ ```json
142
+ { "name": "Ball", "type": "RigidBody2D",
143
+ "props": { "collider": { "shape": "circle", "radius": 10 } },
144
+ "children": [
145
+ { "name": "Rope", "type": "Joint2D",
146
+ "props": { "type": "rope", "target": "%Anchor", "length": 100 } }
147
+ ]}
148
+ ```
149
+
150
+ Types — 2D: `fixed` (weld) · `revolute` (pin/hinge) · `rope` (max distance)
151
+ · `spring` (pull toward `length` with `stiffness`/`damping`). 3D swaps
152
+ `revolute` for `spherical` (ball joint). `length: 0` measures the body
153
+ distance at creation. `anchor`/`targetAnchor` are local offsets (px / m).
154
+ Chains work (pendulums, bridges: each link a body + joint to the previous).
155
+
156
+ ## Raycasts
157
+
158
+ Both runtimes expose the same query (2D in PIXELS y-down, 3D in meters):
159
+
160
+ ```ts
161
+ const hit = physics.castRay(origin, dir, maxLen, excludeBody?, { staticOnly?: true });
162
+ // → { distance, normal, node } | null (sensors never block rays)
163
+ ```
164
+
165
+ Exclude the shooter's own body when casting from inside it.
166
+
167
+ ## Gamepad
168
+
169
+ `engine.input.attachGamepad(engine)` polls the first connected pad every
170
+ frame (headless no-op). Buttons are codes `Pad0`..`Pad16` in the SAME space
171
+ as keys — declare them in actions: `"jump": { "keys": ["Space", "Pad0"] }`
172
+ (standard mapping: Pad0=A/×, Pad1=B/○, Pad9=Start). Sticks:
173
+ `input.padAxes(0)` / `padAxes(1)` → deadzoned `{x, y}`. Unplugging releases
174
+ everything (no stuck inputs).
175
+
176
+ ## Changing colliders at runtime
177
+
178
+ REPLACE the object — `body.collider = { shape: 'rect', size: [w, 60] }` —
179
+ and physics rebuilds the body that step. In-place mutation of the existing
180
+ collider object (`body.collider.size[0] = w`) is NOT watched (the per-step
181
+ change scan was removed for performance).
182
+
136
183
  ## Debug drawing
137
184
 
138
185
  `physics.debugDraw = true` (the instance `enablePhysics2D/3D` returns) renders