@its-not-rocket-science/ananke 0.1.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 (311) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/LICENSE +21 -0
  3. package/README.md +2199 -0
  4. package/STABLE_API.md +266 -0
  5. package/dist/src/anatomy/anatomy-compiler.d.ts +14 -0
  6. package/dist/src/anatomy/anatomy-compiler.js +277 -0
  7. package/dist/src/anatomy/anatomy-contracts.d.ts +94 -0
  8. package/dist/src/anatomy/anatomy-contracts.js +1 -0
  9. package/dist/src/anatomy/anatomy-helpers.d.ts +82 -0
  10. package/dist/src/anatomy/anatomy-helpers.js +233 -0
  11. package/dist/src/anatomy/anatomy-schema.d.ts +28 -0
  12. package/dist/src/anatomy/anatomy-schema.js +388 -0
  13. package/dist/src/anatomy/index.d.ts +4 -0
  14. package/dist/src/anatomy/index.js +4 -0
  15. package/dist/src/archetypes.d.ts +87 -0
  16. package/dist/src/archetypes.js +285 -0
  17. package/dist/src/arena.d.ts +173 -0
  18. package/dist/src/arena.js +695 -0
  19. package/dist/src/bridge/bridge-engine.d.ts +46 -0
  20. package/dist/src/bridge/bridge-engine.js +252 -0
  21. package/dist/src/bridge/index.d.ts +4 -0
  22. package/dist/src/bridge/index.js +5 -0
  23. package/dist/src/bridge/interpolation.d.ts +64 -0
  24. package/dist/src/bridge/interpolation.js +130 -0
  25. package/dist/src/bridge/mapping.d.ts +33 -0
  26. package/dist/src/bridge/mapping.js +54 -0
  27. package/dist/src/bridge/types.d.ts +94 -0
  28. package/dist/src/bridge/types.js +2 -0
  29. package/dist/src/campaign.d.ts +141 -0
  30. package/dist/src/campaign.js +235 -0
  31. package/dist/src/channels.d.ts +15 -0
  32. package/dist/src/channels.js +20 -0
  33. package/dist/src/chronicle.d.ts +124 -0
  34. package/dist/src/chronicle.js +232 -0
  35. package/dist/src/collective-activities.d.ts +154 -0
  36. package/dist/src/collective-activities.js +247 -0
  37. package/dist/src/competence/acoustic.d.ts +101 -0
  38. package/dist/src/competence/acoustic.js +242 -0
  39. package/dist/src/competence/catalogue.d.ts +30 -0
  40. package/dist/src/competence/catalogue.js +241 -0
  41. package/dist/src/competence/crafting.d.ts +35 -0
  42. package/dist/src/competence/crafting.js +88 -0
  43. package/dist/src/competence/engineering.d.ts +53 -0
  44. package/dist/src/competence/engineering.js +108 -0
  45. package/dist/src/competence/framework.d.ts +68 -0
  46. package/dist/src/competence/framework.js +694 -0
  47. package/dist/src/competence/index.d.ts +12 -0
  48. package/dist/src/competence/index.js +13 -0
  49. package/dist/src/competence/interspecies.d.ts +81 -0
  50. package/dist/src/competence/interspecies.js +108 -0
  51. package/dist/src/competence/language.d.ts +79 -0
  52. package/dist/src/competence/language.js +115 -0
  53. package/dist/src/competence/naturalist.d.ts +97 -0
  54. package/dist/src/competence/naturalist.js +187 -0
  55. package/dist/src/competence/navigation.d.ts +24 -0
  56. package/dist/src/competence/navigation.js +48 -0
  57. package/dist/src/competence/performance.d.ts +125 -0
  58. package/dist/src/competence/performance.js +210 -0
  59. package/dist/src/competence/teaching.d.ts +64 -0
  60. package/dist/src/competence/teaching.js +121 -0
  61. package/dist/src/competence/willpower.d.ts +74 -0
  62. package/dist/src/competence/willpower.js +114 -0
  63. package/dist/src/crafting/index.d.ts +55 -0
  64. package/dist/src/crafting/index.js +229 -0
  65. package/dist/src/crafting/manufacturing.d.ts +83 -0
  66. package/dist/src/crafting/manufacturing.js +165 -0
  67. package/dist/src/crafting/materials.d.ts +53 -0
  68. package/dist/src/crafting/materials.js +120 -0
  69. package/dist/src/crafting/recipes.d.ts +75 -0
  70. package/dist/src/crafting/recipes.js +233 -0
  71. package/dist/src/crafting/workshops.d.ts +61 -0
  72. package/dist/src/crafting/workshops.js +170 -0
  73. package/dist/src/debug.d.ts +86 -0
  74. package/dist/src/debug.js +76 -0
  75. package/dist/src/derive.d.ts +21 -0
  76. package/dist/src/derive.js +88 -0
  77. package/dist/src/describe.d.ts +29 -0
  78. package/dist/src/describe.js +276 -0
  79. package/dist/src/dialogue.d.ts +122 -0
  80. package/dist/src/dialogue.js +266 -0
  81. package/dist/src/dist.d.ts +20 -0
  82. package/dist/src/dist.js +39 -0
  83. package/dist/src/downtime.d.ts +89 -0
  84. package/dist/src/downtime.js +391 -0
  85. package/dist/src/economy.d.ts +116 -0
  86. package/dist/src/economy.js +182 -0
  87. package/dist/src/emotional-contagion.d.ts +142 -0
  88. package/dist/src/emotional-contagion.js +274 -0
  89. package/dist/src/equipment.d.ts +206 -0
  90. package/dist/src/equipment.js +598 -0
  91. package/dist/src/faction.d.ts +102 -0
  92. package/dist/src/faction.js +237 -0
  93. package/dist/src/generate.d.ts +35 -0
  94. package/dist/src/generate.js +166 -0
  95. package/dist/src/index.d.ts +42 -0
  96. package/dist/src/index.js +54 -0
  97. package/dist/src/inheritance.d.ts +69 -0
  98. package/dist/src/inheritance.js +136 -0
  99. package/dist/src/inventory.d.ts +194 -0
  100. package/dist/src/inventory.js +637 -0
  101. package/dist/src/item-durability.d.ts +69 -0
  102. package/dist/src/item-durability.js +308 -0
  103. package/dist/src/legend.d.ts +97 -0
  104. package/dist/src/legend.js +269 -0
  105. package/dist/src/lod.d.ts +9 -0
  106. package/dist/src/lod.js +84 -0
  107. package/dist/src/metrics.d.ts +51 -0
  108. package/dist/src/metrics.js +91 -0
  109. package/dist/src/model3d.d.ts +138 -0
  110. package/dist/src/model3d.js +214 -0
  111. package/dist/src/mythology.d.ts +101 -0
  112. package/dist/src/mythology.js +308 -0
  113. package/dist/src/narrative-render.d.ts +42 -0
  114. package/dist/src/narrative-render.js +194 -0
  115. package/dist/src/narrative-stress.d.ts +123 -0
  116. package/dist/src/narrative-stress.js +183 -0
  117. package/dist/src/narrative.d.ts +44 -0
  118. package/dist/src/narrative.js +257 -0
  119. package/dist/src/party.d.ts +70 -0
  120. package/dist/src/party.js +226 -0
  121. package/dist/src/polity.d.ts +262 -0
  122. package/dist/src/polity.js +398 -0
  123. package/dist/src/presets.d.ts +42 -0
  124. package/dist/src/presets.js +170 -0
  125. package/dist/src/progression.d.ts +170 -0
  126. package/dist/src/progression.js +256 -0
  127. package/dist/src/quest-generators.d.ts +76 -0
  128. package/dist/src/quest-generators.js +534 -0
  129. package/dist/src/quest.d.ts +239 -0
  130. package/dist/src/quest.js +520 -0
  131. package/dist/src/relationships-effects.d.ts +75 -0
  132. package/dist/src/relationships-effects.js +219 -0
  133. package/dist/src/relationships.d.ts +104 -0
  134. package/dist/src/relationships.js +347 -0
  135. package/dist/src/replay.d.ts +47 -0
  136. package/dist/src/replay.js +82 -0
  137. package/dist/src/rng.d.ts +9 -0
  138. package/dist/src/rng.js +37 -0
  139. package/dist/src/settlement-services.d.ts +67 -0
  140. package/dist/src/settlement-services.js +267 -0
  141. package/dist/src/settlement.d.ts +143 -0
  142. package/dist/src/settlement.js +419 -0
  143. package/dist/src/sim/action.d.ts +28 -0
  144. package/dist/src/sim/action.js +12 -0
  145. package/dist/src/sim/aging.d.ts +95 -0
  146. package/dist/src/sim/aging.js +243 -0
  147. package/dist/src/sim/ai/decide.d.ts +10 -0
  148. package/dist/src/sim/ai/decide.js +267 -0
  149. package/dist/src/sim/ai/perception.d.ts +12 -0
  150. package/dist/src/sim/ai/perception.js +54 -0
  151. package/dist/src/sim/ai/personality.d.ts +54 -0
  152. package/dist/src/sim/ai/personality.js +202 -0
  153. package/dist/src/sim/ai/presets.d.ts +2 -0
  154. package/dist/src/sim/ai/presets.js +28 -0
  155. package/dist/src/sim/ai/system.d.ts +6 -0
  156. package/dist/src/sim/ai/system.js +13 -0
  157. package/dist/src/sim/ai/targeting.d.ts +8 -0
  158. package/dist/src/sim/ai/targeting.js +42 -0
  159. package/dist/src/sim/ai/types.d.ts +14 -0
  160. package/dist/src/sim/ai/types.js +1 -0
  161. package/dist/src/sim/body.d.ts +9 -0
  162. package/dist/src/sim/body.js +32 -0
  163. package/dist/src/sim/bodyplan.d.ts +161 -0
  164. package/dist/src/sim/bodyplan.js +677 -0
  165. package/dist/src/sim/capability.d.ts +135 -0
  166. package/dist/src/sim/capability.js +8 -0
  167. package/dist/src/sim/combat.d.ts +21 -0
  168. package/dist/src/sim/combat.js +77 -0
  169. package/dist/src/sim/commandBuilders.d.ts +11 -0
  170. package/dist/src/sim/commandBuilders.js +39 -0
  171. package/dist/src/sim/commands.d.ts +71 -0
  172. package/dist/src/sim/commands.js +8 -0
  173. package/dist/src/sim/condition.d.ts +35 -0
  174. package/dist/src/sim/condition.js +21 -0
  175. package/dist/src/sim/cone.d.ts +40 -0
  176. package/dist/src/sim/cone.js +44 -0
  177. package/dist/src/sim/context.d.ts +68 -0
  178. package/dist/src/sim/context.js +1 -0
  179. package/dist/src/sim/density.d.ts +14 -0
  180. package/dist/src/sim/density.js +33 -0
  181. package/dist/src/sim/disease.d.ts +141 -0
  182. package/dist/src/sim/disease.js +353 -0
  183. package/dist/src/sim/entity.d.ts +251 -0
  184. package/dist/src/sim/entity.js +19 -0
  185. package/dist/src/sim/events.d.ts +25 -0
  186. package/dist/src/sim/events.js +5 -0
  187. package/dist/src/sim/explosion.d.ts +40 -0
  188. package/dist/src/sim/explosion.js +40 -0
  189. package/dist/src/sim/formation-unit.d.ts +138 -0
  190. package/dist/src/sim/formation-unit.js +197 -0
  191. package/dist/src/sim/formation.d.ts +12 -0
  192. package/dist/src/sim/formation.js +54 -0
  193. package/dist/src/sim/frontage.d.ts +30 -0
  194. package/dist/src/sim/frontage.js +84 -0
  195. package/dist/src/sim/grapple.d.ts +100 -0
  196. package/dist/src/sim/grapple.js +480 -0
  197. package/dist/src/sim/hazard.d.ts +104 -0
  198. package/dist/src/sim/hazard.js +201 -0
  199. package/dist/src/sim/hydrostatic.d.ts +58 -0
  200. package/dist/src/sim/hydrostatic.js +117 -0
  201. package/dist/src/sim/impairment.d.ts +20 -0
  202. package/dist/src/sim/impairment.js +162 -0
  203. package/dist/src/sim/indexing.d.ts +7 -0
  204. package/dist/src/sim/indexing.js +7 -0
  205. package/dist/src/sim/injury.d.ts +54 -0
  206. package/dist/src/sim/injury.js +66 -0
  207. package/dist/src/sim/intent.d.ts +26 -0
  208. package/dist/src/sim/intent.js +7 -0
  209. package/dist/src/sim/kernel.d.ts +45 -0
  210. package/dist/src/sim/kernel.js +1992 -0
  211. package/dist/src/sim/kinds.d.ts +64 -0
  212. package/dist/src/sim/kinds.js +56 -0
  213. package/dist/src/sim/knockback.d.ts +50 -0
  214. package/dist/src/sim/knockback.js +82 -0
  215. package/dist/src/sim/limb.d.ts +48 -0
  216. package/dist/src/sim/limb.js +78 -0
  217. package/dist/src/sim/medical.d.ts +32 -0
  218. package/dist/src/sim/medical.js +33 -0
  219. package/dist/src/sim/morale.d.ts +69 -0
  220. package/dist/src/sim/morale.js +92 -0
  221. package/dist/src/sim/mount.d.ts +150 -0
  222. package/dist/src/sim/mount.js +225 -0
  223. package/dist/src/sim/nutrition.d.ts +74 -0
  224. package/dist/src/sim/nutrition.js +168 -0
  225. package/dist/src/sim/occlusion.d.ts +8 -0
  226. package/dist/src/sim/occlusion.js +71 -0
  227. package/dist/src/sim/push.d.ts +11 -0
  228. package/dist/src/sim/push.js +79 -0
  229. package/dist/src/sim/ranged.d.ts +44 -0
  230. package/dist/src/sim/ranged.js +69 -0
  231. package/dist/src/sim/seeds.d.ts +3 -0
  232. package/dist/src/sim/seeds.js +16 -0
  233. package/dist/src/sim/sensory-extended.d.ts +103 -0
  234. package/dist/src/sim/sensory-extended.js +181 -0
  235. package/dist/src/sim/sensory.d.ts +38 -0
  236. package/dist/src/sim/sensory.js +109 -0
  237. package/dist/src/sim/skills.d.ts +70 -0
  238. package/dist/src/sim/skills.js +69 -0
  239. package/dist/src/sim/sleep.d.ts +107 -0
  240. package/dist/src/sim/sleep.js +215 -0
  241. package/dist/src/sim/spatial.d.ts +8 -0
  242. package/dist/src/sim/spatial.js +59 -0
  243. package/dist/src/sim/step/capability.d.ts +8 -0
  244. package/dist/src/sim/step/capability.js +77 -0
  245. package/dist/src/sim/step/concentration.d.ts +9 -0
  246. package/dist/src/sim/step/concentration.js +25 -0
  247. package/dist/src/sim/step/effects.d.ts +17 -0
  248. package/dist/src/sim/step/effects.js +96 -0
  249. package/dist/src/sim/step/energy.d.ts +3 -0
  250. package/dist/src/sim/step/energy.js +31 -0
  251. package/dist/src/sim/step/hazards.d.ts +4 -0
  252. package/dist/src/sim/step/hazards.js +19 -0
  253. package/dist/src/sim/step/injury.d.ts +10 -0
  254. package/dist/src/sim/step/injury.js +353 -0
  255. package/dist/src/sim/step/morale.d.ts +11 -0
  256. package/dist/src/sim/step/morale.js +130 -0
  257. package/dist/src/sim/step/movement.d.ts +5 -0
  258. package/dist/src/sim/step/movement.js +172 -0
  259. package/dist/src/sim/step/push.d.ts +11 -0
  260. package/dist/src/sim/step/push.js +79 -0
  261. package/dist/src/sim/step/substances.d.ts +3 -0
  262. package/dist/src/sim/step/substances.js +75 -0
  263. package/dist/src/sim/substance.d.ts +38 -0
  264. package/dist/src/sim/substance.js +57 -0
  265. package/dist/src/sim/systemic-toxicology.d.ts +109 -0
  266. package/dist/src/sim/systemic-toxicology.js +263 -0
  267. package/dist/src/sim/team.d.ts +9 -0
  268. package/dist/src/sim/team.js +37 -0
  269. package/dist/src/sim/tech.d.ts +36 -0
  270. package/dist/src/sim/tech.js +46 -0
  271. package/dist/src/sim/terrain.d.ts +121 -0
  272. package/dist/src/sim/terrain.js +141 -0
  273. package/dist/src/sim/testing.d.ts +13 -0
  274. package/dist/src/sim/testing.js +100 -0
  275. package/dist/src/sim/thermoregulation.d.ts +77 -0
  276. package/dist/src/sim/thermoregulation.js +161 -0
  277. package/dist/src/sim/tick.d.ts +3 -0
  278. package/dist/src/sim/tick.js +3 -0
  279. package/dist/src/sim/toxicology.d.ts +52 -0
  280. package/dist/src/sim/toxicology.js +104 -0
  281. package/dist/src/sim/trace.d.ts +141 -0
  282. package/dist/src/sim/trace.js +1 -0
  283. package/dist/src/sim/tuning.d.ts +16 -0
  284. package/dist/src/sim/tuning.js +42 -0
  285. package/dist/src/sim/vec3.d.ts +14 -0
  286. package/dist/src/sim/vec3.js +31 -0
  287. package/dist/src/sim/weapon_dynamics.d.ts +102 -0
  288. package/dist/src/sim/weapon_dynamics.js +142 -0
  289. package/dist/src/sim/weather.d.ts +95 -0
  290. package/dist/src/sim/weather.js +105 -0
  291. package/dist/src/sim/world.d.ts +52 -0
  292. package/dist/src/sim/world.js +1 -0
  293. package/dist/src/sim/wound-aging.d.ts +120 -0
  294. package/dist/src/sim/wound-aging.js +223 -0
  295. package/dist/src/species.d.ts +106 -0
  296. package/dist/src/species.js +664 -0
  297. package/dist/src/story-arcs.d.ts +17 -0
  298. package/dist/src/story-arcs.js +276 -0
  299. package/dist/src/tech-diffusion.d.ts +80 -0
  300. package/dist/src/tech-diffusion.js +185 -0
  301. package/dist/src/traits.d.ts +25 -0
  302. package/dist/src/traits.js +178 -0
  303. package/dist/src/types.d.ts +117 -0
  304. package/dist/src/types.js +1 -0
  305. package/dist/src/units.d.ts +41 -0
  306. package/dist/src/units.js +64 -0
  307. package/dist/src/weapons.d.ts +20 -0
  308. package/dist/src/weapons.js +824 -0
  309. package/dist/src/world-generation.d.ts +52 -0
  310. package/dist/src/world-generation.js +301 -0
  311. package/package.json +74 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Phase 6 — Formation System
3
+ *
4
+ * Pure computation module for tactical formation mechanics:
5
+ * - Shield walls: adjacent shield-bearers pool block coverage
6
+ * - Rank depth: entities sorted by forward projection; front/rear split
7
+ * - Casualty fill: rear-rank entities promote when front rank is lost
8
+ * - Push of pike: total formation momentum from mass × velocity
9
+ * - Formation cohesion: morale bonus / rout-contagion penalty
10
+ *
11
+ * No Entity or WorldState imports — callers extract the needed values and
12
+ * pass plain numbers / maps. All arithmetic is integer fixed-point.
13
+ */
14
+ import { SCALE, q, clampQ, mulDiv, qMul } from "../../src/units.js";
15
+ // ─── Constants ────────────────────────────────────────────────────────────────
16
+ /**
17
+ * Efficiency of shield coverage sharing between adjacent bearers.
18
+ * Each additional bearer beyond the first contributes at this fraction.
19
+ */
20
+ export const SHIELD_SHARING_FRAC = q(0.60);
21
+ /** Hard cap on combined shield wall coverage. */
22
+ export const SHIELD_WALL_MAX_COVERAGE = SCALE.Q;
23
+ /** Default rank depth used to split front/rear ranks (2 m in SCALE.m). */
24
+ export const RANK_DEPTH_DEFAULT_m = Math.round(2.0 * SCALE.m);
25
+ /**
26
+ * Minimum intact fraction for a formation to be considered cohesive.
27
+ * Below this threshold the rout-contagion penalty applies instead of the
28
+ * morale bonus.
29
+ */
30
+ export const FORMATION_INTACT_THRESHOLD = q(0.60);
31
+ /** Fear decay bonus per tick granted to entities in an intact formation. */
32
+ export const FORMATION_MORALE_BONUS = q(0.008);
33
+ /** Fear increment per tick when formation integrity has collapsed. */
34
+ export const FORMATION_MORALE_PENALTY = q(0.010);
35
+ /** Per-tick fear decay granted per alive formation ally (vs q(0.002) unaffiliated). */
36
+ export const FORMATION_ALLY_FEAR_DECAY = q(0.004);
37
+ /** Maximum number of formation allies counted for the fear decay bonus. */
38
+ export const FORMATION_ALLY_DECAY_CAP = 8;
39
+ // ─── Shield wall ──────────────────────────────────────────────────────────────
40
+ /**
41
+ * Compute the combined block coverage of a shield wall.
42
+ *
43
+ * The bearer with the highest individual coverage contributes at full strength.
44
+ * Each subsequent bearer (sorted descending by coverage) contributes at
45
+ * SHIELD_SHARING_FRAC efficiency, modelling timing gaps and partial overlap
46
+ * between adjacent shields.
47
+ *
48
+ * Result is capped at SHIELD_WALL_MAX_COVERAGE = q(1.0).
49
+ */
50
+ export function computeShieldWallCoverage(coverageFracs) {
51
+ if (coverageFracs.length === 0)
52
+ return q(0);
53
+ // Sort descending so the highest coverage bearer contributes fully.
54
+ const sorted = [...coverageFracs].sort((a, b) => b - a);
55
+ let total = sorted[0];
56
+ for (let i = 1; i < sorted.length; i++) {
57
+ total += mulDiv(sorted[i], SHIELD_SHARING_FRAC, SCALE.Q);
58
+ }
59
+ return clampQ(Math.round(total), 0, SHIELD_WALL_MAX_COVERAGE);
60
+ }
61
+ // ─── Rank depth ───────────────────────────────────────────────────────────────
62
+ /**
63
+ * Split formation entity IDs into front and rear ranks.
64
+ *
65
+ * Each entity position is projected onto the facing direction. The entity
66
+ * with the greatest projection defines depth = 0. Entities within
67
+ * `rankDepth_m` of the frontmost entity are placed in the front rank; the
68
+ * rest go to the rear rank. Both ranks are sorted front-to-back.
69
+ *
70
+ * @param entityIds IDs to split
71
+ * @param positions Map: entity ID → {x, y} in SCALE.m
72
+ * @param facingDirQ Unit vector with components in Q units (SCALE.Q = 1.0)
73
+ * @param rankDepth_m Depth of front rank in SCALE.m (default: RANK_DEPTH_DEFAULT_m)
74
+ */
75
+ export function deriveRankSplit(entityIds, positions, facingDirQ, rankDepth_m = RANK_DEPTH_DEFAULT_m) {
76
+ if (entityIds.length === 0)
77
+ return { frontRank: [], rearRank: [] };
78
+ // Project each entity onto the facing direction (result in SCALE.m).
79
+ const projections = new Map();
80
+ for (const id of entityIds) {
81
+ const pos = positions.get(id);
82
+ if (!pos) {
83
+ projections.set(id, 0);
84
+ continue;
85
+ }
86
+ const proj = Math.trunc((pos.x * facingDirQ.x + pos.y * facingDirQ.y) / SCALE.Q);
87
+ projections.set(id, proj);
88
+ }
89
+ // Frontmost entity has the largest projection.
90
+ let maxProj = -Infinity;
91
+ for (const proj of projections.values()) {
92
+ if (proj > maxProj)
93
+ maxProj = proj;
94
+ }
95
+ const frontRank = [];
96
+ const rearRank = [];
97
+ for (const id of entityIds) {
98
+ const proj = projections.get(id) ?? 0;
99
+ if (proj >= maxProj - rankDepth_m) {
100
+ frontRank.push(id);
101
+ }
102
+ else {
103
+ rearRank.push(id);
104
+ }
105
+ }
106
+ // Sort front-to-back (highest projection first) for determinism.
107
+ const byProjDesc = (a, b) => (projections.get(b) ?? 0) - (projections.get(a) ?? 0);
108
+ frontRank.sort(byProjDesc);
109
+ rearRank.sort(byProjDesc);
110
+ return { frontRank, rearRank };
111
+ }
112
+ // ─── Casualty fill ────────────────────────────────────────────────────────────
113
+ /**
114
+ * Promote rear-rank entities to fill vacancies left by front-rank losses.
115
+ *
116
+ * Returns new front and rear rank arrays with dead entities removed and
117
+ * replacements drawn from the front of the (alive) rear rank.
118
+ */
119
+ export function stepFormationCasualtyFill(rankSplit, deadIds) {
120
+ const aliveFront = rankSplit.frontRank.filter(id => !deadIds.has(id));
121
+ const aliveRear = rankSplit.rearRank.filter(id => !deadIds.has(id));
122
+ const vacancies = rankSplit.frontRank.length - aliveFront.length;
123
+ const replacements = aliveRear.splice(0, vacancies);
124
+ return { frontRank: [...aliveFront, ...replacements], rearRank: aliveRear };
125
+ }
126
+ // ─── Push of pike / formation momentum ───────────────────────────────────────
127
+ /**
128
+ * Compute the total forward momentum of a formation (push-of-pike model).
129
+ *
130
+ * `momentum_Skg_mps` is in (SCALE.kg × m/s) units — i.e. fixed-point mass
131
+ * multiplied by real-valued speed (speed already divided by SCALE.mps).
132
+ * Divide by SCALE.kg (= 1000) to obtain physical kg·m/s.
133
+ *
134
+ * Only entities with speed > 0 contribute.
135
+ *
136
+ * @param masses_Skg Entity masses in SCALE.kg units
137
+ * @param speeds_Smps Entity speeds (magnitude) in SCALE.mps units
138
+ */
139
+ export function computeFormationMomentum(masses_Skg, speeds_Smps) {
140
+ const n = Math.min(masses_Skg.length, speeds_Smps.length);
141
+ let sum = 0;
142
+ let count = 0;
143
+ for (let i = 0; i < n; i++) {
144
+ const speed = speeds_Smps[i];
145
+ if (speed <= 0)
146
+ continue;
147
+ sum += Math.trunc(masses_Skg[i] * speed / SCALE.mps);
148
+ count++;
149
+ }
150
+ return { momentum_Skg_mps: sum, entityCount: count };
151
+ }
152
+ // ─── Formation cohesion / morale ─────────────────────────────────────────────
153
+ /**
154
+ * Derive the morale state of a formation from casualty / rout status.
155
+ *
156
+ * An entity is counted as lost if its ID appears in `deadOrRoutedIds`.
157
+ * When intactFrac_Q ≥ FORMATION_INTACT_THRESHOLD the formation grants a fear
158
+ * decay bonus; below it a rout-contagion penalty applies instead.
159
+ *
160
+ * An empty formation is considered vacuously intact but grants no bonus.
161
+ */
162
+ export function deriveFormationCohesion(entityIds, deadOrRoutedIds) {
163
+ const total = entityIds.length;
164
+ if (total === 0) {
165
+ return {
166
+ intact: true,
167
+ intactFrac_Q: SCALE.Q,
168
+ moraleBonus_Q: q(0),
169
+ moralePenalty_Q: q(0),
170
+ };
171
+ }
172
+ let lostCount = 0;
173
+ for (const id of entityIds) {
174
+ if (deadOrRoutedIds.has(id))
175
+ lostCount++;
176
+ }
177
+ const intactCount = total - lostCount;
178
+ const intactFrac_Q = clampQ(Math.round((intactCount * SCALE.Q) / total), 0, SCALE.Q);
179
+ const intact = intactFrac_Q >= FORMATION_INTACT_THRESHOLD;
180
+ return {
181
+ intact,
182
+ intactFrac_Q,
183
+ moraleBonus_Q: intact ? FORMATION_MORALE_BONUS : q(0),
184
+ moralePenalty_Q: intact ? q(0) : FORMATION_MORALE_PENALTY,
185
+ };
186
+ }
187
+ /**
188
+ * Compute the per-tick fear decay bonus from alive formation allies.
189
+ *
190
+ * Returns a larger bonus than the unaffiliated ally coefficient (q(0.002)):
191
+ * formation allies grant q(0.004) each, capped at FORMATION_ALLY_DECAY_CAP
192
+ * allies. Result is a Q value to be added to the entity's fear decay term.
193
+ */
194
+ export function deriveFormationAllyFearDecay(aliveFormationAllyCount) {
195
+ const capped = Math.min(aliveFormationAllyCount, FORMATION_ALLY_DECAY_CAP);
196
+ return clampQ((capped * FORMATION_ALLY_FEAR_DECAY), 0, qMul(FORMATION_ALLY_FEAR_DECAY, q(FORMATION_ALLY_DECAY_CAP)));
197
+ }
@@ -0,0 +1,12 @@
1
+ import type { Entity } from "./entity.js";
2
+ import type { WorldIndex } from "./indexing.js";
3
+ import type { SpatialIndex } from "./spatial.js";
4
+ import type { WorldState } from "./world.js";
5
+ export interface EngagementQuery {
6
+ reach_m: number;
7
+ buffer_m: number;
8
+ maxTargets: number;
9
+ requireFrontArc?: boolean;
10
+ minDotQ?: number;
11
+ }
12
+ export declare function pickNearestEnemyInReach(world: WorldState | undefined, attacker: Entity, index: WorldIndex, spatial: SpatialIndex, q: EngagementQuery): Entity | undefined;
@@ -0,0 +1,54 @@
1
+ import { queryNearbyIds } from "./spatial.js";
2
+ import { isEnemy, areEntitiesHostile } from "./team.js";
3
+ import { dotDirQ, normaliseDirCheapQ } from "./vec3.js"; // wherever you keep these
4
+ export function pickNearestEnemyInReach(world, attacker, index, spatial, q) {
5
+ const radius_m = q.reach_m + q.buffer_m;
6
+ const ids = queryNearbyIds(spatial, attacker.position_m, radius_m);
7
+ ids.sort((a, b) => a - b);
8
+ // Collect candidates
9
+ const cand = [];
10
+ for (const id of ids) {
11
+ if (id === attacker.id)
12
+ continue;
13
+ const e = index.byId.get(id);
14
+ if (!e || e.injury.dead)
15
+ continue;
16
+ const hostile = world ? areEntitiesHostile(attacker, e, world) : isEnemy(attacker, e);
17
+ if (!hostile)
18
+ continue;
19
+ if (q.requireFrontArc) {
20
+ const dx = e.position_m.x - attacker.position_m.x;
21
+ const dy = e.position_m.y - attacker.position_m.y;
22
+ const dz = e.position_m.z - attacker.position_m.z;
23
+ const dir = normaliseDirCheapQ({ x: dx, y: dy, z: dz });
24
+ const dot = dotDirQ(attacker.action.facingDirQ, dir);
25
+ const minDot = q.minDotQ ?? 0;
26
+ if (dot < minDot)
27
+ continue;
28
+ }
29
+ cand.push(e);
30
+ if (cand.length >= q.maxTargets) {
31
+ break;
32
+ }
33
+ }
34
+ if (cand.length === 0)
35
+ return undefined;
36
+ // Deterministic pick: smallest distance², tie by id
37
+ let best = cand[0];
38
+ let bestD2 = dist2(attacker, best);
39
+ for (let i = 1; i < cand.length; i++) {
40
+ const e = cand[i];
41
+ const d2 = dist2(attacker, e);
42
+ if (d2 < bestD2 || (d2 === bestD2 && e.id < best.id)) {
43
+ best = e;
44
+ bestD2 = d2;
45
+ }
46
+ }
47
+ return best;
48
+ }
49
+ function dist2(a, b) {
50
+ const dx = b.position_m.x - a.position_m.x;
51
+ const dy = b.position_m.y - a.position_m.y;
52
+ const dz = b.position_m.z - a.position_m.z;
53
+ return BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
54
+ }
@@ -0,0 +1,30 @@
1
+ import type { ImpactEvent } from "./events.js";
2
+ import type { WorldIndex } from "./indexing.js";
3
+ import type { ObstacleGrid } from "./terrain.js";
4
+ export interface FrontageRules {
5
+ maxEngagersPerTarget: number;
6
+ }
7
+ export declare function applyFrontageCap(impacts: ImpactEvent[], index: WorldIndex, rules: FrontageRules): ImpactEvent[];
8
+ /**
9
+ * Phase 32E: Compute the passable frontage width (in entity-widths) through a
10
+ * corridor at `position_m` perpendicular to `facingDir`.
11
+ *
12
+ * Scans cells across the perpendicular axis within `scanRange_m` and counts
13
+ * cells where cover < SCALE.Q (not fully impassable). Returns the count as a
14
+ * rough maximum engager cap. Falls back to `defaultCap` when no grid is provided.
15
+ *
16
+ * @param obstacleGrid Obstacle grid (may be undefined)
17
+ * @param cellSize_m Grid cell size in SCALE.m units
18
+ * @param position_m Centre of the corridor query
19
+ * @param facingDir Unit direction of movement (used to determine perpendicular)
20
+ * @param scanRange_m How far left/right to scan (SCALE.m)
21
+ * @param entityWidth_m Average entity body width (SCALE.m; default 0.5 m)
22
+ * @param defaultCap Cap to return when obstacleGrid is undefined
23
+ */
24
+ export declare function computeChokeCapacity(obstacleGrid: ObstacleGrid | undefined, cellSize_m: number, position_m: {
25
+ x: number;
26
+ y: number;
27
+ }, facingDir: {
28
+ x: number;
29
+ y: number;
30
+ }, scanRange_m: number, entityWidth_m?: number, defaultCap?: number): number;
@@ -0,0 +1,84 @@
1
+ import { SCALE } from "../units.js";
2
+ import { coverFractionAtPosition } from "./terrain.js";
3
+ export function applyFrontageCap(impacts, index, rules) {
4
+ const maxK = Math.max(1, rules.maxEngagersPerTarget);
5
+ // group by targetId
6
+ const byTarget = new Map();
7
+ for (const ev of impacts) {
8
+ let arr = byTarget.get(ev.targetId);
9
+ if (!arr) {
10
+ arr = [];
11
+ byTarget.set(ev.targetId, arr);
12
+ }
13
+ arr.push(ev);
14
+ }
15
+ const kept = [];
16
+ for (const [targetId, arr] of byTarget.entries()) {
17
+ if (arr.length <= maxK) {
18
+ for (const ev of arr)
19
+ kept.push(ev);
20
+ continue;
21
+ }
22
+ const target = index.byId.get(targetId);
23
+ if (!target)
24
+ continue;
25
+ // sort attackers by distance² then attackerId
26
+ arr.sort((a, b) => {
27
+ const da = dist2ByIds(index, a.attackerId, target);
28
+ const db = dist2ByIds(index, b.attackerId, target);
29
+ if (da < db)
30
+ return -1;
31
+ if (da > db)
32
+ return 1;
33
+ return a.attackerId - b.attackerId;
34
+ });
35
+ for (let i = 0; i < maxK; i++)
36
+ kept.push(arr[i]);
37
+ }
38
+ return kept;
39
+ }
40
+ /**
41
+ * Phase 32E: Compute the passable frontage width (in entity-widths) through a
42
+ * corridor at `position_m` perpendicular to `facingDir`.
43
+ *
44
+ * Scans cells across the perpendicular axis within `scanRange_m` and counts
45
+ * cells where cover < SCALE.Q (not fully impassable). Returns the count as a
46
+ * rough maximum engager cap. Falls back to `defaultCap` when no grid is provided.
47
+ *
48
+ * @param obstacleGrid Obstacle grid (may be undefined)
49
+ * @param cellSize_m Grid cell size in SCALE.m units
50
+ * @param position_m Centre of the corridor query
51
+ * @param facingDir Unit direction of movement (used to determine perpendicular)
52
+ * @param scanRange_m How far left/right to scan (SCALE.m)
53
+ * @param entityWidth_m Average entity body width (SCALE.m; default 0.5 m)
54
+ * @param defaultCap Cap to return when obstacleGrid is undefined
55
+ */
56
+ export function computeChokeCapacity(obstacleGrid, cellSize_m, position_m, facingDir, scanRange_m, entityWidth_m = Math.trunc(0.5 * SCALE.m), defaultCap = 100) {
57
+ if (!obstacleGrid)
58
+ return defaultCap;
59
+ if (cellSize_m <= 0 || entityWidth_m <= 0)
60
+ return defaultCap;
61
+ // Perpendicular to facing (rotate 90°)
62
+ const perpX = -facingDir.y;
63
+ const perpY = facingDir.x;
64
+ let passableCells = 0;
65
+ const steps = Math.max(1, Math.round(scanRange_m / cellSize_m));
66
+ for (let i = -steps; i <= steps; i++) {
67
+ const cx = position_m.x + Math.trunc(perpX * i * cellSize_m);
68
+ const cy = position_m.y + Math.trunc(perpY * i * cellSize_m);
69
+ const cov = coverFractionAtPosition(obstacleGrid, cellSize_m, cx, cy);
70
+ if (cov < SCALE.Q)
71
+ passableCells++;
72
+ }
73
+ const passableWidth = passableCells * cellSize_m;
74
+ return Math.max(1, Math.floor(passableWidth / entityWidth_m));
75
+ }
76
+ function dist2ByIds(index, attackerId, target) {
77
+ const a = index.byId.get(attackerId);
78
+ if (!a)
79
+ return (1n << 62n); // big + safe, avoids magic decimal
80
+ const dx = target.position_m.x - a.position_m.x;
81
+ const dy = target.position_m.y - a.position_m.y;
82
+ const dz = target.position_m.z - a.position_m.z;
83
+ return BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
84
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Phase 2A: Grapple resolution — deterministic close-combat control.
3
+ *
4
+ * Design rules:
5
+ * - All randomness via eventSeed() + salt, never Math.random()
6
+ * - Pair-based ordering: idLo = Math.min(a, b), idHi = Math.max(a, b)
7
+ * - No mutation of entities mid-resolution across independent pairs
8
+ * - All physical quantities in SI fixed-point (SCALE.*)
9
+ */
10
+ import { type Q } from "../units.js";
11
+ import type { Entity } from "./entity.js";
12
+ import type { WorldState } from "./world.js";
13
+ import type { WorldIndex } from "./indexing.js";
14
+ import type { FunctionalState } from "./impairment.js";
15
+ import type { TraceSink } from "./trace.js";
16
+ import type { SimulationTuning } from "./tuning.js";
17
+ import type { ImpactEvent } from "./events.js";
18
+ import type { Weapon } from "../equipment.js";
19
+ export declare const GRIP_DECAY_PER_TICK: Q;
20
+ export declare const GRAPPLE_THROW_WPN: Weapon;
21
+ export declare const GRAPPLE_JOINTLOCK_WPN: Weapon;
22
+ /**
23
+ * Compute an entity's grapple contest score in Q [0.05, 0.95].
24
+ *
25
+ * Combines:
26
+ * 50% peak force (normalised to human baseline)
27
+ * 30% technique (controlQuality × stability)
28
+ * 20% body mass (normalised to human baseline)
29
+ *
30
+ * The result is modulated by the entity's current functional state
31
+ * (injury, fatigue) via manipulationMul.
32
+ *
33
+ * A healthy average human scores ≈ q(0.47).
34
+ */
35
+ export declare function grappleContestScore(e: Entity, func: FunctionalState): Q;
36
+ /**
37
+ * Attempt to initiate a grapple on the target.
38
+ *
39
+ * Contest: scoreA × intensity vs scoreB. Success probability centred at 0.50
40
+ * with ±40% swing per unit score difference (mirrors melee hit formula).
41
+ *
42
+ * On success:
43
+ * - Attacker's grapple.holdingTargetId and gripQ are set
44
+ * - Target's grapple.heldByIds is updated (sorted, deduplicated)
45
+ * - Overwhelming leverage differential causes immediate trip (prone + small impact)
46
+ *
47
+ * On failure: grappleCooldownTicks set, energy still drained.
48
+ */
49
+ export declare function resolveGrappleAttempt(world: WorldState, attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning, impacts: ImpactEvent[], trace: TraceSink): void;
50
+ /**
51
+ * Attempt to throw or trip the grappled target.
52
+ *
53
+ * Requires: attacker already holds the target (holdingTargetId === target.id).
54
+ * Success probability based on signed leverage differential.
55
+ *
56
+ * On success: target goes prone, kinetic impact queued, grapple released.
57
+ * On failure: cooldown set, energy still drained.
58
+ *
59
+ * Impact energy ∝ target mass × leverage advantage × intensity (see formula in code).
60
+ */
61
+ export declare function resolveGrappleThrow(world: WorldState, attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning, impacts: ImpactEvent[], trace: TraceSink): void;
62
+ /**
63
+ * Apply a choke hold: accumulates suffocation on the target.
64
+ *
65
+ * Requires position !== "standing" in tactical/sim (must be on the ground).
66
+ * Sufficient grip quality (> 0.60) transitions the position to "pinned" and
67
+ * sets target.condition.pinned.
68
+ */
69
+ export declare function resolveGrappleChoke(attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning): void;
70
+ /**
71
+ * Apply a joint-lock: structural damage to a target limb.
72
+ *
73
+ * Requires position !== "standing" in tactical/sim.
74
+ * Target limb selected deterministically (stable across seeds).
75
+ *
76
+ * Impact energy = peakForce × 0.05 m effective displacement × grip × intensity.
77
+ */
78
+ export declare function resolveGrappleJointLock(world: WorldState, attacker: Entity, target: Entity, intensity: Q, tuning: SimulationTuning, impacts: ImpactEvent[]): void;
79
+ /**
80
+ * Attempt to break free from all current holders.
81
+ *
82
+ * Pair-based: each holder gets an independent contest (lower id owns the seed).
83
+ * On success: releaseGrapple() called for that holder.
84
+ * Energy drained per holder attempt regardless of outcome.
85
+ */
86
+ export declare function resolveBreakGrapple(world: WorldState, breaker: Entity, intensity: Q, tuning: SimulationTuning, index: WorldIndex, trace: TraceSink): void;
87
+ /**
88
+ * Per-tick maintenance for active grapples.
89
+ * Call once per entity per tick (regardless of whether a grapple command was issued).
90
+ *
91
+ * - Drains stamina from the holder
92
+ * - Decays gripQ by GRIP_DECAY_PER_TICK
93
+ * - Releases grapple when grip reaches 0 or target is dead/missing
94
+ */
95
+ export declare function stepGrappleTick(world: WorldState, entity: Entity, index: WorldIndex): void;
96
+ /**
97
+ * Release a grapple link, updating both the holder and (optionally) the target.
98
+ * Safe to call with a null target (e.g. when target entity was already removed).
99
+ */
100
+ export declare function releaseGrapple(holder: Entity, target: Entity | null): void;