incanto 0.4.3 → 0.5.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 (43) hide show
  1. package/dist/2d.d.ts +74 -6
  2. package/dist/2d.js +4 -4
  3. package/dist/3d.d.ts +173 -15
  4. package/dist/3d.js +4 -4
  5. package/dist/{audio-player-DqUR3XFs.d.ts → audio-player-DkBqRTs4.d.ts} +1 -1
  6. package/dist/{behavior-BAQq7HGM.d.ts → behavior-CWhW3oa6.d.ts} +54 -0
  7. package/dist/{create-game-CZHROKcT.js → create-game-BFESHv1X.js} +37 -25
  8. package/dist/{create-game-DiTq3-fQ.js → create-game-D8UvwXme.js} +231 -8
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DP2WPYom.js → duplicate-BEvGBtb_.js} +1 -1
  11. package/dist/{gameplay-Ccruc3Wd.js → gameplay-CDFgSG6z.js} +293 -24
  12. package/dist/gameplay.d.ts +113 -2
  13. package/dist/gameplay.js +2 -2
  14. package/dist/index.d.ts +139 -5
  15. package/dist/index.js +59 -6
  16. package/dist/{loader-CGs_G-r0.js → loader-B4OEXDZ8.js} +60 -0
  17. package/dist/{loader-Mo0KghCv.d.ts → loader-D2u0fVW2.d.ts} +1 -1
  18. package/dist/net.d.ts +1 -1
  19. package/dist/net.js +1 -1
  20. package/dist/{particle-sim-DYuSUxvK.js → particle-sim-CFkILGwh.js} +84 -3
  21. package/dist/{particle-sim-CbN4YUuH.d.ts → particle-sim-CwJ5rI_P.d.ts} +3 -0
  22. package/dist/{physics-2d-KuMWPTf6.js → physics-2d-KXn1N1jF.js} +95 -10
  23. package/dist/{physics-3d-DmNCeh58.js → physics-3d-C58oQzTX.js} +121 -43
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
  27. package/dist/{register-DPEV9_9t.js → register-D71C4rDC.js} +76 -5
  28. package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
  29. package/dist/{register-BRy8FNox.js → register-DvPHJdVj.js} +379 -49
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-DryTXVd4.js → agent8-DruKhR22.js} +1 -1
  33. package/editor/assets/index-D2qufqUo.js +7417 -0
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +847 -133
  37. package/skills/incanto-3d-models.md +24 -1
  38. package/skills/incanto-building-3d-games.md +33 -1
  39. package/skills/incanto-gameplay-behaviors.md +60 -0
  40. package/skills/incanto-hud.md +58 -0
  41. package/skills/incanto-node-reference.md +113 -0
  42. package/skills/incanto-physics-and-input.md +47 -0
  43. package/editor/assets/index-N8se-PhP.js +0 -7365
@@ -1,12 +1,12 @@
1
- import { f as Node } from "./loader-CGs_G-r0.js";
2
- import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-DYuSUxvK.js";
1
+ import { f as Node } from "./loader-B4OEXDZ8.js";
2
+ import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CFkILGwh.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { t as registerCoreNodes } from "./register-BuUV1_KB.js";
5
+ import { t as registerCoreNodes } from "./register-Dl_ixIJe.js";
6
6
  import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
7
7
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CroQPEER.js";
9
- import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
9
+ import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
11
11
  //#region src/3d/camera-rig.ts
12
12
  const QUARTER_PITCH = Math.atan(1 / Math.SQRT2);
@@ -286,7 +286,21 @@ var PhysicsBody3D = class extends Node3D {
286
286
  static props = { collider: { default: {} } };
287
287
  /** The unified collision model: every collider participant can emit these. */
288
288
  static signals = ["triggerEnter", "triggerExit"];
289
- collider = {};
289
+ _collider = {};
290
+ /** @internal Physics resync flag — raised when `collider` is REPLACED. */
291
+ _colliderChanged = false;
292
+ /**
293
+ * Collider shape prop. Change it by REPLACING the object
294
+ * (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the
295
+ * body on replacement; in-place mutation of the old object is not watched.
296
+ */
297
+ get collider() {
298
+ return this._collider;
299
+ }
300
+ set collider(value) {
301
+ this._collider = value;
302
+ this._colliderChanged = true;
303
+ }
290
304
  /** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
291
305
  static validateJson(node) {
292
306
  const body = node;
@@ -373,6 +387,78 @@ var CharacterBody3D = class extends PhysicsBody3D {
373
387
  }
374
388
  };
375
389
  //#endregion
390
+ //#region src/3d/nodes/joint-3d.ts
391
+ /**
392
+ * A physics joint linking its PARENT body to `target` (a node path to the
393
+ * other body). Godot-style placement: the joint lives as a child of body A.
394
+ *
395
+ * { "name": "Hinge", "type": "Joint3D",
396
+ * "props": { "type": "spherical", "target": "%Anchor", "anchor": [0, -20] } }
397
+ *
398
+ * Types: `fixed` welds the bodies rigidly · `spherical` is a ball joint at the
399
+ * anchors · `rope` caps the anchor distance at `length` (m; 0 = measured at
400
+ * creation) · `spring` pulls toward `length` with `stiffness`/`damping`.
401
+ * Anchors are LOCAL meter offsets on each body.
402
+ */
403
+ var Joint3D = class extends Node3D {
404
+ static typeName = "Joint3D";
405
+ static props = {
406
+ type: {
407
+ default: "spherical",
408
+ options: [
409
+ "fixed",
410
+ "spherical",
411
+ "rope",
412
+ "spring"
413
+ ]
414
+ },
415
+ target: { default: "" },
416
+ anchor: { default: [
417
+ 0,
418
+ 0,
419
+ 0
420
+ ] },
421
+ targetAnchor: { default: [
422
+ 0,
423
+ 0,
424
+ 0
425
+ ] },
426
+ length: { default: 0 },
427
+ stiffness: { default: 50 },
428
+ damping: { default: 5 }
429
+ };
430
+ type = "spherical";
431
+ target = "";
432
+ anchor = [
433
+ 0,
434
+ 0,
435
+ 0
436
+ ];
437
+ targetAnchor = [
438
+ 0,
439
+ 0,
440
+ 0
441
+ ];
442
+ /** rope/spring rest length in m; 0 = measure body distance at creation. */
443
+ length = 0;
444
+ stiffness = 50;
445
+ damping = 5;
446
+ onEnterTree() {
447
+ if (!(this.parent instanceof PhysicsBody3D)) throw new IncantoError("TREE_VIOLATION", `Joint3D '${this.name}' must be a child of a physics body (its body A); parent is '${this.parent?.name ?? "none"}'.`);
448
+ if (this.target === "") throw new IncantoError("BAD_FORMAT", `Joint3D '${this.name}' needs a "target" node path to the body it links to.`);
449
+ }
450
+ /** @internal The two bodies, resolved (throws on a bad target at step time). */
451
+ _resolveBodies() {
452
+ const a = this.parent;
453
+ const b = this.getNodeOrNull(this.target);
454
+ if (!(b instanceof PhysicsBody3D)) throw new IncantoError("NODE_NOT_FOUND", `Joint3D '${this.name}': target '${this.target}' is not a physics body.`);
455
+ return {
456
+ a,
457
+ b
458
+ };
459
+ }
460
+ };
461
+ //#endregion
376
462
  //#region src/3d/terrain/splat-material.ts
377
463
  /** Splice `code` immediately AFTER `marker` — a missing marker is a hard error
378
464
  * (a silently unpatched terrain shader renders weight-colored vertex soup). */
@@ -4885,6 +4971,9 @@ const MATERIAL_KEYS = [
4885
4971
  "wireframe",
4886
4972
  "flatShading",
4887
4973
  "opacity",
4974
+ "clearcoat",
4975
+ "clearcoatRoughness",
4976
+ "envMapIntensity",
4888
4977
  "depthTest",
4889
4978
  "depthWrite",
4890
4979
  "map",
@@ -4897,7 +4986,8 @@ const MATERIAL_KEYS = [
4897
4986
  * - `size [x,y,z]`: box extents; sphere/gem radius = x; capsule radius = x,
4898
4987
  * height = y; plane = x·z ground plane (laid flat); cylinder radius = x, height = y
4899
4988
  * - `material`: `{color, metalness, roughness, wireframe, flatShading, opacity,
4900
- * depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat}` —
4989
+ * clearcoat, clearcoatRoughness, envMapIntensity, depthTest, depthWrite,
4990
+ * emissive, emissiveIntensity, map, normalMap, repeat}` —
4901
4991
  * `map`/`normalMap` are texture URLs loaded lazily (headless-safe: geometry and
4902
4992
  * physics never wait on them), tiled by `repeat: [u, v]`. `gem` + `flatShading:
4903
4993
  * true` + a low `roughness` reads as a sparkling faceted jewel (spin it for the
@@ -4938,13 +5028,13 @@ var MeshInstance3D = class extends Node3D {
4938
5028
  receiveShadow = false;
4939
5029
  /** Loader hook: malformed materials fail at LOAD time, not at render. */
4940
5030
  static validateJson(node) {
4941
- validateMaterial(node.material, node.name);
5031
+ validateMeshMaterial(node.material, node.name);
4942
5032
  }
4943
5033
  geometryKey = "";
4944
5034
  textureKey = "";
4945
5035
  loadedTextures = [];
4946
5036
  _createObject3D() {
4947
- return new Mesh(new BoxGeometry(1, 1, 1), new MeshStandardMaterial());
5037
+ return new Mesh(new BoxGeometry(1, 1, 1), new MeshPhysicalMaterial());
4948
5038
  }
4949
5039
  _syncObject3D() {
4950
5040
  super._syncObject3D();
@@ -4956,22 +5046,7 @@ var MeshInstance3D = class extends Node3D {
4956
5046
  this.geometryKey = key;
4957
5047
  }
4958
5048
  const mat = m.material;
4959
- mat.color.set(this.material.color ?? "#ffffff");
4960
- mat.metalness = this.material.metalness ?? 0;
4961
- mat.roughness = this.material.roughness ?? 1;
4962
- mat.wireframe = this.material.wireframe ?? false;
4963
- mat.emissive.set(this.material.emissive ?? "#000000");
4964
- mat.emissiveIntensity = this.material.emissiveIntensity ?? 1;
4965
- const opacity = this.material.opacity ?? 1;
4966
- mat.opacity = opacity;
4967
- mat.transparent = opacity < 1;
4968
- mat.depthTest = this.material.depthTest ?? true;
4969
- mat.depthWrite = this.material.depthWrite ?? true;
4970
- const flat = this.material.flatShading ?? false;
4971
- if (mat.flatShading !== flat) {
4972
- mat.flatShading = flat;
4973
- mat.needsUpdate = true;
4974
- }
5049
+ applyMeshMaterial(mat, this.material);
4975
5050
  this.syncTextures(mat);
4976
5051
  m.castShadow = this.castShadow;
4977
5052
  m.receiveShadow = this.receiveShadow;
@@ -5014,11 +5089,42 @@ var MeshInstance3D = class extends Node3D {
5014
5089
  super.free();
5015
5090
  }
5016
5091
  };
5092
+ /**
5093
+ * Apply the shared material-prop pipeline (everything except textures) with
5094
+ * reset-to-default semantics: the JSON object IS the material state — a
5095
+ * replaced `{color}` must not keep a stale wireframe from a previous value.
5096
+ * Shared by every node that takes a `MeshMaterialProps` (MeshInstance3D,
5097
+ * LoftMesh3D).
5098
+ */
5099
+ function applyMeshMaterial(mat, props) {
5100
+ mat.color.set(props.color ?? "#ffffff");
5101
+ mat.metalness = props.metalness ?? 0;
5102
+ mat.roughness = props.roughness ?? 1;
5103
+ mat.wireframe = props.wireframe ?? false;
5104
+ mat.emissive.set(props.emissive ?? "#000000");
5105
+ mat.emissiveIntensity = props.emissiveIntensity ?? 1;
5106
+ const opacity = props.opacity ?? 1;
5107
+ mat.opacity = opacity;
5108
+ mat.transparent = opacity < 1;
5109
+ const clearcoat = props.clearcoat ?? 0;
5110
+ if (mat.clearcoat > 0 !== clearcoat > 0) mat.needsUpdate = true;
5111
+ mat.clearcoat = clearcoat;
5112
+ mat.clearcoatRoughness = props.clearcoatRoughness ?? 0;
5113
+ mat.envMapIntensity = props.envMapIntensity ?? 1;
5114
+ mat.depthTest = props.depthTest ?? true;
5115
+ mat.depthWrite = props.depthWrite ?? true;
5116
+ const flat = props.flatShading ?? false;
5117
+ if (mat.flatShading !== flat) {
5118
+ mat.flatShading = flat;
5119
+ mat.needsUpdate = true;
5120
+ }
5121
+ }
5017
5122
  /** Hard-fail on malformed material JSON — typos and bad shapes surface at
5018
- * load with the valid options, instead of silently rendering defaults. */
5019
- function validateMaterial(material, name) {
5123
+ * load with the valid options, instead of silently rendering defaults.
5124
+ * Exported for nodes that reuse `MeshMaterialProps` (LoftMesh3D). */
5125
+ function validateMeshMaterial(material, name, typeName = "MeshInstance3D") {
5020
5126
  const fail = (message) => {
5021
- throw new IncantoError("BAD_FORMAT", `MeshInstance3D '${name}' ${message}`, {
5127
+ throw new IncantoError("BAD_FORMAT", `${typeName} '${name}' ${message}`, {
5022
5128
  prop: "material",
5023
5129
  validOptions: [...MATERIAL_KEYS]
5024
5130
  });
@@ -5037,11 +5143,14 @@ function validateMaterial(material, name) {
5037
5143
  for (const key of [
5038
5144
  "metalness",
5039
5145
  "roughness",
5040
- "opacity"
5146
+ "opacity",
5147
+ "clearcoat",
5148
+ "clearcoatRoughness"
5041
5149
  ]) {
5042
5150
  const value = material[key];
5043
5151
  if (value !== void 0 && !(typeof value === "number" && value >= 0 && value <= 1)) fail(`material.${key} must be a number in 0..1, got ${JSON.stringify(value)}.`);
5044
5152
  }
5153
+ if (material.envMapIntensity !== void 0 && !(typeof material.envMapIntensity === "number" && Number.isFinite(material.envMapIntensity) && material.envMapIntensity >= 0)) fail(`material.envMapIntensity must be a number >= 0, got ${JSON.stringify(material.envMapIntensity)}.`);
5045
5154
  for (const key of [
5046
5155
  "wireframe",
5047
5156
  "flatShading",
@@ -5071,6 +5180,185 @@ function buildGeometry(kind, size) {
5071
5180
  }
5072
5181
  }
5073
5182
  //#endregion
5183
+ //#region src/3d/nodes/loft-mesh-3d.ts
5184
+ /**
5185
+ * A smooth swept hull ("loft"): superellipse cross-sections interpolated
5186
+ * along Z with rounded end caps and per-vertex normals — curved highlights
5187
+ * and sky reflections sweep across it like sheet metal, which no box/capsule
5188
+ * assembly can do. THE node for vehicle bodies, glass canopies, boat hulls,
5189
+ * fuselages.
5190
+ *
5191
+ * - `sections`: ≥2 stations front→back (see LoftSection)
5192
+ * - `slices`: vertices around each ring (8..64, default 24)
5193
+ * - `smooth`: interpolated rings per section gap (1..16, default 4)
5194
+ * - `material`: same object as MeshInstance3D **minus textures** (a loft has
5195
+ * no UVs — map/normalMap/repeat hard-fail). Pair `clearcoat` paint or a
5196
+ * high `envMapIntensity` glass with the smooth normals for the automotive
5197
+ * look.
5198
+ */
5199
+ var LoftMesh3D = class extends Node3D {
5200
+ static typeName = "LoftMesh3D";
5201
+ static props = {
5202
+ sections: { default: [] },
5203
+ slices: { default: 24 },
5204
+ smooth: { default: 4 },
5205
+ material: { default: {} },
5206
+ castShadow: { default: false },
5207
+ receiveShadow: { default: false }
5208
+ };
5209
+ sections = [];
5210
+ slices = 24;
5211
+ smooth = 4;
5212
+ material = {};
5213
+ castShadow = false;
5214
+ receiveShadow = false;
5215
+ /** Loader hook: malformed sections/materials fail at LOAD time. */
5216
+ static validateJson(node) {
5217
+ const loft = node;
5218
+ validateSections(loft.sections, node.name);
5219
+ validateMeshMaterial(loft.material, node.name, "LoftMesh3D");
5220
+ for (const key of [
5221
+ "map",
5222
+ "normalMap",
5223
+ "repeat"
5224
+ ]) if (loft.material[key] !== void 0) throw new IncantoError("BAD_FORMAT", `LoftMesh3D '${node.name}' material does not support '${key}' — a loft has no UVs; use color/metalness/roughness/clearcoat/envMapIntensity instead.`, { prop: "material" });
5225
+ }
5226
+ geometryKey = "";
5227
+ _createObject3D() {
5228
+ return new Mesh(new BufferGeometry(), new MeshPhysicalMaterial());
5229
+ }
5230
+ _syncObject3D() {
5231
+ super._syncObject3D();
5232
+ const m = this._ensureObject3D();
5233
+ const key = JSON.stringify([
5234
+ this.sections,
5235
+ this.slices,
5236
+ this.smooth
5237
+ ]);
5238
+ if (key !== this.geometryKey && this.sections.length >= 2) {
5239
+ m.geometry.dispose();
5240
+ m.geometry = buildLoftGeometry(this.sections, this.slices, this.smooth);
5241
+ this.geometryKey = key;
5242
+ }
5243
+ applyMeshMaterial(m.material, this.material);
5244
+ m.castShadow = this.castShadow;
5245
+ m.receiveShadow = this.receiveShadow;
5246
+ }
5247
+ };
5248
+ function validateSections(sections, name) {
5249
+ const fail = (message) => {
5250
+ throw new IncantoError("BAD_FORMAT", `LoftMesh3D '${name}' ${message}`, { prop: "sections" });
5251
+ };
5252
+ if (!Array.isArray(sections) || sections.length < 2) fail(`sections needs at least 2 stations, got ${JSON.stringify(sections)}.`);
5253
+ for (const [i, s] of sections.entries()) {
5254
+ if (typeof s !== "object" || s === null) fail(`sections[${i}] must be an object.`);
5255
+ for (const k of [
5256
+ "z",
5257
+ "width",
5258
+ "height",
5259
+ "y"
5260
+ ]) if (typeof s[k] !== "number" || !Number.isFinite(s[k])) fail(`sections[${i}].${k} must be a finite number, got ${JSON.stringify(s[k])}.`);
5261
+ if (s.width <= 0 || s.height <= 0) fail(`sections[${i}] width/height must be positive, got ${s.width}×${s.height}.`);
5262
+ if (s.corner !== void 0 && !(typeof s.corner === "number" && s.corner >= 0 && s.corner <= 1)) fail(`sections[${i}].corner must be in 0..1, got ${JSON.stringify(s.corner)}.`);
5263
+ if (i > 0 && s.z <= sections[i - 1].z) fail(`sections[].z must be strictly ascending (sections[${i}].z=${s.z}).`);
5264
+ }
5265
+ }
5266
+ /** Endpoint-clamped Catmull-Rom through scalar samples. */
5267
+ function catmullRom(p0, p1, p2, p3, t) {
5268
+ const t2 = t * t;
5269
+ const t3 = t2 * t;
5270
+ return .5 * (2 * p1 + (p2 - p0) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3);
5271
+ }
5272
+ /** Sample the section list into (K-1)*smooth+1 interpolated rings. */
5273
+ function sampleRings(sections, smooth) {
5274
+ const get = (i) => {
5275
+ const s = sections[Math.max(0, Math.min(sections.length - 1, i))];
5276
+ return {
5277
+ z: s.z,
5278
+ width: s.width,
5279
+ height: s.height,
5280
+ y: s.y,
5281
+ corner: s.corner ?? .7
5282
+ };
5283
+ };
5284
+ const rings = [];
5285
+ for (let i = 0; i < sections.length - 1; i++) {
5286
+ const a = get(i - 1);
5287
+ const b = get(i);
5288
+ const c = get(i + 1);
5289
+ const d = get(i + 2);
5290
+ const steps = i === sections.length - 2 ? smooth + 1 : smooth;
5291
+ for (let s = 0; s < steps; s++) {
5292
+ const t = s / smooth;
5293
+ rings.push({
5294
+ z: catmullRom(a.z, b.z, c.z, d.z, t),
5295
+ width: Math.max(1e-4, catmullRom(a.width, b.width, c.width, d.width, t)),
5296
+ height: Math.max(1e-4, catmullRom(a.height, b.height, c.height, d.height, t)),
5297
+ y: catmullRom(a.y, b.y, c.y, d.y, t),
5298
+ corner: Math.min(1, Math.max(0, catmullRom(a.corner, b.corner, c.corner, d.corner, t)))
5299
+ });
5300
+ }
5301
+ }
5302
+ return rings;
5303
+ }
5304
+ /**
5305
+ * Build the swept surface: ring grid + two cap-centre vertices, indexed
5306
+ * without a seam so `computeVertexNormals` shades the hull perfectly smooth.
5307
+ */
5308
+ function buildLoftGeometry(sections, slices, smooth) {
5309
+ const S = Math.max(8, Math.min(64, Math.round(slices)));
5310
+ const rings = sampleRings(sections, Math.max(1, Math.min(16, Math.round(smooth))));
5311
+ const R = rings.length;
5312
+ const positions = new Float32Array((R * S + 2) * 3);
5313
+ const first = rings[0];
5314
+ const last = rings[R - 1];
5315
+ positions.set([
5316
+ 0,
5317
+ first.y,
5318
+ first.z
5319
+ ], 0);
5320
+ positions.set([
5321
+ 0,
5322
+ last.y,
5323
+ last.z
5324
+ ], 3);
5325
+ for (let i = 0; i < R; i++) {
5326
+ const ring = rings[i];
5327
+ const nTop = 8 - 6 * ring.corner;
5328
+ const nBottom = Math.max(nTop, 5);
5329
+ for (let j = 0; j < S; j++) {
5330
+ const theta = j / S * Math.PI * 2;
5331
+ const c = Math.cos(theta);
5332
+ const sn = Math.sin(theta);
5333
+ const n = sn >= 0 ? nTop : nBottom;
5334
+ const x = ring.width / 2 * Math.sign(c) * Math.abs(c) ** (2 / n);
5335
+ const yy = ring.height / 2 * Math.sign(sn) * Math.abs(sn) ** (2 / n);
5336
+ const v = (2 + i * S + j) * 3;
5337
+ positions[v] = x;
5338
+ positions[v + 1] = ring.y + yy;
5339
+ positions[v + 2] = ring.z;
5340
+ }
5341
+ }
5342
+ const indices = [];
5343
+ const at = (i, j) => 2 + i * S + j % S;
5344
+ for (let i = 0; i < R - 1; i++) for (let j = 0; j < S; j++) {
5345
+ const a = at(i, j);
5346
+ const b = at(i, j + 1);
5347
+ const c = at(i + 1, j);
5348
+ const d = at(i + 1, j + 1);
5349
+ indices.push(a, b, c, b, d, c);
5350
+ }
5351
+ for (let j = 0; j < S; j++) {
5352
+ indices.push(0, at(0, j + 1), at(0, j));
5353
+ indices.push(1, at(R - 1, j), at(R - 1, j + 1));
5354
+ }
5355
+ const geo = new BufferGeometry();
5356
+ geo.setAttribute("position", new BufferAttribute(positions, 3));
5357
+ geo.setIndex(indices);
5358
+ geo.computeVertexNormals();
5359
+ return geo;
5360
+ }
5361
+ //#endregion
5074
5362
  //#region src/3d/animation/rigmap.ts
5075
5363
  /**
5076
5364
  * Mixamo rig name → VRM humanoid bone name (the vibe-starter-3d /
@@ -5227,7 +5515,9 @@ var ModelInstance3D = class extends Node3D {
5227
5515
  castShadow: { default: false },
5228
5516
  animationLoop: { default: true },
5229
5517
  receiveShadow: { default: false },
5230
- tint: { default: "" }
5518
+ tint: { default: "" },
5519
+ metalness: { default: -1 },
5520
+ roughness: { default: -1 }
5231
5521
  };
5232
5522
  model = "";
5233
5523
  animation = "";
@@ -5239,6 +5529,12 @@ var ModelInstance3D = class extends Node3D {
5239
5529
  /** Hex colour multiplied into every material — RESKIN one shared GLB into many
5240
5530
  * variants (e.g. a sickly-green zombie from the base human). '' = untinted. */
5241
5531
  tint = "";
5532
+ /** Override every material's metalness (0..1); -1 keeps the authored value.
5533
+ * Low-poly kits often ship fully rough — 0.5–0.8 turns flat paint into a
5534
+ * glinting clear-coat under the scene's IBL. Cloned per instance like tint. */
5535
+ metalness = -1;
5536
+ /** Override every material's roughness (0..1); -1 keeps the authored value. */
5537
+ roughness = -1;
5242
5538
  store = null;
5243
5539
  entry = null;
5244
5540
  mountedRef = "";
@@ -5252,11 +5548,25 @@ var ModelInstance3D = class extends Node3D {
5252
5548
  * frames after `this.animation` already moved on). */
5253
5549
  actionKeys = /* @__PURE__ */ new WeakMap();
5254
5550
  warnedClaim = false;
5255
- /** Per-mesh original (shared) materials, captured at mount so `tint` can always
5256
- * re-derive from the source. Our clones are tracked for disposal. */
5551
+ /** Per-mesh original (shared) materials, captured at mount so tint/overrides
5552
+ * can always re-derive from the source. Our clones are tracked for disposal. */
5257
5553
  tintTargets = [];
5258
5554
  tintedClones = [];
5259
- appliedTint = "";
5555
+ appliedMods = "";
5556
+ /** Named sub-node poses (degrees), re-applied every sync — drive wheels,
5557
+ * turrets, doors from behaviors without touching three directly. */
5558
+ nodePoses = /* @__PURE__ */ new Map();
5559
+ /**
5560
+ * Rotate a named sub-node of the loaded model (local Euler XYZ, degrees) —
5561
+ * e.g. spin a GLB car's `wheel-front-left` every frame, aim a turret, swing a
5562
+ * door. Poses persist and re-apply on every sync (safe to set before the
5563
+ * model finishes loading). Unknown names are ignored. `npx incanto-model
5564
+ * <file>` lists a model's node names.
5565
+ */
5566
+ poseNode(name, rotation) {
5567
+ this.nodePoses.set(name, rotation);
5568
+ this.applyPoses();
5569
+ }
5260
5570
  _createObject3D() {
5261
5571
  return new Group();
5262
5572
  }
@@ -5310,7 +5620,7 @@ var ModelInstance3D = class extends Node3D {
5310
5620
  });
5311
5621
  this.tintTargets = [];
5312
5622
  this.tintedClones = [];
5313
- this.appliedTint = "";
5623
+ this.appliedMods = "";
5314
5624
  instance.traverse((obj) => {
5315
5625
  const mesh = obj;
5316
5626
  if (mesh.isMesh && mesh.material) this.tintTargets.push({
@@ -5339,34 +5649,51 @@ var ModelInstance3D = class extends Node3D {
5339
5649
  mesh.receiveShadow = this.receiveShadow;
5340
5650
  }
5341
5651
  });
5342
- this.applyTint();
5652
+ this.applyMaterialMods();
5653
+ this.applyPoses();
5343
5654
  this.syncAnimation(assets);
5344
5655
  }
5345
- /** Multiply `tint` into every material a per-instance reskin of one shared
5346
- * GLB (e.g. base human green zombie). Clones the captured originals (never
5347
- * mutates the shared source), disposing prior clones on change. */
5348
- applyTint() {
5349
- if (this.tint === this.appliedTint) return;
5656
+ /** Apply per-instance material mods`tint` (multiplied into colour) and the
5657
+ * `metalness`/`roughness` overrides by cloning the captured originals
5658
+ * (never mutates the shared source), disposing prior clones on change. */
5659
+ applyMaterialMods() {
5660
+ const key = `${this.tint}|${this.metalness}|${this.roughness}`;
5661
+ if (key === this.appliedMods) return;
5350
5662
  for (const c of this.tintedClones) c.dispose();
5351
5663
  this.tintedClones = [];
5352
5664
  const tint = this.tint ? new Color(this.tint) : null;
5665
+ const hasMods = tint !== null || this.metalness >= 0 || this.roughness >= 0;
5353
5666
  for (const t of this.tintTargets) {
5354
- if (!tint) {
5667
+ if (!hasMods) {
5355
5668
  t.mesh.material = t.original;
5356
5669
  continue;
5357
5670
  }
5358
- const tintOne = (src) => {
5671
+ const modOne = (src) => {
5359
5672
  const clone = src.clone();
5360
- const cm = clone;
5361
- const sc = src.color;
5362
- if (cm.color) cm.color = sc ? sc.clone().multiply(tint) : tint.clone();
5673
+ if (tint) {
5674
+ const cm = clone;
5675
+ const sc = src.color;
5676
+ if (cm.color) cm.color = sc ? sc.clone().multiply(tint) : tint.clone();
5677
+ }
5678
+ const pbr = clone;
5679
+ if (this.metalness >= 0 && typeof pbr.metalness === "number") pbr.metalness = this.metalness;
5680
+ if (this.roughness >= 0 && typeof pbr.roughness === "number") pbr.roughness = this.roughness;
5363
5681
  clone.userData.incantoTinted = true;
5364
5682
  this.tintedClones.push(clone);
5365
5683
  return clone;
5366
5684
  };
5367
- t.mesh.material = Array.isArray(t.original) ? t.original.map(tintOne) : tintOne(t.original);
5685
+ t.mesh.material = Array.isArray(t.original) ? t.original.map(modOne) : modOne(t.original);
5686
+ }
5687
+ this.appliedMods = key;
5688
+ }
5689
+ /** Re-apply every queued named-node pose onto the mounted instance. */
5690
+ applyPoses() {
5691
+ if (!this.fitGroup || this.nodePoses.size === 0) return;
5692
+ const DEG = Math.PI / 180;
5693
+ for (const [name, rot] of this.nodePoses) {
5694
+ const obj = this.fitGroup.getObjectByName(name);
5695
+ if (obj) obj.rotation.set(rot[0] * DEG, rot[1] * DEG, rot[2] * DEG);
5368
5696
  }
5369
- this.appliedTint = this.tint;
5370
5697
  }
5371
5698
  syncAnimation(assets) {
5372
5699
  if (!this.mixer || !this.entry || this.playing === this.animation) return;
@@ -5426,7 +5753,7 @@ var ModelInstance3D = class extends Node3D {
5426
5753
  for (const c of this.tintedClones) c.dispose();
5427
5754
  this.tintedClones = [];
5428
5755
  this.tintTargets = [];
5429
- this.appliedTint = "";
5756
+ this.appliedMods = "";
5430
5757
  this.fitGroup = null;
5431
5758
  this.mixer = null;
5432
5759
  this.currentAction = null;
@@ -5573,7 +5900,7 @@ var Particles3D = class Particles3D extends Node3D {
5573
5900
  gravity: [
5574
5901
  (this.gravity[0] ?? 0) * PX_TO_M,
5575
5902
  (this.gravity[1] ?? 0) * PX_TO_M,
5576
- 0
5903
+ (this.gravity[2] ?? 0) * PX_TO_M
5577
5904
  ],
5578
5905
  drag: this.drag,
5579
5906
  maxParticles: this.maxParticles,
@@ -5584,6 +5911,7 @@ var Particles3D = class Particles3D extends Node3D {
5584
5911
  }
5585
5912
  update(dt) {
5586
5913
  const sim = this._ensureSim();
5914
+ sim.setRate(this.emitting ? this.rate : 0);
5587
5915
  if (this.burst > 0 && !this.bursted) {
5588
5916
  this.bursted = true;
5589
5917
  sim.burst(this.burst);
@@ -11095,11 +11423,13 @@ function registerNodes3D() {
11095
11423
  registerNode(Node3D);
11096
11424
  registerNode(Particles3D);
11097
11425
  registerNode(MeshInstance3D);
11426
+ registerNode(LoftMesh3D);
11098
11427
  registerNode(Sprite3D);
11099
11428
  registerNode(AnimatedSprite3D);
11100
11429
  registerNode(Billboard3D);
11101
11430
  registerNode(CharacterController3D);
11102
11431
  registerNode(VoxelGrid3D);
11432
+ registerNode(Joint3D);
11103
11433
  registerNode(Terrain3D);
11104
11434
  registerNode(Water3D);
11105
11435
  registerNode(Foliage3D);
@@ -11115,4 +11445,4 @@ function registerNodes3D() {
11115
11445
  registerNode(CharacterBody3D);
11116
11446
  }
11117
11447
  //#endregion
11118
- export { validateCollider3D as A, terrainThemeLayers as C, RigidBody3D as D, PhysicsBody3D as E, rigPose as F, cameraRelative as M, keyboardIntensity as N, StaticBody3D as O, movementState as P, TERRAIN_THEMES as S, CharacterBody3D as T, CharacterController3D as _, VoxelGrid3D as a, Terrain3D as b, ModelInstance3D as c, OmniLight3D as d, Foliage3D as f, FLOWER_VARIETIES as g, resolveFlowerDensity as h, VOXEL_PALETTE as i, QUARTER_PITCH as j, Node3D as k, MeshInstance3D as l, Flowers3D as m, Water3D as n, Tree3D as o, DENSITY_PRESETS as p, WATER_MAX_RIPPLES as r, Particles3D as s, registerNodes3D as t, DirectionalLight3D as u, Camera3D as v, Area3D as w, DEFAULT_TERRAIN_TEXTURE_BASE as x, Billboard3D as y };
11448
+ export { StaticBody3D as A, TERRAIN_THEMES as C, CharacterBody3D as D, Area3D as E, keyboardIntensity as F, movementState as I, rigPose as L, validateCollider3D as M, QUARTER_PITCH as N, PhysicsBody3D as O, cameraRelative as P, DEFAULT_TERRAIN_TEXTURE_BASE as S, Joint3D as T, FLOWER_VARIETIES as _, VoxelGrid3D as a, Billboard3D as b, ModelInstance3D as c, DirectionalLight3D as d, OmniLight3D as f, resolveFlowerDensity as g, Flowers3D as h, VOXEL_PALETTE as i, Node3D as j, RigidBody3D as k, LoftMesh3D as l, DENSITY_PRESETS as m, Water3D as n, Tree3D as o, Foliage3D as p, WATER_MAX_RIPPLES as r, Particles3D as s, registerNodes3D as t, MeshInstance3D as u, CharacterController3D as v, terrainThemeLayers as w, Terrain3D as x, Camera3D as y };
package/dist/test.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { c as JsonValue, i as SceneJson } from "./schema-CcoWb32N.js";
2
- import { O as LogEntry, T as Node, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAQq7HGM.js";
3
- import { t as LoadSceneOptions } from "./loader-Mo0KghCv.js";
2
+ import { O as LogEntry, T as Node, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-CWhW3oa6.js";
3
+ import { t as LoadSceneOptions } from "./loader-D2u0fVW2.js";
4
4
  import { t as IncantoError } from "./errors-BMFaY68Q.js";
5
5
 
6
6
  //#region src/test/index.d.ts
package/dist/test.js CHANGED
@@ -1,12 +1,12 @@
1
- import { _ as registerBehavior, n as loadScene } from "./loader-CGs_G-r0.js";
2
- import { a as Engine } from "./particle-sim-DYuSUxvK.js";
1
+ import { _ as registerBehavior, n as loadScene } from "./loader-B4OEXDZ8.js";
2
+ import { a as Engine } from "./particle-sim-CFkILGwh.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
6
- import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
7
- import { t as registerNodes2D } from "./register-DPEV9_9t.js";
8
- import { t as registerNodes3D } from "./register-BRy8FNox.js";
9
- import { t as registerNodesNet } from "./register-B0gq63VW.js";
6
+ import { n as registerGameplayBehaviors } from "./gameplay-CDFgSG6z.js";
7
+ import { t as registerNodes2D } from "./register-D71C4rDC.js";
8
+ import { t as registerNodes3D } from "./register-DvPHJdVj.js";
9
+ import { t as registerNodesNet } from "./register-C35HDZpm.js";
10
10
  //#region src/test/index.ts
11
11
  /**
12
12
  * incanto/test — the browserless verification harness.
@@ -129,10 +129,10 @@ async function runScript(json, opts) {
129
129
  engine.setScene(scene);
130
130
  const physics = opts.physics ?? "auto";
131
131
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
132
- const { enablePhysics2D } = await import("./physics-2d-KuMWPTf6.js").then((n) => n.r);
132
+ const { enablePhysics2D } = await import("./physics-2d-KXn1N1jF.js").then((n) => n.r);
133
133
  await enablePhysics2D(engine);
134
134
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
135
- const { enablePhysics3D } = await import("./physics-3d-DmNCeh58.js").then((n) => n.r);
135
+ const { enablePhysics3D } = await import("./physics-3d-C58oQzTX.js").then((n) => n.r);
136
136
  await enablePhysics3D(engine);
137
137
  }
138
138
  const failures = [];
@@ -234,10 +234,10 @@ async function createPlaySession(json, opts = {}) {
234
234
  engine.setScene(scene);
235
235
  const physics = opts.physics ?? "auto";
236
236
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
237
- const { enablePhysics2D } = await import("./physics-2d-KuMWPTf6.js").then((n) => n.r);
237
+ const { enablePhysics2D } = await import("./physics-2d-KXn1N1jF.js").then((n) => n.r);
238
238
  await enablePhysics2D(engine);
239
239
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
240
- const { enablePhysics3D } = await import("./physics-3d-DmNCeh58.js").then((n) => n.r);
240
+ const { enablePhysics3D } = await import("./physics-3d-C58oQzTX.js").then((n) => n.r);
241
241
  await enablePhysics3D(engine);
242
242
  }
243
243
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -1 +1 @@
1
- import{t as e}from"./index-N8se-PhP.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};
1
+ import{t as e}from"./index-D2qufqUo.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};