@jgengine/shell 0.18.0 → 0.19.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 +265 -2
- package/dist/GamePlayerShell.js +27 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +9 -8
- package/dist/Shell3dPresentation.js +57 -8
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +13 -2
- package/dist/audio/audioEngine.d.ts +15 -1
- package/dist/audio/audioEngine.js +132 -11
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +77 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +38 -6
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +74 -26
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +10 -5
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +22 -0
- package/dist/input/gamepadSource.js +153 -0
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +11 -8
- package/dist/postfx/PostProcessing.js +38 -16
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -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/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +39 -0
- package/dist/render/modelLoad.js +89 -1
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +58 -0
- package/dist/render/useFootIk.js +216 -0
- package/dist/render/useModelAnimation.d.ts +26 -0
- package/dist/render/useModelAnimation.js +184 -107
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +666 -241
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +10 -2
- package/dist/settings/appliedSettings.js +9 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +9 -2
- package/dist/useShellMultiplayerSync.js +30 -8
- 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 +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +20 -15
- package/skills/jgengine-ui/api.md +538 -326
- package/skills/jgengine-ui/capabilities.md +37 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -78
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -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`;
|
|
@@ -14,7 +14,7 @@ function QuickToggle({ row }) {
|
|
|
14
14
|
function QuickSelect({ row }) {
|
|
15
15
|
const options = row.options ?? [];
|
|
16
16
|
const current = String(row.value);
|
|
17
|
-
return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("div", { className: "flex
|
|
17
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("div", { className: "flex flex-wrap justify-end gap-1 rounded-md bg-neutral-950/60 p-0.5 ring-1 ring-white/10", children: options.map((option) => {
|
|
18
18
|
const selected = option.value === current;
|
|
19
19
|
return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: `rounded px-2 py-1 font-medium transition ${selected ? "bg-emerald-500 text-neutral-950" : "text-neutral-300 hover:bg-neutral-800"}`, children: option.label }, option.value));
|
|
20
20
|
}) })] }));
|
|
@@ -40,7 +40,7 @@ function ToggleRow({ row }) {
|
|
|
40
40
|
function SelectRow({ row }) {
|
|
41
41
|
const options = row.options ?? [];
|
|
42
42
|
const current = String(row.value);
|
|
43
|
-
return (_jsx(RowShell, { label: row.label, control: _jsx("div", { className: "flex
|
|
43
|
+
return (_jsx(RowShell, { label: row.label, control: _jsx("div", { className: "flex flex-wrap justify-end gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
|
|
44
44
|
const selected = option.value === current;
|
|
45
45
|
return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: "min-h-[34px] rounded-md px-3 text-sm font-medium transition", style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: option.label }, option.value));
|
|
46
46
|
}) }) }));
|
|
@@ -1,5 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { type GraphicsProfileOverrides } from "@jgengine/core/settings/graphicsSettings";
|
|
3
5
|
import { type TouchJoystickVariant, type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
4
6
|
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
5
7
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
@@ -9,12 +11,18 @@ export declare function useTouchStyle(store: SettingsStore, fallback: TouchStyle
|
|
|
9
11
|
/** Shell-internal: binds the SettingsStore joystick-variant choice (fixed vs floating) to the touch renderer. @internal */
|
|
10
12
|
export declare function useTouchJoystickVariant(store: SettingsStore): TouchJoystickVariant;
|
|
11
13
|
export declare function useSettingsRevision(store: SettingsStore): number;
|
|
12
|
-
|
|
14
|
+
/** The player's graphics choices as the shell applies them. */
|
|
15
|
+
export interface AppliedGraphicsSettings {
|
|
13
16
|
shadows: boolean;
|
|
14
17
|
dpr: number;
|
|
15
18
|
uiScale: number;
|
|
16
19
|
quality: GraphicsQuality;
|
|
17
|
-
|
|
20
|
+
profile: GraphicsProfile;
|
|
21
|
+
/** Frames-per-second cap; 0 renders every display refresh. */
|
|
22
|
+
frameRateLimit: number;
|
|
23
|
+
showFps: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean, overrides?: GraphicsProfileOverrides): AppliedGraphicsSettings;
|
|
18
26
|
export declare function AudioSettingsBridge({ store, engine, buses, }: {
|
|
19
27
|
store: SettingsStore;
|
|
20
28
|
engine: AudioEngine;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
-
import { busVolumeSettingId,
|
|
2
|
+
import { busVolumeSettingId, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { readGraphicsSettings } from "@jgengine/core/settings/graphicsSettings";
|
|
4
|
+
import { readFrameRateLimit } from "@jgengine/core/settings/frameRateLimit";
|
|
3
5
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANTS, TOUCH_STYLES, } from "@jgengine/core/input/touchScheme";
|
|
4
6
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
5
7
|
export const TOUCH_STYLE_AUTO = "auto";
|
|
@@ -26,16 +28,18 @@ export function useSettingsRevision(store) {
|
|
|
26
28
|
const getVersion = useCallback(() => versionRef.current, []);
|
|
27
29
|
return useSyncExternalStore(subscribe, getVersion, getVersion);
|
|
28
30
|
}
|
|
29
|
-
export function useGraphicsSettings(store, shadowsDefault) {
|
|
31
|
+
export function useGraphicsSettings(store, shadowsDefault, overrides) {
|
|
30
32
|
useSettingsRevision(store);
|
|
31
|
-
const
|
|
32
|
-
const quality = GRAPHICS_QUALITY_DPR[rawQuality] !== undefined ? rawQuality : "high";
|
|
33
|
+
const { quality, profile } = readGraphicsSettings(store, overrides);
|
|
33
34
|
const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
|
|
34
35
|
return {
|
|
35
36
|
shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
|
|
36
|
-
dpr:
|
|
37
|
+
dpr: profile.renderScale,
|
|
37
38
|
uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
|
|
38
39
|
quality,
|
|
40
|
+
profile,
|
|
41
|
+
frameRateLimit: readFrameRateLimit(store),
|
|
42
|
+
showFps: store.get(SETTING_IDS.graphicsShowFps, false),
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
export function AudioSettingsBridge({ store, engine, buses, }) {
|
|
@@ -2,6 +2,7 @@ import { bindingLabel, type ActionCodesMap } from "@jgengine/core/input/actionBi
|
|
|
2
2
|
import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
|
|
3
3
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
4
4
|
import { type GameSettingDef, type SettingCategory, type SettingCategoryDef } from "@jgengine/core/settings/settingsModel";
|
|
5
|
+
import { type GraphicsProfileOverrides } from "@jgengine/core/settings/graphicsSettings";
|
|
5
6
|
import { type SettingsActionView, type SettingsKeybindRow, type SettingsCategoryView, type SettingsController, type SettingsRow } from "@jgengine/react/settings";
|
|
6
7
|
export type { SettingsActionView, SettingsKeybindRow, SettingsCategoryView, SettingsController, SettingsRow };
|
|
7
8
|
export interface SettingsControllerInput {
|
|
@@ -11,6 +12,8 @@ export interface SettingsControllerInput {
|
|
|
11
12
|
categories: readonly SettingCategoryDef[];
|
|
12
13
|
hide: readonly SettingCategory[];
|
|
13
14
|
fovEnabled: boolean;
|
|
15
|
+
/** The game's per-tier profile overrides (`defineGame({ graphics })`), so tier defaults match what renders. */
|
|
16
|
+
graphics?: GraphicsProfileOverrides;
|
|
14
17
|
hideBindings: readonly string[];
|
|
15
18
|
/** Show the touch-controls skin selector in Controls (touch device with a dock). */
|
|
16
19
|
touchStyle: boolean;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
2
|
-
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId,
|
|
2
|
+
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId, DEFAULT_GRAPHICS_SHADOWS, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { ANISOTROPY_OPTIONS, applyGraphicsQuality, GRAPHICS_POST_STAGE_LABELS, GRAPHICS_POST_STAGES, graphicsPostStageSettingId, readGraphicsSettings, RENDER_SCALE_MAX, RENDER_SCALE_MIN, RENDER_SCALE_STEP, } from "@jgengine/core/settings/graphicsSettings";
|
|
4
|
+
import { FRAME_RATE_LIMIT_DISPLAY, FRAME_RATE_LIMIT_OPTIONS } from "@jgengine/core/settings/frameRateLimit";
|
|
3
5
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANT_OPTIONS, TOUCH_STYLE_OPTIONS, } from "@jgengine/core/input/touchScheme";
|
|
4
6
|
import { TOUCH_STYLE_AUTO, useSettingsRevision } from "./appliedSettings.js";
|
|
5
7
|
import { useSettingsStore, } from "@jgengine/react/settings";
|
|
@@ -61,14 +63,42 @@ export function useSettingsCategories(config) {
|
|
|
61
63
|
}),
|
|
62
64
|
...extrasFor("sound"),
|
|
63
65
|
];
|
|
66
|
+
const graphics = readGraphicsSettings(store, config.graphics);
|
|
64
67
|
const graphicsRows = [
|
|
65
68
|
{
|
|
66
69
|
id: SETTING_IDS.graphicsQuality,
|
|
67
70
|
label: "Quality",
|
|
68
71
|
kind: "select",
|
|
69
|
-
value:
|
|
72
|
+
value: graphics.quality,
|
|
70
73
|
options: GRAPHICS_QUALITY_OPTIONS,
|
|
71
|
-
set: (value) => store
|
|
74
|
+
set: (value) => applyGraphicsQuality(store, String(value), config.graphics),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: SETTING_IDS.graphicsRenderScale,
|
|
78
|
+
label: "Render scale",
|
|
79
|
+
kind: "slider",
|
|
80
|
+
value: graphics.profile.renderScale,
|
|
81
|
+
min: RENDER_SCALE_MIN,
|
|
82
|
+
max: RENDER_SCALE_MAX,
|
|
83
|
+
step: RENDER_SCALE_STEP,
|
|
84
|
+
format: percent,
|
|
85
|
+
set: (value) => store.set(SETTING_IDS.graphicsRenderScale, value),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: SETTING_IDS.graphicsFrameRateLimit,
|
|
89
|
+
label: "Frame rate limit",
|
|
90
|
+
kind: "select",
|
|
91
|
+
value: store.get(SETTING_IDS.graphicsFrameRateLimit, FRAME_RATE_LIMIT_DISPLAY),
|
|
92
|
+
options: FRAME_RATE_LIMIT_OPTIONS,
|
|
93
|
+
set: (value) => store.set(SETTING_IDS.graphicsFrameRateLimit, String(value)),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: SETTING_IDS.graphicsAnisotropy,
|
|
97
|
+
label: "Texture filtering",
|
|
98
|
+
kind: "select",
|
|
99
|
+
value: String(graphics.profile.anisotropy),
|
|
100
|
+
options: ANISOTROPY_OPTIONS,
|
|
101
|
+
set: (value) => store.set(SETTING_IDS.graphicsAnisotropy, String(value)),
|
|
72
102
|
},
|
|
73
103
|
{
|
|
74
104
|
id: SETTING_IDS.graphicsShadows,
|
|
@@ -77,6 +107,13 @@ export function useSettingsCategories(config) {
|
|
|
77
107
|
value: store.get(SETTING_IDS.graphicsShadows, DEFAULT_GRAPHICS_SHADOWS),
|
|
78
108
|
set: (value) => store.set(SETTING_IDS.graphicsShadows, value),
|
|
79
109
|
},
|
|
110
|
+
...GRAPHICS_POST_STAGES.map((stage) => ({
|
|
111
|
+
id: graphicsPostStageSettingId(stage),
|
|
112
|
+
label: GRAPHICS_POST_STAGE_LABELS[stage],
|
|
113
|
+
kind: "toggle",
|
|
114
|
+
value: graphics.profile.postStages[stage],
|
|
115
|
+
set: (value) => store.set(graphicsPostStageSettingId(stage), value),
|
|
116
|
+
})),
|
|
80
117
|
{
|
|
81
118
|
id: SETTING_IDS.graphicsUiScale,
|
|
82
119
|
label: "UI scale",
|
|
@@ -88,6 +125,13 @@ export function useSettingsCategories(config) {
|
|
|
88
125
|
format: percent,
|
|
89
126
|
set: (value) => store.set(SETTING_IDS.graphicsUiScale, value),
|
|
90
127
|
},
|
|
128
|
+
{
|
|
129
|
+
id: SETTING_IDS.graphicsShowFps,
|
|
130
|
+
label: "Show FPS",
|
|
131
|
+
kind: "toggle",
|
|
132
|
+
value: store.get(SETTING_IDS.graphicsShowFps, false),
|
|
133
|
+
set: (value) => store.set(SETTING_IDS.graphicsShowFps, value),
|
|
134
|
+
},
|
|
91
135
|
...extrasFor("graphics"),
|
|
92
136
|
];
|
|
93
137
|
const gameplayRows = [
|
|
@@ -5,6 +5,7 @@ import * as THREE from "three";
|
|
|
5
5
|
import { sharedGltfLoader } from "../render/modelLoad.js";
|
|
6
6
|
import { buildScatterModelSources, disposeScatterModelSources, } from "../scatter/scatterModels.js";
|
|
7
7
|
import { composeBuildingKitMatrix, measureBuildingKitModel, } from "./buildingKitFit.js";
|
|
8
|
+
import { applyBuildingSurface, surfaceKey, useBuildingSurfaceTextures } from "./buildingSurface.js";
|
|
8
9
|
function KitSourceInstances({ source, material, matrices, }) {
|
|
9
10
|
const meshRef = useRef(null);
|
|
10
11
|
useEffect(() => {
|
|
@@ -22,16 +23,21 @@ function KitSourceInstances({ source, material, matrices, }) {
|
|
|
22
23
|
}, [matrices, source]);
|
|
23
24
|
return (_jsx("instancedMesh", { ref: meshRef, args: [source.geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
+
function styleOne(material, tint, surface, textures) {
|
|
26
27
|
const clone = material.clone();
|
|
27
28
|
const tintable = clone;
|
|
28
|
-
if (tintable.color !== undefined)
|
|
29
|
-
tintable.color.copy(
|
|
29
|
+
if (tint !== undefined && tintable.color !== undefined)
|
|
30
|
+
tintable.color.copy(tint);
|
|
31
|
+
if (surface !== undefined && clone.isMeshStandardMaterial === true) {
|
|
32
|
+
applyBuildingSurface(clone, surface, textures);
|
|
33
|
+
}
|
|
30
34
|
return clone;
|
|
31
35
|
}
|
|
32
|
-
function
|
|
33
|
-
const color = new THREE.Color(tint);
|
|
34
|
-
return Array.isArray(material)
|
|
36
|
+
function styleMaterial(material, tint, surface, textures) {
|
|
37
|
+
const color = tint === "" ? undefined : new THREE.Color(tint);
|
|
38
|
+
return Array.isArray(material)
|
|
39
|
+
? material.map((entry) => styleOne(entry, color, surface, textures))
|
|
40
|
+
: styleOne(material, color, surface, textures);
|
|
35
41
|
}
|
|
36
42
|
function disposeMaterial(material) {
|
|
37
43
|
if (Array.isArray(material))
|
|
@@ -52,27 +58,25 @@ export function BuildingKitBatch({ url, instances }) {
|
|
|
52
58
|
useEffect(() => () => disposeScatterModelSources(root), [root]);
|
|
53
59
|
const bounds = useMemo(() => measureBuildingKitModel(root), [root]);
|
|
54
60
|
const groups = useMemo(() => {
|
|
55
|
-
const
|
|
61
|
+
const byKey = new Map();
|
|
56
62
|
for (const instance of instances) {
|
|
57
63
|
const matrix = composeBuildingKitMatrix(instance, bounds, new THREE.Matrix4());
|
|
58
64
|
const tint = instance.part.tint ?? "";
|
|
59
|
-
const
|
|
65
|
+
const surface = instance.part.material;
|
|
66
|
+
const key = `${tint}|${surfaceKey(surface)}`;
|
|
67
|
+
const bucket = byKey.get(key);
|
|
60
68
|
if (bucket === undefined)
|
|
61
|
-
|
|
69
|
+
byKey.set(key, { key, tint, surface, matrices: [matrix] });
|
|
62
70
|
else
|
|
63
|
-
bucket.push(matrix);
|
|
71
|
+
bucket.matrices.push(matrix);
|
|
64
72
|
}
|
|
65
|
-
return [...
|
|
73
|
+
return [...byKey.values()];
|
|
66
74
|
}, [instances, bounds]);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return map;
|
|
75
|
-
}, [groups, sources]);
|
|
76
|
-
useEffect(() => () => tinted.forEach((list) => list.forEach(disposeMaterial)), [tinted]);
|
|
77
|
-
return (_jsx(_Fragment, { children: groups.map(([tint, matrices]) => sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: tinted.get(tint)?.[index] ?? source.material, matrices: matrices }, `${tint}:${index}`)))) }));
|
|
75
|
+
return (_jsx(_Fragment, { children: groups.map((group) => group.tint === "" && group.surface === undefined ? (sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: source.material, matrices: group.matrices }, `${group.key}:${index}`)))) : (_jsx(StyledKitSources, { sources: sources, group: group }, group.key))) }));
|
|
76
|
+
}
|
|
77
|
+
function StyledKitSources({ sources, group }) {
|
|
78
|
+
const textures = useBuildingSurfaceTextures(group.surface);
|
|
79
|
+
const styled = useMemo(() => sources.map((source) => styleMaterial(source.material, group.tint, group.surface, textures)), [sources, group.tint, group.surface, textures]);
|
|
80
|
+
useEffect(() => () => styled.forEach(disposeMaterial), [styled]);
|
|
81
|
+
return (_jsx(_Fragment, { children: sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: styled[index], matrices: group.matrices }, index))) }));
|
|
78
82
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { type BuildingKitSlot, type BuildingSurface } from "@jgengine/core/world/buildings";
|
|
3
3
|
import type { BuildingKit } from "@jgengine/core/world/buildingKit";
|
|
4
4
|
export type BuildingFacade = "front" | "back" | "left" | "right" | "roof";
|
|
5
5
|
export type BuildingPartKind = "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner";
|
|
@@ -17,20 +17,8 @@ export interface GeneratedBuildingData {
|
|
|
17
17
|
id: string;
|
|
18
18
|
parts: readonly BuildingPartPlacement[];
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
window?: string;
|
|
23
|
-
awning?: string;
|
|
24
|
-
airConditioner?: string;
|
|
25
|
-
clothesline?: string;
|
|
26
|
-
storefront?: string;
|
|
27
|
-
shutter?: string;
|
|
28
|
-
storeSign?: string;
|
|
29
|
-
roof?: string;
|
|
30
|
-
roofProp?: string;
|
|
31
|
-
guardrail?: string;
|
|
32
|
-
corner?: string;
|
|
33
|
-
}
|
|
20
|
+
/** Per part kind: a hex colour, or a colour plus tiled PBR maps (`BuildingSurfaceMaterial`). */
|
|
21
|
+
export type BuildingMaterialPalette = Partial<Record<BuildingPartKind, BuildingSurface>>;
|
|
34
22
|
export interface BuildingKitRenderer {
|
|
35
23
|
renderPart?: (part: BuildingPartPlacement) => ReactNode | undefined;
|
|
36
24
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
|
+
import { buildingSurfaceColor, resolveBuildingSurface, } from "@jgengine/core/world/buildings";
|
|
4
5
|
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
import { BuildingKitBatch } from "./BuildingKitBatch.js";
|
|
6
7
|
import { bucketBuildingParts, normalFor, outwardOffset } from "./buildingKitFit.js";
|
|
8
|
+
import { applyBuildingSurface, useBuildingSurfaceTextures } from "./buildingSurface.js";
|
|
7
9
|
const DEFAULT_PALETTE = {
|
|
8
10
|
wall: "#83766a",
|
|
9
11
|
window: "#8ecae6",
|
|
@@ -18,8 +20,12 @@ const DEFAULT_PALETTE = {
|
|
|
18
20
|
guardrail: "#a8a29e",
|
|
19
21
|
corner: "#6b6258",
|
|
20
22
|
};
|
|
23
|
+
function surfaceFor(kind, palette) {
|
|
24
|
+
const surface = resolveBuildingSurface(palette?.[kind] ?? DEFAULT_PALETTE[kind]);
|
|
25
|
+
return surface.color === undefined ? { ...surface, color: DEFAULT_PALETTE[kind] } : surface;
|
|
26
|
+
}
|
|
21
27
|
function colorFor(kind, palette) {
|
|
22
|
-
return palette?.[kind]
|
|
28
|
+
return buildingSurfaceColor(palette?.[kind], DEFAULT_PALETTE[kind]);
|
|
23
29
|
}
|
|
24
30
|
function materialFor(part, palette) {
|
|
25
31
|
const color = colorFor(part.kind, palette);
|
|
@@ -34,22 +40,22 @@ function materialFor(part, palette) {
|
|
|
34
40
|
}
|
|
35
41
|
return _jsx("meshStandardMaterial", { color: color, roughness: 0.88, metalness: 0 });
|
|
36
42
|
}
|
|
37
|
-
function batchMaterialFor(kind,
|
|
38
|
-
const color =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
return new THREE.MeshStandardMaterial({ color, roughness: 0.88, metalness: 0 });
|
|
43
|
+
function batchMaterialFor(kind, surface, textures) {
|
|
44
|
+
const color = surface.color ?? DEFAULT_PALETTE[kind];
|
|
45
|
+
const base = kind === "window" || kind === "storefront"
|
|
46
|
+
? new THREE.MeshPhysicalMaterial({ color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 })
|
|
47
|
+
: kind === "storeSign"
|
|
48
|
+
? new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 })
|
|
49
|
+
: kind === "roofProp"
|
|
50
|
+
? new THREE.MeshStandardMaterial({ color, roughness: 0.65, metalness: 0.1 })
|
|
51
|
+
: new THREE.MeshStandardMaterial({ color, roughness: 0.88, metalness: 0 });
|
|
52
|
+
return applyBuildingSurface(base, surface, textures);
|
|
49
53
|
}
|
|
50
|
-
function BuildingKindBatch({ kind, matrices, palette, geometry
|
|
54
|
+
function BuildingKindBatch({ kind, matrices, palette, geometry }) {
|
|
55
|
+
const surface = useMemo(() => surfaceFor(kind, palette), [kind, palette]);
|
|
56
|
+
const textures = useBuildingSurfaceTextures(surface);
|
|
51
57
|
const meshRef = useRef(null);
|
|
52
|
-
const material = useDisposable(() => batchMaterialFor(kind,
|
|
58
|
+
const material = useDisposable(() => batchMaterialFor(kind, surface, textures), [kind, surface, textures]);
|
|
53
59
|
useLayoutEffect(() => {
|
|
54
60
|
const mesh = meshRef.current;
|
|
55
61
|
if (mesh === null)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { BuildingSurfaceMaps, BuildingSurfaceMaterial } from "@jgengine/core/world/buildings";
|
|
3
|
+
import type { MaterialOverrideTextures } from "../materialOverride.js";
|
|
4
|
+
/** A surface that names at least one map URL. */
|
|
5
|
+
export type TexturedBuildingSurface = BuildingSurfaceMaterial & {
|
|
6
|
+
maps: BuildingSurfaceMaps;
|
|
7
|
+
};
|
|
8
|
+
/** True when the surface names at least one map URL, so the batch needs the textured path. */
|
|
9
|
+
export declare function surfaceHasMaps(surface: BuildingSurfaceMaterial | undefined): surface is TexturedBuildingSurface;
|
|
10
|
+
/** A stable key for bucketing instances that share one surface (same colour, maps, tiling, and response). */
|
|
11
|
+
export declare function surfaceKey(surface: BuildingSurfaceMaterial | undefined): string;
|
|
12
|
+
/**
|
|
13
|
+
* Loads a surface's PBR maps and returns per-caller clones configured to tile `repeat` times per
|
|
14
|
+
* slot, or `undefined` until the first map arrives so the batch draws its flat colour meanwhile.
|
|
15
|
+
* Clones share the decoded image, so two kinds using one map at different repeats cost one
|
|
16
|
+
* download. Maps whose file is missing are skipped rather than failing the whole surface.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function useBuildingSurfaceTextures(surface: BuildingSurfaceMaterial | undefined): MaterialOverrideTextures | undefined;
|
|
20
|
+
/** Applies a surface's colour, response, and loaded maps onto a standard or physical material in place. @internal */
|
|
21
|
+
export declare function applyBuildingSurface(material: THREE.MeshStandardMaterial, surface: BuildingSurfaceMaterial, textures: MaterialOverrideTextures | undefined): THREE.MeshStandardMaterial;
|