From a60221393e5c49a1f975e56de4365906a23542d6 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 7 Nov 2017 21:45:35 -0500 Subject: [PATCH 01/21] Spell Cache WIP --- zone/CMakeLists.txt | 2 + zone/spell_cache.cpp | 113 +++++++++++++++++++++++++++++++++++++++++++ zone/spell_cache.h | 69 ++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 zone/spell_cache.cpp create mode 100644 zone/spell_cache.h diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 0171c8eb9..3971666b1 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -112,6 +112,7 @@ SET(zone_sources spawn2.h spawngroup.cpp special_attacks.cpp + spell_cache.cpp spell_effects.cpp spells.cpp tasks.cpp @@ -217,6 +218,7 @@ SET(zone_headers spawn2.cpp spawn2.h spawngroup.h + spell_cache.h string_ids.h tasks.h titles.h diff --git a/zone/spell_cache.cpp b/zone/spell_cache.cpp new file mode 100644 index 000000000..5291a7574 --- /dev/null +++ b/zone/spell_cache.cpp @@ -0,0 +1,113 @@ +#include "spell_cache.h" + +void SpellCache::InsertSpellEffect(int affect, int value, int subindex) +{ + sEffectCache *effect = nullptr; + + auto range = m_spelleffect.equal_range(affect); + auto effect_iter = range.first; + + while (effect_iter != range.second) { + if (effect_iter->second.base2 == subindex) + break; + ++effect_iter; + } + + if (effect_iter == range.second) {// we didn't find one + sEffectCache e{affect, value, subindex}; + m_spelleffect.emplace(affect, e); + return; + } + + // we gotta update + effect_iter->second.base1 = value; +} + +void SpellCache::InsertItemEffect(int affect, int value, int subindex) +{ + sEffectCache *effect = nullptr; + + auto range = m_itemeffect.equal_range(affect); + auto effect_iter = range.first; + + while (effect_iter != range.second) { + if (effect_iter->second.base2 == subindex) + break; + ++effect_iter; + } + + if (effect_iter == range.second) {// we didn't find one + sEffectCache e{affect, value, subindex}; + m_itemeffect.emplace(affect, e); + return; + } + + // we gotta update + effect_iter->second.base1 = value; +} + +void SpellCache::InsertAltEffect(int affect, int value, int subindex) +{ + sEffectCache *effect = nullptr; + + auto range = m_alteffect.equal_range(affect); + auto effect_iter = range.first; + + while (effect_iter != range.second) { + if (effect_iter->second.base2 == subindex) + break; + ++effect_iter; + } + + if (effect_iter == range.second) {// we didn't find one + sEffectCache e{affect, value, subindex}; + m_alteffect.emplace(affect, e); + return; + } + + // we gotta update + effect_iter->second.base1 = value; +} + +const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subindex) +{ + auto range = m_spelleffect.equal_range(affect); + auto effect_iter = range.first; + + while (effect_iter != range.second) { + if (effect_iter->second.base2 == subindex) + return &effect_iter->second; + ++effect_iter; + } + + return nullptr; +} + +const SpellCache::sEffectCache *SpellCache::GetItemCached(int affect, int subindex) +{ + auto range = m_itemeffect.equal_range(affect); + auto effect_iter = range.first; + + while (effect_iter != range.second) { + if (effect_iter->second.base2 == subindex) + return &effect_iter->second; + ++effect_iter; + } + + return nullptr; +} + +const SpellCache::sEffectCache *SpellCache::GetAltCached(int affect, int subindex) +{ + auto range = m_alteffect.equal_range(affect); + auto effect_iter = range.first; + + while (effect_iter != range.second) { + if (effect_iter->second.base2 == subindex) + return &effect_iter->second; + ++effect_iter; + } + + return nullptr; +} + diff --git a/zone/spell_cache.h b/zone/spell_cache.h new file mode 100644 index 000000000..a4753456a --- /dev/null +++ b/zone/spell_cache.h @@ -0,0 +1,69 @@ +#ifndef SPELL_CACHE_H +#define SPELL_CACHE_H + +#include + +class SpellCache +{ +public: + struct sEffectCache { + int affect; + int base1; + int base2; + }; + + SpellCache() : spell_cached(false), item_cached(false), alt_cached(false) {} + ~SpellCache() {} + + void InsertSpellEffect(int affect, int value, int subindex); + void InsertItemEffect(int affect, int value, int subindex); + void InsertAltEffect(int affect, int value, int subindex); + + inline void ClearSpellEffect() { m_spelleffect.clear(); } + inline void ClearItemEffect() { m_itemeffect.clear(); } + inline void ClearAltEffect() { m_alteffect.clear(); } + + inline void SetSpellCached(bool v) { spell_cached = v; } + inline void SetItemCached(bool v) { item_cached = v; } + inline void SetAltCached(bool v) { alt_cached = v; } + + inline bool IsSpellCached() { return spell_cached; } + inline bool IsItemCached() { return item_cached; } + inline bool IsAltCached() { return alt_cached; } + + const sEffectCache *GetSpellCached(int affect, int subindex = 0); + const sEffectCache *GetItemCached(int affect, int subindex = 0); + const sEffectCache *GetAltCached(int affect, int subindex = 0); + + // inlines for common operations + inline int GetCachedPlayerEffect(int affect, int subindex = 0) { + auto res = GetSpellCached(affect, subindex); + if (res) + return res->base1; + return 0; + } + + inline int GetCachedItemEffect(int affect, int subindex = 0) { + auto res = GetItemCached(affect, subindex); + if (res) + return res->base1; + return 0; + } + + inline int GetCachedAltEffect(int affect, int subindex = 0) { + auto res = GetAltCached(affect, subindex); + if (res) + return res->base1; + return 0; + } + +private: + bool spell_cached; + bool item_cached; + bool alt_cached; + std::multimap m_spelleffect; + std::multimap m_itemeffect; + std::multimap m_alteffect; +}; + +#endif /* !SPELL_CACHE_H */ From 4b44461c700c50f9fb324ec6486137aa86dbc24e Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 7 Nov 2017 21:46:17 -0500 Subject: [PATCH 02/21] Work on spell cache functions Implement Suppression Effects and start work on TotalEffects --- common/spdat.h | 9 ++- zone/client.h | 4 + zone/mob.h | 32 ++++++++ zone/npc.h | 4 + zone/spell_effects.cpp | 168 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 1 deletion(-) diff --git a/common/spdat.h b/common/spdat.h index 0f9e7a47a..6af297d50 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -213,6 +213,13 @@ typedef enum { DS_THORNS = 249 } DmgShieldType; +enum SuppressionMask { + SM_Buffs = 1, + SM_Items = 2, + SM_AAs = 4, + SM_All = 255 +}; + //Spell Effect IDs // https://forums.daybreakgames.com/eq/index.php?threads/enumerated-spa-list.206288/ // mirror: http://pastebin.com/MYeQqGwe @@ -532,7 +539,7 @@ typedef enum { #define SE_ForageAdditionalItems 313 // implemented[AA] - chance to forage additional items #define SE_Invisibility2 314 // implemented - fixed duration invisible #define SE_InvisVsUndead2 315 // implemented - fixed duration ITU -//#define SE_ImprovedInvisAnimals 316 // not used +#define SE_ImprovedInvisAnimals 316 // not used #define SE_ItemHPRegenCapIncrease 317 // implemented[AA] - increases amount of health regen gained via items #define SE_ItemManaRegenCapIncrease 318 // implemented - increases amount of mana regen you can gain via items #define SE_CriticalHealOverTime 319 // implemented diff --git a/zone/client.h b/zone/client.h index 88622fcff..11871ffc3 100644 --- a/zone/client.h +++ b/zone/client.h @@ -437,6 +437,10 @@ public: void EnableAreaRegens(int value); void DisableAreaRegens(); + // new spell cache stuff + virtual void RecacheItemEffects(); + virtual void RecacheSuppressionItems(); // work around for NPC invs being dumb + void ServerFilter(SetServerFilter_Struct* filter); void BulkSendTraderInventory(uint32 char_id); void SendSingleTraderItem(uint32 char_id, int uniqueid); diff --git a/zone/mob.h b/zone/mob.h index 058774c54..e96cc6223 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -25,6 +25,7 @@ #include "position.h" #include "aa_ability.h" #include "aa.h" +#include "spell_cache.h" #include "../common/light_source.h" #include "../common/emu_constants.h" #include @@ -518,6 +519,36 @@ public: inline const int8 GetFlyMode() const { return flymode; } bool IsBoat() const; + // Spell Cache stuff + int TotalEffect(int spaID, int subindex = 0, bool bIncludeItems = true, bool bIncludeAA = true, bool bIncludeBuffs = true); + // this one just skips special behavior, it's /not/ usable with SPAs with special behavior + int TotalEffectSimple(int spaID, int subindex = 0) { return GetCachedPlayerEffect(spaID, subindex) + GetCachedItemEffect(spaID, subindex) + GetCachedAltEffect(spaID, subindex); } + // returns the highest, also not usable with SPAs that need special behavior, but I guess this it's own special behavior + int BestEffect(int spaID, int subindex = 0) { return std::max(GetCachedPlayerEffect(spaID, subindex), std::max(GetCachedItemEffect(spaID, subindex), GetCachedAltEffect(spaID, subindex))); } + void RecacheSpellEffects(); + virtual void RecacheItemEffects() {} // virtual since mob/client have different inv structs + void RecacheAltEffects(); + void RecacheSuppressionSpells(); + virtual void RecacheSuppressionItems() {} // work around for invs being different for mobs/clients + inline int GetSuppresionSpellMask(int spaID) { + return GetCachedPlayerEffect(SE_NegateSpellEffect, spaID) | GetCachedItemEffect(SE_NegateSpellEffect, spaID) | GetCachedAltEffect(SE_NegateSpellEffect, spaID); + } + inline int GetCachedPlayerEffect(int spaID, int subindex = 0) { + if (!m_spell_cache.IsSpellCached()) + RecacheSpellEffects(); + return m_spell_cache.GetCachedPlayerEffect(spaID, subindex); + } + inline int GetCachedItemEffect(int spaID, int subindex = 0) { + if (!m_spell_cache.IsItemCached()) + RecacheItemEffects(); + return m_spell_cache.GetCachedItemEffect(spaID, subindex); + } + inline int GetCachedAltEffect(int spaID, int subindex = 0) { + if (!m_spell_cache.IsAltCached()) + RecacheAltEffects(); + return m_spell_cache.GetCachedAltEffect(spaID, subindex); + } + //Group virtual bool HasRaid() = 0; virtual bool HasGroup() = 0; @@ -1197,6 +1228,7 @@ protected: StatBonuses itembonuses; StatBonuses spellbonuses; StatBonuses aabonuses; + SpellCache m_spell_cache; uint16 petid; uint16 ownerid; PetType typeofpet; diff --git a/zone/npc.h b/zone/npc.h index c973eb79d..d3fd8f36d 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -158,6 +158,10 @@ public: virtual void InitializeBuffSlots(); virtual void UninitializeBuffSlots(); + // new spell cache stuff + virtual void RecacheItemEffects(); + virtual void RecacheSuppressionItems(); // work around for NPC invs being dumb + virtual void SetAttackTimer(); virtual void RangedAttack(Mob* other); virtual void ThrowingAttack(Mob* other) { } diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 9f78cb35d..96cee85de 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -6903,3 +6903,171 @@ void Client::BreakFeignDeathWhenCastOn(bool IsResisted) Message_StringID(MT_SpellFailure,FD_CAST_ON); } } + +/* This function will recache the buff, item, and AA SE_NegateSpellEffects + * This has to be done before the other effects are cached, since they make use of this data + * This function does not clear the cache, so we will have to do it before we call it + */ +void Mob::RecacheSuppressionSpells() +{ + // first we cache buffs + int buff_count = GetMaxTotalSlots(); + for (int i = buff_count; i < buff_count; ++i) { + if (!IsValidSpell(buffs[i].spellid)) + continue; + const auto &spell = spells[buffs[i].spellid]; + for (int j = 0; j < EFFECT_COUNT; ++j) { + if (spell.effectid[j] != SE_NegateSpellEffect) + continue; + auto base = spell.base[j] == 0 ? SM_All : spell.base[j]; + m_spell_cache.InsertSpellEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedPlayerEffect(SE_NegateSpellEffect, spell.base2[j]) | base, spell.base2[j]); + } + } + m_spell_cache.InsertSpellEffect(SE_NegateSpellEffect, SM_All, SE_NegateSpellEffect); // this will cause it to be ignored in recache + + // now we cache items -- NPC invs are dumb, so work around + RecacheSuppressionItems(); + + m_spell_cache.InsertItemEffect(SE_NegateSpellEffect, SM_All, SE_NegateSpellEffect); // this will cause it to be ignored in recache + // now we cache AAs + for (const auto &aa : aa_ranks) { + auto ability_rank = zone->GetAlternateAdvancementAbilityAndRank(aa.first, aa.second.first); + auto ability = ability_rank.first; + auto rank = ability_rank.second; + + if (!ability || rank->effects.empty()) + continue; + + for (const auto &e : rank->effects) { + if (e.effect_id != SE_NegateSpellEffect) + continue; + + auto base = e.base1 == 0 ? SM_All : e.base1; + m_spell_cache.InsertAltEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedAltEffect(SE_NegateSpellEffect, e.base2) | base, e.base2); + } + } + m_spell_cache.InsertAltEffect(SE_NegateSpellEffect, SM_All, SE_NegateSpellEffect); // this will cause it to be ignored in recache +} + +// NPCs and clients have incompatible inventory systems, so we work around that issue here +void NPC::RecacheSuppressionItems() +{ + // luckily we get to skip augs and shit here! and tributes! + for (int i = 0; i < EQEmu::legacy::EQUIPMENT_SIZE; ++i) { + const auto cur = database.GetItem(equipment[i]); + if (cur->Worn.Effect > 0 && cur->Worn.Type == EQEmu::item::ItemEffectWorn) { + if (!IsValidSpell(cur->Worn.Effect)) + continue; + const auto &spell = spells[cur->Worn.Effect]; + for (int j = 0; j < EFFECT_COUNT; ++j) { + if (spell.effectid[j] != SE_NegateSpellEffect) + continue; + auto base = spell.base[j] == 0 ? SM_All : spell.base[j]; + m_spell_cache.InsertItemEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedItemEffect(SE_NegateSpellEffect, spell.base2[j]) | base, spell.base2[j]); + } + } + } +} + +void Client::RecacheSuppressionItems() +{ + auto apply_effect = [this] (const EQEmu::ItemInstance *inst) { + const auto item = inst->GetItem(); + if (item->Worn.Effect > 0 && item->Worn.Type == EQEmu::item::ItemEffectWorn) { + if (!IsValidSpell(item->Worn.Effect)) + return; + const auto &spell = spells[item->Worn.Effect]; + for (int i = 0; i < EFFECT_COUNT; ++i) { + if (spell.effectid[i] != SE_NegateSpellEffect) + continue; + auto base = spell.base[i] == 0 ? SM_All : spell.base[i]; + m_spell_cache.InsertItemEffect(SE_NegateSpellEffect, m_spell_cache.GetCachedItemEffect(SE_NegateSpellEffect, spell.base2[i]) | base, spell.base2[i]); + } + } + }; + + // we don't do ammo slot here. The client also does not seem to be visiting augs + for (int i = EQEmu::inventory::slotCharm; i < EQEmu::inventory::slotAmmo; ++i) { + const auto inst = m_inv[i]; + if (inst == nullptr || !inst->IsClassCommon() || inst->GetExp() < 0) + continue; + apply_effect(inst); + } + + // powersource ... + if (ClientVersion() >= EQEmu::versions::ClientVersion::SoF) { + const auto inst = m_inv[EQEmu::inventory::slotPowerSource]; + if (inst) + apply_effect(inst); + } + + for (int i = 0; i < EQEmu::legacy::TRIBUTE_SIZE; ++i) { + const auto inst = m_inv[EQEmu::legacy::TRIBUTE_BEGIN + i]; + if (inst == nullptr || !inst->IsClassCommon() || inst->GetExp() < 0) + continue; + apply_effect(inst); + } +} + +int Mob::TotalEffect(int spaID, int subindex, bool bIncludeItems, bool bIncludeAA, bool bIncludeBuffs) +{ + // pure melee don't have mana regen + if ((spaID == SE_CurrentMana || spaID == SE_ManaRegen_v2) && + (GetClass() == MONK || GetClass() == ROGUE || GetClass() == WARRIOR || GetClass() == BERSERKER)) + return 0; + int total = 0, item_total = 0, aa_total = 0; + + // invis / see invis has special capping + switch (spaID) { + case SE_Invisibility: + case SE_SeeInvis: + case SE_InvisVsUndead: + case SE_InvisVsAnimals: + case SE_Invisibility2: + case SE_InvisVsUndead2: + case SE_ImprovedInvisAnimals: + if (bIncludeBuffs) + total = GetCachedPlayerEffect(spaID); + if (bIncludeItems) + item_total = GetCachedItemEffect(spaID); + if (bIncludeAA) + aa_total = GetCachedAltEffect(spaID); + return std::min(3000, std::max({total, item_total, aa_total})); + default: + break; + } + + // SE_MovementSpeed is only items or buffs (this is client behavior so useless for custom) + // and they don't stack, take best or worst if one is negative + if (spaID == SE_MovementSpeed) { + if (bIncludeBuffs) + total = GetCachedPlayerEffect(spaID); + if (bIncludeItems) + item_total = GetCachedItemEffect(spaID); + if (total < 0 || item_total < 0) + return std::min(total, item_total); + return std::max(total, item_total); + } + + // bunch of special behavior here! + if (spaID == SE_AttackSpeed) { + int haste = GetCachedPlayerEffect(SE_AttackSpeed); + int haste_v2 = GetCachedPlayerEffect(SE_AttackSpeed2); + int overhaste = GetCachedPlayerEffect(SE_AttackSpeed3); + int slow = GetCachedPlayerEffect(SE_AttackSpeed4); + + if (slow) { + ; + } + } + + if (bIncludeBuffs) + total = GetCachedPlayerEffect(spaID, subindex); + if (bIncludeAA) + total += GetCachedAltEffect(spaID, subindex); + if (bIncludeItems) // the client is using 0 for the mana regens in items, but subindex for others ... + total += + GetCachedItemEffect(spaID, (spaID == SE_CurrentMana || spaID == SE_ManaRegen_v2) ? 0 : subindex); + return total; +} + From 2176eb5e8e20970b7c3fd16e6e0c1e11a81ea6cf Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Wed, 8 Nov 2017 13:15:37 -0500 Subject: [PATCH 03/21] Remove some unneeded code --- zone/spell_cache.cpp | 6 ------ zone/spell_cache.h | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/zone/spell_cache.cpp b/zone/spell_cache.cpp index 5291a7574..bcf463c61 100644 --- a/zone/spell_cache.cpp +++ b/zone/spell_cache.cpp @@ -2,8 +2,6 @@ void SpellCache::InsertSpellEffect(int affect, int value, int subindex) { - sEffectCache *effect = nullptr; - auto range = m_spelleffect.equal_range(affect); auto effect_iter = range.first; @@ -25,8 +23,6 @@ void SpellCache::InsertSpellEffect(int affect, int value, int subindex) void SpellCache::InsertItemEffect(int affect, int value, int subindex) { - sEffectCache *effect = nullptr; - auto range = m_itemeffect.equal_range(affect); auto effect_iter = range.first; @@ -48,8 +44,6 @@ void SpellCache::InsertItemEffect(int affect, int value, int subindex) void SpellCache::InsertAltEffect(int affect, int value, int subindex) { - sEffectCache *effect = nullptr; - auto range = m_alteffect.equal_range(affect); auto effect_iter = range.first; diff --git a/zone/spell_cache.h b/zone/spell_cache.h index a4753456a..1e0728ced 100644 --- a/zone/spell_cache.h +++ b/zone/spell_cache.h @@ -61,6 +61,7 @@ private: bool spell_cached; bool item_cached; bool alt_cached; + // maybe use unordered_multimap std::multimap m_spelleffect; std::multimap m_itemeffect; std::multimap m_alteffect; From 61241e32458cdcbe73e7014ba307002cd86c41c6 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Thu, 9 Nov 2017 13:30:11 -0500 Subject: [PATCH 04/21] More work on gross haste special case --- zone/spell_effects.cpp | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 96cee85de..d1f2f27bf 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -7055,9 +7055,51 @@ int Mob::TotalEffect(int spaID, int subindex, bool bIncludeItems, bool bIncludeA int haste_v2 = GetCachedPlayerEffect(SE_AttackSpeed2); int overhaste = GetCachedPlayerEffect(SE_AttackSpeed3); int slow = GetCachedPlayerEffect(SE_AttackSpeed4); + int item_haste = 0; /* bIncludeItems ? item_bonuses.Haste : 0; //or something */ - if (slow) { - ; + if (haste) { // we have a non-zero haste + if (haste < 100) { // we're slowed! special case for SE_AttackSpeed4 + int temp = 100 - slow; + if (haste > temp) // SE_AttackSpeed4 is hurting us more, so use that + haste = temp; + } else { // SE_AttackSpeed4 will lower our SE_AttackSpeed + haste -= slow; + } + } else { + haste = 100 - slow; + } + if (haste <= 0) + haste = 1; + // if we have no attack speed buffs, haste will be 100, if it's less than 100 we abort adn we are done :P + if (haste >= 100) { + total = haste + item_haste; + if (overhaste > 0) { + if (overhaste > 100) + overhaste -= 100; + if (GetLevel() <= 50) + overhaste = std::min(10, overhaste); + else + overhaste = std::min(25, overhaste); + } + + // ItemHaste function returns value + 100 + if (item_haste >= 100) + total -= 100; + + if (haste_v2 > 100 && GetLevel() > 49) + total += std::min(haste_v2, 110) - 100; + + int haste_cap = 0; + if (IsClient()) { + if (GetLevel() > 59) + haste_cap = 200; + else if (GetLevel() > 50) + haste_cap = 185; + else + haste_cap = GetLevel() + 125; + } else { + // Pet/NPC caps + } } } From ec9c7baef74fd3a6c97b96b2a3098015876cf9da Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Thu, 9 Nov 2017 22:34:10 -0500 Subject: [PATCH 05/21] Rest of haste stuff This is still untested --- zone/spell_effects.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index d1f2f27bf..dd1a9cda8 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -7097,9 +7097,29 @@ int Mob::TotalEffect(int spaID, int subindex, bool bIncludeItems, bool bIncludeA haste_cap = 185; else haste_cap = GetLevel() + 125; - } else { - // Pet/NPC caps + } else { // Pets/NPC + if (IsPetOwnerClient()) { // client checks the bSummoned flag, which seems true for PC summoned pets/swarm pets + haste_cap = GetLevel() + 110; + Mob *owner = nullptr; + if (IsPet()) + owner = GetOwner(); + else if (IsNPC() && CastToNPC()->GetSwarmOwner()) + owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); + if (owner) + haste_cap += std::max(0, owner->GetLevel() - 39) + std::max(0, owner->GetLevel() - 60); + } else { + haste_cap = 250; // normal NPCs + } } + + haste_cap += overhaste; + + if (total) + total += overhaste; + else if (overhaste) + total = overhaste + 100; + + return std::min(total, haste_cap); } } From 7d79d2fc4aeb3a4a24eb084d39d7360555fac2e6 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Sat, 11 Nov 2017 14:09:20 -0500 Subject: [PATCH 06/21] More work (mostly AAs) --- common/ruletypes.h | 1 + zone/aa.cpp | 4 +++ zone/bonuses.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++++-- zone/mob.cpp | 3 +- zone/mob.h | 4 +-- 5 files changed, 81 insertions(+), 6 deletions(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index c3a8fd5dc..5cf79a4f5 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -405,6 +405,7 @@ RULE_BOOL(Spells, IgnoreSpellDmgLvlRestriction, false) // ignore the 5 level spr RULE_BOOL(Spells, AllowItemTGB, false) // TGB doesn't work with items on live, custom servers want it though RULE_BOOL(Spells, NPCInnateProcOverride, true) // NPC innate procs override the target type to single target. RULE_BOOL(Spells, OldRainTargets, false) // use old incorrectly implemented max targets for rains +RULE_BOOL(Spells, Oct182017AAResistOtherSPAFix, false) // they were not saving the base2 of AAs before this patch RULE_CATEGORY_END() RULE_CATEGORY(Combat) diff --git a/zone/aa.cpp b/zone/aa.cpp index 31ad9c162..97e57dad7 100644 --- a/zone/aa.cpp +++ b/zone/aa.cpp @@ -485,6 +485,8 @@ void Client::ResetAA() { m_pp.group_leadership_exp = 0; m_pp.raid_leadership_exp = 0; + m_spell_cache.SetAltCached(false); + database.DeleteCharacterLeadershipAAs(CharacterID()); // undefined for these clients if (ClientVersionBit() & EQEmu::versions::bit_TitaniumAndEarlier) @@ -800,6 +802,7 @@ void Client::RefundAA() { Save(); } + m_spell_cache.SetAltCached(false); SendAlternateAdvancementTable(); SendAlternateAdvancementPoints(); SendAlternateAdvancementStats(); @@ -1114,6 +1117,7 @@ void Client::FinishAlternateAdvancementPurchase(AA::Rank *rank, bool ignore_cost } } + m_spell_cache.SetAltCached(false); CalcBonuses(); if(cost > 0) { diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index c444ff9dd..c6c4965e1 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -628,7 +628,9 @@ void Client::CalcEdibleBonuses(StatBonuses* newbon) { void Mob::CalcAABonuses(StatBonuses *newbon) { - memset(newbon, 0, sizeof(StatBonuses)); // start fresh + m_spell_cache.ClearAltEffect(); + + RecacheSuppressionSpells(); // we gotta do this :P for (const auto &aa : aa_ranks) { auto ability_rank = zone->GetAlternateAdvancementAbilityAndRank(aa.first, aa.second.first); @@ -645,6 +647,8 @@ void Mob::CalcAABonuses(StatBonuses *newbon) ApplyAABonuses(*rank, newbon); } + + m_spell_cache.SetAltCached(true); } //A lot of the normal spell functions (IsBlankSpellEffect, etc) are set for just spells (in common/spdat.h). @@ -663,14 +667,14 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) for (const auto &e : rank.effects) { effect = e.effect_id; base1 = e.base1; - base2 = e.base2; + base2 = 0; // only some AAs use the base2 slot = e.slot; // we default to 0 (SE_CurrentHP) for the effect, so if there aren't any base1/2 values, we'll just skip it if (effect == 0 && base1 == 0 && base2 == 0) continue; - // IsBlankSpellEffect() + // IsBlankSpellEffect() -- these shouldn't be in AAs if (effect == SE_Blank || (effect == SE_CHA && base1 == 0) || effect == SE_StackingCommand_Block || effect == SE_StackingCommand_Overwrite) continue; @@ -678,12 +682,77 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) Log(Logs::Detail, Logs::AA, "Applying Effect %d from AA %u in slot %d (base1: %d, base2: %d) on %s", effect, rank.id, slot, base1, base2, GetCleanName()); + // this should be removed when rewritten uint8 focus = IsFocusEffect(0, 0, true, effect); if (focus) { newbon->FocusEffects[focus] = static_cast(effect); continue; } + // SE_NegateSpellEffects + if (!((effect | GetSuppresionSpellMask(effect)) & SM_AAs)) + continue; + + // Okay, effects that use base2 + switch (effect) { + case SE_SpellEffectResistChance: + if (!RuleB(Spells, Oct182017AAResistOtherSPAFix)) // this is broken before this patch + break; + case SE_Levitate: + case SE_CriticalHitChance: + case SE_MeleeSkillCheck: + case SE_HitChance: + case SE_DamageModifier: + case SE_MinDamageModifier: + case SE_SkillDamageTaken: + case SE_Accuracy: + case SE_SkillDamageAmount: + case SE_GiveDoubleRiposte: + case SE_ReduceSkillTimer: + case SE_RaiseSkillCap: + case SE_AddSingingMod: + case SE_RaiseStatCap: + case SE_HastenedAASkill: + case SE_AddPetCommand: + case SE_ReduceTradeskillFail: + case SE_CriticalDamageMob: + case SE_SkillDamageAmount2: + base2 = e.base2; + break; + default: + break; + } + + // these don't stack and some treat base2 differently and don't stack + if (effect == SE_MasteryofPast || effect == SE_Assassinate || effect == SE_AssassinateLevel || + effect == SE_HeadShot || effect == SE_HeadShotLevel || effect == SE_FinishingBlowLvl || + effect == SE_FinishingBlow) { + if (base1 > m_spell_cache.GetCachedAltEffect(effect)) + m_spell_cache.InsertAltEffect(effect, base1, 0); + } else { // stacking! + m_spell_cache.InsertAltEffect(effect, base1 + m_spell_cache.GetCachedAltEffect(effect), base2); + } + + // RoF2 doesn't have SE_CriticalSpellChance here, but our AA data looks like it's processed like this + if (effect == SE_Assassinate || effect == SE_HeadShot || effect == SE_FinishingBlowLvl || + effect == SE_FinishingBlow || effect == SE_AssassinateLevel || effect == SE_HeadShotLevel || + effect == SE_CriticalSpellChance) { + base2 = e.base2; // uses base2 for fun times! + if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) + m_spell_cache.InsertAltEffect(effect, base2, 1); + } + /* TODO: Other SPAs that need special handling + * SE_ProcOnKillShot + * SE_SpellOnDeath + * SE_SkillAttackProc + * SE_SlayUndead + * SE_DivineSave + * SE_FrenziedDevastation + * SE_SkillProc + * SE_SkillProcSuccess + * SE_PC_Pet_Rampage + */ + switch (effect) { case SE_ACv2: case SE_ArmorClass: diff --git a/zone/mob.cpp b/zone/mob.cpp index 66e221b0b..a6ad022bc 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -116,7 +116,8 @@ Mob::Mob(const char* in_name, fix_z_timer(300), fix_z_timer_engaged(100), attack_anim_timer(1000), - position_update_melee_push_timer(1000) + position_update_melee_push_timer(1000), + bItemDirty(false) { targeted = 0; tar_ndx=0; diff --git a/zone/mob.h b/zone/mob.h index e96cc6223..d1d31f81d 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -527,7 +527,6 @@ public: int BestEffect(int spaID, int subindex = 0) { return std::max(GetCachedPlayerEffect(spaID, subindex), std::max(GetCachedItemEffect(spaID, subindex), GetCachedAltEffect(spaID, subindex))); } void RecacheSpellEffects(); virtual void RecacheItemEffects() {} // virtual since mob/client have different inv structs - void RecacheAltEffects(); void RecacheSuppressionSpells(); virtual void RecacheSuppressionItems() {} // work around for invs being different for mobs/clients inline int GetSuppresionSpellMask(int spaID) { @@ -545,7 +544,7 @@ public: } inline int GetCachedAltEffect(int spaID, int subindex = 0) { if (!m_spell_cache.IsAltCached()) - RecacheAltEffects(); + CalcAABonuses(&aabonuses); return m_spell_cache.GetCachedAltEffect(spaID, subindex); } @@ -1225,6 +1224,7 @@ protected: uint32 scalerate; Buffs_Struct *buffs; uint32 current_buff_count; + bool bItemDirty; StatBonuses itembonuses; StatBonuses spellbonuses; StatBonuses aabonuses; From b609f9363b27d6537fe815542562a584563141ff Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Sat, 11 Nov 2017 22:25:58 -0500 Subject: [PATCH 07/21] Add handling for SE_SkillAttackProc --- zone/attack.cpp | 14 ++++++++------ zone/bonuses.cpp | 10 ++++++++++ zone/spell_cache.cpp | 5 +++++ zone/spell_cache.h | 22 +++++++++++++++++++++- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index d12d8d36c..f9702986d 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -1477,12 +1477,14 @@ bool Client::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, b /////////////////////////////////////////////////////////// ////// Send Attack Damage /////////////////////////////////////////////////////////// - if (my_hit.damage_done > 0 && aabonuses.SkillAttackProc[0] && aabonuses.SkillAttackProc[1] == my_hit.skill && - IsValidSpell(aabonuses.SkillAttackProc[2])) { - float chance = aabonuses.SkillAttackProc[0] / 1000.0f; - if (zone->random.Roll(chance)) - SpellFinished(aabonuses.SkillAttackProc[2], other, EQEmu::CastingSlot::Item, 0, -1, - spells[aabonuses.SkillAttackProc[2]].ResistDiff); + if (my_hit.damage_done > 0 && m_spell_cache.HasSkillProcs()) { + auto end = m_spell_cache.skill_proc_end(); + for (auto it = m_spell_cache.skill_proc_begin(); it != end; ++it) { + if (it->skill == my_hit.skill && IsValidSpell(it->spell)) { + if (zone->random.Roll(it->chance / 1000.0f)) + SpellFinished(it->spell, other, EQEmu::CastingSlot::Item, 0, -1, spells[it->spell].ResistDiff); + } + } } other->Damage(this, my_hit.damage_done, SPELL_UNKNOWN, my_hit.skill, true, -1, false, m_specialattacks); diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index c6c4965e1..cdbd64969 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -753,6 +753,16 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) * SE_PC_Pet_Rampage */ + // special handling for AAs that live doesn't appear to do + // Alternative to this would be to just iterate over the AA list + switch (effect) { + case SE_SkillAttackProc: + m_spell_cache.InsertSkillProc(base1, e.base2, rank.spell); + break; + default: + break; + } + switch (effect) { case SE_ACv2: case SE_ArmorClass: diff --git a/zone/spell_cache.cpp b/zone/spell_cache.cpp index bcf463c61..deceedd30 100644 --- a/zone/spell_cache.cpp +++ b/zone/spell_cache.cpp @@ -63,6 +63,11 @@ void SpellCache::InsertAltEffect(int affect, int value, int subindex) effect_iter->second.base1 = value; } +void SpellCache::InsertSkillProc(int chance, int skill, int spell) +{ + m_skill_proc.push_back({chance, skill, spell}); +} + const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subindex) { auto range = m_spelleffect.equal_range(affect); diff --git a/zone/spell_cache.h b/zone/spell_cache.h index 1e0728ced..0231467fe 100644 --- a/zone/spell_cache.h +++ b/zone/spell_cache.h @@ -2,6 +2,7 @@ #define SPELL_CACHE_H #include +#include class SpellCache { @@ -12,16 +13,26 @@ public: int base2; }; + // can't really get away with doing a cache trick with this since you can have multiple + // (see Decapitation) + // This is for SE_SkillAttackProc (SPA 288) only, which is purely AA + struct sSkillProc { + int chance; + int skill; + int spell; + }; + SpellCache() : spell_cached(false), item_cached(false), alt_cached(false) {} ~SpellCache() {} void InsertSpellEffect(int affect, int value, int subindex); void InsertItemEffect(int affect, int value, int subindex); void InsertAltEffect(int affect, int value, int subindex); + void InsertSkillProc(int chace, int skill, int spell); inline void ClearSpellEffect() { m_spelleffect.clear(); } inline void ClearItemEffect() { m_itemeffect.clear(); } - inline void ClearAltEffect() { m_alteffect.clear(); } + inline void ClearAltEffect() { m_alteffect.clear(); m_skill_proc.clear(); } inline void SetSpellCached(bool v) { spell_cached = v; } inline void SetItemCached(bool v) { item_cached = v; } @@ -30,11 +41,15 @@ public: inline bool IsSpellCached() { return spell_cached; } inline bool IsItemCached() { return item_cached; } inline bool IsAltCached() { return alt_cached; } + inline bool HasSkillProcs() { return !m_skill_proc.empty(); } const sEffectCache *GetSpellCached(int affect, int subindex = 0); const sEffectCache *GetItemCached(int affect, int subindex = 0); const sEffectCache *GetAltCached(int affect, int subindex = 0); + std::vector::const_iterator skill_proc_begin() { return m_skill_proc.cbegin(); } + std::vector::const_iterator skill_proc_end() { return m_skill_proc.cend(); } + // inlines for common operations inline int GetCachedPlayerEffect(int affect, int subindex = 0) { auto res = GetSpellCached(affect, subindex); @@ -65,6 +80,11 @@ private: std::multimap m_spelleffect; std::multimap m_itemeffect; std::multimap m_alteffect; + + // this should be fine for how it works with live AAs + // if custom servers want to give someone a million of these ahhh + // we need something better :P + std::vector m_skill_proc; }; #endif /* !SPELL_CACHE_H */ From b424fc9815333cbec12c54f20ccfa888947dcd6f Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Sun, 12 Nov 2017 13:30:12 -0500 Subject: [PATCH 08/21] Fix AA SE_NegateSpellEffects --- zone/bonuses.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index cdbd64969..c0c40f430 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -690,7 +690,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) } // SE_NegateSpellEffects - if (!((effect | GetSuppresionSpellMask(effect)) & SM_AAs)) + if (!(GetSuppresionSpellMask(effect) & SM_AAs)) continue; // Okay, effects that use base2 From 406cb825c339efb8deef940307401f852390899a Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Mon, 13 Nov 2017 00:20:02 -0500 Subject: [PATCH 09/21] Rename SE_SkillAttackProc to match our name --- zone/attack.cpp | 6 +++--- zone/bonuses.cpp | 2 +- zone/spell_cache.cpp | 4 ++-- zone/spell_cache.h | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index f9702986d..3f182daab 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -1477,9 +1477,9 @@ bool Client::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, b /////////////////////////////////////////////////////////// ////// Send Attack Damage /////////////////////////////////////////////////////////// - if (my_hit.damage_done > 0 && m_spell_cache.HasSkillProcs()) { - auto end = m_spell_cache.skill_proc_end(); - for (auto it = m_spell_cache.skill_proc_begin(); it != end; ++it) { + if (my_hit.damage_done > 0 && m_spell_cache.HasSkillAttackProcs()) { + auto end = m_spell_cache.skill_attack_proc_end(); + for (auto it = m_spell_cache.skill_attack_proc_begin(); it != end; ++it) { if (it->skill == my_hit.skill && IsValidSpell(it->spell)) { if (zone->random.Roll(it->chance / 1000.0f)) SpellFinished(it->spell, other, EQEmu::CastingSlot::Item, 0, -1, spells[it->spell].ResistDiff); diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index c0c40f430..cfaf1bbc0 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -757,7 +757,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) // Alternative to this would be to just iterate over the AA list switch (effect) { case SE_SkillAttackProc: - m_spell_cache.InsertSkillProc(base1, e.base2, rank.spell); + m_spell_cache.InsertSkillAttackProc(base1, e.base2, rank.spell); break; default: break; diff --git a/zone/spell_cache.cpp b/zone/spell_cache.cpp index deceedd30..acff2eadd 100644 --- a/zone/spell_cache.cpp +++ b/zone/spell_cache.cpp @@ -63,9 +63,9 @@ void SpellCache::InsertAltEffect(int affect, int value, int subindex) effect_iter->second.base1 = value; } -void SpellCache::InsertSkillProc(int chance, int skill, int spell) +void SpellCache::InsertSkillAttackProc(int chance, int skill, int spell) { - m_skill_proc.push_back({chance, skill, spell}); + m_skill_attack_proc.push_back({chance, skill, spell}); } const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subindex) diff --git a/zone/spell_cache.h b/zone/spell_cache.h index 0231467fe..4274a6072 100644 --- a/zone/spell_cache.h +++ b/zone/spell_cache.h @@ -16,7 +16,7 @@ public: // can't really get away with doing a cache trick with this since you can have multiple // (see Decapitation) // This is for SE_SkillAttackProc (SPA 288) only, which is purely AA - struct sSkillProc { + struct sSkillAttackProc { int chance; int skill; int spell; @@ -28,11 +28,11 @@ public: void InsertSpellEffect(int affect, int value, int subindex); void InsertItemEffect(int affect, int value, int subindex); void InsertAltEffect(int affect, int value, int subindex); - void InsertSkillProc(int chace, int skill, int spell); + void InsertSkillAttackProc(int chace, int skill, int spell); inline void ClearSpellEffect() { m_spelleffect.clear(); } inline void ClearItemEffect() { m_itemeffect.clear(); } - inline void ClearAltEffect() { m_alteffect.clear(); m_skill_proc.clear(); } + inline void ClearAltEffect() { m_alteffect.clear(); m_skill_attack_proc.clear(); } inline void SetSpellCached(bool v) { spell_cached = v; } inline void SetItemCached(bool v) { item_cached = v; } @@ -41,14 +41,14 @@ public: inline bool IsSpellCached() { return spell_cached; } inline bool IsItemCached() { return item_cached; } inline bool IsAltCached() { return alt_cached; } - inline bool HasSkillProcs() { return !m_skill_proc.empty(); } + inline bool HasSkillAttackProcs() { return !m_skill_attack_proc.empty(); } const sEffectCache *GetSpellCached(int affect, int subindex = 0); const sEffectCache *GetItemCached(int affect, int subindex = 0); const sEffectCache *GetAltCached(int affect, int subindex = 0); - std::vector::const_iterator skill_proc_begin() { return m_skill_proc.cbegin(); } - std::vector::const_iterator skill_proc_end() { return m_skill_proc.cend(); } + std::vector::const_iterator skill_attack_proc_begin() { return m_skill_attack_proc.cbegin(); } + std::vector::const_iterator skill_attack_proc_end() { return m_skill_attack_proc.cend(); } // inlines for common operations inline int GetCachedPlayerEffect(int affect, int subindex = 0) { @@ -84,7 +84,7 @@ private: // this should be fine for how it works with live AAs // if custom servers want to give someone a million of these ahhh // we need something better :P - std::vector m_skill_proc; + std::vector m_skill_attack_proc; }; #endif /* !SPELL_CACHE_H */ From e0b2867e8bb71e4ee551e281751aa1833c98d687 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Mon, 13 Nov 2017 22:54:06 -0500 Subject: [PATCH 10/21] Add caching for SE_SkillProc and SE_SkillProcSuccess --- zone/attack.cpp | 189 +++++++++---------------------------------- zone/bonuses.cpp | 58 ++++++++----- zone/spell_cache.cpp | 121 ++++++++++++++++++++++++++- zone/spell_cache.h | 70 ++++++++++++---- 4 files changed, 248 insertions(+), 190 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index 3f182daab..dc550c702 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -4646,177 +4646,64 @@ void Mob::ApplyDamageTable(DamageHitInfo &hit) void Mob::TrySkillProc(Mob *on, uint16 skill, uint16 ReuseTime, bool Success, uint16 hand, bool IsDefensive) { - if (!on) { SetTarget(nullptr); Log(Logs::General, Logs::Error, "A null Mob object was passed to Mob::TrySkillProc for evaluation!"); return; } - if (!spellbonuses.LimitToSkill[skill] && !itembonuses.LimitToSkill[skill] && !aabonuses.LimitToSkill[skill]) - return; - /*Allow one proc from each (Spell/Item/AA) - Kayen: Due to limited avialability of effects on live it is too difficult - to confirm how they stack at this time, will adjust formula when more data is avialablle to test.*/ - bool CanProc = true; + Kayen: Due to limited availability of effects on live it is too difficult + to confirm how they stack at this time, will adjust formula when more data is available to test.*/ - uint16 base_spell_id = 0; - uint16 proc_spell_id = 0; - float ProcMod = 0; - float chance = 0; + float ProcMod = 0.0f; + float chance = 0.0f; if (IsDefensive) chance = on->GetSkillProcChances(ReuseTime, hand); else chance = GetSkillProcChances(ReuseTime, hand); - if (spellbonuses.LimitToSkill[skill]) { + std::vector::const_iterator it, end; + auto type = SpellCache::eSkillProc::Buff; + int i = 0; - for (int e = 0; e < MAX_SKILL_PROCS; e++) { - if (CanProc && - ((!Success && spellbonuses.SkillProc[e] && IsValidSpell(spellbonuses.SkillProc[e])) - || (Success && spellbonuses.SkillProcSuccess[e] && IsValidSpell(spellbonuses.SkillProcSuccess[e])))) { + while (i < 3) { + if (Success) { + it = m_spell_cache.skill_proc_success_begin(type); + end = m_spell_cache.skill_proc_success_end(type); + } else { + it = m_spell_cache.skill_proc_attempt_begin(type); + end = m_spell_cache.skill_proc_attempt_end(type); + } - if (Success) - base_spell_id = spellbonuses.SkillProcSuccess[e]; - else - base_spell_id = spellbonuses.SkillProc[e]; - - proc_spell_id = 0; - ProcMod = 0; - - for (int i = 0; i < EFFECT_COUNT; i++) { - - if (spells[base_spell_id].effectid[i] == SE_SkillProc || spells[base_spell_id].effectid[i] == SE_SkillProcSuccess) { - proc_spell_id = spells[base_spell_id].base[i]; - ProcMod = static_cast(spells[base_spell_id].base2[i]); - } - - else if (spells[base_spell_id].effectid[i] == SE_LimitToSkill && spells[base_spell_id].base[i] <= EQEmu::skills::HIGHEST_SKILL) { - - if (CanProc && spells[base_spell_id].base[i] == skill && IsValidSpell(proc_spell_id)) { - float final_chance = chance * (ProcMod / 100.0f); - if (zone->random.Roll(final_chance)) { - ExecWeaponProc(nullptr, proc_spell_id, on); - CheckNumHitsRemaining(NumHit::OffensiveSpellProcs, 0, - base_spell_id); - CanProc = false; - break; - } - } - } - else { - //Reset and check for proc in sequence - proc_spell_id = 0; - ProcMod = 0; - } + for (; it != end; ++it) { + // abilities that have no skill limits can always proc? (see Lingering Death, Shrouding Speed Discipline) + if ((it->skills.empty() || + it->skills.count(static_cast(skill)) != 0) && + IsValidSpell(it->spell)) { + ProcMod = static_cast(it->chance); + float final_chance = chance * (ProcMod / 100.0f); + if (zone->random.Roll(final_chance)) { + ExecWeaponProc(nullptr, it->spell, on); + if (type == SpellCache::eSkillProc::Buff) + CheckNumHitsRemaining(NumHit::OffensiveSpellProcs, 0, + buffs[it->slot].spellid); + return; // do we want to allow others to have a chance? } } } - } - if (itembonuses.LimitToSkill[skill]) { - CanProc = true; - for (int e = 0; e < MAX_SKILL_PROCS; e++) { - if (CanProc && - ((!Success && itembonuses.SkillProc[e] && IsValidSpell(itembonuses.SkillProc[e])) - || (Success && itembonuses.SkillProcSuccess[e] && IsValidSpell(itembonuses.SkillProcSuccess[e])))) { - - if (Success) - base_spell_id = itembonuses.SkillProcSuccess[e]; - else - base_spell_id = itembonuses.SkillProc[e]; - - proc_spell_id = 0; - ProcMod = 0; - - for (int i = 0; i < EFFECT_COUNT; i++) { - if (spells[base_spell_id].effectid[i] == SE_SkillProc || spells[base_spell_id].effectid[i] == SE_SkillProcSuccess) { - proc_spell_id = spells[base_spell_id].base[i]; - ProcMod = static_cast(spells[base_spell_id].base2[i]); - } - - else if (spells[base_spell_id].effectid[i] == SE_LimitToSkill && spells[base_spell_id].base[i] <= EQEmu::skills::HIGHEST_SKILL) { - - if (CanProc && spells[base_spell_id].base[i] == skill && IsValidSpell(proc_spell_id)) { - float final_chance = chance * (ProcMod / 100.0f); - if (zone->random.Roll(final_chance)) { - ExecWeaponProc(nullptr, proc_spell_id, on); - CanProc = false; - break; - } - } - } - else { - proc_spell_id = 0; - ProcMod = 0; - } - } - } - } - } - - if (IsClient() && aabonuses.LimitToSkill[skill]) { - - CanProc = true; - uint32 effect_id = 0; - int32 base1 = 0; - int32 base2 = 0; - uint32 slot = 0; - - for (int e = 0; e < MAX_SKILL_PROCS; e++) { - if (CanProc && - ((!Success && aabonuses.SkillProc[e]) - || (Success && aabonuses.SkillProcSuccess[e]))) { - int aaid = 0; - - if (Success) - base_spell_id = aabonuses.SkillProcSuccess[e]; - else - base_spell_id = aabonuses.SkillProc[e]; - - proc_spell_id = 0; - ProcMod = 0; - - for (auto &rank_info : aa_ranks) { - auto ability_rank = zone->GetAlternateAdvancementAbilityAndRank(rank_info.first, rank_info.second.first); - auto ability = ability_rank.first; - auto rank = ability_rank.second; - - if (!ability) { - continue; - } - - for (auto &effect : rank->effects) { - effect_id = effect.effect_id; - base1 = effect.base1; - base2 = effect.base2; - slot = effect.slot; - - if (effect_id == SE_SkillProc || effect_id == SE_SkillProcSuccess) { - proc_spell_id = base1; - ProcMod = static_cast(base2); - } - else if (effect_id == SE_LimitToSkill && base1 <= EQEmu::skills::HIGHEST_SKILL) { - - if (CanProc && base1 == skill && IsValidSpell(proc_spell_id)) { - float final_chance = chance * (ProcMod / 100.0f); - - if (zone->random.Roll(final_chance)) { - ExecWeaponProc(nullptr, proc_spell_id, on); - CanProc = false; - break; - } - } - } - else { - proc_spell_id = 0; - ProcMod = 0; - } - } - } - } + ++i; + switch (i) { + case 1: + type = SpellCache::eSkillProc::Worn; + break; + case 2: + type = SpellCache::eSkillProc::AA; + break; + default: + break; } } } diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index cfaf1bbc0..c378b8d49 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -664,6 +664,8 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) int32 base2 = 0; // only really used for SE_RaiseStatCap & SE_ReduceSkillTimer in aa_effects table uint32 slot = 0; + int last_skill_proc = 0; // some book keeping for SE_SkillProc and SE_SkillProcSuccess for SE_LimitToSkill + for (const auto &e : rank.effects) { effect = e.effect_id; base1 = e.base1; @@ -693,6 +695,40 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) if (!(GetSuppresionSpellMask(effect) & SM_AAs)) continue; + /* TODO: Other SPAs that need special handling + * SE_ProcOnKillShot + * SE_SpellOnDeath + * SE_SkillAttackProc + * SE_SlayUndead + * SE_DivineSave + * SE_FrenziedDevastation + * SE_SkillProc + * SE_SkillProcSuccess + * SE_PC_Pet_Rampage + */ + + // special handling for AAs that live doesn't appear to do + // Alternative to this would be to just iterate over the AA list + // each of these are special and we skip below + switch (effect) { + case SE_SkillAttackProc: + m_spell_cache.InsertSkillAttackProc(base1, e.base2, rank.spell); + continue; + case SE_SkillProc: + last_skill_proc = effect; + m_spell_cache.InsertSkillProcAttempt(SpellCache::eSkillProc::AA, base1, e.base2); + continue; + case SE_SkillProcSuccess: + last_skill_proc = effect; + m_spell_cache.InsertSkillProcSuccess(SpellCache::eSkillProc::AA, base1, e.base2); + continue; + case SE_LimitToSkill: + m_spell_cache.InsertSkillLimit(SpellCache::eSkillProc::AA, static_cast(base1), last_skill_proc == SE_SkillProcSuccess); + continue; + default: + break; + } + // Okay, effects that use base2 switch (effect) { case SE_SpellEffectResistChance: @@ -741,28 +777,8 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); } - /* TODO: Other SPAs that need special handling - * SE_ProcOnKillShot - * SE_SpellOnDeath - * SE_SkillAttackProc - * SE_SlayUndead - * SE_DivineSave - * SE_FrenziedDevastation - * SE_SkillProc - * SE_SkillProcSuccess - * SE_PC_Pet_Rampage - */ - - // special handling for AAs that live doesn't appear to do - // Alternative to this would be to just iterate over the AA list - switch (effect) { - case SE_SkillAttackProc: - m_spell_cache.InsertSkillAttackProc(base1, e.base2, rank.spell); - break; - default: - break; - } + // TODO remove switch (effect) { case SE_ACv2: case SE_ArmorClass: diff --git a/zone/spell_cache.cpp b/zone/spell_cache.cpp index acff2eadd..3d6bfce40 100644 --- a/zone/spell_cache.cpp +++ b/zone/spell_cache.cpp @@ -68,7 +68,7 @@ void SpellCache::InsertSkillAttackProc(int chance, int skill, int spell) m_skill_attack_proc.push_back({chance, skill, spell}); } -const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subindex) +const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subindex) const { auto range = m_spelleffect.equal_range(affect); auto effect_iter = range.first; @@ -82,7 +82,7 @@ const SpellCache::sEffectCache *SpellCache::GetSpellCached(int affect, int subin return nullptr; } -const SpellCache::sEffectCache *SpellCache::GetItemCached(int affect, int subindex) +const SpellCache::sEffectCache *SpellCache::GetItemCached(int affect, int subindex) const { auto range = m_itemeffect.equal_range(affect); auto effect_iter = range.first; @@ -96,7 +96,7 @@ const SpellCache::sEffectCache *SpellCache::GetItemCached(int affect, int subind return nullptr; } -const SpellCache::sEffectCache *SpellCache::GetAltCached(int affect, int subindex) +const SpellCache::sEffectCache *SpellCache::GetAltCached(int affect, int subindex) const { auto range = m_alteffect.equal_range(affect); auto effect_iter = range.first; @@ -110,3 +110,118 @@ const SpellCache::sEffectCache *SpellCache::GetAltCached(int affect, int subinde return nullptr; } +void SpellCache::InsertSkillProcAttempt(SpellCache::eSkillProc type, int spell, int chance, int slot) +{ + sSkillProc e; + e.spell = spell; + e.chance = chance; + e.slot = slot; + + switch (type) { + case eSkillProc::AA: + m_skill_proc_attempt.aa.push_back(e); + return; + case eSkillProc::Buff: + m_skill_proc_attempt.buff.push_back(e); + return; + case eSkillProc::Worn: + m_skill_proc_attempt.worn.push_back(e); + return; + } +} + +void SpellCache::InsertSkillProcSuccess(SpellCache::eSkillProc type, int spell, int chance, int slot) +{ + sSkillProc e; + e.spell = spell; + e.chance = chance; + e.slot = slot; + + switch (type) { + case eSkillProc::AA: + m_skill_proc_success.aa.push_back(e); + return; + case eSkillProc::Buff: + m_skill_proc_success.buff.push_back(e); + return; + case eSkillProc::Worn: + m_skill_proc_success.worn.push_back(e); + return; + } +} + +void SpellCache::InsertSkillLimit(SpellCache::eSkillProc type, EQEmu::skills::SkillType skill, bool on_success) +{ + auto &which = on_success ? m_skill_proc_success : m_skill_proc_attempt; + + // we have to assume the last one inserted is correct, otherwise it would be bad data + switch (type) { + case eSkillProc::AA: { + auto it = which.aa.rbegin(); + if (it != which.aa.rend()) + it->skills.insert(skill); + return; + } + case eSkillProc::Buff: { + auto it = which.buff.rbegin(); + if (it != which.buff.rend()) + it->skills.insert(skill); + return; + } + case eSkillProc::Worn: { + auto it = which.worn.rbegin(); + if (it != which.worn.rend()) + it->skills.insert(skill); + return; + } + } +} + +std::vector::const_iterator SpellCache::skill_proc_attempt_begin(SpellCache::eSkillProc type) const +{ + switch (type) { + case eSkillProc::AA: + return m_skill_proc_attempt.aa.cbegin(); + case eSkillProc::Buff: + return m_skill_proc_attempt.buff.cbegin(); + case eSkillProc::Worn: + return m_skill_proc_attempt.worn.cbegin(); + } +} + +std::vector::const_iterator SpellCache::skill_proc_attempt_end(SpellCache::eSkillProc type) const +{ + switch (type) { + case eSkillProc::AA: + return m_skill_proc_attempt.aa.cend(); + case eSkillProc::Buff: + return m_skill_proc_attempt.buff.cend(); + case eSkillProc::Worn: + return m_skill_proc_attempt.worn.cend(); + } +} + +std::vector::const_iterator SpellCache::skill_proc_success_begin(SpellCache::eSkillProc type) const +{ + switch (type) { + case eSkillProc::AA: + return m_skill_proc_success.aa.cbegin(); + case eSkillProc::Buff: + return m_skill_proc_success.buff.cbegin(); + case eSkillProc::Worn: + return m_skill_proc_success.worn.cbegin(); + } +} + +std::vector::const_iterator SpellCache::skill_proc_success_end(SpellCache::eSkillProc type) const +{ + switch (type) { + case eSkillProc::AA: + return m_skill_proc_success.aa.cend(); + case eSkillProc::Buff: + return m_skill_proc_success.buff.cend(); + case eSkillProc::Worn: + return m_skill_proc_success.worn.cend(); + } +} + diff --git a/zone/spell_cache.h b/zone/spell_cache.h index 4274a6072..c1118d124 100644 --- a/zone/spell_cache.h +++ b/zone/spell_cache.h @@ -3,6 +3,9 @@ #include #include +#include + +#include "../common/skills.h" class SpellCache { @@ -22,6 +25,28 @@ public: int spell; }; + // enum for skill proc functions + enum class eSkillProc { + Buff, + Worn, + AA + }; + + // this is used for SE_SkillProc and SE_SkillProcSuccess which are matched with SE_LimitToSkill + struct sSkillProc { + int chance; + int spell; + int slot; // used for buff slot when it's a buff + std::set skills; + }; + + // we need to recache these separately + struct sSkillProcs { + std::vector buff; + std::vector worn; + std::vector aa; + }; + SpellCache() : spell_cached(false), item_cached(false), alt_cached(false) {} ~SpellCache() {} @@ -30,42 +55,54 @@ public: void InsertAltEffect(int affect, int value, int subindex); void InsertSkillAttackProc(int chace, int skill, int spell); - inline void ClearSpellEffect() { m_spelleffect.clear(); } - inline void ClearItemEffect() { m_itemeffect.clear(); } - inline void ClearAltEffect() { m_alteffect.clear(); m_skill_attack_proc.clear(); } + inline void ClearSpellEffect() { m_spelleffect.clear(); m_skill_proc_attempt.buff.clear(); m_skill_proc_success.buff.clear(); } + inline void ClearItemEffect() { m_itemeffect.clear(); m_skill_proc_attempt.worn.clear(); m_skill_proc_success.worn.clear(); } + inline void ClearAltEffect() { m_alteffect.clear(); m_skill_attack_proc.clear(); m_skill_proc_attempt.aa.clear(); m_skill_proc_success.aa.clear(); } inline void SetSpellCached(bool v) { spell_cached = v; } inline void SetItemCached(bool v) { item_cached = v; } inline void SetAltCached(bool v) { alt_cached = v; } - inline bool IsSpellCached() { return spell_cached; } - inline bool IsItemCached() { return item_cached; } - inline bool IsAltCached() { return alt_cached; } - inline bool HasSkillAttackProcs() { return !m_skill_attack_proc.empty(); } + inline bool IsSpellCached() const { return spell_cached; } + inline bool IsItemCached() const { return item_cached; } + inline bool IsAltCached() const { return alt_cached; } + inline bool HasSkillAttackProcs() const { return !m_skill_attack_proc.empty(); } - const sEffectCache *GetSpellCached(int affect, int subindex = 0); - const sEffectCache *GetItemCached(int affect, int subindex = 0); - const sEffectCache *GetAltCached(int affect, int subindex = 0); + void InsertSkillProcAttempt(eSkillProc type, int spell, int chance, int slot = -1); + void InsertSkillProcSuccess(eSkillProc type, int spell, int chance, int slot = -1); + void InsertSkillLimit(eSkillProc type, EQEmu::skills::SkillType skill, bool on_success); + inline bool HasSkillProcAttempt() const { return !m_skill_proc_attempt.aa.empty() || !m_skill_proc_attempt.buff.empty() || !m_skill_proc_attempt.worn.empty(); } + inline bool HasSkillProcSuccess() const { return !m_skill_proc_success.aa.empty() || !m_skill_proc_success.buff.empty() || !m_skill_proc_success.worn.empty(); } - std::vector::const_iterator skill_attack_proc_begin() { return m_skill_attack_proc.cbegin(); } - std::vector::const_iterator skill_attack_proc_end() { return m_skill_attack_proc.cend(); } + const sEffectCache *GetSpellCached(int affect, int subindex = 0) const; + const sEffectCache *GetItemCached(int affect, int subindex = 0) const; + const sEffectCache *GetAltCached(int affect, int subindex = 0) const; + + std::vector::const_iterator skill_attack_proc_begin() const { return m_skill_attack_proc.cbegin(); } + std::vector::const_iterator skill_attack_proc_end() const { return m_skill_attack_proc.cend(); } + + std::vector::const_iterator skill_proc_attempt_begin(eSkillProc type) const; + std::vector::const_iterator skill_proc_attempt_end(eSkillProc type) const; + + std::vector::const_iterator skill_proc_success_begin(eSkillProc type) const; + std::vector::const_iterator skill_proc_success_end(eSkillProc type) const; // inlines for common operations - inline int GetCachedPlayerEffect(int affect, int subindex = 0) { + inline int GetCachedPlayerEffect(int affect, int subindex = 0) const { auto res = GetSpellCached(affect, subindex); if (res) return res->base1; return 0; } - inline int GetCachedItemEffect(int affect, int subindex = 0) { + inline int GetCachedItemEffect(int affect, int subindex = 0) const { auto res = GetItemCached(affect, subindex); if (res) return res->base1; return 0; } - inline int GetCachedAltEffect(int affect, int subindex = 0) { + inline int GetCachedAltEffect(int affect, int subindex = 0) const { auto res = GetAltCached(affect, subindex); if (res) return res->base1; @@ -85,6 +122,9 @@ private: // if custom servers want to give someone a million of these ahhh // we need something better :P std::vector m_skill_attack_proc; + + sSkillProcs m_skill_proc_attempt; // SE_SkillProc + sSkillProcs m_skill_proc_success; // SE_SkillProcSuccess }; #endif /* !SPELL_CACHE_H */ From be97d2ac9ed5c946251ee2b159df5d9094954e54 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 15:50:52 -0500 Subject: [PATCH 11/21] Add handling for SE_DivineSave --- zone/bonuses.cpp | 5 +++-- zone/spell_effects.cpp | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index c378b8d49..f1e27e18b 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -762,7 +762,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) // these don't stack and some treat base2 differently and don't stack if (effect == SE_MasteryofPast || effect == SE_Assassinate || effect == SE_AssassinateLevel || effect == SE_HeadShot || effect == SE_HeadShotLevel || effect == SE_FinishingBlowLvl || - effect == SE_FinishingBlow) { + effect == SE_FinishingBlow || SE_DivineSave) { if (base1 > m_spell_cache.GetCachedAltEffect(effect)) m_spell_cache.InsertAltEffect(effect, base1, 0); } else { // stacking! @@ -770,9 +770,10 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) } // RoF2 doesn't have SE_CriticalSpellChance here, but our AA data looks like it's processed like this + // client doesn't handle SE_DivineSave like this if (effect == SE_Assassinate || effect == SE_HeadShot || effect == SE_FinishingBlowLvl || effect == SE_FinishingBlow || effect == SE_AssassinateLevel || effect == SE_HeadShotLevel || - effect == SE_CriticalSpellChance) { + effect == SE_CriticalSpellChance || SE_DivineSave) { base2 = e.base2; // uses base2 for fun times! if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index dd1a9cda8..d5c891b26 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -5831,16 +5831,16 @@ bool Mob::TryDivineSave() -If desired, additional spells can be triggered from the AA/item/spell effect, generally a heal. */ - int32 SuccessChance = aabonuses.DivineSaveChance[0] + itembonuses.DivineSaveChance[0] + spellbonuses.DivineSaveChance[0]; + int32 SuccessChance = TotalEffect(SE_DivineSave, 0); // base2 == 0 is chances if (SuccessChance && zone->random.Roll(SuccessChance)) { SetHP(1); int32 EffectsToTry[] = { - aabonuses.DivineSaveChance[1], - itembonuses.DivineSaveChance[1], - spellbonuses.DivineSaveChance[1] + m_spell_cache.GetCachedPlayerEffect(SE_DivineSave, 1), // base2 == 1 is spell + m_spell_cache.GetCachedItemEffect(SE_DivineSave, 1), + m_spell_cache.GetCachedAltEffect(SE_DivineSave, 1) }; //Fade the divine save effect here after saving the old effects off. //That way, if desired, the effect could apply SE_DivineSave again. From 76b956debfe41f011d86e8bee9aa6703e23f4d33 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 16:07:55 -0500 Subject: [PATCH 12/21] Assassinate --- zone/special_attacks.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index 03b144b5a..f1a6e1b5a 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -2049,7 +2049,7 @@ int Mob::TryAssassinate(Mob *defender, EQEmu::skills::SkillType skillInUse) if (IsClient()) chance += CastToClient()->GetHeroicDEX(); chance *= 10; - int norm = aabonuses.AssassinateLevel[1]; + int norm = TotalEffect(SE_AssassinateLevel, 1); if (norm > 0) chance = chance * norm / 100; } else if (skillInUse == EQEmu::skills::SkillThrowing) { @@ -2059,14 +2059,11 @@ int Mob::TryAssassinate(Mob *defender, EQEmu::skills::SkillType skillInUse) chance += 5; } - chance += aabonuses.Assassinate[0] + spellbonuses.Assassinate[0] + itembonuses.Assassinate[0]; + chance += TotalEffect(SE_Assassinate, 0); // base2 == 0 is chance - uint32 Assassinate_Dmg = - aabonuses.Assassinate[1] + spellbonuses.Assassinate[1] + itembonuses.Assassinate[1]; + int Assassinate_Dmg = TotalEffect(SE_Assassinate, 1); // base2 == 1 is dmg - uint8 Assassinate_Level = 0; // Get Highest Headshot Level - Assassinate_Level = std::max( - {aabonuses.AssassinateLevel[0], spellbonuses.AssassinateLevel[0], itembonuses.AssassinateLevel[0]}); + int Assassinate_Level = BestEffect(SE_AssassinateLevel, 0); // revamped AAs require AA line I believe? if (!Assassinate_Level) From ef50f54462a5752d4dbee01b903619b66585076d Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 16:18:03 -0500 Subject: [PATCH 13/21] Finishing Blow --- zone/attack.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index dc550c702..5de6b5146 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -4349,20 +4349,12 @@ bool Mob::TryFinishingBlow(Mob *defender, int &damage) { // base2 of FinishingBlowLvl is the HP limit (cur / max) * 1000, 10% is listed as 100 if (defender && !defender->IsClient() && defender->GetHPRatio() < 10) { + int FB_Dmg = TotalEffect(SE_FinishingBlow, 1); - uint32 FB_Dmg = - aabonuses.FinishingBlow[1] + spellbonuses.FinishingBlow[1] + itembonuses.FinishingBlow[1]; - - uint32 FB_Level = 0; - FB_Level = aabonuses.FinishingBlowLvl[0]; - if (FB_Level < spellbonuses.FinishingBlowLvl[0]) - FB_Level = spellbonuses.FinishingBlowLvl[0]; - else if (FB_Level < itembonuses.FinishingBlowLvl[0]) - FB_Level = itembonuses.FinishingBlowLvl[0]; + int FB_Level = BestEffect(SE_FinishingBlowLvl, 0); // modern AA description says rank 1 (500) is 50% chance - int ProcChance = - aabonuses.FinishingBlow[0] + spellbonuses.FinishingBlow[0] + spellbonuses.FinishingBlow[0]; + int ProcChance = TotalEffect(SE_FinishingBlow, 0); if (FB_Level && FB_Dmg && (defender->GetLevel() <= FB_Level) && (ProcChance >= zone->random.Int(1, 1000))) { From 69fbe24766cc755992e328be83494ed50655c925 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 16:35:45 -0500 Subject: [PATCH 14/21] Headshot --- zone/special_attacks.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index f1a6e1b5a..474d16d40 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -2014,9 +2014,8 @@ int Mob::TryHeadShot(Mob *defender, EQEmu::skills::SkillType skillInUse) // Only works on YOUR target. if (defender && defender->GetBodyType() == BT_Humanoid && !defender->IsClient() && skillInUse == EQEmu::skills::SkillArchery && GetTarget() == defender) { - uint32 HeadShot_Dmg = aabonuses.HeadShot[1] + spellbonuses.HeadShot[1] + itembonuses.HeadShot[1]; - uint8 HeadShot_Level = 0; // Get Highest Headshot Level - HeadShot_Level = std::max({aabonuses.HSLevel[0], spellbonuses.HSLevel[0], itembonuses.HSLevel[0]}); + int HeadShot_Dmg = TotalEffect(SE_HeadShot, 1); + int HeadShot_Level = BestEffect(SE_HeadShotLevel); if (HeadShot_Dmg && HeadShot_Level && (defender->GetLevel() <= HeadShot_Level)) { int chance = GetDEX(); @@ -2024,10 +2023,10 @@ int Mob::TryHeadShot(Mob *defender, EQEmu::skills::SkillType skillInUse) if (IsClient()) chance += CastToClient()->GetHeroicDEX() / 25; chance *= 10; - int norm = aabonuses.HSLevel[1]; + int norm = TotalEffect(SE_HeadShotLevel, 1); if (norm > 0) chance = chance * norm / 100; - chance += aabonuses.HeadShot[0] + spellbonuses.HeadShot[0] + itembonuses.HeadShot[0]; + chance += TotalEffect(SE_HeadShot, 0); if (zone->random.Int(1, 1000) <= chance) { entity_list.MessageClose_StringID(this, false, 200, MT_CritMelee, FATAL_BOW_SHOT, GetName()); From 44dfbc4e0b70e0ed78a981275925854299d0f9ad Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 16:42:54 -0500 Subject: [PATCH 15/21] Fix Divine Save --- zone/bonuses.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index f1e27e18b..ca97b4bda 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -773,7 +773,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) // client doesn't handle SE_DivineSave like this if (effect == SE_Assassinate || effect == SE_HeadShot || effect == SE_FinishingBlowLvl || effect == SE_FinishingBlow || effect == SE_AssassinateLevel || effect == SE_HeadShotLevel || - effect == SE_CriticalSpellChance || SE_DivineSave) { + effect == SE_CriticalSpellChance || effect == SE_DivineSave) { base2 = e.base2; // uses base2 for fun times! if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); From eb7d6669ef7e4b33130d454d62c3ecd3732d6518 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 16:44:28 -0500 Subject: [PATCH 16/21] Spell crit --- zone/bonuses.cpp | 3 ++- zone/effects.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index ca97b4bda..e5f0a198f 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -771,9 +771,10 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) // RoF2 doesn't have SE_CriticalSpellChance here, but our AA data looks like it's processed like this // client doesn't handle SE_DivineSave like this + // we support older Frenzied Devastation SPA too, this was revamped on live at some point if (effect == SE_Assassinate || effect == SE_HeadShot || effect == SE_FinishingBlowLvl || effect == SE_FinishingBlow || effect == SE_AssassinateLevel || effect == SE_HeadShotLevel || - effect == SE_CriticalSpellChance || effect == SE_DivineSave) { + effect == SE_CriticalSpellChance || effect == SE_DivineSave || effect == SE_FrenziedDevastation) { base2 = e.base2; // uses base2 for fun times! if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); diff --git a/zone/effects.cpp b/zone/effects.cpp index 2741a924b..3c1e1f136 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -62,8 +62,8 @@ int32 Mob::GetActSpellDamage(uint16 spell_id, int32 value, Mob* target) { value -= GetAA(aaUnholyTouch) * 450; //Unholy Touch chance = RuleI(Spells, BaseCritChance); //Wizard base critical chance is 2% (Does not scale with level) - chance += itembonuses.CriticalSpellChance + spellbonuses.CriticalSpellChance + aabonuses.CriticalSpellChance; - chance += itembonuses.FrenziedDevastation + spellbonuses.FrenziedDevastation + aabonuses.FrenziedDevastation; + chance += TotalEffect(SE_CriticalSpellChance, 0); + chance += TotalEffect(SE_FrenziedDevastation, 1); //Crtical Hit Calculation pathway if (chance > 0 || (IsClient() && GetClass() == WIZARD && GetLevel() >= RuleI(Spells, WizCritLevel))) { @@ -79,8 +79,8 @@ int32 Mob::GetActSpellDamage(uint16 spell_id, int32 value, Mob* target) { if (zone->random.Roll(chance)) { Critical = true; - ratio += itembonuses.SpellCritDmgIncrease + spellbonuses.SpellCritDmgIncrease + aabonuses.SpellCritDmgIncrease; - ratio += itembonuses.SpellCritDmgIncNoStack + spellbonuses.SpellCritDmgIncNoStack + aabonuses.SpellCritDmgIncNoStack; + ratio += TotalEffect(SE_SpellCritDmgIncrease); + ratio += TotalEffect(SE_CriticalSpellChance, 1); } else if ((IsClient() && GetClass() == WIZARD) || (IsMerc() && GetClass() == CASTERDPS)) { From 4b53fd775cfe18dff1e7b8308bdb684fd3cd2203 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 18:14:15 -0500 Subject: [PATCH 17/21] Add minified StatBonus that will be used in the new system --- zone/common.h | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/zone/common.h b/zone/common.h index f7b157115..45bba20de 100644 --- a/zone/common.h +++ b/zone/common.h @@ -285,6 +285,61 @@ struct Buffs_Struct { bool UpdateClient; }; +// we will use this for item stat bonus now only rather then spell effects +// TODO: rename when other is removed +struct StatBonuses2 { + int STR; + int STA; + int CHA; + int DEX; + int INT; + int AGI; + int WIS; + int MR; + int CR; + int FR; + int PR; + int DR; + int Corrup; + int HP; + int Mana; + int Endurance; + int AC; + int ProcChance; // ProcChance/10 == % increase i = CombatEffects + int MeleeMitigation; //i = Shielding + int SpellShield; + int AvoidMeleeChance; //AvoidMeleeChance/10 == % chance i = Avoidance (item mod) + int HitChance; //HitChance/15 == % increase i = Accuracy (Item: Accuracy) + int StunResist; //i + int DoTShielding; + int DSMitigation; // Item Effect + int HeroicSTR; + int HeroicINT; + int HeroicWIS; + int HeroicAGI; + int HeroicDEX; + int HeroicSTA; + int HeroicCHA; + int HeroicMR; + int HeroicFR; + int HeroicCR; + int HeroicDR; + int HeroicPR; + int HeroicCorrup; + int HealAmt; // Item Effect + int SpellDmg; // Item Effect + int Clairvoyance; // Item Effect + int StrikeThrough; // PoP: Strike Through % + int ATK; + int HPRegen; + int ManaRegen; + int EnduranceRegen; + int DamageShield; // this is damage done to mobs that attack this + int PotionBeltSlots; + int SkillMinDamageMod[9]; + int haste; +}; + struct StatBonuses { int32 AC; int32 HP; From cfb270a1dd81048a7536eb21d88717ee4038f056 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Tue, 14 Nov 2017 22:15:11 -0500 Subject: [PATCH 18/21] Slay undead --- zone/attack.cpp | 5 ++--- zone/bonuses.cpp | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index 5de6b5146..f197b17e2 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -4173,12 +4173,11 @@ void Mob::TryCriticalHit(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions * // 1: Try Slay Undead if (defender->GetBodyType() == BT_Undead || defender->GetBodyType() == BT_SummonedUndead || defender->GetBodyType() == BT_Vampire) { - int SlayRateBonus = aabonuses.SlayUndead[0] + itembonuses.SlayUndead[0] + spellbonuses.SlayUndead[0]; + int SlayRateBonus = TotalEffect(SE_SlayUndead, 0); if (SlayRateBonus) { float slayChance = static_cast(SlayRateBonus) / 10000.0f; if (zone->random.Roll(slayChance)) { - int SlayDmgBonus = std::max( - { aabonuses.SlayUndead[1], itembonuses.SlayUndead[1], spellbonuses.SlayUndead[1] }); + int SlayDmgBonus = BestEffect(SE_SlayUndead, 1); hit.damage_done = std::max(hit.damage_done, hit.base_damage) + 5; hit.damage_done = (hit.damage_done * SlayDmgBonus) / 100; diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index e5f0a198f..36edf3185 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -772,9 +772,11 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) // RoF2 doesn't have SE_CriticalSpellChance here, but our AA data looks like it's processed like this // client doesn't handle SE_DivineSave like this // we support older Frenzied Devastation SPA too, this was revamped on live at some point + // SE_SlayUndead isn't done here on client, but it should work for how it works if (effect == SE_Assassinate || effect == SE_HeadShot || effect == SE_FinishingBlowLvl || effect == SE_FinishingBlow || effect == SE_AssassinateLevel || effect == SE_HeadShotLevel || - effect == SE_CriticalSpellChance || effect == SE_DivineSave || effect == SE_FrenziedDevastation) { + effect == SE_CriticalSpellChance || effect == SE_DivineSave || effect == SE_FrenziedDevastation || + effect == SE_SlayUndead) { base2 = e.base2; // uses base2 for fun times! if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); From 98c9f9b835cf98e23229318ae1af80fed1aa4fa7 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Wed, 15 Nov 2017 00:09:48 -0500 Subject: [PATCH 19/21] Convert to switches for easier maintenance --- zone/bonuses.cpp | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 36edf3185..4d051d5e5 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -760,26 +760,43 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) } // these don't stack and some treat base2 differently and don't stack - if (effect == SE_MasteryofPast || effect == SE_Assassinate || effect == SE_AssassinateLevel || - effect == SE_HeadShot || effect == SE_HeadShotLevel || effect == SE_FinishingBlowLvl || - effect == SE_FinishingBlow || SE_DivineSave) { + switch (effect) { + case SE_MasteryofPast: + case SE_Assassinate: + case SE_AssassinateLevel: + case SE_HeadShot: + case SE_HeadShotLevel: + case SE_FinishingBlowLvl: + case SE_FinishingBlow: + case SE_DivineSave: // non stacking if (base1 > m_spell_cache.GetCachedAltEffect(effect)) m_spell_cache.InsertAltEffect(effect, base1, 0); - } else { // stacking! + break; + defualt: //stacking m_spell_cache.InsertAltEffect(effect, base1 + m_spell_cache.GetCachedAltEffect(effect), base2); + break; } // RoF2 doesn't have SE_CriticalSpellChance here, but our AA data looks like it's processed like this // client doesn't handle SE_DivineSave like this // we support older Frenzied Devastation SPA too, this was revamped on live at some point // SE_SlayUndead isn't done here on client, but it should work for how it works - if (effect == SE_Assassinate || effect == SE_HeadShot || effect == SE_FinishingBlowLvl || - effect == SE_FinishingBlow || effect == SE_AssassinateLevel || effect == SE_HeadShotLevel || - effect == SE_CriticalSpellChance || effect == SE_DivineSave || effect == SE_FrenziedDevastation || - effect == SE_SlayUndead) { + switch (effect) { + case SE_Assassinate: + case SE_HeadShot: + case SE_FinishingBlow: + case SE_AssassinateLevel: + case SE_HeadShotLevel: + case SE_CriticalSpellChance: + case SE_DivineSave: + case SE_FrenziedDevastation: + case SE_SlayUndead: base2 = e.base2; // uses base2 for fun times! if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); + break; + default: + break; } // TODO remove From 7d7fa32d6cb9613124da18242665f4fdedc90d7e Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Wed, 15 Nov 2017 00:12:39 -0500 Subject: [PATCH 20/21] Default! --- zone/bonuses.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 4d051d5e5..59fa32730 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -772,7 +772,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) if (base1 > m_spell_cache.GetCachedAltEffect(effect)) m_spell_cache.InsertAltEffect(effect, base1, 0); break; - defualt: //stacking + default: //stacking m_spell_cache.InsertAltEffect(effect, base1 + m_spell_cache.GetCachedAltEffect(effect), base2); break; } From a92a7e97e1fdea7a1c236d87fa66ce9cd137174b Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Fri, 17 Nov 2017 00:34:13 -0500 Subject: [PATCH 21/21] Work on proc on death --- zone/bonuses.cpp | 8 +------- zone/spell_cache.h | 32 +++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 59fa32730..22fa9fc3d 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -698,13 +698,6 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) /* TODO: Other SPAs that need special handling * SE_ProcOnKillShot * SE_SpellOnDeath - * SE_SkillAttackProc - * SE_SlayUndead - * SE_DivineSave - * SE_FrenziedDevastation - * SE_SkillProc - * SE_SkillProcSuccess - * SE_PC_Pet_Rampage */ // special handling for AAs that live doesn't appear to do @@ -791,6 +784,7 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) case SE_DivineSave: case SE_FrenziedDevastation: case SE_SlayUndead: + case SE_PC_Pet_Rampage: base2 = e.base2; // uses base2 for fun times! if (base2 > m_spell_cache.GetCachedAltEffect(effect, 1)) m_spell_cache.InsertAltEffect(effect, base2, 1); diff --git a/zone/spell_cache.h b/zone/spell_cache.h index c1118d124..0f8ab027d 100644 --- a/zone/spell_cache.h +++ b/zone/spell_cache.h @@ -32,6 +32,13 @@ public: AA }; + // SE_SpellOnDeath + struct sDeathProc { + int chance; + int spell; + int slot; + }; + // this is used for SE_SkillProc and SE_SkillProcSuccess which are matched with SE_LimitToSkill struct sSkillProc { int chance; @@ -41,10 +48,11 @@ public: }; // we need to recache these separately - struct sSkillProcs { - std::vector buff; - std::vector worn; - std::vector aa; + template + struct sProcs { + std::vector buff; + std::vector worn; + std::vector aa; }; SpellCache() : spell_cached(false), item_cached(false), alt_cached(false) {} @@ -55,9 +63,9 @@ public: void InsertAltEffect(int affect, int value, int subindex); void InsertSkillAttackProc(int chace, int skill, int spell); - inline void ClearSpellEffect() { m_spelleffect.clear(); m_skill_proc_attempt.buff.clear(); m_skill_proc_success.buff.clear(); } - inline void ClearItemEffect() { m_itemeffect.clear(); m_skill_proc_attempt.worn.clear(); m_skill_proc_success.worn.clear(); } - inline void ClearAltEffect() { m_alteffect.clear(); m_skill_attack_proc.clear(); m_skill_proc_attempt.aa.clear(); m_skill_proc_success.aa.clear(); } + inline void ClearSpellEffect() { m_spelleffect.clear(); m_skill_proc_attempt.buff.clear(); m_skill_proc_success.buff.clear(); m_death_proc.buff.clear(); } + inline void ClearItemEffect() { m_itemeffect.clear(); m_skill_proc_attempt.worn.clear(); m_skill_proc_success.worn.clear(); m_death_proc.worn.clear(); } + inline void ClearAltEffect() { m_alteffect.clear(); m_skill_attack_proc.clear(); m_skill_proc_attempt.aa.clear(); m_skill_proc_success.aa.clear(); m_death_proc.aa.clear(); } inline void SetSpellCached(bool v) { spell_cached = v; } inline void SetItemCached(bool v) { item_cached = v; } @@ -74,6 +82,8 @@ public: inline bool HasSkillProcAttempt() const { return !m_skill_proc_attempt.aa.empty() || !m_skill_proc_attempt.buff.empty() || !m_skill_proc_attempt.worn.empty(); } inline bool HasSkillProcSuccess() const { return !m_skill_proc_success.aa.empty() || !m_skill_proc_success.buff.empty() || !m_skill_proc_success.worn.empty(); } + void InsertDeathProc(eSkillProc type, int spell, int chance, int slot = -1); + const sEffectCache *GetSpellCached(int affect, int subindex = 0) const; const sEffectCache *GetItemCached(int affect, int subindex = 0) const; const sEffectCache *GetAltCached(int affect, int subindex = 0) const; @@ -87,6 +97,9 @@ public: std::vector::const_iterator skill_proc_success_begin(eSkillProc type) const; std::vector::const_iterator skill_proc_success_end(eSkillProc type) const; + std::vector::const_iterator death_proc_begin(eSkillProc type) const; + std::vector::const_iterator death_proc_end(eSkillProc type) const; + // inlines for common operations inline int GetCachedPlayerEffect(int affect, int subindex = 0) const { auto res = GetSpellCached(affect, subindex); @@ -123,8 +136,9 @@ private: // we need something better :P std::vector m_skill_attack_proc; - sSkillProcs m_skill_proc_attempt; // SE_SkillProc - sSkillProcs m_skill_proc_success; // SE_SkillProcSuccess + sProcs m_skill_proc_attempt; // SE_SkillProc + sProcs m_skill_proc_success; // SE_SkillProcSuccess + sProcs m_death_proc; // SE_SpellOnDeath }; #endif /* !SPELL_CACHE_H */