@jgengine/shell 0.2.0 → 0.4.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/dist/GamePlayerShell.js +77 -24
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -0
- package/dist/camera/GameFirstPersonCamera.js +63 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +6 -3
- package/dist/camera/index.d.ts +1 -1
- package/dist/camera/index.js +1 -1
- package/dist/camera/orbitCameraMath.d.ts +7 -0
- package/dist/camera/orbitCameraMath.js +7 -0
- package/dist/world/WorldHud.d.ts +14 -0
- package/dist/world/WorldHud.js +78 -0
- package/package.json +6 -6
package/dist/GamePlayerShell.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
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
3
|
import { Component, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
-
import {
|
|
5
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
|
+
import { createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
|
|
7
|
+
import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
6
8
|
import { advancePlayerMotion, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, } from "@jgengine/core/movement/movementModel";
|
|
7
9
|
import { createGameContext } from "@jgengine/core/runtime/gameContext";
|
|
8
10
|
import { useGameContext } from "@jgengine/react/provider";
|
|
9
11
|
import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
10
12
|
import { GameProvider } from "@jgengine/react/provider";
|
|
11
13
|
import { GAME_SIM_FRAME_PRIORITY, GameOrbitCamera } from "./camera/index.js";
|
|
14
|
+
import { GameFirstPersonCamera } from "./camera/GameFirstPersonCamera.js";
|
|
15
|
+
import { ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText } from "./world/WorldHud.js";
|
|
12
16
|
const DEV_USER_ID = "dev-player";
|
|
13
17
|
const TURN_SPEED = 2.4;
|
|
14
18
|
const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
|
|
@@ -25,10 +29,24 @@ function logRuntimeError(error, phase) {
|
|
|
25
29
|
console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
|
|
26
30
|
return diagnostic;
|
|
27
31
|
}
|
|
32
|
+
const RESERVED_INPUT_ACTIONS = new Set([
|
|
33
|
+
"moveForward",
|
|
34
|
+
"moveBack",
|
|
35
|
+
"moveLeft",
|
|
36
|
+
"moveRight",
|
|
37
|
+
"turnLeft",
|
|
38
|
+
"turnRight",
|
|
39
|
+
"sprint",
|
|
40
|
+
"jump",
|
|
41
|
+
"tabTarget",
|
|
42
|
+
"clearTarget",
|
|
43
|
+
"useAbility",
|
|
44
|
+
"interact",
|
|
45
|
+
]);
|
|
28
46
|
function findHotbarSlotActions(input) {
|
|
29
47
|
return Object.keys(input ?? {}).flatMap((action) => {
|
|
30
|
-
const
|
|
31
|
-
return
|
|
48
|
+
const slot = hotbarSlotActionIndex(action);
|
|
49
|
+
return slot === null ? [] : [{ action, slot }];
|
|
32
50
|
});
|
|
33
51
|
}
|
|
34
52
|
function hotbarIdFor(playable) {
|
|
@@ -36,7 +54,7 @@ function hotbarIdFor(playable) {
|
|
|
36
54
|
const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
|
|
37
55
|
return (hud ?? declarations[0])?.[0] ?? null;
|
|
38
56
|
}
|
|
39
|
-
function executeHotbarSlot(ctx, hotbarId, slot, yaw) {
|
|
57
|
+
function executeHotbarSlot(ctx, hotbarId, slot, yaw, pitch) {
|
|
40
58
|
const stack = ctx.player.inventory.state(hotbarId).slots[slot];
|
|
41
59
|
if (stack === undefined || stack === null)
|
|
42
60
|
return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
|
|
@@ -44,7 +62,7 @@ function executeHotbarSlot(ctx, hotbarId, slot, yaw) {
|
|
|
44
62
|
from: ctx.player.userId,
|
|
45
63
|
itemId: stack.itemId,
|
|
46
64
|
inventoryId: hotbarId,
|
|
47
|
-
aim: { yaw, pitch
|
|
65
|
+
aim: { yaw, pitch },
|
|
48
66
|
});
|
|
49
67
|
return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
|
|
50
68
|
}
|
|
@@ -63,13 +81,27 @@ function EntitySprite({ sprite }) {
|
|
|
63
81
|
}, [texture]);
|
|
64
82
|
return (_jsx("sprite", { "position-y": sprite.y, scale: [sprite.width, sprite.height, 1], children: _jsx("spriteMaterial", { map: texture, transparent: true, alphaTest: 0.08, depthWrite: false }) }));
|
|
65
83
|
}
|
|
66
|
-
function
|
|
84
|
+
function EntityModel({ model }) {
|
|
85
|
+
const gltf = useLoader(GLTFLoader, model.url);
|
|
86
|
+
const scene = useMemo(() => gltf.scene.clone(true), [gltf]);
|
|
87
|
+
const scale = model.scale ?? 1;
|
|
88
|
+
return _jsx("primitive", { object: scene, "position-y": model.y ?? 0, scale: [scale, scale, scale] });
|
|
89
|
+
}
|
|
90
|
+
function resolveModel(value, assets) {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
if (typeof value !== "string")
|
|
94
|
+
return value;
|
|
95
|
+
const url = assets.resolve(value)?.url;
|
|
96
|
+
return url === undefined ? undefined : { url };
|
|
97
|
+
}
|
|
98
|
+
function EntityMarker({ entity, model, sprite, isLocal, targeted, onSelect, }) {
|
|
67
99
|
const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
|
|
68
100
|
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, onPointerDown: (event) => {
|
|
69
101
|
event.stopPropagation();
|
|
70
102
|
if (!isLocal)
|
|
71
103
|
onSelect(entity);
|
|
72
|
-
}, children: [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] }));
|
|
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] }));
|
|
73
105
|
}
|
|
74
106
|
function GroundPlane() {
|
|
75
107
|
const geometry = useMemo(() => {
|
|
@@ -102,7 +134,7 @@ function RockField() {
|
|
|
102
134
|
}), []);
|
|
103
135
|
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))) }));
|
|
104
136
|
}
|
|
105
|
-
function WorldView({ entitySprites }) {
|
|
137
|
+
function WorldView({ entitySprites, entityModels, objectModels, assets, }) {
|
|
106
138
|
const ctx = useGameContext();
|
|
107
139
|
const entities = useSceneEntities();
|
|
108
140
|
const objects = useSceneObjects();
|
|
@@ -112,12 +144,16 @@ function WorldView({ entitySprites }) {
|
|
|
112
144
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
113
145
|
ctx.scene.entity.setTarget(player.userId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
114
146
|
};
|
|
115
|
-
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {}), entities.map((entity) => (_jsx(EntityMarker, { entity: entity, sprite: entitySprites?.[entity.name], isLocal: entity.id === player.userId, targeted: entity.id === targetId, onSelect: handleSelect }, entity.id))), objects.map((object) =>
|
|
147
|
+
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {}), entities.map((entity) => (_jsx(EntityMarker, { entity: entity, model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === player.userId, targeted: entity.id === targetId, onSelect: handleSelect }, entity.id))), objects.map((object) => {
|
|
148
|
+
const model = resolveModel(objectModels?.[object.catalogId], assets) ??
|
|
149
|
+
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));
|
|
151
|
+
})] }));
|
|
116
152
|
}
|
|
117
153
|
function RemotePlayers({ rows }) {
|
|
118
154
|
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))) }));
|
|
119
155
|
}
|
|
120
|
-
function FrameDriver({ ctx, playable, tracker, yawRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, }) {
|
|
156
|
+
function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, }) {
|
|
121
157
|
const motionRef = useRef(createPlayerMotionState());
|
|
122
158
|
const hasReportedTickError = useRef(false);
|
|
123
159
|
const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
|
|
@@ -164,21 +200,29 @@ function FrameDriver({ ctx, playable, tracker, yawRef, primaryClickRef, onRuntim
|
|
|
164
200
|
else
|
|
165
201
|
ctx.scene.entity.setTarget(playerId, null);
|
|
166
202
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
for (const action of Object.keys(playable.game.input ?? {})) {
|
|
204
|
+
if (!tracker.wasPressed(action))
|
|
205
|
+
continue;
|
|
206
|
+
if (action === "interact") {
|
|
207
|
+
const prompts = playable.prompts?.(ctx);
|
|
208
|
+
const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
|
|
209
|
+
if (prompts !== undefined && focus !== null) {
|
|
210
|
+
const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
|
|
211
|
+
if (active !== null && active.prompt.invoke !== null) {
|
|
212
|
+
ctx.game.commands.run(active.prompt.invoke.name, active.prompt.invoke.input);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
175
216
|
}
|
|
217
|
+
const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), RESERVED_INPUT_ACTIONS);
|
|
218
|
+
if (command !== null)
|
|
219
|
+
ctx.game.commands.run(command, {});
|
|
176
220
|
}
|
|
177
221
|
if (hotbarId !== null) {
|
|
178
222
|
for (const { action, slot } of slotActions) {
|
|
179
223
|
if (!tracker.wasPressed(action))
|
|
180
224
|
continue;
|
|
181
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current);
|
|
225
|
+
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
182
226
|
if (!result.ok)
|
|
183
227
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
184
228
|
}
|
|
@@ -191,7 +235,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, primaryClickRef, onRuntim
|
|
|
191
235
|
? preferred
|
|
192
236
|
: slots.findIndex((stack) => stack !== null);
|
|
193
237
|
if (slot >= 0) {
|
|
194
|
-
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current);
|
|
238
|
+
const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
|
|
195
239
|
if (!result.ok)
|
|
196
240
|
console.warn(`[jgengine:item-use] ${result.error}`);
|
|
197
241
|
}
|
|
@@ -207,7 +251,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, primaryClickRef, onRuntim
|
|
|
207
251
|
y: focus.position[1],
|
|
208
252
|
z: focus.position[2],
|
|
209
253
|
rotationY: focus.rotationY,
|
|
210
|
-
rotationPitch:
|
|
254
|
+
rotationPitch: pitchRef.current,
|
|
211
255
|
});
|
|
212
256
|
}
|
|
213
257
|
}
|
|
@@ -247,6 +291,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
247
291
|
const [remotePlayers, setRemotePlayers] = useState([]);
|
|
248
292
|
const wrapperRef = useRef(null);
|
|
249
293
|
const yawRef = useRef(0);
|
|
294
|
+
const pitchRef = useRef(0);
|
|
250
295
|
const serverIdRef = useRef(null);
|
|
251
296
|
const cameraDraggingRef = useRef(false);
|
|
252
297
|
const primaryClickRef = useRef(false);
|
|
@@ -347,6 +392,14 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
347
392
|
return _jsx("div", { className: "h-full w-full bg-neutral-950" });
|
|
348
393
|
const GameUI = playable.GameUI;
|
|
349
394
|
const WorldOverlay = playable.WorldOverlay;
|
|
395
|
+
const firstPerson = playable.camera?.perspective === "first";
|
|
396
|
+
const showReticle = firstPerson && playable.camera?.firstPerson?.reticle !== false;
|
|
397
|
+
const worldBars = playable.worldHealthBars;
|
|
398
|
+
const barsStatId = worldBars === undefined || worldBars === false
|
|
399
|
+
? null
|
|
400
|
+
: worldBars === true
|
|
401
|
+
? "health"
|
|
402
|
+
: worldBars.statId ?? "health";
|
|
350
403
|
return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
|
|
351
404
|
if (event.code === "Tab" || event.code === "Space")
|
|
352
405
|
event.preventDefault();
|
|
@@ -377,7 +430,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
|
|
|
377
430
|
else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
|
|
378
431
|
ctx.game.commands.run("ui.hotbarScrollPrev", {});
|
|
379
432
|
}
|
|
380
|
-
}, 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 }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, _jsx(GameOrbitCamera, { yawRef: yawRef, config: playable.camera, followEntityId: playable.camera?.followEntityId, onCameraFollow: playable.camera?.onCameraFollow, onDragChange: (dragging) => {
|
|
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, {}), firstPerson ? (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera?.firstPerson, followEntityId: playable.camera?.followEntityId })) : (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera, followEntityId: playable.camera?.followEntityId, onCameraFollow: playable.camera?.onCameraFollow, onDragChange: (dragging) => {
|
|
381
434
|
cameraDraggingRef.current = dragging;
|
|
382
|
-
} })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
|
|
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 })] }));
|
|
383
436
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import type { FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
export interface GameFirstPersonCameraProps {
|
|
4
|
+
yawRef: MutableRefObject<number>;
|
|
5
|
+
pitchRef: MutableRefObject<number>;
|
|
6
|
+
config?: FirstPersonCameraConfig;
|
|
7
|
+
followEntityId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { usePlayer } from "@jgengine/react/hooks";
|
|
7
|
+
import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
8
|
+
const DEFAULT_EYE_HEIGHT = 1.6;
|
|
9
|
+
const DEFAULT_SENSITIVITY = 0.0025;
|
|
10
|
+
const DEFAULT_MAX_PITCH = 1.45;
|
|
11
|
+
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }) {
|
|
12
|
+
const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
13
|
+
const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
14
|
+
const maxPitch = config?.maxPitch ?? DEFAULT_MAX_PITCH;
|
|
15
|
+
const { userId } = usePlayer();
|
|
16
|
+
const ctx = useGameContext();
|
|
17
|
+
const camera = useThree((state) => state.camera);
|
|
18
|
+
const domElement = useThree((state) => state.gl.domElement);
|
|
19
|
+
const followId = followEntityId ?? userId;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const requestLock = () => {
|
|
22
|
+
if (document.pointerLockElement !== domElement)
|
|
23
|
+
void domElement.requestPointerLock?.();
|
|
24
|
+
};
|
|
25
|
+
const onMove = (event) => {
|
|
26
|
+
if (document.pointerLockElement !== domElement)
|
|
27
|
+
return;
|
|
28
|
+
yawRef.current -= event.movementX * sensitivity;
|
|
29
|
+
pitchRef.current = Math.max(-maxPitch, Math.min(maxPitch, pitchRef.current - event.movementY * sensitivity));
|
|
30
|
+
};
|
|
31
|
+
domElement.addEventListener("click", requestLock);
|
|
32
|
+
window.addEventListener("mousemove", onMove);
|
|
33
|
+
return () => {
|
|
34
|
+
domElement.removeEventListener("click", requestLock);
|
|
35
|
+
window.removeEventListener("mousemove", onMove);
|
|
36
|
+
};
|
|
37
|
+
}, [domElement, sensitivity, maxPitch, yawRef, pitchRef]);
|
|
38
|
+
useFrame(() => {
|
|
39
|
+
const entity = ctx.scene.entity.get(followId);
|
|
40
|
+
if (entity === null)
|
|
41
|
+
return;
|
|
42
|
+
const cosPitch = Math.cos(pitchRef.current);
|
|
43
|
+
camera.position.set(entity.position[0], entity.position[1] + eyeHeight, entity.position[2]);
|
|
44
|
+
camera.lookAt(camera.position.x + Math.sin(yawRef.current) * cosPitch, camera.position.y + Math.sin(pitchRef.current), camera.position.z + Math.cos(yawRef.current) * cosPitch);
|
|
45
|
+
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
46
|
+
if (config?.viewmodel === false)
|
|
47
|
+
return null;
|
|
48
|
+
return _jsx(FirstPersonViewmodel, { camera: camera });
|
|
49
|
+
}
|
|
50
|
+
function FirstPersonViewmodel({ camera }) {
|
|
51
|
+
const groupRef = useRef(null);
|
|
52
|
+
useFrame(() => {
|
|
53
|
+
const group = groupRef.current;
|
|
54
|
+
if (group === null)
|
|
55
|
+
return;
|
|
56
|
+
group.position.copy(camera.position);
|
|
57
|
+
group.quaternion.copy(camera.quaternion);
|
|
58
|
+
group.translateX(0.34);
|
|
59
|
+
group.translateY(-0.26);
|
|
60
|
+
group.translateZ(-0.72);
|
|
61
|
+
}, GAME_SIM_FRAME_PRIORITY);
|
|
62
|
+
return (_jsxs("group", { ref: groupRef, children: [_jsxs("mesh", { position: [0, 0, -0.22], children: [_jsx("boxGeometry", { args: [0.09, 0.11, 0.55] }), _jsx("meshStandardMaterial", { color: "#22262d", metalness: 0.6, roughness: 0.35 })] }), _jsxs("mesh", { position: [0, -0.13, 0.04], rotation: [0.35, 0, 0], children: [_jsx("boxGeometry", { args: [0.08, 0.18, 0.11] }), _jsx("meshStandardMaterial", { color: "#33373f", metalness: 0.4, roughness: 0.5 })] }), _jsxs("mesh", { position: [0, 0.03, -0.52], rotation: [Math.PI / 2, 0, 0], children: [_jsx("cylinderGeometry", { args: [0.022, 0.022, 0.18, 10] }), _jsx("meshStandardMaterial", { color: "#0e0f12", metalness: 0.7, roughness: 0.3 })] })] }));
|
|
63
|
+
}
|
|
@@ -5,6 +5,8 @@ import { type CameraFollowState, type OrbitCameraConfig, type Vec3 } from "./orb
|
|
|
5
5
|
export type CameraFollowListener = (state: CameraFollowState) => void;
|
|
6
6
|
export interface GameOrbitCameraProps {
|
|
7
7
|
yawRef: MutableRefObject<number>;
|
|
8
|
+
/** Written each frame with the camera's look elevation (radians) for aim.pitch. */
|
|
9
|
+
pitchRef?: MutableRefObject<number>;
|
|
8
10
|
config?: Partial<OrbitCameraConfig>;
|
|
9
11
|
followEntityId?: string;
|
|
10
12
|
/** Override orbit target derived from the followed entity. */
|
|
@@ -12,6 +14,6 @@ export interface GameOrbitCameraProps {
|
|
|
12
14
|
onDragChange?: (dragging: boolean) => void;
|
|
13
15
|
onCameraFollow?: CameraFollowListener;
|
|
14
16
|
}
|
|
15
|
-
export declare function GameOrbitCamera({ yawRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, }: GameOrbitCameraProps): import("react").JSX.Element;
|
|
17
|
+
export declare function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, }: GameOrbitCameraProps): import("react").JSX.Element;
|
|
16
18
|
/** Seed orbit target before controls mount (demo spawn at origin). */
|
|
17
19
|
export declare function seedOrbitCameraTarget(camera: Camera, target: Vector3, distance: number, height: number): void;
|
|
@@ -5,8 +5,8 @@ import { useEffect, useRef } from "react";
|
|
|
5
5
|
import { MOUSE, Vector3 } from "three";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
|
-
import { ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, seedOrbitFollowState, } from "./orbitCameraMath.js";
|
|
9
|
-
export function GameOrbitCamera({ yawRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, }) {
|
|
8
|
+
import { cameraLookPitch, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, seedOrbitFollowState, } from "./orbitCameraMath.js";
|
|
9
|
+
export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, }) {
|
|
10
10
|
const config = resolveOrbitCameraConfig(configPatch);
|
|
11
11
|
const controlsRef = useRef(null);
|
|
12
12
|
const runtimeRef = useRef(null);
|
|
@@ -61,6 +61,9 @@ export function GameOrbitCamera({ yawRef, config: configPatch, followEntityId, r
|
|
|
61
61
|
};
|
|
62
62
|
const [px, , pz] = entity.position;
|
|
63
63
|
yawRef.current = orbitYawFromCamera(camera.position.x, camera.position.z, px, pz);
|
|
64
|
+
if (pitchRef !== undefined) {
|
|
65
|
+
pitchRef.current = cameraLookPitch({ x: camera.position.x, y: camera.position.y, z: camera.position.z }, stepped.target);
|
|
66
|
+
}
|
|
64
67
|
onCameraFollow?.({
|
|
65
68
|
entityId: followId,
|
|
66
69
|
target: stepped.target,
|
|
@@ -68,7 +71,7 @@ export function GameOrbitCamera({ yawRef, config: configPatch, followEntityId, r
|
|
|
68
71
|
distance: runtimeRef.current.lockedDistance ?? stepped.distance,
|
|
69
72
|
});
|
|
70
73
|
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
71
|
-
return (_jsx(OrbitControls, { ref: controlsRef, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, enablePan: false, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, maxPolarAngle:
|
|
74
|
+
return (_jsx(OrbitControls, { ref: controlsRef, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, enablePan: false, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, maxPolarAngle: config.maxPolarAngle, minPolarAngle: config.minPolarAngle, screenSpacePanning: false, mouseButtons: { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: undefined }, onStart: () => {
|
|
72
75
|
draggingRef.current = true;
|
|
73
76
|
onDragChange?.(true);
|
|
74
77
|
}, onEnd: () => {
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { GameOrbitCamera, type CameraFollowListener, type GameOrbitCameraProps } from "./GameOrbitCamera.js";
|
|
2
|
-
export { cameraFollowStep, DEFAULT_ORBIT_CAMERA, distanceBetween, GAME_SIM_FRAME_PRIORITY, lerpVec3, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, resolveTargetSmoothing, seedOrbitFollowState, smoothBlend, type CameraFollowState, type OrbitCameraConfig, type OrbitFollowRuntimeState, type ResolvedOrbitCameraConfig, type Vec3, } from "./orbitCameraMath.js";
|
|
2
|
+
export { cameraFollowStep, cameraLookPitch, DEFAULT_ORBIT_CAMERA, distanceBetween, GAME_SIM_FRAME_PRIORITY, lerpVec3, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, resolveTargetSmoothing, seedOrbitFollowState, smoothBlend, type CameraFollowState, type OrbitCameraConfig, type OrbitFollowRuntimeState, type ResolvedOrbitCameraConfig, type Vec3, } from "./orbitCameraMath.js";
|
package/dist/camera/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
2
|
-
export { cameraFollowStep, DEFAULT_ORBIT_CAMERA, distanceBetween, GAME_SIM_FRAME_PRIORITY, lerpVec3, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, resolveTargetSmoothing, seedOrbitFollowState, smoothBlend, } from "./orbitCameraMath.js";
|
|
2
|
+
export { cameraFollowStep, cameraLookPitch, DEFAULT_ORBIT_CAMERA, distanceBetween, GAME_SIM_FRAME_PRIORITY, lerpVec3, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, resolveTargetSmoothing, seedOrbitFollowState, smoothBlend, } from "./orbitCameraMath.js";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Horizontal facing derived from an orbit camera orbiting a target on the XZ plane. */
|
|
2
2
|
export declare function orbitYawFromCamera(cameraX: number, cameraZ: number, targetX: number, targetZ: number): number;
|
|
3
|
+
/** Elevation the camera looks along (radians): negative = aiming down, positive = aiming up, 0 = level. Feeds aim.pitch so vertical aim tracks the camera. */
|
|
4
|
+
export declare function cameraLookPitch(camera: Vec3, target: Vec3): number;
|
|
3
5
|
export interface Vec3 {
|
|
4
6
|
x: number;
|
|
5
7
|
y: number;
|
|
@@ -34,6 +36,9 @@ export interface OrbitCameraConfig {
|
|
|
34
36
|
dragTargetSmoothing?: number;
|
|
35
37
|
/** Exponential smoothing when re-locking orbit distance. */
|
|
36
38
|
distanceSmoothing?: number;
|
|
39
|
+
/** Vertical clamp (three.js polar angle, radians): 0 = top-down over the head, PI/2 = level, >PI/2 = look up from below the target. Widen to allow top-down or vertical aim; leave unset for the standard chase feel. */
|
|
40
|
+
minPolarAngle?: number;
|
|
41
|
+
maxPolarAngle?: number;
|
|
37
42
|
}
|
|
38
43
|
/** Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA. */
|
|
39
44
|
export interface ResolvedOrbitCameraConfig {
|
|
@@ -51,6 +56,8 @@ export interface ResolvedOrbitCameraConfig {
|
|
|
51
56
|
targetSmoothing: number;
|
|
52
57
|
dragTargetSmoothing: number;
|
|
53
58
|
distanceSmoothing: number;
|
|
59
|
+
minPolarAngle: number;
|
|
60
|
+
maxPolarAngle: number;
|
|
54
61
|
}
|
|
55
62
|
export declare const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig;
|
|
56
63
|
/** Run simulation/movement before orbit follow so poses are current. */
|
|
@@ -4,6 +4,11 @@ export function orbitYawFromCamera(cameraX, cameraZ, targetX, targetZ) {
|
|
|
4
4
|
const dz = targetZ - cameraZ;
|
|
5
5
|
return Math.atan2(dx, dz);
|
|
6
6
|
}
|
|
7
|
+
/** Elevation the camera looks along (radians): negative = aiming down, positive = aiming up, 0 = level. Feeds aim.pitch so vertical aim tracks the camera. */
|
|
8
|
+
export function cameraLookPitch(camera, target) {
|
|
9
|
+
const horizontal = Math.hypot(target.x - camera.x, target.z - camera.z);
|
|
10
|
+
return Math.atan2(target.y - camera.y, horizontal);
|
|
11
|
+
}
|
|
7
12
|
export const DEFAULT_ORBIT_CAMERA = {
|
|
8
13
|
minDistance: 4,
|
|
9
14
|
maxDistance: 28,
|
|
@@ -18,6 +23,8 @@ export const DEFAULT_ORBIT_CAMERA = {
|
|
|
18
23
|
targetSmoothing: 8,
|
|
19
24
|
dragTargetSmoothing: 11,
|
|
20
25
|
distanceSmoothing: 5,
|
|
26
|
+
minPolarAngle: 0.12,
|
|
27
|
+
maxPolarAngle: Math.PI / 2.05,
|
|
21
28
|
};
|
|
22
29
|
/** Run simulation/movement before orbit follow so poses are current. */
|
|
23
30
|
export const GAME_SIM_FRAME_PRIORITY = 0;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function WorldEntityBars({ statId, height }: {
|
|
2
|
+
statId: string;
|
|
3
|
+
height?: number;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
export declare function WorldFloatText({ height, lifeMs }: {
|
|
6
|
+
height?: number;
|
|
7
|
+
lifeMs?: number;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
export declare function ProjectileTracers({ lifeMs }: {
|
|
10
|
+
lifeMs?: number;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export declare function Reticle({ className }: {
|
|
13
|
+
className?: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html, Line } from "@react-three/drei";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { useEntityStat, useSceneEntities } from "@jgengine/react/hooks";
|
|
7
|
+
const MUZZLE_HEIGHT = 1.4;
|
|
8
|
+
function EntityBar({ entity, statId, height }) {
|
|
9
|
+
const stat = useEntityStat(entity.id, statId);
|
|
10
|
+
if (stat === null)
|
|
11
|
+
return null;
|
|
12
|
+
const range = stat.max - stat.min;
|
|
13
|
+
const percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
14
|
+
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}%` } }) }) }));
|
|
15
|
+
}
|
|
16
|
+
export function WorldEntityBars({ statId, height = 2.2 }) {
|
|
17
|
+
const ctx = useGameContext();
|
|
18
|
+
const entities = useSceneEntities();
|
|
19
|
+
const playerId = ctx.player.userId;
|
|
20
|
+
return (_jsx(_Fragment, { children: entities.map((entity) => entity.id === playerId ? null : (_jsx(EntityBar, { entity: entity, statId: statId, height: height }, entity.id))) }));
|
|
21
|
+
}
|
|
22
|
+
function FloatTextItem({ text, kind, lifeMs }) {
|
|
23
|
+
const [shown, setShown] = useState(false);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const raf = requestAnimationFrame(() => setShown(true));
|
|
26
|
+
return () => cancelAnimationFrame(raf);
|
|
27
|
+
}, []);
|
|
28
|
+
return (_jsx("span", { className: kind === "heal" ? "text-emerald-300" : "text-amber-200", style: {
|
|
29
|
+
display: "inline-block",
|
|
30
|
+
fontWeight: 800,
|
|
31
|
+
fontSize: "18px",
|
|
32
|
+
textShadow: "0 1px 3px rgba(0,0,0,0.95)",
|
|
33
|
+
transition: `transform ${lifeMs}ms ease-out, opacity ${lifeMs}ms ease-out`,
|
|
34
|
+
transform: shown ? "translateY(-30px)" : "translateY(0)",
|
|
35
|
+
opacity: shown ? 0 : 1,
|
|
36
|
+
}, children: text }));
|
|
37
|
+
}
|
|
38
|
+
export function WorldFloatText({ height = 1.9, lifeMs = 950 }) {
|
|
39
|
+
const ctx = useGameContext();
|
|
40
|
+
const [floaters, setFloaters] = useState([]);
|
|
41
|
+
const nextId = useRef(0);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return ctx.game.events.on("entity.floatText", (event) => {
|
|
44
|
+
const id = nextId.current++;
|
|
45
|
+
setFloaters((current) => [
|
|
46
|
+
...current,
|
|
47
|
+
{ id, position: event.position, text: event.text, kind: event.kind },
|
|
48
|
+
]);
|
|
49
|
+
window.setTimeout(() => setFloaters((current) => current.filter((floater) => floater.id !== id)), lifeMs);
|
|
50
|
+
});
|
|
51
|
+
}, [ctx, lifeMs]);
|
|
52
|
+
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, { text: floater.text, kind: floater.kind, lifeMs: lifeMs }) }, floater.id))) }));
|
|
53
|
+
}
|
|
54
|
+
export function ProjectileTracers({ lifeMs = 130 }) {
|
|
55
|
+
const ctx = useGameContext();
|
|
56
|
+
const [tracers, setTracers] = useState([]);
|
|
57
|
+
const nextId = useRef(0);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
return ctx.game.events.on("projectile.settled", (event) => {
|
|
60
|
+
const id = nextId.current++;
|
|
61
|
+
setTracers((current) => [
|
|
62
|
+
...current,
|
|
63
|
+
{
|
|
64
|
+
id,
|
|
65
|
+
points: [
|
|
66
|
+
new THREE.Vector3(event.origin[0], event.origin[1] + MUZZLE_HEIGHT, event.origin[2]),
|
|
67
|
+
new THREE.Vector3(event.at[0], event.at[1], event.at[2]),
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
window.setTimeout(() => setTracers((current) => current.filter((tracer) => tracer.id !== id)), lifeMs);
|
|
72
|
+
});
|
|
73
|
+
}, [ctx, lifeMs]);
|
|
74
|
+
return (_jsx(_Fragment, { children: tracers.map((tracer) => (_jsx(Line, { points: tracer.points, color: "#fde68a", lineWidth: 2, transparent: true, opacity: 0.85 }, tracer.id))) }));
|
|
75
|
+
}
|
|
76
|
+
export function Reticle({ className }) {
|
|
77
|
+
return (_jsx("div", { className: className ?? "pointer-events-none absolute inset-0 z-20 flex items-center justify-center", children: _jsxs("div", { className: "relative h-6 w-6", children: [_jsx("span", { className: "absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white/90 shadow" }), _jsx("span", { className: "absolute left-1/2 top-0 h-2 w-px -translate-x-1/2 bg-white/80" }), _jsx("span", { className: "absolute bottom-0 left-1/2 h-2 w-px -translate-x-1/2 bg-white/80" }), _jsx("span", { className: "absolute left-0 top-1/2 h-px w-2 -translate-y-1/2 bg-white/80" }), _jsx("span", { className: "absolute right-0 top-1/2 h-px w-2 -translate-y-1/2 bg-white/80" })] }) }));
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "
|
|
22
|
-
"check-types": "
|
|
21
|
+
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
22
|
+
"check-types": "tsgo --noEmit -p tsconfig.json",
|
|
23
23
|
"test": "bun test src"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@jgengine/core": "^0.
|
|
27
|
-
"@jgengine/react": "^0.
|
|
28
|
-
"@jgengine/ws": "^0.
|
|
26
|
+
"@jgengine/core": "^0.4.0",
|
|
27
|
+
"@jgengine/react": "^0.4.0",
|
|
28
|
+
"@jgengine/ws": "^0.4.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@react-three/drei": "^10.0.0",
|