From 21ec4b28c77bf8c510c3805dc52df8e3f976a527 Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Sat, 8 Nov 2025 11:43:07 +0100 Subject: [PATCH] Fixes and reworking of item use --- PkmnLib.Dynamic/BattleFlow/TurnRunner.cs | 9 +-- PkmnLib.Dynamic/Models/Battle.cs | 15 +++++ PkmnLib.Dynamic/Models/Choices/ItemChoice.cs | 51 ++++++++++---- PkmnLib.Dynamic/Models/ItemTargetType.cs | 67 +++++++++++++++++++ PkmnLib.Dynamic/ScriptHandling/ItemScript.cs | 2 +- .../ScriptHandling/PokeballScript.cs | 2 +- .../ScriptHandling/ScriptExecution.cs | 24 ++++--- Plugins/PkmnLib.Plugin.Gen7/Data/Items.json | 7 ++ .../Scripts/Items/EvolutionItem.cs | 2 +- .../Scripts/Items/HealingItem.cs | 2 +- .../Scripts/Moves/BugBite.cs | 3 +- 11 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 PkmnLib.Dynamic/Models/ItemTargetType.cs diff --git a/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs b/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs index f8f5a4c..63a3aed 100644 --- a/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs +++ b/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs @@ -179,13 +179,8 @@ public static class TurnRunner var battleData = user.BattleData; if (battleData == null) return; - 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 target = itemChoice.GetTargetPokemon(battle); battle.EventHook.Invoke(new ItemUseEvent(user, itemChoice.Item)); - itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, battle.EventHook); + itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, user, battle, battle.EventHook); } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs index 7c260af..4ea6c15 100644 --- a/PkmnLib.Dynamic/Models/Battle.cs +++ b/PkmnLib.Dynamic/Models/Battle.cs @@ -351,6 +351,21 @@ public class BattleImpl : ScriptSource, IBattle if (switchChoice.SwitchTo == switchChoice.User) return false; } + else if (choice is IItemChoice itemChoice) + { + if (!Library.ScriptResolver.TryResolveBattleItemScript(itemChoice.Item, out var itemScript)) + return false; + if (!itemScript.IsItemUsable) + return false; + if (itemScript.TargetType != ItemTargetType.None) + { + var target = itemChoice.GetTargetPokemon(this); + if (target is null || !itemScript.IsTargetValid(target)) + return false; + if (!itemScript.TargetType.IsValidTarget(this, choice.User, target)) + return false; + } + } return true; diff --git a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs index e166833..4396cb7 100644 --- a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs @@ -11,40 +11,67 @@ public interface IItemChoice : ITurnChoice /// /// The item that is used. /// - public IItem Item { get; } + IItem Item { get; } /// - /// The side the move is targeted at. + /// The target Pokémon of the item, if any. /// - byte? TargetSide { get; } - - /// - /// The position the move is targeted at. - /// - byte? TargetPosition { get; } + IPokemon? GetTargetPokemon(IBattle battle); } /// public class ItemChoice : TurnChoice, IItemChoice { /// - public ItemChoice(IPokemon user, IItem item, byte? targetSide, byte? targetPosition) : base(user) + private ItemChoice(IPokemon user, IItem item, byte? targetSide, byte? targetPosition, IPokemon? targetPokemon) : + base(user) { Item = item; TargetSide = targetSide; TargetPosition = targetPosition; + TargetPokemon = targetPokemon; } + public static ItemChoice CreateWithoutTarget(IPokemon user, IItem item) => + new(user, item, null, null, null); + + public static ItemChoice CreateForOpponent(IPokemon user, IItem item, byte targetSide, byte targetPosition) => + new(user, item, targetSide, targetPosition, null); + + public static ItemChoice CreateForPartyMember(IPokemon user, IItem item, IPokemon targetPokemon) => + new(user, item, null, null, targetPokemon); + /// /// The item that is used. /// public IItem Item { get; } /// - public byte? TargetSide { get; } + public IPokemon? GetTargetPokemon(IBattle battle) + { + if (TargetPokemon != null) + return TargetPokemon; - /// - public byte? TargetPosition { get; } + if (!TargetSide.HasValue || !TargetPosition.HasValue) + return null; + var side = battle.Sides[TargetSide.Value]; + return side.Pokemon[TargetPosition.Value]; + } + + /// + /// The target side of the item, if any. + /// + private byte? TargetSide { get; } + + /// + /// The target position of the item, if any. + /// + private byte? TargetPosition { get; } + + /// + /// The target Pokémon of the item, if any. This is used for party members. + /// + private IPokemon? TargetPokemon { get; } /// public override int ScriptCount => User.ScriptCount; diff --git a/PkmnLib.Dynamic/Models/ItemTargetType.cs b/PkmnLib.Dynamic/Models/ItemTargetType.cs new file mode 100644 index 0000000..ac21259 --- /dev/null +++ b/PkmnLib.Dynamic/Models/ItemTargetType.cs @@ -0,0 +1,67 @@ +namespace PkmnLib.Dynamic.Models; + +/// +/// Types of targets an item can have. +/// +[Flags] +public enum ItemTargetType +{ + /// + /// The item does not require a target. + /// + None = 0, + + /// + /// The item targets Pokémon that are part of the user's own team. + /// + OwnPokemon = 1, + + /// + /// The item targets allied Pokémon. + /// + AllyPokemon = 2, + + /// + /// The item targets opposing Pokémon. + /// + FoePokemon = 4, +} + +/// +/// Helper methods for ItemTargetType. +/// +public static class ItemTargetTypeHelpers +{ + /// + /// Determines if the given target is valid based on the ItemTargetType. + /// + public static bool IsValidTarget(this ItemTargetType targetType, IBattle battle, IPokemon user, IPokemon target) + { + if (targetType == ItemTargetType.None) + return true; + + if (targetType.HasFlag(ItemTargetType.OwnPokemon)) + { + var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); + var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target)); + if (userParty is not null && targetParty is not null && userParty == targetParty) + return true; + } + + if (targetType.HasFlag(ItemTargetType.AllyPokemon)) + { + if (user.BattleData?.BattleSide == target.BattleData?.BattleSide) + return true; + } + + if (targetType.HasFlag(ItemTargetType.FoePokemon)) + { + var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); + var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target)); + if (userParty is not null && targetParty is not null && userParty != targetParty) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs index 4f8aa4f..5b2786f 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs @@ -36,7 +36,7 @@ public abstract class ItemScript : IDeepCloneable /// /// Returns whether the item requires a target to be used. /// - public virtual bool RequiresTarget => false; + public virtual ItemTargetType TargetType => ItemTargetType.None; /// /// Returns whether the item is usable on the given target. diff --git a/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs b/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs index bb81689..4fdc006 100644 --- a/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs +++ b/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs @@ -29,7 +29,7 @@ public abstract class PokeballScript : ItemScript public override bool IsItemUsable => true; /// - public override bool RequiresTarget => true; + public override ItemTargetType TargetType => ItemTargetType.FoePokemon; /// public override bool IsTargetValid(IPokemon target) => diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs index 31fb07c..1d9d216 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs @@ -100,8 +100,8 @@ public static class ScriptExecution /// /// Executes a script on an item. /// - public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IPokemon? target, - EventHook eventHook) + public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IPokemon? target, IPokemon user, + IBattle battle, EventHook eventHook) { if (!scriptResolver.TryResolveBattleItemScript(item, out var itemScript)) { @@ -111,16 +111,20 @@ public static class ScriptExecution return; itemScript.OnInitialize(item.BattleEffect!.Parameters); - var requiresTarget = itemScript.RequiresTarget; - if (requiresTarget) - { - if (target == null) - throw new ArgumentNullException(nameof(target), "Item script requires a target."); - itemScript.OnUseWithTarget(target, eventHook); - } - else + var targetType = itemScript.TargetType; + if (targetType == ItemTargetType.None) { itemScript.OnUse(eventHook); } + else + { + if (target == null) + throw new ArgumentNullException(nameof(target), "Item script requires a target."); + if (!targetType.IsValidTarget(battle, user, target)) + throw new InvalidOperationException("Item script target is not valid for the required target type."); + if (!itemScript.IsTargetValid(target)) + throw new InvalidOperationException("Item script target is not valid."); + itemScript.OnUseWithTarget(target, eventHook); + } } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Data/Items.json b/Plugins/PkmnLib.Plugin.Gen7/Data/Items.json index 04a236d..d6825df 100755 --- a/Plugins/PkmnLib.Plugin.Gen7/Data/Items.json +++ b/Plugins/PkmnLib.Plugin.Gen7/Data/Items.json @@ -2672,6 +2672,7 @@ { "name": "hyper_potion", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 1500, "additionalData": { @@ -3549,6 +3550,7 @@ { "name": "max_potion", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 2500, "additionalData": { @@ -3567,6 +3569,7 @@ { "name": "max_revive", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 4000, "additionalData": { @@ -4584,6 +4587,7 @@ { "name": "potion", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 200, "additionalData": { @@ -5230,6 +5234,7 @@ { "name": "revival_herb", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 2800, "additionalData": { @@ -5239,6 +5244,7 @@ { "name": "revive", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 2000, "additionalData": { @@ -6185,6 +6191,7 @@ { "name": "super_potion", "itemType": "Medicine", + "battleType": "Healing", "flags": [], "price": 700, "additionalData": { diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/EvolutionItem.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/EvolutionItem.cs index 94522dd..e5d98ae 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/EvolutionItem.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/EvolutionItem.cs @@ -14,7 +14,7 @@ public class EvolutionItem : ItemScript public override bool IsItemUsable => true; /// - public override bool RequiresTarget => true; + public override ItemTargetType TargetType => ItemTargetType.OwnPokemon; /// public override bool IsTargetValid(IPokemon target) diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/HealingItem.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/HealingItem.cs index 0647789..860f460 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/HealingItem.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Items/HealingItem.cs @@ -14,7 +14,7 @@ public class HealingItem : ItemScript public override bool IsItemUsable => true; /// - public override bool RequiresTarget => true; + public override ItemTargetType TargetType => ItemTargetType.OwnPokemon; /// public override void OnInitialize(IReadOnlyDictionary? parameters) diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BugBite.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BugBite.cs index 26b9e28..8088d57 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BugBite.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BugBite.cs @@ -20,6 +20,7 @@ public class BugBite : Script, IScriptOnSecondaryEffect } _ = target.ForceSetHeldItem(null); - targetHeldItem.RunItemScript(battleData.Battle.Library.ScriptResolver, user, move.Battle.EventHook); + targetHeldItem.RunItemScript(battleData.Battle.Library.ScriptResolver, user, user, battleData.Battle, + move.Battle.EventHook); } } \ No newline at end of file