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,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.