diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs index 0ef293abd..4ca739970 100644 --- a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs +++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs @@ -350,6 +350,80 @@ public class ThrowingTests } } + // DefMaxRange STR scaling (uses SoulGlaive: StrReq 60, Min 8, Max 11) + + [Fact] + public void DefMaxRange_AtStrReq_EqualsMinThrowRange() + { + var weapon = new SoulGlaive(); + var attacker = CreateMobile(Map.Felucca, new Point3D(5800, 500, 0)); + try + { + attacker.RawStr = 60; // == AosStrengthReq + attacker.AddItem(weapon); + Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8 + } + finally + { + weapon.Delete(); + attacker.Delete(); + } + } + + [Fact] + public void DefMaxRange_At140Str_EqualsMaxThrowRange() + { + var weapon = new SoulGlaive(); + var attacker = CreateMobile(Map.Felucca, new Point3D(5810, 500, 0)); + try + { + attacker.RawStr = 140; + attacker.AddItem(weapon); + Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11 + } + finally + { + weapon.Delete(); + attacker.Delete(); + } + } + + [Fact] + public void DefMaxRange_AboveMaxStr_IsCappedAtMaxThrowRange() + { + var weapon = new SoulGlaive(); + var attacker = CreateMobile(Map.Felucca, new Point3D(5820, 500, 0)); + try + { + attacker.RawStr = 200; // uncapped formula would give 13 + attacker.AddItem(weapon); + Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11, not 13 + } + finally + { + weapon.Delete(); + attacker.Delete(); + } + } + + [Fact] + public void DefMaxRange_BelowStrReq_FlooredAtMinThrowRange() + { + var weapon = new SoulGlaive(); + var attacker = CreateMobile(Map.Felucca, new Point3D(5830, 500, 0)); + try + { + attacker.RawStr = 10; // below StrReq 60 + attacker.AddItem(weapon); + Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8, not 6 + } + finally + { + weapon.Delete(); + attacker.Delete(); + } + } + private static PlayerMobile CreateMobile(Map map, Point3D location) { var mobile = new PlayerMobile(World.NewMobile); diff --git a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs index d881bbdc9..2b765a740 100644 --- a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs +++ b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs @@ -14,17 +14,25 @@ public abstract partial class BaseThrown : BaseRanged public virtual int MaxThrowRange => MinThrowRange + 3; - // Dynamic max range scaled by attacker Strength. + // Dynamic max range scaled by attacker Strength, clamped to the weapon's throw band. // At StrReq the effective range equals MinThrowRange; at 140 Str it reaches MaxThrowRange. public override int DefMaxRange { get { - var baseRange = MaxThrowRange; + if (Parent is not Mobile attacker) + { + return MaxThrowRange; + } - return Parent is Mobile attacker - ? baseRange - 3 + (attacker.Str - AosStrengthReq) / ((140 - AosStrengthReq) / 3) - : baseRange; + var divisor = (140 - AosStrengthReq) / 3; + if (divisor <= 0) + { + return MaxThrowRange; + } + + var scaled = MaxThrowRange - 3 + (attacker.Str - AosStrengthReq) / divisor; + return Math.Clamp(scaled, MinThrowRange, MaxThrowRange); } }