@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,109 @@
1
+ /**
2
+ * Phase 4: Sensory environment model.
3
+ *
4
+ * Fixed-point only. No Math.random() — all randomness via eventSeed if needed.
5
+ * Light, smoke, and noise modifiers are Q values (SCALE.Q = full normal conditions).
6
+ */
7
+ import { SCALE, q, mulDiv } from "../units.js";
8
+ import { getSkill } from "./skills.js";
9
+ // Default perception — used as init guard for entities without Phase 4 attributes.
10
+ export const DEFAULT_PERCEPTION = {
11
+ visionRange_m: Math.trunc(200 * SCALE.m),
12
+ visionArcDeg: 120,
13
+ halfArcCosQ: Math.round(Math.cos((120 / 2) * (Math.PI / 180)) * SCALE.Q),
14
+ hearingRange_m: Math.trunc(50 * SCALE.m),
15
+ decisionLatency_s: Math.trunc(0.5 * SCALE.s),
16
+ attentionDepth: 4,
17
+ threatHorizon_m: Math.trunc(40 * SCALE.m),
18
+ };
19
+ export const DEFAULT_SENSORY_ENV = {
20
+ lightMul: q(1.0),
21
+ smokeMul: q(1.0),
22
+ noiseMul: q(1.0),
23
+ };
24
+ /**
25
+ * Compute detection quality of `subject` by `observer`.
26
+ *
27
+ * Returns a Q value:
28
+ * q(1.0) = fully visible (within vision arc and range)
29
+ * q(0.4) = heard only (within hearing range but not vision)
30
+ * q(0) = undetected
31
+ *
32
+ * Vision check: dot-product of facing direction vs observer→subject vector.
33
+ * Hearing: omnidirectional.
34
+ *
35
+ * Pure function — no side effects.
36
+ */
37
+ export function canDetect(observer, subject, env,
38
+ /** Phase 11C: optional sensor boost from the observer's loadout. */
39
+ sensorBoost) {
40
+ const perc = (observer.attributes).perception ?? DEFAULT_PERCEPTION;
41
+ // Phase 10C: blinded observer cannot see
42
+ const isBlind = (observer.condition).blindTicks > 0;
43
+ const dx = subject.position_m.x - observer.position_m.x;
44
+ const dy = subject.position_m.y - observer.position_m.y;
45
+ const dz = subject.position_m.z - observer.position_m.z;
46
+ // Squared distance (still in SCALE.m² fixed-point)
47
+ const dist2 = BigInt(dx) * BigInt(dx) + BigInt(dy) * BigInt(dy) + BigInt(dz) * BigInt(dz);
48
+ // ---- Vision ----
49
+ // Phase 10C: blinded observer has zero effective vision range
50
+ let effectiveVision = isBlind ? 0 : mulDiv(mulDiv(perc.visionRange_m, env.lightMul, SCALE.Q), env.smokeMul, SCALE.Q);
51
+ if (!isBlind && sensorBoost)
52
+ effectiveVision = mulDiv(effectiveVision, sensorBoost.visionRangeMul, SCALE.Q);
53
+ const visionR2 = BigInt(effectiveVision) * BigInt(effectiveVision);
54
+ if (dist2 <= visionR2) {
55
+ // Check if subject is within observer's facing arc.
56
+ // For 360° arc (visionArcDeg >= 360) skip the arc check.
57
+ if (perc.visionArcDeg >= 360)
58
+ return q(1.0);
59
+ // Dot product of normalized facing vs direction to subject.
60
+ // We compare in fixed-point Q units using the pre-computed halfArcCosQ.
61
+ const facing = observer.action.facingDirQ;
62
+ const dotQ = dotQ3(facing, dx, dy, dz, dist2);
63
+ if (dotQ >= perc.halfArcCosQ)
64
+ return q(1.0);
65
+ }
66
+ // ---- Hearing ----
67
+ // Phase 7: stealth.dispersionMul reduces subject's acoustic signature
68
+ // (multiplied into observer's effective hearing range for this subject)
69
+ const stealthSkill = getSkill(subject.skills, "stealth");
70
+ let effectiveHearing = mulDiv(mulDiv(perc.hearingRange_m, env.noiseMul, SCALE.Q), stealthSkill.dispersionMul, SCALE.Q);
71
+ if (sensorBoost)
72
+ effectiveHearing = mulDiv(effectiveHearing, sensorBoost.hearingRangeMul, SCALE.Q);
73
+ const hearingR2 = BigInt(effectiveHearing) * BigInt(effectiveHearing);
74
+ if (dist2 <= hearingR2)
75
+ return q(0.4);
76
+ return q(0);
77
+ }
78
+ /**
79
+ * Dot product of a normalized facing direction (Q components) against an unnormalized
80
+ * vector (dx, dy, dz) with squared magnitude dist2. Returns a Q value.
81
+ *
82
+ * facing is already in Q units (each component is Q scaled, magnitude ≈ SCALE.Q).
83
+ * dx/dy/dz are in SCALE.m units; we normalise them into Q using dist_m.
84
+ *
85
+ * Result in Q: positive = same direction, negative = opposite.
86
+ */
87
+ function dotQ3(facing, dx, dy, dz, dist2) {
88
+ if (dist2 === 0n)
89
+ return q(0);
90
+ // Approximate dist_m via integer sqrt of dist2
91
+ let r = dist2;
92
+ let r1 = (r + 1n) >> 1n;
93
+ while (r1 < r) {
94
+ r = r1;
95
+ r1 = (r + dist2 / r) >> 1n;
96
+ }
97
+ const dist_m = Number(r);
98
+ if (dist_m === 0)
99
+ return q(0);
100
+ // Normalise dx/dy/dz into Q space
101
+ const ndx = mulDiv(dx, SCALE.Q, dist_m);
102
+ const ndy = mulDiv(dy, SCALE.Q, dist_m);
103
+ const ndz = mulDiv(dz, SCALE.Q, dist_m);
104
+ // Both facing and n* are in Q units; dot product → divide by SCALE.Q
105
+ const raw = mulDiv(facing.x, ndx, SCALE.Q)
106
+ + mulDiv(facing.y, ndy, SCALE.Q)
107
+ + mulDiv(facing.z, ndz, SCALE.Q);
108
+ return Math.max(-SCALE.Q, Math.min(SCALE.Q, raw));
109
+ }
@@ -0,0 +1,70 @@
1
+ import { type Q, type I32 } from "../units.js";
2
+ export declare const SKILL_IDS: readonly ["meleeCombat", "meleeDefence", "grappling", "rangedCombat", "throwingWeapons", "shieldCraft", "medical", "athleticism", "tactics", "stealth"];
3
+ export type SkillId = (typeof SKILL_IDS)[number];
4
+ /**
5
+ * A SkillLevel is a set of physical outcome modifiers for one skill domain.
6
+ * All fields default to the neutral value (no effect on simulation output).
7
+ */
8
+ export interface SkillLevel {
9
+ /**
10
+ * Timing offset (SCALE.s units). Negative = faster action or reaction.
11
+ * meleeCombat: reduces attack recovery time (fewer ticks until next attack).
12
+ * tactics: reduces AI decision latency (faster plan revisions).
13
+ */
14
+ hitTimingOffset_s: I32;
15
+ /**
16
+ * Efficiency multiplier (Q). > q(1.0) = beneficial.
17
+ * meleeCombat: multiplied into strike impact energy.
18
+ * meleeDefence: multiplied into effective defence skill (parry / block quality).
19
+ * grappling: multiplied into grapple contest score (leverage bonus).
20
+ * throwingWeapons: multiplied into thrown weapon launch energy.
21
+ * shieldCraft: multiplied into effective defence skill when blocking with a shield.
22
+ */
23
+ energyTransferMul: Q;
24
+ /**
25
+ * Dispersion multiplier (Q). < q(1.0) = tighter spread or smaller signature.
26
+ * rangedCombat: multiplied into adjusted dispersion (more accurate fire).
27
+ * stealth: multiplied into the observer's effective hearing range for this
28
+ * entity (reduces acoustic signature — harder to detect by hearing).
29
+ */
30
+ dispersionMul: Q;
31
+ /**
32
+ * Treatment rate multiplier (Q). > q(1.0) = better self-care.
33
+ * medical: divides the effective bleed-to-fluid-loss increment each tick
34
+ * (passive wound management — slower fluid loss from bleeding).
35
+ */
36
+ treatmentRateMul: Q;
37
+ /**
38
+ * Fatigue rate multiplier (Q). < q(1.0) = less fatigue per tick.
39
+ * athleticism: multiplied into the fatigue delta each energy tick.
40
+ */
41
+ fatigueRateMul: Q;
42
+ }
43
+ export type SkillMap = Map<SkillId, SkillLevel>;
44
+ /** Returns a SkillLevel with all fields at the neutral (no-effect) value. */
45
+ export declare function defaultSkillLevel(): SkillLevel;
46
+ /**
47
+ * Build a SkillMap from a partial record.
48
+ * Any missing fields in each entry default to the neutral values.
49
+ */
50
+ export declare function buildSkillMap(entries: Partial<Record<SkillId, Partial<SkillLevel>>>): SkillMap;
51
+ /** Look up a skill level; returns neutral defaults when the map is absent or the skill is not set. */
52
+ export declare function getSkill(skills: SkillMap | undefined, id: SkillId): SkillLevel;
53
+ /**
54
+ * Combine two SkillLevels into one composite level.
55
+ *
56
+ * Use this in the host application to express synergy bonuses or to composite
57
+ * a base skill with a situational modifier before building the SkillMap.
58
+ * The engine itself has no concept of synergies — compositing happens outside.
59
+ *
60
+ * Combination rules:
61
+ * hitTimingOffset_s — additive (both offsets reduce timing independently)
62
+ * energyTransferMul — qMul (efficiency gains multiply)
63
+ * dispersionMul — qMul (tighter spreads multiply)
64
+ * treatmentRateMul — qMul (healing bonuses multiply)
65
+ * fatigueRateMul — qMul (fatigue reductions multiply)
66
+ *
67
+ * Example: meleeCombat synergised with an athleticism bonus
68
+ * buildSkillMap({ meleeCombat: combineSkillLevels(baseMelee, athleticismSynergyBonus) })
69
+ */
70
+ export declare function combineSkillLevels(a: SkillLevel, b: SkillLevel): SkillLevel;
@@ -0,0 +1,69 @@
1
+ // src/sim/skills.ts — Phase 7: Skill System
2
+ //
3
+ // Skills are learned technique modifiers separate from physical attributes.
4
+ // They adjust physical outcomes rather than providing abstract point bonuses.
5
+ // The engine consumes skill values; progression is managed by the host application.
6
+ import { q } from "../units.js";
7
+ export const SKILL_IDS = [
8
+ "meleeCombat",
9
+ "meleeDefence",
10
+ "grappling",
11
+ "rangedCombat",
12
+ "throwingWeapons",
13
+ "shieldCraft",
14
+ "medical",
15
+ "athleticism",
16
+ "tactics",
17
+ "stealth",
18
+ ];
19
+ /** Returns a SkillLevel with all fields at the neutral (no-effect) value. */
20
+ export function defaultSkillLevel() {
21
+ return {
22
+ hitTimingOffset_s: 0,
23
+ energyTransferMul: q(1.0),
24
+ dispersionMul: q(1.0),
25
+ treatmentRateMul: q(1.0),
26
+ fatigueRateMul: q(1.0),
27
+ };
28
+ }
29
+ /**
30
+ * Build a SkillMap from a partial record.
31
+ * Any missing fields in each entry default to the neutral values.
32
+ */
33
+ export function buildSkillMap(entries) {
34
+ const m = new Map();
35
+ for (const [k, v] of Object.entries(entries)) {
36
+ m.set(k, { ...defaultSkillLevel(), ...v });
37
+ }
38
+ return m;
39
+ }
40
+ /** Look up a skill level; returns neutral defaults when the map is absent or the skill is not set. */
41
+ export function getSkill(skills, id) {
42
+ return skills?.get(id) ?? defaultSkillLevel();
43
+ }
44
+ /**
45
+ * Combine two SkillLevels into one composite level.
46
+ *
47
+ * Use this in the host application to express synergy bonuses or to composite
48
+ * a base skill with a situational modifier before building the SkillMap.
49
+ * The engine itself has no concept of synergies — compositing happens outside.
50
+ *
51
+ * Combination rules:
52
+ * hitTimingOffset_s — additive (both offsets reduce timing independently)
53
+ * energyTransferMul — qMul (efficiency gains multiply)
54
+ * dispersionMul — qMul (tighter spreads multiply)
55
+ * treatmentRateMul — qMul (healing bonuses multiply)
56
+ * fatigueRateMul — qMul (fatigue reductions multiply)
57
+ *
58
+ * Example: meleeCombat synergised with an athleticism bonus
59
+ * buildSkillMap({ meleeCombat: combineSkillLevels(baseMelee, athleticismSynergyBonus) })
60
+ */
61
+ export function combineSkillLevels(a, b) {
62
+ return {
63
+ hitTimingOffset_s: (a.hitTimingOffset_s + b.hitTimingOffset_s),
64
+ energyTransferMul: Math.trunc(a.energyTransferMul * b.energyTransferMul / 10_000),
65
+ dispersionMul: Math.trunc(a.dispersionMul * b.dispersionMul / 10_000),
66
+ treatmentRateMul: Math.trunc(a.treatmentRateMul * b.treatmentRateMul / 10_000),
67
+ fatigueRateMul: Math.trunc(a.fatigueRateMul * b.fatigueRateMul / 10_000),
68
+ };
69
+ }
@@ -0,0 +1,107 @@
1
+ import { type Q } from "../units.js";
2
+ import type { IndividualAttributes } from "../types.js";
3
+ import type { Entity } from "./entity.js";
4
+ /** Current sleep phase. "awake" when the entity is not sleeping. */
5
+ export type SleepPhase = "awake" | "light" | "deep" | "rem";
6
+ /** Deprivation-driven attribute multipliers (all Q). */
7
+ export interface SleepDeprivationMuls {
8
+ /** Fluid cognition multiplier: degrades fastest under sleep loss [Q]. */
9
+ cognitionFluid_Q: Q;
10
+ /** Reaction time multiplier: > q(1.0) = slower reaction [Q]. */
11
+ reactionTime_Q: Q;
12
+ /** Balance / postural stability multiplier [Q]. */
13
+ stability_Q: Q;
14
+ /** Distress tolerance multiplier: emotional dysregulation [Q]. */
15
+ distressTolerance_Q: Q;
16
+ }
17
+ /** Per-entity sleep state stored on `entity.sleep`. */
18
+ export interface SleepState {
19
+ /** Current sleep phase ("awake" when not sleeping). */
20
+ phase: SleepPhase;
21
+ /** Seconds spent in the current phase. */
22
+ phaseSeconds: number;
23
+ /** Cumulative sleep deficit in seconds (capped at MAX_SLEEP_DEBT_S). */
24
+ sleepDebt_s: number;
25
+ /** Continuous seconds since last sleep bout. Resets to 0 on sleep onset. */
26
+ awakeSeconds: number;
27
+ }
28
+ /** Optimal sleep duration per 24-hour period [s]. */
29
+ export declare const OPTIMAL_SLEEP_S: number;
30
+ /** Optimal waking duration per 24-hour period [s]. */
31
+ export declare const OPTIMAL_AWAKE_S: number;
32
+ /** Continuous wake time above which cognitive/motor impairment begins [s]. */
33
+ export declare const IMPAIR_THRESHOLD_S: number;
34
+ /** Maximum sleep debt tracked (3 days of total sleep deprivation) [s]. */
35
+ export declare const MAX_SLEEP_DEBT_S: number;
36
+ /** Coefficient for cognition fluid degradation per unit impair fraction [numeric]. */
37
+ export declare const COGNITION_FLUID_COEFF = 0.798;
38
+ /** Coefficient for reaction time slowdown per unit impair fraction [numeric]. */
39
+ export declare const REACTION_TIME_COEFF = 0.45;
40
+ /** Coefficient for stability degradation per unit impair fraction [numeric]. */
41
+ export declare const STABILITY_COEFF = 0.25;
42
+ /** Coefficient for distress tolerance degradation per unit impair fraction [numeric]. */
43
+ export declare const DISTRESS_TOLERANCE_COEFF = 0.35;
44
+ /** Duration of the light-sleep (NREM-1/2) phase per cycle [s]. */
45
+ export declare const LIGHT_PHASE_S: number;
46
+ /** Duration of the deep-sleep (slow-wave) phase per cycle [s]. */
47
+ export declare const DEEP_PHASE_S: number;
48
+ /** Duration of the REM phase per cycle [s]. */
49
+ export declare const REM_PHASE_S: number;
50
+ /**
51
+ * Circadian alertness at a given time of day.
52
+ *
53
+ * @param hourOfDay Float in [0, 24). Values outside this range are normalised.
54
+ * @returns Q in [q(0.30), q(1.0)]: q(1.0) at ~17:00, q(0.30) at ~03:00.
55
+ */
56
+ export declare function circadianAlertness(hourOfDay: number): Q;
57
+ /**
58
+ * Derive sleep-deprivation attribute multipliers from the entity's sleep state.
59
+ *
60
+ * Impairment is driven by the greater of:
61
+ * - `awakeSeconds` — continuous wake duration (resets on sleep)
62
+ * - `sleepDebt_s` — cumulative shortfall from prior nights
63
+ *
64
+ * Below IMPAIR_THRESHOLD_S (17 h) both drivers produce no impairment.
65
+ * Full impairment is reached at MAX_SLEEP_DEBT_S (72 h).
66
+ *
67
+ * Multiplier ranges at max deprivation:
68
+ * cognitionFluid_Q: q(1.0) → q(0.202) (−79.8%)
69
+ * reactionTime_Q: q(1.0) → q(1.45) (+45% slower)
70
+ * stability_Q: q(1.0) → q(0.75) (−25%)
71
+ * distressTolerance_Q: q(1.0) → q(0.65) (−35%)
72
+ */
73
+ export declare function deriveSleepDeprivationMuls(state: SleepState): SleepDeprivationMuls;
74
+ /**
75
+ * Advance an entity's sleep state by `elapsedSeconds`.
76
+ *
77
+ * When `isSleeping = false` (awake):
78
+ * - `awakeSeconds` accumulates.
79
+ * - `sleepDebt_s` accrues at ½ s/s for each second spent beyond OPTIMAL_AWAKE_S.
80
+ * (16 h waking × ½ = 8 h debt — exactly one night's repayment if sleep was ideal.)
81
+ * - Phase stays or transitions to "awake".
82
+ *
83
+ * When `isSleeping = true`:
84
+ * - On sleep onset (phase was "awake"): `awakeSeconds` resets to 0; phase enters "light".
85
+ * - `sleepDebt_s` decrements 1:1 with elapsed sleep time (floored at 0).
86
+ * - Phase cycles: light → deep → rem → light (90-minute NREM/REM cycle).
87
+ *
88
+ * Mutates: `entity.sleep`.
89
+ */
90
+ export declare function stepSleep(entity: Entity, elapsedSeconds: number, isSleeping: boolean): void;
91
+ /**
92
+ * Apply sleep-deprivation multipliers to a base attribute set, returning a new object.
93
+ *
94
+ * Attributes affected:
95
+ * - control.reactionTime_s, stability
96
+ * - resilience.distressTolerance
97
+ * - cognition (if present): fluid dimensions (logical, spatial, kinesthetic, musical)
98
+ *
99
+ * Immutable — does not mutate `base`.
100
+ * Pattern matches `applyAgingToAttributes` (Phase 57).
101
+ */
102
+ export declare function applySleepToAttributes(base: IndividualAttributes, state: SleepState): IndividualAttributes;
103
+ /**
104
+ * Return the entity's accumulated sleep debt in hours.
105
+ * Returns 0 if `entity.sleep` is absent.
106
+ */
107
+ export declare function entitySleepDebt_h(entity: Entity): number;
@@ -0,0 +1,215 @@
1
+ // src/sim/sleep.ts — Phase 58: Sleep & Circadian Rhythm
2
+ //
3
+ // Models circadian alertness, sleep-phase cycling, and attribute degradation from
4
+ // sleep deprivation. Designed as a companion to Phase 57 (Aging & Lifespan):
5
+ // same fixed-point arithmetic, same immutable apply… pattern.
6
+ //
7
+ // Two-factor impairment model:
8
+ // awakeSeconds — continuous wake duration since last sleep (primary driver)
9
+ // sleepDebt_s — cumulative shortfall from prior nights (secondary; persists across sleep)
10
+ //
11
+ // Sleep phase cycle (90 min): light (45 min) → deep (25 min) → rem (20 min) → light …
12
+ //
13
+ // Public API:
14
+ // circadianAlertness(hourOfDay) → Q [0..SCALE.Q]
15
+ // deriveSleepDeprivationMuls(state) → SleepDeprivationMuls
16
+ // stepSleep(entity, elapsedSeconds, sleeping) → mutates entity.sleep
17
+ // applySleepToAttributes(base, state) → IndividualAttributes (new object)
18
+ // entitySleepDebt_h(entity) → number
19
+ import { q, clampQ, SCALE } from "../units.js";
20
+ // ── Constants ─────────────────────────────────────────────────────────────────
21
+ /** Optimal sleep duration per 24-hour period [s]. */
22
+ export const OPTIMAL_SLEEP_S = 8 * 3600; // 28 800
23
+ /** Optimal waking duration per 24-hour period [s]. */
24
+ export const OPTIMAL_AWAKE_S = 16 * 3600; // 57 600
25
+ /** Continuous wake time above which cognitive/motor impairment begins [s]. */
26
+ export const IMPAIR_THRESHOLD_S = 17 * 3600; // 61 200
27
+ /** Maximum sleep debt tracked (3 days of total sleep deprivation) [s]. */
28
+ export const MAX_SLEEP_DEBT_S = 72 * 3600; // 259 200
29
+ /** Coefficient for cognition fluid degradation per unit impair fraction [numeric]. */
30
+ export const COGNITION_FLUID_COEFF = 0.798;
31
+ /** Coefficient for reaction time slowdown per unit impair fraction [numeric]. */
32
+ export const REACTION_TIME_COEFF = 0.45;
33
+ /** Coefficient for stability degradation per unit impair fraction [numeric]. */
34
+ export const STABILITY_COEFF = 0.25;
35
+ /** Coefficient for distress tolerance degradation per unit impair fraction [numeric]. */
36
+ export const DISTRESS_TOLERANCE_COEFF = 0.35;
37
+ /** Duration of the light-sleep (NREM-1/2) phase per cycle [s]. */
38
+ export const LIGHT_PHASE_S = 45 * 60; // 2 700
39
+ /** Duration of the deep-sleep (slow-wave) phase per cycle [s]. */
40
+ export const DEEP_PHASE_S = 25 * 60; // 1 500
41
+ /** Duration of the REM phase per cycle [s]. */
42
+ export const REM_PHASE_S = 20 * 60; // 1 200
43
+ // ── Circadian alertness ───────────────────────────────────────────────────────
44
+ // Piecewise-linear hourly alertness table [hourOfDay → Q].
45
+ // Peaks at ~17:00 (afternoon), nadir at ~03:00 (pre-dawn), secondary dip at 14:00.
46
+ const CIRCADIAN_KNOTS = [
47
+ [0, q(0.45)],
48
+ [3, q(0.30)], // nadir ~03:00
49
+ [6, q(0.60)], // morning rise
50
+ [10, q(0.95)], // morning peak
51
+ [14, q(0.80)], // post-lunch dip
52
+ [17, q(1.00)], // afternoon peak
53
+ [21, q(0.70)], // evening decline
54
+ [24, q(0.45)], // back to midnight
55
+ ];
56
+ /**
57
+ * Circadian alertness at a given time of day.
58
+ *
59
+ * @param hourOfDay Float in [0, 24). Values outside this range are normalised.
60
+ * @returns Q in [q(0.30), q(1.0)]: q(1.0) at ~17:00, q(0.30) at ~03:00.
61
+ */
62
+ export function circadianAlertness(hourOfDay) {
63
+ const h = ((hourOfDay % 24) + 24) % 24;
64
+ for (let i = 1; i < CIRCADIAN_KNOTS.length; i++) {
65
+ const [x0, y0] = CIRCADIAN_KNOTS[i - 1];
66
+ const [x1, y1] = CIRCADIAN_KNOTS[i];
67
+ if (h <= x1) {
68
+ const span = x1 - x0;
69
+ if (span === 0)
70
+ return y0;
71
+ const t = Math.round((h - x0) * SCALE.Q / span);
72
+ return (y0 + Math.round((y1 - y0) * t / SCALE.Q));
73
+ }
74
+ }
75
+ return CIRCADIAN_KNOTS[CIRCADIAN_KNOTS.length - 1][1];
76
+ }
77
+ // ── Sleep deprivation ─────────────────────────────────────────────────────────
78
+ /**
79
+ * Derive sleep-deprivation attribute multipliers from the entity's sleep state.
80
+ *
81
+ * Impairment is driven by the greater of:
82
+ * - `awakeSeconds` — continuous wake duration (resets on sleep)
83
+ * - `sleepDebt_s` — cumulative shortfall from prior nights
84
+ *
85
+ * Below IMPAIR_THRESHOLD_S (17 h) both drivers produce no impairment.
86
+ * Full impairment is reached at MAX_SLEEP_DEBT_S (72 h).
87
+ *
88
+ * Multiplier ranges at max deprivation:
89
+ * cognitionFluid_Q: q(1.0) → q(0.202) (−79.8%)
90
+ * reactionTime_Q: q(1.0) → q(1.45) (+45% slower)
91
+ * stability_Q: q(1.0) → q(0.75) (−25%)
92
+ * distressTolerance_Q: q(1.0) → q(0.65) (−35%)
93
+ */
94
+ export function deriveSleepDeprivationMuls(state) {
95
+ const effectiveS = Math.max(state.awakeSeconds, state.sleepDebt_s);
96
+ const raw = Math.max(0, effectiveS - IMPAIR_THRESHOLD_S);
97
+ const range = MAX_SLEEP_DEBT_S - IMPAIR_THRESHOLD_S; // 198 000
98
+ const impairFrac_Q = clampQ(Math.round(raw * SCALE.Q / range), q(0), SCALE.Q);
99
+ const cognitionFluid_Q = clampQ((SCALE.Q - Math.round(impairFrac_Q * COGNITION_FLUID_COEFF)), q(0), SCALE.Q);
100
+ // > SCALE.Q means slower than baseline (mirrors aging reactionTime_Q convention)
101
+ const reactionTime_Q = (SCALE.Q + Math.round(impairFrac_Q * REACTION_TIME_COEFF));
102
+ const stability_Q = clampQ((SCALE.Q - Math.round(impairFrac_Q * STABILITY_COEFF)), q(0), SCALE.Q);
103
+ const distressTolerance_Q = clampQ((SCALE.Q - Math.round(impairFrac_Q * DISTRESS_TOLERANCE_COEFF)), q(0), SCALE.Q);
104
+ return { cognitionFluid_Q, reactionTime_Q, stability_Q, distressTolerance_Q };
105
+ }
106
+ // ── stepSleep ─────────────────────────────────────────────────────────────────
107
+ /**
108
+ * Advance an entity's sleep state by `elapsedSeconds`.
109
+ *
110
+ * When `isSleeping = false` (awake):
111
+ * - `awakeSeconds` accumulates.
112
+ * - `sleepDebt_s` accrues at ½ s/s for each second spent beyond OPTIMAL_AWAKE_S.
113
+ * (16 h waking × ½ = 8 h debt — exactly one night's repayment if sleep was ideal.)
114
+ * - Phase stays or transitions to "awake".
115
+ *
116
+ * When `isSleeping = true`:
117
+ * - On sleep onset (phase was "awake"): `awakeSeconds` resets to 0; phase enters "light".
118
+ * - `sleepDebt_s` decrements 1:1 with elapsed sleep time (floored at 0).
119
+ * - Phase cycles: light → deep → rem → light (90-minute NREM/REM cycle).
120
+ *
121
+ * Mutates: `entity.sleep`.
122
+ */
123
+ export function stepSleep(entity, elapsedSeconds, isSleeping) {
124
+ if (!entity.sleep) {
125
+ entity.sleep = { phase: "awake", phaseSeconds: 0, sleepDebt_s: 0, awakeSeconds: 0 };
126
+ }
127
+ const s = entity.sleep;
128
+ if (!isSleeping) {
129
+ if (s.phase !== "awake") {
130
+ s.phase = "awake";
131
+ s.phaseSeconds = 0;
132
+ }
133
+ const prevAwake = s.awakeSeconds;
134
+ s.awakeSeconds += elapsedSeconds;
135
+ s.phaseSeconds += elapsedSeconds;
136
+ // Debt accrues only for time spent beyond the optimal waking window
137
+ const debtStart = Math.max(prevAwake, OPTIMAL_AWAKE_S);
138
+ const debtEnd = s.awakeSeconds;
139
+ if (debtEnd > debtStart) {
140
+ s.sleepDebt_s = Math.min(MAX_SLEEP_DEBT_S, s.sleepDebt_s + Math.round((debtEnd - debtStart) / 2));
141
+ }
142
+ }
143
+ else {
144
+ if (s.phase === "awake") {
145
+ // Sleep onset: enter light phase and reset continuous wake timer
146
+ s.phase = "light";
147
+ s.phaseSeconds = 0;
148
+ s.awakeSeconds = 0;
149
+ }
150
+ // Repay debt 1:1 (cannot go below 0)
151
+ s.sleepDebt_s = Math.max(0, s.sleepDebt_s - elapsedSeconds);
152
+ s.phaseSeconds += elapsedSeconds;
153
+ // Advance through NREM/REM cycle (phase is "light"|"deep"|"rem" at this point)
154
+ for (;;) {
155
+ const dur = s.phase === "light" ? LIGHT_PHASE_S
156
+ : s.phase === "deep" ? DEEP_PHASE_S
157
+ : REM_PHASE_S;
158
+ if (s.phaseSeconds < dur)
159
+ break;
160
+ s.phaseSeconds -= dur;
161
+ s.phase = s.phase === "light" ? "deep"
162
+ : s.phase === "deep" ? "rem"
163
+ : "light"; // rem → back to light
164
+ }
165
+ }
166
+ }
167
+ // ── applySleepToAttributes ────────────────────────────────────────────────────
168
+ /**
169
+ * Apply sleep-deprivation multipliers to a base attribute set, returning a new object.
170
+ *
171
+ * Attributes affected:
172
+ * - control.reactionTime_s, stability
173
+ * - resilience.distressTolerance
174
+ * - cognition (if present): fluid dimensions (logical, spatial, kinesthetic, musical)
175
+ *
176
+ * Immutable — does not mutate `base`.
177
+ * Pattern matches `applyAgingToAttributes` (Phase 57).
178
+ */
179
+ export function applySleepToAttributes(base, state) {
180
+ const m = deriveSleepDeprivationMuls(state);
181
+ return {
182
+ ...base,
183
+ control: {
184
+ ...base.control,
185
+ reactionTime_s: Math.max(1, Math.round(base.control.reactionTime_s * m.reactionTime_Q / SCALE.Q)),
186
+ stability: clampQ(Math.round(base.control.stability * m.stability_Q / SCALE.Q), q(0), SCALE.Q),
187
+ },
188
+ resilience: {
189
+ ...base.resilience,
190
+ distressTolerance: clampQ(Math.round(base.resilience.distressTolerance * m.distressTolerance_Q / SCALE.Q), q(0), SCALE.Q),
191
+ },
192
+ // exactOptionalPropertyTypes: spread present cognition, otherwise omit the key entirely.
193
+ ...(base.cognition
194
+ ? {
195
+ cognition: {
196
+ ...base.cognition,
197
+ logicalMathematical: clampQ(Math.round(base.cognition.logicalMathematical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
198
+ spatial: clampQ(Math.round(base.cognition.spatial * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
199
+ bodilyKinesthetic: clampQ(Math.round(base.cognition.bodilyKinesthetic * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
200
+ musical: clampQ(Math.round(base.cognition.musical * m.cognitionFluid_Q / SCALE.Q), q(0), SCALE.Q),
201
+ },
202
+ }
203
+ : {}),
204
+ };
205
+ }
206
+ // ── Entity convenience ────────────────────────────────────────────────────────
207
+ /**
208
+ * Return the entity's accumulated sleep debt in hours.
209
+ * Returns 0 if `entity.sleep` is absent.
210
+ */
211
+ export function entitySleepDebt_h(entity) {
212
+ if (!entity.sleep)
213
+ return 0;
214
+ return entity.sleep.sleepDebt_s / 3600;
215
+ }
@@ -0,0 +1,8 @@
1
+ import type { Vec3 } from "./vec3.js";
2
+ import type { WorldState } from "./world.js";
3
+ export interface SpatialIndex {
4
+ cell_m: number;
5
+ cells: Map<number, number[]>;
6
+ }
7
+ export declare function buildSpatialIndex(world: WorldState, cellSize_m: number): SpatialIndex;
8
+ export declare function queryNearbyIds(index: SpatialIndex, pos: Vec3, radius_m: number, maxCount?: number): number[];
@@ -0,0 +1,59 @@
1
+ function cellCoord(pos_m, cell_m) {
2
+ // floor division that behaves for negative coordinates too
3
+ const q = Math.trunc(pos_m / cell_m);
4
+ return pos_m < 0 && pos_m % cell_m !== 0 ? q - 1 : q;
5
+ }
6
+ function pack(cx, cy) {
7
+ // deterministic 32-bit packing (cx,cy limited by map bounds in practice)
8
+ // offset to avoid negative mixing issues
9
+ const ax = (cx & 0xffff) >>> 0;
10
+ const ay = (cy & 0xffff) >>> 0;
11
+ return ((ax << 16) | ay) >>> 0;
12
+ }
13
+ export function buildSpatialIndex(world, cellSize_m) {
14
+ const cell_m = Math.max(1, Math.trunc(cellSize_m)); // already in fixed-point metres
15
+ const cells = new Map();
16
+ for (const e of world.entities) {
17
+ if (e.injury.dead)
18
+ continue;
19
+ const cx = cellCoord(e.position_m.x, cell_m);
20
+ const cy = cellCoord(e.position_m.y, cell_m);
21
+ const key = pack(cx, cy);
22
+ let arr = cells.get(key);
23
+ if (!arr) {
24
+ arr = [];
25
+ cells.set(key, arr);
26
+ }
27
+ arr.push(e.id);
28
+ }
29
+ // deterministic: sort IDs inside each cell
30
+ for (const arr of cells.values())
31
+ arr.sort((a, b) => a - b);
32
+ return { cell_m, cells };
33
+ }
34
+ export function queryNearbyIds(index, pos, radius_m, maxCount) {
35
+ const cell_m = index.cell_m;
36
+ const r = Math.max(0, radius_m);
37
+ const cx0 = cellCoord(pos.x - r, cell_m);
38
+ const cx1 = cellCoord(pos.x + r, cell_m);
39
+ const cy0 = cellCoord(pos.y - r, cell_m);
40
+ const cy1 = cellCoord(pos.y + r, cell_m);
41
+ const out = [];
42
+ for (let cy = cy0; cy <= cy1; cy++) {
43
+ for (let cx = cx0; cx <= cx1; cx++) {
44
+ const key = pack(cx, cy);
45
+ const ids = index.cells.get(key);
46
+ if (!ids)
47
+ continue;
48
+ // already sorted
49
+ for (const id of ids) {
50
+ out.push(id);
51
+ if (maxCount !== undefined && out.length >= maxCount)
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ // deterministic overall order: sort once (cheap; neighbourhood small)
57
+ out.sort((a, b) => a - b);
58
+ return out;
59
+ }
@@ -0,0 +1,8 @@
1
+ import type { Entity } from "../entity.js";
2
+ import type { WorldState } from "../world.js";
3
+ import type { KernelContext } from "../context.js";
4
+ /**
5
+ * Per-entity per-tick regen of all capability sources.
6
+ * Called after stepMovement so velocity is current.
7
+ */
8
+ export declare function stepCapabilitySources(e: Entity, world: WorldState, ctx: KernelContext): void;