@jgengine/shell 0.11.0 → 0.13.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 (151) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/CombatPresentation.d.ts +5 -0
  3. package/dist/CombatPresentation.js +8 -0
  4. package/dist/GameHost.d.ts +27 -1
  5. package/dist/GameHost.js +83 -4
  6. package/dist/GamePlayer.d.ts +5 -0
  7. package/dist/GamePlayer.js +5 -0
  8. package/dist/GamePlayerShell.d.ts +5 -0
  9. package/dist/GamePlayerShell.js +63 -359
  10. package/dist/GameUiPreview.js +3 -0
  11. package/dist/Shell3dPresentation.d.ts +63 -0
  12. package/dist/Shell3dPresentation.js +302 -0
  13. package/dist/ShellChrome.d.ts +51 -0
  14. package/dist/ShellChrome.js +51 -0
  15. package/dist/ShellHudPresentation.d.ts +41 -0
  16. package/dist/ShellHudPresentation.js +22 -0
  17. package/dist/audio/audioEngine.d.ts +4 -0
  18. package/dist/audio/audioEngine.js +111 -26
  19. package/dist/audio/audioWire.d.ts +22 -4
  20. package/dist/audio/audioWire.js +46 -3
  21. package/dist/audio/loopParams.d.ts +20 -0
  22. package/dist/audio/loopParams.js +28 -0
  23. package/dist/camera/GameCameraRig.js +6 -1
  24. package/dist/camera/GameOrbitCamera.js +7 -1
  25. package/dist/camera/cameraCollision.d.ts +28 -0
  26. package/dist/camera/cameraCollision.js +34 -0
  27. package/dist/camera/cameraRigs.d.ts +3 -1
  28. package/dist/camera/cameraRigs.js +24 -10
  29. package/dist/camera/index.d.ts +1 -0
  30. package/dist/camera/index.js +1 -0
  31. package/dist/camera/rigMath.d.ts +27 -0
  32. package/dist/camera/rigMath.js +45 -0
  33. package/dist/defineGame.d.ts +15 -1
  34. package/dist/defineGame.js +26 -4
  35. package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
  36. package/dist/devtools/DevtoolsOverlay.js +28 -12
  37. package/dist/devtools/agentBridge.js +4 -0
  38. package/dist/devtools/perfDiagnose.js +1 -1
  39. package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
  40. package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
  41. package/dist/environment/EnvironmentScene.js +17 -3
  42. package/dist/environment/RoadJunctions.d.ts +12 -0
  43. package/dist/environment/RoadJunctions.js +69 -0
  44. package/dist/environment/RoadRibbons.d.ts +8 -2
  45. package/dist/environment/RoadRibbons.js +26 -8
  46. package/dist/environment/terrainGroundColor.d.ts +9 -0
  47. package/dist/environment/terrainGroundColor.js +22 -0
  48. package/dist/gameKit.d.ts +27 -0
  49. package/dist/gameKit.js +29 -0
  50. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  51. package/dist/postfx/PostProcessing.d.ts +9 -1
  52. package/dist/postfx/PostProcessing.js +11 -4
  53. package/dist/presentationResolve.d.ts +39 -0
  54. package/dist/presentationResolve.js +53 -0
  55. package/dist/registry.d.ts +1 -0
  56. package/dist/render/PartMotion.d.ts +18 -0
  57. package/dist/render/PartMotion.js +91 -0
  58. package/dist/render/SceneModels.d.ts +9 -2
  59. package/dist/render/SceneModels.js +31 -180
  60. package/dist/render/assetBase.d.ts +17 -0
  61. package/dist/render/assetBase.js +35 -0
  62. package/dist/render/measureBounds.d.ts +34 -0
  63. package/dist/render/measureBounds.js +127 -0
  64. package/dist/render/modelLoad.js +4 -1
  65. package/dist/render/resolveModel.d.ts +15 -0
  66. package/dist/render/resolveModel.js +43 -2
  67. package/dist/render/useModelAnimation.d.ts +10 -0
  68. package/dist/render/useModelAnimation.js +198 -0
  69. package/dist/scatter/InstancedScatter.js +5 -0
  70. package/dist/scatter/scatterProxies.js +24 -0
  71. package/dist/scene/AuthoredScene.d.ts +3 -1
  72. package/dist/scene/AuthoredScene.js +7 -2
  73. package/dist/scene/CityRenderer.d.ts +2 -0
  74. package/dist/scene/CityRenderer.js +1031 -0
  75. package/dist/scene/GrassFieldRenderer.js +53 -5
  76. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  77. package/dist/scene/PoleLineRenderer.js +287 -0
  78. package/dist/scene/SoilPatchRenderer.js +70 -7
  79. package/dist/scene/WaterRenderer.js +34 -10
  80. package/dist/scene/builtinSceneKindRenderers.js +4 -0
  81. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  82. package/dist/scene/cityWindowMaterial.js +81 -0
  83. package/dist/scene/sceneKindRenderers.d.ts +2 -0
  84. package/dist/settings/appliedSettings.d.ts +2 -1
  85. package/dist/settings/appliedSettings.js +4 -2
  86. package/dist/structures/GeneratedBuilding.d.ts +4 -0
  87. package/dist/structures/GeneratedBuilding.js +17 -7
  88. package/dist/structures/TransformGizmo.d.ts +11 -1
  89. package/dist/structures/TransformGizmo.js +9 -5
  90. package/dist/terrain/GrassField.d.ts +11 -3
  91. package/dist/terrain/GrassField.js +16 -6
  92. package/dist/terrain/grassGeometry.d.ts +33 -1
  93. package/dist/terrain/grassGeometry.js +83 -21
  94. package/dist/terrain/grassMaterial.d.ts +18 -0
  95. package/dist/terrain/grassMaterial.js +66 -13
  96. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  97. package/dist/terrain/heightfieldGeometry.js +109 -0
  98. package/dist/terrain/index.d.ts +1 -0
  99. package/dist/terrain/index.js +1 -0
  100. package/dist/terrain/soilPatchMaterial.d.ts +15 -2
  101. package/dist/terrain/soilPatchMaterial.js +46 -8
  102. package/dist/terrain/terrainDetailMaterial.js +15 -2
  103. package/dist/water/Ocean.d.ts +7 -1
  104. package/dist/water/Ocean.js +16 -3
  105. package/dist/water/OceanConfig.d.ts +9 -0
  106. package/dist/water/OceanConfig.js +6 -0
  107. package/dist/water/OceanMaterial.d.ts +12 -0
  108. package/dist/water/OceanMaterial.js +9 -0
  109. package/dist/water/OceanShader.d.ts +2 -2
  110. package/dist/water/OceanShader.js +78 -10
  111. package/dist/world/PlaceScene.d.ts +11 -0
  112. package/dist/world/PlaceScene.js +27 -0
  113. package/dist/world/WorldHud.d.ts +3 -1
  114. package/dist/world/WorldHud.js +2 -2
  115. package/dist/world/WorldScene.js +32 -4
  116. package/dist/worldSky.d.ts +7 -1
  117. package/dist/worldSky.js +11 -1
  118. package/package.json +4 -4
  119. package/skills/game-design/SKILL.md +28 -0
  120. package/skills/game-design/agents/openai.yaml +4 -0
  121. package/skills/game-design/references/application-playbook.md +170 -0
  122. package/skills/game-design/references/field-guide.md +288 -0
  123. package/skills/jgengine/SKILL.md +8 -4
  124. package/skills/jgengine/api.md +182 -81
  125. package/skills/jgengine/capabilities.md +104 -2
  126. package/skills/jgengine/recipes/minimal-game.md +88 -0
  127. package/skills/jgengine-ui/SKILL.md +17 -10
  128. package/skills/jgengine-ui/api.md +316 -51
  129. package/skills/jgengine-ui/capabilities.md +135 -2
  130. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  131. package/skills/jgengine-ui/reference.md +59 -9
  132. package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
  133. package/skills/jgengine-world/SKILL.md +13 -0
  134. package/skills/jgengine-world/api.md +453 -21
  135. package/skills/jgengine-world/capabilities.md +112 -2
  136. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  137. package/skills/jgengine-world/reference.md +43 -4
  138. package/skills/level-design/SKILL.md +54 -0
  139. package/skills/level-design/agents/openai.yaml +4 -0
  140. package/skills/level-design/references/application-playbook.md +192 -0
  141. package/skills/level-design/references/field-guide.md +337 -0
  142. package/skills/level-design/references/live-editor-loop.md +179 -0
  143. package/dist/cartridge/runtime.d.ts +0 -15
  144. package/dist/cartridge/runtime.js +0 -556
  145. package/dist/cartridge/spec.d.ts +0 -188
  146. package/dist/cartridge/spec.js +0 -19
  147. package/dist/cartridge/validate.d.ts +0 -2
  148. package/dist/cartridge/validate.js +0 -78
  149. package/dist/cartridge.d.ts +0 -145
  150. package/dist/cartridge.js +0 -246
  151. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -0,0 +1,302 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Canvas } from "@react-three/fiber";
3
+ import { useMemo, useRef, useState, } from "react";
4
+ import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
5
+ import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
6
+ import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
7
+ import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
8
+ import { BUILT_IN_SETTING_CATEGORIES, } from "@jgengine/core/settings/settingsModel";
9
+ import { playControlsActive } from "@jgengine/core/game/controlGate";
10
+ import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
11
+ import { resolveGameLook } from "@jgengine/core/render/lookPreset";
12
+ import { GameViewportProvider } from "@jgengine/react/gameViewport";
13
+ import { GameProvider } from "@jgengine/react/provider";
14
+ import { SettingsProvider } from "@jgengine/react/settings";
15
+ import { resolveWorldSky } from "./worldSky.js";
16
+ import { pointerAimFor, pointerContextMenu } from "./shellPointer.js";
17
+ import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
18
+ import { PostProcessing } from "./postfx/PostProcessing.js";
19
+ import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
20
+ import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
21
+ import { DevtoolsRendererProbe } from "./devtools/DevtoolsOverlay.js";
22
+ import { CAMERA_TRANSPARENT_USERDATA, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
23
+ import { CullingProvider } from "./visibility/CullingProvider.js";
24
+ import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
25
+ import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
26
+ import { EnvironmentScene } from "./environment/EnvironmentScene.js";
27
+ import { PointerProbe } from "./pointer/PointerProbe.js";
28
+ import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
29
+ import { createPointerService } from "./pointer/pointerService.js";
30
+ import { Reticle, WorldEntityBars, WorldNameplates } from "./world/WorldHud.js";
31
+ import { GridWorldScene } from "./world/GridWorldScene.js";
32
+ import { PlaceScene } from "./world/PlaceScene.js";
33
+ import { WorldItems } from "./world/WorldItems.js";
34
+ import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch/TouchControlsOverlay.js";
35
+ import { SettingsRuntime } from "./settings/SettingsRuntime.js";
36
+ import { SettingsChrome } from "./settings/SettingsChrome.js";
37
+ import { AudioSettingsBridge } from "./settings/appliedSettings.js";
38
+ import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
39
+ import { WorldView, RemotePlayers } from "./world/WorldScene.js";
40
+ import { FrameDriver } from "./drivers/FrameDriver.js";
41
+ import { createShellKeyHandlers, ShellDebugOverlays, ShellGameUiChrome } from "./ShellChrome.js";
42
+ import { CombatPresentation } from "./CombatPresentation.js";
43
+ import { resolvePresentationEffects, resolveWorldOverlayBars, } from "./presentationResolve.js";
44
+ const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
45
+ const DEFAULT_BACKGROUND_COLOR = "#14161b";
46
+ /** 3D play surface: canvas, world overlays, and shared chrome. @internal */
47
+ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }) {
48
+ const GameUI = playable.GameUI;
49
+ const WorldOverlay = playable.WorldOverlay;
50
+ const pointerService = useMemo(() => createPointerService(), []);
51
+ const selection = useMemo(() => createSelectionSet(), [playable]);
52
+ const controlledEntityId = ctx.player.possession.active(ctx.player.userId);
53
+ const cameraConfig = playable.camera?.followEntityId !== undefined
54
+ ? playable.camera
55
+ : { ...playable.camera, followEntityId: controlledEntityId };
56
+ const rigKind = resolveRigKind(cameraConfig);
57
+ const firstPerson = rigKind === "first";
58
+ const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
59
+ const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
60
+ const bars = resolveWorldOverlayBars(playable.worldHealthBars);
61
+ const nameplates = resolveWorldOverlayBars(playable.nameplates);
62
+ const effects = resolvePresentationEffects(playable.presentationEffects);
63
+ const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
64
+ const pointer = playable.pointer;
65
+ const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
66
+ const selectFilter = pointer?.selectFilter;
67
+ const worldSky = resolveWorldSky(playable.game.world);
68
+ const world = playable.game.world;
69
+ const biomeBands = world?.kind === "environment" ? world.terrain?.biomeBands : undefined;
70
+ const AutoEnvironment = playable.environment ??
71
+ (world?.kind === "environment"
72
+ ? () => _jsx(EnvironmentScene, { feature: world })
73
+ : world?.kind === "place"
74
+ ? () => _jsx(PlaceScene, { feature: world })
75
+ : world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
76
+ ? () => _jsx(GridWorldScene, { feature: world })
77
+ : undefined);
78
+ const resolvedLook = resolveGameLook({
79
+ look: playable.look,
80
+ lighting: playable.lighting,
81
+ backdrop: playable.backdrop,
82
+ postProcessing: playable.postProcessing,
83
+ hasWorldSky: worldSky !== undefined,
84
+ });
85
+ const cinematicLook = (playable.look ?? "cinematic") !== "flat";
86
+ const backdrop = resolvedLook.backdrop;
87
+ const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
88
+ const effectiveSky = backdropSky ?? worldSky;
89
+ const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
90
+ const lighting = resolvedLook.lighting;
91
+ const orthographic = playable.camera?.projection === "orthographic";
92
+ const pointerDownRef = useRef(null);
93
+ const marqueeStartRef = useRef(null);
94
+ const [marquee, setMarquee] = useState(null);
95
+ const [selectedIds, setSelectedIds] = useState(() => new Set());
96
+ const [contextMenu, setContextMenu] = useState(null);
97
+ const localXY = (event) => {
98
+ const rect = wrapperRef.current?.getBoundingClientRect();
99
+ return rect === undefined
100
+ ? { x: event.clientX, y: event.clientY }
101
+ : { x: event.clientX - rect.left, y: event.clientY - rect.top };
102
+ };
103
+ const commitSelection = (ids) => {
104
+ selection.replace(ids);
105
+ setSelectedIds(new Set(ids));
106
+ };
107
+ const finishMarquee = (rect) => {
108
+ const candidates = [];
109
+ for (const entity of ctx.scene.entity.list()) {
110
+ if (selectFilter !== undefined && !selectFilter(entity.id))
111
+ continue;
112
+ const screen = pointerService.screenOf(entity.position);
113
+ if (screen !== null)
114
+ candidates.push({ id: entity.id, x: screen.x, y: screen.y });
115
+ }
116
+ commitSelection(selectWithinRect(candidates, rect));
117
+ };
118
+ /** DOM UI sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
119
+ const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
120
+ const handlePointerDown = (event) => {
121
+ if (isWorldPointerTarget(event))
122
+ wrapperRef.current?.focus();
123
+ trackPointerAxis(event);
124
+ audioEngine.resume();
125
+ if (contextMenu !== null)
126
+ setContextMenu(null);
127
+ if (event.button === 0 && isWorldPointerTarget(event)) {
128
+ const point = localXY(event);
129
+ pointerDownRef.current = point;
130
+ if (pointer?.select === true)
131
+ marqueeStartRef.current = point;
132
+ }
133
+ };
134
+ const handlePointerMove = (event) => {
135
+ trackPointerAxis(event);
136
+ if (pointer?.select !== true)
137
+ return;
138
+ const start = marqueeStartRef.current;
139
+ if (start === null || (event.buttons & 1) === 0)
140
+ return;
141
+ const point = localXY(event);
142
+ setMarquee(screenRect(start.x, start.y, point.x, point.y));
143
+ };
144
+ const handlePointerUp = (event) => {
145
+ if (event.button !== 0 || pointerDownRef.current === null)
146
+ return;
147
+ const start = pointerDownRef.current;
148
+ pointerDownRef.current = null;
149
+ marqueeStartRef.current = null;
150
+ const end = localXY(event);
151
+ const rect = screenRect(start.x, start.y, end.x, end.y);
152
+ const wasMarquee = marquee !== null && isMarquee(rect, PRIMARY_CLICK_MOVE_THRESHOLD_PX);
153
+ setMarquee(null);
154
+ if (pointer?.select === true && wasMarquee) {
155
+ finishMarquee(rect);
156
+ return;
157
+ }
158
+ if (cameraDraggingRef.current)
159
+ return;
160
+ if (pointer?.grabWorldItems === true) {
161
+ const hit = pointerService.worldHit();
162
+ const itemInstanceId = hit?.entity ?? null;
163
+ const record = itemInstanceId === null ? null : ctx.scene.worldItem.get(itemInstanceId);
164
+ if (record !== null && itemInstanceId !== null) {
165
+ const itemEntity = ctx.scene.entity.get(itemInstanceId);
166
+ const localPlayer = ctx.scene.entity.get(ctx.player.userId);
167
+ const pickupRadius = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
168
+ const withinRadius = itemEntity !== null &&
169
+ localPlayer !== null &&
170
+ Math.hypot(localPlayer.position[0] - itemEntity.position[0], localPlayer.position[1] - itemEntity.position[1], localPlayer.position[2] - itemEntity.position[2]) <= pickupRadius;
171
+ if (withinRadius) {
172
+ ctx.scene.worldItem.pickup(itemInstanceId, ctx.player.userId);
173
+ return;
174
+ }
175
+ }
176
+ }
177
+ if (pointer?.select === true) {
178
+ const hit = pointerService.worldHit();
179
+ if (hit !== null && hit.entity !== null && (selectFilter === undefined || selectFilter(hit.entity))) {
180
+ commitSelection([hit.entity]);
181
+ }
182
+ else {
183
+ commitSelection([]);
184
+ }
185
+ return;
186
+ }
187
+ if (pointer?.moveCommand !== undefined) {
188
+ const hit = pointerService.worldHit();
189
+ if (hit !== null && ctx.game.commands.has(pointer.moveCommand)) {
190
+ ctx.game.commands.run(pointer.moveCommand, { point: hit.point, entity: hit.entity, object: hit.object });
191
+ }
192
+ return;
193
+ }
194
+ const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
195
+ if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
196
+ primaryClickRef.current = true;
197
+ };
198
+ const handleContextMenu = (event) => {
199
+ if (pointer === undefined)
200
+ return;
201
+ if (!isWorldPointerTarget(event))
202
+ return;
203
+ event.preventDefault();
204
+ const hit = pointerService.worldHit();
205
+ if (hit === null)
206
+ return;
207
+ if (pointer.contextMenu === true && (hit.entity !== null || hit.object !== null)) {
208
+ const menu = pointerContextMenu(ctx, playable, hit);
209
+ if (menu !== null) {
210
+ const point = localXY(event);
211
+ setContextMenu({ menu, x: point.x, y: point.y });
212
+ return;
213
+ }
214
+ }
215
+ if (pointer.select === true && pointer.orderCommand !== undefined && selection.size() > 0) {
216
+ if (ctx.game.commands.has(pointer.orderCommand)) {
217
+ ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
218
+ }
219
+ return;
220
+ }
221
+ if (pointer.secondaryCommand !== undefined && ctx.game.commands.has(pointer.secondaryCommand)) {
222
+ const aim = (pointer.aim === true ? pointerAimFor(ctx, pointerService) : undefined) ?? {
223
+ yaw: yawRef.current,
224
+ pitch: pitchRef.current,
225
+ };
226
+ ctx.game.commands.run(pointer.secondaryCommand, { point: hit.point, entity: hit.entity, object: hit.object, aim });
227
+ }
228
+ };
229
+ const handleVerbPick = (verb) => {
230
+ const state = contextMenu;
231
+ setContextMenu(null);
232
+ if (state !== null && ctx.game.commands.has(verb.command)) {
233
+ ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
234
+ }
235
+ };
236
+ const controlsActive = playControlsActive(ctx);
237
+ const settingsDisabled = playable.settings === false;
238
+ const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
239
+ const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
240
+ const settingsVariant = settingsConfig.variant ?? "panel";
241
+ const hideCategories = settingsDisabled ? BUILT_IN_SETTING_CATEGORIES : settingsConfig.hide ?? [];
242
+ const fovControlEnabled = !orthographic && playable.camera?.playerFov?.control !== false;
243
+ const settingsHostsFov = !settingsDisabled && !hideCategories.includes("gameplay") && fovControlEnabled;
244
+ const settingsActions = settingsDisabled
245
+ ? []
246
+ : (settingsConfig.actions ?? []).map((action) => ({
247
+ id: action.id,
248
+ label: action.label,
249
+ kind: action.kind ?? "default",
250
+ description: action.description,
251
+ run: () => action.run(ctx),
252
+ }));
253
+ const touchScale = compact ? 0.88 : 1;
254
+ const dockMounted = !poster &&
255
+ !orientationGate &&
256
+ coarsePointer &&
257
+ controlsActive &&
258
+ touchScheme !== null &&
259
+ (touchScheme.joystick !== null || touchScheme.buttons.length > 0);
260
+ const keys = createShellKeyHandlers({
261
+ f2HeldRef,
262
+ tracker,
263
+ devtoolsEnabled,
264
+ setDevtoolsOpen,
265
+ controlsActive: () => playControlsActive(ctx),
266
+ exitPointerLockOnDevtools: true,
267
+ });
268
+ return (_jsx(SettingsProvider, { store: settingsStore, children: _jsxs(PlayerFovProvider, { config: playable.camera, orthographic: orthographic, children: [_jsx(AudioSettingsBridge, { store: settingsStore, engine: audioEngine, buses: playable.audio?.buses }), _jsx(SettingsRuntime, { variant: settingsVariant, surface: settingsSurface, actions: settingsActions, input: playable.game.input ?? {}, buses: playable.audio?.buses, extra: settingsConfig.extra ?? [], categories: settingsConfig.categories ?? [], hide: hideCategories, fovEnabled: fovControlEnabled, hideBindings: settingsConfig.hideBindings ?? [], touchStyle: coarsePointer &&
269
+ touchScheme !== null &&
270
+ (touchScheme.joystick !== null || touchScheme.buttons.length > 0), overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsx("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
271
+ "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
272
+ }, onKeyDown: keys.onKeyDown, onKeyUp: keys.onKeyUp, onBlur: keys.onBlur, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, onContextMenu: handleContextMenu, onWheel: (event) => {
273
+ if (!event.shiftKey)
274
+ return;
275
+ event.preventDefault();
276
+ if (event.deltaY < 0 && ctx.game.commands.has("ui.hotbarScrollNext")) {
277
+ ctx.game.commands.run("ui.hotbarScrollNext", {});
278
+ }
279
+ else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
280
+ ctx.game.commands.run("ui.hotbarScrollPrev", {});
281
+ }
282
+ }, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
283
+ ? {
284
+ zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
285
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
286
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
287
+ }
288
+ : {
289
+ fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
290
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
291
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
292
+ }, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
293
+ cameraDraggingRef.current = dragging;
294
+ } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
295
+ !orientationGate &&
296
+ coarsePointer &&
297
+ controlsActive &&
298
+ touchScheme !== null &&
299
+ (touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
300
+ primaryClickRef.current = true;
301
+ } })) : null, _jsx(ShellGameUiChrome, { ctx: ctx, playable: playable, GameUI: GameUI, uiScale: graphics.uiScale, orientationGate: orientationGate, onRuntimeError: reportRuntimeError }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, style: touchStyle, scale: touchScale })) : null, orientationGateEl, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, _jsx(ShellDebugOverlays, { ctx: ctx, playable: playable, multiplayer: multiplayer, diagnostics: diagnostics, devtoolsEnabled: devtoolsEnabled, devtoolsOpen: devtoolsOpen, hideDiagnostics: poster }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
302
+ }
@@ -0,0 +1,51 @@
1
+ import type { ComponentType } from "react";
2
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
3
+ import { type RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
4
+ import type { ShellMultiplayer } from "./multiplayer.js";
5
+ import type { PlayableGame } from "./registry.js";
6
+ /** Shared GameUI mount: error boundary → GameProvider → phase stamp → HudViewport. @internal */
7
+ export declare function ShellGameUiChrome({ ctx, playable, GameUI, uiScale, orientationGate, onRuntimeError, }: {
8
+ ctx: GameContext;
9
+ playable: PlayableGame;
10
+ GameUI: ComponentType;
11
+ uiScale: number;
12
+ orientationGate: boolean;
13
+ onRuntimeError: (error: unknown, phase: string, componentStack?: string) => void;
14
+ }): import("react").JSX.Element;
15
+ /** Devtools + diagnostic overlays shared by HUD and 3D presentation paths. @internal */
16
+ export declare function ShellDebugOverlays({ ctx, playable, multiplayer, diagnostics, devtoolsEnabled, devtoolsOpen, hideDiagnostics, }: {
17
+ ctx: GameContext;
18
+ playable: PlayableGame;
19
+ multiplayer: ShellMultiplayer | null;
20
+ diagnostics: RuntimeDiagnostic[];
21
+ devtoolsEnabled: boolean;
22
+ devtoolsOpen: boolean;
23
+ hideDiagnostics?: boolean;
24
+ }): import("react").JSX.Element;
25
+ /** Key handler bundle shared by HUD and 3D presentation paths. @internal */
26
+ export type ShellKeyHandlers = {
27
+ onKeyDown: (event: {
28
+ code: string;
29
+ preventDefault: () => void;
30
+ }) => void;
31
+ onKeyUp: (event: {
32
+ code: string;
33
+ }) => void;
34
+ onBlur: () => void;
35
+ };
36
+ /** F2 chord + gameplay action tracker key handlers shared by both presentation paths. @internal */
37
+ export declare function createShellKeyHandlers({ f2HeldRef, tracker, devtoolsEnabled, setDevtoolsOpen, controlsActive, exitPointerLockOnDevtools, }: {
38
+ f2HeldRef: {
39
+ current: boolean;
40
+ };
41
+ tracker: {
42
+ handleDown: (code: string) => void;
43
+ handleUp: (code: string) => void;
44
+ reset: () => void;
45
+ };
46
+ devtoolsEnabled: boolean;
47
+ setDevtoolsOpen: (update: boolean | ((current: boolean) => boolean)) => void;
48
+ controlsActive: () => boolean;
49
+ /** 3D path exits pointer lock when opening devtools. */
50
+ exitPointerLockOnDevtools?: boolean;
51
+ }): ShellKeyHandlers;
@@ -0,0 +1,51 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HudViewportProvider } from "@jgengine/react/hudViewport";
3
+ import { GameProvider } from "@jgengine/react/provider";
4
+ import { DevtoolsOverlay } from "./devtools/DevtoolsOverlay.js";
5
+ import { DiagnosticOverlay, GameUiErrorBoundary, } from "./diagnostics/RuntimeDiagnostics.js";
6
+ import { GamePhaseStamp } from "./GamePhaseStamp.js";
7
+ /** Shared GameUI mount: error boundary → GameProvider → phase stamp → HudViewport. @internal */
8
+ export function ShellGameUiChrome({ ctx, playable, GameUI, uiScale, orientationGate, onRuntimeError, }) {
9
+ return (_jsx(GameUiErrorBoundary, { onRuntimeError: onRuntimeError, children: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: uiScale, children: orientationGate ? null : _jsx(GameUI, {}) })] }) }));
10
+ }
11
+ /** Devtools + diagnostic overlays shared by HUD and 3D presentation paths. @internal */
12
+ export function ShellDebugOverlays({ ctx, playable, multiplayer, diagnostics, devtoolsEnabled, devtoolsOpen, hideDiagnostics = false, }) {
13
+ return (_jsxs(_Fragment, { children: [devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, hideDiagnostics ? null : (_jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name }))] }));
14
+ }
15
+ /** F2 chord + gameplay action tracker key handlers shared by both presentation paths. @internal */
16
+ export function createShellKeyHandlers({ f2HeldRef, tracker, devtoolsEnabled, setDevtoolsOpen, controlsActive, exitPointerLockOnDevtools = false, }) {
17
+ return {
18
+ onKeyDown: (event) => {
19
+ if (event.code === "F2") {
20
+ event.preventDefault();
21
+ f2HeldRef.current = true;
22
+ return;
23
+ }
24
+ if (f2HeldRef.current) {
25
+ if (event.code === "KeyD" && devtoolsEnabled) {
26
+ event.preventDefault();
27
+ if (exitPointerLockOnDevtools)
28
+ document.exitPointerLock?.();
29
+ setDevtoolsOpen((current) => !current);
30
+ }
31
+ return;
32
+ }
33
+ if (event.code === "Tab" || event.code === "Space")
34
+ event.preventDefault();
35
+ if (controlsActive())
36
+ tracker.handleDown(event.code);
37
+ },
38
+ onKeyUp: (event) => {
39
+ if (event.code === "F2") {
40
+ f2HeldRef.current = false;
41
+ return;
42
+ }
43
+ if (controlsActive())
44
+ tracker.handleUp(event.code);
45
+ },
46
+ onBlur: () => {
47
+ f2HeldRef.current = false;
48
+ tracker.reset();
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,41 @@
1
+ import type { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
2
+ import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
+ import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
4
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
5
+ import type { TouchScheme } from "@jgengine/core/input/touchScheme";
6
+ import type { RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
7
+ import type { ShellMultiplayer } from "./multiplayer.js";
8
+ import type { PlayableGame } from "./registry.js";
9
+ /** HUD-only play surface for non-3D shells. @internal */
10
+ export declare function ShellHudPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, touchScheme, touchSink, orientationGate, orientationGateEl, coarsePointer, uiScale, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, onPointerResumeAudio, }: {
11
+ playable: PlayableGame;
12
+ ctx: GameContext;
13
+ multiplayer: ShellMultiplayer | null;
14
+ tracker: ActionStateTracker<string>;
15
+ pointerAxisRef: MutableRefObject<PointerAxisState | null>;
16
+ gateRef: MutableRefObject<boolean>;
17
+ wrapperRef: RefObject<HTMLDivElement | null>;
18
+ f2HeldRef: MutableRefObject<boolean>;
19
+ yawRef: MutableRefObject<number>;
20
+ pitchRef: MutableRefObject<number>;
21
+ touchScheme: TouchScheme | null;
22
+ touchSink: {
23
+ onCodeDown: (code: string) => void;
24
+ onCodeUp: (code: string) => void;
25
+ };
26
+ orientationGate: boolean;
27
+ orientationGateEl: React.ReactNode;
28
+ coarsePointer: boolean;
29
+ uiScale: number;
30
+ diagnostics: RuntimeDiagnostic[];
31
+ devtoolsEnabled: boolean;
32
+ devtoolsOpen: boolean;
33
+ setDevtoolsOpen: Dispatch<SetStateAction<boolean>>;
34
+ reportRuntimeError: (error: unknown, phase: string, componentStack?: string) => void;
35
+ trackPointerAxis: (event: {
36
+ clientX: number;
37
+ clientY: number;
38
+ }) => void;
39
+ deactivatePointerAxis: () => void;
40
+ onPointerResumeAudio: () => void;
41
+ }): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { playControlsActive } from "@jgengine/core/game/controlGate";
3
+ import { GameViewportProvider } from "@jgengine/react/gameViewport";
4
+ import { HudOnlyDriver } from "./drivers/HudOnlyDriver.js";
5
+ import { createShellKeyHandlers, ShellDebugOverlays, ShellGameUiChrome } from "./ShellChrome.js";
6
+ import { TouchPlaySurface } from "./touch/TouchControlsOverlay.js";
7
+ /** HUD-only play surface for non-3D shells. @internal */
8
+ export function ShellHudPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, touchScheme, touchSink, orientationGate, orientationGateEl, coarsePointer, uiScale, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, onPointerResumeAudio, }) {
9
+ const GameUI = playable.GameUI;
10
+ const keys = createShellKeyHandlers({
11
+ f2HeldRef,
12
+ tracker,
13
+ devtoolsEnabled,
14
+ setDevtoolsOpen,
15
+ controlsActive: () => playControlsActive(ctx),
16
+ });
17
+ return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: keys.onKeyDown, onKeyUp: keys.onKeyUp, onBlur: keys.onBlur, onPointerDown: onPointerResumeAudio, onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError }), !orientationGate &&
18
+ coarsePointer &&
19
+ touchScheme !== null &&
20
+ touchScheme.gestures !== null &&
21
+ playControlsActive(ctx) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: 0, onPrimaryTap: () => undefined })) : null, _jsx(ShellGameUiChrome, { ctx: ctx, playable: playable, GameUI: GameUI, uiScale: uiScale, orientationGate: orientationGate, onRuntimeError: reportRuntimeError }), orientationGateEl, _jsx(ShellDebugOverlays, { ctx: ctx, playable: playable, multiplayer: multiplayer, diagnostics: diagnostics, devtoolsEnabled: devtoolsEnabled, devtoolsOpen: devtoolsOpen })] }) }));
22
+ }
@@ -16,6 +16,10 @@ export interface AudioSceneConfig {
16
16
  }
17
17
  export interface AudioEmitterHandle {
18
18
  setPosition(position: Vec3): void;
19
+ /** Live pitch of a retained loop: `rate` multiplies the authored playback rate (1 = authored), clamped to 0.25–4 and ramped ~20 ms to avoid zipper noise (#1051). */
20
+ setRate(rate: number): void;
21
+ /** Live volume of a retained loop: `gain` scales the source (0–1), clamped and ramped ~20 ms (#1051). */
22
+ setGain(gain: number): void;
19
23
  stop(): void;
20
24
  }
21
25
  export interface AudioEngine {