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