From 191aa575f853a2db6afd8160745e499324939726 Mon Sep 17 00:00:00 2001 From: KayenEQ Date: Thu, 27 Nov 2014 22:12:13 -0500 Subject: [PATCH] Projectiles (ie Arrows) fired from an archery attacks will do damage upon actually hitting the target, instead of instantly when fired. Consistent with live. Optional SQL added to disable this. Throwing will be implemented in a future update. --- changelog.txt | 5 + common/ruletypes.h | 1 + .../2014_11_27_ProjectileDmgOnImpact.sql | 1 + zone/client_process.cpp | 2 + zone/common.h | 14 + zone/mob.cpp | 15 + zone/mob.h | 9 +- zone/special_attacks.cpp | 402 ++++++++++++------ 8 files changed, 323 insertions(+), 126 deletions(-) create mode 100644 utils/sql/git/optional/2014_11_27_ProjectileDmgOnImpact.sql diff --git a/changelog.txt b/changelog.txt index b05fc26ee..c05951538 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- + +== 11/27/2014 == +Kayen: Projectiles (ie Arrows) fired from archery will now do damage upon impact instead of instantly (consistent w/ live). +Optional SQL: utils/sql/git/optional/2014_11_27_ProjectileDmgOnImpact.sql + == 11/24/2014 == Trevius: Spells that modify model size are now limited to 2 size adjustments from the base size. Trevius: Fix to prevent Mercenaries from being set as Group Leader. diff --git a/common/ruletypes.h b/common/ruletypes.h index b3e9b4f1c..8ff077809 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -419,6 +419,7 @@ RULE_INT ( Combat, ArcheryBonusChance, 50) RULE_INT ( Combat, BerserkerFrenzyStart, 35) RULE_INT ( Combat, BerserkerFrenzyEnd, 45) RULE_BOOL ( Combat, OneProcPerWeapon, true) //If enabled, One proc per weapon per round +RULE_BOOL ( Combat, ProjectileDmgOnImpact, true) //If enabled, projectiles (ie arrows) will hit on impact, instead of instantly. RULE_CATEGORY_END() RULE_CATEGORY( NPC ) diff --git a/utils/sql/git/optional/2014_11_27_ProjectileDmgOnImpact.sql b/utils/sql/git/optional/2014_11_27_ProjectileDmgOnImpact.sql new file mode 100644 index 000000000..ce7e16905 --- /dev/null +++ b/utils/sql/git/optional/2014_11_27_ProjectileDmgOnImpact.sql @@ -0,0 +1 @@ +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Combat:ProjectileDmgOnImpact', 'true', 'If enabled, projectiles (ie arrows) will hit on impact, instead of instantly.'); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index c07a73aea..40b7420d3 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -579,6 +579,8 @@ bool Client::Process() { viral_timer_counter = 0; } + ProjectileAttack(); + if(projectile_timer.Check()) SpellProjectileEffect(); diff --git a/zone/common.h b/zone/common.h index ea299ab11..d1aad5e40 100644 --- a/zone/common.h +++ b/zone/common.h @@ -459,6 +459,20 @@ struct Shielders_Struct { uint16 shielder_bonus; }; +typedef struct +{ + uint16 increment; + uint16 hit_increment; + uint16 target_id; + int32 wpn_dmg; + float origin_x; + float origin_y; + float origin_z; + uint32 ranged_id; + uint32 ammo_id; + uint8 skill; +} tProjatk; + //eventually turn this into a typedef and //make DoAnim take it instead of int, to enforce its use. enum { //type arguments to DoAnim diff --git a/zone/mob.cpp b/zone/mob.cpp index 35a019252..3b74d2417 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -287,6 +287,21 @@ Mob::Mob(const char* in_name, for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_z[i] = 0; } projectile_timer.Disable(); + ActiveProjectileATK = false; + for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) + { + ProjectileAtk[i].increment = 0; + ProjectileAtk[i].hit_increment = 0; + ProjectileAtk[i].target_id = 0; + ProjectileAtk[i].wpn_dmg = 0; + ProjectileAtk[i].origin_x = 0.0f; + ProjectileAtk[i].origin_y = 0.0f; + ProjectileAtk[i].origin_z = 0.0f; + ProjectileAtk[i].ranged_id = 0; + ProjectileAtk[i].ammo_id = 0; + ProjectileAtk[i].skill = 0; + } + memset(&itembonuses, 0, sizeof(StatBonuses)); memset(&spellbonuses, 0, sizeof(StatBonuses)); memset(&aabonuses, 0, sizeof(StatBonuses)); diff --git a/zone/mob.h b/zone/mob.h index 525cbb2e8..df694aae9 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -724,7 +724,11 @@ public: virtual void DoSpecialAttackDamage(Mob *who, SkillUseTypes skill, int32 max_damage, int32 min_damage = 1, int32 hate_override = -1, int ReuseTime = 10, bool HitChance=false, bool CanAvoid=true); virtual void DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const Item_Struct* item=nullptr, uint16 weapon_damage=0, int16 chance_mod=0,int16 focus=0, int ReuseTime=0); virtual void DoMeleeSkillAttackDmg(Mob* other, uint16 weapon_damage, SkillUseTypes skillinuse, int16 chance_mod=0, int16 focus=0, bool CanRiposte=false, int ReuseTime=0); - virtual void DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const ItemInst* Ammo=nullptr, uint16 weapon_damage=0, int16 chance_mod=0, int16 focus=0, int ReuseTime=0); + virtual void DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const ItemInst* Ammo=nullptr, uint16 weapon_damage=0, int16 chance_mod=0, int16 focus=0, int ReuseTime=0, uint32 range_id=0, uint32 ammo_id=0, const Item_Struct *AmmoItem=nullptr); + bool TryProjectileAttack(Mob* other, const Item_Struct *item, SkillUseTypes skillInUse, uint16 weapon_dmg, const ItemInst* RangeWeapon, const ItemInst* Ammo); + void ProjectileAttack(); + inline bool HasProjectileAttack() const { return ActiveProjectileATK; } + inline void SetProjectileAttack(bool value) { ActiveProjectileATK = value; } bool CanDoSpecialAttack(Mob *other); bool Flurry(ExtraAttackOptions *opts); bool Rampage(ExtraAttackOptions *opts); @@ -1096,6 +1100,9 @@ protected: uint8 projectile_increment[MAX_SPELL_PROJECTILE]; float projectile_x[MAX_SPELL_PROJECTILE], projectile_y[MAX_SPELL_PROJECTILE], projectile_z[MAX_SPELL_PROJECTILE]; + bool ActiveProjectileATK; + tProjatk ProjectileAtk[MAX_SPELL_PROJECTILE]; + float rewind_x; float rewind_y; float rewind_z; diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index 3625c9180..879515d20 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -789,8 +789,8 @@ void Client::RangedAttack(Mob* other, bool CanDoubleAttack) { return; } - SendItemAnimation(GetTarget(), AmmoItem, SkillArchery); - DoArcheryAttackDmg(GetTarget(), RangeWeapon, Ammo); + //Shoots projectile and/or applies the archery damage + DoArcheryAttackDmg(GetTarget(), RangeWeapon, Ammo,0,0,0,0,0,0, AmmoItem); //EndlessQuiver AA base1 = 100% Chance to avoid consumption arrow. int ChanceAvoidConsume = aabonuses.ConsumeProjectile + itembonuses.ConsumeProjectile + spellbonuses.ConsumeProjectile; @@ -806,147 +806,206 @@ void Client::RangedAttack(Mob* other, bool CanDoubleAttack) { CommonBreakInvisible(); } -void Mob::DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon, const ItemInst* Ammo, uint16 weapon_damage, int16 chance_mod, int16 focus, int ReuseTime) { - if (!CanDoSpecialAttack(other)) +void Mob::DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon, const ItemInst* Ammo, uint16 weapon_damage, int16 chance_mod, int16 focus, int ReuseTime, + uint32 range_id, uint32 ammo_id, const Item_Struct *AmmoItem) { + + if ((other == nullptr || + ((IsClient() && CastToClient()->dead) || + (other->IsClient() && other->CastToClient()->dead)) || + HasDied() || + (!IsAttackAllowed(other)) || + (other->GetInvul() || + other->GetSpecialAbility(IMMUNE_MELEE)))) + { return; + } - if (!other->CheckHitChance(this, SkillArchery, MainPrimary, chance_mod)) { + const ItemInst* _RangeWeapon = nullptr; + const ItemInst* _Ammo = nullptr; + const Item_Struct* ammo_lost = nullptr; + + /* + If LaunchProjectile is false this function will do archery damage on target, + otherwise it will shoot the projectile at the target, once the projectile hits target + this function is then run again to do the damage portion + */ + bool LaunchProjectile = false; + bool ProjectileMiss = false; + + if (RuleB(Combat, ProjectileDmgOnImpact)){ + + if (AmmoItem) + LaunchProjectile = true; + else{ + /* + Item sync check on projectile landing. + Weapon damage is already calculated so this only affects procs! + Ammo proc check will use database to find proc if you used up your last ammo. + If you change range item mid projectile flight, you loose your chance to proc from bow (Deal with it!). + */ + + if (!RangeWeapon && !Ammo && range_id && ammo_id){ + + if (weapon_damage == 0) + ProjectileMiss = true; //This indicates that MISS was originally calculated. + + if (IsClient()){ + + _RangeWeapon = CastToClient()->m_inv[MainRange]; + if (!_RangeWeapon || _RangeWeapon->GetItem()->ID != range_id) + RangeWeapon = nullptr; + else + RangeWeapon = _RangeWeapon; + + _Ammo = CastToClient()->m_inv[MainAmmo]; + if (!_Ammo || _Ammo->GetItem()->ID != ammo_id) + ammo_lost = database.GetItem(ammo_id); + else + Ammo = _Ammo; + } + } + } + } + else if (AmmoItem) + SendItemAnimation(other, AmmoItem, SkillArchery); + + if (ProjectileMiss || !other->CheckHitChance(this, SkillArchery, MainPrimary, chance_mod)) { mlog(COMBAT__RANGED, "Ranged attack missed %s.", other->GetName()); - other->Damage(this, 0, SPELL_UNKNOWN, SkillArchery); + + if (LaunchProjectile){ + TryProjectileAttack(other, AmmoItem, SkillArchery, 0, RangeWeapon, Ammo); + return; + } + else + other->Damage(this, 0, SPELL_UNKNOWN, SkillArchery); } else { mlog(COMBAT__RANGED, "Ranged attack hit %s.", other->GetName()); + bool HeadShot = false; + uint32 HeadShot_Dmg = TryHeadShot(other, SkillArchery); + if (HeadShot_Dmg) + HeadShot = true; - bool HeadShot = false; - uint32 HeadShot_Dmg = TryHeadShot(other, SkillArchery); - if (HeadShot_Dmg) - HeadShot = true; + int32 hate = 0; + int32 TotalDmg = 0; + int16 WDmg = 0; + int16 ADmg = 0; + if (!weapon_damage){ + WDmg = GetWeaponDamage(other, RangeWeapon); + ADmg = GetWeaponDamage(other, Ammo); + } + else + WDmg = weapon_damage; - int32 TotalDmg = 0; - int16 WDmg = 0; - int16 ADmg = 0; - if (!weapon_damage){ - WDmg = GetWeaponDamage(other, RangeWeapon); - ADmg = GetWeaponDamage(other, Ammo); - } - else - WDmg = weapon_damage; + if (LaunchProjectile){//1: Shoot the Projectile once we calculate weapon damage. + TryProjectileAttack(other, AmmoItem, SkillArchery, WDmg, RangeWeapon, Ammo); + return; + } - if (focus) //From FcBaseEffects - WDmg += WDmg*focus/100; + if (focus) //From FcBaseEffects + WDmg += WDmg*focus/100; - if((WDmg > 0) || (ADmg > 0)) { - if(WDmg < 0) - WDmg = 0; - if(ADmg < 0) - ADmg = 0; - uint32 MaxDmg = (RuleR(Combat, ArcheryBaseDamageBonus)*(WDmg+ADmg)*GetDamageTable(SkillArchery)) / 100; - int32 hate = ((WDmg+ADmg)); - - if (HeadShot) - MaxDmg = HeadShot_Dmg; - - uint16 bonusArcheryDamageModifier = aabonuses.ArcheryDamageModifier + itembonuses.ArcheryDamageModifier + spellbonuses.ArcheryDamageModifier; - - MaxDmg += MaxDmg*bonusArcheryDamageModifier / 100; - - mlog(COMBAT__RANGED, "Bow DMG %d, Arrow DMG %d, Max Damage %d.", WDmg, ADmg, MaxDmg); - - bool dobonus = false; - if(GetClass() == RANGER && GetLevel() > 50) - { - int bonuschance = RuleI(Combat, ArcheryBonusChance); - - bonuschance = mod_archery_bonus_chance(bonuschance, RangeWeapon); - - if( !RuleB(Combat, UseArcheryBonusRoll) || (MakeRandomInt(1, 100) < bonuschance) ) - { - if(RuleB(Combat, ArcheryBonusRequiresStationary)) - { - if(other->IsNPC() && !other->IsMoving() && !other->IsRooted()) - { - dobonus = true; - } - } - else - { - dobonus = true; - } - } - - if(dobonus) - { - MaxDmg *= 2; - hate *= 2; - MaxDmg = mod_archery_bonus_damage(MaxDmg, RangeWeapon); - - mlog(COMBAT__RANGED, "Ranger. Double damage success roll, doubling damage to %d", MaxDmg); - Message_StringID(MT_CritMelee, BOW_DOUBLE_DAMAGE); - } - } - - if (MaxDmg == 0) - MaxDmg = 1; - - if(RuleB(Combat, UseIntervalAC)) - TotalDmg = MaxDmg; - else - TotalDmg = MakeRandomInt(1, MaxDmg); - - int minDmg = 1; - if(GetLevel() > 25){ - //twice, for ammo and weapon - TotalDmg += (2*((GetLevel()-25)/3)); - minDmg += (2*((GetLevel()-25)/3)); - minDmg += minDmg * GetMeleeMinDamageMod_SE(SkillArchery) / 100; - hate += (2*((GetLevel()-25)/3)); - } - - if (!HeadShot) - other->AvoidDamage(this, TotalDmg, false); - - other->MeleeMitigation(this, TotalDmg, minDmg); - if(TotalDmg > 0) - { - ApplyMeleeDamageBonus(SkillArchery, TotalDmg); - TotalDmg += other->GetFcDamageAmtIncoming(this, 0, true, SkillArchery); - TotalDmg += (itembonuses.HeroicDEX / 10) + (TotalDmg * other->GetSkillDmgTaken(SkillArchery) / 100) + GetSkillDmgAmt(SkillArchery); - - TotalDmg = mod_archery_damage(TotalDmg, dobonus, RangeWeapon); - - TryCriticalHit(other, SkillArchery, TotalDmg); - other->AddToHateList(this, hate, 0, false); - CheckNumHitsRemaining(NUMHIT_OutgoingHitSuccess); - } - } - else - TotalDmg = -5; + if((WDmg > 0) || (ADmg > 0)) { + if(WDmg < 0) + WDmg = 0; + if(ADmg < 0) + ADmg = 0; + uint32 MaxDmg = (RuleR(Combat, ArcheryBaseDamageBonus)*(WDmg+ADmg)*GetDamageTable(SkillArchery)) / 100; + hate = ((WDmg+ADmg)); if (HeadShot) - entity_list.MessageClose_StringID(this, false, 200, MT_CritMelee, FATAL_BOW_SHOT, GetName()); - - other->Damage(this, TotalDmg, SPELL_UNKNOWN, SkillArchery); - - if (TotalDmg > 0 && HasSkillProcSuccess() && GetTarget() && other && !other->HasDied()){ - if (ReuseTime) - TrySkillProc(other, SkillArchery, ReuseTime); - else - TrySkillProc(other, SkillArchery, 0, true, MainRange); + MaxDmg = HeadShot_Dmg; + + uint16 bonusArcheryDamageModifier = aabonuses.ArcheryDamageModifier + itembonuses.ArcheryDamageModifier + spellbonuses.ArcheryDamageModifier; + + MaxDmg += MaxDmg*bonusArcheryDamageModifier / 100; + + mlog(COMBAT__RANGED, "Bow DMG %d, Arrow DMG %d, Max Damage %d.", WDmg, ADmg, MaxDmg); + + bool dobonus = false; + if(GetClass() == RANGER && GetLevel() > 50){ + + int bonuschance = RuleI(Combat, ArcheryBonusChance); + bonuschance = mod_archery_bonus_chance(bonuschance, RangeWeapon); + + if( !RuleB(Combat, UseArcheryBonusRoll) || (MakeRandomInt(1, 100) < bonuschance)){ + if(RuleB(Combat, ArcheryBonusRequiresStationary)){ + if(other->IsNPC() && !other->IsMoving() && !other->IsRooted()) + dobonus = true; + } + else + dobonus = true; + } + + if(dobonus){ + MaxDmg *= 2; + hate *= 2; + MaxDmg = mod_archery_bonus_damage(MaxDmg, RangeWeapon); + + mlog(COMBAT__RANGED, "Ranger. Double damage success roll, doubling damage to %d", MaxDmg); + Message_StringID(MT_CritMelee, BOW_DOUBLE_DAMAGE); + } } + + if (MaxDmg == 0) + MaxDmg = 1; + + if(RuleB(Combat, UseIntervalAC)) + TotalDmg = MaxDmg; + else + TotalDmg = MakeRandomInt(1, MaxDmg); + + int minDmg = 1; + if(GetLevel() > 25){ + //twice, for ammo and weapon + TotalDmg += (2*((GetLevel()-25)/3)); + minDmg += (2*((GetLevel()-25)/3)); + minDmg += minDmg * GetMeleeMinDamageMod_SE(SkillArchery) / 100; + hate += (2*((GetLevel()-25)/3)); + } + + if (!HeadShot) + other->AvoidDamage(this, TotalDmg, false); + + other->MeleeMitigation(this, TotalDmg, minDmg); + if(TotalDmg > 0){ + CommonOutgoingHitSuccess(other, TotalDmg, SkillArchery); + TotalDmg = mod_archery_damage(TotalDmg, dobonus, RangeWeapon); + } + } + else + TotalDmg = -5; + + if (HeadShot) + entity_list.MessageClose_StringID(this, false, 200, MT_CritMelee, FATAL_BOW_SHOT, GetName()); + + other->AddToHateList(this, hate, 0, false); + other->Damage(this, TotalDmg, SPELL_UNKNOWN, SkillArchery); + + //Skill Proc Success + if (TotalDmg > 0 && HasSkillProcSuccess() && other && !other->HasDied()){ + if (ReuseTime) + TrySkillProc(other, SkillArchery, ReuseTime); + else + TrySkillProc(other, SkillArchery, 0, true, MainRange); + } } - //try proc on hits and misses - if((RangeWeapon != nullptr) && GetTarget() && other && !other->HasDied()){ + if (LaunchProjectile) + return;//Shouldn't reach this point, but just in case. + + //Weapon Proc + if(!RangeWeapon && other && !other->HasDied()) TryWeaponProc(RangeWeapon, other, MainRange); - } - //Arrow procs because why not? - if((Ammo != NULL) && GetTarget() && other && !other->HasDied()) - { - TryWeaponProc(Ammo, other, MainRange); - } + //Ammo Proc + if (ammo_lost) + TryWeaponProc(nullptr, ammo_lost, other, MainRange); + else if(Ammo && other && !other->HasDied()) + TryWeaponProc(Ammo, other, MainRange); - if (HasSkillProcs() && GetTarget() && other && !other->HasDied()){ + //Skill Proc + if (HasSkillProcs() && other && !other->HasDied()){ if (ReuseTime) TrySkillProc(other, SkillArchery, ReuseTime); else @@ -954,6 +1013,99 @@ void Mob::DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon, const Item } } +bool Mob::TryProjectileAttack(Mob* other, const Item_Struct *item, SkillUseTypes skillInUse, uint16 weapon_dmg, const ItemInst* RangeWeapon, const ItemInst* Ammo){ + + if (!other) + return false; + + int slot = -1; + + //Make sure there is an avialable slot. + for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { + if (ProjectileAtk[i].target_id == 0){ + slot = i; + break; + } + } + + if (slot < 0) + return false; + + float distance = other->CalculateDistance(GetX(), GetY(), GetZ()); + float hit = 60.0f + (distance / 1.8f); //Calcuation: 60 = Animation Lag, 1.8 = Speed modifier for speed of (4) + + ProjectileAtk[slot].increment = 1; + ProjectileAtk[slot].hit_increment = hit; //This projected hit time if target does NOT MOVE + ProjectileAtk[slot].target_id = other->GetID(); + ProjectileAtk[slot].wpn_dmg = weapon_dmg; + ProjectileAtk[slot].origin_x = GetX(); + ProjectileAtk[slot].origin_y = GetY(); + ProjectileAtk[slot].origin_z = GetZ(); + ProjectileAtk[slot].ranged_id = RangeWeapon->GetItem()->ID; + ProjectileAtk[slot].ammo_id = Ammo->GetItem()->ID; + ProjectileAtk[slot].skill = skillInUse; + + SetProjectileAttack(true); + + if(item) + SendItemAnimation(other, item, skillInUse); + else if (IsNPC()) + ProjectileAnimation(other, 0,false,0,0,0,0,CastToNPC()->GetAmmoIDfile(),skillInUse); + + return true; +} + + +void Mob::ProjectileAttack() +{ + if (!HasProjectileAttack()) + return;; + + Mob* target = nullptr; + bool disable = true; + + for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { + + if (ProjectileAtk[i].increment == 0){ + continue; + } + + disable = false; + Mob* target = entity_list.GetMobID(ProjectileAtk[i].target_id); + + float distance = 0.0f; + + if (target && IsMoving()){ //Only recalculate hit increment if target moving + distance = target->CalculateDistance(ProjectileAtk[i].origin_x, ProjectileAtk[i].origin_y, ProjectileAtk[i].origin_z); + float hit = 60.0f + (distance / 1.8f); //Calcuation: 60 = Animation Lag, 1.8 = Speed modifier for speed of (4) + ProjectileAtk[i].hit_increment = static_cast(hit); + } + + if (ProjectileAtk[i].hit_increment <= ProjectileAtk[i].increment){ + + if (ProjectileAtk[i].skill == SkillArchery) + DoArcheryAttackDmg(target, nullptr, nullptr,ProjectileAtk[i].wpn_dmg,0,0,0,ProjectileAtk[i].ranged_id, ProjectileAtk[i].ammo_id); + + ProjectileAtk[i].increment = 0; + ProjectileAtk[i].target_id = 0; + ProjectileAtk[i].wpn_dmg = 0; + ProjectileAtk[i].origin_x = 0.0f; + ProjectileAtk[i].origin_y = 0.0f; + ProjectileAtk[i].origin_z = 0.0f; + ProjectileAtk[i].ranged_id = 0; + ProjectileAtk[i].ammo_id = 0; + ProjectileAtk[i].skill = 0; + } + + else { + ProjectileAtk[i].increment++; + } + } + + if (disable) + SetProjectileAttack(false); +} + void NPC::RangedAttack(Mob* other) {