@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,243 @@
1
+ // src/sim/aging.ts — Phase 57: Aging & Lifespan
2
+ //
3
+ // Attribute curves parameterized by normalized age fraction (ageFrac = ageYears / lifespanYears).
4
+ // Species-agnostic: a human at 25 years and an elf at 187 years both have ageFrac ≈ 0.31 and
5
+ // receive the same multipliers — the underlying biology follows the same developmental arc.
6
+ //
7
+ // Seven multiplier dimensions, each modelled as a piecewise-linear Q curve:
8
+ // muscularStrength — peakForce, peakPower, continuousPower (peaks ~0.28 ageFrac)
9
+ // reactionTime — multiplier on reactionTime_s (>q(1.0) = slower; peaks ~0.28)
10
+ // motorControl — controlQuality, stability, fineControl (peaks ~0.28)
11
+ // stature — stature_m (stable adult, slight compression in ancient)
12
+ // cognitionFluid — logical, spatial, kinesthetic, musical (peaks ~0.28)
13
+ // cognitionCrystal — linguistic, interpersonal, intrapersonal (peaks ~0.58)
14
+ // distressTolerance — pain/fear tolerance; wisdom accumulates to middle age
15
+ //
16
+ // Public API:
17
+ // computeAgeFrac(ageYears, lifespanYears?) → Q [0..SCALE.Q]
18
+ // getAgePhase(ageYears, lifespanYears?) → AgePhase
19
+ // deriveAgeMultipliers(ageYears, lifespanYears?) → AgeMultipliers
20
+ // applyAgingToAttributes(base, ageYears, ...) → IndividualAttributes (new object)
21
+ // stepAging(entity, elapsedSeconds) → mutates entity.age
22
+ import { q, clampQ, SCALE } from "../units.js";
23
+ // ── Constants ─────────────────────────────────────────────────────────────────
24
+ /** Seconds in one year (non-leap). */
25
+ export const SECONDS_PER_YEAR = 365 * 86_400; // 31 536 000
26
+ /** Default lifespan for entities without a species override [years]. */
27
+ export const HUMAN_LIFESPAN_YEARS = 80;
28
+ const MUSCULAR_STRENGTH_KNOTS = [
29
+ [q(0.00), q(0.05)], // birth
30
+ [q(0.15), q(0.55)], // child
31
+ [q(0.25), q(1.00)], // peak young adult
32
+ [q(0.45), q(0.95)], // maintained adult
33
+ [q(0.65), q(0.75)], // elder decline
34
+ [q(1.00), q(0.40)], // ancient
35
+ ];
36
+ // reactionTime_Q > SCALE.Q means reaction is SLOWER than the archetype baseline.
37
+ const REACTION_TIME_KNOTS = [
38
+ [q(0.00), 25_000], // infant (2.5× baseline — newborn has negligible voluntary motor)
39
+ [q(0.15), 12_000], // adolescent (1.2×)
40
+ [q(0.28), 10_000], // peak (1.0× = no change from archetype)
41
+ [q(0.50), 10_800], // slight adult slowdown (1.08×)
42
+ [q(0.70), 12_500], // elder (1.25×)
43
+ [q(1.00), 20_000], // ancient (2.0×)
44
+ ];
45
+ const MOTOR_CONTROL_KNOTS = [
46
+ [q(0.00), q(0.30)], // infant
47
+ [q(0.15), q(0.80)], // adolescent
48
+ [q(0.28), q(1.00)], // peak
49
+ [q(0.60), q(0.95)], // adult maintained
50
+ [q(0.85), q(0.75)], // elder
51
+ [q(1.00), q(0.55)], // ancient
52
+ ];
53
+ const STATURE_KNOTS = [
54
+ [q(0.00), q(0.30)], // infant
55
+ [q(0.20), q(0.95)], // adolescent growth
56
+ [q(0.25), q(1.00)], // peak adult height
57
+ [q(0.70), q(1.00)], // stable through adulthood
58
+ [q(0.90), q(0.97)], // slight compression in elder
59
+ [q(1.00), q(0.94)], // ancient
60
+ ];
61
+ const COGNITION_FLUID_KNOTS = [
62
+ [q(0.00), q(0.10)], // infant
63
+ [q(0.20), q(0.90)], // adolescent rapid development
64
+ [q(0.28), q(1.00)], // peak young adult (~22 years for human)
65
+ [q(0.50), q(0.90)], // adult gradual decline
66
+ [q(0.70), q(0.70)], // elder
67
+ [q(1.00), q(0.35)], // ancient
68
+ ];
69
+ const COGNITION_CRYSTAL_KNOTS = [
70
+ [q(0.00), q(0.10)], // infant
71
+ [q(0.25), q(0.75)], // young adult still accumulating wisdom
72
+ [q(0.55), q(1.00)], // peak middle age (~44 years for human)
73
+ [q(0.80), q(0.98)], // elder — wisdom mostly preserved
74
+ [q(1.00), q(0.78)], // ancient
75
+ ];
76
+ const DISTRESS_TOLERANCE_KNOTS = [
77
+ [q(0.00), q(0.50)], // infant
78
+ [q(0.20), q(0.80)], // adolescent
79
+ [q(0.45), q(1.00)], // peaks middle age (hard-won tolerance)
80
+ [q(0.75), q(1.05)], // elder slightly above baseline (wisdom bonus)
81
+ [q(1.00), q(0.85)], // ancient — some decline
82
+ ];
83
+ // ── Core computation ──────────────────────────────────────────────────────────
84
+ /** Piecewise-linear interpolation between sorted knot pairs. */
85
+ function interpKnots(x_Q, knots) {
86
+ if (x_Q <= knots[0][0])
87
+ return knots[0][1];
88
+ for (let i = 1; i < knots.length; i++) {
89
+ const [x0, y0] = knots[i - 1];
90
+ const [x1, y1] = knots[i];
91
+ if (x_Q <= x1) {
92
+ const span = x1 - x0;
93
+ if (span === 0)
94
+ return y0;
95
+ const t = Math.round((x_Q - x0) * SCALE.Q / span);
96
+ return (y0 + Math.round((y1 - y0) * t / SCALE.Q));
97
+ }
98
+ }
99
+ return knots[knots.length - 1][1];
100
+ }
101
+ /**
102
+ * Compute normalized age fraction [0..SCALE.Q] for a given age and lifespan.
103
+ *
104
+ * A 25-year-old human (lifespan 80) → q(0.3125).
105
+ * A 187-year-old elf (lifespan 600) → q(0.312) — effectively the same developmental stage.
106
+ *
107
+ * @param ageYears Current age in years.
108
+ * @param lifespanYears Expected lifespan (default: HUMAN_LIFESPAN_YEARS).
109
+ */
110
+ export function computeAgeFrac(ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
111
+ if (lifespanYears <= 0)
112
+ return q(0);
113
+ return clampQ(Math.round(ageYears * SCALE.Q / lifespanYears), q(0), SCALE.Q);
114
+ }
115
+ /**
116
+ * Classify the entity's life stage from their normalized age fraction.
117
+ *
118
+ * Boundaries (ageFrac):
119
+ * infant 0–0.05 | child 0.05–0.15 | adolescent 0.15–0.22 |
120
+ * young_adult 0.22–0.38 | adult 0.38–0.62 | elder 0.62–0.88 | ancient 0.88+
121
+ */
122
+ export function getAgePhase(ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
123
+ const f = computeAgeFrac(ageYears, lifespanYears);
124
+ if (f < q(0.05))
125
+ return "infant";
126
+ if (f < q(0.15))
127
+ return "child";
128
+ if (f < q(0.22))
129
+ return "adolescent";
130
+ if (f < q(0.38))
131
+ return "young_adult";
132
+ if (f < q(0.62))
133
+ return "adult";
134
+ if (f < q(0.88))
135
+ return "elder";
136
+ return "ancient";
137
+ }
138
+ /**
139
+ * Derive age-based attribute multipliers from normalized age and lifespan.
140
+ *
141
+ * All returned Q values except `reactionTime_Q` are in [0, SCALE.Q].
142
+ * `reactionTime_Q` may exceed SCALE.Q (values > q(1.0) indicate slower reaction
143
+ * than the archetype baseline).
144
+ */
145
+ export function deriveAgeMultipliers(ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
146
+ const f = computeAgeFrac(ageYears, lifespanYears);
147
+ return {
148
+ muscularStrength_Q: interpKnots(f, MUSCULAR_STRENGTH_KNOTS),
149
+ reactionTime_Q: interpKnots(f, REACTION_TIME_KNOTS),
150
+ motorControl_Q: interpKnots(f, MOTOR_CONTROL_KNOTS),
151
+ stature_Q: interpKnots(f, STATURE_KNOTS),
152
+ cognitionFluid_Q: interpKnots(f, COGNITION_FLUID_KNOTS),
153
+ cognitionCrystal_Q: interpKnots(f, COGNITION_CRYSTAL_KNOTS),
154
+ distressTolerance_Q: interpKnots(f, DISTRESS_TOLERANCE_KNOTS),
155
+ };
156
+ }
157
+ /**
158
+ * Apply age multipliers to a base attribute set, returning a new object.
159
+ *
160
+ * The input `base` is treated as the archetype peak (typically from `generateIndividual`).
161
+ * The caller is responsible for caching the base and recomputing aged attributes when
162
+ * age advances (e.g. once per in-game month for campaign simulation).
163
+ *
164
+ * Attributes affected:
165
+ * - morphology.stature_m
166
+ * - performance.peakForce_N, peakPower_W, continuousPower_W
167
+ * - control.reactionTime_s, controlQuality, stability, fineControl
168
+ * - resilience.distressTolerance
169
+ * - cognition (if present): fluid dims + crystal dims scaled independently
170
+ *
171
+ * All Q outputs are clamped to [0, SCALE.Q]; reactionTime_s is clamped to ≥ 1.
172
+ *
173
+ * @param base Archetype-peak attributes (unmodified).
174
+ * @param ageYears Current age in years.
175
+ * @param lifespanYears Expected lifespan (default: HUMAN_LIFESPAN_YEARS).
176
+ */
177
+ export function applyAgingToAttributes(base, ageYears, lifespanYears = HUMAN_LIFESPAN_YEARS) {
178
+ const m = deriveAgeMultipliers(ageYears, lifespanYears);
179
+ return {
180
+ ...base,
181
+ morphology: {
182
+ ...base.morphology,
183
+ stature_m: Math.max(1, Math.round(base.morphology.stature_m * m.stature_Q / SCALE.Q)),
184
+ },
185
+ performance: {
186
+ ...base.performance,
187
+ peakForce_N: Math.max(1, Math.round(base.performance.peakForce_N * m.muscularStrength_Q / SCALE.Q)),
188
+ peakPower_W: Math.max(1, Math.round(base.performance.peakPower_W * m.muscularStrength_Q / SCALE.Q)),
189
+ continuousPower_W: Math.max(1, Math.round(base.performance.continuousPower_W * m.muscularStrength_Q / SCALE.Q)),
190
+ },
191
+ control: {
192
+ ...base.control,
193
+ reactionTime_s: Math.max(1, Math.round(base.control.reactionTime_s * m.reactionTime_Q / SCALE.Q)),
194
+ controlQuality: clampQ(Math.round(base.control.controlQuality * m.motorControl_Q / SCALE.Q), q(0), SCALE.Q),
195
+ stability: clampQ(Math.round(base.control.stability * m.motorControl_Q / SCALE.Q), q(0), SCALE.Q),
196
+ fineControl: clampQ(Math.round(base.control.fineControl * m.motorControl_Q / SCALE.Q), q(0), SCALE.Q),
197
+ },
198
+ resilience: {
199
+ ...base.resilience,
200
+ distressTolerance: clampQ(Math.round(base.resilience.distressTolerance * m.distressTolerance_Q / SCALE.Q), q(0), SCALE.Q),
201
+ },
202
+ // exactOptionalPropertyTypes: spread present cognition, otherwise omit the key entirely.
203
+ ...(base.cognition
204
+ ? {
205
+ cognition: {
206
+ ...base.cognition,
207
+ // Fluid intelligence (peaks young, declines earlier)
208
+ logicalMathematical: clampQ(Math.round(base.cognition.logicalMathematical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
209
+ spatial: clampQ(Math.round(base.cognition.spatial * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
210
+ bodilyKinesthetic: clampQ(Math.round(base.cognition.bodilyKinesthetic * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
211
+ musical: clampQ(Math.round(base.cognition.musical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
212
+ // Crystallized intelligence (peaks mid-life, persists through elder)
213
+ linguistic: clampQ(Math.round(base.cognition.linguistic * m.cognitionCrystal_Q / SCALE.Q), q(0), SCALE.Q),
214
+ interpersonal: clampQ(Math.round(base.cognition.interpersonal * m.cognitionCrystal_Q / SCALE.Q), q(0), SCALE.Q),
215
+ intrapersonal: clampQ(Math.round(base.cognition.intrapersonal * m.cognitionCrystal_Q / SCALE.Q), q(0), SCALE.Q),
216
+ },
217
+ }
218
+ : {}),
219
+ };
220
+ }
221
+ /**
222
+ * Advance an entity's age by `elapsedSeconds`.
223
+ *
224
+ * Initializes `entity.age` if absent. Does NOT recompute attributes — the host
225
+ * should call `applyAgingToAttributes` when it needs current aged stats.
226
+ *
227
+ * Mutates: `entity.age`.
228
+ */
229
+ export function stepAging(entity, elapsedSeconds) {
230
+ if (!entity.age) {
231
+ entity.age = { ageSeconds: 0 };
232
+ }
233
+ entity.age.ageSeconds += elapsedSeconds;
234
+ }
235
+ /**
236
+ * Convenience helper: return the current age in fractional years from entity.age.
237
+ * Returns 0 if `entity.age` is absent.
238
+ */
239
+ export function entityAgeYears(entity) {
240
+ if (!entity.age)
241
+ return 0;
242
+ return entity.age.ageSeconds / SECONDS_PER_YEAR;
243
+ }
@@ -0,0 +1,10 @@
1
+ import type { Entity } from "../entity.js";
2
+ import type { WorldState } from "../world.js";
3
+ import type { WorldIndex } from "../indexing.js";
4
+ import type { SpatialIndex } from "../spatial.js";
5
+ import type { Command } from "../commands.js";
6
+ import { type I32 } from "../../units.js";
7
+ import type { AIPolicy } from "./types.js";
8
+ import { type SensoryEnvironment } from "../sensory.js";
9
+ import { type ObstacleGrid } from "../terrain.js";
10
+ export declare function decideCommandsForEntity(world: WorldState, index: WorldIndex, spatial: SpatialIndex, self: Entity, policy: AIPolicy, env?: SensoryEnvironment, obstacleGrid?: ObstacleGrid, cellSize_m?: I32): readonly Command[];
@@ -0,0 +1,267 @@
1
+ import { q, clampQ, qMul, mulDiv, SCALE } from "../../units.js";
2
+ import { pickTarget, updateFocus } from "./targeting.js";
3
+ import { findWeapon } from "../../equipment.js";
4
+ import { v3, normaliseDirCheapQ } from "../vec3.js";
5
+ import { DEFAULT_PERCEPTION, DEFAULT_SENSORY_ENV } from "../sensory.js";
6
+ import { isRouting, moraleThreshold } from "../morale.js";
7
+ import { eventSeed } from "../seeds.js";
8
+ import { coverFractionAtPosition, terrainKey } from "../terrain.js";
9
+ import { getSkill } from "../skills.js";
10
+ import { TICK_HZ } from "../tick.js";
11
+ import { effectiveStanding, STANDING_FRIENDLY_THRESHOLD } from "../../faction.js";
12
+ import { computeEffectiveRetreatRange, computeDefenceIntensityBoost, applyLoyaltyBias, applyOpportunismBias, computeEffectiveLoyalty, } from "./personality.js";
13
+ export function decideCommandsForEntity(world, index, spatial, self, policy, env = DEFAULT_SENSORY_ENV, obstacleGrid, cellSize_m) {
14
+ if (self.injury.dead)
15
+ return [];
16
+ // Feature 4: surrendered entities are permanently passive
17
+ if ((self.condition).surrendered) {
18
+ return [
19
+ { kind: "defend", mode: "none", intensity: q(0) },
20
+ { kind: "setProne", prone: true },
21
+ ];
22
+ }
23
+ // tick down AI cooldowns
24
+ if (!self.ai)
25
+ self.ai = { focusTargetId: 0, retargetCooldownTicks: 0, decisionCooldownTicks: 0 };
26
+ if ((self.ai).decisionCooldownTicks === undefined)
27
+ (self.ai).decisionCooldownTicks = 0;
28
+ self.ai.retargetCooldownTicks = Math.max(0, self.ai.retargetCooldownTicks - 1);
29
+ self.ai.decisionCooldownTicks = Math.max(0, self.ai.decisionCooldownTicks - 1);
30
+ // Phase 4: decision latency — while cooling down, skip replanning and repeat current intent.
31
+ if (self.ai.decisionCooldownTicks > 0) {
32
+ // Emit the same defend and move as last tick (intent is already set from previous tick).
33
+ return [];
34
+ }
35
+ // Charge latency for next decision cycle
36
+ const perc = (self.attributes).perception ?? DEFAULT_PERCEPTION;
37
+ // Phase 7: tactics.hitTimingOffset_s reduces decision latency (max 50% reduction)
38
+ const tacticsSkill = getSkill(self.skills, "tactics");
39
+ const adjustedLatency_s = Math.max(Math.trunc(perc.decisionLatency_s / 2), perc.decisionLatency_s + tacticsSkill.hitTimingOffset_s);
40
+ // Phase 33: logicalMathematical reduces decision latency (faster tactical processing)
41
+ // Formula: mul = q(1.20) − logMath × q(0.40); human (0.60) → ×0.96; Vulcan (0.95) → ×0.82
42
+ const logMath = self.attributes.cognition?.logicalMathematical ?? 0;
43
+ const logLatencyMul = logMath
44
+ ? clampQ((q(1.20) - Math.trunc(mulDiv(q(0.40), logMath, SCALE.Q))), q(0.50), q(1.20))
45
+ : SCALE.Q;
46
+ const scaledLatency_s = logMath ? mulDiv(adjustedLatency_s, logLatencyMul, SCALE.Q) : adjustedLatency_s;
47
+ const latencyTicks = Math.max(1, Math.trunc((scaledLatency_s * TICK_HZ) / SCALE.s));
48
+ self.ai.decisionCooldownTicks = latencyTicks;
49
+ // Phase 5: morale states — routing flees; hesitant suppresses attacks
50
+ const fearQ = (self.condition).fearQ ?? q(0);
51
+ const distressTol = self.attributes.resilience.distressTolerance;
52
+ const fearResp = (self.attributes.resilience).fearResponse ?? "flight";
53
+ // Phase 47: personality-driven overrides
54
+ const personality = self.personality;
55
+ // Feature 6: berserk entities never route or hesitate
56
+ // Phase 47: high-aggression entities (> q(0.70)) also override hesitation
57
+ const isHesitant = fearResp !== "berserk" &&
58
+ !isRouting(fearQ, distressTol) &&
59
+ fearQ >= qMul(moraleThreshold(distressTol), q(0.70)) &&
60
+ (!personality || personality.aggression < q(0.70));
61
+ if (fearResp !== "berserk" && isRouting(fearQ, distressTol)) {
62
+ // Feature 6: freeze archetype routes by freezing instead of fleeing
63
+ if (fearResp === "freeze") {
64
+ return [];
65
+ }
66
+ // Feature 4: panic action variety — seeded surrender/freeze/flee roll
67
+ const panicSeed = eventSeed(world.seed, world.tick, self.id, 0, 0xFA115);
68
+ const surrenderChance = Math.trunc(qMul(q(0.10), (SCALE.Q - distressTol)));
69
+ const freezeChance = Math.trunc(qMul(q(0.15), (SCALE.Q - distressTol)));
70
+ const r = panicSeed % SCALE.Q;
71
+ if (r < surrenderChance) {
72
+ (self.condition).surrendered = true;
73
+ return [
74
+ { kind: "defend", mode: "none", intensity: q(0) },
75
+ { kind: "setProne", prone: true },
76
+ ];
77
+ }
78
+ if (r < surrenderChance + freezeChance) {
79
+ return [];
80
+ }
81
+ const nearestThreat = pickTarget(world, self, index, spatial, policy, env);
82
+ if (nearestThreat) {
83
+ const fdx = self.position_m.x - nearestThreat.position_m.x;
84
+ const fdy = self.position_m.y - nearestThreat.position_m.y;
85
+ return [
86
+ { kind: "defend", mode: "none", intensity: q(0) },
87
+ {
88
+ kind: "move",
89
+ dir: normaliseDirCheapQ(v3(fdx !== 0 || fdy !== 0 ? fdx : 1, fdy, 0)),
90
+ intensity: q(1.0),
91
+ mode: "sprint",
92
+ },
93
+ ];
94
+ }
95
+ return [{ kind: "defend", mode: "none", intensity: q(0) }];
96
+ }
97
+ // Phase 3 extension: suppression response — go prone when sustained under fire (low distressTol only)
98
+ const suppressedTicks = (self.condition).suppressedTicks ?? 0;
99
+ if (suppressedTicks >= 3 && distressTol < q(0.50)) {
100
+ const suppCmds = [{ kind: "defend", mode: "none", intensity: q(0) }];
101
+ if (!self.condition.prone) {
102
+ suppCmds.push({ kind: "setProne", prone: true });
103
+ }
104
+ return suppCmds;
105
+ }
106
+ let target = pickTarget(world, self, index, spatial, policy, env);
107
+ // Phase 47: personality-driven target bias (loyalty before opportunism)
108
+ const effectiveLoyalty = computeEffectiveLoyalty(self, world);
109
+ target = applyLoyaltyBias(self, world, target, effectiveLoyalty);
110
+ if (personality) {
111
+ target = applyOpportunismBias(self, world, target, personality.opportunism);
112
+ }
113
+ // Phase 24: faction standing — suppress attack on friendly entities.
114
+ // Self-defence override: if self has taken damage (shock > 0 or fluid loss > 0),
115
+ // faction check is bypassed (attacker is fought back regardless of standing).
116
+ if (target && self.faction) {
117
+ const factionRegistry = (world).__factionRegistry;
118
+ if (factionRegistry) {
119
+ const standing = effectiveStanding(factionRegistry, self, target);
120
+ const selfDefence = self.injury.shock > 0 || self.injury.fluidLoss > 0;
121
+ if (!selfDefence && standing >= STANDING_FRIENDLY_THRESHOLD) {
122
+ target = undefined;
123
+ }
124
+ }
125
+ }
126
+ updateFocus(self, target, policy);
127
+ // Default defend
128
+ let defendMode = "none";
129
+ let defendIntensity = q(0);
130
+ if (target) {
131
+ const dx = target.position_m.x - self.position_m.x;
132
+ const dy = target.position_m.y - self.position_m.y;
133
+ const d2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy);
134
+ const threatR = Math.max(1, policy.threatRange_m);
135
+ const threatD2 = BigInt(threatR) * BigInt(threatR);
136
+ if (d2 < threatD2) {
137
+ defendMode = pickDefenceModeDeterministic(policy);
138
+ // Phase 47: caution boosts/reduces defence intensity (±q(0.20) max at extremes)
139
+ defendIntensity = personality
140
+ ? computeDefenceIntensityBoost(policy.defendWhenThreatenedQ, personality.caution)
141
+ : clampQ(policy.defendWhenThreatenedQ, q(0), q(1.0));
142
+ }
143
+ }
144
+ const cmds = [];
145
+ cmds.push({ kind: "defend", mode: defendMode, intensity: defendIntensity });
146
+ if (!target) {
147
+ // still emit a “no move”
148
+ cmds.push({ kind: "move", dir: v3(0, 0, 0), intensity: q(0), mode: "walk" });
149
+ return cmds;
150
+ }
151
+ // movement: try to maintain desired range
152
+ const dx = target.position_m.x - self.position_m.x;
153
+ const dy = target.position_m.y - self.position_m.y;
154
+ const distApprox = approxDist(dx, dy);
155
+ const want = policy.desiredRange_m;
156
+ const engage = policy.engageRange_m;
157
+ // Move toward if too far, back off if too close
158
+ // Phase 47: aggression shifts the effective retreat range (aggressive → less retreat)
159
+ const effectiveRetreatRange = personality
160
+ ? computeEffectiveRetreatRange(policy.retreatRange_m, personality.aggression)
161
+ : policy.retreatRange_m;
162
+ let dirX = 0, dirY = 0;
163
+ if (distApprox > want) {
164
+ dirX = dx;
165
+ dirY = dy;
166
+ }
167
+ else if (distApprox < effectiveRetreatRange) {
168
+ dirX = -dx;
169
+ dirY = -dy;
170
+ }
171
+ let moveMode = distApprox > engage ? "sprint" : "run";
172
+ // Phase 6: cover-seeking — if exposed to enemies with no cover, move toward the best adjacent cell.
173
+ const aiCellSize = cellSize_m ?? Math.trunc(4 * SCALE.m);
174
+ const selfCoverQ = obstacleGrid
175
+ ? coverFractionAtPosition(obstacleGrid, aiCellSize, self.position_m.x, self.position_m.y)
176
+ : 0;
177
+ const coverThreshold = suppressedTicks > 0 ? q(0.50) : q(0.30);
178
+ if (selfCoverQ < coverThreshold && !isRouting(fearQ, distressTol)) {
179
+ const enemyCount = world.entities.filter(en => en.teamId !== self.teamId && !en.injury.dead &&
180
+ approxDist(en.position_m.x - self.position_m.x, en.position_m.y - self.position_m.y) < Math.trunc(30 * SCALE.m)).length;
181
+ if (enemyCount > 0) {
182
+ const coverDir = findBestCoverDir(self, obstacleGrid, aiCellSize);
183
+ if (coverDir) {
184
+ dirX = coverDir.x;
185
+ dirY = coverDir.y;
186
+ moveMode = "run";
187
+ }
188
+ }
189
+ }
190
+ if (dirX !== 0 || dirY !== 0) {
191
+ cmds.push({
192
+ kind: "move",
193
+ dir: normaliseDirCheapQ(v3(dirX, dirY, 0)),
194
+ intensity: q(1.0),
195
+ mode: moveMode,
196
+ });
197
+ }
198
+ else {
199
+ cmds.push({
200
+ kind: "move",
201
+ dir: v3(0, 0, 0),
202
+ intensity: q(0),
203
+ mode: "walk",
204
+ });
205
+ }
206
+ // attack when within engage range — hesitant or rallying entities hold back
207
+ const weapon = findWeapon(self.loadout, undefined);
208
+ const isRallying = ((self.condition).rallyCooldownTicks ?? 0) > 0;
209
+ if (weapon && !isHesitant && !isRallying) {
210
+ const reach = weapon.reach_m ?? Math.trunc(self.attributes.morphology.stature_m * 0.45);
211
+ if (distApprox <= reach + Math.trunc(0.25 * SCALE.m)) {
212
+ cmds.push({
213
+ kind: "attack",
214
+ targetId: target.id,
215
+ weaponId: weapon.id,
216
+ intensity: q(1.0),
217
+ mode: "strike",
218
+ });
219
+ }
220
+ }
221
+ return cmds;
222
+ }
223
+ function pickDefenceModeDeterministic(policy) {
224
+ // Deterministic selection (no RNG): allow dodge path.
225
+ // If dodge preference is strong, dodge. Else if parry preference strong, parry. Else block.
226
+ if (policy.dodgeBiasQ > policy.parryBiasQ && policy.dodgeBiasQ > q(0.50))
227
+ return "dodge";
228
+ if (policy.parryBiasQ > q(0.35))
229
+ return "parry";
230
+ return "block";
231
+ }
232
+ function approxDist(dx, dy) {
233
+ const adx = dx < 0 ? -dx : dx;
234
+ const ady = dy < 0 ? -dy : dy;
235
+ return adx > ady ? adx + (ady >> 1) : ady + (adx >> 1);
236
+ }
237
+ /**
238
+ * Scan the 8 adjacent cells and return a direction toward the one with the highest
239
+ * cover fraction that is better than the current cell (and not impassable).
240
+ * Returns undefined if already at the local cover maximum.
241
+ */
242
+ function findBestCoverDir(self, grid, cellSize_m) {
243
+ if (!grid)
244
+ return undefined;
245
+ const cs = Math.max(1, cellSize_m);
246
+ const cx = Math.trunc(self.position_m.x / cs);
247
+ const cy = Math.trunc(self.position_m.y / cs);
248
+ const currentCover = grid.get(terrainKey(cx, cy)) ?? 0;
249
+ let bestCover = currentCover;
250
+ let bestDx = 0, bestDy = 0;
251
+ for (let ddx = -1; ddx <= 1; ddx++) {
252
+ for (let ddy = -1; ddy <= 1; ddy++) {
253
+ if (ddx === 0 && ddy === 0)
254
+ continue;
255
+ const frac = grid.get(terrainKey(cx + ddx, cy + ddy)) ?? 0;
256
+ // Prefer higher cover but skip impassable cells (q(1.0) = SCALE.Q)
257
+ if (frac > bestCover && frac < SCALE.Q) {
258
+ bestCover = frac;
259
+ bestDx = ddx;
260
+ bestDy = ddy;
261
+ }
262
+ }
263
+ }
264
+ if (bestDx === 0 && bestDy === 0)
265
+ return undefined;
266
+ return { x: bestDx, y: bestDy, z: 0 };
267
+ }
@@ -0,0 +1,12 @@
1
+ import type { Entity } from "../entity.js";
2
+ import type { WorldState } from "../world.js";
3
+ import type { WorldIndex } from "../indexing.js";
4
+ import type { SpatialIndex } from "../spatial.js";
5
+ import { type SensoryEnvironment } from "../sensory.js";
6
+ export interface LocalPerception {
7
+ enemies: Entity[];
8
+ allies: Entity[];
9
+ }
10
+ /** @deprecated Use LocalPerception */
11
+ export type Perception = LocalPerception;
12
+ export declare function perceiveLocal(world: WorldState | undefined, self: Entity, index: WorldIndex, spatial: SpatialIndex, radius_m: number, maxCount?: number, env?: SensoryEnvironment): LocalPerception;
@@ -0,0 +1,54 @@
1
+ import { queryNearbyIds } from "../spatial.js";
2
+ import { isEnemy, areEntitiesHostile } from "../team.js";
3
+ import { q } from "../../units.js";
4
+ import { canDetect, DEFAULT_PERCEPTION, DEFAULT_SENSORY_ENV } from "../sensory.js";
5
+ import { findSensor } from "../../equipment.js";
6
+ export function perceiveLocal(world, self, index, spatial, radius_m, maxCount = 24, env = DEFAULT_SENSORY_ENV) {
7
+ const perc = (self.attributes).perception ?? DEFAULT_PERCEPTION;
8
+ // Use the threat horizon as the spatial query radius if it is smaller than the requested radius.
9
+ const effectiveRadius = Math.min(radius_m, perc.threatHorizon_m);
10
+ const ids = queryNearbyIds(spatial, self.position_m, effectiveRadius);
11
+ ids.sort((a, b) => a - b);
12
+ const enemies = [];
13
+ const allies = [];
14
+ for (const id of ids) {
15
+ if (id === self.id)
16
+ continue;
17
+ const e = index.byId.get(id);
18
+ if (!e || e.injury.dead)
19
+ continue;
20
+ // Phase 4: filter by sensory detection
21
+ // Phase 11C: derive sensor boost from loadout
22
+ const sensor = findSensor(self.loadout);
23
+ const sensorBoost = sensor
24
+ ? { visionRangeMul: sensor.visionRangeMul, hearingRangeMul: sensor.hearingRangeMul }
25
+ : undefined;
26
+ const detQ = canDetect(self, e, env, sensorBoost);
27
+ if (detQ <= q(0))
28
+ continue;
29
+ const hostile = world ? areEntitiesHostile(self, e, world) : isEnemy(self, e);
30
+ if (hostile)
31
+ enemies.push(e);
32
+ else
33
+ allies.push(e);
34
+ if (enemies.length + allies.length >= maxCount)
35
+ break;
36
+ }
37
+ // deterministic order: distance² then id
38
+ const sortByDist = (a, b) => {
39
+ const dxA = a.position_m.x - self.position_m.x;
40
+ const dyA = a.position_m.y - self.position_m.y;
41
+ const d2A = BigInt(dxA) * BigInt(dxA) + BigInt(dyA) * BigInt(dyA);
42
+ const dxB = b.position_m.x - self.position_m.x;
43
+ const dyB = b.position_m.y - self.position_m.y;
44
+ const d2B = BigInt(dxB) * BigInt(dxB) + BigInt(dyB) * BigInt(dyB);
45
+ if (d2A < d2B)
46
+ return -1;
47
+ if (d2A > d2B)
48
+ return 1;
49
+ return a.id - b.id;
50
+ };
51
+ enemies.sort(sortByDist);
52
+ allies.sort(sortByDist);
53
+ return { enemies, allies };
54
+ }
@@ -0,0 +1,54 @@
1
+ import type { Q } from "../../units.js";
2
+ import type { Entity } from "../entity.js";
3
+ import type { WorldState } from "../world.js";
4
+ import type { IndividualAttributes, PersonalityTraits, PersonalityId } from "../../types.js";
5
+ export type { PersonalityTraits, PersonalityId };
6
+ /**
7
+ * Neutral personality: q(0.50) on all axes.
8
+ * Produces identical behaviour to an entity with no personality set.
9
+ */
10
+ export declare const NEUTRAL_PERSONALITY: PersonalityTraits;
11
+ /** Named predefined personalities. */
12
+ export declare const PERSONALITIES: Record<PersonalityId, PersonalityTraits>;
13
+ /**
14
+ * Derive a personality from existing cognitive and resilience attributes.
15
+ *
16
+ * Mapping rationale:
17
+ * aggression ← distressTolerance (pain tolerance → willing to keep fighting)
18
+ * caution ← intrapersonal (self-awareness → more careful and defensive)
19
+ * loyalty ← interpersonal (social empathy → protects allies)
20
+ * opportunism ← logicalMathematical (planning → targets strategically)
21
+ */
22
+ export declare function derivePersonalityFromCognition(attrs: IndividualAttributes): PersonalityTraits;
23
+ /**
24
+ * Compute effective loyalty combining personality loyalty and companion loyalty to party leader.
25
+ * If entity belongs to a party and has a relationship with the leader, companion loyalty is used.
26
+ * Otherwise falls back to personality loyalty (or neutral q(0.50) if no personality).
27
+ */
28
+ export declare function computeEffectiveLoyalty(self: Entity, world: WorldState): Q;
29
+ /**
30
+ * Effective retreat range after aggression bias.
31
+ *
32
+ * aggression q(0.90) → range reduced by ~0.20m (fights more aggressively)
33
+ * aggression q(0.50) → unchanged
34
+ * aggression q(0.10) → range increased by ~0.20m (retreats sooner)
35
+ */
36
+ export declare function computeEffectiveRetreatRange(baseRange_m: number, aggression: Q): number;
37
+ /**
38
+ * Effective defence intensity after caution bias.
39
+ *
40
+ * caution q(0.90) → +q(0.20) max boost
41
+ * caution q(0.50) → unchanged
42
+ * caution q(0.10) → −q(0.20) max reduction
43
+ */
44
+ export declare function computeDefenceIntensityBoost(baseIntensity: Q, caution: Q): Q;
45
+ /**
46
+ * Loyalty override: if an ally is in distress and has an enemy nearby, switch target to
47
+ * that enemy. Only triggers when loyalty > q(0.50); roll probability = loyaltyQ / SCALE.Q.
48
+ */
49
+ export declare function applyLoyaltyBias(self: Entity, world: WorldState, currentTarget: Entity | undefined, loyaltyQ: Q): Entity | undefined;
50
+ /**
51
+ * Opportunism override: if a significantly more-wounded enemy is present, switch target.
52
+ * Only triggers when opportunism > q(0.50); roll probability = opportunismQ / SCALE.Q.
53
+ */
54
+ export declare function applyOpportunismBias(self: Entity, world: WorldState, currentTarget: Entity | undefined, opportunismQ: Q): Entity | undefined;