@jgengine/core 0.6.0 → 0.8.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 (330) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +2 -0
  3. package/dist/ai/crowd.d.ts +36 -0
  4. package/dist/ai/crowd.js +213 -0
  5. package/dist/ai/jobBoard.d.ts +49 -0
  6. package/dist/ai/jobBoard.js +155 -0
  7. package/dist/ai/spawnDirector.d.ts +61 -0
  8. package/dist/ai/spawnDirector.js +162 -0
  9. package/dist/ai/threat.d.ts +25 -0
  10. package/dist/ai/threat.js +67 -0
  11. package/dist/anim/easing.d.ts +14 -0
  12. package/dist/anim/easing.js +55 -0
  13. package/dist/audio/audioFalloff.d.ts +38 -0
  14. package/dist/audio/audioFalloff.js +32 -0
  15. package/dist/behaviour/behaviour.d.ts +136 -0
  16. package/dist/behaviour/behaviour.js +388 -0
  17. package/dist/board/laneBoard.d.ts +63 -0
  18. package/dist/board/laneBoard.js +114 -0
  19. package/dist/board/timelineBoard.d.ts +36 -0
  20. package/dist/board/timelineBoard.js +70 -0
  21. package/dist/cards/cardPile.d.ts +64 -0
  22. package/dist/cards/cardPile.js +175 -0
  23. package/dist/cards/modifierPipeline.d.ts +33 -0
  24. package/dist/cards/modifierPipeline.js +48 -0
  25. package/dist/combat/abilityKit.d.ts +47 -0
  26. package/dist/combat/abilityKit.js +139 -0
  27. package/dist/combat/animationState.d.ts +47 -0
  28. package/dist/combat/animationState.js +101 -0
  29. package/dist/combat/attackTags.d.ts +14 -0
  30. package/dist/combat/attackTags.js +26 -0
  31. package/dist/combat/breakMeters.d.ts +32 -0
  32. package/dist/combat/breakMeters.js +51 -0
  33. package/dist/combat/comboString.d.ts +35 -0
  34. package/dist/combat/comboString.js +59 -0
  35. package/dist/combat/death.d.ts +8 -0
  36. package/dist/combat/death.js +5 -2
  37. package/dist/combat/defensiveWindow.d.ts +34 -0
  38. package/dist/combat/defensiveWindow.js +66 -0
  39. package/dist/combat/downed.d.ts +35 -0
  40. package/dist/combat/downed.js +104 -0
  41. package/dist/combat/effects.d.ts +12 -1
  42. package/dist/combat/effects.js +31 -19
  43. package/dist/combat/hitReaction.d.ts +23 -0
  44. package/dist/combat/hitReaction.js +20 -0
  45. package/dist/combat/projectiles.d.ts +42 -5
  46. package/dist/combat/projectiles.js +98 -12
  47. package/dist/combat/resistance.d.ts +13 -0
  48. package/dist/combat/resistance.js +46 -0
  49. package/dist/combat/telegraph.d.ts +31 -0
  50. package/dist/combat/telegraph.js +51 -0
  51. package/dist/commands/commandRegistry.d.ts +1 -1
  52. package/dist/commands/commandRegistry.js +2 -1
  53. package/dist/crafting/crop.d.ts +59 -0
  54. package/dist/crafting/crop.js +181 -0
  55. package/dist/crafting/production.d.ts +64 -0
  56. package/dist/crafting/production.js +130 -0
  57. package/dist/crafting/recipe.d.ts +62 -0
  58. package/dist/crafting/recipe.js +101 -0
  59. package/dist/data/dataSource.d.ts +26 -0
  60. package/dist/data/dataSource.js +93 -0
  61. package/dist/data/devProxy.d.ts +11 -0
  62. package/dist/data/devProxy.js +38 -0
  63. package/dist/data/fetchJson.d.ts +20 -0
  64. package/dist/data/fetchJson.js +43 -0
  65. package/dist/data/jsonDataSource.d.ts +4 -0
  66. package/dist/data/jsonDataSource.js +6 -0
  67. package/dist/devtools/devtools.d.ts +157 -0
  68. package/dist/devtools/devtools.js +484 -0
  69. package/dist/devtools/transformTunables.d.ts +14 -0
  70. package/dist/devtools/transformTunables.js +38 -0
  71. package/dist/economy/sharedWallet.d.ts +28 -0
  72. package/dist/economy/sharedWallet.js +62 -0
  73. package/dist/economy/techTree.d.ts +40 -0
  74. package/dist/economy/techTree.js +66 -0
  75. package/dist/game/chat.d.ts +66 -0
  76. package/dist/game/chat.js +165 -0
  77. package/dist/game/cosmetics.d.ts +25 -0
  78. package/dist/game/cosmetics.js +51 -0
  79. package/dist/game/defineGame.d.ts +10 -2
  80. package/dist/game/defineGame.js +2 -1
  81. package/dist/game/events.d.ts +87 -0
  82. package/dist/game/levelSequence.d.ts +44 -0
  83. package/dist/game/levelSequence.js +78 -0
  84. package/dist/game/lootFilter.d.ts +33 -0
  85. package/dist/game/lootFilter.js +48 -0
  86. package/dist/game/ping.d.ts +67 -0
  87. package/dist/game/ping.js +86 -0
  88. package/dist/game/playableGame.d.ts +516 -8
  89. package/dist/game/playableGame.js +6 -1
  90. package/dist/game/progression.d.ts +1 -0
  91. package/dist/game/progression.js +23 -2
  92. package/dist/game/race.d.ts +100 -0
  93. package/dist/game/race.js +236 -0
  94. package/dist/game/runDraft.d.ts +34 -0
  95. package/dist/game/runDraft.js +97 -0
  96. package/dist/game/social.d.ts +69 -0
  97. package/dist/game/social.js +138 -0
  98. package/dist/game/spawnPoints.d.ts +19 -0
  99. package/dist/game/spawnPoints.js +23 -0
  100. package/dist/game/worldItem.d.ts +95 -0
  101. package/dist/game/worldItem.js +138 -0
  102. package/dist/input/actionBindings.d.ts +10 -0
  103. package/dist/input/actionBindings.js +14 -0
  104. package/dist/input/axisInput.d.ts +43 -0
  105. package/dist/input/axisInput.js +70 -0
  106. package/dist/input/gestureSurface.d.ts +31 -0
  107. package/dist/input/gestureSurface.js +120 -0
  108. package/dist/input/pointer.d.ts +69 -0
  109. package/dist/input/pointer.js +65 -0
  110. package/dist/input/touchScheme.d.ts +83 -0
  111. package/dist/input/touchScheme.js +115 -0
  112. package/dist/interaction/contextMenu.d.ts +30 -0
  113. package/dist/interaction/contextMenu.js +22 -0
  114. package/dist/interaction/qte.d.ts +20 -0
  115. package/dist/interaction/qte.js +24 -0
  116. package/dist/interaction/skillCheck.d.ts +20 -0
  117. package/dist/interaction/skillCheck.js +26 -0
  118. package/dist/inventory/shapedGrid.d.ts +56 -0
  119. package/dist/inventory/shapedGrid.js +176 -0
  120. package/dist/inventory/storageTier.d.ts +52 -0
  121. package/dist/inventory/storageTier.js +77 -0
  122. package/dist/item/affix.d.ts +54 -0
  123. package/dist/item/affix.js +115 -0
  124. package/dist/item/durability.d.ts +51 -0
  125. package/dist/item/durability.js +78 -0
  126. package/dist/item/modularItem.d.ts +46 -0
  127. package/dist/item/modularItem.js +75 -0
  128. package/dist/meta/changelog.d.ts +1 -1
  129. package/dist/meta/changelog.js +71 -1
  130. package/dist/movement/dash.d.ts +37 -0
  131. package/dist/movement/dash.js +65 -0
  132. package/dist/movement/movementModel.d.ts +36 -3
  133. package/dist/movement/movementModel.js +81 -5
  134. package/dist/movement/voxelController.d.ts +61 -0
  135. package/dist/movement/voxelController.js +229 -0
  136. package/dist/multiplayer/chatContract.d.ts +42 -0
  137. package/dist/multiplayer/chatContract.js +57 -0
  138. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  139. package/dist/multiplayer/combatSnapshot.js +89 -0
  140. package/dist/multiplayer/identity.d.ts +13 -0
  141. package/dist/multiplayer/identity.js +20 -0
  142. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  143. package/dist/multiplayer/lagCompensation.js +154 -0
  144. package/dist/multiplayer/matchmaking.d.ts +33 -0
  145. package/dist/multiplayer/matchmaking.js +60 -0
  146. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  147. package/dist/multiplayer/poseSyncGate.js +13 -1
  148. package/dist/multiplayer/presenceModel.d.ts +3 -0
  149. package/dist/multiplayer/presenceModel.js +15 -1
  150. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  151. package/dist/multiplayer/simultaneousCommit.js +53 -0
  152. package/dist/multiplayer/voiceContract.d.ts +44 -0
  153. package/dist/multiplayer/voiceContract.js +108 -0
  154. package/dist/nav/navConstrain.d.ts +14 -0
  155. package/dist/nav/navConstrain.js +26 -0
  156. package/dist/nav/navFromEnvironment.d.ts +11 -0
  157. package/dist/nav/navFromEnvironment.js +16 -0
  158. package/dist/nav/navGrid.d.ts +56 -0
  159. package/dist/nav/navGrid.js +265 -0
  160. package/dist/nav/pathFollow.d.ts +31 -0
  161. package/dist/nav/pathFollow.js +69 -0
  162. package/dist/physics/ballisticSweep.d.ts +18 -0
  163. package/dist/physics/ballisticSweep.js +49 -0
  164. package/dist/physics/buoyancy.d.ts +49 -0
  165. package/dist/physics/buoyancy.js +112 -0
  166. package/dist/physics/carryable.d.ts +46 -0
  167. package/dist/physics/carryable.js +110 -0
  168. package/dist/physics/damageZones.d.ts +56 -0
  169. package/dist/physics/damageZones.js +107 -0
  170. package/dist/physics/forceVolume.d.ts +48 -0
  171. package/dist/physics/forceVolume.js +119 -0
  172. package/dist/physics/physicsWorld.d.ts +124 -2
  173. package/dist/physics/physicsWorld.js +493 -41
  174. package/dist/physics/ragdoll.d.ts +49 -0
  175. package/dist/physics/ragdoll.js +103 -0
  176. package/dist/physics/spatialGrid.d.ts +41 -0
  177. package/dist/physics/spatialGrid.js +143 -0
  178. package/dist/physics/structure.d.ts +83 -0
  179. package/dist/physics/structure.js +0 -0
  180. package/dist/physics/traversal.d.ts +75 -0
  181. package/dist/physics/traversal.js +138 -0
  182. package/dist/physics/vehicleBody.d.ts +99 -0
  183. package/dist/physics/vehicleBody.js +233 -0
  184. package/dist/puzzle/cellGrid.d.ts +26 -0
  185. package/dist/puzzle/cellGrid.js +110 -0
  186. package/dist/puzzle/fallingPiece.d.ts +24 -0
  187. package/dist/puzzle/fallingPiece.js +51 -0
  188. package/dist/random/nameGen.d.ts +18 -0
  189. package/dist/random/nameGen.js +35 -0
  190. package/dist/random/rng.d.ts +2 -0
  191. package/dist/random/rng.js +23 -0
  192. package/dist/runtime/adapter.d.ts +35 -0
  193. package/dist/runtime/adapter.js +37 -1
  194. package/dist/runtime/cameraDirector.d.ts +10 -0
  195. package/dist/runtime/cameraDirector.js +25 -0
  196. package/dist/runtime/gameContext.d.ts +107 -3
  197. package/dist/runtime/gameContext.js +253 -7
  198. package/dist/runtime/hostPersistence.d.ts +39 -0
  199. package/dist/runtime/hostPersistence.js +54 -0
  200. package/dist/runtime/inputSnapshot.d.ts +7 -0
  201. package/dist/runtime/inputSnapshot.js +12 -0
  202. package/dist/runtime/motionIntents.d.ts +25 -0
  203. package/dist/runtime/motionIntents.js +25 -0
  204. package/dist/runtime/persistenceScope.d.ts +32 -0
  205. package/dist/runtime/persistenceScope.js +64 -0
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/scene/autoTarget.d.ts +15 -0
  208. package/dist/scene/autoTarget.js +54 -0
  209. package/dist/scene/behaviors.d.ts +13 -1
  210. package/dist/scene/behaviors.js +3 -0
  211. package/dist/scene/captureCheck.d.ts +7 -0
  212. package/dist/scene/captureCheck.js +11 -0
  213. package/dist/scene/entityStore.d.ts +9 -1
  214. package/dist/scene/entityStore.js +25 -2
  215. package/dist/scene/form.d.ts +40 -0
  216. package/dist/scene/form.js +56 -0
  217. package/dist/scene/mount.d.ts +62 -0
  218. package/dist/scene/mount.js +107 -0
  219. package/dist/scene/movementSpeed.d.ts +35 -0
  220. package/dist/scene/movementSpeed.js +20 -0
  221. package/dist/scene/objectQuery.d.ts +18 -0
  222. package/dist/scene/objectQuery.js +108 -0
  223. package/dist/scene/objectStore.d.ts +11 -0
  224. package/dist/scene/objectStore.js +109 -4
  225. package/dist/scene/paintLayer.d.ts +15 -0
  226. package/dist/scene/paintLayer.js +53 -0
  227. package/dist/scene/possession.d.ts +30 -0
  228. package/dist/scene/possession.js +51 -0
  229. package/dist/scene/roster.d.ts +25 -0
  230. package/dist/scene/roster.js +57 -0
  231. package/dist/scene/selection.d.ts +30 -0
  232. package/dist/scene/selection.js +38 -0
  233. package/dist/scene/spatial.d.ts +21 -0
  234. package/dist/scene/spatial.js +63 -1
  235. package/dist/scene/stationClaim.d.ts +43 -0
  236. package/dist/scene/stationClaim.js +109 -0
  237. package/dist/sensor/concealment.d.ts +21 -0
  238. package/dist/sensor/concealment.js +63 -0
  239. package/dist/sensor/freezeMonitor.d.ts +17 -0
  240. package/dist/sensor/freezeMonitor.js +35 -0
  241. package/dist/sensor/frustumSensor.d.ts +60 -0
  242. package/dist/sensor/frustumSensor.js +104 -0
  243. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  244. package/dist/sensor/hiddenStateProbe.js +40 -0
  245. package/dist/sensor/recordingBuffer.d.ts +29 -0
  246. package/dist/sensor/recordingBuffer.js +61 -0
  247. package/dist/sensor/revealQuery.d.ts +22 -0
  248. package/dist/sensor/revealQuery.js +36 -0
  249. package/dist/session/contestedChannel.d.ts +36 -0
  250. package/dist/session/contestedChannel.js +100 -0
  251. package/dist/session/extraction.d.ts +54 -0
  252. package/dist/session/extraction.js +108 -0
  253. package/dist/session/ring.d.ts +36 -0
  254. package/dist/session/ring.js +88 -0
  255. package/dist/session/roles.d.ts +6 -0
  256. package/dist/session/roles.js +32 -0
  257. package/dist/session/roundState.d.ts +79 -0
  258. package/dist/session/roundState.js +158 -0
  259. package/dist/stats/accumulatorMeter.d.ts +31 -0
  260. package/dist/stats/accumulatorMeter.js +81 -0
  261. package/dist/stats/eventMeter.d.ts +34 -0
  262. package/dist/stats/eventMeter.js +76 -0
  263. package/dist/stats/rollCheck.d.ts +15 -0
  264. package/dist/stats/rollCheck.js +15 -0
  265. package/dist/survival/decayMeter.d.ts +59 -0
  266. package/dist/survival/decayMeter.js +95 -0
  267. package/dist/survival/moodle.d.ts +48 -0
  268. package/dist/survival/moodle.js +91 -0
  269. package/dist/survival/regionHealth.d.ts +72 -0
  270. package/dist/survival/regionHealth.js +129 -0
  271. package/dist/tactics/fallingGrid.d.ts +62 -0
  272. package/dist/tactics/fallingGrid.js +158 -0
  273. package/dist/tactics/predictiveQuery.d.ts +34 -0
  274. package/dist/tactics/predictiveQuery.js +44 -0
  275. package/dist/tactics/snapshot.d.ts +18 -0
  276. package/dist/tactics/snapshot.js +65 -0
  277. package/dist/tactics/surface.d.ts +52 -0
  278. package/dist/tactics/surface.js +135 -0
  279. package/dist/tactics/tacticalGrid.d.ts +68 -0
  280. package/dist/tactics/tacticalGrid.js +261 -0
  281. package/dist/time/beatClock.d.ts +40 -0
  282. package/dist/time/beatClock.js +73 -0
  283. package/dist/time/simClock.d.ts +25 -1
  284. package/dist/time/simClock.js +28 -3
  285. package/dist/turn/commit.d.ts +27 -0
  286. package/dist/turn/commit.js +72 -0
  287. package/dist/turn/intent.d.ts +14 -0
  288. package/dist/turn/intent.js +29 -0
  289. package/dist/turn/turnLoop.d.ts +60 -0
  290. package/dist/turn/turnLoop.js +223 -0
  291. package/dist/world/buildPermissions.d.ts +47 -0
  292. package/dist/world/buildPermissions.js +134 -0
  293. package/dist/world/carve.d.ts +105 -0
  294. package/dist/world/carve.js +175 -0
  295. package/dist/world/connectors.d.ts +40 -0
  296. package/dist/world/connectors.js +67 -0
  297. package/dist/world/envField.d.ts +71 -0
  298. package/dist/world/envField.js +96 -0
  299. package/dist/world/environmentSummary.d.ts +53 -0
  300. package/dist/world/environmentSummary.js +129 -0
  301. package/dist/world/features.d.ts +87 -6
  302. package/dist/world/features.js +52 -2
  303. package/dist/world/fog.d.ts +40 -0
  304. package/dist/world/fog.js +137 -0
  305. package/dist/world/gridInstances.d.ts +13 -0
  306. package/dist/world/gridInstances.js +22 -0
  307. package/dist/world/markers.d.ts +52 -0
  308. package/dist/world/markers.js +96 -0
  309. package/dist/world/minimap.d.ts +42 -0
  310. package/dist/world/minimap.js +72 -0
  311. package/dist/world/placedStructureStore.d.ts +39 -0
  312. package/dist/world/placedStructureStore.js +95 -0
  313. package/dist/world/placementController.d.ts +69 -0
  314. package/dist/world/placementController.js +153 -0
  315. package/dist/world/realm.d.ts +61 -0
  316. package/dist/world/realm.js +66 -0
  317. package/dist/world/support.d.ts +27 -0
  318. package/dist/world/support.js +75 -0
  319. package/dist/world/terraform.d.ts +54 -0
  320. package/dist/world/terraform.js +186 -0
  321. package/dist/world/terrain.d.ts +23 -1
  322. package/dist/world/terrain.js +64 -1
  323. package/dist/world/voxelField.d.ts +54 -0
  324. package/dist/world/voxelField.js +241 -0
  325. package/dist/world/walls.d.ts +61 -0
  326. package/dist/world/walls.js +221 -0
  327. package/dist/world/weather.d.ts +92 -0
  328. package/dist/world/weather.js +169 -0
  329. package/llms.txt +4102 -0
  330. package/package.json +3 -2
@@ -0,0 +1,61 @@
1
+ function evict(frames, options) {
2
+ if (options.maxDurationSeconds !== undefined && frames.length > 0) {
3
+ const cutoff = frames[frames.length - 1].t - options.maxDurationSeconds;
4
+ let dropTo = 0;
5
+ while (dropTo < frames.length && frames[dropTo].t < cutoff)
6
+ dropTo += 1;
7
+ if (dropTo > 0)
8
+ frames.splice(0, dropTo);
9
+ }
10
+ if (options.maxFrames !== undefined && frames.length > options.maxFrames) {
11
+ frames.splice(0, frames.length - options.maxFrames);
12
+ }
13
+ }
14
+ /**
15
+ * A session-recording buffer for replay / photo mode / kill-cam: append
16
+ * timestamped snapshots (poses, camera state, whatever `T` is) on game-time,
17
+ * then seek/scrub the recording independent of the live sim. Frames are
18
+ * expected in non-decreasing `t` order (one push per tick); `seek`/`range`
19
+ * binary-search on that assumption.
20
+ */
21
+ export function createRecordingBuffer(options = {}) {
22
+ const frames = [];
23
+ function floorIndex(t) {
24
+ let lo = 0;
25
+ let hi = frames.length - 1;
26
+ let result = -1;
27
+ while (lo <= hi) {
28
+ const mid = (lo + hi) >> 1;
29
+ if (frames[mid].t <= t) {
30
+ result = mid;
31
+ lo = mid + 1;
32
+ }
33
+ else {
34
+ hi = mid - 1;
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ return {
40
+ append(t, data) {
41
+ frames.push({ t, data });
42
+ evict(frames, options);
43
+ },
44
+ seek(t) {
45
+ const index = floorIndex(t);
46
+ return index === -1 ? null : frames[index];
47
+ },
48
+ range(fromT, toT) {
49
+ return frames.filter((frame) => frame.t >= fromT && frame.t <= toT);
50
+ },
51
+ clear() {
52
+ frames.length = 0;
53
+ },
54
+ duration() {
55
+ return frames.length === 0 ? 0 : frames[frames.length - 1].t - frames[0].t;
56
+ },
57
+ frames() {
58
+ return frames;
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,22 @@
1
+ import type { EntityPosition } from "../scene/entityStore.js";
2
+ export interface RevealHit {
3
+ instanceId: string;
4
+ distance: number;
5
+ tags: readonly string[];
6
+ }
7
+ export interface RevealQueryOptions {
8
+ resolvePosition: (instanceId: string) => EntityPosition | undefined;
9
+ resolveTags: (instanceId: string) => readonly string[];
10
+ candidates: () => string[];
11
+ }
12
+ export interface RevealQuery {
13
+ inRadius(center: EntityPosition | string, radius: number, tags: readonly string[]): RevealHit[];
14
+ }
15
+ /**
16
+ * `inRadius` already ignores occlusion; combat's AoE resolution is what layers a
17
+ * line-of-sight filter on top of it (see `effect({ at, radius, los })`). This is
18
+ * the same unfiltered radius query, scoped to catalog-declared tags and shaped
19
+ * for a vision readout (Dark Sight / detective-vision reveal) instead of damage
20
+ * resolution — it never consults an occluder, by design.
21
+ */
22
+ export declare function createRevealQuery(options: RevealQueryOptions): RevealQuery;
@@ -0,0 +1,36 @@
1
+ import { distanceBetween } from "../scene/spatial.js";
2
+ /**
3
+ * `inRadius` already ignores occlusion; combat's AoE resolution is what layers a
4
+ * line-of-sight filter on top of it (see `effect({ at, radius, los })`). This is
5
+ * the same unfiltered radius query, scoped to catalog-declared tags and shaped
6
+ * for a vision readout (Dark Sight / detective-vision reveal) instead of damage
7
+ * resolution — it never consults an occluder, by design.
8
+ */
9
+ export function createRevealQuery(options) {
10
+ const { resolvePosition, resolveTags, candidates } = options;
11
+ return {
12
+ inRadius(center, radius, tags) {
13
+ const centerId = typeof center === "string" ? center : null;
14
+ const centerPosition = typeof center === "string" ? resolvePosition(center) : center;
15
+ if (centerPosition === undefined)
16
+ return [];
17
+ const hits = [];
18
+ for (const instanceId of candidates()) {
19
+ if (instanceId === centerId)
20
+ continue;
21
+ const position = resolvePosition(instanceId);
22
+ if (position === undefined)
23
+ continue;
24
+ const distance = distanceBetween(centerPosition, position);
25
+ if (distance > radius)
26
+ continue;
27
+ const entityTags = resolveTags(instanceId);
28
+ if (tags.length > 0 && !tags.some((tag) => entityTags.includes(tag)))
29
+ continue;
30
+ hits.push({ instanceId, distance, tags: entityTags });
31
+ }
32
+ hits.sort((a, b) => a.distance - b.distance);
33
+ return hits;
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,36 @@
1
+ export type ContestedPhase = "idle" | "active" | "paused" | "contested" | "complete" | "interrupted";
2
+ export type ContestReaction = "pause" | "decay";
3
+ export interface ContestedChannelConfig {
4
+ duration: number;
5
+ interruptOnDamage?: boolean;
6
+ resetOnInterrupt?: boolean;
7
+ favorability?: Record<string, number>;
8
+ ratePerOccupant?: boolean;
9
+ contested?: ContestReaction;
10
+ decayRate?: number;
11
+ }
12
+ export type ContestedEventKind = "start" | "tick" | "contested" | "paused" | "complete" | "interrupted";
13
+ export interface ContestedEvent {
14
+ kind: ContestedEventKind;
15
+ owner: string | null;
16
+ progress: number;
17
+ reason?: string;
18
+ }
19
+ export interface ContestedSnapshot {
20
+ phase: ContestedPhase;
21
+ progress: number;
22
+ owner: string | null;
23
+ remaining: number;
24
+ }
25
+ export interface ContestedChannel {
26
+ start(team: string): ContestedEvent | null;
27
+ tick(dt: number, occupants?: Record<string, number>): ContestedEvent[];
28
+ damage(reason?: string): ContestedEvent | null;
29
+ reset(): void;
30
+ progress(): number;
31
+ phase(): ContestedPhase;
32
+ owner(): string | null;
33
+ remaining(): number;
34
+ snapshot(): ContestedSnapshot;
35
+ }
36
+ export declare function createContestedChannel(config: ContestedChannelConfig): ContestedChannel;
@@ -0,0 +1,100 @@
1
+ function occupantsOf(occupants, team) {
2
+ return occupants?.[team] ?? 0;
3
+ }
4
+ function anyContester(occupants, owner) {
5
+ if (occupants === undefined)
6
+ return false;
7
+ for (const [team, count] of Object.entries(occupants)) {
8
+ if (team !== owner && count > 0)
9
+ return true;
10
+ }
11
+ return false;
12
+ }
13
+ export function createContestedChannel(config) {
14
+ const duration = config.duration > 0 ? config.duration : Number.EPSILON;
15
+ const interruptOnDamage = config.interruptOnDamage ?? true;
16
+ const resetOnInterrupt = config.resetOnInterrupt ?? false;
17
+ const favorability = config.favorability ?? {};
18
+ const contested = config.contested ?? "pause";
19
+ const decayRate = config.decayRate !== undefined ? Math.abs(config.decayRate) : 1 / duration;
20
+ let phase = "idle";
21
+ let progress = 0;
22
+ let owner = null;
23
+ function fillRate(occupantCount) {
24
+ const multiplier = favorability[owner ?? ""] ?? 1;
25
+ const scale = config.ratePerOccupant ? Math.max(1, occupantCount) : 1;
26
+ return (multiplier * scale) / duration;
27
+ }
28
+ function snapshot() {
29
+ return { phase, progress, owner, remaining: Math.max(0, (1 - progress) * duration) };
30
+ }
31
+ function emit(kind, reason) {
32
+ const event = { kind, owner, progress };
33
+ if (reason !== undefined)
34
+ event.reason = reason;
35
+ return event;
36
+ }
37
+ return {
38
+ start(team) {
39
+ if (phase === "active" || phase === "contested" || phase === "paused")
40
+ return null;
41
+ owner = team;
42
+ progress = phase === "interrupted" && !resetOnInterrupt ? progress : 0;
43
+ phase = "active";
44
+ return emit("start");
45
+ },
46
+ tick(dt, occupants) {
47
+ if (dt <= 0)
48
+ return [];
49
+ if (phase !== "active" && phase !== "paused" && phase !== "contested")
50
+ return [];
51
+ if (owner === null)
52
+ return [];
53
+ const ownerCount = occupantsOf(occupants, owner);
54
+ if (occupants !== undefined && ownerCount <= 0) {
55
+ if (phase !== "paused") {
56
+ phase = "paused";
57
+ return [emit("paused", "owner-left")];
58
+ }
59
+ return [];
60
+ }
61
+ if (anyContester(occupants, owner)) {
62
+ if (contested === "pause") {
63
+ const changed = phase !== "contested";
64
+ phase = "contested";
65
+ return changed ? [emit("contested")] : [];
66
+ }
67
+ progress = Math.max(0, progress - decayRate * dt);
68
+ phase = "contested";
69
+ return [emit("contested")];
70
+ }
71
+ phase = "active";
72
+ progress = Math.min(1, progress + fillRate(ownerCount) * dt);
73
+ if (progress >= 1) {
74
+ phase = "complete";
75
+ return [emit("tick"), emit("complete")];
76
+ }
77
+ return [emit("tick")];
78
+ },
79
+ damage(reason) {
80
+ if (phase !== "active" && phase !== "paused" && phase !== "contested")
81
+ return null;
82
+ if (!interruptOnDamage)
83
+ return null;
84
+ phase = "interrupted";
85
+ if (resetOnInterrupt)
86
+ progress = 0;
87
+ return emit("interrupted", reason ?? "damage");
88
+ },
89
+ reset() {
90
+ phase = "idle";
91
+ progress = 0;
92
+ owner = null;
93
+ },
94
+ progress: () => progress,
95
+ phase: () => phase,
96
+ owner: () => owner,
97
+ remaining: () => Math.max(0, (1 - progress) * duration),
98
+ snapshot,
99
+ };
100
+ }
@@ -0,0 +1,54 @@
1
+ import { type ConsolationPolicy, type ContainerSnapshot, type DeliveryEntry, type DeliveryQueue, type InsurancePolicy, type ItemStack } from "../inventory/storageTier.js";
2
+ import { type ContestedChannel, type ContestedEvent } from "./contestedChannel.js";
3
+ import type { RingPoint } from "./ring.js";
4
+ export interface ExtractPoint {
5
+ id: string;
6
+ center: RingPoint;
7
+ radius: number;
8
+ holdSeconds: number;
9
+ favorability?: Record<string, number>;
10
+ interruptOnDamage?: boolean;
11
+ }
12
+ export interface RaidSessionConfig {
13
+ extracts: readonly ExtractPoint[];
14
+ insurance?: InsurancePolicy;
15
+ consolation?: ConsolationPolicy;
16
+ }
17
+ export type RaidStatus = "in-raid" | "extracting" | "extracted" | "dead";
18
+ export interface ExtractionAttempt {
19
+ extractId: string;
20
+ channel: ContestedChannel;
21
+ }
22
+ export interface ExtractionResult {
23
+ userId: string;
24
+ extractId: string;
25
+ banked: readonly ItemStack[];
26
+ }
27
+ export interface DeathResult {
28
+ userId: string;
29
+ kept: readonly ItemStack[];
30
+ lost: readonly ItemStack[];
31
+ scheduled: DeliveryEntry | null;
32
+ consolationLoadoutId: string | null;
33
+ }
34
+ export interface RaidPlayerSnapshot {
35
+ status: RaidStatus;
36
+ extractId: string | null;
37
+ progress: number;
38
+ remaining: number;
39
+ }
40
+ export interface RaidSession {
41
+ beginExtract(userId: string, extractId: string, team?: string): ContestedEvent | null;
42
+ tickExtract(userId: string, dt: number, occupants?: Record<string, number>): ContestedEvent[];
43
+ damage(userId: string, reason?: string): ContestedEvent | null;
44
+ cancelExtract(userId: string): void;
45
+ resolveExtraction(userId: string, containers: readonly ContainerSnapshot[]): ExtractionResult;
46
+ resolveDeath(userId: string, containers: readonly ContainerSnapshot[], now: number, rng?: () => number): DeathResult;
47
+ claimDeliveries(now: number): readonly DeliveryEntry[];
48
+ pendingDeliveries(userId?: string): readonly DeliveryEntry[];
49
+ deliveries(): DeliveryQueue;
50
+ extractPoint(extractId: string): ExtractPoint | undefined;
51
+ status(userId: string): RaidStatus;
52
+ playerSnapshot(userId: string): RaidPlayerSnapshot;
53
+ }
54
+ export declare function createRaidSession(config: RaidSessionConfig): RaidSession;
@@ -0,0 +1,108 @@
1
+ import { createDeliveryQueue, insureLost, partitionOnDeath, resolveConsolation, } from "../inventory/storageTier.js";
2
+ import { createContestedChannel, } from "./contestedChannel.js";
3
+ function mergeAll(containers) {
4
+ const totals = new Map();
5
+ for (const container of containers) {
6
+ for (const stack of container.items) {
7
+ totals.set(stack.itemId, (totals.get(stack.itemId) ?? 0) + stack.count);
8
+ }
9
+ }
10
+ const out = [];
11
+ for (const [itemId, count] of totals)
12
+ if (count > 0)
13
+ out.push({ itemId, count });
14
+ return out;
15
+ }
16
+ export function createRaidSession(config) {
17
+ const points = new Map();
18
+ for (const point of config.extracts)
19
+ points.set(point.id, point);
20
+ const attempts = new Map();
21
+ const statuses = new Map();
22
+ const queue = createDeliveryQueue();
23
+ function statusOf(userId) {
24
+ return statuses.get(userId) ?? "in-raid";
25
+ }
26
+ return {
27
+ beginExtract(userId, extractId, team) {
28
+ const point = points.get(extractId);
29
+ if (point === undefined)
30
+ return null;
31
+ if (statusOf(userId) === "extracted" || statusOf(userId) === "dead")
32
+ return null;
33
+ const channel = createContestedChannel({
34
+ duration: point.holdSeconds,
35
+ interruptOnDamage: point.interruptOnDamage ?? true,
36
+ favorability: point.favorability,
37
+ });
38
+ const event = channel.start(team ?? userId);
39
+ attempts.set(userId, { extractId, channel });
40
+ statuses.set(userId, "extracting");
41
+ return event;
42
+ },
43
+ tickExtract(userId, dt, occupants) {
44
+ const attempt = attempts.get(userId);
45
+ if (attempt === undefined)
46
+ return [];
47
+ const events = attempt.channel.tick(dt, occupants);
48
+ if (attempt.channel.phase() === "complete")
49
+ statuses.set(userId, "extracted");
50
+ return events;
51
+ },
52
+ damage(userId, reason) {
53
+ const attempt = attempts.get(userId);
54
+ if (attempt === undefined)
55
+ return null;
56
+ const event = attempt.channel.damage(reason);
57
+ if (attempt.channel.phase() === "interrupted")
58
+ statuses.set(userId, "in-raid");
59
+ return event;
60
+ },
61
+ cancelExtract(userId) {
62
+ attempts.delete(userId);
63
+ if (statusOf(userId) === "extracting")
64
+ statuses.set(userId, "in-raid");
65
+ },
66
+ resolveExtraction(userId, containers) {
67
+ const attempt = attempts.get(userId);
68
+ const extractId = attempt?.extractId ?? "";
69
+ attempts.delete(userId);
70
+ statuses.set(userId, "extracted");
71
+ return { userId, extractId, banked: mergeAll(containers) };
72
+ },
73
+ resolveDeath(userId, containers, now, rng) {
74
+ attempts.delete(userId);
75
+ statuses.set(userId, "dead");
76
+ const partition = partitionOnDeath(containers);
77
+ let scheduled = null;
78
+ if (config.insurance !== undefined) {
79
+ const insured = insureLost(partition.lost, config.insurance, userId, now, rng);
80
+ if (insured !== null)
81
+ scheduled = queue.schedule(insured);
82
+ }
83
+ const consolation = config.consolation !== undefined ? resolveConsolation(config.consolation, partition) : null;
84
+ return {
85
+ userId,
86
+ kept: partition.kept,
87
+ lost: partition.lost,
88
+ scheduled,
89
+ consolationLoadoutId: consolation?.loadoutId ?? null,
90
+ };
91
+ },
92
+ claimDeliveries: (now) => queue.claimDue(now),
93
+ pendingDeliveries: (userId) => queue.pending(userId),
94
+ deliveries: () => queue,
95
+ extractPoint: (extractId) => points.get(extractId),
96
+ status: statusOf,
97
+ playerSnapshot: (userId) => {
98
+ const attempt = attempts.get(userId);
99
+ const channel = attempt?.channel;
100
+ return {
101
+ status: statusOf(userId),
102
+ extractId: attempt?.extractId ?? null,
103
+ progress: channel?.progress() ?? 0,
104
+ remaining: channel?.remaining() ?? 0,
105
+ };
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,36 @@
1
+ export type RingPoint = [number, number];
2
+ export interface RingPhase {
3
+ startTime: number;
4
+ shrinkDuration: number;
5
+ fromRadius: number;
6
+ toRadius: number;
7
+ damagePerSecond: number;
8
+ center?: RingPoint;
9
+ }
10
+ export interface RingConfig {
11
+ center: RingPoint;
12
+ phases: readonly RingPhase[];
13
+ }
14
+ export interface RingSample {
15
+ center: RingPoint;
16
+ radius: number;
17
+ damagePerSecond: number;
18
+ phase: number;
19
+ shrinking: boolean;
20
+ }
21
+ export interface RingHit {
22
+ id: string;
23
+ damage: number;
24
+ distanceOutside: number;
25
+ }
26
+ export interface Ring {
27
+ at(time: number): RingSample;
28
+ isOutside(time: number, position: RingPoint): boolean;
29
+ distanceOutside(time: number, position: RingPoint): number;
30
+ damageOutside(time: number, dt: number, positions: Iterable<{
31
+ id: string;
32
+ position: RingPoint;
33
+ }>): RingHit[];
34
+ }
35
+ export declare function ringSampleAt(config: RingConfig, time: number): RingSample;
36
+ export declare function createRing(config: RingConfig): Ring;
@@ -0,0 +1,88 @@
1
+ function lerp(from, to, t) {
2
+ return from + (to - from) * t;
3
+ }
4
+ function lerpPoint(from, to, t) {
5
+ return [lerp(from[0], to[0], t), lerp(from[1], to[1], t)];
6
+ }
7
+ function distance(a, b) {
8
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
9
+ }
10
+ export function ringSampleAt(config, time) {
11
+ const phases = config.phases;
12
+ if (phases.length === 0) {
13
+ return { center: config.center, radius: 0, damagePerSecond: 0, phase: -1, shrinking: false };
14
+ }
15
+ const first = phases[0];
16
+ if (time < first.startTime) {
17
+ return {
18
+ center: config.center,
19
+ radius: first.fromRadius,
20
+ damagePerSecond: 0,
21
+ phase: -1,
22
+ shrinking: false,
23
+ };
24
+ }
25
+ let index = 0;
26
+ for (let i = 0; i < phases.length; i += 1) {
27
+ if (time >= phases[i].startTime)
28
+ index = i;
29
+ else
30
+ break;
31
+ }
32
+ const phase = phases[index];
33
+ const prevCenter = index === 0 ? config.center : centerAfter(config, index - 1);
34
+ const target = phase.center ?? prevCenter;
35
+ const elapsed = time - phase.startTime;
36
+ if (elapsed < phase.shrinkDuration && phase.shrinkDuration > 0) {
37
+ const t = elapsed / phase.shrinkDuration;
38
+ return {
39
+ center: lerpPoint(prevCenter, target, t),
40
+ radius: lerp(phase.fromRadius, phase.toRadius, t),
41
+ damagePerSecond: phase.damagePerSecond,
42
+ phase: index,
43
+ shrinking: true,
44
+ };
45
+ }
46
+ return {
47
+ center: target,
48
+ radius: phase.toRadius,
49
+ damagePerSecond: phase.damagePerSecond,
50
+ phase: index,
51
+ shrinking: false,
52
+ };
53
+ }
54
+ function centerAfter(config, index) {
55
+ let center = config.center;
56
+ for (let i = 0; i <= index; i += 1) {
57
+ const phase = config.phases[i];
58
+ if (phase.center !== undefined)
59
+ center = phase.center;
60
+ }
61
+ return center;
62
+ }
63
+ export function createRing(config) {
64
+ return {
65
+ at: (time) => ringSampleAt(config, time),
66
+ isOutside: (time, position) => {
67
+ const sample = ringSampleAt(config, time);
68
+ return distance(position, sample.center) > sample.radius;
69
+ },
70
+ distanceOutside: (time, position) => {
71
+ const sample = ringSampleAt(config, time);
72
+ return Math.max(0, distance(position, sample.center) - sample.radius);
73
+ },
74
+ damageOutside: (time, dt, positions) => {
75
+ const sample = ringSampleAt(config, time);
76
+ const hits = [];
77
+ if (dt <= 0 || sample.damagePerSecond <= 0)
78
+ return hits;
79
+ for (const { id, position } of positions) {
80
+ const distanceOutside = distance(position, sample.center) - sample.radius;
81
+ if (distanceOutside > 0) {
82
+ hits.push({ id, damage: sample.damagePerSecond * dt, distanceOutside });
83
+ }
84
+ }
85
+ return hits;
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,6 @@
1
+ export interface RoleSpec {
2
+ id: string;
3
+ count?: number;
4
+ ratio?: number;
5
+ }
6
+ export declare function assignRoles(userIds: readonly string[], roles: readonly RoleSpec[], rng?: () => number): Record<string, string>;
@@ -0,0 +1,32 @@
1
+ import { shuffleWithRng } from "../cards/cardPile.js";
2
+ export function assignRoles(userIds, roles, rng = Math.random) {
3
+ if (roles.length === 0)
4
+ throw new Error("assignRoles needs at least one role");
5
+ const shuffled = shuffleWithRng(userIds, rng);
6
+ const assignment = {};
7
+ let index = 0;
8
+ const countRoles = roles.filter((role) => role.count !== undefined);
9
+ const ratioRoles = roles.filter((role) => role.count === undefined && role.ratio !== undefined);
10
+ const fillRole = roles.find((role) => role.count === undefined && role.ratio === undefined);
11
+ const fallbackRole = fillRole ?? roles[roles.length - 1];
12
+ for (const role of countRoles) {
13
+ const take = Math.max(0, Math.min(role.count, shuffled.length - index));
14
+ for (let i = 0; i < take; i += 1) {
15
+ assignment[shuffled[index]] = role.id;
16
+ index += 1;
17
+ }
18
+ }
19
+ for (const role of ratioRoles) {
20
+ const wanted = Math.round(role.ratio * shuffled.length);
21
+ const take = Math.max(0, Math.min(wanted, shuffled.length - index));
22
+ for (let i = 0; i < take; i += 1) {
23
+ assignment[shuffled[index]] = role.id;
24
+ index += 1;
25
+ }
26
+ }
27
+ while (index < shuffled.length) {
28
+ assignment[shuffled[index]] = fallbackRole.id;
29
+ index += 1;
30
+ }
31
+ return assignment;
32
+ }
@@ -0,0 +1,79 @@
1
+ export type RoundPhase = string;
2
+ /** Default phase-duration shape for the built-in buy/live/end cycle; pass a wider `Record<string, number>` when using a custom `phaseOrder`. */
3
+ export interface RoundPhaseDurations {
4
+ buy: number;
5
+ live: number;
6
+ end: number;
7
+ }
8
+ export interface LossBonusRule {
9
+ base: number;
10
+ step: number;
11
+ max: number;
12
+ }
13
+ /** A team entry with an optional role tag (e.g. "attacker", "defender") retrievable via `RoundState.roleOf`. */
14
+ export interface RoundTeam {
15
+ id: string;
16
+ role?: string;
17
+ }
18
+ export interface RoundConfig<TPhase extends string = RoundPhase> {
19
+ /** Duration in seconds for each phase name in `phaseOrder` (or the default `RoundPhaseDurations` shape). */
20
+ phases: Record<TPhase, number>;
21
+ teams: readonly string[] | readonly RoundTeam[];
22
+ /**
23
+ * Overrides the default `["buy", "live", "end"]` cycle. Phases other than the first and last are
24
+ * "conclude-eligible": `concludeRound`/`evaluate` may settle the round only while the current phase
25
+ * is neither the first nor the last entry, matching the original buy (no conclude) / live (conclude) /
26
+ * end (no conclude, settles the round) roles. The cycle wraps from the last entry back to the first.
27
+ */
28
+ phaseOrder?: readonly TPhase[];
29
+ /** Evaluated by `evaluate()`; returning a team id concludes the round with that team as winner. */
30
+ winCondition?: (state: RoundSnapshot<TPhase>) => string | null;
31
+ maxRounds?: number;
32
+ winReward?: number;
33
+ lossBonus?: LossBonusRule;
34
+ }
35
+ export type RoundEventKind = "phase.start" | "phase.end" | "round.win" | "round.economy" | "match.end";
36
+ export interface RoundEconomy {
37
+ team: string;
38
+ reward: number;
39
+ won: boolean;
40
+ lossStreak: number;
41
+ }
42
+ export interface RoundEvent<TPhase extends string = RoundPhase> {
43
+ kind: RoundEventKind;
44
+ round: number;
45
+ phase?: TPhase;
46
+ nextPhase?: TPhase;
47
+ winner?: string;
48
+ economy?: RoundEconomy[];
49
+ }
50
+ export type PhaseEndHook<TPhase extends string = RoundPhase> = (endingPhase: TPhase, nextPhase: TPhase, round: number) => void;
51
+ export interface RoundSnapshot<TPhase extends string = RoundPhase> {
52
+ round: number;
53
+ phase: TPhase;
54
+ timeLeft: number;
55
+ scores: Record<string, number>;
56
+ lossStreaks: Record<string, number>;
57
+ roles: Record<string, string | undefined>;
58
+ matchOver: boolean;
59
+ }
60
+ export interface RoundState<TPhase extends string = RoundPhase> {
61
+ tick(dt: number): RoundEvent<TPhase>[];
62
+ concludeRound(winner: string): RoundEvent<TPhase>[];
63
+ /** Calls `RoundConfig.winCondition` (a no-op when absent) and concludes the round if it returns a team id. */
64
+ evaluate(): RoundEvent<TPhase>[];
65
+ onPhaseEnd(hook: PhaseEndHook<TPhase>): () => void;
66
+ phase(): TPhase;
67
+ round(): number;
68
+ timeLeft(): number;
69
+ score(team: string): number;
70
+ economyFor(team: string): RoundEconomy;
71
+ /** The role tag configured for `team`, if any. */
72
+ roleOf(team: string): string | undefined;
73
+ snapshot(): RoundSnapshot<TPhase>;
74
+ }
75
+ export declare function lossBonusFor(rule: LossBonusRule | undefined, streak: number): number;
76
+ export declare function createRoundState(config: RoundConfig<RoundPhase>): RoundState<RoundPhase>;
77
+ export declare function createRoundState<TPhase extends string>(config: RoundConfig<TPhase> & {
78
+ phaseOrder: readonly TPhase[];
79
+ }): RoundState<TPhase>;