@jgengine/shell 0.6.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 +167 -2
- package/dist/GamePlayerShell.js +210 -39
- 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/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/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/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -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/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/index.d.ts +4 -1
- package/dist/terrain/index.js +4 -1
- package/dist/terrain/terrainMath.d.ts +14 -1
- package/dist/terrain/terrainMath.js +80 -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/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/index.d.ts +1 -0
- package/dist/weather/index.js +1 -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 +8 -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)
|
|
@@ -103,13 +132,13 @@ function resolveModel(value, assets) {
|
|
|
103
132
|
return undefined;
|
|
104
133
|
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
105
134
|
}
|
|
106
|
-
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, onSelect, }) {
|
|
135
|
+
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
107
136
|
const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
|
|
108
|
-
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) => {
|
|
109
138
|
event.stopPropagation();
|
|
110
139
|
if (!isLocal)
|
|
111
140
|
onSelect(entity);
|
|
112
|
-
}, children: [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] }));
|
|
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] }));
|
|
113
142
|
}
|
|
114
143
|
function GroundPlane() {
|
|
115
144
|
const geometry = useMemo(() => {
|
|
@@ -142,26 +171,29 @@ function RockField() {
|
|
|
142
171
|
}), []);
|
|
143
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))) }));
|
|
144
173
|
}
|
|
145
|
-
function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, }) {
|
|
174
|
+
function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, selectedIds, }) {
|
|
146
175
|
const ctx = useGameContext();
|
|
147
176
|
const entities = useSceneEntities();
|
|
148
177
|
const objects = useSceneObjects();
|
|
149
178
|
const player = usePlayer();
|
|
150
179
|
const targetId = useTarget(player.userId);
|
|
180
|
+
const controlledId = ctx.player.possession.active(player.userId);
|
|
151
181
|
const handleSelect = (entity) => {
|
|
152
182
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
153
|
-
ctx.scene.entity.setTarget(
|
|
183
|
+
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
154
184
|
};
|
|
155
|
-
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
|
|
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) => {
|
|
156
188
|
const model = resolveModel(objectModels?.[object.catalogId], assets) ??
|
|
157
189
|
resolveModel(object.catalogId, assets);
|
|
158
|
-
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));
|
|
159
191
|
})] }));
|
|
160
192
|
}
|
|
161
193
|
function RemotePlayers({ rows }) {
|
|
162
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))) }));
|
|
163
195
|
}
|
|
164
|
-
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, }) {
|
|
165
197
|
const motionRef = useRef(createPlayerMotionState());
|
|
166
198
|
const hasReportedTickError = useRef(false);
|
|
167
199
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
@@ -174,7 +206,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
174
206
|
yawRef.current += TURN_SPEED * dt;
|
|
175
207
|
if (tracker.isDown("turnRight"))
|
|
176
208
|
yawRef.current -= TURN_SPEED * dt;
|
|
177
|
-
const playerId = ctx.player.userId;
|
|
209
|
+
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
178
210
|
const player = ctx.scene.entity.get(playerId);
|
|
179
211
|
const forwardX = Math.sin(yawRef.current);
|
|
180
212
|
const forwardZ = Math.cos(yawRef.current);
|
|
@@ -210,9 +242,22 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
210
242
|
else
|
|
211
243
|
ctx.scene.entity.setTarget(playerId, null);
|
|
212
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
|
+
}
|
|
213
256
|
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
214
257
|
if (!tracker.wasPressed(action))
|
|
215
258
|
continue;
|
|
259
|
+
if (action === "ping" && pingCommand !== undefined)
|
|
260
|
+
continue;
|
|
216
261
|
if (action === "interact") {
|
|
217
262
|
const prompts = playable.prompts?.(ctx);
|
|
218
263
|
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
@@ -229,10 +274,11 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
229
274
|
ctx.game.commands.run(command, {});
|
|
230
275
|
}
|
|
231
276
|
if (hotbarId !== null) {
|
|
277
|
+
const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
|
|
232
278
|
for (const { action, slot } of slotActions) {
|
|
233
279
|
if (!tracker.wasPressed(action))
|
|
234
280
|
continue;
|
|
235
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
281
|
+
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
236
282
|
if (!result.ok)
|
|
237
283
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
238
284
|
}
|
|
@@ -245,7 +291,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
|
|
|
245
291
|
? preferred
|
|
246
292
|
: slots.findIndex((stack) => stack !== null);
|
|
247
293
|
if (slot >= 0) {
|
|
248
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
294
|
+
const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
|
|
249
295
|
if (!result.ok)
|
|
250
296
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
251
297
|
}
|
|
@@ -306,7 +352,15 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
306
352
|
const cameraDraggingRef = useRef(false);
|
|
307
353
|
const primaryClickRef = useRef(false);
|
|
308
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);
|
|
309
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]);
|
|
310
364
|
const userId = multiplayer?.userId ?? DEV_USER_ID;
|
|
311
365
|
const reportRuntimeError = (error, phase) => {
|
|
312
366
|
const diagnostic = logRuntimeError(error, phase);
|
|
@@ -398,39 +452,156 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
398
452
|
useEffect(() => {
|
|
399
453
|
wrapperRef.current?.focus();
|
|
400
454
|
}, [ctx]);
|
|
455
|
+
useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
|
|
401
456
|
if (ctx === null)
|
|
402
457
|
return _jsx("div", { className: "h-full w-full bg-neutral-950" });
|
|
403
458
|
const GameUI = playable.GameUI;
|
|
404
459
|
const WorldOverlay = playable.WorldOverlay;
|
|
405
|
-
const
|
|
406
|
-
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";
|
|
407
467
|
const worldBars = playable.worldHealthBars;
|
|
408
468
|
const barsStatId = worldBars === undefined || worldBars === false
|
|
409
469
|
? null
|
|
410
470
|
: worldBars === true
|
|
411
471
|
? "health"
|
|
412
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
|
+
};
|
|
413
600
|
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
414
601
|
if (event.code === "Tab" || event.code === "Space")
|
|
415
602
|
event.preventDefault();
|
|
416
603
|
tracker.handleDown(event.code);
|
|
417
|
-
}, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: (event) => {
|
|
418
|
-
wrapperRef.current?.focus();
|
|
419
|
-
if (event.button === 0) {
|
|
420
|
-
pointerDownRef.current = { x: event.clientX, y: event.clientY };
|
|
421
|
-
}
|
|
422
|
-
}, onPointerUp: (event) => {
|
|
423
|
-
if (event.button !== 0 || pointerDownRef.current === null)
|
|
424
|
-
return;
|
|
425
|
-
const start = pointerDownRef.current;
|
|
426
|
-
pointerDownRef.current = null;
|
|
427
|
-
const dx = event.clientX - start.x;
|
|
428
|
-
const dy = event.clientY - start.y;
|
|
429
|
-
const moved = dx * dx + dy * dy;
|
|
430
|
-
if (!cameraDraggingRef.current && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX) {
|
|
431
|
-
primaryClickRef.current = true;
|
|
432
|
-
}
|
|
433
|
-
}, onWheel: (event) => {
|
|
604
|
+
}, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onContextMenu: handleContextMenu, onWheel: (event) => {
|
|
434
605
|
if (ctx === null || !event.shiftKey)
|
|
435
606
|
return;
|
|
436
607
|
event.preventDefault();
|
|
@@ -440,7 +611,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
440
611
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
441
612
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
442
613
|
}
|
|
443
|
-
}, 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 }), 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) => {
|
|
444
615
|
cameraDraggingRef.current = dragging;
|
|
445
|
-
} }))] }), _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 })] }));
|
|
446
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;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, TopDownRig, } from "./cameraRigs.js";
|
|
4
|
+
import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
5
|
+
import { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
6
|
+
import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
|
|
7
|
+
export function resolveRigKind(config) {
|
|
8
|
+
if (config?.rig !== undefined)
|
|
9
|
+
return config.rig;
|
|
10
|
+
if (config?.perspective === "first")
|
|
11
|
+
return "first";
|
|
12
|
+
return "orbit";
|
|
13
|
+
}
|
|
14
|
+
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls }) {
|
|
15
|
+
const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
|
|
16
|
+
const followEntityId = config?.followEntityId;
|
|
17
|
+
const rig = (() => {
|
|
18
|
+
if (config?.cinematic !== undefined) {
|
|
19
|
+
return _jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
20
|
+
}
|
|
21
|
+
const kind = resolveRigKind(config);
|
|
22
|
+
switch (kind) {
|
|
23
|
+
case "first":
|
|
24
|
+
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined }));
|
|
25
|
+
case "topDown":
|
|
26
|
+
return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
27
|
+
case "rts":
|
|
28
|
+
return _jsx(RtsRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
29
|
+
case "shoulder":
|
|
30
|
+
return _jsx(ShoulderRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
31
|
+
case "lockOn":
|
|
32
|
+
return _jsx(LockOnRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
33
|
+
case "chase":
|
|
34
|
+
return _jsx(ChaseRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
35
|
+
case "observer":
|
|
36
|
+
return _jsx(ObserverRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
37
|
+
default:
|
|
38
|
+
return (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId ?? undefined, onCameraFollow: config?.onCameraFollow, onDragChange: onDragChange, pointerControls: pointerControls }));
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return _jsx(CameraShakeContext.Provider, { value: channel, children: rig });
|
|
42
|
+
}
|