@jgengine/shell 0.11.0 → 0.13.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 +52 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +27 -1
- package/dist/GameHost.js +83 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +63 -359
- package/dist/GameUiPreview.js +3 -0
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/GameOrbitCamera.js +7 -1
- package/dist/camera/cameraCollision.d.ts +28 -0
- package/dist/camera/cameraCollision.js +34 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +24 -10
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +27 -0
- package/dist/camera/rigMath.js +45 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +17 -3
- package/dist/environment/RoadJunctions.d.ts +12 -0
- package/dist/environment/RoadJunctions.js +69 -0
- package/dist/environment/RoadRibbons.d.ts +8 -2
- package/dist/environment/RoadRibbons.js +26 -8
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +39 -0
- package/dist/presentationResolve.js +53 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/PartMotion.d.ts +18 -0
- package/dist/render/PartMotion.js +91 -0
- package/dist/render/SceneModels.d.ts +9 -2
- package/dist/render/SceneModels.js +31 -180
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/measureBounds.d.ts +34 -0
- package/dist/render/measureBounds.js +127 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +43 -2
- package/dist/render/useModelAnimation.d.ts +10 -0
- package/dist/render/useModelAnimation.js +198 -0
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +4 -0
- package/dist/structures/GeneratedBuilding.js +17 -7
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +2 -2
- package/dist/world/WorldScene.js +32 -4
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The happy-path game kit — one import surface covering a whole simple game: authoring
|
|
3
|
+
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), the authored-scene document helpers, HUD primitives, and deterministic
|
|
5
|
+
* randomness. Start here; reach for deep package paths only when a game outgrows the kit.
|
|
6
|
+
*
|
|
7
|
+
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
|
+
*/
|
|
9
|
+
export { defineGame, type GameConfig } from "./defineGame.js";
|
|
10
|
+
export { GameHost, type GameHostProps, type EditorSummonModule } from "./GameHost.js";
|
|
11
|
+
export type { PlayableGame } from "./registry.js";
|
|
12
|
+
export { defineStore, type StoreHandle } from "@jgengine/core/store/defineStore";
|
|
13
|
+
export { defineKeyedStore } from "@jgengine/core/store/defineKeyedStore";
|
|
14
|
+
export { defineSystem, type SystemDefinition } from "@jgengine/core/game/defineSystem";
|
|
15
|
+
export type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
16
|
+
export type { GameLoop } from "@jgengine/core/game/defineGame";
|
|
17
|
+
export { normalizeEditorLayers } from "@jgengine/core/editor/document";
|
|
18
|
+
export type { EditorDocument, EditorCatalogDefinition } from "@jgengine/core/editor/types";
|
|
19
|
+
export { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn";
|
|
20
|
+
export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
21
|
+
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
22
|
+
export { environment } from "@jgengine/core/world/features";
|
|
23
|
+
export type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
24
|
+
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
|
+
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
|
+
export { seededRng } from "@jgengine/core/random/rng";
|
|
27
|
+
export { offline } from "@jgengine/core/runtime/adapter";
|
package/dist/gameKit.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The happy-path game kit — one import surface covering a whole simple game: authoring
|
|
3
|
+
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), the authored-scene document helpers, HUD primitives, and deterministic
|
|
5
|
+
* randomness. Start here; reach for deep package paths only when a game outgrows the kit.
|
|
6
|
+
*
|
|
7
|
+
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
|
+
*/
|
|
9
|
+
// Authoring + mounting — the one public path from config to a running game.
|
|
10
|
+
export { defineGame } from "./defineGame.js";
|
|
11
|
+
export { GameHost } from "./GameHost.js";
|
|
12
|
+
// Serializable state + composable behavior.
|
|
13
|
+
export { defineStore } from "@jgengine/core/store/defineStore";
|
|
14
|
+
export { defineKeyedStore } from "@jgengine/core/store/defineKeyedStore";
|
|
15
|
+
export { defineSystem } from "@jgengine/core/game/defineSystem";
|
|
16
|
+
// Authored scene document — normalize `editor.scene.json`, read spawns and trigger volumes.
|
|
17
|
+
export { normalizeEditorLayers } from "@jgengine/core/editor/document";
|
|
18
|
+
export { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn";
|
|
19
|
+
export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
20
|
+
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
21
|
+
// World + camera.
|
|
22
|
+
export { environment } from "@jgengine/core/world/features";
|
|
23
|
+
// HUD layout + optional building blocks — games own custom chrome (not a stock face).
|
|
24
|
+
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
|
+
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
|
+
// Deterministic randomness (inject seeds; never Math.random in game logic).
|
|
27
|
+
export { seededRng } from "@jgengine/core/random/rng";
|
|
28
|
+
// Multiplayer story: omit `multiplayer` entirely for solo — offline is the shell default.
|
|
29
|
+
export { offline } from "@jgengine/core/runtime/adapter";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type MarkerCollection, type MarkerKindStyle } from "@jgengine/core/world/markers";
|
|
2
2
|
export interface MapMarkerBeaconsProps {
|
|
3
|
-
|
|
3
|
+
/** Static views, an external marker source, or a native JGengine MarkerSet. */
|
|
4
|
+
markers: MarkerCollection;
|
|
4
5
|
kindStyles?: Record<string, MarkerKindStyle>;
|
|
5
6
|
height?: number;
|
|
6
7
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { PostProcessingConfig } from "@jgengine/core/render/postProcessing";
|
|
2
|
+
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
2
3
|
/**
|
|
3
4
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
4
5
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
5
6
|
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
6
7
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
8
|
+
*
|
|
9
|
+
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
10
|
+
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
11
|
+
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
12
|
+
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
13
|
+
* tone mapping, and grade stay on every tier.
|
|
7
14
|
*/
|
|
8
|
-
export declare function PostProcessing({ config }: {
|
|
15
|
+
export declare function PostProcessing({ config, quality }: {
|
|
9
16
|
config: PostProcessingConfig;
|
|
17
|
+
quality?: GraphicsQuality;
|
|
10
18
|
}): null;
|
|
@@ -36,8 +36,14 @@ function syncSize(built, width, height, pixelRatio) {
|
|
|
36
36
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
37
37
|
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
38
38
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
39
|
+
*
|
|
40
|
+
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
41
|
+
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
42
|
+
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
43
|
+
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
44
|
+
* tone mapping, and grade stay on every tier.
|
|
39
45
|
*/
|
|
40
|
-
export function PostProcessing({ config }) {
|
|
46
|
+
export function PostProcessing({ config, quality = "high" }) {
|
|
41
47
|
const gl = useThree((s) => s.gl);
|
|
42
48
|
const scene = useThree((s) => s.scene);
|
|
43
49
|
const camera = useThree((s) => s.camera);
|
|
@@ -52,7 +58,8 @@ export function PostProcessing({ config }) {
|
|
|
52
58
|
});
|
|
53
59
|
const composer = new EffectComposer(gl, target);
|
|
54
60
|
composer.addPass(new RenderPass(scene, camera));
|
|
55
|
-
|
|
61
|
+
const heavyPasses = quality === "high";
|
|
62
|
+
if (heavyPasses && config.ao !== undefined && config.ao !== false) {
|
|
56
63
|
const ao = new GTAOPass(scene, camera, width, height);
|
|
57
64
|
ao.blendIntensity = config.ao.blend ?? 1;
|
|
58
65
|
ao.updateGtaoMaterial({
|
|
@@ -69,7 +76,7 @@ export function PostProcessing({ config }) {
|
|
|
69
76
|
composer.addPass(new UnrealBloomPass(new THREE.Vector2(width, height), b.strength ?? 0.32, b.radius ?? 0.55, b.threshold ?? 0.85));
|
|
70
77
|
}
|
|
71
78
|
let dof = null;
|
|
72
|
-
if (config.dof !== undefined && config.dof !== false) {
|
|
79
|
+
if (heavyPasses && config.dof !== undefined && config.dof !== false) {
|
|
73
80
|
const d = config.dof;
|
|
74
81
|
dof = new BokehPass(scene, camera, {
|
|
75
82
|
focus: d.focus ?? 18,
|
|
@@ -91,7 +98,7 @@ export function PostProcessing({ config }) {
|
|
|
91
98
|
disposeGraph(graph);
|
|
92
99
|
builtRef.current = null;
|
|
93
100
|
};
|
|
94
|
-
}, [gl, scene, camera, config]);
|
|
101
|
+
}, [gl, scene, camera, config, quality]);
|
|
95
102
|
useEffect(() => {
|
|
96
103
|
const prevTone = gl.toneMapping;
|
|
97
104
|
const prevExposure = gl.toneMappingExposure;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { PresentationEffectsConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/** Resolved world-bar / nameplate config, or `null` when the feature is off. @internal */
|
|
4
|
+
export interface ResolvedWorldOverlayBars {
|
|
5
|
+
statId: string;
|
|
6
|
+
roles?: readonly CatalogEntityRole[];
|
|
7
|
+
maxDistance?: number;
|
|
8
|
+
/** Nameplates only: draw the built-in HP bar. `false` yields a name-only plate. Defaults to `true`. */
|
|
9
|
+
showHealth?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Normalize `worldHealthBars` / `nameplates` (`boolean | object | undefined`) into a
|
|
13
|
+
* single resolved shape. `undefined` / `false` → `null` (off); `true` → default stat `"health"`;
|
|
14
|
+
* object → explicit fields with `statId` defaulting to `"health"`. `showHealth` is nameplate-only
|
|
15
|
+
* (ignored by `worldHealthBars`, which is itself a bar).
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveWorldOverlayBars(config: boolean | {
|
|
19
|
+
statId?: string;
|
|
20
|
+
roles?: readonly CatalogEntityRole[];
|
|
21
|
+
maxDistance?: number;
|
|
22
|
+
showHealth?: boolean;
|
|
23
|
+
} | undefined): ResolvedWorldOverlayBars | null;
|
|
24
|
+
/** Resolved combat presentation channels (each boolean is whether that stack piece mounts). @internal */
|
|
25
|
+
export interface ResolvedPresentationEffects {
|
|
26
|
+
telegraphs: boolean;
|
|
27
|
+
vfx: boolean;
|
|
28
|
+
floatText: boolean;
|
|
29
|
+
tracers: boolean;
|
|
30
|
+
shake: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve `presentationEffects` for the 3D shell.
|
|
34
|
+
* - `undefined` / `true` → all channels on (historical default).
|
|
35
|
+
* - `false` → none.
|
|
36
|
+
* - object → per-channel; missing keys default to on so games can disable one channel.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolvePresentationEffects(config: boolean | PresentationEffectsConfig | undefined): ResolvedPresentationEffects;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize `worldHealthBars` / `nameplates` (`boolean | object | undefined`) into a
|
|
3
|
+
* single resolved shape. `undefined` / `false` → `null` (off); `true` → default stat `"health"`;
|
|
4
|
+
* object → explicit fields with `statId` defaulting to `"health"`. `showHealth` is nameplate-only
|
|
5
|
+
* (ignored by `worldHealthBars`, which is itself a bar).
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export function resolveWorldOverlayBars(config) {
|
|
9
|
+
if (config === undefined || config === false)
|
|
10
|
+
return null;
|
|
11
|
+
if (config === true)
|
|
12
|
+
return { statId: "health" };
|
|
13
|
+
return {
|
|
14
|
+
statId: config.statId ?? "health",
|
|
15
|
+
...(config.roles === undefined ? {} : { roles: config.roles }),
|
|
16
|
+
...(config.maxDistance === undefined ? {} : { maxDistance: config.maxDistance }),
|
|
17
|
+
...(config.showHealth === undefined ? {} : { showHealth: config.showHealth }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const ALL_ON = {
|
|
21
|
+
telegraphs: true,
|
|
22
|
+
vfx: true,
|
|
23
|
+
floatText: true,
|
|
24
|
+
tracers: true,
|
|
25
|
+
shake: true,
|
|
26
|
+
};
|
|
27
|
+
const ALL_OFF = {
|
|
28
|
+
telegraphs: false,
|
|
29
|
+
vfx: false,
|
|
30
|
+
floatText: false,
|
|
31
|
+
tracers: false,
|
|
32
|
+
shake: false,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Resolve `presentationEffects` for the 3D shell.
|
|
36
|
+
* - `undefined` / `true` → all channels on (historical default).
|
|
37
|
+
* - `false` → none.
|
|
38
|
+
* - object → per-channel; missing keys default to on so games can disable one channel.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export function resolvePresentationEffects(config) {
|
|
42
|
+
if (config === false)
|
|
43
|
+
return ALL_OFF;
|
|
44
|
+
if (config === undefined || config === true)
|
|
45
|
+
return ALL_ON;
|
|
46
|
+
return {
|
|
47
|
+
telegraphs: config.telegraphs !== false,
|
|
48
|
+
vfx: config.vfx !== false,
|
|
49
|
+
floatText: config.floatText !== false,
|
|
50
|
+
tracers: config.tracers !== false,
|
|
51
|
+
shake: config.shake !== false,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
|
5
5
|
import type { ViewmodelProps } from "./camera/GameFirstPersonCamera.js";
|
|
6
6
|
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
7
7
|
export type RenderObject = (object: SceneObject) => ReactNode;
|
|
8
|
+
/** The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle. */
|
|
8
9
|
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps>>;
|
|
9
10
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
10
11
|
export declare function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ModelConfig, ModelPart } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/**
|
|
4
|
+
* Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see
|
|
5
|
+
* `@jgengine/core/game/partAnimation`). Wraps the whole composition in a root group that bobs,
|
|
6
|
+
* breathes, flinches on `combat.hitReaction`, and topples on `entity.died`, while each
|
|
7
|
+
* role-tagged part swings around its authored transform — legs/arms counter-phase from the
|
|
8
|
+
* entity's live movement speed, head counter-sway, tail wag, wing flap. Untagged parts render
|
|
9
|
+
* as static kit pieces. Children are the base model content (primitive, attachments).
|
|
10
|
+
*/
|
|
11
|
+
export declare function PartMotionRig({ parts, model, instanceId, renderPart, children, }: {
|
|
12
|
+
parts: readonly ModelPart[];
|
|
13
|
+
model: ModelConfig;
|
|
14
|
+
instanceId?: string;
|
|
15
|
+
/** Renders one part's model content (the shell passes its part renderer to avoid an import cycle). */
|
|
16
|
+
renderPart: (part: ModelPart, index: number) => ReactNode;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}): ReactNode;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { createPartPose, partMotionPhase, sampleBodyPose, samplePartPose, } from "@jgengine/core/game/partAnimation";
|
|
6
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
7
|
+
const FLINCH_RELEASE_SEC = 0.35;
|
|
8
|
+
const DEATH_RAMP_PER_SEC = 2.5;
|
|
9
|
+
const SPEED_SMOOTHING = 12;
|
|
10
|
+
/**
|
|
11
|
+
* Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see
|
|
12
|
+
* `@jgengine/core/game/partAnimation`). Wraps the whole composition in a root group that bobs,
|
|
13
|
+
* breathes, flinches on `combat.hitReaction`, and topples on `entity.died`, while each
|
|
14
|
+
* role-tagged part swings around its authored transform — legs/arms counter-phase from the
|
|
15
|
+
* entity's live movement speed, head counter-sway, tail wag, wing flap. Untagged parts render
|
|
16
|
+
* as static kit pieces. Children are the base model content (primitive, attachments).
|
|
17
|
+
*/
|
|
18
|
+
export function PartMotionRig({ parts, model, instanceId, renderPart, children, }) {
|
|
19
|
+
const ctx = useGameContext();
|
|
20
|
+
const rootRef = useRef(null);
|
|
21
|
+
const partRefs = useRef([]);
|
|
22
|
+
const motionRef = useRef({
|
|
23
|
+
lastPos: null,
|
|
24
|
+
speed: 0,
|
|
25
|
+
flinch: 0,
|
|
26
|
+
death: 0,
|
|
27
|
+
});
|
|
28
|
+
const phase = useMemo(() => partMotionPhase(instanceId ?? model.url), [instanceId, model.url]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (instanceId === undefined)
|
|
31
|
+
return;
|
|
32
|
+
const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
|
|
33
|
+
if (event.instanceId === instanceId)
|
|
34
|
+
motionRef.current.flinch = 1;
|
|
35
|
+
});
|
|
36
|
+
const offDied = ctx.game.events.on("entity.died", (event) => {
|
|
37
|
+
if (event.instanceId === instanceId)
|
|
38
|
+
motionRef.current.death = Math.max(motionRef.current.death, 1e-6);
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
offHit();
|
|
42
|
+
offDied();
|
|
43
|
+
};
|
|
44
|
+
}, [ctx, instanceId]);
|
|
45
|
+
const input = useMemo(() => ({ timeSec: 0, speed: 0, phase, flinch: 0, death: 0 }), [phase]);
|
|
46
|
+
const pose = useMemo(() => createPartPose(), []);
|
|
47
|
+
useFrame((frameState, delta) => {
|
|
48
|
+
const motion = motionRef.current;
|
|
49
|
+
if (delta > 0 && instanceId !== undefined) {
|
|
50
|
+
const entity = ctx.scene.entity.get(instanceId);
|
|
51
|
+
if (entity !== null) {
|
|
52
|
+
const [x, , z] = entity.position;
|
|
53
|
+
if (motion.lastPos !== null) {
|
|
54
|
+
const instantSpeed = Math.hypot(x - motion.lastPos[0], z - motion.lastPos[2]) / delta;
|
|
55
|
+
motion.speed += (instantSpeed - motion.speed) * Math.min(1, delta * SPEED_SMOOTHING);
|
|
56
|
+
}
|
|
57
|
+
motion.lastPos = [x, entity.position[1], z];
|
|
58
|
+
}
|
|
59
|
+
motion.flinch = Math.max(0, motion.flinch - delta / FLINCH_RELEASE_SEC);
|
|
60
|
+
if (motion.death > 0)
|
|
61
|
+
motion.death = Math.min(1, motion.death + delta * DEATH_RAMP_PER_SEC);
|
|
62
|
+
}
|
|
63
|
+
input.timeSec = frameState.clock.elapsedTime;
|
|
64
|
+
input.speed = motion.speed;
|
|
65
|
+
input.flinch = motion.flinch;
|
|
66
|
+
input.death = motion.death;
|
|
67
|
+
const root = rootRef.current;
|
|
68
|
+
if (root !== null) {
|
|
69
|
+
sampleBodyPose(input, model.partMotion, pose);
|
|
70
|
+
root.position.set(pose.position[0], pose.position[1], pose.position[2]);
|
|
71
|
+
root.rotation.set(pose.rotation[0], pose.rotation[1], pose.rotation[2]);
|
|
72
|
+
root.scale.set(pose.scale[0], pose.scale[1], pose.scale[2]);
|
|
73
|
+
}
|
|
74
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
75
|
+
const part = parts[index];
|
|
76
|
+
const group = partRefs.current[index];
|
|
77
|
+
if (group === null || group === undefined || part.role === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
samplePartPose(part.role, input, model.partMotion, pose);
|
|
80
|
+
const base = part.position ?? [0, 0, 0];
|
|
81
|
+
const baseRotation = part.rotation ?? [0, 0, 0];
|
|
82
|
+
group.position.set(base[0] + pose.position[0], base[1] + pose.position[1], base[2] + pose.position[2]);
|
|
83
|
+
group.rotation.set(baseRotation[0] + pose.rotation[0], baseRotation[1] + pose.rotation[1], baseRotation[2] + pose.rotation[2]);
|
|
84
|
+
const baseScale = part.scale ?? 1;
|
|
85
|
+
group.scale.set(baseScale * pose.scale[0], baseScale * pose.scale[1], baseScale * pose.scale[2]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return (_jsxs("group", { ref: rootRef, children: [children, parts.map((part, index) => (_jsx("group", { ref: (group) => {
|
|
89
|
+
partRefs.current[index] = group;
|
|
90
|
+
}, position: part.position ?? [0, 0, 0], rotation: part.rotation ?? [0, 0, 0], scale: part.scale ?? 1, children: renderPart(part, index) }, index)))] }));
|
|
91
|
+
}
|
|
@@ -3,12 +3,19 @@ import type { EntitySpriteConfig, ModelConfig } from "@jgengine/core/game/playab
|
|
|
3
3
|
export declare function EntitySprite({ sprite }: {
|
|
4
4
|
sprite: EntitySpriteConfig;
|
|
5
5
|
}): import("react").JSX.Element;
|
|
6
|
-
export declare function IsolatedEntityModel({ model, instanceId, fallback, }: {
|
|
6
|
+
export declare function IsolatedEntityModel({ model, instanceId, measure, fallback, }: {
|
|
7
7
|
model: ModelConfig;
|
|
8
8
|
instanceId?: string;
|
|
9
|
+
measure?: MeasureTarget;
|
|
9
10
|
fallback?: ReactNode;
|
|
10
11
|
}): import("react").JSX.Element;
|
|
11
|
-
|
|
12
|
+
/** Where a measured model reports its rendered bounds: an entity kind or an object catalog id. */
|
|
13
|
+
export interface MeasureTarget {
|
|
14
|
+
target: "entity" | "object";
|
|
15
|
+
key: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function EntityModel({ model, instanceId, measure, }: {
|
|
12
18
|
model: ModelConfig;
|
|
13
19
|
instanceId?: string;
|
|
20
|
+
measure?: MeasureTarget;
|
|
14
21
|
}): import("react").JSX.Element;
|
|
@@ -3,9 +3,11 @@ import { useFrame, useLoader } from "@react-three/fiber";
|
|
|
3
3
|
import { useTexture } from "@react-three/drei";
|
|
4
4
|
import { Component, Suspense, useEffect, useMemo, useRef } from "react";
|
|
5
5
|
import * as THREE from "three";
|
|
6
|
-
import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
|
|
7
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
8
7
|
import { sharedGltfLoader } from "./modelLoad.js";
|
|
8
|
+
import { measureLocalBounds, reportMeasuredBounds } from "./measureBounds.js";
|
|
9
|
+
import { useModelAnimation } from "./useModelAnimation.js";
|
|
10
|
+
import { PartMotionRig } from "./PartMotion.js";
|
|
9
11
|
import { applyMaterialOverride } from "../materialOverride.js";
|
|
10
12
|
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
|
|
11
13
|
export function EntitySprite({ sprite }) {
|
|
@@ -26,8 +28,8 @@ class ModelFallbackBoundary extends Component {
|
|
|
26
28
|
return this.state.failed ? this.props.fallback : this.props.children;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
|
-
export function IsolatedEntityModel({ model, instanceId, fallback, }) {
|
|
30
|
-
return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
|
|
31
|
+
export function IsolatedEntityModel({ model, instanceId, measure, fallback, }) {
|
|
32
|
+
return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId, measure: measure }) }) }));
|
|
31
33
|
}
|
|
32
34
|
function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
|
|
33
35
|
const gltf = useLoader(sharedGltfLoader, model.url);
|
|
@@ -83,7 +85,7 @@ function ModelMaterialMapsApplier({ scene, maps }) {
|
|
|
83
85
|
}, [scene, textures]);
|
|
84
86
|
return null;
|
|
85
87
|
}
|
|
86
|
-
export function EntityModel({ model, instanceId }) {
|
|
88
|
+
export function EntityModel({ model, instanceId, measure, }) {
|
|
87
89
|
const gltf = useLoader(sharedGltfLoader, model.url);
|
|
88
90
|
const ctx = useGameContext();
|
|
89
91
|
const material = model.material;
|
|
@@ -119,150 +121,26 @@ export function EntityModel({ model, instanceId }) {
|
|
|
119
121
|
useEffect(() => () => {
|
|
120
122
|
disposeClonedMaterials(scene);
|
|
121
123
|
}, [scene]);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const activeOneShotRef = useRef(null);
|
|
124
|
+
// A model without index-measured dims can't drive the fitted-collider path, so report the live
|
|
125
|
+
// measurement (the primitive mounts with only this uniform scale + position) instead of letting
|
|
126
|
+
// the kind fall back to a fixed-size box. Index dims keep priority — skip when they exist.
|
|
127
|
+
const measureTarget = measure?.target;
|
|
128
|
+
const measureKey = measure?.key;
|
|
129
|
+
const dimsMaxY = dims?.maxY;
|
|
130
|
+
const [positionX, positionY, positionZ] = position;
|
|
130
131
|
useEffect(() => {
|
|
131
|
-
if (
|
|
132
|
-
mixerRef.current = null;
|
|
133
|
-
stateActionsRef.current = null;
|
|
132
|
+
if (measureTarget === undefined || measureKey === undefined || dimsMaxY !== undefined)
|
|
134
133
|
return;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (states !== undefined) {
|
|
138
|
-
const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
|
|
139
|
-
const actions = {
|
|
140
|
-
idle: mixer.clipAction(clipFor(states.idle)),
|
|
141
|
-
walk: mixer.clipAction(clipFor(states.walk)),
|
|
142
|
-
...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
|
|
143
|
-
};
|
|
144
|
-
for (const action of Object.values(actions)) {
|
|
145
|
-
action.setLoop(THREE.LoopRepeat, Infinity);
|
|
146
|
-
action.timeScale = animation.timeScale ?? 1;
|
|
147
|
-
action.enabled = true;
|
|
148
|
-
}
|
|
149
|
-
actions.idle.play();
|
|
150
|
-
mixer.update(0);
|
|
151
|
-
mixerRef.current = mixer;
|
|
152
|
-
stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
|
|
153
|
-
animationPausedRef.current = false;
|
|
154
|
-
let onOneShotFinished = null;
|
|
155
|
-
if (oneShots !== undefined) {
|
|
156
|
-
const clipNames = new Set();
|
|
157
|
-
for (const spec of Object.values(oneShots)) {
|
|
158
|
-
if (typeof spec === "string")
|
|
159
|
-
clipNames.add(spec);
|
|
160
|
-
else
|
|
161
|
-
for (const name of spec)
|
|
162
|
-
clipNames.add(name);
|
|
163
|
-
}
|
|
164
|
-
const oneShotActions = new Map();
|
|
165
|
-
for (const name of clipNames) {
|
|
166
|
-
const found = THREE.AnimationClip.findByName(gltf.animations, name);
|
|
167
|
-
if (found === null)
|
|
168
|
-
continue;
|
|
169
|
-
const oneShotAction = mixer.clipAction(found);
|
|
170
|
-
oneShotAction.setLoop(THREE.LoopOnce, 1);
|
|
171
|
-
oneShotAction.enabled = true;
|
|
172
|
-
oneShotActions.set(name, oneShotAction);
|
|
173
|
-
}
|
|
174
|
-
onOneShotFinished = ({ action }) => {
|
|
175
|
-
const active = activeOneShotRef.current;
|
|
176
|
-
if (active === null || action !== active.action || active.isDeath)
|
|
177
|
-
return;
|
|
178
|
-
const machine = stateActionsRef.current;
|
|
179
|
-
const back = machine?.actions[machine.active];
|
|
180
|
-
action.fadeOut(0.15);
|
|
181
|
-
if (back !== undefined)
|
|
182
|
-
back.reset().fadeIn(0.15).play();
|
|
183
|
-
activeOneShotRef.current = null;
|
|
184
|
-
};
|
|
185
|
-
mixer.addEventListener("finished", onOneShotFinished);
|
|
186
|
-
oneShotPlayRef.current = (event) => {
|
|
187
|
-
const active = activeOneShotRef.current;
|
|
188
|
-
if (active !== null && active.isDeath)
|
|
189
|
-
return;
|
|
190
|
-
const clipName = resolveOneShotClip(oneShots, event, Math.random());
|
|
191
|
-
if (clipName === null)
|
|
192
|
-
return;
|
|
193
|
-
const oneShotAction = oneShotActions.get(clipName);
|
|
194
|
-
if (oneShotAction === undefined)
|
|
195
|
-
return;
|
|
196
|
-
const machine = stateActionsRef.current;
|
|
197
|
-
machine?.actions[machine.active]?.fadeOut(0.1);
|
|
198
|
-
if (active !== null && active.action !== oneShotAction)
|
|
199
|
-
active.action.stop();
|
|
200
|
-
oneShotAction.clampWhenFinished = event === "death";
|
|
201
|
-
oneShotAction.reset().fadeIn(0.1).play();
|
|
202
|
-
activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
return () => {
|
|
206
|
-
if (onOneShotFinished !== null)
|
|
207
|
-
mixer.removeEventListener("finished", onOneShotFinished);
|
|
208
|
-
oneShotPlayRef.current = null;
|
|
209
|
-
activeOneShotRef.current = null;
|
|
210
|
-
mixer.stopAllAction();
|
|
211
|
-
mixerRef.current = null;
|
|
212
|
-
stateActionsRef.current = null;
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
216
|
-
gltf.animations[0];
|
|
217
|
-
const action = mixer.clipAction(clip);
|
|
218
|
-
action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
|
|
219
|
-
action.clampWhenFinished = animation.loop === false;
|
|
220
|
-
action.timeScale = animation.timeScale ?? 1;
|
|
221
|
-
action.enabled = true;
|
|
222
|
-
action.paused = animation.paused === true;
|
|
223
|
-
action.play();
|
|
224
|
-
if (animation.time !== undefined)
|
|
225
|
-
action.time = animation.time;
|
|
226
|
-
mixer.update(0);
|
|
227
|
-
mixerRef.current = mixer;
|
|
228
|
-
animationPausedRef.current = animation.paused === true;
|
|
229
|
-
return () => {
|
|
230
|
-
mixer.stopAllAction();
|
|
231
|
-
mixerRef.current = null;
|
|
232
|
-
};
|
|
233
|
-
}, [
|
|
234
|
-
scene,
|
|
235
|
-
gltf,
|
|
236
|
-
animation?.clip,
|
|
237
|
-
animation?.loop,
|
|
238
|
-
animation?.timeScale,
|
|
239
|
-
animation?.paused,
|
|
240
|
-
animation?.time,
|
|
241
|
-
states,
|
|
242
|
-
oneShots,
|
|
243
|
-
]);
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
if (instanceId === undefined || oneShots === undefined)
|
|
134
|
+
const raw = measureLocalBounds(scene);
|
|
135
|
+
if (raw === null)
|
|
246
136
|
return;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
137
|
+
reportMeasuredBounds(ctx, measureTarget, measureKey, {
|
|
138
|
+
min: [raw.min[0] * scale + positionX, raw.min[1] * scale + positionY, raw.min[2] * scale + positionZ],
|
|
139
|
+
max: [raw.max[0] * scale + positionX, raw.max[1] * scale + positionY, raw.max[2] * scale + positionZ],
|
|
140
|
+
meshCount: raw.meshCount,
|
|
251
141
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
fire("hit");
|
|
255
|
-
});
|
|
256
|
-
const offDied = ctx.game.events.on("entity.died", (event) => {
|
|
257
|
-
if (event.instanceId === instanceId)
|
|
258
|
-
fire("death");
|
|
259
|
-
});
|
|
260
|
-
return () => {
|
|
261
|
-
offAnimation();
|
|
262
|
-
offHit();
|
|
263
|
-
offDied();
|
|
264
|
-
};
|
|
265
|
-
}, [ctx, instanceId, oneShots]);
|
|
142
|
+
}, [ctx, scene, scale, positionX, positionY, positionZ, measureTarget, measureKey, dimsMaxY]);
|
|
143
|
+
useModelAnimation(scene, gltf.animations, model.animation, instanceId);
|
|
266
144
|
const paintCanvasRef = useRef(null);
|
|
267
145
|
const paintDrawnCountRef = useRef(0);
|
|
268
146
|
const paintVersionRef = useRef(-1);
|
|
@@ -276,41 +154,7 @@ export function EntityModel({ model, instanceId }) {
|
|
|
276
154
|
paintVersionRef.current = -1;
|
|
277
155
|
materialCacheRef.current = null;
|
|
278
156
|
}, [scene]);
|
|
279
|
-
useFrame((
|
|
280
|
-
const stateMachine = stateActionsRef.current;
|
|
281
|
-
if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
|
|
282
|
-
const entity = ctx.scene.entity.get(instanceId);
|
|
283
|
-
if (entity !== null) {
|
|
284
|
-
const [x, , z] = entity.position;
|
|
285
|
-
if (stateMachine.lastPos !== null) {
|
|
286
|
-
const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
|
|
287
|
-
stateMachine.smoothedSpeed +=
|
|
288
|
-
(instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
|
|
289
|
-
}
|
|
290
|
-
stateMachine.lastPos = [x, entity.position[1], z];
|
|
291
|
-
const walkSpeed = states.walkSpeed ?? 0.5;
|
|
292
|
-
const runSpeed = states.runSpeed ?? 6;
|
|
293
|
-
const next = stateMachine.smoothedSpeed < walkSpeed
|
|
294
|
-
? "idle"
|
|
295
|
-
: stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
|
|
296
|
-
? "run"
|
|
297
|
-
: "walk";
|
|
298
|
-
if (next !== stateMachine.active) {
|
|
299
|
-
if (activeOneShotRef.current === null) {
|
|
300
|
-
const fade = states.fadeSec ?? 0.2;
|
|
301
|
-
const from = stateMachine.actions[stateMachine.active];
|
|
302
|
-
const to = stateMachine.actions[next];
|
|
303
|
-
if (from !== undefined && to !== undefined) {
|
|
304
|
-
to.reset().fadeIn(fade).play();
|
|
305
|
-
from.fadeOut(fade);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
stateMachine.active = next;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
313
|
-
mixerRef.current.update(delta);
|
|
157
|
+
useFrame(() => {
|
|
314
158
|
if (instanceId === undefined)
|
|
315
159
|
return;
|
|
316
160
|
const paint = ctx.scene.entity.paint;
|
|
@@ -333,5 +177,12 @@ export function EntityModel({ model, instanceId }) {
|
|
|
333
177
|
}
|
|
334
178
|
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
335
179
|
});
|
|
336
|
-
|
|
180
|
+
const base = (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), material?.maps !== undefined ? _jsx(ModelMaterialMapsApplier, { scene: scene, maps: material.maps }) : null, (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`)))] }));
|
|
181
|
+
const parts = model.parts ?? [];
|
|
182
|
+
// Any role-tagged part switches the composition onto the procedural part-motion rig:
|
|
183
|
+
// root bob/flinch/topple plus per-role limb swing, driven from the entity's live state.
|
|
184
|
+
if (parts.some((part) => part.role !== undefined)) {
|
|
185
|
+
return (_jsx(PartMotionRig, { parts: parts, model: model, instanceId: instanceId, renderPart: (part) => (typeof part.model === "string" ? null : _jsx(EntityModel, { model: part.model })), children: base }));
|
|
186
|
+
}
|
|
187
|
+
return (_jsxs(_Fragment, { children: [base, parts.map((part, index) => typeof part.model === "string" ? null : (_jsx(ModelPartGroup, { model: part.model, position: part.position, rotation: part.rotation, scale: part.scale }, index)))] }));
|
|
337
188
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
|
|
3
|
+
* installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
|
|
4
|
+
* `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
|
|
5
|
+
* as the URL modifier on THREE loading managers so every model and texture
|
|
6
|
+
* load resolves correctly wherever the app is mounted.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveAssetBaseUrl(url: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
|
|
11
|
+
* asset paths load from under it. Call once at app startup, before any game
|
|
12
|
+
* loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
|
|
13
|
+
* default. Also registers {@link resolveAssetBaseUrl} on
|
|
14
|
+
* `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
|
|
15
|
+
* / `useGLTF`, and every other loader on the default manager.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installAssetBase(base: string): void;
|