@jgengine/shell 0.6.0 → 0.8.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 (118) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +26 -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 +33 -1
  8. package/dist/GamePlayerShell.js +641 -59
  9. package/dist/audio/AudioComponents.d.ts +12 -0
  10. package/dist/audio/AudioComponents.js +53 -0
  11. package/dist/audio/audioEngine.d.ts +23 -0
  12. package/dist/audio/audioEngine.js +110 -0
  13. package/dist/behaviour.d.ts +31 -0
  14. package/dist/behaviour.js +63 -0
  15. package/dist/camera/GameCameraRig.d.ts +16 -0
  16. package/dist/camera/GameCameraRig.js +56 -0
  17. package/dist/camera/GameFirstPersonCamera.js +2 -0
  18. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  19. package/dist/camera/GameInspectionCamera.js +24 -0
  20. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  21. package/dist/camera/GameOrbitCamera.js +4 -2
  22. package/dist/camera/cameraRigs.d.ts +26 -0
  23. package/dist/camera/cameraRigs.js +522 -0
  24. package/dist/camera/index.d.ts +7 -0
  25. package/dist/camera/index.js +7 -0
  26. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  27. package/dist/camera/inspectionCameraMath.js +44 -0
  28. package/dist/camera/rigMath.d.ts +169 -0
  29. package/dist/camera/rigMath.js +393 -0
  30. package/dist/camera/rigResolve.d.ts +15 -0
  31. package/dist/camera/rigResolve.js +52 -0
  32. package/dist/camera/shakeChannel.d.ts +29 -0
  33. package/dist/camera/shakeChannel.js +38 -0
  34. package/dist/defineGame.d.ts +16 -0
  35. package/dist/defineGame.js +51 -0
  36. package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
  37. package/dist/devtools/DevtoolsOverlay.js +334 -0
  38. package/dist/environment/Daylight.d.ts +50 -0
  39. package/dist/environment/Daylight.js +100 -0
  40. package/dist/environment/EnvironmentScene.js +18 -26
  41. package/dist/environment/GroundPad.d.ts +7 -0
  42. package/dist/environment/GroundPad.js +12 -0
  43. package/dist/environment/daylightCycle.d.ts +26 -0
  44. package/dist/environment/daylightCycle.js +116 -0
  45. package/dist/environment/groundPadMath.d.ts +13 -0
  46. package/dist/environment/groundPadMath.js +10 -0
  47. package/dist/environment/index.d.ts +2 -0
  48. package/dist/environment/index.js +2 -0
  49. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  50. package/dist/map/MapMarkerBeacons.js +16 -0
  51. package/dist/map/index.d.ts +2 -0
  52. package/dist/map/index.js +2 -0
  53. package/dist/map/terrainMap.d.ts +23 -0
  54. package/dist/map/terrainMap.js +79 -0
  55. package/dist/materialOverride.d.ts +8 -0
  56. package/dist/materialOverride.js +32 -0
  57. package/dist/multiplayer.d.ts +16 -9
  58. package/dist/multiplayer.js +62 -12
  59. package/dist/pointer/PointerOverlays.d.ts +12 -0
  60. package/dist/pointer/PointerOverlays.js +17 -0
  61. package/dist/pointer/PointerProbe.d.ts +4 -0
  62. package/dist/pointer/PointerProbe.js +40 -0
  63. package/dist/pointer/pointerService.d.ts +26 -0
  64. package/dist/pointer/pointerService.js +89 -0
  65. package/dist/registry.d.ts +4 -1
  66. package/dist/registry.js +3 -1
  67. package/dist/render/modelRender.d.ts +14 -0
  68. package/dist/render/modelRender.js +79 -0
  69. package/dist/replay/useSessionRecorder.d.ts +14 -0
  70. package/dist/replay/useSessionRecorder.js +22 -0
  71. package/dist/structures/PlacementGhost.d.ts +8 -0
  72. package/dist/structures/PlacementGhost.js +11 -0
  73. package/dist/structures/index.d.ts +2 -0
  74. package/dist/structures/index.js +2 -0
  75. package/dist/structures.d.ts +1 -0
  76. package/dist/structures.js +1 -0
  77. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  78. package/dist/terrain/CarvedTerrain.js +19 -0
  79. package/dist/terrain/EditableGround.d.ts +11 -0
  80. package/dist/terrain/EditableGround.js +38 -0
  81. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  82. package/dist/terrain/TerraformBrushCursor.js +13 -0
  83. package/dist/terrain/index.d.ts +4 -1
  84. package/dist/terrain/index.js +4 -1
  85. package/dist/terrain/terrainMath.d.ts +15 -1
  86. package/dist/terrain/terrainMath.js +87 -1
  87. package/dist/touch/TouchControlsOverlay.d.ts +18 -0
  88. package/dist/touch/TouchControlsOverlay.js +173 -0
  89. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  90. package/dist/vision/FrustumSensorHud.js +67 -0
  91. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  92. package/dist/vision/HiddenStateProbeHud.js +11 -0
  93. package/dist/vision/RevealVision.d.ts +28 -0
  94. package/dist/vision/RevealVision.js +53 -0
  95. package/dist/water/OceanShader.d.ts +1 -1
  96. package/dist/water/OceanShader.js +3 -3
  97. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  98. package/dist/weather/FireSpreadLayer.js +68 -0
  99. package/dist/weather/index.d.ts +1 -0
  100. package/dist/weather/index.js +1 -0
  101. package/dist/world/DataObjects.d.ts +44 -0
  102. package/dist/world/DataObjects.js +75 -0
  103. package/dist/world/GridWorldScene.d.ts +10 -0
  104. package/dist/world/GridWorldScene.js +54 -0
  105. package/dist/world/InstancedJoints.d.ts +14 -0
  106. package/dist/world/InstancedJoints.js +32 -0
  107. package/dist/world/WorldHud.d.ts +7 -1
  108. package/dist/world/WorldHud.js +76 -14
  109. package/dist/world/WorldItems.d.ts +5 -0
  110. package/dist/world/WorldItems.js +50 -0
  111. package/dist/world/floatTextStyle.d.ts +14 -0
  112. package/dist/world/floatTextStyle.js +39 -0
  113. package/llms.txt +2382 -0
  114. package/package.json +10 -5
  115. package/dist/demo/demoGame.d.ts +0 -2
  116. package/dist/demo/demoGame.js +0 -208
  117. package/dist/demo/environmentShowcase.d.ts +0 -2
  118. package/dist/demo/environmentShowcase.js +0 -15
@@ -1,7 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
- import { generateBuildingDistrict } from "@jgengine/core/world/buildings";
4
- import { resolveTerrainField } from "@jgengine/core/world/terrain";
3
+ import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
4
+ import { resolveTerrainField, resolveTerrainPalette } from "@jgengine/core/world/terrain";
5
+ import { SkyDaylight } from "./Daylight.js";
6
+ import { GroundPad } from "./GroundPad.js";
5
7
  import { GeneratedBuilding } from "../structures/GeneratedBuilding.js";
6
8
  import { GrassField } from "../terrain/GrassField.js";
7
9
  import { ProceduralGround } from "../terrain/ProceduralGround.js";
@@ -11,15 +13,21 @@ import { SnowField } from "../weather/SnowField.js";
11
13
  import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
12
14
  const DEFAULT_TERRAIN_FREQUENCY = 0.03;
13
15
  function TerrainGround({ terrain }) {
16
+ const palette = resolveTerrainPalette(terrain);
14
17
  return (_jsx(ProceduralGround, { terrain: {
15
18
  size: [terrain.bounds.w, terrain.bounds.d],
19
+ segments: terrain.segments,
16
20
  height: terrain.height,
17
21
  seed: terrain.seed,
18
22
  moundScale: terrain.frequency ?? DEFAULT_TERRAIN_FREQUENCY,
19
23
  octaves: terrain.octaves,
20
24
  ridged: terrain.ridged,
21
25
  baseOffset: terrain.baseHeight,
22
- }, colors: terrain.waterLevel === undefined ? undefined : { waterlineHeight: terrain.waterLevel } }));
26
+ }, colors: {
27
+ low: palette.low,
28
+ high: palette.high,
29
+ ...(terrain.waterLevel === undefined ? {} : { waterline: palette.waterline, waterlineHeight: terrain.waterLevel }),
30
+ } }));
23
31
  }
24
32
  function Vegetation({ grass, field }) {
25
33
  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 } }));
@@ -37,39 +45,23 @@ function Weather({ weather }) {
37
45
  return (_jsx(WeatherUniformProvider, { children: weather.map((entry, index) => (_jsx(Precipitation, { weather: entry, index: index }, `${entry.kind}-${index}`))) }));
38
46
  }
39
47
  function Water({ ocean }) {
40
- return (_jsx(Ocean, { "position-y": ocean.level, config: {
48
+ const [x, z] = ocean.position ?? [0, 0];
49
+ return (_jsx(Ocean, { position: [x, ocean.level, z], config: {
41
50
  size: Math.max(ocean.bounds.w, ocean.bounds.d),
42
51
  amplitude: ocean.waveHeight,
43
52
  speed: ocean.waveSpeed,
44
53
  color: { shallow: ocean.color },
45
54
  } }));
46
55
  }
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))) }));
56
+ function Structures({ structures, field }) {
57
+ const buildings = useMemo(() => resolveStructureBuildings(structures), [structures]);
58
+ return (_jsx(_Fragment, { children: buildings.map((building) => (_jsx("group", { "position-y": field.sampleHeight(building.center[0], building.center[1]), children: _jsx(GeneratedBuilding, { building: building }) }, building.id))) }));
68
59
  }
69
60
  export function EnvironmentScene({ feature }) {
70
61
  const field = useMemo(() => resolveTerrainField(feature.terrain), [feature.terrain]);
71
62
  const vegetation = feature.vegetation ?? [];
72
63
  const water = feature.water ?? [];
73
64
  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] }));
65
+ const pads = feature.pads ?? [];
66
+ return (_jsxs(_Fragment, { children: [feature.sky !== undefined && !feature.sky.timeOfDay ? _jsx(SkyDaylight, { sky: feature.sky }) : null, 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, 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
67
  }
@@ -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">): number;
13
+ export declare function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height">): 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 groundHeight + pad.height;
7
+ }
8
+ export function resolvePadMeshY(groundHeight, pad) {
9
+ return resolvePadSurfaceY(groundHeight, pad) - PAD_THICKNESS / 2;
10
+ }
@@ -1 +1,3 @@
1
+ export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, 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";
@@ -1 +1,3 @@
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";
@@ -0,0 +1,12 @@
1
+ import { type MarkerKindStyle, type MarkerSet } from "@jgengine/core/world/markers";
2
+ export interface MapMarkerBeaconsProps {
3
+ markers: MarkerSet;
4
+ kindStyles?: Record<string, MarkerKindStyle>;
5
+ height?: number;
6
+ }
7
+ /**
8
+ * World-space beacons for map markers (the visible in-world side of a ping):
9
+ * a floating diamond over a soft light beam, colored by marker kind. Wire it
10
+ * through `PlayableGame.WorldOverlay`.
11
+ */
12
+ export declare function MapMarkerBeacons({ markers, kindStyles, height }: MapMarkerBeaconsProps): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { markerKindStyle } from "@jgengine/core/world/markers";
3
+ import { useMarkers } from "@jgengine/react/map";
4
+ /**
5
+ * World-space beacons for map markers (the visible in-world side of a ping):
6
+ * a floating diamond over a soft light beam, colored by marker kind. Wire it
7
+ * through `PlayableGame.WorldOverlay`.
8
+ */
9
+ export function MapMarkerBeacons({ markers, kindStyles, height = 5 }) {
10
+ const list = useMarkers(markers);
11
+ return (_jsx(_Fragment, { children: list.map((marker) => {
12
+ const style = markerKindStyle(marker.kind, kindStyles);
13
+ const baseY = marker.position[1];
14
+ return (_jsxs("group", { position: [marker.position[0], baseY, marker.position[2]], children: [_jsxs("mesh", { "position-y": height / 2, children: [_jsx("cylinderGeometry", { args: [0.09, 0.09, height, 6] }), _jsx("meshBasicMaterial", { color: style.color, transparent: true, opacity: 0.4 })] }), _jsxs("mesh", { "position-y": height + 0.5, rotation: [0, Math.PI / 4, 0], children: [_jsx("octahedronGeometry", { args: [0.55, 0] }), _jsx("meshBasicMaterial", { color: style.color })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.06, children: [_jsx("ringGeometry", { args: [0.7, 0.95, 24] }), _jsx("meshBasicMaterial", { color: style.color, transparent: true, opacity: 0.75 })] })] }, marker.id));
15
+ }) }));
16
+ }
@@ -0,0 +1,2 @@
1
+ export { bakeTerrainMap, type BakedMap, type BakeTerrainMapOptions, type MapBakeBounds } from "./terrainMap.js";
2
+ export { MapMarkerBeacons, type MapMarkerBeaconsProps } from "./MapMarkerBeacons.js";
@@ -0,0 +1,2 @@
1
+ export { bakeTerrainMap } from "./terrainMap.js";
2
+ export { MapMarkerBeacons } from "./MapMarkerBeacons.js";
@@ -0,0 +1,23 @@
1
+ import type { TerrainField } from "@jgengine/core/world/terrain";
2
+ export interface MapBakeBounds {
3
+ minX: number;
4
+ minZ: number;
5
+ maxX: number;
6
+ maxZ: number;
7
+ }
8
+ export interface BakeTerrainMapOptions {
9
+ resolution?: number;
10
+ landLow?: readonly [number, number, number];
11
+ landHigh?: readonly [number, number, number];
12
+ water?: readonly [number, number, number];
13
+ }
14
+ export interface BakedMap {
15
+ url: string;
16
+ bounds: MapBakeBounds;
17
+ }
18
+ /**
19
+ * Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds`
20
+ * for the react `Minimap` / `WorldMap` background. Runs in the browser via a
21
+ * 2D canvas; renderer-side, so it lives in the shell.
22
+ */
23
+ export declare function bakeTerrainMap(field: TerrainField, bounds: MapBakeBounds, options?: BakeTerrainMapOptions): BakedMap | null;
@@ -0,0 +1,79 @@
1
+ import { isRegionField } from "@jgengine/core/world/regions";
2
+ function mix(a, b, t) {
3
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
4
+ }
5
+ const DEFAULT_LAND_LOW = [46, 74, 45];
6
+ const DEFAULT_LAND_HIGH = [180, 172, 128];
7
+ const DEFAULT_WATER = [28, 74, 108];
8
+ /**
9
+ * Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds`
10
+ * for the react `Minimap` / `WorldMap` background. Runs in the browser via a
11
+ * 2D canvas; renderer-side, so it lives in the shell.
12
+ */
13
+ export function bakeTerrainMap(field, bounds, options = {}) {
14
+ const res = options.resolution ?? 160;
15
+ if (typeof document === "undefined")
16
+ return null;
17
+ const canvas = document.createElement("canvas");
18
+ canvas.width = res;
19
+ canvas.height = res;
20
+ const context = canvas.getContext("2d");
21
+ if (context === null)
22
+ return null;
23
+ const region = isRegionField(field) ? field : null;
24
+ const waterLevel = field.waterLevel ?? Number.NEGATIVE_INFINITY;
25
+ const worldW = bounds.maxX - bounds.minX;
26
+ const worldD = bounds.maxZ - bounds.minZ;
27
+ const landLow = options.landLow ?? DEFAULT_LAND_LOW;
28
+ const landHigh = options.landHigh ?? DEFAULT_LAND_HIGH;
29
+ const water = options.water ?? DEFAULT_WATER;
30
+ const heights = new Float32Array(res * res);
31
+ let minHeight = Number.POSITIVE_INFINITY;
32
+ let maxHeight = Number.NEGATIVE_INFINITY;
33
+ for (let py = 0; py < res; py += 1) {
34
+ for (let px = 0; px < res; px += 1) {
35
+ const worldX = bounds.minX + ((px + 0.5) / res) * worldW;
36
+ const worldZ = bounds.minZ + ((py + 0.5) / res) * worldD;
37
+ const height = field.sampleHeight(worldX, worldZ);
38
+ heights[py * res + px] = height;
39
+ if (height < minHeight)
40
+ minHeight = height;
41
+ if (height > maxHeight)
42
+ maxHeight = height;
43
+ }
44
+ }
45
+ const span = maxHeight - minHeight || 1;
46
+ const image = context.createImageData(res, res);
47
+ for (let py = 0; py < res; py += 1) {
48
+ for (let px = 0; px < res; px += 1) {
49
+ const index = py * res + px;
50
+ const height = heights[index];
51
+ const normalized = (height - minHeight) / span;
52
+ let color;
53
+ if (height < waterLevel) {
54
+ color = [...water];
55
+ }
56
+ else if (region !== null) {
57
+ const worldX = bounds.minX + ((px + 0.5) / res) * worldW;
58
+ const worldZ = bounds.minZ + ((py + 0.5) / res) * worldD;
59
+ const sample = region.sampleRegion(worldX, worldZ);
60
+ const shade = 0.55 + normalized * 0.5;
61
+ color = [
62
+ Math.min(255, sample.tint[0] * 255 * shade),
63
+ Math.min(255, sample.tint[1] * 255 * shade),
64
+ Math.min(255, sample.tint[2] * 255 * shade),
65
+ ];
66
+ }
67
+ else {
68
+ color = mix(landLow, landHigh, normalized);
69
+ }
70
+ const offset = index * 4;
71
+ image.data[offset] = color[0];
72
+ image.data[offset + 1] = color[1];
73
+ image.data[offset + 2] = color[2];
74
+ image.data[offset + 3] = 255;
75
+ }
76
+ }
77
+ context.putImageData(image, 0, 0);
78
+ return { url: canvas.toDataURL("image/png"), bounds };
79
+ }
@@ -0,0 +1,8 @@
1
+ import * as THREE from "three";
2
+ import type { ModelMaterialOverride } from "@jgengine/core/game/playableGame";
3
+ /**
4
+ * Clones each `MeshStandardMaterial` under `root` and applies `override`'s
5
+ * color/finish onto the clone, so shared GLTF-cache scenes are never mutated
6
+ * in place (#151.3).
7
+ */
8
+ export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride): void;
@@ -0,0 +1,32 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * Clones each `MeshStandardMaterial` under `root` and applies `override`'s
4
+ * color/finish onto the clone, so shared GLTF-cache scenes are never mutated
5
+ * in place (#151.3).
6
+ */
7
+ export function applyMaterialOverride(root, override) {
8
+ root.traverse((node) => {
9
+ const mesh = node;
10
+ if (!mesh.isMesh)
11
+ return;
12
+ mesh.material = Array.isArray(mesh.material)
13
+ ? mesh.material.map((material) => overrideOne(material, override))
14
+ : overrideOne(mesh.material, override);
15
+ });
16
+ }
17
+ function overrideOne(material, override) {
18
+ if (!(material instanceof THREE.MeshStandardMaterial))
19
+ return material;
20
+ const clone = material.clone();
21
+ if (override.color !== undefined)
22
+ clone.color.set(override.color);
23
+ if (override.metalness !== undefined)
24
+ clone.metalness = override.metalness;
25
+ if (override.roughness !== undefined)
26
+ clone.roughness = override.roughness;
27
+ if (override.emissive !== undefined)
28
+ clone.emissive.set(override.emissive);
29
+ if (override.emissiveIntensity !== undefined)
30
+ clone.emissiveIntensity = override.emissiveIntensity;
31
+ return clone;
32
+ }
@@ -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
  }
@@ -0,0 +1,12 @@
1
+ import type { ContextMenu, ContextVerb } from "@jgengine/core/interaction/contextMenu";
2
+ import type { ScreenRect } from "@jgengine/core/scene/selection";
3
+ export declare function MarqueeBox({ rect }: {
4
+ rect: ScreenRect;
5
+ }): import("react").JSX.Element;
6
+ export declare function ContextMenuView({ menu, x, y, onPick, onClose, }: {
7
+ menu: ContextMenu;
8
+ x: number;
9
+ y: number;
10
+ onPick: (verb: ContextVerb) => void;
11
+ onClose: () => void;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function MarqueeBox({ rect }) {
3
+ return (_jsx("div", { className: "pointer-events-none absolute z-40 rounded-sm border border-emerald-300/80 bg-emerald-300/15", style: {
4
+ left: rect.minX,
5
+ top: rect.minY,
6
+ width: rect.maxX - rect.minX,
7
+ height: rect.maxY - rect.minY,
8
+ } }));
9
+ }
10
+ export function ContextMenuView({ menu, x, y, onPick, onClose, }) {
11
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40", onPointerDown: onClose, onContextMenu: (event) => event.preventDefault() }), _jsxs("div", { className: "pointer-events-auto absolute z-50 min-w-40 overflow-hidden rounded-md border border-white/15 bg-neutral-900/95 py-1 shadow-2xl backdrop-blur-sm", style: { left: x, top: y }, onContextMenu: (event) => event.preventDefault(), children: [_jsx("div", { className: "border-b border-white/10 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/45", children: menu.kind === "entity" ? "Unit" : "Object" }), menu.verbs.map((verb, index) => (_jsx("button", { type: "button", disabled: verb.disabled === true, onPointerDown: (event) => {
12
+ event.stopPropagation();
13
+ if (verb.disabled === true)
14
+ return;
15
+ onPick(verb);
16
+ }, className: "flex w-full items-center px-3 py-1.5 text-left text-[13px] text-white/85 hover:bg-emerald-400/20 hover:text-white disabled:cursor-not-allowed disabled:text-white/30 disabled:hover:bg-transparent", children: verb.label }, `${verb.command}-${index}`)))] })] }));
17
+ }
@@ -0,0 +1,4 @@
1
+ import type { PointerService } from "./pointerService.js";
2
+ export declare function PointerProbe({ service }: {
3
+ service: PointerService;
4
+ }): null;