azeroth-shell 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +26 -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 +126 -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,1726 @@
1
+ // Character → CombatantSpec assembler + combat kit helpers (combatItems, stealth).
2
+ // Extracted from character.ts (Wave 1 S3 barrel split). Pure move — no behavior change.
3
+
4
+ import type { DataStore } from '../../data/dataStore.ts';
5
+ import type { JudgementBurst, SpellDef, ShapeshiftFormId } from '../../data/schema.ts';
6
+ import { SKILL_LINE_BY_ID } from '../../data/schema/profession.ts';
7
+ import type { CombatantSpec, CombatItem } from '../../engine/combat.ts';
8
+ import type { Inventory } from '../items/inventory.ts';
9
+ import { countItem } from '../items/inventory.ts';
10
+ import type { Character } from './character.ts';
11
+ import { isHovering, setHover } from '../core/hover.ts';
12
+ import { equippedEntry, equippedInstance } from '../items/itemInstance.ts';
13
+ import { castItemSurvivesUse, maxChargesOf } from '../../domain/items/inventory.ts';
14
+ import {
15
+ checkAmmoCompatibility,
16
+ equippedQuiverRangedHastePct,
17
+ projectileSubclassOf,
18
+ weaponIsThrown,
19
+ weaponIsWand,
20
+ weaponNeedsProjectileAmmo,
21
+ weaponSubclassOf,
22
+ } from '../items/ammo.ts';
23
+ import { classUsesRangedAuto } from '../core/ids.ts';
24
+ import { consumableFamilyKey, restoredPowerTypeIndex } from '../items/consumableFamily.ts';
25
+ import {
26
+ hasCombatSelfPayload,
27
+ hasFriendlyPayload,
28
+ hasHostilePayload,
29
+ isBeneficialSpeed,
30
+ isFoodDrinkPayload,
31
+ onUseAimsAtEnemy,
32
+ onUseEffects,
33
+ } from '../items/onUsePayload.ts';
34
+ import {
35
+ formAttackTimeMs, classHasParallelMana, formResourceOnEnter, resolveMeleeWeaponDamageRange,
36
+ formCombatPackage,
37
+ } from './forms.ts';
38
+ import type { SpellBook } from './spellbook.ts';
39
+ import { isBroken } from '../items/durability.ts';
40
+ import {
41
+ combatUseEffectFromInterpret,
42
+ gearProcTriggerSpellIds,
43
+ resolveCombatSpell,
44
+ resolveSpellEffects,
45
+ } from '../../engine/spells/interpreter/item-triggers.ts';
46
+ import { combatPayloadFromSpell } from '../../engine/spells/interpreter/weapon-imbue-kit.ts';
47
+ import { SPELL_ATTR, SPELL_ATTR_EX3, hasAttr, hasAttrEx3 } from '../../engine/spells/spellAttributes.ts';
48
+ import { syntheticHunterTrapPayload } from '../../engine/spells/interpreter/hunter-kit.ts';
49
+ import {
50
+ SETUP_TRIGGER_SPELL_ID,
51
+ syntheticTalentTriggerPayload,
52
+ } from '../../engine/spells/interpreter/class-scripts-kit.ts';
53
+ import {
54
+ AURA_TYPE,
55
+ immunityBoostAurasForSpell,
56
+ mechanicIdFromName,
57
+ } from '../../engine/spells/immunity.ts';
58
+ import {
59
+ equippedItems,
60
+ activeSetBonusSpells,
61
+ powerType,
62
+ createMana,
63
+ createHealth,
64
+ maxMana,
65
+ maxHealth,
66
+ hasResourceBaseline,
67
+ maxPower,
68
+ effectiveStats,
69
+ equippedStatBonuses,
70
+ effectiveArmor,
71
+ baseArmorValue,
72
+ attackPower,
73
+ rangedAttackPower,
74
+ blockValue,
75
+ healingPower,
76
+ resistances,
77
+ mechanicResistPct,
78
+ combatantCreatureType,
79
+ weaponProfile,
80
+ offhandWeaponProfile,
81
+ rangedProfile,
82
+ pruneExpiredAuras,
83
+ getActiveBuffIds,
84
+ attackPowerVersusBonuses,
85
+ rangedAttackPowerVersusBonuses,
86
+ spellDamageVersusBonuses,
87
+ damageDoneVersusPctBonuses,
88
+ damageDoneCreatureBonuses,
89
+ critDamageVersusPctBonuses,
90
+ offhandDamagePctBonus,
91
+ attackerChanceBonus,
92
+ attackerSpellHitBySchool,
93
+ overrideClassScriptIds,
94
+ permanentEnchantCombatProcs,
95
+ permanentEnchantProcTriggers,
96
+ equipResistancePenetration,
97
+ permanentSpeedAuras,
98
+ mergeSchoolAmounts,
99
+ } from './characterStats.ts';
100
+ import {
101
+ activeTalentEffects,
102
+ activeTalentProcsWithHolder,
103
+ talentManaRegenInterruptPct,
104
+ talentResistancePenetration,
105
+ talentThreatModifiers,
106
+ talentDamagePctBySchool,
107
+ talentCritBonus,
108
+ } from './characterTalents.ts';
109
+ import { formTalentAuraDeltas } from './formTalentAuras.ts';
110
+ import { radiusSpellModsFromTalentEffects } from '../../engine/spells/spellMods.ts';
111
+ import { ownedSpellModsFromSpell } from '../../engine/spells/spellModIntents.ts';
112
+ import { pendingSpellModFromResolved } from '../../engine/combat/roles.ts';
113
+ import { victimAddTargetTriggerRows } from '../../engine/spells/addTargetTrigger.ts';
114
+ import { knownCombatSpells } from './characterSpells.ts';
115
+ import { racialSpells } from './races.ts';
116
+ import {
117
+ canParry,
118
+ hasShieldEquipped,
119
+ critChance,
120
+ spellCritChanceFromStats,
121
+ spellHitBonusFromGear,
122
+ hitChance,
123
+ haste,
124
+ dodgeChance,
125
+ parryChance,
126
+ blockChance,
127
+ weaponSkill,
128
+ defenseSkill,
129
+ weaponSkillIdForHand,
130
+ weaponSkillForHand,
131
+ collectSkillBonuses,
132
+ spellPower,
133
+ spellDamageBySchool,
134
+ } from './characterRatings.ts';
135
+ import { getTempEnchant } from '../professions/characterEnchants.ts';
136
+ import { combatHeldEnchantFromEquipped } from '../spells/equipSpells.ts';
137
+ import { rocketBootsMalfunction } from '../items/rocketBoots.ts';
138
+ import { specialCasePayloadIds } from '../../engine/spells/specialCasePayloads.ts';
139
+ import { gearEnchantGrant } from '../professions/gearEnchant.ts';
140
+ import { itemUseCooldown } from '../items/itemUseCooldown.ts';
141
+ import { auraHolderCharges, auraStackIdentity } from '../../engine/spells/auraStacking.ts';
142
+ import { SHEET_BAKED_BUFF_MODS } from '../../engine/combat/seedDisplayAuras.ts';
143
+ import { isContinuousPartyAura } from '../spells/areaAuraMembership.ts';
144
+
145
+ type DisplayAuraEntry = NonNullable<CombatantSpec['startDisplayAuras']>[number];
146
+
147
+ // A party area aura's sheet-folded mods, with the identity a sibling rank strips it by. Only these
148
+ // sheet rows can leave mid-fight (holder gone / out of radius). Cite: SpellAuras.cpp:591-620.
149
+ function areaAuraSeedFields(
150
+ sp: Pick<SpellDef, 'effects' | 'exclusionGroup' | 'spellFamilyName' | 'spellFamilyFlags' | 'rawEffects' | 'attributes'>,
151
+ ): Pick<DisplayAuraEntry, 'sheetBuffs' | 'exclusionGroup' | 'stackIdent'> {
152
+ const sheetBuffs: NonNullable<DisplayAuraEntry['sheetBuffs']> = [];
153
+ for (const e of sp.effects) {
154
+ if (e.kind !== 'buff' || !e.mod || e.hostile === true) continue;
155
+ if (!SHEET_BAKED_BUFF_MODS.has(e.mod) || (e.amount ?? 0) === 0) continue;
156
+ sheetBuffs.push({ mod: e.mod, amount: e.amount, ...(e.school ? { school: e.school } : {}) });
157
+ }
158
+ const stackIdent = auraStackIdentity(sp);
159
+ return {
160
+ ...(sheetBuffs.length ? { sheetBuffs } : {}),
161
+ ...(sp.exclusionGroup != null ? { exclusionGroup: sp.exclusionGroup } : {}),
162
+ ...(stackIdent ? { stackIdent } : {}),
163
+ };
164
+ }
165
+
166
+ /** Sum active OOC buffs with mod 'mp5' (SPELL_AURA_MOD_POWER_REGEN total over 5s). */
167
+ function buffMp5(char: Character, ds: DataStore): number {
168
+ let total = 0;
169
+ for (const id of getActiveBuffIds(char, undefined, ds)) {
170
+ const raw = ds.spell(id);
171
+ if (!raw) continue;
172
+ // SI5 / oil equip auras: bake effects[] may be empty — resolve from rawEffects.
173
+ // Cite: Player.cpp ApplyEnchantment TYPE_EQUIP_SPELL; map-spell-effects aura 85.
174
+ const effs = (raw.effects?.length ? raw.effects : resolveCombatSpell(raw).effects) ?? [];
175
+ for (const eff of effs) {
176
+ if (eff.kind === 'buff' && eff.mod === 'mp5') total += eff.amount;
177
+ }
178
+ }
179
+ return total;
180
+ }
181
+
182
+ // OOC aura 110 / 134 totals for combat seed. Cite: StatSystem.cpp:551–562; Unit.cpp:4611–4622; KI-424
183
+ // Evocation.
184
+ function buffManaRegenAuras(char: Character, ds: DataStore): {
185
+ pctMult: number;
186
+ interruptPct: number;
187
+ } {
188
+ let pctMult = 1;
189
+ let interruptPct = 0;
190
+ for (const id of getActiveBuffIds(char, undefined, ds)) {
191
+ const raw = ds.spell(id);
192
+ if (!raw) continue;
193
+ const effs = (raw.effects?.length ? raw.effects : resolveCombatSpell(raw).effects) ?? [];
194
+ for (const eff of effs) {
195
+ if (eff.kind !== 'buff') continue;
196
+ if (eff.mod === 'mana_regen_pct') {
197
+ pctMult *= (100 + (eff.amount | 0)) / 100;
198
+ } else if (eff.mod === 'mana_regen_interrupt') {
199
+ interruptPct += eff.amount | 0;
200
+ }
201
+ }
202
+ }
203
+ return { pctMult, interruptPct };
204
+ }
205
+
206
+ // item_template.RequiredSkill / RequiredSkillRank use-gate. Only Engineering DEVICES carry one, so
207
+ // an absent requiredSkill is ungated. Gate on the SKILL, never the rank. Cite: Player.cpp ~9756.
208
+ export function meetsItemSkillGate(char: Character, it: { requiredSkill?: number; requiredSkillRank?: number }): boolean {
209
+ const skillId = it.requiredSkill ?? 0;
210
+ if (skillId <= 0) return true;
211
+ const line = SKILL_LINE_BY_ID[skillId];
212
+ // An unmapped skill line (lockpicking 633, poisons 40) is not a profession we model.
213
+ // Refusing on it would silently delete working items, so it stays ungated.
214
+ if (!line) return true;
215
+ return (char.skills?.[line]?.current ?? 0) >= (it.requiredSkillRank ?? 0);
216
+ }
217
+
218
+ /** Charges left on a WORN unit, when the equipped instance carries a count. */
219
+ function equippedChargesOf(char: Character, itemEntry: number): number | undefined {
220
+ for (const v of Object.values(char.equipped ?? {})) {
221
+ if (equippedEntry(v) !== itemEntry) continue;
222
+ return equippedInstance(v)?.charges;
223
+ }
224
+ return undefined;
225
+ }
226
+
227
+ function isEntryEquipped(char: Character, itemEntry: number): boolean {
228
+ const eq = char.equipped;
229
+ if (!eq) return false;
230
+ for (const v of Object.values(eq)) {
231
+ if (equippedEntry(v) === itemEntry) return true;
232
+ }
233
+ return false;
234
+ }
235
+
236
+ export function combatItems(char: Character, inv: Inventory, ds: DataStore): CombatItem[] {
237
+ const byEntry = new Map<number, number>();
238
+ for (const s of inv.stacks) byEntry.set(s.item, (byEntry.get(s.item) ?? 0) + s.count);
239
+ // Equipped on-use (trinkets etc.) leave bags on equip — still enter the fight kit.
240
+ // High count so personal CD is the gate (use does not consume the gear).
241
+ for (const it of equippedItems(char, ds)) {
242
+ if (!(it.useInCombat || it.useBuff || it.useSpell != null)) continue;
243
+ if (!byEntry.has(it.entry)) byEntry.set(it.entry, 99);
244
+ }
245
+ const out: CombatItem[] = [];
246
+ for (const [entry, count] of byEntry) {
247
+ const it = ds.item(entry);
248
+ if (!it) continue;
249
+ if (!meetsItemSkillGate(char, it)) continue;
250
+ // Permanent on-use gear only when equipped (HandleUseItemOpcode InventoryType gate).
251
+ if (it.slot != null && !isEntryEquipped(char, entry)) continue;
252
+
253
+ // ItemTemplate.RequiredLevel is the only combat-kit level floor — not the spell or useInCombat
254
+ // reqLevel, which over-gate pots (KI-855) and under-gate alone (KI-837).
255
+ const itemReq = (it.requiredLevel | 0);
256
+ if (itemReq > 0 && char.level < itemReq) continue;
257
+
258
+ // What a use SPENDS. `reusable` = the unit survives (spellcharges 0 or POSITIVE, never the equip
259
+ // slot); `charges` = this unit's remaining count. Cite: Spell.cpp:4209; domain/items/inventory.ts.
260
+ const spend = ((): { reusable?: boolean; charges?: number } => {
261
+ const max = maxChargesOf(it);
262
+ const counted = (it.charges ?? 0) !== 0 && max > 1;
263
+ const left = counted
264
+ ? (equippedChargesOf(char, entry) ?? inv.stacks.find((st) => st.item === entry && st.charges != null)?.charges ?? max)
265
+ : undefined;
266
+ const keeps = castItemSurvivesUse(it);
267
+ return { ...(keeps ? { reusable: true } : {}), ...(left != null ? { charges: left } : {}) };
268
+ })();
269
+
270
+ // Shared item-template CDs. itemUseCooldown reads useInCombat and useBuff first, then the
271
+ // unconditional top-level carrier; spell RecoveryTime is the last fallback.
272
+ const itemCd = itemUseCooldown(it);
273
+ const uicCd = (_u: NonNullable<typeof it.useInCombat> | undefined, spCd: number) => ({
274
+ cooldownMs: itemCd.cooldownMs > 0 ? itemCd.cooldownMs : spCd,
275
+ category: itemCd.category > 0 ? itemCd.category : undefined,
276
+ categoryCooldownMs: itemCd.categoryCooldownMs > 0 ? itemCd.categoryCooldownMs : undefined,
277
+ });
278
+
279
+ // 1) Instant restore (potions / healthstone / mana gem). cast_spell falls through so the
280
+ // full SpellDef is resolved (grenades must not be flattened to min/max).
281
+ const u = it.useInCombat;
282
+ if (u && (u.effect === 'restore_health' || u.effect === 'restore_power')) {
283
+ const min = u.min ?? 0;
284
+ const max = u.max ?? min;
285
+ out.push({
286
+ entry,
287
+ name: it.name,
288
+ icon: it.icon,
289
+ // Which pool a mana gem / Thistle Tea fills. The bake's stamp when it has one, else the
290
+ // use-spell's own energize miscValue — a pre-powerType bake must not read as mana.
291
+ effect: {
292
+ kind: u.effect, min, max,
293
+ ...(u.effect === 'restore_power'
294
+ ? (() => {
295
+ const idx = restoredPowerTypeIndex(it, ds);
296
+ return idx != null ? { powerType: idx } : {};
297
+ })()
298
+ : {}),
299
+ },
300
+ ...uicCd(u, 0),
301
+ count,
302
+ ...spend,
303
+ });
304
+ continue;
305
+ }
306
+
307
+ // Resolve the on-use spell up front — branch 2 needs to know how many effects it has.
308
+ // SI5/SI6: resolve via interpreter when rawEffects exist (same path as combat).
309
+ const useSp = it.useSpell ?? (u?.effect === 'cast_spell' ? u.spellId : undefined);
310
+ const spRaw = useSp != null ? ds.spell(useSp) : undefined;
311
+ const sp = spRaw ? resolveCombatSpell(spRaw) : undefined;
312
+ // A real on-use only (item_template spelltrigger 0 / the useBuff classifier). Class-9 recipe
313
+ // books carry their TAUGHT spell in useSpell, and one of those must never ride as a cast.
314
+ const spellRidesWhole = (u != null || it.useBuff != null) && (sp?.effects.length ?? 0) > 1;
315
+
316
+ // Direct buff consumable (elixir / flask / scroll), only when there is no cast_spell
317
+ // useInCombat. A multi-effect spell rides its SpellDef along so itemUse casts it whole.
318
+ const useB = it.useBuff;
319
+ if (useB && u?.effect !== 'cast_spell') {
320
+ out.push({
321
+ entry,
322
+ name: it.name,
323
+ icon: it.icon,
324
+ effect: {
325
+ kind: 'apply_buff',
326
+ mod: useB.mod,
327
+ amount: useB.amount,
328
+ durationMs: useB.durationMs,
329
+ ...(spellRidesWhole && useSp != null && sp ? { spellId: useSp, spell: sp } : {}),
330
+ },
331
+ ...uicCd(u, 0),
332
+ count,
333
+ ...spend,
334
+ });
335
+ continue;
336
+ }
337
+
338
+ // 3) useSpell → buff / heal / hostile_cast. cast_spell useInCombat lands here with CDs from
339
+ // the item row. Skip food/drink restore_* (OOC only).
340
+ if (useSp == null || !spRaw || !sp) continue;
341
+ // Read through the triggers: a BG ration's restore sits two hops down (24411 → 25695/25696).
342
+ const payload = onUseEffects(sp, ds);
343
+ if (isFoodDrinkPayload(payload)) continue;
344
+ // 3a) HOSTILE on-use — grenades, bombs, Tidal Charm, Slumber Sand. Checked BEFORE the buff
345
+ // branches, or a bomb's cc slot falls past every one. The whole SpellDef rides to castOnTargets.
346
+ const friendlyPayload = hasFriendlyPayload(sp.effects);
347
+ if (hasHostilePayload(sp.effects) && !friendlyPayload) {
348
+ out.push({
349
+ entry,
350
+ name: it.name,
351
+ icon: it.icon,
352
+ effect: { kind: 'hostile_cast', spellId: useSp, spell: sp },
353
+ ...uicCd(u, sp.cooldownMs ?? 0),
354
+ count,
355
+ ...spend,
356
+ });
357
+ continue;
358
+ }
359
+
360
+ // 3b) DEFENSIVE on-use whose payload is an immunity, cleanse, reflect, or guardian summon
361
+ const defensiveEff = sp.effects.some(
362
+ (e) => e.kind === 'mechanic_immunity'
363
+ || e.kind === 'school_immunity'
364
+ || e.kind === 'damage_immunity'
365
+ || e.kind === 'dispel_immunity'
366
+ || e.kind === 'state_immunity'
367
+ || e.kind === 'effect_immunity'
368
+ || e.kind === 'cleanse'
369
+ || e.kind === 'dispel'
370
+ || e.kind === 'dispel_mechanic'
371
+ || e.kind === 'reflect'
372
+ || e.kind === 'summon_guardian',
373
+ );
374
+ if (defensiveEff) {
375
+ out.push({
376
+ entry,
377
+ name: it.name,
378
+ icon: it.icon,
379
+ effect: { kind: 'friendly_cast', spellId: useSp, spell: sp },
380
+ ...uicCd(u, sp.cooldownMs ?? 0),
381
+ count,
382
+ ...spend,
383
+ });
384
+ continue;
385
+ }
386
+
387
+ // 3c) Speed on-use — Swiftness Potion, Rocket Boots, Skull's Flee. A pure aura-31 payload is
388
+ // not a `buff`, so all four fell past every branch. It rides as a full SpellDef for the bucket.
389
+ if (sp.effects.some(isBeneficialSpeed)) {
390
+ const malfunction = rocketBootsMalfunction(entry, ds, resolveCombatSpell);
391
+ out.push({
392
+ entry,
393
+ name: it.name,
394
+ icon: it.icon,
395
+ effect: { kind: 'friendly_cast', spellId: useSp, spell: sp },
396
+ ...uicCd(u, sp.cooldownMs ?? 0),
397
+ count,
398
+ ...spend,
399
+ ...(malfunction ? { malfunction } : {}),
400
+ });
401
+ continue;
402
+ }
403
+
404
+ // A buff consumable whose spell lands more than one effect carries the WHOLE SpellDef, since
405
+ // mod+amount holds only the first. `mod` stays the primary for the UI label and buffPresent.
406
+ const buffEff = sp.effects.find((e) => e.kind === 'buff');
407
+ if (buffEff && buffEff.kind === 'buff') {
408
+ out.push({
409
+ entry,
410
+ name: it.name,
411
+ icon: it.icon,
412
+ effect: {
413
+ kind: 'apply_buff',
414
+ mod: buffEff.mod,
415
+ amount: buffEff.amount,
416
+ durationMs: buffEff.durationMs,
417
+ spellId: useSp,
418
+ ...(spellRidesWhole ? { spell: sp } : {}),
419
+ },
420
+ ...uicCd(u, sp.cooldownMs ?? 0),
421
+ count,
422
+ ...spend,
423
+ });
424
+ continue;
425
+ }
426
+
427
+ const healEff = sp.effects.find((e) => e.kind === 'heal');
428
+ if (healEff && healEff.kind === 'heal') {
429
+ out.push({
430
+ entry,
431
+ name: it.name,
432
+ icon: it.icon,
433
+ effect: { kind: 'restore_health', min: healEff.min, max: healEff.max },
434
+ ...uicCd(u, sp.cooldownMs ?? 0),
435
+ count,
436
+ });
437
+ continue;
438
+ }
439
+
440
+ // energize without bake useInCombat (legacy) → restore_power
441
+ const energizeEff = sp.effects.find((e) => e.kind === 'energize');
442
+ if (energizeEff && energizeEff.kind === 'energize') {
443
+ out.push({
444
+ entry,
445
+ name: it.name,
446
+ icon: it.icon,
447
+ effect: {
448
+ kind: 'restore_power', min: energizeEff.min, max: energizeEff.max,
449
+ powerType: energizeEff.powerType | 0,
450
+ },
451
+ ...uicCd(u, sp.cooldownMs ?? 0),
452
+ count,
453
+ });
454
+ continue;
455
+ }
456
+
457
+ // 3d) An absorb, resistance, proc aura, spell-mod, or a payload a trigger down: the whole
458
+ // SpellDef rides to castOnTargets. Cite: bgConsumes.ts:161, which probed for this hole.
459
+ if (
460
+ hasCombatSelfPayload(payload) || hasHostilePayload(payload)
461
+ || hasFriendlyPayload(payload) || onUseAimsAtEnemy(spRaw)
462
+ ) {
463
+ // The implicit target row is the server's own answer; the payload kinds only decide the
464
+ // rows that aim at TARGET_UNIT/none. Cite: SpellTargets.cpp:31.
465
+ const enemyBound = onUseAimsAtEnemy(spRaw)
466
+ || (hasHostilePayload(payload) && !hasFriendlyPayload(payload));
467
+ out.push({
468
+ entry,
469
+ name: it.name,
470
+ icon: it.icon,
471
+ effect: enemyBound
472
+ ? { kind: 'hostile_cast', spellId: useSp, spell: sp }
473
+ : { kind: 'friendly_cast', spellId: useSp, spell: sp },
474
+ ...uicCd(u, sp.cooldownMs ?? 0),
475
+ count,
476
+ ...spend,
477
+ });
478
+ continue;
479
+ }
480
+
481
+ // Fallback: combatUseEffectFromInterpret for edge shapes not in effects[].
482
+ if (spRaw.rawEffects?.length) {
483
+ const interpreted = resolveSpellEffects(spRaw);
484
+ const use = combatUseEffectFromInterpret(interpreted, useSp);
485
+ if (!use) continue;
486
+ if (use.kind === 'apply_buff') {
487
+ out.push({
488
+ entry,
489
+ name: it.name,
490
+ icon: it.icon,
491
+ effect: use,
492
+ ...uicCd(u, sp.cooldownMs ?? 0),
493
+ count,
494
+ });
495
+ } else {
496
+ out.push({
497
+ entry,
498
+ name: it.name,
499
+ icon: it.icon,
500
+ effect: { kind: 'restore_health', min: use.min, max: use.max },
501
+ ...uicCd(u, sp.cooldownMs ?? 0),
502
+ count,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ // Stamp the rank ladder each row belongs to. `CombatItem` is in-session only and createSession
508
+ // holds no DataStore, so a family row can only resolve if the kit says which ladder each is on.
509
+ for (const ci of out) {
510
+ const it = ds.item(ci.entry);
511
+ const family = consumableFamilyKey(it, ds);
512
+ if (family) {
513
+ ci.family = family;
514
+ ci.rank = it?.itemLevel ?? 0;
515
+ }
516
+ }
517
+ return out;
518
+ }
519
+
520
+ /** Sum MOD_STEALTH_LEVEL (154) from known passives/talents (Shadowmeld Passive, Master of Deception, …). */
521
+ export function stealthLevelBonusFromKnown(sb: SpellBook, ds: DataStore): number {
522
+ let n = 0;
523
+ for (const id of sb.known) {
524
+ const s = ds.spell(id);
525
+ if (!s) continue;
526
+ for (const e of s.effects) {
527
+ if (e.kind === 'stealth_level') n += e.amount | 0;
528
+ }
529
+ }
530
+ return n;
531
+ }
532
+
533
+ // Active MOD_STEALTH from OOC auras (Stealth / Prowl / Shadowmeld / Vanish). Returns the best (highest
534
+ // strength) active stealth aura, or null if none.
535
+ export function activeStealthAura(
536
+ char: Character,
537
+ ds: DataStore,
538
+ now: number = Date.now(),
539
+ ): { spellId: number; strength: number; breakFlags?: number } | null {
540
+ let best: { spellId: number; strength: number; breakFlags?: number } | null = null;
541
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
542
+ const s = ds.spell(a.spellId);
543
+ if (!s) continue;
544
+ for (const e of s.effects) {
545
+ if (e.kind !== 'stealth') continue;
546
+ if (!best || e.strength > best.strength) {
547
+ best = { spellId: s.id, strength: e.strength | 0, breakFlags: e.breakFlags };
548
+ }
549
+ }
550
+ }
551
+ return best;
552
+ }
553
+
554
+ // TYPE_DAMAGE flat amount for a paperdoll slot: gearEnchantGrant first, then the legacy EnchantDef
555
+ // cache, then item.weaponDamageFlat. A random prop stacks with PERM, and a broken item is inert.
556
+ function weaponDamageFlatForSlot(
557
+ char: Character,
558
+ ds: DataStore,
559
+ slot: 'mainhand' | 'offhand' | 'ranged',
560
+ ): number {
561
+ const entry = equippedEntry(char.equipped[slot]);
562
+ if (entry == null) return 0;
563
+ let flat = 0;
564
+ const inst = equippedInstance(char.equipped[slot]);
565
+ // LOCK 2: instance.permEnchantId first, then legacy slot map.
566
+ const enchId = inst?.permEnchantId ?? char.enchants?.[slot];
567
+ if (enchId != null) {
568
+ const grant = gearEnchantGrant(enchId, ds);
569
+ if (grant?.weaponDamageFlat) flat = grant.weaponDamageFlat;
570
+ else {
571
+ const enc = ds.enchant(enchId) as { weaponDamageFlat?: number } | undefined;
572
+ if (enc?.weaponDamageFlat) flat = enc.weaponDamageFlat;
573
+ }
574
+ }
575
+ const it = ds.item(entry);
576
+ if (!flat) flat = it?.weaponDamageFlat ?? 0;
577
+ // Random property TYPE_DAMAGE — stacks with profession PERM; suppressed when slot is broken
578
+ // (same inert rule as equippedWithEnchant for stats/armor).
579
+ if (it && !isBroken(char, slot, it)) {
580
+ const rpId = inst?.randomPropertyId;
581
+ if (rpId != null && rpId > 0) {
582
+ const prop = ds.itemRandomProperty(rpId);
583
+ if (prop?.weaponDamageFlat) flat += prop.weaponDamageFlat;
584
+ }
585
+ }
586
+ return flat;
587
+ }
588
+
589
+ // Fill the judgement burst from its own carrier spell in the store — damage range AND coefficient —
590
+ // instead of the transcribed tables, which are a frozen copy no re-bake can correct.
591
+ function withJudgeFromCarrier(
592
+ judge: JudgementBurst | undefined,
593
+ ds: DataStore,
594
+ ): JudgementBurst | undefined {
595
+ if (!judge?.spellId) return judge;
596
+ const carrier = ds.spell(judge.spellId);
597
+ if (!carrier) return judge;
598
+ const dmg = carrier.effects?.find((e) => e.kind === 'direct_damage');
599
+ const slot = dmg?.effectSlot ?? 0;
600
+ const coeff = carrier.effectBonusCoefficient?.[slot] ?? 0;
601
+ const next: JudgementBurst = { ...judge };
602
+ if (dmg && dmg.min > 0 && dmg.max >= dmg.min) {
603
+ next.min = dmg.min;
604
+ next.max = dmg.max;
605
+ }
606
+ if (judge.coeff == null && coeff > 0) next.coeff = coeff;
607
+ return next;
608
+ }
609
+
610
+ export function buildCombatant(
611
+ char: Character,
612
+ sb: SpellBook,
613
+ ds: DataStore,
614
+ inv?: Inventory,
615
+ ): CombatantSpec {
616
+ const ranged = rangedProfile(char, ds);
617
+ // Pass spellbook so CanParry requires trained Parry (EffectParry), not bare MH weapon.
618
+ // Cite: Player.cpp m_canParry; characterRatings.canParry; KI-818.
619
+ const canP = canParry(char, ds, sb);
620
+ const hasSh = hasShieldEquipped(char, ds);
621
+ const form = ((char.form ?? 0) | 0) as ShapeshiftFormId;
622
+ // The spec is the FORM-FREE body (fighterFactory.ts:744): applyFormCombatEnter adds the package, the
623
+ // Cat agility AP and these talent auras, and clearFormCombatBonuses takes all three back on a shift.
624
+ const formTalentAuras = formTalentAuraDeltas(char, ds, sb);
625
+ // The BAR follows the form even though the body does not: a druid carried into the next fight in
626
+ // Cat Form holds energy in `power` and its mana in `mana`. Both are read before the strip.
627
+ const pt = powerType(char);
628
+ const maxP = maxPower(char, ds);
629
+ char = char.form == null ? char : { ...char, form: undefined };
630
+ const baseWeapon = weaponProfile(char, ds);
631
+ const offhand = offhandWeaponProfile(char, ds);
632
+ // Form attack-time override (Player.cpp InitDataForForm): cat 1.0s / bear 2.5s.
633
+ // Forms that override attack speed do not dual-wield (single form timer).
634
+ const formAtk = formAttackTimeMs(form);
635
+ const dmgRange = resolveMeleeWeaponDamageRange(form, char.level, baseWeapon.dmgMin, baseWeapon.dmgMax, formAtk);
636
+ const weapon = formAtk > 0
637
+ ? { ...baseWeapon, speedMs: formAtk, dmgMin: dmgRange.min, dmgMax: dmgRange.max }
638
+ : baseWeapon;
639
+ const dualWield = formAtk <= 0 && offhand != null;
640
+ const mMana = maxMana(char, ds);
641
+ const startMana = pt === 'mana' ? char.power : (char.mana ?? mMana);
642
+ // Parallel mana for multi-power classes (Druid) — always present for display + form costs.
643
+ const multiPower = classHasParallelMana(char.cls);
644
+ // A seat SEEDED into a form never ran the shapeshift handler, and is told apart from a shifted
645
+ // druid by the parallel `mana` that shift splits off. Its pool starts where the handler leaves
646
+ // it. Cite: SpellAuras.cpp:1959; oocCastEffects.ts:478 writes the shifted sheet.
647
+ const seededIntoForm = pt !== 'mana' && multiPower && char.mana == null;
648
+ const startPower = pt === 'mana' ? char.power
649
+ : seededIntoForm ? Math.max(0, formResourceOnEnter(pt))
650
+ : Math.min(char.power, maxP);
651
+ const cMana = createMana(char, ds);
652
+ const cHealth = createHealth(char, ds);
653
+ const levelBonus = stealthLevelBonusFromKnown(sb, ds);
654
+ const activeStealth = activeStealthAura(char, ds);
655
+ const stats = effectiveStats(char, ds);
656
+ const gearStats = equippedStatBonuses(char, ds);
657
+ const mh = maxHealth(char, ds);
658
+ const victimAtt = (() => {
659
+ const rows: NonNullable<CombatantSpec['startAddTargetTriggers']> = [];
660
+ const skipHolders = new Set<number>();
661
+ for (const { holderSpellId } of activeTalentProcsWithHolder(char, ds)) {
662
+ const hid = holderSpellId | 0;
663
+ if (!hid || skipHolders.has(hid)) continue;
664
+ const holder = ds.spell(hid);
665
+ if (!holder) continue;
666
+ const att = victimAddTargetTriggerRows(holder, (id) => ds.spell(id));
667
+ if (!att.length) continue;
668
+ skipHolders.add(hid);
669
+ for (const r of att) rows.push({ spellId: hid, ...r });
670
+ }
671
+ return { rows, skipHolders };
672
+ })();
673
+ return {
674
+ name: char.name,
675
+ level: char.level,
676
+ gender: char.gender,
677
+ classId: char.cls,
678
+ ...(() => {
679
+ const equippedItemEntries: number[] = [];
680
+ for (const v of Object.values(char.equipped ?? {})) {
681
+ const e = equippedEntry(v);
682
+ if (e) equippedItemEntries.push(e);
683
+ }
684
+ return equippedItemEntries.length ? { equippedItemEntries } : {};
685
+ })(),
686
+ // TYPEID_PLAYER, whatever seat it takes. Only creatures skip this builder, so the flag is
687
+ // unconditional; the side fallback it replaces read a companion in an enemy seat as a creature.
688
+ isPlayerOrPet: true,
689
+ ...(char.cls === 5 || char.cls === 8 || char.cls === 9
690
+ ? { wandUser: true, casterGlance: true }
691
+ : {}),
692
+ // Live drunk state for in-combat alcohol. Cite: Player m_drunk; KI-734.
693
+ ...(typeof char.drunkValue === 'number' && char.drunkValue > 0
694
+ ? { drunkValue: Math.min(0xFFFF, char.drunkValue | 0) }
695
+ : {}),
696
+ ...(isHovering(char.movementFlags ?? 0)
697
+ ? { movementFlags: setHover(char.movementFlags ?? 0, true) }
698
+ : {}),
699
+ maxHealth: mh,
700
+ // Clamped, not passed through: a lapsed buff lowers maxHealth while char.health still holds the
701
+ // buffed value. `?? mh` guards an undefined health, and hasResourceBaseline the missing row.
702
+ // When entering in form, subtract form health bonus so applyFormCombatEnter restores live HP.
703
+ ...(() => {
704
+ const startFormHealth = form ? (formCombatPackage(form).healthFlat + (formTalentAuras?.[form]?.health ?? 0)) : 0;
705
+ const startHp = char.health != null ? Math.max(1, char.health - startFormHealth) : mh;
706
+ return { startHealth: hasResourceBaseline(char, ds) ? Math.min(startHp, mh) : char.health };
707
+ })(),
708
+ powerType: pt,
709
+ maxPower: maxP,
710
+ startPower,
711
+ // OCT mana regen inputs (combat 5SR + OOC). source: Unit.cpp OCTRegen*; StatSystem UpdateManaRegen
712
+ spirit: stats.spi,
713
+ // Gear ITEM_MOD_MANA_REGENERATION + buff mod 'mp5' (SPELL_AURA_MOD_POWER_REGEN total / 5s).
714
+ mp5: (gearStats.mp5 ?? 0) + buffMp5(char, ds),
715
+ // Meditation talents plus OOC auras 134 and 110 (Evocation ×16, Innervate), and aura 116
716
+ // MOD_REGEN_DURING_COMBAT from racials and live auras. Cite: StatSystem.cpp ~551-558; KI-898.
717
+ ...(() => {
718
+ let combatHpRegenPct = 0;
719
+ // Aura 88 MOD_HEALTH_REGEN_PERCENT multiplies the OOC spirit rate (Troll Regeneration 20555).
720
+ // Cite: Player.cpp:2393-2398; the same fold rest.ts:367 healthRegenPercentMultiplier runs.
721
+ let oocHpRegenMult = 1;
722
+ const seenRegen = new Set<number>();
723
+ const foldRegenDuringCombat = (spellId: number): void => {
724
+ const id = spellId | 0;
725
+ if (!id || seenRegen.has(id)) return;
726
+ seenRegen.add(id);
727
+ const raw = ds.spell(id);
728
+ if (!raw) return;
729
+ const sp = resolveCombatSpell(raw, { casterLevel: char.level });
730
+ for (const e of sp.effects ?? []) {
731
+ if (e.kind === 'mod_regen_during_combat') combatHpRegenPct += e.amount | 0;
732
+ else if (e.kind === 'mod_health_regen_percent' && (e.amount | 0)) {
733
+ oocHpRegenMult *= (100 + (e.amount | 0)) / 100;
734
+ }
735
+ }
736
+ };
737
+ for (const s of racialSpells(ds, char.race)) foldRegenDuringCombat(s.id);
738
+ for (const a of pruneExpiredAuras(char.activeAuras)) foldRegenDuringCombat(a.spellId);
739
+ return {
740
+ ...(combatHpRegenPct > 0 ? { healthRegenDuringCombatPct: combatHpRegenPct } : {}),
741
+ ...(oocHpRegenMult !== 1 ? { healthRegenPctMult: oocHpRegenMult } : {}),
742
+ };
743
+ })(),
744
+ ...(() => {
745
+ const { pctMult, interruptPct } = buffManaRegenAuras(char, ds);
746
+ return {
747
+ manaRegenInterruptPct: Math.min(
748
+ 100,
749
+ Math.max(0, talentManaRegenInterruptPct(char, ds) + interruptPct),
750
+ ),
751
+ manaRegenPctMult: pctMult,
752
+ };
753
+ })(),
754
+ // MOD_TARGET_RESISTANCE → GetResistancePenetration. Cite: Unit.cpp ~3890.
755
+ // Talents + equip / OOC buff auras (spell pen gear, Bonereaver proc shell).
756
+ resistancePenetration: mergeSchoolAmounts(
757
+ talentResistancePenetration(char, ds),
758
+ equipResistancePenetration(char, ds),
759
+ ),
760
+ // Creature-type versus AP and flat spell damage. Aura 59 MOD_DAMAGE_DONE_CREATURE is flat
761
+ // DoneFlat for spells AND melee, so it folds into both paths. Cite: Unit.cpp:7465, :7061.
762
+ ...(() => {
763
+ const ddc = damageDoneCreatureBonuses(char, ds);
764
+ const spellVs = spellDamageVersusBonuses(char, ds);
765
+ return {
766
+ attackPowerVersus: attackPowerVersusBonuses(char, ds),
767
+ rangedAttackPowerVersus: rangedAttackPowerVersusBonuses(char, ds),
768
+ spellDamageVersus: ddc.length ? [...spellVs, ...ddc] : spellVs,
769
+ damageDoneVersusPct: damageDoneVersusPctBonuses(char, ds),
770
+ // Aura 169 crit MULTIPLIER vs creature type (Monster / Humanoid Slaying).
771
+ critDamageVersusPct: critDamageVersusPctBonuses(char, ds),
772
+ ...(ddc.length ? { damageDoneCreature: ddc } : {}),
773
+ };
774
+ })(),
775
+ // Aura 122 off-hand TOTAL_PCT and the victim-side 185-188 chance auras. Both are talent-only
776
+ // in vanilla, so they read zero until a rank is spent. Cite: SpellAuras.cpp:4175; Unit.cpp:3525.
777
+ ...(() => {
778
+ const offhand = offhandDamagePctBonus(char, ds);
779
+ const mCrit = attackerChanceBonus(char, ds, 'attacker_melee_crit');
780
+ const rCrit = attackerChanceBonus(char, ds, 'attacker_ranged_crit');
781
+ const rHit = attackerChanceBonus(char, ds, 'attacker_ranged_hit');
782
+ const sHit = attackerSpellHitBySchool(char, ds);
783
+ const scriptIds = overrideClassScriptIds(char, ds, sb);
784
+ return {
785
+ ...(offhand ? { offhandDamagePct: offhand } : {}),
786
+ ...(mCrit ? { attackerMeleeCrit: mCrit } : {}),
787
+ ...(rCrit ? { attackerRangedCrit: rCrit } : {}),
788
+ ...(rHit ? { attackerRangedHit: rHit } : {}),
789
+ ...(Object.keys(sHit).length ? { attackerSpellHitBySchool: sHit } : {}),
790
+ ...(scriptIds.length ? { overrideScriptIds: scriptIds } : {}),
791
+ };
792
+ })(),
793
+ // MOD_THREAT → m_threatModifier[school]. Cite: SpellAuras.cpp HandleModThreat.
794
+ threatModifierBySchool: talentThreatModifiers(char, ds),
795
+ // createMana is the BASE pool a ManaCostPercentage spell is a percentage OF. Stamped only for
796
+ // parallel-mana classes before, so every other % cost fell back to maxMana and overcharged.
797
+ createMana: cMana,
798
+ ...(multiPower ? { mana: startMana, maxMana: mMana } : {}),
799
+ // POWER_HEALTH % costs (Bloodrage) — always available when classlevel baseHp is known.
800
+ ...(cHealth > 0 ? { createHealth: cHealth } : {}),
801
+ ...(form ? { form } : {}),
802
+ ...(activeStealth
803
+ ? { stealth: {
804
+ spellId: activeStealth.spellId,
805
+ strength: activeStealth.strength,
806
+ levelBonus,
807
+ breakFlags: activeStealth.breakFlags,
808
+ } }
809
+ : levelBonus > 0 ? { stealthLevelBonus: levelBonus } : {}),
810
+ baseWeaponSpeedMs: baseWeapon.speedMs,
811
+ baseWeaponDmgMin: baseWeapon.dmgMin,
812
+ baseWeaponDmgMax: baseWeapon.dmgMax,
813
+ armor: effectiveArmor(char, ds),
814
+ // The slice a mid-fight aura 142 multiplies — item armor only, agi×2 and flat buffs excluded.
815
+ baseArmor: Math.floor(baseArmorValue(char, ds)),
816
+ weapon,
817
+ // Off-hand weapon only when dual-wielding (not shield). Forms strip OH autos via dualWield false.
818
+ ...(dualWield && offhand ? { offhand, dualWield: true as const } : {}),
819
+ // A wand lights up Shoot on demand; a Hunter with a bow/gun auto-attacks at range.
820
+ ranged: ranged ?? undefined,
821
+ rangedAuto: !!ranged && classUsesRangedAuto(char.cls),
822
+ // Ammo DPS only when the selection is compatible and count ≥ 1, mirroring _ApplyAmmoBonuses.
823
+ // The EquippedItem* subclasses for HasItemFitToSpellReqirements ride the same paperdoll walk.
824
+ ...(() => {
825
+ const out: {
826
+ ammoDpsMin?: number;
827
+ ammoDpsMax?: number;
828
+ ammoEntry?: number;
829
+ ammoRemaining?: number;
830
+ ammoSubclass?: number;
831
+ rangedWeaponSubclass?: number;
832
+ mainhandWeaponSubclass?: number;
833
+ offhandWeaponSubclass?: number;
834
+ brokenMainhandWeaponSubclass?: number;
835
+ brokenOffhandWeaponSubclass?: number;
836
+ brokenRangedWeaponSubclass?: number;
837
+ offhandArmorSubclass?: number;
838
+ bodyArmorSubclasses?: number[];
839
+ rangedArmorSubclass?: number;
840
+ equippedItemFitReady: true;
841
+ thrownRemaining?: number;
842
+ thrownEntry?: number;
843
+ } = { equippedItemFitReady: true };
844
+
845
+ // MH / OH / body / ranged-armor subclasses for the EquippedItem* fit. A broken weapon stays
846
+ // equipped but combat-inert, recorded under broken* so the gate says "broken", not "none".
847
+ const mhEntry = equippedEntry(char.equipped.mainhand);
848
+ const mhItem = mhEntry != null ? ds.item(mhEntry) : undefined;
849
+ if (mhItem?.weapon) {
850
+ const mhSub = weaponSubclassOf(mhItem);
851
+ if (mhSub != null) {
852
+ if (isBroken(char, 'mainhand', mhItem)) out.brokenMainhandWeaponSubclass = mhSub;
853
+ else out.mainhandWeaponSubclass = mhSub;
854
+ }
855
+ }
856
+ const ohEntry = equippedEntry(char.equipped.offhand);
857
+ const ohItem = ohEntry != null ? ds.item(ohEntry) : undefined;
858
+ if (ohItem) {
859
+ if (ohItem.weapon) {
860
+ const ohSub = weaponSubclassOf(ohItem);
861
+ if (ohSub != null) {
862
+ if (isBroken(char, 'offhand', ohItem)) out.brokenOffhandWeaponSubclass = ohSub;
863
+ else out.offhandWeaponSubclass = ohSub;
864
+ }
865
+ } else if (!isBroken(char, 'offhand', ohItem)) {
866
+ // Shield / holdable armor — ITEM_CLASS_ARMOR in OH slot.
867
+ // Cite: Player.cpp:18611–18614; ItemPrototype armor subclasses.
868
+ const aSub = ohItem.armorSubclass
869
+ ?? (ohItem.typeName === 'Shield' ? 6 : undefined);
870
+ if (aSub != null) out.offhandArmorSubclass = aSub;
871
+ }
872
+ }
873
+ // Body slots = EQUIPMENT_SLOT_START..MAINHAND exclusive (Player.cpp:18605–18609).
874
+ const BODY_SLOTS = [
875
+ 'head', 'neck', 'shoulder', 'shirt', 'chest', 'waist', 'legs', 'feet',
876
+ 'wrist', 'hands', 'finger1', 'finger2', 'trinket1', 'trinket2', 'back',
877
+ ] as const;
878
+ const bodySubs: number[] = [];
879
+ for (const slot of BODY_SLOTS) {
880
+ const entry = equippedEntry(char.equipped[slot]);
881
+ if (entry == null) continue;
882
+ const it = ds.item(entry);
883
+ if (!it || isBroken(char, slot, it) || it.weapon) continue;
884
+ if (it.armorSubclass != null) bodySubs.push(it.armorSubclass);
885
+ }
886
+ if (bodySubs.length) out.bodyArmorSubclasses = bodySubs;
887
+
888
+ const rangedEntry = equippedEntry(char.equipped.ranged);
889
+ const weapon = rangedEntry != null ? ds.item(rangedEntry) : undefined;
890
+ if (weapon) {
891
+ if (weapon.weapon) {
892
+ const wSub = weaponSubclassOf(weapon);
893
+ if (wSub != null) {
894
+ if (isBroken(char, 'ranged', weapon)) out.brokenRangedWeaponSubclass = wSub;
895
+ else out.rangedWeaponSubclass = wSub;
896
+ }
897
+ } else if (!isBroken(char, 'ranged', weapon) && weapon.armorSubclass != null) {
898
+ // Relic (libram/idol/totem) in ranged — armor class scan.
899
+ // Cite: Player.cpp:18616–18619.
900
+ out.rangedArmorSubclass = weapon.armorSubclass;
901
+ }
902
+ }
903
+
904
+ if (weaponIsThrown(weapon) && rangedEntry != null) {
905
+ // Equipped unit + bag stacks (stackable thrown). Cite: Spell.cpp TakeAmmo thrown.
906
+ const bagN = inv ? countItem(inv, rangedEntry) : 0;
907
+ out.thrownRemaining = bagN + 1;
908
+ out.thrownEntry = rangedEntry;
909
+ return out;
910
+ }
911
+ if (weaponIsWand(weapon) || !weaponNeedsProjectileAmmo(weapon)) return out;
912
+
913
+ // Always expose empty ammo state so combat NO_AMMO gate cannot skip free shots.
914
+ // Cite: Spell.cpp CheckCast — missing PLAYER_AMMO_ID / count → fail.
915
+ out.ammoRemaining = 0;
916
+
917
+ if (char.ammoItem == null) return out;
918
+ const ammo = ds.item(char.ammoItem);
919
+ if (!ammo?.ammoDps) return out;
920
+ if (!checkAmmoCompatibility(weapon, ammo)) return out;
921
+ const have = inv ? countItem(inv, char.ammoItem) : 1; // no inv → assume present for unit tests
922
+ if (have < 1) return out;
923
+ out.ammoDpsMin = ammo.ammoDps.dpsMin;
924
+ out.ammoDpsMax = ammo.ammoDps.dpsMax;
925
+ out.ammoEntry = char.ammoItem;
926
+ out.ammoRemaining = have;
927
+ const aSub = projectileSubclassOf(ammo);
928
+ if (aSub != null) out.ammoSubclass = aSub;
929
+ return out;
930
+ })(),
931
+ // TYPE_DAMAGE enchant flats when baked on EnchantDef or item (optional fields).
932
+ enchantFlatMh: weaponDamageFlatForSlot(char, ds, 'mainhand'),
933
+ enchantFlatOh: weaponDamageFlatForSlot(char, ds, 'offhand'),
934
+ enchantFlatRanged: weaponDamageFlatForSlot(char, ds, 'ranged'),
935
+ // CombatantSpec.attackPower is form-package-free (humanoid base); combat applies the Cat AGI
936
+ // and shapeshift package on form enter. The sheet UI still includes form AGI.
937
+ attackPower: attackPower({ ...char, form: 0 }, ds),
938
+ // TEMP_ENCHANTMENT_SLOT imbues and poisons → combat procs and the rotation gate. When apply
939
+ // stamped only combatSpellId, materialize from the combat spell so Instant Poison really procs.
940
+ tempEnchants: (() => {
941
+ const now = Date.now();
942
+ const out: CombatantSpec['tempEnchants'] = {};
943
+ for (const hand of ['mainhand', 'offhand'] as const) {
944
+ const te = getTempEnchant(char, hand, now);
945
+ if (!te) continue;
946
+ let combat = te.combat;
947
+ if (!combat && te.combatSpellId) {
948
+ const proc = ds.spell(te.combatSpellId);
949
+ if (proc) {
950
+ const resolved = resolveCombatSpell(proc, { casterLevel: char.level });
951
+ combat = combatPayloadFromSpell(resolved) as typeof te.combat;
952
+ }
953
+ }
954
+ out[hand] = {
955
+ spellId: te.spellId,
956
+ name: ds.spell(te.spellId)?.name ?? `imbue ${te.spellId}`,
957
+ expiresAt: te.expiresAt ?? (now + 30 * 60 * 1000),
958
+ enchantId: te.enchantId,
959
+ family: te.family,
960
+ attackPower: te.attackPower,
961
+ combatChancePct: te.combatChancePct,
962
+ combatPpm: te.combatPpm,
963
+ damageFlat: te.damageFlat,
964
+ rockbiterAmount: te.rockbiterAmount,
965
+ combat,
966
+ };
967
+ // Mirror buff so rotation buffPresent / UI see the imbue mid-fight too.
968
+ }
969
+ // KI-680: a Firestone equip is an ENCHANT_HELD_ITEM combat kit, so when OOC tempEnchants was
970
+ // cleared but the stone is still equipped, re-seed the main hand.
971
+ if (!out.mainhand) {
972
+ const held = combatHeldEnchantFromEquipped(char, ds);
973
+ if (held) {
974
+ let combat = held.combat;
975
+ if (!combat && held.combatSpellId) {
976
+ const proc = ds.spell(held.combatSpellId);
977
+ if (proc) {
978
+ const resolved = resolveCombatSpell(proc, { casterLevel: char.level });
979
+ combat = combatPayloadFromSpell(resolved) as typeof held.combat;
980
+ }
981
+ }
982
+ out.mainhand = {
983
+ spellId: held.spellId,
984
+ name: ds.spell(held.spellId)?.name ?? `imbue ${held.spellId}`,
985
+ expiresAt: now + 30 * 60 * 1000,
986
+ enchantId: held.enchantId,
987
+ combatChancePct: held.combatChancePct,
988
+ combatPpm: held.combatPpm,
989
+ combat,
990
+ };
991
+ }
992
+ }
993
+ return Object.keys(out).length ? out : undefined;
994
+ })(),
995
+ // OOC damage shields (Lightning Shield, Thorns) → combat with charges. The bake may leave
996
+ // effects:[] for an aura-42 LS, so resolve at pull the way castOutOfCombat does.
997
+ startDamageShields: (() => {
998
+ const now = Date.now();
999
+ const out: NonNullable<CombatantSpec['startDamageShields']> = [];
1000
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1001
+ const raw = ds.spell(a.spellId);
1002
+ if (!raw) continue;
1003
+ const sp = resolveCombatSpell(raw, { casterLevel: char.level });
1004
+ const dse = sp.effects.find((e) => e.kind === 'damage_shield');
1005
+ if (!dse || dse.kind !== 'damage_shield') continue;
1006
+ const remainingMs = a.expiresAt != null
1007
+ ? Math.max(0, a.expiresAt - now)
1008
+ : dse.durationMs;
1009
+ if (remainingMs <= 0) continue;
1010
+ const charges = (a.charges != null && a.charges > 0)
1011
+ ? a.charges
1012
+ : (dse.charges != null && dse.charges > 0
1013
+ ? dse.charges
1014
+ : ((sp.procCharges ?? 0) > 0 ? sp.procCharges : undefined));
1015
+ out.push({
1016
+ spellId: sp.id,
1017
+ name: sp.name,
1018
+ school: dse.school,
1019
+ min: dse.min,
1020
+ max: dse.max,
1021
+ remainingMs,
1022
+ ...(charges != null ? { charges } : {}),
1023
+ cooldownMs: dse.cooldownMs ?? sp.spellProcEvent?.cooldownMs,
1024
+ });
1025
+ }
1026
+ return out.length ? out : undefined;
1027
+ })(),
1028
+ // OOC aura-42 procs (Frost Armor chill, Nature's Grasp, Touch of Weakness). The bake often
1029
+ // keeps only the armor row, so resolveCombatSpell materializes the proc from rawEffects.
1030
+ startProcTriggers: (() => {
1031
+ const now = Date.now();
1032
+ const out: NonNullable<CombatantSpec['startProcTriggers']> = [];
1033
+ const seenHolders = new Set<number>();
1034
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1035
+ const raw = ds.spell(a.spellId);
1036
+ if (!raw) continue;
1037
+ const sp = resolveCombatSpell(raw, { casterLevel: char.level });
1038
+ const pe = sp.effects.find((e) => e.kind === 'proc_trigger_spell');
1039
+ if (!pe || pe.kind !== 'proc_trigger_spell') continue;
1040
+ const remainingMs = a.expiresAt != null
1041
+ ? Math.max(0, a.expiresAt - now)
1042
+ : (pe.durationMs > 0 ? pe.durationMs : 600_000);
1043
+ if (remainingMs <= 0) continue;
1044
+ // Deal vs take filtering + chance/PPM from spell_template / spell_proc_event.
1045
+ // Cite: UnitAuraProcHandler.cpp:523–526, :590–595; SpellMgr.h ProcFlags.
1046
+ const procFlags = sp.procFlags ?? sp.spellProcEvent?.procFlags;
1047
+ const procPpm = sp.spellProcEvent?.ppmRate ?? null;
1048
+ let procChance: number | null = null;
1049
+ if (sp.spellProcEvent?.customChance != null && sp.spellProcEvent.customChance > 0) {
1050
+ const c = sp.spellProcEvent.customChance;
1051
+ procChance = c > 1 ? Math.min(1, c / 100) : Math.min(1, c);
1052
+ } else if (sp.procChance != null && sp.procChance > 0) {
1053
+ // DB ProcChance is percent. Cite: UnitAuraProcHandler roll_chance_f(percent).
1054
+ procChance = Math.min(1, sp.procChance / 100);
1055
+ }
1056
+ seenHolders.add(sp.id);
1057
+ out.push({
1058
+ spellId: sp.id,
1059
+ name: sp.name,
1060
+ triggerSpellId: pe.triggerSpellId,
1061
+ remainingMs,
1062
+ charges: a.charges ?? pe.charges ?? sp.procCharges,
1063
+ cooldownMs: pe.cooldownMs ?? sp.spellProcEvent?.cooldownMs,
1064
+ ...(procFlags != null && procFlags !== 0 ? { procFlags } : {}),
1065
+ ...(procPpm != null && procPpm > 0 ? { procPpm } : {}),
1066
+ ...(procChance != null ? { procChance } : {}),
1067
+ });
1068
+ }
1069
+ // Permanent chest Absorption etc. — equip_spell proc_spell never lands in activeAuras.
1070
+ // Cite: Player.cpp ApplyEnchantment TYPE_EQUIP_SPELL; KI-1045.
1071
+ for (const row of permanentEnchantProcTriggers(char, ds)) {
1072
+ if (seenHolders.has(row.spellId)) continue;
1073
+ seenHolders.add(row.spellId);
1074
+ out.push(row);
1075
+ }
1076
+ return out.length ? out : undefined;
1077
+ })(),
1078
+ // OOC school_absorb (PW:Shield, the Wards, Ice Barrier) → combat with remaining duration. Many
1079
+ // ranks bake effects=[] with raw aura 69, so a hollow aura must still seed a pool.
1080
+ // Boots enchant and speed talents ride as permanent aura rows, so a Sprint or a pair of Rocket
1081
+ // Boots REPLACES them rather than adding to them. Cite: Unit.cpp:8352 GetMaxPositiveAuraModifier.
1082
+ permanentSpeedAuras: (() => {
1083
+ const rows = permanentSpeedAuras(char, ds);
1084
+ return rows.length ? rows : undefined;
1085
+ })(),
1086
+ startSchoolAbsorbs: (() => {
1087
+ const now = Date.now();
1088
+ const out: NonNullable<CombatantSpec['startSchoolAbsorbs']> = [];
1089
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1090
+ const spRaw = ds.spell(a.spellId);
1091
+ if (!spRaw) continue;
1092
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1093
+ const se = sp.effects.find((e) => e.kind === 'school_absorb');
1094
+ if (!se || se.kind !== 'school_absorb') continue;
1095
+ const remainingMs = a.expiresAt != null
1096
+ ? Math.max(0, a.expiresAt - now)
1097
+ : se.durationMs;
1098
+ if (remainingMs <= 0) continue;
1099
+ out.push({
1100
+ spellId: sp.id,
1101
+ name: sp.name,
1102
+ schoolMask: se.schoolMask,
1103
+ min: se.min,
1104
+ max: se.max,
1105
+ remainingMs,
1106
+ });
1107
+ }
1108
+ return out.length ? out : undefined;
1109
+ })(),
1110
+ // OOC restore_health HoTs (Renew, Rejuvenation) → combat with remaining duration, resolving raw
1111
+ // when the bake is empty. `pctOfMax` is OBS_MOD_HEALTH (Fel Stamina 18790).
1112
+ startRestoreHealth: (() => {
1113
+ const now = Date.now();
1114
+ const out: NonNullable<CombatantSpec['startRestoreHealth']> = [];
1115
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1116
+ const spRaw = ds.spell(a.spellId);
1117
+ if (!spRaw) continue;
1118
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1119
+ const he = sp.effects.find((e) => e.kind === 'restore_health');
1120
+ if (!he || he.kind !== 'restore_health') continue;
1121
+ // Pet-target HoT (Health Funnel / Mend Pet) is on the pet, not the owner.
1122
+ if (he.applyTo === 'pet' || sp.role === 'pet_heal') continue;
1123
+ const remainingMs = a.expiresAt != null
1124
+ ? Math.max(0, a.expiresAt - now)
1125
+ : he.durationMs;
1126
+ if (remainingMs <= 0) continue;
1127
+ out.push({
1128
+ spellId: sp.id,
1129
+ name: sp.name,
1130
+ amountPerTick: he.amountPerTick,
1131
+ tickMs: he.tickMs,
1132
+ remainingMs,
1133
+ ...((he.pctOfMax ?? 0) > 0 ? { pctOfMax: Math.trunc(he.pctOfMax!) } : {}),
1134
+ });
1135
+ }
1136
+ return out.length ? out : undefined;
1137
+ })(),
1138
+ // OOC restore_power (Fel Energy 18792 OBS_MOD_MANA) → combat ticks.
1139
+ // Cite: SpellAuras.cpp OBS_MOD_MANA; Warlock.cpp DemonicSacrifice Felhunter; KI-833.
1140
+ startRestorePower: (() => {
1141
+ const now = Date.now();
1142
+ const out: NonNullable<CombatantSpec['startRestorePower']> = [];
1143
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1144
+ const spRaw = ds.spell(a.spellId);
1145
+ if (!spRaw) continue;
1146
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1147
+ const pe = sp.effects.find((e) => e.kind === 'restore_power');
1148
+ if (!pe || pe.kind !== 'restore_power') continue;
1149
+ // Skip hostile leech (Viper Sting): amountPerTick < 0 without pctOfMax.
1150
+ if ((pe.amountPerTick ?? 0) < 0 && !(pe.pctOfMax && pe.pctOfMax > 0)) continue;
1151
+ const remainingMs = a.expiresAt != null
1152
+ ? Math.max(0, a.expiresAt - now)
1153
+ : pe.durationMs;
1154
+ if (remainingMs <= 0) continue;
1155
+ out.push({
1156
+ spellId: sp.id,
1157
+ name: sp.name,
1158
+ amountPerTick: pe.amountPerTick,
1159
+ tickMs: pe.tickMs,
1160
+ remainingMs,
1161
+ ...((pe.pctOfMax ?? 0) > 0 ? { pctOfMax: Math.trunc(pe.pctOfMax!) } : {}),
1162
+ ...(pe.powerType != null ? { powerType: pe.powerType | 0 } : {}),
1163
+ });
1164
+ }
1165
+ return out.length ? out : undefined;
1166
+ })(),
1167
+ // OOC mana_shield → combat by the same import path as school_absorb. Mana Shield ranks bake
1168
+ // effects=[] with raw aura 97, so they materialize through resolveCombatSpell.
1169
+ startManaShields: (() => {
1170
+ const now = Date.now();
1171
+ const out: NonNullable<CombatantSpec['startManaShields']> = [];
1172
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1173
+ const spRaw = ds.spell(a.spellId);
1174
+ if (!spRaw) continue;
1175
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1176
+ const me = sp.effects.find((e) => e.kind === 'mana_shield');
1177
+ if (!me || me.kind !== 'mana_shield') continue;
1178
+ const remainingMs = a.expiresAt != null
1179
+ ? Math.max(0, a.expiresAt - now)
1180
+ : me.durationMs;
1181
+ if (remainingMs <= 0) continue;
1182
+ out.push({
1183
+ spellId: sp.id,
1184
+ name: sp.name,
1185
+ schoolMask: me.schoolMask,
1186
+ min: me.min,
1187
+ max: me.max,
1188
+ manaMultiplier: me.manaMultiplier,
1189
+ remainingMs,
1190
+ });
1191
+ }
1192
+ return out.length ? out : undefined;
1193
+ })(),
1194
+ // OOC next_cast_mod (Presence of Mind, Nature's Swiftness, Cold Blood) → pendingSpellMod. Bake
1195
+ // rows are often effects:[] with aura 107/108 only, so the engine effect materializes here.
1196
+ startPendingSpellMod: (() => {
1197
+ const now = Date.now();
1198
+ let best: NonNullable<CombatantSpec['startPendingSpellMod']> | undefined;
1199
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1200
+ const spRaw = ds.spell(a.spellId);
1201
+ if (!spRaw) continue;
1202
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1203
+ const mod = pendingSpellModFromResolved(sp);
1204
+ if (!mod) continue;
1205
+ const me = sp.effects.find((e) => e.kind === 'next_cast_mod');
1206
+ const scriptedDur = me && me.kind === 'next_cast_mod' ? me.durationMs : 0;
1207
+ const remainingMs = a.expiresAt != null
1208
+ ? Math.max(0, a.expiresAt - now)
1209
+ : (scriptedDur > 0 ? scriptedDur : 600_000);
1210
+ if (remainingMs <= 0) continue;
1211
+ // Last matching aura wins (combat also replaces any prior pendingSpellMod).
1212
+ best = { ...mod, remainingMs };
1213
+ }
1214
+ return best;
1215
+ })(),
1216
+ // Lasting 107/108 from OOC auras + known/set-bonus passives (not talent ranks —
1217
+ // those stay on withTalentMods). Cite: Player.cpp AddSpellMod; spellModIntents.ts.
1218
+ startOwnedSpellMods: (() => {
1219
+ const now = Date.now();
1220
+ const out: NonNullable<CombatantSpec['startOwnedSpellMods']> = [];
1221
+ const seen = new Set<number>();
1222
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1223
+ const spRaw = ds.spell(a.spellId);
1224
+ if (!spRaw) continue;
1225
+ const remainingMs = a.expiresAt != null ? a.expiresAt - now : 1;
1226
+ if (a.expiresAt != null && remainingMs <= 0) continue;
1227
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1228
+ const me = sp.effects.find((e) => e.kind === 'owned_spell_mods');
1229
+ if (!me || me.kind !== 'owned_spell_mods') continue;
1230
+ seen.add(sp.id);
1231
+ for (const m of me.mods) {
1232
+ out.push({
1233
+ spellId: sp.id,
1234
+ op: m.op,
1235
+ type: m.type,
1236
+ value: m.amount,
1237
+ familyName: sp.spellFamilyName,
1238
+ familyFlags: m.familyFlags,
1239
+ });
1240
+ }
1241
+ }
1242
+ const talentRankIds = new Set<number>();
1243
+ for (const tree of ds.talentTrees(char.cls)) {
1244
+ for (const t of tree.talents ?? []) {
1245
+ for (const r of t.ranks ?? []) {
1246
+ if (r.spellId) talentRankIds.add(r.spellId | 0);
1247
+ }
1248
+ }
1249
+ }
1250
+ const considerPassive = (id: number) => {
1251
+ if (seen.has(id) || talentRankIds.has(id)) return;
1252
+ const raw = ds.spell(id);
1253
+ if (!raw?.passive) return;
1254
+ const mods = ownedSpellModsFromSpell(raw);
1255
+ if (!mods.length) return;
1256
+ seen.add(id);
1257
+ out.push(...mods);
1258
+ };
1259
+ for (const id of sb.known) considerPassive(id);
1260
+ for (const id of activeSetBonusSpells(char, ds)) considerPassive(id);
1261
+ return out.length ? out : undefined;
1262
+ })(),
1263
+ // OOC Combustion → combat fire-crit counter. Cite: UnitAuraProcHandler.cpp:991-1003.
1264
+ startCombustion: (() => {
1265
+ const now = Date.now();
1266
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1267
+ const spRaw = ds.spell(a.spellId);
1268
+ if (!spRaw) continue;
1269
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1270
+ const ce = sp.effects.find((e) => e.kind === 'combustion');
1271
+ if (!ce || ce.kind !== 'combustion') continue;
1272
+ const remainingMs = a.expiresAt != null ? Math.max(0, a.expiresAt - now) : 600_000;
1273
+ if (remainingMs <= 0) continue;
1274
+ const charges = (a.charges != null && a.charges > 0) ? a.charges : ce.charges;
1275
+ if (charges <= 0) continue;
1276
+ return { spellId: sp.id, name: sp.name, charges, stacks: 1, remainingMs };
1277
+ }
1278
+ return undefined;
1279
+ })(),
1280
+ // OOC Sweeping Strikes → combat charge counter. Cite: UnitAuraProcHandler.cpp:810–841.
1281
+ startSweepingStrikes: (() => {
1282
+ const now = Date.now();
1283
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1284
+ const spRaw = ds.spell(a.spellId);
1285
+ if (!spRaw) continue;
1286
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1287
+ const se = sp.effects.find((e) => e.kind === 'sweeping_strikes');
1288
+ if (!se || se.kind !== 'sweeping_strikes') continue;
1289
+ const remainingMs = a.expiresAt != null
1290
+ ? Math.max(0, a.expiresAt - now)
1291
+ : (se.durationMs > 0 ? se.durationMs : 20_000);
1292
+ if (remainingMs <= 0) continue;
1293
+ const charges = (a.charges != null && a.charges > 0) ? a.charges : se.charges;
1294
+ if (charges <= 0) continue;
1295
+ return { spellId: sp.id, name: sp.name, charges, remainingMs };
1296
+ }
1297
+ return undefined;
1298
+ })(),
1299
+ // OOC Vampiric Embrace → combat leech pct. Cite: UnitAuraProcHandler.cpp:1015–1028.
1300
+ startVampiricEmbrace: (() => {
1301
+ const now = Date.now();
1302
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1303
+ const spRaw = ds.spell(a.spellId);
1304
+ if (!spRaw) continue;
1305
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1306
+ const se = sp.effects.find((e) => e.kind === 'vampiric_embrace');
1307
+ if (!se || se.kind !== 'vampiric_embrace') continue;
1308
+ const remainingMs = a.expiresAt != null
1309
+ ? Math.max(0, a.expiresAt - now)
1310
+ : (se.durationMs > 0 ? se.durationMs : 60_000);
1311
+ if (remainingMs <= 0) continue;
1312
+ return { spellId: sp.id, name: sp.name, pct: se.pct, remainingMs };
1313
+ }
1314
+ return undefined;
1315
+ })(),
1316
+ // OOC paladin seal → combat ActiveSeal and buff bar. The bake is often effects:[], so
1317
+ // resolveCombatSpell materializes the seal through paladin-seals.ts. Cite: KI-894.
1318
+ startActiveSeal: (() => {
1319
+ const now = Date.now();
1320
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1321
+ const spRaw = ds.spell(a.spellId);
1322
+ if (!spRaw) continue;
1323
+ const sp = resolveCombatSpell(spRaw, { casterLevel: char.level });
1324
+ const se = sp.effects.find((e) => e.kind === 'seal');
1325
+ if (!se || se.kind !== 'seal') continue;
1326
+ const remainingMs = a.expiresAt != null
1327
+ ? Math.max(0, a.expiresAt - now)
1328
+ : (se.durationMs > 0 ? se.durationMs : 30_000);
1329
+ if (remainingMs <= 0) continue;
1330
+ // One seal — last matching aura wins (OOC cast already exclusivity-filters).
1331
+ return {
1332
+ spellId: sp.id,
1333
+ name: sp.name,
1334
+ seal: se.seal,
1335
+ coeff: se.coeff,
1336
+ procPpm: se.procPpm,
1337
+ onSwingWeaponPct: se.onSwingWeaponPct,
1338
+ onSwingHeal: se.onSwingHeal,
1339
+ onSwingMana: se.onSwingMana,
1340
+ judge: withJudgeFromCarrier(se.judge, ds),
1341
+ remainingMs,
1342
+ };
1343
+ }
1344
+ return undefined;
1345
+ })(),
1346
+ // OOC school_immunity (Divine Shield / DP / BoP) → combat ImmunityState.
1347
+ // Cite: SPELL_AURA_SCHOOL_IMMUNITY; KI-086 pre-pull DS; createSession seedSchoolImmunities.
1348
+ startSchoolImmunities: (() => {
1349
+ const now = Date.now();
1350
+ const out: NonNullable<CombatantSpec['startSchoolImmunities']> = [];
1351
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1352
+ // Prefer resolved effects so thin bake rows still carry immunity (SI5).
1353
+ const spRaw = ds.spell(a.spellId);
1354
+ if (!spRaw) continue;
1355
+ const sp = resolveCombatSpell(spRaw);
1356
+ for (const se of sp.effects) {
1357
+ if (se.kind !== 'school_immunity') continue;
1358
+ const remainingMs = a.expiresAt != null
1359
+ ? Math.max(0, a.expiresAt - now)
1360
+ : (se.durationMs > 0 ? se.durationMs : 0);
1361
+ if (remainingMs <= 0) continue;
1362
+ out.push({
1363
+ spellId: sp.id,
1364
+ name: sp.name,
1365
+ schoolMask: se.schoolMask | 0,
1366
+ remainingMs,
1367
+ positive: se.positive !== false,
1368
+ mechanic: mechanicIdFromName(sp.mechanic) || undefined,
1369
+ });
1370
+ }
1371
+ }
1372
+ return out.length ? out : undefined;
1373
+ })(),
1374
+ // OOC buffs, aspects, forms and auras → the combat buff bar; the start* importers own shield,
1375
+ // HoT and absorb rows. Many self-buffs ship empty effects[], so resolve them the same way.
1376
+ startDisplayAuras: (() => {
1377
+ const now = Date.now();
1378
+ const out: NonNullable<CombatantSpec['startDisplayAuras']> = [];
1379
+ // An allowlist, not a denylist: characterRatings already folds crit and defense from
1380
+ // activeAuras, so admitting them here applies twice. damage_done_pct and haste are combat-only.
1381
+ const COMBAT_ONLY_BUFF_MODS = new Set(['threat_mod', 'armor_pct', 'dodge', 'damage_done_pct', 'haste', 'cast_haste']);
1382
+ for (const a of pruneExpiredAuras(char.activeAuras, now)) {
1383
+ const spRaw = ds.spell(a.spellId);
1384
+ if (!spRaw) continue;
1385
+ // Resolve scripted residuals so next_cast_mod chips stay on the bar, and interpreter effects
1386
+ // when the bake is empty. Berserking icon 1661 needs the health % for its haste mod.
1387
+ const hpPct = mh > 0 ? ((char.health ?? mh) / mh) * 100 : 100;
1388
+ const sp = resolveCombatSpell(spRaw, {
1389
+ casterLevel: char.level,
1390
+ casterHealthPercent: hpPct,
1391
+ casterMaxHealth: mh,
1392
+ });
1393
+ const kinds = new Set(sp.effects.map((e) => e.kind));
1394
+ const visible = kinds.has('buff') || kinds.has('speed') || kinds.has('shapeshift')
1395
+ || kinds.has('stealth') || kinds.has('feather_fall') || kinds.has('hover')
1396
+ || kinds.has('school_immunity')
1397
+ || kinds.has('damage_immunity') || kinds.has('mechanic_immunity')
1398
+ || kinds.has('dispel_immunity')
1399
+ || kinds.has('next_cast_mod')
1400
+ || kinds.has('owned_spell_mods')
1401
+ || kinds.has('sweeping_strikes')
1402
+ || kinds.has('combustion')
1403
+ || kinds.has('vampiric_embrace')
1404
+ // Mage Armor resist-only self aura. Cite: KI-482; resistances() from activeAuras.
1405
+ || kinds.has('resist')
1406
+ // SPELL_AURA_DUMMY (4) equip — Onyxia Scale Cloak 22683. Cite: SpellEffects.cpp:3756.
1407
+ || (spRaw.rawEffects ?? []).some((r) => (r.effect | 0) === 6 && (r.auraName | 0) === 4);
1408
+ if (!visible) continue;
1409
+ // Pure hostile debuffs never ride player OOC auras for display carry, and a baked
1410
+ // `hostile:false` is respected — Divine Shield's haste −100 is a self penalty.
1411
+ const onlyHostile = sp.effects.length > 0 && sp.effects.every((e) =>
1412
+ (e.kind === 'buff' && e.hostile !== false && !!(e.hostile || e.amount < 0))
1413
+ || (e.kind === 'speed' && e.hostile !== false && !!(e.hostile || e.pct < 0))
1414
+ || e.kind === 'dot' || e.kind === 'direct_damage' || e.kind === 'root' || e.kind === 'cc',
1415
+ );
1416
+ if (onlyHostile) continue;
1417
+ const permanent = a.expiresAt == null;
1418
+ const remainingMs = permanent
1419
+ ? undefined
1420
+ : Math.max(0, (a.expiresAt as number) - now);
1421
+ if (!permanent && (remainingMs ?? 0) <= 0) continue;
1422
+ // Combat-only buff mods; passives skip the kitSpell seed. Permanent passive dodge is already
1423
+ // folded into domain dodgeChance, so it must not re-apply. Timed Evasion still seeds.
1424
+ const liveBuffs: NonNullable<NonNullable<CombatantSpec['startDisplayAuras']>[number]['liveBuffs']> = [];
1425
+ const mechanicImmunities: number[] = [];
1426
+ const dispelImmunities: number[] = [];
1427
+ for (const e of sp.effects) {
1428
+ if (
1429
+ e.kind === 'buff'
1430
+ && e.mod
1431
+ && COMBAT_ONLY_BUFF_MODS.has(e.mod)
1432
+ && e.hostile !== true
1433
+ && (e.amount ?? 0) !== 0
1434
+ // Permanent passive dodge is already in the sheet's dodgeChance (racial Quickness);
1435
+ // timed Evasion still seeds. Cite: passiveBuffModPoints; KI-522.
1436
+ && !(sp.passive && e.mod === 'dodge' && (e.durationMs ?? 0) <= 0)
1437
+ ) {
1438
+ liveBuffs.push({
1439
+ mod: e.mod,
1440
+ amount: e.amount,
1441
+ ...(e.school ? { school: e.school } : {}),
1442
+ });
1443
+ } else if (e.kind === 'mechanic_immunity') {
1444
+ mechanicImmunities.push(e.mechanic | 0);
1445
+ } else if (e.kind === 'dispel_immunity') {
1446
+ dispelImmunities.push(e.dispelType | 0);
1447
+ }
1448
+ }
1449
+ // Scripted boost immunities (Stoneform 20594 → 20612 disease + bleed).
1450
+ // Cite: SpellAuras.cpp:5567 HandleSpellSpecificBoosts; immunityBoostAurasForSpell.
1451
+ const boostDur = remainingMs ?? 0;
1452
+ for (const boost of immunityBoostAurasForSpell(sp.id, boostDur)) {
1453
+ if (boost.auraType === AURA_TYPE.MECHANIC_IMMUNITY && (boost.mechanic ?? 0) > 0) {
1454
+ mechanicImmunities.push(boost.mechanic! | 0);
1455
+ } else if (boost.auraType === AURA_TYPE.DISPEL_IMMUNITY && (boost.dispelType ?? 0) > 0) {
1456
+ dispelImmunities.push(boost.dispelType! | 0);
1457
+ }
1458
+ }
1459
+ out.push({
1460
+ spellId: sp.id,
1461
+ name: sp.name,
1462
+ ...(permanent ? { permanent: true as const } : { remainingMs }),
1463
+ ...(liveBuffs.length ? { liveBuffs } : {}),
1464
+ ...(mechanicImmunities.length ? { mechanicImmunities } : {}),
1465
+ ...(dispelImmunities.length ? { dispelImmunities } : {}),
1466
+ ...(isContinuousPartyAura(spRaw) ? areaAuraSeedFields(sp) : {}),
1467
+ ...(() => {
1468
+ // Carriers seed via startDamageShields / startProcTriggers — do not also
1469
+ // stamp the display buff (Holy Shield block + shield).
1470
+ if (kinds.has('damage_shield') || kinds.has('proc_trigger_spell')) return {};
1471
+ const n = (a.charges != null && a.charges > 0) ? a.charges : auraHolderCharges(sp);
1472
+ if (!(n > 0)) return {};
1473
+ const chargedSheetBuffs: NonNullable<NonNullable<CombatantSpec['startDisplayAuras']>[number]['chargedSheetBuffs']> = [];
1474
+ for (const e of sp.effects) {
1475
+ if (
1476
+ e.kind === 'buff'
1477
+ && e.mod
1478
+ && SHEET_BAKED_BUFF_MODS.has(e.mod)
1479
+ && e.hostile !== true
1480
+ && (e.amount ?? 0) !== 0
1481
+ ) {
1482
+ chargedSheetBuffs.push({
1483
+ mod: e.mod,
1484
+ amount: e.amount,
1485
+ ...(e.school ? { school: e.school } : {}),
1486
+ });
1487
+ }
1488
+ }
1489
+ const pf = (sp.procFlags ?? 0) | 0;
1490
+ return {
1491
+ charges: n,
1492
+ ...(pf !== 0 ? { procFlags: pf } : {}),
1493
+ ...(chargedSheetBuffs.length ? { chargedSheetBuffs } : {}),
1494
+ };
1495
+ })(),
1496
+ });
1497
+ }
1498
+ // Equip SPELL_AURA_DUMMY even when applyItemEquipSpells did not seed activeAuras
1499
+ // (companion paperdoll). Cite: SpellEffects.cpp:3756 GetDummyAura(22683).
1500
+ const seen = new Set(out.map((a) => a.spellId));
1501
+ for (const it of equippedItems(char, ds)) {
1502
+ for (const sid of it.equipSpells ?? []) {
1503
+ if (seen.has(sid)) continue;
1504
+ const spRaw = ds.spell(sid);
1505
+ const dummy = !spRaw || (spRaw.rawEffects ?? []).some(
1506
+ (r) => (r.effect | 0) === 6 && (r.auraName | 0) === 4,
1507
+ );
1508
+ if (!dummy) continue;
1509
+ seen.add(sid);
1510
+ out.push({ spellId: sid, name: spRaw?.name ?? it.name, permanent: true });
1511
+ }
1512
+ }
1513
+ return out.length ? out : undefined;
1514
+ })(),
1515
+ /** Sheet AGI for mid-fight Cat AP (SHAPESHIFT_FLAG_AGILITY_ATTACK_BONUS). */
1516
+ agility: stats.agi,
1517
+ rangedAttackPower: rangedAttackPower(char, ds),
1518
+ critChance: critChance(char, ds),
1519
+ spellCritChance: spellCritChanceFromStats(char, ds),
1520
+ hitChance: hitChance(char, ds),
1521
+ // SPELL_AURA_MOD_SPELL_HIT_CHANCE gear — seeds Fighter.spellHitBonusPercent.
1522
+ // Cite: SpellAuras.cpp HandleModSpellHitChance; OPEN 197 hollow Equip: spell_hit.
1523
+ spellHitBonusPercent: spellHitBonusFromGear(char, ds),
1524
+ // Melee/cast haste from gear stats; quiver ammo-haste is ranged-only (rangedHastePct).
1525
+ haste: haste(char, ds),
1526
+ // SPELL_AURA_MOD_RANGED_AMMO_HASTE from equipped quiver/pouch equipSpells.
1527
+ // Cite: SpellAuras.cpp HandleRangedAmmoHaste (highest only).
1528
+ ...((): { rangedHastePct?: number } => {
1529
+ const pct = equippedQuiverRangedHastePct(char, ds);
1530
+ return pct > 0 ? { rangedHastePct: pct } : {};
1531
+ })(),
1532
+ dodgeChance: dodgeChance(char, ds, sb),
1533
+ parryChance: parryChance(char, ds, sb),
1534
+ blockChance: blockChance(char, ds, sb),
1535
+ blockValue: hasSh ? blockValue(char, ds) : 0,
1536
+ canParry: canP,
1537
+ canBlock: hasSh,
1538
+ // GetWeaponSkillValue / GetDefenseSkillValue = pure trained + MOD_SKILL bonuses.
1539
+ // Cite: Unit.cpp :4036 / :2970; Player.h GetSkillValue.
1540
+ ...(() => {
1541
+ const skillBonuses = collectSkillBonuses(char, ds, sb);
1542
+ const mhId = weaponSkillIdForHand(char, ds, 'mainhand');
1543
+ const ohId = weaponSkillIdForHand(char, ds, 'offhand') || undefined;
1544
+ const rngId = weaponSkillIdForHand(char, ds, 'ranged') || undefined;
1545
+ return {
1546
+ weaponSkill: weaponSkill(char, ds, sb),
1547
+ defenseSkill: defenseSkill(char, ds, sb),
1548
+ weaponSkillId: mhId,
1549
+ offhandSkillId: ohId,
1550
+ rangedSkillId: rngId,
1551
+ offhandWeaponSkill: ohId
1552
+ ? weaponSkillForHand(char, ds, 'offhand', sb, skillBonuses)
1553
+ : undefined,
1554
+ rangedWeaponSkill: rngId
1555
+ ? weaponSkillForHand(char, ds, 'ranged', sb, skillBonuses)
1556
+ : undefined,
1557
+ skillBonuses: Object.keys(skillBonuses).length ? { ...skillBonuses } : undefined,
1558
+ // Live map for mid-fight skill gain; absent → pure L×5 (legacy) but bonuses still apply.
1559
+ combatSkills: char.combatSkills ? { ...char.combatSkills } : undefined,
1560
+ };
1561
+ })(),
1562
+ intellect: stats.int,
1563
+ // Gear + spell_power auras; recomputed each build (not frozen across OOC buff lifecycle).
1564
+ spellPower: spellPower(char, ds),
1565
+ // One-school MOD_DAMAGE_DONE extras. Benefit = spellPower + map[school].
1566
+ // Cite: Unit.cpp SpellBaseDamageBonusDone ~7129.
1567
+ ...(() => {
1568
+ const bySchool = spellDamageBySchool(char, ds);
1569
+ return Object.keys(bySchool).length ? { spellDamageBySchool: bySchool } : {};
1570
+ })(),
1571
+ // SIE healing_power + equipMods healing_done. Classic separates heal bonus from SP. KI-590.
1572
+ healingPower: healingPower(char, ds),
1573
+ resistances: resistances(char, ds), // racial + buffs + talents + gear (Item 5)
1574
+ // Hardiness etc. Cite: Unit.cpp GetTotalAuraModifierByMiscValue(MOD_MECHANIC_RESISTANCE).
1575
+ ...(() => {
1576
+ const mr = mechanicResistPct(char, ds, sb?.known);
1577
+ return Object.keys(mr).length ? { mechanicResistPct: mr } : {};
1578
+ })(),
1579
+ ...(formTalentAuras ? { formTalentAuras } : {}),
1580
+ creatureType: combatantCreatureType(char), // Undead heroes are poly-immune; everyone else is Humanoid
1581
+ damagePctBySchool: talentDamagePctBySchool(char, ds),
1582
+ critChanceBonus: talentCritBonus(char, ds),
1583
+ // SPELLMOD_RADIUS (op 6) mods this unit owns — Totemic Mastery, Guardian Totems, baked set
1584
+ // rows. Combat reads them at the area-aura seam: ONE pipeline, no hardcoded "Mastery = 30".
1585
+ radiusSpellMods: radiusSpellModsFromTalentEffects(activeTalentEffects(char, ds)),
1586
+ ...(victimAtt.rows.length ? { startAddTargetTriggers: victimAtt.rows } : {}),
1587
+ procs: activeTalentProcsWithHolder(char, ds)
1588
+ .filter(({ holderSpellId }) => !victimAtt.skipHolders.has(holderSpellId | 0))
1589
+ .map(({ effect: e, holderSpellId, stances }) => {
1590
+ // Prefer filters baked on the talent effect, from the talent rank's own proc event, and
1591
+ // fall back to the trigger spell's — companions usually have no family mask.
1592
+ const pe = ds.spell(e.triggerSpellId)?.spellProcEvent;
1593
+ // EX3_CAN_PROC_FROM_PROCS lives on the talent RANK spell that owns the proc, so it is
1594
+ // resolved here where the DataStore is in hand. Cite: UnitAuraProcHandler.cpp:610.
1595
+ const holder = holderSpellId > 0 ? ds.spell(holderSpellId) : undefined;
1596
+ const canProcFromProcs = holder != null
1597
+ && hasAttrEx3(holder, SPELL_ATTR_EX3.CAN_PROC_FROM_PROCS);
1598
+ // PROC_FAILURE_BURNS_CHARGE is Attributes bit 0, on the holder too.
1599
+ const failureBurnsCharge = holder != null && hasAttr(holder, SPELL_ATTR.PROC_FAILURE_BURNS_CHARGE);
1600
+ return {
1601
+ triggerSpellId: e.triggerSpellId,
1602
+ ...(canProcFromProcs ? { canProcFromProcs: true } : {}),
1603
+ ...(failureBurnsCharge ? { failureBurnsCharge: true } : {}),
1604
+ ...(holder?.spellIconId != null ? { spellIconId: holder.spellIconId } : {}),
1605
+ // Blood Frenzy is cat, Primal Fury bear/dire — the PROC path reads the live form.
1606
+ ...(stances ? { stances } : {}),
1607
+ chance: e.chance ?? null,
1608
+ ppm: e.ppm ?? pe?.ppmRate ?? null,
1609
+ procFlags: e.procFlags ?? pe?.procFlags,
1610
+ procEx: e.procEx ?? pe?.procEx,
1611
+ // Setup bake ICD 1000s is wrong — CMaNGOS Cooldown=0. Cite: mangos.sql 13983.
1612
+ cooldownMs: e.triggerSpellId === SETUP_TRIGGER_SPELL_ID
1613
+ ? 0
1614
+ : (e.cooldownMs ?? pe?.cooldownMs),
1615
+ schoolMask: e.schoolMask ?? pe?.schoolMask,
1616
+ spellFamilyName: e.spellFamilyName ?? pe?.spellFamilyName,
1617
+ spellFamilyMask: e.spellFamilyMask ?? pe?.spellFamilyMask,
1618
+ };
1619
+ }),
1620
+ gearProcs: (() => {
1621
+ const fromItems = equippedItems(char, ds).flatMap((it) => (it.procs ?? []).map((p) => ({ ...p })));
1622
+ // Permanent weapon TYPE_COMBAT_SPELL (Crusader / Fiery / …). Cite: CastItemCombatSpell; KI-1044.
1623
+ const fromEnchants = permanentEnchantCombatProcs(char, ds);
1624
+ if (!fromEnchants.length) return fromItems;
1625
+ const seen = new Set(fromItems.map((p) => p.spell));
1626
+ const merged = fromItems.slice();
1627
+ for (const p of fromEnchants) {
1628
+ if (seen.has(p.spell)) continue;
1629
+ seen.add(p.spell);
1630
+ merged.push(p);
1631
+ }
1632
+ return merged;
1633
+ })(),
1634
+ spells: (() => {
1635
+ const base = knownCombatSpells(char, sb, ds);
1636
+ const tEffs = activeTalentEffects(char, ds);
1637
+ const procIds = new Set<number>();
1638
+ for (const e of tEffs) if (e.kind === 'proc' && e.triggerSpellId) procIds.add(e.triggerSpellId);
1639
+ for (const r of victimAtt.rows) procIds.add(r.triggerSpellId);
1640
+ // Channel and aura trigger payloads on the known kit, plus SPELL_EFFECT_TRIGGER_SPELL (64)
1641
+ // secondaries, must be re-attached here or applySpell's trigger lookup fails.
1642
+ for (const s of base) {
1643
+ for (const e of s.effects) {
1644
+ if (e.kind === 'proc_trigger_spell' && e.triggerSpellId) procIds.add(e.triggerSpellId);
1645
+ else if (e.kind === 'trigger_spell' && e.spellId) procIds.add(e.spellId);
1646
+ }
1647
+ }
1648
+ // An OOC-armed aura-42 may sit only on activeAuras, since the parent can be level-gated out
1649
+ // of knownCombatSpells while castOutOfCombat armed it. Resolve the holder so 28598 rewrites.
1650
+ {
1651
+ const nowAura = Date.now();
1652
+ for (const a of pruneExpiredAuras(char.activeAuras, nowAura)) {
1653
+ const raw = ds.spell(a.spellId);
1654
+ if (!raw) continue;
1655
+ const sp = resolveCombatSpell(raw, { casterLevel: char.level });
1656
+ for (const e of sp.effects) {
1657
+ if (e.kind === 'proc_trigger_spell' && e.triggerSpellId) procIds.add(e.triggerSpellId);
1658
+ else if (e.kind === 'trigger_spell' && e.spellId) procIds.add(e.spellId);
1659
+ }
1660
+ }
1661
+ }
1662
+ // Permanent enchant combat_spell + Absorption holders/triggers (KI-1044 / KI-1045).
1663
+ for (const p of permanentEnchantCombatProcs(char, ds)) procIds.add(p.spell);
1664
+ for (const row of permanentEnchantProcTriggers(char, ds)) {
1665
+ procIds.add(row.spellId);
1666
+ procIds.add(row.triggerSpellId);
1667
+ }
1668
+ const extra: SpellDef[] = [];
1669
+ for (const pid of procIds) {
1670
+ if (!base.some((s) => s.id === pid)) {
1671
+ const ps = ds.spell(pid);
1672
+ if (ps) {
1673
+ // SI5: materialize trigger payloads (Chilled 6136 haste+speed, etc.) — bake may
1674
+ // only carry one effect while rawEffects has the rest. Cite: KI-483 Chilled 6136.
1675
+ extra.push(resolveCombatSpell(ps, { casterLevel: char.level }));
1676
+ } else {
1677
+ // Freezing and Frost trap effect rows are absent from the bake, so a SQL-sourced
1678
+ // synthetic payload is injected for fireDamageShields to resolve the armed proc.
1679
+ const synth = syntheticHunterTrapPayload(pid) ?? syntheticTalentTriggerPayload(pid);
1680
+ if (synth) extra.push(synth);
1681
+ }
1682
+ }
1683
+ }
1684
+ // Gear proc trigger spells (E1) + SI6 aura-42 nested triggers (empty-effects holders).
1685
+ for (const it of equippedItems(char, ds)) {
1686
+ const itemProcIds = gearProcTriggerSpellIds(it.procs, (id) => ds.spell(id));
1687
+ for (const sid of itemProcIds) {
1688
+ if (!base.some((s) => s.id === sid) && !extra.some((s) => s.id === sid)) {
1689
+ const ps = ds.spell(sid);
1690
+ if (ps) extra.push(ps);
1691
+ }
1692
+ }
1693
+ }
1694
+ // ItemSetDef bonus spells when piece count met (character derivation + combat) (E1)
1695
+ for (const sid of activeSetBonusSpells(char, ds)) {
1696
+ if (!base.some((s) => s.id === sid) && !extra.some((s) => s.id === sid)) {
1697
+ const ps = ds.spell(sid);
1698
+ if (ps) extra.push(ps);
1699
+ }
1700
+ }
1701
+ // A HOLDER THAT ARRIVES IN `extra` NEEDS ITS PAYLOAD TOO: the procIds walk runs over `base`
1702
+ // only, so five live 6pc set bonuses armed their branch and delivered nothing.
1703
+ {
1704
+ const payloadQueue: SpellDef[] = [...base, ...extra];
1705
+ for (let qi = 0; qi < payloadQueue.length; qi++) {
1706
+ const holder = payloadQueue[qi]!;
1707
+ const ids: number[] = [];
1708
+ for (const e of holder.effects) {
1709
+ if (e.kind === 'proc_trigger_spell' && e.triggerSpellId) ids.push(e.triggerSpellId);
1710
+ else if (e.kind === 'trigger_spell' && e.spellId) ids.push(e.spellId);
1711
+ }
1712
+ for (const pid of specialCasePayloadIds(holder.id)) ids.push(pid);
1713
+ for (const pid of ids) {
1714
+ if (base.some((x) => x.id === pid) || extra.some((x) => x.id === pid)) continue;
1715
+ const ps = ds.spell(pid);
1716
+ if (!ps) continue;
1717
+ const resolved = resolveCombatSpell(ps, { casterLevel: char.level });
1718
+ extra.push(resolved);
1719
+ payloadQueue.push(resolved);
1720
+ }
1721
+ }
1722
+ }
1723
+ return [...base, ...extra];
1724
+ })(),
1725
+ };
1726
+ }