@jgengine/shell 0.6.0 → 0.7.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 +167 -2
- package/dist/GamePlayerShell.js +210 -39
- package/dist/audio/AudioComponents.d.ts +12 -0
- package/dist/audio/AudioComponents.js +53 -0
- package/dist/audio/audioEngine.d.ts +23 -0
- package/dist/audio/audioEngine.js +110 -0
- package/dist/camera/GameCameraRig.d.ts +11 -0
- package/dist/camera/GameCameraRig.js +42 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +22 -0
- package/dist/camera/cameraRigs.js +485 -0
- package/dist/camera/index.d.ts +5 -0
- package/dist/camera/index.js +5 -0
- package/dist/camera/rigMath.d.ts +130 -0
- package/dist/camera/rigMath.js +338 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/demo/builderDemo.d.ts +2 -0
- package/dist/demo/builderDemo.js +189 -0
- package/dist/demo/mapDemo.d.ts +2 -0
- package/dist/demo/mapDemo.js +206 -0
- package/dist/demo/pointerDemo.d.ts +2 -0
- package/dist/demo/pointerDemo.js +151 -0
- package/dist/demo/sensorShowcase.d.ts +2 -0
- package/dist/demo/sensorShowcase.js +131 -0
- package/dist/demo/survivalDemo.d.ts +2 -0
- package/dist/demo/survivalDemo.js +291 -0
- package/dist/map/MapMarkerBeacons.d.ts +12 -0
- package/dist/map/MapMarkerBeacons.js +16 -0
- package/dist/map/index.d.ts +2 -0
- package/dist/map/index.js +2 -0
- package/dist/map/terrainMap.d.ts +23 -0
- package/dist/map/terrainMap.js +79 -0
- package/dist/pointer/PointerOverlays.d.ts +12 -0
- package/dist/pointer/PointerOverlays.js +17 -0
- package/dist/pointer/PointerProbe.d.ts +4 -0
- package/dist/pointer/PointerProbe.js +27 -0
- package/dist/pointer/pointerService.d.ts +24 -0
- package/dist/pointer/pointerService.js +68 -0
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/PlacementGhost.d.ts +8 -0
- package/dist/structures/PlacementGhost.js +11 -0
- package/dist/structures/index.d.ts +2 -0
- package/dist/structures/index.js +2 -0
- package/dist/structures.d.ts +1 -0
- package/dist/structures.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +22 -0
- package/dist/terrain/CarvedTerrain.js +19 -0
- package/dist/terrain/EditableGround.d.ts +11 -0
- package/dist/terrain/EditableGround.js +38 -0
- package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/index.d.ts +4 -1
- package/dist/terrain/index.js +4 -1
- package/dist/terrain/terrainMath.d.ts +14 -1
- package/dist/terrain/terrainMath.js +80 -0
- package/dist/vision/FrustumSensorHud.d.ts +21 -0
- package/dist/vision/FrustumSensorHud.js +67 -0
- package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
- package/dist/vision/HiddenStateProbeHud.js +11 -0
- package/dist/vision/RevealVision.d.ts +28 -0
- package/dist/vision/RevealVision.js +53 -0
- package/dist/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/index.d.ts +1 -0
- package/dist/weather/index.js +1 -0
- package/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +2 -0
- package/dist/world/WorldHud.js +70 -12
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +31 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/package.json +8 -4
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { defineGame } from "@jgengine/core/game/defineGame";
|
|
5
|
+
import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
6
|
+
import { noiseField } from "@jgengine/core/world/terrain";
|
|
7
|
+
import { createEditableTerrain, createTerraformBrush } from "@jgengine/core/world/terraform";
|
|
8
|
+
import { createPlacementController, } from "@jgengine/core/world/placementController";
|
|
9
|
+
import { createPlacedStructureStore, } from "@jgengine/core/world/placedStructureStore";
|
|
10
|
+
import { createPointerService } from "../pointer/pointerService.js";
|
|
11
|
+
import { EditableGround } from "../terrain/EditableGround.js";
|
|
12
|
+
import { TerraformBrushCursor } from "../terrain/TerraformBrushCursor.js";
|
|
13
|
+
import { PlacementGhost } from "../structures/PlacementGhost.js";
|
|
14
|
+
const BOUNDS = { minX: -20, minZ: -20, maxX: 20, maxZ: 20 };
|
|
15
|
+
const FOOTPRINT = { w: 3, d: 3 };
|
|
16
|
+
const HUT_HEIGHT = 2.4;
|
|
17
|
+
const terrain = createEditableTerrain({
|
|
18
|
+
bounds: BOUNDS,
|
|
19
|
+
base: noiseField({ seed: "builder", amplitude: 0.6, frequency: 0.04 }),
|
|
20
|
+
cellSize: 1,
|
|
21
|
+
});
|
|
22
|
+
const brush = createTerraformBrush(terrain, { radius: 3.5, strength: 0.6 });
|
|
23
|
+
const structures = createPlacedStructureStore();
|
|
24
|
+
function seedScene() {
|
|
25
|
+
structures.clear();
|
|
26
|
+
terrain.reset();
|
|
27
|
+
terrain.apply({ mode: "raise", center: [8, -6], radius: 7, strength: 3 });
|
|
28
|
+
terrain.apply({ mode: "flatten", center: [-6, 4], radius: 6, strength: 1, target: 0 });
|
|
29
|
+
for (let i = 0; i < 6; i += 1)
|
|
30
|
+
terrain.apply({ mode: "paint", center: [-14 + i * 3, -12 + i * 2], radius: 1.6, surface: "path" });
|
|
31
|
+
structures.add({ catalogId: "hut", position: [-6, terrain.sampleHeight(-6, 4), 4], rotationY: 0 });
|
|
32
|
+
structures.add({ catalogId: "hut", position: [-2, terrain.sampleHeight(-2, 6), 6], rotationY: Math.PI / 6 });
|
|
33
|
+
structures.add({ catalogId: "hut", position: [2, terrain.sampleHeight(2, 3), 3], rotationY: -Math.PI / 5 });
|
|
34
|
+
}
|
|
35
|
+
seedScene();
|
|
36
|
+
function Hut({ structure }) {
|
|
37
|
+
return (_jsxs("group", { position: [structure.position[0], structure.position[1], structure.position[2]], "rotation-y": structure.rotationY, children: [_jsxs("mesh", { "position-y": HUT_HEIGHT / 2, castShadow: true, children: [_jsx("boxGeometry", { args: [FOOTPRINT.w, HUT_HEIGHT, FOOTPRINT.d] }), _jsx("meshStandardMaterial", { color: "#b08968", roughness: 0.85 })] }), _jsxs("mesh", { "position-y": HUT_HEIGHT + 0.55, "rotation-y": Math.PI / 4, children: [_jsx("coneGeometry", { args: [FOOTPRINT.w * 0.82, 1.3, 4] }), _jsx("meshStandardMaterial", { color: "#6d4c3d", roughness: 0.9 })] })] }));
|
|
38
|
+
}
|
|
39
|
+
function obstaclesFrom(list) {
|
|
40
|
+
return list.map((s) => ({
|
|
41
|
+
id: s.id,
|
|
42
|
+
aabb: {
|
|
43
|
+
minX: s.position[0] - FOOTPRINT.w / 2,
|
|
44
|
+
maxX: s.position[0] + FOOTPRINT.w / 2,
|
|
45
|
+
minZ: s.position[2] - FOOTPRINT.d / 2,
|
|
46
|
+
maxZ: s.position[2] + FOOTPRINT.d / 2,
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function BuilderScene() {
|
|
51
|
+
const three = useThree();
|
|
52
|
+
const pointer = useMemo(() => createPointerService(), []);
|
|
53
|
+
const controller = useRef(createPlacementController({ footprint: FOOTPRINT, snapMode: "grid", grid: 1, rules: { bounds: BOUNDS } }));
|
|
54
|
+
const [preview, setPreview] = useState(null);
|
|
55
|
+
const [brushCenter, setBrushCenter] = useState([8, -6]);
|
|
56
|
+
const [placed, setPlaced] = useState(structures.list());
|
|
57
|
+
const [version, setVersion] = useState(0);
|
|
58
|
+
const stateRef = useRef({ mode: "build", terraformMode: "raise", snapMode: "grid" });
|
|
59
|
+
useEffect(() => structures.subscribe(() => setPlaced(structures.list())), []);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const seed = controller.current.hover({ point: [-6, terrain.sampleHeight(-6, -4), -4], normal: [0, 1, 0] });
|
|
62
|
+
setPreview(seed);
|
|
63
|
+
}, []);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const dom = three.gl.domElement;
|
|
66
|
+
const onMove = (event) => {
|
|
67
|
+
const rect = dom.getBoundingClientRect();
|
|
68
|
+
const ndcX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
69
|
+
const ndcY = -(((event.clientY - rect.top) / rect.height) * 2 - 1);
|
|
70
|
+
pointer.setCursor(ndcX, ndcY, true);
|
|
71
|
+
pointer.bind({ camera: three.camera, scene: three.scene, width: rect.width, height: rect.height });
|
|
72
|
+
const hit = pointer.worldHit();
|
|
73
|
+
if (hit === null)
|
|
74
|
+
return;
|
|
75
|
+
const st = stateRef.current;
|
|
76
|
+
if (st.mode === "build") {
|
|
77
|
+
controller.current.setRules({ bounds: BOUNDS, obstacles: obstaclesFrom(structures.list()) });
|
|
78
|
+
setPreview(controller.current.hover({ point: hit.point, normal: hit.normal }));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setBrushCenter([hit.point[0], hit.point[2]]);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const onLeave = () => pointer.setCursor(0, 0, false);
|
|
85
|
+
const onDown = (event) => {
|
|
86
|
+
if (event.button !== 0)
|
|
87
|
+
return;
|
|
88
|
+
const st = stateRef.current;
|
|
89
|
+
if (st.mode === "build") {
|
|
90
|
+
const commit = controller.current.commit();
|
|
91
|
+
if (commit === null)
|
|
92
|
+
return;
|
|
93
|
+
structures.add({
|
|
94
|
+
catalogId: "hut",
|
|
95
|
+
position: [commit.center[0], terrain.sampleHeight(commit.center[0], commit.center[1]), commit.center[1]],
|
|
96
|
+
rotationY: commit.rotationY,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const center = brushCenter;
|
|
101
|
+
if (center === null)
|
|
102
|
+
return;
|
|
103
|
+
if (st.terraformMode === "raise")
|
|
104
|
+
brush.raise(center);
|
|
105
|
+
else if (st.terraformMode === "lower")
|
|
106
|
+
brush.lower(center);
|
|
107
|
+
else if (st.terraformMode === "flatten")
|
|
108
|
+
brush.flatten(center);
|
|
109
|
+
else
|
|
110
|
+
brush.paint(center, "gravel");
|
|
111
|
+
setVersion((v) => v + 1);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const onKey = (event) => {
|
|
115
|
+
const st = stateRef.current;
|
|
116
|
+
if (event.code === "KeyB")
|
|
117
|
+
st.mode = "build";
|
|
118
|
+
else if (event.code === "KeyT")
|
|
119
|
+
st.mode = "terraform";
|
|
120
|
+
else if (event.code === "KeyR")
|
|
121
|
+
setPreview(controller.current.rotate());
|
|
122
|
+
else if (event.code === "KeyG") {
|
|
123
|
+
st.snapMode = controller.current.cycleSnapMode();
|
|
124
|
+
setPreview(controller.current.current());
|
|
125
|
+
}
|
|
126
|
+
else if (event.code === "Digit1")
|
|
127
|
+
st.terraformMode = "raise";
|
|
128
|
+
else if (event.code === "Digit2")
|
|
129
|
+
st.terraformMode = "lower";
|
|
130
|
+
else if (event.code === "Digit3")
|
|
131
|
+
st.terraformMode = "flatten";
|
|
132
|
+
else if (event.code === "Digit4")
|
|
133
|
+
st.terraformMode = "paint";
|
|
134
|
+
};
|
|
135
|
+
dom.addEventListener("pointermove", onMove);
|
|
136
|
+
dom.addEventListener("pointerleave", onLeave);
|
|
137
|
+
dom.addEventListener("pointerdown", onDown);
|
|
138
|
+
window.addEventListener("keydown", onKey);
|
|
139
|
+
return () => {
|
|
140
|
+
dom.removeEventListener("pointermove", onMove);
|
|
141
|
+
dom.removeEventListener("pointerleave", onLeave);
|
|
142
|
+
dom.removeEventListener("pointerdown", onDown);
|
|
143
|
+
window.removeEventListener("keydown", onKey);
|
|
144
|
+
};
|
|
145
|
+
}, [pointer, three, brushCenter]);
|
|
146
|
+
const invalidGhost = useMemo(() => {
|
|
147
|
+
const c = createPlacementController({ footprint: FOOTPRINT, snapMode: "free", rules: { bounds: BOUNDS, obstacles: obstaclesFrom(structures.list()) } });
|
|
148
|
+
return c.hover({ point: [-6, terrain.sampleHeight(-6, 4) + 0.05, 4], normal: [0, 1, 0] });
|
|
149
|
+
}, [placed]);
|
|
150
|
+
return (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: ["#cfe8ff", "#3a3320", 0.8] }), _jsx(EditableGround, { terrain: terrain, bounds: BOUNDS, version: version, baseColor: "#4b7f3f" }), placed.map((structure) => (_jsx(Hut, { structure: structure }, structure.id))), _jsx(PlacementGhost, { preview: preview, height: HUT_HEIGHT }), _jsx(PlacementGhost, { preview: invalidGhost, height: HUT_HEIGHT }), _jsx(TerraformBrushCursor, { center: brushCenter, radius: brush.config().radius, mode: "raise" })] }));
|
|
151
|
+
}
|
|
152
|
+
const game = defineGame({
|
|
153
|
+
name: "builder-sandbox",
|
|
154
|
+
assets: createAssetCatalog(),
|
|
155
|
+
multiplayer: null,
|
|
156
|
+
inventories: {},
|
|
157
|
+
input: {
|
|
158
|
+
moveForward: ["KeyW"],
|
|
159
|
+
moveBack: ["KeyS"],
|
|
160
|
+
moveLeft: ["KeyA"],
|
|
161
|
+
moveRight: ["KeyD"],
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
function onInit(_ctx) {
|
|
165
|
+
seedScene();
|
|
166
|
+
}
|
|
167
|
+
function onNewPlayer(_ctx) { }
|
|
168
|
+
function onTick(_ctx, _dt) { }
|
|
169
|
+
function BuilderUI() {
|
|
170
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-sans text-white", children: [_jsxs("div", { className: "absolute left-4 top-4 rounded-lg border border-amber-300/25 bg-neutral-900/80 px-4 py-3 shadow-xl backdrop-blur-sm", children: [_jsx("h1", { className: "text-sm font-semibold tracking-wide text-amber-200", children: "Build & Terraform Sandbox" }), _jsx("p", { className: "text-xs text-white/60", children: "Ghost placement \u00B7 valid/invalid tint \u00B7 terraform brush" })] }), _jsxs("div", { className: "absolute bottom-4 left-4 w-72 rounded-lg border border-white/15 bg-neutral-900/80 p-3 text-[13px] leading-6 shadow-xl backdrop-blur-sm", children: [_jsx("div", { className: "mb-1 border-b border-white/10 pb-1 text-[10px] font-semibold uppercase tracking-widest text-white/45", children: "Tools" }), _jsxs("ul", { className: "text-white/80", children: [_jsxs("li", { children: [_jsx("span", { className: "text-amber-300", children: "B" }), " \u2014 build mode \u00B7 ", _jsx("span", { className: "text-amber-300", children: "Left-click" }), " place hut"] }), _jsxs("li", { children: [_jsx("span", { className: "text-amber-300", children: "R" }), " \u2014 rotate ghost \u00B7 ", _jsx("span", { className: "text-amber-300", children: "G" }), " \u2014 grid/free/surface snap"] }), _jsxs("li", { children: [_jsx("span", { className: "text-amber-300", children: "T" }), " \u2014 terraform \u00B7 ", _jsx("span", { className: "text-amber-300", children: "1-4" }), " raise/lower/flatten/paint"] }), _jsxs("li", { children: [_jsx("span", { className: "text-emerald-300", children: "Green ghost" }), " valid \u00B7 ", _jsx("span", { className: "text-red-300", children: "Red ghost" }), " blocked"] })] })] })] }));
|
|
171
|
+
}
|
|
172
|
+
export const builderDemoGame = {
|
|
173
|
+
game,
|
|
174
|
+
content: {},
|
|
175
|
+
loop: { onInit, onNewPlayer, onTick },
|
|
176
|
+
GameUI: BuilderUI,
|
|
177
|
+
environment: BuilderScene,
|
|
178
|
+
camera: {
|
|
179
|
+
perspective: "third",
|
|
180
|
+
followEnabled: false,
|
|
181
|
+
initialDistance: 46,
|
|
182
|
+
initialHeight: 40,
|
|
183
|
+
minDistance: 14,
|
|
184
|
+
maxDistance: 120,
|
|
185
|
+
targetHeight: 1,
|
|
186
|
+
targetOffset: { x: 0, z: -2 },
|
|
187
|
+
maxPolarAngle: 1.15,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
import { defineGame } from "@jgengine/core/game/defineGame";
|
|
4
|
+
import { createPingSystem, DEFAULT_PING_CATEGORIES, PING_FEED_ACTION, } from "@jgengine/core/game/ping";
|
|
5
|
+
import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
6
|
+
import { environment, grass, terrain } from "@jgengine/core/world/features";
|
|
7
|
+
import { createFogField } from "@jgengine/core/world/fog";
|
|
8
|
+
import { createMarkerSet } from "@jgengine/core/world/markers";
|
|
9
|
+
import { headingToBearing } from "@jgengine/core/world/minimap";
|
|
10
|
+
import { resolveTerrainField } from "@jgengine/core/world/terrain";
|
|
11
|
+
import { Compass, Minimap, WorldMap } from "@jgengine/react/map";
|
|
12
|
+
import { usePlayer, useSceneEntities, useFeed } from "@jgengine/react/hooks";
|
|
13
|
+
import { EnvironmentScene } from "../environment/EnvironmentScene.js";
|
|
14
|
+
import { bakeTerrainMap } from "../map/terrainMap.js";
|
|
15
|
+
import { MapMarkerBeacons } from "../map/MapMarkerBeacons.js";
|
|
16
|
+
const SCOUT = "scout";
|
|
17
|
+
const RAIDER = "raider";
|
|
18
|
+
const CACHE = "cache";
|
|
19
|
+
const HAZARD = "hazard";
|
|
20
|
+
const RAIDER_ID = "raider-1";
|
|
21
|
+
const RAIDER_MARKER = "raider-marker";
|
|
22
|
+
const BOUNDS = { minX: -60, minZ: -60, maxX: 60, maxZ: 60 };
|
|
23
|
+
const FOG_CELL = 6;
|
|
24
|
+
const terrainFeature = environment({
|
|
25
|
+
terrain: terrain({ bounds: { w: 120, d: 120 }, height: 5, frequency: 0.03, seed: "extraction", waterLevel: -2 }),
|
|
26
|
+
vegetation: grass({ area: { w: 100, d: 100 }, density: 4, colors: ["#2f4f1e", "#8fbf4a"], seed: "extraction" }),
|
|
27
|
+
});
|
|
28
|
+
const objectPingCategory = {
|
|
29
|
+
[CACHE]: "loot",
|
|
30
|
+
[HAZARD]: "danger",
|
|
31
|
+
};
|
|
32
|
+
const entityCatalog = {
|
|
33
|
+
[SCOUT]: { movement: { walkSpeed: 6 }, role: "player" },
|
|
34
|
+
[RAIDER]: { movement: { walkSpeed: 4 }, role: "enemy" },
|
|
35
|
+
};
|
|
36
|
+
const objectCatalog = {
|
|
37
|
+
[CACHE]: {},
|
|
38
|
+
[HAZARD]: {},
|
|
39
|
+
};
|
|
40
|
+
const OBJECTIVES = [
|
|
41
|
+
{ id: "obj-extract", label: "Extraction", position: [44, 0, -46] },
|
|
42
|
+
{ id: "obj-relay", label: "Relay", position: [-40, 0, 30] },
|
|
43
|
+
];
|
|
44
|
+
const CACHES = [
|
|
45
|
+
[18, 0, 12],
|
|
46
|
+
[-26, 0, -18],
|
|
47
|
+
[8, 0, -34],
|
|
48
|
+
];
|
|
49
|
+
const HAZARDS = [[-12, 0, -6]];
|
|
50
|
+
const terrainField = resolveTerrainField(terrainFeature.terrain);
|
|
51
|
+
const markerSet = createMarkerSet();
|
|
52
|
+
const fogField = createFogField({ bounds: BOUNDS, cellSize: FOG_CELL });
|
|
53
|
+
let pingSystem = null;
|
|
54
|
+
let raiderAngle = 0;
|
|
55
|
+
let lastPlayerXZ = null;
|
|
56
|
+
let worldMapOpen = true;
|
|
57
|
+
const mapListeners = new Set();
|
|
58
|
+
function setWorldMapOpen(open) {
|
|
59
|
+
worldMapOpen = open;
|
|
60
|
+
for (const listener of mapListeners)
|
|
61
|
+
listener();
|
|
62
|
+
}
|
|
63
|
+
const game = defineGame({
|
|
64
|
+
name: "extraction-map",
|
|
65
|
+
assets: createAssetCatalog(),
|
|
66
|
+
multiplayer: null,
|
|
67
|
+
inventories: {},
|
|
68
|
+
input: {
|
|
69
|
+
moveForward: ["KeyW"],
|
|
70
|
+
moveBack: ["KeyS"],
|
|
71
|
+
moveLeft: ["KeyA"],
|
|
72
|
+
moveRight: ["KeyD"],
|
|
73
|
+
ping: ["KeyQ"],
|
|
74
|
+
toggleMap: ["KeyM"],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
function seedMarkers() {
|
|
78
|
+
markerSet.clear();
|
|
79
|
+
for (const objective of OBJECTIVES) {
|
|
80
|
+
markerSet.add({ id: objective.id, kind: "objective", position: objective.position, label: objective.label });
|
|
81
|
+
}
|
|
82
|
+
CACHES.forEach((position, index) => {
|
|
83
|
+
markerSet.add({ id: `cache-${index}`, kind: "loot", position, label: "Supply cache" });
|
|
84
|
+
});
|
|
85
|
+
HAZARDS.forEach((position, index) => {
|
|
86
|
+
markerSet.add({ id: `hazard-${index}`, kind: "danger", position, label: "Minefield" });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function onInit(ctx) {
|
|
90
|
+
raiderAngle = 0;
|
|
91
|
+
lastPlayerXZ = null;
|
|
92
|
+
worldMapOpen = true;
|
|
93
|
+
fogField.reset();
|
|
94
|
+
seedMarkers();
|
|
95
|
+
fogField.reveal(0, 0, 22);
|
|
96
|
+
fogField.revealAlong([0, 0], [18, 12], 10);
|
|
97
|
+
fogField.revealAlong([0, 0], [-26, -18], 8);
|
|
98
|
+
pingSystem = createPingSystem({
|
|
99
|
+
markers: markerSet,
|
|
100
|
+
feed: { push: (action, entry) => ctx.game.feed.push(action, entry) },
|
|
101
|
+
party: ctx.game.social.party,
|
|
102
|
+
ttlMs: 45_000,
|
|
103
|
+
categories: DEFAULT_PING_CATEGORIES,
|
|
104
|
+
classify: {
|
|
105
|
+
roleOf: (id) => {
|
|
106
|
+
const entity = ctx.scene.entity.get(id);
|
|
107
|
+
return entity === null ? null : entityCatalog[entity.name]?.role ?? null;
|
|
108
|
+
},
|
|
109
|
+
categoryOf: (id) => {
|
|
110
|
+
const object = ctx.scene.object.get(id);
|
|
111
|
+
return object === null ? null : objectPingCategory[object.catalogId] ?? null;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
ctx.game.commands.define("map.ping", {
|
|
116
|
+
apply(state, input) {
|
|
117
|
+
if (pingSystem === null)
|
|
118
|
+
return state;
|
|
119
|
+
pingSystem.ping(state.player.userId, input);
|
|
120
|
+
return state;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
ctx.game.commands.define("ui.toggleMap", {
|
|
124
|
+
apply(state) {
|
|
125
|
+
setWorldMapOpen(!worldMapOpen);
|
|
126
|
+
return state;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
for (const position of CACHES)
|
|
130
|
+
ctx.scene.object.place(CACHE, position[0], 0, position[2]);
|
|
131
|
+
for (const position of HAZARDS)
|
|
132
|
+
ctx.scene.object.place(HAZARD, position[0], 0, position[2]);
|
|
133
|
+
}
|
|
134
|
+
function onNewPlayer(ctx) {
|
|
135
|
+
ctx.scene.entity.spawn(SCOUT, { id: ctx.player.userId, position: [0, 0, 0], role: "player" });
|
|
136
|
+
ctx.scene.entity.spawn(RAIDER, { id: RAIDER_ID, position: [24, 0, -24], role: "npc" });
|
|
137
|
+
markerSet.add({ id: RAIDER_MARKER, kind: "enemy", position: [24, 0, -24], label: "Raider" });
|
|
138
|
+
if (pingSystem !== null) {
|
|
139
|
+
pingSystem.ping(ctx.player.userId, {
|
|
140
|
+
point: [30, 0, 8],
|
|
141
|
+
normal: [0, 1, 0],
|
|
142
|
+
entity: null,
|
|
143
|
+
object: null,
|
|
144
|
+
}, "location");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function onTick(ctx, dt) {
|
|
148
|
+
raiderAngle += dt * 0.4;
|
|
149
|
+
const raiderX = 24 + Math.cos(raiderAngle) * 14;
|
|
150
|
+
const raiderZ = -24 + Math.sin(raiderAngle) * 14;
|
|
151
|
+
ctx.scene.entity.setPose(RAIDER_ID, { position: [raiderX, 0, raiderZ], rotationY: raiderAngle, dt });
|
|
152
|
+
markerSet.add({ id: RAIDER_MARKER, kind: "enemy", position: [raiderX, 0, raiderZ], label: "Raider" });
|
|
153
|
+
const player = ctx.scene.entity.get(ctx.player.userId);
|
|
154
|
+
if (player !== null) {
|
|
155
|
+
const here = [player.position[0], player.position[2]];
|
|
156
|
+
if (lastPlayerXZ === null)
|
|
157
|
+
fogField.reveal(here[0], here[1], 14);
|
|
158
|
+
else
|
|
159
|
+
fogField.revealAlong(lastPlayerXZ, here, 14);
|
|
160
|
+
lastPlayerXZ = here;
|
|
161
|
+
}
|
|
162
|
+
markerSet.prune(Date.now());
|
|
163
|
+
}
|
|
164
|
+
function useWorldMapOpen() {
|
|
165
|
+
return useSyncExternalStore((listener) => {
|
|
166
|
+
mapListeners.add(listener);
|
|
167
|
+
return () => mapListeners.delete(listener);
|
|
168
|
+
}, () => worldMapOpen, () => worldMapOpen);
|
|
169
|
+
}
|
|
170
|
+
function MapUI() {
|
|
171
|
+
const player = usePlayer();
|
|
172
|
+
const entities = useSceneEntities();
|
|
173
|
+
const pings = useFeed({ action: PING_FEED_ACTION, limit: 3 });
|
|
174
|
+
const open = useWorldMapOpen();
|
|
175
|
+
const self = entities.find((entity) => entity.id === player.userId) ??
|
|
176
|
+
entities.find((entity) => entity.role === "player") ??
|
|
177
|
+
null;
|
|
178
|
+
const center = self === null ? [0, 0] : [self.position[0], self.position[2]];
|
|
179
|
+
const heading = self === null ? 0 : headingToBearing(self.rotationY);
|
|
180
|
+
const baked = useMemo(() => bakeTerrainMap(terrainField, BOUNDS, { resolution: 160 }), []);
|
|
181
|
+
const background = baked?.url;
|
|
182
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-sans text-white", children: [_jsx("div", { className: "absolute left-1/2 top-4 -translate-x-1/2", children: _jsx(Compass, { heading: heading, center: center, markers: markerSet, width: 360 }) }), open ? (_jsx("div", { className: "pointer-events-auto absolute left-4 top-4", children: _jsx(WorldMap, { markers: markerSet, bounds: BOUNDS, fog: fogField, player: center, heading: heading, background: background, width: 340, onClose: () => setWorldMapOpen(false) }) })) : (_jsxs("div", { className: "absolute left-4 top-16 rounded-md border border-white/15 bg-neutral-900/80 px-3 py-2 text-[11px] text-white/60", children: ["Press ", _jsx("span", { className: "text-emerald-300", children: "M" }), " for the world map"] })), _jsx("div", { className: "absolute bottom-4 right-4", children: _jsx(Minimap, { markers: markerSet, fog: fogField, center: center, heading: heading, worldRadius: 46, size: 188, background: background, mapBounds: BOUNDS, title: "Sector" }) }), _jsxs("div", { className: "absolute bottom-4 left-4 w-64 rounded-lg border border-white/15 bg-neutral-900/80 p-3 text-[12px] leading-6 shadow-xl backdrop-blur-sm", children: [_jsx("div", { className: "mb-1 border-b border-white/10 pb-1 text-[10px] font-semibold uppercase tracking-widest text-white/45", children: "Comms" }), pings.length === 0 ? (_jsx("p", { className: "text-white/50", children: "Press Q to ping what you aim at." })) : (_jsx("ul", { className: "text-white/85", children: [...pings].reverse().map((entry, index) => {
|
|
183
|
+
const payload = entry.data;
|
|
184
|
+
return (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-cyan-300", children: "\u25CE" }), payload.callout] }, `${entry.at}-${index}`));
|
|
185
|
+
}) }))] })] }));
|
|
186
|
+
}
|
|
187
|
+
export const mapDemoGame = {
|
|
188
|
+
game,
|
|
189
|
+
content: {
|
|
190
|
+
entityById: (catalogId) => entityCatalog[catalogId] ?? null,
|
|
191
|
+
objectById: (catalogId) => objectCatalog[catalogId] ?? null,
|
|
192
|
+
},
|
|
193
|
+
loop: { onInit, onNewPlayer, onTick },
|
|
194
|
+
GameUI: MapUI,
|
|
195
|
+
environment: () => _jsx(EnvironmentScene, { feature: terrainFeature }),
|
|
196
|
+
WorldOverlay: () => _jsx(MapMarkerBeacons, { markers: markerSet }),
|
|
197
|
+
pointer: { pingCommand: "map.ping" },
|
|
198
|
+
camera: {
|
|
199
|
+
initialDistance: 40,
|
|
200
|
+
initialHeight: 34,
|
|
201
|
+
minDistance: 16,
|
|
202
|
+
maxDistance: 80,
|
|
203
|
+
targetHeight: 0,
|
|
204
|
+
maxPolarAngle: 1.35,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineGame } from "@jgengine/core/game/defineGame";
|
|
3
|
+
import { contextVerb } from "@jgengine/core/interaction/contextMenu";
|
|
4
|
+
import { createNavGrid, findPath, } from "@jgengine/core/nav/navGrid";
|
|
5
|
+
import { advancePathFollow, createPathFollow, pathFromNav, } from "@jgengine/core/nav/pathFollow";
|
|
6
|
+
import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
7
|
+
const VILLAGER = "villager";
|
|
8
|
+
const CREEP = "creep";
|
|
9
|
+
const BOULDER = "boulder";
|
|
10
|
+
const CREEP_ID = "creep-1";
|
|
11
|
+
const BOUNDS = { minX: -24, minZ: -24, maxX: 24, maxZ: 24 };
|
|
12
|
+
const CELL = 1.5;
|
|
13
|
+
const OBSTACLES = [
|
|
14
|
+
[-6, -4],
|
|
15
|
+
[5, 6],
|
|
16
|
+
[8, -9],
|
|
17
|
+
[-11, 8],
|
|
18
|
+
];
|
|
19
|
+
const SQUAD = [
|
|
20
|
+
[3, 0, 2],
|
|
21
|
+
[-3, 0, 2],
|
|
22
|
+
[0, 0, 4],
|
|
23
|
+
];
|
|
24
|
+
const CREEP_PATH = [
|
|
25
|
+
[-22, 0, -16],
|
|
26
|
+
[-8, 0, -16],
|
|
27
|
+
[-8, 0, 10],
|
|
28
|
+
[10, 0, 10],
|
|
29
|
+
[10, 0, -14],
|
|
30
|
+
[22, 0, -14],
|
|
31
|
+
];
|
|
32
|
+
const entityCatalog = {
|
|
33
|
+
[VILLAGER]: {
|
|
34
|
+
movement: { walkSpeed: 5 },
|
|
35
|
+
role: "npc",
|
|
36
|
+
verbs: [contextVerb("Stop", "unit.stop"), contextVerb("Cheer", "unit.cheer")],
|
|
37
|
+
},
|
|
38
|
+
[CREEP]: { role: "enemy" },
|
|
39
|
+
};
|
|
40
|
+
const objectCatalog = {
|
|
41
|
+
[BOULDER]: { verbs: [contextVerb("Inspect", "world.inspect")] },
|
|
42
|
+
};
|
|
43
|
+
let nav = null;
|
|
44
|
+
const movers = new Map();
|
|
45
|
+
let creep = null;
|
|
46
|
+
function orderUnit(ctx, id, point) {
|
|
47
|
+
if (nav === null)
|
|
48
|
+
return;
|
|
49
|
+
const unit = ctx.scene.entity.get(id);
|
|
50
|
+
if (unit === null)
|
|
51
|
+
return;
|
|
52
|
+
const route = findPath(nav, [unit.position[0], unit.position[2]], [point[0], point[2]], { clearance: 1.4 });
|
|
53
|
+
if (route === null)
|
|
54
|
+
return;
|
|
55
|
+
const config = { waypoints: pathFromNav(route, 0), speed: unit.movement.walkSpeed ?? 5 };
|
|
56
|
+
movers.set(id, { config, state: createPathFollow(config) });
|
|
57
|
+
}
|
|
58
|
+
const game = defineGame({
|
|
59
|
+
name: "pointer-commander",
|
|
60
|
+
assets: createAssetCatalog(),
|
|
61
|
+
multiplayer: null,
|
|
62
|
+
inventories: {},
|
|
63
|
+
input: {
|
|
64
|
+
moveForward: ["KeyW"],
|
|
65
|
+
moveBack: ["KeyS"],
|
|
66
|
+
moveLeft: ["KeyA"],
|
|
67
|
+
moveRight: ["KeyD"],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
function onInit(ctx) {
|
|
71
|
+
movers.clear();
|
|
72
|
+
creep = null;
|
|
73
|
+
const grid = createNavGrid({ bounds: BOUNDS, cellSize: CELL });
|
|
74
|
+
for (const [x, z] of OBSTACLES) {
|
|
75
|
+
ctx.scene.object.place(BOULDER, x, 0, z);
|
|
76
|
+
grid.blockAabb({ minX: x - 1.6, minZ: z - 1.6, maxX: x + 1.6, maxZ: z + 1.6 });
|
|
77
|
+
}
|
|
78
|
+
nav = grid;
|
|
79
|
+
ctx.game.commands.define("unit.order", {
|
|
80
|
+
apply(state, input) {
|
|
81
|
+
for (const id of input.selection)
|
|
82
|
+
orderUnit(state, id, input.point);
|
|
83
|
+
return state;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
ctx.game.commands.define("unit.stop", {
|
|
87
|
+
apply(state, input) {
|
|
88
|
+
movers.delete(input.target);
|
|
89
|
+
return state;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
ctx.game.commands.define("unit.cheer", {
|
|
93
|
+
apply(state, input) {
|
|
94
|
+
state.scene.entity.floatText({ instanceId: input.target, text: "For the realm!", kind: "info" });
|
|
95
|
+
return state;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
ctx.game.commands.define("world.inspect", {
|
|
99
|
+
apply(state, input) {
|
|
100
|
+
if (input.point !== undefined)
|
|
101
|
+
state.scene.entity.floatText({ position: input.point, text: "Granite", kind: "info" });
|
|
102
|
+
return state;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function onNewPlayer(ctx) {
|
|
107
|
+
ctx.scene.entity.spawn(VILLAGER, { id: ctx.player.userId, position: [0, 0, 0], role: "player" });
|
|
108
|
+
for (const position of SQUAD)
|
|
109
|
+
ctx.scene.entity.spawn(VILLAGER, { position, role: "npc" });
|
|
110
|
+
const config = { waypoints: CREEP_PATH, speed: 3.5, loop: true };
|
|
111
|
+
creep = { config, state: createPathFollow(config) };
|
|
112
|
+
ctx.scene.entity.spawn(CREEP, { id: CREEP_ID, position: CREEP_PATH[0], role: "npc" });
|
|
113
|
+
}
|
|
114
|
+
function onTick(ctx, dt) {
|
|
115
|
+
for (const [id, mover] of movers) {
|
|
116
|
+
mover.state = advancePathFollow(mover.config, mover.state, dt);
|
|
117
|
+
ctx.scene.entity.setPose(id, { position: mover.state.position, rotationY: mover.state.heading, dt });
|
|
118
|
+
if (mover.state.done)
|
|
119
|
+
movers.delete(id);
|
|
120
|
+
}
|
|
121
|
+
if (creep !== null) {
|
|
122
|
+
creep.state = advancePathFollow(creep.config, creep.state, dt);
|
|
123
|
+
ctx.scene.entity.setPose(CREEP_ID, { position: creep.state.position, rotationY: creep.state.heading, dt });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function CommanderUI() {
|
|
127
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-sans text-white", children: [_jsxs("div", { className: "absolute left-4 top-4 rounded-lg border border-emerald-300/25 bg-neutral-900/80 px-4 py-3 shadow-xl backdrop-blur-sm", children: [_jsx("h1", { className: "text-sm font-semibold tracking-wide text-emerald-200", children: "RTS Commander" }), _jsx("p", { className: "text-xs text-white/60", children: "Pointer-driven unit command" })] }), _jsxs("div", { className: "absolute bottom-4 left-4 w-72 rounded-lg border border-white/15 bg-neutral-900/80 p-3 text-[13px] leading-6 shadow-xl backdrop-blur-sm", children: [_jsx("div", { className: "mb-1 border-b border-white/10 pb-1 text-[10px] font-semibold uppercase tracking-widest text-white/45", children: "Controls" }), _jsxs("ul", { className: "text-white/80", children: [_jsxs("li", { children: [_jsx("span", { className: "text-emerald-300", children: "Left-drag" }), " \u2014 box-select villagers"] }), _jsxs("li", { children: [_jsx("span", { className: "text-emerald-300", children: "Left-click" }), " \u2014 select one"] }), _jsxs("li", { children: [_jsx("span", { className: "text-emerald-300", children: "Right-click ground" }), " \u2014 move here (A*)"] }), _jsxs("li", { children: [_jsx("span", { className: "text-emerald-300", children: "Right-click unit / rock" }), " \u2014 verb menu"] }), _jsxs("li", { children: [_jsx("span", { className: "text-emerald-300", children: "Middle-drag" }), " \u2014 orbit \u00B7 wheel \u2014 zoom"] })] })] }), _jsx("div", { className: "absolute bottom-4 right-4 max-w-[220px] rounded-lg border border-white/10 bg-neutral-900/70 px-3 py-2 text-right text-[11px] text-white/50", children: "Green ring = selected. The looping creep walks an authored path (tower-defense pathFollow)." })] }));
|
|
128
|
+
}
|
|
129
|
+
export const pointerDemoGame = {
|
|
130
|
+
game,
|
|
131
|
+
content: {
|
|
132
|
+
entityById: (catalogId) => entityCatalog[catalogId] ?? null,
|
|
133
|
+
objectById: (catalogId) => objectCatalog[catalogId] ?? null,
|
|
134
|
+
},
|
|
135
|
+
loop: { onInit, onNewPlayer, onTick },
|
|
136
|
+
GameUI: CommanderUI,
|
|
137
|
+
pointer: {
|
|
138
|
+
select: true,
|
|
139
|
+
selectFilter: (id) => id !== CREEP_ID,
|
|
140
|
+
orderCommand: "unit.order",
|
|
141
|
+
contextMenu: true,
|
|
142
|
+
},
|
|
143
|
+
camera: {
|
|
144
|
+
initialDistance: 34,
|
|
145
|
+
initialHeight: 28,
|
|
146
|
+
minDistance: 12,
|
|
147
|
+
maxDistance: 64,
|
|
148
|
+
targetHeight: 0,
|
|
149
|
+
maxPolarAngle: 1.25,
|
|
150
|
+
},
|
|
151
|
+
};
|