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,2220 @@
1
+ // Peeled from createSession (Wave 4 S32 eventAi hub). Pattern (D).
2
+ // Uses local aliases so moved bodies stay nearly verbatim (determinism-safe).
3
+
4
+ import type { CombatContext } from './context.ts';
5
+ import type { Fighter } from './types.ts';
6
+ import { isAllySide, isEnemySide, type Side, type EnemySide } from '../core/events.ts';
7
+ import type { EventAiIntent } from '../../domain/creature/creatureAi.ts';
8
+ import { AURA_INTERRUPT } from '../../domain/spells/stealth.ts';
9
+ import {
10
+ AI_EVENT_TYPE,
11
+ AIEVENT_DEFAULT_THROW_RADIUS_YD,
12
+ CAST_FLAG,
13
+ EVENT_T,
14
+ markEventAiUsed,
15
+ processEventAiActions,
16
+ processThrowAiEventDamageTaken,
17
+ processThrowAiEventHealedBy,
18
+ pulseEventAi,
19
+ resolveCallForHelpRadius,
20
+ rollEventAiChance,
21
+ shouldThrowAiEventJustDied,
22
+ stepEventAi,
23
+ } from '../../domain/creature/creatureAi.ts';
24
+ import {
25
+ collectReachCastsDue,
26
+ dataGatePasses,
27
+ handleReceiveAiEvent,
28
+ handleSpellHit,
29
+ tickDieTimer,
30
+ hasTrackedSummonSides,
31
+ noteJustSummonedSide,
32
+ planNonCharmAuraOnApply,
33
+ scriptDevByCreature,
34
+ tickScriptDev,
35
+ noteScriptDevHitCount,
36
+ lifecycleActions,
37
+ } from '../../domain/scriptdev/index.ts';
38
+ import { runEvadeLifecycle } from './evadeLifecycle.ts';
39
+ import { resetCreatureCastSlotsOnEvade, rearmCreatureCastSlotsOnPhase } from '../../domain/creature/creatureCastList.ts';
40
+ import type { ScriptDevAction, ScriptDevRuntimeState } from '../../domain/scriptdev/types.ts';
41
+ import { formatEventAiChatLine, lookupEventAiText } from '../../domain/creature/eventAiTexts.ts';
42
+ import { INSTANCE_DATA } from '../../domain/scriptdev/types.ts';
43
+ import {
44
+ scriptTargetCreatureEntries,
45
+ scriptTargetPickMode,
46
+ } from '../spells/scriptTargets.ts';
47
+ import {
48
+ FLAMELASH_SPIRIT_DESPAWN_MS,
49
+ FLAMELASH_SPIRIT_REACH_YD,
50
+ NPC_AMBASSADOR_FLAMELASH,
51
+ NPC_BURNING_SPIRIT,
52
+ SPELL_BURNING_SPIRIT,
53
+ } from '../../domain/scriptdev/blackrockDepths.ts';
54
+ import { REACT_STATE } from '../../domain/creature/creatureAiConstants.ts';
55
+ import {
56
+ NPC_STONESKIN_GARGOYLE_ENTRY,
57
+ allowStoneskinGargoyleStoneForm,
58
+ } from '../../domain/scriptdev/naxxramas.ts';
59
+ import { navSightClear } from './navSight.ts';
60
+ import { maxPowerOfType, powerOfType, setPowerOfType } from './powerPools.ts';
61
+ import { planCastSummons } from './summonCast.ts';
62
+ import { durMs } from '../spells/interpreter/duration.ts';
63
+ import { EVENT_AI_TARGET_SELF } from '../../domain/creature/creatureAiConstants.ts';
64
+ import { pulsePartyCombat } from './combatFlag.ts';
65
+ import { emptySlowDiag, slowThinkDue, wakeForDecision, SLOW_IDLE_RATE } from './slowUpdate.ts';
66
+ import { UNIT_FLAG } from '../../domain/creature/unitFlags.ts';
67
+ import { emitSimEffect } from './simEffects.ts';
68
+
69
+ // Percent-mirror damage onto the twin partner. twinDamage = floor(damage / selfMax * partnerMax). Cite:
70
+ // boss_twinemperors.cpp:103-108.
71
+ export function mirrorDamagePercent(
72
+ damage: number,
73
+ selfMaxHealth: number,
74
+ partnerMaxHealth: number,
75
+ ): number {
76
+ const selfMax = Math.max(1, selfMaxHealth | 0);
77
+ const partnerMax = Math.max(1, partnerMaxHealth | 0);
78
+ return Math.floor((damage / selfMax) * partnerMax);
79
+ }
80
+
81
+ /** Percent-mirror heal onto the twin partner. Cite: boss_twinemperors.cpp:120-123. */
82
+ export function mirrorHealPercent(
83
+ healAmount: number,
84
+ selfMaxHealth: number,
85
+ partnerMaxHealth: number,
86
+ ): number {
87
+ const selfMax = Math.max(1, selfMaxHealth | 0);
88
+ const partnerMax = Math.max(1, partnerMaxHealth | 0);
89
+ return Math.floor((healAmount / selfMax) * partnerMax);
90
+ }
91
+
92
+ export type EventAiDeps = {
93
+ injectEnemySummon: (
94
+ summoner: Fighter,
95
+ creatureId: number,
96
+ durationMs: number,
97
+ worldPos?: { x: number; y: number },
98
+ opts?: { attackStart?: boolean },
99
+ ) => Fighter | null;
100
+ forceDespawnEnemy: (ef: Fighter) => void;
101
+ resolveEventAiTargetFighter: (summoner: Fighter, targetSel: number) => Fighter;
102
+ removeAurasDueToSpell: (unit: Fighter, spellId: number) => number;
103
+ dead: (f: Fighter) => boolean;
104
+ hostileTargetOf: (enemyF: Fighter) => Fighter;
105
+ breakCharm: (f: Fighter) => void;
106
+ };
107
+
108
+ export function installEventAi(ctx: CombatContext, deps: EventAiDeps): void {
109
+ // Aliases — same bindings createSession closures used (maps/arrays by reference).
110
+ const push = ctx.push.bind(ctx);
111
+ const enemyCastBySide = ctx.enemyCastBySide;
112
+ const player = ctx.player;
113
+ const instanceData = ctx.instanceData;
114
+ const guardianSidesBySummoner = ctx.guardianSidesBySummoner;
115
+ const distanceBetween = ctx.distanceBetween;
116
+ const posOf = ctx.posOf;
117
+ const threatOf = ctx.threatOf;
118
+ const livingHostilesFor = ctx.livingHostilesFor;
119
+ const bySide = ctx.bySide;
120
+ /** Mandokir player-kill ding. Cite: boss_mandokir.cpp:190 m_uiKillCount. */
121
+ const mandokirKillCount = new WeakMap<Fighter, number>();
122
+ const isCharmed = ctx.isCharmed;
123
+ const rng = ctx.rng;
124
+ const tickMs = ctx.tickMs;
125
+ const cfg = ctx.cfg;
126
+ const injectEnemySummon = deps.injectEnemySummon;
127
+ const forceDespawnEnemy = deps.forceDespawnEnemy;
128
+ const resolveEventAiTargetFighter = deps.resolveEventAiTargetFighter;
129
+ const removeAurasDueToSpell = deps.removeAurasDueToSpell;
130
+ const dead = deps.dead;
131
+ const hostileTargetOf = deps.hostileTargetOf;
132
+ const breakCharm = deps.breakCharm;
133
+
134
+ // The bodies below read `t` as a free variable, so each of the three functions takes a local copy of
135
+ // ctx.t at its start rather than closing over a stale binding.
136
+
137
+ /** Find living enemy with creatureEntry. */
138
+ const findPartner = (entry: number): Fighter | null => {
139
+ for (const e of ctx.livingEnemies()) {
140
+ if (((e.spec.creatureEntry ?? 0) | 0) === entry) return e;
141
+ }
142
+ return null;
143
+ };
144
+
145
+ // DamageTaken mirror. Installed lazily over ctx.onDamaged (crowdControl assigns it after us). Cite:
146
+ // boss_twinemperors.cpp:96-109.
147
+ const applyDamageHealMirrorDamage = (
148
+ victim: Fighter,
149
+ damage: number,
150
+ fromSpellId?: number,
151
+ ): void => {
152
+ if (damage <= 0 || dead(victim) || !isEnemySide(victim.side)) return;
153
+ const cs = enemyCastBySide.get(victim.side as EnemySide);
154
+ const mirror = cs?.scriptDev?.ir.damageHealMirror;
155
+ if (!mirror) return;
156
+ if (fromSpellId === mirror.empathySpellId) return;
157
+ const partner = findPartner(mirror.partnerEntry | 0);
158
+ if (!partner) return;
159
+ const twinDamage = mirrorDamagePercent(damage, victim.spec.maxHealth, partner.spec.maxHealth);
160
+ if (twinDamage <= 0) return;
161
+ // Apply the scripted percent as a flat HP packet, not a second mitigation pass — DamageTaken
162
+ // already saw the post-mitigation number. The fallback schools cover a stripped bake.
163
+ const twinSpell = (id: number) =>
164
+ victim.spec.spells.find((s) => s.id === id)
165
+ ?? partner.spec.spells.find((s) => s.id === id);
166
+ if (partner.hp <= twinDamage) {
167
+ partner.hp = 0;
168
+ const suicide = twinSpell(mirror.suicideSpellId);
169
+ push({
170
+ t: ctx.t,
171
+ kind: 'spell_hit',
172
+ actor: victim.side,
173
+ target: partner.side,
174
+ spellId: mirror.suicideSpellId,
175
+ name: suicide?.name ?? 'Suicide',
176
+ result: 'hit',
177
+ amount: twinDamage,
178
+ school: suicide?.school ?? 'physical',
179
+ });
180
+ return;
181
+ }
182
+ partner.hp = Math.max(0, Math.round(partner.hp - twinDamage));
183
+ const empathy = twinSpell(mirror.empathySpellId);
184
+ push({
185
+ t: ctx.t,
186
+ kind: 'spell_hit',
187
+ actor: victim.side,
188
+ target: partner.side,
189
+ spellId: mirror.empathySpellId,
190
+ name: empathy?.name ?? 'Twin Empathy',
191
+ result: 'hit',
192
+ amount: twinDamage,
193
+ school: empathy?.school ?? 'holy',
194
+ });
195
+ };
196
+
197
+ // HealedBy mirror. Amount is DealHeal addhealth (pre-clamp). Cite: Unit.cpp:6889;
198
+ // boss_twinemperors.cpp:113-125.
199
+ const applyDamageHealMirrorHeal = (
200
+ healed: Fighter,
201
+ healAmount: number,
202
+ _fromSpellId?: number,
203
+ ): void => {
204
+ if (healAmount <= 0 || dead(healed) || !isEnemySide(healed.side)) return;
205
+ const cs = enemyCastBySide.get(healed.side as EnemySide);
206
+ const mirror = cs?.scriptDev?.ir.damageHealMirror;
207
+ if (!mirror) return;
208
+ const partner = findPartner(mirror.partnerEntry | 0);
209
+ if (!partner || dead(partner)) return;
210
+ const twinHeal = mirrorHealPercent(healAmount, healed.spec.maxHealth, partner.spec.maxHealth);
211
+ if (twinHeal <= 0) return;
212
+ const before = partner.hp;
213
+ partner.hp = Math.min(partner.spec.maxHealth, Math.round(partner.hp + twinHeal));
214
+ const gained = partner.hp - before;
215
+ if (gained > 0) {
216
+ push({ t: ctx.t, kind: 'heal', actor: partner.side, amount: gained, source: healed.side });
217
+ }
218
+ };
219
+
220
+ let mirrorHooksInstalled = false;
221
+ const ensureMirrorHooks = (): void => {
222
+ if (mirrorHooksInstalled) return;
223
+ mirrorHooksInstalled = true;
224
+ const prevDamaged = ctx.onDamaged;
225
+ if (prevDamaged) {
226
+ ctx.onDamaged = (f, amount, damageType, fromSpellId, info) => {
227
+ prevDamaged(f, amount, damageType, fromSpellId, info);
228
+ applyDamageHealMirrorDamage(f, amount, fromSpellId);
229
+ };
230
+ } else {
231
+ ctx.onDamaged = (f, amount, _damageType, fromSpellId) => {
232
+ applyDamageHealMirrorDamage(f, amount, fromSpellId);
233
+ };
234
+ }
235
+ const prevHealed = ctx.onHealed;
236
+ if (prevHealed) {
237
+ ctx.onHealed = (f, amount, fromSpellId) => {
238
+ prevHealed(f, amount, fromSpellId);
239
+ applyDamageHealMirrorHeal(f, amount, fromSpellId);
240
+ };
241
+ } else {
242
+ ctx.onHealed = (f, amount, fromSpellId) => {
243
+ applyDamageHealMirrorHeal(f, amount, fromSpellId);
244
+ };
245
+ }
246
+ };
247
+
248
+ // MoveInLineOfSight Heal Brother within 60 yd while the encounter is in progress. Gated only on a
249
+ // live GCD or an in-progress cast, so the cast-if-can style failures still apply.
250
+ const maybeCastHealBrother = (ef: Fighter): void => {
251
+ const cs = enemyCastBySide.get(ef.side as EnemySide);
252
+ const mirror = cs?.scriptDev?.ir.damageHealMirror;
253
+ if (!mirror?.healBrotherSpellId || mirror.healBrotherRangeYd == null) return;
254
+ if (mirror.encounterField != null) {
255
+ if ((instanceData.get(mirror.encounterField) ?? 0) !== INSTANCE_DATA.IN_PROGRESS) return;
256
+ }
257
+ const t = ctx.t;
258
+ if (ef.gcdUntil > t) return;
259
+ if (ef.castingSpell != null || ef.channeling) return;
260
+ const partner = findPartner(mirror.partnerEntry | 0);
261
+ if (!partner) return;
262
+ const dist = distanceBetween(ef, partner);
263
+ if (dist == null || dist > mirror.healBrotherRangeYd) return;
264
+ const spell =
265
+ ef.spec.spells.find((s) => s.id === mirror.healBrotherSpellId)
266
+ ?? partner.spec.spells.find((s) => s.id === mirror.healBrotherSpellId);
267
+ if (!spell || !ctx.applySpell) return;
268
+ ctx.applySpell(ef, partner, spell);
269
+ };
270
+
271
+ /** Apply one EventAI intent (phase already mutated in pure layer for set_phase/flee). */
272
+ const applyEventAiIntent = (summoner: Fighter, intent: EventAiIntent): void => {
273
+ const t = ctx.t;
274
+ const castState = enemyCastBySide.get(summoner.side as EnemySide);
275
+ switch (intent.kind) {
276
+ case 'set_phase':
277
+ push({ t, kind: 'phase', actor: summoner.side, phase: intent.phase });
278
+ break;
279
+ case 'flee':
280
+ push({ t, kind: 'flee', actor: summoner.side });
281
+ break;
282
+ case 'start_relay':
283
+ // ScriptsStart for a relay. 151 of the 158 DB rows name a relay id directly and all of them
284
+ // exist; the 7 template rows cannot resolve without that table, so they are reported.
285
+ if (intent.relayId > 0) {
286
+ push({ t, kind: 'relay_script', actor: summoner.side, relayId: intent.relayId });
287
+ emitSimEffect(ctx, { kind: 'relayScript', relayId: intent.relayId, actor: summoner.side });
288
+ } else {
289
+ push({
290
+ t,
291
+ kind: 'flavor',
292
+ text: `${summoner.spec.name} relay template ${intent.templateId} unresolved (dbscript_random_templates unbaked).`,
293
+ });
294
+ }
295
+ break;
296
+ case 'killed_monster':
297
+ // Credit a creature ENTRY, not a quest id: 12296 credits 12297, 12298 credits 12299.
298
+ // Cite: CreatureEventAI.cpp ProcessAction case 33.
299
+ if (intent.creatureId > 0) {
300
+ push({ t, kind: 'kill_credit', actor: summoner.side, creatureEntry: intent.creatureId });
301
+ emitSimEffect(ctx, { kind: 'killCredit', creatureEntry: intent.creatureId });
302
+ }
303
+ break;
304
+ case 'pause_waypoints':
305
+ // The engine has no waypoint walker for an enemy, so pausing one is expressed as the
306
+ // thing it means on the floor: stop moving. Cite: CreatureEventAI.h:142.
307
+ if (castState) castState.eventAi.waypointsPaused = intent.pause;
308
+ push({
309
+ t,
310
+ kind: 'flavor',
311
+ text: `${summoner.spec.name} ${intent.pause ? 'stops' : 'resumes'} patrolling.`,
312
+ });
313
+ break;
314
+ case 'set_facing':
315
+ // P11's `facing` scalar is exactly what this action writes. reset = drop the override
316
+ // and go back to deriving it; otherwise face the current victim.
317
+ {
318
+ const oldFacing = summoner.facing;
319
+ if (intent.reset) {
320
+ summoner.facing = null;
321
+ } else {
322
+ const victim = hostileTargetOf(summoner);
323
+ const heading = victim !== summoner ? ctx.facingOf?.(summoner) : undefined;
324
+ summoner.facing = heading ?? summoner.facing;
325
+ }
326
+ if (summoner.facing !== oldFacing) {
327
+ ctx.removeAurasWithInterruptFlags?.(summoner, AURA_INTERRUPT.TURNING, 'turning');
328
+ }
329
+ }
330
+ break;
331
+ case 'spawn':
332
+ if (intent.creatureId > 0) {
333
+ injectEnemySummon(summoner, intent.creatureId, intent.durationMs);
334
+ }
335
+ break;
336
+ case 'summon_id':
337
+ // creature_ai_summons carries the real spawn point — pass it through, or the add
338
+ // lands on the summoner's feet (Object.cpp's zeroed-position branch).
339
+ if (intent.creatureId > 0) {
340
+ injectEnemySummon(
341
+ summoner,
342
+ intent.creatureId,
343
+ 0,
344
+ intent.placement ? { x: intent.placement.x, y: intent.placement.y } : undefined,
345
+ // TARGET_T_SELF summons and does NOT AttackStart. Celebras the Redeemed (13716,
346
+ // faction 35) opened on the party and held the pull. Cite: CreatureEventAI.cpp:1023.
347
+ { attackStart: (intent.target | 0) !== EVENT_AI_TARGET_SELF },
348
+ );
349
+ }
350
+ break;
351
+ case 'cast':
352
+ // CAST intents are handled by the dedicated cast-slot scheduler.
353
+ break;
354
+ case 'text':
355
+ if (intent.unresolved || !intent.text) break;
356
+ push({
357
+ t,
358
+ kind: 'creature_text',
359
+ actor: summoner.side,
360
+ textId: intent.textId,
361
+ chatType: intent.chatType,
362
+ text: intent.text,
363
+ });
364
+ break;
365
+ case 'emote':
366
+ // SpellDefines.h:367, ChatHandler.cpp:713
367
+ ctx.removeAurasWithInterruptFlags?.(summoner, AURA_INTERRUPT.ANIM_CANCELS, 'anim cancels');
368
+ push({ t, kind: 'emote', actor: summoner.side, emoteId: intent.emoteId });
369
+ // Feed line for combat log readability.
370
+ push({ t, kind: 'flavor', text: `${summoner.spec.name} emotes (${intent.emoteId}).` });
371
+ break;
372
+ case 'sound':
373
+ // playSfx when UI maps the soundId; event always recorded for observability.
374
+ push({ t, kind: 'sound', actor: summoner.side, soundId: intent.soundId });
375
+ break;
376
+ case 'set_ranged_mode':
377
+ // SetRangedMode keeps distance and stops white melee while the kit is ranged — the main
378
+ // spell is the attack, and stacking main-hand whites on top is the double-dip bug.
379
+ if (intent.enabled) {
380
+ summoner.mode = 'ranged';
381
+ summoner.autoAttackEnabled = false;
382
+ if (castState) castState.eventAi.meleeEnabled = false;
383
+ } else {
384
+ summoner.mode = 'melee';
385
+ summoner.autoAttackEnabled = true;
386
+ if (castState) castState.eventAi.meleeEnabled = true;
387
+ }
388
+ push({
389
+ t,
390
+ kind: 'ranged_mode',
391
+ actor: summoner.side,
392
+ enabled: intent.enabled,
393
+ chaseDistance: intent.chaseDistance,
394
+ });
395
+ break;
396
+ case 'auto_attack':
397
+ summoner.autoAttackEnabled = intent.enabled;
398
+ if (castState) castState.eventAi.meleeEnabled = intent.enabled;
399
+ push({ t, kind: 'auto_attack', actor: summoner.side, enabled: intent.enabled });
400
+ break;
401
+ case 'remove_auras_from_spell': {
402
+ if (!intent.spellId) break;
403
+ const tgt = resolveEventAiTargetFighter(summoner, intent.target);
404
+ removeAurasDueToSpell(tgt, intent.spellId);
405
+ push({
406
+ t,
407
+ kind: 'remove_auras',
408
+ actor: summoner.side,
409
+ target: tgt.side,
410
+ spellId: intent.spellId,
411
+ });
412
+ break;
413
+ }
414
+ case 'call_for_help': {
415
+ // DoCallForHelp engages nearby unengaged helpers within radius. A param of 0 means the family
416
+ // assistance radius, NOT the whole session, and line of sight is required.
417
+ if (castState?.eventAi.alreadyCallAssistance) break;
418
+ const radius = resolveCallForHelpRadius(intent.radius);
419
+ push({ t, kind: 'call_for_help', actor: summoner.side, radius });
420
+ if (castState) castState.eventAi.alreadyCallAssistance = true;
421
+ const from = posOf?.(summoner.side);
422
+ const acq = cfg.acquireRangeYd;
423
+ for (const other of ctx.livingEnemies()) {
424
+ if (other === summoner) continue;
425
+ // CanAssistTo: already in combat → skip (hasEnemy path).
426
+ const tm = threatOf(other);
427
+ if (!tm || !tm.isEmpty()) continue;
428
+ const d = distanceBetween(summoner, other);
429
+ if (d == null) {
430
+ // Endless floor always has unit yards for injects — missing d is not "pack".
431
+ // Non-endless multi-enemy without geometry: legacy co-session pack assist.
432
+ if (cfg.endless) continue;
433
+ } else if (d > radius) {
434
+ continue;
435
+ }
436
+ // Endless local combat: a helper must also sit inside the player's acquire range. The
437
+ // caller-centric radius alone pulls a 100 yd passive when a boss calls with r=100.
438
+ if (acq != null && cfg.endless) {
439
+ const toPlayer = distanceBetween(other, player);
440
+ if (toPlayer != null && toPlayer > acq) continue;
441
+ }
442
+ if (cfg.navLos && from) {
443
+ const at = posOf?.(other.side);
444
+ if (at && !navSightClear(cfg, from, at)) continue;
445
+ }
446
+ const otherCast = enemyCastBySide.get(other.side as EnemySide);
447
+ if (otherCast) otherCast.eventAi.alreadyCallAssistance = true;
448
+ tm.addThreatDirectly('player', 1, false);
449
+ for (const a of ctx.livingAllies()) tm.addThreatDirectly(a.side, 1, false);
450
+ if (ctx.pet && !dead(ctx.pet)) tm.addThreatDirectly('pet', 1, false);
451
+ wakeForDecision(other);
452
+ }
453
+ break;
454
+ }
455
+ case 'force_despawn': {
456
+ // delay already applied to state.forceDespawnAt in pure layer.
457
+ if (intent.delayMs <= 0) {
458
+ forceDespawnEnemy(summoner);
459
+ }
460
+ // Delayed path: tickEventAiForceDespawn below when forceDespawnAt elapses.
461
+ break;
462
+ }
463
+ case 'set_stand_state':
464
+ // SpellDefines.h:367, MiscHandler.cpp:413
465
+ ctx.removeAurasWithInterruptFlags?.(summoner, AURA_INTERRUPT.ANIM_CANCELS, 'anim cancels');
466
+ push({ t, kind: 'stand_state', actor: summoner.side, standState: intent.standState });
467
+ break;
468
+ case 'set_inst_data':
469
+ instanceData.set(intent.field | 0, intent.value | 0);
470
+ push({ t, kind: 'instance_data', field: intent.field | 0, value: intent.value | 0 });
471
+ // Persist like the death-bound write (sessionDeaths.ts:233): an ACID SetData outlives
472
+ // the pull. Without this ZG's Vilebranch Speaker set TYPE_OHGAN and the run forgot.
473
+ emitSimEffect(ctx, { kind: 'instanceData', store: new Map(instanceData), openDoors: [] });
474
+ break;
475
+ case 'set_death_prevention':
476
+ push({
477
+ t,
478
+ kind: 'death_prevention',
479
+ actor: summoner.side,
480
+ enabled: intent.enabled,
481
+ });
482
+ break;
483
+ case 'update_template':
484
+ if (intent.creatureId > 0) {
485
+ // Keep combatantEntry on the fighter for observability; full re-stat is S9+ territory.
486
+ (summoner.spec as { creatureEntry?: number }).creatureEntry = intent.creatureId;
487
+ push({
488
+ t,
489
+ kind: 'update_template',
490
+ actor: summoner.side,
491
+ creatureId: intent.creatureId,
492
+ });
493
+ }
494
+ break;
495
+ case 'set_follow_movement':
496
+ push({
497
+ t,
498
+ kind: 'follow_movement',
499
+ actor: summoner.side,
500
+ enabled: intent.enabled,
501
+ });
502
+ break;
503
+ case 'attack_start': {
504
+ const tgt = resolveEventAiTargetFighter(summoner, intent.target);
505
+ const tm = threatOf(summoner);
506
+ if (tm) tm.addThreatDirectly(tgt.side, 1, false);
507
+ if (castState && castState.eventAi.reactState === 0) {
508
+ // Leave passive when forced to AttackStart.
509
+ castState.eventAi.reactState = 2;
510
+ }
511
+ push({ t, kind: 'attack_start', actor: summoner.side, target: tgt.side });
512
+ break;
513
+ }
514
+ case 'throw_ai_event': {
515
+ // SendAIEventAround pulses the receive event on living enemies within radius. The C++ fires
516
+ // only when the radius is above 0 — a radius of 0 is a no-op, not the whole session.
517
+ sendAiEventAround(summoner, intent.eventType, intent.radius);
518
+ break;
519
+ }
520
+ case 'set_throw_mask':
521
+ // State already mutated in ACTION_HANDLERS; log for combat feed / repro.
522
+ push({
523
+ t,
524
+ kind: 'flavor',
525
+ text: `${summoner.spec.name} set throw AI event mask ${intent.mask}.`,
526
+ });
527
+ break;
528
+ case 'set_react_state':
529
+ push({
530
+ t,
531
+ kind: 'react_state',
532
+ actor: summoner.side,
533
+ reactState: intent.reactState,
534
+ });
535
+ break;
536
+ case 'despawn_guardians': {
537
+ push({
538
+ t,
539
+ kind: 'despawn_guardians',
540
+ actor: summoner.side,
541
+ entryId: intent.entryId,
542
+ });
543
+ const owned = guardianSidesBySummoner.get(summoner.side);
544
+ if (!owned) break;
545
+ for (const side of [...owned]) {
546
+ const g = ctx.sideFighter?.(side) ?? null;
547
+ if (!g || dead(g)) {
548
+ owned.delete(side);
549
+ continue;
550
+ }
551
+ if (intent.entryId > 0 && (g.spec.creatureEntry ?? 0) !== intent.entryId) continue;
552
+ forceDespawnEnemy(g);
553
+ owned.delete(side);
554
+ }
555
+ break;
556
+ }
557
+ // ---- S4 tail intents ----
558
+ case 'set_faction':
559
+ push({
560
+ t,
561
+ kind: 'set_faction',
562
+ actor: summoner.side,
563
+ factionId: intent.factionId,
564
+ factionFlags: intent.factionFlags,
565
+ });
566
+ break;
567
+ case 'morph':
568
+ push({
569
+ t,
570
+ kind: 'morph',
571
+ actor: summoner.side,
572
+ creatureId: intent.creatureId,
573
+ modelId: intent.modelId,
574
+ });
575
+ break;
576
+ case 'threat_single': {
577
+ const tgt = resolveEventAiTargetFighter(summoner, intent.target);
578
+ const tm = threatOf(summoner);
579
+ if (tm) {
580
+ if (intent.isDirect) {
581
+ tm.addThreatDirectly(tgt.side, intent.value, false);
582
+ } else {
583
+ tm.modifyThreatPercent(tgt.side, intent.value);
584
+ }
585
+ }
586
+ push({
587
+ t,
588
+ kind: 'threat_single',
589
+ actor: summoner.side,
590
+ target: tgt.side,
591
+ value: intent.value,
592
+ isDirect: intent.isDirect,
593
+ });
594
+ break;
595
+ }
596
+ case 'threat_all_pct': {
597
+ const tm = threatOf(summoner);
598
+ if (tm) tm.modifyAllThreatPercent(intent.percent);
599
+ push({ t, kind: 'threat_all_pct', actor: summoner.side, percent: intent.percent });
600
+ break;
601
+ }
602
+ case 'quest_event':
603
+ // GOOBER-equivalent credit signal → AreaExploredOrEventHappens.
604
+ // Cite: CreatureEventAI.cpp ~847–862; gameObjects useGameObject questCredit.
605
+ push({
606
+ t,
607
+ kind: 'quest_event',
608
+ actor: summoner.side,
609
+ questId: intent.questId,
610
+ rewardGroup: intent.rewardGroup,
611
+ });
612
+ emitSimEffect(ctx, {
613
+ kind: 'questEvent', questId: intent.questId, rewardGroup: intent.rewardGroup,
614
+ });
615
+ break;
616
+ case 'set_unit_field': {
617
+ const tgt = resolveEventAiTargetFighter(summoner, intent.target);
618
+ push({
619
+ t,
620
+ kind: 'set_unit_field',
621
+ actor: summoner.side,
622
+ field: intent.field,
623
+ value: intent.value,
624
+ target: tgt.side,
625
+ });
626
+ // POWER1 (mana) field 23 — apply to fighter power when self/target is a mana unit.
627
+ // Cite: UpdateFields.h UNIT_FIELD_POWER1 = 0x11+OBJECT_END = 23.
628
+ if (intent.field === 23) {
629
+ tgt.power = Math.max(0, Math.min(tgt.spec.maxPower, intent.value | 0));
630
+ }
631
+ break;
632
+ }
633
+ case 'set_unit_flag': {
634
+ const tgt = resolveEventAiTargetFighter(summoner, intent.target);
635
+ const ocs = enemyCastBySide.get(tgt.side as EnemySide);
636
+ if (ocs) ocs.eventAi.unitFlags |= intent.flag;
637
+ // UNIT_FLAG_SPAWNING / PACIFIED must hit the live fighter, not only EventAI bookkeeping.
638
+ // Cite: Unit.h UNIT_FLAG_*; Relations.cpp CanAttack; HandleAuraModPacify.
639
+ if (intent.flag & UNIT_FLAG.SPAWNING) tgt.unattackableUntil = Math.max(tgt.unattackableUntil, t + 3_600_000);
640
+ if (intent.flag & UNIT_FLAG.PACIFIED) tgt.pacifiedUntil = Math.max(tgt.pacifiedUntil, t + 3_600_000);
641
+ if (intent.flag & UNIT_FLAG.STUNNED) tgt.stunnedUntil = Math.max(tgt.stunnedUntil, t + 3_600_000);
642
+ push({
643
+ t,
644
+ kind: 'set_unit_flag',
645
+ actor: summoner.side,
646
+ flag: intent.flag,
647
+ target: tgt.side,
648
+ });
649
+ break;
650
+ }
651
+ case 'remove_unit_flag': {
652
+ const tgt = resolveEventAiTargetFighter(summoner, intent.target);
653
+ const ocs = enemyCastBySide.get(tgt.side as EnemySide);
654
+ if (ocs) ocs.eventAi.unitFlags &= ~intent.flag;
655
+ if (intent.flag & UNIT_FLAG.SPAWNING) tgt.unattackableUntil = 0;
656
+ if (intent.flag & UNIT_FLAG.PACIFIED) tgt.pacifiedUntil = 0;
657
+ if (intent.flag & UNIT_FLAG.STUNNED) tgt.stunnedUntil = 0;
658
+ push({
659
+ t,
660
+ kind: 'remove_unit_flag',
661
+ actor: summoner.side,
662
+ flag: intent.flag,
663
+ target: tgt.side,
664
+ });
665
+ break;
666
+ }
667
+ case 'combat_movement':
668
+ if (castState) castState.eventAi.combatMovement = intent.enabled;
669
+ // When stopping combat movement with melee flag, suppress AA (ranged switch).
670
+ // Cite: ProcessAction ~913–916 SendMeleeAttackStart/Stop.
671
+ if (!intent.enabled && intent.melee) {
672
+ summoner.autoAttackEnabled = false;
673
+ if (castState) castState.eventAi.meleeEnabled = false;
674
+ } else if (intent.enabled && intent.melee) {
675
+ summoner.autoAttackEnabled = true;
676
+ if (castState) castState.eventAi.meleeEnabled = true;
677
+ }
678
+ push({
679
+ t,
680
+ kind: 'combat_movement',
681
+ actor: summoner.side,
682
+ enabled: intent.enabled,
683
+ melee: intent.melee,
684
+ });
685
+ break;
686
+ case 'evade': {
687
+ // combatOnly → CombatStopWithPets; else EnterEvadeMode (clear threat + reset).
688
+ // Cite: ProcessAction ~942–946.
689
+ const tm = threatOf(summoner);
690
+ if (tm) tm.clear();
691
+ if (!intent.combatOnly) {
692
+ // Full evade: heal to full is product-dependent; clear combat flags + pulse EVADE.
693
+ summoner.hp = Math.max(summoner.hp, 1);
694
+ if (castState) {
695
+ const evadeIntents = pulseEventAi(
696
+ castState.eventAi,
697
+ EVENT_T.EVADE,
698
+ {
699
+ t,
700
+ hpPct: Math.floor((summoner.hp * 100) / Math.max(1, summoner.spec.maxHealth)),
701
+ inCombat: false,
702
+ evading: true,
703
+ },
704
+ { rng, summons: summoner.spec.ai?.summons, speakerName: summoner.spec.name },
705
+ );
706
+ for (const ei of evadeIntents) applyEventAiIntent(summoner, ei);
707
+ // Reached-home follows the evade. This engine has no walk-home phase, so the two fire
708
+ // back to back rather than a tick apart. 19 DB rows, six of them 'rel bosses.
709
+ notifyEventAiReachedHome(summoner);
710
+ }
711
+ }
712
+ push({ t, kind: 'evade', actor: summoner.side, combatOnly: intent.combatOnly });
713
+ break;
714
+ }
715
+ case 'die':
716
+ // Suicide() — Cite: ProcessAction ~1085–1093.
717
+ summoner.hp = 0;
718
+ push({ t, kind: 'death', actor: summoner.side });
719
+ break;
720
+ case 'zone_combat_pulse':
721
+ // SetInCombatWithZone — engage all living players/allies on the map.
722
+ // Cite: ProcessAction ~1095–1100; Creature.cpp:2310–2352.
723
+ const tm = threatOf(summoner);
724
+ if (tm) {
725
+ tm.addThreatDirectly('player', 1, false);
726
+ for (const a of ctx.livingAllies()) tm.addThreatDirectly(a.side, 1, false);
727
+ }
728
+ pulsePartyCombat(ctx.partyUnits(), t);
729
+ push({ t, kind: 'zone_combat_pulse', actor: summoner.side });
730
+ break;
731
+ case 'set_sheath':
732
+ if (castState) castState.eventAi.sheath = intent.sheath;
733
+ push({ t, kind: 'set_sheath', actor: summoner.side, sheath: intent.sheath });
734
+ break;
735
+ case 'mount':
736
+ if (castState) {
737
+ castState.eventAi.mountCreatureId = intent.creatureId;
738
+ castState.eventAi.mountDisplayId = intent.modelId;
739
+ }
740
+ push({
741
+ t,
742
+ kind: 'mount',
743
+ actor: summoner.side,
744
+ creatureId: intent.creatureId,
745
+ modelId: intent.modelId,
746
+ });
747
+ break;
748
+ case 'change_movement':
749
+ if (castState) castState.eventAi.movementType = intent.movementType;
750
+ push({
751
+ t,
752
+ kind: 'change_movement',
753
+ actor: summoner.side,
754
+ movementType: intent.movementType,
755
+ wanderOrPathId: intent.wanderOrPathId,
756
+ flags: intent.flags,
757
+ });
758
+ break;
759
+ case 'set_walk':
760
+ push({ t, kind: 'set_walk', actor: summoner.side, walkType: intent.walkType });
761
+ break;
762
+ case 'set_immobilized_state':
763
+ if (castState) castState.eventAi.immobilized = intent.apply;
764
+ if (intent.apply) {
765
+ summoner.rootedUntil = Math.max(summoner.rootedUntil, t + 86_400_000);
766
+ } else {
767
+ summoner.rootedUntil = 0;
768
+ }
769
+ push({
770
+ t,
771
+ kind: 'set_immobilized_state',
772
+ actor: summoner.side,
773
+ apply: intent.apply,
774
+ combatOnly: intent.combatOnly,
775
+ });
776
+ break;
777
+ default:
778
+ break;
779
+ }
780
+ };
781
+
782
+ // SendAIEventAround — pulse the receive event on living enemies in radius. A radius of 0 is a no-op,
783
+ // and the automatic health and death throws use the default 30 yd. Cite: UnitAI.cpp:621–659.
784
+ const sendAiEventAround = (
785
+ sender: Fighter,
786
+ eventType: number,
787
+ radius: number,
788
+ ): void => {
789
+ const t = ctx.t;
790
+ push({
791
+ t,
792
+ kind: 'throw_ai_event',
793
+ actor: sender.side,
794
+ eventType,
795
+ radius,
796
+ });
797
+ if (!(radius > 0)) return;
798
+ const senderEntry = sender.spec.creatureEntry ?? 0;
799
+ for (const other of ctx.livingEnemies()) {
800
+ if (other === sender) continue;
801
+ const d = distanceBetween(sender, other);
802
+ if (d == null) {
803
+ if (cfg.endless) continue;
804
+ } else if (d > radius) {
805
+ continue;
806
+ }
807
+ const ocs = enemyCastBySide.get(other.side as EnemySide);
808
+ if (!ocs || ocs.eventAi.holders.length === 0) continue;
809
+ const recvIntents = pulseEventAi(
810
+ ocs.eventAi,
811
+ EVENT_T.RECEIVE_AI_EVENT,
812
+ {
813
+ t,
814
+ hpPct: Math.floor((other.hp * 100) / Math.max(1, other.spec.maxHealth)),
815
+ inCombat: true,
816
+ receivedAiEvent: {
817
+ eventType,
818
+ senderEntry,
819
+ },
820
+ },
821
+ { rng, summons: other.spec.ai?.summons, speakerName: other.spec.name },
822
+ );
823
+ for (const ri of recvIntents) applyEventAiIntent(other, ri);
824
+ }
825
+ };
826
+
827
+ /** CreatureEventAI::DamageTaken auto-throw after HP drops. Cite: CreatureEventAI.cpp:1841–1871. */
828
+ const notifyEventAiDamageTaken = (
829
+ target: Fighter,
830
+ healthBefore: number,
831
+ damage: number,
832
+ ): void => {
833
+ if (!isEnemySide(target.side) || !(damage > 0)) return;
834
+ const cs = enemyCastBySide.get(target.side as EnemySide);
835
+ if (!cs) return;
836
+ // Step advances even when mask bits are unset (C++ always writes m_throwAIEventStep).
837
+ const eventType = processThrowAiEventDamageTaken(
838
+ cs.eventAi,
839
+ healthBefore,
840
+ damage,
841
+ target.spec.maxHealth,
842
+ );
843
+ if (eventType == null) return;
844
+ sendAiEventAround(target, eventType, AIEVENT_DEFAULT_THROW_RADIUS_YD);
845
+ };
846
+
847
+ // CreatureEventAI::HealedBy auto-throw when a heal would fill HP. Cite: CreatureEventAI.cpp:1885–1895.
848
+ const notifyEventAiHealedBy = (
849
+ target: Fighter,
850
+ healthBefore: number,
851
+ healedAmount: number,
852
+ ): void => {
853
+ if (!isEnemySide(target.side) || !(healedAmount > 0)) return;
854
+ const cs = enemyCastBySide.get(target.side as EnemySide);
855
+ if (!cs) return;
856
+ const eventType = processThrowAiEventHealedBy(
857
+ cs.eventAi,
858
+ healthBefore,
859
+ healedAmount,
860
+ target.spec.maxHealth,
861
+ );
862
+ if (eventType == null) return;
863
+ sendAiEventAround(target, eventType, AIEVENT_DEFAULT_THROW_RADIUS_YD);
864
+ };
865
+
866
+ // Step EventAI non-CAST actions for one living enemy (phase / spawn / flee). Multiple events may fire in
867
+ // one tick (loop until idle), capped for safety.
868
+ const runEnemyEventAi = (ef: Fighter): void => {
869
+ const t = ctx.t;
870
+ // Damage/heal mirror hooks wrap onDamaged/onHealed once combatants tick.
871
+ ensureMirrorHooks();
872
+ // The timed despawn runs BEFORE the passive and no-holder early-outs, so an idle summon that
873
+ // nobody attacks still expires on schedule.
874
+ if (ef.summonDespawnAt != null && ef.summonDespawnAt > 0 && t >= ef.summonDespawnAt) {
875
+ ef.summonDespawnAt = 0;
876
+ forceDespawnEnemy(ef);
877
+ return;
878
+ }
879
+ const castState = enemyCastBySide.get(ef.side as EnemySide);
880
+ if (!castState) return;
881
+ const possessInvId = castState.scriptDev?.ir.auraScripts?.find((a) => a.possessInvSpellId)
882
+ ?.possessInvSpellId;
883
+ const holdingPossessInv = possessInvId != null && (
884
+ ef.buffActive.has(possessInvId)
885
+ || ef.unattackableUntil > t
886
+ || ctx.effectsOn(ef.side).some((e) => e.spellId === possessInvId)
887
+ );
888
+ // No threat means no victim, so the whole combat EventAI pass is off. Only the endless path was
889
+ // gated, so a world-fight mob ran its combat script from t=0, unpulled. Possess must still tick.
890
+ // oocClock (Kel'Thuzad alone) opts a script's clock back in: his phase-1 ramp runs while
891
+ // unattackable and out of combat. Cite: boss_kelthuzad.cpp:236-238 polls players, not threat.
892
+ const tm = threatOf(ef);
893
+ const oocClock = castState.scriptDev?.ir.oocClock === true;
894
+ if ((!tm || tm.isEmpty()) && !holdingPossessInv && !oocClock) {
895
+ // Out-of-combat line of sight — MoveInLineOfSight while OUT of combat, which is the state this
896
+ // early return covers. Gated on a party member inside acquire range, so it is a sighting.
897
+ if (ctx.nearestPartyInAcquire?.(ef.side)) notifyEventAiOocLos(ef);
898
+ // naxxramas.cpp:1010-1022 MoveInLineOfSight while UNINTERACTIBLE is the OOC path.
899
+ runStoneskinGargoyleReachCasts(ef);
900
+ return;
901
+ }
902
+ if (castState.eventAi.holders.length === 0) {
903
+ // Still tick flee expiry when state exists without holders.
904
+ const st = castState.eventAi;
905
+ if (st.fleeing && st.fleeUntil > 0 && t >= st.fleeUntil) {
906
+ st.fleeing = false;
907
+ st.fleeUntil = 0;
908
+ }
909
+ // ScriptDev-bound bosses have no EventAI holders yet still need the host tick below for phase
910
+ // thresholds, possess-end and leash. Gating them out kept a 40% morph from ever evaluating.
911
+ if (!castState.scriptDev) return;
912
+ }
913
+ // C2: EventAI poll + friendlies snapshot on the SlowUpdate cursor. forceDespawn
914
+ // and ScriptDev stay every tick — ScriptDev timers count down via diffMs.
915
+ const diag = ctx.slowDiag ??= emptySlowDiag();
916
+ const eventAiDue = slowThinkDue({
917
+ side: ef.side,
918
+ t,
919
+ tickMs,
920
+ engaged: true,
921
+ gcdUntil: ef.gcdUntil,
922
+ swingReadyAt: ef.swingReadyAt,
923
+ ohSwingReadyAt: ef.ohSwingReadyAt,
924
+ casting: ef.castingSpell != null || !!ef.channeling,
925
+ });
926
+ if (eventAiDue) {
927
+ diag.eventAiThink++;
928
+ } else {
929
+ diag.eventAiSkip++;
930
+ }
931
+ const maxHp = Math.max(1, ef.spec.maxHealth);
932
+ const hpPct = Math.floor((ef.hp * 100) / maxHp);
933
+ if (eventAiDue) {
934
+ const manaMax = ef.maxMana > 0 ? ef.maxMana : (ef.spec.powerType === 'mana' ? ef.spec.maxPower : 0);
935
+ const manaNow = ef.spec.powerType === 'mana' ? ef.power : ef.mana;
936
+ const manaPct = manaMax > 0 ? Math.floor((manaNow * 100) / manaMax) : undefined;
937
+ const foe = hostileTargetOf(ef);
938
+ const foeMaxHp = Math.max(1, foe.spec.maxHealth);
939
+ const targetHpPct = Math.floor((foe.hp * 100) / foeMaxHp);
940
+ const foeManaMax = foe.maxMana > 0 ? foe.maxMana : (foe.spec.powerType === 'mana' ? foe.spec.maxPower : 0);
941
+ const foeManaNow = foe.spec.powerType === 'mana' ? foe.power : foe.mana;
942
+ const targetManaPct = foeManaMax > 0 ? Math.floor((foeManaNow * 100) / foeManaMax) : undefined;
943
+ const isCcLocked = (u: Fighter): boolean =>
944
+ u.stunnedUntil > t || u.fearedUntil > t || u.incapUntil > t;
945
+ // Endless floor: FRIENDLY_* events only see engaged pack mates, not passive stream.
946
+ // Cite: friendlyPoolOf endless enemy filter; pickAction packMate gate.
947
+ const packMate = (ally: Fighter): boolean => {
948
+ if (ally === ef || !(ally.hp > 0)) return false;
949
+ if (!cfg.endless) return true;
950
+ const tm = threatOf(ally);
951
+ return !!tm && !tm.isEmpty();
952
+ };
953
+ const friendlies = ctx.livingEnemies()
954
+ .filter(packMate)
955
+ .map((ally) => {
956
+ const aMax = Math.max(1, ally.spec.maxHealth);
957
+ return {
958
+ hp: ally.hp,
959
+ maxHp: aMax,
960
+ hpPct: Math.floor((ally.hp * 100) / aMax),
961
+ distanceYd: distanceBetween(ef, ally),
962
+ hasAura: (spellId: number, stacks: number) =>
963
+ ally.buffActive.has(spellId) && stacks <= 1,
964
+ hasCc: isCcLocked(ally),
965
+ };
966
+ });
967
+ {
968
+ const sMax = Math.max(1, ef.spec.maxHealth);
969
+ friendlies.push({
970
+ hp: ef.hp,
971
+ maxHp: sMax,
972
+ hpPct,
973
+ distanceYd: 0,
974
+ hasAura: (spellId: number, stacks: number) =>
975
+ ef.buffActive.has(spellId) && stacks <= 1,
976
+ hasCc: isCcLocked(ef),
977
+ });
978
+ }
979
+ const hitFresh = ef.spellHitAt != null && t - ef.spellHitAt <= tickMs * SLOW_IDLE_RATE;
980
+ // FACING_TARGET — 0 wants the victim's BACK and 1 its front. The creature has the victim in its
981
+ // own front half either way, so what the row asks is whether the VICTIM is facing away.
982
+ const facingTarget: 0 | 1 | undefined = ctx.isInFrontArc
983
+ ? (ctx.isInFrontArc(foe, ef) ? 1 : 0)
984
+ : undefined;
985
+ const aiPulse = { t,
986
+ hpPct,
987
+ manaPct,
988
+ targetHpPct,
989
+ targetManaPct,
990
+ phase: castState.eventAi.phase,
991
+ // The threat gate above already returned for an unengaged creature.
992
+ inCombat: true,
993
+ friendlies,
994
+ hasAura: (spellId: number, stacks: number) => ef.buffActive.has(spellId) && stacks <= 1,
995
+ targetHasAura: (spellId: number, stacks: number) => foe.buffActive.has(spellId) && stacks <= 1,
996
+ targetCasting: foe.castingSpell != null || !!(foe.channeling && t < foe.channeling.endsAt),
997
+ distanceYd: distanceBetween(ef, foe),
998
+ // EVENT_T_SPELLHIT pulse — this tick or the last one. stepEventAi clears it when a
999
+ // SPELLHIT holder fires, so the 8-step loop cannot replay one hit.
1000
+ spellHitId: hitFresh ? ef.spellHitId : undefined,
1001
+ spellHitSchoolMask: hitFresh ? ef.spellHitSchoolMask : undefined,
1002
+ facingTarget,
1003
+ // TARGET_NOT_REACHABLE — a victim the chase cannot path to. The walk path already computed it,
1004
+ // so this is a read rather than a second pathfinder.
1005
+ targetNotReachable: ef.targetUnreachableMs >= 1000,
1006
+ // SELECT_ATTACKING_TARGET — the C++ picks a victim matching the row's range params and fires
1007
+ // when one is found. The poll already resolved a live foe, so presence is what is left.
1008
+ hasAttackingTargetInRange: foe !== ef && !dead(foe),
1009
+ };
1010
+ // Cap steps per tick so a mis-armed 0-repeat loop cannot hang the session.
1011
+ for (let step = 0; step < 8; step++) {
1012
+ const intents = stepEventAi(castState.eventAi, aiPulse, {
1013
+ rng,
1014
+ summons: ef.spec.ai?.summons,
1015
+ speakerName: ef.spec.name,
1016
+ });
1017
+ if (intents.length === 0) break;
1018
+ for (const intent of intents) applyEventAiIntent(ef, intent);
1019
+ // Phase may have changed — keep ctx in sync for next holder this tick.
1020
+ aiPulse.phase = castState.eventAi.phase;
1021
+ }
1022
+ }
1023
+ // ACTION_T_FORCE_DESPAWN delayed path.
1024
+ // Cite: CreatureEventAI.cpp ForcedDespawn ~1112–1114.
1025
+ if (
1026
+ castState.eventAi.forceDespawnAt > 0 &&
1027
+ t >= castState.eventAi.forceDespawnAt &&
1028
+ !dead(ef)
1029
+ ) {
1030
+ castState.eventAi.forceDespawnAt = 0;
1031
+ forceDespawnEnemy(ef);
1032
+ }
1033
+
1034
+ // MoveInLineOfSight proximity answers, OUTSIDE the scriptDev guard on purpose: a body whose whole
1035
+ // script is a proximity check has no timers, so it never gets a cast plan to hang state on.
1036
+ runScriptDevProximityTriggers(ef);
1037
+
1038
+ // aifid S9: ScriptDev host tick — phase HP thresholds + possess-end + leash.
1039
+ // Cite: boss_dathrohan_balnazzar.cpp transform; Anastari m_uiPossessEndTimer.
1040
+ if (castState.scriptDev) {
1041
+ const invId = castState.scriptDev.ir.auraScripts?.find((a) => a.possessInvSpellId)
1042
+ ?.possessInvSpellId;
1043
+ const hasPossessInv = invId != null
1044
+ ? ef.buffActive.has(invId) || ef.unattackableUntil > t
1045
+ : false;
1046
+ const party = ctx.partyUnits();
1047
+ const livingCharmed = party.filter(
1048
+ (u) => !dead(u) && isCharmed(u) && u.charmedBy === ef.side,
1049
+ );
1050
+ let possessedHpFrac: number | null = null;
1051
+ if (castState.scriptDev.possessedSide) {
1052
+ const pos = bySide(castState.scriptDev.possessedSide as Side);
1053
+ // Uncharmed / dead victim is "no player" — CMaNGOS !pPlayer || !IsAlive.
1054
+ if (pos && !dead(pos) && isCharmed(pos) && pos.charmedBy === ef.side) {
1055
+ possessedHpFrac = pos.hp / Math.max(1, pos.spec.maxHealth);
1056
+ } else {
1057
+ possessedHpFrac = null;
1058
+ }
1059
+ } else if (livingCharmed.length > 0) {
1060
+ const pos = livingCharmed[0]!;
1061
+ possessedHpFrac = pos.hp / Math.max(1, pos.spec.maxHealth);
1062
+ }
1063
+ // Threat list size proxy: living hostiles the boss can still hit (+ self if only one).
1064
+ const threatSize = livingHostilesFor(ef).length + (hasPossessInv ? 0 : 0);
1065
+ // While possessing, threat list is "possessed body + maybe nothing else" — use
1066
+ // living hostiles of the *possessed* unit when inv is up.
1067
+ let leashThreat = threatSize;
1068
+ if (hasPossessInv && castState.scriptDev.possessedSide) {
1069
+ const pos = bySide(castState.scriptDev.possessedSide as Side);
1070
+ leashThreat = pos ? livingHostilesFor(pos).length : 0;
1071
+ }
1072
+ const pos = ctx.posOf?.(ef.side);
1073
+ const home = ctx.enemyHomeYd?.get(ef.side);
1074
+ let homeYd: number | undefined;
1075
+ if (pos && home) {
1076
+ const dx = pos.x - home.x;
1077
+ const dy = pos.y - home.y;
1078
+ homeYd = Math.sqrt(dx * dx + dy * dy);
1079
+ }
1080
+ const sdTick = tickScriptDev(castState.scriptDev, {
1081
+ hpPct,
1082
+ t,
1083
+ diffMs: tickMs,
1084
+ threatSize: leashThreat,
1085
+ hasPossessInv,
1086
+ possessedHpFrac,
1087
+ rng,
1088
+ z: pos?.z,
1089
+ x: pos?.x,
1090
+ y: pos?.y,
1091
+ homeYd,
1092
+ });
1093
+ if (sdTick.evade) {
1094
+ // SetLeashingCheck true → EnterEvadeMode. Cite: boss_faerlina.cpp:67-71.
1095
+ if (tm) tm.clear();
1096
+ ef.hp = Math.max(1, ef.spec.maxHealth);
1097
+ ef.power = ef.spec.powerType === 'rage' ? 0 : ef.spec.maxPower;
1098
+ resetCreatureCastSlotsOnEvade(castState.slots, t, rng);
1099
+ castState.pendingSlot = null;
1100
+ runEvadeLifecycle(ctx, ef);
1101
+ return;
1102
+ }
1103
+ if (sdTick.phase != null) {
1104
+ const prevPhase = castState.eventAi.phase;
1105
+ rearmCreatureCastSlotsOnPhase(castState.slots, prevPhase, sdTick.phase, t, rng);
1106
+ castState.eventAi.phase = sdTick.phase;
1107
+ // The same observability event EventAI's set_phase pushes. Without it an IR phase transition
1108
+ // left NO trace in the log, so every oracle had to regex the flavour text instead.
1109
+ push({ t, kind: 'phase', actor: ef.side, phase: sdTick.phase });
1110
+ }
1111
+ const endPossessVanish = (): void => {
1112
+ for (const u of party) {
1113
+ if (u.charmedBy === ef.side || (isCharmed(u) && u.charmedBy === ef.side)) breakCharm(u);
1114
+ }
1115
+ if (invId) removeAurasDueToSpell(ef, invId);
1116
+ ef.unattackableUntil = 0;
1117
+ if (ef.stunnedUntil > t) ef.stunnedUntil = t;
1118
+ if (invId) ef.buffActive.delete(invId);
1119
+ };
1120
+ if (sdTick.breakPossess) {
1121
+ // Cite: boss_baroness_anastari.cpp:102,110 RemoveAurasDueToSpell(SPELL_POSSESS_INV).
1122
+ endPossessVanish();
1123
+ } else if (invId != null && hasPossessInv && livingCharmed.length === 0) {
1124
+ // Victim already dead / uncharmed and inv survived breakCharm — drop vanish.
1125
+ endPossessVanish();
1126
+ }
1127
+ applyScriptDevCombatActions(ef, sdTick.actions);
1128
+ // A spell-hit row can arm a countdown instead of answering at once, and the payload lands when
1129
+ // it expires. The tick was exported and had no caller, so every tagged unit stood there forever.
1130
+ const dieTick = tickDieTimer(castState.scriptDev, tickMs);
1131
+ if (dieTick.actions.length) applyScriptDevCombatActions(ef, dieTick.actions);
1132
+ // JustSummoned reach-cast after summons this tick (and any still walking in).
1133
+ // Cite: boss_ambassador_flamelash.cpp:69-78 MoveInLineOfSight.
1134
+ runScriptDevReachCasts(ef);
1135
+ for (const line of sdTick.flavor) {
1136
+ push({ t, kind: 'flavor', text: line });
1137
+ }
1138
+ // Twin Heal Brother on the SlowUpdate AI poll (not every tickMs). Cite: boss_twinemperors.cpp:209-210.
1139
+ if (eventAiDue) maybeCastHealBrother(ef);
1140
+ }
1141
+ };
1142
+
1143
+ // Fire once when an enemy first reaches 0 HP. The living pass skips corpses, so a death summon
1144
+ // needs this path. Cite: CreatureEventAI.cpp EVENT_T_DEATH; ProcessEvent on JustDied.
1145
+ const runEnemyDeathEventAi = (ef: Fighter): void => {
1146
+ const t = ctx.t;
1147
+ const castState = enemyCastBySide.get(ef.side as EnemySide);
1148
+ if (!castState) return;
1149
+ // AI_EVENT_JUST_DIED auto-throw (mask bit 0) — distinct from EVENT_T_DEATH scripts.
1150
+ // Cite: CreatureEventAI.cpp:1492–1493; Reset() runs first in C++ but mask is read here.
1151
+ if (shouldThrowAiEventJustDied(castState.eventAi)) {
1152
+ sendAiEventAround(ef, AI_EVENT_TYPE.JUST_DIED, AIEVENT_DEFAULT_THROW_RADIUS_YD);
1153
+ }
1154
+ if (castState.eventAi.holders.length === 0) return;
1155
+ for (const holder of castState.eventAi.holders) {
1156
+ if (holder.exhausted) continue;
1157
+ if ((holder.script.eventType | 0) !== EVENT_T.DEATH) continue;
1158
+ if (!rollEventAiChance(holder.script, rng)) {
1159
+ markEventAiUsed(holder, t, rng);
1160
+ continue;
1161
+ }
1162
+ const intents = processEventAiActions(holder, castState.eventAi, {
1163
+ t,
1164
+ summons: ef.spec.ai?.summons,
1165
+ rng,
1166
+ speakerName: ef.spec.name,
1167
+ });
1168
+ markEventAiUsed(holder, t, rng);
1169
+ for (const intent of intents) applyEventAiIntent(ef, intent);
1170
+ }
1171
+ };
1172
+
1173
+ // JustSummoned MoveFollow + guid track for Burning Spirit 9178. Cite:
1174
+ // boss_ambassador_flamelash.cpp:106-114.
1175
+ function applyFlamelashSpiritJustSummoned(
1176
+ state: ScriptDevRuntimeState,
1177
+ add: Fighter,
1178
+ ): void {
1179
+ // REACT_PASSIVE — feed spirits do not AttackStart the party.
1180
+ // Cite: cpp:112 MoveFollow only; no AttackStart on 9178.
1181
+ const ocs = enemyCastBySide.get(add.side as EnemySide);
1182
+ if (ocs) ocs.eventAi.reactState = REACT_STATE.PASSIVE;
1183
+ const tm = ctx.threatByOwner.get(add.side);
1184
+ if (tm) tm.clear();
1185
+ noteJustSummonedSide(state, add.side);
1186
+ }
1187
+
1188
+ /** Which proximity rows each body has already answered. A `once` row is one per spawn. */
1189
+ const proximityFired = new WeakMap<Fighter, Set<string>>();
1190
+
1191
+ // MoveInLineOfSight, generalised: an IR row that answers a player standing too close. The Living
1192
+ // Poison has melee off and no AttackStart, so presence is all it has. Cite: naxxramas.cpp:1084-1089.
1193
+ function runScriptDevProximityTriggers(unit: Fighter): void {
1194
+ if (dead(unit)) return;
1195
+ const ir = enemyCastBySide.get(unit.side as EnemySide)?.scriptDev?.ir
1196
+ ?? scriptDevByCreature((unit.spec.creatureEntry ?? 0) | 0);
1197
+ const rows = ir?.proximityTriggers;
1198
+ if (!rows?.length) return;
1199
+ for (let i = 0; i < rows.length; i++) {
1200
+ const row = rows[i]!;
1201
+ const tag = `proximity_${i}`;
1202
+ const fired = proximityFired.get(unit);
1203
+ if (row.once && fired?.has(tag)) continue;
1204
+ let near = false;
1205
+ for (const h of livingHostilesFor(unit)) {
1206
+ if (dead(h)) continue;
1207
+ if (!(h.side === 'player' || isAllySide(h.side))) continue;
1208
+ const d = distanceBetween(unit, h);
1209
+ if (d != null && d <= row.withinYd) {
1210
+ near = true;
1211
+ break;
1212
+ }
1213
+ }
1214
+ if (!near) continue;
1215
+ if (row.once) {
1216
+ const set = proximityFired.get(unit) ?? new Set<string>();
1217
+ set.add(tag);
1218
+ proximityFired.set(unit, set);
1219
+ }
1220
+ applyScriptDevCombatActions(unit, row.actions);
1221
+ }
1222
+ }
1223
+
1224
+ /** MoveInLineOfSight AttackStart for unattackable Stoneskin Gargoyle. */
1225
+ function runStoneskinGargoyleReachCasts(boss: Fighter): void {
1226
+ if (((boss.spec.creatureEntry ?? 0) | 0) !== NPC_STONESKIN_GARGOYLE_ENTRY) return;
1227
+ const castState = enemyCastBySide.get(boss.side as EnemySide);
1228
+
1229
+ // Only when it has UNIT_FLAG_UNINTERACTIBLE set
1230
+ if (!castState || !(castState.eventAi.unitFlags & UNIT_FLAG.UNINTERACTIBLE)) return;
1231
+
1232
+ // Check player proximity
1233
+ const player = ctx.player;
1234
+ if (!player || dead(player)) return;
1235
+
1236
+ // Not in combat
1237
+ const tm = threatOf(boss);
1238
+ if (tm && !tm.isEmpty()) return;
1239
+
1240
+ // Within 17.0f, not feigned death, LOS.
1241
+ if (player.feigningUntil > ctx.t) return;
1242
+ const d = distanceBetween(boss, player);
1243
+ if (d == null || d > 17.0) return;
1244
+ if (ctx.posOf && ctx.posOf(boss.side) && ctx.posOf(player.side) && !navSightClear(ctx.cfg, ctx.posOf(boss.side)!, ctx.posOf(player.side)!)) return;
1245
+
1246
+ // Cite: naxxramas.cpp:1010-1022 MoveInLineOfSight
1247
+ applyEventAiIntent(boss, { kind: 'attack_start', target: 13 });
1248
+ }
1249
+
1250
+ /** MoveInLineOfSight reach-cast for tracked Flamelash spirits. */
1251
+ function runScriptDevReachCasts(boss: Fighter): void {
1252
+ if (((boss.spec.creatureEntry ?? 0) | 0) !== NPC_AMBASSADOR_FLAMELASH) return;
1253
+ const castState = enemyCastBySide.get(boss.side as EnemySide);
1254
+ const sd = castState?.scriptDev;
1255
+ if (!sd || !hasTrackedSummonSides(sd)) return;
1256
+ const dues = collectReachCastsDue(sd, {
1257
+ bossSide: boss.side,
1258
+ creatureEntry: NPC_BURNING_SPIRIT,
1259
+ spellId: SPELL_BURNING_SPIRIT,
1260
+ withinYd: FLAMELASH_SPIRIT_REACH_YD,
1261
+ cite: 'boss_ambassador_flamelash.cpp:69-78',
1262
+ distanceYd: (a, b) => {
1263
+ const fa = bySide(a as Side);
1264
+ const fb = bySide(b as Side);
1265
+ if (!fa || !fb) return null;
1266
+ // ctx.distanceBetween is number|undefined and collectReachCastsDue guards with
1267
+ // `d == null`, so undefined already behaved as null. Say so in the type.
1268
+ return distanceBetween(fa, fb) ?? null;
1269
+ },
1270
+ isAlive: (side) => {
1271
+ const f = bySide(side as Side);
1272
+ return !!f && !dead(f);
1273
+ },
1274
+ entryOf: (side) => {
1275
+ const f = bySide(side as Side);
1276
+ return (f?.spec.creatureEntry ?? 0) | 0;
1277
+ },
1278
+ });
1279
+ for (const due of dues) {
1280
+ const spirit = bySide(due.summonSide as Side);
1281
+ if (!spirit || dead(spirit) || dead(boss)) continue;
1282
+ const sp =
1283
+ boss.spec.spells.find((s) => s.id === due.spellId)
1284
+ ?? spirit.spec.spells.find((s) => s.id === due.spellId);
1285
+ if (!sp) continue;
1286
+ push({
1287
+ t: ctx.t,
1288
+ kind: 'flavor',
1289
+ text: `${spirit.spec.name} feeds ${boss.spec.name} (${due.spellId})!`,
1290
+ });
1291
+ // pWho->CastSpell(m_creature, SPELL_BURNING_SPIRIT). Cite: cpp:76.
1292
+ if (typeof ctx.applySpell === 'function') {
1293
+ (ctx.applySpell as (src: Fighter, tgt: Fighter, spell: typeof sp) => void)(
1294
+ spirit,
1295
+ boss,
1296
+ sp,
1297
+ );
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ function applyScriptDevCombatActions(
1303
+ actor: Fighter,
1304
+ actions: readonly ScriptDevAction[],
1305
+ target?: Fighter,
1306
+ ): void {
1307
+ // Aggro lifecycle runs here before the first swing — install mirror hooks early.
1308
+ ensureMirrorHooks();
1309
+ const t = ctx.t;
1310
+ const actorCast = enemyCastBySide.get(actor.side as EnemySide);
1311
+ for (const act of actions) {
1312
+ // An unrolled C++ loop keeps its per-iteration instance-data condition on the row (Hakkar's Power:
1313
+ // one stack per High Priest still alive). Cite: boss_hakkar.cpp:83-87 GetData(i) != DONE.
1314
+ if ('gate' in act && act.gate && !dataGatePasses(act.gate, instanceData)) continue;
1315
+ if (act.kind === 'set_instance_data') {
1316
+ const field = act.field | 0;
1317
+ const next = act.increment
1318
+ ? (instanceData.get(field) ?? 0) + (act.value | 0 || 1)
1319
+ : (act.value | 0);
1320
+ instanceData.set(field, next);
1321
+ push({ t, kind: 'instance_data', field, value: next });
1322
+ } else if (act.kind === 'yell') {
1323
+ // if (urand(0, N)) return — skip unless the roll is 0. Cite: boss_vaelastrasz.cpp:141-144.
1324
+ if (act.urandInclusiveMax != null && rng.int(0, act.urandInclusiveMax | 0) !== 0) continue;
1325
+ // script_texts now resolves (core.scriptTexts), so this speaks the line instead of
1326
+ // printing its id. creature_text is what carries the textId to the recorded clip.
1327
+ const textId = act.textIds && act.textIds.length > 0 ? act.textIds[rng.int(0, act.textIds.length - 1)] : (act.textId as number);
1328
+ const row = lookupEventAiText(textId);
1329
+ if (row) {
1330
+ const line = formatEventAiChatLine(actor.spec.name, row);
1331
+ push({
1332
+ t,
1333
+ kind: 'creature_text',
1334
+ actor: actor.side,
1335
+ textId: textId | 0,
1336
+ chatType: line.chatType,
1337
+ text: line.text,
1338
+ });
1339
+ } else {
1340
+ push({ t, kind: 'flavor', text: `${actor.spec.name} yells! (${textId})` });
1341
+ }
1342
+ } else if (act.kind === 'cast_self') {
1343
+ // A scripted triggered self-cast executed by the tick, the single owner of that HP threshold.
1344
+ // Armed as an HP slot as well, it carried no flags, re-armed every tick and heal-looped.
1345
+ const sp = actor.spec.spells.find((s) => s.id === act.spellId);
1346
+ push({
1347
+ t,
1348
+ kind: 'flavor',
1349
+ text: `${actor.spec.name} casts ${sp?.name ?? `spell ${act.spellId}`}!`,
1350
+ });
1351
+ if (sp && typeof ctx.applySpell === 'function') {
1352
+ (ctx.applySpell as (src: Fighter, tgt: Fighter, spell: typeof sp) => void)(actor, actor, sp);
1353
+ } else if (sp) {
1354
+ for (const eff of sp.effects) {
1355
+ if (eff.kind === 'heal_max_health') {
1356
+ const healed = Math.max(0, actor.spec.maxHealth - actor.hp);
1357
+ actor.hp = actor.spec.maxHealth;
1358
+ if (healed > 0) push({ t, kind: 'heal', actor: actor.side, amount: Math.round(healed) });
1359
+ } else if (eff.kind === 'energize') {
1360
+ // EffectMiscValue names the pool; a unit with no such pool gains nothing.
1361
+ // Cite: SpellEffects.cpp EffectEnergize; SpellAuras.cpp:4838.
1362
+ const pwr = eff.powerType | 0;
1363
+ const max = maxPowerOfType(actor, pwr);
1364
+ if (max > 0) {
1365
+ const cur = powerOfType(actor, pwr);
1366
+ setPowerOfType(actor, pwr, Math.min(max, cur + (eff.max ?? eff.min ?? 0)));
1367
+ }
1368
+ }
1369
+ }
1370
+ }
1371
+ const ownSummons = sp ? new Set(planCastSummons(actor, sp).map((o) => o.creature)) : null;
1372
+ for (const step of planNonCharmAuraOnApply(
1373
+ act.spellId,
1374
+ sp?.name ?? `spell ${act.spellId}`,
1375
+ t,
1376
+ actor.spec.spells,
1377
+ )) {
1378
+ // applySpell above already spawned the cast's own 41/42 via runCastSummons.
1379
+ if (step.kind === 'summon' && ownSummons?.has(step.creatureEntry)) continue;
1380
+ if (step.kind === 'summon') {
1381
+ const life = durMs(sp?.durationIndex) ?? 0;
1382
+ for (let i = 0; i < step.count; i++) {
1383
+ const add = injectEnemySummon(actor, step.creatureEntry, life);
1384
+ if (add && step.isGuardian) {
1385
+ add.spec.isGuardian = true;
1386
+ if (step.createdBySpell) add.spec.createdBySpell = step.createdBySpell;
1387
+ }
1388
+ }
1389
+ } else if (step.kind === 'flavor') {
1390
+ push({ t, kind: 'flavor', text: `${actor.spec.name} ${step.text}` });
1391
+ }
1392
+ }
1393
+ } else if (act.kind === 'summon') {
1394
+ if (act.creatureEntry && act.creatureEntry > 0) {
1395
+ const isSpirit =
1396
+ (act.creatureEntry | 0) === NPC_BURNING_SPIRIT
1397
+ && ((actor.spec.creatureEntry ?? 0) | 0) === NPC_AMBASSADOR_FLAMELASH;
1398
+ const add = injectEnemySummon(
1399
+ actor,
1400
+ act.creatureEntry,
1401
+ isSpirit ? FLAMELASH_SPIRIT_DESPAWN_MS : 0,
1402
+ );
1403
+ if (add && isSpirit && actorCast?.scriptDev) {
1404
+ applyFlamelashSpiritJustSummoned(actorCast.scriptDev, add);
1405
+ }
1406
+ }
1407
+ } else if (act.kind === 'summon_count') {
1408
+ if (act.creatureEntry && act.creatureEntry > 0) {
1409
+ const n = Math.max(0, act.count | 0);
1410
+ const worldPos =
1411
+ act.x != null && act.y != null ? { x: act.x, y: act.y } : undefined;
1412
+ const isSpirit =
1413
+ (act.creatureEntry | 0) === NPC_BURNING_SPIRIT
1414
+ && ((actor.spec.creatureEntry ?? 0) | 0) === NPC_AMBASSADOR_FLAMELASH;
1415
+ for (let i = 0; i < n; i++) {
1416
+ const add = injectEnemySummon(
1417
+ actor,
1418
+ act.creatureEntry,
1419
+ isSpirit ? FLAMELASH_SPIRIT_DESPAWN_MS : 0,
1420
+ worldPos,
1421
+ );
1422
+ if (add && isSpirit && actorCast?.scriptDev) {
1423
+ applyFlamelashSpiritJustSummoned(actorCast.scriptDev, add);
1424
+ }
1425
+ }
1426
+ }
1427
+ } else if (act.kind === 'update_entry') {
1428
+ const morph = actor.spec.scriptDevMorphs?.find((m) => m.creatureEntry === act.entry);
1429
+ const oldName = actor.spec.name;
1430
+ actor.spec.creatureEntry = act.entry;
1431
+ if (morph) {
1432
+ actor.spec.name = morph.name;
1433
+ actor.spec.maxHealth = morph.maxHealth;
1434
+ if (morph.armor != null) actor.spec.armor = morph.armor;
1435
+ if (morph.weapon) actor.spec.weapon = morph.weapon;
1436
+ actor.hp = morph.maxHealth;
1437
+ push({ t, kind: 'flavor', text: `${oldName} transforms into ${morph.name}!` });
1438
+ } else {
1439
+ push({ t, kind: 'flavor', text: `${actor.spec.name} transforms! (entry ${act.entry})` });
1440
+ }
1441
+ } else if (act.kind === 'remove_auras') {
1442
+ removeAurasDueToSpell(actor, act.spellId | 0);
1443
+ push({
1444
+ t,
1445
+ kind: 'remove_auras',
1446
+ actor: actor.side,
1447
+ target: actor.side,
1448
+ spellId: act.spellId | 0,
1449
+ });
1450
+ } else if (act.kind === 'flavor') {
1451
+ push({ t, kind: 'flavor', text: `${actor.spec.name} ${act.text}` });
1452
+ } else if (act.kind === 'open_door') {
1453
+ push({
1454
+ t,
1455
+ kind: 'flavor',
1456
+ text: `Door ${act.goEntry} opens.`,
1457
+ });
1458
+ } else if (act.kind === 'close_door') {
1459
+ push({
1460
+ t,
1461
+ kind: 'flavor',
1462
+ text: `Door ${act.goEntry} closes.`,
1463
+ });
1464
+ } else if (act.kind === 'quest_credit') {
1465
+ const id = act.entry | 0;
1466
+ // The entry means a different thing per credit kind, and this arm read every row as a quest
1467
+ // id. Egan's Blaster credits a CREATURE, so the Restless Soul handed back nothing at all.
1468
+ if (id > 0) {
1469
+ const kind = act.credit ?? 'event';
1470
+ if (kind === 'event') {
1471
+ push({ t, kind: 'quest_event', actor: actor.side, questId: id, rewardGroup: false });
1472
+ emitSimEffect(ctx, { kind: 'questEvent', questId: id, rewardGroup: false });
1473
+ } else if (kind === 'creature') {
1474
+ // Same call ACTION_T_KILLED_MONSTER already uses, so the questlog write and the log
1475
+ // line come from one place. Cite: Player.cpp:13477 KilledMonsterCredit.
1476
+ emitSimEffect(ctx, { kind: 'killCredit', creatureEntry: id });
1477
+ }
1478
+ // A talk credit belongs to the world interact path, not a session: it credits a talk
1479
+ // objective, and crediting it as a kill here would be the wrong objective entirely.
1480
+ }
1481
+ } else if (act.kind === 'stop_combat') {
1482
+ if (!act.attackOnly) {
1483
+ push({
1484
+ t,
1485
+ kind: 'flavor',
1486
+ text: `${actor.spec.name} yields!`,
1487
+ });
1488
+ if (actor.hp > 1) actor.hp = Math.max(1, actor.hp);
1489
+ if (!ctx.ended) {
1490
+ ctx.ended = true;
1491
+ ctx.winner = 'player';
1492
+ }
1493
+ } else {
1494
+ // AttackStop(true) — halt melee, encounter continues (Mr. Smite chest walk).
1495
+ // Cite: boss_mr_smite.cpp:214 AttackStop(true).
1496
+ actor.autoAttackEnabled = false;
1497
+ }
1498
+ } else if (act.kind === 'force_despawn') {
1499
+ const delayMs = Math.max(0, (act.delayMs ?? 0) | 0);
1500
+ if (delayMs <= 0) {
1501
+ forceDespawnEnemy(actor);
1502
+ } else if (actorCast) {
1503
+ actorCast.eventAi.forceDespawnAt = t + delayMs;
1504
+ }
1505
+ } else if (act.kind === 'despawn_guardians') {
1506
+ // Cite: CreatureEventAI.cpp ACTION_T_DESPAWN_GUARDIANS; boss_jeklik.cpp:185-195.
1507
+ const owned = guardianSidesBySummoner.get(actor.side);
1508
+ if (owned) {
1509
+ const want = act.entryId | 0;
1510
+ for (const side of [...owned]) {
1511
+ const g = ctx.sideFighter?.(side) ?? null;
1512
+ if (!g || dead(g)) {
1513
+ owned.delete(side);
1514
+ continue;
1515
+ }
1516
+ if (want > 0 && ((g.spec.creatureEntry ?? 0) | 0) !== want) continue;
1517
+ forceDespawnEnemy(g);
1518
+ owned.delete(side);
1519
+ }
1520
+ }
1521
+ } else if (act.kind === 'set_death_prevention') {
1522
+ // Cite: CreatureAI.cpp:130-135 SetDeathPrevention; Unit.cpp:1000-1001 leave 1 HP.
1523
+ if (actorCast) {
1524
+ actorCast.eventAi.deathPrevention = act.enabled;
1525
+ if (act.enabled) actorCast.eventAi.deathPreventionFired = false;
1526
+ }
1527
+ push({ t, kind: 'death_prevention', actor: actor.side, enabled: act.enabled });
1528
+ } else if (act.kind === 'set_phase') {
1529
+ if (actorCast) {
1530
+ rearmCreatureCastSlotsOnPhase(actorCast.slots, actorCast.eventAi.phase, act.phase | 0, t, rng);
1531
+ actorCast.eventAi.phase = act.phase | 0;
1532
+ if (actorCast.scriptDev) actorCast.scriptDev.phase = act.phase | 0;
1533
+ }
1534
+ push({ t, kind: 'phase', actor: actor.side, phase: act.phase | 0 });
1535
+ } else if (act.kind === 'set_health_pct') {
1536
+ // Cite: boss_vaelastrasz.cpp:115 Reset SetHealth(uint32(GetMaxHealth()*.3)). Floor at 1:
1537
+ // this runs on an evade Reset now, and a 0% row there would kill the boss it re-opens.
1538
+ const pct = Math.max(0, Math.min(100, act.pct));
1539
+ actor.hp = Math.max(1, Math.floor(actor.spec.maxHealth * (pct / 100)));
1540
+ push({ t, kind: 'flavor', text: `${actor.spec.name} health set to ${pct}%.` });
1541
+ } else if (act.kind === 'cast_on_player') {
1542
+ // The one-target twin of the group cast. It existed only on the GO-use path, so a scripted
1543
+ // NPC cast aimed at the player did nothing at all inside a session.
1544
+ const sp = actor.spec.spells.find((s) => s.id === act.spellId);
1545
+ if (!dead(player) && !(act.auraNotPresent && player.buffActive.has(act.spellId | 0))) {
1546
+ push({
1547
+ t,
1548
+ kind: 'flavor',
1549
+ text: `${actor.spec.name} casts ${sp?.name ?? `spell ${act.spellId}`} on you!`,
1550
+ });
1551
+ if (sp && typeof ctx.applySpell === 'function') {
1552
+ (ctx.applySpell as (src: Fighter, tgt: Fighter, spell: typeof sp) => void)(
1553
+ actor,
1554
+ player,
1555
+ sp,
1556
+ );
1557
+ }
1558
+ }
1559
+ } else if (act.kind === 'cast_on_group') {
1560
+ // Raid-wide buff (Vael Essence of the Red). Cite: boss_vaelastrasz.cpp:152-153;
1561
+ // stratholmeScripts.cpp:71-87 Group loop shape mirrored for combat party.
1562
+ const sp = actor.spec.spells.find((s) => s.id === act.spellId);
1563
+ const targets = ctx.partyUnits().filter((u) => !dead(u));
1564
+ push({
1565
+ t,
1566
+ kind: 'flavor',
1567
+ text: `${actor.spec.name} casts ${sp?.name ?? `spell ${act.spellId}`} on the group!`,
1568
+ });
1569
+ if (sp && typeof ctx.applySpell === 'function') {
1570
+ for (const tgt of targets) {
1571
+ (ctx.applySpell as (src: Fighter, tgt: Fighter, spell: typeof sp) => void)(
1572
+ actor,
1573
+ tgt,
1574
+ sp,
1575
+ );
1576
+ }
1577
+ }
1578
+ } else if (act.kind === 'cast_on_script_targets') {
1579
+ // DoCastSpellIfCan(nullptr, id) → spell_script_target living entries.
1580
+ // Cite: boss_majordomo_executus.cpp:243; SpellTargets.cpp:40,71; spell_script_target.
1581
+ const sp = actor.spec.spells.find((s) => s.id === act.spellId);
1582
+ const entries = new Set(scriptTargetCreatureEntries(act.spellId | 0));
1583
+ const mode = scriptTargetPickMode(sp?.rawEffects);
1584
+ const radiusYd = sp?.radiusYd != null && sp.radiusYd > 0 ? sp.radiusYd : null;
1585
+ const candidates: Fighter[] = [];
1586
+ for (const other of ctx.livingEnemies()) {
1587
+ if (other === actor) continue;
1588
+ const entry = (other.spec.creatureEntry ?? 0) | 0;
1589
+ if (!entries.has(entry)) continue;
1590
+ if (radiusYd != null) {
1591
+ const d = distanceBetween(actor, other);
1592
+ if (d != null && d > radiusYd) continue;
1593
+ }
1594
+ candidates.push(other);
1595
+ }
1596
+ let targets = candidates;
1597
+ if (mode === 'near' && candidates.length > 1) {
1598
+ candidates.sort((a, b) => {
1599
+ const da = distanceBetween(actor, a) ?? Number.POSITIVE_INFINITY;
1600
+ const db = distanceBetween(actor, b) ?? Number.POSITIVE_INFINITY;
1601
+ return da - db;
1602
+ });
1603
+ targets = candidates.slice(0, 1);
1604
+ }
1605
+ push({
1606
+ t,
1607
+ kind: 'flavor',
1608
+ text: `${actor.spec.name} casts ${sp?.name ?? `spell ${act.spellId}`} on script targets!`,
1609
+ });
1610
+ if (sp && typeof ctx.applySpell === 'function') {
1611
+ for (const tgt of targets) {
1612
+ // Applying a friendly-resolved buff sticks it on the CASTER, so self-cast on each script
1613
+ // target instead — that is what lands the encouragement auras on the Flamewakers.
1614
+ (ctx.applySpell as (src: Fighter, tgt: Fighter, spell: typeof sp) => void)(
1615
+ tgt,
1616
+ tgt,
1617
+ sp,
1618
+ );
1619
+ }
1620
+ }
1621
+ } else if (act.kind === 'set_combat_movement') {
1622
+ // Cite: UnitAI.cpp:301-318 SetCombatMovement. Lived only in the onDeath switch, so a
1623
+ // Reset that turns movement back on (boss_eranikus) never reached the fighter.
1624
+ if (actorCast) actorCast.eventAi.combatMovement = act.enabled;
1625
+ push({
1626
+ t,
1627
+ kind: 'flavor',
1628
+ text: act.enabled
1629
+ ? `${actor.spec.name} resumes combat movement.`
1630
+ : `${actor.spec.name} stops combat movement.`,
1631
+ });
1632
+ } else if (act.kind === 'set_melee_enabled') {
1633
+ if (actorCast) actorCast.eventAi.meleeEnabled = act.enabled;
1634
+ actor.autoAttackEnabled = act.enabled;
1635
+ } else if (act.kind === 'send_ai_event') {
1636
+ // UnitAI::SendAIEvent — receiver->AI()->ReceiveAIEvent. Cite: UnitAI.cpp:664-668.
1637
+ const recvs: Fighter[] = [];
1638
+ if (act.receiver === 'self') {
1639
+ recvs.push(actor);
1640
+ } else {
1641
+ const entry = (act.creatureEntry ?? 0) | 0;
1642
+ if (entry > 0) {
1643
+ for (const other of ctx.livingEnemies()) {
1644
+ if ((other.spec.creatureEntry ?? 0) !== entry) continue;
1645
+ if (act.radiusYd != null) {
1646
+ const d = distanceBetween(actor, other);
1647
+ if (d != null && d > act.radiusYd) continue;
1648
+ }
1649
+ recvs.push(other);
1650
+ }
1651
+ }
1652
+ }
1653
+ for (const recv of recvs) {
1654
+ push({
1655
+ t,
1656
+ kind: 'ai_event',
1657
+ actor: actor.side,
1658
+ eventType: act.eventType | 0,
1659
+ receiver: recv.side,
1660
+ });
1661
+ const ir = enemyCastBySide.get(recv.side as EnemySide)?.scriptDev?.ir
1662
+ ?? scriptDevByCreature((recv.spec.creatureEntry ?? 0) | 0);
1663
+ if (!ir) continue;
1664
+ const hook = handleReceiveAiEvent(ir, act.eventType | 0, (actor.spec.creatureEntry ?? 0) | 0);
1665
+ if (!hook.handled) continue;
1666
+ applyScriptDevCombatActions(recv, hook.actions);
1667
+ }
1668
+ } else if (act.kind === 'reset_threat') {
1669
+ // UnitAI::DoResetThreat → modifyAllThreatPercent(-100). Cite: UnitAI.cpp:773-781.
1670
+ // of:'cast' → modifyThreatPercent(that unit, -100). Cite: boss_onyxia.cpp:387-393.
1671
+ const resetTm = threatOf(actor);
1672
+ if (act.of === 'cast' && target) {
1673
+ if (resetTm) resetTm.modifyThreatPercent(target.side, -100);
1674
+ push({
1675
+ t, kind: 'threat_single', actor: actor.side, target: target.side,
1676
+ value: -100, isDirect: false,
1677
+ });
1678
+ } else {
1679
+ if (resetTm) resetTm.modifyAllThreatPercent(-100);
1680
+ push({ t, kind: 'threat_all_pct', actor: actor.side, percent: -100 });
1681
+ }
1682
+ } else if (act.kind === 'chase_focus') {
1683
+ // DoAttackNewTarget: DoResetThreat + random player + AddThreat + AttackStart.
1684
+ // Cite: boss_buru.cpp:131-136; ThreatManager addThreatDirectly / setCurrentVictimByTarget.
1685
+ const chaseTm = threatOf(actor);
1686
+ if (chaseTm) chaseTm.modifyAllThreatPercent(-100);
1687
+ push({ t, kind: 'threat_all_pct', actor: actor.side, percent: -100 });
1688
+ const pool = livingHostilesFor(actor).filter((h) => {
1689
+ if (dead(h)) return false;
1690
+ // SELECT_FLAG_PLAYER. Cite: boss_buru.cpp:133; Unit.h SelectFlags.
1691
+ return h.side === 'player' || isAllySide(h.side);
1692
+ });
1693
+ if (chaseTm && pool.length > 0) {
1694
+ const tgt = pool[rng.int(0, pool.length - 1)]!;
1695
+ const threatAmt = act.threat;
1696
+ chaseTm.addThreatDirectly(tgt.side, threatAmt, false);
1697
+ chaseTm.setCurrentVictimByTarget(tgt.side);
1698
+ push({
1699
+ t,
1700
+ kind: 'threat_single',
1701
+ actor: actor.side,
1702
+ target: tgt.side,
1703
+ value: threatAmt,
1704
+ isDirect: true,
1705
+ });
1706
+ push({ t, kind: 'attack_start', actor: actor.side, target: tgt.side });
1707
+ }
1708
+ } else if (act.kind === 'adds_threat_players') {
1709
+ // HandleOpenGates AddThreat(player) per add. No side geometry — every listed
1710
+ // living add engages the raid. Cite: boss_gothik.cpp:261-287.
1711
+ const entries = new Set(act.creatureEntries.map((e) => e | 0));
1712
+ for (const add of ctx.livingEnemies()) {
1713
+ if (!entries.has((add.spec.creatureEntry ?? 0) | 0)) continue;
1714
+ const tm = threatOf(add);
1715
+ if (!tm) continue;
1716
+ tm.addThreatDirectly('player', 1, false);
1717
+ for (const a of ctx.livingAllies()) tm.addThreatDirectly(a.side, 1, false);
1718
+ push({ t, kind: 'zone_combat_pulse', actor: add.side });
1719
+ }
1720
+ } else if (act.kind === 'arm_timer') {
1721
+ // CombatAI::ResetCombatAction — clear disabled + arm delayMs.
1722
+ // Cite: boss_marli.cpp:101-105 SpellHitTarget → ResetCombatAction(MARLI_CHARGE, 1000).
1723
+ const ir = actorCast?.scriptDev?.ir;
1724
+ const timer = ir?.timers.find((tm) => tm.id === act.timerId);
1725
+ if (actorCast && timer) {
1726
+ for (const slot of actorCast.slots) {
1727
+ if (slot.entry.spellId !== timer.spellId) continue;
1728
+ slot.exhausted = false;
1729
+ slot.readyAt = t + (act.delayMs | 0);
1730
+ }
1731
+ }
1732
+ } else if (act.kind === 'disable_timer') {
1733
+ // Cite: boss_thekal.cpp:365-368 PreventRevive DisableTimer(ACTION_RESSURECTION).
1734
+ const want = (act.creatureEntry ?? (actor.spec.creatureEntry ?? 0)) | 0;
1735
+ // Every enemy SEAT, corpse included: PreventRevive has to reach the schedule of an add
1736
+ // that is already down. Cite: boss_thekal.cpp:365-368.
1737
+ for (const ef of ctx.enemyBlock()) {
1738
+ if (((ef.spec.creatureEntry ?? 0) | 0) !== want) continue;
1739
+ const cs = enemyCastBySide.get(ef.side as EnemySide);
1740
+ const timer = cs?.scriptDev?.ir.timers.find((tm) => tm.id === act.timerId);
1741
+ if (!cs || !timer) continue;
1742
+ for (const slot of cs.slots) {
1743
+ if (slot.entry.spellId !== timer.spellId) continue;
1744
+ slot.exhausted = true;
1745
+ }
1746
+ }
1747
+ } else if (act.kind === 'store_threat') {
1748
+ // Cite: boss_mandokir.cpp:169 m_targetThreat = getThreat(invoker).
1749
+ const tgt = target;
1750
+ if (tgt && actorCast?.scriptDev) {
1751
+ const amount = threatOf(actor)?.getThreat(tgt.side) ?? 0;
1752
+ actorCast.scriptDev.gazeThreat = { side: tgt.side, amount };
1753
+ }
1754
+ } else if (act.kind === 'charge_if_threat_up') {
1755
+ // Cite: boss_mandokir.cpp:176-181 getThreat > m_targetThreat → SPELL_CHARGE_GAZE.
1756
+ const stored = actorCast?.scriptDev?.gazeThreat;
1757
+ if (stored) {
1758
+ const now = threatOf(actor)?.getThreat(stored.side) ?? 0;
1759
+ if (now > stored.amount) {
1760
+ const tgt = bySide(stored.side as Side);
1761
+ const sp = actor.spec.spells.find((s) => s.id === (act.spellId | 0));
1762
+ if (tgt && !dead(tgt) && sp && typeof ctx.applySpell === 'function') {
1763
+ (ctx.applySpell as (src: Fighter, dst: Fighter, spell: typeof sp) => void)(actor, tgt, sp);
1764
+ }
1765
+ }
1766
+ }
1767
+ } else if (act.kind === 'set_unattackable') {
1768
+ // Until-event stand-in (durationIndex 21). Cite: boss_thaddius.cpp:112.
1769
+ const entry = (actor.spec.creatureEntry ?? 0) | 0;
1770
+ if (!allowStoneskinGargoyleStoneForm(entry, actor.spec.movementType)) continue;
1771
+ actor.unattackableUntil = Math.max(actor.unattackableUntil, t + 3_600_000);
1772
+ if (actorCast) {
1773
+ actorCast.eventAi.unitFlags |= UNIT_FLAG.UNINTERACTIBLE;
1774
+ }
1775
+ } else if (act.kind === 'clear_unattackable') {
1776
+ actor.unattackableUntil = 0;
1777
+ if (actorCast) {
1778
+ actorCast.eventAi.unitFlags &= ~UNIT_FLAG.UNINTERACTIBLE;
1779
+ }
1780
+ } else if (act.kind === 'explode_proximity') {
1781
+ // The payload's bake row carries school and mitigation, the AMOUNT is CastCustomSpell's, and
1782
+ // it sits in no kit — so the bake is the fallback. Cite: boss_buru.cpp:269-281.
1783
+ const payload = actor.spec.spells.find((s) => s.id === (act.payloadSpellId | 0))
1784
+ ?? ctx.spellOf?.(act.payloadSpellId | 0)
1785
+ ?? null;
1786
+ const school = payload?.effects.find(
1787
+ (e): e is Extract<typeof e, { kind: 'direct_damage' }> => e.kind === 'direct_damage',
1788
+ )?.school;
1789
+ const maxYd = act.maxYd > 0 ? act.maxYd : 0;
1790
+ if (payload && school && maxYd > 0 && typeof ctx.dealDamage === 'function') {
1791
+ const level = actor.spec.level | 0;
1792
+ const hit = (tgt: Fighter, raw: number): void => {
1793
+ if (!(raw > 0)) return;
1794
+ // dealDamage does not log — every caller pushes its own beat (groundPoolTick.ts:178).
1795
+ const amount = (ctx.dealDamage as NonNullable<typeof ctx.dealDamage>)(
1796
+ tgt, raw, school, level,
1797
+ { attacker: actor, spell: payload, damageType: 'spell' },
1798
+ );
1799
+ push({
1800
+ t, kind: 'spell_hit', actor: actor.side, target: tgt.side,
1801
+ spellId: payload.id, name: payload.name, result: 'hit', amount, school,
1802
+ });
1803
+ };
1804
+ for (const tgt of livingHostilesFor(actor)) {
1805
+ if (dead(tgt)) continue;
1806
+ // An unmodelled distance is the AoE EDGE, never the centre: a missing position must not
1807
+ // invent the 500 the C++ only pays at contact. Cite: cpp:274 GetDistance(DIST_CALC_COMBAT_REACH).
1808
+ const d = distanceBetween(actor, tgt) ?? maxYd;
1809
+ if (d > maxYd) continue;
1810
+ hit(tgt, act.playerBase + ((maxYd - Math.min(d, maxYd)) / maxYd) * act.playerScale);
1811
+ }
1812
+ for (const row of act.creaturePctMaxHealth ?? []) {
1813
+ const want = row.creatureEntry | 0;
1814
+ for (const other of ctx.livingEnemies()) {
1815
+ if (((other.spec.creatureEntry ?? 0) | 0) !== want) continue;
1816
+ const d = distanceBetween(actor, other);
1817
+ if (d != null && d > maxYd) continue;
1818
+ hit(other, Math.floor((other.spec.maxHealth * row.pct) / 100));
1819
+ // ScheduleNewTarget runs on the CREATURE that took the hit, not on the egg.
1820
+ if (row.actions?.length && !dead(other)) applyScriptDevCombatActions(other, row.actions);
1821
+ }
1822
+ }
1823
+ }
1824
+ } else if (act.kind === 'deal_group_abilities') {
1825
+ // std::shuffle then deal pool[i] to self and each living sibling in range, each self-casting
1826
+ // its own. Cite: mob_anubisath_sentinel.cpp:127-131,140-159,183-205.
1827
+ if (actorCast?.scriptDev && actorCast.scriptDev.dealtAbilitySpellId == null) {
1828
+ const entry = (actor.spec.creatureEntry ?? 0) | 0;
1829
+ const group: Fighter[] = [actor];
1830
+ for (const other of ctx.livingEnemies()) {
1831
+ if (other === actor) continue;
1832
+ if (((other.spec.creatureEntry ?? 0) | 0) !== entry) continue;
1833
+ const d = distanceBetween(actor, other);
1834
+ if (d != null && d > act.radiusYd) continue;
1835
+ group.push(other);
1836
+ }
1837
+ const pool = act.spellIds.map((id) => id | 0);
1838
+ for (let i = pool.length - 1; i > 0; i--) {
1839
+ const j = rng.int(0, i);
1840
+ const tmp = pool[i]!;
1841
+ pool[i] = pool[j]!;
1842
+ pool[j] = tmp;
1843
+ }
1844
+ for (let i = 0; i < group.length && i < pool.length; i++) {
1845
+ const member = group[i]!;
1846
+ const st = enemyCastBySide.get(member.side as EnemySide)?.scriptDev;
1847
+ // SetAbility returns early when m_myAbility is already set — never re-deal.
1848
+ if (!st || st.dealtAbilitySpellId != null) continue;
1849
+ st.dealtAbilitySpellId = pool[i]!;
1850
+ applyScriptDevCombatActions(member, [{
1851
+ kind: 'cast_self',
1852
+ spellId: pool[i]!,
1853
+ castFlags: CAST_FLAG.TRIGGERED,
1854
+ cite: act.cite,
1855
+ }]);
1856
+ }
1857
+ }
1858
+ } else if (act.kind === 'damage_in_radius') {
1859
+ // The payload's own bake row carries the amount, the school and the radius — nothing here
1860
+ // names a number. Cite: naxxramas.cpp:1089; spell_template 28433.
1861
+ const sp = actor.spec.spells.find((s) => s.id === (act.spellId | 0))
1862
+ ?? ctx.spellOf?.(act.spellId | 0)
1863
+ ?? null;
1864
+ const hit = sp?.effects.find(
1865
+ (e): e is Extract<typeof e, { kind: 'direct_damage' }> => e.kind === 'direct_damage',
1866
+ );
1867
+ const radius = sp?.radiusYd ?? 0;
1868
+ if (sp && hit && radius > 0 && typeof ctx.dealDamage === 'function') {
1869
+ const level = actor.spec.level | 0;
1870
+ for (const tgt of livingHostilesFor(actor)) {
1871
+ if (dead(tgt)) continue;
1872
+ const d = distanceBetween(actor, tgt);
1873
+ if (d != null && d > radius) continue;
1874
+ const raw = hit.min + rng.int(0, Math.max(0, (hit.max | 0) - (hit.min | 0)));
1875
+ const amount = (ctx.dealDamage as NonNullable<typeof ctx.dealDamage>)(
1876
+ tgt, raw, hit.school, level,
1877
+ { attacker: actor, spell: sp, damageType: 'spell' },
1878
+ );
1879
+ push({
1880
+ t, kind: 'spell_hit', actor: actor.side, target: tgt.side,
1881
+ spellId: sp.id, name: sp.name, result: 'hit', amount, school: hit.school,
1882
+ });
1883
+ }
1884
+ }
1885
+ } else if (act.kind === 'cast_on_random_players') {
1886
+ // The pick is a SET, shuffled then resized — so it is drawn once and every member takes the
1887
+ // spell, not one unit re-rolled per cast. Cite: boss_maexxna.cpp:212-223.
1888
+ const sp = actor.spec.spells.find((s) => s.id === (act.spellId | 0))
1889
+ ?? ctx.spellOf?.(act.spellId | 0)
1890
+ ?? null;
1891
+ if (sp && typeof ctx.applySpell === 'function') {
1892
+ const victim = ctx.hostileTargetOf?.(actor) ?? null;
1893
+ const pool = livingHostilesFor(actor).filter((h) => {
1894
+ if (dead(h)) return false;
1895
+ // SELECT_FLAG_PLAYER: an ally seat is a player-controlled body here.
1896
+ if (!(h.side === 'player' || isAllySide(h.side))) return false;
1897
+ // SELECT_FLAG_SKIP_TANK is the CURRENT victim, not the role.
1898
+ if (act.skipTopThreat && victim != null && h.side === victim.side) return false;
1899
+ return true;
1900
+ });
1901
+ for (let i = pool.length - 1; i > 0; i--) {
1902
+ const j = rng.int(0, i);
1903
+ const tmp = pool[i]!;
1904
+ pool[i] = pool[j]!;
1905
+ pool[j] = tmp;
1906
+ }
1907
+ const take = Math.min(act.count | 0, pool.length);
1908
+ for (let i = 0; i < take; i++) {
1909
+ const tgt = pool[i]!;
1910
+ (ctx.applySpell as (src: Fighter, dst: Fighter, spell: typeof sp) => void)(actor, tgt, sp);
1911
+ push({ t, kind: 'flavor', text: `${tgt.spec.name} is wrapped in ${sp.name}!` });
1912
+ }
1913
+ }
1914
+ } else if (act.kind === 'proxy_cast_on_victim') {
1915
+ // GetClosestCreatureWithEntry is measured from the VICTIM, not from this script.
1916
+ // Cite: boss_mandokir.cpp:209-211.
1917
+ const victim = target;
1918
+ if (victim) {
1919
+ let nearest: Fighter | undefined;
1920
+ let best = act.radiusYd;
1921
+ for (const e of ctx.livingEnemies()) {
1922
+ if (((e.spec.creatureEntry ?? 0) | 0) !== (act.casterEntry | 0)) continue;
1923
+ const d = distanceBetween(victim, e);
1924
+ if (d != null && d <= best) {
1925
+ best = d;
1926
+ nearest = e;
1927
+ }
1928
+ }
1929
+ const sp = nearest
1930
+ ? (nearest.spec.spells.find((s) => s.id === (act.spellId | 0))
1931
+ ?? actor.spec.spells.find((s) => s.id === (act.spellId | 0))
1932
+ ?? ctx.spellOf?.(act.spellId | 0)
1933
+ ?? null)
1934
+ : null;
1935
+ if (nearest && sp && typeof ctx.applySpell === 'function') {
1936
+ (ctx.applySpell as (src: Fighter, dst: Fighter, spell: typeof sp) => void)(nearest, victim, sp);
1937
+ push({ t, kind: 'flavor', text: `${nearest.spec.name} calls ${victim.spec.name} back (${sp.name})!` });
1938
+ }
1939
+ }
1940
+ } else if (act.kind === 'roll_instance_pair') {
1941
+ // Per-INSTANCE: written only while both fields are NOT_STARTED, and the second index is the
1942
+ // first plus urand(1,4) mod 5. Cite: SpellEffects.cpp:998-999; blackwing_lair.cpp:641,650.
1943
+ const lf = act.leftField | 0;
1944
+ const rf = act.rightField | 0;
1945
+ const lVals = act.leftValues.map((v) => v | 0);
1946
+ const rVals = act.rightValues.map((v) => v | 0);
1947
+ const already = (instanceData.get(lf) ?? 0) !== 0 || (instanceData.get(rf) ?? 0) !== 0;
1948
+ if (!already && lVals.length > 0 && rVals.length === lVals.length) {
1949
+ const n = rVals.length;
1950
+ const r = rng.int(0, n - 1);
1951
+ const l = (r + rng.int(1, n - 1)) % n;
1952
+ for (const [field, value] of [[lf, lVals[l]!], [rf, rVals[r]!]] as const) {
1953
+ instanceData.set(field, value);
1954
+ push({ t, kind: 'instance_data', field, value });
1955
+ }
1956
+ }
1957
+ } else if (act.kind === 'shrink_creature_pct') {
1958
+ // A raw SetHealth, not a damage event: no armor, no resist roll, no threat.
1959
+ // Cite: boss_viscidus.cpp:505-521.
1960
+ const want = (act.creatureEntry | 0);
1961
+ const floor = Math.max(1, act.minHealth ?? 1);
1962
+ for (const other of ctx.livingEnemies()) {
1963
+ if (((other.spec.creatureEntry ?? 0) | 0) !== want) continue;
1964
+ const lose = Math.floor((other.spec.maxHealth * act.pct) / 100);
1965
+ if (lose <= 0 || other.hp <= floor + lose) continue;
1966
+ other.hp -= lose;
1967
+ push({
1968
+ t,
1969
+ kind: 'flavor',
1970
+ text: `${other.spec.name} shrinks (${act.pct}% of its bulk).`,
1971
+ });
1972
+ }
1973
+ } else if (act.kind === 'cast_on_cast_target') {
1974
+ // The unit the ROW's cast resolved to; `cast_on_player` would aim at the party leader.
1975
+ // Cite: boss_sapphiron.cpp:265-279 PeriodicIceBolt.
1976
+ const tgt = target;
1977
+ const sp = actor.spec.spells.find((s) => s.id === (act.spellId | 0))
1978
+ ?? ctx.spellOf?.(act.spellId | 0)
1979
+ ?? null;
1980
+ const already = act.auraNotPresent === true && tgt != null
1981
+ && tgt.buffActive.has(act.spellId | 0);
1982
+ if (tgt && !dead(tgt) && sp && !already && typeof ctx.applySpell === 'function') {
1983
+ (ctx.applySpell as (src: Fighter, dst: Fighter, spell: typeof sp) => void)(actor, tgt, sp);
1984
+ push({ t, kind: 'flavor', text: `${tgt.spec.name} is caught in ${sp.name}!` });
1985
+ }
1986
+ } else if (act.kind === 'raise_corpses') {
1987
+ // The corpse list is the enemy BLOCK, not the living view: a dead body keeps its seat, which
1988
+ // is the same reason PreventRevive walks it. Cite: boss_nefarian.cpp:211-222.
1989
+ const want = new Set(act.fromEntries.map((e) => e | 0));
1990
+ const cap = act.maxCount != null && act.maxCount > 0 ? act.maxCount | 0 : Infinity;
1991
+ let raised = 0;
1992
+ for (const corpse of ctx.enemyBlock()) {
1993
+ if (raised >= cap) break;
1994
+ if (!dead(corpse)) continue;
1995
+ if (!want.has((corpse.spec.creatureEntry ?? 0) | 0)) continue;
1996
+ const at = ctx.posOf?.(corpse.side);
1997
+ const add = injectEnemySummon(
1998
+ actor,
1999
+ act.creatureEntry | 0,
2000
+ 0,
2001
+ at != null && Number.isFinite(at.x) && Number.isFinite(at.y) ? { x: at.x, y: at.y } : undefined,
2002
+ );
2003
+ if (add) raised++;
2004
+ }
2005
+ push({
2006
+ t,
2007
+ kind: 'flavor',
2008
+ text: raised > 0
2009
+ ? `${actor.spec.name} raises ${raised} fallen drakonid${raised === 1 ? '' : 's'}!`
2010
+ : `${actor.spec.name} finds no fallen drakonids to raise.`,
2011
+ });
2012
+ } else if (act.kind === 'cast_on_victim_change') {
2013
+ // m_victimGuid != GetVictim() → cast, then store. Cite: boss_kelthuzad.cpp:504-521.
2014
+ const sd = actorCast?.scriptDev;
2015
+ const victim = ctx.hostileTargetOf?.(actor) ?? null;
2016
+ const side = victim && !dead(victim) ? victim.side : null;
2017
+ if (sd && side != null && sd.lastVictimSide !== side) {
2018
+ const first = sd.lastVictimSide === undefined;
2019
+ sd.lastVictimSide = side;
2020
+ // The first poll only STORES the guid; the C++ casts on the next change, not on the first.
2021
+ if (!first) {
2022
+ const sp = actor.spec.spells.find((s) => s.id === (act.spellId | 0))
2023
+ ?? ctx.spellOf?.(act.spellId | 0)
2024
+ ?? null;
2025
+ if (sp && victim && typeof ctx.applySpell === 'function') {
2026
+ (ctx.applySpell as (src: Fighter, dst: Fighter, spell: typeof sp) => void)(actor, victim, sp);
2027
+ push({ t, kind: 'flavor', text: `${actor.spec.name} drains ${victim.spec.name} (${sp.name})!` });
2028
+ }
2029
+ }
2030
+ }
2031
+ } else if (act.kind === 'transfer_group_ability') {
2032
+ // DoTransferAbility: my ability plus Heal Brethren onto every living sibling in range.
2033
+ // Cite: mob_anubisath_sentinel.cpp:163-181; :225-243 SharePowers casts both on the target.
2034
+ const mine = actorCast?.scriptDev?.dealtAbilitySpellId ?? 0;
2035
+ if (mine > 0) {
2036
+ const entry = (actor.spec.creatureEntry ?? 0) | 0;
2037
+ for (const other of ctx.livingEnemies()) {
2038
+ if (other === actor || dead(other)) continue;
2039
+ if (((other.spec.creatureEntry ?? 0) | 0) !== entry) continue;
2040
+ const d = distanceBetween(actor, other);
2041
+ if (d != null && d > act.radiusYd) continue;
2042
+ const heals = act.healSpellId ? [act.healSpellId | 0] : [];
2043
+ for (const id of [mine, ...heals]) {
2044
+ applyScriptDevCombatActions(other, [{
2045
+ kind: 'cast_self',
2046
+ spellId: id,
2047
+ castFlags: CAST_FLAG.TRIGGERED,
2048
+ cite: act.cite,
2049
+ }]);
2050
+ }
2051
+ }
2052
+ }
2053
+ }
2054
+ }
2055
+ }
2056
+
2057
+ const applyScriptDevTimerActions = (ef: Fighter, spellId: number, target?: Fighter): void => {
2058
+ const ir = enemyCastBySide.get(ef.side as EnemySide)?.scriptDev?.ir;
2059
+ if (!ir) return;
2060
+ const entry = (ef.spec.creatureEntry ?? 0) | 0;
2061
+ for (const timer of ir.timers) {
2062
+ const rotate = timer.rotateSpellIds ?? [];
2063
+ // A per-spawn pick fires under a DIFFERENT id than the row declares, so the row's actions have
2064
+ // to match on the pool too. Cite: zulgurubScripts.cpp:61,110-115.
2065
+ const picked = timer.pickPerSpawnSpellIds ?? [];
2066
+ if (timer.spellId !== spellId && !rotate.includes(spellId) && !picked.includes(spellId)) continue;
2067
+ if (timer.creatureEntry != null && timer.creatureEntry !== entry) continue;
2068
+ applyScriptDevCombatActions(ef, timer.actions ?? [], target);
2069
+ }
2070
+ };
2071
+
2072
+ // One-shot EventAI pulses. Nine gates read a context field no polled build ever sets, so each was
2073
+ // permanently false: 177 DB rows, 51 on census creatures. Death already worked this way.
2074
+ const pulseEnemyEvent = (
2075
+ ef: Fighter,
2076
+ eventType: number,
2077
+ patch: Record<string, unknown>,
2078
+ ): void => {
2079
+ const castState = enemyCastBySide.get(ef.side as EnemySide);
2080
+ if (!castState || castState.eventAi.holders.length === 0) return;
2081
+ // Cheap guard: nothing of this type armed means no work at all for the 99% of creatures
2082
+ // that carry none of these rows.
2083
+ if (!castState.eventAi.holders.some((h) => (h.script.eventType | 0) === (eventType | 0))) return;
2084
+ const intents = pulseEventAi(
2085
+ castState.eventAi,
2086
+ eventType,
2087
+ {
2088
+ t: ctx.t,
2089
+ hpPct: Math.floor((ef.hp * 100) / Math.max(1, ef.spec.maxHealth)),
2090
+ inCombat: (threatOf(ef)?.isEmpty() ?? true) === false,
2091
+ ...patch,
2092
+ } as Parameters<typeof pulseEventAi>[2],
2093
+ { rng, summons: ef.spec.ai?.summons, speakerName: ef.spec.name },
2094
+ );
2095
+ for (const i of intents) applyEventAiIntent(ef, i);
2096
+ };
2097
+
2098
+ /** EVENT_T_KILL (5) — this creature killed a player-controlled unit. 25 DB rows. */
2099
+ const notifyEventAiKill = (killer: Fighter, victim?: Fighter): void => {
2100
+ pulseEnemyEvent(killer, EVENT_T.KILL, { justKilled: true });
2101
+ const ir = scriptDevByCreature((killer.spec.creatureEntry ?? 0) | 0);
2102
+ if (ir) {
2103
+ const acts = lifecycleActions(ir, 'onKill');
2104
+ // KilledUnit's own argument: proxy_cast_on_victim measures its search from the BODY.
2105
+ if (acts.length) applyScriptDevCombatActions(killer, acts, victim);
2106
+ }
2107
+ if (((killer.spec.creatureEntry ?? 0) | 0) === 11382 && victim && !isEnemySide(victim.side)) {
2108
+ // The ding every third kill. The chained-spirit revive that sat here is now his IR's
2109
+ // proxy_cast_on_victim, so Ohgan's copy gets it too. Cite: boss_mandokir.cpp:186-204.
2110
+ const n = (mandokirKillCount.get(killer) ?? 0) + 1;
2111
+ if (n >= 3) {
2112
+ mandokirKillCount.set(killer, 0);
2113
+ const ding = killer.spec.spells.find((s) => (s.id | 0) === 24312);
2114
+ if (ding && typeof ctx.applySpell === 'function') ctx.applySpell(killer, killer, ding);
2115
+ applyScriptDevCombatActions(killer, [{
2116
+ kind: 'yell',
2117
+ textId: -1309016,
2118
+ cite: 'boss_mandokir.cpp:194 DoScriptText(SAY_DING_KILL -1309016)',
2119
+ }]);
2120
+ } else {
2121
+ mandokirKillCount.set(killer, n);
2122
+ }
2123
+ }
2124
+ };
2125
+
2126
+ /** EVENT_T_SUMMONED_UNIT (17) / EVENT_T_SUMMONED_JUST_DIED (25). 1 + 9 DB rows. */
2127
+ const notifyEventAiSummoned = (summoner: Fighter, summonedEntry: number, died: boolean): void => {
2128
+ pulseEnemyEvent(
2129
+ summoner,
2130
+ died ? EVENT_T.SUMMONED_JUST_DIED : EVENT_T.SUMMONED_UNIT,
2131
+ { summonedEntry: summonedEntry | 0 },
2132
+ );
2133
+ };
2134
+
2135
+ // CreatureAI::SpellHit, scripted side, running on the unit a spell was DELIVERED to: it silences the
2136
+ // caster, sheds an aura, yells or despawns. The hook had one caller, so no cast in a fight reached it.
2137
+ function runScriptDevSpellHit(tgt: Fighter, caster: Fighter, spellId: number): void {
2138
+ const state = enemyCastBySide.get(tgt.side as EnemySide)?.scriptDev;
2139
+ if (!state || !(state.ir.spellHits?.length)) return;
2140
+ const active = ctx.tickInputs?.activeQuests;
2141
+ const questIncomplete = active
2142
+ ? (state.ir.spellHits.find((h) => h.spellId === (spellId | 0))?.requireQuestIncomplete ?? null)
2143
+ : null;
2144
+ const hook = handleSpellHit(state, spellId | 0, {
2145
+ isPlayerCaster: caster.side === 'player',
2146
+ // The gate asks "is THIS quest incomplete"; answer from the caller's log, else leave it
2147
+ // null so a quest-gated row stays shut rather than firing on an unknown state.
2148
+ questIncomplete:
2149
+ questIncomplete != null && active?.has(questIncomplete) ? questIncomplete : null,
2150
+ });
2151
+ if (!hook.handled) return;
2152
+ for (const line of hook.flavor) push({ t: ctx.t, kind: 'flavor', text: line });
2153
+ if (hook.actions.length) applyScriptDevCombatActions(tgt, hook.actions);
2154
+ }
2155
+
2156
+ /** EVENT_T_SPELL_CAST (37) — this creature finished a cast. 1 DB row. */
2157
+ const notifyEventAiSpellCast = (caster: Fighter, spellId: number): void => {
2158
+ pulseEnemyEvent(caster, EVENT_T.SPELL_CAST, { spellCastId: spellId | 0 });
2159
+ };
2160
+
2161
+ /** EVENT_T_REACHED_HOME (21) — evade finished. 19 DB rows, 7 on dungeon creatures. */
2162
+ const notifyEventAiReachedHome = (ef: Fighter): void => {
2163
+ pulseEnemyEvent(ef, EVENT_T.REACHED_HOME, { reachedHome: true, inCombat: false });
2164
+ for (const [ownerSide, owned] of guardianSidesBySummoner) {
2165
+ if (!owned.has(ef.side)) continue;
2166
+ const owner = ctx.sideFighter?.(ownerSide);
2167
+ const ir = owner ? scriptDevByCreature((owner.spec.creatureEntry ?? 0) | 0) : undefined;
2168
+ const acts = ir ? lifecycleActions(ir, 'summonedReachedHome') : [];
2169
+ if (acts.length) applyScriptDevCombatActions(ef, acts);
2170
+ break;
2171
+ }
2172
+ };
2173
+
2174
+ // EVENT_T_OOC_LOS (10) — MoveInLineOfSight while out of combat. 11 DB rows. The gate itself also requires
2175
+ // `!inCombat`, so the pulse only fires OOC.
2176
+ const notifyEventAiOocLos = (ef: Fighter): void => {
2177
+ pulseEnemyEvent(ef, EVENT_T.OOC_LOS, { oocLos: true, inCombat: false });
2178
+ };
2179
+
2180
+ ctx.notifyEventAiKill = notifyEventAiKill;
2181
+ ctx.notifyEventAiSummoned = notifyEventAiSummoned;
2182
+ ctx.notifyEventAiSpellCast = notifyEventAiSpellCast;
2183
+ ctx.notifyEventAiReachedHome = notifyEventAiReachedHome;
2184
+ ctx.notifyEventAiOocLos = notifyEventAiOocLos;
2185
+ ctx.applyEventAiIntent = applyEventAiIntent;
2186
+ ctx.runEnemyEventAi = runEnemyEventAi;
2187
+ ctx.runEnemyDeathEventAi = runEnemyDeathEventAi;
2188
+ ctx.notifyEventAiDamageTaken = notifyEventAiDamageTaken;
2189
+ ctx.notifyEventAiHealedBy = notifyEventAiHealedBy;
2190
+ ctx.applyScriptDevCombatActions = applyScriptDevCombatActions;
2191
+ ctx.runScriptDevSpellHit = runScriptDevSpellHit;
2192
+ ctx.applyScriptDevTimerActions = applyScriptDevTimerActions;
2193
+
2194
+ const notifyScriptDevHitCount = (
2195
+ target: Fighter,
2196
+ opts: { school: string; melee: boolean },
2197
+ ): void => {
2198
+ if (!isEnemySide(target.side)) return;
2199
+ const state = enemyCastBySide.get(target.side as EnemySide)?.scriptDev;
2200
+ if (!state?.ir.hitCountGates?.length) return;
2201
+ const before = state.phase | 0;
2202
+ const acts = noteScriptDevHitCount(state, {
2203
+ school: opts.school,
2204
+ melee: opts.melee,
2205
+ hasAura: (id) => target.buffActive.has(id),
2206
+ });
2207
+ if ((state.phase | 0) !== before) {
2208
+ const cs = enemyCastBySide.get(target.side as EnemySide);
2209
+ if (cs) {
2210
+ rearmCreatureCastSlotsOnPhase(cs.slots, before, state.phase | 0, ctx.t, rng);
2211
+ cs.eventAi.phase = state.phase | 0;
2212
+ }
2213
+ push({ t: ctx.t, kind: 'phase', actor: target.side, phase: state.phase | 0 });
2214
+ }
2215
+ if (acts.length) applyScriptDevCombatActions(target, acts);
2216
+ };
2217
+ ctx.notifyScriptDevHitCount = notifyScriptDevHitCount;
2218
+ // sessionInject calls this when arming a damageHealMirror creature (after crowdControl).
2219
+ ctx.ensureDamageHealMirrorHooks = ensureMirrorHooks;
2220
+ }