@jgengine/shell 0.7.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.
- package/CHANGELOG.md +47 -0
- package/README.md +26 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +33 -1
- package/dist/GamePlayerShell.js +439 -28
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +2 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/cameraRigs.d.ts +5 -1
- package/dist/camera/cameraRigs.js +43 -6
- package/dist/camera/index.d.ts +4 -2
- package/dist/camera/index.js +4 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +40 -1
- package/dist/camera/rigMath.js +55 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +51 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
- package/dist/devtools/DevtoolsOverlay.js +334 -0
- package/dist/environment/Daylight.d.ts +50 -0
- package/dist/environment/Daylight.js +100 -0
- package/dist/environment/EnvironmentScene.js +18 -26
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +2 -0
- package/dist/environment/index.js +2 -0
- package/dist/materialOverride.d.ts +8 -0
- package/dist/materialOverride.js +32 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +14 -1
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +45 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +14 -0
- package/dist/render/modelRender.js +79 -0
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +1 -0
- package/dist/terrain/terrainMath.js +8 -2
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +75 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/WorldHud.d.ts +5 -1
- package/dist/world/WorldHud.js +6 -2
- package/dist/world/WorldItems.js +21 -2
- package/llms.txt +2382 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
|
@@ -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
|
+
}
|
package/dist/multiplayer.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { GameDefinition } from "@jgengine/core/game/defineGame";
|
|
2
|
-
import {
|
|
3
|
-
export type ShellMultiplayer =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
}
|
|
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
|
+
}>;
|
package/dist/multiplayer.js
CHANGED
|
@@ -1,21 +1,71 @@
|
|
|
1
|
+
import { adapterOf } from "@jgengine/core/runtime/adapter";
|
|
1
2
|
import { createWsBackend } from "@jgengine/ws/createWsBackend";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
feedActions
|
|
64
|
+
backend,
|
|
65
|
+
feedActions,
|
|
66
|
+
close: () => {
|
|
67
|
+
signaling.close();
|
|
68
|
+
guest.close();
|
|
69
|
+
},
|
|
20
70
|
};
|
|
21
71
|
}
|
|
@@ -8,18 +8,31 @@ export function PointerProbe({ service }) {
|
|
|
8
8
|
service.bind({ camera, scene, width: size.width, height: size.height });
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
const el = gl.domElement;
|
|
11
|
+
let locked = false;
|
|
11
12
|
const onMove = (event) => {
|
|
13
|
+
if (locked)
|
|
14
|
+
return;
|
|
12
15
|
const rect = el.getBoundingClientRect();
|
|
13
16
|
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
14
17
|
const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1);
|
|
15
18
|
service.setCursor(x, y, true);
|
|
16
19
|
};
|
|
17
|
-
const onLeave = () =>
|
|
20
|
+
const onLeave = () => {
|
|
21
|
+
if (!locked)
|
|
22
|
+
service.setCursor(0, 0, false);
|
|
23
|
+
};
|
|
24
|
+
const onLockChange = () => {
|
|
25
|
+
locked = document.pointerLockElement === el;
|
|
26
|
+
if (locked)
|
|
27
|
+
service.setCursor(0, 0, true);
|
|
28
|
+
};
|
|
18
29
|
el.addEventListener("pointermove", onMove);
|
|
19
30
|
el.addEventListener("pointerleave", onLeave);
|
|
31
|
+
document.addEventListener("pointerlockchange", onLockChange);
|
|
20
32
|
return () => {
|
|
21
33
|
el.removeEventListener("pointermove", onMove);
|
|
22
34
|
el.removeEventListener("pointerleave", onLeave);
|
|
35
|
+
document.removeEventListener("pointerlockchange", onLockChange);
|
|
23
36
|
service.bind(null);
|
|
24
37
|
};
|
|
25
38
|
}, [gl, service]);
|
|
@@ -11,6 +11,8 @@ interface PointerDeps {
|
|
|
11
11
|
export interface PointerService {
|
|
12
12
|
/** Cast the current cursor into the world; null when the cursor is off-canvas. */
|
|
13
13
|
worldHit(): PointerHit | null;
|
|
14
|
+
/** Cast from the viewport center regardless of cursor presence (pointer-lock aim); null before the probe binds. */
|
|
15
|
+
worldHitCenter(): PointerHit | null;
|
|
14
16
|
/** Project a world point to CSS pixels for the marquee / HUD; null before the probe binds. */
|
|
15
17
|
screenOf(world: PointerVec3): {
|
|
16
18
|
x: number;
|
|
@@ -11,14 +11,54 @@ function tagOf(object, key) {
|
|
|
11
11
|
}
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
+
function standardMaterialSample(material) {
|
|
15
|
+
const candidates = Array.isArray(material) ? material : [material];
|
|
16
|
+
const standard = candidates.find((entry) => entry instanceof THREE.MeshStandardMaterial);
|
|
17
|
+
if (standard === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return { color: `#${standard.color.getHexString()}`, metalness: standard.metalness, roughness: standard.roughness };
|
|
20
|
+
}
|
|
14
21
|
export function createPointerService() {
|
|
15
22
|
const raycaster = new THREE.Raycaster();
|
|
16
23
|
const ndc = new THREE.Vector2();
|
|
24
|
+
const centerNdc = new THREE.Vector2(0, 0);
|
|
17
25
|
const scratch = new THREE.Vector3();
|
|
18
26
|
const normalMatrix = new THREE.Matrix3();
|
|
19
27
|
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
20
28
|
let deps = null;
|
|
21
29
|
let cursorPresent = false;
|
|
30
|
+
function hitAtNdc(target) {
|
|
31
|
+
if (deps === null)
|
|
32
|
+
return null;
|
|
33
|
+
raycaster.setFromCamera(target, deps.camera);
|
|
34
|
+
const intersects = raycaster.intersectObjects(deps.scene.children, true);
|
|
35
|
+
for (const hit of intersects) {
|
|
36
|
+
const mesh = hit.object;
|
|
37
|
+
if (!mesh.isMesh)
|
|
38
|
+
continue;
|
|
39
|
+
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
40
|
+
let normal = [0, 1, 0];
|
|
41
|
+
if (hit.face !== null && hit.face !== undefined) {
|
|
42
|
+
normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
43
|
+
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
44
|
+
normal = [scratch.x, scratch.y, scratch.z];
|
|
45
|
+
}
|
|
46
|
+
const uv = hit.uv !== undefined ? { u: hit.uv.x, v: hit.uv.y } : undefined;
|
|
47
|
+
return {
|
|
48
|
+
point,
|
|
49
|
+
normal,
|
|
50
|
+
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
51
|
+
object: tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
52
|
+
...(uv !== undefined ? { uv } : {}),
|
|
53
|
+
material: standardMaterialSample(mesh.material),
|
|
54
|
+
...(hit.instanceId !== undefined ? { instanceId: hit.instanceId } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const grounded = raycaster.ray.intersectPlane(groundPlane, scratch);
|
|
58
|
+
if (grounded === null)
|
|
59
|
+
return null;
|
|
60
|
+
return { point: [grounded.x, grounded.y, grounded.z], normal: [0, 1, 0], entity: null, object: null };
|
|
61
|
+
}
|
|
22
62
|
return {
|
|
23
63
|
hasCursor: () => cursorPresent,
|
|
24
64
|
bind: (next) => {
|
|
@@ -29,31 +69,12 @@ export function createPointerService() {
|
|
|
29
69
|
cursorPresent = present;
|
|
30
70
|
},
|
|
31
71
|
worldHit() {
|
|
32
|
-
if (
|
|
33
|
-
return null;
|
|
34
|
-
raycaster.setFromCamera(ndc, deps.camera);
|
|
35
|
-
const intersects = raycaster.intersectObjects(deps.scene.children, true);
|
|
36
|
-
for (const hit of intersects) {
|
|
37
|
-
if (!hit.object.isMesh)
|
|
38
|
-
continue;
|
|
39
|
-
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
40
|
-
let normal = [0, 1, 0];
|
|
41
|
-
if (hit.face !== null && hit.face !== undefined) {
|
|
42
|
-
normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
43
|
-
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
44
|
-
normal = [scratch.x, scratch.y, scratch.z];
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
point,
|
|
48
|
-
normal,
|
|
49
|
-
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
50
|
-
object: tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const grounded = raycaster.ray.intersectPlane(groundPlane, scratch);
|
|
54
|
-
if (grounded === null)
|
|
72
|
+
if (!cursorPresent)
|
|
55
73
|
return null;
|
|
56
|
-
return
|
|
74
|
+
return hitAtNdc(ndc);
|
|
75
|
+
},
|
|
76
|
+
worldHitCenter() {
|
|
77
|
+
return hitAtNdc(centerNdc);
|
|
57
78
|
},
|
|
58
79
|
screenOf(world) {
|
|
59
80
|
if (deps === null)
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import type { PlayableGame as EnginePlayableGame } from "@jgengine/core/game/playableGame";
|
|
3
3
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
4
5
|
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
5
|
-
export type
|
|
6
|
+
export type RenderObject = (object: SceneObject) => ReactNode;
|
|
7
|
+
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject>;
|
|
6
8
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
9
|
+
export declare function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined;
|
package/dist/registry.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
|
|
3
|
+
export declare const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
+
export declare function cloneModelScene(source: THREE.Object3D): THREE.Object3D;
|
|
5
|
+
export declare function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[];
|
|
6
|
+
export interface PaintCanvas {
|
|
7
|
+
canvas: HTMLCanvasElement;
|
|
8
|
+
context: CanvasRenderingContext2D;
|
|
9
|
+
texture: THREE.CanvasTexture;
|
|
10
|
+
}
|
|
11
|
+
export declare function createPaintCanvas(seed: THREE.MeshStandardMaterial, size?: number): PaintCanvas;
|
|
12
|
+
export declare function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void;
|
|
13
|
+
export declare function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void;
|
|
14
|
+
export declare function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number;
|