Files
mod-mythic-plus/src/Scripts/UnitScript.cpp

477 lines
22 KiB
C++

#include "MpLogger.h"
#include "Player.h"
#include "MythicPlus.h"
#include "ScriptMgr.h"
#include "SpellAuraEffects.h"
class MythicPlus_UnitScript : public UnitScript
{
public:
MythicPlus_UnitScript() : UnitScript("MythicPlus_UnitScript", true) { }
private:
/**
* Handles damage from non-creature sources (GameObjects, Players, etc.)
* @tparam DamageType Type of damage (int32/float)
* @param target Target of the damage
* @param attacker The non-creature attacker (passed by reference and may be modified)
* @param damage Reference to damage value (will be modified)
* @param spellInfo The spell being cast
* @param eventType Type of event (spell/melee/etc)
*/
template<typename DamageType>
void HandleNonCreatureAttacker(Unit* target, Unit*& attacker, DamageType& damage,
SpellInfo const* spellInfo, MythicPlus::MP_UNIT_EVENT_TYPE eventType)
{
Map* map = target ? target->GetMap() : nullptr;
std::string attackerType = "nullptr";
std::string attackerName = "unknown";
uint32 entry = 0;
if (attacker) {
if (attacker->GetTypeId() == TYPEID_GAMEOBJECT) {
attackerType = "GameObject";
if (GameObject* go = attacker->ToGameObject()) {
entry = go->GetEntry();
if (GameObjectTemplate const* goInfo = go->GetGOInfo()) {
attackerName = goInfo->name;
}
}
} else if (attacker->GetTypeId() == TYPEID_PLAYER) {
attackerType = "Player";
attackerName = attacker->GetName();
} else if (attacker->GetTypeId() == TYPEID_UNIT) {
attackerType = "Unit (non-creature)";
attackerName = attacker->GetName();
} else {
attackerType = "Unknown Type";
}
}
// Try to find a creature attacker from target's attackers list if we have one use it for scaling
Unit::AttackerSet const& attackers = target ? target->getAttackers() : Unit::AttackerSet();
if (!attackers.empty()) {
attacker = *attackers.begin();
if (Creature* creatureAttacker = attacker->ToCreature()) {
if (MpCreatureData* creatureData = sMpDataStore->GetCreatureData(creatureAttacker->GetGUID())) {
damage = static_cast<DamageType>(modifyIncomingDmgHeal(eventType, target, creatureAttacker,
static_cast<uint32>(damage), spellInfo)) * sMythicPlus->nonCreatureSpellReducer;
return;
}
} else {
MpLogger::debug("====== SPELL SCALING: Non-Creature attacker - Name: {}, Spell: {}({}), Damage: {}",
attackerName,
spellInfo ? spellInfo->SpellName[0] : "No Spell",
spellInfo ? spellInfo->Id : 0,
damage);
if (map) {
if (MpInstanceData* instanceData = sMpDataStore->GetInstanceData(map->GetId(), map->GetInstanceId())) {
damage = static_cast<DamageType>(damage * instanceData->creature.spell * sMythicPlus->nonCreatureSpellReducer);
return;
}
}
}
}
// Fallback to instance-based scaling if we can't find a nearest creature
else if (map) {
if (MpInstanceData* instanceData = sMpDataStore->GetInstanceData(map->GetId(), map->GetInstanceId())) {
damage = static_cast<DamageType>(damage * instanceData->creature.spell * sMythicPlus->nonCreatureSpellReducer);
return;
}
}
// Default scaling if no specific handler applied
return;
}
// Helper function to determine if a spell scales with Attack Power
bool IsAttackPowerScalingSpell(SpellInfo const* spellInfo) {
if (!spellInfo || spellInfo->Effects.empty()) {
return false;
}
auto mainEffect = spellInfo->Effects[0];
// Check 1: Direct weapon damage effects
bool isWeaponEffect = (mainEffect.Effect == SPELL_EFFECT_WEAPON_DAMAGE ||
mainEffect.Effect == SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL ||
mainEffect.Effect == SPELL_EFFECT_NORMALIZED_WEAPON_DMG ||
mainEffect.Effect == SPELL_EFFECT_WEAPON_PERCENT_DAMAGE);
// Check 2: Damage class indicates melee/ranged (scales with AP)
bool isMeleeOrRanged = (spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MELEE ||
spellInfo->DmgClass == SPELL_DAMAGE_CLASS_RANGED);
// Check 3: Requires weapon equipment
bool requiresWeapon = (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON);
// Check 4: Specific spell families known to scale with AP
bool isKnownAPSpell = false;
if (spellInfo->SpellFamilyName == SPELLFAMILY_ROGUE) {
// Rogue poisons and weapon-based abilities
isKnownAPSpell = (spellInfo->SpellFamilyFlags[0] & 0x10000) || // Deadly Poison flag
(spellInfo->SpellFamilyFlags[1] & 0x80000); // Other poison flags
}
// Return true if any indicator suggests AP scaling
return (isWeaponEffect || isMeleeOrRanged || requiresWeapon || isKnownAPSpell);
}
/**
* @brief This functions processes spell damage for DOTs and Direct Damage Spells it
* handles special cases for Melee scaling spells and AP scaling spells also so they
* are not scaled up twice and murder all my friends
*
* @tparam DamageType
* @param target
* @param attacker
* @param damage
* @param spellInfo
* @param eventType
* @param logPrefix
*/
template<typename DamageType>
void ProcessSpellDamage(Unit* target, Unit* attacker, DamageType& damage, SpellInfo const* spellInfo, MythicPlus::MP_UNIT_EVENT_TYPE eventType, const std::string& logPrefix) {
if(damage == 0) {
return;
}
// If this is a special case where the attacker is not a creature
if (!attacker || !attacker->ToCreature()) {
return HandleNonCreatureAttacker(target, attacker, damage, spellInfo, eventType);
}
Creature* creatureCaster = attacker->ToCreature();
MpCreatureData* creatureData = sMpDataStore->GetCreatureData(creatureCaster->GetGUID());
if (!creatureCaster) {
MpLogger::error("Creature caster is null in map {}", attacker ? attacker->GetMap()->GetId() : 0);
return;
}
if (!creatureData) {
MpLogger::error("Failed to get creature data for {} in map {}", creatureCaster->GetName(), attacker ? attacker->GetMap()->GetId() : 0);
return;
}
// Check if this spell scales with Attack Power
if (IsAttackPowerScalingSpell(spellInfo)) {
// need another special case here to determine if a spell was not scaled up by AP meaning the incoming damage is close to the
// original effect of the spell and therefore should instead use spell effect scaling it should be no more than 15% of the original effect
bool notScaledByAP = false;
if (spellInfo && !spellInfo->Effects.empty()) {
int32 baseEffect = spellInfo->Effects[0].CalcValue(attacker, nullptr, nullptr);
if (damage <= (baseEffect * 1.15f)) {
// MpLogger::debug(">>>> MELEE SPELL SCALING: Spell {} (ID: {}) is not scaled by AP damage: {} vs originalEffect: {}",
// spellInfo->SpellName[0], spellInfo->Id, damage, baseEffect);
notScaledByAP = true;
}
} else {
// If we can't determine the base effect, default to treating it as not AP-scaled
notScaledByAP = true;
// MpLogger::debug(">>>> MELEE SPELL SCALING: Could not determine base effect for spell, defaulting to spell scaling");
}
// if the effect type of the spell is not physical (aka not mitigated by armor/defense) then it needs to instead have the typical
// spell damage multiplier applied instead of melee damage scaling
if (! notScaledByAP && (spellInfo->SchoolMask == SPELL_SCHOOL_NORMAL || spellInfo->SchoolMask == SPELL_SCHOOL_MASK_NORMAL)) {
uint32 meleeDamage = static_cast<uint32>(std::max(0, static_cast<int32>(damage)));
damage = modifyIncomingDmgHeal(MythicPlus::UNIT_EVENT_MELEE, target, attacker, meleeDamage);
// MpLogger::debug(">>MELEE SPELL SCALING: {} hits with spell: {} ID: {} meleeDamage: {} damage: {}", attacker->GetName(), spellInfo->SpellName[0], spellInfo->Id, meleeDamage, damage);
} else {
// get the creatures original attack power
SpellEffectInfo const& effect = spellInfo->Effects[0];
uint32 spellDmg = static_cast<uint32>(effect.CalcValue(attacker, nullptr, nullptr) * effect.CalcDamageMultiplier(attacker, nullptr));
// now take the original attack power * 0.08 and add it to the spell damage
uint32 apDmg = static_cast<uint32>(creatureData->originalStats->AttackPower * 0.10f);
uint32 finalDmg = spellDmg + apDmg;
// MpLogger::debug(">> AP BASED DAMAGE Scaledown: origDamage: {} | spellDmg: {} | apDmg: {} | finalDmg: {}", static_cast<int32>(damage), spellDmg, apDmg, finalDmg);
damage = modifyIncomingDmgHeal(MythicPlus::UNIT_EVENT_SPELL, target, attacker, finalDmg, spellInfo);
// need to take into consideration if this is a stacking spell and multiply the final damage by the number of stacks
if(spellInfo->AttributesEx3 & SPELL_ATTR3_DOT_STACKING_RULE) {
Aura* aura = target->GetAura(spellInfo->Id, attacker->GetGUID());
uint32 stacks = aura ? aura->GetStackAmount() : 1;
if(aura) {
damage *= stacks;
}
}
}
return;
}
// Reverse the CalcValue scaling to get original base damage
int32 originalDamage = static_cast<int32>(damage);
if (creatureCaster && creatureData && creatureData->originalLevel < 70) {
CreatureTemplate const* cInfo = creatureCaster->GetCreatureTemplate();
// Get the scaling factors used in CalcValue
CreatureBaseStats const* pCBS = sObjectMgr->GetCreatureBaseStats(creatureCaster->GetLevel(), creatureCaster->getClass());
float CBSPowerCreature = pCBS->BaseDamage[cInfo->expansion];
uint32 tempLevel = spellInfo->SpellLevel;
if(tempLevel == 0) {
tempLevel = creatureData->originalLevel;
}
CreatureBaseStats const* spellCBS = sObjectMgr->GetCreatureBaseStats(tempLevel, creatureCaster->getClass());
float CBSPowerSpell = spellCBS->BaseDamage[cInfo->expansion];
// MpLogger::debug("SPELL SCALING: Creature Lvl {} -> {} | Spell Lvl {} | tempLevel: {} | CBSPowerCreature: {} CBSPowerSpell: {}",
// creatureData->originalLevel, creatureCaster->GetLevel(), tempLevel, CBSPowerCreature, CBSPowerSpell);
// Reverse the CalcValue scaling: originalDamage = scaledDamage / (CBSPowerCreature / CBSPowerSpell)
if (CBSPowerCreature > 0.0f) {
originalDamage = static_cast<int32>(static_cast<int32>(damage) * (CBSPowerSpell / CBSPowerCreature));
// MpLogger::debug("SPELL SCALING: Reversed CalcValue scaling - Scaled: {} -> Original: {} (Factor: {:.2f})",
// damage, originalDamage, CBSPowerSpell / CBSPowerCreature);
}
}
// Apply Mythic+ scaling to the original base damage
damage = static_cast<DamageType>(modifyIncomingDmgHeal(eventType, target, attacker, static_cast<uint32>(originalDamage), spellInfo));
}
public:
void ModifyPeriodicDamageAurasTick(Unit* target, Unit* attacker, uint32& damage, SpellInfo const* spellInfo) override {
if (!target && !attacker) {
return;
}
Map *map = target->GetMap();
if(!sMythicPlus->IsMapEligible(map)) {
return;
}
auto effects = spellInfo->Effects;
bool isHot = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) {
switch(effects[i].Effect) {
case SPELL_EFFECT_HEAL:
case SPELL_EFFECT_HEAL_MAX_HEALTH:
case SPELL_EFFECT_HEAL_MECHANICAL:
case SPELL_EFFECT_HEAL_PCT:
case SPELL_EFFECT_SPIRIT_HEAL:
isHot = true;
break;
}
}
if(isHot) {
damage = modifyIncomingDmgHeal(MythicPlus::UNIT_EVENT_HOT, target, attacker, damage, spellInfo);
} else {
ProcessSpellDamage(target, attacker, damage, spellInfo, MythicPlus::UNIT_EVENT_DOT, "DOT DAMAGE");
}
}
void ModifySpellDamageTaken(Unit* target, Unit* attacker, int32& damage, SpellInfo const* spellInfo) override {
if (!target && !attacker) {
if(spellInfo) {
// MpLogger::info("ModifySpellDamageTaken: Target and attacker are null for spell: {} ID: {}", spellInfo->SpellName[0], spellInfo->Id);
}
return;
}
Map *map = target->GetMap();
if(!sMythicPlus->IsMapEligible(map)) {
return;
}
if(!sMythicPlus->EligibleDamageTarget(target)) {
if(spellInfo) {
// MpLogger::info("ModifySpellDamageTaken: Target is not eligible for spell: {} ID: {}", spellInfo->SpellName[0], spellInfo->Id);
}
return;
}
// MpLogger::debug("ModifySpellDamageTaken: {} hits {} with spell: {} ID: {}", attacker ? attacker->GetName() : "[null]", target ? target->GetName() : "[null]", spellInfo ? spellInfo->SpellName[0] : "[no spell]", spellInfo ? spellInfo->Id : 0);
// Use the generic ProcessSpellDamage function
ProcessSpellDamage(target, attacker, damage, spellInfo, MythicPlus::UNIT_EVENT_SPELL, "SPELL DAMAGE");
}
/**
* Directly Modify the melee damage characters and allied creatures will
* receive from mythic+ scaled enemies.
*/
void ModifyMeleeDamage(Unit* target, Unit* attacker, uint32& damage) override {
if (!target && !attacker) {
return;
}
Map *map = target->GetMap();
if(!sMythicPlus->IsMapEligible(map)) {
return;
}
damage = modifyIncomingDmgHeal(MythicPlus::UNIT_EVENT_MELEE, target, attacker, damage);
}
// When a healing spell hits a mythic+ enemy modify based on the modifiers for the difficulty
void ModifyHealReceived(Unit* target, Unit* healer, uint32& healing, SpellInfo const* spellInfo) override {
if (!target && !healer) {
return;
}
Map *map = target->GetMap();
if(!sMythicPlus->IsMapEligible(map)) {
return;
}
healing = modifyIncomingDmgHeal(MythicPlus::UNIT_EVENT_HEAL, target, healer, healing, spellInfo);
}
uint32 modifyIncomingDmgHeal(MythicPlus::MP_UNIT_EVENT_TYPE eventType,Unit* target, Unit* attacker, uint32 damageOrHeal, SpellInfo const* spellInfo = nullptr) {
if (!target && !attacker) {
// MpLogger::info("modifyIncomingDmgHeal: Target and attacker are null for event {}", eventType);
return damageOrHeal;
}
int32 alteredDmgHeal = 0;
Map *map = target->GetMap();
if(!sMythicPlus->IsMapEligible(map)) {
return damageOrHeal;
}
if(attacker && attacker->IsPlayer()) {
return damageOrHeal;
}
#if defined(MOD_PRESENT_NPCBOTS)
if (attacker && attacker->IsNPCBotOrPet()) {
return damageOrHeal;
}
#endif
Creature* creature = attacker ? attacker->ToCreature() : nullptr;
if (!creature) {
MpLogger::debug("Attacker was considered not a creature");
return damageOrHeal;
}
MpInstanceData* instanceData = sMpDataStore->GetInstanceData(map->GetId(), map->GetInstanceId());
if(!instanceData) {
return damageOrHeal;
}
std::string eventName = "";
switch (eventType) {
case MythicPlus::UNIT_EVENT_MELEE:
eventName = "Melee";
break;
case MythicPlus::UNIT_EVENT_HEAL:
eventName = "Heal";
break;
case MythicPlus::UNIT_EVENT_DOT:
eventName = "DOT";
break;
case MythicPlus::UNIT_EVENT_SPELL:
eventName = "Spell";
break;
case MythicPlus::UNIT_EVENT_HOT:
eventName = "HOT";
break;
}
// If the target is the enemy then increase the amount of healing by the instance data modifier for spell output.
if(sMythicPlus->EligibleDamageTarget(target)) {
/**
* @TODO: Allow more granular control over the scaling of DOT, HOT, and other spell effects
* in the future if needed
*/
switch (eventType) {
case MythicPlus::UNIT_EVENT_MELEE:
// Damage that is not mitigated by armor needs to be debuffed as it hits too hard and without resists
// it hits too hard give everyone a benefit of 30% armor reduction
MpLogger::debug(">>> Modify Melee Damage: Creature Name: {} alteredDmgHeal: {} School Mask: {}", creature->GetName(), alteredDmgHeal, creature->GetMeleeDamageSchoolMask());
if(creature->GetMeleeDamageSchoolMask() != SPELL_SCHOOL_MASK_NORMAL && creature->GetMeleeDamageSchoolMask() != SPELL_SCHOOL_MASK_NONE) {
damageOrHeal = damageOrHeal * 0.50f;
}
if(creature->IsDungeonBoss() || creature->isWorldBoss() || creature->GetEntry() == 23682) {
alteredDmgHeal = damageOrHeal * instanceData->boss.melee;
} else {
alteredDmgHeal = damageOrHeal * instanceData->creature.melee;
}
// MpLogger::debug(">>>>>>>>>>>> Incoming Melee New Damage: {}({}) {} hits {}", alteredDmgHeal, damageOrHeal, attacker->GetName(), target->GetName());
break;
case MythicPlus::UNIT_EVENT_DOT:
case MythicPlus::UNIT_EVENT_SPELL:
if(creature->IsDungeonBoss() || creature->isWorldBoss() || creature->GetEntry() == 23682) {
if(spellInfo) {
// MpLogger::debug("Scaling spell {} using ScaleDamageSpell() Original Damage: {} New Damage: {}", spellInfo->SpellName[0], damageOrHeal, alteredDmgHeal);
alteredDmgHeal = sMythicPlus->ScaleDamageSpell(spellInfo, damageOrHeal, sMpDataStore->GetCreatureData(attacker->GetGUID()), creature, target, instanceData->boss.spell);
} else {
alteredDmgHeal = damageOrHeal * instanceData->boss.spell;
// MpLogger::debug("Scaling spell {} using flat modifier Original Damage: {} New Damage: {}", spellInfo->SpellName[0], damageOrHeal, alteredDmgHeal);
}
} else {
if(spellInfo) {
// MpLogger::debug("Scaling spell {} using ScaleDamageSpell() Original Damage: {} New Damage: {}", spellInfo->SpellName[0], damageOrHeal, alteredDmgHeal);
alteredDmgHeal = sMythicPlus->ScaleDamageSpell(spellInfo, damageOrHeal, sMpDataStore->GetCreatureData(attacker->GetGUID()), creature, target, instanceData->creature.spell);
} else {
// MpLogger::debug("Scaling spell {} using flat modifier Original Damage: {} New Damage: {}", spellInfo->SpellName[0], damageOrHeal, alteredDmgHeal);
alteredDmgHeal = damageOrHeal * instanceData->creature.spell;
}
}
break;
case MythicPlus::UNIT_EVENT_HEAL:
case MythicPlus::UNIT_EVENT_HOT:
break;
}
}
/**
* @TODO: Add more granular control over the scaling of healing spells
*/
if(sMythicPlus->EligibleHealTarget(target) && (eventType == MythicPlus::UNIT_EVENT_HEAL || eventType == MythicPlus::UNIT_EVENT_HOT)) {
if(creature->IsDungeonBoss()) {
if(spellInfo) {
alteredDmgHeal = sMythicPlus->ScaleHealSpell(spellInfo, damageOrHeal, sMpDataStore->GetCreatureData(attacker->GetGUID()), creature, attacker->ToCreature(), instanceData->boss.spell * 0.7f);
} else {
alteredDmgHeal = damageOrHeal * instanceData->boss.spell * 0.7f;
}
} else {
if(spellInfo) {
alteredDmgHeal = sMythicPlus->ScaleHealSpell(spellInfo, damageOrHeal, sMpDataStore->GetCreatureData(attacker->GetGUID()), creature, attacker->ToCreature(), instanceData->creature.spell * 0.7f);
} else {
alteredDmgHeal = damageOrHeal * instanceData->creature.spell * 0.70f;
}
}
MpLogger::debug("Incoming heal: {}({}) {} hits {}",
alteredDmgHeal,
damageOrHeal,
attacker ? attacker->GetName() : "[null]",
target ? target->GetName() : "[null]");
}
return alteredDmgHeal > 0 ? alteredDmgHeal : damageOrHeal;
}
};
void Add_MP_UnitScripts()
{
new MythicPlus_UnitScript();
}