From ecdc9c7654b947db5e127b1384e9055e54a8e715 Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Fri, 10 Jan 2025 13:45:29 +0100 Subject: [PATCH] Implements several more moves --- .../Events/Handling/EventBatchId.cs | 9 +++- PkmnLib.Dynamic/Models/Battle.cs | 2 + PkmnLib.Dynamic/Models/Pokemon.cs | 23 +++++++++- PkmnLib.Dynamic/ScriptHandling/Script.cs | 7 +++ PkmnLib.Tests/Data/Moves.json | 20 +++++++-- .../Scripts/Moves/BeakBlast.cs | 32 +++++++++++++ .../Scripts/Moves/BeatUp.cs | 45 +++++++++++++++++++ .../Scripts/Moves/Belch.cs | 20 +++++++++ .../Scripts/Moves/BellyDrum.cs | 26 +++++++++++ .../Scripts/Pokemon/BeakBlastEffect.cs | 18 ++++++++ .../Scripts/Pokemon/ProtectionEffectScript.cs | 10 +---- .../Scripts/Status/Burned.cs | 10 +++++ 12 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeakBlast.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeatUp.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Belch.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BellyDrum.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BeakBlastEffect.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Burned.cs diff --git a/PkmnLib.Dynamic/Events/Handling/EventBatchId.cs b/PkmnLib.Dynamic/Events/Handling/EventBatchId.cs index eee5a83..559623a 100644 --- a/PkmnLib.Dynamic/Events/Handling/EventBatchId.cs +++ b/PkmnLib.Dynamic/Events/Handling/EventBatchId.cs @@ -8,10 +8,15 @@ namespace PkmnLib.Dynamic.Events; /// For example, when a Pokemon gets hurt by poison, we want to show the purple poison animation and the damage at the /// same time. This is done by batching the events together. /// -public readonly record struct EventBatchId() +public readonly record struct EventBatchId { + public EventBatchId() + { + Id = Guid.NewGuid(); + } + /// /// The unique identifier for this batch of events. /// - public Guid Id { get; init; } = Guid.NewGuid(); + public Guid Id { get; init; } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs index 60278ed..76618ec 100644 --- a/PkmnLib.Dynamic/Models/Battle.cs +++ b/PkmnLib.Dynamic/Models/Battle.cs @@ -242,6 +242,8 @@ public class BattleImpl : ScriptSource, IBattle if (!TargetResolver.IsValidTarget(moveChoice.TargetSide, moveChoice.TargetPosition, moveChoice.ChosenMove.MoveData.Target, moveChoice.User)) return false; + var preventMove = false; + choice.RunScriptHook(script => script.PreventMoveSelection(moveChoice, ref preventMove)); } return true; diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index 5dd2102..3021b5f 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -295,7 +295,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// /// Damages the Pokemon by a certain amount of damage, from a damage source. /// - void Damage(uint damage, DamageSource source, EventBatchId batchId); + void Damage(uint damage, DamageSource source, EventBatchId batchId = default); /// /// Heals the Pokemon by a specific amount. Unless allow_revive is set to true, this will not @@ -389,6 +389,16 @@ public interface IPokemonBattleData : IDeepCloneable /// Adds an opponent to the list of seen opponents. /// void MarkOpponentAsSeen(IPokemon opponent); + + /// + /// A list of items the Pokémon has consumed this battle. + /// + IReadOnlyList ConsumedItems { get; } + + /// + /// Marks an item as consumed. + /// + void MarkItemAsConsumed(IItem itemName); } /// @@ -1062,4 +1072,15 @@ public class PokemonBattleDataImpl : IPokemonBattleData { _seenOpponents.Add(opponent); } + + private readonly List _consumedItems = []; + + /// + public IReadOnlyList ConsumedItems => _consumedItems; + + /// + public void MarkItemAsConsumed(IItem itemName) + { + _consumedItems.Add(itemName); + } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/ScriptHandling/Script.cs b/PkmnLib.Dynamic/ScriptHandling/Script.cs index 132e773..c7ab598 100644 --- a/PkmnLib.Dynamic/ScriptHandling/Script.cs +++ b/PkmnLib.Dynamic/ScriptHandling/Script.cs @@ -76,6 +76,13 @@ public abstract class Script : IDeepCloneable public virtual void OnInitialize(IReadOnlyDictionary? parameters) { } + + /// + /// Override to customize whether the move can be selected at all. + /// + public virtual void PreventMoveSelection(IMoveChoice choice, ref bool prevent) + { + } /// /// This function is ran just before the start of the turn. Everyone has made its choices here, diff --git a/PkmnLib.Tests/Data/Moves.json b/PkmnLib.Tests/Data/Moves.json index d095ed3..24a7845 100755 --- a/PkmnLib.Tests/Data/Moves.json +++ b/PkmnLib.Tests/Data/Moves.json @@ -744,7 +744,10 @@ "category": "physical", "flags": [ "protect" - ] + ], + "effect": { + "name": "beak_blast" + } }, { "name": "beat_up", @@ -758,7 +761,10 @@ "flags": [ "protect", "mirror" - ] + ], + "effect": { + "name": "beat_up" + } }, { "name": "belch", @@ -771,7 +777,10 @@ "category": "special", "flags": [ "protect" - ] + ], + "effect": { + "name": "belch" + } }, { "name": "belly_drum", @@ -784,7 +793,10 @@ "category": "status", "flags": [ "snatch" - ] + ], + "effect": { + "name": "belly_drum" + } }, { "name": "bestow", diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeakBlast.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeakBlast.cs new file mode 100644 index 0000000..d54ce98 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeakBlast.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using PkmnLib.Dynamic.Events; +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.Models.Choices; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; +using PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +namespace PkmnLib.Plugin.Gen7.Scripts.Moves; + +[Script(ScriptCategory.Move, "beak_blast")] +public class BeakBlast : Script +{ + /// + public override void OnBeforeTurnStart(ITurnChoice choice) + { + var battleData = choice.User.BattleData; + if (battleData == null) + return; + choice.User.Volatile.Add(new BeakBlastEffect()); + battleData.Battle.EventHook.Invoke(new DialogEvent("beak_blast_charge", new Dictionary() + { + { "user", choice.User } + })); + } + + /// + public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) + { + move.User.Volatile.Remove(ScriptUtils.ResolveName()); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeatUp.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeatUp.cs new file mode 100644 index 0000000..78c341d --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BeatUp.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.Models.Choices; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Moves; + +[Script(ScriptCategory.Move, "beat_up")] +public class BeatUp : Script +{ + private IPokemon[]? _relevantPartyMembers; + private static IEnumerable GetRelevantPartyMembers(IPokemon user) + { + var battleData = user.BattleData; + if (battleData == null) + return []; + + var party = battleData.Battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); + return party?.Party.WhereNotNull().Where(x => x.IsUsable && x.StatusScript.IsEmpty) ?? []; + } + + /// + public override void ChangeNumberOfHits(IMoveChoice choice, ref byte numberOfHits) + { + var relevantPartyMembers = _relevantPartyMembers ??= GetRelevantPartyMembers(choice.User).ToArray(); + + numberOfHits = (byte)relevantPartyMembers.Count(); + if (numberOfHits == 0) + numberOfHits = 1; + } + + /// + public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref byte basePower) + { + var relevantPartyMembers = _relevantPartyMembers ??= GetRelevantPartyMembers(move.User).ToArray(); + var hittingPokemon = relevantPartyMembers.ElementAtOrDefault(hit); + if (hittingPokemon == null) + return; + + basePower = (byte)(hittingPokemon.Form.BaseStats.Attack / 10 + 5); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Belch.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Belch.cs new file mode 100644 index 0000000..cae9af9 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Belch.cs @@ -0,0 +1,20 @@ +using System.Linq; +using PkmnLib.Dynamic.Models.Choices; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Static; + +namespace PkmnLib.Plugin.Gen7.Scripts.Moves; + +public class Belch : Script +{ + /// + public override void PreventMoveSelection(IMoveChoice choice, ref bool prevent) + { + var battleData = choice.User.BattleData; + if (battleData == null) + return; + + if (battleData.ConsumedItems.All(x => x.Category != ItemCategory.Berry)) + prevent = true; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BellyDrum.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BellyDrum.cs new file mode 100644 index 0000000..c0a28b5 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BellyDrum.cs @@ -0,0 +1,26 @@ +using PkmnLib.Dynamic.Events; +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; +using PkmnLib.Static; + +namespace PkmnLib.Plugin.Gen7.Scripts.Moves; + +[Script(ScriptCategory.Move, "belly_drum")] +public class BellyDrum : Script +{ + /// + public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) + { + var maxHealthHalved = target.BoostedStats.Hp / 2; + if (target.CurrentHealth <= maxHealthHalved) + { + move.GetHitData(target, hit).Fail(); + return; + } + + target.Damage(maxHealthHalved, DamageSource.Misc); + // Raising the user's Attack by 12 stages should always set it to +6. + target.ChangeStatBoost(Statistic.Attack, 12, true); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BeakBlastEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BeakBlastEffect.cs new file mode 100644 index 0000000..4a4366b --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BeakBlastEffect.cs @@ -0,0 +1,18 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; + +namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +[Script(ScriptCategory.Pokemon, "beak_blast_effect")] +public class BeakBlastEffect : Script +{ + /// + public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit) + { + if (move.UseMove.HasFlag("contact")) + { + move.User.SetStatus("burned"); + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs index 86ca7f8..52f9c8c 100644 --- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs @@ -12,15 +12,7 @@ public abstract class ProtectionEffectScript : Script if (target.BattleData == null) return; - var originalTarget = executingMove.MoveChoice.TargetPosition; - var targetPosition = target.BattleData.Position; - - // We only want to block the hit if it's explicitly targeting the Pokemon. - if (targetPosition != originalTarget) - return; - - if (executingMove.UseMove.Target is MoveTarget.All or MoveTarget.SelfUse or MoveTarget.AllAlly - or MoveTarget.AllAdjacent) + if (!executingMove.UseMove.HasFlag("protect")) return; block = true; diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Burned.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Burned.cs new file mode 100644 index 0000000..9d5cb08 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Burned.cs @@ -0,0 +1,10 @@ +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; + +namespace PkmnLib.Plugin.Gen7.Scripts.Status; + +[Script(ScriptCategory.Status, "burned")] +public class Burned : Script +{ + // TODO: Implement the Burned status effect. +} \ No newline at end of file