diff --git a/common/ruletypes.h b/common/ruletypes.h index 1e08a9cbd..9b8326aac 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -546,6 +546,9 @@ RULE_INT(Spells, PointBlankAOEMaxTargets, 0, "Max number of targets a Point-Blan RULE_INT(Spells, DefaultAOEMaxTargets, 0, "Max number of targets that an AOE spell which does not meet other descriptions can cast on. Set to 0 for no limit.") RULE_BOOL(Spells, AllowFocusOnSkillDamageSpells, false, "Allow focus effects 185, 459, and 482 to enhance SkillAttack spell effect 193") RULE_STRING(Spells, AlwaysStackSpells, "", "Comma-Seperated list of spell IDs to always stack with every other spell, except themselves.") +RULE_BOOL(Spells, SuppressDispels, false, "Swaps 'cancel magic' SPA logic with SuppressBuff SPA (527).") +RULE_INT(Spells, SuppressDispelsTime, 6, "Number of tics that dispelled buffs will be suppressed for") +RULE_INT(Spells, SuppressDebuffSpellID, 21840, "Spell ID to send to client when a spell is supprssed. 21840 = 'Suppression Field'") RULE_CATEGORY_END() RULE_CATEGORY(Combat) diff --git a/common/spdat.cpp b/common/spdat.cpp index f4d3df255..2ace98b74 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -966,6 +966,19 @@ bool IsValidSpell(uint32 spell_id) return false; } +bool IsValidOrSuppressedSpell(uint32 spell_id) +{ + if (IsValidSpell(spell_id)) { + return true; + } + + if (spell_id == SPELL_SUPPRESSED) { + return true; + } + + return false; +} + bool IsHarmTouchSpell(uint16 spell_id) { return spell_id == SPELL_HARM_TOUCH || diff --git a/common/spdat.h b/common/spdat.h index 11130d6a4..fd3332103 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -24,6 +24,7 @@ #define SPELL_UNKNOWN 0xFFFF #define POISON_PROC 0xFFFE +#define SPELL_SUPPRESSED 0xFFFD #define SPELLBOOK_UNKNOWN 0xFFFFFFFF //player profile spells are 32 bit //some spell IDs which will prolly change, but are needed @@ -1815,6 +1816,7 @@ bool IsEffectInSpell(uint16 spell_id, int effect_id); uint16 GetSpellTriggerSpellID(uint16 spell_id, int effect_id); bool IsBlankSpellEffect(uint16 spell_id, int effect_index); bool IsValidSpell(uint32 spell_id); +bool IsValidOrSuppressedSpell(uint32 spell_id); bool IsSummonSpell(uint16 spell_id); bool IsDamageSpell(uint16 spell_id); bool IsAnyDamageSpell(uint16 spell_id); diff --git a/zone/client.h b/zone/client.h index d5e8f3ef4..4a2274b9b 100644 --- a/zone/client.h +++ b/zone/client.h @@ -345,6 +345,7 @@ public: bool IsClient() const override { return true; } bool IsOfClientBot() const override { return true; } bool IsOfClientBotMerc() const override { return true; } + void ReapplyBuff(uint32 index, bool from_suppress = false); void CompleteConnect(); bool TryStacking(EQ::ItemInstance* item, uint8 type = ItemPacketTrade, bool try_worn = true, bool try_cursor = true); void SendTraderPacket(Client* trader, uint32 Unknown72 = 51); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 3cf7cd6b3..917a59166 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -527,6 +527,109 @@ int Client::HandlePacket(const EQApplicationPacket *app) return(true); } +//Some buffs have effects that trigger only as part of the original cast. This reapplies those effects. +void Client::ReapplyBuff(uint32 index, bool from_suppress) +{ + LogDebug("ReapplyBuff: index [{}] from_suppress [{}]", index, from_suppress); + if (!IsValidSpell(buffs[index].spellid)) + return; + + const SPDat_Spell_Struct &spell = spells[buffs[index].spellid]; + + int NimbusEffect = GetSpellNimbusEffect(buffs[index].spellid); + if (NimbusEffect) { + if (!IsNimbusEffectActive(NimbusEffect)) + SendSpellEffect(NimbusEffect, 500, 0, 1, 3000, true); + } + + for (int x1 = 0; x1 < EFFECT_COUNT; x1++) { + switch (spell.effect_id[x1]) { + case SE_Illusion: { + if (GetIllusionBlock()) { + break; + } + + if (from_suppress || buffs[index].persistant_buff) { + Mob *caster = entity_list.GetMobID(buffs[index].casterid); + ApplySpellEffectIllusion(spell.id, caster, index, spell.base_value[x1], spell.limit_value[x1], spell.max_value[x1]); + } + break; + } + case SE_SummonHorse: { + if (!from_suppress && (RuleB(Character, PreventMountsFromZoning) || !zone->CanCastOutdoor())) { + BuffFadeByEffect(SE_SummonHorse); + } else { + SummonHorse(buffs[index].spellid); + } + break; + } + case SE_Silence: + { + Silence(true); + break; + } + case SE_Amnesia: + { + Amnesia(true); + break; + } + case SE_DivineAura: + { + invulnerable = true; + break; + } + case SE_Invisibility2: + case SE_Invisibility: + { + SendAppearancePacket(AppearanceType::Invisibility, Invisibility::Invisible); + break; + } + case SE_Levitate: + { + if (!zone->CanLevitate()) { + if (!GetGM()) { + SendAppearancePacket(AppearanceType::FlyMode, 0); + BuffFadeByEffect(SE_Levitate); + Message(Chat::Red, "You can't levitate in this zone."); + break; + } + + Message(Chat::White, "Your GM flag allows you to levitate in this zone."); + } + + SendAppearancePacket( + AppearanceType::FlyMode, + ( + spell.limit_value[x1] == 1 ? + EQ::constants::GravityBehavior::LevitateWhileRunning : + EQ::constants::GravityBehavior::Levitating + ), + true, + !from_suppress + ); + + break; + } + case SE_AddMeleeProc: + case SE_WeaponProc: + { + AddProcToWeapon(GetProcID(buffs[index].spellid, x1), false, 100 + spells[buffs[index].spellid].limit_value[x1], buffs[index].spellid, buffs[index].casterlevel, GetSpellProcLimitTimer(buffs[index].spellid, ProcType::MELEE_PROC)); + break; + } + case SE_DefensiveProc: + { + AddDefensiveProc(GetProcID(buffs[index].spellid, x1), 100 + spells[buffs[index].spellid].limit_value[x1], buffs[index].spellid, GetSpellProcLimitTimer(buffs[index].spellid, ProcType::DEFENSIVE_PROC)); + break; + } + case SE_RangedProc: + { + AddRangedProc(GetProcID(buffs[index].spellid, x1), 100 + spells[buffs[index].spellid].limit_value[x1], buffs[index].spellid, GetSpellProcLimitTimer(buffs[index].spellid, ProcType::RANGED_PROC)); + break; + } + } + } +} + // Finish client connecting state void Client::CompleteConnect() { @@ -655,106 +758,9 @@ void Client::CompleteConnect() //bulk raid send in here eventually - //reapply some buffs uint32 buff_count = GetMaxTotalSlots(); for (uint32 j1 = 0; j1 < buff_count; j1++) { - if (!IsValidSpell(buffs[j1].spellid)) - continue; - - const SPDat_Spell_Struct &spell = spells[buffs[j1].spellid]; - - int NimbusEffect = GetSpellNimbusEffect(buffs[j1].spellid); - if (NimbusEffect) { - if (!IsNimbusEffectActive(NimbusEffect)) - SendSpellEffect(NimbusEffect, 500, 0, 1, 3000, true); - } - - for (int x1 = 0; x1 < EFFECT_COUNT; x1++) { - switch (spell.effect_id[x1]) { - case SpellEffect::Illusion: { - if (GetIllusionBlock()) { - break; - } - - if (buffs[j1].persistant_buff) { - Mob *caster = entity_list.GetMobID(buffs[j1].casterid); - ApplySpellEffectIllusion(spell.id, caster, j1, spell.base_value[x1], spell.limit_value[x1], spell.max_value[x1]); - } - break; - } - case SpellEffect::SummonHorse: { - if (RuleB(Character, PreventMountsFromZoning) || !zone->CanCastOutdoor()) { - BuffFadeByEffect(SpellEffect::SummonHorse); - } else { - SummonHorse(buffs[j1].spellid); - } - break; - } - case SpellEffect::Silence: - { - Silence(true); - break; - } - case SpellEffect::Amnesia: - { - Amnesia(true); - break; - } - case SpellEffect::DivineAura: - { - invulnerable = true; - break; - } - case SpellEffect::Invisibility2: - case SpellEffect::Invisibility: - { - SendAppearancePacket(AppearanceType::Invisibility, Invisibility::Invisible); - break; - } - case SpellEffect::Levitate: - { - if (!zone->CanLevitate()) { - if (!GetGM()) { - SendAppearancePacket(AppearanceType::FlyMode, 0); - BuffFadeByEffect(SpellEffect::Levitate); - Message(Chat::Red, "You can't levitate in this zone."); - break; - } - - Message(Chat::White, "Your GM flag allows you to levitate in this zone."); - } - - SendAppearancePacket( - AppearanceType::FlyMode, - ( - spell.limit_value[x1] == 1 ? - EQ::constants::GravityBehavior::LevitateWhileRunning : - EQ::constants::GravityBehavior::Levitating - ), - true, - true - ); - - break; - } - case SpellEffect::AddMeleeProc: - case SpellEffect::WeaponProc: - { - AddProcToWeapon(GetProcID(buffs[j1].spellid, x1), false, 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid, buffs[j1].casterlevel, GetSpellProcLimitTimer(buffs[j1].spellid, ProcType::MELEE_PROC)); - break; - } - case SpellEffect::DefensiveProc: - { - AddDefensiveProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid, GetSpellProcLimitTimer(buffs[j1].spellid, ProcType::DEFENSIVE_PROC)); - break; - } - case SpellEffect::RangedProc: - { - AddRangedProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid, GetSpellProcLimitTimer(buffs[j1].spellid, ProcType::RANGED_PROC)); - break; - } - } - } + ReapplyBuff(j1); } /* Sends appearances for all mobs not doing Animation::Standing aka sitting, looting, playing dead */ diff --git a/zone/common.h b/zone/common.h index 6a11f7255..ae473b2bc 100644 --- a/zone/common.h +++ b/zone/common.h @@ -226,6 +226,8 @@ struct Buffs_Struct { bool persistant_buff; bool client; //True if the caster is a client bool UpdateClient; + uint16 suppressedid; + uint32 suppressedticsremaining; // cereal template diff --git a/zone/lua_mob.cpp b/zone/lua_mob.cpp index e23eb2d03..5424acfcc 100644 --- a/zone/lua_mob.cpp +++ b/zone/lua_mob.cpp @@ -2167,6 +2167,11 @@ void Lua_Mob::BuffFadeBySlot(int slot, bool recalc_bonuses) { self->BuffFadeBySlot(slot, recalc_bonuses); } +void Lua_Mob::BuffFadeBySlot(int slot, bool recalc_bonuses, bool suppress, int suppress_tics) { + Lua_Safe_Call_Void(); + self->BuffFadeBySlot(slot, recalc_bonuses, suppress, suppress_tics); +} + int Lua_Mob::CanBuffStack(int spell_id, int caster_level) { Lua_Safe_Call_Int(); return self->CanBuffStack(spell_id, caster_level); @@ -3572,6 +3577,7 @@ luabind::scope lua_register_mob() { .def("BuffFadeByEffect", (void(Lua_Mob::*)(int,int))&Lua_Mob::BuffFadeByEffect) .def("BuffFadeBySlot", (void(Lua_Mob::*)(int))&Lua_Mob::BuffFadeBySlot) .def("BuffFadeBySlot", (void(Lua_Mob::*)(int,bool))&Lua_Mob::BuffFadeBySlot) + .def("BuffFadeBySlot", (void(Lua_Mob::*)(int,bool,bool,int))&Lua_Mob::BuffFadeBySlot) .def("BuffFadeBySpellID", (void(Lua_Mob::*)(int))&Lua_Mob::BuffFadeBySpellID) .def("BuffFadeDetrimental", (void(Lua_Mob::*)(void))&Lua_Mob::BuffFadeDetrimental) .def("BuffFadeDetrimentalByCaster", (void(Lua_Mob::*)(Lua_Mob))&Lua_Mob::BuffFadeDetrimentalByCaster) diff --git a/zone/lua_mob.h b/zone/lua_mob.h index b5019aa5f..2a4faa512 100644 --- a/zone/lua_mob.h +++ b/zone/lua_mob.h @@ -434,6 +434,7 @@ public: void BuffFadeAll(); void BuffFadeBySlot(int slot); void BuffFadeBySlot(int slot, bool recalc_bonuses); + void BuffFadeBySlot(int slot, bool recalc_bonuses, bool suppress, int suppress_tics); int CanBuffStack(int spell_id, int caster_level); int CanBuffStack(int spell_id, int caster_level, bool fail_if_overwrite); void SetPseudoRoot(bool in); diff --git a/zone/mob.h b/zone/mob.h index ca0abe268..717468335 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -452,7 +452,7 @@ public: void BuffFadeBeneficial(); void BuffFadeNonPersistDeath(); void BuffFadeDetrimental(); - void BuffFadeBySlot(int slot, bool iRecalcBonuses = true); + void BuffFadeBySlot(int slot, bool iRecalcBonuses = true, bool suppress = false, uint32 suppresstics = 0); void BuffFadeDetrimentalByCaster(Mob *caster); void BuffFadeBySitModifier(); void BuffFadeSongs(); @@ -966,7 +966,9 @@ public: void TrySympatheticProc(Mob *target, uint32 spell_id); uint16 GetSympatheticFocusEffect(focusType type, uint16 spell_id); bool TryFadeEffect(int slot); - void DispelMagic(Mob* casterm, uint16 spell_id, int effect_value); + void DispelMagic(Mob* casterm, uint16 spell_id, int effect_value, int chance = 1000, bool detrimental_only = false, bool benficial_only = false); + bool IsSuppressableBuff(int slot) const; + void SuppressBuff(Mob* caster, uint16 spell_id, int tic_count); bool TrySpellEffectResist(uint16 spell_id); int32 GetVulnerability(Mob *caster, uint32 spell_id, uint32 ticsremaining, bool from_buff_tic = false); int64 GetFcDamageAmtIncoming(Mob *caster, int32 spell_id, bool from_buff_tic = false); diff --git a/zone/perl_mob.cpp b/zone/perl_mob.cpp index dd06db33b..2a0cbc80a 100644 --- a/zone/perl_mob.cpp +++ b/zone/perl_mob.cpp @@ -1044,6 +1044,11 @@ void Perl_Mob_BuffFadeBySlot(Mob* self, int slot, bool recalc_bonuses) // @categ self->BuffFadeBySlot(slot, recalc_bonuses); } +void Perl_Mob_BuffFadeBySlot(Mob* self, int slot, bool recalc_bonuses, bool suppress, int suppress_tics) // @categories Script Utility, Spells and Disciplines +{ + self->BuffFadeBySlot(slot, recalc_bonuses, suppress, suppress_tics); +} + bool Perl_Mob_CanBuffStack(Mob* self, uint16 spell_id, uint8 caster_level) // @categories Script Utility, Spells and Disciplines { return self->CanBuffStack(spell_id, caster_level); @@ -3647,6 +3652,7 @@ void perl_register_mob() package.add("BuffFadeByEffect", (void(*)(Mob*, int, int))&Perl_Mob_BuffFadeByEffect); package.add("BuffFadeBySlot", (void(*)(Mob*, int))&Perl_Mob_BuffFadeBySlot); package.add("BuffFadeBySlot", (void(*)(Mob*, int, bool))&Perl_Mob_BuffFadeBySlot); + package.add("BuffFadeBySlot", (void(*)(Mob*, int, bool, bool, int))&Perl_Mob_BuffFadeBySlot); package.add("BuffFadeBySpellID", &Perl_Mob_BuffFadeBySpellID); package.add("BuffFadeDetrimental", &Perl_Mob_BuffFadeDetrimental); package.add("BuffFadeDetrimentalByCaster", &Perl_Mob_BuffFadeDetrimentalByCaster); diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 42078e322..db927e0da 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -1120,24 +1120,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove caster->MessageString(Chat::SpellFailure, SPELL_NO_EFFECT, spells[spell_id].name); break; } - /* - TODO: Parsing shows there is no level modifier. However, a consistent -2% modifer was - found on spell with value 950 (95% spells would have 7% failure rates). - Further investigation is needed. ~ Kayen - */ - int chance = spells[spell_id].base_value[i]; - int buff_count = GetMaxTotalSlots(); - for(int slot = 0; slot < buff_count; slot++) { - if (IsValidSpell(buffs[slot].spellid) && - IsDetrimentalSpell(buffs[slot].spellid) && - spells[buffs[slot].spellid].dispel_flag == 0) - { - if (zone->random.Int(1, 1000) <= chance){ - BuffFadeBySlot(slot); - slot = buff_count; - } - } - } + + DispelMagic(caster, spell_id, effect_value, spells[spell_id].base_value[i], true); break; } @@ -1152,19 +1136,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove break; } - int chance = spells[spell_id].base_value[i]; - int buff_count = GetMaxTotalSlots(); - for(int slot = 0; slot < buff_count; slot++) { - if (IsValidSpell(buffs[slot].spellid) && - IsBeneficialSpell(buffs[slot].spellid) && - spells[buffs[slot].spellid].dispel_flag == 0) - { - if (zone->random.Int(1, 1000) <= chance) { - BuffFadeBySlot(slot); - slot = buff_count; - } - } - } + DispelMagic(caster, spell_id, effect_value, spells[spell_id].base_value[i], false, true); break; } @@ -3843,18 +3815,20 @@ void Mob::BuffProcess() for (int buffs_i = 0; buffs_i < buff_count; ++buffs_i) { - if (IsValidSpell(buffs[buffs_i].spellid)) + if (IsValidOrSuppressedSpell(buffs[buffs_i].spellid)) { DoBuffTic(buffs[buffs_i], buffs_i, entity_list.GetMob(buffs[buffs_i].casterid)); // If the Mob died during DoBuffTic, then the buff we are currently processing will have been removed - if(!IsValidSpell(buffs[buffs_i].spellid)) { + if(!IsValidOrSuppressedSpell(buffs[buffs_i].spellid)) { continue; } // DF_Permanent uses -1 DF_Aura uses -4 but we need to check negatives for some spells for some reason? - if (spells[buffs[buffs_i].spellid].buff_duration_formula != DF_Permanent && - spells[buffs[buffs_i].spellid].buff_duration_formula != DF_Aura && - buffs[buffs_i].ticsremaining != PERMANENT_BUFF_DURATION) { + if (buffs[buffs_i].spellid == SPELL_SUPPRESSED || + (spells[buffs[buffs_i].spellid].buff_duration_formula != DF_Permanent && + spells[buffs[buffs_i].spellid].buff_duration_formula != DF_Aura && + buffs[buffs_i].ticsremaining != PERMANENT_BUFF_DURATION)) { + if(!zone->BuffTimersSuspended() || !IsSuspendableSpell(buffs[buffs_i].spellid)) { --buffs[buffs_i].ticsremaining; @@ -4249,16 +4223,21 @@ void Mob::DoBuffTic(const Buffs_Struct &buff, int slot, Mob *caster) } // removes the buff in the buff slot 'slot' -void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) +void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses, bool suppress, uint32 suppresstics) { if(slot < 0 || slot > GetMaxTotalSlots()) return; - if(!IsValidSpell(buffs[slot].spellid)) + if(!IsValidOrSuppressedSpell(buffs[slot].spellid)) return; - if (IsClient() && !CastToClient()->IsDead()) - CastToClient()->MakeBuffFadePacket(buffs[slot].spellid, slot); + if (IsClient()) { + auto client = CastToClient(); + if (!client->IsDead()) { + uint32 spell_id = (buffs[slot].spellid != SPELL_SUPPRESSED) ? buffs[slot].spellid : RuleI(Spells, SuppressDebuffSpellID); + client->MakeBuffFadePacket(spell_id, slot); + } + } LogSpells("Fading buff [{}] from slot [{}]", buffs[slot].spellid, slot); @@ -4319,6 +4298,7 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) { uint16 procid = GetProcID(buffs[slot].spellid, i); RemoveDefensiveProc(procid); + break; } @@ -4387,20 +4367,24 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) case SpellEffect::Rune: { - buffs[slot].melee_rune = 0; + if (!suppress) { + buffs[slot].melee_rune = 0; + } break; } case SpellEffect::AbsorbMagicAtt: { - buffs[slot].magic_rune = 0; + if (!suppress) { + buffs[slot].magic_rune = 0; + } break; } case SpellEffect::Familiar: { Mob *mypet = GetPet(); - if (mypet){ + if (mypet && !suppress){ if(mypet->IsNPC()) mypet->CastToNPC()->Depop(); SetPetID(0); @@ -4417,7 +4401,11 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) case SpellEffect::Charm: { - if (IsNPC()) { + //Trying to restore pet ownership after suppression is too much, so just downgrade to a normal dispel + suppress = false; + + if(IsNPC()) + { CastToNPC()->RestoreGuardSpotCharm(); CastToNPC()->ModifyStatsOnCharm(true, GetOwner()); } @@ -4640,7 +4628,7 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) Mob *notify = p; if(p->IsPet()) notify = p->GetOwner(); - if(p) { + if(p && buffs[slot].spellid != SPELL_SUPPRESSED) { notify->MessageString(Chat::SpellWornOff, SPELL_WORN_OFF_OF, spells[buffs[slot].spellid].name, GetCleanName()); } @@ -4663,10 +4651,53 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) Numhits(false); } - if (spells[buffs[slot].spellid].nimbus_effect > 0) + if (IsValidSpell(buffs[slot].spellid) && spells[buffs[slot].spellid].nimbus_effect > 0) RemoveNimbusEffect(spells[buffs[slot].spellid].nimbus_effect); - buffs[slot].spellid = SPELL_UNKNOWN; + if (suppress && suppresstics > 0) { + if (buffs[slot].spellid != SPELL_SUPPRESSED) { + buffs[slot].suppressedid = buffs[slot].spellid; + buffs[slot].suppressedticsremaining = buffs[slot].ticsremaining; + buffs[slot].spellid = SPELL_SUPPRESSED; + } + buffs[slot].ticsremaining = suppresstics; + } else if (buffs[slot].spellid == SPELL_SUPPRESSED) { + buffs[slot].spellid = buffs[slot].suppressedid; + buffs[slot].ticsremaining = buffs[slot].suppressedticsremaining; + buffs[slot].suppressedid = 0; + buffs[slot].suppressedticsremaining = -1; + + if (buffs[slot].hit_number > 0) { + Numhits(true); + } + + if (IsClient()) { + //This bit of magic need to arrive before our BuffCreate. It appears to put the client in a state + //where it will be more accepting of information like 'Make this player levitate' or 'This buff has num-hit counters' + auto client = CastToClient(); + auto* action_packet = new EQApplicationPacket(OP_Action, sizeof(Action_Struct)); + auto* action = reinterpret_cast(action_packet->pBuffer); + + action->source = buffs[slot].casterid; + action->target = client->GetID(); + action->level = client->GetLevel(); + action->instrument_mod = buffs[slot].instrument_mod; + action->force = 0; + action->hit_heading = client->GetHeading(); + action->hit_pitch = 0; + action->type = 231; // 231 means a spell + action->spell = buffs[slot].spellid; + action->spell_level = buffs[slot].casterlevel; + action->effect_flag = 0x04; // Successful action + + client->QueuePacket(action_packet); + safe_delete(action_packet); + client->ReapplyBuff(slot, true); + } + } else { + buffs[slot].spellid = SPELL_UNKNOWN; + } + if(IsPet() && GetOwner() && GetOwner()->IsClient()) { SendPetBuffsToClient(); } @@ -7447,22 +7478,68 @@ bool Mob::PassLimitClass(uint32 Classes_, uint16 Class_) return false; } -void Mob::DispelMagic(Mob* caster, uint16 spell_id, int effect_value) +void Mob::DispelMagic(Mob* caster, uint16 spell_id, int effect_value, int chance, bool detrimental_only, bool beneficial_only) { - for (int slot = 0; slot < GetMaxTotalSlots(); slot++) { - if ( - buffs[slot].spellid != SPELL_UNKNOWN && - spells[buffs[slot].spellid].dispel_flag == 0 && - !IsDiscipline(buffs[slot].spellid) - ) { - if (caster && TryDispel(caster->GetCasterLevel(spell_id), buffs[slot].casterlevel, effect_value)) { - BuffFadeBySlot(slot); - break; + bool target_is_client = IsClient() || (GetUltimateOwner() && GetUltimateOwner()->IsClient()); + bool caster_is_client = caster && (caster->IsClient() || (caster->GetUltimateOwner() && caster->GetUltimateOwner()->IsClient())); + + for (int slot = 0; slot < GetMaxTotalSlots(); ++slot) { + auto s = buffs[slot].spellid; + + if (s == SPELL_UNKNOWN || spells[s].dispel_flag != 0 || IsDiscipline(s)) { + continue; + } + + if (detrimental_only && !IsDetrimentalSpell(s)) { + continue; + } + + if (beneficial_only && IsDetrimentalSpell(s)) { + continue; + } + + if (!detrimental_only && !beneficial_only) { + //This check only happens for SE_CancelMagic + if (!caster || !TryDispel(caster->GetCasterLevel(spell_id), buffs[slot].casterlevel, effect_value)) { + continue; + } + } else { + //This check only happens for SE_DispelDetrimental or SE_DispelBeneficial + if (chance < 1000 && zone->random.Int(1, 1000) > chance) { + continue; } } + + bool perma_remove = caster_is_client || !target_is_client; + if (perma_remove || !RuleB(Spells, SuppressDispels)) { + BuffFadeBySlot(slot); + break; + } else { + //Additional restrictions on which buffs can be suppressed + if (spells[s].short_buff_box) { + continue; + } + + BuffFadeBySlot(slot, true, true, RuleI(Spells, SuppressDispelsTime)); + break; + } } } +bool Mob::IsSuppressableBuff(int slot) const { + auto sid = buffs[slot].spellid; + + //Suppressed spells can be re-suppressed to refresh their timer + if (sid == SPELL_SUPPRESSED) { + return true; + } + + return IsValidSpell(sid) && + IsBeneficialSpell(sid) && + !IsBardSong(sid) && + spells[sid].dispel_flag == 0; +} + bool Mob::TrySpellEffectResist(uint16 spell_id) { /* diff --git a/zone/spells.cpp b/zone/spells.cpp index c37fa4467..c0559eca3 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -3580,10 +3580,17 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid for (buffslot = 0; buffslot < buff_count; buffslot++) { const Buffs_Struct &curbuf = buffs[buffslot]; - if (IsValidSpell(curbuf.spellid)) { + if (curbuf.spellid == SPELL_SUPPRESSED && curbuf.suppressedid == spell_id) { + LogSpells("Adding buff [{}]: Refusing to add a buff that is currently being suppressed in slot [{}]", spell_id, buffslot); + return -1; + } + + if (IsValidOrSuppressedSpell(curbuf.spellid)) { + uint16 buff_spellid = curbuf.spellid == SPELL_SUPPRESSED ? curbuf.suppressedid : curbuf.spellid; + // there's a buff in this slot ret = CheckStackConflict( - curbuf.spellid, + buff_spellid, curbuf.casterlevel, spell_id, caster_level, @@ -3596,7 +3603,7 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid LogSpells( "Adding buff [{}] failed: stacking prevented by spell [{}] in slot [{}] with caster level [{}]", spell_id, - curbuf.spellid, + buff_spellid, buffslot, curbuf.casterlevel ); @@ -3609,7 +3616,7 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid "Your {} did not take hold on {}. (Blocked by {}.)", spells[spell_id].name, GetName(), - spells[curbuf.spellid].name + spells[buff_spellid].name ).c_str() ); } @@ -3622,7 +3629,7 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid caster->GetCleanName(), spells[spell_id].name, GetName(), - spells[curbuf.spellid].name + spells[buff_spellid].name ).c_str() ); } @@ -3630,7 +3637,7 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid std::function f = [&]() { return fmt::format( "{} {}", - curbuf.spellid, + buff_spellid, spell_id ); }; @@ -3644,11 +3651,16 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid return -1; } else if (ret == 1 && !will_overwrite) { + // Reject buffing a superior buff while an inferior one is suppressed or else they end up with both + if (curbuf.spellid == SPELL_SUPPRESSED) { + return -1; + } + // set a flag to indicate that there will be overwriting LogSpells( "Adding buff [{}] will overwrite spell [{}] in slot [{}] with caster level [{}]", spell_id, - curbuf.spellid, + buff_spellid, buffslot, curbuf.casterlevel ); @@ -3665,7 +3677,7 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid LogSpells( "Adding buff [{}] will overwrite spell [{}] in slot [{}] with caster level [{}], but ResurrectionEffectBlock is set to 2. Attempting to move [{}] to an empty buff slot.", spell_id, - curbuf.spellid, + buff_spellid, buffslot, curbuf.casterlevel, spell_id @@ -6628,8 +6640,8 @@ void Mob::SendPetBuffsToClient() for(int buffslot = 0; buffslot < MaxSlots; buffslot++) { - if (IsValidSpell(buffs[buffslot].spellid)) { - pbs->spellid[buffslot] = buffs[buffslot].spellid; + if (IsValidOrSuppressedSpell(buffs[buffslot].spellid)) { + pbs->spellid[buffslot] = buffs[buffslot].spellid != SPELL_SUPPRESSED ? buffs[buffslot].spellid : RuleI(Spells, SuppressDebuffSpellID); pbs->ticsremaining[buffslot] = buffs[buffslot].ticsremaining; PetBuffCount++; } @@ -6667,7 +6679,7 @@ EQApplicationPacket *Mob::MakeBuffsPacket(bool for_target, bool clear_buffs) } for(int i = 0; i < buff_count; ++i) { - if (IsValidSpell(buffs[i].spellid)) { + if (IsValidOrSuppressedSpell(buffs[i].spellid)) { ++count; } } @@ -6704,6 +6716,20 @@ EQApplicationPacket *Mob::MakeBuffsPacket(bool for_target, bool clear_buffs) strn0cpy(buff->entries[index].caster, buffs[i].caster_name, 64); buff->name_lengths += strlen(buff->entries[index].caster); ++index; + + continue; + } + + if (buffs[i].spellid == SPELL_SUPPRESSED) { + buff->entries[index].buff_slot = i; + buff->entries[index].spell_id = RuleI(Spells, SuppressDebuffSpellID); + buff->entries[index].tics_remaining = buffs[i].ticsremaining; + buff->entries[index].num_hits = 0; + strn0cpy(buff->entries[index].caster, buffs[i].caster_name, 64); + buff->name_lengths += strlen(buff->entries[index].caster); + ++index; + + continue; } } diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 0dc143675..a5271442d 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -2897,7 +2897,7 @@ void ZoneDatabase::SaveBuffs(Client *client) uint32 character_buff_count = 0; for (int slot_id = 0; slot_id < max_buff_slots; slot_id++) { - if (!IsValidSpell(buffs[slot_id].spellid)) { + if (!IsValidOrSuppressedSpell(buffs[slot_id].spellid)) { continue; } @@ -2907,16 +2907,18 @@ void ZoneDatabase::SaveBuffs(Client *client) v.reserve(character_buff_count); for (int slot_id = 0; slot_id < max_buff_slots; slot_id++) { - if (!IsValidSpell(buffs[slot_id].spellid)) { + if (!IsValidOrSuppressedSpell(buffs[slot_id].spellid)) { continue; } + bool suppressed = buffs[slot_id].spellid == SPELL_SUPPRESSED; + e.character_id = client->CharacterID(); e.slot_id = slot_id; - e.spell_id = buffs[slot_id].spellid; + e.spell_id = suppressed ? buffs[slot_id].suppressedid : buffs[slot_id].spellid; e.caster_level = buffs[slot_id].casterlevel; e.caster_name = buffs[slot_id].caster_name; - e.ticsremaining = buffs[slot_id].ticsremaining; + e.ticsremaining = suppressed ? buffs[slot_id].suppressedticsremaining : buffs[slot_id].ticsremaining; e.counters = buffs[slot_id].counters; e.numhits = buffs[slot_id].hit_number; e.melee_rune = buffs[slot_id].melee_rune; @@ -2999,6 +3001,8 @@ void ZoneDatabase::LoadBuffs(Client *client) buffs[e.slot_id].virus_spread_time = 0; buffs[e.slot_id].UpdateClient = false; buffs[e.slot_id].instrument_mod = e.instrument_mod; + buffs[e.slot_id].suppressedid = 0; + buffs[e.slot_id].suppressedticsremaining = -1; } // We load up to the most our client supports