diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/MoveDataTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/MoveDataTests.cs
index 00a265b..ff85260 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/MoveDataTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/DataTests/MoveDataTests.cs
@@ -1,6 +1,11 @@
+using System.Buffers;
+using System.Text.Json;
using PkmnLib.Dynamic.Libraries;
+using PkmnLib.Dynamic.Libraries.DataLoaders.Models;
using PkmnLib.Dynamic.ScriptHandling;
+using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static.Moves;
+using TUnit.Core.Logging;
namespace PkmnLib.Plugin.Gen7.Tests.DataTests;
@@ -84,4 +89,106 @@ public class MoveDataTests
await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out _)).IsTrue();
}
+
+ public record HasEitherEffectOrComment(string MoveName, bool HasEffect, bool HasComment)
+ {
+ ///
+ public override string ToString() => MoveName + " has effect: " + HasEffect + ", comment: " + HasComment;
+ }
+
+ public static IEnumerable> EveryMoveHasEitherEffectOrCommentData()
+ {
+ IResourceProvider plugin = new Gen7Plugin();
+ using var movesJson = plugin.GetResource(ResourceFileType.Moves)!.Open();
+ var json = new BinaryReader(movesJson);
+ var moves = json.ReadBytes((int)movesJson.Length);
+ var reader = new Utf8JsonReader(new ReadOnlySequence(moves), new JsonReaderOptions
+ {
+ CommentHandling = JsonCommentHandling.Allow,
+ AllowTrailingCommas = true,
+ });
+ Console.WriteLine("Reading moves.json");
+
+ // The JSON is an object { "data": [] }.
+ // Each move is an object in the "data" array.
+ // Each object needs to have either an "effect" or a "comment" property.
+
+ // Read the first object
+ reader.Read();
+ // Read the properties, until we find the "data" property
+ while (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "data")
+ {
+ reader.Read();
+ }
+ // Read the "data" property
+ reader.Read();
+ // Read the start of the array
+ reader.Read();
+
+ var results = new List();
+ // Read each move object
+ while (reader.TokenType != JsonTokenType.EndArray)
+ {
+ // Read the start of the object
+ if (!reader.Read())
+ break;
+ // Read each property
+ var name = "";
+ var hasEffect = false;
+ var hasComment = false;
+ while (reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType == JsonTokenType.PropertyName)
+ {
+ var propertyName = reader.GetString();
+ if (propertyName == "name")
+ {
+ reader.Read();
+ name = reader.GetString()!;
+ }
+ else if (propertyName == "effect")
+ {
+ hasEffect = true;
+ reader.Read();
+ // Read the effect object
+ while (reader.TokenType != JsonTokenType.EndObject)
+ {
+ reader.Read();
+ if (reader.TokenType != JsonTokenType.StartObject)
+ continue;
+ while (reader.TokenType != JsonTokenType.EndObject)
+ {
+ reader.Read();
+ }
+ reader.Read();
+ }
+ }
+ }
+ else if (reader.TokenType == JsonTokenType.Comment)
+ {
+ // Read the comment
+ var comment = reader.GetComment();
+ hasComment = comment.Trim() == "No secondary effect";
+ }
+
+ reader.Read();
+ }
+ results.Add(new HasEitherEffectOrComment(name, hasEffect, hasComment));
+ }
+ return results.Where(x => !string.IsNullOrWhiteSpace(x.MoveName))
+ .Select>(x => () => x).ToList();
+ }
+
+ ///
+ /// To ensure that all moves have been properly implemented, we require that each move has either an "effect" property,
+ /// or a comment with the exact text "No secondary effect". This ensures that we have not missed any moves.
+ ///
+ /// The implementation of this test is a bit tricky, as we need to read the JSON file and parse it ourselves,
+ /// as System.Text.Json does not support reading comments in JSON files beyond the low-level API.
+ ///
+ ///
+ [Test, MethodDataSource(nameof(EveryMoveHasEitherEffectOrCommentData))]
+ public async Task AllMovesHaveEitherEffectOrComment(HasEitherEffectOrComment test) =>
+ // Check that each move has either an "effect" or a "comment" property
+ await Assert.That(test.HasEffect || test.HasComment).IsTrue();
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc b/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc
index 9531be0..255ce00 100755
--- a/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc
+++ b/Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc
@@ -49,6 +49,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "acid",
@@ -101,6 +102,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "acid_downpour__special",
@@ -112,6 +114,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "acid_spray",
@@ -175,7 +178,7 @@
"type": "flying",
"power": 60,
"pp": 20,
- "accuracy": 0,
+ "accuracy": 255,
"priority": 0,
"target": "Any",
"category": "physical",
@@ -185,6 +188,7 @@
"mirror",
"distance"
]
+ // No secondary effect
},
{
"name": "aeroblast",
@@ -285,6 +289,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "all_out_pummeling__special",
@@ -296,6 +301,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "ally_switch",
@@ -383,6 +389,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "aqua_ring",
@@ -417,6 +424,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "arm_thrust",
@@ -576,6 +584,7 @@
"pulse",
"ballistics"
]
+ // No secondary effect
},
{
"name": "aurora_beam",
@@ -881,6 +890,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "black_hole_eclipse__special",
@@ -892,6 +902,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "blast_burn",
@@ -981,6 +992,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "bloom_doom__special",
@@ -992,6 +1004,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "blue_flare",
@@ -1126,6 +1139,7 @@
"sound",
"ignore-substitute"
]
+ // No secondary effect
},
{
"name": "bounce",
@@ -1180,6 +1194,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "breakneck_blitz__special",
@@ -1191,6 +1206,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "brick_break",
@@ -1241,6 +1257,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "bubble",
@@ -1381,6 +1398,7 @@
"mirror",
"punch"
]
+ // No secondary effect
},
{
"name": "bullet_seed",
@@ -1484,6 +1502,7 @@
"flags": [
"contact"
]
+ // No secondary effect
},
{
"name": "celebrate",
@@ -1823,6 +1842,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "continental_crush__special",
@@ -1834,6 +1854,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "conversion",
@@ -1908,6 +1929,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "corkscrew_crash__special",
@@ -1919,6 +1941,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "cosmic_power",
@@ -2029,7 +2052,10 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "increased_critical_stage"
+ }
},
{
"name": "crafty_shield",
@@ -2058,7 +2084,10 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "increased_critical_stage"
+ }
},
{
"name": "cross_poison",
@@ -2175,6 +2204,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "dark_pulse",
@@ -2248,6 +2278,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "defend_order",
@@ -2347,6 +2378,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "devastating_drake__special",
@@ -2358,6 +2390,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "diamond_storm",
@@ -2425,7 +2458,7 @@
"type": "fairy",
"power": 40,
"pp": 15,
- "accuracy": 0,
+ "accuracy": 255,
"priority": 0,
"target": "AllOpponent",
"category": "special",
@@ -2435,6 +2468,7 @@
"sound",
"ignore-substitute"
]
+ // No secondary effect
},
{
"name": "discharge",
@@ -2683,6 +2717,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "dragon_dance",
@@ -2719,6 +2754,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "dragon_pulse",
@@ -2735,6 +2771,7 @@
"distance",
"pulse"
]
+ // No secondary effect
},
{
"name": "dragon_rage",
@@ -2871,6 +2908,7 @@
"mirror",
"distance"
]
+ // No secondary effect
},
{
"name": "drill_run",
@@ -2885,7 +2923,10 @@
"contact",
"protect",
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "increased_critical_stage"
+ }
},
{
"name": "dual_chop",
@@ -2962,6 +3003,7 @@
"hit_underground",
"effective_against_underground"
]
+ // No secondary effect
},
{
"name": "echoed_voice",
@@ -3017,6 +3059,7 @@
"mirror",
"ballistics"
]
+ // No secondary effect
},
{
"name": "electric_terrain",
@@ -3304,6 +3347,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "facade",
@@ -3353,6 +3397,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "fake_out",
@@ -3444,14 +3489,17 @@
"category": "physical",
"flags": [
"mirror"
- ]
+ ],
+ "effect": {
+ "name": "feint"
+ }
},
{
"name": "feint_attack",
"type": "dark",
"power": 60,
"pp": 20,
- "accuracy": 0,
+ "accuracy": 255,
"priority": 0,
"target": "Any",
"category": "physical",
@@ -3460,6 +3508,7 @@
"protect",
"mirror"
]
+ // No secondary effect
},
{
"name": "fell_stinger",
@@ -4436,6 +4485,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "gigavolt_havoc__special",
@@ -4447,6 +4497,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "glaciate",
@@ -4652,7 +4703,10 @@
"category": "status",
"flags": [
"protect"
- ]
+ ],
+ "effect": {
+ "name": "guard_split"
+ }
},
{
"name": "guard_swap",
@@ -4741,6 +4795,7 @@
"hit_flying",
"effective_against_fly"
]
+ // No secondary effect
},
{
"name": "gyro_ball",
@@ -4809,8 +4864,10 @@
"priority": 0,
"target": "AllAlly",
"category": "status",
- "flags": []
- // TODO: Add effect
+ "flags": [],
+ "effect": {
+ "name": "happy_hour"
+ }
},
{
"name": "harden",
@@ -5217,7 +5274,7 @@
"flags": [
"ignore-substitute"
]
- // Does nothing
+ // No secondary effect
},
{
"name": "hone_claws",
@@ -5376,6 +5433,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "hydro_vortex__special",
@@ -5387,6 +5445,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "hyper_beam",
@@ -5757,6 +5816,7 @@
"target": "Any",
"category": "physical",
"flags": []
+ // No secondary effect
},
{
"name": "inferno_overdrive__special",
@@ -5768,6 +5828,7 @@
"target": "Any",
"category": "special",
"flags": []
+ // No secondary effect
},
{
"name": "infestation",
@@ -6565,7 +6626,10 @@
"snatch",
"distance",
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "magnetic_flux"
+ }
},
{
"name": "magnitude",
@@ -9238,7 +9302,10 @@
"mirror",
"sound",
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "round"
+ }
},
{
"name": "sacred_fire",
@@ -12232,7 +12299,10 @@
"mirror",
"sound",
"ignore-substitute"
- ]
+ ],
+ "effect": {
+ "name": "uproar"
+ }
},
{
"name": "v_create",
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Battle/UproarEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Battle/UproarEffect.cs
new file mode 100644
index 0000000..ee8fbd8
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Battle/UproarEffect.cs
@@ -0,0 +1,53 @@
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Battle;
+
+[Script(ScriptCategory.Battle, "uproar_effect")]
+public class UproarEffect : Script
+{
+ private IPokemon? _placer;
+ private bool _hasUsedUproar;
+ private int _turns = 3;
+
+ public void SetPlacer(IPokemon placer)
+ {
+ _placer = placer;
+ _hasUsedUproar = true;
+ }
+
+ ///
+ public override void PreventStatusChange(IPokemon pokemonImpl, StringKey status, ref bool preventStatus)
+ {
+ if (status == ScriptUtils.ResolveName())
+ {
+ preventStatus = true;
+ }
+ }
+
+ ///
+ public override void OnBeforeTurnStart(ITurnChoice choice)
+ {
+ _hasUsedUproar = false;
+ }
+
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ if (move.User == _placer && move.UseMove.Name == "uproar")
+ _hasUsedUproar = true;
+ }
+
+ ///
+ public override void OnEndTurn(IBattle battle)
+ {
+ if (!_hasUsedUproar)
+ {
+ RemoveSelf();
+ }
+ _turns--;
+ if (_turns <= 0)
+ {
+ RemoveSelf();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Feint.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Feint.cs
new file mode 100644
index 0000000..d51bd1c
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Feint.cs
@@ -0,0 +1,16 @@
+using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "feint")]
+public class Feint : Script
+{
+ ///
+ public override void OnBeforeHit(IExecutingMove move, IPokemon target, byte hitIndex)
+ {
+ if (target.Volatile.Contains())
+ {
+ target.Volatile.Remove();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/HappyHour.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/HappyHour.cs
new file mode 100644
index 0000000..8136f88
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/HappyHour.cs
@@ -0,0 +1,7 @@
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "happy_hour")]
+public class HappyHour : Script
+{
+ // TODO: Implement Happy Hour
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/IncreasedCriticalStage.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/IncreasedCriticalStage.cs
index fd68924..f5f675b 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/IncreasedCriticalStage.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/IncreasedCriticalStage.cs
@@ -8,10 +8,7 @@ public class IncreasedCriticalStage : Script
{
// Extreme edge case, should never happen
if (stage == byte.MaxValue)
- {
- move.GetHitData(target, hit).Fail();
return;
- }
stage += 1;
}
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/MagneticFlux.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/MagneticFlux.cs
new file mode 100644
index 0000000..e28175c
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/MagneticFlux.cs
@@ -0,0 +1,25 @@
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "magnetic_flux")]
+public class MagneticFlux : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ var battleData = move.User.BattleData;
+ if (battleData is null)
+ return;
+
+ foreach (var pokemon in battleData.BattleSide.Pokemon.WhereNotNull())
+ {
+ if (pokemon.ActiveAbility?.Name != "plus" && pokemon.ActiveAbility?.Name != "minus")
+ continue;
+
+ EventBatchId batch = new();
+ pokemon.ChangeStatBoost(Statistic.Defense, 1, pokemon == move.User, batch);
+ pokemon.ChangeStatBoost(Statistic.SpecialDefense, 1, pokemon == move.User, batch);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Uproar.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Uproar.cs
new file mode 100644
index 0000000..694bbb6
--- /dev/null
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/Uproar.cs
@@ -0,0 +1,27 @@
+using PkmnLib.Plugin.Gen7.Scripts.Battle;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
+
+[Script(ScriptCategory.Move, "uproar")]
+public class Uproar : Script
+{
+ ///
+ public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
+ {
+ var battle = move.Battle;
+
+ foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
+ {
+ if (pokemon.HasStatus(ScriptUtils.ResolveName()))
+ {
+ pokemon.ClearStatus();
+ }
+ }
+ var script = battle.Volatile.Add(new UproarEffect());
+ if (script?.Script is UproarEffect uproarEffect)
+ {
+ uproarEffect.SetPlacer(move.User);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BanefulBunkerEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BanefulBunkerEffect.cs
index c562e25..4066d18 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BanefulBunkerEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BanefulBunkerEffect.cs
@@ -2,6 +2,7 @@ using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+[Script(ScriptCategory.Pokemon, "baneful_bunker")]
public class BanefulBunkerEffect : ProtectionEffectScript
{
///
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs
index c67cb50..e8efba2 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs
@@ -1,5 +1,6 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+[Script(ScriptCategory.Pokemon, "protect")]
public class ProtectionEffectScript : Script
{
///
diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/SpikyShieldEffect.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/SpikyShieldEffect.cs
index a56b6f3..8e0169c 100644
--- a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/SpikyShieldEffect.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/SpikyShieldEffect.cs
@@ -1,5 +1,6 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
+[Script(ScriptCategory.Pokemon, "spiky_shield")]
public class SpikyShieldEffect : ProtectionEffectScript
{
///