@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,141 @@
1
+ import { type Q } from "../units.js";
2
+ import type { Entity } from "./entity.js";
3
+ /** Transmission route determines how distance affects spread. */
4
+ export type TransmissionRoute = "airborne" | "contact" | "vector" | "waterborne";
5
+ /** Declarative disease profile. */
6
+ export interface DiseaseProfile {
7
+ id: string;
8
+ name: string;
9
+ transmissionRoute: TransmissionRoute;
10
+ /** Daily probability of spreading from symptomatic carrier to unprotected susceptible [Q]. */
11
+ baseTransmissionRate_Q: Q;
12
+ /** Seconds before symptoms appear. */
13
+ incubationPeriod_s: number;
14
+ /** Seconds of active symptoms (measured from end of incubation). */
15
+ symptomaticDuration_s: number;
16
+ /** Probability of death at end of symptomatic phase [Q]. */
17
+ mortalityRate_Q: Q;
18
+ /** Daily fatigue drain while symptomatic [Q/day]. */
19
+ symptomSeverity_Q: Q;
20
+ /**
21
+ * Max airborne range [SCALE.m]. Only used when transmissionRoute === "airborne".
22
+ * Risk falls linearly to zero at this distance.
23
+ */
24
+ airborneRange_Sm: number;
25
+ /**
26
+ * Immunity granted after recovery [seconds].
27
+ * -1 = permanent; 0 = no immunity (can be reinfected immediately).
28
+ */
29
+ immunityDuration_s: number;
30
+ }
31
+ /** One active disease infection on an entity. */
32
+ export interface DiseaseState {
33
+ diseaseId: string;
34
+ /** "incubating" = latent, no symptoms; "symptomatic" = full symptoms active. */
35
+ phase: "incubating" | "symptomatic";
36
+ /** Seconds elapsed in the current phase. */
37
+ elapsedSeconds: number;
38
+ }
39
+ /** Post-recovery immunity record preventing re-infection. */
40
+ export interface ImmunityRecord {
41
+ diseaseId: string;
42
+ /** Remaining seconds of immunity; -1 = permanent. */
43
+ remainingSeconds: number;
44
+ }
45
+ /** Result returned by `stepDiseaseForEntity`. */
46
+ export interface EntityDiseaseResult {
47
+ /** Disease IDs that transitioned from incubating → symptomatic this step. */
48
+ advancedToSymptomatic: string[];
49
+ /** Disease IDs that ended this step (recovered or fatal). */
50
+ recovered: string[];
51
+ /** True if a mortality roll was triggered and the entity died this step. */
52
+ died: boolean;
53
+ /** Total Q units added to entity.energy.fatigue this step. */
54
+ fatigueApplied: number;
55
+ }
56
+ /** A carrier–target pair supplied by the host's spatial query. */
57
+ export interface NearbyPair {
58
+ carrierId: number;
59
+ targetId: number;
60
+ /** Distance between the two entities [SCALE.m]. */
61
+ dist_Sm: number;
62
+ }
63
+ /** Result returned by `spreadDisease`. */
64
+ export interface SpreadResult {
65
+ /** Number of new exposures applied this call. */
66
+ newExposures: number;
67
+ }
68
+ /** Maximum distance for contact/vector/waterborne transmission [SCALE.m]. */
69
+ export declare const CONTACT_RANGE_Sm = 20000;
70
+ /** All disease profiles indexed by id. */
71
+ export declare const DISEASE_PROFILES: DiseaseProfile[];
72
+ /** Look up a disease profile by id. Returns undefined for unknown ids. */
73
+ export declare function getDiseaseProfile(id: string): DiseaseProfile | undefined;
74
+ /**
75
+ * Attempt to expose an entity to a disease.
76
+ *
77
+ * Returns false (no-op) if:
78
+ * - The disease id is unknown.
79
+ * - The entity already has an active infection with this disease.
80
+ * - The entity has a valid (non-expired) immunity record for this disease.
81
+ *
82
+ * Otherwise creates an incubating DiseaseState and returns true.
83
+ * Does NOT perform a probability roll — the caller (e.g. `spreadDisease`) is
84
+ * responsible for rolling before calling this function.
85
+ *
86
+ * Mutates: `entity.activeDiseases`.
87
+ */
88
+ export declare function exposeToDisease(entity: Entity, diseaseId: string): boolean;
89
+ /**
90
+ * Advance all active diseases on an entity by `delta_s` seconds.
91
+ *
92
+ * For each active disease:
93
+ * - Incubating → symptomatic when elapsedSeconds ≥ incubationPeriod_s.
94
+ * - Symptomatic: drain fatigue at `symptomSeverity_Q × delta_s / 86400`.
95
+ * - Symptomatic → ended when elapsedSeconds ≥ symptomaticDuration_s:
96
+ * roll mortality via eventSeed; if fatal set `entity.injury.dead = true`.
97
+ * If survivor, grant immunity (duration per profile).
98
+ *
99
+ * Also ticks down temporary immunity timers.
100
+ *
101
+ * Mutates: `entity.activeDiseases`, `entity.immunity`, `entity.energy.fatigue`,
102
+ * `entity.injury.dead`.
103
+ *
104
+ * @param worldSeed World seed for deterministic mortality roll.
105
+ * @param tick Current tick for deterministic mortality roll.
106
+ */
107
+ export declare function stepDiseaseForEntity(entity: Entity, delta_s: number, worldSeed: number, tick: number): EntityDiseaseResult;
108
+ /**
109
+ * Compute the transmission risk Q from a symptomatic carrier to a target.
110
+ *
111
+ * Airborne: risk scales linearly from `baseTransmissionRate_Q` at dist 0
112
+ * to 0 at `airborneRange_Sm`. Beyond range → q(0).
113
+ * Contact / vector / waterborne: full `baseTransmissionRate_Q` if within
114
+ * `CONTACT_RANGE_Sm`; q(0) beyond.
115
+ *
116
+ * Returns q(0) if the carrier has no symptomatic instance of this disease,
117
+ * or if target already has immunity / active infection for this disease.
118
+ *
119
+ * @param carrier The potentially infectious entity.
120
+ * @param target The potentially susceptible entity.
121
+ * @param dist_Sm Distance between them [SCALE.m].
122
+ * @param disease The disease profile to evaluate.
123
+ */
124
+ export declare function computeTransmissionRisk(carrier: Entity, target: Entity, dist_Sm: number, disease: DiseaseProfile): Q;
125
+ /**
126
+ * Attempt to spread disease across a set of nearby entity pairs.
127
+ *
128
+ * For each pair the host has identified as spatially close:
129
+ * - Evaluates all symptomatic diseases on the carrier.
130
+ * - Rolls `eventSeed(worldSeed, tick, carrierId, targetId, diseaseIdSalt)`.
131
+ * - If roll < transmissionRisk_Q × SCALE.Q, calls `exposeToDisease`.
132
+ *
133
+ * Deterministic: identical inputs → identical outputs.
134
+ *
135
+ * @param entityMap Map of entity id → Entity (must include all ids in pairs).
136
+ * @param pairs Carrier–target pairs with their SCALE.m distances (from host spatial query).
137
+ * @param worldSeed World seed for eventSeed.
138
+ * @param tick Current tick for eventSeed.
139
+ * @returns Number of new exposures created.
140
+ */
141
+ export declare function spreadDisease(entityMap: Map<number, Entity>, pairs: NearbyPair[], worldSeed: number, tick: number): SpreadResult;
@@ -0,0 +1,353 @@
1
+ // src/sim/disease.ts — Phase 56: Disease & Epidemic Simulation
2
+ //
3
+ // Extends wound-level infection (Phase 9 / `infectedTick`) with entity-to-entity
4
+ // disease transmission. Suitable for downtime (campaign-scale) or continuous
5
+ // epidemic simulation in populated worlds.
6
+ //
7
+ // Three tiers:
8
+ // 1. Entity state — DiseaseState (per active disease) + ImmunityRecord (post-recovery)
9
+ // 2. Per-entity step — stepDiseaseForEntity: advances phase timers, applies fatigue,
10
+ // rolls mortality on recovery, grants immunity.
11
+ // 3. Transmission — computeTransmissionRisk: distance-based risk Q;
12
+ // spreadDisease: deterministic batch exposure using eventSeed.
13
+ //
14
+ // Six disease profiles span fantasy-medieval and speculative scenarios:
15
+ // common_fever wound_fever plague_pneumonic dysentery marsh_fever wasting_sickness
16
+ import { q, clampQ, SCALE } from "../units.js";
17
+ import { eventSeed } from "./seeds.js";
18
+ // ── Constants ─────────────────────────────────────────────────────────────────
19
+ /** Maximum distance for contact/vector/waterborne transmission [SCALE.m]. */
20
+ export const CONTACT_RANGE_Sm = 20_000; // 2 m
21
+ /** Common-fever airborne range [SCALE.m]. */
22
+ const FEVER_AIRBORNE_Sm = 100_000; // 10 m
23
+ /** Plague airborne range [SCALE.m]. */
24
+ const PLAGUE_AIRBORNE_Sm = 50_000; // 5 m
25
+ // ── Disease Catalogue ─────────────────────────────────────────────────────────
26
+ /**
27
+ * Common fever — mild respiratory infection.
28
+ * Airborne, short duration, no mortality. Short-lived immunity (90 days).
29
+ */
30
+ const COMMON_FEVER = {
31
+ id: "common_fever",
32
+ name: "Common Fever",
33
+ transmissionRoute: "airborne",
34
+ baseTransmissionRate_Q: q(0.30),
35
+ incubationPeriod_s: 86_400, // 1 day
36
+ symptomaticDuration_s: 3 * 86_400, // 3 days
37
+ mortalityRate_Q: q(0),
38
+ symptomSeverity_Q: q(0.10),
39
+ airborneRange_Sm: FEVER_AIRBORNE_Sm,
40
+ immunityDuration_s: 90 * 86_400, // 90 days
41
+ };
42
+ /**
43
+ * Wound fever — arises from contact with infected individuals or open wounds.
44
+ * Moderate severity; 5 % mortality. Permanent immunity.
45
+ */
46
+ const WOUND_FEVER = {
47
+ id: "wound_fever",
48
+ name: "Wound Fever",
49
+ transmissionRoute: "contact",
50
+ baseTransmissionRate_Q: q(0.15),
51
+ incubationPeriod_s: 2 * 86_400, // 2 days
52
+ symptomaticDuration_s: 7 * 86_400, // 7 days
53
+ mortalityRate_Q: q(0.05),
54
+ symptomSeverity_Q: q(0.20),
55
+ airborneRange_Sm: 0,
56
+ immunityDuration_s: -1, // permanent
57
+ };
58
+ /**
59
+ * Pneumonic plague — severe airborne variant.
60
+ * High transmission, 60 % mortality. Permanent immunity in survivors.
61
+ */
62
+ const PLAGUE_PNEUMONIC = {
63
+ id: "plague_pneumonic",
64
+ name: "Pneumonic Plague",
65
+ transmissionRoute: "airborne",
66
+ baseTransmissionRate_Q: q(0.80),
67
+ incubationPeriod_s: 86_400, // 1 day
68
+ symptomaticDuration_s: 10 * 86_400, // 10 days
69
+ mortalityRate_Q: q(0.60),
70
+ symptomSeverity_Q: q(0.50),
71
+ airborneRange_Sm: PLAGUE_AIRBORNE_Sm,
72
+ immunityDuration_s: -1, // permanent
73
+ };
74
+ /**
75
+ * Dysentery — waterborne/contact, highly debilitating.
76
+ * 10 % mortality; no lasting immunity (can be reinfected).
77
+ */
78
+ const DYSENTERY = {
79
+ id: "dysentery",
80
+ name: "Dysentery",
81
+ transmissionRoute: "waterborne",
82
+ baseTransmissionRate_Q: q(0.40),
83
+ incubationPeriod_s: 86_400, // 1 day
84
+ symptomaticDuration_s: 14 * 86_400, // 14 days
85
+ mortalityRate_Q: q(0.10),
86
+ symptomSeverity_Q: q(0.30),
87
+ airborneRange_Sm: 0,
88
+ immunityDuration_s: 0, // no immunity
89
+ };
90
+ /**
91
+ * Marsh fever — vector-borne (insects), recurring.
92
+ * Chronic fatigue; 3 % mortality; short-lived immunity (30 days).
93
+ */
94
+ const MARSH_FEVER = {
95
+ id: "marsh_fever",
96
+ name: "Marsh Fever",
97
+ transmissionRoute: "vector",
98
+ baseTransmissionRate_Q: q(0.20),
99
+ incubationPeriod_s: 7 * 86_400, // 7 days
100
+ symptomaticDuration_s: 5 * 86_400, // 5 days
101
+ mortalityRate_Q: q(0.03),
102
+ symptomSeverity_Q: q(0.25),
103
+ airborneRange_Sm: 0,
104
+ immunityDuration_s: 30 * 86_400, // 30 days
105
+ };
106
+ /**
107
+ * Wasting sickness — slow-onset, prolonged contact-spread disease.
108
+ * High fatigue drain; 25 % mortality; long-lived immunity (180 days).
109
+ */
110
+ const WASTING_SICKNESS = {
111
+ id: "wasting_sickness",
112
+ name: "Wasting Sickness",
113
+ transmissionRoute: "contact",
114
+ baseTransmissionRate_Q: q(0.05),
115
+ incubationPeriod_s: 14 * 86_400, // 14 days
116
+ symptomaticDuration_s: 30 * 86_400, // 30 days
117
+ mortalityRate_Q: q(0.25),
118
+ symptomSeverity_Q: q(0.35),
119
+ airborneRange_Sm: 0,
120
+ immunityDuration_s: 180 * 86_400, // 180 days
121
+ };
122
+ /** All disease profiles indexed by id. */
123
+ export const DISEASE_PROFILES = [
124
+ COMMON_FEVER,
125
+ WOUND_FEVER,
126
+ PLAGUE_PNEUMONIC,
127
+ DYSENTERY,
128
+ MARSH_FEVER,
129
+ WASTING_SICKNESS,
130
+ ];
131
+ const _PROFILE_MAP = new Map(DISEASE_PROFILES.map(p => [p.id, p]));
132
+ /** Look up a disease profile by id. Returns undefined for unknown ids. */
133
+ export function getDiseaseProfile(id) {
134
+ return _PROFILE_MAP.get(id);
135
+ }
136
+ // ── Entity-level API ──────────────────────────────────────────────────────────
137
+ /**
138
+ * Attempt to expose an entity to a disease.
139
+ *
140
+ * Returns false (no-op) if:
141
+ * - The disease id is unknown.
142
+ * - The entity already has an active infection with this disease.
143
+ * - The entity has a valid (non-expired) immunity record for this disease.
144
+ *
145
+ * Otherwise creates an incubating DiseaseState and returns true.
146
+ * Does NOT perform a probability roll — the caller (e.g. `spreadDisease`) is
147
+ * responsible for rolling before calling this function.
148
+ *
149
+ * Mutates: `entity.activeDiseases`.
150
+ */
151
+ export function exposeToDisease(entity, diseaseId) {
152
+ if (!getDiseaseProfile(diseaseId))
153
+ return false;
154
+ // Already infected?
155
+ if (entity.activeDiseases?.some(d => d.diseaseId === diseaseId))
156
+ return false;
157
+ // Immune?
158
+ const immune = entity.immunity?.some(r => r.diseaseId === diseaseId && (r.remainingSeconds === -1 || r.remainingSeconds > 0));
159
+ if (immune)
160
+ return false;
161
+ if (!entity.activeDiseases)
162
+ entity.activeDiseases = [];
163
+ entity.activeDiseases.push({
164
+ diseaseId,
165
+ phase: "incubating",
166
+ elapsedSeconds: 0,
167
+ });
168
+ return true;
169
+ }
170
+ /**
171
+ * Advance all active diseases on an entity by `delta_s` seconds.
172
+ *
173
+ * For each active disease:
174
+ * - Incubating → symptomatic when elapsedSeconds ≥ incubationPeriod_s.
175
+ * - Symptomatic: drain fatigue at `symptomSeverity_Q × delta_s / 86400`.
176
+ * - Symptomatic → ended when elapsedSeconds ≥ symptomaticDuration_s:
177
+ * roll mortality via eventSeed; if fatal set `entity.injury.dead = true`.
178
+ * If survivor, grant immunity (duration per profile).
179
+ *
180
+ * Also ticks down temporary immunity timers.
181
+ *
182
+ * Mutates: `entity.activeDiseases`, `entity.immunity`, `entity.energy.fatigue`,
183
+ * `entity.injury.dead`.
184
+ *
185
+ * @param worldSeed World seed for deterministic mortality roll.
186
+ * @param tick Current tick for deterministic mortality roll.
187
+ */
188
+ export function stepDiseaseForEntity(entity, delta_s, worldSeed, tick) {
189
+ const result = {
190
+ advancedToSymptomatic: [],
191
+ recovered: [],
192
+ died: false,
193
+ fatigueApplied: 0,
194
+ };
195
+ if (entity.injury.dead)
196
+ return result;
197
+ // ── Tick immunity timers ──────────────────────────────────────────────────
198
+ if (entity.immunity) {
199
+ for (const r of entity.immunity) {
200
+ if (r.remainingSeconds > 0) {
201
+ r.remainingSeconds = Math.max(0, r.remainingSeconds - delta_s);
202
+ }
203
+ }
204
+ }
205
+ if (!entity.activeDiseases?.length)
206
+ return result;
207
+ const toRemove = [];
208
+ for (const state of entity.activeDiseases) {
209
+ const profile = getDiseaseProfile(state.diseaseId);
210
+ if (!profile) {
211
+ toRemove.push(state.diseaseId);
212
+ continue;
213
+ }
214
+ state.elapsedSeconds += delta_s;
215
+ if (state.phase === "incubating") {
216
+ if (state.elapsedSeconds >= profile.incubationPeriod_s) {
217
+ state.phase = "symptomatic";
218
+ state.elapsedSeconds -= profile.incubationPeriod_s;
219
+ result.advancedToSymptomatic.push(state.diseaseId);
220
+ }
221
+ }
222
+ if (state.phase === "symptomatic") {
223
+ // Apply daily fatigue drain
224
+ const fatigueInc = Math.round(profile.symptomSeverity_Q * delta_s / 86_400);
225
+ if (fatigueInc > 0) {
226
+ entity.energy.fatigue = clampQ((entity.energy.fatigue + fatigueInc), q(0), SCALE.Q);
227
+ result.fatigueApplied += fatigueInc;
228
+ }
229
+ // Recover when symptomatic duration is reached
230
+ if (state.elapsedSeconds >= profile.symptomaticDuration_s) {
231
+ toRemove.push(state.diseaseId);
232
+ result.recovered.push(state.diseaseId);
233
+ // Mortality roll — deterministic via eventSeed
234
+ const salt = diseaseIdSalt(state.diseaseId);
235
+ const roll = eventSeed(worldSeed, tick, entity.id, 0, salt) % SCALE.Q;
236
+ if (roll < profile.mortalityRate_Q) {
237
+ entity.injury.dead = true;
238
+ result.died = true;
239
+ continue; // no immunity needed if dead
240
+ }
241
+ // Grant immunity
242
+ if (profile.immunityDuration_s !== 0) {
243
+ if (!entity.immunity)
244
+ entity.immunity = [];
245
+ entity.immunity.push({
246
+ diseaseId: state.diseaseId,
247
+ remainingSeconds: profile.immunityDuration_s, // -1 = permanent
248
+ });
249
+ }
250
+ }
251
+ }
252
+ }
253
+ // Remove ended diseases
254
+ if (toRemove.length > 0) {
255
+ entity.activeDiseases = entity.activeDiseases.filter(d => !toRemove.includes(d.diseaseId));
256
+ }
257
+ return result;
258
+ }
259
+ // ── Transmission ──────────────────────────────────────────────────────────────
260
+ /**
261
+ * Compute the transmission risk Q from a symptomatic carrier to a target.
262
+ *
263
+ * Airborne: risk scales linearly from `baseTransmissionRate_Q` at dist 0
264
+ * to 0 at `airborneRange_Sm`. Beyond range → q(0).
265
+ * Contact / vector / waterborne: full `baseTransmissionRate_Q` if within
266
+ * `CONTACT_RANGE_Sm`; q(0) beyond.
267
+ *
268
+ * Returns q(0) if the carrier has no symptomatic instance of this disease,
269
+ * or if target already has immunity / active infection for this disease.
270
+ *
271
+ * @param carrier The potentially infectious entity.
272
+ * @param target The potentially susceptible entity.
273
+ * @param dist_Sm Distance between them [SCALE.m].
274
+ * @param disease The disease profile to evaluate.
275
+ */
276
+ export function computeTransmissionRisk(carrier, target, dist_Sm, disease) {
277
+ // Carrier must be symptomatic with this disease
278
+ const carrierState = carrier.activeDiseases?.find(d => d.diseaseId === disease.id && d.phase === "symptomatic");
279
+ if (!carrierState)
280
+ return q(0);
281
+ // Target already infected?
282
+ if (target.activeDiseases?.some(d => d.diseaseId === disease.id))
283
+ return q(0);
284
+ // Target immune?
285
+ const immune = target.immunity?.some(r => r.diseaseId === disease.id && (r.remainingSeconds === -1 || r.remainingSeconds > 0));
286
+ if (immune)
287
+ return q(0);
288
+ if (disease.transmissionRoute === "airborne") {
289
+ if (disease.airborneRange_Sm <= 0 || dist_Sm >= disease.airborneRange_Sm)
290
+ return q(0);
291
+ const proximity_Q = Math.round((disease.airborneRange_Sm - dist_Sm) * SCALE.Q / disease.airborneRange_Sm);
292
+ return Math.round(disease.baseTransmissionRate_Q * proximity_Q / SCALE.Q);
293
+ }
294
+ // contact / vector / waterborne: flat risk within CONTACT_RANGE
295
+ if (dist_Sm > CONTACT_RANGE_Sm)
296
+ return q(0);
297
+ return disease.baseTransmissionRate_Q;
298
+ }
299
+ /**
300
+ * Attempt to spread disease across a set of nearby entity pairs.
301
+ *
302
+ * For each pair the host has identified as spatially close:
303
+ * - Evaluates all symptomatic diseases on the carrier.
304
+ * - Rolls `eventSeed(worldSeed, tick, carrierId, targetId, diseaseIdSalt)`.
305
+ * - If roll < transmissionRisk_Q × SCALE.Q, calls `exposeToDisease`.
306
+ *
307
+ * Deterministic: identical inputs → identical outputs.
308
+ *
309
+ * @param entityMap Map of entity id → Entity (must include all ids in pairs).
310
+ * @param pairs Carrier–target pairs with their SCALE.m distances (from host spatial query).
311
+ * @param worldSeed World seed for eventSeed.
312
+ * @param tick Current tick for eventSeed.
313
+ * @returns Number of new exposures created.
314
+ */
315
+ export function spreadDisease(entityMap, pairs, worldSeed, tick) {
316
+ let newExposures = 0;
317
+ for (const pair of pairs) {
318
+ const carrier = entityMap.get(pair.carrierId);
319
+ const target = entityMap.get(pair.targetId);
320
+ if (!carrier || !target)
321
+ continue;
322
+ if (carrier.injury.dead || target.injury.dead)
323
+ continue;
324
+ if (!carrier.activeDiseases?.length)
325
+ continue;
326
+ for (const state of carrier.activeDiseases) {
327
+ if (state.phase !== "symptomatic")
328
+ continue;
329
+ const profile = getDiseaseProfile(state.diseaseId);
330
+ if (!profile)
331
+ continue;
332
+ const risk_Q = computeTransmissionRisk(carrier, target, pair.dist_Sm, profile);
333
+ if (risk_Q <= 0)
334
+ continue;
335
+ const salt = diseaseIdSalt(state.diseaseId);
336
+ const roll = eventSeed(worldSeed, tick, pair.carrierId, pair.targetId, salt) % SCALE.Q;
337
+ if (roll < risk_Q) {
338
+ if (exposeToDisease(target, state.diseaseId)) {
339
+ newExposures++;
340
+ }
341
+ }
342
+ }
343
+ }
344
+ return { newExposures };
345
+ }
346
+ // ── Internal helpers ──────────────────────────────────────────────────────────
347
+ /** Stable numeric salt from a disease id string (sum of char codes & 0xFFFFFF). */
348
+ function diseaseIdSalt(id) {
349
+ let h = 0;
350
+ for (let i = 0; i < id.length; i++)
351
+ h = (h + id.charCodeAt(i)) & 0xFFFFFF;
352
+ return h;
353
+ }