@jgengine/shell 0.7.0 → 0.9.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +37 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +36 -1
  8. package/dist/GamePlayerShell.js +751 -80
  9. package/dist/GameUiPreview.js +2 -1
  10. package/dist/audio/AudioComponents.js +25 -8
  11. package/dist/audio/audioEngine.d.ts +1 -0
  12. package/dist/audio/audioEngine.js +4 -0
  13. package/dist/behaviour.d.ts +12 -0
  14. package/dist/behaviour.js +27 -0
  15. package/dist/behaviourAttach.d.ts +9 -0
  16. package/dist/behaviourAttach.js +32 -0
  17. package/dist/behaviourDriver.d.ts +7 -0
  18. package/dist/behaviourDriver.js +21 -0
  19. package/dist/camera/GameCameraRig.d.ts +8 -3
  20. package/dist/camera/GameCameraRig.js +28 -14
  21. package/dist/camera/GameFirstPersonCamera.js +16 -0
  22. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  23. package/dist/camera/GameInspectionCamera.js +24 -0
  24. package/dist/camera/GameOrbitCamera.js +10 -1
  25. package/dist/camera/PlayerFov.d.ts +18 -0
  26. package/dist/camera/PlayerFov.js +48 -0
  27. package/dist/camera/cameraBlendMath.d.ts +13 -0
  28. package/dist/camera/cameraBlendMath.js +36 -0
  29. package/dist/camera/cameraRigs.d.ts +8 -1
  30. package/dist/camera/cameraRigs.js +78 -38
  31. package/dist/camera/fovPreference.d.ts +25 -0
  32. package/dist/camera/fovPreference.js +73 -0
  33. package/dist/camera/index.d.ts +7 -2
  34. package/dist/camera/index.js +7 -2
  35. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  36. package/dist/camera/inspectionCameraMath.js +44 -0
  37. package/dist/camera/rigMath.d.ts +50 -1
  38. package/dist/camera/rigMath.js +91 -1
  39. package/dist/camera/rigResolve.d.ts +15 -0
  40. package/dist/camera/rigResolve.js +52 -0
  41. package/dist/camera/shakeChannel.d.ts +3 -17
  42. package/dist/camera/shakeChannel.js +2 -21
  43. package/dist/camera/shakeChannelMath.d.ts +8 -0
  44. package/dist/camera/shakeChannelMath.js +21 -0
  45. package/dist/cartridge.d.ts +145 -0
  46. package/dist/cartridge.js +245 -0
  47. package/dist/defineGame.d.ts +16 -0
  48. package/dist/defineGame.js +52 -0
  49. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  50. package/dist/devtools/CollisionDebugWorld.js +180 -0
  51. package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
  52. package/dist/devtools/DevtoolsOverlay.js +676 -0
  53. package/dist/devtools/collisionDebug.d.ts +57 -0
  54. package/dist/devtools/collisionDebug.js +127 -0
  55. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  56. package/dist/devtools/collisionDebugMath.js +128 -0
  57. package/dist/environment/Daylight.d.ts +56 -0
  58. package/dist/environment/Daylight.js +101 -0
  59. package/dist/environment/EnvironmentScene.js +87 -48
  60. package/dist/environment/GroundPad.d.ts +7 -0
  61. package/dist/environment/GroundPad.js +12 -0
  62. package/dist/environment/daylightCycle.d.ts +26 -0
  63. package/dist/environment/daylightCycle.js +116 -0
  64. package/dist/environment/groundPadMath.d.ts +13 -0
  65. package/dist/environment/groundPadMath.js +10 -0
  66. package/dist/environment/index.d.ts +3 -0
  67. package/dist/environment/index.js +3 -0
  68. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  69. package/dist/environment/skyLightingPolicy.js +6 -0
  70. package/dist/input/mouseLook.d.ts +27 -0
  71. package/dist/input/mouseLook.js +35 -0
  72. package/dist/materialOverride.d.ts +6 -0
  73. package/dist/materialOverride.js +28 -0
  74. package/dist/multiplayer.d.ts +16 -9
  75. package/dist/multiplayer.js +62 -12
  76. package/dist/pointer/PointerProbe.js +20 -3
  77. package/dist/pointer/pointerService.d.ts +5 -0
  78. package/dist/pointer/pointerService.js +51 -24
  79. package/dist/registry.d.ts +4 -1
  80. package/dist/registry.js +3 -1
  81. package/dist/render/modelRender.d.ts +23 -0
  82. package/dist/render/modelRender.js +106 -0
  83. package/dist/render/resolveModel.d.ts +14 -0
  84. package/dist/render/resolveModel.js +24 -0
  85. package/dist/settings/QuickControls.d.ts +4 -0
  86. package/dist/settings/QuickControls.js +42 -0
  87. package/dist/settings/SettingsChrome.d.ts +1 -0
  88. package/dist/settings/SettingsChrome.js +10 -0
  89. package/dist/settings/SettingsMenu.d.ts +6 -0
  90. package/dist/settings/SettingsMenu.js +148 -0
  91. package/dist/settings/SettingsRuntime.d.ts +11 -0
  92. package/dist/settings/SettingsRuntime.js +19 -0
  93. package/dist/settings/appliedSettings.d.ts +14 -0
  94. package/dist/settings/appliedSettings.js +27 -0
  95. package/dist/settings/settingsController.d.ts +20 -0
  96. package/dist/settings/settingsController.js +165 -0
  97. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  98. package/dist/structures/GeneratedBuilding.js +96 -8
  99. package/dist/structures/index.d.ts +1 -1
  100. package/dist/structures/index.js +1 -1
  101. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  102. package/dist/terrain/CarvedTerrain.js +3 -3
  103. package/dist/terrain/GrassField.d.ts +3 -1
  104. package/dist/terrain/GrassField.js +4 -2
  105. package/dist/terrain/grassBudget.d.ts +3 -0
  106. package/dist/terrain/grassBudget.js +6 -0
  107. package/dist/terrain/grassGeometry.js +1 -1
  108. package/dist/terrain/index.d.ts +1 -1
  109. package/dist/terrain/index.js +1 -1
  110. package/dist/terrain/terrainMath.d.ts +9 -0
  111. package/dist/terrain/terrainMath.js +17 -2
  112. package/dist/touch/OrientationHint.d.ts +3 -0
  113. package/dist/touch/OrientationHint.js +13 -0
  114. package/dist/touch/TouchControlsOverlay.d.ts +34 -0
  115. package/dist/touch/TouchControlsOverlay.js +279 -0
  116. package/dist/visibility/CullingProvider.d.ts +21 -0
  117. package/dist/visibility/CullingProvider.js +134 -0
  118. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  119. package/dist/vision/FrustumSensorHud.js +25 -27
  120. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  121. package/dist/vision/frustumSampleEqual.js +10 -0
  122. package/dist/water/Ocean.js +1 -1
  123. package/dist/water/OceanConfig.d.ts +13 -0
  124. package/dist/water/OceanConfig.js +25 -17
  125. package/dist/water/OceanShader.d.ts +1 -1
  126. package/dist/water/OceanShader.js +3 -3
  127. package/dist/water/index.d.ts +1 -1
  128. package/dist/water/index.js +1 -1
  129. package/dist/weather/FireSpreadLayer.js +7 -2
  130. package/dist/weather/RainField.d.ts +3 -1
  131. package/dist/weather/RainField.js +4 -4
  132. package/dist/weather/SnowField.d.ts +3 -1
  133. package/dist/weather/SnowField.js +4 -4
  134. package/dist/weather/fireSpreadPose.d.ts +2 -0
  135. package/dist/weather/fireSpreadPose.js +4 -0
  136. package/dist/weather/weatherMath.d.ts +5 -1
  137. package/dist/weather/weatherMath.js +7 -2
  138. package/dist/world/DataObjects.d.ts +44 -0
  139. package/dist/world/DataObjects.js +77 -0
  140. package/dist/world/GridWorldScene.d.ts +10 -0
  141. package/dist/world/GridWorldScene.js +54 -0
  142. package/dist/world/SpriteBatch.d.ts +44 -0
  143. package/dist/world/SpriteBatch.js +112 -0
  144. package/dist/world/WorldHud.d.ts +8 -1
  145. package/dist/world/WorldHud.js +90 -39
  146. package/dist/world/WorldItems.js +21 -2
  147. package/dist/world/entityPose.d.ts +14 -0
  148. package/dist/world/entityPose.js +10 -0
  149. package/dist/world/telegraphPulse.d.ts +1 -0
  150. package/dist/world/telegraphPulse.js +4 -0
  151. package/dist/world/worldBarSamples.d.ts +30 -0
  152. package/dist/world/worldBarSamples.js +51 -0
  153. package/llms.txt +2643 -0
  154. package/package.json +6 -5
  155. package/dist/demo/builderDemo.d.ts +0 -2
  156. package/dist/demo/builderDemo.js +0 -189
  157. package/dist/demo/demoGame.d.ts +0 -2
  158. package/dist/demo/demoGame.js +0 -208
  159. package/dist/demo/environmentShowcase.d.ts +0 -2
  160. package/dist/demo/environmentShowcase.js +0 -15
  161. package/dist/demo/mapDemo.d.ts +0 -2
  162. package/dist/demo/mapDemo.js +0 -206
  163. package/dist/demo/pointerDemo.d.ts +0 -2
  164. package/dist/demo/pointerDemo.js +0 -151
  165. package/dist/demo/sensorShowcase.d.ts +0 -2
  166. package/dist/demo/sensorShowcase.js +0 -131
  167. package/dist/demo/survivalDemo.d.ts +0 -2
  168. package/dist/demo/survivalDemo.js +0 -291
@@ -1,75 +1,114 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
- import { generateBuildingDistrict } from "@jgengine/core/world/buildings";
4
- import { resolveTerrainField } from "@jgengine/core/world/terrain";
5
- import { GeneratedBuilding } from "../structures/GeneratedBuilding.js";
2
+ import { useMemo, useRef } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import { useGameContext } from "@jgengine/react/provider";
5
+ import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
6
+ import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
7
+ import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
8
+ import { GroundPad } from "./GroundPad.js";
9
+ import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
6
10
  import { GrassField } from "../terrain/GrassField.js";
7
- import { ProceduralGround } from "../terrain/ProceduralGround.js";
11
+ import { CarvedTerrain } from "../terrain/CarvedTerrain.js";
8
12
  import { Ocean } from "../water/Ocean.js";
9
13
  import { RainField } from "../weather/RainField.js";
10
14
  import { SnowField } from "../weather/SnowField.js";
11
15
  import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
12
- const DEFAULT_TERRAIN_FREQUENCY = 0.03;
13
- function TerrainGround({ terrain }) {
14
- return (_jsx(ProceduralGround, { terrain: {
15
- size: [terrain.bounds.w, terrain.bounds.d],
16
- height: terrain.height,
17
- seed: terrain.seed,
18
- moundScale: terrain.frequency ?? DEFAULT_TERRAIN_FREQUENCY,
19
- octaves: terrain.octaves,
20
- ridged: terrain.ridged,
21
- baseOffset: terrain.baseHeight,
22
- }, colors: terrain.waterLevel === undefined ? undefined : { waterlineHeight: terrain.waterLevel } }));
16
+ function TerrainGround({ terrain, field, center, }) {
17
+ const palette = useMemo(() => resolveTerrainPalette(terrain), [terrain]);
18
+ const paletteAt = useMemo(() => {
19
+ if (terrain.materialRegions === undefined || terrain.materialRegions.length === 0)
20
+ return undefined;
21
+ const sampler = createTerrainPaletteSampler(terrain);
22
+ if (center === undefined)
23
+ return sampler;
24
+ return (x, z) => sampler(x - center[0], z - center[1]);
25
+ }, [terrain, center]);
26
+ const colors = useMemo(() => ({
27
+ low: palette.low,
28
+ high: palette.high,
29
+ ...(terrain.waterLevel === undefined ? {} : { waterline: palette.waterline, waterlineHeight: terrain.waterLevel }),
30
+ }), [palette, terrain.waterLevel]);
31
+ const size = useMemo(() => [terrain.bounds.w, terrain.bounds.d], [terrain.bounds]);
32
+ const heightRange = useMemo(() => {
33
+ const base = terrain.baseHeight ?? 0;
34
+ const swing = terrain.height * 1.2;
35
+ return [base - swing, base + swing];
36
+ }, [terrain.baseHeight, terrain.height]);
37
+ return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94 }));
38
+ }
39
+ function areaCenter(area) {
40
+ return area.position ?? [0, 0];
23
41
  }
24
42
  function Vegetation({ grass, field }) {
25
- return (_jsx(GrassField, { area: [grass.area.w, grass.area.d], density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: field.sampleHeight, colorBase: grass.colors[0], colorTip: grass.colors[grass.colors.length - 1], wind: { strength: grass.windStrength } }));
43
+ const [cx, cz] = areaCenter(grass.area);
44
+ const heightAt = useMemo(() => cx === 0 && cz === 0
45
+ ? field.sampleHeight
46
+ : (x, z) => field.sampleHeight(x + cx, z + cz), [cx, cz, field]);
47
+ const area = useMemo(() => [grass.area.w, grass.area.d], [grass.area.w, grass.area.d]);
48
+ const wind = useMemo(() => ({ strength: grass.windStrength }), [grass.windStrength]);
49
+ const colorBase = grass.colors[0];
50
+ const colorTip = grass.colors[grass.colors.length - 1];
51
+ return (_jsx("group", { position: [cx, 0, cz], children: _jsx(GrassField, { area: area, density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: heightAt, colorBase: colorBase, colorTip: colorTip, wind: wind, frustumCulled: true }) }));
26
52
  }
27
53
  function weatherVolume(area) {
28
54
  return [area.w, area.h ?? 60, area.d];
29
55
  }
30
56
  function Precipitation({ weather }) {
57
+ const [cx, cz] = areaCenter(weather.area);
31
58
  if (weather.kind === "rain") {
32
- return (_jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }));
59
+ return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
33
60
  }
34
- return (_jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }));
61
+ return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
35
62
  }
36
63
  function Weather({ weather }) {
37
64
  return (_jsx(WeatherUniformProvider, { children: weather.map((entry, index) => (_jsx(Precipitation, { weather: entry, index: index }, `${entry.kind}-${index}`))) }));
38
65
  }
66
+ function oceanConfig(ocean) {
67
+ return {
68
+ size: ocean.bounds.w,
69
+ depth: ocean.bounds.d,
70
+ amplitude: ocean.waveHeight,
71
+ speed: ocean.waveSpeed,
72
+ waveScale: ocean.waveScale,
73
+ color: { shallow: ocean.color },
74
+ };
75
+ }
76
+ function useOceanConfig(ocean) {
77
+ return useMemo(() => oceanConfig(ocean), [ocean.bounds.w, ocean.bounds.d, ocean.waveHeight, ocean.waveSpeed, ocean.color]);
78
+ }
79
+ function DynamicWater({ ocean }) {
80
+ const ctx = useGameContext();
81
+ const groupRef = useRef(null);
82
+ const [x, z] = ocean.position ?? [0, 0];
83
+ const config = useOceanConfig(ocean);
84
+ useFrame(() => {
85
+ if (groupRef.current !== null)
86
+ groupRef.current.position.y = ocean.levelAt(ctx.time.now());
87
+ });
88
+ return (_jsx("group", { ref: groupRef, position: [0, ocean.level, 0], children: _jsx(Ocean, { position: [x, 0, z], config: config }) }));
89
+ }
39
90
  function Water({ ocean }) {
40
- return (_jsx(Ocean, { "position-y": ocean.level, config: {
41
- size: Math.max(ocean.bounds.w, ocean.bounds.d),
42
- amplitude: ocean.waveHeight,
43
- speed: ocean.waveSpeed,
44
- color: { shallow: ocean.color },
45
- } }));
91
+ const [x, z] = ocean.position ?? [0, 0];
92
+ const config = useOceanConfig(ocean);
93
+ if (ocean.levelAt !== undefined) {
94
+ return _jsx(DynamicWater, { ocean: ocean });
95
+ }
96
+ return _jsx(Ocean, { position: [x, ocean.level, z], config: config });
46
97
  }
47
- function Structures({ structures }) {
48
- const buildings = useMemo(() => {
49
- const columns = Math.max(1, Math.ceil(Math.sqrt(structures.count)));
50
- const rows = Math.max(1, Math.ceil(structures.count / columns));
51
- const spacing = structures.spacing;
52
- const origin = [
53
- -((columns - 1) * (structures.footprint.w + spacing)) / 2,
54
- -((rows - 1) * (structures.footprint.d + spacing)) / 2,
55
- ];
56
- return generateBuildingDistrict({
57
- rows,
58
- columns,
59
- origin,
60
- lotSize: structures.footprint,
61
- streetWidth: spacing,
62
- floorRange: structures.stories,
63
- base: { floorHeight: structures.storyHeight },
64
- ...(structures.seed === undefined ? {} : { seed: structures.seed }),
65
- }).slice(0, structures.count);
66
- }, [structures]);
67
- return (_jsx(_Fragment, { children: buildings.map((building) => (_jsx(GeneratedBuilding, { building: building }, building.id))) }));
98
+ function Structures({ structures, field }) {
99
+ const placements = useMemo(() => resolveStructureBuildings(structures).map((building) => ({
100
+ building,
101
+ position: [0, field.sampleHeight(building.center[0], building.center[1]), 0],
102
+ })), [structures, field]);
103
+ const palette = useMemo(() => resolveBuildingPalette(structures.style, structures.palette), [structures.style, structures.palette]);
104
+ return _jsx(InstancedBuildings, { buildings: placements, palette: palette });
68
105
  }
69
106
  export function EnvironmentScene({ feature }) {
70
- const field = useMemo(() => resolveTerrainField(feature.terrain), [feature.terrain]);
107
+ const field = useMemo(() => resolveEnvironmentField(feature), [feature]);
71
108
  const vegetation = feature.vegetation ?? [];
72
109
  const water = feature.water ?? [];
73
110
  const structures = feature.structures ?? [];
74
- return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain }) : null, water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry }, `structures-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
111
+ const pads = feature.pads ?? [];
112
+ const islands = feature.islands ?? [];
113
+ return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
75
114
  }
@@ -0,0 +1,7 @@
1
+ import type { PadEnvironmentDescriptor } from "@jgengine/core/world/features";
2
+ import type { TerrainField } from "@jgengine/core/world/terrain";
3
+ export interface GroundPadProps {
4
+ pad: PadEnvironmentDescriptor;
5
+ field: TerrainField;
6
+ }
7
+ export declare function GroundPad({ pad, field }: GroundPadProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PAD_THICKNESS, resolvePadMeshY, resolvePadShape } from "./groundPadMath.js";
3
+ export function GroundPad({ pad, field }) {
4
+ const [x, z] = pad.center;
5
+ const groundHeight = field.sampleHeight(x, z);
6
+ const meshY = resolvePadMeshY(groundHeight, pad);
7
+ const shape = resolvePadShape(pad.size);
8
+ if (shape.circular) {
9
+ return (_jsxs("mesh", { position: [x, meshY, z], receiveShadow: true, children: [_jsx("cylinderGeometry", { args: [shape.radius, shape.radius, PAD_THICKNESS, 32] }), _jsx("meshStandardMaterial", { color: pad.color, roughness: 0.9 })] }));
10
+ }
11
+ return (_jsxs("mesh", { position: [x, meshY, z], rotation: [0, pad.rotationY ?? 0, 0], receiveShadow: true, children: [_jsx("boxGeometry", { args: [shape.width, PAD_THICKNESS, shape.depth] }), _jsx("meshStandardMaterial", { color: pad.color, roughness: 0.9 })] }));
12
+ }
@@ -0,0 +1,26 @@
1
+ export interface DaylightCycleConfig {
2
+ horizonColor?: string;
3
+ zenithColor?: string;
4
+ sunIntensity?: number;
5
+ ambientIntensity?: number;
6
+ }
7
+ export interface DaylightState {
8
+ sunPosition: [number, number, number];
9
+ sunIntensity: number;
10
+ ambientIntensity: number;
11
+ skyTop: string;
12
+ skyBottom: string;
13
+ background: string;
14
+ }
15
+ export declare const DEFAULT_DAY_SUN_INTENSITY = 1;
16
+ export declare const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
17
+ export declare const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
18
+ export declare const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
19
+ export declare const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>;
20
+ export declare function lerpHexColor(a: string, b: string, t: number): string;
21
+ /**
22
+ * Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
23
+ * lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
24
+ * keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
25
+ */
26
+ export declare function daylightStateAt(dayFraction: number, config?: DaylightCycleConfig): DaylightState;
@@ -0,0 +1,116 @@
1
+ const NIGHT_SUN_INTENSITY = 0.02;
2
+ const NIGHT_AMBIENT_INTENSITY = 0.08;
3
+ const NIGHT_SKY_TOP = "#02030a";
4
+ const NIGHT_SKY_BOTTOM = "#05070f";
5
+ const DAWN_SUN_INTENSITY = 0.45;
6
+ const DAWN_AMBIENT_INTENSITY = 0.3;
7
+ const DAWN_SKY_TOP = "#3c5a82";
8
+ const DAWN_SKY_BOTTOM = "#ffb37a";
9
+ const DUSK_SUN_INTENSITY = 0.45;
10
+ const DUSK_AMBIENT_INTENSITY = 0.3;
11
+ const DUSK_SKY_TOP = "#4a3a5c";
12
+ const DUSK_SKY_BOTTOM = "#ff8a5c";
13
+ export const DEFAULT_DAY_SUN_INTENSITY = 1;
14
+ export const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
15
+ export const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
16
+ export const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
17
+ const SUN_DISTANCE = 200;
18
+ const SUN_DEPTH_RATIO = 0.4;
19
+ export const SKY_PRESET_DAY_FRACTION = {
20
+ night: 0,
21
+ day: 0.5,
22
+ dusk: 0.75,
23
+ };
24
+ function clamp01(value) {
25
+ return value < 0 ? 0 : value > 1 ? 1 : value;
26
+ }
27
+ function lerp(a, b, t) {
28
+ return a + (b - a) * t;
29
+ }
30
+ function hexToRgb(hex) {
31
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex;
32
+ const value = parseInt(clean, 16);
33
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
34
+ }
35
+ function clampByte(value) {
36
+ return Math.max(0, Math.min(255, Math.round(value)));
37
+ }
38
+ function rgbToHex(rgb) {
39
+ return `#${rgb.map((channel) => clampByte(channel).toString(16).padStart(2, "0")).join("")}`;
40
+ }
41
+ export function lerpHexColor(a, b, t) {
42
+ const [ar, ag, ab] = hexToRgb(a);
43
+ const [br, bg, bb] = hexToRgb(b);
44
+ return rgbToHex([lerp(ar, br, t), lerp(ag, bg, t), lerp(ab, bb, t)]);
45
+ }
46
+ function buildKeyframes(config) {
47
+ const night = {
48
+ t: 0,
49
+ sunIntensity: NIGHT_SUN_INTENSITY,
50
+ ambientIntensity: NIGHT_AMBIENT_INTENSITY,
51
+ skyTop: NIGHT_SKY_TOP,
52
+ skyBottom: NIGHT_SKY_BOTTOM,
53
+ };
54
+ const dawn = {
55
+ t: 0.25,
56
+ sunIntensity: DAWN_SUN_INTENSITY,
57
+ ambientIntensity: DAWN_AMBIENT_INTENSITY,
58
+ skyTop: DAWN_SKY_TOP,
59
+ skyBottom: DAWN_SKY_BOTTOM,
60
+ };
61
+ const day = {
62
+ t: 0.5,
63
+ sunIntensity: config.sunIntensity ?? DEFAULT_DAY_SUN_INTENSITY,
64
+ ambientIntensity: config.ambientIntensity ?? DEFAULT_DAY_AMBIENT_INTENSITY,
65
+ skyTop: config.zenithColor ?? DEFAULT_DAY_SKY_TOP,
66
+ skyBottom: config.horizonColor ?? DEFAULT_DAY_SKY_BOTTOM,
67
+ };
68
+ const dusk = {
69
+ t: 0.75,
70
+ sunIntensity: DUSK_SUN_INTENSITY,
71
+ ambientIntensity: DUSK_AMBIENT_INTENSITY,
72
+ skyTop: DUSK_SKY_TOP,
73
+ skyBottom: DUSK_SKY_BOTTOM,
74
+ };
75
+ const midnight = { ...night, t: 1 };
76
+ return [night, dawn, day, dusk, midnight];
77
+ }
78
+ function wrapFraction(dayFraction) {
79
+ return ((dayFraction % 1) + 1) % 1;
80
+ }
81
+ function segmentAt(wrapped, keyframes) {
82
+ for (let index = 0; index < keyframes.length - 1; index += 1) {
83
+ const from = keyframes[index];
84
+ const to = keyframes[index + 1];
85
+ if (wrapped >= from.t && wrapped <= to.t) {
86
+ const span = to.t - from.t;
87
+ const localT = span <= 0 ? 0 : clamp01((wrapped - from.t) / span);
88
+ return { from, to, localT };
89
+ }
90
+ }
91
+ const last = keyframes[keyframes.length - 1];
92
+ return { from: last, to: last, localT: 0 };
93
+ }
94
+ /**
95
+ * Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
96
+ * lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
97
+ * keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
98
+ */
99
+ export function daylightStateAt(dayFraction, config = {}) {
100
+ const wrapped = wrapFraction(dayFraction);
101
+ const keyframes = buildKeyframes(config);
102
+ const { from, to, localT } = segmentAt(wrapped, keyframes);
103
+ const sunIntensity = lerp(from.sunIntensity, to.sunIntensity, localT);
104
+ const ambientIntensity = lerp(from.ambientIntensity, to.ambientIntensity, localT);
105
+ const skyTop = lerpHexColor(from.skyTop, to.skyTop, localT);
106
+ const skyBottom = lerpHexColor(from.skyBottom, to.skyBottom, localT);
107
+ const angle = (wrapped - 0.25) * 2 * Math.PI;
108
+ const elevation = Math.sin(angle);
109
+ const azimuth = Math.cos(angle);
110
+ const sunPosition = [
111
+ azimuth * SUN_DISTANCE,
112
+ elevation * SUN_DISTANCE,
113
+ SUN_DISTANCE * SUN_DEPTH_RATIO,
114
+ ];
115
+ return { sunPosition, sunIntensity, ambientIntensity, skyTop, skyBottom, background: skyBottom };
116
+ }
@@ -0,0 +1,13 @@
1
+ import type { PadEnvironmentDescriptor, PadSize } from "@jgengine/core/world/features";
2
+ export declare const PAD_THICKNESS = 0.1;
3
+ export type PadShape = {
4
+ circular: true;
5
+ radius: number;
6
+ } | {
7
+ circular: false;
8
+ width: number;
9
+ depth: number;
10
+ };
11
+ export declare function resolvePadShape(size: PadSize): PadShape;
12
+ export declare function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
13
+ export declare function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
@@ -0,0 +1,10 @@
1
+ export const PAD_THICKNESS = 0.1;
2
+ export function resolvePadShape(size) {
3
+ return "radius" in size ? { circular: true, radius: size.radius } : { circular: false, width: size[0], depth: size[1] };
4
+ }
5
+ export function resolvePadSurfaceY(groundHeight, pad) {
6
+ return pad.elevation ?? groundHeight + pad.height;
7
+ }
8
+ export function resolvePadMeshY(groundHeight, pad) {
9
+ return resolvePadSurfaceY(groundHeight, pad) - PAD_THICKNESS / 2;
10
+ }
@@ -1 +1,4 @@
1
+ export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, type SkyDaylightProps, type SkyDomeProps, type TimeOfDayDaylightProps, } from "./Daylight.js";
1
2
  export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
3
+ export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, type DaylightCycleConfig, type DaylightState, } from "./daylightCycle.js";
4
+ export { resolveSkyLightOwnership, skyEmitsLights, type SkyLightOwnership, } from "./skyLightingPolicy.js";
@@ -1 +1,4 @@
1
+ export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, } from "./Daylight.js";
1
2
  export { EnvironmentScene } from "./EnvironmentScene.js";
3
+ export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, } from "./daylightCycle.js";
4
+ export { resolveSkyLightOwnership, skyEmitsLights, } from "./skyLightingPolicy.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Policy for composing sky backdrops with `PlayableGame.lighting`:
3
+ * - authored lighting present → sky renders dome + fog only; lights stay game-owned
4
+ * - no authored lighting → sky may emit its default sun/hemisphere with the dome
5
+ * Time-of-day never rewrites configured lights; it only drives sky colors/fog (and
6
+ * sky-owned lights when the game did not author lighting).
7
+ */
8
+ export type SkyLightOwnership = "authored" | "sky-default";
9
+ export declare function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership;
10
+ export declare function skyEmitsLights(ownership: SkyLightOwnership): boolean;
@@ -0,0 +1,6 @@
1
+ export function resolveSkyLightOwnership(hasAuthoredLighting) {
2
+ return hasAuthoredLighting ? "authored" : "sky-default";
3
+ }
4
+ export function skyEmitsLights(ownership) {
5
+ return ownership === "sky-default";
6
+ }
@@ -0,0 +1,27 @@
1
+ export interface MouseLookOptions {
2
+ /** Radians of look per pixel of mouse movement; default `0.0024`. */
3
+ sensitivity?: number;
4
+ /** Pitch clamp in radians; default `1.15`. */
5
+ maxPitch?: number;
6
+ /** Request pointer lock on click (skipped on coarse-pointer/touch devices); default `true`. */
7
+ pointerLock?: boolean;
8
+ initialYaw?: number;
9
+ initialPitch?: number;
10
+ }
11
+ export interface MouseLookAim {
12
+ yaw: number;
13
+ pitch: number;
14
+ }
15
+ /**
16
+ * The analog mouse-look service chase/orbit-cam games hand-rolled (#282.8) — pointer-lock
17
+ * lifecycle plus delta accumulation into a yaw/pitch aim, decoupled from the first-person rig.
18
+ * Attach it to the canvas, read `aim()` from `onTick`/`useFrame`, dispose on unmount.
19
+ */
20
+ export interface MouseLookTracker {
21
+ aim(): MouseLookAim;
22
+ setAim(yaw: number, pitch: number): void;
23
+ /** True while pointer lock is held on the tracked element. */
24
+ locked(): boolean;
25
+ dispose(): void;
26
+ }
27
+ export declare function createMouseLookTracker(element: HTMLElement, options?: MouseLookOptions): MouseLookTracker;
@@ -0,0 +1,35 @@
1
+ export function createMouseLookTracker(element, options = {}) {
2
+ const sensitivity = options.sensitivity ?? 0.0024;
3
+ const maxPitch = options.maxPitch ?? 1.15;
4
+ const pointerLock = options.pointerLock ?? true;
5
+ let yaw = options.initialYaw ?? 0;
6
+ let pitch = options.initialPitch ?? 0;
7
+ const requestLock = () => {
8
+ if (!pointerLock)
9
+ return;
10
+ if (window.matchMedia?.("(pointer: coarse)").matches)
11
+ return;
12
+ if (document.pointerLockElement !== element)
13
+ void element.requestPointerLock?.();
14
+ };
15
+ const onMove = (event) => {
16
+ if (pointerLock && document.pointerLockElement !== element)
17
+ return;
18
+ yaw -= event.movementX * sensitivity;
19
+ pitch = Math.max(-maxPitch, Math.min(maxPitch, pitch - event.movementY * sensitivity));
20
+ };
21
+ element.addEventListener("click", requestLock);
22
+ window.addEventListener("mousemove", onMove);
23
+ return {
24
+ aim: () => ({ yaw, pitch }),
25
+ setAim(nextYaw, nextPitch) {
26
+ yaw = nextYaw;
27
+ pitch = Math.max(-maxPitch, Math.min(maxPitch, nextPitch));
28
+ },
29
+ locked: () => document.pointerLockElement === element,
30
+ dispose() {
31
+ element.removeEventListener("click", requestLock);
32
+ window.removeEventListener("mousemove", onMove);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,6 @@
1
+ import * as THREE from "three";
2
+ import type { ModelMaterialOverride } from "@jgengine/core/game/playableGame";
3
+ export interface MaterialOverrideOptions {
4
+ clone?: boolean;
5
+ }
6
+ export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void;
@@ -0,0 +1,28 @@
1
+ import * as THREE from "three";
2
+ export function applyMaterialOverride(root, override, options) {
3
+ const clone = options?.clone !== false;
4
+ root.traverse((node) => {
5
+ const mesh = node;
6
+ if (!mesh.isMesh)
7
+ return;
8
+ mesh.material = Array.isArray(mesh.material)
9
+ ? mesh.material.map((material) => overrideOne(material, override, clone))
10
+ : overrideOne(mesh.material, override, clone);
11
+ });
12
+ }
13
+ function overrideOne(material, override, clone) {
14
+ if (!(material instanceof THREE.MeshStandardMaterial))
15
+ return material;
16
+ const target = clone ? material.clone() : material;
17
+ if (override.color !== undefined)
18
+ target.color.set(override.color);
19
+ if (override.metalness !== undefined)
20
+ target.metalness = override.metalness;
21
+ if (override.roughness !== undefined)
22
+ target.roughness = override.roughness;
23
+ if (override.emissive !== undefined)
24
+ target.emissive.set(override.emissive);
25
+ if (override.emissiveIntensity !== undefined)
26
+ target.emissiveIntensity = override.emissiveIntensity;
27
+ return target;
28
+ }
@@ -1,16 +1,23 @@
1
1
  import type { GameDefinition } from "@jgengine/core/game/defineGame";
2
- import { type WsBackend } from "@jgengine/ws/createWsBackend";
3
- export type ShellMultiplayer = {
4
- gameId: string;
5
- userId: string;
6
- backend: WsBackend;
7
- feedActions: string[];
8
- };
9
- export declare function resolveShellMultiplayer(args: {
2
+ import type { MultiplayerSession } from "@jgengine/core/runtime/transport";
3
+ export type ShellMultiplayer = MultiplayerSession;
4
+ export declare const DEFAULT_FEED_ACTIONS: string[];
5
+ export declare function randomPlayerId(): string;
6
+ export type ResolveShellMultiplayerArgs = {
10
7
  game: GameDefinition;
11
8
  gameId: string;
12
9
  url?: string;
13
10
  userId?: string;
14
11
  force?: boolean;
15
12
  feedActions?: string[];
16
- }): ShellMultiplayer | null;
13
+ };
14
+ export declare function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null;
15
+ export declare function resolvePeerShellMultiplayer(args: {
16
+ gameId: string;
17
+ role: "host" | "join";
18
+ room?: string;
19
+ userId?: string;
20
+ feedActions?: string[];
21
+ }): Promise<ShellMultiplayer & {
22
+ close: () => void;
23
+ }>;
@@ -1,21 +1,71 @@
1
+ import { adapterOf } from "@jgengine/core/runtime/adapter";
1
2
  import { createWsBackend } from "@jgengine/ws/createWsBackend";
2
- function adapterKind(multiplayer) {
3
- if (typeof multiplayer !== "object" || multiplayer === null)
4
- return null;
5
- const direct = multiplayer.kind;
6
- if (typeof direct === "string")
7
- return direct;
8
- const nested = multiplayer.adapter?.kind;
9
- return typeof nested === "string" ? nested : null;
3
+ import { announcePeerHost, broadcastChannelSignaling, createPeerGuest, createPeerHost, joinPeerSession, } from "@jgengine/ws/peer";
4
+ export const DEFAULT_FEED_ACTIONS = ["entity.died"];
5
+ const DEFAULT_WS_URL = "ws://localhost:8080/ws";
6
+ export function randomPlayerId() {
7
+ return `player-${Math.random().toString(36).slice(2, 10)}`;
8
+ }
9
+ function lanUrl(adapter) {
10
+ const hasWindow = typeof window !== "undefined";
11
+ const scheme = hasWindow && window.location.protocol === "https:" ? "wss" : "ws";
12
+ const hostname = hasWindow ? window.location.hostname : "localhost";
13
+ const port = adapter.port ?? 8080;
14
+ const path = adapter.path ?? "/ws";
15
+ return `${scheme}://${hostname}:${port}${path}`;
16
+ }
17
+ function buildWsMultiplayer(args) {
18
+ return {
19
+ gameId: args.gameId,
20
+ userId: args.userId,
21
+ backend: createWsBackend({ url: args.url, userId: args.userId }),
22
+ feedActions: args.feedActions,
23
+ };
10
24
  }
11
25
  export function resolveShellMultiplayer(args) {
12
- if (args.force !== true && adapterKind(args.game.multiplayer) !== "ws")
26
+ const userId = args.userId ?? randomPlayerId();
27
+ const feedActions = args.feedActions ?? DEFAULT_FEED_ACTIONS;
28
+ const build = (url) => buildWsMultiplayer({ gameId: args.gameId, userId, feedActions, url });
29
+ if (args.force === true)
30
+ return build(args.url ?? DEFAULT_WS_URL);
31
+ const adapter = adapterOf(args.game.multiplayer);
32
+ if (adapter === null)
13
33
  return null;
14
- const userId = args.userId ?? `player-${Math.random().toString(36).slice(2, 10)}`;
34
+ if (adapter.kind === "ws")
35
+ return build(args.url ?? adapter.url ?? DEFAULT_WS_URL);
36
+ if (adapter.kind === "lan")
37
+ return build(args.url ?? lanUrl(adapter));
38
+ return null;
39
+ }
40
+ export async function resolvePeerShellMultiplayer(args) {
41
+ const userId = args.userId ?? randomPlayerId();
42
+ const feedActions = args.feedActions ?? DEFAULT_FEED_ACTIONS;
43
+ const signaling = broadcastChannelSignaling(args.room ?? `jg-p2p-${args.gameId}`);
44
+ if (args.role === "host") {
45
+ const peerHost = createPeerHost({ userId });
46
+ const stopAnnouncing = announcePeerHost(peerHost, signaling);
47
+ return {
48
+ gameId: args.gameId,
49
+ userId,
50
+ backend: peerHost.backend,
51
+ feedActions,
52
+ close: () => {
53
+ stopAnnouncing();
54
+ signaling.close();
55
+ peerHost.close();
56
+ },
57
+ };
58
+ }
59
+ const guest = createPeerGuest({ userId });
60
+ const backend = await joinPeerSession(guest, signaling);
15
61
  return {
16
62
  gameId: args.gameId,
17
63
  userId,
18
- backend: createWsBackend({ url: args.url ?? "ws://localhost:8080/ws", userId }),
19
- feedActions: args.feedActions ?? ["entity.died"],
64
+ backend,
65
+ feedActions,
66
+ close: () => {
67
+ signaling.close();
68
+ guest.close();
69
+ },
20
70
  };
21
71
  }