@jgengine/shell 0.9.0 → 0.10.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 +21 -1
- package/dist/GamePlayerShell.d.ts +6 -21
- package/dist/GamePlayerShell.js +419 -234
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +33 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +21 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +35 -1
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/cartridge.js +1 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +3 -1
- package/dist/devtools/CollisionDebugWorld.js +1 -0
- package/dist/devtools/collisionDebugMath.d.ts +1 -0
- package/dist/devtools/collisionDebugMath.js +2 -1
- package/dist/environment/Daylight.d.ts +7 -1
- package/dist/environment/Daylight.js +52 -5
- package/dist/environment/EnvironmentScene.js +14 -5
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/multiplayer.js +7 -3
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +82 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +64 -0
- package/dist/terrain/CarvedTerrain.d.ts +4 -1
- package/dist/terrain/CarvedTerrain.js +4 -3
- package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/touch/TouchControlsOverlay.js +11 -1
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +7 -7
- package/dist/world/worldBarSamples.d.ts +1 -1
- package/dist/world/worldBarSamples.js +7 -1
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +140 -22
- package/package.json +4 -4
|
@@ -2,13 +2,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { DEFAULT_EYE_HEIGHT } from "@jgengine/core/combat/shotOrigin";
|
|
5
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
7
8
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
8
9
|
import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
9
|
-
const DEFAULT_EYE_HEIGHT = 1.6;
|
|
10
10
|
const DEFAULT_SENSITIVITY = 0.0025;
|
|
11
11
|
const DEFAULT_MAX_PITCH = 1.45;
|
|
12
|
+
const VIEWMODEL_ORIGIN = new THREE.Vector3(0.34, -0.26, -0.72);
|
|
13
|
+
const MUZZLE_TIP_LOCAL = new THREE.Vector3(0, 0.03, -0.61);
|
|
14
|
+
const MUZZLE_OFFSET = VIEWMODEL_ORIGIN.clone().add(MUZZLE_TIP_LOCAL);
|
|
15
|
+
const muzzleWorld = new THREE.Vector3();
|
|
16
|
+
let muzzleTracked = false;
|
|
17
|
+
/** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
|
|
18
|
+
export function readFirstPersonMuzzle(target) {
|
|
19
|
+
if (!muzzleTracked)
|
|
20
|
+
return false;
|
|
21
|
+
target.copy(muzzleWorld);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
12
24
|
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }) {
|
|
13
25
|
const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
14
26
|
const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
@@ -65,15 +77,20 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
65
77
|
}
|
|
66
78
|
function FirstPersonViewmodel({ camera }) {
|
|
67
79
|
const groupRef = useRef(null);
|
|
80
|
+
useEffect(() => () => {
|
|
81
|
+
muzzleTracked = false;
|
|
82
|
+
}, []);
|
|
68
83
|
useFrame(() => {
|
|
69
84
|
const group = groupRef.current;
|
|
70
85
|
if (group === null)
|
|
71
86
|
return;
|
|
72
87
|
group.position.copy(camera.position);
|
|
73
88
|
group.quaternion.copy(camera.quaternion);
|
|
74
|
-
group.translateX(
|
|
75
|
-
group.translateY(
|
|
76
|
-
group.translateZ(
|
|
89
|
+
group.translateX(VIEWMODEL_ORIGIN.x);
|
|
90
|
+
group.translateY(VIEWMODEL_ORIGIN.y);
|
|
91
|
+
group.translateZ(VIEWMODEL_ORIGIN.z);
|
|
92
|
+
muzzleWorld.copy(MUZZLE_OFFSET).applyQuaternion(camera.quaternion).add(camera.position);
|
|
93
|
+
muzzleTracked = true;
|
|
77
94
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
78
95
|
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 })] })] }));
|
|
79
96
|
}
|
|
@@ -20,5 +20,16 @@ export function GameInspectionCamera({ config: configPatch }) {
|
|
|
20
20
|
controlsRef.current?.update();
|
|
21
21
|
seededRef.current = true;
|
|
22
22
|
}, [camera, config]);
|
|
23
|
-
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!seededRef.current)
|
|
25
|
+
return;
|
|
26
|
+
const controls = controlsRef.current;
|
|
27
|
+
if (controls === null)
|
|
28
|
+
return;
|
|
29
|
+
const previous = controls.target.clone();
|
|
30
|
+
controls.target.set(config.target.x, config.target.y, config.target.z);
|
|
31
|
+
camera.position.add(controls.target.clone().sub(previous));
|
|
32
|
+
controls.update();
|
|
33
|
+
}, [camera, config.target.x, config.target.y, config.target.z]);
|
|
34
|
+
return (_jsx(OrbitControls, { ref: controlsRef, makeDefault: true, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, zoomToCursor: resolveInspectionZoomToCursor(config.anchor), enablePan: config.pan, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, minPolarAngle: config.minPolarAngle, maxPolarAngle: config.maxPolarAngle, screenSpacePanning: true, mouseButtons: { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.PAN } }));
|
|
24
35
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { OrbitControls } from "@react-three/drei";
|
|
3
3
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
|
-
import { MOUSE, PerspectiveCamera, Vector3 } from "three";
|
|
5
|
+
import { MOUSE, PerspectiveCamera, Raycaster, Vector3 } from "three";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
8
|
import { cameraLookPitch, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, seedOrbitFollowState, } from "./orbitCameraMath.js";
|
|
@@ -16,6 +16,10 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
16
16
|
const ctx = useGameContext();
|
|
17
17
|
const playerFov = usePlayerFov();
|
|
18
18
|
const camera = useThree((state) => state.camera);
|
|
19
|
+
const scene = useThree((state) => state.scene);
|
|
20
|
+
const raycasterRef = useRef(new Raycaster());
|
|
21
|
+
const collisionDirRef = useRef(new Vector3());
|
|
22
|
+
const collisionOriginRef = useRef(new Vector3());
|
|
19
23
|
const followId = followEntityId ?? userId;
|
|
20
24
|
useEffect(() => {
|
|
21
25
|
const entity = ctx.scene.entity.get(followId);
|
|
@@ -56,6 +60,36 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
56
60
|
camera.position.set(stepped.camera.x, stepped.camera.y, stepped.camera.z);
|
|
57
61
|
}
|
|
58
62
|
controls.update();
|
|
63
|
+
if (config.collision.enabled) {
|
|
64
|
+
const t = stepped.target;
|
|
65
|
+
const dx = camera.position.x - t.x;
|
|
66
|
+
const dy = camera.position.y - t.y;
|
|
67
|
+
const dz = camera.position.z - t.z;
|
|
68
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
69
|
+
if (dist > config.collision.minTargetDistance) {
|
|
70
|
+
const dir = collisionDirRef.current.set(dx / dist, dy / dist, dz / dist);
|
|
71
|
+
const ray = raycasterRef.current;
|
|
72
|
+
ray.set(collisionOriginRef.current.set(t.x, t.y, t.z), dir);
|
|
73
|
+
ray.near = config.collision.minTargetDistance;
|
|
74
|
+
ray.far = dist;
|
|
75
|
+
let blocked = 0;
|
|
76
|
+
for (const hit of ray.intersectObjects(scene.children, true)) {
|
|
77
|
+
const obj = hit.object;
|
|
78
|
+
if (!obj.visible)
|
|
79
|
+
continue;
|
|
80
|
+
if (obj.isSprite === true)
|
|
81
|
+
continue;
|
|
82
|
+
if (obj.userData.jgCameraTransparent === true)
|
|
83
|
+
continue;
|
|
84
|
+
blocked = hit.distance;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (blocked > 0) {
|
|
88
|
+
const pulled = Math.max(config.collision.minTargetDistance, blocked - config.collision.padding);
|
|
89
|
+
camera.position.set(t.x + dir.x * pulled, t.y + dir.y * pulled, t.z + dir.z * pulled);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
59
93
|
runtimeRef.current = {
|
|
60
94
|
target: stepped.target,
|
|
61
95
|
camera: { x: camera.position.x, y: camera.position.y, z: camera.position.z },
|
|
@@ -39,6 +39,23 @@ export interface OrbitCameraConfig {
|
|
|
39
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
40
|
minPolarAngle?: number;
|
|
41
41
|
maxPolarAngle?: number;
|
|
42
|
+
/** Spring-arm collision: raycast target→camera each frame and pull the boom in past occluders (walls, terrain) so the camera never clips inside geometry. */
|
|
43
|
+
collision?: OrbitCollisionConfig;
|
|
44
|
+
}
|
|
45
|
+
/** Spring-arm occlusion config for the orbit rig. */
|
|
46
|
+
export interface OrbitCollisionConfig {
|
|
47
|
+
/** Enable the pull-in raycast. Default false (unchanged chase camera). */
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
/** Gap kept between the camera and the hit surface, world units. Default 0.35. */
|
|
50
|
+
padding?: number;
|
|
51
|
+
/** Ignore hits closer than this to the target — the player's own body. Default 1.2. */
|
|
52
|
+
minTargetDistance?: number;
|
|
53
|
+
}
|
|
54
|
+
/** An {@link OrbitCollisionConfig} with every field resolved to a concrete value. */
|
|
55
|
+
export interface ResolvedOrbitCollision {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
padding: number;
|
|
58
|
+
minTargetDistance: number;
|
|
42
59
|
}
|
|
43
60
|
/** Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA. */
|
|
44
61
|
export interface ResolvedOrbitCameraConfig {
|
|
@@ -58,6 +75,7 @@ export interface ResolvedOrbitCameraConfig {
|
|
|
58
75
|
distanceSmoothing: number;
|
|
59
76
|
minPolarAngle: number;
|
|
60
77
|
maxPolarAngle: number;
|
|
78
|
+
collision: ResolvedOrbitCollision;
|
|
61
79
|
}
|
|
62
80
|
export declare const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig;
|
|
63
81
|
/** Run simulation/movement before orbit follow so poses are current. */
|
|
@@ -25,6 +25,7 @@ export const DEFAULT_ORBIT_CAMERA = {
|
|
|
25
25
|
distanceSmoothing: 5,
|
|
26
26
|
minPolarAngle: 0.12,
|
|
27
27
|
maxPolarAngle: Math.PI / 2.05,
|
|
28
|
+
collision: { enabled: false, padding: 0.35, minTargetDistance: 1.2 },
|
|
28
29
|
};
|
|
29
30
|
/** Run simulation/movement before orbit follow so poses are current. */
|
|
30
31
|
export const GAME_SIM_FRAME_PRIORITY = 0;
|
|
@@ -35,7 +36,11 @@ export function smoothBlend(deltaSeconds, speed) {
|
|
|
35
36
|
return 1 - Math.exp(-speed * deltaSeconds);
|
|
36
37
|
}
|
|
37
38
|
export function resolveOrbitCameraConfig(patch) {
|
|
38
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
...DEFAULT_ORBIT_CAMERA,
|
|
41
|
+
...patch,
|
|
42
|
+
collision: { ...DEFAULT_ORBIT_CAMERA.collision, ...patch?.collision },
|
|
43
|
+
};
|
|
39
44
|
}
|
|
40
45
|
export function resolveTargetSmoothing(config, dragging) {
|
|
41
46
|
if (dragging) {
|
package/dist/cartridge.js
CHANGED
|
@@ -229,6 +229,7 @@ export function cartridge(config) {
|
|
|
229
229
|
return defineGame({
|
|
230
230
|
name: config.name,
|
|
231
231
|
assets: config.assets,
|
|
232
|
+
features: { leaderboard: config.rules.killLeaderboardStat !== undefined },
|
|
232
233
|
world: config.world,
|
|
233
234
|
physics: config.physics,
|
|
234
235
|
input: config.input ?? WASD_KEYBINDS,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { LiveGameBackend } from "@jgengine/core/runtime/transport";
|
|
3
|
+
/** Where a gameplay command goes when the shell dispatches it — run locally, or sent to the authoritative host. */
|
|
4
|
+
export interface CommandSink {
|
|
5
|
+
run(name: string, input: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
/** Runs commands on the local `ctx` — the default, client-authoritative path. */
|
|
8
|
+
export declare function localCommandSink(ctx: GameContext): CommandSink;
|
|
9
|
+
/** Sends commands to the authoritative host over the transport; the result replicates back through world sync. */
|
|
10
|
+
export declare function remoteCommandSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): CommandSink;
|
|
11
|
+
/**
|
|
12
|
+
* The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into
|
|
13
|
+
* `authority: "server"` and a server is joined, local otherwise. Client-only UI commands (targeting, hotbar
|
|
14
|
+
* scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveCommandSink(ctx: GameContext, opts: {
|
|
17
|
+
serverAuthoritative: boolean;
|
|
18
|
+
backend: Pick<LiveGameBackend, "transport"> | null;
|
|
19
|
+
serverId: string | null;
|
|
20
|
+
}): CommandSink;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Runs commands on the local `ctx` — the default, client-authoritative path. */
|
|
2
|
+
export function localCommandSink(ctx) {
|
|
3
|
+
return {
|
|
4
|
+
run(name, input) {
|
|
5
|
+
ctx.game.commands.run(name, input);
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** Sends commands to the authoritative host over the transport; the result replicates back through world sync. */
|
|
10
|
+
export function remoteCommandSink(backend, serverId) {
|
|
11
|
+
return {
|
|
12
|
+
run(name, input) {
|
|
13
|
+
void backend.transport.runCommand({ serverId, command: name, input }).catch(() => undefined);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into
|
|
19
|
+
* `authority: "server"` and a server is joined, local otherwise. Client-only UI commands (targeting, hotbar
|
|
20
|
+
* scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveCommandSink(ctx, opts) {
|
|
23
|
+
if (opts.serverAuthoritative && opts.backend !== null && opts.serverId !== null) {
|
|
24
|
+
return remoteCommandSink(opts.backend, opts.serverId);
|
|
25
|
+
}
|
|
26
|
+
return localCommandSink(ctx);
|
|
27
|
+
}
|
package/dist/defineGame.js
CHANGED
|
@@ -10,7 +10,7 @@ function worldBackdrop(feature) {
|
|
|
10
10
|
}
|
|
11
11
|
const emptyUi = () => null;
|
|
12
12
|
export function defineGame(config) {
|
|
13
|
-
const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
|
|
13
|
+
const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, postProcessing, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
|
|
14
14
|
const game = defineEngineGame({ ...engineFields, multiplayer: multiplayer ?? offline() });
|
|
15
15
|
return {
|
|
16
16
|
game,
|
|
@@ -19,6 +19,7 @@ export function defineGame(config) {
|
|
|
19
19
|
onInit: loop?.onInit ?? noop,
|
|
20
20
|
onNewPlayer: loop?.onNewPlayer ?? noop,
|
|
21
21
|
onTick: loop?.onTick ?? noop,
|
|
22
|
+
onPlayerLeave: loop?.onPlayerLeave ?? noop,
|
|
22
23
|
},
|
|
23
24
|
GameUI: GameUI ?? emptyUi,
|
|
24
25
|
environment: environment ??
|
|
@@ -44,6 +45,7 @@ export function defineGame(config) {
|
|
|
44
45
|
movement,
|
|
45
46
|
lighting,
|
|
46
47
|
backdrop,
|
|
48
|
+
postProcessing,
|
|
47
49
|
shadows,
|
|
48
50
|
presentation,
|
|
49
51
|
objectStyles,
|
|
@@ -136,6 +136,7 @@ export function CollisionDebugWorld() {
|
|
|
136
136
|
},
|
|
137
137
|
positionOf: (id) => ctx.scene.entity.get(id)?.position,
|
|
138
138
|
rotationYOf: (id) => ctx.scene.entity.get(id)?.rotationY,
|
|
139
|
+
collidersOf: (id) => ctx.scene.entity.collidersOf(id),
|
|
139
140
|
from: probe.from,
|
|
140
141
|
aim: probe.aim,
|
|
141
142
|
originPolicy: probe.originPolicy,
|
|
@@ -75,6 +75,7 @@ export interface ComputeAimLaserInput {
|
|
|
75
75
|
sceneRaycast: SceneRaycastApi;
|
|
76
76
|
positionOf(instanceId: string): EntityPosition | undefined;
|
|
77
77
|
rotationYOf?(instanceId: string): number | undefined;
|
|
78
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
78
79
|
from: string;
|
|
79
80
|
aim: Aim;
|
|
80
81
|
originPolicy?: ShotOriginPolicy;
|
|
@@ -91,7 +91,8 @@ export function computeAimLaser(input) {
|
|
|
91
91
|
const resolved = resolveShot({
|
|
92
92
|
positionOf: input.positionOf,
|
|
93
93
|
rotationYOf: input.rotationYOf,
|
|
94
|
-
|
|
94
|
+
collidersOf: input.collidersOf,
|
|
95
|
+
}, input.from, input.aim, input.originPolicy ?? { kind: "eye" });
|
|
95
96
|
if (resolved === null)
|
|
96
97
|
return null;
|
|
97
98
|
input.counters !== undefined && (input.counters.queries += 1);
|
|
@@ -7,10 +7,16 @@ export interface SkyDomeProps {
|
|
|
7
7
|
radius?: number;
|
|
8
8
|
offset?: number;
|
|
9
9
|
exponent?: number;
|
|
10
|
+
/** Direction toward the sun; a bright HDR sun disc + warm glow is drawn there (blooms through the post chain). Omit to skip the sun. */
|
|
11
|
+
sunDirection?: readonly [number, number, number];
|
|
12
|
+
/** Sun disc/glow colour. Default warm white. */
|
|
13
|
+
sunColor?: string;
|
|
14
|
+
/** Sun disc/glow intensity multiplier. Default 1. */
|
|
15
|
+
sunIntensity?: number;
|
|
10
16
|
/** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
|
|
11
17
|
materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
|
|
12
18
|
}
|
|
13
|
-
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
19
|
+
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
14
20
|
export interface DaylightProps {
|
|
15
21
|
sky?: SkyDomeProps | false;
|
|
16
22
|
fog?: {
|
|
@@ -3,13 +3,36 @@ import { useFrame } from "@react-three/fiber";
|
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { daylightStateAt, SKY_PRESET_DAY_FRACTION } from "./daylightCycle.js";
|
|
6
|
+
function normalizeVec3(v) {
|
|
7
|
+
const out = new THREE.Vector3(v[0], v[1], v[2]);
|
|
8
|
+
return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
|
|
9
|
+
}
|
|
6
10
|
const SKY_TOP = "#3fa4f2";
|
|
7
11
|
const SKY_HORIZON = "#e3f4ff";
|
|
8
12
|
const FOG_COLOR = "#e9f6ff";
|
|
9
13
|
const SUN_COLOR = "#fff1c9";
|
|
10
14
|
const HEMI_SKY = "#bfe3ff";
|
|
11
15
|
const HEMI_GROUND = "#4c6b34";
|
|
12
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Sun directional light whose high-resolution shadow camera follows the view each
|
|
18
|
+
* frame, so grounded shadows stay crisp under the player anywhere in a large world
|
|
19
|
+
* instead of only near the origin.
|
|
20
|
+
*/
|
|
21
|
+
function ShadowCastingSun({ position, intensity, color, }) {
|
|
22
|
+
const ref = useRef(null);
|
|
23
|
+
useFrame((state) => {
|
|
24
|
+
const light = ref.current;
|
|
25
|
+
if (light === null)
|
|
26
|
+
return;
|
|
27
|
+
const cx = state.camera.position.x;
|
|
28
|
+
const cz = state.camera.position.z;
|
|
29
|
+
light.position.set(cx + position[0], position[1], cz + position[2]);
|
|
30
|
+
light.target.position.set(cx, 0, cz);
|
|
31
|
+
light.target.updateMatrixWorld();
|
|
32
|
+
});
|
|
33
|
+
return (_jsx("directionalLight", { ref: ref, position: [position[0], position[1], position[2]], intensity: intensity, color: color, castShadow: true, "shadow-mapSize-width": 2048, "shadow-mapSize-height": 2048, "shadow-camera-left": -90, "shadow-camera-right": 90, "shadow-camera-top": 90, "shadow-camera-bottom": -90, "shadow-camera-near": 10, "shadow-camera-far": 520, "shadow-bias": -0.0004, "shadow-normalBias": 0.02 }));
|
|
34
|
+
}
|
|
35
|
+
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, materialRef, } = {}) {
|
|
13
36
|
const material = useMemo(() => {
|
|
14
37
|
return new THREE.ShaderMaterial({
|
|
15
38
|
uniforms: {
|
|
@@ -17,6 +40,9 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
17
40
|
bottomColor: { value: new THREE.Color(horizonColor) },
|
|
18
41
|
offset: { value: offset },
|
|
19
42
|
exponent: { value: exponent },
|
|
43
|
+
uSunColor: { value: new THREE.Color(sunColor) },
|
|
44
|
+
uSunDirection: { value: sunDirection === undefined ? new THREE.Vector3(0, 1, 0) : normalizeVec3(sunDirection) },
|
|
45
|
+
uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
|
|
20
46
|
},
|
|
21
47
|
vertexShader: `
|
|
22
48
|
varying vec3 vWorldPosition;
|
|
@@ -29,19 +55,40 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
29
55
|
fragmentShader: `
|
|
30
56
|
uniform vec3 topColor;
|
|
31
57
|
uniform vec3 bottomColor;
|
|
58
|
+
uniform vec3 uSunColor;
|
|
59
|
+
uniform vec3 uSunDirection;
|
|
60
|
+
uniform float uSunIntensity;
|
|
32
61
|
uniform float offset;
|
|
33
62
|
uniform float exponent;
|
|
34
63
|
varying vec3 vWorldPosition;
|
|
64
|
+
float sHash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
65
|
+
float sNoise(vec2 p){
|
|
66
|
+
vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);
|
|
67
|
+
return mix(mix(sHash(i), sHash(i + vec2(1.0, 0.0)), u.x),
|
|
68
|
+
mix(sHash(i + vec2(0.0, 1.0)), sHash(i + vec2(1.0, 1.0)), u.x), u.y);
|
|
69
|
+
}
|
|
70
|
+
float sFbm(vec2 p){ float v = 0.0; float a = 0.5; for (int i = 0; i < 5; i++){ v += a * sNoise(p); p *= 2.0; a *= 0.5; } return v; }
|
|
35
71
|
void main() {
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
vec3 dir = normalize(vWorldPosition + vec3(0.0, offset, 0.0));
|
|
73
|
+
float h = max(dir.y, 0.0);
|
|
74
|
+
vec3 col = mix(bottomColor, topColor, pow(h, exponent));
|
|
75
|
+
col = mix(col, bottomColor * 1.12, pow(1.0 - h, 6.0) * 0.5);
|
|
76
|
+
vec3 vd = normalize(vWorldPosition);
|
|
77
|
+
vec2 cuv = vd.xz / (max(vd.y, 0.06) + 0.15) * 1.2;
|
|
78
|
+
float clouds = smoothstep(0.52, 0.82, sFbm(cuv));
|
|
79
|
+
float band = smoothstep(0.08, 0.35, dir.y) * smoothstep(1.0, 0.35, dir.y);
|
|
80
|
+
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band * 0.35);
|
|
81
|
+
float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
|
|
82
|
+
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
83
|
+
col += uSunColor * glow * uSunIntensity;
|
|
84
|
+
gl_FragColor = vec4(col, 1.0);
|
|
38
85
|
}
|
|
39
86
|
`,
|
|
40
87
|
side: THREE.BackSide,
|
|
41
88
|
depthWrite: false,
|
|
42
89
|
fog: false,
|
|
43
90
|
});
|
|
44
|
-
}, [topColor, horizonColor, offset, exponent]);
|
|
91
|
+
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity]);
|
|
45
92
|
useEffect(() => {
|
|
46
93
|
if (materialRef !== undefined)
|
|
47
94
|
materialRef.current = material;
|
|
@@ -55,7 +102,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
55
102
|
}
|
|
56
103
|
export function Daylight({ sky, fog, sun, ambient, lights = true } = {}) {
|
|
57
104
|
const sunPosition = sun?.position ?? [120, 160, 70];
|
|
58
|
-
return (_jsxs(_Fragment, { children: [sky === false ? null : _jsx(SkyDome, { ...(sky ?? {}) }), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(
|
|
105
|
+
return (_jsxs(_Fragment, { children: [sky === false ? null : (_jsx(SkyDome, { ...(sky ?? {}), sunDirection: sunPosition, sunColor: sun?.color ?? SUN_COLOR, sunIntensity: sun?.intensity ?? 1 })), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(ShadowCastingSun, { position: sunPosition, intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR })] })) : null] }));
|
|
59
106
|
}
|
|
60
107
|
/** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates. */
|
|
61
108
|
export function SkyDaylight({ sky, lights = true }) {
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useRef } from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
4
|
import { useGameContext } from "@jgengine/react/provider";
|
|
5
5
|
import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
|
|
6
6
|
import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
|
|
7
|
-
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
7
|
+
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
8
8
|
import { GroundPad } from "./GroundPad.js";
|
|
9
|
+
import { RoadRibbons } from "./RoadRibbons.js";
|
|
9
10
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
10
11
|
import { GrassField } from "../terrain/GrassField.js";
|
|
11
12
|
import { CarvedTerrain } from "../terrain/CarvedTerrain.js";
|
|
13
|
+
import { createTerrainDetailMaterial } from "../terrain/terrainDetailMaterial.js";
|
|
12
14
|
import { Ocean } from "../water/Ocean.js";
|
|
13
15
|
import { RainField } from "../weather/RainField.js";
|
|
14
16
|
import { SnowField } from "../weather/SnowField.js";
|
|
@@ -16,7 +18,9 @@ import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
|
|
|
16
18
|
function TerrainGround({ terrain, field, center, }) {
|
|
17
19
|
const palette = useMemo(() => resolveTerrainPalette(terrain), [terrain]);
|
|
18
20
|
const paletteAt = useMemo(() => {
|
|
19
|
-
|
|
21
|
+
const hasRegions = terrain.materialRegions !== undefined && terrain.materialRegions.length > 0;
|
|
22
|
+
const hasBands = terrain.biomeBands !== undefined && terrain.biomeBands.length > 0;
|
|
23
|
+
if (!hasRegions && !hasBands)
|
|
20
24
|
return undefined;
|
|
21
25
|
const sampler = createTerrainPaletteSampler(terrain);
|
|
22
26
|
if (center === undefined)
|
|
@@ -34,7 +38,11 @@ function TerrainGround({ terrain, field, center, }) {
|
|
|
34
38
|
const swing = terrain.height * 1.2;
|
|
35
39
|
return [base - swing, base + swing];
|
|
36
40
|
}, [terrain.baseHeight, terrain.height]);
|
|
37
|
-
|
|
41
|
+
const detailMaterial = useMemo(() => terrain.detail === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: createTerrainDetailMaterial(resolveTerrainDetail(terrain.detail, terrain.waterLevel)).material, [terrain.detail, terrain.waterLevel]);
|
|
44
|
+
useEffect(() => () => detailMaterial?.dispose(), [detailMaterial]);
|
|
45
|
+
return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94, surfaceMaterial: detailMaterial }));
|
|
38
46
|
}
|
|
39
47
|
function areaCenter(area) {
|
|
40
48
|
return area.position ?? [0, 0];
|
|
@@ -110,5 +118,6 @@ export function EnvironmentScene({ feature }) {
|
|
|
110
118
|
const structures = feature.structures ?? [];
|
|
111
119
|
const pads = feature.pads ?? [];
|
|
112
120
|
const islands = feature.islands ?? [];
|
|
113
|
-
|
|
121
|
+
const roads = feature.roads ?? [];
|
|
122
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field }, `road-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
114
123
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RoadEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
/** Renders one `road()` descriptor: an asphalt ribbon plus optional dashed centerline, draped on the terrain. */
|
|
4
|
+
export declare function RoadRibbons({ road, field }: {
|
|
5
|
+
road: RoadEnvironmentDescriptor;
|
|
6
|
+
field: TerrainField;
|
|
7
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { BufferAttribute, BufferGeometry, DoubleSide } from "three";
|
|
4
|
+
import { buildRoadRibbon, dashSegments } from "@jgengine/core/world/roads";
|
|
5
|
+
import { offsetPath, sidewalkWidthOf } from "@jgengine/core/world/streets";
|
|
6
|
+
function toGeometry(positions, indices) {
|
|
7
|
+
if (positions.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
const geometry = new BufferGeometry();
|
|
10
|
+
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
11
|
+
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
12
|
+
geometry.computeVertexNormals();
|
|
13
|
+
return geometry;
|
|
14
|
+
}
|
|
15
|
+
function ribbonGeometry(path, width, field, elevation) {
|
|
16
|
+
const ribbon = buildRoadRibbon(path, width, (x, z) => field.sampleHeight(x, z), { elevation });
|
|
17
|
+
return toGeometry(ribbon.positions, ribbon.indices);
|
|
18
|
+
}
|
|
19
|
+
function mergedDashGeometry(road, field) {
|
|
20
|
+
const width = Math.max(0.25, road.width * 0.035);
|
|
21
|
+
const ribbons = dashSegments(road.path, 3.2, 4).map((dash) => buildRoadRibbon(dash, width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + 0.04 }));
|
|
22
|
+
const vertexCount = ribbons.reduce((sum, r) => sum + r.positions.length, 0);
|
|
23
|
+
const indexCount = ribbons.reduce((sum, r) => sum + r.indices.length, 0);
|
|
24
|
+
if (vertexCount === 0)
|
|
25
|
+
return null;
|
|
26
|
+
const positions = new Float32Array(vertexCount);
|
|
27
|
+
const indices = new Uint32Array(indexCount);
|
|
28
|
+
let vertexOffset = 0;
|
|
29
|
+
let indexOffset = 0;
|
|
30
|
+
for (const ribbon of ribbons) {
|
|
31
|
+
positions.set(ribbon.positions, vertexOffset);
|
|
32
|
+
const base = vertexOffset / 3;
|
|
33
|
+
for (let i = 0; i < ribbon.indices.length; i += 1) {
|
|
34
|
+
indices[indexOffset + i] = ribbon.indices[i] + base;
|
|
35
|
+
}
|
|
36
|
+
vertexOffset += ribbon.positions.length;
|
|
37
|
+
indexOffset += ribbon.indices.length;
|
|
38
|
+
}
|
|
39
|
+
return toGeometry(positions, indices);
|
|
40
|
+
}
|
|
41
|
+
/** Renders one `road()` descriptor: an asphalt ribbon plus optional dashed centerline, draped on the terrain. */
|
|
42
|
+
export function RoadRibbons({ road, field }) {
|
|
43
|
+
const asphalt = useMemo(() => ribbonGeometry(road.path, road.width, field, road.elevation), [road, field]);
|
|
44
|
+
const dashes = useMemo(() => (road.markings ? mergedDashGeometry(road, field) : null), [road, field]);
|
|
45
|
+
const sidewalks = useMemo(() => {
|
|
46
|
+
const width = sidewalkWidthOf(road);
|
|
47
|
+
if (width <= 0)
|
|
48
|
+
return [];
|
|
49
|
+
const offset = road.width / 2 + width / 2;
|
|
50
|
+
return [offsetPath(road.path, offset), offsetPath(road.path, -offset)]
|
|
51
|
+
.map((path) => ribbonGeometry(path, width, field, road.elevation + 0.06))
|
|
52
|
+
.filter((geometry) => geometry !== null);
|
|
53
|
+
}, [road, field]);
|
|
54
|
+
if (asphalt === null)
|
|
55
|
+
return null;
|
|
56
|
+
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), dashes !== null ? (_jsx("mesh", { geometry: dashes, children: _jsx("meshStandardMaterial", { color: road.markingColor, roughness: 0.8, metalness: 0, side: DoubleSide }) })) : null, sidewalks.map((geometry, index) => (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.sidewalk === false ? "#a7adb8" : road.sidewalk.color, roughness: 0.9, metalness: 0, side: DoubleSide }) }, `walk-${index}`)))] }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type InputFrame } from "@jgengine/core/runtime/hostedGameRunner";
|
|
2
|
+
import type { LiveGameBackend } from "@jgengine/core/runtime/transport";
|
|
3
|
+
/** Where the local player's per-frame input goes: discarded in single-player, sent to the authoritative host under `authority: "server"`. */
|
|
4
|
+
export interface InputSink {
|
|
5
|
+
send(frame: InputFrame): void;
|
|
6
|
+
}
|
|
7
|
+
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself. */
|
|
8
|
+
export declare function noopInputSink(): InputSink;
|
|
9
|
+
/** Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via {@link INPUT_COMMAND}. */
|
|
10
|
+
export declare function remoteInputSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): InputSink;
|
|
11
|
+
/** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise. */
|
|
12
|
+
export declare function resolveInputSink(opts: {
|
|
13
|
+
serverAuthoritative: boolean;
|
|
14
|
+
backend: Pick<LiveGameBackend, "transport"> | null;
|
|
15
|
+
serverId: string | null;
|
|
16
|
+
}): InputSink;
|
|
17
|
+
/** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing. */
|
|
18
|
+
export declare function inputFramesEqual(a: InputFrame, b: InputFrame): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
|
|
2
|
+
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself. */
|
|
3
|
+
export function noopInputSink() {
|
|
4
|
+
return { send() { } };
|
|
5
|
+
}
|
|
6
|
+
/** Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via {@link INPUT_COMMAND}. */
|
|
7
|
+
export function remoteInputSink(backend, serverId) {
|
|
8
|
+
return {
|
|
9
|
+
send(frame) {
|
|
10
|
+
void backend.transport
|
|
11
|
+
.runCommand({ serverId, command: INPUT_COMMAND, input: frame })
|
|
12
|
+
.catch(() => undefined);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise. */
|
|
17
|
+
export function resolveInputSink(opts) {
|
|
18
|
+
if (opts.serverAuthoritative && opts.backend !== null && opts.serverId !== null) {
|
|
19
|
+
return remoteInputSink(opts.backend, opts.serverId);
|
|
20
|
+
}
|
|
21
|
+
return noopInputSink();
|
|
22
|
+
}
|
|
23
|
+
/** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing. */
|
|
24
|
+
export function inputFramesEqual(a, b) {
|
|
25
|
+
if (a.held.length !== b.held.length)
|
|
26
|
+
return false;
|
|
27
|
+
for (let i = 0; i < a.held.length; i++)
|
|
28
|
+
if (a.held[i] !== b.held[i])
|
|
29
|
+
return false;
|
|
30
|
+
const pa = a.pointer;
|
|
31
|
+
const pb = b.pointer;
|
|
32
|
+
if (pa === null || pb === null)
|
|
33
|
+
return pa === pb;
|
|
34
|
+
return pa.x === pb.x && pa.y === pb.y && pa.active === pb.active;
|
|
35
|
+
}
|
package/dist/multiplayer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { adapterOf } from "@jgengine/core/runtime/adapter";
|
|
1
|
+
import { adapterOf, isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
2
2
|
import { createWsBackend } from "@jgengine/ws/createWsBackend";
|
|
3
3
|
import { announcePeerHost, broadcastChannelSignaling, createPeerGuest, createPeerHost, joinPeerSession, } from "@jgengine/ws/peer";
|
|
4
4
|
export const DEFAULT_FEED_ACTIONS = ["entity.died"];
|
|
@@ -31,8 +31,12 @@ export function resolveShellMultiplayer(args) {
|
|
|
31
31
|
const adapter = adapterOf(args.game.multiplayer);
|
|
32
32
|
if (adapter === null)
|
|
33
33
|
return null;
|
|
34
|
-
if (adapter.kind === "ws")
|
|
35
|
-
|
|
34
|
+
if (adapter.kind === "ws") {
|
|
35
|
+
const url = args.url ?? adapter.url;
|
|
36
|
+
if (url === undefined && isServerAuthoritative(args.game.multiplayer))
|
|
37
|
+
return null;
|
|
38
|
+
return build(url ?? DEFAULT_WS_URL);
|
|
39
|
+
}
|
|
36
40
|
if (adapter.kind === "lan")
|
|
37
41
|
return build(args.url ?? lanUrl(adapter));
|
|
38
42
|
return null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PostProcessingConfig } from "@jgengine/core/render/postProcessing";
|
|
2
|
+
/**
|
|
3
|
+
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
4
|
+
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
5
|
+
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
6
|
+
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function PostProcessing({ config }: {
|
|
9
|
+
config: PostProcessingConfig;
|
|
10
|
+
}): null;
|