@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.
- package/CHANGELOG.md +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +135 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
package/dist/world/WorldHud.js
CHANGED
|
@@ -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;
|
package/dist/world/entityPose.js
CHANGED
|
@@ -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] &&
|
|
@@ -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)
|