using System;
using Server;
using Server.Items;
using Server.Mobiles;
using Server.Tests;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class ThrowingTests
{
// Exposes protected ModifyHitChance so the math can be verified directly.
private class TestThrown : BaseThrown
{
public TestThrown() : base(0x8FF) { }
public override int MinThrowRange => 4;
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
/// At optimal range with no shield the chance should not change.
[Fact]
public void ModifyHitChance_OptimalRange_NoShield_NoChange()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5000, 500, 0));
var defender = CreateMobile(map, new Point3D(5005, 500, 0)); // distance 5, within range 4..7
var weapon = new TestThrown();
try
{
attacker.Skills.Throwing.BaseFixedPoint = 0;
attacker.RawDex = 10;
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.8, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
///
/// At distance 1 with no throwing skill and minimum dex, close to the full -12% applies.
/// Elf race avoids the Human Jack-of-All-Trades 20.0 skill floor.
/// RawDex clamps to 1 minimum, so mitigation = (0+1)/20 = 0.05 -> penalty = 0.1195.
///
[Fact]
public void ModifyHitChance_CloseQuarters_MinimumSkill_AppliesNearFullPenalty()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5100, 500, 0));
var defender = CreateMobile(map, new Point3D(5101, 500, 0)); // distance 1
var weapon = new TestThrown();
try
{
attacker.Race = Race.Elf; // humans have Jack-of-All-Trades (+20.0 floor on all skills)
attacker.Skills.Throwing.BaseFixedPoint = 0;
attacker.RawDex = 1; // minimum
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.6805, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
/// At distance 0 with 120 skill and 120 dex, (120+120)/20 = 12 caps mitigation, no penalty.
[Fact]
public void ModifyHitChance_CloseQuarters_MaxSkillAndDex_NoChange()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5200, 500, 0));
var defender = CreateMobile(map, new Point3D(5200, 500, 0)); // same tile, distance 0
var weapon = new TestThrown();
try
{
attacker.Skills.Throwing.BaseFixedPoint = 1200; // 120.0
attacker.RawDex = 120;
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.8, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
/// Distance 2 is above melee range but below MinThrowRange of 4, so the flat -12% applies.
[Fact]
public void ModifyHitChance_BelowMinRange_AppliesFlatPenalty()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5300, 500, 0));
var defender = CreateMobile(map, new Point3D(5302, 500, 0)); // distance 2
var weapon = new TestThrown();
try
{
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.68, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
///
/// Shield at 100 Parry: 1200/100 = 12% penalty, chance * 0.88.
/// Layer must be set explicitly since tile data is not loaded in tests.
///
[Fact]
public void ModifyHitChance_Shield_100Parry_AppliesCorrectPenalty()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5400, 500, 0));
var defender = CreateMobile(map, new Point3D(5405, 500, 0)); // distance 5, optimal
var weapon = new TestThrown();
var shield = new Buckler { Layer = Layer.TwoHanded }; // tile data not loaded in tests
try
{
attacker.Skills.Parry.BaseFixedPoint = 1000; // 100.0
attacker.AddItem(shield);
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.704, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
/// Shield at 120 Parry: 1200/120 = 10% penalty, chance * 0.90.
[Fact]
public void ModifyHitChance_Shield_120Parry_AppliesCorrectPenalty()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5500, 500, 0));
var defender = CreateMobile(map, new Point3D(5505, 500, 0)); // distance 5, optimal
var weapon = new TestThrown();
var shield = new Buckler { Layer = Layer.TwoHanded };
try
{
attacker.Skills.Parry.BaseFixedPoint = 1200; // 120.0
attacker.AddItem(shield);
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.72, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
///
/// Shield at 0 Parry hits the 90% cap, chance * 0.10.
/// Elf race prevents JOAT from raising Parry to 20.0.
///
[Fact]
public void ModifyHitChance_Shield_ZeroParry_CapsAt90Percent()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5600, 500, 0));
var defender = CreateMobile(map, new Point3D(5605, 500, 0)); // distance 5, optimal
var weapon = new TestThrown();
var shield = new Buckler { Layer = Layer.TwoHanded };
try
{
attacker.Race = Race.Elf; // humans have Jack-of-All-Trades (+20.0 floor on all skills)
attacker.Skills.Parry.BaseFixedPoint = 0;
attacker.AddItem(shield);
var result = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.08, result, 10);
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
// 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();
}
}
/// At the outermost admissible tile (dist == MaxRange) a throw loses 47% damage.
[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();
}
}
/// Inside max range, damage is unchanged.
[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();
}
}
/// Close-quarters mitigation uses RawDex, so a Dex debuff does not change the penalty.
[Fact]
public void ModifyHitChance_CloseQuarters_UsesRawDex_IgnoresStatMods()
{
var map = Map.Felucca;
var attacker = CreateMobile(map, new Point3D(5940, 500, 0));
var defender = CreateMobile(map, new Point3D(5941, 500, 0)); // distance 1
var weapon = new TestThrown();
try
{
attacker.Race = Race.Elf; // avoid Human JOAT skill floor
attacker.Skills.Throwing.BaseFixedPoint = 0;
attacker.RawDex = 40;
// RawDex-based mitigation = (0 + 40)/20 = 2.0 -> penalty = (12 - 2)/100 = 0.10
var baseline = weapon.TestModifyHitChance(attacker, defender, 0.8); // 0.70
attacker.AddStatMod(new StatMod(StatType.Dex, "curse", -30, TimeSpan.Zero)); // effective Dex 10
var withMod = weapon.TestModifyHitChance(attacker, defender, 0.8);
Assert.Equal(0.70, baseline, 10);
Assert.Equal(baseline, withMod, 10); // unchanged because RawDex is used
}
finally
{
attacker.Delete();
defender.Delete();
weapon.Delete();
}
}
private static PlayerMobile CreateMobile(Map map, Point3D location)
{
var mobile = new PlayerMobile(World.NewMobile);
mobile.DefaultMobileInit();
mobile.MoveToWorld(location, map);
return mobile;
}
}