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