@jgengine/shell 0.19.0 → 0.22.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 +364 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- 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/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 +12 -5
- package/dist/defineGame.js +25 -10
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- 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/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +19 -15
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +19 -8
- 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 +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- 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/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 +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -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 +3 -0
- package/dist/render/modelLoad.js +9 -0
- 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 +21 -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.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- 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 +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- 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 +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- 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/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +61 -6
- package/dist/terrain/grassGeometry.d.ts +13 -0
- package/dist/terrain/grassGeometry.js +57 -1
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +11 -0
- package/dist/terrain/terrainPaintColor.js +47 -0
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- 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 +209 -61
- 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 +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +219 -70
- package/skills/jgengine-ui/capabilities.md +119 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
/** Marks only a rejected presentation import for page reload; browser module failures are cached. @internal */
|
|
4
|
+
export declare function markPresentationCodeLoadFailure(error: unknown): Error;
|
|
5
|
+
/** @internal Distinguishes module loading from recoverable assets without guessing from error text. */
|
|
6
|
+
export declare function presentationRecoveryAction(error: unknown): "reload" | "retry";
|
|
7
|
+
/** @internal Classifies a reported React error after its owning boundary commits. */
|
|
8
|
+
export declare function deferUnhandledPresentationError(error: unknown, report: () => void): void;
|
|
9
|
+
/** @internal Exact pending texture groups owned by one presentation, including uncommitted Suspense. */
|
|
10
|
+
export declare function createPresentationRetryRegistry(defaultClear?: (inputs: string[]) => void): {
|
|
11
|
+
register(inputs: readonly string[], clear?: ((inputs: string[]) => void) | undefined): () => void;
|
|
12
|
+
retry(): void;
|
|
13
|
+
};
|
|
14
|
+
type RetryRegistry = ReturnType<typeof createPresentationRetryRegistry>;
|
|
15
|
+
type PresentationRetryScope = {
|
|
16
|
+
registry: RetryRegistry;
|
|
17
|
+
fail: (error: unknown, stack?: string) => void;
|
|
18
|
+
recoveredDraw: () => void;
|
|
19
|
+
};
|
|
20
|
+
/** Renderer-only loader adapters share the DOM owner without importing their libraries here. @internal */
|
|
21
|
+
export declare function usePresentationRetryScope(): PresentationRetryScope | null;
|
|
22
|
+
/** @internal Forwards renderer-local loader errors to the owning DOM presentation. */
|
|
23
|
+
export declare function PresentationAssetBoundary({ children }: {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}): ReactNode;
|
|
26
|
+
/** @internal Retires only diagnostics belonging to this exact handled presentation error. */
|
|
27
|
+
export declare function createPresentationDiagnosticOwnership(): {
|
|
28
|
+
register(error: unknown, id: number, phase: string): void;
|
|
29
|
+
recovered(error: unknown): ReadonlySet<number>;
|
|
30
|
+
};
|
|
31
|
+
/** Restores an owned recovered surface only after Suspense has attached and revealed it. @internal */
|
|
32
|
+
export declare function createPlaySurfaceFocusOwnership<Owner>(): {
|
|
33
|
+
request(owner: Owner): void;
|
|
34
|
+
reveal(owner: Owner, surface: HTMLElement | null, active: Element | null, body: HTMLElement): boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Reports the owning presentation attempt and recovery lifecycle. @internal */
|
|
37
|
+
export type PresentationRecoveryEvent = {
|
|
38
|
+
phase: string;
|
|
39
|
+
attempt: number;
|
|
40
|
+
scope: number;
|
|
41
|
+
sameOwner: boolean;
|
|
42
|
+
pending: boolean;
|
|
43
|
+
failed: boolean;
|
|
44
|
+
};
|
|
45
|
+
type RecoveryProps = {
|
|
46
|
+
ctx: GameContext;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
onRuntimeError: (error: unknown, phase: string, componentStack?: string) => void;
|
|
49
|
+
onRetryCommitted?: () => void;
|
|
50
|
+
onRecoveredDraw?: (error: unknown) => void;
|
|
51
|
+
onRecoveryEvent?: (event: PresentationRecoveryEvent) => void;
|
|
52
|
+
};
|
|
53
|
+
type RecoveryState = {
|
|
54
|
+
owner: GameContext;
|
|
55
|
+
failed: boolean;
|
|
56
|
+
error: unknown;
|
|
57
|
+
registry: RetryRegistry;
|
|
58
|
+
};
|
|
59
|
+
/** @internal Retains the live context for asset Retry; failed browser modules require page reload. */
|
|
60
|
+
export declare class PresentationRecovery extends Component<RecoveryProps, RecoveryState> {
|
|
61
|
+
state: RecoveryState;
|
|
62
|
+
static getDerivedStateFromProps(props: RecoveryProps, state: RecoveryState): Partial<RecoveryState> | null;
|
|
63
|
+
static getDerivedStateFromError(error: unknown): Partial<RecoveryState>;
|
|
64
|
+
componentDidCatch(error: unknown, info: ErrorInfo): void;
|
|
65
|
+
componentDidUpdate(previousProps: RecoveryProps, previousState: RecoveryState): void;
|
|
66
|
+
componentWillUnmount(): void;
|
|
67
|
+
private attempt;
|
|
68
|
+
private scopeId;
|
|
69
|
+
private trace;
|
|
70
|
+
private scope;
|
|
71
|
+
private pendingRecovery;
|
|
72
|
+
private retry;
|
|
73
|
+
render(): ReactNode;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component, createContext, useContext, useId, useLayoutEffect } from "react";
|
|
3
|
+
import { suspendPlayControls } from "@jgengine/core/game/controlGate";
|
|
4
|
+
import { ErrorReportActions, errorReportContext } from "./diagnostics/RuntimeDiagnostics.js";
|
|
5
|
+
const handledPresentationErrors = new WeakSet();
|
|
6
|
+
const codeLoadFailures = new WeakSet();
|
|
7
|
+
/** Marks only a rejected presentation import for page reload; browser module failures are cached. @internal */
|
|
8
|
+
export function markPresentationCodeLoadFailure(error) {
|
|
9
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
10
|
+
codeLoadFailures.add(failure);
|
|
11
|
+
return failure;
|
|
12
|
+
}
|
|
13
|
+
/** @internal Distinguishes module loading from recoverable assets without guessing from error text. */
|
|
14
|
+
export function presentationRecoveryAction(error) {
|
|
15
|
+
return typeof error === "object" && error !== null && codeLoadFailures.has(error) ? "reload" : "retry";
|
|
16
|
+
}
|
|
17
|
+
function markPresentationErrorHandled(error) {
|
|
18
|
+
if ((typeof error === "object" && error !== null) || typeof error === "function")
|
|
19
|
+
handledPresentationErrors.add(error);
|
|
20
|
+
}
|
|
21
|
+
function forgetPresentationError(error) {
|
|
22
|
+
if ((typeof error === "object" && error !== null) || typeof error === "function")
|
|
23
|
+
handledPresentationErrors.delete(error);
|
|
24
|
+
}
|
|
25
|
+
/** @internal Classifies a reported React error after its owning boundary commits. */
|
|
26
|
+
export function deferUnhandledPresentationError(error, report) {
|
|
27
|
+
queueMicrotask(() => {
|
|
28
|
+
if (((typeof error === "object" && error !== null) || typeof error === "function") && handledPresentationErrors.has(error))
|
|
29
|
+
return;
|
|
30
|
+
report();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** @internal Exact pending texture groups owned by one presentation, including uncommitted Suspense. */
|
|
34
|
+
export function createPresentationRetryRegistry(defaultClear) {
|
|
35
|
+
const loaders = new Map();
|
|
36
|
+
return {
|
|
37
|
+
register(inputs, clear = defaultClear) {
|
|
38
|
+
if (clear === undefined)
|
|
39
|
+
throw new Error("A recoverable loader must provide its cache clear callback");
|
|
40
|
+
let groups = loaders.get(clear);
|
|
41
|
+
if (groups === undefined) {
|
|
42
|
+
groups = new Map();
|
|
43
|
+
loaders.set(clear, groups);
|
|
44
|
+
}
|
|
45
|
+
const key = JSON.stringify(inputs);
|
|
46
|
+
let group = groups.get(key);
|
|
47
|
+
if (group === undefined) {
|
|
48
|
+
group = { inputs: [...inputs], loaded: false };
|
|
49
|
+
groups.set(key, group);
|
|
50
|
+
}
|
|
51
|
+
const registered = group;
|
|
52
|
+
return () => { registered.loaded = true; };
|
|
53
|
+
},
|
|
54
|
+
retry() {
|
|
55
|
+
for (const [clear, groups] of loaders) {
|
|
56
|
+
for (const [key, group] of groups) {
|
|
57
|
+
if (group.loaded)
|
|
58
|
+
continue;
|
|
59
|
+
clear(group.inputs);
|
|
60
|
+
groups.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const PresentationRetryContext = createContext(null);
|
|
67
|
+
/** Renderer-only loader adapters share the DOM owner without importing their libraries here. @internal */
|
|
68
|
+
export function usePresentationRetryScope() {
|
|
69
|
+
return useContext(PresentationRetryContext);
|
|
70
|
+
}
|
|
71
|
+
class AssetErrorBoundary extends Component {
|
|
72
|
+
state = { failed: false };
|
|
73
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
74
|
+
componentDidCatch(error, info) { this.props.onError(error, info.componentStack ?? undefined); }
|
|
75
|
+
render() { return this.state.failed ? null : this.props.children; }
|
|
76
|
+
}
|
|
77
|
+
/** @internal Forwards renderer-local loader errors to the owning DOM presentation. */
|
|
78
|
+
export function PresentationAssetBoundary({ children }) {
|
|
79
|
+
const scope = useContext(PresentationRetryContext);
|
|
80
|
+
return scope === null ? children : _jsx(AssetErrorBoundary, { onError: scope.fail, children: children });
|
|
81
|
+
}
|
|
82
|
+
/** @internal Retires only diagnostics belonging to this exact handled presentation error. */
|
|
83
|
+
export function createPresentationDiagnosticOwnership() {
|
|
84
|
+
const ids = new WeakMap();
|
|
85
|
+
return {
|
|
86
|
+
register(error, id, phase) {
|
|
87
|
+
if (phase !== "presentation" || typeof error !== "object" || error === null)
|
|
88
|
+
return;
|
|
89
|
+
const owned = ids.get(error) ?? new Set();
|
|
90
|
+
owned.add(id);
|
|
91
|
+
ids.set(error, owned);
|
|
92
|
+
},
|
|
93
|
+
recovered(error) {
|
|
94
|
+
if (typeof error !== "object" || error === null)
|
|
95
|
+
return new Set();
|
|
96
|
+
const owned = ids.get(error) ?? new Set();
|
|
97
|
+
ids.delete(error);
|
|
98
|
+
return owned;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Restores an owned recovered surface only after Suspense has attached and revealed it. @internal */
|
|
103
|
+
export function createPlaySurfaceFocusOwnership() {
|
|
104
|
+
let pending = null;
|
|
105
|
+
return {
|
|
106
|
+
request(owner) { pending = owner; },
|
|
107
|
+
reveal(owner, surface, active, body) {
|
|
108
|
+
if (pending === null)
|
|
109
|
+
return false;
|
|
110
|
+
if (pending !== owner) {
|
|
111
|
+
pending = null;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (surface === null || !surface.isConnected || surface.getClientRects().length === 0)
|
|
115
|
+
return false;
|
|
116
|
+
const bounds = surface.getBoundingClientRect();
|
|
117
|
+
if (bounds.width === 0 || bounds.height === 0)
|
|
118
|
+
return false;
|
|
119
|
+
pending = null;
|
|
120
|
+
if (active !== null && active !== body && !surface.contains(active))
|
|
121
|
+
return false;
|
|
122
|
+
surface.focus({ preventScroll: true });
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function RecoveryFailure({ ctx, error, retry }) {
|
|
128
|
+
useLayoutEffect(() => suspendPlayControls(ctx), [ctx]);
|
|
129
|
+
const descriptionId = useId();
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
const reload = presentationRecoveryAction(error) === "reload";
|
|
132
|
+
const report = ["JGengine presentation error", message, error instanceof Error ? error.stack : null, errorReportContext()]
|
|
133
|
+
.filter((line) => line !== null && line !== undefined).join("\n");
|
|
134
|
+
return (_jsx("div", { style: { display: "grid", width: "100%", height: "100%", placeItems: "center", padding: 24, background: "#08151d", color: "#e9f6ee", fontFamily: "system-ui" }, "data-presentation-recovery": "failed", children: _jsxs("section", { role: "alert", "aria-describedby": descriptionId, style: { width: "100%", maxWidth: 480, padding: 28, border: "1px solid #35616b", borderRadius: 16, background: "#102735", boxShadow: "0 20px 60px #0006" }, children: [_jsx("h2", { style: { margin: 0, fontSize: 22, fontWeight: 650 }, children: "The game view couldn\u2019t load" }), _jsx("p", { id: descriptionId, style: { margin: "12px 0 20px", color: "#c3d9dd", lineHeight: 1.5 }, children: reload ? "Reload the game to reconnect and load its view. If the problem continues, the technical details can help the game’s author investigate." : "Retry to load the view again. If the problem continues, the technical details can help the game’s author investigate." }), _jsx("button", { type: "button", autoFocus: true, style: { border: "1px solid #8ce0c2", borderRadius: 8, background: "#8ce0c2", color: "#082b22", padding: "10px 18px", font: "inherit", fontWeight: 650, cursor: "pointer" }, onClick: retry, children: reload ? "Reload game" : "Retry display" }), _jsxs("details", { style: { marginTop: 24, borderTop: "1px solid #35616b", paddingTop: 16 }, children: [_jsx("summary", { style: { cursor: "pointer", color: "#b7d3da" }, children: "Technical details" }), _jsx("pre", { style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere", maxHeight: 160, overflow: "auto", fontSize: 12, color: "#c3d9dd" }, children: message }), _jsx(ErrorReportActions, { report: report, issueTitle: `Presentation: ${message}` })] })] }) }));
|
|
135
|
+
}
|
|
136
|
+
/** @internal Retains the live context for asset Retry; failed browser modules require page reload. */
|
|
137
|
+
export class PresentationRecovery extends Component {
|
|
138
|
+
state = { owner: this.props.ctx, failed: false, error: null, registry: createPresentationRetryRegistry() };
|
|
139
|
+
static getDerivedStateFromProps(props, state) {
|
|
140
|
+
if (props.ctx === state.owner)
|
|
141
|
+
return null;
|
|
142
|
+
forgetPresentationError(state.error);
|
|
143
|
+
return { owner: props.ctx, failed: false, error: null, registry: createPresentationRetryRegistry() };
|
|
144
|
+
}
|
|
145
|
+
static getDerivedStateFromError(error) { return { failed: true, error }; }
|
|
146
|
+
componentDidCatch(error, info) {
|
|
147
|
+
this.pendingRecovery = null;
|
|
148
|
+
this.trace("caught");
|
|
149
|
+
markPresentationErrorHandled(error);
|
|
150
|
+
this.props.onRuntimeError(error, "presentation", info.componentStack ?? undefined);
|
|
151
|
+
}
|
|
152
|
+
componentDidUpdate(previousProps, previousState) {
|
|
153
|
+
if (previousState.failed && !this.state.failed && previousState.owner === this.state.owner && previousProps.ctx === this.props.ctx) {
|
|
154
|
+
this.trace("retry-committed");
|
|
155
|
+
this.props.onRetryCommitted?.();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
componentWillUnmount() { forgetPresentationError(this.state.error); }
|
|
159
|
+
attempt = 0;
|
|
160
|
+
scopeId = 0;
|
|
161
|
+
trace(phase) {
|
|
162
|
+
this.props.onRecoveryEvent?.({ phase, attempt: this.attempt, scope: this.scopeId, sameOwner: this.props.ctx === this.state.owner, pending: this.pendingRecovery !== null, failed: this.state.failed });
|
|
163
|
+
}
|
|
164
|
+
scope = null;
|
|
165
|
+
pendingRecovery = null;
|
|
166
|
+
retry = () => {
|
|
167
|
+
if (presentationRecoveryAction(this.state.error) === "reload") {
|
|
168
|
+
globalThis.location.reload();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.attempt += 1;
|
|
172
|
+
this.pendingRecovery = { owner: this.state.owner, error: this.state.error };
|
|
173
|
+
this.trace("retry-requested");
|
|
174
|
+
this.state.registry.retry();
|
|
175
|
+
forgetPresentationError(this.state.error);
|
|
176
|
+
this.setState({ failed: false, error: null });
|
|
177
|
+
};
|
|
178
|
+
render() {
|
|
179
|
+
if (this.state.failed)
|
|
180
|
+
return _jsx(RecoveryFailure, { ctx: this.props.ctx, error: this.state.error, retry: this.retry });
|
|
181
|
+
const owner = this.state.owner;
|
|
182
|
+
if (this.scope === null || this.scope.registry !== this.state.registry) {
|
|
183
|
+
this.scopeId += 1;
|
|
184
|
+
this.scope = { registry: this.state.registry, fail: (error, stack) => {
|
|
185
|
+
if (this.props.ctx !== owner)
|
|
186
|
+
return;
|
|
187
|
+
this.pendingRecovery = null;
|
|
188
|
+
this.trace("asset-failed");
|
|
189
|
+
markPresentationErrorHandled(error);
|
|
190
|
+
this.props.onRuntimeError(error, "presentation", stack);
|
|
191
|
+
this.setState({ failed: true, error });
|
|
192
|
+
}, recoveredDraw: () => {
|
|
193
|
+
this.trace("draw-observed");
|
|
194
|
+
const pending = this.pendingRecovery;
|
|
195
|
+
if (pending === null || pending.owner !== owner || this.props.ctx !== owner || this.state.failed)
|
|
196
|
+
return;
|
|
197
|
+
this.pendingRecovery = null;
|
|
198
|
+
this.trace("draw-accepted");
|
|
199
|
+
this.props.onRecoveredDraw?.(pending.error);
|
|
200
|
+
} };
|
|
201
|
+
}
|
|
202
|
+
return _jsx(PresentationRetryContext, { value: this.scope, children: this.props.children });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Registers exact loader inputs and their cache clear before a load can suspend. @internal */
|
|
2
|
+
export declare function useRecoverableTexture<T extends string[] | string | Record<string, string>>(input: T): import("@react-three/drei").MappedTextureType<T>;
|
|
3
|
+
/** Observes the owning renderer after the restored material subtree commits and draws. @internal */
|
|
4
|
+
export declare function PresentationRecoveredDraw(): null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { addAfterEffect, useThree } from "@react-three/fiber";
|
|
2
|
+
import { useTexture } from "@react-three/drei";
|
|
3
|
+
import { useLayoutEffect } from "react";
|
|
4
|
+
import { usePresentationRetryScope } from "./presentationRecovery.js";
|
|
5
|
+
/** Registers exact loader inputs and their cache clear before a load can suspend. @internal */
|
|
6
|
+
export function useRecoverableTexture(input) {
|
|
7
|
+
const scope = usePresentationRetryScope();
|
|
8
|
+
const inputs = typeof input === "string" ? [input] : Array.isArray(input) ? input : Object.values(input);
|
|
9
|
+
const loaded = scope?.registry.register(inputs, useTexture.clear);
|
|
10
|
+
const textures = useTexture(input);
|
|
11
|
+
loaded?.();
|
|
12
|
+
return textures;
|
|
13
|
+
}
|
|
14
|
+
/** Observes the owning renderer after the restored material subtree commits and draws. @internal */
|
|
15
|
+
export function PresentationRecoveredDraw() {
|
|
16
|
+
const gl = useThree((state) => state.gl);
|
|
17
|
+
const scope = usePresentationRetryScope();
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
if (scope === null)
|
|
20
|
+
return;
|
|
21
|
+
const baseline = gl.info.render.frame;
|
|
22
|
+
let stop = () => { };
|
|
23
|
+
stop = addAfterEffect(() => {
|
|
24
|
+
if (gl.info.render.frame <= baseline || gl.info.render.calls <= 0 || !gl.domElement.isConnected || gl.getContext().isContextLost())
|
|
25
|
+
return;
|
|
26
|
+
stop();
|
|
27
|
+
scope.recoveredDraw();
|
|
28
|
+
});
|
|
29
|
+
return () => stop();
|
|
30
|
+
}, [gl, scope]);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
1
2
|
import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
|
|
2
3
|
import type { PresentationEffectsConfig } from "@jgengine/core/game/playableGame";
|
|
3
4
|
/** Resolved world-bar / nameplate config, or `null` when the feature is off. @internal */
|
|
@@ -5,9 +6,12 @@ export interface ResolvedWorldOverlayBars {
|
|
|
5
6
|
statId: string;
|
|
6
7
|
roles?: readonly CatalogEntityRole[];
|
|
7
8
|
maxDistance?: number;
|
|
9
|
+
maxSamples?: number;
|
|
10
|
+
tickMs?: number;
|
|
8
11
|
/** Nameplates only: draw the built-in HP bar. `false` yields a name-only plate. Defaults to `true`. */
|
|
9
12
|
showHealth?: boolean;
|
|
10
|
-
occlude
|
|
13
|
+
occlude: boolean;
|
|
14
|
+
resolveName?: (entity: SceneEntity) => string | null;
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Normalize `worldHealthBars` / `nameplates` (`boolean | object | undefined`) into a
|
|
@@ -20,8 +24,11 @@ export declare function resolveWorldOverlayBars(config: boolean | {
|
|
|
20
24
|
statId?: string;
|
|
21
25
|
roles?: readonly CatalogEntityRole[];
|
|
22
26
|
maxDistance?: number;
|
|
27
|
+
maxSamples?: number;
|
|
28
|
+
tickMs?: number;
|
|
23
29
|
showHealth?: boolean;
|
|
24
30
|
occlude?: boolean;
|
|
31
|
+
resolveName?: (entity: SceneEntity) => string | null;
|
|
25
32
|
} | undefined): ResolvedWorldOverlayBars | null;
|
|
26
33
|
/** Resolved combat presentation channels (each boolean is whether that stack piece mounts). @internal */
|
|
27
34
|
export interface ResolvedPresentationEffects {
|
|
@@ -9,13 +9,16 @@ export function resolveWorldOverlayBars(config) {
|
|
|
9
9
|
if (config === undefined || config === false)
|
|
10
10
|
return null;
|
|
11
11
|
if (config === true)
|
|
12
|
-
return { statId: "health" };
|
|
12
|
+
return { statId: "health", occlude: true };
|
|
13
13
|
return {
|
|
14
14
|
statId: config.statId ?? "health",
|
|
15
15
|
...(config.roles === undefined ? {} : { roles: config.roles }),
|
|
16
|
+
...(config.maxSamples === undefined ? {} : { maxSamples: config.maxSamples }),
|
|
17
|
+
...(config.tickMs === undefined ? {} : { tickMs: config.tickMs }),
|
|
16
18
|
...(config.maxDistance === undefined ? {} : { maxDistance: config.maxDistance }),
|
|
17
19
|
...(config.showHealth === undefined ? {} : { showHealth: config.showHealth }),
|
|
18
|
-
|
|
20
|
+
occlude: config.occlude ?? true,
|
|
21
|
+
...(config.resolveName === undefined ? {} : { resolveName: config.resolveName }),
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
// Every channel defaults on EXCEPT tracers. `fireProjectile` is a generic seam (bullets,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
+
import { CSM } from "three/examples/jsm/csm/CSM.js";
|
|
2
3
|
import type { DirectionalLightingConfig } from "@jgengine/core/game/playableGame";
|
|
3
4
|
/**
|
|
4
5
|
* Cascaded shadow maps for one directional light — outdoor scenes keep shadows past a
|
|
@@ -15,10 +16,14 @@ export declare function CascadedShadows({ entry }: {
|
|
|
15
16
|
/** Structural slice of CSM so the patcher is testable without a renderer. */
|
|
16
17
|
export interface CsmMaterialSetup {
|
|
17
18
|
setupMaterial(material: THREE.Material): void;
|
|
19
|
+
shaders?: Map<unknown, unknown>;
|
|
18
20
|
}
|
|
21
|
+
/** Release the lights and GPU targets that three.js CSM.dispose does not own. @internal */
|
|
22
|
+
export declare function releaseCascadedShadows(csm: Pick<CSM, "remove" | "dispose" | "lights">): void;
|
|
23
|
+
/** Patch streamed meshes on attachment and material replacement without scanning the world each frame. @internal */
|
|
24
|
+
export declare function bindCsmMaterials(scene: THREE.Scene, csm: CsmMaterialSetup): () => void;
|
|
19
25
|
/**
|
|
20
26
|
* Run CSM's `setupMaterial` over every unpatched standard material in the scene,
|
|
21
|
-
* chaining rather than clobbering a material's own `onBeforeCompile`.
|
|
22
|
-
* frame (WeakSet-guarded) so streamed meshes never render lit by all cascades at once.
|
|
27
|
+
* chaining rather than clobbering a material's own `onBeforeCompile`.
|
|
23
28
|
*/
|
|
24
29
|
export declare function patchSceneMaterials(scene: THREE.Scene, csm: CsmMaterialSetup, patched: WeakSet<THREE.Material>): void;
|
|
@@ -18,7 +18,6 @@ export function CascadedShadows({ entry }) {
|
|
|
18
18
|
const camera = useThree((s) => s.camera);
|
|
19
19
|
const scene = useThree((s) => s.scene);
|
|
20
20
|
const csmRef = useRef(null);
|
|
21
|
-
const patchedRef = useRef(new WeakSet());
|
|
22
21
|
useEffect(() => {
|
|
23
22
|
const cascades = Math.max(2, Math.min(4, Math.floor(entry.cascades ?? 3)));
|
|
24
23
|
const lightDirection = new THREE.Vector3(-entry.position[0], -entry.position[1], -entry.position[2]);
|
|
@@ -43,11 +42,11 @@ export function CascadedShadows({ entry }) {
|
|
|
43
42
|
light.color = new THREE.Color(entry.color ?? "#ffffff");
|
|
44
43
|
light.castShadow = true;
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
45
|
+
const unbind = bindCsmMaterials(scene, csm);
|
|
48
46
|
csmRef.current = csm;
|
|
49
47
|
return () => {
|
|
50
|
-
|
|
48
|
+
unbind();
|
|
49
|
+
releaseCascadedShadows(csm);
|
|
51
50
|
csmRef.current = null;
|
|
52
51
|
};
|
|
53
52
|
}, [
|
|
@@ -69,17 +68,106 @@ export function CascadedShadows({ entry }) {
|
|
|
69
68
|
if (csm === null)
|
|
70
69
|
return;
|
|
71
70
|
csm.camera = camera;
|
|
72
|
-
// Every frame: a mesh that renders before setupMaterial receives all cascade
|
|
73
|
-
// lights at once (a cascades× sun) — the WeakSet keeps the traverse cheap.
|
|
74
|
-
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
75
71
|
csm.update();
|
|
76
72
|
});
|
|
77
73
|
return null;
|
|
78
74
|
}
|
|
75
|
+
/** Release the lights and GPU targets that three.js CSM.dispose does not own. @internal */
|
|
76
|
+
export function releaseCascadedShadows(csm) {
|
|
77
|
+
csm.remove();
|
|
78
|
+
for (const light of csm.lights)
|
|
79
|
+
light.shadow.dispose();
|
|
80
|
+
csm.dispose();
|
|
81
|
+
}
|
|
82
|
+
function setupMaterial(material, csm) {
|
|
83
|
+
const prior = material.onBeforeCompile;
|
|
84
|
+
csm.setupMaterial(material);
|
|
85
|
+
const csmHook = material.onBeforeCompile;
|
|
86
|
+
if (prior !== THREE.Material.prototype.onBeforeCompile && prior !== csmHook) {
|
|
87
|
+
material.onBeforeCompile = function (shader, renderer) {
|
|
88
|
+
prior.call(this, shader, renderer);
|
|
89
|
+
csmHook.call(this, shader, renderer);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
material.needsUpdate = true;
|
|
93
|
+
}
|
|
94
|
+
/** Patch streamed meshes on attachment and material replacement without scanning the world each frame. @internal */
|
|
95
|
+
export function bindCsmMaterials(scene, csm) {
|
|
96
|
+
const nodes = new Map();
|
|
97
|
+
const materials = new Map();
|
|
98
|
+
const patch = (material) => {
|
|
99
|
+
if (!isStandardMaterial(material) || materials.has(material))
|
|
100
|
+
return;
|
|
101
|
+
const prior = material.onBeforeCompile;
|
|
102
|
+
const priorDefines = { ...material.defines };
|
|
103
|
+
setupMaterial(material, csm);
|
|
104
|
+
const hook = material.onBeforeCompile;
|
|
105
|
+
const release = () => {
|
|
106
|
+
material.removeEventListener("dispose", release);
|
|
107
|
+
csm.shaders?.delete(material);
|
|
108
|
+
if (material.onBeforeCompile === hook)
|
|
109
|
+
material.onBeforeCompile = prior;
|
|
110
|
+
for (const key of ["USE_CSM", "CSM_CASCADES", "CSM_FADE"]) {
|
|
111
|
+
if (key in priorDefines)
|
|
112
|
+
(material.defines ??= {})[key] = priorDefines[key];
|
|
113
|
+
else if (material.defines !== undefined)
|
|
114
|
+
delete material.defines[key];
|
|
115
|
+
}
|
|
116
|
+
material.needsUpdate = true;
|
|
117
|
+
materials.delete(material);
|
|
118
|
+
};
|
|
119
|
+
materials.set(material, release);
|
|
120
|
+
material.addEventListener("dispose", release);
|
|
121
|
+
};
|
|
122
|
+
const patchMesh = (node) => {
|
|
123
|
+
const mesh = node;
|
|
124
|
+
if (!mesh.isMesh)
|
|
125
|
+
return;
|
|
126
|
+
if (Array.isArray(mesh.material))
|
|
127
|
+
mesh.material.forEach(patch);
|
|
128
|
+
else
|
|
129
|
+
patch(mesh.material);
|
|
130
|
+
};
|
|
131
|
+
const detach = (node) => {
|
|
132
|
+
nodes.get(node)?.();
|
|
133
|
+
nodes.delete(node);
|
|
134
|
+
for (const child of node.children)
|
|
135
|
+
detach(child);
|
|
136
|
+
};
|
|
137
|
+
const attach = (node) => {
|
|
138
|
+
if (nodes.has(node))
|
|
139
|
+
return;
|
|
140
|
+
patchMesh(node);
|
|
141
|
+
const added = ({ child }) => attach(child);
|
|
142
|
+
const removed = ({ child }) => detach(child);
|
|
143
|
+
const prior = node.onBeforeRender;
|
|
144
|
+
const before = function (...args) {
|
|
145
|
+
prior.apply(this, args);
|
|
146
|
+
patchMesh(this);
|
|
147
|
+
};
|
|
148
|
+
node.addEventListener("childadded", added);
|
|
149
|
+
node.addEventListener("childremoved", removed);
|
|
150
|
+
if (node.isMesh)
|
|
151
|
+
node.onBeforeRender = before;
|
|
152
|
+
nodes.set(node, () => {
|
|
153
|
+
node.removeEventListener("childadded", added);
|
|
154
|
+
node.removeEventListener("childremoved", removed);
|
|
155
|
+
if (node.onBeforeRender === before)
|
|
156
|
+
node.onBeforeRender = prior;
|
|
157
|
+
});
|
|
158
|
+
for (const child of node.children)
|
|
159
|
+
attach(child);
|
|
160
|
+
};
|
|
161
|
+
attach(scene);
|
|
162
|
+
return () => {
|
|
163
|
+
detach(scene);
|
|
164
|
+
for (const release of materials.values())
|
|
165
|
+
release();
|
|
166
|
+
};
|
|
167
|
+
}
|
|
79
168
|
/**
|
|
80
169
|
* Run CSM's `setupMaterial` over every unpatched standard material in the scene,
|
|
81
|
-
* chaining rather than clobbering a material's own `onBeforeCompile`.
|
|
82
|
-
* frame (WeakSet-guarded) so streamed meshes never render lit by all cascades at once.
|
|
170
|
+
* chaining rather than clobbering a material's own `onBeforeCompile`.
|
|
83
171
|
*/
|
|
84
172
|
export function patchSceneMaterials(scene, csm, patched) {
|
|
85
173
|
scene.traverse((obj) => {
|
|
@@ -90,17 +178,7 @@ export function patchSceneMaterials(scene, csm, patched) {
|
|
|
90
178
|
for (const mat of materials) {
|
|
91
179
|
if (!isStandardMaterial(mat) || patched.has(mat))
|
|
92
180
|
continue;
|
|
93
|
-
|
|
94
|
-
csm.setupMaterial(mat);
|
|
95
|
-
const csmHook = mat.onBeforeCompile;
|
|
96
|
-
if (prior !== THREE.Material.prototype.onBeforeCompile && prior !== csmHook) {
|
|
97
|
-
// setupMaterial assigns onBeforeCompile, which would drop a material's own
|
|
98
|
-
// shader surgery (rim light, detail maps) — run both, the material's first.
|
|
99
|
-
mat.onBeforeCompile = function (shader, renderer) {
|
|
100
|
-
prior.call(this, shader, renderer);
|
|
101
|
-
csmHook.call(this, shader, renderer);
|
|
102
|
-
};
|
|
103
|
-
}
|
|
181
|
+
setupMaterial(mat, csm);
|
|
104
182
|
patched.add(mat);
|
|
105
183
|
}
|
|
106
184
|
});
|