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) {