@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 type VisionPoint = readonly [number, number];
2
+ /** Structurally matches `world/walls` `WallSegment` — pass those straight in as occluders. */
3
+ export interface VisionWall {
4
+ from: VisionPoint;
5
+ to: VisionPoint;
6
+ }
7
+ export interface VisionConeConfig {
8
+ range: number;
9
+ /** Full cone opening angle in radians (matches `TelegraphShape`'s cone `angle`). */
10
+ angle: number;
11
+ }
12
+ /** 2D segment intersection on the XZ plane (collinear overlaps count) — the LoS building block `revealQuery`/`frustumSensor` never shipped (#286.5). */
13
+ export declare function segmentsIntersect(a1: VisionPoint, a2: VisionPoint, b1: VisionPoint, b2: VisionPoint): boolean;
14
+ /** True when no wall segment crosses the sight line. */
15
+ export declare function hasWallLineOfSight(from: VisionPoint, to: VisionPoint, walls: readonly VisionWall[]): boolean;
16
+ /** Angle + range test only — no occlusion. `heading` is engine yaw (radians, `atan2(dx, dz)`). */
17
+ export declare function pointInCone(origin: VisionPoint, heading: number, config: VisionConeConfig, point: VisionPoint): boolean;
18
+ export interface VisionTarget<TId extends string = string> {
19
+ id: TId;
20
+ at: VisionPoint;
21
+ }
22
+ /**
23
+ * The guard-sight primitive every stealth game hand-rolled: an angle+range cone on the XZ plane
24
+ * whose sight lines are blocked by 2D wall segments (`world/walls` segments fit structurally).
25
+ * Pure math — feed entity positions and heading each tick.
26
+ */
27
+ export interface VisionCone {
28
+ canSee(origin: VisionPoint, heading: number, target: VisionPoint): boolean;
29
+ visibleTargets<TId extends string>(origin: VisionPoint, heading: number, targets: readonly VisionTarget<TId>[]): TId[];
30
+ }
31
+ export declare function createVisionCone(config: VisionConeConfig, walls?: readonly VisionWall[]): VisionCone;
@@ -0,0 +1,72 @@
1
+ function orientation(ax, az, bx, bz, cx, cz) {
2
+ const value = (bz - az) * (cx - bx) - (bx - ax) * (cz - bz);
3
+ if (value > 1e-9)
4
+ return 1;
5
+ if (value < -1e-9)
6
+ return -1;
7
+ return 0;
8
+ }
9
+ function onSegment(ax, az, bx, bz, cx, cz) {
10
+ return (Math.min(ax, bx) - 1e-9 <= cx &&
11
+ cx <= Math.max(ax, bx) + 1e-9 &&
12
+ Math.min(az, bz) - 1e-9 <= cz &&
13
+ cz <= Math.max(az, bz) + 1e-9);
14
+ }
15
+ /** 2D segment intersection on the XZ plane (collinear overlaps count) — the LoS building block `revealQuery`/`frustumSensor` never shipped (#286.5). */
16
+ export function segmentsIntersect(a1, a2, b1, b2) {
17
+ const o1 = orientation(a1[0], a1[1], a2[0], a2[1], b1[0], b1[1]);
18
+ const o2 = orientation(a1[0], a1[1], a2[0], a2[1], b2[0], b2[1]);
19
+ const o3 = orientation(b1[0], b1[1], b2[0], b2[1], a1[0], a1[1]);
20
+ const o4 = orientation(b1[0], b1[1], b2[0], b2[1], a2[0], a2[1]);
21
+ if (o1 !== o2 && o3 !== o4)
22
+ return true;
23
+ if (o1 === 0 && onSegment(a1[0], a1[1], a2[0], a2[1], b1[0], b1[1]))
24
+ return true;
25
+ if (o2 === 0 && onSegment(a1[0], a1[1], a2[0], a2[1], b2[0], b2[1]))
26
+ return true;
27
+ if (o3 === 0 && onSegment(b1[0], b1[1], b2[0], b2[1], a1[0], a1[1]))
28
+ return true;
29
+ if (o4 === 0 && onSegment(b1[0], b1[1], b2[0], b2[1], a2[0], a2[1]))
30
+ return true;
31
+ return false;
32
+ }
33
+ /** True when no wall segment crosses the sight line. */
34
+ export function hasWallLineOfSight(from, to, walls) {
35
+ for (const wall of walls) {
36
+ if (segmentsIntersect(from, to, wall.from, wall.to))
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ /** Angle + range test only — no occlusion. `heading` is engine yaw (radians, `atan2(dx, dz)`). */
42
+ export function pointInCone(origin, heading, config, point) {
43
+ const dx = point[0] - origin[0];
44
+ const dz = point[1] - origin[1];
45
+ const distance = Math.hypot(dx, dz);
46
+ if (distance > config.range)
47
+ return false;
48
+ if (distance < 1e-9)
49
+ return true;
50
+ let delta = Math.atan2(dx, dz) - heading;
51
+ while (delta > Math.PI)
52
+ delta -= Math.PI * 2;
53
+ while (delta < -Math.PI)
54
+ delta += Math.PI * 2;
55
+ return Math.abs(delta) <= config.angle / 2;
56
+ }
57
+ export function createVisionCone(config, walls = []) {
58
+ function canSee(origin, heading, target) {
59
+ return pointInCone(origin, heading, config, target) && hasWallLineOfSight(origin, target, walls);
60
+ }
61
+ return {
62
+ canSee,
63
+ visibleTargets(origin, heading, targets) {
64
+ const seen = [];
65
+ for (const target of targets) {
66
+ if (canSee(origin, heading, target.at))
67
+ seen.push(target.id);
68
+ }
69
+ return seen;
70
+ },
71
+ };
72
+ }
@@ -38,11 +38,11 @@ export interface RaidPlayerSnapshot {
38
38
  remaining: number;
39
39
  }
40
40
  export interface RaidSession {
41
- beginExtract(userId: string, extractId: string, team?: string): ContestedEvent | null;
41
+ beginExtract(userId: string, extractId: string, position: RingPoint, team?: string): ContestedEvent | null;
42
42
  tickExtract(userId: string, dt: number, occupants?: Record<string, number>): ContestedEvent[];
43
43
  damage(userId: string, reason?: string): ContestedEvent | null;
44
44
  cancelExtract(userId: string): void;
45
- resolveExtraction(userId: string, containers: readonly ContainerSnapshot[]): ExtractionResult;
45
+ resolveExtraction(userId: string, containers: readonly ContainerSnapshot[]): ExtractionResult | null;
46
46
  resolveDeath(userId: string, containers: readonly ContainerSnapshot[], now: number, rng?: () => number): DeathResult;
47
47
  claimDeliveries(now: number): readonly DeliveryEntry[];
48
48
  pendingDeliveries(userId?: string): readonly DeliveryEntry[];
@@ -1,5 +1,10 @@
1
1
  import { createDeliveryQueue, insureLost, partitionOnDeath, resolveConsolation, } from "../inventory/storageTier.js";
2
2
  import { createContestedChannel, } from "./contestedChannel.js";
3
+ function inExtractZone(point, position) {
4
+ const dx = position[0] - point.center[0];
5
+ const dz = position[1] - point.center[1];
6
+ return dx * dx + dz * dz <= point.radius * point.radius;
7
+ }
3
8
  function mergeAll(containers) {
4
9
  const totals = new Map();
5
10
  for (const container of containers) {
@@ -24,12 +29,14 @@ export function createRaidSession(config) {
24
29
  return statuses.get(userId) ?? "in-raid";
25
30
  }
26
31
  return {
27
- beginExtract(userId, extractId, team) {
32
+ beginExtract(userId, extractId, position, team) {
28
33
  const point = points.get(extractId);
29
34
  if (point === undefined)
30
35
  return null;
31
36
  if (statusOf(userId) === "extracted" || statusOf(userId) === "dead")
32
37
  return null;
38
+ if (!inExtractZone(point, position))
39
+ return null;
33
40
  const channel = createContestedChannel({
34
41
  duration: point.holdSeconds,
35
42
  interruptOnDamage: point.interruptOnDamage ?? true,
@@ -65,7 +72,11 @@ export function createRaidSession(config) {
65
72
  },
66
73
  resolveExtraction(userId, containers) {
67
74
  const attempt = attempts.get(userId);
68
- const extractId = attempt?.extractId ?? "";
75
+ if (attempt === undefined)
76
+ return null;
77
+ if (attempt.channel.phase() !== "complete" && statusOf(userId) !== "extracted")
78
+ return null;
79
+ const extractId = attempt.extractId;
69
80
  attempts.delete(userId);
70
81
  statuses.set(userId, "extracted");
71
82
  return { userId, extractId, banked: mergeAll(containers) };
@@ -0,0 +1,97 @@
1
+ import type { GameContext } from "../runtime/gameContext.js";
2
+ export type SettingValue = number | boolean | string;
3
+ export type SettingKind = "slider" | "toggle" | "select";
4
+ export type BuiltInSettingCategory = "sound" | "graphics" | "gameplay" | "controls";
5
+ /** Built-in category ids keep autocomplete; any other string makes a fresh category. */
6
+ export type SettingCategory = BuiltInSettingCategory | (string & {});
7
+ export declare const BUILT_IN_SETTING_CATEGORIES: readonly BuiltInSettingCategory[];
8
+ export interface SettingOption {
9
+ value: string;
10
+ label: string;
11
+ }
12
+ /** Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins. */
13
+ export interface SettingCategoryDef {
14
+ id: SettingCategory;
15
+ label: string;
16
+ order?: number;
17
+ }
18
+ /** Extra setting a game appends to a built-in category via `defineGame({ settings: { extra } })`. */
19
+ export interface GameSettingDef {
20
+ id: string;
21
+ label: string;
22
+ category: SettingCategory;
23
+ kind: SettingKind;
24
+ default: SettingValue;
25
+ /** Slider bounds — always pass both for a `slider`; omitted defaults (0..1) collapse the thumb. */
26
+ min?: number;
27
+ max?: number;
28
+ step?: number;
29
+ options?: readonly SettingOption[];
30
+ /** Applies the new value to game state; the value is already persisted before this runs. */
31
+ onChange?: (value: SettingValue, ctx: GameContext) => void;
32
+ }
33
+ /** `quick` shows compact on-screen volume/graphics buttons; `false` (default) mounts no engine trigger — open the menu from your own UI with `<SettingsTrigger>` or `useSettings().open()`. */
34
+ export type SettingsSurface = "quick";
35
+ /** The four themed settings layouts, chosen with `defineGame({ settings: { variant } })`. All read the game's `--jg-*` theme tokens. */
36
+ export type SettingsVariant = "panel" | "sheet" | "sidebar" | "fullscreen";
37
+ /** A game-state action (Restart, Quit to menu, …) shown as rows in the first "Game" settings tab — never a floating button or a rebindable key. */
38
+ export interface SettingsActionDef {
39
+ id: string;
40
+ label: string;
41
+ /** `danger` styles destructive actions (restart, quit) apart from neutral ones. */
42
+ kind?: "default" | "danger";
43
+ /** Optional one-line hint under the label. */
44
+ description?: string;
45
+ /** Runs the action; the menu closes right after. */
46
+ run: (ctx: GameContext) => void;
47
+ }
48
+ export interface GameSettingsConfig {
49
+ /** Layout + skin of the menu; all four theme off `--jg-*`. Default `panel`. */
50
+ variant?: SettingsVariant;
51
+ /** `quick` shows compact on-screen volume/graphics buttons; omitted mounts no engine trigger — open the menu yourself with `<SettingsTrigger>` / `useSettings().open()`, or render `useSettings().categories` your own way. */
52
+ surface?: SettingsSurface | false;
53
+ /** Rows appended to any category — built-in or a brand-new one named by `category`. */
54
+ extra?: readonly GameSettingDef[];
55
+ /** Declare custom category tabs, or relabel/reorder the built-ins. */
56
+ categories?: readonly SettingCategoryDef[];
57
+ /** Built-in categories to hide. */
58
+ hide?: readonly SettingCategory[];
59
+ /** Game-state actions (Restart, Quit…) — become the first "Game" tab, shown before anything else. */
60
+ actions?: readonly SettingsActionDef[];
61
+ /** Input actions to drop from the rebindable Controls list — game-state keys like `restart` that belong in `actions`, not the rebind grid. */
62
+ hideBindings?: readonly string[];
63
+ }
64
+ export declare const SETTINGS_STORAGE_PREFIX = "jgengine:setting:";
65
+ export declare const SETTING_IDS: {
66
+ readonly masterVolume: "sound.master";
67
+ readonly graphicsQuality: "graphics.quality";
68
+ readonly graphicsShadows: "graphics.shadows";
69
+ readonly graphicsUiScale: "graphics.uiScale";
70
+ };
71
+ export type GraphicsQuality = "low" | "medium" | "high";
72
+ export declare const GRAPHICS_QUALITY_OPTIONS: readonly SettingOption[];
73
+ export declare const DEFAULT_MASTER_VOLUME = 1;
74
+ export declare const DEFAULT_GRAPHICS_QUALITY: GraphicsQuality;
75
+ export declare const DEFAULT_GRAPHICS_SHADOWS = true;
76
+ /** Player-controlled multiplier on the HUD's computed fit scale — one lever on desktop and mobile alike. */
77
+ export declare const DEFAULT_UI_SCALE = 1;
78
+ export declare const UI_SCALE_MIN = 0.5;
79
+ export declare const UI_SCALE_MAX = 1.5;
80
+ /** Device-pixel-ratio ceiling per quality tier — the shell's `Canvas` dpr cap. */
81
+ export declare const GRAPHICS_QUALITY_DPR: Record<GraphicsQuality, number>;
82
+ export declare function busVolumeSettingId(busId: string): string;
83
+ export declare function settingStorageKey(id: string): string;
84
+ interface WebStorageLike {
85
+ getItem(key: string): string | null;
86
+ setItem(key: string, value: string): void;
87
+ }
88
+ export declare function loadSettingValue<T extends SettingValue>(id: string, fallback: T, storage?: Pick<WebStorageLike, "getItem"> | null | undefined): T;
89
+ export declare function saveSettingValue(id: string, value: SettingValue, storage?: Pick<WebStorageLike, "setItem"> | null | undefined): void;
90
+ export interface SettingsStore {
91
+ get<T extends SettingValue>(id: string, fallback: T): T;
92
+ set(id: string, value: SettingValue): void;
93
+ subscribe(listener: () => void): () => void;
94
+ }
95
+ /** Reactive, localStorage-backed settings store shared by the shell wiring and React hooks. */
96
+ export declare function createSettingsStore(storage?: Pick<WebStorageLike, "getItem" | "setItem"> | null | undefined): SettingsStore;
97
+ export {};
@@ -0,0 +1,100 @@
1
+ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ export const BUILT_IN_SETTING_CATEGORIES = [
3
+ "sound",
4
+ "graphics",
5
+ "gameplay",
6
+ "controls",
7
+ ];
8
+ export const SETTINGS_STORAGE_PREFIX = "jgengine:setting:";
9
+ export const SETTING_IDS = {
10
+ masterVolume: "sound.master",
11
+ graphicsQuality: "graphics.quality",
12
+ graphicsShadows: "graphics.shadows",
13
+ graphicsUiScale: "graphics.uiScale",
14
+ };
15
+ export const GRAPHICS_QUALITY_OPTIONS = [
16
+ { value: "low", label: "Low" },
17
+ { value: "medium", label: "Medium" },
18
+ { value: "high", label: "High" },
19
+ ];
20
+ export const DEFAULT_MASTER_VOLUME = 1;
21
+ export const DEFAULT_GRAPHICS_QUALITY = "high";
22
+ export const DEFAULT_GRAPHICS_SHADOWS = true;
23
+ /** Player-controlled multiplier on the HUD's computed fit scale — one lever on desktop and mobile alike. */
24
+ export const DEFAULT_UI_SCALE = 1;
25
+ export const UI_SCALE_MIN = 0.5;
26
+ export const UI_SCALE_MAX = 1.5;
27
+ /** Device-pixel-ratio ceiling per quality tier — the shell's `Canvas` dpr cap. */
28
+ export const GRAPHICS_QUALITY_DPR = {
29
+ low: 1,
30
+ medium: 1.5,
31
+ high: 2,
32
+ };
33
+ export function busVolumeSettingId(busId) {
34
+ return `sound.bus.${busId}`;
35
+ }
36
+ export function settingStorageKey(id) {
37
+ return `${SETTINGS_STORAGE_PREFIX}${id}`;
38
+ }
39
+ function defaultStorage() {
40
+ try {
41
+ if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
42
+ return globalThis.localStorage ?? null;
43
+ }
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ return null;
49
+ }
50
+ function coerce(raw, fallback) {
51
+ if (typeof fallback === "number") {
52
+ const n = typeof raw === "number" ? raw : Number(raw);
53
+ return (Number.isFinite(n) ? n : fallback);
54
+ }
55
+ if (typeof fallback === "boolean") {
56
+ if (typeof raw === "boolean")
57
+ return raw;
58
+ return (raw === "true" ? true : raw === "false" ? false : fallback);
59
+ }
60
+ return (typeof raw === "string" ? raw : fallback);
61
+ }
62
+ export function loadSettingValue(id, fallback, storage = defaultStorage()) {
63
+ if (storage == null)
64
+ return fallback;
65
+ try {
66
+ const raw = storage.getItem(settingStorageKey(id));
67
+ if (raw === null)
68
+ return fallback;
69
+ return coerce(JSON.parse(raw), fallback);
70
+ }
71
+ catch {
72
+ return fallback;
73
+ }
74
+ }
75
+ export function saveSettingValue(id, value, storage = defaultStorage()) {
76
+ if (storage == null)
77
+ return;
78
+ try {
79
+ storage.setItem(settingStorageKey(id), JSON.stringify(value));
80
+ }
81
+ catch {
82
+ /* ignore quota / private-mode failures */
83
+ }
84
+ }
85
+ /** Reactive, localStorage-backed settings store shared by the shell wiring and React hooks. */
86
+ export function createSettingsStore(storage = defaultStorage()) {
87
+ const mem = createObservableKeyedStore();
88
+ return {
89
+ get(id, fallback) {
90
+ if (mem.has(id))
91
+ return coerce(mem.get(id), fallback);
92
+ return loadSettingValue(id, fallback, storage ?? undefined);
93
+ },
94
+ set(id, value) {
95
+ saveSettingValue(id, value, storage ?? undefined);
96
+ mem.set(id, value);
97
+ },
98
+ subscribe: mem.subscribe,
99
+ };
100
+ }
@@ -3,6 +3,9 @@ export interface StatModifier {
3
3
  multiply?: number;
4
4
  }
5
5
  export type StatModifierSet<TStat extends string> = Partial<Record<TStat, StatModifier>>;
6
+ export interface CreateStatsOptions {
7
+ now?: () => number;
8
+ }
6
9
  export interface Stats<TStat extends string> {
7
10
  setBase(stat: TStat, value: number): void;
8
11
  getBase(stat: TStat): number;
@@ -10,9 +13,9 @@ export interface Stats<TStat extends string> {
10
13
  expiresAtMs?: number;
11
14
  }): void;
12
15
  removeSource(sourceId: string): void;
13
- hasSource(sourceId: string): boolean;
16
+ hasSource(sourceId: string, nowMs?: number): boolean;
14
17
  get(stat: TStat, nowMs?: number): number;
15
18
  pruneExpired(nowMs: number): string[];
16
19
  sources(): string[];
17
20
  }
18
- export declare function createStats<TStat extends string>(base: Record<TStat, number>): Stats<TStat>;
21
+ export declare function createStats<TStat extends string>(base: Record<TStat, number>, options?: CreateStatsOptions): Stats<TStat>;
@@ -1,9 +1,23 @@
1
- export function createStats(base) {
1
+ export function createStats(base, options) {
2
2
  const baseValues = { ...base };
3
3
  const sourceEntries = new Map();
4
+ const clock = options?.now;
5
+ function resolveNow(nowMs) {
6
+ return nowMs ?? clock?.();
7
+ }
4
8
  function isExpired(entry, nowMs) {
5
9
  return nowMs !== undefined && entry.expiresAtMs !== undefined && entry.expiresAtMs <= nowMs;
6
10
  }
11
+ function dropExpired(nowMs) {
12
+ const pruned = [];
13
+ for (const [sourceId, entry] of sourceEntries) {
14
+ if (isExpired(entry, nowMs)) {
15
+ sourceEntries.delete(sourceId);
16
+ pruned.push(sourceId);
17
+ }
18
+ }
19
+ return pruned;
20
+ }
7
21
  function resolve(stat, nowMs) {
8
22
  let total = baseValues[stat];
9
23
  let multiplier = 1;
@@ -27,27 +41,31 @@ export function createStats(base) {
27
41
  getBase(stat) {
28
42
  return baseValues[stat];
29
43
  },
30
- addSource(sourceId, modifiers, options) {
31
- sourceEntries.set(sourceId, { modifiers, expiresAtMs: options?.expiresAtMs });
44
+ addSource(sourceId, modifiers, sourceOptions) {
45
+ sourceEntries.set(sourceId, { modifiers, expiresAtMs: sourceOptions?.expiresAtMs });
32
46
  },
33
47
  removeSource(sourceId) {
34
48
  sourceEntries.delete(sourceId);
35
49
  },
36
- hasSource(sourceId) {
37
- return sourceEntries.has(sourceId);
50
+ hasSource(sourceId, nowMs) {
51
+ const entry = sourceEntries.get(sourceId);
52
+ if (entry === undefined)
53
+ return false;
54
+ const time = resolveNow(nowMs);
55
+ if (isExpired(entry, time)) {
56
+ sourceEntries.delete(sourceId);
57
+ return false;
58
+ }
59
+ return true;
38
60
  },
39
61
  get(stat, nowMs) {
40
- return resolve(stat, nowMs);
62
+ const time = resolveNow(nowMs);
63
+ if (time !== undefined)
64
+ dropExpired(time);
65
+ return resolve(stat, time);
41
66
  },
42
67
  pruneExpired(nowMs) {
43
- const pruned = [];
44
- for (const [sourceId, entry] of sourceEntries) {
45
- if (isExpired(entry, nowMs)) {
46
- sourceEntries.delete(sourceId);
47
- pruned.push(sourceId);
48
- }
49
- }
50
- return pruned;
68
+ return dropExpired(nowMs);
51
69
  },
52
70
  sources() {
53
71
  return Array.from(sourceEntries.keys());
@@ -6,5 +6,9 @@ export interface ObservableKeyedStore<T> {
6
6
  subscribe(listener: () => void): () => void;
7
7
  mapSnapshot(): ReadonlyMap<string, T>;
8
8
  arraySnapshot(): readonly T[];
9
+ /** Serializable `[key, value]` entries — the transport counterpart of {@link hydrate} for host→client mirroring. */
10
+ snapshot(): readonly (readonly [string, T])[];
11
+ /** Replace all entries with `data` (clears keys absent from it), emitting once. */
12
+ hydrate(data: readonly (readonly [string, T])[]): void;
9
13
  }
10
14
  export declare function createObservableKeyedStore<T>(areEqual?: (previous: T, next: T) => boolean): ObservableKeyedStore<T>;
@@ -3,8 +3,9 @@ export function createObservableKeyedStore(areEqual) {
3
3
  const listeners = new Set();
4
4
  const EMPTY = [];
5
5
  let arrayCache = EMPTY;
6
+ let arrayDirty = false;
6
7
  function emit() {
7
- arrayCache = store.size === 0 ? EMPTY : Array.from(store.values());
8
+ arrayDirty = true;
8
9
  for (const listener of listeners)
9
10
  listener();
10
11
  }
@@ -13,7 +14,8 @@ export function createObservableKeyedStore(areEqual) {
13
14
  const previous = store.get(key);
14
15
  if (previous !== undefined && areEqual?.(previous, value))
15
16
  return;
16
- store.set(key, value);
17
+ if (previous !== value)
18
+ store.set(key, value);
17
19
  emit();
18
20
  },
19
21
  delete(key) {
@@ -36,7 +38,20 @@ export function createObservableKeyedStore(areEqual) {
36
38
  return store;
37
39
  },
38
40
  arraySnapshot() {
41
+ if (arrayDirty) {
42
+ arrayCache = store.size === 0 ? EMPTY : Array.from(store.values());
43
+ arrayDirty = false;
44
+ }
39
45
  return arrayCache;
40
46
  },
47
+ snapshot() {
48
+ return Array.from(store.entries());
49
+ },
50
+ hydrate(data) {
51
+ store.clear();
52
+ for (const [key, value] of data)
53
+ store.set(key, value);
54
+ emit();
55
+ },
41
56
  };
42
57
  }
@@ -41,7 +41,7 @@ export interface DecayMeterSet {
41
41
  refill(id: string, amount: number): void;
42
42
  /** Overwrite the base drain rate (e.g. a game mode with harsher hunger). */
43
43
  setRate(id: string, rate: number): void;
44
- /** Multiply the drain rate this tick onward — cold biome speeds warmth loss, sprint burns stamina. */
44
+ /** Multiply drain until cleared via `setRateModifier(id, 1)` — cold biome, sprint, etc. */
45
45
  setRateModifier(id: string, multiplier: number): void;
46
46
  /** Moodles for every crossed threshold, worst-first per meter. */
47
47
  moodles(): Moodle[];
@@ -43,7 +43,6 @@ export function createDecayMeterSet(configs) {
43
43
  return;
44
44
  for (const runtime of meters.values()) {
45
45
  runtime.value = clamp(runtime.value - runtime.rate * runtime.modifier * dt, runtime.min, runtime.max);
46
- runtime.modifier = 1;
47
46
  }
48
47
  },
49
48
  value(id) {
@@ -49,14 +49,14 @@ export interface GravityIntervalConfig {
49
49
  }
50
50
  /**
51
51
  * Gravity tick interval for a given level: linear speed-up with a floor, generalizing
52
- * `Games/block-stacker`'s `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds
52
+ * a falling-block `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds
53
53
  * with configurable base/step/floor.
54
54
  */
55
55
  export declare function gravityIntervalMs(level: number, config?: GravityIntervalConfig): number;
56
56
  /**
57
57
  * A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy,
58
58
  * footprint placement/collision, line-clear collapse, column-gravity settle (for match-3/sand
59
- * style cascades), and a pure lock-delay helper. Extracted from `Games/block-stacker`'s tetris
59
+ * style cascades), and a pure lock-delay helper. Extracted from falling-block tetris
60
60
  * logic so any falling-block game can adopt it instead of reimplementing the grid.
61
61
  */
62
62
  export declare function createFallingGrid<TCell>(config: FallingGridConfig): FallingGrid<TCell>;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Gravity tick interval for a given level: linear speed-up with a floor, generalizing
3
- * `Games/block-stacker`'s `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds
3
+ * a falling-block `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds
4
4
  * with configurable base/step/floor.
5
5
  */
6
6
  export function gravityIntervalMs(level, config = {}) {
@@ -12,7 +12,7 @@ export function gravityIntervalMs(level, config = {}) {
12
12
  /**
13
13
  * A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy,
14
14
  * footprint placement/collision, line-clear collapse, column-gravity settle (for match-3/sand
15
- * style cascades), and a pure lock-delay helper. Extracted from `Games/block-stacker`'s tetris
15
+ * style cascades), and a pure lock-delay helper. Extracted from falling-block tetris
16
16
  * logic so any falling-block game can adopt it instead of reimplementing the grid.
17
17
  */
18
18
  export function createFallingGrid(config) {
@@ -22,6 +22,24 @@ export interface BeatClock {
22
22
  bpm(): number;
23
23
  }
24
24
  export declare function createBeatClock(config: BeatClockConfig, onBeat?: (beatIndex: number) => void): BeatClock;
25
+ /** Signed offset (game-seconds) from `nowSec` to the nearest beat boundary — negative = early, positive = late (#286.12). */
26
+ export declare function nearestBeatDelta(nowSec: number, beatDurationSec: number): number;
27
+ export interface BeatAccuracyTier {
28
+ id: string;
29
+ /** Maximum absolute delta (game-seconds) from the beat that still earns this tier. */
30
+ windowSec: number;
31
+ }
32
+ export interface BeatJudgement {
33
+ /** The matched tier id, or `"miss"` beyond every tier. */
34
+ tier: string;
35
+ /** Signed delta to the nearest beat — negative = early, positive = late. */
36
+ deltaSec: number;
37
+ }
38
+ export declare const DEFAULT_BEAT_TIERS: readonly BeatAccuracyTier[];
39
+ /** Classify a press against the nearest beat into tiered judgements (perfect/good/miss by default). Tiers must be ordered tightest-first. */
40
+ export declare function classifyBeatAccuracy(nowSec: number, beatDurationSec: number, tiers?: readonly BeatAccuracyTier[]): BeatJudgement;
41
+ /** Whether a snapshot sits inside a bar-relative open window `[fromBeat, toBeat)` — e.g. "act on beats 2-4 of every bar" (#286.12). */
42
+ export declare function inBarWindow(snapshot: BeatSnapshot, fromBeat: number, toBeat: number): boolean;
25
43
  /** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
26
44
  export declare function nextBeatTime(nowSec: number, beatDurationSec: number, epsilon?: number): number;
27
45
  export interface BufferedAction<T> {
@@ -35,6 +35,30 @@ export function createBeatClock(config, onBeat) {
35
35
  bpm: () => config.bpm,
36
36
  };
37
37
  }
38
+ /** Signed offset (game-seconds) from `nowSec` to the nearest beat boundary — negative = early, positive = late (#286.12). */
39
+ export function nearestBeatDelta(nowSec, beatDurationSec) {
40
+ const beatIndex = Math.round(nowSec / beatDurationSec);
41
+ return nowSec - beatIndex * beatDurationSec;
42
+ }
43
+ export const DEFAULT_BEAT_TIERS = [
44
+ { id: "perfect", windowSec: 0.06 },
45
+ { id: "good", windowSec: 0.14 },
46
+ ];
47
+ /** Classify a press against the nearest beat into tiered judgements (perfect/good/miss by default). Tiers must be ordered tightest-first. */
48
+ export function classifyBeatAccuracy(nowSec, beatDurationSec, tiers = DEFAULT_BEAT_TIERS) {
49
+ const deltaSec = nearestBeatDelta(nowSec, beatDurationSec);
50
+ const magnitude = Math.abs(deltaSec);
51
+ for (const tier of tiers) {
52
+ if (magnitude <= tier.windowSec)
53
+ return { tier: tier.id, deltaSec };
54
+ }
55
+ return { tier: "miss", deltaSec };
56
+ }
57
+ /** Whether a snapshot sits inside a bar-relative open window `[fromBeat, toBeat)` — e.g. "act on beats 2-4 of every bar" (#286.12). */
58
+ export function inBarWindow(snapshot, fromBeat, toBeat) {
59
+ const barBeat = snapshot.beatInBar + snapshot.phase;
60
+ return barBeat >= fromBeat && barBeat < toBeat;
61
+ }
38
62
  const QUANTIZE_EPSILON = 1e-6;
39
63
  /** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
40
64
  export function nextBeatTime(nowSec, beatDurationSec, epsilon = QUANTIZE_EPSILON) {
@@ -0,0 +1,29 @@
1
+ export interface IdleWindowConfig {
2
+ maxSeconds?: number;
3
+ efficiency?: number;
4
+ }
5
+ export interface IdleWindow {
6
+ elapsedSeconds: number;
7
+ effectiveSeconds: number;
8
+ capped: boolean;
9
+ }
10
+ export declare function idleWindow(lastSeenMs: number, nowMs: number, config?: IdleWindowConfig): IdleWindow;
11
+ export interface LinearCatchUpInput {
12
+ current: number;
13
+ ratePerSecond: number;
14
+ min?: number;
15
+ max?: number;
16
+ }
17
+ export declare function linearCatchUp(seconds: number, input: LinearCatchUpInput): number;
18
+ export interface ExponentialCatchUpInput {
19
+ current: number;
20
+ factorPerSecond: number;
21
+ min?: number;
22
+ max?: number;
23
+ }
24
+ export declare function exponentialCatchUp(seconds: number, input: ExponentialCatchUpInput): number;
25
+ export interface SteppedCatchUpResult {
26
+ steps: number;
27
+ remainderSeconds: number;
28
+ }
29
+ export declare function steppedCatchUp(seconds: number, stepSeconds: number, step: (index: number) => void, maxSteps?: number): SteppedCatchUpResult;