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.
- package/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
package/skills/README.md
ADDED
|
@@ -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.
|