diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs index 84c2d7a..60278ed 100644 --- a/PkmnLib.Dynamic/Models/Battle.cs +++ b/PkmnLib.Dynamic/Models/Battle.cs @@ -325,6 +325,7 @@ public class BattleImpl : ScriptSource, IBattle { _weatherScript.Clear(); } + // TODO: Trigger weather change script hooks } private IScriptSet Volatile { get; } = new ScriptSet(); diff --git a/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs b/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs index d9921f2..9175c5c 100644 --- a/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs +++ b/PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs @@ -44,8 +44,7 @@ internal static class MoveTurnExecutor return; } - var executingMove = new ExecutingMoveImpl(targets, numberOfHits, moveChoice.User, chosenMove, moveData, - moveChoice.Script); + var executingMove = new ExecutingMoveImpl(targets, numberOfHits, chosenMove, moveData, moveChoice); var prevented = false; executingMove.RunScriptHook(x => x.PreventMove(executingMove, ref prevented)); @@ -150,7 +149,11 @@ internal static class MoveTurnExecutor battle.EventHook.Invoke(new MoveMissEvent(executingMove)); break; } - + + var blockIncomingHit = false; + target.RunScriptHook(x => x.BlockIncomingHit(executingMove, target, hitIndex, ref blockIncomingHit)); + if (blockIncomingHit) + break; if (useMove.Category == MoveCategory.Status) { var secondaryEffect = useMove.SecondaryEffect; diff --git a/PkmnLib.Dynamic/Models/ExecutingMove.cs b/PkmnLib.Dynamic/Models/ExecutingMove.cs index 6273202..9b6c6f0 100644 --- a/PkmnLib.Dynamic/Models/ExecutingMove.cs +++ b/PkmnLib.Dynamic/Models/ExecutingMove.cs @@ -1,3 +1,4 @@ +using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static; using PkmnLib.Static.Moves; @@ -133,6 +134,11 @@ public interface IExecutingMove : IScriptSource /// Gets the targets of this move. /// IReadOnlyList Targets { get; } + + /// + /// The underlying move choice. + /// + IMoveChoice MoveChoice { get; } } /// @@ -142,15 +148,14 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove private readonly IHitData[] _hits; /// - public ExecutingMoveImpl(IReadOnlyList targets, byte numberOfHits, IPokemon user, ILearnedMove chosenMove, - IMoveData useMove, ScriptContainer script) + public ExecutingMoveImpl(IReadOnlyList targets, byte numberOfHits, ILearnedMove chosenMove, + IMoveData useMove, IMoveChoice moveChoice) { _targets = targets; NumberOfHits = numberOfHits; - User = user; ChosenMove = chosenMove; UseMove = useMove; - Script = script; + MoveChoice = moveChoice; var totalHits = targets.Count * numberOfHits; _hits = new IHitData[totalHits]; @@ -167,7 +172,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove public byte NumberOfHits { get; } /// - public IPokemon User { get; } + public IPokemon User => MoveChoice.User; /// public ILearnedMove ChosenMove { get; } @@ -176,7 +181,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove public IMoveData UseMove { get; } /// - public ScriptContainer Script { get; } + public ScriptContainer Script => MoveChoice.Script; /// public IHitData GetHitData(IPokemon target, byte hit) @@ -215,6 +220,9 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove /// public IReadOnlyList Targets => _targets.ToList(); + /// + public IMoveChoice MoveChoice { get; } + /// public override int ScriptCount => 1 + User.ScriptCount; diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index a35330a..5dd2102 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -313,6 +313,10 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// void LearnMove(StringKey moveName, MoveLearnMethod method, byte index); + /// + /// Adds a non-volatile status to the Pokemon. + /// + void SetStatus(StringKey status); /// /// Removes the current non-volatile status from the Pokemon. /// @@ -926,6 +930,14 @@ public class PokemonImpl : ScriptSource, IPokemon _learnedMoves[index] = new LearnedMoveImpl(move, method); } + /// + public void SetStatus(StringKey status) + { + if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript)) + throw new KeyNotFoundException($"Status script {status} not found"); + StatusScript.Set(statusScript); + } + /// public void ClearStatus() => StatusScript.Clear(); diff --git a/PkmnLib.Dynamic/ScriptHandling/Script.cs b/PkmnLib.Dynamic/ScriptHandling/Script.cs index 0b01cd8..132e773 100644 --- a/PkmnLib.Dynamic/ScriptHandling/Script.cs +++ b/PkmnLib.Dynamic/ScriptHandling/Script.cs @@ -497,4 +497,8 @@ public abstract class Script : IDeepCloneable public virtual void ChangeCatchRateBonus(IPokemon pokemon, IItem pokeball, ref byte modifier) { } + + public virtual void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block) + { + } } \ No newline at end of file diff --git a/PkmnLib.Tests/Data/Moves.json b/PkmnLib.Tests/Data/Moves.json index e0f01e0..7fa32b6 100755 --- a/PkmnLib.Tests/Data/Moves.json +++ b/PkmnLib.Tests/Data/Moves.json @@ -482,7 +482,7 @@ "category": "status", "flags": [], "effect": { - "name": "Assist" + "name": "assist" } }, { @@ -564,7 +564,7 @@ "type": "fighting", "power": 80, "pp": 20, - "accuracy": 0, + "accuracy": 255, "priority": 0, "target": "Any", "category": "special", @@ -608,7 +608,10 @@ "category": "status", "flags": [ "snatch" - ] + ], + "effect": { + "name": "aurora_veil" + } }, { "name": "autotomize", @@ -623,7 +626,7 @@ "snatch" ], "effect": { - "name": "automize" + "name": "autotomize" } }, { @@ -670,11 +673,14 @@ "type": "poison", "power": 0, "pp": 10, - "accuracy": 0, + "accuracy": 255, "priority": 4, "target": "Self", "category": "status", - "flags": [] + "flags": [], + "effect": { + "name": "baneful_bunker" + } }, { "name": "barrage", @@ -689,7 +695,10 @@ "protect", "mirror", "ballistics" - ] + ], + "effect": { + "name": "2_5_hit_move" + } }, { "name": "barrier", @@ -702,7 +711,13 @@ "category": "status", "flags": [ "snatch" - ] + ], + "effect": { + "name": "change_target_defense", + "parameters": { + "amount": 2 + } + } }, { "name": "baton_pass", diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BanefulBunker.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BanefulBunker.cs new file mode 100644 index 0000000..08478e0 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/BanefulBunker.cs @@ -0,0 +1,15 @@ +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; +using PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +namespace PkmnLib.Plugin.Gen7.Scripts.Moves; + +[Script(ScriptCategory.Move, "baneful_bunker")] +public class BanefulBunker : ProtectionScript +{ + /// + protected override ProtectionEffectScript GetEffectScript() + { + return new BanefulBunkerEffect(); + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ProtectionScript.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ProtectionScript.cs new file mode 100644 index 0000000..cece74c --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Moves/ProtectionScript.cs @@ -0,0 +1,47 @@ +using System; +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +namespace PkmnLib.Plugin.Gen7.Scripts.Moves; + +public abstract class ProtectionScript : Script +{ + protected abstract ProtectionEffectScript GetEffectScript(); + + /// + public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) + { + var battleData = target.BattleData; + if (battleData == null) + return; + // If the user goes last in the turn, the move will fail + if (battleData.Battle.ChoiceQueue is not null && !battleData.Battle.ChoiceQueue.HasNext()) + { + move.GetHitData(target, hit).Fail(); + return; + } + + var failure = target.Volatile.Get(); + if (failure == null) + { + failure = new ProtectionFailureScript(); + target.Volatile.Add(failure); + } + + var successive = failure.ProtectTurns; + // Each time, the chance of success is divided by 3. + var chance = 1.0 / Math.Pow(3, successive); + var random = target.BattleData!.Battle.Random.GetFloat(); + if (random < chance) + { + failure.ProtectTurns++; + failure.UsedProtect = true; + target.Volatile.Add(GetEffectScript()); + } + else + { + move.GetHitData(target, hit).Fail(); + } + } +} \ 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 new file mode 100644 index 0000000..38eb12c --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/BanefulBunkerEffect.cs @@ -0,0 +1,17 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Static.Moves; + +namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +public class BanefulBunkerEffect : ProtectionEffectScript +{ + /// + public override void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block) + { + base.BlockIncomingHit(executingMove, target, hitIndex, ref block); + if (executingMove.UseMove.Category != MoveCategory.Status && executingMove.UseMove.HasFlag("contact")) + { + executingMove.User.SetStatus("poisoned"); + } + } +} \ 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 new file mode 100644 index 0000000..86ca7f8 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionEffectScript.cs @@ -0,0 +1,28 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Static.Moves; + +namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +public abstract class ProtectionEffectScript : Script +{ + /// + public override void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block) + { + 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) + return; + + block = true; + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionFailureScript.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionFailureScript.cs new file mode 100644 index 0000000..1c5eec0 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Pokemon/ProtectionFailureScript.cs @@ -0,0 +1,32 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Dynamic.Models.Choices; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; + +namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon; + +/// +/// Attached script for to keep track of the consecutive turns of using Protect, +/// or protect-like moves. +/// +[Script(ScriptCategory.Pokemon, "protection_failure")] +public class ProtectionFailureScript : Script +{ + public int ProtectTurns { get; set; } + public bool UsedProtect { get; set; } + + /// + public override void OnBeforeTurnStart(ITurnChoice choice) + { + UsedProtect = false; + } + + /// + public override void OnEndTurn(IBattle battle) + { + if (!UsedProtect) + { + RemoveSelf(); + } + } +} \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Poisoned.cs b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Poisoned.cs new file mode 100644 index 0000000..a281443 --- /dev/null +++ b/Plugins/PkmnLib.Plugin.Gen7/Scripts/Status/Poisoned.cs @@ -0,0 +1,10 @@ +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Dynamic.ScriptHandling.Registry; + +namespace PkmnLib.Plugin.Gen7.Scripts.Status; + +[Script(ScriptCategory.Status, "poisoned")] +public class Poisoned : Script +{ + // TODO: Implement the Poisoned status effect. +} \ No newline at end of file