From 942c7935b23526d3c8d56bb972a67c14f6f75aab Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:31:34 -0700 Subject: [PATCH] fix(throwing): clamp DefMaxRange to throw band and guard divisor Implement Math.Clamp to ensure DefMaxRange stays within [MinThrowRange, MaxThrowRange], and guard against division by zero if the divisor becomes zero or negative. Adds four tests: AtStrReq, At140Str, AboveMaxStr (capping), BelowStrReq (flooring). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Items/Weapons/Throwing/ThrowingTests.cs | 74 +++++++++++++++++++ .../Items/Weapons/Throwing/BaseThrown.cs | 18 +++-- 2 files changed, 87 insertions(+), 5 deletions(-) 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); } }