@jgengine/shell 0.18.1 → 0.21.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 +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EditorDocument } from "@jgengine/core/editor/index";
|
|
2
2
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
3
|
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
4
|
+
import { type ResolveAuthoredObjectsOptions } from "@jgengine/core/world/authoredObjects";
|
|
4
5
|
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
5
6
|
/**
|
|
6
7
|
* Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
|
|
@@ -23,7 +24,7 @@ export interface AuthoredPathsProps {
|
|
|
23
24
|
*/
|
|
24
25
|
export declare function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): import("react").JSX.Element;
|
|
25
26
|
/** Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. */
|
|
26
|
-
export interface AuthoredObjectsProps {
|
|
27
|
+
export interface AuthoredObjectsProps extends ResolveAuthoredObjectsOptions {
|
|
27
28
|
document: EditorDocument;
|
|
28
29
|
field: TerrainField;
|
|
29
30
|
/** Extra lift in meters on top of each marker's own verticalOffset (default 0). */
|
|
@@ -33,16 +34,29 @@ export interface AuthoredObjectsProps {
|
|
|
33
34
|
* `placeAuthoredObjects` call wins and remounts stay remount-safe.
|
|
34
35
|
*/
|
|
35
36
|
onExisting?: "throw" | "replace" | "keep";
|
|
37
|
+
/** Synchronize authored edits and remove previously owned placements absent from the document. */
|
|
38
|
+
synchronize?: boolean;
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
41
|
* Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
|
|
39
42
|
* WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
|
|
40
43
|
* headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
|
|
41
44
|
*/
|
|
42
|
-
export declare function AuthoredObjects({ document, field, verticalOffset, onExisting, }: AuthoredObjectsProps): null;
|
|
45
|
+
export declare function AuthoredObjects({ document, field, verticalOffset, onExisting, synchronize, excludeKinds, }: AuthoredObjectsProps): null;
|
|
46
|
+
/**
|
|
47
|
+
* Writes the document's studio solids (city buildings and any kind with a `solids` hook) into
|
|
48
|
+
* `ctx.world.solids`, so what the studios draw also blocks the player, NPCs and physics.
|
|
49
|
+
* @capability authored-solids collision for studio-authored world content such as city volumes
|
|
50
|
+
*/
|
|
51
|
+
export declare function AuthoredSolids({ document, field }: {
|
|
52
|
+
document: EditorDocument;
|
|
53
|
+
field: TerrainField;
|
|
54
|
+
}): null;
|
|
43
55
|
/** Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. */
|
|
44
56
|
export interface AuthoredSceneProps {
|
|
45
57
|
document: EditorDocument;
|
|
58
|
+
/** Register bounded procedural weather counts in the supported diagnostics probes. */
|
|
59
|
+
diagnostics?: boolean;
|
|
46
60
|
field: TerrainField;
|
|
47
61
|
/** Restrict rendered path kinds; default renders every non-scatter path. */
|
|
48
62
|
pathKinds?: readonly string[];
|
|
@@ -52,6 +66,8 @@ export interface AuthoredSceneProps {
|
|
|
52
66
|
* dedicated map even if it shares entries with `entityModels`). Requires `assets`.
|
|
53
67
|
*/
|
|
54
68
|
scatterModels?: Record<string, string | ModelConfig>;
|
|
69
|
+
/** Animated models keyed by authored cosmetic habitat species. */
|
|
70
|
+
flockModels?: Record<string, string | ModelConfig>;
|
|
55
71
|
/** Catalog `scatterModels` string ids resolve through; required together with `scatterModels`. */
|
|
56
72
|
assets?: AssetCatalog;
|
|
57
73
|
/**
|
|
@@ -63,11 +79,12 @@ export interface AuthoredSceneProps {
|
|
|
63
79
|
/**
|
|
64
80
|
* Place the document's catalog-id markers into the object
|
|
65
81
|
* store — WorldScene renders them via the game's `objectModels` seam. Omit when the game places
|
|
66
|
-
* props itself in onInit with `placeAuthoredObjects`.
|
|
82
|
+
* props itself in onInit with `placeAuthoredObjects`. `excludeKinds` replaces the resolver's
|
|
83
|
+
* default mob/boss exclusions; include those when adding other game-owned entity spawn kinds.
|
|
67
84
|
*/
|
|
68
|
-
placeObjects?: boolean | {
|
|
85
|
+
placeObjects?: boolean | ({
|
|
69
86
|
verticalOffset?: number;
|
|
70
|
-
};
|
|
87
|
+
} & ResolveAuthoredObjectsOptions);
|
|
71
88
|
/** Terrain surface color sampler forwarded to studio renderers (see `SceneKindRenderContext.groundColorAt`). */
|
|
72
89
|
groundColorAt?: (x: number, z: number) => string;
|
|
73
90
|
}
|
|
@@ -81,4 +98,4 @@ export interface AuthoredSceneProps {
|
|
|
81
98
|
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
82
99
|
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
83
100
|
*/
|
|
84
|
-
export declare function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
101
|
+
export declare function AuthoredScene({ document, diagnostics, field, pathKinds, scatterModels, flockModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { AuthoredFlocks } from "../world/AuthoredFlocks.js";
|
|
3
|
+
import { AuthoredWeatherLayer } from "../weather/AuthoredWeatherLayer.js";
|
|
4
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
3
5
|
import * as THREE from "three";
|
|
4
6
|
import { getDocumentLiveSync, subscribeDocumentLiveSyncInstall, } from "@jgengine/core/editor/index";
|
|
5
|
-
import { placeAuthoredObjects,
|
|
7
|
+
import { placeAuthoredObjects, resolveAuthoredObjectsWithDiagnostics, syncAuthoredObjects, } from "@jgengine/core/world/authoredObjects";
|
|
8
|
+
import { syncAuthoredSolids } from "@jgengine/core/world/authoredSolids";
|
|
6
9
|
import { buildRoadRibbon, GROUND_DECAL_LAYERS, roundPathCorners } from "@jgengine/core/world/roads";
|
|
7
10
|
import { isScatterPath, resolveScatter } from "@jgengine/core/world/scatterRegion";
|
|
8
11
|
import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
|
|
@@ -130,17 +133,47 @@ export function AuthoredPaths({ document, field, kinds }) {
|
|
|
130
133
|
* WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
|
|
131
134
|
* headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
|
|
132
135
|
*/
|
|
133
|
-
export function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", }) {
|
|
136
|
+
export function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", synchronize = false, excludeKinds, }) {
|
|
134
137
|
const ctx = useGameContext();
|
|
135
|
-
const
|
|
138
|
+
const previousObjects = useRef([]);
|
|
139
|
+
const previousStore = useRef(ctx.scene.object);
|
|
140
|
+
const resolved = useMemo(() => resolveAuthoredObjectsWithDiagnostics(document, excludeKinds === undefined ? {} : { excludeKinds }), [document, excludeKinds]);
|
|
141
|
+
const warnedResult = useRef(null);
|
|
142
|
+
const objects = resolved.objects;
|
|
136
143
|
useEffect(() => {
|
|
137
|
-
if (
|
|
144
|
+
if (warnedResult.current !== resolved) {
|
|
145
|
+
warnedResult.current = resolved;
|
|
146
|
+
for (const diagnostic of resolved.diagnostics)
|
|
147
|
+
console.warn(`[jgengine] ${diagnostic.path}: ${diagnostic.message}. ${diagnostic.repair}`);
|
|
148
|
+
}
|
|
149
|
+
if (previousStore.current !== ctx.scene.object) {
|
|
150
|
+
previousObjects.current = [];
|
|
151
|
+
previousStore.current = ctx.scene.object;
|
|
152
|
+
}
|
|
153
|
+
if (synchronize) {
|
|
154
|
+
previousObjects.current = syncAuthoredObjects(ctx.scene.object, objects, previousObjects.current, (x, z) => field.sampleHeight(x, z), { verticalOffset });
|
|
138
155
|
return;
|
|
156
|
+
}
|
|
139
157
|
placeAuthoredObjects(ctx.scene.object, objects, (x, z) => field.sampleHeight(x, z), {
|
|
140
158
|
verticalOffset,
|
|
141
159
|
onExisting,
|
|
142
160
|
});
|
|
143
|
-
}, [ctx.scene.object, field, objects, onExisting, verticalOffset]);
|
|
161
|
+
}, [ctx.scene.object, field, resolved, objects, onExisting, verticalOffset, synchronize]);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Writes the document's studio solids (city buildings and any kind with a `solids` hook) into
|
|
166
|
+
* `ctx.world.solids`, so what the studios draw also blocks the player, NPCs and physics.
|
|
167
|
+
* @capability authored-solids collision for studio-authored world content such as city volumes
|
|
168
|
+
*/
|
|
169
|
+
export function AuthoredSolids({ document, field }) {
|
|
170
|
+
const ctx = useGameContext();
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
syncAuthoredSolids(ctx.world.solids, document, (x, z) => field.sampleHeight(x, z));
|
|
173
|
+
}, [ctx.world.solids, document, field]);
|
|
174
|
+
useEffect(() => () => {
|
|
175
|
+
syncAuthoredSolids(ctx.world.solids, { markers: [], volumes: [], paths: [] });
|
|
176
|
+
}, [ctx.world.solids]);
|
|
144
177
|
return null;
|
|
145
178
|
}
|
|
146
179
|
/**
|
|
@@ -153,16 +186,20 @@ export function AuthoredObjects({ document, field, verticalOffset = 0, onExistin
|
|
|
153
186
|
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
154
187
|
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
155
188
|
*/
|
|
156
|
-
export function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, groundColorAt, }) {
|
|
189
|
+
export function AuthoredScene({ document, diagnostics = false, field, pathKinds, scatterModels, flockModels, assets, live = true, placeObjects, groundColorAt, }) {
|
|
190
|
+
const ctx = useGameContext();
|
|
157
191
|
const liveDocument = useLiveEditorDocument(document, live);
|
|
192
|
+
// Authoring updates configuration only. Physics remains owned by the fixed game-time stages.
|
|
193
|
+
useEffect(() => { ctx.environment.retune(liveDocument); }, [ctx.environment, liveDocument]);
|
|
158
194
|
const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]);
|
|
159
195
|
const resolveItem = useMemo(() => createModelMapResolver(scatterModels, assets, "scatterModels"), [scatterModels, assets]);
|
|
160
196
|
const shouldPlaceObjects = placeObjects === true || (typeof placeObjects === "object" && placeObjects !== null);
|
|
161
197
|
const objectVerticalOffset = typeof placeObjects === "object" && placeObjects !== null ? (placeObjects.verticalOffset ?? 0) : 0;
|
|
162
|
-
|
|
198
|
+
const objectExcludeKinds = typeof placeObjects === "object" && placeObjects !== null ? placeObjects.excludeKinds : undefined;
|
|
199
|
+
return (_jsxs(_Fragment, { children: [_jsx(AuthoredWeatherLayer, { document: liveDocument, heightAt: field.sampleHeight, diagnostics: diagnostics }), flockModels === undefined ? null : _jsx(AuthoredFlocks, { models: flockModels, assets: assets }), _jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
|
|
163
200
|
document: liveDocument,
|
|
164
201
|
field,
|
|
165
202
|
...(assets === undefined ? {} : { assets }),
|
|
166
203
|
...(groundColorAt === undefined ? {} : { groundColorAt }),
|
|
167
|
-
} }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset })) : null] }));
|
|
204
|
+
} }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), _jsx(AuthoredSolids, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset, synchronize: live && getDocumentLiveSync() !== null, ...(objectExcludeKinds === undefined ? {} : { excludeKinds: objectExcludeKinds }) })) : null] }));
|
|
168
205
|
}
|
|
@@ -22,7 +22,7 @@ import { offsetPath } from "@jgengine/core/world/streets";
|
|
|
22
22
|
import { triangulatePolygon, triangulateRingBand } from "@jgengine/core/world/cityGeometry";
|
|
23
23
|
import { resolveCityObject, CITY_KIND, CITY_ZONE_KIND, } from "@jgengine/core/world/cityKind";
|
|
24
24
|
import { zoneBand, zoneMetric } from "@jgengine/core/world/cityContent";
|
|
25
|
-
import { generateBuilding, resolveBuildingPalette, resolveBuildingWallTones, } from "@jgengine/core/world/buildings";
|
|
25
|
+
import { generateBuilding, buildingSurfaceColor, resolveBuildingPalette, resolveBuildingWallTones, } from "@jgengine/core/world/buildings";
|
|
26
26
|
import { hashString } from "@jgengine/core/random/rng";
|
|
27
27
|
import { buildScatterProxy } from "../scatter/scatterProxies.js";
|
|
28
28
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
@@ -327,12 +327,12 @@ function unitGeometries() {
|
|
|
327
327
|
const ROLE_JITTER = { wall: 1, roof: 0.5, trim: 0.35, accent: 0.6 };
|
|
328
328
|
function roleColor(palette, role) {
|
|
329
329
|
if (role === "wall")
|
|
330
|
-
return palette.wall;
|
|
330
|
+
return buildingSurfaceColor(palette.wall);
|
|
331
331
|
if (role === "roof")
|
|
332
|
-
return palette.roof;
|
|
332
|
+
return buildingSurfaceColor(palette.roof);
|
|
333
333
|
if (role === "trim")
|
|
334
|
-
return palette.corner;
|
|
335
|
-
return palette.awning;
|
|
334
|
+
return buildingSurfaceColor(palette.corner);
|
|
335
|
+
return buildingSurfaceColor(palette.awning);
|
|
336
336
|
}
|
|
337
337
|
const DETAIL_RADIUS = 95;
|
|
338
338
|
const DETAIL_MAX = 12;
|
|
@@ -547,7 +547,7 @@ function OneCity({ object, context }) {
|
|
|
547
547
|
return mesh;
|
|
548
548
|
};
|
|
549
549
|
const plain = new THREE.MeshStandardMaterial({ roughness: 0.85, metalness: 0.04 });
|
|
550
|
-
const banded = createCityWindowMaterial({ floorHeight: resolved.rules.floorHeight, windowColor: palette.window });
|
|
550
|
+
const banded = createCityWindowMaterial({ floorHeight: resolved.rules.floorHeight, windowColor: buildingSurfaceColor(palette.window) });
|
|
551
551
|
const meshes = [
|
|
552
552
|
makeMesh("box", units.box, plain),
|
|
553
553
|
makeMesh("banded", units.box, banded),
|
|
@@ -1,49 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import * as THREE from "three";
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
4
3
|
import { resolveGeneratorAsset } from "@jgengine/core/scene/assetGenerator";
|
|
5
|
-
|
|
4
|
+
import { StaticShapeInstances } from "../render/staticShapeInstances.js";
|
|
6
5
|
const NEUTRAL = "#b8b0a4";
|
|
7
6
|
/** All parts of one generated asset, batched into one instanced box draw per color. */
|
|
8
7
|
function GeneratedParts({ asset }) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
bucket.push(part);
|
|
18
|
-
}
|
|
19
|
-
return [...groups.entries()];
|
|
20
|
-
}, [asset]);
|
|
21
|
-
return (_jsx(_Fragment, { children: byColor.map(([color, parts]) => (_jsx(ColorBatch, { color: color, parts: parts }, color))) }));
|
|
22
|
-
}
|
|
23
|
-
function ColorBatch({ color, parts }) {
|
|
24
|
-
const meshRef = useRef(null);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const mesh = meshRef.current;
|
|
27
|
-
if (mesh === null)
|
|
28
|
-
return;
|
|
29
|
-
const matrix = new THREE.Matrix4();
|
|
30
|
-
const position = new THREE.Vector3();
|
|
31
|
-
const quaternion = new THREE.Quaternion();
|
|
32
|
-
const euler = new THREE.Euler();
|
|
33
|
-
const scale = new THREE.Vector3();
|
|
34
|
-
parts.forEach((part, index) => {
|
|
35
|
-
position.set(part.position[0], part.position[1], part.position[2]);
|
|
36
|
-
euler.set(0, part.rotationY ?? 0, 0);
|
|
37
|
-
quaternion.setFromEuler(euler);
|
|
38
|
-
scale.set(Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2]));
|
|
39
|
-
matrix.compose(position, quaternion, scale);
|
|
40
|
-
mesh.setMatrixAt(index, matrix);
|
|
41
|
-
});
|
|
42
|
-
mesh.count = parts.length;
|
|
43
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
44
|
-
mesh.computeBoundingSphere();
|
|
45
|
-
}, [parts]);
|
|
46
|
-
return (_jsx("instancedMesh", { ref: meshRef, args: [UNIT_BOX, undefined, Math.max(1, parts.length)], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 0.62, metalness: 0.04 }) }));
|
|
8
|
+
const instances = useMemo(() => asset.parts.map(part => ({
|
|
9
|
+
shape: "box",
|
|
10
|
+
position: part.position,
|
|
11
|
+
rotation: [0, part.rotationY ?? 0, 0],
|
|
12
|
+
scale: [Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2])],
|
|
13
|
+
surface: { color: part.color ?? NEUTRAL, roughness: 0.62, metalness: 0.04 },
|
|
14
|
+
})), [asset]);
|
|
15
|
+
return _jsx(StaticShapeInstances, { instances: instances });
|
|
47
16
|
}
|
|
48
17
|
/**
|
|
49
18
|
* Renders one placed generator-asset instance by re-resolving its `meta` through the registered
|
|
@@ -14,7 +14,7 @@ function QuickToggle({ row }) {
|
|
|
14
14
|
function QuickSelect({ row }) {
|
|
15
15
|
const options = row.options ?? [];
|
|
16
16
|
const current = String(row.value);
|
|
17
|
-
return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("div", { className: "flex
|
|
17
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("div", { className: "flex flex-wrap justify-end gap-1 rounded-md bg-neutral-950/60 p-0.5 ring-1 ring-white/10", children: options.map((option) => {
|
|
18
18
|
const selected = option.value === current;
|
|
19
19
|
return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: `rounded px-2 py-1 font-medium transition ${selected ? "bg-emerald-500 text-neutral-950" : "text-neutral-300 hover:bg-neutral-800"}`, children: option.label }, option.value));
|
|
20
20
|
}) })] }));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
+
import { ScrollRail } from "@jgengine/react/scrollRail";
|
|
4
|
+
import { useDialogBehavior } from "@jgengine/react/dialogBehavior";
|
|
3
5
|
import { useDebouncedCommit } from "@jgengine/react/useDebouncedCommit";
|
|
4
6
|
import {} from "./settingsController.js";
|
|
5
7
|
const surface = "var(--jg-surface, #16161d)";
|
|
@@ -40,9 +42,9 @@ function ToggleRow({ row }) {
|
|
|
40
42
|
function SelectRow({ row }) {
|
|
41
43
|
const options = row.options ?? [];
|
|
42
44
|
const current = String(row.value);
|
|
43
|
-
return (_jsx(RowShell, { label: row.label, control: _jsx("div", { className: "flex
|
|
45
|
+
return (_jsx(RowShell, { label: row.label, control: _jsx("div", { role: "group", "aria-label": row.label, className: "flex flex-wrap justify-end gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
|
|
44
46
|
const selected = option.value === current;
|
|
45
|
-
return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: "min-h-[34px] rounded-md px-3 text-sm font-medium transition", style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: option.label }, option.value));
|
|
47
|
+
return (_jsx("button", { type: "button", onClick: () => row.set(option.value), "aria-pressed": selected, className: "min-h-[34px] rounded-md px-3 text-sm font-medium transition", style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: option.label }, option.value));
|
|
46
48
|
}) }) }));
|
|
47
49
|
}
|
|
48
50
|
function RowControl({ row }) {
|
|
@@ -68,10 +70,10 @@ function ActionRow({ action, onClose }) {
|
|
|
68
70
|
}, children: [_jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-sm font-semibold", style: { color: isDanger ? danger : text }, children: action.label }), action.description !== undefined ? (_jsx("span", { className: "text-xs", style: { color: textDim }, children: action.description })) : null] }), _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4 shrink-0", style: { color: isDanger ? danger : textDim }, "aria-hidden": true, children: _jsx("path", { d: "M9 6l6 6-6 6" }) })] }));
|
|
69
71
|
}
|
|
70
72
|
function TabButton({ tab, selected, orientation, onSelect }) {
|
|
71
|
-
return (_jsx("button", { type: "button", onClick: onSelect, className: `min-h-[
|
|
73
|
+
return (_jsx("button", { type: "button", onClick: onSelect, className: `shrink-0 min-h-[44px] whitespace-nowrap rounded-lg px-4 text-sm font-medium transition ${orientation === "col" ? "w-full text-left" : ""} ${selected ? "" : "hover:bg-white/5"}`, style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: tab.label }));
|
|
72
74
|
}
|
|
73
75
|
function Body({ tab, actions, capturing, setCapturing, onClose }) {
|
|
74
|
-
return (_jsx("div", { className: "jg-settings-scroll flex-1 space-y-2 overflow-y-auto p-4", children: tab.category === null
|
|
76
|
+
return (_jsx("div", { className: "jg-settings-scroll min-h-0 flex-1 space-y-2 overflow-y-auto p-4", children: tab.category === null
|
|
75
77
|
? actions.map((action) => _jsx(ActionRow, { action: action, onClose: onClose }, action.id))
|
|
76
78
|
: (_jsxs(_Fragment, { children: [tab.category.rows.map((row) => (_jsx(RowControl, { row: row }, row.id))), tab.category.keybinds.map((row) => (_jsx(KeybindRowView, { row: row, capturing: capturing?.action === row.action, onCapture: () => setCapturing(row) }, row.action)))] })) }));
|
|
77
79
|
}
|
|
@@ -95,6 +97,7 @@ export function SettingsMenu({ controller, onClose, initialTab, }) {
|
|
|
95
97
|
const [activeId, setActiveId] = useState(initialTab !== undefined && tabs.some((tab) => tab.id === initialTab) ? initialTab : (tabs[0]?.id ?? ""));
|
|
96
98
|
const [capturing, setCapturing] = useState(null);
|
|
97
99
|
const active = tabs.find((tab) => tab.id === activeId) ?? tabs[0] ?? null;
|
|
100
|
+
const dialogRef = useDialogBehavior({ open: active !== null, onClose: capturing === null ? onClose : undefined });
|
|
98
101
|
useEffect(() => {
|
|
99
102
|
if (capturing === null)
|
|
100
103
|
return;
|
|
@@ -124,28 +127,18 @@ export function SettingsMenu({ controller, onClose, initialTab, }) {
|
|
|
124
127
|
window.removeEventListener("contextmenu", onContext, true);
|
|
125
128
|
};
|
|
126
129
|
}, [capturing]);
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (capturing !== null)
|
|
129
|
-
return;
|
|
130
|
-
const onKey = (event) => {
|
|
131
|
-
if (event.code === "Escape")
|
|
132
|
-
onClose();
|
|
133
|
-
};
|
|
134
|
-
window.addEventListener("keydown", onKey);
|
|
135
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
136
|
-
}, [capturing, onClose]);
|
|
137
130
|
if (active === null)
|
|
138
131
|
return null;
|
|
139
132
|
const bodyProps = { tab: active, actions: controller.actions, capturing, setCapturing, onClose };
|
|
140
133
|
const title = (_jsx("h2", { className: "text-base font-semibold tracking-wide", style: { color: text }, children: "Settings" }));
|
|
141
134
|
if (controller.variant === "fullscreen") {
|
|
142
|
-
return (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-40 flex flex-col", style: { background: surface, color: text, fontFamily: fontDisplay }, children: _jsxs("div", { className: "mx-auto flex h-full w-full max-w-2xl flex-col px-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 pb-4 pt-6", children: [_jsx("h2", { className: "text-2xl font-bold tracking-wide", style: { color: text }, children: "Settings" }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(
|
|
135
|
+
return (_jsx("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, className: "pointer-events-auto absolute inset-0 z-40 flex flex-col", style: { background: surface, color: text, fontFamily: fontDisplay }, children: _jsxs("div", { className: "mx-auto flex h-full w-full max-w-2xl flex-col px-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 pb-4 pt-6", children: [_jsx("h2", { className: "text-2xl font-bold tracking-wide", style: { color: text }, children: "Settings" }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(ScrollRail, { label: "settings categories", style: { paddingBottom: 16 }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
|
|
143
136
|
}
|
|
144
137
|
if (controller.variant === "sidebar") {
|
|
145
|
-
return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(94vw, 760px)", height: "min(84vh, 560px)" }, children: [_jsxs("nav", { className: "flex w-44 shrink-0 flex-col gap-1 p-3", style: { background: surfaceDeep, borderRight: `1px solid ${edge}` }, children: [_jsx("span", { className: "px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-widest", style: { color: textDim }, children: "Settings" }), tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "col", onSelect: () => setActiveId(tab.id) }, tab.id)))] }), _jsxs("div", { className: "flex flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: [_jsx("span", { className: "text-sm font-semibold", style: { color: text }, children: active.label }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(Body, { ...bodyProps })] })] }) }));
|
|
138
|
+
return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, onClick: stop, className: "flex overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(94vw, 760px)", height: "min(84vh, 560px)" }, children: [_jsxs("nav", { className: "flex w-44 shrink-0 flex-col gap-1 overflow-y-auto p-3", style: { background: surfaceDeep, borderRight: `1px solid ${edge}` }, children: [_jsx("span", { className: "px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-widest", style: { color: textDim }, children: "Settings" }), tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "col", onSelect: () => setActiveId(tab.id) }, tab.id)))] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: [_jsx("span", { className: "text-sm font-semibold", style: { color: text }, children: active.label }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(Body, { ...bodyProps })] })] }) }));
|
|
146
139
|
}
|
|
147
140
|
if (controller.variant === "sheet") {
|
|
148
|
-
return (_jsx(Backdrop, { onClose: onClose, align: "items-end", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-t-3xl shadow-2xl", style: { ...panelStyle, width: "min(100%, 640px)", height: "min(84vh, 620px)" }, children: [_jsx("div", { className: "flex flex-col items-center pt-2.5", children: _jsx("span", { className: "h-1.5 w-10 rounded-full", style: { background: "rgba(255,255,255,0.25)" } }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("
|
|
141
|
+
return (_jsx(Backdrop, { onClose: onClose, align: "items-end", children: _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, onClick: stop, className: "flex flex-col overflow-hidden rounded-t-3xl shadow-2xl", style: { ...panelStyle, width: "min(100%, 640px)", height: "min(84vh, 620px)" }, children: [_jsx("div", { className: "flex flex-col items-center pt-2.5", children: _jsx("span", { className: "h-1.5 w-10 rounded-full", style: { background: "rgba(255,255,255,0.25)" } }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx(ScrollRail, { label: "settings categories", style: { padding: "0 16px 12px" }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx("div", { style: { borderTop: `1px solid ${edge}` }, className: "flex flex-1 flex-col overflow-hidden", children: _jsx(Body, { ...bodyProps }) })] }) }));
|
|
149
142
|
}
|
|
150
|
-
return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(92vw, 460px)", height: "min(86vh, 580px)" }, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3.5", style: { borderBottom: `1px solid ${edge}` }, children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx(
|
|
143
|
+
return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, onClick: stop, className: "flex flex-col overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(92vw, 460px)", height: "min(86vh, 580px)" }, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3.5", style: { borderBottom: `1px solid ${edge}` }, children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx(ScrollRail, { label: "settings categories", style: { padding: "12px 16px", borderBottom: `1px solid ${edge}` }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
|
|
151
144
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
3
|
import type { SettingsSurface, SettingsVariant } from "@jgengine/core/settings/settingsModel";
|
|
3
4
|
import { type SettingsActionView } from "@jgengine/react/settings";
|
|
4
5
|
import { type SettingsControllerInput } from "./settingsController.js";
|
|
@@ -9,3 +10,7 @@ export interface SettingsRuntimeProps extends SettingsControllerInput {
|
|
|
9
10
|
children: ReactNode;
|
|
10
11
|
}
|
|
11
12
|
export declare function SettingsRuntime({ variant, surface, actions, children, ...input }: SettingsRuntimeProps): import("react").JSX.Element;
|
|
13
|
+
/** Owns settings' suspension for this live context, including close, replacement and unmount. @internal */
|
|
14
|
+
export declare function SettingsPlayControlGate({ ctx }: {
|
|
15
|
+
ctx: GameContext;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { suspendPlayControls } from "@jgengine/core/game/controlGate";
|
|
4
|
+
import { SettingsControllerProvider, useSettings, } from "@jgengine/react/settings";
|
|
5
|
+
import { createPlaySurfaceFocusOwnership } from "../presentationRecovery.js";
|
|
4
6
|
import { useSettingsCategories } from "./settingsController.js";
|
|
5
7
|
export function SettingsRuntime({ variant, surface, actions, children, ...input }) {
|
|
6
8
|
const categories = useSettingsCategories(input);
|
|
@@ -17,3 +19,28 @@ export function SettingsRuntime({ variant, surface, actions, children, ...input
|
|
|
17
19
|
}), [categories, actions, variant, surface, isOpen]);
|
|
18
20
|
return _jsx(SettingsControllerProvider, { controller: controller, children: children });
|
|
19
21
|
}
|
|
22
|
+
/** Owns settings' suspension for this live context, including close, replacement and unmount. @internal */
|
|
23
|
+
export function SettingsPlayControlGate({ ctx }) {
|
|
24
|
+
const { isOpen } = useSettings();
|
|
25
|
+
const anchor = useRef(null);
|
|
26
|
+
const openedSurface = useRef(null);
|
|
27
|
+
const focus = useRef(createPlaySurfaceFocusOwnership());
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
if (isOpen) {
|
|
30
|
+
openedSurface.current = { owner: ctx, surface: anchor.current?.closest('div[tabindex="0"]') ?? null };
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const previous = openedSurface.current;
|
|
34
|
+
openedSurface.current = null;
|
|
35
|
+
if (previous?.owner !== ctx)
|
|
36
|
+
return;
|
|
37
|
+
focus.current.request(ctx);
|
|
38
|
+
focus.current.reveal(ctx, previous.surface, document.activeElement, document.body);
|
|
39
|
+
}, [ctx, isOpen]);
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
if (!isOpen)
|
|
42
|
+
return;
|
|
43
|
+
return suspendPlayControls(ctx);
|
|
44
|
+
}, [ctx, isOpen]);
|
|
45
|
+
return _jsx("span", { hidden: true, ref: anchor });
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
2
|
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
3
|
-
import {
|
|
3
|
+
import type { GraphicsProfile } from "@jgengine/core/settings/graphicsProfile";
|
|
4
|
+
import { type GraphicsProfileOverrides } from "@jgengine/core/settings/graphicsSettings";
|
|
4
5
|
import { type TouchJoystickVariant, type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
5
6
|
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
6
7
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
@@ -10,13 +11,18 @@ export declare function useTouchStyle(store: SettingsStore, fallback: TouchStyle
|
|
|
10
11
|
/** Shell-internal: binds the SettingsStore joystick-variant choice (fixed vs floating) to the touch renderer. @internal */
|
|
11
12
|
export declare function useTouchJoystickVariant(store: SettingsStore): TouchJoystickVariant;
|
|
12
13
|
export declare function useSettingsRevision(store: SettingsStore): number;
|
|
13
|
-
|
|
14
|
+
/** The player's graphics choices as the shell applies them. */
|
|
15
|
+
export interface AppliedGraphicsSettings {
|
|
14
16
|
shadows: boolean;
|
|
15
17
|
dpr: number;
|
|
16
18
|
uiScale: number;
|
|
17
19
|
quality: GraphicsQuality;
|
|
18
20
|
profile: GraphicsProfile;
|
|
19
|
-
|
|
21
|
+
/** Frames-per-second cap; 0 renders every display refresh. */
|
|
22
|
+
frameRateLimit: number;
|
|
23
|
+
showFps: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean, overrides?: GraphicsProfileOverrides): AppliedGraphicsSettings;
|
|
20
26
|
export declare function AudioSettingsBridge({ store, engine, buses, }: {
|
|
21
27
|
store: SettingsStore;
|
|
22
28
|
engine: AudioEngine;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
-
import { busVolumeSettingId,
|
|
3
|
-
import {
|
|
2
|
+
import { busVolumeSettingId, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { readGraphicsSettings } from "@jgengine/core/settings/graphicsSettings";
|
|
4
|
+
import { readFrameRateLimit } from "@jgengine/core/settings/frameRateLimit";
|
|
4
5
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANTS, TOUCH_STYLES, } from "@jgengine/core/input/touchScheme";
|
|
5
6
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
6
7
|
export const TOUCH_STYLE_AUTO = "auto";
|
|
@@ -29,9 +30,7 @@ export function useSettingsRevision(store) {
|
|
|
29
30
|
}
|
|
30
31
|
export function useGraphicsSettings(store, shadowsDefault, overrides) {
|
|
31
32
|
useSettingsRevision(store);
|
|
32
|
-
const
|
|
33
|
-
const quality = GRAPHICS_QUALITY_DPR[rawQuality] !== undefined ? rawQuality : "high";
|
|
34
|
-
const profile = resolveGraphicsProfile(quality, overrides?.[quality]);
|
|
33
|
+
const { quality, profile } = readGraphicsSettings(store, overrides);
|
|
35
34
|
const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
|
|
36
35
|
return {
|
|
37
36
|
shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
|
|
@@ -39,6 +38,8 @@ export function useGraphicsSettings(store, shadowsDefault, overrides) {
|
|
|
39
38
|
uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
|
|
40
39
|
quality,
|
|
41
40
|
profile,
|
|
41
|
+
frameRateLimit: readFrameRateLimit(store),
|
|
42
|
+
showFps: store.get(SETTING_IDS.graphicsShowFps, false),
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
export function AudioSettingsBridge({ store, engine, buses, }) {
|
|
@@ -2,6 +2,7 @@ import { bindingLabel, type ActionCodesMap } from "@jgengine/core/input/actionBi
|
|
|
2
2
|
import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
|
|
3
3
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
4
4
|
import { type GameSettingDef, type SettingCategory, type SettingCategoryDef } from "@jgengine/core/settings/settingsModel";
|
|
5
|
+
import { type GraphicsProfileOverrides } from "@jgengine/core/settings/graphicsSettings";
|
|
5
6
|
import { type SettingsActionView, type SettingsKeybindRow, type SettingsCategoryView, type SettingsController, type SettingsRow } from "@jgengine/react/settings";
|
|
6
7
|
export type { SettingsActionView, SettingsKeybindRow, SettingsCategoryView, SettingsController, SettingsRow };
|
|
7
8
|
export interface SettingsControllerInput {
|
|
@@ -11,6 +12,8 @@ export interface SettingsControllerInput {
|
|
|
11
12
|
categories: readonly SettingCategoryDef[];
|
|
12
13
|
hide: readonly SettingCategory[];
|
|
13
14
|
fovEnabled: boolean;
|
|
15
|
+
/** The game's per-tier profile overrides (`defineGame({ graphics })`), so tier defaults match what renders. */
|
|
16
|
+
graphics?: GraphicsProfileOverrides;
|
|
14
17
|
hideBindings: readonly string[];
|
|
15
18
|
/** Show the touch-controls skin selector in Controls (touch device with a dock). */
|
|
16
19
|
touchStyle: boolean;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
2
|
-
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId,
|
|
2
|
+
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId, DEFAULT_GRAPHICS_SHADOWS, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { ANISOTROPY_OPTIONS, applyGraphicsQuality, GRAPHICS_POST_STAGE_LABELS, GRAPHICS_POST_STAGES, graphicsPostStageSettingId, readGraphicsSettings, RENDER_SCALE_MAX, RENDER_SCALE_MIN, RENDER_SCALE_STEP, } from "@jgengine/core/settings/graphicsSettings";
|
|
4
|
+
import { FRAME_RATE_LIMIT_DISPLAY, FRAME_RATE_LIMIT_OPTIONS } from "@jgengine/core/settings/frameRateLimit";
|
|
3
5
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANT_OPTIONS, TOUCH_STYLE_OPTIONS, } from "@jgengine/core/input/touchScheme";
|
|
4
6
|
import { TOUCH_STYLE_AUTO, useSettingsRevision } from "./appliedSettings.js";
|
|
5
7
|
import { useSettingsStore, } from "@jgengine/react/settings";
|
|
@@ -61,14 +63,42 @@ export function useSettingsCategories(config) {
|
|
|
61
63
|
}),
|
|
62
64
|
...extrasFor("sound"),
|
|
63
65
|
];
|
|
66
|
+
const graphics = readGraphicsSettings(store, config.graphics);
|
|
64
67
|
const graphicsRows = [
|
|
65
68
|
{
|
|
66
69
|
id: SETTING_IDS.graphicsQuality,
|
|
67
70
|
label: "Quality",
|
|
68
71
|
kind: "select",
|
|
69
|
-
value:
|
|
72
|
+
value: graphics.quality,
|
|
70
73
|
options: GRAPHICS_QUALITY_OPTIONS,
|
|
71
|
-
set: (value) => store
|
|
74
|
+
set: (value) => applyGraphicsQuality(store, String(value), config.graphics),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: SETTING_IDS.graphicsRenderScale,
|
|
78
|
+
label: "Render scale",
|
|
79
|
+
kind: "slider",
|
|
80
|
+
value: graphics.profile.renderScale,
|
|
81
|
+
min: RENDER_SCALE_MIN,
|
|
82
|
+
max: RENDER_SCALE_MAX,
|
|
83
|
+
step: RENDER_SCALE_STEP,
|
|
84
|
+
format: percent,
|
|
85
|
+
set: (value) => store.set(SETTING_IDS.graphicsRenderScale, value),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: SETTING_IDS.graphicsFrameRateLimit,
|
|
89
|
+
label: "Frame rate limit",
|
|
90
|
+
kind: "select",
|
|
91
|
+
value: store.get(SETTING_IDS.graphicsFrameRateLimit, FRAME_RATE_LIMIT_DISPLAY),
|
|
92
|
+
options: FRAME_RATE_LIMIT_OPTIONS,
|
|
93
|
+
set: (value) => store.set(SETTING_IDS.graphicsFrameRateLimit, String(value)),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: SETTING_IDS.graphicsAnisotropy,
|
|
97
|
+
label: "Texture filtering",
|
|
98
|
+
kind: "select",
|
|
99
|
+
value: String(graphics.profile.anisotropy),
|
|
100
|
+
options: ANISOTROPY_OPTIONS,
|
|
101
|
+
set: (value) => store.set(SETTING_IDS.graphicsAnisotropy, String(value)),
|
|
72
102
|
},
|
|
73
103
|
{
|
|
74
104
|
id: SETTING_IDS.graphicsShadows,
|
|
@@ -77,6 +107,13 @@ export function useSettingsCategories(config) {
|
|
|
77
107
|
value: store.get(SETTING_IDS.graphicsShadows, DEFAULT_GRAPHICS_SHADOWS),
|
|
78
108
|
set: (value) => store.set(SETTING_IDS.graphicsShadows, value),
|
|
79
109
|
},
|
|
110
|
+
...GRAPHICS_POST_STAGES.map((stage) => ({
|
|
111
|
+
id: graphicsPostStageSettingId(stage),
|
|
112
|
+
label: GRAPHICS_POST_STAGE_LABELS[stage],
|
|
113
|
+
kind: "toggle",
|
|
114
|
+
value: graphics.profile.postStages[stage],
|
|
115
|
+
set: (value) => store.set(graphicsPostStageSettingId(stage), value),
|
|
116
|
+
})),
|
|
80
117
|
{
|
|
81
118
|
id: SETTING_IDS.graphicsUiScale,
|
|
82
119
|
label: "UI scale",
|
|
@@ -88,6 +125,13 @@ export function useSettingsCategories(config) {
|
|
|
88
125
|
format: percent,
|
|
89
126
|
set: (value) => store.set(SETTING_IDS.graphicsUiScale, value),
|
|
90
127
|
},
|
|
128
|
+
{
|
|
129
|
+
id: SETTING_IDS.graphicsShowFps,
|
|
130
|
+
label: "Show FPS",
|
|
131
|
+
kind: "toggle",
|
|
132
|
+
value: store.get(SETTING_IDS.graphicsShowFps, false),
|
|
133
|
+
set: (value) => store.set(SETTING_IDS.graphicsShowFps, value),
|
|
134
|
+
},
|
|
91
135
|
...extrasFor("graphics"),
|
|
92
136
|
];
|
|
93
137
|
const gameplayRows = [
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
|
|
3
|
+
type Ref<T> = {
|
|
4
|
+
current: T;
|
|
5
|
+
};
|
|
6
|
+
/** Clears held input synchronously on play-gate transitions; ordinary context swaps retain it. @internal */
|
|
7
|
+
export declare function attachShellControlSuspension({ ctx, tracker, pointerAxisRef, analogRef, primaryClickRef, cameraDraggingRef, f2HeldRef, }: {
|
|
8
|
+
ctx: GameContext;
|
|
9
|
+
tracker: {
|
|
10
|
+
reset(): void;
|
|
11
|
+
};
|
|
12
|
+
pointerAxisRef: Ref<PointerAxisState | null>;
|
|
13
|
+
analogRef: Ref<Readonly<Record<string, number>> | null>;
|
|
14
|
+
primaryClickRef: Ref<boolean>;
|
|
15
|
+
cameraDraggingRef: Ref<boolean>;
|
|
16
|
+
f2HeldRef: Ref<boolean>;
|
|
17
|
+
}): () => void;
|
|
18
|
+
export {};
|