diff --git a/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs b/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs index 970dc20..85ec479 100644 --- a/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs +++ b/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs @@ -50,7 +50,7 @@ public class DynamicLibraryImpl : IDynamicLibrary /// Initializes a new instance of the class, with the given /// plugins and static library. /// - public static IDynamicLibrary Create(IEnumerable plugins) + public static IDynamicLibrary Create(IEnumerable plugins) { var load = LibraryLoader.LoadPlugins(plugins); diff --git a/PkmnLib.Dynamic/Libraries/LibraryLoader.cs b/PkmnLib.Dynamic/Libraries/LibraryLoader.cs index dcdb2b6..98ce651 100644 --- a/PkmnLib.Dynamic/Libraries/LibraryLoader.cs +++ b/PkmnLib.Dynamic/Libraries/LibraryLoader.cs @@ -18,7 +18,7 @@ public static class LibraryLoader /// /// Loads plugins and creates a static library from them. /// - public static LoadResult LoadPlugins(IEnumerable plugins) + public static LoadResult LoadPlugins(IEnumerable plugins) { var registry = new ScriptRegistry(); var orderedPlugins = plugins.OrderBy(x => x.LoadOrder).ToList(); @@ -39,7 +39,7 @@ public static class LibraryLoader return new LoadResult(registry, scriptResolver, staticLibrary); } - private static StaticLibraryImpl CreateStaticLibrary(IReadOnlyList plugins) + private static StaticLibraryImpl CreateStaticLibrary(IReadOnlyList plugins) { var resourceProviders = plugins.OfType().ToList(); var settings = resourceProviders.Select(x => x.Settings).LastOrDefault(x => x != null); diff --git a/PkmnLib.Dynamic/Models/LearnedMove.cs b/PkmnLib.Dynamic/Models/LearnedMove.cs index 76dbff2..2cfb231 100644 --- a/PkmnLib.Dynamic/Models/LearnedMove.cs +++ b/PkmnLib.Dynamic/Models/LearnedMove.cs @@ -173,4 +173,7 @@ public class LearnedMoveImpl : ILearnedMove { CurrentPp = Math.Min(uses, MaxPp); } + + /// + public override string ToString() => MoveData.Name; } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index f80f596..f7ffaf8 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -1261,6 +1261,14 @@ public class PokemonImpl : ScriptSource, IPokemon side.CollectScripts(scripts); } } + + /// + public override string ToString() + { + if (!string.IsNullOrEmpty(Nickname)) + return $"{Nickname} ({Species.Name})"; + return Species.Name; + } } /// diff --git a/PkmnLib.Dynamic/ScriptHandling/Registry/Plugin.cs b/PkmnLib.Dynamic/ScriptHandling/Registry/Plugin.cs index c9da2ed..675f3ea 100644 --- a/PkmnLib.Dynamic/ScriptHandling/Registry/Plugin.cs +++ b/PkmnLib.Dynamic/ScriptHandling/Registry/Plugin.cs @@ -2,20 +2,37 @@ using JetBrains.Annotations; namespace PkmnLib.Dynamic.ScriptHandling.Registry; +public interface IPlugin +{ + /// + /// The name of the plugin. Mostly used for debugging purposes. + /// + string Name { get; } + + /// + /// When the plugin should be loaded. Lower values are loaded first. + /// 0 should be reserved for the core battle scripts. + /// + uint LoadOrder { get; } + + /// + /// Run the registration of the plugin when we're building the library. + /// + void Register(ScriptRegistry registry); +} + /// /// A plugin is a way to register scripts and other dynamic components to the script registry. /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] -public abstract class Plugin +public abstract class Plugin : IPlugin where TConfiguration : IPluginConfiguration { - /// - protected Plugin() - { - } + public TConfiguration Configuration { get; } - /// - protected Plugin(PluginConfiguration configuration) + /// + protected Plugin(TConfiguration configuration) { + Configuration = configuration; } /// @@ -38,4 +55,4 @@ public abstract class Plugin /// /// Base class for plugin configuration. /// -public abstract class PluginConfiguration; \ No newline at end of file +public interface IPluginConfiguration; \ No newline at end of file diff --git a/PkmnLib.Tests/Integration/IntegrationTestRunner.cs b/PkmnLib.Tests/Integration/IntegrationTestRunner.cs index d1a3ec0..a544878 100644 --- a/PkmnLib.Tests/Integration/IntegrationTestRunner.cs +++ b/PkmnLib.Tests/Integration/IntegrationTestRunner.cs @@ -1,6 +1,8 @@ using System.Text.Json; using EnumerableAsyncProcessor.Extensions; +using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Models; +using PkmnLib.Plugin.Gen7; using PkmnLib.Static.Species; using PkmnLib.Tests.Integration.Models; @@ -10,7 +12,7 @@ public class IntegrationTestRunner { public static IEnumerable> TestCases() { - var files = Directory.GetFiles("Integration/Tests", "*.json"); + var files = Directory.GetFiles("../../../../PkmnLib.Tests/Integration/Tests", "*.json"); var serializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, @@ -32,7 +34,13 @@ public class IntegrationTestRunner [Test, MethodDataSource(nameof(TestCases))] public async Task RunIntegrationTest(IntegrationTestModel test) { - var library = LibraryHelpers.LoadLibrary(); + var library = DynamicLibraryImpl.Create([ + new Gen7Plugin(new Gen7PluginConfiguration + { + DamageCalculatorHasRandomness = true, + }), + ]); + await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{test.FileName}"); TestContext.Current.AddArtifact(new Artifact { diff --git a/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs b/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs index 4f4daf3..baf5bd3 100644 --- a/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs +++ b/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs @@ -3,6 +3,7 @@ using System.Text.Json.Serialization; using CSPath; using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; +using TUnit.Core.Logging; using JsonSerializer = System.Text.Json.JsonSerializer; namespace PkmnLib.Tests.Integration.Models; @@ -23,6 +24,7 @@ public class SetPokemonAction : IntegrationTestAction { var mon = battle.Parties[FromParty[0]].Party[FromParty[1]]; battle.Sides[Place[0]].SwapPokemon(Place[1], mon); + Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}"); return Task.CompletedTask; } } @@ -42,6 +44,8 @@ public class SetMoveChoiceAction : IntegrationTestAction await Assert.That(move).IsNotNull(); var res = battle.TrySetChoice(new MoveChoice(user, move!, Target[0], Target[1])); await Assert.That(res).IsTrue(); + var target = battle.Sides[Target[0]].Pokemon[Target[1]]; + Console.WriteLine($"Choice: {user} used {move} on {target} ({Target[0]}:{Target[1]})"); } } @@ -56,13 +60,14 @@ public class SetPassChoiceAction : IntegrationTestAction await Assert.That(user).IsNotNull(); var res = battle.TrySetChoice(new PassChoice(user!)); await Assert.That(res).IsTrue(); + Console.WriteLine($"Choice: {user} Pass"); } } public class AssertAction : IntegrationTestAction { - public string Value { get; set; } = null!; - public JsonNode Expected { get; set; } = null!; + public string Value { get; init; } = null!; + public JsonNode Expected { get; init; } = null!; /// public override async Task Execute(IBattle battle) @@ -71,6 +76,9 @@ public class AssertAction : IntegrationTestAction var value = list.Count == 1 ? list[0] : list; var serialized = JsonSerializer.Serialize(value); - await Assert.That(serialized).IsEqualTo(Expected.ToJsonString()); +#pragma warning disable TUnitAssertions0003 + await Assert.That(serialized, Value).IsEqualTo(Expected.ToJsonString()); +#pragma warning restore TUnitAssertions0003 + Console.WriteLine($"Assert: {Value} = {serialized}"); } } \ No newline at end of file diff --git a/PkmnLib.Tests/Integration/Tests/BasicSingleTurn.json b/PkmnLib.Tests/Integration/Tests/BasicSingleTurn.json index 1b4a16a..7ea3bf2 100644 --- a/PkmnLib.Tests/Integration/Tests/BasicSingleTurn.json +++ b/PkmnLib.Tests/Integration/Tests/BasicSingleTurn.json @@ -8,22 +8,36 @@ "positionsPerSide": 1, "parties": [ { - "indices": [[0, 0]], + "indices": [ + [ + 0, + 0 + ] + ], "pokemon": [ { "species": "charizard", "level": 50, - "moves": ["ember"] + "moves": [ + "ember" + ] } ] }, { - "indices": [[1, 0]], + "indices": [ + [ + 1, + 0 + ] + ], "pokemon": [ { "species": "venusaur", "level": 50, - "moves": ["vine_whip"] + "moves": [ + "vine_whip" + ] } ] } @@ -32,13 +46,25 @@ "actions": [ { "$type": "setPokemon", - "place": [0, 0], - "fromParty": [0, 0] + "place": [ + 0, + 0 + ], + "fromParty": [ + 0, + 0 + ] }, { "$type": "setPokemon", - "place": [1, 0], - "fromParty": [1, 0] + "place": [ + 1, + 0 + ], + "fromParty": [ + 1, + 0 + ] }, { "$type": "assert", @@ -47,18 +73,27 @@ }, { "$type": "setMoveChoice", - "place": [0, 0], + "place": [ + 0, + 0 + ], "move": "ember", - "target": [1, 0] + "target": [ + 1, + 0 + ] }, { "$type": "setPassChoice", - "place": [1, 0] + "place": [ + 1, + 0 + ] }, { "$type": "assert", "value": ".Sides[1].Pokemon[0].CurrentHealth", - "expected": 78 + "expected": 84 } ] } \ No newline at end of file diff --git a/PkmnLib.Tests/PkmnLib.Tests.csproj b/PkmnLib.Tests/PkmnLib.Tests.csproj index 73df3e9..08d006b 100644 --- a/PkmnLib.Tests/PkmnLib.Tests.csproj +++ b/PkmnLib.Tests/PkmnLib.Tests.csproj @@ -30,16 +30,7 @@ - - - - Always - - - Always - - - + diff --git a/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs b/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs index 6dc7822..ae93c46 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Gen7Plugin.cs @@ -1,27 +1,37 @@ using System; using PkmnLib.Plugin.Gen7.Libraries.Battling; using PkmnLib.Static.Libraries; +using PkmnLib.Static.Species; namespace PkmnLib.Plugin.Gen7; -public class Gen7PluginConfiguration : PluginConfiguration +public class Gen7PluginConfiguration : IPluginConfiguration { - public bool DamageCalculatorHasRandomness { get; set; } = true; + /// + /// Whether the damage calculator has randomness or not. If set to false, the damage calculator will always return + /// the same value for the same inputs. If set to true, the damage calculator will randomize the damage output + /// between 0.85 and 1.00 of the calculated damage. + /// + /// This should be set to true for most cases, as it simulates the actual damage calculation in the games. Only + /// set to false for testing purposes. + /// + public bool DamageCalculatorHasRandomness { get; init; } = true; + + /// + /// The number of times a species has been caught. This is used for critical capture calculations. + /// + public Func TimesSpeciesCaught { get; init; } = _ => 0; } -public class Gen7Plugin : Dynamic.ScriptHandling.Registry.Plugin, IResourceProvider +public class Gen7Plugin : Plugin, IResourceProvider { - private readonly Gen7PluginConfiguration _configuration; - - public Gen7Plugin() + public Gen7Plugin() : base(new Gen7PluginConfiguration()) { - _configuration = new Gen7PluginConfiguration(); } /// - public Gen7Plugin(PluginConfiguration configuration) : base(configuration) + public Gen7Plugin(Gen7PluginConfiguration configuration) : base(configuration) { - _configuration = (Gen7PluginConfiguration)configuration; } /// @@ -35,9 +45,9 @@ public class Gen7Plugin : Dynamic.ScriptHandling.Registry.Plugin, IResourceProvi { registry.RegisterAssemblyScripts(typeof(Gen7Plugin).Assembly); registry.RegisterBattleStatCalculator(new Gen7BattleStatCalculator()); - registry.RegisterDamageCalculator(new Gen7DamageCalculator(_configuration.DamageCalculatorHasRandomness)); + registry.RegisterDamageCalculator(new Gen7DamageCalculator(Configuration.DamageCalculatorHasRandomness)); registry.RegisterMiscLibrary(new Gen7MiscLibrary()); - registry.RegisterCaptureLibrary(new Gen7CaptureLibrary()); + registry.RegisterCaptureLibrary(new Gen7CaptureLibrary(Configuration)); } /// diff --git a/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7CaptureLibrary.cs b/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7CaptureLibrary.cs index 7f9c06f..481904a 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7CaptureLibrary.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Libraries/Battling/Gen7CaptureLibrary.cs @@ -5,6 +5,13 @@ namespace PkmnLib.Plugin.Gen7.Libraries.Battling; public class Gen7CaptureLibrary : ICaptureLibrary { + private readonly Gen7PluginConfiguration _configuration; + + public Gen7CaptureLibrary(Gen7PluginConfiguration configuration) + { + _configuration = configuration; + } + /// public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random) { @@ -22,10 +29,10 @@ public class Gen7CaptureLibrary : ICaptureLibrary byte bonusStatus = 1; target.RunScriptHook(x => x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus)); - var modifiedCatchRate = (3.0 * maxHealth - 2.0 * currentHealth) * catchRate * bonusBall / (3.0 * maxHealth); + var modifiedCatchRate = (3.0f * maxHealth - 2.0f * currentHealth) * catchRate * bonusBall / (3.0f * maxHealth); modifiedCatchRate *= bonusStatus; - var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875); + var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875f); byte shakes = 0; if (modifiedCatchRate >= 255) { @@ -33,7 +40,26 @@ public class Gen7CaptureLibrary : ICaptureLibrary } else { - // FIXME: Implement critical capture + var timesCaught = _configuration.TimesSpeciesCaught(target.Species); + if (timesCaught > 30) + { + var criticalCaptureModifier = timesCaught switch + { + > 600 => 2.5f, + > 450 => 2.0f, + > 300 => 1.5f, + > 150 => 1.0f, + > 30 => 0.5f, + // Default arm, should be heuristically unreachable (due to the timesCaught > 30 check above) + _ => throw new ArgumentOutOfRangeException(), + }; + var criticalCaptureChance = (int)(modifiedCatchRate * criticalCaptureModifier / 6); + if (random.GetInt(0, 256) < criticalCaptureChance) + { + return new CaptureResult(true, 1, true); + } + } + for (var i = 0; i < 4; i++) { if (random.GetInt(0, 65536) < shakeProbability)