@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,262 @@
1
+ import type { Q } from "./units.js";
2
+ import type { TechEra } from "./sim/tech.js";
3
+ import type { DiseaseProfile } from "./sim/disease.js";
4
+ import type { FactionRegistry } from "./faction.js";
5
+ /**
6
+ * A geopolitical entity: city, nation, or empire.
7
+ *
8
+ * Operates at 1 tick per simulated day. All Q fields are fixed-point
9
+ * fractions in [0, SCALE.Q] unless documented otherwise.
10
+ */
11
+ export interface Polity {
12
+ id: string;
13
+ name: string;
14
+ /** Ties into an existing Faction id in the FactionRegistry. */
15
+ factionId: string;
16
+ /** Location IDs (Campaign layer) this polity controls. */
17
+ locationIds: string[];
18
+ /** Headcount (integer people). */
19
+ population: number;
20
+ /** Wealth in cost-units (same scale as src/economy.ts ItemValue.baseValue). */
21
+ treasury_cu: number;
22
+ /** Current technology era (Phase 11C). */
23
+ techEra: TechEra;
24
+ /**
25
+ * Derived fighting capacity [0, SCALE.Q].
26
+ * Recomputed by `deriveMilitaryStrength`; do not set directly.
27
+ */
28
+ militaryStrength_Q: Q;
29
+ /** Internal cohesion [0, SCALE.Q]. Below UNREST_THRESHOLD → morale drain. */
30
+ stabilityQ: Q;
31
+ /** Population morale [0, SCALE.Q]. Low morale → weak military and stability decay. */
32
+ moraleQ: Q;
33
+ }
34
+ /** Registry of all active polities and their geopolitical relationships. */
35
+ export interface PolityRegistry {
36
+ polities: Map<string, Polity>;
37
+ /**
38
+ * Pairs currently at war. Keys are sorted "a:b" (a < b lexicographically)
39
+ * so each pair appears exactly once.
40
+ */
41
+ activeWars: Set<string>;
42
+ /** Diplomatic alliances: polityId → Set of allied polityIds. */
43
+ alliances: Map<string, Set<string>>;
44
+ }
45
+ /** A trade/proximity link between two polities in the Campaign graph. */
46
+ export interface PolityPair {
47
+ polityAId: string;
48
+ polityBId: string;
49
+ /** Number of locations shared on the Campaign border. */
50
+ sharedLocations: number;
51
+ /**
52
+ * Best navigator's `logicalMathematical_Q` among available envoys (Phase 38).
53
+ * Use q(0.50) as a default when no specific navigator is assigned.
54
+ */
55
+ routeQuality_Q: Q;
56
+ }
57
+ export interface PolityTradeResult {
58
+ polityAId: string;
59
+ polityBId: string;
60
+ /** Cost-units credited to each polity's treasury. */
61
+ incomeEach_cu: number;
62
+ }
63
+ export interface PolityWarResult {
64
+ attackerId: string;
65
+ defenderId: string;
66
+ attackerWins: boolean;
67
+ stabilityDeltaAttacker: Q;
68
+ stabilityDeltaDefender: Q;
69
+ /** Location IDs transferred from defender to attacker on victory (first location only). */
70
+ territoryGained: string[];
71
+ }
72
+ export interface PolityDiplomacyResult {
73
+ polityAId: string;
74
+ polityBId: string;
75
+ /** Positive standing delta to apply to the FactionRegistry global standing. */
76
+ standingDelta: Q;
77
+ }
78
+ export interface PolityDiseaseResult {
79
+ polityId: string;
80
+ /** Estimated new disease exposures in the population for this day. */
81
+ newExposures: number;
82
+ /** Net population change; negative = deaths from disease mortality. */
83
+ populationDelta: number;
84
+ }
85
+ export interface PolityDayResult {
86
+ trade: PolityTradeResult[];
87
+ moraleDeltas: Map<string, Q>;
88
+ stabilityDeltas: Map<string, Q>;
89
+ }
90
+ /** Population count at which military potential equals q(1.0). */
91
+ export declare const POLITY_POP_SCALE = 100000;
92
+ /**
93
+ * Military force multiplier by TechEra index.
94
+ * Higher eras give a fractional advantage on top of population and morale.
95
+ */
96
+ export declare const TECH_FORCE_MUL: ReadonlyArray<Q>;
97
+ /**
98
+ * Trade output multiplier by the *lower* of the two polities' TechEra.
99
+ * Advanced tech means more valuable tradeable goods.
100
+ */
101
+ export declare const TECH_TRADE_MUL: ReadonlyArray<Q>;
102
+ /** Fraction of min(treasury) exchanged as mutual trade income per day. */
103
+ export declare const TRADE_RATE_PER_DAY_Q: Q;
104
+ /**
105
+ * Treasury cost to advance one tech era (indexed by current era).
106
+ * Era 8 (DeepSpace) is the maximum; index 8 cost is unused.
107
+ */
108
+ export declare const TECH_ADVANCE_COST: ReadonlyArray<number>;
109
+ /** Defender's structural advantage in war resolution (home terrain, fortifications). */
110
+ export declare const DEFENDER_ADVANTAGE_Q: Q;
111
+ /** Outcome uncertainty range in war: attacker power is scaled by [q(0.80), q(1.30)]. */
112
+ export declare const WAR_UNCERTAINTY_Q: Q;
113
+ /** Stability penalty applied to the losing side per war day-tick. */
114
+ export declare const WAR_LOSER_STABILITY_HIT: Q;
115
+ /** Stability bonus for the winning side per war day-tick. */
116
+ export declare const WAR_WINNER_STABILITY_GAIN: Q;
117
+ /** Daily stability decay absent active governance. */
118
+ export declare const STABILITY_DECAY_PER_DAY: Q;
119
+ /** Daily stability recovery when morale > q(0.50). */
120
+ export declare const STABILITY_RECOVERY_PER_DAY: Q;
121
+ /** Daily morale gain when stability ≥ UNREST_THRESHOLD. */
122
+ export declare const MORALE_RECOVERY_PER_DAY: Q;
123
+ /** Daily morale drain when stability < UNREST_THRESHOLD. */
124
+ export declare const MORALE_DRAIN_PER_DAY: Q;
125
+ /** Stability below this value triggers morale drain instead of recovery. */
126
+ export declare const UNREST_THRESHOLD: Q;
127
+ /** Maximum standing delta per successful diplomatic negotiation. */
128
+ export declare const DIPLOMACY_MAX_DELTA: Q;
129
+ /**
130
+ * Population per controlled location above which airborne disease spreads
131
+ * at polity scale (instead of only entity-to-entity).
132
+ */
133
+ export declare const DENSITY_SPREAD_THRESHOLD = 5000;
134
+ /**
135
+ * Create a Polity with derived `militaryStrength_Q`.
136
+ *
137
+ * Default starting stability and morale represent a stable, reasonably
138
+ * content polity (stability q(0.70), morale q(0.65)).
139
+ */
140
+ export declare function createPolity(id: string, name: string, factionId: string, locationIds: string[], population: number, treasury_cu: number, techEra: TechEra, stabilityQ?: Q, moraleQ?: Q): Polity;
141
+ /**
142
+ * Create a PolityRegistry from an array of polities.
143
+ * No wars or alliances are registered by default.
144
+ */
145
+ export declare function createPolityRegistry(polities: Polity[]): PolityRegistry;
146
+ /**
147
+ * Derive and update `polity.militaryStrength_Q` from population, morale, and
148
+ * tech era.
149
+ *
150
+ * Formula: `clamp(popFrac × morale × techMul, 0, SCALE.Q)`
151
+ *
152
+ * - `popFrac` = `population / POLITY_POP_SCALE`, clamped to [0, SCALE.Q]
153
+ * (100 000 people = q(1.0) military potential)
154
+ * - `morale` and `techMul` are Q multipliers; result is clamped to SCALE.Q.
155
+ *
156
+ * Mutates `polity.militaryStrength_Q` and returns the new value.
157
+ */
158
+ export declare function deriveMilitaryStrength(polity: Polity): Q;
159
+ /**
160
+ * Compute the daily trade income credited to each polity.
161
+ *
162
+ * Both polities receive the same `incomeEach_cu`. Scales with:
163
+ * - min(treasury): limited by the poorer partner
164
+ * - routeQuality_Q: navigator skill (Phase 38 `logicalMathematical`)
165
+ * - lower tech era of the pair: advanced goods multiply trade value
166
+ * - sharedLocations: more border crossings → more trade routes
167
+ *
168
+ * Returns 0 when either treasury is empty or `sharedLocations <= 0`.
169
+ */
170
+ export declare function computeTradeIncome(polityA: Polity, polityB: Polity, sharedLocations: number, routeQuality_Q: Q): number;
171
+ /**
172
+ * Resolve one day of active warfare between two polities.
173
+ *
174
+ * Deterministic given (`worldSeed`, `tick`). The defender receives a
175
+ * built-in structural advantage (DEFENDER_ADVANTAGE_Q = q(1.20)).
176
+ * Attacker power is modified by a deterministic ±q(0.20) uncertainty roll.
177
+ *
178
+ * On attacker victory the first location of the defender is transferred.
179
+ * Stability consequences are returned as deltas; the caller applies them
180
+ * (or call `stepPolityDay` to handle that automatically for active wars).
181
+ */
182
+ export declare function resolveWarOutcome(attacker: Polity, defender: Polity, worldSeed: number, tick: number): PolityWarResult;
183
+ /**
184
+ * Resolve a diplomatic negotiation between two polities.
185
+ *
186
+ * Returns a positive `standingDelta` to apply to the FactionRegistry global
187
+ * standing between the two polities' factions via `applyFactionStanding`.
188
+ *
189
+ * Standing improvement scales with:
190
+ * - `diplomatLinguistic_Q`: best envoy's `linguisticIntelligence_Q` (Phase 37)
191
+ * - headroom: how far below ALLY standing (q(0.70)) the current relation is
192
+ * (no improvement when already at or above ALLY)
193
+ *
194
+ * Maximum delta per negotiation is DIPLOMACY_MAX_DELTA (q(0.08)).
195
+ */
196
+ export declare function resolveDiplomacy(polityA: Polity, polityB: Polity, diplomatLinguistic_Q: Q, currentStanding_Q: Q): PolityDiplomacyResult;
197
+ /**
198
+ * Return true if the polity meets the conditions to advance to the next tech era.
199
+ *
200
+ * Requires:
201
+ * 1. A research project has been completed (`projectCompleted = true`).
202
+ * 2. Treasury meets the advancement cost for the current era.
203
+ * 3. Not already at maximum era (DeepSpace, index 8).
204
+ */
205
+ export declare function canAdvanceTech(polity: Polity, projectCompleted: boolean): boolean;
206
+ /**
207
+ * Advance polity to the next tech era if eligible.
208
+ *
209
+ * Mutates `polity.techEra` and `polity.treasury_cu`.
210
+ * Refreshes `militaryStrength_Q` after advancement.
211
+ * Returns `true` if advancement occurred.
212
+ */
213
+ export declare function advanceTechEra(polity: Polity, projectCompleted: boolean): boolean;
214
+ /**
215
+ * Compute population-scale disease spread for one simulated day.
216
+ *
217
+ * Only `"airborne"` diseases spread at polity scale; other routes remain
218
+ * entity-to-entity (handled by Phase 56 `spreadDisease`).
219
+ *
220
+ * Spread activates when population density
221
+ * (`population / locationIds.length`) exceeds DENSITY_SPREAD_THRESHOLD.
222
+ *
223
+ * Mutates `polity.population` by `populationDelta` (negative = deaths).
224
+ * Returns zeros when conditions are not met.
225
+ */
226
+ export declare function computePolityDiseaseSpread(polity: Polity, profile: DiseaseProfile, worldSeed: number, tick: number): PolityDiseaseResult;
227
+ /**
228
+ * Advance all polities by one simulated day.
229
+ *
230
+ * Performs three phases in order:
231
+ *
232
+ * **Trade**: For each non-warring pair, compute and credit mutual trade income.
233
+ *
234
+ * **War**: For each active war, resolve one day of combat, apply stability
235
+ * consequences, and transfer territory on attacker victory.
236
+ *
237
+ * **Morale/Stability**: For each polity:
238
+ * - Stability decays daily; recovers when morale > q(0.50).
239
+ * - Morale drains when stability < UNREST_THRESHOLD; recovers otherwise.
240
+ * - `militaryStrength_Q` is refreshed.
241
+ *
242
+ * Disease spread is NOT handled here; call `computePolityDiseaseSpread`
243
+ * per-disease per-polity as the host iterates active outbreaks.
244
+ *
245
+ * Mutates polities in `registry.polities` and registry.activeWars (territory
246
+ * transfers may empty `locationIds`, but war entries are not auto-removed).
247
+ */
248
+ export declare function stepPolityDay(registry: PolityRegistry, pairs: PolityPair[], worldSeed: number, tick: number): PolityDayResult;
249
+ /** Register a state of war between two polities. Idempotent. */
250
+ export declare function declareWar(registry: PolityRegistry, polityAId: string, polityBId: string): void;
251
+ /** End the state of war between two polities. Idempotent. */
252
+ export declare function makePeace(registry: PolityRegistry, polityAId: string, polityBId: string): void;
253
+ /** Return true if two polities are currently at war. */
254
+ export declare function areAtWar(registry: PolityRegistry, polityAId: string, polityBId: string): boolean;
255
+ /**
256
+ * Look up the current faction-level standing that polityA's faction holds
257
+ * toward polityB's faction in the FactionRegistry.
258
+ *
259
+ * Returns STANDING_NEUTRAL (q(0.50)) if no relation is registered.
260
+ * Use this as `currentStanding_Q` for `resolveDiplomacy`.
261
+ */
262
+ export declare function polityFactionStanding(factionRegistry: FactionRegistry, polityA: Polity, polityB: Polity): Q;
@@ -0,0 +1,398 @@
1
+ // src/polity.ts — Phase 61: Polity & World-State System
2
+ //
3
+ // Geopolitical simulation layer operating at 1 tick per simulated day.
4
+ // Polities (cities, nations, empires) integrate with:
5
+ // Faction (Phase 24) — via factionId → FactionRegistry
6
+ // Economy (Phase 25) — treasury in cost-unit scale
7
+ // Technology (Phase 11C) — TechEra gates trade multipliers and military force
8
+ // Disease (Phase 56) — airborne epidemic spread at population scale
9
+ // Campaign (Phase 22) — locationIds map to Campaign Location graph
10
+ //
11
+ // No kernel import — pure data-management module, fixed-point arithmetic only.
12
+ import { SCALE, q, clampQ, qMul, mulDiv } from "./units.js";
13
+ import { STANDING_NEUTRAL } from "./faction.js";
14
+ import { eventSeed, hashString } from "./sim/seeds.js";
15
+ import { makeRng } from "./rng.js";
16
+ // ── Constants ─────────────────────────────────────────────────────────────────
17
+ /** Population count at which military potential equals q(1.0). */
18
+ export const POLITY_POP_SCALE = 100_000;
19
+ /**
20
+ * Military force multiplier by TechEra index.
21
+ * Higher eras give a fractional advantage on top of population and morale.
22
+ */
23
+ export const TECH_FORCE_MUL = [
24
+ q(0.40), // 0 — Prehistoric
25
+ q(0.55), // 1 — Ancient
26
+ q(0.65), // 2 — Medieval
27
+ q(0.75), // 3 — EarlyModern
28
+ q(0.85), // 4 — Industrial
29
+ q(0.95), // 5 — Modern
30
+ q(1.10), // 6 — NearFuture
31
+ q(1.30), // 7 — FarFuture
32
+ q(1.50),
33
+ ];
34
+ /**
35
+ * Trade output multiplier by the *lower* of the two polities' TechEra.
36
+ * Advanced tech means more valuable tradeable goods.
37
+ */
38
+ export const TECH_TRADE_MUL = [
39
+ q(0.50), // 0 — Prehistoric
40
+ q(0.60), // 1 — Ancient
41
+ q(0.70), // 2 — Medieval
42
+ q(0.80), // 3 — EarlyModern
43
+ q(0.90), // 4 — Industrial
44
+ q(1.00), // 5 — Modern
45
+ q(1.10), // 6 — NearFuture
46
+ q(1.20), // 7 — FarFuture
47
+ q(1.30),
48
+ ];
49
+ /** Fraction of min(treasury) exchanged as mutual trade income per day. */
50
+ export const TRADE_RATE_PER_DAY_Q = q(0.010);
51
+ /**
52
+ * Treasury cost to advance one tech era (indexed by current era).
53
+ * Era 8 (DeepSpace) is the maximum; index 8 cost is unused.
54
+ */
55
+ export const TECH_ADVANCE_COST = [
56
+ 2_000, // Prehistoric → Ancient
57
+ 8_000, // Ancient → Medieval
58
+ 20_000, // Medieval → EarlyModern
59
+ 50_000, // EarlyModern → Industrial
60
+ 120_000, // Industrial → Modern
61
+ 300_000, // Modern → NearFuture
62
+ 750_000, // NearFuture → FarFuture
63
+ 2_000_000, // FarFuture → DeepSpace
64
+ 0, // DeepSpace (max — unused)
65
+ ];
66
+ /** Defender's structural advantage in war resolution (home terrain, fortifications). */
67
+ export const DEFENDER_ADVANTAGE_Q = q(1.20);
68
+ /** Outcome uncertainty range in war: attacker power is scaled by [q(0.80), q(1.30)]. */
69
+ export const WAR_UNCERTAINTY_Q = q(0.50); // spans +q(0.50) above q(0.80) floor
70
+ /** Stability penalty applied to the losing side per war day-tick. */
71
+ export const WAR_LOSER_STABILITY_HIT = q(0.05);
72
+ /** Stability bonus for the winning side per war day-tick. */
73
+ export const WAR_WINNER_STABILITY_GAIN = q(0.02);
74
+ /** Daily stability decay absent active governance. */
75
+ export const STABILITY_DECAY_PER_DAY = q(0.002);
76
+ /** Daily stability recovery when morale > q(0.50). */
77
+ export const STABILITY_RECOVERY_PER_DAY = q(0.004);
78
+ /** Daily morale gain when stability ≥ UNREST_THRESHOLD. */
79
+ export const MORALE_RECOVERY_PER_DAY = q(0.003);
80
+ /** Daily morale drain when stability < UNREST_THRESHOLD. */
81
+ export const MORALE_DRAIN_PER_DAY = q(0.008);
82
+ /** Stability below this value triggers morale drain instead of recovery. */
83
+ export const UNREST_THRESHOLD = q(0.30);
84
+ /** Maximum standing delta per successful diplomatic negotiation. */
85
+ export const DIPLOMACY_MAX_DELTA = q(0.08);
86
+ /**
87
+ * Population per controlled location above which airborne disease spreads
88
+ * at polity scale (instead of only entity-to-entity).
89
+ */
90
+ export const DENSITY_SPREAD_THRESHOLD = 5_000;
91
+ // ── Factory ───────────────────────────────────────────────────────────────────
92
+ /**
93
+ * Create a Polity with derived `militaryStrength_Q`.
94
+ *
95
+ * Default starting stability and morale represent a stable, reasonably
96
+ * content polity (stability q(0.70), morale q(0.65)).
97
+ */
98
+ export function createPolity(id, name, factionId, locationIds, population, treasury_cu, techEra, stabilityQ = q(0.70), moraleQ = q(0.65)) {
99
+ const polity = {
100
+ id, name, factionId, locationIds, population, treasury_cu, techEra,
101
+ militaryStrength_Q: q(0),
102
+ stabilityQ,
103
+ moraleQ,
104
+ };
105
+ polity.militaryStrength_Q = deriveMilitaryStrength(polity);
106
+ return polity;
107
+ }
108
+ /**
109
+ * Create a PolityRegistry from an array of polities.
110
+ * No wars or alliances are registered by default.
111
+ */
112
+ export function createPolityRegistry(polities) {
113
+ return {
114
+ polities: new Map(polities.map(p => [p.id, p])),
115
+ activeWars: new Set(),
116
+ alliances: new Map(),
117
+ };
118
+ }
119
+ // ── Military ──────────────────────────────────────────────────────────────────
120
+ /**
121
+ * Derive and update `polity.militaryStrength_Q` from population, morale, and
122
+ * tech era.
123
+ *
124
+ * Formula: `clamp(popFrac × morale × techMul, 0, SCALE.Q)`
125
+ *
126
+ * - `popFrac` = `population / POLITY_POP_SCALE`, clamped to [0, SCALE.Q]
127
+ * (100 000 people = q(1.0) military potential)
128
+ * - `morale` and `techMul` are Q multipliers; result is clamped to SCALE.Q.
129
+ *
130
+ * Mutates `polity.militaryStrength_Q` and returns the new value.
131
+ */
132
+ export function deriveMilitaryStrength(polity) {
133
+ const popFrac = clampQ(Math.round(polity.population * SCALE.Q / POLITY_POP_SCALE), 0, SCALE.Q);
134
+ const techMul = (TECH_FORCE_MUL[polity.techEra] ?? q(0.40));
135
+ const withMorale = qMul(popFrac, polity.moraleQ);
136
+ const strength = clampQ(qMul(withMorale, techMul), 0, SCALE.Q);
137
+ polity.militaryStrength_Q = strength;
138
+ return strength;
139
+ }
140
+ // ── Trade ─────────────────────────────────────────────────────────────────────
141
+ /**
142
+ * Compute the daily trade income credited to each polity.
143
+ *
144
+ * Both polities receive the same `incomeEach_cu`. Scales with:
145
+ * - min(treasury): limited by the poorer partner
146
+ * - routeQuality_Q: navigator skill (Phase 38 `logicalMathematical`)
147
+ * - lower tech era of the pair: advanced goods multiply trade value
148
+ * - sharedLocations: more border crossings → more trade routes
149
+ *
150
+ * Returns 0 when either treasury is empty or `sharedLocations <= 0`.
151
+ */
152
+ export function computeTradeIncome(polityA, polityB, sharedLocations, routeQuality_Q) {
153
+ if (sharedLocations <= 0 || polityA.treasury_cu <= 0 || polityB.treasury_cu <= 0)
154
+ return 0;
155
+ const tradeBase = Math.min(polityA.treasury_cu, polityB.treasury_cu);
156
+ const techEra = Math.min(polityA.techEra, polityB.techEra);
157
+ const techMul_Q = (TECH_TRADE_MUL[techEra] ?? q(0.50));
158
+ // Location multiplier: q(0.60) at 1 shared location, capped at q(1.0) at 4+
159
+ const locMul_Q = clampQ((q(0.50) + sharedLocations * q(0.10)), q(0.50), q(1.0));
160
+ // income = tradeBase × TRADE_RATE × routeQuality × techMul × locMul / SCALE.Q⁴
161
+ const step1 = mulDiv(tradeBase, TRADE_RATE_PER_DAY_Q, SCALE.Q);
162
+ const step2 = mulDiv(step1, routeQuality_Q, SCALE.Q);
163
+ const step3 = mulDiv(step2, techMul_Q, SCALE.Q);
164
+ return Math.max(0, Math.round(mulDiv(step3, locMul_Q, SCALE.Q)));
165
+ }
166
+ // ── War ───────────────────────────────────────────────────────────────────────
167
+ /**
168
+ * Resolve one day of active warfare between two polities.
169
+ *
170
+ * Deterministic given (`worldSeed`, `tick`). The defender receives a
171
+ * built-in structural advantage (DEFENDER_ADVANTAGE_Q = q(1.20)).
172
+ * Attacker power is modified by a deterministic ±q(0.20) uncertainty roll.
173
+ *
174
+ * On attacker victory the first location of the defender is transferred.
175
+ * Stability consequences are returned as deltas; the caller applies them
176
+ * (or call `stepPolityDay` to handle that automatically for active wars).
177
+ */
178
+ export function resolveWarOutcome(attacker, defender, worldSeed, tick) {
179
+ const aSalt = hashString(attacker.id);
180
+ const dSalt = hashString(defender.id);
181
+ const seed = eventSeed(worldSeed, tick, aSalt, dSalt, 0xFA17);
182
+ const rng = makeRng(seed, SCALE.Q);
183
+ const roll = rng.q01(); // 0 … SCALE.Q − 1
184
+ // uncertainty_Q ∈ [q(0.80), q(1.20)]
185
+ const uncertainty_Q = (q(0.80) + mulDiv(roll, WAR_UNCERTAINTY_Q, SCALE.Q));
186
+ const attackPower = qMul(attacker.militaryStrength_Q, uncertainty_Q);
187
+ const defendPower = qMul(defender.militaryStrength_Q, DEFENDER_ADVANTAGE_Q);
188
+ const attackerWins = attackPower > defendPower;
189
+ const territoryGained = [];
190
+ if (attackerWins && defender.locationIds.length > 0) {
191
+ territoryGained.push(defender.locationIds[0]);
192
+ }
193
+ return {
194
+ attackerId: attacker.id,
195
+ defenderId: defender.id,
196
+ attackerWins,
197
+ stabilityDeltaAttacker: (attackerWins ? WAR_WINNER_STABILITY_GAIN : -WAR_LOSER_STABILITY_HIT),
198
+ stabilityDeltaDefender: (attackerWins ? -WAR_LOSER_STABILITY_HIT : WAR_WINNER_STABILITY_GAIN),
199
+ territoryGained,
200
+ };
201
+ }
202
+ // ── Diplomacy ─────────────────────────────────────────────────────────────────
203
+ /**
204
+ * Resolve a diplomatic negotiation between two polities.
205
+ *
206
+ * Returns a positive `standingDelta` to apply to the FactionRegistry global
207
+ * standing between the two polities' factions via `applyFactionStanding`.
208
+ *
209
+ * Standing improvement scales with:
210
+ * - `diplomatLinguistic_Q`: best envoy's `linguisticIntelligence_Q` (Phase 37)
211
+ * - headroom: how far below ALLY standing (q(0.70)) the current relation is
212
+ * (no improvement when already at or above ALLY)
213
+ *
214
+ * Maximum delta per negotiation is DIPLOMACY_MAX_DELTA (q(0.08)).
215
+ */
216
+ export function resolveDiplomacy(polityA, polityB, diplomatLinguistic_Q, currentStanding_Q) {
217
+ // headroom = how far below ALLY standing the current relation sits
218
+ const headroom = clampQ(q(0.70) - currentStanding_Q, 0, SCALE.Q);
219
+ const rawDelta = mulDiv(qMul(headroom, diplomatLinguistic_Q), DIPLOMACY_MAX_DELTA, SCALE.Q);
220
+ const standingDelta = clampQ(rawDelta, 0, DIPLOMACY_MAX_DELTA);
221
+ return { polityAId: polityA.id, polityBId: polityB.id, standingDelta };
222
+ }
223
+ // ── Technology ────────────────────────────────────────────────────────────────
224
+ /**
225
+ * Return true if the polity meets the conditions to advance to the next tech era.
226
+ *
227
+ * Requires:
228
+ * 1. A research project has been completed (`projectCompleted = true`).
229
+ * 2. Treasury meets the advancement cost for the current era.
230
+ * 3. Not already at maximum era (DeepSpace, index 8).
231
+ */
232
+ export function canAdvanceTech(polity, projectCompleted) {
233
+ if (!projectCompleted)
234
+ return false;
235
+ const maxEra = TECH_ADVANCE_COST.length - 1;
236
+ if (polity.techEra >= maxEra)
237
+ return false;
238
+ const cost = TECH_ADVANCE_COST[polity.techEra] ?? 0;
239
+ return polity.treasury_cu >= cost;
240
+ }
241
+ /**
242
+ * Advance polity to the next tech era if eligible.
243
+ *
244
+ * Mutates `polity.techEra` and `polity.treasury_cu`.
245
+ * Refreshes `militaryStrength_Q` after advancement.
246
+ * Returns `true` if advancement occurred.
247
+ */
248
+ export function advanceTechEra(polity, projectCompleted) {
249
+ if (!canAdvanceTech(polity, projectCompleted))
250
+ return false;
251
+ const cost = TECH_ADVANCE_COST[polity.techEra] ?? 0;
252
+ polity.treasury_cu -= cost;
253
+ polity.techEra = (polity.techEra + 1);
254
+ deriveMilitaryStrength(polity);
255
+ return true;
256
+ }
257
+ // ── Disease ───────────────────────────────────────────────────────────────────
258
+ /**
259
+ * Compute population-scale disease spread for one simulated day.
260
+ *
261
+ * Only `"airborne"` diseases spread at polity scale; other routes remain
262
+ * entity-to-entity (handled by Phase 56 `spreadDisease`).
263
+ *
264
+ * Spread activates when population density
265
+ * (`population / locationIds.length`) exceeds DENSITY_SPREAD_THRESHOLD.
266
+ *
267
+ * Mutates `polity.population` by `populationDelta` (negative = deaths).
268
+ * Returns zeros when conditions are not met.
269
+ */
270
+ export function computePolityDiseaseSpread(polity, profile, worldSeed, tick) {
271
+ if (profile.transmissionRoute !== "airborne") {
272
+ return { polityId: polity.id, newExposures: 0, populationDelta: 0 };
273
+ }
274
+ const locCount = Math.max(1, polity.locationIds.length);
275
+ const density = Math.round(polity.population / locCount);
276
+ if (density < DENSITY_SPREAD_THRESHOLD) {
277
+ return { polityId: polity.id, newExposures: 0, populationDelta: 0 };
278
+ }
279
+ // Density excess fraction drives spread risk
280
+ const excessFrac_Q = clampQ(Math.round((density - DENSITY_SPREAD_THRESHOLD) * SCALE.Q / DENSITY_SPREAD_THRESHOLD), 0, SCALE.Q);
281
+ const spreadRisk_Q = clampQ(qMul(excessFrac_Q, profile.baseTransmissionRate_Q), 0, SCALE.Q);
282
+ // Deterministic daily exposure roll
283
+ const salt = hashString(profile.id);
284
+ const seed = eventSeed(worldSeed, tick, salt, 0, 0xD15E);
285
+ const rng = makeRng(seed, SCALE.Q);
286
+ const roll_Q = rng.q01();
287
+ const actualRisk_Q = clampQ(mulDiv(spreadRisk_Q, roll_Q, SCALE.Q), 0, SCALE.Q);
288
+ const newExposures = Math.max(0, Math.round(polity.population * actualRisk_Q / SCALE.Q));
289
+ const mortalityFrac_Q = clampQ(qMul(profile.mortalityRate_Q, profile.symptomSeverity_Q), 0, SCALE.Q);
290
+ const populationDelta = -Math.max(0, Math.round(newExposures * mortalityFrac_Q / SCALE.Q));
291
+ polity.population = Math.max(0, polity.population + populationDelta);
292
+ return { polityId: polity.id, newExposures, populationDelta };
293
+ }
294
+ // ── Day step ──────────────────────────────────────────────────────────────────
295
+ /**
296
+ * Advance all polities by one simulated day.
297
+ *
298
+ * Performs three phases in order:
299
+ *
300
+ * **Trade**: For each non-warring pair, compute and credit mutual trade income.
301
+ *
302
+ * **War**: For each active war, resolve one day of combat, apply stability
303
+ * consequences, and transfer territory on attacker victory.
304
+ *
305
+ * **Morale/Stability**: For each polity:
306
+ * - Stability decays daily; recovers when morale > q(0.50).
307
+ * - Morale drains when stability < UNREST_THRESHOLD; recovers otherwise.
308
+ * - `militaryStrength_Q` is refreshed.
309
+ *
310
+ * Disease spread is NOT handled here; call `computePolityDiseaseSpread`
311
+ * per-disease per-polity as the host iterates active outbreaks.
312
+ *
313
+ * Mutates polities in `registry.polities` and registry.activeWars (territory
314
+ * transfers may empty `locationIds`, but war entries are not auto-removed).
315
+ */
316
+ export function stepPolityDay(registry, pairs, worldSeed, tick) {
317
+ const tradeResults = [];
318
+ const moraleDeltas = new Map();
319
+ const stabilityDeltas = new Map();
320
+ // ── Trade phase ────────────────────────────────────────────────────────────
321
+ for (const pair of pairs) {
322
+ const polityA = registry.polities.get(pair.polityAId);
323
+ const polityB = registry.polities.get(pair.polityBId);
324
+ if (!polityA || !polityB)
325
+ continue;
326
+ // No trade during active war
327
+ const warKey = [pair.polityAId, pair.polityBId].sort().join(":");
328
+ if (registry.activeWars.has(warKey))
329
+ continue;
330
+ const income = computeTradeIncome(polityA, polityB, pair.sharedLocations, pair.routeQuality_Q);
331
+ if (income > 0) {
332
+ polityA.treasury_cu += income;
333
+ polityB.treasury_cu += income;
334
+ tradeResults.push({ polityAId: pair.polityAId, polityBId: pair.polityBId, incomeEach_cu: income });
335
+ }
336
+ }
337
+ // ── War phase ──────────────────────────────────────────────────────────────
338
+ for (const warKey of registry.activeWars) {
339
+ const [idA, idB] = warKey.split(":");
340
+ const polityA = registry.polities.get(idA);
341
+ const polityB = registry.polities.get(idB);
342
+ if (!polityA || !polityB)
343
+ continue;
344
+ // Alphabetically-first polity is treated as the aggressor this tick
345
+ const result = resolveWarOutcome(polityA, polityB, worldSeed, tick);
346
+ polityA.stabilityQ = clampQ(polityA.stabilityQ + result.stabilityDeltaAttacker, 0, SCALE.Q);
347
+ polityB.stabilityQ = clampQ(polityB.stabilityQ + result.stabilityDeltaDefender, 0, SCALE.Q);
348
+ // Territory transfer on attacker victory
349
+ if (result.attackerWins) {
350
+ for (const locId of result.territoryGained) {
351
+ polityB.locationIds = polityB.locationIds.filter(id => id !== locId);
352
+ polityA.locationIds = [...polityA.locationIds, locId];
353
+ }
354
+ }
355
+ }
356
+ // ── Morale & stability phase ───────────────────────────────────────────────
357
+ for (const [id, polity] of registry.polities) {
358
+ // Stability: decays always; net positive when morale is healthy
359
+ const stabilityDelta = (polity.moraleQ > q(0.50)
360
+ ? STABILITY_RECOVERY_PER_DAY - STABILITY_DECAY_PER_DAY
361
+ : -STABILITY_DECAY_PER_DAY);
362
+ polity.stabilityQ = clampQ(polity.stabilityQ + stabilityDelta, 0, SCALE.Q);
363
+ stabilityDeltas.set(id, stabilityDelta);
364
+ // Morale: drains under unrest, recovers when stable
365
+ const moraleDelta = (polity.stabilityQ < UNREST_THRESHOLD
366
+ ? -MORALE_DRAIN_PER_DAY
367
+ : MORALE_RECOVERY_PER_DAY);
368
+ polity.moraleQ = clampQ(polity.moraleQ + moraleDelta, 0, SCALE.Q);
369
+ moraleDeltas.set(id, moraleDelta);
370
+ deriveMilitaryStrength(polity);
371
+ }
372
+ return { trade: tradeResults, moraleDeltas, stabilityDeltas };
373
+ }
374
+ // ── War registry helpers ───────────────────────────────────────────────────────
375
+ /** Register a state of war between two polities. Idempotent. */
376
+ export function declareWar(registry, polityAId, polityBId) {
377
+ registry.activeWars.add([polityAId, polityBId].sort().join(":"));
378
+ }
379
+ /** End the state of war between two polities. Idempotent. */
380
+ export function makePeace(registry, polityAId, polityBId) {
381
+ registry.activeWars.delete([polityAId, polityBId].sort().join(":"));
382
+ }
383
+ /** Return true if two polities are currently at war. */
384
+ export function areAtWar(registry, polityAId, polityBId) {
385
+ return registry.activeWars.has([polityAId, polityBId].sort().join(":"));
386
+ }
387
+ // ── Faction standing bridge ────────────────────────────────────────────────────
388
+ /**
389
+ * Look up the current faction-level standing that polityA's faction holds
390
+ * toward polityB's faction in the FactionRegistry.
391
+ *
392
+ * Returns STANDING_NEUTRAL (q(0.50)) if no relation is registered.
393
+ * Use this as `currentStanding_Q` for `resolveDiplomacy`.
394
+ */
395
+ export function polityFactionStanding(factionRegistry, polityA, polityB) {
396
+ return factionRegistry.globalStanding
397
+ .get(polityA.factionId)?.get(polityB.factionId) ?? STANDING_NEUTRAL;
398
+ }