incanto 0.1.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 (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. package/templates/agent8-server.ts +138 -0
@@ -0,0 +1,9 @@
1
+ # incanto skills (shipped)
2
+
3
+ Full agent skills for building games with this **installed version** of Incanto.
4
+ An AI agent should start with `incanto-scene-json-authoring.md`, then the domain skill
5
+ for the task at hand. `incanto-node-reference.md` (+ `../schemas/scene.schema.json`)
6
+ is generated from the engine source and lists every node type, prop, and default.
7
+
8
+ These files are the source of truth; the thin platform-delivery entries live in the
9
+ repo's root `skills/` folder and merely point here.
@@ -0,0 +1,229 @@
1
+ ---
2
+ name: incanto-3d-character
3
+ description: The 3D character stack — CharacterController3D (floating-capsule movement with sprint/jump/fall tuning and an animation-driving movement state) and its camera rigs (free orbit / first person / quarter / side). Use when building any playable 3D character.
4
+ ---
5
+
6
+ # 3D characters: CharacterController3D
7
+
8
+ One node turns a dynamic body into a playable character with a camera:
9
+
10
+ ```json
11
+ {
12
+ "name": "Player", "type": "RigidBody3D",
13
+ "props": { "fixedRotation": true, "friction": 0,
14
+ "collider": { "shape": "capsule", "radius": 0.32, "height": 0.96 } },
15
+ "children": [
16
+ { "name": "Controller", "type": "CharacterController3D",
17
+ "props": { "view": "free", "camDistance": 4 } },
18
+ { "name": "Skin", "type": "ModelInstance3D",
19
+ "props": { "model": "$avatar", "targetHeight": 1.6 } }
20
+ ]
21
+ }
22
+ ```
23
+
24
+ MUST sit under a dynamic `RigidBody3D` (hard error otherwise) with a capsule
25
+ collider and `fixedRotation: true`. Set **`friction: 0`** on that body — the
26
+ character rides the hover spring, not floor contact, so friction would just brake
27
+ movement. Declare `move` (vector2) / `jump` /
28
+ `sprint` actions in the scene input map, and call
29
+ `engine.input.attachPointer(canvas, { lockOnClick: true })` for mouse look.
30
+
31
+ ## Movement model (vibe-starter-3d parity)
32
+
33
+ Impulse-based on the dynamic body: target speed = `maxSpeed ×
34
+ (1 + (sprintMultiplier−1)·intensity)` where keyboard intensity is 0.6
35
+ (any move key) or 1.0 (+sprint). Defaults: maxSpeed 2.5, sprint ×2 → walk
36
+ ≈4 m/s, sprint ≈5 m/s. `jumpVelocity` 4 (+20% at full sprint), falls get
37
+ gravityScale 2.5 with a −20 m/s terminal clamp, and a hover spring holds the
38
+ capsule BOTTOM `floatHeight` above the ground (5-ray probe). Author the Skin
39
+ y-offset as `-(halfHeight + radius + floatHeight)` so the feet sit at the
40
+ capsule bottom (the example templates do this).
41
+
42
+ ## Views
43
+
44
+ | view | what it does | key props |
45
+ |---|---|---|
46
+ | `free` | third-person orbit, mouse yaw/pitch, camera-relative WASD | `camDistance` (4), `pitchMin/Max` |
47
+ | `firstPerson` | camera at the eye (`eyeHeight` 0.64), pointer-lock look | `camDistance` ~0.01 |
48
+ | `quarter` | fixed isometric pitch 35.264°, NO mouse look | `camDistance` 40, `mouseLook: false` |
49
+ | `side` | camera at +z `camDistance`, lock movement to ±x | `mouseLook: false` |
50
+
51
+ The controller drives the scene's `current` Camera3D every frame (smoothed).
52
+ Wheel zooms only when `zoomMax > zoomMin`.
53
+
54
+ In `free` view the camera AIMS at the character every frame (`lookAt`), so the
55
+ character stays centered even while the camera is catching up or pulled in by a
56
+ collision — no jitter.
57
+
58
+ **Camera collision (spring arm).** For the orbit views (`free`/`quarter`/`side`)
59
+ the camera won't clip through walls or the ground: each frame it raycasts from
60
+ the eye toward the camera and, if a collider blocks the boom, smoothly pulls the
61
+ camera in front of it (plus a ground backstop so it never drops below the floor).
62
+ This is on by default (`cameraCollision: true`); set it `false` to get the old raw
63
+ orbit. Only the STATIC/fixed world stops it — give walls/floors/terrain a
64
+ `StaticBody3D` collider (collider-less meshes are invisible to the ray, same as to
65
+ the player). MOVABLE bodies are ignored on purpose: both dynamic ones (projectiles,
66
+ `RigidBody3D`) and kinematic ones (enemies/NPCs are `CharacterBody3D`), so a passing
67
+ enemy never yanks the camera in. `firstPerson` skips it (the camera is at the eye).
68
+
69
+ ## Animations
70
+
71
+ `controller.state` is `idle | walk | run | fastRun | airborne`; the
72
+ `movementStateChanged` signal fires on transitions — map states to clips
73
+ (`$animation` assets retarget onto VRM automatically):
74
+
75
+ ```ts
76
+ controller.on('movementStateChanged', (state) => {
77
+ skin.animation = { idle: '$idle', run: '$run', fastRun: '$runFast',
78
+ airborne: '$jump', walk: '$walk' }[state];
79
+ });
80
+ ```
81
+
82
+ Setting `animation` to a new clip **crossfades** (0.2s blend) from the current
83
+ one — idle↔walk↔run↔jump transitions are smooth, not a hard cut.
84
+
85
+ ## Reskin one model with `tint` (many variants from one GLB)
86
+
87
+ `ModelInstance3D` has a `tint` prop (hex, `""` = off) that MULTIPLIES into every
88
+ material — turn the one base humanoid into a whole cast WITHOUT extra assets: a
89
+ sickly-green zombie, a blue ally, a red boss. It clones each material per instance
90
+ (the shared source is never mutated, so the player stays untinted) and preserves the
91
+ texture/shading detail (it's a multiply, not a flat repaint). Pair it with a
92
+ shambling clip + glowing eyes for an enemy that reads as a different creature.
93
+
94
+ ```json
95
+ { "name": "Skin", "type": "ModelInstance3D",
96
+ "props": { "model": "$avatar", "targetHeight": 1.7, "tint": "#5f8f4a",
97
+ "animation": "$walk", "castShadow": true } }
98
+ ```
99
+
100
+ The controller also yaw-rotates the sibling at `skinPath` ('../Skin') toward
101
+ the move direction at `turnSpeed` rad/s (100 = instant snap) — the body
102
+ itself never rotates. The skin MOUNTS at 180° (facing away from the default
103
+ camera, original parity) and then faces wherever it moves; `skinYawOffset`
104
+ adds a correction for models whose native forward is not +z.
105
+
106
+ ## Facing a direction in 3D — the +Z-FORWARD rule (read before turning ANY skin)
107
+
108
+ This trips people up REPEATEDLY, so here is the one rule. agent8's `base-model` (and
109
+ any model you give `skinYawOffset: 0`) is **+Z-FORWARD**: its face looks down +Z at
110
+ rotation 0. To turn it to face a world heading `(dx, dz)`:
111
+
112
+ ```ts
113
+ const dx = targetX - selfX; // heading you want to face: movement delta, OR (target - self)
114
+ const dz = targetZ - selfZ; // 3D ground plane: x and z (index 0 and 2), NEVER y
115
+ skin.rotation = [skin.rotation[0], Math.atan2(dx, dz) * RAD2DEG, skin.rotation[2]];
116
+ // RAD2DEG = 180/Math.PI ; rotations are DEGREES, Euler XYZ
117
+ ```
118
+
119
+ That is the EXACT formula `CharacterController3D` uses for move-facing
120
+ (`atan2(mx, mz) + skinYawOffset`). Do NOT add 180° to it.
121
+
122
+ **Same formula to LOOK AT / AIM at any target** (turret, NPC turning to face the player,
123
+ an enemy that idles facing you): use the direction from self TO the target —
124
+ `dx = target.position[0] - self.position[0]`, `dz = target.position[2] - self.position[2]`,
125
+ then `rotation[1] = atan2(dx, dz)*RAD2DEG`. Movement-facing is just this with the
126
+ target being "where I'm walking." There is ONE facing formula; only the direction differs.
127
+
128
+ - **The 180° is NOT a facing formula.** It is only the AT-REST MOUNT — the character
129
+ starts turned away from the behind-the-shoulder camera until it first moves. Adding
130
+ 180° to *move-facing* makes the character run BACKWARDS (it shows you its back while
131
+ charging at you). That is the #1 recurring bug when driving a custom enemy/NPC skin.
132
+ - **Don't hand-mix conventions.** `Shoot.faceAim` looks like it omits the 180 because
133
+ its input is a camera *look* vector that already encodes it — a different context.
134
+ For a MOVEMENT/heading vector, the answer is plain `atan2(dx, dz)`, full stop.
135
+ - **Model not +Z-forward?** Don't guess a 90/180 — set `skinYawOffset` (controller) or
136
+ add the same constant to your `atan2`, then verify.
137
+ - **ALWAYS verify facing — never ship it on reasoning alone.** In the browser, the
138
+ decisive check is a dot product: the skin object's world **+Z basis** (matrixWorld
139
+ columns `[8],[10]` = its front in x,z) dotted with the unit direction it should face
140
+ must be ≈ **+1** (−1 means it's backwards). Or just look: a charging enemy must show
141
+ its FACE, not its back. (Headless, no three.js: with the yaw you set, the model's
142
+ forward is `(sin(yaw), cos(yaw))`; dot it with the desired unit dir — `< 0` = wrong way.)
143
+
144
+ **NON-player models (enemies/NPCs) face + animate via the SAME rules, by hand.**
145
+ `CharacterController3D` automates this for the player; a custom-driven model does it
146
+ itself — face with the formula above, and drive `skin.animation` from your AI state (see
147
+ incanto-gameplay-behaviors.md "Driving a model's animation from a custom AI"). Ground a
148
+ moving non-controller body yourself — it won't auto-fall (see incanto-3d-models.md
149
+ "Grounding a model").
150
+
151
+ Mouse look is GATED: deltas only accumulate while pointer-locked or a
152
+ button is held — a free-roaming cursor never spins the camera. Vertical look is
153
+ STANDARD (non-inverted): mouse UP looks up (in free view the camera swings down so
154
+ it looks up at the character against the sky), mouse DOWN looks down.
155
+
156
+ ## First-person weapon viewmodel + the tracer-from-the-muzzle rule
157
+
158
+ A weapon VIEWMODEL is just a `MeshInstance3D`/group child of the **root** `Camera`
159
+ (`/root/Camera/Gun`) — it rides the view for free. Pose it each frame in your Shoot
160
+ behavior and add a recoil impulse on fire (kick back +z, flip up −x, ease back); pulse a
161
+ barrel `OmniLight` for the muzzle flash. Size it for the near plane: a 0.46 m body at
162
+ 0.5 m with fov 80 fills half the screen (it's viewed broadside), so keep viewmodel meshes
163
+ small (~0.3 m) and tucked lower-right.
164
+
165
+ **The tracer must visibly fly FROM the muzzle TO the target.** This recurred many times;
166
+ there are TWO independent things to get right — the START POINT and the ROD ORIENTATION:
167
+
168
+ **(1) Start at a real muzzle NODE.** Put a tiny `Node3D` (`Muzzle`) at the front-centre
169
+ of the barrel mesh (the barrel cylinder's tip), as a child of the gun so it rides the
170
+ view + recoil. Read ITS world position each shot — that IS where the muzzle is on screen,
171
+ at any aim angle:
172
+
173
+ ```ts
174
+ // muzzle = /root/Camera/Gun/Muzzle ; o = muzzle._ensureObject3D()
175
+ o.updateWorldMatrix(true, false);
176
+ const e = o.matrixWorld.elements;
177
+ const from = [e[12], e[13], e[14]]; // tracer ORIGIN = the barrel tip
178
+ ```
179
+
180
+ Never reconstruct the muzzle from `eye + camera-basis × offset` (it drifts from the
181
+ rig-driven camera, and the camera `right` basis is easy to invert — correct is
182
+ `[-fwd.z, 0, fwd.x]`). The node's matrix is the one source of truth.
183
+
184
+ **(2) `end` is the TARGET, and orient the rod with the CORRECT Euler.** Draw the tracer
185
+ from `from` to the hit point (enemy torso, or `eye + fwd*range` if you missed) — a line
186
+ muzzle→target converges on the crosshair. A streak parallel to camera-forward (offset
187
+ from the muzzle) never converges and looks like it fires off to the side.
188
+
189
+ The rod is a +Z-stretched box; orienting it is the subtle trap. `Node3D` rotation is Euler
190
+ order **XYZ**, whose +Z basis column is `(sin ry, −sin rx·cos ry, cos rx·cos ry)`. So to
191
+ aim the rod's +Z along unit `(Dx,Dy,Dz)`:
192
+
193
+ ```ts
194
+ const ry = Math.atan2(Dx, Math.hypot(Dy, Dz)); // NOT atan2(dx, hypot(dx,dz))
195
+ const rx = Math.atan2(-Dy, Dz);
196
+ t.rotation = [rx*RAD2DEG, ry*RAD2DEG, 0];
197
+ ```
198
+
199
+ The "obvious" `[-atan2(dy, hypot(dx,dz)), atan2(dx,dz), 0]` is only correct for a LEVEL
200
+ shot — for XYZ order the yaw denominator must be `hypot(Dy,Dz)`, not the ground `hypot(dx,
201
+ dz)`. With the wrong form the rod skews off-axis when you aim up/down, so its near end
202
+ drifts AWAY from the muzzle (the box is centred on the midpoint, so a tilt swings both
203
+ ends). THIS — not the start point — was the actual "fires from the centre" bug; moving
204
+ the origin around never fixed it.
205
+
206
+ **Verify by MEASUREMENT, not eyeballing.** In the browser, project the muzzle node's world
207
+ position and the rod's two endpoints to screen with the camera matrices, and assert: near
208
+ end ≈ muzzle (a few px), far end on the crosshair, and `rodAxis · (target−muzzle) ≈ 1`.
209
+ Do it at a steep look-DOWN angle (a level shot hides the skew because gun and centre
210
+ overlap). Pitch sign gotcha when scripting the aim: `fwd.y = −sin(pitch)`, so to aim DOWN
211
+ pitch is POSITIVE.
212
+
213
+ **Make it THIN and make it FADE.** A hitscan tracer lives in WORLD space, so a thick,
214
+ opaque, long-lived rod looks like a "wooden stick" left hanging beside you when you fire
215
+ while moving (the camera moves, the rod doesn't). Keep the cross-section tiny (~0.02 m), a
216
+ white/bright emissive (reads as light, not lumber), and FADE its opacity to 0 over a short
217
+ life (~45 ms): each frame set `tracer.material.opacity = start * remain / life` — `syncTree`
218
+ re-applies `material.opacity` every frame, so mutating the node's material prop just works.
219
+ A snappy fading flash never reads as a stationary plank.
220
+
221
+ **Best feel for a hitscan shot = a muzzle FLASH + a SHORT dash, not a full beam.** A rod
222
+ spanning the whole muzzle→target distance (often tens of metres) is the worst "stick"
223
+ offender. Instead: (1) pop a tiny `Particles3D` burst at the muzzle each shot — `burst`
224
+ ~14, `spreadDeg 360` (a 3D sphere; Particles3D sets `spreadZ`), `lifetime [0.05,0.16]`,
225
+ high `drag` so it's a tight pop, `blend:'add'`, `depthTest:false` so the gun doesn't occlude
226
+ it — that's the "shot bursting" punch the player reads as firing; and (2) cap the tracer to
227
+ a SHORT segment (~2.5 m) anchored at the muzzle (`centre = from + dir*seg/2`, `size.z = seg`,
228
+ `min(fullDist, TRACER_LEN)`), not the full distance. The flash sells the shot; the impact is
229
+ sold by the hitmarker + a death burst at the target — the streak doesn't need to reach.
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: incanto-3d-models
3
+ description: Loading GLB/glTF and VRM models in Incanto — the ModelInstance3D node, declaring model assets, sizing with targetHeight, playing embedded animations, and the incanto-model CLI that reports a file's hierarchy/bounding box/animations so you can place it correctly. Use when putting 3D model files into a scene.
4
+ ---
5
+
6
+ # 3D Models (GLB · glTF · VRM)
7
+
8
+ > Shipped inside the `incanto` npm package — this document always matches the
9
+ > installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
10
+
11
+ ## ALWAYS inspect before you place
12
+
13
+ A file name tells you nothing about a model's size or contents. The package ships an
14
+ inspector — run it FIRST and read the numbers:
15
+
16
+ ```bash
17
+ bunx incanto-model public/models/knight.glb
18
+ # size: 1.1 × 1.8 × 0.6 center: [0, 0.9, 0] ← stands 1.8 units tall
19
+ # anim Idle: 2.4s
20
+ # anim Run: 0.8s ← exact clip names for `animation`
21
+ bunx incanto-model avatar.vrm --json # full machine-readable report
22
+ ```
23
+
24
+ The report gives you: the TRANSFORMED scene bounding box (size/center/min/max — skinned
25
+ meshes measured in bind pose), the node hierarchy, per-mesh vertex/triangle counts,
26
+ animation clip names + durations, materials/textures/skins, and for VRM the avatar
27
+ meta (name/authors) + humanoid bone count. Decision rules:
28
+
29
+ - **size.y** tells you the model's natural height → pick `targetHeight` (characters in a
30
+ meters-scale scene: 1.6–1.9) or leave 0 and use the node's `scale`.
31
+ - **center** ≠ [0,0,0] means the model is off-origin — compensate with the node `position`
32
+ (a center.y of half the height usually means feet at origin: good).
33
+ - **animation names** are exact strings for the `animation` prop.
34
+
35
+ ## Scene JSON
36
+
37
+ ```json
38
+ "assets": {
39
+ "knight": { "type": "model", "url": "/models/knight.glb" },
40
+ "avatar": { "type": "model", "url": "/models/avatar.vrm" }
41
+ },
42
+ "root": { "name": "World", "type": "Node3D", "children": [
43
+ { "name": "Knight", "type": "ModelInstance3D",
44
+ "props": { "model": "$knight", "targetHeight": 1.8, "animation": "Idle",
45
+ "position": [0, 0, 0], "castShadow": true } },
46
+ { "name": "Sun", "type": "DirectionalLight3D", "props": { "position": [3, 5, 4] } },
47
+ { "name": "Cam", "type": "Camera3D", "props": { "position": [0, 2, 6], "current": true } }
48
+ ] }
49
+ ```
50
+
51
+ `ModelInstance3D` props: `model` (`$key` or a direct URL), `targetHeight` (>0 uniformly
52
+ scales so the bounding box stands that many units tall — composes with the node `scale`,
53
+ reactive at runtime), `animation` (see below), `tint` (hex; `""`=off — multiplies into
54
+ every material to RESKIN one shared GLB into many variants, e.g. base human → green
55
+ zombie; clones materials per instance so other instances are untouched),
56
+ `castShadow`/`receiveShadow`, plus the usual Node3D transform.
57
+
58
+ **`targetHeight` + skinned rigs (the implausible-scale warning).** Mixamo-style humanoid
59
+ rigs hide their true size behind a ~0.01 armature scale + bone-driven vertex scaling, so
60
+ a bounding box can't measure them. If `targetHeight` would imply a scale `<0.05×` or
61
+ `>20×`, the engine WARNS and renders at the model's authored (1×) scale instead — this is
62
+ EXPECTED and fine: humanoid GLBs already ship at ~1.6–1.9 m, so they look right at 1×.
63
+ Don't fight the warning. If a NON-humanoid (a 1:100 prop) really is mis-sized, use the
64
+ node `scale` instead of `targetHeight`. Always `bunx incanto-model <file>` first to see
65
+ the real `size.y`.
66
+
67
+ ## Grounding a model on the floor / terrain
68
+
69
+ A model's feet sit at the model's OWN origin, which is usually `y=0` but not always —
70
+ `bunx incanto-model` reports `center.y`. Two cases:
71
+
72
+ - **Static placement:** set the node's `position[1]` so the feet rest on the ground
73
+ (e.g. on a Terrain3D, `position[1] = terrain.heightAt(x, z)`; offset by `-center.y`
74
+ if the model's origin isn't at its feet).
75
+ - **A MOVING body driven by a custom AI (enemy/NPC) — IT WON'T AUTO-FALL.** A
76
+ `CharacterBody3D`/`RigidBody3D` whose position you set every frame from script is
77
+ effectively kinematic: nothing integrates gravity, so it floats at its spawn height.
78
+ Ground it yourself in the behavior's `update()`:
79
+
80
+ ```ts
81
+ // keep the husk on the rolling terrain every frame (AI moved only x/z)
82
+ const t = this.node.getRoot().getNodeOrNull('World/Terrain/Ground/Surface'); // your Terrain3D
83
+ if (t) { const p = body.position; body.position = [p[0], t.heightAt(p[0], p[2]) + footLift, p[2]]; }
84
+ ```
85
+
86
+ `heightAt(x, z)` takes WORLD x/z and returns the WORLD surface y. (`incanto/gameplay`
87
+ movement behaviors like `ZombieAI` deliberately move only in the ground plane and leave
88
+ the up axis to you, exactly so you can ground it like this — or let real physics do it.)
89
+
90
+ ## Troubleshooting a 3D model
91
+
92
+ | Symptom | Likely cause → fix |
93
+ |---|---|
94
+ | Faces sideways/backwards | Wrong yaw — see incanto-3d-character.md "+Z-FORWARD rule" (don't add 180° to move-facing; use `skinYawOffset` for non-+Z models). |
95
+ | Giant / tiny | `bunx incanto-model` → read `size.y` → set `targetHeight` to your scene scale. Implausible-scale warning on a humanoid? Accept it (renders at 1×). |
96
+ | Floats / sinks | Origin offset (`center.y`) or an AI-driven body that won't fall — ground it (above). |
97
+ | No animation | Clip name typo (match `bunx incanto-model` output EXACTLY, case-sensitive), or the model/asset isn't declared, or you set `animation` before the model finished loading (it applies once ready), or a GLB clip's bone names don't match the model. |
98
+ | Black silhouette | No light — add a DirectionalLight3D or `environment.ambient`. |
99
+
100
+ ## Animation assets — clips as data
101
+
102
+ Animations are first-class assets: `{type:"animation", url, clip?}` loads a GLB's clips
103
+ INTO MEMORY (never drawn). `animation` then accepts either an EMBEDDED clip name from the
104
+ model file, or an animation-asset reference:
105
+
106
+ ```json
107
+ "assets": {
108
+ "knight": { "type": "model", "url": "/models/knight.glb" },
109
+ "avatar": { "type": "model", "url": "/models/avatar.vrm" },
110
+ "run": { "type": "animation", "url": "/anims/run.glb" }
111
+ },
112
+ … { "props": { "model": "$knight", "animation": "Idle" } } // embedded clip
113
+ … { "props": { "model": "$avatar", "animation": "$run" } } // asset clip
114
+ ```
115
+
116
+ - GLB models bind asset clips by NODE NAME — the animation file's rig must use the same
117
+ bone names as the model (e.g. both exported from the same skeleton).
118
+ - **VRM models retarget automatically**: Mixamo-rigged animation GLBs
119
+ (`mixamorigHips`…) are mapped through the mixamo→VRM humanoid rigmap
120
+ (rest-pose-compensated rotations, hips height scaling, VRM 0.x axis flips) and played
121
+ on the VRM's normalized rig. Unmapped bones are logged. This is how you give any VRM
122
+ avatar a Mixamo walk/run/idle: export the Mixamo animation as GLB (without skin works),
123
+ declare it as an animation asset, set `animation: "$walk"`.
124
+ - `clip` in the asset declaration selects a clip when the file holds several
125
+ (default: the first).
126
+
127
+ Models need LIGHT (standard materials): add a DirectionalLight3D or scene
128
+ `environment.ambient` or you will see black silhouettes.
129
+
130
+ ## How loading works
131
+
132
+ `Renderer3D` owns an `AssetStore3D` (GLTFLoader + `@pixiv/three-vrm`'s VRMLoaderPlugin).
133
+ Models load async and pop in when ready; every node mounts a skeleton-aware CLONE, so one
134
+ asset can back any number of instances. VRM specifics: 0.x files get their facing fixed
135
+ (`rotateVRM0`), unnecessary vertices removed, skeletons combined.
136
+
137
+ ## In the editor
138
+
139
+ Declare the assets in the ⚙ scene row (`assets` JSON) — or open a scene that has them —
140
+ and the inspector's `model`/`animation` fields offer DROPDOWN suggestions (model `$key`s;
141
+ embedded clips + animation `$key`s). Selecting an animation plays it LIVE in the edit
142
+ viewport (game-logic time stays frozen; only model animation previews).
143
+
144
+ ## Limits (be honest with users)
145
+
146
+ - A VRM mounts its source scene: humanoid rig + springbones stay live via `vrm.update`,
147
+ but ONE node per VRM asset (a second node warns and stays empty). GLB models clone
148
+ freely.
149
+ - GLB↔GLB cross-file animation needs matching bone names (no generic retargeting);
150
+ no animation blending yet.
151
+ - Bounding boxes for skinned meshes are bind-pose.
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: incanto-assets
3
+ description: Where game art comes from in Incanto — the FULL built-in catalog shipped in the package (2D sprites with animations, tilesets, item pickups, effects, AND 3D foliage textures: tree leaves, tree bark, terrain/ground splat textures), how to reference each in scene JSON (the catalog `url` contract), the incanto-assets CLI (list/info/copy), external/MCP-provided asset URLs, art-free prototyping with ColorRect2D and data URIs, and the library sprite-animation JSON convention. Use when a game needs sprites, tiles, models, leaf/bark/terrain textures, or any visual asset.
4
+ ---
5
+
6
+ # Assets — where the art comes from
7
+
8
+ > Shipped inside the `incanto` npm package — this document always matches the
9
+ > installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
10
+
11
+ Resolve art in this order: ① built-ins (zero setup) → ② asset MCP servers /
12
+ known URLs → ③ art-free primitives. Never invent asset URLs.
13
+
14
+ ## 1. Built-in assets (in the package)
15
+
16
+ ```bash
17
+ bunx incanto-assets list # the full catalog (see categories below)
18
+ bunx incanto-assets info medieval-knight # description + animation names
19
+ bunx incanto-assets copy medieval-knight --out public/assets
20
+ ```
21
+
22
+ `list --json` prints every entry. Each entry carries a **`url`** — the drop-in
23
+ reference you put in scene JSON so the asset LOADS (the contract, see §1b).
24
+
25
+ ### Categories — the whole built-in set
26
+
27
+ | kind | examples | what they're for |
28
+ |-------------|--------------------------------------------------------------|-----------------------------------------------|
29
+ | `character` | `medieval-knight`, `goblin`, `ghost`, `2dbasic` | animated 2D sprite SHEETS (idle/move/attack…) |
30
+ | `tile` | `floor00`, `wall00`, `minecraft-tiles` | 2D tile textures / tilesheets |
31
+ | `item` | `coin`, `gem`, `gold`, `hp_potion`, `box`, `trap`, `map`, … | 2D pickup sprites |
32
+ | `effect` | `swoosh` | 2D effect sprites |
33
+ | `foliage` | `leaves_oak/ash/pine/aspen` | **Tree3D** `leafTexture` (leaf-cluster cutout)|
34
+ | `foliage` | `bark_oak/birch/pine_{color,normal,roughness}` | **Tree3D** trunk bark (sampled by default) |
35
+ | `foliage` | `ground_grass`, `ground_dirt`, `ground_dirt_normal` | **Terrain3D** grassland ground textures |
36
+ | `audio` | `explosion`, `gold-loot`, `slash`, `heal`, `ui-click`, … | **AudioPlayer** `src` sound files (see below) |
37
+
38
+ ### 1b. The `url` contract — referencing each kind
39
+
40
+ Every catalog entry has a `url` that is directly usable; there are two classes:
41
+
42
+ - **Foliage (leaves / bark / ground)** — `url` is a hosted image on the agent8 CDN
43
+ (`agent8-games.verse8.io`, the ONLY sanctioned external host) — the exact texture
44
+ Tree3D/Terrain3D sample **zero-setup by default**, so a bare node already works.
45
+ Override only to swap the look; drop a `url` straight into the matching prop:
46
+
47
+ ```jsonc
48
+ // Tree3D — a bare node already loads oak leaves from the agent8 CDN; override
49
+ // leafTexture only to swap in a different built-in cutout (e.g. ash)
50
+ { "type": "Tree3D", "props": { "type": "broadleaf",
51
+ "leafTexture": "https://agent8-games.verse8.io/assets/3D/default/textures/vegetation/ash_color.png" } }
52
+
53
+ // MeshInstance3D material — built-in ground/bark color as a map (+ a normal map)
54
+ { "type": "MeshInstance3D", "props": { "material": {
55
+ "map": "https://agent8-games.verse8.io/assets/3D/default/textures/vegetation/ground/grass.jpg",
56
+ "normalMap": "https://agent8-games.verse8.io/assets/3D/default/textures/vegetation/ground/dirt_normal.jpg" } } }
57
+ ```
58
+
59
+ `Terrain3D.textureBase` is a directory base (`<base>/<layer>.png`), not a
60
+ single file — leave it at the default agent8 set or point it at your own base.
61
+
62
+ - **Audio (`kind: "audio"`)** — `url` is `incanto/assets/audio/<file>`, the
63
+ drop-in for `AudioPlayer.src` (e.g. `"src": "incanto/assets/audio/explosion.mp3"`).
64
+ For zero-asset sound, prefer the procedural SFX **presets** instead of a file.
65
+ Full guide: **incanto-audio.md** (presets, AudioPlayer, volume buses).
66
+
67
+ - **Packaged 2D sprites (character / tile / item / effect)** — `url` is
68
+ `incanto/assets/<file>` (e.g. `incanto/assets/items/coin.png`). This is the
69
+ **bundler-import / copy** reference. In a bundler game (vite, the usual target)
70
+ import it; otherwise `incanto-assets copy` puts the file in your project. These
71
+ props are `$asset` REFS, not URLs — the url lives in the scene `assets{}` entry:
72
+
73
+ ```ts
74
+ import coinUrl from 'incanto/assets/items/coin.png';
75
+ // scene assets: { "coin": { "type": "texture", "url": coinUrl } }
76
+ // node: { "type": "Sprite2D", "props": { "texture": "$coin" } }
77
+ ```
78
+
79
+ `copy` does this for you and prints the ready-to-paste JSON — for animated
80
+ sheets it includes the full `animations` map (idle/move/attack…) derived from
81
+ the sheet metadata. `Sprite2D.texture` / `AnimatedSprite2D.sheet` take a
82
+ `"$assetKey"` ref (the engine hard-fails on a raw URL).
83
+
84
+ The scene **composer (incanto-editor)** wires all of this for you: the inspector
85
+ shows an asset PICKER on every texture/sheet/map prop — browse the built-ins by
86
+ name (with kind + description), and picking one inserts the loadable value
87
+ (foliage → the CDN url; 2D sprite → a `$asset` ref + the scene `assets{}` entry).
88
+
89
+ ## 2. External art (asset MCP servers, CDNs, your files)
90
+
91
+ Any URL works in scene `assets{}` — `{ "type": "spritesheet", "url": "<from
92
+ your asset tool>", "frameWidth": …, "frameHeight": … }`. When an asset source
93
+ hands you a sheet WITH a library-convention animation JSON (`{frame: {width,
94
+ height}, animations: {idle: {start, end, frameRate, repeat}}}`), convert it
95
+ mechanically:
96
+
97
+ ```ts
98
+ import { spriteFromLibraryMeta } from 'incanto/2d';
99
+ const { asset, props } = spriteFromLibraryMeta(animJson, {
100
+ url: sheetUrl, assetKey: 'hero',
101
+ });
102
+ // asset → scene assets.hero; props → an AnimatedSprite2D's props. Done.
103
+ ```
104
+
105
+ For 3D files, ALWAYS inspect before placing: `bunx incanto-model file.glb`
106
+ (bounds, animations, rig — see incanto-3d-models).
107
+
108
+ ## 3. No art yet? Ship gameplay anyway
109
+
110
+ - `ColorRect2D` — solid rectangles for paddles/walls/platforms/flashes.
111
+ - `Particles2D` presets — fire/explosion/magic with zero files.
112
+ - `AudioPlayer` SFX **presets** — coin/jump/hurt/explosion/… with zero files
113
+ (the audio analog; see incanto-audio.md).
114
+ - 1×1 data-URI + `tint` + `scale` on a Sprite2D for anything else;
115
+ canvas-generated `data:` URLs also work as asset urls.
116
+
117
+ Swap in real art later by changing ONLY the `assets{}` entry — node props
118
+ stay untouched.