@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,130 @@
1
+ import { SCALE, q, clampQ, qMul, mulDiv } from "../../units.js";
2
+ import { queryNearbyIds } from "../spatial.js";
3
+ import { coverFractionAtPosition } from "../terrain.js";
4
+ import { TraceKinds } from "../kinds.js";
5
+ import { FEAR_PER_SUPPRESSION_TICK, FEAR_FOR_ALLY_DEATH, FEAR_INJURY_MUL, FEAR_OUTNUMBERED, FEAR_ROUTING_CASCADE, fearDecayPerTick, isRouting, LEADER_AURA_FEAR_REDUCTION, BANNER_AURA_FEAR_REDUCTION, AURA_RADIUS_m, RALLY_COOLDOWN_TICKS, } from "../morale.js";
6
+ /**
7
+ * Per-entity morale update — accumulates fear from all sources and applies decay.
8
+ * Emits a MoraleRoute trace event whenever the entity crosses the routing threshold.
9
+ */
10
+ export function stepMoraleForEntity(world, e, index, spatial, aliveBeforeTick, teamRoutingFrac, trace, ctx) {
11
+ if (e.injury.dead)
12
+ return;
13
+ const distressTolBase = e.attributes.resilience.distressTolerance;
14
+ // Phase 33: intrapersonal intelligence boosts effective distress tolerance
15
+ // Formula: base + intrapersonal × q(0.30); human (0.55) → +0.165; clamped to q(0.98)
16
+ const intrapersonal = e.attributes.cognition?.intrapersonal ?? 0;
17
+ const distressTol = intrapersonal
18
+ ? clampQ((distressTolBase + Math.trunc(mulDiv(q(0.30), intrapersonal, SCALE.Q))), q(0.01), q(0.98))
19
+ : distressTolBase;
20
+ const MORALE_RADIUS_m = Math.trunc(30 * SCALE.m); // 30 m awareness radius
21
+ const nearbyIds = queryNearbyIds(spatial, e.position_m, MORALE_RADIUS_m);
22
+ let nearbyAllyCount = 0;
23
+ let nearbyEnemyCount = 0;
24
+ let allyDeathsThisTick = 0;
25
+ for (const nId of nearbyIds) {
26
+ if (nId === e.id)
27
+ continue;
28
+ const neighbor = index.byId.get(nId);
29
+ if (!neighbor)
30
+ continue;
31
+ if (neighbor.teamId === e.teamId) {
32
+ if (!neighbor.injury.dead) {
33
+ nearbyAllyCount++;
34
+ }
35
+ else if (aliveBeforeTick.has(nId)) {
36
+ allyDeathsThisTick++;
37
+ }
38
+ }
39
+ else if (!neighbor.injury.dead) {
40
+ nearbyEnemyCount++;
41
+ }
42
+ }
43
+ // Feature 6: berserk entities ignore all fear — always clear fear and return early
44
+ const fearResponse = (e.attributes.resilience).fearResponse ?? "flight";
45
+ if (fearResponse === "berserk") {
46
+ e.condition.fearQ = q(0);
47
+ return;
48
+ }
49
+ let fearQ = e.condition.fearQ;
50
+ const wasRouting = isRouting(fearQ, distressTol);
51
+ // 1. Suppression ticks add fear per tick — scaled by caliber multiplier (Feature 1)
52
+ if (e.condition.suppressedTicks > 0) {
53
+ const supMul = e.condition.suppressionFearMul ?? SCALE.Q;
54
+ fearQ = clampQ(fearQ + qMul(FEAR_PER_SUPPRESSION_TICK, supMul), 0, SCALE.Q);
55
+ }
56
+ // 2. Ally deaths this tick — with diminishing returns (Feature 2)
57
+ if (allyDeathsThisTick > 0) {
58
+ // Reset window if last death was >100 ticks ago (5s at TICK_HZ=20)
59
+ if (world.tick - e.condition.lastAllyDeathTick > 100) {
60
+ e.condition.recentAllyDeaths = 0;
61
+ }
62
+ // Multiplier: q(1.0) for first death, -q(0.15) per prior, floor q(0.40)
63
+ const mul = Math.max(q(0.40), q(1.0) - Math.trunc(e.condition.recentAllyDeaths * 1500));
64
+ fearQ = clampQ(fearQ + Math.trunc(allyDeathsThisTick * qMul(FEAR_FOR_ALLY_DEATH, mul)), 0, SCALE.Q);
65
+ e.condition.recentAllyDeaths += allyDeathsThisTick;
66
+ e.condition.lastAllyDeathTick = world.tick;
67
+ }
68
+ // 3. Self-injury (shock accumulation) adds fear per tick
69
+ fearQ = clampQ(fearQ + qMul(e.injury.shock, FEAR_INJURY_MUL), 0, SCALE.Q);
70
+ // 4. Being outnumbered by visible enemies adds fear per tick
71
+ // Include self in friendly count: entity + its allies vs enemies.
72
+ if (nearbyEnemyCount > nearbyAllyCount + 1) {
73
+ fearQ = clampQ(fearQ + FEAR_OUTNUMBERED, 0, SCALE.Q);
74
+ }
75
+ // 5. Routing cascade: more than half the team is already routing
76
+ if ((teamRoutingFrac.get(e.teamId) ?? 0) > 0.50) {
77
+ fearQ = clampQ(fearQ + FEAR_ROUTING_CASCADE, 0, SCALE.Q);
78
+ }
79
+ // Fear decay — faster with high tolerance and nearby allies (cohesion)
80
+ fearQ = clampQ(fearQ - fearDecayPerTick(distressTol, nearbyAllyCount), 0, SCALE.Q);
81
+ // Feature 3: leader and standard-bearer aura decay
82
+ // Phase 33: interpersonal intelligence scales effective aura reception radius
83
+ // Formula: base × (0.40 + interpersonal); human (0.60) → ×1.0
84
+ const interpersonal = e.attributes.cognition?.interpersonal ?? 0;
85
+ const effectiveAuraRadius_m = interpersonal
86
+ ? Math.trunc(mulDiv(AURA_RADIUS_m, (4000 + interpersonal), SCALE.Q))
87
+ : AURA_RADIUS_m;
88
+ let leaderCount = 0;
89
+ let bannerCount = 0;
90
+ const auraIds = queryNearbyIds(spatial, e.position_m, effectiveAuraRadius_m);
91
+ for (const aId of auraIds) {
92
+ if (aId === e.id)
93
+ continue;
94
+ const ally = index.byId.get(aId);
95
+ if (!ally || ally.injury.dead || ally.teamId !== e.teamId)
96
+ continue;
97
+ const traits = ally.traits ?? [];
98
+ if (traits.includes("leader"))
99
+ leaderCount++;
100
+ if (traits.includes("standardBearer"))
101
+ bannerCount++;
102
+ }
103
+ if (leaderCount > 0) {
104
+ fearQ = clampQ(fearQ - leaderCount * LEADER_AURA_FEAR_REDUCTION, 0, SCALE.Q);
105
+ }
106
+ if (bannerCount > 0) {
107
+ fearQ = clampQ(fearQ - bannerCount * BANNER_AURA_FEAR_REDUCTION, 0, SCALE.Q);
108
+ }
109
+ // Phase 6: cover provides a psychological safety bonus
110
+ const moraleCellSize = ctx.cellSize_m ?? Math.trunc(4 * SCALE.m);
111
+ const coverForMorale = ctx.obstacleGrid
112
+ ? coverFractionAtPosition(ctx.obstacleGrid, moraleCellSize, e.position_m.x, e.position_m.y)
113
+ : 0;
114
+ if (coverForMorale > q(0.5)) {
115
+ fearQ = clampQ(fearQ - q(0.01), 0, SCALE.Q);
116
+ }
117
+ // Feature 5: rally — detect routing → normal transition and set cooldown
118
+ const nowRouting = isRouting(fearQ, distressTol);
119
+ if (wasRouting && !nowRouting) {
120
+ e.condition.rallyCooldownTicks = RALLY_COOLDOWN_TICKS;
121
+ }
122
+ e.condition.fearQ = fearQ;
123
+ // Emit trace when routing state crosses threshold
124
+ if (!wasRouting && nowRouting) {
125
+ trace.onEvent({ kind: TraceKinds.MoraleRoute, tick: world.tick, entityId: e.id, fearQ });
126
+ }
127
+ else if (wasRouting && !nowRouting) {
128
+ trace.onEvent({ kind: TraceKinds.MoraleRally, tick: world.tick, entityId: e.id, fearQ }); // Phase 18
129
+ }
130
+ }
@@ -0,0 +1,5 @@
1
+ import type { Entity } from "../entity.js";
2
+ import type { WorldState } from "../world.js";
3
+ import type { KernelContext } from "../context.js";
4
+ import type { SimulationTuning } from "../tuning.js";
5
+ export declare function stepMovement(e: Entity, world: WorldState, ctx: KernelContext, tuning: SimulationTuning): void;
@@ -0,0 +1,172 @@
1
+ import { eventSeed } from "../seeds.js";
2
+ import { DT_S } from "../tick.js";
3
+ import { SCALE, clampQ, q, qMul, mulDiv } from "../../units.js";
4
+ import { clampSpeed, scaleDirToSpeed, clampI32 } from "../kernel.js";
5
+ import { v3, normaliseDirCheapQ, integratePos } from "../vec3.js";
6
+ import { coverFractionAtPosition, slopeAtPosition, tractionAtPosition, speedMulAtPosition, } from "../terrain.js";
7
+ import { deriveMovementCaps } from "../../derive.js";
8
+ import { deriveFunctionalState } from "../impairment.js";
9
+ import { findExoskeleton } from "../../equipment.js";
10
+ export function stepMovement(e, world, ctx, tuning) {
11
+ const cellSize = ctx.cellSize_m ?? Math.trunc(4 * SCALE.m);
12
+ const traction = tractionAtPosition(ctx.terrainGrid, cellSize, e.position_m.x, e.position_m.y, ctx.tractionCoeff);
13
+ const caps = deriveMovementCaps(e.attributes, e.loadout, { tractionCoeff: traction });
14
+ const func = deriveFunctionalState(e, tuning);
15
+ // Capability gating
16
+ if (!func.canAct) {
17
+ // unconscious/otherwise incapable: no voluntary movement
18
+ e.intent.move = { dir: { x: 0, y: 0, z: 0 }, intensity: q(0), mode: "walk" };
19
+ }
20
+ if (!func.canStand) {
21
+ // force prone if unable to stand (tactical/sim)
22
+ if (tuning.realism !== "arcade")
23
+ e.condition.prone = true;
24
+ }
25
+ if (e.condition.unconsciousTicks > 0) {
26
+ e.velocity_mps = v3(0, 0, 0);
27
+ return;
28
+ }
29
+ const vmax_mps = caps.maxSprintSpeed_mps;
30
+ const amax_mps2 = caps.maxAcceleration_mps2;
31
+ const controlMulBase = clampQ(q(1.0) - qMul(q(0.7), e.condition.stunned), q(0.1), q(1.0));
32
+ let mobilityMulBase = e.condition.prone ? q(0.25) : q(1.0);
33
+ // crawl tuning
34
+ if (e.condition.prone && e.condition.unconsciousTicks === 0 && tuning.realism !== "arcade") {
35
+ mobilityMulBase = qMul(mobilityMulBase, q(0.20)); // crawling is slow
36
+ }
37
+ // impairment affects control and mobility
38
+ const controlMul = qMul(controlMulBase, func.coordinationMul);
39
+ const mobilityMul = qMul(mobilityMulBase, func.mobilityMul);
40
+ const crowd = ctx.density?.crowdingQ.get(e.id) ?? 0;
41
+ const crowdMul = clampQ(q(1.0) - qMul(q(0.65), crowd), q(0.25), q(1.0));
42
+ const terrainSpeedMul = speedMulAtPosition(ctx.terrainGrid, cellSize, e.position_m.x, e.position_m.y);
43
+ // Phase 6: slope direction adjusts effective speed.
44
+ // uphill: −25% per grade unit, clamped [50%,95%]; downhill: +10% per grade unit, clamped [100%,120%].
45
+ const slope = slopeAtPosition(ctx.slopeGrid, cellSize, e.position_m.x, e.position_m.y);
46
+ const slopeMul = slope
47
+ ? slope.type === "uphill"
48
+ ? clampQ((SCALE.Q - qMul(slope.grade, q(0.25))), q(0.50), q(0.95))
49
+ : clampQ((SCALE.Q + qMul(slope.grade, q(0.10))), q(1.0), q(1.20))
50
+ : SCALE.Q;
51
+ // Phase 11: powered exoskeleton speed boost
52
+ const exo = findExoskeleton(e.loadout);
53
+ const exoSpeedMul = exo ? exo.speedMultiplier : SCALE.Q;
54
+ // Phase 8B: flight locomotion — boost sprint speed when entity can achieve flight
55
+ let flightSpeedMul = SCALE.Q;
56
+ const flightSpec = e.bodyPlan?.locomotion.flight;
57
+ if (flightSpec) {
58
+ const mass = e.attributes.morphology.mass_kg;
59
+ if (mass <= flightSpec.liftCapacity_kg) {
60
+ // Compute average wing damage
61
+ let wingDmgSum = 0;
62
+ let wingCount = 0;
63
+ for (const wid of flightSpec.wingSegments) {
64
+ const ws = e.injury.byRegion[wid];
65
+ if (ws) {
66
+ wingDmgSum += ws.structuralDamage;
67
+ wingCount++;
68
+ }
69
+ }
70
+ const avgWingDmg = wingCount > 0 ? Math.trunc(wingDmgSum / wingCount) : q(0);
71
+ const flightMul = clampQ((SCALE.Q - qMul(avgWingDmg, flightSpec.wingDamagePenalty)), q(0), q(1.0));
72
+ // 1.5× flight speed boost, scaled by wing condition
73
+ flightSpeedMul = qMul(q(1.50), flightMul);
74
+ }
75
+ }
76
+ // Phase 32A: locomotion mode modifiers
77
+ // Validate requested mode against entity's declared locomotion capacities.
78
+ const requestedMode = e.intent.locomotionMode;
79
+ const locomotionModes = e.attributes.locomotionModes;
80
+ const activeCapacity = requestedMode && locomotionModes
81
+ ? locomotionModes.find(c => c.mode === requestedMode)
82
+ : undefined;
83
+ // aquatic depth check: entity without swim capacity and below ground (z < 0) cannot act
84
+ const isSubmerged = e.position_m.z < 0;
85
+ const canSwim = locomotionModes?.some(c => c.mode === "swim") ?? false;
86
+ if (isSubmerged && !canSwim) {
87
+ e.velocity_mps = v3(0, 0, 0);
88
+ return;
89
+ }
90
+ // Locomotion mode speed multipliers
91
+ let locomotionSpeedMul = SCALE.Q;
92
+ let skipTraction = false;
93
+ if (activeCapacity) {
94
+ switch (activeCapacity.mode) {
95
+ case "flight":
96
+ // Flight: bypass ground traction; apply cruiseAlt proportional controller
97
+ skipTraction = true;
98
+ if (activeCapacity.cruiseAlt_m !== undefined) {
99
+ const targetZ = activeCapacity.cruiseAlt_m;
100
+ const dz = targetZ - e.position_m.z;
101
+ const dzStep = clampI32(Math.trunc(dz), -Math.trunc(2 * SCALE.m), Math.trunc(2 * SCALE.m));
102
+ e.position_m = { ...e.position_m, z: e.position_m.z + dzStep };
103
+ }
104
+ // Cap at declared maxSpeed
105
+ if (activeCapacity.maxSpeed_mps < vmax_mps) {
106
+ locomotionSpeedMul = mulDiv(activeCapacity.maxSpeed_mps, SCALE.Q, vmax_mps);
107
+ }
108
+ break;
109
+ case "swim":
110
+ // Hydrodynamic drag: ~40% of surface sprint speed
111
+ locomotionSpeedMul = q(0.40);
112
+ skipTraction = true;
113
+ break;
114
+ case "climb":
115
+ locomotionSpeedMul = q(0.30);
116
+ break;
117
+ default:
118
+ break;
119
+ }
120
+ }
121
+ // If skipping traction, override the traction-derived speed caps
122
+ const effTrackMul = skipTraction ? SCALE.Q : SCALE.Q; // traction already applied above via caps
123
+ const baseMul = qMul(qMul(qMul(qMul(qMul(qMul(qMul(controlMul, mobilityMul), crowdMul), skipTraction ? SCALE.Q : terrainSpeedMul), slopeMul), exoSpeedMul), flightSpeedMul), locomotionSpeedMul);
124
+ void effTrackMul;
125
+ const effVmax = mulDiv(vmax_mps, baseMul, SCALE.Q);
126
+ const effAmax = mulDiv(amax_mps2, baseMul, SCALE.Q);
127
+ let modeMul = e.intent.move.mode === "walk" ? q(0.40) :
128
+ e.intent.move.mode === "run" ? q(0.70) : q(1.0);
129
+ if (e.condition.prone && tuning.realism !== "arcade") {
130
+ // cannot sprint while prone
131
+ if (e.intent.move.mode === "sprint")
132
+ modeMul = q(0.40);
133
+ }
134
+ const dir = normaliseDirCheapQ(e.intent.move.dir);
135
+ const intensity = clampQ(e.intent.move.intensity, 0, SCALE.Q);
136
+ // Sim-only: stumble/fall risk when sprinting with impaired mobility/coordination
137
+ if (tuning.realism === "sim" && intensity > 0 && e.intent.move.mode === "sprint" && !e.condition.prone) {
138
+ const instability = (SCALE.Q - qMul(func.mobilityMul, func.coordinationMul));
139
+ const chance = clampQ(tuning.stumbleBaseChance + qMul(instability, q(0.05)), q(0), q(0.25));
140
+ if (chance > 0) {
141
+ const seed = eventSeed(world.seed, world.tick, e.id, 0, 0xF411);
142
+ const roll = (seed % SCALE.Q);
143
+ if (roll < chance) {
144
+ e.condition.prone = true;
145
+ // a small deterministic shock spike
146
+ e.injury.shock = clampQ(e.injury.shock + q(0.02), 0, SCALE.Q);
147
+ }
148
+ }
149
+ }
150
+ const vTargetMag = mulDiv(mulDiv(effVmax, intensity, SCALE.Q), modeMul, SCALE.Q);
151
+ const targetVel = scaleDirToSpeed(dir, vTargetMag);
152
+ e.velocity_mps = accelToward(e.velocity_mps, targetVel, effAmax);
153
+ e.velocity_mps = clampSpeed(e.velocity_mps, effVmax);
154
+ // Phase 6: obstacle blocking — impassable cells (coverFraction = q(1.0)) prevent entry.
155
+ const nextPos = integratePos(e.position_m, e.velocity_mps, DT_S);
156
+ if (ctx.obstacleGrid) {
157
+ const cov = coverFractionAtPosition(ctx.obstacleGrid, cellSize, nextPos.x, nextPos.y);
158
+ if (cov >= SCALE.Q) {
159
+ e.velocity_mps = v3(0, 0, 0);
160
+ return;
161
+ }
162
+ }
163
+ e.position_m = nextPos;
164
+ }
165
+ function accelToward(v, target, amax_mps2) {
166
+ const maxDv = Math.trunc((amax_mps2 * DT_S) / SCALE.s);
167
+ return {
168
+ x: v.x + clampI32(target.x - v.x, -maxDv, maxDv),
169
+ y: v.y + clampI32(target.y - v.y, -maxDv, maxDv),
170
+ z: v.z + clampI32(target.z - v.z, -maxDv, maxDv),
171
+ };
172
+ }
@@ -0,0 +1,11 @@
1
+ import type { WorldState } from "../world.js";
2
+ import type { SpatialIndex } from "../spatial.js";
3
+ import type { WorldIndex } from "../indexing.js";
4
+ import type { Q } from "../../units.js";
5
+ export interface PushTuning {
6
+ personalRadius_m: number;
7
+ repelAccel_mps2: number;
8
+ pushTransfer: Q;
9
+ maxNeighbours: number;
10
+ }
11
+ export declare function stepPushAndRepulsion(world: WorldState, index: WorldIndex, spatial: SpatialIndex, tuning: PushTuning): void;
@@ -0,0 +1,79 @@
1
+ import { queryNearbyIds } from "../spatial.js";
2
+ import { SCALE, clampQ } from "../../units.js";
3
+ export function stepPushAndRepulsion(world, index, spatial, tuning) {
4
+ const R = tuning.personalRadius_m;
5
+ const R2 = BigInt(R) * BigInt(R);
6
+ // 1) collect all candidate pairs (order-independent)
7
+ const pairs = [];
8
+ // Deterministic: world.entities already sorted by id in stepWorld
9
+ for (const e of world.entities) {
10
+ if (e.injury.dead)
11
+ continue;
12
+ const ids = queryNearbyIds(spatial, e.position_m, R, tuning.maxNeighbours);
13
+ ids.sort((x, y) => x - y);
14
+ for (const id of ids) {
15
+ if (id === e.id)
16
+ continue;
17
+ const a = Math.min(e.id, id);
18
+ const b = Math.max(e.id, id);
19
+ pairs.push({ a, b });
20
+ }
21
+ }
22
+ // 2) de-dupe pairs deterministically
23
+ pairs.sort((p, q) => (p.a - q.a) || (p.b - q.b));
24
+ const uniq = [];
25
+ for (const p of pairs) {
26
+ const last = uniq[uniq.length - 1];
27
+ if (!last || last.a !== p.a || last.b !== p.b)
28
+ uniq.push(p);
29
+ }
30
+ // 3) compute dv per pair, accumulate into dv map (NO entity mutation here)
31
+ const dv = new Map();
32
+ for (const { a, b } of uniq) {
33
+ const A = index.byId.get(a);
34
+ const B = index.byId.get(b);
35
+ if (!A || !B)
36
+ continue;
37
+ if (A.injury.dead || B.injury.dead)
38
+ continue;
39
+ const dx = B.position_m.x - A.position_m.x;
40
+ const dy = B.position_m.y - A.position_m.y;
41
+ const dz = B.position_m.z - A.position_m.z;
42
+ const d2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
43
+ if (d2 >= R2 || d2 === 0n)
44
+ continue;
45
+ // repel along x/y only
46
+ const d = approxDist(dx, dy);
47
+ const overlap = Math.max(0, R - d);
48
+ if (overlap <= 0)
49
+ continue;
50
+ const strengthQ = clampQ(Math.trunc((overlap * SCALE.Q) / R), 0, SCALE.Q);
51
+ const ax = Math.trunc((dx * tuning.repelAccel_mps2 * strengthQ) / (Math.max(1, d) * SCALE.Q));
52
+ const ay = Math.trunc((dy * tuning.repelAccel_mps2 * strengthQ) / (Math.max(1, d) * SCALE.Q));
53
+ // equal + opposite dv
54
+ addDv(dv, A.id, -ax, -ay, 0);
55
+ addDv(dv, B.id, ax, ay, 0);
56
+ }
57
+ // 4) apply dv in stable order (world.entities is stable-sorted)
58
+ for (const e of world.entities) {
59
+ const d = dv.get(e.id);
60
+ if (!d)
61
+ continue;
62
+ e.velocity_mps.x += d.x;
63
+ e.velocity_mps.y += d.y;
64
+ e.velocity_mps.z += d.z;
65
+ }
66
+ }
67
+ function addDv(dv, id, dx, dy, dz) {
68
+ const cur = dv.get(id) ?? { x: 0, y: 0, z: 0 };
69
+ cur.x += dx;
70
+ cur.y += dy;
71
+ cur.z += dz;
72
+ dv.set(id, cur);
73
+ }
74
+ // cheap approx: max + 0.5*min
75
+ function approxDist(dx, dy) {
76
+ const adx = dx < 0 ? -dx : dx;
77
+ const ady = dy < 0 ? -dy : dy;
78
+ return adx > ady ? adx + (ady >> 1) : ady + (adx >> 1);
79
+ }
@@ -0,0 +1,3 @@
1
+ import type { Entity } from "../entity.js";
2
+ import { type Q } from "../../units.js";
3
+ export declare function stepSubstances(e: Entity, ambientTemperature_Q?: Q): void;
@@ -0,0 +1,75 @@
1
+ import { q, clampQ, qMul, mulDiv, SCALE } from "../../units.js";
2
+ import { hasSubstanceType } from "../substance.js";
3
+ export function stepSubstances(e, ambientTemperature_Q) {
4
+ if (!e.substances || e.substances.length === 0)
5
+ return;
6
+ for (const active of e.substances) {
7
+ const sub = active.substance;
8
+ // Absorption: pendingDose → concentration
9
+ const absorbed = qMul(active.pendingDose, sub.absorptionRate);
10
+ active.pendingDose = clampQ(active.pendingDose - absorbed, q(0), q(1.0));
11
+ active.concentration = clampQ(active.concentration + absorbed, q(0), q(1.0));
12
+ // Elimination — base rate, then modifiers
13
+ let effectiveElimRate = sub.eliminationRate;
14
+ // Phase 10C: substance interactions — modify elimination rate
15
+ if (sub.effectType === "haemostatic" && hasSubstanceType(e, "stimulant")) {
16
+ // Stimulant-induced vasoconstriction antagonises haemostatic absorption: clears 30% faster
17
+ effectiveElimRate = qMul(effectiveElimRate, q(1.30));
18
+ }
19
+ if (sub.effectType === "anaesthetic" && hasSubstanceType(e, "stimulant")) {
20
+ // Stimulant partially counteracts anaesthetic: clears 25% faster
21
+ effectiveElimRate = qMul(effectiveElimRate, q(1.25));
22
+ }
23
+ if (sub.effectType === "haemostatic" && hasSubstanceType(e, "poison")) {
24
+ // Haemostatic partially counteracts poison-induced bleeding: clears 20% slower
25
+ effectiveElimRate = qMul(effectiveElimRate, q(0.80));
26
+ }
27
+ // Phase 10C: temperature-dependent metabolism — cold slows hepatic clearance
28
+ if (ambientTemperature_Q !== undefined && ambientTemperature_Q < q(0.35)) {
29
+ const coldFrac = Math.max(q(0.50), mulDiv(ambientTemperature_Q, SCALE.Q, q(0.35)));
30
+ effectiveElimRate = qMul(effectiveElimRate, coldFrac);
31
+ }
32
+ const eliminated = qMul(active.concentration, effectiveElimRate);
33
+ active.concentration = clampQ(active.concentration - eliminated, q(0), q(1.0));
34
+ // Effects — only when above threshold
35
+ if (active.concentration <= sub.effectThreshold)
36
+ continue;
37
+ const delta = clampQ(active.concentration - sub.effectThreshold, q(0), q(1.0));
38
+ // Phase 10C: anaesthetic onset/strength is reduced when a stimulant is active
39
+ let effectStrengthMod = sub.effectStrength;
40
+ if (sub.effectType === "anaesthetic" && hasSubstanceType(e, "stimulant")) {
41
+ effectStrengthMod = qMul(effectStrengthMod, q(0.75));
42
+ }
43
+ const effectDose = qMul(delta, effectStrengthMod);
44
+ switch (sub.effectType) {
45
+ case "stimulant":
46
+ // Reduces fear and slows fatigue accumulation
47
+ e.condition.fearQ = clampQ((e.condition.fearQ ?? 0) - qMul(effectDose, q(0.005)), q(0), q(1.0));
48
+ e.energy.fatigue = clampQ(e.energy.fatigue - qMul(effectDose, q(0.003)), q(0), q(1.0));
49
+ break;
50
+ case "anaesthetic":
51
+ // Erodes consciousness
52
+ e.injury.consciousness = clampQ(e.injury.consciousness - qMul(effectDose, q(0.008)), q(0), q(1.0));
53
+ break;
54
+ case "poison": {
55
+ // Internal damage to torso (or first region)
56
+ const torsoReg = e.injury.byRegion["torso"] ?? Object.values(e.injury.byRegion)[0];
57
+ if (torsoReg) {
58
+ torsoReg.internalDamage = clampQ(torsoReg.internalDamage + qMul(effectDose, q(0.002)), q(0), q(1.0));
59
+ }
60
+ e.injury.shock = clampQ(e.injury.shock + qMul(effectDose, q(0.001)), 0, SCALE.Q);
61
+ break;
62
+ }
63
+ case "haemostatic":
64
+ // Reduces bleeding rate across all regions
65
+ for (const reg of Object.values(e.injury.byRegion)) {
66
+ if (reg.bleedingRate > 0) {
67
+ reg.bleedingRate = clampQ(reg.bleedingRate - qMul(effectDose, q(0.003)), q(0), q(1.0));
68
+ }
69
+ }
70
+ break;
71
+ }
72
+ }
73
+ // Remove exhausted substances (keep only those with meaningful dose or concentration)
74
+ e.substances = e.substances.filter(a => a.pendingDose > 1 || a.concentration > 1);
75
+ }
@@ -0,0 +1,38 @@
1
+ import type { Q } from "../units.js";
2
+ import type { Entity } from "./entity.js";
3
+ export type SubstanceEffectType = "stimulant" | "anaesthetic" | "poison" | "haemostatic";
4
+ export interface Substance {
5
+ id: string;
6
+ name: string;
7
+ /** Fraction of pendingDose absorbed into concentration per tick (0..1 Q). */
8
+ absorptionRate: Q;
9
+ /** Fraction of current concentration cleared per tick (0..1 Q). */
10
+ eliminationRate: Q;
11
+ /** Minimum concentration for effects to activate (0..1 Q). */
12
+ effectThreshold: Q;
13
+ /** Nature of biological effect. */
14
+ effectType: SubstanceEffectType;
15
+ /**
16
+ * Strength multiplier applied to the above-threshold concentration delta.
17
+ * A value of q(1.0) produces the standard effect magnitude.
18
+ */
19
+ effectStrength: Q;
20
+ }
21
+ /**
22
+ * An active dose of a substance present in an entity's system.
23
+ * Add to `entity.substances` when a substance is ingested or injected.
24
+ */
25
+ export interface ActiveSubstance {
26
+ substance: Substance;
27
+ /** Remaining unabsorbed dose (Q 0..1); decreases each tick as absorption occurs. */
28
+ pendingDose: Q;
29
+ /** Current systemic concentration (Q 0..1); rises with absorption, falls with elimination. */
30
+ concentration: Q;
31
+ }
32
+ /**
33
+ * Phase 10C: returns true if the entity has an active substance of the given type
34
+ * with concentration above its effectThreshold.
35
+ */
36
+ export declare function hasSubstanceType(e: Entity, type: SubstanceEffectType): boolean;
37
+ /** Ready-made substance catalogue for common game scenarios. */
38
+ export declare const STARTER_SUBSTANCES: Record<string, Substance>;
@@ -0,0 +1,57 @@
1
+ // src/sim/substance.ts — Phase 10: pharmacokinetics model
2
+ //
3
+ // One-compartment model:
4
+ // d[concentration]/dt = absorptionRate × pendingDose − eliminationRate × concentration
5
+ //
6
+ // Effects activate when concentration exceeds effectThreshold.
7
+ // The engine consumes substance definitions; the host application manages which
8
+ // substances an entity has ingested/injected (by populating entity.substances).
9
+ import { q } from "../units.js";
10
+ /**
11
+ * Phase 10C: returns true if the entity has an active substance of the given type
12
+ * with concentration above its effectThreshold.
13
+ */
14
+ export function hasSubstanceType(e, type) {
15
+ if (!e.substances)
16
+ return false;
17
+ return e.substances.some(a => a.substance.effectType === type && a.concentration > a.substance.effectThreshold);
18
+ }
19
+ /** Ready-made substance catalogue for common game scenarios. */
20
+ export const STARTER_SUBSTANCES = {
21
+ stimulant: {
22
+ id: "stimulant",
23
+ name: "Combat Stimulant",
24
+ absorptionRate: q(0.15), // 15%/tick — fast-acting injection
25
+ eliminationRate: q(0.02), // 2%/tick — clears in ~50 ticks (2.5 s)
26
+ effectThreshold: q(0.10),
27
+ effectType: "stimulant",
28
+ effectStrength: q(0.80),
29
+ },
30
+ anaesthetic: {
31
+ id: "anaesthetic",
32
+ name: "Anaesthetic",
33
+ absorptionRate: q(0.08), // slower onset
34
+ eliminationRate: q(0.008), // slow clearance — lasts many ticks
35
+ effectThreshold: q(0.05),
36
+ effectType: "anaesthetic",
37
+ effectStrength: q(1.00),
38
+ },
39
+ poison: {
40
+ id: "poison",
41
+ name: "Contact Poison",
42
+ absorptionRate: q(0.06), // slow skin absorption
43
+ eliminationRate: q(0.004), // very slow clearance
44
+ effectThreshold: q(0.05),
45
+ effectType: "poison",
46
+ effectStrength: q(0.80),
47
+ },
48
+ haemostatic: {
49
+ id: "haemostatic",
50
+ name: "Haemostatic Agent",
51
+ absorptionRate: q(0.20), // rapid absorption (injected/applied)
52
+ eliminationRate: q(0.03), // clears in ~33 ticks (1.6 s)
53
+ effectThreshold: q(0.10),
54
+ effectType: "haemostatic",
55
+ effectStrength: q(0.60),
56
+ },
57
+ };