diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs
index 18713cc..ce9983b 100644
--- a/PkmnLib.Dynamic/Models/Pokemon.cs
+++ b/PkmnLib.Dynamic/Models/Pokemon.cs
@@ -252,12 +252,18 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// The stat to be changed
/// The amount to change the stat by
/// Whether the change was self-inflicted. This can be relevant in scripts.
- bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted);
+ /// The event batch ID this change is a part of. This is relevant for visual handling
+ bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, EventBatchId batchId = default);
+ ///
+ /// Suppresses the ability of the Pokémon.
+ ///
+ public void SuppressAbility();
+
///
/// Returns the currently active ability.
///
- IAbility ActiveAbility { get; }
+ IAbility? ActiveAbility { get; }
///
/// Calculates the flat stats on the Pokemon. This should be called when for example the base
@@ -362,6 +368,11 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// the Pokémon already has it.
///
bool AddType(TypeIdentifier type);
+
+ ///
+ /// Replace the types of the Pokémon with the provided types.
+ ///
+ void SetTypes(IReadOnlyList types);
///
/// Converts the data structure to a serializable format.
@@ -714,7 +725,7 @@ public class PokemonImpl : ScriptSource, IPokemon
}
///
- public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted)
+ public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, EventBatchId batchId = default)
{
var prevented = false;
this.RunScriptHook(script => script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
@@ -736,18 +747,34 @@ public class PokemonImpl : ScriptSource, IPokemon
if (BattleData != null)
{
var newBoost = StatBoost.GetStatistic(stat);
- BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost));
+ BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost)
+ {
+ BatchId = batchId,
+ });
}
RecalculateBoostedStats();
return true;
}
+
+ ///
+ /// Whether the ability of the Pokémon is suppressed.
+ ///
+ public bool AbilitySuppressed { get; private set; }
+
+ ///
+ public void SuppressAbility()
+ {
+ OverrideAbility = null;
+ }
///
- public IAbility ActiveAbility
+ public IAbility? ActiveAbility
{
get
{
+ if (AbilitySuppressed)
+ return null;
if (OverrideAbility != null)
return OverrideAbility;
var ability = Form.GetAbility(AbilityIndex);
@@ -1004,6 +1031,9 @@ public class PokemonImpl : ScriptSource, IPokemon
Volatile.Clear();
WeightInKg = Form.Weight;
HeightInMeters = Form.Height;
+ Types = Form.Types;
+ OverrideAbility = null;
+ AbilitySuppressed = false;
}
}
}
@@ -1032,6 +1062,12 @@ public class PokemonImpl : ScriptSource, IPokemon
return true;
}
+ ///
+ public void SetTypes(IReadOnlyList types)
+ {
+ _types = types.ToList();
+ }
+
///
public SerializedPokemon Serialize() => new(this);
diff --git a/PkmnLib.Static/Libraries/TypeLibrary.cs b/PkmnLib.Static/Libraries/TypeLibrary.cs
index 9886bac..9123d36 100644
--- a/PkmnLib.Static/Libraries/TypeLibrary.cs
+++ b/PkmnLib.Static/Libraries/TypeLibrary.cs
@@ -28,6 +28,8 @@ public interface IReadOnlyTypeLibrary
/// This is equivalent to running get_single_effectiveness on each defending type, and multiplying the results with each other.
///
float GetEffectiveness(TypeIdentifier attacking, IReadOnlyList defending);
+
+ IEnumerable<(TypeIdentifier type, float effectiveness)> GetAllEffectivenessFromAttacking(TypeIdentifier attacking);
}
///
@@ -70,6 +72,17 @@ public class TypeLibrary : IReadOnlyTypeLibrary
defending.Aggregate(1,
(current, type) => current * GetSingleEffectiveness(attacking, type));
+ ///
+ public IEnumerable<(TypeIdentifier, float)> GetAllEffectivenessFromAttacking(TypeIdentifier attacking)
+ {
+ if (attacking.Value < 1 || attacking.Value > _effectiveness.Count)
+ throw new ArgumentOutOfRangeException(nameof(attacking));
+ for (var i = 0; i < _effectiveness.Count; i++)
+ {
+ yield return (new TypeIdentifier((byte)(i + 1)), _effectiveness[attacking.Value - 1][i]);
+ }
+ }
+
///
/// Registers a new type in the library.
///
diff --git a/PkmnLib.Static/Statistic.cs b/PkmnLib.Static/Statistic.cs
index c21dd50..b6e4e5a 100644
--- a/PkmnLib.Static/Statistic.cs
+++ b/PkmnLib.Static/Statistic.cs
@@ -29,4 +29,16 @@ public enum Statistic : byte
/// Speed determines the order that a Pokémon can act in battle.
///
Speed,
+
+ ///
+ /// Evasion determines the likelihood that a Pokémon will dodge an attack.
+ /// This is not part of base stats, but is a temporary stat boost.
+ ///
+ Evasion,
+
+ ///
+ /// Accuracy determines the likelihood that a Pokémon will hit an attack.
+ /// This is not part of base stats, but is a temporary stat boost.
+ ///
+ Accuracy,
}
\ No newline at end of file
diff --git a/PkmnLib.Static/StatisticSet.cs b/PkmnLib.Static/StatisticSet.cs
index 1e69186..e6457f3 100644
--- a/PkmnLib.Static/StatisticSet.cs
+++ b/PkmnLib.Static/StatisticSet.cs
@@ -168,12 +168,24 @@ public record StatisticSet : ImmutableStatisticSet, IEnumerable, IDeepC
Speed = Add(Speed, value);
break;
default:
- throw new ArgumentException("Invalid statistic.");
+ SetUnknownStat(stat, Add(GetUnknownStat(stat), value));
+ break;
}
return true;
}
+ protected virtual T GetUnknownStat(Statistic stat)
+ {
+ throw new ArgumentException($"Invalid statistic {stat}");
+ }
+
+ protected virtual void SetUnknownStat(Statistic stat, T value)
+ {
+ throw new ArgumentException($"Invalid statistic {stat}");
+ }
+
+
///
/// Decreases a statistic in the set by a value.
///
@@ -200,14 +212,15 @@ public record StatisticSet : ImmutableStatisticSet, IEnumerable, IDeepC
Speed = Subtract(Speed, value);
break;
default:
- throw new ArgumentException("Invalid statistic.");
+ SetUnknownStat(stat, Subtract(GetUnknownStat(stat), value));
+ break;
}
return true;
}
///
- public IEnumerator GetEnumerator()
+ public virtual IEnumerator GetEnumerator()
{
yield return Hp;
yield return Attack;
@@ -332,6 +345,47 @@ public record StatBoostStatisticSet : ClampedStatisticSet
sbyte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
{
}
+
+ ///
+ protected override sbyte GetUnknownStat(Statistic stat)
+ {
+ return stat switch
+ {
+ Statistic.Evasion => Evasion,
+ Statistic.Accuracy => Accuracy,
+ _ => throw new ArgumentException($"Invalid statistic {stat}"),
+ };
+ }
+
+ ///
+ ///
+ protected override void SetUnknownStat(Statistic stat, sbyte value)
+ {
+ switch (stat)
+ {
+ case Statistic.Evasion:
+ Evasion = value;
+ break;
+ case Statistic.Accuracy:
+ Accuracy = value;
+ break;
+ default:
+ throw new ArgumentException($"Invalid statistic {stat}");
+ }
+ }
+
+ ///
+ public override IEnumerator GetEnumerator()
+ {
+ yield return Hp;
+ yield return Attack;
+ yield return Defense;
+ yield return SpecialAttack;
+ yield return SpecialDefense;
+ yield return Speed;
+ yield return Evasion;
+ yield return Accuracy;
+ }
}
///
diff --git a/PkmnLib.Tests/Data/Moves.json b/PkmnLib.Tests/Data/Moves.json
index f5b1225..a32881c 100755
--- a/PkmnLib.Tests/Data/Moves.json
+++ b/PkmnLib.Tests/Data/Moves.json
@@ -1424,7 +1424,10 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "calm_mind"
+ }
},
{
"name": "camouflage",
@@ -1437,7 +1440,10 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "camouflage"
+ }
},
{
"name": "captivate",
@@ -1452,7 +1458,10 @@
"protect",
"reflectable",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "captivate"
+ }
},
{
"name": "catastropika",
@@ -1476,7 +1485,10 @@
"priority": 0,
"target": "Self",
"category": "status",
- "flags": []
+ "flags": [],
+ "effect": {
+ "name": "celebrate"
+ }
},
{
"name": "charge",
@@ -1489,7 +1501,10 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "charge"
+ }
},
{
"name": "charge_beam",
@@ -1503,7 +1518,14 @@
"flags": [
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "change_user_special_attack",
+ "chance": 70,
+ "parameters": {
+ "amount": 1
+ }
+ }
},
{
"name": "charm",
@@ -1518,7 +1540,13 @@
"protect",
"reflectable",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "change_target_attack",
+ "parameters": {
+ "amount": -2
+ }
+ }
},
{
"name": "chatter",
@@ -1535,7 +1563,10 @@
"sound",
"distance",
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "confuse"
+ }
},
{
"name": "chip_away",
@@ -1550,7 +1581,10 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "chip_away"
+ }
},
{
"name": "circle_throw",
@@ -1565,7 +1599,10 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "circle_throw"
+ }
},
{
"name": "clamp",
@@ -1580,7 +1617,10 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "bind"
+ }
},
{
"name": "clanging_scales",
@@ -1596,7 +1636,13 @@
"mirror",
"sound",
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "change_user_defense",
+ "parameters": {
+ "amount": -1
+ }
+ }
},
{
"name": "clear_smog",
@@ -1610,7 +1656,10 @@
"flags": [
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "reset_target_stats"
+ }
},
{
"name": "close_combat",
@@ -1625,7 +1674,13 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "change_user_defense",
+ "parameters": {
+ "amount": -1
+ }
+ }
},
{
"name": "coil",
@@ -1638,7 +1693,10 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "coil"
+ }
},
{
"name": "comet_punch",
@@ -1654,7 +1712,10 @@
"protect",
"mirror",
"punch"
- ]
+ ],
+ "effect": {
+ "name": "2_5_hit_move"
+ }
},
{
"name": "confide",
@@ -1670,7 +1731,13 @@
"mirror",
"sound",
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "change_target_special_attack",
+ "parameters": {
+ "amount": -1
+ }
+ }
},
{
"name": "confuse_ray",
@@ -1685,7 +1752,10 @@
"protect",
"reflectable",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "confuse"
+ }
},
{
"name": "confusion",
@@ -1699,7 +1769,11 @@
"flags": [
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "confuse",
+ "chance": 10
+ }
},
{
"name": "constrict",
@@ -1714,7 +1788,14 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "change_target_speed",
+ "chance": 10,
+ "parameters": {
+ "amount": -1
+ }
+ }
},
{
"name": "continental_crush__physical",
@@ -1749,7 +1830,10 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "conversion"
+ }
},
{
"name": "conversion_2",
@@ -1762,7 +1846,10 @@
"category": "status",
"flags": [
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "conversion_2"
+ }
},
{
"name": "copycat",
@@ -1773,7 +1860,10 @@
"priority": 0,
"target": "Self",
"category": "status",
- "flags": []
+ "flags": [],
+ "effect": {
+ "name": "copycat"
+ }
},
{
"name": "core_enforcer",
@@ -1787,7 +1877,10 @@
"flags": [
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "core_enforcer"
+ }
},
{
"name": "corkscrew_crash__physical",
@@ -1822,7 +1915,10 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "cosmic_power"
+ }
},
{
"name": "cotton_guard",
@@ -1835,7 +1931,13 @@
"category": "status",
"flags": [
"snatch"
- ]
+ ],
+ "effect": {
+ "name": "change_user_defense",
+ "parameters": {
+ "amount": 3
+ }
+ }
},
{
"name": "cotton_spore",
@@ -1851,7 +1953,13 @@
"reflectable",
"mirror",
"powder"
- ]
+ ],
+ "effect": {
+ "name": "change_target_speed",
+ "parameters": {
+ "amount": -2
+ }
+ }
},
{
"name": "counter",
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BulkUp.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BulkUp.cs
index 37832ce..dc79cc1 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BulkUp.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BulkUp.cs
@@ -8,7 +8,8 @@ public class BulkUp : Script
///
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
- move.User.ChangeStatBoost(Statistic.Attack, 1, true);
- move.User.ChangeStatBoost(Statistic.Defense, 1, true);
+ EventBatchId eventBatchId = new();
+ move.User.ChangeStatBoost(Statistic.Attack, 1, true, eventBatchId);
+ move.User.ChangeStatBoost(Statistic.Defense, 1, true, eventBatchId);
}
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CalmMind.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CalmMind.cs
new file mode 100644
index 0000000..27688f2
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CalmMind.cs
@@ -0,0 +1,15 @@
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "calm_mind")]
+public class CalmMind : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ EventBatchId eventBatchId = new();
+ move.User.ChangeStatBoost(Statistic.SpecialAttack, 1, true, eventBatchId);
+ move.User.ChangeStatBoost(Statistic.SpecialDefense, 1, true, eventBatchId);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Camouflage.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Camouflage.cs
new file mode 100644
index 0000000..a5d1721
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Camouflage.cs
@@ -0,0 +1,7 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "camouflage")]
+public class Camouflage : Script
+{
+ // FIXME: Implement this. How to get the terrain in a sane manner?
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs
new file mode 100644
index 0000000..8843f2c
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Captivate.cs
@@ -0,0 +1,25 @@
+using PkmnLib.Static;
+using PkmnLib.Static.Species;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "captivate")]
+public class Captivate : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ var user = move.User;
+ if (target.Gender == Gender.Genderless || target.Gender == user.Gender)
+ {
+ move.GetHitData(target, hit).Fail();
+ return;
+ }
+ if (target.ActiveAbility?.Name == "oblivious")
+ {
+ move.GetHitData(target, hit).Fail();
+ return;
+ }
+ target.ChangeStatBoost(Statistic.SpecialAttack, -2, false);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Celebrate.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Celebrate.cs
new file mode 100644
index 0000000..ffeb81b
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Celebrate.cs
@@ -0,0 +1,13 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "celebrate")]
+public class Celebrate : Script
+{
+ ///
+ public override void PreventMoveSelection(IMoveChoice choice, ref bool prevent)
+ {
+ // This move is mostly useless, and it's not worth the effort to implement it.
+ // Prevent it from being selected.
+ prevent = true;
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs
new file mode 100644
index 0000000..a6a0509
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChangeUserStats.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using PkmnLib.Static;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+public abstract class ChangeUserStats : Script
+{
+ private readonly Statistic _stat;
+ private sbyte _amount;
+
+ protected ChangeUserStats(Statistic stat)
+ {
+ _stat = stat;
+ }
+
+ ///
+ public override void OnInitialize(IReadOnlyDictionary? parameters)
+ {
+ if (parameters == null)
+ {
+ throw new ArgumentNullException(nameof(parameters));
+ }
+
+ if (!parameters.TryGetValue("amount", out var amount) || amount == null)
+ {
+ throw new ArgumentException("Parameter 'amount' is required.");
+ }
+
+ _amount = (sbyte)amount;
+ }
+
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ move.User.ChangeStatBoost(_stat, _amount, true);
+ }
+}
+
+[Script(ScriptCategory.Move, "change_user_attack")]
+public class ChangeUserAttack : ChangeUserStats
+{
+ public ChangeUserAttack() : base(Statistic.Attack)
+ {
+ }
+}
+
+[Script(ScriptCategory.Move, "change_user_defense")]
+public class ChangeUserDefense : ChangeUserStats
+{
+ public ChangeUserDefense() : base(Statistic.Defense)
+ {
+ }
+}
+
+[Script(ScriptCategory.Move, "change_user_special_attack")]
+public class ChangeUserSpecialAttack : ChangeUserStats
+{
+ public ChangeUserSpecialAttack() : base(Statistic.SpecialAttack)
+ {
+ }
+}
+
+[Script(ScriptCategory.Move, "change_user_special_defense")]
+public class ChangeUserSpecialDefense : ChangeUserStats
+{
+ public ChangeUserSpecialDefense() : base(Statistic.SpecialDefense)
+ {
+ }
+}
+
+[Script(ScriptCategory.Move, "change_user_speed")]
+public class ChangeUserSpeed : ChangeUserStats
+{
+ public ChangeUserSpeed() : base(Statistic.Speed)
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Charge.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Charge.cs
new file mode 100644
index 0000000..6ec9fce
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Charge.cs
@@ -0,0 +1,15 @@
+using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "charge")]
+public class Charge : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ move.User.ChangeStatBoost(Statistic.SpecialDefense, 1, true);
+ move.User.Volatile.Add(new ChargeEffect());
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChipAway.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChipAway.cs
new file mode 100644
index 0000000..7b7ec1e
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ChipAway.cs
@@ -0,0 +1,13 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "chip_away")]
+public class ChipAway : Script
+{
+ ///
+ public override void BypassDefensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass)
+ {
+ bypass = true;
+ }
+
+ // TODO: bypass evasion stat.
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CircleThrow.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CircleThrow.cs
new file mode 100644
index 0000000..24a92e5
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CircleThrow.cs
@@ -0,0 +1,7 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "circle_throw")]
+public class CircleThrow : Script
+{
+ // TODO: Implement this. How to handle forced switch?
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CloseCombat.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CloseCombat.cs
new file mode 100644
index 0000000..18662bf
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CloseCombat.cs
@@ -0,0 +1,15 @@
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "close_combat")]
+public class CloseCombat : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ EventBatchId eventBatchId = new();
+ move.User.ChangeStatBoost(Statistic.Defense, -1, true, eventBatchId);
+ move.User.ChangeStatBoost(Statistic.SpecialDefense, -1, true, eventBatchId);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Coil.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Coil.cs
new file mode 100644
index 0000000..cc383a9
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Coil.cs
@@ -0,0 +1,16 @@
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "coil")]
+public class Coil : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ EventBatchId eventBatchId = new();
+ move.User.ChangeStatBoost(Statistic.Attack, 1, true, eventBatchId);
+ move.User.ChangeStatBoost(Statistic.Defense, 1, true, eventBatchId);
+ move.User.ChangeStatBoost(Statistic.Accuracy, 1, true, eventBatchId);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Confuse.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Confuse.cs
new file mode 100644
index 0000000..47a259b
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Confuse.cs
@@ -0,0 +1,13 @@
+using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "confuse")]
+public class Confuse : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ target.Volatile.Add(new Confusion());
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion.cs
new file mode 100644
index 0000000..2607654
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion.cs
@@ -0,0 +1,20 @@
+using System.Linq;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "conversion")]
+public class Conversion : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ var moveType = move.User.Moves.WhereNotNull().FirstOrDefault()?.MoveData.MoveType;
+ if (moveType == null)
+ {
+ move.GetHitData(target, hit).Fail();
+ return;
+ }
+ move.User.SetTypes([moveType.Value]);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs
new file mode 100644
index 0000000..69f9c08
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Conversion2.cs
@@ -0,0 +1,57 @@
+using System.Linq;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "conversion_2")]
+public class Conversion2 : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ var previousTurnChoices = target.BattleData?.Battle.PreviousTurnChoices;
+ var nextExecutingChoice = target.BattleData?.Battle.ChoiceQueue?.Peek();
+ var lastMoveByTarget = previousTurnChoices?
+ // The previous turn choices include the choices of the current turn, so we need to have special handling for
+ // the current turn
+ .Select((x, index) =>
+ {
+ // All choices before the current turn are valid
+ if (index < previousTurnChoices.Count - 1)
+ return x;
+ // If there is no next choice, we're at the end of the list, so we can just return the whole list
+ if (nextExecutingChoice == null)
+ return x;
+ // Otherwise we determine where the next choice is and return everything before that
+ var indexOfNext = x.IndexOf(nextExecutingChoice);
+ if (indexOfNext == -1)
+ return x;
+ return x.Take(indexOfNext);
+ })
+ .SelectMany(x => x)
+ // We only want the last move choice by the target
+ .OfType().FirstOrDefault(x => x.User == target);
+ if (lastMoveByTarget == null)
+ {
+ move.GetHitData(target, hit).Fail();
+ return;
+ }
+
+ var typeLibrary = move.User.BattleData!.Battle.Library.StaticLibrary.Types;
+ // Get all types against which the last move would be not very effective
+ var type = typeLibrary.GetAllEffectivenessFromAttacking(lastMoveByTarget.ChosenMove.MoveData.MoveType)
+ .Where(x => x.effectiveness < 1)
+ // Shuffle them randomly, but deterministically
+ .OrderBy(_ => move.User.BattleData.Battle.Random.GetInt())
+ .ThenBy(x => x.type.Value)
+ // And grab the first one
+ .Select(x => x.type)
+ .FirstOrDefault();
+ if (type == null)
+ {
+ move.GetHitData(target, hit).Fail();
+ return;
+ }
+ move.User.SetTypes([type]);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs
new file mode 100644
index 0000000..47a69eb
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Copycat.cs
@@ -0,0 +1,24 @@
+using System.Linq;
+using PkmnLib.Plugin.Gen7.Scripts.Utils;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "copycat")]
+public class Copycat : Script
+{
+ ///
+ public override void ChangeMove(IMoveChoice choice, ref StringKey moveName)
+ {
+ var lastMove = choice.User.BattleData?.Battle.PreviousTurnChoices
+ .SelectMany(x => x)
+ .OfType()
+ .LastOrDefault();
+ if (lastMove == null || !lastMove.ChosenMove.MoveData.CanCopyMove())
+ {
+ choice.Fail();
+ return;
+ }
+ moveName = lastMove.ChosenMove.MoveData.Name;
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs
new file mode 100644
index 0000000..d035787
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CoreEnforcer.cs
@@ -0,0 +1,25 @@
+using System.Linq;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "core_enforcer")]
+public class CoreEnforcer : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ var battleData = target.BattleData;
+ if (battleData == null)
+ return;
+ var turnChoices = battleData.Battle.PreviousTurnChoices.Last();
+ var currentChoiceIndex = turnChoices.IndexOf(move.MoveChoice);
+ if (currentChoiceIndex == -1 ||
+ !turnChoices.Take(currentChoiceIndex).Any(choice => choice is IMoveChoice or IItemChoice))
+ {
+ move.GetHitData(target, hit).Fail();
+ return;
+ }
+ target.SuppressAbility();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CosmicPower.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CosmicPower.cs
new file mode 100644
index 0000000..1ed972f
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/CosmicPower.cs
@@ -0,0 +1,15 @@
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "cosmic_power")]
+public class CosmicPower : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ EventBatchId eventBatchId = new();
+ move.User.ChangeStatBoost(Statistic.Defense, 1, true, eventBatchId);
+ move.User.ChangeStatBoost(Statistic.SpecialDefense, 1, true, eventBatchId);
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ResetTargetStats.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ResetTargetStats.cs
new file mode 100644
index 0000000..a0d6dc4
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ResetTargetStats.cs
@@ -0,0 +1,18 @@
+using System;
+using PkmnLib.Static;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "reset_target_stats")]
+public class ResetTargetStats : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ EventBatchId eventBatchId = new();
+ foreach (Statistic stat in Enum.GetValues(typeof(Statistic)))
+ {
+ target.ChangeStatBoost(stat, (sbyte)-target.StatBoost.GetStatistic(stat), true, eventBatchId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ChargeEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ChargeEffect.cs
new file mode 100644
index 0000000..ef0c135
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ChargeEffect.cs
@@ -0,0 +1,30 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+
+[Script(ScriptCategory.Pokemon, "charge_effect")]
+public class ChargeEffect : Script
+{
+ private bool _turnOfUse = true;
+
+ ///
+ public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
+ {
+ var library = target.BattleData?.Battle.Library;
+ if (library == null)
+ return;
+ if (!library.StaticLibrary.Types.TryGetTypeIdentifier("electric", out var electricType))
+ return;
+ if (move.UseMove.MoveType == electricType)
+ modifier *= 2;
+ }
+
+ ///
+ public override void OnEndTurn(IBattle battle)
+ {
+ if (_turnOfUse)
+ {
+ _turnOfUse = false;
+ return;
+ }
+ RemoveSelf();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/Confusion.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/Confusion.cs
new file mode 100644
index 0000000..6e35836
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/Confusion.cs
@@ -0,0 +1,7 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+
+[Script(ScriptCategory.Pokemon, "confusion")]
+public class Confusion : Script
+{
+ // TODO: Implement confusion
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Weather/Hail.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Weather/Hail.cs
index f81cf64..ac37c7f 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Weather/Hail.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Weather/Hail.cs
@@ -36,7 +36,7 @@ public class Hail : Script
continue;
if (pokemon.Types.Contains(iceType))
continue;
- if (_hailIgnoreAbilities.Contains(pokemon.ActiveAbility.Name))
+ if (pokemon.ActiveAbility != null && _hailIgnoreAbilities.Contains(pokemon.ActiveAbility.Name))
continue;
var maxHealth = pokemon.BoostedStats.Hp;