@jgengine/shell 0.7.0 → 0.9.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 (168) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +37 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +36 -1
  8. package/dist/GamePlayerShell.js +751 -80
  9. package/dist/GameUiPreview.js +2 -1
  10. package/dist/audio/AudioComponents.js +25 -8
  11. package/dist/audio/audioEngine.d.ts +1 -0
  12. package/dist/audio/audioEngine.js +4 -0
  13. package/dist/behaviour.d.ts +12 -0
  14. package/dist/behaviour.js +27 -0
  15. package/dist/behaviourAttach.d.ts +9 -0
  16. package/dist/behaviourAttach.js +32 -0
  17. package/dist/behaviourDriver.d.ts +7 -0
  18. package/dist/behaviourDriver.js +21 -0
  19. package/dist/camera/GameCameraRig.d.ts +8 -3
  20. package/dist/camera/GameCameraRig.js +28 -14
  21. package/dist/camera/GameFirstPersonCamera.js +16 -0
  22. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  23. package/dist/camera/GameInspectionCamera.js +24 -0
  24. package/dist/camera/GameOrbitCamera.js +10 -1
  25. package/dist/camera/PlayerFov.d.ts +18 -0
  26. package/dist/camera/PlayerFov.js +48 -0
  27. package/dist/camera/cameraBlendMath.d.ts +13 -0
  28. package/dist/camera/cameraBlendMath.js +36 -0
  29. package/dist/camera/cameraRigs.d.ts +8 -1
  30. package/dist/camera/cameraRigs.js +78 -38
  31. package/dist/camera/fovPreference.d.ts +25 -0
  32. package/dist/camera/fovPreference.js +73 -0
  33. package/dist/camera/index.d.ts +7 -2
  34. package/dist/camera/index.js +7 -2
  35. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  36. package/dist/camera/inspectionCameraMath.js +44 -0
  37. package/dist/camera/rigMath.d.ts +50 -1
  38. package/dist/camera/rigMath.js +91 -1
  39. package/dist/camera/rigResolve.d.ts +15 -0
  40. package/dist/camera/rigResolve.js +52 -0
  41. package/dist/camera/shakeChannel.d.ts +3 -17
  42. package/dist/camera/shakeChannel.js +2 -21
  43. package/dist/camera/shakeChannelMath.d.ts +8 -0
  44. package/dist/camera/shakeChannelMath.js +21 -0
  45. package/dist/cartridge.d.ts +145 -0
  46. package/dist/cartridge.js +245 -0
  47. package/dist/defineGame.d.ts +16 -0
  48. package/dist/defineGame.js +52 -0
  49. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  50. package/dist/devtools/CollisionDebugWorld.js +180 -0
  51. package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
  52. package/dist/devtools/DevtoolsOverlay.js +676 -0
  53. package/dist/devtools/collisionDebug.d.ts +57 -0
  54. package/dist/devtools/collisionDebug.js +127 -0
  55. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  56. package/dist/devtools/collisionDebugMath.js +128 -0
  57. package/dist/environment/Daylight.d.ts +56 -0
  58. package/dist/environment/Daylight.js +101 -0
  59. package/dist/environment/EnvironmentScene.js +87 -48
  60. package/dist/environment/GroundPad.d.ts +7 -0
  61. package/dist/environment/GroundPad.js +12 -0
  62. package/dist/environment/daylightCycle.d.ts +26 -0
  63. package/dist/environment/daylightCycle.js +116 -0
  64. package/dist/environment/groundPadMath.d.ts +13 -0
  65. package/dist/environment/groundPadMath.js +10 -0
  66. package/dist/environment/index.d.ts +3 -0
  67. package/dist/environment/index.js +3 -0
  68. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  69. package/dist/environment/skyLightingPolicy.js +6 -0
  70. package/dist/input/mouseLook.d.ts +27 -0
  71. package/dist/input/mouseLook.js +35 -0
  72. package/dist/materialOverride.d.ts +6 -0
  73. package/dist/materialOverride.js +28 -0
  74. package/dist/multiplayer.d.ts +16 -9
  75. package/dist/multiplayer.js +62 -12
  76. package/dist/pointer/PointerProbe.js +20 -3
  77. package/dist/pointer/pointerService.d.ts +5 -0
  78. package/dist/pointer/pointerService.js +51 -24
  79. package/dist/registry.d.ts +4 -1
  80. package/dist/registry.js +3 -1
  81. package/dist/render/modelRender.d.ts +23 -0
  82. package/dist/render/modelRender.js +106 -0
  83. package/dist/render/resolveModel.d.ts +14 -0
  84. package/dist/render/resolveModel.js +24 -0
  85. package/dist/settings/QuickControls.d.ts +4 -0
  86. package/dist/settings/QuickControls.js +42 -0
  87. package/dist/settings/SettingsChrome.d.ts +1 -0
  88. package/dist/settings/SettingsChrome.js +10 -0
  89. package/dist/settings/SettingsMenu.d.ts +6 -0
  90. package/dist/settings/SettingsMenu.js +148 -0
  91. package/dist/settings/SettingsRuntime.d.ts +11 -0
  92. package/dist/settings/SettingsRuntime.js +19 -0
  93. package/dist/settings/appliedSettings.d.ts +14 -0
  94. package/dist/settings/appliedSettings.js +27 -0
  95. package/dist/settings/settingsController.d.ts +20 -0
  96. package/dist/settings/settingsController.js +165 -0
  97. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  98. package/dist/structures/GeneratedBuilding.js +96 -8
  99. package/dist/structures/index.d.ts +1 -1
  100. package/dist/structures/index.js +1 -1
  101. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  102. package/dist/terrain/CarvedTerrain.js +3 -3
  103. package/dist/terrain/GrassField.d.ts +3 -1
  104. package/dist/terrain/GrassField.js +4 -2
  105. package/dist/terrain/grassBudget.d.ts +3 -0
  106. package/dist/terrain/grassBudget.js +6 -0
  107. package/dist/terrain/grassGeometry.js +1 -1
  108. package/dist/terrain/index.d.ts +1 -1
  109. package/dist/terrain/index.js +1 -1
  110. package/dist/terrain/terrainMath.d.ts +9 -0
  111. package/dist/terrain/terrainMath.js +17 -2
  112. package/dist/touch/OrientationHint.d.ts +3 -0
  113. package/dist/touch/OrientationHint.js +13 -0
  114. package/dist/touch/TouchControlsOverlay.d.ts +34 -0
  115. package/dist/touch/TouchControlsOverlay.js +279 -0
  116. package/dist/visibility/CullingProvider.d.ts +21 -0
  117. package/dist/visibility/CullingProvider.js +134 -0
  118. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  119. package/dist/vision/FrustumSensorHud.js +25 -27
  120. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  121. package/dist/vision/frustumSampleEqual.js +10 -0
  122. package/dist/water/Ocean.js +1 -1
  123. package/dist/water/OceanConfig.d.ts +13 -0
  124. package/dist/water/OceanConfig.js +25 -17
  125. package/dist/water/OceanShader.d.ts +1 -1
  126. package/dist/water/OceanShader.js +3 -3
  127. package/dist/water/index.d.ts +1 -1
  128. package/dist/water/index.js +1 -1
  129. package/dist/weather/FireSpreadLayer.js +7 -2
  130. package/dist/weather/RainField.d.ts +3 -1
  131. package/dist/weather/RainField.js +4 -4
  132. package/dist/weather/SnowField.d.ts +3 -1
  133. package/dist/weather/SnowField.js +4 -4
  134. package/dist/weather/fireSpreadPose.d.ts +2 -0
  135. package/dist/weather/fireSpreadPose.js +4 -0
  136. package/dist/weather/weatherMath.d.ts +5 -1
  137. package/dist/weather/weatherMath.js +7 -2
  138. package/dist/world/DataObjects.d.ts +44 -0
  139. package/dist/world/DataObjects.js +77 -0
  140. package/dist/world/GridWorldScene.d.ts +10 -0
  141. package/dist/world/GridWorldScene.js +54 -0
  142. package/dist/world/SpriteBatch.d.ts +44 -0
  143. package/dist/world/SpriteBatch.js +112 -0
  144. package/dist/world/WorldHud.d.ts +8 -1
  145. package/dist/world/WorldHud.js +90 -39
  146. package/dist/world/WorldItems.js +21 -2
  147. package/dist/world/entityPose.d.ts +14 -0
  148. package/dist/world/entityPose.js +10 -0
  149. package/dist/world/telegraphPulse.d.ts +1 -0
  150. package/dist/world/telegraphPulse.js +4 -0
  151. package/dist/world/worldBarSamples.d.ts +30 -0
  152. package/dist/world/worldBarSamples.js +51 -0
  153. package/llms.txt +2643 -0
  154. package/package.json +6 -5
  155. package/dist/demo/builderDemo.d.ts +0 -2
  156. package/dist/demo/builderDemo.js +0 -189
  157. package/dist/demo/demoGame.d.ts +0 -2
  158. package/dist/demo/demoGame.js +0 -208
  159. package/dist/demo/environmentShowcase.d.ts +0 -2
  160. package/dist/demo/environmentShowcase.js +0 -15
  161. package/dist/demo/mapDemo.d.ts +0 -2
  162. package/dist/demo/mapDemo.js +0 -206
  163. package/dist/demo/pointerDemo.d.ts +0 -2
  164. package/dist/demo/pointerDemo.js +0 -151
  165. package/dist/demo/sensorShowcase.d.ts +0 -2
  166. package/dist/demo/sensorShowcase.js +0 -131
  167. package/dist/demo/survivalDemo.d.ts +0 -2
  168. package/dist/demo/survivalDemo.js +0 -291
@@ -0,0 +1,27 @@
1
+ import { useEffect, useReducer } 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";
3
+ export function useSettingsRevision(store) {
4
+ const [rev, bump] = useReducer((n) => n + 1, 0);
5
+ useEffect(() => store.subscribe(() => bump()), [store]);
6
+ return rev;
7
+ }
8
+ export function useGraphicsSettings(store, shadowsDefault) {
9
+ useSettingsRevision(store);
10
+ const quality = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
11
+ const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
12
+ return {
13
+ shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
14
+ dpr: GRAPHICS_QUALITY_DPR[quality] ?? GRAPHICS_QUALITY_DPR.high,
15
+ uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
16
+ };
17
+ }
18
+ export function AudioSettingsBridge({ store, engine, buses, }) {
19
+ const rev = useSettingsRevision(store);
20
+ useEffect(() => {
21
+ engine.setMasterGain(store.get(SETTING_IDS.masterVolume, DEFAULT_MASTER_VOLUME));
22
+ for (const bus of Object.values(buses ?? {})) {
23
+ engine.setBusGain(bus.id, store.get(busVolumeSettingId(bus.id), bus.gain ?? 1));
24
+ }
25
+ }, [store, engine, buses, rev]);
26
+ return null;
27
+ }
@@ -0,0 +1,20 @@
1
+ import { bindingLabel, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
2
+ import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
3
+ import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
4
+ import { type GameSettingDef, type SettingCategory, type SettingCategoryDef } from "@jgengine/core/settings/settingsModel";
5
+ import { type SettingsActionView, type SettingsKeybindRow, type SettingsCategoryView, type SettingsController, type SettingsRow } from "@jgengine/react/settings";
6
+ export type { SettingsActionView, SettingsKeybindRow, SettingsCategoryView, SettingsController, SettingsRow };
7
+ export interface SettingsControllerInput {
8
+ input: ActionCodesMap;
9
+ buses: Record<string, AudioBusDef> | undefined;
10
+ extra: readonly GameSettingDef[];
11
+ categories: readonly SettingCategoryDef[];
12
+ hide: readonly SettingCategory[];
13
+ fovEnabled: boolean;
14
+ hideBindings: readonly string[];
15
+ overrides: BindingOverrides;
16
+ rebind: (action: string, code: string) => void;
17
+ resetBinding: (action: string) => void;
18
+ }
19
+ export declare function useSettingsCategories(config: SettingsControllerInput): SettingsCategoryView[];
20
+ export { bindingLabel };
@@ -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";
@@ -9,6 +9,7 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
9
9
  center?: FieldGroundOptions["center"];
10
10
  colors?: FieldGroundOptions["colors"];
11
11
  heightRange?: FieldGroundOptions["heightRange"];
12
+ paletteAt?: FieldGroundOptions["paletteAt"];
12
13
  roughness?: number;
13
14
  metalness?: number;
14
15
  /** Bump after a runtime carve/deposit to re-mesh the deformed surface. */
@@ -19,4 +20,4 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
19
20
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
20
21
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
21
22
  */
22
- export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness, metalness, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
23
+ export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness, metalness, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
@@ -8,10 +8,10 @@ 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, 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]);
14
+ [field, size, segments, center, colors, heightRange, paletteAt, epoch]);
15
15
  const material = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
16
16
  useEffect(() => () => geometry.dispose(), [geometry]);
17
17
  useEffect(() => () => material.dispose(), [material]);
@@ -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)),
@@ -6,5 +6,5 @@ export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./Terrafor
6
6
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
7
7
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
8
8
  export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
9
- export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
9
+ export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
10
10
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
@@ -6,5 +6,5 @@ export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
6
6
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
7
7
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
8
8
  export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
9
- export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
9
+ export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
10
10
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
@@ -27,16 +27,25 @@ export interface TerrainVertexColorOptions {
27
27
  waterline?: THREE.ColorRepresentation;
28
28
  waterlineHeight?: number;
29
29
  }
30
+ export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
30
31
  export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainSize;
31
32
  export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
32
33
  export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
33
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
+ };
34
41
  export interface FieldGroundOptions {
35
42
  size?: TerrainArea;
36
43
  segments?: number | readonly [x: number, z: number];
37
44
  center?: readonly [x: number, z: number];
38
45
  colors?: TerrainVertexColorOptions;
39
46
  heightRange?: readonly [min: number, max: number];
47
+ /** Sampled per vertex when set; `colors` still supplies `waterlineHeight` and the fallback. */
48
+ paletteAt?: TerrainPaletteSampler;
40
49
  }
41
50
  /**
42
51
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
@@ -1,6 +1,12 @@
1
1
  import { noiseField } from "@jgengine/core/world/terrain";
2
2
  import * as THREE from "three";
3
3
  import {} from "./random.js";
4
+ export function normalizeHeightBlend(height, minHeight, maxHeight) {
5
+ const range = maxHeight - minHeight;
6
+ if (range <= 1e-6)
7
+ return 0.5;
8
+ return THREE.MathUtils.clamp((height - minHeight) / range, 0, 1);
9
+ }
4
10
  export function resolveTerrainSize(size = 40) {
5
11
  return typeof size === "number" ? { width: size, depth: size } : { width: size[0], depth: size[1] };
6
12
  }
@@ -36,10 +42,12 @@ export function createFieldGroundGeometry(field, options = {}) {
36
42
  colors: options.colors ?? {},
37
43
  center: [cx, cz],
38
44
  heightRange: options.heightRange,
45
+ paletteAt: options.paletteAt,
39
46
  });
40
47
  }
41
48
  function buildGroundGeometry(size, segments, sampler, opts) {
42
49
  const colors = opts.colors;
50
+ const paletteAt = opts.paletteAt;
43
51
  const vertexCountX = segments.x + 1;
44
52
  const vertexCountZ = segments.z + 1;
45
53
  const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
@@ -66,7 +74,14 @@ function buildGroundGeometry(size, segments, sampler, opts) {
66
74
  positions[index + 2] = z;
67
75
  uvs[uvIndex] = u;
68
76
  uvs[uvIndex + 1] = v;
69
- const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
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
+ }
84
+ const blend = normalizeHeightBlend(y, minHeight, maxHeight);
70
85
  const color = low.clone().lerp(high, blend);
71
86
  if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
72
87
  color.lerp(waterline, 0.65);
@@ -133,7 +148,7 @@ export function createProceduralGroundGeometry(config = {}, colors = {}) {
133
148
  positions[index + 2] = z;
134
149
  uvs[uvIndex] = u;
135
150
  uvs[uvIndex + 1] = v;
136
- const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
151
+ const blend = normalizeHeightBlend(y, minHeight, maxHeight);
137
152
  const color = low.clone().lerp(high, blend);
138
153
  if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
139
154
  color.lerp(waterline, 0.65);
@@ -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
+ }
@@ -0,0 +1,34 @@
1
+ import { type MutableRefObject } from "react";
2
+ import { type TouchScheme } from "@jgengine/core/input/touchScheme";
3
+ export interface TouchCodeSink {
4
+ onCodeDown(code: string): void;
5
+ onCodeUp(code: string): void;
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;
22
+ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: {
23
+ scheme: TouchScheme;
24
+ sink: TouchCodeSink;
25
+ yawRef: MutableRefObject<number>;
26
+ pitchRef: MutableRefObject<number>;
27
+ maxPitch: number;
28
+ onPrimaryTap: () => void;
29
+ }): import("react").JSX.Element;
30
+ export declare function TouchControlsDock({ scheme, sink, scale }: {
31
+ scheme: TouchScheme;
32
+ sink: TouchCodeSink;
33
+ scale?: number;
34
+ }): import("react").JSX.Element;