incanto 0.3.3 → 0.4.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
@@ -543,6 +543,51 @@ declare class Environment3D {
543
543
  dispose(): void;
544
544
  }
545
545
  //#endregion
546
+ //#region src/3d/nodes/billboard-3d.d.ts
547
+ /** Valid `Billboard3D.mode` values — drives the prop options AND validateJson. */
548
+ declare const BILLBOARD_GROUP_MODES: readonly ["screen", "y", "none"];
549
+ type BillboardGroupMode = (typeof BILLBOARD_GROUP_MODES)[number];
550
+ /**
551
+ * Billboard3D — a transform GROUP that orients itself toward the camera every
552
+ * rendered frame; children inherit the rotation through the scene graph. Hang
553
+ * world-space UI off a character — HP bars, markers, icons — without
554
+ * hand-rolling camera math in game code.
555
+ *
556
+ * - `mode: "screen"` (default) copies the camera's orientation wholesale, so a
557
+ * child rectangle projects as an upright rectangle ANYWHERE on screen. This is
558
+ * what flat UI wants — a yaw-only look-at visibly tilts off-center shapes when
559
+ * the camera pitches. (Deliberately NOT the same as Sprite3D's `"full"`, which
560
+ * faces the camera POSITION and keeps that perspective tilt.)
561
+ * - `mode: "y"` yaws the group's +Z upright toward the camera (like Sprite3D's
562
+ * `"y"`) — for standing content that must not pitch back as the camera rises.
563
+ * - `mode: "none"` billboarding off — the authored transform applies again on
564
+ * the next frame (`_syncObject3D` re-stamps it), so toggling is lossless.
565
+ *
566
+ * The node's own `rotation` prop is overwritten while billboarding ("none"
567
+ * restores it — also handy in the editor to pose children with the gizmo).
568
+ * The group keeps orienting while `visible: false` ON PURPOSE: a hidden HP bar
569
+ * revealed later is already correct — no one-frame orientation pop. Headless
570
+ * runs never invoke the render hook — the group keeps its authored transform.
571
+ */
572
+ declare class Billboard3D extends Node3D {
573
+ static override readonly typeName: string;
574
+ static override readonly props: PropSchema;
575
+ /** `"screen"` copy the camera orientation (screen-aligned) · `"y"` upright
576
+ * yaw-to-camera · `"none"` billboarding off (authored rotation applies). */
577
+ mode: BillboardGroupMode;
578
+ /** Last successfully billboarded WORLD orientation — reapplied on the frames
579
+ * where `"y"` has no answer (camera dead overhead), because `_syncObject3D`
580
+ * re-stamps the authored rotation every frame and a bare early-return would
581
+ * visibly snap the group to it. Lazily created; `null` until first success. */
582
+ private _lastDesired;
583
+ /** Scene-load validation: a typo'd mode must fail HARD, not silently behave
584
+ * like one of the real modes (agents self-correct on load errors). */
585
+ static validateJson(node: Node): void;
586
+ /** @internal Orient the BACKING object each rendered frame — unlike Sprite3D
587
+ * (which spins only its inner quad) the whole group turns, so children follow. */
588
+ _onRender3D(ctx: RenderContext3D): void;
589
+ }
590
+ //#endregion
546
591
  //#region src/3d/nodes/camera-3d.d.ts
547
592
  /**
548
593
  * A perspective camera. Mark exactly one camera `current: true`; with none
@@ -740,7 +785,11 @@ type FoliageStyle = "mesh" | "tufts" | "blades" | "simple";
740
785
  * (`colorA`→`colorB`, clump-coherent, dry patches) and the ported sin·cos
741
786
  * tuft sway. Dozens of overlapping textured fans read as a DENSE meadow
742
787
  * (잔디밭) at a fraction of the instances — the look that per-blade
743
- * geometry never reaches at sane budgets.
788
+ * geometry never reaches at sane budgets. NOTE: tuft cards are big — drawn
789
+ * ~1.5× the `height` prop and wider than tall — and opaque (depth-writing),
790
+ * so over a field of SMALL gameplay sprites (mobs, items) they legitimately
791
+ * occlude actors shorter than the grass. For such fields prefer `'blades'`
792
+ * (thin) or keep `height` low so actors read above the grass line.
744
793
  *
745
794
  * - `'mesh'` (default, kind 'grass' only): the per-blade path — ONE
746
795
  * InstancedMesh where every instance is a REAL tapered blade (7-vertex
@@ -786,7 +835,9 @@ declare class Foliage3D extends Node3D implements SunConsumer3D {
786
835
  /** Instance cap (hard ceiling 200_000). */
787
836
  maxInstances: number;
788
837
  /** Base blade height in meters — mesh style additionally scales it 0.55–1.7×
789
- * regionally (low-frequency noise: tall patches and short worn patches). */
838
+ * regionally (low-frequency noise: tall patches and short worn patches), and
839
+ * `tufts` cards render ~1.5× this value (so 0.4 ≈ 0.6 m tall). Keep it low under
840
+ * small gameplay sprites or they hide behind the grass — see the `style` doc. */
790
841
  height: number;
791
842
  /** colorA = bottom/lerp-start, colorB = top/lerp-end. Refinement pass 3:
792
843
  * lifted toward the reference's warm sunlit green (the old olives read
@@ -962,6 +1013,14 @@ interface MeshMaterialProps {
962
1013
  flatShading?: boolean;
963
1014
  /** 0..1 surface opacity; below 1 turns on transparency (glassy gems, etc.). */
964
1015
  opacity?: number;
1016
+ /** Depth-test against the scene (default true). Set false so the mesh ALWAYS
1017
+ * draws on top of geometry behind it — the standard trick for flat ground
1018
+ * decals (AoE telegraphs, selection rings) that must never z-fight with or be
1019
+ * occluded by bumpy terrain. Pair with a high `renderOrder`. */
1020
+ depthTest?: boolean;
1021
+ /** Write to the depth buffer (default true). Set false for additive/overlay
1022
+ * decals so they don't occlude effects drawn after them. */
1023
+ depthWrite?: boolean;
965
1024
  /** Color texture URL (sampled sRGB, tinted by `color`). */
966
1025
  map?: string;
967
1026
  /** Tangent-space normal map URL (sampled linear). */
@@ -978,10 +1037,12 @@ interface MeshMaterialProps {
978
1037
  * - `size [x,y,z]`: box extents; sphere/gem radius = x; capsule radius = x,
979
1038
  * height = y; plane = x·z ground plane (laid flat); cylinder radius = x, height = y
980
1039
  * - `material`: `{color, metalness, roughness, wireframe, flatShading, opacity,
981
- * emissive, emissiveIntensity, map, normalMap, repeat}` — `map`/`normalMap` are
982
- * texture URLs loaded lazily (headless-safe: geometry and physics never
983
- * wait on them), tiled by `repeat: [u, v]`. `gem` + `flatShading: true` + a low
984
- * `roughness` reads as a sparkling faceted jewel (spin it for the sparkle).
1040
+ * depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat}` —
1041
+ * `map`/`normalMap` are texture URLs loaded lazily (headless-safe: geometry and
1042
+ * physics never wait on them), tiled by `repeat: [u, v]`. `gem` + `flatShading:
1043
+ * true` + a low `roughness` reads as a sparkling faceted jewel (spin it for the
1044
+ * sparkle). `depthTest: false` makes a flat `plane`/`cylinder` an always-on-top
1045
+ * ground decal (AoE telegraph, selection ring) — no z-fighting with terrain.
985
1046
  */
986
1047
  declare class MeshInstance3D extends Node3D {
987
1048
  static override readonly typeName: string;
@@ -1860,4 +1921,4 @@ interface WalkState {
1860
1921
  alpha: number;
1861
1922
  }
1862
1923
  //#endregion
1863
- export { Area3D, AssetStore3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type Game3D, type Heightmap, type HeightmapOptions, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type RigView, RigidBody3D, type Ripple, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainTheme, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_MAX_RIPPLES, Water3D, buildHeightmap, cameraRelative, createGame3D, enablePhysics3D, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, registerNodes3D, resolveFlowerDensity, rigPose, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers };
1924
+ export { Area3D, AssetStore3D, Billboard3D, type BillboardGroupMode, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type Game3D, type Heightmap, type HeightmapOptions, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type RigView, RigidBody3D, type Ripple, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainTheme, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_MAX_RIPPLES, Water3D, buildHeightmap, cameraRelative, createGame3D, enablePhysics3D, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, registerNodes3D, resolveFlowerDensity, rigPose, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers };
package/dist/3d.js CHANGED
@@ -1,5 +1,5 @@
1
- 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-DKdR28SI.js";
2
- import { A as QUARTER_PITCH, C as Area3D, D as StaticBody3D, E as RigidBody3D, M as keyboardIntensity, N as movementState, O as Node3D, P as rigPose, S as terrainThemeLayers, T as PhysicsBody3D, _ as CharacterController3D, a as VoxelGrid3D, b as DEFAULT_TERRAIN_TEXTURE_BASE, c as ModelInstance3D, d as OmniLight3D, f as Foliage3D, g as FLOWER_VARIETIES, h as resolveFlowerDensity, i as VOXEL_PALETTE, j as cameraRelative, l as MeshInstance3D, m as Flowers3D, n as Water3D, o as Tree3D, p as DENSITY_PRESETS, r as WATER_MAX_RIPPLES, s as Particles3D, t as registerNodes3D, u as DirectionalLight3D, v as Camera3D, w as CharacterBody3D, x as TERRAIN_THEMES, y as Terrain3D } from "./register-CljkZG08.js";
1
+ 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-DlTZUCSj.js";
2
+ import { C as terrainThemeLayers, D as RigidBody3D, E as PhysicsBody3D, F as rigPose, M as cameraRelative, N as keyboardIntensity, O as StaticBody3D, P as movementState, S as TERRAIN_THEMES, T as CharacterBody3D, _ as CharacterController3D, a as VoxelGrid3D, b as Terrain3D, c as ModelInstance3D, d as OmniLight3D, f as Foliage3D, g as FLOWER_VARIETIES, h as resolveFlowerDensity, i as VOXEL_PALETTE, j as QUARTER_PITCH, k as Node3D, l as MeshInstance3D, m as Flowers3D, n as Water3D, o as Tree3D, p as DENSITY_PRESETS, r as WATER_MAX_RIPPLES, s as Particles3D, t as registerNodes3D, u as DirectionalLight3D, v as Camera3D, w as Area3D, x as DEFAULT_TERRAIN_TEXTURE_BASE, y as Billboard3D } from "./register-9mSsSUS1.js";
3
3
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CroQPEER.js";
4
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DXODbpY1.js";
5
- export { Area3D, AssetStore3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, StaticBody3D, TERRAIN_THEMES, Terrain3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_MAX_RIPPLES, Water3D, buildHeightmap, cameraRelative, createGame3D, enablePhysics3D, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, registerNodes3D, resolveFlowerDensity, rigPose, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers };
4
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-Yvle9Clx.js";
5
+ export { Area3D, AssetStore3D, Billboard3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, StaticBody3D, TERRAIN_THEMES, Terrain3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_MAX_RIPPLES, Water3D, buildHeightmap, cameraRelative, createGame3D, enablePhysics3D, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, registerNodes3D, resolveFlowerDensity, rigPose, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers };
@@ -6,8 +6,8 @@ import { r as AudioPlayer } from "./register-BuUV1_KB.js";
6
6
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
7
7
  import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
8
8
  import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
9
- import { O as Node3D, T as PhysicsBody3D, c as ModelInstance3D, t as registerNodes3D, u as DirectionalLight3D, v as Camera3D } from "./register-CljkZG08.js";
10
- import { n as enablePhysics3D } from "./physics-3d-DXODbpY1.js";
9
+ import { E as PhysicsBody3D, c as ModelInstance3D, k as Node3D, t as registerNodes3D, u as DirectionalLight3D, v as Camera3D } from "./register-9mSsSUS1.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-Yvle9Clx.js";
11
11
  import { ACESFilmicToneMapping, AmbientLight, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
12
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
package/dist/index.js CHANGED
@@ -57,6 +57,6 @@ function newUid() {
57
57
  //#endregion
58
58
  //#region src/index.ts
59
59
  /** Engine version. Kept in sync with package.json by the release pipeline. */
60
- const VERSION = "0.3.3";
60
+ const VERSION = "0.4.0";
61
61
  //#endregion
62
62
  export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, IncantoError, InputMap, LogManager, MusicManager, Node, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, crossfadeGains, duplicateNode, fadeGain, getBehavior, getNodeSchema, getNodeSignals, getNodeType, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, 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 { C as Area3D, E as RigidBody3D, O as Node3D, T as PhysicsBody3D, k as validateCollider3D, w as CharacterBody3D, y as Terrain3D } from "./register-CljkZG08.js";
4
+ import { A as validateCollider3D, D as RigidBody3D, E as PhysicsBody3D, T as CharacterBody3D, b as Terrain3D, k as Node3D, w as Area3D } from "./register-9mSsSUS1.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-DKdR28SI.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CZHROKcT.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DlTZUCSj.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CZHROKcT.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1339,9 +1339,9 @@ function blobTexture() {
1339
1339
  _blobTex = new CanvasTexture(c);
1340
1340
  return _blobTex;
1341
1341
  }
1342
- const Z_AXIS = new Vector3(0, 0, 1);
1342
+ const Z_AXIS$1 = new Vector3(0, 0, 1);
1343
1343
  const _spritePos = new Vector3();
1344
- const _dir = new Vector3();
1344
+ const _dir$1 = new Vector3();
1345
1345
  const _target = new Quaternion();
1346
1346
  const _parent = new Quaternion();
1347
1347
  const _sunLocal = new Vector3();
@@ -1550,22 +1550,22 @@ var Sprite3D = class extends Node3D {
1550
1550
  /** @internal Aim the quad's +Z at a world point, honoring billboard mode. */
1551
1551
  _faceToward(quad, targetWorld) {
1552
1552
  quad.getWorldPosition(_spritePos);
1553
- _dir.copy(targetWorld).sub(_spritePos);
1554
- if (this.billboard === "y") _dir.y = 0;
1555
- if (_dir.lengthSq() < 1e-12) return;
1556
- _dir.normalize();
1557
- _target.setFromUnitVectors(Z_AXIS, _dir);
1553
+ _dir$1.copy(targetWorld).sub(_spritePos);
1554
+ if (this.billboard === "y") _dir$1.y = 0;
1555
+ if (_dir$1.lengthSq() < 1e-12) return;
1556
+ _dir$1.normalize();
1557
+ _target.setFromUnitVectors(Z_AXIS$1, _dir$1);
1558
1558
  quad.parent?.getWorldQuaternion(_parent);
1559
1559
  quad.quaternion.copy(_parent.invert()).multiply(_target);
1560
1560
  }
1561
1561
  /** @internal Aim a mesh's +Z along a world DIRECTION (vs a point), honoring
1562
1562
  * billboard mode — keeps the silhouette shadow caster broad to the light. */
1563
1563
  _faceDir(mesh, dir) {
1564
- _dir.copy(dir);
1565
- if (this.billboard === "y") _dir.y = 0;
1566
- if (_dir.lengthSq() < 1e-12) return;
1567
- _dir.normalize();
1568
- _target.setFromUnitVectors(Z_AXIS, _dir);
1564
+ _dir$1.copy(dir);
1565
+ if (this.billboard === "y") _dir$1.y = 0;
1566
+ if (_dir$1.lengthSq() < 1e-12) return;
1567
+ _dir$1.normalize();
1568
+ _target.setFromUnitVectors(Z_AXIS$1, _dir$1);
1569
1569
  mesh.parent?.getWorldQuaternion(_parent);
1570
1570
  mesh.quaternion.copy(_parent.invert()).multiply(_target);
1571
1571
  }
@@ -1694,6 +1694,87 @@ function resolveFrames(frames, name) {
1694
1694
  return [...frames];
1695
1695
  }
1696
1696
  //#endregion
1697
+ //#region src/3d/nodes/billboard-3d.ts
1698
+ const _parentQ = new Quaternion();
1699
+ const _rotQ = new Quaternion();
1700
+ const _pos = new Vector3();
1701
+ const _camPos = new Vector3();
1702
+ const _dir = new Vector3();
1703
+ const Z_AXIS = new Vector3(0, 0, 1);
1704
+ /** Valid `Billboard3D.mode` values — drives the prop options AND validateJson. */
1705
+ const BILLBOARD_GROUP_MODES = [
1706
+ "screen",
1707
+ "y",
1708
+ "none"
1709
+ ];
1710
+ /**
1711
+ * Billboard3D — a transform GROUP that orients itself toward the camera every
1712
+ * rendered frame; children inherit the rotation through the scene graph. Hang
1713
+ * world-space UI off a character — HP bars, markers, icons — without
1714
+ * hand-rolling camera math in game code.
1715
+ *
1716
+ * - `mode: "screen"` (default) copies the camera's orientation wholesale, so a
1717
+ * child rectangle projects as an upright rectangle ANYWHERE on screen. This is
1718
+ * what flat UI wants — a yaw-only look-at visibly tilts off-center shapes when
1719
+ * the camera pitches. (Deliberately NOT the same as Sprite3D's `"full"`, which
1720
+ * faces the camera POSITION and keeps that perspective tilt.)
1721
+ * - `mode: "y"` yaws the group's +Z upright toward the camera (like Sprite3D's
1722
+ * `"y"`) — for standing content that must not pitch back as the camera rises.
1723
+ * - `mode: "none"` billboarding off — the authored transform applies again on
1724
+ * the next frame (`_syncObject3D` re-stamps it), so toggling is lossless.
1725
+ *
1726
+ * The node's own `rotation` prop is overwritten while billboarding ("none"
1727
+ * restores it — also handy in the editor to pose children with the gizmo).
1728
+ * The group keeps orienting while `visible: false` ON PURPOSE: a hidden HP bar
1729
+ * revealed later is already correct — no one-frame orientation pop. Headless
1730
+ * runs never invoke the render hook — the group keeps its authored transform.
1731
+ */
1732
+ var Billboard3D = class extends Node3D {
1733
+ static typeName = "Billboard3D";
1734
+ static props = { mode: {
1735
+ default: "screen",
1736
+ options: BILLBOARD_GROUP_MODES
1737
+ } };
1738
+ /** `"screen"` copy the camera orientation (screen-aligned) · `"y"` upright
1739
+ * yaw-to-camera · `"none"` billboarding off (authored rotation applies). */
1740
+ mode = "screen";
1741
+ /** Last successfully billboarded WORLD orientation — reapplied on the frames
1742
+ * where `"y"` has no answer (camera dead overhead), because `_syncObject3D`
1743
+ * re-stamps the authored rotation every frame and a bare early-return would
1744
+ * visibly snap the group to it. Lazily created; `null` until first success. */
1745
+ _lastDesired = null;
1746
+ /** Scene-load validation: a typo'd mode must fail HARD, not silently behave
1747
+ * like one of the real modes (agents self-correct on load errors). */
1748
+ static validateJson(node) {
1749
+ const b = node;
1750
+ if (!BILLBOARD_GROUP_MODES.includes(b.mode)) throw new IncantoError("BAD_FORMAT", `Billboard3D '${node.name}' mode must be one of [${BILLBOARD_GROUP_MODES.join(", ")}], got '${b.mode}'.`, {
1751
+ prop: "mode",
1752
+ validOptions: BILLBOARD_GROUP_MODES
1753
+ });
1754
+ }
1755
+ /** @internal Orient the BACKING object each rendered frame — unlike Sprite3D
1756
+ * (which spins only its inner quad) the whole group turns, so children follow. */
1757
+ _onRender3D(ctx) {
1758
+ if (this.mode === "none") return;
1759
+ const o = this._ensureObject3D();
1760
+ if (this.mode === "screen") ctx.camera.getWorldQuaternion(_rotQ);
1761
+ else {
1762
+ o.getWorldPosition(_pos);
1763
+ ctx.camera.getWorldPosition(_camPos);
1764
+ _dir.copy(_camPos).sub(_pos);
1765
+ _dir.y = 0;
1766
+ if (_dir.lengthSq() >= 1e-12) _rotQ.setFromUnitVectors(Z_AXIS, _dir.normalize());
1767
+ else if (this._lastDesired) _rotQ.copy(this._lastDesired);
1768
+ else return;
1769
+ }
1770
+ if (!this._lastDesired) this._lastDesired = new Quaternion();
1771
+ this._lastDesired.copy(_rotQ);
1772
+ o.parent?.getWorldQuaternion(_parentQ);
1773
+ if (o.parent) o.quaternion.copy(_parentQ.invert()).multiply(_rotQ);
1774
+ else o.quaternion.copy(_rotQ);
1775
+ }
1776
+ };
1777
+ //#endregion
1697
1778
  //#region src/3d/nodes/camera-3d.ts
1698
1779
  /**
1699
1780
  * A perspective camera. Mark exactly one camera `current: true`; with none
@@ -3981,7 +4062,11 @@ const BEND_AIR_GAP = .5;
3981
4062
  * (`colorA`→`colorB`, clump-coherent, dry patches) and the ported sin·cos
3982
4063
  * tuft sway. Dozens of overlapping textured fans read as a DENSE meadow
3983
4064
  * (잔디밭) at a fraction of the instances — the look that per-blade
3984
- * geometry never reaches at sane budgets.
4065
+ * geometry never reaches at sane budgets. NOTE: tuft cards are big — drawn
4066
+ * ~1.5× the `height` prop and wider than tall — and opaque (depth-writing),
4067
+ * so over a field of SMALL gameplay sprites (mobs, items) they legitimately
4068
+ * occlude actors shorter than the grass. For such fields prefer `'blades'`
4069
+ * (thin) or keep `height` low so actors read above the grass line.
3985
4070
  *
3986
4071
  * - `'mesh'` (default, kind 'grass' only): the per-blade path — ONE
3987
4072
  * InstancedMesh where every instance is a REAL tapered blade (7-vertex
@@ -4062,7 +4147,9 @@ var Foliage3D = class Foliage3D extends Node3D {
4062
4147
  /** Instance cap (hard ceiling 200_000). */
4063
4148
  maxInstances = 5e4;
4064
4149
  /** Base blade height in meters — mesh style additionally scales it 0.55–1.7×
4065
- * regionally (low-frequency noise: tall patches and short worn patches). */
4150
+ * regionally (low-frequency noise: tall patches and short worn patches), and
4151
+ * `tufts` cards render ~1.5× this value (so 0.4 ≈ 0.6 m tall). Keep it low under
4152
+ * small gameplay sprites or they hide behind the grass — see the `style` doc. */
4066
4153
  height = .25;
4067
4154
  /** colorA = bottom/lerp-start, colorB = top/lerp-end. Refinement pass 3:
4068
4155
  * lifted toward the reference's warm sunlit green (the old olives read
@@ -4797,6 +4884,8 @@ const MATERIAL_KEYS = [
4797
4884
  "wireframe",
4798
4885
  "flatShading",
4799
4886
  "opacity",
4887
+ "depthTest",
4888
+ "depthWrite",
4800
4889
  "map",
4801
4890
  "normalMap",
4802
4891
  "repeat"
@@ -4807,10 +4896,12 @@ const MATERIAL_KEYS = [
4807
4896
  * - `size [x,y,z]`: box extents; sphere/gem radius = x; capsule radius = x,
4808
4897
  * height = y; plane = x·z ground plane (laid flat); cylinder radius = x, height = y
4809
4898
  * - `material`: `{color, metalness, roughness, wireframe, flatShading, opacity,
4810
- * emissive, emissiveIntensity, map, normalMap, repeat}` — `map`/`normalMap` are
4811
- * texture URLs loaded lazily (headless-safe: geometry and physics never
4812
- * wait on them), tiled by `repeat: [u, v]`. `gem` + `flatShading: true` + a low
4813
- * `roughness` reads as a sparkling faceted jewel (spin it for the sparkle).
4899
+ * depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat}` —
4900
+ * `map`/`normalMap` are texture URLs loaded lazily (headless-safe: geometry and
4901
+ * physics never wait on them), tiled by `repeat: [u, v]`. `gem` + `flatShading:
4902
+ * true` + a low `roughness` reads as a sparkling faceted jewel (spin it for the
4903
+ * sparkle). `depthTest: false` makes a flat `plane`/`cylinder` an always-on-top
4904
+ * ground decal (AoE telegraph, selection ring) — no z-fighting with terrain.
4814
4905
  */
4815
4906
  var MeshInstance3D = class extends Node3D {
4816
4907
  static typeName = "MeshInstance3D";
@@ -4873,6 +4964,8 @@ var MeshInstance3D = class extends Node3D {
4873
4964
  const opacity = this.material.opacity ?? 1;
4874
4965
  mat.opacity = opacity;
4875
4966
  mat.transparent = opacity < 1;
4967
+ mat.depthTest = this.material.depthTest ?? true;
4968
+ mat.depthWrite = this.material.depthWrite ?? true;
4876
4969
  const flat = this.material.flatShading ?? false;
4877
4970
  if (mat.flatShading !== flat) {
4878
4971
  mat.flatShading = flat;
@@ -4948,7 +5041,12 @@ function validateMaterial(material, name) {
4948
5041
  const value = material[key];
4949
5042
  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)}.`);
4950
5043
  }
4951
- for (const key of ["wireframe", "flatShading"]) {
5044
+ for (const key of [
5045
+ "wireframe",
5046
+ "flatShading",
5047
+ "depthTest",
5048
+ "depthWrite"
5049
+ ]) {
4952
5050
  const value = material[key];
4953
5051
  if (value !== void 0 && typeof value !== "boolean") fail(`material.${key} must be a boolean, got ${JSON.stringify(value)}.`);
4954
5052
  }
@@ -10993,6 +11091,7 @@ function registerNodes3D() {
10993
11091
  registerNode(MeshInstance3D);
10994
11092
  registerNode(Sprite3D);
10995
11093
  registerNode(AnimatedSprite3D);
11094
+ registerNode(Billboard3D);
10996
11095
  registerNode(CharacterController3D);
10997
11096
  registerNode(VoxelGrid3D);
10998
11097
  registerNode(Terrain3D);
@@ -11010,4 +11109,4 @@ function registerNodes3D() {
11010
11109
  registerNode(CharacterBody3D);
11011
11110
  }
11012
11111
  //#endregion
11013
- export { QUARTER_PITCH as A, Area3D as C, StaticBody3D as D, RigidBody3D as E, keyboardIntensity as M, movementState as N, Node3D as O, rigPose as P, terrainThemeLayers as S, PhysicsBody3D as T, CharacterController3D as _, VoxelGrid3D as a, DEFAULT_TERRAIN_TEXTURE_BASE as b, ModelInstance3D as c, OmniLight3D as d, Foliage3D as f, FLOWER_VARIETIES as g, resolveFlowerDensity as h, VOXEL_PALETTE as i, cameraRelative as j, validateCollider3D 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, CharacterBody3D as w, TERRAIN_THEMES as x, Terrain3D as y };
11112
+ 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 };
package/dist/test.js CHANGED
@@ -5,7 +5,7 @@ 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
6
  import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
7
7
  import { t as registerNodes2D } from "./register-DPEV9_9t.js";
8
- import { t as registerNodes3D } from "./register-CljkZG08.js";
8
+ import { t as registerNodes3D } from "./register-9mSsSUS1.js";
9
9
  import { t as registerNodesNet } from "./register-Dasmnurl.js";
10
10
  //#region src/test/index.ts
11
11
  /**
@@ -132,7 +132,7 @@ async function runScript(json, opts) {
132
132
  const { enablePhysics2D } = await import("./physics-2d-KuMWPTf6.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-DXODbpY1.js").then((n) => n.r);
135
+ const { enablePhysics3D } = await import("./physics-3d-Yvle9Clx.js").then((n) => n.r);
136
136
  await enablePhysics3D(engine);
137
137
  }
138
138
  const failures = [];
@@ -237,7 +237,7 @@ async function createPlaySession(json, opts = {}) {
237
237
  const { enablePhysics2D } = await import("./physics-2d-KuMWPTf6.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-DXODbpY1.js").then((n) => n.r);
240
+ const { enablePhysics3D } = await import("./physics-3d-Yvle9Clx.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-DMghTAy4.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;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.rawConnect()}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){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(){await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}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-BN0hHg3U.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;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.rawConnect()}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){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(){await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}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};