@jgengine/shell 0.8.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.
- package/CHANGELOG.md +16 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +5 -2
- package/dist/GamePlayerShell.js +358 -98
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +5 -24
- package/dist/behaviour.js +14 -50
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.js +14 -0
- package/dist/camera/GameOrbitCamera.js +10 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +3 -0
- package/dist/camera/cameraRigs.js +36 -33
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +3 -0
- package/dist/camera/index.js +3 -0
- package/dist/camera/rigMath.d.ts +10 -0
- package/dist/camera/rigMath.js +36 -1
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +245 -0
- package/dist/defineGame.js +2 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
- package/dist/devtools/DevtoolsOverlay.js +383 -41
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +13 -7
- package/dist/environment/Daylight.js +12 -11
- package/dist/environment/EnvironmentScene.js +82 -35
- package/dist/environment/groundPadMath.d.ts +2 -2
- package/dist/environment/groundPadMath.js +1 -1
- package/dist/environment/index.d.ts +2 -1
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -0
- package/dist/render/modelRender.d.ts +10 -1
- package/dist/render/modelRender.js +32 -5
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/terrainMath.d.ts +8 -0
- package/dist/terrain/terrainMath.js +9 -0
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +17 -1
- package/dist/touch/TouchControlsOverlay.js +115 -9
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +1 -1
- package/dist/world/DataObjects.js +3 -1
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +3 -0
- package/dist/world/WorldHud.js +89 -42
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +51 -0
- package/llms.txt +1404 -1143
- 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
|
|
78
|
-
|
|
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
|
-
|
|
162
|
+
continue;
|
|
81
163
|
const rendered = kit?.renderPart?.(part);
|
|
82
|
-
if (rendered !== undefined)
|
|
83
|
-
|
|
84
|
-
|
|
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";
|
package/dist/structures/index.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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,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 ??
|
|
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)),
|
|
@@ -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,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;
|