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,2026 @@
1
+ // Wave 4 — late applySpell effect cases (summon_totem → end of loop). Pattern (D).
2
+ import type { ActiveEffect, Fighter } from './types.ts';
3
+ import type { SpellDef, SpellEffect } from '../../data/schema.ts';
4
+ import { isAllySide, isEnemySide, isTotemSide, type Side, type TotemSide } from '../core/events.ts';
5
+ import {
6
+ TUNING, rollBetween, spellCritChance, critMultiplierWithVersus,
7
+ } from './formulas.ts';
8
+ import { fighterSpellCrit, applyBuffMod, reverseBuffMod, applyResistMod, applyMechanicResist, makeFighter } from './fighterFactory.ts';
9
+ import { spellDamageLandChance } from './mitigation.ts';
10
+ import { POWER_MANA } from '../spells/effects.ts';
11
+ import { getDispelMask, processDispelList, dispelClaimKey } from '../../domain/spells/dispel.ts';
12
+ import { applyDispelMechanicOnUnit } from './applyDispelMechanic.ts';
13
+ import type { DispellableAura } from '../../domain/spells/dispel.ts';
14
+ import { DEVOUR_MAGIC_HEAL, SPELLCATEGORY_DEVOUR_MAGIC } from '../spells/interpreter/handlers/shared.ts';
15
+ import { resolveSpellEffects } from '../spells/interpreter/index.ts';
16
+ import { buildTotemFromEffect } from '../../domain/spells/totem.ts';
17
+ import { applySpellMagnetToParty } from './totemAuras.ts';
18
+ import { isWithinRadiusYd } from './combatGeometry.ts';
19
+ import {
20
+ areaAuraCarrierSides, effectiveAreaRadiusYd, hasAreaAuraRow, stripAreaAuraRowsFrom,
21
+ } from './areaAura.ts';
22
+ import {
23
+ isImmuneToMechanic,
24
+ isTotemCreatureType,
25
+ totemRefusesNegativeAuraApply,
26
+ } from '../spells/immunity.ts';
27
+ import { spellCanCrit } from '../spells/spellAttributes.ts';
28
+ import {
29
+ JUDGEMENT_PAYLOAD_TRIGGER_FLAGS,
30
+ triggeredSpellIgnoresHitResult,
31
+ } from '../spells/interpreter/paladin-seals.ts';
32
+ import { SPELL_EFFECT_SCHOOL_DAMAGE } from '../spells/interpreter/opcodes.ts';
33
+ import { CLEAN_ESCAPE_AURA_SPELL_ID, HEALING_WAY_MAX_STACKS } from '../spells/interpreter/class-scripts-kit.ts';
34
+ import { isHostileBuffEffect } from './predicates.ts';
35
+ import { isHostileSpeedEffect, markSpeedDirty } from './speedAura.ts';
36
+ import { mechanicMaskOfLiveEffect } from './crowdControl.ts';
37
+ import type { HeartbeatHolder } from './heartbeatResist.ts';
38
+ import { addOwnedSpellMods } from '../spells/spellModIntents.ts';
39
+ import { COMBUSTION_UNTIL_CANCELLED_MS, setCombustionStacks } from './combustion.ts';
40
+ import { pendingSpellModFromResolved, roleOf } from './roles.ts';
41
+ import {
42
+ filterPartyFanByRadius, resolveHealApplyTo, resolvePetHealFighter, sameSubgroup,
43
+ selectedOrSelfFriendly,
44
+ } from './healTargets.ts';
45
+ import type { ActiveWeaponImbue } from './types.ts';
46
+ import { isStealthShapeshift } from '../../domain/character/forms.ts';
47
+ import { applyKnockBackEffect } from './knockBack.ts';
48
+ import { applyLeapEffect } from './leap.ts';
49
+ import { clampDisplacementToFloor, displaceFloorFromCfg } from './displaceFloor.ts';
50
+ import { applyModTauntAura } from './modTaunt.ts';
51
+ import { applyDemonicSacrificeEffect } from './applyDemonicSacrifice.ts';
52
+ import { getSpellRecoveryTime } from '../spells/recovery.ts';
53
+ import {
54
+ IMPROVED_SPRINT_TRIGGER_SPELL_ID,
55
+ improvedSprintShouldProc,
56
+ improvedSprintTriggerSpellDef,
57
+ isSprintSpell,
58
+ } from '../spells/cmangos-special-cases-s21.ts';
59
+ import { applyHealEffect, applyHealingWayStack } from './applyHealEffect.ts';
60
+ import { moneyMarked } from '../../domain/economy/money.ts';
61
+ import { isPossessInvSpell } from '../../domain/scriptdev/index.ts';
62
+ import { setHover } from '../../domain/core/hover.ts';
63
+
64
+ export function applySpellEffectLate(
65
+ L: Record<string, any>,
66
+ src: Fighter,
67
+ tgt: Fighter,
68
+ spell: SpellDef,
69
+ eff: SpellEffect,
70
+ state: {
71
+ t: number;
72
+ durFor: (n: number) => number;
73
+ crit: boolean;
74
+ binary: any;
75
+ hitBox?: { finisherHit: boolean };
76
+ hitCount?: number;
77
+ unitTargetGender?: 0 | 1;
78
+ /** Armed when this speed row owns the spell's one heartbeat clock. Cite: Spell.cpp:1483-1489. */
79
+ heartbeat?: HeartbeatHolder | null;
80
+ },
81
+ ): void {
82
+ const t = state.t;
83
+ const durFor = state.durFor;
84
+ const crit = state.crit;
85
+ const binary = state.binary;
86
+ // Mutable finisher flag shared with applySpell's loop (set true on land).
87
+ let finisherHit = state.hitBox?.finisherHit ?? false;
88
+ const markFinisherHit = () => {
89
+ finisherHit = true;
90
+ if (state.hitBox) state.hitBox.finisherHit = true;
91
+ };
92
+ const {
93
+ push, effects: effectsRaw, rng, cfg, player, pet, totems, enemy,
94
+ dealDamage, applyFlatThreat, applyHealThreat, applySpellThreatBonus, applyDamageThreat,
95
+ powerOfType, maxPowerOfType, setPowerOfType, grantImmunityAura,
96
+ consumeChargedImmunityOnMechanicBlock,
97
+ resolveDispelTarget, dispellableAurasOn, stripAuraOnDispel, dispelTypeOfSpell,
98
+ dispelClaim,
99
+ livingFriendlies: livingFriendliesRaw, livingAllies, dead, foeOf, lowestHpFriendly, unsummonTotem,
100
+ sealRighteousnessSwing, spellCost, spend, addComboPoints, clearComboPoints, comboOn,
101
+ threatOf, bySide, sideFighter, factionOf, distanceBetween, livingHostilesFor, reflectChanceFor,
102
+ rollHeal, applyControl, applyDr, ccImmune, onDamaged, enterForm, leaveForm,
103
+ applyStealth, breakStealth, maybeBreakStealthOn, behindOk,
104
+ // Wave 4 fix-up: these were referenced as bare identifiers after the peel
105
+ // (ReferenceError at runtime on totem party auras / triggered casts / procs).
106
+ isCharmed, applySpell, livingEnemies, triggerProcs, rollGearProcs,
107
+ consumePendingSpellMod,
108
+ // Geometry + class-A area-aura plumbing (totem origin / radius maintain).
109
+ posOf, setPos, displaced, registerAreaAuraSource, reverseActiveEffect,
110
+ runCastSummons, effectsOn: effectsOnRaw, effectsFrom: effectsFromRaw,
111
+ setEffectSource: setEffectSourceRaw, bumpEffectsVersion: bumpEffectsVersionRaw,
112
+ applyBuffMod: _ab, // may come from L
113
+ touchEffect: touchEffectRaw, touchPeriodicDue,
114
+ } = L;
115
+ const touchEffect = (e: ActiveEffect): void => { (touchEffectRaw as ((ae: ActiveEffect) => void) | undefined)?.(e); };
116
+ // L is a Record bag; narrow the live-effect table + friendlies for callback typing.
117
+ const effects = effectsRaw as ActiveEffect[];
118
+ const effectsOn = effectsOnRaw as ((side: string) => ActiveEffect[]) | undefined;
119
+ const effectsFrom = effectsFromRaw as ((side: string) => ActiveEffect[]) | undefined;
120
+ const rowsOn = (f: Fighter): ActiveEffect[] => effectsOn?.(f.side) ?? effects;
121
+ const rowsFrom = (f: Fighter): ActiveEffect[] => effectsFrom?.(f.side) ?? effects;
122
+ const setEffectSource = setEffectSourceRaw as ((e: ActiveEffect, side: string) => void) | undefined;
123
+ const bumpEffectsVersion = bumpEffectsVersionRaw as (() => void) | undefined;
124
+ const livingFriendlies = livingFriendliesRaw as () => Fighter[];
125
+ // Ensure threatOf always available (hub on CombatContext)
126
+ const threatOfFn = threatOf ?? ((owner: any) => L.threatOf?.(owner));
127
+ void threatOfFn;
128
+ // Prefer L.applyBuffMod if present
129
+ const applyBuffModFn = L.applyBuffMod ?? applyBuffMod;
130
+ void applyBuffModFn;
131
+
132
+ if (eff.kind === 'summon_totem') {
133
+ // Shaman totem: replace the slot, spawn the fighter, apply the passives or arm the cast AI.
134
+ // The player OR a party ally — a companion shaman is a shaman. A CREATURE'S is not our slot.
135
+ if (src.side !== 'player' && !isAllySide(src.side)) {
136
+ const made = runCastSummons(src, spell);
137
+ if (made.length > 0) {
138
+ push({ t, kind: 'flavor', text: `${src.spec.name} summons ${made[0]!.spec.name}.` });
139
+ }
140
+ markFinisherHit();
141
+ return;
142
+ }
143
+ if (eff.slot === 'none') return;
144
+ const built = buildTotemFromEffect(
145
+ eff,
146
+ spell.id,
147
+ src.spec.level,
148
+ src.spec.critChance,
149
+ src.spec.hitChance,
150
+ spell.name,
151
+ src.side,
152
+ );
153
+ if (!built) return;
154
+ // `if (damage)` is false for 23419, so that totem keeps the creature_template health rather
155
+ // than the 1 hp a bare max(1) gave it. Cite: SpellEffects.cpp:4538-4542.
156
+ if ((eff.health | 0) <= 0) {
157
+ const dbHp = (cfg.resolveSummon?.(eff.creature | 0)?.maxHealth ?? 0) | 0;
158
+ if (dbHp > 0) {
159
+ built.maxHp = dbHp;
160
+ built.spec.maxHealth = dbHp;
161
+ built.spec.startHealth = dbHp;
162
+ }
163
+ }
164
+ // Only THIS caster's seat is replaced. Cite: SpellEffects.cpp:4500 `m_caster->GetTotem(slot)`.
165
+ if (totems.has(built.side)) unsummonTotem(built.side, 'replace');
166
+ const tot = makeFighter(built.side, built.spec);
167
+ tot.totemExpiresAt = t + built.durationMs;
168
+ (touchPeriodicDue as ((at: number) => void) | undefined)?.(tot.totemExpiresAt);
169
+ tot.totemSlot = built.slot;
170
+ tot.totemSummonSpellId = built.summonSpellId;
171
+ // GetSpell() + aura-23 payloads: the pulse schedule resolves triggerSpellId against these,
172
+ // and UnSummon strips only auras carrying kitSpellId. Cite: Totem.cpp UnSummon / GetSpell.
173
+ tot.totemKitSpellId = built.kitSpellId;
174
+ if (built.triggerSpells.length) tot.totemTriggerSpells = built.triggerSpells;
175
+ if (built.auraRadiusYd > 0) tot.totemAuraRadiusYd = built.auraRadiusYd;
176
+ // No white melee for totems (TotemAI only CastSpell).
177
+ tot.swingReadyAt = Number.POSITIVE_INFINITY;
178
+ L.rosterSetTotem(built.side, tot); // seat + fightersOrder generation, one writer
179
+ // Mark the summon spell up so buffPresent gates stop re-spamming the same totem, on the
180
+ // CASTER: marking the player for an ally's totem gated the wrong unit and drew a phantom chip.
181
+ tot.totemOwnerSide = src.side;
182
+ // Meter merge survives UnSummon (Searing Totem damage still credits the sham).
183
+ (L.noteTotemOwner as ((s: string, o: Side, n: string) => void) | undefined)
184
+ ?.(built.side, src.side, built.name);
185
+ // Geometry: a totem is dropped AT THE CASTER'S FEET and never moves, so its position — not the
186
+ // shaman's — is the origin of every friendly-radius test. Cite: SpellEffects EffectSummonTotem.
187
+ {
188
+ const ownerPos = posOf?.(src.side);
189
+ if (ownerPos) setPos?.(built.side, { x: ownerPos.x, y: ownerPos.y });
190
+ }
191
+ src.buffActive.add(built.summonSpellId);
192
+ push({ t, kind: 'flavor', text: `${src.spec.name} summons ${built.name}.` });
193
+ push({ t, kind: 'aura', actor: src.side, name: built.name, applied: true });
194
+ if (built.type === 'passive') {
195
+ // Totem::Summon TOTEM_PASSIVE casts each kit spell on summon. Party auras reach the owner and
196
+ // same-subgroup members, and UnSummon strips that set. Pets are not Group members.
197
+ const totemParty = (): Fighter[] => {
198
+ const out: Fighter[] = [];
199
+ if (player.hp > 0 && !isCharmed(player)) out.push(player);
200
+ for (const al of livingAllies()) if (!isCharmed(al)) out.push(al);
201
+ // UnSummon strips "owner + same-subgroup members" because that is exactly who
202
+ // the kit aura reached — a totem never buffed the other seven raid groups.
203
+ return out.filter((fr) => sameSubgroup(src, fr));
204
+ };
205
+ // The effective radius for one kit ability: the DBC yards scaled by the OWNER's radius mods,
206
+ // so Totemic Mastery rides one pipeline rather than an if-mastery branch.
207
+ const kitRadiusYd = (abil: SpellDef): number | undefined =>
208
+ effectiveAreaRadiusYd(abil.radiusYd, src.radiusSpellMods);
209
+ /** In-radius members of the totem's party (fails open on a missing radius). */
210
+ const inRadiusParty = (abil: SpellDef): Fighter[] => {
211
+ const r = kitRadiusYd(abil);
212
+ const origin = posOf?.(built.side);
213
+ return totemParty().filter((fr) =>
214
+ isWithinRadiusYd(origin, posOf?.(fr.side), r),
215
+ );
216
+ };
217
+ // Register the kit aura as a live class-A holder so the maintain pass keeps re-checking the
218
+ // totem-to-ally distance. The origin is the TOTEM; the mod owner is the shaman.
219
+ const registerTotemAreaAura = (abil: SpellDef, apply: (fr: Fighter) => void): void => {
220
+ registerAreaAuraSource?.({
221
+ holderSide: built.side,
222
+ spellId: abil.id,
223
+ name: abil.name,
224
+ baseRadiusYd: abil.radiusYd,
225
+ modOwnerSide: src.side,
226
+ apply,
227
+ strip: (fr: Fighter) => {
228
+ stripAreaAuraRowsFrom(effects, fr, abil.id, built.side, reverseActiveEffect, rowsOn(fr));
229
+ },
230
+ members: totemParty,
231
+ carriers: () => areaAuraCarrierSides(effectsFrom?.(built.side) ?? effects, abil.id, built.side),
232
+ alive: () => totems.get(built.side) === tot && !dead(tot),
233
+ });
234
+ };
235
+ for (const abil of built.abilities) {
236
+ // Weapon-proc payloads fire from the enchant proc on the wielder's SWING, never as a
237
+ // standing party aura — applying them here handed the party a PERMANENT +315 AP.
238
+ if (abil.effects.some((e) => e.kind === 'extra_attacks')) continue;
239
+ const hasOffense = abil.effects.some((e) => e.kind === 'direct_damage' || e.kind === 'dot');
240
+ if (hasOffense) {
241
+ const foe = foeOf(tot);
242
+ // foeOf returns the caster itself when no hostiles live, and a Searing Totem must not fire
243
+ // at itself. The recursive applySpell is fine: a kit id is never the summon spell.
244
+ if (foe !== tot && !dead(foe)) applySpell(tot, foe, abil);
245
+ }
246
+ for (const ae of abil.effects) {
247
+ if (ae.kind === 'buff') {
248
+ const amount = ae.amount;
249
+ // An area-party kit aura bakes durationMs 0 and lives as long as the totem, but a kit
250
+ // spell with its OWN duration is a proc payload — honour its clock when it has one.
251
+ const dur = ae.durationMs > 0 ? ae.durationMs : built.durationMs;
252
+ // Kit buffs from area-party totems use applyTo party; totem passives without a hostile
253
+ // flag default to it. Party reach is the TOTEM's radius, not everyone alive.
254
+ const buffTgts = ae.hostile
255
+ ? livingEnemies().filter((en: Fighter) =>
256
+ isWithinRadiusYd(posOf?.(built.side), posOf?.(en.side), kitRadiusYd(abil) ?? 10),
257
+ )
258
+ : (ae.applyTo === 'self' ? [player] : inRadiusParty(abil));
259
+ const applyKitBuff = (fr: Fighter): void => {
260
+ if (dead(fr)) return;
261
+ if (hasAreaAuraRow(rowsOn(fr), fr, abil.id, built.side)) return;
262
+ applyBuffMod(fr, ae.mod, amount);
263
+ fr.buffActive.add(abil.id);
264
+ effects.push({
265
+ kind: 'buff', source: built.side, target: fr.side, spellId: abil.id, name: abil.name,
266
+ mod: ae.mod, amount, expiresAt: t + dur, fromTotem: built.side,
267
+ });
268
+ push({ t, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
269
+ };
270
+ // continue (not return): one dead friendly must not abort party aura reach.
271
+ // Cite: Totem.cpp UnSummon party strip :145–157; APPLY_AREA_AURA_PARTY effect 35.
272
+ for (const fr of buffTgts) applyKitBuff(fr);
273
+ if (!ae.hostile && ae.applyTo !== 'self') {
274
+ registerTotemAreaAura(abil, applyKitBuff);
275
+ }
276
+ } else if (ae.kind === 'restore_health' || ae.kind === 'restore_power') {
277
+ // Tick while the totem lives (Healing Stream / Mana Spring) — party reach.
278
+ // Cite: APPLY_AREA_AURA_PARTY; Totem.cpp UnSummon removes aura on all subgroup members.
279
+ const tickMs = Math.max(1, ae.tickMs);
280
+ const kitPower = ae.kind === 'restore_power' ? (ae.powerType ?? POWER_MANA) | 0 : undefined;
281
+ const applyKitTick = (fr: Fighter): void => {
282
+ if (dead(fr)) return;
283
+ if (hasAreaAuraRow(rowsOn(fr), fr, abil.id, built.side)) return;
284
+ // The aura lands on every living party member for strip symmetry; the pool it pays
285
+ // rides on the row, so Mana Spring on a warrior ticks nothing rather than rage.
286
+ fr.buffActive.add(abil.id);
287
+ effects.push({
288
+ kind: ae.kind, source: built.side, target: fr.side, spellId: abil.id, name: abil.name,
289
+ amountPerTick: ae.amountPerTick, pctOfMax: ae.pctOfMax, tickMs, nextTickAt: t + tickMs,
290
+ ...(kitPower != null ? { powerType: kitPower } : {}),
291
+ expiresAt: t + built.durationMs, fromTotem: built.side,
292
+ });
293
+ push({ t, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
294
+ };
295
+ // Healing Stream / Mana Spring only tick on allies standing in the totem's
296
+ // radius — walk out and the stream stops for you (§4.9.1).
297
+ for (const fr of inRadiusParty(abil)) applyKitTick(fr);
298
+ registerTotemAreaAura(abil, applyKitTick);
299
+ } else if (ae.kind === 'proc_trigger_spell') {
300
+ // PERIODIC_TRIGGER_SPELL on a totem kit: the kit spell is only the metronome and the
301
+ // payload is its trigger. Dropping it made every cleanser and Windfury grant nothing.
302
+ const tickMs = Math.max(1, ae.tickMs ?? 0);
303
+ effects.push({
304
+ kind: 'proc_trigger_spell',
305
+ source: built.side,
306
+ target: built.side,
307
+ spellId: abil.id,
308
+ name: abil.name,
309
+ triggerSpellId: ae.triggerSpellId,
310
+ tickMs,
311
+ nextTickAt: t + tickMs,
312
+ expiresAt: t + built.durationMs,
313
+ fromTotem: built.side,
314
+ });
315
+ } else if (ae.kind === 'resist') {
316
+ // MOD_RESISTANCE party aura — the Frost, Fire and Nature Resistance Totems. Same party
317
+ // reach and strip symmetry as the stat buffs; without it they were summoned decoration.
318
+ const applyKitResist = (fr: Fighter): void => {
319
+ if (dead(fr)) return;
320
+ if (hasAreaAuraRow(rowsOn(fr), fr, abil.id, built.side)) return;
321
+ applyResistMod(fr, ae.school, ae.amount);
322
+ fr.buffActive.add(abil.id);
323
+ effects.push({
324
+ kind: 'resist', source: built.side, target: fr.side, spellId: abil.id,
325
+ name: abil.name, school: ae.school, amount: ae.amount,
326
+ expiresAt: t + built.durationMs, fromTotem: built.side,
327
+ });
328
+ push({ t, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
329
+ };
330
+ for (const fr of inRadiusParty(abil)) applyKitResist(fr);
331
+ registerTotemAreaAura(abil, applyKitResist);
332
+ } else if (ae.kind === 'threat') {
333
+ // SPELL_EFFECT_THREAT on totem kit (Stoneclaw Effect) — real list score.
334
+ // Cite: SpellEffects.cpp EffectThreat; amount from EffectBasePoints.
335
+ applyFlatThreat(tot, ae.amount, abil as unknown as SpellDef);
336
+ } else if (ae.kind === 'spell_magnet') {
337
+ const magnetDur = ae.durationMs > 0 ? ae.durationMs : built.durationMs;
338
+ const applyKitMagnet = (fr: Fighter): void => {
339
+ applySpellMagnetToParty({
340
+ effects, effectsOn, party: [fr], source: built.side, spell: abil,
341
+ expiresAt: t + magnetDur, t, push, touchEffect,
342
+ });
343
+ };
344
+ for (const fr of inRadiusParty(abil)) applyKitMagnet(fr);
345
+ registerTotemAreaAura(abil, applyKitMagnet);
346
+ } else if (ae.kind === 'speed' && (ae.hostile || ae.pct < 0)) {
347
+ // Earthbind-style snare on passive totem kit — radius of the TOTEM, not
348
+ // the whole floor stream. Cite: Earthbind 3600 radius 10; SpellRadius.dbc.
349
+ const snareR = kitRadiusYd(abil) ?? 10;
350
+ const snareOrigin = posOf?.(built.side);
351
+ for (const enemy of livingEnemies()) {
352
+ if (dead(enemy)) continue;
353
+ if (!isWithinRadiusYd(snareOrigin, posOf?.(enemy.side), snareR)) continue;
354
+ const snareDur = Math.max(ae.durationMs || 0, built.durationMs);
355
+ effects.push({
356
+ kind: 'speed',
357
+ source: built.side,
358
+ target: enemy.side,
359
+ spellId: abil.id,
360
+ name: abil.name,
361
+ aura: ae.aura,
362
+ pct: ae.pct,
363
+ expiresAt: t + snareDur,
364
+ fromTotem: built.side,
365
+ });
366
+ push({ t, kind: 'aura', actor: enemy.side, name: abil.name, applied: true });
367
+ }
368
+ }
369
+ }
370
+ }
371
+ // 8178 lives on triggerAbilities (kit 8179 is the metronome). Apply magnet now.
372
+ // Cite: creature 5925 spells=[8179]; 8178 APPLY_AREA_AURA_PARTY aura 96.
373
+ for (const trig of built.triggerSpells) {
374
+ const magnetFx = trig.effects.find((e) => e.kind === 'spell_magnet');
375
+ if (!magnetFx || magnetFx.kind !== 'spell_magnet') continue;
376
+ const magnetDur = magnetFx.durationMs > 0 ? magnetFx.durationMs : built.durationMs;
377
+ const applyTrigMagnet = (fr: Fighter): void => {
378
+ applySpellMagnetToParty({
379
+ effects, effectsOn, party: [fr], source: built.side,
380
+ spell: { id: trig.id, name: trig.name, procCharges: magnetFx.charges },
381
+ expiresAt: t + magnetDur, t, push, touchEffect,
382
+ });
383
+ };
384
+ for (const fr of inRadiusParty(trig)) applyTrigMagnet(fr);
385
+ registerTotemAreaAura(trig, applyTrigMagnet);
386
+ }
387
+ }
388
+ } else if (eff.kind === 'heal') {
389
+ // Party / chain / triage — see applyHealEffect.ts (was inlined; double-heal fix).
390
+ applyHealEffect(L, src, tgt, spell, eff, t);
391
+ } else if (eff.kind === 'aura_dummy_holder' || eff.kind === 'aura_dummy') {
392
+ // An empty-body dummy still shows an icon and timer when it has a real duration AND is the
393
+ // spell's only payload. A dummy riding damage, a proc or haste is a script hook, not a chip.
394
+ const rawDur = (eff.durationMs ?? 0) | 0;
395
+ const untilCancelled = rawDur <= 0 && (((spell.durationIndex ?? 0) | 0) === 21 || !!spell.passive);
396
+ const dummyIsRider = spell.effects.some(
397
+ (e) => e.kind !== 'aura_dummy' && e.kind !== 'aura_dummy_holder',
398
+ );
399
+ const hostileDummy = eff.hostile === true;
400
+ const skipHostileOnSelf = hostileDummy
401
+ && (tgt === src || isEnemySide(src.side) === isEnemySide(tgt.side));
402
+ if (dummyIsRider || (rawDur <= 0 && !untilCancelled) || skipHostileOnSelf) {
403
+ markFinisherHit();
404
+ } else {
405
+ const holder = (eff.auraOnTarget === true || hostileDummy) && tgt !== src ? tgt : src;
406
+ holder.buffActive.add(spell.id);
407
+ effects.push({
408
+ kind: 'buff', source: src.side, target: holder.side, spellId: spell.id, name: spell.name,
409
+ expiresAt: untilCancelled ? Number.POSITIVE_INFINITY : t + durFor(rawDur),
410
+ });
411
+ push({ t, kind: 'aura', actor: holder.side, name: spell.name, applied: true });
412
+ markFinisherHit();
413
+ }
414
+ } else if (eff.kind === 'durability_damage') {
415
+ // SPELL_EFFECT_DURABILITY_DAMAGE_PCT — Character apply is settleFight. Cite: :5236–5260.
416
+ push({
417
+ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id,
418
+ name: spell.name, result: 'hit', amount: 0, school: spell.school || 'physical',
419
+ });
420
+ markFinisherHit();
421
+ } else if (eff.kind === 'instakill') {
422
+ // SPELL_EFFECT_INSTAKILL — DealDamage full remaining HP. Cite: SpellEffects.cpp:205–221.
423
+ if (!dead(tgt)) {
424
+ const amount = Math.max(1, Math.round(tgt.hp));
425
+ dealDamage(tgt, amount, spell.school || 'physical', src.spec.level, {
426
+ damageType: 'spell', attacker: src, spell,
427
+ });
428
+ push({
429
+ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id,
430
+ name: spell.name, result: 'hit', amount, school: spell.school || 'physical',
431
+ });
432
+ markFinisherHit();
433
+ }
434
+ } else if (eff.kind === 'summon_critter') {
435
+ // SPELL_EFFECT_SUMMON_CRITTER — mini-pet companion (cosmetic). Cite: SpellEffects.cpp:5031–5111.
436
+ if (!eff.despawnOnly) {
437
+ push({
438
+ t, kind: 'flavor',
439
+ text: `${src.spec.name} summons a companion (${eff.creature}).`,
440
+ });
441
+ } else {
442
+ push({ t, kind: 'flavor', text: `${src.spec.name} dismisses their companion.` });
443
+ }
444
+ markFinisherHit();
445
+ } else if (eff.kind === 'mod_scale') {
446
+ // SPELL_AURA_MOD_SCALE — cosmetic size. Stored as a timed buff aura row.
447
+ // Cite: SpellAuras.cpp:2182–2185.
448
+ src.buffActive.add(spell.id);
449
+ effects.push({
450
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
451
+ // durationMs 0 = permanent, the same read every other aura branch takes
452
+ // (applySpell expAt). Strength of Ossirian 25176 is aura 79 + aura 61 on
453
+ // DurationIndex 21: the scale row took one tick and took the whole spell with it.
454
+ amount: eff.pct,
455
+ expiresAt: eff.durationMs > 0 ? t + durFor(eff.durationMs) : Number.POSITIVE_INFINITY,
456
+ });
457
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
458
+ markFinisherHit();
459
+ } else if (eff.kind === 'mod_detect_range') {
460
+ // MOD_DETECT_RANGE (91) — Mind Soothe and Soothe Animal. The attack distance takes the total
461
+ // aura modifier on the CREATURE, and a negative amount is hostile. Cite: Unit.cpp:11772.
462
+ const amount = Number(eff.amount) || 0;
463
+ const auraTgt = amount < 0 ? tgt : src;
464
+ if (amount < 0 && !isEnemySide(tgt.side)) {
465
+ // Hostile soothe needs an enemy; do not self-stick.
466
+ } else {
467
+ const dur = durFor(eff.durationMs);
468
+ const exp = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
469
+ // One (caster, spell, target) row — refresh and reverse prior detectRangeMod.
470
+ const mine = rowsOn(auraTgt);
471
+ for (let i = mine.length - 1; i >= 0; i--) {
472
+ const e = mine[i]!;
473
+ if (
474
+ e.kind === 'buff'
475
+ && e.spellId === spell.id
476
+ && e.source === src.side
477
+ ) {
478
+ if (e.detectRangeAmount != null) {
479
+ auraTgt.detectRangeMod = (auraTgt.detectRangeMod ?? 0) - e.detectRangeAmount;
480
+ }
481
+ const idx = effects.lastIndexOf(e);
482
+ if (idx >= 0) effects.splice(idx, 1);
483
+ }
484
+ }
485
+ auraTgt.detectRangeMod = (auraTgt.detectRangeMod ?? 0) + amount;
486
+ auraTgt.buffActive.add(spell.id);
487
+ effects.push({
488
+ kind: 'buff',
489
+ source: src.side,
490
+ target: auraTgt.side,
491
+ spellId: spell.id,
492
+ name: spell.name,
493
+ amount,
494
+ detectRangeAmount: amount,
495
+ expiresAt: exp,
496
+ });
497
+ push({
498
+ t, kind: 'aura', actor: auraTgt.side, name: spell.name, applied: true, spellId: spell.id,
499
+ });
500
+ if (auraTgt.side !== src.side) {
501
+ push({
502
+ t, kind: 'spell_hit', actor: src.side, target: auraTgt.side,
503
+ spellId: spell.id, name: spell.name, result: 'hit', amount: 0, school: spell.school,
504
+ });
505
+ }
506
+ markFinisherHit();
507
+ }
508
+ } else if (eff.kind === 'mod_invisibility') {
509
+ // SPELL_AURA_MOD_INVISIBILITY — stealth / invis. Cite: SpellAuras.cpp:2626–2675.
510
+ src.buffActive.add(spell.id);
511
+ const invisUntil = t + durFor(eff.durationMs);
512
+ // Possess Inv 17250: invis + school immune + stun. Vanish is untargetable.
513
+ // Cite: boss_baroness_anastari.cpp:37 SPELL_POSSESS_INV.
514
+ if (isPossessInvSpell(spell.id)) {
515
+ src.unattackableUntil = Math.max(src.unattackableUntil, invisUntil);
516
+ }
517
+ effects.push({
518
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
519
+ amount: eff.amount, expiresAt: invisUntil,
520
+ });
521
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true, spellId: spell.id });
522
+ markFinisherHit();
523
+ } else if (eff.kind === 'track_creatures') {
524
+ // SPELL_AURA_TRACK_CREATURES — minimap. Cite: SpellAuras.cpp:2152–2161.
525
+ src.buffActive.add(spell.id);
526
+ effects.push({
527
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
528
+ amount: eff.trackFlag || eff.creatureType, expiresAt: t + durFor(eff.durationMs),
529
+ });
530
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
531
+ markFinisherHit();
532
+ } else if (eff.kind === 'channel_death_item') {
533
+ // Apply is a no-op; shards ride the DoT. Cite: SpellAuras.cpp:2072; sessionDeaths.ts:246.
534
+ markFinisherHit();
535
+ } else if (eff.kind === 'leap') {
536
+ // LEAP (29) — a near-teleport of radiusYd along the facing. The facing is the foe when hostile,
537
+ // else the move intent, else +x. Cite: SpellEffects.cpp:4895–4906.
538
+ const dist = (spell.radiusYd != null && spell.radiusYd > 0) ? spell.radiusYd : 0;
539
+ const foe = typeof foeOf === 'function' ? foeOf(src) : null;
540
+ let faceToward = foe && foe.side !== src.side ? posOf?.(foe.side) : undefined;
541
+ if (!faceToward && typeof L.moveIntentOf === 'function' && typeof posOf === 'function') {
542
+ const intent = L.moveIntentOf(src.side) as { x: number; y: number } | null | undefined;
543
+ const from = posOf(src.side) as { x: number; y: number } | undefined;
544
+ if (intent && from) faceToward = { x: from.x + intent.x, y: from.y + intent.y };
545
+ }
546
+ applyLeapEffect({
547
+ t,
548
+ spell,
549
+ actor: src.side,
550
+ distanceYd: dist,
551
+ faceToward,
552
+ posOf,
553
+ setPos,
554
+ displaced,
555
+ clampLanding: (from, dest) => clampDisplacementToFloor(from, dest, displaceFloorFromCfg(cfg)),
556
+ push,
557
+ });
558
+ markFinisherHit();
559
+ } else if (eff.kind === 'trade_skill') {
560
+ // Profession open — OOC utility; emit flavor so the cast is observable.
561
+ push({
562
+ t, kind: 'flavor',
563
+ text: `${src.spec.name} works a trade skill.`,
564
+ });
565
+ markFinisherHit();
566
+ } else if (eff.kind === 'inebriate') {
567
+ // Alcohol: apply drunkMod on player fighters. Cite: EffectInebriate; KI-734.
568
+ const mod = Number((eff as { drunkMod?: number }).drunkMod) || 0;
569
+ if (mod > 0 && src.side === 'player') {
570
+ const cur = Math.max(0, Math.min(0xFFFF, (src.spec.drunkValue ?? 0) | 0));
571
+ src.spec.drunkValue = Math.min(0xFFFF, cur + Math.trunc(mod));
572
+ }
573
+ push({
574
+ t, kind: 'flavor',
575
+ text: `${src.spec.name} feels tipsy.`,
576
+ });
577
+ markFinisherHit();
578
+ } else if (eff.kind === 'mod_skill') {
579
+ // MOD_SKILL / MOD_SKILL_TALENT — a live ModifySkillBonus on the target. Permanent talent ranks
580
+ // are baked at build, but an equip or temp aura applied mid-fight still has to raise the value.
581
+ const skillId = ((eff as { skillId?: number }).skillId ?? 0) | 0;
582
+ const amount = ((eff as { amount?: number }).amount ?? 0) | 0;
583
+ if (skillId && amount) {
584
+ const bonuses = { ...(src.spec.skillBonuses ?? {}) };
585
+ bonuses[skillId] = (bonuses[skillId] ?? 0) + amount;
586
+ src.spec.skillBonuses = bonuses;
587
+ const pureWeapon = (id: number): number => {
588
+ const row = src.spec.combatSkills?.[id];
589
+ if (row) return row.current | 0;
590
+ // Legacy absent map → L×5 pure.
591
+ return Math.max(1, (src.spec.level | 0) * 5);
592
+ };
593
+ if (skillId === 95 /* SKILL_DEFENSE */) {
594
+ src.spec.defenseSkill = pureWeapon(skillId) + (bonuses[skillId] ?? 0);
595
+ } else {
596
+ if (src.spec.weaponSkillId === skillId) {
597
+ src.spec.weaponSkill = pureWeapon(skillId) + (bonuses[skillId] ?? 0);
598
+ }
599
+ if (src.spec.offhandSkillId === skillId) {
600
+ src.spec.offhandWeaponSkill = pureWeapon(skillId) + (bonuses[skillId] ?? 0);
601
+ }
602
+ if (src.spec.rangedSkillId === skillId) {
603
+ src.spec.rangedWeaponSkill = pureWeapon(skillId) + (bonuses[skillId] ?? 0);
604
+ }
605
+ }
606
+ src.buffActive.add(spell.id);
607
+ effects.push({
608
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
609
+ amount, expiresAt: t + durFor((eff as { durationMs?: number }).durationMs || 15_000),
610
+ });
611
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
612
+ }
613
+ markFinisherHit();
614
+ } else if (
615
+ eff.kind === 'summon_wild'
616
+ || eff.kind === 'summon_guardian'
617
+ || eff.kind === 'summon_demon'
618
+ || eff.kind === 'summon_possessed'
619
+ || eff.kind === 'summon_pet'
620
+ ) {
621
+ // SUMMON* (28/41/42/56/73/112) — a creature body on the caster's own side, once per cast
622
+ // however many effect slots or AoE targets this pass covers.
623
+ const made = runCastSummons(src, spell);
624
+ if (made.length > 0) {
625
+ const label = made.length > 1 ? `${made[0]!.spec.name} x${made.length}` : made[0]!.spec.name;
626
+ push({ t, kind: 'flavor', text: `${src.spec.name} summons ${label}.` });
627
+ } else if (eff.kind === 'summon_guardian') {
628
+ // No body landed (no resolveSummon, unknown entry, roster full): the old
629
+ // stand-in still pulls focus off the caster so Target Dummy reads as doing something.
630
+ for (const foe of livingHostilesFor(src)) {
631
+ const tm = threatOf(foe);
632
+ if (tm) tm.addThreatDirectly(src.side, -2000, false);
633
+ }
634
+ }
635
+ markFinisherHit();
636
+ } else if (
637
+ eff.kind === 'transmitted_object'
638
+ || eff.kind === 'summon_object_slot'
639
+ || eff.kind === 'enchant_held_item'
640
+ || eff.kind === 'sanctuary'
641
+ || eff.kind === 'mod_health_regen_percent'
642
+ || eff.kind === 'skill_step'
643
+ || eff.kind === 'skill_marker'
644
+ || eff.kind === 'mod_stalked'
645
+ || eff.kind === 'mod_language'
646
+ || eff.kind === 'force_reaction'
647
+ || eff.kind === 'prevents_fleeing'
648
+ || eff.kind === 'teleport_face_caster'
649
+ || eff.kind === 'add_farsight'
650
+ || eff.kind === 'summon_change_item'
651
+ || eff.kind === 'script_handled_noop'
652
+ // Feed Pet is OOC (CheckCast pet combat); IR marker only in combat.
653
+ // Cite: Spell.cpp:5332–5355; EffectFeedPet; KI-854.
654
+ || eff.kind === 'feed_pet'
655
+ ) {
656
+ // Utility / world / skill surfaces — aura or flavor so the cast is observable.
657
+ // Cite: SpellEffects / SpellAuras for each kind.
658
+ const auraKinds = new Set([
659
+ 'sanctuary', 'mod_health_regen_percent', 'mod_stalked', 'mod_language',
660
+ 'force_reaction', 'prevents_fleeing',
661
+ ]);
662
+ if (auraKinds.has(eff.kind)) {
663
+ // mod_stalked tracks the judged foe; mod_language and prevents_fleeing land on the enemy the
664
+ // curse names, never as a caster self-buff (KI-765).
665
+ const utilityHostile = (
666
+ eff.kind === 'mod_language' || eff.kind === 'prevents_fleeing'
667
+ ) && (
668
+ (eff as { hostile?: boolean }).hostile === true
669
+ || spell.effects.some((e) => e.kind === 'buff' && isHostileBuffEffect(e, e.amount ?? 0))
670
+ );
671
+ const tgtUnit = (eff.kind === 'mod_stalked' || utilityHostile) ? tgt : src;
672
+ // Track-unit only. When the same spell also carries a real combat mod, do NOT seed a no-mod
673
+ // effect: that trips the sheet-baked path and the attack-power rider never lands.
674
+ const skipHollowStalked =
675
+ eff.kind === 'mod_stalked'
676
+ && spell.effects.some((e) => e.kind === 'buff' && e.mod != null);
677
+ const skipHollowUtility =
678
+ utilityHostile
679
+ && spell.effects.some((e) => e.kind === 'buff' && e.mod != null);
680
+ if (!skipHollowStalked && !skipHollowUtility) {
681
+ tgtUnit.buffActive.add(spell.id);
682
+ effects.push({
683
+ kind: 'buff', source: src.side, target: tgtUnit.side, spellId: spell.id, name: spell.name,
684
+ amount: (eff as { amount?: number }).amount ?? 0,
685
+ expiresAt: t + durFor((eff as { durationMs?: number }).durationMs || 15_000),
686
+ });
687
+ push({ t, kind: 'aura', actor: tgtUnit.side, name: spell.name, applied: true });
688
+ }
689
+ // Vanish OnCast: Clean Escape 23582 → heal 23583. Cite: Rogue.cpp:87–88.
690
+ if (
691
+ eff.kind === 'sanctuary'
692
+ && (eff as { vanishRogueSpecial?: boolean }).vanishRogueSpecial
693
+ && src.buffActive.has(CLEAN_ESCAPE_AURA_SPELL_ID)
694
+ ) {
695
+ // spell_template 23583 EffectHeal bp 499 → 500.
696
+ const before = src.hp;
697
+ src.hp = Math.min(src.spec.maxHealth, src.hp + 500);
698
+ if (src.hp !== before) {
699
+ push({ t, kind: 'heal', actor: src.side, amount: src.hp - before });
700
+ }
701
+ }
702
+ } else {
703
+ push({
704
+ t, kind: 'flavor',
705
+ text: `${src.spec.name} uses ${spell.name}.`,
706
+ });
707
+ }
708
+ markFinisherHit();
709
+ } else if (eff.kind === 'threat') {
710
+ // THREAT (63) — a flat AddThreat on the enemy's list for the caster. Cast on a specific enemy
711
+ // it touches only that manager; otherwise every living one, which is the totem path.
712
+ const threatTgt = isEnemySide(tgt.side) ? tgt : undefined;
713
+ applyFlatThreat(src, eff.amount, spell, threatTgt);
714
+ // spell_threat flat (Distracting Shot row amount 100) rides the same hit.
715
+ // Cite: Spell.cpp HandleThreatSpells; spell_threat.threat.
716
+ if (threatTgt) applySpellThreatBonus(src, spell, threatTgt, state.hitCount);
717
+ else applySpellThreatBonus(src, spell, undefined, state.hitCount);
718
+ // Combat log: pure THREAT has no damage row — still attribute the cast (KI-746).
719
+ if (threatTgt) {
720
+ push({
721
+ t, kind: 'spell_hit', actor: src.side, target: threatTgt.side,
722
+ spellId: spell.id, name: spell.name, result: 'hit', amount: 0, school: spell.school,
723
+ });
724
+ }
725
+ markFinisherHit();
726
+ } else if (eff.kind === 'taunt') {
727
+ // SPELL_EFFECT_ATTACK_ME (114) — EffectTaunt equalize + force victim.
728
+ // Cite: SpellEffects.cpp:3413–3435. Wave 4A: taunt the targeted enemy only.
729
+ if (isEnemySide(tgt.side)) {
730
+ const tm = threatOf(tgt);
731
+ if (tm) tm.applyTaunt(src.side, false);
732
+ // spell_threat flat rides the taunt hit too (Growl bakes 415 flat) —
733
+ // HandleThreatSpells applies on any negative spell hit, not only damage.
734
+ applySpellThreatBonus(src, spell, tgt, state.hitCount);
735
+ // Combat log: a pure ATTACK_ME has no damage or aura row, so attribute the cast to the caster
736
+ // anyway or an ally tank kit never appears in the fight log.
737
+ push({
738
+ t, kind: 'spell_hit', actor: src.side, target: tgt.side,
739
+ spellId: spell.id, name: spell.name, result: 'hit', amount: 0, school: spell.school,
740
+ });
741
+ markFinisherHit();
742
+ }
743
+ } else if (eff.kind === 'mod_taunt') {
744
+ // MOD_TAUNT (11) — a TauntUpdate hold for the duration, distinct from the EffectTaunt equalize.
745
+ // Cite: SpellAuras.cpp:2860–2875; ThreatManager.cpp:557–575.
746
+ if (isEnemySide(tgt.side)) {
747
+ const dur = durFor(eff.durationMs);
748
+ const exp = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
749
+ // One (caster, spell, target) hold at a time — refresh like other auras.
750
+ const mine = rowsOn(tgt);
751
+ for (let i = mine.length - 1; i >= 0; i--) {
752
+ const e = mine[i]!;
753
+ if (
754
+ e.kind === 'mod_taunt'
755
+ && e.spellId === spell.id
756
+ && e.source === src.side
757
+ ) {
758
+ const idx = effects.lastIndexOf(e);
759
+ if (idx >= 0) effects.splice(idx, 1);
760
+ }
761
+ }
762
+ const tm = threatOf(tgt);
763
+ if (tm) {
764
+ const row = applyModTauntAura(tm, src, tgt, spell.id, spell.name, exp);
765
+ if (row) effects.push(row);
766
+ }
767
+ }
768
+ } else if (eff.kind === 'restore_health' || eff.kind === 'restore_power') {
769
+ // PERIODIC_MANA_LEECH bakes as restore_power with a negative per-tick amount, so apply it as a
770
+ // hostile periodic on the target rather than a self restore (KI-101).
771
+ if (eff.kind === 'restore_power' && eff.amountPerTick < 0 && eff.pctOfMax == null) {
772
+ const dur = durFor(eff.durationMs);
773
+ // DurationIndex 21 (−1 infinite) → until-cancelled, same as speed/reflect.
774
+ // Cite: duration.ts FALLBACK 21; SpellAuras.cpp:1031–1042 Nitrous until mana runs out.
775
+ const exp = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
776
+ const tickMs = Math.max(1, eff.tickMs);
777
+ // hostile:false → self maintain cost (Nitrous); default → foe leech (Viper Sting).
778
+ // Cite: schema restore_power.hostile; SpellAuras.cpp:1031–1042.
779
+ const drainTgt = eff.hostile === false ? src : tgt;
780
+ // One (caster, spell, target) leech at a time — refresh like DoTs.
781
+ const mine = rowsOn(drainTgt);
782
+ for (let i = mine.length - 1; i >= 0; i--) {
783
+ const e = mine[i]!;
784
+ if (
785
+ e.kind === 'restore_power'
786
+ && e.spellId === spell.id
787
+ && e.source === src.side
788
+ ) {
789
+ const idx = effects.lastIndexOf(e);
790
+ if (idx >= 0) effects.splice(idx, 1);
791
+ }
792
+ }
793
+ const manaLeechMult =
794
+ typeof eff.leechMultiple === 'number' && eff.leechMultiple > 0
795
+ ? eff.leechMultiple
796
+ : undefined;
797
+ effects.push({
798
+ kind: 'restore_power',
799
+ source: src.side,
800
+ target: drainTgt.side,
801
+ spellId: spell.id,
802
+ name: spell.name,
803
+ amountPerTick: eff.amountPerTick,
804
+ // The pool drained is the effect's own, not the victim's primary bar. Cite: :4746-4753.
805
+ ...(eff.powerType != null ? { powerType: eff.powerType | 0 } : {}),
806
+ tickMs,
807
+ nextTickAt: t + tickMs,
808
+ expiresAt: exp,
809
+ // Drain Mana transfers; Viper Sting leaves this unset (pure drain). KI-702.
810
+ ...(manaLeechMult != null ? { leechMultiple: manaLeechMult } : {}),
811
+ });
812
+ push({ t, kind: 'aura', actor: drainTgt.side, name: spell.name, applied: true });
813
+ markFinisherHit();
814
+ } else {
815
+ // A periodic HoT or power regen applies as a lasting effect the sweep ticks, never one lump
816
+ // sum. A pet-targeted funnel sits on the pet; a friendly HoT follows selected-or-self.
817
+ const restoreTgt = (() => {
818
+ // PERIODIC_ENERGIZE sits on GetTarget(), not the caster, and a self-cast simply makes the
819
+ // two the same. Cite: SpellAuras.cpp:4809-4844; boss_vaelastrasz.cpp:153.
820
+ if (eff.kind === 'restore_power') return tgt;
821
+ if (eff.kind !== 'restore_health') return src;
822
+ const applyTo = resolveHealApplyTo(spell, eff);
823
+ if (applyTo === 'pet' || roleOf(spell) === 'pet_heal') {
824
+ return resolvePetHealFighter(src, sideFighter, pet, dead, isAllySide);
825
+ }
826
+ if (applyTo === 'self') return src;
827
+ // TARGET_UNIT_FRIEND is the selected unit, not heal triage. Cite: Spell.cpp:2298-2306.
828
+ const pinned = src.friendlyPin ? sideFighter(src.friendlyPin) : null;
829
+ return selectedOrSelfFriendly(src, tgt, pinned, dead, isEnemySide);
830
+ })();
831
+ if (!restoreTgt || dead(restoreTgt)) return;
832
+ // Essence of the Red has three energize rows and the C++ skips a power the unit has no max
833
+ // of. The pool comes off the effect; rawEffects is the fallback for a pre-powerType bake.
834
+ // No x10 rage conversion here: the interpreter already did it when it rebuilt these
835
+ // effects, and doing it twice read 2 rage a second instead of 20.
836
+ let restorePower: number | undefined;
837
+ if (eff.kind === 'restore_power') {
838
+ if (eff.powerType != null) restorePower = eff.powerType | 0;
839
+ else {
840
+ const idx = spell.effects.indexOf(eff);
841
+ const misc = idx >= 0 ? spell.rawEffects?.[idx]?.miscValue : undefined;
842
+ if (misc != null) restorePower = misc | 0;
843
+ }
844
+ // GetMaxPower(power) == 0 → the aura never lands. Cite: SpellAuras.cpp:4838.
845
+ if (restorePower != null && maxPowerOfType(restoreTgt, restorePower) <= 0) return;
846
+ }
847
+ const dur = durFor(eff.durationMs);
848
+ if (dur <= 0) return;
849
+ const tickMs = Math.max(1, eff.tickMs);
850
+ const amountPerTick = Math.max(0, eff.amountPerTick);
851
+ const pctOfMax = eff.pctOfMax != null && eff.pctOfMax > 0 ? Math.trunc(eff.pctOfMax) : undefined;
852
+ const live = rowsOn(restoreTgt).find(
853
+ (e) => e.kind === eff.kind && e.spellId === spell.id,
854
+ );
855
+ if (live) {
856
+ // Refresh: reset duration + tick schedule (DoT refresh pattern).
857
+ live.amountPerTick = amountPerTick;
858
+ live.pctOfMax = pctOfMax;
859
+ live.tickMs = tickMs;
860
+ live.nextTickAt = t + tickMs;
861
+ live.expiresAt = t + dur;
862
+ if (setEffectSource) setEffectSource(live, src.side); else live.source = src.side;
863
+ live.name = spell.name;
864
+ if (restorePower != null) live.powerType = restorePower;
865
+ touchEffect(live);
866
+ } else {
867
+ effects.push({
868
+ kind: eff.kind, source: src.side, target: restoreTgt.side,
869
+ spellId: spell.id, name: spell.name,
870
+ amountPerTick, pctOfMax, tickMs, nextTickAt: t + tickMs,
871
+ ...(restorePower != null ? { powerType: restorePower } : {}),
872
+ expiresAt: t + dur,
873
+ });
874
+ }
875
+ restoreTgt.buffActive.add(spell.id);
876
+ push({ t, kind: 'aura', actor: restoreTgt.side, name: spell.name, applied: true });
877
+ }
878
+ } else if (eff.kind === 'seal') {
879
+ // Cast a paladin seal (only one at a time). Drop any existing seal, arm the new one, and
880
+ // mirror it as a self-buff so the rotation/UI see it as present (drives seal upkeep).
881
+ if (src.activeSeal && src.activeSeal.spellId !== spell.id) {
882
+ const old = src.activeSeal.spellId;
883
+ src.buffActive.delete(old);
884
+ for (const e of rowsOn(src)) if (e.kind === 'buff' && e.spellId === old) { e.expiresAt = Math.min(e.expiresAt, t); touchEffect(e); }
885
+ }
886
+ src.activeSeal = {
887
+ spellId: spell.id, name: spell.name, seal: eff.seal, coeff: eff.coeff, procPpm: eff.procPpm,
888
+ onSwingWeaponPct: eff.onSwingWeaponPct, onSwingHeal: eff.onSwingHeal, onSwingMana: eff.onSwingMana,
889
+ judge: eff.judge, expiresAt: t + eff.durationMs,
890
+ };
891
+ src.buffActive.add(spell.id);
892
+ effects.push({ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name, expiresAt: t + eff.durationMs });
893
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
894
+ } else if (eff.kind === 'weapon_imbue') {
895
+ // The temp enchant lands on the main hand by default, or the off-hand when the fighter targets
896
+ // it. A refresh strips the previous hand's AP first. Pending is the one-shot and wins.
897
+ const hand: 'mainhand' | 'offhand' = src.pendingWeaponImbueHand
898
+ ?? (src.spec.weaponImbueCastHand === 'offhand' ? 'offhand' : 'mainhand');
899
+ // One-shot pending hand so a later cast defaults back to MH unless re-armed.
900
+ if (src.pendingWeaponImbueHand) src.pendingWeaponImbueHand = undefined;
901
+ const prev = src.weaponImbues[hand];
902
+ if (prev) {
903
+ // The old enchant's TYPE_EQUIP_SPELL aura goes with it, so a swapped oil leaves no
904
+ // passive behind. Cite: Player.cpp:11299-11310 RemoveAurasDueToItemSpell.
905
+ const goneIds = prev.equipSpellId
906
+ ? [prev.spellId, prev.equipSpellId]
907
+ : [prev.spellId];
908
+ for (const id of goneIds) src.buffActive.delete(id);
909
+ if (prev.attackPower) src.attackPower = Math.max(0, src.attackPower - prev.attackPower);
910
+ for (const e of rowsOn(src)) {
911
+ if (e.kind === 'buff' && e.spellId != null && goneIds.includes(e.spellId)) {
912
+ e.expiresAt = Math.min(e.expiresAt, t);
913
+ touchEffect(e);
914
+ }
915
+ }
916
+ if (prev.spellId !== spell.id) {
917
+ push({ t, kind: 'aura', actor: src.side, name: prev.name, applied: false });
918
+ }
919
+ }
920
+ const imbue: ActiveWeaponImbue = {
921
+ spellId: spell.id,
922
+ name: spell.name,
923
+ expiresAt: t + eff.durationMs,
924
+ enchantId: eff.enchantId,
925
+ family: eff.family,
926
+ attackPower: eff.attackPower,
927
+ equipSpellId: eff.equipSpellId,
928
+ combatChancePct: eff.combatChancePct,
929
+ combatPpm: eff.combatPpm,
930
+ damageFlat: eff.damageFlat,
931
+ rockbiterAmount: eff.rockbiterAmount,
932
+ combat: eff.combat,
933
+ };
934
+ src.weaponImbues[hand] = imbue;
935
+ if (eff.attackPower) src.attackPower += eff.attackPower;
936
+ src.buffActive.add(spell.id);
937
+ // Refresh duration on existing buff effect when re-applying same imbue.
938
+ const liveImbueBuff = rowsOn(src).find(
939
+ (e) => e.kind === 'buff' && e.spellId === spell.id,
940
+ );
941
+ if (liveImbueBuff) {
942
+ liveImbueBuff.expiresAt = t + eff.durationMs;
943
+ touchEffect(liveImbueBuff);
944
+ } else {
945
+ effects.push({
946
+ kind: 'buff', source: src.side, target: src.side,
947
+ spellId: spell.id, name: spell.name, expiresAt: t + eff.durationMs,
948
+ });
949
+ }
950
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
951
+ } else if (eff.kind === 'judgement') {
952
+ // Judgement consumes the active seal and unleashes its payload; with no seal up it is inert.
953
+ // A damage seal is a holy burst, a utility seal a debuff whose on-hit proc pays the striker.
954
+ const seal = src.activeSeal;
955
+ if (!seal) return;
956
+ src.activeSeal = null;
957
+ src.buffActive.delete(seal.spellId);
958
+ for (const e of rowsOn(src)) if (e.kind === 'buff' && e.spellId === seal.spellId) { e.expiresAt = Math.min(e.expiresAt, t); touchEffect(e); }
959
+ push({ t, kind: 'aura', actor: src.side, name: seal.name, applied: false });
960
+ const j = seal.judge;
961
+ if (j) {
962
+ const tgtTotem = totemRefusesNegativeAuraApply(
963
+ tgt.creatureType ?? tgt.spec.creatureType,
964
+ true,
965
+ ) || isTotemSide(tgt.side);
966
+ // JoL and JoW apply or refresh the on-hit debuff on the target, with no direct damage. A
967
+ // totem refuses the aura index while the script effect still fires.
968
+ if (!tgtTotem && j.debuffSpellId && (j.onHitHeal || j.onHitMana)) {
969
+ const debuffName = j.debuffName ?? (j.onHitHeal ? 'Judgement of Light' : 'Judgement of Wisdom');
970
+ const debuffDur = j.debuffDurationMs ?? 10_000;
971
+ // One JoL/JoW family at a time on the target (refresh or replace prior judgement debuff).
972
+ for (const e of rowsOn(tgt)) {
973
+ if (
974
+ e.kind === 'proc_trigger_spell'
975
+ && (e.onHitHeal != null || e.onHitMana != null)
976
+ ) {
977
+ e.expiresAt = Math.min(e.expiresAt, t);
978
+ touchEffect(e);
979
+ }
980
+ }
981
+ const live = rowsOn(tgt).find(
982
+ (e) =>
983
+ e.kind === 'proc_trigger_spell'
984
+ && e.spellId === j.debuffSpellId
985
+ && e.expiresAt > t,
986
+ );
987
+ if (live) {
988
+ live.expiresAt = t + debuffDur;
989
+ live.onHitHeal = j.onHitHeal;
990
+ live.onHitMana = j.onHitMana;
991
+ live.name = debuffName;
992
+ touchEffect(live);
993
+ } else {
994
+ effects.push({
995
+ kind: 'proc_trigger_spell',
996
+ source: src.side,
997
+ target: tgt.side,
998
+ spellId: j.debuffSpellId,
999
+ name: debuffName,
1000
+ // Intermediate 5373/1826 is empty in the bake; onHit* is the real consumer.
1001
+ // Cite: Paladin.cpp JoL/JoW intermediate scripts; accepted JoC bake-gap pattern.
1002
+ triggerSpellId: 0,
1003
+ onHitHeal: j.onHitHeal,
1004
+ onHitMana: j.onHitMana,
1005
+ expiresAt: t + debuffDur,
1006
+ nextProcAt: 0,
1007
+ });
1008
+ }
1009
+ push({ t, kind: 'aura', actor: tgt.side, name: debuffName, applied: true });
1010
+ }
1011
+
1012
+ // Judgement of the Crusader is a hostile MOD_DAMAGE_TAKEN — flat spell and holy taken.
1013
+ // Cite: Paladin.cpp:78–116 spell_judgement; SpellAuras.cpp SPELL_AURA_MOD_DAMAGE_TAKEN.
1014
+ if (!tgtTotem && j.debuffSpellId && j.spellTaken != null && j.spellTaken !== 0) {
1015
+ const debuffName = j.debuffName ?? 'Judgement of the Crusader';
1016
+ const debuffDur = j.debuffDurationMs ?? 10_000;
1017
+ const amount = j.spellTaken;
1018
+ // One JoC-family (spell_taken from a Judgement of the Crusader) at a time.
1019
+ for (const e of rowsOn(tgt)) {
1020
+ if (
1021
+ e.kind === 'buff'
1022
+ && e.mod === 'spell_taken'
1023
+ && e.spellId != null
1024
+ && (
1025
+ e.spellId === 21183 || e.spellId === 20188 || e.spellId === 20300
1026
+ || e.spellId === 20301 || e.spellId === 20302 || e.spellId === 20303
1027
+ || (e.name != null && /Judgement of the Crusader/i.test(e.name))
1028
+ )
1029
+ ) {
1030
+ if (e.amount) reverseBuffMod(tgt, 'spell_taken', e.amount);
1031
+ e.expiresAt = Math.min(e.expiresAt, t);
1032
+ tgt.buffActive.delete(e.spellId);
1033
+ touchEffect(e);
1034
+ }
1035
+ }
1036
+ const live = rowsOn(tgt).find(
1037
+ (e) =>
1038
+ e.kind === 'buff'
1039
+ && e.spellId === j.debuffSpellId
1040
+ && e.expiresAt > t,
1041
+ );
1042
+ if (live && live.kind === 'buff') {
1043
+ if (live.mod === 'spell_taken' && live.amount) {
1044
+ reverseBuffMod(tgt, 'spell_taken', live.amount);
1045
+ }
1046
+ live.expiresAt = t + debuffDur;
1047
+ live.mod = 'spell_taken';
1048
+ live.amount = amount;
1049
+ live.name = debuffName;
1050
+ touchEffect(live);
1051
+ applyBuffMod(tgt, 'spell_taken', amount);
1052
+ } else {
1053
+ effects.push({
1054
+ kind: 'buff',
1055
+ source: src.side,
1056
+ target: tgt.side,
1057
+ spellId: j.debuffSpellId,
1058
+ name: debuffName,
1059
+ mod: 'spell_taken',
1060
+ amount,
1061
+ expiresAt: t + debuffDur,
1062
+ });
1063
+ applyBuffMod(tgt, 'spell_taken', amount);
1064
+ }
1065
+ tgt.buffActive.add(j.debuffSpellId);
1066
+ push({ t, kind: 'aura', actor: tgt.side, name: debuffName, applied: true });
1067
+ }
1068
+
1069
+ // JoR / Judgement of Command direct holy burst (max > 0). Cite: Judgement of Righteousness/Command carriers.
1070
+ if (j.max > 0) {
1071
+ // Paladin.cpp:115 TRIGGERED_IGNORE_HIT_CALCULATION → Spell.cpp:458/951 SPELL_MISS_NONE.
1072
+ const skipHit = triggeredSpellIgnoresHitResult(JUDGEMENT_PAYLOAD_TRIGGER_FLAGS);
1073
+ let landed = true;
1074
+ if (!skipHit) {
1075
+ const land = spellDamageLandChance({
1076
+ atkLevel: src.spec.level,
1077
+ defLevel: tgt.spec.level,
1078
+ school: j.school,
1079
+ resistance: tgt.resistances[j.school] ?? 0,
1080
+ binary: false,
1081
+ spellHitBonusPercent: src.spellHitBonusPercent,
1082
+ aoeAvoidancePercent: spell.aoe ? (tgt.aoeAvoidancePercent ?? 0) : 0,
1083
+ attackerPlayerControlled: src.side === 'player' || src.side === 'pet' || isAllySide(src.side),
1084
+ victimIsNpc: isEnemySide(tgt.side),
1085
+ victimPlayerControlled: tgt.side === 'player' || tgt.side === 'pet' || isAllySide(tgt.side),
1086
+ });
1087
+ landed = rng.chance(land);
1088
+ }
1089
+ if (!landed) {
1090
+ push({ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id, name: spell.name, result: 'miss', amount: 0, school: j.school });
1091
+ } else {
1092
+ let raw = rollBetween(j.min, j.max, rng.next());
1093
+ // Judgement of Command hits for half unless the target is stunned (cmangos JudgementOfCommand).
1094
+ if (j.halveIfNotStunned && !(tgt.stunnedUntil > t)) raw = raw / 2;
1095
+ const payload = j.spellId != null
1096
+ ? src.spec.spells.find((s) => s.id === j.spellId)
1097
+ : undefined;
1098
+ const crit = spellCanCrit(payload ?? { rawEffects: [{ effect: SPELL_EFFECT_SCHOOL_DAMAGE }] })
1099
+ && rng.chance(spellCritChance(fighterSpellCrit(src), src.spec.level, tgt.spec.level, spell.dmgClass));
1100
+ if (crit) raw *= critMultiplierWithVersus(TUNING.spellCritMult, src.critDamageVersusPct, tgt.creatureType);
1101
+ const dmgPct = src.damagePctBySchool[j.school] ?? 0;
1102
+ if (dmgPct) raw *= (1 + dmgPct / 100);
1103
+ const amount = dealDamage(tgt, raw, j.school, src.spec.level, {
1104
+ binary: false, damageType: 'spell', attacker: src,
1105
+ });
1106
+ push({ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id, name: spell.name, result: crit ? 'crit' : 'hit', amount, school: j.school });
1107
+ if (amount > 0) {
1108
+ triggerProcs(src, tgt, !!crit, 'spell', spell);
1109
+ rollGearProcs(src, tgt, crit ? 'crit' : 'hit', spell);
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ } else if (eff.kind === 'shapeshift') {
1115
+ // Form enter/toggle. FORM_STEALTH pairs with a stealth effect on the same spell —
1116
+ // only set the stance bit when entering (toggle-off is handled by the stealth effect).
1117
+ if (isStealthShapeshift(eff.form) && spell.effects.some((e) => e.kind === 'stealth')) {
1118
+ if (!src.stealthed) enterForm(src, eff.form, spell.name, spell.id);
1119
+ // if already stealthed, stealth effect below will break (toggle off)
1120
+ } else {
1121
+ enterForm(src, eff.form, spell.name, spell.id);
1122
+ }
1123
+ markFinisherHit();
1124
+ } else if (eff.kind === 'stealth') {
1125
+ // Classic toggle: recasting Stealth/Prowl while stealthed cancels the aura.
1126
+ if (src.stealthed) {
1127
+ breakStealth(src, 'toggle');
1128
+ } else {
1129
+ applyStealth(src, eff.strength, eff.durationMs, eff.breakFlags, spell);
1130
+ }
1131
+ markFinisherHit();
1132
+ } else if (eff.kind === 'stealth_level') {
1133
+ src.stealthLevelBonus += Math.max(0, eff.amount | 0);
1134
+ markFinisherHit();
1135
+ } else if (eff.kind === 'stealth_detect') {
1136
+ src.stealthDetectStrength += Math.max(0, eff.amount | 0);
1137
+ src.buffActive.add(spell.id);
1138
+ effects.push({
1139
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1140
+ expiresAt: t + Math.max(0, eff.durationMs | 0),
1141
+ });
1142
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1143
+ markFinisherHit();
1144
+ } else if (eff.kind === 'reset_cooldowns') {
1145
+ // Scripted dummy: finish cooldowns on a cluster of the caster's own spells, never the resetter
1146
+ // itself. It must `continue` on a non-match, or one miss aborts every later reset.
1147
+ let reset = 0;
1148
+ for (const cd of src.spec.spells) {
1149
+ if (cd.id === spell.id) continue; // never reset the resetter itself
1150
+ if (eff.scope === 'frost' && (cd.school !== 'frost' || getSpellRecoveryTime(cd) === 0)) continue;
1151
+ // rogue / hunter scopes: all other kit abilities (combatant list is class-scoped)
1152
+ const readyAt = src.cooldowns.get(cd.id) ?? 0;
1153
+ const catReadyAt = cd.category != null ? (src.categoryCooldowns.get(cd.category) ?? 0) : 0;
1154
+ if (readyAt <= t && catReadyAt <= t) continue;
1155
+ src.cooldowns.delete(cd.id);
1156
+ if (cd.category != null) src.categoryCooldowns.delete(cd.category);
1157
+ // A cooldown-on-event spell has not started its real timer yet, so the pending entry goes too
1158
+ // — otherwise the aura fading later re-stamps the cooldown a reset just cleared.
1159
+ src.deferredCooldowns?.delete(cd.id);
1160
+ src.deferredCategoryCooldowns?.delete(cd.id);
1161
+ reset++;
1162
+ }
1163
+ if (reset > 0) push({ t, kind: 'flavor', text: `${spell.name} resets ${reset} cooldown${reset === 1 ? '' : 's'}.` });
1164
+ markFinisherHit();
1165
+ } else if (eff.kind === 'split_damage') {
1166
+ // Soul Link redirects a percent of the caster's incoming damage to the pet. dealDamage already
1167
+ // consumes the split; this just arms it. A toggle for the fight, and it needs a live pet.
1168
+ if (pet && !dead(pet)) {
1169
+ src.splitDamage = { schoolMask: eff.schoolMask, pct: eff.pct, linkedTargetId: pet.side, id: spell.id };
1170
+ src.buffActive.add(spell.id);
1171
+ effects.push({ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name, expiresAt: t + durFor(eff.durationMs) });
1172
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1173
+ }
1174
+ markFinisherHit();
1175
+ } else if (eff.kind === 'split_damage_flat') {
1176
+ // Blessing of Sacrifice: the aura sits on a friendly and the damage splits to the caster.
1177
+ // Casting it on yourself is a no-op in the C++, so it needs a living ally.
1178
+ const splitTgt = livingFriendlies().find((f) => f.side !== src.side) ?? null;
1179
+ if (splitTgt && !dead(splitTgt) && splitTgt.side !== src.side) {
1180
+ splitTgt.splitDamageFlat = {
1181
+ schoolMask: eff.schoolMask,
1182
+ amount: eff.amount,
1183
+ linkedTargetId: src.side,
1184
+ id: spell.id,
1185
+ };
1186
+ splitTgt.buffActive.add(spell.id);
1187
+ // Track as buff for expiry (same pattern as Soul Link split_damage).
1188
+ effects.push({
1189
+ kind: 'buff',
1190
+ source: src.side,
1191
+ target: splitTgt.side,
1192
+ spellId: spell.id,
1193
+ name: spell.name,
1194
+ expiresAt: t + durFor(eff.durationMs),
1195
+ });
1196
+ push({ t, kind: 'aura', actor: splitTgt.side, name: spell.name, applied: true });
1197
+ }
1198
+ markFinisherHit();
1199
+ } else if (eff.kind === 'resist_pushback') {
1200
+ // Concentration Aura / Barkskin pushback resist %. Cite: Spell.cpp:6817–6820.
1201
+ const pushPool = typeof L.friendlyPoolOf === 'function' ? L.friendlyPoolOf(src) : livingFriendlies();
1202
+ const pushTargets: Fighter[] =
1203
+ eff.applyTo === 'party'
1204
+ ? filterPartyFanByRadius(
1205
+ src,
1206
+ pushPool,
1207
+ spell,
1208
+ eff.applyTo,
1209
+ (a, b) => (typeof L.distanceBetween === 'function' ? L.distanceBetween(a, b) : undefined),
1210
+ )
1211
+ : [src];
1212
+ for (const pt of pushTargets) {
1213
+ pt.pushbackResistChance += eff.amount;
1214
+ pt.buffActive.add(spell.id);
1215
+ // amount = resist %; no BuffMod — expiry subtracts pushbackResistChance.
1216
+ effects.push({
1217
+ kind: 'buff',
1218
+ source: src.side,
1219
+ target: pt.side,
1220
+ spellId: spell.id,
1221
+ name: spell.name,
1222
+ expiresAt: t + durFor(eff.durationMs),
1223
+ amount: eff.amount,
1224
+ });
1225
+ push({ t, kind: 'aura', actor: pt.side, name: spell.name, applied: true });
1226
+ }
1227
+ markFinisherHit();
1228
+ } else if (eff.kind === 'mod_mechanic_resistance') {
1229
+ // Focused Casting / Hardiness — interrupt/stun resist %. Cite: Unit.cpp:4015–4031.
1230
+ const mechanic = eff.mechanic | 0;
1231
+ const amount = eff.amount | 0;
1232
+ if (mechanic && amount) {
1233
+ applyMechanicResist(src, mechanic, amount);
1234
+ src.buffActive.add(spell.id);
1235
+ const dur = durFor(eff.durationMs ?? 0);
1236
+ effects.push({
1237
+ kind: 'buff',
1238
+ source: src.side,
1239
+ target: src.side,
1240
+ spellId: spell.id,
1241
+ name: spell.name,
1242
+ expiresAt: dur > 0 ? t + dur : Number.POSITIVE_INFINITY,
1243
+ mechanicResist: { mechanic, amount },
1244
+ });
1245
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1246
+ }
1247
+ markFinisherHit();
1248
+ } else if (eff.kind === 'healing_way_talent') {
1249
+ src.healingWayChancePct = eff.chancePct | 0;
1250
+ src.buffActive.add(spell.id);
1251
+ effects.push({
1252
+ kind: 'buff',
1253
+ source: src.side,
1254
+ target: src.side,
1255
+ spellId: spell.id,
1256
+ name: spell.name,
1257
+ expiresAt: (eff.durationMs | 0) > 0 ? t + durFor(eff.durationMs) : Number.POSITIVE_INFINITY,
1258
+ });
1259
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1260
+ markFinisherHit();
1261
+ } else if (eff.kind === 'healing_way') {
1262
+ applyHealingWayStack(
1263
+ tgt,
1264
+ src,
1265
+ t,
1266
+ effects,
1267
+ push,
1268
+ eff.amountPct | 0,
1269
+ durFor(eff.durationMs),
1270
+ (eff.maxStacks | 0) || HEALING_WAY_MAX_STACKS,
1271
+ rowsOn(tgt),
1272
+ );
1273
+ markFinisherHit();
1274
+ } else if (eff.kind === 'blessing_of_light') {
1275
+ // Blessing of Light on friendly/party. Cite: Paladin.cpp:119–138.
1276
+ const bolRaw = typeof L.friendlyPoolOf === 'function' ? L.friendlyPoolOf(src) : livingFriendlies();
1277
+ const bolPool = filterPartyFanByRadius(
1278
+ src,
1279
+ bolRaw,
1280
+ spell,
1281
+ eff.applyTo,
1282
+ (a, b) => (typeof L.distanceBetween === 'function' ? L.distanceBetween(a, b) : undefined),
1283
+ );
1284
+ const bolTriage = typeof L.lowestHpFriendlyOf === 'function'
1285
+ ? L.lowestHpFriendlyOf(src)
1286
+ : lowestHpFriendly();
1287
+ const bolTargets: Fighter[] =
1288
+ eff.applyTo === 'party' ? bolPool
1289
+ : eff.applyTo === 'friendly' ? [bolTriage ?? src]
1290
+ : [src];
1291
+ for (const bt of bolTargets) {
1292
+ bt.blessingOfLight = {
1293
+ holyLight: eff.holyLight,
1294
+ flashOfLight: eff.flashOfLight,
1295
+ spellId: spell.id,
1296
+ };
1297
+ bt.buffActive.add(spell.id);
1298
+ effects.push({
1299
+ kind: 'buff',
1300
+ source: src.side,
1301
+ target: bt.side,
1302
+ spellId: spell.id,
1303
+ name: spell.name,
1304
+ expiresAt: t + durFor(eff.durationMs),
1305
+ });
1306
+ push({ t, kind: 'aura', actor: bt.side, name: spell.name, applied: true });
1307
+ }
1308
+ markFinisherHit();
1309
+ } else if (eff.kind === 'holy_shock') {
1310
+ // Holy Shock heals a friendly target or deals holy damage to a hostile one, branching on the
1311
+ // target reaction. Hostility is faction-relative — an enemy caster must not take the heal arm.
1312
+ const srcFac = typeof L.factionOfFighter === 'function'
1313
+ ? L.factionOfFighter(src)
1314
+ : (isEnemySide(src.side) ? 'enemy' : 'player');
1315
+ const tgtFac = typeof L.factionOfFighter === 'function'
1316
+ ? L.factionOfFighter(tgt)
1317
+ : (isEnemySide(tgt.side) ? 'enemy' : 'player');
1318
+ if (srcFac !== tgtFac) {
1319
+ const d = eff.damage;
1320
+ const spellCrit = fighterSpellCrit(src) + (src.castCritAdd ?? 0);
1321
+ // SpellEffects.cpp:1452 casts 25912 (SCHOOL_DAMAGE), not the parent DUMMY 20473.
1322
+ const crit = spellCanCrit({ rawEffects: [{ effect: SPELL_EFFECT_SCHOOL_DAMAGE }] })
1323
+ && rng.chance(spellCritChance(spellCrit, src.spec.level, tgt.spec.level, spell.dmgClass));
1324
+ let raw = rollBetween(d.min, d.max, rng.next());
1325
+ if (crit) raw *= critMultiplierWithVersus(TUNING.spellCritMult, src.critDamageVersusPct, tgt.creatureType);
1326
+ const dmgPct = src.damagePctBySchool[d.school] ?? 0;
1327
+ if (dmgPct) raw *= (1 + dmgPct / 100);
1328
+ const amount = dealDamage(tgt, raw, d.school, src.spec.level, { damageType: 'spell', attacker: src, spell, crit });
1329
+ push({ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id, name: spell.name, result: crit ? 'crit' : 'hit', amount, school: d.school });
1330
+ if (amount > 0) applySpellThreatBonus(src, spell, tgt, state.hitCount);
1331
+ } else {
1332
+ const healTarget = (typeof L.lowestHpFriendlyOf === 'function'
1333
+ ? L.lowestHpFriendlyOf(src)
1334
+ : lowestHpFriendly()) ?? tgt;
1335
+ if (!dead(healTarget)) {
1336
+ const { amount, crit } = rollHeal(src, eff.heal.min, eff.heal.max);
1337
+ if (amount > 0) {
1338
+ healTarget.hp = Math.min(healTarget.spec.maxHealth, Math.round(healTarget.hp + amount));
1339
+ push({
1340
+ t, kind: 'heal', actor: healTarget.side, amount,
1341
+ source: src.side, name: spell.name, spellId: spell.id,
1342
+ ...(crit ? { crit: true } : {}),
1343
+ });
1344
+ applyHealThreat(src, amount, spell, healTarget.side);
1345
+ }
1346
+ }
1347
+ }
1348
+ markFinisherHit();
1349
+ } else if (eff.kind === 'combustion') {
1350
+ // Three FIRE CRITS, not three casts. The stack that slot 1 already triggered is counted
1351
+ // here so the buff bar and the crit mod agree. Cite: UnitAuraProcHandler.cpp:991-1003.
1352
+ src.combustionCharges = eff.charges;
1353
+ setCombustionStacks(src, eff, 1);
1354
+ src.buffActive.add(spell.id);
1355
+ effects.push({
1356
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1357
+ expiresAt: t + (eff.durationMs > 0 ? durFor(eff.durationMs) : COMBUSTION_UNTIL_CANCELLED_MS),
1358
+ charges: eff.charges, stacks: 1,
1359
+ });
1360
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1361
+ markFinisherHit();
1362
+ } else if (eff.kind === 'sweeping_strikes') {
1363
+ // Arm the cleave counter the melee/strike path consumes. Cite: Sweeping Strikes 5-charge proc.
1364
+ src.sweepingStrikesCharges = eff.charges;
1365
+ src.buffActive.add(spell.id);
1366
+ effects.push({ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name, expiresAt: t + durFor(eff.durationMs) });
1367
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1368
+ markFinisherHit();
1369
+ } else if (eff.kind === 'vampiric_embrace') {
1370
+ // Target debuff: shadow damage YOU deal to THIS target heals caster+party for pct%.
1371
+ // Not a self buff (KI-701). Cite: UnitAuraProcHandler.cpp:1015–1028; talent-scripted 15286.
1372
+ const until = t + durFor(eff.durationMs);
1373
+ const fromSrc = rowsFrom(src);
1374
+ for (let i = fromSrc.length - 1; i >= 0; i--) {
1375
+ const e = fromSrc[i]!;
1376
+ if (e.kind === 'buff' && e.spellId === spell.id) {
1377
+ const prev = (bySide?.(e.target) as Fighter | undefined)
1378
+ ?? (e.target === tgt.side ? tgt : e.target === src.side ? src : undefined);
1379
+ if (prev) {
1380
+ prev.vampiricEmbracePct = 0;
1381
+ prev.vampiricEmbraceCasterSide = undefined;
1382
+ }
1383
+ const idx = effects.lastIndexOf(e);
1384
+ if (idx >= 0) effects.splice(idx, 1);
1385
+ }
1386
+ }
1387
+ // Clear stale self-buff residual from older builds / OOC seed.
1388
+ src.vampiricEmbracePct = 0;
1389
+ src.buffActive.delete(spell.id);
1390
+ tgt.vampiricEmbracePct = eff.pct;
1391
+ tgt.vampiricEmbraceCasterSide = src.side;
1392
+ effects.push({
1393
+ kind: 'buff',
1394
+ source: src.side,
1395
+ target: tgt.side,
1396
+ spellId: spell.id,
1397
+ name: spell.name,
1398
+ expiresAt: until,
1399
+ hostile: true,
1400
+ });
1401
+ push({ t, kind: 'aura', actor: src.side, target: tgt.side, name: spell.name, applied: true });
1402
+ markFinisherHit();
1403
+ } else if (eff.kind === 'eye_for_an_eye') {
1404
+ // Self-aura: magic damage taken → reflect pct% (capped 50% max HP) via 25997.
1405
+ // Cite: UnitAuraProcHandler.cpp:795–808; cmangos-special-cases eyeForAnEyeReflectAmount.
1406
+ src.eyeForAnEyePct = eff.pct;
1407
+ src.buffActive.add(spell.id);
1408
+ effects.push({
1409
+ kind: 'buff',
1410
+ source: src.side,
1411
+ target: src.side,
1412
+ spellId: spell.id,
1413
+ name: spell.name,
1414
+ expiresAt: t + durFor(eff.durationMs),
1415
+ });
1416
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1417
+ markFinisherHit();
1418
+ } else if (eff.kind === 'oracle_healing_bonus') {
1419
+ // Self-aura: heals cast → self-heal pct% via 26170.
1420
+ // Cite: UnitAuraProcHandler.cpp:1046–1052 case 26169.
1421
+ src.oracleHealingBonusPct = eff.pct;
1422
+ src.buffActive.add(spell.id);
1423
+ effects.push({
1424
+ kind: 'buff',
1425
+ source: src.side,
1426
+ target: src.side,
1427
+ spellId: spell.id,
1428
+ name: spell.name,
1429
+ expiresAt: t + durFor(eff.durationMs),
1430
+ });
1431
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1432
+ markFinisherHit();
1433
+ } else if (eff.kind === 'obsidian_armor') {
1434
+ // Self-aura: non-physical school damage taken → school resist buff.
1435
+ // Cite: UnitAuraProcHandler.cpp:910–930 case 27539.
1436
+ src.obsidianArmorActive = true;
1437
+ src.buffActive.add(spell.id);
1438
+ effects.push({
1439
+ kind: 'buff',
1440
+ source: src.side,
1441
+ target: src.side,
1442
+ spellId: spell.id,
1443
+ name: spell.name,
1444
+ expiresAt: t + durFor(eff.durationMs),
1445
+ });
1446
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1447
+ markFinisherHit();
1448
+ } else if (eff.kind === 'adaptive_warding') {
1449
+ // Self-aura: non-physical/non-holy school damage taken under Mage Armor → resist buff.
1450
+ // Cite: UnitAuraProcHandler.cpp:870–907 case 28764.
1451
+ src.adaptiveWardingActive = true;
1452
+ src.buffActive.add(spell.id);
1453
+ effects.push({
1454
+ kind: 'buff',
1455
+ source: src.side,
1456
+ target: src.side,
1457
+ spellId: spell.id,
1458
+ name: spell.name,
1459
+ expiresAt: t + durFor(eff.durationMs),
1460
+ });
1461
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1462
+ markFinisherHit();
1463
+ } else if (eff.kind === 'class_power_set') {
1464
+ src.classPowerSetKind = eff.setKind;
1465
+ src.buffActive.add(spell.id);
1466
+ effects.push({
1467
+ kind: 'buff',
1468
+ source: src.side,
1469
+ target: src.side,
1470
+ spellId: spell.id,
1471
+ name: spell.name,
1472
+ expiresAt: t + durFor(eff.durationMs),
1473
+ });
1474
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1475
+ markFinisherHit();
1476
+ } else if (eff.kind === 'vestments_greater_heal') {
1477
+ src.vestmentsGreaterHealActive = true;
1478
+ src.buffActive.add(spell.id);
1479
+ effects.push({
1480
+ kind: 'buff',
1481
+ source: src.side,
1482
+ target: src.side,
1483
+ spellId: spell.id,
1484
+ name: spell.name,
1485
+ expiresAt: t + durFor(eff.durationMs),
1486
+ });
1487
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1488
+ markFinisherHit();
1489
+ } else if (eff.kind === 'dreamwalker_healing_touch') {
1490
+ src.dreamwalkerHealingTouchActive = true;
1491
+ src.buffActive.add(spell.id);
1492
+ effects.push({
1493
+ kind: 'buff',
1494
+ source: src.side,
1495
+ target: src.side,
1496
+ spellId: spell.id,
1497
+ name: spell.name,
1498
+ expiresAt: t + durFor(eff.durationMs),
1499
+ });
1500
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1501
+ markFinisherHit();
1502
+ } else if (eff.kind === 'idol_healing_touch_refund') {
1503
+ src.idolHealingTouchRefundActive = true;
1504
+ src.buffActive.add(spell.id);
1505
+ effects.push({
1506
+ kind: 'buff',
1507
+ source: src.side,
1508
+ target: src.side,
1509
+ spellId: spell.id,
1510
+ name: spell.name,
1511
+ expiresAt: t + durFor(eff.durationMs),
1512
+ });
1513
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1514
+ markFinisherHit();
1515
+ } else if (eff.kind === 'totem_lesser_healing_wave') {
1516
+ src.totemLesserHealingWaveActive = true;
1517
+ src.buffActive.add(spell.id);
1518
+ effects.push({
1519
+ kind: 'buff',
1520
+ source: src.side,
1521
+ target: src.side,
1522
+ spellId: spell.id,
1523
+ name: spell.name,
1524
+ expiresAt: t + durFor(eff.durationMs),
1525
+ });
1526
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1527
+ markFinisherHit();
1528
+ } else if (eff.kind === 'cheat_death') {
1529
+ src.cheatDeathActive = true;
1530
+ src.buffActive.add(spell.id);
1531
+ effects.push({
1532
+ kind: 'buff',
1533
+ source: src.side,
1534
+ target: src.side,
1535
+ spellId: spell.id,
1536
+ name: spell.name,
1537
+ expiresAt: t + durFor(eff.durationMs),
1538
+ });
1539
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1540
+ markFinisherHit();
1541
+ } else if (eff.kind === 'master_of_elements') {
1542
+ src.masterOfElementsPct = eff.pct;
1543
+ src.buffActive.add(spell.id);
1544
+ effects.push({
1545
+ kind: 'buff',
1546
+ source: src.side,
1547
+ target: src.side,
1548
+ spellId: spell.id,
1549
+ name: spell.name,
1550
+ expiresAt: t + durFor(eff.durationMs),
1551
+ });
1552
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1553
+ markFinisherHit();
1554
+ } else if (eff.kind === 'magic_absorption') {
1555
+ src.magicAbsorptionPct = eff.pct;
1556
+ src.buffActive.add(spell.id);
1557
+ effects.push({
1558
+ kind: 'buff',
1559
+ source: src.side,
1560
+ target: src.side,
1561
+ spellId: spell.id,
1562
+ name: spell.name,
1563
+ expiresAt: t + durFor(eff.durationMs),
1564
+ });
1565
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1566
+ markFinisherHit();
1567
+ } else if (eff.kind === 'ignite') {
1568
+ // Self-aura: fire damage dealt → DoT 12654 at rank%. Cite: UnitAuraProcHandler.cpp:970–985.
1569
+ src.igniteRankSpellId = eff.rankSpellId;
1570
+ src.buffActive.add(spell.id);
1571
+ effects.push({
1572
+ kind: 'buff',
1573
+ source: src.side,
1574
+ target: src.side,
1575
+ spellId: spell.id,
1576
+ name: spell.name,
1577
+ expiresAt: t + durFor(eff.durationMs),
1578
+ });
1579
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1580
+ markFinisherHit();
1581
+ } else if (eff.kind === 'deep_wounds') {
1582
+ // Self-aura: melee crit → DoT 12721 at weaponDamage×rank%/6.
1583
+ // Cite: UnitAuraProcHandler.cpp:1316–1342.
1584
+ src.deepWoundsRankSpellId = eff.rankSpellId;
1585
+ src.buffActive.add(spell.id);
1586
+ effects.push({
1587
+ kind: 'buff',
1588
+ source: src.side,
1589
+ target: src.side,
1590
+ spellId: spell.id,
1591
+ name: spell.name,
1592
+ expiresAt: t + durFor(eff.durationMs),
1593
+ });
1594
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1595
+ markFinisherHit();
1596
+ } else if (eff.kind === 'add_target_trigger') {
1597
+ // NoImmediateEffect: register holder; fire is applySpell ProcSpellAuraTriggers.
1598
+ // Cite: SpellAuras.cpp:170; Spell.cpp:7433–7468.
1599
+ const trig = eff.triggerSpellId | 0;
1600
+ if (!trig) return;
1601
+ const dur = durFor(eff.durationMs);
1602
+ const exp = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
1603
+ src.addTargetTriggers = src.addTargetTriggers.filter((r) => r.spellId !== spell.id);
1604
+ src.addTargetTriggers.push({
1605
+ spellId: spell.id,
1606
+ triggerSpellId: trig,
1607
+ chancePct: eff.chancePct | 0,
1608
+ familyName: (eff.familyName ?? spell.spellFamilyName ?? 0) | 0,
1609
+ familyFlags: (eff.familyFlags ?? 0) | 0,
1610
+ expiresAt: exp,
1611
+ });
1612
+ src.buffActive.add(spell.id);
1613
+ effects.push({
1614
+ kind: 'buff',
1615
+ source: src.side,
1616
+ target: src.side,
1617
+ spellId: spell.id,
1618
+ name: spell.name,
1619
+ expiresAt: exp,
1620
+ });
1621
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1622
+ markFinisherHit();
1623
+ } else if (eff.kind === 'improved_sprint') {
1624
+ // Self-aura: on successful Sprint cast, chance% → cast 30918 (purge root+snare).
1625
+ // Cite: Spell.cpp:7433–7468 ProcSpellAuraTriggers; SpellEffects.cpp:4140–4147.
1626
+ src.improvedSprintChancePct = eff.chancePct | 0;
1627
+ src.buffActive.add(spell.id);
1628
+ effects.push({
1629
+ kind: 'buff',
1630
+ source: src.side,
1631
+ target: src.side,
1632
+ spellId: spell.id,
1633
+ name: spell.name,
1634
+ expiresAt: t + durFor(eff.durationMs),
1635
+ });
1636
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1637
+ markFinisherHit();
1638
+ } else if (eff.kind === 'pyroclasm') {
1639
+ // Self-aura: Hellfire/RoF ticks → chance stun 18093.
1640
+ // Cite: UnitAuraProcHandler.cpp:1347–1375.
1641
+ src.pyroclasmRankSpellId = eff.rankSpellId;
1642
+ src.buffActive.add(spell.id);
1643
+ effects.push({
1644
+ kind: 'buff',
1645
+ source: src.side,
1646
+ target: src.side,
1647
+ spellId: spell.id,
1648
+ name: spell.name,
1649
+ expiresAt: t + durFor(eff.durationMs),
1650
+ });
1651
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1652
+ markFinisherHit();
1653
+ } else if (eff.kind === 'speed') {
1654
+ // A raw speed aura. Route by "is the target someone else", NOT by enemy side: written from the
1655
+ // player's seat, a creature's snare fell through and the caster snared itself.
1656
+ const isHostileSpeed = isHostileSpeedEffect(eff);
1657
+ // dest-at-src fear/nova primary can be the caster — do not self-apply the victim speed.
1658
+ const skipHostileOnSelf = isHostileSpeed
1659
+ && (tgt === src || isEnemySide(src.side) === isEnemySide(tgt.side));
1660
+ const snareTgt = skipHostileOnSelf ? src
1661
+ : isHostileSpeed && tgt !== src ? tgt : src;
1662
+ if (!skipHostileOnSelf) {
1663
+ // Snare immunity (Free Action / template mask). Cite: SharedDefines MECHANIC_SNARE=11.
1664
+ const speedMech = isHostileSpeed
1665
+ ? ((eff as { mechanic?: string }).mechanic || spell.mechanic || 'snare')
1666
+ : undefined;
1667
+ if (isHostileSpeed && snareTgt !== src && (
1668
+ isTotemCreatureType(snareTgt.creatureType)
1669
+ || isTotemCreatureType(snareTgt.spec.creatureType)
1670
+ || isTotemSide(snareTgt.side)
1671
+ || isImmuneToMechanic(snareTgt.immunity, speedMech!, {
1672
+ schoolImmuneMask: snareTgt.spec.schoolImmuneMask,
1673
+ mechanicImmuneMask: snareTgt.spec.mechanicImmuneMask,
1674
+ })
1675
+ )) {
1676
+ consumeChargedImmunityOnMechanicBlock?.(snareTgt, speedMech!);
1677
+ push({ t, kind: 'cc_immune', target: snareTgt.side, control: 'snare', name: spell.name });
1678
+ return;
1679
+ }
1680
+ const dur = durFor(eff.durationMs);
1681
+ const exp = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
1682
+ const live = rowsOn(snareTgt).find(
1683
+ (e) => e.kind === 'speed' && e.spellId === spell.id && e.aura === eff.aura,
1684
+ );
1685
+ if (live) {
1686
+ live.expiresAt = exp; live.pct = eff.pct; live.hostile = isHostileSpeed; markSpeedDirty(snareTgt); touchEffect(live);
1687
+ if (state.heartbeat) { live.heartbeat = state.heartbeat; bumpEffectsVersion?.(); }
1688
+ } else {
1689
+ effects.push({
1690
+ kind: 'speed',
1691
+ source: src.side,
1692
+ target: snareTgt.side,
1693
+ spellId: spell.id,
1694
+ name: spell.name,
1695
+ aura: eff.aura,
1696
+ pct: eff.pct,
1697
+ hostile: isHostileSpeed,
1698
+ expiresAt: exp,
1699
+ ...(state.heartbeat ? { heartbeat: state.heartbeat } : {}),
1700
+ });
1701
+ }
1702
+ // Beneficial aspects / sprints show on the buff bar (not as debuffs).
1703
+ if (!isHostileSpeed) snareTgt.buffActive.add(spell.id);
1704
+ // The fade already logs `control: snare` (crowdControl.ts:936); without this the log carried
1705
+ // a Hamstring wearing off that nobody ever saw land.
1706
+ if (isHostileSpeed && !live) {
1707
+ push({ t, kind: 'control', target: snareTgt.side, control: 'snare', applied: true, name: spell.name });
1708
+ }
1709
+ push({ t, kind: 'aura', actor: snareTgt.side, name: spell.name, applied: true, spellId: spell.id });
1710
+ markFinisherHit();
1711
+ // Improved Sprint: a target trigger on a successful Sprint hit that removes root and snare.
1712
+ // Cite: Spell.cpp:7433–7468; SpellEffects.cpp:4140–4147; Unit.cpp:10562–10584.
1713
+ if (
1714
+ !isHostileSpeed
1715
+ && snareTgt === src
1716
+ && src.improvedSprintChancePct > 0
1717
+ && isSprintSpell(
1718
+ spell.id,
1719
+ spell.spellFamilyName,
1720
+ spell.spellFamilyFlags as number | undefined,
1721
+ )
1722
+ && improvedSprintShouldProc(src.improvedSprintChancePct, rng.next())
1723
+ ) {
1724
+ const trig =
1725
+ src.spec.spells.find((s) => s.id === IMPROVED_SPRINT_TRIGGER_SPELL_ID)
1726
+ ?? improvedSprintTriggerSpellDef();
1727
+ applySpell(src, src, trig);
1728
+ }
1729
+ }
1730
+ } else if (eff.kind === 'feather_fall') {
1731
+ // Slow Fall — register self aura (no movement sim in text combat).
1732
+ const dur = durFor(eff.durationMs);
1733
+ src.buffActive.add(spell.id);
1734
+ effects.push({
1735
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1736
+ expiresAt: dur > 0 ? t + dur : Number.POSITIVE_INFINITY,
1737
+ });
1738
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1739
+ markFinisherHit();
1740
+ } else if (eff.kind === 'hover') {
1741
+ // Levitate — SetHover / MOVEFLAG_HOVER. Cite: SpellAuras.cpp:1681–1688; Unit.cpp:12428.
1742
+ const dur = durFor(eff.durationMs);
1743
+ src.buffActive.add(spell.id);
1744
+ src.movementFlags = setHover(src.movementFlags ?? 0, true);
1745
+ effects.push({
1746
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1747
+ expiresAt: dur > 0 ? t + dur : Number.POSITIVE_INFINITY,
1748
+ });
1749
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1750
+ markFinisherHit();
1751
+ } else if (eff.kind === 'reflect') {
1752
+ // The spell-reflect self-buff, read back at incoming-magic time. A durationMs of 0 is
1753
+ // until-cancelled, so it lasts the fight, and a recast refreshes it.
1754
+ const dur = durFor(eff.durationMs);
1755
+ const exp = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
1756
+ const live = rowsOn(src).find(
1757
+ (e) => e.kind === 'reflect' && e.spellId === spell.id && e.school === eff.school,
1758
+ );
1759
+ if (live) { live.expiresAt = exp; live.pct = eff.pct; touchEffect(live); }
1760
+ else {
1761
+ effects.push({
1762
+ kind: 'reflect', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1763
+ school: eff.school, pct: eff.pct, expiresAt: exp,
1764
+ });
1765
+ }
1766
+ src.buffActive.add(spell.id);
1767
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1768
+ markFinisherHit();
1769
+ } else if (eff.kind === 'dispel') {
1770
+ // SPELL_EFFECT_DISPEL. NOTHING_TO_DISPEL pure gate upstream (Spell.cpp:4890–4919; KI-610/120).
1771
+ // Success → strip + spell_hit. Cite: SpellEffects.cpp:2577–2594 EvaluateResultLists.
1772
+ const res = resolveDispelTarget(src, tgt, eff.dispelType);
1773
+ const releaseDispelClaim = (unitSide: string): void => {
1774
+ if (!dispelClaim) return;
1775
+ const key = dispelClaimKey(unitSide, eff.dispelType);
1776
+ if (dispelClaim.get(key) === src.side) dispelClaim.delete(key);
1777
+ };
1778
+ if (res) {
1779
+ const { unit, friendlyDispel } = res;
1780
+ const mask = getDispelMask(eff.dispelType);
1781
+ const candidates: DispellableAura[] = (dispellableAurasOn(unit, mask, friendlyDispel) as ActiveEffect[]).map((e: ActiveEffect) => ({
1782
+ spellId: e.spellId, dispelType: dispelTypeOfSpell(e.spellId), stacks: e.stacks ?? 1,
1783
+ }));
1784
+ const { success } = processDispelList(candidates, eff.count, rng);
1785
+ const removed = new Map<number, number>();
1786
+ for (const s of success) removed.set(s.spellId, (removed.get(s.spellId) ?? 0) + 1);
1787
+ let stripped = 0;
1788
+ for (const [spellId, n] of removed) { stripAuraOnDispel(unit, spellId, n); stripped += n; }
1789
+ if (stripped > 0) {
1790
+ push({ t, kind: 'spell_hit', actor: src.side, target: unit.side, spellId: spell.id,
1791
+ name: spell.name, result: 'hit', amount: stripped, school: spell.school });
1792
+
1793
+ // Priest.cpp:80-86 SetHelpfulThreatCoefficient(0.5f).
1794
+ // Dispel Magic base threat rides applyHealThreat when friendly.
1795
+ if (friendlyDispel && (spell.id === 527 || spell.id === 988)) {
1796
+ applyHealThreat(src, 0, spell, unit.side);
1797
+ }
1798
+
1799
+ // On success dispel, Devour Magic casts the rank's heal on the pet. Intent first, table fallback.
1800
+ // Cite: SpellEffects.cpp:2596-2612; SpellMgr.h SPELLCATEGORY_DEVOUR_MAGIC=12.
1801
+ let healSpellId = DEVOUR_MAGIC_HEAL[spell.id | 0];
1802
+ if (healSpellId || ((spell.category ?? 0) | 0) === SPELLCATEGORY_DEVOUR_MAGIC) {
1803
+ const intents = resolveSpellEffects(spell, { forceInterpreter: true }).intents ?? [];
1804
+ for (const intent of intents) {
1805
+ if (intent.kind === 'devour_magic_heal') {
1806
+ healSpellId = intent.healSpellId;
1807
+ break;
1808
+ }
1809
+ }
1810
+ }
1811
+ if (healSpellId) {
1812
+ const healSpell = L.spellOf?.(healSpellId);
1813
+ if (healSpell) {
1814
+ applySpell(src, src, healSpell);
1815
+ }
1816
+ }
1817
+ }
1818
+ releaseDispelClaim(unit.side);
1819
+ } else {
1820
+ releaseDispelClaim(tgt.side);
1821
+ }
1822
+ markFinisherHit();
1823
+ } else if (eff.kind === 'dispel_mechanic') {
1824
+ // SPELL_EFFECT_DISPEL_MECHANIC — strip HasMechanic(misc) holders on unitTarget.
1825
+ // Cite: SpellEffects.cpp:2499–2519; SpellAuras.cpp:5786–5794.
1826
+ const order = (L.fightersOrder?.() ?? [src, tgt]) as Fighter[];
1827
+ const lookup = (id: number): SpellDef | undefined => {
1828
+ for (const f of order) {
1829
+ const sp = f.spec.spells.find((s) => s.id === id);
1830
+ if (sp) return sp;
1831
+ }
1832
+ return undefined;
1833
+ };
1834
+ const stripped = applyDispelMechanicOnUnit(
1835
+ tgt, eff.mechanic, eff.count, effects, lookup, rng,
1836
+ (spellId, n) => stripAuraOnDispel(tgt, spellId, n),
1837
+ );
1838
+ if (stripped > 0) {
1839
+ push({ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id,
1840
+ name: spell.name, result: 'hit', amount: stripped, school: spell.school });
1841
+ }
1842
+ markFinisherHit();
1843
+ } else if (eff.kind === 'mechanic_immunity_purge') {
1844
+ const purgeMechMask = eff.mechanicMask ?? 0;
1845
+ if (purgeMechMask > 0) {
1846
+ const dropIds = new Set<number>();
1847
+ for (const e of rowsOn(tgt)) {
1848
+ if (e.expiresAt <= t || e.broken) continue;
1849
+ // The spell applies the mask bit or the kind 'root'/'cc' defines it.
1850
+ // Since CC maps to mechanics directly, check the kind.
1851
+ if ((mechanicMaskOfLiveEffect(e) & purgeMechMask) !== 0) {
1852
+ dropIds.add(e.spellId);
1853
+ }
1854
+ }
1855
+ for (const spellId of dropIds) L.removeAurasDueToSpell?.(tgt, spellId);
1856
+ }
1857
+ } else if (eff.kind === 'combo_points') {
1858
+ addComboPoints(src, tgt, eff.amount); // Cite: EffectAddComboPoints.
1859
+ markFinisherHit();
1860
+ } else if (eff.kind === 'retain_combo_points') {
1861
+ // Self-aura expire strip CP. Cite: SpellAuras.cpp:4436–4452.
1862
+ const dur = durFor(eff.durationMs);
1863
+ const expAt = dur > 0 ? t + dur : Number.POSITIVE_INFINITY;
1864
+ const live = rowsOn(src).find(
1865
+ (e) => e.kind === 'buff' && e.spellId === spell.id && e.retainComboAmount != null,
1866
+ );
1867
+ if (live) {
1868
+ live.expiresAt = expAt;
1869
+ live.retainComboAmount = eff.amount;
1870
+ live.amount = eff.amount;
1871
+ touchEffect(live);
1872
+ } else {
1873
+ effects.push({
1874
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1875
+ expiresAt: expAt, retainComboAmount: eff.amount, amount: eff.amount,
1876
+ });
1877
+ src.buffActive.add(spell.id);
1878
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1879
+ }
1880
+ markFinisherHit();
1881
+ } else if (eff.kind === 'lightwell') {
1882
+ // Place the well (Lightwell 724). It reactively renews the most-hurt friendly each interval
1883
+ // until its charges run out or it expires. Cite: Priest.cpp Lightwell / Renew 7001.
1884
+ src.lightwell = {
1885
+ charges: eff.charges, healPerCharge: eff.healPerCharge, intervalMs: eff.intervalMs,
1886
+ expiresAt: t + durFor(eff.durationMs), nextHealAt: t, spellId: spell.id,
1887
+ };
1888
+ src.buffActive.add(spell.id);
1889
+ effects.push({ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name, expiresAt: t + durFor(eff.durationMs) });
1890
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1891
+ markFinisherHit();
1892
+ } else if (eff.kind === 'demonic_sacrifice') {
1893
+ // Cite: Warlock.cpp:214–234; applyDemonicSacrifice.ts; spell_template 18789–18792.
1894
+ applyDemonicSacrificeEffect({ src, pet, spell, eff, effects, t, durFor, dead, push });
1895
+ markFinisherHit();
1896
+ } else if (eff.kind === 'next_cast_mod') {
1897
+ // Arm a one-shot modifier the caster's next matching cast consumes, replacing any already
1898
+ // armed. A display aura carries the optional timeout; the cast site consumes it early.
1899
+ if (src.pendingSpellMod) consumePendingSpellMod(src);
1900
+ // One builder for every arming site, so the DBC 107/108 rows ride along and the mask —
1901
+ // not the appliesTo shorthand — decides what consumes this. Cite: roles.ts.
1902
+ src.pendingSpellMod = pendingSpellModFromResolved(spell) ?? {
1903
+ spellId: spell.id, name: spell.name, appliesTo: eff.appliesTo,
1904
+ critAdd: eff.critAdd, instant: !!eff.instant, freeCost: !!eff.freeCost,
1905
+ // costModPct = SPELLMOD_COST; never SpellDef.costPct (ManaCostPercentage).
1906
+ costModPct: eff.costModPct ?? eff.costPct, effectPct: eff.effectPct,
1907
+ castTimeFlat: eff.castTimeFlat,
1908
+ spellFamilyName: spell.spellFamilyName,
1909
+ familyFlags: eff.familyFlags,
1910
+ };
1911
+ const dur = eff.durationMs > 0 ? eff.durationMs : 600_000; // 0 = until consumed
1912
+ src.buffActive.add(spell.id);
1913
+ const charges = ((spell.procCharges ?? 0) > 0) ? spell.procCharges : 1;
1914
+ effects.push({
1915
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1916
+ expiresAt: t + dur, charges,
1917
+ });
1918
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1919
+ markFinisherHit();
1920
+ } else if (eff.kind === 'owned_spell_mods') {
1921
+ // A lasting flat or percent modifier. Sibling buffs hold the display duration, and a holder is
1922
+ // still added when none exists. Cite: SpellAuras.cpp HandleAddModifier; Player.cpp AddSpellMod.
1923
+ addOwnedSpellMods(src, eff.mods.map((m) => ({
1924
+ spellId: spell.id,
1925
+ op: m.op,
1926
+ type: m.type,
1927
+ value: m.amount,
1928
+ familyName: spell.spellFamilyName,
1929
+ familyFlags: m.familyFlags,
1930
+ })));
1931
+ const dur = eff.durationMs > 0 ? eff.durationMs : 0;
1932
+ src.buffActive.add(spell.id);
1933
+ if (dur > 0 && !rowsOn(src).some((e) => e.kind === 'buff' && e.spellId === spell.id)) {
1934
+ effects.push({
1935
+ kind: 'buff', source: src.side, target: src.side, spellId: spell.id, name: spell.name,
1936
+ expiresAt: t + dur,
1937
+ });
1938
+ }
1939
+ push({ t, kind: 'aura', actor: src.side, name: spell.name, applied: true });
1940
+ markFinisherHit();
1941
+ } else if (eff.kind === 'pickpocket') {
1942
+ // PICKPOCKET (71): grant the loot and gold in-fight, down the same path as the OOC verb.
1943
+ // Vanilla does NOT break stealth. CheckCast gates on PickpocketLootId alone, which the
1944
+ // grant below reads — a creature-type test here refused the 253 undead, demon and totem
1945
+ // tables the bake carries. Cite: SpellEffects.cpp:2662–2706; Spell.cpp:5501-5511; KI-612.
1946
+ {
1947
+ const entry = (tgt.spec as { creatureEntry?: number }).creatureEntry
1948
+ ?? (tgt as { creatureEntry?: number }).creatureEntry
1949
+ ?? 0;
1950
+ const granted = entry > 0 && cfg?.onPickPocket
1951
+ ? cfg.onPickPocket(entry)
1952
+ : { ok: false as const, reason: 'no pickpocket host' };
1953
+ const dropN = (granted.drops ?? []).reduce((n: number, d: { count?: number }) => n + (d.count || 0), 0);
1954
+ const gold = granted.goldCopper ?? 0;
1955
+ if (granted.ok) {
1956
+ const bits: string[] = [];
1957
+ if (dropN > 0) bits.push(`${dropN} item(s)`);
1958
+ if (gold > 0) bits.push(moneyMarked(gold));
1959
+ push({
1960
+ t, kind: 'flavor', actor: src.side, spellId: spell.id,
1961
+ text: bits.length
1962
+ ? `${src.spec.name} picks ${tgt.spec.name}'s pocket (${bits.join(', ')}).`
1963
+ : `${src.spec.name} picks ${tgt.spec.name}'s pocket.`,
1964
+ });
1965
+ } else {
1966
+ push({
1967
+ t, kind: 'flavor', actor: src.side, spellId: spell.id,
1968
+ text: granted.reason
1969
+ ? `${spell.name} fails - ${granted.reason}.`
1970
+ : `${src.spec.name} picks ${tgt.spec.name}'s pocket.`,
1971
+ });
1972
+ }
1973
+ push({
1974
+ t, kind: 'spell_hit', actor: src.side, target: tgt.side, spellId: spell.id, name: spell.name,
1975
+ result: 'hit', amount: gold > 0 ? gold : dropN, school: spell.school,
1976
+ });
1977
+ }
1978
+ markFinisherHit();
1979
+ } else if (eff.kind === 'knock_back') {
1980
+ // SPELL_EFFECT_KNOCK_BACK (98). Materialized speeds → event + geometry push.
1981
+ // Cite: SpellEffects.cpp:5113–5121; combat/knockBack.ts; Unit.cpp KnockBackFrom.
1982
+ applyKnockBackEffect({
1983
+ t,
1984
+ spell,
1985
+ actor: src.side,
1986
+ target: tgt.side,
1987
+ horizontal: eff.horizontal,
1988
+ vertical: eff.vertical,
1989
+ now: t,
1990
+ targetRootedUntil: tgt.rootedUntil,
1991
+ posOf,
1992
+ setPos,
1993
+ displaced,
1994
+ clampLanding: (from, dest) => clampDisplacementToFloor(from, dest, displaceFloorFromCfg(cfg)),
1995
+ push,
1996
+ });
1997
+ markFinisherHit();
1998
+ } else if (eff.kind === 'pull_towards') {
1999
+ applyKnockBackEffect({
2000
+ t,
2001
+ spell,
2002
+ actor: src.side,
2003
+ target: tgt.side,
2004
+ horizontal: eff.speedXY,
2005
+ vertical: eff.speedZ ?? 0,
2006
+ now: t,
2007
+ targetRootedUntil: tgt.rootedUntil,
2008
+ posOf,
2009
+ setPos,
2010
+ displaced,
2011
+ clampLanding: (from, dest) => clampDisplacementToFloor(from, dest, displaceFloorFromCfg(cfg)),
2012
+ push,
2013
+ });
2014
+ markFinisherHit();
2015
+ } else if (eff.kind === 'destroy_all_totems') {
2016
+ // SPELL_EFFECT_DESTROY_ALL_TOTEMS (110) takes the CASTER's own four seats, never the party's.
2017
+ // Cite: SpellEffects.cpp:5204–5206 `m_caster->GetTotem(TotemSlot(slot))`.
2018
+ for (const tot of [...totems.values()]) {
2019
+ if ((tot.totemOwnerSide ?? 'player') !== src.side) continue;
2020
+ unsummonTotem(tot.side as TotemSide, 'owner');
2021
+ }
2022
+ }
2023
+
2024
+
2025
+ }
2026
+