[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)
This commit is contained in:
Fryguy 2024-07-07 00:53:29 -04:00 committed by GitHub
parent a85f4fb703
commit 95cbadade5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 181 deletions

View File

@ -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")

View File

@ -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<int>(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<float>(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<int>(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)

View File

@ -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;
}

View File

@ -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

View File

@ -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<bool>(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);