azeroth-shell 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +26 -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 +126 -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,3241 @@
|
|
|
1
|
+
// Combat session hub: start and resolve fights, dungeon pulls, session sync. Owns the pending fight and
|
|
2
|
+
// the live session; travel and squad re-entry go through bridges filled in createGame.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AllyActionProvider, CombatantSpec, CombatConfig, CombatResult, CombatSession,
|
|
6
|
+
} from '../combat.ts';
|
|
7
|
+
import { createSession } from '../combat.ts';
|
|
8
|
+
import { seedReagentBagCounts } from '../combat/spellReagents.ts';
|
|
9
|
+
import { mergeCombatSkillGains, type CombatSkillMap } from '../../domain/character/combatSkills.ts';
|
|
10
|
+
import { unsummonTotemsOnLeaveWorld } from '../combat/sessionDeaths.ts';
|
|
11
|
+
import { mulberry32, hashSeed } from '../core/rng.ts';
|
|
12
|
+
|
|
13
|
+
import type { Rng } from '../core/rng.ts';
|
|
14
|
+
import { navMeshForMap } from '../../domain/nav/nav.ts';
|
|
15
|
+
import { navMeshSourceUrl } from '../../domain/nav/navmesh.ts';
|
|
16
|
+
import { collisionSourceUrl } from '../../domain/nav/navCollision.ts';
|
|
17
|
+
import { outdoorsSourceUrl } from '../../domain/nav/navOutdoors.ts';
|
|
18
|
+
import type { ActionProviderSpec } from '../../domain/bots/actionProviderSpec.ts';
|
|
19
|
+
import { partyRoleOf } from '../../domain/bots/partyRole.ts';
|
|
20
|
+
import { seedPrePullStealth } from '../../domain/bots/prePullStealth.ts';
|
|
21
|
+
import {
|
|
22
|
+
applyEncounterPullGearToCompanion,
|
|
23
|
+
applyEncounterPullGearToPlayer,
|
|
24
|
+
} from '../../domain/raid/raidEncounterGear.ts';
|
|
25
|
+
import { isCompanionKeepUpSpell, rotationActionProvider, syncCompanionBagItemRules } from '../../domain/bots/companionRotation.ts';
|
|
26
|
+
import {
|
|
27
|
+
combatObjectSitesFromBake,
|
|
28
|
+
combatObjectsFromBake,
|
|
29
|
+
combatObjectsReusableFromBake,
|
|
30
|
+
} from '../../domain/raid/raidEncounterTactics.ts';
|
|
31
|
+
import { combatObjectTrapsFromBake, spellTrapSitesFromBake } from './combatObjectTraps.ts';
|
|
32
|
+
import { isMultiTargetCombatSpell } from '../../domain/rotation/classify.ts';
|
|
33
|
+
import { isRotationValid } from '../../domain/rotation/build.ts';
|
|
34
|
+
import { activeRotationProfile, ensurePlayerRotation, playerMacroExpandEnv } from './rotation.ts';
|
|
35
|
+
import { rotationWithMacros } from '../../domain/macro/rotation.ts';
|
|
36
|
+
import { stampStandYards } from '../../domain/rotation/stand.ts';
|
|
37
|
+
import {
|
|
38
|
+
resolveCreatureDefForCombat,
|
|
39
|
+
} from '../../data/dungeons/boss-eventai.ts';
|
|
40
|
+
import { getDungeonSpawn } from '../../data/dungeonSpawns.ts';
|
|
41
|
+
import {
|
|
42
|
+
materializeRingOfLawEncounter,
|
|
43
|
+
ringOfLawAttemptSeed,
|
|
44
|
+
} from '../../data/dungeons/blackrock-depths.ts';
|
|
45
|
+
import { goLootKind } from '../../domain/items/pendingLoot.ts';
|
|
46
|
+
import type { EncounterDef } from '../../data/schema.ts';
|
|
47
|
+
import {
|
|
48
|
+
buildCombatant, combatItems, maxHealth, maxPower, powerType,
|
|
49
|
+
pruneExpiredAuras, getActiveBuffIds,
|
|
50
|
+
} from '../../domain/character/character.ts';
|
|
51
|
+
import type { PetState } from '../../domain/character/character.ts';
|
|
52
|
+
import {
|
|
53
|
+
isStealthShapeshift, classHasParallelMana,
|
|
54
|
+
} from '../../domain/character/forms.ts';
|
|
55
|
+
import { DAZED_SPELL_ID } from '../combat/facing.ts';
|
|
56
|
+
import {
|
|
57
|
+
buildPet, petHasHappiness, applyKillLoyalty, HAPPINESS_DEATH_LOSS,
|
|
58
|
+
decayHappiness, LOYALTY_REBELLIOUS, reconcilePetPartyAuras, checkPetLearning,
|
|
59
|
+
petPartyAuraSpellIds, stripPetPartyAuras,
|
|
60
|
+
} from '../../domain/pet/pet.ts';
|
|
61
|
+
import {
|
|
62
|
+
applyCombatWear,
|
|
63
|
+
} from '../../domain/items/durability.ts';
|
|
64
|
+
import { applyDurabilityDamageFromLandedSpells } from './applySpellDurability.ts';
|
|
65
|
+
import { refuseIfDead } from '../../domain/run/death.ts';
|
|
66
|
+
import { tickPendingWellFed } from '../../progression/rest.ts';
|
|
67
|
+
import {
|
|
68
|
+
type DeathFlowMode,
|
|
69
|
+
createAliveDeathFlow,
|
|
70
|
+
deathToGhostAtGraveyard,
|
|
71
|
+
toGraveyardEntry,
|
|
72
|
+
usesFullDeathFlow,
|
|
73
|
+
} from '../../domain/run/death.ts';
|
|
74
|
+
import type { CreatureDef, GatherNodeDef } from '../../data/schema.ts';
|
|
75
|
+
import { CLASS, CREATURE_TYPE } from '../../data/schema.ts';
|
|
76
|
+
import { SOUL_SHARD_ITEM } from '../../domain/core/ids.ts';
|
|
77
|
+
import {
|
|
78
|
+
emptyInventory, addItem, removeItem, countItem, latestRandomPropertyId,
|
|
79
|
+
isExpendableCastItem, freeSlots, canFit,
|
|
80
|
+
} from '../../domain/items/inventory.ts';
|
|
81
|
+
import { bagCapacity } from '../../domain/items/equipment.ts';
|
|
82
|
+
import { soulShardRoom, isSoulShard } from '../../domain/bots/soulShardCap.ts';
|
|
83
|
+
import { ownedInventory } from '../../domain/items/keyring.ts';
|
|
84
|
+
import { consumeOwnedCharges } from '../../domain/items/inventory.ts';
|
|
85
|
+
import { equippedEntry, equippedInstance } from '../../domain/items/itemInstance.ts';
|
|
86
|
+
import type { LootDrop } from '../../domain/items/loot.ts';
|
|
87
|
+
import { rollLootDeep } from '../../domain/items/loot.ts';
|
|
88
|
+
import { gainSkill } from '../../domain/professions/professions.ts';
|
|
89
|
+
import {
|
|
90
|
+
miningHarvestTapCount, miningLockReqSkill, miningVeinOpenRange, mergeLootDrops,
|
|
91
|
+
} from '../../domain/professions/miningHarvest.ts';
|
|
92
|
+
import { clampGold } from '../../domain/economy/trading.ts';
|
|
93
|
+
import { formatItemName } from '../../domain/items/itemDisplayName.ts';
|
|
94
|
+
import { withPendingIdentities } from './loot.ts';
|
|
95
|
+
import {
|
|
96
|
+
recordKill, recordSpellCast, recordEventHappens,
|
|
97
|
+
} from '../../domain/quests/questlog.ts';
|
|
98
|
+
import {
|
|
99
|
+
creditSquadEvent,
|
|
100
|
+
creditSquadKill,
|
|
101
|
+
creditSquadLogs,
|
|
102
|
+
creditSquadSpellCast,
|
|
103
|
+
giveSquadQuestItem,
|
|
104
|
+
partyNeededQuestItems,
|
|
105
|
+
} from './squadQuestShare.ts';
|
|
106
|
+
import { questFightEventQuestId } from '../../domain/quests/questFight.ts';
|
|
107
|
+
import { creditQuestFightWin } from './questFightCredit.ts';
|
|
108
|
+
import { runCreatureDeathScripts, type ScriptSummon } from './applyDeathAndSpellScripts.ts';
|
|
109
|
+
import { deliverScriptMail } from './mail.ts';
|
|
110
|
+
import { makeDbScriptEvaluateCondition } from './applyTerminateFromDbScript.ts';
|
|
111
|
+
import {
|
|
112
|
+
rewardKillReputation,
|
|
113
|
+
atWarFactionIds,
|
|
114
|
+
reputationAuraModFromSpells,
|
|
115
|
+
} from '../../domain/creature/reputation.ts';
|
|
116
|
+
import { factionOf, racialSpells } from '../../domain/character/races.ts';
|
|
117
|
+
import { avAllowsLoot } from '../../domain/bg/avQuestItems.ts';
|
|
118
|
+
import { canAttackCreature } from '../../domain/creature/faction.ts';
|
|
119
|
+
import {
|
|
120
|
+
TEAM_ALLIANCE,
|
|
121
|
+
TEAM_HORDE,
|
|
122
|
+
} from '../../domain/run/death.ts';
|
|
123
|
+
import {
|
|
124
|
+
partyHasSkinner,
|
|
125
|
+
tryProfessionAutoSkin,
|
|
126
|
+
} from './killSkin.ts';
|
|
127
|
+
import {
|
|
128
|
+
canPickPocket,
|
|
129
|
+
rollPickPocket,
|
|
130
|
+
canRestockPickpocket,
|
|
131
|
+
startPickpocketRestock,
|
|
132
|
+
pickpocketLootKey,
|
|
133
|
+
} from '../../domain/professions/pickpocket.ts';
|
|
134
|
+
import { dismount as doDismount } from '../../domain/items/mounts.ts';
|
|
135
|
+
import { grantXp } from '../../progression/leveling.ts';
|
|
136
|
+
import {
|
|
137
|
+
killXpAmount, restBonusSpend,
|
|
138
|
+
} from '../../progression/xpGain.ts';
|
|
139
|
+
import { getCreatureAttackDistance } from '../../domain/creature/creatureAggro.ts';
|
|
140
|
+
import { CREATURE_FAMILY_ASSISTANCE_DELAY_MS } from '../../domain/creature/creatureAiConstants.ts';
|
|
141
|
+
import {
|
|
142
|
+
packAssistForPull,
|
|
143
|
+
guardAiAssistForPull,
|
|
144
|
+
questTargets,
|
|
145
|
+
spatialAddsForPull,
|
|
146
|
+
zoneHasSpatialSpawns,
|
|
147
|
+
} from '../../world/world.ts';
|
|
148
|
+
import { aoePullCap, aoePullClass, aoePullExtras } from '../../world/aoePull.ts';
|
|
149
|
+
import {
|
|
150
|
+
injectEnemySlotPosition,
|
|
151
|
+
isEnemyCasterKit,
|
|
152
|
+
PARTY_FOLLOW_YD,
|
|
153
|
+
} from '../combat/combatGeometry.ts';
|
|
154
|
+
import { detCos, detSin } from '../core/detMath.ts';
|
|
155
|
+
import { pullOpenYd, seedPullGeometry } from './combatSeed.ts';
|
|
156
|
+
import {
|
|
157
|
+
normalizeAutopilotConfig,
|
|
158
|
+
pullCountsFor,
|
|
159
|
+
resolvePullOpenYards,
|
|
160
|
+
firstOffensiveRotationRangeYd,
|
|
161
|
+
} from '../../autopilot/config.ts';
|
|
162
|
+
import {
|
|
163
|
+
buildEnemySpecFromCreature,
|
|
164
|
+
resolveSummonForFight,
|
|
165
|
+
} from './combatEnemySpec.ts';
|
|
166
|
+
import {
|
|
167
|
+
isComplete, isCleared, isUnlocked, encounterOf,
|
|
168
|
+
applyEncounterCleared, resolveEncounterPhase, encounterPhaseCount,
|
|
169
|
+
findObjectGate, useObjectGate,
|
|
170
|
+
hpThresholdEvents, flagsForHpThresholds, hasFlag, setFlag,
|
|
171
|
+
materializeEncounterCse,
|
|
172
|
+
routeEncounterList,
|
|
173
|
+
} from '../../world/instances.ts';
|
|
174
|
+
import { applyDmNorthKingPackClear } from '../../data/dungeons/dire-maul-north.ts';
|
|
175
|
+
import {
|
|
176
|
+
createThermapluggFightState,
|
|
177
|
+
thermapluggActiveFaceCount,
|
|
178
|
+
thermapluggButtonFaceIndex,
|
|
179
|
+
thermapluggButtonIsLive,
|
|
180
|
+
thermapluggDeactivateFace,
|
|
181
|
+
} from '../../data/dungeons/gnomeregan.ts';
|
|
182
|
+
import {
|
|
183
|
+
instanceDefaultOutdoors,
|
|
184
|
+
outdoorsForSpawnGuid,
|
|
185
|
+
} from '../../data/dungeons/apply-encounter-outdoors.ts';
|
|
186
|
+
import { getDungeonGameObject } from '../../data/dungeonGameObjects.ts';
|
|
187
|
+
import { GAMEOBJECT_TYPE_SUMMONING_RITUAL } from '../../domain/world/gameObjects.ts';
|
|
188
|
+
import { applyInstanceSummoningRitual } from './summoningRitualUse.ts';
|
|
189
|
+
import {
|
|
190
|
+
persistScriptInstanceData,
|
|
191
|
+
runScriptDevGoUse,
|
|
192
|
+
scriptDevGoUseAllowed,
|
|
193
|
+
scriptInstanceDataMap,
|
|
194
|
+
} from './scriptDevGoUse.ts';
|
|
195
|
+
import { scriptDevByGo } from '../../domain/scriptdev/index.ts';
|
|
196
|
+
import {
|
|
197
|
+
DEFAULT_PULL_FUSION,
|
|
198
|
+
DEFAULT_ROUTE_DENSITY,
|
|
199
|
+
fusionBatchFrom,
|
|
200
|
+
mergeRoutePulls,
|
|
201
|
+
resolveFusionEncounters,
|
|
202
|
+
isFusionBreak,
|
|
203
|
+
combatantCount,
|
|
204
|
+
MAYHEM_SOFT_CAP,
|
|
205
|
+
} from '../../world/dungeonRoute.ts';
|
|
206
|
+
import {
|
|
207
|
+
makeFusionBudgetGuard,
|
|
208
|
+
packBudgetPresetFor,
|
|
209
|
+
} from '../../world/packBudget.ts';
|
|
210
|
+
import { isConditionSatisfied } from '../spells/conditions.ts';
|
|
211
|
+
import type { ConditionTable } from '../spells/conditions.ts';
|
|
212
|
+
import { allySideAt, isAllySide } from '../core/events.ts';
|
|
213
|
+
import type { Side } from '../core/events.ts';
|
|
214
|
+
import {
|
|
215
|
+
GO_OSSIRIAN_CRYSTAL,
|
|
216
|
+
NPC_OSSIRIAN,
|
|
217
|
+
OSSIRIAN_WEAKNESS_SPELLS,
|
|
218
|
+
SPELL_OSSIRIAN_SUPREME,
|
|
219
|
+
} from '../../domain/scriptdev/ruinsOfAhnqiraj.ts';
|
|
220
|
+
import {
|
|
221
|
+
sessionSeedFromWorldCooldowns,
|
|
222
|
+
worldCooldownsFromSessionSnapshot,
|
|
223
|
+
} from '../spells/recovery.ts';
|
|
224
|
+
import {
|
|
225
|
+
bindFightEnter,
|
|
226
|
+
commitFightClock,
|
|
227
|
+
fightClockNow,
|
|
228
|
+
fightEnterOf,
|
|
229
|
+
isWallWorldClock,
|
|
230
|
+
peekFightChain,
|
|
231
|
+
peekOrAnchorFightChain,
|
|
232
|
+
} from './combatSessionClock.ts';
|
|
233
|
+
import {
|
|
234
|
+
companionCharacter,
|
|
235
|
+
companionLevel,
|
|
236
|
+
encodeAoeMode,
|
|
237
|
+
resolveAoeMode,
|
|
238
|
+
resolveOffHeal,
|
|
239
|
+
type Companion,
|
|
240
|
+
} from '../../domain/bots/squad.ts';
|
|
241
|
+
import { reconcileGamePartyAuras } from './partyAuras.ts';
|
|
242
|
+
import { carryCombatBuffs, type CombatBuffChip } from './persistCombatAuras.ts';
|
|
243
|
+
import { syncSheetStealthFromSession } from './oocAuraInterrupt.ts';
|
|
244
|
+
import { applyCompanionKillXp } from '../../domain/bots/companionXp.ts';
|
|
245
|
+
import { applyCompanionKillReputation } from '../../domain/bots/companionRep.ts';
|
|
246
|
+
import { requiredKeyItemEntry } from '../../domain/world/openLock.ts';
|
|
247
|
+
import type {
|
|
248
|
+
ActionResult,
|
|
249
|
+
BeginFight,
|
|
250
|
+
FightOutcome,
|
|
251
|
+
Game,
|
|
252
|
+
GameState,
|
|
253
|
+
PullAdditionalPackResult,
|
|
254
|
+
} from './types.ts';
|
|
255
|
+
import type { GameCtx } from './context.ts';
|
|
256
|
+
import { moneyMarked } from '../../domain/economy/money.ts';
|
|
257
|
+
import { createSimFacade, rawOf, type SimFacade } from '../combat/simFacade.ts';
|
|
258
|
+
|
|
259
|
+
// Fold a chained phase's push-time tallies onto the prior phase's. They are per-session and survive the
|
|
260
|
+
// event ring, so merging them is what makes a two-phase boss report its FIRST phase at all.
|
|
261
|
+
function mergePushTallies(
|
|
262
|
+
prior: CombatResult,
|
|
263
|
+
chained: CombatResult,
|
|
264
|
+
shiftMs: number,
|
|
265
|
+
): Partial<CombatResult> {
|
|
266
|
+
const out: Partial<CombatResult> = {};
|
|
267
|
+
if (prior.mechanics || chained.mechanics) {
|
|
268
|
+
const m: NonNullable<CombatResult['mechanics']> = { ...(prior.mechanics ?? {}) };
|
|
269
|
+
for (const [kind, slot] of Object.entries(chained.mechanics ?? {})) {
|
|
270
|
+
const before = m[kind];
|
|
271
|
+
m[kind] = before
|
|
272
|
+
? {
|
|
273
|
+
count: before.count + slot.count,
|
|
274
|
+
firstT: before.firstT,
|
|
275
|
+
lastT: slot.lastT + shiftMs,
|
|
276
|
+
}
|
|
277
|
+
: { count: slot.count, firstT: slot.firstT + shiftMs, lastT: slot.lastT + shiftMs };
|
|
278
|
+
}
|
|
279
|
+
out.mechanics = m;
|
|
280
|
+
}
|
|
281
|
+
if (prior.damageTakenBySide || chained.damageTakenBySide) {
|
|
282
|
+
const d: Record<string, number> = { ...(prior.damageTakenBySide ?? {}) };
|
|
283
|
+
for (const [side, n] of Object.entries(chained.damageTakenBySide ?? {})) {
|
|
284
|
+
d[side] = (d[side] ?? 0) + n;
|
|
285
|
+
}
|
|
286
|
+
out.damageTakenBySide = d;
|
|
287
|
+
}
|
|
288
|
+
if (prior.deaths?.length || chained.deaths?.length) {
|
|
289
|
+
out.deaths = [
|
|
290
|
+
...(prior.deaths ?? []),
|
|
291
|
+
...(chained.deaths ?? []).map((x) => ({ ...x, t: x.t + shiftMs })),
|
|
292
|
+
];
|
|
293
|
+
out.deathsTotal = (prior.deathsTotal ?? 0) + (chained.deathsTotal ?? 0);
|
|
294
|
+
}
|
|
295
|
+
const dropped = (prior.eventsDropped ?? 0) + (chained.eventsDropped ?? 0);
|
|
296
|
+
if (dropped > 0) out.eventsDropped = dropped;
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* A creature a corpse's death script asked for, plus the guid of the corpse that owes it. The floor
|
|
302
|
+
* places it where that body fell — SummonCreature takes the summoner's position when the script row
|
|
303
|
+
* carries no coordinate. Cite: ScriptMgr.cpp:2027-2051 TEMP_SPAWN_CREATURE; :2134-2180 CAST_SPELL.
|
|
304
|
+
*/
|
|
305
|
+
export type FloorDeathSummon = ScriptSummon & { fromGuid?: number };
|
|
306
|
+
|
|
307
|
+
/** Pending fight metadata owned by the combat session module. */
|
|
308
|
+
export type PendingFight = {
|
|
309
|
+
rng: Rng;
|
|
310
|
+
creatureEntry: number;
|
|
311
|
+
/** Actual enemy level after SelectLevel (or cdef.level). Used for Formulas::Gain. */
|
|
312
|
+
enemyLevel: number;
|
|
313
|
+
// Wave 4B pack members that joined (entry + SelectLevel level) for XP/loot/quest credit. Primary is
|
|
314
|
+
// `creatureEntry` / `enemyLevel`; pack is assistants only.
|
|
315
|
+
pack?: Array<{ entry: number; level: number; guid?: number }>;
|
|
316
|
+
encounter?: {
|
|
317
|
+
dungeonId: string;
|
|
318
|
+
encounterId: string;
|
|
319
|
+
phaseIndex?: number;
|
|
320
|
+
/** DR fusion: all route encounter ids cleared when this pull wins. */
|
|
321
|
+
fusedIds?: string[];
|
|
322
|
+
/** Kept so the phase chain can hand the next pull the same roof it opened under. */
|
|
323
|
+
outdoors?: boolean;
|
|
324
|
+
/** Sticky multi-phase snapshot (Ring of Law rolls); phase chain uses this. */
|
|
325
|
+
resolved?: EncounterDef;
|
|
326
|
+
};
|
|
327
|
+
/** S3: re-apply squad allies when multi-phase chains call startFight again. */
|
|
328
|
+
withSquad?: boolean;
|
|
329
|
+
/** Continuous floor shell: seats outlive their pull, so only creditFloorKills pays by guid. */
|
|
330
|
+
endless?: boolean;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export type CombatSessionExtras = {
|
|
334
|
+
deathFlowMode: () => DeathFlowMode;
|
|
335
|
+
busyTravelling: () => boolean;
|
|
336
|
+
buildSquadBattleParty: (targetEntry?: number) => {
|
|
337
|
+
allySpecs: CombatantSpec[];
|
|
338
|
+
allyActionProviders: (AllyActionProvider | null)[];
|
|
339
|
+
allyProviderSpecs: (ActionProviderSpec | null)[];
|
|
340
|
+
allyPetIndex: (number | null)[];
|
|
341
|
+
};
|
|
342
|
+
syncCompanionPetAfterLevel: (comp: Companion, leveledTo: readonly number[]) => void;
|
|
343
|
+
buildConditionCtx: () => NonNullable<CombatantSpec['conditionCtx']>;
|
|
344
|
+
conditionTableById: () => ConditionTable;
|
|
345
|
+
lootConditionSnapshot: () => {
|
|
346
|
+
quests: Record<string, 'none' | 'taken' | 'complete' | 'rewarded'>;
|
|
347
|
+
level: number;
|
|
348
|
+
/** SharedDefines.h Team: ALLIANCE=469, HORDE=67 — required for CONDITION_TEAM (type 6). */
|
|
349
|
+
team: number;
|
|
350
|
+
/** HonorRankInfo.rank 0..18. Cite: Conditions.cpp:236-243 CONDITION_PVP_RANK. */
|
|
351
|
+
pvpRank: number;
|
|
352
|
+
/** CONDITION_AD_COMMISSION_AURA (10). Cite: Conditions.cpp:228-234. */
|
|
353
|
+
auras?: number[];
|
|
354
|
+
/** CONDITION_INSTANCE_SCRIPT (18) — DM North guards-alive. */
|
|
355
|
+
instanceCondition?: (conditionId: number) => boolean;
|
|
356
|
+
/** CONDITION_COMPLETED_ENCOUNTER (31) — AQ40 Twin Emperors / Ouro SetData. */
|
|
357
|
+
completedEncounter?: (encounterId: number) => boolean;
|
|
358
|
+
/** CONDITION_WORLD_SCRIPT (40) — sWorldState.IsConditionFulfilled. */
|
|
359
|
+
worldScript: (conditionId: number, state: number) => boolean;
|
|
360
|
+
};
|
|
361
|
+
runAutoEquipUpgrades: (max?: number) => number;
|
|
362
|
+
/** Put the corpse's drops + coin in the loot container (resolveFight's holdLoot path). */
|
|
363
|
+
openLoot: Game['openLoot'];
|
|
364
|
+
applyGather: (
|
|
365
|
+
node: GatherNodeDef,
|
|
366
|
+
rng?: Rng,
|
|
367
|
+
) => ActionResult<{ yielded: LootDrop[]; skillUp?: number }>;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export type CombatSessionModule = {
|
|
371
|
+
methods: Pick<
|
|
372
|
+
Game,
|
|
373
|
+
| 'fight'
|
|
374
|
+
| 'beginFight'
|
|
375
|
+
| 'resolveFight'
|
|
376
|
+
| 'beginEncounter'
|
|
377
|
+
| 'beginFloorPull'
|
|
378
|
+
| 'beginFloorShell'
|
|
379
|
+
| 'injectFloorMember'
|
|
380
|
+
| 'noteFloorPackMember'
|
|
381
|
+
| 'creditFloorKills'
|
|
382
|
+
| 'pullAdditionalPack'
|
|
383
|
+
| 'useInstanceObject'
|
|
384
|
+
| 'lootInstanceObject'
|
|
385
|
+
| 'liveFightSession'
|
|
386
|
+
>;
|
|
387
|
+
startFight: (
|
|
388
|
+
creatureEntry: number,
|
|
389
|
+
bossSpellIds: number[] | undefined,
|
|
390
|
+
encounter:
|
|
391
|
+
| {
|
|
392
|
+
dungeonId: string;
|
|
393
|
+
encounterId: string;
|
|
394
|
+
pack?: { creatureEntry: number; count: number }[];
|
|
395
|
+
phaseIndex?: number;
|
|
396
|
+
fusedIds?: string[];
|
|
397
|
+
origin?: { x: number; y: number };
|
|
398
|
+
spawnGuids?: number[];
|
|
399
|
+
mapId?: number;
|
|
400
|
+
outdoors?: boolean;
|
|
401
|
+
/** Sim-time since instance enter — MT=2 poses (L05). */
|
|
402
|
+
elapsedMs?: number;
|
|
403
|
+
/** Sticky wave materialization (Ring of Law). */
|
|
404
|
+
resolved?: EncounterDef;
|
|
405
|
+
}
|
|
406
|
+
| undefined,
|
|
407
|
+
opts?: {
|
|
408
|
+
withSquad?: boolean; endless?: boolean; noPrimary?: boolean; sitting?: boolean;
|
|
409
|
+
attack?: boolean; noAoe?: boolean;
|
|
410
|
+
},
|
|
411
|
+
) => ActionResult<BeginFight>;
|
|
412
|
+
resolveFight: (session: CombatSession) => FightOutcome;
|
|
413
|
+
syncCharFromSession: (session: CombatSession) => void;
|
|
414
|
+
persistSessionCooldowns: (session: CombatSession) => void;
|
|
415
|
+
getPendingFight: () => PendingFight | null;
|
|
416
|
+
setPendingFight: (p: PendingFight | null) => void;
|
|
417
|
+
getLiveFightSession: () => CombatSession | null;
|
|
418
|
+
setLiveFightSession: (s: CombatSession | null) => void;
|
|
419
|
+
// `ally_N` this companion occupies in an open session, or null when it is not in the party that built it.
|
|
420
|
+
// By the pinned layout, never by squad index — hiring reorders `state.squad`.
|
|
421
|
+
allySideForCompanion: (session: CombatSession, compId: string) => string | null;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
export function createCombatSessionModule(
|
|
425
|
+
ctx: GameCtx,
|
|
426
|
+
extras: CombatSessionExtras,
|
|
427
|
+
): CombatSessionModule {
|
|
428
|
+
const { ds, state, log, takeDrop, refreshQuests, cap } = ctx;
|
|
429
|
+
const {
|
|
430
|
+
deathFlowMode,
|
|
431
|
+
busyTravelling,
|
|
432
|
+
buildSquadBattleParty,
|
|
433
|
+
syncCompanionPetAfterLevel,
|
|
434
|
+
buildConditionCtx,
|
|
435
|
+
conditionTableById,
|
|
436
|
+
lootConditionSnapshot,
|
|
437
|
+
runAutoEquipUpgrades,
|
|
438
|
+
openLoot,
|
|
439
|
+
applyGather,
|
|
440
|
+
} = extras;
|
|
441
|
+
|
|
442
|
+
// Creatures a spell summons, for a death script's CAST_SPELL row. Cite: SpellEffects.cpp:2729
|
|
443
|
+
// EffectSummonWild — Eject Sneed (5141) is effect 41, EffectMiscValue 643.
|
|
444
|
+
const spellSummonsOf = (spellId: number): { creatureEntry: number; count: number }[] => {
|
|
445
|
+
const sp = ds.spell(spellId);
|
|
446
|
+
if (!sp) return [];
|
|
447
|
+
const out: { creatureEntry: number; count: number }[] = [];
|
|
448
|
+
for (const eff of sp.effects) {
|
|
449
|
+
if (
|
|
450
|
+
eff.kind !== 'summon_wild' && eff.kind !== 'summon_guardian'
|
|
451
|
+
&& eff.kind !== 'summon_demon' && eff.kind !== 'summon_possessed'
|
|
452
|
+
) continue;
|
|
453
|
+
const entry = eff.creature | 0;
|
|
454
|
+
if (entry > 0) out.push({ creatureEntry: entry, count: Math.max(1, (eff.count ?? 1) | 0) });
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/** Unit.cpp:1382 ScriptsStart(SCRIPT_TYPE_CREATURE_DEATH) after a live kill credit. */
|
|
460
|
+
const applyCreatureDeathScripts = (entry: number): ScriptSummon[] => {
|
|
461
|
+
const rows = ds.creatureDeathScripts(entry);
|
|
462
|
+
if (!rows.length) return [];
|
|
463
|
+
const applied = runCreatureDeathScripts(entry, rows, {
|
|
464
|
+
questlog: state.questlog,
|
|
465
|
+
inventory: state.inventory,
|
|
466
|
+
itemOf: (e) => ds.item(e),
|
|
467
|
+
bagCap: cap(),
|
|
468
|
+
goRuntime: state.goRuntime,
|
|
469
|
+
goRuntimeKey: (e) => `${state.char.zoneId}:${e}`,
|
|
470
|
+
worldStates: state.worldStates,
|
|
471
|
+
relayScripts: (id) => ds.relayScripts(id),
|
|
472
|
+
evaluateCondition: makeDbScriptEvaluateCondition(
|
|
473
|
+
lootConditionSnapshot(),
|
|
474
|
+
conditionTableById(),
|
|
475
|
+
),
|
|
476
|
+
questOf: (e) => ds.quest(e),
|
|
477
|
+
spellSummonsOf,
|
|
478
|
+
log,
|
|
479
|
+
onMail: (rec) => deliverScriptMail(
|
|
480
|
+
ctx,
|
|
481
|
+
rec,
|
|
482
|
+
sessionClockNow(),
|
|
483
|
+
ds.creature(entry)?.name ?? 'Mailbox',
|
|
484
|
+
),
|
|
485
|
+
});
|
|
486
|
+
state.questlog = applied.questlog;
|
|
487
|
+
if (applied.inventory) state.inventory = applied.inventory;
|
|
488
|
+
if (applied.goRuntime) state.goRuntime = applied.goRuntime;
|
|
489
|
+
if (applied.worldStates) state.worldStates = applied.worldStates;
|
|
490
|
+
return applied.summons ?? [];
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// The rng of the in-flight fight is kept so loot and gold roll on the same seeded stream after combat
|
|
494
|
+
// ends. `encounter` credits a win to the instance run; `phaseIndex` tracks a multi-phase pull.
|
|
495
|
+
let pendingFight: PendingFight | null = null;
|
|
496
|
+
|
|
497
|
+
// ally_N to whose world cooldowns to write back, captured when the party was built: hiring between
|
|
498
|
+
// pulls reorders the squad, so resolving by index alone would stamp the wrong companion.
|
|
499
|
+
const sessionPartyLayout = new WeakMap<
|
|
500
|
+
CombatSession,
|
|
501
|
+
{ compIds: string[]; petAllyIndex: (number | null)[] }
|
|
502
|
+
>();
|
|
503
|
+
|
|
504
|
+
/** Most recent opened combat session; cleared on resolveFight. */
|
|
505
|
+
let liveFightSession: CombatSession | null = null;
|
|
506
|
+
// What THIS session has already been debited for. `itemsConsumed` is cumulative, so a pack clear
|
|
507
|
+
// and the final settle must charge the DELTA, never the running total.
|
|
508
|
+
let itemSpendApplied = new Map<number, number>();
|
|
509
|
+
// Companion spends, keyed `${allyIndex}:${entry}`. Same delta rule as the player ledger.
|
|
510
|
+
let allyItemSpendApplied = new Map<string, number>();
|
|
511
|
+
// The other direction, same delta rule: what a pull PUT in the bags. Drain Soul shards on the live
|
|
512
|
+
// floor were credited only at zone-out, so a warlock's stone run died mid-dungeon.
|
|
513
|
+
let itemGainApplied = new Map<number, number>();
|
|
514
|
+
let allyItemGainApplied = new Map<string, number>();
|
|
515
|
+
let shardGainApplied = 0;
|
|
516
|
+
// The seam every holder reads and writes through (PERF-RUNSHEET S6b). One per RUN rather than per
|
|
517
|
+
// fight, so a handle taken at mount time is already looking at the current pull.
|
|
518
|
+
let runFacade: SimFacade | null = null;
|
|
519
|
+
|
|
520
|
+
// A live fight reads enter plus session.t, else the per-state chain, with a wall anchor only before
|
|
521
|
+
// this game has opened one. Wall mode short-circuits: the sim runs at 1x to 4x and drifts off it.
|
|
522
|
+
const sessionClockNow = (session?: CombatSession | null): number => {
|
|
523
|
+
if (isWallWorldClock(state)) return Date.now();
|
|
524
|
+
const s = session ?? liveFightSession;
|
|
525
|
+
if (s && fightEnterOf(s) != null) return fightClockNow(s, s.t);
|
|
526
|
+
const chained = peekFightChain(state);
|
|
527
|
+
if (chained != null) return chained;
|
|
528
|
+
return peekOrAnchorFightChain(state, Date.now());
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/** KI-612: in-fight Pick Pocket — same gates/loot as OOC professions.pickPocket. */
|
|
532
|
+
const grantInFightPickPocket = (creatureEntry: number): {
|
|
533
|
+
ok: boolean;
|
|
534
|
+
drops?: LootDrop[];
|
|
535
|
+
goldCopper?: number;
|
|
536
|
+
reason?: string;
|
|
537
|
+
} => {
|
|
538
|
+
const cdef = ds.creature(creatureEntry);
|
|
539
|
+
if (!cdef) return { ok: false, reason: 'No such creature' };
|
|
540
|
+
const nowMs = sessionClockNow();
|
|
541
|
+
const spawnKey = `${state.char.zoneId}:${creatureEntry}`;
|
|
542
|
+
if (!canRestockPickpocket(state.pickpocketRestock, spawnKey, nowMs)) {
|
|
543
|
+
return { ok: false, reason: 'Already pickpocketed' };
|
|
544
|
+
}
|
|
545
|
+
// Combat cast already required stealth form; do not re-fail on char aura lag.
|
|
546
|
+
const gate = canPickPocket({
|
|
547
|
+
cls: state.char.cls,
|
|
548
|
+
targetAlive: true,
|
|
549
|
+
stealthOk: true,
|
|
550
|
+
});
|
|
551
|
+
if (!gate.ok) return { ok: false, reason: gate.reason };
|
|
552
|
+
const defLoot = (cdef as { pickpocketLootId?: number }).pickpocketLootId;
|
|
553
|
+
const lootId: number | string = (defLoot != null && defLoot > 0)
|
|
554
|
+
? defLoot
|
|
555
|
+
: pickpocketLootKey(creatureEntry);
|
|
556
|
+
{
|
|
557
|
+
const probe = ds.lootTable(lootId);
|
|
558
|
+
if (probe.length === 0 && typeof lootId === 'string') {
|
|
559
|
+
return { ok: false, reason: 'Target has no pockets' };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
|
|
563
|
+
const rolled = rollPickPocket({
|
|
564
|
+
pickpocketLootId: lootId,
|
|
565
|
+
getTable: (id) => {
|
|
566
|
+
const rows = ds.lootTable(id);
|
|
567
|
+
return rows.length > 0 ? rows : undefined;
|
|
568
|
+
},
|
|
569
|
+
rng,
|
|
570
|
+
playerLevel: state.char.level,
|
|
571
|
+
targetLevel: cdef.level,
|
|
572
|
+
skipSkillCheck: true,
|
|
573
|
+
});
|
|
574
|
+
if (!rolled.ok) return { ok: false, reason: rolled.reason };
|
|
575
|
+
const granted: LootDrop[] = [];
|
|
576
|
+
for (const d of rolled.drops) {
|
|
577
|
+
const took = takeDrop(d.item, d.count);
|
|
578
|
+
if (took > 0) granted.push({ item: d.item, count: took });
|
|
579
|
+
}
|
|
580
|
+
if (rolled.goldCopper > 0) {
|
|
581
|
+
state.char = { ...state.char, gold: state.char.gold + rolled.goldCopper };
|
|
582
|
+
}
|
|
583
|
+
state.pickpocketRestock = startPickpocketRestock(state.pickpocketRestock, spawnKey, nowMs);
|
|
584
|
+
return { ok: true, drops: granted, goldCopper: rolled.goldCopper };
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Sessions already paid out, with the outcome they produced: resolving is a one-time settlement. A UI
|
|
588
|
+
// remount that re-fired the end screen was duplicating rewards about six times a second.
|
|
589
|
+
const settled = new WeakMap<CombatSession, FightOutcome>();
|
|
590
|
+
|
|
591
|
+
// Shared fight setup: build the enemy spec, optionally with boss abilities, and open a seeded session.
|
|
592
|
+
// Outdoor pulls bring CallAssistance helpers, and `withSquad` folds the squad in as real allies.
|
|
593
|
+
const startFight = (
|
|
594
|
+
creatureEntry: number,
|
|
595
|
+
bossSpellIds: number[] | undefined,
|
|
596
|
+
encounter:
|
|
597
|
+
| {
|
|
598
|
+
dungeonId: string;
|
|
599
|
+
encounterId: string;
|
|
600
|
+
pack?: { creatureEntry: number; count: number }[];
|
|
601
|
+
/** D-4 multi-phase index (0 = first pull). Absent = 0. */
|
|
602
|
+
phaseIndex?: number;
|
|
603
|
+
/** DR fusion: mark all these ids defeated on win (default: [encounterId]). */
|
|
604
|
+
fusedIds?: string[];
|
|
605
|
+
// Geometry v2.1a: cluster centroid + spawn guids for real-coord seed. Absent / unresolved →
|
|
606
|
+
// seedMeleePackPositions fallback.
|
|
607
|
+
origin?: { x: number; y: number };
|
|
608
|
+
spawnGuids?: number[];
|
|
609
|
+
/** Classic map id for pack-clusters.json (e.g. 36 Deadmines). Optional — guids also resolve via full cluster scan. */
|
|
610
|
+
mapId?: number;
|
|
611
|
+
// EncounterDef.outdoors — false only when the bake measured a WMO roof over this pull. Absent is
|
|
612
|
+
// outdoors (TerrainInfo::IsOutdoors), so nothing outside a dungeon changes.
|
|
613
|
+
outdoors?: boolean;
|
|
614
|
+
/** Sim-time since instance enter — MT=2 poses (L05). */
|
|
615
|
+
elapsedMs?: number;
|
|
616
|
+
resolved?: EncounterDef;
|
|
617
|
+
}
|
|
618
|
+
| undefined,
|
|
619
|
+
// `noPrimary` opens the session with an EMPTY enemy list (no blanked creature 598 shell).
|
|
620
|
+
// `attack` reads the Attack pull counts; `noAoe` skips farm-dial extras (CallAssistance still fires).
|
|
621
|
+
opts?: {
|
|
622
|
+
withSquad?: boolean; endless?: boolean; noPrimary?: boolean; sitting?: boolean;
|
|
623
|
+
attack?: boolean; noAoe?: boolean;
|
|
624
|
+
},
|
|
625
|
+
): ActionResult<BeginFight> => {
|
|
626
|
+
// Dead / ghost cannot open combat (vanilla 1.12). Shared refuseIfDead (KI-861).
|
|
627
|
+
// Cite: domain/run/death.ts; Unit.h IsAlive; Spell.cpp SPELL_FAILED_CASTER_DEAD.
|
|
628
|
+
{
|
|
629
|
+
const dead = refuseIfDead(state.char.health, state.deathFlow);
|
|
630
|
+
if (dead) return dead;
|
|
631
|
+
}
|
|
632
|
+
// Combat stands you up — a food well-fed still waiting on the sit gate is cancelled.
|
|
633
|
+
if (state.char.pendingWellFed) {
|
|
634
|
+
state.char = tickPendingWellFed(state.char, ds, 0, false);
|
|
635
|
+
}
|
|
636
|
+
// Busy while wall-clock travelling.
|
|
637
|
+
if (busyTravelling()) {
|
|
638
|
+
return { ok: false, reason: 'You are travelling' };
|
|
639
|
+
}
|
|
640
|
+
// One live combat session only: a second beginFight must not replace the pointer or leave an orphan
|
|
641
|
+
// session resolveFight can still settle for XP. The multi-phase chain clears it first.
|
|
642
|
+
if (liveFightSession) {
|
|
643
|
+
return { ok: false, reason: 'Already in combat' };
|
|
644
|
+
}
|
|
645
|
+
// Bake first, then Wave-V SUMMON_ONLY_TEMPLATES (and any other summon stubs).
|
|
646
|
+
// Phase-chains / packs that only exist as summon-only rows must still begin.
|
|
647
|
+
const cdef = opts?.noPrimary
|
|
648
|
+
? undefined
|
|
649
|
+
: resolveCreatureDefForCombat(creatureEntry, (id) => ds.creature(id));
|
|
650
|
+
if (!cdef && !opts?.noPrimary) {
|
|
651
|
+
return { ok: false, reason: `Unknown creature entry ${creatureEntry}` };
|
|
652
|
+
}
|
|
653
|
+
// No friendly fire: same-faction and Friendly-plus NPCs cannot be engaged. Instance trash is always
|
|
654
|
+
// legal, and an active quest kill target stays attackable unless its credit is script-only.
|
|
655
|
+
if (cdef && !encounter && !canAttackCreature(ds, state.char.race, creatureEntry, atWarFactionIds(
|
|
656
|
+
ds.factions(), state.char.reputations, state.char.factionFlags, state.char.race, state.char.cls,
|
|
657
|
+
))) {
|
|
658
|
+
// noXp + friendly: script-credit workers (Lazy Peon) — not combat kill targets.
|
|
659
|
+
const scriptOnlyCredit = !!cdef.noXp;
|
|
660
|
+
const questKill = !scriptOnlyCredit
|
|
661
|
+
&& questTargets(ds, state.questlog).kills.has(creatureEntry);
|
|
662
|
+
const questFight = questFightEventQuestId(ds, state.questlog, creatureEntry) != null;
|
|
663
|
+
if (!questKill && !questFight) {
|
|
664
|
+
return { ok: false, reason: `${cdef.name} is friendly` };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
state.char.activeAuras = pruneExpiredAuras(state.char.activeAuras); // out-of-combat pruning on fight start
|
|
668
|
+
// Per-fight cloak / resist set. Cite: SpellEffects.cpp:3756; AUTOPILOT-PLAN §4 row 25.
|
|
669
|
+
{
|
|
670
|
+
const role = partyRoleOf(state.char, ds, state.squadRole);
|
|
671
|
+
const geared = applyEncounterPullGearToPlayer(
|
|
672
|
+
state.char,
|
|
673
|
+
state.inventory,
|
|
674
|
+
ds,
|
|
675
|
+
creatureEntry,
|
|
676
|
+
role === 'damage' ? 'dps' : role,
|
|
677
|
+
);
|
|
678
|
+
state.char = geared.char;
|
|
679
|
+
state.inventory = geared.inventory;
|
|
680
|
+
if (opts?.withSquad && state.squad?.length) {
|
|
681
|
+
const lvl = state.char.level;
|
|
682
|
+
state.squad = state.squad.map((c) => applyEncounterPullGearToCompanion(c, ds, creatureEntry, lvl));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Class-A party auras (Devotion / Blood Pact) drop when nobody in this subgroup holds them.
|
|
686
|
+
// Cite: SpellAuras.cpp AreaAura::Update ~592–624; RANGE-ARCHITECTURE §4.8.3.
|
|
687
|
+
reconcileGamePartyAuras(state, ds, Date.now(), opts?.withSquad === true);
|
|
688
|
+
state.lastCorpse = null; // a fresh fight: the previous corpse is no longer at hand to cannibalize
|
|
689
|
+
// Dismount on combat entry, mounts being out-of-combat only. Vanilla does not dismount on damage,
|
|
690
|
+
// so this is the port's own rule rather than an interrupt flag. Cite: Unit::Unmount.
|
|
691
|
+
if (state.char.mountedItem != null) {
|
|
692
|
+
const d = doDismount(state.char);
|
|
693
|
+
if (d.ok) {
|
|
694
|
+
state.char = d.char;
|
|
695
|
+
log('You dismount as combat begins.');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Feed Pet Effect breaks on combat (spell 1539 AuraInterruptFlags 0x3C03: ATTACKING|DAMAGE).
|
|
699
|
+
if (state.char.pet?.feedRemaining) {
|
|
700
|
+
state.char.pet = { ...state.char.pet, feedRemaining: undefined, feedPerSec: undefined };
|
|
701
|
+
log(`${state.char.pet.name} stops eating.`);
|
|
702
|
+
}
|
|
703
|
+
// The SEED, not just the stream: a Worker rebuilds the fight from it, and spec building draws
|
|
704
|
+
// nothing from the rng (1500 templates, 0 draws), so both sides start at the same position.
|
|
705
|
+
const sessionSeed = hashSeed(state.rngSeed, state.fightCounter++);
|
|
706
|
+
const rng = mulberry32(sessionSeed);
|
|
707
|
+
// Enemy stats: SelectLevel when the template carries raw scaling, else the legacy hp roll. Same seed
|
|
708
|
+
// means the same path, so the stream is stable. With no primary the floor opens empty and draws none.
|
|
709
|
+
const primary = cdef ? buildEnemySpecFromCreature(ds, cdef, rng, bossSpellIds) : undefined;
|
|
710
|
+
const enemySpecs: CombatantSpec[] = primary ? [primary.spec] : [];
|
|
711
|
+
const packMeta: Array<{ entry: number; level: number }> = [];
|
|
712
|
+
const packAssistInfo: NonNullable<BeginFight['packAssist']> = [];
|
|
713
|
+
|
|
714
|
+
// CallAssistance on combat enter, off the sourced spawns and the call radius. Empty with no positions
|
|
715
|
+
// or with NO_CALL_ASSIST, and never on the floor shell — it must not auto-pull outdoor helpers.
|
|
716
|
+
if (cdef && !encounter && !opts?.endless) {
|
|
717
|
+
const helpers = packAssistForPull(ds, state.char.zoneId, creatureEntry).slice();
|
|
718
|
+
const seenAssist = new Set(helpers.map((h) => h.entry));
|
|
719
|
+
for (const h of guardAiAssistForPull(ds, state.char.zoneId, creatureEntry)) {
|
|
720
|
+
if (seenAssist.has(h.entry)) continue;
|
|
721
|
+
seenAssist.add(h.entry);
|
|
722
|
+
helpers.push(h);
|
|
723
|
+
}
|
|
724
|
+
for (const h of helpers) {
|
|
725
|
+
const adef = resolveCreatureDefForCombat(h.entry, (id) => ds.creature(id));
|
|
726
|
+
if (!adef) continue;
|
|
727
|
+
const built = buildEnemySpecFromCreature(ds, adef, rng);
|
|
728
|
+
enemySpecs.push(built.spec);
|
|
729
|
+
packMeta.push({ entry: h.entry, level: built.level });
|
|
730
|
+
packAssistInfo.push({
|
|
731
|
+
entry: h.entry,
|
|
732
|
+
distance: h.distance,
|
|
733
|
+
name: adef.name,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (packAssistInfo.length) {
|
|
737
|
+
const names = packAssistInfo.map((p) => p.name).join(', ');
|
|
738
|
+
log(`${cdef.name} calls for help! ${names} ${packAssistInfo.length === 1 ? 'joins' : 'join'} the fight.`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Player-chosen outdoor AoE: nearby same-class spawns up to the surface's own pull counts —
|
|
743
|
+
// the Attack button's pair by hand, the run bar's pair on a run (config.ts pullCountsFor).
|
|
744
|
+
// Named bosses stay 1. CallAssistance above is separate and still fires.
|
|
745
|
+
const aoePullInfo: NonNullable<BeginFight['aoePull']> = [];
|
|
746
|
+
if (cdef && !encounter && !opts?.endless && !opts?.noAoe) {
|
|
747
|
+
const pullCfg = pullCountsFor(
|
|
748
|
+
normalizeAutopilotConfig(state.autopilot), opts?.attack ? 'attack' : 'auto',
|
|
749
|
+
);
|
|
750
|
+
const cap = aoePullCap(aoePullClass(ds, creatureEntry), pullCfg);
|
|
751
|
+
// The mobs the run still owes a quest fill the pull first: a grind on "kill 10 of each"
|
|
752
|
+
// drags in credit, not bystanders. Cite: world.ts questTargets (active, unmet killTargets).
|
|
753
|
+
const questKills = questTargets(ds, state.questlog).kills;
|
|
754
|
+
const extras = aoePullExtras(
|
|
755
|
+
ds,
|
|
756
|
+
state.char.zoneId,
|
|
757
|
+
creatureEntry,
|
|
758
|
+
pullCfg,
|
|
759
|
+
state.char.level,
|
|
760
|
+
{
|
|
761
|
+
race: state.char.race,
|
|
762
|
+
atWar: atWarFactionIds(
|
|
763
|
+
ds.factions(), state.char.reputations, state.char.factionFlags, state.char.race, state.char.cls,
|
|
764
|
+
),
|
|
765
|
+
...(questKills.size ? { prefer: questKills } : {}),
|
|
766
|
+
},
|
|
767
|
+
);
|
|
768
|
+
const assistEntries = new Set(packAssistInfo.map((p) => p.entry));
|
|
769
|
+
for (const h of extras) {
|
|
770
|
+
if (enemySpecs.length >= cap) break;
|
|
771
|
+
if (h.entry !== creatureEntry && assistEntries.has(h.entry)) continue;
|
|
772
|
+
const mdef = resolveCreatureDefForCombat(h.entry, (id) => ds.creature(id));
|
|
773
|
+
if (!mdef) continue;
|
|
774
|
+
const built = buildEnemySpecFromCreature(ds, mdef, rng);
|
|
775
|
+
enemySpecs.push(built.spec);
|
|
776
|
+
packMeta.push({ entry: h.entry, level: built.level });
|
|
777
|
+
aoePullInfo.push({ entry: h.entry, distance: h.distance, name: mdef.name });
|
|
778
|
+
}
|
|
779
|
+
if (aoePullInfo.length) {
|
|
780
|
+
const names = aoePullInfo.map((p) => p.name).join(', ');
|
|
781
|
+
log(`${names} ${aoePullInfo.length === 1 ? 'joins' : 'join'} the pull.`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// An authored dungeon pack expands into extra enemies through the same multi-enemy path as an outdoor
|
|
786
|
+
// assist, in authored order. Members join the pack meta so they credit XP, loot and quest kills.
|
|
787
|
+
if (encounter?.pack) {
|
|
788
|
+
for (const member of encounter.pack) {
|
|
789
|
+
if (member.count <= 0) continue;
|
|
790
|
+
const mdef = resolveCreatureDefForCombat(member.creatureEntry, (id) => ds.creature(id));
|
|
791
|
+
if (!mdef) {
|
|
792
|
+
log(`Pack member skipped: no template for creature entry ${member.creatureEntry}.`);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
for (let n = 0; n < member.count; n++) {
|
|
796
|
+
const built = buildEnemySpecFromCreature(ds, mdef, rng);
|
|
797
|
+
enemySpecs.push(built.spec);
|
|
798
|
+
packMeta.push({ entry: member.creatureEntry, level: built.level });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// The chain clock, read before the spec is built because the pull's own sneak is on it.
|
|
804
|
+
const now0 = peekOrAnchorFightChain(state, Date.now());
|
|
805
|
+
// Rogues and ferals open every pull at full energy: energy ticks out of combat at the same rate, but
|
|
806
|
+
// chaining pulls without a full regen window left openers stuck at 0-20.
|
|
807
|
+
if (powerType(state.char) === 'energy') {
|
|
808
|
+
state.char.power = maxPower(state.char, ds);
|
|
809
|
+
}
|
|
810
|
+
// The player's engage, where a party rogue already stealths (squadDungeon.ts:504-506). Stealth is
|
|
811
|
+
// NOT_IN_COMBAT_ONLY_PEACEFUL, so this is the last moment the pack's stealth opener can arm.
|
|
812
|
+
{
|
|
813
|
+
const sneak = seedPrePullStealth(state.char, state.spellbook, ds, {
|
|
814
|
+
spellCooldowns: state.spellCooldowns, now: now0,
|
|
815
|
+
});
|
|
816
|
+
// Spend the entry cost so the loop below carries it into the fight and a chain pull inside the
|
|
817
|
+
// 10 s recovery arrives unstealthed. Cite: spell_template 1784-1787.
|
|
818
|
+
const sneakCd = sneak ? (ds.spell(sneak.id)?.cooldownMs ?? 0) : 0;
|
|
819
|
+
if (sneak && sneakCd > 0) (state.spellCooldowns ??= {})[sneak.id] = now0 + sneakCd;
|
|
820
|
+
}
|
|
821
|
+
const player = buildCombatant(state.char, state.spellbook, ds, state.inventory);
|
|
822
|
+
player.items = combatItems(state.char, state.inventory, ds); // potions/healthstone usable mid-fight
|
|
823
|
+
// CheckItems / TakeReagents bag snapshot (Soul Shard on Shadowburn, runes, …). Mid-fight casts mutate
|
|
824
|
+
// bagItemCounts → itemsConsumed at resolveFight. Cite: Spell.cpp CheckItems / TakeReagents; KI-804.
|
|
825
|
+
{
|
|
826
|
+
const bag = seedReagentBagCounts(player.spells, (item) => countItem(state.inventory, item));
|
|
827
|
+
if (bag) player.bagItemCounts = bag;
|
|
828
|
+
}
|
|
829
|
+
// The player is a party member like any other and ALWAYS carries a role. Leaving it unset excluded
|
|
830
|
+
// the human from threat seeding, peel, heal triage and the front and back bands.
|
|
831
|
+
player.groupRole = partyRoleOf(state.char, ds, state.squadRole);
|
|
832
|
+
// Conditions player-state snapshot (REPUTATION_RANK/SKILL/ACTIVE_GAME_EVENT/TEAM).
|
|
833
|
+
// Precomputed plain data so the session stays deterministic. Cite: Conditions.cpp Meets.
|
|
834
|
+
player.conditionCtx = buildConditionCtx();
|
|
835
|
+
|
|
836
|
+
// Seed pre-existing world cooldowns into the session so usable checks inside the fight respect them.
|
|
837
|
+
// Values are remaining ms from t=0, on the chain clock rather than wall time.
|
|
838
|
+
const initSpellCds: Record<number, number> = {};
|
|
839
|
+
const initCatCds: Record<number, number> = {};
|
|
840
|
+
for (const sp of player.spells || []) {
|
|
841
|
+
const rem = Math.max(0, (state.spellCooldowns?.[sp.id] ?? 0) - now0);
|
|
842
|
+
if (rem > 0) initSpellCds[sp.id] = rem;
|
|
843
|
+
if (sp.category != null) {
|
|
844
|
+
const crem = Math.max(0, (state.categoryCooldowns?.[sp.category] ?? 0) - now0);
|
|
845
|
+
if (crem > 0) initCatCds[sp.category] = crem;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// The loop above walks SPELLS only, so a potion drunk in the world did not lock its shared category
|
|
849
|
+
// inside the next pull. Item personal cooldowns live on the use-spell id, so they ride the same map.
|
|
850
|
+
for (const it of player.items || []) {
|
|
851
|
+
if (it.category != null && it.categoryCooldownMs != null && it.categoryCooldownMs > 0) {
|
|
852
|
+
const crem = Math.max(0, (state.categoryCooldowns?.[it.category] ?? 0) - now0);
|
|
853
|
+
if (crem > 0) initCatCds[it.category] = Math.max(initCatCds[it.category] ?? 0, crem);
|
|
854
|
+
}
|
|
855
|
+
const useSp = ds.item(it.entry)?.useSpell;
|
|
856
|
+
if (useSp != null && it.cooldownMs > 0) {
|
|
857
|
+
const rem = Math.max(0, (state.spellCooldowns?.[useSp] ?? 0) - now0);
|
|
858
|
+
if (rem > 0) initSpellCds[useSp] = Math.max(initSpellCds[useSp] ?? 0, rem);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// S3 group pulls: optional real squad allies. Empty squad (or withSquad off) leaves
|
|
862
|
+
// allySpecs null so createSession stays solo-equivalent (no extra rng draws).
|
|
863
|
+
let allySpecs: CombatantSpec[] | null = null;
|
|
864
|
+
// null provider = pet uses S0 damage heuristics (createSession accepts null). Cite: combat/types.ts.
|
|
865
|
+
let allyActionProviders: (AllyActionProvider | null)[] | undefined;
|
|
866
|
+
let allyProviderSpecs: (ActionProviderSpec | null)[] | undefined;
|
|
867
|
+
let allyPetIndex: (number | null)[] | undefined;
|
|
868
|
+
if (opts?.withSquad) {
|
|
869
|
+
const party = buildSquadBattleParty(creatureEntry);
|
|
870
|
+
if (party.allySpecs.length > 0) {
|
|
871
|
+
allySpecs = party.allySpecs;
|
|
872
|
+
allyActionProviders = party.allyActionProviders;
|
|
873
|
+
allyProviderSpecs = party.allyProviderSpecs;
|
|
874
|
+
allyPetIndex = party.allyPetIndex;
|
|
875
|
+
// squadDungeon seeds companion CDs with Date.now — restamp on the fight clock (KI-956).
|
|
876
|
+
const squad = state.squad ?? [];
|
|
877
|
+
for (let i = 0; i < squad.length; i++) {
|
|
878
|
+
const comp = squad[i]!;
|
|
879
|
+
const spec = allySpecs[i];
|
|
880
|
+
if (!spec) continue;
|
|
881
|
+
spec.initialSpellCooldowns = sessionSeedFromWorldCooldowns(comp.spellCooldowns, now0);
|
|
882
|
+
spec.initialCategoryCooldowns = sessionSeedFromWorldCooldowns(comp.categoryCooldowns, now0);
|
|
883
|
+
const petIdx = allyPetIndex[i];
|
|
884
|
+
if (petIdx != null) {
|
|
885
|
+
const petSpec = allySpecs[petIdx];
|
|
886
|
+
if (petSpec) {
|
|
887
|
+
petSpec.initialSpellCooldowns =
|
|
888
|
+
sessionSeedFromWorldCooldowns(comp.pet?.spellCooldowns, now0);
|
|
889
|
+
petSpec.initialCategoryCooldowns =
|
|
890
|
+
sessionSeedFromWorldCooldowns(comp.pet?.categoryCooldowns, now0);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// The player's own rotation for the ticks the ENGINE drives them. Without it those fights fell to
|
|
897
|
+
// pickAction's class heuristics, so an auto-resolved pull ignored the rotation the user wrote.
|
|
898
|
+
const autoCfgForPlayer = normalizeAutopilotConfig(state.autopilot);
|
|
899
|
+
// Rotation-editor play knobs that ride the spec into the motion phase.
|
|
900
|
+
player.movement = autoCfgForPlayer.movement;
|
|
901
|
+
player.packClose = autoCfgForPlayer.aoeMode !== 'single';
|
|
902
|
+
// Rebuild untouched defaults when the kit, level or spec drifted, so a headless fight matches the UI.
|
|
903
|
+
// The profile in force is the one it fights from, and its own stand yards.
|
|
904
|
+
const activeProfile = activeRotationProfile(state);
|
|
905
|
+
// Macro rules compile to spell and item rules HERE, before anything reads the list: the open yards,
|
|
906
|
+
// the stand band and the provider must all see the spell a macro actually casts.
|
|
907
|
+
// Bag potions ride the same inject companions get; rotationForCombat is the UI twin.
|
|
908
|
+
const liveRot = rotationWithMacros(
|
|
909
|
+
syncCompanionBagItemRules(
|
|
910
|
+
ensurePlayerRotation(state, ds, activeProfile),
|
|
911
|
+
state.char,
|
|
912
|
+
state.inventory,
|
|
913
|
+
ds,
|
|
914
|
+
),
|
|
915
|
+
playerMacroExpandEnv(state, ds, ctx.opts),
|
|
916
|
+
);
|
|
917
|
+
stampStandYards(player, {
|
|
918
|
+
explicit: liveRot?.standYards,
|
|
919
|
+
specName: liveRot?.autoSig?.spec ?? state.rotationSpec,
|
|
920
|
+
offenseYd: firstOffensiveRotationRangeYd(liveRot, (id) => ds.spell(id)),
|
|
921
|
+
});
|
|
922
|
+
const playerRot = liveRot && isRotationValid(liveRot, state.char.cls) ? liveRot : null;
|
|
923
|
+
// The plain half first, so the Worker's copy is built from the SAME spec (actionProviderSpec.ts).
|
|
924
|
+
const playerProviderSpec: ActionProviderSpec | null = playerRot
|
|
925
|
+
? {
|
|
926
|
+
rotation: playerRot,
|
|
927
|
+
...(creatureEntry != null ? { targetEntry: creatureEntry } : {}),
|
|
928
|
+
aoeMode: autoCfgForPlayer.aoeMode === 'aoe' || autoCfgForPlayer.aoeMode === 'single'
|
|
929
|
+
? autoCfgForPlayer.aoeMode
|
|
930
|
+
: 'auto',
|
|
931
|
+
offHealAllowed: autoCfgForPlayer.offHeal === true,
|
|
932
|
+
}
|
|
933
|
+
: null;
|
|
934
|
+
const playerActionProvider = playerProviderSpec
|
|
935
|
+
? rotationActionProvider(playerProviderSpec.rotation, creatureEntry, {
|
|
936
|
+
aoeMode: playerProviderSpec.aoeMode,
|
|
937
|
+
offHealAllowed: playerProviderSpec.offHealAllowed,
|
|
938
|
+
isMultiSpell: (id) => isMultiTargetCombatSpell(ds.spell(id)),
|
|
939
|
+
isOocKeepUp: (id) => isCompanionKeepUpSpell(ds.spell(id)),
|
|
940
|
+
})
|
|
941
|
+
: null;
|
|
942
|
+
|
|
943
|
+
// Every real product fight seeds per-unit yards, so spell range, AoE radius, motion and area auras
|
|
944
|
+
// all engage. Branch policy and the "never open on the melee arc" rule live in combatSeed.ts.
|
|
945
|
+
const petSpecForSession = buildPet(state.char, ds);
|
|
946
|
+
// The pet keeps its own world CDs (Dash/Dive/Sacrifice are minutes) — same store, same clock.
|
|
947
|
+
if (petSpecForSession) {
|
|
948
|
+
petSpecForSession.initialSpellCooldowns =
|
|
949
|
+
sessionSeedFromWorldCooldowns(state.char.pet?.spellCooldowns, now0);
|
|
950
|
+
petSpecForSession.initialCategoryCooldowns =
|
|
951
|
+
sessionSeedFromWorldCooldowns(state.char.pet?.categoryCooldowns, now0);
|
|
952
|
+
}
|
|
953
|
+
// Open yards come from the autopilot's pull setting, not only creature detection, so a frostbolt pull
|
|
954
|
+
// seeds at bolt range. They belong to the pulled creature, and a floor body sets its own band.
|
|
955
|
+
const creatureOpenYd = primary && cdef
|
|
956
|
+
? pullOpenYd({
|
|
957
|
+
creatureLevel: primary.level,
|
|
958
|
+
playerLevel: state.char.level,
|
|
959
|
+
detectionRange: cdef.detectionRange,
|
|
960
|
+
})
|
|
961
|
+
: 0;
|
|
962
|
+
const primaryOpenYd = resolvePullOpenYards(
|
|
963
|
+
normalizeAutopilotConfig(state.autopilot),
|
|
964
|
+
// The active profile's opener decides the open range: a healer on its DPS list pulls at
|
|
965
|
+
// bolt range, not at the heal band.
|
|
966
|
+
liveRot,
|
|
967
|
+
(id) => ds.spell(id),
|
|
968
|
+
creatureOpenYd,
|
|
969
|
+
);
|
|
970
|
+
const seededGeometry = seedPullGeometry({
|
|
971
|
+
player,
|
|
972
|
+
enemySpecs,
|
|
973
|
+
allySpecs,
|
|
974
|
+
includePet: petSpecForSession != null,
|
|
975
|
+
primaryOpenYd,
|
|
976
|
+
helperDistancesYd: packAssistInfo.map((h) => h.distance),
|
|
977
|
+
// The dial's own mobs seat too, or the pull is a fight against bodies the stage never draws.
|
|
978
|
+
extraDistancesYd: aoePullInfo.map((h) => h.distance),
|
|
979
|
+
primaryEntry: creatureEntry,
|
|
980
|
+
cluster: encounter
|
|
981
|
+
? {
|
|
982
|
+
origin: encounter.origin,
|
|
983
|
+
spawnGuids: encounter.spawnGuids,
|
|
984
|
+
mapId: encounter.mapId,
|
|
985
|
+
fusedIds: encounter.fusedIds,
|
|
986
|
+
elapsedMs: encounter.elapsedMs,
|
|
987
|
+
}
|
|
988
|
+
: undefined,
|
|
989
|
+
// The pack's melee-vs-caster split reads the kit; the store says what it can reach.
|
|
990
|
+
spellOf: (id) => ds.spell(id),
|
|
991
|
+
});
|
|
992
|
+
const unitPositionsYd = seededGeometry.positions;
|
|
993
|
+
// T3: the frame these offsets live in, so the tactical view knows whether it may draw
|
|
994
|
+
// them on the floorplan or must fall back to an abstract backdrop.
|
|
995
|
+
const geometryAnchor = seededGeometry.anchor;
|
|
996
|
+
// The endless floor MUST clamp motion to the mesh or the party Euclidean-walks into roofed void, and
|
|
997
|
+
// sight comes with it. The party seeds as a tight clump on the origin until the controller restamps.
|
|
998
|
+
let sessionPositions = unitPositionsYd;
|
|
999
|
+
let sessionAnchor = geometryAnchor;
|
|
1000
|
+
/** Does this map have geometry to ask? Sight and mesh walk both ride on it. */
|
|
1001
|
+
let floorGeometry = false;
|
|
1002
|
+
if (opts?.endless && encounter?.origin) {
|
|
1003
|
+
sessionAnchor = { frame: 'world-translated', origin: encounter.origin, mapId: encounter.mapId };
|
|
1004
|
+
sessionPositions = { player: { x: 0, y: 0 } };
|
|
1005
|
+
if (allySpecs?.length) {
|
|
1006
|
+
for (let i = 0; i < allySpecs.length; i++) {
|
|
1007
|
+
const ring = Math.floor(i / 6) + 1;
|
|
1008
|
+
const slot = i % 6;
|
|
1009
|
+
const a = (slot / 6) * 2 * Math.PI + (ring % 2 === 0 ? Math.PI / 6 : 0);
|
|
1010
|
+
const r = 1.6 * ring;
|
|
1011
|
+
sessionPositions[`ally_${i}`] = { x: detCos(a) * r, y: detSin(a) * r };
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (encounter.mapId != null) {
|
|
1015
|
+
// The mesh is the enable gate. A raster that had not decoded turned sight OFF for a whole session
|
|
1016
|
+
// with a mesh sitting in the registry, and the raster is gone now.
|
|
1017
|
+
floorGeometry = navMeshForMap(encounter.mapId) != null;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// Seed ScriptDev instanceData from the run (TYPE_PROTECTORS remaining survives across
|
|
1021
|
+
// the endless floor session / save). Cite: sunken_temple.cpp:162-178; instances.ts scriptInstanceData.
|
|
1022
|
+
const seededInstanceData = state.instance
|
|
1023
|
+
? Object.fromEntries(scriptInstanceDataMap(state.instance))
|
|
1024
|
+
: undefined;
|
|
1025
|
+
// Hoisted so the Worker payload is DERIVED from the same object the main thread uses, not from a
|
|
1026
|
+
// second list that drifts (PERF-RUNSHEET S6b).
|
|
1027
|
+
const sessionCfg: CombatConfig = {
|
|
1028
|
+
initialSpellCooldowns: initSpellCds,
|
|
1029
|
+
initialCategoryCooldowns: initCatCds,
|
|
1030
|
+
sitting: opts?.sitting,
|
|
1031
|
+
// `encounter` is optional — a plain game.fight(creatureEntry) pull has none, and every
|
|
1032
|
+
// other use in this function guards it. Unguarded, a world pull threw on .mapId.
|
|
1033
|
+
...(encounter?.mapId != null
|
|
1034
|
+
? {
|
|
1035
|
+
combatObjects: combatObjectsFromBake(encounter.mapId, creatureEntry),
|
|
1036
|
+
combatObjectsReusable: combatObjectsReusableFromBake(encounter.mapId, creatureEntry),
|
|
1037
|
+
// Where each bag entry stands, so the clicker walks to it.
|
|
1038
|
+
combatObjectSites: combatObjectSitesFromBake(
|
|
1039
|
+
encounter.mapId,
|
|
1040
|
+
combatObjectsFromBake(encounter.mapId, creatureEntry),
|
|
1041
|
+
sessionAnchor?.frame === 'world-translated' ? sessionAnchor.origin : undefined,
|
|
1042
|
+
),
|
|
1043
|
+
// The click's PAYLOAD, plus every trap gameobject those spells can light with its own spawn.
|
|
1044
|
+
// The spell is in no creature kit, so it is resolved here off the bake and handed down.
|
|
1045
|
+
spellTrapSites: spellTrapSitesFromBake({
|
|
1046
|
+
mapId: encounter.mapId,
|
|
1047
|
+
spellIds: enemySpecs.flatMap((e) => (e.spells ?? []).map((sp) => sp.id | 0)),
|
|
1048
|
+
anchorOrigin: sessionAnchor?.frame === 'world-translated'
|
|
1049
|
+
? sessionAnchor.origin
|
|
1050
|
+
: undefined,
|
|
1051
|
+
gameObject: (entry) => ds.gameObject(entry),
|
|
1052
|
+
spell: (id) => ds.spell(id),
|
|
1053
|
+
}),
|
|
1054
|
+
combatObjectTraps: combatObjectTrapsFromBake({
|
|
1055
|
+
mapId: encounter.mapId,
|
|
1056
|
+
goEntries: combatObjectsFromBake(encounter.mapId, creatureEntry),
|
|
1057
|
+
anchorOrigin: sessionAnchor?.frame === 'world-translated'
|
|
1058
|
+
? sessionAnchor.origin
|
|
1059
|
+
: undefined,
|
|
1060
|
+
goUseScripts: (goEntry) => ds.goUseScripts(goEntry),
|
|
1061
|
+
gameObject: (entry) => ds.gameObject(entry),
|
|
1062
|
+
spell: (id) => ds.spell(id),
|
|
1063
|
+
}),
|
|
1064
|
+
}
|
|
1065
|
+
: {}),
|
|
1066
|
+
// THE per-tick inputs (PERF-RUNSHEET S6b mechanism 2): built once at the top of each step, never
|
|
1067
|
+
// asked for during it, so the sim can run where these values are unreachable.
|
|
1068
|
+
tickInputs: () => {
|
|
1069
|
+
// Quest entries the log holds as 'active'. A 1.12 log caps around 20, so this is nothing.
|
|
1070
|
+
const activeQuests = new Set<number>();
|
|
1071
|
+
for (const q of state.questlog.quests) {
|
|
1072
|
+
if (q.status === 'active') activeQuests.add(q.entry | 0);
|
|
1073
|
+
}
|
|
1074
|
+
// The two knobs the provider used to read off a LIVE companion ref (S6b). `ally_i` is squad
|
|
1075
|
+
// order — squadDungeon pushes one ally spec per companion before any pet.
|
|
1076
|
+
const companionKnobs = new Map<string, { aoeMode: number; offHeal: boolean }>();
|
|
1077
|
+
const squad = state.squad ?? [];
|
|
1078
|
+
for (let i = 0; i < squad.length; i++) {
|
|
1079
|
+
const comp = squad[i]!;
|
|
1080
|
+
companionKnobs.set(`ally_${i}`, {
|
|
1081
|
+
aoeMode: encodeAoeMode(resolveAoeMode(comp)),
|
|
1082
|
+
offHeal: resolveOffHeal(comp),
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
const mapId = encounter?.mapId;
|
|
1086
|
+
if (mapId == null) return { activeQuests, companionKnobs };
|
|
1087
|
+
// Which bag entries are worth pressing. Only Gnomeregan answers today: all six gnomeface
|
|
1088
|
+
// buttons are present all fight and only the lit face closes a vent.
|
|
1089
|
+
const actionableObjects = new Set<number>();
|
|
1090
|
+
for (const goEntry of combatObjectsFromBake(mapId, creatureEntry)) {
|
|
1091
|
+
// The gnomeface path owns its own liveness (six buttons, one lit).
|
|
1092
|
+
if (thermapluggButtonFaceIndex(goEntry) != null) {
|
|
1093
|
+
if (thermapluggButtonIsLive(state.instance?.thermapluggFaces, goEntry)) {
|
|
1094
|
+
actionableObjects.add(goEntry);
|
|
1095
|
+
}
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
// Otherwise the click has to be able to DO something: `useInstanceObject` refuses with
|
|
1099
|
+
// "Nothing happens" when the def hosts no objectGate for this entry, and an unanswerable
|
|
1100
|
+
// row still costs the walk. Razorgore's orb 177808 is named by a use_object tactics row
|
|
1101
|
+
// while blackwing-lair's def has no objectGate at all, so the raid spent every Razorgore
|
|
1102
|
+
// attempt walking to it: move 803, moveDone 0, 21 minutes of sim per attempt and 615 dps
|
|
1103
|
+
// from 40 bodies.
|
|
1104
|
+
const encDef = ds.instance(state.instance?.dungeonId ?? '');
|
|
1105
|
+
if (encDef && findObjectGate(encDef, goEntry)) actionableObjects.add(goEntry);
|
|
1106
|
+
}
|
|
1107
|
+
return { activeQuests, companionKnobs, actionableObjects };
|
|
1108
|
+
},
|
|
1109
|
+
// Baked `conditions` rows so spells carrying only a conditionId resolve in-fight.
|
|
1110
|
+
conditionTable: conditionTableById(),
|
|
1111
|
+
// The one in-fight DataStore read the combat engine cannot do itself: SCALES_WITH_CREATURE_LEVEL
|
|
1112
|
+
// needs BaseDamage(level, unitClass). Cite: Object.cpp:3005-3013; dataStore.creatureClassLvlStats.
|
|
1113
|
+
creatureBaseDamageAt: (lvl, unitClass) => ds.creatureClassLvlStats(lvl, unitClass)?.baseDamage,
|
|
1114
|
+
// The rank ladder, for aura identity (P7). Cite: dataStore.spellChain.
|
|
1115
|
+
spellChainOf: (spellId) => ds.spellChain(spellId),
|
|
1116
|
+
// Spell 1604 Dazed, for the back-arc NPC white hit. Cite: Unit.cpp:2227.
|
|
1117
|
+
dazeSpell: ds.spell(DAZED_SPELL_ID),
|
|
1118
|
+
// AuraInterruptFlags by id, for holders the engine only knows by spell id.
|
|
1119
|
+
auraInterruptFlagsOf: (spellId) => (ds.spell(spellId)?.auraInterruptFlags ?? 0) | 0,
|
|
1120
|
+
// The global spell store: a triggered payload resolves by id, not by kit membership.
|
|
1121
|
+
// Cite: Unit.cpp:1447-1449; ENGINE-REBUILD.md E6.
|
|
1122
|
+
spellOf: (id) => ds.spell(id),
|
|
1123
|
+
...(seededInstanceData && Object.keys(seededInstanceData).length
|
|
1124
|
+
? { instanceData: seededInstanceData }
|
|
1125
|
+
: {}),
|
|
1126
|
+
// Map::IsDungeon: inside an instance run. Keeps the outdoor far-evade despawn off the floor, so a
|
|
1127
|
+
// boss chased 150 yd from its start point walks home instead of vanishing.
|
|
1128
|
+
...(state.instance ? { dungeonMap: true } : {}),
|
|
1129
|
+
// Continuous floor: a pack wipe does not end the session, a member with nothing to fight walks
|
|
1130
|
+
// back to 14 yd, and a mob you have not touched does not move — the old t=0 seed ran untouched
|
|
1131
|
+
// helpers 27 yd each.
|
|
1132
|
+
...(opts?.endless
|
|
1133
|
+
? {
|
|
1134
|
+
endless: true,
|
|
1135
|
+
seedAttackStart: false,
|
|
1136
|
+
maxMs: 48 * 60 * 60_000,
|
|
1137
|
+
// Flat for every roster size. The 35 yd raid leash this replaced existed only to let the back
|
|
1138
|
+
// line camp 30 yd out, and the floor formation is 3 yd deep at any size.
|
|
1139
|
+
partyFollowYd: PARTY_FOLLOW_YD,
|
|
1140
|
+
acquireRangeYd: 45,
|
|
1141
|
+
}
|
|
1142
|
+
: {
|
|
1143
|
+
seedAttackStart: false,
|
|
1144
|
+
packAssistDelayMs: CREATURE_FAMILY_ASSISTANCE_DELAY_MS,
|
|
1145
|
+
}),
|
|
1146
|
+
// Mid-fight EventAI inject: an unresolved template skips with an honest log and combat continues.
|
|
1147
|
+
// The spell list's combat condition gates it. Cite: UnitAI.cpp:1218-1224.
|
|
1148
|
+
combatConditionTable: {
|
|
1149
|
+
combatCondition: (id) => ds.combatCondition(id),
|
|
1150
|
+
unitCondition: (id) => ds.unitCondition(id),
|
|
1151
|
+
},
|
|
1152
|
+
resolveSummon: (creatureId) => {
|
|
1153
|
+
const spec = resolveSummonForFight(ds, creatureId, rng);
|
|
1154
|
+
if (!spec) {
|
|
1155
|
+
log(`Summon skipped: no template for creature entry ${creatureId}.`);
|
|
1156
|
+
}
|
|
1157
|
+
return spec;
|
|
1158
|
+
},
|
|
1159
|
+
// ScriptDev SpellHit rows gated on an open quest. 'active' is QUEST_STATUS_INCOMPLETE; 'complete'
|
|
1160
|
+
// means objectives met and not turned in. Cite: stranglethorn_vale.cpp:55-64.
|
|
1161
|
+
// `isQuestIncomplete` was a closure the tick called mid-step; the ACTIVE set now rides in with
|
|
1162
|
+
// the tick (S6b mechanism 2). A 1.12 log holds ~20 quests, so building it per tick is nothing.
|
|
1163
|
+
// The recorded-effect applier (S6b mechanism 1). The tick pushes; `ctx.step` flushes here at the
|
|
1164
|
+
// end of the step, and off-thread the same array rides back with the batch. One switch, one place.
|
|
1165
|
+
applyEffect: (effect) => {
|
|
1166
|
+
switch (effect.kind) {
|
|
1167
|
+
// ACTION_T_QUEST_EVENT gives the same credit as a GOOBER event. The solo product has no
|
|
1168
|
+
// party, so personal credit is the group-event equivalent. Cite: CreatureEventAI.cpp ~847-862.
|
|
1169
|
+
case 'questEvent': {
|
|
1170
|
+
const questId = effect.questId;
|
|
1171
|
+
if (!questId) return;
|
|
1172
|
+
const before = state.questlog;
|
|
1173
|
+
state.questlog = recordEventHappens(state.questlog, questId);
|
|
1174
|
+
creditSquadEvent(state, questId);
|
|
1175
|
+
if (state.questlog !== before) {
|
|
1176
|
+
const qdef = ds.quest(questId);
|
|
1177
|
+
log(qdef?.title
|
|
1178
|
+
? `Event credit: ${qdef.title}.`
|
|
1179
|
+
: `Event credit for quest ${questId}.`);
|
|
1180
|
+
refreshQuests();
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
// A skill point the swing earned, onto the sheet as it lands. Off-thread the reading side
|
|
1185
|
+
// never swings, so this is the only way a weapon point crosses. Cite: Player.cpp
|
|
1186
|
+
// UpdateCombatSkills; weapon.ts tryMeleeSkillGains.
|
|
1187
|
+
case 'skillGain': {
|
|
1188
|
+
const id = effect.skillId | 0;
|
|
1189
|
+
if (!id || !state.char.combatSkills) return;
|
|
1190
|
+
carryCombatSkillGains({ [id]: { current: effect.current, max: effect.max } });
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
// A consumable the tick spent. Off-thread the reading side's `itemsSpent` is empty, so this
|
|
1194
|
+
// is the only way a spend crosses before zone-out. Cite: itemUse.ts ctx.useItem seam.
|
|
1195
|
+
case 'itemSpend': {
|
|
1196
|
+
const e = effect.entry | 0;
|
|
1197
|
+
if (!e) return;
|
|
1198
|
+
const side = effect.side;
|
|
1199
|
+
if (side && isAllySide(side)) {
|
|
1200
|
+
applyAllyItemSpends([{ allyIndex: Number(side.slice(5)), entry: e, count: effect.total | 0 }]);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
applyItemSpends(
|
|
1204
|
+
[{ entry: e, count: effect.total | 0 }],
|
|
1205
|
+
effect.charges != null ? [{ entry: e, charges: effect.charges | 0 }] : undefined,
|
|
1206
|
+
);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
// ACTION_T_KILLED_MONSTER: kill credit for an entry OTHER than the one that died, which is
|
|
1210
|
+
// why those templates are attackable by faction and must not be pruned as doodads.
|
|
1211
|
+
case 'killCredit': {
|
|
1212
|
+
const e = effect.creatureEntry | 0;
|
|
1213
|
+
if (!e) return;
|
|
1214
|
+
const before = state.questlog;
|
|
1215
|
+
state.questlog = recordKill(state.questlog, e, (q) => ds.quest(q));
|
|
1216
|
+
creditSquadKill(state, e, ds);
|
|
1217
|
+
if (state.questlog !== before) {
|
|
1218
|
+
log(`Kill credit: ${ds.creature(e)?.name ?? `creature ${e}`}.`);
|
|
1219
|
+
refreshQuests();
|
|
1220
|
+
}
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
// ACTION_T_START_RELAY_SCRIPT (53) — run the dbscripts_on_relay stream through the same
|
|
1224
|
+
// path a GO use takes. Cite: CreatureEventAI.cpp:1225-1244; goEventFire runBakedCommand51.
|
|
1225
|
+
case 'relayScript': {
|
|
1226
|
+
const rows = ds.relayScripts(effect.relayId | 0);
|
|
1227
|
+
if (!rows.length) return;
|
|
1228
|
+
log(`Relay script ${effect.relayId} runs (${rows.length} step${rows.length === 1 ? '' : 's'}).`);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
// OnCreatureDeath SetData → persist + open doors on goRuntime (ST 149431 / #1385 shape).
|
|
1232
|
+
// Cite: sunken_temple.cpp:162-178; sessionDeaths applyInstanceSetData.
|
|
1233
|
+
case 'instanceData': {
|
|
1234
|
+
if (!state.instance) return;
|
|
1235
|
+
state.instance = persistScriptInstanceData(state.instance, new Map(effect.store));
|
|
1236
|
+
for (const go of effect.openDoors) {
|
|
1237
|
+
const key = `${state.char.zoneId}:${go}`;
|
|
1238
|
+
const prev = state.goRuntime?.[key] ?? {};
|
|
1239
|
+
state.goRuntime = {
|
|
1240
|
+
...(state.goRuntime ?? {}),
|
|
1241
|
+
[key]: { ...prev, open: true },
|
|
1242
|
+
};
|
|
1243
|
+
log(`Door ${go} opens.`);
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
// THE CLICK NEEDS AN EXECUTOR: nothing had assigned this hook, so both shipped use_object
|
|
1248
|
+
// rows did nothing — Razorgore's orb and Ossirian's crystals, two whole encounter phases.
|
|
1249
|
+
case 'useCombatObject': {
|
|
1250
|
+
const r = useInstanceObject(effect.goEntry);
|
|
1251
|
+
if (!r.ok) log(`Combat object ${effect.goEntry}: ${r.reason}`);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
default:
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
// KI-612: in-fight Pick Pocket grants loot/gold (same domain path as OOC pickPocket).
|
|
1259
|
+
// Cite: SpellEffects.cpp EffectPickPocket; domain/professions/pickpocket.ts.
|
|
1260
|
+
onPickPocket: (entry) => grantInFightPickPocket(entry),
|
|
1261
|
+
...(allyActionProviders ? { allyActionProviders } : {}),
|
|
1262
|
+
...(playerActionProvider ? { playerActionProvider } : {}),
|
|
1263
|
+
...(allyPetIndex ? { allyPetIndex } : {}),
|
|
1264
|
+
...(sessionPositions ? { unitPositionsYd: sessionPositions } : {}),
|
|
1265
|
+
...(sessionPositions ? { geometryAnchor: sessionAnchor } : {}),
|
|
1266
|
+
// Floor pulls get mesh walk AND sight: a cast or an AoE fan through a wall was pulling the pack
|
|
1267
|
+
// in the next room. Sight fails open off-mesh, which is what kept the cast freeze from recurring.
|
|
1268
|
+
...(floorGeometry ? { navLos: true } : {}),
|
|
1269
|
+
// Spell.cpp:4604-4612 — only a measured-indoors pull sets this; absent is outdoors.
|
|
1270
|
+
...(encounter?.outdoors === false ? { outdoors: false } : {}),
|
|
1271
|
+
};
|
|
1272
|
+
const session = createSession(
|
|
1273
|
+
player,
|
|
1274
|
+
enemySpecs.length === 1 ? enemySpecs[0]! : enemySpecs,
|
|
1275
|
+
rng,
|
|
1276
|
+
sessionCfg,
|
|
1277
|
+
petSpecForSession,
|
|
1278
|
+
allySpecs,
|
|
1279
|
+
);
|
|
1280
|
+
// What a Worker would be handed instead of this session (PERF-RUNSHEET S6b). DERIVED from the
|
|
1281
|
+
// config above by dropping its function members, so the two cannot drift apart.
|
|
1282
|
+
// `onPickPocket` is the one member no worker can answer, so a kit that holds Pick Pocket keeps the
|
|
1283
|
+
// fight on this thread (smoke/pickPocketSurface.mjs pins that surface at two spells).
|
|
1284
|
+
const cloneableCfgEntries: [string, unknown][] = [];
|
|
1285
|
+
const droppedCfgNames: string[] = [];
|
|
1286
|
+
for (const [k, v] of Object.entries(sessionCfg)) {
|
|
1287
|
+
if (typeof v === 'function') { droppedCfgNames.push(k); continue; }
|
|
1288
|
+
try {
|
|
1289
|
+
structuredClone(v);
|
|
1290
|
+
cloneableCfgEntries.push([k, v]);
|
|
1291
|
+
} catch {
|
|
1292
|
+
// An object holding a closure — `combatConditionTable` is the one that taught us this.
|
|
1293
|
+
droppedCfgNames.push(k);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const PICK_POCKET_IDS = new Set([921, 5967]);
|
|
1297
|
+
const kitHasPickPocket = (spec?: CombatantSpec | null): boolean =>
|
|
1298
|
+
!!spec?.spells?.some((sp) => PICK_POCKET_IDS.has(sp.id | 0));
|
|
1299
|
+
if (!kitHasPickPocket(player) && !(allySpecs ?? []).some(kitHasPickPocket)) {
|
|
1300
|
+
session.workerInit = {
|
|
1301
|
+
kind: 'init',
|
|
1302
|
+
player,
|
|
1303
|
+
enemies: enemySpecs,
|
|
1304
|
+
...(allySpecs ? { allies: allySpecs } : {}),
|
|
1305
|
+
...(petSpecForSession ? { pet: petSpecForSession } : {}),
|
|
1306
|
+
seed: sessionSeed,
|
|
1307
|
+
cfg: {
|
|
1308
|
+
...Object.fromEntries(cloneableCfgEntries),
|
|
1309
|
+
wantsSpellStore: true,
|
|
1310
|
+
},
|
|
1311
|
+
...(allyProviderSpecs ? { allyProviderSpecs } : {}),
|
|
1312
|
+
...(playerProviderSpec ? { playerProviderSpec } : {}),
|
|
1313
|
+
// The floor, by the URLs this thread loaded it from, so the far side stands on the same mesh.
|
|
1314
|
+
...(encounter?.mapId != null && navMeshSourceUrl(encounter.mapId)
|
|
1315
|
+
? {
|
|
1316
|
+
navAssets: {
|
|
1317
|
+
mapId: encounter.mapId,
|
|
1318
|
+
meshUrl: navMeshSourceUrl(encounter.mapId) ?? undefined,
|
|
1319
|
+
collisionUrl: collisionSourceUrl(encounter.mapId) ?? undefined,
|
|
1320
|
+
outdoorsUrl: outdoorsSourceUrl(encounter.mapId) ?? undefined,
|
|
1321
|
+
},
|
|
1322
|
+
}
|
|
1323
|
+
: {}),
|
|
1324
|
+
// Every write tees through the facade (simFacade.ts commandOf), seats are forced to the same
|
|
1325
|
+
// name on both sides, and commands posted during the boot are held rather than dropped.
|
|
1326
|
+
commandChannel: true,
|
|
1327
|
+
// What the filter had to drop. The worker rebuilds these from its own store; naming them means a
|
|
1328
|
+
// member that grows a closure is visible instead of silently missing on the far side.
|
|
1329
|
+
droppedCfg: droppedCfgNames,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
bindFightEnter(session, now0);
|
|
1333
|
+
// Live pet HP is the combat unit, since Health Funnel ticks it. Mirror it onto the character's pet so
|
|
1334
|
+
// outdoor autofight sees the heal without waiting for resolveFight.
|
|
1335
|
+
const innerTick = session.tick.bind(session);
|
|
1336
|
+
session.tick = (action, inputs) => {
|
|
1337
|
+
const ev = innerTick(action, inputs);
|
|
1338
|
+
const pv = session.petView();
|
|
1339
|
+
if (state.char.pet && pv) state.char.pet.health = pv.hp;
|
|
1340
|
+
return ev;
|
|
1341
|
+
};
|
|
1342
|
+
if (allySpecs && allyPetIndex) {
|
|
1343
|
+
// Humans occupy ally_0..n-1 in state.squad order (the same mapping allyItemsConsumed uses).
|
|
1344
|
+
sessionPartyLayout.set(session, {
|
|
1345
|
+
compIds: (state.squad ?? []).slice(0, allyPetIndex.length).map((c) => c.id),
|
|
1346
|
+
petAllyIndex: allyPetIndex,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
pendingFight = {
|
|
1350
|
+
rng,
|
|
1351
|
+
creatureEntry,
|
|
1352
|
+
enemyLevel: primary?.level ?? 0,
|
|
1353
|
+
pack: packMeta.length ? packMeta : undefined,
|
|
1354
|
+
...(opts?.endless ? { endless: true } : {}),
|
|
1355
|
+
encounter: encounter
|
|
1356
|
+
? {
|
|
1357
|
+
dungeonId: encounter.dungeonId,
|
|
1358
|
+
encounterId: encounter.encounterId,
|
|
1359
|
+
phaseIndex: encounter.phaseIndex ?? 0,
|
|
1360
|
+
...(encounter.outdoors === false ? { outdoors: false } : {}),
|
|
1361
|
+
...(encounter.fusedIds?.length ? { fusedIds: encounter.fusedIds } : {}),
|
|
1362
|
+
...(encounter.resolved ? { resolved: encounter.resolved } : {}),
|
|
1363
|
+
}
|
|
1364
|
+
: undefined,
|
|
1365
|
+
withSquad: opts?.withSquad === true ? true : undefined,
|
|
1366
|
+
};
|
|
1367
|
+
// Wave 4C metadata: who would social-aggro by GetAttackDistance (larger than CallForHelp).
|
|
1368
|
+
let spatialAdds: BeginFight['spatialAdds'];
|
|
1369
|
+
if (!encounter && zoneHasSpatialSpawns(ds, state.char.zoneId)) {
|
|
1370
|
+
const hits = spatialAddsForPull(ds, state.char.zoneId, creatureEntry, state.char.level);
|
|
1371
|
+
if (hits.length) {
|
|
1372
|
+
spatialAdds = hits.map((h) => ({
|
|
1373
|
+
entry: h.entry,
|
|
1374
|
+
distance: h.distance,
|
|
1375
|
+
attackDistance: h.attackDistance,
|
|
1376
|
+
}));
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (runFacade) {
|
|
1380
|
+
runFacade.rebind(session);
|
|
1381
|
+
} else {
|
|
1382
|
+
runFacade = createSimFacade(session);
|
|
1383
|
+
// The pet mirror again, on the READING side, registered once for the run. The `tick` wrapper
|
|
1384
|
+
// above only fires where the tick runs, and off-thread that is the worker.
|
|
1385
|
+
const held = runFacade.session;
|
|
1386
|
+
runFacade.onBatch(() => {
|
|
1387
|
+
const pv = held.petView();
|
|
1388
|
+
if (state.char.pet && pv) state.char.pet.health = pv.hp;
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
const faceted = runFacade.session;
|
|
1392
|
+
liveFightSession = faceted;
|
|
1393
|
+
// A new fight starts a fresh debit ledger. Cite: applyItemSpends delta tracking below.
|
|
1394
|
+
itemSpendApplied = new Map<number, number>();
|
|
1395
|
+
allyItemSpendApplied = new Map<string, number>();
|
|
1396
|
+
itemGainApplied = new Map<number, number>();
|
|
1397
|
+
allyItemGainApplied = new Map<string, number>();
|
|
1398
|
+
shardGainApplied = 0;
|
|
1399
|
+
// S4A: surface clear-credit batch so live Incoming / openFusedIds stay aligned.
|
|
1400
|
+
const beginFusedIds = encounter?.fusedIds?.length
|
|
1401
|
+
? encounter.fusedIds
|
|
1402
|
+
: encounter ? [encounter.encounterId] : undefined;
|
|
1403
|
+
return {
|
|
1404
|
+
ok: true,
|
|
1405
|
+
session: faceted,
|
|
1406
|
+
enemyName: cdef?.name ?? '',
|
|
1407
|
+
enemyEntry: creatureEntry,
|
|
1408
|
+
playerMax: player.maxHealth,
|
|
1409
|
+
enemyMax: primary?.spec.maxHealth ?? 0,
|
|
1410
|
+
...(packAssistInfo.length ? { packAssist: packAssistInfo } : {}),
|
|
1411
|
+
...(aoePullInfo.length ? { aoePull: aoePullInfo } : {}),
|
|
1412
|
+
spatialAdds,
|
|
1413
|
+
...(beginFusedIds ? { fusedIds: beginFusedIds } : {}),
|
|
1414
|
+
};
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
// `withSquad` folds the hired companions in as real allies (same path dungeon pulls use).
|
|
1418
|
+
// Outdoor fights stay solo unless a caller asks — Auto Quest asks for elite pulls (setup).
|
|
1419
|
+
// `attack` reads the Attack dial; `noAoe` skips farm-dial extras (a detected pickpocket).
|
|
1420
|
+
const beginFight = (
|
|
1421
|
+
creatureEntry: number,
|
|
1422
|
+
opts?: { withSquad?: boolean; sitting?: boolean; attack?: boolean; noAoe?: boolean },
|
|
1423
|
+
): ActionResult<BeginFight> =>
|
|
1424
|
+
startFight(creatureEntry, undefined, undefined, {
|
|
1425
|
+
withSquad: opts?.withSquad, sitting: opts?.sitting, attack: opts?.attack, noAoe: opts?.noAoe,
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
// Roof over a live-floor pull. `spawnGuids` on the baked encounters is the join key the floor has; an
|
|
1429
|
+
// unbaked guid falls back to the instance majority. Cite: apply-encounter-outdoors.ts.
|
|
1430
|
+
const floorPullOutdoors = (dungeonId: string, guids: readonly number[]): boolean => {
|
|
1431
|
+
const def = ds.instance(dungeonId);
|
|
1432
|
+
if (def) {
|
|
1433
|
+
for (const g of guids) {
|
|
1434
|
+
const hit = outdoorsForSpawnGuid(def, g);
|
|
1435
|
+
if (hit !== undefined) return hit;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return instanceDefaultOutdoors(dungeonId);
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
// Free-floor pull: fight exactly these spawn guids at their world coords, with no route wave or pack
|
|
1442
|
+
// table. The encounter id is synthetic so a win does not auto-clear route ids.
|
|
1443
|
+
const beginFloorPull = (opts: {
|
|
1444
|
+
dungeonId: string;
|
|
1445
|
+
mapId: number;
|
|
1446
|
+
members: readonly { guid: number; entry: number; x: number; y: number }[];
|
|
1447
|
+
withSquad?: boolean;
|
|
1448
|
+
elapsedMs?: number;
|
|
1449
|
+
}): ActionResult<BeginFight> => {
|
|
1450
|
+
const members = opts.members.filter((m) => m.entry > 0 && Number.isFinite(m.x) && Number.isFinite(m.y));
|
|
1451
|
+
if (!members.length) return { ok: false, reason: 'No hostiles to pull' };
|
|
1452
|
+
if (!state.instance || state.instance.dungeonId !== opts.dungeonId) {
|
|
1453
|
+
return { ok: false, reason: 'You are not in that dungeon' };
|
|
1454
|
+
}
|
|
1455
|
+
const primary = members[0]!;
|
|
1456
|
+
const pack =
|
|
1457
|
+
members.length > 1
|
|
1458
|
+
? members.slice(1).map((m) => ({ creatureEntry: m.entry, count: 1 }))
|
|
1459
|
+
: undefined;
|
|
1460
|
+
const ox = members.reduce((s, m) => s + m.x, 0) / members.length;
|
|
1461
|
+
const oy = members.reduce((s, m) => s + m.y, 0) / members.length;
|
|
1462
|
+
const guids = members.map((m) => m.guid);
|
|
1463
|
+
// The floor pulls by guid, not by route id — join through EncounterDef.spawnGuids and fall
|
|
1464
|
+
// back to the instance's majority when this guid belongs to no baked pull.
|
|
1465
|
+
const floorOutdoors = floorPullOutdoors(opts.dungeonId, guids);
|
|
1466
|
+
return startFight(primary.entry, undefined, {
|
|
1467
|
+
dungeonId: opts.dungeonId,
|
|
1468
|
+
encounterId: `floor-${primary.guid}`,
|
|
1469
|
+
...(floorOutdoors === false ? { outdoors: false } : {}),
|
|
1470
|
+
...(pack?.length ? { pack } : {}),
|
|
1471
|
+
origin: { x: ox, y: oy },
|
|
1472
|
+
spawnGuids: guids,
|
|
1473
|
+
mapId: opts.mapId,
|
|
1474
|
+
...(opts.elapsedMs != null && opts.elapsedMs > 0 ? { elapsedMs: opts.elapsedMs } : {}),
|
|
1475
|
+
}, opts.withSquad ? { withSquad: true } : undefined);
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// Continuous dungeon shell: ONE endless combat session for the whole run, opened with NO enemies at all.
|
|
1479
|
+
// combatView mounts once; pulls inject in place — no scene thrash.
|
|
1480
|
+
const beginFloorShell = (opts: {
|
|
1481
|
+
dungeonId: string;
|
|
1482
|
+
mapId: number;
|
|
1483
|
+
origin: { x: number; y: number; z?: number };
|
|
1484
|
+
withSquad?: boolean;
|
|
1485
|
+
}): ActionResult<BeginFight> => {
|
|
1486
|
+
if (!state.instance || state.instance.dungeonId !== opts.dungeonId) {
|
|
1487
|
+
return { ok: false, reason: 'You are not in that dungeon' };
|
|
1488
|
+
}
|
|
1489
|
+
return startFight(0, undefined, {
|
|
1490
|
+
dungeonId: opts.dungeonId,
|
|
1491
|
+
encounterId: 'floor-shell',
|
|
1492
|
+
// Shell opens on the instance majority; injectFloorMember re-points it per pull.
|
|
1493
|
+
...(instanceDefaultOutdoors(opts.dungeonId) ? {} : { outdoors: false }),
|
|
1494
|
+
origin: { x: opts.origin.x, y: opts.origin.y },
|
|
1495
|
+
mapId: opts.mapId,
|
|
1496
|
+
spawnGuids: [],
|
|
1497
|
+
}, {
|
|
1498
|
+
withSquad: opts.withSquad === true,
|
|
1499
|
+
endless: true,
|
|
1500
|
+
noPrimary: true,
|
|
1501
|
+
});
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
/** Inject one DB spawn into the open endless floor session at world coords. */
|
|
1505
|
+
const injectFloorMember = (
|
|
1506
|
+
session: CombatSession,
|
|
1507
|
+
member: { guid: number; entry: number; x: number; y: number; z?: number },
|
|
1508
|
+
opts?: { passive?: boolean },
|
|
1509
|
+
): ActionResult<{ side: string; name: string; entry: number; level: number }> => {
|
|
1510
|
+
if (!liveFightSession || liveFightSession !== session) {
|
|
1511
|
+
return { ok: false, reason: 'No open floor session' };
|
|
1512
|
+
}
|
|
1513
|
+
if (!pendingFight) return { ok: false, reason: 'No pending floor fight' };
|
|
1514
|
+
if (!(member.entry > 0) || !Number.isFinite(member.x) || !Number.isFinite(member.y)) {
|
|
1515
|
+
return { ok: false, reason: 'Invalid floor member' };
|
|
1516
|
+
}
|
|
1517
|
+
const mdef = resolveCreatureDefForCombat(member.entry, (id) => ds.creature(id));
|
|
1518
|
+
if (!mdef) return { ok: false, reason: `No template for entry ${member.entry}` };
|
|
1519
|
+
const built = buildEnemySpecFromCreature(ds, mdef, pendingFight.rng);
|
|
1520
|
+
const spawn = getDungeonSpawn(member.guid);
|
|
1521
|
+
if (spawn) built.spec.movementType = spawn.MovementType | 0; // GetDefaultMovementType. Cite: naxxramas.cpp:995
|
|
1522
|
+
// Ask the FRAME, as the other origin lifts do: `origin` exists only on a world-translated anchor, and
|
|
1523
|
+
// falling through to the member's own position stacked every injected body on the session origin.
|
|
1524
|
+
const anchor = typeof session.geometryAnchor === 'function' ? session.geometryAnchor() : undefined;
|
|
1525
|
+
const origin = anchor?.frame === 'world-translated' ? anchor.origin : undefined;
|
|
1526
|
+
if (!origin) {
|
|
1527
|
+
return { ok: false, reason: 'Floor session has no world origin to place this body against' };
|
|
1528
|
+
}
|
|
1529
|
+
// Carry absolute world z (same rule as CombatPosYd) so storey LOS / sameStorey work.
|
|
1530
|
+
const pos = member.z != null && Number.isFinite(member.z)
|
|
1531
|
+
? { x: member.x - origin.x, y: member.y - origin.y, z: member.z }
|
|
1532
|
+
: { x: member.x - origin.x, y: member.y - origin.y };
|
|
1533
|
+
const passive = opts?.passive === true;
|
|
1534
|
+
// The world's guid rides in with the spec (E1.0). The side this comes back as is a slot that
|
|
1535
|
+
// gets recycled; the guid is the unit.
|
|
1536
|
+
const side = session.injectEnemy({ ...built.spec, guid: member.guid }, pos, { passive });
|
|
1537
|
+
if (!side) return { ok: false, reason: 'Failed to inject enemy into combat' };
|
|
1538
|
+
// One shell session spans the whole run, so the roof travels with the pull, not the open.
|
|
1539
|
+
if (!passive && state.instance && typeof session.setOutdoors === 'function') {
|
|
1540
|
+
session.setOutdoors(floorPullOutdoors(state.instance.dungeonId, [member.guid]));
|
|
1541
|
+
}
|
|
1542
|
+
// Kill credit pack only once combat starts (active inject / later engage). Passive pool
|
|
1543
|
+
// streams in passive so walking past does not queue every spawn as a pull.
|
|
1544
|
+
if (!passive) {
|
|
1545
|
+
pendingFight = {
|
|
1546
|
+
...pendingFight,
|
|
1547
|
+
pack: [...(pendingFight.pack ?? []), { entry: member.entry, level: built.level, guid: member.guid }],
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
ok: true,
|
|
1552
|
+
side,
|
|
1553
|
+
name: built.spec.name,
|
|
1554
|
+
entry: member.entry,
|
|
1555
|
+
level: built.level,
|
|
1556
|
+
};
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
/** Queue kill-credit for a floor guid that entered combat after a passive stream inject. */
|
|
1560
|
+
const noteFloorPackMember = (
|
|
1561
|
+
member: { guid: number; entry: number; level: number },
|
|
1562
|
+
): ActionResult => {
|
|
1563
|
+
if (!pendingFight) return { ok: false, reason: 'No pending floor fight' };
|
|
1564
|
+
const pack = pendingFight.pack ?? [];
|
|
1565
|
+
if (pack.some((k) => k.guid === member.guid)) return { ok: true };
|
|
1566
|
+
pendingFight = {
|
|
1567
|
+
...pendingFight,
|
|
1568
|
+
pack: [...pack, { entry: member.entry, level: member.level, guid: member.guid }],
|
|
1569
|
+
};
|
|
1570
|
+
return { ok: true };
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
// Weapon/defense skill the swings earned, onto the sheet. The floor credits a pack without ending the
|
|
1574
|
+
// session, so this is the only write-back an endless run gets. Cite: Player.cpp UpdateCombatSkills.
|
|
1575
|
+
const carryCombatSkillGains = (gained: CombatSkillMap | undefined): void => {
|
|
1576
|
+
const merged = mergeCombatSkillGains(state.char.combatSkills, gained);
|
|
1577
|
+
if (merged && merged !== state.char.combatSkills) state.char.combatSkills = merged;
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// Settle XP/loot/gold for pending pack members without ending the endless session. Call when living
|
|
1581
|
+
// hostiles hit 0 after a floor pull.
|
|
1582
|
+
/**
|
|
1583
|
+
* Debit what a fight SPENT. Cite: Spell.cpp TakeAmmo; Spell.cpp:4209 TakeCastItem.
|
|
1584
|
+
* Called from settleFight AND from creditFloorKills — the live floor clears packs without ever
|
|
1585
|
+
* reaching resolveFight ("full resolveFight only at zone-out", liveCockpitWalk.ts:304), so a
|
|
1586
|
+
* grenade or a quest item used on the floor was never taken out of the bags.
|
|
1587
|
+
*/
|
|
1588
|
+
const applyItemSpends = (
|
|
1589
|
+
consumed: readonly { entry: number; count: number }[] | undefined,
|
|
1590
|
+
chargesLeft: readonly { entry: number; charges: number }[] | undefined,
|
|
1591
|
+
): void => {
|
|
1592
|
+
const delta: { entry: number; count: number }[] = [];
|
|
1593
|
+
for (const c of consumed ?? []) {
|
|
1594
|
+
const already = itemSpendApplied.get(c.entry) ?? 0;
|
|
1595
|
+
const n = c.count - already;
|
|
1596
|
+
if (n > 0) { delta.push({ entry: c.entry, count: n }); itemSpendApplied.set(c.entry, c.count); }
|
|
1597
|
+
}
|
|
1598
|
+
consumed = delta;
|
|
1599
|
+
|
|
1600
|
+
// combatItems reads equippedChargesOf first, so TakeCastItem's instance is the worn one.
|
|
1601
|
+
const wornSlotOf = (entry: number): keyof NonNullable<typeof state.char.equipped> | null => {
|
|
1602
|
+
const eq = state.char.equipped;
|
|
1603
|
+
if (!eq) return null;
|
|
1604
|
+
for (const slot of Object.keys(eq) as (keyof typeof eq)[]) {
|
|
1605
|
+
if (equippedEntry(eq[slot]) === entry) return slot;
|
|
1606
|
+
}
|
|
1607
|
+
return null;
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// Charges first: writing after a last-charge destroy would land on a bag spare of the same entry.
|
|
1611
|
+
// Cite: Spell.cpp:4209 TakeCastItem; buildCombatant.ts equippedChargesOf.
|
|
1612
|
+
for (const c of chargesLeft ?? []) {
|
|
1613
|
+
const slot = wornSlotOf(c.entry);
|
|
1614
|
+
if (slot != null) {
|
|
1615
|
+
const equipped = { ...state.char.equipped };
|
|
1616
|
+
const inst = equippedInstance(equipped[slot])!;
|
|
1617
|
+
equipped[slot] = { ...inst, charges: c.charges };
|
|
1618
|
+
state.char = { ...state.char, equipped };
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const stacks = state.inventory.stacks ?? [];
|
|
1622
|
+
const idx = stacks.findIndex((st) => st.item === c.entry);
|
|
1623
|
+
if (idx >= 0) {
|
|
1624
|
+
const next = stacks.map((st) => ({ ...st }));
|
|
1625
|
+
next[idx] = { ...next[idx]!, charges: c.charges };
|
|
1626
|
+
state.inventory = { ...state.inventory, stacks: next };
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Remove consumables and ammo/thrown. Cite: Spell.cpp TakeAmmo; Spell.cpp:4209 TakeCastItem.
|
|
1631
|
+
for (const c of consumed ?? []) {
|
|
1632
|
+
let n = c.count;
|
|
1633
|
+
if (n > 0 && isExpendableCastItem(ds.item(c.entry))) {
|
|
1634
|
+
const slot = wornSlotOf(c.entry);
|
|
1635
|
+
if (slot != null) {
|
|
1636
|
+
const equipped = { ...state.char.equipped };
|
|
1637
|
+
delete equipped[slot];
|
|
1638
|
+
state.char = { ...state.char, equipped };
|
|
1639
|
+
n -= 1;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const bagHave = countItem(state.inventory, c.entry);
|
|
1643
|
+
const fromBags = Math.min(bagHave, n);
|
|
1644
|
+
if (fromBags > 0) state.inventory = removeItem(state.inventory, c.entry, fromBags)[0];
|
|
1645
|
+
const still = n - fromBags;
|
|
1646
|
+
// Leftover after bags: thrown depleted off the equipped stack, or Rocket Boots malfunction.
|
|
1647
|
+
if (still > 0) {
|
|
1648
|
+
const equipped = { ...state.char.equipped };
|
|
1649
|
+
let cleared = false;
|
|
1650
|
+
for (const slot of Object.keys(equipped) as (keyof typeof equipped)[]) {
|
|
1651
|
+
if (equippedEntry(equipped[slot]) !== c.entry) continue;
|
|
1652
|
+
delete equipped[slot];
|
|
1653
|
+
cleared = true;
|
|
1654
|
+
}
|
|
1655
|
+
if (cleared) state.char = { ...state.char, equipped };
|
|
1656
|
+
}
|
|
1657
|
+
if (state.char.ammoItem === c.entry && countItem(state.inventory, c.entry) < 1) {
|
|
1658
|
+
const next = { ...state.char };
|
|
1659
|
+
delete next.ammoItem;
|
|
1660
|
+
state.char = next;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
const applyAllyItemSpends = (
|
|
1666
|
+
consumed: readonly { allyIndex: number; entry: number; count: number }[] | undefined,
|
|
1667
|
+
): void => {
|
|
1668
|
+
for (const c of consumed ?? []) {
|
|
1669
|
+
const key = `${c.allyIndex | 0}:${c.entry | 0}`;
|
|
1670
|
+
const already = allyItemSpendApplied.get(key) ?? 0;
|
|
1671
|
+
const n = (c.count | 0) - already;
|
|
1672
|
+
if (n <= 0) continue;
|
|
1673
|
+
allyItemSpendApplied.set(key, already + n);
|
|
1674
|
+
const comp = state.squad?.[c.allyIndex];
|
|
1675
|
+
if (!comp) continue;
|
|
1676
|
+
const bags = comp.bags ?? emptyInventory();
|
|
1677
|
+
const take = Math.min(countItem(bags, c.entry), n);
|
|
1678
|
+
if (take > 0) comp.bags = removeItem(bags, c.entry, take)[0];
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
// Items a fight PUT in the bags — the player's own drops, a warlock's Drain Soul shards, and the same
|
|
1683
|
+
// for every companion seat. Cumulative like the spend ledgers, so both callers charge the delta.
|
|
1684
|
+
const bankItemsGained = (result: {
|
|
1685
|
+
itemsGained?: readonly { entry: number; count: number }[];
|
|
1686
|
+
allyItemsGained?: readonly { allyIndex: number; entry: number; count: number }[];
|
|
1687
|
+
soulShardsGained?: number;
|
|
1688
|
+
}): void => {
|
|
1689
|
+
for (const g of result.itemsGained ?? []) {
|
|
1690
|
+
const already = itemGainApplied.get(g.entry) ?? 0;
|
|
1691
|
+
const n = (g.count | 0) - already;
|
|
1692
|
+
if (n <= 0) continue;
|
|
1693
|
+
itemGainApplied.set(g.entry, already + n);
|
|
1694
|
+
state.inventory = addItem(state.inventory, g.entry, n, ds.item(g.entry)?.stackable ?? 1);
|
|
1695
|
+
}
|
|
1696
|
+
// A shard is one bag slot (stackable 1): past a full bag DoCreateItem creates nothing.
|
|
1697
|
+
// Cite: Spell.cpp DoCreateItem EQUIP_ERR_INVENTORY_FULL.
|
|
1698
|
+
const shards = ((result.soulShardsGained ?? 0) | 0) - shardGainApplied;
|
|
1699
|
+
if (shards > 0 && state.char.cls === CLASS.WARLOCK && ds.item(SOUL_SHARD_ITEM)) {
|
|
1700
|
+
shardGainApplied += shards;
|
|
1701
|
+
const room = freeSlots(state.inventory, bagCapacity(state.char, ds));
|
|
1702
|
+
const got = Math.min(shards, room);
|
|
1703
|
+
if (got > 0) {
|
|
1704
|
+
state.inventory = addItem(state.inventory, SOUL_SHARD_ITEM, got, ds.item(SOUL_SHARD_ITEM)?.stackable ?? 1);
|
|
1705
|
+
log(`You gain ${got} soul shard${got > 1 ? 's' : ''}.`);
|
|
1706
|
+
}
|
|
1707
|
+
if (got < shards) log('Your bags are full. The soul shard is lost.');
|
|
1708
|
+
}
|
|
1709
|
+
for (const g of result.allyItemsGained ?? []) {
|
|
1710
|
+
const key = `${g.allyIndex | 0}:${g.entry | 0}`;
|
|
1711
|
+
const already = allyItemGainApplied.get(key) ?? 0;
|
|
1712
|
+
const n = (g.count | 0) - already;
|
|
1713
|
+
if (n <= 0) continue;
|
|
1714
|
+
allyItemGainApplied.set(key, already + n);
|
|
1715
|
+
const comp = state.squad?.[g.allyIndex];
|
|
1716
|
+
if (!comp) continue;
|
|
1717
|
+
const bags = comp.bags ?? emptyInventory();
|
|
1718
|
+
// Bounded by the bag, and for shards also by the carry cap: an unstacked shard costs a slot, and a
|
|
1719
|
+
// warlock that banked every drain kill filled its bags and could not take a gear swap again.
|
|
1720
|
+
const shell = companionCharacter(comp, companionLevel(comp, state.char.level), ds);
|
|
1721
|
+
const cap = bagCapacity(shell, ds);
|
|
1722
|
+
const maxStack = Math.max(1, ds.item(g.entry)?.stackable ?? 1);
|
|
1723
|
+
let got = isSoulShard(g.entry)
|
|
1724
|
+
? Math.min(n, soulShardRoom(bags, freeSlots(bags, cap)))
|
|
1725
|
+
: n;
|
|
1726
|
+
while (got > 0 && !canFit(bags, g.entry, got, maxStack, cap)) got--;
|
|
1727
|
+
if (got <= 0) {
|
|
1728
|
+
if (isSoulShard(g.entry)) log(`${comp.name} cannot carry another soul shard.`);
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
comp.bags = addItem(bags, g.entry, got, maxStack);
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
const creditFloorKills = (opts?: {
|
|
1736
|
+
/** Credit only these spawn guids (evade: dead mobs pay, leashed mobs don't). */
|
|
1737
|
+
guids?: number[];
|
|
1738
|
+
/** Drop whatever is left uncredited (wipe/evade — no payout for reset mobs). */
|
|
1739
|
+
dropRest?: boolean;
|
|
1740
|
+
/** Hold drops + coin in the loot window instead of bagging them (boss kill). */
|
|
1741
|
+
holdLoot?: boolean;
|
|
1742
|
+
lootTitle?: string;
|
|
1743
|
+
lootCreatureEntry?: number;
|
|
1744
|
+
}): ActionResult<{
|
|
1745
|
+
xpGained: number;
|
|
1746
|
+
goldGained: number;
|
|
1747
|
+
loot: LootDrop[];
|
|
1748
|
+
killCount: number;
|
|
1749
|
+
/** What each corpse's death script summoned. The floor owns where the body lands. */
|
|
1750
|
+
summons: FloorDeathSummon[];
|
|
1751
|
+
}> => {
|
|
1752
|
+
if (!pendingFight) return { ok: false, reason: 'No pending floor fight' };
|
|
1753
|
+
// Ahead of the empty-pack return: an evade or a wipe still swung, and those points are earned.
|
|
1754
|
+
if (liveFightSession) {
|
|
1755
|
+
const r = liveFightSession.result();
|
|
1756
|
+
carryCombatSkillGains(r.player.combatSkills);
|
|
1757
|
+
// What the pull SPENT leaves the bags here. The floor clears packs without ever reaching
|
|
1758
|
+
// resolveFight ("full resolveFight only at zone-out" — liveCockpitWalk.ts:304), and the kit is
|
|
1759
|
+
// rebuilt from bags each session, so a grenade or a quest item used on the floor was free.
|
|
1760
|
+
applyItemSpends(r.itemsConsumed, r.itemChargesLeft);
|
|
1761
|
+
applyAllyItemSpends(r.allyItemsConsumed);
|
|
1762
|
+
// …and what it EARNED. Only the spends were here, so a companion warlock's Drain Soul shards
|
|
1763
|
+
// waited for zone-out and its healthstone run ran dry after the spawn kit's 10.
|
|
1764
|
+
bankItemsGained(r);
|
|
1765
|
+
// Auras were never in this write-back: a stealth the pull broke stayed on the sheet all run,
|
|
1766
|
+
// so the buff bar showed it and the next fight seeded from it.
|
|
1767
|
+
for (const gone of syncSheetStealthFromSession(state.char, ds, liveFightSession)) {
|
|
1768
|
+
log(`${gone.name} fades.`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const pack = pendingFight.pack ?? [];
|
|
1772
|
+
const take = opts?.guids
|
|
1773
|
+
? pack.filter((k) => k.guid != null && opts.guids!.includes(k.guid))
|
|
1774
|
+
: pack;
|
|
1775
|
+
const rest = opts?.dropRest ? [] : pack.filter((k) => !take.includes(k));
|
|
1776
|
+
const kills = take;
|
|
1777
|
+
if (!kills.length) {
|
|
1778
|
+
pendingFight = { ...pendingFight, pack: rest };
|
|
1779
|
+
return { ok: true, xpGained: 0, goldGained: 0, loot: [], killCount: 0, summons: [] };
|
|
1780
|
+
}
|
|
1781
|
+
const deathSummons: FloorDeathSummon[] = [];
|
|
1782
|
+
const rng = pendingFight.rng;
|
|
1783
|
+
const neededLootItems = partyNeededQuestItems(state, ds);
|
|
1784
|
+
const lootRollCtx = {
|
|
1785
|
+
allowsQuestDrop: (entry: { item: number }) => neededLootItems.has(entry.item),
|
|
1786
|
+
allowsCondition: (conditionId: number) =>
|
|
1787
|
+
isConditionSatisfied(conditionId, lootConditionSnapshot(), conditionTableById()).ok,
|
|
1788
|
+
// The other faction's Alterac quest item never drops for you. Cite: avQuestItems.ts — the DB's
|
|
1789
|
+
// own rows are asymmetric, so the lock comes from whose quest the item belongs to.
|
|
1790
|
+
allowsItem: (itemId: number) => avAllowsLoot(
|
|
1791
|
+
itemId,
|
|
1792
|
+
factionOf(ds, state.char.race) === 'horde' ? 'horde' : 'alliance',
|
|
1793
|
+
),
|
|
1794
|
+
};
|
|
1795
|
+
const loot: LootDrop[] = [];
|
|
1796
|
+
const heldDrops: LootDrop[] = [];
|
|
1797
|
+
let goldGained = 0;
|
|
1798
|
+
let xpGained = 0;
|
|
1799
|
+
for (const k of kills) {
|
|
1800
|
+
const def = ds.creature(k.entry);
|
|
1801
|
+
if (!def) continue;
|
|
1802
|
+
const rolled = rollLootDeep(ds.lootTable(def.lootId), (id) => ds.lootTable(id), rng, 0, 8, lootRollCtx);
|
|
1803
|
+
for (const l of rolled) giveSquadQuestItem(state, ds, l.item, l.count);
|
|
1804
|
+
if (opts?.holdLoot) {
|
|
1805
|
+
for (const l of rolled) { heldDrops.push(l); loot.push(l); }
|
|
1806
|
+
} else {
|
|
1807
|
+
for (const l of rolled) {
|
|
1808
|
+
const took = takeDrop(l.item, l.count);
|
|
1809
|
+
if (took > 0) {
|
|
1810
|
+
const rp = latestRandomPropertyId(state.inventory, l.item);
|
|
1811
|
+
loot.push(rp != null ? { item: l.item, count: took, randomPropertyId: rp } : { item: l.item, count: took });
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
// Floor has no skin-corpse UI: auto-skin when the party can, else skip the hide.
|
|
1816
|
+
{
|
|
1817
|
+
const skinNode = ds.skinNode(def.entry);
|
|
1818
|
+
const skinDef = skinNode != null ? ds.gatherNode(skinNode) : undefined;
|
|
1819
|
+
if (skinDef) {
|
|
1820
|
+
tryProfessionAutoSkin({
|
|
1821
|
+
ds, state, cdefName: def.name, skinDef, rng, applyGather, loot, log,
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
goldGained += rng.int(def.minGold, def.maxGold);
|
|
1826
|
+
state.questlog = recordKill(state.questlog, def.entry, (e) => ds.quest(e));
|
|
1827
|
+
creditSquadKill(state, def.entry, ds);
|
|
1828
|
+
for (const s of applyCreatureDeathScripts(def.entry)) {
|
|
1829
|
+
deathSummons.push({ ...s, ...(k.guid != null ? { fromGuid: k.guid } : {}) });
|
|
1830
|
+
}
|
|
1831
|
+
const killXp = killXpAmount(def, state.char.level, k.level);
|
|
1832
|
+
xpGained += killXp;
|
|
1833
|
+
if (killXp > 0 && (state.char.restedXp ?? 0) > 0) {
|
|
1834
|
+
const spend = restBonusSpend(state.char.restedXp ?? 0, killXp);
|
|
1835
|
+
if (spend.bonus > 0) {
|
|
1836
|
+
xpGained += spend.bonus;
|
|
1837
|
+
state.char = { ...state.char, restedXp: spend.remainingRested };
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (def.onKillRep) {
|
|
1841
|
+
const team = factionOf(ds, state.char.race) ?? 'alliance';
|
|
1842
|
+
// Diplomacy etc. Cite: Player.cpp GetTotalAuraModifier(SPELL_AURA_MOD_REPUTATION_GAIN).
|
|
1843
|
+
const repSpells = [
|
|
1844
|
+
...racialSpells(ds, state.char.race),
|
|
1845
|
+
...(state.spellbook?.known ?? [])
|
|
1846
|
+
.map((id) => ds.spell(id))
|
|
1847
|
+
.filter((s): s is NonNullable<typeof s> => !!s?.passive),
|
|
1848
|
+
];
|
|
1849
|
+
const repMod = reputationAuraModFromSpells(repSpells);
|
|
1850
|
+
const repResult = rewardKillReputation({
|
|
1851
|
+
onKillRep: def.onKillRep,
|
|
1852
|
+
playerTeam: team,
|
|
1853
|
+
playerLevel: state.char.level,
|
|
1854
|
+
creatureLevel: k.level,
|
|
1855
|
+
raceId: state.char.race,
|
|
1856
|
+
classId: state.char.cls,
|
|
1857
|
+
reputations: state.char.reputations ?? {},
|
|
1858
|
+
factions: ds.factions(),
|
|
1859
|
+
rates: ds.reputationRates(),
|
|
1860
|
+
spillover: ds.reputationSpillover(),
|
|
1861
|
+
flags: state.char.factionFlags ?? {},
|
|
1862
|
+
...(repMod ? { repMod } : {}),
|
|
1863
|
+
});
|
|
1864
|
+
state.char = {
|
|
1865
|
+
...state.char,
|
|
1866
|
+
reputations: repResult.reputations,
|
|
1867
|
+
factionFlags: repResult.flags ?? state.char.factionFlags,
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
// holdLoot: coin stays in the container. Else ModifyMoney clamp — goldGained is applied copper.
|
|
1872
|
+
if (opts?.holdLoot) {
|
|
1873
|
+
const room = Math.max(0, clampGold(state.char.gold + goldGained) - state.char.gold);
|
|
1874
|
+
goldGained = Math.min(goldGained, room);
|
|
1875
|
+
if (heldDrops.length || goldGained > 0) {
|
|
1876
|
+
const first = kills.map((k) => ds.creature(k.entry)).find((d) => !!d);
|
|
1877
|
+
openLoot({
|
|
1878
|
+
kind: 'corpse',
|
|
1879
|
+
title: opts.lootTitle ?? first?.name ?? 'Corpse',
|
|
1880
|
+
creatureEntry: opts.lootCreatureEntry ?? first?.entry,
|
|
1881
|
+
}, heldDrops, goldGained);
|
|
1882
|
+
loot.splice(0, loot.length, ...withPendingIdentities(loot, state.pendingLoot ?? null));
|
|
1883
|
+
}
|
|
1884
|
+
} else {
|
|
1885
|
+
const before = state.char.gold;
|
|
1886
|
+
state.char = { ...state.char, gold: clampGold(before + goldGained) };
|
|
1887
|
+
goldGained = state.char.gold - before;
|
|
1888
|
+
}
|
|
1889
|
+
refreshQuests();
|
|
1890
|
+
const lv = grantXp(state.char, xpGained, ds);
|
|
1891
|
+
state.char = lv.char;
|
|
1892
|
+
if (xpGained > 0 || goldGained > 0) {
|
|
1893
|
+
log(
|
|
1894
|
+
`Floor pull cleared. +${xpGained} XP`
|
|
1895
|
+
+ (goldGained ? `, +${moneyMarked(goldGained)}` : '')
|
|
1896
|
+
+ (kills.length > 1 ? ` (${kills.length} kills)` : '')
|
|
1897
|
+
+ '.',
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
for (const nl of lv.leveledTo) log(`You reached level ${nl}!`);
|
|
1901
|
+
// Trash bags immediately (no LootFrame). Equip before sweepGrays sells keepers.
|
|
1902
|
+
if (!opts?.holdLoot && loot.length) runAutoEquipUpgrades(8);
|
|
1903
|
+
// Pack credit settled — keep session + pendingFight shell for the next inject.
|
|
1904
|
+
pendingFight = { ...pendingFight, pack: rest };
|
|
1905
|
+
return { ok: true, xpGained, goldGained, loot, killCount: kills.length, summons: deathSummons };
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
const beginEncounter = (
|
|
1909
|
+
dungeonId: string,
|
|
1910
|
+
encounterId: string,
|
|
1911
|
+
opts?: { withSquad?: boolean; elapsedMs?: number; continuousFloor?: boolean },
|
|
1912
|
+
): ActionResult<BeginFight> => {
|
|
1913
|
+
if (!state.instance || state.instance.dungeonId !== dungeonId) {
|
|
1914
|
+
return { ok: false, reason: 'You are not in that dungeon' };
|
|
1915
|
+
}
|
|
1916
|
+
const def = ds.instance(dungeonId);
|
|
1917
|
+
const enc = def ? encounterOf(def, encounterId, state.instance) : undefined;
|
|
1918
|
+
if (!def || !enc) return { ok: false, reason: 'Unknown encounter' };
|
|
1919
|
+
if (isCleared(state.instance, encounterId)) return { ok: false, reason: 'Already defeated' };
|
|
1920
|
+
// Continuous product: free-floor pull of any uncleared pack the party walked into.
|
|
1921
|
+
// isUnlocked is `requiresFlag` + on-the-route now, so this refuses a gate, not an order.
|
|
1922
|
+
if (!opts?.continuousFloor && !isUnlocked(def, state.instance, encounterId)) {
|
|
1923
|
+
return { ok: false, reason: 'Clear the path to it first' };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// DR pull fusion: softCap + PACK_THREAT_BUDGET (mayhem must not reassemble split atoms).
|
|
1927
|
+
const fusion = state.instance.pullFusion ?? DEFAULT_PULL_FUSION;
|
|
1928
|
+
const route = routeEncounterList(def, state.instance);
|
|
1929
|
+
const density = state.instance.routeDensity ?? DEFAULT_ROUTE_DENSITY;
|
|
1930
|
+
const budgetPreset =
|
|
1931
|
+
state.instance.packBudgetPreset
|
|
1932
|
+
?? packBudgetPresetFor(density, fusion);
|
|
1933
|
+
const canAbsorb = makeFusionBudgetGuard(
|
|
1934
|
+
state.char.level,
|
|
1935
|
+
(entry) => ds.creature(entry),
|
|
1936
|
+
(id) => ds.spell(id),
|
|
1937
|
+
budgetPreset,
|
|
1938
|
+
def.maxPlayers,
|
|
1939
|
+
);
|
|
1940
|
+
const fuseBatch = fusionBatchFrom(
|
|
1941
|
+
route,
|
|
1942
|
+
encounterId,
|
|
1943
|
+
fusion,
|
|
1944
|
+
(id) => isCleared(state.instance!, id),
|
|
1945
|
+
MAYHEM_SOFT_CAP,
|
|
1946
|
+
canAbsorb,
|
|
1947
|
+
);
|
|
1948
|
+
// Prefer run.routeEncounters so pack-budget splits are not reloaded from the
|
|
1949
|
+
// static InstanceDef (first sub-pull reuses the original id with a smaller pack).
|
|
1950
|
+
const fusedEncounters = resolveFusionEncounters(def, fuseBatch, (id) =>
|
|
1951
|
+
encounterOf(def, id, state.instance),
|
|
1952
|
+
);
|
|
1953
|
+
const fusedIds = fuseBatch.encounterIds;
|
|
1954
|
+
|
|
1955
|
+
// CSE pool slots: uniform roll at fight start (seeded rng — LOCK 4 / CMaNGOS ResetEntry).
|
|
1956
|
+
const cseRng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
|
|
1957
|
+
const rngNext = () => cseRng.next();
|
|
1958
|
+
|
|
1959
|
+
// Ring of Law: same attempt seed as floor startGauntlet (clearedGuids / floorClearedGuids).
|
|
1960
|
+
const phaseEnc =
|
|
1961
|
+
fusedEncounters.length <= 1
|
|
1962
|
+
? materializeRingOfLawEncounter(
|
|
1963
|
+
enc,
|
|
1964
|
+
enc.id === 'ring-of-law'
|
|
1965
|
+
? (() => {
|
|
1966
|
+
const salt = state.instance!.floorClearedGuids?.length ?? 0;
|
|
1967
|
+
const rng = mulberry32(ringOfLawAttemptSeed(enc.id, salt));
|
|
1968
|
+
return () => rng.next();
|
|
1969
|
+
})()
|
|
1970
|
+
: rngNext,
|
|
1971
|
+
)
|
|
1972
|
+
: enc;
|
|
1973
|
+
|
|
1974
|
+
let pull: { creatureEntry: number; bossSpells?: number[]; pack?: { creatureEntry: number; count: number }[] } | null;
|
|
1975
|
+
if (fusedEncounters.length > 1) {
|
|
1976
|
+
// Materialize CSE per atom, then union pack members into one multi-enemy pull.
|
|
1977
|
+
const materialized = fusedEncounters.map((e) => materializeEncounterCse(e, rngNext));
|
|
1978
|
+
pull = mergeRoutePulls(materialized);
|
|
1979
|
+
} else {
|
|
1980
|
+
// D-4: phase 0 may be primary+pack, a waves[0] table, etc.
|
|
1981
|
+
pull = resolveEncounterPhase(phaseEnc, 0, rngNext);
|
|
1982
|
+
}
|
|
1983
|
+
if (!pull) return { ok: false, reason: 'Encounter has no pull for phase 0' };
|
|
1984
|
+
// World-translated seed from the encounter origin, plus spawn guids when present. A fused pull has no
|
|
1985
|
+
// coords and takes the synthetic slots; mapId and elapsed time stand path members where they walked.
|
|
1986
|
+
const clusterGeom =
|
|
1987
|
+
fusedIds.length <= 1 && enc.origin
|
|
1988
|
+
? {
|
|
1989
|
+
origin: enc.origin,
|
|
1990
|
+
...(enc.spawnGuids?.length ? { spawnGuids: enc.spawnGuids } : {}),
|
|
1991
|
+
mapId: def.mapId,
|
|
1992
|
+
...(opts?.elapsedMs != null && opts.elapsedMs > 0
|
|
1993
|
+
? { elapsedMs: opts.elapsedMs }
|
|
1994
|
+
: {}),
|
|
1995
|
+
}
|
|
1996
|
+
: {};
|
|
1997
|
+
return startFight(pull.creatureEntry, pull.bossSpells, {
|
|
1998
|
+
dungeonId,
|
|
1999
|
+
encounterId,
|
|
2000
|
+
pack: pull.pack,
|
|
2001
|
+
phaseIndex: 0,
|
|
2002
|
+
// Carried even when clusterGeom is skipped (fused pulls): the roof does not depend on
|
|
2003
|
+
// whether we could resolve the cluster centroid.
|
|
2004
|
+
...((phaseEnc.outdoors ?? enc.outdoors) === false ? { outdoors: false } : {}),
|
|
2005
|
+
...(fusedIds.length > 1 ? { fusedIds } : {}),
|
|
2006
|
+
...(phaseEnc !== enc ? { resolved: phaseEnc } : {}),
|
|
2007
|
+
...clusterGeom,
|
|
2008
|
+
}, opts?.withSquad ? { withSquad: true } : undefined);
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
/** S4A Pull +1 — inject next uncleared route TRASH; never boss/fusion-break. CSE + #187 skip. */
|
|
2012
|
+
const pullAdditionalPack = (
|
|
2013
|
+
dungeonId: string,
|
|
2014
|
+
session: CombatSession,
|
|
2015
|
+
): ActionResult<PullAdditionalPackResult> => {
|
|
2016
|
+
if (!state.instance || state.instance.dungeonId !== dungeonId) {
|
|
2017
|
+
return { ok: false, reason: 'You are not in that dungeon' };
|
|
2018
|
+
}
|
|
2019
|
+
if (session.ended) return { ok: false, reason: 'Combat has already ended' };
|
|
2020
|
+
if (!liveFightSession || liveFightSession !== session) {
|
|
2021
|
+
return { ok: false, reason: 'No open combat session for this pull' };
|
|
2022
|
+
}
|
|
2023
|
+
if (!pendingFight?.encounter || pendingFight.encounter.dungeonId !== dungeonId) {
|
|
2024
|
+
return { ok: false, reason: 'No open dungeon pull' };
|
|
2025
|
+
}
|
|
2026
|
+
const def = ds.instance(dungeonId);
|
|
2027
|
+
if (!def) return { ok: false, reason: 'Unknown dungeon' };
|
|
2028
|
+
|
|
2029
|
+
const currentId = pendingFight.encounter.encounterId;
|
|
2030
|
+
const alreadyFused = new Set(
|
|
2031
|
+
pendingFight.encounter.fusedIds?.length
|
|
2032
|
+
? pendingFight.encounter.fusedIds
|
|
2033
|
+
: [currentId],
|
|
2034
|
+
);
|
|
2035
|
+
const currentEnc = encounterOf(def, currentId, state.instance);
|
|
2036
|
+
if (currentEnc && isFusionBreak(currentEnc)) {
|
|
2037
|
+
return { ok: false, reason: 'Cannot pull additional packs during a boss or scripted encounter' };
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const route = routeEncounterList(def, state.instance);
|
|
2041
|
+
const routeIds = route.map((e) => e.id);
|
|
2042
|
+
const startIdx = routeIds.indexOf(currentId);
|
|
2043
|
+
let next: typeof route[number] | undefined;
|
|
2044
|
+
const from = startIdx >= 0 ? startIdx + 1 : 0;
|
|
2045
|
+
for (let i = from; i < route.length; i++) {
|
|
2046
|
+
const enc = route[i]!;
|
|
2047
|
+
if (alreadyFused.has(enc.id) || isCleared(state.instance, enc.id)) continue;
|
|
2048
|
+
let blocked = false;
|
|
2049
|
+
for (let j = 0; j < i; j++) {
|
|
2050
|
+
const prevId = routeIds[j]!;
|
|
2051
|
+
if (isCleared(state.instance, prevId) || alreadyFused.has(prevId)) continue;
|
|
2052
|
+
blocked = true;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
if (blocked) continue;
|
|
2056
|
+
// Pretend open fusion batch is cleared so later route atoms unlock for inject.
|
|
2057
|
+
if (!isUnlocked(def, {
|
|
2058
|
+
...state.instance,
|
|
2059
|
+
defeated: [
|
|
2060
|
+
...state.instance.defeated,
|
|
2061
|
+
...[...alreadyFused].filter((id) => !state.instance!.defeated.includes(id)),
|
|
2062
|
+
],
|
|
2063
|
+
}, enc.id)) {
|
|
2064
|
+
if (isFusionBreak(enc) || enc.kind === 'boss') {
|
|
2065
|
+
return { ok: false, reason: 'Next encounter is a boss or fusion break' };
|
|
2066
|
+
}
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (isFusionBreak(enc) || enc.kind === 'boss') {
|
|
2070
|
+
return { ok: false, reason: 'Next encounter is a boss or fusion break' };
|
|
2071
|
+
}
|
|
2072
|
+
if (enc.kind !== 'trash') continue;
|
|
2073
|
+
next = enc;
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
if (!next) return { ok: false, reason: 'No pullable pack remaining on the route' };
|
|
2077
|
+
|
|
2078
|
+
const rng = pendingFight.rng;
|
|
2079
|
+
const pull = resolveEncounterPhase(next, 0, () => rng.next());
|
|
2080
|
+
if (!pull) return { ok: false, reason: `Encounter ${next.id} has no pull for phase 0` };
|
|
2081
|
+
|
|
2082
|
+
const toBuild: Array<{ entry: number }> = [{ entry: pull.creatureEntry }];
|
|
2083
|
+
for (const m of pull.pack ?? []) {
|
|
2084
|
+
for (let n = 0; n < m.count; n++) toBuild.push({ entry: m.creatureEntry });
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const injected: PullAdditionalPackResult['injected'] = [];
|
|
2088
|
+
const packCredit: Array<{ entry: number; level: number }> = [];
|
|
2089
|
+
let baseEnemyIdx = session.enemiesView().length;
|
|
2090
|
+
for (const row of toBuild) {
|
|
2091
|
+
const mdef = resolveCreatureDefForCombat(row.entry, (id) => ds.creature(id));
|
|
2092
|
+
if (!mdef) {
|
|
2093
|
+
log(`Pull +1 skipped: no template for creature entry ${row.entry}.`);
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const built = buildEnemySpecFromCreature(ds, mdef, rng);
|
|
2097
|
+
const caster = isEnemyCasterKit(built.spec.spells, { maxPower: built.spec.maxPower });
|
|
2098
|
+
// Ring 1 inject policy: the +1 pack arrives at its honest open band and walks in,
|
|
2099
|
+
// not stapled onto the melee arc. Cite: Unit.cpp GetAttackDistance.
|
|
2100
|
+
const pos = injectEnemySlotPosition({
|
|
2101
|
+
enemyIndex: baseEnemyIdx,
|
|
2102
|
+
isCaster: caster,
|
|
2103
|
+
meleeOpenYd: getCreatureAttackDistance(built.level, state.char.level, {
|
|
2104
|
+
detectionRange: mdef.detectionRange,
|
|
2105
|
+
}),
|
|
2106
|
+
});
|
|
2107
|
+
const side = session.injectEnemy(built.spec, pos);
|
|
2108
|
+
if (!side) return { ok: false, reason: 'Failed to inject enemy into combat' };
|
|
2109
|
+
injected.push({ side, name: built.spec.name, entry: row.entry });
|
|
2110
|
+
packCredit.push({ entry: row.entry, level: built.level });
|
|
2111
|
+
baseEnemyIdx++;
|
|
2112
|
+
}
|
|
2113
|
+
if (injected.length === 0) {
|
|
2114
|
+
return { ok: false, reason: `No creatures could be materialized for ${next.name}` };
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
const fusedIds = [...alreadyFused, next.id];
|
|
2118
|
+
pendingFight = {
|
|
2119
|
+
...pendingFight,
|
|
2120
|
+
pack: [...(pendingFight.pack ?? []), ...packCredit],
|
|
2121
|
+
encounter: { ...pendingFight.encounter, fusedIds },
|
|
2122
|
+
};
|
|
2123
|
+
const living = session.livingEnemiesView().length;
|
|
2124
|
+
const softCapWarning = living > MAYHEM_SOFT_CAP;
|
|
2125
|
+
log(
|
|
2126
|
+
`Pull +1: ${next.name} joins the fight (${injected.length} ${injected.length === 1 ? 'enemy' : 'enemies'})` +
|
|
2127
|
+
(softCapWarning ? ` - soft cap ${MAYHEM_SOFT_CAP} exceeded` : '') + '.',
|
|
2128
|
+
);
|
|
2129
|
+
return {
|
|
2130
|
+
ok: true,
|
|
2131
|
+
encounterId: next.id,
|
|
2132
|
+
name: next.name,
|
|
2133
|
+
injected,
|
|
2134
|
+
softCapWarning,
|
|
2135
|
+
combatantCount: combatantCount(next),
|
|
2136
|
+
fusedIds,
|
|
2137
|
+
};
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
// Use an instance GO wired as `objectGate` on any encounter in the current dungeon. Multi-step gates set
|
|
2141
|
+
// the flag only after needUses; once-per-instance when gate.once !== false and the flag is already set.
|
|
2142
|
+
const useInstanceObject = (
|
|
2143
|
+
goEntry: number,
|
|
2144
|
+
opts?: { ignoreRequiredItem?: boolean },
|
|
2145
|
+
): ActionResult<{ flag: string; already: boolean; uses: number; needUses: number; completed: boolean }> => {
|
|
2146
|
+
if (!state.instance) return { ok: false, reason: 'You are not in an instance' };
|
|
2147
|
+
// Corpse cannot fire route flags / object gates (KI-960). Cite: Spell.cpp:4586 CASTER_DEAD.
|
|
2148
|
+
{
|
|
2149
|
+
const dead = refuseIfDead(state.char.health, state.deathFlow);
|
|
2150
|
+
if (dead) return dead;
|
|
2151
|
+
}
|
|
2152
|
+
const def = ds.instance(state.instance.dungeonId);
|
|
2153
|
+
if (!def) return { ok: false, reason: 'Unknown dungeon' };
|
|
2154
|
+
if (goEntry === GO_OSSIRIAN_CRYSTAL) {
|
|
2155
|
+
const live = liveFightSession;
|
|
2156
|
+
const fighting = !!live?.enemiesView().some((e) => e.creatureEntry === NPC_OSSIRIAN);
|
|
2157
|
+
if (!live || !fighting) {
|
|
2158
|
+
return { ok: false, reason: 'The crystals only answer while Ossirian is engaged.' };
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
// The gnomeface buttons are the one GO use whose whole payload is instance state, so the object gate
|
|
2162
|
+
// has nothing for them and refused every click. Cite: instance_gnomeregan.cpp:263-274.
|
|
2163
|
+
{
|
|
2164
|
+
const faceIdx = state.instance.dungeonId === 'gnomeregan'
|
|
2165
|
+
? thermapluggButtonFaceIndex(goEntry)
|
|
2166
|
+
: null;
|
|
2167
|
+
if (faceIdx != null) {
|
|
2168
|
+
const faces = state.instance.thermapluggFaces ?? createThermapluggFightState();
|
|
2169
|
+
if (!faces.engaged) {
|
|
2170
|
+
return { ok: false, reason: 'Thermaplugg is not engaged; no vent is open.' };
|
|
2171
|
+
}
|
|
2172
|
+
const was = thermapluggActiveFaceCount(faces);
|
|
2173
|
+
const next = thermapluggDeactivateFace(faces, faceIdx);
|
|
2174
|
+
const now = thermapluggActiveFaceCount(next);
|
|
2175
|
+
state.instance = { ...state.instance, thermapluggFaces: next };
|
|
2176
|
+
if (now < was) log(`Bomb face ${faceIdx + 1} deactivates (${now} vents still open).`);
|
|
2177
|
+
return { ok: true, flag: '', already: now === was, uses: 1, needUses: 1, completed: true };
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
const ritualGo = ds.gameObject(goEntry);
|
|
2181
|
+
let ritualJustCompleted = false;
|
|
2182
|
+
const wasSummoningRitual = ritualGo?.type === GAMEOBJECT_TYPE_SUMMONING_RITUAL;
|
|
2183
|
+
if (wasSummoningRitual && ritualGo) {
|
|
2184
|
+
const ritual = applyInstanceSummoningRitual({
|
|
2185
|
+
go: ritualGo,
|
|
2186
|
+
dungeonId: state.instance.dungeonId,
|
|
2187
|
+
char: state.char,
|
|
2188
|
+
questlog: state.questlog,
|
|
2189
|
+
goRuntime: state.goRuntime,
|
|
2190
|
+
squadLength: state.squad?.length ?? 0,
|
|
2191
|
+
nowMs: sessionClockNow(),
|
|
2192
|
+
spellOf: (id) => ds.spell(id),
|
|
2193
|
+
eventScripts: (id) => ds.eventScripts(id),
|
|
2194
|
+
log,
|
|
2195
|
+
});
|
|
2196
|
+
if (!ritual.ok) return { ok: false, reason: ritual.reason };
|
|
2197
|
+
state.char = ritual.char;
|
|
2198
|
+
state.questlog = ritual.questlog;
|
|
2199
|
+
state.goRuntime = ritual.goRuntime;
|
|
2200
|
+
ritualJustCompleted = ritual.ritualReady;
|
|
2201
|
+
}
|
|
2202
|
+
const found = findObjectGate(def, goEntry);
|
|
2203
|
+
if (!found) return { ok: false, reason: 'Nothing happens' };
|
|
2204
|
+
// molten_coreScripts.cpp:47-52: GetData(type) != DONE → return true (no SetData).
|
|
2205
|
+
// Peek before counting the objectGate so seven early clicks cannot spawn 12018.
|
|
2206
|
+
const sdIrPeek = scriptDevByGo(goEntry);
|
|
2207
|
+
if (sdIrPeek?.goUse && !scriptDevGoUseAllowed(sdIrPeek, state.instance, goEntry)) {
|
|
2208
|
+
const alreadyFlag = found.gate.once !== false && hasFlag(state.instance, found.gate.setsFlag);
|
|
2209
|
+
const prevUsed = state.instance.objectGateUsedEntries?.[found.gate.setsFlag] ?? [];
|
|
2210
|
+
const alreadyEntry = prevUsed.includes(goEntry);
|
|
2211
|
+
if (!alreadyFlag && !alreadyEntry) {
|
|
2212
|
+
return { ok: false, reason: 'Nothing happens' };
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
const required = found.gate.requiredItem;
|
|
2216
|
+
// Bags + keyring: Seal of Ascension and friends are BagFamily KEYS, so they are never in bags.
|
|
2217
|
+
const owned = required == null || countItem(ownedInventory(state.inventory, state.keyring), required) > 0;
|
|
2218
|
+
// Auto-run bypass — same forced `okItem` as autoFireObjectGates (instances.ts:985-988).
|
|
2219
|
+
const hasItem = owned || opts?.ignoreRequiredItem === true;
|
|
2220
|
+
const r = useObjectGate(state.instance, found.gate, hasItem, {
|
|
2221
|
+
goEntry,
|
|
2222
|
+
itemCharges: required != null ? ds.item(required)?.charges : undefined,
|
|
2223
|
+
});
|
|
2224
|
+
if (!r.ok) return { ok: false, reason: r.reason };
|
|
2225
|
+
state.instance = r.run;
|
|
2226
|
+
// Caller spends what useObjectGate reported — never on already / fail / permanent keys.
|
|
2227
|
+
if (r.consumeItem != null && owned) {
|
|
2228
|
+
const spent = consumeOwnedCharges(state.inventory, state.keyring, r.consumeItem, ds.item(r.consumeItem));
|
|
2229
|
+
state.inventory = spent.bags;
|
|
2230
|
+
state.keyring = spent.keyring;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// ScriptDev GO-use, the same seam as interactObject. A type-18 ritual waits for the summoning
|
|
2234
|
+
// trigger so the arm matches the send-event. Cite: instance_uldaman.cpp:313-324.
|
|
2235
|
+
const sdIr = scriptDevByGo(goEntry);
|
|
2236
|
+
if (sdIr?.goUse && (!wasSummoningRitual || ritualJustCompleted)) {
|
|
2237
|
+
const dungeonId = state.instance.dungeonId;
|
|
2238
|
+
runScriptDevGoUse(sdIr, state, ds, {
|
|
2239
|
+
nowMs: sessionClockNow(),
|
|
2240
|
+
goRuntimeKey: (entry) => `${dungeonId}:${entry}`,
|
|
2241
|
+
log,
|
|
2242
|
+
usedGoEntry: goEntry,
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
if (goEntry === GO_OSSIRIAN_CRYSTAL && !r.already) {
|
|
2247
|
+
const live = liveFightSession;
|
|
2248
|
+
if (live) {
|
|
2249
|
+
const ossirian = live.enemiesView().find((e) => e.creatureEntry === NPC_OSSIRIAN);
|
|
2250
|
+
if (ossirian) {
|
|
2251
|
+
const side = ossirian.side;
|
|
2252
|
+
live.removeAurasDueToSpell(side, SPELL_OSSIRIAN_SUPREME);
|
|
2253
|
+
const weaknessId = OSSIRIAN_WEAKNESS_SPELLS[(Math.max(0, r.uses - 1)) % OSSIRIAN_WEAKNESS_SPELLS.length];
|
|
2254
|
+
const weakness = ds.spell(weaknessId);
|
|
2255
|
+
if (weakness) live.applySpellToUnit(side, weakness);
|
|
2256
|
+
log('The crystal flares - Ossirian weakens!');
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
} else if (r.already) {
|
|
2260
|
+
log('That has already been used.');
|
|
2261
|
+
} else if (!r.completed && r.needUses > 1) {
|
|
2262
|
+
log(`You use the object (${goEntry}). (${r.uses}/${r.needUses})`);
|
|
2263
|
+
} else {
|
|
2264
|
+
log(`You use the object (${goEntry}).`);
|
|
2265
|
+
}
|
|
2266
|
+
return {
|
|
2267
|
+
ok: true,
|
|
2268
|
+
flag: r.flag,
|
|
2269
|
+
already: r.already,
|
|
2270
|
+
uses: r.uses,
|
|
2271
|
+
needUses: r.needUses,
|
|
2272
|
+
completed: r.completed,
|
|
2273
|
+
};
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
// Loot an instance GO authored on `InstanceDef.lootObjects`, once per run per guid, off the baked go
|
|
2277
|
+
// loot table with the same quest gating as kill loot. Contents are HELD in the standard loot window.
|
|
2278
|
+
const lootInstanceObject = (
|
|
2279
|
+
goGuid: number,
|
|
2280
|
+
): ActionResult<{ loot: LootDrop[]; name: string }> => {
|
|
2281
|
+
if (!state.instance) return { ok: false, reason: 'You are not in an instance' };
|
|
2282
|
+
const def = ds.instance(state.instance.dungeonId);
|
|
2283
|
+
if (!def) return { ok: false, reason: 'Unknown dungeon' };
|
|
2284
|
+
const go = getDungeonGameObject(goGuid);
|
|
2285
|
+
if (!go) return { ok: false, reason: 'Nothing there' };
|
|
2286
|
+
const authored = (def.lootObjects ?? []).find((l) => l.goEntry === go.entry);
|
|
2287
|
+
// Authoring is an OVERRIDE, not the gate: any baked CHEST (type 3) with a lootId is
|
|
2288
|
+
// lootable, or the 1,289 chest pins derived from the bake would all open "Nothing happens".
|
|
2289
|
+
if (!authored && !(go.type === 3 && (go.lootId ?? 0) > 0)) {
|
|
2290
|
+
return { ok: false, reason: 'Nothing happens' };
|
|
2291
|
+
}
|
|
2292
|
+
if (authored?.revealedByFlag && !hasFlag(state.instance, authored.revealedByFlag)) {
|
|
2293
|
+
return { ok: false, reason: 'Nothing happens' };
|
|
2294
|
+
}
|
|
2295
|
+
const lootedFlag = `go-looted:${goGuid}`;
|
|
2296
|
+
if (hasFlag(state.instance, lootedFlag)) {
|
|
2297
|
+
return { ok: false, reason: 'It is empty.' };
|
|
2298
|
+
}
|
|
2299
|
+
// lootTable() is [] until attachLoot. Opening now would mark the guid looted and yield nothing.
|
|
2300
|
+
if (!ds.lootAttached()) {
|
|
2301
|
+
return { ok: false, reason: 'Nothing happens' };
|
|
2302
|
+
}
|
|
2303
|
+
// Authored key wins; otherwise the baked lockId decides, and only an ITEM lock blocks —
|
|
2304
|
+
// a skill lock (lockpicking/mining) is not a missing key.
|
|
2305
|
+
const keyItem = authored?.requiredItem
|
|
2306
|
+
?? (go.lockId ? requiredKeyItemEntry(ds.lock(go.lockId)) ?? undefined : undefined);
|
|
2307
|
+
if (keyItem != null && countItem(ownedInventory(state.inventory, state.keyring), keyItem) <= 0) {
|
|
2308
|
+
const key = ds.item(keyItem)?.name ?? `item ${keyItem}`;
|
|
2309
|
+
return { ok: false, reason: `Locked - you need ${key}` };
|
|
2310
|
+
}
|
|
2311
|
+
const rng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
|
|
2312
|
+
const neededItems = partyNeededQuestItems(state, ds);
|
|
2313
|
+
const table = go.lootId != null && go.lootId !== 0 ? ds.lootTable(`go:${go.lootId}`) : [];
|
|
2314
|
+
const rollCtx = {
|
|
2315
|
+
allowsQuestDrop: (entry: { item: number }) => neededItems.has(entry.item),
|
|
2316
|
+
allowsCondition: (conditionId: number) =>
|
|
2317
|
+
isConditionSatisfied(conditionId, lootConditionSnapshot(), conditionTableById()).ok,
|
|
2318
|
+
// The other faction's Alterac quest item never drops for you. Cite: avQuestItems.ts — the DB's
|
|
2319
|
+
// own rows are asymmetric, so the lock comes from whose quest the item belongs to.
|
|
2320
|
+
allowsItem: (itemId: number) => avAllowsLoot(
|
|
2321
|
+
itemId,
|
|
2322
|
+
factionOf(ds, state.char.race) === 'horde' ? 'horde' : 'alliance',
|
|
2323
|
+
),
|
|
2324
|
+
};
|
|
2325
|
+
const vein = ds.gatherNode(go.entry);
|
|
2326
|
+
let taps = 1;
|
|
2327
|
+
if (vein?.skill === 'mining') {
|
|
2328
|
+
const tpl = ds.gameObject(go.entry);
|
|
2329
|
+
const lock = (go.lockId ? ds.lock(go.lockId) : tpl?.lockId != null ? ds.lock(tpl.lockId) : undefined);
|
|
2330
|
+
const skill = state.char.skills?.mining?.current ?? 0;
|
|
2331
|
+
const range = miningVeinOpenRange(vein, tpl?.chest);
|
|
2332
|
+
taps = miningHarvestTapCount(range.min, range.max, skill, miningLockReqSkill(lock), rng);
|
|
2333
|
+
}
|
|
2334
|
+
const rolled = table.length
|
|
2335
|
+
? mergeLootDrops(Array.from({ length: taps }, () =>
|
|
2336
|
+
rollLootDeep(table, (id) => ds.lootTable(id), rng, 0, 8, rollCtx)).flat())
|
|
2337
|
+
: [];
|
|
2338
|
+
for (const l of rolled) giveSquadQuestItem(state, ds, l.item, l.count);
|
|
2339
|
+
if (vein?.skill === 'mining' && state.char.skills?.mining) {
|
|
2340
|
+
for (let i = 0; i < taps; i++) {
|
|
2341
|
+
const gain = gainSkill(state.char, 'mining', vein, rng);
|
|
2342
|
+
state.char = gain.char;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
state.instance = setFlag(state.instance, lootedFlag);
|
|
2346
|
+
openLoot(
|
|
2347
|
+
{ kind: goLootKind(go), title: go.name, keyUsed: keyItem != null },
|
|
2348
|
+
rolled,
|
|
2349
|
+
);
|
|
2350
|
+
log(`You open the ${go.name}.`);
|
|
2351
|
+
refreshQuests();
|
|
2352
|
+
return { ok: true, loot: withPendingIdentities(rolled, state.pendingLoot ?? null), name: go.name };
|
|
2353
|
+
};
|
|
2354
|
+
|
|
2355
|
+
/** Sessions whose pet CheckLearning rolls already ran — sync can be called twice (flee). */
|
|
2356
|
+
const petTeachRolled = new WeakSet<CombatSession>();
|
|
2357
|
+
|
|
2358
|
+
function companionImbueSpellIds(comp: Companion): Set<number> {
|
|
2359
|
+
const ids = new Set<number>();
|
|
2360
|
+
for (const hand of ['mainhand', 'offhand'] as const) {
|
|
2361
|
+
const te = comp.tempEnchants?.[hand];
|
|
2362
|
+
if (!te) continue;
|
|
2363
|
+
ids.add(te.spellId);
|
|
2364
|
+
if (te.equipSpellId) ids.add(te.equipSpellId);
|
|
2365
|
+
}
|
|
2366
|
+
return ids;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Humans occupy ally_0..n-1. Pets are appended after, so views[i] is the companion not its pet.
|
|
2370
|
+
const persistCompanionAurasFromSession = (session: CombatSession, now: number): void => {
|
|
2371
|
+
const lookup = (id: number) => ds.spell(id);
|
|
2372
|
+
// A pet keeps what it was wearing, exactly as its owner does; otherwise it walks into the next pull
|
|
2373
|
+
// naked and the party's buffers pay for it again. Cite: Spell.cpp:7345-7350 withPets.
|
|
2374
|
+
const carryPet = (pet: PetState | undefined, view: { hp: number; buffs: readonly CombatBuffChip[] } | null | undefined): void => {
|
|
2375
|
+
if (!pet || !view || !(view.hp > 0)) return;
|
|
2376
|
+
const carried = carryCombatBuffs({ prior: pet.prePullAuras, buffs: view.buffs, now, spell: lookup, persistForms: false });
|
|
2377
|
+
pet.prePullAuras = carried.length ? carried : undefined;
|
|
2378
|
+
};
|
|
2379
|
+
// `petView` answers with an Enslave Demon thrall when one is up, which is not this pet's bar.
|
|
2380
|
+
carryPet(state.char.pet, session.viewOfSide?.('pet'));
|
|
2381
|
+
const layout = sessionPartyLayout.get(rawOf(session));
|
|
2382
|
+
if (!layout) return;
|
|
2383
|
+
const views = session.alliesView();
|
|
2384
|
+
for (let i = 0; i < layout.compIds.length; i++) {
|
|
2385
|
+
const comp = (state.squad ?? []).find((c) => c.id === layout.compIds[i]);
|
|
2386
|
+
if (!comp) continue;
|
|
2387
|
+
const petIdx = layout.petAllyIndex[i];
|
|
2388
|
+
if (petIdx != null) carryPet(comp.pet, views[petIdx]);
|
|
2389
|
+
const view = views[i];
|
|
2390
|
+
if (!view) continue;
|
|
2391
|
+
const skip = companionImbueSpellIds(comp);
|
|
2392
|
+
const buffs = skip.size ? view.buffs.filter((b) => !skip.has(b.spellId)) : view.buffs;
|
|
2393
|
+
const carried = carryCombatBuffs({
|
|
2394
|
+
prior: comp.prePullAuras,
|
|
2395
|
+
buffs,
|
|
2396
|
+
now,
|
|
2397
|
+
spell: lookup,
|
|
2398
|
+
stealthed: !!view.stealthed,
|
|
2399
|
+
persistForms: false,
|
|
2400
|
+
});
|
|
2401
|
+
comp.prePullAuras = carried.length ? carried : undefined;
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2405
|
+
// Write the session's ending player and pet state back onto the character: vitals, form, parallel mana,
|
|
2406
|
+
// skill gains, consumables. Shared by win and defeat — an escape must not lose what it spent.
|
|
2407
|
+
const syncCharFromSession = (session: CombatSession): void => {
|
|
2408
|
+
// Totems are fight-scoped (oocCast KI-817): session teardown is RemoveFromWorld.
|
|
2409
|
+
// UnSummon first so GetSpell() auras never copy onto the character sheet.
|
|
2410
|
+
unsummonTotemsOnLeaveWorld(session);
|
|
2411
|
+
const result = session.result();
|
|
2412
|
+
// Combat HP is written after the aura carry so the domain max health is known: combat may have
|
|
2413
|
+
// inflated it if a party aura was re-applied on top of sheet-baked stats. Always clamp to the max.
|
|
2414
|
+
const combatHp = result.player.hp;
|
|
2415
|
+
// Keep the power you ended the fight with - rage no longer snaps to 0 the instant combat
|
|
2416
|
+
// ends; it bleeds off over time out of combat (see regen()), just like health/mana settle.
|
|
2417
|
+
state.char.power = result.player.power;
|
|
2418
|
+
// Carry form and parallel mana out of combat. Stealth never survives a fight that engaged, so clear
|
|
2419
|
+
// the form and its auras unless the session still shows the player stealthed.
|
|
2420
|
+
if (result.player.form != null) state.char.form = result.player.form || undefined;
|
|
2421
|
+
else state.char.form = undefined;
|
|
2422
|
+
const pv = session.playerView();
|
|
2423
|
+
{
|
|
2424
|
+
const stillStealthed = !!pv.stealthed;
|
|
2425
|
+
if (!stillStealthed) {
|
|
2426
|
+
const auras = state.char.activeAuras ?? [];
|
|
2427
|
+
state.char.activeAuras = auras.filter((a) => {
|
|
2428
|
+
const sp = ds.spell(a.spellId);
|
|
2429
|
+
return !sp?.effects.some((e) => e.kind === 'stealth');
|
|
2430
|
+
});
|
|
2431
|
+
if (isStealthShapeshift((state.char.form ?? 0) | 0)) state.char.form = undefined;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
// Carry combat buffs back onto the sheet with remaining combat time converted to an expiry.
|
|
2435
|
+
// Companions used to walk into the next pull empty and recast every keep-up.
|
|
2436
|
+
{
|
|
2437
|
+
const now = sessionClockNow(session);
|
|
2438
|
+
const lookup = (id: number) => ds.spell(id);
|
|
2439
|
+
state.char.activeAuras = carryCombatBuffs({
|
|
2440
|
+
prior: state.char.activeAuras,
|
|
2441
|
+
buffs: pv.buffs,
|
|
2442
|
+
now,
|
|
2443
|
+
spell: lookup,
|
|
2444
|
+
stealthed: !!pv.stealthed,
|
|
2445
|
+
form: pv.form,
|
|
2446
|
+
formSpellId: pv.formSpellId,
|
|
2447
|
+
persistForms: true,
|
|
2448
|
+
});
|
|
2449
|
+
state.char.activeBuffs = getActiveBuffIds(state.char, now, ds);
|
|
2450
|
+
persistCompanionAurasFromSession(session, now);
|
|
2451
|
+
reconcileGamePartyAuras(state, ds, now, pendingFight?.withSquad === true);
|
|
2452
|
+
}
|
|
2453
|
+
// After auras are on the sheet, clamp combat HP to domain max (never persist over-cap).
|
|
2454
|
+
state.char.health = Math.min(combatHp, maxHealth(state.char, ds));
|
|
2455
|
+
if (result.player.mana != null && classHasParallelMana(state.char.cls)) {
|
|
2456
|
+
if ((result.player.powerType ?? powerType(state.char)) === 'mana') {
|
|
2457
|
+
state.char.power = result.player.mana;
|
|
2458
|
+
delete state.char.mana;
|
|
2459
|
+
} else {
|
|
2460
|
+
state.char.mana = result.player.mana;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
// Weapon/defense skill gains from the fight. Cite: Player.cpp UpdateCombatSkills
|
|
2464
|
+
carryCombatSkillGains(result.player.combatSkills);
|
|
2465
|
+
applyItemSpends(result.itemsConsumed, result.itemChargesLeft);
|
|
2466
|
+
// M7: debit companion bags for items they spent — never the player's inventory.
|
|
2467
|
+
// ally_0 → state.squad[0]; ownership by containment (giveItemToBot).
|
|
2468
|
+
applyAllyItemSpends(result.allyItemsConsumed);
|
|
2469
|
+
// Items the fight PUT in the bags: a Rocket Boots malfunction's scorched pair, and every seat's
|
|
2470
|
+
// Drain Soul shards. Same ledger as the floor, so a run's pack clears are not banked twice.
|
|
2471
|
+
bankItemsGained(result);
|
|
2472
|
+
// Carry the pet's ending HP and power back onto the character. Power persists between fights, 0 HP is
|
|
2473
|
+
// slain until re-summon, and a hunter pet death costs one happiness level.
|
|
2474
|
+
if (state.char.pet && result.pet) {
|
|
2475
|
+
const pet = state.char.pet;
|
|
2476
|
+
// The owner's death unsummons the pet ALIVE, and the live petView mirror has already written the
|
|
2477
|
+
// despawn's 0 onto the character, so the result's flag is what says it was standing.
|
|
2478
|
+
const unsummoned = result.pet.unsummoned === true;
|
|
2479
|
+
const wasAlive = pet.health > 0 || unsummoned;
|
|
2480
|
+
pet.health = result.pet.hp;
|
|
2481
|
+
pet.power = result.pet.power;
|
|
2482
|
+
// LooseHappiness for fight duration (×1.5 in combat). OOC path is rest.ts; this was missing. Cite: Pet.cpp ~731.
|
|
2483
|
+
if (wasAlive) {
|
|
2484
|
+
const petDef = ds.creature(pet.creature);
|
|
2485
|
+
if (petDef && petHasHappiness(petDef)) {
|
|
2486
|
+
const durationS = (result.durationMs ?? 0) / 1000;
|
|
2487
|
+
if (durationS > 0) {
|
|
2488
|
+
const loyalty = pet.loyalty ?? LOYALTY_REBELLIOUS;
|
|
2489
|
+
pet.happiness = decayHappiness(pet.happiness, loyalty, durationS, true);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
if (unsummoned) {
|
|
2494
|
+
// RemovePet(PET_SAVE_REAGENTS): out of the world and out of the current slot, which is the slot
|
|
2495
|
+
// Call Pet reads back. Cite: Player.cpp:1670; Pet.cpp:404-412; LoadPetFromDB Pet.cpp:177-182.
|
|
2496
|
+
const partyAuraIds = petPartyAuraSpellIds(state.char, ds);
|
|
2497
|
+
if (partyAuraIds.length > 0) state.char = stripPetPartyAuras(state.char, ds, partyAuraIds);
|
|
2498
|
+
state.char.storedPet = pet;
|
|
2499
|
+
state.char.pet = undefined;
|
|
2500
|
+
state.char = reconcilePetPartyAuras(state.char, ds);
|
|
2501
|
+
log(`${pet.name} is unsummoned.`);
|
|
2502
|
+
// Reagents come back when the pet was alive at the owner's death — the warlock's Soul Shard.
|
|
2503
|
+
// Cite: Pet.cpp:1029-1045 (the BG branch, where the minion is auto-resurrected, is not modelled).
|
|
2504
|
+
const summonSpell = pet.createdBySpell ? ds.spell(pet.createdBySpell) : undefined;
|
|
2505
|
+
for (const r of summonSpell?.reagents ?? []) {
|
|
2506
|
+
const idef = ds.item(r.item);
|
|
2507
|
+
state.inventory = addItem(state.inventory, r.item, r.count, idef?.stackable ?? 1);
|
|
2508
|
+
if (idef) log(`${idef.name} is returned.`);
|
|
2509
|
+
}
|
|
2510
|
+
} else if (result.pet.hp <= 0 && wasAlive) {
|
|
2511
|
+
// A dead unit wears nothing — the buffs it was carrying do not come back with Revive Pet.
|
|
2512
|
+
// Cite: Unit.cpp:1106 victim->RemoveAllAurasOnDeath().
|
|
2513
|
+
pet.prePullAuras = undefined;
|
|
2514
|
+
const petDef = ds.creature(pet.creature);
|
|
2515
|
+
if (petDef && petHasHappiness(petDef)) {
|
|
2516
|
+
pet.happiness = Math.max(0, pet.happiness - HAPPINESS_DEATH_LOSS);
|
|
2517
|
+
}
|
|
2518
|
+
// A party area aura falls when the pet casting it dies, and the domain auras can re-merge from
|
|
2519
|
+
// the pre-fight chips above, so reconciling with a dead pet is what clears them off the owner.
|
|
2520
|
+
state.char = reconcilePetPartyAuras(state.char, ds);
|
|
2521
|
+
log(`${pet.name} was slain.`); if ((state.char.cls | 0) === 9) state.char.pet = undefined; // warlock: no Revive Pet (KI-833)
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
// CheckLearning: each cast this fight of an ability the pet can still teach rolled 10% for the owner
|
|
2525
|
+
// to learn the rank. The roll is per cast, and learning survives the pet's later death.
|
|
2526
|
+
const teachPet = state.char.pet;
|
|
2527
|
+
if (teachPet?.teachSpells?.length && !petTeachRolled.has(session)) {
|
|
2528
|
+
petTeachRolled.add(session);
|
|
2529
|
+
const casts: number[] = [];
|
|
2530
|
+
for (const e of result.events ?? []) {
|
|
2531
|
+
if (e.kind === 'cast_finish' && e.actor === 'pet') casts.push(e.spellId);
|
|
2532
|
+
}
|
|
2533
|
+
if (casts.length > 0) {
|
|
2534
|
+
const rng = pendingFight?.rng ?? mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
|
|
2535
|
+
const r = checkPetLearning(teachPet, state.char.petSpells ?? [], casts, rng, ds);
|
|
2536
|
+
state.char.pet = r.pet;
|
|
2537
|
+
if (r.learnedIds.length > 0) {
|
|
2538
|
+
state.char.petSpells = state.char.petSpells ?? [];
|
|
2539
|
+
for (const id of r.learnedIds) {
|
|
2540
|
+
if (!state.char.petSpells.includes(id)) state.char.petSpells.push(id);
|
|
2541
|
+
const s = ds.spell(id);
|
|
2542
|
+
if (s) log(`You have learned a new spell: ${s.name}${s.rank ? ` (Rank ${s.rank})` : ''}.`);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
// Every body in the party carries its cooldowns back out to the world clock, not just the player: a
|
|
2550
|
+
// companion Bloodlusting each mob and a pet Dashing each mob were the same defect.
|
|
2551
|
+
const persistPartyCooldowns = (session: CombatSession, cnow: number): void => {
|
|
2552
|
+
const snapOf = (side: Side): { spell: Record<number, number>; category: Record<number, number> } =>
|
|
2553
|
+
(session as any).cooldownSnapshot
|
|
2554
|
+
? (session as any).cooldownSnapshot(side)
|
|
2555
|
+
: { spell: {}, category: {} };
|
|
2556
|
+
if (state.char.pet) {
|
|
2557
|
+
const s = snapOf('pet');
|
|
2558
|
+
state.char.pet.spellCooldowns =
|
|
2559
|
+
worldCooldownsFromSessionSnapshot(state.char.pet.spellCooldowns, s.spell, cnow);
|
|
2560
|
+
state.char.pet.categoryCooldowns =
|
|
2561
|
+
worldCooldownsFromSessionSnapshot(state.char.pet.categoryCooldowns, s.category, cnow);
|
|
2562
|
+
}
|
|
2563
|
+
const layout = sessionPartyLayout.get(rawOf(session));
|
|
2564
|
+
if (!layout) return;
|
|
2565
|
+
for (let i = 0; i < layout.compIds.length; i++) {
|
|
2566
|
+
const comp = (state.squad ?? []).find((c) => c.id === layout.compIds[i]);
|
|
2567
|
+
if (!comp) continue;
|
|
2568
|
+
const s = snapOf(allySideAt(i));
|
|
2569
|
+
comp.spellCooldowns = worldCooldownsFromSessionSnapshot(comp.spellCooldowns, s.spell, cnow);
|
|
2570
|
+
comp.categoryCooldowns = worldCooldownsFromSessionSnapshot(comp.categoryCooldowns, s.category, cnow);
|
|
2571
|
+
const petIdx = layout.petAllyIndex[i];
|
|
2572
|
+
if (petIdx != null && comp.pet) {
|
|
2573
|
+
const ps = snapOf(allySideAt(petIdx));
|
|
2574
|
+
comp.pet.spellCooldowns = worldCooldownsFromSessionSnapshot(comp.pet.spellCooldowns, ps.spell, cnow);
|
|
2575
|
+
comp.pet.categoryCooldowns = worldCooldownsFromSessionSnapshot(comp.pet.categoryCooldowns, ps.category, cnow);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// Persist remaining spell/category cooldowns out of a session (long CDs survive the fight). Shared by win /
|
|
2581
|
+
// defeat / flee resolution paths.
|
|
2582
|
+
const persistSessionCooldowns = (session: CombatSession): void => {
|
|
2583
|
+
const snap = (session as any).cooldownSnapshot ? (session as any).cooldownSnapshot() : { spell: {}, category: {} };
|
|
2584
|
+
// enter + session.t, then advance the chain for the next pull's seed (KI-956).
|
|
2585
|
+
const cnow = commitFightClock(state, session, session.t);
|
|
2586
|
+
persistPartyCooldowns(session, cnow);
|
|
2587
|
+
state.spellCooldowns = state.spellCooldowns || {};
|
|
2588
|
+
state.categoryCooldowns = state.categoryCooldowns || {};
|
|
2589
|
+
for (const [sidStr, rem] of Object.entries(snap.spell as Record<string, number>)) {
|
|
2590
|
+
state.spellCooldowns[Number(sidStr)] = cnow + (rem as number);
|
|
2591
|
+
}
|
|
2592
|
+
for (const [cStr, rem] of Object.entries(snap.category as Record<string, number>)) {
|
|
2593
|
+
state.categoryCooldowns[Number(cStr)] = cnow + (rem as number);
|
|
2594
|
+
}
|
|
2595
|
+
// prune stale
|
|
2596
|
+
for (const k of Object.keys(state.spellCooldowns)) if (state.spellCooldowns[Number(k)] <= cnow) delete state.spellCooldowns[Number(k)];
|
|
2597
|
+
for (const k of Object.keys(state.categoryCooldowns)) if (state.categoryCooldowns[Number(k)] <= cnow) delete state.categoryCooldowns[Number(k)];
|
|
2598
|
+
};
|
|
2599
|
+
|
|
2600
|
+
// Settle a finished fight ONCE. A repeat call replays the first outcome without re-granting anything;
|
|
2601
|
+
// a multi-phase chain resolves a different session per phase, so each phase still settles on its own.
|
|
2602
|
+
const resolveFight = (session: CombatSession, opts?: { holdLoot?: boolean }): FightOutcome => {
|
|
2603
|
+
const already = settled.get(rawOf(session));
|
|
2604
|
+
if (already) return already;
|
|
2605
|
+
const outcome = settleFight(session, opts);
|
|
2606
|
+
settled.set(rawOf(session), outcome);
|
|
2607
|
+
return outcome;
|
|
2608
|
+
};
|
|
2609
|
+
|
|
2610
|
+
// The pull's authored bodies (primary + pack meta), in credit order.
|
|
2611
|
+
const authoredBodies = (
|
|
2612
|
+
cdef: CreatureDef | undefined,
|
|
2613
|
+
): Array<{ def: CreatureDef; level: number }> => {
|
|
2614
|
+
const out: Array<{ def: CreatureDef; level: number }> = [];
|
|
2615
|
+
if (cdef) out.push({ def: cdef, level: pendingFight?.enemyLevel ?? cdef.level });
|
|
2616
|
+
for (const p of pendingFight?.pack ?? []) {
|
|
2617
|
+
const pd = ds.creature(p.entry);
|
|
2618
|
+
if (pd) out.push({ def: pd, level: p.level });
|
|
2619
|
+
}
|
|
2620
|
+
return out;
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
// Those of them that DIED, plus any add a script summoned and lost. A win trusts the authored list:
|
|
2624
|
+
// a later summon recycles a dead seat (sessionInject freeSeatIn) and takes that corpse out of view.
|
|
2625
|
+
const bodiesThatDied = (
|
|
2626
|
+
session: CombatSession,
|
|
2627
|
+
authored: ReadonlyArray<{ def: CreatureDef; level: number }>,
|
|
2628
|
+
won: boolean,
|
|
2629
|
+
): Array<{ def: CreatureDef; level: number }> => {
|
|
2630
|
+
const corpses = new Map<number, number[]>();
|
|
2631
|
+
for (const e of session.enemiesView()) {
|
|
2632
|
+
if (e.hp > 0 || e.creatureEntry == null) continue;
|
|
2633
|
+
const rows = corpses.get(e.creatureEntry) ?? [];
|
|
2634
|
+
rows.push(e.level);
|
|
2635
|
+
corpses.set(e.creatureEntry, rows);
|
|
2636
|
+
}
|
|
2637
|
+
const out: Array<{ def: CreatureDef; level: number }> = [];
|
|
2638
|
+
for (const k of authored) {
|
|
2639
|
+
const rows = corpses.get(k.def.entry);
|
|
2640
|
+
if (rows?.length) rows.shift();
|
|
2641
|
+
else if (!won) continue;
|
|
2642
|
+
out.push(k);
|
|
2643
|
+
}
|
|
2644
|
+
// Floor shell: a credited corpse keeps its seat all run, so harvesting seats here would pay it
|
|
2645
|
+
// twice — creditFloorKills owns those bodies, by guid.
|
|
2646
|
+
if (pendingFight?.endless) return out;
|
|
2647
|
+
for (const [entry, rows] of corpses) {
|
|
2648
|
+
if (!rows.length) continue;
|
|
2649
|
+
const def = resolveCreatureDefForCombat(entry, (id) => ds.creature(id));
|
|
2650
|
+
if (!def) continue;
|
|
2651
|
+
// Cite: Formulas.h:368 — IsTotem / IsPet / IsNoXp / IsCritter gain nothing.
|
|
2652
|
+
if (def.noXp || def.creatureType === CREATURE_TYPE.TOTEM
|
|
2653
|
+
|| def.creatureType === CREATURE_TYPE.CRITTER) continue;
|
|
2654
|
+
for (const lvl of rows) out.push({ def, level: lvl });
|
|
2655
|
+
}
|
|
2656
|
+
return out;
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
// Auto-skin each body the party can take, hides into `loot`; the last one it could not becomes the
|
|
2660
|
+
// corpse the Skin button offers. Once per BODY, pack members included (KI-735).
|
|
2661
|
+
const skinTheFallen = (
|
|
2662
|
+
kills: ReadonlyArray<{ def: CreatureDef; level: number }>,
|
|
2663
|
+
rng: Rng,
|
|
2664
|
+
loot: LootDrop[],
|
|
2665
|
+
): { skinnable: boolean; kept: GameState['corpse'] } => {
|
|
2666
|
+
let skinnable = false;
|
|
2667
|
+
let kept: GameState['corpse'] = null;
|
|
2668
|
+
for (const k of kills) {
|
|
2669
|
+
const skinNode = ds.skinNode(k.def.entry);
|
|
2670
|
+
const skinDef = skinNode != null ? ds.gatherNode(skinNode) : undefined;
|
|
2671
|
+
if (skinNode == null || !skinDef) continue;
|
|
2672
|
+
skinnable = true;
|
|
2673
|
+
const skinnedOk = tryProfessionAutoSkin({
|
|
2674
|
+
ds, state, cdefName: k.def.name, skinDef, rng, applyGather, loot, log,
|
|
2675
|
+
});
|
|
2676
|
+
if (!skinnedOk) kept = { creatureEntry: k.def.entry, node: skinNode };
|
|
2677
|
+
}
|
|
2678
|
+
return { skinnable, kept };
|
|
2679
|
+
};
|
|
2680
|
+
|
|
2681
|
+
// Loot, coin, XP, quest credit, rep and pet loyalty for each body — per victim at the moment of
|
|
2682
|
+
// death, never per fight verdict. Cite: Unit.cpp:1084-1091 -> Player.cpp:18733.
|
|
2683
|
+
const payKillBodies = (
|
|
2684
|
+
kills: ReadonlyArray<{ def: CreatureDef; level: number }>,
|
|
2685
|
+
rng: Rng,
|
|
2686
|
+
opts?: { holdLoot?: boolean },
|
|
2687
|
+
): {
|
|
2688
|
+
loot: LootDrop[];
|
|
2689
|
+
heldDrops: LootDrop[];
|
|
2690
|
+
xpGained: number;
|
|
2691
|
+
goldGained: number;
|
|
2692
|
+
restedBonusXp: number;
|
|
2693
|
+
} => {
|
|
2694
|
+
const loot: LootDrop[] = [];
|
|
2695
|
+
let goldGained = 0;
|
|
2696
|
+
let xpGained = 0;
|
|
2697
|
+
let restedBonusXp = 0;
|
|
2698
|
+
// Quest-required loot rows only when an active quest still needs the item, and condition rows
|
|
2699
|
+
// through the baked conditions. Cite: partyNeededQuestItems; isConditionSatisfied.
|
|
2700
|
+
const neededLootItems = partyNeededQuestItems(state, ds);
|
|
2701
|
+
const lootRollCtx = {
|
|
2702
|
+
allowsQuestDrop: (entry: { item: number }) => neededLootItems.has(entry.item),
|
|
2703
|
+
allowsCondition: (conditionId: number) =>
|
|
2704
|
+
isConditionSatisfied(conditionId, lootConditionSnapshot(), conditionTableById()).ok,
|
|
2705
|
+
};
|
|
2706
|
+
// holdLoot: the corpse KEEPS its drops and coin for the player to click (loot window). Every
|
|
2707
|
+
// other caller — autopilot, bots, dungeon runs — still takes them straight into the bags.
|
|
2708
|
+
const heldDrops: LootDrop[] = [];
|
|
2709
|
+
for (const k of kills) {
|
|
2710
|
+
// Creature kill loot stays bare numeric lootId (LOCK 3 go: prefix is GO-only).
|
|
2711
|
+
const rolled = rollLootDeep(ds.lootTable(k.def.lootId), (id) => ds.lootTable(id), rng, 0, 8, lootRollCtx);
|
|
2712
|
+
for (const l of rolled) giveSquadQuestItem(state, ds, l.item, l.count);
|
|
2713
|
+
if (opts?.holdLoot) {
|
|
2714
|
+
for (const l of rolled) { heldDrops.push(l); loot.push(l); }
|
|
2715
|
+
} else {
|
|
2716
|
+
for (const l of rolled) {
|
|
2717
|
+
const took = takeDrop(l.item, l.count);
|
|
2718
|
+
if (took > 0) {
|
|
2719
|
+
const rp = latestRandomPropertyId(state.inventory, l.item);
|
|
2720
|
+
loot.push(rp != null ? { item: l.item, count: took, randomPropertyId: rp } : { item: l.item, count: took });
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
goldGained += rng.int(k.def.minGold, k.def.maxGold);
|
|
2725
|
+
state.questlog = recordKill(state.questlog, k.def.entry, (e) => ds.quest(e));
|
|
2726
|
+
creditSquadKill(state, k.def.entry, ds);
|
|
2727
|
+
applyCreatureDeathScripts(k.def.entry);
|
|
2728
|
+
// Formulas.h Gain per kill (BaseGain × elite × ExperienceMultiplier once).
|
|
2729
|
+
const killXp = killXpAmount(k.def, state.char.level, k.level);
|
|
2730
|
+
xpGained += killXp;
|
|
2731
|
+
// Rested bonus is kill-only and spends the pool (Player::GetXPRestBonus ~7754:
|
|
2732
|
+
// bonus = min(restBonus, xp), GiveXP(xp + bonus)). Quest/explore paths never spend.
|
|
2733
|
+
if (killXp > 0 && (state.char.restedXp ?? 0) > 0) {
|
|
2734
|
+
const spend = restBonusSpend(state.char.restedXp ?? 0, killXp);
|
|
2735
|
+
if (spend.bonus > 0) {
|
|
2736
|
+
xpGained += spend.bonus;
|
|
2737
|
+
restedBonusXp += spend.bonus;
|
|
2738
|
+
state.char = { ...state.char, restedXp: spend.remainingRested };
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
// Living hunter pet: loyalty-XP gate + KillLoyaltyBonus per kill. Cite: Pet.cpp GivePetXP ~1087.
|
|
2742
|
+
if (state.char.pet && state.char.pet.health > 0) {
|
|
2743
|
+
const petDef = ds.creature(state.char.pet.creature);
|
|
2744
|
+
if (petDef && petHasHappiness(petDef)) {
|
|
2745
|
+
const ownerXp = ds.xpToNext(state.char.level);
|
|
2746
|
+
state.char.pet = applyKillLoyalty(
|
|
2747
|
+
state.char.pet,
|
|
2748
|
+
killXp,
|
|
2749
|
+
state.char.level,
|
|
2750
|
+
Number.isFinite(ownerXp) ? ownerXp : undefined,
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
// Kill reputation (creature_onkill_reputation). Cite: Player.cpp RewardReputation(Creature).
|
|
2755
|
+
if (k.def.onKillRep) {
|
|
2756
|
+
const team = factionOf(ds, state.char.race) ?? 'alliance';
|
|
2757
|
+
const repSpells = [
|
|
2758
|
+
...racialSpells(ds, state.char.race),
|
|
2759
|
+
...(state.spellbook?.known ?? [])
|
|
2760
|
+
.map((id) => ds.spell(id))
|
|
2761
|
+
.filter((s): s is NonNullable<typeof s> => !!s?.passive),
|
|
2762
|
+
];
|
|
2763
|
+
const repMod = reputationAuraModFromSpells(repSpells);
|
|
2764
|
+
const repResult = rewardKillReputation({
|
|
2765
|
+
onKillRep: k.def.onKillRep,
|
|
2766
|
+
playerTeam: team,
|
|
2767
|
+
playerLevel: state.char.level,
|
|
2768
|
+
creatureLevel: k.level,
|
|
2769
|
+
raceId: state.char.race,
|
|
2770
|
+
classId: state.char.cls,
|
|
2771
|
+
reputations: state.char.reputations ?? {},
|
|
2772
|
+
factions: ds.factions(),
|
|
2773
|
+
rates: ds.reputationRates(),
|
|
2774
|
+
spillover: ds.reputationSpillover(),
|
|
2775
|
+
flags: state.char.factionFlags ?? {},
|
|
2776
|
+
...(repMod ? { repMod } : {}),
|
|
2777
|
+
});
|
|
2778
|
+
state.char = {
|
|
2779
|
+
...state.char,
|
|
2780
|
+
reputations: repResult.reputations,
|
|
2781
|
+
// SetVisible / auto at-war ride along, so a newly-earned faction shows up in the pane.
|
|
2782
|
+
factionFlags: repResult.flags ?? state.char.factionFlags,
|
|
2783
|
+
};
|
|
2784
|
+
for (const a of repResult.applied) {
|
|
2785
|
+
if (a.delta === 0) continue;
|
|
2786
|
+
const name = ds.faction(a.faction)?.name ?? `faction ${a.faction}`;
|
|
2787
|
+
const sign = a.delta > 0 ? '+' : '';
|
|
2788
|
+
log(`${sign}${a.delta} reputation with ${name}`);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return { loot, heldDrops, xpGained, goldGained, restedBonusXp };
|
|
2793
|
+
};
|
|
2794
|
+
|
|
2795
|
+
const settleFight = (session: CombatSession, opts?: { holdLoot?: boolean }): FightOutcome => {
|
|
2796
|
+
const result = session.result();
|
|
2797
|
+
// ScriptDev UpdateEntry morph (Dathrohan → Balnazzar): loot / gold / XP / quest kill
|
|
2798
|
+
// credit follow the FINAL form's template, not the entry that was pulled.
|
|
2799
|
+
const pulledDef = pendingFight ? ds.creature(pendingFight.creatureEntry) : undefined;
|
|
2800
|
+
const finalEntry = result.enemy?.finalCreatureEntry;
|
|
2801
|
+
const cdef =
|
|
2802
|
+
finalEntry != null && finalEntry !== pendingFight?.creatureEntry
|
|
2803
|
+
? resolveCreatureDefForCombat(finalEntry, (id) => ds.creature(id)) ?? pulledDef
|
|
2804
|
+
: pulledDef;
|
|
2805
|
+
const rng = pendingFight?.rng ?? mulberry32(1);
|
|
2806
|
+
syncCharFromSession(session);
|
|
2807
|
+
|
|
2808
|
+
// Vitals for a player who ended the fight at 0 HP; true when they repop as a ghost. Full death
|
|
2809
|
+
// loop only when the setting is `full` and outside a dungeon (wipes stay repair-bill-only).
|
|
2810
|
+
const applyPlayerDeath = (): boolean => {
|
|
2811
|
+
if (usesFullDeathFlow(deathFlowMode()) && !state.instance) {
|
|
2812
|
+
// Zone-based text RPG: death pos is (0,0,0) until continuous world coords exist.
|
|
2813
|
+
// Closest-GY still ranks by distance when bake supplies coordinates.
|
|
2814
|
+
state.deathFlow = deathToGhostAtGraveyard({
|
|
2815
|
+
nowMs: sessionClockNow(session),
|
|
2816
|
+
deathPos: { x: 0, y: 0, z: 0 },
|
|
2817
|
+
zoneId: state.char.zoneId,
|
|
2818
|
+
faction: factionOf(ds, state.char.race) === 'horde' ? TEAM_HORDE : TEAM_ALLIANCE,
|
|
2819
|
+
graveyards: ds.graveyards().map(toGraveyardEntry),
|
|
2820
|
+
deathExpireTimeSec: state.deathFlow?.deathExpireTimeSec ?? 0,
|
|
2821
|
+
});
|
|
2822
|
+
// Ghost: 1 HP (BuildPlayerRepop SetHealth(1)); power cleared like a corpse.
|
|
2823
|
+
state.char.health = 1;
|
|
2824
|
+
state.char.power = 0;
|
|
2825
|
+
return true;
|
|
2826
|
+
}
|
|
2827
|
+
// repair-only (default): revive at half health — autopilot / dungeon wipe path.
|
|
2828
|
+
state.deathFlow = createAliveDeathFlow(state.deathFlow?.deathExpireTimeSec ?? 0);
|
|
2829
|
+
state.char.health = Math.max(1, Math.round(maxHealth(state.char, ds) * 0.5));
|
|
2830
|
+
const pt = powerType(state.char);
|
|
2831
|
+
state.char.power = (pt === 'mana' || pt === 'energy') ? maxPower(state.char, ds) : 0;
|
|
2832
|
+
return false;
|
|
2833
|
+
};
|
|
2834
|
+
// Per-hit wear (Unit.cpp:1217 HandleDamageDealt) + 10% on death. Timeout ≠ death (KI-740).
|
|
2835
|
+
const died =
|
|
2836
|
+
result.winner === 'enemy' || (result.player.hp <= 0 && result.winner !== 'player');
|
|
2837
|
+
const soloEnemy = session.enemiesView().length <= 1;
|
|
2838
|
+
const wear = { died, events: result.events, rng, soloEnemy };
|
|
2839
|
+
state.char = applyCombatWear(state.char, ds.item, wear);
|
|
2840
|
+
state.char = applyDurabilityDamageFromLandedSpells(state.char, result.events, (id) => ds.spell(id), ds.item);
|
|
2841
|
+
for (const [i, comp] of (state.squad ?? []).entries()) {
|
|
2842
|
+
// Only allies in the session pay, each for their own death: no end state means a solo pull.
|
|
2843
|
+
const end = result.allies?.[i];
|
|
2844
|
+
if (!end) continue;
|
|
2845
|
+
const shell = companionCharacter(comp, companionLevel(comp, state.char.level), ds);
|
|
2846
|
+
const worn = applyCombatWear(shell, ds.item, { ...wear, died: end.hp <= 0, side: `ally_${i}` });
|
|
2847
|
+
comp.durability = worn.durability ?? {};
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// Credit spell cast targets for each player cast finished this fight, on defeat too so cast progress
|
|
2851
|
+
// can complete an objective without a win. The resolve-time pass is an idempotent backup.
|
|
2852
|
+
let spellCastCredited = false;
|
|
2853
|
+
let questEventCredited = false;
|
|
2854
|
+
for (const e of result.events ?? []) {
|
|
2855
|
+
// RewardPlayerAndGroupAtCast skips a next-melee-swing spell — Heroic Strike and Cleave never
|
|
2856
|
+
// pay a "cast N times" objective. Cite: Spell.cpp:1339, :1578.
|
|
2857
|
+
if (e.kind === 'cast_finish' && e.actor === 'player' && e.spellId
|
|
2858
|
+
&& !ds.spell(e.spellId)?.onNextSwing) {
|
|
2859
|
+
state.questlog = recordSpellCast(state.questlog, e.spellId, (id) => ds.quest(id));
|
|
2860
|
+
creditSquadSpellCast(state, e.spellId, ds);
|
|
2861
|
+
spellCastCredited = true;
|
|
2862
|
+
}
|
|
2863
|
+
if (e.kind === 'quest_event' && e.questId) {
|
|
2864
|
+
const before = state.questlog;
|
|
2865
|
+
state.questlog = recordEventHappens(state.questlog, e.questId);
|
|
2866
|
+
creditSquadEvent(state, e.questId);
|
|
2867
|
+
if (state.questlog !== before) questEventCredited = true;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (spellCastCredited || questEventCredited) refreshQuests();
|
|
2871
|
+
|
|
2872
|
+
if (result.winner === 'player' && cdef) {
|
|
2873
|
+
// Quest-fight yield stand-in (Bartleby 1640, Calvin 590, Dashel 1447). Cite: questFightCredit.ts.
|
|
2874
|
+
{
|
|
2875
|
+
const duel = creditQuestFightWin(ds, state.questlog, pendingFight?.creatureEntry, cdef.name);
|
|
2876
|
+
if (duel.message != null) {
|
|
2877
|
+
state.questlog = duel.questlog;
|
|
2878
|
+
creditSquadLogs(state, (log) =>
|
|
2879
|
+
creditQuestFightWin(ds, log, pendingFight?.creatureEntry, cdef.name).questlog);
|
|
2880
|
+
log(duel.message);
|
|
2881
|
+
refreshQuests();
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
// Record the fallen for Cannibalize: an Undead can devour a fresh Humanoid/Undead corpse to
|
|
2886
|
+
// heal out of combat (consumed by cannibalize(), and cleared on the next fight / travel).
|
|
2887
|
+
state.lastCorpse = { creatureType: cdef.creatureType ?? 0, creatureEntry: cdef.entry };
|
|
2888
|
+
|
|
2889
|
+
// Loot + XP for primary, Wave 4B pack assistants and any add a script summoned into the pull.
|
|
2890
|
+
const kills = bodiesThatDied(session, authoredBodies(cdef), true);
|
|
2891
|
+
// Every body that fell pays once. The verdict below only decides the penalties.
|
|
2892
|
+
const paid = payKillBodies(kills, rng, opts);
|
|
2893
|
+
const loot = paid.loot;
|
|
2894
|
+
const heldDrops = paid.heldDrops;
|
|
2895
|
+
const xpGained = paid.xpGained;
|
|
2896
|
+
let goldGained = paid.goldGained;
|
|
2897
|
+
const restedBonusXp = paid.restedBonusXp;
|
|
2898
|
+
if (opts?.holdLoot) {
|
|
2899
|
+
// Coin rides in the container too, so the money row is a click like it is in WoW.
|
|
2900
|
+
// Cap held copper so takeLootMoney cannot push past MAX_MONEY either.
|
|
2901
|
+
const room = Math.max(0, clampGold(state.char.gold + goldGained) - state.char.gold);
|
|
2902
|
+
goldGained = Math.min(goldGained, room);
|
|
2903
|
+
openLoot({
|
|
2904
|
+
kind: 'corpse',
|
|
2905
|
+
title: cdef.name,
|
|
2906
|
+
creatureEntry: cdef.entry,
|
|
2907
|
+
}, heldDrops, goldGained);
|
|
2908
|
+
loot.splice(0, loot.length, ...withPendingIdentities(loot, state.pendingLoot ?? null));
|
|
2909
|
+
} else {
|
|
2910
|
+
// ModifyMoney clamp — goldGained reports applied copper only (never exceed MAX_MONEY).
|
|
2911
|
+
const before = state.char.gold;
|
|
2912
|
+
state.char = { ...state.char, gold: clampGold(before + goldGained) };
|
|
2913
|
+
goldGained = state.char.gold - before;
|
|
2914
|
+
}
|
|
2915
|
+
refreshQuests();
|
|
2916
|
+
const lv = grantXp(state.char, xpGained, ds);
|
|
2917
|
+
state.char = lv.char;
|
|
2918
|
+
const packNote = kills.length > 1 ? ` (pack of ${kills.length})` : '';
|
|
2919
|
+
const restedNote = restedBonusXp > 0 ? ` (${restedBonusXp} rested)` : '';
|
|
2920
|
+
log(`You defeated ${cdef.name}${packNote}. +${xpGained} XP${restedNote}${goldGained ? `, +${moneyMarked(goldGained)}` : ''}.`);
|
|
2921
|
+
if (!opts?.holdLoot) {
|
|
2922
|
+
for (const l of loot) {
|
|
2923
|
+
const base = ds.item(l.item)?.name ?? String(l.item);
|
|
2924
|
+
const suffix = l.randomPropertyId != null && l.randomPropertyId > 0
|
|
2925
|
+
? ds.itemRandomProperty(l.randomPropertyId)?.nameSuffix
|
|
2926
|
+
: undefined;
|
|
2927
|
+
log(`Looted ${l.count}x ${formatItemName(base, suffix)}.`);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
for (const nl of lv.leveledTo) log(`You reached level ${nl}!`);
|
|
2931
|
+
|
|
2932
|
+
// Equipment helper: equip strict upgrades from newly acquired gear, weighted against this
|
|
2933
|
+
// character's current stats. Only 'auto' mutates; 'suggest' leaves them for the UI to offer.
|
|
2934
|
+
if (runAutoEquipUpgrades(8) > 0) log('Auto-equipped better gear.');
|
|
2935
|
+
|
|
2936
|
+
// Party Skinning: auto-skin (knife + skill-up). Nobody in the group holds the line: no hide.
|
|
2937
|
+
// A group skinner who cannot skin now (no knife / bags full) leaves the corpse (KI-735).
|
|
2938
|
+
// Once per BODY, pack members included — the floor path already skins per kill
|
|
2939
|
+
// (harvestFloorKills), so a pull of four beasts here left three hides on the ground.
|
|
2940
|
+
// state.corpse holds one body, so the last one the party could not take waits for the button.
|
|
2941
|
+
{
|
|
2942
|
+
const skin = skinTheFallen(kills, rng, loot);
|
|
2943
|
+
if (skin.skinnable) {
|
|
2944
|
+
state.corpse = skin.kept != null && partyHasSkinner(state) ? skin.kept : null;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
// Credit a dungeon encounter, on the final phase only. Death-summon, wave and HP-spawn rows chain
|
|
2948
|
+
// as the next phase of the same encounter id with no downtime for auto-resolve.
|
|
2949
|
+
const encounter = pendingFight?.encounter;
|
|
2950
|
+
if (encounter && state.instance && state.instance.dungeonId === encounter.dungeonId) {
|
|
2951
|
+
const def = ds.instance(encounter.dungeonId);
|
|
2952
|
+
const enc = def ? encounterOf(def, encounter.encounterId, state.instance) : undefined;
|
|
2953
|
+
if (def && enc) {
|
|
2954
|
+
const phaseIndex = encounter.phaseIndex ?? 0;
|
|
2955
|
+
const totalPhases = encounterPhaseCount(enc);
|
|
2956
|
+
|
|
2957
|
+
// HP phase flags: primary died ⇒ 0% crossed every authored threshold.
|
|
2958
|
+
if (phaseIndex === 0) {
|
|
2959
|
+
const thr = hpThresholdEvents(enc);
|
|
2960
|
+
if (thr.length) {
|
|
2961
|
+
const extra = flagsForHpThresholds(thr, 0, state.instance.flags ?? []);
|
|
2962
|
+
for (const f of extra) state.instance = setFlag(state.instance, f);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
if (phaseIndex + 1 < totalPhases) {
|
|
2967
|
+
// Intermediate phase: chain the next pull without marking the encounter cleared.
|
|
2968
|
+
const phaseRng = mulberry32(hashSeed(state.rngSeed, state.fightCounter++));
|
|
2969
|
+
const nextPhase = phaseIndex + 1;
|
|
2970
|
+
const phaseEnc = encounter.resolved ?? enc;
|
|
2971
|
+
const nextPull = resolveEncounterPhase(phaseEnc, nextPhase, () => phaseRng.next());
|
|
2972
|
+
persistSessionCooldowns(session);
|
|
2973
|
+
if (nextPull) {
|
|
2974
|
+
const nextName =
|
|
2975
|
+
resolveCreatureDefForCombat(nextPull.creatureEntry, (id) => ds.creature(id))?.name
|
|
2976
|
+
?? `creature entry ${nextPull.creatureEntry}`;
|
|
2977
|
+
log(`${nextName} appears!`);
|
|
2978
|
+
// Preserve S3 squad allies across dungeon phase chains.
|
|
2979
|
+
const keepSquad = pendingFight?.withSquad === true;
|
|
2980
|
+
// Phase N settled enough to chain — drop live pointer so startFight busy-gate allows phase N+1.
|
|
2981
|
+
liveFightSession = null;
|
|
2982
|
+
const nextBegin = startFight(nextPull.creatureEntry, nextPull.bossSpells, {
|
|
2983
|
+
dungeonId: encounter.dungeonId,
|
|
2984
|
+
encounterId: encounter.encounterId,
|
|
2985
|
+
pack: nextPull.pack,
|
|
2986
|
+
phaseIndex: nextPhase,
|
|
2987
|
+
// Same room, same roof — a phase chain never moves the pull.
|
|
2988
|
+
...(encounter.outdoors === false ? { outdoors: false } : {}),
|
|
2989
|
+
...(encounter.fusedIds?.length ? { fusedIds: encounter.fusedIds } : {}),
|
|
2990
|
+
...(encounter.resolved ? { resolved: encounter.resolved } : {}),
|
|
2991
|
+
}, keepSquad ? { withSquad: true } : undefined);
|
|
2992
|
+
if (nextBegin.ok) {
|
|
2993
|
+
nextBegin.session.runToEnd();
|
|
2994
|
+
const chained = resolveFight(nextBegin.session);
|
|
2995
|
+
// KI-760: merge prior-phase events/duration with chained phase (XP/loot already summed).
|
|
2996
|
+
const priorMs = result.durationMs;
|
|
2997
|
+
const chainedEvents = chained.result.events.map((e) =>
|
|
2998
|
+
priorMs > 0 ? { ...e, t: e.t + priorMs } : e,
|
|
2999
|
+
);
|
|
3000
|
+
const mergedResult = {
|
|
3001
|
+
...chained.result,
|
|
3002
|
+
events: [...result.events, ...chainedEvents],
|
|
3003
|
+
durationMs: priorMs + chained.result.durationMs,
|
|
3004
|
+
...(result.soulShardsGained || chained.result.soulShardsGained
|
|
3005
|
+
? {
|
|
3006
|
+
soulShardsGained:
|
|
3007
|
+
(result.soulShardsGained ?? 0) + (chained.result.soulShardsGained ?? 0),
|
|
3008
|
+
}
|
|
3009
|
+
: {}),
|
|
3010
|
+
...(result.itemsConsumed?.length || chained.result.itemsConsumed?.length
|
|
3011
|
+
? {
|
|
3012
|
+
itemsConsumed: [
|
|
3013
|
+
...(result.itemsConsumed ?? []),
|
|
3014
|
+
...(chained.result.itemsConsumed ?? []),
|
|
3015
|
+
],
|
|
3016
|
+
}
|
|
3017
|
+
: {}),
|
|
3018
|
+
...(result.allyItemsConsumed?.length || chained.result.allyItemsConsumed?.length
|
|
3019
|
+
? {
|
|
3020
|
+
allyItemsConsumed: [
|
|
3021
|
+
...(result.allyItemsConsumed ?? []),
|
|
3022
|
+
...(chained.result.allyItemsConsumed ?? []),
|
|
3023
|
+
],
|
|
3024
|
+
}
|
|
3025
|
+
: {}),
|
|
3026
|
+
// The push-time tallies are per-SESSION and a phase chain is two sessions, so the
|
|
3027
|
+
// spread above reported phase 1's mechanics, damage taken and deaths as zero.
|
|
3028
|
+
...mergePushTallies(result, chained.result, priorMs),
|
|
3029
|
+
};
|
|
3030
|
+
return {
|
|
3031
|
+
ok: true,
|
|
3032
|
+
outcome: chained.outcome,
|
|
3033
|
+
result: mergedResult,
|
|
3034
|
+
xpGained: xpGained + chained.xpGained,
|
|
3035
|
+
goldGained: goldGained + chained.goldGained,
|
|
3036
|
+
loot: [...loot, ...chained.loot],
|
|
3037
|
+
leveledTo: [...lv.leveledTo, ...chained.leveledTo],
|
|
3038
|
+
killCount: kills.length + chained.killCount,
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
// Next phase failed to start — fall through and clear the encounter so the
|
|
3042
|
+
// run cannot soft-lock. Reason is always concrete (never "unknown").
|
|
3043
|
+
const phaseReason =
|
|
3044
|
+
(nextBegin.reason && nextBegin.reason.trim())
|
|
3045
|
+
|| `missing failure reason (internal) for phase ${nextPhase} entry ${nextPull.creatureEntry}`;
|
|
3046
|
+
log(`The next phase could not start (${phaseReason}).`);
|
|
3047
|
+
} else {
|
|
3048
|
+
// encounterPhaseCount promised another phase but resolve returned null.
|
|
3049
|
+
log(
|
|
3050
|
+
`The next phase could not start (no pull for phase ${nextPhase} of ${enc.id}).`,
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// DR fusion: clear every encounter id in the fused batch (single id when unfused).
|
|
3056
|
+
const fusedIds = encounter.fusedIds?.length
|
|
3057
|
+
? encounter.fusedIds
|
|
3058
|
+
: [encounter.encounterId];
|
|
3059
|
+
for (const fid of fusedIds) {
|
|
3060
|
+
const fe = encounterOf(def, fid, state.instance);
|
|
3061
|
+
if (fe) state.instance = applyEncounterCleared(state.instance, fe);
|
|
3062
|
+
}
|
|
3063
|
+
// Dire Maul North: a Cho'Rush pack with King must clear the observer on pack death, or the
|
|
3064
|
+
// next encounter rematerializes him. Cite: instance_dire_maul.cpp:443-460.
|
|
3065
|
+
if (def.id === 'dire-maul-north' && fusedIds.includes('king-gordok')) {
|
|
3066
|
+
const killed = new Set<number>();
|
|
3067
|
+
if (pendingFight?.creatureEntry) killed.add(pendingFight.creatureEntry);
|
|
3068
|
+
for (const p of pendingFight?.pack ?? []) killed.add(p.entry);
|
|
3069
|
+
state.instance = applyDmNorthKingPackClear(state.instance, def, killed);
|
|
3070
|
+
}
|
|
3071
|
+
if (enc.kind === 'boss') log(`${enc.name} has been defeated!`);
|
|
3072
|
+
else if (fusedIds.length > 1) {
|
|
3073
|
+
log(`Pull cleared (${fusedIds.length} packs).`);
|
|
3074
|
+
}
|
|
3075
|
+
if (isComplete(def, state.instance)) log(`You have cleared ${def.name}!`);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
// Persist remaining spell/category cooldowns (long CDs survive the fight). Use the session's
|
|
3079
|
+
// elapsed t as the time delta for this fight (correct for both watch and fast resolve).
|
|
3080
|
+
persistSessionCooldowns(session);
|
|
3081
|
+
|
|
3082
|
+
// An outdoor win grants kill XP to each ACTIVE companion on the full personal curve, capped at the
|
|
3083
|
+
// player. Dungeon encounter wins grant elsewhere, so they are skipped here to avoid double-dipping.
|
|
3084
|
+
if (!pendingFight?.encounter) {
|
|
3085
|
+
for (const comp of state.squad ?? []) {
|
|
3086
|
+
for (const k of kills) {
|
|
3087
|
+
const r = applyCompanionKillXp(comp, k.def, state.char.level, ds, k.level);
|
|
3088
|
+
for (const nl of r.leveledTo) {
|
|
3089
|
+
log(`${comp.name} reaches level ${nl}!`);
|
|
3090
|
+
}
|
|
3091
|
+
if (r.leveledTo.length) syncCompanionPetAfterLevel(comp, r.leveledTo);
|
|
3092
|
+
applyCompanionKillReputation(comp, k.def, state.char.level, ds, k.level, log);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
// The party finished the kill while the player lay dead: partyWiped() is false with an ally up
|
|
3097
|
+
// (sessionPools.ts:307). Cite: Unit::DealDamage calls KillPlayer on the victim, not on a verdict.
|
|
3098
|
+
if (result.player.hp <= 0) {
|
|
3099
|
+
if (applyPlayerDeath()) log('Your party finished the kill. Your spirit releases to the graveyard.');
|
|
3100
|
+
else log('Your party finished the kill. You woke up weakened.');
|
|
3101
|
+
}
|
|
3102
|
+
pendingFight = null;
|
|
3103
|
+
liveFightSession = null;
|
|
3104
|
+
return {
|
|
3105
|
+
ok: true, outcome: 'win', result, xpGained, goldGained, loot,
|
|
3106
|
+
leveledTo: lv.leveledTo, killCount: kills.length,
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// Persist cooldowns on non-win paths too (time advanced).
|
|
3111
|
+
persistSessionCooldowns(session);
|
|
3112
|
+
|
|
3113
|
+
// A wipe or a stall does not un-kill what already fell, so the bodies pay and only the penalties
|
|
3114
|
+
// below read the verdict. Cite: Unit.cpp:1084-1091.
|
|
3115
|
+
const fallen = bodiesThatDied(session, authoredBodies(cdef), false);
|
|
3116
|
+
const paidFallen = fallen.length ? payKillBodies(fallen, rng) : null;
|
|
3117
|
+
let fallenXp = 0;
|
|
3118
|
+
let fallenGold = 0;
|
|
3119
|
+
let fallenLoot: LootDrop[] = [];
|
|
3120
|
+
let fallenLevels: number[] = [];
|
|
3121
|
+
if (paidFallen) {
|
|
3122
|
+
const before = state.char.gold;
|
|
3123
|
+
state.char = { ...state.char, gold: clampGold(before + paidFallen.goldGained) };
|
|
3124
|
+
fallenGold = state.char.gold - before;
|
|
3125
|
+
fallenLoot = paidFallen.loot;
|
|
3126
|
+
fallenXp = paidFallen.xpGained;
|
|
3127
|
+
refreshQuests();
|
|
3128
|
+
const lvF = grantXp(state.char, fallenXp, ds);
|
|
3129
|
+
state.char = lvF.char;
|
|
3130
|
+
fallenLevels = lvF.leveledTo;
|
|
3131
|
+
if (!pendingFight?.encounter) {
|
|
3132
|
+
for (const comp of state.squad ?? []) {
|
|
3133
|
+
for (const k of fallen) {
|
|
3134
|
+
const r = applyCompanionKillXp(comp, k.def, state.char.level, ds, k.level);
|
|
3135
|
+
for (const nl of r.leveledTo) log(`${comp.name} reaches level ${nl}!`);
|
|
3136
|
+
if (r.leveledTo.length) syncCompanionPetAfterLevel(comp, r.leveledTo);
|
|
3137
|
+
applyCompanionKillReputation(comp, k.def, state.char.level, ds, k.level, log);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
log(
|
|
3142
|
+
`${fallen.length} kill${fallen.length === 1 ? '' : 's'} credited. +${fallenXp} XP`
|
|
3143
|
+
+ (fallenGold ? `, +${moneyMarked(fallenGold)}` : '') + '.',
|
|
3144
|
+
);
|
|
3145
|
+
for (const l of fallenLoot) {
|
|
3146
|
+
const base = ds.item(l.item)?.name ?? String(l.item);
|
|
3147
|
+
const suffix = l.randomPropertyId != null && l.randomPropertyId > 0
|
|
3148
|
+
? ds.itemRandomProperty(l.randomPropertyId)?.nameSuffix
|
|
3149
|
+
: undefined;
|
|
3150
|
+
log(`Looted ${l.count}x ${formatItemName(base, suffix)}.`);
|
|
3151
|
+
}
|
|
3152
|
+
for (const nl of fallenLevels) log(`You reached level ${nl}!`);
|
|
3153
|
+
}
|
|
3154
|
+
// The beasts that fell are still skinnable — a wipe leaves hides on the ground otherwise. The
|
|
3155
|
+
// corpse the party could not take waits for the Skin button, as on a win.
|
|
3156
|
+
const skinFallen = skinTheFallen(fallen, rng, fallenLoot);
|
|
3157
|
+
const keptCorpse = skinFallen.skinnable && skinFallen.kept != null && partyHasSkinner(state)
|
|
3158
|
+
? skinFallen.kept
|
|
3159
|
+
: null;
|
|
3160
|
+
|
|
3161
|
+
// Timeout or stall with the player still alive aborts without death penalties: a combat cap is not a
|
|
3162
|
+
// kill, so no half HP, no ghost, no cancelled journey and no defeat.
|
|
3163
|
+
if (!died && result.player.hp > 0) {
|
|
3164
|
+
if (cdef) {
|
|
3165
|
+
log(`The fight with ${cdef.name} drags on with no end. You disengage.`);
|
|
3166
|
+
} else {
|
|
3167
|
+
log('The fight drags on with no end. You disengage.');
|
|
3168
|
+
}
|
|
3169
|
+
state.corpse = keptCorpse;
|
|
3170
|
+
pendingFight = null;
|
|
3171
|
+
liveFightSession = null;
|
|
3172
|
+
return {
|
|
3173
|
+
ok: true, outcome: 'timeout', result,
|
|
3174
|
+
xpGained: fallenXp, goldGained: fallenGold, loot: fallenLoot, leveledTo: fallenLevels,
|
|
3175
|
+
killCount: fallen.length,
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
state.corpse = keptCorpse; // skinnable creature corpse — not the player corpse record
|
|
3179
|
+
|
|
3180
|
+
const ghosted = applyPlayerDeath();
|
|
3181
|
+
// A ghost is at the graveyard, not over the body, so the kept corpse does not travel with them.
|
|
3182
|
+
if (ghosted) state.corpse = null;
|
|
3183
|
+
if (ghosted) {
|
|
3184
|
+
if (cdef) log(`You were slain by ${cdef.name}. Your spirit releases to the graveyard.`);
|
|
3185
|
+
else log('You have died. Your spirit releases to the graveyard.');
|
|
3186
|
+
if (state.deathFlow?.graveyard) {
|
|
3187
|
+
log('A spirit healer waits at the graveyard - or run back to your corpse.');
|
|
3188
|
+
}
|
|
3189
|
+
} else if (cdef) {
|
|
3190
|
+
log(`You were defeated by ${cdef.name} and woke up weakened.`);
|
|
3191
|
+
}
|
|
3192
|
+
pendingFight = null;
|
|
3193
|
+
liveFightSession = null;
|
|
3194
|
+
return {
|
|
3195
|
+
ok: true, outcome: 'defeat', result,
|
|
3196
|
+
xpGained: fallenXp, goldGained: fallenGold, loot: fallenLoot, leveledTo: fallenLevels,
|
|
3197
|
+
killCount: fallen.length,
|
|
3198
|
+
};
|
|
3199
|
+
};
|
|
3200
|
+
|
|
3201
|
+
const fight = (
|
|
3202
|
+
creatureEntry: number,
|
|
3203
|
+
opts?: { withSquad?: boolean; noAoe?: boolean },
|
|
3204
|
+
): ActionResult<Omit<FightOutcome, 'ok'>> => {
|
|
3205
|
+
const b = beginFight(creatureEntry, opts);
|
|
3206
|
+
if (!b.ok) return b;
|
|
3207
|
+
b.session.runToEnd();
|
|
3208
|
+
return resolveFight(b.session);
|
|
3209
|
+
};
|
|
3210
|
+
|
|
3211
|
+
|
|
3212
|
+
return {
|
|
3213
|
+
methods: {
|
|
3214
|
+
fight,
|
|
3215
|
+
beginFight,
|
|
3216
|
+
resolveFight,
|
|
3217
|
+
beginEncounter,
|
|
3218
|
+
beginFloorPull,
|
|
3219
|
+
beginFloorShell,
|
|
3220
|
+
injectFloorMember,
|
|
3221
|
+
noteFloorPackMember,
|
|
3222
|
+
creditFloorKills,
|
|
3223
|
+
pullAdditionalPack,
|
|
3224
|
+
useInstanceObject,
|
|
3225
|
+
lootInstanceObject,
|
|
3226
|
+
liveFightSession: () => liveFightSession,
|
|
3227
|
+
},
|
|
3228
|
+
startFight,
|
|
3229
|
+
resolveFight,
|
|
3230
|
+
syncCharFromSession,
|
|
3231
|
+
persistSessionCooldowns,
|
|
3232
|
+
getPendingFight: () => pendingFight,
|
|
3233
|
+
setPendingFight: (p) => { pendingFight = p; },
|
|
3234
|
+
getLiveFightSession: () => liveFightSession,
|
|
3235
|
+
setLiveFightSession: (s) => { liveFightSession = s; },
|
|
3236
|
+
allySideForCompanion: (session, compId) => {
|
|
3237
|
+
const i = sessionPartyLayout.get(rawOf(session))?.compIds.indexOf(compId) ?? -1;
|
|
3238
|
+
return i >= 0 ? allySideAt(i) : null;
|
|
3239
|
+
},
|
|
3240
|
+
};
|
|
3241
|
+
}
|