@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,42 @@
1
+ /**
2
+ * Phase 15: entity factory functions for named real-world archetypes.
3
+ *
4
+ * Each factory returns a fully-initialised Entity ready for use in WorldState.
5
+ * No dependency on src/sim/testing.ts — safe to import from production code.
6
+ */
7
+ import type { Entity } from "./sim/entity.js";
8
+ /**
9
+ * Create an amateur or pro boxer at the given position.
10
+ *
11
+ * Loadout: boxing gloves.
12
+ * Skills: meleeCombat, meleeDefence, athleticism — scaled by level.
13
+ */
14
+ export declare function mkBoxer(id: number, teamId: number, x: number, y: number, level?: "amateur" | "pro"): Entity;
15
+ /**
16
+ * Create a Greco-Roman wrestler at the given position.
17
+ *
18
+ * Loadout: none (grapple only).
19
+ * Skills: grappling q(1.50), athleticism fatigueRateMul q(0.85).
20
+ */
21
+ export declare function mkWrestler(id: number, teamId: number, x: number, y: number): Entity;
22
+ /**
23
+ * Create a medieval knight at the given position.
24
+ *
25
+ * Loadout: longsword + plate armour (heaviest available, resist_J=800).
26
+ * Skills: meleeCombat q(1.25), meleeDefence q(1.25).
27
+ */
28
+ export declare function mkKnight(id: number, teamId: number, x: number, y: number): Entity;
29
+ /**
30
+ * Create a large Pacific octopus at the given position.
31
+ *
32
+ * Loadout: none (grapple only via arms).
33
+ * Body plan: OCTOPOID_PLAN (mantle + 8 arms).
34
+ * Skills: grappling q(1.60) — 8 arm-suckers provide extreme leverage bonus.
35
+ */
36
+ export declare function mkOctopus(id: number, teamId: number, x: number, y: number): Entity;
37
+ /**
38
+ * Create a baseline scuba diver (unarmed, no special skills) at the given position.
39
+ *
40
+ * Used as a reference opponent for octopus scenarios.
41
+ */
42
+ export declare function mkScubaDiver(id: number, teamId: number, x: number, y: number): Entity;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Phase 15: entity factory functions for named real-world archetypes.
3
+ *
4
+ * Each factory returns a fully-initialised Entity ready for use in WorldState.
5
+ * No dependency on src/sim/testing.ts — safe to import from production code.
6
+ */
7
+ import { generateIndividual } from "./generate.js";
8
+ import { AMATEUR_BOXER, PRO_BOXER, GRECO_WRESTLER, KNIGHT_INFANTRY, LARGE_PACIFIC_OCTOPUS, HUMAN_BASE, } from "./archetypes.js";
9
+ import { STARTER_WEAPONS, STARTER_ARMOUR } from "./equipment.js";
10
+ import { defaultIntent } from "./sim/intent.js";
11
+ import { defaultAction } from "./sim/action.js";
12
+ import { defaultCondition } from "./sim/condition.js";
13
+ import { defaultInjury } from "./sim/injury.js";
14
+ import { segmentIds, HUMANOID_PLAN, OCTOPOID_PLAN } from "./sim/bodyplan.js";
15
+ import { buildSkillMap } from "./sim/skills.js";
16
+ import { v3 } from "./sim/vec3.js";
17
+ import { q } from "./units.js";
18
+ // ── internal helpers ──────────────────────────────────────────────────────────
19
+ function findWeapon(id) {
20
+ const w = STARTER_WEAPONS.find(w => w.id === id);
21
+ if (!w)
22
+ throw new Error(`weapon ${id} not found in STARTER_WEAPONS`);
23
+ return w;
24
+ }
25
+ // ── factories ─────────────────────────────────────────────────────────────────
26
+ /**
27
+ * Create an amateur or pro boxer at the given position.
28
+ *
29
+ * Loadout: boxing gloves.
30
+ * Skills: meleeCombat, meleeDefence, athleticism — scaled by level.
31
+ */
32
+ export function mkBoxer(id, teamId, x, y, level = "amateur") {
33
+ const arch = level === "pro" ? PRO_BOXER : AMATEUR_BOXER;
34
+ const attrs = generateIndividual(id, arch);
35
+ const gloves = findWeapon("wpn_boxing_gloves");
36
+ return {
37
+ id,
38
+ teamId,
39
+ attributes: attrs,
40
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
41
+ loadout: { items: [gloves] },
42
+ traits: [],
43
+ bodyPlan: HUMANOID_PLAN,
44
+ skills: buildSkillMap({
45
+ meleeCombat: { energyTransferMul: level === "pro" ? q(1.15) : q(1.05) },
46
+ meleeDefence: { energyTransferMul: level === "pro" ? q(1.15) : q(1.05) },
47
+ athleticism: { fatigueRateMul: level === "pro" ? q(0.82) : q(0.90) },
48
+ }),
49
+ position_m: v3(x, y, 0),
50
+ velocity_mps: v3(0, 0, 0),
51
+ intent: defaultIntent(),
52
+ action: defaultAction(),
53
+ condition: defaultCondition(),
54
+ injury: defaultInjury(segmentIds(HUMANOID_PLAN)),
55
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
56
+ };
57
+ }
58
+ /**
59
+ * Create a Greco-Roman wrestler at the given position.
60
+ *
61
+ * Loadout: none (grapple only).
62
+ * Skills: grappling q(1.50), athleticism fatigueRateMul q(0.85).
63
+ */
64
+ export function mkWrestler(id, teamId, x, y) {
65
+ const attrs = generateIndividual(id, GRECO_WRESTLER);
66
+ return {
67
+ id,
68
+ teamId,
69
+ attributes: attrs,
70
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
71
+ loadout: { items: [] },
72
+ traits: [],
73
+ bodyPlan: HUMANOID_PLAN,
74
+ skills: buildSkillMap({
75
+ grappling: { energyTransferMul: q(1.50) },
76
+ athleticism: { fatigueRateMul: q(0.85) },
77
+ }),
78
+ position_m: v3(x, y, 0),
79
+ velocity_mps: v3(0, 0, 0),
80
+ intent: defaultIntent(),
81
+ action: defaultAction(),
82
+ condition: defaultCondition(),
83
+ injury: defaultInjury(segmentIds(HUMANOID_PLAN)),
84
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
85
+ };
86
+ }
87
+ /**
88
+ * Create a medieval knight at the given position.
89
+ *
90
+ * Loadout: longsword + plate armour (heaviest available, resist_J=800).
91
+ * Skills: meleeCombat q(1.25), meleeDefence q(1.25).
92
+ */
93
+ export function mkKnight(id, teamId, x, y) {
94
+ const attrs = generateIndividual(id, KNIGHT_INFANTRY);
95
+ const longsword = findWeapon("wpn_longsword");
96
+ const plateArmour = STARTER_ARMOUR[2]; // arm_plate — heaviest, resist_J = 800
97
+ return {
98
+ id,
99
+ teamId,
100
+ attributes: attrs,
101
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
102
+ loadout: { items: [longsword, plateArmour] },
103
+ traits: [],
104
+ bodyPlan: HUMANOID_PLAN,
105
+ skills: buildSkillMap({
106
+ meleeCombat: { energyTransferMul: q(1.25) },
107
+ meleeDefence: { energyTransferMul: q(1.25) },
108
+ }),
109
+ position_m: v3(x, y, 0),
110
+ velocity_mps: v3(0, 0, 0),
111
+ intent: defaultIntent(),
112
+ action: defaultAction(),
113
+ condition: defaultCondition(),
114
+ injury: defaultInjury(segmentIds(HUMANOID_PLAN)),
115
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
116
+ };
117
+ }
118
+ /**
119
+ * Create a large Pacific octopus at the given position.
120
+ *
121
+ * Loadout: none (grapple only via arms).
122
+ * Body plan: OCTOPOID_PLAN (mantle + 8 arms).
123
+ * Skills: grappling q(1.60) — 8 arm-suckers provide extreme leverage bonus.
124
+ */
125
+ export function mkOctopus(id, teamId, x, y) {
126
+ const attrs = generateIndividual(id, LARGE_PACIFIC_OCTOPUS);
127
+ return {
128
+ id,
129
+ teamId,
130
+ attributes: attrs,
131
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
132
+ loadout: { items: [] },
133
+ traits: [],
134
+ bodyPlan: OCTOPOID_PLAN,
135
+ skills: buildSkillMap({
136
+ grappling: { energyTransferMul: q(1.60) },
137
+ }),
138
+ position_m: v3(x, y, 0),
139
+ velocity_mps: v3(0, 0, 0),
140
+ intent: defaultIntent(),
141
+ action: defaultAction(),
142
+ condition: defaultCondition(),
143
+ injury: defaultInjury(segmentIds(OCTOPOID_PLAN)),
144
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
145
+ };
146
+ }
147
+ /**
148
+ * Create a baseline scuba diver (unarmed, no special skills) at the given position.
149
+ *
150
+ * Used as a reference opponent for octopus scenarios.
151
+ */
152
+ export function mkScubaDiver(id, teamId, x, y) {
153
+ const attrs = generateIndividual(id, HUMAN_BASE);
154
+ return {
155
+ id,
156
+ teamId,
157
+ attributes: attrs,
158
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
159
+ loadout: { items: [] },
160
+ traits: [],
161
+ bodyPlan: HUMANOID_PLAN,
162
+ position_m: v3(x, y, 0),
163
+ velocity_mps: v3(0, 0, 0),
164
+ intent: defaultIntent(),
165
+ action: defaultAction(),
166
+ condition: defaultCondition(),
167
+ injury: defaultInjury(segmentIds(HUMANOID_PLAN)),
168
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
169
+ };
170
+ }
@@ -0,0 +1,170 @@
1
+ import { type Q } from "./units.js";
2
+ import type { SkillId, SkillLevel, SkillMap } from "./sim/skills.js";
3
+ import type { RegionInjury } from "./sim/injury.js";
4
+ import type { BodyPlan } from "./sim/bodyplan.js";
5
+ import type { IndividualAttributes } from "./types.js";
6
+ /** First milestone threshold (novice → competent is earned quickly). */
7
+ export declare const BASE_XP = 20;
8
+ /**
9
+ * Each subsequent milestone requires GROWTH_FACTOR × more XP than the last.
10
+ * This produces a logarithmic mastery curve: early gains are fast, late gains
11
+ * require massive accumulated experience.
12
+ */
13
+ export declare const GROWTH_FACTOR = 1.8;
14
+ /**
15
+ * Partial adjustment to a SkillLevel applied when a milestone fires.
16
+ * All fields are additive increments to the existing skill level.
17
+ *
18
+ * hitTimingOffset_s: negative → faster action (reduce in SCALE.s units)
19
+ * energyTransferMul: positive → better efficiency (add to current Q value)
20
+ * dispersionMul: negative → tighter grouping (subtract from current Q value)
21
+ * treatmentRateMul: positive → faster healing
22
+ * fatigueRateMul: negative → less fatigue per tick
23
+ */
24
+ export type SkillDelta = Partial<SkillLevel>;
25
+ export interface XPLedger {
26
+ entries: Map<SkillId, number>;
27
+ }
28
+ export interface MilestoneRecord {
29
+ domain: SkillId;
30
+ /** 0-indexed milestone index (milestone 0 = first, requires BASE_XP). */
31
+ milestone: number;
32
+ tick: number;
33
+ delta: SkillDelta;
34
+ }
35
+ export interface ProgressionState {
36
+ xp: XPLedger;
37
+ milestones: MilestoneRecord[];
38
+ trainingLog: Array<{
39
+ tick: number;
40
+ attribute: string;
41
+ delta: number;
42
+ }>;
43
+ sequelae: Array<{
44
+ region: string;
45
+ type: string;
46
+ penalty: number;
47
+ }>;
48
+ }
49
+ export interface TrainingSession {
50
+ attribute: "peakForce_N" | "peakPower_W" | "reserveEnergy_J" | "continuousPower_W";
51
+ intensity_Q: Q;
52
+ duration_s: number;
53
+ }
54
+ export interface TrainingPlan {
55
+ sessions: TrainingSession[];
56
+ frequency_d: number;
57
+ /**
58
+ * Genetic/pharmacological ceiling for the trained attribute,
59
+ * in the same fixed-point units as the attribute.
60
+ * For peakForce_N: use to.N(3500) for a human elite athlete ceiling.
61
+ * Gain approaches zero as currentValue approaches this ceiling.
62
+ */
63
+ ceiling: number;
64
+ }
65
+ /**
66
+ * Flat attribute delta returned by stepAgeing.
67
+ * All values are in fixed-point units (same as IndividualAttributes fields).
68
+ * Negative values = decline; positive = improvement.
69
+ */
70
+ export interface AgeingDelta {
71
+ peakForce_N?: number;
72
+ peakPower_W?: number;
73
+ continuousPower_W?: number;
74
+ reserveEnergy_J?: number;
75
+ decisionLatency_s?: number;
76
+ }
77
+ /**
78
+ * Default SkillDelta applied at every milestone, keyed by domain.
79
+ *
80
+ * Calibration check — meleeCombat:
81
+ * 100 combats × 1 XP → crosses thresholds at 20, 36, 65 XP → 3 milestones.
82
+ * hitTimingOffset_s += 3 × (−270) = −810 SCALE.s = −0.081 s ≈ −80 ms. ✓
83
+ */
84
+ export declare const DEFAULT_MILESTONE_DELTA: Record<SkillId, SkillDelta>;
85
+ /**
86
+ * XP threshold for the nth milestone (0-indexed).
87
+ *
88
+ * threshold(0) = 20, threshold(1) = 36, threshold(2) = 65, threshold(3) = 117 …
89
+ */
90
+ export declare function milestoneThreshold(n: number): number;
91
+ /** Create a fresh, empty ProgressionState. */
92
+ export declare function createProgressionState(): ProgressionState;
93
+ /**
94
+ * Award XP in a skill domain and trigger any newly-reached milestones.
95
+ *
96
+ * Milestones are recorded in `state.milestones` and returned as an array
97
+ * of newly-triggered records (empty if none triggered this call).
98
+ *
99
+ * @param amount XP to add (may be fractional, e.g. 0.5 for a near-miss).
100
+ */
101
+ export declare function awardXP(state: ProgressionState, domain: SkillId, amount: number, tick: number): MilestoneRecord[];
102
+ /**
103
+ * Apply a SkillDelta additively to the skill entry for `domain` in `skills`.
104
+ * Returns a new SkillMap (the original is not mutated).
105
+ *
106
+ * Field bounds applied:
107
+ * hitTimingOffset_s — clamped to [−5000, 5000] SCALE.s
108
+ * energyTransferMul — clamped to [0, 30000]
109
+ * dispersionMul — clamped to [100, SCALE.Q] (at most q(1.0); never negative)
110
+ * treatmentRateMul — clamped to [100, 30000]
111
+ * fatigueRateMul — clamped to [100, SCALE.Q] (at most q(1.0))
112
+ */
113
+ export declare function advanceSkill(skills: SkillMap, domain: SkillId, delta: SkillDelta): SkillMap;
114
+ /**
115
+ * Apply one training session to a physical attribute.
116
+ *
117
+ * Formula:
118
+ * δ = BASE_GAIN_RATE × (intensity_Q / SCALE.Q) × (1 − currentValue / ceiling) × (1 − fatiguePenalty)
119
+ * fatiguePenalty = clamp((sessionsInLast7d − 5) × 0.08, 0, 0.50)
120
+ *
121
+ * @param currentValue Attribute value in fixed-point (e.g. `to.N(1840)` for peakForce_N).
122
+ * @param plan.ceiling Ceiling in the same fixed-point units.
123
+ * @param sessionsInLast7d How many sessions were logged in the last 7 days (incl. this one).
124
+ * @returns New attribute value (clamped to plan.ceiling, never below currentValue).
125
+ */
126
+ export declare function applyTrainingSession(currentValue: number, plan: TrainingPlan, session: TrainingSession, sessionsInLast7d: number): number;
127
+ /**
128
+ * Compute attribute delta for ONE year of ageing at `ageYears`.
129
+ *
130
+ * Rates (per year):
131
+ * peakForce_N, peakPower_W, continuousPower_W: −1% after age 35
132
+ * reserveEnergy_J: −0.5% after age 35
133
+ * decisionLatency_s: +20 SCALE.s (+2 ms) after age 45
134
+ *
135
+ * Caller is responsible for mutating attrs via `applyAgeingDelta`.
136
+ * The returned delta is suitable for integer accumulation (per-year granularity).
137
+ *
138
+ * @param ageYears Age in years at the START of this year.
139
+ */
140
+ export declare function stepAgeing(attrs: IndividualAttributes, ageYears: number): AgeingDelta;
141
+ /**
142
+ * Merge an AgeingDelta into an IndividualAttributes in-place.
143
+ * All fields are clamped to non-negative values (cannot decline to sub-zero).
144
+ */
145
+ export declare function applyAgeingDelta(attrs: IndividualAttributes, delta: AgeingDelta): void;
146
+ /**
147
+ * Derive permanent injury sequelae from a region's current injury state.
148
+ *
149
+ * Sequelae are one-time permanent modifiers to IndividualAttributes recorded
150
+ * in ProgressionState. They should be derived at the point of injury resolution
151
+ * (e.g. when structural damage crosses FRACTURE_THRESHOLD) or at end-of-combat.
152
+ *
153
+ * Rules:
154
+ * fracture_malunion — fractured region with permanentDamage ≥ q(0.20) →
155
+ * −15% peak force in the affected limb.
156
+ * nerve_damage — internalDamage ≥ q(0.70) → −10% fine control.
157
+ * scar_tissue — permanentDamage > 0 AND surfaceDamage > 0 →
158
+ * lower surface bleed threshold (penalty 0.05).
159
+ *
160
+ * @param _bodyPlan Reserved for locomotion role lookups in future phases.
161
+ * @returns Array of sequela descriptors (no mutation; caller records them).
162
+ */
163
+ export declare function deriveSequelae(regionInjury: RegionInjury, _bodyPlan: BodyPlan): Array<{
164
+ type: string;
165
+ penalty: number;
166
+ }>;
167
+ /** Serialise a ProgressionState to a JSON string (Map-aware). */
168
+ export declare function serialiseProgression(state: ProgressionState): string;
169
+ /** Deserialise a ProgressionState from a JSON string produced by serialiseProgression. */
170
+ export declare function deserialiseProgression(json: string): ProgressionState;
@@ -0,0 +1,256 @@
1
+ // src/progression.ts — Phase 21: Character Progression
2
+ //
3
+ // Pure bookkeeping module: no kernel imports, no RNG needed.
4
+ // All functions are deterministic given the same inputs.
5
+ // Host persists ProgressionState alongside Entity between sessions.
6
+ //
7
+ // Three subsystems:
8
+ // 1. XP / milestones — skill domains accumulate XP from contested use;
9
+ // geometric thresholds trigger discrete skill deltas.
10
+ // 2. Physical training — attribute drift bounded by genetic ceiling,
11
+ // modulated by intensity and overtraining penalty.
12
+ // 3. Ageing — slow per-year decline after peak age (35 for
13
+ // performance, 45 for cognition), reversible only
14
+ // by pharmacological or magical intervention.
15
+ //
16
+ // Injury sequelae (permanent damage from wounds) are also derived here,
17
+ // as they feed back into IndividualAttributes like ageing does.
18
+ import { q, SCALE } from "./units.js";
19
+ import { defaultSkillLevel } from "./sim/skills.js";
20
+ // ── Milestone constants ───────────────────────────────────────────────────────
21
+ /** First milestone threshold (novice → competent is earned quickly). */
22
+ export const BASE_XP = 20;
23
+ /**
24
+ * Each subsequent milestone requires GROWTH_FACTOR × more XP than the last.
25
+ * This produces a logarithmic mastery curve: early gains are fast, late gains
26
+ * require massive accumulated experience.
27
+ */
28
+ export const GROWTH_FACTOR = 1.80;
29
+ // ── Training constant ─────────────────────────────────────────────────────────
30
+ /**
31
+ * Base attribute gain per training session in fixed-point attribute units,
32
+ * evaluated at max intensity (q(1.0)) starting from zero proximity to ceiling.
33
+ *
34
+ * Calibration:
35
+ * At moderate intensity (q(0.50)) with 50% proximity to ceiling,
36
+ * δ = 2400 × 0.5 × 0.5 = 600 fp = 6 N for peakForce_N.
37
+ * Over 36 moderate sessions (12-week programme, 3×/week):
38
+ * ≈ 150–300 N total gain (ceiling effects reduce later sessions). ✓
39
+ */
40
+ const BASE_GAIN_RATE = 2400;
41
+ // ── Default milestone deltas ──────────────────────────────────────────────────
42
+ /**
43
+ * Default SkillDelta applied at every milestone, keyed by domain.
44
+ *
45
+ * Calibration check — meleeCombat:
46
+ * 100 combats × 1 XP → crosses thresholds at 20, 36, 65 XP → 3 milestones.
47
+ * hitTimingOffset_s += 3 × (−270) = −810 SCALE.s = −0.081 s ≈ −80 ms. ✓
48
+ */
49
+ export const DEFAULT_MILESTONE_DELTA = {
50
+ meleeCombat: { hitTimingOffset_s: -270 }, // −27 ms per milestone
51
+ meleeDefence: { energyTransferMul: 400 }, // +4% parry quality
52
+ grappling: { energyTransferMul: 300 }, // +3% leverage
53
+ rangedCombat: { dispersionMul: -300 }, // −3% spread
54
+ throwingWeapons: { energyTransferMul: 300 }, // +3% throw energy
55
+ shieldCraft: { energyTransferMul: 400 }, // +4% block quality
56
+ medical: { treatmentRateMul: 400 }, // +4% treatment speed
57
+ athleticism: { fatigueRateMul: -300 }, // −3% fatigue per tick
58
+ tactics: { hitTimingOffset_s: -200 }, // −20 ms decision latency
59
+ stealth: { dispersionMul: -300 }, // −3% acoustic signature
60
+ };
61
+ // ── Milestone arithmetic ──────────────────────────────────────────────────────
62
+ /**
63
+ * XP threshold for the nth milestone (0-indexed).
64
+ *
65
+ * threshold(0) = 20, threshold(1) = 36, threshold(2) = 65, threshold(3) = 117 …
66
+ */
67
+ export function milestoneThreshold(n) {
68
+ return Math.round(BASE_XP * Math.pow(GROWTH_FACTOR, n));
69
+ }
70
+ // ── Core progression functions ────────────────────────────────────────────────
71
+ /** Create a fresh, empty ProgressionState. */
72
+ export function createProgressionState() {
73
+ return {
74
+ xp: { entries: new Map() },
75
+ milestones: [],
76
+ trainingLog: [],
77
+ sequelae: [],
78
+ };
79
+ }
80
+ /**
81
+ * Award XP in a skill domain and trigger any newly-reached milestones.
82
+ *
83
+ * Milestones are recorded in `state.milestones` and returned as an array
84
+ * of newly-triggered records (empty if none triggered this call).
85
+ *
86
+ * @param amount XP to add (may be fractional, e.g. 0.5 for a near-miss).
87
+ */
88
+ export function awardXP(state, domain, amount, tick) {
89
+ const prev = state.xp.entries.get(domain) ?? 0;
90
+ const next = prev + amount;
91
+ state.xp.entries.set(domain, next);
92
+ // Count milestones already achieved before this award
93
+ let already = 0;
94
+ while (milestoneThreshold(already) <= prev)
95
+ already++;
96
+ // Count milestones now achieved
97
+ const triggered = [];
98
+ let idx = already;
99
+ while (milestoneThreshold(idx) <= next) {
100
+ const record = {
101
+ domain,
102
+ milestone: idx,
103
+ tick,
104
+ delta: DEFAULT_MILESTONE_DELTA[domain],
105
+ };
106
+ triggered.push(record);
107
+ state.milestones.push(record);
108
+ idx++;
109
+ }
110
+ return triggered;
111
+ }
112
+ /**
113
+ * Apply a SkillDelta additively to the skill entry for `domain` in `skills`.
114
+ * Returns a new SkillMap (the original is not mutated).
115
+ *
116
+ * Field bounds applied:
117
+ * hitTimingOffset_s — clamped to [−5000, 5000] SCALE.s
118
+ * energyTransferMul — clamped to [0, 30000]
119
+ * dispersionMul — clamped to [100, SCALE.Q] (at most q(1.0); never negative)
120
+ * treatmentRateMul — clamped to [100, 30000]
121
+ * fatigueRateMul — clamped to [100, SCALE.Q] (at most q(1.0))
122
+ */
123
+ export function advanceSkill(skills, domain, delta) {
124
+ const cur = skills.get(domain) ?? defaultSkillLevel();
125
+ const updated = {
126
+ hitTimingOffset_s: Math.max(-5000, Math.min(5000, cur.hitTimingOffset_s + (delta.hitTimingOffset_s ?? 0))),
127
+ energyTransferMul: Math.max(0, Math.min(30000, cur.energyTransferMul + (delta.energyTransferMul ?? 0))),
128
+ dispersionMul: Math.max(100, Math.min(SCALE.Q, cur.dispersionMul + (delta.dispersionMul ?? 0))),
129
+ treatmentRateMul: Math.max(100, Math.min(30000, cur.treatmentRateMul + (delta.treatmentRateMul ?? 0))),
130
+ fatigueRateMul: Math.max(100, Math.min(SCALE.Q, cur.fatigueRateMul + (delta.fatigueRateMul ?? 0))),
131
+ };
132
+ const out = new Map(skills);
133
+ out.set(domain, updated);
134
+ return out;
135
+ }
136
+ /**
137
+ * Apply one training session to a physical attribute.
138
+ *
139
+ * Formula:
140
+ * δ = BASE_GAIN_RATE × (intensity_Q / SCALE.Q) × (1 − currentValue / ceiling) × (1 − fatiguePenalty)
141
+ * fatiguePenalty = clamp((sessionsInLast7d − 5) × 0.08, 0, 0.50)
142
+ *
143
+ * @param currentValue Attribute value in fixed-point (e.g. `to.N(1840)` for peakForce_N).
144
+ * @param plan.ceiling Ceiling in the same fixed-point units.
145
+ * @param sessionsInLast7d How many sessions were logged in the last 7 days (incl. this one).
146
+ * @returns New attribute value (clamped to plan.ceiling, never below currentValue).
147
+ */
148
+ export function applyTrainingSession(currentValue, plan, session, sessionsInLast7d) {
149
+ if (plan.ceiling <= 0 || currentValue >= plan.ceiling)
150
+ return currentValue;
151
+ const fatiguePenalty = Math.min(0.50, Math.max(0, (sessionsInLast7d - 5) * 0.08));
152
+ const proximity = (plan.ceiling - currentValue) / plan.ceiling; // 0 = at ceiling, 1 = far below
153
+ const intensityFrac = session.intensity_Q / SCALE.Q;
154
+ const delta = Math.trunc(BASE_GAIN_RATE * intensityFrac * proximity * (1 - fatiguePenalty));
155
+ return Math.min(plan.ceiling, currentValue + Math.max(0, delta));
156
+ }
157
+ // ── Ageing ────────────────────────────────────────────────────────────────────
158
+ const DECLINE_START_AGE = 35;
159
+ const COGNITIVE_DECLINE_START = 45;
160
+ /**
161
+ * Compute attribute delta for ONE year of ageing at `ageYears`.
162
+ *
163
+ * Rates (per year):
164
+ * peakForce_N, peakPower_W, continuousPower_W: −1% after age 35
165
+ * reserveEnergy_J: −0.5% after age 35
166
+ * decisionLatency_s: +20 SCALE.s (+2 ms) after age 45
167
+ *
168
+ * Caller is responsible for mutating attrs via `applyAgeingDelta`.
169
+ * The returned delta is suitable for integer accumulation (per-year granularity).
170
+ *
171
+ * @param ageYears Age in years at the START of this year.
172
+ */
173
+ export function stepAgeing(attrs, ageYears) {
174
+ const delta = {};
175
+ if (ageYears >= DECLINE_START_AGE) {
176
+ // 1% performance decline per year
177
+ delta.peakForce_N = -Math.round(attrs.performance.peakForce_N * 0.01);
178
+ delta.peakPower_W = -Math.round(attrs.performance.peakPower_W * 0.01);
179
+ delta.continuousPower_W = -Math.round(attrs.performance.continuousPower_W * 0.01);
180
+ // 0.5% reserve decline per year (energy reserves decline more slowly)
181
+ delta.reserveEnergy_J = -Math.round(attrs.performance.reserveEnergy_J * 0.005);
182
+ }
183
+ if (ageYears >= COGNITIVE_DECLINE_START) {
184
+ // +2 ms per year = +20 SCALE.s per year
185
+ delta.decisionLatency_s = Math.round(2 * SCALE.s / 1000);
186
+ }
187
+ return delta;
188
+ }
189
+ /**
190
+ * Merge an AgeingDelta into an IndividualAttributes in-place.
191
+ * All fields are clamped to non-negative values (cannot decline to sub-zero).
192
+ */
193
+ export function applyAgeingDelta(attrs, delta) {
194
+ if (delta.peakForce_N !== undefined)
195
+ attrs.performance.peakForce_N = Math.max(0, attrs.performance.peakForce_N + delta.peakForce_N);
196
+ if (delta.peakPower_W !== undefined)
197
+ attrs.performance.peakPower_W = Math.max(0, attrs.performance.peakPower_W + delta.peakPower_W);
198
+ if (delta.continuousPower_W !== undefined)
199
+ attrs.performance.continuousPower_W = Math.max(0, attrs.performance.continuousPower_W + delta.continuousPower_W);
200
+ if (delta.reserveEnergy_J !== undefined)
201
+ attrs.performance.reserveEnergy_J = Math.max(0, attrs.performance.reserveEnergy_J + delta.reserveEnergy_J);
202
+ if (delta.decisionLatency_s !== undefined)
203
+ if (attrs.perception)
204
+ attrs.perception.decisionLatency_s = (attrs.perception.decisionLatency_s + delta.decisionLatency_s);
205
+ }
206
+ // ── Injury sequelae ───────────────────────────────────────────────────────────
207
+ /**
208
+ * Derive permanent injury sequelae from a region's current injury state.
209
+ *
210
+ * Sequelae are one-time permanent modifiers to IndividualAttributes recorded
211
+ * in ProgressionState. They should be derived at the point of injury resolution
212
+ * (e.g. when structural damage crosses FRACTURE_THRESHOLD) or at end-of-combat.
213
+ *
214
+ * Rules:
215
+ * fracture_malunion — fractured region with permanentDamage ≥ q(0.20) →
216
+ * −15% peak force in the affected limb.
217
+ * nerve_damage — internalDamage ≥ q(0.70) → −10% fine control.
218
+ * scar_tissue — permanentDamage > 0 AND surfaceDamage > 0 →
219
+ * lower surface bleed threshold (penalty 0.05).
220
+ *
221
+ * @param _bodyPlan Reserved for locomotion role lookups in future phases.
222
+ * @returns Array of sequela descriptors (no mutation; caller records them).
223
+ */
224
+ export function deriveSequelae(regionInjury, _bodyPlan) {
225
+ const result = [];
226
+ if (regionInjury.fractured && regionInjury.permanentDamage >= q(0.20)) {
227
+ result.push({ type: "fracture_malunion", penalty: 0.15 });
228
+ }
229
+ if (regionInjury.internalDamage >= q(0.70)) {
230
+ result.push({ type: "nerve_damage", penalty: 0.10 });
231
+ }
232
+ if (regionInjury.permanentDamage > 0 && regionInjury.surfaceDamage > 0) {
233
+ result.push({ type: "scar_tissue", penalty: 0.05 });
234
+ }
235
+ return result;
236
+ }
237
+ // ── Serialisation ─────────────────────────────────────────────────────────────
238
+ /** Serialise a ProgressionState to a JSON string (Map-aware). */
239
+ export function serialiseProgression(state) {
240
+ return JSON.stringify({
241
+ xp: { entries: Array.from(state.xp.entries.entries()) },
242
+ milestones: state.milestones,
243
+ trainingLog: state.trainingLog,
244
+ sequelae: state.sequelae,
245
+ });
246
+ }
247
+ /** Deserialise a ProgressionState from a JSON string produced by serialiseProgression. */
248
+ export function deserialiseProgression(json) {
249
+ const raw = JSON.parse(json);
250
+ return {
251
+ xp: { entries: new Map(raw.xp.entries) },
252
+ milestones: raw.milestones,
253
+ trainingLog: raw.trainingLog,
254
+ sequelae: raw.sequelae,
255
+ };
256
+ }