@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,17 @@
1
+ import type { Chronicle, StoryArc } from "./chronicle.js";
2
+ /** Detect all story arcs in a chronicle. */
3
+ export declare function detectStoryArcs(chronicle: Chronicle): StoryArc[];
4
+ /** Re-detect arcs and update chronicle. */
5
+ export declare function updateDetectedArcs(chronicle: Chronicle): void;
6
+ /** Get all arcs involving a specific entity. */
7
+ export declare function getArcsForEntity(chronicle: Chronicle, entityId: number): StoryArc[];
8
+ /** Get the most significant arc in a chronicle. */
9
+ export declare function getMostSignificantArc(chronicle: Chronicle): StoryArc | undefined;
10
+ /** Get arcs within a time period. */
11
+ export declare function getArcsInPeriod(chronicle: Chronicle, startTick: number, endTick: number): StoryArc[];
12
+ /** Check if an arc is ongoing (no end tick). */
13
+ export declare function isOngoingArc(arc: StoryArc, currentTick: number): boolean;
14
+ /** Generate a human-readable summary of an arc. */
15
+ export declare function renderArcDescription(arc: StoryArc): string;
16
+ /** Get arcs that form a connected narrative (shared actors). */
17
+ export declare function getConnectedArcs(chronicle: Chronicle, arcId: string): StoryArc[];
@@ -0,0 +1,276 @@
1
+ // src/story-arcs.ts — Phase 45: Emergent Story Generation — Story Arc Detection
2
+ //
3
+ // Pattern detection across chronicle entries to identify emergent narratives.
4
+ import { getEntriesForEntity } from "./chronicle.js";
5
+ // ── Arc Detection Entry Points ─────────────────────────────────────────────────
6
+ /** Detect all story arcs in a chronicle. */
7
+ export function detectStoryArcs(chronicle) {
8
+ const arcs = [];
9
+ // Detect each arc type
10
+ const riseOfHeroArcs = detectRiseOfHeroArcs(chronicle);
11
+ arcs.push(...riseOfHeroArcs);
12
+ const tragicFallArcs = detectTragicFallArcs(chronicle);
13
+ arcs.push(...tragicFallArcs);
14
+ const rivalryArcs = detectRivalryArcs(chronicle);
15
+ arcs.push(...rivalryArcs);
16
+ const settlementGrowthArcs = detectSettlementGrowthArcs(chronicle);
17
+ arcs.push(...settlementGrowthArcs);
18
+ const legendaryCraftsmanArcs = detectLegendaryCraftsmanArcs(chronicle);
19
+ arcs.push(...legendaryCraftsmanArcs);
20
+ // Sort arcs by start tick
21
+ arcs.sort((a, b) => a.startTick - b.startTick);
22
+ return arcs;
23
+ }
24
+ /** Re-detect arcs and update chronicle. */
25
+ export function updateDetectedArcs(chronicle) {
26
+ chronicle.detectedArcs = detectStoryArcs(chronicle);
27
+ }
28
+ // ── Rise of a Hero ────────────────────────────────────────────────────────────
29
+ /**
30
+ * Detect "Rise of a Hero" arcs.
31
+ * Pattern: Entity survives multiple combats, gains positive reputation,
32
+ * completes difficult quests, doesn't die.
33
+ */
34
+ function detectRiseOfHeroArcs(chronicle) {
35
+ const arcs = [];
36
+ const entityCompletions = new Map();
37
+ // Find quest completions and combat victories by entity
38
+ for (const entry of chronicle.entries) {
39
+ if (entry.eventType === "quest_completed" || entry.eventType === "combat_victory") {
40
+ for (const actor of entry.actors) {
41
+ if (!entityCompletions.has(actor)) {
42
+ entityCompletions.set(actor, []);
43
+ }
44
+ entityCompletions.get(actor).push(entry);
45
+ }
46
+ }
47
+ }
48
+ // Check for entities with multiple successes
49
+ for (const [entityId, entries] of entityCompletions) {
50
+ if (entries.length >= 3) {
51
+ // Check if entity has a death entry (arc would be cut short)
52
+ const deathEntry = getEntriesForEntity(chronicle, entityId).find(e => e.eventType === "entity_death");
53
+ if (!deathEntry) {
54
+ // Living hero - check for legendary deed
55
+ const legendaryEntry = entries.find(e => e.eventType === "legendary_deed");
56
+ const significance = 60 + entries.length * 5 + (legendaryEntry ? 20 : 0);
57
+ arcs.push({
58
+ arcId: `rise_hero_${entityId}_${entries[0].tick}`,
59
+ arcType: "rise_of_hero",
60
+ entryIds: entries.map(e => e.entryId),
61
+ primaryActors: [entityId],
62
+ startTick: entries[0].tick,
63
+ endTick: entries[entries.length - 1].tick,
64
+ significance: Math.min(100, significance),
65
+ description: `The rise of a hero through ${entries.length} notable victories`,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ return arcs;
71
+ }
72
+ // ── Tragic Fall ───────────────────────────────────────────────────────────────
73
+ /**
74
+ * Detect "Tragic Fall" arcs.
75
+ * Pattern: Entity with high reputation commits betrayal, becomes villain,
76
+ * or falls from grace in some other way.
77
+ */
78
+ function detectTragicFallArcs(chronicle) {
79
+ const arcs = [];
80
+ const entityEntries = new Map();
81
+ // Group entries by actor
82
+ for (const entry of chronicle.entries) {
83
+ for (const actor of entry.actors) {
84
+ if (!entityEntries.has(actor)) {
85
+ entityEntries.set(actor, []);
86
+ }
87
+ entityEntries.get(actor).push(entry);
88
+ }
89
+ }
90
+ // Look for betrayal after positive events
91
+ for (const [entityId, entries] of entityEntries) {
92
+ const betrayalIndex = entries.findIndex(e => e.eventType === "relationship_betrayal");
93
+ if (betrayalIndex > 0) {
94
+ // Check for prior positive events
95
+ const priorEntries = entries.slice(0, betrayalIndex);
96
+ const positiveEvents = priorEntries.filter(e => e.eventType === "quest_completed" || e.eventType === "relationship_formed");
97
+ if (positiveEvents.length >= 2) {
98
+ const relevantEntries = [...positiveEvents, entries[betrayalIndex]];
99
+ arcs.push({
100
+ arcId: `tragic_fall_${entityId}_${entries[0].tick}`,
101
+ arcType: "tragic_fall",
102
+ entryIds: relevantEntries.map(e => e.entryId),
103
+ primaryActors: [entityId],
104
+ startTick: relevantEntries[0].tick,
105
+ endTick: entries[entries.length - 1]?.tick,
106
+ significance: 75,
107
+ description: `A tragic fall from grace after betrayal of former allies`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ return arcs;
113
+ }
114
+ // ── Rivalry ───────────────────────────────────────────────────────────────────
115
+ /**
116
+ * Detect "Rivalry" arcs.
117
+ * Pattern: Two entities repeatedly fight/combat, neither dies,
118
+ * relationship remains negative or hostile.
119
+ */
120
+ function detectRivalryArcs(chronicle) {
121
+ const arcs = [];
122
+ const combatPairs = new Map();
123
+ // Find combats with exactly 2 actors
124
+ for (const entry of chronicle.entries) {
125
+ if ((entry.eventType === "combat_victory" || entry.eventType === "combat_defeat") &&
126
+ entry.actors.length === 2) {
127
+ const pairKey = entry.actors.sort().join("_");
128
+ if (!combatPairs.has(pairKey)) {
129
+ combatPairs.set(pairKey, []);
130
+ }
131
+ combatPairs.get(pairKey).push(entry);
132
+ }
133
+ }
134
+ // Check for repeated combats between same pair
135
+ for (const [pairKey, entries] of combatPairs) {
136
+ if (entries.length >= 2) {
137
+ const ids = pairKey.split("_").map(Number);
138
+ const entityA = ids[0];
139
+ const entityB = ids[1];
140
+ // Check neither has died
141
+ const deathA = getEntriesForEntity(chronicle, entityA).find(e => e.eventType === "entity_death");
142
+ const deathB = getEntriesForEntity(chronicle, entityB).find(e => e.eventType === "entity_death");
143
+ if (!deathA && !deathB) {
144
+ arcs.push({
145
+ arcId: `rivalry_${pairKey}_${entries[0].tick}`,
146
+ arcType: "rivalry",
147
+ entryIds: entries.map(e => e.entryId),
148
+ primaryActors: [entityA, entityB],
149
+ startTick: entries[0].tick,
150
+ endTick: entries[entries.length - 1].tick,
151
+ significance: 50 + entries.length * 5,
152
+ description: `An ongoing rivalry with ${entries.length} recorded confrontations`,
153
+ });
154
+ }
155
+ }
156
+ }
157
+ return arcs;
158
+ }
159
+ // ── Settlement Growth ─────────────────────────────────────────────────────────
160
+ /**
161
+ * Detect "Settlement Growth" arcs.
162
+ * Pattern: Settlement founded → upgraded multiple times → population growth.
163
+ */
164
+ function detectSettlementGrowthArcs(chronicle) {
165
+ const arcs = [];
166
+ const settlementEvents = new Map();
167
+ // Group settlement-related entries
168
+ for (const entry of chronicle.entries) {
169
+ if (entry.settlementId && (entry.eventType === "settlement_founded" ||
170
+ entry.eventType === "settlement_upgraded" ||
171
+ entry.eventType === "facility_completed")) {
172
+ if (!settlementEvents.has(entry.settlementId)) {
173
+ settlementEvents.set(entry.settlementId, []);
174
+ }
175
+ settlementEvents.get(entry.settlementId).push(entry);
176
+ }
177
+ }
178
+ // Check for settlements with growth trajectory
179
+ for (const [settlementId, entries] of settlementEvents) {
180
+ const hasFoundation = entries.some(e => e.eventType === "settlement_founded");
181
+ const upgrades = entries.filter(e => e.eventType === "settlement_upgraded").length;
182
+ if (hasFoundation && upgrades >= 1) {
183
+ arcs.push({
184
+ arcId: `settlement_growth_${settlementId}_${entries[0].tick}`,
185
+ arcType: "settlement_growth",
186
+ entryIds: entries.map(e => e.entryId),
187
+ primaryActors: [], // Settlement itself is the subject
188
+ startTick: entries[0].tick,
189
+ endTick: entries[entries.length - 1].tick,
190
+ significance: 55 + upgrades * 10,
191
+ description: `The growth of a settlement from founding through ${upgrades} upgrades`,
192
+ });
193
+ }
194
+ }
195
+ return arcs;
196
+ }
197
+ // ── Legendary Craftsman ───────────────────────────────────────────────────────
198
+ /**
199
+ * Detect "Legendary Craftsman" arcs.
200
+ * Pattern: Entity produces multiple masterwork items, exceptional competence outcomes.
201
+ */
202
+ function detectLegendaryCraftsmanArcs(chronicle) {
203
+ const arcs = [];
204
+ const entityCrafts = new Map();
205
+ // Find masterwork crafting events
206
+ for (const entry of chronicle.entries) {
207
+ if (entry.eventType === "masterwork_crafted") {
208
+ for (const actor of entry.actors) {
209
+ if (!entityCrafts.has(actor)) {
210
+ entityCrafts.set(actor, []);
211
+ }
212
+ entityCrafts.get(actor).push(entry);
213
+ }
214
+ }
215
+ }
216
+ // Check for craftsmen with multiple masterworks
217
+ for (const [entityId, entries] of entityCrafts) {
218
+ if (entries.length >= 2) {
219
+ arcs.push({
220
+ arcId: `legendary_craftsman_${entityId}_${entries[0].tick}`,
221
+ arcType: "legendary_craftsman",
222
+ entryIds: entries.map(e => e.entryId),
223
+ primaryActors: [entityId],
224
+ startTick: entries[0].tick,
225
+ endTick: entries[entries.length - 1].tick,
226
+ significance: 60 + entries.length * 8,
227
+ description: `A legendary craftsman who created ${entries.length} masterwork items`,
228
+ });
229
+ }
230
+ }
231
+ return arcs;
232
+ }
233
+ // ── Arc Analysis ──────────────────────────────────────────────────────────────
234
+ /** Get all arcs involving a specific entity. */
235
+ export function getArcsForEntity(chronicle, entityId) {
236
+ return chronicle.detectedArcs.filter(arc => arc.primaryActors.includes(entityId));
237
+ }
238
+ /** Get the most significant arc in a chronicle. */
239
+ export function getMostSignificantArc(chronicle) {
240
+ if (chronicle.detectedArcs.length === 0)
241
+ return undefined;
242
+ return chronicle.detectedArcs.reduce((max, arc) => arc.significance > max.significance ? arc : max);
243
+ }
244
+ /** Get arcs within a time period. */
245
+ export function getArcsInPeriod(chronicle, startTick, endTick) {
246
+ return chronicle.detectedArcs.filter(arc => arc.startTick <= endTick && (arc.endTick ?? Infinity) >= startTick);
247
+ }
248
+ /** Check if an arc is ongoing (no end tick). */
249
+ export function isOngoingArc(arc, currentTick) {
250
+ return arc.endTick === undefined || arc.endTick >= currentTick;
251
+ }
252
+ // ── Arc Rendering ─────────────────────────────────────────────────────────────
253
+ /** Generate a human-readable summary of an arc. */
254
+ export function renderArcDescription(arc) {
255
+ const typeDescriptions = {
256
+ rise_of_hero: "A hero's rise to prominence",
257
+ tragic_fall: "A tragic fall from grace",
258
+ rivalry: "An ongoing rivalry",
259
+ great_migration: "A great migration",
260
+ settlement_growth: "The growth of a settlement",
261
+ fallen_settlement: "The fall of a settlement",
262
+ legendary_craftsman: "A legendary craftsman's legacy",
263
+ notorious_villain: "The rise of a notorious villain",
264
+ unlikely_friendship: "An unlikely friendship",
265
+ betrayal_and_redemption: "A tale of betrayal and redemption",
266
+ };
267
+ return typeDescriptions[arc.arcType] || arc.description;
268
+ }
269
+ /** Get arcs that form a connected narrative (shared actors). */
270
+ export function getConnectedArcs(chronicle, arcId) {
271
+ const targetArc = chronicle.detectedArcs.find(a => a.arcId === arcId);
272
+ if (!targetArc)
273
+ return [];
274
+ return chronicle.detectedArcs.filter(arc => arc.arcId !== arcId &&
275
+ arc.primaryActors.some(actor => targetArc.primaryActors.includes(actor)));
276
+ }
@@ -0,0 +1,80 @@
1
+ import { type Q } from "./units.js";
2
+ import type { Polity, PolityRegistry, PolityPair } from "./polity.js";
3
+ /** Maximum tech era index (DeepSpace = 8). */
4
+ export declare const MAX_TECH_ERA: number;
5
+ /**
6
+ * Base daily probability of era advance when the era gap is 1 and route quality
7
+ * is q(0.50) and there is one shared location. At this rate a lagging polity
8
+ * advances roughly once per 200 days (~7 months) under median conditions.
9
+ */
10
+ export declare const BASE_DIFFUSION_RATE_Q: Q;
11
+ /**
12
+ * Multiplier applied per additional era of gap beyond 1.
13
+ * At gap=2: ×1.5×; gap=3: ×2.0×; capped at gap=4 (×2.5×).
14
+ * Ensures large knowledge gradients trigger faster catch-up.
15
+ */
16
+ export declare const ERA_GAP_BONUS_Q: Q;
17
+ /** Maximum era-gap bonus (caps at gap=4 → ×3.0× the base). */
18
+ export declare const ERA_GAP_BONUS_MAX: Q;
19
+ /**
20
+ * Route quality contributes up to this multiplier on top of the base rate.
21
+ * routeQuality_Q = q(1.0) → +100% boost → 2× base rate.
22
+ */
23
+ export declare const ROUTE_QUALITY_MUL_MAX: Q;
24
+ /**
25
+ * Each additional shared location beyond 1 adds this fractional bonus.
26
+ * e.g., 3 shared locations → +2 × q(0.20) = +40% of base rate.
27
+ */
28
+ export declare const SHARED_LOCATION_BONUS: Q;
29
+ /** Maximum combined bonus from shared locations (caps at 5 locations). */
30
+ export declare const SHARED_LOCATION_MAX: Q;
31
+ /**
32
+ * Stability threshold below which a polity cannot absorb new technology.
33
+ * Unstable societies are too disorganised to institutionalise advances.
34
+ */
35
+ export declare const STABILITY_DIFFUSION_THRESHOLD: Q;
36
+ /** Outcome of a single polity's tech advance in one day-tick. */
37
+ export interface TechDiffusionResult {
38
+ polityId: string;
39
+ previousTechEra: number;
40
+ newTechEra: number;
41
+ }
42
+ /**
43
+ * Compute the daily diffusion pressure (probability of era advance) that the
44
+ * `source` polity exerts on the `target` via one `pair`.
45
+ *
46
+ * Returns q(0) when:
47
+ * - `source.techEra <= target.techEra` (no gradient)
48
+ * - `target.stabilityQ < STABILITY_DIFFUSION_THRESHOLD` (target is unstable)
49
+ * - `warActive === true` (war disrupts cultural contact)
50
+ *
51
+ * Otherwise returns a Q in (0, SCALE.Q] representing the per-day probability
52
+ * of the target advancing one era. The caller rolls against this value.
53
+ */
54
+ export declare function computeDiffusionPressure(source: Polity, target: Polity, pair: PolityPair, warActive: boolean): Q;
55
+ /**
56
+ * Advance technology through the polity pair graph for one simulated day.
57
+ *
58
+ * For each pair, checks both directions (A→B and B→A) and rolls against
59
+ * `computeDiffusionPressure`. A polity that advances during this step is
60
+ * not eligible to advance again in the same tick (one advance per tick max).
61
+ *
62
+ * Mutates `polity.techEra` (and refreshes `militaryStrength_Q`) for any
63
+ * polity that advances.
64
+ *
65
+ * Returns a `TechDiffusionResult[]` for every polity that advanced this tick.
66
+ */
67
+ export declare function stepTechDiffusion(registry: PolityRegistry, pairs: ReadonlyArray<PolityPair>, worldSeed: number, tick: number): TechDiffusionResult[];
68
+ /**
69
+ * Compute the net inbound diffusion pressure on a single polity from all its
70
+ * neighbours in the pair graph. Useful for AI queries ("how likely is this
71
+ * polity to advance soon?").
72
+ *
73
+ * War pairs are excluded. Pressure values are summed (uncapped).
74
+ */
75
+ export declare function totalInboundPressure(polityId: string, registry: PolityRegistry, pairs: ReadonlyArray<PolityPair>): Q;
76
+ /**
77
+ * Return the set of tech-era names available for a given era index.
78
+ * Useful for display in tools and reports.
79
+ */
80
+ export declare function techEraName(era: number): string;
@@ -0,0 +1,185 @@
1
+ // src/tech-diffusion.ts — Phase 67: Technology Diffusion at Polity Scale
2
+ //
3
+ // Technology eras spread from more-advanced polities to less-advanced neighbours
4
+ // via trade routes and cultural contact. Each day, for every pair where one
5
+ // polity leads the other by at least one era, the lagging polity rolls for a
6
+ // chance to advance — exactly one era at a time, never skipping.
7
+ //
8
+ // Diffusion pressure scales with:
9
+ // - era gap : larger gaps produce stronger "pull" (knowledge gradient)
10
+ // - route quality : better navigators carry ideas faster (Phase 61 routeQuality_Q)
11
+ // - shared locations : more border crossings → more cultural contact
12
+ // - war : active war between the pair sets diffusion to 0
13
+ // - stability : unstable polities are poor hosts for new ideas
14
+ //
15
+ // Historically grounded: trade routes (Silk Road, Mediterranean, Hanseatic League)
16
+ // were the primary vector for technology transfer in the pre-modern world.
17
+ // Model source: Bockstette, Chanda & Putterman (2002) "States and Markets: The
18
+ // Advantage of an Early Start".
19
+ //
20
+ // Phase hooks:
21
+ // Phase 61 (Polity, PolityPair, areAtWar) — registry and route graph
22
+ // Phase 11C (TechEra) — 9-era progression 0 (Prehistoric) → 8 (DeepSpace)
23
+ import { q, clampQ, qMul, SCALE } from "./units.js";
24
+ import { eventSeed, hashString } from "./sim/seeds.js";
25
+ import { makeRng } from "./rng.js";
26
+ import { areAtWar, deriveMilitaryStrength } from "./polity.js";
27
+ import { TechEra } from "./sim/tech.js";
28
+ // ── Constants ─────────────────────────────────────────────────────────────────
29
+ /** Maximum tech era index (DeepSpace = 8). */
30
+ export const MAX_TECH_ERA = TechEra.DeepSpace;
31
+ /**
32
+ * Base daily probability of era advance when the era gap is 1 and route quality
33
+ * is q(0.50) and there is one shared location. At this rate a lagging polity
34
+ * advances roughly once per 200 days (~7 months) under median conditions.
35
+ */
36
+ export const BASE_DIFFUSION_RATE_Q = q(0.005);
37
+ /**
38
+ * Multiplier applied per additional era of gap beyond 1.
39
+ * At gap=2: ×1.5×; gap=3: ×2.0×; capped at gap=4 (×2.5×).
40
+ * Ensures large knowledge gradients trigger faster catch-up.
41
+ */
42
+ export const ERA_GAP_BONUS_Q = q(0.50);
43
+ /** Maximum era-gap bonus (caps at gap=4 → ×3.0× the base). */
44
+ export const ERA_GAP_BONUS_MAX = q(2.00);
45
+ /**
46
+ * Route quality contributes up to this multiplier on top of the base rate.
47
+ * routeQuality_Q = q(1.0) → +100% boost → 2× base rate.
48
+ */
49
+ export const ROUTE_QUALITY_MUL_MAX = q(1.00);
50
+ /**
51
+ * Each additional shared location beyond 1 adds this fractional bonus.
52
+ * e.g., 3 shared locations → +2 × q(0.20) = +40% of base rate.
53
+ */
54
+ export const SHARED_LOCATION_BONUS = q(0.20);
55
+ /** Maximum combined bonus from shared locations (caps at 5 locations). */
56
+ export const SHARED_LOCATION_MAX = q(0.80);
57
+ /**
58
+ * Stability threshold below which a polity cannot absorb new technology.
59
+ * Unstable societies are too disorganised to institutionalise advances.
60
+ */
61
+ export const STABILITY_DIFFUSION_THRESHOLD = q(0.25);
62
+ // ── Core computation ──────────────────────────────────────────────────────────
63
+ /**
64
+ * Compute the daily diffusion pressure (probability of era advance) that the
65
+ * `source` polity exerts on the `target` via one `pair`.
66
+ *
67
+ * Returns q(0) when:
68
+ * - `source.techEra <= target.techEra` (no gradient)
69
+ * - `target.stabilityQ < STABILITY_DIFFUSION_THRESHOLD` (target is unstable)
70
+ * - `warActive === true` (war disrupts cultural contact)
71
+ *
72
+ * Otherwise returns a Q in (0, SCALE.Q] representing the per-day probability
73
+ * of the target advancing one era. The caller rolls against this value.
74
+ */
75
+ export function computeDiffusionPressure(source, target, pair, warActive) {
76
+ if (warActive)
77
+ return q(0);
78
+ if (source.techEra <= target.techEra)
79
+ return q(0);
80
+ if (target.stabilityQ < STABILITY_DIFFUSION_THRESHOLD)
81
+ return q(0);
82
+ // Era gap bonus: 0 for gap=1, +ERA_GAP_BONUS_Q per additional gap, capped
83
+ const gap = source.techEra - target.techEra;
84
+ const gapBonus = clampQ(ERA_GAP_BONUS_Q * (gap - 1), 0, ERA_GAP_BONUS_MAX);
85
+ const gapMul = (SCALE.Q + gapBonus);
86
+ // Route quality bonus: routeQuality scales from 0 → ROUTE_QUALITY_MUL_MAX bonus
87
+ const routeBonus = qMul(pair.routeQuality_Q, ROUTE_QUALITY_MUL_MAX);
88
+ const routeMul = (SCALE.Q + routeBonus);
89
+ // Shared locations bonus: extra locations beyond 1 add fractional boost
90
+ const extraLocs = Math.max(0, pair.sharedLocations - 1);
91
+ const locBonus = clampQ(SHARED_LOCATION_BONUS * extraLocs, 0, SHARED_LOCATION_MAX);
92
+ const locMul = (SCALE.Q + locBonus);
93
+ // Combine: base × gapMul × routeMul × locMul (all ÷ SCALE.Q after each multiply)
94
+ const step1 = qMul(BASE_DIFFUSION_RATE_Q, gapMul);
95
+ const step2 = qMul(step1, routeMul);
96
+ const step3 = qMul(step2, locMul);
97
+ return clampQ(step3, 0, SCALE.Q);
98
+ }
99
+ // ── Day step ──────────────────────────────────────────────────────────────────
100
+ /**
101
+ * Advance technology through the polity pair graph for one simulated day.
102
+ *
103
+ * For each pair, checks both directions (A→B and B→A) and rolls against
104
+ * `computeDiffusionPressure`. A polity that advances during this step is
105
+ * not eligible to advance again in the same tick (one advance per tick max).
106
+ *
107
+ * Mutates `polity.techEra` (and refreshes `militaryStrength_Q`) for any
108
+ * polity that advances.
109
+ *
110
+ * Returns a `TechDiffusionResult[]` for every polity that advanced this tick.
111
+ */
112
+ export function stepTechDiffusion(registry, pairs, worldSeed, tick) {
113
+ const advanced = new Set(); // prevent double-advance per tick
114
+ const results = [];
115
+ for (const pair of pairs) {
116
+ const polityA = registry.polities.get(pair.polityAId);
117
+ const polityB = registry.polities.get(pair.polityBId);
118
+ if (!polityA || !polityB)
119
+ continue;
120
+ const atWar = areAtWar(registry, pair.polityAId, pair.polityBId);
121
+ // Check both directions
122
+ for (const [source, target] of [[polityA, polityB], [polityB, polityA]]) {
123
+ if (advanced.has(target.id))
124
+ continue;
125
+ if (target.techEra >= MAX_TECH_ERA)
126
+ continue;
127
+ const pressure = computeDiffusionPressure(source, target, pair, atWar);
128
+ if (pressure <= 0)
129
+ continue;
130
+ // Deterministic roll
131
+ const salt = hashString(source.id) ^ hashString(target.id);
132
+ const seed = eventSeed(worldSeed, tick, hashString(source.id), hashString(target.id), salt);
133
+ const rng = makeRng(seed, SCALE.Q);
134
+ const roll = rng.q01();
135
+ if (roll < pressure) {
136
+ const prev = target.techEra;
137
+ target.techEra = (target.techEra + 1);
138
+ deriveMilitaryStrength(target);
139
+ advanced.add(target.id);
140
+ results.push({ polityId: target.id, previousTechEra: prev, newTechEra: target.techEra });
141
+ }
142
+ }
143
+ }
144
+ return results;
145
+ }
146
+ // ── Query helpers ─────────────────────────────────────────────────────────────
147
+ /**
148
+ * Compute the net inbound diffusion pressure on a single polity from all its
149
+ * neighbours in the pair graph. Useful for AI queries ("how likely is this
150
+ * polity to advance soon?").
151
+ *
152
+ * War pairs are excluded. Pressure values are summed (uncapped).
153
+ */
154
+ export function totalInboundPressure(polityId, registry, pairs) {
155
+ const target = registry.polities.get(polityId);
156
+ if (!target)
157
+ return q(0);
158
+ let total = 0;
159
+ for (const pair of pairs) {
160
+ let sourceId = null;
161
+ if (pair.polityAId === polityId)
162
+ sourceId = pair.polityBId;
163
+ else if (pair.polityBId === polityId)
164
+ sourceId = pair.polityAId;
165
+ if (!sourceId)
166
+ continue;
167
+ const source = registry.polities.get(sourceId);
168
+ if (!source)
169
+ continue;
170
+ const atWar = areAtWar(registry, polityId, sourceId);
171
+ total += computeDiffusionPressure(source, target, pair, atWar);
172
+ }
173
+ return clampQ(total, 0, SCALE.Q);
174
+ }
175
+ /**
176
+ * Return the set of tech-era names available for a given era index.
177
+ * Useful for display in tools and reports.
178
+ */
179
+ export function techEraName(era) {
180
+ const names = [
181
+ "Prehistoric", "Ancient", "Medieval", "EarlyModern",
182
+ "Industrial", "Modern", "NearFuture", "FarFuture", "DeepSpace",
183
+ ];
184
+ return names[era] ?? `Era${era}`;
185
+ }
@@ -0,0 +1,25 @@
1
+ import { type ChannelMask } from "./channels.js";
2
+ import { Q } from "./units.js";
3
+ import type { IndividualAttributes } from "./types.js";
4
+ export type TraitId = "sealed" | "nonConductive" | "distributedControl" | "noSurfaceLayer" | "noBulkMedium" | "highThermalMass" | "fragileStructure" | "reinforcedStructure" | "chemicalImmune" | "radiationHardened" | "leader" | "standardBearer";
5
+ export type TraitMult = Partial<Record<TraitMultKey, Q>>;
6
+ export interface TraitEffect {
7
+ id: TraitId;
8
+ name: string;
9
+ description: string;
10
+ immuneTo?: ChannelMask;
11
+ resistantTo?: ChannelMask;
12
+ mult?: TraitMult;
13
+ }
14
+ export declare const TRAITS: Record<TraitId, TraitEffect>;
15
+ export type TraitMultKey = "shockTolerance" | "concussionTolerance" | "distressTolerance" | "surfaceIntegrity" | "bulkIntegrity" | "structureIntegrity" | "structureScale" | "heatTolerance" | "coldTolerance" | "controlQuality";
16
+ type AttrMutator = (a: IndividualAttributes) => void;
17
+ export declare const TRAIT_MUTATORS: Record<TraitId, AttrMutator>;
18
+ export interface TraitProfile {
19
+ traits: TraitId[];
20
+ immuneMask: ChannelMask;
21
+ resistantMask: ChannelMask;
22
+ }
23
+ export declare function buildTraitProfile(traits: readonly TraitId[]): TraitProfile;
24
+ export declare function applyTraitsToAttributes(base: IndividualAttributes, traits: readonly TraitId[]): IndividualAttributes;
25
+ export {};