@jgengine/shell 0.5.0 → 0.7.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 +191 -2
- package/README.md +27 -0
- package/dist/GamePlayerShell.js +224 -43
- package/dist/audio/AudioComponents.d.ts +12 -0
- package/dist/audio/AudioComponents.js +53 -0
- package/dist/audio/audioEngine.d.ts +23 -0
- package/dist/audio/audioEngine.js +110 -0
- package/dist/camera/GameCameraRig.d.ts +11 -0
- package/dist/camera/GameCameraRig.js +42 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +22 -0
- package/dist/camera/cameraRigs.js +485 -0
- package/dist/camera/index.d.ts +5 -0
- package/dist/camera/index.js +5 -0
- package/dist/camera/rigMath.d.ts +130 -0
- package/dist/camera/rigMath.js +338 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/demo/builderDemo.d.ts +2 -0
- package/dist/demo/builderDemo.js +189 -0
- package/dist/demo/demoGame.js +10 -2
- package/dist/demo/environmentShowcase.d.ts +2 -0
- package/dist/demo/environmentShowcase.js +15 -0
- package/dist/demo/mapDemo.d.ts +2 -0
- package/dist/demo/mapDemo.js +206 -0
- package/dist/demo/pointerDemo.d.ts +2 -0
- package/dist/demo/pointerDemo.js +151 -0
- package/dist/demo/sensorShowcase.d.ts +2 -0
- package/dist/demo/sensorShowcase.js +131 -0
- package/dist/demo/survivalDemo.d.ts +2 -0
- package/dist/demo/survivalDemo.js +291 -0
- package/dist/environment/EnvironmentScene.d.ts +5 -0
- package/dist/environment/EnvironmentScene.js +75 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment.d.ts +1 -0
- package/dist/environment.js +1 -0
- package/dist/map/MapMarkerBeacons.d.ts +12 -0
- package/dist/map/MapMarkerBeacons.js +16 -0
- package/dist/map/index.d.ts +2 -0
- package/dist/map/index.js +2 -0
- package/dist/map/terrainMap.d.ts +23 -0
- package/dist/map/terrainMap.js +79 -0
- package/dist/pointer/PointerOverlays.d.ts +12 -0
- package/dist/pointer/PointerOverlays.js +17 -0
- package/dist/pointer/PointerProbe.d.ts +4 -0
- package/dist/pointer/PointerProbe.js +27 -0
- package/dist/pointer/pointerService.d.ts +24 -0
- package/dist/pointer/pointerService.js +68 -0
- package/dist/registry.d.ts +4 -2
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/GeneratedBuilding.d.ts +44 -0
- package/dist/structures/GeneratedBuilding.js +86 -0
- package/dist/structures/PlacementGhost.d.ts +8 -0
- package/dist/structures/PlacementGhost.js +11 -0
- package/dist/structures/index.d.ts +2 -0
- package/dist/structures/index.js +2 -0
- package/dist/structures.d.ts +1 -0
- package/dist/structures.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +22 -0
- package/dist/terrain/CarvedTerrain.js +19 -0
- package/dist/terrain/EditableGround.d.ts +11 -0
- package/dist/terrain/EditableGround.js +38 -0
- package/dist/terrain/GrassField.d.ts +21 -0
- package/dist/terrain/GrassField.js +31 -0
- package/dist/terrain/ProceduralGround.d.ts +9 -0
- package/dist/terrain/ProceduralGround.js +17 -0
- package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/grassGeometry.d.ts +27 -0
- package/dist/terrain/grassGeometry.js +77 -0
- package/dist/terrain/grassMaterial.d.ts +33 -0
- package/dist/terrain/grassMaterial.js +108 -0
- package/dist/terrain/index.d.ts +10 -0
- package/dist/terrain/index.js +10 -0
- package/dist/terrain/random.d.ts +4 -0
- package/dist/terrain/random.js +27 -0
- package/dist/terrain/terrainMath.d.ts +47 -0
- package/dist/terrain/terrainMath.js +172 -0
- package/dist/terrain.d.ts +1 -0
- package/dist/terrain.js +1 -0
- package/dist/vision/FrustumSensorHud.d.ts +21 -0
- package/dist/vision/FrustumSensorHud.js +67 -0
- package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
- package/dist/vision/HiddenStateProbeHud.js +11 -0
- package/dist/vision/RevealVision.d.ts +28 -0
- package/dist/vision/RevealVision.js +53 -0
- package/dist/water/Ocean.d.ts +6 -0
- package/dist/water/Ocean.js +30 -0
- package/dist/water/OceanConfig.d.ts +90 -0
- package/dist/water/OceanConfig.js +131 -0
- package/dist/water/OceanMaterial.d.ts +54 -0
- package/dist/water/OceanMaterial.js +51 -0
- package/dist/water/OceanShader.d.ts +2 -0
- package/dist/water/OceanShader.js +78 -0
- package/dist/water/index.d.ts +3 -0
- package/dist/water/index.js +3 -0
- package/dist/water.d.ts +1 -0
- package/dist/water.js +1 -0
- package/dist/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/LightningStrike.d.ts +17 -0
- package/dist/weather/LightningStrike.js +130 -0
- package/dist/weather/RainField.d.ts +20 -0
- package/dist/weather/RainField.js +107 -0
- package/dist/weather/SnowField.d.ts +19 -0
- package/dist/weather/SnowField.js +108 -0
- package/dist/weather/WeatherLayer.d.ts +17 -0
- package/dist/weather/WeatherLayer.js +16 -0
- package/dist/weather/index.d.ts +6 -0
- package/dist/weather/index.js +6 -0
- package/dist/weather/weatherGeometry.d.ts +2 -0
- package/dist/weather/weatherGeometry.js +14 -0
- package/dist/weather/weatherMath.d.ts +7 -0
- package/dist/weather/weatherMath.js +30 -0
- package/dist/weather/weatherUniforms.d.ts +18 -0
- package/dist/weather/weatherUniforms.js +34 -0
- package/dist/world/InstancedBodies.d.ts +17 -0
- package/dist/world/InstancedBodies.js +84 -0
- package/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +2 -0
- package/dist/world/WorldHud.js +70 -12
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +31 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/package.json +12 -4
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Canvas, useFrame, useLoader } from "@react-three/fiber";
|
|
3
|
-
import { Component, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Component, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
6
|
import { createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
7
|
+
import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
|
|
7
8
|
import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
9
|
+
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
10
|
+
import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
|
|
8
11
|
import { advancePlayerMotion, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, } from "@jgengine/core/movement/movementModel";
|
|
9
12
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
13
|
+
import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
10
14
|
import { useGameContext } from "@jgengine/react/provider";
|
|
11
15
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
12
16
|
import { GameProvider } from "@jgengine/react/provider";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
17
|
+
import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
|
|
18
|
+
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
19
|
+
import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, resolveRigKind } from "./camera/index.js";
|
|
20
|
+
import { PointerProbe } from "./pointer/PointerProbe.js";
|
|
21
|
+
import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
|
|
22
|
+
import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
|
|
23
|
+
import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
24
|
+
import { WorldItems } from "./world/WorldItems.js";
|
|
16
25
|
const DEV_USER_ID = "dev-player";
|
|
17
26
|
const TURN_SPEED = 2.4;
|
|
18
27
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
@@ -54,18 +63,38 @@ function hotbarIdFor(playable) {
|
|
|
54
63
|
const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
|
|
55
64
|
return (hud ?? declarations[0])?.[0] ?? null;
|
|
56
65
|
}
|
|
57
|
-
function executeHotbarSlot(ctx, hotbarId, slot, yaw, pitch) {
|
|
66
|
+
function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
|
|
58
67
|
const stack = ctx.player.inventory.state(hotbarId).slots[slot];
|
|
59
68
|
if (stack === undefined || stack === null)
|
|
60
69
|
return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
|
|
61
70
|
const result = ctx.item.use.use({
|
|
62
|
-
from:
|
|
71
|
+
from: fromId,
|
|
63
72
|
itemId: stack.itemId,
|
|
64
73
|
inventoryId: hotbarId,
|
|
65
|
-
aim: { yaw, pitch },
|
|
74
|
+
aim: aimOverride ?? { yaw, pitch },
|
|
66
75
|
});
|
|
67
76
|
return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
|
|
68
77
|
}
|
|
78
|
+
function pointerAimFor(ctx, service) {
|
|
79
|
+
const hit = service.worldHit();
|
|
80
|
+
if (hit === null)
|
|
81
|
+
return undefined;
|
|
82
|
+
const player = ctx.scene.entity.get(ctx.player.userId);
|
|
83
|
+
const origin = player === null ? hit.point : player.position;
|
|
84
|
+
return aimToPoint([origin[0], origin[1] + 1, origin[2]], hit.point);
|
|
85
|
+
}
|
|
86
|
+
function pointerContextMenu(ctx, playable, hit) {
|
|
87
|
+
if (hit.entity !== null) {
|
|
88
|
+
const entity = ctx.scene.entity.get(hit.entity);
|
|
89
|
+
const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
|
|
90
|
+
return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
|
|
91
|
+
}
|
|
92
|
+
if (hit.object !== null) {
|
|
93
|
+
const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
|
|
94
|
+
return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
69
98
|
function colorFromId(id) {
|
|
70
99
|
let hash = 0;
|
|
71
100
|
for (let index = 0; index < id.length; index += 1)
|
|
@@ -85,23 +114,31 @@ function EntityModel({ model }) {
|
|
|
85
114
|
const gltf = useLoader(GLTFLoader, model.url);
|
|
86
115
|
const scene = useMemo(() => gltf.scene.clone(true), [gltf]);
|
|
87
116
|
const scale = model.scale ?? 1;
|
|
88
|
-
|
|
117
|
+
const baseY = model.y ?? 0;
|
|
118
|
+
const dims = model.dims;
|
|
119
|
+
const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
|
|
120
|
+
const position = centered
|
|
121
|
+
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
122
|
+
: [0, baseY, 0];
|
|
123
|
+
return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
|
|
89
124
|
}
|
|
90
125
|
function resolveModel(value, assets) {
|
|
91
126
|
if (value === undefined)
|
|
92
127
|
return undefined;
|
|
93
128
|
if (typeof value !== "string")
|
|
94
129
|
return value;
|
|
95
|
-
const
|
|
96
|
-
|
|
130
|
+
const ref = assets.resolve(value);
|
|
131
|
+
if (ref === null)
|
|
132
|
+
return undefined;
|
|
133
|
+
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
97
134
|
}
|
|
98
|
-
function EntityMarker({ entity, model, sprite, isLocal, targeted, onSelect, }) {
|
|
135
|
+
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
99
136
|
const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
|
|
100
|
-
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, onPointerDown: (event) => {
|
|
137
|
+
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, userData: { [POINTER_ENTITY_KEY]: entity.id }, onPointerDown: (event) => {
|
|
101
138
|
event.stopPropagation();
|
|
102
139
|
if (!isLocal)
|
|
103
140
|
onSelect(entity);
|
|
104
|
-
}, children: [model !== undefined ? (_jsx(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _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] }));
|
|
141
|
+
}, 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(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _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] }));
|
|
105
142
|
}
|
|
106
143
|
function GroundPlane() {
|
|
107
144
|
const geometry = useMemo(() => {
|
|
@@ -134,26 +171,29 @@ function RockField() {
|
|
|
134
171
|
}), []);
|
|
135
172
|
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], children: [_jsx("dodecahedronGeometry", { args: [0.8, 0] }), _jsx("meshStandardMaterial", { color: "#6b6f63", roughness: 1 })] }, rock.id))) }));
|
|
136
173
|
}
|
|
137
|
-
function WorldView({ entitySprites, entityModels, objectModels, assets, }) {
|
|
174
|
+
function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, selectedIds, }) {
|
|
138
175
|
const ctx = useGameContext();
|
|
139
176
|
const entities = useSceneEntities();
|
|
140
177
|
const objects = useSceneObjects();
|
|
141
178
|
const player = usePlayer();
|
|
142
179
|
const targetId = useTarget(player.userId);
|
|
180
|
+
const controlledId = ctx.player.possession.active(player.userId);
|
|
143
181
|
const handleSelect = (entity) => {
|
|
144
182
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
145
|
-
ctx.scene.entity.setTarget(
|
|
183
|
+
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
146
184
|
};
|
|
147
|
-
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})
|
|
185
|
+
return (_jsxs(_Fragment, { children: [Environment !== undefined ? (_jsx(Environment, {})) : (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] })), entities
|
|
186
|
+
.filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
|
|
187
|
+
.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
|
|
148
188
|
const model = resolveModel(objectModels?.[object.catalogId], assets) ??
|
|
149
189
|
resolveModel(object.catalogId, assets);
|
|
150
|
-
return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, children: model !== undefined ? (_jsx(EntityModel, { model: model })) : (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: colorFromId(object.catalogId) })] })) }, object.instanceId));
|
|
190
|
+
return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, userData: { [POINTER_OBJECT_KEY]: object.instanceId }, children: model !== undefined ? (_jsx(EntityModel, { model: model })) : (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: colorFromId(object.catalogId) })] })) }, object.instanceId));
|
|
151
191
|
})] }));
|
|
152
192
|
}
|
|
153
193
|
function RemotePlayers({ rows }) {
|
|
154
194
|
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))) }));
|
|
155
195
|
}
|
|
156
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, }) {
|
|
196
|
+
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
|
|
157
197
|
const motionRef = useRef(createPlayerMotionState());
|
|
158
198
|
const hasReportedTickError = useRef(false);
|
|
159
199
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
@@ -161,11 +201,12 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
161
201
|
useFrame((_state, rawDt) => {
|
|
162
202
|
try {
|
|
163
203
|
const dt = Math.min(rawDt, 0.05);
|
|
204
|
+
const gameDt = ctx.time.advance(dt);
|
|
164
205
|
if (tracker.isDown("turnLeft"))
|
|
165
206
|
yawRef.current += TURN_SPEED * dt;
|
|
166
207
|
if (tracker.isDown("turnRight"))
|
|
167
208
|
yawRef.current -= TURN_SPEED * dt;
|
|
168
|
-
const playerId = ctx.player.userId;
|
|
209
|
+
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
169
210
|
const player = ctx.scene.entity.get(playerId);
|
|
170
211
|
const forwardX = Math.sin(yawRef.current);
|
|
171
212
|
const forwardZ = Math.cos(yawRef.current);
|
|
@@ -185,9 +226,10 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
185
226
|
rotationY: intent.moving
|
|
186
227
|
? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
|
|
187
228
|
: player.rotationY,
|
|
229
|
+
dt: rawDt,
|
|
188
230
|
});
|
|
189
231
|
}
|
|
190
|
-
playable.loop.onTick(ctx,
|
|
232
|
+
playable.loop.onTick(ctx, gameDt);
|
|
191
233
|
if (tracker.wasPressed("tabTarget")) {
|
|
192
234
|
if (ctx.game.commands.has("target.cycle"))
|
|
193
235
|
ctx.game.commands.run("target.cycle", {});
|
|
@@ -200,9 +242,22 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
200
242
|
else
|
|
201
243
|
ctx.scene.entity.setTarget(playerId, null);
|
|
202
244
|
}
|
|
245
|
+
if (pingCommand !== undefined && tracker.wasPressed("ping")) {
|
|
246
|
+
const hit = pointerService.worldHit();
|
|
247
|
+
if (hit !== null && ctx.game.commands.has(pingCommand)) {
|
|
248
|
+
ctx.game.commands.run(pingCommand, {
|
|
249
|
+
point: hit.point,
|
|
250
|
+
entity: hit.entity,
|
|
251
|
+
object: hit.object,
|
|
252
|
+
normal: hit.normal,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
203
256
|
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
204
257
|
if (!tracker.wasPressed(action))
|
|
205
258
|
continue;
|
|
259
|
+
if (action === "ping" && pingCommand !== undefined)
|
|
260
|
+
continue;
|
|
206
261
|
if (action === "interact") {
|
|
207
262
|
const prompts = playable.prompts?.(ctx);
|
|
208
263
|
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
@@ -219,10 +274,11 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
219
274
|
ctx.game.commands.run(command, {});
|
|
220
275
|
}
|
|
221
276
|
if (hotbarId !== null) {
|
|
277
|
+
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
222
278
|
for (const { action, slot } of slotActions) {
|
|
223
279
|
if (!tracker.wasPressed(action))
|
|
224
280
|
continue;
|
|
225
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
281
|
+
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
226
282
|
if (!result.ok)
|
|
227
283
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
228
284
|
}
|
|
@@ -235,7 +291,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
235
291
|
? preferred
|
|
236
292
|
: slots.findIndex((stack) => stack !== null);
|
|
237
293
|
if (slot >= 0) {
|
|
238
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
294
|
+
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
239
295
|
if (!result.ok)
|
|
240
296
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
241
297
|
}
|
|
@@ -296,7 +352,15 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
296
352
|
const cameraDraggingRef = useRef(false);
|
|
297
353
|
const primaryClickRef = useRef(false);
|
|
298
354
|
const pointerDownRef = useRef(null);
|
|
355
|
+
const marqueeStartRef = useRef(null);
|
|
356
|
+
const pointerService = useMemo(() => createPointerService(), []);
|
|
357
|
+
const selection = useMemo(() => createSelectionSet(), [playable]);
|
|
358
|
+
const [marquee, setMarquee] = useState(null);
|
|
359
|
+
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
360
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
299
361
|
const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(playable.game.input ?? {})), [playable]);
|
|
362
|
+
const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
|
|
363
|
+
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
300
364
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
301
365
|
const reportRuntimeError = (error, phase) => {
|
|
302
366
|
const diagnostic = logRuntimeError(error, phase);
|
|
@@ -388,39 +452,156 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
388
452
|
useEffect(() => {
|
|
389
453
|
wrapperRef.current?.focus();
|
|
390
454
|
}, [ctx]);
|
|
455
|
+
useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
|
|
391
456
|
if (ctx === null)
|
|
392
457
|
return _jsx("div", { className: "h-full w-full bg-neutral-950" });
|
|
393
458
|
const GameUI = playable.GameUI;
|
|
394
459
|
const WorldOverlay = playable.WorldOverlay;
|
|
395
|
-
const
|
|
396
|
-
const
|
|
460
|
+
const controlledEntityId = ctx.player.possession.active(userId);
|
|
461
|
+
const cameraConfig = playable.camera?.followEntityId !== undefined
|
|
462
|
+
? playable.camera
|
|
463
|
+
: { ...playable.camera, followEntityId: controlledEntityId };
|
|
464
|
+
const rigKind = resolveRigKind(cameraConfig);
|
|
465
|
+
const firstPerson = rigKind === "first";
|
|
466
|
+
const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
|
|
397
467
|
const worldBars = playable.worldHealthBars;
|
|
398
468
|
const barsStatId = worldBars === undefined || worldBars === false
|
|
399
469
|
? null
|
|
400
470
|
: worldBars === true
|
|
401
471
|
? "health"
|
|
402
472
|
: worldBars.statId ?? "health";
|
|
473
|
+
const pointer = playable.pointer;
|
|
474
|
+
const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
|
|
475
|
+
const selectFilter = pointer?.selectFilter;
|
|
476
|
+
const localXY = (event) => {
|
|
477
|
+
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
478
|
+
return rect === undefined
|
|
479
|
+
? { x: event.clientX, y: event.clientY }
|
|
480
|
+
: { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
481
|
+
};
|
|
482
|
+
const commitSelection = (ids) => {
|
|
483
|
+
selection.replace(ids);
|
|
484
|
+
setSelectedIds(new Set(ids));
|
|
485
|
+
};
|
|
486
|
+
const finishMarquee = (rect) => {
|
|
487
|
+
const candidates = [];
|
|
488
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
489
|
+
if (selectFilter !== undefined && !selectFilter(entity.id))
|
|
490
|
+
continue;
|
|
491
|
+
const screen = pointerService.screenOf(entity.position);
|
|
492
|
+
if (screen !== null)
|
|
493
|
+
candidates.push({ id: entity.id, x: screen.x, y: screen.y });
|
|
494
|
+
}
|
|
495
|
+
commitSelection(selectWithinRect(candidates, rect));
|
|
496
|
+
};
|
|
497
|
+
const handlePointerDown = (event) => {
|
|
498
|
+
wrapperRef.current?.focus();
|
|
499
|
+
audioEngine.resume();
|
|
500
|
+
if (contextMenu !== null)
|
|
501
|
+
setContextMenu(null);
|
|
502
|
+
if (event.button === 0) {
|
|
503
|
+
const point = localXY(event);
|
|
504
|
+
pointerDownRef.current = point;
|
|
505
|
+
if (pointer?.select === true)
|
|
506
|
+
marqueeStartRef.current = point;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const handlePointerMove = (event) => {
|
|
510
|
+
if (pointer?.select !== true)
|
|
511
|
+
return;
|
|
512
|
+
const start = marqueeStartRef.current;
|
|
513
|
+
if (start === null || (event.buttons & 1) === 0)
|
|
514
|
+
return;
|
|
515
|
+
const point = localXY(event);
|
|
516
|
+
setMarquee(screenRect(start.x, start.y, point.x, point.y));
|
|
517
|
+
};
|
|
518
|
+
const handlePointerUp = (event) => {
|
|
519
|
+
if (event.button !== 0 || pointerDownRef.current === null)
|
|
520
|
+
return;
|
|
521
|
+
const start = pointerDownRef.current;
|
|
522
|
+
pointerDownRef.current = null;
|
|
523
|
+
marqueeStartRef.current = null;
|
|
524
|
+
const end = localXY(event);
|
|
525
|
+
const rect = screenRect(start.x, start.y, end.x, end.y);
|
|
526
|
+
const wasMarquee = marquee !== null && isMarquee(rect, PRIMARY_CLICK_MOVE_THRESHOLD_PX);
|
|
527
|
+
setMarquee(null);
|
|
528
|
+
if (pointer?.select === true && wasMarquee) {
|
|
529
|
+
finishMarquee(rect);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (cameraDraggingRef.current)
|
|
533
|
+
return;
|
|
534
|
+
if (pointer?.grabWorldItems === true) {
|
|
535
|
+
const hit = pointerService.worldHit();
|
|
536
|
+
const itemInstanceId = hit?.entity ?? null;
|
|
537
|
+
const record = itemInstanceId === null ? null : ctx.scene.worldItem.get(itemInstanceId);
|
|
538
|
+
if (record !== null && itemInstanceId !== null) {
|
|
539
|
+
const itemEntity = ctx.scene.entity.get(itemInstanceId);
|
|
540
|
+
const localPlayer = ctx.scene.entity.get(ctx.player.userId);
|
|
541
|
+
const pickupRadius = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
|
|
542
|
+
const withinRadius = itemEntity !== null &&
|
|
543
|
+
localPlayer !== null &&
|
|
544
|
+
Math.hypot(localPlayer.position[0] - itemEntity.position[0], localPlayer.position[1] - itemEntity.position[1], localPlayer.position[2] - itemEntity.position[2]) <= pickupRadius;
|
|
545
|
+
if (withinRadius) {
|
|
546
|
+
ctx.scene.worldItem.pickup(itemInstanceId, ctx.player.userId);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (pointer?.select === true) {
|
|
552
|
+
const hit = pointerService.worldHit();
|
|
553
|
+
if (hit !== null && hit.entity !== null && (selectFilter === undefined || selectFilter(hit.entity))) {
|
|
554
|
+
commitSelection([hit.entity]);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
commitSelection([]);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (pointer?.moveCommand !== undefined) {
|
|
562
|
+
const hit = pointerService.worldHit();
|
|
563
|
+
if (hit !== null && ctx.game.commands.has(pointer.moveCommand)) {
|
|
564
|
+
ctx.game.commands.run(pointer.moveCommand, { point: hit.point, entity: hit.entity, object: hit.object });
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
|
|
569
|
+
if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
|
|
570
|
+
primaryClickRef.current = true;
|
|
571
|
+
};
|
|
572
|
+
const handleContextMenu = (event) => {
|
|
573
|
+
if (pointer === undefined)
|
|
574
|
+
return;
|
|
575
|
+
event.preventDefault();
|
|
576
|
+
const hit = pointerService.worldHit();
|
|
577
|
+
if (hit === null)
|
|
578
|
+
return;
|
|
579
|
+
if (pointer.contextMenu === true && (hit.entity !== null || hit.object !== null)) {
|
|
580
|
+
const menu = pointerContextMenu(ctx, playable, hit);
|
|
581
|
+
if (menu !== null) {
|
|
582
|
+
const point = localXY(event);
|
|
583
|
+
setContextMenu({ menu, x: point.x, y: point.y });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (pointer.select === true && pointer.orderCommand !== undefined && selection.size() > 0) {
|
|
588
|
+
if (ctx.game.commands.has(pointer.orderCommand)) {
|
|
589
|
+
ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const handleVerbPick = (verb) => {
|
|
594
|
+
const state = contextMenu;
|
|
595
|
+
setContextMenu(null);
|
|
596
|
+
if (state !== null && ctx.game.commands.has(verb.command)) {
|
|
597
|
+
ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
|
|
598
|
+
}
|
|
599
|
+
};
|
|
403
600
|
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
404
601
|
if (event.code === "Tab" || event.code === "Space")
|
|
405
602
|
event.preventDefault();
|
|
406
603
|
tracker.handleDown(event.code);
|
|
407
|
-
}, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: (event) => {
|
|
408
|
-
wrapperRef.current?.focus();
|
|
409
|
-
if (event.button === 0) {
|
|
410
|
-
pointerDownRef.current = { x: event.clientX, y: event.clientY };
|
|
411
|
-
}
|
|
412
|
-
}, onPointerUp: (event) => {
|
|
413
|
-
if (event.button !== 0 || pointerDownRef.current === null)
|
|
414
|
-
return;
|
|
415
|
-
const start = pointerDownRef.current;
|
|
416
|
-
pointerDownRef.current = null;
|
|
417
|
-
const dx = event.clientX - start.x;
|
|
418
|
-
const dy = event.clientY - start.y;
|
|
419
|
-
const moved = dx * dx + dy * dy;
|
|
420
|
-
if (!cameraDraggingRef.current && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX) {
|
|
421
|
-
primaryClickRef.current = true;
|
|
422
|
-
}
|
|
423
|
-
}, onWheel: (event) => {
|
|
604
|
+
}, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onContextMenu: handleContextMenu, onWheel: (event) => {
|
|
424
605
|
if (ctx === null || !event.shiftKey)
|
|
425
606
|
return;
|
|
426
607
|
event.preventDefault();
|
|
@@ -430,7 +611,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
430
611
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
431
612
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
432
613
|
}
|
|
433
|
-
}, children: [_jsxs(Canvas, { camera: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, assets: playable.game.assets }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : null, _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}),
|
|
614
|
+
}, children: [_jsxs(Canvas, { camera: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, environment: playable.environment, assets: playable.game.assets, renderEntity: playable.renderEntity, selectedIds: selectedIds }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), _jsx(CombatCameraShake, {}), _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, pointerControls: pointerUsesLeft, onDragChange: (dragging) => {
|
|
434
615
|
cameraDraggingRef.current = dragging;
|
|
435
|
-
} }))] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
|
|
616
|
+
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
|
|
436
617
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AudioEngine } from "./audioEngine.js";
|
|
2
|
+
export declare function AudioListener({ engine }: {
|
|
3
|
+
engine: AudioEngine;
|
|
4
|
+
}): null;
|
|
5
|
+
export declare function EntityAudioEmitters({ engine, entitySounds, }: {
|
|
6
|
+
engine: AudioEngine;
|
|
7
|
+
entitySounds: Record<string, string> | undefined;
|
|
8
|
+
}): null;
|
|
9
|
+
export declare function ObjectAudioEmitters({ engine, objectSounds, }: {
|
|
10
|
+
engine: AudioEngine;
|
|
11
|
+
objectSounds: Record<string, string> | undefined;
|
|
12
|
+
}): null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useSceneEntities, useSceneObjects } from "@jgengine/react/hooks";
|
|
4
|
+
export function AudioListener({ engine }) {
|
|
5
|
+
const camera = useThree((state) => state.camera);
|
|
6
|
+
useFrame(() => {
|
|
7
|
+
engine.setListenerPose({ x: camera.position.x, y: camera.position.y, z: camera.position.z });
|
|
8
|
+
});
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function useTrackedEmitters(engine, soundByKey, entries) {
|
|
12
|
+
const handles = useRef(new Map());
|
|
13
|
+
useEffect(() => () => {
|
|
14
|
+
for (const handle of handles.current.values())
|
|
15
|
+
handle.stop();
|
|
16
|
+
handles.current.clear();
|
|
17
|
+
}, [engine]);
|
|
18
|
+
useFrame(() => {
|
|
19
|
+
if (soundByKey === undefined)
|
|
20
|
+
return;
|
|
21
|
+
const live = new Set();
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const soundId = soundByKey[entry.key];
|
|
24
|
+
if (soundId === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
live.add(entry.id);
|
|
27
|
+
let handle = handles.current.get(entry.id);
|
|
28
|
+
if (handle === undefined) {
|
|
29
|
+
handle = engine.playLoop(soundId, { x: entry.position[0], y: entry.position[1], z: entry.position[2] }) ?? undefined;
|
|
30
|
+
if (handle !== undefined)
|
|
31
|
+
handles.current.set(entry.id, handle);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
handle.setPosition({ x: entry.position[0], y: entry.position[1], z: entry.position[2] });
|
|
35
|
+
}
|
|
36
|
+
for (const [id, handle] of handles.current) {
|
|
37
|
+
if (!live.has(id)) {
|
|
38
|
+
handle.stop();
|
|
39
|
+
handles.current.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function EntityAudioEmitters({ engine, entitySounds, }) {
|
|
45
|
+
const entities = useSceneEntities();
|
|
46
|
+
useTrackedEmitters(engine, entitySounds, entities.map((entity) => ({ id: entity.id, key: entity.name, position: entity.position })));
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export function ObjectAudioEmitters({ engine, objectSounds, }) {
|
|
50
|
+
const objects = useSceneObjects();
|
|
51
|
+
useTrackedEmitters(engine, objectSounds, objects.map((object) => ({ id: object.instanceId, key: object.catalogId, position: object.position })));
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AudioBusDef, type SoundDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
export interface Vec3 {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AudioSceneConfig {
|
|
8
|
+
sounds?: Record<string, SoundDef>;
|
|
9
|
+
buses?: Record<string, AudioBusDef>;
|
|
10
|
+
}
|
|
11
|
+
export interface AudioEmitterHandle {
|
|
12
|
+
setPosition(position: Vec3): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface AudioEngine {
|
|
16
|
+
setListenerPose(position: Vec3): void;
|
|
17
|
+
playOneShot(soundId: string, position?: Vec3): void;
|
|
18
|
+
playLoop(soundId: string, position?: Vec3): AudioEmitterHandle | null;
|
|
19
|
+
setBusGain(busId: string, gain: number): void;
|
|
20
|
+
resume(): void;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createAudioEngine(config?: AudioSceneConfig): AudioEngine;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
function createNoopEngine() {
|
|
3
|
+
return {
|
|
4
|
+
setListenerPose: () => undefined,
|
|
5
|
+
playOneShot: () => undefined,
|
|
6
|
+
playLoop: () => null,
|
|
7
|
+
setBusGain: () => undefined,
|
|
8
|
+
resume: () => undefined,
|
|
9
|
+
dispose: () => undefined,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function resolveAudioContextCtor() {
|
|
13
|
+
if (typeof window === "undefined")
|
|
14
|
+
return undefined;
|
|
15
|
+
return (window.AudioContext ??
|
|
16
|
+
window.webkitAudioContext);
|
|
17
|
+
}
|
|
18
|
+
export function createAudioEngine(config = {}) {
|
|
19
|
+
const sounds = config.sounds ?? {};
|
|
20
|
+
const busDefs = config.buses ?? {};
|
|
21
|
+
const AudioContextCtor = resolveAudioContextCtor();
|
|
22
|
+
if (AudioContextCtor === undefined)
|
|
23
|
+
return createNoopEngine();
|
|
24
|
+
let context;
|
|
25
|
+
let masterGain;
|
|
26
|
+
try {
|
|
27
|
+
context = new AudioContextCtor();
|
|
28
|
+
masterGain = context.createGain();
|
|
29
|
+
masterGain.connect(context.destination);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return createNoopEngine();
|
|
33
|
+
}
|
|
34
|
+
const busGains = new Map();
|
|
35
|
+
function busGainNode(busId) {
|
|
36
|
+
let node = busGains.get(busId);
|
|
37
|
+
if (node === undefined) {
|
|
38
|
+
node = context.createGain();
|
|
39
|
+
node.gain.value = busDefs[busId]?.gain ?? 1;
|
|
40
|
+
node.connect(masterGain);
|
|
41
|
+
busGains.set(busId, node);
|
|
42
|
+
}
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
const bufferCache = new Map();
|
|
46
|
+
function loadBuffer(url) {
|
|
47
|
+
let pending = bufferCache.get(url);
|
|
48
|
+
if (pending === undefined) {
|
|
49
|
+
pending = fetch(url)
|
|
50
|
+
.then((response) => response.arrayBuffer())
|
|
51
|
+
.then((data) => context.decodeAudioData(data))
|
|
52
|
+
.catch(() => null);
|
|
53
|
+
bufferCache.set(url, pending);
|
|
54
|
+
}
|
|
55
|
+
return pending;
|
|
56
|
+
}
|
|
57
|
+
let listenerPosition = { x: 0, y: 0, z: 0 };
|
|
58
|
+
function playInternal(soundId, position, loop) {
|
|
59
|
+
const sound = sounds[soundId];
|
|
60
|
+
if (sound === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const bus = busGainNode(sound.bus);
|
|
63
|
+
let currentPosition = position ?? listenerPosition;
|
|
64
|
+
let gainNode = null;
|
|
65
|
+
let stopped = false;
|
|
66
|
+
void loadBuffer(sound.url).then((buffer) => {
|
|
67
|
+
if (stopped || buffer === null)
|
|
68
|
+
return;
|
|
69
|
+
const sourceNode = context.createBufferSource();
|
|
70
|
+
sourceNode.buffer = buffer;
|
|
71
|
+
sourceNode.loop = loop || (sound.loop ?? false);
|
|
72
|
+
gainNode = context.createGain();
|
|
73
|
+
gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
74
|
+
sourceNode.connect(gainNode);
|
|
75
|
+
gainNode.connect(bus);
|
|
76
|
+
sourceNode.start();
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
setPosition(next) {
|
|
80
|
+
currentPosition = next;
|
|
81
|
+
if (gainNode !== null) {
|
|
82
|
+
gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
stop() {
|
|
86
|
+
stopped = true;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
setListenerPose(position) {
|
|
92
|
+
listenerPosition = position;
|
|
93
|
+
},
|
|
94
|
+
playOneShot(soundId, position) {
|
|
95
|
+
playInternal(soundId, position, false);
|
|
96
|
+
},
|
|
97
|
+
playLoop(soundId, position) {
|
|
98
|
+
return playInternal(soundId, position, true);
|
|
99
|
+
},
|
|
100
|
+
setBusGain(busId, gain) {
|
|
101
|
+
busGainNode(busId).gain.value = gain;
|
|
102
|
+
},
|
|
103
|
+
resume() {
|
|
104
|
+
void context.resume().catch(() => undefined);
|
|
105
|
+
},
|
|
106
|
+
dispose() {
|
|
107
|
+
void context.close().catch(() => undefined);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import type { CameraRigKind, GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
export interface GameCameraRigProps {
|
|
4
|
+
yawRef: MutableRefObject<number>;
|
|
5
|
+
pitchRef: MutableRefObject<number>;
|
|
6
|
+
config?: GameCameraConfig;
|
|
7
|
+
onDragChange?: (dragging: boolean) => void;
|
|
8
|
+
pointerControls?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind;
|
|
11
|
+
export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls }: GameCameraRigProps): import("react").JSX.Element;
|