@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/GamePlayerShell.js +27 -4
  3. package/dist/JoinGate.d.ts +17 -0
  4. package/dist/JoinGate.js +14 -0
  5. package/dist/Shell3dPresentation.d.ts +9 -8
  6. package/dist/Shell3dPresentation.js +57 -8
  7. package/dist/ShellChrome.d.ts +2 -0
  8. package/dist/ShellChrome.js +6 -0
  9. package/dist/ShellHudPresentation.d.ts +9 -1
  10. package/dist/ShellHudPresentation.js +21 -0
  11. package/dist/TerritoryOverlay.d.ts +21 -0
  12. package/dist/TerritoryOverlay.js +9 -0
  13. package/dist/audio/AudioComponents.js +13 -2
  14. package/dist/audio/audioEngine.d.ts +15 -1
  15. package/dist/audio/audioEngine.js +132 -11
  16. package/dist/audio/audioWire.d.ts +3 -0
  17. package/dist/audio/audioWire.js +7 -1
  18. package/dist/audio/loopParams.d.ts +39 -0
  19. package/dist/audio/loopParams.js +43 -0
  20. package/dist/camera/GameCameraRig.js +1 -1
  21. package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
  22. package/dist/camera/GameFirstPersonCamera.js +52 -15
  23. package/dist/camera/Viewports.d.ts +34 -0
  24. package/dist/camera/Viewports.js +123 -0
  25. package/dist/camera/cameraRigs.js +77 -62
  26. package/dist/camera/orbitCameraMath.js +2 -2
  27. package/dist/camera/rigMath.d.ts +82 -4
  28. package/dist/camera/rigMath.js +133 -15
  29. package/dist/defineGame.d.ts +9 -0
  30. package/dist/defineGame.js +38 -6
  31. package/dist/devtools/DevtoolsOverlay.js +2 -0
  32. package/dist/devtools/KeysPanel.js +3 -3
  33. package/dist/devtools/agentBridge.d.ts +6 -0
  34. package/dist/devtools/agentBridge.js +15 -1
  35. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  36. package/dist/diagnostics/FpsCounter.js +32 -0
  37. package/dist/drivers/FrameDriver.d.ts +5 -1
  38. package/dist/drivers/FrameDriver.js +74 -26
  39. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  40. package/dist/drivers/FrameRateLimiter.js +39 -0
  41. package/dist/drivers/HudOnlyDriver.js +10 -5
  42. package/dist/environment/DayNightSky.js +5 -5
  43. package/dist/environment/Daylight.d.ts +35 -1
  44. package/dist/environment/Daylight.js +98 -28
  45. package/dist/environment/EnvironmentScene.js +1 -1
  46. package/dist/environment/VolumetricClouds.js +20 -2
  47. package/dist/environment/daylightCycle.d.ts +20 -0
  48. package/dist/environment/daylightCycle.js +59 -7
  49. package/dist/environment/sunShadowMath.d.ts +45 -0
  50. package/dist/environment/sunShadowMath.js +50 -0
  51. package/dist/input/gamepadMerge.d.ts +10 -0
  52. package/dist/input/gamepadMerge.js +12 -0
  53. package/dist/input/gamepadPoll.d.ts +47 -0
  54. package/dist/input/gamepadPoll.js +142 -0
  55. package/dist/input/gamepadSource.d.ts +22 -0
  56. package/dist/input/gamepadSource.js +153 -0
  57. package/dist/input/mouseLook.js +2 -1
  58. package/dist/input/padHaptics.d.ts +17 -0
  59. package/dist/input/padHaptics.js +32 -0
  60. package/dist/input/pointerLock.d.ts +11 -0
  61. package/dist/input/pointerLock.js +19 -0
  62. package/dist/materialOverride.d.ts +4 -1
  63. package/dist/materialOverride.js +10 -1
  64. package/dist/postfx/PostProcessing.d.ts +11 -8
  65. package/dist/postfx/PostProcessing.js +38 -16
  66. package/dist/postfx/gradeShader.js +13 -0
  67. package/dist/postfx/stylizePass.d.ts +23 -0
  68. package/dist/postfx/stylizePass.js +216 -0
  69. package/dist/render/CascadedShadows.d.ts +11 -0
  70. package/dist/render/CascadedShadows.js +19 -8
  71. package/dist/render/EnvironmentLighting.d.ts +4 -1
  72. package/dist/render/EnvironmentLighting.js +45 -9
  73. package/dist/render/SceneLighting.d.ts +16 -2
  74. package/dist/render/SceneLighting.js +56 -6
  75. package/dist/render/SceneModels.js +45 -9
  76. package/dist/render/SkinnedInstances.d.ts +91 -0
  77. package/dist/render/SkinnedInstances.js +249 -0
  78. package/dist/render/TextureFiltering.d.ts +16 -0
  79. package/dist/render/TextureFiltering.js +64 -0
  80. package/dist/render/displayColor.d.ts +33 -0
  81. package/dist/render/displayColor.js +138 -0
  82. package/dist/render/modelLoad.d.ts +39 -0
  83. package/dist/render/modelLoad.js +89 -1
  84. package/dist/render/modelRender.d.ts +3 -0
  85. package/dist/render/modelRender.js +6 -2
  86. package/dist/render/resolveModel.js +4 -0
  87. package/dist/render/useFootIk.d.ts +58 -0
  88. package/dist/render/useFootIk.js +216 -0
  89. package/dist/render/useModelAnimation.d.ts +26 -0
  90. package/dist/render/useModelAnimation.js +184 -107
  91. package/dist/scene/AuthoredScene.d.ts +9 -0
  92. package/dist/scene/AuthoredScene.js +17 -1
  93. package/dist/scene/CityRenderer.js +666 -241
  94. package/dist/scene/cityWindowMaterial.d.ts +8 -6
  95. package/dist/scene/cityWindowMaterial.js +80 -21
  96. package/dist/settings/QuickControls.js +1 -1
  97. package/dist/settings/SettingsMenu.js +1 -1
  98. package/dist/settings/appliedSettings.d.ts +10 -2
  99. package/dist/settings/appliedSettings.js +9 -5
  100. package/dist/settings/settingsController.d.ts +3 -0
  101. package/dist/settings/settingsController.js +47 -3
  102. package/dist/structures/BuildingKitBatch.js +26 -22
  103. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  104. package/dist/structures/GeneratedBuilding.js +21 -15
  105. package/dist/structures/buildingSurface.d.ts +21 -0
  106. package/dist/structures/buildingSurface.js +134 -0
  107. package/dist/terrain/terrainDetailMaterial.js +7 -3
  108. package/dist/useShellMultiplayerSync.d.ts +9 -2
  109. package/dist/useShellMultiplayerSync.js +30 -8
  110. package/dist/visibility/CullingProvider.d.ts +2 -1
  111. package/dist/visibility/CullingProvider.js +7 -2
  112. package/dist/world/GridWorldScene.js +4 -0
  113. package/dist/world/TileLayerRenderer.d.ts +9 -0
  114. package/dist/world/TileLayerRenderer.js +64 -0
  115. package/dist/world/WorldParticles.d.ts +31 -0
  116. package/dist/world/WorldParticles.js +128 -0
  117. package/dist/world/WorldScene.d.ts +2 -1
  118. package/dist/world/WorldScene.js +46 -4
  119. package/dist/world/entityPose.d.ts +4 -0
  120. package/dist/world/entityPose.js +9 -0
  121. package/dist/world/tileLayerInstances.d.ts +11 -0
  122. package/dist/world/tileLayerInstances.js +28 -0
  123. package/dist/worldSync.d.ts +5 -1
  124. package/dist/worldSync.js +25 -4
  125. package/package.json +4 -4
  126. package/skills/ce-handoff/SKILL.md +2 -2
  127. package/skills/game-design/SKILL.md +11 -1
  128. package/skills/jgengine/SKILL.md +9 -5
  129. package/skills/jgengine/api.md +205 -101
  130. package/skills/jgengine/capabilities.md +56 -0
  131. package/skills/jgengine/recipes/minimal-game.md +3 -0
  132. package/skills/jgengine-ui/SKILL.md +20 -15
  133. package/skills/jgengine-ui/api.md +538 -326
  134. package/skills/jgengine-ui/capabilities.md +37 -0
  135. package/skills/jgengine-ui/reference-primitives.md +89 -0
  136. package/skills/jgengine-ui/reference.md +2 -78
  137. package/skills/jgengine-world/SKILL.md +21 -3
  138. package/skills/jgengine-world/api.md +1172 -625
  139. package/skills/jgengine-world/capabilities.md +229 -3
  140. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  141. package/skills/jgengine-world/recipes/controls.md +44 -0
  142. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  143. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  144. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  145. package/skills/jgengine-world/reference.md +31 -2
  146. package/skills/jgengine-world/visibility.md +112 -0
  147. package/skills/level-design/SKILL.md +1 -1
  148. 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 procedural window strips on the side faces of every instanced unit box — floor bands from
5
- * the district's real `floorHeight`, window columns on a fixed pitch, and a per-cell hash that
6
- * dims/lights individual windows so no two facades read identical. Ground floors (plus any buried
7
- * foundation, via the `instanceGroundOffset` attribute) stay solid wall so street level reads as
8
- * storefront/lobby rather than floating glass. Same `onBeforeCompile` idiom as the grass/terrain
9
- * materials; instance colors keep tinting the wall exactly like the plain massing material.
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 procedural window strips on the side faces of every instanced unit box — floor bands from
5
- * the district's real `floorHeight`, window columns on a fixed pitch, and a per-cell hash that
6
- * dims/lights individual windows so no two facades read identical. Ground floors (plus any buried
7
- * foundation, via the `instanceGroundOffset` attribute) stay solid wall so street level reads as
8
- * storefront/lobby rather than floating glass. Same `onBeforeCompile` idiom as the grass/terrain
9
- * materials; instance colors keep tinting the wall exactly like the plain massing material.
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
- float along = abs(vCityNormal.x) > 0.5 ? (vCityLocal.z + 0.5) * vCityScale.z : (vCityLocal.x + 0.5) * vCityScale.x;
59
- float faceWidth = abs(vCityNormal.x) > 0.5 ? vCityScale.z : vCityScale.x;
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 pitch = 2.4;
62
- float floorT = fract(up / uFloorHeight);
63
- float colT = fract(along / pitch);
64
- float floorIndex = floor(up / uFloorHeight);
65
- float colIndex = floor(along / pitch);
66
- bool window = up > uFloorHeight * 1.05 && floorT > 0.32 && floorT < 0.78 && colT > 0.2 && colT < 0.8
67
- && along > 0.7 && along < faceWidth - 0.7;
68
- if (window) {
69
- float cell = cityHash(vec2(floorIndex, colIndex) + vec2(vCityScale.x, vCityScale.z));
70
- vec3 pane = uWindowColor * mix(0.55, 1.25, cell);
71
- diffuseColor.rgb = mix(diffuseColor.rgb, pane, 0.88);
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
- // A thin darker plinth line right above grade grounds the facade.
74
- if (up > 0.0 && up < 0.35) diffuseColor.rgb *= 0.8;
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 shrink-0 gap-1 rounded-md bg-neutral-950/60 p-0.5 ring-1 ring-white/10", children: options.map((option) => {
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 shrink-0 gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
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
- export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
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, 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";
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 rawQuality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
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: GRAPHICS_QUALITY_DPR[quality],
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, DEFAULT_GRAPHICS_QUALITY, 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";
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: store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY),
72
+ value: graphics.quality,
70
73
  options: GRAPHICS_QUALITY_OPTIONS,
71
- set: (value) => store.set(SETTING_IDS.graphicsQuality, value),
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 tintOne(material, color) {
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(color);
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 tintMaterial(material, tint) {
33
- const color = new THREE.Color(tint);
34
- return Array.isArray(material) ? material.map((entry) => tintOne(entry, color)) : tintOne(material, color);
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 byTint = new Map();
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 bucket = byTint.get(tint);
65
+ const surface = instance.part.material;
66
+ const key = `${tint}|${surfaceKey(surface)}`;
67
+ const bucket = byKey.get(key);
60
68
  if (bucket === undefined)
61
- byTint.set(tint, [matrix]);
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 [...byTint.entries()];
73
+ return [...byKey.values()];
66
74
  }, [instances, bounds]);
67
- const tinted = useMemo(() => {
68
- const map = new Map();
69
- for (const [tint] of groups) {
70
- if (tint === "")
71
- continue;
72
- map.set(tint, sources.map((source) => tintMaterial(source.material, tint)));
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 { BuildingKitSlot } from "@jgengine/core/world/buildings";
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
- export interface BuildingMaterialPalette {
21
- wall?: string;
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] ?? DEFAULT_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, palette) {
38
- const color = colorFor(kind, palette);
39
- if (kind === "window" || kind === "storefront") {
40
- return new THREE.MeshPhysicalMaterial({ color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 });
41
- }
42
- if (kind === "storeSign") {
43
- return new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 });
44
- }
45
- if (kind === "roofProp") {
46
- return new THREE.MeshStandardMaterial({ color, roughness: 0.65, metalness: 0.1 });
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, palette), [kind, palette]);
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;