From 2b74d71ff5e86d40b24792ebc3a45535cf0292ba Mon Sep 17 00:00:00 2001 From: KayenEQ Date: Tue, 20 Jul 2021 11:06:20 -0400 Subject: [PATCH] [Feature] New SPAs pass 2 (#1459) * Implemented SPA Duration Pct Implemented new spell effects SE_Duration_HP_Pct 524 SE_Duration_Mana_Pct 525 SE_Duration_Endurance_Pct 526 Consumes 'base1' % of your maximum health/mana/endurance every 6 seconds. 'max' is maximum amount that can be consumed per tic. Additional Functionality Can be used as a heal/gain % by setting the base1 value to a positive. * Implemented SPA Instant Mana/End pct Fixes for SPA 524-526 Implemented SE_Instant_Mana_Pct 522 SE_Instant_Endurance_Pct 523 Extracts 'base1' percent of your maximum mana/endurance, or 'max', whichever is lower. * Implemented: SPA 521 EndAbsorbPctDmg Implemented SE_Endurance_Absorb_Pct_Damage 521 Absorb Damage using Endurance: base1 % (base2 End per 1 HP) Note: Both base1 and base2 need to be divided by 100 for actually value * Implemented SE_HealthTransfer 509 Implemented SE_Health_Transfer 509 'life burn' Consume base2 % of Hit Points to Damage for base % of Hit Points Can be used for heal Act of Valor * Implemented SPA 515,516,518,496 Implemented SE_AC_Avoidance_Max_Percent 515 SE_AC_Mitigation_Max_Percent 516 SE_Attack_Accuracy_Max_Percent 518 Above are stackable defense and offensive mods SE_Critical_Melee_Damage_Mod_Max 496 - This is a non stackable melee critical modifier * Implemented SPA 503 , 505 SE_Melee_Damage_Position_Mod 503 define SE_Damage_Taken_Position_Mod 505 SPA 503 increase/decreases melee damage by percent base1 based on your position base2 0=back 1=front SPA 504 increase/decreases melee damage taken by percent base1 based on your position base2 0=back 1=front * Implemented 467,468 Implemented SE_DS_Mitigation_Amount 467 SE_DS_Mitigation_Percentage 468 Reduce incoming DS by amt or percentage. base1 is value, if a reduction is desired it should be set to negative for both. * Fixes Formula fixes * Update spdat.h Added spa descriptions. * Implemented SPA 469, 470 Implemented SE_Chance_Best_in_Spell_Grp 469 Chance to cast highest scribed spell within a spell group. All base2 spells share roll chance, only 1 cast. SE_Trigger_Best_in_Spell_Grp 470 Chance to cast highest scribed spell within a spell group. Each spell has own chance. Additional Changes: Rewrote TrySpellTrigger function used for SPA 340 since it incorporates SPA 469. Improved code so that chance of spell being triggered should be more accurate statistically. * Implemented SPA 474, 494 Implemented SE_Pet_Crit_Melee_Damage_Pct_Owner 474 - Gives pets a critical melee damage modifier from the owner SE_Pet_Add_Atk 494 - Gives pet a ATK bonus from the owner Fixed SE_PetMeleeMitigation 397 - The bonus was not being calculated * Implemented SPA 465,477,478 Implemented SE_PC_Pet_AE_Rampage 465 Chance for pet to AE rampage with a damage modifier SE_Hatelist_To_Top_Index 477 Chance to be put on top of RAMPAGE list SE_Hatelist_To_Tail_Index 478 Chance to be put on bottom of RAMPAGE list * Implemented Implemented SE_Fearstun 502 Stun with a max level limit. Normal stun restrictions don't apply. Base1 duration, base2 PC duration, max is level limit SE_TwinCastBlocker 39 Previously unused spell effect that is now used on live. Simply, if this effect is present in a spell then the spell can not be twin cast. * Implemented SPA 483 Implemented Fc_Spell_Damage_Pct_IncomingPC 483 - Focus effect that modifies iby percent incoming spell damage on the target. Base1= min Base2= max. Final percent is random between max and min each time focus is applied from a spell cast. Note: Written to stack with similar functioning focus SPA 269 SE_FcSpellVulnerability. * Implemented SPA 484 Implemented SE_Fc_Spell_Damage_Amt_IncomingPC 484 // focus effect that modifies incoming spell damage by flat amount. Consider it a debuff that adds damage to incoming spells. Positive value to add additional damage. * Implemented SPA 481, 485,486,512 Implemented SE_Fc_Cast_Spell_On_Land 481 Focus effect that is checked when a spell is cast on a target, if target has this focus effect and all limiting criteria are met, then the target will cast a spell as specified by the focus. Can be given a roll chance for success. Base1=Chance, Base2=Spellid Note: This spell has a huge amount of potential applications. See 'Alliance' type spells on live. (ie live spell 50247) Implemented associated focus limits seen in live spells. SE_Ff_CasterClass 485 - Caster of spell on target with a focus effect that is checked by incoming spells must be specified class or classes. SE_Ff_Same_Caster 486 -Caster of spell on target with a focus effect that is checked by incoming spells 0=Must be different caster 1=Must be same caster The following is an associated effect seen with SPA 481 SE_Proc_Timer_Modifier 512 This provides a way to rate limit the amount of spell triggers generated by SPA 481. For example after 1 successful spell trigger no additional spells can be triggered for 1.5 seconds. Ie. Base=1 and Base2 1500. Written in a flexible format to allow scaling of multiple different buffs with this effect at same time. * Stacking fixes for new effects Stacking fixes for new effects. * merge with upstream master merge and update up spdat.h * Update spdat.h * Fix for bolt spell targeting self if target zone/died while casting. Fix for bolt spell targeting self if target zone/died while casting. Despite the name being "ST_TargetOptional", this target type is reserved for projectile spells which all require a target, thus should be treated like any other targeted spell. --- common/spdat.cpp | 6 + common/spdat.h | 47 +++--- zone/aggro.cpp | 1 + zone/attack.cpp | 77 ++++++--- zone/bonuses.cpp | 65 +++++++ zone/client.h | 1 + zone/client_process.cpp | 3 + zone/common.h | 11 +- zone/mob.cpp | 251 +++++++++++++++++++++------ zone/mob.h | 14 +- zone/mob_ai.cpp | 24 ++- zone/spell_effects.cpp | 365 +++++++++++++++++++++++++++++++++++++--- zone/spells.cpp | 38 ++++- zone/zonedb.cpp | 2 + 14 files changed, 766 insertions(+), 139 deletions(-) diff --git a/common/spdat.cpp b/common/spdat.cpp index fe6e4db9c..6c6e7dcd7 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -1218,6 +1218,12 @@ bool IsEffectIgnoredInStacking(int spa) case SE_LimitUseType: case SE_GravityEffect: case 425: + //Spell effects implemented after ROF2, following same pattern, lets assume these should go here. + case SE_Fc_Spell_Damage_Pct_IncomingPC: + case SE_Fc_Spell_Damage_Amt_IncomingPC: + case SE_Ff_CasterClass: + case SE_Ff_Same_Caster: + case SE_Proc_Timer_Modifier: return true; default: return false; diff --git a/common/spdat.h b/common/spdat.h index 24aac5890..4dbbc7bbb 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -165,6 +165,7 @@ #define MAX_SYMPATHETIC_PROCS 10 // Number of sympathetic procs a client can have (This is arbitrary) + const int Z_AGGRO=10; const uint32 MobAISpellRange=100; // max range of buffs @@ -278,7 +279,7 @@ enum RESISTTYPE //Target Type IDs typedef enum { -/* 01 */ ST_TargetOptional = 0x01, +/* 01 */ ST_TargetOptional = 0x01, //only used for targeted projectile spells /* 02 */ ST_AEClientV1 = 0x02, /* 03 */ ST_GroupTeleport = 0x03, /* 04 */ ST_AECaster = 0x04, @@ -381,7 +382,7 @@ typedef enum { #define SE_PoisonCounter 36 // implemented //#define SE_DetectHostile 37 // not used //#define SE_DetectMagic 38 // not used -//#define SE_DetectPoison 39 // not used +#define SE_TwinCastBlocker 39 // implemented - If present in spell, then the spell can not be twincast. #define SE_DivineAura 40 // implemented #define SE_Destroy 41 // implemented - Disintegrate, Banishment of Shadows #define SE_ShadowStep 42 // implemented @@ -464,7 +465,7 @@ typedef enum { #define SE_AttackSpeed3 119 // implemented #define SE_HealRate 120 // implemented - reduces healing by a % #define SE_ReverseDS 121 // implemented -//#define SE_ReduceSkill 122 // not used +//#define SE_ReduceSkill 122 // not implemented TODO: Now used on live, decreases skills by percent #define SE_Screech 123 // implemented Spell Blocker(If have buff with value +1 will block any effect with -1) #define SE_ImprovedDamage 124 // implemented #define SE_ImprovedHeal 125 // implemented @@ -488,7 +489,7 @@ typedef enum { #define SE_LimitCastTimeMin 143 // implemented #define SE_LimitCastTimeMax 144 // implemented (*not used in any known live spell) #define SE_Teleport2 145 // implemented - Banishment of the Pantheon -//#define SE_ElectricityResist 146 // *not implemented (Lightning Rod: 23233) +//#define SE_ElectricityResist 146 // *not implemented TODO: Now used on live, xyz for teleport spells? also in temp pets? #define SE_PercentalHeal 147 // implemented #define SE_StackingCommand_Block 148 // implemented? #define SE_StackingCommand_Overwrite 149 // implemented? @@ -638,7 +639,7 @@ typedef enum { #define SE_FrontalStunResist 293 // implemented[AA] - Reduce chance to be stunned from front. -- live descriptions sounds like this isn't limited to frontal anymore #define SE_CriticalSpellChance 294 // implemented - increase chance to critical hit and critical damage modifier. //#define SE_ReduceTimerSpecial 295 // not used -#define SE_FcSpellVulnerability 296 // implemented - increase in incoming spell damage +#define SE_FcSpellVulnerability 296 // implemented - increase in incoming spell damage [base1= min dmg base2= max dmg] #define SE_FcDamageAmtIncoming 297 // implemented - debuff that adds points damage to spells cast on target (focus effect). #define SE_ChangeHeight 298 // implemented #define SE_WakeTheDead 299 // implemented @@ -674,7 +675,7 @@ typedef enum { #define SE_ManaAbsorbPercentDamage 329 // implemented #define SE_CriticalDamageMob 330 // implemented #define SE_Salvage 331 // implemented - chance to recover items that would be destroyed in failed tradeskill combine -#define SE_SummonToCorpse 332 // *not implemented AA - Call of the Wild (Druid/Shaman Res spell with no exp) +#define SE_SummonToCorpse 332 // *not implemented AA - Call of the Wild (Druid/Shaman Res spell with no exp) TOOD: implement this. #define SE_CastOnRuneFadeEffect 333 // implemented #define SE_BardAEDot 334 // implemented #define SE_BlockNextSpellFocus 335 // implemented - base1 chance to block next spell ie Puratus (8494) @@ -682,7 +683,7 @@ typedef enum { #define SE_PercentXPIncrease 337 // implemented #define SE_SummonAndResAllCorpses 338 // implemented #define SE_TriggerOnCast 339 // implemented -#define SE_SpellTrigger 340 // implemented - chance to trigger spell +#define SE_SpellTrigger 340 // implemented - chance to trigger spell [Share rolls with 469] All base2 spells share roll chance, only 1 cast. #define SE_ItemAttackCapIncrease 341 // implemented[AA] - increases the maximum amount of attack you can gain from items. #define SE_ImmuneFleeing 342 // implemented - stop mob from fleeing #define SE_InterruptCasting 343 // implemented - % chance to interrupt spells being cast every tic. Cacophony (8272) @@ -739,7 +740,7 @@ typedef enum { #define SE_FcHealAmtIncoming 394 // implemented - Adds/Removes amount of healing on target by X value with foucs restrictions. #define SE_FcHealPctCritIncoming 395 // implemented[AA] - Increases chance of having a heal crit when cast on you. [focus limited] #define SE_FcHealAmtCrit 396 // implemented - Adds a direct healing amount to spells -#define SE_PetMeleeMitigation 397 // implemented[AA] - additional mitigation to your pets. Adds AC. +#define SE_PetMeleeMitigation 397 // implemented[AA] - additional mitigation to your pets. Adds AC #define SE_SwarmPetDuration 398 // implemented - Affects the duration of swarm pets #define SE_FcTwincast 399 // implemented - cast 2 spells for every 1 #define SE_HealGroupFromMana 400 // implemented - Drains mana and heals for each point of mana drained @@ -807,28 +808,28 @@ typedef enum { #define SE_FcDamageAmt2 462 // implemented - Increase spell damage by flat amount (SE_Fc_Damage_Amt2) //#define SE_Shield_Target 463 // #define SE_PC_Pet_Rampage 464 // implemented - Base1 % chance to do rampage for base2 % of damage each melee round -//#define SE_PC_Pet_AE_Rampage 465 // Would assume as above but need to confirm. +#define SE_PC_Pet_AE_Rampage 465 // implemented - Base1 % chance to do AE rampage for base2 % of damage each melee round #define SE_PC_Pet_Flurry_Chance 466 // implemented - Base1 % chance to do flurry from double attack hit. #define SE_DS_Mitigation_Amount 467 // implemented - Modify incoming damage shield damage by a flat amount #define SE_DS_Mitigation_Percentage 468 // implemented - Modify incoming damage shield damage by percentage -//#define SE_Chance_Best_in_Spell_Grp 469 // -//#define SE_Trigger_Best_in_Spell Grp 470 // +#define SE_Chance_Best_in_Spell_Grp 469 // implemented - Chance to cast highest scribed spell within a spell group. All base2 spells share roll chance, only 1 cast. +#define SE_Trigger_Best_in_Spell_Grp 470 // implemented - Chance to cast highest scribed spell within a spell group. Each spell has own chance. //#define SE_Double_Melee_Round 471 // //#define SE_Buy_AA_Rank 472 // #define SE_Double_Backstab_Front 473 // implemented - Chance to double backstab from front -//#define SE_Pet_Crit_Melee_Damage_Pct_Owner 474 // -//#define SE_Trigger_Spell_Non_Item 475 // +#define SE_Pet_Crit_Melee_Damage_Pct_Owner 474 // implemenetd - Critical damage mod applied to pets from owner +#define SE_Trigger_Spell_Non_Item 475 // implemented - Trigger spell on cast only if not from item click. //#define SE_Weapon_Stance 476 // -//#define SE_Hatelist_To_Top_Index 477 // -//#define SE_Hatelist_To_Tail_Index 478 // +#define SE_Hatelist_To_Top_Index 477 // Implemented - Chance to be set to top of rampage list +#define SE_Hatelist_To_Tail_Index 478 // Implemented - Chance to be set to bottom of rampage list //#define SE_Ff_Value_Min 479 // //#define SE_Ff_Value_Max 480 // -//#define SE_Fc_Cast_Spell_On_Land 481 // +#define SE_Fc_Cast_Spell_On_Land 481 // Implemented - [FOCUS] Spells cast on target with this Focus Effect will have chance to cause a Spell to be cast if limits met. //#define SE_Skill_Base_Damage_Mod 482 // -//#define SE_Fc_Spell_Damage_Pct_IncomingPC 483 // -//#define SE_Fc_Spell_Damage_Amt_IncomingPC 484 // -//#define SE_Ff_CasterClass 485 // -//#define SE_Ff_Same_Caster 486 // +#define SE_Fc_Spell_Damage_Pct_IncomingPC 483 // Implemented - [FOCUS] modifies incoming spell damage by percent +#define SE_Fc_Spell_Damage_Amt_IncomingPC 484 // Implemented - [FOCUS] modifies incoming spell damage by flat amount. Typically adds damage to incoming spells. +#define SE_Ff_CasterClass 485 // Implemented - [FOCUS LIMIT] Caster of spell on target with a focus effect that is checked by incoming spells must be specified class. +#define SE_Ff_Same_Caster 486 // Implemented - [FOCUS LIMIT] Caster of spell on target with a focus effect that is checked by incoming spells 0=Must be different caster 1=Must be same caster //#define SE_Extend_Tradeskill_Cap 487 // //#define SE_Defender_Melee_Force_Pct_PC 488 // //#define SE_Worn_Endurance_Regen_Cap 489 // @@ -836,7 +837,7 @@ typedef enum { //#define SE_Ff_ReuseTimeMax 491 // //#define SE_Ff_Endurance_Min 492 // //#define SE_Ff_Endurance_Max 493 // -//#define SE_Pet_Add_Atk 494 // +#define SE_Pet_Add_Atk 494 // implemented - Bonus on pet owner which gives their pet increased attack stat //#define SE_Ff_DurationMax 495 // #define SE_Critical_Melee_Damage_Mod_Max 496 // implemented - increase or decrease by percent critical damage (not stackable) //#define SE_Ff_FocusCastProcNoBypass 497 // @@ -844,7 +845,7 @@ typedef enum { //#define SE_AddExtraAttackPct_1h_Secondary 499 // //#define SE_Fc_CastTimeMod2 500 // //#define SE_Fc_CastTimeAmt 501 // -//#define SE_Fearstun 502 // +#define SE_Fearstun 502 // implemented - Stun with a max level limit. Normal stun restrictions don't apply. #define SE_Melee_Damage_Position_Mod 503 // implemented - modify melee damage by pct if done from Front or Behind //#define SE_Melee_Damage_Position_Amt 504 // #define SE_Damage_Taken_Position_Mod 505 // implemented - mitigate melee damage by pct if dmg taken from Front or Behind @@ -854,7 +855,7 @@ typedef enum { #define SE_Health_Transfer 509 // implemented - exchange health for damage or healing on a target. ie Lifeburn/Act of Valor //#define SE_Fc_ResistIncoming 510 // //#define SE_Ff_FocusTimerMin 511 // -//#define SE_Proc_Timer_Modifier 512 // +#define SE_Proc_Timer_Modifier 512 // implemented - spell trigger limiter used currently with SPA 481, ie. limit to 1 proc every 1.5 seconds (base=1 base2=1500). //#define SE_Mana_Max_Percent 513 // //#define SE_Endurance_Max_Percent 514 // #define SE_AC_Avoidance_Max_Percent 515 // implemented - stackable avoidance modifier diff --git a/zone/aggro.cpp b/zone/aggro.cpp index 5829bc3a3..a3f52fa55 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -1018,6 +1018,7 @@ int32 Mob::CheckAggroAmount(uint16 spell_id, Mob *target, bool isproc) case SE_Mez: case SE_Charm: case SE_Fear: + case SE_Fearstun: AggroAmount += default_aggro; break; case SE_Root: diff --git a/zone/attack.cpp b/zone/attack.cpp index 303d46fe5..d09271c55 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -274,14 +274,8 @@ int Mob::GetTotalDefense() // 172 Evasion aka SE_AvoidMeleeChance evasion_bonus += itembonuses.AvoidMeleeChanceEffect + aabonuses.AvoidMeleeChanceEffect; // item bonus here isn't mod2 avoidance - Mob *owner = nullptr; - if (IsPet()) - owner = GetOwner(); - else if (IsNPC() && CastToNPC()->GetSwarmOwner()) - owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); - - if (owner) // 215 Pet Avoidance % aka SE_PetAvoidance - evasion_bonus += owner->aabonuses.PetAvoidance + owner->spellbonuses.PetAvoidance + owner->itembonuses.PetAvoidance; + // 215 Pet Avoidance % aka SE_PetAvoidance + evasion_bonus += GetPetAvoidanceBonusFromOwner(); // Evasion is a percentage bonus according to AA descriptions if (evasion_bonus) @@ -823,13 +817,7 @@ int Mob::ACSum(bool skip_caps) // According to the guild hall Combat Dummies, a level 50 classic EQ mob it should be ~115 // For a 60 PoP mob ~120, 70 OoW ~120 ac += GetAC(); - Mob *owner = nullptr; - if (IsPet()) - owner = GetOwner(); - else if (CastToNPC()->GetSwarmOwner()) - owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); - if (owner) - ac += owner->aabonuses.PetAvoidance + owner->spellbonuses.PetAvoidance + owner->itembonuses.PetAvoidance; + ac += GetPetACBonusFromOwner(); auto spell_aa_ac = aabonuses.AC + spellbonuses.AC; ac += GetSkill(EQ::skills::SkillDefense) / 5; if (EQ::ValueWithin(static_cast(GetClass()), NECROMANCER, ENCHANTER)) @@ -925,7 +913,7 @@ int Mob::offense(EQ::skills::SkillType skill) if (stat_bonus >= 75) offense += (2 * stat_bonus - 150) / 3; - offense += GetATK(); + offense += GetATK() + GetPetATKBonusFromOwner(); return offense; } @@ -4343,7 +4331,7 @@ void Mob::TryPetCriticalHit(Mob *defender, DamageHitInfo &hit) if (critChance > 0) { if (zone->random.Roll(critChance)) { - critMod += GetCritDmgMod(hit.skill); + critMod += GetCritDmgMod(hit.skill, owner); hit.damage_done += 5; hit.damage_done = (hit.damage_done * critMod) / 100; @@ -5266,15 +5254,22 @@ void Mob::CommonOutgoingHitSuccess(Mob* defender, DamageHitInfo &hit, ExtraAttac if (mod > 0) spec_mod = mod; if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { - int spell = spellbonuses.PC_Pet_Rampage[1] + itembonuses.PC_Pet_Rampage[1] + aabonuses.PC_Pet_Rampage[1]; - if (spell > spec_mod) - spec_mod = spell; + //SE_PC_Pet_Rampage SPA 464 on pet, damage modifier + int spell_mod = spellbonuses.PC_Pet_Rampage[1] + itembonuses.PC_Pet_Rampage[1] + aabonuses.PC_Pet_Rampage[1]; + if (spell_mod > spec_mod) + spec_mod = spell_mod; } } else if (IsSpecialAttack(eSpecialAttacks::AERampage)) { int mod = GetSpecialAbilityParam(SPECATK_AREA_RAMPAGE, 2); if (mod > 0) spec_mod = mod; + if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + //SE_PC_Pet_AE_Rampage SPA 465 on pet, damage modifier + int spell_mod = spellbonuses.PC_Pet_AE_Rampage[1] + itembonuses.PC_Pet_AE_Rampage[1] + aabonuses.PC_Pet_AE_Rampage[1]; + if (spell_mod > spec_mod) + spec_mod = spell_mod; + } } if (spec_mod > 0) hit.damage_done = (hit.damage_done * spec_mod) / 100; @@ -5625,6 +5620,48 @@ void Mob::DoOffHandAttackRounds(Mob *target, ExtraAttackOptions *opts) } } + +int Mob::GetPetAvoidanceBonusFromOwner() +{ + Mob *owner = nullptr; + if (IsPet()) + owner = GetOwner(); + else if (IsNPC() && CastToNPC()->GetSwarmOwner()) + owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); + + if (owner) + return owner->aabonuses.PetAvoidance + owner->spellbonuses.PetAvoidance + owner->itembonuses.PetAvoidance; + + return 0; +} +int Mob::GetPetACBonusFromOwner() +{ + Mob *owner = nullptr; + if (IsPet()) + owner = GetOwner(); + else if (IsNPC() && CastToNPC()->GetSwarmOwner()) + owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); + + if (owner) + return owner->aabonuses.PetMeleeMitigation + owner->spellbonuses.PetMeleeMitigation + owner->itembonuses.PetMeleeMitigation; + + return 0; +} +int Mob::GetPetATKBonusFromOwner() +{ + Mob *owner = nullptr; + if (IsPet()) + owner = GetOwner(); + else if (IsNPC() && CastToNPC()->GetSwarmOwner()) + owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); + + if (owner) + return owner->aabonuses.Pet_Add_Atk + owner->spellbonuses.Pet_Add_Atk + owner->itembonuses.Pet_Add_Atk; + + return 0; +} + + bool Mob::GetWasSpawnedInWater() const { return spawned_in_water; } diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index bc8075275..3f067aee9 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -1438,6 +1438,13 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) break; } + case SE_PC_Pet_AE_Rampage: { + newbon->PC_Pet_AE_Rampage[0] += base1; //Chance to rampage + if (newbon->PC_Pet_AE_Rampage[1] < base2) + newbon->PC_Pet_AE_Rampage[1] = base2; //Damage modifer - take highest + break; + } + case SE_PC_Pet_Flurry_Chance: newbon->PC_Pet_Flurry += base1; //Chance to Flurry break; @@ -1546,6 +1553,13 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) newbon->DS_Mitigation_Percentage += base1; break; + case SE_Pet_Crit_Melee_Damage_Pct_Owner: + newbon->Pet_Crit_Melee_Damage_Pct_Owner += base1; + break; + + case SE_Pet_Add_Atk: + newbon->Pet_Add_Atk += base1; + break; // to do case SE_PetDiscipline: @@ -3282,6 +3296,13 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne break; } + case SE_PC_Pet_AE_Rampage: { + new_bonus->PC_Pet_AE_Rampage[0] += effect_value; //Chance to rampage + if (new_bonus->PC_Pet_AE_Rampage[1] < base2) + new_bonus->PC_Pet_AE_Rampage[1] = base2; //Damage modifer - take highest + break; + } + case SE_PC_Pet_Flurry_Chance: new_bonus->PC_Pet_Flurry += effect_value; //Chance to Flurry break; @@ -3386,7 +3407,13 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne new_bonus->DS_Mitigation_Percentage += effect_value; break; + case SE_Pet_Crit_Melee_Damage_Pct_Owner: + new_bonus->Pet_Crit_Melee_Damage_Pct_Owner += effect_value; + break; + case SE_Pet_Add_Atk: + new_bonus->Pet_Add_Atk += effect_value; + break; //Special custom cases for loading effects on to NPC from 'npc_spels_effects' table if (IsAISpellEffect) { @@ -3708,6 +3735,8 @@ uint8 Mob::IsFocusEffect(uint16 spell_id,int effect_index, bool AA,uint32 aa_eff return 0; //This is calculated as an actual bonus case SE_FcSpellVulnerability: return focusSpellVulnerability; + case SE_Fc_Spell_Damage_Pct_IncomingPC: + return focusFcSpellDamagePctIncomingPC; case SE_BlockNextSpellFocus: //return focusBlockNextSpell; return 0; //This is calculated as an actual bonus @@ -3725,6 +3754,8 @@ uint8 Mob::IsFocusEffect(uint16 spell_id,int effect_index, bool AA,uint32 aa_eff return focusFcDamagePctCrit; case SE_FcDamageAmtIncoming: return focusFcDamageAmtIncoming; + case SE_Fc_Spell_Damage_Amt_IncomingPC: + return focusFcSpellDamageAmtIncomingPC; case SE_FcHealAmtIncoming: return focusFcHealAmtIncoming; case SE_FcHealPctIncoming: @@ -3739,6 +3770,8 @@ uint8 Mob::IsFocusEffect(uint16 spell_id,int effect_index, bool AA,uint32 aa_eff return focusFcMute; case SE_FcTimerRefresh: return focusFcTimerRefresh; + case SE_Fc_Cast_Spell_On_Land: + return focusFcCastSpellOnLand; case SE_FcStunTimeMod: return focusFcStunTimeMod; case SE_FcHealPctCritIncoming: @@ -3747,6 +3780,7 @@ uint8 Mob::IsFocusEffect(uint16 spell_id,int effect_index, bool AA,uint32 aa_eff return focusFcHealAmt; case SE_FcHealAmtCrit: return focusFcHealAmtCrit; + } return 0; } @@ -4978,6 +5012,37 @@ void Mob::NegateSpellsBonuses(uint16 spell_id) aabonuses.DS_Mitigation_Percentage = effect_value; break; + case SE_Pet_Crit_Melee_Damage_Pct_Owner: + spellbonuses.Pet_Crit_Melee_Damage_Pct_Owner = effect_value; + itembonuses.Pet_Crit_Melee_Damage_Pct_Owner = effect_value; + aabonuses.Pet_Crit_Melee_Damage_Pct_Owner = effect_value; + break; + + case SE_Pet_Add_Atk: + spellbonuses.Pet_Add_Atk = effect_value; + itembonuses.Pet_Add_Atk = effect_value; + aabonuses.Pet_Add_Atk = effect_value; + break; + + case SE_PC_Pet_Rampage: + spellbonuses.PC_Pet_Rampage[0] = effect_value; + itembonuses.PC_Pet_Rampage[0] = effect_value; + aabonuses.PC_Pet_Rampage[0] = effect_value; + spellbonuses.PC_Pet_Rampage[1] = effect_value; + itembonuses.PC_Pet_Rampage[1] = effect_value; + aabonuses.PC_Pet_Rampage[1] = effect_value; + break; + + case SE_PC_Pet_AE_Rampage: + spellbonuses.PC_Pet_AE_Rampage[0] = effect_value; + itembonuses.PC_Pet_AE_Rampage[0] = effect_value; + aabonuses.PC_Pet_AE_Rampage[0] = effect_value; + spellbonuses.PC_Pet_AE_Rampage[1] = effect_value; + itembonuses.PC_Pet_AE_Rampage[1] = effect_value; + aabonuses.PC_Pet_AE_Rampage[1] = effect_value; + break; + + case SE_SkillProcSuccess:{ for(int e = 0; e < MAX_SKILL_PROCS; e++) { diff --git a/zone/client.h b/zone/client.h index 734bc66ff..74e2eed54 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1021,6 +1021,7 @@ public: int GetNextAvailableSpellBookSlot(int starting_slot = 0); inline uint32 GetSpellByBookSlot(int book_slot) { return m_pp.spell_book[book_slot]; } inline bool HasSpellScribed(int spellid) { return FindSpellBookSlotBySpellID(spellid) != -1; } + uint32 GetHighestScribedSpellinSpellGroup(uint32 spell_group); uint16 GetMaxSkillAfterSpecializationRules(EQ::skills::SkillType skillid, uint16 maxSkill); void SendPopupToClient(const char *Title, const char *Text, uint32 PopupID = 0, uint32 Buttons = 0, uint32 Duration = 0); void SendFullPopup(const char *Title, const char *Text, uint32 PopupID = 0, uint32 NegativeID = 0, uint32 Buttons = 0, uint32 Duration = 0, const char *ButtonName0 = 0, const char *ButtonName1 = 0, uint32 SoundControls = 0); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index a78095a30..d805be19a 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -538,6 +538,9 @@ bool Client::Process() { } } + if (focus_proc_limit_timer.Check() && !dead) + FocusProcLimitProcess(); + if (client_state == CLIENT_KICKED) { Save(); OnDisconnect(true); diff --git a/zone/common.h b/zone/common.h index 01c074b11..1acb335de 100644 --- a/zone/common.h +++ b/zone/common.h @@ -124,6 +124,7 @@ typedef enum { //focus types focusSpellHateMod, focusTriggerOnCast, focusSpellVulnerability, + focusFcSpellDamagePctIncomingPC, focusTwincast, focusSympatheticProc, focusFcDamageAmt, @@ -135,6 +136,8 @@ typedef enum { //focus types focusBlockNextSpell, focusFcHealPctIncoming, focusFcDamageAmtIncoming, + focusFcSpellDamageAmtIncomingPC, + focusFcCastSpellOnLand, focusFcHealAmtIncoming, focusFcBaseEffects, focusIncreaseNumHits, @@ -319,6 +322,8 @@ struct Buffs_Struct { int32 ExtraDIChance; int16 RootBreakChance; //Not saved to dbase uint32 instrument_mod; + int16 focusproclimit_time; //timer to limit number of procs from focus effects + int16 focusproclimit_procamt; //amount of procs that can be cast before timer limiter is set bool persistant_buff; bool client; //True if the caster is a client bool UpdateClient; @@ -524,6 +529,7 @@ struct StatBonuses { uint32 SkillProc[MAX_SKILL_PROCS]; // Max number of spells containing skill_procs. uint32 SkillProcSuccess[MAX_SKILL_PROCS]; // Max number of spells containing skill_procs_success. uint32 PC_Pet_Rampage[2]; // 0= % chance to rampage, 1=damage modifier + uint32 PC_Pet_AE_Rampage[2]; // 0= % chance to AE rampage, 1=damage modifier uint32 PC_Pet_Flurry; // Percent chance flurry from double attack int32 Attack_Accuracy_Max_Percent; // Increase ATK accuracy by percent. int32 AC_Mitigation_Max_Percent; // Increase AC mitigation by percent @@ -532,7 +538,10 @@ struct StatBonuses { int32 Melee_Damage_Position_Mod[2]; // base = percent melee damage increase base2 0=back 1=front. [0]Back[1]Front int32 Double_Backstab_Front; // base = percent chance to double back stab front int32 DS_Mitigation_Amount; // base = flat amt DS mitigation. Negative value to reduce - int32 DS_Mitigation_Percentage; // base = percent amt of DS mitigation. Negative value to reduce + int32 DS_Mitigation_Percentage; // base = percent amt of DS mitigation. Negative value to reduce + int32 Pet_Crit_Melee_Damage_Pct_Owner; // base = percent mod for pet critcal damage from owner + int32 Pet_Add_Atk; // base = Pet ATK bonus from owner + // AAs int8 Packrat; //weight reduction for items, 1 point = 10% diff --git a/zone/mob.cpp b/zone/mob.cpp index 46d9447f9..e26c5406c 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -102,6 +102,7 @@ Mob::Mob( ranged_timer(2000), tic_timer(6000), mana_timer(2000), + focus_proc_limit_timer(250), spellend_timer(0), rewind_timer(30000), bindwound_timer(10000), @@ -3537,57 +3538,82 @@ void Mob::TriggerOnCast(uint32 focus_spell, uint32 spell_id, bool aa_trigger) } } + + + bool Mob::TrySpellTrigger(Mob *target, uint32 spell_id, int effect) { - if(!target || !IsValidSpell(spell_id)) + if (!target || !IsValidSpell(spell_id)) return false; - int spell_trig = 0; - // Count all the percentage chances to trigger for all effects - for(int i = 0; i < EFFECT_COUNT; i++) - { - if (spells[spell_id].effectid[i] == SE_SpellTrigger) - spell_trig += spells[spell_id].base[i]; - } - // If all the % add to 100, then only one of the effects can fire but one has to fire. - if (spell_trig == 100) - { - int trig_chance = 100; - for(int i = 0; i < EFFECT_COUNT; i++) - { - if (spells[spell_id].effectid[i] == SE_SpellTrigger) - { - if(zone->random.Int(0, trig_chance) <= spells[spell_id].base[i]) - { - // If we trigger an effect then its over. - if (IsValidSpell(spells[spell_id].base2[i])){ - SpellFinished(spells[spell_id].base2[i], target, EQ::spells::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); - return true; - } - } - else - { - // Increase the chance to fire for the next effect, if all effects fail, the final effect will fire. - trig_chance -= spells[spell_id].base[i]; - } - } + /*The effects SE_SpellTrigger (SPA 340) and SE_Chance_Best_in_Spell_Grp (SPA 469) work as follows, you typically will have 2-3 different spells each with their own + chance to be triggered with all chances equaling up to 100 pct, with only 1 spell out of the group being ultimately cast. + (ie Effect1 trigger spellA with 30% chance, Effect2 triggers spellB with 20% chance, Effect3 triggers spellC with 50% chance). + The following function ensures a stastically accurate chance for each spell to be cast based on their chance values. These effects are also used in spells where there + is only 1 effect using the trigger effect. In those situations we simply roll a chance for that spell to be cast once. + Note: Both SPA 340 and 469 can be in same spell and both cummulative add up to 100 pct chances. SPA469 only difference being the spell cast will + be "best in spell group", instead of a defined spell_id.*/ - } - } - // if the chances don't add to 100, then each effect gets a chance to fire, chance for no trigger as well. - else + int chance_array[EFFECT_COUNT] = {}; + int total_chance = 0; + int effect_slot = effect; + bool CastSpell = false; + + for (int i = 0; i < EFFECT_COUNT; i++) { - if(zone->random.Int(0, 100) <= spells[spell_id].base[effect]) - { - if (IsValidSpell(spells[spell_id].base2[effect])){ - SpellFinished(spells[spell_id].base2[effect], target, EQ::spells::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[effect]].ResistDiff); - return true; //Only trigger once of these per spell effect. + if (spells[spell_id].effectid[i] == SE_SpellTrigger || spells[spell_id].effectid[i] == SE_Chance_Best_in_Spell_Grp) + total_chance += spells[spell_id].base[i]; + } + + if (total_chance == 100) + { + int current_chance = 0; + int cummulative_chance = 0; + + for (int i = 0; i < EFFECT_COUNT; i++){ + //Find spells with SPA 340 and add the cummulative percent chances to the roll array + if ((spells[spell_id].effectid[i] == SE_SpellTrigger) || (spells[spell_id].effectid[i] == SE_Chance_Best_in_Spell_Grp)){ + + cummulative_chance = current_chance + spells[spell_id].base[i]; + chance_array[i] = cummulative_chance; + current_chance = cummulative_chance; + } + } + int random_roll = zone->random.Int(1, 100); + //Determine which spell out of the group of the spells (each with own percent chance out of 100) will be cast based on a single roll. + for (int i = 0; i < EFFECT_COUNT; i++){ + if (chance_array[i] != 0 && random_roll <= chance_array[i]) { + effect_slot = i; + CastSpell = true; + break; } } } + + //If the chances don't add to 100, then each effect gets a chance to fire, chance for no trigger as well. + else if (zone->random.Roll(spells[spell_id].base[effect])) { + CastSpell = true; //In this case effect_slot is what was passed into function. + } + + if (CastSpell) { + if (spells[spell_id].effectid[effect_slot] == SE_SpellTrigger && IsValidSpell(spells[spell_id].base2[effect_slot])) { + SpellFinished(spells[spell_id].base2[effect_slot], target, EQ::spells::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[effect_slot]].ResistDiff); + return true; + } + else if (IsClient() & spells[spell_id].effectid[effect_slot] == SE_Chance_Best_in_Spell_Grp) { + uint32 best_spell_id = CastToClient()->GetHighestScribedSpellinSpellGroup(spells[spell_id].base2[effect_slot]); + if (IsValidSpell(best_spell_id)) { + SpellFinished(best_spell_id, target, EQ::spells::CastingSlot::Item, 0, -1, spells[best_spell_id].ResistDiff); + } + return true;//Do nothing if you don't have the any spell in spell group scribed. + } + } + return false; } + + void Mob::TryTriggerOnValueAmount(bool IsHP, bool IsMana, bool IsEndur, bool IsPet) { /* @@ -3676,6 +3702,9 @@ void Mob::TryTwincast(Mob *caster, Mob *target, uint32 spell_id) if(!IsValidSpell(spell_id)) return; + if (IsEffectInSpell(spell_id, SE_TwinCastBlocker)) + return; + if(IsClient()) { int32 focus = CastToClient()->GetFocusEffect(focusTwincast, spell_id); @@ -3739,34 +3768,46 @@ void Mob::TryOnSpellFinished(Mob *caster, Mob *target, uint16 spell_id) int32 Mob::GetVulnerability(Mob* caster, uint32 spell_id, uint32 ticsremaining) { + /* + Modifies incoming spell damage by percent, to increase or decrease damage, can be limited to specific resists. + Can be applied through quest function, spell focus or npc_spells_effects table. This function is run on the target of the spell. + */ + if (!IsValidSpell(spell_id)) return 0; if (!caster) return 0; - int32 value = 0; + int32 total_mod = 0; + int32 innate_mod = 0; + int32 fc_spell_vulnerability_mod = 0; + int32 fc_spell_damage_pct_incomingPC_mod = 0; - //Apply innate vulnerabilities + //Apply innate vulnerabilities from quest functions and tables if (Vulnerability_Mod[GetSpellResistType(spell_id)] != 0) - value = Vulnerability_Mod[GetSpellResistType(spell_id)]; - + innate_mod = Vulnerability_Mod[GetSpellResistType(spell_id)]; else if (Vulnerability_Mod[HIGHEST_RESIST+1] != 0) - value = Vulnerability_Mod[HIGHEST_RESIST+1]; + innate_mod = Vulnerability_Mod[HIGHEST_RESIST+1]; - //Apply spell derived vulnerabilities - if (spellbonuses.FocusEffects[focusSpellVulnerability]){ + //[Apply spell derived vulnerabilities] Step 1: Check this focus effect exists on the mob. + if (spellbonuses.FocusEffects[focusSpellVulnerability]){ int32 tmp_focus = 0; int tmp_buffslot = -1; + /* + Find all buffs that may contain SPA 296, then find which slot has the highest possible effect. Since the focus can use + a min and max amount value to determine final focus amt. To find the best focus, use only max value if possible. Once the + best is found. Run it again to get the final value randoming between min and max. + */ int buff_count = GetMaxTotalSlots(); for(int i = 0; i < buff_count; i++) { if((IsValidSpell(buffs[i].spellid) && IsEffectInSpell(buffs[i].spellid, SE_FcSpellVulnerability))){ - int32 focus = caster->CalcFocusEffect(focusSpellVulnerability, buffs[i].spellid, spell_id, true); + int32 focus = caster->CalcFocusEffect(focusSpellVulnerability, buffs[i].spellid, spell_id, true, buffs[tmp_buffslot].casterid); if (!focus) continue; @@ -3780,21 +3821,61 @@ int32 Mob::GetVulnerability(Mob* caster, uint32 spell_id, uint32 ticsremaining) tmp_focus = focus; tmp_buffslot = i; } - } } - tmp_focus = caster->CalcFocusEffect(focusSpellVulnerability, buffs[tmp_buffslot].spellid, spell_id); - - if (tmp_focus < -99) - tmp_focus = -99; - - value += tmp_focus; + fc_spell_vulnerability_mod = caster->CalcFocusEffect(focusSpellVulnerability, buffs[tmp_buffslot].spellid, spell_id, false, buffs[tmp_buffslot].casterid); if (tmp_buffslot >= 0) CheckNumHitsRemaining(NumHit::MatchingSpells, tmp_buffslot); } - return value; + + if (spellbonuses.FocusEffects[focusFcSpellDamagePctIncomingPC]) { + + int32 tmp_focus = 0; + int tmp_buffslot = -1; + + /* + Find all buffs that may contain SPA 483, then find which slot has the highest possible effect. Since the focus can use + a min and max amount value to determine final focus amt. To find the best focus, use only max value if possible. Once the + best is found. Run it again to get the final value randoming between min and max. + */ + int buff_count = GetMaxTotalSlots(); + for (int i = 0; i < buff_count; i++) { + + if ((IsValidSpell(buffs[i].spellid) && IsEffectInSpell(buffs[i].spellid, SE_Fc_Spell_Damage_Pct_IncomingPC))) { + + int32 focus = caster->CalcFocusEffect(focusFcSpellDamagePctIncomingPC, buffs[i].spellid, spell_id, true, buffs[tmp_buffslot].casterid); + + if (!focus) + continue; + + if (tmp_focus && focus > tmp_focus) { + tmp_focus = focus; + tmp_buffslot = i; + } + + else if (!tmp_focus) { + tmp_focus = focus; + tmp_buffslot = i; + } + } + } + + fc_spell_damage_pct_incomingPC_mod = caster->CalcFocusEffect(focusFcSpellDamagePctIncomingPC, buffs[tmp_buffslot].spellid, spell_id, false, buffs[tmp_buffslot].casterid); + + if (tmp_buffslot >= 0) + CheckNumHitsRemaining(NumHit::MatchingSpells, tmp_buffslot); + } + + total_mod = fc_spell_vulnerability_mod + fc_spell_damage_pct_incomingPC_mod; + + //Don't let focus derived mods reduce past 99% mitigation. Quest related can, and for custom functionality if negative will give a healing affect instead of damage. + if (total_mod < -99) + total_mod = -99; + + total_mod += innate_mod; + return total_mod; } int32 Mob::GetSkillDmgTaken(const EQ::skills::SkillType skill_used, ExtraAttackOptions *opts) @@ -3856,6 +3937,61 @@ int16 Mob::GetHealRate(uint16 spell_id, Mob* caster) { return heal_rate; } +void Mob::SetBottomRampageList() +{ + auto &mob_list = entity_list.GetCloseMobList(this); + + for (auto &e : mob_list) { + auto mob = e.second; + if (!mob) { + continue; + } + + if (!mob->GetSpecialAbility(SPECATK_RAMPAGE)) { + continue; + } + + if (mob->IsNPC() && mob->CheckAggro(this)) { + for (int i = 0; i < mob->RampageArray.size(); i++) { + // Find this mob in the rampage list + if (this->GetID() == mob->RampageArray[i]) { + //Move to bottom of Rampage List + auto it = mob->RampageArray.begin() + i; + std::rotate(it, it + 1, mob->RampageArray.end()); + } + } + } + } +} + +void Mob::SetTopRampageList() +{ + auto &mob_list = entity_list.GetCloseMobList(this); + + for (auto &e : mob_list) { + auto mob = e.second; + if (!mob) { + continue; + } + + if (!mob->GetSpecialAbility(SPECATK_RAMPAGE)) { + continue; + } + + if (mob->IsNPC() && mob->CheckAggro(this)) { + for (int i = 0; i < mob->RampageArray.size(); i++) { + // Find this mob in the rampage list + if (this->GetID() == mob->RampageArray[i]) { + //Move to Top of Rampage List + auto it = mob->RampageArray.begin() + i; + std::rotate(it, it + 1, mob->RampageArray.end()); + std::rotate(mob->RampageArray.rbegin(), mob->RampageArray.rbegin() + 1, mob->RampageArray.rend()); + } + } + } + } +} + bool Mob::TryFadeEffect(int slot) { if (!buffs[slot].spellid) @@ -4647,7 +4783,7 @@ bool Mob::TrySpellOnDeath() //in death because the heal will not register before the script kills you. } -int16 Mob::GetCritDmgMod(uint16 skill) +int16 Mob::GetCritDmgMod(uint16 skill, Mob* owner) { int critDmg_mod = 0; @@ -4658,6 +4794,9 @@ int16 Mob::GetCritDmgMod(uint16 skill) critDmg_mod += itembonuses.CritDmgModNoStack[EQ::skills::HIGHEST_SKILL + 1] + spellbonuses.CritDmgModNoStack[EQ::skills::HIGHEST_SKILL + 1] + aabonuses.CritDmgModNoStack[EQ::skills::HIGHEST_SKILL + 1] + itembonuses.CritDmgModNoStack[skill] + spellbonuses.CritDmgModNoStack[skill] + aabonuses.CritDmgModNoStack[skill]; + if (owner) //Checked in TryPetCriticalHit + critDmg_mod += owner->aabonuses.Pet_Crit_Melee_Damage_Pct_Owner + owner->itembonuses.Pet_Crit_Melee_Damage_Pct_Owner + owner->spellbonuses.Pet_Crit_Melee_Damage_Pct_Owner; + return critDmg_mod; } diff --git a/zone/mob.h b/zone/mob.h index c0ce7bf5a..4e051d76f 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -770,7 +770,7 @@ public: void QuestJournalledSay(Client *QuestInitiator, const char *str, Journal::Options &opts); int32 GetItemStat(uint32 itemid, const char *identifier); - int16 CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, bool best_focus=false); + int16 CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, bool best_focus=false, uint16 casterid=0); uint8 IsFocusEffect(uint16 spellid, int effect_index, bool AA=false,uint32 aa_effect=0); void SendIllusionPacket(uint16 in_race, uint8 in_gender = 0xFF, uint8 in_texture = 0xFF, uint8 in_helmtexture = 0xFF, uint8 in_haircolor = 0xFF, uint8 in_beardcolor = 0xFF, uint8 in_eyecolor1 = 0xFF, uint8 in_eyecolor2 = 0xFF, @@ -814,7 +814,7 @@ public: void CastOnCure(uint32 spell_id); void CastOnNumHitFade(uint32 spell_id); void SlowMitigation(Mob* caster); - int16 GetCritDmgMod(uint16 skill); + int16 GetCritDmgMod(uint16 skill, Mob* owner = nullptr); int16 GetMeleeDamageMod_SE(uint16 skill); int16 GetMeleeMinDamageMod_SE(uint16 skill); int16 GetCrippBlowChance(); @@ -837,6 +837,10 @@ public: inline int16 GetSpellPowerDistanceMod() const { return SpellPowerDistanceMod; }; inline void SetSpellPowerDistanceMod(int16 value) { SpellPowerDistanceMod = value; }; int32 GetSpellStat(uint32 spell_id, const char *identifier, uint8 slot = 0); + + void CastSpellOnLand(Mob* caster, uint32 spell_id); + void FocusProcLimitProcess(); + bool ApplyFocusProcLimiter(uint32 spell_id, int buffslot = -1); void ModSkillDmgTaken(EQ::skills::SkillType skill_num, int value); int16 GetModSkillDmgTaken(const EQ::skills::SkillType skill_num); @@ -909,6 +913,9 @@ public: inline bool IsTempPet() const { return _IsTempPet; } inline void SetTempPet(bool value) { _IsTempPet = value; } inline bool IsHorse() { return is_horse; } + int GetPetAvoidanceBonusFromOwner(); + int GetPetACBonusFromOwner(); + int GetPetATKBonusFromOwner(); inline const bodyType GetBodyType() const { return bodytype; } inline const bodyType GetOrigBodyType() const { return orig_bodytype; } @@ -966,6 +973,8 @@ public: bool Rampage(ExtraAttackOptions *opts); bool AddRampage(Mob*); void ClearRampage(); + void SetBottomRampageList(); + void SetTopRampageList(); void AreaRampage(ExtraAttackOptions *opts); inline bool IsSpecialAttack(eSpecialAttacks in) { return m_specialattacks == in; } @@ -1414,6 +1423,7 @@ protected: int16 slow_mitigation; // Allows for a slow mitigation (100 = 100%, 50% = 50%) Timer tic_timer; Timer mana_timer; + Timer focus_proc_limit_timer; //spell casting vars Timer spellend_timer; diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index 306c6dd64..3766a2546 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -1125,6 +1125,9 @@ void Mob::AI_Process() { ProjectileAttack(); + if (focus_proc_limit_timer.Check()) + FocusProcLimitProcess(); + auto npcSpawnPoint = CastToNPC()->GetSpawnPoint(); if (GetSpecialAbility(TETHER)) { float tether_range = static_cast(GetSpecialAbilityParam(TETHER, 0)); @@ -1218,16 +1221,15 @@ void Mob::AI_Process() { } } + + //SE_PC_Pet_Rampage SPA 464 on pet, chance modifier if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { - if (spellbonuses.PC_Pet_Rampage[0] || itembonuses.PC_Pet_Rampage[0] || - aabonuses.PC_Pet_Rampage[0]) { - int chance = spellbonuses.PC_Pet_Rampage[0] + itembonuses.PC_Pet_Rampage[0] + - aabonuses.PC_Pet_Rampage[0]; - if (zone->random.Roll(chance)) { - Rampage(nullptr); - } + int chance = spellbonuses.PC_Pet_Rampage[0] + itembonuses.PC_Pet_Rampage[0] + aabonuses.PC_Pet_Rampage[0]; + if (chance && zone->random.Roll(chance)) { + Rampage(nullptr); } } + if (GetSpecialAbility(SPECATK_RAMPAGE) && !specialed) { int rampage_chance = GetSpecialAbilityParam(SPECATK_RAMPAGE, 0); @@ -1263,6 +1265,14 @@ void Mob::AI_Process() { } } + //SE_PC_Pet_Rampage SPA 465 on pet, chance modifier + if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + int chance = spellbonuses.PC_Pet_AE_Rampage[0] + itembonuses.PC_Pet_AE_Rampage[0] + aabonuses.PC_Pet_AE_Rampage[0]; + if (chance && zone->random.Roll(chance)) { + Rampage(nullptr); + } + } + if (GetSpecialAbility(SPECATK_AREA_RAMPAGE) && !specialed) { int rampage_chance = GetSpecialAbilityParam(SPECATK_AREA_RAMPAGE, 0); rampage_chance = rampage_chance > 0 ? rampage_chance : 20; diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 801700bd5..e5e447b5b 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -197,7 +197,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove if (!IsPowerDistModSpell(spell_id)) SetSpellPowerDistanceMod(0); - bool SE_SpellTrigger_HasCast = false; + bool spell_trigger_cast_complete = false; //Used with SE_Spell_Trigger and SE_Chance_Best_in_Spell_Grp, true when spell has been triggered. // if buff slot, use instrument mod there, otherwise calc it uint32 instrument_mod = buffslot > -1 ? buffs[buffslot].instrument_mod : caster ? caster->GetInstrumentMod(spell_id) : 10; @@ -2822,9 +2822,9 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_SpellTrigger: { - if (!SE_SpellTrigger_HasCast) { + if (!spell_trigger_cast_complete) { if (caster && caster->TrySpellTrigger(this, spell_id, i)) - SE_SpellTrigger_HasCast = true; + spell_trigger_cast_complete = true; } break; } @@ -2873,8 +2873,83 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove HealDamage(amt, caster); break; } - + case SE_Chance_Best_in_Spell_Grp: { + if (!spell_trigger_cast_complete) { + if (caster && caster->TrySpellTrigger(this, spell_id, i)) + spell_trigger_cast_complete = true; + } + break; + } + + case SE_Trigger_Best_in_Spell_Grp: { + + if (caster && !caster->IsClient()) + break; + + if (zone->random.Roll(spells[spell_id].base[i])) { + uint32 best_spell_id = caster->CastToClient()->GetHighestScribedSpellinSpellGroup(spells[spell_id].base2[i]); + + if (caster && IsValidSpell(best_spell_id)) + caster->SpellFinished(best_spell_id, this, EQ::spells::CastingSlot::Item, 0, -1, spells[best_spell_id].ResistDiff); + } + break; + } + + case SE_Trigger_Spell_Non_Item: { + //Only trigger if not from item + if (caster && caster->IsClient() && GetCastedSpellInvSlot() > 0) + break; + + if (zone->random.Roll(spells[spell_id].base[i]) && IsValidSpell(spells[spell_id].base2[i])) + caster->SpellFinished(spells[spell_id].base2[i], this, EQ::spells::CastingSlot::Item, 0, -1, spells[spells[spell_id].base2[i]].ResistDiff); + + break; + } + + case SE_Hatelist_To_Tail_Index: { + if (caster && zone->random.Roll(spells[spell_id].base[i])) + caster->SetBottomRampageList(); + break; + } + + case SE_Hatelist_To_Top_Index: { + if (caster && zone->random.Roll(spells[spell_id].base[i])) + caster->SetTopRampageList(); + break; + } + + case SE_Fearstun: { + //Normal 'stun' restrictions do not apply. base1=duration, base2=PC duration, max =lv restrict + if (!caster) + break; + + if (IsNPC() && GetSpecialAbility(UNSTUNABLE)) { + caster->MessageString(Chat::SpellFailure, IMMUNE_STUN); + break; + } + + if (IsNPC() && GetSpecialAbility(UNFEARABLE)) { + caster->MessageString(Chat::SpellFailure, IMMUNE_FEAR); + break; + } + + if (spells[spell_id].max[i] == 0 || GetLevel() <= spells[spell_id].max[i]) { + if (IsClient() && spells[spell_id].base2[i]) + Stun(spells[spell_id].base2[i]); + else + Stun(spells[spell_id].base[i]); + } + else + caster->MessageString(Chat::SpellFailure, FEAR_TOO_HIGH); + break; + } + + case SE_Proc_Timer_Modifier:{ + buffs[buffslot].focusproclimit_procamt = spells[spell_id].base[i]; //Set max amount of procs before lockout timer + break; + } + case SE_PersistentEffect: MakeAura(spell_id); break; @@ -2990,6 +3065,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_HealRate: case SE_SkillDamageTaken: case SE_FcSpellVulnerability: + case SE_Fc_Spell_Damage_Pct_IncomingPC: + case SE_Fc_Spell_Damage_Amt_IncomingPC: case SE_FcTwincast: case SE_DelayDeath: case SE_CastOnFadeEffect: @@ -3131,6 +3208,12 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_DS_Mitigation_Amount: case SE_DS_Mitigation_Percentage: case SE_Double_Backstab_Front: + case SE_Pet_Crit_Melee_Damage_Pct_Owner: + case SE_Pet_Add_Atk: + case SE_TwinCastBlocker: + case SE_Fc_Cast_Spell_On_Land: + case SE_Ff_CasterClass: + case SE_Ff_Same_Caster: { break; } @@ -4406,13 +4489,14 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) case SE_Blank: break; - // Handle Focus Limits + // Handle Focus Limits case SE_LimitResist: if (base1 < 0) { if (spell.resisttype == -base1) // Exclude LimitFailure = true; - } else { + } + else { LimitInclude[0] = true; if (spell.resisttype == base1) // Include LimitInclude[1] = true; @@ -4433,12 +4517,13 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) // every level over cap reduces the effect by base2 percent unless from a clicky when // ItemCastsUseFocus is true if (lvldiff > 0 && (spell_level <= RuleI(Character, MaxLevel) || - RuleB(Character, ItemCastsUseFocus) == false)) { + RuleB(Character, ItemCastsUseFocus) == false)) { if (base2 > 0) { lvlModifier -= base2 * lvldiff; if (lvlModifier < 1) LimitFailure = true; - } else + } + else LimitFailure = true; } break; @@ -4462,7 +4547,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { // Exclude if (spell_id == -base1) LimitFailure = true; - } else { + } + else { LimitInclude[2] = true; if (spell_id == base1) // Include LimitInclude[3] = true; @@ -4479,13 +4565,15 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { if (IsEffectInSpell(spell_id, -base1)) // Exclude LimitFailure = true; - } else { + } + else { LimitInclude[4] = true; // they use 33 here for all classes ... unsure if the type check is really needed if (base1 == SE_SummonPet && type == focusReagentCost) { if (IsSummonPetSpell(spell_id) || IsSummonSkeletonSpell(spell_id)) LimitInclude[5] = true; - } else { + } + else { if (IsEffectInSpell(spell_id, base1)) // Include LimitInclude[5] = true; } @@ -4519,7 +4607,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { if (-base1 == spell.targettype) // Exclude LimitFailure = true; - } else { + } + else { LimitInclude[6] = true; if (base1 == spell.targettype) // Include LimitInclude[7] = true; @@ -4538,7 +4627,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { if (-base1 == spell.spellgroup) // Exclude LimitFailure = true; - } else { + } + else { LimitInclude[8] = true; if (base1 == spell.spellgroup) // Include LimitInclude[9] = true; @@ -4549,7 +4639,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { if (-base1 == spell.skill) LimitFailure = true; - } else { + } + else { LimitInclude[10] = true; if (base1 == spell.skill) LimitInclude[11] = true; @@ -4560,7 +4651,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { // Exclude if (CheckSpellCategory(spell_id, base1, SE_LimitSpellClass)) return (0); - } else { + } + else { LimitInclude[12] = true; if (CheckSpellCategory(spell_id, base1, SE_LimitSpellClass)) // Include LimitInclude[13] = true; @@ -4571,7 +4663,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (base1 < 0) { // Exclude if (CheckSpellCategory(spell_id, base1, SE_LimitSpellSubclass)) return (0); - } else { + } + else { LimitInclude[14] = true; if (CheckSpellCategory(spell_id, base1, SE_LimitSpellSubclass)) // Include LimitInclude[15] = true; @@ -4600,7 +4693,12 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) LimitFailure = true; break; - // Handle Focus Effects + /* These are not applicable to AA's because there is never a 'caster' of the 'buff' with the focus effect. + case SE_Ff_Same_Caster: + case SE_Ff_CasterClass: + */ + + // Handle Focus Effects case SE_ImprovedDamage: if (type == focusImprovedDamage && base1 > value) value = base1; @@ -4667,11 +4765,13 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (value > 0) { if (base1 > value) value = base1; - } else { + } + else { if (base1 < value) value = base1; } - } else + } + else value = base1; } break; @@ -4685,7 +4785,8 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (type == focusTriggerOnCast) { if (zone->random.Roll(base1)) { value = base2; - } else { + } + else { value = 0; LimitFailure = true; } @@ -4697,6 +4798,11 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) value = base1; break; + case SE_Fc_Spell_Damage_Pct_IncomingPC: + if (type == focusFcSpellDamagePctIncomingPC) + value = base1; + break; + case SE_BlockNextSpellFocus: if (type == focusBlockNextSpell) { if (zone->random.Roll(base1)) @@ -4709,7 +4815,7 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) value = base1; break; - // Note if using these as AA, make sure this is first focus used. + // Note if using these as AA, make sure this is first focus used. case SE_SympatheticProc: if (type == focusSympatheticProc) value = base2; @@ -4735,6 +4841,11 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) value = base1; break; + case SE_Fc_Spell_Damage_Amt_IncomingPC: + if (type == focusFcSpellDamageAmtIncomingPC) + value = base1; + break; + case SE_FcHealAmtIncoming: if (type == focusFcHealAmtIncoming) value = base1; @@ -4789,6 +4900,14 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) if (type == focusFcStunTimeMod) value = base1; break; + + case SE_Fc_Cast_Spell_On_Land: + if (type == focusFcCastSpellOnLand) { + if (zone->random.Roll(base1)) { + value = base2; + } + break; + } } } @@ -4805,8 +4924,13 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) //given an item/spell's focus ID and the spell being cast, determine the focus ammount, if any //assumes that spell_id is not a bard spell and that both ids are valid spell ids -int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, bool best_focus) +int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, bool best_focus, uint16 casterid) { + /* + 'this' is always the caster of the spell_id, most foci check for effects on the caster, however some check for effects on the target. + 'casterid' is the casterid of the caster of spell_id, used when spell_id is cast on a target with a focus effect that is checked by incoming spell. + */ + if (!IsValidSpell(focus_id) || !IsValidSpell(spell_id)) return 0; @@ -5040,6 +5164,23 @@ int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, boo } break; + case SE_Ff_Same_Caster://hmm do i need to pass casterid from buff slot here + if (focus_spell.base[i] == 0) { + if (casterid == GetID()) + return 0;//Mob casting is same as target, fail if you are casting on yourself. + } + else if (focus_spell.base[i] == 1) { + if (casterid != GetID()) + return 0;//Mob casting is not same as target, fail if you are not casting on yourself. + } + break; + + case SE_Ff_CasterClass: + // Do not use this limit more then once per spell. If multiple class, treat value like items would. + if (!PassLimitClass(focus_spell.base[i], GetClass())) + return 0; + break; + // handle effects case SE_ImprovedDamage: if (type == focusImprovedDamage) { @@ -5211,13 +5352,30 @@ int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, boo if (type == focusSpellVulnerability) { if (best_focus) { if (focus_spell.base2[i] != 0) - value = focus_spell.base2[i]; + value = focus_spell.base2[i]; //max damage else - value = focus_spell.base[i]; + value = focus_spell.base[i]; //min damage } else if (focus_spell.base2[i] == 0 || focus_spell.base[i] == focus_spell.base2[i]) { - value = focus_spell.base[i]; + value = focus_spell.base[i]; //If no max damage set, then default to min damage } else { - value = zone->random.Int(focus_spell.base[i], focus_spell.base2[i]); + value = zone->random.Int(focus_spell.base[i], focus_spell.base2[i]); //else random for value + } + } + break; + + case SE_Fc_Spell_Damage_Pct_IncomingPC: + if (type == focusFcSpellDamagePctIncomingPC) { + if (best_focus) { + if (focus_spell.base2[i] != 0) + value = focus_spell.base2[i]; //max damage + else + value = focus_spell.base[i]; //min damage + } + else if (focus_spell.base2[i] == 0 || focus_spell.base[i] == focus_spell.base2[i]) { + value = focus_spell.base[i]; //If no max damage set, then default to min damage + } + else { + value = zone->random.Int(focus_spell.base[i], focus_spell.base2[i]); //else random for value } } break; @@ -5247,6 +5405,11 @@ int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, boo value = focus_spell.base[i]; break; + case SE_Fc_Spell_Damage_Amt_IncomingPC: + if (type == focusFcSpellDamageAmtIncomingPC) + value = focus_spell.base[i]; + break; + case SE_FcHealAmtIncoming: if (type == focusFcHealAmtIncoming) value = focus_spell.base[i]; @@ -5307,6 +5470,14 @@ int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, boo value = focus_spell.base[i]; break; + case SE_Fc_Cast_Spell_On_Land: + if (type == focusFcCastSpellOnLand) { + if (zone->random.Roll(focus_spell.base[i])) { + value = focus_spell.base2[i]; + } + break; + } + #if EQDEBUG >= 6 // this spits up a lot of garbage when calculating spell focuses // since they have all kinds of extra effects on them. @@ -6232,6 +6403,7 @@ bool Mob::DoHPToManaCovert(uint16 mana_cost) int32 Mob::GetFcDamageAmtIncoming(Mob *caster, uint32 spell_id, bool use_skill, uint16 skill ) { //Used to check focus derived from SE_FcDamageAmtIncoming which adds direct damage to Spells or Skill based attacks. + //Used to check focus derived from SE_Fc_Spell_Damage_Amt_IncomingPC which adds direct damage to Spells. int32 dmg = 0; bool limit_exists = false; bool skill_found = false; @@ -6280,6 +6452,20 @@ int32 Mob::GetFcDamageAmtIncoming(Mob *caster, uint32 spell_id, bool use_skill, } } } + if (spellbonuses.FocusEffects[focusFcSpellDamageAmtIncomingPC]) { + int buff_count = GetMaxTotalSlots(); + for (int i = 0; i < buff_count; i++) { + + if ((IsValidSpell(buffs[i].spellid) && (IsEffectInSpell(buffs[i].spellid, SE_FcDamageAmtIncoming)))) { + + int32 focus = caster->CalcFocusEffect(focusFcSpellDamageAmtIncomingPC, buffs[i].spellid, spell_id); + if (focus) { + dmg += focus; + CheckNumHitsRemaining(NumHit::MatchingSpells, i); + } + } + } + } return dmg; } @@ -6967,6 +7153,133 @@ void Mob::TryTriggerThreshHold(int32 damage, int effect_id, Mob* attacker){ } } +void Mob::CastSpellOnLand(Mob* caster, uint32 spell_id) +{ + /* + This function checks for incoming spells on a mob, if they meet the criteria for focus SE_Fc_Cast_Spell_on_Land then + a new spell will be cast by THIS mob as specified by the focus effect. Note: Chance to cast the spell is determined in + the CalcFocusEffect function if not 100pct. + ApplyFocusProcLimiter() function checks for SE_Proc_Timer_Modifier which allows for limiting how often a spell from effect can be triggered + for example, if set to base=1 and base2= 1500, then for everyone 1 successful trigger, you will be unable to trigger again for 1.5 seconds. + + Live only has this focus in buffs/debuffs that can be placed on a target. TODO: Will consider adding support for it as AA and Item. + */ + if (!caster) + return; + + uint32 trigger_spell_id = 0; + + //Step 1: Check this focus effect exists on the mob. + if (spellbonuses.FocusEffects[focusFcCastSpellOnLand]) { + + int buff_count = GetMaxTotalSlots(); + for (int i = 0; i < buff_count; i++) { + + if ((IsValidSpell(buffs[i].spellid) && (buffs[i].spellid != spell_id) && IsEffectInSpell(buffs[i].spellid, SE_Fc_Cast_Spell_On_Land))) { + + //Step 2: Check if we pass all focus limiters and focus chance roll + trigger_spell_id = caster->CalcFocusEffect(focusFcCastSpellOnLand, buffs[i].spellid, spell_id, false, buffs[i].casterid); + + if (IsValidSpell(trigger_spell_id) && (trigger_spell_id != spell_id)) { + + //Step 3: Check if SE_Proc_Time_Modifier is present and if so apply it. + if (ApplyFocusProcLimiter(buffs[i].spellid, i)) { + //Step 4: Cast spells + if (IsBeneficialSpell(trigger_spell_id)) { + SpellFinished(trigger_spell_id, this, EQ::spells::CastingSlot::Item, 0, -1, spells[trigger_spell_id].ResistDiff); + } + else { + Mob* current_target = GetTarget(); + //For now don't let players cast detrimental effects on themselves if they are targeting themselves. Need to confirm behavior. + if (current_target && current_target->GetID() != GetID()) + SpellFinished(trigger_spell_id, current_target, EQ::spells::CastingSlot::Item, 0, -1, spells[trigger_spell_id].ResistDiff); + } + } + + if (i >= 0) + CheckNumHitsRemaining(NumHit::MatchingSpells, i); + } + } + } + } +} + +bool Mob::ApplyFocusProcLimiter(uint32 spell_id, int buffslot) +{ + if (buffslot < 0) + return false; + + //Do not allow spell cast if timer is active. + if (buffs[buffslot].focusproclimit_time > 0) + return false; + + /* + SE_Proc_Timer_Modifier + base1= amount of total procs allowed until lock out timer is triggered, should be set to at least 1 in any spell for the effect to function. + base2= lock out timer, which prevents any more procs set in ms 1500 = 1.5 seconds + This system allows easy scaling for multiple different buffs with same effects each having seperate active individual timer checks. Ie. + */ + + if (IsValidSpell(spell_id)) { + + for (int i = 0; i < EFFECT_COUNT; i++) { + + //Step 1: Find which slot the spell effect is in. + if (spells[spell_id].effectid[i] == SE_Proc_Timer_Modifier) { + + //Step 2: Check if you still have procs left to trigger, and if so reduce available procs + if (buffs[buffslot].focusproclimit_procamt > 0) { + --buffs[buffslot].focusproclimit_procamt; //Reduce total amount of triggers possible. + } + + //Step 3: If you used all the procs in the time frame then set proc amount back to max + if (buffs[buffslot].focusproclimit_procamt == 0 && spells[spell_id].base[i] > 0) { + buffs[buffslot].focusproclimit_procamt = spells[spell_id].base[i];//reset to max + + //Step 4: Check if timer exists on this spell, and then set it, and activiate global timer if not active + if (buffs[buffslot].focusproclimit_time ==0 && spells[spell_id].base2[i] > 0) { + buffs[buffslot].focusproclimit_time = spells[spell_id].base2[i];//set time + + //Step 5: If timer is not already running, then start it. + if (!focus_proc_limit_timer.Enabled()) { + focus_proc_limit_timer.Start(250); + } + + return true; + } + } + } + } + } + return true; +} + +void Mob::FocusProcLimitProcess() +{ + /* + Fast 250 ms uinversal timer for checking Focus effects that have a proc rate limiter set in actual time. + */ + bool stop_timer = true; + int buff_count = GetMaxTotalSlots(); + for (int buffs_i = 0; buffs_i < buff_count; ++buffs_i) + { + if (IsValidSpell(buffs[buffs_i].spellid)) + { + if (buffs[buffs_i].focusproclimit_time > 0) { + buffs[buffs_i].focusproclimit_time -= 250; + stop_timer = false; + } + + if (buffs[buffs_i].focusproclimit_time < 0) + buffs[buffs_i].focusproclimit_time = 0; + } + } + + if (stop_timer) { + focus_proc_limit_timer.Disable(); + } +} + bool Mob::CheckSpellCategory(uint16 spell_id, int category_id, int effect_id){ if (!IsValidSpell(spell_id) || !category_id) diff --git a/zone/spells.cpp b/zone/spells.cpp index 5e0db7c8a..c246c79a8 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -406,8 +406,7 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, spell.targettype == ST_Self || spell.targettype == ST_AECaster || spell.targettype == ST_Ring || - spell.targettype == ST_Beam || - spell.targettype == ST_TargetOptional) && target_id == 0) + spell.targettype == ST_Beam) && target_id == 0) { LogSpells("Spell [{}] auto-targeted the caster. Group? [{}], target type [{}]", spell_id, IsGroupSpell(spell_id), spell.targettype); target_id = GetID(); @@ -1585,8 +1584,12 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce case ST_TargetOptional: { - if(!spell_target) - spell_target = this; + if (!spell_target) + { + LogSpells("Spell [{}] canceled: invalid target (normal)", spell_id); + MessageString(Chat::Red, SPELL_NEED_TAR); + return false; // can't cast these unless we have a target + } CastAction = SingleTarget; break; } @@ -3350,6 +3353,8 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid buffs[emptyslot].dot_rune = 0; buffs[emptyslot].ExtraDIChance = 0; buffs[emptyslot].RootBreakChance = 0; + buffs[emptyslot].focusproclimit_time = 0; + buffs[emptyslot].focusproclimit_procamt = 0; buffs[emptyslot].instrument_mod = caster ? caster->GetInstrumentMod(spell_id) : 10; if (level_override > 0) { @@ -3972,6 +3977,10 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, bool reflect, bool use_r return false; } + //Check SE_Fc_Cast_Spell_On_Land SPA 481 on target, if hit by this spell and Conditions are Met then target will cast the specified spell. + if (spelltar) + spelltar->CastSpellOnLand(this, spell_id); + if (IsValidSpell(spells[spell_id].RecourseLink) && spells[spell_id].RecourseLink != spell_id) SpellFinished(spells[spell_id].RecourseLink, this, CastingSlot::Item, 0, -1, spells[spells[spell_id].RecourseLink].ResistDiff); @@ -5251,6 +5260,27 @@ int Client::FindSpellBookSlotBySpellID(uint16 spellid) { return -1; //default } +uint32 Client::GetHighestScribedSpellinSpellGroup(uint32 spell_group) +{ + //Typical live spells follow 1/5/10 rank value for actual ranks 1/2/3, but this can technically be set as anything. + + int highest_rank = 0; //highest ranked found in spellgroup + uint32 highest_spell_id = 0; //spell_id of the highest ranked spell you have scribed in that spell rank. + + for (int i = 0; i < EQ::spells::SPELLBOOK_SIZE; i++) { + + if (IsValidSpell(m_pp.spell_book[i])) { + if (spells[m_pp.spell_book[i]].spellgroup == spell_group) { + if (highest_rank < spells[m_pp.spell_book[i]].rank) { + highest_rank = spells[m_pp.spell_book[i]].rank; + highest_spell_id = m_pp.spell_book[i]; + } + } + } + } + return highest_spell_id; +} + bool Client::SpellGlobalCheck(uint16 spell_id, uint32 char_id) { std::string spell_global_name; int spell_global_value; diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 34d437a90..9c493a915 100755 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -3666,6 +3666,8 @@ void ZoneDatabase::LoadBuffs(Client *client) buffs[slot_id].caston_z = caston_z; buffs[slot_id].ExtraDIChance = ExtraDIChance; buffs[slot_id].RootBreakChance = 0; + buffs[slot_id].focusproclimit_time = 0; + buffs[slot_id].focusproclimit_procamt = 0; buffs[slot_id].UpdateClient = false; buffs[slot_id].instrument_mod = instrument_mod; }