@jgengine/shell 0.19.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +364 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +12 -5
- package/dist/defineGame.js +25 -10
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +19 -15
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +19 -8
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +3 -0
- package/dist/render/modelLoad.js +9 -0
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +21 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +61 -6
- package/dist/terrain/grassGeometry.d.ts +13 -0
- package/dist/terrain/grassGeometry.js +57 -1
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +11 -0
- package/dist/terrain/terrainPaintColor.js +47 -0
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +209 -61
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +219 -70
- package/skills/jgengine-ui/capabilities.md +119 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
package/dist/world/WorldScene.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { modelWithAuthoredMaterials } from "@jgengine/core/editor/materialAuthoring";
|
|
3
|
+
import { modelWithAuthoredNodeVisibility } from "@jgengine/core/editor/modelNodeVisibility";
|
|
2
4
|
import { useFrame } from "@react-three/fiber";
|
|
3
5
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
6
|
import * as THREE from "three";
|
|
7
|
+
import { createEmptyEditorDocument } from "@jgengine/core/editor/index";
|
|
8
|
+
import { resolveAuthoredObjects } from "@jgengine/core/world/authoredObjects";
|
|
9
|
+
import { useLiveEditorDocument } from "../scene/AuthoredScene.js";
|
|
10
|
+
import { StaticObjectModels } from "./StaticObjectModels.js";
|
|
11
|
+
import { groupStaticObjects } from "./staticObjectBatches.js";
|
|
5
12
|
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
6
13
|
import { WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
7
14
|
import { beginFallbackSeam, endFallbackPass, reportFallbackSeam, } from "@jgengine/core/devtools/fallbackSeams";
|
|
@@ -12,11 +19,12 @@ import { DefaultSurface, detailMaps } from "../render/defaultSurface.js";
|
|
|
12
19
|
import { useDisposable } from "../render/useDisposable.js";
|
|
13
20
|
import { MeasuredBoundsGroup } from "../render/measureBounds.js";
|
|
14
21
|
import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
|
|
15
|
-
import {
|
|
22
|
+
import { resolveEntityModel, resolveObjectModel } from "../render/resolveModel.js";
|
|
16
23
|
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
17
24
|
import { writeEntityPose, writeRenderPose } from "./entityPose.js";
|
|
18
25
|
import { createSnapshotBuffer } from "@jgengine/core/runtime/snapshotBuffer";
|
|
19
26
|
import { POINTER_ENTITY_KEY, POINTER_OBJECT_KEY } from "../pointer/pointerService.js";
|
|
27
|
+
const EMPTY_DOCUMENT = createEmptyEditorDocument();
|
|
20
28
|
const GROUND_SIZE = 160;
|
|
21
29
|
const GROUND_SEGMENTS = 80;
|
|
22
30
|
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
@@ -130,10 +138,15 @@ function WorldEnvironment({ environment: Environment }) {
|
|
|
130
138
|
reportFallbackSeam("ground", "noScene");
|
|
131
139
|
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx(RockField, {})] }));
|
|
132
140
|
}
|
|
133
|
-
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
141
|
+
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }) {
|
|
134
142
|
const ctx = useGameContext();
|
|
135
143
|
const entityIds = useSceneEntityIds();
|
|
136
144
|
const objectIds = useSceneObjectIds();
|
|
145
|
+
const document = useLiveEditorDocument(editorLayers ?? EMPTY_DOCUMENT, editorLayers !== undefined);
|
|
146
|
+
const materialMarkers = useMemo(() => new Map(document.markers.map((marker) => [marker.id, marker])), [document]);
|
|
147
|
+
const authoredIds = useMemo(() => new Set(resolveAuthoredObjects(document).map((object) => object.instanceId)), [document]);
|
|
148
|
+
const batchCache = useRef(new Map());
|
|
149
|
+
const modelCache = useMemo(() => new Map(), [objectModels, assets]);
|
|
137
150
|
const player = usePlayer();
|
|
138
151
|
const targetId = useTarget(player.userId);
|
|
139
152
|
const controlledId = useGameStore((c) => c.player.possession.active(player.userId));
|
|
@@ -148,6 +161,29 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
148
161
|
useEffect(() => {
|
|
149
162
|
endFallbackPass();
|
|
150
163
|
});
|
|
164
|
+
const candidates = objectIds.flatMap((instanceId) => {
|
|
165
|
+
const object = ctx.scene.object.get(instanceId);
|
|
166
|
+
if (object === null)
|
|
167
|
+
return [];
|
|
168
|
+
const custom = renderObject?.(object);
|
|
169
|
+
const model = modelWithAuthoredNodeVisibility(modelWithAuthoredMaterials(resolveObjectModel(object, objectModels, assets, modelCache), { materialAssets: document.materialAssets, markers: materialMarkers.has(object.instanceId) ? [materialMarkers.get(object.instanceId)] : [] }, object.instanceId), materialMarkers.get(object.instanceId));
|
|
170
|
+
const style = objectStyles?.[object.catalogId];
|
|
171
|
+
// Reaching the primitive box means no model resolved. A present-but-unresolved objectModels
|
|
172
|
+
// key implies its asset pack is not pulled; an absent key is an omitted (often intended) mapping.
|
|
173
|
+
if ((custom === undefined || custom === null) && model === undefined && style?.hidden !== true) {
|
|
174
|
+
reportFallbackSeam("object", objectModels?.[object.catalogId] === undefined ? "omittedMapping" : "unpulledPack");
|
|
175
|
+
}
|
|
176
|
+
return [{ object, model, custom, style, authored: authoredIds.has(instanceId), catalog: ctx.scene.object.catalog(instanceId) }];
|
|
177
|
+
});
|
|
178
|
+
const grouped = groupStaticObjects(candidates.map((candidate) => ({ ...candidate, custom: candidate.custom !== undefined && candidate.custom !== null })), 24, batchCache.current);
|
|
179
|
+
batchCache.current = new Map(grouped.batches.map((batch) => [batch.key, batch]));
|
|
180
|
+
const candidatesById = new Map(candidates.map((candidate) => [candidate.object.instanceId, candidate]));
|
|
181
|
+
const renderSingle = (object) => {
|
|
182
|
+
object = ctx.scene.object.get(object.instanceId) ?? object;
|
|
183
|
+
const candidate = candidatesById.get(object.instanceId);
|
|
184
|
+
const model = modelWithAuthoredNodeVisibility(modelWithAuthoredMaterials(resolveObjectModel(object, objectModels, assets, modelCache), { materialAssets: document.materialAssets, markers: materialMarkers.has(object.instanceId) ? [materialMarkers.get(object.instanceId)] : [] }, object.instanceId), materialMarkers.get(object.instanceId));
|
|
185
|
+
return _jsx(ObjectMarker, { object: object, custom: object === candidate.object ? candidate.custom : renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId);
|
|
186
|
+
};
|
|
151
187
|
return (_jsxs(_Fragment, { children: [entityIds.map((entityId) => {
|
|
152
188
|
const entity = ctx.scene.entity.get(entityId);
|
|
153
189
|
if (entity === null || entity.name === WORLD_ITEM_ENTITY_NAME)
|
|
@@ -163,31 +199,10 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
163
199
|
reportFallbackSeam("entity", "omittedMapping");
|
|
164
200
|
}
|
|
165
201
|
return (_jsx(EntityMarker, { entity: entity, custom: custom, model: model, sprite: sprite, isLocal: entityId === controlledId, targeted: entityId === targetId, selected: selectedIds.has(entityId), onSelect: handleSelect }, entityId));
|
|
166
|
-
}),
|
|
167
|
-
const object = ctx.scene.object.get(instanceId);
|
|
168
|
-
if (object === null)
|
|
169
|
-
return null;
|
|
170
|
-
const custom = renderObject?.(object);
|
|
171
|
-
const resolved = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
172
|
-
seam: "objectModels",
|
|
173
|
-
key: object.catalogId,
|
|
174
|
-
}) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
175
|
-
// An authored per-placement animation override (marker.meta.animation → SceneObject.animation,
|
|
176
|
-
// #1276) wins over catalog resolution's default "auto"; absent, the resolved config is untouched.
|
|
177
|
-
const model = resolved !== undefined && object.animation !== undefined
|
|
178
|
-
? { ...resolved, animation: object.animation }
|
|
179
|
-
: resolved;
|
|
180
|
-
const style = objectStyles?.[object.catalogId];
|
|
181
|
-
// Reaching the primitive box means no model resolved. A present-but-unresolved objectModels
|
|
182
|
-
// key implies its asset pack is not pulled; an absent key is an omitted (often intended) mapping.
|
|
183
|
-
if ((custom === undefined || custom === null) && model === undefined && style?.hidden !== true) {
|
|
184
|
-
reportFallbackSeam("object", objectModels?.[object.catalogId] === undefined ? "omittedMapping" : "unpulledPack");
|
|
185
|
-
}
|
|
186
|
-
return (_jsx(ObjectMarker, { object: object, custom: custom, model: model, style: style }, instanceId));
|
|
187
|
-
})] }));
|
|
202
|
+
}), grouped.singles.map((candidate) => renderSingle(candidate.object)), grouped.batches.map((batch) => _jsx(StaticObjectModels, { batch: batch, single: renderSingle }, batch.key))] }));
|
|
188
203
|
}
|
|
189
|
-
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
190
|
-
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor })] }));
|
|
204
|
+
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }) {
|
|
205
|
+
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor, editorLayers: editorLayers })] }));
|
|
191
206
|
}
|
|
192
207
|
export function RemotePlayers({ rows, presenceInterpolationMs = 100 }) {
|
|
193
208
|
const groups = useRef(new Map());
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelConfig, ObjectStyle } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
4
|
+
import type { GameContextObjectEntry } from "@jgengine/core/runtime/gameContext";
|
|
5
|
+
import type { ScatterModelSource } from "../scatter/scatterModels.js";
|
|
6
|
+
/** @internal */
|
|
7
|
+
export interface StaticObjectCandidate {
|
|
8
|
+
object: SceneObject;
|
|
9
|
+
model?: ModelConfig;
|
|
10
|
+
authored: boolean;
|
|
11
|
+
custom: boolean;
|
|
12
|
+
catalog: GameContextObjectEntry | null;
|
|
13
|
+
style?: ObjectStyle | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** @internal */
|
|
16
|
+
export interface StaticObjectBatch {
|
|
17
|
+
key: string;
|
|
18
|
+
model: ModelConfig;
|
|
19
|
+
objects: SceneObject[];
|
|
20
|
+
}
|
|
21
|
+
/** The same compatibility gate is used when live placements change after grouping. @internal */
|
|
22
|
+
export declare function canBatchStaticObject(candidate: StaticObjectCandidate): boolean;
|
|
23
|
+
/** Checks a live placement without constructing a grouping candidate. @internal */
|
|
24
|
+
export declare function isCompatibleStaticObject(object: SceneObject, model: ModelConfig | undefined, catalog: GameContextObjectEntry | null): boolean;
|
|
25
|
+
/** Stable exclusion snapshot; only compatibility transitions allocate. @internal */
|
|
26
|
+
export declare function incompatibleStaticObjectIds(batch: StaticObjectBatch, get: (id: string) => SceneObject | null, catalog: (id: string) => GameContextObjectEntry | null, previous: readonly string[], fallbackObjects?: Map<string, SceneObject>): readonly string[];
|
|
27
|
+
/** Groups compatible authored props above eight placements per spatial cell. @internal */
|
|
28
|
+
export declare function groupStaticObjects(candidates: readonly StaticObjectCandidate[], chunkSize?: number, previous?: ReadonlyMap<string, StaticObjectBatch>): {
|
|
29
|
+
batches: StaticObjectBatch[];
|
|
30
|
+
singles: StaticObjectCandidate[];
|
|
31
|
+
};
|
|
32
|
+
/** Rejects loaded scenes whose behavior cannot be represented by static instanced draws. @internal */
|
|
33
|
+
export declare function canInstanceStaticScene(scene: THREE.Object3D, clips: readonly THREE.AnimationClip[]): boolean;
|
|
34
|
+
/** Compact visible placements and update picking identities without allocating per instance. @internal */
|
|
35
|
+
export declare function syncStaticObjectSource(mesh: THREE.InstancedMesh, source: ScatterModelSource, objects: readonly SceneObject[], get: (id: string) => SceneObject | null, visible: (id: string) => boolean, scratch: THREE.Object3D): void;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { POINTER_OBJECT_INSTANCES_KEY } from "../pointer/pointerService.js";
|
|
3
|
+
/** The same compatibility gate is used when live placements change after grouping. @internal */
|
|
4
|
+
export function canBatchStaticObject(candidate) {
|
|
5
|
+
const { object, model, catalog } = candidate;
|
|
6
|
+
return candidate.authored && !candidate.custom && candidate.style === undefined &&
|
|
7
|
+
isCompatibleStaticObject(object, model, catalog);
|
|
8
|
+
}
|
|
9
|
+
/** Checks a live placement without constructing a grouping candidate. @internal */
|
|
10
|
+
export function isCompatibleStaticObject(object, model, catalog) {
|
|
11
|
+
const animation = object.animation ?? model?.animation;
|
|
12
|
+
return model !== undefined &&
|
|
13
|
+
object.visual === undefined && object.state === undefined && object.slots === undefined &&
|
|
14
|
+
catalog?.proximityPrompt === undefined && (catalog?.verbs?.length ?? 0) === 0 &&
|
|
15
|
+
(catalog?.breakable === undefined || catalog.breakable === false) && catalog?.slotInventory === undefined &&
|
|
16
|
+
(animation === undefined || animation === "auto" || animation === "none") && model.ik === undefined &&
|
|
17
|
+
(model.scale ?? 1) >= 0 && (model.parts?.length ?? 0) === 0 && (model.attachments?.length ?? 0) === 0 &&
|
|
18
|
+
model.partMotion === undefined && model.material?.maps === undefined && (model.materialAssignments?.length ?? 0) === 0 &&
|
|
19
|
+
(model.hiddenNodes === undefined || (Array.isArray(model.hiddenNodes) && model.hiddenNodes.length === 0));
|
|
20
|
+
}
|
|
21
|
+
/** Stable exclusion snapshot; only compatibility transitions allocate. @internal */
|
|
22
|
+
export function incompatibleStaticObjectIds(batch, get, catalog, previous, fallbackObjects) {
|
|
23
|
+
const compatible = (object) => object.catalogId === batch.objects[0].catalogId &&
|
|
24
|
+
isCompatibleStaticObject(object, batch.model, catalog(object.instanceId));
|
|
25
|
+
let count = 0;
|
|
26
|
+
let changed = false;
|
|
27
|
+
for (const object of batch.objects) {
|
|
28
|
+
const live = get(object.instanceId);
|
|
29
|
+
if (live !== null && !compatible(live)) {
|
|
30
|
+
changed ||= previous[count] !== live.instanceId;
|
|
31
|
+
const before = fallbackObjects?.get(live.instanceId);
|
|
32
|
+
if (before !== undefined)
|
|
33
|
+
changed ||= before.catalogId !== live.catalogId || before.animation !== live.animation ||
|
|
34
|
+
before.visual !== live.visual || before.state !== live.state || before.slots !== live.slots;
|
|
35
|
+
fallbackObjects?.set(live.instanceId, live);
|
|
36
|
+
count++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (fallbackObjects !== undefined)
|
|
40
|
+
for (const id of fallbackObjects.keys()) {
|
|
41
|
+
const live = get(id);
|
|
42
|
+
if (live === null || compatible(live))
|
|
43
|
+
fallbackObjects.delete(id);
|
|
44
|
+
}
|
|
45
|
+
if (!changed && count === previous.length)
|
|
46
|
+
return previous;
|
|
47
|
+
return batch.objects.flatMap((object) => {
|
|
48
|
+
const live = get(object.instanceId);
|
|
49
|
+
return live !== null && !compatible(live) ? [live.instanceId] : [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Groups compatible authored props above eight placements per spatial cell. @internal */
|
|
53
|
+
export function groupStaticObjects(candidates, chunkSize = 24, previous) {
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
const singles = [];
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
const { object, model } = candidate;
|
|
58
|
+
if (model === undefined || !canBatchStaticObject(candidate)) {
|
|
59
|
+
singles.push(candidate);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const key = JSON.stringify([object.catalogId, Math.floor(object.position[0] / chunkSize), Math.floor(object.position[2] / chunkSize), model]);
|
|
63
|
+
let group = groups.get(key);
|
|
64
|
+
if (group === undefined) {
|
|
65
|
+
group = { batch: { key, model, objects: [] }, candidates: [] };
|
|
66
|
+
groups.set(key, group);
|
|
67
|
+
}
|
|
68
|
+
group.batch.objects.push(object);
|
|
69
|
+
group.candidates.push(candidate);
|
|
70
|
+
}
|
|
71
|
+
const batches = [];
|
|
72
|
+
for (const { batch, candidates: members } of groups.values()) {
|
|
73
|
+
if (batch.objects.length > 8) {
|
|
74
|
+
const cached = previous?.get(batch.key);
|
|
75
|
+
batches.push(cached !== undefined && cached.objects.length === batch.objects.length &&
|
|
76
|
+
cached.objects.every((object, index) => object.instanceId === batch.objects[index].instanceId) ? cached : batch);
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
singles.push(...members);
|
|
80
|
+
}
|
|
81
|
+
return { batches, singles };
|
|
82
|
+
}
|
|
83
|
+
/** Rejects loaded scenes whose behavior cannot be represented by static instanced draws. @internal */
|
|
84
|
+
export function canInstanceStaticScene(scene, clips) {
|
|
85
|
+
if (clips.length > 0)
|
|
86
|
+
return false;
|
|
87
|
+
scene.updateMatrixWorld(true);
|
|
88
|
+
if (!scene.matrix.equals(new THREE.Matrix4()))
|
|
89
|
+
return false;
|
|
90
|
+
let supported = true;
|
|
91
|
+
scene.traverse((node) => {
|
|
92
|
+
if (node.layers.mask !== 1 || node.renderOrder !== 0 || node.matrix.determinant() < 0)
|
|
93
|
+
supported = false;
|
|
94
|
+
const mesh = node;
|
|
95
|
+
if (!mesh.isMesh) {
|
|
96
|
+
if (!node.isGroup && node.type !== "Object3D" && node.type !== "Scene")
|
|
97
|
+
supported = false;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (mesh.isSkinnedMesh || mesh.isInstancedMesh ||
|
|
101
|
+
mesh.morphTargetInfluences !== undefined || mesh.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender ||
|
|
102
|
+
mesh.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
|
|
103
|
+
supported = false;
|
|
104
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
105
|
+
if (materials.some((material) => material.transparent || material.isShaderMaterial))
|
|
106
|
+
supported = false;
|
|
107
|
+
const elements = mesh.matrixWorld.elements;
|
|
108
|
+
for (let a = 0; a < 3; a++)
|
|
109
|
+
for (let b = a + 1; b < 3; b++) {
|
|
110
|
+
const dot = elements[a * 4] * elements[b * 4] + elements[a * 4 + 1] * elements[b * 4 + 1] + elements[a * 4 + 2] * elements[b * 4 + 2];
|
|
111
|
+
const lengthA = Math.hypot(elements[a * 4], elements[a * 4 + 1], elements[a * 4 + 2]);
|
|
112
|
+
const lengthB = Math.hypot(elements[b * 4], elements[b * 4 + 1], elements[b * 4 + 2]);
|
|
113
|
+
// Three's instanced normal transform assumes orthogonal matrix columns.
|
|
114
|
+
if (Math.abs(dot) > 1e-5 * lengthA * lengthB)
|
|
115
|
+
supported = false;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return supported;
|
|
119
|
+
}
|
|
120
|
+
/** Compact visible placements and update picking identities without allocating per instance. @internal */
|
|
121
|
+
export function syncStaticObjectSource(mesh, source, objects, get, visible, scratch) {
|
|
122
|
+
const ids = (mesh.userData[POINTER_OBJECT_INSTANCES_KEY] ??= []);
|
|
123
|
+
const poses = (mesh.userData["jgStaticObjectPoses"] ??= []);
|
|
124
|
+
if (mesh.userData["jgStaticObjectSource"] !== source) {
|
|
125
|
+
poses.length = 0;
|
|
126
|
+
mesh.userData["jgStaticObjectSource"] = source;
|
|
127
|
+
}
|
|
128
|
+
let count = 0;
|
|
129
|
+
let changed = false;
|
|
130
|
+
const composed = scratch.matrixWorld;
|
|
131
|
+
for (const object of objects) {
|
|
132
|
+
const live = get(object.instanceId);
|
|
133
|
+
if (live === null || !visible(live.instanceId))
|
|
134
|
+
continue;
|
|
135
|
+
if (poses[count] === live) {
|
|
136
|
+
ids[count++] = live.instanceId;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
scratch.position.fromArray(live.position);
|
|
140
|
+
scratch.rotation.set(0, live.rotationY, 0);
|
|
141
|
+
scratch.updateMatrix();
|
|
142
|
+
composed.multiplyMatrices(scratch.matrix, source.localMatrix);
|
|
143
|
+
const offset = count * 16;
|
|
144
|
+
for (let axis = 0; axis < 16; axis += 1) {
|
|
145
|
+
if (mesh.instanceMatrix.array[offset + axis] !== Math.fround(composed.elements[axis])) {
|
|
146
|
+
changed = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
mesh.setMatrixAt(count, composed);
|
|
151
|
+
poses[count] = live;
|
|
152
|
+
ids[count++] = live.instanceId;
|
|
153
|
+
}
|
|
154
|
+
ids.length = count;
|
|
155
|
+
poses.length = count;
|
|
156
|
+
changed ||= mesh.count !== count;
|
|
157
|
+
mesh.count = count;
|
|
158
|
+
mesh.visible = count > 0;
|
|
159
|
+
if (changed) {
|
|
160
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
161
|
+
mesh.computeBoundingSphere();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
2
|
-
import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
|
|
3
|
-
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
1
|
+
import type { SceneEntity, EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
2
|
+
import type { CatalogEntityRole, GameContext } from "@jgengine/core/runtime/gameContext";
|
|
4
3
|
export interface WorldBarSample {
|
|
4
|
+
/** Live anchor id used by retained samples; omitted for caller-painted screen samples. */
|
|
5
|
+
entityId?: string;
|
|
5
6
|
x: number;
|
|
6
7
|
y: number;
|
|
7
8
|
percent: number;
|
|
@@ -16,49 +17,52 @@ export interface Projectable {
|
|
|
16
17
|
y: number;
|
|
17
18
|
z: number;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* Projects every non-local, role/distance-filtered entity carrying `statId` into `into` as a
|
|
28
|
-
* `WorldBarSample`. Pure data step behind `WorldHealthBars` — call it directly to paint the bars
|
|
29
|
-
* with your own renderer.
|
|
30
|
-
* @internal
|
|
31
|
-
*/
|
|
32
|
-
export declare function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: {
|
|
33
|
-
matrixWorldInverse: unknown;
|
|
20
|
+
/** Render camera matrices; world translation supplies the overlay's viewpoint. */
|
|
21
|
+
export interface WorldOverlayCamera {
|
|
22
|
+
matrixWorld: {
|
|
23
|
+
elements: ArrayLike<number>;
|
|
24
|
+
};
|
|
25
|
+
matrixWorldInverse: {
|
|
26
|
+
elements: ArrayLike<number>;
|
|
27
|
+
};
|
|
34
28
|
projectionMatrix: unknown;
|
|
35
|
-
}
|
|
29
|
+
}
|
|
30
|
+
/** Sampling policy shared by health bars and nameplates. */
|
|
31
|
+
export interface WorldOverlaySampleOptions {
|
|
32
|
+
/** Maximum sampled overlays and visibility rays per refresh. Default 64; unchecked entities stay hidden. */
|
|
33
|
+
maxSamples?: number;
|
|
34
|
+
/** Match a renderer's visibility or membership policy. */
|
|
35
|
+
isVisible?: (id: string) => boolean;
|
|
36
|
+
/** Authored display name; return null to omit the nameplate. */
|
|
37
|
+
resolveName?: (entity: SceneEntity) => string | null;
|
|
38
|
+
}
|
|
39
|
+
/** True when blocking scene geometry lies between the render camera and overlay anchor. @internal */
|
|
40
|
+
export declare function worldBarOccluded(ctx: GameContext, from: EntityPosition, target: EntityPosition): boolean;
|
|
41
|
+
/** Project nearby live non-local entities carrying `statId`; occlusion defaults on. @internal */
|
|
42
|
+
export declare function collectWorldBarSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: WorldOverlayCamera, viewport: {
|
|
36
43
|
width: number;
|
|
37
44
|
height: number;
|
|
38
|
-
}, into: WorldBarSample[], project: Projectable, maxDistance?: number, occlude?: boolean): number;
|
|
39
|
-
/**
|
|
45
|
+
}, into: WorldBarSample[], project: Projectable, maxDistance?: number, occlude?: boolean, options?: WorldOverlaySampleOptions): number;
|
|
46
|
+
/** Reproject retained bar anchors without a nearby query or visibility ray. @internal */
|
|
47
|
+
export declare function refreshWorldBarSamples(ctx: GameContext, height: number, camera: WorldOverlayCamera, viewport: {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}, samples: WorldBarSample[], project: Projectable, maxDistance?: number, options?: WorldOverlaySampleOptions): void;
|
|
51
|
+
/** Projected nameplate with optional health and distance from the render camera. */
|
|
40
52
|
export interface NameplateSample {
|
|
41
53
|
id: string;
|
|
42
54
|
name: string;
|
|
43
55
|
x: number;
|
|
44
56
|
y: number;
|
|
45
|
-
/** Health-stat fraction 0..1, or
|
|
57
|
+
/** Health-stat fraction 0..1, or null for a statless entity. */
|
|
46
58
|
percent: number | null;
|
|
47
59
|
distance: number;
|
|
48
60
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
* `NameplateSample`. Pure data step powering `WorldNameplates` — swap in your
|
|
52
|
-
* own renderer by calling this directly instead of the component.
|
|
53
|
-
* @internal
|
|
54
|
-
*/
|
|
55
|
-
export declare function collectNameplateSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: {
|
|
56
|
-
matrixWorldInverse: unknown;
|
|
57
|
-
projectionMatrix: unknown;
|
|
58
|
-
}, viewport: {
|
|
61
|
+
/** Project nearby live non-local nameplates; occlusion defaults on. @internal */
|
|
62
|
+
export declare function collectNameplateSamples(ctx: GameContext, statId: string, height: number, roles: readonly CatalogEntityRole[] | undefined, resolveRole: ((entity: SceneEntity) => CatalogEntityRole | undefined) | undefined, camera: WorldOverlayCamera, viewport: {
|
|
59
63
|
width: number;
|
|
60
64
|
height: number;
|
|
61
|
-
}, into: NameplateSample[], project: Projectable, maxDistance?: number, occlude?: boolean): number;
|
|
65
|
+
}, into: NameplateSample[], project: Projectable, maxDistance?: number, occlude?: boolean, options?: WorldOverlaySampleOptions): number;
|
|
62
66
|
/** @internal */
|
|
63
67
|
export declare function paintWorldBarSamples(canvas: {
|
|
64
68
|
width: number;
|
|
@@ -1,98 +1,140 @@
|
|
|
1
1
|
import { worldHealthBarAllowsRole } from "@jgengine/core/game/playableGame";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
export function worldBarOccluded(ctx, from, target, eyeHeight) {
|
|
9
|
-
const origin = [from[0], from[1] + eyeHeight, from[2]];
|
|
10
|
-
const dx = target[0] - origin[0];
|
|
11
|
-
const dy = target[1] + eyeHeight - origin[1];
|
|
12
|
-
const dz = target[2] - origin[2];
|
|
2
|
+
/** True when blocking scene geometry lies between the render camera and overlay anchor. @internal */
|
|
3
|
+
export function worldBarOccluded(ctx, from, target) {
|
|
4
|
+
const dx = target[0] - from[0];
|
|
5
|
+
const dy = target[1] - from[1];
|
|
6
|
+
const dz = target[2] - from[2];
|
|
13
7
|
const length = Math.hypot(dx, dy, dz);
|
|
14
8
|
if (length < 0.001)
|
|
15
9
|
return false;
|
|
16
|
-
|
|
17
|
-
origin,
|
|
10
|
+
return ctx.scene.raycast({
|
|
11
|
+
origin: from,
|
|
18
12
|
direction: [dx / length, dy / length, dz / length],
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
maxDistance: length - 0.001,
|
|
14
|
+
filter: { entities: false },
|
|
15
|
+
accept: (hit) => hit.blocks,
|
|
16
|
+
}) !== null;
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
function cameraOrigin(camera) {
|
|
19
|
+
const matrix = camera.matrixWorld.elements;
|
|
20
|
+
return [matrix[12], matrix[13], matrix[14]];
|
|
21
|
+
}
|
|
22
|
+
function projectAnchor(entity, height, camera, project) {
|
|
23
|
+
const anchor = [entity.position[0], entity.position[1] + height, entity.position[2]];
|
|
24
|
+
const inverse = camera.matrixWorldInverse.elements;
|
|
25
|
+
const cameraZ = inverse[2] * anchor[0] + inverse[6] * anchor[1] + inverse[10] * anchor[2] + inverse[14];
|
|
26
|
+
if (!Number.isFinite(cameraZ) || cameraZ >= 0)
|
|
27
|
+
return null;
|
|
28
|
+
project.set(...anchor).project(camera);
|
|
29
|
+
if (!Number.isFinite(project.x) || !Number.isFinite(project.y) || !Number.isFinite(project.z))
|
|
30
|
+
return null;
|
|
31
|
+
if (Math.abs(project.x) > 1 || Math.abs(project.y) > 1 || Math.abs(project.z) > 1)
|
|
32
|
+
return null;
|
|
33
|
+
return anchor;
|
|
34
|
+
}
|
|
35
|
+
function healthFraction(stat) {
|
|
36
|
+
const range = stat.max - stat.min;
|
|
37
|
+
return range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
38
|
+
}
|
|
39
|
+
function eligibleEntity(ctx, id, options) {
|
|
40
|
+
if (id === ctx.player.userId || id === ctx.player.possession.active(ctx.player.userId) || options.isVisible?.(id) === false)
|
|
41
|
+
return null;
|
|
42
|
+
const entity = ctx.scene.entity.get(id);
|
|
43
|
+
if (entity === null || entity.hidden === true)
|
|
44
|
+
return null;
|
|
45
|
+
const health = ctx.scene.entity.stats.get(id, "health");
|
|
46
|
+
if (health !== null && health.current <= health.min)
|
|
47
|
+
return null;
|
|
48
|
+
return entity;
|
|
49
|
+
}
|
|
50
|
+
function sampleLimit(options) {
|
|
51
|
+
return Math.max(0, Math.min(256, Math.floor(options.maxSamples ?? 64) || 0));
|
|
52
|
+
}
|
|
53
|
+
/** Project nearby live non-local entities carrying `statId`; occlusion defaults on. @internal */
|
|
54
|
+
export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance = 60, occlude = true, options = {}) {
|
|
31
55
|
into.length = 0;
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
const origin = cameraOrigin(camera);
|
|
57
|
+
const limit = sampleLimit(options);
|
|
58
|
+
if (limit === 0 || !Number.isFinite(maxDistance) || maxDistance <= 0)
|
|
59
|
+
return 0;
|
|
60
|
+
let checked = 0;
|
|
61
|
+
for (const id of ctx.scene.entity.inRadius(origin, maxDistance)) {
|
|
62
|
+
const entity = eligibleEntity(ctx, id, options);
|
|
63
|
+
if (entity === null || !worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
|
|
36
64
|
continue;
|
|
37
|
-
|
|
65
|
+
const stat = ctx.scene.entity.stats.get(id, statId);
|
|
66
|
+
if (stat === null)
|
|
38
67
|
continue;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
maxDistance) {
|
|
68
|
+
const anchor = projectAnchor(entity, height, camera, project);
|
|
69
|
+
if (anchor === null)
|
|
42
70
|
continue;
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
if (checked++ >= limit)
|
|
72
|
+
break;
|
|
73
|
+
if (occlude && worldBarOccluded(ctx, origin, anchor))
|
|
45
74
|
continue;
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
into.push({
|
|
76
|
+
entityId: id,
|
|
77
|
+
x: (project.x * 0.5 + 0.5) * viewport.width,
|
|
78
|
+
y: (-project.y * 0.5 + 0.5) * viewport.height,
|
|
79
|
+
percent: healthFraction(stat),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return into.length;
|
|
83
|
+
}
|
|
84
|
+
/** Reproject retained bar anchors without a nearby query or visibility ray. @internal */
|
|
85
|
+
export function refreshWorldBarSamples(ctx, height, camera, viewport, samples, project, maxDistance = 60, options = {}) {
|
|
86
|
+
const origin = cameraOrigin(camera);
|
|
87
|
+
let kept = 0;
|
|
88
|
+
for (const sample of samples) {
|
|
89
|
+
if (sample.entityId === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
const entity = eligibleEntity(ctx, sample.entityId, options);
|
|
92
|
+
if (entity === null || Math.hypot(entity.position[0] - origin[0], entity.position[1] - origin[1], entity.position[2] - origin[2]) > maxDistance)
|
|
48
93
|
continue;
|
|
49
|
-
|
|
50
|
-
const percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
51
|
-
project.set(entity.position[0], entity.position[1] + height, entity.position[2]);
|
|
52
|
-
project.project(camera);
|
|
53
|
-
if (project.z < -1 || project.z > 1)
|
|
94
|
+
if (projectAnchor(entity, height, camera, project) === null)
|
|
54
95
|
continue;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
sample.x = (project.x * 0.5 + 0.5) * viewport.width;
|
|
97
|
+
sample.y = (-project.y * 0.5 + 0.5) * viewport.height;
|
|
98
|
+
samples[kept++] = sample;
|
|
58
99
|
}
|
|
59
|
-
|
|
100
|
+
samples.length = kept;
|
|
101
|
+
}
|
|
102
|
+
function defaultDisplayName(entity) {
|
|
103
|
+
const name = entity.name.trim();
|
|
104
|
+
// Entity names also key catalogs; machine identifiers need an explicit display-name resolver.
|
|
105
|
+
return name === entity.id || /^[a-z0-9_-]+$/.test(name) || /[_:/]/.test(name) ? null : name || null;
|
|
60
106
|
}
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
* `NameplateSample`. Pure data step powering `WorldNameplates` — swap in your
|
|
64
|
-
* own renderer by calling this directly instead of the component.
|
|
65
|
-
* @internal
|
|
66
|
-
*/
|
|
67
|
-
export function collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance = 40, occlude = false) {
|
|
107
|
+
/** Project nearby live non-local nameplates; occlusion defaults on. @internal */
|
|
108
|
+
export function collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, viewport, into, project, maxDistance = 40, occlude = true, options = {}) {
|
|
68
109
|
into.length = 0;
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
const origin = cameraOrigin(camera);
|
|
111
|
+
const limit = sampleLimit(options);
|
|
112
|
+
if (limit === 0 || !Number.isFinite(maxDistance) || maxDistance <= 0)
|
|
113
|
+
return 0;
|
|
114
|
+
let checked = 0;
|
|
115
|
+
for (const id of ctx.scene.entity.inRadius(origin, maxDistance)) {
|
|
116
|
+
const entity = eligibleEntity(ctx, id, options);
|
|
117
|
+
if (entity === null || !worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
|
|
73
118
|
continue;
|
|
74
|
-
|
|
119
|
+
const name = (options.resolveName ?? defaultDisplayName)(entity)?.trim();
|
|
120
|
+
if (!name)
|
|
75
121
|
continue;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
: Math.hypot(entity.position[0] - player.position[0], entity.position[2] - player.position[2]);
|
|
79
|
-
if (player !== null && distance > maxDistance)
|
|
122
|
+
const anchor = projectAnchor(entity, height, camera, project);
|
|
123
|
+
if (anchor === null)
|
|
80
124
|
continue;
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let percent = null;
|
|
85
|
-
if (stat !== null) {
|
|
86
|
-
const range = stat.max - stat.min;
|
|
87
|
-
percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
88
|
-
}
|
|
89
|
-
project.set(entity.position[0], entity.position[1] + height, entity.position[2]);
|
|
90
|
-
project.project(camera);
|
|
91
|
-
if (project.z < -1 || project.z > 1)
|
|
125
|
+
if (checked++ >= limit)
|
|
126
|
+
break;
|
|
127
|
+
if (occlude && worldBarOccluded(ctx, origin, anchor))
|
|
92
128
|
continue;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
129
|
+
const stat = ctx.scene.entity.stats.get(id, statId);
|
|
130
|
+
into.push({
|
|
131
|
+
id,
|
|
132
|
+
name,
|
|
133
|
+
x: (project.x * 0.5 + 0.5) * viewport.width,
|
|
134
|
+
y: (-project.y * 0.5 + 0.5) * viewport.height,
|
|
135
|
+
percent: stat === null ? null : healthFraction(stat),
|
|
136
|
+
distance: Math.hypot(entity.position[0] - origin[0], entity.position[1] - origin[1], entity.position[2] - origin[2]),
|
|
137
|
+
});
|
|
96
138
|
}
|
|
97
139
|
return into.length;
|
|
98
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^0.
|
|
38
|
+
"@jgengine/core": "^0.22.0",
|
|
39
|
+
"@jgengine/react": "^0.22.0",
|
|
40
|
+
"@jgengine/ws": "^0.22.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|