@jgengine/shell 0.10.0 → 0.12.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 (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -5,11 +5,12 @@ import { useEffect, useMemo, useRef, useState } from "react";
5
5
  import * as THREE from "three";
6
6
  import { useGameContext } from "@jgengine/react/provider";
7
7
  import { useCameraShake } from "../camera/shakeChannel.js";
8
+ import { CALIBRATED_TRAUMA_SHAKE_DECAY_PER_SECOND } from "../camera/rigMath.js";
8
9
  import { readFirstPersonMuzzle } from "../camera/GameFirstPersonCamera.js";
9
10
  import { resolveFloatTextStyle } from "./floatTextStyle.js";
10
- import { collectWorldBarSamples, paintWorldBarSamples, } from "./worldBarSamples.js";
11
+ import { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples, } from "./worldBarSamples.js";
11
12
  import { telegraphPulseOpacity } from "./telegraphPulse.js";
12
- export { collectWorldBarSamples, paintWorldBarSamples } from "./worldBarSamples.js";
13
+ export { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples } from "./worldBarSamples.js";
13
14
  export { telegraphPulseOpacity } from "./telegraphPulse.js";
14
15
  function pinOverlayToViewport(_el, _camera, size) {
15
16
  return [size.width / 2, size.height / 2];
@@ -41,6 +42,86 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
41
42
  });
42
43
  return (_jsx(Html, { fullscreen: true, calculatePosition: pinOverlayToViewport, zIndexRange: [20, 0], style: { pointerEvents: "none" }, children: _jsx("canvas", { ref: canvasRef, style: { width: "100%", height: "100%", display: "block", pointerEvents: "none" } }) }));
43
44
  }
45
+ /**
46
+ * Billboarded name + 78×6px HP bar over every nearby non-local entity that
47
+ * passes `roles`/`maxDistance` — headless (className/data-* slots on every
48
+ * part, `renderNameplate` for a full swap), turned on declaratively via
49
+ * `defineGame({ nameplates })` rather than mounted by hand.
50
+ *
51
+ * @capability nameplates billboarded name + HP bar over nearby entities
52
+ */
53
+ export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, tickMs = 120, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }) {
54
+ const ctx = useGameContext();
55
+ const camera = useThree((state) => state.camera);
56
+ const size = useThree((state) => state.size);
57
+ const [samples, setSamples] = useState([]);
58
+ const samplesRef = useRef([]);
59
+ const projectRef = useRef(new THREE.Vector3());
60
+ const lastTickRef = useRef(0);
61
+ useFrame((state) => {
62
+ const nowMs = state.clock.elapsedTime * 1000;
63
+ if (nowMs - lastTickRef.current < tickMs)
64
+ return;
65
+ lastTickRef.current = nowMs;
66
+ camera.updateMatrixWorld();
67
+ collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, { width: size.width, height: size.height }, samplesRef.current, projectRef.current, maxDistance);
68
+ setSamples([...samplesRef.current]);
69
+ });
70
+ return (_jsx(Html, { fullscreen: true, calculatePosition: pinOverlayToViewport, zIndexRange: [19, 0], style: { pointerEvents: "none" }, children: _jsx("div", { className: className, "data-nameplates": true, children: samples.map((sample) => renderNameplate !== undefined ? (_jsx("div", { style: { position: "absolute", left: sample.x, top: sample.y }, children: renderNameplate(sample) }, sample.id)) : (_jsxs("div", { className: nameplateClassName, "data-nameplate": sample.id, style: {
71
+ position: "absolute",
72
+ left: sample.x,
73
+ top: sample.y,
74
+ transform: "translate(-50%, -100%)",
75
+ display: "flex",
76
+ flexDirection: "column",
77
+ alignItems: "center",
78
+ gap: 2,
79
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
80
+ }, children: [_jsx("span", { className: nameClassName, "data-nameplate-name": true, style: {
81
+ fontSize: 12,
82
+ fontWeight: 700,
83
+ color: "#f1f5f9",
84
+ textShadow: "0 1px 3px rgba(0,0,0,0.9)",
85
+ whiteSpace: "nowrap",
86
+ }, children: sample.name }), sample.percent !== null ? (_jsx("span", { className: barClassName, "data-nameplate-bar": true, style: {
87
+ display: "block",
88
+ width: 78,
89
+ height: 6,
90
+ borderRadius: 2,
91
+ background: "rgba(0,0,0,0.65)",
92
+ border: "1px solid rgba(0,0,0,0.8)",
93
+ overflow: "hidden",
94
+ }, children: _jsx("span", { className: fillClassName, "data-nameplate-fill": true, style: {
95
+ display: "block",
96
+ width: `${sample.percent * 100}%`,
97
+ height: "100%",
98
+ background: sample.percent > 0.5 ? "#22c55e" : sample.percent > 0.25 ? "#f59e0b" : "#ef4444",
99
+ } }) })) : null] }, sample.id))) }) }));
100
+ }
101
+ /**
102
+ * Ground ring over every `ctx.scene.object.selection`-ed placed object — the object-layer counterpart
103
+ * to `WorldEntityBars`/`WorldNameplates`. Mount it once in the game's scene (headless: no defaults are
104
+ * imposed beyond a visible ring) instead of hand-rolling a selection highlight through `WorldOverlay`
105
+ * against external state.
106
+ *
107
+ * @capability world-object-highlights ground-ring highlight over every selected placed object
108
+ */
109
+ export function WorldObjectHighlights({ color = "#facc15", radius, y = 0.05 }) {
110
+ const ctx = useGameContext();
111
+ const [ids, setIds] = useState(() => ctx.scene.object.selection.list());
112
+ useEffect(() => {
113
+ setIds(ctx.scene.object.selection.list());
114
+ return ctx.subscribe(() => setIds(ctx.scene.object.selection.list()));
115
+ }, [ctx]);
116
+ return (_jsx(_Fragment, { children: ids.map((id) => {
117
+ const object = ctx.scene.object.get(id);
118
+ if (object === null)
119
+ return null;
120
+ const halfExtents = ctx.scene.object.catalog(id)?.halfExtents;
121
+ const ringRadius = radius ?? (halfExtents === undefined ? 1.2 : Math.max(halfExtents[0], halfExtents[2]) * 1.35);
122
+ return (_jsxs("mesh", { position: [object.position[0], object.position[1] + y, object.position[2]], rotation: [-Math.PI / 2, 0, 0], renderOrder: 998, children: [_jsx("ringGeometry", { args: [ringRadius * 0.82, ringRadius, 40] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85, side: THREE.DoubleSide, depthWrite: false, toneMapped: false })] }, id));
123
+ }) }));
124
+ }
44
125
  function FloatTextItem({ event, lifeMs }) {
45
126
  const [shown, setShown] = useState(false);
46
127
  const style = useMemo(() => resolveFloatTextStyle(event), [event]);
@@ -142,6 +223,10 @@ export function CombatCameraShake() {
142
223
  const shake = useCameraShake();
143
224
  useEffect(() => {
144
225
  return ctx.game.events.on("combat.hitReaction", (event) => {
226
+ if (event.trauma !== undefined) {
227
+ shake.shake(event.trauma, CALIBRATED_TRAUMA_SHAKE_DECAY_PER_SECOND);
228
+ return;
229
+ }
145
230
  if (event.shake === undefined)
146
231
  return;
147
232
  shake.shake(event.shake.amplitude, event.shake.decay);
@@ -0,0 +1,21 @@
1
+ import { type ComponentType, type ReactNode } from "react";
2
+ import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
3
+ import type { SceneEntity } from "@jgengine/core/scene/entityStore";
4
+ import { type SceneObject } from "@jgengine/core/scene/objectStore";
5
+ import type { EntitySpriteConfig, ModelConfig, ObjectStyle } from "@jgengine/core/game/playableGame";
6
+ import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
7
+ export declare function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }: {
8
+ entitySprites: Record<string, EntitySpriteConfig> | undefined;
9
+ entityModels: Record<string, string | ModelConfig> | undefined;
10
+ objectModels: Record<string, string | ModelConfig> | undefined;
11
+ objectStyles: Record<string, ObjectStyle> | undefined;
12
+ environment: ComponentType | undefined;
13
+ assets: AssetCatalog;
14
+ renderEntity: ((entity: SceneEntity) => ReactNode) | undefined;
15
+ renderObject: ((object: SceneObject) => ReactNode) | undefined;
16
+ selectedIds: ReadonlySet<string>;
17
+ hideLocalActor: boolean;
18
+ }): import("react").JSX.Element;
19
+ export declare function RemotePlayers({ rows }: {
20
+ rows: PresencePoseRow[];
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,186 @@
1
+ import { Fragment as _Fragment, 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
+ import { objectVisualScale } from "@jgengine/core/scene/objectStore";
6
+ import { WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
7
+ import { beginFallbackSeam, endFallbackPass, reportFallbackSeam, } from "@jgengine/core/devtools/fallbackSeams";
8
+ import { useGameContext } from "@jgengine/react/provider";
9
+ import { useSceneEntityIds, useSceneObjectIds, useGameStore, usePlayer, useTarget } from "@jgengine/react/hooks";
10
+ import { colorFromId } from "../worldSky.js";
11
+ import { DefaultSurface, detailMaps } from "../render/defaultSurface.js";
12
+ import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
13
+ import { resolveModel, resolveEntityModel, tryResolveCatalogModel } from "../render/resolveModel.js";
14
+ import { useRenderVisibility } from "../visibility/CullingProvider.js";
15
+ import { writeEntityPose } from "./entityPose.js";
16
+ import { POINTER_ENTITY_KEY, POINTER_OBJECT_KEY } from "../pointer/pointerService.js";
17
+ const GROUND_SIZE = 160;
18
+ const GROUND_SEGMENTS = 80;
19
+ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
20
+ const groupRef = useRef(null);
21
+ const ctx = useGameContext();
22
+ const visibleRef = useRenderVisibility();
23
+ const entityId = entity.id;
24
+ const role = entity.role;
25
+ const name = entity.name;
26
+ const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
27
+ useFrame(() => {
28
+ const group = groupRef.current;
29
+ if (group === null)
30
+ return;
31
+ const live = ctx.scene.entity.get(entityId);
32
+ if (live === null)
33
+ return;
34
+ writeEntityPose(group, live);
35
+ group.visible = visibleRef.current(entityId);
36
+ });
37
+ return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
38
+ event.stopPropagation();
39
+ if (isLocal)
40
+ return;
41
+ const live = ctx.scene.entity.get(entityId);
42
+ if (live !== null)
43
+ onSelect(live);
44
+ }, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: entityId, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, castShadow: true, receiveShadow: true, children: [_jsx("sphereGeometry", { args: [0.45, 24, 24] }), _jsx(DefaultSurface, { color: color })] })) : (_jsxs("group", { scale: ctx.scene.entity.visualScaleOf(entityId), children: [_jsxs("mesh", { "position-y": 0.95, castShadow: true, receiveShadow: true, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx(DefaultSurface, { color: color, roughness: 0.6 })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
45
+ }
46
+ function ObjectMarker({ object, custom, model, style, }) {
47
+ const groupRef = useRef(null);
48
+ const ctx = useGameContext();
49
+ const visibleRef = useRenderVisibility();
50
+ const instanceId = object.instanceId;
51
+ const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
52
+ const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
53
+ const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
54
+ useFrame(() => {
55
+ const group = groupRef.current;
56
+ if (group === null)
57
+ return;
58
+ const live = ctx.scene.object.get(instanceId);
59
+ if (live === null)
60
+ return;
61
+ writeEntityPose(group, live);
62
+ group.visible = visibleRef.current(instanceId);
63
+ });
64
+ return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: instanceId })) : style?.hidden === true ? null : (_jsxs("mesh", { "position-y": 0.5 * scaleY, scale: [scaleX, scaleY, scaleZ], castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx(DefaultSurface, { color: color, transparent: opacity < 1, opacity: opacity })] })) }));
65
+ }
66
+ function GroundPlane() {
67
+ const geometry = useMemo(() => {
68
+ const next = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE, GROUND_SEGMENTS, GROUND_SEGMENTS);
69
+ const positions = next.attributes.position;
70
+ const base = new THREE.Color("#33452f");
71
+ const high = new THREE.Color("#4a5c3a");
72
+ const colors = new Float32Array(positions.count * 3);
73
+ const tmp = new THREE.Color();
74
+ for (let index = 0; index < positions.count; index += 1) {
75
+ const x = positions.getX(index);
76
+ const y = positions.getY(index);
77
+ const height = Math.sin(x * 0.16) * 0.12 +
78
+ Math.cos(y * 0.11) * 0.1 +
79
+ Math.sin((x + y) * 0.05) * 0.16;
80
+ positions.setZ(index, height);
81
+ const blend = 0.5 + Math.sin(x * 0.21) * 0.25 + Math.cos(y * 0.17) * 0.25;
82
+ tmp.copy(base).lerp(high, Math.min(1, Math.max(0, blend)));
83
+ colors[index * 3] = tmp.r;
84
+ colors[index * 3 + 1] = tmp.g;
85
+ colors[index * 3 + 2] = tmp.b;
86
+ }
87
+ next.setAttribute("color", new THREE.BufferAttribute(colors, 3));
88
+ next.computeVertexNormals();
89
+ return next;
90
+ }, []);
91
+ const material = useMemo(() => {
92
+ const normal = detailMaps().normal.clone();
93
+ normal.repeat.set(48, 48);
94
+ normal.needsUpdate = true;
95
+ return new THREE.MeshStandardMaterial({
96
+ vertexColors: true,
97
+ roughness: 0.95,
98
+ metalness: 0,
99
+ normalMap: normal,
100
+ normalScale: new THREE.Vector2(0.8, 0.8),
101
+ envMapIntensity: 0.4,
102
+ });
103
+ }, []);
104
+ useEffect(() => () => material.dispose(), [material]);
105
+ return _jsx("mesh", { "rotation-x": -Math.PI / 2, geometry: geometry, material: material, receiveShadow: true });
106
+ }
107
+ function RockField() {
108
+ const rocks = useMemo(() => Array.from({ length: 18 }, (_, index) => {
109
+ const angle = index * 2.399963229728653;
110
+ const radius = 10 + ((index * 17) % 58);
111
+ return {
112
+ id: `rock-${index}`,
113
+ x: Math.cos(angle) * radius,
114
+ z: Math.sin(angle) * radius,
115
+ scale: 0.45 + ((index * 13) % 11) / 10,
116
+ rotation: angle,
117
+ };
118
+ }), []);
119
+ return (_jsx(_Fragment, { children: rocks.map((rock) => (_jsxs("mesh", { position: [rock.x, 0.25 * rock.scale, rock.z], rotation: [0.1, rock.rotation, -0.08], scale: [rock.scale * 1.4, rock.scale * 0.7, rock.scale], castShadow: true, receiveShadow: true, children: [_jsx("dodecahedronGeometry", { args: [0.8, 1] }), _jsx(DefaultSurface, { color: "#6b6f63", roughness: 0.95 })] }, rock.id))) }));
120
+ }
121
+ function WorldEnvironment({ environment: Environment }) {
122
+ // Dev diagnostic (no-op unless armed): a missing environment component means the default green
123
+ // ground/rocks stand in for an unauthored scene. Re-zero then report so this is idempotent.
124
+ beginFallbackSeam("ground");
125
+ if (Environment !== undefined)
126
+ return _jsx(Environment, {});
127
+ reportFallbackSeam("ground", "noScene");
128
+ return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx(RockField, {})] }));
129
+ }
130
+ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
131
+ const ctx = useGameContext();
132
+ const entityIds = useSceneEntityIds();
133
+ const objectIds = useSceneObjectIds();
134
+ const player = usePlayer();
135
+ const targetId = useTarget(player.userId);
136
+ const controlledId = useGameStore((c) => c.player.possession.active(player.userId));
137
+ const handleSelect = (entity) => {
138
+ const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
139
+ ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
140
+ };
141
+ // Dev diagnostic (no-op unless armed): re-zero the entity/object seams before this render re-tallies
142
+ // which actors fall back to primitives. Committing the log runs in a useEffect after the render.
143
+ beginFallbackSeam("entity");
144
+ beginFallbackSeam("object");
145
+ useEffect(() => {
146
+ endFallbackPass();
147
+ });
148
+ return (_jsxs(_Fragment, { children: [entityIds.map((entityId) => {
149
+ const entity = ctx.scene.entity.get(entityId);
150
+ if (entity === null || entity.name === WORLD_ITEM_ENTITY_NAME)
151
+ return null;
152
+ if (hideLocalActor && entityId === controlledId)
153
+ return null;
154
+ const custom = renderEntity?.(entity);
155
+ const model = resolveEntityModel(entityModels?.[entity.name], assets, entity.name);
156
+ const sprite = entitySprites?.[entity.name];
157
+ // Entity models resolve via the hard resolveModel (throws on a present-but-missing id), so
158
+ // reaching the primitive capsule with no model or sprite means the mapping was omitted.
159
+ if ((custom === undefined || custom === null) && model === undefined && sprite === undefined) {
160
+ reportFallbackSeam("entity", "omittedMapping");
161
+ }
162
+ return (_jsx(EntityMarker, { entity: entity, custom: custom, model: model, sprite: sprite, isLocal: entityId === controlledId, targeted: entityId === targetId, selected: selectedIds.has(entityId), onSelect: handleSelect }, entityId));
163
+ }), objectIds.map((instanceId) => {
164
+ const object = ctx.scene.object.get(instanceId);
165
+ if (object === null)
166
+ return null;
167
+ const custom = renderObject?.(object);
168
+ const model = resolveModel(objectModels?.[object.catalogId], assets, {
169
+ seam: "objectModels",
170
+ key: object.catalogId,
171
+ }) ?? tryResolveCatalogModel(object.catalogId, assets);
172
+ const style = objectStyles?.[object.catalogId];
173
+ // Reaching the primitive box means no model resolved. A present-but-unresolved objectModels
174
+ // key implies its asset pack is not pulled; an absent key is an omitted (often intended) mapping.
175
+ if ((custom === undefined || custom === null) && model === undefined && style?.hidden !== true) {
176
+ reportFallbackSeam("object", objectModels?.[object.catalogId] === undefined ? "omittedMapping" : "unpulledPack");
177
+ }
178
+ return (_jsx(ObjectMarker, { object: object, custom: custom, model: model, style: style }, instanceId));
179
+ })] }));
180
+ }
181
+ export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
182
+ return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor })] }));
183
+ }
184
+ export function RemotePlayers({ rows }) {
185
+ return (_jsx(_Fragment, { children: rows.map((row) => (_jsxs("group", { position: [row.position.x, row.position.y, row.position.z], "rotation-y": row.rotationY, children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: colorFromId(row.userId) })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] }, row.userId))) }));
186
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Renders `combat.vfx` bursts as additive sprite-particle effects in the world scene. Auto-mounted by the
3
+ * player shell alongside the other world overlays; games emit effects via `ctx.scene.entity.vfx(...)`.
4
+ *
5
+ * @internal
6
+ */
7
+ export declare function WorldSpellVfx({ tailMs }: {
8
+ tailMs?: number;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,184 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import * as THREE from "three";
5
+ import { useGameContext } from "@jgengine/react/provider";
6
+ const PARTICLE_COUNT = {
7
+ projectile: 7,
8
+ beam: 6,
9
+ nova: 14,
10
+ glow: 10,
11
+ spark: 12,
12
+ };
13
+ function planarDir() {
14
+ const angle = Math.random() * Math.PI * 2;
15
+ return new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle));
16
+ }
17
+ function sphereDir() {
18
+ return new THREE.Vector3(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1)
19
+ .normalize();
20
+ }
21
+ function makeParticles(kind) {
22
+ const count = PARTICLE_COUNT[kind];
23
+ const out = [];
24
+ for (let i = 0; i < count; i += 1) {
25
+ if (kind === "nova") {
26
+ out.push({ offset: new THREE.Vector3(), dir: planarDir(), speed: 1, size: 0.5 + Math.random() * 0.4, delay: 0 });
27
+ }
28
+ else if (kind === "spark") {
29
+ const dir = sphereDir();
30
+ dir.y = Math.abs(dir.y) * 0.6 + 0.2;
31
+ out.push({ offset: new THREE.Vector3(), dir, speed: 2.4 + Math.random() * 1.8, size: 0.28 + Math.random() * 0.22, delay: 0 });
32
+ }
33
+ else if (kind === "glow") {
34
+ out.push({
35
+ offset: planarDir().multiplyScalar(0.35 + Math.random() * 0.5),
36
+ dir: new THREE.Vector3(0, 1, 0),
37
+ speed: 0.5 + Math.random() * 0.6,
38
+ size: 0.55 + Math.random() * 0.5,
39
+ delay: Math.random() * 0.3,
40
+ });
41
+ }
42
+ else if (kind === "beam") {
43
+ out.push({ offset: new THREE.Vector3(), dir: new THREE.Vector3(), speed: 0, size: 0.45 + Math.random() * 0.3, delay: 0 });
44
+ }
45
+ else {
46
+ out.push({
47
+ offset: sphereDir().multiplyScalar(0.12),
48
+ dir: new THREE.Vector3(),
49
+ speed: 0,
50
+ size: 0.4 + Math.random() * 0.25,
51
+ delay: i * 0.05,
52
+ });
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ function useRadialTexture() {
58
+ return useMemo(() => {
59
+ const size = 64;
60
+ const canvas = document.createElement("canvas");
61
+ canvas.width = size;
62
+ canvas.height = size;
63
+ const c = canvas.getContext("2d");
64
+ if (c !== null) {
65
+ const gradient = c.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
66
+ gradient.addColorStop(0, "rgba(255,255,255,1)");
67
+ gradient.addColorStop(0.35, "rgba(255,255,255,0.65)");
68
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
69
+ c.fillStyle = gradient;
70
+ c.fillRect(0, 0, size, size);
71
+ }
72
+ const texture = new THREE.CanvasTexture(canvas);
73
+ texture.needsUpdate = true;
74
+ return texture;
75
+ }, []);
76
+ }
77
+ function clamp01(value) {
78
+ return value < 0 ? 0 : value > 1 ? 1 : value;
79
+ }
80
+ function VfxItem({ event, texture }) {
81
+ const born = useRef(performance.now());
82
+ const sprites = useRef([]);
83
+ const ringRef = useRef(null);
84
+ const ringMat = useRef(null);
85
+ const beamMat = useRef(null);
86
+ const color = useMemo(() => new THREE.Color(event.color), [event.color]);
87
+ const from = useMemo(() => new THREE.Vector3(event.from[0], event.from[1] + 1, event.from[2]), [event.from]);
88
+ const to = useMemo(() => (event.to === undefined ? from.clone() : new THREE.Vector3(event.to[0], event.to[1] + 1, event.to[2])), [event.to, from]);
89
+ const radius = event.radius ?? 2.5;
90
+ const beam = useMemo(() => {
91
+ const delta = new THREE.Vector3().subVectors(to, from);
92
+ const length = Math.max(0.001, delta.length());
93
+ const mid = new THREE.Vector3().addVectors(from, to).multiplyScalar(0.5);
94
+ const quat = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.clone().normalize());
95
+ return { length, mid, quat };
96
+ }, [from, to]);
97
+ const particles = useMemo(() => makeParticles(event.kind), [event.kind]);
98
+ const scratch = useRef(new THREE.Vector3());
99
+ useFrame(() => {
100
+ const p = clamp01((performance.now() - born.current) / event.durationMs);
101
+ const kind = event.kind;
102
+ for (let i = 0; i < particles.length; i += 1) {
103
+ const sprite = sprites.current[i];
104
+ const particle = particles[i];
105
+ if (sprite === undefined || sprite === null || particle === undefined)
106
+ continue;
107
+ const pos = scratch.current;
108
+ let opacity = 0;
109
+ let scale = particle.size;
110
+ if (kind === "projectile") {
111
+ const travel = clamp01(p * 1.15 - particle.delay);
112
+ pos.copy(from).lerp(to, travel).add(particle.offset);
113
+ opacity = (1 - p) * (i === 0 ? 1 : 0.55);
114
+ scale = particle.size * (i === 0 ? 1 : 0.7);
115
+ }
116
+ else if (kind === "beam") {
117
+ pos.copy(from).lerp(to, i / Math.max(1, particles.length - 1));
118
+ opacity = (1 - p) * 0.8;
119
+ }
120
+ else if (kind === "nova") {
121
+ pos.set(event.from[0], event.from[1] + 0.4, event.from[2]).addScaledVector(particle.dir, radius * p);
122
+ opacity = (1 - p) * 0.9;
123
+ }
124
+ else if (kind === "glow") {
125
+ const local = clamp01(p - particle.delay);
126
+ pos.copy(from).add(particle.offset).addScaledVector(particle.dir, particle.speed * local);
127
+ opacity = Math.sin(clamp01(p) * Math.PI) * 0.9;
128
+ scale = particle.size * (0.8 + 0.3 * Math.sin(local * Math.PI));
129
+ }
130
+ else {
131
+ pos.copy(to).addScaledVector(particle.dir, particle.speed * p);
132
+ pos.y -= p * p * 1.4;
133
+ opacity = (1 - p) * 0.95;
134
+ }
135
+ sprite.position.copy(pos);
136
+ sprite.scale.set(scale, scale, 1);
137
+ const material = sprite.material;
138
+ material.opacity = opacity;
139
+ }
140
+ if (ringRef.current !== null && ringMat.current !== null) {
141
+ const s = Math.max(0.001, radius * p);
142
+ ringRef.current.scale.set(s, s, s);
143
+ ringMat.current.opacity = (1 - p) * 0.7;
144
+ }
145
+ if (beamMat.current !== null) {
146
+ beamMat.current.opacity = (1 - p) * 0.85;
147
+ }
148
+ });
149
+ return (_jsxs("group", { children: [particles.map((_, i) => (_jsx("sprite", { ref: (node) => {
150
+ sprites.current[i] = node;
151
+ }, children: _jsx("spriteMaterial", { map: texture, color: color, transparent: true, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, toneMapped: false, opacity: 0 }) }, i))), event.kind === "nova" && (_jsxs("mesh", { ref: ringRef, position: [event.from[0], event.from[1] + 0.06, event.from[2]], rotation: [-Math.PI / 2, 0, 0], renderOrder: 998, children: [_jsx("ringGeometry", { args: [0.82, 1, 48] }), _jsx("meshBasicMaterial", { ref: ringMat, color: color, transparent: true, side: THREE.DoubleSide, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, toneMapped: false, opacity: 0 })] })), event.kind === "beam" && (_jsxs("mesh", { position: [beam.mid.x, beam.mid.y, beam.mid.z], quaternion: [beam.quat.x, beam.quat.y, beam.quat.z, beam.quat.w], renderOrder: 998, children: [_jsx("cylinderGeometry", { args: [0.12, 0.12, beam.length, 8, 1, true] }), _jsx("meshBasicMaterial", { ref: beamMat, color: color, transparent: true, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending, toneMapped: false, opacity: 0 })] }))] }));
152
+ }
153
+ /**
154
+ * Renders `combat.vfx` bursts as additive sprite-particle effects in the world scene. Auto-mounted by the
155
+ * player shell alongside the other world overlays; games emit effects via `ctx.scene.entity.vfx(...)`.
156
+ *
157
+ * @internal
158
+ */
159
+ export function WorldSpellVfx({ tailMs = 200 }) {
160
+ const ctx = useGameContext();
161
+ const texture = useRadialTexture();
162
+ const [active, setActive] = useState([]);
163
+ const key = useRef(0);
164
+ const timers = useRef(new Set());
165
+ useEffect(() => {
166
+ const pending = timers.current;
167
+ const off = ctx.game.events.on("combat.vfx", (event) => {
168
+ const entry = { key: key.current++, event };
169
+ setActive((current) => [...current, entry]);
170
+ const timer = setTimeout(() => {
171
+ pending.delete(timer);
172
+ setActive((current) => current.filter((item) => item.key !== entry.key));
173
+ }, event.durationMs + tailMs);
174
+ pending.add(timer);
175
+ });
176
+ return () => {
177
+ off();
178
+ for (const timer of pending)
179
+ clearTimeout(timer);
180
+ pending.clear();
181
+ };
182
+ }, [ctx, tailMs]);
183
+ return (_jsx(_Fragment, { children: active.map((item) => (_jsx(VfxItem, { event: item.event, texture: texture }, item.key))) }));
184
+ }
@@ -10,5 +10,7 @@ export interface PoseSource {
10
10
  position: readonly [number, number, number];
11
11
  rotationY: number;
12
12
  }
13
+ /** @internal */
13
14
  export declare function writeEntityPose(target: PoseWritable, source: PoseSource): void;
15
+ /** @internal */
14
16
  export declare function posesEqual(a: PoseSource, b: PoseSource): boolean;
@@ -1,7 +1,9 @@
1
+ /** @internal */
1
2
  export function writeEntityPose(target, source) {
2
3
  target.position.set(source.position[0], source.position[1], source.position[2]);
3
4
  target.rotation.y = source.rotationY;
4
5
  }
6
+ /** @internal */
5
7
  export function posesEqual(a, b) {
6
8
  return (a.position[0] === b.position[0] &&
7
9
  a.position[1] === b.position[1] &&
@@ -11,4 +11,5 @@ export interface FloatTextStyle {
11
11
  fontWeight: number;
12
12
  glow: string;
13
13
  }
14
+ /** @internal */
14
15
  export declare function resolveFloatTextStyle(info: FloatTextInfo): FloatTextStyle;
@@ -12,6 +12,7 @@ const ELEMENT_COLORS = {
12
12
  holy: "#fef08a",
13
13
  void: "#c084fc",
14
14
  };
15
+ /** @internal */
15
16
  export function resolveFloatTextStyle(info) {
16
17
  const crit = info.crit === true || info.hitType === "crit";
17
18
  let color;
@@ -1 +1,2 @@
1
+ /** @internal */
1
2
  export declare function telegraphPulseOpacity(bornMs: number, windupMs: number, nowMs: number): number;
@@ -1,3 +1,4 @@
1
+ /** @internal */
1
2
  export function telegraphPulseOpacity(bornMs, windupMs, nowMs) {
2
3
  const progress = Math.max(0, Math.min(1, (nowMs - bornMs) / Math.max(1, windupMs)));
3
4
  return 0.45 + 0.5 * progress;
@@ -16,6 +16,7 @@ export interface Projectable {
16
16
  y: number;
17
17
  z: number;
18
18
  }
19
+ /** @internal */
19
20
  export declare function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: {
20
21
  matrixWorldInverse: unknown;
21
22
  projectionMatrix: unknown;
@@ -23,6 +24,30 @@ export declare function collectWorldBarSamples(ctx: GameContext, statId: string,
23
24
  width: number;
24
25
  height: number;
25
26
  }, into: WorldBarSample[], project: Projectable, maxDistance?: number): number;
27
+ /** One entity's projected nameplate: screen `x`/`y`, display `name`, health `percent` (or `null` when statless), and world `distance` from the player. */
28
+ export interface NameplateSample {
29
+ id: string;
30
+ name: string;
31
+ x: number;
32
+ y: number;
33
+ /** Health-stat fraction 0..1, or `null` when the entity carries no `statId` stat (name-only nameplate). */
34
+ percent: number | null;
35
+ distance: number;
36
+ }
37
+ /**
38
+ * Projects every non-local, role/distance-filtered entity into `into` as a
39
+ * `NameplateSample`. Pure data step powering `WorldNameplates` — swap in your
40
+ * own renderer by calling this directly instead of the component.
41
+ * @internal
42
+ */
43
+ export declare function collectNameplateSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: {
44
+ matrixWorldInverse: unknown;
45
+ projectionMatrix: unknown;
46
+ }, viewport: {
47
+ width: number;
48
+ height: number;
49
+ }, into: NameplateSample[], project: Projectable, maxDistance?: number): number;
50
+ /** @internal */
26
51
  export declare function paintWorldBarSamples(canvas: {
27
52
  width: number;
28
53
  height: number;
@@ -1,4 +1,5 @@
1
1
  import { worldHealthBarAllowsRole } from "@jgengine/core/game/playableGame";
2
+ /** @internal */
2
3
  export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance = 60) {
3
4
  into.length = 0;
4
5
  const playerId = ctx.player.userId;
@@ -28,6 +29,43 @@ export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole,
28
29
  }
29
30
  return into.length;
30
31
  }
32
+ /**
33
+ * Projects every non-local, role/distance-filtered entity into `into` as a
34
+ * `NameplateSample`. Pure data step powering `WorldNameplates` — swap in your
35
+ * own renderer by calling this directly instead of the component.
36
+ * @internal
37
+ */
38
+ export function collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance = 40) {
39
+ into.length = 0;
40
+ const playerId = ctx.player.userId;
41
+ const player = ctx.scene.entity.get(playerId);
42
+ for (const entity of ctx.scene.entity.list()) {
43
+ if (entity.id === playerId)
44
+ continue;
45
+ if (!worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
46
+ continue;
47
+ const distance = player === null
48
+ ? 0
49
+ : Math.hypot(entity.position[0] - player.position[0], entity.position[2] - player.position[2]);
50
+ if (player !== null && distance > maxDistance)
51
+ continue;
52
+ const stat = ctx.scene.entity.stats.get(entity.id, statId);
53
+ let percent = null;
54
+ if (stat !== null) {
55
+ const range = stat.max - stat.min;
56
+ percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
57
+ }
58
+ project.set(entity.position[0], entity.position[1] + height, entity.position[2]);
59
+ project.project(camera);
60
+ if (project.z < -1 || project.z > 1)
61
+ continue;
62
+ const x = (project.x * 0.5 + 0.5) * viewport.width;
63
+ const y = (-project.y * 0.5 + 0.5) * viewport.height;
64
+ into.push({ id: entity.id, name: entity.name, x, y, percent, distance });
65
+ }
66
+ return into.length;
67
+ }
68
+ /** @internal */
31
69
  export function paintWorldBarSamples(canvas, samples, dpr, barWidthPx = 112, barHeightPx = 10) {
32
70
  const g = canvas.getContext("2d");
33
71
  if (g === null)