@jgengine/shell 0.6.0 → 0.8.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 (118) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +26 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +33 -1
  8. package/dist/GamePlayerShell.js +641 -59
  9. package/dist/audio/AudioComponents.d.ts +12 -0
  10. package/dist/audio/AudioComponents.js +53 -0
  11. package/dist/audio/audioEngine.d.ts +23 -0
  12. package/dist/audio/audioEngine.js +110 -0
  13. package/dist/behaviour.d.ts +31 -0
  14. package/dist/behaviour.js +63 -0
  15. package/dist/camera/GameCameraRig.d.ts +16 -0
  16. package/dist/camera/GameCameraRig.js +56 -0
  17. package/dist/camera/GameFirstPersonCamera.js +2 -0
  18. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  19. package/dist/camera/GameInspectionCamera.js +24 -0
  20. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  21. package/dist/camera/GameOrbitCamera.js +4 -2
  22. package/dist/camera/cameraRigs.d.ts +26 -0
  23. package/dist/camera/cameraRigs.js +522 -0
  24. package/dist/camera/index.d.ts +7 -0
  25. package/dist/camera/index.js +7 -0
  26. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  27. package/dist/camera/inspectionCameraMath.js +44 -0
  28. package/dist/camera/rigMath.d.ts +169 -0
  29. package/dist/camera/rigMath.js +393 -0
  30. package/dist/camera/rigResolve.d.ts +15 -0
  31. package/dist/camera/rigResolve.js +52 -0
  32. package/dist/camera/shakeChannel.d.ts +29 -0
  33. package/dist/camera/shakeChannel.js +38 -0
  34. package/dist/defineGame.d.ts +16 -0
  35. package/dist/defineGame.js +51 -0
  36. package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
  37. package/dist/devtools/DevtoolsOverlay.js +334 -0
  38. package/dist/environment/Daylight.d.ts +50 -0
  39. package/dist/environment/Daylight.js +100 -0
  40. package/dist/environment/EnvironmentScene.js +18 -26
  41. package/dist/environment/GroundPad.d.ts +7 -0
  42. package/dist/environment/GroundPad.js +12 -0
  43. package/dist/environment/daylightCycle.d.ts +26 -0
  44. package/dist/environment/daylightCycle.js +116 -0
  45. package/dist/environment/groundPadMath.d.ts +13 -0
  46. package/dist/environment/groundPadMath.js +10 -0
  47. package/dist/environment/index.d.ts +2 -0
  48. package/dist/environment/index.js +2 -0
  49. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  50. package/dist/map/MapMarkerBeacons.js +16 -0
  51. package/dist/map/index.d.ts +2 -0
  52. package/dist/map/index.js +2 -0
  53. package/dist/map/terrainMap.d.ts +23 -0
  54. package/dist/map/terrainMap.js +79 -0
  55. package/dist/materialOverride.d.ts +8 -0
  56. package/dist/materialOverride.js +32 -0
  57. package/dist/multiplayer.d.ts +16 -9
  58. package/dist/multiplayer.js +62 -12
  59. package/dist/pointer/PointerOverlays.d.ts +12 -0
  60. package/dist/pointer/PointerOverlays.js +17 -0
  61. package/dist/pointer/PointerProbe.d.ts +4 -0
  62. package/dist/pointer/PointerProbe.js +40 -0
  63. package/dist/pointer/pointerService.d.ts +26 -0
  64. package/dist/pointer/pointerService.js +89 -0
  65. package/dist/registry.d.ts +4 -1
  66. package/dist/registry.js +3 -1
  67. package/dist/render/modelRender.d.ts +14 -0
  68. package/dist/render/modelRender.js +79 -0
  69. package/dist/replay/useSessionRecorder.d.ts +14 -0
  70. package/dist/replay/useSessionRecorder.js +22 -0
  71. package/dist/structures/PlacementGhost.d.ts +8 -0
  72. package/dist/structures/PlacementGhost.js +11 -0
  73. package/dist/structures/index.d.ts +2 -0
  74. package/dist/structures/index.js +2 -0
  75. package/dist/structures.d.ts +1 -0
  76. package/dist/structures.js +1 -0
  77. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  78. package/dist/terrain/CarvedTerrain.js +19 -0
  79. package/dist/terrain/EditableGround.d.ts +11 -0
  80. package/dist/terrain/EditableGround.js +38 -0
  81. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  82. package/dist/terrain/TerraformBrushCursor.js +13 -0
  83. package/dist/terrain/index.d.ts +4 -1
  84. package/dist/terrain/index.js +4 -1
  85. package/dist/terrain/terrainMath.d.ts +15 -1
  86. package/dist/terrain/terrainMath.js +87 -1
  87. package/dist/touch/TouchControlsOverlay.d.ts +18 -0
  88. package/dist/touch/TouchControlsOverlay.js +173 -0
  89. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  90. package/dist/vision/FrustumSensorHud.js +67 -0
  91. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  92. package/dist/vision/HiddenStateProbeHud.js +11 -0
  93. package/dist/vision/RevealVision.d.ts +28 -0
  94. package/dist/vision/RevealVision.js +53 -0
  95. package/dist/water/OceanShader.d.ts +1 -1
  96. package/dist/water/OceanShader.js +3 -3
  97. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  98. package/dist/weather/FireSpreadLayer.js +68 -0
  99. package/dist/weather/index.d.ts +1 -0
  100. package/dist/weather/index.js +1 -0
  101. package/dist/world/DataObjects.d.ts +44 -0
  102. package/dist/world/DataObjects.js +75 -0
  103. package/dist/world/GridWorldScene.d.ts +10 -0
  104. package/dist/world/GridWorldScene.js +54 -0
  105. package/dist/world/InstancedJoints.d.ts +14 -0
  106. package/dist/world/InstancedJoints.js +32 -0
  107. package/dist/world/WorldHud.d.ts +7 -1
  108. package/dist/world/WorldHud.js +76 -14
  109. package/dist/world/WorldItems.d.ts +5 -0
  110. package/dist/world/WorldItems.js +50 -0
  111. package/dist/world/floatTextStyle.d.ts +14 -0
  112. package/dist/world/floatTextStyle.js +39 -0
  113. package/llms.txt +2382 -0
  114. package/package.json +10 -5
  115. package/dist/demo/demoGame.d.ts +0 -2
  116. package/dist/demo/demoGame.js +0 -208
  117. package/dist/demo/environmentShowcase.d.ts +0 -2
  118. package/dist/demo/environmentShowcase.js +0 -15
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from "react";
3
+ import * as THREE from "three";
4
+ import { resolveGridInstances } from "@jgengine/core/world/gridInstances";
5
+ function isGridWorldFeature(feature) {
6
+ return (feature.kind === "biomes" || feature.kind === "voxel" || feature.kind === "plots" || feature.kind === "tilemap");
7
+ }
8
+ /**
9
+ * Data-driven renderer for the `biomes()`/`voxel()`/`plots()`/`tilemap()` world-feature kinds
10
+ * (#207.1): one `InstancedMesh` of extruded, colored boxes built from each feature's declared
11
+ * `cells`, following the same direct-buffer pattern as `InstancedBodies`.
12
+ */
13
+ export function GridWorldScene({ feature }) {
14
+ const instances = useMemo(() => (isGridWorldFeature(feature) ? resolveGridInstances(feature) : []), [feature]);
15
+ const meshRef = useRef(null);
16
+ const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
17
+ const material = useMemo(() => new THREE.MeshStandardMaterial({ roughness: 0.9, metalness: 0 }), []);
18
+ const instanceColor = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(Math.max(instances.length, 1) * 3), 3), [instances.length]);
19
+ useEffect(() => {
20
+ const mesh = meshRef.current;
21
+ if (mesh === null)
22
+ return;
23
+ mesh.instanceColor = instanceColor;
24
+ mesh.count = instances.length;
25
+ const m = mesh.instanceMatrix.array;
26
+ const c = instanceColor.array;
27
+ const color = new THREE.Color();
28
+ for (let i = 0; i < instances.length; i += 1) {
29
+ const instance = instances[i];
30
+ const o = i * 16;
31
+ m[o] = instance.scale[0];
32
+ m[o + 5] = instance.scale[1];
33
+ m[o + 10] = instance.scale[2];
34
+ m[o + 12] = instance.position[0];
35
+ m[o + 13] = instance.position[1];
36
+ m[o + 14] = instance.position[2];
37
+ m[o + 15] = 1;
38
+ color.set(instance.color);
39
+ const co = i * 3;
40
+ c[co] = color.r;
41
+ c[co + 1] = color.g;
42
+ c[co + 2] = color.b;
43
+ }
44
+ mesh.instanceMatrix.needsUpdate = true;
45
+ instanceColor.needsUpdate = true;
46
+ }, [instances, instanceColor]);
47
+ useEffect(() => () => {
48
+ geometry.dispose();
49
+ material.dispose();
50
+ }, [geometry, material]);
51
+ if (instances.length === 0)
52
+ return null;
53
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, instances.length], castShadow: true, receiveShadow: true }));
54
+ }
@@ -0,0 +1,14 @@
1
+ import * as THREE from "three";
2
+ import type { PhysicsWorld } from "@jgengine/core/physics/physicsWorld";
3
+ export interface InstancedJointsProps {
4
+ /** Physics world whose active joints are drawn as line segments between their world anchors. */
5
+ world: PhysicsWorld;
6
+ /** Line color. Default warm yellow. */
7
+ color?: THREE.ColorRepresentation;
8
+ }
9
+ /**
10
+ * Debug overlay drawing a PhysicsWorld's joints (suspension, ragdoll links, carry tethers) as one
11
+ * LineSegments batch. Endpoints are streamed each frame from `world.readJointSegments`; pair with
12
+ * `InstancedBodies` to see the constraint structure over the bodies.
13
+ */
14
+ export declare function InstancedJoints({ world, color }: InstancedJointsProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import * as THREE from "three";
5
+ /**
6
+ * Debug overlay drawing a PhysicsWorld's joints (suspension, ragdoll links, carry tethers) as one
7
+ * LineSegments batch. Endpoints are streamed each frame from `world.readJointSegments`; pair with
8
+ * `InstancedBodies` to see the constraint structure over the bodies.
9
+ */
10
+ export function InstancedJoints({ world, color = "#f5c542" }) {
11
+ const geometryRef = useRef(null);
12
+ const positions = useMemo(() => new Float32Array(world.jointCapacity * 6), [world.jointCapacity]);
13
+ const attribute = useMemo(() => new THREE.BufferAttribute(positions, 3), [positions]);
14
+ const material = useMemo(() => new THREE.LineBasicMaterial({ color }), [color]);
15
+ useEffect(() => {
16
+ const geometry = geometryRef.current;
17
+ if (geometry === null)
18
+ return;
19
+ geometry.setAttribute("position", attribute);
20
+ attribute.setUsage(THREE.DynamicDrawUsage);
21
+ }, [attribute]);
22
+ useFrame(() => {
23
+ const geometry = geometryRef.current;
24
+ if (geometry === null)
25
+ return;
26
+ const count = world.readJointSegments(positions);
27
+ attribute.needsUpdate = true;
28
+ geometry.setDrawRange(0, count * 2);
29
+ });
30
+ useEffect(() => () => material.dispose(), [material]);
31
+ return (_jsxs("lineSegments", { frustumCulled: false, children: [_jsx("bufferGeometry", { ref: geometryRef }), _jsx("primitive", { object: material, attach: "material" })] }));
32
+ }
@@ -1,11 +1,17 @@
1
- export declare function WorldEntityBars({ statId, height }: {
1
+ import type { SceneEntity } from "@jgengine/core/scene/entityStore";
2
+ import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
3
+ export declare function WorldEntityBars({ statId, height, roles, resolveRole, }: {
2
4
  statId: string;
3
5
  height?: number;
6
+ roles?: readonly CatalogEntityRole[];
7
+ resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
4
8
  }): import("react").JSX.Element;
5
9
  export declare function WorldFloatText({ height, lifeMs }: {
6
10
  height?: number;
7
11
  lifeMs?: number;
8
12
  }): import("react").JSX.Element;
13
+ export declare function WorldTelegraphs(): import("react").JSX.Element;
14
+ export declare function CombatCameraShake(): null;
9
15
  export declare function ProjectileTracers({ lifeMs }: {
10
16
  lifeMs?: number;
11
17
  }): import("react").JSX.Element;
@@ -1,9 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Html, Line } from "@react-three/drei";
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useFrame, useThree } from "@react-three/fiber";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
4
5
  import * as THREE from "three";
6
+ import { worldHealthBarAllowsRole } from "@jgengine/core/game/playableGame";
5
7
  import { useGameContext } from "@jgengine/react/provider";
6
8
  import { useEntityStat, useSceneEntities } from "@jgengine/react/hooks";
9
+ import { resolveFloatTextStyle } from "./floatTextStyle.js";
7
10
  const MUZZLE_HEIGHT = 1.4;
8
11
  function EntityBar({ entity, statId, height }) {
9
12
  const stat = useEntityStat(entity.id, statId);
@@ -13,27 +16,32 @@ function EntityBar({ entity, statId, height }) {
13
16
  const percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
14
17
  return (_jsx(Html, { position: [entity.position[0], entity.position[1] + height, entity.position[2]], center: true, distanceFactor: 12, zIndexRange: [20, 0], children: _jsx("div", { className: "h-2.5 w-28 overflow-hidden rounded-sm border border-black/70 bg-black/70 shadow", children: _jsx("div", { className: "h-full bg-gradient-to-r from-rose-600 to-red-400", style: { width: `${percent * 100}%` } }) }) }));
15
18
  }
16
- export function WorldEntityBars({ statId, height = 2.2 }) {
19
+ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, }) {
17
20
  const ctx = useGameContext();
18
21
  const entities = useSceneEntities();
19
22
  const playerId = ctx.player.userId;
20
- return (_jsx(_Fragment, { children: entities.map((entity) => entity.id === playerId ? null : (_jsx(EntityBar, { entity: entity, statId: statId, height: height }, entity.id))) }));
23
+ return (_jsx(_Fragment, { children: entities
24
+ .filter((entity) => entity.id !== playerId)
25
+ .filter((entity) => worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
26
+ .map((entity) => (_jsx(EntityBar, { entity: entity, statId: statId, height: height }, entity.id))) }));
21
27
  }
22
- function FloatTextItem({ text, kind, lifeMs }) {
28
+ function FloatTextItem({ event, lifeMs }) {
23
29
  const [shown, setShown] = useState(false);
30
+ const style = useMemo(() => resolveFloatTextStyle(event), [event]);
24
31
  useEffect(() => {
25
32
  const raf = requestAnimationFrame(() => setShown(true));
26
33
  return () => cancelAnimationFrame(raf);
27
34
  }, []);
28
- return (_jsx("span", { className: kind === "heal" ? "text-emerald-300" : "text-amber-200", style: {
35
+ return (_jsx("span", { style: {
29
36
  display: "inline-block",
30
- fontWeight: 800,
31
- fontSize: "18px",
32
- textShadow: "0 1px 3px rgba(0,0,0,0.95)",
37
+ color: style.color,
38
+ fontWeight: style.fontWeight,
39
+ fontSize: `${style.fontSizePx}px`,
40
+ textShadow: style.glow,
33
41
  transition: `transform ${lifeMs}ms ease-out, opacity ${lifeMs}ms ease-out`,
34
42
  transform: shown ? "translateY(-30px)" : "translateY(0)",
35
43
  opacity: shown ? 0 : 1,
36
- }, children: text }));
44
+ }, children: event.text }));
37
45
  }
38
46
  export function WorldFloatText({ height = 1.9, lifeMs = 950 }) {
39
47
  const ctx = useGameContext();
@@ -42,14 +50,68 @@ export function WorldFloatText({ height = 1.9, lifeMs = 950 }) {
42
50
  useEffect(() => {
43
51
  return ctx.game.events.on("entity.floatText", (event) => {
44
52
  const id = nextId.current++;
45
- setFloaters((current) => [
46
- ...current,
47
- { id, position: event.position, text: event.text, kind: event.kind },
48
- ]);
53
+ setFloaters((current) => [...current, { id, position: event.position, event }]);
49
54
  window.setTimeout(() => setFloaters((current) => current.filter((floater) => floater.id !== id)), lifeMs);
50
55
  });
51
56
  }, [ctx, lifeMs]);
52
- return (_jsx(_Fragment, { children: floaters.map((floater) => (_jsx(Html, { position: [floater.position[0], floater.position[1] + height, floater.position[2]], center: true, distanceFactor: 12, zIndexRange: [30, 0], children: _jsx(FloatTextItem, { text: floater.text, kind: floater.kind, lifeMs: lifeMs }) }, floater.id))) }));
57
+ return (_jsx(_Fragment, { children: floaters.map((floater) => (_jsx(Html, { position: [floater.position[0], floater.position[1] + height, floater.position[2]], center: true, distanceFactor: 12, zIndexRange: [30, 0], children: _jsx(FloatTextItem, { event: floater.event, lifeMs: lifeMs }) }, floater.id))) }));
58
+ }
59
+ function TelegraphGeometry({ shape }) {
60
+ if (shape.kind === "circle")
61
+ return _jsx("circleGeometry", { args: [shape.radius, 48] });
62
+ if (shape.kind === "ring")
63
+ return _jsx("ringGeometry", { args: [shape.innerRadius, shape.radius, 48] });
64
+ if (shape.kind === "cone")
65
+ return _jsx("circleGeometry", { args: [shape.radius, 40, -shape.angle / 2, shape.angle] });
66
+ return _jsx("planeGeometry", { args: [shape.width, shape.length] });
67
+ }
68
+ function TelegraphDecal({ active, nowMs }) {
69
+ const progress = Math.max(0, Math.min(1, (nowMs - active.bornMs) / active.event.windupMs));
70
+ const [x, y, z] = active.event.position;
71
+ const dir = active.event.dir ?? 0;
72
+ const shape = active.event.shape;
73
+ const forwardOffset = shape.kind === "line" ? shape.length / 2 : 0;
74
+ const color = active.event.kind === "danger" ? "#ef4444" : "#f59e0b";
75
+ const pulse = 0.45 + 0.5 * progress;
76
+ return (_jsxs("mesh", { position: [x + Math.sin(dir) * forwardOffset, y + 0.06, z + Math.cos(dir) * forwardOffset], rotation: [-Math.PI / 2, 0, shape.kind === "line" || shape.kind === "cone" ? -dir : 0], renderOrder: 999, children: [_jsx(TelegraphGeometry, { shape: shape }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: pulse, side: THREE.DoubleSide, depthWrite: false, depthTest: false, toneMapped: false })] }));
77
+ }
78
+ export function WorldTelegraphs() {
79
+ const ctx = useGameContext();
80
+ const [telegraphs, setTelegraphs] = useState([]);
81
+ const key = useRef(0);
82
+ const [nowMs, setNowMs] = useState(() => performance.now());
83
+ useFrame(() => setNowMs(performance.now()));
84
+ useEffect(() => {
85
+ return ctx.game.events.on("combat.telegraph", (event) => {
86
+ const entry = { key: key.current++, event, bornMs: performance.now() };
87
+ setTelegraphs((current) => [...current, entry]);
88
+ window.setTimeout(() => setTelegraphs((current) => current.filter((t) => t.key !== entry.key)), event.windupMs + 120);
89
+ });
90
+ }, [ctx]);
91
+ return (_jsx(_Fragment, { children: telegraphs.map((active) => (_jsx(TelegraphDecal, { active: active, nowMs: nowMs }, active.key))) }));
92
+ }
93
+ export function CombatCameraShake() {
94
+ const ctx = useGameContext();
95
+ const camera = useThree((state) => state.camera);
96
+ const trauma = useRef(0);
97
+ const decay = useRef(4);
98
+ useEffect(() => {
99
+ return ctx.game.events.on("combat.hitReaction", (event) => {
100
+ if (event.shake === undefined)
101
+ return;
102
+ trauma.current = Math.min(1, trauma.current + event.shake.amplitude);
103
+ decay.current = event.shake.decay;
104
+ });
105
+ }, [ctx]);
106
+ useFrame((_state, dt) => {
107
+ if (trauma.current <= 0.0001)
108
+ return;
109
+ const magnitude = trauma.current * trauma.current;
110
+ camera.position.x += (Math.random() * 2 - 1) * magnitude * 0.4;
111
+ camera.position.y += (Math.random() * 2 - 1) * magnitude * 0.4;
112
+ trauma.current = Math.max(0, trauma.current - decay.current * dt);
113
+ });
114
+ return null;
53
115
  }
54
116
  export function ProjectileTracers({ lifeMs = 130 }) {
55
117
  const ctx = useGameContext();
@@ -0,0 +1,5 @@
1
+ import type { WorldItemRenderConfig } from "@jgengine/core/game/playableGame";
2
+ /** Rarity→beam/color/label render binding + loot-filter overlay (#32/#33) for every dropped `worldItem`. */
3
+ export declare function WorldItems({ config }: {
4
+ config?: WorldItemRenderConfig;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Html, Line } from "@react-three/drei";
3
+ import { useMemo, useRef } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import { resolveWorldItemPresentation } from "@jgengine/core/game/worldItem";
6
+ import { useGameContext } from "@jgengine/react/provider";
7
+ import { useWorldItems } from "@jgengine/react/hooks";
8
+ import { POINTER_ENTITY_KEY } from "../pointer/pointerService.js";
9
+ const DEFAULT_BEAM_HEIGHT = 2.5;
10
+ const FALLBACK_COLOR = "#e5e7eb";
11
+ const REST_HEIGHT = 0.32;
12
+ const SPAWN_DURATION = 0.4;
13
+ function WorldItemMarker({ instanceId, position, color, beam, label, beamHeight, }) {
14
+ const beamPoints = useMemo(() => [
15
+ [0, 0, 0],
16
+ [0, beamHeight, 0],
17
+ ], [beamHeight]);
18
+ // Drop animation: the gem leaps up on spawn, settles, then idles with a slow
19
+ // spin + hover so a fresh drop reads as "something just fell here". Age is
20
+ // accumulated from frame deltas (Date.now is unavailable to keep runs pure).
21
+ const gemRef = useRef(null);
22
+ const ageRef = useRef(0);
23
+ useFrame((_state, delta) => {
24
+ const gem = gemRef.current;
25
+ if (gem === null)
26
+ return;
27
+ const age = ageRef.current + delta;
28
+ ageRef.current = age;
29
+ const spawn = age < SPAWN_DURATION ? Math.sin((age / SPAWN_DURATION) * Math.PI) * 0.35 : 0;
30
+ const bob = Math.sin(age * 2.2) * 0.06;
31
+ gem.position.y = REST_HEIGHT + bob + spawn;
32
+ gem.rotation.y = age * 1.6;
33
+ });
34
+ return (_jsxs("group", { position: [position[0], position[1], position[2]], userData: { [POINTER_ENTITY_KEY]: instanceId }, children: [_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.26, 0.4, 24] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85 })] }), _jsx("group", { ref: gemRef, "position-y": REST_HEIGHT, children: _jsxs("mesh", { children: [_jsx("octahedronGeometry", { args: [0.22, 0] }), _jsx("meshStandardMaterial", { color: color, emissive: color, emissiveIntensity: 0.6 })] }) }), beam ? _jsx(Line, { points: beamPoints, color: color, lineWidth: 2, transparent: true, opacity: 0.55 }) : null, label !== undefined && label.length > 0 ? (_jsx(Html, { position: [0, beamHeight + 0.2, 0], center: true, distanceFactor: 12, zIndexRange: [15, 0], children: _jsx("div", { className: "whitespace-nowrap rounded-sm border px-1.5 py-0.5 text-[11px] font-bold shadow", style: { borderColor: color, color, backgroundColor: "rgba(10,10,14,0.75)" }, children: label }) })) : null] }));
35
+ }
36
+ /** Rarity→beam/color/label render binding + loot-filter overlay (#32/#33) for every dropped `worldItem`. */
37
+ export function WorldItems({ config }) {
38
+ const ctx = useGameContext();
39
+ const items = useWorldItems();
40
+ const beamHeight = config?.beamHeight ?? DEFAULT_BEAM_HEIGHT;
41
+ return (_jsx(_Fragment, { children: items.map((item) => {
42
+ const entity = ctx.scene.entity.get(item.instanceId);
43
+ if (entity === null)
44
+ return null;
45
+ const presentation = resolveWorldItemPresentation(item, config?.rarityStyle, config?.filter);
46
+ if (presentation.hidden)
47
+ return null;
48
+ return (_jsx(WorldItemMarker, { instanceId: item.instanceId, position: entity.position, color: presentation.color ?? FALLBACK_COLOR, beam: presentation.beam, label: presentation.label, beamHeight: beamHeight }, item.instanceId));
49
+ }) }));
50
+ }
@@ -0,0 +1,14 @@
1
+ export interface FloatTextInfo {
2
+ kind: string;
3
+ hitType?: string;
4
+ element?: string;
5
+ crit?: boolean;
6
+ scale?: number;
7
+ }
8
+ export interface FloatTextStyle {
9
+ color: string;
10
+ fontSizePx: number;
11
+ fontWeight: number;
12
+ glow: string;
13
+ }
14
+ export declare function resolveFloatTextStyle(info: FloatTextInfo): FloatTextStyle;
@@ -0,0 +1,39 @@
1
+ const BASE_SIZE = 18;
2
+ const ELEMENT_COLORS = {
3
+ fire: "#fb923c",
4
+ frost: "#7dd3fc",
5
+ ice: "#7dd3fc",
6
+ lightning: "#fde047",
7
+ shock: "#fde047",
8
+ poison: "#a3e635",
9
+ rot: "#a3e635",
10
+ bleed: "#f87171",
11
+ arcane: "#c084fc",
12
+ holy: "#fef08a",
13
+ void: "#c084fc",
14
+ };
15
+ export function resolveFloatTextStyle(info) {
16
+ const crit = info.crit === true || info.hitType === "crit";
17
+ let color;
18
+ if (info.kind === "heal") {
19
+ color = "#6ee7b7";
20
+ }
21
+ else if (info.element !== undefined && ELEMENT_COLORS[info.element] !== undefined) {
22
+ color = ELEMENT_COLORS[info.element];
23
+ }
24
+ else if (crit) {
25
+ color = "#fbbf24";
26
+ }
27
+ else {
28
+ color = "#fde68a";
29
+ }
30
+ const critScale = crit ? 1.55 : 1;
31
+ const scale = info.scale ?? 1;
32
+ const fontSizePx = Math.round(BASE_SIZE * critScale * scale);
33
+ return {
34
+ color,
35
+ fontSizePx,
36
+ fontWeight: crit ? 900 : 800,
37
+ glow: crit ? `0 0 10px ${color}` : "0 1px 3px rgba(0,0,0,0.95)",
38
+ };
39
+ }