@jgengine/shell 0.18.1 → 0.21.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 +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- 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/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- 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 +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- 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/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- 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 +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- 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/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -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 +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -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 +32 -0
- package/dist/render/modelLoad.js +70 -1
- 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 +25 -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.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- 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 +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- 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 +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- 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/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- 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 +210 -62
- 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 +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import type { LiveProjectile } from "@jgengine/core/combat/projectiles";
|
|
4
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
5
|
+
/** Explicit opt-in shaft/head geometry when a game has no projectile model. */
|
|
6
|
+
export interface ProjectileShaft {
|
|
7
|
+
length?: number;
|
|
8
|
+
radius?: number;
|
|
9
|
+
shaftColor?: THREE.ColorRepresentation;
|
|
10
|
+
headColor?: THREE.ColorRepresentation;
|
|
11
|
+
}
|
|
12
|
+
/** Render simulated flights without owning their travel or settlement. */
|
|
13
|
+
export interface ProjectileModelsProps {
|
|
14
|
+
model?: ModelConfig | ((projectile: LiveProjectile) => ModelConfig | undefined);
|
|
15
|
+
/** Defaults to no fallback geometry. */
|
|
16
|
+
fallback?: ProjectileShaft;
|
|
17
|
+
/** The model's nose axis. Default +Y, matching the shaft fallback. */
|
|
18
|
+
forward?: readonly [number, number, number];
|
|
19
|
+
maxInstances?: number;
|
|
20
|
+
/** An alternate authoritative source; otherwise uses the context's live flights. */
|
|
21
|
+
source?: () => readonly LiveProjectile[];
|
|
22
|
+
}
|
|
23
|
+
/** @internal Apply live position and full velocity orientation without frame allocations. */
|
|
24
|
+
export declare function applyProjectilePose(group: THREE.Group, projectile: Pick<LiveProjectile, "position" | "velocity">, forward: THREE.Vector3, direction: THREE.Vector3): void;
|
|
25
|
+
/**
|
|
26
|
+
* Mount caller-owned models at authoritative projectile positions. Flight ids
|
|
27
|
+
* determine lifetime; each frame only updates transforms, and settlement removes
|
|
28
|
+
* the model. Optional shaft/head geometry is enabled only by the caller.
|
|
29
|
+
* @capability projectile-models render authoritative projectile flights with caller models or opt-in shaft/head geometry, velocity orientation, bounded instances, and settlement cleanup
|
|
30
|
+
*/
|
|
31
|
+
export declare function ProjectileModels({ model, fallback, forward, maxInstances, source }: ProjectileModelsProps): ReactElement;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { IsolatedEntityModel } from "../render/SceneModels.js";
|
|
7
|
+
/** @internal Apply live position and full velocity orientation without frame allocations. */
|
|
8
|
+
export function applyProjectilePose(group, projectile, forward, direction) {
|
|
9
|
+
group.position.set(...projectile.position);
|
|
10
|
+
direction.set(...projectile.velocity);
|
|
11
|
+
if (direction.lengthSq() > 1e-12)
|
|
12
|
+
group.quaternion.setFromUnitVectors(forward, direction.normalize());
|
|
13
|
+
}
|
|
14
|
+
function Shaft({ length = 0.85, radius = 0.012, shaftColor = "#947044", headColor = "#b9c4d0" }) {
|
|
15
|
+
const headLength = length * 0.18;
|
|
16
|
+
return _jsxs(_Fragment, { children: [_jsxs("mesh", { position: [0, -(length + headLength) / 2, 0], children: [_jsx("cylinderGeometry", { args: [radius, radius, length - headLength, 6] }), _jsx("meshStandardMaterial", { color: shaftColor, roughness: 0.78 })] }), _jsxs("mesh", { position: [0, -headLength / 2, 0], children: [_jsx("coneGeometry", { args: [radius * 3.5, headLength, 6] }), _jsx("meshStandardMaterial", { color: headColor, metalness: 0.5, roughness: 0.38 })] })] });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mount caller-owned models at authoritative projectile positions. Flight ids
|
|
20
|
+
* determine lifetime; each frame only updates transforms, and settlement removes
|
|
21
|
+
* the model. Optional shaft/head geometry is enabled only by the caller.
|
|
22
|
+
* @capability projectile-models render authoritative projectile flights with caller models or opt-in shaft/head geometry, velocity orientation, bounded instances, and settlement cleanup
|
|
23
|
+
*/
|
|
24
|
+
export function ProjectileModels({ model, fallback, forward = [0, 1, 0], maxInstances = 256, source }) {
|
|
25
|
+
const ctx = useGameContext();
|
|
26
|
+
const entries = useRef(new Map());
|
|
27
|
+
const seen = useRef(new Set());
|
|
28
|
+
const direction = useRef(new THREE.Vector3());
|
|
29
|
+
const nose = useRef(new THREE.Vector3());
|
|
30
|
+
const [, setGeneration] = useState(0);
|
|
31
|
+
nose.current.set(...forward).normalize();
|
|
32
|
+
if (nose.current.lengthSq() === 0)
|
|
33
|
+
nose.current.set(0, 1, 0);
|
|
34
|
+
const limit = Number.isFinite(maxInstances) ? Math.max(0, Math.min(1024, Math.floor(maxInstances))) : 256;
|
|
35
|
+
useFrame(() => {
|
|
36
|
+
let changed = false;
|
|
37
|
+
const live = source?.() ?? ctx.scene.entity.activeProjectiles();
|
|
38
|
+
seen.current.clear();
|
|
39
|
+
for (let i = 0, n = Math.min(limit, live.length); i < n; i++) {
|
|
40
|
+
const flight = live[i];
|
|
41
|
+
const key = `${flight.shotId}:${flight.pellet}`;
|
|
42
|
+
seen.current.add(key);
|
|
43
|
+
const current = entries.current.get(key);
|
|
44
|
+
if (current === undefined) {
|
|
45
|
+
entries.current.set(key, { flight, group: null });
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
current.flight = flight;
|
|
50
|
+
if (current.group !== null)
|
|
51
|
+
applyProjectilePose(current.group, flight, nose.current, direction.current);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const key of entries.current.keys())
|
|
55
|
+
if (!seen.current.has(key)) {
|
|
56
|
+
entries.current.delete(key);
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
if (changed)
|
|
60
|
+
setGeneration((n) => n + 1);
|
|
61
|
+
});
|
|
62
|
+
return _jsx(_Fragment, { children: Array.from(entries.current, ([key, entry]) => {
|
|
63
|
+
const selected = typeof model === "function" ? model(entry.flight) : model;
|
|
64
|
+
return _jsx("group", { ref: (group) => {
|
|
65
|
+
entry.group = group;
|
|
66
|
+
if (group !== null)
|
|
67
|
+
applyProjectilePose(group, entry.flight, nose.current, direction.current);
|
|
68
|
+
}, children: selected === undefined ? fallback === undefined ? null : _jsx(Shaft, { ...fallback }) : _jsx(IsolatedEntityModel, { model: selected, instanceId: key }) }, key);
|
|
69
|
+
}) });
|
|
70
|
+
}
|
package/dist/commandSink.d.ts
CHANGED
|
@@ -14,8 +14,7 @@ export declare function localCommandSink(ctx: GameContext): CommandSink;
|
|
|
14
14
|
export declare function remoteCommandSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): CommandSink;
|
|
15
15
|
/**
|
|
16
16
|
* The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into
|
|
17
|
-
* `authority: "server"` and a server is joined
|
|
18
|
-
* scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
|
|
17
|
+
* `authority: "server"` and a server is joined. Before join it discards commands rather than mutating a replica.
|
|
19
18
|
* @internal
|
|
20
19
|
*/
|
|
21
20
|
export declare function resolveCommandSink(ctx: GameContext, opts: {
|
package/dist/commandSink.js
CHANGED
|
@@ -20,13 +20,14 @@ export function remoteCommandSink(backend, serverId) {
|
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into
|
|
23
|
-
* `authority: "server"` and a server is joined
|
|
24
|
-
* scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
|
|
23
|
+
* `authority: "server"` and a server is joined. Before join it discards commands rather than mutating a replica.
|
|
25
24
|
* @internal
|
|
26
25
|
*/
|
|
27
26
|
export function resolveCommandSink(ctx, opts) {
|
|
28
27
|
if (opts.serverAuthoritative && opts.backend !== null && opts.serverId !== null) {
|
|
29
28
|
return remoteCommandSink(opts.backend, opts.serverId);
|
|
30
29
|
}
|
|
30
|
+
if (opts.serverAuthoritative)
|
|
31
|
+
return { run() { } };
|
|
31
32
|
return localCommandSink(ctx);
|
|
32
33
|
}
|
package/dist/defineGame.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { type ComponentType } from "react";
|
|
2
|
+
import type { EditorDocument } from "@jgengine/core/editor/types";
|
|
2
3
|
import { type GameDefinitionConfig, type GameLoop } from "@jgengine/core/game/defineGame";
|
|
4
|
+
import type { BackdropConfig } from "@jgengine/core/game/playableGame";
|
|
3
5
|
import type { GameContext, GameContextContent } from "@jgengine/core/runtime/gameContext";
|
|
4
6
|
import type { ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
|
|
5
7
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
8
|
+
import type { SkyEnvironmentConfig } from "@jgengine/core/world/features";
|
|
9
|
+
import type { ResolveAuthoredObjectsOptions } from "@jgengine/core/world/authoredObjects";
|
|
6
10
|
import type { PlayableGame } from "./registry.js";
|
|
7
11
|
type EngineFields<TAssetRef extends ModelAssetRef> = Omit<GameDefinitionConfig<TAssetRef>, "loop" | "ui" | "multiplayer"> & {
|
|
8
12
|
multiplayer?: GameDefinitionConfig<TAssetRef>["multiplayer"];
|
|
@@ -11,14 +15,26 @@ type PresentationFields = Omit<PlayableGame, "game" | "content" | "loop" | "Game
|
|
|
11
15
|
content?: GameContextContent;
|
|
12
16
|
loop?: Partial<GameLoop<GameContext>>;
|
|
13
17
|
GameUI?: ComponentType;
|
|
14
|
-
/** Tunes how the auto-mounted `AuthoredScene` places
|
|
15
|
-
scenePlacement?: boolean | {
|
|
18
|
+
/** Tunes how the auto-mounted `AuthoredScene` places catalog-id markers into the object store. Default `true`; `excludeKinds` replaces the resolver's mob/boss exclusions, so include those when adding game-owned spawn kinds. Pass `false` when the game places all content itself. */
|
|
19
|
+
scenePlacement?: boolean | ({
|
|
16
20
|
verticalOffset?: number;
|
|
17
|
-
};
|
|
21
|
+
} & ResolveAuthoredObjectsOptions);
|
|
18
22
|
/** GLB models for the auto-mounted scene's scatter palette items, keyed by palette item id; string ids resolve through the game's asset catalog. Unmatched items keep the built-in proxy meshes. */
|
|
19
23
|
sceneScatterModels?: Record<string, string | ModelConfig>;
|
|
24
|
+
/** Animated species models for cosmetic authored habitats; gameplay actors remain game-owned. */
|
|
25
|
+
sceneFlockModels?: Record<string, string | ModelConfig>;
|
|
26
|
+
/** Visible ground path kinds; omit to draw every non-scatter path, or restrict to keep flight/patrol guides out of world roads. */
|
|
27
|
+
scenePathKinds?: readonly string[];
|
|
20
28
|
};
|
|
21
29
|
export type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields;
|
|
30
|
+
/**
|
|
31
|
+
* The scene document's authored sky fills `backdrop.sky` when the game did not set one, so editor
|
|
32
|
+
* lighting-workspace edits render in `place()` worlds too. Defined document fields overlay the
|
|
33
|
+
* legacy world's sky, preserving settings the editor cannot represent. An environment carrying only
|
|
34
|
+
* point lights yields no sky fields and leaves the world's sky alone.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function withDocumentSky(backdrop: BackdropConfig | undefined, doc: EditorDocument | undefined, worldSky?: SkyEnvironmentConfig): BackdropConfig | undefined;
|
|
22
38
|
/**
|
|
23
39
|
* The one public authoring entry point: compose engine fields (systems, world, physics, input) and
|
|
24
40
|
* presentation fields (camera, HUD, audio, authored scene) into a `PlayableGame` ready for `GameHost`.
|
package/dist/defineGame.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { defineGameDefinition as defineEngineGame, } from "@jgengine/core/game/defineGame";
|
|
4
4
|
import { syncLifecyclePhase } from "@jgengine/core/game/gamePhase";
|
|
5
5
|
import { offline } from "@jgengine/core/runtime/adapter";
|
|
6
|
-
import { lightingFromDocument } from "@jgengine/core/editor/environment";
|
|
6
|
+
import { lightingFromDocument, skyFromDocument } from "@jgengine/core/editor/environment";
|
|
7
7
|
import { resolveGameLook } from "@jgengine/core/render/lookPreset";
|
|
8
8
|
import { EnvironmentScene } from "./environment.js";
|
|
9
9
|
import { terrainGroundColorSampler } from "./environment/terrainGroundColor.js";
|
|
@@ -14,14 +14,36 @@ function worldBackdrop(feature) {
|
|
|
14
14
|
return _jsx(EnvironmentScene, { feature: feature });
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The scene document's authored sky fills `backdrop.sky` when the game did not set one, so editor
|
|
19
|
+
* lighting-workspace edits render in `place()` worlds too. Defined document fields overlay the
|
|
20
|
+
* legacy world's sky, preserving settings the editor cannot represent. An environment carrying only
|
|
21
|
+
* point lights yields no sky fields and leaves the world's sky alone.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export function withDocumentSky(backdrop, doc, worldSky) {
|
|
25
|
+
if (backdrop?.sky !== undefined || doc === undefined)
|
|
26
|
+
return backdrop;
|
|
27
|
+
const sky = skyFromDocument(doc);
|
|
28
|
+
if (sky === undefined || Object.keys(sky).length === 0)
|
|
29
|
+
return backdrop;
|
|
30
|
+
return { ...backdrop, sky: {
|
|
31
|
+
...worldSky,
|
|
32
|
+
...sky,
|
|
33
|
+
...(sky.sun === undefined ? {} : { sun: { ...worldSky?.sun,
|
|
34
|
+
...Object.fromEntries(Object.entries(sky.sun).filter(([, value]) => value !== undefined)) } }),
|
|
35
|
+
...(sky.fog === undefined ? {} : { fog: { ...worldSky?.fog,
|
|
36
|
+
...Object.fromEntries(Object.entries(sky.fog).filter(([, value]) => value !== undefined)) } }),
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
17
39
|
function isEnvironmentSource(value) {
|
|
18
40
|
return typeof value === "object" && value !== null && "kind" in value && ["gradient", "hdri", "cube"].includes(value.kind);
|
|
19
41
|
}
|
|
20
|
-
function authoredSceneOverlay(document, placement, scatterModels, assets, world, Vfx) {
|
|
42
|
+
function authoredSceneOverlay(document, diagnostics, placement, scatterModels, flockModels, pathKinds, assets, world, Vfx) {
|
|
21
43
|
const terrain = world !== undefined && world.kind === "environment" ? world.terrain : undefined;
|
|
22
44
|
return function AuthoredSceneOverlay(props) {
|
|
23
45
|
const groundColorAt = useMemo(() => terrainGroundColorSampler(terrain, props.ctx.world.ground), [props.ctx.world.ground]);
|
|
24
|
-
return (_jsxs(_Fragment, { children: [_jsx(AuthoredScene, { document: document, field: props.ctx.world.ground, placeObjects: placement, ...(scatterModels === undefined ? {} : { scatterModels,
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx(AuthoredScene, { document: document, diagnostics: diagnostics, field: props.ctx.world.ground, placeObjects: placement, assets: assets, ...(scatterModels === undefined ? {} : { scatterModels }), ...(flockModels === undefined ? {} : { flockModels }), ...(pathKinds === undefined ? {} : { pathKinds }), ...(groundColorAt === undefined ? {} : { groundColorAt }) }), Vfx === undefined ? null : _jsx(Vfx, { ...props })] }));
|
|
25
47
|
};
|
|
26
48
|
}
|
|
27
49
|
const emptyUi = () => null;
|
|
@@ -33,16 +55,22 @@ const emptyUi = () => null;
|
|
|
33
55
|
* @capability define-game single public game-authoring path — compose systems, world, presentation, and authored scene in one definition
|
|
34
56
|
*/
|
|
35
57
|
export function defineGame(config) {
|
|
36
|
-
const { content, loop, GameUI, environment, camera, multiplayer, editorLayers, editorCatalogs, scenePlacement, sceneScatterModels, WorldOverlay, viewmodel, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, nameplates, presentationEffects, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting: authoredLighting, environmentSource, backdrop, look, postProcessing, shadows, graphics, presentation, objectStyles, devtools, capture, platforms, hudFit, ...engineFields } = config;
|
|
58
|
+
const { content, loop, GameUI, environment, camera, multiplayer, editorLayers, editorCatalogs, scenePlacement, sceneScatterModels, sceneFlockModels, scenePathKinds, WorldOverlay, viewmodel, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, gamepad, localPlayers, viewports, orientation, worldHealthBars, nameplates, presentationEffects, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting: authoredLighting, environmentSource, backdrop, look, postProcessing, shadows, graphics, presentation, settings, objectStyles, devtools, capture, platforms, hudFit, ...engineFields } = config;
|
|
37
59
|
const game = defineEngineGame({
|
|
38
60
|
...engineFields,
|
|
61
|
+
...(editorLayers === undefined ? {} : { authoredDocument: editorLayers }),
|
|
39
62
|
multiplayer: multiplayer ?? offline(),
|
|
40
|
-
loop,
|
|
63
|
+
loop: { ...loop, onDispose(ctx) { try {
|
|
64
|
+
loop?.onDispose?.(ctx);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
ctx.environment.dispose();
|
|
68
|
+
} } },
|
|
41
69
|
});
|
|
42
70
|
const resolvedLook = resolveGameLook({
|
|
43
71
|
look,
|
|
44
72
|
lighting: authoredLighting,
|
|
45
|
-
backdrop,
|
|
73
|
+
backdrop: withDocumentSky(backdrop, editorLayers, engineFields.world?.kind === "environment" ? engineFields.world.sky : undefined),
|
|
46
74
|
postProcessing,
|
|
47
75
|
hasWorldSky: editorLayers?.environment?.preset !== undefined,
|
|
48
76
|
});
|
|
@@ -73,7 +101,7 @@ export function defineGame(config) {
|
|
|
73
101
|
editorCatalogs,
|
|
74
102
|
WorldOverlay: editorLayers === undefined
|
|
75
103
|
? WorldOverlay
|
|
76
|
-
: authoredSceneOverlay(editorLayers, scenePlacement ?? true, sceneScatterModels, game.assets, game.world, WorldOverlay),
|
|
104
|
+
: authoredSceneOverlay(editorLayers, devtools !== false, scenePlacement ?? true, sceneScatterModels, sceneFlockModels, scenePathKinds, game.assets, game.world, WorldOverlay),
|
|
77
105
|
viewmodel,
|
|
78
106
|
renderEntity,
|
|
79
107
|
renderObject,
|
|
@@ -84,6 +112,9 @@ export function defineGame(config) {
|
|
|
84
112
|
prompts,
|
|
85
113
|
pointer,
|
|
86
114
|
touch,
|
|
115
|
+
gamepad,
|
|
116
|
+
localPlayers,
|
|
117
|
+
viewports,
|
|
87
118
|
orientation,
|
|
88
119
|
worldHealthBars,
|
|
89
120
|
nameplates,
|
|
@@ -101,6 +132,7 @@ export function defineGame(config) {
|
|
|
101
132
|
shadows,
|
|
102
133
|
graphics,
|
|
103
134
|
presentation,
|
|
135
|
+
settings,
|
|
104
136
|
objectStyles,
|
|
105
137
|
devtools,
|
|
106
138
|
capture,
|
|
@@ -1,78 +1,11 @@
|
|
|
1
|
-
import { type DevtoolsSnapshot } from "@jgengine/core/devtools/devtools";
|
|
2
1
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
2
|
import type { ShellMultiplayer } from "../multiplayer.js";
|
|
4
3
|
import type { PlayableGame } from "../registry.js";
|
|
5
4
|
export { applyStoredDevtoolsOverrides, persistDevtoolsOverrides } from "./devtoolsOverrides.js";
|
|
6
|
-
|
|
7
|
-
export declare function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer;
|
|
5
|
+
export { withDevtoolsLatency } from "./latencyInstrumentation.js";
|
|
8
6
|
/** @internal */
|
|
9
7
|
export declare function DevtoolsRendererProbe(): null;
|
|
10
|
-
|
|
11
|
-
export declare function buildLeanReport(playable: PlayableGame): {
|
|
12
|
-
game: string;
|
|
13
|
-
at: number;
|
|
14
|
-
why: string | null;
|
|
15
|
-
frame: {
|
|
16
|
-
fps: number;
|
|
17
|
-
avgFrameMs: number;
|
|
18
|
-
p95FrameMs: number;
|
|
19
|
-
maxFrameMs: number;
|
|
20
|
-
avgSimMs: number;
|
|
21
|
-
maxSimMs: number;
|
|
22
|
-
avgOutsideMs: number;
|
|
23
|
-
maxOutsideMs: number;
|
|
24
|
-
longFrames: number;
|
|
25
|
-
samples: number;
|
|
26
|
-
phases: {
|
|
27
|
-
name: string;
|
|
28
|
-
avgMs: number;
|
|
29
|
-
maxMs: number;
|
|
30
|
-
pctOfSim: number;
|
|
31
|
-
}[];
|
|
32
|
-
} | null;
|
|
33
|
-
render: import("@jgengine/core/devtools/types").RenderSample | null;
|
|
34
|
-
latency: import("@jgengine/core/devtools/types").LatencyStats | null;
|
|
35
|
-
longFrameSummary: {
|
|
36
|
-
count: number;
|
|
37
|
-
culprits: {
|
|
38
|
-
culprit: string;
|
|
39
|
-
count: number;
|
|
40
|
-
}[];
|
|
41
|
-
maxFrameMs: number;
|
|
42
|
-
maxDrawCalls: number | undefined;
|
|
43
|
-
maxGeometries: number | undefined;
|
|
44
|
-
} | null;
|
|
45
|
-
longFrames: {
|
|
46
|
-
at: number;
|
|
47
|
-
frameMs: number;
|
|
48
|
-
simMs: number;
|
|
49
|
-
outsideMs: number;
|
|
50
|
-
culprit: string;
|
|
51
|
-
reason: string;
|
|
52
|
-
phases: {
|
|
53
|
-
name: string;
|
|
54
|
-
ms: number;
|
|
55
|
-
}[];
|
|
56
|
-
probes: Record<string, unknown>;
|
|
57
|
-
render: import("@jgengine/core/devtools/types").RenderSample | null;
|
|
58
|
-
}[];
|
|
59
|
-
probes: Record<string, unknown>;
|
|
60
|
-
logs: import("@jgengine/core/devtools/types").DevtoolsLogEntry[] | undefined;
|
|
61
|
-
controls: {
|
|
62
|
-
name: string;
|
|
63
|
-
value: unknown;
|
|
64
|
-
initial: unknown;
|
|
65
|
-
}[] | undefined;
|
|
66
|
-
discovered: {
|
|
67
|
-
id: string;
|
|
68
|
-
value: unknown;
|
|
69
|
-
}[] | undefined;
|
|
70
|
-
discoveredCount: number;
|
|
71
|
-
};
|
|
72
|
-
/** @internal */
|
|
73
|
-
export declare function buildFullReport(playable: PlayableGame): DevtoolsSnapshot & {
|
|
74
|
-
game: string;
|
|
75
|
-
};
|
|
8
|
+
export { buildLeanReport, buildFullReport } from "./devtoolsReports.js";
|
|
76
9
|
/** @internal */
|
|
77
10
|
export declare function DevtoolsOverlay({ open, ctx, playable, multiplayer, }: {
|
|
78
11
|
open: boolean;
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
-
import { devtools,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
|
|
8
|
-
import { collisionDebug } from "./collisionDebug.js";
|
|
9
|
-
import { readStoredOverrides } from "./devtoolsOverrides.js";
|
|
10
|
-
import { diagnose } from "./perfDiagnose.js";
|
|
4
|
+
import { devtools, } from "@jgengine/core/devtools/devtools";
|
|
5
|
+
import { heavyModels } from "../render/modelLoad.js";
|
|
6
|
+
import { buildLeanReport } from "./devtoolsReports.js";
|
|
11
7
|
import { PerfPanel } from "./PerfPanel.js";
|
|
12
8
|
import { LogsPanel } from "./LogsPanel.js";
|
|
13
9
|
import { NetPanel } from "./NetPanel.js";
|
|
@@ -16,20 +12,12 @@ import { ColPanel } from "./ColPanel.js";
|
|
|
16
12
|
import { TunePanel } from "./TunePanel.js";
|
|
17
13
|
export { applyStoredDevtoolsOverrides, persistDevtoolsOverrides } from "./devtoolsOverrides.js";
|
|
18
14
|
const REFRESH_MS = 250;
|
|
19
|
-
|
|
20
|
-
export function withDevtoolsLatency(multiplayer) {
|
|
21
|
-
return {
|
|
22
|
-
...multiplayer,
|
|
23
|
-
backend: {
|
|
24
|
-
...instrumentLatency(multiplayer.backend, ["pushFeedEntry"]),
|
|
25
|
-
transport: instrumentLatency(multiplayer.backend.transport, ["joinServer", "leaveServer", "runCommand"]),
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
15
|
+
export { withDevtoolsLatency } from "./latencyInstrumentation.js";
|
|
29
16
|
const RENDER_SAMPLE_MS = 500;
|
|
30
17
|
const RENDER_PROBE_FRAME_PRIORITY = -10;
|
|
31
18
|
/** @internal */
|
|
32
19
|
export function DevtoolsRendererProbe() {
|
|
20
|
+
useEffect(() => devtools.probes.register("heavyModels", () => heavyModels()), []);
|
|
33
21
|
const gl = useThree((state) => state.gl);
|
|
34
22
|
const lastSampleAt = useRef(0);
|
|
35
23
|
useEffect(() => {
|
|
@@ -65,165 +53,25 @@ const TABS = [
|
|
|
65
53
|
{ id: "net", label: "Net" },
|
|
66
54
|
{ id: "keys", label: "Keys" },
|
|
67
55
|
];
|
|
68
|
-
|
|
69
|
-
const scale = 10 ** digits;
|
|
70
|
-
return Math.round(value * scale) / scale;
|
|
71
|
-
}
|
|
72
|
-
function compactLongFrame(event) {
|
|
73
|
-
return {
|
|
74
|
-
at: event.at,
|
|
75
|
-
frameMs: roundMs(event.frameMs),
|
|
76
|
-
simMs: roundMs(event.simMs),
|
|
77
|
-
outsideMs: roundMs(event.outsideMs),
|
|
78
|
-
culprit: event.culprit,
|
|
79
|
-
reason: event.reason,
|
|
80
|
-
phases: event.phases.slice(0, 4).map((phase) => ({
|
|
81
|
-
name: phase.name,
|
|
82
|
-
ms: roundMs(phase.ms),
|
|
83
|
-
})),
|
|
84
|
-
probes: event.probes,
|
|
85
|
-
render: event.render,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function summarizeLongFrames(events) {
|
|
89
|
-
if (events.length === 0)
|
|
90
|
-
return null;
|
|
91
|
-
const byCulprit = new Map();
|
|
92
|
-
let maxFrameMs = 0;
|
|
93
|
-
let maxDraws = 0;
|
|
94
|
-
let maxGeometries = 0;
|
|
95
|
-
for (const event of events) {
|
|
96
|
-
byCulprit.set(event.culprit, (byCulprit.get(event.culprit) ?? 0) + 1);
|
|
97
|
-
maxFrameMs = Math.max(maxFrameMs, event.frameMs);
|
|
98
|
-
if (event.render !== null) {
|
|
99
|
-
maxDraws = Math.max(maxDraws, event.render.drawCalls);
|
|
100
|
-
maxGeometries = Math.max(maxGeometries, event.render.geometries);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const culprits = [...byCulprit.entries()]
|
|
104
|
-
.sort((a, b) => b[1] - a[1])
|
|
105
|
-
.map(([culprit, count]) => ({ culprit, count }));
|
|
106
|
-
return {
|
|
107
|
-
count: events.length,
|
|
108
|
-
culprits,
|
|
109
|
-
maxFrameMs: roundMs(maxFrameMs),
|
|
110
|
-
maxDrawCalls: maxDraws > 0 ? maxDraws : undefined,
|
|
111
|
-
maxGeometries: maxGeometries > 0 ? maxGeometries : undefined,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
/** @internal */
|
|
115
|
-
export function buildLeanReport(playable) {
|
|
116
|
-
const snap = devtools.snapshot();
|
|
117
|
-
const frame = snap.frame;
|
|
118
|
-
const longs = snap.longFrames;
|
|
119
|
-
const why = frame !== null ? diagnose(frame, longs) : null;
|
|
120
|
-
const changedControls = snap.controls
|
|
121
|
-
.filter((control) => !Object.is(control.value, control.initial))
|
|
122
|
-
.map((control) => ({ name: control.name, value: control.value, initial: control.initial }));
|
|
123
|
-
const enabledDiscovered = snap.discovered
|
|
124
|
-
.filter((entry) => entry.enabled)
|
|
125
|
-
.map((entry) => ({ id: entry.id, value: entry.value }));
|
|
126
|
-
const hotLogs = snap.logs.filter((entry) => entry.level === "warn" || entry.level === "error").slice(-20);
|
|
127
|
-
return {
|
|
128
|
-
game: playable.game.name,
|
|
129
|
-
at: snap.at,
|
|
130
|
-
why,
|
|
131
|
-
frame: frame === null
|
|
132
|
-
? null
|
|
133
|
-
: {
|
|
134
|
-
fps: roundMs(frame.fps, 1),
|
|
135
|
-
avgFrameMs: roundMs(frame.avgFrameMs),
|
|
136
|
-
p95FrameMs: roundMs(frame.p95FrameMs),
|
|
137
|
-
maxFrameMs: roundMs(frame.maxFrameMs),
|
|
138
|
-
avgSimMs: roundMs(frame.avgSimMs),
|
|
139
|
-
maxSimMs: roundMs(frame.maxSimMs),
|
|
140
|
-
avgOutsideMs: roundMs(frame.avgOutsideMs),
|
|
141
|
-
maxOutsideMs: roundMs(frame.maxOutsideMs),
|
|
142
|
-
longFrames: frame.longFrames,
|
|
143
|
-
samples: frame.samples,
|
|
144
|
-
phases: frame.phases.slice(0, 8).map((phase) => ({
|
|
145
|
-
name: phase.name,
|
|
146
|
-
avgMs: roundMs(phase.avgMs),
|
|
147
|
-
maxMs: roundMs(phase.maxMs),
|
|
148
|
-
pctOfSim: Math.round(phase.pctOfSim),
|
|
149
|
-
})),
|
|
150
|
-
},
|
|
151
|
-
render: snap.render,
|
|
152
|
-
latency: snap.latency,
|
|
153
|
-
longFrameSummary: summarizeLongFrames(longs),
|
|
154
|
-
longFrames: longs.slice(-6).map(compactLongFrame),
|
|
155
|
-
probes: snap.probes,
|
|
156
|
-
logs: hotLogs.length > 0 ? hotLogs : undefined,
|
|
157
|
-
controls: changedControls.length > 0 ? changedControls : undefined,
|
|
158
|
-
discovered: enabledDiscovered.length > 0 ? enabledDiscovered : undefined,
|
|
159
|
-
discoveredCount: snap.discovered.length,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
/** @internal */
|
|
163
|
-
export function buildFullReport(playable) {
|
|
164
|
-
return { game: playable.game.name, ...devtools.snapshot() };
|
|
165
|
-
}
|
|
56
|
+
export { buildLeanReport, buildFullReport } from "./devtoolsReports.js";
|
|
166
57
|
/** @internal */
|
|
167
58
|
export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
168
59
|
const [tab, setTab] = useState("perf");
|
|
169
60
|
const [, setTick] = useState(0);
|
|
170
61
|
const [copied, setCopied] = useState(false);
|
|
171
62
|
useSyncExternalStore(devtools.signal.subscribe, devtools.signal.version, devtools.signal.version);
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
devtools.logs.captureConsole();
|
|
174
|
-
globalThis.__JG_DEVTOOLS = {
|
|
175
|
-
snapshot: () => buildLeanReport(playable),
|
|
176
|
-
snapshotFull: () => buildFullReport(playable),
|
|
177
|
-
controls: devtools.controls,
|
|
178
|
-
discover: devtools.discover,
|
|
179
|
-
frame: devtools.frame,
|
|
180
|
-
profile: devtools.profile,
|
|
181
|
-
collisionDebug,
|
|
182
|
-
};
|
|
183
|
-
}, [playable]);
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
devtools.discover.scanTable("game", playable);
|
|
186
|
-
devtools.discover.scanTable("engine", { movement: MOVEMENT_TUNING });
|
|
187
|
-
}, [playable]);
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
const stored = readStoredOverrides(playable.game.name);
|
|
190
|
-
if (stored === null)
|
|
191
|
-
return;
|
|
192
|
-
const appliedEnables = new Set();
|
|
193
|
-
const appliedValues = new Set();
|
|
194
|
-
const applyLateRegistrations = () => {
|
|
195
|
-
const discovered = new Map(devtools.discover.list().map((entry) => [entry.id, entry]));
|
|
196
|
-
const needEnable = stored.enabled.filter((id) => !appliedEnables.has(id) && discovered.get(id)?.enabled === false);
|
|
197
|
-
const needValue = Object.entries(stored.values).filter(([name]) => !appliedValues.has(name) && devtools.controls.get(name) !== null);
|
|
198
|
-
if (needEnable.length === 0 && needValue.length === 0)
|
|
199
|
-
return;
|
|
200
|
-
for (const id of needEnable)
|
|
201
|
-
appliedEnables.add(id);
|
|
202
|
-
for (const [name] of needValue)
|
|
203
|
-
appliedValues.add(name);
|
|
204
|
-
devtools.overrides.apply({ enabled: needEnable, values: Object.fromEntries(needValue) });
|
|
205
|
-
};
|
|
206
|
-
applyLateRegistrations();
|
|
207
|
-
return devtools.signal.subscribe(applyLateRegistrations);
|
|
208
|
-
}, [playable]);
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
const disposers = [
|
|
211
|
-
devtools.probes.register("entities", () => ctx.scene.entity.list().length),
|
|
212
|
-
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
213
|
-
devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
|
|
214
|
-
devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
|
|
215
|
-
];
|
|
216
|
-
return () => {
|
|
217
|
-
for (const dispose of disposers)
|
|
218
|
-
dispose();
|
|
219
|
-
};
|
|
220
|
-
}, [ctx]);
|
|
221
63
|
useEffect(() => {
|
|
222
64
|
if (!open)
|
|
223
65
|
return;
|
|
224
|
-
|
|
225
|
-
return () =>
|
|
226
|
-
|
|
66
|
+
let lastRefresh = 0;
|
|
67
|
+
return ctx.subscribe(() => {
|
|
68
|
+
const now = performance.now();
|
|
69
|
+
if (now - lastRefresh < REFRESH_MS)
|
|
70
|
+
return;
|
|
71
|
+
lastRefresh = now;
|
|
72
|
+
setTick((current) => current + 1);
|
|
73
|
+
});
|
|
74
|
+
}, [ctx, open]);
|
|
227
75
|
if (!open)
|
|
228
76
|
return null;
|
|
229
77
|
const copyReport = () => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { PlayableGame } from "../registry.js";
|
|
3
|
+
/** Keeps saved overrides and diagnostic registration live while the visual panel is closed. @internal */
|
|
4
|
+
export declare function DevtoolsRuntime({ ctx, playable }: {
|
|
5
|
+
ctx: GameContext;
|
|
6
|
+
playable: PlayableGame;
|
|
7
|
+
}): null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
3
|
+
import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
|
|
4
|
+
import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors";
|
|
5
|
+
import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
|
|
6
|
+
import { collisionDebug } from "./collisionDebug.js";
|
|
7
|
+
import { readStoredOverrides } from "./devtoolsOverrides.js";
|
|
8
|
+
import { buildLeanReport, buildFullReport } from "./devtoolsReports.js";
|
|
9
|
+
/** Keeps saved overrides and diagnostic registration live while the visual panel is closed. @internal */
|
|
10
|
+
export function DevtoolsRuntime({ ctx, playable }) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
devtools.logs.captureConsole();
|
|
13
|
+
globalThis.__JG_DEVTOOLS = {
|
|
14
|
+
snapshot: () => buildLeanReport(playable),
|
|
15
|
+
snapshotFull: () => buildFullReport(playable),
|
|
16
|
+
controls: devtools.controls,
|
|
17
|
+
discover: devtools.discover,
|
|
18
|
+
frame: devtools.frame,
|
|
19
|
+
profile: devtools.profile,
|
|
20
|
+
collisionDebug,
|
|
21
|
+
};
|
|
22
|
+
}, [playable]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
devtools.discover.scanTable("game", playable);
|
|
25
|
+
devtools.discover.scanTable("engine", { movement: MOVEMENT_TUNING });
|
|
26
|
+
}, [playable]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const stored = readStoredOverrides(playable.game.name);
|
|
29
|
+
if (stored === null)
|
|
30
|
+
return;
|
|
31
|
+
const appliedEnables = new Set();
|
|
32
|
+
const appliedValues = new Set();
|
|
33
|
+
const applyLateRegistrations = () => {
|
|
34
|
+
const discovered = new Map(devtools.discover.list().map((entry) => [entry.id, entry]));
|
|
35
|
+
const needEnable = stored.enabled.filter((id) => !appliedEnables.has(id) && discovered.get(id)?.enabled === false);
|
|
36
|
+
const needValue = Object.entries(stored.values).filter(([name]) => !appliedValues.has(name) && devtools.controls.get(name) !== null);
|
|
37
|
+
if (needEnable.length === 0 && needValue.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
for (const id of needEnable)
|
|
40
|
+
appliedEnables.add(id);
|
|
41
|
+
for (const [name] of needValue)
|
|
42
|
+
appliedValues.add(name);
|
|
43
|
+
devtools.overrides.apply({ enabled: needEnable, values: Object.fromEntries(needValue) });
|
|
44
|
+
};
|
|
45
|
+
applyLateRegistrations();
|
|
46
|
+
return devtools.signal.subscribe(applyLateRegistrations);
|
|
47
|
+
}, [playable]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const disposers = [
|
|
50
|
+
devtools.probes.register("entities", () => ctx.scene.entity.list().length),
|
|
51
|
+
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
52
|
+
devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
|
|
53
|
+
devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
|
|
54
|
+
];
|
|
55
|
+
return () => {
|
|
56
|
+
for (const dispose of disposers)
|
|
57
|
+
dispose();
|
|
58
|
+
};
|
|
59
|
+
}, [ctx]);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
@@ -15,6 +15,12 @@ type EditorHost = {
|
|
|
15
15
|
gameId: string;
|
|
16
16
|
handle(request: AgentBridgeRequest): unknown;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
* Resolves true once `isLive()` reports a mounted editor host, polling every `intervalMs` up to
|
|
21
|
+
* `timeoutMs`. `editor_summon` awaits this so the next RPC verb in a `drive` batch finds the editor.
|
|
22
|
+
*/
|
|
23
|
+
export declare function waitForEditorHost(isLive: () => boolean, timeoutMs?: number, intervalMs?: number, sleep?: (ms: number) => Promise<void>): Promise<boolean>;
|
|
18
24
|
declare global {
|
|
19
25
|
interface Window {
|
|
20
26
|
__jgengineAgent?: {
|