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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-building-2d-games
|
|
3
|
+
description: Build 2D web games with Incanto — y-down pixel coordinates, string-keyed assets, sprites/spritesheets, following camera, screen-space HUD. Use when creating or modifying 2D scenes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building 2D Games with Incanto
|
|
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
|
+
Prerequisite: `incanto-scene-json-authoring.md` (this directory) for the file format. This skill covers the 2D taxonomy.
|
|
12
|
+
|
|
13
|
+
## Coordinate convention (Phaser/Godot prior)
|
|
14
|
+
|
|
15
|
+
**1 unit = 1 px · (0,0) top-left · +y DOWN · positive rotation = clockwise (degrees).**
|
|
16
|
+
|
|
17
|
+
## Boot sequence
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createGame2D } from 'incanto/2d';
|
|
21
|
+
import { PlayerControl } from './behaviors';
|
|
22
|
+
import sceneJson from './my.scene.json';
|
|
23
|
+
|
|
24
|
+
const game = await createGame2D({
|
|
25
|
+
canvas: document.querySelector('canvas')!,
|
|
26
|
+
scene: sceneJson, // cloned internally — no cast, no structuredClone
|
|
27
|
+
behaviors: { PlayerControl }, // registered for you (hot-replace tolerant)
|
|
28
|
+
});
|
|
29
|
+
// game.engine / game.scene / game.renderer / game.physics
|
|
30
|
+
// game.dispose() — ONE call tears everything down (SPA unmount)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
One call does register → load → physics → input → renderer → start. Physics is
|
|
34
|
+
`'auto'`: Rapier is enabled iff the tree has physics bodies (force with
|
|
35
|
+
`physics: true|false`). Keyboard attaches to `window` (`keyboard: false`
|
|
36
|
+
disables, or pass a target). `seed` / `fixedHz` / `pixelRatio` / `antialias`
|
|
37
|
+
pass through. Gesture-blocked audio retries on the first canvas pointerdown.
|
|
38
|
+
The manual `Engine` + `loadScene` + `Renderer2D` boot still exists for full
|
|
39
|
+
control.
|
|
40
|
+
|
|
41
|
+
## Assets (scene JSON `assets{}` — referenced as `"$key"`)
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
"assets": {
|
|
45
|
+
"hero": { "type": "texture", "url": "hero.png", "filter": "nearest" },
|
|
46
|
+
"hero_run": { "type": "spritesheet", "url": "run.png", "frameWidth": 16, "frameHeight": 16 }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
- `filter: "nearest"` = pixel-art mode (no smoothing, no mipmaps). Default is `linear`.
|
|
50
|
+
- Data URLs work as `url` (you can generate textures at runtime and inject them before `loadScene`).
|
|
51
|
+
- Unknown `$refs` are hard `UNKNOWN_ASSET` errors listing declared keys.
|
|
52
|
+
|
|
53
|
+
## 2D node taxonomy
|
|
54
|
+
|
|
55
|
+
All 2D nodes extend `Node2D`: `position [x,y]` px · `rotation` deg · `scale [x,y]` ·
|
|
56
|
+
`renderOrder` (draw order, higher = on top — the SAME prop name as 3D; legacy
|
|
57
|
+
`zIndex` still loads as an alias) · `visible`.
|
|
58
|
+
|
|
59
|
+
### `ColorRect2D` (no-art prototyping)
|
|
60
|
+
A solid-colored rectangle — no texture, no asset file. Props: `size [100,100]`,
|
|
61
|
+
`color '#ffffff'`, `opacity 1`, `anchor [0.5,0.5]`. The prototyping workhorse
|
|
62
|
+
(paddles, walls, platforms, flashes); swap in a `Sprite2D` when art arrives.
|
|
63
|
+
|
|
64
|
+
### `Sprite2D`
|
|
65
|
+
| Prop | Default | Notes |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `texture` | `""` | `'$key'`; empty = hidden |
|
|
68
|
+
| `anchor` | `[0.5,0.5]` | `[0,0]` = top-left sits on the origin |
|
|
69
|
+
| `flipX` / `flipY` | `false` | |
|
|
70
|
+
| `tint` | `"#ffffff"` | multiplies the texture |
|
|
71
|
+
| `opacity` | `1` | |
|
|
72
|
+
|
|
73
|
+
### `AnimatedSprite2D` (extends Sprite2D — but use `sheet`, not `texture`)
|
|
74
|
+
```json
|
|
75
|
+
{ "sheet": "$hero_run", "autoplay": "run",
|
|
76
|
+
"animations": { "run": { "frames": [0, 7], "fps": 12, "loop": true },
|
|
77
|
+
"hit": { "frames": [8, 8, 10], "fps": 10, "loop": false } } }
|
|
78
|
+
```
|
|
79
|
+
- `frames` with EXACTLY two ASCENDING numbers = inclusive `[start, end]` RANGE; any other
|
|
80
|
+
shape (longer arrays, or a descending pair) = explicit frame list. Frame indices are
|
|
81
|
+
row-major across the sheet.
|
|
82
|
+
- API: `play(name)` (hard `UNKNOWN_ANIMATION` error if missing), `stop()`,
|
|
83
|
+
`currentAnimation`, `currentFrame`, `playing`; emits `animationFinished(name)` on
|
|
84
|
+
non-loop completion. `autoplay` starts on ready.
|
|
85
|
+
|
|
86
|
+
**⚠️ STAND THE FEET ON THE GROUND — the `anchor` is almost never the feet.** A
|
|
87
|
+
character that looks like it FLOATS above the platform (or sinks into it) is the #1
|
|
88
|
+
sprite bug, and the fix is mechanical — you do NOT need to eyeball it. Two facts
|
|
89
|
+
combine: (1) a sheet frame has TRANSPARENT PADDING below the art, and (2) the artist
|
|
90
|
+
`origin`/anchor is usually the hip/centre, NOT the feet. So when the node sits where
|
|
91
|
+
physics rests (a `CharacterBody2D` capsule rests its BOTTOM on the floor → the node is
|
|
92
|
+
the collider CENTRE, `halfH` above the floor) but the sprite's feet render above that,
|
|
93
|
+
the character floats on the invisible lower half of the collider.
|
|
94
|
+
|
|
95
|
+
**Step 1 — find the feet row `feetFrameY` (pixels down the frame).** Easiest: read the
|
|
96
|
+
sheet's companion metadata (`bunx incanto-assets info <name>`, or the `<sheet>.json`
|
|
97
|
+
next to it). It has `frame` (the frame size), `origin` (the artist pivot — usually the
|
|
98
|
+
hip, which is exactly why it's not the feet), and `body {offset, size}` (the character
|
|
99
|
+
box). The feet ≈ the body BOTTOM: `feetFrameY = body.offset.y + body.size.y`
|
|
100
|
+
(e.g. medieval-knight `76 + 60 = 136` in a 192px frame). No metadata? Measure exactly:
|
|
101
|
+
draw the sheet to a `<canvas>` in the browser and scan for the lowest row whose alpha
|
|
102
|
+
> 0 within frame 0.
|
|
103
|
+
|
|
104
|
+
**Step 2 — solve the anchor.** The collider extends `halfH` below the node (capsule
|
|
105
|
+
total height = `height + 2·radius`; e.g. `radius 18, height 30` → 66 tall → `halfH 33`).
|
|
106
|
+
The sprite's feet render at `node + (feetFrameY − anchor.y·frameH)·scale`; set that equal
|
|
107
|
+
to the collider bottom (`node + halfH`) and solve:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
anchor.y = (feetFrameY − halfH / scale) / frameH
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
e.g. knight `(136 − 33/0.72) / 192 ≈ 0.46` (NOT the sheet's 0.61). For a colliderLESS
|
|
114
|
+
patroller (a plain Node2D enemy) there's no `halfH` — pick `anchor.y` so the feet render
|
|
115
|
+
on the floor at the y you patrol it at: `anchor.y = (feetFrameY − (floorY − patrolY)/scale) / frameH`.
|
|
116
|
+
|
|
117
|
+
Equivalent fixes if you prefer: nudge the `Skin` down with a `position` offset, or give
|
|
118
|
+
the collider an `offset` so its bottom meets the feet. The anchor route is cleanest.
|
|
119
|
+
ALWAYS confirm in the browser (`bun run dev`) that the feet touch the surface.
|
|
120
|
+
|
|
121
|
+
### `Camera2D`
|
|
122
|
+
| Prop | Default | Notes |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `follow` | `""` | NodePath to track (e.g. `"../Player"`) |
|
|
125
|
+
| `smoothing` | `0` | 0 = snap; 0.85–0.95 = smooth chase (frame-rate independent) |
|
|
126
|
+
| `zoom` | `1` | 2 = world pixels doubled |
|
|
127
|
+
| `limits` | `[]` | `[minX,minY,maxX,maxY]` — view rect clamped inside |
|
|
128
|
+
| `current` | `false` | mark exactly one |
|
|
129
|
+
Camera `position` is the view CENTER. With no camera, the view shows (0,0)–(w,h).
|
|
130
|
+
|
|
131
|
+
### `Label`
|
|
132
|
+
`text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'`.
|
|
133
|
+
CanvasTexture-rendered; mutate `label.text` freely (re-rasterizes only on change).
|
|
134
|
+
|
|
135
|
+
### `UILayer`
|
|
136
|
+
Plain container; everything under it renders in SCREEN space (ignores the camera) —
|
|
137
|
+
HUD, score, menus. Put `Label`s and `Sprite2D`s under it with screen-pixel positions.
|
|
138
|
+
`anchor` (default `top-left`) pins the layer origin to a screen corner/edge/center —
|
|
139
|
+
see "Responsive layout" below.
|
|
140
|
+
|
|
141
|
+
## Responsive layout (viewport + anchors)
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
"viewport": { "design": [960, 540], "fit": "expand" }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Author the WORLD in fixed design pixels — the renderer maps them onto whatever
|
|
148
|
+
canvas size the page provides. Fits: `expand` (the design rect is always fully
|
|
149
|
+
visible; extra world shows beyond it — default), `letterbox` (exactly the design
|
|
150
|
+
rect, centered bars), `integer` (whole-number scaling for pixel art). World
|
|
151
|
+
geometry belongs in design px in the JSON — never hand-place it with
|
|
152
|
+
`window.innerWidth` math in TS.
|
|
153
|
+
|
|
154
|
+
`UILayer.anchor` (`top-left` default · `top` · `top-right` · `left` · `center` ·
|
|
155
|
+
`right` · `bottom-left` · `bottom` · `bottom-right`) keeps HUD coordinates tiny
|
|
156
|
+
offsets that survive any canvas size: a `Label` at `[-16, 16]` under a
|
|
157
|
+
`top-right` layer hugs the corner everywhere. Misspelled anchors fail at load
|
|
158
|
+
listing the valid set. With a viewport design, UI coordinates are design px.
|
|
159
|
+
|
|
160
|
+
## Patterns
|
|
161
|
+
|
|
162
|
+
- Group pickups/enemies (`"groups": ["coins"]`) and query `scene.tree.getNodesInGroup('coins')`.
|
|
163
|
+
- Destroy with `node.queueFree()` (safe mid-update; flushed at end of frame).
|
|
164
|
+
- Sound: an `AudioPlayer` node — zero-asset procedural SFX via `preset`
|
|
165
|
+
(coin/jump/hurt/explosion/…) OR an audio file via `src` (`volume`/`loop`/`autoplay`;
|
|
166
|
+
`play()`/`stop()`, `finished` signal). Global volume via `engine.audio`
|
|
167
|
+
(master/sfx/music + mute). Browsers gate audio behind a gesture; `createGame2D`
|
|
168
|
+
retries blocked plays on the first pointerdown. Headless-safe no-op. Full guide:
|
|
169
|
+
**incanto-audio.md**.
|
|
170
|
+
- Reference example: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) —
|
|
171
|
+
spritesheet character with a walk/jump/attack behavior state machine.
|
|
172
|
+
- Pointer→world mapping (the InputMap covers keyboard actions; pointer projection is
|
|
173
|
+
this one-liner):
|
|
174
|
+
`world = camera.clampedCenter(w,h) + (pointer - viewport/2) / camera.zoom`.
|
|
175
|
+
|
|
176
|
+
## Game flow recipes
|
|
177
|
+
|
|
178
|
+
- **Scene transition** (level 2, title → game):
|
|
179
|
+
```ts
|
|
180
|
+
import { loadScene } from 'incanto';
|
|
181
|
+
game.engine.setScene(loadScene(level2Json));
|
|
182
|
+
```
|
|
183
|
+
`setScene` frees the old root, clears + redeclares the input map from the new
|
|
184
|
+
scene's `input{}`, and emits `sceneChanged` — `createGame2D`'s touch overlay
|
|
185
|
+
rebuilds itself on that signal, and the renderer loads the new scene's assets
|
|
186
|
+
on demand. Register any extra behaviors/node types BEFORE the `loadScene` call.
|
|
187
|
+
- **Game over / restart**: swap to a fresh load of the SAME JSON —
|
|
188
|
+
`engine.setScene(loadScene(levelJson))`. `loadScene` treats the JSON as
|
|
189
|
+
read-only (everything it keeps is cloned), so reloading the same imported
|
|
190
|
+
object yields a clean run; only clone (`structuredClone(levelJson)`) first if
|
|
191
|
+
your own code mutated that object. (`createGame`'s `scene` option already
|
|
192
|
+
structuredClones internally.)
|
|
193
|
+
- **Pause / resume**: `engine.stop()` halts the loop (clock and accumulator
|
|
194
|
+
reset — no banked time on resume); `engine.start()` resumes.
|
|
195
|
+
- **Save / load** (a behavior, localStorage):
|
|
196
|
+
```ts
|
|
197
|
+
localStorage.setItem('save', JSON.stringify({ score: this.score }));
|
|
198
|
+
const save = JSON.parse(localStorage.getItem('save') ?? '{}');
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Rendering crispness
|
|
202
|
+
|
|
203
|
+
`Renderer2D` defaults MATCH PHASER: `antialias: true`, `pixelRatio: 1` (the
|
|
204
|
+
browser upscales on hiDPI screens — edges read soft). Texture sampling is
|
|
205
|
+
linear by default; declare `"filter": "nearest"` on a texture/spritesheet
|
|
206
|
+
asset for pixel art. For crisp retina rendering pass
|
|
207
|
+
`pixelRatio: window.devicePixelRatio` to `createGame2D` (or the `Renderer2D`
|
|
208
|
+
constructor).
|
|
209
|
+
|
|
210
|
+
## Particles (effects in one node)
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{ "name": "Torch", "type": "Particles2D", "props": { "preset": "fire" } }
|
|
214
|
+
{ "name": "Boom", "type": "Particles2D",
|
|
215
|
+
"props": { "preset": "explosion", "rate": 0, "burst": 60 } }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Presets: `fire smoke sparks fireworks explosion flash lightning rain snow magic`
|
|
219
|
+
(unknown names fail at load listing these). The preset seeds every value;
|
|
220
|
+
any prop you write overrides it (`colorStart`, `colorEnd`, `speed: [min,max]`,
|
|
221
|
+
`directionDeg` (-90 = up), `spreadDeg`, `gravity`, `sizeStart/End`,
|
|
222
|
+
`alphaStart/End`, `blend: "add"|"normal"`, `maxParticles`). Two props add
|
|
223
|
+
per-particle variety (each keyed off the particle's stable random seed, so it
|
|
224
|
+
never flickers): **`paletteColors: ["#hex", …]`** — when set, every particle
|
|
225
|
+
picks ONE palette colour at birth (multi-colour confetti) instead of the single
|
|
226
|
+
`colorStart`→`colorEnd` ramp; **`shimmer`** (Hz, 0 = off) — a per-particle alpha
|
|
227
|
+
twinkle so sparks glitter on their own rhythm. Both work in 2D and 3D and apply
|
|
228
|
+
to any preset (fire embers, magic shimmer, multi-colour fireworks). **Blend vs
|
|
229
|
+
backdrop:** `blend: "add"` GLOWS but only over a DARK backdrop (night sky, caves)
|
|
230
|
+
— over a BRIGHT scene (daylight grass) additive washes toward white and the
|
|
231
|
+
sparks nearly vanish. For colourful particles on a bright scene use
|
|
232
|
+
`blend: "normal"` (OPAQUE saturated dots that read on anything) with biggish
|
|
233
|
+
sizes (small soft sprites read as pale haloes — the bigger the dot, the more its
|
|
234
|
+
solid core carries the colour). `rate: 0` +
|
|
235
|
+
`burst: N` makes a one-shot that emits `finished` — connect it to clean up:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{ "signal": "finished", "from": "Boom", "to": ".", "handler": "onBoomDone" }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`node.replay()` re-arms a one-shot. Simulation is deterministic under the
|
|
242
|
+
engine seed — runScript verifies particle gameplay reproducibly.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-building-3d-games
|
|
3
|
+
description: Build 3D web games with Incanto — Node3D taxonomy, scene environment, Engine + Renderer3D setup. Use when creating or modifying 3D scenes, meshes, cameras, or lights.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building 3D Games with Incanto
|
|
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
|
+
Prerequisite: read `incanto-scene-json-authoring.md` (this directory) for the scene file format, node paths, and
|
|
12
|
+
connections. This skill covers the 3D node taxonomy and runtime setup.
|
|
13
|
+
|
|
14
|
+
## Model files
|
|
15
|
+
|
|
16
|
+
GLB/glTF/VRM files load via `ModelInstance3D` — see `incanto-3d-models.md` for the
|
|
17
|
+
asset declaration, `targetHeight` sizing, and the `bunx incanto-model` inspector that
|
|
18
|
+
reports a file's real bounding box and animation names.
|
|
19
|
+
|
|
20
|
+
## Boot sequence (the only TypeScript you need to start)
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createGame3D } from 'incanto/3d';
|
|
24
|
+
import sceneJson from './my.scene.json';
|
|
25
|
+
|
|
26
|
+
const game = await createGame3D({
|
|
27
|
+
canvas: document.querySelector('canvas')!,
|
|
28
|
+
scene: sceneJson, // cloned internally — no cast, no structuredClone
|
|
29
|
+
pointer: true, // pointer-look + lock-on-click (FPS pattern); default off
|
|
30
|
+
// behaviors: { ... }, // registered for you (hot-replace tolerant)
|
|
31
|
+
});
|
|
32
|
+
// game.engine / game.scene / game.renderer / game.physics
|
|
33
|
+
// game.dispose() — ONE call tears everything down (SPA unmount)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
One call does register → load → physics → input → renderer → start. Physics is
|
|
37
|
+
`'auto'`: Rapier is enabled iff the tree has physics bodies (force with
|
|
38
|
+
`physics: true|false`). Keyboard attaches to `window` (`keyboard: false`
|
|
39
|
+
disables); `pointer: { lockOnClick: false }` for look without lock. `seed` /
|
|
40
|
+
`fixedHz` / `pixelRatio` pass through. The manual `Engine` + `loadScene` +
|
|
41
|
+
`Renderer3D` boot still exists for full control.
|
|
42
|
+
|
|
43
|
+
Imperative per-frame logic can hang off `game.engine.updated.connect((dt) => { ... })`; for
|
|
44
|
+
structured game logic attach Behaviors to nodes (see `incanto-behaviors-and-scripts.md`).
|
|
45
|
+
|
|
46
|
+
Game flow (scene transitions, restart, pause, save) works identically in 3D —
|
|
47
|
+
see "Game flow recipes" in `incanto-building-2d-games.md`.
|
|
48
|
+
|
|
49
|
+
## 3D node taxonomy
|
|
50
|
+
|
|
51
|
+
All 3D nodes extend `Node3D` and therefore have the transform props:
|
|
52
|
+
|
|
53
|
+
| Prop | Default | Notes |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `position` | `[0,0,0]` | meters; +Y up |
|
|
56
|
+
| `rotation` | `[0,0,0]` | **degrees**, Euler XYZ |
|
|
57
|
+
| `scale` | `[1,1,1]` | |
|
|
58
|
+
| `visible` | `true` | hides the whole subtree's rendering |
|
|
59
|
+
|
|
60
|
+
### `MeshInstance3D`
|
|
61
|
+
| Prop | Default | Notes |
|
|
62
|
+
|---|---|---|
|
|
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
|
+
| `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, emissive, emissiveIntensity, map, normalMap, repeat: [u,v]}` — the object IS the material state: omitted keys reset to defaults (`#ffffff`, 0, 1, 1, false, false). `opacity < 1` turns on transparency; `flatShading` gives faceted per-face glints (the gem sparkle). `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 |
|
|
66
|
+
| `castShadow` / `receiveShadow` | `false` | |
|
|
67
|
+
|
|
68
|
+
### `Camera3D`
|
|
69
|
+
`fov: 60`, `near: 0.1`, `far: 1000`, `current: false`.
|
|
70
|
+
Mark exactly ONE camera `current: true` (otherwise the first camera in tree order is used).
|
|
71
|
+
Aspect ratio is automatic. To frame an origin-centered scene, a good default is
|
|
72
|
+
`position: [7, 6, 9]`, `rotation: [-26, 36, 0]`.
|
|
73
|
+
|
|
74
|
+
### Lights
|
|
75
|
+
- `DirectionalLight3D` — `color: '#ffffff'`, `intensity: 1`, `castShadow: false`.
|
|
76
|
+
Sun-style; its `position` sets the incoming direction (it aims at the origin).
|
|
77
|
+
Shadow knobs: `shadowArea` (half-extent of the shadow box, default ±75),
|
|
78
|
+
`shadowMapSize` (default 2048), and `shadowFollowsCamera` (default false). In a
|
|
79
|
+
big roaming world a fixed shadow box must be huge → low-res → the player's own
|
|
80
|
+
shadow nearly vanishes; set `shadowFollowsCamera: true` with a SMALL `shadowArea`
|
|
81
|
+
(~60–80) so the box tracks the camera and contact shadows stay crisp everywhere.
|
|
82
|
+
- `OmniLight3D` — `color`, `intensity` (physical units — values like 20–40 are normal),
|
|
83
|
+
`range` (0 = unlimited).
|
|
84
|
+
|
|
85
|
+
### Vegetation & water
|
|
86
|
+
|
|
87
|
+
`Foliage3D` (curved-blade shader grass: clumps, rolling wind, distance LOD,
|
|
88
|
+
character bend), `Flowers3D` (instanced flower PLANTS — stems, leaves,
|
|
89
|
+
multi-petal heads; lush/sparse/none density dial, Voronoi patches),
|
|
90
|
+
`Tree3D` (ez-tree
|
|
91
|
+
groves: branchy trunks + textured leaf billboards — conifer/broadleaf/dead ×
|
|
92
|
+
simple/medium/high cost tiers, count×tris budget-checked at load) and
|
|
93
|
+
`Water3D` (animated plane) dress a level in a handful of nodes. Full prop
|
|
94
|
+
tables and the generator CLI: `incanto-environment.md` (this directory).
|
|
95
|
+
|
|
96
|
+
### Scene-level `environment` header (not a node)
|
|
97
|
+
```json
|
|
98
|
+
"environment": {
|
|
99
|
+
"ambient": { "color": "#ffffff", "intensity": 0.45 },
|
|
100
|
+
"background": "#0e1220"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
Without `ambient`, unlit faces are pitch black — almost always set it (0.3–0.6;
|
|
104
|
+
drop to ~0.15–0.2 when a `sky` provides image-based ambience — see the
|
|
105
|
+
atmosphere recipe below).
|
|
106
|
+
|
|
107
|
+
## Composition rules
|
|
108
|
+
|
|
109
|
+
- Group moving parts under a plain `Node3D` pivot and rotate the pivot — children inherit
|
|
110
|
+
the transform (a spinning `Pivot` parent makes child meshes orbit — no math in scripts).
|
|
111
|
+
- Plain `Node` (non-3D) children are fine anywhere; they're transparent to rendering
|
|
112
|
+
(their 3D descendants attach to the nearest `Node3D` ancestor).
|
|
113
|
+
- Mutate node props (plain JSON values) — never touch three.js objects. The renderer
|
|
114
|
+
pushes props each frame. Replace arrays whole (`node.rotation = [0, a, 0]`) or mutate
|
|
115
|
+
in place; both work.
|
|
116
|
+
|
|
117
|
+
## Reference example
|
|
118
|
+
|
|
119
|
+
A minimal 3D scene — ground plane + 3 orbiting primitives + sun +
|
|
120
|
+
lamp + camera, verified rendering. Copy it as a starting point.
|
|
121
|
+
|
|
122
|
+
## Atmosphere (sky, fog, shadows, exposure)
|
|
123
|
+
|
|
124
|
+
The whole rendering stage is scene JSON — this is THE recipe that turns a flat
|
|
125
|
+
demo into a commercial-looking outdoor scene:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"environment": {
|
|
129
|
+
"sky": { "type": "atmosphere", "elevationDeg": 35, "azimuthDeg": 140, "turbidity": 2.4 },
|
|
130
|
+
"fog": { "near": 120, "far": 700 },
|
|
131
|
+
"shadows": true,
|
|
132
|
+
"exposure": 1,
|
|
133
|
+
"ambient": { "color": "#ffffff", "intensity": 0.18 },
|
|
134
|
+
"background": "#9bc4e2"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- `sky` (physical atmosphere, three Sky shader): sun via `elevationDeg`
|
|
139
|
+
(−90..90 above horizon) + `azimuthDeg` (0 = +Z, 90 = +X) **or** a raw
|
|
140
|
+
`sunPosition: [x, y, z]` — never both. `turbidity` (haze, 2 ≈ clear day,
|
|
141
|
+
~7 dusty) and `rayleigh` (blue scattering, 1 ≈ earth). Defaults are a
|
|
142
|
+
pleasant mid-morning. The sky does three jobs at once: backdrop, PBR
|
|
143
|
+
image-based ambience (PMREM, regenerated only when the sky changes), and the
|
|
144
|
+
scene's unified SUN — it aims the main `DirectionalLight3D` (highest
|
|
145
|
+
intensity; lights are never auto-created) and feeds `Water3D` /
|
|
146
|
+
`Foliage3D` `sunDirection` uniforms while those props sit at their defaults
|
|
147
|
+
(an explicitly authored prop wins).
|
|
148
|
+
- `fog`: linear `THREE.Fog` — `{ color?, near?, far? }` meters. Without
|
|
149
|
+
`color` under a sky, a horizon-ish haze is derived from the sky itself.
|
|
150
|
+
Water and mesh-grass shaders fog identically to standard materials.
|
|
151
|
+
- `clouds`: a VOLUMETRIC cloud deck — the renderer raymarches a world-space
|
|
152
|
+
altitude slab and composites real, parallax-correct clouds you fly through
|
|
153
|
+
(a fullscreen pass, like the underwater caustics — no node, no geometry).
|
|
154
|
+
`{ coverage?, density?, base?, top?, color?, shadeColor?, speed?, scale? }`:
|
|
155
|
+
`coverage` 0..1 (how much sky is cloudy, ~0.5), `density` optical thickness
|
|
156
|
+
(~1–2.2), `base`/`top` the deck's world-Y band (e.g. 95→340 — cruise above
|
|
157
|
+
it and dive through), `color` sunlit + `shadeColor` shaded underside, `speed`
|
|
158
|
+
wind drift, `scale` feature size in world units (~230, bigger = larger soft
|
|
159
|
+
masses). Lit by the scene sun; melts into the `fog` color at distance. COST:
|
|
160
|
+
one extra fullscreen pass per frame — keep `Water3D` at `quality:"simple"`
|
|
161
|
+
(zero re-renders) when pairing with clouds to hold 60fps. Absent = zero cost.
|
|
162
|
+
Ideal for flight/sky scenes; see `examples/dogfight-3d`.
|
|
163
|
+
- `shadows`: `true` | `false` | `{ "mapSize": 1024|2048, "radius": n }`.
|
|
164
|
+
Truthy makes the main DirectionalLight cast (PCF soft, terrain + trees +
|
|
165
|
+
meshes cast/receive, mesh grass RECEIVES only). One static ortho box (±75 m
|
|
166
|
+
default; lights with `castShadow`+`shadowArea` keep their own) — no
|
|
167
|
+
cascaded shadow maps yet, so giant worlds get soft, not razor, shadows.
|
|
168
|
+
`false` force-disables shadow maps.
|
|
169
|
+
- `exposure`: ACES filmic tone mapping is always on (exposure 1 default) —
|
|
170
|
+
raise/lower for bright beach vs moody dusk.
|
|
171
|
+
- `iblIntensity`: multiplies the sky's image-based ambient fill (≥0, default
|
|
172
|
+
1; the sky base is 0.55). Lower it (~0.5) for a hot-sun-vs-small-ambient
|
|
173
|
+
stage that carves a deep shadow side on crowns — the "moody" look — while
|
|
174
|
+
a higher exposure keeps the lit side bright.
|
|
175
|
+
- All keys hard-validate at apply time with errors listing valid options;
|
|
176
|
+
scenes without them render exactly as before (flat `background`, no fog).
|
|
177
|
+
- Sun balance that reads well under ACES + sky ambience: sun intensity
|
|
178
|
+
~1.5–2, ambient ~0.15–0.2 (a strong flat ambient washes shadows out).
|
|
179
|
+
- `runGenerator('terrain', …)` worlds: pair with `terrainEnvironment(theme)`
|
|
180
|
+
from `incanto/env` — the themed sky/fog/shadows header in one call.
|
|
181
|
+
- `runGenerator('maze', …)` likewise pairs with `mazeEnvironment(theme)` —
|
|
182
|
+
a moody low-sun/fog grade (dungeon stone, overcast hedge, dusk canyon).
|
|
183
|
+
|
|
184
|
+
## HDRI environments (image-based lighting)
|
|
185
|
+
|
|
186
|
+
`environment.preset: "sunset"` (any drei preset name: apartment/city/dawn/
|
|
187
|
+
forest/lobby/night/park/studio/sunset/warehouse) or `environment.hdri: <url>`
|
|
188
|
+
loads an equirect HDR for lighting/reflections; add `"skybox": true` to also
|
|
189
|
+
show it as the background (otherwise `background` color applies). Standard
|
|
190
|
+
materials need this OR explicit lights to be visible. An explicit HDRI wins
|
|
191
|
+
over the `sky`'s image-based ambience.
|
|
192
|
+
|
|
193
|
+
## Preloading with a progress bar
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { assetUrls, preloadUrls } from 'incanto';
|
|
197
|
+
await preloadUrls(assetUrls(sceneJson.assets), (done, total) =>
|
|
198
|
+
bar.style.width = `${(done / total) * 100}%`);
|
|
199
|
+
```
|
|
200
|
+
Warms the HTTP cache before the engine starts; failures warn, never block.
|
|
201
|
+
|
|
202
|
+
## Particles
|
|
203
|
+
|
|
204
|
+
`Particles3D` mirrors `Particles2D` (same presets — `fire`, `explosion`,
|
|
205
|
+
`magic`… and the same one-shot `rate: 0` + `burst` → `finished` pattern) as
|
|
206
|
+
point sprites in meters: preset distances auto-scale ÷100 (presets are authored
|
|
207
|
+
in 2D px). See incanto-building-2d-games for the full prop list.
|
|
208
|
+
|
|
209
|
+
Points render through a built-in soft sprite with a SOLID opaque core (not hard
|
|
210
|
+
squares), so colours read even at small sizes, and `'normal'` blend fades cleanly
|
|
211
|
+
to transparent (per-particle RGBA alpha). `paletteColors` (per-particle
|
|
212
|
+
multi-colour) + `shimmer` (per-particle twinkle) work in 3D too — see the 2D skill.
|
|
213
|
+
|
|
214
|
+
- **Size gotcha:** on-screen size is `(sizeStart + sizeEnd) / 2 ÷ 100` meters
|
|
215
|
+
(sizeAttenuation), so the 2D-px `custom` defaults (8/2 → 0.05 m) are
|
|
216
|
+
near-invisible a few meters away — author sizes in the TENS (e.g. 64/14 → ~0.4 m).
|
|
217
|
+
- **Blend vs backdrop:** `'add'` only GLOWS over a DARK backdrop; over a BRIGHT
|
|
218
|
+
scene (daylight grass) it washes to white — use `'normal'` (opaque) for colourful
|
|
219
|
+
daytime particles.
|
|
220
|
+
- **VFX over grass / transparent scenery — raise `renderOrder` (e.g. 100).**
|
|
221
|
+
Foliage3D grass is a TRANSPARENT material (renderOrder ~2) drawn LATER in the
|
|
222
|
+
transparent pass, so a default particle (renderOrder 0) gets painted OVER even
|
|
223
|
+
when geometrically ABOVE the blades — it's draw ORDER, not depth (so toggling
|
|
224
|
+
`depthTest` does nothing about it). `renderOrder:100` draws the VFX after the
|
|
225
|
+
grass; that alone fixes it. Leave `depthTest` at its default `true` so the VFX is
|
|
226
|
+
still correctly hidden by SOLID objects (tree trunks, the player). Only set
|
|
227
|
+
`depthTest:false` for a deliberately always-on-top effect (it then draws through
|
|
228
|
+
solid cover too).
|
|
229
|
+
|
|
230
|
+
A celebratory FIREWORK: a few stacked one-shots choreographed by their lifetimes —
|
|
231
|
+
a brief BIG white flash (`blend:'add'`, the pop), a multi-jewel confetti shell
|
|
232
|
+
(`blend:'normal', burst:60, speed:[150,360], drag:1.9, sizeStart:96/sizeEnd:22,
|
|
233
|
+
paletteColors:[…saturated…], shimmer:8`), a lingering ember glitter
|
|
234
|
+
(`blend:'normal', gravity:[0,0], shimmer:5`) — all bursting UP (`directionDeg:-90,
|
|
235
|
+
spreadDeg:180`) with `renderOrder:100`, each parented to a stable
|
|
236
|
+
node (NOT the collectible's container), self-removing on `finished`.
|
|
237
|
+
|
|
238
|
+
## Terrain
|
|
239
|
+
|
|
240
|
+
`Terrain3D` is procedural heightfield terrain with biome texture splatting —
|
|
241
|
+
one node, themed (`island`/`alpine`/`plains`/`desert`/`custom`), deterministic
|
|
242
|
+
from a `seed`, with `heightAt(x, z)` for spawning/placement and a
|
|
243
|
+
`{shape: 'heightfield'}` collider recipe (Terrain3D as the CHILD of a
|
|
244
|
+
`StaticBody3D`). Full themes table, custom layer format and the water/beach
|
|
245
|
+
pairing live in the **incanto-environment** skill.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-editor
|
|
3
|
+
description: The visual scene composer shipped in the incanto package — bunx incanto-editor serves a page that renders a scene with the installed engine, lets you compose nodes and edit every prop, saves back to the file, and speaks a postMessage protocol for iframe embedding. Use when the user wants to visually arrange or tweak a scene, or when integrating the editor into a host service.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Scene Editor
|
|
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
|
+
## Launch
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bunx incanto-editor # PROJECT mode: discovers every *.scene.json
|
|
15
|
+
bunx incanto-editor src/game.scene.json # SINGLE mode: one fixed file
|
|
16
|
+
bunx incanto-editor level.scene.json --output build/level.scene.json --port 5179 --host 0.0.0.0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Project mode** (no file argument — the usual way) scans the current directory for
|
|
20
|
+
`*.scene.json` (skipping node_modules/dist/hidden dirs): one scene auto-opens; several
|
|
21
|
+
show a picker; the picker can also CREATE a new scene (`levels/arena.scene.json` →
|
|
22
|
+
minimal 2D scene, parent dirs made). A `scenes` button switches files anytime. The page
|
|
23
|
+
can only ever address `*.scene.json` files under the launch directory.
|
|
24
|
+
|
|
25
|
+
Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
26
|
+
|
|
27
|
+
- **Explorer** — two collapsible sections: **ASSETS** on top (icon rows by
|
|
28
|
+
type; keys with a `group/` prefix nest under collapsible folders (any depth),
|
|
29
|
+
each showing its recursive asset count — refs are `$group/key`; click the
|
|
30
|
+
icon for a blurb, the row to edit in the inspector; DRAG asset rows onto a
|
|
31
|
+
folder (or the section background = root) to move them — references rewrite
|
|
32
|
+
automatically, as they do when a group is renamed; the folder button creates
|
|
33
|
+
a group, and deleting a non-empty group asks first) and the
|
|
34
|
+
**SCENE tree** below — type icons left of each name (click one for a balloon
|
|
35
|
+
explaining the type), chevron collapse/expand with depth guide lines,
|
|
36
|
+
ctrl/cmd-click **multi-select**, and **drag-and-drop restructuring**: drop ONTO
|
|
37
|
+
a row to make the dragged node(s) its children, drop on a row's top/bottom edge
|
|
38
|
+
to reorder before/after. Dragging a selected row moves the whole selection.
|
|
39
|
+
Illegal drops (engine rules — e.g. a CharacterController2D outside a
|
|
40
|
+
CharacterBody2D) are ROLLED BACK entirely with the error in the banner; the
|
|
41
|
+
tree never shows a state the engine would reject. Right-click for
|
|
42
|
+
duplicate / rename (or double-click the name) / cut / copy / paste-as-child /
|
|
43
|
+
delete — all act on the multi-selection. `+` adds a child of any registered
|
|
44
|
+
type, `✕` deletes the selection. Selecting the ⚙ scene row edits the header
|
|
45
|
+
(dimension, gravity with real inputs, environment/input/assets/multiplayer).
|
|
46
|
+
- **Viewport with DIRECT MANIPULATION** — a mode toolbar (top left) + the
|
|
47
|
+
Unity/Godot keys: **W move · E rotate · R scale** (active mode highlighted).
|
|
48
|
+
A collider-visibility toggle sits beside the mode tools: light-green
|
|
49
|
+
wireframes for every body (2D shapes AND 3D box/sphere/capsule), persisted,
|
|
50
|
+
and carried into play mode as the engine's physics debugDraw.
|
|
51
|
+
Move: axis arrows (X red, Y green, Z blue in 3D) + center square for free
|
|
52
|
+
movement. Rotate: drag the ring(s) — one ring in 2D, three world-axis rings
|
|
53
|
+
in 3D — with a live degree readout at the cursor. Scale: the same arrows with
|
|
54
|
+
cube tips (center = uniform); right-dragging a handle scales in any mode.
|
|
55
|
+
**Esc cancels the drag in progress** and reverts the value; **hold Shift to
|
|
56
|
+
SNAP** — rotations to 15°, positions to the grid (10 px in 2D, 0.5 u in 3D),
|
|
57
|
+
scales to 0.25 steps (the readout shows the snapped value).
|
|
58
|
+
2D additionally supports click-pick, body drag, wheel zoom-at-cursor,
|
|
59
|
+
right/middle/Shift-drag pan, Alt+wheel scale, and collider wireframes;
|
|
60
|
+
`F` frames the scene in both dimensions. The add-node dropdown is grouped by
|
|
61
|
+
category (Core / 2D / 2D Physics / 3D / 3D Physics / Network, plus a trailing
|
|
62
|
+
Other catch-all so an unclaimed type can never vanish from the list). The
|
|
63
|
+
dropdown, inspector and in-editor docs are registry/schema-driven — newly
|
|
64
|
+
registered node types appear automatically. Edits re-validate through the real
|
|
65
|
+
loader: hard `IncantoError`s appear in a banner. Validation is STRUCTURE-level —
|
|
66
|
+
physics semantics are checked when physics actually runs.
|
|
67
|
+
- **Inspector** — schema-driven from the node registry, with STRUCTURED editors for
|
|
68
|
+
the hard parts: `collider` (shape dropdown + per-shape dimensions, mirrored by
|
|
69
|
+
the wireframe), `network` (mode dropdown + sync-key chips + throttle),
|
|
70
|
+
`script` (attach/detach + name + props, with copy-paste Behavior boilerplate in
|
|
71
|
+
its help), `groups` (tag chips). Every one has a `?` help popover with examples.
|
|
72
|
+
Values equal to the default are removed (delta-only, like the serializer).
|
|
73
|
+
|
|
74
|
+
**3D scenes** get full camera navigation: drag orbits, right/middle/Shift-drag pans,
|
|
75
|
+
wheel zooms, `F` frames the contents, and **`0` / the `game cam` button** returns to the
|
|
76
|
+
GAME's own camera — exactly what running the scene shows (the free camera re-syncs so
|
|
77
|
+
the next drag continues from there). A clickable ORIENTATION GIZMO (top right): the filled
|
|
78
|
+
X/Y/Z handles snap to right/top/front views, hollow ones to the opposite sides. The
|
|
79
|
+
hint bar swaps to the camera controls. AMBIENT VISUALS preview LIVE while logic time
|
|
80
|
+
stays frozen: ModelInstance3D plays its `animation`, Particles2D/Particles3D emit,
|
|
81
|
+
Water3D waves bob, Foliage3D sways — behaviors, physics and timers still wait for
|
|
82
|
+
play. The inspector suggests model/animation values from the scene's assets (plus
|
|
83
|
+
embedded clip names).
|
|
84
|
+
|
|
85
|
+
**▶ Play** runs the scene AS COMPOSED in the same pane: physics enabled, keyboard
|
|
86
|
+
attached, CharacterController2D and every engine-native feature live — tune a
|
|
87
|
+
collider or gravity, hit play, feel it, Esc back, keep editing. Game `script`s and
|
|
88
|
+
their signal wiring run only in your real game (a notice says so while simulating).
|
|
89
|
+
|
|
90
|
+
A **docs** button opens the in-editor node documentation: an overview of how
|
|
91
|
+
Incanto scenes work plus a tab per category with a detailed entry and live
|
|
92
|
+
props table for every node type — in English AND Korean (the 한/EN toggle also
|
|
93
|
+
localizes the inspector's help popovers and type balloons; values stay English).
|
|
94
|
+
|
|
95
|
+
Node types are FIXED once created (retyping would smuggle children past
|
|
96
|
+
parent rules) — but the ROOT is deletable like any node: the scene goes
|
|
97
|
+
empty and the next node added becomes the new root. The add-node dropdown
|
|
98
|
+
leads with the scene's dimension (3D defaults to MeshInstance3D) and
|
|
99
|
+
DISABLES types the engine would reject at the current target — probed by
|
|
100
|
+
validating a candidate scene, so any loader rule (e.g. CharacterController2D
|
|
101
|
+
needs a CharacterBody2D parent) is honored automatically, for children and
|
|
102
|
+
for root candidates alike.
|
|
103
|
+
|
|
104
|
+
Every node carries an immutable `uid` (assigned at creation; legacy scenes are
|
|
105
|
+
backfilled when opened) shown read-only above the name — click to copy. Saved
|
|
106
|
+
files are formatted biome-compatibly (80-col inline-when-fits), so saving from
|
|
107
|
+
the editor never breaks `pnpm lint`.
|
|
108
|
+
|
|
109
|
+
Save activates only when the working scene differs from the original
|
|
110
|
+
(`Ctrl/Cmd+S`); undo is `Ctrl/Cmd+Z`. Saving writes pretty-printed JSON to
|
|
111
|
+
`--output` (default: the input file).
|
|
112
|
+
|
|
113
|
+
## Generate environments
|
|
114
|
+
|
|
115
|
+
The **✦ button** beside the add-node controls opens the Generate dialog — the
|
|
116
|
+
`incanto/env` generators inside the editor, driven by the same `GENERATORS`
|
|
117
|
+
catalog as the `incanto-env` CLI and filtered to the open scene's dimension
|
|
118
|
+
(3D: arena, terrain, meadow, forest, maze, rocks, clouds, island; 2D:
|
|
119
|
+
platforms2d, maze2d, dungeon2d). The param form is built from the catalog
|
|
120
|
+
metadata — numbers clamp to their min/max, option lists become dropdowns — so
|
|
121
|
+
a new generator needs zero editor changes. The seed starts random (↻ rerolls);
|
|
122
|
+
the same seed always generates the same level. **insert** runs the generator
|
|
123
|
+
and splices the subtree under the selected node (the root when nothing is
|
|
124
|
+
selected) through the normal validated path: sibling names uniquify, every
|
|
125
|
+
node gets a fresh uid, the whole insert is ONE undo step, and the new subtree
|
|
126
|
+
arrives selected in the tree. Esc / backdrop / ✕ dismiss the dialog.
|
|
127
|
+
|
|
128
|
+
## Embedding (host frontends / agent8 containers)
|
|
129
|
+
|
|
130
|
+
Run the CLI inside the container (`--host 0.0.0.0`), iframe the served URL — the same
|
|
131
|
+
flow as a build preview. **The iframe URL must carry the host's origin**:
|
|
132
|
+
|
|
133
|
+
```html
|
|
134
|
+
<iframe src="http://<container>:5179/?parentOrigin=https://your.app"></iframe>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Messages are posted ONLY to that pinned origin (never `*`); without the param an
|
|
138
|
+
embedded editor posts nothing. The page posts to its parent window:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
{ type: 'incanto-editor:ready', input, output, version } // input/output null until a scene opens (project mode)
|
|
142
|
+
{ type: 'incanto-editor:open', input, output } // a scene was opened/switched (project mode)
|
|
143
|
+
{ type: 'incanto-editor:change', dirty } // Save-button state
|
|
144
|
+
{ type: 'incanto-editor:save', input, output, data } // data = full scene JSON
|
|
145
|
+
{ type: 'incanto-editor:error', message } // loader/save errors
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The page can never choose filesystem paths — `input`/`output` are fixed at launch;
|
|
149
|
+
the API only reads the input and writes the output. Saves are rejected (422, file
|
|
150
|
+
untouched) unless the body is scene-shaped (`format: 1`, `type: "scene"`, `name`,
|
|
151
|
+
`root`). On the default loopback bind, non-loopback `Host` headers are rejected
|
|
152
|
+
(DNS-rebinding defense) and cross-origin writes are refused; `--host 0.0.0.0`
|
|
153
|
+
trusts the surrounding network — use it only inside containers.
|
|
154
|
+
|
|
155
|
+
## Scope
|
|
156
|
+
|
|
157
|
+
The EDIT view freezes game time — nothing falls or fires until you press play;
|
|
158
|
+
only ambient visuals (model animations, particles, water, foliage sway) keep
|
|
159
|
+
moving. The PLAY view simulates everything engine-native but cannot execute the
|
|
160
|
+
game's TypeScript behaviors. 3D scenes render and edit via the inspector; direct
|
|
161
|
+
viewport manipulation is 2D-only for now. Runtime-injected textures (asset URLs
|
|
162
|
+
like `"GENERATED_AT_RUNTIME"`) render as a magenta checkerboard — position/size
|
|
163
|
+
stay visible; the real art appears in the running game.
|