@jgengine/shell 0.7.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 (168) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +37 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +36 -1
  8. package/dist/GamePlayerShell.js +751 -80
  9. package/dist/GameUiPreview.js +2 -1
  10. package/dist/audio/AudioComponents.js +25 -8
  11. package/dist/audio/audioEngine.d.ts +1 -0
  12. package/dist/audio/audioEngine.js +4 -0
  13. package/dist/behaviour.d.ts +12 -0
  14. package/dist/behaviour.js +27 -0
  15. package/dist/behaviourAttach.d.ts +9 -0
  16. package/dist/behaviourAttach.js +32 -0
  17. package/dist/behaviourDriver.d.ts +7 -0
  18. package/dist/behaviourDriver.js +21 -0
  19. package/dist/camera/GameCameraRig.d.ts +8 -3
  20. package/dist/camera/GameCameraRig.js +28 -14
  21. package/dist/camera/GameFirstPersonCamera.js +16 -0
  22. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  23. package/dist/camera/GameInspectionCamera.js +24 -0
  24. package/dist/camera/GameOrbitCamera.js +10 -1
  25. package/dist/camera/PlayerFov.d.ts +18 -0
  26. package/dist/camera/PlayerFov.js +48 -0
  27. package/dist/camera/cameraBlendMath.d.ts +13 -0
  28. package/dist/camera/cameraBlendMath.js +36 -0
  29. package/dist/camera/cameraRigs.d.ts +8 -1
  30. package/dist/camera/cameraRigs.js +78 -38
  31. package/dist/camera/fovPreference.d.ts +25 -0
  32. package/dist/camera/fovPreference.js +73 -0
  33. package/dist/camera/index.d.ts +7 -2
  34. package/dist/camera/index.js +7 -2
  35. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  36. package/dist/camera/inspectionCameraMath.js +44 -0
  37. package/dist/camera/rigMath.d.ts +50 -1
  38. package/dist/camera/rigMath.js +91 -1
  39. package/dist/camera/rigResolve.d.ts +15 -0
  40. package/dist/camera/rigResolve.js +52 -0
  41. package/dist/camera/shakeChannel.d.ts +3 -17
  42. package/dist/camera/shakeChannel.js +2 -21
  43. package/dist/camera/shakeChannelMath.d.ts +8 -0
  44. package/dist/camera/shakeChannelMath.js +21 -0
  45. package/dist/cartridge.d.ts +145 -0
  46. package/dist/cartridge.js +245 -0
  47. package/dist/defineGame.d.ts +16 -0
  48. package/dist/defineGame.js +52 -0
  49. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  50. package/dist/devtools/CollisionDebugWorld.js +180 -0
  51. package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
  52. package/dist/devtools/DevtoolsOverlay.js +676 -0
  53. package/dist/devtools/collisionDebug.d.ts +57 -0
  54. package/dist/devtools/collisionDebug.js +127 -0
  55. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  56. package/dist/devtools/collisionDebugMath.js +128 -0
  57. package/dist/environment/Daylight.d.ts +56 -0
  58. package/dist/environment/Daylight.js +101 -0
  59. package/dist/environment/EnvironmentScene.js +87 -48
  60. package/dist/environment/GroundPad.d.ts +7 -0
  61. package/dist/environment/GroundPad.js +12 -0
  62. package/dist/environment/daylightCycle.d.ts +26 -0
  63. package/dist/environment/daylightCycle.js +116 -0
  64. package/dist/environment/groundPadMath.d.ts +13 -0
  65. package/dist/environment/groundPadMath.js +10 -0
  66. package/dist/environment/index.d.ts +3 -0
  67. package/dist/environment/index.js +3 -0
  68. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  69. package/dist/environment/skyLightingPolicy.js +6 -0
  70. package/dist/input/mouseLook.d.ts +27 -0
  71. package/dist/input/mouseLook.js +35 -0
  72. package/dist/materialOverride.d.ts +6 -0
  73. package/dist/materialOverride.js +28 -0
  74. package/dist/multiplayer.d.ts +16 -9
  75. package/dist/multiplayer.js +62 -12
  76. package/dist/pointer/PointerProbe.js +20 -3
  77. package/dist/pointer/pointerService.d.ts +5 -0
  78. package/dist/pointer/pointerService.js +51 -24
  79. package/dist/registry.d.ts +4 -1
  80. package/dist/registry.js +3 -1
  81. package/dist/render/modelRender.d.ts +23 -0
  82. package/dist/render/modelRender.js +106 -0
  83. package/dist/render/resolveModel.d.ts +14 -0
  84. package/dist/render/resolveModel.js +24 -0
  85. package/dist/settings/QuickControls.d.ts +4 -0
  86. package/dist/settings/QuickControls.js +42 -0
  87. package/dist/settings/SettingsChrome.d.ts +1 -0
  88. package/dist/settings/SettingsChrome.js +10 -0
  89. package/dist/settings/SettingsMenu.d.ts +6 -0
  90. package/dist/settings/SettingsMenu.js +148 -0
  91. package/dist/settings/SettingsRuntime.d.ts +11 -0
  92. package/dist/settings/SettingsRuntime.js +19 -0
  93. package/dist/settings/appliedSettings.d.ts +14 -0
  94. package/dist/settings/appliedSettings.js +27 -0
  95. package/dist/settings/settingsController.d.ts +20 -0
  96. package/dist/settings/settingsController.js +165 -0
  97. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  98. package/dist/structures/GeneratedBuilding.js +96 -8
  99. package/dist/structures/index.d.ts +1 -1
  100. package/dist/structures/index.js +1 -1
  101. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  102. package/dist/terrain/CarvedTerrain.js +3 -3
  103. package/dist/terrain/GrassField.d.ts +3 -1
  104. package/dist/terrain/GrassField.js +4 -2
  105. package/dist/terrain/grassBudget.d.ts +3 -0
  106. package/dist/terrain/grassBudget.js +6 -0
  107. package/dist/terrain/grassGeometry.js +1 -1
  108. package/dist/terrain/index.d.ts +1 -1
  109. package/dist/terrain/index.js +1 -1
  110. package/dist/terrain/terrainMath.d.ts +9 -0
  111. package/dist/terrain/terrainMath.js +17 -2
  112. package/dist/touch/OrientationHint.d.ts +3 -0
  113. package/dist/touch/OrientationHint.js +13 -0
  114. package/dist/touch/TouchControlsOverlay.d.ts +34 -0
  115. package/dist/touch/TouchControlsOverlay.js +279 -0
  116. package/dist/visibility/CullingProvider.d.ts +21 -0
  117. package/dist/visibility/CullingProvider.js +134 -0
  118. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  119. package/dist/vision/FrustumSensorHud.js +25 -27
  120. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  121. package/dist/vision/frustumSampleEqual.js +10 -0
  122. package/dist/water/Ocean.js +1 -1
  123. package/dist/water/OceanConfig.d.ts +13 -0
  124. package/dist/water/OceanConfig.js +25 -17
  125. package/dist/water/OceanShader.d.ts +1 -1
  126. package/dist/water/OceanShader.js +3 -3
  127. package/dist/water/index.d.ts +1 -1
  128. package/dist/water/index.js +1 -1
  129. package/dist/weather/FireSpreadLayer.js +7 -2
  130. package/dist/weather/RainField.d.ts +3 -1
  131. package/dist/weather/RainField.js +4 -4
  132. package/dist/weather/SnowField.d.ts +3 -1
  133. package/dist/weather/SnowField.js +4 -4
  134. package/dist/weather/fireSpreadPose.d.ts +2 -0
  135. package/dist/weather/fireSpreadPose.js +4 -0
  136. package/dist/weather/weatherMath.d.ts +5 -1
  137. package/dist/weather/weatherMath.js +7 -2
  138. package/dist/world/DataObjects.d.ts +44 -0
  139. package/dist/world/DataObjects.js +77 -0
  140. package/dist/world/GridWorldScene.d.ts +10 -0
  141. package/dist/world/GridWorldScene.js +54 -0
  142. package/dist/world/SpriteBatch.d.ts +44 -0
  143. package/dist/world/SpriteBatch.js +112 -0
  144. package/dist/world/WorldHud.d.ts +8 -1
  145. package/dist/world/WorldHud.js +90 -39
  146. package/dist/world/WorldItems.js +21 -2
  147. package/dist/world/entityPose.d.ts +14 -0
  148. package/dist/world/entityPose.js +10 -0
  149. package/dist/world/telegraphPulse.d.ts +1 -0
  150. package/dist/world/telegraphPulse.js +4 -0
  151. package/dist/world/worldBarSamples.d.ts +30 -0
  152. package/dist/world/worldBarSamples.js +51 -0
  153. package/llms.txt +2643 -0
  154. package/package.json +6 -5
  155. package/dist/demo/builderDemo.d.ts +0 -2
  156. package/dist/demo/builderDemo.js +0 -189
  157. package/dist/demo/demoGame.d.ts +0 -2
  158. package/dist/demo/demoGame.js +0 -208
  159. package/dist/demo/environmentShowcase.d.ts +0 -2
  160. package/dist/demo/environmentShowcase.js +0 -15
  161. package/dist/demo/mapDemo.d.ts +0 -2
  162. package/dist/demo/mapDemo.js +0 -206
  163. package/dist/demo/pointerDemo.d.ts +0 -2
  164. package/dist/demo/pointerDemo.js +0 -151
  165. package/dist/demo/sensorShowcase.d.ts +0 -2
  166. package/dist/demo/sensorShowcase.js +0 -131
  167. package/dist/demo/survivalDemo.d.ts +0 -2
  168. package/dist/demo/survivalDemo.js +0 -291
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
3
+ * specific: an explicit `rig` field always wins; then `perspective: "first"`
4
+ * (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence
5
+ * of a rig's own config block selects that rig, checked in the fixed order below
6
+ * (#207.8) so a config carrying more than one block resolves deterministically
7
+ * instead of depending on object key order. Set `rig` explicitly to break a tie.
8
+ */
9
+ export function resolveRigKind(config) {
10
+ if (config?.rig !== undefined)
11
+ return config.rig;
12
+ if (config?.perspective === "first")
13
+ return "first";
14
+ if (config?.topDown !== undefined)
15
+ return "topDown";
16
+ if (config?.rts !== undefined)
17
+ return "rts";
18
+ if (config?.shoulder !== undefined)
19
+ return "shoulder";
20
+ if (config?.lockOn !== undefined)
21
+ return "lockOn";
22
+ if (config?.chase !== undefined)
23
+ return "chase";
24
+ if (config?.observer !== undefined)
25
+ return "observer";
26
+ if (config?.turntable !== undefined)
27
+ return "turntable";
28
+ if (config?.sideScroll !== undefined)
29
+ return "sideScroll";
30
+ if (config?.inspection !== undefined)
31
+ return "inspection";
32
+ return "orbit";
33
+ }
34
+ /**
35
+ * The turntable rig is a flat facade over the observer's point-orbit mode: map
36
+ * its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
37
+ */
38
+ export function turntableAsObserver(config) {
39
+ const t = config?.turntable;
40
+ if (t === undefined)
41
+ return config ?? {};
42
+ const observer = {
43
+ ...(t.target !== undefined ? { bind: { kind: "point", position: t.target } } : {}),
44
+ ...(t.distance !== undefined ? { distance: t.distance } : {}),
45
+ ...(t.height !== undefined ? { height: t.height } : {}),
46
+ ...(t.lookHeight !== undefined ? { lookHeight: t.lookHeight } : {}),
47
+ ...(t.orbitSpeed !== undefined ? { orbitSpeed: t.orbitSpeed } : {}),
48
+ ...(t.startAngle !== undefined ? { startAngle: t.startAngle } : {}),
49
+ ...(t.fov !== undefined ? { fov: t.fov } : {}),
50
+ };
51
+ return { ...config, observer };
52
+ }
@@ -1,20 +1,6 @@
1
- import { shakeOffset, type ShakeOffset } from "./rigMath.js";
2
- export interface CameraShakeChannel {
3
- /**
4
- * Add a trauma impulse. `amplitude` is a 0..1 fraction (1 = a full boss slam);
5
- * `decayPerSecond` optionally overrides how fast this and subsequent trauma
6
- * bleed off. Any gameplay system (combat hitstop, explosions, vehicle crashes)
7
- * can call this — the active rig reads the resulting shake every frame.
8
- */
9
- shake(amplitude: number, decayPerSecond?: number): void;
10
- /** Advance decay + the shake clock (called once per frame by the mounted rig). */
11
- step(dt: number): void;
12
- /** Current positional + roll shake for the given tuning. */
13
- sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset;
14
- /** Live trauma in [0,1] (for HUD/debug). */
15
- trauma(): number;
16
- }
17
- export declare function createCameraShakeChannel(defaultDecayPerSecond?: number): CameraShakeChannel;
1
+ import { type CameraShakeChannel } from "./shakeChannelMath.js";
2
+ export type { CameraShakeChannel } from "./shakeChannelMath.js";
3
+ export { createCameraShakeChannel } from "./shakeChannelMath.js";
18
4
  /**
19
5
  * Process-wide default channel. A shell mounts its own channel via
20
6
  * `CameraShakeContext`, but game systems that have no React context (e.g. a
@@ -1,25 +1,6 @@
1
1
  import { createContext, useContext } from "react";
2
- import { addTrauma, createTrauma, shakeOffset, stepTrauma } from "./rigMath.js";
3
- export function createCameraShakeChannel(defaultDecayPerSecond = 1.6) {
4
- const state = createTrauma();
5
- let decay = defaultDecayPerSecond;
6
- return {
7
- shake(amplitude, decayPerSecond) {
8
- if (decayPerSecond !== undefined)
9
- decay = decayPerSecond;
10
- addTrauma(state, amplitude);
11
- },
12
- step(dt) {
13
- stepTrauma(state, decay, dt);
14
- },
15
- sample(config) {
16
- return shakeOffset(state, config);
17
- },
18
- trauma() {
19
- return state.trauma;
20
- },
21
- };
22
- }
2
+ import { createCameraShakeChannel, } from "./shakeChannelMath.js";
3
+ export { createCameraShakeChannel } from "./shakeChannelMath.js";
23
4
  /**
24
5
  * Process-wide default channel. A shell mounts its own channel via
25
6
  * `CameraShakeContext`, but game systems that have no React context (e.g. a
@@ -0,0 +1,8 @@
1
+ import { shakeOffset, type ShakeOffset } from "./rigMath.js";
2
+ export interface CameraShakeChannel {
3
+ shake(amplitude: number, decayPerSecond?: number): void;
4
+ step(dt: number): void;
5
+ sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset;
6
+ trauma(): number;
7
+ }
8
+ export declare function createCameraShakeChannel(defaultDecayPerSecond?: number): CameraShakeChannel;
@@ -0,0 +1,21 @@
1
+ import { addTrauma, createTrauma, shakeOffset, stepTrauma } from "./rigMath.js";
2
+ export function createCameraShakeChannel(defaultDecayPerSecond = 1.6) {
3
+ const state = createTrauma();
4
+ let decay = defaultDecayPerSecond;
5
+ return {
6
+ shake(amplitude, decayPerSecond) {
7
+ if (decayPerSecond !== undefined)
8
+ decay = decayPerSecond;
9
+ addTrauma(state, amplitude);
10
+ },
11
+ step(dt) {
12
+ stepTrauma(state, decay, dt);
13
+ },
14
+ sample(config) {
15
+ return shakeOffset(state, config);
16
+ },
17
+ trauma() {
18
+ return state.trauma;
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,145 @@
1
+ import { type ComponentType } from "react";
2
+ import { type CartridgeSpec } from "@jgengine/core/cartridge/spec";
3
+ import type { AbilitySlotState } from "@jgengine/core/combat/abilityKit";
4
+ import type { HudAnchor } from "@jgengine/core/ui/hudLayout";
5
+ import { type GameConfig } from "./defineGame.js";
6
+ import type { PlayableGame } from "./registry.js";
7
+ export type CartridgePanelItem = {
8
+ kind: "vital";
9
+ stat: string;
10
+ label: string;
11
+ tone?: string;
12
+ width?: number;
13
+ } | {
14
+ kind: "xp";
15
+ width?: number;
16
+ } | {
17
+ kind: "timer";
18
+ label: string;
19
+ } | {
20
+ kind: "score";
21
+ source: "kills";
22
+ label: string;
23
+ digits?: number;
24
+ } | {
25
+ kind: "abilityBar";
26
+ icons: Record<string, string>;
27
+ } | {
28
+ kind: "component";
29
+ Component: ComponentType;
30
+ };
31
+ export interface CartridgeHudPanelSpec {
32
+ id: string;
33
+ anchor: HudAnchor;
34
+ inset?: {
35
+ x: number;
36
+ y: number;
37
+ };
38
+ items: readonly CartridgePanelItem[];
39
+ }
40
+ export type CartridgeResultLine = {
41
+ label: string;
42
+ accent?: boolean;
43
+ } & ({
44
+ source: "kills" | "level";
45
+ } | {
46
+ value: string | number;
47
+ });
48
+ export interface CartridgeScreens {
49
+ start?: {
50
+ title: string;
51
+ subtitle?: string;
52
+ buttonLabel?: string;
53
+ };
54
+ win?: {
55
+ title: string;
56
+ lines: readonly CartridgeResultLine[];
57
+ };
58
+ lose?: {
59
+ title: string;
60
+ subtitle?: string;
61
+ };
62
+ }
63
+ export interface CartridgeAbilitySlot {
64
+ id: string;
65
+ icon: string;
66
+ state: AbilitySlotState;
67
+ cooldownFraction: number;
68
+ justCast: boolean;
69
+ }
70
+ export interface CartridgePanels {
71
+ Vital: ComponentType<{
72
+ value: {
73
+ current: number;
74
+ max: number;
75
+ min?: number;
76
+ };
77
+ label: string;
78
+ tone?: string;
79
+ width?: number;
80
+ }>;
81
+ Xp: ComponentType<{
82
+ fraction: number;
83
+ level?: number;
84
+ width?: number;
85
+ }>;
86
+ Timer: ComponentType<{
87
+ seconds: number;
88
+ label: string;
89
+ }>;
90
+ Score: ComponentType<{
91
+ value: number;
92
+ label: string;
93
+ digits?: number;
94
+ }>;
95
+ AbilityBar: ComponentType<{
96
+ slots: readonly CartridgeAbilitySlot[];
97
+ }>;
98
+ DraftModal: ComponentType<{
99
+ offers: readonly {
100
+ id: string;
101
+ label: string;
102
+ }[];
103
+ choose(id: string): void;
104
+ }>;
105
+ StartScreen: ComponentType<{
106
+ title: string;
107
+ subtitle?: string;
108
+ buttonLabel?: string;
109
+ begin(): void;
110
+ }>;
111
+ Countdown: ComponentType<{
112
+ seconds: number;
113
+ }>;
114
+ WinScreen: ComponentType<{
115
+ title: string;
116
+ lines: readonly {
117
+ label: string;
118
+ value: string | number;
119
+ accent?: boolean;
120
+ }[];
121
+ }>;
122
+ LoseScreen: ComponentType<{
123
+ title: string;
124
+ subtitle?: string;
125
+ }>;
126
+ }
127
+ export type CartridgeConfig = CartridgeSpec & {
128
+ name: string;
129
+ panels: CartridgePanels;
130
+ hud: {
131
+ storageKey?: string;
132
+ panels: readonly CartridgeHudPanelSpec[];
133
+ };
134
+ screens: CartridgeScreens;
135
+ theme?: Record<string, string>;
136
+ world?: GameConfig["world"];
137
+ physics?: GameConfig["physics"];
138
+ assets?: GameConfig["assets"];
139
+ save?: GameConfig["save"];
140
+ camera?: GameConfig["camera"];
141
+ entitySprites?: GameConfig["entitySprites"];
142
+ worldItem?: GameConfig["worldItem"];
143
+ settings?: GameConfig["settings"];
144
+ };
145
+ export declare function cartridge(config: CartridgeConfig): PlayableGame;
@@ -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
+ }
@@ -0,0 +1,16 @@
1
+ import type { ComponentType } from "react";
2
+ import { type GameDefinitionConfig, type GameLoop } from "@jgengine/core/game/defineGame";
3
+ import type { GameContext, GameContextContent } from "@jgengine/core/runtime/gameContext";
4
+ import type { ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
5
+ import type { PlayableGame } from "./registry.js";
6
+ type EngineFields<TAssetRef extends ModelAssetRef> = Omit<GameDefinitionConfig<TAssetRef>, "loop" | "ui" | "multiplayer"> & {
7
+ multiplayer?: GameDefinitionConfig<TAssetRef>["multiplayer"];
8
+ };
9
+ type PresentationFields = Omit<PlayableGame, "game" | "content" | "loop" | "GameUI"> & {
10
+ content?: GameContextContent;
11
+ loop?: Partial<GameLoop<GameContext>>;
12
+ GameUI?: ComponentType;
13
+ };
14
+ export type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields;
15
+ export declare function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame;
16
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { defineGame as defineEngineGame, } from "@jgengine/core/game/defineGame";
3
+ import { offline } from "@jgengine/core/runtime/adapter";
4
+ import { EnvironmentScene } from "./environment.js";
5
+ const noop = () => { };
6
+ function worldBackdrop(feature) {
7
+ return function WorldBackdrop() {
8
+ return _jsx(EnvironmentScene, { feature: feature });
9
+ };
10
+ }
11
+ const emptyUi = () => null;
12
+ export function defineGame(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
+ const game = defineEngineGame({ ...engineFields, multiplayer: multiplayer ?? offline() });
15
+ return {
16
+ game,
17
+ content: content ?? {},
18
+ loop: {
19
+ onInit: loop?.onInit ?? noop,
20
+ onNewPlayer: loop?.onNewPlayer ?? noop,
21
+ onTick: loop?.onTick ?? noop,
22
+ },
23
+ GameUI: GameUI ?? emptyUi,
24
+ environment: environment ??
25
+ (game.world?.kind === "environment" ? worldBackdrop(game.world) : undefined),
26
+ camera: camera ?? { perspective: "third" },
27
+ WorldOverlay,
28
+ renderEntity,
29
+ renderObject,
30
+ entitySprites,
31
+ entityModels,
32
+ objectModels,
33
+ hotbarSelection,
34
+ prompts,
35
+ pointer,
36
+ touch,
37
+ orientation,
38
+ worldHealthBars,
39
+ audio,
40
+ entitySounds,
41
+ objectSounds,
42
+ worldItem,
43
+ collision,
44
+ movement,
45
+ lighting,
46
+ backdrop,
47
+ shadows,
48
+ presentation,
49
+ objectStyles,
50
+ devtools,
51
+ };
52
+ }
@@ -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;