@jgengine/shell 0.5.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +191 -2
  2. package/README.md +27 -0
  3. package/dist/GamePlayerShell.js +224 -43
  4. package/dist/audio/AudioComponents.d.ts +12 -0
  5. package/dist/audio/AudioComponents.js +53 -0
  6. package/dist/audio/audioEngine.d.ts +23 -0
  7. package/dist/audio/audioEngine.js +110 -0
  8. package/dist/camera/GameCameraRig.d.ts +11 -0
  9. package/dist/camera/GameCameraRig.js +42 -0
  10. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  11. package/dist/camera/GameOrbitCamera.js +4 -2
  12. package/dist/camera/cameraRigs.d.ts +22 -0
  13. package/dist/camera/cameraRigs.js +485 -0
  14. package/dist/camera/index.d.ts +5 -0
  15. package/dist/camera/index.js +5 -0
  16. package/dist/camera/rigMath.d.ts +130 -0
  17. package/dist/camera/rigMath.js +338 -0
  18. package/dist/camera/shakeChannel.d.ts +29 -0
  19. package/dist/camera/shakeChannel.js +38 -0
  20. package/dist/demo/builderDemo.d.ts +2 -0
  21. package/dist/demo/builderDemo.js +189 -0
  22. package/dist/demo/demoGame.js +10 -2
  23. package/dist/demo/environmentShowcase.d.ts +2 -0
  24. package/dist/demo/environmentShowcase.js +15 -0
  25. package/dist/demo/mapDemo.d.ts +2 -0
  26. package/dist/demo/mapDemo.js +206 -0
  27. package/dist/demo/pointerDemo.d.ts +2 -0
  28. package/dist/demo/pointerDemo.js +151 -0
  29. package/dist/demo/sensorShowcase.d.ts +2 -0
  30. package/dist/demo/sensorShowcase.js +131 -0
  31. package/dist/demo/survivalDemo.d.ts +2 -0
  32. package/dist/demo/survivalDemo.js +291 -0
  33. package/dist/environment/EnvironmentScene.d.ts +5 -0
  34. package/dist/environment/EnvironmentScene.js +75 -0
  35. package/dist/environment/index.d.ts +1 -0
  36. package/dist/environment/index.js +1 -0
  37. package/dist/environment.d.ts +1 -0
  38. package/dist/environment.js +1 -0
  39. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  40. package/dist/map/MapMarkerBeacons.js +16 -0
  41. package/dist/map/index.d.ts +2 -0
  42. package/dist/map/index.js +2 -0
  43. package/dist/map/terrainMap.d.ts +23 -0
  44. package/dist/map/terrainMap.js +79 -0
  45. package/dist/pointer/PointerOverlays.d.ts +12 -0
  46. package/dist/pointer/PointerOverlays.js +17 -0
  47. package/dist/pointer/PointerProbe.d.ts +4 -0
  48. package/dist/pointer/PointerProbe.js +27 -0
  49. package/dist/pointer/pointerService.d.ts +24 -0
  50. package/dist/pointer/pointerService.js +68 -0
  51. package/dist/registry.d.ts +4 -2
  52. package/dist/replay/useSessionRecorder.d.ts +14 -0
  53. package/dist/replay/useSessionRecorder.js +22 -0
  54. package/dist/structures/GeneratedBuilding.d.ts +44 -0
  55. package/dist/structures/GeneratedBuilding.js +86 -0
  56. package/dist/structures/PlacementGhost.d.ts +8 -0
  57. package/dist/structures/PlacementGhost.js +11 -0
  58. package/dist/structures/index.d.ts +2 -0
  59. package/dist/structures/index.js +2 -0
  60. package/dist/structures.d.ts +1 -0
  61. package/dist/structures.js +1 -0
  62. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  63. package/dist/terrain/CarvedTerrain.js +19 -0
  64. package/dist/terrain/EditableGround.d.ts +11 -0
  65. package/dist/terrain/EditableGround.js +38 -0
  66. package/dist/terrain/GrassField.d.ts +21 -0
  67. package/dist/terrain/GrassField.js +31 -0
  68. package/dist/terrain/ProceduralGround.d.ts +9 -0
  69. package/dist/terrain/ProceduralGround.js +17 -0
  70. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  71. package/dist/terrain/TerraformBrushCursor.js +13 -0
  72. package/dist/terrain/grassGeometry.d.ts +27 -0
  73. package/dist/terrain/grassGeometry.js +77 -0
  74. package/dist/terrain/grassMaterial.d.ts +33 -0
  75. package/dist/terrain/grassMaterial.js +108 -0
  76. package/dist/terrain/index.d.ts +10 -0
  77. package/dist/terrain/index.js +10 -0
  78. package/dist/terrain/random.d.ts +4 -0
  79. package/dist/terrain/random.js +27 -0
  80. package/dist/terrain/terrainMath.d.ts +47 -0
  81. package/dist/terrain/terrainMath.js +172 -0
  82. package/dist/terrain.d.ts +1 -0
  83. package/dist/terrain.js +1 -0
  84. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  85. package/dist/vision/FrustumSensorHud.js +67 -0
  86. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  87. package/dist/vision/HiddenStateProbeHud.js +11 -0
  88. package/dist/vision/RevealVision.d.ts +28 -0
  89. package/dist/vision/RevealVision.js +53 -0
  90. package/dist/water/Ocean.d.ts +6 -0
  91. package/dist/water/Ocean.js +30 -0
  92. package/dist/water/OceanConfig.d.ts +90 -0
  93. package/dist/water/OceanConfig.js +131 -0
  94. package/dist/water/OceanMaterial.d.ts +54 -0
  95. package/dist/water/OceanMaterial.js +51 -0
  96. package/dist/water/OceanShader.d.ts +2 -0
  97. package/dist/water/OceanShader.js +78 -0
  98. package/dist/water/index.d.ts +3 -0
  99. package/dist/water/index.js +3 -0
  100. package/dist/water.d.ts +1 -0
  101. package/dist/water.js +1 -0
  102. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  103. package/dist/weather/FireSpreadLayer.js +68 -0
  104. package/dist/weather/LightningStrike.d.ts +17 -0
  105. package/dist/weather/LightningStrike.js +130 -0
  106. package/dist/weather/RainField.d.ts +20 -0
  107. package/dist/weather/RainField.js +107 -0
  108. package/dist/weather/SnowField.d.ts +19 -0
  109. package/dist/weather/SnowField.js +108 -0
  110. package/dist/weather/WeatherLayer.d.ts +17 -0
  111. package/dist/weather/WeatherLayer.js +16 -0
  112. package/dist/weather/index.d.ts +6 -0
  113. package/dist/weather/index.js +6 -0
  114. package/dist/weather/weatherGeometry.d.ts +2 -0
  115. package/dist/weather/weatherGeometry.js +14 -0
  116. package/dist/weather/weatherMath.d.ts +7 -0
  117. package/dist/weather/weatherMath.js +30 -0
  118. package/dist/weather/weatherUniforms.d.ts +18 -0
  119. package/dist/weather/weatherUniforms.js +34 -0
  120. package/dist/world/InstancedBodies.d.ts +17 -0
  121. package/dist/world/InstancedBodies.js +84 -0
  122. package/dist/world/InstancedJoints.d.ts +14 -0
  123. package/dist/world/InstancedJoints.js +32 -0
  124. package/dist/world/WorldHud.d.ts +2 -0
  125. package/dist/world/WorldHud.js +70 -12
  126. package/dist/world/WorldItems.d.ts +5 -0
  127. package/dist/world/WorldItems.js +31 -0
  128. package/dist/world/floatTextStyle.d.ts +14 -0
  129. package/dist/world/floatTextStyle.js +39 -0
  130. package/package.json +12 -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
+ };
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineGame } from "@jgengine/core/game/defineGame";
3
3
  import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
4
4
  import { CurrencyPill, HealthBar, SlotGrid } from "@jgengine/react/components";
5
- import { useFeed, useGameStore, usePlayer, useTarget } from "@jgengine/react/hooks";
5
+ import { useFeed, useGameClock, useGameStore, usePlayer, useTarget } from "@jgengine/react/hooks";
6
6
  const HERO = "hero";
7
7
  const MOB = "gloomSlime";
8
8
  const LOOT_TABLE = "slime-drops";
@@ -39,6 +39,7 @@ const game = defineGame({
39
39
  assets: createAssetCatalog(),
40
40
  multiplayer: null,
41
41
  inventories: { [HOTBAR]: { slots: 4, hud: "hotbar" } },
42
+ time: { start: 8 * 3600, speeds: [1, 2, 3, 4] },
42
43
  input: {
43
44
  moveForward: ["KeyW"],
44
45
  moveBack: ["KeyS"],
@@ -185,9 +186,16 @@ function KillFeedPanel() {
185
186
  function HotbarPanel() {
186
187
  return (_jsx(SlotGrid, { inventoryId: HOTBAR, className: "flex gap-1.5", renderSlot: (slot, index) => (_jsxs("div", { className: "w-16 rounded border border-white/20 bg-black/60 px-1.5 py-1 text-center text-[11px]", children: [_jsx("span", { className: "text-white/50", children: index + 1 }), _jsx("p", { className: "truncate text-white", children: slot === null ? "—" : slot.itemId })] })) }));
187
188
  }
189
+ function ClockPanel() {
190
+ const { paused, playSpeed, speeds, calendar, controls } = useGameClock();
191
+ const pad = (value) => value.toString().padStart(2, "0");
192
+ return (_jsxs("div", { className: "pointer-events-auto flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs", children: [_jsxs("span", { className: "tabular-nums text-white", children: ["Day ", calendar.day + 1, " \u00B7 ", pad(calendar.hour), ":", pad(calendar.minute)] }), _jsx("button", { onClick: () => controls.toggle(), className: "rounded border border-white/25 px-1.5 py-0.5 text-white/80 hover:bg-white/10", children: paused ? "▶" : "⏸" }), _jsx("div", { className: "flex gap-0.5", children: speeds.map((speed) => (_jsxs("button", { onClick: () => controls.setSpeed(speed), className: `rounded border px-1.5 py-0.5 ${!paused && speed === playSpeed
193
+ ? "border-amber-300 bg-amber-300/20 text-amber-200"
194
+ : "border-white/25 text-white/70 hover:bg-white/10"}`, children: [speed, "\u00D7"] }, speed))) })] }));
195
+ }
188
196
  function DemoGameUI() {
189
197
  const player = usePlayer();
190
- return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-mono text-white", children: [_jsx("div", { className: "absolute left-1/2 top-4 w-56 -translate-x-1/2", children: _jsx(TargetPanel, { userId: player.userId }) }), _jsxs("div", { className: "absolute right-4 top-4 flex flex-col items-end gap-2", children: [_jsx(CurrencyPill, { currencyId: GOLD, className: "rounded bg-black/60 px-2 py-1 text-sm text-amber-300" }), _jsx(KillFeedPanel, {})] }), _jsx("div", { className: "absolute bottom-4 left-4 w-64", children: _jsx(VitalsPanel, { userId: player.userId }) }), _jsx("div", { className: "absolute bottom-4 left-1/2 -translate-x-1/2", children: _jsx(HotbarPanel, {}) }), _jsx("div", { className: "absolute bottom-4 right-4 max-w-[220px] text-right text-[11px] leading-4 text-white/40", children: "WASD move \u00B7 Space jump \u00B7 Shift sprint \u00B7 Q/E turn \u00B7 Tab target \u00B7 Esc clear \u00B7 1 sword \u00B7 2 zap" })] }));
198
+ return (_jsxs("div", { className: "pointer-events-none absolute inset-0 font-mono text-white", children: [_jsx("div", { className: "absolute left-4 top-4", children: _jsx(ClockPanel, {}) }), _jsx("div", { className: "absolute left-1/2 top-4 w-56 -translate-x-1/2", children: _jsx(TargetPanel, { userId: player.userId }) }), _jsxs("div", { className: "absolute right-4 top-4 flex flex-col items-end gap-2", children: [_jsx(CurrencyPill, { currencyId: GOLD, className: "rounded bg-black/60 px-2 py-1 text-sm text-amber-300" }), _jsx(KillFeedPanel, {})] }), _jsx("div", { className: "absolute bottom-4 left-4 w-64", children: _jsx(VitalsPanel, { userId: player.userId }) }), _jsx("div", { className: "absolute bottom-4 left-1/2 -translate-x-1/2", children: _jsx(HotbarPanel, {}) }), _jsx("div", { className: "absolute bottom-4 right-4 max-w-[220px] text-right text-[11px] leading-4 text-white/40", children: "WASD move \u00B7 Space jump \u00B7 Shift sprint \u00B7 Q/E turn \u00B7 Tab target \u00B7 Esc clear \u00B7 1 sword \u00B7 2 zap" })] }));
191
199
  }
192
200
  export const demoGame = {
193
201
  game,
@@ -0,0 +1,2 @@
1
+ import type { PlayableGame } from "../registry.js";
2
+ export declare const environmentShowcaseGame: PlayableGame;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { building, environment, grass, ocean, rain, terrain, } from "@jgengine/core/world/features";
3
+ import { EnvironmentScene } from "../environment/EnvironmentScene.js";
4
+ import { demoGame } from "./demoGame.js";
5
+ const showcase = environment({
6
+ terrain: terrain({ bounds: { w: 220, d: 220 }, height: 4, frequency: 0.035, seed: "showcase", waterLevel: -1 }),
7
+ vegetation: grass({ area: { w: 120, d: 120 }, density: 5, colors: ["#31531f", "#8fbf4a"], seed: "showcase" }),
8
+ water: ocean({ bounds: { w: 520, d: 520 }, level: -1.4, waveHeight: 0.9, waveSpeed: 0.5, color: "#1f6f92" }),
9
+ weather: rain({ area: { w: 160, d: 160, h: 70 }, density: 0.5, wind: [1.4, 0.4] }),
10
+ structures: building({ count: 6, footprint: { w: 12, d: 9 }, stories: [3, 7], storyHeight: 2.8, spacing: 6, seed: "showcase" }),
11
+ });
12
+ export const environmentShowcaseGame = {
13
+ ...demoGame,
14
+ environment: () => _jsx(EnvironmentScene, { feature: showcase }),
15
+ };
@@ -0,0 +1,2 @@
1
+ import type { PlayableGame } from "../registry.js";
2
+ export declare const mapDemoGame: PlayableGame;
@@ -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,2 @@
1
+ import type { PlayableGame } from "../registry.js";
2
+ export declare const pointerDemoGame: PlayableGame;
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import type { PlayableGame } from "../registry.js";
2
+ export declare const sensorShowcaseGame: PlayableGame;