@jgengine/shell 0.17.0 → 0.18.1
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 +130 -2
- package/dist/GamePlayerShell.js +10 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +8 -1
- package/dist/Shell3dPresentation.js +34 -5
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +8 -1
- package/dist/audio/audioEngine.d.ts +7 -1
- package/dist/audio/audioEngine.js +67 -5
- package/dist/camera/cameraRigs.js +7 -0
- package/dist/defineGame.js +21 -6
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +61 -24
- package/dist/drivers/HudOnlyDriver.js +9 -4
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadSource.d.ts +12 -0
- package/dist/input/gamepadSource.js +107 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +10 -7
- package/dist/postfx/PostProcessing.js +33 -15
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/modelLoad.d.ts +10 -0
- package/dist/render/modelLoad.js +28 -0
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +15 -0
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +30 -0
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +13 -0
- package/dist/render/testFixtures/proceduralTileLayer.js +30 -0
- package/dist/render/useFootIk.d.ts +9 -0
- package/dist/render/useFootIk.js +89 -0
- package/dist/render/useModelAnimation.d.ts +2 -0
- package/dist/render/useModelAnimation.js +163 -107
- package/dist/scene/CityRenderer.js +661 -236
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +5 -2
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +8 -1
- package/dist/useShellMultiplayerSync.js +25 -5
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +98 -22
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-ui/SKILL.md +8 -0
- package/skills/jgengine-ui/api.md +127 -16
- package/skills/jgengine-ui/capabilities.md +21 -0
- package/skills/jgengine-ui/reference.md +3 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
/**
|
|
3
3
|
* Instanced facade material for city massing walls: a `MeshStandardMaterial` whose fragment stage
|
|
4
|
-
* paints
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* paints a real facade on the side faces of every instanced unit box — floor bands from the
|
|
5
|
+
* district's `floorHeight`, spandrels between floors, framed panes with mullions, a per-cell hash
|
|
6
|
+
* that lights a minority of windows (emissive), and a ground floor of plinth, storefront glazing
|
|
7
|
+
* and fascia so street level never repeats floor 30. Two instanced attributes drive it:
|
|
8
|
+
* `instanceGroundOffset` (buried foundation to skip) and `instanceFacade` (`x` = per-building seed,
|
|
9
|
+
* `y` = facade kind — 0 punched openings, 1 curtain wall, 2 open parking deck). Same
|
|
10
|
+
* `onBeforeCompile` idiom as the grass/terrain materials; instance colors keep tinting the wall
|
|
11
|
+
* exactly like the plain massing material.
|
|
10
12
|
* @internal — created by `CityRenderer` for the banded-piece InstancedMesh.
|
|
11
13
|
*/
|
|
12
14
|
export declare function createCityWindowMaterial(options: {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
/**
|
|
3
3
|
* Instanced facade material for city massing walls: a `MeshStandardMaterial` whose fragment stage
|
|
4
|
-
* paints
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* paints a real facade on the side faces of every instanced unit box — floor bands from the
|
|
5
|
+
* district's `floorHeight`, spandrels between floors, framed panes with mullions, a per-cell hash
|
|
6
|
+
* that lights a minority of windows (emissive), and a ground floor of plinth, storefront glazing
|
|
7
|
+
* and fascia so street level never repeats floor 30. Two instanced attributes drive it:
|
|
8
|
+
* `instanceGroundOffset` (buried foundation to skip) and `instanceFacade` (`x` = per-building seed,
|
|
9
|
+
* `y` = facade kind — 0 punched openings, 1 curtain wall, 2 open parking deck). Same
|
|
10
|
+
* `onBeforeCompile` idiom as the grass/terrain materials; instance colors keep tinting the wall
|
|
11
|
+
* exactly like the plain massing material.
|
|
10
12
|
* @internal — created by `CityRenderer` for the banded-piece InstancedMesh.
|
|
11
13
|
*/
|
|
12
14
|
export function createCityWindowMaterial(options) {
|
|
@@ -25,10 +27,12 @@ export function createCityWindowMaterial(options) {
|
|
|
25
27
|
.replace("#include <common>", `
|
|
26
28
|
#include <common>
|
|
27
29
|
attribute float instanceGroundOffset;
|
|
30
|
+
attribute vec2 instanceFacade;
|
|
28
31
|
varying vec3 vCityLocal;
|
|
29
32
|
varying vec3 vCityScale;
|
|
30
33
|
varying vec3 vCityNormal;
|
|
31
34
|
varying float vCityGround;
|
|
35
|
+
varying vec2 vCityFacade;
|
|
32
36
|
`)
|
|
33
37
|
.replace("#include <begin_vertex>", `
|
|
34
38
|
#include <begin_vertex>
|
|
@@ -36,6 +40,7 @@ vCityLocal = position;
|
|
|
36
40
|
vCityScale = vec3(length(instanceMatrix[0].xyz), length(instanceMatrix[1].xyz), length(instanceMatrix[2].xyz));
|
|
37
41
|
vCityNormal = normal;
|
|
38
42
|
vCityGround = instanceGroundOffset;
|
|
43
|
+
vCityFacade = instanceFacade;
|
|
39
44
|
`);
|
|
40
45
|
shader.fragmentShader = shader.fragmentShader
|
|
41
46
|
.replace("#include <common>", `
|
|
@@ -46,6 +51,8 @@ varying vec3 vCityLocal;
|
|
|
46
51
|
varying vec3 vCityScale;
|
|
47
52
|
varying vec3 vCityNormal;
|
|
48
53
|
varying float vCityGround;
|
|
54
|
+
varying vec2 vCityFacade;
|
|
55
|
+
float cityLit = 0.0;
|
|
49
56
|
float cityHash(vec2 cell) {
|
|
50
57
|
return fract(sin(dot(cell, vec2(127.1, 311.7))) * 43758.5453);
|
|
51
58
|
}
|
|
@@ -55,25 +62,77 @@ float cityHash(vec2 cell) {
|
|
|
55
62
|
{
|
|
56
63
|
// Side faces only — roofs and undersides stay plain wall.
|
|
57
64
|
if (abs(vCityNormal.y) < 0.5) {
|
|
58
|
-
|
|
59
|
-
float
|
|
65
|
+
bool sideX = abs(vCityNormal.x) > 0.5;
|
|
66
|
+
float along = sideX ? (vCityLocal.z + 0.5) * vCityScale.z : (vCityLocal.x + 0.5) * vCityScale.x;
|
|
67
|
+
float faceWidth = sideX ? vCityScale.z : vCityScale.x;
|
|
60
68
|
float up = (vCityLocal.y + 0.5) * vCityScale.y - vCityGround;
|
|
61
|
-
float
|
|
62
|
-
float
|
|
63
|
-
float
|
|
64
|
-
float
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
float fh = uFloorHeight;
|
|
70
|
+
float seed = vCityFacade.x;
|
|
71
|
+
float kind = vCityFacade.y;
|
|
72
|
+
float face = (sideX ? 2.0 : 0.0) + (vCityNormal.x + vCityNormal.z > 0.0 ? 1.0 : 0.0);
|
|
73
|
+
vec3 wall = diffuseColor.rgb;
|
|
74
|
+
float floorT = fract(up / fh);
|
|
75
|
+
float floorIndex = floor(up / fh);
|
|
76
|
+
// The facade is a procedural stripe pattern with no mip chain, so once a floor band shrinks
|
|
77
|
+
// toward a pixel it aliases into speckled static. Measure the on-screen footprint of one metre
|
|
78
|
+
// of facade and dissolve the detail into its own average before that happens.
|
|
79
|
+
float facadeFootprint = fwidth(up) + fwidth(along);
|
|
80
|
+
float detail = 1.0 - smoothstep(fh * 0.08, fh * 0.45, facadeFootprint);
|
|
81
|
+
if (kind > 1.5) {
|
|
82
|
+
// Open parking deck: shadowed slots between column bays, solid spandrel below each level.
|
|
83
|
+
float bay = fract(along / 6.0);
|
|
84
|
+
bool slot = up > fh * 0.55 && floorT > 0.22 && floorT < 0.82
|
|
85
|
+
&& along > 1.4 && along < faceWidth - 1.4 && bay > 0.14;
|
|
86
|
+
diffuseColor.rgb = slot ? wall * 0.16 : wall * (floorT < 0.18 ? 0.86 : 1.0);
|
|
87
|
+
} else if (up > fh * 1.02) {
|
|
88
|
+
float curtain = step(0.5, kind);
|
|
89
|
+
float pitch = mix(2.45, 1.75, curtain);
|
|
90
|
+
float glassW = mix(0.46, 0.7, curtain);
|
|
91
|
+
float margin = mix(0.9, 0.55, curtain);
|
|
92
|
+
float sillT = mix(0.36, 0.24, curtain);
|
|
93
|
+
float headT = mix(0.8, 0.87, curtain);
|
|
94
|
+
float colIndex = floor(along / pitch);
|
|
95
|
+
float colC = abs(fract(along / pitch) - 0.5) * 2.0;
|
|
96
|
+
bool inBand = along > margin && along < faceWidth - margin;
|
|
97
|
+
if (inBand && floorT > sillT && floorT < headT && colC < glassW) {
|
|
98
|
+
float cell = cityHash(vec2(floorIndex, colIndex) + vec2(seed * 41.0, face * 7.0));
|
|
99
|
+
float lit = step(0.9, cell);
|
|
100
|
+
vec3 pane = uWindowColor * mix(0.34, 0.86, cell);
|
|
101
|
+
pane = mix(pane, vec3(1.0, 0.9, 0.7), lit * 0.5);
|
|
102
|
+
// Pane frame + a transom across tall curtain-wall bays: the grid that reads as glazing.
|
|
103
|
+
float frame = step(glassW - 0.14, colC) + curtain * step(abs(floorT - (sillT + headT) * 0.5), 0.022);
|
|
104
|
+
diffuseColor.rgb = mix(pane, wall * mix(0.7, 0.5, curtain), clamp(frame, 0.0, 1.0));
|
|
105
|
+
cityLit = lit * (1.0 - clamp(frame, 0.0, 1.0)) * 0.3;
|
|
106
|
+
} else if (inBand && (floorT <= sillT || floorT >= headT)) {
|
|
107
|
+
diffuseColor.rgb = wall * mix(0.9, 0.78, curtain);
|
|
108
|
+
// Sill lip under each opening catches the light and breaks the flat band.
|
|
109
|
+
if (colC < glassW && floorT > sillT - 0.055) diffuseColor.rgb = wall * mix(1.08, 0.9, curtain);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Ground floor: plinth, pier-and-glass shopfront, fascia band under the first slab.
|
|
113
|
+
if (up < 0.55) {
|
|
114
|
+
diffuseColor.rgb = wall * 0.62;
|
|
115
|
+
} else if (faceWidth > 5.0) {
|
|
116
|
+
float pier = abs(fract(along / 3.1) - 0.5) * 2.0;
|
|
117
|
+
bool store = up > 0.75 && up < fh * 0.74 && pier < 0.78
|
|
118
|
+
&& along > 1.1 && along < faceWidth - 1.1;
|
|
119
|
+
if (store) {
|
|
120
|
+
diffuseColor.rgb = uWindowColor * 0.17;
|
|
121
|
+
cityLit = 0.1;
|
|
122
|
+
} else if (up > fh * 0.78 && up < fh * 0.96) {
|
|
123
|
+
diffuseColor.rgb = wall * 0.75;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
72
126
|
}
|
|
73
|
-
|
|
74
|
-
|
|
127
|
+
vec3 distant = mix(wall, uWindowColor * 0.5, 0.42);
|
|
128
|
+
diffuseColor.rgb = mix(distant, diffuseColor.rgb, detail);
|
|
129
|
+
cityLit *= detail;
|
|
75
130
|
}
|
|
76
131
|
}
|
|
132
|
+
`)
|
|
133
|
+
.replace("#include <emissivemap_fragment>", `
|
|
134
|
+
#include <emissivemap_fragment>
|
|
135
|
+
totalEmissiveRadiance += mix(uWindowColor, vec3(1.0, 0.86, 0.6), 0.6) * cityLit;
|
|
77
136
|
`);
|
|
78
137
|
};
|
|
79
138
|
material.customProgramCacheKey = () => `jgengine-city-windows`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
2
|
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { type GraphicsProfile } from "@jgengine/core/settings/graphicsProfile";
|
|
3
4
|
import { type TouchJoystickVariant, type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
4
5
|
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
5
6
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
@@ -9,11 +10,12 @@ export declare function useTouchStyle(store: SettingsStore, fallback: TouchStyle
|
|
|
9
10
|
/** Shell-internal: binds the SettingsStore joystick-variant choice (fixed vs floating) to the touch renderer. @internal */
|
|
10
11
|
export declare function useTouchJoystickVariant(store: SettingsStore): TouchJoystickVariant;
|
|
11
12
|
export declare function useSettingsRevision(store: SettingsStore): number;
|
|
12
|
-
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
|
|
13
|
+
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean, overrides?: Partial<Record<GraphicsQuality, Partial<GraphicsProfile>>>): {
|
|
13
14
|
shadows: boolean;
|
|
14
15
|
dpr: number;
|
|
15
16
|
uiScale: number;
|
|
16
17
|
quality: GraphicsQuality;
|
|
18
|
+
profile: GraphicsProfile;
|
|
17
19
|
};
|
|
18
20
|
export declare function AudioSettingsBridge({ store, engine, buses, }: {
|
|
19
21
|
store: SettingsStore;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_DPR, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { resolveGraphicsProfile } from "@jgengine/core/settings/graphicsProfile";
|
|
3
4
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANTS, TOUCH_STYLES, } from "@jgengine/core/input/touchScheme";
|
|
4
5
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
5
6
|
export const TOUCH_STYLE_AUTO = "auto";
|
|
@@ -26,16 +27,18 @@ export function useSettingsRevision(store) {
|
|
|
26
27
|
const getVersion = useCallback(() => versionRef.current, []);
|
|
27
28
|
return useSyncExternalStore(subscribe, getVersion, getVersion);
|
|
28
29
|
}
|
|
29
|
-
export function useGraphicsSettings(store, shadowsDefault) {
|
|
30
|
+
export function useGraphicsSettings(store, shadowsDefault, overrides) {
|
|
30
31
|
useSettingsRevision(store);
|
|
31
32
|
const rawQuality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
|
|
32
33
|
const quality = GRAPHICS_QUALITY_DPR[rawQuality] !== undefined ? rawQuality : "high";
|
|
34
|
+
const profile = resolveGraphicsProfile(quality, overrides?.[quality]);
|
|
33
35
|
const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
|
|
34
36
|
return {
|
|
35
37
|
shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
|
|
36
|
-
dpr:
|
|
38
|
+
dpr: profile.renderScale,
|
|
37
39
|
uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
|
|
38
40
|
quality,
|
|
41
|
+
profile,
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
export function AudioSettingsBridge({ store, engine, buses, }) {
|
|
@@ -48,6 +48,8 @@ export function createTerrainDetailMaterial(detail, textures) {
|
|
|
48
48
|
uRockColor: { value: new THREE.Color(detail.rockColor) },
|
|
49
49
|
uSandColor: { value: new THREE.Color(detail.sandColor) },
|
|
50
50
|
uSnowColor: { value: new THREE.Color(detail.snowColor) },
|
|
51
|
+
uSweepDry: { value: new THREE.Vector3(...detail.sweeps.dry) },
|
|
52
|
+
uSweepWet: { value: new THREE.Vector3(...detail.sweeps.wet) },
|
|
51
53
|
uRockSlopeStart: { value: detail.rockSlopeStart },
|
|
52
54
|
uSnowHeight: { value: detail.snowHeight },
|
|
53
55
|
uWaterLevel: { value: detail.waterLevel },
|
|
@@ -126,6 +128,8 @@ varying vec3 vJgWorldNormal;
|
|
|
126
128
|
uniform vec3 uRockColor;
|
|
127
129
|
uniform vec3 uSandColor;
|
|
128
130
|
uniform vec3 uSnowColor;
|
|
131
|
+
uniform vec3 uSweepDry;
|
|
132
|
+
uniform vec3 uSweepWet;
|
|
129
133
|
uniform float uRockSlopeStart;
|
|
130
134
|
uniform float uSnowHeight;
|
|
131
135
|
uniform float uWaterLevel;
|
|
@@ -149,9 +153,9 @@ float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
|
|
|
149
153
|
float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
|
|
150
154
|
vec3 jgBase = diffuseColor.rgb;
|
|
151
155
|
jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
|
|
152
|
-
//
|
|
153
|
-
jgBase = mix(jgBase, jgBase *
|
|
154
|
-
jgBase = mix(jgBase, jgBase *
|
|
156
|
+
// Biome patchwork: hue-shifted dry sweeps and cooler wet pockets, tinted per-world via detail.sweeps.
|
|
157
|
+
jgBase = mix(jgBase, jgBase * uSweepDry, smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
|
|
158
|
+
jgBase = mix(jgBase, jgBase * uSweepWet, smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
|
|
155
159
|
// Rolling large-scale light/dark sweeps keep far hills from flattening into one tone.
|
|
156
160
|
jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
|
|
157
161
|
jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgNear;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { type Dispatch, type SetStateAction } from "react";
|
|
2
2
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
3
|
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
4
|
+
import { type AuthoritativeFrameHandler } from "./worldSync.js";
|
|
4
5
|
import type { ShellMultiplayer } from "./multiplayer.js";
|
|
5
6
|
import type { PlayableGame } from "./registry.js";
|
|
6
7
|
/** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
|
|
7
8
|
export declare function useShellMultiplayerSync(ctx: GameContext | null, multiplayer: ShellMultiplayer | null, playable: PlayableGame, serverIdRef: {
|
|
8
9
|
current: string | null;
|
|
9
|
-
}, setRemotePlayers: Dispatch<SetStateAction<PresencePoseRow[]
|
|
10
|
+
}, setRemotePlayers: Dispatch<SetStateAction<PresencePoseRow[]>>, authoritativeFrameRef?: {
|
|
11
|
+
current: AuthoritativeFrameHandler | null;
|
|
12
|
+
}): {
|
|
13
|
+
status: "joining" | "joined" | "failed";
|
|
14
|
+
failureReason: string | null;
|
|
15
|
+
retry: () => void;
|
|
16
|
+
};
|
|
@@ -1,23 +1,37 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
3
3
|
import { attachWorldSync } from "./worldSync.js";
|
|
4
4
|
/** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
|
|
5
|
-
export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers) {
|
|
5
|
+
export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers, authoritativeFrameRef) {
|
|
6
|
+
const [status, setStatus] = useState("joining");
|
|
7
|
+
const [failureReason, setFailureReason] = useState(null);
|
|
8
|
+
const [attempt, setAttempt] = useState(0);
|
|
9
|
+
const retry = useCallback(() => setAttempt((value) => value + 1), []);
|
|
6
10
|
useEffect(() => {
|
|
7
11
|
if (ctx === null || multiplayer === null)
|
|
8
12
|
return;
|
|
13
|
+
setStatus("joining");
|
|
14
|
+
setFailureReason(null);
|
|
9
15
|
let disposed = false;
|
|
10
16
|
const cleanups = [];
|
|
11
17
|
void multiplayer.backend.transport
|
|
12
18
|
.joinServer({ gameId: multiplayer.gameId })
|
|
13
19
|
.then((joined) => {
|
|
20
|
+
if (!joined.ok) {
|
|
21
|
+
if (!disposed) {
|
|
22
|
+
setFailureReason(joined.reason);
|
|
23
|
+
setStatus("failed");
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
14
27
|
if (disposed) {
|
|
15
28
|
void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
|
|
16
29
|
return;
|
|
17
30
|
}
|
|
18
31
|
serverIdRef.current = joined.serverId;
|
|
32
|
+
setStatus("joined");
|
|
19
33
|
if (isServerAuthoritative(playable.game.multiplayer) && multiplayer.backend.feeds !== undefined) {
|
|
20
|
-
cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx));
|
|
34
|
+
cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx, (revision) => authoritativeFrameRef?.current?.(revision)));
|
|
21
35
|
}
|
|
22
36
|
cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
|
|
23
37
|
setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
|
|
@@ -82,7 +96,12 @@ export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef,
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
})
|
|
85
|
-
.catch(() =>
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
if (!disposed) {
|
|
101
|
+
setFailureReason(error instanceof Error ? error.message : "Connection failed");
|
|
102
|
+
setStatus("failed");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
86
105
|
return () => {
|
|
87
106
|
disposed = true;
|
|
88
107
|
for (const cleanup of cleanups)
|
|
@@ -94,5 +113,6 @@ export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef,
|
|
|
94
113
|
void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
|
|
95
114
|
}
|
|
96
115
|
};
|
|
97
|
-
}, [ctx, multiplayer, playable]);
|
|
116
|
+
}, [ctx, multiplayer, playable, attempt]);
|
|
117
|
+
return { status: multiplayer === null ? "joined" : status, failureReason, retry };
|
|
98
118
|
}
|
|
@@ -14,8 +14,9 @@ export declare function useRenderVisibility(): MutableRefObject<VisiblePredicate
|
|
|
14
14
|
* conservative preload margin) are never submitted to the renderer, without unmounting them or
|
|
15
15
|
* touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
16
16
|
*/
|
|
17
|
-
export declare function CullingProvider({ config, children }: {
|
|
17
|
+
export declare function CullingProvider({ config, drawDistance, children }: {
|
|
18
18
|
config: VisibilityConfig | undefined;
|
|
19
|
+
drawDistance?: number;
|
|
19
20
|
children: ReactNode;
|
|
20
21
|
}): ReactNode;
|
|
21
22
|
export {};
|
|
@@ -110,12 +110,17 @@ function viewFromCamera(camera) {
|
|
|
110
110
|
* conservative preload margin) are never submitted to the renderer, without unmounting them or
|
|
111
111
|
* touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
112
112
|
*/
|
|
113
|
-
export function CullingProvider({ config, children }) {
|
|
113
|
+
export function CullingProvider({ config, drawDistance, children }) {
|
|
114
114
|
const ctx = useGameContext();
|
|
115
115
|
const enabled = config?.enabled !== false;
|
|
116
116
|
const predicateRef = useRef(ALWAYS_VISIBLE);
|
|
117
117
|
const camera = useThree((state) => state.camera);
|
|
118
|
-
const
|
|
118
|
+
const resolvedConfig = useMemo(() => {
|
|
119
|
+
if (drawDistance === undefined)
|
|
120
|
+
return config;
|
|
121
|
+
return { ...config, culling: { ...config?.culling, defaultMaxRenderDistance: config?.culling?.defaultMaxRenderDistance ?? drawDistance } };
|
|
122
|
+
}, [config, drawDistance]);
|
|
123
|
+
const driver = useMemo(() => (enabled ? buildSystem(ctx, resolvedConfig) : null), [ctx, resolvedConfig, enabled]);
|
|
119
124
|
useFrame(() => {
|
|
120
125
|
if (driver === null) {
|
|
121
126
|
predicateRef.current = ALWAYS_VISIBLE;
|
|
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { resolveGridInstances } from "@jgengine/core/world/gridInstances";
|
|
5
5
|
import { useDisposable } from "../render/useDisposable.js";
|
|
6
|
+
import { TileLayerRenderer } from "./TileLayerRenderer.js";
|
|
6
7
|
function isGridWorldFeature(feature) {
|
|
7
8
|
return (feature.kind === "biomes" || feature.kind === "voxel" || feature.kind === "plots" || feature.kind === "tilemap");
|
|
8
9
|
}
|
|
@@ -12,6 +13,9 @@ function isGridWorldFeature(feature) {
|
|
|
12
13
|
* `cells`, following the same direct-buffer pattern as `InstancedBodies`.
|
|
13
14
|
*/
|
|
14
15
|
export function GridWorldScene({ feature }) {
|
|
16
|
+
if (feature.kind === "tilemap" && feature.tileSet !== undefined) {
|
|
17
|
+
return _jsx(TileLayerRenderer, { config: feature });
|
|
18
|
+
}
|
|
15
19
|
const instances = useMemo(() => (isGridWorldFeature(feature) ? resolveGridInstances(feature) : []), [feature]);
|
|
16
20
|
const meshRef = useRef(null);
|
|
17
21
|
const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TilemapWorldConfig } from "@jgengine/core/world/features";
|
|
2
|
+
/** Inputs for the instanced textured tile-layer renderer. */
|
|
3
|
+
export interface TileLayerRendererProps {
|
|
4
|
+
config: TilemapWorldConfig;
|
|
5
|
+
parallax?: readonly [number, number];
|
|
6
|
+
pixelated?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Renders a tilemap as one instanced quad mesh with atlas UV rectangles. */
|
|
9
|
+
export declare function TileLayerRenderer({ config, parallax, pixelated }: TileLayerRendererProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useLoader, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { sortingOrder } from "@jgengine/core/render/sprite2d";
|
|
6
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
7
|
+
import { resolveTileLayerInstances } from "./tileLayerInstances.js";
|
|
8
|
+
/** Renders a tilemap as one instanced quad mesh with atlas UV rectangles. */
|
|
9
|
+
export function TileLayerRenderer({ config, parallax = [1, 1], pixelated = true }) {
|
|
10
|
+
const tileSet = config.tileSet;
|
|
11
|
+
const { camera } = useThree();
|
|
12
|
+
const texture = useLoader(THREE.TextureLoader, tileSet?.atlas.image ?? "");
|
|
13
|
+
const instances = useMemo(() => resolveTileLayerInstances(config), [config]);
|
|
14
|
+
const meshRef = useRef(null);
|
|
15
|
+
const geometry = useDisposable(() => new THREE.PlaneGeometry(1, 1).rotateX(-Math.PI / 2), []);
|
|
16
|
+
const uvOffset = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(Math.max(instances.length, 1) * 2), 2), [instances.length]);
|
|
17
|
+
const uvScale = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(Math.max(instances.length, 1) * 2), 2), [instances.length]);
|
|
18
|
+
const material = useDisposable(() => {
|
|
19
|
+
const mat = new THREE.MeshBasicMaterial({ map: texture, transparent: true, alphaTest: 0.08, depthWrite: false });
|
|
20
|
+
mat.onBeforeCompile = (shader) => {
|
|
21
|
+
shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nattribute vec2 tileUvOffset;\nattribute vec2 tileUvScale;").replace("#include <uv_vertex>", "#include <uv_vertex>\n#ifdef USE_MAP\n vMapUv = vMapUv * tileUvScale + tileUvOffset;\n#endif");
|
|
22
|
+
};
|
|
23
|
+
mat.customProgramCacheKey = () => "jgengine-tile-layer";
|
|
24
|
+
return mat;
|
|
25
|
+
}, [texture]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
28
|
+
texture.magFilter = pixelated ? THREE.NearestFilter : THREE.LinearFilter;
|
|
29
|
+
texture.minFilter = pixelated ? THREE.NearestFilter : THREE.LinearMipmapLinearFilter;
|
|
30
|
+
texture.needsUpdate = true;
|
|
31
|
+
}, [texture, pixelated]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
geometry.setAttribute("tileUvOffset", uvOffset);
|
|
34
|
+
geometry.setAttribute("tileUvScale", uvScale);
|
|
35
|
+
}, [geometry, uvOffset, uvScale]);
|
|
36
|
+
const matrix = useMemo(() => new THREE.Matrix4(), []);
|
|
37
|
+
useFrame(() => {
|
|
38
|
+
const mesh = meshRef.current;
|
|
39
|
+
if (mesh === null)
|
|
40
|
+
return;
|
|
41
|
+
mesh.position.set(-camera.position.x * (1 - parallax[0]), 0, camera.position.z * (1 - parallax[1]));
|
|
42
|
+
});
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const mesh = meshRef.current;
|
|
45
|
+
if (mesh === null)
|
|
46
|
+
return;
|
|
47
|
+
mesh.count = instances.length;
|
|
48
|
+
const array = mesh.instanceMatrix.array;
|
|
49
|
+
const offset = uvOffset.array;
|
|
50
|
+
const scale = uvScale.array;
|
|
51
|
+
for (let i = 0; i < instances.length; i += 1) {
|
|
52
|
+
const instance = instances[i];
|
|
53
|
+
matrix.makeTranslation(instance.x, 0, instance.z).toArray(array, i * 16);
|
|
54
|
+
offset.set(instance.uvOffset, i * 2);
|
|
55
|
+
scale.set(instance.uvScale, i * 2);
|
|
56
|
+
}
|
|
57
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
58
|
+
uvOffset.needsUpdate = true;
|
|
59
|
+
uvScale.needsUpdate = true;
|
|
60
|
+
}, [instances, matrix, uvOffset, uvScale]);
|
|
61
|
+
if (tileSet === undefined || instances.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return _jsx("instancedMesh", { ref: meshRef, args: [geometry, material, instances.length], renderOrder: sortingOrder(["ground", "objects", "effects"], config.sortingLayer ?? "ground", 0), frustumCulled: false });
|
|
64
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import type { ParticleEmitterSpec } from "@jgengine/core/vfx/particleDirector";
|
|
4
|
+
/**
|
|
5
|
+
* The per-effect particle pool a graphics tier allows: the requested `max`
|
|
6
|
+
* clamped to the tier cap, so lower tiers degrade density instead of dropping
|
|
7
|
+
* effects entirely.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveParticleBudget(quality: GraphicsQuality, requested: number | undefined, particleCap?: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a spec change touches something the JSX reads (blending, static offset,
|
|
12
|
+
* follow binding) and therefore needs a React re-render — config-only retunes are
|
|
13
|
+
* applied imperatively to the live system and must stay render-free.
|
|
14
|
+
*
|
|
15
|
+
* @internal WorldParticles render-boundary helper, exported for its regression test.
|
|
16
|
+
*/
|
|
17
|
+
export declare function specNeedsRender(prev: ParticleEmitterSpec, next: ParticleEmitterSpec): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Renders `ctx.particles` — the game-reachable particle seam. Standing emitters
|
|
20
|
+
* become persistent `ParticleField`s (recreated only when their pool size changes,
|
|
21
|
+
* retuned in place otherwise), `follow` emitters track their entity's live pose
|
|
22
|
+
* each frame, and drained bursts become transient systems reaped once empty.
|
|
23
|
+
* Auto-mounted by the player shell; pools are capped by graphics quality via
|
|
24
|
+
* {@link resolveParticleBudget}.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function WorldParticles({ quality, particleCap }: {
|
|
29
|
+
quality: GraphicsQuality;
|
|
30
|
+
particleCap?: number;
|
|
31
|
+
}): ReactElement;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createParticleSystem } from "@jgengine/core/vfx/particles";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { ParticleField } from "../vfx/ParticleField.js";
|
|
7
|
+
const QUALITY_POOL_CAP = { low: 128, medium: 256, high: 512 };
|
|
8
|
+
/**
|
|
9
|
+
* The per-effect particle pool a graphics tier allows: the requested `max`
|
|
10
|
+
* clamped to the tier cap, so lower tiers degrade density instead of dropping
|
|
11
|
+
* effects entirely.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveParticleBudget(quality, requested, particleCap) {
|
|
14
|
+
const cap = Math.max(1, Math.floor(particleCap ?? QUALITY_POOL_CAP[quality]));
|
|
15
|
+
return Math.max(1, Math.min(requested ?? cap, cap));
|
|
16
|
+
}
|
|
17
|
+
const MAX_CONCURRENT_BURSTS = 32;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a spec change touches something the JSX reads (blending, static offset,
|
|
20
|
+
* follow binding) and therefore needs a React re-render — config-only retunes are
|
|
21
|
+
* applied imperatively to the live system and must stay render-free.
|
|
22
|
+
*
|
|
23
|
+
* @internal WorldParticles render-boundary helper, exported for its regression test.
|
|
24
|
+
*/
|
|
25
|
+
export function specNeedsRender(prev, next) {
|
|
26
|
+
if (prev.blending !== next.blending || prev.follow !== next.follow)
|
|
27
|
+
return true;
|
|
28
|
+
const a = prev.offset;
|
|
29
|
+
const b = next.offset;
|
|
30
|
+
if (a === b)
|
|
31
|
+
return false;
|
|
32
|
+
if (a === undefined || b === undefined)
|
|
33
|
+
return true;
|
|
34
|
+
return a[0] !== b[0] || a[1] !== b[1] || a[2] !== b[2];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders `ctx.particles` — the game-reachable particle seam. Standing emitters
|
|
38
|
+
* become persistent `ParticleField`s (recreated only when their pool size changes,
|
|
39
|
+
* retuned in place otherwise), `follow` emitters track their entity's live pose
|
|
40
|
+
* each frame, and drained bursts become transient systems reaped once empty.
|
|
41
|
+
* Auto-mounted by the player shell; pools are capped by graphics quality via
|
|
42
|
+
* {@link resolveParticleBudget}.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export function WorldParticles({ quality, particleCap }) {
|
|
47
|
+
const ctx = useGameContext();
|
|
48
|
+
const emittersRef = useRef(new Map());
|
|
49
|
+
const burstsRef = useRef([]);
|
|
50
|
+
const [, setGeneration] = useState(0);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const emitters = emittersRef.current;
|
|
53
|
+
function sync() {
|
|
54
|
+
let changed = false;
|
|
55
|
+
const specs = ctx.particles.emitters();
|
|
56
|
+
const liveIds = new Set();
|
|
57
|
+
for (const spec of specs) {
|
|
58
|
+
liveIds.add(spec.id);
|
|
59
|
+
const poolMax = resolveParticleBudget(quality, spec.config.max, particleCap);
|
|
60
|
+
const current = emitters.get(spec.id);
|
|
61
|
+
if (current === undefined || current.poolMax !== poolMax) {
|
|
62
|
+
emitters.set(spec.id, {
|
|
63
|
+
spec,
|
|
64
|
+
poolMax,
|
|
65
|
+
system: createParticleSystem({ ...spec.config, max: poolMax, seed: spec.id }),
|
|
66
|
+
});
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
else if (current.spec !== spec) {
|
|
70
|
+
if (specNeedsRender(current.spec, spec))
|
|
71
|
+
changed = true;
|
|
72
|
+
current.system.configure({ ...spec.config, max: poolMax });
|
|
73
|
+
current.spec = spec;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const id of emitters.keys()) {
|
|
77
|
+
if (liveIds.has(id))
|
|
78
|
+
continue;
|
|
79
|
+
emitters.delete(id);
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
for (const burst of ctx.particles.drainBursts()) {
|
|
83
|
+
const poolMax = resolveParticleBudget(quality, Math.min(burst.count, burst.config.max ?? burst.count), particleCap);
|
|
84
|
+
const system = createParticleSystem({ ...burst.config, max: poolMax, seed: burst.seq });
|
|
85
|
+
system.emit(burst.count);
|
|
86
|
+
burstsRef.current.push({ seq: burst.seq, system, blending: burst.blending ?? "additive" });
|
|
87
|
+
if (burstsRef.current.length > MAX_CONCURRENT_BURSTS)
|
|
88
|
+
burstsRef.current.shift();
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
if (changed)
|
|
92
|
+
setGeneration((n) => n + 1);
|
|
93
|
+
}
|
|
94
|
+
sync();
|
|
95
|
+
const off = ctx.particles.subscribe(sync);
|
|
96
|
+
return () => {
|
|
97
|
+
off();
|
|
98
|
+
emitters.clear();
|
|
99
|
+
burstsRef.current = [];
|
|
100
|
+
};
|
|
101
|
+
}, [ctx, quality, particleCap]);
|
|
102
|
+
useFrame(() => {
|
|
103
|
+
for (const instance of emittersRef.current.values()) {
|
|
104
|
+
const follow = instance.spec.follow;
|
|
105
|
+
if (follow === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const entity = ctx.scene.entity.get(follow);
|
|
108
|
+
if (entity === null || entity === undefined)
|
|
109
|
+
continue;
|
|
110
|
+
const offset = instance.spec.offset ?? [0, 0, 0];
|
|
111
|
+
instance.system.configure({
|
|
112
|
+
position: [
|
|
113
|
+
entity.position[0] + offset[0],
|
|
114
|
+
entity.position[1] + offset[1],
|
|
115
|
+
entity.position[2] + offset[2],
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const finished = burstsRef.current.filter((burst) => burst.system.count() === 0);
|
|
120
|
+
if (finished.length > 0) {
|
|
121
|
+
burstsRef.current = burstsRef.current.filter((burst) => burst.system.count() > 0);
|
|
122
|
+
setGeneration((n) => n + 1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return (_jsxs(_Fragment, { children: [Array.from(emittersRef.current.values()).map((instance) => (_jsx("group", { position: instance.spec.follow === undefined && instance.spec.offset !== undefined
|
|
126
|
+
? instance.spec.offset
|
|
127
|
+
: [0, 0, 0], children: _jsx(ParticleField, { system: instance.system, blending: instance.spec.blending ?? "normal" }) }, instance.spec.id))), burstsRef.current.map((burst) => (_jsx(ParticleField, { system: burst.system, blending: burst.blending }, burst.seq)))] }));
|
|
128
|
+
}
|
|
@@ -16,6 +16,7 @@ export declare function WorldView({ entitySprites, entityModels, objectModels, o
|
|
|
16
16
|
selectedIds: ReadonlySet<string>;
|
|
17
17
|
hideLocalActor: boolean;
|
|
18
18
|
}): import("react").JSX.Element;
|
|
19
|
-
export declare function RemotePlayers({ rows }: {
|
|
19
|
+
export declare function RemotePlayers({ rows, presenceInterpolationMs }: {
|
|
20
20
|
rows: PresencePoseRow[];
|
|
21
|
+
presenceInterpolationMs?: number;
|
|
21
22
|
}): import("react").JSX.Element;
|