@jgengine/shell 0.9.0 → 0.11.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 +34 -3
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayerShell.d.ts +4 -31
- package/dist/GamePlayerShell.js +162 -932
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +63 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +2 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
- package/dist/camera/GameFirstPersonCamera.js +29 -7
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +13 -2
- package/dist/camera/GameOrbitCamera.js +59 -10
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +54 -6
- package/dist/camera/orbitCameraMath.js +61 -6
- package/dist/camera/rigMath.d.ts +94 -24
- package/dist/camera/rigMath.js +100 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/cartridge.d.ts +1 -1
- package/dist/cartridge.js +4 -3
- package/dist/commandSink.d.ts +25 -0
- package/dist/commandSink.js +32 -0
- package/dist/defineGame.js +26 -5
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/CollisionDebugWorld.js +1 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
- package/dist/devtools/DevtoolsOverlay.js +19 -450
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +262 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +9 -2
- package/dist/devtools/collisionDebugMath.js +25 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +29 -7
- package/dist/environment/Daylight.js +153 -13
- package/dist/environment/EnvironmentScene.js +42 -7
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +30 -0
- package/dist/inputSink.js +85 -0
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +14 -4
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +120 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +72 -0
- package/dist/registry.d.ts +3 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +73 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +39 -3
- package/dist/render/resolveModel.js +79 -2
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +130 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +94 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +82 -0
- package/dist/scene/AuthoredScene.js +173 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +25 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +21 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +37 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +22 -0
- package/dist/scene/sceneKindRenderers.d.ts +28 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +5 -0
- package/dist/settings/appliedSettings.js +9 -0
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +43 -0
- package/dist/structures/TransformGizmo.js +55 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +4 -1
- package/dist/terrain/CarvedTerrain.js +4 -3
- package/dist/terrain/GrassField.js +1 -1
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +3 -0
- package/dist/terrain/grassMaterial.d.ts +2 -0
- package/dist/terrain/grassMaterial.js +2 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +10 -0
- package/dist/terrain/soilPatchMaterial.js +109 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
- package/dist/terrain/terrainDetailMaterial.js +157 -0
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +211 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/OceanConfig.d.ts +2 -0
- package/dist/water/OceanConfig.js +2 -0
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +52 -3
- package/dist/world/WorldHud.js +94 -9
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +159 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +26 -1
- package/dist/world/worldBarSamples.js +45 -1
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +11 -0
- package/dist/worldSync.js +20 -0
- package/package.json +6 -6
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +38 -0
- package/skills/jgengine-world/api.md +2652 -0
- package/skills/jgengine-world/capabilities.md +370 -0
- package/skills/jgengine-world/reference.md +222 -0
- package/llms.txt +0 -2643
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
2
2
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
3
|
export interface ModelResolveContext {
|
|
4
|
-
seam: "entityModels" | "objectModels";
|
|
4
|
+
seam: "entityModels" | "objectModels" | "scatterModels";
|
|
5
5
|
key: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
|
|
9
9
|
* ids throw — silent generic-primitive fallback only happens when the mapping
|
|
10
10
|
* omits the key entirely (or uses tryResolveCatalogModel for optional ids).
|
|
11
|
-
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
12
13
|
export declare function resolveModel(value: string | ModelConfig | undefined, assets: AssetCatalog, context?: ModelResolveContext): ModelConfig | undefined;
|
|
13
|
-
/** Soft lookup used when an object catalog id may double as a model asset id.
|
|
14
|
+
/** Soft lookup used when an object catalog id may double as a model asset id.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
14
17
|
export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
|
|
20
|
+
* so mounts receive fully-resolved `ModelConfig`s.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveEntityModel(value: string | ModelConfig | undefined, assets: AssetCatalog, key: string): ModelConfig | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Preferred + optional fallback catalog ids for a single entity/object slot.
|
|
26
|
+
* Soft-resolves through the catalog: when neither id is live (pack not pulled/
|
|
27
|
+
* reindexed yet), the mapping is omitted and the shell keeps its primitive.
|
|
28
|
+
* Re-home later by fixing ids / pulling packs — no Kenney, no hard throws.
|
|
29
|
+
*/
|
|
30
|
+
export type ModelPick = {
|
|
31
|
+
model?: string;
|
|
32
|
+
fallbackModel?: string;
|
|
33
|
+
style?: Omit<ModelConfig, "url" | "dims">;
|
|
34
|
+
};
|
|
35
|
+
/** Soft-resolve `model`, then `fallbackModel`; merge `style` when either hits.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickModel(assets: AssetCatalog, pick: ModelPick): ModelConfig | undefined;
|
|
39
|
+
/** Build an entityModels/objectModels map from a plan; missing catalog ids drop out (primitive).
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<string, ModelPick>): Record<string, ModelConfig>;
|
|
43
|
+
/**
|
|
44
|
+
* Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
|
|
45
|
+
* `undefined` when either half is missing so the caller can fall back to its own default renderer.
|
|
46
|
+
* An unmapped key resolves to `null` (deliberate fallback); a mapped key with a missing/misspelled
|
|
47
|
+
* catalog id throws via {@link resolveModel}, same as the `entityModels`/`objectModels` seams.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export declare function createModelMapResolver(map: Record<string, string | ModelConfig> | undefined, assets: AssetCatalog | undefined, seam: ModelResolveContext["seam"]): ((key: string) => ModelConfig | null) | undefined;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
|
|
3
3
|
* ids throw — silent generic-primitive fallback only happens when the mapping
|
|
4
4
|
* omits the key entirely (or uses tryResolveCatalogModel for optional ids).
|
|
5
|
-
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
6
7
|
export function resolveModel(value, assets, context) {
|
|
7
8
|
if (value === undefined)
|
|
8
9
|
return undefined;
|
|
@@ -15,10 +16,86 @@ export function resolveModel(value, assets, context) {
|
|
|
15
16
|
}
|
|
16
17
|
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
17
18
|
}
|
|
18
|
-
/** Soft lookup used when an object catalog id may double as a model asset id.
|
|
19
|
+
/** Soft lookup used when an object catalog id may double as a model asset id.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
19
22
|
export function tryResolveCatalogModel(id, assets) {
|
|
20
23
|
const ref = assets.resolve(id);
|
|
21
24
|
if (ref === null)
|
|
22
25
|
return undefined;
|
|
23
26
|
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
|
|
30
|
+
* so mounts receive fully-resolved `ModelConfig`s.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export function resolveEntityModel(value, assets, key) {
|
|
34
|
+
const model = resolveModel(value, assets, { seam: "entityModels", key });
|
|
35
|
+
if (model === undefined)
|
|
36
|
+
return model;
|
|
37
|
+
if (model.attachments === undefined && model.parts === undefined)
|
|
38
|
+
return model;
|
|
39
|
+
return {
|
|
40
|
+
...model,
|
|
41
|
+
...(model.attachments === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: {
|
|
44
|
+
attachments: model.attachments.map((attachment) => ({
|
|
45
|
+
...attachment,
|
|
46
|
+
model: resolveModel(attachment.model, assets) ?? attachment.model,
|
|
47
|
+
})),
|
|
48
|
+
}),
|
|
49
|
+
...(model.parts === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: {
|
|
52
|
+
parts: model.parts.map((part) => ({
|
|
53
|
+
...part,
|
|
54
|
+
model: resolveModel(part.model, assets) ?? part.model,
|
|
55
|
+
})),
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Soft-resolve `model`, then `fallbackModel`; merge `style` when either hits.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export function pickModel(assets, pick) {
|
|
63
|
+
for (const id of [pick.model, pick.fallbackModel]) {
|
|
64
|
+
if (id === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
const resolved = tryResolveCatalogModel(id, assets);
|
|
67
|
+
if (resolved === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
return pick.style === undefined ? resolved : { ...resolved, ...pick.style };
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Build an entityModels/objectModels map from a plan; missing catalog ids drop out (primitive).
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export function resolveModelPlan(assets, plan) {
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [key, pick] of Object.entries(plan)) {
|
|
79
|
+
const model = pickModel(assets, pick);
|
|
80
|
+
if (model !== undefined)
|
|
81
|
+
out[key] = model;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
|
|
87
|
+
* `undefined` when either half is missing so the caller can fall back to its own default renderer.
|
|
88
|
+
* An unmapped key resolves to `null` (deliberate fallback); a mapped key with a missing/misspelled
|
|
89
|
+
* catalog id throws via {@link resolveModel}, same as the `entityModels`/`objectModels` seams.
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function createModelMapResolver(map, assets, seam) {
|
|
93
|
+
if (map === undefined || assets === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
return (key) => {
|
|
96
|
+
const mapped = map[key];
|
|
97
|
+
if (mapped === undefined)
|
|
98
|
+
return null;
|
|
99
|
+
return resolveModel(mapped, assets, { seam, key }) ?? null;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import { type EntityRenderCues, type RenderCueTuning } from "@jgengine/core/combat/renderCues";
|
|
3
|
+
/**
|
|
4
|
+
* Live motion + animation cues for one entity, read from a mutable ref inside
|
|
5
|
+
* your own `useFrame` — no re-render per frame, no diffing the parent group's
|
|
6
|
+
* position, no game-side module map for attack/hit timing. Backs both custom
|
|
7
|
+
* `renderEntity` rigs and a custom first-person viewmodel (#542): call it with
|
|
8
|
+
* `entity.id` / the local player's userId and drive bob/recoil/reload poses
|
|
9
|
+
* from `cuesRef.current` inside the calling component's own `useFrame`.
|
|
10
|
+
*
|
|
11
|
+
* @capability entity-render-cues live velocity/bob/fire/reload/hit/death cues for a custom renderEntity or viewmodel component
|
|
12
|
+
*/
|
|
13
|
+
export declare function useEntityRenderCues(instanceId: string | undefined, tuning?: RenderCueTuning): MutableRefObject<EntityRenderCues>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useFrame } from "@react-three/fiber";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { advanceMotionCues, applyRenderAnimationEvent, applyRenderDeathEvent, applyRenderHitEvent, DEFAULT_FIRE_PULSE_SECONDS, DEFAULT_HIT_PULSE_SECONDS, DEFAULT_RENDER_CUES, } from "@jgengine/core/combat/renderCues";
|
|
4
|
+
import { groundSpeed } from "@jgengine/core/scene/entityStore";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
/**
|
|
7
|
+
* Live motion + animation cues for one entity, read from a mutable ref inside
|
|
8
|
+
* your own `useFrame` — no re-render per frame, no diffing the parent group's
|
|
9
|
+
* position, no game-side module map for attack/hit timing. Backs both custom
|
|
10
|
+
* `renderEntity` rigs and a custom first-person viewmodel (#542): call it with
|
|
11
|
+
* `entity.id` / the local player's userId and drive bob/recoil/reload poses
|
|
12
|
+
* from `cuesRef.current` inside the calling component's own `useFrame`.
|
|
13
|
+
*
|
|
14
|
+
* @capability entity-render-cues live velocity/bob/fire/reload/hit/death cues for a custom renderEntity or viewmodel component
|
|
15
|
+
*/
|
|
16
|
+
export function useEntityRenderCues(instanceId, tuning) {
|
|
17
|
+
const ctx = useGameContext();
|
|
18
|
+
const cuesRef = useRef({ ...DEFAULT_RENDER_CUES });
|
|
19
|
+
const timersRef = useRef({ fire: 0, hit: 0 });
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
cuesRef.current = { ...DEFAULT_RENDER_CUES };
|
|
22
|
+
timersRef.current = { fire: 0, hit: 0 };
|
|
23
|
+
if (instanceId === undefined)
|
|
24
|
+
return;
|
|
25
|
+
const offAnimation = ctx.game.events.on("entity.animation", (event) => {
|
|
26
|
+
if (event.instanceId !== instanceId)
|
|
27
|
+
return;
|
|
28
|
+
cuesRef.current = applyRenderAnimationEvent(cuesRef.current, event.event);
|
|
29
|
+
if (event.event === "fire")
|
|
30
|
+
timersRef.current.fire = tuning?.firePulseSeconds ?? DEFAULT_FIRE_PULSE_SECONDS;
|
|
31
|
+
});
|
|
32
|
+
const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
|
|
33
|
+
if (event.instanceId !== instanceId)
|
|
34
|
+
return;
|
|
35
|
+
cuesRef.current = applyRenderHitEvent(cuesRef.current);
|
|
36
|
+
timersRef.current.hit = tuning?.hitPulseSeconds ?? DEFAULT_HIT_PULSE_SECONDS;
|
|
37
|
+
});
|
|
38
|
+
const offDied = ctx.game.events.on("entity.died", (event) => {
|
|
39
|
+
if (event.instanceId !== instanceId)
|
|
40
|
+
return;
|
|
41
|
+
cuesRef.current = applyRenderDeathEvent(cuesRef.current);
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
offAnimation();
|
|
45
|
+
offHit();
|
|
46
|
+
offDied();
|
|
47
|
+
};
|
|
48
|
+
}, [ctx, instanceId, tuning?.firePulseSeconds, tuning?.hitPulseSeconds]);
|
|
49
|
+
useFrame((_state, delta) => {
|
|
50
|
+
if (instanceId === undefined || delta <= 0)
|
|
51
|
+
return;
|
|
52
|
+
const entity = ctx.scene.entity.get(instanceId);
|
|
53
|
+
if (entity === null)
|
|
54
|
+
return;
|
|
55
|
+
const timers = timersRef.current;
|
|
56
|
+
timers.fire = Math.max(0, timers.fire - delta);
|
|
57
|
+
timers.hit = Math.max(0, timers.hit - delta);
|
|
58
|
+
const advanced = advanceMotionCues(cuesRef.current, groundSpeed(entity), delta, tuning);
|
|
59
|
+
cuesRef.current = { ...advanced, firing: timers.fire > 0, hit: timers.hit > 0 };
|
|
60
|
+
});
|
|
61
|
+
return cuesRef;
|
|
62
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
import { type ScatterInstance } from "@jgengine/core/world/scatterRegion";
|
|
3
|
+
/** Props for {@link InstancedScatter}: the resolved instances plus chunking/instance-cap knobs. */
|
|
4
|
+
export interface InstancedScatterProps {
|
|
5
|
+
instances: readonly ScatterInstance[];
|
|
6
|
+
/** Chunk edge in meters; dense fields split into independently frustum-culled buckets. Default 24. */
|
|
7
|
+
chunkSize?: number;
|
|
8
|
+
/** Hard cap on rendered instances. Default 20000. */
|
|
9
|
+
maxInstances?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Per-species override: return a real catalog `ModelConfig` for an item to GPU-instance the actual
|
|
12
|
+
* GLB instead of the stylized proxy; return `null` (or omit the prop) to keep `buildScatterProxy`.
|
|
13
|
+
*/
|
|
14
|
+
resolveItem?: (item: string) => ModelConfig | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Renders resolved {@link ScatterInstance}s grouped into per-chunk, per-species GPU-instanced draws
|
|
18
|
+
* that frustum-cull independently. Species with no `resolveItem` mapping (or a `null` result) render
|
|
19
|
+
* as stylized proxy models (trunked trees, stacked pines, round bushes, faceted rocks, grass tufts);
|
|
20
|
+
* species `resolveItem` resolves to a `ModelConfig` GPU-instance the real catalog GLB instead. The one
|
|
21
|
+
* runtime foliage renderer shared by the editor's scatter preview and games that consume
|
|
22
|
+
* `resolveScatter` — never one node per placement.
|
|
23
|
+
*/
|
|
24
|
+
export declare function InstancedScatter({ instances, chunkSize, maxInstances, resolveItem, }: InstancedScatterProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useLoader } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
|
+
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
7
|
+
import { chunkScatterInstances } from "@jgengine/core/world/scatterRegion";
|
|
8
|
+
import { buildScatterProxy } from "./scatterProxies.js";
|
|
9
|
+
import { buildScatterModelSources, disposeScatterModelSources } from "./scatterModels.js";
|
|
10
|
+
/** Chunk edge (m): dense fields split into this-sized buckets, each frustum-culled independently. */
|
|
11
|
+
const DEFAULT_CHUNK_SIZE = 24;
|
|
12
|
+
const DEFAULT_MAX_INSTANCES = 20000;
|
|
13
|
+
/** Groups resolved instances by spatial chunk then species so each batch is one cullable draw. */
|
|
14
|
+
function batchInstances(instances, chunkSize) {
|
|
15
|
+
const batches = [];
|
|
16
|
+
const matrix = new THREE.Matrix4();
|
|
17
|
+
const position = new THREE.Vector3();
|
|
18
|
+
const quaternion = new THREE.Quaternion();
|
|
19
|
+
const euler = new THREE.Euler();
|
|
20
|
+
const scale = new THREE.Vector3();
|
|
21
|
+
for (const chunk of chunkScatterInstances(instances, chunkSize)) {
|
|
22
|
+
const byItem = new Map();
|
|
23
|
+
for (const instance of chunk.instances) {
|
|
24
|
+
position.set(instance.x, instance.y, instance.z);
|
|
25
|
+
euler.set(0, instance.rotationY, 0);
|
|
26
|
+
quaternion.setFromEuler(euler);
|
|
27
|
+
scale.set(instance.scale, instance.scale, instance.scale);
|
|
28
|
+
matrix.compose(position, quaternion, scale);
|
|
29
|
+
const bucket = byItem.get(instance.item);
|
|
30
|
+
if (bucket === undefined)
|
|
31
|
+
byItem.set(instance.item, [matrix.clone()]);
|
|
32
|
+
else
|
|
33
|
+
bucket.push(matrix.clone());
|
|
34
|
+
}
|
|
35
|
+
for (const [item, matrices] of byItem)
|
|
36
|
+
batches.push({ key: `${chunk.key}:${item}`, item, matrices });
|
|
37
|
+
}
|
|
38
|
+
return batches;
|
|
39
|
+
}
|
|
40
|
+
/** One species-in-a-chunk InstancedMesh; frustum-culled as a unit so offscreen chunks cost nothing. */
|
|
41
|
+
function ScatterBatchMesh({ batch, geometry }) {
|
|
42
|
+
const meshRef = useRef(null);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const mesh = meshRef.current;
|
|
45
|
+
if (mesh === null)
|
|
46
|
+
return;
|
|
47
|
+
for (let i = 0; i < batch.matrices.length; i += 1)
|
|
48
|
+
mesh.setMatrixAt(i, batch.matrices[i]);
|
|
49
|
+
mesh.count = batch.matrices.length;
|
|
50
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
51
|
+
mesh.computeBoundingSphere();
|
|
52
|
+
}, [batch]);
|
|
53
|
+
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, undefined, batch.matrices.length], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 0.85, metalness: 0 }) }));
|
|
54
|
+
}
|
|
55
|
+
/** One GLB-backed draw source instanced across every chunk of one species. */
|
|
56
|
+
function ScatterModelSourceInstances({ source, matrices }) {
|
|
57
|
+
const meshRef = useRef(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const mesh = meshRef.current;
|
|
60
|
+
if (mesh === null)
|
|
61
|
+
return;
|
|
62
|
+
const composed = new THREE.Matrix4();
|
|
63
|
+
for (let i = 0; i < matrices.length; i += 1) {
|
|
64
|
+
composed.multiplyMatrices(matrices[i], source.localMatrix);
|
|
65
|
+
mesh.setMatrixAt(i, composed);
|
|
66
|
+
}
|
|
67
|
+
mesh.count = matrices.length;
|
|
68
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
69
|
+
mesh.computeBoundingSphere();
|
|
70
|
+
}, [matrices, source]);
|
|
71
|
+
return _jsx("instancedMesh", { ref: meshRef, args: [source.geometry, source.material, matrices.length], castShadow: true, receiveShadow: true });
|
|
72
|
+
}
|
|
73
|
+
/** Loads one species' real catalog GLB once, harvests its instanceable draw sources, then GPU-instances them across every chunk that species appears in. */
|
|
74
|
+
function ScatterModelSpecies({ model, batches }) {
|
|
75
|
+
const gltf = useLoader(GLTFLoader, model.url, (loader) => {
|
|
76
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
77
|
+
});
|
|
78
|
+
const { sources, root } = useMemo(() => buildScatterModelSources(gltf.scene, model), [gltf, model]);
|
|
79
|
+
useEffect(() => () => disposeScatterModelSources(root), [root]);
|
|
80
|
+
return (_jsx(_Fragment, { children: sources.map((source, sourceIndex) => batches.map((batch) => (_jsx(ScatterModelSourceInstances, { source: source, matrices: batch.matrices }, `${batch.key}:${sourceIndex}`)))) }));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Renders resolved {@link ScatterInstance}s grouped into per-chunk, per-species GPU-instanced draws
|
|
84
|
+
* that frustum-cull independently. Species with no `resolveItem` mapping (or a `null` result) render
|
|
85
|
+
* as stylized proxy models (trunked trees, stacked pines, round bushes, faceted rocks, grass tufts);
|
|
86
|
+
* species `resolveItem` resolves to a `ModelConfig` GPU-instance the real catalog GLB instead. The one
|
|
87
|
+
* runtime foliage renderer shared by the editor's scatter preview and games that consume
|
|
88
|
+
* `resolveScatter` — never one node per placement.
|
|
89
|
+
*/
|
|
90
|
+
export function InstancedScatter({ instances, chunkSize = DEFAULT_CHUNK_SIZE, maxInstances = DEFAULT_MAX_INSTANCES, resolveItem, }) {
|
|
91
|
+
const batches = useMemo(() => batchInstances(instances.length > maxInstances ? instances.slice(0, maxInstances) : instances, chunkSize), [instances, chunkSize, maxInstances]);
|
|
92
|
+
const { proxyBatches, modelSpecies } = useMemo(() => {
|
|
93
|
+
const proxy = [];
|
|
94
|
+
const bySpecies = new Map();
|
|
95
|
+
const knownProxy = new Set();
|
|
96
|
+
for (const batch of batches) {
|
|
97
|
+
const species = bySpecies.get(batch.item);
|
|
98
|
+
if (species !== undefined) {
|
|
99
|
+
species.batches.push(batch);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (knownProxy.has(batch.item)) {
|
|
103
|
+
proxy.push(batch);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const resolved = resolveItem?.(batch.item) ?? null;
|
|
107
|
+
if (resolved === null) {
|
|
108
|
+
knownProxy.add(batch.item);
|
|
109
|
+
proxy.push(batch);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
bySpecies.set(batch.item, { model: resolved, batches: [batch] });
|
|
113
|
+
}
|
|
114
|
+
return { proxyBatches: proxy, modelSpecies: [...bySpecies.entries()] };
|
|
115
|
+
}, [batches, resolveItem]);
|
|
116
|
+
const geometries = useMemo(() => {
|
|
117
|
+
const items = new Set(proxyBatches.map((batch) => batch.item));
|
|
118
|
+
const map = new Map();
|
|
119
|
+
for (const item of items)
|
|
120
|
+
map.set(item, buildScatterProxy(item));
|
|
121
|
+
return map;
|
|
122
|
+
}, [proxyBatches]);
|
|
123
|
+
useEffect(() => () => geometries.forEach((geometry) => geometry.dispose()), [geometries]);
|
|
124
|
+
if (batches.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
return (_jsxs(_Fragment, { children: [proxyBatches.map((batch) => {
|
|
127
|
+
const geometry = geometries.get(batch.item);
|
|
128
|
+
return geometry === undefined ? null : _jsx(ScatterBatchMesh, { batch: batch, geometry: geometry }, batch.key);
|
|
129
|
+
}), modelSpecies.map(([item, species]) => (_jsx(Suspense, { fallback: null, children: _jsx(ScatterModelSpecies, { model: species.model, batches: species.batches }) }, item)))] }));
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/** One instanceable draw source harvested from a resolved GLB — geometry stays shared with the loader cache, `localMatrix` bakes the model's own transform (from its scene root) plus scale/anchor normalization. */
|
|
4
|
+
export interface ScatterModelSource {
|
|
5
|
+
geometry: THREE.BufferGeometry;
|
|
6
|
+
material: THREE.Material | THREE.Material[];
|
|
7
|
+
localMatrix: THREE.Matrix4;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Clones a loaded GLTF scene (so shared cache materials stay untouched), applies the model's material
|
|
11
|
+
* override, then harvests every mesh as a {@link ScatterModelSource} — one `InstancedMesh` draw per
|
|
12
|
+
* source, matrices composed per-instance at render time. Geometry is left unowned (still the loader
|
|
13
|
+
* cache's), only the cloned materials need disposal via {@link disposeScatterModelSources}.
|
|
14
|
+
* @internal — the GLB-harvesting behind `InstancedScatter`'s `resolveItem` override.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildScatterModelSources(gltfScene: THREE.Object3D, model: ModelConfig): {
|
|
17
|
+
sources: ScatterModelSource[];
|
|
18
|
+
root: THREE.Object3D;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Disposes the cloned materials harvested by {@link buildScatterModelSources}; never disposes geometry
|
|
22
|
+
* (still owned by the loader cache).
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function disposeScatterModelSources(root: THREE.Object3D): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { applyMaterialOverride } from "../materialOverride.js";
|
|
3
|
+
import { cloneModelScene, disposeClonedMaterials } from "../render/modelRender.js";
|
|
4
|
+
/** The scale + position offset a `ModelConfig` applies before any per-instance transform — same anchor/targetHeight rules as the single-instance entity renderer. */
|
|
5
|
+
function scatterModelBaseTransform(scene, model) {
|
|
6
|
+
let scale = model.scale ?? 1;
|
|
7
|
+
let minY = 0;
|
|
8
|
+
let centerX = 0;
|
|
9
|
+
let centerZ = 0;
|
|
10
|
+
if (model.targetHeight !== undefined) {
|
|
11
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
12
|
+
const height = box.max.y - box.min.y;
|
|
13
|
+
if (Number.isFinite(height) && height > 0) {
|
|
14
|
+
scale *= model.targetHeight / height;
|
|
15
|
+
minY = box.min.y;
|
|
16
|
+
centerX = (box.min.x + box.max.x) / 2;
|
|
17
|
+
centerZ = (box.min.z + box.max.z) / 2;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if ((model.anchor ?? "center") === "center" && model.dims !== undefined) {
|
|
21
|
+
minY = model.dims.minY;
|
|
22
|
+
centerX = model.dims.center.x;
|
|
23
|
+
centerZ = model.dims.center.z;
|
|
24
|
+
}
|
|
25
|
+
const baseY = model.y ?? 0;
|
|
26
|
+
return new THREE.Matrix4().compose(new THREE.Vector3(-scale * centerX, baseY - scale * minY, -scale * centerZ), new THREE.Quaternion(), new THREE.Vector3(scale, scale, scale));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Clones a loaded GLTF scene (so shared cache materials stay untouched), applies the model's material
|
|
30
|
+
* override, then harvests every mesh as a {@link ScatterModelSource} — one `InstancedMesh` draw per
|
|
31
|
+
* source, matrices composed per-instance at render time. Geometry is left unowned (still the loader
|
|
32
|
+
* cache's), only the cloned materials need disposal via {@link disposeScatterModelSources}.
|
|
33
|
+
* @internal — the GLB-harvesting behind `InstancedScatter`'s `resolveItem` override.
|
|
34
|
+
*/
|
|
35
|
+
export function buildScatterModelSources(gltfScene, model) {
|
|
36
|
+
const root = cloneModelScene(gltfScene);
|
|
37
|
+
if (model.material !== undefined)
|
|
38
|
+
applyMaterialOverride(root, model.material, { clone: false });
|
|
39
|
+
root.updateMatrixWorld(true);
|
|
40
|
+
const base = scatterModelBaseTransform(root, model);
|
|
41
|
+
const sources = [];
|
|
42
|
+
root.traverse((node) => {
|
|
43
|
+
const mesh = node;
|
|
44
|
+
if (!mesh.isMesh)
|
|
45
|
+
return;
|
|
46
|
+
sources.push({
|
|
47
|
+
geometry: mesh.geometry,
|
|
48
|
+
material: mesh.material,
|
|
49
|
+
localMatrix: base.clone().multiply(mesh.matrixWorld),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return { sources, root };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Disposes the cloned materials harvested by {@link buildScatterModelSources}; never disposes geometry
|
|
56
|
+
* (still owned by the loader cache).
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export function disposeScatterModelSources(root) {
|
|
60
|
+
disposeClonedMaterials(root);
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the merged proxy geometry for a scatter item id, memoized per builder call by the caller.
|
|
4
|
+
* @internal — the geometry catalog behind the `InstancedScatter` renderer.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildScatterProxy(item: string): THREE.BufferGeometry;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const BARK = "#5b4127";
|
|
3
|
+
const LEAF = "#3f6b30";
|
|
4
|
+
const PINE = "#2f5330";
|
|
5
|
+
const OAK = "#4a7a37";
|
|
6
|
+
const BUSH = "#4d7d3a";
|
|
7
|
+
const ROCK = "#82858c";
|
|
8
|
+
const GRASS = "#5f9a3c";
|
|
9
|
+
/** Concatenates part geometries into one vertex-colored buffer (position + normal + color). */
|
|
10
|
+
function mergeParts(parts) {
|
|
11
|
+
const geos = parts.map((part) => (part.geometry.index === null ? part.geometry : part.geometry.toNonIndexed()));
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const geo of geos)
|
|
14
|
+
total += geo.attributes.position.count;
|
|
15
|
+
const position = new Float32Array(total * 3);
|
|
16
|
+
const normal = new Float32Array(total * 3);
|
|
17
|
+
const color = new Float32Array(total * 3);
|
|
18
|
+
const tint = new THREE.Color();
|
|
19
|
+
let offset = 0;
|
|
20
|
+
for (let i = 0; i < geos.length; i += 1) {
|
|
21
|
+
const geo = geos[i];
|
|
22
|
+
const pos = geo.attributes.position;
|
|
23
|
+
const nor = geo.attributes.normal;
|
|
24
|
+
position.set(pos.array, offset * 3);
|
|
25
|
+
normal.set(nor.array, offset * 3);
|
|
26
|
+
tint.set(parts[i].color);
|
|
27
|
+
for (let v = 0; v < pos.count; v += 1) {
|
|
28
|
+
color[(offset + v) * 3] = tint.r;
|
|
29
|
+
color[(offset + v) * 3 + 1] = tint.g;
|
|
30
|
+
color[(offset + v) * 3 + 2] = tint.b;
|
|
31
|
+
}
|
|
32
|
+
offset += pos.count;
|
|
33
|
+
}
|
|
34
|
+
for (const geo of geos)
|
|
35
|
+
geo.dispose();
|
|
36
|
+
const merged = new THREE.BufferGeometry();
|
|
37
|
+
merged.setAttribute("position", new THREE.BufferAttribute(position, 3));
|
|
38
|
+
merged.setAttribute("normal", new THREE.BufferAttribute(normal, 3));
|
|
39
|
+
merged.setAttribute("color", new THREE.BufferAttribute(color, 3));
|
|
40
|
+
merged.computeBoundingSphere();
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
function trunk(height, radius) {
|
|
44
|
+
const geo = new THREE.CylinderGeometry(radius * 0.8, radius, height, 5);
|
|
45
|
+
geo.translate(0, height / 2, 0);
|
|
46
|
+
return geo;
|
|
47
|
+
}
|
|
48
|
+
function cone(radius, height, y, segments = 7) {
|
|
49
|
+
const geo = new THREE.ConeGeometry(radius, height, segments);
|
|
50
|
+
geo.translate(0, y + height / 2, 0);
|
|
51
|
+
return geo;
|
|
52
|
+
}
|
|
53
|
+
function sphere(radius, y, squash = 1) {
|
|
54
|
+
const geo = new THREE.IcosahedronGeometry(radius, 1);
|
|
55
|
+
geo.scale(1, squash, 1);
|
|
56
|
+
geo.translate(0, y, 0);
|
|
57
|
+
return geo;
|
|
58
|
+
}
|
|
59
|
+
const BUILDERS = {
|
|
60
|
+
tree: () => mergeParts([
|
|
61
|
+
{ geometry: trunk(1.1, 0.12), color: BARK },
|
|
62
|
+
{ geometry: cone(0.8, 1.7, 0.9), color: LEAF },
|
|
63
|
+
]),
|
|
64
|
+
pine: () => mergeParts([
|
|
65
|
+
{ geometry: trunk(0.9, 0.1), color: BARK },
|
|
66
|
+
{ geometry: cone(0.7, 1.0, 0.7), color: PINE },
|
|
67
|
+
{ geometry: cone(0.55, 0.9, 1.35), color: PINE },
|
|
68
|
+
{ geometry: cone(0.38, 0.8, 1.95), color: PINE },
|
|
69
|
+
]),
|
|
70
|
+
oak: () => mergeParts([
|
|
71
|
+
{ geometry: trunk(1.0, 0.14), color: BARK },
|
|
72
|
+
{ geometry: sphere(0.85, 1.55, 0.85), color: OAK },
|
|
73
|
+
]),
|
|
74
|
+
bush: () => mergeParts([{ geometry: sphere(0.55, 0.45, 0.8), color: BUSH }]),
|
|
75
|
+
shrub: () => mergeParts([{ geometry: sphere(0.5, 0.4, 0.75), color: BUSH }]),
|
|
76
|
+
rock: () => mergeParts([{ geometry: sphere(0.5, 0.28, 0.7), color: ROCK }]),
|
|
77
|
+
stone: () => mergeParts([{ geometry: sphere(0.4, 0.22, 0.65), color: ROCK }]),
|
|
78
|
+
grass: () => mergeParts([
|
|
79
|
+
{ geometry: cone(0.09, 0.7, 0), color: GRASS },
|
|
80
|
+
{ geometry: cone(0.08, 0.55, 0).translate(0.12, 0, 0.05), color: GRASS },
|
|
81
|
+
{ geometry: cone(0.08, 0.6, 0).translate(-0.1, 0, -0.06), color: GRASS },
|
|
82
|
+
]),
|
|
83
|
+
};
|
|
84
|
+
function defaultProxy() {
|
|
85
|
+
return mergeParts([{ geometry: cone(0.35, 1.2, 0), color: GRASS }]);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Builds the merged proxy geometry for a scatter item id, memoized per builder call by the caller.
|
|
89
|
+
* @internal — the geometry catalog behind the `InstancedScatter` renderer.
|
|
90
|
+
*/
|
|
91
|
+
export function buildScatterProxy(item) {
|
|
92
|
+
const builder = BUILDERS[item];
|
|
93
|
+
return builder === undefined ? defaultProxy() : builder();
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./scatter/InstancedScatter.js";
|
package/dist/scatter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./scatter/InstancedScatter.js";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { EditorDocument } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
4
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
|
|
7
|
+
* installed (editor host live), subscribe to document patches; otherwise use the prop.
|
|
8
|
+
* Document is authoritative — runtime overrides never land here unless written back.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function useLiveEditorDocument(document: EditorDocument, live?: boolean): EditorDocument;
|
|
12
|
+
/** Props for {@link AuthoredPaths}: the document, the ground field to drape over, and a kind filter. */
|
|
13
|
+
export interface AuthoredPathsProps {
|
|
14
|
+
document: EditorDocument;
|
|
15
|
+
field: TerrainField;
|
|
16
|
+
/** Only render these path kinds; default renders every non-scatter path. */
|
|
17
|
+
kinds?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the
|
|
21
|
+
* editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes
|
|
22
|
+
* from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): import("react").JSX.Element;
|
|
25
|
+
/** Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. */
|
|
26
|
+
export interface AuthoredObjectsProps {
|
|
27
|
+
document: EditorDocument;
|
|
28
|
+
field: TerrainField;
|
|
29
|
+
/** Extra lift in meters on top of each marker's own verticalOffset (default 0). */
|
|
30
|
+
verticalOffset?: number;
|
|
31
|
+
/**
|
|
32
|
+
* How to treat instance ids already in the object store. Default `"keep"` so an onInit
|
|
33
|
+
* `placeAuthoredObjects` call wins and remounts stay remount-safe.
|
|
34
|
+
*/
|
|
35
|
+
onExisting?: "throw" | "replace" | "keep";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
|
|
39
|
+
* WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
|
|
40
|
+
* headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function AuthoredObjects({ document, field, verticalOffset, onExisting, }: AuthoredObjectsProps): null;
|
|
43
|
+
/** Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. */
|
|
44
|
+
export interface AuthoredSceneProps {
|
|
45
|
+
document: EditorDocument;
|
|
46
|
+
field: TerrainField;
|
|
47
|
+
/** Restrict rendered path kinds; default renders every non-scatter path. */
|
|
48
|
+
pathKinds?: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Scatter palette item id → model asset id or `ModelConfig`; a mapped item GPU-instances the real
|
|
51
|
+
* catalog GLB instead of the stylized proxy. Keyed by scatter `item` (not entity name — pass a
|
|
52
|
+
* dedicated map even if it shares entries with `entityModels`). Requires `assets`.
|
|
53
|
+
*/
|
|
54
|
+
scatterModels?: Record<string, string | ModelConfig>;
|
|
55
|
+
/** Catalog `scatterModels` string ids resolve through; required together with `scatterModels`. */
|
|
56
|
+
assets?: AssetCatalog;
|
|
57
|
+
/**
|
|
58
|
+
* When true (default), subscribe to the global document live-sync bus if the editor host is
|
|
59
|
+
* mounted so headless/RPC document patches hot-apply without a full reload. Pass false to pin
|
|
60
|
+
* the `document` prop (tests, one-shot previews).
|
|
61
|
+
*/
|
|
62
|
+
live?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Place the document's catalog-id markers into the object
|
|
65
|
+
* store — WorldScene renders them via the game's `objectModels` seam. Omit when the game places
|
|
66
|
+
* props itself in onInit with `placeAuthoredObjects`.
|
|
67
|
+
*/
|
|
68
|
+
placeObjects?: boolean | {
|
|
69
|
+
verticalOffset?: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one
|
|
74
|
+
* mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor:
|
|
75
|
+
* drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke
|
|
76
|
+
* render code. When a live-sync bus is installed (editor host), document patches stream in and
|
|
77
|
+
* re-render automatically — document is authoritative; runtime overrides stay ephemeral unless
|
|
78
|
+
* written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`);
|
|
79
|
+
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
80
|
+
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
81
|
+
*/
|
|
82
|
+
export declare function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live, placeObjects, }: AuthoredSceneProps): import("react").JSX.Element;
|