azeroth-shell 0.0.0-stage → 0.1.1

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 (1163) hide show
  1. package/README.md +138 -2
  2. package/bin/azsh.mjs +19 -0
  3. package/docs/aifid/eventai-texts.json +4414 -0
  4. package/docs/dungeons/atlas/atlas-rows-dungeons.json +8989 -0
  5. package/docs/dungeons/survey/pack-id-migration.json +2418 -0
  6. package/package.json +35 -4
  7. package/src/autopilot/bossSkip.ts +26 -0
  8. package/src/autopilot/combatRotation.ts +824 -0
  9. package/src/autopilot/config.ts +533 -0
  10. package/src/autopilot/consumables.ts +58 -0
  11. package/src/autopilot/encounterStand.ts +121 -0
  12. package/src/autopilot/objectGatePlan.ts +53 -0
  13. package/src/autopilot/pathEngage.ts +229 -0
  14. package/src/autopilot/plan.ts +1643 -0
  15. package/src/autopilot/questPolicy.ts +274 -0
  16. package/src/autopilot/routeStep.ts +69 -0
  17. package/src/autopilot/sessionReport.ts +88 -0
  18. package/src/autopilot/topOffPower.ts +26 -0
  19. package/src/autopilot/view.ts +476 -0
  20. package/src/cli/bootCheck.mjs +34 -0
  21. package/src/cli/bootData.mjs +57 -0
  22. package/src/cli/combatLogFilter.ts +153 -0
  23. package/src/cli/combatLogFormat.ts +613 -0
  24. package/src/cli/fileRepository.mjs +105 -0
  25. package/src/cli/gzLoader.mjs +31 -0
  26. package/src/cli/liveGauntletEscort.ts +978 -0
  27. package/src/cli/liveStratUndead.ts +305 -0
  28. package/src/cli/macroHostCombat.ts +545 -0
  29. package/src/cli/macroHostWorld.ts +330 -0
  30. package/src/cli/prefs.mjs +31 -0
  31. package/src/cli/register.mjs +4 -0
  32. package/src/cli/repl.mjs +693 -0
  33. package/src/cli/rotationText.mjs +226 -0
  34. package/src/cli/squad.mjs +105 -0
  35. package/src/cli/term.mjs +144 -0
  36. package/src/cli/text.mjs +122 -0
  37. package/src/cli/tui.mjs +626 -0
  38. package/src/cli/tuiArena.mjs +109 -0
  39. package/src/cli/tuiAutopilotSettings.mjs +93 -0
  40. package/src/cli/tuiBags.mjs +180 -0
  41. package/src/cli/tuiBars.mjs +133 -0
  42. package/src/cli/tuiBg.mjs +243 -0
  43. package/src/cli/tuiBulk.mjs +162 -0
  44. package/src/cli/tuiCheck.mjs +1882 -0
  45. package/src/cli/tuiCity.mjs +396 -0
  46. package/src/cli/tuiCompanionKit.mjs +300 -0
  47. package/src/cli/tuiCompanions.mjs +471 -0
  48. package/src/cli/tuiDev.mjs +62 -0
  49. package/src/cli/tuiDungeon.mjs +606 -0
  50. package/src/cli/tuiEnchantTarget.mjs +120 -0
  51. package/src/cli/tuiFight.mjs +577 -0
  52. package/src/cli/tuiFightPanes.mjs +228 -0
  53. package/src/cli/tuiFloor.mjs +506 -0
  54. package/src/cli/tuiGear.mjs +158 -0
  55. package/src/cli/tuiHonor.mjs +74 -0
  56. package/src/cli/tuiLoot.mjs +97 -0
  57. package/src/cli/tuiMacros.mjs +144 -0
  58. package/src/cli/tuiOutfitter.mjs +115 -0
  59. package/src/cli/tuiPet.mjs +116 -0
  60. package/src/cli/tuiProfessions.mjs +800 -0
  61. package/src/cli/tuiRaid.mjs +127 -0
  62. package/src/cli/tuiRep.mjs +65 -0
  63. package/src/cli/tuiSaves.mjs +69 -0
  64. package/src/cli/tuiSearch.mjs +363 -0
  65. package/src/cli/tuiSettings.mjs +151 -0
  66. package/src/cli/tuiSimLab.mjs +80 -0
  67. package/src/cli/tuiTalents.mjs +60 -0
  68. package/src/cli/tuiViews.mjs +1178 -0
  69. package/src/cli/tuiWiki.mjs +181 -0
  70. package/src/cli/tuiWorld.mjs +361 -0
  71. package/src/cli/wikiIndex.ts +259 -0
  72. package/src/cli/wikiSources.ts +355 -0
  73. package/src/data/alteracValley.ts +176 -0
  74. package/src/data/appearanceOptions.ts +534 -0
  75. package/src/data/applyLootChanceSanity.ts +21 -0
  76. package/src/data/createFlavor.ts +96 -0
  77. package/src/data/customItems.ts +33 -0
  78. package/src/data/dataStore.ts +1904 -0
  79. package/src/data/dungeonGameObjects.ts +84 -0
  80. package/src/data/dungeonPlateCover.ts +66 -0
  81. package/src/data/dungeonSpawns.ts +294 -0
  82. package/src/data/dungeons/alterac-valley.ts +135 -0
  83. package/src/data/dungeons/apply-dungeon-rooms.ts +29 -0
  84. package/src/data/dungeons/apply-encounter-outdoors.ts +72 -0
  85. package/src/data/dungeons/apply-instance-geometry.ts +65 -0
  86. package/src/data/dungeons/apply-spawn-placement.ts +171 -0
  87. package/src/data/dungeons/blackfathom-deeps.ts +539 -0
  88. package/src/data/dungeons/blackrock-depths.ts +1410 -0
  89. package/src/data/dungeons/blackwing-lair.ts +129 -0
  90. package/src/data/dungeons/boss-eventai.ts +8 -0
  91. package/src/data/dungeons/deadmines.ts +410 -0
  92. package/src/data/dungeons/dire-maul-east.ts +980 -0
  93. package/src/data/dungeons/dire-maul-north.ts +661 -0
  94. package/src/data/dungeons/dire-maul-west.ts +785 -0
  95. package/src/data/dungeons/dungeon-rooms.json +5479 -0
  96. package/src/data/dungeons/encounter-outdoors.json +359 -0
  97. package/src/data/dungeons/eventai/blackfathom-deeps.ts +308 -0
  98. package/src/data/dungeons/eventai/blackrock-depths.ts +1257 -0
  99. package/src/data/dungeons/eventai/deadmines.ts +264 -0
  100. package/src/data/dungeons/eventai/dire-maul-east.ts +320 -0
  101. package/src/data/dungeons/eventai/gnomeregan.ts +213 -0
  102. package/src/data/dungeons/eventai/helpers.ts +79 -0
  103. package/src/data/dungeons/eventai/index.ts +133 -0
  104. package/src/data/dungeons/eventai/lbrs.ts +372 -0
  105. package/src/data/dungeons/eventai/maraudon.ts +323 -0
  106. package/src/data/dungeons/eventai/ragefire-chasm.ts +108 -0
  107. package/src/data/dungeons/eventai/razorfen-downs.ts +207 -0
  108. package/src/data/dungeons/eventai/razorfen-kraul.ts +326 -0
  109. package/src/data/dungeons/eventai/scholomance.ts +680 -0
  110. package/src/data/dungeons/eventai/shadowfang-keep.ts +230 -0
  111. package/src/data/dungeons/eventai/sm-cathedral.ts +75 -0
  112. package/src/data/dungeons/eventai/sm-graveyard.ts +150 -0
  113. package/src/data/dungeons/eventai/sm-library.ts +55 -0
  114. package/src/data/dungeons/eventai/stratholme-undead.ts +127 -0
  115. package/src/data/dungeons/eventai/sunken-temple.ts +582 -0
  116. package/src/data/dungeons/eventai/the-stockade.ts +228 -0
  117. package/src/data/dungeons/eventai/ubrs.ts +225 -0
  118. package/src/data/dungeons/eventai/uldaman.ts +355 -0
  119. package/src/data/dungeons/eventai/wailing-caverns.ts +396 -0
  120. package/src/data/dungeons/eventai/zul-farrak.ts +591 -0
  121. package/src/data/dungeons/gnomeregan.ts +909 -0
  122. package/src/data/dungeons/index.ts +126 -0
  123. package/src/data/dungeons/instance-geometry.json +5233 -0
  124. package/src/data/dungeons/key-sources.json +169 -0
  125. package/src/data/dungeons/lbrs.ts +996 -0
  126. package/src/data/dungeons/maraudon.ts +786 -0
  127. package/src/data/dungeons/molten-core.ts +194 -0
  128. package/src/data/dungeons/naxxramas.ts +173 -0
  129. package/src/data/dungeons/onyxias-lair.ts +71 -0
  130. package/src/data/dungeons/pack-from-clusters.ts +1256 -0
  131. package/src/data/dungeons/ragefire-chasm.ts +238 -0
  132. package/src/data/dungeons/razorfen-downs.ts +698 -0
  133. package/src/data/dungeons/razorfen-kraul.ts +766 -0
  134. package/src/data/dungeons/ruins-of-ahnqiraj.ts +121 -0
  135. package/src/data/dungeons/scholomance.ts +1076 -0
  136. package/src/data/dungeons/shadowfang-keep.ts +740 -0
  137. package/src/data/dungeons/sm-armory.ts +266 -0
  138. package/src/data/dungeons/sm-cathedral.ts +430 -0
  139. package/src/data/dungeons/sm-graveyard.ts +482 -0
  140. package/src/data/dungeons/sm-library.ts +337 -0
  141. package/src/data/dungeons/stratholme-live.ts +808 -0
  142. package/src/data/dungeons/stratholme-undead.ts +1164 -0
  143. package/src/data/dungeons/sunken-temple.ts +931 -0
  144. package/src/data/dungeons/temple-of-ahnqiraj.ts +119 -0
  145. package/src/data/dungeons/the-stockade.ts +282 -0
  146. package/src/data/dungeons/ubrs.ts +1286 -0
  147. package/src/data/dungeons/uldaman.ts +685 -0
  148. package/src/data/dungeons/wailing-caverns.ts +356 -0
  149. package/src/data/dungeons/zul-farrak.ts +686 -0
  150. package/src/data/dungeons/zul-gurub.ts +142 -0
  151. package/src/data/dungeons.ts +42 -0
  152. package/src/data/fixtures.ts +446 -0
  153. package/src/data/gameobject-data.json.gz +0 -0
  154. package/src/data/item-data.json.gz +0 -0
  155. package/src/data/loot-data.json.gz +0 -0
  156. package/src/data/maps/atlas-approach-maps.json +1425 -0
  157. package/src/data/maps/atlas-landmarks.json +1711 -0
  158. package/src/data/maps/dungeon-viewports.json +392 -0
  159. package/src/data/materializeModDamagePercentTaken.ts +70 -0
  160. package/src/data/materializeModHealthRegen.ts +89 -0
  161. package/src/data/materializeSpellReputation.ts +26 -0
  162. package/src/data/pageTexts.ts +19 -0
  163. package/src/data/profession-data.json.gz +0 -0
  164. package/src/data/raid-plans/blackfathom-deeps.json +86 -0
  165. package/src/data/raid-plans/blackrock-depths.json +419 -0
  166. package/src/data/raid-plans/blackwing-lair.json +328 -0
  167. package/src/data/raid-plans/deadmines.json +55 -0
  168. package/src/data/raid-plans/dire-maul-east.json +94 -0
  169. package/src/data/raid-plans/dire-maul-north.json +70 -0
  170. package/src/data/raid-plans/dire-maul-west.json +75 -0
  171. package/src/data/raid-plans/gnomeregan.json +131 -0
  172. package/src/data/raid-plans/lbrs.json +176 -0
  173. package/src/data/raid-plans/maraudon.json +102 -0
  174. package/src/data/raid-plans/molten-core.json +169 -0
  175. package/src/data/raid-plans/naxxramas.json +354 -0
  176. package/src/data/raid-plans/onyxias-lair.json +18 -0
  177. package/src/data/raid-plans/ragefire-chasm.json +30 -0
  178. package/src/data/raid-plans/razorfen-downs.json +75 -0
  179. package/src/data/raid-plans/razorfen-kraul.json +53 -0
  180. package/src/data/raid-plans/ruins-of-ahnqiraj.json +110 -0
  181. package/src/data/raid-plans/scholomance.json +170 -0
  182. package/src/data/raid-plans/shadowfang-keep.json +87 -0
  183. package/src/data/raid-plans/sm-armory.json +53 -0
  184. package/src/data/raid-plans/sm-cathedral.json +72 -0
  185. package/src/data/raid-plans/sm-graveyard.json +63 -0
  186. package/src/data/raid-plans/sm-library.json +23 -0
  187. package/src/data/raid-plans/stratholme-live.json +129 -0
  188. package/src/data/raid-plans/stratholme-undead.json +73 -0
  189. package/src/data/raid-plans/sunken-temple.json +159 -0
  190. package/src/data/raid-plans/temple-of-ahnqiraj.json +163 -0
  191. package/src/data/raid-plans/the-stockade.json +42 -0
  192. package/src/data/raid-plans/ubrs.json +53 -0
  193. package/src/data/raid-plans/uldaman.json +131 -0
  194. package/src/data/raid-plans/wailing-caverns.json +90 -0
  195. package/src/data/raid-plans/zul-farrak.json +156 -0
  196. package/src/data/raid-plans/zul-gurub.json +307 -0
  197. package/src/data/raidInstanceCreatures.mjs +473 -0
  198. package/src/data/rotations/index.ts +152 -0
  199. package/src/data/rotations/layout.mjs +72 -0
  200. package/src/data/rotations/pve/druid/balance.json +754 -0
  201. package/src/data/rotations/pve/druid/feral-tank.json +2448 -0
  202. package/src/data/rotations/pve/druid/feral.json +2660 -0
  203. package/src/data/rotations/pve/druid/restoration.json +424 -0
  204. package/src/data/rotations/pve/hunter/beast-mastery.json +1288 -0
  205. package/src/data/rotations/pve/hunter/marksmanship.json +1260 -0
  206. package/src/data/rotations/pve/hunter/survival.json +1373 -0
  207. package/src/data/rotations/pve/mage/arcane-leveling.json +1798 -0
  208. package/src/data/rotations/pve/mage/fire-leveling.json +2003 -0
  209. package/src/data/rotations/pve/mage/frost-leveling.json +1912 -0
  210. package/src/data/rotations/pve/paladin/holy.json +755 -0
  211. package/src/data/rotations/pve/paladin/protection.json +962 -0
  212. package/src/data/rotations/pve/paladin/retribution.json +924 -0
  213. package/src/data/rotations/pve/priest/discipline.json +772 -0
  214. package/src/data/rotations/pve/priest/holy.json +707 -0
  215. package/src/data/rotations/pve/priest/shadow.json +787 -0
  216. package/src/data/rotations/pve/rogue/assassination.json +2477 -0
  217. package/src/data/rotations/pve/rogue/combat.json +2483 -0
  218. package/src/data/rotations/pve/rogue/subtlety.json +2523 -0
  219. package/src/data/rotations/pve/shaman/elemental.json +1157 -0
  220. package/src/data/rotations/pve/shaman/enhancement.json +1085 -0
  221. package/src/data/rotations/pve/shaman/restoration.json +592 -0
  222. package/src/data/rotations/pve/warlock/affliction.json +1185 -0
  223. package/src/data/rotations/pve/warlock/demonology.json +1165 -0
  224. package/src/data/rotations/pve/warlock/destruction.json +1177 -0
  225. package/src/data/rotations/pve/warrior/arms.json +3020 -0
  226. package/src/data/rotations/pve/warrior/fury.json +2996 -0
  227. package/src/data/rotations/pve/warrior/protection.json +2509 -0
  228. package/src/data/rotations/pvp/druid/balance.json +593 -0
  229. package/src/data/rotations/pvp/druid/feral.json +3276 -0
  230. package/src/data/rotations/pvp/druid/restoration.json +662 -0
  231. package/src/data/rotations/pvp/hunter/beast-mastery.json +665 -0
  232. package/src/data/rotations/pvp/hunter/marksmanship.json +2406 -0
  233. package/src/data/rotations/pvp/hunter/survival.json +635 -0
  234. package/src/data/rotations/pvp/mage/arcane.json +574 -0
  235. package/src/data/rotations/pvp/mage/fire.json +570 -0
  236. package/src/data/rotations/pvp/mage/frost.json +4048 -0
  237. package/src/data/rotations/pvp/paladin/holy.json +1498 -0
  238. package/src/data/rotations/pvp/paladin/protection.json +567 -0
  239. package/src/data/rotations/pvp/paladin/retribution.json +1110 -0
  240. package/src/data/rotations/pvp/priest/discipline.json +1848 -0
  241. package/src/data/rotations/pvp/priest/holy.json +6340 -0
  242. package/src/data/rotations/pvp/priest/shadow.json +2329 -0
  243. package/src/data/rotations/pvp/rogue/assassination.json +1322 -0
  244. package/src/data/rotations/pvp/rogue/combat.json +5258 -0
  245. package/src/data/rotations/pvp/rogue/subtlety.json +586 -0
  246. package/src/data/rotations/pvp/shaman/elemental.json +1404 -0
  247. package/src/data/rotations/pvp/shaman/enhancement.json +1277 -0
  248. package/src/data/rotations/pvp/shaman/restoration.json +2490 -0
  249. package/src/data/rotations/pvp/warlock/affliction.json +1017 -0
  250. package/src/data/rotations/pvp/warlock/demonology.json +1503 -0
  251. package/src/data/rotations/pvp/warlock/destruction.json +638 -0
  252. package/src/data/rotations/pvp/warrior/arms.json +5665 -0
  253. package/src/data/rotations/pvp/warrior/fury.json +850 -0
  254. package/src/data/rotations/pvp/warrior/protection.json +777 -0
  255. package/src/data/schema/bundle.ts +511 -0
  256. package/src/data/schema/constants.ts +49 -0
  257. package/src/data/schema/creature.ts +454 -0
  258. package/src/data/schema/encounter.ts +155 -0
  259. package/src/data/schema/faction.ts +48 -0
  260. package/src/data/schema/item.ts +297 -0
  261. package/src/data/schema/itemEnchant.ts +105 -0
  262. package/src/data/schema/npc.ts +44 -0
  263. package/src/data/schema/primitives.ts +295 -0
  264. package/src/data/schema/profession.ts +226 -0
  265. package/src/data/schema/quest.ts +154 -0
  266. package/src/data/schema/spell.ts +1149 -0
  267. package/src/data/schema/talent.ts +74 -0
  268. package/src/data/schema/world.ts +124 -0
  269. package/src/data/schema.ts +19 -0
  270. package/src/data/skinNodeGapFill.ts +68 -0
  271. package/src/data/spell-data.json.gz +0 -0
  272. package/src/data/spell-script-targets.json +1 -0
  273. package/src/data/tables/ahItemValues.json.gz +0 -0
  274. package/src/data/tables/alteracValley.json.gz +0 -0
  275. package/src/data/tables/areatriggerTeleports.json.gz +0 -0
  276. package/src/data/tables/botNames.json.gz +0 -0
  277. package/src/data/tables/botPopulation.json.gz +0 -0
  278. package/src/data/tables/botSpecs.json.gz +0 -0
  279. package/src/data/tables/classLevelStats.json.gz +0 -0
  280. package/src/data/tables/combatConditions.json.gz +0 -0
  281. package/src/data/tables/combatSkillMeta.json.gz +0 -0
  282. package/src/data/tables/conditions.json.gz +0 -0
  283. package/src/data/tables/createInfo.json.gz +0 -0
  284. package/src/data/tables/creatureAggroGroups.json.gz +0 -0
  285. package/src/data/tables/creatureClassLevelStats.json.gz +0 -0
  286. package/src/data/tables/creaturePatrolPacks.json.gz +0 -0
  287. package/src/data/tables/creatureWeapons.json.gz +0 -0
  288. package/src/data/tables/creatures.json.gz +0 -0
  289. package/src/data/tables/dbScripts.json.gz +0 -0
  290. package/src/data/tables/dungeonAuthoredRoutes.json.gz +0 -0
  291. package/src/data/tables/dungeonCensus.json.gz +0 -0
  292. package/src/data/tables/dungeonGameObjects.json.gz +0 -0
  293. package/src/data/tables/dungeonPacks.json.gz +0 -0
  294. package/src/data/tables/dungeonPlateCover.json.gz +0 -0
  295. package/src/data/tables/dungeonSpawns.json.gz +0 -0
  296. package/src/data/tables/dungeonSurvey.json.gz +0 -0
  297. package/src/data/tables/durabilityCosts.json.gz +0 -0
  298. package/src/data/tables/durabilityQuality.json.gz +0 -0
  299. package/src/data/tables/enchantApplicators.json.gz +0 -0
  300. package/src/data/tables/entranceYards.json.gz +0 -0
  301. package/src/data/tables/explorationBasexp.json.gz +0 -0
  302. package/src/data/tables/factionTemplates.json.gz +0 -0
  303. package/src/data/tables/factions.json.gz +0 -0
  304. package/src/data/tables/gameEvents.json.gz +0 -0
  305. package/src/data/tables/gameObjects.json.gz +0 -0
  306. package/src/data/tables/gossipMenus.json.gz +0 -0
  307. package/src/data/tables/graveyards.json.gz +0 -0
  308. package/src/data/tables/instanceMaps.json.gz +0 -0
  309. package/src/data/tables/itemEnchantmentTemplates.json.gz +0 -0
  310. package/src/data/tables/itemEnchants.json.gz +0 -0
  311. package/src/data/tables/itemRandomProperties.json.gz +0 -0
  312. package/src/data/tables/itemSets.json.gz +0 -0
  313. package/src/data/tables/items.json.gz +0 -0
  314. package/src/data/tables/levelStats.json.gz +0 -0
  315. package/src/data/tables/locks.json.gz +0 -0
  316. package/src/data/tables/map-pin-overrides.json.gz +0 -0
  317. package/src/data/tables/nonCombatTrash.json.gz +0 -0
  318. package/src/data/tables/npcTexts.json.gz +0 -0
  319. package/src/data/tables/pageTexts.json.gz +0 -0
  320. package/src/data/tables/petAbilityTP.json.gz +0 -0
  321. package/src/data/tables/petGenericSpells.json.gz +0 -0
  322. package/src/data/tables/petLevelStats.json.gz +0 -0
  323. package/src/data/tables/playerbot-weightscales.json.gz +0 -0
  324. package/src/data/tables/pools.json.gz +0 -0
  325. package/src/data/tables/quests.json.gz +0 -0
  326. package/src/data/tables/races.json.gz +0 -0
  327. package/src/data/tables/reputationRanks.json.gz +0 -0
  328. package/src/data/tables/reputationRates.json.gz +0 -0
  329. package/src/data/tables/reputationSpillover.json.gz +0 -0
  330. package/src/data/tables/ridingSpells.json.gz +0 -0
  331. package/src/data/tables/scriptTexts.json.gz +0 -0
  332. package/src/data/tables/soundClipVolume.json.gz +0 -0
  333. package/src/data/tables/spellChain.json.gz +0 -0
  334. package/src/data/tables/spellTeleports.json.gz +0 -0
  335. package/src/data/tables/spells.json.gz +0 -0
  336. package/src/data/tables/talents.json.gz +0 -0
  337. package/src/data/tables/tavernTriggers.json.gz +0 -0
  338. package/src/data/tables/taxiNodes.json.gz +0 -0
  339. package/src/data/tables/taxiRoutes.json.gz +0 -0
  340. package/src/data/tables/trainers.json.gz +0 -0
  341. package/src/data/tables/transports.json.gz +0 -0
  342. package/src/data/tables/travelAnchors.json.gz +0 -0
  343. package/src/data/tables/travelEdges.json.gz +0 -0
  344. package/src/data/tables/unitConditions.json.gz +0 -0
  345. package/src/data/tables/vendors.json.gz +0 -0
  346. package/src/data/tables/weaponEnchantProcs.json.gz +0 -0
  347. package/src/data/tables/xp.json.gz +0 -0
  348. package/src/data/tables/zoneWeather.json.gz +0 -0
  349. package/src/data/tables/zones.json.gz +0 -0
  350. package/src/data/world-data.ts +56 -0
  351. package/src/data/zone-music-sets.json +2751 -0
  352. package/src/dev/etl/lib/loot-chance-sanity.mjs +180 -0
  353. package/src/domain/audioQuality.mjs +145 -0
  354. package/src/domain/bg/ab.ts +433 -0
  355. package/src/domain/bg/arenaComps.ts +644 -0
  356. package/src/domain/bg/arenaLab.ts +695 -0
  357. package/src/domain/bg/arenaPace.ts +31 -0
  358. package/src/domain/bg/arenaRating.ts +110 -0
  359. package/src/domain/bg/av.ts +758 -0
  360. package/src/domain/bg/avEnemyPush.ts +219 -0
  361. package/src/domain/bg/avField.ts +283 -0
  362. package/src/domain/bg/avInstanceNodes.ts +197 -0
  363. package/src/domain/bg/avInstanceScore.ts +123 -0
  364. package/src/domain/bg/avQuestItems.ts +55 -0
  365. package/src/domain/bg/avQuests.ts +210 -0
  366. package/src/domain/bg/avSession.ts +196 -0
  367. package/src/domain/bg/bgConsumes.ts +269 -0
  368. package/src/domain/bg/bgPrebuff.ts +153 -0
  369. package/src/domain/bg/bgRest.ts +91 -0
  370. package/src/domain/bg/bgRoster.ts +305 -0
  371. package/src/domain/bg/bgTier.ts +61 -0
  372. package/src/domain/bg/bgWorld.ts +862 -0
  373. package/src/domain/bg/match.ts +215 -0
  374. package/src/domain/bg/strategies.ts +552 -0
  375. package/src/domain/bg/types.ts +291 -0
  376. package/src/domain/bg/wsg.ts +264 -0
  377. package/src/domain/bots/actionProviderSpec.ts +38 -0
  378. package/src/domain/bots/autoConsumables.ts +285 -0
  379. package/src/domain/bots/betweenPullRest.ts +239 -0
  380. package/src/domain/bots/botGear.ts +248 -0
  381. package/src/domain/bots/botInventory.ts +486 -0
  382. package/src/domain/bots/botPet.ts +535 -0
  383. package/src/domain/bots/botPool.ts +291 -0
  384. package/src/domain/bots/botProfessions.ts +334 -0
  385. package/src/domain/bots/companionActionBar.ts +209 -0
  386. package/src/domain/bots/companionHireCost.ts +236 -0
  387. package/src/domain/bots/companionMissions.ts +582 -0
  388. package/src/domain/bots/companionPrep.ts +254 -0
  389. package/src/domain/bots/companionQuests.ts +270 -0
  390. package/src/domain/bots/companionRep.ts +75 -0
  391. package/src/domain/bots/companionRotation.ts +708 -0
  392. package/src/domain/bots/companionXp.ts +66 -0
  393. package/src/domain/bots/consumableShare.ts +811 -0
  394. package/src/domain/bots/dpsMate.ts +1072 -0
  395. package/src/domain/bots/groupCompReport.ts +132 -0
  396. package/src/domain/bots/healthstoneSupply.ts +198 -0
  397. package/src/domain/bots/outfitter.ts +1056 -0
  398. package/src/domain/bots/paladinPrePull.ts +231 -0
  399. package/src/domain/bots/partyRole.ts +40 -0
  400. package/src/domain/bots/playerbotEquipCache.ts +412 -0
  401. package/src/domain/bots/playerbotItemEligibility.ts +60 -0
  402. package/src/domain/bots/playerbotStatWeight.ts +853 -0
  403. package/src/domain/bots/playerbotUnavailableItems.ts +147 -0
  404. package/src/domain/bots/prePullBuffs.ts +219 -0
  405. package/src/domain/bots/prePullStealth.ts +82 -0
  406. package/src/domain/bots/runSupply.ts +251 -0
  407. package/src/domain/bots/runSupplyPrefs.ts +28 -0
  408. package/src/domain/bots/sendTradeGoods.ts +394 -0
  409. package/src/domain/bots/soulShardCap.ts +35 -0
  410. package/src/domain/bots/squad.ts +842 -0
  411. package/src/domain/bots/squadRun.ts +81 -0
  412. package/src/domain/character/actionbar.ts +346 -0
  413. package/src/domain/character/appearance.ts +165 -0
  414. package/src/domain/character/buildCombatant.ts +1726 -0
  415. package/src/domain/character/character.ts +356 -0
  416. package/src/domain/character/characterName.ts +43 -0
  417. package/src/domain/character/characterRatings.ts +424 -0
  418. package/src/domain/character/characterSpells.ts +282 -0
  419. package/src/domain/character/characterStats.ts +1559 -0
  420. package/src/domain/character/characterTalents.ts +398 -0
  421. package/src/domain/character/combatSkills.ts +994 -0
  422. package/src/domain/character/displayForm.ts +72 -0
  423. package/src/domain/character/formTalentAuras.ts +171 -0
  424. package/src/domain/character/forms.ts +499 -0
  425. package/src/domain/character/races.ts +40 -0
  426. package/src/domain/character/resurrectionSickness.ts +49 -0
  427. package/src/domain/character/spellbook.ts +169 -0
  428. package/src/domain/character/stanceBar.ts +354 -0
  429. package/src/domain/character/stancePrereq.ts +70 -0
  430. package/src/domain/character/talentBuilds.ts +63 -0
  431. package/src/domain/character/talentImport.ts +130 -0
  432. package/src/domain/character/talentLink.ts +123 -0
  433. package/src/domain/character/talents.ts +201 -0
  434. package/src/domain/core/conditionSnapshot.ts +92 -0
  435. package/src/domain/core/hover.ts +14 -0
  436. package/src/domain/core/ids.ts +58 -0
  437. package/src/domain/core/internalNameTags.ts +5 -0
  438. package/src/domain/core/runtimeDataPatches.ts +357 -0
  439. package/src/domain/creature/aiTargeting.ts +588 -0
  440. package/src/domain/creature/combatCondition.ts +157 -0
  441. package/src/domain/creature/creatureAggro.ts +804 -0
  442. package/src/domain/creature/creatureAi.ts +206 -0
  443. package/src/domain/creature/creatureAiConstants.ts +661 -0
  444. package/src/domain/creature/creatureCastList.ts +1007 -0
  445. package/src/domain/creature/creatureEventAi.ts +1409 -0
  446. package/src/domain/creature/creatureGroups.ts +144 -0
  447. package/src/domain/creature/creatureLinking.ts +333 -0
  448. package/src/domain/creature/creatureScaling.ts +320 -0
  449. package/src/domain/creature/creatureTrigger.ts +53 -0
  450. package/src/domain/creature/creatureWander.ts +74 -0
  451. package/src/domain/creature/eventAiTexts.ts +145 -0
  452. package/src/domain/creature/faction.ts +287 -0
  453. package/src/domain/creature/forcedReaction.ts +48 -0
  454. package/src/domain/creature/outdoorHuntable.ts +74 -0
  455. package/src/domain/creature/patrolPacks.ts +210 -0
  456. package/src/domain/creature/reputation.ts +967 -0
  457. package/src/domain/creature/unitFlags.ts +74 -0
  458. package/src/domain/creature/worldBosses.ts +393 -0
  459. package/src/domain/dungeonMusic.mjs +48 -0
  460. package/src/domain/economy/auction.ts +282 -0
  461. package/src/domain/economy/auctionBot.ts +1060 -0
  462. package/src/domain/economy/honorPrice.ts +100 -0
  463. package/src/domain/economy/mail.ts +958 -0
  464. package/src/domain/economy/money.ts +42 -0
  465. package/src/domain/economy/syntheticClassTrainers.ts +62 -0
  466. package/src/domain/economy/trading.ts +296 -0
  467. package/src/domain/economy/vendorSellAll.ts +252 -0
  468. package/src/domain/economy/vendorStock.ts +146 -0
  469. package/src/domain/items/ammo.ts +817 -0
  470. package/src/domain/items/bank.ts +162 -0
  471. package/src/domain/items/consumableFamily.ts +205 -0
  472. package/src/domain/items/craftUpgrades.ts +83 -0
  473. package/src/domain/items/createItem.ts +93 -0
  474. package/src/domain/items/durability.ts +581 -0
  475. package/src/domain/items/equipment.ts +441 -0
  476. package/src/domain/items/equipmentHelper.ts +767 -0
  477. package/src/domain/items/inventory.ts +561 -0
  478. package/src/domain/items/itemBonding.ts +93 -0
  479. package/src/domain/items/itemContainer.ts +245 -0
  480. package/src/domain/items/itemDisplayName.ts +33 -0
  481. package/src/domain/items/itemInstance.ts +181 -0
  482. package/src/domain/items/itemRandomEnchantOutcomes.ts +315 -0
  483. package/src/domain/items/itemRandomProps.ts +77 -0
  484. package/src/domain/items/itemRestrictions.ts +329 -0
  485. package/src/domain/items/itemTeaches.ts +82 -0
  486. package/src/domain/items/itemUseCooldown.ts +52 -0
  487. package/src/domain/items/itemUseTarget.ts +90 -0
  488. package/src/domain/items/junkCategories.ts +174 -0
  489. package/src/domain/items/junkFilter.ts +368 -0
  490. package/src/domain/items/keyring.ts +131 -0
  491. package/src/domain/items/loot.ts +221 -0
  492. package/src/domain/items/mounts.ts +190 -0
  493. package/src/domain/items/onUsePayload.ts +102 -0
  494. package/src/domain/items/pendingLoot.ts +107 -0
  495. package/src/domain/items/relic.ts +105 -0
  496. package/src/domain/items/rocketBoots.ts +36 -0
  497. package/src/domain/items/sortContainer.ts +139 -0
  498. package/src/domain/items/spareContainers.ts +71 -0
  499. package/src/domain/items/spellReagentIndex.ts +131 -0
  500. package/src/domain/items/staleConsumables.ts +66 -0
  501. package/src/domain/items/weaponSpender.ts +110 -0
  502. package/src/domain/listDungeonIds.mjs +28 -0
  503. package/src/domain/macro/compose.ts +239 -0
  504. package/src/domain/macro/conditions.ts +260 -0
  505. package/src/domain/macro/creatureTypes.ts +40 -0
  506. package/src/domain/macro/describe.ts +169 -0
  507. package/src/domain/macro/icon.ts +60 -0
  508. package/src/domain/macro/index.ts +20 -0
  509. package/src/domain/macro/normalize.ts +112 -0
  510. package/src/domain/macro/parse.ts +316 -0
  511. package/src/domain/macro/plan.ts +288 -0
  512. package/src/domain/macro/rotation.ts +590 -0
  513. package/src/domain/macro/rotationEnv.ts +173 -0
  514. package/src/domain/macro/slots.ts +101 -0
  515. package/src/domain/macro/snapshot.ts +114 -0
  516. package/src/domain/macro/templates.ts +446 -0
  517. package/src/domain/macro/types.ts +70 -0
  518. package/src/domain/macro/units.ts +93 -0
  519. package/src/domain/macro/weaponTypes.ts +83 -0
  520. package/src/domain/nav/cityTeleports.ts +10 -0
  521. package/src/domain/nav/mounting.ts +113 -0
  522. package/src/domain/nav/nav.ts +43 -0
  523. package/src/domain/nav/navCollision.ts +606 -0
  524. package/src/domain/nav/navOutdoors.ts +91 -0
  525. package/src/domain/nav/navRegistry.ts +179 -0
  526. package/src/domain/nav/navRoomGraph.ts +302 -0
  527. package/src/domain/nav/navTypes.ts +43 -0
  528. package/src/domain/nav/navmesh.ts +1715 -0
  529. package/src/domain/nav/speed.ts +200 -0
  530. package/src/domain/nav/storey.ts +10 -0
  531. package/src/domain/nav/travel.ts +725 -0
  532. package/src/domain/nav/travelTypes.ts +75 -0
  533. package/src/domain/pet/pet.ts +1150 -0
  534. package/src/domain/pet/petActionBar.ts +180 -0
  535. package/src/domain/pet/petSpellbook.ts +39 -0
  536. package/src/domain/pet/petStable.ts +81 -0
  537. package/src/domain/professions/autoDisenchant.ts +236 -0
  538. package/src/domain/professions/characterEnchants.ts +176 -0
  539. package/src/domain/professions/craftCooldown.ts +52 -0
  540. package/src/domain/professions/enchantApply.ts +62 -0
  541. package/src/domain/professions/enchantTargets.ts +114 -0
  542. package/src/domain/professions/enchanting.ts +236 -0
  543. package/src/domain/professions/gatherFilter.ts +149 -0
  544. package/src/domain/professions/gearEnchant.ts +137 -0
  545. package/src/domain/professions/heldTools.ts +29 -0
  546. package/src/domain/professions/learnAllRecipes.ts +146 -0
  547. package/src/domain/professions/miningHarvest.ts +69 -0
  548. package/src/domain/professions/partyDisenchant.ts +294 -0
  549. package/src/domain/professions/partyWork.ts +83 -0
  550. package/src/domain/professions/pickpocket.ts +172 -0
  551. package/src/domain/professions/poisons.ts +240 -0
  552. package/src/domain/professions/professions.ts +677 -0
  553. package/src/domain/professions/rareNodes.ts +45 -0
  554. package/src/domain/professions/skinLoot.ts +27 -0
  555. package/src/domain/professions/vendorReagents.ts +152 -0
  556. package/src/domain/professions/weaponImbueHand.ts +171 -0
  557. package/src/domain/questFarmFilter.mjs +42 -0
  558. package/src/domain/quests/doQuest.ts +1204 -0
  559. package/src/domain/quests/gossip.ts +308 -0
  560. package/src/domain/quests/gossipTalk.ts +298 -0
  561. package/src/domain/quests/interpreter/canComplete.ts +150 -0
  562. package/src/domain/quests/interpreter/claim.ts +137 -0
  563. package/src/domain/quests/interpreter/dbscript/handlers.ts +1371 -0
  564. package/src/domain/quests/interpreter/dbscript/handlersBatch.ts +1251 -0
  565. package/src/domain/quests/interpreter/dbscript/hooks.ts +331 -0
  566. package/src/domain/quests/interpreter/dbscript/index.ts +212 -0
  567. package/src/domain/quests/interpreter/dbscript/opcodes.ts +257 -0
  568. package/src/domain/quests/interpreter/dbscript/registry.ts +1194 -0
  569. package/src/domain/quests/interpreter/dbscript/registryAreatriggerScripts.ts +539 -0
  570. package/src/domain/quests/interpreter/dbscript/registryAshenvale.ts +553 -0
  571. package/src/domain/quests/interpreter/dbscript/registryAzshara.ts +272 -0
  572. package/src/domain/quests/interpreter/dbscript/registryDarkshore.ts +637 -0
  573. package/src/domain/quests/interpreter/dbscript/registryDesolace.ts +501 -0
  574. package/src/domain/quests/interpreter/dbscript/registryDustwallowMarsh.ts +735 -0
  575. package/src/domain/quests/interpreter/dbscript/registryFelwood.ts +493 -0
  576. package/src/domain/quests/interpreter/dbscript/registryFeralas.ts +485 -0
  577. package/src/domain/quests/interpreter/dbscript/registryGoScripts.ts +203 -0
  578. package/src/domain/quests/interpreter/dbscript/registryMoonglade.ts +288 -0
  579. package/src/domain/quests/interpreter/dbscript/registryNpcProfessions.ts +230 -0
  580. package/src/domain/quests/interpreter/dbscript/registryNpcsSpecial.ts +545 -0
  581. package/src/domain/quests/interpreter/dbscript/registryRazorfenDowns.ts +293 -0
  582. package/src/domain/quests/interpreter/dbscript/registryRazorfenKraul.ts +221 -0
  583. package/src/domain/quests/interpreter/dbscript/registryRedridge.ts +112 -0
  584. package/src/domain/quests/interpreter/dbscript/registrySearingGorge.ts +125 -0
  585. package/src/domain/quests/interpreter/dbscript/registrySiblings.ts +51 -0
  586. package/src/domain/quests/interpreter/dbscript/registrySilverpineForest.ts +172 -0
  587. package/src/domain/quests/interpreter/dbscript/registryStonetalonMountains.ts +180 -0
  588. package/src/domain/quests/interpreter/dbscript/registryStormwindCity.ts +369 -0
  589. package/src/domain/quests/interpreter/dbscript/registryStranglethornVale.ts +115 -0
  590. package/src/domain/quests/interpreter/dbscript/registryStratholme.ts +146 -0
  591. package/src/domain/quests/interpreter/dbscript/registrySunkenTemple.ts +149 -0
  592. package/src/domain/quests/interpreter/dbscript/registrySwampOfSorrows.ts +163 -0
  593. package/src/domain/quests/interpreter/dbscript/registryTanaris.ts +318 -0
  594. package/src/domain/quests/interpreter/dbscript/registryTeldrassil.ts +120 -0
  595. package/src/domain/quests/interpreter/dbscript/registryTheBarrens.ts +603 -0
  596. package/src/domain/quests/interpreter/dbscript/registryThousandNeedles.ts +499 -0
  597. package/src/domain/quests/interpreter/dbscript/registryTirisfalGlades.ts +222 -0
  598. package/src/domain/quests/interpreter/dbscript/registryUngoroCrater.ts +291 -0
  599. package/src/domain/quests/interpreter/dbscript/registryWarEffort.ts +773 -0
  600. package/src/domain/quests/interpreter/dbscript/registryWesternPlaguelands.ts +206 -0
  601. package/src/domain/quests/interpreter/dbscript/registryWestfall.ts +190 -0
  602. package/src/domain/quests/interpreter/dbscript/registryWetlands.ts +186 -0
  603. package/src/domain/quests/interpreter/dbscript/registryWinterspring.ts +235 -0
  604. package/src/domain/quests/interpreter/dbscript/types.ts +817 -0
  605. package/src/domain/quests/interpreter/dbscript/vm.ts +171 -0
  606. package/src/domain/quests/interpreter/diff.ts +349 -0
  607. package/src/domain/quests/interpreter/fixtures/oracle-questlog-qi2.json +2417 -0
  608. package/src/domain/quests/interpreter/flags.ts +101 -0
  609. package/src/domain/quests/interpreter/index.ts +113 -0
  610. package/src/domain/quests/interpreter/live.ts +178 -0
  611. package/src/domain/quests/interpreter/types.ts +81 -0
  612. package/src/domain/quests/interpreter/updaters.ts +412 -0
  613. package/src/domain/quests/questCorpseUse.ts +155 -0
  614. package/src/domain/quests/questDialog.ts +43 -0
  615. package/src/domain/quests/questFight.ts +34 -0
  616. package/src/domain/quests/questObjectiveLabel.ts +39 -0
  617. package/src/domain/quests/questRewardChoice.ts +85 -0
  618. package/src/domain/quests/questScriptCredit.ts +26 -0
  619. package/src/domain/quests/questScriptItemUse.ts +74 -0
  620. package/src/domain/quests/questTameRods.ts +79 -0
  621. package/src/domain/quests/questlog.ts +1545 -0
  622. package/src/domain/quests/talkDeliver.ts +170 -0
  623. package/src/domain/raid/globalActionBar.ts +195 -0
  624. package/src/domain/raid/instanceRoster.ts +194 -0
  625. package/src/domain/raid/partySummon.ts +198 -0
  626. package/src/domain/raid/partyVault.ts +320 -0
  627. package/src/domain/raid/raidBossAbilities.ts +268 -0
  628. package/src/domain/raid/raidComp.ts +335 -0
  629. package/src/domain/raid/raidCustomRows.ts +453 -0
  630. package/src/domain/raid/raidEncounterGear.ts +298 -0
  631. package/src/domain/raid/raidEncounterTactics.ts +412 -0
  632. package/src/domain/raid/raidPlan.ts +509 -0
  633. package/src/domain/raid/raidPlanEdits.ts +98 -0
  634. package/src/domain/raid/raidPlanPacks.ts +480 -0
  635. package/src/domain/raid/raidRecovery.ts +183 -0
  636. package/src/domain/raid/raidRosterTemplate.ts +138 -0
  637. package/src/domain/rotation/auraSide.ts +68 -0
  638. package/src/domain/rotation/build.ts +3031 -0
  639. package/src/domain/rotation/classes/druid.ts +58 -0
  640. package/src/domain/rotation/classes/hooks.ts +59 -0
  641. package/src/domain/rotation/classes/hunter.ts +156 -0
  642. package/src/domain/rotation/classes/index.ts +104 -0
  643. package/src/domain/rotation/classes/mage.ts +99 -0
  644. package/src/domain/rotation/classes/paladin.ts +296 -0
  645. package/src/domain/rotation/classes/priest.ts +94 -0
  646. package/src/domain/rotation/classes/pullOpener.ts +92 -0
  647. package/src/domain/rotation/classes/rogue.ts +69 -0
  648. package/src/domain/rotation/classes/shaman.ts +29 -0
  649. package/src/domain/rotation/classes/supportSpec.ts +94 -0
  650. package/src/domain/rotation/classes/warlock.ts +81 -0
  651. package/src/domain/rotation/classes/warrior.ts +402 -0
  652. package/src/domain/rotation/classify.ts +1381 -0
  653. package/src/domain/rotation/condMetricPicker.ts +379 -0
  654. package/src/domain/rotation/consumables.ts +242 -0
  655. package/src/domain/rotation/evaluate.ts +1115 -0
  656. package/src/domain/rotation/finisherLadder.ts +145 -0
  657. package/src/domain/rotation/invent.ts +938 -0
  658. package/src/domain/rotation/killOrder.ts +30 -0
  659. package/src/domain/rotation/openerForm.ts +22 -0
  660. package/src/domain/rotation/paladinBuffs.ts +568 -0
  661. package/src/domain/rotation/prebuff.ts +283 -0
  662. package/src/domain/rotation/raidRowTuning.ts +34 -0
  663. package/src/domain/rotation/rotation.ts +11 -0
  664. package/src/domain/rotation/rotationProfiles.ts +180 -0
  665. package/src/domain/rotation/rotationSuggestions.ts +851 -0
  666. package/src/domain/rotation/ruleAfterChain.ts +49 -0
  667. package/src/domain/rotation/spec.ts +70 -0
  668. package/src/domain/rotation/spellPicker.ts +140 -0
  669. package/src/domain/rotation/stand.ts +143 -0
  670. package/src/domain/rotation/types.ts +718 -0
  671. package/src/domain/run/battlegroundRun.ts +111 -0
  672. package/src/domain/run/canEnterContext.ts +32 -0
  673. package/src/domain/run/death.ts +561 -0
  674. package/src/domain/run/deathRecovery.ts +289 -0
  675. package/src/domain/run/duel.ts +141 -0
  676. package/src/domain/run/dungeonRun.ts +344 -0
  677. package/src/domain/run/floorRest.ts +159 -0
  678. package/src/domain/run/honor.ts +557 -0
  679. package/src/domain/run/honorWeek.ts +180 -0
  680. package/src/domain/run/whenMap.ts +977 -0
  681. package/src/domain/scriptdev/arathiHighlands.ts +445 -0
  682. package/src/domain/scriptdev/ashenvale.ts +635 -0
  683. package/src/domain/scriptdev/ashenvaleHelpers.ts +1215 -0
  684. package/src/domain/scriptdev/azshara.ts +892 -0
  685. package/src/domain/scriptdev/blackfathomDeeps.ts +212 -0
  686. package/src/domain/scriptdev/blackrockDepths.ts +1434 -0
  687. package/src/domain/scriptdev/blackrockSpire.ts +730 -0
  688. package/src/domain/scriptdev/blackwingLair.ts +521 -0
  689. package/src/domain/scriptdev/burningSteppes.ts +62 -0
  690. package/src/domain/scriptdev/classFilter.ts +43 -0
  691. package/src/domain/scriptdev/darkshore.ts +817 -0
  692. package/src/domain/scriptdev/darkshoreHelpers.ts +853 -0
  693. package/src/domain/scriptdev/deadmines.ts +331 -0
  694. package/src/domain/scriptdev/desolace.ts +1239 -0
  695. package/src/domain/scriptdev/direMaul.ts +859 -0
  696. package/src/domain/scriptdev/durotar.ts +532 -0
  697. package/src/domain/scriptdev/dustwallowMarsh.ts +758 -0
  698. package/src/domain/scriptdev/dustwallowMarshHelpers.ts +994 -0
  699. package/src/domain/scriptdev/easternPlaguelands.ts +21 -0
  700. package/src/domain/scriptdev/felwood.ts +1055 -0
  701. package/src/domain/scriptdev/feralas.ts +571 -0
  702. package/src/domain/scriptdev/feralasHelpers.ts +838 -0
  703. package/src/domain/scriptdev/gnomeregan.ts +466 -0
  704. package/src/domain/scriptdev/guards.ts +616 -0
  705. package/src/domain/scriptdev/hinterlands.ts +693 -0
  706. package/src/domain/scriptdev/host.ts +1134 -0
  707. package/src/domain/scriptdev/index.ts +123 -0
  708. package/src/domain/scriptdev/instanceRaids.ts +136 -0
  709. package/src/domain/scriptdev/itemScripts.ts +37 -0
  710. package/src/domain/scriptdev/lochModan.ts +283 -0
  711. package/src/domain/scriptdev/maraudon.ts +232 -0
  712. package/src/domain/scriptdev/moltenCore.ts +831 -0
  713. package/src/domain/scriptdev/moonglade.ts +659 -0
  714. package/src/domain/scriptdev/moongladeHelpers.ts +933 -0
  715. package/src/domain/scriptdev/naxxramas.ts +577 -0
  716. package/src/domain/scriptdev/naxxramasConstruct.ts +364 -0
  717. package/src/domain/scriptdev/naxxramasFrostwyrm.ts +378 -0
  718. package/src/domain/scriptdev/naxxramasMilitary.ts +276 -0
  719. package/src/domain/scriptdev/npcProfessions.ts +1025 -0
  720. package/src/domain/scriptdev/npcsSpecial.ts +1379 -0
  721. package/src/domain/scriptdev/onyxiasLair.ts +313 -0
  722. package/src/domain/scriptdev/orgrimmar.ts +147 -0
  723. package/src/domain/scriptdev/professionSpecConsumer.ts +481 -0
  724. package/src/domain/scriptdev/razorfenDowns.ts +468 -0
  725. package/src/domain/scriptdev/razorfenKraul.ts +410 -0
  726. package/src/domain/scriptdev/redridgeMountains.ts +163 -0
  727. package/src/domain/scriptdev/registry.ts +945 -0
  728. package/src/domain/scriptdev/ruinsOfAhnqiraj.ts +356 -0
  729. package/src/domain/scriptdev/scarletMonastery.ts +713 -0
  730. package/src/domain/scriptdev/scholomance.ts +682 -0
  731. package/src/domain/scriptdev/searingGorge.ts +295 -0
  732. package/src/domain/scriptdev/shadowfangKeep.ts +181 -0
  733. package/src/domain/scriptdev/silithus.ts +71 -0
  734. package/src/domain/scriptdev/silverpineForest.ts +662 -0
  735. package/src/domain/scriptdev/stonetalonMountains.ts +493 -0
  736. package/src/domain/scriptdev/stormwindCity.ts +1192 -0
  737. package/src/domain/scriptdev/stranglethornVale.ts +402 -0
  738. package/src/domain/scriptdev/stratholme.ts +650 -0
  739. package/src/domain/scriptdev/stratholmeScripts.ts +505 -0
  740. package/src/domain/scriptdev/sunkenTemple.ts +736 -0
  741. package/src/domain/scriptdev/swampOfSorrows.ts +22 -0
  742. package/src/domain/scriptdev/tanaris.ts +391 -0
  743. package/src/domain/scriptdev/tanarisHelpers.ts +661 -0
  744. package/src/domain/scriptdev/teldrassil.ts +143 -0
  745. package/src/domain/scriptdev/templeOfAhnqiraj.ts +1013 -0
  746. package/src/domain/scriptdev/theBarrens.ts +709 -0
  747. package/src/domain/scriptdev/theBarrensHelpers.ts +1026 -0
  748. package/src/domain/scriptdev/thousandNeedles.ts +426 -0
  749. package/src/domain/scriptdev/thousandNeedlesHelpers.ts +566 -0
  750. package/src/domain/scriptdev/tirisfalGlades.ts +533 -0
  751. package/src/domain/scriptdev/types.ts +1026 -0
  752. package/src/domain/scriptdev/uldaman.ts +437 -0
  753. package/src/domain/scriptdev/ungoroCrater.ts +549 -0
  754. package/src/domain/scriptdev/ungoroCraterHelpers.ts +1006 -0
  755. package/src/domain/scriptdev/wailingCaverns.ts +700 -0
  756. package/src/domain/scriptdev/warEffort.ts +371 -0
  757. package/src/domain/scriptdev/westernPlaguelands.ts +952 -0
  758. package/src/domain/scriptdev/westfall.ts +767 -0
  759. package/src/domain/scriptdev/wetlands.ts +660 -0
  760. package/src/domain/scriptdev/winterspring.ts +482 -0
  761. package/src/domain/scriptdev/winterspringHelpers.ts +801 -0
  762. package/src/domain/scriptdev/worldBosses.ts +605 -0
  763. package/src/domain/scriptdev/zulfarrak.ts +478 -0
  764. package/src/domain/scriptdev/zulgurub.ts +1152 -0
  765. package/src/domain/sim/dummySim.ts +408 -0
  766. package/src/domain/sim/targetDummy.ts +29 -0
  767. package/src/domain/social/guild.ts +148 -0
  768. package/src/domain/spells/areaAuraMembership.ts +93 -0
  769. package/src/domain/spells/breakableStunCc.ts +166 -0
  770. package/src/domain/spells/cancelAura.ts +80 -0
  771. package/src/domain/spells/diminishing.ts +167 -0
  772. package/src/domain/spells/dispel.ts +149 -0
  773. package/src/domain/spells/dispelMechanic.ts +105 -0
  774. package/src/domain/spells/equipSpells.ts +247 -0
  775. package/src/domain/spells/learnOnTargets.ts +260 -0
  776. package/src/domain/spells/mergeSchoolImmunityFromRaw.ts +66 -0
  777. package/src/domain/spells/resurrectSpell.ts +124 -0
  778. package/src/domain/spells/spellPushback.ts +39 -0
  779. package/src/domain/spells/spellReflect.ts +43 -0
  780. package/src/domain/spells/stealth.ts +183 -0
  781. package/src/domain/spells/totem.ts +394 -0
  782. package/src/domain/spells/worldCast.ts +187 -0
  783. package/src/domain/world/areaTriggerLocks.ts +177 -0
  784. package/src/domain/world/dungeonMusic.ts +108 -0
  785. package/src/domain/world/gameEvents.ts +119 -0
  786. package/src/domain/world/gameObjects.ts +1030 -0
  787. package/src/domain/world/goEventFire.ts +234 -0
  788. package/src/domain/world/goMoTransport.ts +74 -0
  789. package/src/domain/world/goTransport.ts +81 -0
  790. package/src/domain/world/hazards.ts +459 -0
  791. package/src/domain/world/openLock.ts +398 -0
  792. package/src/domain/world/pools.ts +470 -0
  793. package/src/domain/world/restArea.ts +37 -0
  794. package/src/domain/world/weather.ts +201 -0
  795. package/src/domain/zoneAmbience.mjs +87 -0
  796. package/src/domain/zoneMusic.mjs +62 -0
  797. package/src/domain/zoneMusicQueue.mjs +7 -0
  798. package/src/engine/combat/activeSeal.ts +39 -0
  799. package/src/engine/combat/aiTargetResolve.ts +64 -0
  800. package/src/engine/combat/aoeCast.ts +289 -0
  801. package/src/engine/combat/applyConsumeAura.ts +74 -0
  802. package/src/engine/combat/applyDemonicSacrifice.ts +70 -0
  803. package/src/engine/combat/applyDispelMechanic.ts +30 -0
  804. package/src/engine/combat/applyHealEffect.ts +301 -0
  805. package/src/engine/combat/applySchoolAbsorb.ts +114 -0
  806. package/src/engine/combat/applySpell.ts +3169 -0
  807. package/src/engine/combat/applySpellLate.ts +2026 -0
  808. package/src/engine/combat/applySpellLateBag.ts +144 -0
  809. package/src/engine/combat/areaAura.ts +283 -0
  810. package/src/engine/combat/attackTable.ts +508 -0
  811. package/src/engine/combat/battleRez.ts +87 -0
  812. package/src/engine/combat/castAction.ts +36 -0
  813. package/src/engine/combat/castTargetGates.ts +75 -0
  814. package/src/engine/combat/castableKit.ts +68 -0
  815. package/src/engine/combat/ccMirrorUpgrade.ts +39 -0
  816. package/src/engine/combat/chainFan.ts +210 -0
  817. package/src/engine/combat/channel.ts +423 -0
  818. package/src/engine/combat/chaseCloseness.ts +95 -0
  819. package/src/engine/combat/cmangosCastGates.ts +129 -0
  820. package/src/engine/combat/combatFlag.ts +77 -0
  821. package/src/engine/combat/combatGeometry.ts +1329 -0
  822. package/src/engine/combat/combustion.ts +68 -0
  823. package/src/engine/combat/context.ts +730 -0
  824. package/src/engine/combat/createSession.ts +1165 -0
  825. package/src/engine/combat/creatureLeash.ts +38 -0
  826. package/src/engine/combat/crowdControl.ts +1259 -0
  827. package/src/engine/combat/dispelDuty.ts +54 -0
  828. package/src/engine/combat/dispelReflect.ts +609 -0
  829. package/src/engine/combat/displaceFloor.ts +113 -0
  830. package/src/engine/combat/duelBounds.ts +47 -0
  831. package/src/engine/combat/equippedItemGate.ts +297 -0
  832. package/src/engine/combat/evadeLifecycle.ts +63 -0
  833. package/src/engine/combat/eventAi.ts +2220 -0
  834. package/src/engine/combat/facing.ts +74 -0
  835. package/src/engine/combat/fearFlee.ts +137 -0
  836. package/src/engine/combat/fighterFactory.ts +1144 -0
  837. package/src/engine/combat/forceDespawnAction.ts +51 -0
  838. package/src/engine/combat/formulas.ts +807 -0
  839. package/src/engine/combat/friendlyFan.ts +55 -0
  840. package/src/engine/combat/groundPoolTick.ts +210 -0
  841. package/src/engine/combat/hazards.ts +114 -0
  842. package/src/engine/combat/healTargets.ts +226 -0
  843. package/src/engine/combat/heartbeatResist.ts +49 -0
  844. package/src/engine/combat/hostileItemUse.ts +75 -0
  845. package/src/engine/combat/interruptAuras.ts +66 -0
  846. package/src/engine/combat/itemRescue.ts +49 -0
  847. package/src/engine/combat/itemUse.ts +260 -0
  848. package/src/engine/combat/kitById.ts +23 -0
  849. package/src/engine/combat/knockBack.ts +201 -0
  850. package/src/engine/combat/leap.ts +89 -0
  851. package/src/engine/combat/mitigation.ts +587 -0
  852. package/src/engine/combat/modTaunt.ts +43 -0
  853. package/src/engine/combat/motionClaim.ts +268 -0
  854. package/src/engine/combat/motionGate.ts +150 -0
  855. package/src/engine/combat/motionGenerator.ts +27 -0
  856. package/src/engine/combat/navSight.ts +138 -0
  857. package/src/engine/combat/perfSink.ts +28 -0
  858. package/src/engine/combat/periodicDue.ts +55 -0
  859. package/src/engine/combat/petAuras.ts +86 -0
  860. package/src/engine/combat/pickAction.ts +3449 -0
  861. package/src/engine/combat/powerForm.ts +735 -0
  862. package/src/engine/combat/powerPools.ts +49 -0
  863. package/src/engine/combat/predicates.ts +538 -0
  864. package/src/engine/combat/procsDamage.ts +1823 -0
  865. package/src/engine/combat/reactiveGates.ts +135 -0
  866. package/src/engine/combat/roles.ts +389 -0
  867. package/src/engine/combat/rotationSubject.ts +78 -0
  868. package/src/engine/combat/ruleAfter.ts +106 -0
  869. package/src/engine/combat/seedDisplayAuras.ts +203 -0
  870. package/src/engine/combat/seedOocEffects.ts +317 -0
  871. package/src/engine/combat/seedSchoolImmunities.ts +45 -0
  872. package/src/engine/combat/session-types.ts +458 -0
  873. package/src/engine/combat/sessionApi.ts +1785 -0
  874. package/src/engine/combat/sessionCharge.ts +124 -0
  875. package/src/engine/combat/sessionChase.ts +869 -0
  876. package/src/engine/combat/sessionDeaths.ts +469 -0
  877. package/src/engine/combat/sessionFocus.ts +506 -0
  878. package/src/engine/combat/sessionFollow.ts +167 -0
  879. package/src/engine/combat/sessionGeometry.ts +188 -0
  880. package/src/engine/combat/sessionHome.ts +75 -0
  881. package/src/engine/combat/sessionInject.ts +719 -0
  882. package/src/engine/combat/sessionMechanicMove.ts +555 -0
  883. package/src/engine/combat/sessionMotion.ts +1553 -0
  884. package/src/engine/combat/sessionMotionKit.ts +247 -0
  885. package/src/engine/combat/sessionMoveIntent.ts +49 -0
  886. package/src/engine/combat/sessionMoveOrder.ts +200 -0
  887. package/src/engine/combat/sessionPathTo.ts +657 -0
  888. package/src/engine/combat/sessionPools.ts +688 -0
  889. package/src/engine/combat/sessionSteer.ts +144 -0
  890. package/src/engine/combat/sessionStepBack.ts +151 -0
  891. package/src/engine/combat/sessionStepWrap.ts +116 -0
  892. package/src/engine/combat/sessionView.ts +442 -0
  893. package/src/engine/combat/simEffects.ts +32 -0
  894. package/src/engine/combat/simFacade.ts +620 -0
  895. package/src/engine/combat/simPump.ts +264 -0
  896. package/src/engine/combat/simSnapshot.ts +165 -0
  897. package/src/engine/combat/simThreadPool.ts +35 -0
  898. package/src/engine/combat/skeramImages.ts +57 -0
  899. package/src/engine/combat/slowUpdate.ts +178 -0
  900. package/src/engine/combat/snapToFloor.ts +42 -0
  901. package/src/engine/combat/speedAura.ts +29 -0
  902. package/src/engine/combat/spellReagents.ts +71 -0
  903. package/src/engine/combat/stealthDetect.ts +93 -0
  904. package/src/engine/combat/stepBackMotion.ts +171 -0
  905. package/src/engine/combat/stepSession.ts +3102 -0
  906. package/src/engine/combat/summonCast.ts +243 -0
  907. package/src/engine/combat/thaddiusPolarity.ts +97 -0
  908. package/src/engine/combat/threat.ts +388 -0
  909. package/src/engine/combat/threatOps.ts +272 -0
  910. package/src/engine/combat/tickWorkerBody.ts +211 -0
  911. package/src/engine/combat/totemAuras.ts +122 -0
  912. package/src/engine/combat/triggerPayload.ts +42 -0
  913. package/src/engine/combat/triggeredCompanion.ts +63 -0
  914. package/src/engine/combat/types.ts +1897 -0
  915. package/src/engine/combat/useObjectTrap.ts +140 -0
  916. package/src/engine/combat/walkSurface.ts +54 -0
  917. package/src/engine/combat/weapon.ts +1248 -0
  918. package/src/engine/combat/worldFrame.ts +43 -0
  919. package/src/engine/combat.solo-golden.json +2242 -0
  920. package/src/engine/combat.ts +84 -0
  921. package/src/engine/core/detMath.ts +289 -0
  922. package/src/engine/core/events.ts +357 -0
  923. package/src/engine/core/rng.ts +36 -0
  924. package/src/engine/game/actionBars.ts +124 -0
  925. package/src/engine/game/activateObjectFromSpell.ts +320 -0
  926. package/src/engine/game/applyActiveObjectFromDbScript.ts +98 -0
  927. package/src/engine/game/applyDeathAndSpellScripts.ts +268 -0
  928. package/src/engine/game/applyDoorFromDbScript.ts +388 -0
  929. package/src/engine/game/applyEscortFromDbScript.ts +290 -0
  930. package/src/engine/game/applyGoSpellcasterCast.ts +40 -0
  931. package/src/engine/game/applyHoverFromDbScript.ts +110 -0
  932. package/src/engine/game/applyOocResurrect.ts +71 -0
  933. package/src/engine/game/applySpellDurability.ts +115 -0
  934. package/src/engine/game/applyTailFromDbScript.ts +478 -0
  935. package/src/engine/game/applyTerminateFromDbScript.ts +95 -0
  936. package/src/engine/game/auctionMail.ts +269 -0
  937. package/src/engine/game/bagOverflow.ts +130 -0
  938. package/src/engine/game/bank.ts +110 -0
  939. package/src/engine/game/barAutofill.ts +26 -0
  940. package/src/engine/game/battleground.ts +369 -0
  941. package/src/engine/game/combatEnemySpec.ts +216 -0
  942. package/src/engine/game/combatObjectTraps.ts +167 -0
  943. package/src/engine/game/combatSeed.ts +320 -0
  944. package/src/engine/game/combatSession.ts +3241 -0
  945. package/src/engine/game/combatSessionClock.ts +62 -0
  946. package/src/engine/game/companionQuestActions.ts +242 -0
  947. package/src/engine/game/companionTraining.ts +40 -0
  948. package/src/engine/game/consumables.ts +624 -0
  949. package/src/engine/game/context.ts +53 -0
  950. package/src/engine/game/createHealthstone.ts +45 -0
  951. package/src/engine/game/deathFlow.ts +127 -0
  952. package/src/engine/game/devGrant.ts +117 -0
  953. package/src/engine/game/dynamicObject.ts +365 -0
  954. package/src/engine/game/enchantItem.ts +177 -0
  955. package/src/engine/game/equipmentHelper.ts +806 -0
  956. package/src/engine/game/factionState.ts +108 -0
  957. package/src/engine/game/garmentsOoc.ts +69 -0
  958. package/src/engine/game/globalActionBar.ts +179 -0
  959. package/src/engine/game/goInteractQuestCredit.ts +51 -0
  960. package/src/engine/game/goMoTransportUse.ts +61 -0
  961. package/src/engine/game/goTransportUse.ts +60 -0
  962. package/src/engine/game/healthstoneUpkeep.ts +59 -0
  963. package/src/engine/game/killSkin.ts +124 -0
  964. package/src/engine/game/learnFromItem.ts +118 -0
  965. package/src/engine/game/loadReconcile.ts +551 -0
  966. package/src/engine/game/loot.ts +294 -0
  967. package/src/engine/game/macros.ts +125 -0
  968. package/src/engine/game/mail.ts +293 -0
  969. package/src/engine/game/meetingStoneQueue.ts +75 -0
  970. package/src/engine/game/meetingStoneUse.ts +54 -0
  971. package/src/engine/game/newGameState.ts +363 -0
  972. package/src/engine/game/oocAuraInterrupt.ts +92 -0
  973. package/src/engine/game/oocCast.ts +241 -0
  974. package/src/engine/game/oocCastEffects.ts +803 -0
  975. package/src/engine/game/oocCastMethods.ts +1077 -0
  976. package/src/engine/game/openLockFromSpell.ts +174 -0
  977. package/src/engine/game/outdoorPvp.ts +187 -0
  978. package/src/engine/game/outfitter.ts +644 -0
  979. package/src/engine/game/paladinBoard.ts +393 -0
  980. package/src/engine/game/partyAuras.ts +121 -0
  981. package/src/engine/game/persistCombatAuras.ts +91 -0
  982. package/src/engine/game/pets.ts +459 -0
  983. package/src/engine/game/prePullHealthstones.ts +154 -0
  984. package/src/engine/game/professions.ts +2227 -0
  985. package/src/engine/game/pruneStaleRuntime.ts +39 -0
  986. package/src/engine/game/questFightCredit.ts +36 -0
  987. package/src/engine/game/questItemOnCorpse.ts +166 -0
  988. package/src/engine/game/questItemOnCreature.ts +103 -0
  989. package/src/engine/game/questLog.ts +879 -0
  990. package/src/engine/game/questSpellComplete.ts +21 -0
  991. package/src/engine/game/questTurnIn.ts +598 -0
  992. package/src/engine/game/raidParty.ts +32 -0
  993. package/src/engine/game/reagentBuyIn.ts +39 -0
  994. package/src/engine/game/regenRest.ts +186 -0
  995. package/src/engine/game/rotation.ts +425 -0
  996. package/src/engine/game/runFullDungeon.ts +611 -0
  997. package/src/engine/game/runSupply.ts +80 -0
  998. package/src/engine/game/sanitizeDriftedItems.ts +135 -0
  999. package/src/engine/game/scriptDevGoUse.ts +397 -0
  1000. package/src/engine/game/scriptDevInnkeeper.ts +150 -0
  1001. package/src/engine/game/scriptDevProfessionSpec.ts +99 -0
  1002. package/src/engine/game/sendEventFromSpell.ts +87 -0
  1003. package/src/engine/game/sortContainers.ts +85 -0
  1004. package/src/engine/game/spellReputation.ts +69 -0
  1005. package/src/engine/game/squad/squadDungeonSeating.ts +197 -0
  1006. package/src/engine/game/squadDungeon.ts +4222 -0
  1007. package/src/engine/game/squadQuestShare.ts +260 -0
  1008. package/src/engine/game/summonObjectSlot.ts +69 -0
  1009. package/src/engine/game/summonObjectWild.ts +73 -0
  1010. package/src/engine/game/summoningRitualUse.ts +141 -0
  1011. package/src/engine/game/talents.ts +356 -0
  1012. package/src/engine/game/tamingRodUse.ts +66 -0
  1013. package/src/engine/game/tempSpawnCreature.ts +136 -0
  1014. package/src/engine/game/tickDungeonPatrolScripts.ts +205 -0
  1015. package/src/engine/game/tickTimedQuests.ts +28 -0
  1016. package/src/engine/game/trade.ts +586 -0
  1017. package/src/engine/game/travel.ts +1155 -0
  1018. package/src/engine/game/types.ts +1855 -0
  1019. package/src/engine/game/vendorOffers.ts +244 -0
  1020. package/src/engine/game/weaponImbueItem.ts +281 -0
  1021. package/src/engine/game/worldSession.ts +183 -0
  1022. package/src/engine/game/worldState.ts +166 -0
  1023. package/src/engine/game.ts +968 -0
  1024. package/src/engine/spells/GAP.md +67 -0
  1025. package/src/engine/spells/absorbs.ts +376 -0
  1026. package/src/engine/spells/addTargetTrigger.ts +53 -0
  1027. package/src/engine/spells/auraPerSecondCost.ts +50 -0
  1028. package/src/engine/spells/auraStacking.ts +497 -0
  1029. package/src/engine/spells/cmangos-special-cases-combustion.ts +13 -0
  1030. package/src/engine/spells/cmangos-special-cases-s18.ts +445 -0
  1031. package/src/engine/spells/cmangos-special-cases-s19.ts +324 -0
  1032. package/src/engine/spells/cmangos-special-cases-s20.ts +385 -0
  1033. package/src/engine/spells/cmangos-special-cases-s21.ts +116 -0
  1034. package/src/engine/spells/cmangos-special-cases-s22.ts +24 -0
  1035. package/src/engine/spells/cmangos-special-cases-s23.ts +52 -0
  1036. package/src/engine/spells/cmangos-special-cases.ts +1570 -0
  1037. package/src/engine/spells/conditions.ts +718 -0
  1038. package/src/engine/spells/effects.ts +203 -0
  1039. package/src/engine/spells/fixtures/oracle-effects-lock9a.json +1 -0
  1040. package/src/engine/spells/immunity.ts +942 -0
  1041. package/src/engine/spells/intentEffects.ts +351 -0
  1042. package/src/engine/spells/interpreter/GAPS.md +21 -0
  1043. package/src/engine/spells/interpreter/amount.ts +109 -0
  1044. package/src/engine/spells/interpreter/class-scripts-kit.ts +478 -0
  1045. package/src/engine/spells/interpreter/diff.ts +246 -0
  1046. package/src/engine/spells/interpreter/duration.ts +90 -0
  1047. package/src/engine/spells/interpreter/form-attrs.json +1 -0
  1048. package/src/engine/spells/interpreter/form-attrs.ts +95 -0
  1049. package/src/engine/spells/interpreter/handlers/aura-remove.ts +113 -0
  1050. package/src/engine/spells/interpreter/handlers/auras-control.ts +1547 -0
  1051. package/src/engine/spells/interpreter/handlers/auras-core.ts +1583 -0
  1052. package/src/engine/spells/interpreter/handlers/auras-misc.ts +1774 -0
  1053. package/src/engine/spells/interpreter/handlers/effect-disenchant.ts +17 -0
  1054. package/src/engine/spells/interpreter/handlers/effect-skinning.ts +20 -0
  1055. package/src/engine/spells/interpreter/handlers/effects-action-utility.ts +1455 -0
  1056. package/src/engine/spells/interpreter/handlers/effects-core.ts +432 -0
  1057. package/src/engine/spells/interpreter/handlers/effects-remainder.ts +105 -0
  1058. package/src/engine/spells/interpreter/handlers/effects-summon-item.ts +687 -0
  1059. package/src/engine/spells/interpreter/handlers/mod-increase-energy-percent.ts +36 -0
  1060. package/src/engine/spells/interpreter/handlers/mod-increase-energy.ts +36 -0
  1061. package/src/engine/spells/interpreter/handlers/periodic-trigger-zero.ts +327 -0
  1062. package/src/engine/spells/interpreter/handlers/procs.ts +131 -0
  1063. package/src/engine/spells/interpreter/handlers/registry.ts +975 -0
  1064. package/src/engine/spells/interpreter/handlers/shared.ts +667 -0
  1065. package/src/engine/spells/interpreter/handlers.ts +9 -0
  1066. package/src/engine/spells/interpreter/hunter-kit.ts +202 -0
  1067. package/src/engine/spells/interpreter/index.ts +525 -0
  1068. package/src/engine/spells/interpreter/item-triggers.ts +1121 -0
  1069. package/src/engine/spells/interpreter/lift-consume-energy.ts +37 -0
  1070. package/src/engine/spells/interpreter/mage-portals.ts +37 -0
  1071. package/src/engine/spells/interpreter/materialize-charm.ts +88 -0
  1072. package/src/engine/spells/interpreter/materialize-dispel-mechanic.ts +25 -0
  1073. package/src/engine/spells/interpreter/materialize-rating-buffs.ts +81 -0
  1074. package/src/engine/spells/interpreter/materialize-speed.ts +109 -0
  1075. package/src/engine/spells/interpreter/materialize-spell-magnet.ts +22 -0
  1076. package/src/engine/spells/interpreter/opcodes.ts +789 -0
  1077. package/src/engine/spells/interpreter/paladin-kit.ts +95 -0
  1078. package/src/engine/spells/interpreter/paladin-seals.ts +499 -0
  1079. package/src/engine/spells/interpreter/priest-warlock-kit.ts +135 -0
  1080. package/src/engine/spells/interpreter/proc.ts +443 -0
  1081. package/src/engine/spells/interpreter/registry/helpers.ts +16 -0
  1082. package/src/engine/spells/interpreter/registry/index.ts +216 -0
  1083. package/src/engine/spells/interpreter/registry/packs/class-abilities.ts +304 -0
  1084. package/src/engine/spells/interpreter/registry/packs/dire-maul.ts +44 -0
  1085. package/src/engine/spells/interpreter/registry/packs/dummy-script-tail.ts +1065 -0
  1086. package/src/engine/spells/interpreter/registry/packs/ex-wave-m.ts +70 -0
  1087. package/src/engine/spells/interpreter/registry/packs/life-tap.ts +107 -0
  1088. package/src/engine/spells/interpreter/registry/packs/paladin.ts +246 -0
  1089. package/src/engine/spells/interpreter/registry/packs/raid-script-summons.ts +26 -0
  1090. package/src/engine/spells/interpreter/registry/packs/raid-threat-wipe.ts +22 -0
  1091. package/src/engine/spells/interpreter/registry/packs/shaman.ts +214 -0
  1092. package/src/engine/spells/interpreter/registry/packs/si4-dummy-generic-a.ts +269 -0
  1093. package/src/engine/spells/interpreter/registry/packs/si4-dummy-generic-b.ts +370 -0
  1094. package/src/engine/spells/interpreter/registry/packs/si4-dummy-generic-c.ts +172 -0
  1095. package/src/engine/spells/interpreter/registry/packs/si4-dummy-hunter.ts +60 -0
  1096. package/src/engine/spells/interpreter/registry/packs/si4-dummy-warrior.ts +31 -0
  1097. package/src/engine/spells/interpreter/registry/packs/si4-script-generic-a.ts +114 -0
  1098. package/src/engine/spells/interpreter/registry/packs/si4-script-generic-b.ts +249 -0
  1099. package/src/engine/spells/interpreter/registry/packs/si4-script-generic-c.ts +233 -0
  1100. package/src/engine/spells/interpreter/registry/types.ts +48 -0
  1101. package/src/engine/spells/interpreter/talent-scripted.ts +329 -0
  1102. package/src/engine/spells/interpreter/targeting.ts +90 -0
  1103. package/src/engine/spells/interpreter/types-intent.ts +1501 -0
  1104. package/src/engine/spells/interpreter/types.ts +419 -0
  1105. package/src/engine/spells/interpreter/warrior-druid-kit.ts +314 -0
  1106. package/src/engine/spells/interpreter/weapon-imbue-kit.ts +384 -0
  1107. package/src/engine/spells/interrupt.ts +217 -0
  1108. package/src/engine/spells/meleeAttackReset.ts +60 -0
  1109. package/src/engine/spells/oocTriggerSpell.ts +49 -0
  1110. package/src/engine/spells/overrideClassScripts.ts +319 -0
  1111. package/src/engine/spells/parryHaste.ts +20 -0
  1112. package/src/engine/spells/procs.ts +300 -0
  1113. package/src/engine/spells/recovery.ts +203 -0
  1114. package/src/engine/spells/scriptTargetDest.ts +76 -0
  1115. package/src/engine/spells/scriptTargets.ts +85 -0
  1116. package/src/engine/spells/singleTargetAura.ts +82 -0
  1117. package/src/engine/spells/specialCasePayloads.ts +57 -0
  1118. package/src/engine/spells/spellAttributes.ts +249 -0
  1119. package/src/engine/spells/spellBonus.ts +143 -0
  1120. package/src/engine/spells/spellModIntents.ts +419 -0
  1121. package/src/engine/spells/spellMods.ts +283 -0
  1122. package/src/engine/spells/spellRadius.ts +15 -0
  1123. package/src/engine/spells/trapObjects.ts +26 -0
  1124. package/src/json.d.ts +7 -0
  1125. package/src/persistence/memoryRepository.ts +23 -0
  1126. package/src/persistence/saveBackups.ts +162 -0
  1127. package/src/persistence/saveFile.ts +68 -0
  1128. package/src/persistence/saveRepository.ts +11 -0
  1129. package/src/persistence/saveShape.ts +668 -0
  1130. package/src/progression/leveling.ts +89 -0
  1131. package/src/progression/rest.ts +1434 -0
  1132. package/src/progression/training.ts +226 -0
  1133. package/src/progression/xpGain.ts +369 -0
  1134. package/src/quests/quests.ts +1580 -0
  1135. package/src/world/aoePull.ts +198 -0
  1136. package/src/world/areaTableDbc.ts +39 -0
  1137. package/src/world/areaTableMapIdPairs.ts +141 -0
  1138. package/src/world/bakeSlug.ts +43 -0
  1139. package/src/world/continuousDungeon.ts +1250 -0
  1140. package/src/world/dungeonBakeProjection.ts +190 -0
  1141. package/src/world/dungeonMapDensity.ts +342 -0
  1142. package/src/world/dungeonMapProjection.ts +135 -0
  1143. package/src/world/dungeonNav.ts +904 -0
  1144. package/src/world/dungeonNpcSim.ts +147 -0
  1145. package/src/world/dungeonObjectives.ts +234 -0
  1146. package/src/world/dungeonPreset.ts +35 -0
  1147. package/src/world/dungeonRoute.ts +311 -0
  1148. package/src/world/dungeonRouteGeometry.ts +732 -0
  1149. package/src/world/dungeonRouteOrder.ts +1037 -0
  1150. package/src/world/dungeonWingRooms.ts +426 -0
  1151. package/src/world/escort.ts +296 -0
  1152. package/src/world/goPoolRoll.ts +75 -0
  1153. package/src/world/instanceObjects.ts +785 -0
  1154. package/src/world/instances.ts +1572 -0
  1155. package/src/world/meetingStones.ts +56 -0
  1156. package/src/world/navmeshFloors.ts +145 -0
  1157. package/src/world/nearGrid.ts +167 -0
  1158. package/src/world/packBudget.ts +613 -0
  1159. package/src/world/rareRoll.ts +335 -0
  1160. package/src/world/routeAuthoring.ts +132 -0
  1161. package/src/world/routeEdit.ts +209 -0
  1162. package/src/world/world.ts +812 -0
  1163. package/src/world/zoneHazards.ts +47 -0
@@ -0,0 +1,2227 @@
1
+ // Professions, gather, craft, enchant, GO interact and pickpocket facade methods. The gather helpers
2
+ // are module-local, and the apply-gather export lets fight-end auto-skin share the same path.
3
+
4
+ import {
5
+ rollGather, gainSkill, canGather, professionSkillValue, skillGainTenths,
6
+ gatherSkillGainChanceTenths,
7
+ rollCharges, nodeReady, respawnRemainingMs, nodeCooldownMs, isFishingPole, isMiningPick, isSkinningKnife,
8
+ ensureFishingPoleEquipped,
9
+ craft as doCraft, trainProfession as doTrainProfession, unlearnProfession as doUnlearnProfession,
10
+ learnRecipe as doLearnRecipe,
11
+ grantStarterRecipes as doGrantStarterRecipes,
12
+ } from '../../domain/professions/professions.ts';
13
+ import {
14
+ recipeCooldownRemainingMs, stampRecipeCooldown, craftCooldownLabel,
15
+ } from '../../domain/professions/craftCooldown.ts';
16
+ import {
17
+ miningHarvestTapCount, miningLockReqSkill, miningVeinOpenRange, mergeLootDrops,
18
+ } from '../../domain/professions/miningHarvest.ts';
19
+ import {
20
+ isRotationNode, rotationNodeUp, rotationNodeNextUpMs,
21
+ } from '../../domain/professions/rareNodes.ts';
22
+ import {
23
+ nearestPartyWorker,
24
+ partyWorkRefusalReason,
25
+ pickPartyWorker,
26
+ type PartyWorker,
27
+ } from '../../domain/professions/partyWork.ts';
28
+ import { companionLevel, companionSkillMap, type Companion } from '../../domain/bots/squad.ts';
29
+ import { dumpGatherGoodsFromState } from '../../domain/bots/sendTradeGoods.ts';
30
+ import type { CompanionProfessionMap } from '../../domain/bots/companionMissions.ts';
31
+ import { getRank, REP_RANK } from '../../domain/creature/reputation.ts';
32
+ import { hasAttrEx2, SPELL_ATTR_EX2 } from '../spells/spellAttributes.ts';
33
+ import {
34
+ blacksmithGossipHello,
35
+ BS_BOUND_CREATURES,
36
+ leatherGossipHello,
37
+ LW_BOUND_CREATURES,
38
+ N_TRAINER_SMITHOMNI1,
39
+ N_TRAINER_SMITHOMNI2,
40
+ REP_ARMOR,
41
+ REP_WEAPON,
42
+ } from '../../domain/scriptdev/npcProfessions.ts';
43
+ import {
44
+ applyBlacksmithSpecSelect,
45
+ applyLeatherSpecSelect,
46
+ knownProfessionSpecs,
47
+ } from '../../domain/scriptdev/professionSpecConsumer.ts';
48
+ import { consumeProfessionSpecKind } from './scriptDevProfessionSpec.ts';
49
+ import { buyNames, stageVendorReagents } from './reagentBuyIn.ts';
50
+ import { maxCraftsWithVendor } from '../../domain/professions/vendorReagents.ts';
51
+ import {
52
+ applyEnchant as doApplyEnchant, learnEnchant as doLearnEnchant,
53
+ } from '../../domain/professions/enchanting.ts';
54
+ import { partyHeldTools } from '../../domain/professions/heldTools.ts';
55
+ import {
56
+ partyDisenchantCheck,
57
+ resolveRosterEnchanters,
58
+ runPartyDisenchant,
59
+ type DisenchantPick,
60
+ } from '../../domain/professions/partyDisenchant.ts';
61
+ import {
62
+ canPickPocket,
63
+ rollPickPocket,
64
+ canRestockPickpocket,
65
+ startPickpocketRestock,
66
+ pickpocketLootKey,
67
+ PICK_POCKET_SPELL_ID,
68
+ PICKPOCKET_ERR,
69
+ } from '../../domain/professions/pickpocket.ts';
70
+ import {
71
+ useGameObject,
72
+ useSummoningRitualParty,
73
+ applyLinkedTrap,
74
+ goRuntimeAvailability,
75
+ GAMEOBJECT_TYPE_QUESTGIVER,
76
+ GAMEOBJECT_TYPE_FISHINGHOLE,
77
+ GAMEOBJECT_TYPE_SUMMONING_RITUAL,
78
+ GAMEOBJECT_TYPE_TEXT,
79
+ GAMEOBJECT_TYPE_MEETINGSTONE,
80
+ goLootIsQuestOnly,
81
+ isGoFishingSchool,
82
+ type GoRuntimeState,
83
+ type UseGameObjectReason,
84
+ } from '../../domain/world/gameObjects.ts';
85
+ import { applyType15MoTransportUse } from './goMoTransportUse.ts';
86
+ import { applyType11TransportUse, type Type11DepartHooks } from './goTransportUse.ts';
87
+ import {
88
+ applySummoningRitualUse,
89
+ ritualPartyUserIds,
90
+ } from './summoningRitualUse.ts';
91
+ import {
92
+ meetingStoneGroupTarget,
93
+ meetingStoneUseCtx,
94
+ placeMeetingStoneSummon,
95
+ } from './meetingStoneUse.ts';
96
+ import { mergeWildSummoned } from './summonObjectWild.ts';
97
+ import {
98
+ matchingKeyItemEntry,
99
+ canOpenLock,
100
+ requiredKeyItemEntry,
101
+ hardestSkillLockCase,
102
+ isExpendableLockKeyItem,
103
+ isFreeOpenLockType,
104
+ skillByLockType,
105
+ LOCK_KEY_SKILL,
106
+ SKILL_LOCKPICKING as LOCK_SKILL_LOCKPICKING,
107
+ type LockDef,
108
+ } from '../../domain/world/openLock.ts';
109
+ import {
110
+ canOpenContainer,
111
+ companionLockpickingSkill,
112
+ containerPicklockReq,
113
+ groupLockpickingSkill,
114
+ lockpickCompanionPool,
115
+ lockpickWorkers,
116
+ playerLockpickingForLocks,
117
+ } from '../../domain/items/itemContainer.ts';
118
+ import { tryLockpickingSkillUp } from '../../domain/world/openLock.ts';
119
+ import { SKILL_LINE_BY_ID } from '../../data/schema/profession.ts';
120
+ import { consumeItemCharges, isExpendableCastItem } from '../../domain/items/inventory.ts';
121
+ import { activeStealthAura } from '../../domain/character/character.ts';
122
+
123
+ import {
124
+ addItem, addInstance, removeItem, canFit, countItem,
125
+ } from '../../domain/items/inventory.ts';
126
+ import { ownedInventory } from '../../domain/items/keyring.ts';
127
+ import { equippedEntry, hasIdentity, identityCapable, type EquippedValue } from '../../domain/items/itemInstance.ts';
128
+ import { createItemInstance } from '../../domain/items/createItem.ts';
129
+ import { applyBindToInstance, resolveBonding } from '../../domain/items/itemBonding.ts';
130
+ import { itemTeachIntent, unavailableTeachReason } from '../../domain/items/itemTeaches.ts';
131
+ import { rollLoot, isExclusiveQuestLootRow, type LootDrop } from '../../domain/items/loot.ts';
132
+ import {
133
+ gatherLootKind, goLootKind, type PendingLoot,
134
+ } from '../../domain/items/pendingLoot.ts';
135
+ import {
136
+ getQuest,
137
+ recordGoEnderInteract, recordEventHappens, recordKill,
138
+ questNeededItems, hasQuestForItem, goQuestGiverOffer, type QuestPlayerSnapshot,
139
+ } from '../../domain/quests/questlog.ts';
140
+ import { applyGoUseAndEventDoors } from './applyDoorFromDbScript.ts';
141
+ import { applyFailedQuests } from './applyTerminateFromDbScript.ts';
142
+ import { applyTailFromDbScript } from './applyTailFromDbScript.ts';
143
+ import { deliverScriptMail } from './mail.ts';
144
+ import { ensureWorldStates } from './worldState.ts';
145
+ import {
146
+ applyEpTowerCaptureFromGoUse,
147
+ TEAM_ALLIANCE,
148
+ TEAM_HORDE,
149
+ } from './outdoorPvp.ts';
150
+ import { factionOf } from '../../domain/character/races.ts';
151
+ import { canInteractWithCreature } from '../../domain/creature/faction.ts';
152
+ import { activeEventIds } from '../../domain/world/gameEvents.ts';
153
+ import type { GatherNodeDef, RecipeDef } from '../../data/schema.ts';
154
+ import { mulberry32, hashSeed } from '../core/rng.ts';
155
+ import { spellMissChancePercent } from '../combat/mitigation.ts';
156
+ import type { Rng } from '../core/rng.ts';
157
+ import type { ConditionTable } from '../spells/conditions.ts';
158
+ import { isConditionSatisfied } from '../spells/conditions.ts';
159
+ import type { ActionResult, Game } from './types.ts';
160
+ import type { GameCtx } from './context.ts';
161
+ import { announceCompanionRanks } from './companionTraining.ts';
162
+ import { scriptDevByGo } from '../../domain/scriptdev/index.ts';
163
+ import { runScriptDevGoUse } from './scriptDevGoUse.ts';
164
+ import { applyInteractObjectQuestCredit } from './goInteractQuestCredit.ts';
165
+ import {
166
+ creditSquadEvent,
167
+ creditSquadGoEnder,
168
+ creditSquadKill,
169
+ creditSquadLogs,
170
+ } from './squadQuestShare.ts';
171
+ import { applyGoSpellcasterCast } from './applyGoSpellcasterCast.ts';
172
+ import { moneyMarked } from '../../domain/economy/money.ts';
173
+ import { refuseIfDead } from '../../domain/run/death.ts';
174
+
175
+ const nodeKey = (zoneId: string, nodeId: number): string => `${zoneId}:${nodeId}`;
176
+
177
+ /** Start-of-harvest charge count: re-roll when pristine or respawned (charges already 0). */
178
+ function startGatherCharges(ctx: GameCtx, node: GatherNodeDef, nodeId: number, rng: Rng): number {
179
+ const prior = ctx.state.nodeState[nodeKey(ctx.state.char.zoneId, nodeId)];
180
+ if (!prior || prior.charges <= 0) return rollCharges(node, rng);
181
+ return prior.charges;
182
+ }
183
+
184
+ // Spend one harvest charge on a gather node and mirror it into the GO runtime under the same key:
185
+ // the node state is authoritative, and the runtime tracks the same restock window.
186
+ function commitGatherNodeCharge(
187
+ ctx: GameCtx,
188
+ node: GatherNodeDef,
189
+ nodeId: number,
190
+ nowMs: number,
191
+ startCharges: number,
192
+ spent = 1,
193
+ ): { remaining: number; depleted: boolean; respawnMs: number } {
194
+ const { state } = ctx;
195
+ const key = nodeKey(state.char.zoneId, nodeId);
196
+ const remaining = Math.max(0, startCharges - spent);
197
+ const depleted = remaining <= 0;
198
+ const respawnMs = depleted
199
+ ? (isRotationNode(nodeId)
200
+ ? Math.max(0, rotationNodeNextUpMs(state.char.zoneId, nodeId, nowMs) - nowMs)
201
+ : nodeCooldownMs(node))
202
+ : 0;
203
+ const readyAt = depleted ? nowMs + respawnMs : 0;
204
+ state.nodeState[key] = { charges: remaining, readyAt };
205
+ // Mirror into goRuntime so useGameObject restock / world panel refuse the same window.
206
+ const entry = node.sourceId ?? nodeId;
207
+ const rtKey = nodeKey(state.char.zoneId, entry);
208
+ const prevRt = state.goRuntime?.[rtKey] ?? {};
209
+ state.goRuntime = {
210
+ ...(state.goRuntime ?? {}),
211
+ [rtKey]: {
212
+ ...prevRt,
213
+ // charges left → ready now; depleted → restock at readyAt (not permanent MAX_SAFE)
214
+ readyAtMs: depleted ? readyAt : nowMs,
215
+ },
216
+ };
217
+ return { remaining, depleted, respawnMs };
218
+ }
219
+
220
+ // Item entries the player has "on hand" (bags + equipped) - used for tool checks.
221
+ function hasFishingPole(ctx: GameCtx): boolean {
222
+ const { ds, state } = ctx;
223
+ return state.inventory.stacks.some((s) => isFishingPole(ds.item(s.item)))
224
+ || Object.values(state.char.equipped).some((v) => {
225
+ const e = equippedEntry(v);
226
+ return e != null && isFishingPole(ds.item(e));
227
+ });
228
+ }
229
+
230
+ /** Wear a bagged pole before a school throw. Spell.cpp EquippedItem subclass 20. */
231
+ function wearFishingPole(ctx: GameCtx): boolean {
232
+ const worn = ensureFishingPoleEquipped(ctx.state.char, ctx.state.inventory, ctx.ds);
233
+ if (!worn.ok) return false;
234
+ ctx.state.char = worn.char;
235
+ ctx.state.inventory = worn.inventory;
236
+ return true;
237
+ }
238
+
239
+ // A herb or vein on the floor is a GROUP job: the player's skills and every squad member's
240
+ // professions are real trade skills. The point goes to whoever swung at it.
241
+
242
+ /** Narrow an Object.entries key back to a SkillLine — the map is keyed by one by construction. */
243
+ function isSkillLine(line: string): line is GatherNodeDef['skill'] {
244
+ return Object.values(SKILL_LINE_BY_ID).includes(line as GatherNodeDef['skill']);
245
+ }
246
+
247
+ /** The tool this gathering line cannot work without, or null when it needs none. */
248
+ function gatherToolPredicate(
249
+ line: GatherNodeDef['skill'],
250
+ ): ((def: ReturnType<GameCtx['ds']['item']>) => boolean) | null {
251
+ if (line === 'mining') return isMiningPick;
252
+ if (line === 'skinning') return isSkinningKnife;
253
+ if (line === 'fishing') return isFishingPole;
254
+ return null;
255
+ }
256
+
257
+ /** Tool in these bags or on this paperdoll — same "bags or equipped" rule as the player. */
258
+ function holdsGatherTool(
259
+ ctx: GameCtx,
260
+ line: GatherNodeDef['skill'],
261
+ bags: { stacks: readonly { item: number }[] } | undefined,
262
+ equipped: Record<string, EquippedValue | undefined> | undefined,
263
+ ): boolean {
264
+ const pred = gatherToolPredicate(line);
265
+ if (!pred) return true;
266
+ if (bags?.stacks?.some((s) => pred(ctx.ds.item(s.item)))) return true;
267
+ for (const v of Object.values(equipped ?? {})) {
268
+ const e = equippedEntry(v);
269
+ if (e != null && pred(ctx.ds.item(e))) return true;
270
+ }
271
+ return false;
272
+ }
273
+
274
+ // Everyone who could work the node, and everyone who merely HAS the line, for an honest refusal.
275
+ // Candidates run player-first then squad order, and every tie breaks on that, so it is stable.
276
+ function gatherWorkers(
277
+ ctx: GameCtx,
278
+ node: GatherNodeDef,
279
+ ): { candidates: PartyWorker[]; holders: PartyWorker[] } {
280
+ const { ds, state } = ctx;
281
+ const line = node.skill;
282
+ const candidates: PartyWorker[] = [];
283
+ const holders: PartyWorker[] = [];
284
+
285
+ const consider = (w: PartyWorker, hasTool: boolean): void => {
286
+ holders.push(w);
287
+ if (w.skill >= node.orange && hasTool) candidates.push(w);
288
+ };
289
+
290
+ const own = state.char.skills[line];
291
+ if (own) {
292
+ const value = professionSkillValue(state.char, line, ds, state.spellbook) ?? own.current;
293
+ consider(
294
+ {
295
+ name: state.char.name,
296
+ skill: value,
297
+ max: own.max,
298
+ gainTenths: skillGainTenths(line, own, node),
299
+ },
300
+ holdsGatherTool(ctx, line, state.inventory, state.char.equipped),
301
+ );
302
+ }
303
+
304
+ for (const comp of state.squad ?? []) {
305
+ // Away on a send-out mission — the same skip partyHasSkinner and the auto-skin fallback make.
306
+ if (comp.mission) continue;
307
+ const lvl = companionLevel(comp, state.char.level);
308
+ const cur = companionSkillMap(comp, lvl)[line];
309
+ if (!cur) continue;
310
+ consider(
311
+ {
312
+ compId: comp.id,
313
+ name: comp.name,
314
+ skill: cur.current,
315
+ max: cur.max,
316
+ gainTenths: skillGainTenths(line, cur, node),
317
+ },
318
+ holdsGatherTool(ctx, line, comp.bags, comp.equipped),
319
+ );
320
+ }
321
+ return { candidates, holders };
322
+ }
323
+
324
+ /** Honest refusal when nobody in the group can work the node (skill or missing tool). */
325
+ function gatherGroupRefusal(ctx: GameCtx, node: GatherNodeDef): string {
326
+ const { candidates, holders } = gatherWorkers(ctx, node);
327
+ if (candidates.length) return '';
328
+ const skillName = ctx.ds.profession(node.skill)?.name ?? node.skill;
329
+ // Someone qualifies on skill but nobody holds the tool — say the tool, not the skill.
330
+ const skilled = holders.filter((h) => h.skill >= node.orange);
331
+ const pred = gatherToolPredicate(node.skill);
332
+ if (skilled.length && pred) {
333
+ const toolName = node.skill === 'mining'
334
+ ? 'a Mining Pick'
335
+ : node.skill === 'skinning' ? 'a Skinning Knife' : 'a Fishing Pole';
336
+ return `Requires ${toolName} - nobody in the group carries one`;
337
+ }
338
+ return partyWorkRefusalReason(skillName, nearestPartyWorker(holders, node.orange));
339
+ }
340
+
341
+ function miningTapsFor(
342
+ ctx: GameCtx,
343
+ node: GatherNodeDef,
344
+ miningSkill: number,
345
+ rng: Rng,
346
+ ): number {
347
+ const entry = node.sourceId ?? node.id;
348
+ const go = ctx.ds.gameObject(entry);
349
+ const lock = go?.lockId != null ? ctx.ds.lock(go.lockId) : undefined;
350
+ const { min, max } = miningVeinOpenRange(node, go?.chest);
351
+ return miningHarvestTapCount(min, max, miningSkill, miningLockReqSkill(lock), rng);
352
+ }
353
+
354
+ function grantWorkerGatherSkillTaps(
355
+ ctx: GameCtx,
356
+ worker: PartyWorker,
357
+ node: GatherNodeDef,
358
+ rng: Rng,
359
+ taps: number,
360
+ ): number | undefined {
361
+ let skillUp: number | undefined;
362
+ let w = worker;
363
+ for (let i = 0; i < taps; i++) {
364
+ const up = grantWorkerGatherSkill(ctx, w, node, rng);
365
+ if (up == null) continue;
366
+ skillUp = up;
367
+ w = {
368
+ ...w,
369
+ skill: up,
370
+ gainTenths: skillGainTenths(node.skill, { current: up, max: w.max }, node),
371
+ };
372
+ }
373
+ return skillUp;
374
+ }
375
+
376
+ // Grant the node's skill point to the worker. A companion rolls the SAME chance and freezes its
377
+ // seed-derived professions first, or learning one point would wipe the two it arrived with.
378
+ function grantWorkerGatherSkill(
379
+ ctx: GameCtx,
380
+ worker: PartyWorker,
381
+ node: GatherNodeDef,
382
+ rng: Rng,
383
+ ): number | undefined {
384
+ const { state } = ctx;
385
+ if (worker.compId == null) {
386
+ const gain = gainSkill(state.char, node.skill, node, rng);
387
+ state.char = gain.char;
388
+ return gain.up;
389
+ }
390
+ const comp = (state.squad ?? []).find((m) => m.id === worker.compId);
391
+ if (!comp) return undefined;
392
+ const tenths = skillGainTenths(node.skill, { current: worker.skill, max: worker.max }, node);
393
+ if (tenths <= 0 || !rng.chance(tenths / 1000)) return undefined;
394
+ const lvl = companionLevel(comp, state.char.level);
395
+ if (comp.professions == null || Object.keys(comp.professions).length === 0) {
396
+ const frozen: CompanionProfessionMap = {};
397
+ for (const [line, cur] of Object.entries(companionSkillMap(comp, lvl))) {
398
+ if (isSkillLine(line) && cur) frozen[line] = cur.current;
399
+ }
400
+ comp.professions = frozen;
401
+ }
402
+ // worker.max is the trainer rank they hold, not the raw level cap — an untrained rung stops here.
403
+ const next = Math.min(worker.max, worker.skill + 1);
404
+ if (next <= worker.skill) return undefined;
405
+ comp.professions = { ...comp.professions, [node.skill]: next };
406
+ announceCompanionRanks(comp, lvl, ctx.ds, ctx.log);
407
+ return next;
408
+ }
409
+
410
+ // True when Lock.dbc has a free-open case, which succeeds with no profession skill line at all.
411
+ // Cite: Spell.cpp CanOpenLock (the cases are OR'd); domain/world/openLock.ts.
412
+ function lockHasFreeOpenCase(lock: LockDef | undefined): boolean {
413
+ if (!lock?.cases?.length) return false;
414
+ for (const c of lock.cases) {
415
+ if (c.type === LOCK_KEY_SKILL && isFreeOpenLockType(c.index)) return true;
416
+ }
417
+ return false;
418
+ }
419
+
420
+ function gameObjectLockDef(ctx: GameCtx, objectEntry: number): LockDef | undefined {
421
+ const go = ctx.ds.gameObject(objectEntry);
422
+ if (go?.lockId == null) return undefined;
423
+ return ctx.ds.lock(go.lockId) as LockDef | undefined;
424
+ }
425
+
426
+ /** GO-sourced gather node whose lock free-opens (quest plant, not herb/mine skill). */
427
+ function isFreeOpenGatherNode(ctx: GameCtx, node: GatherNodeDef): boolean {
428
+ if (node.source === 'fishing' || node.source === 'skinning') return false;
429
+ const entry = node.sourceId ?? node.id;
430
+ return lockHasFreeOpenCase(gameObjectLockDef(ctx, entry));
431
+ }
432
+
433
+ // The gather node for a world gameobject, so interactObject shares the gates and skill-ups. A
434
+ // free-open lock is NOT a profession node even when the bake mislists it as herbalism.
435
+ function gatherNodeForGameObject(ctx: GameCtx, objectEntry: number): GatherNodeDef | undefined {
436
+ const node = ctx.ds.gatherNode(objectEntry);
437
+ if (!node) return undefined;
438
+ // Skinning is corpse-only; fishing nodes are area schools, not GO chests.
439
+ if (node.skill === 'skinning' || node.skill === 'fishing') return undefined;
440
+ if (node.source != null && node.source !== 'gameobject') return undefined;
441
+ if (node.sourceId != null && node.sourceId !== objectEntry) return undefined;
442
+ if (lockHasFreeOpenCase(gameObjectLockDef(ctx, objectEntry))) return undefined;
443
+ return node;
444
+ }
445
+
446
+ // Profession skill and tool gates for a herb or mine GO, the same ones gather() applies. Returns a
447
+ // refuse reason, or null when the player may open the node.
448
+ function gatherProfessionGateReason(
449
+ ctx: GameCtx,
450
+ node: GatherNodeDef,
451
+ ): string | null {
452
+ // KI-810: free-open LockType — no profession / pick / pole gate.
453
+ if (isFreeOpenGatherNode(ctx, node)) return null;
454
+ // Group gate: the player OR any party/raid companion with the line and the tool.
455
+ return gatherGroupRefusal(ctx, node) || null;
456
+ }
457
+
458
+ /** GO runtime key - zone-scoped so restock does not leak across travel. */
459
+ const goRuntimeKey = (ctx: GameCtx, entry: number): string => `${ctx.state.char.zoneId}:${entry}`;
460
+
461
+ const goRefuseReason = (
462
+ reason: UseGameObjectReason | undefined,
463
+ opts?: {
464
+ restockAtMs?: number;
465
+ nowMs?: number;
466
+ /** Named key for LOCK_KEY_ITEM refuse (ds.item name). */
467
+ keyName?: string;
468
+ /** Skill gate detail for low_skill (e.g. "Lockpicking 25"). */
469
+ skillNeed?: string;
470
+ },
471
+ ): string => {
472
+ switch (reason) {
473
+ case 'locked':
474
+ return opts?.keyName
475
+ ? `Locked - you need ${opts.keyName}`
476
+ : 'Locked - you need the matching key';
477
+ case 'low_skill':
478
+ return opts?.skillNeed
479
+ ? `Your skill is too low - needs ${opts.skillNeed}`
480
+ : 'Your skill is too low to open this';
481
+ case 'quest_incomplete': return 'You do not have the required quest';
482
+ case 'event_inactive': return 'That is not available right now';
483
+ case 'not_ready': {
484
+ // An honest wait or route when every multi-spawn instance is consumed; a permanent despawn uses
485
+ // a maximum ready time. Cite: GameObject.h IsDespawnAtAction.
486
+ const ready = opts?.restockAtMs;
487
+ const now = opts?.nowMs ?? 0;
488
+ if (ready != null && ready >= Number.MAX_SAFE_INTEGER / 2) {
489
+ return 'All instances consumed - none remain in this zone';
490
+ }
491
+ if (ready != null && ready > now) {
492
+ const secs = Math.max(1, Math.ceil((ready - now) / 1000));
493
+ return `Not ready yet - wait ~${secs}s for another to appear`;
494
+ }
495
+ return 'Not ready yet';
496
+ }
497
+ case 'cooldown': return 'On cooldown';
498
+ case 'no_charges': return 'No charges remaining';
499
+ case 'party_only': return "Only the caster's party may use that";
500
+ case 'fishing_failed': return 'Nothing bites…';
501
+ case 'unsupported': return 'You cannot use that object';
502
+ case 'no_interact': return 'You cannot interact with that';
503
+ case 'missing_context': return 'Missing context to use this object';
504
+ case 'not_in_group': return 'You must be in the group';
505
+ case 'is_owner': return 'You are already channeling';
506
+ case 'owner_not_channeling': return 'The ritual has not started';
507
+ case 'no_target': return 'You must target a group member';
508
+ case 'level_out_of_range': return 'Your group is the wrong level for this stone';
509
+ default: return 'Cannot use object';
510
+ }
511
+ };
512
+
513
+ /** Lock.dbc skill case → GetSkillValue (pure + MOD_SKILL). Cite: openLock.ts; KI-935. */
514
+ function skillValueForLockType(ctx: GameCtx, lockType: number): number | undefined {
515
+ const { state, ds } = ctx;
516
+ const skillId = skillByLockType(lockType);
517
+ // The GROUP's best value for the line. A herb or vein GO is gated twice on the interact path, here
518
+ // and again in the profession gate, and both have to see the party or the herbalist opens nothing.
519
+ if (skillId === LOCK_SKILL_LOCKPICKING) {
520
+ const mine = playerLockpickingForLocks(state.char);
521
+ return groupLockpickingSkill(
522
+ mine, state.char.level, state.squad, ds.combatSkillMeta?.() ?? null,
523
+ );
524
+ }
525
+ const line = SKILL_LINE_BY_ID[skillId];
526
+ if (!line) return undefined;
527
+ let best = professionSkillValue(state.char, line, ds, state.spellbook);
528
+ for (const comp of state.squad ?? []) {
529
+ const cur = companionSkillMap(comp, companionLevel(comp, state.char.level))[line];
530
+ if (cur == null) continue;
531
+ if (best == null || cur.current > best) best = cur.current;
532
+ }
533
+ return best;
534
+ }
535
+
536
+ // Whether a node is harvestable right now — skill, tool and depletion — plus how long a depleted one
537
+ // has left and how many charges remain. Shared by the UI status and the authoritative gather check.
538
+ function computeGatherStatus(
539
+ ctx: GameCtx,
540
+ nodeId: number,
541
+ nowMs: number,
542
+ ): { ok: boolean; reason?: string; respawnMs: number; chargesLeft: number } {
543
+ const { ds, state } = ctx;
544
+ const node = ds.gatherNode(nodeId);
545
+ if (!node) return { ok: false, reason: 'Nothing to gather', respawnMs: 0, chargesLeft: 0 };
546
+ // Skinning is only reachable via a fresh corpse (use skin()); other nodes must
547
+ // belong to the current zone.
548
+ if (node.skill === 'skinning') return { ok: false, reason: 'Skin a corpse instead', respawnMs: 0, chargesLeft: 0 };
549
+ if (!ds.zone(state.char.zoneId)?.nodes?.includes(nodeId)) return { ok: false, reason: "That's not here", respawnMs: 0, chargesLeft: 0 };
550
+ const key = nodeKey(state.char.zoneId, nodeId);
551
+ const st = state.nodeState[key];
552
+ // KI-810: free-open Lock.dbc cases skip profession skill + tools (quest plants).
553
+ const freeOpen = isFreeOpenGatherNode(ctx, node);
554
+ if (!freeOpen) {
555
+ // Skill AND tool over the whole group: a party herbalist works the node the player cannot, and the
556
+ // refusal names who came closest. Fishing stays solo — the player holds the pole.
557
+ const refusal = node.skill === 'fishing'
558
+ ? (canGather(state.char, node, professionSkillValue(state.char, node.skill, ds, state.spellbook)).ok
559
+ ? (hasFishingPole(ctx) ? '' : 'Requires a Fishing Pole')
560
+ : `Requires ${node.skill} skill ${node.orange}`)
561
+ : gatherGroupRefusal(ctx, node);
562
+ if (refusal) return { ok: false, reason: refusal, respawnMs: 0, chargesLeft: st?.charges ?? 0 };
563
+ }
564
+ if (!rotationNodeUp(state.char.zoneId, nodeId, nowMs)) {
565
+ const nextUp = rotationNodeNextUpMs(state.char.zoneId, nodeId, nowMs);
566
+ return { ok: false, reason: 'Not in bloom', respawnMs: Math.max(0, nextUp - nowMs), chargesLeft: 0 };
567
+ }
568
+ if (!nodeReady(st, nowMs)) return { ok: false, reason: 'Depleted', respawnMs: respawnRemainingMs(st, nowMs), chargesLeft: 0 };
569
+ // KI-807: interactObject may have consumed the GO via goRuntime — same zone:entry key.
570
+ if (node.skill === 'herbalism' || node.skill === 'mining' || node.source === 'gameobject') {
571
+ const avail = goRuntimeAvailability(state.goRuntime?.[key], nowMs);
572
+ if (avail.kind === 'consumed') {
573
+ return { ok: false, reason: 'Depleted', respawnMs: 0, chargesLeft: 0 };
574
+ }
575
+ if (avail.kind === 'restock') {
576
+ return {
577
+ ok: false,
578
+ reason: 'Depleted',
579
+ respawnMs: Math.max(0, avail.readyAtMs - nowMs),
580
+ chargesLeft: 0,
581
+ };
582
+ }
583
+ }
584
+ return { ok: true, respawnMs: 0, chargesLeft: st && st.charges > 0 ? st.charges : 0 };
585
+ }
586
+
587
+ // Where a held haul goes: the facade's `openLoot` bound to this source. Returning the container lets the
588
+ // caller hand it back to the UI in one result.
589
+ export type LootHoldSink = (drops: readonly LootDrop[]) => PendingLoot;
590
+
591
+ /** Condition/quest gate a node's loot roll consults (same shape interactObject builds). */
592
+ export type GatherLootGate = {
593
+ lootConditionSnapshot: ProfessionsExtras['lootConditionSnapshot'];
594
+ conditionTableById: ProfessionsExtras['conditionTableById'];
595
+ };
596
+
597
+ // The authoritative loot table key for a gather node, by DB provenance: the gameobject's own loot id,
598
+ // the fishing area, or the skinning id. Covers all 758 baked nodes; null for a pre-DB fixture.
599
+ function gatherLootKey(ctx: GameCtx, node: GatherNodeDef): string | null {
600
+ if (node.sourceId == null) return null;
601
+ if (node.source === 'fishing') return `fishing:${node.sourceId}`;
602
+ if (node.source === 'skinning') return `skinning:${node.sourceId}`;
603
+ if (node.source === 'gameobject') {
604
+ const lootId = ctx.ds.gameObject(node.sourceId)?.lootId ?? 0;
605
+ return lootId > 0 ? `go:${lootId}` : null;
606
+ }
607
+ return null;
608
+ }
609
+
610
+ // One harvest is ONE roll of the node's real loot template, with group semantics: the flattened yield
611
+ // view handed the player every passing row at once, so a cast landed 1.40 items from a one-item table.
612
+ function rollNodeLoot(
613
+ ctx: GameCtx,
614
+ node: GatherNodeDef,
615
+ rng: Rng,
616
+ gate?: GatherLootGate,
617
+ ): LootDrop[] {
618
+ const key = gatherLootKey(ctx, node);
619
+ const table = key != null ? ctx.ds.lootTable(key) : [];
620
+ if (!table.length) return rollGather(node, rng); // fixture / no baked template
621
+ const needed = questNeededItems(ctx.state.questlog, (e) => ctx.ds.quest(e));
622
+ return rollLoot(table, rng, {
623
+ allowsQuestDrop: (entry: { item: number }) => needed.has(entry.item),
624
+ allowsCondition: gate
625
+ ? (conditionId: number) => isConditionSatisfied(
626
+ conditionId,
627
+ gate.lootConditionSnapshot(),
628
+ gate.conditionTableById(),
629
+ ).ok
630
+ : undefined,
631
+ });
632
+ }
633
+
634
+ // Harvest a node: roll yields into bags, then skill-up. Mining `taps` = every vein refill in one go.
635
+ // Cite: LootMgr.cpp:1265-1304. Seeded sub-stream, a fresh one per skin.
636
+ function applyGatherImpl(
637
+ ctx: GameCtx,
638
+ node: GatherNodeDef,
639
+ rng: Rng = mulberry32(hashSeed(ctx.state.rngSeed, ctx.state.fightCounter++)),
640
+ gate?: GatherLootGate,
641
+ hold?: LootHoldSink,
642
+ taps = 1,
643
+ ): ActionResult<{ yielded: LootDrop[]; skillUp?: number; workedBy?: string; held?: PendingLoot }> {
644
+ const { ds, state, log, takeDrop, refreshQuests, cap } = ctx;
645
+ // KI-810: free-open quest plants skip profession skill; SkillByLockType → SKILL_NONE so no skill-up.
646
+ const freeOpen = isFreeOpenGatherNode(ctx, node);
647
+ // Who swings at it. Skill AND tool over the whole group — 1.12 skinning needs the knife in bags or
648
+ // equipped — so a party skinner's knife counts as the group's. Cite: SharedDefines.h skinning knife.
649
+ let worker: PartyWorker | undefined;
650
+ if (!freeOpen) {
651
+ const { candidates } = gatherWorkers(ctx, node);
652
+ worker = pickPartyWorker(candidates);
653
+ if (!worker) return { ok: false, reason: gatherGroupRefusal(ctx, node) };
654
+ }
655
+ const n = Math.max(1, taps | 0);
656
+ const rolled = mergeLootDrops(
657
+ Array.from({ length: n }, () => rollNodeLoot(ctx, node, rng, gate)).flat(),
658
+ );
659
+ // HOLD: the node keeps its harvest until the player takes it (WoW's loot window). Otherwise the
660
+ // old direct-to-bags path, which autopilot / bots / dungeon runs still use.
661
+ const yielded: LootDrop[] = [];
662
+ let held: PendingLoot | undefined;
663
+ if (hold) {
664
+ held = hold(rolled);
665
+ yielded.push(...rolled);
666
+ } else {
667
+ // Bag-full must refuse before the skill-up and the node charge, as craft and disenchant do: never
668
+ // deplete a node or farm free skill while the mats vanish.
669
+ if (rolled.length > 0) {
670
+ let probe = state.inventory;
671
+ for (const l of rolled) {
672
+ const maxStack = ds.item(l.item)?.stackable ?? 1;
673
+ if (!canFit(probe, l.item, l.count, maxStack, cap())) {
674
+ return { ok: false, reason: 'Not enough bag space' };
675
+ }
676
+ probe = addItem(probe, l.item, l.count, maxStack);
677
+ }
678
+ }
679
+ for (const l of rolled) {
680
+ const took = takeDrop(l.item, l.count);
681
+ if (took > 0) yielded.push({ item: l.item, count: took });
682
+ }
683
+ if (yielded.length
684
+ && (node.skill === 'mining' || node.skill === 'herbalism' || node.skill === 'skinning')) {
685
+ dumpGatherGoodsFromState(state, ds, log);
686
+ }
687
+ }
688
+ let skillUp: number | undefined;
689
+ if (!freeOpen && worker) skillUp = grantWorkerGatherSkillTaps(ctx, worker, node, rng, n);
690
+ if (!hold) refreshQuests(); // gathered reagents can satisfy a gather objective (held loot on take)
691
+ const names = yielded.map((l) => `${l.count}x ${ds.item(l.item)?.name ?? l.item}`).join(', ');
692
+ // Name the hand that worked it when it was not the player's — the skill point went there.
693
+ const by = worker?.compId != null ? ` (${worker.name})` : '';
694
+ log(`${node.name}${by}: ${names || 'nothing of use'}.${skillUp != null ? ` (${node.skill} → ${skillUp})` : ''}`);
695
+ return { ok: true, yielded, skillUp, ...(worker?.compId != null ? { workedBy: worker.name } : {}), ...(held ? { held } : {}) };
696
+ }
697
+
698
+ /** Export applyGather for game.ts fight-end auto-skin (professions must not import game.ts). */
699
+ export function createGatherHelpers(ctx: GameCtx, gate?: GatherLootGate): {
700
+ applyGather: (
701
+ node: GatherNodeDef,
702
+ rng?: Rng,
703
+ ) => ActionResult<{ yielded: LootDrop[]; skillUp?: number }>;
704
+ } {
705
+ return {
706
+ applyGather: (node, rng) =>
707
+ rng !== undefined
708
+ ? applyGatherImpl(ctx, node, rng, gate)
709
+ : applyGatherImpl(ctx, node, undefined, gate),
710
+ };
711
+ }
712
+
713
+ // Locals from createGame that interactObject needs but that are not on GameCtx (shared quest/loot condition
714
+ // helpers still owned by the facade).
715
+ export type ProfessionsExtras = {
716
+ standingFor: (factionId: number) => number;
717
+ trainerDiscountMod: (vendorEntry?: number) => number;
718
+ // Put a haul in the loot container (facade's loot module). Only used on the `{ hold: true }` path, so
719
+ // autopilot / bots / dungeon runs keep dropping straight into the bags.
720
+ openLoot: Game['openLoot'];
721
+ lootConditionSnapshot: () => {
722
+ quests: Record<string, 'none' | 'taken' | 'complete' | 'rewarded'>;
723
+ level: number;
724
+ /** SharedDefines.h Team: ALLIANCE=469, HORDE=67 — required for CONDITION_TEAM (type 6). */
725
+ team: number;
726
+ /** HonorRankInfo.rank 0..18. Cite: Conditions.cpp:236-243 CONDITION_PVP_RANK. */
727
+ pvpRank: number;
728
+ /** CONDITION_AD_COMMISSION_AURA (10). Cite: Conditions.cpp:228-234. */
729
+ auras?: number[];
730
+ /** CONDITION_WORLD_SCRIPT (40) — sWorldState.IsConditionFulfilled. */
731
+ worldScript: (conditionId: number, state: number) => boolean;
732
+ };
733
+ conditionTableById: () => ConditionTable;
734
+ /** Late-bound: travelCore pace + journeyTick after a tram (11) or boat/zep (15) depart. */
735
+ transportDepart: Type11DepartHooks;
736
+ };
737
+
738
+ export function createProfessionsModule(
739
+ ctx: GameCtx,
740
+ extras: ProfessionsExtras,
741
+ ): Pick<
742
+ Game,
743
+ | 'learnProfession'
744
+ | 'unlearnProfession'
745
+ | 'professionSpecGossip'
746
+ | 'selectProfessionSpec'
747
+ | 'ensureProfessionStarters'
748
+ | 'learnRecipe'
749
+ | 'learnRecipeFromItem'
750
+ | 'learnProfessionRankFromItem'
751
+ | 'gatherStatus'
752
+ | 'gather'
753
+ | 'skin'
754
+ | 'pickPocket'
755
+ | 'interactObject'
756
+ | 'openContainerItem'
757
+ | 'craft'
758
+ | 'craftAll'
759
+ | 'learnEnchant'
760
+ | 'learnEnchantFromItem'
761
+ | 'enchantItem'
762
+ | 'disenchant'
763
+ | 'disenchantStack'
764
+ | 'disenchantCompanionBagStack'
765
+ > {
766
+ const { ds, state, log, cap, refreshQuests } = ctx;
767
+ // The spell focus objects the crafter can reach, from the zone's baked list, so a refusal means "go
768
+ // to a town" rather than "dead recipe". Null for an unknown zone turns the gate off.
769
+ const reachableCraftFocus = (): ReadonlySet<number> | null => {
770
+ const zone = ds.zone(state.char.zoneId);
771
+ if (!zone) return null;
772
+ return new Set(zone.craftingFocus ?? []);
773
+ };
774
+ // "Not ready yet" carries no number, and a 48 h transmute wait is the one refusal the player has
775
+ // to be told the length of. Null when the recipe is off cooldown.
776
+ const craftRefusal = (recipe: RecipeDef, now: number, reason?: string): string => {
777
+ if (reason !== 'Not ready yet') return reason ?? 'Cannot craft';
778
+ return `Ready in ${craftCooldownLabel(recipeCooldownRemainingMs(recipe, state, now))}`;
779
+ };
780
+ /** Any body the player owns — squad seat, benched roster, or a raid seat. */
781
+ const findRosterCompanion = (id: string): Companion | undefined =>
782
+ (state.squad ?? []).find((c) => c.id === id)
783
+ ?? state.botRoster?.[id]
784
+ ?? state.raidRoster?.members?.[id];
785
+
786
+ // The party's held tools (hammer, spanner, rod). Never consumed, so one copy anywhere in the
787
+ // party's bags satisfies the gate for the player and every companion.
788
+ const partyTools = (): ReadonlySet<number> => partyHeldTools(state.inventory, [
789
+ ...(state.squad ?? []),
790
+ ...Object.values(state.botRoster ?? {}),
791
+ ], ds);
792
+
793
+ const { standingFor, lootConditionSnapshot, conditionTableById, openLoot, transportDepart } = extras;
794
+ // Condition gate for node loot rolls (gather / skin) — same snapshot interactObject uses.
795
+ const lootGate: GatherLootGate = { lootConditionSnapshot, conditionTableById };
796
+
797
+ const isBsTrainer = (entry: number) =>
798
+ (BS_BOUND_CREATURES as readonly number[]).includes(entry);
799
+ const isLwTrainer = (entry: number) =>
800
+ (LW_BOUND_CREATURES as readonly number[]).includes(entry);
801
+
802
+ const repRank = (factionId: number): number => {
803
+ const fac = ds.faction(factionId);
804
+ if (!fac) return REP_RANK.HATED;
805
+ return getRank(state.char.reputations, fac, state.char.race, state.char.cls);
806
+ };
807
+
808
+ const rewardedQuestIds = (): number[] =>
809
+ state.questlog.quests.filter((q) => q.status === 'turnedIn').map((q) => q.entry);
810
+
811
+ // The one disenchant body. `slot` names a specific bag stack (bulk confirm list); omitted, the first
812
+ // stack of `itemEntry` is taken (single click, cast-on-item). `holder` = a companion carrying the
813
+ // gear: it breaks down in their bags and the mats land in yours, since mats are never soulbound.
814
+ const disenchantOne = (
815
+ itemEntry: number,
816
+ slot: number | undefined,
817
+ opts?: { workerId?: string },
818
+ holder?: Companion,
819
+ ): ActionResult<{ yielded: LootDrop[]; skillUp?: number; worker?: string }> => {
820
+ const source = holder ? (holder.bags ?? { stacks: [] }) : state.inventory;
821
+ const enchanters = resolveRosterEnchanters(
822
+ state.char, state.squad, state.botRoster, state.raidRoster?.members, ds,
823
+ );
824
+ let who: DisenchantPick | undefined;
825
+ if (opts?.workerId) {
826
+ const only = enchanters.find((e) => e.kind === 'companion' && e.comp.id === opts.workerId);
827
+ if (!only) return { ok: false, reason: 'That companion is not available' };
828
+ const chk = partyDisenchantCheck(itemEntry, source, ds, [only]);
829
+ if (!chk.ok) return { ok: false, reason: chk.reason };
830
+ who = chk.who;
831
+ } else {
832
+ const chk = partyDisenchantCheck(itemEntry, source, ds, enchanters);
833
+ if (!chk.ok) return { ok: false, reason: chk.reason };
834
+ who = chk.who;
835
+ }
836
+ const comp = who.kind === 'companion' ? findRosterCompanion(who.id!) : undefined;
837
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
838
+ const res = runPartyDisenchant({
839
+ who, comp, player: state.char, inventory: state.inventory, ds, entry: itemEntry, rng,
840
+ cap: cap(), ...(slot != null ? { slot } : {}), ...(holder ? { holderBags: source } : {}),
841
+ });
842
+ if (!res.ok) return { ok: false, reason: res.reason };
843
+ state.char = res.char;
844
+ state.inventory = res.inventory;
845
+ if (holder && res.holderBags) holder.bags = res.holderBags;
846
+ if (comp && res.companionProfessions) comp.professions = res.companionProfessions;
847
+ refreshQuests(); // destroying gear can revert a quest that wanted it turned in
848
+ const name = ds.item(itemEntry)?.name ?? itemEntry;
849
+ const mats = res.yielded.map((y) => `${y.count}x ${ds.item(y.item)?.name ?? y.item}`).join(', ') || 'nothing';
850
+ const by = who.kind === 'companion' ? ` (${who.name})` : '';
851
+ log(`Disenchanted ${name}${by}: ${mats}.${res.skillUp ? ` (enchanting → ${res.skillUp})` : ''}`);
852
+ return {
853
+ ok: true,
854
+ yielded: res.yielded,
855
+ skillUp: res.skillUp,
856
+ ...(who.kind === 'companion' ? { worker: who.name } : {}),
857
+ };
858
+ };
859
+
860
+ return {
861
+ learnProfession(profTrainerEntry) {
862
+ const trainer = ds.profTrainer(profTrainerEntry);
863
+ if (!trainer) return { ok: false, reason: 'No trainer here' };
864
+ if (!canInteractWithCreature(ds, state.char.race, profTrainerEntry)) {
865
+ return { ok: false, reason: 'They refuse to deal with you.' };
866
+ }
867
+ const prof = ds.profession(trainer.skill);
868
+ if (!prof) return { ok: false, reason: 'Unknown profession' };
869
+ const firstRank = !state.char.skills[prof.id];
870
+ const discountMod = extras.trainerDiscountMod(profTrainerEntry);
871
+ const res = doTrainProfession(state.char, prof, { discountMod });
872
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot train' };
873
+ state.char = res.char;
874
+ log(`Trained ${prof.name} (skill ${state.char.skills[prof.id]?.current}/${state.char.skills[prof.id]?.max}).`);
875
+ // Apprentice starters (LearnOnGetSkill): free orange≤1 trainer recipes for the line.
876
+ // Cite: SkillLineAbility learnOnGetSkill; extract-professions tags these source:'trainer'.
877
+ if (firstRank) {
878
+ const starters = doGrantStarterRecipes(state.char, prof.id, ds.recipes());
879
+ if (starters.granted.length) {
880
+ state.char = starters.char;
881
+ log(`Learned: ${starters.granted.map((r) => r.name).join(', ')}.`);
882
+ }
883
+ }
884
+ // Fishing needs a pole to work a school; hand a basic one to a fresh angler who has
885
+ // none so training isn't a dead end (it's cheap and every fishing vendor sells it).
886
+ if (prof.id === 'fishing' && !hasFishingPole(ctx)) {
887
+ const POLE = 6256; // "Fishing Pole" - the plain starter rod
888
+ const pdef = ds.item(POLE);
889
+ if (pdef && canFit(state.inventory, POLE, 1, pdef.stackable ?? 1, cap())) {
890
+ state.inventory = addItem(state.inventory, POLE, 1, pdef.stackable ?? 1);
891
+ log('Received a Fishing Pole.');
892
+ }
893
+ }
894
+ return { ok: true };
895
+ },
896
+
897
+ unlearnProfession(skill) {
898
+ const prof = ds.profession(skill);
899
+ if (!prof) return { ok: false, reason: 'Unknown profession' };
900
+ const res = doUnlearnProfession(state.char, prof, ds.recipes());
901
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot unlearn' };
902
+ state.char = res.char;
903
+ log(`Unlearned ${prof.name}.`);
904
+ return { ok: true };
905
+ },
906
+
907
+ // Specialization gossip hello for ScriptDev-bound BS/LW trainers. Cite: npc_professions.cpp
908
+ // GossipHello_npc_prof_blacksmith / _leather.
909
+ professionSpecGossip(trainerEntry) {
910
+ if (isBsTrainer(trainerEntry)) {
911
+ const skill = state.char.skills.blacksmithing?.current ?? 0;
912
+ const hello = blacksmithGossipHello({
913
+ creatureEntry: trainerEntry,
914
+ skillBlacksmithing: skill,
915
+ level: state.char.level,
916
+ knownSpells: state.spellbook.known,
917
+ // Omni trainers gate learn on Friendly craft-faction rep.
918
+ // Cite: npc_professions.cpp:293-296 GetReputationRank == REP_FRIENDLY.
919
+ repArmorRank:
920
+ trainerEntry === N_TRAINER_SMITHOMNI1 || trainerEntry === N_TRAINER_SMITHOMNI2
921
+ ? repRank(REP_ARMOR)
922
+ : 0,
923
+ repWeaponRank:
924
+ trainerEntry === N_TRAINER_SMITHOMNI1 || trainerEntry === N_TRAINER_SMITHOMNI2
925
+ ? repRank(REP_WEAPON)
926
+ : 0,
927
+ isTrainer: true,
928
+ isVendor: !!ds.vendor(trainerEntry),
929
+ });
930
+ return {
931
+ script: 'npc_prof_blacksmith' as const,
932
+ items: hello.items.map((i) => ({
933
+ kind: i.kind,
934
+ action: i.action,
935
+ cite: i.cite,
936
+ })),
937
+ cite: hello.cite,
938
+ };
939
+ }
940
+ if (isLwTrainer(trainerEntry)) {
941
+ const skill = state.char.skills.leatherworking?.current ?? 0;
942
+ const hello = leatherGossipHello({
943
+ creatureEntry: trainerEntry,
944
+ skillLeatherworking: skill,
945
+ level: state.char.level,
946
+ knownSpells: state.spellbook.known,
947
+ questRewarded: rewardedQuestIds(),
948
+ isTrainer: true,
949
+ isVendor: !!ds.vendor(trainerEntry),
950
+ });
951
+ return {
952
+ script: 'npc_prof_leather' as const,
953
+ items: hello.items.map((i) => {
954
+ if (
955
+ i.kind === 'unlearn_dragon' ||
956
+ i.kind === 'unlearn_elemental' ||
957
+ i.kind === 'unlearn_tribal'
958
+ ) {
959
+ return {
960
+ kind: i.kind,
961
+ action: i.action,
962
+ costCopper: i.costCopper,
963
+ cite: i.cite,
964
+ };
965
+ }
966
+ return { kind: i.kind, action: i.action, cite: i.cite };
967
+ }),
968
+ cite: hello.cite,
969
+ };
970
+ }
971
+ return {
972
+ script: null,
973
+ items: [],
974
+ cite: 'no npc_prof_blacksmith/leather binding for entry',
975
+ };
976
+ },
977
+
978
+ // Apply specialization gossip select — mutates spellbook / gold / knownRecipes. Cite:
979
+ // npc_professions.cpp SendActionMenu; Spell.sql Effect 36 → EffectTriggerSpell.
980
+ selectProfessionSpec(trainerEntry, opts) {
981
+ if (isBsTrainer(trainerEntry)) {
982
+ const applied = applyBlacksmithSpecSelect({
983
+ sender: opts.sender,
984
+ action: opts.action,
985
+ level: state.char.level,
986
+ knownSpells: state.spellbook.known,
987
+ creatureEntry: trainerEntry,
988
+ moneyCopper: state.char.gold,
989
+ });
990
+ if (!applied.ok) return { ok: false, reason: applied.reason };
991
+ consumeProfessionSpecKind(applied, state, trainerEntry);
992
+ return {
993
+ ok: true,
994
+ phase: applied.phase,
995
+ kind: applied.kind ?? undefined,
996
+ grantedSpellId: applied.grantedSpellId ?? undefined,
997
+ removedSpellId: applied.removedSpellId ?? undefined,
998
+ confirmCostCopper: applied.confirmCostCopper ?? undefined,
999
+ knownSpecializationSpells: knownProfessionSpecs(state.spellbook.known),
1000
+ blacksmithUnlearnGap: applied.blacksmithUnlearnGap,
1001
+ openTrainer: applied.openTrainer,
1002
+ openVendor: applied.openVendor,
1003
+ };
1004
+ }
1005
+ if (isLwTrainer(trainerEntry)) {
1006
+ const applied = applyLeatherSpecSelect({
1007
+ sender: opts.sender,
1008
+ action: opts.action,
1009
+ level: state.char.level,
1010
+ moneyCopper: state.char.gold,
1011
+ knownSpells: state.spellbook.known,
1012
+ equippedOk: opts.equippedOk !== false,
1013
+ });
1014
+ if (!applied.ok) return { ok: false, reason: applied.reason };
1015
+ if (applied.phase === 'error_money') {
1016
+ return { ok: false, reason: 'Not enough money' };
1017
+ }
1018
+ if (applied.phase === 'error_equipped') {
1019
+ return { ok: false, reason: 'Unequip specialty gear first' };
1020
+ }
1021
+ consumeProfessionSpecKind(applied, state, trainerEntry);
1022
+ return {
1023
+ ok: true,
1024
+ phase: applied.phase,
1025
+ kind: applied.kind ?? undefined,
1026
+ grantedSpellId: applied.grantedSpellId ?? undefined,
1027
+ removedSpellId: applied.removedSpellId ?? undefined,
1028
+ confirmCostCopper: applied.confirmCostCopper ?? undefined,
1029
+ knownSpecializationSpells: knownProfessionSpecs(state.spellbook.known),
1030
+ openTrainer: applied.openTrainer,
1031
+ openVendor: applied.openVendor,
1032
+ };
1033
+ }
1034
+ return { ok: false, reason: 'Not a specialization trainer' };
1035
+ },
1036
+
1037
+ // Repair path for saves trained before starter auto-grant: free orange≤1 trainer recipes for every
1038
+ // profession the character already has. Idempotent.
1039
+ ensureProfessionStarters() {
1040
+ let any = false;
1041
+ for (const prof of ds.professions()) {
1042
+ if (!state.char.skills[prof.id]) continue;
1043
+ const starters = doGrantStarterRecipes(state.char, prof.id, ds.recipes());
1044
+ if (starters.granted.length) {
1045
+ state.char = starters.char;
1046
+ any = true;
1047
+ }
1048
+ }
1049
+ return { ok: true as const, granted: any };
1050
+ },
1051
+
1052
+ learnRecipe(recipeId, trainerEntry) {
1053
+ const recipe = ds.recipe(recipeId);
1054
+ if (!recipe) return { ok: false, reason: 'Unknown recipe' };
1055
+ const discountMod = extras.trainerDiscountMod(trainerEntry);
1056
+ const res = doLearnRecipe(state.char, recipe, { discountMod });
1057
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot learn' };
1058
+ state.char = res.char;
1059
+ const spent = Math.floor((recipe.cost ?? 0) * discountMod);
1060
+ log(`Learned recipe: ${recipe.name}${spent ? ` for ${moneyMarked(spent)}` : ''}.`);
1061
+ return { ok: true };
1062
+ },
1063
+
1064
+ // Read a recipe item out of the bags: learn what it teaches, then consume it. The item gates class
1065
+ // and level; the recipe's orange skill still gates crafting, as in WoW.
1066
+ learnRecipeFromItem(itemEntry) {
1067
+ const it = ds.item(itemEntry);
1068
+ // Resolve through the shared teach intent, not the item's own field: seven obtainable recipe
1069
+ // items have the recipe row baked but no field, and were unlearnable for that reason alone.
1070
+ const teach = itemTeachIntent(it, ds);
1071
+ if (teach?.kind === 'unavailable') return { ok: false, reason: unavailableTeachReason(teach.what) };
1072
+ if (!it || teach?.kind !== 'recipe') return { ok: false, reason: 'That item teaches no recipe' };
1073
+ if (it.allowClass && !it.allowClass.includes(state.char.cls)) return { ok: false, reason: 'Your class cannot learn this' };
1074
+ if (it.requiredLevel > state.char.level) return { ok: false, reason: `Requires level ${it.requiredLevel}` };
1075
+ if (countItem(state.inventory, itemEntry) <= 0) return { ok: false, reason: "You don't have that recipe" };
1076
+ const recipe = ds.recipe(teach.id);
1077
+ if (!recipe) return { ok: false, reason: 'That recipe is unknown' };
1078
+ // The learn call checks the profession is trained and refuses a duplicate. Cost is 0 on a vendor
1079
+ // or drop recipe, and `fromItem` skips the trainer's orange-skill gate — crafting still needs it.
1080
+ const res = doLearnRecipe(state.char, recipe, { fromItem: true });
1081
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot learn' };
1082
+ state.char = res.char;
1083
+ state.inventory = removeItem(state.inventory, itemEntry, 1)[0];
1084
+ log(`You study ${it.name} and learn to craft ${recipe.name}.`);
1085
+ return { ok: true, recipeId: recipe.id, name: recipe.name };
1086
+ },
1087
+
1088
+ gatherStatus(nodeId, nowMs) {
1089
+ return computeGatherStatus(ctx, nodeId, nowMs);
1090
+ },
1091
+
1092
+ gather(nodeId, nowMs = 0, opts) {
1093
+ // Same Use gate as interactObject / goTransportUse — ghosts cannot farm nodes.
1094
+ // Cite: domain/run/death.ts refuseIfDead; goTransportUse.ts:24; Spell.cpp:4586.
1095
+ const deadGather = refuseIfDead(state.char.health, state.deathFlow);
1096
+ if (deadGather) return deadGather;
1097
+ const node = ds.gatherNode(nodeId);
1098
+ if (!node) return { ok: false, reason: 'Nothing to gather' };
1099
+ if (node.skill === 'fishing' && !wearFishingPole(ctx)) {
1100
+ return { ok: false, reason: 'Requires a Fishing Pole' };
1101
+ }
1102
+ const status = computeGatherStatus(ctx, nodeId, nowMs);
1103
+ if (!status.ok) return { ok: false, reason: status.reason ?? 'Cannot gather' };
1104
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
1105
+ const mining = node.skill === 'mining';
1106
+ let taps = 1;
1107
+ if (mining) {
1108
+ const picked = pickPartyWorker(gatherWorkers(ctx, node).candidates);
1109
+ taps = miningTapsFor(ctx, node, picked?.skill ?? 0, rng);
1110
+ }
1111
+ const startCharges = mining ? taps : startGatherCharges(ctx, node, nodeId, rng);
1112
+ const res = applyGatherImpl(ctx, node, rng, lootGate, opts?.hold
1113
+ ? (drops) => openLoot({
1114
+ kind: gatherLootKind(node.skill),
1115
+ title: node.name,
1116
+ icon: ds.profession(node.skill)?.icon,
1117
+ }, drops)
1118
+ : undefined, taps);
1119
+ if (!res.ok) return res;
1120
+ // Mining spends every refill in this harvest; herbs/fish still tick one charge.
1121
+ const spent = mining ? startCharges : 1;
1122
+ const { remaining, depleted, respawnMs } = commitGatherNodeCharge(
1123
+ ctx, node, nodeId, nowMs, startCharges, spent,
1124
+ );
1125
+ if (depleted) log(`The ${node.name} is depleted.`);
1126
+ return { ...res, depleted, chargesLeft: remaining, respawnMs };
1127
+ },
1128
+
1129
+ skin(opts) {
1130
+ if (!state.corpse) return { ok: false, reason: 'No corpse to skin' };
1131
+ const node = ds.gatherNode(state.corpse.node);
1132
+ if (!node) { state.corpse = null; return { ok: false, reason: 'Nothing to skin' }; }
1133
+ // Skill AND knife over the whole GROUP, the same gate a vein is worked under: a party
1134
+ // skinner's knife counts as the group's. Refused before apply so a missing knife never
1135
+ // clears the corpse or rolls skill.
1136
+ const refusal = gatherGroupRefusal(ctx, node);
1137
+ if (refusal) return { ok: false, reason: refusal };
1138
+ const creatureEntry = state.corpse.creatureEntry;
1139
+ const res = applyGatherImpl(ctx, node, undefined, lootGate, opts?.hold
1140
+ ? (drops) => openLoot({
1141
+ kind: 'skinning',
1142
+ title: `Skinned ${ds.creature(creatureEntry)?.name ?? 'the corpse'}`,
1143
+ icon: ds.profession('skinning')?.icon,
1144
+ creatureEntry,
1145
+ }, drops)
1146
+ : undefined);
1147
+ if (res.ok) state.corpse = null; // the corpse is consumed whether or not leather dropped
1148
+ return res;
1149
+ },
1150
+
1151
+ pickPocket(creatureEntry, opts) {
1152
+ // FI7: pure canPickPocket + rollPickPocket; loot via ds.lootTable (same path as corpse loot).
1153
+ // pickpocketLootId is NOT on the live CreatureDef bake — prefer opts / optional def field.
1154
+ const cdef = ds.creature(creatureEntry);
1155
+ if (!cdef) return { ok: false, reason: 'No such creature' };
1156
+
1157
+ const nowMs = opts?.nowMs ?? Date.now();
1158
+ const spawnKey = `${state.char.zoneId}:${creatureEntry}`;
1159
+ if (!canRestockPickpocket(state.pickpocketRestock, spawnKey, nowMs)) {
1160
+ return { ok: false, reason: 'Already pickpocketed' };
1161
+ }
1162
+
1163
+ // Player-only: Pick Pocket is reached by casting, and OOC casting is the player's spellbook
1164
+ // alone. A group path here would have no surface to click.
1165
+ const stealth = activeStealthAura(state.char, ds, nowMs);
1166
+ const gate = canPickPocket({
1167
+ cls: state.char.cls,
1168
+ targetAlive: true, // OOC pickpocket; dead targets use corpse loot instead
1169
+ stealthOk: stealth != null,
1170
+ });
1171
+ if (!gate.ok) return { ok: false, reason: gate.reason };
1172
+
1173
+ // The loot key is the baked pickpocket id when there is one, else the live per-entry key.
1174
+ // Cite: Spell.cpp CheckCast TARGET_NO_POCKETS when the id is 0.
1175
+ const defLoot = (cdef as { pickpocketLootId?: number }).pickpocketLootId;
1176
+ const lootId: number | string = (defLoot != null && defLoot > 0)
1177
+ ? defLoot
1178
+ : (opts?.pickpocketLootId != null && opts.pickpocketLootId > 0)
1179
+ ? opts.pickpocketLootId
1180
+ : pickpocketLootKey(creatureEntry);
1181
+ // Spell.cpp: no PickpocketLootId → TARGET_NO_POCKETS (unknown/missing table).
1182
+ {
1183
+ const probe = ds.lootTable(lootId);
1184
+ if (probe.length === 0 && typeof lootId === 'string') {
1185
+ return { ok: false, reason: 'Target has no pockets' };
1186
+ }
1187
+ if (typeof lootId === 'number' && lootId <= 0) {
1188
+ return { ok: false, reason: 'Target has no pockets' };
1189
+ }
1190
+ }
1191
+
1192
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
1193
+ // "Failure is handled through normal spell resist": 921 is DmgClass MAGIC with neither
1194
+ // NO_AVOIDANCE nor ALWAYS_HIT, so it takes the ordinary binary-magic die — 4% at even level,
1195
+ // +1%/level to +2 and +11%/level beyond. A resist runs no effect, so no loot object is made
1196
+ // and the pocket is NOT spent; the mob notices you. Cite: SpellEffects.cpp:2673;
1197
+ // Unit.cpp:2858 MagicSpellHitResult; Unit.cpp:3841 CalculateSpellMissChance.
1198
+ const missPct = spellMissChancePercent({
1199
+ atkLevel: state.char.level,
1200
+ defLevel: cdef.level,
1201
+ victimPlayerControlled: false,
1202
+ });
1203
+ if (rng.next() * 100 < missPct) {
1204
+ log(`Pick Pocket (${cdef.name}): failed.`);
1205
+ return { ok: false, reason: PICKPOCKET_ERR.FAILED, aggro: true };
1206
+ }
1207
+ // EffectPickPocket itself has no skill roll — the die above is the only way it fails, and the
1208
+ // loot opens whenever the cast lands.
1209
+ const rolled = rollPickPocket({
1210
+ pickpocketLootId: lootId,
1211
+ getTable: (id) => {
1212
+ const rows = ds.lootTable(id);
1213
+ // Our loot lookup returns an empty array for a missing key as well as a present empty one,
1214
+ // so treat empty as undefined to keep the no-table and empty-table cases apart.
1215
+ return rows.length > 0 ? rows : undefined;
1216
+ },
1217
+ rng,
1218
+ playerLevel: state.char.level,
1219
+ targetLevel: cdef.level,
1220
+ skipSkillCheck: true,
1221
+ });
1222
+ if (!rolled.ok) {
1223
+ // Pure skill-fail (when enabled) would set aggro; with skipSkillCheck this is table errors.
1224
+ return {
1225
+ ok: false,
1226
+ reason: rolled.reason,
1227
+ ...(rolled.aggro ? { aggro: true as const } : {}),
1228
+ };
1229
+ }
1230
+
1231
+ // Grant item drops into bags (same stack path as other OOC loot).
1232
+ const granted: LootDrop[] = [];
1233
+ for (const d of rolled.drops) {
1234
+ const maxStack = ds.item(d.item)?.stackable ?? 1;
1235
+ if (!canFit(state.inventory, d.item, d.count, maxStack, cap())) continue;
1236
+ state.inventory = addItem(state.inventory, d.item, d.count, maxStack);
1237
+ granted.push(d);
1238
+ }
1239
+ if (rolled.goldCopper > 0) {
1240
+ state.char = { ...state.char, gold: state.char.gold + rolled.goldCopper };
1241
+ }
1242
+ state.pickpocketRestock = startPickpocketRestock(state.pickpocketRestock, spawnKey, nowMs);
1243
+
1244
+ const names = granted.map((l) => `${l.count}x ${ds.item(l.item)?.name ?? l.item}`).join(', ');
1245
+ const goldNote = rolled.goldCopper > 0 ? ` +${moneyMarked(rolled.goldCopper)}` : '';
1246
+ log(`Pick Pocket (${cdef.name}): ${names || 'nothing'}${goldNote}.`);
1247
+ // Spell 921 (PICK_POCKET_SPELL_ID) is the classic ability; this is the OOC action wire.
1248
+ void PICK_POCKET_SPELL_ID;
1249
+ return { ok: true, drops: granted, goldCopper: rolled.goldCopper, aggro: false };
1250
+ },
1251
+
1252
+ interactObject(objectEntry, opts) {
1253
+ // Pure useGameObject plus canOpenLock, with the free-open cases and the key-consume rule. `opts`
1254
+ // may be a bare clock number. A dead or ghost player cannot Use a world GO.
1255
+ const dead = refuseIfDead(state.char.health, state.deathFlow);
1256
+ if (dead) return dead;
1257
+ const hold = typeof opts === 'number' ? undefined : opts?.hold;
1258
+ const nowMs = typeof opts === 'number'
1259
+ ? opts
1260
+ : (opts?.nowMs ?? Date.now());
1261
+ const go = ds.gameObject(objectEntry);
1262
+ if (!go) return { ok: false, reason: 'No such object here' };
1263
+
1264
+ // A fishing hole is reached by a Fishing cast with a pole, and interactObject is that surface
1265
+ // here, so require the skill line and the pole before any loot. Cite: GameObject.cpp:1701-1727.
1266
+ if (isGoFishingSchool(go)) {
1267
+ if (state.char.skills?.fishing == null) {
1268
+ return { ok: false, reason: 'Requires fishing' };
1269
+ }
1270
+ if (!wearFishingPole(ctx)) {
1271
+ return { ok: false, reason: 'Requires a Fishing Pole' };
1272
+ }
1273
+ }
1274
+
1275
+ const incompleteQuestIds = state.questlog.quests
1276
+ .filter((q) => q.status === 'active')
1277
+ .map((q) => q.entry);
1278
+ const lockDef = (go.lockId != null ? ds.lock(go.lockId) : undefined) as LockDef | undefined;
1279
+ const skillValueFor = (lockType: number) => skillValueForLockType(ctx, lockType);
1280
+
1281
+ // Herb and mine GOs share gather()'s profession and tool gates: the lock's own skill requirement
1282
+ // is often 0. A free-open lock case is an OR, so the profession gate does not apply then.
1283
+ const gatherNode = gatherNodeForGameObject(ctx, objectEntry);
1284
+ if (gatherNode) {
1285
+ if (!lockHasFreeOpenCase(lockDef)) {
1286
+ const gateReason = gatherProfessionGateReason(ctx, gatherNode);
1287
+ if (gateReason) return { ok: false, reason: gateReason };
1288
+ }
1289
+ // KI-807: share nodeState depletion with gather() (same zone:entry key + clock).
1290
+ // Depletion binds even on a free-open lock — a consumed GO has nothing left to give.
1291
+ const nKey = nodeKey(state.char.zoneId, objectEntry);
1292
+ const st = state.nodeState[nKey];
1293
+ if (!rotationNodeUp(state.char.zoneId, objectEntry, nowMs)) {
1294
+ return { ok: false, reason: 'Not in bloom' };
1295
+ }
1296
+ if (!nodeReady(st, nowMs)) return { ok: false, reason: 'Depleted' };
1297
+ const avail = goRuntimeAvailability(state.goRuntime?.[nKey], nowMs);
1298
+ if (avail.kind === 'consumed' || avail.kind === 'restock') {
1299
+ return {
1300
+ ok: false,
1301
+ reason: goRefuseReason('not_ready', {
1302
+ restockAtMs: avail.kind === 'restock' ? avail.readyAtMs : Number.MAX_SAFE_INTEGER,
1303
+ nowMs,
1304
+ }),
1305
+ };
1306
+ }
1307
+ }
1308
+
1309
+ // Prefer a free or skill open so a key is not burned when Use does not need one; pick the key
1310
+ // item only when the lock fails without it. Cite: Spell.cpp CanOpenLock.
1311
+ let keyItemEntry: number | undefined;
1312
+ if (lockDef) {
1313
+ const openFree = canOpenLock({ lock: lockDef, skillValueFor });
1314
+ if (!openFree.ok) {
1315
+ // Bags + keyring: keys are BagFamily KEYS and live off the bags by design, so this
1316
+ // read found nothing and every keyed world object answered "locked".
1317
+ const held = ownedInventory(state.inventory, state.keyring);
1318
+ keyItemEntry =
1319
+ go.keyId != null && countItem(held, go.keyId) > 0
1320
+ ? go.keyId
1321
+ : matchingKeyItemEntry(
1322
+ lockDef,
1323
+ (entry) => countItem(held, entry) > 0,
1324
+ );
1325
+ }
1326
+ }
1327
+
1328
+ const rtKey = goRuntimeKey(ctx, objectEntry);
1329
+ // Gather nodes: nodeState already said ready — strip a stale permanent goRuntime mark
1330
+ // so useChest restock gate does not block a legitimate respawn open.
1331
+ let runtime = state.goRuntime?.[rtKey];
1332
+ if (gatherNode && runtime?.readyAtMs != null && runtime.readyAtMs > nowMs) {
1333
+ // Should be unreachable after gates above; keep useGameObject input honest.
1334
+ runtime = { ...runtime, readyAtMs: nowMs };
1335
+ }
1336
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
1337
+ // Refuse quest-only GO loot before the Use, so a pre-accept click never permanently despawns
1338
+ // the instance with an empty yield. A questgiver Use is a dialog examine, not a loot open.
1339
+ {
1340
+ const endsActiveQuest =
1341
+ (go.questsEnded ?? []).some((qid) => incompleteQuestIds.includes(qid)) ||
1342
+ state.questlog.quests.some((qs) => {
1343
+ if (qs.status !== 'active') return false;
1344
+ const qd = ds.quest(qs.entry);
1345
+ if (!qd) return false;
1346
+ return qd.goEnder === objectEntry || (qd.goEnderEntries ?? []).includes(objectEntry);
1347
+ });
1348
+ const skipLootGate =
1349
+ go.type === GAMEOBJECT_TYPE_QUESTGIVER || endsActiveQuest;
1350
+ const preLootId = go.lootId;
1351
+ if (!skipLootGate && preLootId != null && preLootId !== 0) {
1352
+ const preTable = ds.lootTable(`go:${preLootId}`);
1353
+ if (goLootIsQuestOnly(preTable)) {
1354
+ const needed = questNeededItems(state.questlog, (e) => ds.quest(e));
1355
+ const anyNeeded = preTable.some((e) => needed.has(e.item));
1356
+ if (!anyNeeded) {
1357
+ return { ok: false, reason: goRefuseReason('quest_incomplete') };
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ // Calendar game_event snapshot is spawn-side only; goober/chest eventId is fire-on-use.
1363
+ const activeEvents = activeEventIds(ds.gameEvents(), nowMs);
1364
+ const ritualParty = ritualPartyUserIds(state.squad?.length ?? 0);
1365
+ const ms = go.meetingstone;
1366
+ const msTarget = go.type === GAMEOBJECT_TYPE_MEETINGSTONE
1367
+ ? meetingStoneGroupTarget(
1368
+ state.squad,
1369
+ state.char.level,
1370
+ ms?.minLevel ?? 0,
1371
+ ms?.maxLevel ?? 0,
1372
+ )
1373
+ : undefined;
1374
+ const goUseCtx = {
1375
+ nowMs,
1376
+ rng,
1377
+ lock: lockDef,
1378
+ keyItemEntry,
1379
+ skillValueFor,
1380
+ incompleteQuestIds,
1381
+ runtime,
1382
+ activeEventIds: activeEvents,
1383
+ // Fishing: skill from character; zoneSkill from bake (zones.mjs:203). Omit when absent → missing_context.
1384
+ fishingSkill: state.char.skills?.fishing?.current,
1385
+ zoneSkill: ds.zone(state.char.zoneId)?.zoneSkill,
1386
+ userId: 'player',
1387
+ uniqueUserIds: runtime?.uniqueUsers,
1388
+ firstUser: runtime?.firstUser,
1389
+ delayedActionUntilMs: runtime?.delayedActionUntilMs,
1390
+ inGroup: ritualParty.length > 1,
1391
+ goSpellId: state.wildSummonedObjects?.find((w) => w.goEntry === objectEntry)?.spellId,
1392
+ ...meetingStoneUseCtx(state.char.level, msTarget),
1393
+ };
1394
+ let used = go.type === GAMEOBJECT_TYPE_SUMMONING_RITUAL
1395
+ ? useSummoningRitualParty(go, { ...goUseCtx, partyUserIds: ritualParty })
1396
+ : useGameObject(go, goUseCtx);
1397
+ // SPELL_FOCUS Use is TriggerLinkedGameObject — consume linkedTrapId here (GameObject.cpp:1569-1575).
1398
+ if (used.ok && used.kind === 'spell_focus') {
1399
+ used = applyLinkedTrap(used, ds.gameObject(used.linkedTrapId ?? 0), { nowMs });
1400
+ }
1401
+ // A board is a journey depart, not a loot/quest interact — return before the GO tail.
1402
+ if (used.ok && used.kind === 'transport') {
1403
+ return applyType11TransportUse(ctx, go, nowMs, transportDepart);
1404
+ }
1405
+ if (used.ok && used.kind === 'mo_transport') {
1406
+ return applyType15MoTransportUse(ctx, go, nowMs, transportDepart);
1407
+ }
1408
+ if (!used.ok) {
1409
+ const keyEntry = go.keyId ?? requiredKeyItemEntry(lockDef);
1410
+ const keyName = keyEntry != null ? (ds.item(keyEntry)?.name ?? `item ${keyEntry}`) : undefined;
1411
+ const hard = hardestSkillLockCase(lockDef);
1412
+ const skillNeed = hard
1413
+ ? `${hard.skillName} ${hard.skillReq}`
1414
+ : undefined;
1415
+ return {
1416
+ ok: false,
1417
+ reason: goRefuseReason(used.reason, {
1418
+ restockAtMs: used.restockAtMs ?? runtime?.readyAtMs,
1419
+ nowMs,
1420
+ keyName: used.reason === 'locked' ? keyName : undefined,
1421
+ skillNeed: used.reason === 'low_skill' ? skillNeed : undefined,
1422
+ }),
1423
+ };
1424
+ }
1425
+
1426
+ // SPELLCASTER: SpellStart on user, then AddUse. Cite: GameObject.cpp:1962–1970.
1427
+ if (used.kind === 'spellcaster' && used.castSpellId) {
1428
+ const applied = applyGoSpellcasterCast(used.castSpellId, state, ds, nowMs);
1429
+ if (!applied.ok) return { ok: false, reason: applied.reason };
1430
+ }
1431
+
1432
+ // GOOBER data10 casts on the user at Use, the same helper as a spellcaster's charge.
1433
+ // Cite: GameObject.cpp:1645 spellCaster->CastSpell(player, spellId).
1434
+ const gooberSpellId = used.kind === 'goober' ? (used.castSpellId ?? 0) : 0;
1435
+ if (gooberSpellId > 0) applyGoSpellcasterCast(gooberSpellId, state, ds, nowMs);
1436
+
1437
+ // MEETINGSTONE Use casts 23598 (player caster) — GameObject.cpp:1888–1890, :1955–1973.
1438
+ if (used.kind === 'meetingstone' && used.ok && (used.castSpellId ?? 0) > 0) {
1439
+ const portal = placeMeetingStoneSummon(ds, nowMs, used.castSpellId);
1440
+ if (portal) {
1441
+ state.wildSummonedObjects = mergeWildSummoned(state.wildSummonedObjects, [portal], nowMs);
1442
+ const portalName = ds.gameObject(portal.goEntry)?.name ?? `object ${portal.goEntry}`;
1443
+ log(`You summon ${portalName}.`);
1444
+ }
1445
+ }
1446
+
1447
+ // Persist restock / trap / door runtime bookkeeping.
1448
+ if (used.restockAtMs != null || used.trapUses != null || used.trapCooldownUntilMs != null || used.state) {
1449
+ const nextRt: GoRuntimeState = {
1450
+ ...(runtime ?? {}),
1451
+ readyAtMs: used.restockAtMs ?? runtime?.readyAtMs,
1452
+ uses: used.trapUses ?? runtime?.uses,
1453
+ cooldownUntilMs: used.trapCooldownUntilMs ?? runtime?.cooldownUntilMs,
1454
+ open: used.state === 'activated' ? true : used.state === 'ready' ? false : runtime?.open,
1455
+ };
1456
+ state.goRuntime = { ...(state.goRuntime ?? {}), [rtKey]: nextRt };
1457
+ }
1458
+ if (used.kind === 'summoning_ritual') {
1459
+ const applied = applySummoningRitualUse({
1460
+ used,
1461
+ go,
1462
+ char: state.char,
1463
+ questlog: state.questlog,
1464
+ goRuntime: state.goRuntime,
1465
+ rtKey,
1466
+ nowMs,
1467
+ spellOf: (id) => ds.spell(id),
1468
+ eventScripts: (id) => ds.eventScripts(id),
1469
+ log,
1470
+ });
1471
+ state.char = applied.char;
1472
+ state.questlog = applied.questlog;
1473
+ state.goRuntime = applied.goRuntime;
1474
+ }
1475
+
1476
+ // The ScriptDev GO-use host, the player-reachable path for it. It must run after the lock and
1477
+ // Use succeed, so a locked door or postbox still needs its key.
1478
+ const sdIr = scriptDevByGo(objectEntry);
1479
+ if (sdIr?.goUse) {
1480
+ runScriptDevGoUse(sdIr, state, ds, {
1481
+ nowMs,
1482
+ goRuntimeKey: (entry) => goRuntimeKey(ctx, entry),
1483
+ log,
1484
+ usedGoEntry: objectEntry,
1485
+ });
1486
+ }
1487
+ if (used.kind === 'mailbox') {
1488
+ log(`Opened ${go.name}.`);
1489
+ return { ok: true, yielded: [], mailboxOpen: true, mailbox: state.mailbox };
1490
+ }
1491
+ // KI-807: herb/mine GOs share gather() charge/respawn — never permanent-despawn here.
1492
+ // commitGatherNodeCharge after loot/skill-up owns goRuntime for those nodes.
1493
+ if (used.despawn && !(gatherNode && (gatherNode.skill === 'herbalism' || gatherNode.skill === 'mining'))) {
1494
+ // Consumable despawn (GameObject.h:377). Restock the de-duped proxy only for objectTargets
1495
+ // or exclusive quest-container rows, not world-chest group loot (Solid Chest / Gold Bar).
1496
+ let stillNeeded = 0;
1497
+ const needLootId = used.lootId ?? go.lootId;
1498
+ const needLootItems = new Set(
1499
+ needLootId != null && needLootId !== 0
1500
+ ? ds.lootTable(`go:${needLootId}`).filter((e) => isExclusiveQuestLootRow(e)).map((e) => e.item)
1501
+ : [],
1502
+ );
1503
+ for (const qs of state.questlog.quests) {
1504
+ if (qs.status !== 'active') continue;
1505
+ const qd = ds.quest(qs.entry);
1506
+ if (!qd) continue;
1507
+ // objectTargets multi-count: credit for this use lands just below.
1508
+ const ot = qd.objectTargets?.find((t) => t.objectEntry === objectEntry);
1509
+ if (ot) {
1510
+ const cur = (qs.objects || {})[String(objectEntry)] ?? 0;
1511
+ if (cur + 1 < ot.count) stillNeeded = Math.max(stillNeeded, ot.count - cur - 1);
1512
+ }
1513
+ // itemTargets / sourceItemTargets multi-count from this GO's loot. Inventory is pre-grant; treat
1514
+ // this open as +1 so the final needed open permanent-despawns after filling the stack.
1515
+ for (const it of [...(qd.itemTargets || []), ...(qd.sourceItemTargets || [])]) {
1516
+ if (!needLootItems.has(it.item)) continue;
1517
+ const have = countItem(state.inventory, it.item);
1518
+ if (have + 1 < it.count) stillNeeded = Math.max(stillNeeded, it.count - have - 1);
1519
+ }
1520
+ }
1521
+ if (stillNeeded > 0) {
1522
+ // Another concurrent pile of the same template is already up, so this is ready immediately:
1523
+ // the goober and chest timers are per-instance reuse, not inter-pile delays.
1524
+ state.goRuntime = {
1525
+ ...(state.goRuntime ?? {}),
1526
+ [rtKey]: { ...(state.goRuntime?.[rtKey] ?? {}), readyAtMs: nowMs },
1527
+ };
1528
+ } else {
1529
+ // Consumable fully done for quests: mark not_ready far in the future.
1530
+ state.goRuntime = {
1531
+ ...(state.goRuntime ?? {}),
1532
+ [rtKey]: { ...(state.goRuntime?.[rtKey] ?? {}), readyAtMs: Number.MAX_SAFE_INTEGER },
1533
+ };
1534
+ }
1535
+ }
1536
+
1537
+ // A questgiver may also carry a loot id. Container tables are keyed `go:<lootId>`, since a bare
1538
+ // id collides with creature loot; quest-required and condition rows are gated as usual.
1539
+ const rawLootId = used.lootId ?? go.lootId;
1540
+ const goLootTable = rawLootId != null && rawLootId !== 0
1541
+ ? ds.lootTable(`go:${rawLootId}`)
1542
+ : [];
1543
+ const goLootCtx = {
1544
+ // Player.cpp:13710 HasQuestForItem — skip once GiveQuestSourceItemIfNeed
1545
+ // already topped up SrcItem==ReqItem (accept + GO must not stack 2).
1546
+ allowsQuestDrop: (entry: { item: number }) =>
1547
+ hasQuestForItem(state.questlog, (e) => ds.quest(e), state.inventory, entry.item,
1548
+ (i) => ds.item(i)?.stackable),
1549
+ allowsCondition: (conditionId: number) =>
1550
+ isConditionSatisfied(conditionId, lootConditionSnapshot(), conditionTableById()).ok,
1551
+ };
1552
+ let miningTaps = 1;
1553
+ let miningWorker: PartyWorker | undefined;
1554
+ if (gatherNode?.skill === 'mining') {
1555
+ miningWorker = pickPartyWorker(gatherWorkers(ctx, gatherNode).candidates);
1556
+ miningTaps = miningTapsFor(ctx, gatherNode, miningWorker?.skill ?? 0, rng);
1557
+ }
1558
+ const yielded: LootDrop[] = goLootTable.length
1559
+ ? mergeLootDrops(Array.from({ length: miningTaps }, () => rollLoot(goLootTable, rng, goLootCtx)).flat())
1560
+ : [];
1561
+ // HOLD: the object keeps its contents until the player clicks them (the loot window rolls
1562
+ // each item's identity as it fills, so taking never re-rolls a suffix).
1563
+ let heldLoot: PendingLoot | undefined;
1564
+ if (hold) {
1565
+ heldLoot = openLoot({
1566
+ kind: goLootKind(go),
1567
+ title: go.name,
1568
+ keyUsed: keyItemEntry != null && go.lockId != null,
1569
+ }, yielded);
1570
+ } else {
1571
+ for (const y of yielded) {
1572
+ const def = ds.item(y.item);
1573
+ if (!def) continue;
1574
+ // Wave Q P2: GO container loot is a new-copy create site.
1575
+ // BoP / quest bind on loot.
1576
+ const inst = applyBindToInstance(
1577
+ createItemInstance(y.item, y.count, ds, rng),
1578
+ resolveBonding(def),
1579
+ 'loot',
1580
+ );
1581
+ state.inventory = (hasIdentity(inst) || identityCapable(def))
1582
+ ? addInstance(state.inventory, inst, def.stackable ?? 1)
1583
+ : addItem(state.inventory, y.item, y.count, def.stackable ?? 1);
1584
+ }
1585
+ }
1586
+
1587
+ // Consume the key item only when TakeCastItem would: an expendable key is destroyed, a permanent
1588
+ // one opens and stays in the bags, and a multi-use one decrements its charges.
1589
+ let usedKeyEntry: number | undefined;
1590
+ if (keyItemEntry != null && go.lockId) {
1591
+ const keyDef = ds.item(keyItemEntry);
1592
+ if (isExpendableLockKeyItem(keyItemEntry, keyDef)) {
1593
+ const charged = consumeItemCharges(state.inventory, keyItemEntry, keyDef);
1594
+ if (charged.ok) {
1595
+ state.inventory = charged.inventory;
1596
+ usedKeyEntry = keyItemEntry;
1597
+ } else {
1598
+ // Fallback: single-unit remove (stack path).
1599
+ state.inventory = removeItem(state.inventory, keyItemEntry, 1)[0];
1600
+ usedKeyEntry = keyItemEntry;
1601
+ }
1602
+ }
1603
+ // Permanent key: open succeeds, key remains, keyUsed unset (no metal-click cue).
1604
+ }
1605
+
1606
+ // Quest credit: plain Use + cast-on-GO (goober spellId or lock-key useSpell).
1607
+ // Cite: GameObject.cpp:1635; Spell.cpp:1581; Player.cpp CastedCreatureOrGO; KI-923.
1608
+ state.questlog = applyInteractObjectQuestCredit({
1609
+ questlog: state.questlog,
1610
+ objectEntry,
1611
+ questCredit: used.questCredit,
1612
+ castSpellId: used.castSpellId,
1613
+ keyItemEntry,
1614
+ keyUseSpell: keyItemEntry != null ? (ds.item(keyItemEntry)?.useSpell ?? 0) : 0,
1615
+ questOf: (e) => ds.quest(e),
1616
+ activeQuests: state.questlog.quests,
1617
+ });
1618
+ creditSquadLogs(state, (log) => applyInteractObjectQuestCredit({
1619
+ questlog: log,
1620
+ objectEntry,
1621
+ questCredit: used.questCredit,
1622
+ castSpellId: used.castSpellId,
1623
+ keyItemEntry,
1624
+ keyUseSpell: keyItemEntry != null ? (ds.item(keyItemEntry)?.useSpell ?? 0) : 0,
1625
+ questOf: (e) => ds.quest(e),
1626
+ activeQuests: log.quests,
1627
+ }));
1628
+ // Event + go-use OPEN/CLOSE_DOOR onto goRuntime. Cite: ScriptMgr.cpp:2055–2094.
1629
+ const doors = applyGoUseAndEventDoors({
1630
+ goEntry: objectEntry,
1631
+ firedEventId: used.firedEventId,
1632
+ eventScripts: (id) => ds.eventScripts(id),
1633
+ goGuidScripts: (guid) => ds.goGuidUseScripts(guid),
1634
+ goTemplateScripts: (entry) => ds.goUseScripts(entry),
1635
+ goRuntime: state.goRuntime,
1636
+ goRuntimeKey: (e) => goRuntimeKey(ctx, e),
1637
+ gameObject: (e) => ds.gameObject(e),
1638
+ log,
1639
+ });
1640
+ if (doors.goRuntime) state.goRuntime = doors.goRuntime;
1641
+ // Cite: OutdoorPvPEP.cpp:223-337 ProcessCaptureEvent on banner Use.
1642
+ {
1643
+ const team = factionOf(ds, state.char.race) === 'horde' ? TEAM_HORDE : TEAM_ALLIANCE;
1644
+ state.worldStates = ensureWorldStates(state.worldStates);
1645
+ applyEpTowerCaptureFromGoUse(state.worldStates, objectEntry, team);
1646
+ }
1647
+ for (const qid of doors.exploreQuestIds) {
1648
+ state.questlog = recordEventHappens(state.questlog, qid);
1649
+ creditSquadEvent(state, qid);
1650
+ }
1651
+ for (const entry of doors.killCreditEntries) {
1652
+ state.questlog = recordKill(state.questlog, entry, (e) => ds.quest(e));
1653
+ creditSquadKill(state, entry, ds);
1654
+ }
1655
+ // Cite: ScriptMgr.cpp:2719–2734 TERMINATE_COND failQuest.
1656
+ if (doors.failedQuestIds.length) {
1657
+ state.questlog = applyFailedQuests(state.questlog, doors.failedQuestIds);
1658
+ }
1659
+ // Cite: ScriptMgr.cpp:3264–3267 / :2828–2843 / :2182–2218 / :1815–1911 / :2881–2992 / :2938–2948.
1660
+ for (const run of doors.scriptRuns) {
1661
+ state.worldStates = ensureWorldStates(state.worldStates);
1662
+ const tail = applyTailFromDbScript(run.world, run.intents, {
1663
+ source: run.world.source,
1664
+ worldStates: state.worldStates,
1665
+ log,
1666
+ relayScripts: (id) => ds.relayScripts(id),
1667
+ questlog: state.questlog,
1668
+ goRuntime: state.goRuntime,
1669
+ goRuntimeKey: (e) => goRuntimeKey(ctx, e),
1670
+ onMail: (rec) => deliverScriptMail(ctx, rec, Date.now(), 'Mailbox'),
1671
+ });
1672
+ if (tail.questlog) state.questlog = tail.questlog;
1673
+ if (tail.goRuntime) state.goRuntime = tail.goRuntime;
1674
+ }
1675
+ const fired = doors.fired;
1676
+ // Non-credit opcodes are dispositioned (SESSION 16) — log so it is never silent.
1677
+ if (fired && fired.rowCount > 0 && fired.hasNonCreditCommands) {
1678
+ log(
1679
+ `Event ${fired.eventId} fired (${fired.rowCount} script row(s); cmds [${fired.commands.join(',')}]) - ${fired.disposition}`,
1680
+ );
1681
+ } else if (fired && fired.rowCount > 0) {
1682
+ log(`Event ${fired.eventId} fired (${fired.rowCount} script row(s)).`);
1683
+ }
1684
+ // A GO ender arrival: an empty-objective talk or examine quest ends at a questgiver GO. Credit
1685
+ // by the ender link, the reverse list, or an empty find objective naming this GO.
1686
+ state.questlog = recordGoEnderInteract(
1687
+ state.questlog,
1688
+ objectEntry,
1689
+ (e) => ds.quest(e),
1690
+ state.inventory,
1691
+ { goName: go.name, questsEnded: go.questsEnded },
1692
+ );
1693
+ creditSquadGoEnder(state, ds, objectEntry, { goName: go.name, questsEnded: go.questsEnded });
1694
+ refreshQuests();
1695
+
1696
+ // Surface accept, turn-in and incomplete at the GO's quest links, falling back to matching the
1697
+ // quest definition's own starter and ender entries against the zone offers and the log.
1698
+ let questsOffered: number[] | undefined;
1699
+ let questsTurnIn: number[] | undefined;
1700
+ let questsIncomplete: number[] | undefined;
1701
+ const hasReverse =
1702
+ (go.questsStarted?.length ?? 0) > 0 || (go.questsEnded?.length ?? 0) > 0;
1703
+ const fallbackStarted: number[] = [];
1704
+ const fallbackEnded: number[] = [];
1705
+ if (!hasReverse) {
1706
+ const seenS = new Set<number>();
1707
+ const seenE = new Set<number>();
1708
+ const isStarterOf = (q: NonNullable<ReturnType<typeof ds.quest>>, entry: number) =>
1709
+ q.goStarter === entry || (q.goStarterEntries ?? []).includes(entry);
1710
+ const isEnderOf = (q: NonNullable<ReturnType<typeof ds.quest>>, entry: number) =>
1711
+ q.goEnder === entry || (q.goEnderEntries ?? []).includes(entry);
1712
+ const consider = (qe: number) => {
1713
+ const q = ds.quest(qe);
1714
+ if (!q) return;
1715
+ if (isStarterOf(q, objectEntry) && !seenS.has(qe)) {
1716
+ seenS.add(qe);
1717
+ fallbackStarted.push(qe);
1718
+ }
1719
+ if (isEnderOf(q, objectEntry) && !seenE.has(qe)) {
1720
+ seenE.add(qe);
1721
+ fallbackEnded.push(qe);
1722
+ }
1723
+ };
1724
+ for (const qe of ds.zone(state.char.zoneId)?.quests ?? []) consider(qe);
1725
+ for (const qs of state.questlog.quests) consider(qs.entry);
1726
+ }
1727
+ if (hasReverse || fallbackStarted.length || fallbackEnded.length) {
1728
+ const snap: QuestPlayerSnapshot = {
1729
+ level: state.char.level,
1730
+ classId: state.char.cls,
1731
+ raceId: state.char.race,
1732
+ questStatus: (id) => {
1733
+ const s = getQuest(state.questlog, id);
1734
+ if (!s) return 'none';
1735
+ if (s.status === 'turnedIn') return 'turnedIn';
1736
+ if (s.status === 'complete') return 'complete';
1737
+ return 'active';
1738
+ },
1739
+ skillValue: (skillId) => state.char.combatSkills?.[skillId]?.current ?? 0,
1740
+ standingFor,
1741
+ hasActiveTimedQuest: state.questlog.quests.some((qq) => {
1742
+ if (qq.status === 'turnedIn' || qq.status === 'failed') return false;
1743
+ const qd = ds.quest(qq.entry);
1744
+ return !!(qd?.limitTime && qd.limitTime > 0);
1745
+ }),
1746
+ };
1747
+ const offer = goQuestGiverOffer(
1748
+ go,
1749
+ state.questlog,
1750
+ snap,
1751
+ (e) => ds.quest(e),
1752
+ state.inventory,
1753
+ {
1754
+ questDefFor: (id) => ds.quest(id),
1755
+ standingFor,
1756
+ fallbackStarted: hasReverse ? undefined : fallbackStarted,
1757
+ fallbackEnded: hasReverse ? undefined : fallbackEnded,
1758
+ },
1759
+ );
1760
+ questsOffered = offer.acceptable;
1761
+ questsTurnIn = offer.turnInReady;
1762
+ questsIncomplete = offer.incomplete;
1763
+ }
1764
+
1765
+ // A successful herb or mine open grants profession skill-ups the same way gather() does.
1766
+ // Cite: SpellEffects.cpp:2232-2244 EffectOpenLock → UpdateGatherSkill.
1767
+ let skillUp: number | undefined;
1768
+ let workedBy: string | undefined;
1769
+ if (gatherNode && (gatherNode.skill === 'herbalism' || gatherNode.skill === 'mining')) {
1770
+ const picked = miningWorker
1771
+ ?? pickPartyWorker(gatherWorkers(ctx, gatherNode).candidates);
1772
+ if (picked) {
1773
+ const taps = gatherNode.skill === 'mining' ? miningTaps : 1;
1774
+ skillUp = grantWorkerGatherSkillTaps(ctx, picked, gatherNode, rng, taps);
1775
+ if (picked.compId != null) workedBy = picked.name;
1776
+ }
1777
+ // Mining spends every refill now; herbs still tick one charge. KI-807 clock shared with gather().
1778
+ const mining = gatherNode.skill === 'mining';
1779
+ const startCharges = mining ? miningTaps : startGatherCharges(ctx, gatherNode, objectEntry, rng);
1780
+ const { depleted } = commitGatherNodeCharge(
1781
+ ctx, gatherNode, objectEntry, nowMs, startCharges, mining ? startCharges : 1,
1782
+ );
1783
+ if (depleted) log(`The ${gatherNode.name} is depleted.`);
1784
+ }
1785
+ // KI-858: fishing hole success advances Fishing like bobber resolve (UpdateFishingSkill).
1786
+ // Cite: GameObject.cpp:1727 player->UpdateFishingSkill(); Player.cpp:5141–5161.
1787
+ if (used.kind === 'fishinghole' || go.type === GAMEOBJECT_TYPE_FISHINGHOLE) {
1788
+ const gain = gainSkill(state.char, 'fishing', { orange: 1, yellow: 1, green: 1, gray: 375 }, rng);
1789
+ state.char = gain.char;
1790
+ skillUp = gain.up ?? skillUp;
1791
+ if (used.despawn) log(`The ${go.name} is depleted.`);
1792
+ }
1793
+
1794
+ if (yielded.length) {
1795
+ const line = yielded.map((l) => `${l.count}× ${ds.item(l.item)?.name ?? l.item}`).join(', ');
1796
+ const skillLine = gatherNode?.skill
1797
+ ?? ((used.kind === 'fishinghole' || go.type === GAMEOBJECT_TYPE_FISHINGHOLE) ? 'fishing' : undefined);
1798
+ const skillNote = skillUp != null && skillLine != null ? ` (${skillLine} → ${skillUp})` : '';
1799
+ // Name the companion who worked it — the skill point went to them, not the player.
1800
+ log(`Opened ${go.name}${workedBy ? ` (${workedBy})` : ''}: ${line}.${skillNote}`);
1801
+ } else if (
1802
+ (questsOffered?.length ?? 0) > 0 ||
1803
+ (questsTurnIn?.length ?? 0) > 0 ||
1804
+ (questsIncomplete?.length ?? 0) > 0
1805
+ ) {
1806
+ const bits: string[] = [];
1807
+ if (questsOffered?.length) bits.push(`${questsOffered.length} quest(s) to accept`);
1808
+ if (questsTurnIn?.length) bits.push(`${questsTurnIn.length} to turn in`);
1809
+ // Honest incomplete: active at ender but objectives unmet — never a false complete.
1810
+ // Cite: QuestHandler.cpp getDialogStatus DIALOG_STATUS_INCOMPLETE.
1811
+ if (questsIncomplete?.length) bits.push(`${questsIncomplete.length} incomplete`);
1812
+ log(`Examined ${go.name}: ${bits.join(', ')}.`);
1813
+ } else if ((used.kind === 'spellcaster' || used.kind === 'goober') && used.castSpellId) {
1814
+ const sn = ds.spell(used.castSpellId)?.name ?? `spell ${used.castSpellId}`;
1815
+ log(`Used ${go.name}: ${sn}.`);
1816
+ } else if (used.kind === 'text' || go.type === GAMEOBJECT_TYPE_TEXT) {
1817
+ log(`You read ${go.name}.`);
1818
+ } else {
1819
+ log(`Used ${go.name}.`);
1820
+ }
1821
+ // keyUsed: the LOCK_KEY_ITEM entry consumed on this open (same field openContainerItem
1822
+ // reports). The UI leads the loot window's sound cue with the lock click when set.
1823
+ return {
1824
+ ok: true,
1825
+ yielded,
1826
+ questsOffered,
1827
+ questsTurnIn,
1828
+ questsIncomplete,
1829
+ keyUsed: usedKeyEntry,
1830
+ skillUp,
1831
+ ...(workedBy ? { workedBy } : {}),
1832
+ pageId: used.pageId,
1833
+ ...(heldLoot ? { held: heldLoot } : {}),
1834
+ };
1835
+ },
1836
+
1837
+ // Open a container item in the bags: roll its loot template, bag the contents, consume the
1838
+ // container. Same shape as the chest path, because in the core they are the same act.
1839
+ openContainerItem(slotIndex, opts) {
1840
+ const stack = state.inventory.stacks[slotIndex];
1841
+ if (!stack) return { ok: false, reason: "You don't have that" };
1842
+ const def = ds.item(stack.item);
1843
+ if (!def) return { ok: false, reason: 'Unknown item' };
1844
+
1845
+ const hasItemLootTable = (entry: number) => ds.lootTable(`item:${entry}`).length > 0;
1846
+ const lock = def.lockId != null ? ds.lock(def.lockId) : undefined;
1847
+ const hasItem = (entry: number) =>
1848
+ countItem(state.inventory, entry) > 0
1849
+ || countItem(state.keyring ?? { stacks: [] }, entry) > 0;
1850
+ const skillMeta = ds.combatSkillMeta?.() ?? null;
1851
+ const playerSkill = playerLockpickingForLocks(state.char);
1852
+
1853
+ // Roster rogue lockpick: explicit companion, else best owned rogue who covers the req.
1854
+ let lockpickingSkill = playerSkill;
1855
+ let openerName: string | undefined;
1856
+ if (opts?.lockpickerId) {
1857
+ const comp = findRosterCompanion(opts.lockpickerId);
1858
+ if (!comp) return { ok: false, reason: 'No such companion' };
1859
+ const skill = companionLockpickingSkill(comp, state.char.level, skillMeta);
1860
+ if (skill == null) {
1861
+ return { ok: false, reason: `${comp.name} cannot pick locks` };
1862
+ }
1863
+ lockpickingSkill = skill;
1864
+ openerName = comp.name;
1865
+ }
1866
+
1867
+ let check = canOpenContainer({
1868
+ def,
1869
+ level: state.char.level,
1870
+ hasItemLootTable,
1871
+ lock,
1872
+ hasItem,
1873
+ lockpickingSkill,
1874
+ });
1875
+ // No explicit picker: the whole group is eligible, and the box goes to whoever can still
1876
+ // gain a Lockpicking point from it — the player, until they cap out for this lock.
1877
+ let openerIsPlayer = true;
1878
+ // A key the player holds opens it, full stop — never re-route that to a rogue's picks.
1879
+ const keyOpens = check.ok && check.keyItemEntry != null;
1880
+ const refusal = check.ok ? undefined : check.kind;
1881
+ if (!keyOpens && refusal !== 'no_loot' && refusal !== 'level' && !opts?.lockpickerId) {
1882
+ const req = containerPicklockReq(lock);
1883
+ const { candidates, holders } = lockpickWorkers({
1884
+ playerName: state.char.name,
1885
+ playerSkill: playerSkill,
1886
+ // No playerMax: `skillMaxForLevel(level)` is the cap. Passing the CURRENT skill made
1887
+ // `cur >= max` always true, so the player never gained and a companion took every box.
1888
+ playerLevel: state.char.level,
1889
+ squad: lockpickCompanionPool(state.squad, state.botRoster, state.raidRoster?.members),
1890
+ meta: skillMeta,
1891
+ required: req,
1892
+ gainTenths: (cur, max, red) => (
1893
+ cur >= max ? 0 : gatherSkillGainChanceTenths(cur, red)
1894
+ ),
1895
+ });
1896
+ const picked = req > 0 ? pickPartyWorker(candidates) : undefined;
1897
+ if (picked) {
1898
+ const re = canOpenContainer({
1899
+ def, level: state.char.level, hasItemLootTable, lock, hasItem,
1900
+ lockpickingSkill: picked.skill,
1901
+ });
1902
+ if (re.ok) {
1903
+ check = re;
1904
+ openerIsPlayer = picked.compId == null;
1905
+ if (!openerIsPlayer) openerName = picked.name;
1906
+ }
1907
+ } else if (!check.ok && check.kind === 'needs_skill' && req > 0) {
1908
+ const near = nearestPartyWorker(holders, req);
1909
+ return { ok: false, reason: `Locked - ${partyWorkRefusalReason('Lockpicking', near)}` };
1910
+ }
1911
+ }
1912
+ if (!check.ok) return { ok: false, reason: check.reason };
1913
+
1914
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
1915
+ // The player picking their own lock trains Lockpicking, exactly as casting Pick Lock does. A
1916
+ // companion rogue is at the level cap and gains nothing, which is why the picker prefers you.
1917
+ let skillUp: number | undefined;
1918
+ if (openerIsPlayer && check.keyItemEntry == null && (def.lockId ?? 0) > 0) {
1919
+ const req = containerPicklockReq(lock);
1920
+ if (req > 0) {
1921
+ const gain = tryLockpickingSkillUp(
1922
+ state.char.combatSkills,
1923
+ req,
1924
+ (p) => rng.chance(p),
1925
+ );
1926
+ if (gain.up != null) {
1927
+ state.char = { ...state.char, combatSkills: gain.map };
1928
+ skillUp = gain.up;
1929
+ }
1930
+ }
1931
+ }
1932
+ const table = ds.lootTable(`item:${def.entry}`);
1933
+ // Item containers the player already holds. Quest-gated rows still roll here: the acquire gate
1934
+ // was the drop of the container itself, so opening one is extract, not a fresh loot roll.
1935
+ const yielded: LootDrop[] = rollLoot(table, rng, {
1936
+ allowsQuestDrop: () => true,
1937
+ allowsCondition: (conditionId: number) =>
1938
+ isConditionSatisfied(conditionId, lootConditionSnapshot(), conditionTableById()).ok,
1939
+ });
1940
+
1941
+ // requireRoom (the auto-opener): dry-run the whole grant into a probe inventory and refuse the
1942
+ // act rather than consume the container and spill. Cite: the gather path at professions.ts:670.
1943
+ if (opts?.requireRoom && !opts?.hold) {
1944
+ let probe = removeItem(state.inventory, def.entry, 1)[0];
1945
+ for (const y of yielded) {
1946
+ const maxStack = ds.item(y.item)?.stackable ?? 1;
1947
+ if (!canFit(probe, y.item, y.count, maxStack, cap())) {
1948
+ return { ok: false, reason: 'Not enough bag space' };
1949
+ }
1950
+ probe = addItem(probe, y.item, y.count, maxStack);
1951
+ }
1952
+ }
1953
+
1954
+ // The container itself goes first, so its slot is free for what was inside it.
1955
+ state.inventory = removeItem(state.inventory, def.entry, 1)[0];
1956
+ // A key case consumes the key on open — a charge off a charged key, and nothing off a permanent
1957
+ // one (Skeleton Key stays). Same rule as interactObject. Cite: Spell.cpp:4209 TakeCastItem.
1958
+ let usedKeyEntry: number | undefined;
1959
+ if (check.keyItemEntry != null) {
1960
+ const keyDef = ds.item(check.keyItemEntry);
1961
+ const spent = consumeItemCharges(state.inventory, check.keyItemEntry, keyDef);
1962
+ if (spent.ok) state.inventory = spent.inventory;
1963
+ // keyUsed is the key CONSUMED by this open, so only a destroy-at-zero row reports one.
1964
+ if (isExpendableCastItem(keyDef)) usedKeyEntry = check.keyItemEntry;
1965
+ }
1966
+
1967
+ const via = openerName ? ` (${openerName})` : '';
1968
+ // Key OR lockpick skill (player or party rogue) → lock-click cue in the loot window.
1969
+ const lockOpened = check.keyItemEntry != null || ((def.lockId ?? 0) > 0);
1970
+ // HOLD keeps the contents in the loot window, which is the only place they live once the
1971
+ // container is consumed. A full backpack just leaves the row in the window.
1972
+ if (opts?.hold) {
1973
+ const held = openLoot({
1974
+ kind: 'container',
1975
+ title: def.name,
1976
+ icon: def.icon,
1977
+ containerItem: def.entry,
1978
+ keyUsed: lockOpened,
1979
+ }, yielded);
1980
+ refreshQuests();
1981
+ log(`Opened ${def.name}${via}.${skillUp != null ? ` (Lockpicking → ${skillUp})` : ''}`);
1982
+ return {
1983
+ ok: true, yielded, spilled: [], keyUsed: usedKeyEntry,
1984
+ ...(skillUp != null ? { skillUp } : {}),
1985
+ ...(openerName ? { openedBy: openerName } : {}),
1986
+ held,
1987
+ };
1988
+ }
1989
+
1990
+ const granted: LootDrop[] = [];
1991
+ const spilled: LootDrop[] = [];
1992
+ for (const y of yielded) {
1993
+ const itemDef = ds.item(y.item);
1994
+ if (!itemDef) continue;
1995
+ const maxStack = itemDef.stackable ?? 1;
1996
+ if (!canFit(state.inventory, y.item, y.count, maxStack, cap())) {
1997
+ spilled.push(y); // bags full: report it rather than silently eating the drop
1998
+ continue;
1999
+ }
2000
+ const inst = applyBindToInstance(
2001
+ createItemInstance(y.item, y.count, ds, rng),
2002
+ resolveBonding(itemDef),
2003
+ 'loot',
2004
+ );
2005
+ state.inventory = (hasIdentity(inst) || identityCapable(itemDef))
2006
+ ? addInstance(state.inventory, inst, maxStack)
2007
+ : addItem(state.inventory, y.item, y.count, maxStack);
2008
+ granted.push(y);
2009
+ }
2010
+ refreshQuests(); // container contents can complete a gather objective
2011
+
2012
+ const line = granted.map((l) => `${l.count}× ${ds.item(l.item)?.name ?? l.item}`).join(', ');
2013
+ log(`Opened ${def.name}${via}: ${line || 'nothing inside'}.`
2014
+ + `${skillUp != null ? ` (Lockpicking → ${skillUp})` : ''}`
2015
+ + `${spilled.length ? ' (bags full - some contents lost)' : ''}`);
2016
+ return {
2017
+ ok: true, yielded: granted, spilled, keyUsed: usedKeyEntry,
2018
+ ...(skillUp != null ? { skillUp } : {}),
2019
+ ...(openerName ? { openedBy: openerName } : {}),
2020
+ };
2021
+ },
2022
+
2023
+ // The zone's reachable spell focus ids: a focus-requiring recipe needs its id here. Zone
2024
+ // granularity, because outside a session the world model has no in-zone position.
2025
+ craft(recipeId) {
2026
+ const recipe = ds.recipe(recipeId);
2027
+ if (!recipe) return { ok: false, reason: 'Unknown recipe' };
2028
+ const productStack = ds.item(recipe.product.item)?.stackable ?? 1;
2029
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
2030
+ // Vendor goods the bags lack (vials, thread, dye) are bought into the craft at vendor price.
2031
+ const staged = stageVendorReagents(ds, state.char, recipe.reagents, state.inventory, cap());
2032
+ if (!staged.ok) return { ok: false, reason: staged.reason };
2033
+ // Wave Q P2: craft product is a new-copy create site (roll when product has randomProperty).
2034
+ const now = Date.now();
2035
+ const res = doCraft(
2036
+ state.char, staged.inv, recipe, productStack, rng, cap(), ds, undefined,
2037
+ reachableCraftFocus(),
2038
+ { spellCooldowns: state.spellCooldowns, categoryCooldowns: state.categoryCooldowns, now },
2039
+ partyTools(),
2040
+ );
2041
+ if (!res.ok) return { ok: false, reason: craftRefusal(recipe, now, res.reason) };
2042
+ state.char = staged.cost ? { ...res.char, gold: res.char.gold - staged.cost } : res.char;
2043
+ state.inventory = res.inventory;
2044
+ stampRecipeCooldown(recipe, state, now);
2045
+ refreshQuests(); // a crafted item can satisfy a gather objective
2046
+ const pname = ds.item(recipe.product.item)?.name ?? recipe.product.item;
2047
+ const made = res.produced?.count ?? 1;
2048
+ if (staged.cost) log(`Bought ${buyNames(ds, staged.buys)} for ${moneyMarked(staged.cost)}.`);
2049
+ log(`Crafted ${made > 1 ? `${made}x ` : ''}${pname}.${res.skillUp ? ` (${recipe.skill} → ${res.skillUp})` : ''}`);
2050
+ return { ok: true, produced: res.produced!, skillUp: res.skillUp, ...(staged.cost ? { spent: staged.cost } : {}) };
2051
+ },
2052
+
2053
+ craftAll(recipeId) {
2054
+ const recipe = ds.recipe(recipeId);
2055
+ if (!recipe) return { ok: false, reason: 'Unknown recipe' };
2056
+ const productStack = ds.item(recipe.product.item)?.stackable ?? 1;
2057
+ // Upper bound = how many the reagents on hand allow (a full bag can stop us sooner).
2058
+ // A reagent-free recipe (shouldn't exist) is capped at 1 so a bad data row can't spin here.
2059
+ const maxByReagents = recipe.reagents.length
2060
+ ? maxCraftsWithVendor(ds, state.char, recipe.reagents, state.inventory)
2061
+ : 1;
2062
+ const skillBefore = state.char.skills[recipe.skill]?.current;
2063
+ let crafted = 0;
2064
+ let units = 0;
2065
+ let spent = 0;
2066
+ let produced: LootDrop | undefined;
2067
+ let lastReason: string | undefined;
2068
+ for (let i = 0; i < maxByReagents; i++) {
2069
+ // Each craft rolls its own skill-up, so give each a fresh sub-seed like craft() does.
2070
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
2071
+ const staged = stageVendorReagents(ds, state.char, recipe.reagents, state.inventory, cap());
2072
+ if (!staged.ok) { lastReason = staged.reason; break; }
2073
+ const now = Date.now();
2074
+ const res = doCraft(
2075
+ state.char, staged.inv, recipe, productStack, rng, cap(), ds, undefined,
2076
+ reachableCraftFocus(),
2077
+ { spellCooldowns: state.spellCooldowns, categoryCooldowns: state.categoryCooldowns, now },
2078
+ partyTools(),
2079
+ );
2080
+ if (!res.ok) { lastReason = craftRefusal(recipe, now, res.reason); break; }
2081
+ state.char = staged.cost ? { ...res.char, gold: res.char.gold - staged.cost } : res.char;
2082
+ state.inventory = res.inventory;
2083
+ stampRecipeCooldown(recipe, state, now);
2084
+ spent += staged.cost;
2085
+ produced = res.produced;
2086
+ units += res.produced?.count ?? 1;
2087
+ crafted++;
2088
+ }
2089
+ if (crafted === 0) return { ok: false, reason: lastReason ?? 'Cannot craft' };
2090
+ refreshQuests(); // a crafted item can satisfy a gather objective
2091
+ const skillAfter = state.char.skills[recipe.skill]?.current;
2092
+ const pname = ds.item(recipe.product.item)?.name ?? recipe.product.item;
2093
+ const skillNote = skillBefore != null && skillAfter != null && skillAfter > skillBefore ? ` (${recipe.skill} ${skillBefore} → ${skillAfter})` : '';
2094
+ log(`Crafted ${units}x ${pname}.${spent ? ` Vendor reagents ${moneyMarked(spent)}.` : ''}${skillNote}`);
2095
+ return { ok: true, crafted, produced: produced!, skillBefore, skillAfter, ...(spent ? { spent } : {}) };
2096
+ },
2097
+
2098
+ learnEnchant(enchantId, trainerEntry) {
2099
+ const enchant = ds.enchant(enchantId);
2100
+ if (!enchant) return { ok: false, reason: 'Unknown enchant' };
2101
+ const discountMod = extras.trainerDiscountMod(trainerEntry);
2102
+ const res = doLearnEnchant(state.char, enchant, { discountMod });
2103
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot learn' };
2104
+ state.char = res.char;
2105
+ const spent = Math.floor((enchant.cost ?? 0) * discountMod);
2106
+ log(`Learned enchant: ${enchant.name}${spent ? ` for ${moneyMarked(spent)}` : ''}.`);
2107
+ return { ok: true };
2108
+ },
2109
+
2110
+ // The enchant analogue of learnRecipeFromItem: read a Formula: item, learn its formula, consume it.
2111
+ learnEnchantFromItem(itemEntry) {
2112
+ const it = ds.item(itemEntry);
2113
+ // The same derivation as the recipe path. About 76 obtainable formula items teach an enchant
2114
+ // with no baked row, so they come back unavailable — which refuses and KEEPS the formula.
2115
+ const teach = itemTeachIntent(it, ds);
2116
+ if (teach?.kind === 'unavailable') return { ok: false, reason: unavailableTeachReason(teach.what) };
2117
+ if (!it || teach?.kind !== 'enchant') return { ok: false, reason: 'That item teaches no formula' };
2118
+ if (it.allowClass && !it.allowClass.includes(state.char.cls)) return { ok: false, reason: 'Your class cannot learn this' };
2119
+ if (it.requiredLevel > state.char.level) return { ok: false, reason: `Requires level ${it.requiredLevel}` };
2120
+ if (countItem(state.inventory, itemEntry) <= 0) return { ok: false, reason: "You don't have that formula" };
2121
+ const enchant = ds.enchant(teach.id);
2122
+ if (!enchant) return { ok: false, reason: 'That formula is unknown' };
2123
+ // fromItem: true — skip trainer skill≥orange gate (KI-143); apply still requires skill ≥ orange.
2124
+ const res = doLearnEnchant(state.char, enchant, { fromItem: true });
2125
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot learn' };
2126
+ state.char = res.char;
2127
+ state.inventory = removeItem(state.inventory, itemEntry, 1)[0];
2128
+ log(`You study ${it.name} and learn ${enchant.name}.`);
2129
+ return { ok: true, enchantId: enchant.id, name: enchant.name };
2130
+ },
2131
+
2132
+ // A profession TIER book (Expert Cookbook and its siblings): a vendor-sold book whose on-use spell
2133
+ // is a learn of the tier spell, so no teach field was baked. Free: the book's price was the cost.
2134
+ learnProfessionRankFromItem(itemEntry) {
2135
+ const it = ds.item(itemEntry);
2136
+ const teach = itemTeachIntent(it, ds);
2137
+ if (!it || teach?.kind !== 'profRank') return { ok: false, reason: 'That book teaches no skill' };
2138
+ if (it.allowClass && !it.allowClass.includes(state.char.cls)) return { ok: false, reason: 'Your class cannot learn this' };
2139
+ if (it.requiredLevel > state.char.level) return { ok: false, reason: `Requires level ${it.requiredLevel}` };
2140
+ if (countItem(state.inventory, itemEntry) <= 0) return { ok: false, reason: "You don't have that book" };
2141
+ // The tier spell is named after its skill line ("Cooking" / "Fishing" / "First Aid"), which is
2142
+ // exactly ProfessionDef.name — no new bake field needed to bridge SkillLine id → our id.
2143
+ const taughtName = ds.spell(teach.id)?.name?.trim().toLowerCase();
2144
+ const prof = taughtName
2145
+ ? ds.professions().find((p) => p.name.trim().toLowerCase() === taughtName)
2146
+ : undefined;
2147
+ if (!prof) return { ok: false, reason: 'That skill is not in the game yet' };
2148
+ const have = state.char.skills[prof.id];
2149
+ if (!have) return { ok: false, reason: `Learn ${prof.name} from a trainer first` };
2150
+ const target = prof.ranks.find((r) => r.rank === teach.rank);
2151
+ if (!target) return { ok: false, reason: `${prof.name} has no such rank` };
2152
+ if (have.max >= target.cap) return { ok: false, reason: `You already know ${target.name} ${prof.name}` };
2153
+ // The book grants exactly ONE tier — refuse when the player is more than one rank short
2154
+ // rather than skipping a tier they never trained.
2155
+ const next = prof.ranks.find((r) => r.cap > have.max);
2156
+ if (!next || next.rank !== teach.rank) {
2157
+ return { ok: false, reason: `Train ${next?.name ?? 'the next rank'} ${prof.name} first` };
2158
+ }
2159
+ const res = doTrainProfession(state.char, prof, { free: true });
2160
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot learn' };
2161
+ state.char = res.char;
2162
+ state.inventory = removeItem(state.inventory, itemEntry, 1)[0];
2163
+ log(`You study ${it.name} and learn ${target.name} ${prof.name}.`);
2164
+ return { ok: true, skill: prof.id, name: `${target.name} ${prof.name}` };
2165
+ },
2166
+
2167
+ enchantItem(enchantId, slot, companionId) {
2168
+ const enchant = ds.enchant(enchantId);
2169
+ if (!enchant) return { ok: false, reason: 'Unknown enchant' };
2170
+ const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
2171
+ if (companionId) {
2172
+ if (hasAttrEx2(ds.spell(enchant.spellId), SPELL_ATTR_EX2.ENCHANT_OWN_ITEM_ONLY)) {
2173
+ return { ok: false, reason: 'You can only enchant your own items with that' };
2174
+ }
2175
+ // A profession enchant on a companion's gear: your skill and reagents, their paperdoll. A
2176
+ // companion has no slot map, so the permanent enchant lives on the equipped instance.
2177
+ const comp = findRosterCompanion(companionId);
2178
+ if (!comp) return { ok: false, reason: 'That companion is not with you' };
2179
+ const playerEquipped = state.char.equipped;
2180
+ const playerEnchants = state.char.enchants;
2181
+ const wearShell = { ...state.char, equipped: comp.equipped, enchants: undefined };
2182
+ const staged = stageVendorReagents(ds, state.char, enchant.reagents, state.inventory, cap());
2183
+ if (!staged.ok) return { ok: false, reason: staged.reason };
2184
+ const res = doApplyEnchant(wearShell, staged.inv, ds, enchant, slot, rng, partyTools());
2185
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot enchant' };
2186
+ // Skill-up stays on the player; gear reverts to player, companion gets enchanted kit.
2187
+ state.char = { ...res.char, equipped: playerEquipped, enchants: playerEnchants, gold: res.char.gold - staged.cost };
2188
+ comp.equipped = res.char.equipped;
2189
+ state.inventory = res.inventory;
2190
+ const target = ds.item(equippedEntry(comp.equipped[slot])!)?.name ?? slot;
2191
+ if (staged.cost) log(`Bought ${buyNames(ds, staged.buys)} for ${moneyMarked(staged.cost)}.`);
2192
+ log(`Applied ${enchant.name} to ${comp.name}'s ${target}.${res.skillUp ? ` (enchanting → ${res.skillUp})` : ''}`);
2193
+ return { ok: true, skillUp: res.skillUp };
2194
+ }
2195
+ const staged = stageVendorReagents(ds, state.char, enchant.reagents, state.inventory, cap());
2196
+ if (!staged.ok) return { ok: false, reason: staged.reason };
2197
+ const res = doApplyEnchant(state.char, staged.inv, ds, enchant, slot, rng, partyTools());
2198
+ if (!res.ok) return { ok: false, reason: res.reason ?? 'Cannot enchant' };
2199
+ state.char = staged.cost ? { ...res.char, gold: res.char.gold - staged.cost } : res.char;
2200
+ state.inventory = res.inventory;
2201
+ const target = ds.item(equippedEntry(state.char.equipped[slot])!)?.name ?? slot;
2202
+ if (staged.cost) log(`Bought ${buyNames(ds, staged.buys)} for ${moneyMarked(staged.cost)}.`);
2203
+ log(`Applied ${enchant.name} to ${target}.${res.skillUp ? ` (enchanting → ${res.skillUp})` : ''}`);
2204
+ return { ok: true, skillUp: res.skillUp };
2205
+ },
2206
+
2207
+ // A GROUP job like a herb node: the item and mats are the player's, and the Enchanting line that
2208
+ // gates it belongs to whoever swings. Roster reach, because the player picked this verb.
2209
+ disenchant(itemEntry, opts) {
2210
+ return disenchantOne(itemEntry, undefined, opts);
2211
+ },
2212
+ disenchantStack(slotIndex, opts) {
2213
+ const stk = state.inventory.stacks[slotIndex];
2214
+ if (!stk) return { ok: false, reason: "You don't have that" };
2215
+ return disenchantOne(stk.item, slotIndex, opts);
2216
+ },
2217
+ // A companion's own bag stack, broken down where it lies. Soulbound gear never leaves the body
2218
+ // carrying it (partyVault.ts:259), and disenchant materials carry no bind, so the dust is yours.
2219
+ disenchantCompanionBagStack(id, slotIndex, opts) {
2220
+ const holder = findRosterCompanion(id);
2221
+ if (!holder) return { ok: false, reason: 'That companion is not with you' };
2222
+ const stk = (holder.bags ?? { stacks: [] }).stacks[slotIndex];
2223
+ if (!stk) return { ok: false, reason: 'Invalid slot' };
2224
+ return disenchantOne(stk.item, slotIndex, opts, holder);
2225
+ },
2226
+ };
2227
+ }