fix(throwing): apply overthrow -47% as post-bonus damage via ModifyDamage hook

Adds a ModifyDamage(attacker, defender, damage) virtual on BaseWeapon,
mirroring the existing ModifyHitChance hook, invoked once in OnHit
immediately after the percentage-bonus pool scale. BaseThrown overrides
it to cut 47% of the final post-bonus damage at the outermost admissible
tile (dist == MaxRange), replacing the old dead ComputeDamage override
(the swing gate already guarantees the attacker is within MaxRange, so
the prior >= comparison never fired).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-07-02 20:39:21 -07:00
parent 9ea79dc634
commit 5c1196cdae
3 changed files with 71 additions and 7 deletions

View file

@ -19,6 +19,9 @@ public class ThrowingTests
public double TestModifyHitChance(Mobile attacker, Mobile defender, double chance) =>
ModifyHitChance(attacker, defender, chance);
public int TestModifyDamage(Mobile attacker, Mobile defender, int damage) =>
ModifyDamage(attacker, defender, damage);
}
// Hit chance modifiers
@ -291,6 +294,62 @@ public class ThrowingTests
}
}
/// <summary>At the outermost admissible tile (dist == MaxRange) a throw loses 47% damage.</summary>
[Fact]
public void ModifyDamage_AtMaxRange_Reduces47Percent()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5900, 500, 0));
var weapon = new TestThrown(); // MinThrowRange 4 -> MaxThrowRange 7
try
{
attacker.RawStr = 140; // MaxRange == MaxThrowRange == 7
attacker.AddItem(weapon);
var defender = CreateMobile(map, new Point3D(5907, 500, 0)); // distance 7 == MaxRange
try
{
Assert.Equal(53, weapon.TestModifyDamage(attacker, defender, 100));
}
finally
{
defender.Delete();
}
}
finally
{
weapon.Delete();
attacker.Delete();
}
}
/// <summary>Inside max range, damage is unchanged.</summary>
[Fact]
public void ModifyDamage_WithinRange_NoChange()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5920, 500, 0));
var weapon = new TestThrown();
try
{
attacker.RawStr = 140; // MaxRange 7
attacker.AddItem(weapon);
var defender = CreateMobile(map, new Point3D(5925, 500, 0)); // distance 5 < 7
try
{
Assert.Equal(100, weapon.TestModifyDamage(attacker, defender, 100));
}
finally
{
defender.Delete();
}
}
finally
{
weapon.Delete();
attacker.Delete();
}
}
private static PlayerMobile CreateMobile(Map map, Point3D location)
{
var mobile = new PlayerMobile(World.NewMobile);

View file

@ -1374,6 +1374,12 @@ public abstract partial class BaseWeapon
/// </summary>
protected virtual double ModifyHitChance(Mobile attacker, Mobile defender, double chance) => chance;
/// <summary>
/// Allows subclasses to modify the final post-bonus damage (e.g. range-based penalties)
/// before defender mitigation is applied. Mirrors <see cref="ModifyHitChance" />.
/// </summary>
protected virtual int ModifyDamage(Mobile attacker, Mobile defender, int damage) => damage;
public virtual TimeSpan GetDelay(Mobile m)
{
double speed = Speed;
@ -1883,6 +1889,7 @@ public abstract partial class BaseWeapon
percentageBonus = Math.Min(percentageBonus, 300);
damage = AOS.Scale(damage, 100 + percentageBonus);
damage = ModifyDamage(attacker, defender, damage);
var defLoc = new WorldLocation(defender);
var bcAtt = attacker as BaseCreature;

View file

@ -86,15 +86,13 @@ public abstract partial class BaseThrown : BaseRanged
return chance;
}
// Overthrow penalty: -47% damage when target is beyond the attacker's current max range.
// MaxRange returns DefMaxRange (STR-scaled), so this reflects the dynamic per-attack value.
public override int ComputeDamage(Mobile attacker, Mobile defender)
// Overthrow: a throw that reaches the edge of its (STR-scaled) range lands with 47% less
// damage, applied on top of all offensive bonuses. MaxRange is the dynamic DefMaxRange.
protected override int ModifyDamage(Mobile attacker, Mobile defender, int damage)
{
var damage = base.ComputeDamage(attacker, defender);
if (!attacker.InRange(defender.Location, MaxRange))
if (!attacker.InRange(defender.Location, MaxRange - 1))
{
damage = (int)(damage * 0.53);
damage = damage * 53 / 100;
}
return damage;