@jgengine/shell 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/CombatPresentation.js +2 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +16 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +15 -1
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -0
- package/dist/devtools/collisionDebugMath.js +14 -5
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +38 -5
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/postfx/postfxOverlay.d.ts +24 -0
- package/dist/postfx/postfxOverlay.js +35 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EntityPreview.d.ts +53 -0
- package/dist/render/EntityPreview.js +23 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +52 -10
- package/dist/render/measureBounds.js +4 -0
- package/dist/render/measureCollisionMesh.d.ts +26 -0
- package/dist/render/measureCollisionMesh.js +108 -0
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +8 -3
- package/skills/jgengine-ui/api.md +732 -23
- package/skills/jgengine-ui/capabilities.md +371 -2
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
package/dist/render/modelLoad.js
CHANGED
|
@@ -2,7 +2,23 @@ import * as THREE from "three";
|
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
4
4
|
import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
5
|
+
import { reportTextureLoadError } from "@jgengine/core/devtools/textureErrors";
|
|
5
6
|
import { resolveAssetBaseUrl } from "./assetBase.js";
|
|
7
|
+
/** Model containers already covered by the whole-model fallback probe; not "texture" errors. @internal */
|
|
8
|
+
const MODEL_URL_RE = /\.(glb|gltf)(\?|#|$)/i;
|
|
9
|
+
/**
|
|
10
|
+
* The shared GLB {@link THREE.LoadingManager}'s `onError` sink. It fires for EVERY item the manager
|
|
11
|
+
* loads — the GLB container AND each texture/image a `GLTFLoader` fetches for it. The container's own
|
|
12
|
+
* failure is already surfaced by the model-fallback probe (it resolves to a placeholder), so this records
|
|
13
|
+
* only the sub-resource (texture) failures the fallback probe cannot see — a model that resolves but whose
|
|
14
|
+
* textures 404. Split out and exported so the wiring is unit-testable without a GL context.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export function recordManagerLoadError(url) {
|
|
18
|
+
if (MODEL_URL_RE.test(url))
|
|
19
|
+
return;
|
|
20
|
+
reportTextureLoadError(url);
|
|
21
|
+
}
|
|
6
22
|
/**
|
|
7
23
|
* Dedicated LoadingManager for every GLB load instead of THREE.DefaultLoadingManager.
|
|
8
24
|
* The shared default manager is process-wide; under repeated dev-server navigations its
|
|
@@ -12,6 +28,9 @@ import { resolveAssetBaseUrl } from "./assetBase.js";
|
|
|
12
28
|
*/
|
|
13
29
|
const modelLoadingManager = new THREE.LoadingManager();
|
|
14
30
|
modelLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
31
|
+
// Capture GLTF texture/image load failures that never reach the top-level model onError (the model still
|
|
32
|
+
// resolves). Feeds the `textureErrors` probe so a texture-404'd scene is visible in debug_snapshot.
|
|
33
|
+
modelLoadingManager.onError = recordManagerLoadError;
|
|
15
34
|
/** How many leading bytes to read when probing a failed model URL for its signature. @internal */
|
|
16
35
|
const PROBE_BYTE_LIMIT = 64;
|
|
17
36
|
/**
|
|
@@ -42,11 +61,71 @@ export async function probeModelUrl(url, logicalId, fetchImpl = fetch) {
|
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
63
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
64
|
+
* Build a stand-in {@link GLTF} whose scene is a single magenta placeholder box,
|
|
65
|
+
* returned in place of a model that could not be loaded. Consumers clone and
|
|
66
|
+
* mount `gltf.scene` exactly as they would a real model, so one broken asset
|
|
67
|
+
* degrades to a visible primitive instead of taking down the scene. The optional
|
|
68
|
+
* `diagnosis` is stashed on `userData.jgengineDiagnosis` for debugging.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export function createFallbackModel(diagnosis) {
|
|
72
|
+
const scene = new THREE.Group();
|
|
73
|
+
scene.name = "jgengine-missing-model";
|
|
74
|
+
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ color: 0xd63384, roughness: 0.7, metalness: 0 }));
|
|
75
|
+
mesh.name = "jgengine-missing-model-box";
|
|
76
|
+
mesh.castShadow = true;
|
|
77
|
+
mesh.receiveShadow = true;
|
|
78
|
+
scene.add(mesh);
|
|
79
|
+
const userData = { jgengineFallback: true };
|
|
80
|
+
if (diagnosis !== undefined)
|
|
81
|
+
userData.jgengineDiagnosis = diagnosis;
|
|
82
|
+
return {
|
|
83
|
+
scene,
|
|
84
|
+
scenes: [scene],
|
|
85
|
+
animations: [],
|
|
86
|
+
cameras: [],
|
|
87
|
+
asset: { generator: "jgengine-fallback-model" },
|
|
88
|
+
// The fallback never runs a GLTF parser; consumers only read `scene`/`animations`.
|
|
89
|
+
parser: undefined,
|
|
90
|
+
userData,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function warnMissingModel(message) {
|
|
94
|
+
if (typeof console !== "undefined") {
|
|
95
|
+
console.warn(`[jgengine] ${message} Rendering a placeholder primitive in its place.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The failure seam for {@link DiagnosticGLTFLoader}: re-probe the URL a load just
|
|
100
|
+
* failed on and decide what the loader should hand back. A diagnosed broken asset
|
|
101
|
+
* (missing / HTML dev-server fallback / corrupt / unsupported) resolves via
|
|
102
|
+
* `onLoad` to a {@link createFallbackModel} primitive so the failure is contained
|
|
103
|
+
* at the load seam for **every** consumer — no reliance on a React error boundary
|
|
104
|
+
* catching a rejected Suspense promise, which does not fire reliably for async
|
|
105
|
+
* loader rejections inside the react-three-fiber reconciler. Only a genuine parse
|
|
106
|
+
* error over bytes that still look like a valid model (`diagnosis.ok`) is surfaced
|
|
107
|
+
* through `onError`. Split out from the loader so it is unit-testable without a GL
|
|
108
|
+
* context; `overrides` inject the probe and warning sink in tests.
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
export async function handleModelLoadFailure(resolvedUrl, originalEvent, onLoad, onError, overrides = {}) {
|
|
112
|
+
const probe = overrides.probe ?? probeModelUrl;
|
|
113
|
+
const diagnosis = await probe(resolvedUrl);
|
|
114
|
+
if (diagnosis.ok) {
|
|
115
|
+
// The bytes look like a real model on re-fetch — surface the original parse error unchanged.
|
|
116
|
+
onError(originalEvent);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
(overrides.warn ?? warnMissingModel)(diagnosis.message);
|
|
120
|
+
onLoad(createFallbackModel(diagnosis));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
|
|
124
|
+
* gracefully: when a load errors, it probes the URL and — for a diagnosed broken
|
|
125
|
+
* asset (missing / HTML fallback / corrupt / unsupported) — resolves to a
|
|
126
|
+
* {@link createFallbackModel} placeholder primitive instead of rejecting, so the
|
|
127
|
+
* one broken model never bubbles a throw past its mount. Drop-in for the shared
|
|
128
|
+
* loader, so `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
|
|
50
129
|
* @internal
|
|
51
130
|
*/
|
|
52
131
|
export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
@@ -55,19 +134,7 @@ export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
|
55
134
|
if (onError === undefined)
|
|
56
135
|
return;
|
|
57
136
|
// Probe the URL the loader actually fetched (base-resolved), not the raw input.
|
|
58
|
-
void
|
|
59
|
-
.then((diagnosis) => {
|
|
60
|
-
if (diagnosis.ok) {
|
|
61
|
-
// The bytes look fine on re-fetch — surface the original parse error unchanged.
|
|
62
|
-
onError(event);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const enriched = new Error(`[jgengine] ${diagnosis.message}`);
|
|
66
|
-
enriched.diagnosis = diagnosis;
|
|
67
|
-
enriched.cause = event;
|
|
68
|
-
onError(enriched);
|
|
69
|
-
})
|
|
70
|
-
.catch(() => onError(event));
|
|
137
|
+
void handleModelLoadFailure(this.manager.resolveURL(url), event, onLoad, onError).catch(() => onError(event));
|
|
71
138
|
});
|
|
72
139
|
}
|
|
73
140
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The bit of a WebGL renderer scene capture needs — its backing `<canvas>`. */
|
|
2
|
+
export interface CaptureRenderer {
|
|
3
|
+
domElement: {
|
|
4
|
+
toDataURL(type?: string): string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Read the current frame to a PNG data URL. Requires the R3F `<Canvas>` to have
|
|
9
|
+
* been created with `gl={{ preserveDrawingBuffer: true }}` (the shell's game
|
|
10
|
+
* canvas already is); returns null if the backing canvas can't be read.
|
|
11
|
+
*
|
|
12
|
+
* @capability capture-canvas read the live R3F frame to a PNG data URL (needs preserveDrawingBuffer)
|
|
13
|
+
*/
|
|
14
|
+
export declare function captureCanvas(gl: CaptureRenderer): string | null;
|
|
15
|
+
/** Trigger a browser download of an image data URL (the photo-mode "save" action). */
|
|
16
|
+
export declare function downloadImage(dataUrl: string, filename?: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* In-`<Canvas>` binder that hands the scene-capture function out to HUD code
|
|
19
|
+
* living outside the reconciler (a photo-mode Capture button). Mount it inside
|
|
20
|
+
* the game's `WorldOverlay`; call `bind` receives a `() => string | null` that
|
|
21
|
+
* grabs the current frame. Renders nothing.
|
|
22
|
+
*
|
|
23
|
+
* @capability scene-capture-binding expose the in-Canvas scene-capture function to outside-Canvas HUD (photo mode)
|
|
24
|
+
*/
|
|
25
|
+
export declare function SceneCaptureBinding({ bind }: {
|
|
26
|
+
bind: (capture: () => string | null) => void;
|
|
27
|
+
}): null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Read the current frame to a PNG data URL. Requires the R3F `<Canvas>` to have
|
|
5
|
+
* been created with `gl={{ preserveDrawingBuffer: true }}` (the shell's game
|
|
6
|
+
* canvas already is); returns null if the backing canvas can't be read.
|
|
7
|
+
*
|
|
8
|
+
* @capability capture-canvas read the live R3F frame to a PNG data URL (needs preserveDrawingBuffer)
|
|
9
|
+
*/
|
|
10
|
+
export function captureCanvas(gl) {
|
|
11
|
+
try {
|
|
12
|
+
const url = gl.domElement.toDataURL("image/png");
|
|
13
|
+
return url.startsWith("data:image/png") ? url : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Trigger a browser download of an image data URL (the photo-mode "save" action). */
|
|
20
|
+
export function downloadImage(dataUrl, filename = "screenshot.png") {
|
|
21
|
+
if (typeof document === "undefined")
|
|
22
|
+
return;
|
|
23
|
+
const anchor = document.createElement("a");
|
|
24
|
+
anchor.href = dataUrl;
|
|
25
|
+
anchor.download = filename;
|
|
26
|
+
anchor.click();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* In-`<Canvas>` binder that hands the scene-capture function out to HUD code
|
|
30
|
+
* living outside the reconciler (a photo-mode Capture button). Mount it inside
|
|
31
|
+
* the game's `WorldOverlay`; call `bind` receives a `() => string | null` that
|
|
32
|
+
* grabs the current frame. Renders nothing.
|
|
33
|
+
*
|
|
34
|
+
* @capability scene-capture-binding expose the in-Canvas scene-capture function to outside-Canvas HUD (photo mode)
|
|
35
|
+
*/
|
|
36
|
+
export function SceneCaptureBinding({ bind }) {
|
|
37
|
+
const gl = useThree((state) => state.gl);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
bind(() => captureCanvas(gl));
|
|
40
|
+
return () => bind(() => null);
|
|
41
|
+
}, [gl, bind]);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DependencyList } from "react";
|
|
2
|
+
/** Anything exposing dispose() — three.js geometries, materials, textures, render targets. */
|
|
3
|
+
export interface Disposable {
|
|
4
|
+
dispose(): void;
|
|
5
|
+
}
|
|
6
|
+
/** Dispose a single resource or every resource in a tuple. */
|
|
7
|
+
export declare function disposeAll(value: Disposable | readonly Disposable[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* Memoize a three.js resource (or tuple of resources) and dispose it when `deps` change or the
|
|
10
|
+
* component unmounts. Owns the useMemo + dispose-effect pair that otherwise repeats at every
|
|
11
|
+
* GPU-resource call site. The factory must return objects exposing `dispose()` (geometries,
|
|
12
|
+
* materials, textures, render targets).
|
|
13
|
+
*
|
|
14
|
+
* @capability disposable-resource memoize a three.js GPU resource (or tuple) with automatic dispose on change/unmount — no hand-rolled useMemo + dispose-effect pairs
|
|
15
|
+
*/
|
|
16
|
+
export declare function useDisposable<T extends Disposable | readonly Disposable[]>(create: () => T, deps: DependencyList): T;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
|
+
/** Dispose a single resource or every resource in a tuple. */
|
|
3
|
+
export function disposeAll(value) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
for (const item of value)
|
|
6
|
+
item.dispose();
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
value.dispose();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Memoize a three.js resource (or tuple of resources) and dispose it when `deps` change or the
|
|
13
|
+
* component unmounts. Owns the useMemo + dispose-effect pair that otherwise repeats at every
|
|
14
|
+
* GPU-resource call site. The factory must return objects exposing `dispose()` (geometries,
|
|
15
|
+
* materials, textures, render targets).
|
|
16
|
+
*
|
|
17
|
+
* @capability disposable-resource memoize a three.js GPU resource (or tuple) with automatic dispose on change/unmount — no hand-rolled useMemo + dispose-effect pairs
|
|
18
|
+
*/
|
|
19
|
+
export function useDisposable(create, deps) {
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
const value = useMemo(create, deps);
|
|
22
|
+
useEffect(() => () => disposeAll(value), [value]);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
@@ -3,7 +3,46 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { resolveAnimationConfig } from "@jgengine/core/game/clipRoles";
|
|
5
5
|
import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
|
|
6
|
-
import {
|
|
6
|
+
import { useOptionalGameContext } from "@jgengine/react/provider";
|
|
7
|
+
/**
|
|
8
|
+
* An explicit `animation` config always wins over `"auto"`, and both the state machine and the
|
|
9
|
+
* one-shot table degrade quietly when a named clip is absent: states fall back to `clips[0]` (often
|
|
10
|
+
* an attack clip, so an actor "idles" mid-swing) and one-shots are dropped, so deaths and attacks
|
|
11
|
+
* never play. Neither leaves a trace, which makes a mistyped or wrong-pack clip name one of the
|
|
12
|
+
* hardest render bugs to see — the model loads, animates, and is simply wrong forever.
|
|
13
|
+
*
|
|
14
|
+
* Warn once per mount with the names that missed and the names the rig actually has, so the fix
|
|
15
|
+
* (usually deleting the hand-written config and letting `"auto"` derive it) is obvious.
|
|
16
|
+
*/
|
|
17
|
+
function warnMissingClips(animation, clips) {
|
|
18
|
+
if (typeof console === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
const available = new Set(clips.map((clip) => clip.name));
|
|
21
|
+
const missing = new Set();
|
|
22
|
+
const { states, oneShots } = animation;
|
|
23
|
+
if (states !== undefined) {
|
|
24
|
+
for (const name of [states.idle, states.walk, states.run]) {
|
|
25
|
+
if (name !== undefined && !available.has(name))
|
|
26
|
+
missing.add(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (oneShots !== undefined) {
|
|
30
|
+
for (const spec of Object.values(oneShots)) {
|
|
31
|
+
for (const name of typeof spec === "string" ? [spec] : spec) {
|
|
32
|
+
if (!available.has(name))
|
|
33
|
+
missing.add(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (animation.clip !== undefined && !available.has(animation.clip))
|
|
38
|
+
missing.add(animation.clip);
|
|
39
|
+
if (missing.size === 0)
|
|
40
|
+
return;
|
|
41
|
+
console.warn(`[jgengine] model animation: clip(s) ${[...missing].map((name) => `"${name}"`).join(", ")} ` +
|
|
42
|
+
`not found on this rig. Available: ${clips.map((clip) => clip.name).join(", ")}. ` +
|
|
43
|
+
`States fall back to the first clip and one-shots are skipped — set animation: "auto" to ` +
|
|
44
|
+
`derive states/one-shots from the rig's own clip names.`);
|
|
45
|
+
}
|
|
7
46
|
/**
|
|
8
47
|
* The engine's model animation driver as a standalone hook — the same mixer `EntityModel` runs,
|
|
9
48
|
* for games that render a cloned scene themselves (custom materials, procedural composition).
|
|
@@ -12,7 +51,10 @@ import { useGameContext } from "@jgengine/react/provider";
|
|
|
12
51
|
* `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
|
|
13
52
|
*/
|
|
14
53
|
export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
15
|
-
|
|
54
|
+
// Optional: a model must still animate its bind pose / auto clip in a preview or inspector that
|
|
55
|
+
// has no running game — a hard context requirement made every part composition unviewable outside
|
|
56
|
+
// the world, which is half of why #1588 took a session to see.
|
|
57
|
+
const ctx = useOptionalGameContext();
|
|
16
58
|
// "auto" (stamped by catalog resolution, or set inline) derives states/one-shots from the
|
|
17
59
|
// loaded GLB's actual clip names; "none" and absent render the bind pose.
|
|
18
60
|
const animation = useMemo(() => resolveAnimationConfig(animationInput, clips.map((clip) => clip.name)), [animationInput, clips]);
|
|
@@ -29,6 +71,7 @@ export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
|
29
71
|
stateActionsRef.current = null;
|
|
30
72
|
return;
|
|
31
73
|
}
|
|
74
|
+
warnMissingClips(animation, clips);
|
|
32
75
|
const mixer = new THREE.AnimationMixer(scene);
|
|
33
76
|
if (states !== undefined) {
|
|
34
77
|
const clipFor = (name) => THREE.AnimationClip.findByName(clips, name) ?? clips[0];
|
|
@@ -138,7 +181,7 @@ export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
|
138
181
|
oneShots,
|
|
139
182
|
]);
|
|
140
183
|
useEffect(() => {
|
|
141
|
-
if (instanceId === undefined || oneShots === undefined)
|
|
184
|
+
if (ctx === null || instanceId === undefined || oneShots === undefined)
|
|
142
185
|
return;
|
|
143
186
|
const fire = (event) => oneShotPlayRef.current?.(event);
|
|
144
187
|
const offAnimation = ctx.game.events.on("entity.animation", (event) => {
|
|
@@ -161,7 +204,7 @@ export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
|
161
204
|
}, [ctx, instanceId, oneShots]);
|
|
162
205
|
useFrame((_state, delta) => {
|
|
163
206
|
const stateMachine = stateActionsRef.current;
|
|
164
|
-
if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
|
|
207
|
+
if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0 && ctx !== null) {
|
|
165
208
|
const entity = ctx.scene.entity.get(instanceId);
|
|
166
209
|
if (entity !== null) {
|
|
167
210
|
const [x, , z] = entity.position;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useEffect, useMemo,
|
|
2
|
+
import { Fragment, useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { getDocumentLiveSync, subscribeDocumentLiveSyncInstall, } from "@jgengine/core/editor/index";
|
|
5
5
|
import { placeAuthoredObjects, resolveAuthoredObjects, } from "@jgengine/core/world/authoredObjects";
|
|
6
|
-
import { buildRoadRibbon, roundPathCorners } from "@jgengine/core/world/roads";
|
|
6
|
+
import { buildRoadRibbon, GROUND_DECAL_LAYERS, roundPathCorners } from "@jgengine/core/world/roads";
|
|
7
7
|
import { isScatterPath, resolveScatter } from "@jgengine/core/world/scatterRegion";
|
|
8
8
|
import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
|
|
9
9
|
import { useGameContext } from "@jgengine/react/provider";
|
|
@@ -12,6 +12,7 @@ import { InstancedScatter } from "../scatter/InstancedScatter.js";
|
|
|
12
12
|
import { GeneratedAssetInstance } from "./GeneratedAssetRenderer.js";
|
|
13
13
|
import { registerBuiltinSceneKindRenderers } from "./builtinSceneKindRenderers.js";
|
|
14
14
|
import { getSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
15
|
+
const noopSubscribe = () => () => undefined;
|
|
15
16
|
/**
|
|
16
17
|
* Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
|
|
17
18
|
* installed (editor host live), subscribe to document patches; otherwise use the prop.
|
|
@@ -19,32 +20,20 @@ import { getSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
|
19
20
|
* @internal
|
|
20
21
|
*/
|
|
21
22
|
export function useLiveEditorDocument(document, live = true) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
setResolved(document);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
let unsubDoc;
|
|
29
|
-
const attach = () => {
|
|
23
|
+
const subscribeLive = useCallback((onStoreChange) => {
|
|
24
|
+
let unsubDoc = getDocumentLiveSync()?.subscribeDocument(onStoreChange);
|
|
25
|
+
const unsubInstall = subscribeDocumentLiveSyncInstall(() => {
|
|
30
26
|
unsubDoc?.();
|
|
31
|
-
unsubDoc =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setResolved(document);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
setResolved(sync.getDocument());
|
|
38
|
-
unsubDoc = sync.subscribeDocument((event) => setResolved(event.document));
|
|
39
|
-
};
|
|
40
|
-
attach();
|
|
41
|
-
const unsubInstall = subscribeDocumentLiveSyncInstall(attach);
|
|
27
|
+
unsubDoc = getDocumentLiveSync()?.subscribeDocument(onStoreChange);
|
|
28
|
+
onStoreChange();
|
|
29
|
+
});
|
|
42
30
|
return () => {
|
|
43
31
|
unsubInstall();
|
|
44
32
|
unsubDoc?.();
|
|
45
33
|
};
|
|
46
|
-
}, [
|
|
47
|
-
|
|
34
|
+
}, []);
|
|
35
|
+
const getDocument = useCallback(() => (live ? (getDocumentLiveSync()?.getDocument() ?? document) : document), [live, document]);
|
|
36
|
+
return useSyncExternalStore(live ? subscribeLive : noopSubscribe, getDocument, getDocument);
|
|
48
37
|
}
|
|
49
38
|
registerBuiltinSceneKindRenderers();
|
|
50
39
|
function markerToKindObject(marker) {
|
|
@@ -106,7 +95,8 @@ function DrapedPath({ points, width, color, field, }) {
|
|
|
106
95
|
// Fillet sharp turns first so bends read as smooth arcs, not overlapping rectangles.
|
|
107
96
|
const rounded = roundPathCorners(points, Math.max(1.5, width * 0.9), 6);
|
|
108
97
|
const ribbon = buildRoadRibbon(rounded, width, (x, z) => field.sampleHeight(x, z), {
|
|
109
|
-
|
|
98
|
+
// Generic authored paths (dirt roads, routes, corridors) drape on the shared road decal layer.
|
|
99
|
+
elevation: GROUND_DECAL_LAYERS.road,
|
|
110
100
|
maxSegmentLength: 1,
|
|
111
101
|
});
|
|
112
102
|
const geo = new THREE.BufferGeometry();
|