diff --git a/common/spdat.cpp b/common/spdat.cpp index 486a5a499..08530df80 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -1462,6 +1462,23 @@ bool IsInstrumentModAppliedToSpellEffect(int32 spell_id, int effect) //Allowing anything not confirmed to be restricted / allowed to receive modifiers, as to not inhbit anyone making custom bard songs. } +bool IsPulsingBardSong(int32 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + if (spells[spell_id].buff_duration == 0xFFFF || + spells[spell_id].recast_time> 0 || + spells[spell_id].mana > 0 || + IsEffectInSpell(spell_id, SE_TemporaryPets) || + IsEffectInSpell(spell_id, SE_Familiar)) { + return false; + } + + return true; +} + int GetSpellStatValue(uint32 spell_id, const char* stat_identifier, uint8 slot) { if (!IsValidSpell(spell_id)) diff --git a/common/spdat.h b/common/spdat.h index b0aba49f5..7e0582f91 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -179,8 +179,6 @@ #define SPELLGROUP_FURIOUS_RAMPAGE 38106 #define SPELLGROUP_SHROUD_OF_PRAYER 41050 - - #define EFFECT_COUNT 12 #define MAX_SPELL_TRIGGER 12 // One for each slot(only 6 for AA since AA use 2) #define MAX_RESISTABLE_EFFECTS 12 // Number of effects that are typcially checked agianst resists. @@ -193,6 +191,12 @@ #define MAX_APPEARANCE_EFFECTS 20 //Up to 20 Appearance Effects can be saved to a mobs appearance effect array, these will be sent to other clients when they enter a zone (This is arbitrary) #define MAX_CAST_ON_SKILL_USE 36 //Actual amount is MAX/3 +//instrument item id's used as song components +#define INSTRUMENT_HAND_DRUM 13000 +#define INSTRUMENT_WOODEN_FLUTE 13001 +#define INSTRUMENT_LUTE 13011 +#define INSTRUMENT_HORN 13012 + const int Z_AGGRO=10; @@ -1536,9 +1540,9 @@ int GetViralMinSpreadTime(int32 spell_id); int GetViralMaxSpreadTime(int32 spell_id); int GetViralSpreadRange(int32 spell_id); bool IsInstrumentModAppliedToSpellEffect(int32 spell_id, int effect); +bool IsPulsingBardSong(int32 spell_id); uint32 GetProcLimitTimer(int32 spell_id, int proc_type); bool IgnoreCastingRestriction(int32 spell_id); - int CalcPetHp(int levelb, int classb, int STA = 75); int GetSpellEffectDescNum(uint16 spell_id); DmgShieldType GetDamageShieldType(uint16 spell_id, int32 DSType = 0); diff --git a/zone/aa.cpp b/zone/aa.cpp index 84b78d4f8..f455bfd53 100644 --- a/zone/aa.cpp +++ b/zone/aa.cpp @@ -1311,8 +1311,11 @@ void Client::ActivateAlternateAdvancementAbility(int rank_id, int target_id) { TogglePassiveAlternativeAdvancement(*rank, ability->id); } else { - // Bards can cast instant cast AAs while they are casting another song - if (spells[rank->spell].cast_time == 0 && GetClass() == BARD && IsBardSong(casting_spell_id)) { + // Bards can cast instant cast AAs while they are casting or channeling item cast. + if (GetClass() == BARD && IsCasting() && spells[rank->spell].cast_time == 0) { + if (!DoCastingChecksOnCaster(rank->spell)) { + return; + } if (!SpellFinished(rank->spell, entity_list.GetMob(target_id), EQ::spells::CastingSlot::AltAbility, spells[rank->spell].mana, -1, spells[rank->spell].resist_difficulty, false)) { return; } diff --git a/zone/aggro.cpp b/zone/aggro.cpp index 660bc95c9..7d3b81554 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -660,6 +660,9 @@ bool Mob::IsAttackAllowed(Mob *target, bool isSpellAttack) if (target->GetSpecialAbility(IMMUNE_DAMAGE_NPC) && IsNPC()) return false; + if (target->IsHorse()) + return false; + // can't damage own pet (applies to everthing) Mob *target_owner = target->GetOwner(); Mob *our_owner = GetOwner(); diff --git a/zone/api_service.cpp b/zone/api_service.cpp index 60f36418b..109ef3de8 100644 --- a/zone/api_service.cpp +++ b/zone/api_service.cpp @@ -436,7 +436,6 @@ Json::Value ApiGetMobListDetail(EQ::Net::WebsocketServerConnection *connection, row["cwp"] = mob->GetCWP(); row["cwpp"] = mob->GetCWPP(); row["divine_aura"] = mob->DivineAura(); - row["do_casting_checks"] = mob->DoCastingChecks(); row["dont_buff_me_before"] = mob->DontBuffMeBefore(); row["dont_cure_me_before"] = mob->DontCureMeBefore(); row["dont_dot_me_before"] = mob->DontDotMeBefore(); diff --git a/zone/bot.cpp b/zone/bot.cpp index c2b6b2d59..32599248c 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -7349,7 +7349,7 @@ void Bot::GenerateSpecialAttacks() { bool Bot::DoFinishedSpellAETarget(uint16 spell_id, Mob* spellTarget, EQ::spells::CastingSlot slot, bool& stopLogic) { if(GetClass() == BARD) { - if(!ApplyNextBardPulse(bardsong, this, bardsong_slot)) + if(!ApplyBardPulse(bardsong, this, bardsong_slot)) InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); stopLogic = true; diff --git a/zone/client.h b/zone/client.h index 4962d1e8f..f30f9add7 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1491,7 +1491,10 @@ public: void LeaveRaidXTargets(Raid *r); bool GroupFollow(Client* inviter); inline bool GetRunMode() const { return runmode; } - void SendItemRecastTimer(uint32 recast_type, uint32 recast_delay = 0); + + void SendItemRecastTimer(int32 recast_type, uint32 recast_delay = 0); + void SetItemRecastTimer(int32 spell_id, uint32 inventory_slot); + bool HasItemRecastTimer(int32 spell_id, uint32 inventory_slot); inline bool AggroMeterAvailable() const { return ((m_ClientVersionBit & EQ::versions::maskRoF2AndLater)) && RuleB(Character, EnableAggroMeter); } // RoF untested inline void SetAggroMeterLock(int in) { m_aggrometer.set_lock_id(in); } diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 9a532362d..bab86a465 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -4053,7 +4053,6 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app) { EQ::ItemInstance* p_inst = (EQ::ItemInstance*)inst; int i = parse->EventItem(EVENT_ITEM_CLICK_CAST, this, p_inst, nullptr, "", castspell->inventoryslot); - if (i == 0) { CastSpell(item->Click.Effect, castspell->target_id, slot, item->CastTime, 0, 0, castspell->inventoryslot); } @@ -8791,6 +8790,7 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) } spell_id = item->Click.Effect; + bool is_casting_bard_song = false; if ( @@ -8812,10 +8812,20 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) ) ) { - SendSpellBarEnable(spell_id); - return; + /* + Bards on live can click items while casting spell gems, it stops that song cast and replaces it with item click cast. + Can not click while casting other items. + */ + if (GetClass() == BARD && IsCasting() && casting_spell_slot < CastingSlot::MaxGems) + { + is_casting_bard_song = true; + } + else + { + SendSpellBarEnable(spell_id); + return; + } } - // Modern clients don't require pet targeted for item clicks that are ST_Pet if (spell_id > 0 && (spells[spell_id].target_type == ST_Pet || spells[spell_id].target_type == ST_SummonedPet)) target_id = GetPetID(); @@ -8903,11 +8913,16 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) { return; } - if (i == 0) { - if (!IsCastWhileInvis(item->Click.Effect)) + if (!IsCastWhileInvis(item->Click.Effect)) { CommonBreakInvisible(); // client can't do this for us :( - CastSpell(item->Click.Effect, target_id, CastingSlot::Item, item->CastTime, 0, 0, slot_id); + } + if (GetClass() == BARD){ + DoBardCastingFromItemClick(is_casting_bard_song, item->CastTime, item->Click.Effect, target_id, CastingSlot::Item, slot_id, item->RecastType, item->RecastDelay); + } + else { + CastSpell(item->Click.Effect, target_id, CastingSlot::Item, item->CastTime, 0, 0, slot_id); + } } } else @@ -8944,9 +8959,15 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) } if (i == 0) { - if (!IsCastWhileInvis(augitem->Click.Effect)) + if (!IsCastWhileInvis(augitem->Click.Effect)) { CommonBreakInvisible(); // client can't do this for us :( - CastSpell(augitem->Click.Effect, target_id, CastingSlot::Item, augitem->CastTime, 0, 0, slot_id); + } + if (GetClass() == BARD) { + DoBardCastingFromItemClick(is_casting_bard_song, augitem->CastTime, augitem->Click.Effect, target_id, CastingSlot::Item, slot_id, augitem->RecastType, augitem->RecastDelay); + } + else { + CastSpell(augitem->Click.Effect, target_id, CastingSlot::Item, augitem->CastTime, 0, 0, slot_id); + } } } else @@ -9552,16 +9573,20 @@ void Client::Handle_OP_ManaChange(const EQApplicationPacket *app) { if (app->size == 0) { // i think thats the sign to stop the songs - if (IsBardSong(casting_spell_id) || bardsong != 0) - InterruptSpell(SONG_ENDS, 0x121); - else + if (IsBardSong(casting_spell_id) || HasActiveSong()) { + InterruptSpell(SONG_ENDS, 0x121); //Live doesn't send song end message anymore (~Kayen 1/26/22) + } + else { InterruptSpell(INTERRUPT_SPELL, 0x121); - + } return; } - else // I don't think the client sends proper manachanges - { // with a length, just the 0 len ones for stopping songs - //ManaChange_Struct* p = (ManaChange_Struct*)app->pBuffer; + /* + I don't think the client sends proper manachanges + with a length, just the 0 len ones for stopping songs + ManaChange_Struct* p = (ManaChange_Struct*)app->pBuffer; + */ + else{ printf("OP_ManaChange from client:\n"); DumpPacket(app); } diff --git a/zone/client_process.cpp b/zone/client_process.cpp index e96e2ac02..454787517 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -238,9 +238,9 @@ bool Client::Process() { InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); } else { - if (!ApplyNextBardPulse(bardsong, song_target, bardsong_slot)) + if (!ApplyBardPulse(bardsong, song_target, bardsong_slot)) { InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); - //SpellFinished(bardsong, bardsong_target, bardsong_slot, spells[bardsong].mana); + } } } diff --git a/zone/effects.cpp b/zone/effects.cpp index 80b48fc0f..24f9afd52 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -821,22 +821,24 @@ bool Client::UseDiscipline(uint32 spell_id, uint32 target) { instant_recast = false; if (GetClass() == BARD && IsCasting() && spells[spell_id].cast_time == 0) { - if (DoCastingChecks(spell_id, target)) { - SpellFinished(spell_id, entity_list.GetMob(target), EQ::spells::CastingSlot::Discipline, 0, -1, spells[spell_id].resist_difficulty, false, -1, (uint32)DiscTimer, reduced_recast); + if (DoCastingChecksOnCaster(spell_id)) { + if (SpellFinished(spell_id, entity_list.GetMob(target), EQ::spells::CastingSlot::Discipline, 0, -1, spells[spell_id].resist_difficulty, false, -1, (uint32)DiscTimer, reduced_recast, false)) { + SendDisciplineTimer(spells[spell_id].timer_id, reduced_recast); + } } } else { - CastSpell(spell_id, target, EQ::spells::CastingSlot::Discipline, -1, -1, 0, -1, (uint32)DiscTimer, reduced_recast); + if (CastSpell(spell_id, target, EQ::spells::CastingSlot::Discipline, -1, -1, 0, -1, (uint32)DiscTimer, reduced_recast)) { + SendDisciplineTimer(spells[spell_id].timer_id, reduced_recast); + } } - - SendDisciplineTimer(spells[spell_id].timer_id, reduced_recast); } } if (instant_recast) { if (GetClass() == BARD && IsCasting() && spells[spell_id].cast_time == 0) { - if (DoCastingChecks(spell_id, target)) { - SpellFinished(spell_id, entity_list.GetMob(target), EQ::spells::CastingSlot::Discipline); + if (DoCastingChecksOnCaster(spell_id)) { + SpellFinished(spell_id, entity_list.GetMob(target), EQ::spells::CastingSlot::Discipline, 0, -1, spells[spell_id].resist_difficulty, false, -1, 0xFFFFFFFF, 0, false); } } else { @@ -1194,90 +1196,6 @@ void EntityList::MassGroupBuff( } } -/** - * Causes caster to hit every mob within dist range of center with a bard pulse of spell_id - * NPC spells will only affect other NPCs with compatible faction - * - * @param caster - * @param center - * @param spell_id - * @param affect_caster - */ -void EntityList::AEBardPulse( - Mob *caster, - Mob *center, - uint16 spell_id, - bool affect_caster) -{ - Mob *current_mob = nullptr; - float distance = caster->GetAOERange(spell_id); - float distance_squared = distance * distance; - bool is_detrimental_spell = IsDetrimentalSpell(spell_id); - bool is_npc = caster->IsNPC(); - - for (auto &it : entity_list.GetCloseMobList(caster, distance)) { - current_mob = it.second; - - /** - * Skip self - */ - if (current_mob == center) { - continue; - } - - if (current_mob == caster && !affect_caster) { - continue; - } - - if (DistanceSquared(center->GetPosition(), current_mob->GetPosition()) > distance_squared) { //make sure they are in range - continue; - } - - /** - * check npc->npc casting - */ - if (is_npc && current_mob->IsNPC()) { - FACTION_VALUE faction = current_mob->GetReverseFactionCon(caster); - if (is_detrimental_spell) { - //affect mobs that are on our hate list, or - //which have bad faction with us - if (!(caster->CheckAggro(current_mob) || faction == FACTION_THREATENINGLY || faction == FACTION_SCOWLS)) { - continue; - } - } - else { - //only affect mobs we would assist. - if (!(faction <= FACTION_AMIABLY)) { - continue; - } - } - } - - /** - * LOS - */ - if (is_detrimental_spell) { - if (!center->CheckLosFN(current_mob)) { - continue; - } - } - else { // check to stop casting beneficial ae buffs (to wit: bard songs) on enemies... - // See notes in AESpell() above for more info. - if (caster->IsAttackAllowed(current_mob, true)) { - continue; - } - if (caster->CheckAggro(current_mob)) { - continue; - } - } - - current_mob->BardPulse(spell_id, caster); - } - if (caster->IsClient()) { - caster->CastToClient()->CheckSongSkillIncrease(spell_id); - } -} - /** * Rampage - Normal and Duration rampages * NPCs handle it differently in Mob::Rampage diff --git a/zone/entity.h b/zone/entity.h index c92a3d4f5..a743a18b0 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -422,7 +422,6 @@ public: int *max_targets = nullptr ); void MassGroupBuff(Mob *caster, Mob *center, uint16 spell_id, bool affect_caster = true); - void AEBardPulse(Mob *caster, Mob *center, uint16 spell_id, bool affect_caster = true); //trap stuff Mob* GetTrapTrigger(Trap* trap); diff --git a/zone/groups.cpp b/zone/groups.cpp index aa4fc857a..40110b10e 100644 --- a/zone/groups.cpp +++ b/zone/groups.cpp @@ -847,42 +847,6 @@ void Group::CastGroupSpell(Mob* caster, uint16 spell_id) { disbandcheck = true; } -// does the caster + group -void Group::GroupBardPulse(Mob* caster, uint16 spell_id) { - uint32 z; - float range, distance; - - if(!caster) - return; - - castspell = true; - range = caster->GetAOERange(spell_id); - - float range2 = range*range; - - for(z=0; z < MAX_GROUP_MEMBERS; z++) { - if(members[z] == caster) { - caster->BardPulse(spell_id, caster); -#ifdef GROUP_BUFF_PETS - if(caster->GetPet() && caster->HasPetAffinity() && !caster->GetPet()->IsCharmed()) - caster->BardPulse(spell_id, caster->GetPet()); -#endif - } - else if(members[z] != nullptr) - { - distance = DistanceSquared(caster->GetPosition(), members[z]->GetPosition()); - if(distance <= range2) { - members[z]->BardPulse(spell_id, caster); -#ifdef GROUP_BUFF_PETS - if(members[z]->GetPet() && members[z]->HasPetAffinity() && !members[z]->GetPet()->IsCharmed()) - members[z]->GetPet()->BardPulse(spell_id, caster); -#endif - } else - LogSpells("Group bard pulse: [{}] is out of range [{}] at distance [{}] from [{}]", members[z]->GetName(), range, distance, caster->GetName()); - } - } -} - bool Group::IsGroupMember(Mob* client) { bool Result = false; diff --git a/zone/groups.h b/zone/groups.h index 57dfdbfb6..1a90b13ae 100644 --- a/zone/groups.h +++ b/zone/groups.h @@ -71,7 +71,6 @@ public: bool IsGroup() { return true; } void SendGroupJoinOOZ(Mob* NewMember); void CastGroupSpell(Mob* caster,uint16 spellid); - void GroupBardPulse(Mob* caster,uint16 spellid); void SplitExp(uint32 exp, Mob* other); void GroupMessage(Mob* sender,uint8 language,uint8 lang_skill,const char* message); void GroupMessageString(Mob* sender, uint32 type, uint32 string_id, const char* message,const char* message2=0,const char* message3=0,const char* message4=0,const char* message5=0,const char* message6=0,const char* message7=0,const char* message8=0,const char* message9=0, uint32 distance = 0); diff --git a/zone/mob.cpp b/zone/mob.cpp index d16182eae..d6f56a8db 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -4465,14 +4465,15 @@ void Mob::TryTwincast(Mob *caster, Mob *target, uint32 spell_id) } //Used for effects that should occur after the completion of the spell -void Mob::TryOnSpellFinished(Mob *caster, Mob *target, uint16 spell_id) +void Mob::ApplyHealthTransferDamage(Mob *caster, Mob *target, uint16 spell_id) { if (!IsValidSpell(spell_id)) return; - /*Apply damage from Lifeburn type effects on caster at end of spell cast. - This allows for the AE spells to function without repeatedly killing caster - Damage or heal portion can be found as regular single use spell effect + /* + Apply damage from Lifeburn type effects on caster at end of spell cast. + This allows for the AE spells to function without repeatedly killing caster + Damage or heal portion can be found as regular single use spell effect */ if (IsEffectInSpell(spell_id, SE_Health_Transfer)){ for (int i = 0; i < EFFECT_COUNT; i++) { @@ -4481,10 +4482,12 @@ void Mob::TryOnSpellFinished(Mob *caster, Mob *target, uint16 spell_id) int new_hp = GetMaxHP(); new_hp -= GetMaxHP() * spells[spell_id].base_value[i] / 1000; - if (new_hp > 0) + if (new_hp > 0) { SetHP(new_hp); - else + } + else { Kill(); + } } } } diff --git a/zone/mob.h b/zone/mob.h index 45b895dea..f0af8f8b9 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -285,11 +285,6 @@ public: void SetInvisible(uint8 state); void SetMobTextureProfile(uint8 material_slot, uint16 texture, uint32 color = 0, uint32 hero_forge_model = 0); - //Song - bool UseBardSpellLogic(uint16 spell_id = 0xffff, int slot = -1); - bool ApplyNextBardPulse(uint16 spell_id, Mob *spell_target, EQ::spells::CastingSlot slot); - void BardPulse(uint16 spell_id, Mob *caster); - //Spell void SendSpellEffect(uint32 effect_id, uint32 duration, uint32 finish_delay, bool zone_wide, uint32 unk020, bool perm_effect = false, Client *c = nullptr, uint32 caster_id = 0, uint32 target_id = 0); @@ -331,13 +326,16 @@ public: void CastedSpellFinished(uint16 spell_id, uint32 target_id, EQ::spells::CastingSlot slot, uint16 mana_used, uint32 inventory_slot = 0xFFFFFFFF, int16 resist_adjust = 0); bool SpellFinished(uint16 spell_id, Mob *target, EQ::spells::CastingSlot slot = EQ::spells::CastingSlot::Item, uint16 mana_used = 0, - uint32 inventory_slot = 0xFFFFFFFF, int16 resist_adjust = 0, bool isproc = false, int level_override = -1, uint32 timer = 0xFFFFFFFF, uint32 timer_duration = 0); + uint32 inventory_slot = 0xFFFFFFFF, int16 resist_adjust = 0, bool isproc = false, int level_override = -1, uint32 timer = 0xFFFFFFFF, uint32 timer_duration = 0, bool from_casted_spell = false); void SendBeginCast(uint16 spell_id, uint32 casttime); virtual bool SpellOnTarget(uint16 spell_id, Mob* spelltar, int reflect_effectiveness = 0, bool use_resist_adjust = false, int16 resist_adjust = 0, bool isproc = false, int level_override = -1, int32 duration_override = 0); virtual bool SpellEffect(Mob* caster, uint16 spell_id, float partial = 100, int level_override = -1, int reflect_effectiveness = 0, int32 duration_override = 0); virtual bool DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, EQ::spells::CastingSlot slot, bool isproc = false); + bool DoCastingChecksOnCaster(int32 spell_id); + bool DoCastingChecksZoneRestrictions(bool check_on_casting, int32 spell_id); + bool DoCastingChecksOnTarget(bool check_on_casting, int32 spell_id, Mob* spell_target); virtual bool CheckFizzle(uint16 spell_id); virtual bool CheckSpellLevelRestriction(uint16 spell_id); virtual bool IsImmuneToSpell(uint16 spell_id, Mob *caster); @@ -345,9 +343,9 @@ public: void InterruptSpell(uint16 spellid = SPELL_UNKNOWN); void InterruptSpell(uint16, uint16, uint16 spellid = SPELL_UNKNOWN); void StopCasting(); + void StopCastSpell(int32 spell_id, bool send_spellbar_enable); inline bool IsCasting() const { return((casting_spell_id != 0)); } uint16 CastingSpellID() const { return casting_spell_id; } - bool DoCastingChecks(int32 spell_id = SPELL_UNKNOWN, uint16 target_id = 0); bool TryDispel(uint8 caster_level, uint8 buff_level, int level_modifier); bool TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed = 1.5f); void ResourceTap(int32 damage, uint16 spell_id); @@ -355,10 +353,19 @@ public: void CalcDestFromHeading(float heading, float distance, float MaxZDiff, float StartX, float StartY, float &dX, float &dY, float &dZ); void BeamDirectional(uint16 spell_id, int16 resist_adjust); void ConeDirectional(uint16 spell_id, int16 resist_adjust); - void TryOnSpellFinished(Mob *caster, Mob *target, uint16 spell_id); + void ApplyHealthTransferDamage(Mob *caster, Mob *target, uint16 spell_id); void ApplySpellEffectIllusion(int32 spell_id, Mob* caster, int buffslot, int base, int limit, int max); void ApplyIllusionToCorpse(int32 spell_id, Corpse* new_corpse); void SendIllusionWearChange(Client* c); + + //Bard + bool ApplyBardPulse(int32 spell_id, Mob *spell_target, EQ::spells::CastingSlot slot); + bool IsActiveBardSong(int32 spell_id); + bool HasActiveSong() const { return(bardsong != 0); } + void ZeroBardPulseVars(); + void DoBardCastingFromItemClick(bool is_casting_bard_song, uint32 cast_time, int32 spell_id, uint16 target_id, EQ::spells::CastingSlot slot, uint32 item_slot, + uint32 recast_type , uint32 recast_delay); + bool UseBardSpellLogic(uint16 spell_id = 0xffff, int slot = -1); //Buff void BuffProcess(); @@ -847,6 +854,7 @@ public: int32 GetExtraSpellAmt(uint16 spell_id, int32 extra_spell_amt, int32 base_spell_dmg); void MeleeLifeTap(int32 damage); bool PassCastRestriction(int value); + void SendCastRestrictionMessage(int requirement_id, bool is_target_requirement = true, bool is_discipline = false); bool ImprovedTaunt(); bool TryRootFadeByDamage(int buffslot, Mob* attacker); float GetSlowMitigation() const { return slow_mitigation; } @@ -1126,7 +1134,6 @@ public: void InstillDoubt(Mob *who); int16 GetResist(uint8 type) const; - bool HasActiveSong() const { return(bardsong != 0); } bool Charmed() const { return typeofpet == petCharmed; } static uint32 GetLevelHP(uint8 tlevel); uint32 GetZoneID() const; //for perl @@ -1735,7 +1742,6 @@ protected: MobMovementManager *mMovementManager; private: - void _StopSong(); //this is not what you think it is Mob* target; diff --git a/zone/raids.cpp b/zone/raids.cpp index ef883b917..712d0a1d5 100644 --- a/zone/raids.cpp +++ b/zone/raids.cpp @@ -827,42 +827,6 @@ void Raid::SplitMoney(uint32 gid, uint32 copper, uint32 silver, uint32 gold, uin } } -void Raid::GroupBardPulse(Mob* caster, uint16 spellid, uint32 gid){ - uint32 z; - float range, distance; - - if(!caster) - return; - - range = caster->GetAOERange(spellid); - - float range2 = range*range; - - for(z=0; z < MAX_RAID_MEMBERS; z++) { - if(members[z].member == caster) { - caster->BardPulse(spellid, caster); -#ifdef GROUP_BUFF_PETS - if(caster->GetPet() && caster->HasPetAffinity() && !caster->GetPet()->IsCharmed()) - caster->BardPulse(spellid, caster->GetPet()); -#endif - } - else if(members[z].member != nullptr) - { - if(members[z].GroupNumber == gid){ - distance = DistanceSquared(caster->GetPosition(), members[z].member->GetPosition()); - if(distance <= range2) { - members[z].member->BardPulse(spellid, caster); -#ifdef GROUP_BUFF_PETS - if(members[z].member->GetPet() && members[z].member->HasPetAffinity() && !members[z].member->GetPet()->IsCharmed()) - members[z].member->GetPet()->BardPulse(spellid, caster); -#endif - } else - LogSpells("Group bard pulse: [{}] is out of range [{}] at distance [{}] from [{}]", members[z].member->GetName(), range, distance, caster->GetName()); - } - } - } -} - void Raid::TeleportGroup(Mob* sender, uint32 zoneID, uint16 instance_id, float x, float y, float z, float heading, uint32 gid) { for(int i = 0; i < MAX_RAID_MEMBERS; i++) diff --git a/zone/raids.h b/zone/raids.h index a04d1eb54..e1d1cadb3 100644 --- a/zone/raids.h +++ b/zone/raids.h @@ -160,7 +160,6 @@ public: void BalanceMana(int32 penalty, uint32 gid, float range = 0, Mob* caster = nullptr, int32 limit = 0); void HealGroup(uint32 heal_amt, Mob* caster, uint32 gid, float range = 0); void SplitMoney(uint32 gid, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, Client *splitter = nullptr); - void GroupBardPulse(Mob* caster, uint16 spellid, uint32 gid); void TeleportGroup(Mob* sender, uint32 zoneID, uint16 instance_id, float x, float y, float z, float heading, uint32 gid); void TeleportRaid(Mob* sender, uint32 zoneID, uint16 instance_id, float x, float y, float z, float heading); diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 6a860c1b3..ff09b955d 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -2834,16 +2834,20 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove } break; } - /*Calc for base1 is found in TryOnSpellFinished() due to needing to account for AOE functionality - since effect can potentially kill caster*/ + /* + Calc for base1 is found in ApplyHealthTransferDamage() due to needing to account for AOE functionality + since effect can potentially kill caster. + */ case SE_Health_Transfer: { effect_value = spells[spell_id].limit_value[i]; int32 amt = abs(caster->GetMaxHP() * effect_value / 1000); - if (effect_value < 0) + if (effect_value < 0) { Damage(caster, amt, spell_id, spell.skill, false, buffslot, false); - else + } + else { HealDamage(amt, caster); + } break; } @@ -8189,6 +8193,1098 @@ bool Mob::PassCastRestriction(int value) return false; } +void Mob::SendCastRestrictionMessage(int requirement_id, bool target_requirement, bool is_discipline) { + + if (!IsClient()) { + return; + } + /* + Most of these are the live messages that modern clients give. Current live dbstr_us type 39 + Having these messages display greatly enhances the useabllity of these fields. (CastRestriction=target, caster_requirement_id=caster) + If target_requirement is false then use the caster requirement message. + Added support for different messages for certain high yield restrictions based on if + target or caster requirements. + + */ + + std::string msg = ""; + + if (target_requirement) { + msg = "Your target does not meet the spell requirements. "; + } + else { + if (is_discipline) { + msg = "Your combat ability is interrupted. "; + } + else { + msg = "Your spell is interrupted. "; + } + } + + switch (requirement_id) + { + case IS_ANIMAL_OR_HUMANOID: + Message(Chat::Red, "%s This spell will only work on animals or humanoid creatures.", msg); + break; + case IS_DRAGON: + Message(Chat::Red, "%s This spell will only work on dragons.", msg); + break; + case IS_ANIMAL_OR_INSECT: + Message(Chat::Red, "%s This spell will only work on animals or insects.", msg); + break; + case IS_BODY_TYPE_MISC: + Message(Chat::Red, "%s This spell will only work on humanoids, lycanthropes, giants, Kael Drakkel giants, Coldain, animals, insects, constructs, dragons, Skyshrine dragons, Muramites, or creatures constructed from magic.", msg); + break; + case IS_BODY_TYPE_MISC2: + Message(Chat::Red, "%s This spell will only work on humanoids, lycanthropes, giants, Kael Drakkel giants, Coldain, animals, or insects.", msg); + break; + case IS_PLANT: + Message(Chat::Red, "%s This spell will only work on plants.", msg); + break; + case IS_GIANT: + Message(Chat::Red, "%s This spell will only work on animals.", msg); + break; + case IS_NOT_ANIMAL_OR_HUMANOID: + Message(Chat::Red, "%s This spell will only work on targets that are neither animals or humanoid.", msg); + break; + case IS_BIXIE: + Message(Chat::Red, "%s This spell will only work on bixies.", msg); + break; + case IS_HARPY: + Message(Chat::Red, "%s This spell will only work on harpies.", msg); + break; + case IS_GNOLL: + Message(Chat::Red, "%s This spell will only work on gnolls.", msg); + break; + case IS_SPORALI: + Message(Chat::Red, "%s This spell will only work on fungusoids.", msg); + break; + case IS_KOBOLD: + Message(Chat::Red, "%s This spell will only work on kobolds.", msg); + break; + case IS_FROSTCRYPT_SHADE: + Message(Chat::Red, "%s This spell will only work on undead creatures or the Shades of Frostcrypt.", msg); + break; + case IS_DRAKKIN: + Message(Chat::Red, "%s This spell will only work on Drakkin.", msg); + break; + case IS_UNDEAD_OR_VALDEHOLM_GIANT: + Message(Chat::Red, "%s This spell will only work on undead creatures or the inhabitants of Valdeholm.", msg); + break; + case IS_ANIMAL_OR_PLANT: + Message(Chat::Red, "%s This spell will only work on plants or animals.", msg); + break; + case IS_SUMMONED: + Message(Chat::Red, "%s This spell will only work on constructs, elementals, or summoned elemental minions.", msg); + break; + case IS_WIZARD_USED_ON_MAGE_FIRE_PET: + Message(Chat::Red, "%s This spell will only work on wizards.", msg); + break; + case IS_UNDEAD: + Message(Chat::Red, "%s This spell will only work on undead.", msg); + break; + case IS_NOT_UNDEAD_OR_SUMMONED_OR_VAMPIRE: + Message(Chat::Red, "%s This spell will only work on creatures that are not undead, constructs, elementals, or vampires.", msg); + break; + case IS_FAE_OR_PIXIE: + Message(Chat::Red, "%s This spell will only work on Fae or pixies.", msg); + break; + case IS_HUMANOID: + Message(Chat::Red, "%s This spell will only work on humanoids.", msg); + break; + case IS_UNDEAD_AND_HP_LESS_THAN_10_PCT: + Message(Chat::Red, "%s The Essence Extractor whirrs but does not light up.", msg); + break; + case IS_CLOCKWORK_AND_HP_LESS_THAN_45_PCT: + Message(Chat::Red, "%s This spell will only work on clockwork gnomes.", msg); + break; + case IS_WISP_AND_HP_LESS_THAN_10_PCT: + Message(Chat::Red, "%s This spell will only work on wisps at or below 10 pct of their maximum HP.", msg); + break; + case IS_CLASS_MELEE_THAT_CAN_BASH_OR_KICK_EXCEPT_BARD: + Message(Chat::Red, "%s This spell will only work on non-bard targets that can bash or kick.", msg); + break; + case IS_CLASS_PURE_MELEE: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect melee classes (warriors, monks, rogues, and berserkers).", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by melee classes (warriors, monks, rogues, and berserkers).", msg); + } + break; + case IS_CLASS_PURE_CASTER: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect pure caster classes (necromancers, wizards, magicians, and enchanters).", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by pure caster classes (necromancers, wizards, magicians, and enchanters).", msg); + } + break; + case IS_CLASS_HYBRID_CLASS: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect hybrid classes (paladins, rangers, shadow knights, bards, and beastlords).", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by hybrid classes (paladins, rangers, shadow knights, bards, and beastlords).", msg); + } + break; + case IS_CLASS_WARRIOR: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Warriors.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Warriors.", msg); + } + break; + case IS_CLASS_CLERIC: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Clerics.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Clerics.", msg); + } + break; + case IS_CLASS_PALADIN: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Paladins.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Paladins.", msg); + } + break; + case IS_CLASS_RANGER: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Warriors.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Warriors.", msg); + } + break; + case IS_CLASS_SHADOWKNIGHT: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Shadow Knights.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Shadow Knights.", msg); + } + break; + case IS_CLASS_DRUID: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Druids.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Druids.", msg); + } + break; + case IS_CLASS_MONK: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Monks.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Monks.", msg); + } + break; + case IS_CLASS_BARD: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Bards.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Bards..", msg); + } + break; + case IS_CLASS_ROGUE: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Rogues.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Rogues.", msg); + } + break; + case IS_CLASS_SHAMAN: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Shamans.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Shamans.", msg); + } + break; + case IS_CLASS_NECRO: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Necromancers.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Necromancers.", msg); + } + break; + case IS_CLASS_WIZARD: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Wizards.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Wizards.", msg); + } + break; + case IS_CLASS_MAGE: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Magicians.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Magicians.", msg); + } + break; + case IS_CLASS_ENCHANTER: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Enchanters.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Enchanters.", msg); + } + break; + case IS_CLASS_BEASTLORD: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Beastlords.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Beastlords.", msg); + } + break; + case IS_CLASS_BERSERKER: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Berserkers.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Berserkers.", msg); + } + break; + case IS_CLASS_CLR_SHM_DRU: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect priest classes (clerics, druids, and shaman).", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by priest classes (clerics, druids, and shaman).", msg); + } + break; + case IS_CLASS_NOT_WAR_PAL_SK: + if (target_requirement) { + Message(Chat::Red, "%s This spell will not affect Warriors, Paladins, or Shadow Knights.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Warriors, Paladins, or Shadow Knights.", msg); + } + break; + case IS_LEVEL_UNDER_100: + Message(Chat::Red, "%s This spell will not affect any target over level 100.", msg); + break; + case IS_NOT_RAID_BOSS: + Message(Chat::Red, "%s This spell will not affect raid bosses.", msg); + break; + case IS_RAID_BOSS: + Message(Chat::Red, "%s This spell will only affect raid bosses.", msg); + break; + case FRENZIED_BURNOUT_ACTIVE: + Message(Chat::Red, "%s This spell will only cast if you have Frenzied Burnout active.", msg); + break; + case FRENZIED_BURNOUT_NOT_ACTIVE: + Message(Chat::Red, "%s This spell will only cast if you do not have Frenzied Burnout active.", msg); + break; + case IS_HP_ABOVE_75_PCT: + Message(Chat::Red, "%s Your taret's HP must be at or above 75 pct of its maximum.", msg); + break; + case IS_HP_LESS_THAN_20_PCT: + Message(Chat::Red, "%s Your target's HP must be at 20 pct of its maximum or below.", msg); + break; + case IS_HP_LESS_THAN_50_PCT: + Message(Chat::Red, "%s Your target's HP must be at 50 pct of its maximum or below.", msg); + break; + case IS_HP_LESS_THAN_75_PCT: + Message(Chat::Red, "%s Your target's HP must be at 75 pct of its maximum or below.", msg); + break; + case IS_NOT_IN_COMBAT: + Message(Chat::Red, "%s This spell will only affect creatures that are not in combat.", msg); + break; + case HAS_AT_LEAST_1_PET_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 1 pet on their hate list.", msg); + break; + case HAS_AT_LEAST_2_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 2 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_3_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 3 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_4_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 4 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_5_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 5 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_6_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 6 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_7_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 7 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_8_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 8 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_9_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 9 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_10_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 10 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_11_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 11 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_12_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 12 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_13_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 13 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_14_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 14 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_15_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 15 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_16_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 16 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_17_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 17 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_18_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 18 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_19_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 19 pets on their hate list.", msg); + break; + case HAS_AT_LEAST_20_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have least 20 pets on their hate list.", msg); + break; + case IS_HP_LESS_THAN_35_PCT: + Message(Chat::Red, "%s Your target's HP must be at 35 pct of its maximum or below.", msg); + break; + case HAS_BETWEEN_1_TO_2_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have between 1 and 2 pets on their hate list.", msg); + break; + case HAS_BETWEEN_3_TO_5_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have between 3 and 5 pets on their hate list.", msg); + break; + case HAS_BETWEEN_6_TO_9_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have between 6 and 9 pets on their hate list.", msg); + break; + case HAS_BETWEEN_10_TO_14_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have between 10 and 14 pets on their hate list.", msg); + break; + case HAS_MORE_THAN_14_PETS_ON_HATELIST: + Message(Chat::Red, "%s Your target must have between 15 or more pets on their hate list.", msg); + break; + case IS_CLASS_CHAIN_OR_PLATE: + Message(Chat::Red, "%s This spell will only affect plate or chain wearing classes.", msg); + break; + case IS_HP_BETWEEN_5_AND_9_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 5 pct and 9 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 5 pct and 9 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_10_AND_14_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 10 pct and 14 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 10 pct and 14 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_15_AND_19_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 15 pct and 19 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 15 pct and 19 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_20_AND_24_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 20 pct and 54 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 20 pct and 24 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_25_AND_29_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 25 pct and 29 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 25 pct and 29 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_30_AND_34_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 30 pct and 34 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 30 pct and 34 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_35_AND_39_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 35 pct and 39 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 35 pct and 39 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_40_AND_44_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 40 pct and 44 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 40 pct and 44 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_45_AND_49_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 45 pct and 49 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 45 pct and 49 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_50_AND_54_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 50 pct and 54 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 50 pct and 54 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_55_AND_59_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 55 pct and 59 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 55 pct and 59 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_5_AND_15_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 5 pct and 15 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 5 pct and 15 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_15_AND_25_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 15 pct and 25 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 15 pct and 25 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_1_AND_25_PCT: + Message(Chat::Red, "%s Your target's HP must be at 25 pct of its maximum or below.", msg); + break; + case IS_HP_BETWEEN_25_AND_35_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 25 pct and 35 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 25 pct and 35 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_35_AND_45_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 35 pct and 45 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 35 pct and 45 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_45_AND_55_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 45 pct and 55 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 45 pct and 55 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_55_AND_65_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 55 pct and 65 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 55 pct and 65 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_65_AND_75_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 65 pct and 75 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 65 pct and 75 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_75_AND_85_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 75 pct and 85 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 75 pct and 85 pct of your maximum HP.", msg); + } + break; + case IS_HP_BETWEEN_85_AND_95_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be between 85 pct and 95 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be between 85 pct and 95 pct of your maximum HP.", msg); + } + break; + case IS_HP_ABOVE_45_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at least 45 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at least 45 pct of your maximum HP.", msg); + } + break; + case IS_HP_ABOVE_55_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at least 55 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at least 55 pct of your maximum HP.", msg); + } + break; + case UNKNOWN_TOO_MUCH_HP_410: + Message(Chat::Red, "%s Your target has too much HP to be affected by this spell.", msg); + break; + case UNKNOWN_TOO_MUCH_HP_411: + Message(Chat::Red, "%s Your target has too much HP to be affected by this spell.", msg); + break; + case IS_HP_ABOVE_99_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at or above 99 pct of its maximum.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or above 99 pct of your maximum HP.", msg); + } + break; + case IS_MANA_ABOVE_10_PCT: + Message(Chat::Red, "%s You must have at least 10 pct of your maximum mana available to cast this spell.", msg); + break; + case IS_HP_BELOW_5_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 5 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 5 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_10_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 10 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 10 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_15_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 15 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 15 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_20_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 20 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 20 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_25_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 25 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 25 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_30_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 30 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 30 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_35_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 35 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 35 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_40_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 40 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 40 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_45_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 45 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 45 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_50_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 50 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 50 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_55_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 55 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 55 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_60_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 60 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 60 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_65_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 65 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 65 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_70_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 70 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 70 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_75_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 75 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 75 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_80_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 80 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 80 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_85_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 85 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 85 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_90_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 90 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 90 pct of your maximum HP.", msg); + } + break; + case IS_HP_BELOW_95_PCT: + if (target_requirement) { + Message(Chat::Red, "%s Your target's HP must be at 95 pct of its maximum or below.", msg); + } + else { + Message(Chat::Red, "%s This ability requires you to be at or below 95 pct of your maximum HP.", msg); + } + break; + case IS_ENDURANCE_BELOW_40_PCT: + Message(Chat::Red, "%s This ability requires you to be at or below 40 pct of your maximum endurance.", msg); + break; + case IS_MANA_BELOW_40_PCT: + Message(Chat::Red, "%s This spells requires you to be at or below 40 pct of your maximum mana.", msg); + break; + case IS_HP_ABOVE_20_PCT: + Message(Chat::Red, "%s Your target's HP must be at least 21 pct of its maximum.", msg); + break; + case IS_BODY_TYPE_UNDEFINED: + Message(Chat::Red, "%s This spell will only work on creatures with an undefined body type.", msg); + break; + case IS_BODY_TYPE_HUMANOID: + Message(Chat::Red, "%s This spell will only work on humanoid creatures.", msg); + break; + case IS_BODY_TYPE_WEREWOLF: + Message(Chat::Red, "%s This spell will only work on lycanthrope creatures.", msg); + break; + case IS_BODY_TYPE_UNDEAD: + Message(Chat::Red, "%s This spell will only work on undead creatures.", msg); + break; + case IS_BODY_TYPE_GIANTS: + Message(Chat::Red, "%s This spell will only work on giants.", msg); + break; + case IS_BODY_TYPE_CONSTRUCTS: + Message(Chat::Red, "%s This spell will only work on constructs.", msg); + break; + case IS_BODY_TYPE_EXTRAPLANAR: + Message(Chat::Red, "%s This spell will only work on extraplanar creatures.", msg); + break; + case IS_BODY_TYPE_MAGICAL_CREATURE: + Message(Chat::Red, "%s This spell will only work on creatures constructed from magic.", msg); + break; + case IS_BODY_TYPE_UNDEADPET: + Message(Chat::Red, "%s This spell will only work on animated undead servants.", msg); + break; + case IS_BODY_TYPE_KAELGIANT: + Message(Chat::Red, "%s This spell will only work on the Giants of Kael Drakkal.", msg); + break; + case IS_BODY_TYPE_COLDAIN: + Message(Chat::Red, "%s This spell will only work on Coldain Dwarves.", msg); + break; + case IS_BODY_TYPE_VAMPIRE: + Message(Chat::Red, "%s This spell will only work on vampires.", msg); + break; + case IS_BODY_TYPE_ATEN_HA_RA: + Message(Chat::Red, "%s This spell will only work on Aten Ha Ra.", msg); + break; + case IS_BODY_TYPE_GREATER_AHKEVANS: + Message(Chat::Red, "%s This spell will only work on Greater Ahkevans.", msg); + break; + case IS_BODY_TYPE_KHATI_SHA: + Message(Chat::Red, "%s This spell will only work on Khati Sha.", msg); + break; + case IS_BODY_TYPE_LORD_INQUISITOR_SERU: + Message(Chat::Red, "%s This spell will only work on Lord Inquisitor Seru.", msg); + break; + case IS_BODY_TYPE_GRIEG_VENEFICUS: + Message(Chat::Red, "%s This spell will only work on Grieg Veneficus.", msg); + break; + case IS_BODY_TYPE_FROM_PLANE_OF_WAR: + Message(Chat::Red, "%s This spell will only work on creatures from the Plane of War.", msg); + break; + case IS_BODY_TYPE_LUGGALD: + Message(Chat::Red, "%s This spell will only work on Luggalds.", msg); + break; + case IS_BODY_TYPE_ANIMAL: + Message(Chat::Red, "%s This spell will only work on animals.", msg); + break; + case IS_BODY_TYPE_INSECT: + Message(Chat::Red, "%s This spell will only work on insects.", msg); + break; + case IS_BODY_TYPE_MONSTER: + Message(Chat::Red, "%s This spell will only work on monsters.", msg); + break; + case IS_BODY_TYPE_ELEMENTAL: + Message(Chat::Red, "%s This spell will only work on elemental creatures.", msg); + break; + case IS_BODY_TYPE_PLANT: + Message(Chat::Red, "%s This spell will only work on plants.", msg); + break; + case IS_BODY_TYPE_DRAGON2: + Message(Chat::Red, "%s This spell will only work on dragons.", msg); + break; + case IS_BODY_TYPE_SUMMONED_ELEMENTAL: + Message(Chat::Red, "%s This spell will only work on summoned elementals.", msg); + break; + case IS_BODY_TYPE_DRAGON_OF_TOV: + Message(Chat::Red, "%s This spell will only work on Dragons of Veeshan's Temple.", msg); + break; + case IS_BODY_TYPE_FAMILIAR: + Message(Chat::Red, "%s This spell will only work on familiars.", msg); + break; + case IS_BODY_TYPE_MURAMITE: + Message(Chat::Red, "%s This spell will only work on Muramites.", msg); + break; + case IS_NOT_UNDEAD_OR_SUMMONED: + Message(Chat::Red, "%s This spell will only targets that are not undead or summoned.", msg); + break; + case IS_NOT_PLANT: + Message(Chat::Red, "%s This spell will not affect plants.", msg); + break; + case IS_NOT_CLIENT: + Message(Chat::Red, "%s This spell will not work on adventurers.", msg); + break; + case IS_CLIENT: + Message(Chat::Red, "%s This spell will only work on adventurers.", msg); + break; + case IS_LEVEL_ABOVE_42_AND_IS_CLIENT: + Message(Chat::Red, "%s This spell will only work on level 43 or higher adventurers.", msg); + break; + case IS_TREANT: + Message(Chat::Red, "%s This spell will only work on treants.", msg); + break; + case IS_BIXIE2: + Message(Chat::Red, "%s This spell will only work on bixies.", msg); + break; + case IS_SCARECROW: + Message(Chat::Red, "%s This spell will only work on scarecrows.", msg); + break; + case IS_VAMPIRE_OR_UNDEAD_OR_UNDEADPET: + Message(Chat::Red, "%s This spell will only work on vampires, undead, or animated undead creatures.", msg); + break; + case IS_NOT_VAMPIRE_OR_UNDEAD: + Message(Chat::Red, "%s This spell will not work on vampires or undead creatures.", msg); + break; + case IS_CLASS_KNIGHT_HYBRID_MELEE: + Message(Chat::Red, "%s This spell will only work on knights, hybrids, or melee classes.", msg); + break; + case IS_CLASS_WARRIOR_CASTER_PRIEST: + Message(Chat::Red, "%s This spell will only work on warriors, casters, or priests.", msg); + break; + case IS_END_BELOW_21_PCT: + Message(Chat::Red, "%s This ability requires you to be at or below 21 pct of your maximum endurance.", msg); + break; + case IS_END_BELOW_25_PCT: + Message(Chat::Red, "%s This ability requires you to be at or below 25 pct of your maximum endurance.", msg); + break; + case IS_END_BELOW_29_PCT: + Message(Chat::Red, "%s This ability requires you to be at or below 29 pct of your maximum endurance.", msg); + break; + case IS_HUMANOID_LEVEL_84_MAX: + Message(Chat::Red, "%s This spell will only work on humanoids up to level 84.", msg); + break; + case IS_HUMANOID_LEVEL_86_MAX: + Message(Chat::Red, "%s This spell will only work on humanoids up to level 86.", msg); + break; + case IS_HUMANOID_LEVEL_88_MAX: + Message(Chat::Red, "%s This spell will only work on humanoids up to level 88.", msg); + break; + case HAS_CRYSTALLIZED_FLAME_BUFF: + Message(Chat::Red, "%s This spell will only work on targets afflicted by Crystallized Flame.", msg); + break; + case HAS_INCENDIARY_OOZE_BUFF: + Message(Chat::Red, "%s This spell will only work on targets afflicted by Incendiary Ooze.", msg); + break; + case IS_LEVEL_90_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 90.", msg); + break; + case IS_LEVEL_92_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 92.", msg); + break; + case IS_LEVEL_94_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 94.", msg); + break; + case IS_LEVEL_95_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 95.", msg); + break; + case IS_LEVEL_97_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 97.", msg); + break; + case IS_LEVEL_99_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 99.", msg); + break; + case IS_LEVEL_100_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 100.", msg); + break; + case IS_LEVEL_102_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 102.", msg); + break; + case IS_LEVEL_104_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 104.", msg); + break; + case IS_LEVEL_105_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 105.", msg); + break; + case IS_LEVEL_107_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 107.", msg); + break; + case IS_LEVEL_109_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 109.", msg); + break; + case IS_LEVEL_110_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 110.", msg); + break; + case IS_LEVEL_112_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 112.", msg); + break; + case IS_LEVEL_114_MAX: + Message(Chat::Red, "%s This spell will ony work targets level up to level 114.", msg); + break; + case HAS_TBL_ESIANTI_ACCESS: + Message(Chat::Red, "%s This spell will only transport adventurers who have gained access to Esianti: Palace of the Winds.", msg); + break; + case IS_BETWEEN_LEVEL_1_AND_75: + Message(Chat::Red, "%s This spell will only work on targets between level 1 and 75.", msg); + break; + case IS_BETWEEN_LEVEL_76_AND_85: + Message(Chat::Red, "%s This spell will only work on targets between level 76 and 85.", msg); + break; + case IS_BETWEEN_LEVEL_86_AND_95: + Message(Chat::Red, "%s This spell will only work on targets between level 86 and 95.", msg); + break; + case IS_BETWEEN_LEVEL_96_AND_105: + Message(Chat::Red, "%s This spell will only work on targets between level 96 and 105.", msg); + break; + case IS_HP_LESS_THAN_80_PCT: + Message(Chat::Red, "%s Your target's HP must be at 80 pct of its maximum or below.", msg); + break; + case IS_LEVEL_ABOVE_34: + Message(Chat::Red, "%s Your target must be level 35 or higher.", msg); + break; + case IN_TWO_HANDED_STANCE: + Message(Chat::Red, "%s You must be in your two-handed stance to use this ability.", msg); + break; + case IN_DUAL_WIELD_HANDED_STANCE: + Message(Chat::Red, "%s You must be in your dual-wielding stance to use this ability.", msg); + break; + case IN_SHIELD_STANCE: + Message(Chat::Red, "%s You must be in your shield stance to use this ability.", msg); + break; + case NOT_IN_TWO_HANDED_STANCE: + Message(Chat::Red, "%s You may not use this ability if you are in your two-handed stance.", msg); + break; + case NOT_IN_DUAL_WIELD_HANDED_STANCE: + Message(Chat::Red, "%s You may not use this ability if you are in your dual-wielding stance.", msg); + break; + case NOT_IN_SHIELD_STANCE: + Message(Chat::Red, "%s You may not use this ability if you are in your shield stance.", msg); + break; + case LEVEL_46_MAX: + Message(Chat::Red, "%s Your target must be level 46 or below.", msg); + break; + case HAS_NO_MANA_BURN_BUFF: + Message(Chat::Red, "%s This spell will not take hold until the effects of the previous Mana Burn have expired.", msg); + break; + case IS_CLIENT_AND_MALE_PLATE_USER: + Message(Chat::Red, "%s Your target wouldn't look right as that Jann.", msg); + break; + case IS_CLEINT_AND_MALE_DRUID_ENCHANTER_MAGICIAN_NECROANCER_SHAMAN_OR_WIZARD: + Message(Chat::Red, "%s Your target wouldn't look right as that Jann.", msg); + break; + case IS_CLIENT_AND_MALE_BEASTLORD_BERSERKER_MONK_RANGER_OR_ROGUE: + Message(Chat::Red, "%s Your target wouldn't look right as that Jann.", msg); + break; + case IS_CLIENT_AND_FEMALE_PLATE_USER: + Message(Chat::Red, "%s Your target wouldn't look right as that Jann.", msg); + break; + case IS_CLIENT_AND_FEMALE_DRUID_ENCHANTER_MAGICIAN_NECROANCER_SHAMAN_OR_WIZARD: + Message(Chat::Red, "%s Your target wouldn't look right as that Jann.", msg); + break; + case IS_CLIENT_AND_FEMALE_BEASTLORD_BERSERKER_MONK_RANGER_OR_ROGUE: + Message(Chat::Red, "%s Your target wouldn't look right as that Jann.", msg); + break; + case HAS_TRAVELED_TO_STRATOS: + Message(Chat::Red, "%s You must travel to Stratos at least once before wishing to go there.", msg); + break; + case HAS_TRAVELED_TO_AALISHAI: + Message(Chat::Red, "%s You must travel to Aalishai at least once before wishing to go there.", msg); + break; + case HAS_TRAVELED_TO_MEARATS: + Message(Chat::Red, "%s You must travel to Mearatas at least once before wishing to go there.", msg); + break; + case IS_HP_ABOVE_50_PCT: + Message(Chat::Red, "%s This target must be above 50 pct of its maximum hit points.", msg); + break; + case IS_HP_UNDER_50_PCT: + Message(Chat::Red, "%s This target must be at oe below 50 pct of its maximum hit points.", msg); + break; + case IS_OFF_HAND_EQUIPED: + Message(Chat::Red, "%s You must be wielding a weapon or shield in your offhand to use this ability.", msg); + break; + case HAS_NO_PACT_OF_FATE_RECOURSE_BUFF: + Message(Chat::Red, "%s This spell will not work while Pact of Fate Recourse is active.", msg); + break; + case HAS_NO_SHROUD_OF_PRAYER_BUFF: + Message(Chat::Red, "%s Your target cannot receive another Quiet Prayer this soon.", msg); + break; + case IS_MANA_BELOW_20_PCT: + Message(Chat::Red, "%s This ability requires you to be at or below 20 pct of your maximum mana.", msg); + break; + case IS_MANA_ABOVE_50_PCT: + Message(Chat::Red, "%s This ability requires you to be at or above 50 pct of your maximum mana.", msg); + break; + case COMPLETED_ACHIEVEMENT_LEGENDARY_ANSWERER: + Message(Chat::Red, "%s You have completed Legendary Answerer.", msg); + break; + case HAS_NO_ROGUES_FURY_BUFF: + Message(Chat::Red, "%s This spell will not affect anyone that currently has Rogue's Fury active.", msg); + break; + case NOT_COMPLETED_ACHIEVEMENT_LEGENDARY_ANSWERER: + Message(Chat::Red, "%s You must complete Legendary Answerer.", msg); + break; + case IS_SUMMONED_OR_UNDEAD: + Message(Chat::Red, "%s This spell can only be used on summoned or undead.", msg); + break; + case IS_CLASS_CASTER_PRIEST: + Message(Chat::Red, "%s This ability requires you to be a caster or priest.", msg); + break; + case IS_END_OR_MANA_ABOVE_20_PCT: + Message(Chat::Red, "%s You must have at least 20 pct of your maximum mana and endurance to use this ability.", msg); + break; + case IS_END_OR_MANA_BELOW_30_PCT: + Message(Chat::Red, "%s Your target already has 30 pct or more of their maximum mana or endurance.", msg); + break; + case IS_CLASS_BARD2: + if (target_requirement) { + Message(Chat::Red, "%s This spell will only affect Bards.", msg); + } + else { + Message(Chat::Red, "%s This ability can only be used by Bards.", msg); + } + break; + case IS_NOT_CLASS_BARD: + if (target_requirement) { + Message(Chat::Red, "%s This spell can not affect Bards.", msg); + } + else { + Message(Chat::Red, "%s This ability can not be used by Bards.", msg); + } + break; + case HAS_NO_FURIOUS_RAMPAGE_BUFF: + Message(Chat::Red, "%s This ability cannot be activated while Furious Rampage is active.", msg); + break; + case IS_END_OR_MANA_BELOW_30_PCT2: + Message(Chat::Red, "%s You can only perform this solo if you have less than 30 pct mana or endurance.", msg); + break; + case HAS_NO_HARMONIOUS_PRECISION_BUFF: + Message(Chat::Red, "%s This spell will not work if you have the Harmonious Precision line active.", msg); + break; + case HAS_NO_HARMONIOUS_EXPANSE_BUFF: + Message(Chat::Red, "%s This spell will not work if you have the Harmonious Expanse line active.", msg); + break; + default: + if (target_requirement) { + Message(Chat::Red, "Your target does not meet the spell requirements.", msg); + } + else { + Message(Chat::Red, "Your spell would not take hold on your target.", msg); + } + break; + } +} + bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed) { /*For mage 'Bolt' line and other various spells. @@ -8526,7 +9622,7 @@ bool Mob::PassCharmTargetRestriction(Mob *target) { if (!target) { return false; } - + if (target->IsClient() && IsClient()) { MessageString(Chat::Red, CANNOT_AFFECT_PC); LogSpells("Spell casting canceled: Can not cast charm on a client."); diff --git a/zone/spells.cpp b/zone/spells.cpp index ed7f27c10..e4decc0f6 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -157,102 +157,52 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, uint32 aa_id) { LogSpells("CastSpell called for spell [{}] ([{}]) on entity [{}], slot [{}], time [{}], mana [{}], from item slot [{}]", - (IsValidSpell(spell_id))?spells[spell_id].name:"UNKNOWN SPELL", spell_id, target_id, static_cast(slot), cast_time, mana_cost, (item_slot==0xFFFFFFFF)?999:item_slot); + (IsValidSpell(spell_id)) ? spells[spell_id].name : "UNKNOWN SPELL", spell_id, target_id, static_cast(slot), cast_time, mana_cost, (item_slot == 0xFFFFFFFF) ? 999 : item_slot); if (casting_spell_id == spell_id) { ZeroCastingVars(); } - if - ( - !IsValidSpell(spell_id) || + + //If spell fails checks here determine if we need to send packet to client to reset spell bar. + bool send_spellbar_enable = true; + if ((item_slot != -1 && cast_time == 0) || aa_id) { + send_spellbar_enable = false; + } + + if (!IsValidSpell(spell_id) || casting_spell_id || delaytimer || - spellend_timer.Enabled() || - (IsStunned() && !IgnoreCastingRestriction(spell_id)) || - IsFeared() || - (IsMezzed() && !IgnoreCastingRestriction(spell_id)) || - (IsSilenced() && !IsDiscipline(spell_id)) || - (IsAmnesiad() && IsDiscipline(spell_id)) - ) - { - LogSpells("Spell casting canceled: not able to cast now. Valid? [{}], casting [{}], waiting? [{}], spellend? [{}], stunned? [{}], feared? [{}], mezed? [{}], silenced? [{}], amnesiad? [{}]", - IsValidSpell(spell_id), casting_spell_id, delaytimer, spellend_timer.Enabled(), IsStunned(), IsFeared(), IsMezzed(), IsSilenced(), IsAmnesiad() ); - - if (IsSilenced() && !IsDiscipline(spell_id)) { - MessageString(Chat::Red, SILENCED_STRING); - } - if (IsAmnesiad() && IsDiscipline(spell_id)) { - MessageString(Chat::Red, MELEE_SILENCE); - } - if (IsClient()) { - CastToClient()->SendSpellBarEnable(spell_id); - } - if (casting_spell_id && IsNPC()) { - CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); - } - return(false); + spellend_timer.Enabled()) { + LogSpells("Spell casting canceled: not able to cast now. Valid? [{}], casting [{}], waiting? [{}], spellend? [{}]", + IsValidSpell(spell_id), casting_spell_id, delaytimer, spellend_timer.Enabled()); + StopCastSpell(spell_id, send_spellbar_enable); + return false; } + + if (!DoCastingChecksOnCaster(spell_id) || + !DoCastingChecksZoneRestrictions(true, spell_id) || + !DoCastingChecksOnTarget(true, spell_id, entity_list.GetMobID(target_id))) { + StopCastSpell(spell_id, send_spellbar_enable); + return false; + } + else { + casting_spell_checks = true; + } + //It appears that the Sanctuary effect is removed by a check on the client side (keep this however for redundancy) - if (spellbonuses.Sanctuary && (spells[spell_id].target_type != ST_Self && GetTarget() != this) || IsDetrimentalSpell(spell_id)) + if (spellbonuses.Sanctuary && (spells[spell_id].target_type != ST_Self && GetTarget() != this) || IsDetrimentalSpell(spell_id)) { BuffFadeByEffect(SE_Sanctuary); - - if(IsClient()){ - int chance = CastToClient()->GetFocusEffect(focusFcMute, spell_id);//Client only - - if (zone->random.Roll(chance)) { - MessageString(Chat::Red, SILENCED_STRING); - if(IsClient()) - CastToClient()->SendSpellBarEnable(spell_id); - return(false); - } - } - - if(IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()){ - MessageString(Chat::Red, SPELL_WOULDNT_HOLD); - if(IsClient()) - CastToClient()->SendSpellBarEnable(spell_id); - if(casting_spell_id && IsNPC()) - CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); - return(false); - } - - //cannot cast under divine aura, unless spell has 'cast_not_standing' flag. - if(DivineAura() && !IgnoreCastingRestriction(spell_id)) { - LogSpells("Spell casting canceled: cannot cast while Divine Aura is in effect"); - InterruptSpell(173, 0x121, false); - if(IsClient()) { - CastToClient()->SendSpellBarEnable(spell_id); - } - return(false); } if (spellbonuses.NegateIfCombat) { BuffFadeByEffect(SE_NegateIfCombat); } - if (IsClient() && IsHarmonySpell(spell_id) && !HarmonySpellLevelCheck(spell_id, entity_list.GetMobID(target_id))) { - InterruptSpell(SPELL_NO_EFFECT, 0x121, spell_id); - return false; - } - - if (IsEffectInSpell(spell_id, SE_Charm) && !PassCharmTargetRestriction(entity_list.GetMobID(target_id))) { - bool can_send_spellbar_enable = true; - if ((item_slot != -1 && cast_time == 0) || aa_id) { - can_send_spellbar_enable = false; - } - if (can_send_spellbar_enable) { - SendSpellBarEnable(spell_id); - } - if (casting_spell_id && IsNPC()) { - CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); - } - return false; - } - - if (HasActiveSong() && IsBardSong(spell_id)) { + //Casting a spell from an item click will also stop bard pulse. + if (HasActiveSong() && (IsBardSong(spell_id) || slot == CastingSlot::Item)) { LogSpells("Casting a new song while singing a song. Killing old song [{}]", bardsong); //Note: this does NOT tell the client - _StopSong(); + ZeroBardPulseVars(); } //Added to prevent MQ2 exploitation of equipping normally-unequippable/clickable items with effects and clicking them for benefits. @@ -442,8 +392,9 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, cast_time = GetActSpellCasttime(spell_id, cast_time); } } - else + else { orgcasttime = cast_time; + } // we checked for spells not requiring targets above if(target_id == 0) { @@ -455,83 +406,50 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, } else { InterruptSpell(0, 0, 0); //the 0 args should cause no messages } + ZeroCastingVars(); return(false); } // ok now we know the target casting_spell_targetid = target_id; - if (RuleB(Spells, InvisRequiresGroup) && IsInvisSpell(spell_id)) { - if (IsClient() && GetTarget() && GetTarget()->IsClient()) { - Client* spell_caster = this->CastToClient(); - Client* spell_target = entity_list.GetClientByID(target_id); - if (spell_target && spell_target->GetID() != GetID()) { - bool cast_failed = true; - if (spell_target->IsGrouped()) { - Group *target_group = spell_target->GetGroup(); - Group *my_group = GetGroup(); - if ( - target_group && - my_group && - (target_group->GetID() == my_group->GetID()) - ) { - cast_failed = false; - } - } else if (spell_target->IsRaidGrouped()) { - Raid *target_raid = spell_target->GetRaid(); - Raid *my_raid = GetRaid(); - if ( - target_raid && - my_raid && - (target_raid->GetGroup(spell_target) == my_raid->GetGroup(spell_caster)) - ) { - cast_failed = false; - } - } - - if (cast_failed) { - InterruptSpell(spell_id); - MessageString(Chat::Red, TARGET_GROUP_MEMBER); - return false; - } - } - } - } - // We don't get actual mana cost here, that's done when we consume the mana - if (mana_cost == -1) + if (mana_cost == -1) { mana_cost = spell.mana; + } // mana is checked for clients on the frontend. we need to recheck it for NPCs though // If you're at full mana, let it cast even if you dont have enough mana // we calculated this above, now enforce it - if(mana_cost > 0 && slot != CastingSlot::Item) - { + if(mana_cost > 0 && slot != CastingSlot::Item) { int my_curmana = GetMana(); int my_maxmana = GetMaxMana(); - if(my_curmana < mana_cost) // not enough mana - { + if(my_curmana < mana_cost) {// not enough mana //this is a special case for NPCs with no mana... - if(IsNPC() && my_curmana == my_maxmana) - { + if(IsNPC() && my_curmana == my_maxmana){ mana_cost = 0; - } else { + } + else { + //The client will prevent spell casting if insufficient mana, this is only for serverside enforcement. LogSpells("Spell Error not enough mana spell=[{}] mymana=[{}] cost=[{}]\n", spell_id, my_curmana, mana_cost); if(IsClient()) { //clients produce messages... npcs should not for this case MessageString(Chat::Red, INSUFFICIENT_MANA); InterruptSpell(); - } else { + } + else { InterruptSpell(0, 0, 0); //the 0 args should cause no messages } + ZeroCastingVars(); return(false); } } } - if(mana_cost > GetMana()) + if (mana_cost > GetMana()) { mana_cost = GetMana(); + } // we know our mana cost now casting_spell_mana = mana_cost; @@ -544,16 +462,13 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, // now tell the people in the area -- we ALWAYS want to send this, even instant cast spells. // The only time this is skipped is for NPC innate procs and weapon procs. Procs from buffs // oddly still send this. Since those cases don't reach here, we don't need to check them - if (slot != CastingSlot::Discipline) + if (slot != CastingSlot::Discipline) { SendBeginCast(spell_id, orgcasttime); + } // cast time is 0, just finish it right now and be done with it if(cast_time == 0) { - if (!DoCastingChecks()) { - StopCasting(); - return false; - } - CastedSpellFinished(spell_id, target_id, slot, mana_cost, item_slot, resist_adjust); + CastedSpellFinished(spell_id, target_id, slot, mana_cost, item_slot, resist_adjust); // return(true); } @@ -580,11 +495,6 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, MessageString(Chat::Spells, BEGINS_TO_GLOW, item->GetItem()->Name); } - if (!DoCastingChecks()) { - StopCasting(); - return false; - } - return(true); } @@ -611,77 +521,376 @@ void Mob::SendBeginCast(uint16 spell_id, uint32 casttime) safe_delete(outapp); } -/* - * Some failures should be caught before the spell finishes casting - * This is especially helpful to clients when they cast really long things - * If this passes it sets casting_spell_checks to true which is checked in - * SpellProcess(), if a situation ever arises where a spell is delayed by these - * it's probably doing something wrong. - */ +bool Mob::DoCastingChecksOnCaster(int32 spell_id) { -bool Mob::DoCastingChecks(int32 spell_id, uint16 target_id) -{ - if (!IsClient() || (IsClient() && CastToClient()->GetGM())) { - casting_spell_checks = true; - return true; - } - - bool ignore_casting_spell_checks = false; /* - If variables are passed into this function it is NOT being called from main spell process - thefore we do not want to set the 'casting_spell_checks' state keeping variable. + These are casting requirements on the CASTER that will cancel a spell before spell finishes casting or prevent spell from casting. + - caster_requirmement_id : checks specific requirements on caster (cast initiates) + - linked timer spells. (cast initiates) [cancel before begin cast message] + - must be out of combat spell field. (client blocks) + - must be in combat spell field. (client blocks) + + Always checked at the start of CastSpell. + Checked before special cases for bards casting from SpellFinished. */ - if (spell_id != SPELL_UNKNOWN || target_id) { - ignore_casting_spell_checks = true; + + /* + Cannot cast if stunned or mezzed, unless spell has 'cast_not_standing' flag. + */ + if ((IsStunned() || IsMezzed()) && !IgnoreCastingRestriction(spell_id)) { + LogSpells("Spell casting canceled [{}] : can not cast spell when stunned.", spell_id); + return false; + } + /* + Can not cast if feared. + */ + if (IsFeared()) { + LogSpells("Spell casting canceled [{}] : can not cast spell when feared.", spell_id); + return false; + } + /* + Can not cast if spell + */ + if ((IsSilenced() && !IsDiscipline(spell_id))) { + MessageString(Chat::Red, SILENCED_STRING); + LogSpells("Spell casting canceled [{}] : can not cast spell when silenced.", spell_id); + return false; + } + /* + Can not cast if discipline. + */ + if (IsAmnesiad() && IsDiscipline(spell_id)) { + MessageString(Chat::Red, MELEE_SILENCE); + LogSpells("Spell casting canceled [{}] : can not use discipline with amnesia.", spell_id); + return false; + } + /* + Cannot cast under divine aura, unless spell has 'cast_not_standing' flag. + */ + if (DivineAura() && !IgnoreCastingRestriction(spell_id)) { + LogSpells("Spell casting canceled [{}] : cannot cast while Divine Aura is in effect.", spell_id); + InterruptSpell(173, 0x121, false); //not sure we need this. + return false; + } + /* + Linked Reused Timers that are not ready + */ + if (IsClient() && spells[spell_id].timer_id > 0 && casting_spell_slot < CastingSlot::MaxGems) { + if (!CastToClient()->IsLinkedSpellReuseTimerReady(spells[spell_id].timer_id)) { + LogSpells("Spell casting canceled [{}] : linked reuse timer not ready.", spell_id); + return false; + } + } + /* + Spells that use caster_requirement_id field which requires specific conditions on caster to be met before casting. + */ + if (spells[spell_id].caster_requirement_id && !PassCastRestriction(spells[spell_id].caster_requirement_id)) { + SendCastRestrictionMessage(spells[spell_id].caster_requirement_id, false, IsDiscipline(spell_id)); + LogSpells("Spell casting canceled [{}] : caster requirement id [{}] not met.", spell_id, spells[spell_id].caster_requirement_id); + return false; + } + /* + Spells that use field can_cast_in_comabt or can_cast_out of combat restricting + caster to meet one of those conditions. If beneficial spell check casters state. + If detrimental check the targets state (done elsewhere in this function). + */ + if (!spells[spell_id].can_cast_in_combat && spells[spell_id].can_cast_out_of_combat) { + if (IsBeneficialSpell(spell_id)) { + if ((IsNPC() && IsEngaged()) || (IsClient() && CastToClient()->GetAggroCount())) { + if (IsDiscipline(spell_id)) { + MessageString(Chat::Red, NO_ABILITY_IN_COMBAT); + } + else { + MessageString(Chat::Red, NO_CAST_IN_COMBAT); + } + LogSpells("Spell casting canceled [{}] : can not use spell while in combat.", spell_id); + return false; + } + } + } + else if (spells[spell_id].can_cast_in_combat && !spells[spell_id].can_cast_out_of_combat) { + if (IsBeneficialSpell(spell_id)) { + if ((IsNPC() && !IsEngaged()) || (IsClient() && !CastToClient()->GetAggroCount())) { + if (IsDiscipline(spell_id)) { + MessageString(Chat::Red, NO_ABILITY_OUT_OF_COMBAT); + } + else { + MessageString(Chat::Red, NO_CAST_OUT_OF_COMBAT); + } + LogSpells("Spell casting canceled [{}] : can not use spell while out of combat.", spell_id); + return false; + } + } + } + /* + Focus version of Silence will prevent spell casting + */ + if (IsClient() && !IsDiscipline(spell_id)) { + int chance = CastToClient()->GetFocusEffect(focusFcMute, spell_id);//client only + if (chance && zone->random.Roll(chance)) { + MessageString(Chat::Red, SILENCED_STRING); + LogSpells("Spell casting canceled: can not cast spell when silenced from SPA 357 FcMute."); + return(false); + } } - if (spell_id == SPELL_UNKNOWN) { - spell_id = casting_spell_id; - } - if (!target_id) { - target_id = casting_spell_targetid; + return true; +} + +bool Mob::DoCastingChecksZoneRestrictions(bool check_on_casting, int32 spell_id) { + + /* + These are casting requirements determined by ZONE limiters that will cancel a spell before spell finishes casting or prevent spell from casting. + - levitate zone restriction (client blocks) [cancel before begin cast message] + - can not cast outdoor [cancels after spell finishes channeling] + + If the spell is a casted spell, check on CastSpell and ignore on SpellFinished. + If the spell is a initiated from SpellFinished, then check at start of SpellFinished. + */ + + bool ignore_if_npc_or_gm = false; + if (!IsClient() || (IsClient() && CastToClient()->GetGM())) { + ignore_if_npc_or_gm = true; } - Mob *spell_target = entity_list.GetMob(target_id); + /* + Zone ares that prevent blocked spells from being cast. + If on cast iniated then check any mob casting, if on spellfinished only check if is from client. + */ + if ((check_on_casting && !ignore_if_npc_or_gm) || (!check_on_casting && IsClient())) { + if (zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) { + if (IsClient()) { + if (!CastToClient()->GetGM()) { + const char *msg = zone->GetSpellBlockedMessage(spell_id, glm::vec3(GetPosition())); + if (msg) { + Message(Chat::Red, msg); + return false; + } + else { + Message(Chat::Red, "You can't cast this spell here."); + return false; + } + LogSpells("Spell casting canceled [{}] : can not cast in this zone location blocked spell.", spell_id); + } + else { + LogSpells("GM Cast Blocked Spell: [{}] (ID [{}])", GetSpellName(spell_id), spell_id); + } + } + return false; + } + } + /* + Zones where you can not use levitate spells. + */ + if (!ignore_if_npc_or_gm && !zone->CanLevitate() && IsEffectInSpell(spell_id, SE_Levitate)) { //check on spellfinished. + Message(Chat::Red, "You have entered an area where levitation effects do not function."); + LogSpells("Spell casting canceled [{}] : can not cast levitation in this zone.", spell_id); + return false; + } + /* + Zones where you can not use detrimental spells. + */ + if (IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()) { + Message(Chat::Red, "You cannot cast detrimental spells here."); + return false; + } - if (RuleB(Spells, BuffLevelRestrictions)) { + if (check_on_casting) { + /* + Zones where you can not cast out door only spells. This is only checked when casting is completed. + */ + if (!ignore_if_npc_or_gm && spells[spell_id].zone_type == 1 && !zone->CanCastOutdoor()) { + if (IsClient() && !CastToClient()->GetGM()) { + MessageString(Chat::Red, CAST_OUTDOORS); + LogSpells("Spell casting canceled [{}] : can not cast outdoors.", spell_id); + return false; + } + } + /* + Zones where you can not gate. + */ + if (IsClient() && + (zone->GetZoneID() == Zones::TUTORIAL || zone->GetZoneID() == Zones::LOAD) && + CastToClient()->Admin() < AccountStatus::QuestTroupe) { + if (IsEffectInSpell(spell_id, SE_Gate) || + IsEffectInSpell(spell_id, SE_Translocate) || + IsEffectInSpell(spell_id, SE_Teleport)) { + Message(Chat::White, "The Gods brought you here, only they can send you away."); + return false; + } + } + } + + return true; +} + + +bool Mob::DoCastingChecksOnTarget(bool check_on_casting, int32 spell_id, Mob *spell_target) { + + /* + These are casting requirements or TARGETS that will cancel a spell before spell finishes casting or prevent spell from casting. + - cast_restriction : checks specific requirements on target (cast initiates) + - target level restriction on buffs (cast initiates) + - can not cast life tap on self (client blocks) [cancel before begin cast message] + - can not cast sacrifice on self (cast initiates) [cancel before begin cast message] + - charm restrictions (cast initiates) [cancel before begin cast message] + - pcnpc_only_flag - (client blocks] [cancel before being cast message] + + If the spell is a casted spell, check on CastSpell and ignore on SpellFinished. + If the spell is a initiated from SpellFinished, then check at start of SpellFinished. + Always check again on SpellOnTarget to account for AE checks. + */ + + + bool ignore_if_npc_or_gm = false; + if (!IsClient() || (IsClient() && CastToClient()->GetGM())) { + ignore_if_npc_or_gm = true; + } + + if (check_on_casting && !spell_target){ + + if (IsGroupSpell(spell_id) || + spells[spell_id].target_type == ST_AEClientV1 || + spells[spell_id].target_type == ST_AECaster || + spells[spell_id].target_type == ST_Ring || + spells[spell_id].target_type == ST_Beam){ + return true; + } + else if (spells[spell_id].target_type == ST_Self) { + spell_target = this; + } + } + + //If we still do not have a target end. + if (!spell_target){ + return false; + } + + /* + Spells that use caster_restriction field which requires specific conditions on target to be met before casting. + [Insufficient mana first] + */ + if (spells[spell_id].cast_restriction && !spell_target->PassCastRestriction(spells[spell_id].cast_restriction)) { + SendCastRestrictionMessage(spells[spell_id].cast_restriction, true, IsDiscipline(spell_id)); + LogSpells("Spell casting canceled [{}] : target requirement id [{}] not met.", spell_id, spells[spell_id].caster_requirement_id); + return false; + } + /* + Spells that use field can_cast_in_comabt or can_cast_out of combat restricting + caster to meet one of those conditions. If beneficial spell check casters state (done else where in this function) + if detrimental check the targets state. + */ + if (!spells[spell_id].can_cast_in_combat && spells[spell_id].can_cast_out_of_combat) { + if (IsDetrimentalSpell(spell_id)) { + if (((spell_target->IsNPC() && spell_target->IsEngaged()) || + (spell_target->IsClient() && spell_target->CastToClient()->GetAggroCount()))) { + MessageString(Chat::Red, SPELL_NO_EFFECT); // Unsure correct string + LogSpells("Spell casting canceled [{}] : can not use spell while your target is in combat.", spell_id); + return false; + } + } + } + else if (spells[spell_id].can_cast_in_combat && !spells[spell_id].can_cast_out_of_combat) { + if (IsDetrimentalSpell(spell_id)) { + if (((spell_target->IsNPC() && !spell_target->IsEngaged()) || + (spell_target->IsClient() && !spell_target->CastToClient()->GetAggroCount()))) { + MessageString(Chat::Red, SPELL_NO_EFFECT); // Unsure correct string + LogSpells("Spell casting canceled [{}] : can not use spell while your target is out of combat.", spell_id); + return false; + } + } + } + /* + Prevent buffs from being cast on targets who don't meet level restriction + */ + if (!ignore_if_npc_or_gm && RuleB(Spells, BuffLevelRestrictions)) { // casting_spell_targetid is guaranteed to be what we went, check for ST_Self for now should work though - if (spell_target && spells[spell_id].target_type != ST_Self && !spell_target->CheckSpellLevelRestriction(spell_id)) { + if (spells[spell_id].target_type != ST_Self && !spell_target->CheckSpellLevelRestriction(spell_id)) { LogSpells("Spell [{}] failed: recipient did not meet the level restrictions", spell_id); - if (!IsBardSong(spell_id)) + if (!IsBardSong(spell_id)) { MessageString(Chat::SpellFailure, SPELL_TOO_POWERFUL); + } return false; } } - - if (spells[spell_id].zone_type == 1 && !zone->CanCastOutdoor()) { - MessageString(Chat::Red, CAST_OUTDOORS); - return false; - } - - if (IsEffectInSpell(spell_id, SE_Levitate) && !zone->CanLevitate()) { - Message(Chat::Red, "You can't levitate in this zone."); - return false; - } - - if(zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) { - const char *msg = zone->GetSpellBlockedMessage(spell_id, glm::vec3(GetPosition())); - if (msg) { - Message(Chat::Red, msg); + /* + Prevents buff from being cast based on tareget ing PC OR NPC (1 = PCs, 2 = NPCs) + These target types skip pcnpc only check (according to dev quotes) + */ + if (spells[spell_id].pcnpc_only_flag && spells[spell_id].target_type != ST_AETargetHateList && + spells[spell_id].target_type != ST_HateList) { + if (spells[spell_id].pcnpc_only_flag == 1 && !spell_target->IsClient() && !spell_target->IsMerc() && !spell_target->IsBot()) { return false; - } else { - Message(Chat::Red, "You can't cast this spell here."); + } + else if (spells[spell_id].pcnpc_only_flag == 2 && (spell_target->IsClient() || spell_target->IsMerc() || spell_target->IsBot())) { return false; } } - - if (IsClient() && spells[spell_id].timer_id > 0 && casting_spell_slot < CastingSlot::MaxGems) - if (!CastToClient()->IsLinkedSpellReuseTimerReady(spells[spell_id].timer_id)) - return false; - - if (!ignore_casting_spell_checks){ - casting_spell_checks = true; + /* + Cannot cast life tap on self + */ + if (this == spell_target && IsLifetapSpell(spell_id)) { + LogSpells("You cannot lifetap yourself"); + MessageString(Chat::SpellFailure, CANT_DRAIN_SELF); + return false; } + /* + Cannot cast sacrifice on self + */ + if (this == spell_target && IsSacrificeSpell(spell_id)) { + LogSpells("You cannot sacrifice yourself"); + MessageString(Chat::SpellFailure, CANNOT_SAC_SELF); + return false; + } + /* + Max level of target for harmony to take hold + */ + if (IsClient() && IsHarmonySpell(spell_id) && !HarmonySpellLevelCheck(spell_id, spell_target)) { + MessageString(Chat::SpellFailure, SPELL_NO_EFFECT); + LogSpells("Spell casting canceled [{}] : can not use harmony on this target.", spell_id); + return false; + } + /* + Various charm related target restrictions + */ + if (IsEffectInSpell(spell_id, SE_Charm) && !PassCharmTargetRestriction(spell_target)) { + LogSpells("Spell casting canceled [{}] : can not use charm on this target.", spell_id); + return false; + } + /* + Requires target to be in same group or same raid in order to apply invisible. + */ + if (check_on_casting && RuleB(Spells, InvisRequiresGroup) && IsInvisSpell(spell_id)) { + if (IsClient() && spell_target && spell_target->IsClient()) { + if (spell_target && spell_target->GetID() != GetID()) { + bool cast_failed = true; + if (spell_target->IsGrouped()) { + Group *target_group = spell_target->GetGroup(); + Group *my_group = GetGroup(); + if (target_group && + my_group && + (target_group->GetID() == my_group->GetID())) { + cast_failed = false; + } + } + else if (spell_target->IsRaidGrouped()) { + Raid *target_raid = spell_target->GetRaid(); + Raid *my_raid = GetRaid(); + if (target_raid && + my_raid && + (target_raid->GetGroup(spell_target->CastToClient()) == my_raid->GetGroup(this->CastToClient()))) { + cast_failed = false; + } + } + + if (cast_failed) { + MessageString(Chat::Red, TARGET_GROUP_MEMBER); + return false; + } + } + } + } + return true; } @@ -912,6 +1121,16 @@ void Mob::ZeroCastingVars() delaytimer = false; } + +//This will cause server to stop trying to pulse a bard song. Does not stop song clientside. +void Mob::ZeroBardPulseVars() +{ + bardsong = 0; + bardsong_target_id = 0; + bardsong_slot = CastingSlot::Gem1; + bardsong_timer.Disable(); +} + void Mob::InterruptSpell(uint16 spellid) { if (spellid == SPELL_UNKNOWN) @@ -951,8 +1170,9 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) if(!spellid) return; - if (bardsong || IsBardSong(casting_spell_id)) - _StopSong(); + if (bardsong || IsBardSong(casting_spell_id)) { + ZeroBardPulseVars(); + } if(bard_song_mode) { return; @@ -1040,6 +1260,23 @@ void Mob::StopCasting() ZeroCastingVars(); } +void Mob::StopCastSpell(int32 spell_id, bool send_spellbar_enable) +{ + /* + This is used when spells fail at CastSpell or when CastSpell is bypassed and spell is launched initially from SpellFinished. + send_spellbar_enabled is false when the following + -AA that fail at CastSpell because they never get timer set. + -Instant cast items that fail at CastSpell because they never get timer set. + */ + if (casting_spell_id && IsNPC()) { + CastToNPC()->AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); + } + + if (send_spellbar_enable) { + SendSpellBarEnable(spell_id); + } +} + // this is called after the timer is up and the spell is finished // casting. everything goes through here, including items with zero cast time // only to be used from SpellProcess @@ -1049,6 +1286,13 @@ void Mob::StopCasting() void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slot, uint16 mana_used, uint32 inventory_slot, int16 resist_adjust) { + if (!IsValidSpell(spell_id)) + { + LogSpells("Casting of [{}] canceled: invalid spell id", spell_id); + InterruptSpell(); + return; + } + bool IsFromItem = false; EQ::ItemInstance *item = nullptr; @@ -1062,31 +1306,22 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo } } /* + Reinforcement only. Checks Item and Augment click recasts. Titanium client will prevent item recast on its own. This is only used to enforce. Titanium items are cast from Handle_OP_CastSpell. SOF+ client does not prevent item recast on its own. We enforce this in Handle_OP_ItemVerifyRequest where items are cast from. */ if(IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt)) { IsFromItem = true; - item = CastToClient()->GetInv().GetItem(inventory_slot); - if(item && item->GetItem()->RecastDelay > 0) - { - if(!CastToClient()->GetPTimers().Expired(&database, (pTimerItemStart + item->GetItem()->RecastType), false)) { - MessageString(Chat::Red, SPELL_RECAST); - LogSpells("Casting of [{}] canceled: item spell reuse timer not expired", spell_id); - StopCasting(); - return; - } + item = CastToClient()->GetInv().GetItem(inventory_slot); //checked for in reagents and charges. + if (CastToClient()->HasItemRecastTimer(spell_id, inventory_slot)) { + MessageString(Chat::Red, SPELL_RECAST); + LogSpells("Casting of [{}] canceled: item spell reuse timer not expired", spell_id); + StopCasting(); + return; } } - if(!IsValidSpell(spell_id)) - { - LogSpells("Casting of [{}] canceled: invalid spell id", spell_id); - InterruptSpell(); - return; - } - // prevent rapid recast - this can happen if somehow the spell gems // become desynced and the player casts again. if(IsClient()) @@ -1116,27 +1351,27 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo // a spell bar slot if(GetClass() == BARD) // bard's can move when casting any spell... { - if (IsBardSong(spell_id)) { - if(spells[spell_id].buff_duration == 0xFFFF) { + if (IsBardSong(spell_id) && slot < CastingSlot::MaxGems) { + if (spells[spell_id].buff_duration == 0xFFFF) { LogSpells("Bard song [{}] not applying bard logic because duration. dur=[{}], recast=[{}]", spells[spell_id].buff_duration); - } else { - // So long recast bard songs need special bard logic, although the effects don't repulse like other songs - // This is basically a hack to get that effect - // You can hold down the long recast spells, but you only get the effects once - // Songs with mana cost also do not repulse - // AAs that use SE_TemporaryPets or SE_Familiar also do not repulse - // TODO fuck bards. - if (spells[spell_id].recast_time == 0 && spells[spell_id].mana == 0 && !IsEffectInSpell(spell_id, SE_TemporaryPets) && !IsEffectInSpell(spell_id, SE_Familiar)) { + } + else { + if (IsPulsingBardSong(spell_id)) { bardsong = spell_id; bardsong_slot = slot; - //NOTE: theres a lot more target types than this to think about... - if (spell_target == nullptr || (spells[spell_id].target_type != ST_Target && spells[spell_id].target_type != ST_AETarget)) - bardsong_target_id = GetID(); - else + + if (spell_target) { bardsong_target_id = spell_target->GetID(); + } + else if (spells[spell_id].target_type != ST_Target && spells[spell_id].target_type != ST_AETarget) { + bardsong_target_id = GetID(); //This is a failsafe, you should always have a spell_target unless that target died/zoned. + } + else { + InterruptSpell(); + } bardsong_timer.Start(6000); } - LogSpells("Bard song [{}] started: slot [{}], target id [{}]", bardsong, (int) bardsong_slot, bardsong_target_id); + LogSpells("Bard song [{}] started: slot [{}], target id [{}]", bardsong, (int)bardsong_slot, bardsong_target_id); bard_song_mode = true; } } @@ -1270,7 +1505,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo continue; // no instrument required, go to next component // percussion songs (13000 = hand drum) - case 13000: + case INSTRUMENT_HAND_DRUM: if(itembonuses.percussionMod == 0) { // check for the appropriate instrument type HasInstrument = false; c->MessageString(Chat::Red, SONG_NEEDS_DRUM); // send an error message if missing @@ -1278,7 +1513,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo break; // wind songs (13001 = wooden flute) - case 13001: + case INSTRUMENT_WOODEN_FLUTE: if(itembonuses.windMod == 0) { HasInstrument = false; c->MessageString(Chat::Red, SONG_NEEDS_WIND); @@ -1286,7 +1521,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo break; // string songs (13011 = lute) - case 13011: + case INSTRUMENT_LUTE: if(itembonuses.stringedMod == 0) { HasInstrument = false; c->MessageString(Chat::Red, SONG_NEEDS_STRINGS); @@ -1294,7 +1529,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo break; // brass songs (13012 = horn) - case 13012: + case INSTRUMENT_HORN: if(itembonuses.brassMod == 0) { HasInstrument = false; c->MessageString(Chat::Red, SONG_NEEDS_BRASS); @@ -1422,29 +1657,6 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo break; } - //Test the aug recast delay - if(IsClient() && fromaug && recastdelay > 0) - { - if(!CastToClient()->GetPTimers().Expired(&database, (pTimerItemStart + recasttype), false)) { - MessageString(Chat::Red, SPELL_RECAST); - LogSpells("Casting of [{}] canceled: item spell reuse timer not expired", spell_id); - StopCasting(); - return; - } - else - { - //Can we start the timer here? I don't see why not. - CastToClient()->GetPTimers().Start((pTimerItemStart + recasttype), recastdelay); - if (recasttype != -1) { - database.UpdateItemRecastTimestamps( - CastToClient()->CharacterID(), - recasttype, - CastToClient()->GetPTimers().Get(pTimerItemStart + recasttype)->GetReadyTimestamp() - ); - } - } - } - if (item && item->IsClassCommon() && (item->GetItem()->Click.Effect == spell_id) && item->GetCharges() || fromaug) { //const ItemData* item = item->GetItem(); @@ -1469,7 +1681,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo } // we're done casting, now try to apply the spell - if( !SpellFinished(spell_id, spell_target, slot, mana_used, inventory_slot, resist_adjust) ) + if(!SpellFinished(spell_id, spell_target, slot, mana_used, inventory_slot, resist_adjust, false,-1, 0xFFFFFFFF, 0, true)) { LogSpells("Casting of [{}] canceled: SpellFinished returned false", spell_id); // most of the cases we return false have a message already or are logic errors that shouldn't happen @@ -1482,9 +1694,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo CheckNumHitsRemaining(NumHit::MatchingSpells); TrySympatheticProc(target, spell_id); } - - TryOnSpellFinished(this, target, spell_id); //Use for effects that should be checked after SpellFinished is completed. - + TryTwincast(this, target, spell_id); TryTriggerOnCastFocusEffect(focusTriggerOnCast, spell_id); @@ -1492,24 +1702,6 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo if(DeleteChargeFromSlot >= 0) CastToClient()->DeleteItemInInventory(DeleteChargeFromSlot, 1, true); - if (IsClient() && IsEffectInSpell(spell_id, SE_BindSight)) { - for (int i = 0; i < GetMaxTotalSlots(); i++) { - if (buffs[i].spellid == spell_id) { - CastToClient()->SendBuffNumHitPacket(buffs[i], i);//its hack, it works. - } - } - } - - //Check if buffs has numhits, then resend packet so it displays the hit count. - if (IsClient() && spells[spell_id].hit_number) { - for (int i = 0; i < GetMaxTotalSlots(); i++) { - if (buffs[i].spellid == spell_id && buffs[i].hit_number > 0) { - CastToClient()->SendBuffNumHitPacket(buffs[i], i); - break; - } - } - } - // // at this point the spell has successfully been cast // @@ -1535,6 +1727,10 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, CastingSlot slo if (RuleB(Spells, EnableBardMelody)) { c->MemorizeSpell(static_cast(slot), spell_id, memSpellSpellbar, casting_spell_recast_adjust); } + + if (!IsFromItem) { + c->CheckSongSkillIncrease(spell_id); + } } LogSpells("Bard song [{}] should be started", spell_id); } @@ -1632,62 +1828,6 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce if (isproc && IsNPC() && CastToNPC()->GetInnateProcSpellID() == spell_id) targetType = ST_Target; - if (spell_target && spells[spell_id].cast_restriction && !spell_target->PassCastRestriction(spells[spell_id].cast_restriction)){ - Message(Chat::Red, "Your target does not meet the spell requirements."); //Current live also adds description after this from dbstr_us type 39 - return false; - } - - if (spells[spell_id].caster_requirement_id && !PassCastRestriction(spells[spell_id].caster_requirement_id)) { - MessageString(Chat::Red, SPELL_WOULDNT_HOLD); - return false; - } - - //Must be out of combat. (If Beneficial checks casters combat state, Deterimental checks targets) - if (!spells[spell_id].can_cast_in_combat && spells[spell_id].can_cast_out_of_combat) { - if (IsDetrimentalSpell(spell_id)) { - if (spell_target && - ((spell_target->IsNPC() && spell_target->IsEngaged()) || - (spell_target->IsClient() && spell_target->CastToClient()->GetAggroCount()))) { - MessageString(Chat::Red, SPELL_NO_EFFECT); // Unsure correct string - return false; - } - } - - else if (IsBeneficialSpell(spell_id)) { - if ((IsNPC() && IsEngaged()) || (IsClient() && CastToClient()->GetAggroCount())) { - if (IsDiscipline(spell_id)) - MessageString(Chat::Red, NO_ABILITY_IN_COMBAT); - else - MessageString(Chat::Red, NO_CAST_IN_COMBAT); - - return false; - } - } - } - - // Must be in combat. (If Beneficial checks casters combat state, Deterimental checks targets) - else if (spells[spell_id].can_cast_in_combat && !spells[spell_id].can_cast_out_of_combat) { - if (IsDetrimentalSpell(spell_id)) { - if (spell_target && - ((spell_target->IsNPC() && !spell_target->IsEngaged()) || - (spell_target->IsClient() && !spell_target->CastToClient()->GetAggroCount()))) { - MessageString(Chat::Red, SPELL_NO_EFFECT); // Unsure correct string - return false; - } - } - - else if (IsBeneficialSpell(spell_id)) { - if ((IsNPC() && !IsEngaged()) || (IsClient() && !CastToClient()->GetAggroCount())) { - if (IsDiscipline(spell_id)) - MessageString(Chat::Red, NO_ABILITY_OUT_OF_COMBAT); - else - MessageString(Chat::Red, NO_CAST_OUT_OF_COMBAT); - - return false; - } - } - } - switch (targetType) { // single target spells @@ -2160,9 +2300,8 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce // if you need to abort the casting, return false bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, uint16 mana_used, uint32 inventory_slot, int16 resist_adjust, bool isproc, int level_override, - uint32 timer, uint32 timer_duration) + uint32 timer, uint32 timer_duration, bool from_casted_spell) { - //EQApplicationPacket *outapp = nullptr; Mob *ae_center = nullptr; if(!IsValidSpell(spell_id)) @@ -2177,7 +2316,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, ui } } - //Guard Assist Code + //Guard Assist Code if (RuleB(Character, PVPEnableGuardFactionAssist) && spell_target && IsDetrimentalSpell(spell_id) && spell_target != this) { if (IsClient() && spell_target->IsClient()|| (HasOwner() && GetOwner()->IsClient() && spell_target->IsClient())) { auto& mob_list = entity_list.GetCloseMobList(spell_target); @@ -2193,72 +2332,27 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, ui } } } - } - } - - if( spells[spell_id].zone_type == 1 && !zone->CanCastOutdoor()){ - if(IsClient()){ - if(!CastToClient()->GetGM()){ - MessageString(Chat::Red, CAST_OUTDOORS); - return false; - } - } - } - - if(IsEffectInSpell(spell_id, SE_Levitate) && !zone->CanLevitate()){ - if(IsClient()){ - if(!CastToClient()->GetGM()){ - Message(Chat::Red, "You can't levitate in this zone."); - return false; - } - } - } - - if(IsClient() && !CastToClient()->GetGM()){ - - if(zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))){ - const char *msg = zone->GetSpellBlockedMessage(spell_id, glm::vec3(GetPosition())); - if(msg){ - Message(Chat::Red, msg); - return false; - } - else{ - Message(Chat::Red, "You can't cast this spell here."); - return false; - } - } } - if (IsClient() && CastToClient()->GetGM()){ - if (zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))){ - LogSpells("GM Cast Blocked Spell: [{}] (ID [{}])", GetSpellName(spell_id), spell_id); + //If spell was casted then we already checked these so skip, otherwise check here if being called directly from spell finished. + if (!from_casted_spell){ + if (!DoCastingChecksZoneRestrictions(true, spell_id)) { + LogSpells("Spell [{}]: Zone restriction failure.", spell_id); + return false; } - } - - if - ( - this->IsClient() && - (zone->GetZoneID() == 183 || zone->GetZoneID() == 184) && // load - CastToClient()->Admin() < AccountStatus::QuestTroupe - ) - { - if - ( - IsEffectInSpell(spell_id, SE_Gate) || - IsEffectInSpell(spell_id, SE_Translocate) || - IsEffectInSpell(spell_id, SE_Teleport) - ) - { - Message(0, "The Gods brought you here, only they can send you away."); + if (!DoCastingChecksOnTarget(true, spell_id, spell_target)) { + LogSpells("Spell [{}]: Casting checks on Target failure.", spell_id); return false; } } //determine the type of spell target we have CastAction_type CastAction; - if(!DetermineSpellTargets(spell_id, spell_target, ae_center, CastAction, slot, isproc)) + if (!DetermineSpellTargets(spell_id, spell_target, ae_center, CastAction, slot, isproc)) { + LogSpells("Spell [{}]: Determine spell targets failure.", spell_id); return(false); + } LogSpells("Spell [{}]: target type [{}], target [{}], AE center [{}]", spell_id, CastAction, spell_target?spell_target->GetName():"NONE", ae_center?ae_center->GetName():"NONE"); @@ -2299,11 +2393,10 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, ui range = spells[spell_id].aoe_range; range = GetActSpellRange(spell_id, range); - if(IsPlayerIllusionSpell(spell_id) - && IsClient() - && (HasProjectIllusion())){ + if(IsClient() && IsPlayerIllusionSpell(spell_id) && (HasProjectIllusion())){ range = 100; } + if(spell_target != nullptr && spell_target != this) { //casting a spell on somebody but ourself, make sure they are in range float dist2 = DistanceSquared(m_Position, spell_target->GetPosition()); @@ -2578,16 +2671,18 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, ui SetEndurance(GetEndurance() - EQ::ClampUpper(end_cost, GetEndurance())); TryTriggerOnCastRequirement(); } - if (mgb) + if (mgb) { SetMGB(false); - - //set our reuse timer on long ass reuse_time spells... + } + /* + Set Recast Timer on spells. + */ if(IsClient() && !isproc) { - //Support for bards to get disc recast timers while singing. + //Support for bards to get disc recast timers while singing if (GetClass() == BARD && spell_id != casting_spell_id && timer != 0xFFFFFFFF) { CastToClient()->GetPTimers().Start(timer, timer_duration); - LogSpells("Spell [{}]: Setting bard custom disciple reuse timer [{}] to [{}]", spell_id, timer, timer_duration); + LogSpells("Spell [{}]: Setting bard disciple reuse timer from spell finished [{}] to [{}]", spell_id, timer, timer_duration); } if(casting_spell_aa_id) { @@ -2632,293 +2727,72 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, ui } } } + /* + Set Recast Timer on item clicks, including augmenets. + */ + if(IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt)){ + CastToClient()->SetItemRecastTimer(spell_id, inventory_slot); + } - if(IsClient() && (slot == CastingSlot::Item || slot == CastingSlot::PotionBelt)) - { - EQ::ItemInstance *itm = CastToClient()->GetInv().GetItem(inventory_slot); - if(itm && itm->GetItem()->RecastDelay > 0){ - auto recast_type = itm->GetItem()->RecastType; - int recast_delay = itm->GetItem()->RecastDelay; - //must use SPA 415 with focus (SPA 310) to reduce item recast - int reduction = CastToClient()->GetFocusEffect(focusReduceRecastTime, spell_id); - if (reduction) { - recast_delay -= reduction; - } - recast_delay = std::max(recast_delay, 0); + if (IsNPC()) { + CastToNPC()->AI_Event_SpellCastFinished(true, static_cast(slot)); + } - if (recast_delay > 0) { + ApplyHealthTransferDamage(this, target, spell_id); - CastToClient()->GetPTimers().Start((pTimerItemStart + recast_type), recast_delay); - if (recast_type != -1) { - database.UpdateItemRecastTimestamps( - CastToClient()->CharacterID(), - recast_type, - CastToClient()->GetPTimers().Get(pTimerItemStart + recast_type)->GetReadyTimestamp() - ); - } - - auto outapp = new EQApplicationPacket(OP_ItemRecastDelay, sizeof(ItemRecastDelay_Struct)); - ItemRecastDelay_Struct *ird = (ItemRecastDelay_Struct *)outapp->pBuffer; - ird->recast_delay = static_cast(recast_delay); - ird->recast_type = recast_type; - CastToClient()->QueuePacket(outapp); - safe_delete(outapp); + //This needs to be here for bind sight to update correctly on client. + if (IsClient() && IsEffectInSpell(spell_id, SE_BindSight)) { + for (int i = 0; i < GetMaxTotalSlots(); i++) { + if (buffs[i].spellid == spell_id) { + CastToClient()->SendBuffNumHitPacket(buffs[i], i);//its hack, it works. + } + } + } + //Check if buffs has numhits, then resend packet so it displays the hit count. + if (IsClient() && spells[spell_id].hit_number) { + for (int i = 0; i < GetMaxTotalSlots(); i++) { + if (buffs[i].spellid == spell_id && buffs[i].hit_number > 0) { + CastToClient()->SendBuffNumHitPacket(buffs[i], i); + break; } } } - - if(IsNPC()) - CastToNPC()->AI_Event_SpellCastFinished(true, static_cast(slot)); return true; } -/* - * handle bard song pulses... - * - * we make several assumptions that SpellFinished does not: - * - there are no AEDuration (beacon) bard songs - * - there are no recourse spells on bard songs - * - there is no long recast delay on bard songs - * - * return false to stop the song - */ -bool Mob::ApplyNextBardPulse(uint16 spell_id, Mob *spell_target, CastingSlot slot) { - if(slot == CastingSlot::Item) { - //bard songs should never come from items... - LogSpells("Bard Song Pulse [{}]: Supposidly cast from an item. Killing song", spell_id); - return(false); +bool Mob::ApplyBardPulse(int32 spell_id, Mob *spell_target, CastingSlot slot) { + + /* + Check any bard specific special behaviors we need before applying the next pulse. + Note: Silence does not stop an active bard pulse. + */ + if (!spell_target) { + return false; + } + /* + Bard song charm that have no mana will continue to try and pulse on target, but will only reapply when charm fades. + Live does not spam client with do not take hold messages. Checking here avoids that from happening. Only try to reapply if charm fades. + */ + if (spell_target->IsCharmed() && spells[spell_id].mana == 0 && spell_target->GetOwner() == this && IsEffectInSpell(spell_id, SE_Charm)) { + return true; + } + /* + If divine aura applied while pulsing, it is not interrupted but does not reapply until DA fades. + */ + if (DivineAura() && !IgnoreCastingRestriction(spell_id)) { + return true; + } + /* + Fear will stop pulsing. + */ + if (IsFeared()) { + return false; } - //determine the type of spell target we have - Mob *ae_center = nullptr; - CastAction_type CastAction; - if(!DetermineSpellTargets(spell_id, spell_target, ae_center, CastAction, slot)) { - LogSpells("Bard Song Pulse [{}]: was unable to determine target. Stopping", spell_id); - return(false); + if (!SpellFinished(spell_id, spell_target, slot, spells[spell_id].mana, 0xFFFFFFFF, spells[spell_id].resist_difficulty)) { + return false; } - - if(ae_center != nullptr && ae_center->IsBeacon()) { - LogSpells("Bard Song Pulse [{}]: Unsupported Beacon NPC AE spell", spell_id); - return(false); - } - - //use mana, if this spell has a mana cost - int mana_used = spells[spell_id].mana; - if(mana_used > 0) { - if(mana_used > GetMana()) { - //ran out of mana... this calls StopSong() for us - LogSpells("Ran out of mana while singing song [{}]", spell_id); - return(false); - } - - LogSpells("Bard Song Pulse [{}]: consuming [{}] mana (have [{}])", spell_id, mana_used, GetMana()); - SetMana(GetMana() - mana_used); - } - - // check line of sight to target if it's a detrimental spell - if(spell_target && IsDetrimentalSpell(spell_id) && !CheckLosFN(spell_target)) - { - LogSpells("Bard Song Pulse [{}]: cannot see target [{}]", spell_target->GetName()); - MessageString(Chat::Red, CANT_SEE_TARGET); - return(false); - } - - //range check our target, if we have one and it is not us - float range = 0.00f; - - range = GetActSpellRange(spell_id, spells[spell_id].range, true); - if(spell_target != nullptr && spell_target != this) { - //casting a spell on somebody but ourself, make sure they are in range - float dist2 = DistanceSquared(m_Position, spell_target->GetPosition()); - float range2 = range * range; - if(dist2 > range2) { - //target is out of range. - LogSpells("Bard Song Pulse [{}]: Spell target is out of range (squared: [{}] > [{}])", spell_id, dist2, range2); - MessageString(Chat::Red, TARGET_OUT_OF_RANGE); - return(false); - } - } - - // - // Switch #2 - execute the spell - // - switch(CastAction) - { - default: - case CastActUnknown: - case SingleTarget: - { - if(spell_target == nullptr) { - LogSpells("Bard Song Pulse [{}]: Targeted spell, but we have no target", spell_id); - return(false); - } - LogSpells("Bard Song Pulse: Targeted. spell [{}], target [{}]", spell_id, spell_target->GetName()); - spell_target->BardPulse(spell_id, this); - break; - } - - case AECaster: - { - if(IsBeneficialSpell(spell_id)) - SpellOnTarget(spell_id, this); - - bool affect_caster = !IsNPC(); //NPC AE spells do not affect the NPC caster - entity_list.AEBardPulse(this, this, spell_id, affect_caster); - break; - } - case AETarget: - { - // we can't cast an AE spell without something to center it on - if(ae_center == nullptr) { - LogSpells("Bard Song Pulse [{}]: AE Targeted spell, but we have no target", spell_id); - return(false); - } - - // regular PB AE or targeted AE spell - spell_target is null if PB - if(spell_target) { // this must be an AETarget spell - // affect the target too - spell_target->BardPulse(spell_id, this); - LogSpells("Bard Song Pulse: spell [{}], AE target [{}]", spell_id, spell_target->GetName()); - } else { - LogSpells("Bard Song Pulse: spell [{}], AE with no target", spell_id); - } - bool affect_caster = !IsNPC(); //NPC AE spells do not affect the NPC caster - entity_list.AEBardPulse(this, ae_center, spell_id, affect_caster); - break; - } - - case GroupSpell: - { - if(spell_target->IsGrouped()) { - LogSpells("Bard Song Pulse: spell [{}], Group targeting group of [{}]", spell_id, spell_target->GetName()); - Group *target_group = entity_list.GetGroupByMob(spell_target); - if(target_group) - target_group->GroupBardPulse(this, spell_id); - } - else if(spell_target->IsRaidGrouped() && spell_target->IsClient()) { - LogSpells("Bard Song Pulse: spell [{}], Raid group targeting raid group of [{}]", spell_id, spell_target->GetName()); - Raid *r = entity_list.GetRaidByClient(spell_target->CastToClient()); - if(r){ - uint32 gid = r->GetGroup(spell_target->GetName()); - if(gid < 12){ - r->GroupBardPulse(this, spell_id, gid); - } - else{ - BardPulse(spell_id, this); -#ifdef GROUP_BUFF_PETS - if (GetPet() && HasPetAffinity() && !GetPet()->IsCharmed()) - GetPet()->BardPulse(spell_id, this); -#endif - } - } - } - else { - LogSpells("Bard Song Pulse: spell [{}], Group target without group. Affecting caster", spell_id); - BardPulse(spell_id, this); -#ifdef GROUP_BUFF_PETS - if (GetPet() && HasPetAffinity() && !GetPet()->IsCharmed()) - GetPet()->BardPulse(spell_id, this); -#endif - } - break; - } - } - - if(IsClient()) - CastToClient()->CheckSongSkillIncrease(spell_id); - - return(true); -} - -void Mob::BardPulse(uint16 spell_id, Mob *caster) { - // so for Solon's Song of the Sirens (725) if we're repulsing, we need to skip - // other charms have mana and don't repulse - // This is probably not the ideal place for this, but it will work - if (IsCharmed() && GetOwner() == caster && IsEffectInSpell(spell_id, SE_Charm)) { - return; - } - int buffs_i; - int buff_count = GetMaxTotalSlots(); - for (buffs_i = 0; buffs_i < buff_count; buffs_i++) { - if(buffs[buffs_i].spellid != spell_id) - continue; - if(buffs[buffs_i].casterid != caster->GetID()) { - LogSpells("Bard Pulse for [{}]: found buff from caster [{}] and we are pulsing for [{}] are there two bards playing the same song???", spell_id, buffs[buffs_i].casterid, caster->GetID()); - return; - } - //extend the spell if it will expire before the next pulse - if(buffs[buffs_i].ticsremaining <= 3) { - buffs[buffs_i].ticsremaining += 3; - LogSpells("Bard Song Pulse [{}]: extending duration in slot [{}] to [{}] tics", spell_id, buffs_i, buffs[buffs_i].ticsremaining); - } - - //should we send this buff update to the client... seems like it would - //be a lot of traffic for no reason... -//this may be the wrong packet... - if(IsClient()) { - auto packet = new EQApplicationPacket(OP_Action, sizeof(Action_Struct)); - - Action_Struct* action = (Action_Struct*) packet->pBuffer; - action->source = caster->GetID(); - action->target = GetID(); - action->spell = spell_id; - action->force = spells[spell_id].push_back; - action->hit_heading = GetHeading(); - action->hit_pitch = spells[spell_id].push_up; - action->instrument_mod = caster->GetInstrumentMod(spell_id); - action->effect_flag = 0; - action->spell_level = action->level = buffs[buffs_i].casterlevel; - action->type = DamageTypeSpell; - entity_list.QueueCloseClients(this, packet, false, RuleI(Range, SongMessages), 0, true, IsClient() ? FilterPCSpells : FilterNPCSpells); - - action->effect_flag = 4; - - if (spells[spell_id].push_back != 0.0f || spells[spell_id].push_up != 0.0f) - { - if (IsClient()) - { - if (!IsBuffSpell(spell_id)) - { - CastToClient()->cheat_manager.SetExemptStatus(KnockBack, true); - } - } - } - - if (IsClient() && IsEffectInSpell(spell_id, SE_ShadowStep)) - { - CastToClient()->cheat_manager.SetExemptStatus(ShadowStep, true); - } - - if(!IsEffectInSpell(spell_id, SE_BindAffinity)) - { - CastToClient()->QueuePacket(packet); - } - - auto message_packet = new EQApplicationPacket(OP_Damage, sizeof(CombatDamage_Struct)); - CombatDamage_Struct *cd = (CombatDamage_Struct *)message_packet->pBuffer; - cd->target = action->target; - cd->source = action->source; - cd->type = DamageTypeSpell; - cd->spellid = action->spell; - cd->force = action->force; - cd->hit_heading = action->hit_heading; - cd->hit_pitch = action->hit_pitch; - cd->damage = 0; - if(!IsEffectInSpell(spell_id, SE_BindAffinity)) - { - entity_list.QueueCloseClients(this, message_packet, false, RuleI(Range, SongMessages), 0, true, IsClient() ? FilterPCSpells : FilterNPCSpells); - } - safe_delete(message_packet); - safe_delete(packet); - - } - //we are done... - return; - } - LogSpells("Bard Song Pulse [{}]: Buff not found, reapplying spell", spell_id); - //this spell is not affecting this mob, apply it. - caster->SpellOnTarget(spell_id, this); } /////////////////////////////////////////////////////////////////////////////// @@ -3501,6 +3375,8 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid return -1; } } + //do not fade buff if from bard pulse, live does not give a fades message. + bool from_bard_song_pulse = caster ? caster->IsActiveBardSong(spell_id) : false; // at this point we know that this buff will stick, but we have // to remove some other buffs already worn if will_overwrite is true @@ -3510,7 +3386,9 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid end = overwrite_slots.end(); for (; cur != end; ++cur) { // strip spell - BuffFadeBySlot(*cur, false); + if (!from_bard_song_pulse) { + BuffFadeBySlot(*cur, false); + } // if we hadn't found a free slot before, or if this is earlier // we use it @@ -3653,8 +3531,6 @@ int Mob::CanBuffStack(uint16 spellid, uint8 caster_level, bool iFailIfOverwrite) bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, int reflect_effectiveness, bool use_resist_adjust, int16 resist_adjust, bool isproc, int level_override, int32 duration_override) { - bool is_damage_or_lifetap_spell = IsDamageSpell(spell_id) || IsLifetapSpell(spell_id); - // well we can't cast a spell on target without a target if(!spelltar) { @@ -3667,6 +3543,11 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, int reflect_effectivenes return false; } + if (!IsValidSpell(spell_id)) + return false; + + bool is_damage_or_lifetap_spell = IsDamageSpell(spell_id) || IsLifetapSpell(spell_id); + if(IsDetrimentalSpell(spell_id) && !IsAttackAllowed(spelltar, true) && !IsResurrectionEffects(spell_id) && !IsEffectInSpell(spell_id, SE_BindSight)) { if(!IsClient() || !CastToClient()->GetGM()) { MessageString(Chat::SpellFailure, SPELL_NO_HOLD); @@ -3674,12 +3555,13 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, int reflect_effectivenes } } + if (!DoCastingChecksOnTarget(false, spell_id, spelltar)) { + return false; + } + EQApplicationPacket *action_packet = nullptr, *message_packet = nullptr; float spell_effectiveness; - if(!IsValidSpell(spell_id)) - return false; - // these target types skip pcnpc only check (according to dev quotes) // other AE spells this is redundant, oh well // 1 = PCs, 2 = NPCs @@ -6025,17 +5907,6 @@ int Mob::GetCasterLevel(uint16 spell_id) { return std::max(1, level); } -//this method does NOT tell the client to stop singing the song. -//this is NOT the right way to stop a mob from singing, use InterruptSpell -//you should really know what your doing before you call this -void Mob::_StopSong() -{ - bardsong = 0; - bardsong_target_id = 0; - bardsong_slot = CastingSlot::Gem1; - bardsong_timer.Disable(); -} - //This member function sets the buff duration on the client //however it does not work if sent quickly after an action packets, which is what one might perfer to do //Thus I use this in the buff process to update the correct duration once after casting @@ -6280,8 +6151,12 @@ void Client::SendSpellAnim(uint16 targetid, uint16 spell_id) entity_list.QueueCloseClients(this, &app, false, RuleI(Range, SpellParticles)); } -void Client::SendItemRecastTimer(uint32 recast_type, uint32 recast_delay) +void Client::SendItemRecastTimer(int32 recast_type, uint32 recast_delay) { + if (recast_type == -1) { + return; + } + if (!recast_delay) { recast_delay = GetPTimers().GetRemainingTime(pTimerItemStart + recast_type); } @@ -6290,12 +6165,125 @@ void Client::SendItemRecastTimer(uint32 recast_type, uint32 recast_delay) auto outapp = new EQApplicationPacket(OP_ItemRecastDelay, sizeof(ItemRecastDelay_Struct)); ItemRecastDelay_Struct *ird = (ItemRecastDelay_Struct *)outapp->pBuffer; ird->recast_delay = recast_delay; - ird->recast_type = recast_type; + ird->recast_type = static_cast(recast_type); QueuePacket(outapp); safe_delete(outapp); } } +void Client::SetItemRecastTimer(int32 spell_id, uint32 inventory_slot) +{ + EQ::ItemInstance *item = CastToClient()->GetInv().GetItem(inventory_slot); + + int recast_delay = 0; + int recast_type = 0; + bool from_augment = false; + + if (!item) { + return; + } + + //Check primary item. + if (item->GetItem()->RecastDelay > 0) { + recast_type = item->GetItem()->RecastType; + recast_delay = item->GetItem()->RecastDelay; + } + //Check augmenent + else{ + for (int r = EQ::invaug::SOCKET_BEGIN; r <= EQ::invaug::SOCKET_END; r++) { + const EQ::ItemInstance* aug_i = item->GetAugment(r); + + if (!aug_i) { + continue; + } + const EQ::ItemData* aug = aug_i->GetItem(); + if (!aug) { + continue; + } + + if (aug->Click.Effect == spell_id) { + recast_delay = aug_i->GetItem()->RecastDelay; + recast_type = aug_i->GetItem()->RecastType; + from_augment = true; + break; + } + } + } + //must use SPA 415 with focus (SPA 310) to reduce item recast + int reduction = GetFocusEffect(focusReduceRecastTime, spell_id); + if (reduction) { + recast_delay -= reduction; + } + + recast_delay = std::max(recast_delay, 0); + + if (recast_delay > 0) { + + GetPTimers().Start((pTimerItemStart + recast_type), static_cast(recast_delay)); + if (recast_type != -1) { + database.UpdateItemRecastTimestamps( + CharacterID(), + recast_type, + GetPTimers().Get(pTimerItemStart + recast_type)->GetReadyTimestamp() + ); + } + + if (!from_augment) { + SendItemRecastTimer(recast_type, static_cast(recast_delay)); + } + } +} + +bool Client::HasItemRecastTimer(int32 spell_id, uint32 inventory_slot) +{ + EQ::ItemInstance *item = CastToClient()->GetInv().GetItem(inventory_slot); + + int recast_delay = 0; + int recast_type = 0; + bool from_augment = false; + + if (!item) { + return false; + } + + if (!item->GetItem()) { + return false; + } + + //Check primary item. + if (item->GetItem()->RecastDelay > 0) { + recast_type = item->GetItem()->RecastType; + recast_delay = item->GetItem()->RecastDelay; + } + //Check augmenent + else { + for (int r = EQ::invaug::SOCKET_BEGIN; r <= EQ::invaug::SOCKET_END; r++) { + const EQ::ItemInstance* aug_i = item->GetAugment(r); + + if (!aug_i) { + continue; + } + const EQ::ItemData* aug = aug_i->GetItem(); + if (!aug) { + continue; + } + + if (aug->Click.Effect == spell_id) { + recast_delay = aug_i->GetItem()->RecastDelay; + recast_type = aug_i->GetItem()->RecastType; + break; + } + } + } + + if (!CastToClient()->GetPTimers().Expired(&database, (pTimerItemStart + recast_type), false)) { + return true; + } + + return false; +} + + void Mob::CalcDestFromHeading(float heading, float distance, float MaxZDiff, float StartX, float StartY, float &dX, float &dY, float &dZ) { if (!distance) { return; } @@ -6563,3 +6551,43 @@ void Client::ResetCastbarCooldownBySpellID(uint32 spell_id) { } } } + +bool Mob::IsActiveBardSong(int32 spell_id) { + + if (spell_id == bardsong) { + return true; + } + return false; +} + +void Mob::DoBardCastingFromItemClick(bool is_casting_bard_song, uint32 cast_time, int32 spell_id, uint16 target_id, EQ::spells::CastingSlot slot, uint32 item_slot, uint32 recast_type, uint32 recast_delay) +{ + /* + Known bug: When a bard uses an augment with a clicky that has a cast time, the cast won't display. This issue only affects bards. + */ + if (is_casting_bard_song) { + //For spells with cast times. Cancel song cast, stop pusling and start item cast. + if (cast_time != 0) { + EQApplicationPacket *outapp = nullptr; + outapp = new EQApplicationPacket(OP_InterruptCast, sizeof(InterruptCast_Struct)); + InterruptCast_Struct* ic = (InterruptCast_Struct*)outapp->pBuffer; + ic->messageid = SONG_ENDS; + ic->spawnid = GetID(); + outapp->priority = 5; + CastToClient()->QueuePacket(outapp); + safe_delete(outapp); + + SendSpellBarDisable(); + ZeroCastingVars(); + ZeroBardPulseVars(); + } + } + + if (cast_time != 0) { + CastSpell(spell_id, target_id, CastingSlot::Item, cast_time, 0, 0, item_slot); + } + //Instant cast items do not stop bard songs or interrupt casting. + else if (DoCastingChecksOnCaster(spell_id)) { + SpellFinished(spell_id, entity_list.GetMob(target_id), CastingSlot::Item, 0, item_slot); + } +}