@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -0,0 +1,31 @@
1
+ export function idleWindow(lastSeenMs, nowMs, config = {}) {
2
+ const elapsedSeconds = Math.max(0, (nowMs - lastSeenMs) / 1000);
3
+ const capped = config.maxSeconds !== undefined && elapsedSeconds > config.maxSeconds;
4
+ const boundedSeconds = Math.min(elapsedSeconds, config.maxSeconds ?? Number.POSITIVE_INFINITY);
5
+ const efficiency = Math.max(0, config.efficiency ?? 1);
6
+ return {
7
+ elapsedSeconds,
8
+ effectiveSeconds: boundedSeconds * efficiency,
9
+ capped,
10
+ };
11
+ }
12
+ export function linearCatchUp(seconds, input) {
13
+ const clampedSeconds = Math.max(0, seconds);
14
+ const value = input.current + input.ratePerSecond * clampedSeconds;
15
+ return Math.min(input.max ?? Number.POSITIVE_INFINITY, Math.max(input.min ?? Number.NEGATIVE_INFINITY, value));
16
+ }
17
+ export function exponentialCatchUp(seconds, input) {
18
+ const clampedSeconds = Math.max(0, seconds);
19
+ const factor = Math.max(0, input.factorPerSecond);
20
+ const value = input.current * factor ** clampedSeconds;
21
+ return Math.min(input.max ?? Number.POSITIVE_INFINITY, Math.max(input.min ?? Number.NEGATIVE_INFINITY, value));
22
+ }
23
+ export function steppedCatchUp(seconds, stepSeconds, step, maxSteps) {
24
+ if (stepSeconds <= 0 || seconds <= 0) {
25
+ return { steps: 0, remainderSeconds: Math.max(0, seconds) };
26
+ }
27
+ const steps = Math.min(Math.floor(seconds / stepSeconds), maxSteps ?? Number.POSITIVE_INFINITY);
28
+ for (let index = 0; index < steps; index++)
29
+ step(index);
30
+ return { steps, remainderSeconds: seconds - steps * stepSeconds };
31
+ }
@@ -9,8 +9,14 @@ export interface ServerTickPlan<TSystemId extends string = string> {
9
9
  due: TSystemId[];
10
10
  anchors: TickAnchors;
11
11
  }
12
+ export type PlanServerTickOptions = {
13
+ /** Cap catch-up runs per system per heartbeat to avoid spiral-of-death. Default 3. */
14
+ maxCatchUp?: number;
15
+ };
12
16
  /**
13
17
  * Decides which systems a heartbeat should run. A system with no anchor runs immediately.
18
+ * When wall time stalls past multiple intervals, the system id is repeated (bounded by
19
+ * maxCatchUp). Excess lag past the bound resyncs the anchor to `now`.
14
20
  * Returned anchors carry only systems present in the pipeline, so removed systems clean up.
15
21
  */
16
- export declare function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number): ServerTickPlan<TSystemId>;
22
+ export declare function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number, options?: PlanServerTickOptions): ServerTickPlan<TSystemId>;
@@ -1,18 +1,41 @@
1
+ const DEFAULT_MAX_CATCH_UP = 3;
1
2
  /**
2
3
  * Decides which systems a heartbeat should run. A system with no anchor runs immediately.
4
+ * When wall time stalls past multiple intervals, the system id is repeated (bounded by
5
+ * maxCatchUp). Excess lag past the bound resyncs the anchor to `now`.
3
6
  * Returned anchors carry only systems present in the pipeline, so removed systems clean up.
4
7
  */
5
- export function planServerTick(systems, anchors, now) {
8
+ export function planServerTick(systems, anchors, now, options) {
9
+ const maxCatchUp = options?.maxCatchUp ?? DEFAULT_MAX_CATCH_UP;
6
10
  const due = [];
7
11
  const nextAnchors = {};
8
12
  for (const system of systems) {
9
13
  const lastRunAt = anchors[system.id];
10
- if (lastRunAt !== undefined && now - lastRunAt < system.intervalMs) {
14
+ if (lastRunAt === undefined) {
15
+ due.push(system.id);
16
+ nextAnchors[system.id] = now;
17
+ continue;
18
+ }
19
+ if (system.intervalMs <= 0) {
20
+ due.push(system.id);
21
+ nextAnchors[system.id] = now;
22
+ continue;
23
+ }
24
+ const elapsed = now - lastRunAt;
25
+ if (elapsed < system.intervalMs) {
11
26
  nextAnchors[system.id] = lastRunAt;
12
27
  continue;
13
28
  }
14
- due.push(system.id);
15
- nextAnchors[system.id] = now;
29
+ const missed = Math.floor(elapsed / system.intervalMs);
30
+ if (missed > maxCatchUp) {
31
+ for (let i = 0; i < maxCatchUp; i += 1)
32
+ due.push(system.id);
33
+ nextAnchors[system.id] = now;
34
+ continue;
35
+ }
36
+ for (let i = 0; i < missed; i += 1)
37
+ due.push(system.id);
38
+ nextAnchors[system.id] = lastRunAt + missed * system.intervalMs;
16
39
  }
17
40
  return { due, anchors: nextAnchors };
18
41
  }
@@ -0,0 +1,57 @@
1
+ export interface SchedulePhase<T> {
2
+ state: T;
3
+ durationSeconds: number;
4
+ }
5
+ export interface StateScheduleConfig<T> {
6
+ phases: readonly SchedulePhase<T>[];
7
+ /** Repeat the phase list forever (default); `false` clamps to the last phase after one pass. */
8
+ loop?: boolean;
9
+ /** Shifts the whole timeline so `t = 0` lands `offsetSeconds` into the cycle — staggers identical schedules. */
10
+ offsetSeconds?: number;
11
+ }
12
+ export interface ScheduleSample<T> {
13
+ state: T;
14
+ /** Index into `phases`. */
15
+ index: number;
16
+ elapsedSeconds: number;
17
+ /** Seconds until this phase ends; `Infinity` on the clamped final phase of a non-looping schedule. */
18
+ remainingSeconds: number;
19
+ /** Elapsed fraction of the phase in `[0, 1]`; `1` on the clamped final phase. */
20
+ fraction: number;
21
+ }
22
+ export interface ScheduleWindow {
23
+ start: number;
24
+ end: number;
25
+ }
26
+ /**
27
+ * A deterministic timeline of states — position-in-schedule is a pure function of absolute time,
28
+ * so a forecast preview and the live state can never disagree. One primitive behind zoned weather
29
+ * shifts, hazard cycles, and timetabled machinery: `stateAt`/`sampleAt` for the live state,
30
+ * `nextTransitionAt` for countdowns, `windowsOf`/`nextWindow` for "when is it safe" forecasts.
31
+ */
32
+ export interface StateSchedule<T> {
33
+ /** Sum of all phase durations — one full cycle. */
34
+ readonly cycleSeconds: number;
35
+ stateAt(t: number): T;
36
+ sampleAt(t: number): ScheduleSample<T>;
37
+ /** Absolute time of the next phase boundary after `t`; `Infinity` once a non-looping schedule ends. */
38
+ nextTransitionAt(t: number): number;
39
+ /** All windows within `[fromT, fromT + horizonSeconds]` where the state matches. */
40
+ windowsOf(match: (state: T) => boolean, fromT: number, horizonSeconds: number): ScheduleWindow[];
41
+ /** The first matching window at or after `fromT` (looking ahead `horizonSeconds`, default one full cycle). */
42
+ nextWindow(match: (state: T) => boolean, fromT: number, horizonSeconds?: number): ScheduleWindow | null;
43
+ }
44
+ export declare function createStateSchedule<T>(config: StateScheduleConfig<T>): StateSchedule<T>;
45
+ export interface ClearWindowScan {
46
+ fromSeconds: number;
47
+ horizonSeconds: number;
48
+ /** Sampling resolution; smaller catches shorter gaps. */
49
+ stepSeconds: number;
50
+ /** Reject windows shorter than this; default `0`. */
51
+ minDurationSeconds?: number;
52
+ }
53
+ /**
54
+ * Forward-scan any predicate-of-time — a timetable mover's "is the crossing clear at `t`" — for the
55
+ * next open window. Sampling-based: pick `stepSeconds` at or below half the shortest gap that matters.
56
+ */
57
+ export declare function nextClearWindow(isClear: (t: number) => boolean, scan: ClearWindowScan): ScheduleWindow | null;
@@ -0,0 +1,121 @@
1
+ export function createStateSchedule(config) {
2
+ if (config.phases.length === 0)
3
+ throw new Error("state schedule needs at least one phase");
4
+ const phases = config.phases.map((phase) => {
5
+ if (!(phase.durationSeconds > 0)) {
6
+ throw new Error(`schedule phase duration must be positive, got ${phase.durationSeconds}`);
7
+ }
8
+ return phase;
9
+ });
10
+ const loop = config.loop ?? true;
11
+ const offset = config.offsetSeconds ?? 0;
12
+ const starts = [];
13
+ let cycleSeconds = 0;
14
+ for (const phase of phases) {
15
+ starts.push(cycleSeconds);
16
+ cycleSeconds += phase.durationSeconds;
17
+ }
18
+ function localTime(t) {
19
+ const shifted = t + offset;
20
+ if (loop)
21
+ return ((shifted % cycleSeconds) + cycleSeconds) % cycleSeconds;
22
+ return Math.max(0, shifted);
23
+ }
24
+ function sampleAt(t) {
25
+ const local = localTime(t);
26
+ if (!loop && local >= cycleSeconds) {
27
+ const index = phases.length - 1;
28
+ const phase = phases[index];
29
+ return {
30
+ state: phase.state,
31
+ index,
32
+ elapsedSeconds: local - starts[index],
33
+ remainingSeconds: Number.POSITIVE_INFINITY,
34
+ fraction: 1,
35
+ };
36
+ }
37
+ let index = phases.length - 1;
38
+ for (let i = 0; i < phases.length; i += 1) {
39
+ if (local < starts[i] + phases[i].durationSeconds) {
40
+ index = i;
41
+ break;
42
+ }
43
+ }
44
+ const phase = phases[index];
45
+ const elapsed = local - starts[index];
46
+ return {
47
+ state: phase.state,
48
+ index,
49
+ elapsedSeconds: elapsed,
50
+ remainingSeconds: phase.durationSeconds - elapsed,
51
+ fraction: Math.max(0, Math.min(1, elapsed / phase.durationSeconds)),
52
+ };
53
+ }
54
+ function nextTransitionAt(t) {
55
+ const sample = sampleAt(t);
56
+ if (!Number.isFinite(sample.remainingSeconds))
57
+ return Number.POSITIVE_INFINITY;
58
+ return t + sample.remainingSeconds;
59
+ }
60
+ function windowsOf(match, fromT, horizonSeconds) {
61
+ const windows = [];
62
+ const end = fromT + horizonSeconds;
63
+ let cursor = fromT;
64
+ let open = null;
65
+ let guard = phases.length * (Math.ceil(horizonSeconds / cycleSeconds) + 2) + 2;
66
+ while (cursor < end && guard > 0) {
67
+ guard -= 1;
68
+ const sample = sampleAt(cursor);
69
+ const matches = match(sample.state);
70
+ if (matches && open === null)
71
+ open = cursor;
72
+ const boundary = Number.isFinite(sample.remainingSeconds) ? cursor + sample.remainingSeconds : end;
73
+ const stop = Math.min(boundary, end);
74
+ if (!matches && open !== null) {
75
+ windows.push({ start: open, end: cursor });
76
+ open = null;
77
+ }
78
+ cursor = stop;
79
+ if (!Number.isFinite(sample.remainingSeconds))
80
+ break;
81
+ }
82
+ if (open !== null)
83
+ windows.push({ start: open, end: Math.min(cursor, end) });
84
+ return windows;
85
+ }
86
+ return {
87
+ cycleSeconds,
88
+ stateAt: (t) => sampleAt(t).state,
89
+ sampleAt,
90
+ nextTransitionAt,
91
+ windowsOf,
92
+ nextWindow(match, fromT, horizonSeconds = cycleSeconds) {
93
+ const windows = windowsOf(match, fromT, horizonSeconds);
94
+ return windows.length > 0 ? windows[0] : null;
95
+ },
96
+ };
97
+ }
98
+ /**
99
+ * Forward-scan any predicate-of-time — a timetable mover's "is the crossing clear at `t`" — for the
100
+ * next open window. Sampling-based: pick `stepSeconds` at or below half the shortest gap that matters.
101
+ */
102
+ export function nextClearWindow(isClear, scan) {
103
+ const minDuration = scan.minDurationSeconds ?? 0;
104
+ const end = scan.fromSeconds + scan.horizonSeconds;
105
+ let open = null;
106
+ for (let t = scan.fromSeconds; t <= end; t += scan.stepSeconds) {
107
+ if (isClear(t)) {
108
+ if (open === null)
109
+ open = t;
110
+ continue;
111
+ }
112
+ if (open !== null) {
113
+ if (t - open >= minDuration)
114
+ return { start: open, end: t };
115
+ open = null;
116
+ }
117
+ }
118
+ if (open !== null && end - open >= minDuration)
119
+ return { start: open, end };
120
+ return null;
121
+ }
@@ -1,4 +1,4 @@
1
- export type CommitMode = "immediate" | "simultaneous" | "rewind";
1
+ export type CommitMode = "immediate" | "simultaneous" | "resealable";
2
2
  export interface SubmittedAction<TAction> {
3
3
  participant: string;
4
4
  action: TAction;
@@ -21,7 +21,7 @@ export interface CommitController<TAction> {
21
21
  pending(): SubmittedAction<TAction>[];
22
22
  reveal(): SubmittedAction<TAction>[];
23
23
  commit(): SubmittedAction<TAction>[];
24
- rewind(): SubmittedAction<TAction>[];
24
+ discard(): SubmittedAction<TAction>[];
25
25
  clear(): void;
26
26
  }
27
27
  export declare function createCommitController<TAction>(config: CommitControllerConfig): CommitController<TAction>;
@@ -31,7 +31,7 @@ export function createCommitController(config) {
31
31
  if (mode === "immediate") {
32
32
  return { status: "committed", committed: [{ participant, action }] };
33
33
  }
34
- if (sealed.has(participant) && !(mode === "rewind")) {
34
+ if (sealed.has(participant) && mode !== "resealable") {
35
35
  return { status: "rejected", committed: [], reason: "already-submitted" };
36
36
  }
37
37
  sealed.set(participant, action);
@@ -62,7 +62,7 @@ export function createCommitController(config) {
62
62
  clear();
63
63
  return out;
64
64
  },
65
- rewind() {
65
+ discard() {
66
66
  const discarded = orderedSubmissions();
67
67
  clear();
68
68
  return discarded;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared mobile-composition geometry. Every UI subsystem (HUD, touch controls,
3
+ * system chrome, screens) registers the physical rectangle it occupies; the
4
+ * engine allocates the viewport once and detects when two subsystems collide,
5
+ * instead of each independently claiming an edge. Pure math — no DOM, no React.
6
+ */
7
+ import type { LayoutOrientation } from "./orientation.js";
8
+ /** The explicit composition mode a game renders for — not a scaled desktop layout. */
9
+ export type GameLayoutMode = "desktop-wide" | "desktop-compact" | "mobile-landscape" | "mobile-portrait";
10
+ /** Edge insets in CSS pixels (safe areas, reservations). */
11
+ export interface Insets {
12
+ top: number;
13
+ right: number;
14
+ bottom: number;
15
+ left: number;
16
+ }
17
+ /** All-zero insets. */
18
+ export declare const ZERO_INSETS: Insets;
19
+ /** Axis-aligned rectangle in CSS pixels (origin top-left). Structurally compatible with a `DOMRect`'s edge fields. */
20
+ export interface LayoutRect {
21
+ left: number;
22
+ top: number;
23
+ right: number;
24
+ bottom: number;
25
+ }
26
+ /** Width of a rect (clamped to ≥0). */
27
+ export declare function rectWidth(rect: LayoutRect): number;
28
+ /** Height of a rect (clamped to ≥0). */
29
+ export declare function rectHeight(rect: LayoutRect): number;
30
+ /** Area of a rect in px². */
31
+ export declare function rectArea(rect: LayoutRect): number;
32
+ /** Do the two rectangles share any interior area? Touching edges do not count. */
33
+ export declare function intersects(a: LayoutRect, b: LayoutRect): boolean;
34
+ /** Overlapping area in px²; 0 when the rectangles don't intersect. */
35
+ export declare function overlapArea(a: LayoutRect, b: LayoutRect): number;
36
+ /** Portrait when taller than wide, else landscape. */
37
+ export declare function orientationOf(width: number, height: number): LayoutOrientation;
38
+ /** Inputs to `resolveLayoutMode`. */
39
+ export interface LayoutModeInput {
40
+ /** Live visible viewport dimensions (prefer `visualViewport`). */
41
+ width: number;
42
+ height: number;
43
+ /** Touchscreen is the primary input (`(pointer: coarse)`). */
44
+ coarsePointer: boolean;
45
+ /** Does the game support phones at all (`platforms` includes `"mobile"`)? Default true. */
46
+ mobileSupported?: boolean;
47
+ /** Desktop width below which the layout is "compact". Default 900. */
48
+ compactWidth?: number;
49
+ }
50
+ /**
51
+ * Resolve the explicit composition mode. A mobile layout is not a scaled
52
+ * desktop layout: a coarse-pointer device on a game that supports mobile is
53
+ * always a `mobile-*` mode, split by live orientation; everything else is a
54
+ * desktop mode split by width.
55
+ */
56
+ export declare function resolveLayoutMode(input: LayoutModeInput): GameLayoutMode;
57
+ /** Whether a mode is one of the phone modes. */
58
+ export declare function isMobileMode(mode: GameLayoutMode): boolean;
59
+ /** What kind of UI a registered region belongs to. */
60
+ export type LayoutRegionKind = "hud" | "control" | "system" | "screen";
61
+ /** How a region participates in collision reporting. */
62
+ export type LayoutCollisionPolicy = "forbid" | "allow" | "warn";
63
+ /** Gameplay-importance tier of a HUD element. */
64
+ export type HudPriority = "critical" | "secondary" | "tertiary";
65
+ /** How a HUD element adapts on phones. */
66
+ export type MobileHudBehavior = "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal";
67
+ /** A physical rectangle a UI subsystem occupies, published to the shared registry. */
68
+ export interface LayoutRegion {
69
+ id: string;
70
+ kind: LayoutRegionKind;
71
+ rect: LayoutRect;
72
+ /** `forbid` reports any overlap, `warn` reports softly, `allow` opts this region out of collision reporting entirely. */
73
+ collisionPolicy: LayoutCollisionPolicy;
74
+ priority?: HudPriority;
75
+ /** Region ids this one may overlap without a report. */
76
+ allowOverlapWith?: readonly string[];
77
+ /** Regions sharing a non-empty group never report against each other. */
78
+ collisionGroup?: string;
79
+ }
80
+ /** One detected forbidden/warned overlap between two regions. */
81
+ export interface LayoutCollision {
82
+ a: string;
83
+ b: string;
84
+ kindA: LayoutRegionKind;
85
+ kindB: LayoutRegionKind;
86
+ /** Overlapping area in px². */
87
+ area: number;
88
+ severity: "forbid" | "warn";
89
+ }
90
+ /** Options for `detectLayoutCollisions`. */
91
+ export interface DetectCollisionsOptions {
92
+ /** Ignore overlaps smaller than this many px² — avoids meaningless sub-pixel warnings. Default 64. */
93
+ minArea?: number;
94
+ }
95
+ /**
96
+ * Every forbidden or warned rectangle intersection across the registered
97
+ * regions. A pair is skipped when either side's policy is `allow`, when either
98
+ * opts into the other via `allowOverlapWith`, when they share a
99
+ * `collisionGroup`, or when the overlap is below `minArea`.
100
+ */
101
+ export declare function detectLayoutCollisions(regions: readonly LayoutRegion[], options?: DetectCollisionsOptions): LayoutCollision[];
102
+ /** Human-readable diagnostic block for a set of collisions (empty string when there are none). */
103
+ export declare function formatLayoutCollisions(collisions: readonly LayoutCollision[]): string;
104
+ /**
105
+ * The usable gameplay rectangle after reserving physical zones for touch
106
+ * controls and system UI. Starts from the safe-area-inset viewport and carves
107
+ * each reserved zone from whichever edge it hugs.
108
+ */
109
+ export declare function computeGameplayRect(viewport: LayoutRect, safeArea: Insets, reserved: readonly LayoutRect[]): LayoutRect;
110
+ /** The shared live geometry the engine allocates once and every UI subsystem reads. */
111
+ export interface GameViewportLayout {
112
+ /** The layout (CSS-pixel) viewport. */
113
+ viewport: LayoutRect;
114
+ /** The live visible viewport (`window.visualViewport`), which excludes mobile browser chrome. */
115
+ visualViewport: LayoutRect;
116
+ safeArea: Insets;
117
+ mode: GameLayoutMode;
118
+ orientation: LayoutOrientation;
119
+ /** All registered regions with their live rects. */
120
+ regions: readonly LayoutRegion[];
121
+ /** Rects reserved by touch controls and system UI. */
122
+ controlZones: readonly LayoutRect[];
123
+ /** Usable world/gameplay area after reservations. */
124
+ gameplayRect: LayoutRect;
125
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Shared mobile-composition geometry. Every UI subsystem (HUD, touch controls,
3
+ * system chrome, screens) registers the physical rectangle it occupies; the
4
+ * engine allocates the viewport once and detects when two subsystems collide,
5
+ * instead of each independently claiming an edge. Pure math — no DOM, no React.
6
+ */
7
+ /** All-zero insets. */
8
+ export const ZERO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
9
+ /** Width of a rect (clamped to ≥0). */
10
+ export function rectWidth(rect) {
11
+ return Math.max(0, rect.right - rect.left);
12
+ }
13
+ /** Height of a rect (clamped to ≥0). */
14
+ export function rectHeight(rect) {
15
+ return Math.max(0, rect.bottom - rect.top);
16
+ }
17
+ /** Area of a rect in px². */
18
+ export function rectArea(rect) {
19
+ return rectWidth(rect) * rectHeight(rect);
20
+ }
21
+ /** Do the two rectangles share any interior area? Touching edges do not count. */
22
+ export function intersects(a, b) {
23
+ return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
24
+ }
25
+ /** Overlapping area in px²; 0 when the rectangles don't intersect. */
26
+ export function overlapArea(a, b) {
27
+ const w = Math.min(a.right, b.right) - Math.max(a.left, b.left);
28
+ const h = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
29
+ return w > 0 && h > 0 ? w * h : 0;
30
+ }
31
+ /** Portrait when taller than wide, else landscape. */
32
+ export function orientationOf(width, height) {
33
+ return height > width ? "portrait" : "landscape";
34
+ }
35
+ const DEFAULT_COMPACT_WIDTH = 900;
36
+ /**
37
+ * Resolve the explicit composition mode. A mobile layout is not a scaled
38
+ * desktop layout: a coarse-pointer device on a game that supports mobile is
39
+ * always a `mobile-*` mode, split by live orientation; everything else is a
40
+ * desktop mode split by width.
41
+ */
42
+ export function resolveLayoutMode(input) {
43
+ const orientation = orientationOf(input.width, input.height);
44
+ const mobile = input.coarsePointer && (input.mobileSupported ?? true);
45
+ if (mobile)
46
+ return orientation === "portrait" ? "mobile-portrait" : "mobile-landscape";
47
+ const compactWidth = input.compactWidth ?? DEFAULT_COMPACT_WIDTH;
48
+ return input.width < compactWidth ? "desktop-compact" : "desktop-wide";
49
+ }
50
+ /** Whether a mode is one of the phone modes. */
51
+ export function isMobileMode(mode) {
52
+ return mode === "mobile-portrait" || mode === "mobile-landscape";
53
+ }
54
+ function allowsOverlap(a, b) {
55
+ if (a.allowOverlapWith?.includes(b.id) === true)
56
+ return true;
57
+ if (b.allowOverlapWith?.includes(a.id) === true)
58
+ return true;
59
+ if (a.collisionGroup !== undefined && a.collisionGroup === b.collisionGroup)
60
+ return true;
61
+ return false;
62
+ }
63
+ const DEFAULT_MIN_AREA = 64;
64
+ /**
65
+ * Every forbidden or warned rectangle intersection across the registered
66
+ * regions. A pair is skipped when either side's policy is `allow`, when either
67
+ * opts into the other via `allowOverlapWith`, when they share a
68
+ * `collisionGroup`, or when the overlap is below `minArea`.
69
+ */
70
+ export function detectLayoutCollisions(regions, options) {
71
+ const minArea = options?.minArea ?? DEFAULT_MIN_AREA;
72
+ const out = [];
73
+ for (let i = 0; i < regions.length; i += 1) {
74
+ for (let j = i + 1; j < regions.length; j += 1) {
75
+ const a = regions[i];
76
+ const b = regions[j];
77
+ if (a.collisionPolicy === "allow" || b.collisionPolicy === "allow")
78
+ continue;
79
+ if (allowsOverlap(a, b))
80
+ continue;
81
+ const area = overlapArea(a.rect, b.rect);
82
+ if (area < minArea)
83
+ continue;
84
+ const severity = a.collisionPolicy === "forbid" || b.collisionPolicy === "forbid" ? "forbid" : "warn";
85
+ out.push({ a: a.id, b: b.id, kindA: a.kind, kindB: b.kind, area: Math.round(area), severity });
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function withThousands(n) {
91
+ return Math.round(n)
92
+ .toString()
93
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
94
+ }
95
+ /** Human-readable diagnostic block for a set of collisions (empty string when there are none). */
96
+ export function formatLayoutCollisions(collisions) {
97
+ if (collisions.length === 0)
98
+ return "";
99
+ const lines = collisions.map((c) => `${c.a} intersects ${c.b} by ${withThousands(c.area)} px²`);
100
+ return ["JGENGINE MOBILE LAYOUT COLLISION", ...lines].join("\n");
101
+ }
102
+ /**
103
+ * The usable gameplay rectangle after reserving physical zones for touch
104
+ * controls and system UI. Starts from the safe-area-inset viewport and carves
105
+ * each reserved zone from whichever edge it hugs.
106
+ */
107
+ export function computeGameplayRect(viewport, safeArea, reserved) {
108
+ let top = viewport.top + safeArea.top;
109
+ let bottom = viewport.bottom - safeArea.bottom;
110
+ let left = viewport.left + safeArea.left;
111
+ let right = viewport.right - safeArea.right;
112
+ const midX = (viewport.left + viewport.right) / 2;
113
+ const midY = (viewport.top + viewport.bottom) / 2;
114
+ for (const zone of reserved) {
115
+ const zoneMidX = (zone.left + zone.right) / 2;
116
+ const zoneMidY = (zone.top + zone.bottom) / 2;
117
+ const spanX = zone.right - zone.left;
118
+ const spanY = zone.bottom - zone.top;
119
+ if (zoneMidY > midY)
120
+ bottom = Math.min(bottom, zone.top);
121
+ else if (zoneMidY < midY && spanX >= spanY)
122
+ top = Math.max(top, zone.bottom);
123
+ else if (zoneMidX < midX)
124
+ left = Math.max(left, zone.right);
125
+ else
126
+ right = Math.min(right, zone.left);
127
+ }
128
+ return { left, top, right: Math.max(left, right), bottom: Math.max(top, bottom) };
129
+ }
@@ -0,0 +1,58 @@
1
+ export type HudAnchor = "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right";
2
+ export interface HudPlacement {
3
+ anchor: HudAnchor;
4
+ dx: number;
5
+ dy: number;
6
+ }
7
+ export interface HudRect {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ export interface HudSize {
14
+ width: number;
15
+ height: number;
16
+ }
17
+ export interface HudPanelState {
18
+ id: string;
19
+ placement: HudPlacement;
20
+ z: number;
21
+ moved: boolean;
22
+ }
23
+ export interface HudLayoutState {
24
+ panels: Record<string, HudPanelState>;
25
+ locked: boolean;
26
+ editing: boolean;
27
+ }
28
+ export interface HudLayoutOptions {
29
+ snap?: number;
30
+ locked?: boolean;
31
+ }
32
+ export declare function isPanelDraggable(state: HudLayoutState, id: string): boolean;
33
+ export interface HudLayoutStore {
34
+ getState(): HudLayoutState;
35
+ subscribe(listener: (state: HudLayoutState) => void): () => void;
36
+ register(id: string, placement: HudPlacement): void;
37
+ move(id: string, rect: HudRect, viewport: HudSize): void;
38
+ bringToFront(id: string): void;
39
+ setLocked(locked: boolean): void;
40
+ setEditing(editing: boolean): void;
41
+ reset(id?: string): void;
42
+ serialize(): string;
43
+ hydrate(raw: string | null | undefined): void;
44
+ }
45
+ export declare const HUD_ANCHOR_FRACTIONS: Record<HudAnchor, {
46
+ fx: number;
47
+ fy: number;
48
+ }>;
49
+ export declare function isHudAnchor(value: unknown): value is HudAnchor;
50
+ export declare function anchoredPlacement(anchor: HudAnchor, inset: {
51
+ x: number;
52
+ y: number;
53
+ }): HudPlacement;
54
+ export declare function nearestAnchor(rect: HudRect, viewport: HudSize): HudAnchor;
55
+ export declare function clampRect(rect: HudRect, viewport: HudSize): HudRect;
56
+ export declare function placementFromRect(rect: HudRect, viewport: HudSize, snap?: number): HudPlacement;
57
+ export declare function rectFromPlacement(placement: HudPlacement, size: HudSize, viewport: HudSize): HudRect;
58
+ export declare function createHudLayout(options?: HudLayoutOptions): HudLayoutStore;