From 95cbadade5ab9ae2d35dc57bf3e241e1bae3f285 Mon Sep 17 00:00:00 2001 From: Fryguy Date: Sun, 7 Jul 2024 00:53:29 -0400 Subject: [PATCH] [Bug Fix] Slay Adjustments (#4389) Previous change did not account for the modern slay undead and holyforge spells. Reverted some of the changes and cleaned up others. Rule Renamed (Default value was incorrect, this was a clean way to fix that) - SlayDamageAdjustment -> SlayDamageMultiplier Also added a rate multiplier RULE_REAL(Combat, SlayRateMultiplier, 1.0, "Slay Rate Adjustments - Multiply final slay rate check by this value. Default: 1.0") Fixed the ordering of the constants for the slay undead SPA that were backwards and causing major headaches with tuning and setting up slay undead correctly. Base = Damage Mod (100 is base, so 240 = 140% more) Limit = Proc Rate - Value is divided by 10000 for a Float %. e.g. 1700 becomes 0.17 (Or 17% proc rate). Damage bonus should be additive not std::max as AA, Spells and Item bonuses should stack. e.g. Slay Undead RK3 240 + Holy Forge 140 should = 380 (280% damage) --- common/ruletypes.h | 3 +- zone/attack.cpp | 331 ++++++++++++++++++++++----------------------- zone/bonuses.cpp | 8 +- zone/common.h | 4 +- zone/mob.h | 6 - 5 files changed, 171 insertions(+), 181 deletions(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index 55a237154..96f3e9162 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -619,7 +619,8 @@ RULE_INT(Combat, PCAccuracyAvoidanceMod2Scale, 100, "Scale Factor for PC Accurac RULE_BOOL(Combat, AllowRaidTargetBlind, false, "Toggle to allow raid targets to be blinded, default is false (Live-like)") RULE_BOOL(Combat, RogueBackstabHasteCorrection, false, "Toggle to enable correction for Haste impacting Backstab DPS too much. DEFAULT: false") RULE_BOOL(Combat, LegacyComputeDefense, false, "Trim AGI Scaling of defense mostly for lower levels to help compensate for the newer agi based defense system. Default: False") -RULE_REAL(Combat, SlayDamageAdjustment, 0.5, "Slay Damage Adjustment - Multiply final slay damage by this value. Default: 0.5") +RULE_REAL(Combat, SlayDamageMultiplier, 1.0, "Slay Damage Adjustment - Multiply final slay damage by this value. Default: 1.0") +RULE_REAL(Combat, SlayRateMultiplier, 1.0, "Slay Rate Adjustments - Multiply final slay rate check by this value. Default: 1.0") RULE_INT(Combat, MaximumLevelStunsCripplingBlow, 55, "Maximum level that Crippling Blows will stun a npc. Default: 55") RULE_INT(Combat, ArcheryBaseDamage, 0, "Archery base damage, default is 0") RULE_INT(Combat, BackstabBaseDamage, 0, "Backstab base damage, default is 0") diff --git a/zone/attack.cpp b/zone/attack.cpp index bda224368..025ab4425 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -5388,101 +5388,6 @@ void Mob::TryPetCriticalHit(Mob *defender, DamageHitInfo &hit) } } -bool Mob::RollMeleeCritCheck(Mob *defender, EQ::skills::SkillType skill) -{ - // We either require an innate crit chance or some SPA 169 to crit - bool innate_crit = false; - int crit_chance = GetCriticalChanceBonus(skill); - // Paladin check - if (defender->IsUndeadForSlay()) { - crit_chance = crit_chance + GetUndeadSlayRate(); - } - - if (GetLevel() >= 12) { - if ( - GetClass() == Class::Warrior || - (GetClass() == Class::Ranger && skill == EQ::skills::SkillArchery) || - (GetClass() == Class::Rogue && skill == EQ::skills::SkillThrowing) || - GetClass() == Class::Berserker - ) { - innate_crit = true; - } - } - - // we have a chance to crit! - if (innate_crit || crit_chance) { - int difficulty = 0; - - if (skill == EQ::skills::SkillArchery) { - difficulty = RuleI(Combat, ArcheryCritDifficulty); - } else if (skill == EQ::skills::SkillThrowing) { - difficulty = RuleI(Combat, ThrowingCritDifficulty); - } else { - difficulty = RuleI(Combat, MeleeCritDifficulty); - } - - int roll = zone->random.Int(1, difficulty); - - int dex_bonus = GetDEX(); - - if (dex_bonus > 255) { - dex_bonus = 255 + ((dex_bonus - 255) / 5); - } - - dex_bonus += 45; // chances did not match live without a small boost - - // so if we have an innate crit we have a better chance, except for ber throwing - if (!innate_crit || (GetClass() == Class::Berserker && skill == EQ::skills::SkillThrowing)) { - dex_bonus = dex_bonus * 3 / 5; - } - - LogCombat("Crit Chance: dex_bonus ({}) * crit_chance ({}) / 100", dex_bonus, crit_chance); - - if (crit_chance) { - dex_bonus += dex_bonus * crit_chance / 100; - } - - // check if we crited - LogCombat("Final Roll! Difficulty = [{}] -- Dex_Bonus = [{}] ", difficulty, dex_bonus); - return (roll < dex_bonus); - } - - return false; -} - -int Mob::GetUndeadSlayRate() -{ - return aabonuses.SlayUndead[0] + itembonuses.SlayUndead[0] + spellbonuses.SlayUndead[0]; -} - -void Mob::DoUndeadSlay(DamageHitInfo &hit, int crit_mod) -{ - - int slay_damage_bonus = std::max( - { aabonuses.SlayUndead[1], itembonuses.SlayUndead[1], spellbonuses.SlayUndead[1] }); - - LogCombatDetail("Slayundead bonus [{}]", slay_damage_bonus); - - hit.damage_done = std::max(hit.damage_done, hit.base_damage) + 5; - hit.damage_done = (hit.damage_done * slay_damage_bonus * crit_mod) / 100; - hit.damage_done = static_cast(hit.damage_done * RuleR(Combat, SlayDamageAdjustment)); - - LogCombatDetail("Slayundead damage [{}]", hit.damage_done); - - int slay_sex = GetGender() == Gender::Female ? FEMALE_SLAYUNDEAD : MALE_SLAYUNDEAD; - - entity_list.FilteredMessageString( - this, /* Sender */ - false, /* Skip Sender */ - Chat::MeleeCrit, /* Type: 301 */ - FilterMeleeCrits, /* FilterType: 12 */ - slay_sex, /* MessageFormat: %1's holy blade cleanses her target!(%2) */ - GetCleanName(), /* Message1 */ - itoa(hit.damage_done) /* Message2 */ - ); -} - -// a lot of good info: http://giline.versus.jp/shiden/damage_e.htm, http://giline.versus.jp/shiden/su.htm void Mob::TryCriticalHit(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions *opts) { #ifdef LUA_EQEMU @@ -5494,8 +5399,9 @@ void Mob::TryCriticalHit(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions * } #endif - if (hit.damage_done < 1 || !defender) + if (hit.damage_done < 1 || !defender) { return; + } // decided to branch this into it's own function since it's going to be duplicating a lot of the // code in here, but could lead to some confusion otherwise @@ -5513,112 +5419,201 @@ void Mob::TryCriticalHit(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions * return; } - // Step 1: Check if we are critting - if (!RollMeleeCritCheck(defender, hit.skill)) { - return; - } + // 1: Try Slay Undead + if (defender->GetBodyType() == BodyType::Undead || defender->GetBodyType() == BodyType::SummonedUndead || + defender->GetBodyType() == BodyType::Vampire) { + int slay_rate_bonus = aabonuses.SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD] + itembonuses.SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD] + spellbonuses.SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD]; - int crit_mod = EQ::ClampLower((170 + GetCritDmgMod(hit.skill)), 100); + LogCombatDetail("Slayundead hit rate [{}]", slay_rate_bonus); - // Step 2: Calculate damage - hit.damage_done = std::max(hit.damage_done, hit.base_damage) + 5; - int og_damage = hit.damage_done; - hit.damage_done = hit.damage_done * crit_mod / 100; + if (slay_rate_bonus) { + float slay_chance = ((static_cast(slay_rate_bonus) / 10000.0f) * RuleR(Combat, SlayRateMultiplier)); - LogCombatDetail("Crit info: [{}] scaled from: [{}] - IsUndeadForSlay: [{}]", hit.damage_done, og_damage, IsUndeadForSlay() ? "true" : "false"); + if (zone->random.Roll(slay_chance)) { + int slay_damage_bonus = aabonuses.SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] + itembonuses.SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] + spellbonuses.SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD]; - // Try Slay Undead - if (defender->IsUndeadForSlay()) { - float chance = GetUndeadSlayRate() / 100.0f; - LogCombatDetail("Trying Undead slay: Chance: [{}]", chance); + LogCombatDetail("Slayundead damage bonus [{}]", slay_damage_bonus); - if(zone->random.Roll(chance)) { - DoUndeadSlay(hit, crit_mod); - return; - } - } + hit.damage_done = std::max(hit.damage_done, hit.base_damage) + 5; + hit.damage_done = (hit.damage_done * slay_damage_bonus) / 100; + hit.damage_done = static_cast(hit.damage_done * RuleR(Combat, SlayDamageMultiplier)); - // Step 3: Check deadly strike - if (GetClass() == Class::Rogue && hit.skill == EQ::skills::SkillThrowing && BehindMob(defender, GetX(), GetY())) { - int chance = GetLevel() * 12; - if (zone->random.Int(1, 1000) < chance) { - // Check assassinate - int assassinate_damage = TryAssassinate(defender, hit.skill); + int min_slay = (hit.min_damage + 5) * slay_damage_bonus / 100; - if (assassinate_damage) { - hit.damage_done = assassinate_damage; - return; - } + LogCombatDetail(" Calculated Slayundead damage [{}] - Min Slay Undead Damage [{}]", hit.damage_done, min_slay); - hit.damage_done = hit.damage_done * 200 / 100; + if (hit.damage_done < min_slay) { + hit.damage_done = min_slay; + } - entity_list.FilteredMessageCloseString( + LogCombatDetail("Final Slayundead damage [{}]", hit.damage_done); + + int slay_sex = GetGender() == Gender::Female ? FEMALE_SLAYUNDEAD : MALE_SLAYUNDEAD; + + entity_list.FilteredMessageCloseString( this, /* Sender */ false, /* Skip Sender */ RuleI(Range, CriticalDamage), Chat::MeleeCrit, /* Type: 301 */ FilterMeleeCrits, /* FilterType: 12 */ - DEADLY_STRIKE, /* MessageFormat: %1 scores a Deadly Strike!(%2) */ + slay_sex, 0, GetCleanName(), /* Message1 */ itoa(hit.damage_done) /* Message2 */ - ); - return; + ); + return; + } } } - // Step 4: check cripple - bool berserk = spellbonuses.BerserkSPA || itembonuses.BerserkSPA || aabonuses.BerserkSPA; + // 2: Try Melee Critical + // a lot of good info: http://giline.versus.jp/shiden/damage_e.htm, http://giline.versus.jp/shiden/su.htm - if (!berserk && zone->random.Roll(GetCrippBlowChance())) { - berserk = true; + // We either require an innate crit chance or some SPA 169 to crit + bool innate_crit = false; + int crit_chance = GetCriticalChanceBonus(hit.skill); + if ((GetClass() == Class::Warrior || GetClass() == Class::Berserker) && GetLevel() >= 12) { + innate_crit = true; + } else if (GetClass() == Class::Ranger && GetLevel() >= 12 && hit.skill == EQ::skills::SkillArchery) { + innate_crit = true; + } else if (GetClass() == Class::Rogue && GetLevel() >= 12 && hit.skill == EQ::skills::SkillThrowing) { + innate_crit = true; } - if (IsBerserk() || berserk) { - hit.damage_done += og_damage * 119 / 100; - LogCombatDetail("Crippling damage [{}]", hit.damage_done); + // we have a chance to crit! + if (innate_crit || crit_chance) { + int difficulty = 0; - entity_list.FilteredMessageCloseString( + if (hit.skill == EQ::skills::SkillArchery) { + difficulty = RuleI(Combat, ArcheryCritDifficulty); + } else if (hit.skill == EQ::skills::SkillThrowing) { + difficulty = RuleI(Combat, ThrowingCritDifficulty); + } else { + difficulty = RuleI(Combat, MeleeCritDifficulty); + } + + int roll = zone->random.Int(1, difficulty); + int dex_bonus = GetDEX(); + + if (dex_bonus > 255) { + dex_bonus = 255 + ((dex_bonus - 255) / 5); + } + + dex_bonus += 45; // chances did not match live without a small boost + + // so if we have an innate crit we have a better chance, except for ber throwing + if (!innate_crit || (GetClass() == Class::Berserker && hit.skill == EQ::skills::SkillThrowing)) { + dex_bonus = dex_bonus * 3 / 5; + } + + if (crit_chance) { + dex_bonus += dex_bonus * crit_chance / 100; + } + + // check if we crited + if (roll < dex_bonus) { + // step 1: check for finishing blow + if (TryFinishingBlow(defender, hit.damage_done)) { + return; + } + + // step 2: calculate damage + hit.damage_done = std::max(hit.damage_done, hit.base_damage) + 5; + int og_damage = hit.damage_done; + int crit_mod = 170 + GetCritDmgMod(hit.skill); + + if (crit_mod < 100) { + crit_mod = 100; + } + + hit.damage_done = hit.damage_done * crit_mod / 100; + LogCombatDetail("Crit success roll [{}] dex chance [{}] og dmg [{}] crit_mod [{}] new dmg [{}]", roll, dex_bonus, og_damage, crit_mod, hit.damage_done); + + // step 3: check deadly strike + if (GetClass() == Class::Rogue && hit.skill == EQ::skills::SkillThrowing) { + if (BehindMob(defender, GetX(), GetY())) { + int chance = GetLevel() * 12; + if (zone->random.Int(1, 1000) < chance) { + // step 3a: check assassinate + int assassinate_damage = TryAssassinate(defender, hit.skill); // I don't think this is right + if (assassinate_damage) { + hit.damage_done = assassinate_damage; + return; + } + hit.damage_done = hit.damage_done * 200 / 100; + + entity_list.FilteredMessageCloseString( + this, /* Sender */ + false, /* Skip Sender */ + RuleI(Range, CriticalDamage), + Chat::MeleeCrit, /* Type: 301 */ + FilterMeleeCrits, /* FilterType: 12 */ + DEADLY_STRIKE, /* MessageFormat: %1 scores a Deadly Strike!(%2) */ + 0, + GetCleanName(), /* Message1 */ + itoa(hit.damage_done + hit.min_damage) /* Message2 */ + ); + return; + } + } + } + + // step 4: check crips + // this SPA was reused on live ... + bool berserk = spellbonuses.BerserkSPA || itembonuses.BerserkSPA || aabonuses.BerserkSPA; + if (!berserk) { + if (zone->random.Roll(GetCrippBlowChance())) { + berserk = true; + } + } + + if (IsBerserk() || berserk) { + hit.damage_done += og_damage * 119 / 100; + LogCombat("Crip damage [{}]", hit.damage_done); + + entity_list.FilteredMessageCloseString( + this, /* Sender */ + false, /* Skip Sender */ + RuleI(Range, CriticalDamage), + Chat::MeleeCrit, /* Type: 301 */ + FilterMeleeCrits, /* FilterType: 12 */ + CRIPPLING_BLOW, /* MessageFormat: %1 lands a Crippling Blow!(%2) */ + 0, + GetCleanName(), /* Message1 */ + itoa(hit.damage_done + hit.min_damage) /* Message2 */ + ); + + // Crippling blows also have a chance to stun + // Kayen: Crippling Blow would cause a chance to interrupt for npcs < 55, with a + // staggers message. + if (defender->GetLevel() <= 55 && !defender->GetSpecialAbility(SpecialAbility::StunImmunity)) { + entity_list.MessageCloseString( + defender, + true, + RuleI(Range, Emote), + Chat::Emote, + STAGGERS, + GetName() + ); + defender->Stun(RuleI(Combat, StunDuration)); + } + return; + } + + /* Normal Critical hit message */ + entity_list.FilteredMessageCloseString( this, /* Sender */ false, /* Skip Sender */ RuleI(Range, CriticalDamage), Chat::MeleeCrit, /* Type: 301 */ FilterMeleeCrits, /* FilterType: 12 */ - CRIPPLING_BLOW, /* MessageFormat: %1 lands a Crippling Blow!(%2) */ + CRITICAL_HIT, /* MessageFormat: %1 scores a critical hit! (%2) */ 0, GetCleanName(), /* Message1 */ - itoa(hit.damage_done) /* Message2 */ - ); - - // Crippling blows also have a chance to stun - // Kayen: Crippling Blow would cause a chance to interrupt for npcs < 55, with a - // staggers message. - if (defender->GetLevel() <= RuleI(Combat, MaximumLevelStunsCripplingBlow) && !defender->GetSpecialAbility(SpecialAbility::StunImmunity)) { - entity_list.MessageCloseString( - defender, - true, - RuleI(Range, Emote), - Chat::Emote, - STAGGERS, - GetName() + itoa(hit.damage_done + hit.min_damage) /* Message2 */ ); - defender->Stun(RuleI(Combat, StunDuration)); } - return; } - - /* Normal Critical hit message */ - entity_list.FilteredMessageCloseString( - this, /* Sender */ - false, /* Skip Sender */ - RuleI(Range, CriticalDamage), - Chat::MeleeCrit, /* Type: 301 */ - FilterMeleeCrits, /* FilterType: 12 */ - CRITICAL_HIT, /* MessageFormat: %1 scores a critical hit! (%2) */ - 0, - GetCleanName(), /* Message1 */ - itoa(hit.damage_done) /* Message2 */ - ); } bool Mob::TryFinishingBlow(Mob *defender, int64 &damage) diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 7f1e7cd71..b9376da29 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -1439,8 +1439,8 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) case SE_SlayUndead: { if (newbon->SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] < base_value) { - newbon->SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD] = base_value; // Rate - newbon->SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] = limit_value; // Damage Modifier + newbon->SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] = base_value; // Rate + newbon->SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD] = limit_value; // Damage Modifier } break; } @@ -3589,8 +3589,8 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne case SE_SlayUndead: { if (new_bonus->SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] < effect_value) { - new_bonus->SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD] = effect_value; // Rate - new_bonus->SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] = limit_value; // Damage Modifier + new_bonus->SlayUndead[SBIndex::SLAYUNDEAD_RATE_MOD] = limit_value; // Rate + new_bonus->SlayUndead[SBIndex::SLAYUNDEAD_DMG_MOD] = effect_value; // Damage Modifier } break; } diff --git a/zone/common.h b/zone/common.h index 68db779f5..98d0eda6d 100644 --- a/zone/common.h +++ b/zone/common.h @@ -633,8 +633,8 @@ namespace SBIndex { constexpr uint16 SKILLATK_PROC_SPELL_ID = 0; // SPA 288 constexpr uint16 SKILLATK_PROC_CHANCE = 1; // SPA 288 constexpr uint16 SKILLATK_PROC_SKILL = 2; // SPA 288 - constexpr uint16 SLAYUNDEAD_RATE_MOD = 0; // SPA 219 - constexpr uint16 SLAYUNDEAD_DMG_MOD = 1; // SPA 219 + constexpr uint16 SLAYUNDEAD_DMG_MOD = 0; // SPA 219 + constexpr uint16 SLAYUNDEAD_RATE_MOD = 1; // SPA 219 constexpr uint16 DOUBLE_RIPOSTE_CHANCE = 0; // SPA 223 constexpr uint16 DOUBLE_RIPOSTE_SKILL_ATK_CHANCE = 1; // SPA 223 constexpr uint16 DOUBLE_RIPOSTE_SKILL = 2; // SPA 223 diff --git a/zone/mob.h b/zone/mob.h index 54fe3f3ef..86b8db5d5 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -234,13 +234,7 @@ public: int compute_defense(); int GetTotalDefense(); // compute_defense + spell bonuses bool CheckHitChance(Mob* attacker, DamageHitInfo &hit); - bool RollMeleeCritCheck(Mob *defender, EQ::skills::SkillType skill); - inline bool CanUndeadSlay() { return static_cast(GetUndeadSlayRate());} - inline bool IsUndeadForSlay() { return (GetBodyType() == BodyType::Undead || GetBodyType() == BodyType::SummonedUndead || GetBodyType() == BodyType::Vampire); } - int GetUndeadSlayRate(); - void DoUndeadSlay(DamageHitInfo &hit, int crit_mod); void TryCriticalHit(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions *opts = nullptr); - bool TryUndeadSlay(Mob *defender, DamageHitInfo &hit, ExtraAttackOptions *opts = nullptr); void TryPetCriticalHit(Mob *defender, DamageHitInfo &hit); virtual bool TryFinishingBlow(Mob *defender, int64 &damage); int TryHeadShot(Mob* defender, EQ::skills::SkillType skillInUse);