incanto 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/3d.d.ts CHANGED
@@ -1598,6 +1598,24 @@ interface RiverHit {
1598
1598
  speed: number;
1599
1599
  halfWidth: number;
1600
1600
  }
1601
+ /** A place where the river stops flowing and starts falling. */
1602
+ interface RiverFall {
1603
+ /** Where the water LANDS (local frame) — the plunge point. */
1604
+ x: number;
1605
+ z: number;
1606
+ /** Water surface at the lip and at the base. */
1607
+ topY: number;
1608
+ baseY: number;
1609
+ /** Vertical drop in meters. */
1610
+ drop: number;
1611
+ /** Channel half-width at the base — how wide the curtain is. */
1612
+ halfWidth: number;
1613
+ /** Downstream direction at the base (a fall keeps carrying the water). */
1614
+ dirX: number;
1615
+ dirZ: number;
1616
+ /** Current speed at the base. */
1617
+ speed: number;
1618
+ }
1601
1619
  interface DownhillTraceOptions {
1602
1620
  /** Where the water starts (local frame, same as the sampler). */
1603
1621
  x: number;
@@ -1676,9 +1694,17 @@ declare class River3D extends Node3D implements SunConsumer3D {
1676
1694
  terrain: string;
1677
1695
  /** How hard the current sweeps bodies downstream (0 = visual only). */
1678
1696
  flowForce: number;
1697
+ /**
1698
+ * Mist at the foot of every waterfall the course contains: the node finds
1699
+ * the drops itself (see {@link detectFalls}) and hangs a Particles3D plume
1700
+ * on each, sized by how far the water falls. 0 = off.
1701
+ */
1702
+ spray: number;
1679
1703
  override renderOrder: number;
1680
1704
  private time;
1681
1705
  private rings;
1706
+ private falls;
1707
+ private sprayKey;
1682
1708
  private courseKey;
1683
1709
  private bedAt;
1684
1710
  private world;
@@ -1696,6 +1722,16 @@ declare class River3D extends Node3D implements SunConsumer3D {
1696
1722
  sampleAt(x: number, z: number): (RiverHit & {
1697
1723
  surfaceY: number;
1698
1724
  }) | null;
1725
+ /**
1726
+ * The waterfalls on this course — where the water stops flowing and starts
1727
+ * falling, with the drop height and the plunge point in WORLD coordinates.
1728
+ * Hang your own effects (sound, a rainbow, a hidden cave) on these.
1729
+ */
1730
+ fallsAt(): (RiverFall & {
1731
+ worldX: number;
1732
+ worldY: number;
1733
+ worldZ: number;
1734
+ })[];
1699
1735
  /** @internal The derived centerline stations (surface, grade, current). */
1700
1736
  _rings(): readonly RiverRing[];
1701
1737
  /**
@@ -1716,6 +1752,12 @@ declare class River3D extends Node3D implements SunConsumer3D {
1716
1752
  private buildMaterial;
1717
1753
  private syncUniforms;
1718
1754
  /**
1755
+ * Hang a mist plume on every fall. Runtime children (`__spray*`), rebuilt
1756
+ * only when the course or the dial changes — a plume is a Particles3D, so it
1757
+ * carries its own budget, culling and editor visibility for free.
1758
+ */
1759
+ private syncSpray;
1760
+ /**
1719
1761
  * Drag every body floating in the channel toward the water's own velocity.
1720
1762
  * Drag rather than thrust: a raft accelerates until it matches the current
1721
1763
  * and then coasts, and a swimmer can still fight across a slow ford.
@@ -2276,6 +2318,13 @@ declare class Water3D extends Node3D implements RenderHook3D, SunConsumer3D {
2276
2318
  swellDirectionDeg: number;
2277
2319
  /** Primary swell wavelength in meters (the two crossing trains derive). */
2278
2320
  swellWavelength: number;
2321
+ /**
2322
+ * GERSTNER pinch on the swell: 0 keeps the legacy height-only trains, 1 is
2323
+ * maximum trochoid (crests narrow to a peak, troughs go broad). It also
2324
+ * decides where whitecaps break — the surface's Jacobian collapses on a
2325
+ * folding crest, and that is where foam belongs.
2326
+ */
2327
+ swellSteepness: number;
2279
2328
  /** Open-water whitecap foam on the tallest crests, 0–1 (fancy only). Works
2280
2329
  * best with `swell` — the caps ride the swell's crest lines. */
2281
2330
  whitecaps: number;
package/dist/3d.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-pNf3sI-Q.js";
3
- import { A as TERRAIN_THEMES, B as QUARTER_PITCH, C as CharacterController3D, D as Billboard3D, E as BoneAttachment3D, F as PhysicsBody3D, H as keyboardIntensity, I as RigidBody3D, L as StaticBody3D, M as Joint3D, N as Area3D, O as Terrain3D, P as CharacterBody3D, R as Node3D, S as FLOWER_VARIETIES, T as BoneLookAt3D, U as movementState, V as cameraRelative, W as rigPose, _ as MeshInstance3D, a as VoxelGrid3D, b as Flowers3D, c as River3D, d as Particles3D, f as ModelInstance3D, g as InstancedMesh3D, h as OmniLight3D, i as VOXEL_PALETTE, j as terrainThemeLayers, k as DEFAULT_TERRAIN_TEXTURE_BASE, l as traceDownhillPath, m as DirectionalLight3D, n as Water3D, o as Tree3D, p as LoftMesh3D, s as Trail3D, t as registerNodes3D, u as WATER_MAX_RIPPLES, v as Foliage3D, w as Camera3D, x as resolveFlowerDensity, y as DENSITY_PRESETS } from "./register-BWFgsGra.js";
2
+ import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-xbxZRFwR.js";
3
+ import { A as TERRAIN_THEMES, B as QUARTER_PITCH, C as CharacterController3D, D as Billboard3D, E as BoneAttachment3D, F as PhysicsBody3D, H as keyboardIntensity, I as RigidBody3D, L as StaticBody3D, M as Joint3D, N as Area3D, O as Terrain3D, P as CharacterBody3D, R as Node3D, S as FLOWER_VARIETIES, T as BoneLookAt3D, U as movementState, V as cameraRelative, W as rigPose, _ as MeshInstance3D, a as VoxelGrid3D, b as Flowers3D, c as River3D, d as Particles3D, f as ModelInstance3D, g as InstancedMesh3D, h as OmniLight3D, i as VOXEL_PALETTE, j as terrainThemeLayers, k as DEFAULT_TERRAIN_TEXTURE_BASE, l as traceDownhillPath, m as DirectionalLight3D, n as Water3D, o as Tree3D, p as LoftMesh3D, s as Trail3D, t as registerNodes3D, u as WATER_MAX_RIPPLES, v as Foliage3D, w as Camera3D, x as resolveFlowerDensity, y as DENSITY_PRESETS } from "./register-BC55Ez6I.js";
4
4
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
5
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DxFDfUey.js";
5
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DHTZUrr0.js";
6
6
  //#region src/3d/environment-runtime.ts
7
7
  /**
8
8
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -5,8 +5,8 @@ import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
6
6
  import { n as registerGameplayBehaviors } from "./gameplay-sSqrUcnz.js";
7
7
  import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
8
- import { F as PhysicsBody3D, R as Node3D, f as ModelInstance3D, m as DirectionalLight3D, r as createCausticsQuad, t as registerNodes3D, w as Camera3D } from "./register-BWFgsGra.js";
9
- import { n as enablePhysics3D } from "./physics-3d-DxFDfUey.js";
8
+ import { F as PhysicsBody3D, R as Node3D, f as ModelInstance3D, m as DirectionalLight3D, r as createCausticsQuad, t as registerNodes3D, w as Camera3D } from "./register-BC55Ez6I.js";
9
+ import { n as enablePhysics3D } from "./physics-3d-DHTZUrr0.js";
10
10
  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";
11
11
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
12
12
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
package/dist/index.js CHANGED
@@ -295,6 +295,6 @@ function newUid() {
295
295
  //#endregion
296
296
  //#region src/index.ts
297
297
  /** Engine version. Kept in sync with package.json by the release pipeline. */
298
- const VERSION = "0.20.0";
298
+ const VERSION = "0.21.0";
299
299
  //#endregion
300
300
  export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiText, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, startRecording, synthSfx };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
4
- import { F as PhysicsBody3D, I as RigidBody3D, M as Joint3D, N as Area3D, O as Terrain3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./register-BWFgsGra.js";
4
+ import { F as PhysicsBody3D, I as RigidBody3D, M as Joint3D, N as Area3D, O as Terrain3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./register-BC55Ez6I.js";
5
5
  import { Euler, Quaternion } from "three";
6
6
  //#region src/3d/physics/physics-3d.ts
7
7
  var physics_3d_exports = /* @__PURE__ */ __exportAll({
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-pNf3sI-Q.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-Ct86zczq.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-xbxZRFwR.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-Ct86zczq.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -6910,15 +6910,17 @@ function buildRiverRings(opts) {
6910
6910
  const raw = bed[i];
6911
6911
  descending[i] = i === 0 ? raw : Math.min(raw, descending[i - 1]);
6912
6912
  }
6913
- const smoothed = descending.map((_, i) => {
6913
+ const keep = Math.max(.5, opts.depth * 1.5);
6914
+ const smoothed = descending.map((center, i) => {
6914
6915
  let sum = 0;
6915
6916
  let n = 0;
6916
6917
  for (let k = -3; k <= PROFILE_SMOOTH; k++) {
6917
- const j = Math.min(Math.max(i + k, 0), descending.length - 1);
6918
- sum += descending[j];
6918
+ const h = descending[Math.min(Math.max(i + k, 0), descending.length - 1)];
6919
+ if (Math.abs(h - center) > keep) continue;
6920
+ sum += h;
6919
6921
  n++;
6920
6922
  }
6921
- return sum / n;
6923
+ return n > 0 ? sum / n : center;
6922
6924
  });
6923
6925
  const profile = new Array(smoothed.length);
6924
6926
  for (let i = 0; i < smoothed.length; i++) {
@@ -7079,6 +7081,39 @@ function hitFrom(a, b, t, x, z, capped) {
7079
7081
  };
7080
7082
  }
7081
7083
  /**
7084
+ * Find the waterfalls in a course: maximal runs of steep stations, reported at
7085
+ * the station where the water lands. Callers hang spray on these, and the
7086
+ * shader aerates the curtain from the same grade the detector reads — one
7087
+ * source of truth for "this is falling, not flowing".
7088
+ */
7089
+ function detectFalls(rings, opts = {}) {
7090
+ const minGrade = opts.minGrade ?? .45;
7091
+ const minDrop = opts.minDrop ?? 1.2;
7092
+ const out = [];
7093
+ let start = -1;
7094
+ for (let i = 0; i <= rings.length; i++) {
7095
+ const steep = i < rings.length && rings[i].slope >= minGrade;
7096
+ if (steep && start < 0) start = i;
7097
+ if (steep || start < 0) continue;
7098
+ const lip = rings[Math.max(0, start - 1)];
7099
+ const base = rings[Math.min(rings.length - 1, i)];
7100
+ const drop = lip.surfaceY - base.surfaceY;
7101
+ if (drop >= minDrop) out.push({
7102
+ x: base.x,
7103
+ z: base.z,
7104
+ topY: lip.surfaceY,
7105
+ baseY: base.surfaceY,
7106
+ drop,
7107
+ halfWidth: base.halfWidth,
7108
+ dirX: base.tx,
7109
+ dirZ: base.tz,
7110
+ speed: base.speed
7111
+ });
7112
+ start = -1;
7113
+ }
7114
+ return out;
7115
+ }
7116
+ /**
7082
7117
  * Trace where water would actually run from a point: repeated steepest descent
7083
7118
  * with momentum, so the line follows a valley floor instead of rattling between
7084
7119
  * its walls. Ends when the ground goes flat (a pool), the trace leaves the play
@@ -7225,7 +7260,10 @@ void main() {
7225
7260
 
7226
7261
  // hydraulic jump: in a chute the wave train stands still in the world (phase
7227
7262
  // on the downstream coordinate, NEVER scrolled) and only breathes in time
7228
- float chute = smoothstep(0.035, 0.16, grade) * smoothstep(0.6, 2.4, speed);
7263
+ // standing waves belong to a RAPID: past ~20° the water has left the bed and
7264
+ // a wave train on a falling curtain reads as a rippling flag
7265
+ float chute = smoothstep(0.035, 0.16, grade) * smoothstep(0.6, 2.4, speed) *
7266
+ (1.0 - smoothstep(0.35, 0.7, grade));
7229
7267
  float standing =
7230
7268
  sin(aFlowUv.x * 1.9 + sin(uTime * 0.7) * 0.35) * 0.6 +
7231
7269
  sin(aFlowUv.x * 3.6 + sin(uTime * 0.9) * 0.5) * 0.4;
@@ -7329,8 +7367,11 @@ void main() {
7329
7367
  vec3 ripB = riverRipple(base - vec2(travel * pB, 0.0));
7330
7368
  vec3 ripple = mix(ripA, ripB, blend);
7331
7369
 
7332
- float streakA = riverStreak(base * 0.55 - vec2(travel * pA * 0.55, 0.0));
7333
- float streakB = riverStreak(base * 0.55 - vec2(travel * pB * 0.55, 0.0));
7370
+ // on a curtain the pattern stretches into strands: the same field, sampled
7371
+ // with the downstream axis squeezed hard, is what reads as falling water
7372
+ float streakScale = mix(0.55, 0.16, smoothstep(0.35, 0.9, grade));
7373
+ float streakA = riverStreak(base * streakScale - vec2(travel * pA * streakScale, 0.0));
7374
+ float streakB = riverStreak(base * streakScale - vec2(travel * pB * streakScale, 0.0));
7334
7375
  float streak = mix(streakA, streakB, blend);
7335
7376
 
7336
7377
  // ripples flatten out where the water thins to a film over the bed
@@ -7356,14 +7397,25 @@ void main() {
7356
7397
  // shallow is RELATIVE to this river: a hand's depth is a riffle in a brook
7357
7398
  // and nothing at all in a millrace, so the thresholds ride uMaxDepth
7358
7399
  float shallowT = column / max(uMaxDepth, 0.05);
7359
- float riffle = (1.0 - smoothstep(0.12, 0.42, shallowT)) * smoothstep(0.7, 2.2, speed);
7360
- float drive = clamp(rapids * 1.0 + bankShear * 0.35 + riffle * 0.5, 0.0, 1.3) * uFoam;
7400
+ // a riffle needs water to break OVER something a centimetres-thin film on
7401
+ // a drying bar is a wet rock, and painting it white is what turns the tail of
7402
+ // a creek into a bright sheet
7403
+ float riffle =
7404
+ smoothstep(0.04, 0.13, column) *
7405
+ (1.0 - smoothstep(0.12, 0.42, shallowT)) *
7406
+ smoothstep(0.7, 2.2, speed);
7407
+ // FALLING, not flowing: past ~20° the sheet leaves the bed, shatters and
7408
+ // aerates. Aerated water is a white DIFFUSE body — it stops mirroring the
7409
+ // sky (that is the tell that separates a waterfall from a steep chute).
7410
+ float fall = smoothstep(0.35, 0.9, grade);
7411
+ float drive = clamp(rapids * 1.0 + bankShear * 0.35 + riffle * 0.5 + fall * 1.6, 0.0, 2.0) * uFoam;
7361
7412
  // COVERAGE is the thing to be strict about: whitewater is where the surface
7362
7413
  // has actually broken, and a river that fizzes everywhere reads as milk
7363
7414
  float foam = smoothstep(0.74 - 0.45 * drive, 1.02 - 0.24 * drive, streak) * min(drive, 1.0);
7364
7415
  // crest froth: the tops of standing waves in a real rapid always carry white
7365
7416
  foam = max(foam, smoothstep(0.68, 0.96, ripple.x) * smoothstep(0.4, 1.0, rapids) * uFoam * 0.5);
7366
- foam = clamp(foam, 0.0, 1.0);
7417
+ // froth can never be more present than the water carrying it
7418
+ foam = clamp(foam, 0.0, 1.0) * smoothstep(0.02, 0.14, column);
7367
7419
 
7368
7420
  // ── body ────────────────────────────────────────────────────────────────
7369
7421
  float depthT = clamp(column / max(uMaxDepth, 0.01), 0.0, 1.0);
@@ -7413,10 +7465,16 @@ void main() {
7413
7465
  spec *= 0.45 + 0.55 * glint * uSparkle;
7414
7466
  spec *= 1.0 - foam * 0.85; // froth is rough — it kills the mirror
7415
7467
 
7468
+ fresnel *= 1.0 - 0.85 * fall; // froth scatters; it does not reflect
7416
7469
  vec3 color = mix(body, sky, fresnel);
7417
7470
  color += uSunColor * spec * 1.6;
7418
7471
  color = mix(color, uFoamColor, foam * 0.88);
7419
7472
 
7473
+ // the LIP reads glassy-dark for a moment before the sheet breaks — the
7474
+ // contrast against the white curtain below is most of the drop's drama
7475
+ float lip = smoothstep(0.12, 0.32, grade) * (1.0 - smoothstep(0.32, 0.55, grade));
7476
+ color = mix(color, color * 0.72, lip * 0.6);
7477
+
7420
7478
  // ── edges ───────────────────────────────────────────────────────────────
7421
7479
  // the ribbon is built wider than the channel; what makes its outline is the
7422
7480
  // water running out, so bars and boulders cut it exactly like the ground does
@@ -7521,7 +7579,8 @@ var River3D = class River3D extends Node3D {
7521
7579
  sunColor: { default: "#fff6e0" },
7522
7580
  sunIntensity: { default: 1 },
7523
7581
  terrain: { default: "" },
7524
- flowForce: { default: 1 }
7582
+ flowForce: { default: 1 },
7583
+ spray: { default: 1 }
7525
7584
  };
7526
7585
  /** Centerline control points `[[x, z], …]` in the node's local frame. */
7527
7586
  path = [];
@@ -7557,9 +7616,17 @@ var River3D = class River3D extends Node3D {
7557
7616
  terrain = "";
7558
7617
  /** How hard the current sweeps bodies downstream (0 = visual only). */
7559
7618
  flowForce = 1;
7619
+ /**
7620
+ * Mist at the foot of every waterfall the course contains: the node finds
7621
+ * the drops itself (see {@link detectFalls}) and hangs a Particles3D plume
7622
+ * on each, sized by how far the water falls. 0 = off.
7623
+ */
7624
+ spray = 1;
7560
7625
  renderOrder = 1;
7561
7626
  time = 0;
7562
7627
  rings = [];
7628
+ falls = [];
7629
+ sprayKey = "";
7563
7630
  courseKey = "";
7564
7631
  bedAt = null;
7565
7632
  world = [
@@ -7585,6 +7652,7 @@ var River3D = class River3D extends Node3D {
7585
7652
  "foam",
7586
7653
  "ripples",
7587
7654
  "flowForce",
7655
+ "spray",
7588
7656
  "sunIntensity"
7589
7657
  ]) {
7590
7658
  const v = r[key];
@@ -7604,6 +7672,7 @@ var River3D = class River3D extends Node3D {
7604
7672
  update(dt) {
7605
7673
  this.time += dt;
7606
7674
  this.ensureCourse();
7675
+ this.syncSpray();
7607
7676
  if (this.flowForce > 0 && this.rings.length > 1) this.sweepBodies(dt);
7608
7677
  }
7609
7678
  _syncObject3D() {
@@ -7627,6 +7696,21 @@ var River3D = class River3D extends Node3D {
7627
7696
  surfaceY: hit.surfaceY + wy
7628
7697
  } : null;
7629
7698
  }
7699
+ /**
7700
+ * The waterfalls on this course — where the water stops flowing and starts
7701
+ * falling, with the drop height and the plunge point in WORLD coordinates.
7702
+ * Hang your own effects (sound, a rainbow, a hidden cave) on these.
7703
+ */
7704
+ fallsAt() {
7705
+ this.ensureCourse();
7706
+ const [wx, wy, wz] = this.world;
7707
+ return this.falls.map((f) => ({
7708
+ ...f,
7709
+ worldX: f.x + wx,
7710
+ worldY: f.baseY + wy,
7711
+ worldZ: f.z + wz
7712
+ }));
7713
+ }
7630
7714
  /** @internal The derived centerline stations (surface, grade, current). */
7631
7715
  _rings() {
7632
7716
  return this.rings;
@@ -7677,6 +7761,7 @@ var River3D = class River3D extends Node3D {
7677
7761
  this.bedAt = null;
7678
7762
  if (this.path.length < 2) {
7679
7763
  this.rings = [];
7764
+ this.falls = [];
7680
7765
  return true;
7681
7766
  }
7682
7767
  const [wx, wy, wz] = world;
@@ -7691,6 +7776,7 @@ var River3D = class River3D extends Node3D {
7691
7776
  step: Math.min(Math.max(narrow * STEP_PER_WIDTH, STEP_MIN), STEP_MAX),
7692
7777
  bedAt: this.bedAt
7693
7778
  });
7779
+ this.falls = detectFalls(this.rings);
7694
7780
  return true;
7695
7781
  }
7696
7782
  rebuildIfNeeded(mesh) {
@@ -7790,6 +7876,48 @@ var River3D = class River3D extends Node3D {
7790
7876
  set("uSparkle", this.ripples);
7791
7877
  }
7792
7878
  /**
7879
+ * Hang a mist plume on every fall. Runtime children (`__spray*`), rebuilt
7880
+ * only when the course or the dial changes — a plume is a Particles3D, so it
7881
+ * carries its own budget, culling and editor visibility for free.
7882
+ */
7883
+ syncSpray() {
7884
+ const key = this.spray > 0 ? `${this.spray}|${JSON.stringify(this.falls)}` : "";
7885
+ if (key === this.sprayKey) return;
7886
+ this.sprayKey = key;
7887
+ for (const child of [...this.children]) if (child.name.startsWith("__spray")) child.free();
7888
+ if (this.spray <= 0) return;
7889
+ this.falls.forEach((fall, i) => {
7890
+ const plume = new Particles3D(`__spray${i}`);
7891
+ plume.position = [
7892
+ fall.x + fall.dirX * .6,
7893
+ fall.baseY + .15,
7894
+ fall.z + fall.dirZ * .6
7895
+ ];
7896
+ const power = Math.min(1.6, .4 + fall.drop * .12) * this.spray;
7897
+ plume.rate = Math.round(Math.min(46, 10 + fall.drop * 3) * this.spray);
7898
+ plume.maxParticles = 160;
7899
+ plume.lifetime = [1, 2.1];
7900
+ plume.speed = [55 * power, 170 * power];
7901
+ plume.directionDeg = -90;
7902
+ plume.spreadDeg = 58;
7903
+ plume.gravity = [
7904
+ 0,
7905
+ -240,
7906
+ 0
7907
+ ];
7908
+ plume.drag = 1.2;
7909
+ plume.sizeStart = 16 * power;
7910
+ plume.sizeEnd = 80 * power;
7911
+ plume.colorStart = "#ffffff";
7912
+ plume.colorEnd = "#dfe9ec";
7913
+ plume.alphaStart = .2;
7914
+ plume.alphaEnd = 0;
7915
+ plume.blend = "normal";
7916
+ plume.depthTest = true;
7917
+ this.addChild(plume);
7918
+ });
7919
+ }
7920
+ /**
7793
7921
  * Drag every body floating in the channel toward the water's own velocity.
7794
7922
  * Drag rather than thrust: a raft accelerates until it matches the current
7795
7923
  * and then coasts, and a swimmer can still fight across a slow ford.
@@ -11569,6 +11697,9 @@ uniform vec4 uRipples[8];
11569
11697
  uniform float uSwellAmplitude;
11570
11698
  uniform vec2 uSwellDir;
11571
11699
  uniform float uSwellWavelength;
11700
+ // incanto GERSTNER: 0 = the legacy height-only swell, 1 = maximum trochoid
11701
+ // pinch (crests narrow to a peak). Physical steepness, capped per train.
11702
+ uniform float uSwellSteepness;
11572
11703
  // incanto shore: the SWASH SURGE shares the shore-waves dial — a very long
11573
11704
  // slow bulge marching shoreward; ±9 cm of water height walks the waterline
11574
11705
  // meters up and down a gentle beach (the lapping runup).
@@ -11581,6 +11712,8 @@ uniform float uEdgeFadeOn;
11581
11712
  varying vec3 vNormal;
11582
11713
  varying vec3 vWorldPosition;
11583
11714
  varying vec4 vScreenPosition;
11715
+ // incanto GERSTNER: how far the surface has folded here (whitecap seed)
11716
+ varying float vFold;
11584
11717
 
11585
11718
  // Simplex 3D Noise
11586
11719
  // by Ian McEwan, Stefan Gustavson (https://github.com/stegu/webgl-noise)
@@ -11683,6 +11816,43 @@ float getSwell(vec2 p) {
11683
11816
  + swellWave(p, d3, uSwellWavelength * 0.31, uSwellAmplitude * 0.16, 1.8);
11684
11817
  }
11685
11818
 
11819
+ // incanto GERSTNER: the horizontal half of a swell. A height field can only
11820
+ // raise and lower water; a real wave train also DRAGS it toward the crest,
11821
+ // which is what makes crests narrow and troughs broad — the single strongest
11822
+ // tell of open ocean. The same derivatives give the JACOBIAN of the surface
11823
+ // map, and where that collapses the water is folding over itself: whitecaps
11824
+ // belong exactly there, not wherever the height happens to be high.
11825
+ //
11826
+ // Returns (pinch.x, pinch.z, fold) — fold is 1 where the surface folds.
11827
+ vec3 getSwellPinch(vec2 p) {
11828
+ if (uSwellAmplitude <= 0.0 || uSwellSteepness <= 0.0) return vec3(0.0);
11829
+ vec2 d1 = uSwellDir;
11830
+ vec2 d2 = normalize(vec2(d1.x * 0.906 - d1.y * 0.423, d1.x * 0.423 + d1.y * 0.906));
11831
+ vec2 d3 = normalize(vec2(d1.x * 0.946 + d1.y * 0.326, -d1.x * 0.326 + d1.y * 0.946));
11832
+ vec2 pinch = vec2(0.0);
11833
+ float jxx = 1.0;
11834
+ float jzz = 1.0;
11835
+ float jxz = 0.0;
11836
+ // (direction, wavelength, amplitude) — the same three trains as getSwell
11837
+ for (int i = 0; i < 3; i++) {
11838
+ vec2 d = i == 0 ? d1 : (i == 1 ? d2 : d3);
11839
+ float lambda = uSwellWavelength * (i == 0 ? 1.0 : (i == 1 ? 0.53 : 0.31));
11840
+ float amp = uSwellAmplitude * (i == 0 ? 0.60 : (i == 1 ? 0.27 : 0.16));
11841
+ float k = 6.2831853 / max(lambda, 1.0);
11842
+ float c = sqrt(9.8 / k);
11843
+ float phase = dot(p, d) * k - uTime * c * k;
11844
+ // steepness per train, capped so the trochoid never loops back on itself
11845
+ float q = min(uSwellSteepness / max(k * amp * 3.0, 1e-4), 1.0);
11846
+ pinch -= d * (q * amp * cos(phase));
11847
+ float f = q * amp * k * sin(phase);
11848
+ jxx -= d.x * d.x * f;
11849
+ jzz -= d.y * d.y * f;
11850
+ jxz -= d.x * d.y * f;
11851
+ }
11852
+ float jacobian = jxx * jzz - jxz * jxz;
11853
+ return vec3(pinch, 1.0 - smoothstep(0.15, 0.75, jacobian));
11854
+ }
11855
+
11686
11856
  // incanto: waves + swell + ripples — used for displacement AND the normal
11687
11857
  // samples so every layer catches the light like real waves.
11688
11858
  // incanto shore: two incommensurate long surges — the waterline breathes in
@@ -11704,6 +11874,13 @@ void main() {
11704
11874
 
11705
11875
  float elevation = getHeight(modelPosition.x, modelPosition.z);
11706
11876
  modelPosition.y += elevation;
11877
+ // Gerstner pinch: the height field is sampled at the UNDISPLACED position
11878
+ // (so the analytic finite-difference normals below stay consistent), then
11879
+ // the surface is dragged horizontally toward its crests. At the steepness
11880
+ // this ships the normal error is far under the detail-normal layers.
11881
+ vec3 pinch = getSwellPinch(modelPosition.xz);
11882
+ modelPosition.xz += pinch.xy;
11883
+ vFold = pinch.z;
11707
11884
 
11708
11885
  // Height adjustment to smooth water edges
11709
11886
  // Lower water height as it approaches scale boundaries
@@ -11836,6 +12013,8 @@ uniform float uEdgeWidth;
11836
12013
  varying vec3 vNormal;
11837
12014
  varying vec3 vWorldPosition;
11838
12015
  varying vec4 vScreenPosition;
12016
+ // incanto GERSTNER: how far the surface has folded here (whitecap seed)
12017
+ varying float vFold;
11839
12018
 
11840
12019
  uniform samplerCube uEnvironmentMap;
11841
12020
  // incanto MIRROR: a planar reflection rendered from the camera mirrored across
@@ -12493,6 +12672,10 @@ void main() {
12493
12672
  float capNoise = noise(capUV * 2.2 + uTime * vec2(0.10, 0.06)) * 0.65
12494
12673
  + noise(capUV * 6.5 - uTime * vec2(0.05, 0.14)) * 0.35;
12495
12674
  float capFade = 1.0 - smoothstep(220.0, 640.0, camDist);
12675
+ // height says WHERE the crest is; the Jacobian says where it is actually
12676
+ // breaking. Taking the max keeps the tuned height-based caps and adds the
12677
+ // ones physics insists on — the pinched crest faces about to spill.
12678
+ cap = max(cap, vFold);
12496
12679
  float whitecap = uWhitecaps * cap * smoothstep(0.42, 0.78, capNoise) * capFade;
12497
12680
  foamAmount = max(foamAmount, whitecap);
12498
12681
  }
@@ -13009,6 +13192,7 @@ var Water3D = class Water3D extends Node3D {
13009
13192
  reflection: { default: true },
13010
13193
  reflectionInterval: { default: 1e3 },
13011
13194
  mirror: { default: false },
13195
+ swellSteepness: { default: .35 },
13012
13196
  foam: { default: true },
13013
13197
  interaction: { default: true },
13014
13198
  splash: { default: true },
@@ -13107,6 +13291,13 @@ var Water3D = class Water3D extends Node3D {
13107
13291
  swellDirectionDeg = 0;
13108
13292
  /** Primary swell wavelength in meters (the two crossing trains derive). */
13109
13293
  swellWavelength = 30;
13294
+ /**
13295
+ * GERSTNER pinch on the swell: 0 keeps the legacy height-only trains, 1 is
13296
+ * maximum trochoid (crests narrow to a peak, troughs go broad). It also
13297
+ * decides where whitecaps break — the surface's Jacobian collapses on a
13298
+ * folding crest, and that is where foam belongs.
13299
+ */
13300
+ swellSteepness = .35;
13110
13301
  /** Open-water whitecap foam on the tallest crests, 0–1 (fancy only). Works
13111
13302
  * best with `swell` — the caps ride the swell's crest lines. */
13112
13303
  whitecaps = 0;
@@ -13509,6 +13700,7 @@ var Water3D = class Water3D extends Node3D {
13509
13700
  uSwellAmplitude: { value: this.rp("swell") },
13510
13701
  uSwellDir: { value: new Vector2(0, 1) },
13511
13702
  uSwellWavelength: { value: this.rp("swellWavelength") },
13703
+ uSwellSteepness: { value: this.rp("swellSteepness") },
13512
13704
  uWhitecaps: { value: this.rp("whitecaps") },
13513
13705
  uWaveEnvelope: { value: 0 },
13514
13706
  uCausticsAbove: { value: this.rp("causticsAbove") },
@@ -13578,6 +13770,7 @@ var Water3D = class Water3D extends Node3D {
13578
13770
  if (u.uAbsorption) u.uAbsorption.value = this.rp("absorption");
13579
13771
  if (u.uSwellAmplitude) u.uSwellAmplitude.value = this.rp("swell");
13580
13772
  if (u.uSwellWavelength) u.uSwellWavelength.value = this.rp("swellWavelength");
13773
+ if (u.uSwellSteepness) u.uSwellSteepness.value = this.rp("swellSteepness");
13581
13774
  if (u.uWhitecaps) u.uWhitecaps.value = this.rp("whitecaps");
13582
13775
  if (u.uCausticsAbove) u.uCausticsAbove.value = this.rp("causticsAbove");
13583
13776
  if (u.uShoreWaves) u.uShoreWaves.value = this.rp("shoreWaves");
package/dist/test.js CHANGED
@@ -6,7 +6,7 @@ import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
6
6
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
7
7
  import { n as registerGameplayBehaviors } from "./gameplay-sSqrUcnz.js";
8
8
  import { t as registerNodes2D } from "./register-Djwckzgx.js";
9
- import { t as registerNodes3D } from "./register-BWFgsGra.js";
9
+ import { t as registerNodes3D } from "./register-BC55Ez6I.js";
10
10
  import { t as registerNodesNet } from "./register-BFFE1Mh1.js";
11
11
  //#region src/test/index.ts
12
12
  /**
@@ -127,7 +127,7 @@ async function runScript(json, opts) {
127
127
  const { enablePhysics2D } = await import("./physics-2d-BiIdl51r.js").then((n) => n.r);
128
128
  await enablePhysics2D(engine);
129
129
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
130
- const { enablePhysics3D } = await import("./physics-3d-DxFDfUey.js").then((n) => n.r);
130
+ const { enablePhysics3D } = await import("./physics-3d-DHTZUrr0.js").then((n) => n.r);
131
131
  await enablePhysics3D(engine);
132
132
  }
133
133
  const failures = [];
@@ -232,7 +232,7 @@ async function createPlaySession(json, opts = {}) {
232
232
  const { enablePhysics2D } = await import("./physics-2d-BiIdl51r.js").then((n) => n.r);
233
233
  await enablePhysics2D(engine);
234
234
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
235
- const { enablePhysics3D } = await import("./physics-3d-DxFDfUey.js").then((n) => n.r);
235
+ const { enablePhysics3D } = await import("./physics-3d-DHTZUrr0.js").then((n) => n.r);
236
236
  await enablePhysics3D(engine);
237
237
  }
238
238
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -1 +1 @@
1
- import{t as e}from"./index-CXdpxO_B.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-Dt44Z9ka.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};