azeroth-shell 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -2
- package/bin/azsh.mjs +19 -0
- package/docs/aifid/eventai-texts.json +4414 -0
- package/docs/dungeons/atlas/atlas-rows-dungeons.json +8989 -0
- package/docs/dungeons/survey/pack-id-migration.json +2418 -0
- package/package.json +35 -4
- package/src/autopilot/bossSkip.ts +26 -0
- package/src/autopilot/combatRotation.ts +824 -0
- package/src/autopilot/config.ts +533 -0
- package/src/autopilot/consumables.ts +58 -0
- package/src/autopilot/encounterStand.ts +121 -0
- package/src/autopilot/objectGatePlan.ts +53 -0
- package/src/autopilot/pathEngage.ts +229 -0
- package/src/autopilot/plan.ts +1643 -0
- package/src/autopilot/questPolicy.ts +274 -0
- package/src/autopilot/routeStep.ts +69 -0
- package/src/autopilot/sessionReport.ts +88 -0
- package/src/autopilot/topOffPower.ts +26 -0
- package/src/autopilot/view.ts +476 -0
- package/src/cli/bootCheck.mjs +34 -0
- package/src/cli/bootData.mjs +57 -0
- package/src/cli/combatLogFilter.ts +153 -0
- package/src/cli/combatLogFormat.ts +613 -0
- package/src/cli/fileRepository.mjs +105 -0
- package/src/cli/gzLoader.mjs +31 -0
- package/src/cli/liveGauntletEscort.ts +978 -0
- package/src/cli/liveStratUndead.ts +305 -0
- package/src/cli/macroHostCombat.ts +545 -0
- package/src/cli/macroHostWorld.ts +330 -0
- package/src/cli/prefs.mjs +31 -0
- package/src/cli/register.mjs +4 -0
- package/src/cli/repl.mjs +693 -0
- package/src/cli/rotationText.mjs +226 -0
- package/src/cli/squad.mjs +105 -0
- package/src/cli/term.mjs +144 -0
- package/src/cli/text.mjs +122 -0
- package/src/cli/tui.mjs +626 -0
- package/src/cli/tuiArena.mjs +109 -0
- package/src/cli/tuiAutopilotSettings.mjs +93 -0
- package/src/cli/tuiBags.mjs +180 -0
- package/src/cli/tuiBars.mjs +133 -0
- package/src/cli/tuiBg.mjs +243 -0
- package/src/cli/tuiBulk.mjs +162 -0
- package/src/cli/tuiCheck.mjs +1882 -0
- package/src/cli/tuiCity.mjs +396 -0
- package/src/cli/tuiCompanionKit.mjs +300 -0
- package/src/cli/tuiCompanions.mjs +471 -0
- package/src/cli/tuiDev.mjs +62 -0
- package/src/cli/tuiDungeon.mjs +606 -0
- package/src/cli/tuiEnchantTarget.mjs +120 -0
- package/src/cli/tuiFight.mjs +577 -0
- package/src/cli/tuiFightPanes.mjs +228 -0
- package/src/cli/tuiFloor.mjs +506 -0
- package/src/cli/tuiGear.mjs +158 -0
- package/src/cli/tuiHonor.mjs +74 -0
- package/src/cli/tuiLoot.mjs +97 -0
- package/src/cli/tuiMacros.mjs +144 -0
- package/src/cli/tuiOutfitter.mjs +115 -0
- package/src/cli/tuiPet.mjs +116 -0
- package/src/cli/tuiProfessions.mjs +800 -0
- package/src/cli/tuiRaid.mjs +127 -0
- package/src/cli/tuiRep.mjs +65 -0
- package/src/cli/tuiSaves.mjs +69 -0
- package/src/cli/tuiSearch.mjs +363 -0
- package/src/cli/tuiSettings.mjs +151 -0
- package/src/cli/tuiSimLab.mjs +80 -0
- package/src/cli/tuiTalents.mjs +60 -0
- package/src/cli/tuiViews.mjs +1178 -0
- package/src/cli/tuiWiki.mjs +181 -0
- package/src/cli/tuiWorld.mjs +361 -0
- package/src/cli/wikiIndex.ts +259 -0
- package/src/cli/wikiSources.ts +355 -0
- package/src/data/alteracValley.ts +176 -0
- package/src/data/appearanceOptions.ts +534 -0
- package/src/data/applyLootChanceSanity.ts +21 -0
- package/src/data/createFlavor.ts +96 -0
- package/src/data/customItems.ts +33 -0
- package/src/data/dataStore.ts +1904 -0
- package/src/data/dungeonGameObjects.ts +84 -0
- package/src/data/dungeonPlateCover.ts +66 -0
- package/src/data/dungeonSpawns.ts +294 -0
- package/src/data/dungeons/alterac-valley.ts +135 -0
- package/src/data/dungeons/apply-dungeon-rooms.ts +29 -0
- package/src/data/dungeons/apply-encounter-outdoors.ts +72 -0
- package/src/data/dungeons/apply-instance-geometry.ts +65 -0
- package/src/data/dungeons/apply-spawn-placement.ts +171 -0
- package/src/data/dungeons/blackfathom-deeps.ts +539 -0
- package/src/data/dungeons/blackrock-depths.ts +1410 -0
- package/src/data/dungeons/blackwing-lair.ts +129 -0
- package/src/data/dungeons/boss-eventai.ts +8 -0
- package/src/data/dungeons/deadmines.ts +410 -0
- package/src/data/dungeons/dire-maul-east.ts +980 -0
- package/src/data/dungeons/dire-maul-north.ts +661 -0
- package/src/data/dungeons/dire-maul-west.ts +785 -0
- package/src/data/dungeons/dungeon-rooms.json +5479 -0
- package/src/data/dungeons/encounter-outdoors.json +359 -0
- package/src/data/dungeons/eventai/blackfathom-deeps.ts +308 -0
- package/src/data/dungeons/eventai/blackrock-depths.ts +1257 -0
- package/src/data/dungeons/eventai/deadmines.ts +264 -0
- package/src/data/dungeons/eventai/dire-maul-east.ts +320 -0
- package/src/data/dungeons/eventai/gnomeregan.ts +213 -0
- package/src/data/dungeons/eventai/helpers.ts +79 -0
- package/src/data/dungeons/eventai/index.ts +133 -0
- package/src/data/dungeons/eventai/lbrs.ts +372 -0
- package/src/data/dungeons/eventai/maraudon.ts +323 -0
- package/src/data/dungeons/eventai/ragefire-chasm.ts +108 -0
- package/src/data/dungeons/eventai/razorfen-downs.ts +207 -0
- package/src/data/dungeons/eventai/razorfen-kraul.ts +326 -0
- package/src/data/dungeons/eventai/scholomance.ts +680 -0
- package/src/data/dungeons/eventai/shadowfang-keep.ts +230 -0
- package/src/data/dungeons/eventai/sm-cathedral.ts +75 -0
- package/src/data/dungeons/eventai/sm-graveyard.ts +150 -0
- package/src/data/dungeons/eventai/sm-library.ts +55 -0
- package/src/data/dungeons/eventai/stratholme-undead.ts +127 -0
- package/src/data/dungeons/eventai/sunken-temple.ts +582 -0
- package/src/data/dungeons/eventai/the-stockade.ts +228 -0
- package/src/data/dungeons/eventai/ubrs.ts +225 -0
- package/src/data/dungeons/eventai/uldaman.ts +355 -0
- package/src/data/dungeons/eventai/wailing-caverns.ts +396 -0
- package/src/data/dungeons/eventai/zul-farrak.ts +591 -0
- package/src/data/dungeons/gnomeregan.ts +909 -0
- package/src/data/dungeons/index.ts +126 -0
- package/src/data/dungeons/instance-geometry.json +5233 -0
- package/src/data/dungeons/key-sources.json +169 -0
- package/src/data/dungeons/lbrs.ts +996 -0
- package/src/data/dungeons/maraudon.ts +786 -0
- package/src/data/dungeons/molten-core.ts +194 -0
- package/src/data/dungeons/naxxramas.ts +173 -0
- package/src/data/dungeons/onyxias-lair.ts +71 -0
- package/src/data/dungeons/pack-from-clusters.ts +1256 -0
- package/src/data/dungeons/ragefire-chasm.ts +238 -0
- package/src/data/dungeons/razorfen-downs.ts +698 -0
- package/src/data/dungeons/razorfen-kraul.ts +766 -0
- package/src/data/dungeons/ruins-of-ahnqiraj.ts +121 -0
- package/src/data/dungeons/scholomance.ts +1076 -0
- package/src/data/dungeons/shadowfang-keep.ts +740 -0
- package/src/data/dungeons/sm-armory.ts +266 -0
- package/src/data/dungeons/sm-cathedral.ts +430 -0
- package/src/data/dungeons/sm-graveyard.ts +482 -0
- package/src/data/dungeons/sm-library.ts +337 -0
- package/src/data/dungeons/stratholme-live.ts +808 -0
- package/src/data/dungeons/stratholme-undead.ts +1164 -0
- package/src/data/dungeons/sunken-temple.ts +931 -0
- package/src/data/dungeons/temple-of-ahnqiraj.ts +119 -0
- package/src/data/dungeons/the-stockade.ts +282 -0
- package/src/data/dungeons/ubrs.ts +1286 -0
- package/src/data/dungeons/uldaman.ts +685 -0
- package/src/data/dungeons/wailing-caverns.ts +356 -0
- package/src/data/dungeons/zul-farrak.ts +686 -0
- package/src/data/dungeons/zul-gurub.ts +142 -0
- package/src/data/dungeons.ts +42 -0
- package/src/data/fixtures.ts +446 -0
- package/src/data/gameobject-data.json.gz +0 -0
- package/src/data/item-data.json.gz +0 -0
- package/src/data/loot-data.json.gz +0 -0
- package/src/data/maps/atlas-approach-maps.json +1425 -0
- package/src/data/maps/atlas-landmarks.json +1711 -0
- package/src/data/maps/dungeon-viewports.json +392 -0
- package/src/data/materializeModDamagePercentTaken.ts +70 -0
- package/src/data/materializeModHealthRegen.ts +89 -0
- package/src/data/materializeSpellReputation.ts +26 -0
- package/src/data/pageTexts.ts +19 -0
- package/src/data/profession-data.json.gz +0 -0
- package/src/data/raid-plans/blackfathom-deeps.json +86 -0
- package/src/data/raid-plans/blackrock-depths.json +419 -0
- package/src/data/raid-plans/blackwing-lair.json +328 -0
- package/src/data/raid-plans/deadmines.json +55 -0
- package/src/data/raid-plans/dire-maul-east.json +94 -0
- package/src/data/raid-plans/dire-maul-north.json +70 -0
- package/src/data/raid-plans/dire-maul-west.json +75 -0
- package/src/data/raid-plans/gnomeregan.json +131 -0
- package/src/data/raid-plans/lbrs.json +176 -0
- package/src/data/raid-plans/maraudon.json +102 -0
- package/src/data/raid-plans/molten-core.json +169 -0
- package/src/data/raid-plans/naxxramas.json +354 -0
- package/src/data/raid-plans/onyxias-lair.json +18 -0
- package/src/data/raid-plans/ragefire-chasm.json +30 -0
- package/src/data/raid-plans/razorfen-downs.json +75 -0
- package/src/data/raid-plans/razorfen-kraul.json +53 -0
- package/src/data/raid-plans/ruins-of-ahnqiraj.json +110 -0
- package/src/data/raid-plans/scholomance.json +170 -0
- package/src/data/raid-plans/shadowfang-keep.json +87 -0
- package/src/data/raid-plans/sm-armory.json +53 -0
- package/src/data/raid-plans/sm-cathedral.json +72 -0
- package/src/data/raid-plans/sm-graveyard.json +63 -0
- package/src/data/raid-plans/sm-library.json +23 -0
- package/src/data/raid-plans/stratholme-live.json +129 -0
- package/src/data/raid-plans/stratholme-undead.json +73 -0
- package/src/data/raid-plans/sunken-temple.json +159 -0
- package/src/data/raid-plans/temple-of-ahnqiraj.json +163 -0
- package/src/data/raid-plans/the-stockade.json +42 -0
- package/src/data/raid-plans/ubrs.json +53 -0
- package/src/data/raid-plans/uldaman.json +131 -0
- package/src/data/raid-plans/wailing-caverns.json +90 -0
- package/src/data/raid-plans/zul-farrak.json +156 -0
- package/src/data/raid-plans/zul-gurub.json +307 -0
- package/src/data/raidInstanceCreatures.mjs +473 -0
- package/src/data/rotations/index.ts +152 -0
- package/src/data/rotations/layout.mjs +72 -0
- package/src/data/rotations/pve/druid/balance.json +754 -0
- package/src/data/rotations/pve/druid/feral-tank.json +2448 -0
- package/src/data/rotations/pve/druid/feral.json +2660 -0
- package/src/data/rotations/pve/druid/restoration.json +424 -0
- package/src/data/rotations/pve/hunter/beast-mastery.json +1288 -0
- package/src/data/rotations/pve/hunter/marksmanship.json +1260 -0
- package/src/data/rotations/pve/hunter/survival.json +1373 -0
- package/src/data/rotations/pve/mage/arcane-leveling.json +1798 -0
- package/src/data/rotations/pve/mage/fire-leveling.json +2003 -0
- package/src/data/rotations/pve/mage/frost-leveling.json +1912 -0
- package/src/data/rotations/pve/paladin/holy.json +755 -0
- package/src/data/rotations/pve/paladin/protection.json +962 -0
- package/src/data/rotations/pve/paladin/retribution.json +924 -0
- package/src/data/rotations/pve/priest/discipline.json +772 -0
- package/src/data/rotations/pve/priest/holy.json +707 -0
- package/src/data/rotations/pve/priest/shadow.json +787 -0
- package/src/data/rotations/pve/rogue/assassination.json +2477 -0
- package/src/data/rotations/pve/rogue/combat.json +2483 -0
- package/src/data/rotations/pve/rogue/subtlety.json +2523 -0
- package/src/data/rotations/pve/shaman/elemental.json +1157 -0
- package/src/data/rotations/pve/shaman/enhancement.json +1085 -0
- package/src/data/rotations/pve/shaman/restoration.json +592 -0
- package/src/data/rotations/pve/warlock/affliction.json +1185 -0
- package/src/data/rotations/pve/warlock/demonology.json +1165 -0
- package/src/data/rotations/pve/warlock/destruction.json +1177 -0
- package/src/data/rotations/pve/warrior/arms.json +3020 -0
- package/src/data/rotations/pve/warrior/fury.json +2996 -0
- package/src/data/rotations/pve/warrior/protection.json +2509 -0
- package/src/data/rotations/pvp/druid/balance.json +593 -0
- package/src/data/rotations/pvp/druid/feral.json +3276 -0
- package/src/data/rotations/pvp/druid/restoration.json +662 -0
- package/src/data/rotations/pvp/hunter/beast-mastery.json +665 -0
- package/src/data/rotations/pvp/hunter/marksmanship.json +2406 -0
- package/src/data/rotations/pvp/hunter/survival.json +635 -0
- package/src/data/rotations/pvp/mage/arcane.json +574 -0
- package/src/data/rotations/pvp/mage/fire.json +570 -0
- package/src/data/rotations/pvp/mage/frost.json +4048 -0
- package/src/data/rotations/pvp/paladin/holy.json +1498 -0
- package/src/data/rotations/pvp/paladin/protection.json +567 -0
- package/src/data/rotations/pvp/paladin/retribution.json +1110 -0
- package/src/data/rotations/pvp/priest/discipline.json +1848 -0
- package/src/data/rotations/pvp/priest/holy.json +6340 -0
- package/src/data/rotations/pvp/priest/shadow.json +2329 -0
- package/src/data/rotations/pvp/rogue/assassination.json +1322 -0
- package/src/data/rotations/pvp/rogue/combat.json +5258 -0
- package/src/data/rotations/pvp/rogue/subtlety.json +586 -0
- package/src/data/rotations/pvp/shaman/elemental.json +1404 -0
- package/src/data/rotations/pvp/shaman/enhancement.json +1277 -0
- package/src/data/rotations/pvp/shaman/restoration.json +2490 -0
- package/src/data/rotations/pvp/warlock/affliction.json +1017 -0
- package/src/data/rotations/pvp/warlock/demonology.json +1503 -0
- package/src/data/rotations/pvp/warlock/destruction.json +638 -0
- package/src/data/rotations/pvp/warrior/arms.json +5665 -0
- package/src/data/rotations/pvp/warrior/fury.json +850 -0
- package/src/data/rotations/pvp/warrior/protection.json +777 -0
- package/src/data/schema/bundle.ts +511 -0
- package/src/data/schema/constants.ts +49 -0
- package/src/data/schema/creature.ts +454 -0
- package/src/data/schema/encounter.ts +155 -0
- package/src/data/schema/faction.ts +48 -0
- package/src/data/schema/item.ts +297 -0
- package/src/data/schema/itemEnchant.ts +105 -0
- package/src/data/schema/npc.ts +44 -0
- package/src/data/schema/primitives.ts +295 -0
- package/src/data/schema/profession.ts +226 -0
- package/src/data/schema/quest.ts +154 -0
- package/src/data/schema/spell.ts +1149 -0
- package/src/data/schema/talent.ts +74 -0
- package/src/data/schema/world.ts +124 -0
- package/src/data/schema.ts +19 -0
- package/src/data/skinNodeGapFill.ts +68 -0
- package/src/data/spell-data.json.gz +0 -0
- package/src/data/spell-script-targets.json +1 -0
- package/src/data/tables/ahItemValues.json.gz +0 -0
- package/src/data/tables/alteracValley.json.gz +0 -0
- package/src/data/tables/areatriggerTeleports.json.gz +0 -0
- package/src/data/tables/botNames.json.gz +0 -0
- package/src/data/tables/botPopulation.json.gz +0 -0
- package/src/data/tables/botSpecs.json.gz +0 -0
- package/src/data/tables/classLevelStats.json.gz +0 -0
- package/src/data/tables/combatConditions.json.gz +0 -0
- package/src/data/tables/combatSkillMeta.json.gz +0 -0
- package/src/data/tables/conditions.json.gz +0 -0
- package/src/data/tables/createInfo.json.gz +0 -0
- package/src/data/tables/creatureAggroGroups.json.gz +0 -0
- package/src/data/tables/creatureClassLevelStats.json.gz +0 -0
- package/src/data/tables/creaturePatrolPacks.json.gz +0 -0
- package/src/data/tables/creatureWeapons.json.gz +0 -0
- package/src/data/tables/creatures.json.gz +0 -0
- package/src/data/tables/dbScripts.json.gz +0 -0
- package/src/data/tables/dungeonAuthoredRoutes.json.gz +0 -0
- package/src/data/tables/dungeonCensus.json.gz +0 -0
- package/src/data/tables/dungeonGameObjects.json.gz +0 -0
- package/src/data/tables/dungeonPacks.json.gz +0 -0
- package/src/data/tables/dungeonPlateCover.json.gz +0 -0
- package/src/data/tables/dungeonSpawns.json.gz +0 -0
- package/src/data/tables/dungeonSurvey.json.gz +0 -0
- package/src/data/tables/durabilityCosts.json.gz +0 -0
- package/src/data/tables/durabilityQuality.json.gz +0 -0
- package/src/data/tables/enchantApplicators.json.gz +0 -0
- package/src/data/tables/entranceYards.json.gz +0 -0
- package/src/data/tables/explorationBasexp.json.gz +0 -0
- package/src/data/tables/factionTemplates.json.gz +0 -0
- package/src/data/tables/factions.json.gz +0 -0
- package/src/data/tables/gameEvents.json.gz +0 -0
- package/src/data/tables/gameObjects.json.gz +0 -0
- package/src/data/tables/gossipMenus.json.gz +0 -0
- package/src/data/tables/graveyards.json.gz +0 -0
- package/src/data/tables/instanceMaps.json.gz +0 -0
- package/src/data/tables/itemEnchantmentTemplates.json.gz +0 -0
- package/src/data/tables/itemEnchants.json.gz +0 -0
- package/src/data/tables/itemRandomProperties.json.gz +0 -0
- package/src/data/tables/itemSets.json.gz +0 -0
- package/src/data/tables/items.json.gz +0 -0
- package/src/data/tables/levelStats.json.gz +0 -0
- package/src/data/tables/locks.json.gz +0 -0
- package/src/data/tables/map-pin-overrides.json.gz +0 -0
- package/src/data/tables/nonCombatTrash.json.gz +0 -0
- package/src/data/tables/npcTexts.json.gz +0 -0
- package/src/data/tables/pageTexts.json.gz +0 -0
- package/src/data/tables/petAbilityTP.json.gz +0 -0
- package/src/data/tables/petGenericSpells.json.gz +0 -0
- package/src/data/tables/petLevelStats.json.gz +0 -0
- package/src/data/tables/playerbot-weightscales.json.gz +0 -0
- package/src/data/tables/pools.json.gz +0 -0
- package/src/data/tables/quests.json.gz +0 -0
- package/src/data/tables/races.json.gz +0 -0
- package/src/data/tables/reputationRanks.json.gz +0 -0
- package/src/data/tables/reputationRates.json.gz +0 -0
- package/src/data/tables/reputationSpillover.json.gz +0 -0
- package/src/data/tables/ridingSpells.json.gz +0 -0
- package/src/data/tables/scriptTexts.json.gz +0 -0
- package/src/data/tables/soundClipVolume.json.gz +0 -0
- package/src/data/tables/spellChain.json.gz +0 -0
- package/src/data/tables/spellTeleports.json.gz +0 -0
- package/src/data/tables/spells.json.gz +0 -0
- package/src/data/tables/talents.json.gz +0 -0
- package/src/data/tables/tavernTriggers.json.gz +0 -0
- package/src/data/tables/taxiNodes.json.gz +0 -0
- package/src/data/tables/taxiRoutes.json.gz +0 -0
- package/src/data/tables/trainers.json.gz +0 -0
- package/src/data/tables/transports.json.gz +0 -0
- package/src/data/tables/travelAnchors.json.gz +0 -0
- package/src/data/tables/travelEdges.json.gz +0 -0
- package/src/data/tables/unitConditions.json.gz +0 -0
- package/src/data/tables/vendors.json.gz +0 -0
- package/src/data/tables/weaponEnchantProcs.json.gz +0 -0
- package/src/data/tables/xp.json.gz +0 -0
- package/src/data/tables/zoneWeather.json.gz +0 -0
- package/src/data/tables/zones.json.gz +0 -0
- package/src/data/world-data.ts +56 -0
- package/src/data/zone-music-sets.json +2751 -0
- package/src/dev/etl/lib/loot-chance-sanity.mjs +180 -0
- package/src/domain/audioQuality.mjs +145 -0
- package/src/domain/bg/ab.ts +433 -0
- package/src/domain/bg/arenaComps.ts +644 -0
- package/src/domain/bg/arenaLab.ts +695 -0
- package/src/domain/bg/arenaPace.ts +31 -0
- package/src/domain/bg/arenaRating.ts +110 -0
- package/src/domain/bg/av.ts +758 -0
- package/src/domain/bg/avEnemyPush.ts +219 -0
- package/src/domain/bg/avField.ts +283 -0
- package/src/domain/bg/avInstanceNodes.ts +197 -0
- package/src/domain/bg/avInstanceScore.ts +123 -0
- package/src/domain/bg/avQuestItems.ts +55 -0
- package/src/domain/bg/avQuests.ts +210 -0
- package/src/domain/bg/avSession.ts +196 -0
- package/src/domain/bg/bgConsumes.ts +269 -0
- package/src/domain/bg/bgPrebuff.ts +153 -0
- package/src/domain/bg/bgRest.ts +91 -0
- package/src/domain/bg/bgRoster.ts +305 -0
- package/src/domain/bg/bgTier.ts +61 -0
- package/src/domain/bg/bgWorld.ts +862 -0
- package/src/domain/bg/match.ts +215 -0
- package/src/domain/bg/strategies.ts +552 -0
- package/src/domain/bg/types.ts +291 -0
- package/src/domain/bg/wsg.ts +264 -0
- package/src/domain/bots/actionProviderSpec.ts +38 -0
- package/src/domain/bots/autoConsumables.ts +285 -0
- package/src/domain/bots/betweenPullRest.ts +239 -0
- package/src/domain/bots/botGear.ts +248 -0
- package/src/domain/bots/botInventory.ts +486 -0
- package/src/domain/bots/botPet.ts +535 -0
- package/src/domain/bots/botPool.ts +291 -0
- package/src/domain/bots/botProfessions.ts +334 -0
- package/src/domain/bots/companionActionBar.ts +209 -0
- package/src/domain/bots/companionHireCost.ts +236 -0
- package/src/domain/bots/companionMissions.ts +582 -0
- package/src/domain/bots/companionPrep.ts +254 -0
- package/src/domain/bots/companionQuests.ts +270 -0
- package/src/domain/bots/companionRep.ts +75 -0
- package/src/domain/bots/companionRotation.ts +708 -0
- package/src/domain/bots/companionXp.ts +66 -0
- package/src/domain/bots/consumableShare.ts +811 -0
- package/src/domain/bots/dpsMate.ts +1072 -0
- package/src/domain/bots/groupCompReport.ts +132 -0
- package/src/domain/bots/healthstoneSupply.ts +198 -0
- package/src/domain/bots/outfitter.ts +1056 -0
- package/src/domain/bots/paladinPrePull.ts +231 -0
- package/src/domain/bots/partyRole.ts +40 -0
- package/src/domain/bots/playerbotEquipCache.ts +412 -0
- package/src/domain/bots/playerbotItemEligibility.ts +60 -0
- package/src/domain/bots/playerbotStatWeight.ts +853 -0
- package/src/domain/bots/playerbotUnavailableItems.ts +147 -0
- package/src/domain/bots/prePullBuffs.ts +219 -0
- package/src/domain/bots/prePullStealth.ts +82 -0
- package/src/domain/bots/runSupply.ts +251 -0
- package/src/domain/bots/runSupplyPrefs.ts +28 -0
- package/src/domain/bots/sendTradeGoods.ts +394 -0
- package/src/domain/bots/soulShardCap.ts +35 -0
- package/src/domain/bots/squad.ts +842 -0
- package/src/domain/bots/squadRun.ts +81 -0
- package/src/domain/character/actionbar.ts +346 -0
- package/src/domain/character/appearance.ts +165 -0
- package/src/domain/character/buildCombatant.ts +1726 -0
- package/src/domain/character/character.ts +356 -0
- package/src/domain/character/characterName.ts +43 -0
- package/src/domain/character/characterRatings.ts +424 -0
- package/src/domain/character/characterSpells.ts +282 -0
- package/src/domain/character/characterStats.ts +1559 -0
- package/src/domain/character/characterTalents.ts +398 -0
- package/src/domain/character/combatSkills.ts +994 -0
- package/src/domain/character/displayForm.ts +72 -0
- package/src/domain/character/formTalentAuras.ts +171 -0
- package/src/domain/character/forms.ts +499 -0
- package/src/domain/character/races.ts +40 -0
- package/src/domain/character/resurrectionSickness.ts +49 -0
- package/src/domain/character/spellbook.ts +169 -0
- package/src/domain/character/stanceBar.ts +354 -0
- package/src/domain/character/stancePrereq.ts +70 -0
- package/src/domain/character/talentBuilds.ts +63 -0
- package/src/domain/character/talentImport.ts +130 -0
- package/src/domain/character/talentLink.ts +123 -0
- package/src/domain/character/talents.ts +201 -0
- package/src/domain/core/conditionSnapshot.ts +92 -0
- package/src/domain/core/hover.ts +14 -0
- package/src/domain/core/ids.ts +58 -0
- package/src/domain/core/internalNameTags.ts +5 -0
- package/src/domain/core/runtimeDataPatches.ts +357 -0
- package/src/domain/creature/aiTargeting.ts +588 -0
- package/src/domain/creature/combatCondition.ts +157 -0
- package/src/domain/creature/creatureAggro.ts +804 -0
- package/src/domain/creature/creatureAi.ts +206 -0
- package/src/domain/creature/creatureAiConstants.ts +661 -0
- package/src/domain/creature/creatureCastList.ts +1007 -0
- package/src/domain/creature/creatureEventAi.ts +1409 -0
- package/src/domain/creature/creatureGroups.ts +144 -0
- package/src/domain/creature/creatureLinking.ts +333 -0
- package/src/domain/creature/creatureScaling.ts +320 -0
- package/src/domain/creature/creatureTrigger.ts +53 -0
- package/src/domain/creature/creatureWander.ts +74 -0
- package/src/domain/creature/eventAiTexts.ts +145 -0
- package/src/domain/creature/faction.ts +287 -0
- package/src/domain/creature/forcedReaction.ts +48 -0
- package/src/domain/creature/outdoorHuntable.ts +74 -0
- package/src/domain/creature/patrolPacks.ts +210 -0
- package/src/domain/creature/reputation.ts +967 -0
- package/src/domain/creature/unitFlags.ts +74 -0
- package/src/domain/creature/worldBosses.ts +393 -0
- package/src/domain/dungeonMusic.mjs +48 -0
- package/src/domain/economy/auction.ts +282 -0
- package/src/domain/economy/auctionBot.ts +1060 -0
- package/src/domain/economy/honorPrice.ts +100 -0
- package/src/domain/economy/mail.ts +958 -0
- package/src/domain/economy/money.ts +42 -0
- package/src/domain/economy/syntheticClassTrainers.ts +62 -0
- package/src/domain/economy/trading.ts +296 -0
- package/src/domain/economy/vendorSellAll.ts +252 -0
- package/src/domain/economy/vendorStock.ts +146 -0
- package/src/domain/items/ammo.ts +817 -0
- package/src/domain/items/bank.ts +162 -0
- package/src/domain/items/consumableFamily.ts +205 -0
- package/src/domain/items/craftUpgrades.ts +83 -0
- package/src/domain/items/createItem.ts +93 -0
- package/src/domain/items/durability.ts +581 -0
- package/src/domain/items/equipment.ts +441 -0
- package/src/domain/items/equipmentHelper.ts +767 -0
- package/src/domain/items/inventory.ts +561 -0
- package/src/domain/items/itemBonding.ts +93 -0
- package/src/domain/items/itemContainer.ts +245 -0
- package/src/domain/items/itemDisplayName.ts +33 -0
- package/src/domain/items/itemInstance.ts +181 -0
- package/src/domain/items/itemRandomEnchantOutcomes.ts +315 -0
- package/src/domain/items/itemRandomProps.ts +77 -0
- package/src/domain/items/itemRestrictions.ts +329 -0
- package/src/domain/items/itemTeaches.ts +82 -0
- package/src/domain/items/itemUseCooldown.ts +52 -0
- package/src/domain/items/itemUseTarget.ts +90 -0
- package/src/domain/items/junkCategories.ts +174 -0
- package/src/domain/items/junkFilter.ts +368 -0
- package/src/domain/items/keyring.ts +131 -0
- package/src/domain/items/loot.ts +221 -0
- package/src/domain/items/mounts.ts +190 -0
- package/src/domain/items/onUsePayload.ts +102 -0
- package/src/domain/items/pendingLoot.ts +107 -0
- package/src/domain/items/relic.ts +105 -0
- package/src/domain/items/rocketBoots.ts +36 -0
- package/src/domain/items/sortContainer.ts +139 -0
- package/src/domain/items/spareContainers.ts +71 -0
- package/src/domain/items/spellReagentIndex.ts +131 -0
- package/src/domain/items/staleConsumables.ts +66 -0
- package/src/domain/items/weaponSpender.ts +110 -0
- package/src/domain/listDungeonIds.mjs +28 -0
- package/src/domain/macro/compose.ts +239 -0
- package/src/domain/macro/conditions.ts +260 -0
- package/src/domain/macro/creatureTypes.ts +40 -0
- package/src/domain/macro/describe.ts +169 -0
- package/src/domain/macro/icon.ts +60 -0
- package/src/domain/macro/index.ts +20 -0
- package/src/domain/macro/normalize.ts +112 -0
- package/src/domain/macro/parse.ts +316 -0
- package/src/domain/macro/plan.ts +288 -0
- package/src/domain/macro/rotation.ts +590 -0
- package/src/domain/macro/rotationEnv.ts +173 -0
- package/src/domain/macro/slots.ts +101 -0
- package/src/domain/macro/snapshot.ts +114 -0
- package/src/domain/macro/templates.ts +446 -0
- package/src/domain/macro/types.ts +70 -0
- package/src/domain/macro/units.ts +93 -0
- package/src/domain/macro/weaponTypes.ts +83 -0
- package/src/domain/nav/cityTeleports.ts +10 -0
- package/src/domain/nav/mounting.ts +113 -0
- package/src/domain/nav/nav.ts +43 -0
- package/src/domain/nav/navCollision.ts +606 -0
- package/src/domain/nav/navOutdoors.ts +91 -0
- package/src/domain/nav/navRegistry.ts +179 -0
- package/src/domain/nav/navRoomGraph.ts +302 -0
- package/src/domain/nav/navTypes.ts +43 -0
- package/src/domain/nav/navmesh.ts +1715 -0
- package/src/domain/nav/speed.ts +200 -0
- package/src/domain/nav/storey.ts +10 -0
- package/src/domain/nav/travel.ts +725 -0
- package/src/domain/nav/travelTypes.ts +75 -0
- package/src/domain/pet/pet.ts +1150 -0
- package/src/domain/pet/petActionBar.ts +180 -0
- package/src/domain/pet/petSpellbook.ts +39 -0
- package/src/domain/pet/petStable.ts +81 -0
- package/src/domain/professions/autoDisenchant.ts +236 -0
- package/src/domain/professions/characterEnchants.ts +176 -0
- package/src/domain/professions/craftCooldown.ts +52 -0
- package/src/domain/professions/enchantApply.ts +62 -0
- package/src/domain/professions/enchantTargets.ts +114 -0
- package/src/domain/professions/enchanting.ts +236 -0
- package/src/domain/professions/gatherFilter.ts +149 -0
- package/src/domain/professions/gearEnchant.ts +137 -0
- package/src/domain/professions/heldTools.ts +29 -0
- package/src/domain/professions/learnAllRecipes.ts +146 -0
- package/src/domain/professions/miningHarvest.ts +69 -0
- package/src/domain/professions/partyDisenchant.ts +294 -0
- package/src/domain/professions/partyWork.ts +83 -0
- package/src/domain/professions/pickpocket.ts +172 -0
- package/src/domain/professions/poisons.ts +240 -0
- package/src/domain/professions/professions.ts +677 -0
- package/src/domain/professions/rareNodes.ts +45 -0
- package/src/domain/professions/skinLoot.ts +27 -0
- package/src/domain/professions/vendorReagents.ts +152 -0
- package/src/domain/professions/weaponImbueHand.ts +171 -0
- package/src/domain/questFarmFilter.mjs +42 -0
- package/src/domain/quests/doQuest.ts +1204 -0
- package/src/domain/quests/gossip.ts +308 -0
- package/src/domain/quests/gossipTalk.ts +298 -0
- package/src/domain/quests/interpreter/canComplete.ts +150 -0
- package/src/domain/quests/interpreter/claim.ts +137 -0
- package/src/domain/quests/interpreter/dbscript/handlers.ts +1371 -0
- package/src/domain/quests/interpreter/dbscript/handlersBatch.ts +1251 -0
- package/src/domain/quests/interpreter/dbscript/hooks.ts +331 -0
- package/src/domain/quests/interpreter/dbscript/index.ts +212 -0
- package/src/domain/quests/interpreter/dbscript/opcodes.ts +257 -0
- package/src/domain/quests/interpreter/dbscript/registry.ts +1194 -0
- package/src/domain/quests/interpreter/dbscript/registryAreatriggerScripts.ts +539 -0
- package/src/domain/quests/interpreter/dbscript/registryAshenvale.ts +553 -0
- package/src/domain/quests/interpreter/dbscript/registryAzshara.ts +272 -0
- package/src/domain/quests/interpreter/dbscript/registryDarkshore.ts +637 -0
- package/src/domain/quests/interpreter/dbscript/registryDesolace.ts +501 -0
- package/src/domain/quests/interpreter/dbscript/registryDustwallowMarsh.ts +735 -0
- package/src/domain/quests/interpreter/dbscript/registryFelwood.ts +493 -0
- package/src/domain/quests/interpreter/dbscript/registryFeralas.ts +485 -0
- package/src/domain/quests/interpreter/dbscript/registryGoScripts.ts +203 -0
- package/src/domain/quests/interpreter/dbscript/registryMoonglade.ts +288 -0
- package/src/domain/quests/interpreter/dbscript/registryNpcProfessions.ts +230 -0
- package/src/domain/quests/interpreter/dbscript/registryNpcsSpecial.ts +545 -0
- package/src/domain/quests/interpreter/dbscript/registryRazorfenDowns.ts +293 -0
- package/src/domain/quests/interpreter/dbscript/registryRazorfenKraul.ts +221 -0
- package/src/domain/quests/interpreter/dbscript/registryRedridge.ts +112 -0
- package/src/domain/quests/interpreter/dbscript/registrySearingGorge.ts +125 -0
- package/src/domain/quests/interpreter/dbscript/registrySiblings.ts +51 -0
- package/src/domain/quests/interpreter/dbscript/registrySilverpineForest.ts +172 -0
- package/src/domain/quests/interpreter/dbscript/registryStonetalonMountains.ts +180 -0
- package/src/domain/quests/interpreter/dbscript/registryStormwindCity.ts +369 -0
- package/src/domain/quests/interpreter/dbscript/registryStranglethornVale.ts +115 -0
- package/src/domain/quests/interpreter/dbscript/registryStratholme.ts +146 -0
- package/src/domain/quests/interpreter/dbscript/registrySunkenTemple.ts +149 -0
- package/src/domain/quests/interpreter/dbscript/registrySwampOfSorrows.ts +163 -0
- package/src/domain/quests/interpreter/dbscript/registryTanaris.ts +318 -0
- package/src/domain/quests/interpreter/dbscript/registryTeldrassil.ts +120 -0
- package/src/domain/quests/interpreter/dbscript/registryTheBarrens.ts +603 -0
- package/src/domain/quests/interpreter/dbscript/registryThousandNeedles.ts +499 -0
- package/src/domain/quests/interpreter/dbscript/registryTirisfalGlades.ts +222 -0
- package/src/domain/quests/interpreter/dbscript/registryUngoroCrater.ts +291 -0
- package/src/domain/quests/interpreter/dbscript/registryWarEffort.ts +773 -0
- package/src/domain/quests/interpreter/dbscript/registryWesternPlaguelands.ts +206 -0
- package/src/domain/quests/interpreter/dbscript/registryWestfall.ts +190 -0
- package/src/domain/quests/interpreter/dbscript/registryWetlands.ts +186 -0
- package/src/domain/quests/interpreter/dbscript/registryWinterspring.ts +235 -0
- package/src/domain/quests/interpreter/dbscript/types.ts +817 -0
- package/src/domain/quests/interpreter/dbscript/vm.ts +171 -0
- package/src/domain/quests/interpreter/diff.ts +349 -0
- package/src/domain/quests/interpreter/fixtures/oracle-questlog-qi2.json +2417 -0
- package/src/domain/quests/interpreter/flags.ts +101 -0
- package/src/domain/quests/interpreter/index.ts +113 -0
- package/src/domain/quests/interpreter/live.ts +178 -0
- package/src/domain/quests/interpreter/types.ts +81 -0
- package/src/domain/quests/interpreter/updaters.ts +412 -0
- package/src/domain/quests/questCorpseUse.ts +155 -0
- package/src/domain/quests/questDialog.ts +43 -0
- package/src/domain/quests/questFight.ts +34 -0
- package/src/domain/quests/questObjectiveLabel.ts +39 -0
- package/src/domain/quests/questRewardChoice.ts +85 -0
- package/src/domain/quests/questScriptCredit.ts +26 -0
- package/src/domain/quests/questScriptItemUse.ts +74 -0
- package/src/domain/quests/questTameRods.ts +79 -0
- package/src/domain/quests/questlog.ts +1545 -0
- package/src/domain/quests/talkDeliver.ts +170 -0
- package/src/domain/raid/globalActionBar.ts +195 -0
- package/src/domain/raid/instanceRoster.ts +194 -0
- package/src/domain/raid/partySummon.ts +198 -0
- package/src/domain/raid/partyVault.ts +320 -0
- package/src/domain/raid/raidBossAbilities.ts +268 -0
- package/src/domain/raid/raidComp.ts +335 -0
- package/src/domain/raid/raidCustomRows.ts +453 -0
- package/src/domain/raid/raidEncounterGear.ts +298 -0
- package/src/domain/raid/raidEncounterTactics.ts +412 -0
- package/src/domain/raid/raidPlan.ts +509 -0
- package/src/domain/raid/raidPlanEdits.ts +98 -0
- package/src/domain/raid/raidPlanPacks.ts +480 -0
- package/src/domain/raid/raidRecovery.ts +183 -0
- package/src/domain/raid/raidRosterTemplate.ts +138 -0
- package/src/domain/rotation/auraSide.ts +68 -0
- package/src/domain/rotation/build.ts +3031 -0
- package/src/domain/rotation/classes/druid.ts +58 -0
- package/src/domain/rotation/classes/hooks.ts +59 -0
- package/src/domain/rotation/classes/hunter.ts +156 -0
- package/src/domain/rotation/classes/index.ts +104 -0
- package/src/domain/rotation/classes/mage.ts +99 -0
- package/src/domain/rotation/classes/paladin.ts +296 -0
- package/src/domain/rotation/classes/priest.ts +94 -0
- package/src/domain/rotation/classes/pullOpener.ts +92 -0
- package/src/domain/rotation/classes/rogue.ts +69 -0
- package/src/domain/rotation/classes/shaman.ts +29 -0
- package/src/domain/rotation/classes/supportSpec.ts +94 -0
- package/src/domain/rotation/classes/warlock.ts +81 -0
- package/src/domain/rotation/classes/warrior.ts +402 -0
- package/src/domain/rotation/classify.ts +1381 -0
- package/src/domain/rotation/condMetricPicker.ts +379 -0
- package/src/domain/rotation/consumables.ts +242 -0
- package/src/domain/rotation/evaluate.ts +1115 -0
- package/src/domain/rotation/finisherLadder.ts +145 -0
- package/src/domain/rotation/invent.ts +938 -0
- package/src/domain/rotation/killOrder.ts +30 -0
- package/src/domain/rotation/openerForm.ts +22 -0
- package/src/domain/rotation/paladinBuffs.ts +568 -0
- package/src/domain/rotation/prebuff.ts +283 -0
- package/src/domain/rotation/raidRowTuning.ts +34 -0
- package/src/domain/rotation/rotation.ts +11 -0
- package/src/domain/rotation/rotationProfiles.ts +180 -0
- package/src/domain/rotation/rotationSuggestions.ts +851 -0
- package/src/domain/rotation/ruleAfterChain.ts +49 -0
- package/src/domain/rotation/spec.ts +70 -0
- package/src/domain/rotation/spellPicker.ts +140 -0
- package/src/domain/rotation/stand.ts +143 -0
- package/src/domain/rotation/types.ts +718 -0
- package/src/domain/run/battlegroundRun.ts +111 -0
- package/src/domain/run/canEnterContext.ts +32 -0
- package/src/domain/run/death.ts +561 -0
- package/src/domain/run/deathRecovery.ts +289 -0
- package/src/domain/run/duel.ts +141 -0
- package/src/domain/run/dungeonRun.ts +344 -0
- package/src/domain/run/floorRest.ts +159 -0
- package/src/domain/run/honor.ts +557 -0
- package/src/domain/run/honorWeek.ts +180 -0
- package/src/domain/run/whenMap.ts +977 -0
- package/src/domain/scriptdev/arathiHighlands.ts +445 -0
- package/src/domain/scriptdev/ashenvale.ts +635 -0
- package/src/domain/scriptdev/ashenvaleHelpers.ts +1215 -0
- package/src/domain/scriptdev/azshara.ts +892 -0
- package/src/domain/scriptdev/blackfathomDeeps.ts +212 -0
- package/src/domain/scriptdev/blackrockDepths.ts +1434 -0
- package/src/domain/scriptdev/blackrockSpire.ts +730 -0
- package/src/domain/scriptdev/blackwingLair.ts +521 -0
- package/src/domain/scriptdev/burningSteppes.ts +62 -0
- package/src/domain/scriptdev/classFilter.ts +43 -0
- package/src/domain/scriptdev/darkshore.ts +817 -0
- package/src/domain/scriptdev/darkshoreHelpers.ts +853 -0
- package/src/domain/scriptdev/deadmines.ts +331 -0
- package/src/domain/scriptdev/desolace.ts +1239 -0
- package/src/domain/scriptdev/direMaul.ts +859 -0
- package/src/domain/scriptdev/durotar.ts +532 -0
- package/src/domain/scriptdev/dustwallowMarsh.ts +758 -0
- package/src/domain/scriptdev/dustwallowMarshHelpers.ts +994 -0
- package/src/domain/scriptdev/easternPlaguelands.ts +21 -0
- package/src/domain/scriptdev/felwood.ts +1055 -0
- package/src/domain/scriptdev/feralas.ts +571 -0
- package/src/domain/scriptdev/feralasHelpers.ts +838 -0
- package/src/domain/scriptdev/gnomeregan.ts +466 -0
- package/src/domain/scriptdev/guards.ts +616 -0
- package/src/domain/scriptdev/hinterlands.ts +693 -0
- package/src/domain/scriptdev/host.ts +1134 -0
- package/src/domain/scriptdev/index.ts +123 -0
- package/src/domain/scriptdev/instanceRaids.ts +136 -0
- package/src/domain/scriptdev/itemScripts.ts +37 -0
- package/src/domain/scriptdev/lochModan.ts +283 -0
- package/src/domain/scriptdev/maraudon.ts +232 -0
- package/src/domain/scriptdev/moltenCore.ts +831 -0
- package/src/domain/scriptdev/moonglade.ts +659 -0
- package/src/domain/scriptdev/moongladeHelpers.ts +933 -0
- package/src/domain/scriptdev/naxxramas.ts +577 -0
- package/src/domain/scriptdev/naxxramasConstruct.ts +364 -0
- package/src/domain/scriptdev/naxxramasFrostwyrm.ts +378 -0
- package/src/domain/scriptdev/naxxramasMilitary.ts +276 -0
- package/src/domain/scriptdev/npcProfessions.ts +1025 -0
- package/src/domain/scriptdev/npcsSpecial.ts +1379 -0
- package/src/domain/scriptdev/onyxiasLair.ts +313 -0
- package/src/domain/scriptdev/orgrimmar.ts +147 -0
- package/src/domain/scriptdev/professionSpecConsumer.ts +481 -0
- package/src/domain/scriptdev/razorfenDowns.ts +468 -0
- package/src/domain/scriptdev/razorfenKraul.ts +410 -0
- package/src/domain/scriptdev/redridgeMountains.ts +163 -0
- package/src/domain/scriptdev/registry.ts +945 -0
- package/src/domain/scriptdev/ruinsOfAhnqiraj.ts +356 -0
- package/src/domain/scriptdev/scarletMonastery.ts +713 -0
- package/src/domain/scriptdev/scholomance.ts +682 -0
- package/src/domain/scriptdev/searingGorge.ts +295 -0
- package/src/domain/scriptdev/shadowfangKeep.ts +181 -0
- package/src/domain/scriptdev/silithus.ts +71 -0
- package/src/domain/scriptdev/silverpineForest.ts +662 -0
- package/src/domain/scriptdev/stonetalonMountains.ts +493 -0
- package/src/domain/scriptdev/stormwindCity.ts +1192 -0
- package/src/domain/scriptdev/stranglethornVale.ts +402 -0
- package/src/domain/scriptdev/stratholme.ts +650 -0
- package/src/domain/scriptdev/stratholmeScripts.ts +505 -0
- package/src/domain/scriptdev/sunkenTemple.ts +736 -0
- package/src/domain/scriptdev/swampOfSorrows.ts +22 -0
- package/src/domain/scriptdev/tanaris.ts +391 -0
- package/src/domain/scriptdev/tanarisHelpers.ts +661 -0
- package/src/domain/scriptdev/teldrassil.ts +143 -0
- package/src/domain/scriptdev/templeOfAhnqiraj.ts +1013 -0
- package/src/domain/scriptdev/theBarrens.ts +709 -0
- package/src/domain/scriptdev/theBarrensHelpers.ts +1026 -0
- package/src/domain/scriptdev/thousandNeedles.ts +426 -0
- package/src/domain/scriptdev/thousandNeedlesHelpers.ts +566 -0
- package/src/domain/scriptdev/tirisfalGlades.ts +533 -0
- package/src/domain/scriptdev/types.ts +1026 -0
- package/src/domain/scriptdev/uldaman.ts +437 -0
- package/src/domain/scriptdev/ungoroCrater.ts +549 -0
- package/src/domain/scriptdev/ungoroCraterHelpers.ts +1006 -0
- package/src/domain/scriptdev/wailingCaverns.ts +700 -0
- package/src/domain/scriptdev/warEffort.ts +371 -0
- package/src/domain/scriptdev/westernPlaguelands.ts +952 -0
- package/src/domain/scriptdev/westfall.ts +767 -0
- package/src/domain/scriptdev/wetlands.ts +660 -0
- package/src/domain/scriptdev/winterspring.ts +482 -0
- package/src/domain/scriptdev/winterspringHelpers.ts +801 -0
- package/src/domain/scriptdev/worldBosses.ts +605 -0
- package/src/domain/scriptdev/zulfarrak.ts +478 -0
- package/src/domain/scriptdev/zulgurub.ts +1152 -0
- package/src/domain/sim/dummySim.ts +408 -0
- package/src/domain/sim/targetDummy.ts +29 -0
- package/src/domain/social/guild.ts +148 -0
- package/src/domain/spells/areaAuraMembership.ts +93 -0
- package/src/domain/spells/breakableStunCc.ts +166 -0
- package/src/domain/spells/cancelAura.ts +80 -0
- package/src/domain/spells/diminishing.ts +167 -0
- package/src/domain/spells/dispel.ts +149 -0
- package/src/domain/spells/dispelMechanic.ts +105 -0
- package/src/domain/spells/equipSpells.ts +247 -0
- package/src/domain/spells/learnOnTargets.ts +260 -0
- package/src/domain/spells/mergeSchoolImmunityFromRaw.ts +66 -0
- package/src/domain/spells/resurrectSpell.ts +124 -0
- package/src/domain/spells/spellPushback.ts +39 -0
- package/src/domain/spells/spellReflect.ts +43 -0
- package/src/domain/spells/stealth.ts +183 -0
- package/src/domain/spells/totem.ts +394 -0
- package/src/domain/spells/worldCast.ts +187 -0
- package/src/domain/world/areaTriggerLocks.ts +177 -0
- package/src/domain/world/dungeonMusic.ts +108 -0
- package/src/domain/world/gameEvents.ts +119 -0
- package/src/domain/world/gameObjects.ts +1030 -0
- package/src/domain/world/goEventFire.ts +234 -0
- package/src/domain/world/goMoTransport.ts +74 -0
- package/src/domain/world/goTransport.ts +81 -0
- package/src/domain/world/hazards.ts +459 -0
- package/src/domain/world/openLock.ts +398 -0
- package/src/domain/world/pools.ts +470 -0
- package/src/domain/world/restArea.ts +37 -0
- package/src/domain/world/weather.ts +201 -0
- package/src/domain/zoneAmbience.mjs +87 -0
- package/src/domain/zoneMusic.mjs +62 -0
- package/src/domain/zoneMusicQueue.mjs +7 -0
- package/src/engine/combat/activeSeal.ts +39 -0
- package/src/engine/combat/aiTargetResolve.ts +64 -0
- package/src/engine/combat/aoeCast.ts +289 -0
- package/src/engine/combat/applyConsumeAura.ts +74 -0
- package/src/engine/combat/applyDemonicSacrifice.ts +70 -0
- package/src/engine/combat/applyDispelMechanic.ts +30 -0
- package/src/engine/combat/applyHealEffect.ts +301 -0
- package/src/engine/combat/applySchoolAbsorb.ts +114 -0
- package/src/engine/combat/applySpell.ts +3169 -0
- package/src/engine/combat/applySpellLate.ts +2026 -0
- package/src/engine/combat/applySpellLateBag.ts +144 -0
- package/src/engine/combat/areaAura.ts +283 -0
- package/src/engine/combat/attackTable.ts +508 -0
- package/src/engine/combat/battleRez.ts +87 -0
- package/src/engine/combat/castAction.ts +36 -0
- package/src/engine/combat/castTargetGates.ts +75 -0
- package/src/engine/combat/castableKit.ts +68 -0
- package/src/engine/combat/ccMirrorUpgrade.ts +39 -0
- package/src/engine/combat/chainFan.ts +210 -0
- package/src/engine/combat/channel.ts +423 -0
- package/src/engine/combat/chaseCloseness.ts +95 -0
- package/src/engine/combat/cmangosCastGates.ts +129 -0
- package/src/engine/combat/combatFlag.ts +77 -0
- package/src/engine/combat/combatGeometry.ts +1329 -0
- package/src/engine/combat/combustion.ts +68 -0
- package/src/engine/combat/context.ts +730 -0
- package/src/engine/combat/createSession.ts +1165 -0
- package/src/engine/combat/creatureLeash.ts +38 -0
- package/src/engine/combat/crowdControl.ts +1259 -0
- package/src/engine/combat/dispelDuty.ts +54 -0
- package/src/engine/combat/dispelReflect.ts +609 -0
- package/src/engine/combat/displaceFloor.ts +113 -0
- package/src/engine/combat/duelBounds.ts +47 -0
- package/src/engine/combat/equippedItemGate.ts +297 -0
- package/src/engine/combat/evadeLifecycle.ts +63 -0
- package/src/engine/combat/eventAi.ts +2220 -0
- package/src/engine/combat/facing.ts +74 -0
- package/src/engine/combat/fearFlee.ts +137 -0
- package/src/engine/combat/fighterFactory.ts +1144 -0
- package/src/engine/combat/forceDespawnAction.ts +51 -0
- package/src/engine/combat/formulas.ts +807 -0
- package/src/engine/combat/friendlyFan.ts +55 -0
- package/src/engine/combat/groundPoolTick.ts +210 -0
- package/src/engine/combat/hazards.ts +114 -0
- package/src/engine/combat/healTargets.ts +226 -0
- package/src/engine/combat/heartbeatResist.ts +49 -0
- package/src/engine/combat/hostileItemUse.ts +75 -0
- package/src/engine/combat/interruptAuras.ts +66 -0
- package/src/engine/combat/itemRescue.ts +49 -0
- package/src/engine/combat/itemUse.ts +260 -0
- package/src/engine/combat/kitById.ts +23 -0
- package/src/engine/combat/knockBack.ts +201 -0
- package/src/engine/combat/leap.ts +89 -0
- package/src/engine/combat/mitigation.ts +587 -0
- package/src/engine/combat/modTaunt.ts +43 -0
- package/src/engine/combat/motionClaim.ts +268 -0
- package/src/engine/combat/motionGate.ts +150 -0
- package/src/engine/combat/motionGenerator.ts +27 -0
- package/src/engine/combat/navSight.ts +138 -0
- package/src/engine/combat/perfSink.ts +28 -0
- package/src/engine/combat/periodicDue.ts +55 -0
- package/src/engine/combat/petAuras.ts +86 -0
- package/src/engine/combat/pickAction.ts +3449 -0
- package/src/engine/combat/powerForm.ts +735 -0
- package/src/engine/combat/powerPools.ts +49 -0
- package/src/engine/combat/predicates.ts +538 -0
- package/src/engine/combat/procsDamage.ts +1823 -0
- package/src/engine/combat/reactiveGates.ts +135 -0
- package/src/engine/combat/roles.ts +389 -0
- package/src/engine/combat/rotationSubject.ts +78 -0
- package/src/engine/combat/ruleAfter.ts +106 -0
- package/src/engine/combat/seedDisplayAuras.ts +203 -0
- package/src/engine/combat/seedOocEffects.ts +317 -0
- package/src/engine/combat/seedSchoolImmunities.ts +45 -0
- package/src/engine/combat/session-types.ts +458 -0
- package/src/engine/combat/sessionApi.ts +1785 -0
- package/src/engine/combat/sessionCharge.ts +124 -0
- package/src/engine/combat/sessionChase.ts +869 -0
- package/src/engine/combat/sessionDeaths.ts +469 -0
- package/src/engine/combat/sessionFocus.ts +506 -0
- package/src/engine/combat/sessionFollow.ts +167 -0
- package/src/engine/combat/sessionGeometry.ts +188 -0
- package/src/engine/combat/sessionHome.ts +75 -0
- package/src/engine/combat/sessionInject.ts +719 -0
- package/src/engine/combat/sessionMechanicMove.ts +555 -0
- package/src/engine/combat/sessionMotion.ts +1553 -0
- package/src/engine/combat/sessionMotionKit.ts +247 -0
- package/src/engine/combat/sessionMoveIntent.ts +49 -0
- package/src/engine/combat/sessionMoveOrder.ts +200 -0
- package/src/engine/combat/sessionPathTo.ts +657 -0
- package/src/engine/combat/sessionPools.ts +688 -0
- package/src/engine/combat/sessionSteer.ts +144 -0
- package/src/engine/combat/sessionStepBack.ts +151 -0
- package/src/engine/combat/sessionStepWrap.ts +116 -0
- package/src/engine/combat/sessionView.ts +442 -0
- package/src/engine/combat/simEffects.ts +32 -0
- package/src/engine/combat/simFacade.ts +620 -0
- package/src/engine/combat/simPump.ts +264 -0
- package/src/engine/combat/simSnapshot.ts +165 -0
- package/src/engine/combat/simThreadPool.ts +35 -0
- package/src/engine/combat/skeramImages.ts +57 -0
- package/src/engine/combat/slowUpdate.ts +178 -0
- package/src/engine/combat/snapToFloor.ts +42 -0
- package/src/engine/combat/speedAura.ts +29 -0
- package/src/engine/combat/spellReagents.ts +71 -0
- package/src/engine/combat/stealthDetect.ts +93 -0
- package/src/engine/combat/stepBackMotion.ts +171 -0
- package/src/engine/combat/stepSession.ts +3102 -0
- package/src/engine/combat/summonCast.ts +243 -0
- package/src/engine/combat/thaddiusPolarity.ts +97 -0
- package/src/engine/combat/threat.ts +388 -0
- package/src/engine/combat/threatOps.ts +272 -0
- package/src/engine/combat/tickWorkerBody.ts +211 -0
- package/src/engine/combat/totemAuras.ts +122 -0
- package/src/engine/combat/triggerPayload.ts +42 -0
- package/src/engine/combat/triggeredCompanion.ts +63 -0
- package/src/engine/combat/types.ts +1897 -0
- package/src/engine/combat/useObjectTrap.ts +140 -0
- package/src/engine/combat/walkSurface.ts +54 -0
- package/src/engine/combat/weapon.ts +1248 -0
- package/src/engine/combat/worldFrame.ts +43 -0
- package/src/engine/combat.solo-golden.json +2242 -0
- package/src/engine/combat.ts +84 -0
- package/src/engine/core/detMath.ts +289 -0
- package/src/engine/core/events.ts +357 -0
- package/src/engine/core/rng.ts +36 -0
- package/src/engine/game/actionBars.ts +124 -0
- package/src/engine/game/activateObjectFromSpell.ts +320 -0
- package/src/engine/game/applyActiveObjectFromDbScript.ts +98 -0
- package/src/engine/game/applyDeathAndSpellScripts.ts +268 -0
- package/src/engine/game/applyDoorFromDbScript.ts +388 -0
- package/src/engine/game/applyEscortFromDbScript.ts +290 -0
- package/src/engine/game/applyGoSpellcasterCast.ts +40 -0
- package/src/engine/game/applyHoverFromDbScript.ts +110 -0
- package/src/engine/game/applyOocResurrect.ts +71 -0
- package/src/engine/game/applySpellDurability.ts +115 -0
- package/src/engine/game/applyTailFromDbScript.ts +478 -0
- package/src/engine/game/applyTerminateFromDbScript.ts +95 -0
- package/src/engine/game/auctionMail.ts +269 -0
- package/src/engine/game/bagOverflow.ts +130 -0
- package/src/engine/game/bank.ts +110 -0
- package/src/engine/game/barAutofill.ts +26 -0
- package/src/engine/game/battleground.ts +369 -0
- package/src/engine/game/combatEnemySpec.ts +216 -0
- package/src/engine/game/combatObjectTraps.ts +167 -0
- package/src/engine/game/combatSeed.ts +320 -0
- package/src/engine/game/combatSession.ts +3241 -0
- package/src/engine/game/combatSessionClock.ts +62 -0
- package/src/engine/game/companionQuestActions.ts +242 -0
- package/src/engine/game/companionTraining.ts +40 -0
- package/src/engine/game/consumables.ts +624 -0
- package/src/engine/game/context.ts +53 -0
- package/src/engine/game/createHealthstone.ts +45 -0
- package/src/engine/game/deathFlow.ts +127 -0
- package/src/engine/game/devGrant.ts +117 -0
- package/src/engine/game/dynamicObject.ts +365 -0
- package/src/engine/game/enchantItem.ts +177 -0
- package/src/engine/game/equipmentHelper.ts +806 -0
- package/src/engine/game/factionState.ts +108 -0
- package/src/engine/game/garmentsOoc.ts +69 -0
- package/src/engine/game/globalActionBar.ts +179 -0
- package/src/engine/game/goInteractQuestCredit.ts +51 -0
- package/src/engine/game/goMoTransportUse.ts +61 -0
- package/src/engine/game/goTransportUse.ts +60 -0
- package/src/engine/game/healthstoneUpkeep.ts +59 -0
- package/src/engine/game/killSkin.ts +124 -0
- package/src/engine/game/learnFromItem.ts +118 -0
- package/src/engine/game/loadReconcile.ts +551 -0
- package/src/engine/game/loot.ts +294 -0
- package/src/engine/game/macros.ts +125 -0
- package/src/engine/game/mail.ts +293 -0
- package/src/engine/game/meetingStoneQueue.ts +75 -0
- package/src/engine/game/meetingStoneUse.ts +54 -0
- package/src/engine/game/newGameState.ts +363 -0
- package/src/engine/game/oocAuraInterrupt.ts +92 -0
- package/src/engine/game/oocCast.ts +241 -0
- package/src/engine/game/oocCastEffects.ts +803 -0
- package/src/engine/game/oocCastMethods.ts +1077 -0
- package/src/engine/game/openLockFromSpell.ts +174 -0
- package/src/engine/game/outdoorPvp.ts +187 -0
- package/src/engine/game/outfitter.ts +644 -0
- package/src/engine/game/paladinBoard.ts +393 -0
- package/src/engine/game/partyAuras.ts +121 -0
- package/src/engine/game/persistCombatAuras.ts +91 -0
- package/src/engine/game/pets.ts +459 -0
- package/src/engine/game/prePullHealthstones.ts +154 -0
- package/src/engine/game/professions.ts +2227 -0
- package/src/engine/game/pruneStaleRuntime.ts +39 -0
- package/src/engine/game/questFightCredit.ts +36 -0
- package/src/engine/game/questItemOnCorpse.ts +166 -0
- package/src/engine/game/questItemOnCreature.ts +103 -0
- package/src/engine/game/questLog.ts +879 -0
- package/src/engine/game/questSpellComplete.ts +21 -0
- package/src/engine/game/questTurnIn.ts +598 -0
- package/src/engine/game/raidParty.ts +32 -0
- package/src/engine/game/reagentBuyIn.ts +39 -0
- package/src/engine/game/regenRest.ts +186 -0
- package/src/engine/game/rotation.ts +425 -0
- package/src/engine/game/runFullDungeon.ts +611 -0
- package/src/engine/game/runSupply.ts +80 -0
- package/src/engine/game/sanitizeDriftedItems.ts +135 -0
- package/src/engine/game/scriptDevGoUse.ts +397 -0
- package/src/engine/game/scriptDevInnkeeper.ts +150 -0
- package/src/engine/game/scriptDevProfessionSpec.ts +99 -0
- package/src/engine/game/sendEventFromSpell.ts +87 -0
- package/src/engine/game/sortContainers.ts +85 -0
- package/src/engine/game/spellReputation.ts +69 -0
- package/src/engine/game/squad/squadDungeonSeating.ts +197 -0
- package/src/engine/game/squadDungeon.ts +4222 -0
- package/src/engine/game/squadQuestShare.ts +260 -0
- package/src/engine/game/summonObjectSlot.ts +69 -0
- package/src/engine/game/summonObjectWild.ts +73 -0
- package/src/engine/game/summoningRitualUse.ts +141 -0
- package/src/engine/game/talents.ts +356 -0
- package/src/engine/game/tamingRodUse.ts +66 -0
- package/src/engine/game/tempSpawnCreature.ts +136 -0
- package/src/engine/game/tickDungeonPatrolScripts.ts +205 -0
- package/src/engine/game/tickTimedQuests.ts +28 -0
- package/src/engine/game/trade.ts +586 -0
- package/src/engine/game/travel.ts +1155 -0
- package/src/engine/game/types.ts +1855 -0
- package/src/engine/game/vendorOffers.ts +244 -0
- package/src/engine/game/weaponImbueItem.ts +281 -0
- package/src/engine/game/worldSession.ts +183 -0
- package/src/engine/game/worldState.ts +166 -0
- package/src/engine/game.ts +968 -0
- package/src/engine/spells/GAP.md +67 -0
- package/src/engine/spells/absorbs.ts +376 -0
- package/src/engine/spells/addTargetTrigger.ts +53 -0
- package/src/engine/spells/auraPerSecondCost.ts +50 -0
- package/src/engine/spells/auraStacking.ts +497 -0
- package/src/engine/spells/cmangos-special-cases-combustion.ts +13 -0
- package/src/engine/spells/cmangos-special-cases-s18.ts +445 -0
- package/src/engine/spells/cmangos-special-cases-s19.ts +324 -0
- package/src/engine/spells/cmangos-special-cases-s20.ts +385 -0
- package/src/engine/spells/cmangos-special-cases-s21.ts +116 -0
- package/src/engine/spells/cmangos-special-cases-s22.ts +24 -0
- package/src/engine/spells/cmangos-special-cases-s23.ts +52 -0
- package/src/engine/spells/cmangos-special-cases.ts +1570 -0
- package/src/engine/spells/conditions.ts +718 -0
- package/src/engine/spells/effects.ts +203 -0
- package/src/engine/spells/fixtures/oracle-effects-lock9a.json +1 -0
- package/src/engine/spells/immunity.ts +942 -0
- package/src/engine/spells/intentEffects.ts +351 -0
- package/src/engine/spells/interpreter/GAPS.md +21 -0
- package/src/engine/spells/interpreter/amount.ts +109 -0
- package/src/engine/spells/interpreter/class-scripts-kit.ts +478 -0
- package/src/engine/spells/interpreter/diff.ts +246 -0
- package/src/engine/spells/interpreter/duration.ts +90 -0
- package/src/engine/spells/interpreter/form-attrs.json +1 -0
- package/src/engine/spells/interpreter/form-attrs.ts +95 -0
- package/src/engine/spells/interpreter/handlers/aura-remove.ts +113 -0
- package/src/engine/spells/interpreter/handlers/auras-control.ts +1547 -0
- package/src/engine/spells/interpreter/handlers/auras-core.ts +1583 -0
- package/src/engine/spells/interpreter/handlers/auras-misc.ts +1774 -0
- package/src/engine/spells/interpreter/handlers/effect-disenchant.ts +17 -0
- package/src/engine/spells/interpreter/handlers/effect-skinning.ts +20 -0
- package/src/engine/spells/interpreter/handlers/effects-action-utility.ts +1455 -0
- package/src/engine/spells/interpreter/handlers/effects-core.ts +432 -0
- package/src/engine/spells/interpreter/handlers/effects-remainder.ts +105 -0
- package/src/engine/spells/interpreter/handlers/effects-summon-item.ts +687 -0
- package/src/engine/spells/interpreter/handlers/mod-increase-energy-percent.ts +36 -0
- package/src/engine/spells/interpreter/handlers/mod-increase-energy.ts +36 -0
- package/src/engine/spells/interpreter/handlers/periodic-trigger-zero.ts +327 -0
- package/src/engine/spells/interpreter/handlers/procs.ts +131 -0
- package/src/engine/spells/interpreter/handlers/registry.ts +975 -0
- package/src/engine/spells/interpreter/handlers/shared.ts +667 -0
- package/src/engine/spells/interpreter/handlers.ts +9 -0
- package/src/engine/spells/interpreter/hunter-kit.ts +202 -0
- package/src/engine/spells/interpreter/index.ts +525 -0
- package/src/engine/spells/interpreter/item-triggers.ts +1121 -0
- package/src/engine/spells/interpreter/lift-consume-energy.ts +37 -0
- package/src/engine/spells/interpreter/mage-portals.ts +37 -0
- package/src/engine/spells/interpreter/materialize-charm.ts +88 -0
- package/src/engine/spells/interpreter/materialize-dispel-mechanic.ts +25 -0
- package/src/engine/spells/interpreter/materialize-rating-buffs.ts +81 -0
- package/src/engine/spells/interpreter/materialize-speed.ts +109 -0
- package/src/engine/spells/interpreter/materialize-spell-magnet.ts +22 -0
- package/src/engine/spells/interpreter/opcodes.ts +789 -0
- package/src/engine/spells/interpreter/paladin-kit.ts +95 -0
- package/src/engine/spells/interpreter/paladin-seals.ts +499 -0
- package/src/engine/spells/interpreter/priest-warlock-kit.ts +135 -0
- package/src/engine/spells/interpreter/proc.ts +443 -0
- package/src/engine/spells/interpreter/registry/helpers.ts +16 -0
- package/src/engine/spells/interpreter/registry/index.ts +216 -0
- package/src/engine/spells/interpreter/registry/packs/class-abilities.ts +304 -0
- package/src/engine/spells/interpreter/registry/packs/dire-maul.ts +44 -0
- package/src/engine/spells/interpreter/registry/packs/dummy-script-tail.ts +1065 -0
- package/src/engine/spells/interpreter/registry/packs/ex-wave-m.ts +70 -0
- package/src/engine/spells/interpreter/registry/packs/life-tap.ts +107 -0
- package/src/engine/spells/interpreter/registry/packs/paladin.ts +246 -0
- package/src/engine/spells/interpreter/registry/packs/raid-script-summons.ts +26 -0
- package/src/engine/spells/interpreter/registry/packs/raid-threat-wipe.ts +22 -0
- package/src/engine/spells/interpreter/registry/packs/shaman.ts +214 -0
- package/src/engine/spells/interpreter/registry/packs/si4-dummy-generic-a.ts +269 -0
- package/src/engine/spells/interpreter/registry/packs/si4-dummy-generic-b.ts +370 -0
- package/src/engine/spells/interpreter/registry/packs/si4-dummy-generic-c.ts +172 -0
- package/src/engine/spells/interpreter/registry/packs/si4-dummy-hunter.ts +60 -0
- package/src/engine/spells/interpreter/registry/packs/si4-dummy-warrior.ts +31 -0
- package/src/engine/spells/interpreter/registry/packs/si4-script-generic-a.ts +114 -0
- package/src/engine/spells/interpreter/registry/packs/si4-script-generic-b.ts +249 -0
- package/src/engine/spells/interpreter/registry/packs/si4-script-generic-c.ts +233 -0
- package/src/engine/spells/interpreter/registry/types.ts +48 -0
- package/src/engine/spells/interpreter/talent-scripted.ts +329 -0
- package/src/engine/spells/interpreter/targeting.ts +90 -0
- package/src/engine/spells/interpreter/types-intent.ts +1501 -0
- package/src/engine/spells/interpreter/types.ts +419 -0
- package/src/engine/spells/interpreter/warrior-druid-kit.ts +314 -0
- package/src/engine/spells/interpreter/weapon-imbue-kit.ts +384 -0
- package/src/engine/spells/interrupt.ts +217 -0
- package/src/engine/spells/meleeAttackReset.ts +60 -0
- package/src/engine/spells/oocTriggerSpell.ts +49 -0
- package/src/engine/spells/overrideClassScripts.ts +319 -0
- package/src/engine/spells/parryHaste.ts +20 -0
- package/src/engine/spells/procs.ts +300 -0
- package/src/engine/spells/recovery.ts +203 -0
- package/src/engine/spells/scriptTargetDest.ts +76 -0
- package/src/engine/spells/scriptTargets.ts +85 -0
- package/src/engine/spells/singleTargetAura.ts +82 -0
- package/src/engine/spells/specialCasePayloads.ts +57 -0
- package/src/engine/spells/spellAttributes.ts +249 -0
- package/src/engine/spells/spellBonus.ts +143 -0
- package/src/engine/spells/spellModIntents.ts +419 -0
- package/src/engine/spells/spellMods.ts +283 -0
- package/src/engine/spells/spellRadius.ts +15 -0
- package/src/engine/spells/trapObjects.ts +26 -0
- package/src/json.d.ts +7 -0
- package/src/persistence/memoryRepository.ts +23 -0
- package/src/persistence/saveBackups.ts +162 -0
- package/src/persistence/saveFile.ts +68 -0
- package/src/persistence/saveRepository.ts +11 -0
- package/src/persistence/saveShape.ts +668 -0
- package/src/progression/leveling.ts +89 -0
- package/src/progression/rest.ts +1434 -0
- package/src/progression/training.ts +226 -0
- package/src/progression/xpGain.ts +369 -0
- package/src/quests/quests.ts +1580 -0
- package/src/world/aoePull.ts +198 -0
- package/src/world/areaTableDbc.ts +39 -0
- package/src/world/areaTableMapIdPairs.ts +141 -0
- package/src/world/bakeSlug.ts +43 -0
- package/src/world/continuousDungeon.ts +1250 -0
- package/src/world/dungeonBakeProjection.ts +190 -0
- package/src/world/dungeonMapDensity.ts +342 -0
- package/src/world/dungeonMapProjection.ts +135 -0
- package/src/world/dungeonNav.ts +904 -0
- package/src/world/dungeonNpcSim.ts +147 -0
- package/src/world/dungeonObjectives.ts +234 -0
- package/src/world/dungeonPreset.ts +35 -0
- package/src/world/dungeonRoute.ts +311 -0
- package/src/world/dungeonRouteGeometry.ts +732 -0
- package/src/world/dungeonRouteOrder.ts +1037 -0
- package/src/world/dungeonWingRooms.ts +426 -0
- package/src/world/escort.ts +296 -0
- package/src/world/goPoolRoll.ts +75 -0
- package/src/world/instanceObjects.ts +785 -0
- package/src/world/instances.ts +1572 -0
- package/src/world/meetingStones.ts +56 -0
- package/src/world/navmeshFloors.ts +145 -0
- package/src/world/nearGrid.ts +167 -0
- package/src/world/packBudget.ts +613 -0
- package/src/world/rareRoll.ts +335 -0
- package/src/world/routeAuthoring.ts +132 -0
- package/src/world/routeEdit.ts +209 -0
- package/src/world/world.ts +812 -0
- package/src/world/zoneHazards.ts +47 -0
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
// Peeled from createSession (createSession.md P10) — pattern (D).
|
|
2
|
+
// Public CombatSession surface (return object of createSession).
|
|
3
|
+
|
|
4
|
+
import type { CombatContext } from './context.ts';
|
|
5
|
+
import { AURA_INTERRUPT } from '../../domain/spells/stealth.ts';
|
|
6
|
+
import { dispelClaimedByOtherSeat } from './dispelDuty.ts';
|
|
7
|
+
import type {
|
|
8
|
+
AbilityStatus,
|
|
9
|
+
AllyActionProvider,
|
|
10
|
+
CombatAction,
|
|
11
|
+
CombatantSpec,
|
|
12
|
+
CombatItemStatus,
|
|
13
|
+
CombatResult,
|
|
14
|
+
CombatSession,
|
|
15
|
+
CombatantView,
|
|
16
|
+
Fighter,
|
|
17
|
+
GroupRole,
|
|
18
|
+
SimTickInputs,
|
|
19
|
+
TotemView,
|
|
20
|
+
TrapView,
|
|
21
|
+
} from './types.ts';
|
|
22
|
+
import type { QueuedCast } from './session-types.ts';
|
|
23
|
+
import { resolveStandYards } from '../../domain/rotation/stand.ts';
|
|
24
|
+
import type { SpellDef, TotemSlot } from '../../data/schema.ts';
|
|
25
|
+
import type { CombatEvent, Side, TotemSide, EnemySide } from '../core/events.ts';
|
|
26
|
+
import { isAllySide, isEnemySide, isDuelistSide, sideForTotemSlot } from '../core/events.ts';
|
|
27
|
+
import type { CombatantId } from './threat.ts';
|
|
28
|
+
import type { CombatPosYd, PullGeometryAnchor } from './combatGeometry.ts';
|
|
29
|
+
import { fillPoseFrame, POSE_STRIDE, type PoseFiller, type SimPoseFrame } from './simSnapshot.ts';
|
|
30
|
+
import { isInSpellRange, BASE_RUN_SPEED, MELEE_REACH_YD, MELEE_PARK_SLACK_YD } from './combatGeometry.ts';
|
|
31
|
+
import { AURA_INCREASE_MOUNTED_SPEED } from '../../domain/nav/speed.ts';
|
|
32
|
+
import type { MotionGateReport } from './motionGate.ts';
|
|
33
|
+
import { detHypot } from '../core/detMath.ts';
|
|
34
|
+
import { channelInterruptCtx, interruptChannel } from './channel.ts';
|
|
35
|
+
import { wakeForDecision } from './slowUpdate.ts';
|
|
36
|
+
import { CORRIDOR_STAND_SHORT_YD, standShortYdFor } from './sessionPathTo.ts';
|
|
37
|
+
import { isFormSpell, resolveFormCast } from '../../domain/character/forms.ts';
|
|
38
|
+
import { SPELL_ATTR_EX2, SPELL_ATTR_EX4, hasAttrEx2, hasAttrEx4 } from '../spells/spellAttributes.ts';
|
|
39
|
+
import {
|
|
40
|
+
isHostileSpellDef,
|
|
41
|
+
spellNeedsRangeGate,
|
|
42
|
+
spellGateRangeMaxYd,
|
|
43
|
+
spellGateRangeMinYd,
|
|
44
|
+
spellFoeDirectedGeometry,
|
|
45
|
+
spellNeedsLosGate,
|
|
46
|
+
spellTargetTypeAllowed,
|
|
47
|
+
isNonCastableTalentProc,
|
|
48
|
+
isSelfCenteredAoe,
|
|
49
|
+
focusImmunityBlocksSpell,
|
|
50
|
+
} from './predicates.ts';
|
|
51
|
+
import { isGroundTargetedMulti } from '../../domain/rotation/classify.ts';
|
|
52
|
+
import { reactiveCastGate } from './reactiveGates.ts';
|
|
53
|
+
import { isPlayerCharacterTarget, targetCastGate } from './castTargetGates.ts';
|
|
54
|
+
import { playerEquippedItemRefuseReason } from './equippedItemGate.ts';
|
|
55
|
+
import { roleOf } from './roles.ts';
|
|
56
|
+
import { SPELL_DAMAGE_CLASS } from './mitigation.ts';
|
|
57
|
+
import { canPaySpellReagents } from './spellReagents.ts';
|
|
58
|
+
import { isPureDispelSpell, firstDispelEffect } from '../../domain/spells/dispel.ts';
|
|
59
|
+
import { navSightClear } from './navSight.ts';
|
|
60
|
+
import { fighterIsInCombat, maybeAggroZonePulse } from './combatFlag.ts';
|
|
61
|
+
import { applyRuleAfter, armRuleAfter, consumeFollowUp, isStepBackWalking } from './ruleAfter.ts';
|
|
62
|
+
import type { RuleAfter } from '../../domain/rotation/rotation.ts';
|
|
63
|
+
import { totemGrantedAuras } from './totemAuras.ts';
|
|
64
|
+
import { resetCreatureCastSlotsOnEvade } from '../../domain/creature/creatureCastList.ts';
|
|
65
|
+
import { runEvadeLifecycle } from './evadeLifecycle.ts';
|
|
66
|
+
import { HUNTER_TRAP_RADIUS_YD, isHunterTrapArmProc } from '../spells/interpreter/hunter-kit.ts';
|
|
67
|
+
import { spellAllowedAtLocation } from '../spells/spellAttributes.ts';
|
|
68
|
+
import { effectiveSpeeds } from '../../domain/nav/speed.ts';
|
|
69
|
+
import {
|
|
70
|
+
isPacifyPrevented, isSilencePrevented, pruneInterruptClaims,
|
|
71
|
+
} from '../spells/interrupt.ts';
|
|
72
|
+
import { HOME_DESPAWN_DISTANCE_SQ } from '../../domain/creature/creatureAiConstants.ts';
|
|
73
|
+
|
|
74
|
+
export type SessionApiHooks = {
|
|
75
|
+
dead: (f: Fighter) => boolean;
|
|
76
|
+
step: (action?: CombatAction, inputs?: SimTickInputs) => CombatEvent[];
|
|
77
|
+
result: () => CombatResult;
|
|
78
|
+
memoView: (f: Fighter) => CombatantView;
|
|
79
|
+
invalidateViewMemo: () => void;
|
|
80
|
+
enemiesView: () => CombatantView[];
|
|
81
|
+
alliesView: () => CombatantView[];
|
|
82
|
+
finalize: () => void;
|
|
83
|
+
totemOwnerMeta: Map<TotemSide, { ownerSide: Side; name: string }>;
|
|
84
|
+
unitMoveOrders: Map<string, CombatPosYd[]>;
|
|
85
|
+
unitMoveIntents: Map<string, CombatPosYd>;
|
|
86
|
+
unitManualMotion: Set<string>;
|
|
87
|
+
unitStance: Map<string, 'retaliate' | 'passive' | 'hold' | 'patrol'>;
|
|
88
|
+
unitPatrolLoop: Map<string, CombatPosYd[]>;
|
|
89
|
+
unitAnchors: Map<string, CombatPosYd>;
|
|
90
|
+
unitCastOrders: Map<Side, QueuedCast[]>;
|
|
91
|
+
clearManualMotion: (side: string) => void;
|
|
92
|
+
cancelCastForMove: (unit: Fighter) => void;
|
|
93
|
+
/** Optional: pull-detect stealth at session build (called once by createSession before return). */
|
|
94
|
+
stealthStrengthOf?: (f: Fighter) => number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// A cooldown-on-event entry that has not started holds a permanent sentinel, so its remaining reads as
|
|
98
|
+
// infinite. Every reader wants a finite number, and the honest one is the duration that will run.
|
|
99
|
+
function displayCooldownMs(unit: Fighter, spellId: number, remaining: number): number {
|
|
100
|
+
if (Number.isFinite(remaining)) return remaining;
|
|
101
|
+
return unit.deferredCooldowns?.get(spellId) ?? 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildCombatSessionApi(ctx: CombatContext, hooks: SessionApiHooks): CombatSession {
|
|
105
|
+
const {
|
|
106
|
+
dead, step, result, memoView, invalidateViewMemo, enemiesView, alliesView, finalize,
|
|
107
|
+
totemOwnerMeta, unitMoveOrders, unitMoveIntents, unitManualMotion, unitStance,
|
|
108
|
+
unitPatrolLoop, unitAnchors, unitCastOrders,
|
|
109
|
+
clearManualMotion, cancelCastForMove,
|
|
110
|
+
} = hooks;
|
|
111
|
+
|
|
112
|
+
const {
|
|
113
|
+
player, pet, allies, enemies, enemy, totems, effects, cfg,
|
|
114
|
+
playerItems, itemCd, deathEmitted, instanceData, allyManualTarget,
|
|
115
|
+
} = ctx;
|
|
116
|
+
|
|
117
|
+
// WHO IS IN THIS SEAT — asked of the side-to-unit store, never by scanning a seat array. The map has
|
|
118
|
+
// been the storage since the axis flip; the arrays hold ORDER. The faction guard stays in each helper.
|
|
119
|
+
const allyUnitOfSide = (side: string): Fighter | null =>
|
|
120
|
+
(isAllySide(side) ? ctx.sideFighter?.(side) ?? null : null);
|
|
121
|
+
const enemyUnitOfSide = (side: string): Fighter | null =>
|
|
122
|
+
(isEnemySide(side) ? ctx.sideFighter?.(side) ?? null : null);
|
|
123
|
+
/** Player, its pet, or an ally. Null for an enemy or a totem - the party-command seats. */
|
|
124
|
+
const partyUnitOfSide = (side: string): Fighter | null => {
|
|
125
|
+
if (side === 'player') return player;
|
|
126
|
+
if (side === 'pet') return pet;
|
|
127
|
+
return allyUnitOfSide(side);
|
|
128
|
+
};
|
|
129
|
+
/** The party seats plus enemy seats. Totems stay out (5 hp summons, not revivable bodies). */
|
|
130
|
+
const fightingUnitOfSide = (side: string): Fighter | null =>
|
|
131
|
+
partyUnitOfSide(side) ?? enemyUnitOfSide(side);
|
|
132
|
+
// The Fighter for a side id, or null when no seat holds it. The store misses to the primary enemy or
|
|
133
|
+
// the PLAYER rather than throwing, so the old catch arm was dead and cancelled the wrong cast.
|
|
134
|
+
const fighterOfSide = (side: CombatantId | Side | string): Fighter | null =>
|
|
135
|
+
ctx.sideFighter?.(side as Side) ?? null;
|
|
136
|
+
|
|
137
|
+
const ensurePlayerFocus = ctx.ensurePlayerFocus!;
|
|
138
|
+
const foeOf = ctx.foeOf!;
|
|
139
|
+
const hostileTargetOf = ctx.hostileTargetOf!;
|
|
140
|
+
const nearestPartyInAcquire = ctx.nearestPartyInAcquire!;
|
|
141
|
+
const posOf = ctx.posOf;
|
|
142
|
+
const setPos = ctx.setPos;
|
|
143
|
+
const placeEnemyAt = ctx.placeEnemyAt!;
|
|
144
|
+
const snapToMesh = ctx.snapToMesh!;
|
|
145
|
+
const distanceBetween = ctx.distanceBetween;
|
|
146
|
+
const livingEnemies = ctx.livingEnemies;
|
|
147
|
+
const isUnattackable = ctx.isUnattackable;
|
|
148
|
+
const isCharmed = ctx.isCharmed;
|
|
149
|
+
const threatOf = ctx.threatOf;
|
|
150
|
+
const bySide = ctx.bySide;
|
|
151
|
+
const injectEnemySpec = ctx.injectEnemySpec!;
|
|
152
|
+
const castOnTargets = ctx.castOnTargets!;
|
|
153
|
+
const useItem = ctx.useItem!;
|
|
154
|
+
const resolveItemTarget = ctx.resolveItemTarget!;
|
|
155
|
+
const comboOn = ctx.comboOn!;
|
|
156
|
+
let abilitiesHold: AbilityStatus[] | null = null;
|
|
157
|
+
let abilitiesHoldT = -1;
|
|
158
|
+
let abilitiesHoldGcd = -1;
|
|
159
|
+
let abilitiesHoldCast: unknown = null;
|
|
160
|
+
let abilitiesHoldChan: unknown = null;
|
|
161
|
+
let abilitiesHoldForm = 0;
|
|
162
|
+
let abilitiesHoldFocus = '';
|
|
163
|
+
let abilitiesHoldSwing = 0;
|
|
164
|
+
let abilitiesHoldPower = 0;
|
|
165
|
+
let abilitiesHoldCc = 0;
|
|
166
|
+
let allyAbilitiesHold: AbilityStatus[] | null = null;
|
|
167
|
+
let allyAbilitiesHoldKey = '';
|
|
168
|
+
// The loss-of-control mask the strip greys on and the memo keys off. One expression, two readers.
|
|
169
|
+
const ccMaskOf = (u: Fighter): number =>
|
|
170
|
+
(u.stunnedUntil > ctx.t ? 1 : 0)
|
|
171
|
+
| (u.fearedUntil > ctx.t ? 2 : 0)
|
|
172
|
+
| (u.incapUntil > ctx.t ? 4 : 0)
|
|
173
|
+
| (u.silencedUntil > ctx.t ? 8 : 0)
|
|
174
|
+
| (u.disarmedUntil > ctx.t ? 16 : 0)
|
|
175
|
+
| (u.pacifiedUntil > ctx.t ? 32 : 0);
|
|
176
|
+
|
|
177
|
+
// EVERY CheckCast gate the action bar shows, for one unit against one focus. `abilities()` is this
|
|
178
|
+
// plus the player memo; `abilitiesFor` is the same answer for a seat that is not the lead.
|
|
179
|
+
const abilityStatusesFor = (unit: Fighter, focus: Fighter): AbilityStatus[] => {
|
|
180
|
+
const cc = ccMaskOf(unit);
|
|
181
|
+
const underCc = cc !== 0;
|
|
182
|
+
const casterPos = posOf(unit.side);
|
|
183
|
+
const focusPos = posOf(focus.side);
|
|
184
|
+
const losOk = !cfg.navLos || navSightClear(cfg, casterPos, focusPos);
|
|
185
|
+
const livingN = (unit === player ? livingEnemies() : ctx.livingHostilesFor(unit)).length;
|
|
186
|
+
const combo = comboOn(unit, focus);
|
|
187
|
+
const list = unit.spec.spells
|
|
188
|
+
.filter((spell) => !(ctx.isTriggeredCompanion as (f: Fighter, s: SpellDef) => boolean)(unit, spell) && !isNonCastableTalentProc(spell))
|
|
189
|
+
.map((spell) => {
|
|
190
|
+
const ownRem = Math.max(0, (unit.cooldowns.get(spell.id) ?? 0) - ctx.t);
|
|
191
|
+
let catRem = 0;
|
|
192
|
+
if (spell.category != null) {
|
|
193
|
+
catRem = Math.max(0, (unit.categoryCooldowns.get(spell.category) ?? 0) - ctx.t);
|
|
194
|
+
}
|
|
195
|
+
const cdRem = displayCooldownMs(unit, spell.id, Math.max(ownRem, catRem));
|
|
196
|
+
const onSwing = !!spell.onNextSwing;
|
|
197
|
+
const armed = onSwing && unit.pendingSwing?.id === spell.id;
|
|
198
|
+
let usable = true; let reason: string | undefined;
|
|
199
|
+
// Breaker usable only under controls it cleanses (WotF under fear, not stun). Cite: KI-741.
|
|
200
|
+
// Skip the effects walk when no CC is on the unit (Firefox 2026-08-23 ZG: isBreaker 30ms self).
|
|
201
|
+
const breaker = underCc && (ctx.isBreaker as (s: SpellDef, f?: Fighter) => boolean)(spell, unit);
|
|
202
|
+
// Loss of control outranks every other reason: hard control greys the whole bar, silence greys the
|
|
203
|
+
// spells it prevents, pacify greys its own, and disarm greys weapon strikes.
|
|
204
|
+
if (!breaker && unit.stunnedUntil > ctx.t) { usable = false; reason = 'stunned'; }
|
|
205
|
+
else if (!breaker && unit.fearedUntil > ctx.t) { usable = false; reason = 'feared'; }
|
|
206
|
+
else if (!breaker && unit.incapUntil > ctx.t) { usable = false; reason = 'incapacitated'; }
|
|
207
|
+
else if (!breaker && unit.silencedUntil > ctx.t && isSilencePrevented(spell)) { usable = false; reason = 'silenced'; }
|
|
208
|
+
else if (!breaker && unit.disarmedUntil > ctx.t && (ctx.isWeaponAction as (s: SpellDef) => boolean)(spell)) { usable = false; reason = 'disarmed'; }
|
|
209
|
+
else if (!breaker && unit.pacifiedUntil > ctx.t && isPacifyPrevented(spell)) { usable = false; reason = 'pacified'; }
|
|
210
|
+
else if (!breaker && (ctx.schoolLocked as (f: Fighter, s: SpellDef) => boolean)(unit, spell)) { usable = false; reason = 'school_lockout'; }
|
|
211
|
+
else if (armed) { usable = false; reason = 'armed'; } // this strike is already waiting for your swing
|
|
212
|
+
else if (onSwing && unit.pendingSwing) { usable = false; reason = 'armed'; } // a different on-next-swing is queued
|
|
213
|
+
// A next-swing arm lands in CURRENT_MELEE_SPELL, which breaks nothing and nothing breaks.
|
|
214
|
+
// Cite: Unit.cpp:4207-4272 SetCurrentCastedSpell default case.
|
|
215
|
+
else if (!onSwing && unit.castingSpell && !hasAttrEx4(spell, SPELL_ATTR_EX4.ALLOW_CAST_WHILE_CASTING)) { usable = false; reason = 'casting'; }
|
|
216
|
+
// A channel OCCUPIES the caster for its whole duration. Beginning one sets no cast bar, so nothing
|
|
217
|
+
// here blocked the next cast and a channel was spammable while delivering one tick.
|
|
218
|
+
else if (!onSwing && unit.channeling && ctx.t < unit.channeling.endsAt && !hasAttrEx4(spell, SPELL_ATTR_EX4.ALLOW_CAST_WHILE_CASTING)) { usable = false; reason = 'channeling'; }
|
|
219
|
+
else if ((ctx.spellIncursGcd as (s: SpellDef) => boolean)(spell) && ctx.t < unit.gcdUntil) { usable = false; reason = 'gcd'; } // only GCD-incurring spells wait for it; off-GCD may fire during
|
|
220
|
+
else if (cdRem > 0) { usable = false; reason = 'cooldown'; }
|
|
221
|
+
else {
|
|
222
|
+
// Form gate before resource (better feedback: "Must be in Cat Form" vs bare "energy").
|
|
223
|
+
const gate = resolveFormCast(spell, unit.form);
|
|
224
|
+
if (!gate.ok) { usable = false; reason = gate.reason; }
|
|
225
|
+
else if (!(ctx.canAfford as (f: Fighter, s: SpellDef) => boolean)(unit, spell)) {
|
|
226
|
+
usable = false;
|
|
227
|
+
const need = spell.powerType === 'health' ? 'health'
|
|
228
|
+
: spell.powerType === 'rage' ? 'rage'
|
|
229
|
+
: spell.powerType === 'energy' ? 'energy'
|
|
230
|
+
: (spell.powerType === 'mana' || isFormSpell(spell) || spell.costPct) ? 'mana'
|
|
231
|
+
: unit.spec.powerType === 'rage' ? 'rage' : unit.spec.powerType === 'energy' ? 'energy' : 'mana';
|
|
232
|
+
reason = need;
|
|
233
|
+
}
|
|
234
|
+
else if (!canPaySpellReagents(unit, spell)) { usable = false; reason = 'reagents'; }
|
|
235
|
+
// The outdoors-only refusal. The bar has to grey it: the cast is already refused, so a lit button
|
|
236
|
+
// under a roof would arm the queue and then do nothing.
|
|
237
|
+
else if (!spellAllowedAtLocation(spell, cfg.outdoors !== false, cfg.battlegroundMap)) {
|
|
238
|
+
usable = false;
|
|
239
|
+
reason = 'wrong location';
|
|
240
|
+
}
|
|
241
|
+
else if (spell.requiresComboPoints && combo <= 0) { usable = false; reason = 'combo points'; }
|
|
242
|
+
// SpellLevel / baseLevel: grey over-level ranks kept in the kit for visibility (KI-641).
|
|
243
|
+
// Cite: Spell.cpp CheckCast SPELL_FAILED_LEVEL_REQUIREMENT.
|
|
244
|
+
else if (((spell.spellLevel ?? spell.reqLevel ?? 0) | 0) > (unit.spec.level | 0)
|
|
245
|
+
&& ((spell.spellLevel ?? spell.reqLevel ?? 0) | 0) > 0) {
|
|
246
|
+
usable = false;
|
|
247
|
+
reason = 'level';
|
|
248
|
+
}
|
|
249
|
+
else if (
|
|
250
|
+
// Bow / gun / crossbow shots — Auto Shot (role shoot) and Arcane/Tranq (dmgClass RANGED).
|
|
251
|
+
// Cite: Spell.cpp CheckCast NO_AMMO 6730–6793; weapon.ts rangedStrike ammo gate.
|
|
252
|
+
unit.spec.rangedWeaponSubclass != null
|
|
253
|
+
&& (unit.spec.rangedWeaponSubclass === 2 || unit.spec.rangedWeaponSubclass === 3 || unit.spec.rangedWeaponSubclass === 18)
|
|
254
|
+
&& (unit.spec.ammoRemaining != null && unit.spec.ammoRemaining <= 0)
|
|
255
|
+
&& (roleOf(spell) === 'shoot' || (spell.dmgClass ?? -1) === SPELL_DAMAGE_CLASS.RANGED)
|
|
256
|
+
) {
|
|
257
|
+
usable = false;
|
|
258
|
+
reason = 'no ammo';
|
|
259
|
+
}
|
|
260
|
+
// Player-only EquippedItem full slot scan (creatures skip — Spell.cpp:6542).
|
|
261
|
+
// Cite: Spell.cpp:6539–6543; Player.cpp:18572 HasItemFitToSpellReqirements; equippedItemGate.ts.
|
|
262
|
+
else {
|
|
263
|
+
const equipReason = (unit === player || isPlayerCharacterTarget(unit))
|
|
264
|
+
? playerEquippedItemRefuseReason(spell, unit.spec) : undefined;
|
|
265
|
+
if (equipReason) { usable = false; reason = equipReason; }
|
|
266
|
+
else {
|
|
267
|
+
// Ground dest (Blizzard / RoF): no unit target — dest range is checked at place / startOrCast.
|
|
268
|
+
// Cite: SpellEffects.cpp EffectPersistentAA; isGroundTargetedMulti.
|
|
269
|
+
const groundDest = isGroundTargetedMulti(spell);
|
|
270
|
+
// Endless floor: focus is the caster between pulls. Hostile spells need a live foe.
|
|
271
|
+
// Cite: pickAction needsLiveHostile; Spell.cpp SPELL_FAILED_BAD_TARGETS.
|
|
272
|
+
if (!groundDest && isHostileSpellDef(spell) && !isSelfCenteredAoe(spell) && (focus === unit || (dead(focus) && !hasAttrEx2(spell, SPELL_ATTR_EX2.ALLOW_DEAD_TARGET)))) {
|
|
273
|
+
usable = false;
|
|
274
|
+
reason = 'No target';
|
|
275
|
+
} else if (!groundDest && isHostileSpellDef(spell) && !isSelfCenteredAoe(spell) && !spellTargetTypeAllowed(spell, focus.creatureType)) {
|
|
276
|
+
usable = false;
|
|
277
|
+
reason = 'invalid target';
|
|
278
|
+
} else if (
|
|
279
|
+
// The immune refusal, which greys the bar to match the cast gate and includes weapon
|
|
280
|
+
// strikes. A ground rain in a pack can still hit others, so only the 1v1 case is skipped.
|
|
281
|
+
(!groundDest || livingN <= 1)
|
|
282
|
+
&& focusImmunityBlocksSpell(spell, focus, livingN)
|
|
283
|
+
) {
|
|
284
|
+
usable = false;
|
|
285
|
+
reason = 'immune';
|
|
286
|
+
} else if ((ctx.requiresLivingPetTarget as (s: SpellDef) => boolean)(spell) && !(ctx.livingPetOf as (f: Fighter) => Fighter | null)(unit)) {
|
|
287
|
+
usable = false;
|
|
288
|
+
reason = 'invalid target';
|
|
289
|
+
} else {
|
|
290
|
+
const se = (ctx.stealthPosError as (f: Fighter, focus: Fighter, s: SpellDef) => string | null)(unit, focus, spell);
|
|
291
|
+
if (se) { usable = false; reason = se; }
|
|
292
|
+
else if (
|
|
293
|
+
// DG + W13: strip must match canCastSpell / chosenPlayerSpell (range + LOS).
|
|
294
|
+
// A usable:true bar over a refused cast is the classic facade.
|
|
295
|
+
!groundDest
|
|
296
|
+
&& (spellNeedsRangeGate(spell) || spellFoeDirectedGeometry(spell))
|
|
297
|
+
&& !isInSpellRange(
|
|
298
|
+
distanceBetween(unit, focus),
|
|
299
|
+
spellGateRangeMaxYd(spell), spellGateRangeMinYd(spell),
|
|
300
|
+
ctx.isMoving(unit.side) && ctx.isMoving(focus.side),
|
|
301
|
+
unit.spec.combatReachYd, focus.spec.combatReachYd,
|
|
302
|
+
)
|
|
303
|
+
) {
|
|
304
|
+
usable = false;
|
|
305
|
+
reason = 'out of range';
|
|
306
|
+
} else if (
|
|
307
|
+
!groundDest
|
|
308
|
+
&& cfg.navLos
|
|
309
|
+
&& !losOk
|
|
310
|
+
&& spellNeedsLosGate(spell)
|
|
311
|
+
) {
|
|
312
|
+
// The LOS arm used to sit on the else-chain without testing sight, so a
|
|
313
|
+
// hostile ranged spell skipped the reactive/dispel arms below entirely.
|
|
314
|
+
usable = false;
|
|
315
|
+
reason = 'line of sight';
|
|
316
|
+
} else {
|
|
317
|
+
// Reactive windows (Mongoose Bite after self dodge, Overpower after target dodge, …).
|
|
318
|
+
const react = reactiveCastGate(unit, focus, spell, ctx.t)
|
|
319
|
+
?? targetCastGate(unit, focus, spell, ctx.t);
|
|
320
|
+
if (react) { usable = false; reason = react; }
|
|
321
|
+
else if (isPureDispelSpell(spell)) {
|
|
322
|
+
// Classic pure-DISPEL CheckCast: NOTHING_TO_DISPEL when no matching aura.
|
|
323
|
+
// Cite: Spell.cpp:4890–4919; KI-610 Purge mana sink with empty target.
|
|
324
|
+
const de = firstDispelEffect(spell);
|
|
325
|
+
if (de && ctx.resolveDispelTarget!(unit, focus, de.dispelType) == null) {
|
|
326
|
+
usable = false;
|
|
327
|
+
reason = 'nothing to dispel';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { spell, usable, reason, cooldownRemainingMs: cdRem, queued: armed };
|
|
336
|
+
});
|
|
337
|
+
return list;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const pullSpentSpellsFor = ctx.pullSpentSpellsFor;
|
|
341
|
+
const pullSpentItemsFor = ctx.pullSpentItemsFor;
|
|
342
|
+
const push = ctx.push;
|
|
343
|
+
|
|
344
|
+
const pruneInterruptClaimsOnCtx = (): void => {
|
|
345
|
+
pruneInterruptClaims(
|
|
346
|
+
ctx.interruptClaim,
|
|
347
|
+
(id) => {
|
|
348
|
+
const f = fighterOfSide(id);
|
|
349
|
+
return !!f && !dead(f);
|
|
350
|
+
},
|
|
351
|
+
(id) => {
|
|
352
|
+
const f = fighterOfSide(id);
|
|
353
|
+
return !!f && f.castingSpell != null;
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
const interruptClaimedByOther = (targetId: CombatantId, actorId: CombatantId): boolean => {
|
|
358
|
+
pruneInterruptClaimsOnCtx();
|
|
359
|
+
const owner = ctx.interruptClaim.get(targetId);
|
|
360
|
+
return owner != null && owner !== actorId;
|
|
361
|
+
};
|
|
362
|
+
/** One answer for every seat, player included. See dispelDuty.ts. */
|
|
363
|
+
const dispelClaimedByOther = (
|
|
364
|
+
targetId: CombatantId,
|
|
365
|
+
actorId: CombatantId,
|
|
366
|
+
cat: number,
|
|
367
|
+
): boolean => dispelClaimedByOtherSeat(ctx, targetId, actorId, cat, dead);
|
|
368
|
+
|
|
369
|
+
// Put one step on a friendly seat's cast list — the twin of the move order, where null clears and the
|
|
370
|
+
// queue flag appends. EVERY friendly seat queues here, or the Pre-buff order's own caster cannot cast.
|
|
371
|
+
const queueUnitCast = (side: string, cast: QueuedCast | null, queue = false): boolean => {
|
|
372
|
+
const isPetSide = side === 'pet';
|
|
373
|
+
const isPlayerSide = side === player.side;
|
|
374
|
+
const al = isPetSide ? pet : isPlayerSide ? player : allyUnitOfSide(side);
|
|
375
|
+
if (!isPetSide && !isPlayerSide && !isAllySide(side as Side)) return false;
|
|
376
|
+
if (!al || dead(al)) return false;
|
|
377
|
+
// null = clear, the setMoveOrder(side, null) idiom. Nothing is dropped on the floor: the
|
|
378
|
+
// caller is saying it no longer wants the steps it queued.
|
|
379
|
+
if (cast === null) {
|
|
380
|
+
unitCastOrders.delete(side as Side);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
let step = cast;
|
|
384
|
+
const tgt = step.targetSide;
|
|
385
|
+
// An explicit target must be legal NOW or the whole step is refused — a CC menu entry that
|
|
386
|
+
// silently retargets to whatever the AI likes is worse than a dead click.
|
|
387
|
+
if (tgt != null) {
|
|
388
|
+
if (isEnemySide(tgt)) {
|
|
389
|
+
const ef = enemyUnitOfSide(tgt);
|
|
390
|
+
if (!ef || dead(ef) || isUnattackable(ef)) return false;
|
|
391
|
+
// Creature type is the one gate that can NEVER come good, so refuse now rather than hold a step
|
|
392
|
+
// that silently expires. Cooldown, GCD and range are transient and stay in the list.
|
|
393
|
+
const wantId = step.spellId;
|
|
394
|
+
if (wantId != null) {
|
|
395
|
+
const kit = isPetSide ? (al.spec.petFullKit ?? al.spec.spells) : al.spec.spells;
|
|
396
|
+
const sp = kit.find((s) => s.id === wantId);
|
|
397
|
+
if (sp && isHostileSpellDef(sp) && !spellTargetTypeAllowed(sp, ef.creatureType)) return false;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
// Friendly aim (Pre-buff): a HoT / shield on a named party member. Without it a
|
|
401
|
+
// non-hostile queued spell is a self-cast, which put the imp's Fire Shield on the imp.
|
|
402
|
+
const ff = resolveItemTarget(tgt as CombatantId);
|
|
403
|
+
if (!ff || dead(ff)) return false;
|
|
404
|
+
// Store the RESOLVED body — resolveItemTarget is what turns a party alias into a side.
|
|
405
|
+
step = { ...step, targetSide: ff.side as Side };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const list = unitCastOrders.get(side as Side);
|
|
409
|
+
if (queue && list) list.push(step);
|
|
410
|
+
else unitCastOrders.set(side as Side, [step]);
|
|
411
|
+
// The list reports itself. Nothing polls it, and nothing keeps a clock beside it.
|
|
412
|
+
if (step.spellId != null) {
|
|
413
|
+
push({
|
|
414
|
+
t: ctx.t,
|
|
415
|
+
kind: 'cast_order',
|
|
416
|
+
actor: side as Side,
|
|
417
|
+
spellId: step.spellId,
|
|
418
|
+
...(step.targetSide != null ? { target: step.targetSide } : {}),
|
|
419
|
+
state: 'queued',
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/** The four seats a rest, a revive or a BG drink may top up. */
|
|
426
|
+
const restorableUnitOf = (side: string): Fighter | null => {
|
|
427
|
+
if (side === 'player') return player;
|
|
428
|
+
if (side === 'pet') return pet;
|
|
429
|
+
return allyUnitOfSide(side) ?? enemyUnitOfSide(side);
|
|
430
|
+
};
|
|
431
|
+
// Write health, power and the out-of-combat flags, touching nothing IN FLIGHT — the whole difference
|
|
432
|
+
// from the full restore, and why a 500 ms rest tick can run beside a 2 s heal.
|
|
433
|
+
const writeVitals = (side: string, hpPct: number, powerPct: number): Fighter | null => {
|
|
434
|
+
const unit = restorableUnitOf(side);
|
|
435
|
+
if (!unit || !(unit.hp > 0)) return null;
|
|
436
|
+
unit.hp = Math.max(1, Math.round(unit.spec.maxHealth * Math.max(0.01, Math.min(1, hpPct))));
|
|
437
|
+
// Rage empties out of combat; mana/energy refill. Cite: Player.cpp Regenerate / rage decay.
|
|
438
|
+
unit.power = unit.spec.powerType === 'rage'
|
|
439
|
+
? 0
|
|
440
|
+
: Math.round(unit.spec.maxPower * Math.max(0, Math.min(1, powerPct)));
|
|
441
|
+
// The endless floor keeps ONE session per run, so chargeUsed held Charge to once per DUNGEON
|
|
442
|
+
// instead of once per pull (operator 2026-08-06 evening).
|
|
443
|
+
unit.chargeUsed = false;
|
|
444
|
+
unit.combatUntil = 0;
|
|
445
|
+
unit.combatEnteredAt = 0; // full OOC after rest — Charge / Stealth legal again
|
|
446
|
+
// Do NOT clear move orders. The out-of-combat refill ran every few seconds while walking, and
|
|
447
|
+
// deleting them made the floor treat mid-corridor as arrival, then aggro the boss from across the map.
|
|
448
|
+
invalidateViewMemo();
|
|
449
|
+
return unit;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
get events() { return ctx.events; },
|
|
454
|
+
/** Rows the ring has discarded. `absoluteIndex - eventsDropped` = index into `events`. */
|
|
455
|
+
get eventsDropped() { return ctx.eventsDropped ?? 0; },
|
|
456
|
+
get t() { return ctx.t; },
|
|
457
|
+
get ended() { return ctx.ended; },
|
|
458
|
+
get winner() { return ctx.winner; },
|
|
459
|
+
get aiPlayer() { return ctx.aiPlayer; },
|
|
460
|
+
set aiPlayer(v: boolean) { ctx.aiPlayer = v; },
|
|
461
|
+
playerView: () => memoView(player),
|
|
462
|
+
// Focus target (not always enemies[0]). After a pack member dies, auto-retarget so the
|
|
463
|
+
// unit frame / rotation see the living foe instead of a 0 HP corpse that "won't die".
|
|
464
|
+
enemyView: () => memoView(ensurePlayerFocus()),
|
|
465
|
+
// Advertised charm occupies the pet bar (Enslave Demon thrall). Cite: Unit.cpp:11328
|
|
466
|
+
// TakeCharmOf UnsummonPetTemporaryIfAny + CharmSpellInitialize; KI-896.
|
|
467
|
+
petView: () => {
|
|
468
|
+
const thrall = ctx.livingEnemies().find(
|
|
469
|
+
(e) => ctx.isCharmed(e) && e.charmedBy === 'player',
|
|
470
|
+
);
|
|
471
|
+
if (thrall) return memoView(thrall);
|
|
472
|
+
return pet ? memoView(pet) : null;
|
|
473
|
+
},
|
|
474
|
+
alliesView: () => alliesView(),
|
|
475
|
+
// The view row for ONE seat, or null — the same memoized object the list views hand out, found
|
|
476
|
+
// through the store instead of by scanning. Eleven UI sites scanned the whole raid per row per frame.
|
|
477
|
+
viewOfSide: (side: string): CombatantView | null => {
|
|
478
|
+
const unit = ctx.sideFighter?.(side as Side) ?? null;
|
|
479
|
+
return unit ? memoView(unit) : null;
|
|
480
|
+
},
|
|
481
|
+
totemsView: (): TotemView[] => {
|
|
482
|
+
const out: TotemView[] = [];
|
|
483
|
+
// Every seat the party holds, not four: each shaman owns its own four (Unit.h:2625).
|
|
484
|
+
for (const tot of totems.values()) {
|
|
485
|
+
const slot = tot.totemSlot;
|
|
486
|
+
if (!slot || dead(tot)) continue;
|
|
487
|
+
// Real kit auras this totem grants right now — what the UI shows instead of a phantom chip.
|
|
488
|
+
const auras = totemGrantedAuras(ctx.effectsOn('player'), tot.side as TotemSide, ctx.t);
|
|
489
|
+
const side = tot.side as TotemSide;
|
|
490
|
+
const ownerSide = tot.totemOwnerSide ?? totemOwnerMeta.get(side)?.ownerSide ?? 'player';
|
|
491
|
+
// The ground this totem covers, stamped at summon from the whole kit. `auras` is
|
|
492
|
+
// party-facing only, and `spec.spells` holds TotemAI casts only (empty for passives).
|
|
493
|
+
const auraRadiusYd = tot.totemAuraRadiusYd ?? 0;
|
|
494
|
+
out.push({
|
|
495
|
+
slot,
|
|
496
|
+
side,
|
|
497
|
+
name: tot.spec.name,
|
|
498
|
+
hp: Math.max(0, Math.round(tot.hp)),
|
|
499
|
+
maxHp: tot.spec.maxHealth,
|
|
500
|
+
remainingMs: tot.totemExpiresAt != null ? Math.max(0, Math.round(tot.totemExpiresAt - ctx.t)) : 0,
|
|
501
|
+
...(tot.totemSummonSpellId ? { summonSpellId: tot.totemSummonSpellId } : {}),
|
|
502
|
+
ownerSide,
|
|
503
|
+
...(auras.length ? { auras } : {}),
|
|
504
|
+
...(auraRadiusYd > 0 ? { auraRadiusYd } : {}),
|
|
505
|
+
casting: tot.castingSpell
|
|
506
|
+
? { spellId: tot.castingSpell.id, name: tot.castingSpell.name, startedAt: tot.castStartedAt, endsAt: tot.castFinishAt }
|
|
507
|
+
: null,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
},
|
|
512
|
+
trapsView: (): TrapView[] => {
|
|
513
|
+
const out: TrapView[] = [];
|
|
514
|
+
const now = ctx.t;
|
|
515
|
+
for (const u of ctx.fightersOrder()) {
|
|
516
|
+
for (const e of ctx.effectsOn(u.side)) {
|
|
517
|
+
if (!isHunterTrapArmProc(e) || e.expiresAt <= now) continue;
|
|
518
|
+
if (e.charges != null && e.charges <= 0) continue;
|
|
519
|
+
const pos = e.trapPosYd ?? posOf?.(e.target);
|
|
520
|
+
out.push({
|
|
521
|
+
spellId: e.spellId,
|
|
522
|
+
name: e.name,
|
|
523
|
+
remainingMs: Number.isFinite(e.expiresAt) ? Math.max(0, Math.round(e.expiresAt - now)) : 0,
|
|
524
|
+
radiusYd: HUNTER_TRAP_RADIUS_YD,
|
|
525
|
+
ownerSide: e.target,
|
|
526
|
+
triggerSpellId: e.triggerSpellId ?? 0,
|
|
527
|
+
...(pos ? { pos: { x: pos.x, y: pos.y, ...(pos.z != null ? { z: pos.z } : {}) } } : {}),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
},
|
|
533
|
+
/** Totem→owner map for meter merge; includes despawned totems this fight. */
|
|
534
|
+
totemOwners: (): Array<{ side: TotemSide; ownerSide: Side; name: string }> =>
|
|
535
|
+
[...totemOwnerMeta.entries()].map(([side, m]) => ({ side, ownerSide: m.ownerSide, name: m.name })),
|
|
536
|
+
// A companion pet mapped to its owner, off the roster's own field. It replaces a parallel array of
|
|
537
|
+
// seat INDICES keyed by seat index, which ten readers had to hop through and three had to parse.
|
|
538
|
+
companionPets: (): Array<{ side: Side; ownerSide: Side; name: string }> => {
|
|
539
|
+
const out: Array<{ side: Side; ownerSide: Side; name: string }> = [];
|
|
540
|
+
for (const a of ctx.allyBlock()) {
|
|
541
|
+
const petSide = a.spec.petSide;
|
|
542
|
+
if (petSide == null) continue;
|
|
543
|
+
const petUnit = ctx.sideFighter?.(petSide);
|
|
544
|
+
out.push({ side: petSide, ownerSide: a.side, name: petUnit?.spec.name ?? 'Pet' });
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
},
|
|
548
|
+
hasTotem: (slot: TotemSlot, owner: Side = 'player') => {
|
|
549
|
+
const tot = totems.get(sideForTotemSlot(slot, owner));
|
|
550
|
+
return !!(tot && !dead(tot));
|
|
551
|
+
},
|
|
552
|
+
unsummonAllTotems: (reason: 'owner' | 'expire' | 'zone', owner?: Side): void => {
|
|
553
|
+
ctx.unsummonAllTotems?.(reason, owner);
|
|
554
|
+
},
|
|
555
|
+
// The player kit strip: only castable parents. Trigger and proc payloads are re-attached for lookup
|
|
556
|
+
// but must never surface as free castables, and free talent procs stay off the bar too.
|
|
557
|
+
abilities: (): AbilityStatus[] => {
|
|
558
|
+
const focus = ensurePlayerFocus();
|
|
559
|
+
const power = player.power;
|
|
560
|
+
const cc = ccMaskOf(player);
|
|
561
|
+
if (
|
|
562
|
+
abilitiesHold
|
|
563
|
+
&& abilitiesHoldT === ctx.t
|
|
564
|
+
&& abilitiesHoldGcd === player.gcdUntil
|
|
565
|
+
&& abilitiesHoldCast === player.castingSpell
|
|
566
|
+
&& abilitiesHoldChan === player.channeling
|
|
567
|
+
&& abilitiesHoldForm === (player.form ?? 0)
|
|
568
|
+
&& abilitiesHoldFocus === focus.side
|
|
569
|
+
&& abilitiesHoldSwing === (player.pendingSwing?.id ?? 0)
|
|
570
|
+
&& abilitiesHoldPower === power
|
|
571
|
+
&& abilitiesHoldCc === cc
|
|
572
|
+
) return abilitiesHold;
|
|
573
|
+
const list = abilityStatusesFor(player, focus);
|
|
574
|
+
abilitiesHold = list;
|
|
575
|
+
abilitiesHoldT = ctx.t;
|
|
576
|
+
abilitiesHoldGcd = player.gcdUntil;
|
|
577
|
+
abilitiesHoldCast = player.castingSpell;
|
|
578
|
+
abilitiesHoldChan = player.channeling;
|
|
579
|
+
abilitiesHoldForm = player.form ?? 0;
|
|
580
|
+
abilitiesHoldFocus = focus.side;
|
|
581
|
+
abilitiesHoldSwing = player.pendingSwing?.id ?? 0;
|
|
582
|
+
abilitiesHoldPower = power;
|
|
583
|
+
abilitiesHoldCc = cc;
|
|
584
|
+
return list;
|
|
585
|
+
},
|
|
586
|
+
// The same strip for ANY seat: a non-lead ally, or the `enemy` lead of the other team. No memo,
|
|
587
|
+
// for the reason `rotationSnapshot` has none — the pinned-target form is not one key.
|
|
588
|
+
abilitiesFor: (side: string, targetSide?: string | null): AbilityStatus[] => {
|
|
589
|
+
const f = fighterOfSide(side);
|
|
590
|
+
if (!f) return [];
|
|
591
|
+
let foe: Fighter | null = null;
|
|
592
|
+
if (targetSide != null) {
|
|
593
|
+
const cand = fighterOfSide(targetSide);
|
|
594
|
+
if (cand && !dead(cand)) foe = cand;
|
|
595
|
+
}
|
|
596
|
+
// The player seat keeps its own resolver, so this and `abilities()` answer for the same body.
|
|
597
|
+
if (!foe) {
|
|
598
|
+
foe = f === player ? ensurePlayerFocus()
|
|
599
|
+
: isEnemySide(f.side) ? hostileTargetOf(f) : foeOf(f);
|
|
600
|
+
}
|
|
601
|
+
return abilityStatusesFor(f, foe);
|
|
602
|
+
},
|
|
603
|
+
items: () => playerItems.map((it) => {
|
|
604
|
+
const cdRem = Math.max(0, (itemCd.get(it.entry) ?? 0) - ctx.t);
|
|
605
|
+
const catRem = (it.category != null && it.categoryCooldownMs != null && it.categoryCooldownMs > 0)
|
|
606
|
+
? Math.max(0, (player.categoryCooldowns.get(it.category) ?? 0) - ctx.t)
|
|
607
|
+
: 0;
|
|
608
|
+
let usable = true; let reason: string | undefined;
|
|
609
|
+
if (it.count <= 0) { usable = false; reason = 'none left'; }
|
|
610
|
+
else if (player.castingSpell) { usable = false; reason = 'casting'; }
|
|
611
|
+
else if (cdRem > 0 || catRem > 0) { usable = false; reason = 'cooldown'; }
|
|
612
|
+
return { item: it, usable, reason, cooldownRemainingMs: Math.max(cdRem, catRem), count: it.count };
|
|
613
|
+
}),
|
|
614
|
+
tick: (action, inputs) => step(action, inputs),
|
|
615
|
+
// The two halves of the seam a pump needs off this side: replay what a far tick recorded, and
|
|
616
|
+
// build what the next far tick reads (sessionStepWrap.ts:85 is the on-thread twin of both).
|
|
617
|
+
applySimEffect: (effect) => { ctx.cfg.applyEffect?.(effect); },
|
|
618
|
+
tickInputsNow: () => ctx.cfg.tickInputs?.(),
|
|
619
|
+
runToEnd: () => { ctx.aiPlayer = true; let guard = 0; while (!ctx.ended && guard++ < 100000) step(); finalize(); return result(); },
|
|
620
|
+
result,
|
|
621
|
+
cooldownRemainingMs: (id: number) => displayCooldownMs(
|
|
622
|
+
player, id, Math.max(0, (player.cooldowns.get(id) ?? 0) - ctx.t),
|
|
623
|
+
),
|
|
624
|
+
categoryCooldownRemainingMs: (c: number) => Math.max(0, (player.categoryCooldowns.get(c) ?? 0) - ctx.t),
|
|
625
|
+
cooldownSnapshot: (side?: Side) => {
|
|
626
|
+
const spell: Record<number, number> = {};
|
|
627
|
+
const category: Record<number, number> = {};
|
|
628
|
+
// Any friendly body, not just the player: a companion's Bloodlust or a pet's Dash has to
|
|
629
|
+
// leave the session on the world clock or the next pull re-arms the whole squad's kit.
|
|
630
|
+
const unit = side == null || side === 'player'
|
|
631
|
+
? player
|
|
632
|
+
: side === 'pet' ? pet
|
|
633
|
+
: allyUnitOfSide(side);
|
|
634
|
+
if (!unit) return { spell, category };
|
|
635
|
+
// A cooldown-on-event entry is the permanent sentinel while its aura is up. Leaving the session
|
|
636
|
+
// ends that aura, so the real timer is what leaves with the unit — and infinity would save as null.
|
|
637
|
+
unit.cooldowns.forEach((ready, id) => {
|
|
638
|
+
const r = Number.isFinite(ready)
|
|
639
|
+
? Math.max(0, ready - ctx.t)
|
|
640
|
+
: (unit.deferredCooldowns?.get(id) ?? 0);
|
|
641
|
+
if (r > 0) spell[id] = r;
|
|
642
|
+
});
|
|
643
|
+
unit.categoryCooldowns.forEach((ready, c) => {
|
|
644
|
+
let r = Math.max(0, ready - ctx.t);
|
|
645
|
+
if (!Number.isFinite(ready)) {
|
|
646
|
+
r = 0;
|
|
647
|
+
for (const pending of unit.deferredCategoryCooldowns?.values() ?? []) {
|
|
648
|
+
if (pending.category === c) r = Math.max(r, pending.ms);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (r > 0) category[c] = r;
|
|
652
|
+
});
|
|
653
|
+
return { spell, category };
|
|
654
|
+
},
|
|
655
|
+
enemyThreatList: (ownerId?: CombatantId) => {
|
|
656
|
+
// Threat lists exist only on enemy owners, and a possessed ally is not one. With no primary — a
|
|
657
|
+
// floor session between pulls — there is no default owner and so no list.
|
|
658
|
+
const id = ownerId ?? enemy?.side;
|
|
659
|
+
if (id == null || !isEnemySide(id)) return [];
|
|
660
|
+
const tm = threatOf(id);
|
|
661
|
+
return tm ? tm.getThreatList() : [];
|
|
662
|
+
},
|
|
663
|
+
enemyThreatVictim: (ownerId?: CombatantId) => {
|
|
664
|
+
// The autopilot may pass a focus that is a possessed ally, since focus prefers a charmed
|
|
665
|
+
// friendly. A missing manager reads null and never throws — only enemy sides own one.
|
|
666
|
+
const id = ownerId ?? enemy?.side;
|
|
667
|
+
if (id == null || !isEnemySide(id)) return null;
|
|
668
|
+
const tm = threatOf(id);
|
|
669
|
+
return tm ? tm.getCurrentVictimId() : null;
|
|
670
|
+
},
|
|
671
|
+
enemiesView: () => enemiesView(),
|
|
672
|
+
livingEnemiesView: () => livingEnemies().map((ef) => memoView(ef)),
|
|
673
|
+
/** Geometry v2.1b: live position snapshot (undefined when no-geometry session). */
|
|
674
|
+
unitPosYd: (side: Side | string): CombatPosYd | undefined => posOf(side),
|
|
675
|
+
/** The heading the unit is actually facing - the same `facingOf` the dodge/parry arc reads
|
|
676
|
+
* (sessionGeometry.ts:94). Undefined on a session with no geometry. */
|
|
677
|
+
unitFacing: (side: Side | string): number | undefined => {
|
|
678
|
+
const f = fighterOfSide(side);
|
|
679
|
+
return f ? ctx.facingOf?.(f) : undefined;
|
|
680
|
+
},
|
|
681
|
+
// The per-tick payload channel (PERF-RUNSHEET S6). The same two reads as `unitPosYd` and
|
|
682
|
+
// `unitHpPct` over the whole roster in one pass, written into the caller's buffer.
|
|
683
|
+
mirrorPoseFrame: (frame: SimPoseFrame): void => {
|
|
684
|
+
let poolMoved = false;
|
|
685
|
+
for (let i = 0; i < frame.sides.length; i++) {
|
|
686
|
+
const side = frame.sides[i]!;
|
|
687
|
+
const o = i * POSE_STRIDE;
|
|
688
|
+
const x = frame.buf[o]!;
|
|
689
|
+
if (!Number.isNaN(x)) {
|
|
690
|
+
const z = frame.buf[o + 2]!;
|
|
691
|
+
setPos(side, Number.isNaN(z) ? { x, y: frame.buf[o + 1]! } : { x, y: frame.buf[o + 1]!, z });
|
|
692
|
+
}
|
|
693
|
+
const pct = frame.buf[o + 3]!;
|
|
694
|
+
const unit = fighterOfSide(side);
|
|
695
|
+
if (!unit || Number.isNaN(pct)) continue;
|
|
696
|
+
const hp = Math.round(unit.spec.maxHealth * (pct / 100));
|
|
697
|
+
if (hp === unit.hp) continue;
|
|
698
|
+
// Crossing zero either way is pool membership (sessionPools.ts living* walks).
|
|
699
|
+
if ((hp > 0) !== (unit.hp > 0)) poolMoved = true;
|
|
700
|
+
unit.hp = hp;
|
|
701
|
+
}
|
|
702
|
+
if (poolMoved) ctx.bumpPool?.();
|
|
703
|
+
invalidateViewMemo();
|
|
704
|
+
},
|
|
705
|
+
// The far side is the authority for what the fight DID, so its verdict lands here rather than
|
|
706
|
+
// being re-derived: this copy's death sweep never ran (mirrorPoseFrame only writes hp).
|
|
707
|
+
mirrorOutcome: (isEnded: boolean, who: Side | 'timeout' | null): void => {
|
|
708
|
+
let moved = false;
|
|
709
|
+
if (isEnded && !ctx.ended) { ctx.ended = true; moved = true; }
|
|
710
|
+
if (who != null && ctx.winner == null) { ctx.winner = who; moved = true; }
|
|
711
|
+
// Only when something actually changed: this runs on every drained reply, and dropping the
|
|
712
|
+
// memo per tick rebuilt every view and cost the log and the icons their smoothness.
|
|
713
|
+
if (moved) invalidateViewMemo();
|
|
714
|
+
},
|
|
715
|
+
poseFrame: (filler: PoseFiller): SimPoseFrame => fillPoseFrame(
|
|
716
|
+
filler,
|
|
717
|
+
ctx.allSeats(),
|
|
718
|
+
ctx.unitPositionsMutable,
|
|
719
|
+
(side) => posOf(side),
|
|
720
|
+
(side) => fighterOfSide(side) ?? undefined,
|
|
721
|
+
),
|
|
722
|
+
/** Live hp 0..100 for the Recoil hp-fill lerp — a raw fighter read, no view build. */
|
|
723
|
+
unitHpPct: (side: Side | string): number | undefined => {
|
|
724
|
+
const f = fighterOfSide(side);
|
|
725
|
+
if (!f || !(f.spec.maxHealth > 0)) return undefined;
|
|
726
|
+
return Math.max(0, Math.min(100, (f.hp / f.spec.maxHealth) * 100));
|
|
727
|
+
},
|
|
728
|
+
/** Live DynamicObject ground pools for stage / tactical discs. */
|
|
729
|
+
groundPools: () => ctx.groundPools.map((p) => ({
|
|
730
|
+
id: p.id,
|
|
731
|
+
spellId: p.spellId,
|
|
732
|
+
name: p.name,
|
|
733
|
+
school: p.school,
|
|
734
|
+
...(p.radius != null ? { radius: p.radius } : {}),
|
|
735
|
+
...(p.origin ? { origin: { ...p.origin } } : {}),
|
|
736
|
+
expiresAt: p.expiresAt,
|
|
737
|
+
source: p.source,
|
|
738
|
+
})),
|
|
739
|
+
/** In-flight SpellEntry.speed missiles for stage VFX (empty without geometry). */
|
|
740
|
+
missilesInFlight: () => {
|
|
741
|
+
const list = ctx.pendingMissiles;
|
|
742
|
+
if (!list.length) return [];
|
|
743
|
+
return list.map((m) => ({
|
|
744
|
+
launchAt: m.launchAt,
|
|
745
|
+
landAt: m.landAt,
|
|
746
|
+
casterSide: m.casterSide,
|
|
747
|
+
targetSide: m.targetSide,
|
|
748
|
+
spellId: m.spell.id,
|
|
749
|
+
...(m.school ? { school: m.school } : {}),
|
|
750
|
+
}));
|
|
751
|
+
},
|
|
752
|
+
/** Per-side motion-resolution tally — diagnosis surface, empty until motion runs. */
|
|
753
|
+
navDiag: (): Record<string, Record<string, number>> =>
|
|
754
|
+
Object.fromEntries(ctx.navDiag ?? new Map()),
|
|
755
|
+
/** The gate that resolved this unit's motion last tick — null until motion has run. */
|
|
756
|
+
motionExplain: (side: string): MotionGateReport | null =>
|
|
757
|
+
ctx.motionGates?.read(side) ?? null,
|
|
758
|
+
motionExplainAll: (): Record<string, MotionGateReport> =>
|
|
759
|
+
ctx.motionGates?.all() ?? {},
|
|
760
|
+
/** C2 SlowUpdate think/skip tallies. */
|
|
761
|
+
slowDiag: () => ctx.slowDiag ?? {
|
|
762
|
+
pickThink: 0, pickSkip: 0, eventAiThink: 0, eventAiSkip: 0,
|
|
763
|
+
areaAuraRadius: 0, areaAuraSkip: 0,
|
|
764
|
+
},
|
|
765
|
+
// Which frame the offsets live in, so a renderer knows whether origin plus offset is a real world
|
|
766
|
+
// position. Undefined when the session was created without geometry — never guess for a fixture.
|
|
767
|
+
geometryAnchor: (): PullGeometryAnchor | undefined =>
|
|
768
|
+
ctx.unitPositionsMutable ? (cfg.geometryAnchor ?? { frame: 'pull-local' }) : undefined,
|
|
769
|
+
/** Reposition a unit; number = pull-axis yards → {x:n,y:0}. No-op without geometry. */
|
|
770
|
+
setUnitPosYd: (side: Side | string, pos: CombatPosYd | number): void => {
|
|
771
|
+
const p: CombatPosYd = typeof pos === 'number' ? { x: pos, y: 0 } : pos;
|
|
772
|
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) return;
|
|
773
|
+
const targetPos: CombatPosYd = p.z != null && Number.isFinite(p.z)
|
|
774
|
+
? { x: p.x, y: p.y, z: p.z }
|
|
775
|
+
: { x: p.x, y: p.y };
|
|
776
|
+
setPos(side, targetPos);
|
|
777
|
+
// A repositioned friendly holds ITS NEW ground (revive / world resync).
|
|
778
|
+
if (cfg.friendlyHoldYd != null && !isEnemySide(side)) {
|
|
779
|
+
unitAnchors.set(side, { x: p.x, y: p.y });
|
|
780
|
+
}
|
|
781
|
+
// A teleported enemy (scripted move / BG resync) evades to where it was PUT, not to the
|
|
782
|
+
// room it came from. Cite: Creature.h:834-835 SetRespawnCoord on a relocation.
|
|
783
|
+
if (isEnemySide(side)) {
|
|
784
|
+
const at = posOf(side);
|
|
785
|
+
if (at) ctx.enemyHomeYd?.set(side, { ...at });
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
setUnitAnchor: (side: string, pos: CombatPosYd): void => {
|
|
789
|
+
if (!Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return;
|
|
790
|
+
unitAnchors.set(side, { x: pos.x, y: pos.y });
|
|
791
|
+
},
|
|
792
|
+
// One companion's castable kit with live usability for the takeover bar. Leaner than the player
|
|
793
|
+
// pipeline — no equipped-item or stealth nuances — and the cast gate is the same final one.
|
|
794
|
+
allyAbilities: (allySide: string, targetSide?: string | null): AbilityStatus[] => {
|
|
795
|
+
const al = allyUnitOfSide(allySide);
|
|
796
|
+
if (!al) return [];
|
|
797
|
+
const holdKey = `${allySide}|${targetSide ?? ''}|${ctx.t}|${al.gcdUntil}|${al.castingSpell?.id ?? 0}|${al.form ?? 0}`;
|
|
798
|
+
if (allyAbilitiesHold && allyAbilitiesHoldKey === holdKey) return allyAbilitiesHold;
|
|
799
|
+
// A named target asks "can this companion cast X at THAT mob", which is a different question
|
|
800
|
+
// from "at whatever it is fighting". The cast gate then applies the type mask for real.
|
|
801
|
+
const named = targetSide != null ? enemyUnitOfSide(targetSide) : null;
|
|
802
|
+
if (targetSide != null && (!named || dead(named) || isUnattackable(named))) return [];
|
|
803
|
+
const foe = named ?? foeOf(al);
|
|
804
|
+
const out = al.spec.spells
|
|
805
|
+
.filter((spell) => !(ctx.isTriggeredCompanion as (f: Fighter, s: SpellDef) => boolean)(al, spell))
|
|
806
|
+
.map((spell) => {
|
|
807
|
+
const ownRem = Math.max(0, (al.cooldowns.get(spell.id) ?? 0) - ctx.t);
|
|
808
|
+
const catRem = spell.category != null
|
|
809
|
+
? Math.max(0, (al.categoryCooldowns.get(spell.category) ?? 0) - ctx.t)
|
|
810
|
+
: 0;
|
|
811
|
+
const cdRem = displayCooldownMs(al, spell.id, Math.max(ownRem, catRem));
|
|
812
|
+
let usable = true;
|
|
813
|
+
let reason: string | undefined;
|
|
814
|
+
if ((ctx.isControlledHard as ((u: Fighter) => boolean) | undefined)?.(al)) {
|
|
815
|
+
usable = false; reason = 'controlled';
|
|
816
|
+
}
|
|
817
|
+
// Same CheckCast gates as the player bar — a silenced companion greys its casts.
|
|
818
|
+
else if (al.silencedUntil > ctx.t && isSilencePrevented(spell)) { usable = false; reason = 'silenced'; }
|
|
819
|
+
else if (al.pacifiedUntil > ctx.t && isPacifyPrevented(spell)) { usable = false; reason = 'pacified'; }
|
|
820
|
+
else if ((ctx.schoolLocked as (f: Fighter, s: SpellDef) => boolean)(al, spell)) { usable = false; reason = 'school_lockout'; }
|
|
821
|
+
else if (!spell.onNextSwing && al.castingSpell && !hasAttrEx4(spell, SPELL_ATTR_EX4.ALLOW_CAST_WHILE_CASTING)) { usable = false; reason = 'casting'; }
|
|
822
|
+
else if (!spell.onNextSwing && al.channeling && ctx.t < al.channeling.endsAt && !hasAttrEx4(spell, SPELL_ATTR_EX4.ALLOW_CAST_WHILE_CASTING)) { usable = false; reason = 'channeling'; }
|
|
823
|
+
else if ((ctx.spellIncursGcd as (s: SpellDef) => boolean)(spell) && ctx.t < al.gcdUntil) { usable = false; reason = 'gcd'; }
|
|
824
|
+
else if (cdRem > 0) { usable = false; reason = 'cooldown'; }
|
|
825
|
+
else if (!(ctx.canCastSpell as (f: Fighter, tgt: Fighter, s: SpellDef) => boolean)(al, foe, spell)) {
|
|
826
|
+
usable = false; reason = 'unusable';
|
|
827
|
+
}
|
|
828
|
+
return { spell, usable, reason, cooldownRemainingMs: cdRem, queued: false };
|
|
829
|
+
});
|
|
830
|
+
allyAbilitiesHold = out;
|
|
831
|
+
allyAbilitiesHoldKey = holdKey;
|
|
832
|
+
return out;
|
|
833
|
+
},
|
|
834
|
+
/** Direct one companion's hostile actions at a specific enemy (null = release to AI). */
|
|
835
|
+
setAllyTarget: (allySide: string, targetSide: string | null, opts?: { skipMove?: boolean }): boolean => {
|
|
836
|
+
const al = allyUnitOfSide(allySide);
|
|
837
|
+
if (!al) return false;
|
|
838
|
+
if (targetSide == null) {
|
|
839
|
+
allyManualTarget.delete(allySide as Side);
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
const ef = enemyUnitOfSide(targetSide);
|
|
843
|
+
if (!ef || dead(ef) || isUnattackable(ef)) return false;
|
|
844
|
+
allyManualTarget.set(allySide as Side, targetSide as EnemySide);
|
|
845
|
+
if (opts?.skipMove) return true;
|
|
846
|
+
const dest = posOf(targetSide);
|
|
847
|
+
// The ORDERED ally, not the player: measuring from the player's body issued no path at all
|
|
848
|
+
// when the player already stood in melee of the mob, so "send this one in" did nothing.
|
|
849
|
+
const from = posOf(allySide) ?? posOf('player');
|
|
850
|
+
if (dest && from) {
|
|
851
|
+
const planar = detHypot(dest.x - from.x, dest.y - from.y);
|
|
852
|
+
const floorOk = dest.z == null || from.z == null || Math.abs(dest.z - from.z) <= 4;
|
|
853
|
+
if (planar > MELEE_REACH_YD + MELEE_PARK_SLACK_YD || !floorOk) {
|
|
854
|
+
const q = unitMoveOrders.get(allySide)?.[0];
|
|
855
|
+
const already = q != null && detHypot(q.x - dest.x, q.y - dest.y) <= 8 + MELEE_REACH_YD
|
|
856
|
+
&& (dest.z == null || q.z == null || Math.abs(q.z - dest.z) <= 4);
|
|
857
|
+
if (!already) {
|
|
858
|
+
ctx.pathTo?.(allySide, dest, {
|
|
859
|
+
keepFocus: true,
|
|
860
|
+
standShortYd: standShortYdFor(al.spec.standYards),
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return true;
|
|
866
|
+
},
|
|
867
|
+
/** Mid-fight role stamp — groupRole + stand + decideDirty (+ optional provider swap). */
|
|
868
|
+
setAllyGroupRole: (
|
|
869
|
+
allySide: string,
|
|
870
|
+
role: GroupRole,
|
|
871
|
+
provider?: AllyActionProvider | null,
|
|
872
|
+
): boolean => {
|
|
873
|
+
if (!isAllySide(allySide as Side)) return false;
|
|
874
|
+
if (role !== 'tank' && role !== 'healer' && role !== 'damage') return false;
|
|
875
|
+
const al = allyUnitOfSide(allySide);
|
|
876
|
+
if (!al) return false;
|
|
877
|
+
al.spec.groupRole = role;
|
|
878
|
+
// Motion closes to standYards; restamp so tank holds melee and healer parks back.
|
|
879
|
+
al.spec.standYards = resolveStandYards({
|
|
880
|
+
groupRole: role,
|
|
881
|
+
classId: al.spec.classId,
|
|
882
|
+
rangedAuto: al.spec.rangedAuto,
|
|
883
|
+
});
|
|
884
|
+
al.decideDirty = true;
|
|
885
|
+
// One seat write. This used to COPY the whole seat-indexed cfg array, pad it out to the
|
|
886
|
+
// seat with undefined, and write the slot (E1.1).
|
|
887
|
+
if (provider !== undefined) {
|
|
888
|
+
if (provider) ctx.unitActionProvider.set(al.side, provider);
|
|
889
|
+
else ctx.unitActionProvider.delete(al.side);
|
|
890
|
+
}
|
|
891
|
+
invalidateViewMemo();
|
|
892
|
+
return true;
|
|
893
|
+
},
|
|
894
|
+
// Latch the pet's standing kill-target (or null to release). Does not flip Follow/Stay. A player-only
|
|
895
|
+
// retarget uses this so owner-assist does not drag the pet onto the new focus.
|
|
896
|
+
setPetTarget: (targetSide: string | null): boolean => {
|
|
897
|
+
if (!pet || dead(pet)) return false;
|
|
898
|
+
if (targetSide == null) {
|
|
899
|
+
ctx.petManualTarget = null;
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
const ef = enemyUnitOfSide(targetSide);
|
|
903
|
+
if (!ef || dead(ef) || isUnattackable(ef)) return false;
|
|
904
|
+
ctx.petManualTarget = targetSide as EnemySide;
|
|
905
|
+
return true;
|
|
906
|
+
},
|
|
907
|
+
// The pet bar commands. Attack sends the pet at a named enemy, defaulting to the player's focus as
|
|
908
|
+
// the bare button does, while follow and stay release the victim and take over its motion.
|
|
909
|
+
setPetCommand: (cmd: 'attack' | 'follow' | 'stay', targetSide?: string | null): boolean => {
|
|
910
|
+
if (!pet || dead(pet)) return false;
|
|
911
|
+
if (cmd === 'attack') {
|
|
912
|
+
// RESOLVE the focus, never read the raw field: it holds the player itself as the stand-down
|
|
913
|
+
// sentinel and may sit on a corpse, so a bare Attack read a non-enemy side and refused.
|
|
914
|
+
let want = targetSide ?? null;
|
|
915
|
+
if (want == null) {
|
|
916
|
+
const focus = ensurePlayerFocus();
|
|
917
|
+
if (isEnemySide(focus.side)) want = focus.side;
|
|
918
|
+
else {
|
|
919
|
+
const own = ctx.freeFireHostilesFor!(pet).find((h) => !dead(h) && !isUnattackable(h))
|
|
920
|
+
?? ctx.livingHostilesFor(pet).find((h) => !dead(h) && !isUnattackable(h));
|
|
921
|
+
want = own && isEnemySide(own.side) ? own.side : null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (want == null) return false;
|
|
925
|
+
const ef = enemyUnitOfSide(want);
|
|
926
|
+
if (!ef || dead(ef) || isUnattackable(ef)) return false;
|
|
927
|
+
ctx.petManualTarget = want as EnemySide;
|
|
928
|
+
ctx.petCommand = 'attack';
|
|
929
|
+
// An Attack order cancels a recall in progress (PetHandler.cpp:190 SetIsRetreating()).
|
|
930
|
+
ctx.petRetreating = false;
|
|
931
|
+
// Attack while passive is a legal order and does NOT change the react state
|
|
932
|
+
// (PetAI.cpp:480 only gates the unprompted fight-back).
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
ctx.petManualTarget = null;
|
|
936
|
+
ctx.petCommand = cmd;
|
|
937
|
+
// Follow is a RECALL: it holds the pet off every victim until it is back at heel, which is the
|
|
938
|
+
// only reason the button does anything mid-fight. Stay just stops where it is.
|
|
939
|
+
ctx.petRetreating = cmd === 'follow';
|
|
940
|
+
// The stay spot is wherever the pet stands when the order lands (UpdateStayPosition);
|
|
941
|
+
// motion stamps it if geometry is not up yet.
|
|
942
|
+
ctx.petStayPos = cmd === 'stay' ? (posOf('pet') ?? null) : null;
|
|
943
|
+
// Follow/Stay drop the current victim the way COMMAND_FOLLOW does (AttackStop).
|
|
944
|
+
unitMoveOrders.delete('pet');
|
|
945
|
+
unitMoveIntents.delete('pet');
|
|
946
|
+
unitManualMotion.delete('pet');
|
|
947
|
+
ctx.unitHoldCourse?.delete('pet');
|
|
948
|
+
ctx.unitDirectMove?.delete('pet');
|
|
949
|
+
// The attack stop interrupts what it was doing. Without this the recall stalls on its own cast,
|
|
950
|
+
// since motion's first guard is the cast bar and a channel holds still.
|
|
951
|
+
if (pet.castingSpell) {
|
|
952
|
+
pet.castingSpell = null;
|
|
953
|
+
pet.castStartedAt = 0;
|
|
954
|
+
pet.castFinishAt = 0;
|
|
955
|
+
}
|
|
956
|
+
if (pet.channeling) interruptChannel(pet, channelInterruptCtx(ctx), { quiet: true });
|
|
957
|
+
pet.pendingCharge = null;
|
|
958
|
+
return true;
|
|
959
|
+
},
|
|
960
|
+
/** Pet bar react state (PetActionButton slots 8-10). Cite: UnitAI.h:81-86 ReactStates. */
|
|
961
|
+
setPetReact: (react: 'aggressive' | 'defensive' | 'passive'): boolean => {
|
|
962
|
+
if (!pet || dead(pet)) return false;
|
|
963
|
+
ctx.petReact = react;
|
|
964
|
+
// Going passive is also a stand-down: it drops the victim it was told to keep.
|
|
965
|
+
if (react === 'passive') ctx.petManualTarget = null;
|
|
966
|
+
return true;
|
|
967
|
+
},
|
|
968
|
+
petBarState: () => ({
|
|
969
|
+
command: ctx.petCommand,
|
|
970
|
+
react: ctx.petReact,
|
|
971
|
+
targetSide: ctx.petManualTarget,
|
|
972
|
+
/** Recall in progress — the pet is walking home and will not fight until it arrives. */
|
|
973
|
+
retreating: ctx.petRetreating,
|
|
974
|
+
/** Ability ids the pet KNOWS, and which of them the AI is currently allowed to pick. */
|
|
975
|
+
kit: (pet?.spec.petFullKit ?? pet?.spec.spells ?? []).map((s) => {
|
|
976
|
+
// Same max(own, category) the player's own bar reads, off the PET's maps - the bar
|
|
977
|
+
// had no cooldown source at all, so Dash and Growl drew as ready mid-recovery.
|
|
978
|
+
const ownRem = pet ? Math.max(0, (pet.cooldowns.get(s.id) ?? 0) - ctx.t) : 0;
|
|
979
|
+
const catRem = (pet && s.category != null)
|
|
980
|
+
? Math.max(0, (pet.categoryCooldowns.get(s.category) ?? 0) - ctx.t)
|
|
981
|
+
: 0;
|
|
982
|
+
return {
|
|
983
|
+
id: s.id,
|
|
984
|
+
autocast: (pet?.spec.spells ?? []).some((k) => k.id === s.id),
|
|
985
|
+
cooldownRemainingMs: pet ? displayCooldownMs(pet, s.id, Math.max(ownRem, catRem)) : 0,
|
|
986
|
+
cooldownMs: s.cooldownMs || 0,
|
|
987
|
+
};
|
|
988
|
+
}),
|
|
989
|
+
}),
|
|
990
|
+
// A pet ability slot, right-clicked: flip autocast for THIS fight. The kit is read off the spec, so
|
|
991
|
+
// the toggle has to move the entry in and out of that array or a mid-fight flip is inert.
|
|
992
|
+
setPetAutocast: (spellId: number, on: boolean): boolean => {
|
|
993
|
+
if (!pet || dead(pet)) return false;
|
|
994
|
+
const full = pet.spec.petFullKit ?? pet.spec.spells;
|
|
995
|
+
const def = full.find((s) => s.id === spellId);
|
|
996
|
+
if (!def) return false;
|
|
997
|
+
const live = pet.spec.spells;
|
|
998
|
+
const at = live.findIndex((s) => s.id === spellId);
|
|
999
|
+
if (on && at < 0) {
|
|
1000
|
+
// Back in the kit in its authored order, so pick priority does not depend on
|
|
1001
|
+
// the order the player happened to click the buttons.
|
|
1002
|
+
const want = full.findIndex((s) => s.id === spellId);
|
|
1003
|
+
const before = live.findIndex((s) => full.findIndex((k) => k.id === s.id) > want);
|
|
1004
|
+
live.splice(before < 0 ? live.length : before, 0, def);
|
|
1005
|
+
} else if (!on && at >= 0) {
|
|
1006
|
+
live.splice(at, 1);
|
|
1007
|
+
}
|
|
1008
|
+
return true;
|
|
1009
|
+
},
|
|
1010
|
+
// A pet ability slot, left-clicked: cast it NOW on the pet's current victim. An autocast-off
|
|
1011
|
+
// ability is castable this way, which is the whole point of the client's left click.
|
|
1012
|
+
queuePetAction: (spellId: number): boolean => {
|
|
1013
|
+
if (!pet || dead(pet)) return false;
|
|
1014
|
+
const full = pet.spec.petFullKit ?? pet.spec.spells;
|
|
1015
|
+
const def = full.find((s) => s.id === spellId);
|
|
1016
|
+
if (!def) return false;
|
|
1017
|
+
unitCastOrders.set('pet' as Side, [{ spellId }]);
|
|
1018
|
+
// Hostile bar clicks need a victim. foeOf(self) is the OOC sentinel and
|
|
1019
|
+
// canCastSpell refuses it, so the queue sat armed and the button did nothing.
|
|
1020
|
+
if (isHostileSpellDef(def) && !isSelfCenteredAoe(def)) {
|
|
1021
|
+
const cur = ctx.foeOf?.(pet);
|
|
1022
|
+
if (!cur || cur === pet || dead(cur)) {
|
|
1023
|
+
let want: string | null = null;
|
|
1024
|
+
const focus = ensurePlayerFocus();
|
|
1025
|
+
if (isEnemySide(focus.side)) want = focus.side;
|
|
1026
|
+
else {
|
|
1027
|
+
const own = ctx.freeFireHostilesFor!(pet).find((h) => !dead(h) && !isUnattackable(h))
|
|
1028
|
+
?? ctx.livingHostilesFor(pet).find((h) => !dead(h) && !isUnattackable(h));
|
|
1029
|
+
want = own && isEnemySide(own.side) ? own.side : null;
|
|
1030
|
+
}
|
|
1031
|
+
const ef = want != null ? enemyUnitOfSide(want) : null;
|
|
1032
|
+
if (ef && !dead(ef) && !isUnattackable(ef)) ctx.petManualTarget = want as EnemySide;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return true;
|
|
1036
|
+
},
|
|
1037
|
+
// A click-to-move order: walk a friendly unit to a combat-yard point, outranking gap-close until
|
|
1038
|
+
// arrival. Null clears it, and it is a no-op without geometry or for an enemy or dead side.
|
|
1039
|
+
setMoveOrder: (
|
|
1040
|
+
side: string,
|
|
1041
|
+
pos: CombatPosYd | null,
|
|
1042
|
+
queue = false,
|
|
1043
|
+
opts?: { keepFocus?: boolean; keepTravel?: boolean; holdCourse?: boolean; direct?: boolean },
|
|
1044
|
+
): boolean => {
|
|
1045
|
+
if (!ctx.unitPositionsMutable) return false;
|
|
1046
|
+
const unit = partyUnitOfSide(side);
|
|
1047
|
+
if (!unit || dead(unit)) return false;
|
|
1048
|
+
if (pos == null) {
|
|
1049
|
+
// Stop / ground-clear cancels a kill-target the way an RTS does — not a queued append.
|
|
1050
|
+
if (!queue && !opts?.keepFocus) {
|
|
1051
|
+
if (side === 'player') ctx.explicitEnemyFocus = null;
|
|
1052
|
+
else if (isAllySide(side as Side)) allyManualTarget.delete(side as Side);
|
|
1053
|
+
}
|
|
1054
|
+
unitMoveOrders.delete(side);
|
|
1055
|
+
ctx.unitHoldCourse?.delete(side);
|
|
1056
|
+
ctx.unitDirectMove?.delete(side);
|
|
1057
|
+
// Stop is a release, not a refusal: whoever held motion no longer does.
|
|
1058
|
+
ctx.motionClaims?.breakAll(side);
|
|
1059
|
+
const cur = posOf(side);
|
|
1060
|
+
if (cfg.friendlyHoldYd != null && cur) unitAnchors.set(side, { x: cur.x, y: cur.y });
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
if (!ctx.pathTo) return false;
|
|
1064
|
+
return ctx.pathTo(side, pos, {
|
|
1065
|
+
queue,
|
|
1066
|
+
keepFocus: opts?.keepFocus,
|
|
1067
|
+
holdCourse: opts?.holdCourse,
|
|
1068
|
+
direct: opts?.direct,
|
|
1069
|
+
});
|
|
1070
|
+
},
|
|
1071
|
+
pathTo: (side, dest, opts) => ctx.pathTo?.(side, dest, opts) ?? false,
|
|
1072
|
+
motionOwnerOf: (side: string): string | null => ctx.motionClaims?.ownerOf(side, ctx.t) ?? null,
|
|
1073
|
+
motionClaimStats: () => ({
|
|
1074
|
+
refused: ctx.motionClaims?.refused() ?? 0,
|
|
1075
|
+
reaimsKept: ctx.motionClaims?.reaimsKept() ?? 0,
|
|
1076
|
+
handovers: ctx.motionClaims?.handovers() ?? 0,
|
|
1077
|
+
}),
|
|
1078
|
+
standShortYd: (side: string): number => {
|
|
1079
|
+
if (side === 'player') return standShortYdFor(player.spec.standYards);
|
|
1080
|
+
if (side === 'pet') return CORRIDOR_STAND_SHORT_YD;
|
|
1081
|
+
const a = allyUnitOfSide(side);
|
|
1082
|
+
if (a) return standShortYdFor(a.spec.standYards);
|
|
1083
|
+
return CORRIDOR_STAND_SHORT_YD;
|
|
1084
|
+
},
|
|
1085
|
+
/** Current manual move dest for a side, or null when AI motion drives. */
|
|
1086
|
+
moveOrderOf: (side: string): CombatPosYd | null =>
|
|
1087
|
+
unitMoveOrders.get(side)?.[0] ?? null,
|
|
1088
|
+
holdCourseOf: (side: string): boolean => ctx.unitHoldCourse?.has(side) === true,
|
|
1089
|
+
noteRuleAfter: (side: string, after: RuleAfter | null | undefined): void => {
|
|
1090
|
+
if (!after) return;
|
|
1091
|
+
const unit = partyUnitOfSide(side);
|
|
1092
|
+
if (!unit) return;
|
|
1093
|
+
applyRuleAfter(unit, after, ctx.t);
|
|
1094
|
+
},
|
|
1095
|
+
armRuleAfter: (side: string, spellId: number, after: RuleAfter | null | undefined): void => {
|
|
1096
|
+
const unit = partyUnitOfSide(side);
|
|
1097
|
+
if (!unit) return;
|
|
1098
|
+
armRuleAfter(unit, spellId, after);
|
|
1099
|
+
},
|
|
1100
|
+
stepBackActive: (side: string): boolean => {
|
|
1101
|
+
const unit = partyUnitOfSide(side);
|
|
1102
|
+
return !!unit && isStepBackWalking(unit);
|
|
1103
|
+
},
|
|
1104
|
+
consumeFollowUp: (side: string, pickedSpellId: number): void => {
|
|
1105
|
+
const unit = partyUnitOfSide(side);
|
|
1106
|
+
if (!unit) return;
|
|
1107
|
+
consumeFollowUp(unit, pickedSpellId, ctx.t);
|
|
1108
|
+
},
|
|
1109
|
+
// Snap combat yards to walkable floor, null when there is none within radius. A reference z
|
|
1110
|
+
// restricts the answer to that floor and reports its height back; without it the search is planar.
|
|
1111
|
+
snapMoveDest: (
|
|
1112
|
+
pos: CombatPosYd,
|
|
1113
|
+
radiusYd = 20,
|
|
1114
|
+
refZ?: number | null,
|
|
1115
|
+
): CombatPosYd | null => {
|
|
1116
|
+
if (!Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return null;
|
|
1117
|
+
if (!ctx.unitPositionsMutable) return { x: pos.x, y: pos.y };
|
|
1118
|
+
return snapToMesh(pos, radiusYd > 0 ? radiusYd : 20, refZ ?? pos.z ?? null);
|
|
1119
|
+
},
|
|
1120
|
+
// Held-direction locomotion. The vector is normalised so a diagonal is not faster than a cardinal,
|
|
1121
|
+
// and it outranks a queued click route while latching ownership, so letting go leaves it standing.
|
|
1122
|
+
setMoveIntent: (side: string, dir: { x: number; y: number } | null): boolean => {
|
|
1123
|
+
if (!ctx.unitPositionsMutable) return false;
|
|
1124
|
+
const unit = partyUnitOfSide(side);
|
|
1125
|
+
if (!unit || dead(unit)) return false;
|
|
1126
|
+
if (dir == null) {
|
|
1127
|
+
unitMoveIntents.delete(side);
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
if (!Number.isFinite(dir.x) || !Number.isFinite(dir.y)) return false;
|
|
1131
|
+
const len = detHypot(dir.x, dir.y);
|
|
1132
|
+
if (!(len > 1e-6)) {
|
|
1133
|
+
unitMoveIntents.delete(side);
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
unitMoveIntents.set(side, { x: dir.x / len, y: dir.y / len });
|
|
1137
|
+
unitManualMotion.add(side);
|
|
1138
|
+
unitMoveOrders.delete(side);
|
|
1139
|
+
ctx.unitHoldCourse?.delete(side);
|
|
1140
|
+
ctx.unitDirectMove?.delete(side);
|
|
1141
|
+
// WASD / stick is a plain "go now" — cancel the cast the same way a ground click does.
|
|
1142
|
+
cancelCastForMove(unit);
|
|
1143
|
+
if (side === 'player') ctx.explicitEnemyFocus = null;
|
|
1144
|
+
else if (isAllySide(side as Side)) allyManualTarget.delete(side as Side);
|
|
1145
|
+
return true;
|
|
1146
|
+
},
|
|
1147
|
+
moveIntentOf: (side: string): CombatPosYd | null => unitMoveIntents.get(side) ?? null,
|
|
1148
|
+
// Who the player is driving as one unit. The `partyFollowYd` bubble tows only these sides, and only
|
|
1149
|
+
// while the player's own body is among them; null hands every side back to the default.
|
|
1150
|
+
setFollowGroup: (sides: readonly string[] | null): boolean => {
|
|
1151
|
+
ctx.followGroupSides = sides ? new Set(sides) : undefined;
|
|
1152
|
+
return true;
|
|
1153
|
+
},
|
|
1154
|
+
followGroupOf: (): readonly string[] | null =>
|
|
1155
|
+
(ctx.followGroupSides ? [...ctx.followGroupSides] : null),
|
|
1156
|
+
/** Take (or hand back) motion ownership for a side. Abilities are never affected. */
|
|
1157
|
+
setManualMotion: (side: string, on: boolean): boolean => {
|
|
1158
|
+
if (!ctx.unitPositionsMutable) return false;
|
|
1159
|
+
if (on) {
|
|
1160
|
+
unitManualMotion.add(side);
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
unitManualMotion.delete(side);
|
|
1164
|
+
unitMoveIntents.delete(side);
|
|
1165
|
+
return true;
|
|
1166
|
+
},
|
|
1167
|
+
manualMotionOf: (side: string): boolean => unitManualMotion.has(side),
|
|
1168
|
+
// The RTS stance for a friendly: hold stops moving now and keeps fighting, passive stands after the
|
|
1169
|
+
// current walk, patrol needs two waypoints, and retaliate restores AI gap-close.
|
|
1170
|
+
setUnitStance: (
|
|
1171
|
+
side: string,
|
|
1172
|
+
stance: 'retaliate' | 'passive' | 'hold' | 'patrol',
|
|
1173
|
+
opts?: { patrolWaypoints?: readonly CombatPosYd[] },
|
|
1174
|
+
): boolean => {
|
|
1175
|
+
if (!ctx.unitPositionsMutable) return false;
|
|
1176
|
+
const unit = partyUnitOfSide(side);
|
|
1177
|
+
if (!unit || dead(unit)) return false;
|
|
1178
|
+
if (stance === 'retaliate') {
|
|
1179
|
+
unitStance.delete(side);
|
|
1180
|
+
unitPatrolLoop.delete(side);
|
|
1181
|
+
unitManualMotion.delete(side);
|
|
1182
|
+
unit.autoAttackEnabled = true;
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
unitStance.set(side, stance);
|
|
1186
|
+
if (stance === 'hold') {
|
|
1187
|
+
unitMoveOrders.delete(side);
|
|
1188
|
+
unitMoveIntents.delete(side);
|
|
1189
|
+
ctx.unitHoldCourse?.delete(side);
|
|
1190
|
+
ctx.unitDirectMove?.delete(side);
|
|
1191
|
+
unitManualMotion.add(side);
|
|
1192
|
+
unitPatrolLoop.delete(side);
|
|
1193
|
+
// Cancel an in-flight Charge — otherwise the tank finishes the leap and "hold" lost.
|
|
1194
|
+
// A charge is motion; the rotation, heals and white swings are not, so they stay on.
|
|
1195
|
+
unit.pendingCharge = null;
|
|
1196
|
+
const cur = posOf(side);
|
|
1197
|
+
if (cur) unitAnchors.set(side, { x: cur.x, y: cur.y });
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
if (stance === 'passive') {
|
|
1201
|
+
unitManualMotion.add(side);
|
|
1202
|
+
unitPatrolLoop.delete(side);
|
|
1203
|
+
// Move-only: no free-fire / charge while walking or standing.
|
|
1204
|
+
unit.pendingCharge = null;
|
|
1205
|
+
unit.autoAttackEnabled = false;
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
// patrol
|
|
1209
|
+
const pts = opts?.patrolWaypoints?.length
|
|
1210
|
+
? opts.patrolWaypoints.map((p) => ({ x: p.x, y: p.y }))
|
|
1211
|
+
: (unitMoveOrders.get(side) ?? []).map((p) => ({ x: p.x, y: p.y }));
|
|
1212
|
+
if (pts.length >= 2) {
|
|
1213
|
+
unitPatrolLoop.set(side, pts);
|
|
1214
|
+
unitMoveOrders.set(side, pts.slice());
|
|
1215
|
+
} else if (pts.length === 1) {
|
|
1216
|
+
// Single point: A = here, B = that point.
|
|
1217
|
+
const cur = posOf(side);
|
|
1218
|
+
if (cur) {
|
|
1219
|
+
const loop = [{ x: cur.x, y: cur.y }, pts[0]!];
|
|
1220
|
+
unitPatrolLoop.set(side, loop);
|
|
1221
|
+
unitMoveOrders.set(side, [pts[0]!]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
unitManualMotion.delete(side);
|
|
1225
|
+
return true;
|
|
1226
|
+
},
|
|
1227
|
+
unitStanceOf: (side: string): 'retaliate' | 'passive' | 'hold' | 'patrol' =>
|
|
1228
|
+
unitStance.get(side) ?? 'retaliate',
|
|
1229
|
+
/** Late-bind the floor flags when the bake lands. `los` opts into sight gating. */
|
|
1230
|
+
setFloorFlags: (opts?: { clampOnly?: boolean; los?: boolean }): void => {
|
|
1231
|
+
if (opts?.clampOnly) cfg.navClampOnly = true;
|
|
1232
|
+
else delete cfg.navClampOnly;
|
|
1233
|
+
if (opts?.los === true) cfg.navLos = true;
|
|
1234
|
+
},
|
|
1235
|
+
/** Roof over the CURRENT pull (floor shell re-points it per inject). */
|
|
1236
|
+
setOutdoors: (outdoors: boolean): void => {
|
|
1237
|
+
if (outdoors) delete cfg.outdoors;
|
|
1238
|
+
else cfg.outdoors = false;
|
|
1239
|
+
},
|
|
1240
|
+
// An external world-rules hook, such as a BG powerup: apply a spell's effects to ONE unit as a free
|
|
1241
|
+
// self-cast — no cost, no cooldown and no GCD, because the trap is the caster and not the unit.
|
|
1242
|
+
applySpellToUnit: (
|
|
1243
|
+
side: string,
|
|
1244
|
+
spell: SpellDef,
|
|
1245
|
+
opts?: { weaponImbueHand?: 'mainhand' | 'offhand' },
|
|
1246
|
+
): boolean => {
|
|
1247
|
+
const unit = fightingUnitOfSide(side);
|
|
1248
|
+
if (!unit || dead(unit)) return false;
|
|
1249
|
+
// The caller already picked the hand (bag coat / rotation item rule); applySpellLate
|
|
1250
|
+
// consumes this one-shot. Cite: SpellEffects.cpp:3099 EffectEnchantItemTmp itemTarget.
|
|
1251
|
+
if (opts?.weaponImbueHand) unit.pendingWeaponImbueHand = opts.weaponImbueHand;
|
|
1252
|
+
castOnTargets(unit, unit, spell);
|
|
1253
|
+
invalidateViewMemo();
|
|
1254
|
+
return true;
|
|
1255
|
+
},
|
|
1256
|
+
useUnitItem: (side: string, entry: number, targetId?: CombatantId): boolean => {
|
|
1257
|
+
const f = resolveItemTarget(side as CombatantId);
|
|
1258
|
+
if (!f || dead(f)) return false;
|
|
1259
|
+
// Hard control refuses everything except the item whose own immunity cancels it —
|
|
1260
|
+
// the same CheckCasterAuras exemption as the player's manual tap (W12).
|
|
1261
|
+
if ((ctx.isControlledHard as (u: Fighter) => boolean)(f)) {
|
|
1262
|
+
const bag = f === player ? playerItems : ctx.unitItems.get(side as Side);
|
|
1263
|
+
const it = bag?.find((x) => x.entry === entry);
|
|
1264
|
+
if (!it || it.effect.kind !== 'friendly_cast'
|
|
1265
|
+
|| ctx.spellCancelsOwnControl?.(f, it.effect.spell) !== true) return false;
|
|
1266
|
+
}
|
|
1267
|
+
return useItem(entry, targetId, side as Side);
|
|
1268
|
+
},
|
|
1269
|
+
consumeUnitItemsGained: (): { side: Side; entry: number; count: number }[] => {
|
|
1270
|
+
const out: { side: Side; entry: number; count: number }[] = [];
|
|
1271
|
+
for (const [side, got] of ctx.unitItemsGained) {
|
|
1272
|
+
for (const [entry, count] of got) if (count > 0) out.push({ side, entry, count });
|
|
1273
|
+
got.clear();
|
|
1274
|
+
}
|
|
1275
|
+
return out;
|
|
1276
|
+
},
|
|
1277
|
+
unitItems: (side: string): CombatItemStatus[] => {
|
|
1278
|
+
const f = resolveItemTarget(side as CombatantId);
|
|
1279
|
+
const bag = side === 'player' ? playerItems
|
|
1280
|
+
: (isAllySide(side as Side) || isEnemySide(side as Side))
|
|
1281
|
+
? ctx.unitItems.get(side as Side)
|
|
1282
|
+
: undefined;
|
|
1283
|
+
if (!f || !bag) return [];
|
|
1284
|
+
const cdMap = side === 'player'
|
|
1285
|
+
? itemCd
|
|
1286
|
+
: ctx.unitItemCd.get(side as Side) ?? new Map<number, number>();
|
|
1287
|
+
return bag.map((it) => {
|
|
1288
|
+
const cdRem = Math.max(0, (cdMap.get(it.entry) ?? 0) - ctx.t);
|
|
1289
|
+
const catRem = (it.category != null && it.categoryCooldownMs != null && it.categoryCooldownMs > 0)
|
|
1290
|
+
? Math.max(0, (f.categoryCooldowns.get(it.category) ?? 0) - ctx.t)
|
|
1291
|
+
: 0;
|
|
1292
|
+
let usable = true; let reason: string | undefined;
|
|
1293
|
+
if (it.count <= 0) { usable = false; reason = 'none left'; }
|
|
1294
|
+
else if (f.castingSpell) { usable = false; reason = 'casting'; }
|
|
1295
|
+
else if (cdRem > 0 || catRem > 0) { usable = false; reason = 'cooldown'; }
|
|
1296
|
+
return { item: it, usable, reason, cooldownRemainingMs: Math.max(cdRem, catRem), count: it.count };
|
|
1297
|
+
});
|
|
1298
|
+
},
|
|
1299
|
+
unitRunSpeedYd: (side: string): number => {
|
|
1300
|
+
const live: { aura: number; pct: number }[] = [];
|
|
1301
|
+
// Same bucket as sessionMotionKit: permanent enchant and talent speed is MAXed against
|
|
1302
|
+
// whatever is running, never added to it.
|
|
1303
|
+
const unit = fightingUnitOfSide(side);
|
|
1304
|
+
for (const p of unit?.spec.permanentSpeedAuras ?? []) live.push({ aura: p.aura, pct: p.pct });
|
|
1305
|
+
const mount = unit?.spec.mount;
|
|
1306
|
+
if (mount) live.push({ aura: AURA_INCREASE_MOUNTED_SPEED, pct: mount.speedPct });
|
|
1307
|
+
for (const e of ctx.effectsOn(side as Side)) {
|
|
1308
|
+
if (e.kind !== 'speed' || e.aura == null) continue;
|
|
1309
|
+
live.push({ aura: e.aura, pct: e.pct ?? 0 });
|
|
1310
|
+
}
|
|
1311
|
+
if (live.length === 0) return BASE_RUN_SPEED;
|
|
1312
|
+
return effectiveSpeeds({ effects: live, mounted: mount != null }).run;
|
|
1313
|
+
},
|
|
1314
|
+
unitBaseRunSpeedYd: (side: string): number => {
|
|
1315
|
+
const unit = fightingUnitOfSide(side);
|
|
1316
|
+
const rows = [...(unit?.spec.permanentSpeedAuras ?? [])];
|
|
1317
|
+
const mount = unit?.spec.mount;
|
|
1318
|
+
if (mount) rows.push({ aura: AURA_INCREASE_MOUNTED_SPEED, pct: mount.speedPct });
|
|
1319
|
+
if (rows.length === 0) return BASE_RUN_SPEED;
|
|
1320
|
+
return effectiveSpeeds({ effects: rows, mounted: mount != null }).run;
|
|
1321
|
+
},
|
|
1322
|
+
/**
|
|
1323
|
+
* Ride or step off. Unit::Mount / Unit::Unmount — the caller owns WHETHER (mounting.ts gates
|
|
1324
|
+
* the ground, mounts.ts the rider); this only carries it into UpdateSpeed.
|
|
1325
|
+
*/
|
|
1326
|
+
setUnitMounted: (side: string, mount: { speedPct: number } | null): boolean => {
|
|
1327
|
+
const unit = fightingUnitOfSide(side);
|
|
1328
|
+
if (!unit) return false;
|
|
1329
|
+
const had = unit.spec.mount != null;
|
|
1330
|
+
if (mount && !had) {
|
|
1331
|
+
// Mount path strips holders of spells with AURA_INTERRUPT_FLAG_MOUNTING
|
|
1332
|
+
ctx.removeAurasWithInterruptFlags?.(unit, AURA_INTERRUPT.MOUNTING, 'mounting');
|
|
1333
|
+
}
|
|
1334
|
+
if (mount) unit.spec.mount = { speedPct: mount.speedPct };
|
|
1335
|
+
else delete unit.spec.mount;
|
|
1336
|
+
if (had && !mount) {
|
|
1337
|
+
ctx.removeAurasWithInterruptFlags?.(unit, AURA_INTERRUPT.DISMOUNT, 'unmount');
|
|
1338
|
+
}
|
|
1339
|
+
if (had !== (mount != null)) unit.speedDirty = true;
|
|
1340
|
+
return true;
|
|
1341
|
+
},
|
|
1342
|
+
unitMounted: (side: string): boolean => fightingUnitOfSide(side)?.spec.mount != null,
|
|
1343
|
+
unitInCombat: (side: string): boolean => {
|
|
1344
|
+
const unit = fightingUnitOfSide(side);
|
|
1345
|
+
return unit ? fighterIsInCombat(unit, ctx.t) : false;
|
|
1346
|
+
},
|
|
1347
|
+
reviveUnit: (side: string, hpPct = 1): boolean => {
|
|
1348
|
+
const unit = fightingUnitOfSide(side);
|
|
1349
|
+
if (!unit) return false;
|
|
1350
|
+
if (!dead(unit)) return true;
|
|
1351
|
+
unit.hp = Math.max(1, Math.round(unit.spec.maxHealth * Math.max(0.01, Math.min(1, hpPct))));
|
|
1352
|
+
unit.castingSpell = null;
|
|
1353
|
+
if (unit.channeling) interruptChannel(unit, channelInterruptCtx(ctx), { quiet: true });
|
|
1354
|
+
unit.pendingCharge = null;
|
|
1355
|
+
// Fresh life, fresh pull — the opener is available again (same rule as restoreUnit).
|
|
1356
|
+
unit.chargeUsed = false;
|
|
1357
|
+
unit.combatUntil = 0;
|
|
1358
|
+
unit.combatEnteredAt = 0;
|
|
1359
|
+
clearManualMotion(side);
|
|
1360
|
+
// Rez is pool membership. living* stale-hp walks only drop corpses (sessionPools.ts).
|
|
1361
|
+
ctx.bumpPool?.();
|
|
1362
|
+
invalidateViewMemo();
|
|
1363
|
+
return true;
|
|
1364
|
+
},
|
|
1365
|
+
// Endless-floor downtime: an eat-and-drink refill for a friendly between pulls. The revive path
|
|
1366
|
+
// skips the living, and without this a survivor limped into the next pack and chain-wiped the run.
|
|
1367
|
+
restoreVitals: (side: string, hpPct = 1, powerPct = 1): boolean =>
|
|
1368
|
+
writeVitals(side, hpPct, powerPct) != null,
|
|
1369
|
+
partyWiped: (): boolean => ctx.partyWiped!(),
|
|
1370
|
+
restoreUnit: (side: string, hpPct = 1, powerPct = 1): boolean => {
|
|
1371
|
+
const unit = writeVitals(side, hpPct, powerPct);
|
|
1372
|
+
if (!unit) return false;
|
|
1373
|
+
// The RESET half. A revive and a between-pull reset want whatever was in flight gone; a repeating
|
|
1374
|
+
// rest trickle does NOT, and calling this from one wiped a cast bar every 500 ms.
|
|
1375
|
+
unit.castingSpell = null;
|
|
1376
|
+
if (unit.channeling) interruptChannel(unit, channelInterruptCtx(ctx), { quiet: true });
|
|
1377
|
+
unit.pendingCharge = null;
|
|
1378
|
+
return true;
|
|
1379
|
+
},
|
|
1380
|
+
interruptClaimedByOther,
|
|
1381
|
+
dispelClaimedByOther,
|
|
1382
|
+
/** Current manual target of a companion, or null when the AI drives. */
|
|
1383
|
+
allyTargetOf: (allySide: string): string | null =>
|
|
1384
|
+
allyManualTarget.get(allySide as Side) ?? null,
|
|
1385
|
+
// The held one-shot action armed for this seat, or null. Read by the tick's action chooser so an
|
|
1386
|
+
// automatic rotation pick cannot starve a queued Pre-buff cast.
|
|
1387
|
+
queuedActionOf: (side: string): number | { item: number } | null => {
|
|
1388
|
+
const head = unitCastOrders.get(side as Side)?.[0];
|
|
1389
|
+
if (!head) return null;
|
|
1390
|
+
if (head.spellId != null) return head.spellId;
|
|
1391
|
+
return head.itemEntry != null ? { item: head.itemEntry } : null;
|
|
1392
|
+
},
|
|
1393
|
+
queueUnitCast,
|
|
1394
|
+
unitCastOrderOf: (side: string): readonly QueuedCast[] =>
|
|
1395
|
+
unitCastOrders.get(side as Side) ?? [],
|
|
1396
|
+
// Queue ONE manual action for a companion — it wins the next pick and then the rotation resumes. An
|
|
1397
|
+
// unknown or uncastable pick is dropped in favour of the rotation, never a wedge.
|
|
1398
|
+
queueAllyAction: (
|
|
1399
|
+
allySide: string,
|
|
1400
|
+
action: number | { item: number } | null,
|
|
1401
|
+
targetSide?: string | null,
|
|
1402
|
+
): boolean => {
|
|
1403
|
+
if (action === null) return queueUnitCast(allySide, null);
|
|
1404
|
+
const aim = targetSide != null ? { targetSide: targetSide as Side } : {};
|
|
1405
|
+
return queueUnitCast(
|
|
1406
|
+
allySide,
|
|
1407
|
+
typeof action === 'number'
|
|
1408
|
+
? { spellId: action, ...aim }
|
|
1409
|
+
: { itemEntry: action.item, ...aim },
|
|
1410
|
+
);
|
|
1411
|
+
},
|
|
1412
|
+
setPlayerTarget: (side: CombatantId, opts?: { skipMove?: boolean }): boolean => {
|
|
1413
|
+
// Allow focusing a possessed ally (burn-to-50% Anastari) as well as enemies.
|
|
1414
|
+
// Cite: boss_baroness_anastari.cpp — party damages possessed player to break possess.
|
|
1415
|
+
if (isEnemySide(side)) {
|
|
1416
|
+
const ef = enemyUnitOfSide(side);
|
|
1417
|
+
if (!ef || dead(ef)) return false;
|
|
1418
|
+
if (isUnattackable(ef)) {
|
|
1419
|
+
const possessed = ctx.partyUnits().find(
|
|
1420
|
+
(u) => !dead(u) && isCharmed(u) && !isUnattackable(u),
|
|
1421
|
+
);
|
|
1422
|
+
if (!possessed) return false;
|
|
1423
|
+
ctx.playerFocusFighter = possessed;
|
|
1424
|
+
ctx.explicitEnemyFocus = null;
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
ctx.playerFocusFighter = ef;
|
|
1428
|
+
// Click / kill-target: free-fire this side even while still threat-empty (floor passive).
|
|
1429
|
+
ctx.explicitEnemyFocus = side as EnemySide;
|
|
1430
|
+
if (opts?.skipMove) return true;
|
|
1431
|
+
const dest = posOf(side);
|
|
1432
|
+
const from = posOf('player');
|
|
1433
|
+
if (dest && from) {
|
|
1434
|
+
const planar = detHypot(dest.x - from.x, dest.y - from.y);
|
|
1435
|
+
const floorOk = dest.z == null || from.z == null || Math.abs(dest.z - from.z) <= 4;
|
|
1436
|
+
if (planar > MELEE_REACH_YD + MELEE_PARK_SLACK_YD || !floorOk) {
|
|
1437
|
+
const q = unitMoveOrders.get('player')?.[0];
|
|
1438
|
+
const already = q != null && detHypot(q.x - dest.x, q.y - dest.y) <= 8 + MELEE_REACH_YD
|
|
1439
|
+
&& (dest.z == null || q.z == null || Math.abs(q.z - dest.z) <= 4);
|
|
1440
|
+
if (!already) {
|
|
1441
|
+
ctx.pathTo?.('player', dest, {
|
|
1442
|
+
keepFocus: true,
|
|
1443
|
+
standShortYd: standShortYdFor(player.spec.standYards),
|
|
1444
|
+
});
|
|
1445
|
+
// NOT the pet. Clicking a target is not a pet order: the pet takes the focus and gap-closes
|
|
1446
|
+
// on its own. This aimed it at the mob's CENTRE and outranked that, so it stood inside it.
|
|
1447
|
+
for (const a of ctx.livingAllies()) {
|
|
1448
|
+
ctx.pathTo?.(a.side, dest, {
|
|
1449
|
+
keepFocus: true,
|
|
1450
|
+
standShortYd: standShortYdFor(a.spec.standYards),
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
if (side === 'player' && isCharmed(player) && !dead(player)) {
|
|
1459
|
+
ctx.playerFocusFighter = player;
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
if (side === 'pet' && pet && isCharmed(pet) && !dead(pet)) {
|
|
1463
|
+
ctx.playerFocusFighter = pet;
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
if (isAllySide(side)) {
|
|
1467
|
+
const al = allyUnitOfSide(side);
|
|
1468
|
+
if (!al || dead(al) || !isCharmed(al)) return false;
|
|
1469
|
+
ctx.playerFocusFighter = al;
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
return false;
|
|
1473
|
+
},
|
|
1474
|
+
playerTargetId: () => ensurePlayerFocus().side,
|
|
1475
|
+
targetOf: (side: string): CombatantId | null => {
|
|
1476
|
+
if (side === 'pet' && !pet) return null;
|
|
1477
|
+
const f = fighterOfSide(side);
|
|
1478
|
+
if (!f || dead(f)) return null;
|
|
1479
|
+
const foe = isEnemySide(f.side) ? hostileTargetOf(f) : foeOf(f);
|
|
1480
|
+
if (!foe || foe === f || dead(foe)) return null;
|
|
1481
|
+
return foe.side;
|
|
1482
|
+
},
|
|
1483
|
+
pullSpentSpellsFor,
|
|
1484
|
+
pullSpentItemsFor,
|
|
1485
|
+
// The engine's own rotation snapshot for one unit, byte-for-byte what a companion provider gets.
|
|
1486
|
+
// Exposed so the autopilot delegates: two builders for one evaluator is how they drifted.
|
|
1487
|
+
rotationSnapshot: (side: Side, foeSide?: string) => {
|
|
1488
|
+
const f = fighterOfSide(side);
|
|
1489
|
+
if (!f) return null;
|
|
1490
|
+
// A dead unit still gets a snapshot (self.alive false) — callers gate on it, and
|
|
1491
|
+
// returning null there would silently degrade the player's rotation on a death tick.
|
|
1492
|
+
let foe: Fighter | null = null;
|
|
1493
|
+
// A pinned foe, because the player's focus may be a possessed ally the foe lookup would not pick.
|
|
1494
|
+
// Without it the delegated target could describe a different unit than the caller resolved.
|
|
1495
|
+
if (foeSide) {
|
|
1496
|
+
const cand = fighterOfSide(foeSide);
|
|
1497
|
+
if (cand && !dead(cand)) foe = cand;
|
|
1498
|
+
}
|
|
1499
|
+
if (!foe) foe = isEnemySide(f.side) ? hostileTargetOf(f) : foeOf(f);
|
|
1500
|
+
return (ctx.allySnapshot as Function)(f, foe);
|
|
1501
|
+
},
|
|
1502
|
+
// WHO, not which slot. A side is a seat handed to a new body the moment the last is recycled, so
|
|
1503
|
+
// this is derived from the roster on every call and a recycled seat cannot answer for its predecessor.
|
|
1504
|
+
sideOfGuid: (guid: number): CombatantId | null => {
|
|
1505
|
+
const g = guid | 0;
|
|
1506
|
+
if (g === 0) return null;
|
|
1507
|
+
// Index, not a walk: callers ask this inside loops over streamed bodies. Maintained by the
|
|
1508
|
+
// one roster writer, same as bySideMap. E1.2.
|
|
1509
|
+
return ctx.unitByGuid?.(g)?.side ?? null;
|
|
1510
|
+
},
|
|
1511
|
+
// Record a world id on a seat combat created itself, since a summon enters with no floor guid. It
|
|
1512
|
+
// NEVER re-mints: a seat already carrying one refuses, so a recycled seat cannot inherit an id.
|
|
1513
|
+
stampUnitGuid: (side: string, guid: number): boolean => {
|
|
1514
|
+
const g = guid | 0;
|
|
1515
|
+
if (g === 0) return false;
|
|
1516
|
+
const f = ctx.sideFighter?.(side as Side);
|
|
1517
|
+
if (!f || (f.spec.guid ?? 0) !== 0) return false;
|
|
1518
|
+
f.spec.guid = g;
|
|
1519
|
+
ctx.noteUnitGuid?.(f.side, g);
|
|
1520
|
+
return true;
|
|
1521
|
+
},
|
|
1522
|
+
// The un-bind half: this seat is no longer the world body it was bound to. De-streaming drops a body
|
|
1523
|
+
// without killing it, and before this the two sides only ever BOUND together.
|
|
1524
|
+
releaseUnitGuid: (side: string, expectGuid?: number): boolean => {
|
|
1525
|
+
const f = fighterOfSide(side);
|
|
1526
|
+
if (!f || (f.spec.guid ?? 0) === 0) return false;
|
|
1527
|
+
// The expected guid is for a caller moving ONE body between seats: release the seat it left,
|
|
1528
|
+
// unless that seat was already recycled onto someone else, whose id would be stripped.
|
|
1529
|
+
if (expectGuid != null && (f.spec.guid ?? 0) !== (expectGuid | 0)) return false;
|
|
1530
|
+
f.spec.guid = undefined;
|
|
1531
|
+
ctx.noteUnitGuid?.(f.side, null);
|
|
1532
|
+
return true;
|
|
1533
|
+
},
|
|
1534
|
+
// The level this seat's unit was BUILT at, rolled from the template band rather than the row's
|
|
1535
|
+
// static midpoint — which is why the live layer kept a third store to remember it.
|
|
1536
|
+
unitLevelOf: (side: string): number | undefined => fighterOfSide(side)?.spec.level,
|
|
1537
|
+
/** The inverse, read the same way. Null on a seat the world never minted (fixtures, Pull +1). */
|
|
1538
|
+
guidOfSide: (side: string): number | null => {
|
|
1539
|
+
const g = fighterOfSide(side)?.spec.guid;
|
|
1540
|
+
return g != null && (g | 0) !== 0 ? g | 0 : null;
|
|
1541
|
+
},
|
|
1542
|
+
// A battleground NPC that walked into the fight on OUR side — AV's tower guards, marshals and
|
|
1543
|
+
// general are creature bodies, and they only fight if they hold a real seat.
|
|
1544
|
+
injectAlly: (
|
|
1545
|
+
spec: CombatantSpec,
|
|
1546
|
+
position?: CombatPosYd,
|
|
1547
|
+
opts?: { side?: string },
|
|
1548
|
+
): CombatantId | null => {
|
|
1549
|
+
const add = ctx.injectAllySpec?.(spec, { ownerSide: 'player', position, side: opts?.side });
|
|
1550
|
+
if (!add) return null;
|
|
1551
|
+
invalidateViewMemo();
|
|
1552
|
+
return add.side;
|
|
1553
|
+
},
|
|
1554
|
+
/** S4A conveyor / floor pool: mid-fight inject with optional geometry slot position. */
|
|
1555
|
+
injectEnemy: (
|
|
1556
|
+
spec: CombatantSpec,
|
|
1557
|
+
position?: CombatPosYd,
|
|
1558
|
+
opts?: { passive?: boolean; side?: string },
|
|
1559
|
+
): CombatantId | null => {
|
|
1560
|
+
const add = injectEnemySpec(spec, {
|
|
1561
|
+
summonerSide: 'player',
|
|
1562
|
+
creatureId: spec.creatureEntry,
|
|
1563
|
+
position,
|
|
1564
|
+
passive: opts?.passive === true,
|
|
1565
|
+
side: opts?.side,
|
|
1566
|
+
});
|
|
1567
|
+
invalidateViewMemo(); // a new enemy slot changes enemiesView length
|
|
1568
|
+
return add ? add.side : null;
|
|
1569
|
+
},
|
|
1570
|
+
// An attack-start seed on an already-present enemy, the floor's passive-to-aggro path. Refused when
|
|
1571
|
+
// geometry says the unit is out of local combat, so a skipped check cannot re-open combat at 600 yd.
|
|
1572
|
+
seedEnemyThreat: (enemySide: string, _on: CombatantId = 'player', opts?: { force?: boolean }): boolean => {
|
|
1573
|
+
const ef = enemyUnitOfSide(enemySide);
|
|
1574
|
+
if (!ef || dead(ef)) return false;
|
|
1575
|
+
// Event spawns (Solakar / stadium waves) AttackStart the raid from the tunnel.
|
|
1576
|
+
// Cite: instance_blackrock_spire.cpp:628-634 MovePoint then combat; CreatureAI AttackStart.
|
|
1577
|
+
const seedOn = opts?.force
|
|
1578
|
+
? (!dead(player) ? 'player'
|
|
1579
|
+
: pet && !dead(pet) ? 'pet'
|
|
1580
|
+
: ctx.livingAllies()[0]?.side ?? null)
|
|
1581
|
+
: nearestPartyInAcquire(enemySide);
|
|
1582
|
+
if (!seedOn) return false;
|
|
1583
|
+
const tm = threatOf(ef);
|
|
1584
|
+
const opened = !!tm && tm.isEmpty();
|
|
1585
|
+
if (tm) {
|
|
1586
|
+
// Where it stood when combat opened is where it evades back to — the spawn coord is
|
|
1587
|
+
// only the fallback. Cite: HomeMovementGenerator.cpp:48-51.
|
|
1588
|
+
if (opened) {
|
|
1589
|
+
const at = posOf(enemySide);
|
|
1590
|
+
if (at) ctx.enemyHomeYd?.set(enemySide, { ...at });
|
|
1591
|
+
}
|
|
1592
|
+
tm.addThreatDirectly(seedOn, 1, false);
|
|
1593
|
+
}
|
|
1594
|
+
// S2 OOC nextWakeAt=Inf; seed skips combatUntil so combat-enter dirty never fires.
|
|
1595
|
+
if (opened) {
|
|
1596
|
+
wakeForDecision(ef);
|
|
1597
|
+
for (const f of ctx.partyUnits()) wakeForDecision(f);
|
|
1598
|
+
}
|
|
1599
|
+
maybeAggroZonePulse(ef, ctx.partyUnits(), ctx.t);
|
|
1600
|
+
invalidateViewMemo(); // `engaged` is threat-derived
|
|
1601
|
+
return true;
|
|
1602
|
+
},
|
|
1603
|
+
// Endless floor wipe: drop every living hostile so an inject can recycle the slots. It does not end
|
|
1604
|
+
// the session. Prefer the scoped disengage so the passive stream is not nuked with the pull.
|
|
1605
|
+
disengageAllEnemies: (): number => {
|
|
1606
|
+
let n = 0;
|
|
1607
|
+
for (const ef of ctx.livingEnemies()) {
|
|
1608
|
+
ef.hp = 0;
|
|
1609
|
+
deathEmitted.add(ef.side); // despawn only — not a combat kill
|
|
1610
|
+
push({ t: ctx.t, kind: 'despawn', actor: ef.side });
|
|
1611
|
+
n += 1;
|
|
1612
|
+
}
|
|
1613
|
+
invalidateViewMemo();
|
|
1614
|
+
return n;
|
|
1615
|
+
},
|
|
1616
|
+
/** The ally-block mirror: de-stream injected friendly bodies (AV's guards) without a death. Only
|
|
1617
|
+
* a seat THIS session summoned goes, which is the same seat seatFriendly recycles. */
|
|
1618
|
+
disengageAllySides: (sides: readonly string[]): number => {
|
|
1619
|
+
if (!sides.length) return 0;
|
|
1620
|
+
const want = new Set(sides);
|
|
1621
|
+
let n = 0;
|
|
1622
|
+
for (const af of ctx.livingAllies()) {
|
|
1623
|
+
if (!want.has(af.side) || af.spec.summonOwnerSide == null) continue;
|
|
1624
|
+
af.hp = 0;
|
|
1625
|
+
deathEmitted.add(af.side); // stream GC, not a combat death
|
|
1626
|
+
push({ t: ctx.t, kind: 'despawn', actor: af.side });
|
|
1627
|
+
n += 1;
|
|
1628
|
+
}
|
|
1629
|
+
invalidateViewMemo();
|
|
1630
|
+
return n;
|
|
1631
|
+
},
|
|
1632
|
+
/** Drop only listed living sides — passives not listed stay. */
|
|
1633
|
+
disengageEnemySides: (sides: readonly string[]): number => {
|
|
1634
|
+
if (!sides.length) return 0;
|
|
1635
|
+
const want = new Set(sides);
|
|
1636
|
+
let n = 0;
|
|
1637
|
+
for (const ef of ctx.livingEnemies()) {
|
|
1638
|
+
if (!want.has(ef.side)) continue;
|
|
1639
|
+
ef.hp = 0;
|
|
1640
|
+
deathEmitted.add(ef.side); // stream GC / evade pool drop — not a combat kill
|
|
1641
|
+
push({ t: ctx.t, kind: 'despawn', actor: ef.side });
|
|
1642
|
+
n += 1;
|
|
1643
|
+
}
|
|
1644
|
+
invalidateViewMemo();
|
|
1645
|
+
return n;
|
|
1646
|
+
},
|
|
1647
|
+
// Evade / wipe survivor: empty threat table + full vitals + back to its home spot, still in the pool as
|
|
1648
|
+
// passive. Cite: Unit::EvadeTimerExpired — reset without despawn when the session owns the floor.
|
|
1649
|
+
releaseEnemyToPassive: (enemySide: string): boolean => {
|
|
1650
|
+
const ef = enemyUnitOfSide(enemySide);
|
|
1651
|
+
if (!ef || dead(ef)) return false;
|
|
1652
|
+
const tm = threatOf(ef);
|
|
1653
|
+
if (tm) tm.clear();
|
|
1654
|
+
ef.hp = Math.max(1, ef.spec.maxHealth);
|
|
1655
|
+
ef.power = ef.spec.powerType === 'rage' ? 0 : ef.spec.maxPower;
|
|
1656
|
+
// EnterEvadeMode is the timer reset plus Reset(), so a wiped boss re-opens on its ctor state —
|
|
1657
|
+
// without it a re-pull faced a full-health Vaelastrasz, over three times the written bar.
|
|
1658
|
+
const cast = ctx.enemyCastBySide.get(enemySide as EnemySide);
|
|
1659
|
+
if (cast) {
|
|
1660
|
+
resetCreatureCastSlotsOnEvade(cast.slots, ctx.t, ctx.rng);
|
|
1661
|
+
cast.pendingSlot = null;
|
|
1662
|
+
}
|
|
1663
|
+
ef.castingSpell = null;
|
|
1664
|
+
// An evading mob drops its channel with everything else — otherwise the motion freeze
|
|
1665
|
+
// holds it in place on a channel nothing will ever interrupt.
|
|
1666
|
+
if (ef.channeling) interruptChannel(ef, channelInterruptCtx(ctx), { quiet: true });
|
|
1667
|
+
ef.pendingCharge = null;
|
|
1668
|
+
// Reset() runs after the CombatStop half, so a Reset cast is not cleared by it.
|
|
1669
|
+
// Cite: UnitAI.cpp:113-126 then CreatureAI.cpp:69-70.
|
|
1670
|
+
runEvadeLifecycle(ctx, ef);
|
|
1671
|
+
unitMoveOrders.delete(enemySide);
|
|
1672
|
+
// MoveTargetedHome, snapped rather than walked: the motion phase skips an enemy with an empty
|
|
1673
|
+
// threat table, so a returning mob has no mover, and the far-evade despawn guard was dropped.
|
|
1674
|
+
const home = ctx.cfg.dungeonMap ? undefined : ctx.enemyHomeYd?.get(enemySide);
|
|
1675
|
+
const curr = posOf(enemySide);
|
|
1676
|
+
if (home && curr) {
|
|
1677
|
+
const dx = curr.x - home.x;
|
|
1678
|
+
const dy = curr.y - home.y;
|
|
1679
|
+
if (dx * dx + dy * dy > HOME_DESPAWN_DISTANCE_SQ) {
|
|
1680
|
+
ef.hp = 0;
|
|
1681
|
+
deathEmitted.add(ef.side);
|
|
1682
|
+
push({ t: ctx.t, kind: 'despawn', actor: ef.side });
|
|
1683
|
+
invalidateViewMemo();
|
|
1684
|
+
return true;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const walkHome = ctx.enemyHomeYd?.get(enemySide);
|
|
1688
|
+
if (walkHome && ctx.unitPositionsMutable) {
|
|
1689
|
+
setPos(enemySide, placeEnemyAt({ ...walkHome }));
|
|
1690
|
+
ctx.clearMotionCaches?.(enemySide);
|
|
1691
|
+
}
|
|
1692
|
+
invalidateViewMemo();
|
|
1693
|
+
return true;
|
|
1694
|
+
},
|
|
1695
|
+
// S2 D1: full spell-aura strip. Cite: Unit.cpp RemoveAurasDueToSpell ~5165–5178. Side may be
|
|
1696
|
+
// player/enemy/pet/ally_N/enemy_N.
|
|
1697
|
+
removeAurasDueToSpell: (side: CombatantId | Side, spellId: number): number => {
|
|
1698
|
+
const unit = fighterOfSide(side);
|
|
1699
|
+
if (!unit) return 0;
|
|
1700
|
+
invalidateViewMemo();
|
|
1701
|
+
return ctx.removeAurasDueToSpell!(unit, spellId | 0);
|
|
1702
|
+
},
|
|
1703
|
+
removeAurasWithInterruptFlags: (
|
|
1704
|
+
side: string | Side,
|
|
1705
|
+
flags: number,
|
|
1706
|
+
reason: string,
|
|
1707
|
+
opts?: { skipStealth?: boolean; exceptSpellId?: number; skipInvisibility?: boolean },
|
|
1708
|
+
) => {
|
|
1709
|
+
const unit = fighterOfSide(side);
|
|
1710
|
+
if (unit) ctx.removeAurasWithInterruptFlags?.(unit, flags, reason, opts);
|
|
1711
|
+
},
|
|
1712
|
+
// Surrender: both sides stop, the loser Grovels and the opponent wins. The emote is pushed as a cast
|
|
1713
|
+
// rather than routed through the cast path, which would need a GCD, a range and a school for nothing.
|
|
1714
|
+
forfeit: (side: CombatantId | Side): boolean => {
|
|
1715
|
+
if (ctx.ended) return false;
|
|
1716
|
+
const unit = fighterOfSide(side);
|
|
1717
|
+
if (!unit) return false;
|
|
1718
|
+
// Same predicate the yield clamp uses (events.ts), not a second spelling of it.
|
|
1719
|
+
if (!isDuelistSide(unit.side)) return false;
|
|
1720
|
+
ctx.ended = true;
|
|
1721
|
+
ctx.winner = unit.side === 'player' ? 'enemy' : 'player';
|
|
1722
|
+
const beg = ctx.cfg.spellOf?.(7267);
|
|
1723
|
+
ctx.push({
|
|
1724
|
+
t: ctx.t, kind: 'cast_finish', actor: unit.side,
|
|
1725
|
+
spellId: 7267, name: beg?.name ?? 'Grovel',
|
|
1726
|
+
});
|
|
1727
|
+
ctx.push({ t: ctx.t, kind: 'flavor', text: `${unit.spec.name} forfeits.` });
|
|
1728
|
+
invalidateViewMemo();
|
|
1729
|
+
return true;
|
|
1730
|
+
},
|
|
1731
|
+
// The cancel-aura gate inputs for the player. The possessed flag reads the charm state directly,
|
|
1732
|
+
// since the view resolves a stun first and would hide a possess. Own-aura is the caster test.
|
|
1733
|
+
auraCancelState: (spellId: number): { possessed: boolean; ownAura: boolean } | null => {
|
|
1734
|
+
const id = spellId | 0;
|
|
1735
|
+
if (!id) return null;
|
|
1736
|
+
const unit = bySide('player');
|
|
1737
|
+
if (!unit) return null;
|
|
1738
|
+
const rows = ctx.effectsOn(unit.side).filter((e) => e.spellId === id);
|
|
1739
|
+
if (!rows.length) return null;
|
|
1740
|
+
return {
|
|
1741
|
+
possessed: ctx.isCharmed(unit),
|
|
1742
|
+
// Every row of this holder must be self-cast for it to be "own"; a party area aura
|
|
1743
|
+
// supplied by an ally carries that ally's side as `source`.
|
|
1744
|
+
ownAura: rows.every((e) => e.source === unit.side),
|
|
1745
|
+
};
|
|
1746
|
+
},
|
|
1747
|
+
// Cite: CreatureAI.cpp:130-135; boss_razorgore.cpp:97 CUSTOM_A SetDeathPrevention(false).
|
|
1748
|
+
setDeathPrevention: (side: CombatantId | Side, enabled: boolean): boolean => {
|
|
1749
|
+
if (!isEnemySide(side)) return false;
|
|
1750
|
+
const cast = ctx.enemyCastBySide.get(side as EnemySide);
|
|
1751
|
+
if (!cast) return false;
|
|
1752
|
+
cast.eventAi.deathPrevention = enabled;
|
|
1753
|
+
return true;
|
|
1754
|
+
},
|
|
1755
|
+
leaveForm: (side?: CombatantId | Side): void => {
|
|
1756
|
+
const unit = fighterOfSide(side ?? 'player');
|
|
1757
|
+
if (!unit) return;
|
|
1758
|
+
ctx.leaveForm?.(unit);
|
|
1759
|
+
invalidateViewMemo();
|
|
1760
|
+
},
|
|
1761
|
+
/** `/stopcasting` — the cast-cancel a move order already performs, on demand. */
|
|
1762
|
+
stopCasting: (side?: CombatantId | Side): boolean => {
|
|
1763
|
+
const unit = fighterOfSide(side ?? 'player');
|
|
1764
|
+
if (!unit || dead(unit)) return false;
|
|
1765
|
+
const had = !!unit.castingSpell || !!unit.channeling;
|
|
1766
|
+
cancelCastForMove(unit);
|
|
1767
|
+
invalidateViewMemo();
|
|
1768
|
+
return had;
|
|
1769
|
+
},
|
|
1770
|
+
/** `/startattack` / `/stopattack` — swing state only; motion and rotation are untouched. */
|
|
1771
|
+
setAutoAttack: (side: CombatantId | Side | string, on: boolean): boolean => {
|
|
1772
|
+
const unit = fighterOfSide(side);
|
|
1773
|
+
if (!unit || dead(unit)) return false;
|
|
1774
|
+
if (unit.autoAttackEnabled === on) return true;
|
|
1775
|
+
unit.autoAttackEnabled = on;
|
|
1776
|
+
// Starting a swing must not hand the unit a free instant hit: the next swing lands on
|
|
1777
|
+
// its own timer, exactly as Unit::Attack schedules it.
|
|
1778
|
+
if (on && unit.swingReadyAt > ctx.t + 60_000) unit.swingReadyAt = ctx.t;
|
|
1779
|
+
invalidateViewMemo();
|
|
1780
|
+
return true;
|
|
1781
|
+
},
|
|
1782
|
+
getInstanceData: (field: number): number => instanceData.get(field | 0) ?? 0,
|
|
1783
|
+
instanceDataView: (): ReadonlyMap<number, number> => instanceData,
|
|
1784
|
+
};
|
|
1785
|
+
}
|