@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,91 @@
1
+ export const MOODLE_SEVERITY_ORDER = {
2
+ critical: 0,
3
+ warning: 1,
4
+ neutral: 2,
5
+ good: 3,
6
+ };
7
+ function mergeStack(into, moodle) {
8
+ const existing = into.get(moodle.id);
9
+ if (existing === undefined) {
10
+ into.set(moodle.id, { ...moodle });
11
+ return;
12
+ }
13
+ into.set(moodle.id, {
14
+ ...existing,
15
+ stacks: existing.stacks + moodle.stacks,
16
+ severity: MOODLE_SEVERITY_ORDER[moodle.severity] < MOODLE_SEVERITY_ORDER[existing.severity]
17
+ ? moodle.severity
18
+ : existing.severity,
19
+ });
20
+ }
21
+ /**
22
+ * Merge any number of moodle groups into one stack — meters, ailments, and buffs
23
+ * share this display. Same-id moodles fold together (stacks add, worst severity wins);
24
+ * the result is ordered worst-first so the HUD reads critical statuses at a glance.
25
+ */
26
+ export function stackMoodles(...groups) {
27
+ const merged = new Map();
28
+ for (const group of groups)
29
+ for (const moodle of group)
30
+ mergeStack(merged, moodle);
31
+ return [...merged.values()].sort((a, b) => MOODLE_SEVERITY_ORDER[a.severity] - MOODLE_SEVERITY_ORDER[b.severity]);
32
+ }
33
+ /**
34
+ * A stateful holder for timed status moodles (food buffs, temporary shelter, warmth).
35
+ * Meters and multi-region health derive their own moodles on read; combine all three
36
+ * through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display.
37
+ */
38
+ export function createMoodleStack() {
39
+ const moodles = new Map();
40
+ return {
41
+ add(input) {
42
+ moodles.set(input.id, {
43
+ label: input.label,
44
+ severity: input.severity ?? "good",
45
+ stacks: input.stacks ?? 1,
46
+ ...(input.icon === undefined ? {} : { icon: input.icon }),
47
+ ...(input.note === undefined ? {} : { note: input.note }),
48
+ remaining: input.duration ?? null,
49
+ total: input.duration ?? null,
50
+ });
51
+ },
52
+ remove(id) {
53
+ moodles.delete(id);
54
+ },
55
+ has(id) {
56
+ return moodles.has(id);
57
+ },
58
+ tick(dt) {
59
+ if (dt <= 0)
60
+ return;
61
+ for (const [id, state] of moodles) {
62
+ if (state.remaining === null)
63
+ continue;
64
+ state.remaining -= dt;
65
+ if (state.remaining <= 0)
66
+ moodles.delete(id);
67
+ }
68
+ },
69
+ list() {
70
+ const out = [];
71
+ for (const [id, state] of moodles) {
72
+ out.push({
73
+ id,
74
+ label: state.label,
75
+ severity: state.severity,
76
+ source: "buff",
77
+ stacks: state.stacks,
78
+ ...(state.icon === undefined ? {} : { icon: state.icon }),
79
+ ...(state.note === undefined ? {} : { note: state.note }),
80
+ ...(state.remaining !== null && state.total !== null && state.total > 0
81
+ ? { fraction: Math.max(0, Math.min(1, state.remaining / state.total)) }
82
+ : {}),
83
+ });
84
+ }
85
+ return out;
86
+ },
87
+ clear() {
88
+ moodles.clear();
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,72 @@
1
+ import type { Moodle, MoodleSeverity } from "./moodle.js";
2
+ export interface HealthRegionConfig {
3
+ id: string;
4
+ label: string;
5
+ max: number;
6
+ /** A vital part (head, thorax): the entity dies when it empties. Defaults to false. */
7
+ vital?: boolean;
8
+ /** Extra multiplier on incoming damage — a head takes more per hit. Defaults to 1. */
9
+ vulnerability?: number;
10
+ }
11
+ export interface AilmentConfig {
12
+ id: string;
13
+ label: string;
14
+ /** The part this injury belongs to; drain (bleed) applies here. */
15
+ region?: string;
16
+ severity?: MoodleSeverity;
17
+ icon?: string;
18
+ /** Health lost per game-second per stack while untreated (bleed, infection). */
19
+ drainPerSecond?: number;
20
+ /** Can queue multiple instances (three bleeds stack); otherwise re-applying refreshes. */
21
+ stacking?: boolean;
22
+ /** Treatment item ids that clear this injury — bandage, tourniquet, splint. */
23
+ treatedBy?: readonly string[];
24
+ }
25
+ export interface MultiRegionHealthConfig {
26
+ regions: readonly HealthRegionConfig[];
27
+ /** Ailment catalog keyed by id — the queue references these. */
28
+ ailments?: Record<string, AilmentConfig>;
29
+ }
30
+ export interface RegionHealthState {
31
+ id: string;
32
+ label: string;
33
+ current: number;
34
+ max: number;
35
+ fraction: number;
36
+ vital: boolean;
37
+ }
38
+ export interface AilmentInstance {
39
+ id: string;
40
+ stacks: number;
41
+ }
42
+ export interface DamageResult {
43
+ region: RegionHealthState;
44
+ /** Damage actually applied after vulnerability. */
45
+ applied: number;
46
+ dead: boolean;
47
+ }
48
+ export interface TreatResult {
49
+ /** Ailment ids fully or partially cleared by the item. */
50
+ treated: readonly string[];
51
+ }
52
+ export interface MultiRegionHealth {
53
+ damage(regionId: string, amount: number): DamageResult;
54
+ heal(regionId: string, amount: number): RegionHealthState;
55
+ region(regionId: string): RegionHealthState;
56
+ regions(): RegionHealthState[];
57
+ /** 0..1 aggregate across all parts (share-weighted by max). */
58
+ overall(): number;
59
+ /** Queue an ailment from the catalog; stacking ailments increment, others refresh. */
60
+ applyAilment(ailmentId: string): void;
61
+ /** Clear ailments treated by an item id; one item clears one stack of each match. */
62
+ treat(itemId: string): TreatResult;
63
+ /** Apply ailment drains over game-time `dt`; may kill via a vital part. */
64
+ tick(dt: number): {
65
+ dead: boolean;
66
+ };
67
+ ailments(): AilmentInstance[];
68
+ /** Wounds as moodles for the shared status stack (#78 shares #90's moodle display). */
69
+ ailmentMoodles(): Moodle[];
70
+ readonly dead: boolean;
71
+ }
72
+ export declare function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth;
@@ -0,0 +1,129 @@
1
+ function clamp(value, min, max) {
2
+ return value < min ? min : value > max ? max : value;
3
+ }
4
+ export function createMultiRegionHealth(config) {
5
+ if (config.regions.length === 0) {
6
+ throw new Error("createMultiRegionHealth: regions must be non-empty");
7
+ }
8
+ const regions = new Map();
9
+ const order = [];
10
+ for (const region of config.regions) {
11
+ regions.set(region.id, { config: region, current: region.max });
12
+ order.push(region.id);
13
+ }
14
+ const catalog = config.ailments ?? {};
15
+ const queue = new Map();
16
+ let dead = false;
17
+ const requireRegion = (id) => {
18
+ const runtime = regions.get(id);
19
+ if (runtime === undefined)
20
+ throw new Error(`unknown health region "${id}"`);
21
+ return runtime;
22
+ };
23
+ const toState = (runtime) => ({
24
+ id: runtime.config.id,
25
+ label: runtime.config.label,
26
+ current: runtime.current,
27
+ max: runtime.config.max,
28
+ fraction: runtime.config.max > 0 ? runtime.current / runtime.config.max : 0,
29
+ vital: runtime.config.vital ?? false,
30
+ });
31
+ const applyDamage = (runtime, amount) => {
32
+ const before = runtime.current;
33
+ runtime.current = clamp(runtime.current - amount, 0, runtime.config.max);
34
+ if ((runtime.config.vital ?? false) && runtime.current <= 0)
35
+ dead = true;
36
+ return before - runtime.current;
37
+ };
38
+ return {
39
+ damage(regionId, amount) {
40
+ const runtime = requireRegion(regionId);
41
+ const scaled = Math.max(0, amount) * (runtime.config.vulnerability ?? 1);
42
+ const applied = applyDamage(runtime, scaled);
43
+ return { region: toState(runtime), applied, dead };
44
+ },
45
+ heal(regionId, amount) {
46
+ const runtime = requireRegion(regionId);
47
+ runtime.current = clamp(runtime.current + Math.max(0, amount), 0, runtime.config.max);
48
+ return toState(runtime);
49
+ },
50
+ region(regionId) {
51
+ return toState(requireRegion(regionId));
52
+ },
53
+ regions() {
54
+ return order.map((id) => toState(regions.get(id)));
55
+ },
56
+ overall() {
57
+ let current = 0;
58
+ let max = 0;
59
+ for (const runtime of regions.values()) {
60
+ current += runtime.current;
61
+ max += runtime.config.max;
62
+ }
63
+ return max > 0 ? current / max : 0;
64
+ },
65
+ applyAilment(ailmentId) {
66
+ const def = catalog[ailmentId];
67
+ if (def === undefined)
68
+ throw new Error(`unknown ailment "${ailmentId}"`);
69
+ const current = queue.get(ailmentId) ?? 0;
70
+ queue.set(ailmentId, def.stacking ?? false ? current + 1 : 1);
71
+ },
72
+ treat(itemId) {
73
+ const treated = [];
74
+ for (const [ailmentId, stacks] of queue) {
75
+ const def = catalog[ailmentId];
76
+ if (def === undefined)
77
+ continue;
78
+ if (!(def.treatedBy ?? []).includes(itemId))
79
+ continue;
80
+ treated.push(ailmentId);
81
+ if (stacks <= 1)
82
+ queue.delete(ailmentId);
83
+ else
84
+ queue.set(ailmentId, stacks - 1);
85
+ }
86
+ return { treated };
87
+ },
88
+ tick(dt) {
89
+ if (dt > 0) {
90
+ for (const [ailmentId, stacks] of queue) {
91
+ const def = catalog[ailmentId];
92
+ if (def === undefined || def.drainPerSecond === undefined)
93
+ continue;
94
+ const target = def.region ?? order[0];
95
+ const runtime = regions.get(target);
96
+ if (runtime === undefined)
97
+ continue;
98
+ applyDamage(runtime, def.drainPerSecond * stacks * dt);
99
+ }
100
+ }
101
+ return { dead };
102
+ },
103
+ ailments() {
104
+ return [...queue].map(([id, stacks]) => ({ id, stacks }));
105
+ },
106
+ ailmentMoodles() {
107
+ const out = [];
108
+ for (const [ailmentId, stacks] of queue) {
109
+ const def = catalog[ailmentId];
110
+ if (def === undefined)
111
+ continue;
112
+ const untreated = (def.treatedBy ?? []).length > 0;
113
+ out.push({
114
+ id: ailmentId,
115
+ label: def.label,
116
+ severity: def.severity ?? "warning",
117
+ source: "ailment",
118
+ stacks,
119
+ ...(def.icon === undefined ? {} : { icon: def.icon }),
120
+ ...(untreated ? { note: `needs ${def.treatedBy[0]}` } : {}),
121
+ });
122
+ }
123
+ return out;
124
+ },
125
+ get dead() {
126
+ return dead;
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,62 @@
1
+ export type FallingGridCell = readonly [number, number];
2
+ export interface FallingGridConfig {
3
+ cols: number;
4
+ rows: number;
5
+ /** Milliseconds a grounded piece may sit before `lockExpired` fires. Defaults to 500. */
6
+ lockDelayMs?: number;
7
+ }
8
+ /** Elapsed grounded time for the lock-delay state machine. `null` means airborne / not timing. */
9
+ export interface LockState {
10
+ elapsedMs: number;
11
+ }
12
+ export interface FallingGridSnapshot<TCell> {
13
+ cols: number;
14
+ rows: number;
15
+ cells: readonly (TCell | null)[];
16
+ }
17
+ export interface FallingGrid<TCell> {
18
+ readonly cols: number;
19
+ readonly rows: number;
20
+ occupied(col: number, row: number): boolean;
21
+ cellAt(col: number, row: number): TCell | null;
22
+ setCell(col: number, row: number, value: TCell): void;
23
+ clearCell(col: number, row: number): void;
24
+ /** Bounds + collision check for a footprint (`cells`, relative to `offset`). Rows above the grid (`row < 0`) are allowed through, mirroring a piece spawning above the visible board; only columns are bounds-checked there. */
25
+ canPlace(cells: readonly FallingGridCell[], offset: FallingGridCell): boolean;
26
+ /** Writes `value` into every in-bounds cell of the footprint if `canPlace` allows it. Cells above the grid are silently skipped. Returns whether the placement happened. */
27
+ place(cells: readonly FallingGridCell[], offset: FallingGridCell, value: TCell): boolean;
28
+ /** Row indices that are fully occupied, in ascending order. */
29
+ fullRows(): readonly number[];
30
+ /** Removes the given rows and shifts every row above them down, backfilling the top with empty rows. Returns the number of rows cleared. */
31
+ clearRows(rows: readonly number[]): number;
32
+ /** Column-gravity collapse: every occupied cell in each column falls to the lowest free row below it, preserving relative order. Returns the number of cells that moved. */
33
+ settle(): number;
34
+ /** Advances (or starts) a lock-delay timer while `grounded`, and clears it the instant the piece is airborne. */
35
+ advanceLock(state: LockState | null, grounded: boolean, dtMs: number): LockState | null;
36
+ /** Whether a lock-delay timer has run out `lockDelayMs`. */
37
+ lockExpired(state: LockState | null): boolean;
38
+ snapshot(): FallingGridSnapshot<TCell>;
39
+ /** Clears every cell back to empty. */
40
+ reset(): void;
41
+ }
42
+ export interface GravityIntervalConfig {
43
+ /** Interval at level 0, in milliseconds. Defaults to 800. */
44
+ baseMs?: number;
45
+ /** Milliseconds shaved off per level. Defaults to 70. */
46
+ perLevel?: number;
47
+ /** Floor the interval never drops below, in milliseconds. Defaults to 50. */
48
+ minMs?: number;
49
+ }
50
+ /**
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
53
+ * with configurable base/step/floor.
54
+ */
55
+ export declare function gravityIntervalMs(level: number, config?: GravityIntervalConfig): number;
56
+ /**
57
+ * A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy,
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
60
+ * logic so any falling-block game can adopt it instead of reimplementing the grid.
61
+ */
62
+ export declare function createFallingGrid<TCell>(config: FallingGridConfig): FallingGrid<TCell>;
@@ -0,0 +1,158 @@
1
+ /**
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
4
+ * with configurable base/step/floor.
5
+ */
6
+ export function gravityIntervalMs(level, config = {}) {
7
+ const baseMs = config.baseMs ?? 800;
8
+ const perLevel = config.perLevel ?? 70;
9
+ const minMs = config.minMs ?? 50;
10
+ return Math.max(minMs, baseMs - level * perLevel);
11
+ }
12
+ /**
13
+ * A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy,
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
16
+ * logic so any falling-block game can adopt it instead of reimplementing the grid.
17
+ */
18
+ export function createFallingGrid(config) {
19
+ const cols = config.cols;
20
+ const rows = config.rows;
21
+ const lockDelayMs = config.lockDelayMs ?? 500;
22
+ if (cols <= 0 || rows <= 0)
23
+ throw new Error("falling grid needs positive dimensions");
24
+ let cells = new Array(cols * rows).fill(null);
25
+ function index(col, row) {
26
+ return row * cols + col;
27
+ }
28
+ function inBounds(col, row) {
29
+ return col >= 0 && col < cols && row >= 0 && row < rows;
30
+ }
31
+ function cellAt(col, row) {
32
+ if (!inBounds(col, row))
33
+ return null;
34
+ return cells[index(col, row)] ?? null;
35
+ }
36
+ function occupied(col, row) {
37
+ return cellAt(col, row) !== null;
38
+ }
39
+ function setCell(col, row, value) {
40
+ if (!inBounds(col, row))
41
+ return;
42
+ cells[index(col, row)] = value;
43
+ }
44
+ function clearCell(col, row) {
45
+ if (!inBounds(col, row))
46
+ return;
47
+ cells[index(col, row)] = null;
48
+ }
49
+ function canPlace(footprint, offset) {
50
+ for (const [dc, dr] of footprint) {
51
+ const col = offset[0] + dc;
52
+ const row = offset[1] + dr;
53
+ if (col < 0 || col >= cols || row >= rows)
54
+ return false;
55
+ if (row >= 0 && occupied(col, row))
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+ function place(footprint, offset, value) {
61
+ if (!canPlace(footprint, offset))
62
+ return false;
63
+ for (const [dc, dr] of footprint) {
64
+ const col = offset[0] + dc;
65
+ const row = offset[1] + dr;
66
+ if (row >= 0)
67
+ setCell(col, row, value);
68
+ }
69
+ return true;
70
+ }
71
+ function fullRows() {
72
+ const full = [];
73
+ for (let row = 0; row < rows; row += 1) {
74
+ let rowFull = true;
75
+ for (let col = 0; col < cols; col += 1) {
76
+ if (!occupied(col, row)) {
77
+ rowFull = false;
78
+ break;
79
+ }
80
+ }
81
+ if (rowFull)
82
+ full.push(row);
83
+ }
84
+ return full;
85
+ }
86
+ function clearRows(rowsToClear) {
87
+ const toClear = new Set(rowsToClear);
88
+ if (toClear.size === 0)
89
+ return 0;
90
+ const kept = [];
91
+ for (let row = 0; row < rows; row += 1) {
92
+ if (toClear.has(row))
93
+ continue;
94
+ kept.push(cells.slice(index(0, row), index(0, row) + cols));
95
+ }
96
+ const cleared = rows - kept.length;
97
+ const next = [];
98
+ for (let i = 0; i < cleared; i += 1)
99
+ next.push(...new Array(cols).fill(null));
100
+ for (const row of kept)
101
+ next.push(...row);
102
+ cells = next;
103
+ return cleared;
104
+ }
105
+ function settle() {
106
+ let moved = 0;
107
+ for (let col = 0; col < cols; col += 1) {
108
+ const values = [];
109
+ for (let row = 0; row < rows; row += 1) {
110
+ const value = cellAt(col, row);
111
+ if (value !== null)
112
+ values.push({ row, value });
113
+ }
114
+ for (let row = 0; row < rows; row += 1)
115
+ clearCell(col, row);
116
+ const count = values.length;
117
+ for (let i = 0; i < count; i += 1) {
118
+ const newRow = rows - count + i;
119
+ const { row: oldRow, value } = values[i];
120
+ setCell(col, newRow, value);
121
+ if (oldRow !== newRow)
122
+ moved += 1;
123
+ }
124
+ }
125
+ return moved;
126
+ }
127
+ function advanceLock(state, grounded, dtMs) {
128
+ if (!grounded)
129
+ return null;
130
+ return { elapsedMs: (state?.elapsedMs ?? 0) + dtMs };
131
+ }
132
+ function lockExpired(state) {
133
+ return state !== null && state.elapsedMs >= lockDelayMs;
134
+ }
135
+ function snapshot() {
136
+ return { cols, rows, cells: cells.slice() };
137
+ }
138
+ function reset() {
139
+ cells = new Array(cols * rows).fill(null);
140
+ }
141
+ return {
142
+ cols,
143
+ rows,
144
+ occupied,
145
+ cellAt,
146
+ setCell,
147
+ clearCell,
148
+ canPlace,
149
+ place,
150
+ fullRows,
151
+ clearRows,
152
+ settle,
153
+ advanceLock,
154
+ lockExpired,
155
+ snapshot,
156
+ reset,
157
+ };
158
+ }
@@ -0,0 +1,34 @@
1
+ import { type AreaTargetInput, type CombatSpatialDeps } from "../combat/effects.js";
2
+ import type { EntityPosition } from "../scene/entityStore.js";
3
+ import type { Aim, QueryArcOptions } from "../scene/spatial.js";
4
+ import type { Tile } from "./tacticalGrid.js";
5
+ export interface PredictiveDeps {
6
+ spatial: CombatSpatialDeps;
7
+ queryArc(options: QueryArcOptions): string[];
8
+ canReceive?(instanceId: string, effect: string): string | null;
9
+ }
10
+ export interface PredictedTarget {
11
+ instanceId: string;
12
+ scale: number;
13
+ }
14
+ export interface AreaPredictInput extends AreaTargetInput {
15
+ effect?: string;
16
+ requireReceive?: boolean;
17
+ }
18
+ export interface ArcPredictInput {
19
+ from: string;
20
+ aim: Aim;
21
+ radius: number;
22
+ halfAngleDeg?: number;
23
+ effect?: string;
24
+ requireReceive?: boolean;
25
+ }
26
+ export declare function predictAreaEffect(deps: PredictiveDeps, input: AreaPredictInput): PredictedTarget[];
27
+ export declare function predictArcEffect(deps: PredictiveDeps, input: ArcPredictInput): PredictedTarget[];
28
+ export interface TilePredictInput {
29
+ at: EntityPosition;
30
+ radius: number;
31
+ originTile: Tile;
32
+ tileSize: number;
33
+ }
34
+ export declare function predictTiles(input: TilePredictInput): Tile[];
@@ -0,0 +1,44 @@
1
+ import { resolveAreaTargets, } from "../combat/effects.js";
2
+ function receiveGate(deps, effect, requireReceive) {
3
+ if (!requireReceive || effect === undefined || deps.canReceive === undefined)
4
+ return undefined;
5
+ const gate = deps.canReceive;
6
+ return (instanceId) => gate(instanceId, effect) === null;
7
+ }
8
+ export function predictAreaEffect(deps, input) {
9
+ const requireReceive = input.requireReceive ?? true;
10
+ return resolveAreaTargets(deps.spatial, input, receiveGate(deps, input.effect, requireReceive));
11
+ }
12
+ export function predictArcEffect(deps, input) {
13
+ const requireReceive = input.requireReceive ?? true;
14
+ const gate = receiveGate(deps, input.effect, requireReceive);
15
+ const hit = deps.queryArc({
16
+ from: input.from,
17
+ aim: input.aim,
18
+ radius: input.radius,
19
+ ...(input.halfAngleDeg === undefined ? {} : { halfAngleDeg: input.halfAngleDeg }),
20
+ });
21
+ const targets = [];
22
+ for (const instanceId of hit) {
23
+ if (gate !== undefined && !gate(instanceId))
24
+ continue;
25
+ targets.push({ instanceId, scale: 1 });
26
+ }
27
+ return targets;
28
+ }
29
+ export function predictTiles(input) {
30
+ const reach = Math.ceil(input.radius / input.tileSize);
31
+ const tiles = [];
32
+ for (let dc = -reach; dc <= reach; dc += 1) {
33
+ for (let dr = -reach; dr <= reach; dr += 1) {
34
+ const worldX = input.at[0] + dc * input.tileSize;
35
+ const worldZ = input.at[2] + dr * input.tileSize;
36
+ const dx = worldX - input.at[0];
37
+ const dz = worldZ - input.at[2];
38
+ if (Math.sqrt(dx * dx + dz * dz) <= input.radius) {
39
+ tiles.push([input.originTile[0] + dc, input.originTile[1] + dr]);
40
+ }
41
+ }
42
+ }
43
+ return tiles;
44
+ }
@@ -0,0 +1,18 @@
1
+ export interface SnapshotSlice<T = unknown> {
2
+ capture(): T;
3
+ restore(state: T): void;
4
+ }
5
+ export type Snapshot = Record<string, unknown>;
6
+ export interface SnapshotStore {
7
+ register<T>(id: string, slice: SnapshotSlice<T>): void;
8
+ unregister(id: string): void;
9
+ capture(): Snapshot;
10
+ restore(snapshot: Snapshot): void;
11
+ push(): Snapshot;
12
+ pop(): boolean;
13
+ peek(): Snapshot | null;
14
+ depth(): number;
15
+ clear(): void;
16
+ }
17
+ export declare function deepClone<T>(value: T): T;
18
+ export declare function createSnapshotStore(): SnapshotStore;
@@ -0,0 +1,65 @@
1
+ export function deepClone(value) {
2
+ if (value === null || typeof value !== "object")
3
+ return value;
4
+ if (Array.isArray(value))
5
+ return value.map((item) => deepClone(item));
6
+ if (value instanceof Map) {
7
+ const clone = new Map();
8
+ for (const [k, v] of value)
9
+ clone.set(deepClone(k), deepClone(v));
10
+ return clone;
11
+ }
12
+ if (value instanceof Set) {
13
+ const clone = new Set();
14
+ for (const item of value)
15
+ clone.add(deepClone(item));
16
+ return clone;
17
+ }
18
+ const clone = {};
19
+ for (const [k, v] of Object.entries(value))
20
+ clone[k] = deepClone(v);
21
+ return clone;
22
+ }
23
+ export function createSnapshotStore() {
24
+ const slices = new Map();
25
+ const stack = [];
26
+ function capture() {
27
+ const snapshot = {};
28
+ for (const [id, slice] of slices)
29
+ snapshot[id] = deepClone(slice.capture());
30
+ return snapshot;
31
+ }
32
+ function restore(snapshot) {
33
+ for (const [id, slice] of slices) {
34
+ if (id in snapshot)
35
+ slice.restore(deepClone(snapshot[id]));
36
+ }
37
+ }
38
+ return {
39
+ register: (id, slice) => {
40
+ slices.set(id, slice);
41
+ },
42
+ unregister: (id) => {
43
+ slices.delete(id);
44
+ },
45
+ capture,
46
+ restore,
47
+ push: () => {
48
+ const snapshot = capture();
49
+ stack.push(snapshot);
50
+ return snapshot;
51
+ },
52
+ pop: () => {
53
+ const snapshot = stack.pop();
54
+ if (snapshot === undefined)
55
+ return false;
56
+ restore(snapshot);
57
+ return true;
58
+ },
59
+ peek: () => (stack.length === 0 ? null : stack[stack.length - 1]),
60
+ depth: () => stack.length,
61
+ clear: () => {
62
+ stack.length = 0;
63
+ },
64
+ };
65
+ }