Skip to content

Commit

Permalink
Replace usage of con level in combat formulas with level difference
Browse files Browse the repository at this point in the history
* Con level doesn't appear to be used directly on live. This change mostly helps low level players, and when fighting low orange / low red / low purple targets. It prevents big jumps in effectiveness, miss rate, and resist rate from one target to another when the level difference is only 1. It also prevents asymmetric scaling, since for example a level 51 NPC is orange to a level 50 player, but that player is still yellow to that NPC.
* Critical shot's effectiveness is now lowered by 0.075 per level difference instead of 0.3 per con difference. Cap is unchanged.
* Miss rate, when a NPC is involved, is now modified by a flat 1.33% per level difference instead of a flat 5% per con difference.
* Miss/resist rate for new archery is now modified by a flat 1.33% per level difference instead of a flat 5% per con difference. New archery is very outdated and needs to be checked.
* Miss rate is no longer reduced by a flat 0.1% per spec level (not accurate for any patch).
* Base miss rate of NPCs was reduced from 25% to 18%.
* Miss rate returned by `GetMissChance` now has decimals.
* Base resist rate for spells is now 12.5% instead of 12% (value from Uthgard, based on live tests).
* Resist rate for spells, when a NPC is involved, no longer scales off con difference. It isn’t replaced by anything since the level difference between the spell level and the target level is already used, and an extra penalty or bonus based on caster level is unconfirmed to be accurate. This increases damage against high level targets due to `AdjustDamageForHitChance` only triggering when the resist rate is at least 46%, which requires a level difference (between spell and target) of at least 67.
* Resist rates returned by `CalculateToHitChance` and `CalculateSpellResistChance` now have decimals.
* Triple Wield damage no longer scales off con difference.
* Interrupt chance is now reduced by a flat 3% per level difference instead of a flat 15% per con difference. Still 100% against a target of the same level.
* Made `CharmSpellHandler.ApplyEffectOnTarget` call `CalculateToHitChance` instead of using a base miss rate of 15% for non-pulsing spells.
* Removed `PVE_SPELL_CONHITPERCENT` property.
* Removed usage of `GetConLevel` for interrupt chance in `Archery.CasterIsAttacked`. New archery is very outdated and needs to be checked.
* Removed usage of `GetConLevel` for block chance calculation in `ArrowOnTargetAction.Tick`. New archery is very outdated and needs to be checked.
* Removed obsolete unit tests.
  • Loading branch information
bm01 committed Aug 7, 2024
1 parent 476bfad commit e3ee553
Show file tree
Hide file tree
Showing 49 changed files with 160 additions and 484 deletions.
14 changes: 4 additions & 10 deletions GameServer/ECS-Components/Actions/AttackAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,9 @@ protected virtual bool PrepareRangedAttack()
{
case eRangedAttackType.Critical:
{
double tmpEffectiveness = 2 - 0.3 * _owner.GetConLevel(_target);

if (tmpEffectiveness > 2)
_effectiveness *= 2;
else if (tmpEffectiveness < 1.1)
_effectiveness *= 1.1;
else
_effectiveness *= tmpEffectiveness;

// Reduced effectiveness against higher level targets.
double levelModifier = 2 + (_owner.EffectiveLevel - _target.EffectiveLevel) * 0.075;
_effectiveness *= Math.Clamp(levelModifier, 1.1, 2.0);
break;
}

Expand Down Expand Up @@ -314,7 +308,7 @@ protected virtual bool PrepareRangedAttack()
if (elapsedTime < rapidFireMaxDuration)
{
_effectiveness *= 0.25 + elapsedTime * 0.5 / rapidFireMaxDuration;
_attackInteval = (int)(_attackInteval * _effectiveness);
_attackInteval = (int) (_attackInteval * _effectiveness);
}

break;
Expand Down
52 changes: 26 additions & 26 deletions GameServer/ECS-Components/AttackComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1290,12 +1290,12 @@ public AttackData LivingMakeAttack(WeaponAction action, GameObject target, DbInv
static void PrintDetailedCombatLog(GamePlayer player, double armorFactor, double absorb, double armorMod, double baseWeaponSkill, double specModifier, double weaponSkill, double damageMod, double baseDamageCap, double styleDamageCap)
{
StringBuilder stringBuilder = new();
stringBuilder.Append($"BaseWS: {baseWeaponSkill:0.00} | SpecMod: {specModifier:0.00} | WS: {weaponSkill:0.00}\n");
stringBuilder.Append($"AF: {armorFactor:0.00} | ABS: {absorb * 100:0.00}% | AF/ABS: {armorMod:0.00}\n");
stringBuilder.Append($"DamageMod: {damageMod:0.00} | BaseDamageCap: {baseDamageCap:0.00}");
stringBuilder.Append($"BaseWS: {baseWeaponSkill:0.##} | SpecMod: {specModifier:0.##} | WS: {weaponSkill:0.##}\n");
stringBuilder.Append($"AF: {armorFactor:0.##} | ABS: {absorb * 100:0.##}% | AF/ABS: {armorMod:0.##}\n");
stringBuilder.Append($"DamageMod: {damageMod:0.##} | BaseDamageCap: {baseDamageCap:0.##}");

if (styleDamageCap > 0)
stringBuilder.Append($" | StyleDamageCap: {styleDamageCap:0.00}");
stringBuilder.Append($" | StyleDamageCap: {styleDamageCap:0.##}");

player.Out.SendMessage(stringBuilder.ToString(), eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);
}
Expand Down Expand Up @@ -1667,7 +1667,7 @@ public double CalculateSpecModifier(GameLiving target, int spec)
// Also prevents negative values.
spec = Math.Max(owner.Level < 5 ? 2 : 1, spec);
lowerLimit = Math.Min(0.75 * (spec - 1) / (target.Level + 1) + 0.25, 1.0);
upperLimit = Math.Min(Math.Max(1.25 + (3.0 * (spec - 1) / (target.Level + 1) - 2) * 0.25, 1.25), 1.50);
upperLimit = Math.Min(Math.Max(1.25 + (3.0 * (spec - 1) / (target.Level + 1) - 2) * 0.25, 1.25), 1.5);
}
else
{
Expand Down Expand Up @@ -2119,7 +2119,7 @@ public virtual eAttackResult CalculateEnemyAttackResult(WeaponAction action, Att
// return result;

// Miss chance.
int missChance = GetMissChance(action, ad, lastAttackData, attackerWeapon);
double missChance = GetMissChance(action, ad, lastAttackData, attackerWeapon);

// Check for dirty trick fumbles before misses.
DirtyTricksDetrimentalECSGameEffect dt = (DirtyTricksDetrimentalECSGameEffect)EffectListService.GetAbilityEffectOnTarget(ad.Attacker, eEffect.DirtyTricksDetrimental);
Expand All @@ -2140,14 +2140,14 @@ public virtual eAttackResult CalculateEnemyAttackResult(WeaponAction action, Att

if (ad.Attacker is GamePlayer misser && misser.UseDetailedCombatLog)
{
misser.Out.SendMessage($"miss rate on target: {missChance}% rand: {missRoll * 100:0.##}", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);
misser.Out.SendMessage($"miss rate on target: {missChance:0.##}% rand: {missRoll * 100:0.##}", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);

if (ad.AttackType != AttackData.eAttackType.Ranged)
misser.Out.SendMessage($"Your chance to fumble: {100 * ad.Attacker.ChanceToFumble:0.##}% rand: {100 * missRoll:0.##}", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);
}

if (ad.Target is GamePlayer missee && missee.UseDetailedCombatLog)
missee.Out.SendMessage($"chance to be missed: {missChance}% rand: {missRoll * 100:0.##}", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);
missee.Out.SendMessage($"chance to be missed: {missChance:0.##}% rand: {missRoll * 100:0.##}", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);

if (missChance > missRoll * 100)
{
Expand Down Expand Up @@ -2314,7 +2314,7 @@ and not eAttackResult.Blocked
case eAttackResult.Missed:
string message;
if (ad.MissChance > 0)
message = LanguageMgr.GetTranslation(p.Client.Account.Language, "GamePlayer.Attack.Miss") + $" ({ad.MissChance}%)";
message = LanguageMgr.GetTranslation(p.Client.Account.Language, "GamePlayer.Attack.Miss") + $" ({ad.MissChance:0.##}%)";
else
message = LanguageMgr.GetTranslation(p.Client.Account.Language, "GamePlayer.Attack.StrafMiss");
p.Out.SendMessage(message, eChatType.CT_YouHit, eChatLoc.CL_SystemWindow);
Expand Down Expand Up @@ -2439,7 +2439,7 @@ and not eAttackResult.Blocked
case eAttackResult.Missed:
string message;
if (ad.MissChance > 0)
message = LanguageMgr.GetTranslation(p.Client.Account.Language, "GamePlayer.Attack.Miss") + $" ({ad.MissChance}%)";
message = LanguageMgr.GetTranslation(p.Client.Account.Language, "GamePlayer.Attack.Miss") + $" ({ad.MissChance:0.##}%)";
else
message = LanguageMgr.GetTranslation(p.Client.Account.Language, "GamePlayer.Attack.StrafMiss");
p.Out.SendMessage(message, eChatType.CT_YouHit, eChatLoc.CL_SystemWindow);
Expand Down Expand Up @@ -2574,28 +2574,29 @@ public int CalculateMeleeCriticalDamage(AttackData ad, WeaponAction action, DbIn
}
}

public int GetMissChance(WeaponAction action, AttackData ad, AttackData lastAD, DbInventoryItem weapon)
public double GetMissChance(WeaponAction action, AttackData ad, AttackData lastAD, DbInventoryItem weapon)
{
// No miss if the target is sitting or for Volley attacks.
if ((owner is GamePlayer player && player.IsSitting) || action.RangedAttackType == eRangedAttackType.Volley)
if ((owner is GamePlayer player && player.IsSitting) || action.RangedAttackType is eRangedAttackType.Volley)
return 0;

// In 1.117C, every weapon was given the intrinsic 5% flat bonus special weapons (such as artifacts) had, lowering the base miss rate to 13%.
int missChance = 18;
double missChance = 18;
missChance -= ad.Attacker.GetModified(eProperty.ToHitBonus);

if (owner is not GamePlayer || ad.Attacker is not GamePlayer)
{
missChance += 5 * ad.Attacker.GetConLevel(owner);
// 1.33 per level difference.
missChance -= (ad.Attacker.EffectiveLevel - owner.EffectiveLevel) * (1 + 1 / 3.0);
missChance -= Math.Max(0, Attackers.Count - 1) * Properties.MISSRATE_REDUCTION_PER_ATTACKERS;
}

// Weapon and armor bonuses.
int armorBonus = 0;

if (ad.Target is GamePlayer p)
if (ad.Target is GamePlayer playerTarget)
{
ad.ArmorHitLocation = ((GamePlayer) ad.Target).CalculateArmorHitLocation(ad);
ad.ArmorHitLocation = playerTarget.CalculateArmorHitLocation(ad);

if (ad.Target.Inventory != null)
{
Expand All @@ -2605,7 +2606,7 @@ public int GetMissChance(WeaponAction action, AttackData ad, AttackData lastAD,
armorBonus = armor.Bonus;
}

int bonusCap = GetBonusCapForLevel(p.Level);
int bonusCap = GetBonusCapForLevel(playerTarget.Level);

if (armorBonus > bonusCap)
armorBonus = bonusCap;
Expand All @@ -2631,10 +2632,10 @@ public int GetMissChance(WeaponAction action, AttackData ad, AttackData lastAD,
if (ad.Style != null)
missChance -= ad.Style.BonusToHit;

if (lastAD != null && lastAD.AttackResult == eAttackResult.HitStyle && lastAD.Style != null)
if (lastAD != null && lastAD.AttackResult is eAttackResult.HitStyle && lastAD.Style != null)
missChance += lastAD.Style.BonusToDefense;

if (action.ActiveWeaponSlot == eActiveWeaponSlot.Distance)
if (action.ActiveWeaponSlot is eActiveWeaponSlot.Distance)
{
DbInventoryItem ammo = ad.Attacker.rangeAttackComponent.Ammo;

Expand All @@ -2644,16 +2645,15 @@ public int GetMissChance(WeaponAction action, AttackData ad, AttackData lastAD,
{
// http://rothwellhome.org/guides/archery.htm
case 0:
missChance += (int) Math.Round(missChance * 0.15);
missChance += missChance * 0.15;
break; // Rough
//case 1:
// missrate -= 0;
// break;
case 2:
missChance -= (int) Math.Round(missChance * 0.15);
break; // doesn't exist (?)
missChance -= missChance * 0.15;
break; // Doesn't exist (?)
case 3:
missChance -= (int) Math.Round(missChance * 0.25);
missChance -= missChance * 0.25;
break; // Footed
}
}
Expand Down Expand Up @@ -2728,7 +2728,7 @@ public int CalculateLeftHandSwingCount()
double offhandChance = 25 + (specLevel - 1) * 68 * 0.01 + bonus;

if (playerOwner != null && playerOwner.UseDetailedCombatLog)
playerOwner.Out.SendMessage($"OH swing%: {offhandChance:0.00} ({bonus}% from RAs) \n", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);
playerOwner.Out.SendMessage($"OH swing%: {offhandChance:0.##} ({bonus}% from RAs) \n", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);

return random < offhandChance ? 1 : 0;
}
Expand All @@ -2747,7 +2747,7 @@ public int CalculateLeftHandSwingCount()
double quadHitChance = tripleHitChance + specLevel * 0.0625 + bonus * 0.25; // specLevel >> 4

if (playerOwner != null && playerOwner.UseDetailedCombatLog)
playerOwner.Out.SendMessage( $"Chance for 2 hits: {doubleHitChance:0.00}% | 3 hits: { (specLevel > 25 ? tripleHitChance-doubleHitChance : 0):0.00}% | 4 hits: {(specLevel > 40 ? quadHitChance-tripleHitChance : 0):0.00}% \n", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);
playerOwner.Out.SendMessage( $"Chance for 2 hits: {doubleHitChance:0.##}% | 3 hits: { (specLevel > 25 ? tripleHitChance-doubleHitChance : 0):0.##}% | 4 hits: {(specLevel > 40 ? quadHitChance-tripleHitChance : 0):0.##}% \n", eChatType.CT_DamageAdd, eChatLoc.CL_SystemWindow);

if (random < doubleHitChance)
return 1;
Expand Down
9 changes: 1 addition & 8 deletions GameServer/ECS-Effects/TripleWieldECSEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,9 @@ public void EventHandler(AttackData attackData)
//if (attackData.IsOffHand) return; // only react to main hand
if (attackData.Weapon == null) return; // no weapon attack

int modifier = 100;
//double dpsCap = (1.2 + 0.3 * attacker.Level) * 0.7;
//double dps = Math.Min(atkArgs.AttackData.Weapon.DPS_AF/10.0, dpsCap);
double baseDamage = attackData.Weapon.DPS_AF * attackData.Interval * 0.001;

modifier += 25 * attackData.Target.GetConLevel(attackData.Attacker);
modifier = Math.Min(300, modifier);
modifier = Math.Max(75, modifier);

double damage = baseDamage * modifier * 0.001; // attack speed is 10 times higher (2.5spd=25)
double damage = attackData.Weapon.DPS_AF * attackData.Interval * 0.001;
double damageResisted = damage * target.GetResist(eDamageType.Body) * -0.01;

AttackData ad = new AttackData();
Expand Down
11 changes: 2 additions & 9 deletions GameServer/effects/TripleWieldEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,11 @@ protected void EventHandler(DOLEvent e, object sender, EventArgs arguments)
if (atkArgs.AttackData.IsOffHand) return; // only react to main hand
if (atkArgs.AttackData.Weapon == null) return; // no weapon attack

int modifier = 100;
//double dpsCap = (1.2 + 0.3 * attacker.Level) * 0.7;
//double dps = Math.Min(atkArgs.AttackData.Weapon.DPS_AF/10.0, dpsCap);
double baseDamage = atkArgs.AttackData.Weapon.DPS_AF * atkArgs.AttackData.Interval * 0.001;

modifier += 25 * atkArgs.AttackData.Target.GetConLevel(atkArgs.AttackData.Attacker);
modifier = Math.Min(300, modifier);
modifier = Math.Max(75, modifier);

double damage = baseDamage * modifier * 0.001; // attack speed is 10 times higher (2.5spd=25)
double damage = atkArgs.AttackData.Weapon.DPS_AF * atkArgs.AttackData.Interval * 0.001;
double damageResisted = damage * target.GetResist(eDamageType.Body) * -0.01;

AttackData ad = new AttackData();
ad.Attacker = attacker;
ad.Target = target;
Expand Down
3 changes: 2 additions & 1 deletion GameServer/gameobjects/GameLiving.cs
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,8 @@ public virtual void StartInterruptTimer(int duration, eAttackType attackType, Ga
return;
}

if (!Util.Chance(100 + GetConLevel(attacker) * 15))
// 3% reduced interrupt chance per level difference.
if (!Util.Chance(100 + (attacker.EffectiveLevel - EffectiveLevel) * 3))
return;

// Don't replace the current interrupt with a shorter one.
Expand Down
2 changes: 1 addition & 1 deletion GameServer/gameobjects/GamePlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6077,7 +6077,7 @@ public override void OnAttackedByEnemy(AttackData ad)
if (ad.AttackType == AttackData.eAttackType.Spell)
break;
if (ad.Attacker is GameNPC)
Out.SendMessage(LanguageMgr.GetTranslation(Client.Account.Language, "GamePlayer.Attack.Missed", ad.Attacker.GetName(0, true, Client.Account.Language, (ad.Attacker as GameNPC))) + " (" + Math.Min(ad.MissChance, 100).ToString("0") + "%)", eChatType.CT_Missed, eChatLoc.CL_SystemWindow);
Out.SendMessage(LanguageMgr.GetTranslation(Client.Account.Language, "GamePlayer.Attack.Missed", ad.Attacker.GetName(0, true, Client.Account.Language, (ad.Attacker as GameNPC))) + " (" + Math.Min(ad.MissChance, 100).ToString("0.0") + "%)", eChatType.CT_Missed, eChatLoc.CL_SystemWindow);
else
Out.SendMessage(LanguageMgr.GetTranslation(Client.Account.Language, "GamePlayer.Attack.Missed", ad.Attacker.GetName(0, true)), eChatType.CT_Missed, eChatLoc.CL_SystemWindow);
break;
Expand Down
6 changes: 0 additions & 6 deletions GameServer/serverproperty/ServerProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -828,12 +828,6 @@ public static void InitProperties()
[ServerProperty("rates", "pve_spell_damage", "The PvE Spell Damage Modifier - Edit this to change the amount of spell damage done when fighting mobs e.g 1.5 is 50% more damage 2.0 is twice the damage (100%) 0.5 is half the damage (50%)", 1.0)]
public static double PVE_SPELL_DAMAGE = 1.0;

/// <summary>
/// The percent per con difference (-1 = blue, 0 = yellow, 1 = OJ, 2 = red ...) subtracted to hitchance for spells in PVE. 0 is none, 5 is 5% per con, etc. Default is 10%
/// </summary>
[ServerProperty("rates", "pve_spell_conhitpercent", "The percent per con (1 = OJ, 2 = red ...) subtracted to hitchance for spells in PVE Must be >= 0. 0 is none, 5 is 5% per level, etc. Default is 10%", (uint)10)]
public static uint PVE_SPELL_CONHITPERCENT;

/// <summary>
/// The damage players do against players with melee
/// </summary>
Expand Down
22 changes: 7 additions & 15 deletions GameServer/spells/Archery/Archery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,30 +68,25 @@ public override void SendSpellMessages()
MessageToCaster("You prepare a " + Spell.Name, eChatType.CT_YouHit);
}


public override int CalculateToHitChance(GameLiving target)
public override double CalculateToHitChance(GameLiving target)
{
int bonustohit = Caster.GetModified(eProperty.ToHitBonus);

// miss rate is 0 on same level opponent
int hitchance = 100 + bonustohit;
double hitChance = 100 + Caster.GetModified(eProperty.ToHitBonus);

if (Caster is not GamePlayer || target is not GamePlayer)
{
hitchance -= (int)(Caster.GetConLevel(target) * ServerProperties.Properties.PVE_SPELL_CONHITPERCENT);
hitchance += Math.Max(0, target.attackComponent.Attackers.Count - 1) * ServerProperties.Properties.MISSRATE_REDUCTION_PER_ATTACKERS;
// 1.33 per level difference.
hitChance += (Caster.EffectiveLevel - target.EffectiveLevel) * (1 + 1 / 3.0);
hitChance += Math.Max(0, target.attackComponent.Attackers.Count - 1) * ServerProperties.Properties.MISSRATE_REDUCTION_PER_ATTACKERS;
}

return hitchance;
return hitChance;
}

/// <summary>
/// Adjust damage based on chance to hit.
/// </summary>
/// <param name="damage"></param>
/// <param name="hitChance"></param>
/// <returns></returns>
public override int AdjustDamageForHitChance(int damage, int hitChance)
public override int AdjustDamageForHitChance(int damage, double hitChance)
{
int adjustedDamage = damage;

Expand All @@ -103,7 +98,6 @@ public override int AdjustDamageForHitChance(int damage, int hitChance)
return adjustedDamage;
}


/// <summary>
/// Level mod for effect between target and caster if there is any
/// </summary>
Expand Down Expand Up @@ -330,9 +324,7 @@ public override bool CasterIsAttacked(GameLiving attacker)

if (IsInCastingPhase)
{
int mod = Caster.GetConLevel(attacker);
double chance = 65;
chance += mod * 10;
chance = Math.Max(1, chance);
chance = Math.Min(99, chance);
if (attacker is GamePlayer) chance = 100;
Expand Down
5 changes: 2 additions & 3 deletions GameServer/spells/Archery/Arrow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ protected override int OnTick(ECSGameTimer timer)
if (target == null || !target.IsAlive || target.ObjectState != GameObject.eObjectState.Active || target.CurrentRegionID != caster.CurrentRegionID)
return 0;

int missrate = 100 - m_handler.CalculateToHitChance(target);
double missrate = 100 - m_handler.CalculateToHitChance(target);
// add defence bonus from last executed style if any
AttackData targetAD = target.attackComponent.attackAction.LastAttackData;
if (targetAD != null
Expand All @@ -151,7 +151,7 @@ protected override int OnTick(ECSGameTimer timer)
return 0;
}

if (Util.Chance(missrate))
if (Util.ChanceDouble(missrate))
{
ad.AttackResult = eAttackResult.Missed;
m_handler.MessageToCaster("You miss!", eChatType.CT_YouHit);
Expand Down Expand Up @@ -183,7 +183,6 @@ protected override int OnTick(ECSGameTimer timer)
double shield = 0.5 * player.GetModifiedSpecLevel(Specs.Shields);
double blockchance = ((player.Dexterity * 2) - 100) / 40.0 + shield + (0 * 3) + 5;
blockchance += 30;
blockchance -= target.GetConLevel(caster) * 5;
if (blockchance >= 100) blockchance = 99;
if (blockchance <= 0) blockchance = 1;

Expand Down
Loading

0 comments on commit e3ee553

Please sign in to comment.