@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,637 @@
1
+ // src/inventory.ts — Phase 43: Deep Inventory & Encumbrance
2
+ //
3
+ // Container-based inventory system with nested storage, weight tracking,
4
+ // and encumbrance penalties derived from physical carrying capacity.
5
+ import { q, qMul, clampQ, SCALE, mulDiv } from "./units.js";
6
+ // Category thresholds and penalties
7
+ export const ENCUMBRANCE_CATEGORIES = [
8
+ {
9
+ category: "unencumbered",
10
+ maxRatio: 0.30,
11
+ penalties: {
12
+ fineControlPenalty_Q: q(0),
13
+ dodgeParryLatencyMul: q(1.0),
14
+ speedMul: q(1.0),
15
+ noSprint: false,
16
+ noMove: false,
17
+ },
18
+ },
19
+ {
20
+ category: "light",
21
+ maxRatio: 0.50,
22
+ penalties: {
23
+ fineControlPenalty_Q: q(0.05),
24
+ dodgeParryLatencyMul: q(1.0),
25
+ speedMul: q(0.95),
26
+ noSprint: false,
27
+ noMove: false,
28
+ },
29
+ },
30
+ {
31
+ category: "medium",
32
+ maxRatio: 0.75,
33
+ penalties: {
34
+ fineControlPenalty_Q: q(0.10),
35
+ dodgeParryLatencyMul: q(1.20),
36
+ speedMul: q(0.90),
37
+ noSprint: false,
38
+ noMove: false,
39
+ },
40
+ },
41
+ {
42
+ category: "heavy",
43
+ maxRatio: 1.00,
44
+ penalties: {
45
+ fineControlPenalty_Q: q(0.15),
46
+ dodgeParryLatencyMul: q(1.30),
47
+ speedMul: q(0.75),
48
+ noSprint: true,
49
+ noMove: false,
50
+ },
51
+ },
52
+ {
53
+ category: "overloaded",
54
+ maxRatio: Infinity,
55
+ penalties: {
56
+ fineControlPenalty_Q: q(0.30),
57
+ dodgeParryLatencyMul: q(1.50),
58
+ speedMul: q(0),
59
+ noSprint: true,
60
+ noMove: true,
61
+ },
62
+ },
63
+ ];
64
+ // ── Inventory Creation ────────────────────────────────────────────────────────
65
+ /** Create an empty inventory for an entity. */
66
+ export function createInventory(ownerId) {
67
+ return {
68
+ ownerId,
69
+ containers: [],
70
+ equipped: {
71
+ containers: new Map(),
72
+ },
73
+ encumbrance_Kg: 0,
74
+ maxEncumbrance_Kg: 0,
75
+ currency: 0,
76
+ };
77
+ }
78
+ /** Create a new container. */
79
+ export function createContainer(containerId, name, capacity_Kg, emptyMass_kg, volume_L) {
80
+ return {
81
+ containerId,
82
+ name,
83
+ capacity_Kg,
84
+ volume_L,
85
+ items: [],
86
+ isEquipped: false,
87
+ emptyMass_kg,
88
+ };
89
+ }
90
+ // ── Max Encumbrance Calculation ───────────────────────────────────────────────
91
+ /**
92
+ * Calculate maximum encumbrance based on physical strength.
93
+ * Stronger characters can carry more absolute weight.
94
+ */
95
+ export function calculateMaxEncumbrance_Kg(attributes, capacityFactor = q(0.5)) {
96
+ // Base: peakForce_N relates to lifting/carrying capacity
97
+ // A typical human (1800N peak force) can carry ~50kg at 50% factor
98
+ const peakForce_N = attributes.performance.peakForce_N;
99
+ // Convert force to mass: F = mg → m = F/g
100
+ // Using BigInt for precision: result in kg (scaled by SCALE.kg)
101
+ const numerator = BigInt(peakForce_N) * BigInt(capacityFactor) * BigInt(SCALE.kg);
102
+ const denom = BigInt(SCALE.N) * BigInt(SCALE.Q) * 9810n; // g ≈ 9.81 m/s²
103
+ const maxKg = Number(numerator / denom);
104
+ return Math.max(1, maxKg); // At least 1kg capacity
105
+ }
106
+ // ── Weight Calculations ───────────────────────────────────────────────────────
107
+ /** Calculate total weight of items in a container (including container itself). */
108
+ export function calculateContainerWeight(container) {
109
+ let total = container.emptyMass_kg;
110
+ for (const item of container.items) {
111
+ total += getItemInstanceMass(item);
112
+ }
113
+ return total;
114
+ }
115
+ /** Get mass of a single item instance (including modifications). */
116
+ export function getItemInstanceMass(item) {
117
+ // Base mass would come from template lookup; here we use a placeholder
118
+ // In practice, this would reference the equipment catalogue
119
+ let mass = item.quantity; // Simplified: assume 1kg per quantity unit
120
+ // Apply weight modifications
121
+ if (item.modifications) {
122
+ for (const mod of item.modifications) {
123
+ if (mod.statMultipliers?.weightMul) {
124
+ mass = mulDiv(mass * SCALE.Q, mod.statMultipliers.weightMul, SCALE.Q);
125
+ }
126
+ }
127
+ }
128
+ return mass;
129
+ }
130
+ /** Calculate total encumbrance for an inventory. */
131
+ export function calculateTotalEncumbrance(inventory) {
132
+ let total = 0;
133
+ // Add equipped items weight
134
+ if (inventory.equipped.mainHand) {
135
+ total += getItemInstanceMass(inventory.equipped.mainHand);
136
+ }
137
+ if (inventory.equipped.offHand) {
138
+ total += getItemInstanceMass(inventory.equipped.offHand);
139
+ }
140
+ if (inventory.equipped.body) {
141
+ total += getItemInstanceMass(inventory.equipped.body);
142
+ }
143
+ if (inventory.equipped.head) {
144
+ total += getItemInstanceMass(inventory.equipped.head);
145
+ }
146
+ // Add equipped containers and their contents
147
+ for (const container of inventory.containers) {
148
+ if (container.isEquipped) {
149
+ total += calculateContainerWeight(container);
150
+ }
151
+ }
152
+ return total;
153
+ }
154
+ /** Recalculate encumbrance and update inventory. */
155
+ export function recalculateEncumbrance(inventory, attributes) {
156
+ inventory.encumbrance_Kg = calculateTotalEncumbrance(inventory);
157
+ inventory.maxEncumbrance_Kg = calculateMaxEncumbrance_Kg(attributes);
158
+ }
159
+ // ── Encumbrance Category ──────────────────────────────────────────────────────
160
+ /** Get the encumbrance category and penalties for current load. */
161
+ export function getEncumbranceCategory(inventory) {
162
+ if (inventory.maxEncumbrance_Kg <= 0) {
163
+ return ENCUMBRANCE_CATEGORIES[4]; // Overloaded if no capacity
164
+ }
165
+ const ratio = inventory.encumbrance_Kg / inventory.maxEncumbrance_Kg;
166
+ for (const category of ENCUMBRANCE_CATEGORIES) {
167
+ if (ratio < category.maxRatio) {
168
+ return category;
169
+ }
170
+ }
171
+ return ENCUMBRANCE_CATEGORIES[ENCUMBRANCE_CATEGORIES.length - 1];
172
+ }
173
+ /** Get effective encumbrance penalties (combines with any existing penalties). */
174
+ export function getEffectiveEncumbrancePenalties(inventory, baseFineControl) {
175
+ const categoryDef = getEncumbranceCategory(inventory);
176
+ // Calculate effective fine control after penalty
177
+ const effectiveFineControl = clampQ((baseFineControl - categoryDef.penalties.fineControlPenalty_Q), q(0.01), SCALE.Q);
178
+ return {
179
+ category: categoryDef.category,
180
+ penalties: categoryDef.penalties,
181
+ effectiveFineControl,
182
+ };
183
+ }
184
+ // ── Container Operations ───────────────────────────────────────────────────────
185
+ /** Add a container to inventory. */
186
+ export function addContainer(inventory, container) {
187
+ inventory.containers.push(container);
188
+ }
189
+ /** Remove a container from inventory. */
190
+ export function removeContainer(inventory, containerId) {
191
+ const idx = inventory.containers.findIndex((c) => c.containerId === containerId);
192
+ if (idx >= 0) {
193
+ inventory.containers.splice(idx, 1);
194
+ return true;
195
+ }
196
+ return false;
197
+ }
198
+ /** Equip/unequip a container. */
199
+ export function setContainerEquipped(inventory, containerId, equipped) {
200
+ const container = inventory.containers.find((c) => c.containerId === containerId);
201
+ if (!container)
202
+ return false;
203
+ container.isEquipped = equipped;
204
+ return true;
205
+ }
206
+ /** Find a container by ID. */
207
+ export function findContainer(inventory, containerId) {
208
+ return inventory.containers.find((c) => c.containerId === containerId);
209
+ }
210
+ // ── Item Operations ────────────────────────────────────────────────────────────
211
+ /** Add an item to a specific container. */
212
+ export function addItemToContainer(container, item) {
213
+ // Check weight capacity
214
+ const itemWeight = getItemInstanceMass(item);
215
+ const currentWeight = calculateContainerWeight(container) - container.emptyMass_kg;
216
+ if (currentWeight + itemWeight > container.capacity_Kg) {
217
+ return { success: false, reason: "exceeds_capacity" };
218
+ }
219
+ container.items.push(item);
220
+ return { success: true };
221
+ }
222
+ /** Remove an item from a container by instance ID. */
223
+ export function removeItemFromContainer(container, instanceId) {
224
+ const idx = container.items.findIndex((i) => i.instanceId === instanceId);
225
+ if (idx >= 0) {
226
+ return container.items.splice(idx, 1)[0];
227
+ }
228
+ return undefined;
229
+ }
230
+ /** Find an item anywhere in the inventory. */
231
+ export function findItem(inventory, instanceId) {
232
+ // Check equipped items
233
+ const checkEquipped = (item) => {
234
+ if (item?.instanceId === instanceId) {
235
+ return { item, container: null };
236
+ }
237
+ return undefined;
238
+ };
239
+ let result = checkEquipped(inventory.equipped.mainHand);
240
+ if (result)
241
+ return result;
242
+ result = checkEquipped(inventory.equipped.offHand);
243
+ if (result)
244
+ return result;
245
+ result = checkEquipped(inventory.equipped.body);
246
+ if (result)
247
+ return result;
248
+ result = checkEquipped(inventory.equipped.head);
249
+ if (result)
250
+ return result;
251
+ // Check containers
252
+ for (const container of inventory.containers) {
253
+ const item = container.items.find((i) => i.instanceId === instanceId);
254
+ if (item) {
255
+ return { item, container };
256
+ }
257
+ }
258
+ return undefined;
259
+ }
260
+ /** Move an item between containers. */
261
+ export function moveItem(inventory, itemId, fromContainerId, // null = equipped
262
+ toContainerId) {
263
+ // Find source
264
+ let item;
265
+ let fromContainer;
266
+ if (fromContainerId === null) {
267
+ // Check equipped items - simplified, would need full equipped item removal
268
+ return { success: false, reason: "unequip_first" };
269
+ }
270
+ else {
271
+ fromContainer = findContainer(inventory, fromContainerId);
272
+ if (!fromContainer)
273
+ return { success: false, reason: "source_not_found" };
274
+ item = fromContainer.items.find((i) => i.instanceId === itemId);
275
+ }
276
+ if (!item)
277
+ return { success: false, reason: "item_not_found" };
278
+ // Find destination
279
+ const toContainer = findContainer(inventory, toContainerId);
280
+ if (!toContainer)
281
+ return { success: false, reason: "destination_not_found" };
282
+ // Check destination capacity
283
+ const itemWeight = getItemInstanceMass(item);
284
+ const destCurrentWeight = calculateContainerWeight(toContainer) - toContainer.emptyMass_kg;
285
+ if (destCurrentWeight + itemWeight > toContainer.capacity_Kg) {
286
+ return { success: false, reason: "destination_full" };
287
+ }
288
+ // Move
289
+ if (fromContainer) {
290
+ removeItemFromContainer(fromContainer, itemId);
291
+ }
292
+ toContainer.items.push(item);
293
+ return { success: true };
294
+ }
295
+ /** Equip an item from a container to an equipment slot. */
296
+ export function equipItem(inventory, itemId, slot) {
297
+ // Find item
298
+ const found = findItem(inventory, itemId);
299
+ if (!found)
300
+ return { success: false, reason: "item_not_found" };
301
+ // Remove from container if not already equipped
302
+ if (found.container) {
303
+ removeItemFromContainer(found.container, itemId);
304
+ }
305
+ // Store previous item
306
+ const previousItem = inventory.equipped[slot];
307
+ // Equip new item
308
+ inventory.equipped[slot] = found.item;
309
+ // Put previous item back in first equipped container if possible
310
+ if (previousItem) {
311
+ const equippedContainer = inventory.containers.find((c) => c.isEquipped);
312
+ if (equippedContainer) {
313
+ addItemToContainer(equippedContainer, previousItem);
314
+ }
315
+ }
316
+ return { success: true, previousItem };
317
+ }
318
+ /** Unequip an item and return it to a container. */
319
+ export function unequipItem(inventory, slot, targetContainerId) {
320
+ const item = inventory.equipped[slot];
321
+ if (!item)
322
+ return { success: false, reason: "nothing_equipped" };
323
+ // Find destination container
324
+ let container;
325
+ if (targetContainerId) {
326
+ container = findContainer(inventory, targetContainerId);
327
+ }
328
+ else {
329
+ container = inventory.containers.find((c) => c.isEquipped);
330
+ }
331
+ if (!container)
332
+ return { success: false, reason: "no_container" };
333
+ // Check capacity
334
+ const result = addItemToContainer(container, item);
335
+ if (!result.success) {
336
+ return { success: false, reason: result.reason };
337
+ }
338
+ // Remove from equipped
339
+ delete inventory.equipped[slot];
340
+ return { success: true, item };
341
+ }
342
+ // ── Item Query Functions ─────────────────────────────────────────────────────
343
+ /**
344
+ * Count items in inventory by template ID.
345
+ * Searches all containers and equipped items.
346
+ */
347
+ export function getItemCountByTemplateId(inventory, templateId) {
348
+ let count = 0;
349
+ // Check equipped items
350
+ const checkEquipped = (item) => {
351
+ if (item?.templateId === templateId) {
352
+ count += item.quantity;
353
+ }
354
+ };
355
+ checkEquipped(inventory.equipped.mainHand);
356
+ checkEquipped(inventory.equipped.offHand);
357
+ checkEquipped(inventory.equipped.body);
358
+ checkEquipped(inventory.equipped.head);
359
+ for (const item of inventory.equipped.containers.values()) {
360
+ if (item.templateId === templateId) {
361
+ count += item.quantity;
362
+ }
363
+ }
364
+ // Check containers
365
+ for (const container of inventory.containers) {
366
+ for (const item of container.items) {
367
+ if (item.templateId === templateId) {
368
+ count += item.quantity;
369
+ }
370
+ }
371
+ }
372
+ return count;
373
+ }
374
+ /**
375
+ * Find all material items of a specific material type.
376
+ * Returns an array of Material items (requires kind "material" and materialTypeId).
377
+ * Note: This assumes ItemInstance can be cast to Material if kind === "material".
378
+ * The caller must ensure the item is a material.
379
+ */
380
+ export function findMaterialsByType(inventory, materialTypeId) {
381
+ const results = [];
382
+ // Helper to check and add
383
+ const checkItem = (item) => {
384
+ // We need to examine the item's materialTypeId property.
385
+ // Since ItemInstance doesn't have materialTypeId, we need to rely on the templateId mapping
386
+ // or assume that the item is a Material (which extends ItemBase).
387
+ // For now, we'll assume that templateId indicates material type (e.g., "material_iron").
388
+ // This is a placeholder; we need to integrate with crafting material system.
389
+ if (item.templateId.startsWith("material_") && item.templateId.includes(materialTypeId)) {
390
+ results.push(item);
391
+ }
392
+ };
393
+ // Check equipped items
394
+ if (inventory.equipped.mainHand)
395
+ checkItem(inventory.equipped.mainHand);
396
+ if (inventory.equipped.offHand)
397
+ checkItem(inventory.equipped.offHand);
398
+ if (inventory.equipped.body)
399
+ checkItem(inventory.equipped.body);
400
+ if (inventory.equipped.head)
401
+ checkItem(inventory.equipped.head);
402
+ for (const item of inventory.equipped.containers.values()) {
403
+ checkItem(item);
404
+ }
405
+ // Check containers
406
+ for (const container of inventory.containers) {
407
+ for (const item of container.items) {
408
+ checkItem(item);
409
+ }
410
+ }
411
+ return results;
412
+ }
413
+ /**
414
+ * Consume items by template ID, removing them from inventory.
415
+ * Returns true if enough items were found and consumed.
416
+ */
417
+ export function consumeItemsByTemplateId(inventory, templateId, quantity) {
418
+ if (quantity <= 0)
419
+ return true;
420
+ let remaining = quantity;
421
+ // Helper to consume from an item instance (mutates item.quantity)
422
+ const consumeFromItem = (item) => {
423
+ if (item.templateId === templateId) {
424
+ const take = Math.min(item.quantity, remaining);
425
+ item.quantity -= take;
426
+ remaining -= take;
427
+ return remaining === 0;
428
+ }
429
+ return false;
430
+ };
431
+ // First, try equipped containers (they might be material items)
432
+ for (const container of inventory.containers) {
433
+ if (!container.isEquipped)
434
+ continue;
435
+ for (let i = 0; i < container.items.length; i++) {
436
+ const item = container.items[i];
437
+ if (consumeFromItem(item)) {
438
+ // Remove item if quantity zero
439
+ if (item.quantity === 0) {
440
+ container.items.splice(i, 1);
441
+ }
442
+ return true;
443
+ }
444
+ if (item.quantity === 0) {
445
+ container.items.splice(i, 1);
446
+ i--;
447
+ }
448
+ }
449
+ }
450
+ // Then try any container
451
+ for (const container of inventory.containers) {
452
+ for (let i = 0; i < container.items.length; i++) {
453
+ const item = container.items[i];
454
+ if (consumeFromItem(item)) {
455
+ if (item.quantity === 0) {
456
+ container.items.splice(i, 1);
457
+ }
458
+ return true;
459
+ }
460
+ if (item.quantity === 0) {
461
+ container.items.splice(i, 1);
462
+ i--;
463
+ }
464
+ }
465
+ }
466
+ // Finally, try equipped items (unlikely for materials)
467
+ const equippedItems = [
468
+ inventory.equipped.mainHand,
469
+ inventory.equipped.offHand,
470
+ inventory.equipped.body,
471
+ inventory.equipped.head,
472
+ ];
473
+ for (const item of equippedItems) {
474
+ if (item && consumeFromItem(item)) {
475
+ // Unequip if quantity zero? Probably not, but we can just leave it equipped with zero quantity.
476
+ // For simplicity, we'll ignore zero quantity equipped items.
477
+ return true;
478
+ }
479
+ }
480
+ return remaining === 0;
481
+ }
482
+ /**
483
+ * Add an item to inventory, attempting to place it in a suitable container.
484
+ * Prefers equipped containers with sufficient capacity.
485
+ * Returns success and the container it was added to (or null if equipped).
486
+ */
487
+ export function addItemToInventory(inventory, item) {
488
+ // Try equipped containers first
489
+ for (const container of inventory.containers) {
490
+ if (container.isEquipped) {
491
+ const result = addItemToContainer(container, item);
492
+ if (result.success) {
493
+ return { success: true, container };
494
+ }
495
+ }
496
+ }
497
+ // Try any container
498
+ for (const container of inventory.containers) {
499
+ const result = addItemToContainer(container, item);
500
+ if (result.success) {
501
+ return { success: true, container };
502
+ }
503
+ }
504
+ // No suitable container found
505
+ return { success: false, container: null, reason: "no_capacity" };
506
+ }
507
+ // ── Item Modifications ────────────────────────────────────────────────────────
508
+ /** Apply a modification to an item. */
509
+ export function applyItemMod(item, mod) {
510
+ if (!item.modifications) {
511
+ item.modifications = [];
512
+ }
513
+ item.modifications.push(mod);
514
+ }
515
+ /** Remove a modification from an item. */
516
+ export function removeItemMod(item, modType) {
517
+ if (!item.modifications)
518
+ return false;
519
+ const idx = item.modifications.findIndex((m) => m.type === modType);
520
+ if (idx >= 0) {
521
+ item.modifications.splice(idx, 1);
522
+ return true;
523
+ }
524
+ return false;
525
+ }
526
+ /** Get effective stat multiplier from all modifications. */
527
+ export function getItemStatMultiplier(item, stat) {
528
+ if (!item.modifications || item.modifications.length === 0) {
529
+ return SCALE.Q; // 1.0
530
+ }
531
+ let multiplier = SCALE.Q;
532
+ for (const mod of item.modifications) {
533
+ if (mod.statMultipliers?.[stat]) {
534
+ multiplier = qMul(multiplier, mod.statMultipliers[stat]);
535
+ }
536
+ }
537
+ return multiplier;
538
+ }
539
+ // ── Serialization ─────────────────────────────────────────────────────────────
540
+ /** Serialize inventory to JSON-friendly format. */
541
+ export function serializeInventory(inventory) {
542
+ return {
543
+ ownerId: inventory.ownerId,
544
+ containers: inventory.containers.map((c) => ({
545
+ containerId: c.containerId,
546
+ name: c.name,
547
+ capacity_Kg: c.capacity_Kg,
548
+ volume_L: c.volume_L,
549
+ items: c.items.map(serializeItemInstance),
550
+ isEquipped: c.isEquipped,
551
+ emptyMass_kg: c.emptyMass_kg,
552
+ })),
553
+ equipped: {
554
+ mainHand: inventory.equipped.mainHand
555
+ ? serializeItemInstance(inventory.equipped.mainHand)
556
+ : undefined,
557
+ offHand: inventory.equipped.offHand
558
+ ? serializeItemInstance(inventory.equipped.offHand)
559
+ : undefined,
560
+ body: inventory.equipped.body
561
+ ? serializeItemInstance(inventory.equipped.body)
562
+ : undefined,
563
+ head: inventory.equipped.head
564
+ ? serializeItemInstance(inventory.equipped.head)
565
+ : undefined,
566
+ containers: Array.from(inventory.equipped.containers.entries()).map(([slot, item]) => [slot, serializeItemInstance(item)]),
567
+ },
568
+ encumbrance_Kg: inventory.encumbrance_Kg,
569
+ maxEncumbrance_Kg: inventory.maxEncumbrance_Kg,
570
+ currency: inventory.currency,
571
+ };
572
+ }
573
+ function serializeItemInstance(item) {
574
+ return {
575
+ instanceId: item.instanceId,
576
+ templateId: item.templateId,
577
+ quantity: item.quantity,
578
+ durability_Q: item.durability_Q,
579
+ modifications: item.modifications,
580
+ containerPath: item.containerPath,
581
+ };
582
+ }
583
+ /** Deserialize inventory. */
584
+ export function deserializeInventory(data) {
585
+ const d = data;
586
+ const inventory = {
587
+ ownerId: d.ownerId ?? 0,
588
+ containers: [],
589
+ equipped: {
590
+ containers: new Map(),
591
+ },
592
+ encumbrance_Kg: d.encumbrance_Kg ?? 0,
593
+ maxEncumbrance_Kg: d.maxEncumbrance_Kg ?? 0,
594
+ currency: d.currency ?? 0,
595
+ };
596
+ if (Array.isArray(d.containers)) {
597
+ inventory.containers = d.containers.map((c) => ({
598
+ containerId: c.containerId,
599
+ name: c.name,
600
+ capacity_Kg: c.capacity_Kg,
601
+ volume_L: c.volume_L,
602
+ items: Array.isArray(c.items)
603
+ ? c.items.map(deserializeItemInstance)
604
+ : [],
605
+ isEquipped: Boolean(c.isEquipped),
606
+ emptyMass_kg: c.emptyMass_kg ?? 0,
607
+ }));
608
+ }
609
+ if (d.equipped && typeof d.equipped === "object") {
610
+ const e = d.equipped;
611
+ if (e.mainHand)
612
+ inventory.equipped.mainHand = deserializeItemInstance(e.mainHand);
613
+ if (e.offHand)
614
+ inventory.equipped.offHand = deserializeItemInstance(e.offHand);
615
+ if (e.body)
616
+ inventory.equipped.body = deserializeItemInstance(e.body);
617
+ if (e.head)
618
+ inventory.equipped.head = deserializeItemInstance(e.head);
619
+ if (Array.isArray(e.containers)) {
620
+ for (const [slot, item] of e.containers) {
621
+ inventory.equipped.containers.set(slot, deserializeItemInstance(item));
622
+ }
623
+ }
624
+ }
625
+ return inventory;
626
+ }
627
+ function deserializeItemInstance(data) {
628
+ const d = data;
629
+ return {
630
+ instanceId: d.instanceId ?? "",
631
+ templateId: d.templateId ?? "",
632
+ quantity: d.quantity ?? 1,
633
+ durability_Q: d.durability_Q,
634
+ modifications: Array.isArray(d.modifications) ? d.modifications : undefined,
635
+ containerPath: Array.isArray(d.containerPath) ? d.containerPath : [],
636
+ };
637
+ }