diff --git a/Projects/UOContent.Tests/Tests/Engines/Harvest/FishingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Harvest/FishingTests.cs new file mode 100644 index 000000000..d77cadcae --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Harvest/FishingTests.cs @@ -0,0 +1,108 @@ +using System; +using Server; +using Server.Engines.Harvest; +using Server.Items; +using Server.Mobiles; +using Server.Random; +using Xunit; + +namespace UOContent.Tests; + +[Collection("Sequential UOContent Tests")] +public class FishingTests +{ + private const int Casts = 100_000; + + // On land in Britain, so a cast there never counts as deep water. + private static readonly Point3D _shallow = new(1434, 1699, 0); + + private static readonly Type[] _magicFish = + [typeof(PrizedFish), typeof(WondrousFish), typeof(TrulyRareFish), typeof(PeculiarFish)]; + + private static readonly Type[] _footwear = [typeof(Boots), typeof(Shoes), typeof(Sandals), typeof(ThighBoots)]; + + [Theory] + [InlineData(Expansion.None, 50.0, 0.0, 0.0)] + [InlineData(Expansion.T2A, 0.0, 0.0, 0.75)] + [InlineData(Expansion.T2A, 50.0, 0.0, 0.3877)] + [InlineData(Expansion.T2A, 100.0, 0.025, 0.0247)] + [InlineData(Expansion.UOTD, 80.0, 0.02, 0.1669)] + [InlineData(Expansion.LBR, 0.0, 0.0, 0.75)] + [InlineData(Expansion.LBR, 50.0, 0.025, 0.4519)] + [InlineData(Expansion.AOS, 100.0, 0.05, 0.095)] + [InlineData(Expansion.SA, 50.0, 0.025, 0.4519)] + [InlineData(Expansion.HS, 50.0, 0.0, 0.4635)] + [InlineData(Expansion.HS, 120.0, 0.06, 0.047)] + [InlineData(Expansion.EJ, 120.0, 0.06, 0.047)] + public void MutateType_MagicFishAndFootwear_FollowTheEra( + Expansion expansion, double skill, double magicFishRate, double footwearRate + ) + { + var previousExpansion = Core.Expansion; + var previousGenerator = BuiltInRng.Generator; + var fisher = new PlayerMobile(World.NewMobile); + + try + { + Core.Expansion = expansion; + BuiltInRng.Generator = new System.Random(1); + fisher.DefaultMobileInit(); + fisher.Skills.Fishing.Base = skill; + + Assert.Equal(skill, fisher.Skills.Fishing.Value); + Assert.False(SpecialFishingNet.FullValidation(Map.Felucca, _shallow.X, _shallow.Y)); + + var definition = Fishing.System.Definitions[0]; + var magicFish = 0; + var footwear = 0; + + for (var i = 0; i < Casts; i++) + { + var type = Fishing.System.MutateType(typeof(Fish), fisher, null, definition, Map.Felucca, _shallow, null); + + if (Array.IndexOf(_magicFish, type) >= 0) + { + magicFish++; + } + else if (Array.IndexOf(_footwear, type) >= 0) + { + footwear++; + } + } + + AssertRate(magicFishRate, magicFish); + AssertRate(footwearRate, footwear); + } + finally + { + fisher.Delete(); + BuiltInRng.Generator = previousGenerator; + Core.Expansion = previousExpansion; + } + } + + [Fact] + public void Leviathan_CarriesItsTreasureMapAQuarterOfTheTime() + { + var leviathan = new Leviathan(); + var serpent = new SeaSerpent(); + + try + { + Assert.Equal(0.25, leviathan.TreasureMapChance); + Assert.Equal(TreasureMap.LootChance, serpent.TreasureMapChance); + } + finally + { + leviathan.Delete(); + serpent.Delete(); + } + } + + // Within five standard deviations of the expected rate; a rate of zero must never happen. + private static void AssertRate(double expected, int count) + { + var tolerance = 5 * Math.Sqrt(expected * (1 - expected) / Casts); + Assert.InRange((double)count / Casts, expected - tolerance, expected + tolerance); + } +} diff --git a/Projects/UOContent/Engines/Harvest/Fishing.cs b/Projects/UOContent/Engines/Harvest/Fishing.cs index b45c44b72..d3784ac3d 100644 --- a/Projects/UOContent/Engines/Harvest/Fishing.cs +++ b/Projects/UOContent/Engines/Harvest/Fishing.cs @@ -10,24 +10,29 @@ namespace Server.Engines.Harvest { private static Fishing _system; + private static readonly Type[] _magicFish = + { + typeof(PrizedFish), typeof(WondrousFish), typeof(TrulyRareFish), typeof(PeculiarFish) + }; + + private static readonly Type[] _footwear = { typeof(Boots), typeof(Shoes), typeof(Sandals), typeof(ThighBoots) }; + private static readonly MutateEntry[] _mutateTable = { new(80.0, 80.0, 4080.0, true, Expansion.UOR, typeof(SpecialFishingNet)), new(80.0, 80.0, 4080.0, true, Expansion.UOR, typeof(BigFish)), new(90.0, 80.0, 4080.0, true, Expansion.T2A, typeof(TreasureMap)), new(100.0, 80.0, 4080.0, true, Expansion.T2A, typeof(MessageInABottle)), - new( - 0.0, - 125.0, - -2375.0, - false, - Expansion.T2A, - typeof(PrizedFish), - typeof(WondrousFish), - typeof(TrulyRareFish), - typeof(PeculiarFish) - ), - new(0.0, 105.0, -420.0, false, Expansion.T2A, typeof(Boots), typeof(Shoes), typeof(Sandals), typeof(ThighBoots)), + // T2A-UOTD: needs 80; 2.0% at 80, 2.5% at 100. + // LBR-SA: no minimum; 0% at 0, 5% at 100. + // HS onward: needs 80; 4.0% at 80, 5.0% at 100. + new(80.0, 0.0, 4000.0, false, Expansion.T2A, Expansion.UOTD, _magicFish), + new(0.0, 0.0, 2000.0, false, Expansion.LBR, Expansion.SA, _magicFish), + new(80.0, 0.0, 2000.0, false, Expansion.HS, _magicFish), + new(0.0, 103.5, -34.5, false, Expansion.T2A, Expansion.UOTD, _footwear), + // From LBR the two footwear rows make one curve: 75% at 0 skill, 10% at 100 and 5% at 120. + new(0.0, 100.0, -62.5, false, Expansion.LBR, _footwear), + new(0.0, 140.0, -260.0, false, Expansion.LBR, _footwear), new(0.0, 200.0, -200.0, false, Expansion.None, new Type[] { null }) }; @@ -174,7 +179,7 @@ namespace Server.Engines.Harvest { var entry = _mutateTable[i]; - if (Core.Expansion < entry.m_Expansion) + if (Core.Expansion < entry.m_Expansion || Core.Expansion > entry.m_MaxExpansion) { continue; } @@ -541,6 +546,7 @@ namespace Server.Engines.Harvest { public readonly bool m_DeepWater; public readonly Expansion m_Expansion; + public readonly Expansion? m_MaxExpansion; public readonly double m_MaxSkill; public readonly double m_MinSkill; public readonly double m_ReqSkill; @@ -548,6 +554,13 @@ namespace Server.Engines.Harvest public MutateEntry( double reqSkill, double minSkill, double maxSkill, bool deepWater, Expansion expansion, params Type[] types + ) : this(reqSkill, minSkill, maxSkill, deepWater, expansion, null, types) + { + } + + public MutateEntry( + double reqSkill, double minSkill, double maxSkill, bool deepWater, Expansion expansion, + Expansion? maxExpansion, params Type[] types ) { m_ReqSkill = reqSkill; @@ -555,6 +568,7 @@ namespace Server.Engines.Harvest m_MaxSkill = maxSkill; m_DeepWater = deepWater; m_Expansion = expansion; + m_MaxExpansion = maxExpansion; m_Types = types; } } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index f0210d4f6..bd28ff590 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -1310,6 +1310,8 @@ namespace Server.Mobiles public virtual int TreasureMapLevel => -1; + public virtual double TreasureMapChance => TreasureMap.LootChance; + public virtual bool IgnoreYoungProtection => false; public bool NoKillAwards { get; set; } @@ -3246,7 +3248,7 @@ namespace Server.Mobiles { PackItem(new ParagonChest(Name, treasureLevel)); } - else if ((Map == Map.Felucca || Map == Map.Trammel) && Utility.RandomDouble() < TreasureMap.LootChance) + else if ((Map == Map.Felucca || Map == Map.Trammel) && Utility.RandomDouble() < TreasureMapChance) { PackItem(new TreasureMap(treasureLevel, Map)); } diff --git a/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/Leviathan.cs b/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/Leviathan.cs index e4ffe5ba3..e4adbf05e 100644 --- a/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/Leviathan.cs +++ b/Projects/UOContent/Mobiles/Monsters/Reptile/Magic/Leviathan.cs @@ -62,6 +62,8 @@ namespace Server.Mobiles public override int TreasureMapLevel => 5; + public override double TreasureMapChance => 0.25; + public override bool OnBeforeDeath() { PackItem(new MessageInABottle(Map == Map.Felucca ? Map.Felucca : Map.Trammel));