@jgengine/shell 0.8.0 → 0.9.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 (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +11 -0
  3. package/dist/GamePlayerShell.d.ts +5 -2
  4. package/dist/GamePlayerShell.js +358 -98
  5. package/dist/GameUiPreview.js +2 -1
  6. package/dist/audio/AudioComponents.js +25 -8
  7. package/dist/audio/audioEngine.d.ts +1 -0
  8. package/dist/audio/audioEngine.js +4 -0
  9. package/dist/behaviour.d.ts +5 -24
  10. package/dist/behaviour.js +14 -50
  11. package/dist/behaviourAttach.d.ts +9 -0
  12. package/dist/behaviourAttach.js +32 -0
  13. package/dist/behaviourDriver.d.ts +7 -0
  14. package/dist/behaviourDriver.js +21 -0
  15. package/dist/camera/GameCameraRig.js +1 -1
  16. package/dist/camera/GameFirstPersonCamera.js +14 -0
  17. package/dist/camera/GameOrbitCamera.js +10 -1
  18. package/dist/camera/PlayerFov.d.ts +18 -0
  19. package/dist/camera/PlayerFov.js +48 -0
  20. package/dist/camera/cameraBlendMath.d.ts +13 -0
  21. package/dist/camera/cameraBlendMath.js +36 -0
  22. package/dist/camera/cameraRigs.d.ts +3 -0
  23. package/dist/camera/cameraRigs.js +36 -33
  24. package/dist/camera/fovPreference.d.ts +25 -0
  25. package/dist/camera/fovPreference.js +73 -0
  26. package/dist/camera/index.d.ts +3 -0
  27. package/dist/camera/index.js +3 -0
  28. package/dist/camera/rigMath.d.ts +10 -0
  29. package/dist/camera/rigMath.js +36 -1
  30. package/dist/camera/shakeChannel.d.ts +3 -17
  31. package/dist/camera/shakeChannel.js +2 -21
  32. package/dist/camera/shakeChannelMath.d.ts +8 -0
  33. package/dist/camera/shakeChannelMath.js +21 -0
  34. package/dist/cartridge.d.ts +145 -0
  35. package/dist/cartridge.js +245 -0
  36. package/dist/defineGame.js +2 -1
  37. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  38. package/dist/devtools/CollisionDebugWorld.js +180 -0
  39. package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
  40. package/dist/devtools/DevtoolsOverlay.js +383 -41
  41. package/dist/devtools/collisionDebug.d.ts +57 -0
  42. package/dist/devtools/collisionDebug.js +127 -0
  43. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  44. package/dist/devtools/collisionDebugMath.js +128 -0
  45. package/dist/environment/Daylight.d.ts +13 -7
  46. package/dist/environment/Daylight.js +12 -11
  47. package/dist/environment/EnvironmentScene.js +82 -35
  48. package/dist/environment/groundPadMath.d.ts +2 -2
  49. package/dist/environment/groundPadMath.js +1 -1
  50. package/dist/environment/index.d.ts +2 -1
  51. package/dist/environment/index.js +1 -0
  52. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  53. package/dist/environment/skyLightingPolicy.js +6 -0
  54. package/dist/input/mouseLook.d.ts +27 -0
  55. package/dist/input/mouseLook.js +35 -0
  56. package/dist/materialOverride.d.ts +4 -6
  57. package/dist/materialOverride.js +12 -16
  58. package/dist/pointer/PointerProbe.js +6 -2
  59. package/dist/pointer/pointerService.d.ts +3 -0
  60. package/dist/pointer/pointerService.js +6 -0
  61. package/dist/render/modelRender.d.ts +10 -1
  62. package/dist/render/modelRender.js +32 -5
  63. package/dist/render/resolveModel.d.ts +14 -0
  64. package/dist/render/resolveModel.js +24 -0
  65. package/dist/settings/QuickControls.d.ts +4 -0
  66. package/dist/settings/QuickControls.js +42 -0
  67. package/dist/settings/SettingsChrome.d.ts +1 -0
  68. package/dist/settings/SettingsChrome.js +10 -0
  69. package/dist/settings/SettingsMenu.d.ts +6 -0
  70. package/dist/settings/SettingsMenu.js +148 -0
  71. package/dist/settings/SettingsRuntime.d.ts +11 -0
  72. package/dist/settings/SettingsRuntime.js +19 -0
  73. package/dist/settings/appliedSettings.d.ts +14 -0
  74. package/dist/settings/appliedSettings.js +27 -0
  75. package/dist/settings/settingsController.d.ts +20 -0
  76. package/dist/settings/settingsController.js +165 -0
  77. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  78. package/dist/structures/GeneratedBuilding.js +96 -8
  79. package/dist/structures/index.d.ts +1 -1
  80. package/dist/structures/index.js +1 -1
  81. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  82. package/dist/terrain/CarvedTerrain.js +3 -3
  83. package/dist/terrain/GrassField.d.ts +3 -1
  84. package/dist/terrain/GrassField.js +4 -2
  85. package/dist/terrain/grassBudget.d.ts +3 -0
  86. package/dist/terrain/grassBudget.js +6 -0
  87. package/dist/terrain/grassGeometry.js +1 -1
  88. package/dist/terrain/terrainMath.d.ts +8 -0
  89. package/dist/terrain/terrainMath.js +9 -0
  90. package/dist/touch/OrientationHint.d.ts +3 -0
  91. package/dist/touch/OrientationHint.js +13 -0
  92. package/dist/touch/TouchControlsOverlay.d.ts +17 -1
  93. package/dist/touch/TouchControlsOverlay.js +115 -9
  94. package/dist/visibility/CullingProvider.d.ts +21 -0
  95. package/dist/visibility/CullingProvider.js +134 -0
  96. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  97. package/dist/vision/FrustumSensorHud.js +25 -27
  98. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  99. package/dist/vision/frustumSampleEqual.js +10 -0
  100. package/dist/water/Ocean.js +1 -1
  101. package/dist/water/OceanConfig.d.ts +13 -0
  102. package/dist/water/OceanConfig.js +25 -17
  103. package/dist/water/index.d.ts +1 -1
  104. package/dist/water/index.js +1 -1
  105. package/dist/weather/FireSpreadLayer.js +7 -2
  106. package/dist/weather/RainField.d.ts +3 -1
  107. package/dist/weather/RainField.js +4 -4
  108. package/dist/weather/SnowField.d.ts +3 -1
  109. package/dist/weather/SnowField.js +4 -4
  110. package/dist/weather/fireSpreadPose.d.ts +2 -0
  111. package/dist/weather/fireSpreadPose.js +4 -0
  112. package/dist/weather/weatherMath.d.ts +5 -1
  113. package/dist/weather/weatherMath.js +7 -2
  114. package/dist/world/DataObjects.d.ts +1 -1
  115. package/dist/world/DataObjects.js +3 -1
  116. package/dist/world/SpriteBatch.d.ts +44 -0
  117. package/dist/world/SpriteBatch.js +112 -0
  118. package/dist/world/WorldHud.d.ts +3 -0
  119. package/dist/world/WorldHud.js +89 -42
  120. package/dist/world/entityPose.d.ts +14 -0
  121. package/dist/world/entityPose.js +10 -0
  122. package/dist/world/telegraphPulse.d.ts +1 -0
  123. package/dist/world/telegraphPulse.js +4 -0
  124. package/dist/world/worldBarSamples.d.ts +30 -0
  125. package/dist/world/worldBarSamples.js +51 -0
  126. package/llms.txt +1404 -1143
  127. package/package.json +4 -4
@@ -0,0 +1,245 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import { createCartridge } from "@jgengine/core/cartridge/runtime";
5
+ import { WASD_KEYBINDS, leveled } from "@jgengine/core/cartridge/spec";
6
+ import { validateCartridge } from "@jgengine/core/cartridge/validate";
7
+ import { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react";
8
+ import { useAbilitySlots, useEntityStat } from "@jgengine/react/hooks";
9
+ import { useGameContext } from "@jgengine/react/provider";
10
+ import { defineGame } from "./defineGame.js";
11
+ function useTick(intervalMs) {
12
+ const [, setTick] = useState(0);
13
+ useEffect(() => {
14
+ const id = window.setInterval(() => setTick((value) => value + 1), intervalMs);
15
+ return () => window.clearInterval(id);
16
+ }, [intervalMs]);
17
+ }
18
+ function useRunPhase(ctx, runtime) {
19
+ const run = runtime.run(ctx);
20
+ useSyncExternalStore(run.subscribe, () => `${run.phase}:${run.pendingOffers?.map((offer) => offer.id).join(",") ?? ""}`);
21
+ }
22
+ function winSeconds(config) {
23
+ return config.rules.win?.kind === "survive" ? config.rules.win.seconds : null;
24
+ }
25
+ function VitalItem({ config, item }) {
26
+ const ctx = useGameContext();
27
+ const value = useEntityStat(ctx.player.userId, item.stat);
28
+ if (value === null)
29
+ return null;
30
+ return _jsx(config.panels.Vital, { value: value, label: item.label, tone: item.tone, width: item.width });
31
+ }
32
+ function XpItem({ config, item }) {
33
+ const ctx = useGameContext();
34
+ const xp = useEntityStat(ctx.player.userId, "xp");
35
+ const level = useEntityStat(ctx.player.userId, "level");
36
+ if (xp === null)
37
+ return null;
38
+ const fraction = (xp.current - xp.min) / Math.max(1, xp.max - xp.min);
39
+ return _jsx(config.panels.Xp, { fraction: fraction, level: level?.current, width: item.width });
40
+ }
41
+ function TimerItem({ config, runtime, item, }) {
42
+ const ctx = useGameContext();
43
+ const target = winSeconds(config);
44
+ const elapsed = runtime.run(ctx).playingSeconds;
45
+ const seconds = target === null ? elapsed : Math.max(0, target - elapsed);
46
+ return _jsx(config.panels.Timer, { seconds: seconds, label: item.label });
47
+ }
48
+ function ScoreItem({ config, runtime, item, }) {
49
+ const ctx = useGameContext();
50
+ return _jsx(config.panels.Score, { value: runtime.run(ctx).kills, label: item.label, digits: item.digits });
51
+ }
52
+ function AbilityBarItem({ config, runtime, item, }) {
53
+ const ctx = useGameContext();
54
+ const slots = useAbilitySlots(runtime.weaponKit(ctx));
55
+ return (_jsx(config.panels.AbilityBar, { slots: slots.map((slot) => ({
56
+ id: slot.id,
57
+ icon: item.icons[slot.id] ?? slot.id,
58
+ state: slot.state,
59
+ cooldownFraction: slot.cooldownFraction,
60
+ justCast: slot.justCast,
61
+ })) }));
62
+ }
63
+ function PanelItems({ config, runtime, items, }) {
64
+ return (_jsx(_Fragment, { children: items.map((item, index) => {
65
+ switch (item.kind) {
66
+ case "vital":
67
+ return _jsx(VitalItem, { config: config, item: item }, index);
68
+ case "xp":
69
+ return _jsx(XpItem, { config: config, item: item }, index);
70
+ case "timer":
71
+ return _jsx(TimerItem, { config: config, runtime: runtime, item: item }, index);
72
+ case "score":
73
+ return _jsx(ScoreItem, { config: config, runtime: runtime, item: item }, index);
74
+ case "abilityBar":
75
+ return _jsx(AbilityBarItem, { config: config, runtime: runtime, item: item }, index);
76
+ case "component":
77
+ return _jsx(item.Component, {}, index);
78
+ }
79
+ }) }));
80
+ }
81
+ function WinScreenHost({ config, runtime }) {
82
+ const ctx = useGameContext();
83
+ const level = useEntityStat(ctx.player.userId, "level");
84
+ const win = config.screens.win;
85
+ if (win === undefined)
86
+ return null;
87
+ const run = runtime.run(ctx);
88
+ const lines = win.lines.map((line) => ({
89
+ label: line.label,
90
+ accent: line.accent,
91
+ value: "value" in line ? line.value : line.source === "kills" ? run.kills : (level?.current ?? 1),
92
+ }));
93
+ return _jsx(config.panels.WinScreen, { title: win.title, lines: lines });
94
+ }
95
+ function DraftModalHost({ config, runtime }) {
96
+ const ctx = useGameContext();
97
+ const offers = runtime.run(ctx).pendingOffers;
98
+ if (offers === null)
99
+ return null;
100
+ return _jsx(config.panels.DraftModal, { offers: offers, choose: (id) => runtime.chooseUpgrade(ctx, id) });
101
+ }
102
+ function CartridgeUI({ config, runtime }) {
103
+ const ctx = useGameContext();
104
+ const layout = useHudLayout({ storageKey: config.hud.storageKey ?? config.name });
105
+ useRunPhase(ctx, runtime);
106
+ useTick(200);
107
+ const run = runtime.run(ctx);
108
+ return (_jsxs("div", { style: { ...config.theme, display: "contents" }, children: [run.phase === "start" && (_jsx(config.panels.StartScreen, { title: config.screens.start?.title ?? config.name, subtitle: config.screens.start?.subtitle, buttonLabel: config.screens.start?.buttonLabel, begin: () => runtime.begin(ctx) })), run.phase === "countdown" && _jsx(config.panels.Countdown, { seconds: Math.ceil(run.countdownRemaining) }), run.phase === "lost" && config.screens.lose !== undefined && (_jsx(config.panels.LoseScreen, { title: config.screens.lose.title, subtitle: config.screens.lose.subtitle })), run.phase === "won" && _jsx(WinScreenHost, { config: config, runtime: runtime }), run.phase === "playing" && (_jsxs(HudCanvas, { layout: layout, children: [_jsx(DraftModalHost, { config: config, runtime: runtime }), config.hud.panels.map((panel) => (_jsx(HudPanel, { id: panel.id, anchor: panel.anchor, inset: panel.inset, style: panel.items.length > 1
109
+ ? { display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }
110
+ : undefined, children: _jsx(PanelItems, { config: config, runtime: runtime, items: panel.items }) }, panel.id)))] }))] }));
111
+ }
112
+ const MAX_BOLTS = 10;
113
+ const MAX_PULSES = 6;
114
+ function hideMesh(mesh) {
115
+ if (mesh !== null)
116
+ mesh.visible = false;
117
+ }
118
+ function BoltPool({ runtime, weaponId, weapon, }) {
119
+ const ctx = useGameContext();
120
+ const refs = useRef(new Array(MAX_BOLTS).fill(null));
121
+ useFrame(() => {
122
+ const run = runtime.run(ctx);
123
+ const now = ctx.time.now();
124
+ const bolts = run.bolts.filter((bolt) => bolt.weaponId === weaponId);
125
+ for (let i = 0; i < MAX_BOLTS; i += 1) {
126
+ const mesh = refs.current[i];
127
+ const bolt = bolts[i];
128
+ if (mesh === null)
129
+ continue;
130
+ if (bolt === undefined) {
131
+ hideMesh(mesh);
132
+ continue;
133
+ }
134
+ const t = Math.min(1, Math.max(0, (now - bolt.firedAt) / bolt.travelSeconds));
135
+ mesh.visible = true;
136
+ mesh.position.set(bolt.origin[0] + (bolt.target[0] - bolt.origin[0]) * t, bolt.origin[1] + 0.9, bolt.origin[2] + (bolt.target[2] - bolt.origin[2]) * t);
137
+ }
138
+ });
139
+ return (_jsx(_Fragment, { children: Array.from({ length: MAX_BOLTS }, (_, i) => (_jsxs("mesh", { ref: (mesh) => {
140
+ refs.current[i] = mesh;
141
+ }, visible: false, children: [_jsx("sphereGeometry", { args: [0.16, 8, 8] }), _jsx("meshStandardMaterial", { color: weapon.fxColor ?? "#8be9f0", emissive: weapon.fxEmissive ?? weapon.fxColor ?? "#8be9f0", emissiveIntensity: 1.6 })] }, i))) }));
142
+ }
143
+ function PulsePool({ runtime, weaponId, weapon, }) {
144
+ const ctx = useGameContext();
145
+ const refs = useRef(new Array(MAX_PULSES).fill(null));
146
+ useFrame(() => {
147
+ const run = runtime.run(ctx);
148
+ const now = ctx.time.now();
149
+ const pulses = run.pulses.filter((pulse) => pulse.weaponId === weaponId);
150
+ for (let i = 0; i < MAX_PULSES; i += 1) {
151
+ const mesh = refs.current[i];
152
+ const pulse = pulses[i];
153
+ if (mesh === null)
154
+ continue;
155
+ if (pulse === undefined) {
156
+ hideMesh(mesh);
157
+ continue;
158
+ }
159
+ const t = Math.min(1, Math.max(0, (now - pulse.firedAt) / pulse.durationSeconds));
160
+ const radius = Math.max(0.01, pulse.maxRadius * t);
161
+ mesh.visible = true;
162
+ mesh.position.set(pulse.at[0], pulse.at[1] + 0.05, pulse.at[2]);
163
+ mesh.scale.set(radius, radius, radius);
164
+ const material = mesh.material;
165
+ if (!Array.isArray(material))
166
+ material.opacity = 0.55 * (1 - t);
167
+ }
168
+ });
169
+ return (_jsx(_Fragment, { children: Array.from({ length: MAX_PULSES }, (_, i) => (_jsxs("mesh", { ref: (mesh) => {
170
+ refs.current[i] = mesh;
171
+ }, rotation: [-Math.PI / 2, 0, 0], visible: false, children: [_jsx("ringGeometry", { args: [0.85, 1, 48] }), _jsx("meshBasicMaterial", { color: weapon.fxColor ?? "#a566d9", transparent: true, opacity: 0.5 })] }, i))) }));
172
+ }
173
+ function OrbitPool({ runtime, weaponId, weapon, }) {
174
+ const ctx = useGameContext();
175
+ const maxBlades = Math.ceil(leveled(weapon.blades, weapon.maxLevel));
176
+ const groupRef = useRef(null);
177
+ const refs = useRef(new Array(maxBlades).fill(null));
178
+ useFrame(() => {
179
+ const run = runtime.run(ctx);
180
+ const player = ctx.scene.entity.get(ctx.player.userId);
181
+ if (player === null || run.phase !== "playing") {
182
+ if (groupRef.current !== null)
183
+ groupRef.current.visible = false;
184
+ return;
185
+ }
186
+ const level = run.weaponLevel(weaponId);
187
+ const blades = leveled(weapon.blades, level);
188
+ const radius = leveled(weapon.radius, level);
189
+ const angle = ctx.time.now() * weapon.angularSpeed;
190
+ if (groupRef.current !== null)
191
+ groupRef.current.visible = true;
192
+ for (let i = 0; i < maxBlades; i += 1) {
193
+ const mesh = refs.current[i];
194
+ if (mesh === null)
195
+ continue;
196
+ if (i >= blades) {
197
+ hideMesh(mesh);
198
+ continue;
199
+ }
200
+ const theta = angle + (i * Math.PI * 2) / blades;
201
+ mesh.visible = true;
202
+ mesh.position.set(player.position[0] + Math.cos(theta) * radius, player.position[1] + 0.55, player.position[2] + Math.sin(theta) * radius);
203
+ }
204
+ });
205
+ return (_jsx("group", { ref: groupRef, children: Array.from({ length: maxBlades }, (_, i) => (_jsxs("mesh", { ref: (mesh) => {
206
+ refs.current[i] = mesh;
207
+ }, visible: false, children: [_jsx("boxGeometry", { args: [0.32, 0.08, 0.14] }), _jsx("meshStandardMaterial", { color: weapon.fxColor ?? "#dfe9ea", emissive: weapon.fxEmissive ?? weapon.fxColor ?? "#dfe9ea", emissiveIntensity: 0.8 })] }, i))) }));
208
+ }
209
+ function CartridgeFxOverlay({ config, runtime }) {
210
+ return (_jsx("group", { children: Object.entries(config.weapons).map(([weaponId, weapon]) => {
211
+ if (weapon.kind === "projectile") {
212
+ return _jsx(BoltPool, { runtime: runtime, weaponId: weaponId, weapon: weapon }, weaponId);
213
+ }
214
+ if (weapon.kind === "pulse") {
215
+ return _jsx(PulsePool, { runtime: runtime, weaponId: weaponId, weapon: weapon }, weaponId);
216
+ }
217
+ if (weapon.kind === "orbit") {
218
+ return _jsx(OrbitPool, { runtime: runtime, weaponId: weaponId, weapon: weapon }, weaponId);
219
+ }
220
+ return null;
221
+ }) }));
222
+ }
223
+ export function cartridge(config) {
224
+ const problems = validateCartridge(config);
225
+ if (problems.length > 0) {
226
+ throw new Error(`cartridge "${config.name}": invalid spec:\n${problems.map((p) => ` - ${p}`).join("\n")}`);
227
+ }
228
+ const runtime = createCartridge(config);
229
+ return defineGame({
230
+ name: config.name,
231
+ assets: config.assets,
232
+ world: config.world,
233
+ physics: config.physics,
234
+ input: config.input ?? WASD_KEYBINDS,
235
+ save: config.save ?? "none",
236
+ content: runtime.content,
237
+ loop: runtime.loop,
238
+ GameUI: () => _jsx(CartridgeUI, { config: config, runtime: runtime }),
239
+ WorldOverlay: () => _jsx(CartridgeFxOverlay, { config: config, runtime: runtime }),
240
+ camera: config.camera,
241
+ entitySprites: config.entitySprites,
242
+ worldItem: config.worldItem,
243
+ settings: config.settings,
244
+ });
245
+ }
@@ -10,7 +10,7 @@ function worldBackdrop(feature) {
10
10
  }
11
11
  const emptyUi = () => null;
12
12
  export function defineGame(config) {
13
- const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
13
+ const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
14
14
  const game = defineEngineGame({ ...engineFields, multiplayer: multiplayer ?? offline() });
15
15
  return {
16
16
  game,
@@ -34,6 +34,7 @@ export function defineGame(config) {
34
34
  prompts,
35
35
  pointer,
36
36
  touch,
37
+ orientation,
37
38
  worldHealthBars,
38
39
  audio,
39
40
  entitySounds,
@@ -0,0 +1,5 @@
1
+ /**
2
+ * World-space collision debugger. Performs zero scene scans and zero raycasts
3
+ * when every layer is off. Mount only when shell devtools are enabled.
4
+ */
5
+ export declare function CollisionDebugWorld(): import("react").JSX.Element | null;
@@ -0,0 +1,180 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Html, Line } from "@react-three/drei";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import * as THREE from "three";
6
+ import { useGameContext } from "@jgengine/react/provider";
7
+ import { aimProbeNeeded, anyCollisionLayerOn, colliderScanNeeded, collisionDebug, projectileListenNeeded, } from "./collisionDebug.js";
8
+ import { AIM_DAMAGE_COLOR, AIM_LASER_COLOR, AIM_MISS_COLOR, AIM_SOLID_COLOR, BODY_WIRE_COLOR, collectDebugShapes, computeAimLaser, HITBOX_WIRE_COLOR, muzzleMarkerFromOrigin, PROJECTILE_PATH_COLOR, } from "./collisionDebugMath.js";
9
+ function useCollisionDebugState() {
10
+ return useSyncExternalStore(collisionDebug.subscribe, () => collisionDebug.getState(), () => collisionDebug.getState());
11
+ }
12
+ function WireSphere({ center, radius, color, label, }) {
13
+ return (_jsxs("group", { position: [center[0], center[1], center[2]], children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [radius, 16, 12] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false })] }), _jsx(Html, { center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: label }) })] }));
14
+ }
15
+ function WireAabb({ center, halfExtents, rotationY, color, label, }) {
16
+ return (_jsxs("group", { position: [center[0], center[1], center[2]], rotation: [0, rotationY, 0], children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [halfExtents[0] * 2, halfExtents[1] * 2, halfExtents[2] * 2] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false })] }), _jsx(Html, { position: [0, halfExtents[1] + 0.15, 0], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: label }) })] }));
17
+ }
18
+ function ColliderShapeMesh({ entry }) {
19
+ const color = entry.style === "hitbox" ? HITBOX_WIRE_COLOR : BODY_WIRE_COLOR;
20
+ if (entry.shape.kind === "sphere") {
21
+ return (_jsx(WireSphere, { center: entry.shape.center, radius: entry.shape.radius, color: color, label: entry.label }));
22
+ }
23
+ return (_jsx(WireAabb, { center: entry.shape.center, halfExtents: entry.shape.halfExtents, rotationY: entry.rotationY, color: color, label: entry.label }));
24
+ }
25
+ function endpointColor(kind) {
26
+ if (kind === "damage")
27
+ return AIM_DAMAGE_COLOR;
28
+ if (kind === "solid")
29
+ return AIM_SOLID_COLOR;
30
+ return AIM_MISS_COLOR;
31
+ }
32
+ function AimEndpointMark({ point, kind, }) {
33
+ const color = endpointColor(kind);
34
+ if (kind === "damage") {
35
+ const s = 0.14;
36
+ return (_jsxs("group", { position: [point[0], point[1], point[2]], children: [_jsx(Line, { points: [
37
+ new THREE.Vector3(-s, -s, 0),
38
+ new THREE.Vector3(s, s, 0),
39
+ ], color: color, lineWidth: 2, depthTest: false }), _jsx(Line, { points: [
40
+ new THREE.Vector3(-s, s, 0),
41
+ new THREE.Vector3(s, -s, 0),
42
+ ], color: color, lineWidth: 2, depthTest: false })] }));
43
+ }
44
+ if (kind === "solid") {
45
+ return (_jsxs("mesh", { position: [point[0], point[1], point[2]], children: [_jsx("sphereGeometry", { args: [0.1, 12, 10] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, depthTest: false })] }));
46
+ }
47
+ return (_jsxs("mesh", { position: [point[0], point[1], point[2]], children: [_jsx("sphereGeometry", { args: [0.06, 8, 8] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.55, depthTest: false })] }));
48
+ }
49
+ function MuzzleMark({ origin }) {
50
+ const mark = muzzleMarkerFromOrigin([origin[0], origin[1], origin[2]]);
51
+ return (_jsxs("mesh", { position: [mark.center[0], mark.center[1], mark.center[2]], children: [_jsx("sphereGeometry", { args: [mark.radius, 12, 10] }), _jsx("meshBasicMaterial", { color: mark.color, depthTest: false })] }));
52
+ }
53
+ function collectFromContext(ctx, layers) {
54
+ return collectDebugShapes({
55
+ layers,
56
+ entities: ctx.scene.entity.list().map((entity) => ({
57
+ id: entity.id,
58
+ position: entity.position,
59
+ rotationY: entity.rotationY,
60
+ name: entity.name,
61
+ })),
62
+ objects: ctx.scene.object.list().map((object) => ({
63
+ instanceId: object.instanceId,
64
+ catalogId: object.catalogId,
65
+ position: object.position,
66
+ rotationY: object.rotationY,
67
+ })),
68
+ entityCollidersOf: (id) => ctx.scene.entity.collidersOf(id),
69
+ objectCollidersOf: (id) => ctx.scene.object.collidersOf(id),
70
+ objectHalfExtentsOf: (catalogId) => {
71
+ for (const object of ctx.scene.object.list()) {
72
+ if (object.catalogId !== catalogId)
73
+ continue;
74
+ const half = ctx.scene.object.catalog(object.instanceId)?.halfExtents;
75
+ if (half === undefined)
76
+ return null;
77
+ return [half[0], half[1], half[2]];
78
+ }
79
+ return null;
80
+ },
81
+ });
82
+ }
83
+ /**
84
+ * World-space collision debugger. Performs zero scene scans and zero raycasts
85
+ * when every layer is off. Mount only when shell devtools are enabled.
86
+ */
87
+ export function CollisionDebugWorld() {
88
+ const ctx = useGameContext();
89
+ const state = useCollisionDebugState();
90
+ const [shapes, setShapes] = useState([]);
91
+ const [laser, setLaser] = useState(null);
92
+ const frameCounter = useRef(0);
93
+ useEffect(() => {
94
+ if (!projectileListenNeeded(state.layers))
95
+ return;
96
+ return ctx.game.events.on("projectile.settled", (event) => {
97
+ collisionDebug.pushProjectileTrace({
98
+ origin: event.origin,
99
+ at: event.at,
100
+ hit: event.hit,
101
+ nowMs: performance.now(),
102
+ });
103
+ });
104
+ }, [ctx, state.layers.projectiles, state.layers.muzzles]);
105
+ useFrame(() => {
106
+ if (!anyCollisionLayerOn(state.layers)) {
107
+ if (shapes.length > 0)
108
+ setShapes([]);
109
+ if (laser !== null)
110
+ setLaser(null);
111
+ return;
112
+ }
113
+ frameCounter.current += 1;
114
+ const now = performance.now();
115
+ if (projectileListenNeeded(state.layers)) {
116
+ collisionDebug.pruneProjectileTraces(now);
117
+ }
118
+ if (colliderScanNeeded(state.layers) && frameCounter.current % 2 === 0) {
119
+ setShapes(collectFromContext(ctx, state.layers));
120
+ }
121
+ else if (!colliderScanNeeded(state.layers) && shapes.length > 0) {
122
+ setShapes([]);
123
+ }
124
+ if (aimProbeNeeded(state.layers)) {
125
+ const probe = collisionDebug.getAimProbe();
126
+ if (probe === null) {
127
+ if (laser !== null)
128
+ setLaser(null);
129
+ }
130
+ else {
131
+ const next = computeAimLaser({
132
+ layers: state.layers,
133
+ sceneRaycast: {
134
+ raycast: (input) => ctx.scene.raycast(input),
135
+ raycastAll: (input) => [...ctx.scene.raycastAll(input)],
136
+ },
137
+ positionOf: (id) => ctx.scene.entity.get(id)?.position,
138
+ rotationYOf: (id) => ctx.scene.entity.get(id)?.rotationY,
139
+ from: probe.from,
140
+ aim: probe.aim,
141
+ originPolicy: probe.originPolicy,
142
+ maxDistance: probe.maxDistance ?? 100,
143
+ });
144
+ setLaser(next);
145
+ }
146
+ }
147
+ else if (laser !== null) {
148
+ setLaser(null);
149
+ }
150
+ });
151
+ const projectilePoints = useMemo(() => {
152
+ if (!state.layers.projectiles)
153
+ return [];
154
+ return state.projectileTraces.map((trace) => ({
155
+ id: trace.id,
156
+ points: [
157
+ new THREE.Vector3(trace.origin[0], trace.origin[1], trace.origin[2]),
158
+ new THREE.Vector3(trace.at[0], trace.at[1], trace.at[2]),
159
+ ],
160
+ }));
161
+ }, [state.layers.projectiles, state.projectileTraces]);
162
+ const muzzleOrigins = useMemo(() => {
163
+ if (!state.layers.muzzles)
164
+ return [];
165
+ const origins = state.projectileTraces.map((trace) => ({
166
+ id: trace.id,
167
+ origin: trace.origin,
168
+ }));
169
+ if (laser !== null && state.layers.aimLaser) {
170
+ origins.push({ id: -1, origin: laser.origin });
171
+ }
172
+ return origins;
173
+ }, [state.layers.muzzles, state.layers.aimLaser, state.projectileTraces, laser]);
174
+ if (!anyCollisionLayerOn(state.layers))
175
+ return null;
176
+ return (_jsxs("group", { children: [shapes.map((entry) => (_jsx(ColliderShapeMesh, { entry: entry }, entry.key))), projectilePoints.map((path) => (_jsx(Line, { points: path.points, color: PROJECTILE_PATH_COLOR, lineWidth: 2, transparent: true, opacity: 0.9, depthTest: false }, path.id))), muzzleOrigins.map((entry) => (_jsx(MuzzleMark, { origin: entry.origin }, `muzzle-${entry.id}`))), laser !== null ? (_jsxs(_Fragment, { children: [_jsx(Line, { points: [
177
+ new THREE.Vector3(laser.origin[0], laser.origin[1], laser.origin[2]),
178
+ new THREE.Vector3(laser.end[0], laser.end[1], laser.end[2]),
179
+ ], color: AIM_LASER_COLOR, lineWidth: 1.5, transparent: true, opacity: 0.95, depthTest: false }), _jsx(AimEndpointMark, { point: laser.end, kind: laser.kind })] })) : null] }));
180
+ }
@@ -1,4 +1,4 @@
1
- import { type DevtoolsOverrides } from "@jgengine/core/devtools/devtools";
1
+ import { type DevtoolsOverrides, type DevtoolsSnapshot } from "@jgengine/core/devtools/devtools";
2
2
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
3
3
  import type { ShellMultiplayer } from "../multiplayer.js";
4
4
  import type { PlayableGame } from "../registry.js";
@@ -6,6 +6,70 @@ export declare function persistDevtoolsOverrides(gameName: string): DevtoolsOver
6
6
  export declare function applyStoredDevtoolsOverrides(gameName: string): void;
7
7
  export declare function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer;
8
8
  export declare function DevtoolsRendererProbe(): null;
9
+ export declare function buildLeanReport(playable: PlayableGame): {
10
+ game: string;
11
+ at: number;
12
+ why: string | null;
13
+ frame: {
14
+ fps: number;
15
+ avgFrameMs: number;
16
+ p95FrameMs: number;
17
+ maxFrameMs: number;
18
+ avgSimMs: number;
19
+ maxSimMs: number;
20
+ avgOutsideMs: number;
21
+ maxOutsideMs: number;
22
+ longFrames: number;
23
+ samples: number;
24
+ phases: {
25
+ name: string;
26
+ avgMs: number;
27
+ maxMs: number;
28
+ pctOfSim: number;
29
+ }[];
30
+ } | null;
31
+ render: import("@jgengine/core/devtools/devtools").RenderSample | null;
32
+ latency: import("@jgengine/core/devtools/devtools").LatencyStats | null;
33
+ longFrameSummary: {
34
+ count: number;
35
+ culprits: {
36
+ culprit: string;
37
+ count: number;
38
+ }[];
39
+ maxFrameMs: number;
40
+ maxDrawCalls: number | undefined;
41
+ maxGeometries: number | undefined;
42
+ } | null;
43
+ longFrames: {
44
+ at: number;
45
+ frameMs: number;
46
+ simMs: number;
47
+ outsideMs: number;
48
+ culprit: string;
49
+ reason: string;
50
+ phases: {
51
+ name: string;
52
+ ms: number;
53
+ }[];
54
+ probes: Record<string, unknown>;
55
+ render: import("@jgengine/core/devtools/devtools").RenderSample | null;
56
+ }[];
57
+ probes: Record<string, unknown>;
58
+ logs: import("@jgengine/core/devtools/devtools").DevtoolsLogEntry[] | undefined;
59
+ controls: {
60
+ name: string;
61
+ value: unknown;
62
+ initial: unknown;
63
+ }[] | undefined;
64
+ discovered: {
65
+ id: string;
66
+ value: unknown;
67
+ }[] | undefined;
68
+ discoveredCount: number;
69
+ };
70
+ export declare function buildFullReport(playable: PlayableGame): DevtoolsSnapshot & {
71
+ game: string;
72
+ };
9
73
  export declare function DevtoolsOverlay({ open, ctx, playable, multiplayer, }: {
10
74
  open: boolean;
11
75
  ctx: GameContext;