incanto 0.61.0 → 0.63.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 (54) hide show
  1. package/bin/incanto-verify.mjs +134 -55
  2. package/dist/2d.d.ts +42 -0
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +22 -0
  5. package/dist/3d.js +5 -5
  6. package/dist/{create-game-DH7JI5xx.js → create-game-lLeITaZ5.js} +8 -8
  7. package/dist/{create-game-IX5lEH0P.js → create-game-viBqXUoZ.js} +6 -6
  8. package/dist/{duplicate-CGqAmK2h.js → duplicate-BOOKmkQ7.js} +1 -1
  9. package/dist/editor.js +39 -18
  10. package/dist/{environment-presets-CNxCuhZF.js → environment-presets-QR7_75KJ.js} +5 -3
  11. package/dist/{gameplay-Dtzd2itW.js → gameplay-CuqoHHUB.js} +153 -16
  12. package/dist/gameplay.js +1 -1
  13. package/dist/index.d.ts +92 -1
  14. package/dist/index.js +6 -6
  15. package/dist/{loader-D7jTvDQv.js → loader-zDynoew_.js} +160 -2
  16. package/dist/net.js +1 -1
  17. package/dist/{physics-2d-CBnor8Zf.js → physics-2d-CllJXlic.js} +11 -3
  18. package/dist/{physics-3d-BTUfUSWO.js → physics-3d-BL_pFJ19.js} +123 -17
  19. package/dist/react.js +1 -1
  20. package/dist/{register-DL3izw8j.js → register-1cKM8DEj.js} +60 -9
  21. package/dist/{register-xuSRyD6b.js → register-Btm7_Emq.js} +158 -2
  22. package/dist/{replay-D7-yle3s.js → replay-Bhdntkvq.js} +15 -2
  23. package/dist/{split-screen-5Ban4q4n.js → split-screen-Dx0LvzqS.js} +2 -2
  24. package/dist/{src-DozXvyZS.js → src-BlV9Sv4m.js} +1 -1
  25. package/dist/{teardown-B6rwJOyS.js → teardown-BwhkcNt8.js} +1 -1
  26. package/dist/{test-Dch_7VQD.js → test-Ct1_zf5S.js} +64 -20
  27. package/dist/test.d.ts +41 -0
  28. package/dist/test.js +2 -2
  29. package/dist/vite.js +2 -2
  30. package/editor/assets/{agent8-CF1JL2tR.js → agent8-CLZXBRoM.js} +1 -1
  31. package/editor/assets/{debug-3QzYhOPA.js → debug-eaSKFAKW.js} +1 -1
  32. package/editor/assets/{index-CAD2c5ug.js → index-D66AuRwG.js} +91 -91
  33. package/editor/index.html +1 -1
  34. package/package.json +1 -1
  35. package/schemas/scene.schema.json +91 -0
  36. package/skills/README.md +4 -0
  37. package/skills/incanto-behaviors-and-scripts.md +37 -0
  38. package/skills/incanto-building-2d-games.md +20 -1
  39. package/skills/incanto-building-3d-games.md +1 -1
  40. package/skills/incanto-game-feel.md +3 -2
  41. package/skills/incanto-gameplay-behaviors.md +45 -6
  42. package/skills/incanto-node-reference.md +12 -0
  43. package/skills/incanto-physics-and-input.md +53 -2
  44. package/skills/incanto-scene-json-authoring.md +15 -1
  45. package/skills/incanto-verifying-your-game.md +31 -1
  46. package/skills/incanto-your-first-game.md +237 -0
  47. package/templates-app/beacon-isle-3d/package.json +1 -1
  48. package/templates-app/platformer-2d/package.json +1 -1
  49. package/templates-app/star-survivor/package.json +1 -1
  50. package/templates-app/star-survivor/src/game.scene.json +4 -2
  51. package/templates-app/tps-3d/package.json +1 -1
  52. package/templates-app/tps-3d/src/game.scene.json +3 -2
  53. package/templates-app/tps-3d/verify.ts +38 -0
  54. package/templates-app/village-quest-3d/package.json +1 -1
@@ -147,9 +147,24 @@ const rungs = [];
147
147
  // the legal values sitting one key away in the same object.
148
148
  const broke = out?.results?.find((entry) => entry && entry.ok === false);
149
149
  const detail = broke && (broke.message || broke.code) ? `[${broke.code}] ${broke.message}` : null;
150
+ // What `incanto-check` SAID, beyond whether it passed. The rung read `ok` and
151
+ // dropped `warnings` on the floor, so a scene the child describes as "nothing
152
+ // lights this 3D scene … It will render black" was reported by the headline
153
+ // command as "✓ loads — the scene is legal and its assets resolve". The
154
+ // ladder's own header table names this tool as the one for "the scene will
155
+ // not load, OR RENDERS BLACK".
156
+ const notes = (out?.results ?? []).flatMap((entry) => entry?.warnings ?? []);
157
+ // "and its assets resolve" is a claim, and `missing art` is the check saying
158
+ // it does not. Do not make the claim when the child just contradicted it.
159
+ const missingArt = notes.some((n) => /is not in the project|missing|not found/i.test(String(n)));
150
160
  rungs.push(
151
161
  r.status === 0
152
- ? { name: 'loads', status: 'pass', summary: 'the scene is legal and its assets resolve' }
162
+ ? {
163
+ name: 'loads',
164
+ status: 'pass',
165
+ summary: missingArt ? 'the scene is legal' : 'the scene is legal and its assets resolve',
166
+ ...(notes.length > 0 ? { notes, fix: `incanto-check ${scene}` } : {}),
167
+ }
153
168
  : {
154
169
  name: 'loads',
155
170
  status: 'fail',
@@ -175,62 +190,126 @@ if (rungs[0].status === 'pass') {
175
190
  // walkabout template has no end, and calling that a failure sends its author
176
191
  // hunting a bug that was never there.
177
192
  const noGoal = out && out.declaresWin === false;
178
- // A quest — talk to the NPC, clear the enemies, light the wards cannot be
179
- // finished by a random walker, ever. Reporting that as a FAILED rung means
180
- // the headline command permanently says NOT verified about a correct game,
181
- // which teaches its author to stop reading it. If every run played to the
182
- // end of its budget without erroring, falling or wedging, the rung has not
183
- // failed: it has not measured, and the author's own scripted harness is what
184
- // can judge this game.
185
- // Three of the five outcomes are GAMEPLAY, not defects. `won`, `lost` and
193
+ // Three of the six outcomes are GAMEPLAY, not defects. `won`, `lost` and
186
194
  // `unfinished` all mean the game ran; a random player dying half the time in
187
- // a platformer is the hazards working. The defects are `error` (a behaviour
188
- // threw), `fell` (left the world) and `stuck` (went nowhere), and those are
189
- // what this rung is for.
190
- const PLAYED = new Set(['won', 'lost', 'unfinished']);
191
- const playedOut =
192
- out && (out.runs?.length ?? 0) > 0 && out.runs.every((x) => PLAYED.has(x.outcome));
195
+ // a platformer is the hazards working, and a quest cannot be finished by a
196
+ // random walker ever reporting that as a FAILED rung means the headline
197
+ // command permanently says NOT verified about a correct game, which teaches
198
+ // its author to stop reading it.
199
+ //
200
+ // The other three are DEFECTS: `error` (a behaviour threw), `fell` (left the
201
+ // world) and `stuck` (went nowhere). They are counted BEFORE a branch is
202
+ // chosen, and they used to be counted after — only on the path where a win
203
+ // existed to miss. A scene that declares no win took the `noGoal` branch for
204
+ // ANY mix of outcomes, and that branch's summary was the hard-coded phrase
205
+ // "N runs played without error": an assumption wearing the clothes of a
206
+ // measurement. Measured on the shipped `basic-3d-sideview`, whose player
207
+ // walks off the edge on every seeded run:
208
+ //
209
+ // They used to be counted after, and only on the path where a win existed to
210
+ // miss. A scene that declares no win took the `noGoal` branch for ANY mix of
211
+ // outcomes, and that branch's summary was the hard-coded phrase "N runs
212
+ // played without error" — an assumption wearing the clothes of a
213
+ // measurement. Measured on the shipped `basic-3d-sideview`, whose player
214
+ // walks off the edge on every seeded run:
215
+ //
216
+ // incanto-playtest → ✗ fell in 4/4 (last at y=-48.4)
217
+ // incanto-verify → ? plays — nothing declares a win — 8 runs played
218
+ // without error, and there was no end to reach
219
+ // passes what was measured … VERIFY EXIT=0
220
+ //
221
+ // The child had just printed the defect; the parent reported the opposite of
222
+ // it and exited 0. The three outcomes this rung exists to catch were the
223
+ // three it could not report.
224
+ //
225
+ // `stuck` is the one that needs a fair chance before it counts. It means the
226
+ // player went nowhere, and the player goes nowhere for three different
227
+ // reasons: it is wedged (a defect), there IS no player (`2d-phaser-basic` is
228
+ // a ground, a sprite and a camera — the driver had nobody to move), or the
229
+ // game's own behaviours were never loaded, which is most of what makes a
230
+ // character move. Only the first is this rung's business.
193
231
  const tally = (name) => out?.runs?.filter((x) => x.outcome === name).length ?? 0;
194
- rungs.push(
195
- r.status === 0
196
- ? { name: 'plays', status: 'pass', summary: `${won} of ${total} seeded runs finished it` }
197
- : playedOut
198
- ? {
199
- name: 'plays',
200
- status: 'unmeasured',
201
- summary:
202
- `${total} runs played without reaching a win` +
203
- ` (${[
204
- tally('lost') && `${tally('lost')} lost`,
205
- tally('unfinished') && `${tally('unfinished')} ran out the clock`,
206
- ]
207
- .filter(Boolean)
208
- .join(', ')})`,
209
- fix: 'nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`, or `runScript` from `incanto/test`',
210
- }
211
- : noGoal
212
- ? {
213
- name: 'plays',
214
- status: 'unmeasured',
215
- summary: `nothing declares a win — ${total} runs played without error, and there was no end to reach`,
216
- fix: 'if it is meant to be finishable, emit `won` (GameFlow, ScoreKeeper, or your own behaviour)',
217
- }
218
- : {
219
- name: 'plays',
220
- status: 'fail',
221
- summary:
222
- total > 0
223
- ? `no run finished it (${total} tried)`
224
- : // "could not run" with no reason is the tool doing to its
225
- // reader exactly what this whole ladder exists to prevent:
226
- // reporting a failure it already knows the cause of. The
227
- // child printed one; pass it on.
228
- `the playtest could not run — ${firstLine(r.stderr) || `exit ${r.status}`}`,
229
- fix: behaviors
230
- ? `see which runs stalled and where: \`incanto-playtest ${scene} --behaviors ${behaviors}\``
231
- : `run it with your behaviourswithout them the structure plays and your game logic does not: \`incanto-playtest ${scene} --behaviors src/behaviors.ts\``,
232
- },
233
- );
232
+ const stuckCounts = out?.hasPlayer !== false && Boolean(behaviors);
233
+ const DEFECT = new Set(stuckCounts ? ['error', 'fell', 'stuck'] : ['error', 'fell']);
234
+ const defects = out?.runs?.filter((x) => DEFECT.has(x.outcome)) ?? [];
235
+ const undrivable = total > 0 && defects.length === 0 && tally('stuck') === total;
236
+ const count = (n, word) => n && `${n} ${word}`;
237
+
238
+ if (total === 0) {
239
+ rungs.push({
240
+ name: 'plays',
241
+ status: 'fail',
242
+ // "could not run" with no reason is the tool doing to its reader exactly
243
+ // what this whole ladder exists to prevent: reporting a failure it
244
+ // already knows the cause of. The child printed one; pass it on.
245
+ summary: `the playtest could not run — ${firstLine(r.stderr) || `exit ${r.status}`}`,
246
+ fix: behaviors
247
+ ? `see it directly: \`incanto-playtest ${scene} --behaviors ${behaviors}\``
248
+ : `run it with your behaviours: \`incanto-playtest ${scene} --behaviors src/behaviors.ts\``,
249
+ });
250
+ } else if (defects.length > 0) {
251
+ rungs.push({
252
+ name: 'plays',
253
+ status: 'fail',
254
+ summary:
255
+ `${defects.length} of ${total} runs ended in a defect (` +
256
+ [
257
+ count(tally('fell'), 'fell out of the world'),
258
+ count(tally('error'), 'errored'),
259
+ count(tally('stuck'), 'went nowhere'),
260
+ ]
261
+ .filter(Boolean)
262
+ .join(', ') +
263
+ ')' +
264
+ (won > 0 ? ` ${won} did finish it` : ''),
265
+ fix:
266
+ tally('error') > 0
267
+ ? `read the throw: \`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\`, and \`engine.stats().errors\` in your own harness`
268
+ : tally('fell') > 0
269
+ ? `the player left the world with nothing catching them give the level a floor, walls, or a respawn (\`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\` prints where)`
270
+ : `nothing moved — check the input map and the controller: \`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\``,
271
+ });
272
+ } else if (r.status === 0) {
273
+ rungs.push({
274
+ name: 'plays',
275
+ status: 'pass',
276
+ summary: `${won} of ${total} seeded runs finished it`,
277
+ });
278
+ } else if (undrivable) {
279
+ rungs.push({
280
+ name: 'plays',
281
+ status: 'unmeasured',
282
+ summary:
283
+ out?.hasPlayer === false
284
+ ? `nothing here is drivable — ${total} runs had nobody to move`
285
+ : `${total} runs went nowhere, and your behaviours were not loaded — that is most of what moves a character`,
286
+ fix:
287
+ out?.hasPlayer === false
288
+ ? 'give the player a character controller, the `player` group, or the name Player — otherwise no seeded run can play this'
289
+ : `name them: \`incanto-verify ${scene} --behaviors src/behaviors.ts\``,
290
+ });
291
+ } else if (noGoal) {
292
+ rungs.push({
293
+ name: 'plays',
294
+ status: 'unmeasured',
295
+ // Now this phrase IS measured: `defects.length === 0` got us here.
296
+ summary: `nothing declares a win — ${total} runs played without a defect, and there was no end to reach`,
297
+ fix: 'if it is meant to be finishable, emit `won` (GameFlow, ScoreKeeper, or your own behaviour)',
298
+ });
299
+ } else {
300
+ // A quest — talk to the NPC, clear the enemies, light the wards — cannot be
301
+ // finished by a random walker, ever. Reporting that as a FAILED rung means
302
+ // the headline command permanently says NOT verified about a correct game,
303
+ // which teaches its author to stop reading it.
304
+ rungs.push({
305
+ name: 'plays',
306
+ status: 'unmeasured',
307
+ summary:
308
+ `${total} runs played without reaching a win` +
309
+ ` (${[count(tally('lost'), 'lost'), count(tally('unfinished'), 'ran out the clock')].filter(Boolean).join(', ')})`,
310
+ fix: 'nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`, or `runScript` from `incanto/test`',
311
+ });
312
+ }
234
313
  } else {
235
314
  rungs.push({ name: 'plays', status: 'skipped', summary: 'not run — the scene does not load' });
236
315
  }
package/dist/2d.d.ts CHANGED
@@ -90,6 +90,28 @@ declare class Node2D extends Node {
90
90
  /** @internal Override point. */
91
91
  protected _createObject2D(): Object3D;
92
92
  /** @internal Push JSON props onto the backing object. Called every frame. */
93
+ /**
94
+ * Has this node produced the thing `static: true` is about to freeze?
95
+ *
96
+ * `static` latches after the first sync, and a textured node's first sync
97
+ * happens BEFORE its texture has decoded — `TextureLoader.load()` returns a
98
+ * `Texture` with `image === undefined` and fills it asynchronously, while
99
+ * `Renderer2D.render()` calls `assets.load()` and `syncTree2D()` in the same
100
+ * frame. So the state frozen was `visible = false`, forever. Measured on two
101
+ * identical sprites differing only in `static`:
102
+ *
103
+ * ```
104
+ * frame 1 Backdrop(static) visible: false Control visible: false
105
+ * 600 frames later Backdrop(static) visible: false width 1
106
+ * Control visible: true width 64
107
+ * ```
108
+ *
109
+ * Nothing throws: the texture loaded fine, so `assetErrors()` is empty,
110
+ * `stats().errors` is 0, and `framing` reads props rather than pixels and
111
+ * still calls the node on-screen. Overridden by the nodes that wait on an
112
+ * image; a node with nothing to wait for is ready by definition.
113
+ */
114
+ _staticReady(_assets: AssetStore2D | null): boolean;
93
115
  _syncObject2D(_assets: AssetStore2D | null): void;
94
116
  override free(): void;
95
117
  }
@@ -498,6 +520,7 @@ declare class Sprite2D extends Node2D {
498
520
  protected override _createObject2D(): Object3D;
499
521
  /** Override point: AnimatedSprite2D substitutes its frame window here. */
500
522
  protected resolveTexture(assets: AssetStore2D | null): ResolvedSpriteTexture | null;
523
+ override _staticReady(assets: AssetStore2D | null): boolean;
501
524
  override _syncObject2D(assets: AssetStore2D | null): void;
502
525
  }
503
526
  //#endregion
@@ -568,6 +591,24 @@ declare class Camera2D extends Node2D {
568
591
  get effectiveZoom(): number;
569
592
  override update(dt: number): void;
570
593
  /** View center after clamping the (vw×vh)/zoom view rect inside `limits`. */
594
+ /**
595
+ * The view centre the renderer draws around, in WORLD space.
596
+ *
597
+ * It used to read `this.position` raw — the LOCAL prop — while every other
598
+ * 2D node gets its ancestors composed for free by the three scene graph. So a
599
+ * camera parented to the player, the Godot/Phaser idiom the authoring skill
600
+ * explicitly permits, framed the world origin:
601
+ *
602
+ * ```
603
+ * player world position : [1400, 900]
604
+ * renderer view centre : {"x":0,"y":0}
605
+ * framing : camera /Level/Player/Cam centred [1400, 900]
606
+ * 1 in view, 0 outside it
607
+ * ```
608
+ *
609
+ * `framing` composes full world matrices, so the instrument the skills tell
610
+ * you to trust certified a view the renderer never drew.
611
+ */
571
612
  clampedCenter(vw: number, vh: number): {
572
613
  x: number;
573
614
  y: number;
@@ -882,6 +923,7 @@ declare class TileMap2D extends Node2D {
882
923
  private rebuildColliders;
883
924
  /** @internal The drawable mesh (lazily created under the backing object). */
884
925
  _mesh(): Mesh;
926
+ override _staticReady(assets: AssetStore2D | null): boolean;
885
927
  override _syncObject2D(assets: AssetStore2D | null): void;
886
928
  /** One quad per visible tile, atlas UVs — a single draw call for the level. */
887
929
  private buildGeometry;
package/dist/2d.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DH7JI5xx.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-DL3izw8j.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CBnor8Zf.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-lLeITaZ5.js";
3
+ import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-1cKM8DEj.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CllJXlic.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
package/dist/3d.d.ts CHANGED
@@ -399,6 +399,28 @@ declare class CharacterBody3D extends PhysicsBody3D {
399
399
  /** Max climbable slope angle. */
400
400
  slopeLimitDeg: number;
401
401
  /**
402
+ * How high a step this body walks UP without jumping (0 = none).
403
+ *
404
+ * Rapier's character controller does not autostep unless asked, and nothing
405
+ * asked — so every `CharacterBody3D` was stopped dead by any ledge at all.
406
+ * The player never showed it, because `CharacterController3D` rides a hover
407
+ * spring rather than the KCC; enemies are the ones that walk.
408
+ *
409
+ * Measured on the shipped `tps-3d` template, whose arena has a 0.6 m `Ramp`
410
+ * between the spawn and the player. Standing still for 45 seconds:
411
+ *
412
+ * ```
413
+ * hits=0 hp=100 closest an enemy ever got = 33.91m
414
+ * ```
415
+ *
416
+ * They chase at the right speed the whole time and pile up against the near
417
+ * face of a knee-high box, because a chaser walks a straight line and cannot
418
+ * climb. The template's own playtest had been printing
419
+ * `danger: the player took damage 0 times in 4 runs — nothing here can hurt
420
+ * you` all along, and no rung failed on it.
421
+ */
422
+ stepHeight: number;
423
+ /**
402
424
  * `snapToGround` on a character body is ambiguous, so it is refused.
403
425
  *
404
426
  * A boolean here used to mean the ground-stick, and as placement it means
package/dist/3d.js CHANGED
@@ -1,10 +1,10 @@
1
- import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-D7jTvDQv.js";
1
+ import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-zDynoew_.js";
2
2
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-BSMny7oe.js";
3
- import { B as StaticBody3D, F as WaterCutout3D, I as Area3D, L as CharacterBody3D, N as Water3D, P as WATER_CUTOUT_MAX, R as PhysicsBody3D, V as Node3D, W as WATER_MAX_RIPPLES, z as RigidBody3D } from "./gameplay-Dtzd2itW.js";
4
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-CNxCuhZF.js";
5
- import { a as Environment3D, i as syncTree, o as setEnvironment3D, r as Renderer3D, s as AssetStore3D, t as createGame3D } from "./create-game-IX5lEH0P.js";
3
+ import { B as CharacterBody3D, F as Water3D, H as RigidBody3D, I as WATER_CUTOUT_MAX, L as WaterCutout3D, U as StaticBody3D, V as PhysicsBody3D, W as Node3D, q as WATER_MAX_RIPPLES, z as Area3D } from "./gameplay-CuqoHHUB.js";
4
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-QR7_75KJ.js";
5
+ import { a as Environment3D, i as syncTree, o as setEnvironment3D, r as Renderer3D, s as AssetStore3D, t as createGame3D } from "./create-game-viBqXUoZ.js";
6
6
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
7
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BTUfUSWO.js";
7
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BL_pFJ19.js";
8
8
  //#region src/3d/model-verdict.ts
9
9
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
10
10
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-D7jTvDQv.js";
3
- import { _ as Engine, g as AudioPlayer } from "./register-xuSRyD6b.js";
2
+ import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-zDynoew_.js";
3
+ import { _ as AudioPlayer, v as Engine } from "./register-Btm7_Emq.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-B6rwJOyS.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-BwhkcNt8.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { n as registerGameplayBehaviors } from "./gameplay-Dtzd2itW.js";
9
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DL3izw8j.js";
8
+ import { n as registerGameplayBehaviors } from "./gameplay-CuqoHHUB.js";
9
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-1cKM8DEj.js";
10
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { n as enablePhysics2D } from "./physics-2d-CBnor8Zf.js";
11
+ import { n as enablePhysics2D } from "./physics-2d-CllJXlic.js";
12
12
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
13
13
  //#region src/2d/assets.ts
14
14
  /**
@@ -318,7 +318,7 @@ function walk(node, parentObj, ui, inUi, visited, cameras, emitters, assets, uiS
318
318
  for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, emitters, assets, uiSize, ignoreStatic);
319
319
  if (node instanceof Node2D && !(node instanceof UILayer)) {
320
320
  const obj = node._ensureObject2D();
321
- if (node.static && !ignoreStatic) {
321
+ if (node.static && !ignoreStatic && node._staticReady(assets)) {
322
322
  obj.userData.incantoStaticSynced = true;
323
323
  obj.userData.incantoStatic = true;
324
324
  } else if (obj.userData.incantoStaticSynced === true) {
@@ -539,7 +539,7 @@ var Renderer2D = class {
539
539
  const { activeCamera } = syncTree2D(scene.root, this.worldScene, this.uiScene, this.assets, {
540
540
  width: uiW,
541
541
  height: uiH
542
- }, this.syncScratch);
542
+ }, this.syncScratch, { ignoreStatic: this.ignoreStatic });
543
543
  let center = vp ? {
544
544
  x: vp.design[0] / 2,
545
545
  y: vp.design[1] / 2
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-D7jTvDQv.js";
3
- import { S as qualityRendering, _ as Engine, b as qualityCaps, g as AudioPlayer } from "./register-xuSRyD6b.js";
2
+ import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-zDynoew_.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-Btm7_Emq.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-B6rwJOyS.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-BwhkcNt8.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { R as PhysicsBody3D, U as createCausticsQuad, V as Node3D, n as registerGameplayBehaviors } from "./gameplay-Dtzd2itW.js";
8
+ import { K as createCausticsQuad, V as PhysicsBody3D, W as Node3D, n as registerGameplayBehaviors } from "./gameplay-CuqoHHUB.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-CNxCuhZF.js";
11
- import { n as enablePhysics3D } from "./physics-3d-BTUfUSWO.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-QR7_75KJ.js";
11
+ import { n as enablePhysics3D } from "./physics-3d-BL_pFJ19.js";
12
12
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
13
13
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
14
14
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -1,4 +1,4 @@
1
- import { a as serializeNode, t as buildNodeJson } from "./loader-D7jTvDQv.js";
1
+ import { a as serializeNode, t as buildNodeJson } from "./loader-zDynoew_.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
package/dist/editor.js CHANGED
@@ -1111,6 +1111,13 @@ var kt = {
1111
1111
  en: "Set waitTime (seconds), optionally autostart or oneShot, and connect its timeout signal to any handler. Because it is a node, the whole timing setup lives in the scene file — agents can read and tune it.",
1112
1112
  ko: "waitTime(초)을 정하고 autostart·oneShot을 선택한 뒤 timeout 시그널을 핸들러에 연결하세요. 노드이기 때문에 타이밍 설정 전체가 씬 파일에 남아 에이전트가 읽고 조정할 수 있습니다."
1113
1113
  }),
1114
+ L("Respawn", {
1115
+ en: "Catches whatever it hangs off when it leaves the world, and puts it back.",
1116
+ ko: "부모가 월드 밖으로 떨어지면 붙잡아 원래 자리로 되돌립니다."
1117
+ }, {
1118
+ en: "Make it a child of the player. With no props it catches its parent 50 m under the spawn (1000 px in 2D — the same line incanto-playtest calls \"fell\"), returns it to where it started and zeroes the velocity the fall built up. below names the line, to names the landing, target picks a different node. It emits respawned, and deliberately does not decide what falling COSTS — wire that to a ScoreKeeper or a Health if it should hurt.",
1119
+ ko: "플레이어의 자식으로 두세요. 프롭 없이도 스폰 50 m 아래(2D는 1000 px — incanto-playtest가 \"fell\"로 부르는 그 선)에서 부모를 붙잡아 처음 위치로 되돌리고, 낙하로 쌓인 속도를 0으로 만듭니다. below는 그 선, to는 착지 지점, target은 다른 노드를 지정합니다. respawned 시그널을 쏘며, 추락의 \"대가\"는 일부러 정하지 않습니다 — 피해를 주려면 ScoreKeeper나 Health에 연결하세요."
1120
+ }),
1114
1121
  L("HudLayer", {
1115
1122
  en: "Screen-space HUD overlay above the canvas — parent for UiText/UiBar/UiBanner.",
1116
1123
  ko: "캔버스 위 화면 고정 HUD 오버레이 — UiText/UiBar/UiBanner의 부모."
@@ -9292,8 +9299,15 @@ function yc(e) {
9292
9299
  let t = e.assets;
9293
9300
  if (t) for (let e of Object.values(t)) typeof e?.url == "string" && !e.url.includes("/") && !e.url.includes(".") && (e.url = vc());
9294
9301
  }
9302
+ //#endregion
9303
+ //#region src/main.ts
9304
+ var bc = new Set([
9305
+ "Terrain3D",
9306
+ "Water3D",
9307
+ "River3D"
9308
+ ]);
9295
9309
  m(), b(), k(), ee(), A();
9296
- async function bc(e = {}) {
9310
+ async function xc(e = {}) {
9297
9311
  let t = e.api ?? ze, n = [], r = document.createElement("style");
9298
9312
  r.dataset.incantoEditor = "true", r.textContent = vs, document.head.appendChild(r), n.push(() => r.remove());
9299
9313
  let i = document.createElement("div");
@@ -9390,11 +9404,18 @@ async function bc(e = {}) {
9390
9404
  0,
9391
9405
  0
9392
9406
  ]),
9393
- onNodeScaled3D: (e, t) => de(e, "scale", t, [
9394
- 1,
9395
- 1,
9396
- 1
9397
- ]),
9407
+ onNodeScaled3D: (e, t) => {
9408
+ let n = u.nodeAt(e);
9409
+ if (n && bc.has(String(n.type))) {
9410
+ O(`${n.type} '${n.name}' cannot be scaled — scale would move only the mesh. Resize it with its "size" prop in the inspector.`);
9411
+ return;
9412
+ }
9413
+ de(e, "scale", t, [
9414
+ 1,
9415
+ 1,
9416
+ 1
9417
+ ]);
9418
+ },
9398
9419
  onNodeRotated2D: (e, t) => ue(e, "rotation", t, 0),
9399
9420
  onNodeRotated3D: (e, t) => de(e, "rotation", t, [
9400
9421
  0,
@@ -9480,15 +9501,15 @@ async function bc(e = {}) {
9480
9501
  ...e.shelf ? { shelf: e.shelf } : {},
9481
9502
  ...e.assetType ? { assetType: e.assetType } : {},
9482
9503
  ...e.onProps ? { onProps: e.onProps } : {},
9483
- ...e.asAsset ? { onAsset: (e, t, n) => wc(u, n, t) } : {}
9504
+ ...e.asAsset ? { onAsset: (e, t, n) => Tc(u, n, t) } : {}
9484
9505
  }) : void 0, le = !1, F = () => {
9485
9506
  if (!E) return;
9486
9507
  js(d, u), rt(a("#asset-tree"), u), f.textContent = "", it(f, u, Ce, ce) || Bn(f, u, {
9487
- modelRefs: () => Sc(u, "model"),
9508
+ modelRefs: () => Cc(u, "model"),
9488
9509
  animationsForSelection: () => u.selection ? P.modelAnimationsAt(u.selection) : [],
9489
9510
  bonesForSelection: (e) => u.selection ? P.boneNamesAt(u.selection, e) : [],
9490
- assetRefs: (e) => Cc(u, e),
9491
- addAsset: (e, t) => wc(u, e, t),
9511
+ assetRefs: (e) => wc(u, e),
9512
+ addAsset: (e, t) => Tc(u, e, t),
9492
9513
  ...ce ? { openLibrary: ce } : {},
9493
9514
  confirm: Ce,
9494
9515
  paint: {
@@ -9600,7 +9621,7 @@ async function bc(e = {}) {
9600
9621
  if (e.length === 0) {
9601
9622
  let e = u.working.root;
9602
9623
  if (!e) return;
9603
- let t = xc(e);
9624
+ let t = Sc(e);
9604
9625
  Ce(`This deletes the ROOT '${String(e.name ?? "")}'${t > 0 ? ` AND its ${t} descendant node${t === 1 ? "" : "s"}` : ""} — the scene goes empty, and the next node you add becomes the new root.`, "delete root", () => u.deleteRoot());
9605
9626
  return;
9606
9627
  }
@@ -9608,7 +9629,7 @@ async function bc(e = {}) {
9608
9629
  if (n.length === 0) {
9609
9630
  let n = e.reduce((e, t) => {
9610
9631
  let n = u.nodeAt(t);
9611
- return e + (n ? xc(n) : 0);
9632
+ return e + (n ? Sc(n) : 0);
9612
9633
  }, 0);
9613
9634
  if (n > 0) {
9614
9635
  Ce(`This deletes ${e.length > 1 ? `${e.length} nodes` : `'${String(u.nodeAt(e[0] ?? [])?.name ?? "")}'`} AND ${n} descendant node${n === 1 ? "" : "s"}.`, `delete ${e.length + n} nodes`, () => {
@@ -9784,20 +9805,20 @@ async function bc(e = {}) {
9784
9805
  }
9785
9806
  };
9786
9807
  }
9787
- function xc(e) {
9808
+ function Sc(e) {
9788
9809
  let t = 0;
9789
- for (let n of e.children ?? []) t += 1 + xc(n);
9810
+ for (let n of e.children ?? []) t += 1 + Sc(n);
9790
9811
  return t;
9791
9812
  }
9792
- function Sc(e, t) {
9813
+ function Cc(e, t) {
9793
9814
  let n = e.working.assets;
9794
9815
  return n ? Object.entries(n).filter(([, e]) => e?.type === t).map(([e]) => `$${e}`) : [];
9795
9816
  }
9796
- function Cc(e, t) {
9817
+ function wc(e, t) {
9797
9818
  let n = e.working.assets;
9798
9819
  return n ? Object.entries(n).filter(([, e]) => !t || e?.type !== void 0 && t.includes(e.type)).map(([e]) => `$${e}`) : [];
9799
9820
  }
9800
- function wc(e, t, n) {
9821
+ function Tc(e, t, n) {
9801
9822
  let r = e.working.assets ?? {};
9802
9823
  for (let [e, t] of Object.entries(r)) if (t?.url === n.url) return `$${e}`;
9803
9824
  let i = t;
@@ -9807,4 +9828,4 @@ function wc(e, t, n) {
9807
9828
  }), `$${i}`;
9808
9829
  }
9809
9830
  //#endregion
9810
- export { N as formatSceneJson, ze as httpEditorApi, bc as mountEditor };
9831
+ export { N as formatSceneJson, ze as httpEditorApi, xc as mountEditor };
@@ -1,11 +1,11 @@
1
- import { k as diagnose } from "./loader-D7jTvDQv.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-xuSRyD6b.js";
1
+ import { k as diagnose } from "./loader-zDynoew_.js";
2
+ import { L as translateOn, t as registerCoreNodes } from "./register-Btm7_Emq.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
7
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
8
- import { B as StaticBody3D, F as WaterCutout3D, G as colliderFootDrop, I as Area3D, L as CharacterBody3D, N as Water3D, R as PhysicsBody3D, V as Node3D, w as tolerateUnknownAction, z as RigidBody3D } from "./gameplay-Dtzd2itW.js";
8
+ import { B as CharacterBody3D, F as Water3D, H as RigidBody3D, J as colliderFootDrop, L as WaterCutout3D, R as rejectScale, U as StaticBody3D, V as PhysicsBody3D, W as Node3D, w as tolerateUnknownAction, z as Area3D } from "./gameplay-CuqoHHUB.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
11
11
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
@@ -1575,6 +1575,7 @@ var Terrain3D = class extends Node3D {
1575
1575
  }
1576
1576
  /** Loader hook: bad themes/layers/grids fail at LOAD, not at render. */
1577
1577
  static validateJson(node) {
1578
+ rejectScale(node, "heightAt(), the heightfield collider, drape and terrain-nav all");
1578
1579
  const t = node;
1579
1580
  if (!TERRAIN_THEMES.includes(t.theme)) throw new IncantoError("BAD_FORMAT", `Terrain3D '${node.name}' theme must be one of [${TERRAIN_THEMES.join(", ")}], got '${t.theme}'.`, {
1580
1581
  prop: "theme",
@@ -8805,6 +8806,7 @@ var River3D = class River3D extends Node3D {
8805
8806
  _bodies = [];
8806
8807
  /** Loader hook: a malformed river fails at LOAD, not as an invisible ribbon. */
8807
8808
  static validateJson(node) {
8809
+ rejectScale(node, "the flow query and its collider both");
8808
8810
  const r = node;
8809
8811
  if (!Array.isArray(r.path)) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path must be an array of [x, z] points, got ${JSON.stringify(r.path)}.`, { prop: "path" });
8810
8812
  if (r.path.length > 0 && r.path.length < 2) throw new IncantoError("BAD_FORMAT", `River3D '${node.name}' path needs at least 2 points (source → mouth), got ${r.path.length}.`, { prop: "path" });