From b090aa65f9ac0c54b6373a10381e0309f05e9c01 Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Sat, 31 May 2025 12:29:03 +0200 Subject: [PATCH] First couple abilities implemented --- .../BattleFlow/MoveTurnExecutor.cs | 2 + PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs | 20 ++++ .../DataLoaders/AbilityDataLoader.cs | 2 +- .../DataLoaders/Models/SerializedAbility.cs | 2 + PkmnLib.Dynamic/Models/BattleSide.cs | 11 +- PkmnLib.Dynamic/Models/ExecutingMove.cs | 22 ++++ PkmnLib.Dynamic/Models/Pokemon.cs | 7 +- PkmnLib.Dynamic/ScriptHandling/Script.cs | 7 ++ .../ScriptHandling/ScriptResolver.cs | 5 +- PkmnLib.Static/Species/Ability.cs | 8 +- PkmnLib.Static/Utils/NumericHelpers.cs | 6 + .../DataTests/AbilityDataTests.cs | 56 +++++++++ .../PkmnLib.Plugin.Gen7/Data/Abilities.json | 109 +++++++++++------- .../Scripts/Abilities/Aftermath.cs | 33 ++++++ .../Scripts/Abilities/Analytic.cs | 15 +++ .../Scripts/Abilities/AngerPoint.cs | 19 +++ .../Scripts/Abilities/Anticipation.cs | 39 +++++++ .../Scripts/Abilities/ArenaTrap.cs | 35 ++++++ .../Scripts/Abilities/AromaVeil.cs | 21 ++++ .../Scripts/Abilities/AuraBreak.cs | 7 ++ .../Scripts/Abilities/BadDreams.cs | 35 ++++++ .../Scripts/Abilities/Battery.cs | 21 ++++ .../Scripts/Abilities/BattleBond.cs | 32 +++++ .../Scripts/Abilities/BeastBoost.cs | 17 +++ .../Scripts/Abilities/Berserk.cs | 21 ++++ .../Scripts/Abilities/Bulletproof.cs | 12 ++ .../Abilities/ChangeMoveTypeAbility.cs | 47 ++++++++ .../Scripts/Abilities/IncreasedStabAbility.cs | 18 +++ .../Abilities/PowerUpTypeAtLowHealth.cs | 38 ++++++ .../Scripts/Abilities/PreventCritical.cs | 9 ++ .../Scripts/Abilities/PreventStatLowering.cs | 30 +++++ .../Abilities/SuppressWeatherAbility.cs | 18 +++ .../Scripts/Side/AromaVeilEffect.cs | 32 +++++ .../Scripts/Side/BatteryAbilityEffect.cs | 27 +++++ 34 files changed, 733 insertions(+), 50 deletions(-) create mode 100644 PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/AbilityDataTests.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Aftermath.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Analytic.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AngerPoint.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Anticipation.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ArenaTrap.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AromaVeil.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AuraBreak.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BadDreams.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Battery.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BattleBond.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BeastBoost.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Berserk.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Bulletproof.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ChangeMoveTypeAbility.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/IncreasedStabAbility.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PowerUpTypeAtLowHealth.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventCritical.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventStatLowering.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/SuppressWeatherAbility.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/AromaVeilEffect.cs create mode 100644 Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/BatteryAbilityEffect.cs diff --git a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs index a3dc17d..ee4486d 100644 --- a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs +++ b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs @@ -263,6 +263,8 @@ public static class MoveTurnExecutor } } } + if (target.IsFainted) + executingMove.RunScriptHook(x => x.OnOpponentFaints(executingMove, target, hitIndex)); } } } diff --git a/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs b/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs new file mode 100644 index 0000000..1d140d2 --- /dev/null +++ b/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs @@ -0,0 +1,20 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Static.Species; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dynamic.Events; + +public record AbilityTriggerEvent : IEventData +{ + public IPokemon Pokemon { get; } + public IAbility? Ability { get; } + + public AbilityTriggerEvent(IPokemon pokemon) + { + Pokemon = pokemon; + Ability = pokemon.ActiveAbility; + } + + /// + public EventBatchId BatchId { get; init; } +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/Libraries/DataLoaders/AbilityDataLoader.cs b/PkmnLib.Dynamic/Libraries/DataLoaders/AbilityDataLoader.cs index 6e5d019..24ea9d1 100644 --- a/PkmnLib.Dynamic/Libraries/DataLoaders/AbilityDataLoader.cs +++ b/PkmnLib.Dynamic/Libraries/DataLoaders/AbilityDataLoader.cs @@ -59,7 +59,7 @@ public static class AbilityDataLoader var flags = serialized.Flags.Select(x => new StringKey(x)).ToImmutableHashSet(); - var ability = new AbilityImpl(name, effectName, parameters, flags); + var ability = new AbilityImpl(name, effectName, parameters, flags, serialized.CanBeChanged ?? true); return ability; } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Libraries/DataLoaders/Models/SerializedAbility.cs b/PkmnLib.Dynamic/Libraries/DataLoaders/Models/SerializedAbility.cs index a090166..c2fb6ad 100644 --- a/PkmnLib.Dynamic/Libraries/DataLoaders/Models/SerializedAbility.cs +++ b/PkmnLib.Dynamic/Libraries/DataLoaders/Models/SerializedAbility.cs @@ -19,4 +19,6 @@ public class SerializedAbility /// A collection of arbitrary flags that can be used to mark the ability with specific properties. /// public string[] Flags { get; set; } = []; + + public bool? CanBeChanged { get; set; } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/BattleSide.cs b/PkmnLib.Dynamic/Models/BattleSide.cs index 3d9673f..a5f1071 100644 --- a/PkmnLib.Dynamic/Models/BattleSide.cs +++ b/PkmnLib.Dynamic/Models/BattleSide.cs @@ -271,18 +271,25 @@ public class BattleSideImpl : ScriptSource, IBattleSide pokemon.SetBattleData(Battle, Index); pokemon.SetOnBattlefield(true); pokemon.SetBattleSidePosition(position); + Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon)); + pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position)); + foreach (var side in Battle.Sides) { if (side == this) continue; + var scripts = new List>(10); foreach (var opponent in side.Pokemon.WhereNotNull()) { opponent.MarkOpponentAsSeen(pokemon); pokemon.MarkOpponentAsSeen(opponent); + + scripts.Clear(); + opponent.GetOwnScripts(scripts); + opponent.RunScriptHook(script => script.OnOpponentSwitchIn(pokemon, position)); } + side.RunScriptHook(script => script.OnOpponentSwitchIn(pokemon, position)); } - Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon)); - pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position)); } else { diff --git a/PkmnLib.Dynamic/Models/ExecutingMove.cs b/PkmnLib.Dynamic/Models/ExecutingMove.cs index dca6b79..c6a93bc 100644 --- a/PkmnLib.Dynamic/Models/ExecutingMove.cs +++ b/PkmnLib.Dynamic/Models/ExecutingMove.cs @@ -46,6 +46,16 @@ public interface IHitData /// Fails the hit. /// void Fail(); + + /// + /// Sets a flag on the hit data. This is used to mark certain conditions or states + /// + void SetFlag(StringKey flag); + + /// + /// Checks whether a flag is set on the hit data. + /// + bool HasFlag(StringKey flag); } /// @@ -71,6 +81,18 @@ public record HitData : IHitData /// public void Fail() => HasFailed = true; + + private HashSet? _flags; + + /// + public void SetFlag(StringKey flag) + { + _flags ??= []; + _flags.Add(flag); + } + + /// + public bool HasFlag(StringKey flag) => _flags != null && _flags.Contains(flag); } /// diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index f7ffaf8..eac1629 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -401,7 +401,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// /// Changes the ability of the Pokémon. /// - void ChangeAbility(IAbility ability); + bool ChangeAbility(IAbility ability); /// /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as @@ -1197,8 +1197,10 @@ public class PokemonImpl : ScriptSource, IPokemon } /// - public void ChangeAbility(IAbility ability) + public bool ChangeAbility(IAbility ability) { + if (!ability.CanBeChanged) + return false; OverrideAbility = ability; if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters, out var abilityScript)) @@ -1210,6 +1212,7 @@ public class PokemonImpl : ScriptSource, IPokemon { AbilityScript.Clear(); } + return true; } /// diff --git a/PkmnLib.Dynamic/ScriptHandling/Script.cs b/PkmnLib.Dynamic/ScriptHandling/Script.cs index 65f47e8..c1e0928 100644 --- a/PkmnLib.Dynamic/ScriptHandling/Script.cs +++ b/PkmnLib.Dynamic/ScriptHandling/Script.cs @@ -546,6 +546,13 @@ public abstract class Script : IDeepCloneable { } + /// + /// This function is triggered on a Pokemon and its parents when an opponent switches in. + /// + public virtual void OnOpponentSwitchIn(IPokemon pokemon, byte position) + { + } + /// /// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the /// held item it had. diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptResolver.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptResolver.cs index 3014d8e..9880879 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ScriptResolver.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ScriptResolver.cs @@ -34,10 +34,7 @@ public class ScriptResolver } script = scriptCtor(); - if (parameters != null) - { - script.OnInitialize(parameters); - } + script.OnInitialize(parameters); return true; } diff --git a/PkmnLib.Static/Species/Ability.cs b/PkmnLib.Static/Species/Ability.cs index eb40b39..ef87902 100644 --- a/PkmnLib.Static/Species/Ability.cs +++ b/PkmnLib.Static/Species/Ability.cs @@ -23,6 +23,8 @@ public interface IAbility : INamedValue /// Checks whether the ability has a specific flag. /// bool HasFlag(StringKey key); + + bool CanBeChanged { get; } } /// @@ -30,12 +32,13 @@ public class AbilityImpl : IAbility { /// public AbilityImpl(StringKey name, StringKey? effect, IReadOnlyDictionary parameters, - ImmutableHashSet flags) + ImmutableHashSet flags, bool canBeChanged) { Name = name; Effect = effect; Parameters = parameters; Flags = flags; + CanBeChanged = canBeChanged; } /// @@ -54,6 +57,9 @@ public class AbilityImpl : IAbility /// public bool HasFlag(StringKey key) => Flags.Contains(key); + + /// + public bool CanBeChanged { get; } } /// diff --git a/PkmnLib.Static/Utils/NumericHelpers.cs b/PkmnLib.Static/Utils/NumericHelpers.cs index 7729dc5..638f1e0 100644 --- a/PkmnLib.Static/Utils/NumericHelpers.cs +++ b/PkmnLib.Static/Utils/NumericHelpers.cs @@ -5,6 +5,12 @@ namespace PkmnLib.Static.Utils; /// public static class NumericHelpers { + /// + /// Checks if two floating-point values are approximately equal within a specified tolerance. + /// + public static bool IsApproximatelyEqualTo(this float value, float other, float tolerance = 0.0001f) => + MathF.Abs(value - other) <= tolerance; + /// /// Multiplies two values. If this overflows, returns . /// diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/AbilityDataTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/AbilityDataTests.cs new file mode 100644 index 0000000..7cca405 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/AbilityDataTests.cs @@ -0,0 +1,56 @@ +using PkmnLib.Dynamic.Libraries; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Static.Species; + +namespace PkmnLib.Plugin.Gen7.Tests.DataTests; + +public class AbilityDataTests +{ + public record TestCaseData(IDynamicLibrary Library, IAbility Ability) + { + /// + public override string ToString() => Ability.Name + " has valid scripts"; + } + + public static IEnumerable> AllAbilitiesHaveValidScriptsData() + { + var library = LibraryHelpers.LoadLibrary(); + var abilityLibrary = library.StaticLibrary.Abilities; + foreach (var ability in abilityLibrary) + { + if (ability.Effect is null) + continue; + yield return () => new TestCaseData(library, ability); + } + } + + [Test, MethodDataSource(nameof(AllAbilitiesHaveValidScriptsData)), Explicit] + public async Task AllAbilitiesEffectsHaveValidScripts(TestCaseData test) + { + var scriptName = test.Ability.Effect; + if (scriptName is null) + return; + + try + { + await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Ability, scriptName.Value, + test.Ability.Parameters, out _)).IsTrue(); + } + catch (Exception e) + { + // Helper method to find the line number of the effect in the JSON file + var file = Path.GetFullPath("../../../../Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.json"); + var json = await File.ReadAllLinesAsync(file); + var moveLineNumber = json.Select((line, index) => new { line, index }) + .FirstOrDefault(x => x.line.Contains($"\"name\": \"{test.Ability.Effect}\""))?.index + 1; + var effectLineNumber = moveLineNumber + json.Skip(moveLineNumber ?? 0) + .Select((line, index) => new { line, index }).FirstOrDefault(x => x.line.Contains("effect")) + ?.index + + 1 ?? 0; + + await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{file}:{effectLineNumber}"); + throw new AggregateException($"Failed to resolve script for move {test.Ability} with effect {scriptName}", + e); + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.json b/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.json index 4904743..f4fb4f9 100755 --- a/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.json +++ b/Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.json @@ -1,68 +1,75 @@ { "adaptability": { - "effect": "IncreasedStab" + "effect": "increased_stab" }, "aerilate": { - "effect": "ChangeMoveType", + "effect": "change_move_type", "parameters": { - "from": "normal", - "to": "flying" + "from_type": "normal", + "to_type": "flying" } }, "aftermath": { - "effect": "Aftermath" + "effect": "aftermath" }, "air_lock": { - "effect": "SuppressWeather" + "effect": "suppress_weather" }, "analytic": { - "effect": "Analytic" + "effect": "analytic" }, "anger_point": { - "effect": "AngerPoint" + "effect": "anger_point" }, "anticipation": { - "effect": "Anticipation" + "effect": "anticipation" }, "arena_trap": { - "effect": "ArenaTrap" + "effect": "arena_trap" }, "aroma_veil": { - "effect": "AromaVeil" + "effect": "aroma_veil" }, "aura_break": { - "effect": "AuraBreal" + "effect": "aura_break" }, "bad_dreams": { - "effect": "BadDreams" + "effect": "bad_dreams" }, "battery": { - "effect": "Battery" + "effect": "battery" }, "battle_armor": { - "effect": "PreventCritical" + "effect": "prevent_critical" }, "battle_bond": { - "effect": "BattleBond", - "flags": ["cant_be_changed"] + "effect": "battle_bond", + "canBeChanged": false, + "flags": [ + "cant_be_copied" + ] }, "beast_boost": { - "effect": "BeastBoost" + "effect": "beast_boost" }, "berserk": { - "effect": "Berserk" + "effect": "berserk" }, "big_pecks": { - "effect": "PreventDefLowering" + "effect": "prevent_stat_lowering", + "parameters": { + "stat": "defense" + } }, "blaze": { - "effect": "PowerUpType", + "effect": "power_up_type_at_low_health", "parameters": { - "type": "fire" + "type": "fire", + "threshold": 0.33333 } }, "bulletproof": { - "effect": "Bulletproof" + "effect": "bulletproof" }, "cheek_pouch": { "effect": "CheekPouch" @@ -83,7 +90,7 @@ "effect": "ColorChange" }, "comatose": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "competitive": {}, "compound_eyes": {}, @@ -100,7 +107,10 @@ "delta_stream": {}, "desolate_land": {}, "disguise": { - "flags": ["cant_be_changed", "cant_be_copied"] + "canBeChanged": false, + "flags": [ + "cant_be_copied" + ] }, "download": {}, "drizzle": {}, @@ -116,12 +126,16 @@ "flare_boost": {}, "flash_fire": {}, "flower_gift": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "flower_veil": {}, "fluffy": {}, "forecast": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "forewarn": {}, "friend_guard": {}, @@ -147,11 +161,15 @@ "ice_body": {}, "illuminate": {}, "illusion": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "immunity": {}, "imposter": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "infiltrator": {}, "innards_out": {}, @@ -187,7 +205,7 @@ "moxie": {}, "multiscale": {}, "multitype": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "mummy": {}, "natural_cure": {}, @@ -206,10 +224,15 @@ "poison_point": {}, "poison_touch": {}, "power_construct": { - "flags": ["cant_be_changed", "cant_be_copied"] + "canBeChanged": false, + "flags": [ + "cant_be_copied" + ] }, "power_of_alchemy": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "prankster": {}, "pressure": {}, @@ -223,14 +246,16 @@ "rain_dish": {}, "rattled": {}, "receiver": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "reckless": {}, "refrigerate": {}, "regenerator": {}, "rivalry": {}, "rks_system": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "rock_head": {}, "rough_skin": {}, @@ -241,7 +266,7 @@ "sand_veil": {}, "sap_sipper": {}, "schooling": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "scrappy": {}, "serene_grace": {}, @@ -252,7 +277,7 @@ "shell_armor": {}, "shield_dust": {}, "shields_down": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "simple": {}, "skill_link": {}, @@ -270,7 +295,7 @@ "stall": {}, "stamina": {}, "stance_change": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "static": {}, "steadfast": {}, @@ -299,11 +324,13 @@ "tough_claws": {}, "toxic_boost": {}, "trace": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] }, "triage": {}, "truant": { - "flags": ["cant_be_changed"] + "canBeChanged": false }, "turboblaze": {}, "unaware": {}, @@ -322,6 +349,8 @@ "wonder_guard": {}, "wonder_skin": {}, "zen_mode": { - "flags": ["cant_be_copied"] + "flags": [ + "cant_be_copied" + ] } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Aftermath.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Aftermath.cs new file mode 100644 index 0000000..7eee9eb --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Aftermath.cs @@ -0,0 +1,33 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "aftermath")] +public class Aftermath : Script +{ + private IExecutingMove? _lastAttack; + + /// + public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit) + { + _lastAttack = move; + } + + /// + public override void OnFaint(IPokemon pokemon, DamageSource source) + { + if (source != DamageSource.MoveDamage) + return; + if (_lastAttack is null || !_lastAttack.UseMove.HasFlag("contact")) + return; + var user = _lastAttack.User; + if (!user.IsUsable) + return; + if (user.BattleData is null) + return; + EventBatchId eventBatchId = new(); + user.BattleData.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon) + { + BatchId = eventBatchId, + }); + user.Damage(user.MaxHealth / 4, DamageSource.Misc, eventBatchId); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Analytic.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Analytic.cs new file mode 100644 index 0000000..d6d55ed --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Analytic.cs @@ -0,0 +1,15 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "analytic")] +public class Analytic : Script +{ + /// + public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier) + { + if (move.Battle.ChoiceQueue?.HasNext() == false) + { + move.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User)); + modifier *= 1.3f; + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AngerPoint.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AngerPoint.cs new file mode 100644 index 0000000..763b4eb --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AngerPoint.cs @@ -0,0 +1,19 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "anger_point")] +public class AngerPoint : Script +{ + /// + public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit) + { + if (move.GetHitData(target, hit).IsCritical) + { + EventBatchId batchId = new(); + move.Battle.EventHook.Invoke(new AbilityTriggerEvent(target) + { + BatchId = batchId, + }); + target.ChangeStatBoost(Statistic.Attack, 12, true, batchId); + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Anticipation.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Anticipation.cs new file mode 100644 index 0000000..bc073b5 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Anticipation.cs @@ -0,0 +1,39 @@ +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "anticipation")] +public class Anticipation : Script +{ + private IPokemon? _owner; + + /// + public override void OnAddedToParent(IScriptSource source) + { + if (source is not IPokemon pokemon) + throw new ArgumentException("Anticipation script can only be added to a Pokemon.", nameof(source)); + _owner = pokemon; + } + + /// + public override void OnOpponentSwitchIn(IPokemon pokemon, byte position) + { + if (_owner is null) + return; + var pokemonMoves = pokemon.Moves.WhereNotNull(); + var typeLibrary = pokemon.Library.StaticLibrary.Types; + var relevantMoves = pokemonMoves.Any(move => + // Either the move is super effective against the owner or + typeLibrary.GetEffectiveness(move.MoveData.MoveType, _owner.Types) > 1.0f || + // the move is a OHKO move + move.MoveData.SecondaryEffect?.Name == "one_hit_ko" || + // the move is a self-destruct move + move.MoveData.SecondaryEffect?.Name == "self_destruct" || + move.MoveData.SecondaryEffect?.Name == "explosion"); + + if (relevantMoves) + { + pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(_owner)); + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ArenaTrap.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ArenaTrap.cs new file mode 100644 index 0000000..c8f4b8c --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ArenaTrap.cs @@ -0,0 +1,35 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "arena_trap")] +public class ArenaTrap : Script +{ + private IPokemon? _owner; + + /// + public override void OnAddedToParent(IScriptSource source) + { + if (source is not IPokemon pokemon) + throw new InvalidOperationException("ArenaTrap can only be added to a Pokemon."); + _owner = pokemon; + } + + /// + public override void PreventOpponentRunAway(IFleeChoice choice, ref bool prevent) + { + if (choice.User.IsFloating) + return; + if (_owner is not null) + choice.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(_owner)); + prevent = true; + } + + /// + public override void PreventOpponentSwitch(ISwitchChoice choice, ref bool prevent) + { + if (choice.User.IsFloating) + return; + if (_owner is not null) + choice.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(_owner)); + prevent = true; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AromaVeil.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AromaVeil.cs new file mode 100644 index 0000000..1a34111 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AromaVeil.cs @@ -0,0 +1,21 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "aroma_veil")] +public class AromaVeil : Script +{ + /// + public override void OnSwitchIn(IPokemon pokemon, byte position) + { + var side = pokemon.BattleData?.BattleSide; + var effect = side?.VolatileScripts.Add(new Side.AromaVeilEffect())?.Script as Side.AromaVeilEffect; + effect?.PlacerActivated(pokemon); + } + + /// + public override void OnSwitchOut(IPokemon oldPokemon, byte position) + { + var side = oldPokemon.BattleData?.BattleSide; + var effect = side?.VolatileScripts.Get(); + effect?.PlacerDeactivated(oldPokemon); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AuraBreak.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AuraBreak.cs new file mode 100644 index 0000000..f7d20e0 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/AuraBreak.cs @@ -0,0 +1,7 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "aura_break")] +public class AuraBreak : Script +{ + // FIXME: Implement together with Dark Aura and Fairy Aura. +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BadDreams.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BadDreams.cs new file mode 100644 index 0000000..5f15005 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BadDreams.cs @@ -0,0 +1,35 @@ +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "bad_dreams")] +public class BadDreams : Script +{ + private IPokemon? _owner; + + /// + public override void OnAddedToParent(IScriptSource source) + { + if (source is not IPokemon pokemon) + throw new InvalidOperationException("Bad Dreams ability can only be added to a Pokemon."); + _owner = pokemon; + } + + /// + public override void OnEndTurn(IBattle battle) + { + if (_owner is null) + return; + var opponents = battle.Sides.Where(x => x != _owner?.BattleData?.BattleSide).SelectMany(x => x.Pokemon) + .WhereNotNull(); + + foreach (var opponent in opponents) + { + if (!opponent.HasStatus(ScriptUtils.ResolveName())) + continue; + EventBatchId batchId = new(); + battle.EventHook.Invoke(new AbilityTriggerEvent(_owner)); + opponent.Damage(opponent.MaxHealth / 8, DamageSource.Misc, batchId); + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Battery.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Battery.cs new file mode 100644 index 0000000..5353b14 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Battery.cs @@ -0,0 +1,21 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "battery")] +public class Battery : Script +{ + /// + public override void OnSwitchIn(IPokemon pokemon, byte position) + { + var side = pokemon.BattleData?.BattleSide; + var effect = side?.VolatileScripts.Add(new Side.BatteryAbilityEffect())?.Script as Side.BatteryAbilityEffect; + effect?.PlacerActivated(pokemon); + } + + /// + public override void OnSwitchOut(IPokemon oldPokemon, byte position) + { + var side = oldPokemon.BattleData?.BattleSide; + var effect = side?.VolatileScripts.Get(); + effect?.PlacerDeactivated(oldPokemon); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BattleBond.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BattleBond.cs new file mode 100644 index 0000000..03d5684 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BattleBond.cs @@ -0,0 +1,32 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "battle_bond")] +public class BattleBond : Script +{ + /// + public override void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit) + { + if (move.User.Species.Name == "greninja" && move.User.Form.Name != "ash" && + move.User.Species.TryGetForm("ash", out var ashForm)) + { + move.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User)); + move.User.ChangeForm(ashForm); + } + } + + /// + public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower) + { + if (move.UseMove.Name == "water_shuriken" && move.User.Form.Name == "ash") + basePower = 20; + } + + /// + public override void ChangeNumberOfHits(IMoveChoice choice, ref byte numberOfHits) + { + if (choice.ChosenMove.MoveData.Name == "water_shuriken" && choice.User.Form.Name == "ash") + { + numberOfHits = 3; + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BeastBoost.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BeastBoost.cs new file mode 100644 index 0000000..5242670 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/BeastBoost.cs @@ -0,0 +1,17 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "beast_boost")] +public class BeastBoost : Script +{ + /// + public override void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit) + { + var highestStat = move.User.BoostedStats.OrderByDescending(x => x.value).First().statistic; + EventBatchId batchId = new(); + move.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User) + { + BatchId = batchId, + }); + move.User.ChangeStatBoost(highestStat, 1, true, batchId); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Berserk.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Berserk.cs new file mode 100644 index 0000000..1aed60e --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Berserk.cs @@ -0,0 +1,21 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "berserk")] +public class Berserk : Script +{ + /// + public override void OnDamage(IPokemon pokemon, DamageSource source, uint oldHealth, uint newHealth) + { + if (source is not DamageSource.MoveDamage) + return; + if (oldHealth > pokemon.MaxHealth / 2 || newHealth > pokemon.MaxHealth / 2) + return; + + EventBatchId batchId = new(); + pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon) + { + BatchId = batchId, + }); + pokemon.ChangeStatBoost(Statistic.SpecialAttack, 1, true, batchId); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Bulletproof.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Bulletproof.cs new file mode 100644 index 0000000..89a9a35 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/Bulletproof.cs @@ -0,0 +1,12 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "bulletproof")] +public class Bulletproof : Script +{ + /// + public override void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail) + { + if (move.UseMove.HasFlag("ballistics")) + fail = true; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ChangeMoveTypeAbility.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ChangeMoveTypeAbility.cs new file mode 100644 index 0000000..c1bf74a --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/ChangeMoveTypeAbility.cs @@ -0,0 +1,47 @@ +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "change_move_type")] +public class ChangeMoveTypeAbility : Script +{ + private StringKey _fromType; + private StringKey _toType; + + /// + public override void OnInitialize(IReadOnlyDictionary? parameters) + { + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + + if (!parameters.TryGetValue("from_type", out var fromTypeObj) || fromTypeObj is not string fromType) + throw new ArgumentException("Missing 'from_type' parameter.", nameof(parameters)); + if (!parameters.TryGetValue("to_type", out var toTypeObj) || toTypeObj is not string toType) + throw new ArgumentException("Missing 'to_type' parameter.", nameof(parameters)); + _fromType = fromType; + _toType = toType; + } + + /// + public override void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, + ref TypeIdentifier? typeIdentifier) + { + var typeLibrary = target.Library.StaticLibrary.Types; + // Both types must be valid and the current type must match the from type + if (!typeLibrary.TryGetTypeIdentifier(_fromType, out var fromType) || + !typeLibrary.TryGetTypeIdentifier(_toType, out var toType) || typeIdentifier != fromType) + { + return; + } + move.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User)); + typeIdentifier = toType; + move.GetHitData(target, hit).SetFlag("change_move_type_ability"); + } + + /// + public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower) + { + if (move.GetHitData(target, hit).HasFlag("change_move_type_ability")) + basePower = basePower.MultiplyOrMax(1.3f); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/IncreasedStabAbility.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/IncreasedStabAbility.cs new file mode 100644 index 0000000..f8bb36f --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/IncreasedStabAbility.cs @@ -0,0 +1,18 @@ +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "increased_stab")] +public class IncreasedStab : Script +{ + /// + public override void ChangeStabModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, + ref float modifier) + { + if (modifier.IsApproximatelyEqualTo(1.5f)) + { + executingMove.Battle.EventHook.Invoke(new AbilityTriggerEvent(executingMove.User)); + modifier = 2.0f; + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PowerUpTypeAtLowHealth.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PowerUpTypeAtLowHealth.cs new file mode 100644 index 0000000..6ee1199 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PowerUpTypeAtLowHealth.cs @@ -0,0 +1,38 @@ +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "power_up_type_at_low_health")] +public class PowerUpTypeAtLowHealth : Script +{ + private StringKey _type; + private float _threshold; + + /// + public override void OnInitialize(IReadOnlyDictionary? parameters) + { + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + + if (!parameters.TryGetValue("type", out var type) || type is not string typeName) + throw new ArgumentException("Parameter 'type' is required and must be a string.", nameof(parameters)); + if (!parameters.TryGetValue("threshold", out var threshold) || threshold is not float thresholdValue) + throw new ArgumentException("Parameter 'threshold' is required and must be a float.", nameof(parameters)); + + if (thresholdValue < 0 || thresholdValue > 1) + throw new ArgumentOutOfRangeException(nameof(threshold), "Threshold must be between 0 and 1."); + + _type = typeName; + _threshold = thresholdValue; + } + + /// + public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier) + { + var currentHealthFraction = move.User.CurrentHealth / (float)move.User.MaxHealth; + if (currentHealthFraction <= _threshold && move.GetHitData(target, hit).Type?.Name == _type) + { + modifier *= 1.5f; + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventCritical.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventCritical.cs new file mode 100644 index 0000000..8400d24 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventCritical.cs @@ -0,0 +1,9 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "prevent_critical")] +public class PreventCritical : Script +{ + /// + public override void BlockIncomingCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block) => + block = true; +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventStatLowering.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventStatLowering.cs new file mode 100644 index 0000000..e501b83 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/PreventStatLowering.cs @@ -0,0 +1,30 @@ +using PkmnLib.Static.Utils; + +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "prevent_stat_lowering")] +public class PreventStatLowering : Script +{ + private Statistic _statistic; + + /// + public override void OnInitialize(IReadOnlyDictionary? parameters) + { + if (parameters is null) + throw new ArgumentNullException(nameof(parameters), "Parameters cannot be null."); + if (!parameters.TryGetValue("stat", out var statObj) || statObj is not string statStr) + throw new ArgumentException("Parameter 'stat' is required and must be a string.", nameof(parameters)); + if (!Enum.TryParse(statStr, true, out Statistic stat)) + throw new ArgumentException($"Invalid statistic '{statStr}' provided.", nameof(statStr)); + + _statistic = stat; + } + + /// + public override void PreventStatBoostChange(IPokemon target, Statistic stat, sbyte amount, bool selfInflicted, + ref bool prevent) + { + if (!selfInflicted) + prevent = false; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/SuppressWeatherAbility.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/SuppressWeatherAbility.cs new file mode 100644 index 0000000..cfb6238 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Abilities/SuppressWeatherAbility.cs @@ -0,0 +1,18 @@ +namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; + +[Script(ScriptCategory.Ability, "suppress_weather")] +public class SuppressWeatherAbility : Script +{ + /// + public override void OnBeforeAnyHookInvoked(ref List? suppressedCategories) + { + suppressedCategories ??= []; + suppressedCategories.Add(ScriptCategory.Weather); + } + + /// + public override void OnSwitchIn(IPokemon pokemon, byte position) + { + pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon)); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/AromaVeilEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/AromaVeilEffect.cs new file mode 100644 index 0000000..f93825f --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/AromaVeilEffect.cs @@ -0,0 +1,32 @@ +using PkmnLib.Static.Moves; + +namespace PkmnLib.Plugin.Gen7.Scripts.Side; + +[Script(ScriptCategory.Side, "aroma_veil")] +public class AromaVeilEffect : Script +{ + private HashSet _placers = new(); + + public void PlacerActivated(IPokemon placer) => _placers.Add(placer); + + public void PlacerDeactivated(IPokemon placer) + { + _placers.Remove(placer); + if (_placers.Count == 0) + RemoveSelf(); + } + + /// + public override void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail) + { + if (move.UseMove.HasFlag("mental") && move.UseMove.Category == MoveCategory.Status) + fail = true; + } + + /// + public override void PreventSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent) + { + if (move.UseMove.HasFlag("mental")) + prevent = true; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/BatteryAbilityEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/BatteryAbilityEffect.cs new file mode 100644 index 0000000..e098431 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Side/BatteryAbilityEffect.cs @@ -0,0 +1,27 @@ +using PkmnLib.Static.Moves; + +namespace PkmnLib.Plugin.Gen7.Scripts.Side; + +[Script(ScriptCategory.Side, "battery")] +public class BatteryAbilityEffect : Script +{ + private HashSet _placers = new(); + + public void PlacerActivated(IPokemon placer) => _placers.Add(placer); + + public void PlacerDeactivated(IPokemon placer) + { + _placers.Remove(placer); + if (_placers.Count == 0) + RemoveSelf(); + } + + /// + public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier) + { + if (move.UseMove.Category == MoveCategory.Special) + { + modifier *= 5325f / 4096f; // ~1.3x + } + } +} \ No newline at end of file