@jgengine/shell 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +11 -0
  3. package/dist/GamePlayerShell.d.ts +10 -22
  4. package/dist/GamePlayerShell.js +709 -264
  5. package/dist/GameUiPreview.js +2 -1
  6. package/dist/audio/AudioComponents.js +25 -8
  7. package/dist/audio/audioEngine.d.ts +9 -0
  8. package/dist/audio/audioEngine.js +37 -0
  9. package/dist/audio/musicDirector.d.ts +31 -0
  10. package/dist/audio/musicDirector.js +125 -0
  11. package/dist/audio/musicVoices.d.ts +10 -0
  12. package/dist/audio/musicVoices.js +338 -0
  13. package/dist/audio/synthEngine.d.ts +9 -0
  14. package/dist/audio/synthEngine.js +57 -0
  15. package/dist/behaviour.d.ts +5 -24
  16. package/dist/behaviour.js +14 -50
  17. package/dist/behaviourAttach.d.ts +9 -0
  18. package/dist/behaviourAttach.js +32 -0
  19. package/dist/behaviourDriver.d.ts +7 -0
  20. package/dist/behaviourDriver.js +21 -0
  21. package/dist/camera/GameCameraRig.js +1 -1
  22. package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
  23. package/dist/camera/GameFirstPersonCamera.js +35 -4
  24. package/dist/camera/GameInspectionCamera.js +12 -1
  25. package/dist/camera/GameOrbitCamera.js +44 -1
  26. package/dist/camera/PlayerFov.d.ts +18 -0
  27. package/dist/camera/PlayerFov.js +48 -0
  28. package/dist/camera/cameraBlendMath.d.ts +13 -0
  29. package/dist/camera/cameraBlendMath.js +36 -0
  30. package/dist/camera/cameraRigs.d.ts +3 -0
  31. package/dist/camera/cameraRigs.js +36 -33
  32. package/dist/camera/fovPreference.d.ts +25 -0
  33. package/dist/camera/fovPreference.js +73 -0
  34. package/dist/camera/index.d.ts +3 -0
  35. package/dist/camera/index.js +3 -0
  36. package/dist/camera/orbitCameraMath.d.ts +18 -0
  37. package/dist/camera/orbitCameraMath.js +6 -1
  38. package/dist/camera/rigMath.d.ts +10 -0
  39. package/dist/camera/rigMath.js +36 -1
  40. package/dist/camera/shakeChannel.d.ts +3 -17
  41. package/dist/camera/shakeChannel.js +2 -21
  42. package/dist/camera/shakeChannelMath.d.ts +8 -0
  43. package/dist/camera/shakeChannelMath.js +21 -0
  44. package/dist/cartridge.d.ts +145 -0
  45. package/dist/cartridge.js +246 -0
  46. package/dist/commandSink.d.ts +20 -0
  47. package/dist/commandSink.js +27 -0
  48. package/dist/defineGame.js +4 -1
  49. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  50. package/dist/devtools/CollisionDebugWorld.js +181 -0
  51. package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
  52. package/dist/devtools/DevtoolsOverlay.js +383 -41
  53. package/dist/devtools/collisionDebug.d.ts +57 -0
  54. package/dist/devtools/collisionDebug.js +127 -0
  55. package/dist/devtools/collisionDebugMath.d.ts +103 -0
  56. package/dist/devtools/collisionDebugMath.js +129 -0
  57. package/dist/environment/Daylight.d.ts +20 -8
  58. package/dist/environment/Daylight.js +63 -15
  59. package/dist/environment/EnvironmentScene.js +91 -35
  60. package/dist/environment/RoadRibbons.d.ts +7 -0
  61. package/dist/environment/RoadRibbons.js +57 -0
  62. package/dist/environment/groundPadMath.d.ts +2 -2
  63. package/dist/environment/groundPadMath.js +1 -1
  64. package/dist/environment/index.d.ts +2 -1
  65. package/dist/environment/index.js +1 -0
  66. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  67. package/dist/environment/skyLightingPolicy.js +6 -0
  68. package/dist/input/mouseLook.d.ts +27 -0
  69. package/dist/input/mouseLook.js +35 -0
  70. package/dist/inputSink.d.ts +18 -0
  71. package/dist/inputSink.js +35 -0
  72. package/dist/materialOverride.d.ts +4 -6
  73. package/dist/materialOverride.js +12 -16
  74. package/dist/multiplayer.js +7 -3
  75. package/dist/pointer/PointerProbe.js +6 -2
  76. package/dist/pointer/pointerService.d.ts +3 -0
  77. package/dist/pointer/pointerService.js +6 -0
  78. package/dist/postfx/PostProcessing.d.ts +10 -0
  79. package/dist/postfx/PostProcessing.js +82 -0
  80. package/dist/postfx/gradeShader.d.ts +4 -0
  81. package/dist/postfx/gradeShader.js +64 -0
  82. package/dist/render/modelRender.d.ts +10 -1
  83. package/dist/render/modelRender.js +32 -5
  84. package/dist/render/resolveModel.d.ts +14 -0
  85. package/dist/render/resolveModel.js +24 -0
  86. package/dist/settings/QuickControls.d.ts +4 -0
  87. package/dist/settings/QuickControls.js +42 -0
  88. package/dist/settings/SettingsChrome.d.ts +1 -0
  89. package/dist/settings/SettingsChrome.js +10 -0
  90. package/dist/settings/SettingsMenu.d.ts +6 -0
  91. package/dist/settings/SettingsMenu.js +148 -0
  92. package/dist/settings/SettingsRuntime.d.ts +11 -0
  93. package/dist/settings/SettingsRuntime.js +19 -0
  94. package/dist/settings/appliedSettings.d.ts +14 -0
  95. package/dist/settings/appliedSettings.js +27 -0
  96. package/dist/settings/settingsController.d.ts +20 -0
  97. package/dist/settings/settingsController.js +165 -0
  98. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  99. package/dist/structures/GeneratedBuilding.js +96 -8
  100. package/dist/structures/index.d.ts +1 -1
  101. package/dist/structures/index.js +1 -1
  102. package/dist/terrain/CarvedTerrain.d.ts +5 -1
  103. package/dist/terrain/CarvedTerrain.js +6 -5
  104. package/dist/terrain/GrassField.d.ts +3 -1
  105. package/dist/terrain/GrassField.js +4 -2
  106. package/dist/terrain/grassBudget.d.ts +3 -0
  107. package/dist/terrain/grassBudget.js +6 -0
  108. package/dist/terrain/grassGeometry.js +1 -1
  109. package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
  110. package/dist/terrain/terrainDetailMaterial.js +90 -0
  111. package/dist/terrain/terrainMath.d.ts +8 -0
  112. package/dist/terrain/terrainMath.js +9 -0
  113. package/dist/touch/OrientationHint.d.ts +3 -0
  114. package/dist/touch/OrientationHint.js +13 -0
  115. package/dist/touch/TouchControlsOverlay.d.ts +17 -1
  116. package/dist/touch/TouchControlsOverlay.js +125 -9
  117. package/dist/visibility/CullingProvider.d.ts +21 -0
  118. package/dist/visibility/CullingProvider.js +134 -0
  119. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  120. package/dist/vision/FrustumSensorHud.js +25 -27
  121. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  122. package/dist/vision/frustumSampleEqual.js +10 -0
  123. package/dist/water/Ocean.js +1 -1
  124. package/dist/water/OceanConfig.d.ts +13 -0
  125. package/dist/water/OceanConfig.js +25 -17
  126. package/dist/water/index.d.ts +1 -1
  127. package/dist/water/index.js +1 -1
  128. package/dist/weather/FireSpreadLayer.js +7 -2
  129. package/dist/weather/RainField.d.ts +3 -1
  130. package/dist/weather/RainField.js +4 -4
  131. package/dist/weather/SnowField.d.ts +3 -1
  132. package/dist/weather/SnowField.js +4 -4
  133. package/dist/weather/fireSpreadPose.d.ts +2 -0
  134. package/dist/weather/fireSpreadPose.js +4 -0
  135. package/dist/weather/weatherMath.d.ts +5 -1
  136. package/dist/weather/weatherMath.js +7 -2
  137. package/dist/world/DataObjects.d.ts +1 -1
  138. package/dist/world/DataObjects.js +3 -1
  139. package/dist/world/SpriteBatch.d.ts +44 -0
  140. package/dist/world/SpriteBatch.js +112 -0
  141. package/dist/world/WorldHud.d.ts +6 -1
  142. package/dist/world/WorldHud.js +95 -48
  143. package/dist/world/entityPose.d.ts +14 -0
  144. package/dist/world/entityPose.js +10 -0
  145. package/dist/world/telegraphPulse.d.ts +1 -0
  146. package/dist/world/telegraphPulse.js +4 -0
  147. package/dist/world/worldBarSamples.d.ts +30 -0
  148. package/dist/world/worldBarSamples.js +57 -0
  149. package/dist/worldSync.d.ts +10 -0
  150. package/dist/worldSync.js +19 -0
  151. package/llms.txt +1522 -1143
  152. package/package.json +4 -4
@@ -0,0 +1,165 @@
1
+ import { useEffect, useReducer } from "react";
2
+ import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
3
+ 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";
4
+ import { useSettingsStore, } from "@jgengine/react/settings";
5
+ import { usePlayerFov } from "../camera/PlayerFov.js";
6
+ const DEFAULT_CATEGORY_LABELS = {
7
+ sound: "Sound",
8
+ graphics: "Graphics",
9
+ gameplay: "Gameplay",
10
+ controls: "Controls",
11
+ };
12
+ const percent = (value) => `${Math.round(value * 100)}%`;
13
+ export function useSettingsCategories(config) {
14
+ const store = useSettingsStore();
15
+ const fov = usePlayerFov();
16
+ const [, force] = useReducer((n) => n + 1, 0);
17
+ useEffect(() => store.subscribe(() => force()), [store]);
18
+ const hidden = new Set(config.hide);
19
+ const extrasFor = (category) => config.extra
20
+ .filter((def) => def.category === category)
21
+ .map((def) => ({
22
+ id: def.id,
23
+ label: def.label,
24
+ kind: def.kind,
25
+ value: store.get(def.id, def.default),
26
+ min: def.min,
27
+ max: def.max,
28
+ step: def.step,
29
+ options: def.options,
30
+ set: (value) => store.set(def.id, value),
31
+ }));
32
+ const soundRows = [
33
+ {
34
+ id: SETTING_IDS.masterVolume,
35
+ label: "Master volume",
36
+ kind: "slider",
37
+ value: store.get(SETTING_IDS.masterVolume, DEFAULT_MASTER_VOLUME),
38
+ min: 0,
39
+ max: 1,
40
+ step: 0.01,
41
+ format: percent,
42
+ set: (value) => store.set(SETTING_IDS.masterVolume, value),
43
+ },
44
+ ...Object.values(config.buses ?? {}).map((bus) => {
45
+ const id = busVolumeSettingId(bus.id);
46
+ return {
47
+ id,
48
+ label: `${bus.id.charAt(0).toUpperCase()}${bus.id.slice(1)} volume`,
49
+ kind: "slider",
50
+ value: store.get(id, bus.gain ?? 1),
51
+ min: 0,
52
+ max: 1,
53
+ step: 0.01,
54
+ format: percent,
55
+ set: (value) => store.set(id, value),
56
+ };
57
+ }),
58
+ ...extrasFor("sound"),
59
+ ];
60
+ const graphicsRows = [
61
+ {
62
+ id: SETTING_IDS.graphicsQuality,
63
+ label: "Quality",
64
+ kind: "select",
65
+ value: store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY),
66
+ options: GRAPHICS_QUALITY_OPTIONS,
67
+ set: (value) => store.set(SETTING_IDS.graphicsQuality, value),
68
+ },
69
+ {
70
+ id: SETTING_IDS.graphicsShadows,
71
+ label: "Shadows",
72
+ kind: "toggle",
73
+ value: store.get(SETTING_IDS.graphicsShadows, DEFAULT_GRAPHICS_SHADOWS),
74
+ set: (value) => store.set(SETTING_IDS.graphicsShadows, value),
75
+ },
76
+ {
77
+ id: SETTING_IDS.graphicsUiScale,
78
+ label: "UI scale",
79
+ kind: "slider",
80
+ value: store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE),
81
+ min: UI_SCALE_MIN,
82
+ max: UI_SCALE_MAX,
83
+ step: 0.05,
84
+ format: percent,
85
+ set: (value) => store.set(SETTING_IDS.graphicsUiScale, value),
86
+ },
87
+ ...extrasFor("graphics"),
88
+ ];
89
+ const gameplayRows = [
90
+ ...(config.fovEnabled
91
+ ? [
92
+ {
93
+ id: "gameplay.fov",
94
+ label: "Field of view",
95
+ kind: "slider",
96
+ value: fov.fov,
97
+ min: fov.bounds.min,
98
+ max: fov.bounds.max,
99
+ step: 1,
100
+ format: (value) => `${Math.round(value)}`,
101
+ set: (value) => fov.setFov(Number(value)),
102
+ },
103
+ ]
104
+ : []),
105
+ ...extrasFor("gameplay"),
106
+ ];
107
+ const hiddenBindings = new Set(config.hideBindings);
108
+ const keybinds = Object.keys(config.input)
109
+ .filter((action) => !hiddenBindings.has(action))
110
+ .map((action) => {
111
+ const override = config.overrides[action];
112
+ const effective = override === undefined ? config.input : { ...config.input, [action]: override };
113
+ return {
114
+ action,
115
+ label: humanizeAction(action),
116
+ bindingLabel: actionLabel(effective, action) ?? "Unbound",
117
+ isDefault: override === undefined,
118
+ rebind: (code) => config.rebind(action, code),
119
+ reset: () => config.resetBinding(action),
120
+ };
121
+ });
122
+ const builtInRows = {
123
+ sound: soundRows,
124
+ graphics: graphicsRows,
125
+ gameplay: gameplayRows,
126
+ controls: extrasFor("controls"),
127
+ };
128
+ const builtInKeybinds = { controls: keybinds };
129
+ const declaredLabels = new Map(config.categories.map((c) => [c.id, c.label]));
130
+ const declaredOrder = new Map(config.categories.map((c, index) => [c.id, c.order ?? 1000 + index]));
131
+ const order = [];
132
+ const push = (id) => {
133
+ if (!order.includes(id))
134
+ order.push(id);
135
+ };
136
+ for (const id of BUILT_IN_SETTING_CATEGORIES)
137
+ push(id);
138
+ for (const c of config.categories)
139
+ push(c.id);
140
+ for (const def of config.extra)
141
+ push(def.category);
142
+ const views = order
143
+ .filter((id) => !hidden.has(id))
144
+ .map((id) => ({
145
+ id,
146
+ label: declaredLabels.get(id) ?? DEFAULT_CATEGORY_LABELS[id] ?? humanizeAction(id),
147
+ rows: builtInRows[id] ?? extrasFor(id),
148
+ keybinds: builtInKeybinds[id] ?? [],
149
+ }))
150
+ .filter((view) => view.rows.length > 0 || view.keybinds.length > 0);
151
+ views.sort((a, b) => (declaredOrder.get(a.id) ?? indexOrder(order, a.id)) - (declaredOrder.get(b.id) ?? indexOrder(order, b.id)));
152
+ return views;
153
+ }
154
+ function indexOrder(order, id) {
155
+ const index = order.indexOf(id);
156
+ return index === -1 ? 999 : index;
157
+ }
158
+ function humanizeAction(action) {
159
+ const spaced = action
160
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
161
+ .replace(/[._-]+/g, " ")
162
+ .trim();
163
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
164
+ }
165
+ export { bindingLabel };
@@ -40,5 +40,15 @@ export interface GeneratedBuildingProps {
40
40
  kit?: BuildingKitRenderer;
41
41
  visibleKinds?: readonly BuildingPartKind[];
42
42
  }
43
+ export interface InstancedBuildingPlacement {
44
+ building: GeneratedBuildingData;
45
+ position?: readonly [number, number, number];
46
+ }
47
+ export interface InstancedBuildingsProps {
48
+ buildings: readonly InstancedBuildingPlacement[];
49
+ palette?: BuildingMaterialPalette;
50
+ visibleKinds?: readonly BuildingPartKind[];
51
+ }
52
+ export declare function InstancedBuildings({ buildings, palette, visibleKinds }: InstancedBuildingsProps): import("react").JSX.Element | null;
43
53
  export declare function BuildingBlock({ part, palette }: BuildingBlockProps): import("react").JSX.Element;
44
54
  export declare function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from "react";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
3
+ import * as THREE from "three";
3
4
  const DEFAULT_PALETTE = {
4
5
  wall: "#83766a",
5
6
  window: "#8ecae6",
@@ -50,6 +51,84 @@ function materialFor(part, palette) {
50
51
  }
51
52
  return _jsx("meshStandardMaterial", { color: color, roughness: 0.88, metalness: 0 });
52
53
  }
54
+ function batchMaterialFor(kind, palette) {
55
+ const color = colorFor(kind, palette);
56
+ if (kind === "window" || kind === "storefront") {
57
+ return new THREE.MeshPhysicalMaterial({ color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 });
58
+ }
59
+ if (kind === "storeSign") {
60
+ return new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 });
61
+ }
62
+ if (kind === "roofProp") {
63
+ return new THREE.MeshStandardMaterial({ color, roughness: 0.65, metalness: 0.1 });
64
+ }
65
+ return new THREE.MeshStandardMaterial({ color, roughness: 0.88, metalness: 0 });
66
+ }
67
+ const CLOTHESLINE_ROW_OFFSETS = [-0.09, 0.09];
68
+ function bucketPartMatrices(buildings, visible) {
69
+ const dummy = new THREE.Object3D();
70
+ const buckets = new Map();
71
+ for (const placement of buildings) {
72
+ const [ox, oy, oz] = placement.position ?? [0, 0, 0];
73
+ for (const part of placement.building.parts) {
74
+ if (visible !== null && !visible.has(part.kind))
75
+ continue;
76
+ const [nx, nz] = normalFor(part.facade);
77
+ const offset = outwardOffset(part);
78
+ const px = ox + part.position[0] + nx * offset;
79
+ const py = oy + part.position[1];
80
+ const pz = oz + part.position[2] + nz * offset;
81
+ let bucket = buckets.get(part.kind);
82
+ if (bucket === undefined) {
83
+ bucket = [];
84
+ buckets.set(part.kind, bucket);
85
+ }
86
+ if (part.kind === "clothesline") {
87
+ const sin = Math.sin(part.rotationY);
88
+ const cos = Math.cos(part.rotationY);
89
+ for (const row of CLOTHESLINE_ROW_OFFSETS) {
90
+ dummy.position.set(px + row * sin, py, pz + row * cos);
91
+ dummy.rotation.set(0, part.rotationY, 0);
92
+ dummy.scale.set(part.scale[0], Math.max(part.scale[1], 0.025), 0.025);
93
+ dummy.updateMatrix();
94
+ bucket.push(dummy.matrix.clone());
95
+ }
96
+ continue;
97
+ }
98
+ dummy.position.set(px, py, pz);
99
+ dummy.rotation.set(0, part.rotationY, 0);
100
+ dummy.scale.set(part.scale[0], part.scale[1], part.scale[2]);
101
+ dummy.updateMatrix();
102
+ bucket.push(dummy.matrix.clone());
103
+ }
104
+ }
105
+ return buckets;
106
+ }
107
+ function BuildingKindBatch({ kind, matrices, palette, geometry, }) {
108
+ const meshRef = useRef(null);
109
+ const material = useMemo(() => batchMaterialFor(kind, palette), [kind, palette]);
110
+ useEffect(() => () => material.dispose(), [material]);
111
+ useLayoutEffect(() => {
112
+ const mesh = meshRef.current;
113
+ if (mesh === null)
114
+ return;
115
+ matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix));
116
+ mesh.instanceMatrix.needsUpdate = true;
117
+ mesh.computeBoundingSphere();
118
+ }, [matrices, material]);
119
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
120
+ }
121
+ export function InstancedBuildings({ buildings, palette, visibleKinds }) {
122
+ const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
123
+ useEffect(() => () => geometry.dispose(), [geometry]);
124
+ const buckets = useMemo(() => {
125
+ const visible = visibleKinds === undefined ? null : new Set(visibleKinds);
126
+ return bucketPartMatrices(buildings, visible);
127
+ }, [buildings, visibleKinds]);
128
+ if (buckets.size === 0)
129
+ return null;
130
+ return (_jsx("group", { children: [...buckets.entries()].map(([kind, matrices]) => (_jsx(BuildingKindBatch, { kind: kind, matrices: matrices, palette: palette, geometry: geometry }, kind))) }));
131
+ }
53
132
  function BlockMesh({ part, palette }) {
54
133
  const [nx, nz] = normalFor(part.facade);
55
134
  const offset = outwardOffset(part);
@@ -74,13 +153,22 @@ export function BuildingBlock({ part, palette }) {
74
153
  return _jsx(BlockMesh, { part: part, palette: palette });
75
154
  }
76
155
  export function GeneratedBuilding({ building, palette, kit, visibleKinds }) {
77
- const visible = useMemo(() => (visibleKinds === undefined ? null : new Set(visibleKinds)), [visibleKinds]);
78
- return (_jsx("group", { name: building.id, children: building.parts.map((part) => {
156
+ const { kitParts, batched } = useMemo(() => {
157
+ const visible = visibleKinds === undefined ? null : new Set(visibleKinds);
158
+ const kitRendered = [];
159
+ const batchedParts = [];
160
+ for (const part of building.parts) {
79
161
  if (visible !== null && !visible.has(part.kind))
80
- return null;
162
+ continue;
81
163
  const rendered = kit?.renderPart?.(part);
82
- if (rendered !== undefined)
83
- return _jsx("group", { children: rendered }, part.id);
84
- return _jsx(BuildingBlock, { part: part, palette: palette }, part.id);
85
- }) }));
164
+ if (rendered !== undefined) {
165
+ kitRendered.push({ id: part.id, node: rendered });
166
+ continue;
167
+ }
168
+ batchedParts.push(part);
169
+ }
170
+ const placements = batchedParts.length === 0 ? [] : [{ building: { id: building.id, parts: batchedParts } }];
171
+ return { kitParts: kitRendered, batched: placements };
172
+ }, [building, kit, visibleKinds]);
173
+ return (_jsxs("group", { name: building.id, children: [kitParts.map((entry) => (_jsx("group", { children: entry.node }, entry.id))), _jsx(InstancedBuildings, { buildings: batched, palette: palette })] }));
86
174
  }
@@ -1,2 +1,2 @@
1
- export { BuildingBlock, GeneratedBuilding, type BuildingBlockProps, type GeneratedBuildingProps, } from "./GeneratedBuilding.js";
1
+ export { BuildingBlock, GeneratedBuilding, InstancedBuildings, type BuildingBlockProps, type GeneratedBuildingProps, type InstancedBuildingPlacement, type InstancedBuildingsProps, } from "./GeneratedBuilding.js";
2
2
  export { PlacementGhost, type PlacementGhostProps } from "./PlacementGhost.js";
@@ -1,2 +1,2 @@
1
- export { BuildingBlock, GeneratedBuilding, } from "./GeneratedBuilding.js";
1
+ export { BuildingBlock, GeneratedBuilding, InstancedBuildings, } from "./GeneratedBuilding.js";
2
2
  export { PlacementGhost } from "./PlacementGhost.js";
@@ -1,4 +1,5 @@
1
1
  import { type ThreeElements } from "@react-three/fiber";
2
+ import * as THREE from "three";
2
3
  import type { TerrainField } from "@jgengine/core/world/terrain";
3
4
  import { type FieldGroundOptions } from "./terrainMath.js";
4
5
  export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
@@ -9,8 +10,11 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
9
10
  center?: FieldGroundOptions["center"];
10
11
  colors?: FieldGroundOptions["colors"];
11
12
  heightRange?: FieldGroundOptions["heightRange"];
13
+ paletteAt?: FieldGroundOptions["paletteAt"];
12
14
  roughness?: number;
13
15
  metalness?: number;
16
+ /** Override the default vertex-colour material (e.g. a procedural detail material). The caller owns its lifecycle; it is not disposed here. */
17
+ surfaceMaterial?: THREE.Material;
14
18
  /** Bump after a runtime carve/deposit to re-mesh the deformed surface. */
15
19
  epoch?: number;
16
20
  }
@@ -19,4 +23,4 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
19
23
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
20
24
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
21
25
  */
22
- export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness, metalness, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
26
+ export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness, metalness, surfaceMaterial, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
@@ -8,12 +8,13 @@ import { createFieldGroundGeometry } from "./terrainMath.js";
8
8
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
9
9
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
10
10
  */
11
- export function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }) {
12
- const geometry = useMemo(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange }),
11
+ export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
12
+ const geometry = useMemo(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt }),
13
13
  // eslint-disable-next-line react-hooks/exhaustive-deps
14
- [field, size, segments, center, colors, heightRange, epoch]);
15
- const material = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
14
+ [field, size, segments, center, colors, heightRange, paletteAt, epoch]);
15
+ const defaultMaterial = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
16
+ const material = surfaceMaterial ?? defaultMaterial;
16
17
  useEffect(() => () => geometry.dispose(), [geometry]);
17
- useEffect(() => () => material.dispose(), [material]);
18
+ useEffect(() => () => defaultMaterial.dispose(), [defaultMaterial]);
18
19
  return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
19
20
  }
@@ -2,9 +2,11 @@ import { type ThreeElements } from "@react-three/fiber";
2
2
  import { type GrassBladeGeometryOptions, type GrassRange } from "./grassGeometry.js";
3
3
  import { type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
4
4
  import type { TerrainArea, TerrainHeightSampler } from "./terrainMath.js";
5
+ export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
5
6
  export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
6
7
  count?: number;
7
8
  density?: number;
9
+ budget?: number;
8
10
  area?: TerrainArea;
9
11
  seed?: GrassBladeGeometryOptions["seed"];
10
12
  segments?: number;
@@ -18,4 +20,4 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
18
20
  wind?: GrassWindOptions | false;
19
21
  roughness?: number;
20
22
  }
21
- export declare function GrassField({ count, density, area, seed, segments, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
23
+ export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
@@ -3,7 +3,9 @@ import { useFrame } from "@react-three/fiber";
3
3
  import { useEffect, useMemo } from "react";
4
4
  import { createGrassBladeGeometry } from "./grassGeometry.js";
5
5
  import { createGrassMaterial } from "./grassMaterial.js";
6
- export function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }) {
6
+ import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget, } from "./grassBudget.js";
7
+ export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
8
+ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshProps }) {
7
9
  const geometry = useMemo(() => createGrassBladeGeometry({
8
10
  count,
9
11
  area,
@@ -21,7 +23,7 @@ export function GrassField({ count = 6000, density = 1, area = 40, seed = 1, seg
21
23
  wind,
22
24
  roughness,
23
25
  }), [colorBase, colorTip, colorVariation, roughness, wind]);
24
- geometry.instanceCount = Math.floor(Math.max(0, Math.min(1, density)) * count);
26
+ geometry.instanceCount = resolveGrassInstanceBudget(count, density, budget);
25
27
  useFrame((state) => {
26
28
  handle.uniforms.uTime.value = state.clock.elapsedTime;
27
29
  });
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_GRASS_COUNT = 1500;
2
+ export declare const DEFAULT_GRASS_DENSITY = 1;
3
+ export declare function resolveGrassInstanceBudget(count: number, density: number, budget?: number): number;
@@ -0,0 +1,6 @@
1
+ export const DEFAULT_GRASS_COUNT = 1500;
2
+ export const DEFAULT_GRASS_DENSITY = 1;
3
+ export function resolveGrassInstanceBudget(count, density, budget) {
4
+ const capped = budget === undefined ? count : Math.min(count, Math.max(0, Math.floor(budget)));
5
+ return Math.floor(Math.max(0, Math.min(1, density)) * capped);
6
+ }
@@ -8,7 +8,7 @@ export function resolveGrassRange(value, fallback) {
8
8
  }
9
9
  export function resolveGrassBladeGeometryOptions(options = {}) {
10
10
  return {
11
- count: Math.max(0, Math.floor(options.count ?? 6000)),
11
+ count: Math.max(0, Math.floor(options.count ?? 1500)),
12
12
  area: options.area ?? 40,
13
13
  seed: options.seed ?? 1,
14
14
  segments: Math.max(1, Math.floor(options.segments ?? 4)),
@@ -0,0 +1,14 @@
1
+ import * as THREE from "three";
2
+ import type { ResolvedTerrainDetail } from "@jgengine/core/world/terrain";
3
+ /** The built procedural detail terrain material, ready to mount on the ground mesh. */
4
+ export interface TerrainDetailMaterialHandle {
5
+ material: THREE.MeshStandardMaterial;
6
+ }
7
+ /**
8
+ * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
9
+ * colour as the base ground and blends procedural, noise-broken rock (by slope),
10
+ * sand (by waterline), and snow (by height) over it — textured-reading terrain
11
+ * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
12
+ * material, so it composes with the post-processing chain.
13
+ */
14
+ export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail): TerrainDetailMaterialHandle;
@@ -0,0 +1,90 @@
1
+ import * as THREE from "three";
2
+ const NOISE_GLSL = /* glsl */ `
3
+ float jgHash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
4
+ float jgVNoise(vec2 p){
5
+ vec2 i = floor(p); vec2 f = fract(p);
6
+ vec2 u = f * f * (3.0 - 2.0 * f);
7
+ float a = jgHash(i);
8
+ float b = jgHash(i + vec2(1.0, 0.0));
9
+ float c = jgHash(i + vec2(0.0, 1.0));
10
+ float d = jgHash(i + vec2(1.0, 1.0));
11
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
12
+ }
13
+ float jgFbm(vec2 p){
14
+ float v = 0.0; float amp = 0.5;
15
+ for (int i = 0; i < 4; i++){ v += amp * jgVNoise(p); p *= 2.0; amp *= 0.5; }
16
+ return v;
17
+ }
18
+ `;
19
+ /**
20
+ * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
21
+ * colour as the base ground and blends procedural, noise-broken rock (by slope),
22
+ * sand (by waterline), and snow (by height) over it — textured-reading terrain
23
+ * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
24
+ * material, so it composes with the post-processing chain.
25
+ */
26
+ export function createTerrainDetailMaterial(detail) {
27
+ const uniforms = {
28
+ uRockColor: { value: new THREE.Color(detail.rockColor) },
29
+ uSandColor: { value: new THREE.Color(detail.sandColor) },
30
+ uSnowColor: { value: new THREE.Color(detail.snowColor) },
31
+ uRockSlopeStart: { value: detail.rockSlopeStart },
32
+ uSnowHeight: { value: detail.snowHeight },
33
+ uWaterLevel: { value: detail.waterLevel },
34
+ uDetailScale: { value: Math.max(0.5, detail.detailScale) },
35
+ uMacroScale: { value: Math.max(0.5, detail.macroScale) },
36
+ uStrength: { value: detail.strength },
37
+ };
38
+ const material = new THREE.MeshStandardMaterial({
39
+ color: "#ffffff",
40
+ roughness: detail.roughness,
41
+ metalness: 0,
42
+ vertexColors: true,
43
+ });
44
+ material.onBeforeCompile = (shader) => {
45
+ Object.assign(shader.uniforms, uniforms);
46
+ shader.vertexShader = shader.vertexShader
47
+ .replace("#include <common>", `#include <common>
48
+ varying vec3 vJgWorldPos;
49
+ varying vec3 vJgWorldNormal;`)
50
+ .replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>
51
+ vJgWorldNormal = normalize(mat3(modelMatrix) * objectNormal);`)
52
+ .replace("#include <begin_vertex>", `#include <begin_vertex>
53
+ vJgWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
54
+ shader.fragmentShader = shader.fragmentShader
55
+ .replace("#include <common>", `#include <common>
56
+ varying vec3 vJgWorldPos;
57
+ varying vec3 vJgWorldNormal;
58
+ uniform vec3 uRockColor;
59
+ uniform vec3 uSandColor;
60
+ uniform vec3 uSnowColor;
61
+ uniform float uRockSlopeStart;
62
+ uniform float uSnowHeight;
63
+ uniform float uWaterLevel;
64
+ uniform float uDetailScale;
65
+ uniform float uMacroScale;
66
+ uniform float uStrength;
67
+ ${NOISE_GLSL}`)
68
+ .replace("#include <color_fragment>", `#include <color_fragment>
69
+ vec2 jgWp = vJgWorldPos.xz;
70
+ float jgFine = jgFbm(jgWp / uDetailScale);
71
+ float jgMacro = jgFbm(jgWp / uMacroScale);
72
+ float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
73
+ float jgH = vJgWorldPos.y;
74
+ vec3 jgBase = diffuseColor.rgb;
75
+ jgBase *= mix(1.0 - 0.22 * uStrength, 1.0 + 0.18 * uStrength, jgFine);
76
+ jgBase = mix(jgBase, jgBase * vec3(0.86, 0.9, 0.82), jgMacro * 0.25 * uStrength);
77
+ vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
78
+ float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
79
+ vec3 jgCol = mix(jgBase, jgRock, jgRockW);
80
+ vec3 jgSand = uSandColor * mix(0.9, 1.08, jgFine);
81
+ float jgSandW = smoothstep(uWaterLevel + 2.4, uWaterLevel - 0.4, jgH) * (1.0 - jgRockW) * uStrength;
82
+ jgCol = mix(jgCol, jgSand, jgSandW);
83
+ vec3 jgSnow = uSnowColor * mix(0.94, 1.04, jgFine);
84
+ float jgSnowW = smoothstep(uSnowHeight, uSnowHeight + 8.0, jgH) * (1.0 - jgRockW * 0.6) * uStrength;
85
+ jgCol = mix(jgCol, jgSnow, jgSnowW);
86
+ diffuseColor.rgb = jgCol;`);
87
+ };
88
+ material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}`;
89
+ return { material };
90
+ }
@@ -32,12 +32,20 @@ export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainS
32
32
  export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
33
33
  export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
34
34
  export declare function createProceduralTerrainSampler(config?: ProceduralTerrainConfig): TerrainHeightSampler;
35
+ /** Per-position palette override for multi-biome ground coloring — `createTerrainPaletteSampler` from `@jgengine/core/world/terrain` returns exactly this shape. */
36
+ export type TerrainPaletteSampler = (x: number, z: number) => {
37
+ low: string;
38
+ high: string;
39
+ waterline?: string;
40
+ };
35
41
  export interface FieldGroundOptions {
36
42
  size?: TerrainArea;
37
43
  segments?: number | readonly [x: number, z: number];
38
44
  center?: readonly [x: number, z: number];
39
45
  colors?: TerrainVertexColorOptions;
40
46
  heightRange?: readonly [min: number, max: number];
47
+ /** Sampled per vertex when set; `colors` still supplies `waterlineHeight` and the fallback. */
48
+ paletteAt?: TerrainPaletteSampler;
41
49
  }
42
50
  /**
43
51
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
@@ -42,10 +42,12 @@ export function createFieldGroundGeometry(field, options = {}) {
42
42
  colors: options.colors ?? {},
43
43
  center: [cx, cz],
44
44
  heightRange: options.heightRange,
45
+ paletteAt: options.paletteAt,
45
46
  });
46
47
  }
47
48
  function buildGroundGeometry(size, segments, sampler, opts) {
48
49
  const colors = opts.colors;
50
+ const paletteAt = opts.paletteAt;
49
51
  const vertexCountX = segments.x + 1;
50
52
  const vertexCountZ = segments.z + 1;
51
53
  const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
@@ -72,6 +74,13 @@ function buildGroundGeometry(size, segments, sampler, opts) {
72
74
  positions[index + 2] = z;
73
75
  uvs[uvIndex] = u;
74
76
  uvs[uvIndex + 1] = v;
77
+ if (paletteAt !== undefined) {
78
+ const palette = paletteAt(x, z);
79
+ low.set(palette.low);
80
+ high.set(palette.high);
81
+ if (waterline !== null && palette.waterline !== undefined)
82
+ waterline.set(palette.waterline);
83
+ }
75
84
  const blend = normalizeHeightBlend(y, minHeight, maxHeight);
76
85
  const color = low.clone().lerp(high, blend);
77
86
  if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
@@ -0,0 +1,3 @@
1
+ export declare function OrientationHint({ wanted }: {
2
+ wanted: "landscape" | "portrait";
3
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ const AUTO_HIDE_MS = 6000;
4
+ export function OrientationHint({ wanted }) {
5
+ const [dismissed, setDismissed] = useState(false);
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => setDismissed(true), AUTO_HIDE_MS);
8
+ return () => clearTimeout(timer);
9
+ }, []);
10
+ if (dismissed)
11
+ return null;
12
+ return (_jsx("div", { className: "pointer-events-none absolute inset-x-0 z-50 flex justify-center", style: { top: "calc(env(safe-area-inset-top, 0px) + 12px)" }, children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-2 rounded-full border border-white/25 bg-black/70 px-4 py-2 text-xs font-medium text-white/90 backdrop-blur-sm", children: [_jsx("span", { "aria-hidden": true, className: "text-base leading-none", children: "\u27F3" }), _jsxs("span", { children: ["Best played in ", wanted, " \u2014 rotate your device"] }), _jsx("button", { type: "button", "aria-label": "Dismiss", className: "ml-1 text-white/60 active:text-white", onClick: () => setDismissed(true), children: "\u2715" })] }) }));
13
+ }
@@ -4,6 +4,21 @@ export interface TouchCodeSink {
4
4
  onCodeDown(code: string): void;
5
5
  onCodeUp(code: string): void;
6
6
  }
7
+ /**
8
+ * Thumb-arc placement for primary buttons around the bottom-right corner:
9
+ * up to three on an inner ring, the rest on an outer ring. Null means too
10
+ * many buttons for an arc — the dock falls back to a wrapping grid.
11
+ */
12
+ export declare function primaryButtonOffsets(count: number, scale?: number): {
13
+ right: number;
14
+ bottom: number;
15
+ }[] | null;
16
+ /**
17
+ * Vertical space (px, excluding device safe areas) the dock occupies above
18
+ * the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so
19
+ * `HudCanvas` regions never collide with touch controls.
20
+ */
21
+ export declare function touchDockClearance(scheme: TouchScheme | null, scale?: number): number;
7
22
  export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: {
8
23
  scheme: TouchScheme;
9
24
  sink: TouchCodeSink;
@@ -12,7 +27,8 @@ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPi
12
27
  maxPitch: number;
13
28
  onPrimaryTap: () => void;
14
29
  }): import("react").JSX.Element;
15
- export declare function TouchControlsDock({ scheme, sink }: {
30
+ export declare function TouchControlsDock({ scheme, sink, scale }: {
16
31
  scheme: TouchScheme;
17
32
  sink: TouchCodeSink;
33
+ scale?: number;
18
34
  }): import("react").JSX.Element;