@jgengine/shell 0.10.0 → 0.12.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +135 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BackdropConfig, LightingConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
export declare function ConfiguredLighting({ lighting }: {
|
|
3
|
+
lighting: LightingConfig;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
export declare function BackdropFog({ fog }: {
|
|
6
|
+
fog: BackdropConfig["fog"];
|
|
7
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
3
|
+
function DirectionalShadowLight({ entry }) {
|
|
4
|
+
const size = entry.shadowCameraSize ?? 40;
|
|
5
|
+
return (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": entry.shadowMapSize ?? 1024, "shadow-mapSize-height": entry.shadowMapSize ?? 1024, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": Math.max(200, size * 6), "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
|
|
6
|
+
}
|
|
7
|
+
export function ConfiguredLighting({ lighting }) {
|
|
8
|
+
return (_jsxs(_Fragment, { children: [lighting.ambient !== undefined ? (_jsx("ambientLight", { color: lighting.ambient.color, intensity: lighting.ambient.intensity ?? 0.55 })) : null, lighting.hemisphere !== undefined ? (_jsx("hemisphereLight", { args: [
|
|
9
|
+
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
10
|
+
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
11
|
+
lighting.hemisphere.intensity ?? 0.55,
|
|
12
|
+
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
|
|
13
|
+
}
|
|
14
|
+
export function BackdropFog({ fog }) {
|
|
15
|
+
if (fog === undefined)
|
|
16
|
+
return null;
|
|
17
|
+
return fog.density !== undefined ? (_jsx("fogExp2", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.density] })) : (_jsx("fog", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.near ?? 10, fog.far ?? 200] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { EntitySpriteConfig, ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
export declare function EntitySprite({ sprite }: {
|
|
4
|
+
sprite: EntitySpriteConfig;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function IsolatedEntityModel({ model, instanceId, fallback, }: {
|
|
7
|
+
model: ModelConfig;
|
|
8
|
+
instanceId?: string;
|
|
9
|
+
fallback?: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export declare function EntityModel({ model, instanceId }: {
|
|
12
|
+
model: ModelConfig;
|
|
13
|
+
instanceId?: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useLoader } from "@react-three/fiber";
|
|
3
|
+
import { useTexture } from "@react-three/drei";
|
|
4
|
+
import { Component, Suspense, useEffect, useMemo, useRef } from "react";
|
|
5
|
+
import * as THREE from "three";
|
|
6
|
+
import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
|
|
7
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
8
|
+
import { sharedGltfLoader } from "./modelLoad.js";
|
|
9
|
+
import { applyMaterialOverride } from "../materialOverride.js";
|
|
10
|
+
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
|
|
11
|
+
export function EntitySprite({ sprite }) {
|
|
12
|
+
const texture = useLoader(THREE.TextureLoader, sprite.url);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
15
|
+
texture.anisotropy = 4;
|
|
16
|
+
texture.needsUpdate = true;
|
|
17
|
+
}, [texture]);
|
|
18
|
+
return (_jsx("sprite", { "position-y": sprite.y, scale: [sprite.width, sprite.height, 1], children: _jsx("spriteMaterial", { map: texture, transparent: true, alphaTest: 0.08, depthWrite: false }) }));
|
|
19
|
+
}
|
|
20
|
+
class ModelFallbackBoundary extends Component {
|
|
21
|
+
state = { failed: false };
|
|
22
|
+
static getDerivedStateFromError() {
|
|
23
|
+
return { failed: true };
|
|
24
|
+
}
|
|
25
|
+
render() {
|
|
26
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function IsolatedEntityModel({ model, instanceId, fallback, }) {
|
|
30
|
+
return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
|
|
31
|
+
}
|
|
32
|
+
function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
|
|
33
|
+
const gltf = useLoader(sharedGltfLoader, model.url);
|
|
34
|
+
const weaponScene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
|
|
35
|
+
const px = position?.[0] ?? 0;
|
|
36
|
+
const py = position?.[1] ?? 0;
|
|
37
|
+
const pz = position?.[2] ?? 0;
|
|
38
|
+
const rx = rotation?.[0] ?? 0;
|
|
39
|
+
const ry = rotation?.[1] ?? 0;
|
|
40
|
+
const rz = rotation?.[2] ?? 0;
|
|
41
|
+
const s = scale ?? 1;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const bone = rig.getObjectByName(slot);
|
|
44
|
+
if (bone === undefined) {
|
|
45
|
+
if (typeof console !== "undefined") {
|
|
46
|
+
console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
weaponScene.position.set(px, py, pz);
|
|
51
|
+
weaponScene.rotation.set(rx, ry, rz);
|
|
52
|
+
weaponScene.scale.setScalar(s);
|
|
53
|
+
bone.add(weaponScene);
|
|
54
|
+
return () => {
|
|
55
|
+
bone.remove(weaponScene);
|
|
56
|
+
};
|
|
57
|
+
}, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
|
|
58
|
+
useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** Renders a static kit-of-parts child at a fixed local offset under the parent model — no bone/rig lookup, unlike `BoneAttachment`. The nested `EntityModel` still applies its own `dims`/anchor centering, so scale/pivot sanity carries over per part. */
|
|
62
|
+
function ModelPartGroup({ model, position, rotation, scale, }) {
|
|
63
|
+
return (_jsx("group", { position: position ?? [0, 0, 0], rotation: rotation ?? [0, 0, 0], scale: scale ?? 1, children: _jsx(EntityModel, { model: model }) }));
|
|
64
|
+
}
|
|
65
|
+
function ModelMaterialMapsApplier({ scene, maps }) {
|
|
66
|
+
const entries = useMemo(() => {
|
|
67
|
+
const record = {};
|
|
68
|
+
if (maps.color !== undefined)
|
|
69
|
+
record.color = maps.color;
|
|
70
|
+
if (maps.normal !== undefined)
|
|
71
|
+
record.normal = maps.normal;
|
|
72
|
+
if (maps.roughness !== undefined)
|
|
73
|
+
record.roughness = maps.roughness;
|
|
74
|
+
if (maps.ao !== undefined)
|
|
75
|
+
record.ao = maps.ao;
|
|
76
|
+
return record;
|
|
77
|
+
}, [maps.color, maps.normal, maps.roughness, maps.ao]);
|
|
78
|
+
const textures = useTexture(entries);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (textures.color !== undefined)
|
|
81
|
+
textures.color.colorSpace = THREE.SRGBColorSpace;
|
|
82
|
+
applyMaterialOverride(scene, {}, { clone: false, textures });
|
|
83
|
+
}, [scene, textures]);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
export function EntityModel({ model, instanceId }) {
|
|
87
|
+
const gltf = useLoader(sharedGltfLoader, model.url);
|
|
88
|
+
const ctx = useGameContext();
|
|
89
|
+
const material = model.material;
|
|
90
|
+
const baseY = model.y ?? 0;
|
|
91
|
+
const dims = model.dims;
|
|
92
|
+
const scene = useMemo(() => {
|
|
93
|
+
const cloned = cloneModelScene(gltf.scene);
|
|
94
|
+
if (material !== undefined)
|
|
95
|
+
applyMaterialOverride(cloned, material, { clone: false });
|
|
96
|
+
return cloned;
|
|
97
|
+
}, [gltf, material]);
|
|
98
|
+
const measured = useMemo(() => {
|
|
99
|
+
if (model.targetHeight === undefined)
|
|
100
|
+
return null;
|
|
101
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
102
|
+
const height = box.max.y - box.min.y;
|
|
103
|
+
if (!Number.isFinite(height) || height <= 0)
|
|
104
|
+
return null;
|
|
105
|
+
return {
|
|
106
|
+
normalize: model.targetHeight / height,
|
|
107
|
+
minY: box.min.y,
|
|
108
|
+
centerX: (box.min.x + box.max.x) / 2,
|
|
109
|
+
centerZ: (box.min.z + box.max.z) / 2,
|
|
110
|
+
};
|
|
111
|
+
}, [scene, model.targetHeight]);
|
|
112
|
+
const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
|
|
113
|
+
const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
|
|
114
|
+
const position = measured !== null
|
|
115
|
+
? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
|
|
116
|
+
: centered
|
|
117
|
+
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
118
|
+
: [0, baseY, 0];
|
|
119
|
+
useEffect(() => () => {
|
|
120
|
+
disposeClonedMaterials(scene);
|
|
121
|
+
}, [scene]);
|
|
122
|
+
const animation = model.animation;
|
|
123
|
+
const mixerRef = useRef(null);
|
|
124
|
+
const animationPausedRef = useRef(false);
|
|
125
|
+
const stateActionsRef = useRef(null);
|
|
126
|
+
const states = animation?.states;
|
|
127
|
+
const oneShots = animation?.oneShots;
|
|
128
|
+
const oneShotPlayRef = useRef(null);
|
|
129
|
+
const activeOneShotRef = useRef(null);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (animation === undefined || gltf.animations.length === 0) {
|
|
132
|
+
mixerRef.current = null;
|
|
133
|
+
stateActionsRef.current = null;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const mixer = new THREE.AnimationMixer(scene);
|
|
137
|
+
if (states !== undefined) {
|
|
138
|
+
const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
|
|
139
|
+
const actions = {
|
|
140
|
+
idle: mixer.clipAction(clipFor(states.idle)),
|
|
141
|
+
walk: mixer.clipAction(clipFor(states.walk)),
|
|
142
|
+
...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
|
|
143
|
+
};
|
|
144
|
+
for (const action of Object.values(actions)) {
|
|
145
|
+
action.setLoop(THREE.LoopRepeat, Infinity);
|
|
146
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
147
|
+
action.enabled = true;
|
|
148
|
+
}
|
|
149
|
+
actions.idle.play();
|
|
150
|
+
mixer.update(0);
|
|
151
|
+
mixerRef.current = mixer;
|
|
152
|
+
stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
|
|
153
|
+
animationPausedRef.current = false;
|
|
154
|
+
let onOneShotFinished = null;
|
|
155
|
+
if (oneShots !== undefined) {
|
|
156
|
+
const clipNames = new Set();
|
|
157
|
+
for (const spec of Object.values(oneShots)) {
|
|
158
|
+
if (typeof spec === "string")
|
|
159
|
+
clipNames.add(spec);
|
|
160
|
+
else
|
|
161
|
+
for (const name of spec)
|
|
162
|
+
clipNames.add(name);
|
|
163
|
+
}
|
|
164
|
+
const oneShotActions = new Map();
|
|
165
|
+
for (const name of clipNames) {
|
|
166
|
+
const found = THREE.AnimationClip.findByName(gltf.animations, name);
|
|
167
|
+
if (found === null)
|
|
168
|
+
continue;
|
|
169
|
+
const oneShotAction = mixer.clipAction(found);
|
|
170
|
+
oneShotAction.setLoop(THREE.LoopOnce, 1);
|
|
171
|
+
oneShotAction.enabled = true;
|
|
172
|
+
oneShotActions.set(name, oneShotAction);
|
|
173
|
+
}
|
|
174
|
+
onOneShotFinished = ({ action }) => {
|
|
175
|
+
const active = activeOneShotRef.current;
|
|
176
|
+
if (active === null || action !== active.action || active.isDeath)
|
|
177
|
+
return;
|
|
178
|
+
const machine = stateActionsRef.current;
|
|
179
|
+
const back = machine?.actions[machine.active];
|
|
180
|
+
action.fadeOut(0.15);
|
|
181
|
+
if (back !== undefined)
|
|
182
|
+
back.reset().fadeIn(0.15).play();
|
|
183
|
+
activeOneShotRef.current = null;
|
|
184
|
+
};
|
|
185
|
+
mixer.addEventListener("finished", onOneShotFinished);
|
|
186
|
+
oneShotPlayRef.current = (event) => {
|
|
187
|
+
const active = activeOneShotRef.current;
|
|
188
|
+
if (active !== null && active.isDeath)
|
|
189
|
+
return;
|
|
190
|
+
const clipName = resolveOneShotClip(oneShots, event, Math.random());
|
|
191
|
+
if (clipName === null)
|
|
192
|
+
return;
|
|
193
|
+
const oneShotAction = oneShotActions.get(clipName);
|
|
194
|
+
if (oneShotAction === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const machine = stateActionsRef.current;
|
|
197
|
+
machine?.actions[machine.active]?.fadeOut(0.1);
|
|
198
|
+
if (active !== null && active.action !== oneShotAction)
|
|
199
|
+
active.action.stop();
|
|
200
|
+
oneShotAction.clampWhenFinished = event === "death";
|
|
201
|
+
oneShotAction.reset().fadeIn(0.1).play();
|
|
202
|
+
activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return () => {
|
|
206
|
+
if (onOneShotFinished !== null)
|
|
207
|
+
mixer.removeEventListener("finished", onOneShotFinished);
|
|
208
|
+
oneShotPlayRef.current = null;
|
|
209
|
+
activeOneShotRef.current = null;
|
|
210
|
+
mixer.stopAllAction();
|
|
211
|
+
mixerRef.current = null;
|
|
212
|
+
stateActionsRef.current = null;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
|
|
216
|
+
gltf.animations[0];
|
|
217
|
+
const action = mixer.clipAction(clip);
|
|
218
|
+
action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
|
|
219
|
+
action.clampWhenFinished = animation.loop === false;
|
|
220
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
221
|
+
action.enabled = true;
|
|
222
|
+
action.paused = animation.paused === true;
|
|
223
|
+
action.play();
|
|
224
|
+
if (animation.time !== undefined)
|
|
225
|
+
action.time = animation.time;
|
|
226
|
+
mixer.update(0);
|
|
227
|
+
mixerRef.current = mixer;
|
|
228
|
+
animationPausedRef.current = animation.paused === true;
|
|
229
|
+
return () => {
|
|
230
|
+
mixer.stopAllAction();
|
|
231
|
+
mixerRef.current = null;
|
|
232
|
+
};
|
|
233
|
+
}, [
|
|
234
|
+
scene,
|
|
235
|
+
gltf,
|
|
236
|
+
animation?.clip,
|
|
237
|
+
animation?.loop,
|
|
238
|
+
animation?.timeScale,
|
|
239
|
+
animation?.paused,
|
|
240
|
+
animation?.time,
|
|
241
|
+
states,
|
|
242
|
+
oneShots,
|
|
243
|
+
]);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (instanceId === undefined || oneShots === undefined)
|
|
246
|
+
return;
|
|
247
|
+
const fire = (event) => oneShotPlayRef.current?.(event);
|
|
248
|
+
const offAnimation = ctx.game.events.on("entity.animation", (event) => {
|
|
249
|
+
if (event.instanceId === instanceId)
|
|
250
|
+
fire(event.event);
|
|
251
|
+
});
|
|
252
|
+
const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
|
|
253
|
+
if (event.instanceId === instanceId)
|
|
254
|
+
fire("hit");
|
|
255
|
+
});
|
|
256
|
+
const offDied = ctx.game.events.on("entity.died", (event) => {
|
|
257
|
+
if (event.instanceId === instanceId)
|
|
258
|
+
fire("death");
|
|
259
|
+
});
|
|
260
|
+
return () => {
|
|
261
|
+
offAnimation();
|
|
262
|
+
offHit();
|
|
263
|
+
offDied();
|
|
264
|
+
};
|
|
265
|
+
}, [ctx, instanceId, oneShots]);
|
|
266
|
+
const paintCanvasRef = useRef(null);
|
|
267
|
+
const paintDrawnCountRef = useRef(0);
|
|
268
|
+
const paintVersionRef = useRef(-1);
|
|
269
|
+
const materialCacheRef = useRef(null);
|
|
270
|
+
useEffect(() => () => {
|
|
271
|
+
if (paintCanvasRef.current !== null) {
|
|
272
|
+
disposePaintCanvas(paintCanvasRef.current);
|
|
273
|
+
paintCanvasRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
paintDrawnCountRef.current = 0;
|
|
276
|
+
paintVersionRef.current = -1;
|
|
277
|
+
materialCacheRef.current = null;
|
|
278
|
+
}, [scene]);
|
|
279
|
+
useFrame((_state, delta) => {
|
|
280
|
+
const stateMachine = stateActionsRef.current;
|
|
281
|
+
if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
|
|
282
|
+
const entity = ctx.scene.entity.get(instanceId);
|
|
283
|
+
if (entity !== null) {
|
|
284
|
+
const [x, , z] = entity.position;
|
|
285
|
+
if (stateMachine.lastPos !== null) {
|
|
286
|
+
const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
|
|
287
|
+
stateMachine.smoothedSpeed +=
|
|
288
|
+
(instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
|
|
289
|
+
}
|
|
290
|
+
stateMachine.lastPos = [x, entity.position[1], z];
|
|
291
|
+
const walkSpeed = states.walkSpeed ?? 0.5;
|
|
292
|
+
const runSpeed = states.runSpeed ?? 6;
|
|
293
|
+
const next = stateMachine.smoothedSpeed < walkSpeed
|
|
294
|
+
? "idle"
|
|
295
|
+
: stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
|
|
296
|
+
? "run"
|
|
297
|
+
: "walk";
|
|
298
|
+
if (next !== stateMachine.active) {
|
|
299
|
+
if (activeOneShotRef.current === null) {
|
|
300
|
+
const fade = states.fadeSec ?? 0.2;
|
|
301
|
+
const from = stateMachine.actions[stateMachine.active];
|
|
302
|
+
const to = stateMachine.actions[next];
|
|
303
|
+
if (from !== undefined && to !== undefined) {
|
|
304
|
+
to.reset().fadeIn(fade).play();
|
|
305
|
+
from.fadeOut(fade);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
stateMachine.active = next;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
313
|
+
mixerRef.current.update(delta);
|
|
314
|
+
if (instanceId === undefined)
|
|
315
|
+
return;
|
|
316
|
+
const paint = ctx.scene.entity.paint;
|
|
317
|
+
const version = paint.version(instanceId);
|
|
318
|
+
if (version === paintVersionRef.current)
|
|
319
|
+
return;
|
|
320
|
+
paintVersionRef.current = version;
|
|
321
|
+
const strokes = paint.strokes(instanceId);
|
|
322
|
+
const cache = cacheStandardMaterials(scene, materialCacheRef.current);
|
|
323
|
+
materialCacheRef.current = cache;
|
|
324
|
+
if (paintCanvasRef.current === null) {
|
|
325
|
+
if (strokes.length === 0)
|
|
326
|
+
return;
|
|
327
|
+
const seed = cache.materials[0];
|
|
328
|
+
if (seed === undefined)
|
|
329
|
+
return;
|
|
330
|
+
const paintCanvas = createPaintCanvas(seed);
|
|
331
|
+
paintCanvasRef.current = paintCanvas;
|
|
332
|
+
applyPaintTextureToMaterials(cache.materials, paintCanvas);
|
|
333
|
+
}
|
|
334
|
+
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
335
|
+
});
|
|
336
|
+
return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), material?.maps !== undefined ? _jsx(ModelMaterialMapsApplier, { scene: scene, maps: material.maps }) : null, (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`))), (model.parts ?? []).map((part, index) => typeof part.model === "string" ? null : (_jsx(ModelPartGroup, { model: part.model, position: part.position, rotation: part.rotation, scale: part.scale }, index)))] }));
|
|
337
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
|
|
3
|
+
* installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
|
|
4
|
+
* `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
|
|
5
|
+
* as the URL modifier on THREE loading managers so every model and texture
|
|
6
|
+
* load resolves correctly wherever the app is mounted.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveAssetBaseUrl(url: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
|
|
11
|
+
* asset paths load from under it. Call once at app startup, before any game
|
|
12
|
+
* loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
|
|
13
|
+
* default. Also registers {@link resolveAssetBaseUrl} on
|
|
14
|
+
* `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
|
|
15
|
+
* / `useGLTF`, and every other loader on the default manager.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installAssetBase(base: string): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* The app base URL every root-absolute asset path resolves under. `/` (the
|
|
4
|
+
* default) leaves URLs untouched — standalone game runners serve assets from
|
|
5
|
+
* the site root. The website serves the runner under `/play/`, where a game's
|
|
6
|
+
* `/models/…` or `/materials/…` must load from `/play/models/…`.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
let assetBase = "/";
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
|
|
12
|
+
* installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
|
|
13
|
+
* `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
|
|
14
|
+
* as the URL modifier on THREE loading managers so every model and texture
|
|
15
|
+
* load resolves correctly wherever the app is mounted.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveAssetBaseUrl(url) {
|
|
18
|
+
if (assetBase === "/" || !url.startsWith("/") || url.startsWith("//") || url.startsWith(assetBase)) {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
return `${assetBase.replace(/\/+$/, "")}${url}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
|
|
25
|
+
* asset paths load from under it. Call once at app startup, before any game
|
|
26
|
+
* loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
|
|
27
|
+
* default. Also registers {@link resolveAssetBaseUrl} on
|
|
28
|
+
* `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
|
|
29
|
+
* / `useGLTF`, and every other loader on the default manager.
|
|
30
|
+
*/
|
|
31
|
+
export function installAssetBase(base) {
|
|
32
|
+
const normalized = base.endsWith("/") ? base : `${base}/`;
|
|
33
|
+
assetBase = normalized.startsWith("/") && !normalized.startsWith("//") ? normalized : "/";
|
|
34
|
+
THREE.DefaultLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
/**
|
|
4
|
+
* Shared, lazily-built procedural detail maps (a subtle tangent-space normal map plus a
|
|
5
|
+
* roughness-variation map) used by every un-modeled primitive so a flat colored box reads as a
|
|
6
|
+
* surface, not plastic. Pure JS `DataTexture` — no canvas/DOM, so it imports safely under `bun test`.
|
|
7
|
+
* @internal shell-internal default render surface; games never import it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detailMaps(): {
|
|
10
|
+
normal: THREE.DataTexture;
|
|
11
|
+
roughness: THREE.DataTexture;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Props for {@link DefaultSurface}: a required base `color` plus optional PBR overrides.
|
|
15
|
+
* @internal shell-internal default render surface; games never import it.
|
|
16
|
+
*/
|
|
17
|
+
export interface DefaultSurfaceProps extends Omit<ThreeElements["meshStandardMaterial"], "args" | "normalMap" | "roughnessMap"> {
|
|
18
|
+
color: THREE.ColorRepresentation;
|
|
19
|
+
roughness?: number;
|
|
20
|
+
metalness?: number;
|
|
21
|
+
detail?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The upgraded default material for un-modeled entities/objects: tuned PBR roughness/metalness plus
|
|
25
|
+
* the shared procedural detail maps, so primitive fallbacks catch light and image-based reflections
|
|
26
|
+
* instead of reading as flat plastic. Drop-in replacement for a bare `<meshStandardMaterial color>`.
|
|
27
|
+
* @internal shell-internal default render surface; games never import it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function DefaultSurface({ color, roughness, metalness, detail, ...rest }: DefaultSurfaceProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "@react-three/fiber";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
const DETAIL_SIZE = 64;
|
|
6
|
+
const DETAIL_REPEAT = 3;
|
|
7
|
+
const NORMAL_STRENGTH = 2.2;
|
|
8
|
+
const ROUGHNESS_SPREAD = 0.14;
|
|
9
|
+
function periodicValueNoise(size, cells, seed) {
|
|
10
|
+
const lattice = new Float32Array(cells * cells);
|
|
11
|
+
for (let index = 0; index < lattice.length; index += 1) {
|
|
12
|
+
const h = Math.sin((index + 1) * 12.9898 + seed * 78.233) * 43758.5453;
|
|
13
|
+
lattice[index] = h - Math.floor(h);
|
|
14
|
+
}
|
|
15
|
+
const at = (cx, cy) => lattice[((cy % cells) + cells) % cells * cells + (((cx % cells) + cells) % cells)];
|
|
16
|
+
const field = new Float32Array(size * size);
|
|
17
|
+
const step = cells / size;
|
|
18
|
+
for (let y = 0; y < size; y += 1) {
|
|
19
|
+
for (let x = 0; x < size; x += 1) {
|
|
20
|
+
const fx = x * step;
|
|
21
|
+
const fy = y * step;
|
|
22
|
+
const x0 = Math.floor(fx);
|
|
23
|
+
const y0 = Math.floor(fy);
|
|
24
|
+
const tx = fx - x0;
|
|
25
|
+
const ty = fy - y0;
|
|
26
|
+
const sx = tx * tx * (3 - 2 * tx);
|
|
27
|
+
const sy = ty * ty * (3 - 2 * ty);
|
|
28
|
+
const top = at(x0, y0) * (1 - sx) + at(x0 + 1, y0) * sx;
|
|
29
|
+
const bottom = at(x0, y0 + 1) * (1 - sx) + at(x0 + 1, y0 + 1) * sx;
|
|
30
|
+
field[y * size + x] = top * (1 - sy) + bottom * sy;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return field;
|
|
34
|
+
}
|
|
35
|
+
function heightField(size) {
|
|
36
|
+
const coarse = periodicValueNoise(size, 4, 1);
|
|
37
|
+
const medium = periodicValueNoise(size, 8, 7);
|
|
38
|
+
const fine = periodicValueNoise(size, 16, 13);
|
|
39
|
+
const field = new Float32Array(size * size);
|
|
40
|
+
for (let index = 0; index < field.length; index += 1) {
|
|
41
|
+
field[index] = coarse[index] * 0.55 + medium[index] * 0.3 + fine[index] * 0.15;
|
|
42
|
+
}
|
|
43
|
+
return field;
|
|
44
|
+
}
|
|
45
|
+
let cached = null;
|
|
46
|
+
/**
|
|
47
|
+
* Shared, lazily-built procedural detail maps (a subtle tangent-space normal map plus a
|
|
48
|
+
* roughness-variation map) used by every un-modeled primitive so a flat colored box reads as a
|
|
49
|
+
* surface, not plastic. Pure JS `DataTexture` — no canvas/DOM, so it imports safely under `bun test`.
|
|
50
|
+
* @internal shell-internal default render surface; games never import it.
|
|
51
|
+
*/
|
|
52
|
+
export function detailMaps() {
|
|
53
|
+
if (cached !== null)
|
|
54
|
+
return cached;
|
|
55
|
+
const size = DETAIL_SIZE;
|
|
56
|
+
const height = heightField(size);
|
|
57
|
+
const sample = (x, y) => height[(((y % size) + size) % size) * size + (((x % size) + size) % size)];
|
|
58
|
+
const normalData = new Uint8Array(size * size * 4);
|
|
59
|
+
const roughnessData = new Uint8Array(size * size * 4);
|
|
60
|
+
for (let y = 0; y < size; y += 1) {
|
|
61
|
+
for (let x = 0; x < size; x += 1) {
|
|
62
|
+
const dx = (sample(x - 1, y) - sample(x + 1, y)) * NORMAL_STRENGTH;
|
|
63
|
+
const dy = (sample(x, y - 1) - sample(x, y + 1)) * NORMAL_STRENGTH;
|
|
64
|
+
const length = Math.hypot(dx, dy, 1);
|
|
65
|
+
const offset = (y * size + x) * 4;
|
|
66
|
+
normalData[offset] = Math.round((dx / length * 0.5 + 0.5) * 255);
|
|
67
|
+
normalData[offset + 1] = Math.round((dy / length * 0.5 + 0.5) * 255);
|
|
68
|
+
normalData[offset + 2] = Math.round((1 / length * 0.5 + 0.5) * 255);
|
|
69
|
+
normalData[offset + 3] = 255;
|
|
70
|
+
const rough = Math.round((0.5 + (sample(x, y) - 0.5) * 2 * ROUGHNESS_SPREAD * 0.5) * 255);
|
|
71
|
+
roughnessData[offset] = rough;
|
|
72
|
+
roughnessData[offset + 1] = rough;
|
|
73
|
+
roughnessData[offset + 2] = rough;
|
|
74
|
+
roughnessData[offset + 3] = 255;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const build = (data) => {
|
|
78
|
+
const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
|
|
79
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
80
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
81
|
+
texture.repeat.set(DETAIL_REPEAT, DETAIL_REPEAT);
|
|
82
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
83
|
+
texture.magFilter = THREE.LinearFilter;
|
|
84
|
+
texture.generateMipmaps = true;
|
|
85
|
+
texture.anisotropy = 4;
|
|
86
|
+
texture.needsUpdate = true;
|
|
87
|
+
return texture;
|
|
88
|
+
};
|
|
89
|
+
cached = { normal: build(normalData), roughness: build(roughnessData) };
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The upgraded default material for un-modeled entities/objects: tuned PBR roughness/metalness plus
|
|
94
|
+
* the shared procedural detail maps, so primitive fallbacks catch light and image-based reflections
|
|
95
|
+
* instead of reading as flat plastic. Drop-in replacement for a bare `<meshStandardMaterial color>`.
|
|
96
|
+
* @internal shell-internal default render surface; games never import it.
|
|
97
|
+
*/
|
|
98
|
+
export function DefaultSurface({ color, roughness = 0.72, metalness = 0.04, detail = true, ...rest }) {
|
|
99
|
+
const maps = useMemo(() => (detail ? detailMaps() : null), [detail]);
|
|
100
|
+
return (_jsx("meshStandardMaterial", { color: color, roughness: roughness, metalness: metalness, envMapIntensity: 0.6, normalMap: maps?.normal ?? null, normalScale: maps === null ? undefined : new THREE.Vector2(0.6, 0.6), roughnessMap: maps?.roughness ?? null, ...rest }));
|
|
101
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
2
|
+
import { type AssetLoadDiagnosis } from "@jgengine/core/scene/assetDiagnostics";
|
|
3
|
+
/**
|
|
4
|
+
* Re-fetch a model URL that just failed to load and classify what actually came
|
|
5
|
+
* back — a 404, a dev-server HTML fallback, a corrupt or non-model body — so the
|
|
6
|
+
* runtime can replace an opaque GLTF parse error with a diagnosis naming the
|
|
7
|
+
* broken asset contract. Runs only on the error path (loads rarely fail), and
|
|
8
|
+
* never throws: a failed probe returns a `missing` diagnosis instead.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function probeModelUrl(url: string, logicalId?: string, fetchImpl?: typeof fetch): Promise<AssetLoadDiagnosis>;
|
|
12
|
+
/**
|
|
13
|
+
* A {@link GLTFLoader} whose success path is unchanged but whose failures are
|
|
14
|
+
* enriched: when a load errors, it probes the URL and rejects with an
|
|
15
|
+
* {@link AssetLoadDiagnosis}-derived message (missing / HTML fallback / corrupt /
|
|
16
|
+
* unsupported) instead of the raw parse error. Drop-in for the shared loader, so
|
|
17
|
+
* `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare class DiagnosticGLTFLoader extends GLTFLoader {
|
|
21
|
+
load(url: string, onLoad: (gltf: GLTF) => void, onProgress?: (event: ProgressEvent) => void, onError?: (event: unknown) => void): void;
|
|
22
|
+
}
|
|
23
|
+
/** Shared GLTF loader used by shell entity/object model mounts. @internal */
|
|
24
|
+
export declare const sharedGltfLoader: DiagnosticGLTFLoader;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
|
+
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
4
|
+
import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
5
|
+
import { resolveAssetBaseUrl } from "./assetBase.js";
|
|
6
|
+
/**
|
|
7
|
+
* Dedicated LoadingManager for every GLB load instead of THREE.DefaultLoadingManager.
|
|
8
|
+
* The shared default manager is process-wide; under repeated dev-server navigations its
|
|
9
|
+
* AbortController can already be aborted, which silently stalls GLTFLoader.fetch forever
|
|
10
|
+
* with no thrown error. A private manager sidesteps that (ported from the duet-keys ship).
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const modelLoadingManager = new THREE.LoadingManager();
|
|
14
|
+
modelLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
15
|
+
/** How many leading bytes to read when probing a failed model URL for its signature. @internal */
|
|
16
|
+
const PROBE_BYTE_LIMIT = 64;
|
|
17
|
+
/**
|
|
18
|
+
* Re-fetch a model URL that just failed to load and classify what actually came
|
|
19
|
+
* back — a 404, a dev-server HTML fallback, a corrupt or non-model body — so the
|
|
20
|
+
* runtime can replace an opaque GLTF parse error with a diagnosis naming the
|
|
21
|
+
* broken asset contract. Runs only on the error path (loads rarely fail), and
|
|
22
|
+
* never throws: a failed probe returns a `missing` diagnosis instead.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export async function probeModelUrl(url, logicalId, fetchImpl = fetch) {
|
|
26
|
+
const base = logicalId === undefined ? { url } : { url, logicalId };
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetchImpl(url);
|
|
29
|
+
const buffer = await response.arrayBuffer();
|
|
30
|
+
const bytes = new Uint8Array(buffer.slice(0, PROBE_BYTE_LIMIT));
|
|
31
|
+
return classifyAssetResponse({
|
|
32
|
+
...base,
|
|
33
|
+
status: response.status,
|
|
34
|
+
statusText: response.statusText,
|
|
35
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
36
|
+
bytes,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// The probe fetch itself failed (network/CORS) — the file is effectively unreachable.
|
|
41
|
+
return classifyAssetResponse({ ...base, status: 0 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A {@link GLTFLoader} whose success path is unchanged but whose failures are
|
|
46
|
+
* enriched: when a load errors, it probes the URL and rejects with an
|
|
47
|
+
* {@link AssetLoadDiagnosis}-derived message (missing / HTML fallback / corrupt /
|
|
48
|
+
* unsupported) instead of the raw parse error. Drop-in for the shared loader, so
|
|
49
|
+
* `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
53
|
+
load(url, onLoad, onProgress, onError) {
|
|
54
|
+
super.load(url, onLoad, onProgress, (event) => {
|
|
55
|
+
if (onError === undefined)
|
|
56
|
+
return;
|
|
57
|
+
// Probe the URL the loader actually fetched (base-resolved), not the raw input.
|
|
58
|
+
void probeModelUrl(this.manager.resolveURL(url))
|
|
59
|
+
.then((diagnosis) => {
|
|
60
|
+
if (diagnosis.ok) {
|
|
61
|
+
// The bytes look fine on re-fetch — surface the original parse error unchanged.
|
|
62
|
+
onError(event);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const enriched = new Error(`[jgengine] ${diagnosis.message}`);
|
|
66
|
+
enriched.diagnosis = diagnosis;
|
|
67
|
+
enriched.cause = event;
|
|
68
|
+
onError(enriched);
|
|
69
|
+
})
|
|
70
|
+
.catch(() => onError(event));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Shared GLTF loader used by shell entity/object model mounts. @internal */
|
|
75
|
+
export const sharedGltfLoader = new DiagnosticGLTFLoader(modelLoadingManager);
|
|
76
|
+
sharedGltfLoader.setMeshoptDecoder(MeshoptDecoder);
|