From 0518499a4cfb8ec6e9334eb0776c8d37ea13d54c Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Fri, 10 Jan 2025 11:11:50 +0100 Subject: [PATCH] Initial set up for item use --- PkmnLib.Dataloader/CommonDataLoaderHelper.cs | 21 ++++++++ PkmnLib.Dataloader/ItemDataLoader.cs | 16 +++--- PkmnLib.Dataloader/Models/SerializedItem.cs | 2 + PkmnLib.Dataloader/MoveDataLoader.cs | 12 +---- PkmnLib.Dynamic/Libraries/DynamicLibrary.cs | 2 +- PkmnLib.Dynamic/Models/Battle.cs | 3 +- .../Models/BattleFlow/MoveTurnExecutor.cs | 4 +- .../Models/BattleFlow/TurnRunner.cs | 36 ++++++++++++- PkmnLib.Dynamic/Models/Choices/ItemChoice.cs | 23 +++++++- PkmnLib.Dynamic/Models/Choices/MoveChoice.cs | 4 +- PkmnLib.Dynamic/Models/Pokemon.cs | 8 +-- PkmnLib.Dynamic/ScriptHandling/ItemScript.cs | 54 +++++++++++++++++++ .../Registry/ItemScriptAttribute.cs | 20 +++++++ .../ScriptHandling/Registry/ScriptRegistry.cs | 32 ++++++++++- PkmnLib.Dynamic/ScriptHandling/Script.cs | 3 +- .../ScriptHandling/ScriptResolver.cs | 38 ++++++++++--- PkmnLib.Static/Item.cs | 14 ++++- PkmnLib.Tests/Data/Items.json | 16 +++--- .../Scripts/Items/HealingItem.cs | 40 ++++++++++++++ .../Scripts/Moves/ChangeAllTargetStats.cs | 3 +- .../Scripts/Moves/ChangeTargetStats.cs | 3 +- .../Scripts/Moves/Drain.cs | 5 +- .../Scripts/Moves/HealEachEndOfTurn.cs | 5 +- 23 files changed, 305 insertions(+), 59 deletions(-) create mode 100644 PkmnLib.Dataloader/CommonDataLoaderHelper.cs create mode 100644 PkmnLib.Dynamic/ScriptHandling/ItemScript.cs create mode 100644 PkmnLib.Dynamic/ScriptHandling/Registry/ItemScriptAttribute.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/HealingItem.cs diff --git a/PkmnLib.Dataloader/CommonDataLoaderHelper.cs b/PkmnLib.Dataloader/CommonDataLoaderHelper.cs new file mode 100644 index 0000000..86d09c4 --- /dev/null +++ b/PkmnLib.Dataloader/CommonDataLoaderHelper.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using PkmnLib.Dataloader.Models; +using PkmnLib.Static.Moves; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dataloader; + +internal static class CommonDataLoaderHelper +{ + internal static ISecondaryEffect? ParseEffect(this SerializedMoveEffect? effect) + { + if (effect == null) + return null; + var name = effect.Name; + var chance = effect.Chance; + var parameters = effect.Parameters?.ToDictionary(x => (StringKey)x.Key, x => x.Value.ToParameter()) ?? + new Dictionary(); + return new SecondaryEffectImpl(chance, name, parameters); + } +} \ No newline at end of file diff --git a/PkmnLib.Dataloader/ItemDataLoader.cs b/PkmnLib.Dataloader/ItemDataLoader.cs index 8d2e864..6f9fed3 100644 --- a/PkmnLib.Dataloader/ItemDataLoader.cs +++ b/PkmnLib.Dataloader/ItemDataLoader.cs @@ -7,6 +7,7 @@ using System.Text.Json; using PkmnLib.Dataloader.Models; using PkmnLib.Static; using PkmnLib.Static.Libraries; +using PkmnLib.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Dataloader; @@ -27,20 +28,23 @@ public static class ItemDataLoader library.Add(i); return library; } - + + // ReSharper disable once MemberCanBePrivate.Global public static Func, IItem> ItemConstructor = (_, name, type, battleType, price, flags) => - { - return new ItemImpl(name, type, battleType, price, flags); - }; + IEnumerable, ISecondaryEffect?, ISecondaryEffect?, + // ReSharper disable once FieldCanBeMadeReadOnly.Global + IItem> ItemConstructor = (_, name, type, battleType, price, flags, effect, battleTriggerEffect) => + new ItemImpl(name, type, battleType, price, flags, effect, battleTriggerEffect); private static IItem DeserializeItem(SerializedItem serialized) { if (!Enum.TryParse(serialized.ItemType, true, out var itemType)) throw new InvalidDataException($"Item type {serialized.ItemType} is not valid for item {serialized.Name}."); Enum.TryParse(serialized.BattleType, true, out BattleItemCategory battleType); + var effect = serialized.Effect?.ParseEffect(); + var battleTriggerEffect = serialized.BattleEffect?.ParseEffect(); return ItemConstructor(serialized, serialized.Name, itemType, battleType, serialized.Price, - serialized.Flags.Select(x => (StringKey)x).ToImmutableHashSet()); + serialized.Flags.Select(x => (StringKey)x).ToImmutableHashSet(), effect, battleTriggerEffect); } } \ No newline at end of file diff --git a/PkmnLib.Dataloader/Models/SerializedItem.cs b/PkmnLib.Dataloader/Models/SerializedItem.cs index 4706a7e..fbbf53e 100644 --- a/PkmnLib.Dataloader/Models/SerializedItem.cs +++ b/PkmnLib.Dataloader/Models/SerializedItem.cs @@ -12,6 +12,8 @@ public class SerializedItem public string[] Flags { get; set; } = null!; public int Price { get; set; } public byte FlingPower { get; set; } + public SerializedMoveEffect? Effect { get; set; } + public SerializedMoveEffect? BattleEffect { get; set; } [JsonExtensionData] public Dictionary? ExtensionData { get; set; } diff --git a/PkmnLib.Dataloader/MoveDataLoader.cs b/PkmnLib.Dataloader/MoveDataLoader.cs index 3ef1b87..7abd882 100644 --- a/PkmnLib.Dataloader/MoveDataLoader.cs +++ b/PkmnLib.Dataloader/MoveDataLoader.cs @@ -57,21 +57,11 @@ public static class MoveDataLoader throw new InvalidDataException($"Category {category} is not a valid category."); if (!Enum.TryParse(target, true, out var targetEnum)) throw new InvalidDataException($"Target {target} is not a valid target."); - var secondaryEffect = ParseEffect(effect); + var secondaryEffect = effect.ParseEffect(); var move = MoveConstructor(serialized, serialized.Name, typeIdentifier, categoryEnum, power, accuracy, pp, targetEnum, priority, secondaryEffect, flags.Select(x => (StringKey)x).ToImmutableHashSet()); return move; } - private static ISecondaryEffect? ParseEffect(SerializedMoveEffect? effect) - { - if (effect == null) - return null; - var name = effect.Name; - var chance = effect.Chance; - var parameters = effect.Parameters?.ToDictionary(x => (StringKey)x.Key, x => x.Value.ToParameter()) ?? - new Dictionary(); - return new SecondaryEffectImpl(chance, name, parameters); - } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs b/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs index 3f282fd..0e1f1e0 100644 --- a/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs +++ b/PkmnLib.Dynamic/Libraries/DynamicLibrary.cs @@ -58,7 +58,7 @@ public class DynamicLibraryImpl : IDynamicLibrary throw new InvalidOperationException("Stat calculator not found in plugins."); if (registry.MiscLibrary is null) throw new InvalidOperationException("Misc library not found in plugins."); - var scriptResolver = new ScriptResolver(registry.ScriptTypes); + var scriptResolver = new ScriptResolver(registry.ScriptTypes, registry.ItemScriptTypes); return new DynamicLibraryImpl(staticLibrary, registry.BattleStatCalculator, registry.DamageCalculator, registry.MiscLibrary, scriptResolver); } diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs index a807983..3924d28 100644 --- a/PkmnLib.Dynamic/Models/Battle.cs +++ b/PkmnLib.Dynamic/Models/Battle.cs @@ -314,10 +314,9 @@ public class BattleImpl : ScriptSource, IBattle { if (weatherName.HasValue) { - if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, out var script)) + if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, null, out var script)) throw new InvalidOperationException($"Weather script {weatherName} not found."); _weatherScript.Set(script); - script.OnInitialize(Library, null); } else { diff --git a/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs b/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs index caef755..d9921f2 100644 --- a/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs +++ b/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs @@ -25,10 +25,10 @@ internal static class MoveTurnExecutor var secondaryEffect = moveData.SecondaryEffect; if (secondaryEffect != null) { - if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name, out var script)) + if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name, + secondaryEffect.Parameters, out var script)) { moveChoice.Script.Set(script); - script.OnInitialize(moveChoice.User.Library, secondaryEffect.Parameters); } } diff --git a/PkmnLib.Dynamic/Models/BattleFlow/TurnRunner.cs b/PkmnLib.Dynamic/Models/BattleFlow/TurnRunner.cs index b0d6290..acb1957 100644 --- a/PkmnLib.Dynamic/Models/BattleFlow/TurnRunner.cs +++ b/PkmnLib.Dynamic/Models/BattleFlow/TurnRunner.cs @@ -90,7 +90,9 @@ public static class TurnRunner case IFleeChoice fleeChoice: ExecuteFleeChoice(battle, fleeChoice); break; - // TODO: Implement item choice types + case IItemChoice itemChoice: + ExecuteItemChoice(battle, itemChoice); + break; } } @@ -155,4 +157,36 @@ public static class TurnRunner battle.ValidateBattleState(); } + private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice) + { + var user = itemChoice.User; + var battleData = user.BattleData; + if (battleData == null) + return; + if (!battle.Library.ScriptResolver.TryResolveBattleItemScript(itemChoice.Item, out var itemScript)) + { + return; + } + if (!itemScript.IsItemUsable) + return; + itemScript.OnInitialize(itemChoice.Item.BattleEffect!.Parameters); + IPokemon? target = null; + if (itemChoice is { TargetSide: not null, TargetPosition: not null }) + { + var side = battle.Sides[itemChoice.TargetSide.Value]; + target = side.Pokemon[itemChoice.TargetPosition.Value]; + } + + var requiresTarget = itemScript.RequiresTarget; + if (requiresTarget) + { + target ??= user; + itemScript.OnUseWithTarget(target); + } + else + { + itemScript.OnUse(); + } + } + } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs index 38535ec..80b6e72 100644 --- a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs @@ -8,19 +8,40 @@ namespace PkmnLib.Dynamic.Models.Choices; /// public interface IItemChoice : ITurnChoice { + /// + /// The item that is used. + /// + public IItem Item { get; } + /// + /// The side the move is targeted at. + /// + byte? TargetSide { get; } + + /// + /// The position the move is targeted at. + /// + byte? TargetPosition { get; } } /// public class ItemChoice : TurnChoice, IItemChoice { - public ItemChoice(IPokemon user, IItem item) : base(user) + public ItemChoice(IPokemon user, IItem item, byte? targetSide, byte? targetPosition) : base(user) { Item = item; + TargetSide = targetSide; + TargetPosition = targetPosition; } public IItem Item { get; } + /// + public byte? TargetSide { get; } + + /// + public byte? TargetPosition { get; } + /// public override int ScriptCount => User.ScriptCount; diff --git a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs index b45c4f9..054c1a8 100644 --- a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs @@ -46,10 +46,10 @@ public class MoveChoice : TurnChoice, IMoveChoice var secondaryEffect = usedMove.MoveData.SecondaryEffect; if (secondaryEffect != null) { - if (user.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name, out var script)) + if (user.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name, + secondaryEffect.Parameters, out var script)) { Script.Set(script); - script.OnInitialize(user.Library, secondaryEffect.Parameters); } } } diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index 8d642b4..9780b5d 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -465,7 +465,7 @@ public class PokemonImpl : ScriptSource, IPokemon if (serializedPokemon.Status != null) { - if (!library.ScriptResolver.TryResolve(ScriptCategory.Status, serializedPokemon.Status, + if (!library.ScriptResolver.TryResolve(ScriptCategory.Status, serializedPokemon.Status, null, out var statusScript)) throw new KeyNotFoundException($"Status script {serializedPokemon.Status} not found"); StatusScript.Set(statusScript); @@ -668,7 +668,7 @@ public class PokemonImpl : ScriptSource, IPokemon { if (HeldItem is null) return false; - if (!Library.ScriptResolver.TryResolveItemScript(HeldItem, out _)) + if (!Library.ScriptResolver.TryResolveBattleItemScript(HeldItem, out _)) return false; // TODO: actually consume the item throw new NotImplementedException(); @@ -777,10 +777,10 @@ public class PokemonImpl : ScriptSource, IPokemon AbilityScript.Clear(); if (!Library.StaticLibrary.Abilities.TryGet(newAbility, out var ability)) throw new KeyNotFoundException($"Ability {newAbility} not found."); - if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, newAbility, out var abilityScript)) + if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, newAbility, ability.Parameters, + out var abilityScript)) { AbilityScript.Set(abilityScript); - abilityScript.OnInitialize(Library, ability.Parameters); } else { diff --git a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs new file mode 100644 index 0000000..be0920d --- /dev/null +++ b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs @@ -0,0 +1,54 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dynamic.ScriptHandling; + +public abstract class ItemScript : IDeepCloneable +{ + /// + /// Initializes the script with the given parameters for a specific item + /// + public virtual void OnInitialize(IReadOnlyDictionary? parameters) + { + } + + /// + /// Returns whether the item is usable in the current context. + /// + public virtual bool IsItemUsable => false; + + /// + /// Returns whether the item requires a target to be used. + /// + public virtual bool RequiresTarget => false; + + /// + /// Returns whether the item is usable on the given target. + /// + public virtual bool IsTargetValid(IPokemon target) => false; + + /// + /// Returns whether the item can be held by a Pokémon. + /// + public virtual bool IsHoldable => false; + + /// + /// Returns whether the item can be held by the given target. + /// + public virtual bool CanTargetHold(IPokemon pokemon) => false; + + /// + /// Handles the use of the item. + /// + public virtual void OnUse() + { + } + + /// + /// Handles the use of the item on the given target. + /// + /// + public virtual void OnUseWithTarget(IPokemon target) + { + } +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/ScriptHandling/Registry/ItemScriptAttribute.cs b/PkmnLib.Dynamic/ScriptHandling/Registry/ItemScriptAttribute.cs new file mode 100644 index 0000000..e4e4d76 --- /dev/null +++ b/PkmnLib.Dynamic/ScriptHandling/Registry/ItemScriptAttribute.cs @@ -0,0 +1,20 @@ +using JetBrains.Annotations; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dynamic.ScriptHandling.Registry; + +[AttributeUsage(AttributeTargets.Class)] +[MeansImplicitUse] +public class ItemScriptAttribute : Attribute +{ + /// + /// The name of the script. + /// + public StringKey Name { get; } + + /// + public ItemScriptAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptRegistry.cs b/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptRegistry.cs index 0df56c5..8c22b63 100644 --- a/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptRegistry.cs +++ b/PkmnLib.Dynamic/ScriptHandling/Registry/ScriptRegistry.cs @@ -13,6 +13,7 @@ namespace PkmnLib.Dynamic.ScriptHandling.Registry; public class ScriptRegistry { private readonly Dictionary<(ScriptCategory category, StringKey name), Func