@jgengine/shell 0.7.0 → 0.9.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 +63 -0
- package/README.md +37 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +36 -1
- package/dist/GamePlayerShell.js +751 -80
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +12 -0
- package/dist/behaviour.js +27 -0
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +16 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.js +10 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +8 -1
- package/dist/camera/cameraRigs.js +78 -38
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +7 -2
- package/dist/camera/index.js +7 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +50 -1
- package/dist/camera/rigMath.js +91 -1
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +245 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +52 -0
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
- package/dist/devtools/DevtoolsOverlay.js +676 -0
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +56 -0
- package/dist/environment/Daylight.js +101 -0
- package/dist/environment/EnvironmentScene.js +87 -48
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +3 -0
- package/dist/environment/index.js +3 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/materialOverride.d.ts +6 -0
- package/dist/materialOverride.js +28 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +20 -3
- package/dist/pointer/pointerService.d.ts +5 -0
- package/dist/pointer/pointerService.js +51 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +23 -0
- package/dist/render/modelRender.js +106 -0
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +9 -0
- package/dist/terrain/terrainMath.js +17 -2
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +34 -0
- package/dist/touch/TouchControlsOverlay.js +279 -0
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +77 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +8 -1
- package/dist/world/WorldHud.js +90 -39
- package/dist/world/WorldItems.js +21 -2
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +51 -0
- package/llms.txt +2643 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useLoader, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
const DEFAULT_CAPACITY = 1024;
|
|
6
|
+
const DEFAULT_ALPHA_TEST = 0.08;
|
|
7
|
+
const XZ_BASE_QUATERNION = new THREE.Quaternion().setFromEuler(new THREE.Euler(-Math.PI / 2, 0, 0));
|
|
8
|
+
const LOCAL_NORMAL_AXIS = new THREE.Vector3(0, 0, 1);
|
|
9
|
+
/**
|
|
10
|
+
* Renders a sprite sheet / tile atlas as a single InstancedMesh — one draw call for the whole
|
|
11
|
+
* batch. Each instance picks its atlas frame via a per-instance UV offset attribute patched into
|
|
12
|
+
* the material's vertex shader, so platformer/puzzle-grid presentation never needs one draw call
|
|
13
|
+
* per sprite. Transforms and UV offsets are written directly into the mesh's typed arrays each
|
|
14
|
+
* frame from a plain instance list (bodies never touch the per-entity React path).
|
|
15
|
+
*/
|
|
16
|
+
export function SpriteBatch({ url, columns = 1, rows = 1, capacity = DEFAULT_CAPACITY, instances, plane = "xy", billboard = false, pixelated = true, alphaTest = DEFAULT_ALPHA_TEST, opacity = 1, }) {
|
|
17
|
+
const meshRef = useRef(null);
|
|
18
|
+
const { camera } = useThree();
|
|
19
|
+
const texture = useLoader(THREE.TextureLoader, url);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
22
|
+
if (pixelated) {
|
|
23
|
+
texture.magFilter = THREE.NearestFilter;
|
|
24
|
+
texture.minFilter = THREE.NearestFilter;
|
|
25
|
+
texture.anisotropy = 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
texture.anisotropy = 4;
|
|
29
|
+
}
|
|
30
|
+
texture.needsUpdate = true;
|
|
31
|
+
}, [texture, pixelated]);
|
|
32
|
+
const geometry = useMemo(() => new THREE.PlaneGeometry(1, 1), []);
|
|
33
|
+
const spriteUvOffset = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(capacity * 2), 2), [capacity]);
|
|
34
|
+
const material = useMemo(() => {
|
|
35
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
36
|
+
map: texture,
|
|
37
|
+
transparent: true,
|
|
38
|
+
alphaTest,
|
|
39
|
+
depthWrite: false,
|
|
40
|
+
opacity,
|
|
41
|
+
});
|
|
42
|
+
mat.onBeforeCompile = (shader) => {
|
|
43
|
+
shader.uniforms.spriteUvScale = { value: new THREE.Vector2(1 / columns, 1 / rows) };
|
|
44
|
+
shader.vertexShader = shader.vertexShader
|
|
45
|
+
.replace("#include <common>", `#include <common>
|
|
46
|
+
attribute vec2 spriteUvOffset;
|
|
47
|
+
uniform vec2 spriteUvScale;`)
|
|
48
|
+
.replace("#include <uv_vertex>", `#include <uv_vertex>
|
|
49
|
+
#ifdef USE_MAP
|
|
50
|
+
vMapUv = vMapUv * spriteUvScale + spriteUvOffset;
|
|
51
|
+
#endif`);
|
|
52
|
+
};
|
|
53
|
+
mat.customProgramCacheKey = () => `jgengine-sprite-batch-${columns}x${rows}`;
|
|
54
|
+
return mat;
|
|
55
|
+
}, [texture, columns, rows, alphaTest, opacity]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const mesh = meshRef.current;
|
|
58
|
+
if (mesh === null)
|
|
59
|
+
return;
|
|
60
|
+
mesh.geometry.setAttribute("spriteUvOffset", spriteUvOffset);
|
|
61
|
+
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
62
|
+
spriteUvOffset.setUsage(THREE.DynamicDrawUsage);
|
|
63
|
+
}, [spriteUvOffset]);
|
|
64
|
+
const baseQuaternion = useMemo(() => (plane === "xz" ? XZ_BASE_QUATERNION.clone() : new THREE.Quaternion()), [plane]);
|
|
65
|
+
const position = useMemo(() => new THREE.Vector3(), []);
|
|
66
|
+
const quaternion = useMemo(() => new THREE.Quaternion(), []);
|
|
67
|
+
const rotationQuaternion = useMemo(() => new THREE.Quaternion(), []);
|
|
68
|
+
const scaleVector = useMemo(() => new THREE.Vector3(), []);
|
|
69
|
+
const matrix = useMemo(() => new THREE.Matrix4(), []);
|
|
70
|
+
useFrame(() => {
|
|
71
|
+
const mesh = meshRef.current;
|
|
72
|
+
if (mesh === null)
|
|
73
|
+
return;
|
|
74
|
+
const list = instances();
|
|
75
|
+
const count = Math.min(list.length, capacity);
|
|
76
|
+
mesh.count = count;
|
|
77
|
+
const matrixArray = mesh.instanceMatrix.array;
|
|
78
|
+
const uvArray = spriteUvOffset.array;
|
|
79
|
+
for (let i = 0; i < count; i += 1) {
|
|
80
|
+
const instance = list[i];
|
|
81
|
+
position.set(instance.x, instance.y, instance.z ?? 0);
|
|
82
|
+
if (billboard) {
|
|
83
|
+
quaternion.copy(camera.quaternion);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
quaternion.copy(baseQuaternion);
|
|
87
|
+
}
|
|
88
|
+
const rotation = instance.rotation ?? 0;
|
|
89
|
+
if (rotation !== 0) {
|
|
90
|
+
rotationQuaternion.setFromAxisAngle(LOCAL_NORMAL_AXIS, rotation);
|
|
91
|
+
quaternion.multiply(rotationQuaternion);
|
|
92
|
+
}
|
|
93
|
+
const scale = instance.scale ?? 1;
|
|
94
|
+
scaleVector.set(scale, scale, scale);
|
|
95
|
+
matrix.compose(position, quaternion, scaleVector);
|
|
96
|
+
matrix.toArray(matrixArray, i * 16);
|
|
97
|
+
const frame = instance.frame ?? 0;
|
|
98
|
+
const col = frame % columns;
|
|
99
|
+
const row = Math.floor(frame / columns);
|
|
100
|
+
const uo = i * 2;
|
|
101
|
+
uvArray[uo] = col / columns;
|
|
102
|
+
uvArray[uo + 1] = 1 - (row + 1) / rows;
|
|
103
|
+
}
|
|
104
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
105
|
+
spriteUvOffset.needsUpdate = true;
|
|
106
|
+
});
|
|
107
|
+
useEffect(() => () => {
|
|
108
|
+
geometry.dispose();
|
|
109
|
+
material.dispose();
|
|
110
|
+
}, [geometry, material]);
|
|
111
|
+
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, capacity], frustumCulled: false, castShadow: false, receiveShadow: false }));
|
|
112
|
+
}
|
package/dist/world/WorldHud.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
2
|
+
import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
export type { WorldBarSample } from "./worldBarSamples.js";
|
|
4
|
+
export { collectWorldBarSamples, paintWorldBarSamples } from "./worldBarSamples.js";
|
|
5
|
+
export { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
6
|
+
export declare function WorldEntityBars({ statId, height, roles, resolveRole, }: {
|
|
2
7
|
statId: string;
|
|
3
8
|
height?: number;
|
|
9
|
+
roles?: readonly CatalogEntityRole[];
|
|
10
|
+
resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
|
|
4
11
|
}): import("react").JSX.Element;
|
|
5
12
|
export declare function WorldFloatText({ height, lifeMs }: {
|
|
6
13
|
height?: number;
|
package/dist/world/WorldHud.js
CHANGED
|
@@ -4,22 +4,42 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import * as THREE from "three";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
|
-
import {
|
|
7
|
+
import { useCameraShake } from "../camera/shakeChannel.js";
|
|
8
8
|
import { resolveFloatTextStyle } from "./floatTextStyle.js";
|
|
9
|
+
import { collectWorldBarSamples, paintWorldBarSamples, } from "./worldBarSamples.js";
|
|
10
|
+
import { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
11
|
+
export { collectWorldBarSamples, paintWorldBarSamples } from "./worldBarSamples.js";
|
|
12
|
+
export { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
9
13
|
const MUZZLE_HEIGHT = 1.4;
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
if (stat === null)
|
|
13
|
-
return null;
|
|
14
|
-
const range = stat.max - stat.min;
|
|
15
|
-
const percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
16
|
-
return (_jsx(Html, { position: [entity.position[0], entity.position[1] + height, entity.position[2]], center: true, distanceFactor: 12, zIndexRange: [20, 0], children: _jsx("div", { className: "h-2.5 w-28 overflow-hidden rounded-sm border border-black/70 bg-black/70 shadow", children: _jsx("div", { className: "h-full bg-gradient-to-r from-rose-600 to-red-400", style: { width: `${percent * 100}%` } }) }) }));
|
|
14
|
+
function pinOverlayToViewport(_el, _camera, size) {
|
|
15
|
+
return [size.width / 2, size.height / 2];
|
|
17
16
|
}
|
|
18
|
-
export function WorldEntityBars({ statId, height = 2.2 }) {
|
|
17
|
+
export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, }) {
|
|
19
18
|
const ctx = useGameContext();
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
19
|
+
const camera = useThree((state) => state.camera);
|
|
20
|
+
const size = useThree((state) => state.size);
|
|
21
|
+
const gl = useThree((state) => state.gl);
|
|
22
|
+
const canvasRef = useRef(null);
|
|
23
|
+
const samplesRef = useRef([]);
|
|
24
|
+
const projectRef = useRef(new THREE.Vector3());
|
|
25
|
+
useFrame(() => {
|
|
26
|
+
const canvas = canvasRef.current;
|
|
27
|
+
if (canvas === null)
|
|
28
|
+
return;
|
|
29
|
+
const dpr = Math.min(2, gl.getPixelRatio());
|
|
30
|
+
const cssW = size.width;
|
|
31
|
+
const cssH = size.height;
|
|
32
|
+
const pixelW = Math.max(1, Math.floor(cssW * dpr));
|
|
33
|
+
const pixelH = Math.max(1, Math.floor(cssH * dpr));
|
|
34
|
+
if (canvas.width !== pixelW || canvas.height !== pixelH) {
|
|
35
|
+
canvas.width = pixelW;
|
|
36
|
+
canvas.height = pixelH;
|
|
37
|
+
}
|
|
38
|
+
camera.updateMatrixWorld();
|
|
39
|
+
collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, { width: cssW, height: cssH }, samplesRef.current, projectRef.current);
|
|
40
|
+
paintWorldBarSamples(canvas, samplesRef.current, dpr);
|
|
41
|
+
});
|
|
42
|
+
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" } }) }));
|
|
23
43
|
}
|
|
24
44
|
function FloatTextItem({ event, lifeMs }) {
|
|
25
45
|
const [shown, setShown] = useState(false);
|
|
@@ -43,12 +63,24 @@ export function WorldFloatText({ height = 1.9, lifeMs = 950 }) {
|
|
|
43
63
|
const ctx = useGameContext();
|
|
44
64
|
const [floaters, setFloaters] = useState([]);
|
|
45
65
|
const nextId = useRef(0);
|
|
66
|
+
const timers = useRef(new Set());
|
|
46
67
|
useEffect(() => {
|
|
47
|
-
|
|
68
|
+
const pending = timers.current;
|
|
69
|
+
const off = ctx.game.events.on("entity.floatText", (event) => {
|
|
48
70
|
const id = nextId.current++;
|
|
49
71
|
setFloaters((current) => [...current, { id, position: event.position, event }]);
|
|
50
|
-
|
|
72
|
+
const timer = window.setTimeout(() => {
|
|
73
|
+
pending.delete(timer);
|
|
74
|
+
setFloaters((current) => current.filter((floater) => floater.id !== id));
|
|
75
|
+
}, lifeMs);
|
|
76
|
+
pending.add(timer);
|
|
51
77
|
});
|
|
78
|
+
return () => {
|
|
79
|
+
off();
|
|
80
|
+
for (const timer of pending)
|
|
81
|
+
window.clearTimeout(timer);
|
|
82
|
+
pending.clear();
|
|
83
|
+
};
|
|
52
84
|
}, [ctx, lifeMs]);
|
|
53
85
|
return (_jsx(_Fragment, { children: floaters.map((floater) => (_jsx(Html, { position: [floater.position[0], floater.position[1] + height, floater.position[2]], center: true, distanceFactor: 12, zIndexRange: [30, 0], children: _jsx(FloatTextItem, { event: floater.event, lifeMs: lifeMs }) }, floater.id))) }));
|
|
54
86
|
}
|
|
@@ -61,60 +93,69 @@ function TelegraphGeometry({ shape }) {
|
|
|
61
93
|
return _jsx("circleGeometry", { args: [shape.radius, 40, -shape.angle / 2, shape.angle] });
|
|
62
94
|
return _jsx("planeGeometry", { args: [shape.width, shape.length] });
|
|
63
95
|
}
|
|
64
|
-
function TelegraphDecal({ active
|
|
65
|
-
const
|
|
96
|
+
function TelegraphDecal({ active }) {
|
|
97
|
+
const materialRef = useRef(null);
|
|
66
98
|
const [x, y, z] = active.event.position;
|
|
67
99
|
const dir = active.event.dir ?? 0;
|
|
68
100
|
const shape = active.event.shape;
|
|
69
101
|
const forwardOffset = shape.kind === "line" ? shape.length / 2 : 0;
|
|
70
102
|
const color = active.event.kind === "danger" ? "#ef4444" : "#f59e0b";
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
useFrame(() => {
|
|
104
|
+
const material = materialRef.current;
|
|
105
|
+
if (material === null)
|
|
106
|
+
return;
|
|
107
|
+
material.opacity = telegraphPulseOpacity(active.bornMs, active.event.windupMs, performance.now());
|
|
108
|
+
});
|
|
109
|
+
return (_jsxs("mesh", { position: [x + Math.sin(dir) * forwardOffset, y + 0.06, z + Math.cos(dir) * forwardOffset], rotation: [-Math.PI / 2, 0, shape.kind === "line" || shape.kind === "cone" ? -dir : 0], renderOrder: 999, children: [_jsx(TelegraphGeometry, { shape: shape }), _jsx("meshBasicMaterial", { ref: materialRef, color: color, transparent: true, opacity: 0.45, side: THREE.DoubleSide, depthWrite: false, depthTest: false, toneMapped: false })] }));
|
|
73
110
|
}
|
|
74
111
|
export function WorldTelegraphs() {
|
|
75
112
|
const ctx = useGameContext();
|
|
76
113
|
const [telegraphs, setTelegraphs] = useState([]);
|
|
77
114
|
const key = useRef(0);
|
|
78
|
-
const
|
|
79
|
-
useFrame(() => setNowMs(performance.now()));
|
|
115
|
+
const timers = useRef(new Set());
|
|
80
116
|
useEffect(() => {
|
|
81
|
-
|
|
117
|
+
const pending = timers.current;
|
|
118
|
+
const off = ctx.game.events.on("combat.telegraph", (event) => {
|
|
82
119
|
const entry = { key: key.current++, event, bornMs: performance.now() };
|
|
83
120
|
setTelegraphs((current) => [...current, entry]);
|
|
84
|
-
|
|
121
|
+
const timer = window.setTimeout(() => {
|
|
122
|
+
pending.delete(timer);
|
|
123
|
+
setTelegraphs((current) => current.filter((t) => t.key !== entry.key));
|
|
124
|
+
}, event.windupMs + 120);
|
|
125
|
+
pending.add(timer);
|
|
85
126
|
});
|
|
127
|
+
const offCancel = ctx.game.events.on("combat.telegraphCancelled", (event) => {
|
|
128
|
+
setTelegraphs((current) => current.filter((t) => t.event.id !== event.id));
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
off();
|
|
132
|
+
offCancel();
|
|
133
|
+
for (const timer of pending)
|
|
134
|
+
window.clearTimeout(timer);
|
|
135
|
+
pending.clear();
|
|
136
|
+
};
|
|
86
137
|
}, [ctx]);
|
|
87
|
-
return (_jsx(_Fragment, { children: telegraphs.map((active) => (_jsx(TelegraphDecal, { active: active
|
|
138
|
+
return (_jsx(_Fragment, { children: telegraphs.map((active) => (_jsx(TelegraphDecal, { active: active }, active.key))) }));
|
|
88
139
|
}
|
|
89
140
|
export function CombatCameraShake() {
|
|
90
141
|
const ctx = useGameContext();
|
|
91
|
-
const
|
|
92
|
-
const trauma = useRef(0);
|
|
93
|
-
const decay = useRef(4);
|
|
142
|
+
const shake = useCameraShake();
|
|
94
143
|
useEffect(() => {
|
|
95
144
|
return ctx.game.events.on("combat.hitReaction", (event) => {
|
|
96
145
|
if (event.shake === undefined)
|
|
97
146
|
return;
|
|
98
|
-
|
|
99
|
-
decay.current = event.shake.decay;
|
|
147
|
+
shake.shake(event.shake.amplitude, event.shake.decay);
|
|
100
148
|
});
|
|
101
|
-
}, [ctx]);
|
|
102
|
-
useFrame((_state, dt) => {
|
|
103
|
-
if (trauma.current <= 0.0001)
|
|
104
|
-
return;
|
|
105
|
-
const magnitude = trauma.current * trauma.current;
|
|
106
|
-
camera.position.x += (Math.random() * 2 - 1) * magnitude * 0.4;
|
|
107
|
-
camera.position.y += (Math.random() * 2 - 1) * magnitude * 0.4;
|
|
108
|
-
trauma.current = Math.max(0, trauma.current - decay.current * dt);
|
|
109
|
-
});
|
|
149
|
+
}, [ctx, shake]);
|
|
110
150
|
return null;
|
|
111
151
|
}
|
|
112
152
|
export function ProjectileTracers({ lifeMs = 130 }) {
|
|
113
153
|
const ctx = useGameContext();
|
|
114
154
|
const [tracers, setTracers] = useState([]);
|
|
115
155
|
const nextId = useRef(0);
|
|
156
|
+
const timers = useRef(new Set());
|
|
116
157
|
useEffect(() => {
|
|
117
|
-
|
|
158
|
+
const unsub = ctx.game.events.on("projectile.settled", (event) => {
|
|
118
159
|
const id = nextId.current++;
|
|
119
160
|
setTracers((current) => [
|
|
120
161
|
...current,
|
|
@@ -126,8 +167,18 @@ export function ProjectileTracers({ lifeMs = 130 }) {
|
|
|
126
167
|
],
|
|
127
168
|
},
|
|
128
169
|
]);
|
|
129
|
-
|
|
170
|
+
const handle = setTimeout(() => {
|
|
171
|
+
timers.current.delete(handle);
|
|
172
|
+
setTracers((current) => current.filter((tracer) => tracer.id !== id));
|
|
173
|
+
}, lifeMs);
|
|
174
|
+
timers.current.add(handle);
|
|
130
175
|
});
|
|
176
|
+
return () => {
|
|
177
|
+
unsub();
|
|
178
|
+
for (const handle of timers.current)
|
|
179
|
+
clearTimeout(handle);
|
|
180
|
+
timers.current.clear();
|
|
181
|
+
};
|
|
131
182
|
}, [ctx, lifeMs]);
|
|
132
183
|
return (_jsx(_Fragment, { children: tracers.map((tracer) => (_jsx(Line, { points: tracer.points, color: "#fde68a", lineWidth: 2, transparent: true, opacity: 0.85 }, tracer.id))) }));
|
|
133
184
|
}
|
package/dist/world/WorldItems.js
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Html, Line } from "@react-three/drei";
|
|
3
|
-
import { useMemo } from "react";
|
|
3
|
+
import { useMemo, useRef } from "react";
|
|
4
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
5
|
import { resolveWorldItemPresentation } from "@jgengine/core/game/worldItem";
|
|
5
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
7
|
import { useWorldItems } from "@jgengine/react/hooks";
|
|
7
8
|
import { POINTER_ENTITY_KEY } from "../pointer/pointerService.js";
|
|
8
9
|
const DEFAULT_BEAM_HEIGHT = 2.5;
|
|
9
10
|
const FALLBACK_COLOR = "#e5e7eb";
|
|
11
|
+
const REST_HEIGHT = 0.32;
|
|
12
|
+
const SPAWN_DURATION = 0.4;
|
|
10
13
|
function WorldItemMarker({ instanceId, position, color, beam, label, beamHeight, }) {
|
|
11
14
|
const beamPoints = useMemo(() => [
|
|
12
15
|
[0, 0, 0],
|
|
13
16
|
[0, beamHeight, 0],
|
|
14
17
|
], [beamHeight]);
|
|
15
|
-
|
|
18
|
+
// Drop animation: the gem leaps up on spawn, settles, then idles with a slow
|
|
19
|
+
// spin + hover so a fresh drop reads as "something just fell here". Age is
|
|
20
|
+
// accumulated from frame deltas (Date.now is unavailable to keep runs pure).
|
|
21
|
+
const gemRef = useRef(null);
|
|
22
|
+
const ageRef = useRef(0);
|
|
23
|
+
useFrame((_state, delta) => {
|
|
24
|
+
const gem = gemRef.current;
|
|
25
|
+
if (gem === null)
|
|
26
|
+
return;
|
|
27
|
+
const age = ageRef.current + delta;
|
|
28
|
+
ageRef.current = age;
|
|
29
|
+
const spawn = age < SPAWN_DURATION ? Math.sin((age / SPAWN_DURATION) * Math.PI) * 0.35 : 0;
|
|
30
|
+
const bob = Math.sin(age * 2.2) * 0.06;
|
|
31
|
+
gem.position.y = REST_HEIGHT + bob + spawn;
|
|
32
|
+
gem.rotation.y = age * 1.6;
|
|
33
|
+
});
|
|
34
|
+
return (_jsxs("group", { position: [position[0], position[1], position[2]], userData: { [POINTER_ENTITY_KEY]: instanceId }, children: [_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.26, 0.4, 24] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85 })] }), _jsx("group", { ref: gemRef, "position-y": REST_HEIGHT, children: _jsxs("mesh", { children: [_jsx("octahedronGeometry", { args: [0.22, 0] }), _jsx("meshStandardMaterial", { color: color, emissive: color, emissiveIntensity: 0.6 })] }) }), beam ? _jsx(Line, { points: beamPoints, color: color, lineWidth: 2, transparent: true, opacity: 0.55 }) : null, label !== undefined && label.length > 0 ? (_jsx(Html, { position: [0, beamHeight + 0.2, 0], center: true, distanceFactor: 12, zIndexRange: [15, 0], children: _jsx("div", { className: "whitespace-nowrap rounded-sm border px-1.5 py-0.5 text-[11px] font-bold shadow", style: { borderColor: color, color, backgroundColor: "rgba(10,10,14,0.75)" }, children: label }) })) : null] }));
|
|
16
35
|
}
|
|
17
36
|
/** Rarity→beam/color/label render binding + loot-filter overlay (#32/#33) for every dropped `worldItem`. */
|
|
18
37
|
export function WorldItems({ config }) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PoseWritable {
|
|
2
|
+
position: {
|
|
3
|
+
set(x: number, y: number, z: number): void;
|
|
4
|
+
};
|
|
5
|
+
rotation: {
|
|
6
|
+
y: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface PoseSource {
|
|
10
|
+
position: readonly [number, number, number];
|
|
11
|
+
rotationY: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function writeEntityPose(target: PoseWritable, source: PoseSource): void;
|
|
14
|
+
export declare function posesEqual(a: PoseSource, b: PoseSource): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function writeEntityPose(target, source) {
|
|
2
|
+
target.position.set(source.position[0], source.position[1], source.position[2]);
|
|
3
|
+
target.rotation.y = source.rotationY;
|
|
4
|
+
}
|
|
5
|
+
export function posesEqual(a, b) {
|
|
6
|
+
return (a.position[0] === b.position[0] &&
|
|
7
|
+
a.position[1] === b.position[1] &&
|
|
8
|
+
a.position[2] === b.position[2] &&
|
|
9
|
+
a.rotationY === b.rotationY);
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function telegraphPulseOpacity(bornMs: number, windupMs: number, nowMs: number): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
2
|
+
import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
4
|
+
export interface WorldBarSample {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
percent: number;
|
|
8
|
+
}
|
|
9
|
+
export interface Projectable {
|
|
10
|
+
set(x: number, y: number, z: number): this;
|
|
11
|
+
project(camera: {
|
|
12
|
+
matrixWorldInverse: unknown;
|
|
13
|
+
projectionMatrix: unknown;
|
|
14
|
+
}): this;
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
z: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: {
|
|
20
|
+
matrixWorldInverse: unknown;
|
|
21
|
+
projectionMatrix: unknown;
|
|
22
|
+
}, viewport: {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}, into: WorldBarSample[], project: Projectable): number;
|
|
26
|
+
export declare function paintWorldBarSamples(canvas: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
getContext(kind: "2d"): CanvasRenderingContext2D | null;
|
|
30
|
+
}, samples: readonly WorldBarSample[], dpr: number, barWidthPx?: number, barHeightPx?: number): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { worldHealthBarAllowsRole } from "@jgengine/core/game/playableGame";
|
|
2
|
+
export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project) {
|
|
3
|
+
into.length = 0;
|
|
4
|
+
const playerId = ctx.player.userId;
|
|
5
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
6
|
+
if (entity.id === playerId)
|
|
7
|
+
continue;
|
|
8
|
+
if (!worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
|
|
9
|
+
continue;
|
|
10
|
+
const stat = ctx.scene.entity.stats.get(entity.id, statId);
|
|
11
|
+
if (stat === null)
|
|
12
|
+
continue;
|
|
13
|
+
const range = stat.max - stat.min;
|
|
14
|
+
const percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
15
|
+
project.set(entity.position[0], entity.position[1] + height, entity.position[2]);
|
|
16
|
+
project.project(camera);
|
|
17
|
+
if (project.z < -1 || project.z > 1)
|
|
18
|
+
continue;
|
|
19
|
+
const x = (project.x * 0.5 + 0.5) * viewport.width;
|
|
20
|
+
const y = (-project.y * 0.5 + 0.5) * viewport.height;
|
|
21
|
+
into.push({ x, y, percent });
|
|
22
|
+
}
|
|
23
|
+
return into.length;
|
|
24
|
+
}
|
|
25
|
+
export function paintWorldBarSamples(canvas, samples, dpr, barWidthPx = 112, barHeightPx = 10) {
|
|
26
|
+
const g = canvas.getContext("2d");
|
|
27
|
+
if (g === null)
|
|
28
|
+
return;
|
|
29
|
+
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
30
|
+
g.clearRect(0, 0, canvas.width, canvas.height);
|
|
31
|
+
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32
|
+
const halfW = barWidthPx / 2;
|
|
33
|
+
const halfH = barHeightPx / 2;
|
|
34
|
+
for (const sample of samples) {
|
|
35
|
+
const left = sample.x - halfW;
|
|
36
|
+
const top = sample.y - halfH;
|
|
37
|
+
g.fillStyle = "rgba(0,0,0,0.7)";
|
|
38
|
+
g.strokeStyle = "rgba(0,0,0,0.7)";
|
|
39
|
+
g.lineWidth = 1;
|
|
40
|
+
g.fillRect(left, top, barWidthPx, barHeightPx);
|
|
41
|
+
g.strokeRect(left + 0.5, top + 0.5, barWidthPx - 1, barHeightPx - 1);
|
|
42
|
+
const fill = Math.max(0, Math.min(barWidthPx, sample.percent * barWidthPx));
|
|
43
|
+
if (fill > 0) {
|
|
44
|
+
const gradient = g.createLinearGradient(left, top, left + barWidthPx, top);
|
|
45
|
+
gradient.addColorStop(0, "#e11d48");
|
|
46
|
+
gradient.addColorStop(1, "#f87171");
|
|
47
|
+
g.fillStyle = gradient;
|
|
48
|
+
g.fillRect(left, top, fill, barHeightPx);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|