incanto 0.59.0 → 0.61.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 (64) hide show
  1. package/bin/incanto-check.mjs +58 -4
  2. package/dist/2d.d.ts +22 -3
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +5 -5
  5. package/dist/3d.js +5 -4
  6. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  7. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  8. package/dist/{create-game-CHDLDQsQ.js → create-game-DH7JI5xx.js} +76 -11
  9. package/dist/{create-game-BiW8Men_.js → create-game-IX5lEH0P.js} +21 -294
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/debug.js +1 -1
  12. package/dist/{duplicate-DJQd44CD.js → duplicate-CGqAmK2h.js} +1 -1
  13. package/dist/editor.js +2 -1
  14. package/dist/{environment-presets-DRAz5EV9.js → environment-presets-CNxCuhZF.js} +48 -13
  15. package/dist/{gameplay-BBEjPFsR.js → gameplay-Dtzd2itW.js} +53 -11
  16. package/dist/gameplay.d.ts +1 -1
  17. package/dist/gameplay.js +1 -1
  18. package/dist/index.d.ts +23 -6
  19. package/dist/index.js +7 -7
  20. package/dist/{loader-D8n7TU8W.js → loader-D7jTvDQv.js} +797 -2
  21. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +1 -1
  24. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  25. package/dist/{physics-2d-BaRSRrrZ.js → physics-2d-CBnor8Zf.js} +2 -2
  26. package/dist/{physics-3d-CYxjh-HW.js → physics-3d-BTUfUSWO.js} +3 -3
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-BpFcgdcL.js → register-DL3izw8j.js} +46 -9
  30. package/dist/{register-CDrAQqPp.js → register-xuSRyD6b.js} +119 -21
  31. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  32. package/dist/{replay-O-yAGM76.d.ts → replay-C5x2vPF5.d.ts} +16 -3
  33. package/dist/{replay-CEPyQtF_.js → replay-D7-yle3s.js} +56 -205
  34. package/dist/{split-screen-DDMZutQ6.js → split-screen-5Ban4q4n.js} +3 -3
  35. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  36. package/dist/{src-CY21B462.js → src-DozXvyZS.js} +1 -1
  37. package/dist/{teardown-RApWnM1G.js → teardown-B6rwJOyS.js} +1 -1
  38. package/dist/{test-DHYuFyAu.js → test-Dch_7VQD.js} +19 -14
  39. package/dist/test.d.ts +24 -4
  40. package/dist/test.js +2 -2
  41. package/dist/vite.js +2 -2
  42. package/editor/assets/{agent8-BoRGtVxK.js → agent8-CF1JL2tR.js} +1 -1
  43. package/editor/assets/{debug-CzdyCg75.js → debug-3QzYhOPA.js} +1 -1
  44. package/editor/assets/{index-VesuVEhe.js → index-CAD2c5ug.js} +92 -92
  45. package/editor/index.html +1 -1
  46. package/package.json +1 -1
  47. package/schemas/scene.schema.json +4 -0
  48. package/skills/incanto-3d-models.md +1 -1
  49. package/skills/incanto-assets.md +10 -1
  50. package/skills/incanto-audio.md +21 -13
  51. package/skills/incanto-behaviors-and-scripts.md +7 -3
  52. package/skills/incanto-building-2d-games.md +12 -3
  53. package/skills/incanto-editor.md +1 -1
  54. package/skills/incanto-gameplay-behaviors.md +24 -1
  55. package/skills/incanto-hud.md +5 -3
  56. package/skills/incanto-localization.md +31 -0
  57. package/skills/incanto-node-reference.md +1 -0
  58. package/skills/incanto-scene-json-authoring.md +6 -1
  59. package/templates-app/beacon-isle-3d/package.json +1 -1
  60. package/templates-app/platformer-2d/package.json +1 -1
  61. package/templates-app/star-survivor/package.json +1 -1
  62. package/templates-app/tps-3d/package.json +1 -1
  63. package/templates-app/village-quest-3d/package.json +1 -1
  64. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { j as registerBehavior, n as loadScene, w as diagnose } from "./loader-D8n7TU8W.js";
3
- import { S as qualityRendering, _ as Engine, b as qualityCaps, g as AudioPlayer } from "./register-CDrAQqPp.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";
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-RApWnM1G.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";
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-BBEjPFsR.js";
8
+ import { R as PhysicsBody3D, U as createCausticsQuad, V as Node3D, n as registerGameplayBehaviors } from "./gameplay-Dtzd2itW.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-DRAz5EV9.js";
11
- import { n as enablePhysics3D } from "./physics-3d-CYxjh-HW.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";
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";
@@ -125,6 +125,10 @@ var AssetStore3D = class {
125
125
  } else entry.scene = gltf.scene;
126
126
  entry.animations = gltf.animations ?? [];
127
127
  entry.status = "ready";
128
+ if (!hasAnyMesh(entry.scene)) {
129
+ entry.error = "loaded, but it contains no meshes — this is an animation-only GLB. A clip file belongs in `animation`, not `model`.";
130
+ diagnose(this.engine, "error", `incanto: '${url}' ${entry.error}`);
131
+ }
128
132
  }, void 0, (error) => {
129
133
  entry.status = "error";
130
134
  entry.error = error instanceof Error ? error.message : String(error);
@@ -172,6 +176,16 @@ var AssetStore3D = class {
172
176
  this.animations.clear();
173
177
  }
174
178
  };
179
+ /** Does this loaded scene graph contain anything that will actually draw? */
180
+ function hasAnyMesh(scene) {
181
+ const walk = scene?.traverse;
182
+ if (!walk) return true;
183
+ let found = false;
184
+ walk.call(scene, (o) => {
185
+ if (o.isMesh) found = true;
186
+ });
187
+ return found;
188
+ }
175
189
  //#endregion
176
190
  //#region src/core/yield-frame.ts
177
191
  /**
@@ -226,293 +240,6 @@ function yieldToFrame(maxWaitMs = DEFAULT_MAX_WAIT_MS, raf = typeof requestAnima
226
240
  });
227
241
  }
228
242
  //#endregion
229
- //#region src/3d/environment.ts
230
- const SKY_KEYS = [
231
- "type",
232
- "sunPosition",
233
- "elevationDeg",
234
- "azimuthDeg",
235
- "turbidity",
236
- "rayleigh"
237
- ];
238
- const FOG_KEYS = [
239
- "color",
240
- "near",
241
- "far"
242
- ];
243
- const CLOUD_KEYS = [
244
- "coverage",
245
- "density",
246
- "base",
247
- "top",
248
- "color",
249
- "shadeColor",
250
- "speed",
251
- "scale"
252
- ];
253
- const BLOOM_KEYS = ["threshold", "strength"];
254
- const POST_KEYS = [
255
- "vignette",
256
- "saturation",
257
- "contrast"
258
- ];
259
- const SHADOW_KEYS = [
260
- "mapSize",
261
- "radius",
262
- "static"
263
- ];
264
- const SHADOW_MAP_SIZES = [1024, 2048];
265
- const DEFAULT_TURBIDITY = 2;
266
- const DEFAULT_RAYLEIGH = 1;
267
- const DEFAULT_FOG_NEAR = 50;
268
- const DEFAULT_FOG_FAR = 800;
269
- /** Fallback haze when fog is declared without a sky to derive a horizon from. */
270
- const DEFAULT_FOG_COLOR = "#cfd8e0";
271
- const DEG2RAD = Math.PI / 180;
272
- /** Parse + hard-validate the 3D slice of a scene's `environment` header. */
273
- function parseEnvironment3D(env) {
274
- return {
275
- exposure: parseExposure(env?.exposure),
276
- sky: parseSky(env?.sky),
277
- fog: parseFog(env?.fog, env?.sky !== void 0),
278
- clouds: parseClouds(env?.clouds),
279
- bloom: parseBloom(env?.bloom),
280
- post: parsePost(env?.post),
281
- shadows: parseShadows(env?.shadows)
282
- };
283
- }
284
- /**
285
- * elevation/azimuth (degrees) → unit direction. Matches three's Sky example
286
- * (`Vector3.setFromSphericalCoords`): azimuth 0 = +Z, 90 = +X; elevation 90 =
287
- * straight up.
288
- */
289
- function sunDirectionFromElevationAzimuth(elevationDeg, azimuthDeg) {
290
- const phi = (90 - elevationDeg) * DEG2RAD;
291
- const theta = azimuthDeg * DEG2RAD;
292
- return [
293
- Math.sin(phi) * Math.sin(theta),
294
- Math.cos(phi),
295
- Math.sin(phi) * Math.cos(theta)
296
- ];
297
- }
298
- /** The sky's sun as a UNIT vector — what Water3D/Foliage3D uniforms consume. */
299
- function sunDirectionFromSky(sky) {
300
- const [x, y, z] = sky.sunPosition;
301
- const len = Math.hypot(x, y, z) || 1;
302
- return [
303
- x / len,
304
- y / len,
305
- z / len
306
- ];
307
- }
308
- /**
309
- * A horizon-ish haze color derived from the sky config — the default fog
310
- * color. Cheap model, judged by eye: clear skies haze blue-grey, turbid skies
311
- * whiten, and a low sun warms the band toward amber.
312
- */
313
- function horizonColorFromSky(sky) {
314
- const clear = [
315
- 191,
316
- 213,
317
- 232
318
- ];
319
- const hazy = [
320
- 233,
321
- 228,
322
- 217
323
- ];
324
- const warm = [
325
- 242,
326
- 201,
327
- 150
328
- ];
329
- const t = clamp01((sky.turbidity - DEFAULT_TURBIDITY) / 8);
330
- const dir = sunDirectionFromSky(sky);
331
- const w = clamp01((18 - Math.asin(clamp(dir[1], -1, 1)) / DEG2RAD) / 18) * .8;
332
- const mix = (i) => Math.round(lerp(lerp(clear[i], hazy[i], t), warm[i], w));
333
- return `#${[
334
- mix(0),
335
- mix(1),
336
- mix(2)
337
- ].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
338
- }
339
- function parseExposure(value) {
340
- if (value === void 0) return 1;
341
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new IncantoError("BAD_FORMAT", `environment.exposure must be a finite number > 0 (tone-mapping exposure, default 1), got ${JSON.stringify(value)}.`, { prop: "exposure" });
342
- return value;
343
- }
344
- function parseSky(value) {
345
- if (value === void 0) return null;
346
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.sky must be an object ({ type?: "atmosphere", sunPosition? | elevationDeg?+azimuthDeg?, turbidity?, rayleigh? }), got ${JSON.stringify(value)}.`, { prop: "sky" });
347
- const sky = value;
348
- for (const key of Object.keys(sky)) if (!SKY_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.sky has unknown key '${key}'. Valid keys: [${SKY_KEYS.join(", ")}].`, {
349
- prop: "sky",
350
- validOptions: SKY_KEYS
351
- });
352
- if (sky.type !== void 0 && sky.type !== "atmosphere") throw new IncantoError("BAD_FORMAT", `environment.sky.type must be 'atmosphere' (the only sky type so far), got ${JSON.stringify(sky.type)}.`, {
353
- prop: "sky",
354
- validOptions: ["atmosphere"]
355
- });
356
- const hasAngles = sky.elevationDeg !== void 0 || sky.azimuthDeg !== void 0;
357
- if (sky.sunPosition !== void 0 && hasAngles) throw new IncantoError("BAD_FORMAT", `environment.sky takes sunPosition OR elevationDeg/azimuthDeg, not both.`, {
358
- prop: "sky",
359
- validOptions: ["sunPosition", "elevationDeg+azimuthDeg"]
360
- });
361
- let sunPosition;
362
- if (sky.sunPosition !== void 0) {
363
- const sp = sky.sunPosition;
364
- if (!Array.isArray(sp) || sp.length !== 3 || !sp.every((v) => typeof v === "number" && Number.isFinite(v)) || Math.hypot(sp[0], sp[1], sp[2]) === 0) throw new IncantoError("BAD_FORMAT", `environment.sky.sunPosition must be a non-zero [x, y, z] vector, got ${JSON.stringify(sp)}.`, { prop: "sky" });
365
- sunPosition = [
366
- sp[0],
367
- sp[1],
368
- sp[2]
369
- ];
370
- } else {
371
- const elevation = numberOr(sky.elevationDeg, 32, "sky.elevationDeg");
372
- const azimuth = numberOr(sky.azimuthDeg, 135, "sky.azimuthDeg");
373
- if (elevation < -90 || elevation > 90) throw new IncantoError("BAD_FORMAT", `environment.sky.elevationDeg must be in [-90, 90] (degrees above the horizon), got ${elevation}.`, { prop: "sky" });
374
- sunPosition = sunDirectionFromElevationAzimuth(elevation, azimuth);
375
- }
376
- const turbidity = numberOr(sky.turbidity, DEFAULT_TURBIDITY, "sky.turbidity");
377
- if (turbidity <= 0) throw new IncantoError("BAD_FORMAT", `environment.sky.turbidity must be > 0 (atmospheric haze; 2 ≈ clear day), got ${turbidity}.`, { prop: "sky" });
378
- const rayleigh = numberOr(sky.rayleigh, DEFAULT_RAYLEIGH, "sky.rayleigh");
379
- if (rayleigh < 0) throw new IncantoError("BAD_FORMAT", `environment.sky.rayleigh must be >= 0 (Rayleigh scattering; 1 ≈ earth-like), got ${rayleigh}.`, { prop: "sky" });
380
- return {
381
- sunPosition,
382
- turbidity,
383
- rayleigh
384
- };
385
- }
386
- function parseFog(value, hasSky) {
387
- if (value === void 0) return null;
388
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.fog must be an object ({ color?, near?, far? }), got ${JSON.stringify(value)}.`, { prop: "fog" });
389
- const fog = value;
390
- for (const key of Object.keys(fog)) if (!FOG_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.fog has unknown key '${key}'. Valid keys: [${FOG_KEYS.join(", ")}].`, {
391
- prop: "fog",
392
- validOptions: FOG_KEYS
393
- });
394
- if (fog.color !== void 0 && typeof fog.color !== "string") throw new IncantoError("BAD_FORMAT", `environment.fog.color must be a hex color string, got ${JSON.stringify(fog.color)}.`, { prop: "fog" });
395
- const near = numberOr(fog.near, DEFAULT_FOG_NEAR, "fog.near");
396
- const far = numberOr(fog.far, DEFAULT_FOG_FAR, "fog.far");
397
- if (near < 0) throw new IncantoError("BAD_FORMAT", `environment.fog.near must be >= 0 meters, got ${near}.`, { prop: "fog" });
398
- if (far <= near) throw new IncantoError("BAD_FORMAT", `environment.fog.far must be > near (got near ${near}, far ${far}).`, { prop: "fog" });
399
- return {
400
- color: fog.color ?? (hasSky ? "" : DEFAULT_FOG_COLOR),
401
- near,
402
- far
403
- };
404
- }
405
- function parseClouds(value) {
406
- if (value === void 0) return null;
407
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.clouds must be an object ({ coverage?, density?, base?, top?, color?, shadeColor?, speed?, scale? }), got ${JSON.stringify(value)}.`, { prop: "clouds" });
408
- const c = value;
409
- for (const key of Object.keys(c)) if (!CLOUD_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.clouds has unknown key '${key}'. Valid keys: [${CLOUD_KEYS.join(", ")}].`, {
410
- prop: "clouds",
411
- validOptions: CLOUD_KEYS
412
- });
413
- for (const k of ["color", "shadeColor"]) if (c[k] !== void 0 && typeof c[k] !== "string") throw new IncantoError("BAD_FORMAT", `environment.clouds.${k} must be a hex color string, got ${JSON.stringify(c[k])}.`, { prop: "clouds" });
414
- const coverage = numberOr(c.coverage, .5, "clouds.coverage");
415
- if (coverage < 0 || coverage > 1) throw new IncantoError("BAD_FORMAT", `environment.clouds.coverage must be in [0, 1] (how much sky is cloudy), got ${coverage}.`, { prop: "clouds" });
416
- const density = numberOr(c.density, 1, "clouds.density");
417
- if (density < 0) throw new IncantoError("BAD_FORMAT", `environment.clouds.density must be >= 0 (optical thickness), got ${density}.`, { prop: "clouds" });
418
- const base = numberOr(c.base, 120, "clouds.base");
419
- const top = numberOr(c.top, 320, "clouds.top");
420
- if (top <= base) throw new IncantoError("BAD_FORMAT", `environment.clouds.top must be > base (got base ${base}, top ${top}).`, { prop: "clouds" });
421
- const speed = numberOr(c.speed, 1, "clouds.speed");
422
- const scale = numberOr(c.scale, 240, "clouds.scale");
423
- if (scale <= 0) throw new IncantoError("BAD_FORMAT", `environment.clouds.scale must be > 0 (feature size in world units), got ${scale}.`, { prop: "clouds" });
424
- return {
425
- coverage,
426
- density,
427
- base,
428
- top,
429
- color: c.color ?? "#ffffff",
430
- shadeColor: c.shadeColor ?? "#9fb0c8",
431
- speed,
432
- scale
433
- };
434
- }
435
- function parseBloom(value) {
436
- if (value === void 0) return null;
437
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.bloom must be an object ({ threshold?, strength? }), got ${JSON.stringify(value)}.`, { prop: "bloom" });
438
- const b = value;
439
- for (const key of Object.keys(b)) if (!BLOOM_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.bloom has unknown key '${key}'. Valid keys: [${BLOOM_KEYS.join(", ")}].`, {
440
- prop: "bloom",
441
- validOptions: BLOOM_KEYS
442
- });
443
- const threshold = numberOr(b.threshold, 1, "bloom.threshold");
444
- if (threshold < 0 || threshold > 8) throw new IncantoError("BAD_FORMAT", `environment.bloom.threshold must be in [0, 8] (linear-HDR luminance; 1 = white), got ${threshold}.`, { prop: "bloom" });
445
- const strength = numberOr(b.strength, .8, "bloom.strength");
446
- if (strength < 0) throw new IncantoError("BAD_FORMAT", `environment.bloom.strength must be >= 0, got ${strength}.`, { prop: "bloom" });
447
- return {
448
- threshold,
449
- strength
450
- };
451
- }
452
- function parsePost(value) {
453
- if (value === void 0) return null;
454
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.post must be an object ({ vignette?, saturation?, contrast? }), got ${JSON.stringify(value)}.`, { prop: "post" });
455
- const p = value;
456
- for (const key of Object.keys(p)) if (!POST_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.post has unknown key '${key}'. Valid keys: [${POST_KEYS.join(", ")}].`, {
457
- prop: "post",
458
- validOptions: POST_KEYS
459
- });
460
- const vignette = numberOr(p.vignette, 0, "post.vignette");
461
- if (vignette < 0 || vignette > 1) throw new IncantoError("BAD_FORMAT", `environment.post.vignette must be in [0, 1], got ${vignette}.`, { prop: "post" });
462
- const saturation = numberOr(p.saturation, 1, "post.saturation");
463
- if (saturation < 0 || saturation > 4) throw new IncantoError("BAD_FORMAT", `environment.post.saturation must be in [0, 4] (1 = neutral), got ${saturation}.`, { prop: "post" });
464
- const contrast = numberOr(p.contrast, 1, "post.contrast");
465
- if (contrast < .2 || contrast > 3) throw new IncantoError("BAD_FORMAT", `environment.post.contrast must be in [0.2, 3] (1 = neutral), got ${contrast}.`, { prop: "post" });
466
- return {
467
- vignette,
468
- saturation,
469
- contrast
470
- };
471
- }
472
- function parseShadows(value) {
473
- if (value === void 0) return null;
474
- if (value === false) return false;
475
- if (value === true) return {
476
- mapSize: 2048,
477
- radius: 1,
478
- static: false
479
- };
480
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.shadows must be true, false or an object ({ mapSize?, radius? }), got ${JSON.stringify(value)}.`, { prop: "shadows" });
481
- const shadows = value;
482
- for (const key of Object.keys(shadows)) if (!SHADOW_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.shadows has unknown key '${key}'. Valid keys: [${SHADOW_KEYS.join(", ")}].`, {
483
- prop: "shadows",
484
- validOptions: SHADOW_KEYS
485
- });
486
- const mapSize = shadows.mapSize === void 0 ? 2048 : shadows.mapSize;
487
- if (!SHADOW_MAP_SIZES.includes(mapSize)) throw new IncantoError("BAD_FORMAT", `environment.shadows.mapSize must be one of [${SHADOW_MAP_SIZES.join(", ")}], got ${JSON.stringify(shadows.mapSize)}.`, {
488
- prop: "shadows",
489
- validOptions: SHADOW_MAP_SIZES.map(String)
490
- });
491
- const radius = numberOr(shadows.radius, 1, "shadows.radius");
492
- if (radius < 0) throw new IncantoError("BAD_FORMAT", `environment.shadows.radius must be >= 0, got ${radius}.`, { prop: "shadows" });
493
- const staticShadows = shadows.static === void 0 ? false : shadows.static;
494
- if (typeof staticShadows !== "boolean") throw new IncantoError("BAD_FORMAT", `environment.shadows.static must be a boolean, got ${JSON.stringify(shadows.static)}.`, { prop: "shadows" });
495
- return {
496
- mapSize,
497
- radius,
498
- static: staticShadows
499
- };
500
- }
501
- function numberOr(value, fallback, at) {
502
- if (value === void 0) return fallback;
503
- if (typeof value !== "number" || !Number.isFinite(value)) throw new IncantoError("BAD_FORMAT", `environment.${at} must be a finite number, got ${JSON.stringify(value)}.`, { prop: at });
504
- return value;
505
- }
506
- function clamp(v, min, max) {
507
- return Math.min(Math.max(v, min), max);
508
- }
509
- function clamp01(v) {
510
- return clamp(v, 0, 1);
511
- }
512
- function lerp(a, b, t) {
513
- return a + (b - a) * t;
514
- }
515
- //#endregion
516
243
  //#region src/3d/environment-runtime.ts
517
244
  /**
518
245
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -2666,4 +2393,4 @@ function wireAudioUnlock(engine, root, canvas) {
2666
2393
  return detachAll;
2667
2394
  }
2668
2395
  //#endregion
2669
- export { Environment3D as a, parseEnvironment3D as c, AssetStore3D as d, syncTree as i, sunDirectionFromElevationAzimuth as l, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, horizonColorFromSky as s, createGame3D as t, sunDirectionFromSky as u };
2396
+ export { Environment3D as a, syncTree as i, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, AssetStore3D as s, createGame3D as t };
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Pt as LogLevel, T as RendererStats, b as Engine } from "./behavior-DWKTUzKI.js";
1
+ import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-DsgayMsH.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
package/dist/debug.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as jsonClone } from "./json-CwwhxQgb.js";
2
- import { s as mergeStaticProps } from "./registry-WWcQcfMr.js";
2
+ import { s as mergeStaticProps } from "./registry-CF70EArN.js";
3
3
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
4
  //#region src/debug/capture.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { a as serializeNode, t as buildNodeJson } from "./loader-D8n7TU8W.js";
1
+ import { a as serializeNode, t as buildNodeJson } from "./loader-D7jTvDQv.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
@@ -2372,7 +2372,8 @@ function Bn(e, t, n) {
2372
2372
  var Vn = [
2373
2373
  "connections",
2374
2374
  "input",
2375
- "multiplayer"
2375
+ "multiplayer",
2376
+ "strings"
2376
2377
  ];
2377
2378
  function Hn(e, t, n) {
2378
2379
  e.appendChild(Tr("scene")), e.appendChild(Ar("name", String(t.working.name ?? ""), (e) => {
@@ -1,11 +1,11 @@
1
- import { w as diagnose } from "./loader-D8n7TU8W.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-CDrAQqPp.js";
1
+ import { k as diagnose } from "./loader-D7jTvDQv.js";
2
+ import { I as translateOn, t as registerCoreNodes } from "./register-xuSRyD6b.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
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-BBEjPFsR.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";
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";
@@ -727,7 +727,8 @@ var Joint3D = class extends Node3D {
727
727
  },
728
728
  target: {
729
729
  default: "",
730
- nodePath: true
730
+ nodePath: true,
731
+ required: true
731
732
  },
732
733
  anchor: { default: [
733
734
  0,
@@ -2335,7 +2336,8 @@ var BoneAttachment3D = class extends Node3D {
2335
2336
  /** Node path to the ModelInstance3D whose skeleton to ride. */
2336
2337
  target: {
2337
2338
  default: "",
2338
- nodePath: true
2339
+ nodePath: true,
2340
+ required: true
2339
2341
  },
2340
2342
  /** Bone name ("RightHand", "Head", …). Empty = inert. */
2341
2343
  bone: { default: "" }
@@ -2428,7 +2430,8 @@ var BoneLookAt3D = class extends Node3D {
2428
2430
  /** Node path to the ModelInstance3D whose bone to steer. */
2429
2431
  target: {
2430
2432
  default: "",
2431
- nodePath: true
2433
+ nodePath: true,
2434
+ required: true
2432
2435
  },
2433
2436
  /** Bone name ("Head", "Neck", "Spine2" — Mixamo spellings matched). */
2434
2437
  bone: { default: "Head" },
@@ -2440,7 +2443,13 @@ var BoneLookAt3D = class extends Node3D {
2440
2443
  /** Comfort cone half-angle, degrees — beyond it the head returns to the anim. */
2441
2444
  maxAngleDeg: { default: 75 },
2442
2445
  /** Blend weight 0..1 (how far toward the target the bone turns). */
2443
- weight: { default: .85 },
2446
+ weight: {
2447
+ default: .85,
2448
+ range: {
2449
+ min: 0,
2450
+ max: 1
2451
+ }
2452
+ },
2444
2453
  /** The bone's local facing axis. */
2445
2454
  forwardAxis: {
2446
2455
  default: "+z",
@@ -6449,9 +6458,21 @@ var ModelInstance3D = class extends Node3D {
6449
6458
  static signals = ["animationFinished"];
6450
6459
  static props = {
6451
6460
  ...Node3D.props,
6452
- model: { default: "" },
6453
- animation: { default: "" },
6454
- targetHeight: { default: 0 },
6461
+ model: {
6462
+ default: "",
6463
+ asset: {
6464
+ kind: "model",
6465
+ refOnly: true
6466
+ }
6467
+ },
6468
+ animation: {
6469
+ default: "",
6470
+ asset: { kind: "animation" }
6471
+ },
6472
+ targetHeight: {
6473
+ default: 0,
6474
+ range: { min: 0 }
6475
+ },
6455
6476
  castShadow: { default: false },
6456
6477
  animationLoop: { default: true },
6457
6478
  /** Playback rate: 1 = authored speed, 0.5 = half, 2 = double. */
@@ -6466,8 +6487,22 @@ var ModelInstance3D = class extends Node3D {
6466
6487
  upperBodyRoot: { default: "Spine" },
6467
6488
  receiveShadow: { default: false },
6468
6489
  tint: { default: "" },
6469
- metalness: { default: -1 },
6470
- roughness: { default: -1 }
6490
+ metalness: {
6491
+ default: -1,
6492
+ range: {
6493
+ min: 0,
6494
+ max: 1,
6495
+ allow: [-1]
6496
+ }
6497
+ },
6498
+ roughness: {
6499
+ default: -1,
6500
+ range: {
6501
+ min: 0,
6502
+ max: 1,
6503
+ allow: [-1]
6504
+ }
6505
+ }
6471
6506
  };
6472
6507
  model = "";
6473
6508
  animation = "";
@@ -1,7 +1,7 @@
1
- import { C as Node, E as Behavior, b as effectiveOrder, j as registerBehavior, n as loadScene, p as SaveSlots, w as diagnose } from "./loader-D8n7TU8W.js";
1
+ import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-D7jTvDQv.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-CwwhxQgb.js";
4
- import { t as duplicateNode } from "./duplicate-DJQd44CD.js";
4
+ import { t as duplicateNode } from "./duplicate-CGqAmK2h.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -4975,6 +4975,7 @@ var FloatAway = class extends Behavior {
4975
4975
  onReady() {
4976
4976
  const node = this.node;
4977
4977
  this.start = [...node.position ?? []];
4978
+ if (node.opacity === void 0) diagnose(this.node.tree?.engine, "warn", `[incanto] FloatAway on '${this.node.getPath()}' will not fade — ${this.node.constructor.name} has no "opacity" prop, so it pops out at full brightness instead. Use a node that has one (Label, Label3D, Sprite2D, Sprite3D).`);
4978
4979
  }
4979
4980
  update(dt) {
4980
4981
  if (this.done) return;
@@ -5024,9 +5025,18 @@ var FollowCamera = class extends Behavior {
5024
5025
  /** Constant offset added to the target's position ([x,y] or [x,y,z]). */
5025
5026
  offset: { default: [] },
5026
5027
  /** 0..1 retention factor: 0 = snap, higher = smoother chase. */
5027
- smoothing: { default: 0 },
5028
+ smoothing: {
5029
+ default: 0,
5030
+ range: {
5031
+ min: 0,
5032
+ max: 1
5033
+ }
5034
+ },
5028
5035
  /** Don't move until the target is at least this far from the desired point. */
5029
- deadzone: { default: 0 }
5036
+ deadzone: {
5037
+ default: 0,
5038
+ range: { min: 0 }
5039
+ }
5030
5040
  };
5031
5041
  static signals = [];
5032
5042
  target = "";
@@ -5109,7 +5119,6 @@ function tolerateUnknownAction(ctx, action, read, fallback) {
5109
5119
  function restartScene(engine) {
5110
5120
  const source = engine.scene?.source;
5111
5121
  if (!source) return;
5112
- engine.timeScale = 1;
5113
5122
  engine.setScene(loadScene(jsonClone(source)));
5114
5123
  }
5115
5124
  /**
@@ -5119,7 +5128,6 @@ function restartScene(engine) {
5119
5128
  function goToScene(engine, sceneJson, opts) {
5120
5129
  const fade = opts?.fadeSeconds ?? .4;
5121
5130
  const swap = () => {
5122
- engine.timeScale = 1;
5123
5131
  engine.setScene(loadScene(jsonClone(sceneJson)));
5124
5132
  };
5125
5133
  if (typeof document === "undefined" || fade <= 0) {
@@ -5500,13 +5508,14 @@ function screenFlash(engine, color = "#ffffff", opacity = .35, seconds = .25) {
5500
5508
  */
5501
5509
  function hitStop(engine, seconds = .08) {
5502
5510
  recordEffect(engine, "hitstop", "", "engine", seconds);
5503
- const state = hitStops.get(engine);
5511
+ const state = liveHitStop(engine);
5504
5512
  if (state) {
5505
5513
  state.until = Math.max(state.until, engine.unscaledTime + seconds);
5506
5514
  return;
5507
5515
  }
5508
5516
  const restore = engine.timeScale;
5509
5517
  const entry = {
5518
+ startedAt: engine.unscaledTime,
5510
5519
  until: engine.unscaledTime + seconds,
5511
5520
  restore,
5512
5521
  release: () => {}
@@ -5514,12 +5523,35 @@ function hitStop(engine, seconds = .08) {
5514
5523
  hitStops.set(engine, entry);
5515
5524
  engine.timeScale = 0;
5516
5525
  entry.release = engine.updated.connect(() => {
5526
+ if (!liveHitStop(engine)) return;
5517
5527
  if (engine.unscaledTime < entry.until) return;
5518
5528
  engine.timeScale = entry.restore;
5519
5529
  entry.release();
5520
5530
  hitStops.delete(engine);
5521
5531
  });
5522
5532
  }
5533
+ /**
5534
+ * The engine's pending freeze, if the clock it was measured against still runs.
5535
+ *
5536
+ * `setScene` resets `unscaledTime` to 0, and a freeze deadline is an ABSOLUTE
5537
+ * reading of that clock — so a hit-stop taken on the killing blow, two minutes
5538
+ * into a level, survived the restart as a deadline two minutes in the future.
5539
+ * Measured: `hitStop(engine, 0.08)`, then the restart, then five real seconds
5540
+ * of ticks — 2.08 of them frozen, and a level that long freezes that long. The
5541
+ * quieter half is the next scene's OWN hit-stop: it found the stale entry,
5542
+ * extended a deadline nobody was waiting on, and never froze anything.
5543
+ *
5544
+ * A clock that reads earlier than the freeze began cannot be the clock the
5545
+ * freeze was scheduled on. `setScene` already put `timeScale` back to 1, so
5546
+ * there is nothing to restore — just let go.
5547
+ */
5548
+ function liveHitStop(engine) {
5549
+ const entry = hitStops.get(engine);
5550
+ if (!entry) return void 0;
5551
+ if (engine.unscaledTime >= entry.startedAt) return entry;
5552
+ entry.release();
5553
+ hitStops.delete(engine);
5554
+ }
5523
5555
  const hitStops = /* @__PURE__ */ new Map();
5524
5556
  //#endregion
5525
5557
  //#region src/gameplay/lifetime.ts
@@ -5605,7 +5637,10 @@ function ease(curve, t) {
5605
5637
  var MoveTo = class extends Behavior {
5606
5638
  static props = {
5607
5639
  /** Destination position ([x,y] or [x,y,z]). */
5608
- to: { default: [] },
5640
+ to: {
5641
+ default: [],
5642
+ required: true
5643
+ },
5609
5644
  /** Seconds the move takes. */
5610
5645
  duration: { default: 1 },
5611
5646
  /** Easing curve. */
@@ -5819,7 +5854,10 @@ function distanceBetween(a, b) {
5819
5854
  var Patrol = class extends Behavior {
5820
5855
  static props = {
5821
5856
  /** Waypoints: array of position arrays OR node-path strings to read. */
5822
- points: { default: [] },
5857
+ points: {
5858
+ default: [],
5859
+ required: true
5860
+ },
5823
5861
  /** Units per second along the path. */
5824
5862
  speed: { default: 60 },
5825
5863
  /** Wrap to the first point after the last (ignored in pingpong). */
@@ -6355,7 +6393,8 @@ var Spawner = class extends Behavior {
6355
6393
  /** Node path of the template to clone. */
6356
6394
  prefab: {
6357
6395
  default: "",
6358
- nodePath: true
6396
+ nodePath: true,
6397
+ required: true
6359
6398
  },
6360
6399
  /** Seconds between spawns. */
6361
6400
  interval: { default: 1 },
@@ -6508,7 +6547,10 @@ var Wander = class extends Behavior {
6508
6547
  var WaveSpawner = class extends Behavior {
6509
6548
  static props = {
6510
6549
  /** Array of { prefab, count, interval, delayBefore }. */
6511
- waves: { default: [] },
6550
+ waves: {
6551
+ default: [],
6552
+ required: true
6553
+ },
6512
6554
  /** Begin the first wave's countdown at ready. */
6513
6555
  autoStart: { default: true }
6514
6556
  };
@@ -1,4 +1,4 @@
1
- import { At as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-DWKTUzKI.js";
1
+ import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-DsgayMsH.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as Health, C as restartScene, D as DayNight, E as FloatAway, M as Chase, O as phaseOf, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Collector, k as DamageOnContact, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-BBEjPFsR.js";
1
+ import { A as Health, C as restartScene, D as DayNight, E as FloatAway, M as Chase, O as phaseOf, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Collector, k as DamageOnContact, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-Dtzd2itW.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };