diff --git a/AI/AIRunner/TestCommandRunner.cs b/AI/AIRunner/TestCommandRunner.cs index c288ffd..2562de4 100644 --- a/AI/AIRunner/TestCommandRunner.cs +++ b/AI/AIRunner/TestCommandRunner.cs @@ -163,19 +163,19 @@ public static class TestCommandRunner var pokemon1 = battle.Sides[0].Pokemon[0]; while (pokemon1 is null && !battle.HasEnded) { - pokemon1 = battle.Parties[0].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable); - if (pokemon1 is null) + var replacement1 = battle.Parties[0].BattlePokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable); + if (replacement1 is null) throw new InvalidOperationException("No usable Pokémon found in party 1."); - battle.Sides[0].SwapPokemon(0, pokemon1); + battle.Sides[0].SwapPokemon(0, replacement1); pokemon1 = battle.Sides[0].Pokemon[0]; } var pokemon2 = battle.Sides[1].Pokemon[0]; while (pokemon2 is null && !battle.HasEnded) { - pokemon2 = battle.Parties[1].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable); - if (pokemon2 is null) + var replacement2 = battle.Parties[1].BattlePokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable); + if (replacement2 is null) throw new InvalidOperationException("No usable Pokémon found in party 2."); - battle.Sides[1].SwapPokemon(0, pokemon2); + battle.Sides[1].SwapPokemon(0, replacement2); pokemon2 = battle.Sides[1].Pokemon[0]; } if (pokemon1 is null || pokemon2 is null) diff --git a/PkmnLib.Dynamic/AI/AIHelpers.cs b/PkmnLib.Dynamic/AI/AIHelpers.cs index 147b2dd..888f9da 100644 --- a/PkmnLib.Dynamic/AI/AIHelpers.cs +++ b/PkmnLib.Dynamic/AI/AIHelpers.cs @@ -14,7 +14,7 @@ public static class AIHelpers /// /// Estimates the amount of damage that will be done by a move against a target. /// - public static uint CalculateDamageEstimation(IMoveData move, IPokemon user, IPokemon target, + public static uint CalculateDamageEstimation(IMoveData move, IBattlePokemon user, IBattlePokemon target, IDynamicLibrary library) { var hitData = new CustomHitData diff --git a/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs b/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs index 8d19a0f..6967093 100644 --- a/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs +++ b/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs @@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit; public class AIMoveState { /// - public AIMoveState(IPokemon user, IMoveData move) + public AIMoveState(IBattlePokemon user, IMoveData move) { User = user; Move = move; @@ -18,7 +18,7 @@ public class AIMoveState /// /// The user that's being wrapper /// - public IPokemon User { get; } + public IBattlePokemon User { get; } /// /// The move that's being wrapper diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs index 1a0c59d..a5851db 100644 --- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs @@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit; public partial class ExplicitAI { - private bool TryChooseToSwitchOut(IBattle battle, IPokemon pokemon, bool terribleMoves, + private bool TryChooseToSwitchOut(IBattle battle, IBattlePokemon pokemon, bool terribleMoves, [NotNullWhen(true)] out ITurnChoice? choice) { choice = null; @@ -17,13 +17,13 @@ public partial class ExplicitAI return false; if (TrainerHighSkill) { - var opponentSide = battle.Sides.First(x => x != pokemon.BattleData?.BattleSide); + var opponentSide = battle.Sides.First(x => x != pokemon.BattleSide); var foeCanAct = opponentSide.Pokemon.WhereNotNull().Any(CanAttack); if (!foeCanAct) return false; } var party = battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex( - new ResponsibleIndex(pokemon.BattleData!.SideIndex, pokemon.BattleData.Position))); + new ResponsibleIndex(pokemon.SideIndex, pokemon.Position))); if (party is null) return false; var usablePokemon = party.GetUsablePokemonNotInField().ToList(); @@ -44,20 +44,20 @@ public partial class ExplicitAI if (!shouldSwitch) return false; } - var battleSide = pokemon.BattleData!.BattleSide; + var battleSide = pokemon.BattleSide; var bestReplacement = ChooseBestReplacementPokemon(terribleMoves, usablePokemon, battleSide); if (bestReplacement is null) { AILogging.LogInformation( - $"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.BattleData.Position}."); + $"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.Position}."); return false; } choice = new SwitchChoice(pokemon, bestReplacement); return true; } - private IPokemon? ChooseBestReplacementPokemon(bool terribleMoves, IReadOnlyList usablePokemon, - IBattleSide battleSide) + private IBattlePokemon? ChooseBestReplacementPokemon(bool terribleMoves, + IReadOnlyList usablePokemon, IBattleSide battleSide) { var options = usablePokemon.Where((_, index) => { @@ -84,7 +84,7 @@ public partial class ExplicitAI private static readonly StringKey ToxicSpikesName = "toxic_spikes"; private static readonly StringKey StickyWebName = "sticky_web"; - private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide) + private int RateReplacementPokemon(IBattlePokemon pokemon, IBattleSide battleSide) { var score = 0; var types = pokemon.Types; @@ -107,7 +107,7 @@ public partial class ExplicitAI var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide); foreach (var foe in opponentSide.Pokemon.WhereNotNull()) { - var lastMoveUsed = foe.BattleData?.LastMoveChoice; + var lastMoveUsed = foe.LastMoveChoice; if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status) continue; var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType; @@ -134,22 +134,19 @@ public partial class ExplicitAI /// /// Calculates the expected entry hazard damage for a given Pokémon on a given battle side. /// - public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side) + public static uint CalculateEntryHazardDamage(IBattlePokemon pokemon, IBattleSide side) { var damage = 0u; side.RunScriptHook(x => x.ExpectedEntryDamage(pokemon, ref damage)); return damage; } - private static bool CanSwitch(IPokemon pokemon) + private static bool CanSwitch(IBattlePokemon pokemon) { - var battleData = pokemon.BattleData; - if (battleData == null) + if (pokemon.Battle.IsWildBattle) return false; - if (battleData.Battle.IsWildBattle) - return false; - var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x => - x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position))); + var partyForIndex = pokemon.Battle.Parties.FirstOrDefault(x => + x.IsResponsibleForIndex(new ResponsibleIndex(pokemon.SideIndex, pokemon.Position))); return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField(); } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs index 4414f37..4b74009 100644 --- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs @@ -37,7 +37,7 @@ public partial class ExplicitAI private static readonly StringKey KomalaName = "komala"; private static readonly StringKey MiniorName = "minior"; - private static bool CanBePoisoned(IPokemon pokemon, IBattle battle) + private static bool CanBePoisoned(IBattlePokemon pokemon, IBattle battle) { if (battle.TerrainName == MistyTerrainName) return false; @@ -60,7 +60,7 @@ public partial class ExplicitAI return true; } - private static bool CanAbsorbMove(IPokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle) + private static bool CanAbsorbMove(IBattlePokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle) { if (pokemon.ActiveAbility == null) return false; diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs index 928186b..898d624 100644 --- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs @@ -118,7 +118,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI public IRandom Random => _random; /// - public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) + public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) { if (battle.HasForcedTurn(pokemon, out var choice)) return choice; @@ -131,8 +131,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI var moveChoices = GetMoveScores(pokemon, battle); if (moveChoices.Count == 0) { - var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0); - return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position); + var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0); + return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position); } var maxScore = moveChoices.Max(x => x.score); if (TrainerHighSkill && CanSwitch(pokemon)) @@ -144,7 +144,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI if (!badMoves && _random.GetInt(100) < 25) badMoves = true; } - else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 && + else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.TurnsOnField > 2 && _random.GetInt(100) < 80) { badMoves = true; @@ -164,8 +164,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI var totalScore = considerChoices.Sum(x => x.Item2); if (totalScore == 0) { - var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0); - return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position); + var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0); + return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position); } var initialRandomValue = _random.GetFloat(0, totalScore); var randomValue = initialRandomValue; @@ -177,15 +177,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI var (index, _, targetIndex) = considerChoices[i].x; var learnedMove = pokemon.Moves[index]; - var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0); + var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0); if (targetIndex == -1) - targetIndex = pokemon.BattleData.Position; + targetIndex = pokemon.Position; return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex); } throw new InvalidOperationException("No valid move choice found. This should not happen."); } - private List<(int index, int score, int targetIndex)> GetMoveScores(IPokemon user, IBattle battle) + private List<(int index, int score, int targetIndex)> GetMoveScores(IBattlePokemon user, IBattle battle) { var choices = new List<(int index, int score, int targetIndex)>(); foreach (var (learnedMove, index) in user.Moves.Select((x, i) => (x, i)).Where(x => x.x != null)) @@ -249,30 +249,24 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI // TODO: get redirected target foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull()) { - var battleData = pokemon.BattleData; - if (battleData == null) + if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user)) continue; - if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user)) - continue; - if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex) + if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex) { continue; } var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle); - AddMoveToChoices(index, score, battleData.Position); + AddMoveToChoices(index, score, pokemon.Position); } } else { - var targets = new List(); + var targets = new List(); foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull()) { - var battleData = pokemon.BattleData; - if (battleData == null) + if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user)) continue; - if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user)) - continue; - if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex) + if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex) { continue; } @@ -295,7 +289,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI } } - private bool PredictMoveFailure(IPokemon user, IBattle battle, AIMoveState aiMove) + private bool PredictMoveFailure(IBattlePokemon user, IBattle battle, AIMoveState aiMove) { if (user.HasStatus("sleep")) { @@ -333,14 +327,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI private static readonly StringKey SubstituteName = new("substitute"); private static readonly StringKey InfiltratorName = new("infiltrator"); - private bool PredictMoveFailureAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle) + private bool PredictMoveFailureAgainstTarget(IBattlePokemon user, AIMoveState aiMove, IBattlePokemon target, + IBattle battle) { if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this, aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target))) return true; if (aiMove.Move.Priority > 0) { - if (target.BattleData?.SideIndex != user.BattleData?.SideIndex) + if (target.SideIndex != user.SideIndex) { // Psychic Terrain makes all priority moves fail if the target is affected if (battle.TerrainName == PsychicTerrainName && !target.IsFloating) @@ -348,7 +343,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI return true; } // Dazzling and Queenly Majesty prevent priority moves from being used against the Pokémon with those abilities - if (target.BattleData?.BattleSide.Pokemon.WhereNotNull().Any(x => + if (target.BattleSide.Pokemon.WhereNotNull().Any(x => x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true) { return true; @@ -362,7 +357,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0) return true; if (user.ActiveAbility?.Name == PranksterName && aiMove.Move.Category == MoveCategory.Status && - target.Types.Any(x => x.Name == DarkName) && target.BattleData?.SideIndex != user.BattleData?.SideIndex) + target.Types.Any(x => x.Name == DarkName) && target.SideIndex != user.SideIndex) return true; if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating) return true; @@ -375,7 +370,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI return false; } - private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList? targets = null) + private int GetMoveScore(IBattlePokemon user, AIMoveState aiMove, IBattle battle, + IReadOnlyList? targets = null) { var score = MoveBaseScore; if (targets != null) @@ -411,7 +407,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI return score; } - private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle) + private int GetMoveScoreAgainstTarget(IBattlePokemon user, AIMoveState aiMove, IBattlePokemon target, + IBattle battle) { if (_skillFlags.CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle)) { @@ -427,8 +424,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI _handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score); } - if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex && - target.BattleData?.Position != user.BattleData?.Position) + if (aiMove.Move.Target.TargetsFoe() && target.SideIndex == user.SideIndex && target.Position != user.Position) { if (score == MoveUselessScore) return -1; @@ -442,7 +438,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI private static readonly StringKey OvercoatName = new("overcoat"); private static readonly StringKey SafetyGogglesName = new("safety_goggles"); - private static bool AffectedByPowder(IPokemon pokemon) + private static bool AffectedByPowder(IBattlePokemon pokemon) { if (pokemon.Types.Any(x => x.Name == GrassName)) return false; @@ -456,7 +452,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI private static readonly StringKey TruantName = "truant"; private static readonly StringKey TruantEffectName = "truant_effect"; - private static bool CanAttack(IPokemon pokemon) + private static bool CanAttack(IBattlePokemon pokemon) { if (pokemon.Volatile.Contains("requires_recharge")) return false; diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs index 0962465..c787cee 100644 --- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs +++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs @@ -6,7 +6,7 @@ namespace PkmnLib.Dynamic.AI.Explicit; /// /// An option where a move is used against a target /// -public record struct MoveOption(AIMoveState Move, IBattle Battle, IPokemon? Target, uint EstimatedDamage = 0); +public record struct MoveOption(AIMoveState Move, IBattle Battle, IBattlePokemon? Target, uint EstimatedDamage = 0); /// /// A function that takes an explicit AI and a move option and returns a boolean value. @@ -16,8 +16,8 @@ public delegate bool AIBoolHandler(IExplicitAI ai, MoveOption option); /// /// A function for returning whether a Pokemon should switch. /// -public delegate bool AISwitchBoolHandler(IExplicitAI ai, IPokemon pokemon, IBattle battle, - IReadOnlyList reserves); +public delegate bool AISwitchBoolHandler(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, + IReadOnlyList reserves); /// /// A function for returning the base power of a move. @@ -112,7 +112,7 @@ public interface IReadOnlyExplicitAIHandlers /// /// Indicates whether a Pokemon should switch into another Pokemon /// - bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves); + bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, IReadOnlyList reserves); /// /// Functions that indicate whether a Pokemon should NOT switch @@ -122,7 +122,8 @@ public interface IReadOnlyExplicitAIHandlers /// /// Indicates whether a Pokemon should NOT switch into another Pokemon /// - bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves); + bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, + IReadOnlyList reserves); /// /// Scores abilities @@ -246,7 +247,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers ShouldSwitchFunctions; /// - public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) + public bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, + IReadOnlyList reserves) { var shouldSwitch = false; foreach (var (_, handler) in ShouldSwitchFunctions) @@ -272,7 +274,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers public FunctionHandlerDictionary AbilityRanking = []; /// - public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves) + public bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, + IReadOnlyList reserves) { var shouldNotSwitch = false; foreach (var (_, handler) in ShouldNotSwitchFunctions) diff --git a/PkmnLib.Dynamic/AI/HighestDamageAI.cs b/PkmnLib.Dynamic/AI/HighestDamageAI.cs index 4cdb753..2a0cc88 100644 --- a/PkmnLib.Dynamic/AI/HighestDamageAI.cs +++ b/PkmnLib.Dynamic/AI/HighestDamageAI.cs @@ -15,9 +15,9 @@ public class HighestDamageAI : PokemonAI } /// - public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) + public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) { - var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; + var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0; var opponent = battle.Sides[opponentSide].Pokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable); var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0))) .ToList(); diff --git a/PkmnLib.Dynamic/AI/PassTurnAI.cs b/PkmnLib.Dynamic/AI/PassTurnAI.cs index b309daf..cd81d18 100644 --- a/PkmnLib.Dynamic/AI/PassTurnAI.cs +++ b/PkmnLib.Dynamic/AI/PassTurnAI.cs @@ -14,5 +14,5 @@ public class PassTurnAI : PokemonAI } /// - public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) => new PassChoice(pokemon); + public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) => new PassChoice(pokemon); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/AI/PokemonAI.cs b/PkmnLib.Dynamic/AI/PokemonAI.cs index 73e6cbb..9b232ac 100644 --- a/PkmnLib.Dynamic/AI/PokemonAI.cs +++ b/PkmnLib.Dynamic/AI/PokemonAI.cs @@ -26,70 +26,69 @@ public abstract class PokemonAI /// /// Gets the choice for the Pokémon. /// - public abstract ITurnChoice GetChoice(IBattle battle, IPokemon pokemon); + public abstract ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon); /// /// For a given user and move, returns the valid targets for that move. /// - public IEnumerable<(byte side, byte position)> GetValidTargetsForMove(IPokemon user, ILearnedMove move) + public IEnumerable<(byte side, byte position)> GetValidTargetsForMove(IBattlePokemon user, ILearnedMove move) { - var userBattleData = user.BattleData!; switch (move.MoveData.Target) { case MoveTarget.Adjacent: - yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); + yield return (GetOppositeSide(user.SideIndex), user.Position); break; case MoveTarget.AdjacentAlly: - if (userBattleData.Position > 0) - yield return (userBattleData.SideIndex, (byte)(userBattleData.Position - 1)); - if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1) - yield return (userBattleData.SideIndex, (byte)(userBattleData.Position + 1)); + if (user.Position > 0) + yield return (user.SideIndex, (byte)(user.Position - 1)); + if (user.Battle.PositionsPerSide > user.Position + 1) + yield return (user.SideIndex, (byte)(user.Position + 1)); break; case MoveTarget.AdjacentAllySelf: - if (userBattleData.Position > 0) - yield return (userBattleData.SideIndex, (byte)(userBattleData.Position - 1)); - if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1) - yield return (userBattleData.SideIndex, (byte)(userBattleData.Position + 1)); - yield return (userBattleData.SideIndex, userBattleData.Position); + if (user.Position > 0) + yield return (user.SideIndex, (byte)(user.Position - 1)); + if (user.Battle.PositionsPerSide > user.Position + 1) + yield return (user.SideIndex, (byte)(user.Position + 1)); + yield return (user.SideIndex, user.Position); break; case MoveTarget.AdjacentOpponent: - yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); - if (userBattleData.Position > 0) - yield return (GetOppositeSide(userBattleData.SideIndex), (byte)(userBattleData.Position - 1)); - if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1) - yield return (GetOppositeSide(userBattleData.SideIndex), (byte)(userBattleData.Position + 1)); + yield return (GetOppositeSide(user.SideIndex), user.Position); + if (user.Position > 0) + yield return (GetOppositeSide(user.SideIndex), (byte)(user.Position - 1)); + if (user.Battle.PositionsPerSide > user.Position + 1) + yield return (GetOppositeSide(user.SideIndex), (byte)(user.Position + 1)); break; case MoveTarget.All: - yield return (userBattleData.SideIndex, userBattleData.Position); + yield return (user.SideIndex, user.Position); break; case MoveTarget.AllAdjacent: - yield return (userBattleData.SideIndex, userBattleData.Position); + yield return (user.SideIndex, user.Position); break; case MoveTarget.AllAdjacentOpponent: - yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); + yield return (GetOppositeSide(user.SideIndex), user.Position); break; case MoveTarget.AllAlly: - yield return (userBattleData.SideIndex, userBattleData.Position); + yield return (user.SideIndex, user.Position); break; case MoveTarget.AllOpponent: - yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); + yield return (GetOppositeSide(user.SideIndex), user.Position); break; case MoveTarget.Any: - foreach (var side in userBattleData.Battle.Sides) + foreach (var side in user.Battle.Sides) { foreach (var pokemon in side.Pokemon) { - if (pokemon?.BattleData == null) + if (pokemon == null) continue; - yield return (side.Index, pokemon.BattleData!.Position); + yield return (side.Index, pokemon.Position); } } break; case MoveTarget.RandomOpponent: - yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); + yield return (GetOppositeSide(user.SideIndex), user.Position); break; case MoveTarget.SelfUse: - yield return (userBattleData.SideIndex, userBattleData.Position); + yield return (user.SideIndex, user.Position); break; default: throw new ArgumentOutOfRangeException(); diff --git a/PkmnLib.Dynamic/AI/PrescientAI.cs b/PkmnLib.Dynamic/AI/PrescientAI.cs index 4d63f7a..c06cfe7 100644 --- a/PkmnLib.Dynamic/AI/PrescientAI.cs +++ b/PkmnLib.Dynamic/AI/PrescientAI.cs @@ -18,9 +18,9 @@ public class PrescientAI : PokemonAI } /// - public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) + public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) { - var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; + var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0; var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0))) .ToList(); @@ -34,13 +34,13 @@ public class PrescientAI : PokemonAI } private static IEnumerable<(ITurnChoice Choice, float Score)> ScoreChoices(IBattle battle, - IReadOnlyList moves, IPokemon pokemon) + IReadOnlyList moves, IBattlePokemon pokemon) { - var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; + var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0; foreach (var learnedMoveOriginal in moves.WhereNotNull()) { var battleClone = battle.DeepClone(); - var pokemonClone = battleClone.Sides[pokemon.BattleData!.SideIndex].Pokemon[pokemon.BattleData.Position]!; + var pokemonClone = battleClone.Sides[pokemon.SideIndex].Pokemon[pokemon.Position]!; var learnedMove = pokemonClone.Moves.WhereNotNull() .First(m => m.MoveData.Name == learnedMoveOriginal.MoveData.Name); var choice = new MoveChoice(pokemonClone, learnedMove, opponentSide, 0); @@ -57,17 +57,16 @@ public class PrescientAI : PokemonAI } if (battleClone.TrySetChoice(choice)) { - var score = CalculateScore(battleClone.Parties[pokemon.BattleData.SideIndex], - battleClone.Parties[opponentSide]); + var score = CalculateScore(battleClone.Parties[pokemon.SideIndex], battleClone.Parties[opponentSide]); var realChoice = new MoveChoice(pokemon, learnedMoveOriginal, opponentSide, 0); yield return (realChoice, score); } } } - private static ITurnChoice GetOpponentChoice(IBattle battle, IPokemon pokemon) + private static ITurnChoice GetOpponentChoice(IBattle battle, IBattlePokemon pokemon) { - var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0; + var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0; var opponent = battle.Sides[opponentSide].Pokemon[0]; if (opponent is null) { diff --git a/PkmnLib.Dynamic/AI/RandomAI.cs b/PkmnLib.Dynamic/AI/RandomAI.cs index defeb98..2e62377 100644 --- a/PkmnLib.Dynamic/AI/RandomAI.cs +++ b/PkmnLib.Dynamic/AI/RandomAI.cs @@ -19,7 +19,7 @@ public class RandomAI : PokemonAI } /// - public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) + public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) { var moves = pokemon.Moves.WhereNotNull().Where(x => x.CurrentPp > 0).ToList(); while (moves.Count > 0) @@ -28,7 +28,7 @@ public class RandomAI : PokemonAI var targets = GetValidTargetsForMove(pokemon, move).ToArray(); if (move.MoveData.Category is MoveCategory.Physical or MoveCategory.Special) { - targets = targets.Where(x => x.side != pokemon.BattleData!.SideIndex).ToArray(); + targets = targets.Where(x => x.side != pokemon.SideIndex).ToArray(); } if (targets.Length == 0) { @@ -43,7 +43,7 @@ public class RandomAI : PokemonAI } moves.Remove(move); } - return battle.Library.MiscLibrary.ReplacementChoice(pokemon, - pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0, pokemon.BattleData.Position); + return battle.Library.MiscLibrary.ReplacementChoice(pokemon, pokemon.SideIndex == 0 ? (byte)1 : (byte)0, + pokemon.Position); } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs index 0a8be67..cb449f2 100644 --- a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs +++ b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs @@ -15,7 +15,7 @@ public static class MoveTurnExecutor { internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice) { - moveChoice.User.BattleData!.LastMoveChoice = moveChoice; + moveChoice.User.LastMoveChoice = moveChoice; var chosenMove = moveChoice.ChosenMove; var useMove = chosenMove.MoveData; @@ -126,7 +126,7 @@ public static class MoveTurnExecutor private static readonly ThreadLocal> TypeListCache = new(() => []); - private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IPokemon target) + private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IBattlePokemon target) { var failed = false; target.RunScriptHook(x => x.FailIncomingMove(executingMove, target, ref failed)); diff --git a/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs b/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs index c48baed..3e302d3 100644 --- a/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs +++ b/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs @@ -11,7 +11,8 @@ public static class TargetResolver /// /// Get the targets of a move based on the target type, and the selected side and position to target. /// - public static IReadOnlyList ResolveTargets(IBattle battle, byte side, byte position, MoveTarget target) + public static IReadOnlyList ResolveTargets(IBattle battle, byte side, byte position, + MoveTarget target) { return target switch { @@ -29,13 +30,10 @@ public static class TargetResolver /// /// Validates whether a given target is valid for a move choice. Returns true if the target is valid. /// - public static bool IsValidTarget(byte side, byte position, MoveTarget target, IPokemon user) + public static bool IsValidTarget(byte side, byte position, MoveTarget target, IBattlePokemon user) { - var userBattleData = user.BattleData; - if (userBattleData == null) - throw new ArgumentNullException(nameof(user.BattleData)); - var userSide = userBattleData.SideIndex; - var userPosition = userBattleData.Position; + var userSide = user.SideIndex; + var userPosition = user.Position; switch (target) { @@ -80,7 +78,7 @@ public static class TargetResolver throw new ArgumentOutOfRangeException(nameof(target), target, null); } - private static IReadOnlyList GetAllTargets(IBattle battle) => + private static IReadOnlyList GetAllTargets(IBattle battle) => battle.Sides.SelectMany(x => x.Pokemon).ToList(); private static byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0; @@ -89,7 +87,7 @@ public static class TargetResolver /// Gets all Pokémon that are adjacent to of directly opposite of a Pokémon. This means the target, /// the Pokémon left of it, the Pokémon right of it, and the Pokémon opposite of it. /// - private static IReadOnlyList GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position) + private static IReadOnlyList GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position) { var left = position - 1; var right = position + 1; @@ -123,7 +121,7 @@ public static class TargetResolver ]; } - private static IReadOnlyList GetAllAdjacent(IBattle battle, byte side, byte position) + private static IReadOnlyList GetAllAdjacent(IBattle battle, byte side, byte position) { var left = position - 1; var right = position + 1; diff --git a/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs b/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs index 63a3aed..26f36e1 100644 --- a/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs +++ b/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs @@ -85,7 +85,7 @@ public static class TurnRunner return; if (!choice.User.IsUsable) return; - if (choice.User.BattleData?.IsOnBattlefield != true) + if (!choice.User.IsOnBattlefield) return; switch (choice) { @@ -108,9 +108,6 @@ public static class TurnRunner private static void ExecuteSwitchChoice(IBattle battle, ISwitchChoice fleeChoice) { var user = fleeChoice.User; - var battleData = user.BattleData; - if (battleData == null) - return; var preventSwitch = false; fleeChoice.RunScriptHook(script => script.PreventSelfSwitch(fleeChoice, ref preventSwitch)); @@ -118,7 +115,7 @@ public static class TurnRunner return; foreach (var side in battle.Sides) { - if (side.Index == battleData.SideIndex) + if (side.Index == user.SideIndex) continue; foreach (var pokemon in side.Pokemon.WhereNotNull()) { @@ -129,16 +126,12 @@ public static class TurnRunner } } user.Volatile.Clear(); - var userSide = battle.Sides[battleData.SideIndex]; - userSide.SwapPokemon(battleData.Position, fleeChoice.SwitchTo); + user.BattleSide.SwapPokemon(user.Position, fleeChoice.SwitchTo); } private static void ExecuteFleeChoice(IBattle battle, IFleeChoice fleeChoice) { var user = fleeChoice.User; - var battleData = user.BattleData; - if (battleData == null) - return; if (!battle.CanFlee) return; @@ -150,7 +143,7 @@ public static class TurnRunner foreach (var side in battle.Sides) { - if (side.Index == battleData.SideIndex) + if (side.Index == user.SideIndex) continue; foreach (var pokemon in side.Pokemon.WhereNotNull()) { @@ -167,8 +160,7 @@ public static class TurnRunner return; } - var userSide = battle.Sides[battleData.SideIndex]; - userSide.MarkAsFled(); + user.BattleSide.MarkAsFled(); battle.EventHook.Invoke(new FleeEvent(user, true)); battle.ValidateBattleState(); } @@ -176,9 +168,6 @@ public static class TurnRunner private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice) { var user = itemChoice.User; - var battleData = user.BattleData; - if (battleData == null) - return; var target = itemChoice.GetTargetPokemon(battle); battle.EventHook.Invoke(new ItemUseEvent(user, itemChoice.Item)); itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, user, battle, battle.EventHook); diff --git a/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs b/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs index 3e41d04..643814f 100644 --- a/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs +++ b/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs @@ -12,7 +12,7 @@ public record AbilityTriggerEvent : IEventData /// /// The Pokémon whose ability is being triggered. /// - public IPokemon Pokemon { get; } + public IBattlePokemon Pokemon { get; } /// /// The ability that is being triggered for the Pokémon. @@ -22,7 +22,7 @@ public record AbilityTriggerEvent : IEventData public Dictionary? Metadata { get; init; } = null; /// - public AbilityTriggerEvent(IPokemon pokemon) + public AbilityTriggerEvent(IBattlePokemon pokemon) { Pokemon = pokemon; Ability = pokemon.ActiveAbility; diff --git a/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs b/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs index d8bb3db..6b16822 100644 --- a/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs +++ b/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs @@ -10,7 +10,7 @@ namespace PkmnLib.Dynamic.Events; public class CaptureAttemptEvent : IEventData { /// - public CaptureAttemptEvent(IPokemon target, CaptureResult result, IItem captureItem) + public CaptureAttemptEvent(IBattlePokemon target, CaptureResult result, IItem captureItem) { Target = target; Result = result; @@ -20,7 +20,7 @@ public class CaptureAttemptEvent : IEventData /// /// The Pokémon that is being captured. /// - public IPokemon Target { get; init; } + public IBattlePokemon Target { get; init; } /// /// The result of the capture attempt. diff --git a/PkmnLib.Dynamic/Events/DamageEvent.cs b/PkmnLib.Dynamic/Events/DamageEvent.cs index b1ea789..12ae68d 100644 --- a/PkmnLib.Dynamic/Events/DamageEvent.cs +++ b/PkmnLib.Dynamic/Events/DamageEvent.cs @@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events; public record DamageEvent : IEventData { /// - public DamageEvent(IPokemon pokemon, uint previousHealth, uint newHealth, DamageSource source) + public DamageEvent(IBattlePokemon pokemon, uint previousHealth, uint newHealth, DamageSource source) { Pokemon = pokemon; PreviousHealth = previousHealth; @@ -19,7 +19,7 @@ public record DamageEvent : IEventData /// /// The Pokemon that took damage. /// - public IPokemon Pokemon { get; init; } + public IBattlePokemon Pokemon { get; init; } /// /// The previous health of the Pokemon. diff --git a/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs b/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs index 8e0f488..34c5436 100644 --- a/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs +++ b/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs @@ -5,11 +5,11 @@ namespace PkmnLib.Dynamic.Events; public class DisplaySpeciesChangeEvent : IEventData { - public IPokemon Pokemon { get; } + public IBattlePokemon Pokemon { get; } public ISpecies? Species { get; } public IForm? Form { get; } - public DisplaySpeciesChangeEvent(IPokemon pokemon, ISpecies? species, IForm? form) + public DisplaySpeciesChangeEvent(IBattlePokemon pokemon, ISpecies? species, IForm? form) { Pokemon = pokemon; Species = species; diff --git a/PkmnLib.Dynamic/Events/FaintEvent.cs b/PkmnLib.Dynamic/Events/FaintEvent.cs index 5b2780a..54491c0 100644 --- a/PkmnLib.Dynamic/Events/FaintEvent.cs +++ b/PkmnLib.Dynamic/Events/FaintEvent.cs @@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events; public class FaintEvent : IEventData { /// - public FaintEvent(IPokemon pokemon) + public FaintEvent(IBattlePokemon pokemon) { Pokemon = pokemon; } @@ -16,7 +16,7 @@ public class FaintEvent : IEventData /// /// The Pokemon that fainted. /// - public IPokemon Pokemon { get; init; } + public IBattlePokemon Pokemon { get; init; } /// public EventBatchId BatchId { get; init; } = new(); diff --git a/PkmnLib.Dynamic/Events/FleeEvent.cs b/PkmnLib.Dynamic/Events/FleeEvent.cs index 2a82cad..5d893b1 100644 --- a/PkmnLib.Dynamic/Events/FleeEvent.cs +++ b/PkmnLib.Dynamic/Events/FleeEvent.cs @@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events; public class FleeEvent : IEventData { /// - public FleeEvent(IPokemon pokemon, bool success) + public FleeEvent(IBattlePokemon pokemon, bool success) { Pokemon = pokemon; Success = success; @@ -17,7 +17,7 @@ public class FleeEvent : IEventData /// /// The Pokémon that attempted to flee. /// - public IPokemon Pokemon { get; } + public IBattlePokemon Pokemon { get; } /// /// Indicates whether the flee attempt was successful. diff --git a/PkmnLib.Dynamic/Events/ItemUseEvent.cs b/PkmnLib.Dynamic/Events/ItemUseEvent.cs index 811816e..51629fb 100644 --- a/PkmnLib.Dynamic/Events/ItemUseEvent.cs +++ b/PkmnLib.Dynamic/Events/ItemUseEvent.cs @@ -5,13 +5,13 @@ namespace PkmnLib.Dynamic.Events; public record ItemUseEvent : IEventData { - public ItemUseEvent(IPokemon pokemon, IItem itemUsed) + public ItemUseEvent(IBattlePokemon pokemon, IItem itemUsed) { Pokemon = pokemon; ItemUsed = itemUsed; } - public IPokemon Pokemon { get; set; } + public IBattlePokemon Pokemon { get; set; } public IItem ItemUsed { get; set; } /// diff --git a/PkmnLib.Dynamic/Events/MoveHitEvent.cs b/PkmnLib.Dynamic/Events/MoveHitEvent.cs index 7969a84..3ec3e5c 100644 --- a/PkmnLib.Dynamic/Events/MoveHitEvent.cs +++ b/PkmnLib.Dynamic/Events/MoveHitEvent.cs @@ -20,10 +20,10 @@ public class MoveHitEvent : IEventData /// /// The target of the move. /// - public IPokemon Target { get; } + public IBattlePokemon Target { get; } /// - public MoveHitEvent(IExecutingMove executingMove, IHitData hitData, IPokemon target) + public MoveHitEvent(IExecutingMove executingMove, IHitData hitData, IBattlePokemon target) { ExecutingMove = executingMove; HitData = hitData; diff --git a/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs b/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs index d8a7bf1..f3b5fc1 100644 --- a/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs +++ b/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs @@ -5,9 +5,9 @@ namespace PkmnLib.Dynamic.Events; public class MoveInvulnerableEvent : IEventData { public IExecutingMove ExecutingMove { get; } - public IPokemon Target { get; } + public IBattlePokemon Target { get; } - public MoveInvulnerableEvent(IExecutingMove executingMove, IPokemon target) + public MoveInvulnerableEvent(IExecutingMove executingMove, IBattlePokemon target) { ExecutingMove = executingMove; Target = target; diff --git a/PkmnLib.Dynamic/Events/StatBoostEvent.cs b/PkmnLib.Dynamic/Events/StatBoostEvent.cs index ca01d33..101a998 100644 --- a/PkmnLib.Dynamic/Events/StatBoostEvent.cs +++ b/PkmnLib.Dynamic/Events/StatBoostEvent.cs @@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.Events; public class StatBoostEvent : IEventData { /// - public StatBoostEvent(IPokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost) + public StatBoostEvent(IBattlePokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost) { Pokemon = pokemon; Statistic = statistic; @@ -20,7 +20,7 @@ public class StatBoostEvent : IEventData /// /// The Pokemon that had its stat boosted. /// - public IPokemon Pokemon { get; } + public IBattlePokemon Pokemon { get; } /// /// The statistic that was boosted. diff --git a/PkmnLib.Dynamic/Events/StatusChangeEvent.cs b/PkmnLib.Dynamic/Events/StatusChangeEvent.cs index 8cfe7dd..9c46a1d 100644 --- a/PkmnLib.Dynamic/Events/StatusChangeEvent.cs +++ b/PkmnLib.Dynamic/Events/StatusChangeEvent.cs @@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.Events; public record StatusChangeEvent : IEventData { /// - public StatusChangeEvent(IPokemon pokemon, StringKey? previousStatus, StringKey? newStatus) + public StatusChangeEvent(IBattlePokemon pokemon, StringKey? previousStatus, StringKey? newStatus) { Pokemon = pokemon; PreviousStatus = previousStatus; @@ -19,7 +19,7 @@ public record StatusChangeEvent : IEventData /// /// The Pokémon whose status has changed. /// - public IPokemon Pokemon { get; } + public IBattlePokemon Pokemon { get; } /// /// The new status of the Pokémon after the change. diff --git a/PkmnLib.Dynamic/Events/SwitchEvent.cs b/PkmnLib.Dynamic/Events/SwitchEvent.cs index 95289d2..7ea0748 100644 --- a/PkmnLib.Dynamic/Events/SwitchEvent.cs +++ b/PkmnLib.Dynamic/Events/SwitchEvent.cs @@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events; public class SwitchEvent : IEventData { /// - public SwitchEvent(byte sideIndex, byte position, IPokemon? pokemon) + public SwitchEvent(byte sideIndex, byte position, IBattlePokemon? pokemon) { SideIndex = sideIndex; Position = position; @@ -28,7 +28,7 @@ public class SwitchEvent : IEventData /// /// The Pokémon that is switching in. If null, no Pokémon is switching in, and the slot is empty after the switch. /// - public IPokemon? Pokemon { get; init; } + public IBattlePokemon? Pokemon { get; init; } /// public EventBatchId BatchId { get; init; } diff --git a/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs b/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs index 99b64c6..4dea97f 100644 --- a/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs +++ b/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs @@ -21,15 +21,16 @@ public interface IBattleStatCalculator /// /// Calculate all the boosted stats of a Pokemon, including stat boosts. /// - void CalculateBoostedStats(IPokemon pokemon, StatisticSet stats); + void CalculateBoostedStats(IBattlePokemon pokemon, StatisticSet stats); /// /// Calculate a single boosted stat of a Pokemon, including stat boosts. /// - uint CalculateBoostedStat(IPokemon pokemon, Statistic stat); + uint CalculateBoostedStat(IBattlePokemon pokemon, Statistic stat); /// /// Calculates the accuracy for a move, taking into account any accuracy modifiers. /// - byte CalculateModifiedAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, byte moveAccuracy); + byte CalculateModifiedAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, + byte moveAccuracy); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs b/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs index c575899..6092211 100644 --- a/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs +++ b/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs @@ -46,5 +46,5 @@ public interface ICaptureLibrary /// /// Attempts to capture a Pokémon using a specified item (e.g., Poké Ball). /// - CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random); + CaptureResult TryCapture(IBattlePokemon target, IItem captureItem, IBattleRandom random); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Libraries/DamageCalculator.cs b/PkmnLib.Dynamic/Libraries/DamageCalculator.cs index 93f3590..56e7bbb 100644 --- a/PkmnLib.Dynamic/Libraries/DamageCalculator.cs +++ b/PkmnLib.Dynamic/Libraries/DamageCalculator.cs @@ -11,16 +11,16 @@ public interface IDamageCalculator /// /// Calculate the damage for a given hit on a Pokemon. /// - uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IPokemon user, IPokemon target, + uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IBattlePokemon user, IBattlePokemon target, int targetCount, byte hitNumber, IHitData hitData); /// /// Calculate the base power for a given hit on a Pokemon. /// - ushort GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData); + ushort GetBasePower(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, IHitData hitData); /// /// Returns whether a specified hit should be critical or not. /// - bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber); + bool IsCritical(IBattle battle, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Libraries/MiscLibrary.cs b/PkmnLib.Dynamic/Libraries/MiscLibrary.cs index 8178190..1b1ca65 100644 --- a/PkmnLib.Dynamic/Libraries/MiscLibrary.cs +++ b/PkmnLib.Dynamic/Libraries/MiscLibrary.cs @@ -13,7 +13,7 @@ public interface IMiscLibrary /// Returns the choice that's used when a Pokemon is unable to make the move choice it wants to, or when it has no /// moves left, yet wants to make a move. /// - ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition); + ITurnChoice ReplacementChoice(IBattlePokemon user, byte targetSide, byte targetPosition); /// /// Returns whether the given choice is the choice that is used when the user is unable to make a move choice. diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs index 47ebe11..5d4698d 100644 --- a/PkmnLib.Dynamic/Models/Battle.cs +++ b/PkmnLib.Dynamic/Models/Battle.cs @@ -92,7 +92,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable /// /// Get a Pokemon on the battlefield, on a specific side and an index on that side. /// - IPokemon? GetPokemon(byte side, byte position); + IBattlePokemon? GetPokemon(byte side, byte position); /// /// Returns whether a slot on the battlefield can still be filled. If no party is responsible @@ -116,7 +116,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable /// Checks whether a Pokemon has a forced turn choice. If it does, this returns true and the choice /// is set in the out parameter. If it does not, this returns false and the out parameter is null. /// - bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice); + bool HasForcedTurn(IBattlePokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice); /// /// Checks whether a choice is actually possible. @@ -202,6 +202,8 @@ public class BattleImpl : ScriptSource, IBattle Sides = sides; Random = randomSeed.HasValue ? new BattleRandomImpl(randomSeed.Value) : new BattleRandomImpl(); EventHook = new EventHook(); + foreach (var party in parties) + party.Initialize(this); } /// @@ -248,12 +250,22 @@ public class BattleImpl : ScriptSource, IBattle public BattleChoiceQueue? ChoiceQueue { get; private set; } /// - public IPokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position]; + public IBattlePokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position]; /// public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x => x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField()); + private readonly List _capturedPokemon = []; + + /// + /// Attaches battle-wide result data, such as the captured Pokémon, to a result. + /// + private BattleResult FinalizeResult(BattleResult result) => result with + { + CapturedPokemon = _capturedPokemon.ToList(), + }; + /// public void ValidateBattleState() { @@ -265,7 +277,7 @@ public class BattleImpl : ScriptSource, IBattle { if (side.HasFledBattle) { - Result = BattleResult.Inconclusive; + Result = FinalizeResult(BattleResult.Inconclusive); HasEnded = true; return; } @@ -283,13 +295,13 @@ public class BattleImpl : ScriptSource, IBattle // If every side is defeated, the battle is a draw if (!survivingSideExists) { - Result = BattleResult.Inconclusive; + Result = FinalizeResult(BattleResult.Inconclusive); HasEnded = true; return; } // If only one side is left, that side has won - Result = BattleResult.Conclusive(survivingSide!.Index); + Result = FinalizeResult(BattleResult.Conclusive(survivingSide!.Index)); HasEnded = true; } @@ -297,22 +309,15 @@ public class BattleImpl : ScriptSource, IBattle public void ForceEndBattle() { HasEnded = true; - Result = BattleResult.Inconclusive; + Result = FinalizeResult(BattleResult.Inconclusive); } /// - public bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice) + public bool HasForcedTurn(IBattlePokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice) { - var battleData = pokemon.BattleData; - if (battleData == null) - { - choice = null; - return false; - } - ITurnChoice? forcedChoice = null; pokemon.RunScriptHook(script => - script.ForceTurnSelection(this, battleData.SideIndex, battleData.Position, ref forcedChoice)); + script.ForceTurnSelection(this, pokemon.SideIndex, pokemon.Position, ref forcedChoice)); choice = forcedChoice; return choice != null; } @@ -346,7 +351,7 @@ public class BattleImpl : ScriptSource, IBattle if (!switchChoice.SwitchTo.IsUsable) return false; // Can't switch to a Pokémon already on the field - if (switchChoice.SwitchTo.BattleData is { IsOnBattlefield: true }) + if (switchChoice.SwitchTo.IsOnBattlefield) return false; if (switchChoice.SwitchTo == switchChoice.User) return false; @@ -389,10 +394,10 @@ public class BattleImpl : ScriptSource, IBattle { if (!CanUse(choice)) return false; - if (choice.User.BattleData?.IsOnBattlefield != true) + if (!choice.User.IsOnBattlefield) return false; - var side = Sides[choice.User.BattleData!.SideIndex]; - side.SetChoice(choice.User.BattleData!.Position, choice); + var side = Sides[choice.User.SideIndex]; + side.SetChoice(choice.User.Position, choice); CheckChoicesSetAndRun(); return true; } @@ -555,8 +560,8 @@ public class BattleImpl : ScriptSource, IBattle if (attemptCapture.IsCaught) { target.MarkAsCaught(); - var side = Sides[target.BattleData!.SideIndex]; - side.ForceClearPokemonFromField(target.BattleData.Position); + _capturedPokemon.Add(target.UnderlyingPokemon); + target.BattleSide.ForceClearPokemonFromField(target.Position); } EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture, item)); @@ -592,9 +597,9 @@ public class BattleImpl : ScriptSource, IBattle { foreach (var party in Parties) { - foreach (var pokemon in party.Party.WhereNotNull()) + foreach (var pokemon in party.BattlePokemon.WhereNotNull()) { - pokemon.ClearBattleData(); + pokemon.OnBattleEnd(); } } _weatherScript.Clear(); diff --git a/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs b/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs index ffd31ce..59d7182 100644 --- a/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs +++ b/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs @@ -87,7 +87,7 @@ public class BattleChoiceQueue : IDeepCloneable /// /// Returns true if the Pokémon was found and moved, false otherwise. /// - public bool MovePokemonChoiceNext(IPokemon pokemon) + public bool MovePokemonChoiceNext(IBattlePokemon pokemon) { var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon); if (index == -1) @@ -110,7 +110,7 @@ public class BattleChoiceQueue : IDeepCloneable /// /// Returns true if the Pokémon was found and moved, false otherwise. /// - public bool MovePokemonChoiceLast(IPokemon pokemon) + public bool MovePokemonChoiceLast(IBattlePokemon pokemon) { var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon); if (index == -1) diff --git a/PkmnLib.Dynamic/Models/BattleParty.cs b/PkmnLib.Dynamic/Models/BattleParty.cs index 3639770..aee4fb3 100644 --- a/PkmnLib.Dynamic/Models/BattleParty.cs +++ b/PkmnLib.Dynamic/Models/BattleParty.cs @@ -4,15 +4,35 @@ namespace PkmnLib.Dynamic.Models; /// /// A battle party is a wrapper around a Pokemon party that provides additional functionality for battles. -/// It indicates for which side and position the party is responsible. +/// It indicates for which side and position the party is responsible, and holds the battle-scoped +/// wrappers for the party's Pokémon. /// public interface IBattleParty : IDeepCloneable { /// - /// The backing Pokemon party. + /// The backing Pokemon party. Battle code should generally use instead, as that + /// view contains the battle-scoped wrappers. /// IPokemonParty Party { get; } + /// + /// The battle-scoped view of the party. Index-aligned with . Only available after + /// has been called by the battle. + /// + IReadOnlyList BattlePokemon { get; } + + /// + /// Gets the battle-scoped wrapper for a Pokémon in this party. Returns null if the Pokémon is not in this + /// party. Accepts either the underlying Pokémon or the wrapper itself. + /// + IBattlePokemon? GetBattlePokemon(IPokemon pokemon); + + /// + /// Initializes the battle-scoped wrappers for this party. This is called by the battle when it is created, + /// and should not be called by user code. + /// + void Initialize(IBattle battle); + /// /// Whether the party is responsible for the specified side and position. /// @@ -26,7 +46,7 @@ public interface IBattleParty : IDeepCloneable /// /// Gets all usable Pokemon that are not currently in the field. /// - IEnumerable GetUsablePokemonNotInField(); + IEnumerable GetUsablePokemonNotInField(); } /// @@ -39,6 +59,8 @@ public record struct ResponsibleIndex(byte Side, byte Position); public class BattlePartyImpl : IBattleParty { private readonly ResponsibleIndex[] _responsibleIndices; + private IBattlePokemon?[] _battlePokemon = []; + private IBattle? _battle; /// public BattlePartyImpl(IPokemonParty party, ResponsibleIndex[] responsibleIndices) @@ -50,14 +72,53 @@ public class BattlePartyImpl : IBattleParty /// public IPokemonParty Party { get; } + /// + public IReadOnlyList BattlePokemon => _battlePokemon; + + /// + public IBattlePokemon? GetBattlePokemon(IPokemon pokemon) => + _battlePokemon.FirstOrDefault(x => x != null && (x == pokemon || x.UnderlyingPokemon == pokemon)); + + /// + public void Initialize(IBattle battle) + { + if (_battle != null) + throw new InvalidOperationException("This battle party has already been initialized for a battle."); + if (_responsibleIndices.Length == 0) + throw new InvalidOperationException("A battle party must be responsible for at least one position."); + var sideIndex = _responsibleIndices[0].Side; + if (_responsibleIndices.Any(x => x.Side != sideIndex)) + throw new InvalidOperationException("A battle party can only be responsible for a single side."); + + _battle = battle; + _battlePokemon = new IBattlePokemon?[Party.Count]; + for (var i = 0; i < Party.Count; i++) + { + var pokemon = Party[i]; + if (pokemon != null) + _battlePokemon[i] = new BattlePokemonImpl(pokemon, battle, sideIndex); + } + + Party.OnSwapInto += (_, args) => + { + var (pokemon, index) = args; + _battlePokemon[index] = pokemon == null ? null : new BattlePokemonImpl(pokemon, battle, sideIndex); + }; + Party.OnSwap += (_, args) => + { + var (index1, index2) = args; + (_battlePokemon[index1], _battlePokemon[index2]) = (_battlePokemon[index2], _battlePokemon[index1]); + }; + } + /// public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index); /// public bool HasUsablePokemonNotInField() => - Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true); + _battlePokemon.WhereNotNull().Any(x => x.IsUsable && !x.IsOnBattlefield); /// - public IEnumerable GetUsablePokemonNotInField() => - Party.WhereNotNull().Where(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true); + public IEnumerable GetUsablePokemonNotInField() => + _battlePokemon.WhereNotNull().Where(x => x.IsUsable && !x.IsOnBattlefield); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/BattlePokemon.cs b/PkmnLib.Dynamic/Models/BattlePokemon.cs new file mode 100644 index 0000000..7f4f20e --- /dev/null +++ b/PkmnLib.Dynamic/Models/BattlePokemon.cs @@ -0,0 +1,1043 @@ +using System.Diagnostics.CodeAnalysis; +using PkmnLib.Dynamic.Events; +using PkmnLib.Dynamic.Libraries; +using PkmnLib.Dynamic.Models.Choices; +using PkmnLib.Dynamic.Models.Serialized; +using PkmnLib.Dynamic.ScriptHandling; +using PkmnLib.Static; +using PkmnLib.Static.Species; +using PkmnLib.Static.Utils; + +namespace PkmnLib.Dynamic.Models; + +/// +/// A Pokémon taking part in a battle. This wraps an and holds all state that is only +/// relevant for the duration of a single battle. The wrapper is ephemeral: it is created when the battle is +/// created, and simply dropped when the battle ends, taking all battle-only state with it. Persistent data is +/// proxied to the underlying . +/// +public interface IBattlePokemon : IPokemon +{ + /// + /// The persistent Pokémon this battle Pokémon wraps. + /// + IPokemon UnderlyingPokemon { get; } + + /// + /// The battle the Pokémon is in. + /// + IBattle Battle { get; } + + /// + /// The index of the side of the Pokémon. + /// + byte SideIndex { get; } + + /// + /// The index of the position of the Pokémon on the field. + /// + byte Position { get; } + + /// + /// The side the Pokémon is on. + /// + IBattleSide BattleSide { get; } + + /// + /// Whether the Pokémon is on the battlefield. + /// + bool IsOnBattlefield { get; } + + /// + /// A list of opponents the Pokémon has seen this battle. + /// + IReadOnlyList SeenOpponents { get; } + + /// + /// Adds an opponent to the list of seen opponents. + /// + void MarkOpponentAsSeen(IBattlePokemon opponent); + + /// + /// A list of items the Pokémon has consumed this battle. + /// + IReadOnlyList ConsumedItems { get; } + + /// + /// Marks an item as consumed. + /// + void MarkItemAsConsumed(IItem item); + + /// + /// The turn the Pokémon last switched in. + /// + uint SwitchInTurn { get; } + + /// + /// The number of turns the Pokémon has been on the field. + /// + uint TurnsOnField { get; } + + /// + /// The species of the Pokémon at the time the battle started. + /// + ISpecies OriginalSpecies { get; } + + /// + /// The form of the Pokémon at the time the battle started. + /// + IForm OriginalForm { get; } + + /// + /// The last move choice executed by the Pokémon. + /// + IMoveChoice? LastMoveChoice { get; set; } + + /// + /// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below -6. + /// + StatBoostStatisticSet StatBoost { get; } + + /// + /// The stats of the Pokemon including the stat boosts. + /// + StatisticSet BoostedStats { get; } + + /// + /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats. + /// This should be called when a stat boost changes. + /// + void RecalculateBoostedStats(); + + /// + /// Change a boosted stat by a certain amount. + /// + /// The stat to be changed + /// The amount to change the stat by + /// Whether the change was self-inflicted. This can be relevant in scripts. + /// Whether to skip the script hooks that can prevent or change the boost + /// The event batch ID this change is a part of. This is relevant for visual handling + bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default); + + /// + /// The volatile status scripts of the Pokemon. + /// + IScriptSet Volatile { get; } + + /// + /// The script for the ability. + /// + ScriptContainer AbilityScript { get; } + + /// + /// The script for the held item. + /// + ScriptContainer HeldItemTriggerScript { get; } + + /// + /// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy + /// ability. + /// + IAbility? OverrideAbility { get; } + + /// + /// Changes the ability of the Pokémon. + /// + bool ChangeAbility(IAbility ability); + + /// + /// Whether the ability of the Pokémon is suppressed. + /// + bool AbilitySuppressed { get; } + + /// + /// Suppresses the ability of the Pokémon. + /// + bool SuppressAbility(); + + /// + /// Returns the currently active ability, taking suppression and overrides into account. + /// + IAbility? ActiveAbility { get; } + + /// + /// An optional display species of the Pokemon. If this is set, the client should display this + /// species. An example of usage for this is the Illusion ability. + /// + ISpecies? DisplaySpecies { get; } + + /// + /// An optional display form of the Pokemon. If this is set, the client should display this + /// form. An example of usage for this is the Illusion ability. + /// + IForm? DisplayForm { get; } + + /// + /// Sets the display species and form of the Pokemon. This is used for abilities like Illusion. + /// + void SetDisplaySpecies(ISpecies? species, IForm? form); + + /// + /// The height of the Pokémon in meters. This can be changed during battle by effects such as Autotomize. + /// + new float HeightInMeters { get; set; } + + /// + /// Removes a type from the Pokémon. Returns whether the type was removed. + /// + bool RemoveType(TypeIdentifier type); + + /// + /// Adds a type to the Pokémon. Returns whether the type was added. It will not add the type if + /// 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); + + /// + /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as + /// Levitate. + /// + bool IsFloating { get; } + + /// + /// The permanently learned moves of the Pokemon, ignoring any temporary replacements made through + /// . This is of a set length of . Empty move + /// slots are null. + /// + IReadOnlyList BaseMoves { get; } + + /// + /// Temporarily replaces the move in the given slot until the Pokemon leaves the battlefield. The permanently + /// learned move in the slot is left untouched and becomes visible again automatically. Used by effects such as + /// Mimic. + /// + /// Thrown when the move is not found in the move library. + void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index); + + /// + /// Whether or not this Pokemon was caught this battle. + /// + bool IsCaught { get; } + + /// + /// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore. + /// + void MarkAsCaught(); + + /// + /// Whether the held item has been removed for the duration of the battle. + /// + bool HasItemBeenRemovedForBattle { get; } + + /// + /// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item. + /// + /// + /// This is used for moves that remove a held item, but do not consume it. The held item of the underlying + /// Pokémon is never touched; the removal simply ends with the battle. + /// + IItem? RemoveHeldItemForBattle(); + + /// + /// Tries to steal the held item of the Pokémon. If successful, the item is removed from the Pokémon and returned. + /// If the Pokémon does not have a held item, or the item is a form changer, this will return false. + /// + bool TryStealHeldItem([NotNullWhen(true)] out IItem? item); + + /// + /// Restores the held item of a Pokémon if it was temporarily removed. + /// + void RestoreRemovedHeldItem(); + + /// + /// Makes the Pokemon uses its held item. Returns whether the item was consumed. + /// + bool ConsumeHeldItem(); + + /// + /// Called by the battle when the Pokémon is sent onto the battlefield. This should not be called by + /// user code; use instead. + /// + void OnSwitchedIn(byte position); + + /// + /// Called by the battle when the Pokémon leaves the battlefield. Resets all state that only lasts while + /// the Pokémon is on the field. This should not be called by user code. + /// + void OnSwitchedOut(); + + /// + /// Sets the position the Pokémon has within its side, without running switch handling. + /// + void SetPosition(byte position); + + /// + /// Called when the battle has ended. Reverts the few battle effects that write through to the underlying + /// Pokémon, such as battle-only forms. + /// + void OnBattleEnd(); +} + +/// +public class BattlePokemonImpl : ScriptSource, IBattlePokemon +{ + private readonly IPokemon _pokemon; + + /// + public BattlePokemonImpl(IPokemon pokemon, IBattle battle, byte sideIndex) + { + _pokemon = pokemon; + Battle = battle; + SideIndex = sideIndex; + OriginalSpecies = pokemon.Species; + OriginalForm = pokemon.Form; + Volatile = new ScriptSet(this); + _types = pokemon.Types.ToList(); + HeightInMeters = pokemon.HeightInMeters; + _heldItem = pokemon.HeldItem; + RecalculateBoostedStats(); + // A status that was set outside of the battle is parented to the underlying Pokémon. Re-parent it to + // this wrapper, so that it has battle context for the duration of the battle. + pokemon.StatusScript.Script?.OnAddedToParent(this); + } + + /// + public IPokemon UnderlyingPokemon => _pokemon; + + /// + public IBattle Battle { get; } + + /// + public byte SideIndex { get; } + + /// + public byte Position { get; private set; } + + /// + public IBattleSide BattleSide => Battle.Sides[SideIndex]; + + /// + public bool IsOnBattlefield { get; private set; } + + private readonly List _seenOpponents = []; + + /// + public IReadOnlyList SeenOpponents => _seenOpponents; + + /// + public void MarkOpponentAsSeen(IBattlePokemon opponent) + { + if (!_seenOpponents.Contains(opponent)) + _seenOpponents.Add(opponent); + } + + private readonly List _consumedItems = []; + + /// + public IReadOnlyList ConsumedItems => _consumedItems; + + /// + public void MarkItemAsConsumed(IItem item) + { + _consumedItems.Add(item); + BattleSide.SetConsumedItem(Position, item); + } + + /// + public uint SwitchInTurn { get; private set; } + + /// + public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn; + + /// + public ISpecies OriginalSpecies { get; } + + /// + public IForm OriginalForm { get; } + + /// + public IMoveChoice? LastMoveChoice { get; set; } + + /// + public void OnSwitchedIn(byte position) + { + Position = position; + SwitchInTurn = Battle.CurrentTurnNumber; + IsOnBattlefield = true; + ResolveAbilityScript(); + } + + /// + public void OnSwitchedOut() + { + IsOnBattlefield = false; + Volatile.Clear(); + _temporaryMoves = null; + HeightInMeters = _pokemon.Form.Height; + _types = _pokemon.Form.Types.ToList(); + OverrideAbility = null; + AbilitySuppressed = false; + StatBoost.Reset(); + RecalculateBoostedStats(); + } + + /// + public void SetPosition(byte position) => Position = position; + + /// + public void OnBattleEnd() + { + if (_pokemon.Form.IsBattleOnlyForm) + { + _pokemon.ChangeForm(OriginalSpecies == _pokemon.Species ? OriginalForm : _pokemon.Species.GetDefaultForm()); + } + } + + private void ResolveAbilityScript() + { + var ability = ActiveAbility; + if (ability != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, + ability.Parameters, out var abilityScript)) + { + AbilityScript.Set(abilityScript); + abilityScript.OnAddedToParent(this); + } + else + { + AbilityScript.Clear(); + } + } + + /// + public IDynamicLibrary Library => _pokemon.Library; + + /// + public ISpecies Species => _pokemon.Species; + + /// + public IForm Form => _pokemon.Form; + + /// + public LevelInt Level => _pokemon.Level; + + /// + public uint Experience => _pokemon.Experience; + + /// + public uint PersonalityValue => _pokemon.PersonalityValue; + + /// + public Gender Gender => _pokemon.Gender; + + /// + public byte Coloring => _pokemon.Coloring; + + /// + public bool IsShiny => _pokemon.IsShiny; + + /// + public uint CurrentHealth => _pokemon.CurrentHealth; + + /// + public byte Happiness + { + get => _pokemon.Happiness; + set => _pokemon.Happiness = value; + } + + /// + public StatisticSet FlatStats => _pokemon.FlatStats; + + /// + public IndividualValueStatisticSet IndividualValues => _pokemon.IndividualValues; + + /// + public EffortValueStatisticSet EffortValues => _pokemon.EffortValues; + + /// + public INature Nature => _pokemon.Nature; + + /// + public string? Nickname => _pokemon.Nickname; + + /// + public AbilityIndex AbilityIndex => _pokemon.AbilityIndex; + + /// + public IAbility Ability => _pokemon.Ability; + + /// + public bool AllowedExperience => _pokemon.AllowedExperience; + + /// + public bool IsEgg => _pokemon.IsEgg; + + /// + public ScriptContainer StatusScript => _pokemon.StatusScript; + + /// + public int? GetStatusTurnsLeft => _pokemon.GetStatusTurnsLeft; + + /// + public bool HasStatus(StringKey status) => _pokemon.HasStatus(status); + + /// + public bool IsFainted => _pokemon.IsFainted; + + /// + public SerializedPokemon Serialize() => _pokemon.Serialize(); + + /// + public bool AddExperience(uint experience, EventHook? eventHook = null) => + _pokemon.AddExperience(experience, eventHook ?? Battle.EventHook); + + /// + public void RestoreAllPP() + { + foreach (var move in Moves) + { + move?.RestoreAllUses(); + } + } + + /// + public void LearnMove(StringKey moveName, MoveLearnMethod method, byte index) => + _pokemon.LearnMove(moveName, method, index); + + /// + public void ChangeLevelBy(int change) + { + _pokemon.ChangeLevelBy(change); + RecalculateBoostedStats(); + } + + /// + public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null) => + _pokemon.EvolveTo(evolution, eventHook ?? Battle.EventHook); + + /// + public StatBoostStatisticSet StatBoost { get; } = new(); + + /// + public StatisticSet BoostedStats { get; } = new(); + + /// + public uint MaxHealth => BoostedStats.Hp; + + /// + public void RecalculateFlatStats() + { + _pokemon.RecalculateFlatStats(); + RecalculateBoostedStats(); + } + + /// + public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats); + + /// + public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, + EventBatchId batchId = default) + { + if (!force) + { + var prevented = false; + this.RunScriptHook(script => + script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented)); + if (prevented) + return false; + this.RunScriptHook(script => + script.ChangeStatBoostChange(this, stat, selfInflicted, ref change)); + if (change == 0) + return false; + } + var changed = false; + var oldBoost = StatBoost.GetStatistic(stat); + changed = change switch + { + > 0 => StatBoost.IncreaseStatistic(stat, change), + < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change), + _ => changed, + }; + if (!changed) + return false; + var newBoost = StatBoost.GetStatistic(stat); + Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost) + { + BatchId = batchId, + }); + + RecalculateBoostedStats(); + this.RunScriptHook(script => + script.OnAfterStatBoostChange(this, stat, selfInflicted, change)); + return true; + } + + /// + public ISpecies? DisplaySpecies { get; private set; } + + /// + public IForm? DisplayForm { get; private set; } + + /// + public void SetDisplaySpecies(ISpecies? species, IForm? form) + { + DisplaySpecies = species; + DisplayForm = form; + + Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form) + { + BatchId = new EventBatchId(), + }); + } + + /// + public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null) + { + var oldAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex); + _pokemon.ChangeSpecies(species, form, eventHook ?? Battle.EventHook, random ?? Battle.Random); + OnAfterFormWriteThrough(oldAbilityName); + } + + /// + public void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null) + { + if (form == Form) + return; + var oldAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex); + _pokemon.ChangeForm(form, batchId, eventHook ?? Battle.EventHook); + OnAfterFormWriteThrough(oldAbilityName); + } + + /// + /// A form or species change writes through to the underlying Pokémon; the battle overlays that derive + /// from the form need to be re-initialized from the new form. + /// + private void OnAfterFormWriteThrough(StringKey oldAbilityName) + { + _types = _pokemon.Form.Types.ToList(); + HeightInMeters = _pokemon.Form.Height; + var newAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex); + if (OverrideAbility == null && !AbilitySuppressed && oldAbilityName != newAbilityName) + ResolveAbilityScript(); + RecalculateBoostedStats(); + } + + private List _types; + + /// + public IReadOnlyList Types => _types; + + /// + public bool RemoveType(TypeIdentifier type) => _types.Remove(type); + + /// + public bool AddType(TypeIdentifier type) + { + if (_types.Contains(type)) + return false; + _types.Add(type); + return true; + } + + /// + public void SetTypes(IReadOnlyList types) => _types = types.ToList(); + + /// + public float HeightInMeters { get; set; } + + /// + public float WeightInKg + { + get + { + var weight = _pokemon.WeightInKg; + // ReSharper disable once AccessToModifiedClosure + this.RunScriptHook(script => script.ModifyWeight(ref weight)); + if (weight < 0.1f) + weight = 0.1f; + return weight; + } + } + + private static readonly StringKey FlyingTypeName = "flying"; + + /// + public bool IsFloating + { + get + { + var isFloating = Types.Any(x => x.Name == FlyingTypeName); + this.RunScriptHook(x => x.IsFloating(this, ref isFloating)); + return isFloating; + } + } + + /// + public IAbility? OverrideAbility { get; private set; } + + /// + public bool AbilitySuppressed { get; private set; } + + /// + public IAbility? ActiveAbility + { + get + { + if (AbilitySuppressed) + return null; + if (OverrideAbility != null) + return OverrideAbility; + return _pokemon.Ability; + } + } + + /// + public bool SuppressAbility() + { + if (ActiveAbility?.CanBeChanged == false) + return false; + + AbilitySuppressed = true; + AbilityScript.Clear(); + return true; + } + + /// + 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)) + { + AbilityScript.Set(abilityScript); + abilityScript.OnAddedToParent(this); + } + else + { + AbilityScript.Clear(); + } + return true; + } + + /// + /// Battle-only per-slot overrides of the underlying moveset. The permanent moveset is never mutated by + /// temporary moves; discarding this array is all that is needed to restore the original moves. + /// + private ILearnedMove?[]? _temporaryMoves; + + /// + public IReadOnlyList Moves => + _temporaryMoves?.Select((move, index) => move ?? BaseMoves[index]).ToArray() ?? BaseMoves; + + /// + public IReadOnlyList BaseMoves => _pokemon.Moves; + + /// + public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName); + + /// + public void SwapMoves(byte index1, byte index2) + { + if (index1 >= Const.MovesCount || index2 >= Const.MovesCount) + return; + _pokemon.SwapMoves(index1, index2); + if (_temporaryMoves != null) + (_temporaryMoves[index1], _temporaryMoves[index2]) = (_temporaryMoves[index2], _temporaryMoves[index1]); + } + + /// + public void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index) + { + if (index >= Const.MovesCount) + throw new ArgumentOutOfRangeException(nameof(index), $"Move slot {index} is out of range."); + if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move)) + throw new KeyNotFoundException($"Move {moveName} not found."); + + _temporaryMoves ??= new ILearnedMove?[Const.MovesCount]; + _temporaryMoves[index] = new LearnedMoveImpl(move, method); + } + + private IItem? _heldItem; + + /// + public IItem? HeldItem => _heldItem; + + /// + public bool HasHeldItem(StringKey itemName) => _heldItem?.Name == itemName; + + /// + public IItem? ForceSetHeldItem(IItem? item) + { + var previous = _heldItem; + _heldItem = item; + this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item)); + return previous; + } + + /// + public IItem? RemoveHeldItem() + { + if (_heldItem is not null && _heldItem.Category == ItemCategory.FormChanger) + return null; + var previous = _heldItem; + _heldItem = null; + this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null)); + return previous; + } + + private IItem? _removedHeldItem; + + /// + public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null; + + /// + public IItem? RemoveHeldItemForBattle() => _removedHeldItem = RemoveHeldItem(); + + /// + public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item) + { + if (_heldItem is null || _heldItem.Category == ItemCategory.FormChanger) + { + item = null; + return false; + } + var prevent = false; + this.RunScriptHook(script => + script.PreventHeldItemSteal(this, _heldItem, ref prevent)); + if (prevent) + { + item = null; + return false; + } + item = RemoveHeldItemForBattle(); + return item is not null; + } + + /// + public void RestoreRemovedHeldItem() + { + _ = ForceSetHeldItem(_removedHeldItem); + _removedHeldItem = null; + } + + /// + public bool ConsumeHeldItem() + { + if (_heldItem is null) + return false; + if (!Library.ScriptResolver.TryResolveBattleItemScript(_heldItem, out _)) + return false; + + var prevented = false; + this.RunScriptHook(script => + script.PreventHeldItemConsume(this, _heldItem, ref prevented)); + if (prevented) + return false; + MarkItemAsConsumed(_heldItem); + + UseItem(ForceSetHeldItem(null)!); + return true; + } + + /// + public void UseItem(IItem item) + { + // TODO: actually consume the item + + this.RunScriptHook(x => x.OnAfterItemConsume(this, item)); + } + + /// + public bool IsCaught { get; private set; } + + /// + public void MarkAsCaught() + { + IsCaught = true; + } + + /// + public bool IsUsable => !IsCaught && _pokemon.IsUsable; + + /// + public void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false) + { + if (IsFainted) + return; + if (!forceDamage) + { + var dmg = damage; + this.RunScriptHook(script => + script.ChangeIncomingDamage(this, source, ref dmg)); + damage = dmg; + } + if (damage == 0) + return; + + // If the damage is more than the current health, we cap it at the current health, to prevent + // underflow. + if (damage >= CurrentHealth) + damage = CurrentHealth; + var newHealth = CurrentHealth - damage; + // Trigger an event to the front-end. + Battle.EventHook.Invoke(new DamageEvent(this, CurrentHealth, newHealth, source) + { + BatchId = batchId, + }); + // And allow scripts to execute. + this.RunScriptHook(script => script.OnDamage(this, source, CurrentHealth, newHealth)); + + _pokemon.Damage(damage, source, batchId, true); + + // If the Pokémon is now fainted, we also run faint handling. + if (IsFainted) + { + OnFaint(source); + } + } + + /// + public void Faint(DamageSource source, EventBatchId batchId = default) + { + _pokemon.Faint(source, batchId); + OnFaint(source); + } + + private void OnFaint(DamageSource source) + { + // Trigger the faint event to the front-end. + Battle.EventHook.Invoke(new FaintEvent(this)); + + // Allow scripts to trigger based on the faint. + this.RunScriptHook(script => script.OnFaint(this, source)); + foreach (var ally in BattleSide.Pokemon.WhereNotNull().Where(x => x != this)) + { + ally.RunScriptHook(script => script.OnAllyFaint(ally, this)); + } + + // Make sure the OnRemove script is run. + this.RunScriptHook(script => script.OnRemove()); + + // Mark the position as unfillable if it can't be filled by any party. + if (!Battle.CanSlotBeFilled(SideIndex, Position)) + { + BattleSide.MarkPositionAsUnfillable(Position); + } + BattleSide.MarkFaint(Position); + BattleSide.ForceClearPokemonFromField(Position); + + foreach (var opponent in SeenOpponents) + { + if (!opponent.IsUsable) + continue; + if (!opponent.AllowedExperience) + continue; + opponent.AddExperience(Library.ExperienceGainCalculator.CalculateExperienceGain(this, opponent)); + } + + // Validate the battle state to see if the battle is over. + Battle.ValidateBattleState(); + } + + /// + public bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false, + EventHook? customEventHook = null) + { + if (IsFainted && !allowRevive) + return false; + + var maxAmount = BoostedStats.Hp - CurrentHealth; + if (heal > maxAmount) + heal = maxAmount; + if (heal == 0) + return false; + if (!forceHeal) + { + var prevented = false; + this.RunScriptHook(x => x.PreventHeal(this, heal, allowRevive, ref prevented)); + if (prevented) + return false; + } + + customEventHook ??= Battle.EventHook; + return _pokemon.Heal(heal, allowRevive, batchId, true, customEventHook); + } + + /// + public bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default) + { + if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript)) + throw new KeyNotFoundException($"Status script {status} not found"); + + if (!StatusScript.IsEmpty) + return false; + var oldStatus = StatusScript.Script?.Name; + var selfInflicted = originPokemon == this || + (originPokemon is IBattlePokemon origin && origin.UnderlyingPokemon == _pokemon); + + var preventStatus = false; + this.RunScriptHook(script => + script.PreventStatusChange(this, status, selfInflicted, ref preventStatus)); + if (preventStatus) + return false; + + StatusScript.Set(statusScript); + statusScript.OnAddedToParent(this); + Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status) + { + BatchId = batchId, + }); + this.RunScriptHook(script => + script.OnAfterStatusChange(this, status, originPokemon)); + return true; + } + + /// + public void ClearStatus(EventBatchId batchId = default) + { + var oldStatus = StatusScript.Script?.Name; + _pokemon.ClearStatus(batchId); + Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, null) + { + BatchId = batchId, + }); + } + + /// + public ScriptContainer HeldItemTriggerScript { get; } = new(); + + /// + public ScriptContainer AbilityScript { get; } = new(); + + /// + public IScriptSet Volatile { get; } + + /// + public override int ScriptCount => 4 + BattleSide.ScriptCount; + + /// + public override void GetOwnScripts(List> scripts) + { + scripts.Add(HeldItemTriggerScript); + scripts.Add(AbilityScript); + scripts.Add(StatusScript); + scripts.Add(Volatile); + } + + /// + public override void CollectScripts(List> scripts) + { + GetOwnScripts(scripts); + BattleSide.CollectScripts(scripts); + } + + /// + public override string ToString() => _pokemon.ToString()!; +} \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/BattleRandom.cs b/PkmnLib.Dynamic/Models/BattleRandom.cs index 013ce3c..fb7fdee 100644 --- a/PkmnLib.Dynamic/Models/BattleRandom.cs +++ b/PkmnLib.Dynamic/Models/BattleRandom.cs @@ -13,7 +13,7 @@ public interface IBattleRandom : IRandom, IDeepCloneable /// rolls whether it triggers. As a side effect this run scripts to allow modifying this random /// chance. /// - bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber); + bool EffectChance(float chance, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber); } /// @@ -36,7 +36,7 @@ public class BattleRandomImpl : RandomImpl, IBattleRandom } /// - public bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber) + public bool EffectChance(float chance, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber) { executingMove.RunScriptHook(script => script.ChangeEffectChance(executingMove, target, hitNumber, ref chance)); diff --git a/PkmnLib.Dynamic/Models/BattleResult.cs b/PkmnLib.Dynamic/Models/BattleResult.cs index 62a24ce..b76ec8a 100644 --- a/PkmnLib.Dynamic/Models/BattleResult.cs +++ b/PkmnLib.Dynamic/Models/BattleResult.cs @@ -30,4 +30,11 @@ public record struct BattleResult /// The side that won the battle. If null, no side has won. /// public byte? WinningSide { get; } + + /// + /// The Pokémon that were captured during the battle. These are the underlying Pokémon, so they remain + /// valid after the battle has been disposed. This is set for both conclusive and inconclusive results, as + /// captures complete the moment they happen. + /// + public IReadOnlyList CapturedPokemon { get; init; } = []; } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/BattleSide.cs b/PkmnLib.Dynamic/Models/BattleSide.cs index b6cd47b..bb486f3 100644 --- a/PkmnLib.Dynamic/Models/BattleSide.cs +++ b/PkmnLib.Dynamic/Models/BattleSide.cs @@ -24,7 +24,7 @@ public interface IBattleSide : IScriptSource, IDeepCloneable /// /// A list of Pokémon currently on the battlefield. /// - IReadOnlyList Pokemon { get; } + IReadOnlyList Pokemon { get; } /// /// The currently set choices for all Pokémon on the battlefield. Cleared when the turn starts. @@ -84,7 +84,14 @@ public interface IBattleSide : IScriptSource, IDeepCloneable /// Switches out a spot on the field for a different Pokémon. If null is passed, the spot is /// cleared. Returns the Pokémon that was previously in the spot. /// - IPokemon? SwapPokemon(byte position, IPokemon? pokemon); + IBattlePokemon? SwapPokemon(byte position, IBattlePokemon? pokemon); + + /// + /// Sends out a Pokémon by its persistent representation. This resolves the battle-scoped wrapper for + /// the Pokémon from the parties in the battle, and swaps it into the given position. This is the + /// convenient entry point for hosts, which generally hold the persistent party Pokémon. + /// + IBattlePokemon? SendOut(byte position, IPokemon pokemon); /// /// Swaps two Pokémon on the side. @@ -94,7 +101,7 @@ public interface IBattleSide : IScriptSource, IDeepCloneable /// /// Checks whether a Pokemon is on the field in this side. /// - bool IsPokemonOnSide(IPokemon pokemon); + bool IsPokemonOnSide(IBattlePokemon pokemon); /// /// Marks a slot as unfillable. This happens when no parties are able to fill the slot anymore. @@ -166,7 +173,7 @@ public class BattleSideImpl : ScriptSource, IBattleSide { Index = index; NumberOfPositions = numberOfPositions; - _pokemon = new IPokemon?[numberOfPositions]; + _pokemon = new IBattlePokemon?[numberOfPositions]; _setChoices = new ITurnChoice?[numberOfPositions]; _fillablePositions = new bool[numberOfPositions]; for (byte i = 0; i < numberOfPositions; i++) @@ -183,10 +190,10 @@ public class BattleSideImpl : ScriptSource, IBattleSide /// public byte NumberOfPositions { get; } - private readonly IPokemon?[] _pokemon; + private readonly IBattlePokemon?[] _pokemon; /// - public IReadOnlyList Pokemon => _pokemon; + public IReadOnlyList Pokemon => _pokemon; private readonly ITurnChoice?[] _setChoices; @@ -249,28 +256,31 @@ public class BattleSideImpl : ScriptSource, IBattleSide if (pokemon is not null) { pokemon.RunScriptHook(script => script.OnRemove()); - pokemon.SetOnBattlefield(false); + pokemon.OnSwitchedOut(); } _pokemon[index] = null; } /// - public IPokemon? SwapPokemon(byte position, IPokemon? pokemon) + public IBattlePokemon? SwapPokemon(byte position, IBattlePokemon? pokemon) { var oldPokemon = _pokemon[position]; if (oldPokemon is not null) { oldPokemon.RunScriptHook(script => script.OnSwitchOut(oldPokemon, position)); oldPokemon.RunScriptHook(script => script.OnRemove()); - oldPokemon.SetOnBattlefield(false); + oldPokemon.OnSwitchedOut(); } _pokemon[position] = pokemon; if (pokemon is not null) { - pokemon.SetBattleData(Battle, Index); - pokemon.SetOnBattlefield(true); - pokemon.SetBattleSidePosition(position); + if (pokemon.SideIndex != Index) + { + throw new InvalidOperationException( + "A battle Pokémon can only be sent out on the side its party is responsible for."); + } + pokemon.OnSwitchedIn(position); Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon)); pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position)); @@ -300,6 +310,14 @@ public class BattleSideImpl : ScriptSource, IBattleSide return oldPokemon; } + /// + public IBattlePokemon? SendOut(byte position, IPokemon pokemon) + { + var wrapper = Battle.Parties.Select(p => p.GetBattlePokemon(pokemon)).FirstOrDefault(x => x != null) ?? + throw new InvalidOperationException("The Pokémon does not belong to any party in this battle."); + return SwapPokemon(position, wrapper); + } + /// public void SwapPokemon(byte position1, byte position2) { @@ -307,7 +325,7 @@ public class BattleSideImpl : ScriptSource, IBattleSide } /// - public bool IsPokemonOnSide(IPokemon pokemon) => _pokemon.Contains(pokemon); + public bool IsPokemonOnSide(IBattlePokemon pokemon) => _pokemon.Contains(pokemon); /// public void MarkPositionAsUnfillable(byte position) => _fillablePositions[position] = false; diff --git a/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs b/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs index 4595e47..ef6caa5 100644 --- a/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs @@ -13,7 +13,7 @@ public interface IFleeChoice : ITurnChoice public class FleeTurnChoice : TurnChoice, IFleeChoice { /// - public FleeTurnChoice(IPokemon user) : base(user) + public FleeTurnChoice(IBattlePokemon user) : base(user) { } diff --git a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs index 4396cb7..ff7d4ad 100644 --- a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs @@ -16,15 +16,15 @@ public interface IItemChoice : ITurnChoice /// /// The target Pokémon of the item, if any. /// - IPokemon? GetTargetPokemon(IBattle battle); + IBattlePokemon? GetTargetPokemon(IBattle battle); } /// public class ItemChoice : TurnChoice, IItemChoice { /// - private ItemChoice(IPokemon user, IItem item, byte? targetSide, byte? targetPosition, IPokemon? targetPokemon) : - base(user) + private ItemChoice(IBattlePokemon user, IItem item, byte? targetSide, byte? targetPosition, + IBattlePokemon? targetPokemon) : base(user) { Item = item; TargetSide = targetSide; @@ -32,13 +32,13 @@ public class ItemChoice : TurnChoice, IItemChoice TargetPokemon = targetPokemon; } - public static ItemChoice CreateWithoutTarget(IPokemon user, IItem item) => + public static ItemChoice CreateWithoutTarget(IBattlePokemon user, IItem item) => new(user, item, null, null, null); - public static ItemChoice CreateForOpponent(IPokemon user, IItem item, byte targetSide, byte targetPosition) => + public static ItemChoice CreateForOpponent(IBattlePokemon user, IItem item, byte targetSide, byte targetPosition) => new(user, item, targetSide, targetPosition, null); - public static ItemChoice CreateForPartyMember(IPokemon user, IItem item, IPokemon targetPokemon) => + public static ItemChoice CreateForPartyMember(IBattlePokemon user, IItem item, IBattlePokemon targetPokemon) => new(user, item, null, null, targetPokemon); /// @@ -47,7 +47,7 @@ public class ItemChoice : TurnChoice, IItemChoice public IItem Item { get; } /// - public IPokemon? GetTargetPokemon(IBattle battle) + public IBattlePokemon? GetTargetPokemon(IBattle battle) { if (TargetPokemon != null) return TargetPokemon; @@ -71,7 +71,7 @@ public class ItemChoice : TurnChoice, IItemChoice /// /// The target Pokémon of the item, if any. This is used for party members. /// - private IPokemon? TargetPokemon { get; } + private IBattlePokemon? TargetPokemon { get; } /// public override int ScriptCount => User.ScriptCount; diff --git a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs index b756d3d..e07b64b 100644 --- a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs @@ -51,7 +51,7 @@ public interface IMoveChoice : ITurnChoice public class MoveChoice : TurnChoice, IMoveChoice { /// - public MoveChoice(IPokemon user, ILearnedMove usedMove, byte targetSide, byte targetPosition) : base(user) + public MoveChoice(IBattlePokemon user, ILearnedMove usedMove, byte targetSide, byte targetPosition) : base(user) { ChosenMove = usedMove; TargetSide = targetSide; diff --git a/PkmnLib.Dynamic/Models/Choices/PassChoice.cs b/PkmnLib.Dynamic/Models/Choices/PassChoice.cs index 797cdc6..81d6545 100644 --- a/PkmnLib.Dynamic/Models/Choices/PassChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/PassChoice.cs @@ -13,7 +13,7 @@ public interface IPassChoice : ITurnChoice public class PassChoice : TurnChoice, IPassChoice { /// - public PassChoice(IPokemon user) : base(user) + public PassChoice(IBattlePokemon user) : base(user) { } diff --git a/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs b/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs index b67e0f9..d65c6a6 100644 --- a/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs @@ -10,20 +10,20 @@ public interface ISwitchChoice : ITurnChoice /// /// The Pokémon to switch to. /// - IPokemon SwitchTo { get; } + IBattlePokemon SwitchTo { get; } } /// public class SwitchChoice : TurnChoice, ISwitchChoice { /// - public SwitchChoice(IPokemon user, IPokemon switchTo) : base(user) + public SwitchChoice(IBattlePokemon user, IBattlePokemon switchTo) : base(user) { SwitchTo = switchTo; } /// - public IPokemon SwitchTo { get; } + public IBattlePokemon SwitchTo { get; } /// public override int ScriptCount => User.ScriptCount; diff --git a/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs b/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs index 6be3be6..ad31eea 100644 --- a/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs +++ b/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs @@ -11,7 +11,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable /// /// The user of the turn choice /// - IPokemon User { get; } + IBattlePokemon User { get; } /// /// The speed of the user at the beginning of the turn. @@ -34,7 +34,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable /// Fails the choice. This will prevent it from executing and run a specific fail handling during /// execution. Note that this can not be undone. /// - public void Fail(); + void Fail(); } /// @@ -43,7 +43,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable public abstract class TurnChoice : ScriptSource, ITurnChoice { /// - protected TurnChoice(IPokemon user) + protected TurnChoice(IBattlePokemon user) { User = user; } @@ -51,7 +51,7 @@ public abstract class TurnChoice : ScriptSource, ITurnChoice /// /// The Pokemon for which the choice is made. /// - public IPokemon User { get; } + public IBattlePokemon User { get; } /// /// The speed of the user at the beginning of the turn. diff --git a/PkmnLib.Dynamic/Models/ExecutingMove.cs b/PkmnLib.Dynamic/Models/ExecutingMove.cs index 2a632e5..a1b1e2b 100644 --- a/PkmnLib.Dynamic/Models/ExecutingMove.cs +++ b/PkmnLib.Dynamic/Models/ExecutingMove.cs @@ -129,7 +129,7 @@ public interface IExecutingMove : IScriptSource /// /// The user of the move. /// - IPokemon User { get; } + IBattlePokemon User { get; } /// /// The move the user has actually chosen to do. @@ -151,17 +151,17 @@ public interface IExecutingMove : IScriptSource /// /// Gets a hit data for a target, with a specific index. /// - IHitData GetHitData(IPokemon target, byte hit); + IHitData GetHitData(IBattlePokemon target, byte hit); /// /// Checks whether a Pokémon is a target for this move. /// - bool IsPokemonTarget(IPokemon target); + bool IsPokemonTarget(IBattlePokemon target); /// /// Gets the index of the hits in this move where the hits for a specific target start. /// - int GetTargetIndex(IPokemon target); + int GetTargetIndex(IBattlePokemon target); /// /// Gets a hit based on its raw index. @@ -171,7 +171,7 @@ public interface IExecutingMove : IScriptSource /// /// Gets the targets of this move. /// - IReadOnlyList Targets { get; } + IReadOnlyList Targets { get; } /// /// The underlying move choice. @@ -192,12 +192,12 @@ public interface IExecutingMove : IScriptSource /// public class ExecutingMoveImpl : ScriptSource, IExecutingMove { - private readonly IReadOnlyList _targets; + private readonly IReadOnlyList _targets; private readonly IHitData[] _hits; private readonly IBattle _battle; /// - public ExecutingMoveImpl(IReadOnlyList targets, byte numberOfHits, ILearnedMove chosenMove, + public ExecutingMoveImpl(IReadOnlyList targets, byte numberOfHits, ILearnedMove chosenMove, IMoveData useMove, IMoveChoice moveChoice, IBattle battle) { _targets = targets; @@ -222,7 +222,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove public byte NumberOfHits { get; } /// - public IPokemon User => MoveChoice.User; + public IBattlePokemon User => MoveChoice.User; /// public ILearnedMove ChosenMove { get; } @@ -239,7 +239,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove public IScriptSet Volatile => MoveChoice.Volatile; /// - public IHitData GetHitData(IPokemon target, byte hit) + public IHitData GetHitData(IBattlePokemon target, byte hit) { var targetIndex = _targets.IndexOf(target); if (targetIndex == -1) @@ -252,10 +252,10 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove } /// - public bool IsPokemonTarget(IPokemon target) => _targets.Contains(target); + public bool IsPokemonTarget(IBattlePokemon target) => _targets.Contains(target); /// - public int GetTargetIndex(IPokemon target) + public int GetTargetIndex(IBattlePokemon target) { var targetIndex = _targets.IndexOf(target); if (targetIndex == -1) @@ -273,7 +273,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove } /// - public IReadOnlyList Targets => _targets.ToList(); + public IReadOnlyList Targets => _targets.ToList(); /// public IMoveChoice MoveChoice { get; } diff --git a/PkmnLib.Dynamic/Models/ItemTargetType.cs b/PkmnLib.Dynamic/Models/ItemTargetType.cs index ac21259..424dacc 100644 --- a/PkmnLib.Dynamic/Models/ItemTargetType.cs +++ b/PkmnLib.Dynamic/Models/ItemTargetType.cs @@ -35,29 +35,30 @@ public static class ItemTargetTypeHelpers /// /// Determines if the given target is valid based on the ItemTargetType. /// - public static bool IsValidTarget(this ItemTargetType targetType, IBattle battle, IPokemon user, IPokemon target) + public static bool IsValidTarget(this ItemTargetType targetType, IBattle battle, IBattlePokemon user, + IBattlePokemon target) { if (targetType == ItemTargetType.None) return true; if (targetType.HasFlag(ItemTargetType.OwnPokemon)) { - var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); - var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target)); + var userParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(user)); + var targetParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(target)); if (userParty is not null && targetParty is not null && userParty == targetParty) return true; } if (targetType.HasFlag(ItemTargetType.AllyPokemon)) { - if (user.BattleData?.BattleSide == target.BattleData?.BattleSide) + if (user.BattleSide == target.BattleSide) return true; } if (targetType.HasFlag(ItemTargetType.FoePokemon)) { - var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); - var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target)); + var userParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(user)); + var targetParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(target)); if (userParty is not null && targetParty is not null && userParty != targetParty) return true; } diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs index 4a2749e..8d87462 100644 --- a/PkmnLib.Dynamic/Models/Pokemon.cs +++ b/PkmnLib.Dynamic/Models/Pokemon.cs @@ -1,7 +1,5 @@ -using System.Diagnostics.CodeAnalysis; using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Libraries; -using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.Models.Serialized; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static; @@ -11,7 +9,8 @@ using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.Models; /// -/// The data of a Pokemon. +/// The persistent data of a Pokemon. This holds everything that outlives a battle. All state that is only +/// relevant for the duration of a battle lives on instead. /// public interface IPokemon : IScriptSource, IDeepCloneable { @@ -30,23 +29,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// IForm Form { get; } - /// - /// An optional display species of the Pokemon. If this is set, the client should display this - /// species. An example of usage for this is the Illusion ability. - /// - ISpecies? DisplaySpecies { get; } - - /// - /// An optional display form of the Pokemon. If this is set, the client should display this - /// form. An example of usage for this is the Illusion ability. - /// - IForm? DisplayForm { get; } - - /// - /// Sets the display species and form of the Pokemon. This is used for abilities like Illusion. - /// - void SetDisplaySpecies(ISpecies? species, IForm? form); - /// /// The current level of the Pokemon. /// @@ -58,9 +40,10 @@ public interface IPokemon : IScriptSource, IDeepCloneable uint Experience { get; } /// - /// Increases the experience of the Pokemon. Returns whether any experience was gained. + /// Increases the experience of the Pokemon. Returns whether any experience was gained. If an event hook is + /// passed, experience and level up events are sent to it. /// - bool AddExperience(uint experience); + bool AddExperience(uint experience, EventHook? eventHook = null); /// /// The personality value of the Pokemon. @@ -101,7 +84,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// /// The height of the Pokémon in meters. /// - float HeightInMeters { get; set; } + float HeightInMeters { get; } /// /// The happiness of the Pokemon. Also known as friendship. @@ -113,17 +96,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// StatisticSet FlatStats { get; } - /// - /// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below - /// -6. - /// - StatBoostStatisticSet StatBoost { get; } - - /// - /// The stats of the Pokemon including the stat boosts - /// - StatisticSet BoostedStats { get; } - /// /// The maximum health of the Pokemon. /// @@ -155,32 +127,18 @@ public interface IPokemon : IScriptSource, IDeepCloneable AbilityIndex AbilityIndex { get; } /// - /// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy - /// ability. + /// The ability of the Pokemon, as determined by its form and ability index. /// - IAbility? OverrideAbility { get; } + IAbility Ability { get; } /// - /// If in battle, we have additional data. - /// - IPokemonBattleData? BattleData { get; } - - /// - /// The moves the Pokemon has learned. This is of a set length of . Empty move slots - /// are null. If a move slot is temporarily replaced (see ), this shows the - /// replacement move. + /// The moves the Pokemon has learned. This is of a set length of . Empty move + /// slots are null. /// IReadOnlyList Moves { get; } /// - /// The permanently learned moves of the Pokemon, ignoring any temporary replacements made through - /// . This is of a set length of . Empty move - /// slots are null. - /// - IReadOnlyList BaseMoves { get; } - - /// - /// Checks whether the Pokemon has a specific move in its current moveset. + /// Checks whether the Pokemon has a specific move in its moveset. /// bool HasMove(StringKey moveName); @@ -189,21 +147,13 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// void SwapMoves(byte index1, byte index2); - /// - /// Temporarily replaces the move in the given slot until the Pokemon leaves the battlefield. The permanently - /// learned move in the slot is left untouched and becomes visible again automatically. Used by effects such as - /// Mimic. - /// - /// Thrown when the move is not found in the move library. - void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index); - /// /// Whether or not the Pokemon is allowed to gain experience. /// bool AllowedExperience { get; } /// - /// The current types of the Pokemon. + /// The types of the Pokemon, as determined by its form. /// IReadOnlyList Types { get; } @@ -212,26 +162,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// bool IsEgg { get; } - /// - /// Whether or not this Pokemon was caught this battle. - /// - bool IsCaught { get; } - - /// - /// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore. - /// - void MarkAsCaught(); - - /// - /// The script for the held item. - /// - ScriptContainer HeldItemTriggerScript { get; } - - /// - /// The script for the ability. - /// - ScriptContainer AbilityScript { get; } - /// /// The script for the status. /// @@ -242,11 +172,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// int? GetStatusTurnsLeft { get; } - /// - /// The volatile status scripts of the Pokemon. - /// - IScriptSet Volatile { get; } - /// /// Checks whether the Pokemon is holding an item with a specific name. /// @@ -264,74 +189,17 @@ public interface IPokemon : IScriptSource, IDeepCloneable [MustUseReturnValue] IItem? RemoveHeldItem(); - /// - /// Whether the held item has been removed for the duration of the battle. - /// - bool HasItemBeenRemovedForBattle { get; } - - /// - /// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item. - /// - /// - /// This is used for moves that remove a held item, but do not consume it. In this case, the item needs to be - /// restored after the battle. - /// - IItem? RemoveHeldItemForBattle(); - - /// - /// Tries to steal the held item of the Pokémon. If successful, the item is removed from the Pokémon and returned. - /// If the Pokémon does not have a held item, or the item is a form changer, this will return false. - /// - bool TryStealHeldItem([NotNullWhen(true)] out IItem? item); - - /// - /// Restores the held item of a Pokémon if it was temporarily removed. - /// - void RestoreRemovedHeldItem(); - - /// - /// Makes the Pokemon uses its held item. Returns whether the item was consumed. - /// - bool ConsumeHeldItem(); - /// /// Uses an item on the Pokemon. /// void UseItem(IItem item); - /// - /// Change a boosted stat by a certain amount. - /// - /// The stat to be changed - /// The amount to change the stat by - /// Whether the change was self-inflicted. This can be relevant in scripts. - /// - /// The event batch ID this change is a part of. This is relevant for visual handling - bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default); - - /// - /// Suppresses the ability of the Pokémon. - /// - bool SuppressAbility(); - - /// - /// Returns the currently active ability. - /// - IAbility? ActiveAbility { get; } - /// /// Calculates the flat stats on the Pokemon. This should be called when for example the base - /// stats, level, nature, IV, or EV changes. This has a side effect of recalculating the boosted - /// stats, as those depend on the flat stats. + /// stats, level, nature, IV, or EV changes. /// void RecalculateFlatStats(); - /// - /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats. - /// This should be called when a stat boost changes. - /// - void RecalculateBoostedStats(); - /// /// Evolves the Pokemon to a specific evolution. This will not check whether the evolution is valid, so /// you should check that before calling this method. @@ -339,9 +207,10 @@ public interface IPokemon : IScriptSource, IDeepCloneable bool EvolveTo(IEvolution evolution, EventHook? eventHook = null); /// - /// Change the species of the Pokemon. + /// Change the species of the Pokemon. An optional random can be passed, which is used when the gender of + /// the Pokemon needs to be re-rolled. /// - void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null); + void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null); /// /// Change the form of the Pokemon. @@ -359,12 +228,14 @@ public interface IPokemon : IScriptSource, IDeepCloneable bool IsFainted { get; } /// - /// Damages the Pokemon by a certain amount of damage, from a damage source. + /// Damages the Pokemon by a certain amount of damage. This only changes the health of the Pokemon; + /// battle handling, such as script hooks, events and faint handling, is done by + /// . /// void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false); /// - /// Forces the Pokémon to faint. + /// Sets the health of the Pokémon to 0. /// void Faint(DamageSource source, EventBatchId batchId = default); @@ -391,7 +262,8 @@ public interface IPokemon : IScriptSource, IDeepCloneable bool HasStatus(StringKey status); /// - /// Adds a non-volatile status to the Pokemon. + /// Adds a non-volatile status to the Pokemon. This only sets the status script; battle handling, such as + /// prevention script hooks and events, is done by . /// bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default); @@ -405,157 +277,12 @@ public interface IPokemon : IScriptSource, IDeepCloneable /// void ChangeLevelBy(int change); - /// - /// Sets the current battle the Pokémon is in. - /// - void SetBattleData(IBattle battle, byte sideIndex); - - /// - /// Sets whether the Pokémon is on the battlefield. - /// - void SetOnBattlefield(bool onBattleField); - - /// - /// Sets the position the Pokémon has within its side. - /// - /// - void SetBattleSidePosition(byte position); - - /// - /// Resets the battle data of the Pokémon. This is called when the battle ends. - /// - void ClearBattleData(); - - /// - /// Marks a Pokemon as seen in the battle. - /// - void MarkOpponentAsSeen(IPokemon pokemon); - - /// - /// Removes a type from the Pokémon. Returns whether the type was removed. - /// - bool RemoveType(TypeIdentifier type); - - /// - /// Adds a type to the Pokémon. Returns whether the type was added. It will not add the type if - /// 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); - - /// - /// Changes the ability of the Pokémon. - /// - bool ChangeAbility(IAbility ability); - - /// - /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as - /// Levitate. - /// - bool IsFloating { get; } - /// /// Converts the data structure to a serializable format. /// SerializedPokemon Serialize(); } -/// -/// The data of the Pokémon related to being in a battle. -/// This is only set when the Pokémon is on the field in a battle. -/// -public interface IPokemonBattleData : IDeepCloneable -{ - /// - /// Sets the battle data of the Pokémon. - /// - void SetBattle(IBattle battle, byte sideIndex, uint switchInTurn); - - /// - /// Sets the position of the Pokémon on the field. - /// - void SetPosition(byte position); - - /// - /// The battle the Pokémon is in. - /// - IBattle Battle { get; } - - /// - /// The index of the side of the Pokémon - /// - byte SideIndex { get; } - - /// - /// The index of the position of the Pokémon on the field - /// - byte Position { get; } - - /// - /// A list of opponents the Pokémon has seen this battle. - /// - IReadOnlyList SeenOpponents { get; } - - /// - /// Sets whether the Pokémon is on the battlefield. - /// - void SetOnBattlefield(bool onBattleField); - - /// - /// Whether the Pokémon is on the battlefield. - /// - bool IsOnBattlefield { get; } - - /// - /// 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 item); - - /// - /// The turn the Pokémon switched in. - /// - uint SwitchInTurn { get; } - - /// - /// The number of turns the Pokémon has been on the field. - /// - uint TurnsOnField { get; } - - /// - /// The side the Pokémon is on. - /// - IBattleSide BattleSide { get; } - - /// - /// The species of the Pokémon at the time it was sent out. - /// - ISpecies OriginalSpecies { get; } - - /// - /// The form of the Pokémon at the time it was sent out. - /// - IForm OriginalForm { get; } - - /// - /// The last move choice executed by the Pokémon. - /// - IMoveChoice? LastMoveChoice { get; set; } -} - /// public class PokemonImpl : ScriptSource, IPokemon { @@ -571,18 +298,15 @@ public class PokemonImpl : ScriptSource, IPokemon PersonalityValue = personalityValue; Gender = gender; Coloring = coloring; - Types = form.Types.ToList(); Experience = library.StaticLibrary.GrowthRates.CalculateExperience(species.GrowthRate, level); - HeightInMeters = form.Height; Happiness = species.BaseHappiness; - Volatile = new ScriptSet(this); if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature)) throw new KeyNotFoundException($"Nature {natureName} not found."); Nature = nature; RecalculateFlatStats(); - CurrentHealth = BoostedStats.Hp; + CurrentHealth = FlatStats.Hp; } /// @@ -608,7 +332,6 @@ public class PokemonImpl : ScriptSource, IPokemon } CurrentHealth = serializedPokemon.CurrentHealth; - HeightInMeters = form.Height; Happiness = serializedPokemon.Happiness; IndividualValues = serializedPokemon.IndividualValues.ToIndividualValueStatisticSet(); EffortValues = serializedPokemon.EffortValues.ToEffortValueStatisticSet(); @@ -621,7 +344,6 @@ public class PokemonImpl : ScriptSource, IPokemon AbilityIndex = form.FindAbilityIndex(ability) ?? throw new KeyNotFoundException( $"Ability {ability.Name} not found on species {species.Name} form {form.Name}."); - Volatile = new ScriptSet(this); _learnedMoves = serializedPokemon.Moves.Select(move => { if (move == null) @@ -632,7 +354,6 @@ public class PokemonImpl : ScriptSource, IPokemon }).ToArray(); AllowedExperience = serializedPokemon.AllowedExperience; IsEgg = serializedPokemon.IsEgg; - Types = form.Types; RecalculateFlatStats(); if (serializedPokemon.Status != null) @@ -654,24 +375,6 @@ public class PokemonImpl : ScriptSource, IPokemon /// public IForm Form { get; private set; } - /// - public ISpecies? DisplaySpecies { get; set; } - - /// - public IForm? DisplayForm { get; set; } - - /// - public void SetDisplaySpecies(ISpecies? species, IForm? form) - { - DisplaySpecies = species; - DisplayForm = form; - - BattleData?.Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form) - { - BatchId = new EventBatchId(), - }); - } - /// public LevelInt Level { get; private set; } @@ -679,7 +382,7 @@ public class PokemonImpl : ScriptSource, IPokemon public uint Experience { get; private set; } /// - public bool AddExperience(uint experience) + public bool AddExperience(uint experience, EventHook? eventHook = null) { if (!AllowedExperience) return false; @@ -690,7 +393,7 @@ public class PokemonImpl : ScriptSource, IPokemon var oldExperience = Experience; Experience += experience; var batchId = new EventBatchId(); - BattleData?.Battle.EventHook.Invoke(new ExperienceGainEvent(this, oldExperience, Experience) + eventHook?.Invoke(new ExperienceGainEvent(this, oldExperience, Experience) { BatchId = batchId, }); @@ -700,7 +403,7 @@ public class PokemonImpl : ScriptSource, IPokemon { Level = newLevel; RecalculateFlatStats(); - BattleData?.Battle.EventHook.Invoke(new LevelUpEvent(this, oldLevel, Level) + eventHook?.Invoke(new LevelUpEvent(this, oldLevel, Level) { BatchId = batchId, }); @@ -738,9 +441,6 @@ public class PokemonImpl : ScriptSource, IPokemon get { var weight = Form.Weight; - if (BattleData is not null) - // ReSharper disable once AccessToModifiedClosure - this.RunScriptHook(script => script.ModifyWeight(ref weight)); if (weight < 0.1f) weight = 0.1f; return weight; @@ -748,7 +448,7 @@ public class PokemonImpl : ScriptSource, IPokemon } /// - public float HeightInMeters { get; set; } + public float HeightInMeters => Form.Height; /// public byte Happiness { get; set; } @@ -757,13 +457,7 @@ public class PokemonImpl : ScriptSource, IPokemon public StatisticSet FlatStats { get; } = new(); /// - public StatBoostStatisticSet StatBoost { get; } = new(); - - /// - public StatisticSet BoostedStats { get; } = new(); - - /// - public uint MaxHealth => BoostedStats.Hp; + public uint MaxHealth => FlatStats.Hp; /// public IndividualValueStatisticSet IndividualValues { get; } = new(); @@ -780,30 +474,31 @@ public class PokemonImpl : ScriptSource, IPokemon /// public AbilityIndex AbilityIndex { get; } - /// - public IAbility? OverrideAbility { get; private set; } + private (IAbility Ability, IForm Form, AbilityIndex Index)? _abilityCache; /// - public IPokemonBattleData? BattleData { get; private set; } + public IAbility Ability + { + get + { + if (_abilityCache is not null && _abilityCache.Value.Form == Form && + _abilityCache.Value.Index == AbilityIndex) + return _abilityCache.Value.Ability; + var ability = Form.GetAbility(AbilityIndex); + if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj)) + throw new KeyNotFoundException($"Ability {ability} not found."); + _abilityCache = (abilityObj, Form, AbilityIndex); + return abilityObj; + } + } private readonly ILearnedMove?[] _learnedMoves = new ILearnedMove[Const.MovesCount]; - /// - /// Battle-only per-slot overrides of . The permanent moveset is never mutated by - /// temporary moves; discarding this array is all that is needed to restore the original moves. - /// - private ILearnedMove?[]? _temporaryMoves; + /// + public IReadOnlyList Moves => _learnedMoves; /// - public IReadOnlyList Moves => _temporaryMoves == null - ? _learnedMoves - : _temporaryMoves.Select((move, index) => move ?? _learnedMoves[index]).ToArray(); - - /// - public IReadOnlyList BaseMoves => _learnedMoves; - - /// - public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName); + public bool HasMove(StringKey moveName) => _learnedMoves.Any(move => move?.MoveData.Name == moveName); /// public void SwapMoves(byte index1, byte index2) @@ -811,49 +506,23 @@ public class PokemonImpl : ScriptSource, IPokemon if (index1 >= Const.MovesCount || index2 >= Const.MovesCount) return; (_learnedMoves[index1], _learnedMoves[index2]) = (_learnedMoves[index2], _learnedMoves[index1]); - if (_temporaryMoves != null) - (_temporaryMoves[index1], _temporaryMoves[index2]) = (_temporaryMoves[index2], _temporaryMoves[index1]); } /// public bool AllowedExperience { get; set; } - private List _types = new(); - /// - public IReadOnlyList Types - { - get => _types; - private set => _types = value.ToList(); - } + public IReadOnlyList Types => Form.Types; /// public bool IsEgg { get; private set; } - /// - public bool IsCaught { get; private set; } - - /// - public void MarkAsCaught() - { - IsCaught = true; - } - - /// - public ScriptContainer HeldItemTriggerScript { get; } = new(); - - /// - public ScriptContainer AbilityScript { get; } = new(); - /// public ScriptContainer StatusScript { get; } = new(); /// public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft(); - /// - public IScriptSet Volatile { get; } - /// public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName; @@ -862,7 +531,6 @@ public class PokemonImpl : ScriptSource, IPokemon { var previous = HeldItem; HeldItem = item; - this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item)); return previous; } @@ -878,168 +546,17 @@ public class PokemonImpl : ScriptSource, IPokemon } var previous = HeldItem; HeldItem = null; - this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null)); return previous; } /// - public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null; - - private IItem? _removedHeldItem; - - /// - public IItem? RemoveHeldItemForBattle() - { - return _removedHeldItem = RemoveHeldItem(); - } - - /// - public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item) - { - if (HeldItem is null || HeldItem.Category == ItemCategory.FormChanger) - { - item = null; - return false; - } - var prevent = false; - this.RunScriptHook(script => - script.PreventHeldItemSteal(this, HeldItem, ref prevent)); - if (prevent) - { - item = null; - return false; - } - item = RemoveHeldItemForBattle(); - return item is not null; - } - - /// - public void RestoreRemovedHeldItem() - { - _ = ForceSetHeldItem(_removedHeldItem); - _removedHeldItem = null; - } - - /// - public bool ConsumeHeldItem() - { - if (HeldItem is null) - return false; - if (!Library.ScriptResolver.TryResolveBattleItemScript(HeldItem, out _)) - return false; - - if (BattleData != null) - { - var prevented = false; - this.RunScriptHook(script => - script.PreventHeldItemConsume(this, HeldItem, ref prevented)); - if (prevented) - return false; - BattleData.MarkItemAsConsumed(HeldItem); - } - - UseItem(ForceSetHeldItem(null)!); - return true; - } - - /// - /// Uses an item on this Pokémon. - /// - /// public void UseItem(IItem item) { // TODO: actually consume the item - - this.RunScriptHook(x => x.OnAfterItemConsume(this, item)); } /// - public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, - EventBatchId batchId = default) - { - if (!force) - { - var prevented = false; - this.RunScriptHook(script => - script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented)); - if (prevented) - return false; - this.RunScriptHook(script => - script.ChangeStatBoostChange(this, stat, selfInflicted, ref change)); - if (change == 0) - return false; - } - var changed = false; - var oldBoost = StatBoost.GetStatistic(stat); - changed = change switch - { - > 0 => StatBoost.IncreaseStatistic(stat, change), - < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change), - _ => changed, - }; - if (!changed) - return false; - if (BattleData != null) - { - var newBoost = StatBoost.GetStatistic(stat); - BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost) - { - BatchId = batchId, - }); - } - - RecalculateBoostedStats(); - this.RunScriptHook(script => - script.OnAfterStatBoostChange(this, stat, selfInflicted, change)); - return true; - } - - /// - /// Whether the ability of the Pokémon is suppressed. - /// - public bool AbilitySuppressed { get; private set; } - - /// - public bool SuppressAbility() - { - if (ActiveAbility?.CanBeChanged == false) - return false; - - AbilitySuppressed = true; - AbilityScript.Clear(); - return true; - } - - private (IAbility, AbilityIndex)? _abilityCache; - - /// - public IAbility? ActiveAbility - { - get - { - if (AbilitySuppressed) - return null; - if (OverrideAbility != null) - return OverrideAbility; - if (_abilityCache is not null && _abilityCache.Value.Item2 == AbilityIndex) - return _abilityCache.Value.Item1; - var ability = Form.GetAbility(AbilityIndex); - if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj)) - throw new KeyNotFoundException($"Ability {ability} not found."); - _abilityCache = (abilityObj, AbilityIndex); - return abilityObj; - } - } - - /// - public void RecalculateFlatStats() - { - Library.StatCalculator.CalculateFlatStats(this, FlatStats); - RecalculateBoostedStats(); - } - - /// - public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats); + public void RecalculateFlatStats() => Library.StatCalculator.CalculateFlatStats(this, FlatStats); /// public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null) @@ -1058,21 +575,19 @@ public class PokemonImpl : ScriptSource, IPokemon } /// - public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null) + public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null) { - eventHook ??= BattleData?.Battle.EventHook; if (Species == species) { if (form != Form) - ChangeForm(form, new EventBatchId()); + ChangeForm(form, new EventBatchId(), eventHook); return; } // If the Pokémon is genderless, but its new species is not, we want to set its gender if (Gender != Gender.Genderless && species.GenderRate < 0.0) { - var random = (IRandom?)BattleData?.Battle.Random ?? new RandomImpl(); - Gender = species.GetRandomGender(random); + Gender = species.GetRandomGender(random ?? new RandomImpl()); } // Else if the new species is genderless, but the Pokémon has a gender, make the creature genderless. else if (species.GenderRate < 0.0 && Gender != Gender.Genderless) @@ -1086,7 +601,7 @@ public class PokemonImpl : ScriptSource, IPokemon BatchId = batchId, }); Species = species; - ChangeForm(form, batchId); + ChangeForm(form, batchId, eventHook); } /// @@ -1094,13 +609,8 @@ public class PokemonImpl : ScriptSource, IPokemon { if (form == Form) return; - eventHook ??= BattleData?.Battle.EventHook; - - var oldAbility = Form.GetAbility(AbilityIndex); Form = form; - Types = form.Types.ToList(); - HeightInMeters = form.Height; var abilityIndex = AbilityIndex; abilityIndex = AbilityIndex.IsHidden switch { @@ -1115,28 +625,9 @@ public class PokemonImpl : ScriptSource, IPokemon _ => abilityIndex, }; - var newAbility = Form.GetAbility(abilityIndex); - - if (OverrideAbility == null && oldAbility != newAbility) - { - AbilityScript.Clear(); - if (!Library.StaticLibrary.Abilities.TryGet(newAbility, out var ability)) - throw new KeyNotFoundException($"Ability {newAbility} not found."); - if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, newAbility, ability.Parameters, - out var abilityScript)) - { - AbilityScript.Set(abilityScript); - abilityScript.OnAddedToParent(this); - } - else - { - AbilityScript.Clear(); - } - } - - var oldHealth = BoostedStats.Hp; + var oldHealth = FlatStats.Hp; RecalculateFlatStats(); - var diffHealth = (long)BoostedStats.Hp - oldHealth; + var diffHealth = (long)FlatStats.Hp - oldHealth; if (diffHealth > 0) { Heal((uint)diffHealth, true); @@ -1152,26 +643,18 @@ public class PokemonImpl : ScriptSource, IPokemon /// /// - /// Currently this checks the Pokémon is not an egg, not caught, and not fainted. + /// Currently this checks the Pokémon is not an egg and not fainted. /// - public bool IsUsable => !IsCaught && !IsEgg && !IsFainted; + public bool IsUsable => !IsEgg && !IsFainted; /// public bool IsFainted => CurrentHealth == 0; /// - public void Damage(uint damage, DamageSource source, EventBatchId batchId, bool forceDamage = false) + public void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false) { - // If the Pokémon is already fainted, we don't need to do anything. if (IsFainted) return; - if (BattleData is not null && !forceDamage) - { - var dmg = damage; - this.RunScriptHook(script => - script.ChangeIncomingDamage(this, source, ref dmg)); - damage = dmg; - } if (damage == 0) return; @@ -1179,97 +662,29 @@ public class PokemonImpl : ScriptSource, IPokemon // underflow. if (damage >= CurrentHealth) damage = CurrentHealth; - // Calculate the new health. - var newHealth = CurrentHealth - damage; - if (BattleData is not null) - { - // If the Pokémon is in a battle, we trigger an event to the front-end. - BattleData.Battle.EventHook.Invoke(new DamageEvent(this, CurrentHealth, newHealth, source) - { - BatchId = batchId, - }); - // And allow scripts to execute. - this.RunScriptHook(script => script.OnDamage(this, source, CurrentHealth, newHealth)); - } - - CurrentHealth = newHealth; - - // If the Pokémon is now fainted, we also run faint handling. - if (IsFainted) - { - OnFaint(source); - } + CurrentHealth -= damage; } /// public void Faint(DamageSource source, EventBatchId batchId = default) { CurrentHealth = 0; - OnFaint(source); - } - - private void OnFaint(DamageSource source) - { - // If the Pokémon is not in a battle, we don't need to do anything. - if (BattleData is null) - return; - - // Trigger the faint event to the front-end. - BattleData.Battle.EventHook.Invoke(new FaintEvent(this)); - - // Allow scripts to trigger based on the faint. - this.RunScriptHook(script => script.OnFaint(this, source)); - foreach (var ally in BattleData.BattleSide.Pokemon.WhereNotNull().Where(x => x != this)) - { - ally.RunScriptHook(script => script.OnAllyFaint(ally, this)); - } - - // Make sure the OnRemove script is run. - this.RunScriptHook(script => script.OnRemove()); - - // Mark the position as unfillable if it can't be filled by any party. - if (!BattleData.Battle.CanSlotBeFilled(BattleData.SideIndex, BattleData.Position)) - { - BattleData.Battle.Sides[BattleData.SideIndex].MarkPositionAsUnfillable(BattleData.Position); - } - BattleData.BattleSide.MarkFaint(BattleData.Position); - BattleData.BattleSide.ForceClearPokemonFromField(BattleData.Position); - - foreach (var opponent in BattleData.SeenOpponents.WhereNotNull()) - { - if (!opponent.IsUsable) - continue; - if (!opponent.AllowedExperience) - continue; - opponent.AddExperience(Library.ExperienceGainCalculator.CalculateExperienceGain(this, opponent)); - } - - // Validate the battle state to see if the battle is over. - BattleData.Battle.ValidateBattleState(); } /// - public bool Heal(uint heal, bool allowRevive, EventBatchId batchId = default, bool forceHeal = false, + public bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false, EventHook? customEventHook = null) { if (IsFainted && !allowRevive) return false; - var maxAmount = BoostedStats.Hp - CurrentHealth; + var maxAmount = MaxHealth - CurrentHealth; if (heal > maxAmount) heal = maxAmount; if (heal == 0) return false; - if (!forceHeal) - { - var prevented = false; - this.RunScriptHook(x => x.PreventHeal(this, heal, allowRevive, ref prevented)); - if (prevented) - return false; - } var newHealth = CurrentHealth + heal; - customEventHook ??= BattleData?.Battle.EventHook; customEventHook?.Invoke(new HealEvent(this, CurrentHealth, newHealth) { BatchId = batchId, @@ -1281,7 +696,7 @@ public class PokemonImpl : ScriptSource, IPokemon /// public void RestoreAllPP() { - foreach (var move in Moves) + foreach (var move in _learnedMoves) { move?.RestoreAllUses(); } @@ -1312,18 +727,6 @@ public class PokemonImpl : ScriptSource, IPokemon _learnedMoves[index] = new LearnedMoveImpl(move, method); } - /// - public void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index) - { - if (index >= Const.MovesCount) - throw new ArgumentOutOfRangeException(nameof(index), $"Move slot {index} is out of range."); - if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move)) - throw new KeyNotFoundException($"Move {moveName} not found."); - - _temporaryMoves ??= new ILearnedMove?[Const.MovesCount]; - _temporaryMoves[index] = new LearnedMoveImpl(move, method); - } - /// public bool HasStatus(StringKey status) => StatusScript.Script?.Name == status; @@ -1335,23 +738,9 @@ public class PokemonImpl : ScriptSource, IPokemon if (!StatusScript.IsEmpty) return false; - var oldStatus = StatusScript.Script?.Name; - var selfInflicted = originPokemon == this; - - var preventStatus = false; - this.RunScriptHook(script => - script.PreventStatusChange(this, status, selfInflicted, ref preventStatus)); - if (preventStatus) - return false; StatusScript.Set(statusScript); statusScript.OnAddedToParent(this); - BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status) - { - BatchId = batchId, - }); - this.RunScriptHook(script => - script.OnAfterStatusChange(this, status, originPokemon)); return true; } @@ -1359,10 +748,6 @@ public class PokemonImpl : ScriptSource, IPokemon public void ClearStatus(EventBatchId batchId = default) { StatusScript.Clear(); - BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, StatusScript.Script?.Name, null) - { - BatchId = batchId, - }); } /// @@ -1373,166 +758,20 @@ public class PokemonImpl : ScriptSource, IPokemon RecalculateFlatStats(); } - /// - public void SetBattleData(IBattle battle, byte sideIndex) - { - if (BattleData is not null) - { - BattleData.SetBattle(battle, sideIndex, battle.CurrentTurnNumber); - } - else - { - BattleData = new PokemonBattleDataImpl(battle, sideIndex, battle.CurrentTurnNumber, Species, Form); - } - if (ActiveAbility != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ActiveAbility.Name, - ActiveAbility.Parameters, out var abilityScript)) - { - AbilityScript.Set(abilityScript); - abilityScript.OnAddedToParent(this); - } - else - { - AbilityScript.Clear(); - } - } - - /// - public void SetOnBattlefield(bool onBattleField) - { - if (BattleData is not null) - { - BattleData.SetOnBattlefield(onBattleField); - if (!onBattleField) - { - Volatile.Clear(); - _temporaryMoves = null; - HeightInMeters = Form.Height; - Types = Form.Types; - OverrideAbility = null; - AbilitySuppressed = false; - RecalculateFlatStats(); - } - } - } - - /// - public void SetBattleSidePosition(byte position) - { - BattleData?.SetPosition(position); - } - - /// - public void ClearBattleData() - { - var battleData = BattleData; - BattleData = null; - Volatile.Clear(); - _temporaryMoves = null; - HeightInMeters = Form.Height; - Types = Form.Types; - OverrideAbility = null; - AbilitySuppressed = false; - StatBoost.Reset(); - if (battleData != null && Form.IsBattleOnlyForm) - { - ChangeForm(battleData.OriginalSpecies == Species ? battleData.OriginalForm : Species.GetDefaultForm()); - } - DisplaySpecies = null; - DisplayForm = null; - IsCaught = false; - } - - /// - public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon); - - /// - public bool RemoveType(TypeIdentifier type) => _types.Remove(type); - - /// - public bool AddType(TypeIdentifier type) - { - if (_types.Contains(type)) - return false; - _types.Add(type); - return true; - } - - /// - public void SetTypes(IReadOnlyList types) - { - _types = types.ToList(); - } - - /// - 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)) - { - AbilityScript.Set(abilityScript); - abilityScript.OnAddedToParent(this); - } - else - { - AbilityScript.Clear(); - } - return true; - } - - private static readonly StringKey FlyingTypeName = "flying"; - - /// - public bool IsFloating - { - get - { - var isFloating = Types.Any(x => x.Name == FlyingTypeName); - this.RunScriptHook(x => x.IsFloating(this, ref isFloating)); - return isFloating; - } - } - /// public SerializedPokemon Serialize() => new(this); /// - public override int ScriptCount - { - get - { - var c = 4; - if (BattleData != null) - { - var side = BattleData.Battle.Sides[BattleData.SideIndex]; - c += side.ScriptCount; - } - - return c; - } - } + public override int ScriptCount => 1; /// public override void GetOwnScripts(List> scripts) { - scripts.Add(HeldItemTriggerScript); - scripts.Add(AbilityScript); scripts.Add(StatusScript); - scripts.Add(Volatile); } /// - public override void CollectScripts(List> scripts) - { - GetOwnScripts(scripts); - if (BattleData != null) - { - var side = BattleData.Battle.Sides[BattleData.SideIndex]; - side.CollectScripts(scripts); - } - } + public override void CollectScripts(List> scripts) => GetOwnScripts(scripts); /// public override string ToString() @@ -1541,92 +780,4 @@ public class PokemonImpl : ScriptSource, IPokemon return $"{Nickname} ({Species.Name})"; return Species.Name; } -} - -/// -public class PokemonBattleDataImpl : IPokemonBattleData -{ - /// - public PokemonBattleDataImpl(IBattle battle, byte sideIndex, uint switchInTurn, ISpecies originalSpecies, - IForm originalForm) - { - Battle = battle; - SideIndex = sideIndex; - SwitchInTurn = switchInTurn; - OriginalSpecies = originalSpecies; - OriginalForm = originalForm; - } - - /// - public void SetBattle(IBattle battle, byte sideIndex, uint switchInTurn) - { - Battle = battle; - SideIndex = sideIndex; - SwitchInTurn = switchInTurn; - } - - /// - public void SetPosition(byte position) - { - Position = position; - } - - /// - public IBattle Battle { get; set; } - - /// - public byte SideIndex { get; set; } - - /// - public byte Position { get; set; } - - private readonly List _seenOpponents = []; - - /// - public IReadOnlyList SeenOpponents => _seenOpponents; - - /// - public void SetOnBattlefield(bool onBattleField) - { - IsOnBattlefield = onBattleField; - } - - /// - public bool IsOnBattlefield { get; set; } - - /// - public void MarkOpponentAsSeen(IPokemon opponent) - { - _seenOpponents.Add(opponent); - } - - private readonly List _consumedItems = []; - - /// - public IReadOnlyList ConsumedItems => _consumedItems; - - /// - public void MarkItemAsConsumed(IItem item) - { - _consumedItems.Add(item); - BattleSide.SetConsumedItem(Position, item); - } - - /// - public uint SwitchInTurn { get; set; } - - /// - public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn; - - /// - public IBattleSide BattleSide => Battle.Sides[SideIndex]; - - /// - public ISpecies OriginalSpecies { get; } - - /// - public IForm OriginalForm { get; } - - /// - public IMoveChoice? LastMoveChoice { get; set; } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs b/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs index 8bcaef7..5743e6d 100644 --- a/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs +++ b/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs @@ -33,7 +33,7 @@ public record SerializedPokemon Nature = pokemon.Nature.Name; Nickname = pokemon.Nickname; Ability = pokemon.Form.GetAbility(pokemon.AbilityIndex); - Moves = pokemon.BaseMoves.Select(move => + Moves = pokemon.Moves.Select(move => { if (move == null) return null; diff --git a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs index 5b2786f..7cde65f 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs @@ -41,7 +41,7 @@ public abstract class ItemScript : IDeepCloneable /// /// Returns whether the item is usable on the given target. /// - public virtual bool IsTargetValid(IPokemon target) => false; + public virtual bool IsTargetValid(IBattlePokemon target) => false; /// /// Returns whether the item can be held by a Pokémon. @@ -51,7 +51,7 @@ public abstract class ItemScript : IDeepCloneable /// /// Returns whether the item can be held by the given target. /// - public virtual bool CanTargetHold(IPokemon pokemon) => true; + public virtual bool CanTargetHold(IBattlePokemon pokemon) => true; /// /// Handles the use of the item. @@ -63,7 +63,7 @@ public abstract class ItemScript : IDeepCloneable /// /// Handles the use of the item on the given target. /// - public virtual void OnUseWithTarget(IPokemon target, EventHook eventHook) + public virtual void OnUseWithTarget(IBattlePokemon target, EventHook eventHook) { } } \ No newline at end of file diff --git a/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs b/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs index 4fdc006..8cc9a45 100644 --- a/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs +++ b/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs @@ -17,9 +17,9 @@ public abstract class PokeballScript : ItemScript /// /// Returns the catch rate of the Pokéball against the given target Pokémon. /// - public abstract void ChangeCatchRate(IPokemon target, ref byte catchRate); + public abstract void ChangeCatchRate(IBattlePokemon target, ref byte catchRate); - public virtual void OnAfterSuccessfulCapture(IPokemon target) + public virtual void OnAfterSuccessfulCapture(IBattlePokemon target) { // Default implementation does nothing. // Override this method in derived classes to add custom behavior after a successful capture. @@ -32,16 +32,13 @@ public abstract class PokeballScript : ItemScript public override ItemTargetType TargetType => ItemTargetType.FoePokemon; /// - public override bool IsTargetValid(IPokemon target) => - target.BattleData is not null && target.BattleData.Battle.IsWildBattle; + public override bool IsTargetValid(IBattlePokemon target) => target.Battle.IsWildBattle; /// - public override void OnUseWithTarget(IPokemon target, EventHook eventHook) + public override void OnUseWithTarget(IBattlePokemon target, EventHook eventHook) { - var battleData = target.BattleData; - - var result = battleData?.Battle.AttempCapture(battleData.SideIndex, battleData.Position, Item); - if (result is { IsCaught: true }) + var result = target.Battle.AttempCapture(target.SideIndex, target.Position, Item); + if (result.IsCaught) { OnAfterSuccessfulCapture(target); } diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs index 8dcdaa8..c745b89 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs @@ -25,7 +25,7 @@ public interface IAIInfoScriptExpectedEndOfTurnDamage /// This function returns the expected end of turn damage for the script. This is used for scripts that /// have an end of turn effect, such as Poison or Burn. /// - void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage); + void ExpectedEndOfTurnDamage(IBattlePokemon pokemon, ref int damage); } /// @@ -37,5 +37,5 @@ public interface IAIInfoScriptExpectedEntryDamage /// This function returns the expected entry damage for the script. This is used for scripts that have /// an entry hazard effect, such as Spikes or Stealth Rock. /// - void ExpectedEntryDamage(IPokemon pokemon, ref uint damage); + void ExpectedEntryDamage(IBattlePokemon pokemon, ref uint damage); } \ No newline at end of file diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs index 1d9d216..dd068e6 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs @@ -100,8 +100,8 @@ public static class ScriptExecution /// /// Executes a script on an item. /// - public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IPokemon? target, IPokemon user, - IBattle battle, EventHook eventHook) + public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IBattlePokemon? target, + IBattlePokemon user, IBattle battle, EventHook eventHook) { if (!scriptResolver.TryResolveBattleItemScript(item, out var itemScript)) { diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs index 067ea73..f97208e 100644 --- a/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs +++ b/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs @@ -125,7 +125,7 @@ public interface IScriptChangeTargets /// /// Changes the targets of a move choice. This allows for changing the targets of a move before the move starts. /// - void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList targets); + void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList targets); } /// @@ -136,7 +136,7 @@ public interface IScriptChangeIncomingTargets /// /// This function allows you to change the targets of a move choice before the move starts. /// - void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList targets); + void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList targets); } /// @@ -219,7 +219,7 @@ public interface IScriptChangeMoveType /// This function allows the script to change the actual type that is used for the move on a target. /// If this is set to null, the move will be treated as a typeless move. /// - void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? typeIdentifier); + void ChangeMoveType(IExecutingMove move, IBattlePokemon target, byte hit, ref TypeIdentifier? typeIdentifier); } /// @@ -230,7 +230,7 @@ public interface IScriptChangeEffectiveness /// /// This function allows the script to change how effective a move is on a target. /// - void ChangeEffectiveness(IExecutingMove move, IPokemon target, byte hit, ref float effectiveness); + void ChangeEffectiveness(IExecutingMove move, IBattlePokemon target, byte hit, ref float effectiveness); } /// @@ -241,7 +241,7 @@ public interface IScriptChangeIncomingEffectiveness /// /// This function allows the script to override how effective a move is on a target. /// - void ChangeIncomingEffectiveness(IExecutingMove executingMove, IPokemon target, byte hitIndex, + void ChangeIncomingEffectiveness(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref float effectiveness); } @@ -253,7 +253,7 @@ public interface IScriptBlockCriticalHit /// /// This function allows a script to block an outgoing move from being critical. /// - void BlockCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block); + void BlockCriticalHit(IExecutingMove move, IBattlePokemon target, byte hit, ref bool block); } /// @@ -264,7 +264,7 @@ public interface IScriptBlockIncomingCriticalHit /// /// This function allows a script to block an incoming move from being critical. /// - void BlockIncomingCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block); + void BlockIncomingCriticalHit(IExecutingMove move, IBattlePokemon target, byte hit, ref bool block); } /// @@ -276,7 +276,7 @@ public interface IScriptOnIncomingHit /// This function triggers when an incoming hit happens. This triggers after the damage is done, /// but before the secondary effect of the move happens. /// - void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit); + void OnIncomingHit(IExecutingMove move, IBattlePokemon target, byte hit); } /// @@ -287,7 +287,7 @@ public interface IScriptOnOpponentFaints /// /// This function triggers when an opponent on the f ield faints due to the move that is being executed. /// - void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit); + void OnOpponentFaints(IExecutingMove move, IBattlePokemon target, byte hit); } /// @@ -300,7 +300,7 @@ public interface IScriptOnSecondaryEffect /// secondary effects here. Status moves should implement their actual functionality in this /// function as well, as status moves effects are defined as secondary effects for simplicity. /// - void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit); + void OnSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit); } /// @@ -312,7 +312,7 @@ public interface IScriptFailIncomingMove /// This function allows a script to prevent a move that is targeted at its owner. If set to true /// the move fails, and fail events get triggered. /// - void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail); + void FailIncomingMove(IExecutingMove move, IBattlePokemon target, ref bool fail); } /// @@ -323,7 +323,7 @@ public interface IScriptIsInvulnerableToMove /// /// This function allows a script to make its owner invulnerable to an incoming move. /// - void IsInvulnerableToMove(IExecutingMove move, IPokemon target, ref bool invulnerable); + void IsInvulnerableToMove(IExecutingMove move, IBattlePokemon target, ref bool invulnerable); } /// @@ -335,7 +335,7 @@ public interface IScriptOnMoveMiss /// This function allows a script to run when a move misses its target. This is used for moves /// that have a secondary effect that should run even if the move misses, such as Spore. /// - void OnMoveMiss(IExecutingMove move, IPokemon target); + void OnMoveMiss(IExecutingMove move, IBattlePokemon target); } /// @@ -347,7 +347,7 @@ public interface IScriptChangeAccuracyModifier /// This function allows a script to modify the accuracy of a move used. This value represents /// the percentage accuracy, so anything above 100% will make it always hit. /// - void ChangeAccuracyModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); + void ChangeAccuracyModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier); } /// @@ -358,7 +358,7 @@ public interface IScriptChangeCriticalStage /// /// This function allows a script to change the critical stage of the move used. /// - void ChangeCriticalStage(IExecutingMove move, IPokemon target, byte hit, ref byte stage); + void ChangeCriticalStage(IExecutingMove move, IBattlePokemon target, byte hit, ref byte stage); } /// @@ -370,7 +370,7 @@ public interface IScriptChangeCriticalModifier /// This function allows a script to change the damage modifier of a critical hit. This will only /// run when a hit is critical. /// - void ChangeCriticalModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); + void ChangeCriticalModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier); } /// @@ -382,7 +382,7 @@ public interface IScriptChangeStabModifier /// This function allows a script to change the damage modifier of a Same Type Attack Bonus, which /// occurs when the user has the move type as one of its own types. /// - void ChangeStabModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, bool isStab, + void ChangeStabModifier(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, bool isStab, ref float modifier); } @@ -394,7 +394,7 @@ public interface IScriptChangeBasePower /// /// This function allows a script to change the effective base power of a move hit. /// - void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower); + void ChangeBasePower(IExecutingMove move, IBattlePokemon target, byte hit, ref ushort basePower); } /// @@ -407,7 +407,7 @@ public interface IScriptBypassDefensiveStatBoosts /// If this is true, the damage will be calculated as if the target has no positive stat boosts. Negative /// stat boosts will still be applied. /// - void BypassDefensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass); + void BypassDefensiveStatBoosts(IExecutingMove move, IBattlePokemon target, byte hit, ref bool bypass); } /// @@ -419,7 +419,7 @@ public interface IScriptBypassEvasionStatBoosts /// This function allows a script to bypass evasion stat boosts for a move hit. /// If this is true, the move will handle the evasion stat boosts as if the target has no positive stat boosts. /// - void BypassEvasionStatBoosts(IExecutingMove move, IPokemon target, byte hitIndex, ref bool bypass); + void BypassEvasionStatBoosts(IExecutingMove move, IBattlePokemon target, byte hitIndex, ref bool bypass); } /// @@ -432,7 +432,7 @@ public interface IScriptBypassOffensiveStatBoosts /// If this is true, the damage will be calculated as if the user has no negative offensive stat boosts. Positive /// stat boosts will still be applied. /// - void BypassOffensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass); + void BypassOffensiveStatBoosts(IExecutingMove move, IBattlePokemon target, byte hit, ref bool bypass); } /// @@ -443,7 +443,7 @@ public interface IScriptChangeOffensiveStatValue /// /// This function allows a script to change the actual offensive stat values used when calculating damage /// - void ChangeOffensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint defensiveStat, + void ChangeOffensiveStatValue(IExecutingMove move, IBattlePokemon target, byte hit, uint defensiveStat, ImmutableStatisticSet targetStats, Statistic stat, ref uint value); } @@ -455,7 +455,7 @@ public interface IScriptChangeDefensiveStatValue /// /// This function allows a script to change the actual defensive stat values used when calculating damage. /// - void ChangeDefensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint offensiveStat, + void ChangeDefensiveStatValue(IExecutingMove move, IBattlePokemon target, byte hit, uint offensiveStat, ImmutableStatisticSet targetStats, Statistic stat, ref uint value); } @@ -467,7 +467,7 @@ public interface IScriptChangeIncomingMoveOffensiveStatValue /// /// This function allows a script to change the offensive stat value of an incoming move. /// - void ChangeIncomingMoveOffensiveStatValue(IExecutingMove executingMove, IPokemon target, byte hitNumber, + void ChangeIncomingMoveOffensiveStatValue(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, uint defensiveStat, StatisticSet targetStats, Statistic offensive, ref uint offensiveStat); } @@ -479,7 +479,7 @@ public interface IScriptChangeIncomingMoveDefensiveStatValue /// /// This function allows a script to change the defensive stat value of an incoming move. /// - void ChangeIncomingMoveDefensiveStatValue(IExecutingMove executingMove, IPokemon target, byte hitNumber, + void ChangeIncomingMoveDefensiveStatValue(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, uint origOffensiveStat, StatisticSet targetStats, Statistic defensive, ref uint defensiveStat); } @@ -492,7 +492,7 @@ public interface IScriptChangeDamageStatModifier /// This function allows a script to change the raw modifier we retrieved from the stats of the /// defender and attacker. The default value is the offensive stat divided by the defensive stat. /// - void ChangeDamageStatModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); + void ChangeDamageStatModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier); } /// @@ -503,7 +503,7 @@ public interface IScriptChangeDamageModifier /// /// This function allows a script to apply a raw multiplier to the damage done by a move. /// - void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); + void ChangeDamageModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier); } /// @@ -514,7 +514,7 @@ public interface IScriptChangeIncomingMoveDamageModifier /// /// This function allows a script to change the damage modifier of an incoming move. /// - void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, + void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, ref float modifier); } @@ -526,7 +526,7 @@ public interface IScriptChangeMoveDamage /// /// This function allows a script to modify the outgoing damage done by a move. /// - void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage); + void ChangeMoveDamage(IExecutingMove move, IBattlePokemon target, byte hit, ref uint damage); } /// @@ -537,7 +537,7 @@ public interface IScriptChangeIncomingMoveDamage /// /// This function allows a script to modify the incoming damage done by a move. /// - void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage); + void ChangeIncomingMoveDamage(IExecutingMove move, IBattlePokemon target, byte hit, ref uint damage); } /// @@ -549,7 +549,8 @@ public interface IScriptPreventStatBoostChange /// This function allows a script attached to a Pokemon or its parents to prevent stat boost /// changes on that Pokemon. /// - void PreventStatBoostChange(IPokemon target, Statistic stat, sbyte amount, bool selfInflicted, ref bool prevent); + void PreventStatBoostChange(IBattlePokemon target, Statistic stat, sbyte amount, bool selfInflicted, + ref bool prevent); } /// @@ -562,7 +563,7 @@ public interface IScriptChangeStatBoostChange /// which the stat boost will change. If the stat boost is done by the user itself, self /// inflicted will be true, otherwise it will be false. /// - void ChangeStatBoostChange(IPokemon target, Statistic stat, bool selfInflicted, ref sbyte amount); + void ChangeStatBoostChange(IBattlePokemon target, Statistic stat, bool selfInflicted, ref sbyte amount); } /// @@ -573,7 +574,7 @@ public interface IScriptOnAfterStatBoostChange /// /// This function allows a script to run after a stat boost change has been applied. /// - void OnAfterStatBoostChange(IPokemon pokemon, Statistic stat, bool selfInflicted, sbyte change); + void OnAfterStatBoostChange(IBattlePokemon pokemon, Statistic stat, bool selfInflicted, sbyte change); } /// @@ -586,7 +587,7 @@ public interface IScriptPreventSecondaryEffect /// This means the move will still hit and do damage, but not trigger its secondary effect. Note that this /// function is not called for status moves. /// - void PreventSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent); + void PreventSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit, ref bool prevent); } /// @@ -599,7 +600,7 @@ public interface IScriptPreventIncomingSecondaryEffect /// secondary effect. This means the move will still hit and do damage, but not trigger its /// secondary effect. Note that this function is not called for status moves. /// - void PreventIncomingSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent); + void PreventIncomingSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit, ref bool prevent); } /// @@ -613,7 +614,7 @@ public interface IScriptChangeEffectChance /// changing this to above or equal to 100 will make it always hit, while setting it to equal or /// below 0 will make it never hit. /// - void ChangeEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance); + void ChangeEffectChance(IExecutingMove move, IBattlePokemon target, byte hit, ref float chance); } /// @@ -627,7 +628,7 @@ public interface IScriptChangeIncomingEffectChance /// so changing this to above or equal to 100 will make it always hit, while setting it to equal /// or below 0 will make it never hit. /// - void ChangeIncomingEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance); + void ChangeIncomingEffectChance(IExecutingMove move, IBattlePokemon target, byte hit, ref float chance); } /// @@ -638,7 +639,7 @@ public interface IScriptOnAfterHits /// /// This function triggers on a move or its parents when all hits on a target are finished. /// - void OnAfterHits(IExecutingMove move, IPokemon target); + void OnAfterHits(IExecutingMove move, IBattlePokemon target); } /// @@ -706,7 +707,7 @@ public interface IScriptOnDamage /// /// This function is triggered on a Pokemon and its parents when the given Pokemon takes damage. /// - void OnDamage(IPokemon pokemon, DamageSource source, uint oldHealth, uint newHealth); + void OnDamage(IBattlePokemon pokemon, DamageSource source, uint oldHealth, uint newHealth); } /// @@ -717,7 +718,7 @@ public interface IScriptOnFaint /// /// This function is triggered on a Pokemon and its parents when the given Pokemon faints. /// - void OnFaint(IPokemon pokemon, DamageSource source); + void OnFaint(IBattlePokemon pokemon, DamageSource source); } /// @@ -728,7 +729,7 @@ public interface IScriptOnAllyFaint /// /// This function is triggered on a Pokemon when an ally Pokemon faints. /// - void OnAllyFaint(IPokemon ally, IPokemon faintedPokemon); + void OnAllyFaint(IBattlePokemon ally, IBattlePokemon faintedPokemon); } /// @@ -740,7 +741,7 @@ public interface IScriptOnSwitchOut /// This function is triggered on a Pokemon and its parents when the given Pokemon switches out /// of the battlefield. /// - void OnSwitchOut(IPokemon oldPokemon, byte position); + void OnSwitchOut(IBattlePokemon oldPokemon, byte position); } /// @@ -752,7 +753,7 @@ public interface IScriptOnSwitchIn /// This function is triggered on a Pokemon and its parents when the given Pokemon is switched into /// the battlefield. /// - void OnSwitchIn(IPokemon pokemon, byte position); + void OnSwitchIn(IBattlePokemon pokemon, byte position); } /// @@ -763,7 +764,7 @@ public interface IScriptOnOpponentSwitchIn /// /// This function is triggered on a Pokemon and its parents when an opponent switches in. /// - void OnOpponentSwitchIn(IPokemon pokemon, byte position); + void OnOpponentSwitchIn(IBattlePokemon pokemon, byte position); } /// @@ -787,7 +788,7 @@ public interface IScriptOnAfterItemConsume /// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the /// held item it had. /// - void OnAfterItemConsume(IPokemon pokemon, IItem item); + void OnAfterItemConsume(IBattlePokemon pokemon, IItem item); } /// @@ -798,7 +799,7 @@ public interface IScriptBlockIncomingHit /// /// This function allows a script to block an incoming hit. /// - void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block); + void BlockIncomingHit(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool block); } /// @@ -809,7 +810,7 @@ public interface IScriptBlockOutgoingHit /// /// This function allows a script to block an outgoing hit. /// - void BlockOutgoingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block); + void BlockOutgoingHit(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool block); } /// @@ -820,7 +821,7 @@ public interface IScriptPreventHeldItemConsume /// /// This function allows a script to prevent a held item from being consumed. /// - void PreventHeldItemConsume(IPokemon pokemon, IItem heldItem, ref bool prevented); + void PreventHeldItemConsume(IBattlePokemon pokemon, IItem heldItem, ref bool prevented); } /// @@ -831,7 +832,7 @@ public interface IScriptChangeIncomingDamage /// /// This function allows a script to change any kind of damage that is incoming. /// - void ChangeIncomingDamage(IPokemon pokemon, DamageSource source, ref uint damage); + void ChangeIncomingDamage(IBattlePokemon pokemon, DamageSource source, ref uint damage); } /// @@ -853,7 +854,7 @@ public interface IScriptPreventHeal /// /// This function allows a script to prevent a Pokemon from being healed. /// - void PreventHeal(IPokemon pokemon, uint heal, bool allowRevive, ref bool prevented); + void PreventHeal(IBattlePokemon pokemon, uint heal, bool allowRevive, ref bool prevented); } /// @@ -865,7 +866,8 @@ public interface IScriptChangeTypesForMove /// This function allows a script to change the types a target has. Multiple types can be set, and will be used /// for the effectiveness calculation. /// - void ChangeTypesForMove(IExecutingMove executingMove, IPokemon target, byte hitIndex, IList types); + void ChangeTypesForMove(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, + IList types); } /// @@ -877,7 +879,7 @@ public interface IScriptChangeTypesForIncomingMove /// This function allows a script to change the types a Pokemon has for a move that's incoming. Multiple types can /// be set, and will be used for the effectiveness calculation. /// - void ChangeTypesForIncomingMove(IExecutingMove executingMove, IPokemon target, byte hitIndex, + void ChangeTypesForIncomingMove(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, IList types); } @@ -890,7 +892,7 @@ public interface IScriptChangeCategory /// This function allows a script to change the handling of the move category. This is used for moves that /// are sometimes a status move, and sometimes a damaging move, such as pollen puff. /// - void ChangeCategory(IExecutingMove move, IPokemon target, byte hitIndex, ref MoveCategory category); + void ChangeCategory(IExecutingMove move, IBattlePokemon target, byte hitIndex, ref MoveCategory category); } /// @@ -901,7 +903,7 @@ public interface IScriptOnBeforeHit /// /// Triggers first when we're about to hit a target. /// - void OnBeforeHit(IExecutingMove move, IPokemon target, byte hitIndex); + void OnBeforeHit(IExecutingMove move, IBattlePokemon target, byte hitIndex); } /// @@ -912,7 +914,7 @@ public interface IScriptPreventStatusChange /// /// This function allows a script to prevent a Pokemon from being affected by a status condition. /// - void PreventStatusChange(IPokemon pokemon, StringKey status, bool selfInflicted, ref bool preventStatus); + void PreventStatusChange(IBattlePokemon pokemon, StringKey status, bool selfInflicted, ref bool preventStatus); } /// @@ -923,7 +925,7 @@ public interface IScriptOnAfterStatusChange /// /// This function triggers after a status condition has been applied to a Pokemon. /// - void OnAfterStatusChange(IPokemon pokemon, StringKey status, IPokemon? originPokemon); + void OnAfterStatusChange(IBattlePokemon pokemon, StringKey status, IPokemon? originPokemon); } /// @@ -946,7 +948,7 @@ public interface IScriptIsFloating /// This function allows a script to make the Pokémon it is attached to float. This is used for moves /// such as levitate, and allows for moves such as earthquake to not hit the Pokémon. /// - void IsFloating(IPokemon pokemon, ref bool isFloating); + void IsFloating(IBattlePokemon pokemon, ref bool isFloating); } /// @@ -991,7 +993,7 @@ public interface IScriptModifyIsContact /// /// Modifies whether a move is a contact move or not. This is used for abilities such as Long Reach. /// - void ModifyIsContact(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool isContact); + void ModifyIsContact(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool isContact); } /// @@ -1002,7 +1004,7 @@ public interface IScriptPreventHeldItemSteal /// /// This function allows a script to prevent a held item from being stolen by an effect such as Thief or Covet. /// - void PreventHeldItemSteal(IPokemon pokemon, IItem heldItem, ref bool prevent); + void PreventHeldItemSteal(IBattlePokemon pokemon, IItem heldItem, ref bool prevent); } /// @@ -1013,7 +1015,7 @@ public interface IScriptOnAfterHeldItemChange /// /// This function allows a script to run after a held item has changed. /// - void OnAfterHeldItemChange(IPokemon pokemon, IItem? previous, IItem? item); + void OnAfterHeldItemChange(IBattlePokemon pokemon, IItem? previous, IItem? item); } /// @@ -1080,7 +1082,7 @@ public interface IScriptChangeExperienceGained /// This function is triggered on a Pokemon and its parents when the given Pokemon gains experience, /// and allows for changing this amount of experience. /// - void ChangeExperienceGained(IPokemon faintedPokemon, IPokemon winningPokemon, ref uint amount); + void ChangeExperienceGained(IBattlePokemon faintedPokemon, IBattlePokemon winningPokemon, ref uint amount); } /// @@ -1093,7 +1095,8 @@ public interface IScriptShareExperience /// and allows for making the experience be shared across multiple Pokemon. /// Amount is the modifier for how much experience is shared, with 1 being the default amount. /// - void ShareExperience(IPokemon faintedPokemon, IPokemon winningPokemon, ref bool share, ref float amount); + void ShareExperience(IBattlePokemon faintedPokemon, IBattlePokemon winningPokemon, ref bool share, + ref float amount); } /// @@ -1117,7 +1120,7 @@ public interface IScriptChangeCatchRateBonus /// rate of this attempt. Pokeball modifier effects should be implemented here, as well as for /// example status effects that change capture rates. /// - void ChangeCatchRateBonus(IPokemon pokemon, IItem pokeball, ref byte modifier); + void ChangeCatchRateBonus(IBattlePokemon pokemon, IItem pokeball, ref byte modifier); } /// @@ -1142,7 +1145,7 @@ public interface IScriptChangeAccuracy /// A custom case goes when 255 is returned, in which case the entire accuracy check is skipped, and the move /// will always hit. /// - void ChangeAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy); + void ChangeAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref int modifiedAccuracy); } /// @@ -1155,5 +1158,6 @@ public interface IScriptChangeIncomingAccuracy /// A custom case goes when 255 is returned, in which case the entire accuracy check is skipped, and the move /// will always hit. /// - void ChangeIncomingAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy); + void ChangeIncomingAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, + ref int modifiedAccuracy); } \ No newline at end of file diff --git a/PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs b/PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs new file mode 100644 index 0000000..2282aa8 --- /dev/null +++ b/PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs @@ -0,0 +1,160 @@ +using PkmnLib.Dynamic.Libraries; +using PkmnLib.Dynamic.Models; +using PkmnLib.Static; +using PkmnLib.Static.Species; +using PkmnLib.Tests.Integration; + +namespace PkmnLib.Tests.Dynamic; + +/// +/// Regression tests for the battle lifecycle of : battle-only state must die +/// with the battle, and the underlying must come out of a battle unchanged except +/// for the deliberately persistent parts (health, PP, non-volatile status, experience). +/// +public class BattleLifecycleTests +{ + private static IPokemon CreatePokemon(IDynamicLibrary library, string speciesName) + { + if (!library.StaticLibrary.Species.TryGet(speciesName, out var species)) + throw new InvalidOperationException($"Failed to load {speciesName} species."); + return new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex + { + Index = 0, + IsHidden = false, + }, 50, 0, Gender.Male, 0, "hardy"); + } + + private static IBattle CreateBattle(IDynamicLibrary library, IPokemon own, IPokemon opponent, + bool isWildBattle = false, int seed = 0) + { + var party = new PokemonPartyImpl(1); + party.SwapInto(own, 0); + var opponentParty = new PokemonPartyImpl(1); + opponentParty.SwapInto(opponent, 0); + var parties = new[] + { + new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]), + new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]), + }; + var battle = new BattleImpl(library, parties, false, 2, 1, isWildBattle, "grass", seed); + battle.Sides[0].SendOut(0, own); + battle.Sides[1].SendOut(0, opponent); + return battle; + } + + /// + /// The original bug this refactor removes by construction: a captured Pokémon must not carry any + /// battle state into the party of its captor. The capture is reported through + /// as the persistent Pokémon. + /// + [Test] + public async Task Capture_ReportsPersistentPokemonAndLeavesItUsable() + { + var library = LibraryHelpers.LoadLibrary(); + if (!library.StaticLibrary.Items.TryGet("master_ball", out var masterBall)) + throw new InvalidOperationException("Failed to load master ball."); + + // The capture roll is random; find a seed where the capture succeeds. The loop is deterministic, + // so the test always exercises the same battle. + var result = CaptureResult.Failed; + IBattle? battle = null; + IPokemon? wildPokemon = null; + for (var seed = 0; seed < 100 && !result.IsCaught; seed++) + { + battle?.Dispose(); + var own = CreatePokemon(library, "bulbasaur"); + wildPokemon = CreatePokemon(library, "caterpie"); + battle = CreateBattle(library, own, wildPokemon, true, seed); + var wildBattlePokemon = battle.GetPokemon(1, 0)!; + wildBattlePokemon.Damage(wildBattlePokemon.CurrentHealth - 1, DamageSource.MoveDamage); + result = battle.AttempCapture(1, 0, masterBall); + } + + await Assert.That(result.IsCaught).IsTrue(); + await Assert.That(battle!.HasEnded).IsTrue(); + await Assert.That(battle.Result!.Value.CapturedPokemon).Contains(wildPokemon!); + + battle.Dispose(); + + // The persistent Pokémon left the battle without any battle state: it is usable and simply damaged. + await Assert.That(wildPokemon!.IsUsable).IsTrue(); + await Assert.That(wildPokemon.CurrentHealth).IsEqualTo(1u); + } + + /// + /// Items removed or stolen during a battle are a battle-only overlay: after the battle, the victim + /// still holds its item. + /// + [Test] + public async Task StolenHeldItem_IsRestoredAfterBattle() + { + var library = LibraryHelpers.LoadLibrary(); + if (!library.StaticLibrary.Items.TryGet("oran_berry", out var berry)) + throw new InvalidOperationException("Failed to load oran berry."); + var own = CreatePokemon(library, "bulbasaur"); + _ = own.ForceSetHeldItem(berry); + var opponent = CreatePokemon(library, "charmander"); + + var battle = CreateBattle(library, own, opponent); + var battlePokemon = battle.GetPokemon(0, 0)!; + await Assert.That(battlePokemon.TryStealHeldItem(out _)).IsTrue(); + await Assert.That(battlePokemon.HeldItem).IsNull(); + + battle.Dispose(); + + await Assert.That(own.HeldItem).IsSameReferenceAs(berry); + } + + /// + /// A battle-only form (such as a mega evolution) reverts to the original form when the battle ends. + /// + [Test] + public async Task BattleOnlyForm_RevertsWhenBattleEnds() + { + var library = LibraryHelpers.LoadLibrary(); + var own = CreatePokemon(library, "absol"); + var originalForm = own.Form; + if (!own.Species.TryGetForm("mega", out var megaForm)) + throw new InvalidOperationException("Absol has no mega form."); + await Assert.That(megaForm.IsBattleOnlyForm).IsTrue(); + var opponent = CreatePokemon(library, "charmander"); + + var battle = CreateBattle(library, own, opponent); + var battlePokemon = battle.GetPokemon(0, 0)!; + battlePokemon.ChangeForm(megaForm); + await Assert.That(own.Form).IsSameReferenceAs(megaForm); + + battle.Dispose(); + + await Assert.That(own.Form).IsSameReferenceAs(originalForm); + } + + /// + /// A second battle with the same party starts with fresh battle state: no seen opponents or stale + /// original species from the previous battle. + /// + [Test] + public async Task SecondBattleWithSameParty_StartsWithFreshBattleState() + { + var library = LibraryHelpers.LoadLibrary(); + var own = CreatePokemon(library, "bulbasaur"); + var opponent1 = CreatePokemon(library, "charmander"); + + var battle1 = CreateBattle(library, own, opponent1); + var firstWrapper = battle1.GetPokemon(0, 0)!; + await Assert.That(firstWrapper.SeenOpponents.Count).IsEqualTo(1); + firstWrapper.ChangeStatBoost(Statistic.Attack, 3, true, false); + battle1.Dispose(); + + var opponent2 = CreatePokemon(library, "squirtle"); + var battle2 = CreateBattle(library, own, opponent2); + var secondWrapper = battle2.GetPokemon(0, 0)!; + + await Assert.That(secondWrapper).IsNotSameReferenceAs(firstWrapper); + await Assert.That(secondWrapper.SeenOpponents.Count).IsEqualTo(1); + await Assert.That(secondWrapper.SeenOpponents[0].UnderlyingPokemon).IsSameReferenceAs(opponent2); + await Assert.That(secondWrapper.StatBoost.Attack).IsEqualTo((sbyte)0); + await Assert.That(secondWrapper.OriginalSpecies).IsSameReferenceAs(own.Species); + battle2.Dispose(); + } +} \ No newline at end of file diff --git a/PkmnLib.Tests/Dynamic/BattlePokemonTests.cs b/PkmnLib.Tests/Dynamic/BattlePokemonTests.cs new file mode 100644 index 0000000..4cdbc36 --- /dev/null +++ b/PkmnLib.Tests/Dynamic/BattlePokemonTests.cs @@ -0,0 +1,219 @@ +using PkmnLib.Dynamic.Models; +using PkmnLib.Static; +using PkmnLib.Static.Species; +using PkmnLib.Tests.Integration; + +namespace PkmnLib.Tests.Dynamic; + +/// +/// Tests for the ephemeral wrapper. Battle-only state must live on the wrapper +/// and never leak into the underlying ; dropping the wrapper is all the cleanup a +/// battle needs. +/// +public class BattlePokemonTests +{ + private static (IBattle battle, IBattlePokemon wrapper, IPokemon inner) CreateBattleWithWrapper( + string heldItem = "oran_berry") + { + var library = LibraryHelpers.LoadLibrary(); + if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var bulbasaur)) + throw new InvalidOperationException("Failed to load bulbasaur species."); + if (!library.StaticLibrary.Species.TryGet("charmander", out var charmander)) + throw new InvalidOperationException("Failed to load charmander species."); + if (!library.StaticLibrary.Items.TryGet(heldItem, out var item)) + throw new InvalidOperationException($"Failed to load item {heldItem}."); + + var pokemon = new PokemonImpl(library, bulbasaur, bulbasaur.GetDefaultForm(), new AbilityIndex + { + Index = 0, + IsHidden = false, + }, 50, 0, Gender.Male, 0, "hardy"); + pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0); + _ = pokemon.ForceSetHeldItem(item); + + var opponent = new PokemonImpl(library, charmander, charmander.GetDefaultForm(), new AbilityIndex + { + Index = 0, + IsHidden = false, + }, 50, 0, Gender.Male, 0, "hardy"); + + var party1 = new PokemonPartyImpl(6); + party1.SwapInto(pokemon, 0); + var party2 = new PokemonPartyImpl(6); + party2.SwapInto(opponent, 0); + var parties = new[] + { + new BattlePartyImpl(party1, [new ResponsibleIndex(0, 0)]), + new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]), + }; + var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0); + var wrapper = parties[0].GetBattlePokemon(pokemon) ?? + throw new InvalidOperationException("Wrapper not created."); + return (battle, wrapper, pokemon); + } + + [Test] + public async Task PersistentMembersAreProxiedToUnderlyingPokemon() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + + await Assert.That(wrapper.UnderlyingPokemon).IsSameReferenceAs(inner); + await Assert.That(wrapper.Species).IsSameReferenceAs(inner.Species); + await Assert.That(wrapper.Form).IsSameReferenceAs(inner.Form); + await Assert.That(wrapper.Level).IsEqualTo(inner.Level); + await Assert.That(wrapper.CurrentHealth).IsEqualTo(inner.CurrentHealth); + await Assert.That(wrapper.Nature).IsSameReferenceAs(inner.Nature); + await Assert.That(wrapper.OriginalSpecies).IsSameReferenceAs(inner.Species); + await Assert.That(wrapper.OriginalForm).IsSameReferenceAs(inner.Form); + } + + [Test] + public async Task TypeOverlayDoesNotTouchUnderlyingPokemon() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + var originalTypes = inner.Types.ToList(); + + wrapper.SetTypes([new TypeIdentifier(18, "water")]); + + await Assert.That(wrapper.Types.Count).IsEqualTo(1); + await Assert.That(inner.Types.SequenceEqual(originalTypes)).IsTrue(); + + wrapper.OnSwitchedOut(); + await Assert.That(wrapper.Types.SequenceEqual(inner.Form.Types)).IsTrue(); + } + + [Test] + public async Task TemporaryMoveOverlayDoesNotTouchUnderlyingPokemonAndResetsOnSwitchOut() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + + wrapper.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); + + await Assert.That(wrapper.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance"); + await Assert.That(wrapper.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); + await Assert.That(inner.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); + + wrapper.OnSwitchedOut(); + await Assert.That(wrapper.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); + } + + [Test] + public async Task StatBoostLivesOnWrapperAndResetsOnSwitchOut() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + var innerFlatAttack = inner.FlatStats.Attack; + + var changed = wrapper.ChangeStatBoost(Statistic.Attack, 2, true, false); + + await Assert.That(changed).IsTrue(); + await Assert.That(wrapper.StatBoost.Attack).IsEqualTo((sbyte)2); + await Assert.That(wrapper.BoostedStats.Attack > wrapper.FlatStats.Attack).IsTrue(); + await Assert.That(inner.FlatStats.Attack).IsEqualTo(innerFlatAttack); + + wrapper.OnSwitchedOut(); + await Assert.That(wrapper.StatBoost.Attack).IsEqualTo((sbyte)0); + await Assert.That(wrapper.BoostedStats.Attack).IsEqualTo(wrapper.FlatStats.Attack); + } + + [Test] + public async Task HeldItemOverlayDoesNotTouchUnderlyingPokemon() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + var innerItem = inner.HeldItem; + await Assert.That(innerItem).IsNotNull(); + + var removed = wrapper.RemoveHeldItemForBattle(); + + await Assert.That(removed).IsSameReferenceAs(innerItem); + await Assert.That(wrapper.HeldItem).IsNull(); + await Assert.That(wrapper.HasItemBeenRemovedForBattle).IsTrue(); + await Assert.That(inner.HeldItem).IsSameReferenceAs(innerItem); + + wrapper.RestoreRemovedHeldItem(); + await Assert.That(wrapper.HeldItem).IsSameReferenceAs(innerItem); + await Assert.That(wrapper.HasItemBeenRemovedForBattle).IsFalse(); + } + + [Test] + public async Task StealingHeldItemDoesNotTouchUnderlyingPokemon() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + var innerItem = inner.HeldItem; + + var stolen = wrapper.TryStealHeldItem(out var item); + + await Assert.That(stolen).IsTrue(); + await Assert.That(item).IsSameReferenceAs(innerItem); + await Assert.That(wrapper.HeldItem).IsNull(); + await Assert.That(inner.HeldItem).IsSameReferenceAs(innerItem); + } + + [Test] + public async Task MarkAsCaughtOnlyAffectsWrapper() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + + wrapper.MarkAsCaught(); + + await Assert.That(wrapper.IsCaught).IsTrue(); + await Assert.That(wrapper.IsUsable).IsFalse(); + await Assert.That(inner.IsUsable).IsTrue(); + } + + [Test] + public async Task AbilityOverrideAndSuppressionLiveOnWrapper() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + var naturalAbility = inner.Ability; + await Assert.That(naturalAbility).IsNotNull(); + + wrapper.SuppressAbility(); + await Assert.That(wrapper.AbilitySuppressed).IsTrue(); + await Assert.That(wrapper.ActiveAbility).IsNull(); + await Assert.That(inner.Ability).IsSameReferenceAs(naturalAbility); + + wrapper.OnSwitchedOut(); + await Assert.That(wrapper.AbilitySuppressed).IsFalse(); + await Assert.That(wrapper.ActiveAbility).IsSameReferenceAs(naturalAbility); + } + + [Test] + public async Task GetOwnScriptsUsesUnderlyingStatusScript() + { + var (_, wrapper, inner) = CreateBattleWithWrapper(); + + var scripts = new List>(); + wrapper.GetOwnScripts(scripts); + + await Assert.That(scripts.Count).IsEqualTo(4); + await Assert.That(scripts[0]).IsSameReferenceAs(wrapper.HeldItemTriggerScript); + await Assert.That(scripts[1]).IsSameReferenceAs(wrapper.AbilityScript); + await Assert.That(scripts[2]).IsSameReferenceAs(inner.StatusScript); + await Assert.That(scripts[3]).IsSameReferenceAs(wrapper.Volatile); + } + + [Test] + public async Task GetBattlePokemonFindsWrapperByInnerAndByWrapper() + { + var (battle, wrapper, inner) = CreateBattleWithWrapper(); + var party = battle.Parties[0]; + + await Assert.That(party.GetBattlePokemon(inner)).IsSameReferenceAs(wrapper); + await Assert.That(party.GetBattlePokemon(wrapper)).IsSameReferenceAs(wrapper); + await Assert.That(battle.Parties[1].GetBattlePokemon(inner)).IsNull(); + } + + [Test] + public async Task OnSwitchedInTracksTurnAndPosition() + { + var (_, wrapper, _) = CreateBattleWithWrapper(); + await Assert.That(wrapper.IsOnBattlefield).IsFalse(); + + wrapper.OnSwitchedIn(0); + + await Assert.That(wrapper.IsOnBattlefield).IsTrue(); + await Assert.That(wrapper.Position).IsEqualTo((byte)0); + // Bulbasaur's ability (Overgrow) has no script in the Gen7 plugin, so the ability script stays empty. + await Assert.That(wrapper.AbilityScript.IsEmpty).IsTrue(); + } +} \ No newline at end of file diff --git a/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs b/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs index a11f5b4..b4d481b 100644 --- a/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs +++ b/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs @@ -8,8 +8,8 @@ public class ChoiceQueueTests [Test] public async Task ChoiceQueue_HighSpeedFirstWhenPriorityEqual() { - var pokemon1 = Substitute.For(); - var pokemon2 = Substitute.For(); + var pokemon1 = Substitute.For(); + var pokemon2 = Substitute.For(); var choice1 = Substitute.For(); choice1.User.Returns(pokemon1); @@ -28,8 +28,8 @@ public class ChoiceQueueTests [Test] public async Task ChoiceQueue_HighPriorityFirst() { - var pokemon1 = Substitute.For(); - var pokemon2 = Substitute.For(); + var pokemon1 = Substitute.For(); + var pokemon2 = Substitute.For(); var choice1 = Substitute.For(); choice1.User.Returns(pokemon1); @@ -48,10 +48,10 @@ public class ChoiceQueueTests [Test] public async Task ChoiceQueue_MovePokemonChoiceNext() { - var pokemon1 = Substitute.For(); - var pokemon2 = Substitute.For(); - var pokemon3 = Substitute.For(); - var pokemon4 = Substitute.For(); + var pokemon1 = Substitute.For(); + var pokemon2 = Substitute.For(); + var pokemon3 = Substitute.For(); + var pokemon4 = Substitute.For(); var choice1 = Substitute.For(); choice1.User.Returns(pokemon1); @@ -75,10 +75,10 @@ public class ChoiceQueueTests [Test] public async Task ChoiceQueue_MovePokemonChoiceNextFailsIfAlreadyExecuted() { - var pokemon1 = Substitute.For(); - var pokemon2 = Substitute.For(); - var pokemon3 = Substitute.For(); - var pokemon4 = Substitute.For(); + var pokemon1 = Substitute.For(); + var pokemon2 = Substitute.For(); + var pokemon3 = Substitute.For(); + var pokemon4 = Substitute.For(); var choice1 = Substitute.For(); choice1.User.Returns(pokemon1); @@ -103,10 +103,10 @@ public class ChoiceQueueTests [Test] public async Task ChoiceQueue_MovePokemonChoiceLast() { - var pokemon1 = Substitute.For(); - var pokemon2 = Substitute.For(); - var pokemon3 = Substitute.For(); - var pokemon4 = Substitute.For(); + var pokemon1 = Substitute.For(); + var pokemon2 = Substitute.For(); + var pokemon3 = Substitute.For(); + var pokemon4 = Substitute.For(); var choice1 = Substitute.For(); choice1.User.Returns(pokemon1); @@ -133,10 +133,10 @@ public class ChoiceQueueTests [Test] public async Task ChoiceQueue_MovePokemonChoiceLastFailsIfAlreadyExecuted() { - var pokemon1 = Substitute.For(); - var pokemon2 = Substitute.For(); - var pokemon3 = Substitute.For(); - var pokemon4 = Substitute.For(); + var pokemon1 = Substitute.For(); + var pokemon2 = Substitute.For(); + var pokemon3 = Substitute.For(); + var pokemon4 = Substitute.For(); var choice1 = Substitute.For(); choice1.User.Returns(pokemon1); diff --git a/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs b/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs index 911d89a..7a8d346 100644 --- a/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs +++ b/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs @@ -7,16 +7,31 @@ namespace PkmnLib.Tests.Dynamic; public class PokemonStatBoostTests { - private static IPokemon CreatePokemon() + private static IBattlePokemon CreatePokemon() { var library = LibraryHelpers.LoadLibrary(); if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species)) throw new InvalidOperationException("Failed to load bulbasaur species."); - return new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex + var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex { Index = 0, IsHidden = false, }, 50, 0, Gender.Male, 0, "hardy"); + var party = new PokemonPartyImpl(1); + party.SwapInto(pokemon, 0); + var opponentParty = new PokemonPartyImpl(1); + opponentParty.SwapInto(new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex + { + Index = 0, + IsHidden = false, + }, 50, 0, Gender.Male, 0, "hardy"), 0); + var parties = new[] + { + new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]), + new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]), + }; + var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0); + return battle.Parties[0].GetBattlePokemon(pokemon)!; } [Test] diff --git a/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs b/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs index 3deec7c..4e136cd 100644 --- a/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs +++ b/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs @@ -6,65 +6,78 @@ using PkmnLib.Tests.Integration; namespace PkmnLib.Tests.Dynamic; /// -/// Tests for the temporary move overlay (), used by effects such as -/// Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay must be -/// discarded by the engine itself when the Pokemon leaves the battlefield. +/// Tests for the temporary move overlay (), used by effects +/// such as Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay +/// must be discarded by the engine itself when the Pokemon leaves the battlefield. /// public class PokemonTemporaryMoveTests { - private static IPokemon CreatePokemon() + private static (IBattle battle, IBattlePokemon battlePokemon, IPokemon pokemon) CreateBattlePokemon() { var library = LibraryHelpers.LoadLibrary(); if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species)) throw new InvalidOperationException("Failed to load bulbasaur species."); - var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex - { - Index = 0, - IsHidden = false, - }, 50, 0, Gender.Male, 0, "hardy"); + + IPokemon CreateBulbasaur() => + new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex + { + Index = 0, + IsHidden = false, + }, 50, 0, Gender.Male, 0, "hardy"); + + var pokemon = CreateBulbasaur(); pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0); - return pokemon; + var party = new PokemonPartyImpl(1); + party.SwapInto(pokemon, 0); + var opponentParty = new PokemonPartyImpl(1); + opponentParty.SwapInto(CreateBulbasaur(), 0); + var parties = new[] + { + new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]), + new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]), + }; + var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0); + return (battle, battle.Parties[0].GetBattlePokemon(pokemon)!, pokemon); } [Test] public async Task LearnTemporaryMove_ReplacesMoveInMovesButNotInBaseMoves() { - var pokemon = CreatePokemon(); + var (_, battlePokemon, pokemon) = CreateBattlePokemon(); - pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); + battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); - await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance"); - await Assert.That(pokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic); - await Assert.That(pokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); + await Assert.That(battlePokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance"); + await Assert.That(battlePokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic); + await Assert.That(battlePokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); + await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); } [Test] - public async Task SetOnBattlefield_LeavingField_RestoresOriginalMoveWithItsPP() + public async Task OnSwitchedOut_RestoresOriginalMoveWithItsPP() { - var pokemon = CreatePokemon(); - pokemon.SetBattleData(Substitute.For(), 0); - pokemon.SetOnBattlefield(true); + var (_, battlePokemon, _) = CreateBattlePokemon(); + battlePokemon.OnSwitchedIn(0); // Use the original move once, so we can verify its PP survives the temporary replacement untouched. - var originalMove = pokemon.Moves[0]!; + var originalMove = battlePokemon.Moves[0]!; originalMove.TryUse(); var expectedPp = originalMove.CurrentPp; - pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); - pokemon.SetOnBattlefield(false); + battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); + battlePokemon.OnSwitchedOut(); - await Assert.That(ReferenceEquals(pokemon.Moves[0], originalMove)).IsTrue(); - await Assert.That(pokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp); + await Assert.That(ReferenceEquals(battlePokemon.Moves[0], originalMove)).IsTrue(); + await Assert.That(battlePokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp); } [Test] - public async Task ClearBattleData_RestoresOriginalMove() + public async Task BattleEnd_LeavesOriginalMovesUntouched() { - var pokemon = CreatePokemon(); - pokemon.SetBattleData(Substitute.For(), 0); - pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); + var (battle, battlePokemon, pokemon) = CreateBattlePokemon(); + battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); - pokemon.ClearBattleData(); + battle.Dispose(); await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); } @@ -72,10 +85,10 @@ public class PokemonTemporaryMoveTests [Test] public async Task Serialize_WithActiveTemporaryMove_WritesOriginalMove() { - var pokemon = CreatePokemon(); - pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); + var (_, battlePokemon, _) = CreateBattlePokemon(); + battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); - var serialized = pokemon.Serialize(); + var serialized = battlePokemon.Serialize(); await Assert.That(serialized.Moves[0]!.MoveName.ToString()).IsEqualTo("tackle"); } diff --git a/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs b/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs index 9e97c20..1ad02a5 100644 --- a/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs +++ b/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs @@ -22,7 +22,7 @@ public class SetPokemonAction : IntegrationTestAction public override Task Execute(IBattle battle) { - var mon = battle.Parties[FromParty[0]].Party[FromParty[1]]; + var mon = battle.Parties[FromParty[0]].BattlePokemon[FromParty[1]]; battle.Sides[Place[0]].SwapPokemon(Place[1], mon); Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}"); return Task.CompletedTask; diff --git a/PkmnLib.Tests/Static/DeepCloneTests.cs b/PkmnLib.Tests/Static/DeepCloneTests.cs index c1d158c..3014400 100644 --- a/PkmnLib.Tests/Static/DeepCloneTests.cs +++ b/PkmnLib.Tests/Static/DeepCloneTests.cs @@ -110,11 +110,12 @@ public class DeepCloneTests new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]), }; using var battle = new BattleImpl(library, parties, false, 2, 3, false, "grass", 0); - battle.Sides[0].SwapPokemon(0, party1[0]); - battle.Sides[1].SwapPokemon(0, party2[0]); - party1[0]!.ChangeStatBoost(Statistic.Defense, 2, true, false); - await Assert.That(party1[0]!.StatBoost.Defense).IsEqualTo((sbyte)2); - party1[0]!.Volatile.Add(new ChargeBounceEffect(party1[0]!)); + battle.Sides[0].SendOut(0, party1[0]!); + battle.Sides[1].SendOut(0, party2[0]!); + var battlePokemon1 = parties[0].GetBattlePokemon(party1[0]!)!; + battlePokemon1.ChangeStatBoost(Statistic.Defense, 2, true, false); + await Assert.That(battlePokemon1.StatBoost.Defense).IsEqualTo((sbyte)2); + battlePokemon1.Volatile.Add(new ChargeBounceEffect(battlePokemon1)); var clone = battle.DeepClone(); await Assert.That(clone).IsNotEqualTo(battle); @@ -129,11 +130,10 @@ public class DeepCloneTests var pokemon = clone.Sides[0].Pokemon[0]!; await Assert.That(pokemon).IsNotNull(); await Assert.That(pokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!); - await Assert.That(pokemon.BattleData).IsNotNull(); - await Assert.That(pokemon.BattleData).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.BattleData!); - await Assert.That(pokemon.BattleData!.Battle).IsEqualTo((IBattle)clone); - await Assert.That(pokemon.BattleData!.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!); - await Assert.That(pokemon.BattleData!.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!); + await Assert.That(pokemon.UnderlyingPokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.UnderlyingPokemon); + await Assert.That(pokemon.Battle).IsEqualTo((IBattle)clone); + await Assert.That(pokemon.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!); + await Assert.That(pokemon.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!); await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2); await Assert.That(pokemon.Volatile.Get()).IsNotNull(); await Assert.That(pokemon.Volatile.Get()).IsNotEqualTo( @@ -142,7 +142,7 @@ public class DeepCloneTests var ownerGetter = typeof(ChargeBounceEffect).GetField("_owner", BindingFlags.NonPublic | BindingFlags.Instance)!; var owner = ownerGetter.GetValue(pokemon.Volatile.Get()!); - await Assert.That((IPokemon)owner!).IsEqualTo(pokemon); + await Assert.That((IBattlePokemon)owner!).IsEqualTo(pokemon); pokemon.Volatile.Remove(); await Assert.That(pokemon.Volatile.Get()).IsNull(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs index 3ed9f1e..dd1f0bc 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs @@ -23,7 +23,7 @@ public class DamageCalculatorTests [Test] public async Task BulbapediaExampleDamageTest() { - var attacker = Substitute.For(); + var attacker = Substitute.For(); // Imagine a level 75 Glaceon attacker.Level.Returns((byte)75); // with an effective Attack stat of 123 @@ -31,7 +31,7 @@ public class DamageCalculatorTests // We use 10 as the Ice type attacker.Types.Returns([new TypeIdentifier(10, "ice")]); - var defender = Substitute.For(); + var defender = Substitute.For(); // a Garchomp with an effective Defense stat of 163 defender.BoostedStats.Returns(new StatisticSet(1, 1, 163, 1, 1, 1)); defender.GetScripts().Returns(new ScriptIterator([])); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs index 43c47b7..0d47c06 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs @@ -14,12 +14,12 @@ public class AftermathTests /// /// Creates a fully mocked test setup for Aftermath tests. /// - private static (Aftermath aftermath, IExecutingMove move, IPokemon target, IPokemon user, EventHook eventHook, - IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100) + private static (Aftermath aftermath, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, EventHook + eventHook, IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100) { var aftermath = new Aftermath(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(isContact); move.GetHitData(target, 0).Returns(hitData); @@ -30,20 +30,14 @@ public class AftermathTests // Setup empty sides by default (no Damp on field) var side = Substitute.For(); - side.Pokemon.Returns(new List()); + side.Pokemon.Returns(new List()); battle.Sides.Returns(new[] { side }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - - var user = Substitute.For(); + var user = Substitute.For(); user.IsUsable.Returns(true); + user.Battle.Returns(battle); user.MaxHealth.Returns(userMaxHealth); - // Configure BattleData return value using NSubstitute's callback pattern - // First return null to suppress auto-substitution, then configure actual value - user.BattleData.Returns(battleData); - move.User.Returns(user); return (aftermath, move, target, user, eventHook, battle); @@ -52,7 +46,7 @@ public class AftermathTests /// /// Helper to extract the damage amount from a substitute's received Damage calls. /// - private static uint GetDamageDealt(IPokemon user) + private static uint GetDamageDealt(IBattlePokemon user) { var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return damageCall != null ? (uint)damageCall.GetArguments()[0]! : 0; @@ -61,7 +55,7 @@ public class AftermathTests /// /// Helper to extract the damage source from a substitute's received Damage calls. /// - private static DamageSource? GetDamageSource(IPokemon user) + private static DamageSource? GetDamageSource(IBattlePokemon user) { var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return damageCall != null ? (DamageSource)damageCall.GetArguments()[1]! : null; @@ -151,12 +145,12 @@ public class AftermathTests // Arrange var aftermath = new Aftermath(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(true); move.GetHitData(target, 0).Returns(hitData); - var user = Substitute.For(); + var user = Substitute.For(); user.IsUsable.Returns(false); // Attacker already fainted move.User.Returns(user); @@ -168,34 +162,6 @@ public class AftermathTests await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse(); } - /// - /// Bulbapedia: "the attacking Pokémon takes damage". - /// If the attacker has no battle data, no damage should be dealt. - /// - [Test] - public async Task OnFaint_AttackerHasNoBattleData_DoesNotDealDamage() - { - // Arrange - var aftermath = new Aftermath(); - var move = Substitute.For(); - var target = Substitute.For(); - var hitData = Substitute.For(); - hitData.IsContact.Returns(true); - move.GetHitData(target, 0).Returns(hitData); - - var user = Substitute.For(); - user.IsUsable.Returns(true); - user.BattleData.Returns((IPokemonBattleData?)null); - move.User.Returns(user); - - // Act - aftermath.OnIncomingHit(move, target, 0); - aftermath.OnFaint(target, DamageSource.MoveDamage); - - // Assert - Damage should never be called because BattleData is null - await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse(); - } - /// /// Technical test: Verifies OnFaint handles the case where no attack was received. /// @@ -204,7 +170,7 @@ public class AftermathTests { // Arrange var aftermath = new Aftermath(); - var target = Substitute.For(); + var target = Substitute.For(); // Act & Assert - Should not throw when _lastAttack is null aftermath.OnFaint(target, DamageSource.MoveDamage); @@ -220,19 +186,16 @@ public class AftermathTests { // Arrange var aftermath = new Aftermath(); - var target = Substitute.For(); + var target = Substitute.For(); var battle = Substitute.For(); var eventHook = new EventHook(); battle.EventHook.Returns(eventHook); // Setup empty sides (no Damp on field) var side = Substitute.For(); - side.Pokemon.Returns(new List()); + side.Pokemon.Returns(new List()); battle.Sides.Returns(new[] { side }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - // First hit - non-contact var move1 = Substitute.For(); var hitData1 = Substitute.For(); @@ -244,10 +207,10 @@ public class AftermathTests var hitData2 = Substitute.For(); hitData2.IsContact.Returns(true); move2.GetHitData(target, 0).Returns(hitData2); - var user = Substitute.For(); + var user = Substitute.For(); user.IsUsable.Returns(true); - user.BattleData.Returns(battleData); user.MaxHealth.Returns(100u); + user.Battle.Returns(battle); move2.User.Returns(user); // Act @@ -268,7 +231,7 @@ public class AftermathTests { // Arrange var aftermath = new Aftermath(); - var target = Substitute.For(); + var target = Substitute.For(); // First hit - contact var move1 = Substitute.For(); @@ -346,14 +309,14 @@ public class AftermathTests var (aftermath, move, target, user, _, battle) = CreateFullTestSetup(true, 100); // Create a Pokemon with Damp ability on the field - var dampPokemon = Substitute.For(); + var dampPokemon = Substitute.For(); var dampAbility = Substitute.For(); dampAbility.Name.Returns(new StringKey("damp")); dampPokemon.ActiveAbility.Returns(dampAbility); // Update battle sides to include the Damp Pokemon var side = Substitute.For(); - side.Pokemon.Returns(new List { dampPokemon }); + side.Pokemon.Returns(new List { dampPokemon }); battle.Sides.Returns(new[] { side }); // Act diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs index cc7215d..1084e1e 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs @@ -16,9 +16,9 @@ public class MegaLauncherTests { // Arrange var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var healPercent = 0.5f; - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); move.UseMove.Category.Returns(MoveCategory.Special); move.UseMove.HasFlag(MoveFlags.Pulse).Returns(true); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs index 1575224..25822aa 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs @@ -22,7 +22,7 @@ public class FireGrassPledgeMoveTests /// Creates a fully mocked test setup. The target's battle side gets a real as /// its volatile script set, so the sea of fire created by the combined move can be inspected and driven. /// - private static (FireGrassPledgeMove script, IExecutingMove move, IPokemon target, IBattleSide side, IScriptSet + private static (FireGrassPledgeMove script, IExecutingMove move, IBattlePokemon target, IBattleSide side, IScriptSet sideVolatile) CreateTestSetup() { var script = new FireGrassPledgeMove(); @@ -33,11 +33,9 @@ public class FireGrassPledgeMoveTests var sideVolatile = new ScriptSet(side); side.VolatileScripts.Returns(sideVolatile); - var battleData = Substitute.For(); - battleData.BattleSide.Returns(side); - var target = Substitute.For(); - target.BattleData.Returns(battleData); + var target = Substitute.For(); + target.BattleSide.Returns(side); return (script, move, target, side, sideVolatile); } @@ -45,9 +43,9 @@ public class FireGrassPledgeMoveTests /// Creates a Pokémon substitute of the given type with the given maximum HP, for placing on the side /// covered by the sea of fire. /// - private static IPokemon CreateSidePokemon(string typeName, uint maxHealth) + private static IBattlePokemon CreateSidePokemon(string typeName, uint maxHealth) { - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.Types.Returns(new List { new(1, typeName) }); pokemon.MaxHealth.Returns(maxHealth); return pokemon; @@ -56,7 +54,7 @@ public class FireGrassPledgeMoveTests /// /// Helper to count the Damage calls a Pokémon received. /// - private static int CountDamageCalls(IPokemon pokemon) => + private static int CountDamageCalls(IBattlePokemon pokemon) => pokemon.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage"); /// @@ -112,24 +110,6 @@ public class FireGrassPledgeMoveTests await Assert.That(sideVolatile.Get()).IsNotNull(); } - /// - /// Technical test: if the target has no , no sea of fire is created - /// and the script does not throw. - /// - [Test] - public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNothing() - { - // Arrange - var (script, move, target, _, sideVolatile) = CreateTestSetup(); - target.BattleData.Returns((IPokemonBattleData?)null); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - await Assert.That(sideVolatile.Count).IsEqualTo(0); - } - /// /// Bulbapedia: "The sea of fire damages all non-Fire Pokémon on that side of the field for 1/8 of /// their maximum HP at the end of each turn." @@ -141,7 +121,7 @@ public class FireGrassPledgeMoveTests // Arrange var (script, move, target, side, sideVolatile) = CreateTestSetup(); var waterPokemon = CreateSidePokemon("water", maxHealth); - side.Pokemon.Returns(new List { waterPokemon }); + side.Pokemon.Returns(new List { waterPokemon }); script.OnSecondaryEffect(move, target, 0); var seaOfFire = sideVolatile.Get()!; @@ -164,7 +144,7 @@ public class FireGrassPledgeMoveTests // Arrange var (script, move, target, side, sideVolatile) = CreateTestSetup(); var firePokemon = CreateSidePokemon("fire", 100); - side.Pokemon.Returns(new List { firePokemon }); + side.Pokemon.Returns(new List { firePokemon }); script.OnSecondaryEffect(move, target, 0); var seaOfFire = sideVolatile.Get()!; @@ -185,7 +165,7 @@ public class FireGrassPledgeMoveTests // Arrange var (script, move, target, side, sideVolatile) = CreateTestSetup(); var waterPokemon = CreateSidePokemon("water", 96); - side.Pokemon.Returns(new List { waterPokemon }); + side.Pokemon.Returns(new List { waterPokemon }); script.OnSecondaryEffect(move, target, 0); var seaOfFire = sideVolatile.Get()!; var battle = Substitute.For(); @@ -207,7 +187,7 @@ public class FireGrassPledgeMoveTests { // Arrange var (script, move, target, side, sideVolatile) = CreateTestSetup(); - side.Pokemon.Returns(new List()); + side.Pokemon.Returns(new List()); script.OnSecondaryEffect(move, target, 0); var seaOfFire = sideVolatile.Get()!; var battle = Substitute.For(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs index 7c9606d..dbd46c0 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs @@ -20,7 +20,7 @@ public class FireWaterPledgeMoveTests /// Creates a fully mocked test setup. The user's battle side gets a real as its /// volatile script set, so the rainbow created by the combined move can be inspected and driven. /// - private static (FireWaterPledgeMove script, IExecutingMove move, IPokemon user, IBattleSide side, IScriptSet + private static (FireWaterPledgeMove script, IExecutingMove move, IBattlePokemon user, IBattleSide side, IScriptSet sideVolatile) CreateTestSetup() { var script = new FireWaterPledgeMove(); @@ -31,11 +31,9 @@ public class FireWaterPledgeMoveTests var sideVolatile = new ScriptSet(side); side.VolatileScripts.Returns(sideVolatile); - var battleData = Substitute.For(); - battleData.BattleSide.Returns(side); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); move.User.Returns(user); + user.BattleSide.Returns(side); return (script, move, user, side, sideVolatile); } @@ -67,7 +65,7 @@ public class FireWaterPledgeMoveTests { // Arrange var (script, move, _, _, _) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 80; // Act @@ -86,7 +84,7 @@ public class FireWaterPledgeMoveTests { // Arrange var (script, move, _, _, sideVolatile) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -95,25 +93,6 @@ public class FireWaterPledgeMoveTests await Assert.That(sideVolatile.Get()).IsNotNull(); } - /// - /// Technical test: if the user has no , no rainbow is created and the - /// script does not throw. - /// - [Test] - public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing() - { - // Arrange - var (script, move, user, _, sideVolatile) = CreateTestSetup(); - user.BattleData.Returns((IPokemonBattleData?)null); - var target = Substitute.For(); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - await Assert.That(sideVolatile.Count).IsEqualTo(0); - } - /// /// Bulbapedia: "The rainbow doubles the probability of additional effects taking place for moves used /// by that side of the field". @@ -123,7 +102,7 @@ public class FireWaterPledgeMoveTests { // Arrange var (script, move, _, _, sideVolatile) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); script.OnSecondaryEffect(move, target, 0); var rainbow = sideVolatile.Get()!; @@ -143,7 +122,7 @@ public class FireWaterPledgeMoveTests { // Arrange var (script, move, _, side, sideVolatile) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); script.OnSecondaryEffect(move, target, 0); var rainbow = sideVolatile.Get()!; var battle = Substitute.For(); @@ -165,7 +144,7 @@ public class FireWaterPledgeMoveTests { // Arrange var (script, move, _, side, sideVolatile) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); script.OnSecondaryEffect(move, target, 0); var rainbow = sideVolatile.Get()!; var battle = Substitute.For(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs index 001ca84..5c83413 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs @@ -11,9 +11,9 @@ public class AcrobaticsTests { // Arrange var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 10; - var user = Substitute.For(); + var user = Substitute.For(); user.HeldItem.Returns((IItem?)null); move.User.Returns(user); var acrobatics = new Acrobatics(); @@ -30,9 +30,9 @@ public class AcrobaticsTests { // Arrange var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 10; - var user = Substitute.For(); + var user = Substitute.For(); user.HeldItem.Returns(Substitute.For()); move.User.Returns(user); var acrobatics = new Acrobatics(); @@ -49,9 +49,9 @@ public class AcrobaticsTests { // Arrange var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = ushort.MaxValue - 100; - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); user.HeldItem.Returns((IItem?)null); var acrobatics = new Acrobatics(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs index 6073220..86beefb 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs @@ -13,24 +13,22 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class AcupressureTests { - private static (Acupressure script, IExecutingMove move, IPokemon target, IBattleRandom random, IHitData hitData) - CreateTestSetup() + private static (Acupressure script, IExecutingMove move, IBattlePokemon target, IBattleRandom random, IHitData + hitData) CreateTestSetup() { var script = new Acupressure(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); var random = Substitute.For(); var battle = Substitute.For(); battle.Random.Returns(random); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); move.User.Returns(user); + user.Battle.Returns(battle); target.StatBoost.Returns(new StatBoostStatisticSet()); @@ -40,7 +38,7 @@ public class AcupressureTests /// /// Helper to extract the stat argument of a ChangeStatBoost call received by the target. /// - private static object?[]? GetStatBoostCallArgs(IPokemon target) + private static object?[]? GetStatBoostCallArgs(IBattlePokemon target) { var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); return call?.GetArguments(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs index 755ea69..4d84fe4 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs @@ -12,7 +12,7 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class AfterYouTests { - private static IMoveChoice CreateChoice(IPokemon user, uint speed) + private static IMoveChoice CreateChoice(IBattlePokemon user, uint speed) { var choice = Substitute.For(); choice.User.Returns(user); @@ -25,16 +25,14 @@ public class AfterYouTests var script = new AfterYou(); var move = Substitute.For(); var hitData = Substitute.For(); - move.GetHitData(Arg.Any(), Arg.Any()).Returns(hitData); + move.GetHitData(Arg.Any(), Arg.Any()).Returns(hitData); var battle = Substitute.For(); battle.ChoiceQueue.Returns(queue); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); move.User.Returns(user); + user.Battle.Returns(battle); return (script, move, hitData); } @@ -47,9 +45,9 @@ public class AfterYouTests public async Task OnSecondaryEffect_TargetLaterInQueue_TargetMovesNext() { // Arrange - var user = Substitute.For(); - var target = Substitute.For(); - var other = Substitute.For(); + var user = Substitute.For(); + var target = Substitute.For(); + var other = Substitute.For(); // Sorted by speed: user (100), other (75), target (50) var queue = new BattleChoiceQueue([ CreateChoice(user, 100), CreateChoice(other, 75), CreateChoice(target, 50), @@ -75,8 +73,8 @@ public class AfterYouTests public void OnSecondaryEffect_TargetAlreadyMoved_Fails() { // Arrange - var user = Substitute.For(); - var target = Substitute.For(); + var user = Substitute.For(); + var target = Substitute.For(); // Sorted by speed: target (100), user (50) var queue = new BattleChoiceQueue([ CreateChoice(target, 100), CreateChoice(user, 50), @@ -103,8 +101,8 @@ public class AfterYouTests public void OnSecondaryEffect_TargetAlreadyNext_Fails() { // Arrange - var user = Substitute.For(); - var target = Substitute.For(); + var user = Substitute.For(); + var target = Substitute.For(); // Sorted by speed: user (100), target (50) var queue = new BattleChoiceQueue([ CreateChoice(user, 100), CreateChoice(target, 50), @@ -132,7 +130,7 @@ public class AfterYouTests var (script, move, hitData) = CreateTestSetup(null); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert hitData.DidNotReceive().Fail(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs index 10bcbf1..366eaaa 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs @@ -15,9 +15,9 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class AssistTests { - private static IPokemon CreatePokemonWithMoves(params string[] moveNames) + private static IBattlePokemon CreatePokemonWithMoves(params string[] moveNames) { - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); var moves = moveNames.Select(name => { var learned = Substitute.For(); @@ -30,27 +30,23 @@ public class AssistTests return pokemon; } - private static (Assist script, IMoveChoice choice, IPokemon user, IBattleRandom random) CreateTestSetup( - params IPokemon?[] otherPartyMembers) + private static (Assist script, IMoveChoice choice, IBattlePokemon user, IBattleRandom random) CreateTestSetup( + params IBattlePokemon?[] otherPartyMembers) { var script = new Assist(); var user = CreatePokemonWithMoves("tackle"); - var members = new List { user }; + var members = new List { user }; members.AddRange(otherPartyMembers); - var party = Substitute.For(); - party.GetEnumerator().Returns(_ => members.GetEnumerator()); var battleParty = Substitute.For(); - battleParty.Party.Returns(party); + battleParty.BattlePokemon.Returns(members); var random = Substitute.For(); var battle = Substitute.For(); battle.Parties.Returns(new[] { battleParty }); battle.Random.Returns(random); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - user.BattleData.Returns(battleData); + user.Battle.Returns(battle); var choice = Substitute.For(); choice.User.Returns(user); @@ -180,26 +176,4 @@ public class AssistTests await Assert.That(moveName).IsEqualTo(new StringKey("growl")); choice.DidNotReceive().Fail(); } - - /// - /// Technical test: outside of battle (no battle data) the script returns without failing the choice. - /// - [Test] - public async Task ChangeMove_NoBattleData_DoesNothing() - { - // Arrange - var script = new Assist(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - var choice = Substitute.For(); - choice.User.Returns(user); - StringKey moveName = "assist"; - - // Act - script.ChangeMove(choice, ref moveName); - - // Assert - choice.DidNotReceive().Fail(); - await Assert.That(moveName).IsEqualTo(new StringKey("assist")); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs index 36627ed..bd76a58 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs @@ -15,12 +15,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class AttractTests { - private static (Attract script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData) - CreateTestSetup(Gender userGender, Gender targetGender) + private static (Attract script, IExecutingMove move, IBattlePokemon target, IScriptSet targetVolatile, IHitData + hitData) CreateTestSetup(Gender userGender, Gender targetGender) { var script = new Attract(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -28,7 +28,7 @@ public class AttractTests target.Volatile.Returns(targetVolatile); target.Gender.Returns(targetGender); - var user = Substitute.For(); + var user = Substitute.For(); user.Gender.Returns(userGender); move.User.Returns(user); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs index 9f0b8ce..e5707a2 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs @@ -31,12 +31,12 @@ public class AuroraVeilTests } } - private static (AuroraVeil script, IExecutingMove move, IPokemon target, IPokemon user, IScriptSet sideScripts, - IHitData hitData) CreateTestSetup(StringKey? weather) + private static (AuroraVeil script, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, IScriptSet + sideScripts, IHitData hitData) CreateTestSetup(StringKey? weather) { var script = new AuroraVeil(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -51,13 +51,10 @@ public class AuroraVeilTests side.VolatileScripts.Returns(sideScripts); battle.Sides.Returns(new[] { side }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - battleData.SideIndex.Returns((byte)0); - - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); + user.SideIndex.Returns((byte)0); + user.Battle.Returns(battle); move.User.Returns(user); return (script, move, target, user, sideScripts, hitData); @@ -154,25 +151,4 @@ public class AuroraVeilTests var effect = (AuroraVeilEffect)((Func)call.GetArguments()[1]!)()!; await Assert.That(effect.NumberOfTurns).IsEqualTo(8); } - - /// - /// Technical test: outside of battle (no battle data) the script returns without throwing. - /// - [Test] - public async Task OnSecondaryEffect_NoBattleData_DoesNothing() - { - // Arrange - var script = new AuroraVeil(); - var move = Substitute.For(); - var target = Substitute.For(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - move.User.Returns(user); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - no hit data was touched - await Assert.That(move.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "GetHitData")).IsFalse(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AutotomizeTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AutotomizeTests.cs index fac5aa9..0b0d0f5 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AutotomizeTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AutotomizeTests.cs @@ -17,8 +17,8 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class AutotomizeTests { - private static (Autotomize script, IExecutingMove move, IPokemon user, IScriptSet userVolatile, EventHook eventHook) - CreateTestSetup(float weightInKg, bool speedRaiseSucceeds, AutotomizeEffect? existingEffect = null) + private static (Autotomize script, IExecutingMove move, IBattlePokemon user, IScriptSet userVolatile, EventHook + eventHook) CreateTestSetup(float weightInKg, bool speedRaiseSucceeds, AutotomizeEffect? existingEffect = null) { var script = new Autotomize(); var move = Substitute.For(); @@ -26,15 +26,13 @@ public class AutotomizeTests var eventHook = new EventHook(); var battle = Substitute.For(); battle.EventHook.Returns(eventHook); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); var userVolatile = Substitute.For(); userVolatile.Get().Returns(existingEffect); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); user.Volatile.Returns(userVolatile); + user.Battle.Returns(battle); user.WeightInKg.Returns(weightInKg); user.ChangeStatBoost(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(speedRaiseSucceeds); @@ -56,7 +54,7 @@ public class AutotomizeTests var (script, move, user, _, _) = CreateTestSetup(100f, true); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false); @@ -79,7 +77,7 @@ public class AutotomizeTests }; // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue(); @@ -100,7 +98,7 @@ public class AutotomizeTests eventHook.Handler += (_, _) => eventFired = true; // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(ReceivedStackOrAdd(userVolatile)).IsFalse(); @@ -119,7 +117,7 @@ public class AutotomizeTests var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue(); @@ -140,7 +138,7 @@ public class AutotomizeTests var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true, existingEffect); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert - the weight should still be reduced (to the minimum) await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue(); @@ -160,7 +158,7 @@ public class AutotomizeTests eventHook.Handler += (_, _) => eventFired = true; // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BanefulBunkerTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BanefulBunkerTests.cs index ae96e06..50a8087 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BanefulBunkerTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BanefulBunkerTests.cs @@ -19,12 +19,12 @@ public class BanefulBunkerTests /// . The target of the secondary effect is the /// Pokémon using Baneful Bunker itself, as the move is self-targeted. /// - private static (BanefulBunker script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet - ) CreateProtectSetup(bool userMovesLast, float randomRoll) + private static (BanefulBunker script, IExecutingMove move, IBattlePokemon target, IHitData hitData, IScriptSet + volatileSet ) CreateProtectSetup(bool userMovesLast, float randomRoll) { var script = new BanefulBunker(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -38,9 +38,7 @@ public class BanefulBunkerTests battle.ChoiceQueue.Returns(queue); battle.Random.Returns(random); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - target.BattleData.Returns(battleData); + target.Battle.Returns(battle); // Give the mock a real script iterator (used by the volatile add hook) and a real volatile script set. target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); @@ -54,12 +52,12 @@ public class BanefulBunkerTests /// Creates a fully mocked setup for driving , the /// volatile script that attaches to its user. /// - private static (BanefulBunkerEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker) + private static (BanefulBunkerEffect effect, IExecutingMove move, IBattlePokemon target, IBattlePokemon attacker) CreateBlockSetup(bool isContact, bool hasProtectFlag, MoveCategory category = MoveCategory.Physical) { var effect = new BanefulBunkerEffect(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(isContact); move.GetHitData(target, 0).Returns(hitData); @@ -69,19 +67,17 @@ public class BanefulBunkerTests useMove.Category.Returns(category); move.UseMove.Returns(useMove); - var attacker = Substitute.For(); + var attacker = Substitute.For(); attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); move.User.Returns(attacker); - target.BattleData.Returns(Substitute.For()); - return (effect, move, target, attacker); } /// - /// Helper to extract the status name from a Pokémon's received calls. + /// Helper to extract the status name from a Pokémon's received calls. /// - private static string? GetStatusSet(IPokemon pokemon) + private static string? GetStatusSet(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus"); return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BatonPassTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BatonPassTests.cs index 960f557..0b166c1 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BatonPassTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BatonPassTests.cs @@ -21,9 +21,9 @@ public class BatonPassTests /// Creates a mocked Pokémon with a real volatile and a real /// . /// - private static IPokemon CreateMockPokemon(out IScriptSet volatileSet) + private static IBattlePokemon CreateMockPokemon(out IScriptSet volatileSet) { - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); var set = new ScriptSet(pokemon); pokemon.Volatile.Returns(set); @@ -36,8 +36,8 @@ public class BatonPassTests /// Creates a fully mocked test setup for Baton Pass tests. The Pokémon to switch in is stored in the /// move choice's under the to_switch key. /// - private static (BatonPass script, IExecutingMove move, IPokemon user, IPokemon toSwitch, IBattleSide side, - IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup() + private static (BatonPass script, IExecutingMove move, IBattlePokemon user, IBattlePokemon toSwitch, IBattleSide + side, IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup() { var script = new BatonPass(); var move = Substitute.For(); @@ -52,11 +52,9 @@ public class BatonPassTests var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - battleData.SideIndex.Returns((byte)0); - battleData.Position.Returns((byte)1); - user.BattleData.Returns(battleData); + user.Battle.Returns(battle); + user.SideIndex.Returns((byte)0); + user.Position.Returns((byte)1); move.User.Returns(user); return (script, move, user, toSwitch, side, userVolatile, switchInVolatile); @@ -213,7 +211,7 @@ public class BatonPassTests script.OnSecondaryEffect(move, user, 0); // Assert - side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); + side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); } /// @@ -230,26 +228,6 @@ public class BatonPassTests script.OnSecondaryEffect(move, user, 0); // Assert - side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); - } - - /// - /// Technical test: if the user has no , the script returns without - /// switching and without clearing the user's volatile scripts. - /// - [Test] - public async Task OnSecondaryEffect_NoBattleData_DoesNotSwitch() - { - // Arrange - var (script, move, user, _, side, userVolatile, _) = CreateTestSetup(); - user.BattleData.Returns((IPokemonBattleData?)null); - userVolatile.Add(new AutotomizeEffect()); - - // Act - script.OnSecondaryEffect(move, user, 0); - - // Assert - side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); - await Assert.That(userVolatile.Get()).IsNotNull(); + side.DidNotReceive().SwapPokemon(Arg.Any(), Arg.Any()); } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeakBlastTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeakBlastTests.cs index a0c70cb..4d304de 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeakBlastTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeakBlastTests.cs @@ -17,28 +17,19 @@ public class BeakBlastTests /// /// Creates a fully mocked setup for driving . /// - private static (BeakBlast script, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook eventHook) - CreateChargeSetup(bool hasBattleData = true) + private static (BeakBlast script, ITurnChoice choice, IBattlePokemon user, IScriptSet volatileSet, EventHook + eventHook) CreateChargeSetup() { var script = new BeakBlast(); - var user = Substitute.For(); + var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(user); user.Volatile.Returns(volatileSet); var eventHook = new EventHook(); - if (hasBattleData) - { - var battle = Substitute.For(); - battle.EventHook.Returns(eventHook); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - user.BattleData.Returns(battleData); - } - else - { - user.BattleData.Returns((IPokemonBattleData?)null); - } + var battle = Substitute.For(); + battle.EventHook.Returns(eventHook); + user.Battle.Returns(battle); var choice = Substitute.For(); choice.User.Returns(user); @@ -85,24 +76,7 @@ public class BeakBlastTests // Assert await Assert.That(capturedEvent).IsNotNull(); await Assert.That(capturedEvent!.Message).IsEqualTo("beak_blast_charge"); - await Assert.That((IPokemon)capturedEvent.Parameters!["user"]).IsEqualTo(user); - } - - /// - /// Technical test: a Pokémon without is not in battle, so no charging - /// phase starts. - /// - [Test] - public async Task OnBeforeTurnStart_NoBattleData_DoesNotAddChargeEffect() - { - // Arrange - var (script, choice, _, volatileSet, _) = CreateChargeSetup(false); - - // Act - script.OnBeforeTurnStart(choice); - - // Assert - await Assert.That(volatileSet.Get()).IsNull(); + await Assert.That((IBattlePokemon)capturedEvent.Parameters!["user"]).IsEqualTo(user); } /// @@ -115,8 +89,8 @@ public class BeakBlastTests // Arrange var script = new BeakBlast(); var move = Substitute.For(); - var target = Substitute.For(); - var user = Substitute.For(); + var target = Substitute.For(); + var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(user); user.Volatile.Returns(volatileSet); @@ -133,26 +107,26 @@ public class BeakBlastTests /// /// Creates a fully mocked setup for driving . /// - private static (BeakBlastEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker) + private static (BeakBlastEffect effect, IExecutingMove move, IBattlePokemon target, IBattlePokemon attacker) CreateIncomingHitSetup(bool isContact) { var effect = new BeakBlastEffect(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); hitData.IsContact.Returns(isContact); move.GetHitData(target, 0).Returns(hitData); - var attacker = Substitute.For(); + var attacker = Substitute.For(); move.User.Returns(attacker); return (effect, move, target, attacker); } /// - /// Helper to extract the status name from a Pokémon's received calls. + /// Helper to extract the status name from a Pokémon's received calls. /// - private static string? GetStatusSet(IPokemon pokemon) + private static string? GetStatusSet(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus"); return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeatUpTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeatUpTests.cs index dfe28be..6c3199a 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeatUpTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BeatUpTests.cs @@ -19,11 +19,11 @@ public class BeatUpTests { /// /// Creates a mocked party member with the given base Attack stat, usability, and optional non-volatile - /// status script in its . + /// status script in its . /// - private static IPokemon CreatePartyMember(ushort baseAttack = 100, bool usable = true, Script? status = null) + private static IBattlePokemon CreatePartyMember(ushort baseAttack = 100, bool usable = true, Script? status = null) { - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.IsUsable.Returns(usable); pokemon.StatusScript.Returns(status == null ? new ScriptContainer() : new ScriptContainer(status)); var form = Substitute.For(); @@ -35,24 +35,20 @@ public class BeatUpTests /// /// Creates a fully mocked test setup where the user and the given other Pokémon form a party in a battle. /// - private static (BeatUp script, IMoveChoice choice, IExecutingMove move) CreateTestSetup(IPokemon user, - params IPokemon?[] otherPartyMembers) + private static (BeatUp script, IMoveChoice choice, IExecutingMove move) CreateTestSetup(IBattlePokemon user, + params IBattlePokemon?[] otherPartyMembers) { var script = new BeatUp(); - var members = new List { user }; + var members = new List { user }; members.AddRange(otherPartyMembers); - var party = Substitute.For(); - party.GetEnumerator().Returns(_ => members.GetEnumerator()); var battleParty = Substitute.For(); - battleParty.Party.Returns(party); + battleParty.BattlePokemon.Returns(members); var battle = Substitute.For(); battle.Parties.Returns(new[] { battleParty }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - user.BattleData.Returns(battleData); + user.Battle.Returns(battle); var choice = Substitute.For(); choice.User.Returns(user); @@ -121,28 +117,6 @@ public class BeatUpTests await Assert.That(numberOfHits).IsEqualTo((byte)1); } - /// - /// Technical test: outside of battle (no ) there are no relevant party - /// members, and the number of hits falls back to a single strike. - /// - [Test] - public async Task ChangeNumberOfHits_NoBattleData_SingleHit() - { - // Arrange - var script = new BeatUp(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - var choice = Substitute.For(); - choice.User.Returns(user); - byte numberOfHits = 3; - - // Act - script.ChangeNumberOfHits(choice, ref numberOfHits); - - // Assert - await Assert.That(numberOfHits).IsEqualTo((byte)1); - } - /// /// Bulbapedia: "the base power per strike is no longer 10, but instead individually based on the Attack /// base stats of the party Pokémon: BasePower = BaseAttack(PartyMember)/10 + 5". @@ -155,7 +129,7 @@ public class BeatUpTests // Arrange var user = CreatePartyMember(baseAttack); var (script, _, move) = CreateTestSetup(user); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 10; // Act @@ -175,7 +149,7 @@ public class BeatUpTests // Arrange var user = CreatePartyMember(100); var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250)); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 10; // Act @@ -197,7 +171,7 @@ public class BeatUpTests var user = CreatePartyMember(100); var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250, status: new Burned()), CreatePartyMember(60)); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 10; // Act @@ -217,7 +191,7 @@ public class BeatUpTests // Arrange var user = CreatePartyMember(100); var (script, _, move) = CreateTestSetup(user); - var target = Substitute.For(); + var target = Substitute.For(); ushort basePower = 10; // Act diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BelchTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BelchTests.cs index 564ab0f..550bf94 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BelchTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BelchTests.cs @@ -13,13 +13,13 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; public class BelchTests { /// - /// Creates a fully mocked test setup where the user's + /// Creates a fully mocked test setup where the user's /// contains one item per given . /// private static (Belch script, IMoveChoice choice) CreateTestSetup(params ItemCategory[] consumedItemCategories) { var script = new Belch(); - var user = Substitute.For(); + var user = Substitute.For(); var items = consumedItemCategories.Select(category => { @@ -27,9 +27,7 @@ public class BelchTests item.Category.Returns(category); return item; }).ToArray(); - var battleData = Substitute.For(); - battleData.ConsumedItems.Returns(items); - user.BattleData.Returns(battleData); + user.ConsumedItems.Returns(items); var choice = Substitute.For(); choice.User.Returns(user); @@ -108,26 +106,4 @@ public class BelchTests // Assert await Assert.That(prevent).IsFalse(); } - - /// - /// Technical test: outside of battle (no ) the script does not prevent - /// selection. - /// - [Test] - public async Task PreventMoveSelection_NoBattleData_SelectionAllowed() - { - // Arrange - var script = new Belch(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - var choice = Substitute.For(); - choice.User.Returns(user); - var prevent = false; - - // Act - script.PreventMoveSelection(choice, ref prevent); - - // Assert - await Assert.That(prevent).IsFalse(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BellyDrumTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BellyDrumTests.cs index fe07173..243f57d 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BellyDrumTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BellyDrumTests.cs @@ -14,11 +14,11 @@ public class BellyDrumTests /// /// Creates a fully mocked test setup for Belly Drum. The target of the secondary effect is the user itself. /// - private static (BellyDrum script, IExecutingMove move, IPokemon user, IHitData hitData) CreateTestSetup(uint maxHp, - uint currentHp, sbyte attackBoost = 0) + private static (BellyDrum script, IExecutingMove move, IBattlePokemon user, IHitData hitData) CreateTestSetup( + uint maxHp, uint currentHp, sbyte attackBoost = 0) { var script = new BellyDrum(); - var user = Substitute.For(); + var user = Substitute.For(); user.BoostedStats.Returns(new StatisticSet(maxHp, 10, 10, 10, 10, 10)); user.CurrentHealth.Returns(currentHp); user.StatBoost.Returns(new StatBoostStatisticSet(0, attackBoost, 0, 0, 0, 0)); @@ -34,7 +34,7 @@ public class BellyDrumTests /// /// Helper to extract the received Damage call from the user, if any. /// - private static (uint damage, DamageSource source, bool forceDamage)? GetDamageCall(IPokemon user) + private static (uint damage, DamageSource source, bool forceDamage)? GetDamageCall(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); if (call == null) @@ -46,7 +46,7 @@ public class BellyDrumTests /// /// Helper to extract the received ChangeStatBoost call from the user, if any. /// - private static (Statistic stat, sbyte change, bool selfInflicted)? GetStatBoostCall(IPokemon user) + private static (Statistic stat, sbyte change, bool selfInflicted)? GetStatBoostCall(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); if (call == null) diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BestowTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BestowTests.cs index 569987d..4c71b7b 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BestowTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BestowTests.cs @@ -12,17 +12,17 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class BestowTests { - private static (Bestow script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData) + private static (Bestow script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData hitData) CreateTestSetup(IItem? userItem, IItem? targetItem) { var script = new Bestow(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); target.HeldItem.Returns(targetItem); - var user = Substitute.For(); + var user = Substitute.For(); user.HeldItem.Returns(userItem); user.RemoveHeldItemForBattle().Returns(userItem); move.User.Returns(user); @@ -68,7 +68,7 @@ public class BestowTests /// /// Bulbapedia: "Items given away in Trainer battles return to the original Pokémon after the battle." - /// The item is taken from the user through , which only + /// The item is taken from the user through , which only /// removes the item for the duration of the battle. /// [Test] diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs index 5c932ec..ceaa8f0 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BideTests.cs @@ -13,16 +13,16 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class BideTests { - private static (Bide script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet userVolatile, IHitData - hitData) CreateTestSetup() + private static (Bide script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IScriptSet + userVolatile, IHitData hitData) CreateTestSetup() { var script = new Bide(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); - var user = Substitute.For(); + var user = Substitute.For(); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); // A real script set so the volatile Bide effect can actually be added, retrieved and removed. var userVolatile = new ScriptSet(user); @@ -32,12 +32,10 @@ public class BideTests return (script, move, user, target, userVolatile, hitData); } - private static IPokemon CreateAttacker(bool onBattlefield = true) + private static IBattlePokemon CreateAttacker(bool onBattlefield = true) { - var attacker = Substitute.For(); - var battleData = Substitute.For(); - battleData.IsOnBattlefield.Returns(onBattlefield); - attacker.BattleData.Returns(battleData); + var attacker = Substitute.For(); + attacker.IsOnBattlefield.Returns(onBattlefield); return attacker; } @@ -45,8 +43,8 @@ public class BideTests /// Adds a to the user's volatile scripts, as if Bide has already been storing /// energy for the given number of executed turns. /// - private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IPokemon user, byte turns, uint damageTaken, - params IPokemon[] hitBy) + private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IBattlePokemon user, byte turns, + uint damageTaken, params IBattlePokemon[] hitBy) { var effect = new BideEffect(user) { @@ -61,13 +59,13 @@ public class BideTests /// /// Helper to check whether a Pokémon received any Damage call. /// - private static bool ReceivedDamage(IPokemon pokemon) => + private static bool ReceivedDamage(IBattlePokemon pokemon) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage"); /// /// Helper to extract the damage amount from a Pokémon's received Damage calls. /// - private static uint? GetDamageAmount(IPokemon pokemon) + private static uint? GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -76,7 +74,7 @@ public class BideTests /// /// Helper to extract the damage source from a Pokémon's received Damage calls. /// - private static DamageSource? GetDamageSource(IPokemon pokemon) + private static DamageSource? GetDamageSource(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (DamageSource)call.GetArguments()[1]! : null; @@ -245,7 +243,7 @@ public class BideTests public async Task BideEffect_OnDamage_AccumulatesDamageTaken() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var effect = new BideEffect(user); // Act - the user drops from 100 to 60 HP, then from 60 to 50 HP @@ -265,10 +263,10 @@ public class BideTests public async Task BideEffect_OnIncomingHit_RecordsAttacker() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var effect = new BideEffect(user); var incomingMove = Substitute.For(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); incomingMove.User.Returns(attacker); // Act diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs index 927a185..d129f48 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BindTests.cs @@ -44,16 +44,16 @@ public class BindTests } } - private static (Bind script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile) - CreateTestSetup(params Script[] userScripts) + private static (Bind script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IScriptSet + targetVolatile) CreateTestSetup(params Script[] userScripts) { var script = new Bind(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var targetVolatile = Substitute.For(); target.Volatile.Returns(targetVolatile); - var user = Substitute.For(); + var user = Substitute.For(); // RunScriptHook iterates the user's scripts; give the mock a real iterator so the ModifyBind trigger // pass runs (empty unless the test attaches scripts such as the Grip Claw / Binding Band stand-in). var containers = userScripts.Select(IEnumerable (s) => new ScriptContainer(s)).ToArray(); @@ -73,7 +73,7 @@ public class BindTests /// /// Helper to extract the damage amount from the target's first received Damage call. /// - private static uint? GetDamageAmount(IPokemon pokemon) + private static uint? GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -82,7 +82,7 @@ public class BindTests /// /// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target. /// - private static int CountEndTurnDamageTicks(BindEffect effect, IPokemon target, int maxTurns = 10) + private static int CountEndTurnDamageTicks(BindEffect effect, IBattlePokemon target, int maxTurns = 10) { var battle = Substitute.For(); for (var i = 0; i < maxTurns; i++) @@ -204,7 +204,7 @@ public class BindTests public async Task BindEffect_WhileActive_PreventsSwitching() { // Arrange - var target = Substitute.For(); + var target = Substitute.For(); var effect = new BindEffect(target, 5, 1f / 8f); var prevent = false; @@ -223,7 +223,7 @@ public class BindTests public async Task BindEffect_WhileActive_PreventsRunningAway() { // Arrange - var target = Substitute.For(); + var target = Substitute.For(); var effect = new BindEffect(target, 5, 1f / 8f); var prevent = false; @@ -242,7 +242,7 @@ public class BindTests public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching() { // Arrange - var target = Substitute.For(); + var target = Substitute.For(); target.MaxHealth.Returns(160u); var effect = new BindEffect(target, 1, 1f / 8f); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BlockTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BlockTests.cs index 6e466f7..e567211 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BlockTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BlockTests.cs @@ -14,14 +14,15 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class BlockTests { - private static (Block script, IExecutingMove move, IPokemon target, ScriptSet targetVolatile) CreateTestSetup() + private static (Block script, IExecutingMove move, IBattlePokemon target, ScriptSet targetVolatile) + CreateTestSetup() { var script = new Block(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Use a real script set so the volatile script added by Block can be inspected afterwards. var targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); @@ -105,10 +106,8 @@ public class BlockTests var battle = Substitute.For(); battle.Library.Returns(library); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - move.User.BattleData.Returns(battleData); - target.BattleData.Returns(battleData); + move.User.Battle.Returns(battle); + target.Battle.Returns(battle); // Act script.OnSecondaryEffect(move, target, 0); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BounceTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BounceTests.cs index 096d675..01bb536 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BounceTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BounceTests.cs @@ -20,12 +20,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class BounceTests { - private static (Bounce script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice, - IBattleRandom random) CreateTestSetup() + private static (Bounce script, IExecutingMove move, IBattlePokemon user, ScriptSet userVolatile, IMoveChoice + moveChoice, IBattleRandom random) CreateTestSetup() { var script = new Bounce(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); // Use a real script set so the charge volatile added by Bounce can be inspected afterwards. var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); @@ -39,9 +39,7 @@ public class BounceTests var random = Substitute.For(); battle.Random.Returns(random); battle.EventHook.Returns(new EventHook()); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - user.BattleData.Returns(battleData); + user.Battle.Returns(battle); return (script, move, user, userVolatile, moveChoice, random); } @@ -148,7 +146,7 @@ public class BounceTests { // Arrange var (script, move, user, _, _, random) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); random.EffectChance(30, move, target, 0).Returns(true); // Act @@ -168,7 +166,7 @@ public class BounceTests { // Arrange var (script, move, _, _, _, random) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); random.EffectChance(30, move, target, 0).Returns(false); // Act @@ -187,7 +185,7 @@ public class BounceTests { // Arrange var (script, move, _, _, _, random) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -197,24 +195,6 @@ public class BounceTests await Assert.That(random.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "EffectChance")).IsTrue(); } - /// - /// Technical test: outside of battle (no battle data) the secondary effect does nothing and does not throw. - /// - [Test] - public async Task OnSecondaryEffect_NoBattleData_DoesNotParalyzeTarget() - { - // Arrange - var (script, move, user, _, _, _) = CreateTestSetup(); - user.BattleData.Returns((IPokemonBattleData?)null); - var target = Substitute.For(); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse(); - } - /// /// Bulbapedia: "While in Sky-High status, the user is invulnerable to most moves". /// The added by the charge turn blocks incoming hits from moves that diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrickBreakTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrickBreakTests.cs index 2536d26..c2fa3a1 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrickBreakTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrickBreakTests.cs @@ -15,21 +15,19 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; public class BrickBreakTests { /// - /// Creates a mocked Pokémon whose is the given side. + /// Creates a mocked Pokémon whose is the given side. /// - private static IPokemon CreatePokemonOnSide(IBattleSide side) + private static IBattlePokemon CreatePokemonOnSide(IBattleSide side) { - var pokemon = Substitute.For(); - var battleData = Substitute.For(); - battleData.BattleSide.Returns(side); - pokemon.BattleData.Returns(battleData); + var pokemon = Substitute.For(); + pokemon.BattleSide.Returns(side); return pokemon; } /// /// Creates a fully mocked test setup where the user targets a Pokémon on the opposing side. /// - private static (BrickBreak script, IExecutingMove move, IPokemon user, IScriptSet userSideScripts, IScriptSet + private static (BrickBreak script, IExecutingMove move, IBattlePokemon user, IScriptSet userSideScripts, IScriptSet targetSideScripts) CreateTestSetup() { var script = new BrickBreak(); @@ -45,7 +43,7 @@ public class BrickBreakTests var user = CreatePokemonOnSide(userSide); move.User.Returns(user); var target = CreatePokemonOnSide(targetSide); - move.Targets.Returns(new IPokemon?[] { target }); + move.Targets.Returns(new IBattlePokemon?[] { target }); return (script, move, user, userSideScripts, targetSideScripts); } @@ -115,8 +113,8 @@ public class BrickBreakTests { // Arrange var (script, move, user, userSideScripts, _) = CreateTestSetup(); - var ally = CreatePokemonOnSide(user.BattleData!.BattleSide); - move.Targets.Returns(new IPokemon?[] { ally }); + var ally = CreatePokemonOnSide(user.BattleSide); + move.Targets.Returns(new IBattlePokemon?[] { ally }); // Act script.OnBeforeMove(move); @@ -146,7 +144,7 @@ public class BrickBreakTests var user = CreatePokemonOnSide(userSide); move.User.Returns(user); var target = CreatePokemonOnSide(targetSide); - move.Targets.Returns(new IPokemon?[] { target }); + move.Targets.Returns(new IBattlePokemon?[] { target }); // Act script.OnBeforeMove(move); @@ -165,10 +163,9 @@ public class BrickBreakTests // Arrange var script = new BrickBreak(); var move = Substitute.For(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); + var user = Substitute.For(); move.User.Returns(user); - move.Targets.Returns(Array.Empty()); + move.Targets.Returns(Array.Empty()); // Act & Assert await Assert.That(() => script.OnBeforeMove(move)).ThrowsNothing(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrineTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrineTests.cs index ec573dc..488e0a5 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrineTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BrineTests.cs @@ -12,13 +12,13 @@ public class BrineTests { /// /// Creates a fully mocked test setup for Brine tests, with a target whose max HP - /// () and are configured. + /// () and are configured. /// - private static (Brine brine, IExecutingMove move, IPokemon target) CreateTestSetup(uint maxHp, uint currentHp) + private static (Brine brine, IExecutingMove move, IBattlePokemon target) CreateTestSetup(uint maxHp, uint currentHp) { var brine = new Brine(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); target.BoostedStats.Returns(new StatisticSet(maxHp, 0, 0, 0, 0, 0)); target.CurrentHealth.Returns(currentHp); return (brine, move, target); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BugBiteTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BugBiteTests.cs index c5d4583..1975370 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BugBiteTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BugBiteTests.cs @@ -41,8 +41,8 @@ public class BugBiteTests /// real resolver with a single registered item script constructor for , so /// eating a Berry runs a that the test can inspect. /// - private static (BugBite bugBite, IExecutingMove move, IPokemon target, IHitData hitData, List - createdItemScripts) CreateTestSetup(IItem? targetHeldItem, bool canSteal = true) + private static (BugBite bugBite, IExecutingMove move, IBattlePokemon target, IHitData hitData, + List createdItemScripts) CreateTestSetup(IItem? targetHeldItem, bool canSteal = true) { var bugBite = new BugBite(); @@ -66,17 +66,14 @@ public class BugBiteTests dynamicLibrary.ScriptResolver.Returns(resolver); battle.Library.Returns(dynamicLibrary); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); + user.Battle.Returns(battle); var move = Substitute.For(); move.User.Returns(user); move.Battle.Returns(battle); - var target = Substitute.For(); + var target = Substitute.For(); target.HeldItem.Returns(targetHeldItem); if (targetHeldItem != null && canSteal) { @@ -111,7 +108,7 @@ public class BugBiteTests /// /// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect." - /// Eating the Berry removes it from the target via . + /// Eating the Berry removes it from the target via . /// [Test] public async Task OnSecondaryEffect_TargetHoldsBerry_BerryIsRemovedFromTarget() @@ -202,7 +199,7 @@ public class BugBiteTests /// /// Bulbapedia: "Bug Bite will not consume the Berry of a target that has the Ability Sticky Hold." - /// When the Berry cannot be stolen ( returns false, as with + /// When the Berry cannot be stolen ( returns false, as with /// Sticky Hold), the target keeps its Berry and the effect fails. /// [Test] @@ -219,25 +216,4 @@ public class BugBiteTests await Assert.That(createdItemScripts.Count).IsEqualTo(0); await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue(); } - - /// - /// If the user has no , the script does nothing: no Berry is eaten - /// and the hit is not failed. - /// - [Test] - public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing() - { - // Arrange - var (bugBite, move, target, hitData, _) = CreateTestSetup(CreateBerry()); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - move.User.Returns(user); - - // Act - bugBite.OnSecondaryEffect(move, target, 0); - - // Assert - await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ForceSetHeldItem")).IsFalse(); - await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BurnUpTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BurnUpTests.cs index 956dd45..53a1b47 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BurnUpTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/BurnUpTests.cs @@ -18,8 +18,8 @@ public class BurnUpTests /// with "fire" and "water" registered. The user is a Water-type, optionally /// also carrying the Fire type. /// - private static (BurnUp burnUp, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData, TypeIdentifier - fireType) CreateTestSetup(bool userIsFireType, bool userIsFrozen = false) + private static (BurnUp burnUp, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData hitData, + TypeIdentifier fireType) CreateTestSetup(bool userIsFireType, bool userIsFrozen = false) { var burnUp = new BurnUp(); @@ -33,11 +33,9 @@ public class BurnUpTests dynamicLibrary.StaticLibrary.Returns(staticLibrary); var battle = Substitute.For(); battle.Library.Returns(dynamicLibrary); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); + user.Battle.Returns(battle); user.Types.Returns(userIsFireType ? new List { fireType, waterType } : new List { waterType }); @@ -46,7 +44,7 @@ public class BurnUpTests var move = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -54,9 +52,9 @@ public class BurnUpTests } /// - /// Helper that checks whether was called with the given type. + /// Helper that checks whether was called with the given type. /// - private static bool ReceivedRemoveType(IPokemon user, TypeIdentifier type) => + private static bool ReceivedRemoveType(IBattlePokemon user, TypeIdentifier type) => user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveType" && type.Equals((TypeIdentifier)c.GetArguments()[0]!)); @@ -177,23 +175,4 @@ public class BurnUpTests // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse(); } - - /// - /// If the user has no , the script does nothing: no type is removed - /// and the hit is not failed. - /// - [Test] - public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing() - { - // Arrange - var (burnUp, move, user, target, hitData, _) = CreateTestSetup(true); - user.BattleData.Returns((IPokemonBattleData?)null); - - // Act - burnUp.OnSecondaryEffect(move, target, 0); - - // Assert - await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveType")).IsFalse(); - await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs index 6d8dd87..8368182 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CamouflageTests.cs @@ -13,7 +13,7 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class CamouflageTests { - private static (Camouflage script, IExecutingMove move, IPokemon user, IBattle battle) CreateTestSetup( + private static (Camouflage script, IExecutingMove move, IBattlePokemon user, IBattle battle) CreateTestSetup( string? terrainName, string environmentName) { var script = new Camouflage(); @@ -24,7 +24,7 @@ public class CamouflageTests battle.TerrainName.Returns(terrainName == null ? null : new StringKey?(new StringKey(terrainName))); battle.EnvironmentName.Returns(new StringKey(environmentName)); - var user = Substitute.For(); + var user = Substitute.For(); var move = Substitute.For(); move.User.Returns(user); move.Battle.Returns(battle); @@ -34,9 +34,9 @@ public class CamouflageTests /// /// Helper that returns the single type the user was changed to, or null when - /// was never called. + /// was never called. /// - private static TypeIdentifier? GetSetType(IPokemon user) + private static TypeIdentifier? GetSetType(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes"); return call != null ? ((IReadOnlyList)call.GetArguments()[0]!).Single() : null; @@ -56,7 +56,7 @@ public class CamouflageTests battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected); @@ -75,7 +75,7 @@ public class CamouflageTests battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected); @@ -92,7 +92,7 @@ public class CamouflageTests battle.Library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var expected); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected); @@ -107,7 +107,7 @@ public class CamouflageTests { // Arrange var (script, move, _, _) = CreateTestSetup(null, "field"); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs index 5957dca..3269959 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CaptivateTests.cs @@ -14,16 +14,16 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class CaptivateTests { - private static (Captivate script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData) + private static (Captivate script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData hitData) CreateTestSetup(Gender userGender, Gender targetGender) { var script = new Captivate(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.Gender.Returns(userGender); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); target.Gender.Returns(targetGender); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -34,7 +34,7 @@ public class CaptivateTests /// /// Helper that checks whether a stat boost change was applied to the given Pokémon. /// - private static bool ReceivedStatBoost(IPokemon pokemon, Statistic stat, sbyte amount) => + private static bool ReceivedStatBoost(IBattlePokemon pokemon, Statistic stat, sbyte amount) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" && (Statistic)c.GetArguments()[0]! == stat && (sbyte)c.GetArguments()[1]! == amount); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChangeTargetSpecialDefenseTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChangeTargetSpecialDefenseTests.cs index 444a4d8..4d499f6 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChangeTargetSpecialDefenseTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChangeTargetSpecialDefenseTests.cs @@ -25,7 +25,7 @@ public class ChangeTargetSpecialDefenseTests /// /// Helper to extract the arguments of the ChangeStatBoost call received by a substitute target. /// - private static object?[]? GetStatBoostCallArgs(IPokemon target) + private static object?[]? GetStatBoostCallArgs(IBattlePokemon target) { var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); return call?.GetArguments(); @@ -41,9 +41,9 @@ public class ChangeTargetSpecialDefenseTests // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -66,9 +66,9 @@ public class ChangeTargetSpecialDefenseTests // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -90,9 +90,9 @@ public class ChangeTargetSpecialDefenseTests // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -114,9 +114,9 @@ public class ChangeTargetSpecialDefenseTests // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -137,7 +137,7 @@ public class ChangeTargetSpecialDefenseTests // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); // Act - the user is hit by its own move (e.g. redirected) @@ -159,9 +159,9 @@ public class ChangeTargetSpecialDefenseTests // Arrange var script = CreateInitializedScript(amount); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs index 46549d2..1fa6da1 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChargeTests.cs @@ -15,11 +15,11 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class ChargeTests { - private static (Charge script, IExecutingMove move, IPokemon user, IScriptSet userVolatile) CreateTestSetup() + private static (Charge script, IExecutingMove move, IBattlePokemon user, IScriptSet userVolatile) CreateTestSetup() { var script = new Charge(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); // Use a real script set so the volatile script added by Charge can be inspected afterwards. var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); @@ -32,16 +32,14 @@ public class ChargeTests /// Creates an executing move of the given type whose damage modifier can be changed by /// . /// - private static (IExecutingMove move, IPokemon target) CreateExecutingMoveOfType(string typeName) + private static (IExecutingMove move, IBattlePokemon target) CreateExecutingMoveOfType(string typeName) { var library = LibraryHelpers.LoadLibrary(); var battle = Substitute.For(); battle.Library.Returns(library); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var target = Substitute.For(); - target.BattleData.Returns(battleData); + var target = Substitute.For(); + target.Battle.Returns(battle); library.StaticLibrary.Types.TryGetTypeIdentifier(typeName, out var typeIdentifier); var useMove = Substitute.For(); useMove.MoveType.Returns(typeIdentifier); @@ -61,7 +59,7 @@ public class ChargeTests var (script, move, user, _) = CreateTestSetup(); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var boost = user.ReceivedCalls().SingleOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); @@ -82,7 +80,7 @@ public class ChargeTests var (script, move, _, userVolatile) = CreateTestSetup(); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); @@ -134,7 +132,7 @@ public class ChargeTests { // Arrange var (script, move, user, userVolatile) = CreateTestSetup(); - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); await Assert.That(userVolatile.TryGet(out var effect)).IsTrue(); var battle = Substitute.For(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs index 23e7625..b03ab17 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ChipAwayTests.cs @@ -22,7 +22,8 @@ public class ChipAwayTests var bypass = false; // Act - script.BypassDefensiveStatBoosts(Substitute.For(), Substitute.For(), 0, ref bypass); + script.BypassDefensiveStatBoosts(Substitute.For(), Substitute.For(), 0, + ref bypass); // Assert await Assert.That(bypass).IsTrue(); @@ -40,7 +41,8 @@ public class ChipAwayTests var bypass = false; // Act - script.BypassEvasionStatBoosts(Substitute.For(), Substitute.For(), 0, ref bypass); + script.BypassEvasionStatBoosts(Substitute.For(), Substitute.For(), 0, + ref bypass); // Assert await Assert.That(bypass).IsTrue(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs index bea78b9..3ba2732 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/Conversion2Tests.cs @@ -19,8 +19,8 @@ public class Conversion2Tests { private static readonly IDynamicLibrary Library = LibraryHelpers.LoadLibrary(); - private static (Conversion2 script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData) - CreateTestSetup(TypeIdentifier? lastMoveType) + private static (Conversion2 script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData + hitData) CreateTestSetup(TypeIdentifier? lastMoveType) { var script = new Conversion2(); @@ -30,15 +30,13 @@ public class Conversion2Tests battle.Library.Returns(Library); battle.Random.Returns(random); - var userBattleData = Substitute.For(); - userBattleData.Battle.Returns(battle); - var user = Substitute.For(); - user.BattleData.Returns(userBattleData); + var user = Substitute.For(); + user.Battle.Returns(battle); - var targetBattleData = Substitute.For(); + var target = Substitute.For(); if (lastMoveType == null) { - targetBattleData.LastMoveChoice.Returns((IMoveChoice?)null); + target.LastMoveChoice.Returns((IMoveChoice?)null); } else { @@ -48,12 +46,9 @@ public class Conversion2Tests learnedMove.MoveData.Returns(moveData); var lastChoice = Substitute.For(); lastChoice.ChosenMove.Returns(learnedMove); - targetBattleData.LastMoveChoice.Returns(lastChoice); + target.LastMoveChoice.Returns(lastChoice); } - var target = Substitute.For(); - target.BattleData.Returns(targetBattleData); - var move = Substitute.For(); move.User.Returns(user); var hitData = Substitute.For(); @@ -70,9 +65,9 @@ public class Conversion2Tests /// /// Helper that returns the single type the user was changed to, or null when - /// was never called. + /// was never called. /// - private static TypeIdentifier? GetSetType(IPokemon user) + private static TypeIdentifier? GetSetType(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes"); return call != null ? ((IReadOnlyList)call.GetArguments()[0]!).Single() : null; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs index ff2e80c..66bf5af 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ConversionTests.cs @@ -22,15 +22,15 @@ public class ConversionTests return learned; } - private static (Conversion script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData) - CreateTestSetup(params ILearnedMove?[] moves) + private static (Conversion script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData hitData + ) CreateTestSetup(params ILearnedMove?[] moves) { var script = new Conversion(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.Moves.Returns(moves); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); return (script, move, user, target, hitData); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs index c315171..e80a2c2 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CopycatTests.cs @@ -17,11 +17,10 @@ public class CopycatTests private static (Copycat script, IMoveChoice choice) CreateTestSetup(string? lastMoveName) { var script = new Copycat(); - var user = Substitute.For(); - var battleData = Substitute.For(); + var user = Substitute.For(); if (lastMoveName == null) { - battleData.LastMoveChoice.Returns((IMoveChoice?)null); + user.LastMoveChoice.Returns((IMoveChoice?)null); } else { @@ -31,9 +30,8 @@ public class CopycatTests learnedMove.MoveData.Returns(moveData); var lastChoice = Substitute.For(); lastChoice.ChosenMove.Returns(learnedMove); - battleData.LastMoveChoice.Returns(lastChoice); + user.LastMoveChoice.Returns(lastChoice); } - user.BattleData.Returns(battleData); var choice = Substitute.For(); choice.User.Returns(user); @@ -94,25 +92,4 @@ public class CopycatTests choice.Received(1).Fail(); await Assert.That(moveName).IsEqualTo(new StringKey("copycat")); } - - /// - /// Technical test: outside of battle (no battle data) there is no last move, so Copycat fails. - /// - [Test] - public void ChangeMove_NoBattleData_Fails() - { - // Arrange - var script = new Copycat(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - var choice = Substitute.For(); - choice.User.Returns(user); - StringKey moveName = "copycat"; - - // Act - script.ChangeMove(choice, ref moveName); - - // Assert - choice.Received(1).Fail(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs index cd1fc0e..b26c914 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CoreEnforcerTests.cs @@ -14,16 +14,14 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class CoreEnforcerTests { - private static (CoreEnforcer script, IExecutingMove move, IMoveChoice currentChoice, IPokemon target, IHitData + private static (CoreEnforcer script, IExecutingMove move, IMoveChoice currentChoice, IBattlePokemon target, IHitData hitData, IBattle battle) CreateTestSetup() { var script = new CoreEnforcer(); var battle = Substitute.For(); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var target = Substitute.For(); - target.BattleData.Returns(battleData); + var target = Substitute.For(); + target.Battle.Returns(battle); var currentChoice = Substitute.For(); var move = Substitute.For(); move.MoveChoice.Returns(currentChoice); @@ -120,24 +118,6 @@ public class CoreEnforcerTests hitData.Received(1).Fail(); } - /// - /// Technical test: a target without battle data (not in battle) is left untouched. - /// - [Test] - public void OnSecondaryEffect_TargetHasNoBattleData_DoesNothing() - { - // Arrange - var (script, move, _, target, hitData, _) = CreateTestSetup(); - target.BattleData.Returns((IPokemonBattleData?)null); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - target.DidNotReceive().SuppressAbility(); - hitData.DidNotReceive().Fail(); - } - /// /// Bulbapedia: the condition is that "the target has already used a move" — an action by a different /// Pokémon (e.g. the user's ally in a Double Battle) does not count as the target having acted. @@ -148,7 +128,7 @@ public class CoreEnforcerTests // Arrange var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup(); var allyChoice = Substitute.For(); - allyChoice.User.Returns(Substitute.For()); + allyChoice.User.Returns(Substitute.For()); SetTurnChoices(battle, allyChoice, currentChoice); // Act @@ -162,7 +142,7 @@ public class CoreEnforcerTests /// /// Bulbapedia: "The move cannot suppress certain signature abilities including Multitype, Stance /// Change, Schooling, Comatose, Shields Down, Disguise, RKS System, Battle Bond, Power Construct". - /// The script requests the suppression unconditionally; refuses + /// The script requests the suppression unconditionally; refuses /// it when is false, so these abilities must carry that flag in the /// Gen7 data. /// diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs index fa04d89..cae57fe 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CounterTests.cs @@ -21,9 +21,9 @@ public class CounterTests /// Creates a user whose volatile scripts contain a that has recorded /// an incoming physical hit of the given damage by . /// - private static IPokemon CreateUserHitBy(IPokemon? attacker, uint damage, bool physical = true) + private static IBattlePokemon CreateUserHitBy(IBattlePokemon? attacker, uint damage, bool physical = true) { - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); @@ -55,7 +55,7 @@ public class CounterTests { // Arrange var script = new Counter(); - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); @@ -78,11 +78,11 @@ public class CounterTests { // Arrange var script = new Counter(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); var user = CreateUserHitBy(attacker, 40); var choice = Substitute.For(); choice.User.Returns(user); - IReadOnlyList targets = new IPokemon?[] { Substitute.For() }; + IReadOnlyList targets = new IBattlePokemon?[] { Substitute.For() }; // Act script.ChangeTargets(choice, ref targets); @@ -103,7 +103,7 @@ public class CounterTests var user = CreateUserHitBy(null, 0); var choice = Substitute.For(); choice.User.Returns(user); - IReadOnlyList targets = new IPokemon?[] { Substitute.For() }; + IReadOnlyList targets = new IBattlePokemon?[] { Substitute.For() }; // Act script.ChangeTargets(choice, ref targets); @@ -122,7 +122,7 @@ public class CounterTests { // Arrange var script = new Counter(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); var user = CreateUserHitBy(attacker, damageTaken); var move = Substitute.For(); move.User.Returns(user); @@ -144,9 +144,9 @@ public class CounterTests { // Arrange var script = new Counter(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); var user = CreateUserHitBy(attacker, 40); - var someoneElse = Substitute.For(); + var someoneElse = Substitute.For(); var move = Substitute.For(); move.User.Returns(user); var hitData = Substitute.For(); @@ -167,7 +167,7 @@ public class CounterTests public async Task CounterHelperEffect_PhysicalHit_RecordsAttackerAndDamage() { // Arrange - var attacker = Substitute.For(); + var attacker = Substitute.For(); var user = CreateUserHitBy(attacker, 40); // Assert @@ -184,7 +184,7 @@ public class CounterTests public async Task CounterHelperEffect_SpecialHit_IsNotRecorded() { // Arrange - var attacker = Substitute.For(); + var attacker = Substitute.For(); var user = CreateUserHitBy(attacker, 40, false); // Assert diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs index ed10133..a56067e 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CovetTests.cs @@ -12,17 +12,17 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class CovetTests { - private static (Covet script, IExecutingMove move, IPokemon user, IPokemon target) CreateTestSetup( + private static (Covet script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target) CreateTestSetup( IItem? targetItem) { var script = new Covet(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); // Explicitly return null to suppress NSubstitute's auto-substitution; Covet must see an empty-handed user. user.HeldItem.Returns((IItem?)null); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); target.HeldItem.Returns(targetItem); if (targetItem != null) { diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs index 5b5c30d..2a56f77 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CraftyShieldTests.cs @@ -26,13 +26,11 @@ public class CraftyShieldTests var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - battleData.SideIndex.Returns((byte)0); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); var move = Substitute.For(); + user.SideIndex.Returns((byte)0); + user.Battle.Returns(battle); move.User.Returns(user); return (script, move, sideVolatile); @@ -58,7 +56,7 @@ public class CraftyShieldTests var (script, move, sideVolatile) = CreateTestSetup(); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); @@ -99,21 +97,4 @@ public class CraftyShieldTests // Assert await Assert.That(stop).IsFalse(); } - - /// - /// Technical test: outside of battle (no battle data) no shield can be raised and nothing happens. - /// - [Test] - public async Task OnSecondaryEffect_NoBattleData_DoesNothing() - { - // Arrange - var (script, move, sideVolatile) = CreateTestSetup(); - move.User.BattleData.Returns((IPokemonBattleData?)null); - - // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); - - // Assert - no effect is added - await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs index 2a6214e..7e97b61 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CrushGripTests.cs @@ -11,12 +11,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class CrushGripTests { - private static (CrushGrip script, IExecutingMove move, IPokemon target) CreateTestSetup(uint currentHealth, + private static (CrushGrip script, IExecutingMove move, IBattlePokemon target) CreateTestSetup(uint currentHealth, uint maxHealth) { var script = new CrushGrip(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); target.CurrentHealth.Returns(currentHealth); target.BoostedStats.Returns(new StatisticSet(maxHealth, 1, 1, 1, 1, 1)); return (script, move, target); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs index da8a6db..729334e 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/CurseTests.cs @@ -16,8 +16,8 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class CurseTests { - private static (Curse script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile) - CreateTestSetup(bool userIsGhost, uint userMaxHealth = 100, uint userCurrentHealth = 100) + private static (Curse script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IScriptSet + targetVolatile) CreateTestSetup(bool userIsGhost, uint userMaxHealth = 100, uint userCurrentHealth = 100) { var script = new Curse(); var library = LibraryHelpers.LoadLibrary(); @@ -26,18 +26,16 @@ public class CurseTests var battle = Substitute.For(); battle.Library.Returns(library); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); user.Types.Returns(new[] { userIsGhost ? ghostType : normalType }); + user.Battle.Returns(battle); user.MaxHealth.Returns(userMaxHealth); user.CurrentHealth.Returns(userCurrentHealth); var move = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); // Use a real script set so the curse applied to the target can be inspected afterwards. var targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); @@ -49,7 +47,7 @@ public class CurseTests /// /// Helper that checks whether a stat boost change was applied to the given Pokémon. /// - private static bool ReceivedStatBoost(IPokemon pokemon, Statistic stat, sbyte amount) => + private static bool ReceivedStatBoost(IBattlePokemon pokemon, Statistic stat, sbyte amount) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" && (Statistic)c.GetArguments()[0]! == stat && (sbyte)c.GetArguments()[1]! == amount); @@ -57,7 +55,7 @@ public class CurseTests /// /// Helper to extract the damage amount from a substitute's received Damage calls. /// - private static uint? GetDamageAmount(IPokemon pokemon) + private static uint? GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -178,7 +176,7 @@ public class CurseTests public async Task GhostCurseEffect_OnEndTurnAtFullHp_CursedPokemonLosesQuarterOfMaximumHp() { // Arrange - var cursed = Substitute.For(); + var cursed = Substitute.For(); cursed.MaxHealth.Returns(100u); cursed.CurrentHealth.Returns(100u); var effect = new GhostCurseEffect(cursed); @@ -198,7 +196,7 @@ public class CurseTests public async Task GhostCurseEffect_OnEndTurnAtLowHp_CursedPokemonStillLosesQuarterOfMaximumHp() { // Arrange - var cursed = Substitute.For(); + var cursed = Substitute.For(); cursed.MaxHealth.Returns(100u); cursed.CurrentHealth.Returns(40u); var effect = new GhostCurseEffect(cursed); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DarkestLariatTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DarkestLariatTests.cs index 1307a49..8cda600 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DarkestLariatTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DarkestLariatTests.cs @@ -20,7 +20,7 @@ public class DarkestLariatTests // Arrange var script = new DarkestLariat(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var bypass = false; // Act @@ -40,7 +40,7 @@ public class DarkestLariatTests // Arrange var script = new DarkestLariat(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var bypass = false; // Act diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DefogTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DefogTests.cs index 0deadc0..7d4c9fa 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DefogTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DefogTests.cs @@ -19,7 +19,7 @@ public class DefogTests /// /// Creates a fully mocked test setup where the target is on side 1 and the user on side 0. /// - private static (Defog script, IExecutingMove move, IPokemon target, IScriptSet targetSideScripts, IScriptSet + private static (Defog script, IExecutingMove move, IBattlePokemon target, IScriptSet targetSideScripts, IScriptSet userSideScripts) CreateTestSetup() { var script = new Defog(); @@ -35,18 +35,14 @@ public class DefogTests var battle = Substitute.For(); battle.Sides.Returns(new[] { userSide, targetSide }); - var user = Substitute.For(); - var userBattleData = Substitute.For(); - userBattleData.Battle.Returns(battle); - userBattleData.SideIndex.Returns((byte)0); - user.BattleData.Returns(userBattleData); + var user = Substitute.For(); + user.Battle.Returns(battle); + user.SideIndex.Returns((byte)0); move.User.Returns(user); - var target = Substitute.For(); - var targetBattleData = Substitute.For(); - targetBattleData.Battle.Returns(battle); - targetBattleData.SideIndex.Returns((byte)1); - target.BattleData.Returns(targetBattleData); + var target = Substitute.For(); + target.Battle.Returns(battle); + target.SideIndex.Returns((byte)1); return (script, move, target, targetSideScripts, userSideScripts); } @@ -144,21 +140,4 @@ public class DefogTests c.GetMethodInfo().Name == "ChangeStatBoost" && (Statistic)c.GetArguments()[0]! == Statistic.Evasion && (sbyte)c.GetArguments()[1]! == -1)).IsTrue(); } - - /// - /// Technical test: without battle data (outside of battle) the secondary effect does nothing and does - /// not throw. - /// - [Test] - public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNotThrow() - { - // Arrange - var script = new Defog(); - var move = Substitute.For(); - var target = Substitute.For(); - target.BattleData.Returns((IPokemonBattleData?)null); - - // Act & Assert - await Assert.That(() => script.OnSecondaryEffect(move, target, 0)).ThrowsNothing(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DestinyBondTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DestinyBondTests.cs index 95e566d..606bb13 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DestinyBondTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DestinyBondTests.cs @@ -17,14 +17,15 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; public class DestinyBondTests { /// - /// Creates a mocked user whose is a real so + /// Creates a mocked user whose is a real so /// the volatile added by the move can be inspected. /// - private static (DestinyBond script, IExecutingMove move, IPokemon user, ScriptSet userVolatile) CreateTestSetup() + private static (DestinyBond script, IExecutingMove move, IBattlePokemon user, ScriptSet userVolatile) + CreateTestSetup() { var script = new DestinyBond(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); @@ -36,7 +37,7 @@ public class DestinyBondTests /// Creates a fainting Pokémon whose battle's is a move /// choice made by the given attacker. /// - private static IPokemon CreateFaintingPokemonAttackedBy(IPokemon attacker) + private static IBattlePokemon CreateFaintingPokemonAttackedBy(IBattlePokemon attacker) { var moveChoice = Substitute.For(); moveChoice.User.Returns(attacker); @@ -45,10 +46,8 @@ public class DestinyBondTests var battle = Substitute.For(); battle.ChoiceQueue.Returns(queue); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var pokemon = Substitute.For(); - pokemon.BattleData.Returns(battleData); + var pokemon = Substitute.For(); + pokemon.Battle.Returns(battle); return pokemon; } @@ -61,7 +60,7 @@ public class DestinyBondTests { // Arrange var (script, move, _, userVolatile) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -119,7 +118,7 @@ public class DestinyBondTests { // Arrange var effect = new DestinyBondEffect(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); attacker.BoostedStats.Returns(new StatisticSet(100, 1, 1, 1, 1, 1)); var pokemon = CreateFaintingPokemonAttackedBy(attacker); @@ -141,7 +140,7 @@ public class DestinyBondTests { // Arrange var effect = new DestinyBondEffect(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); attacker.BoostedStats.Returns(new StatisticSet(100, 1, 1, 1, 1, 1)); var pokemon = CreateFaintingPokemonAttackedBy(attacker); @@ -162,7 +161,7 @@ public class DestinyBondTests { // Arrange var effect = new DestinyBondEffect(); - var attacker = Substitute.For(); + var attacker = Substitute.For(); attacker.BoostedStats.Returns(new StatisticSet(100, 1, 1, 1, 1, 1)); var pokemon = CreateFaintingPokemonAttackedBy(attacker); @@ -181,7 +180,7 @@ public class DestinyBondTests public async Task OnBeforeMove_UserMovesAgain_RemovesDestinyBondEffect() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DestinyBondEffect(); @@ -194,19 +193,4 @@ public class DestinyBondTests // Assert await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } - - /// - /// Technical test: fainting without battle data (outside of battle) does nothing and does not throw. - /// - [Test] - public async Task OnFaint_NoBattleData_DoesNotThrow() - { - // Arrange - var effect = new DestinyBondEffect(); - var pokemon = Substitute.For(); - pokemon.BattleData.Returns((IPokemonBattleData?)null); - - // Act & Assert - await Assert.That(() => effect.OnFaint(pokemon, DamageSource.MoveDamage)).ThrowsNothing(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DigTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DigTests.cs index 442a14b..6bf3821 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DigTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DigTests.cs @@ -18,12 +18,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class DigTests { - private static (Dig script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice) - CreateTestSetup() + private static (Dig script, IExecutingMove move, IBattlePokemon user, ScriptSet userVolatile, IMoveChoice moveChoice + ) CreateTestSetup() { var script = new Dig(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); // Use a real script set so the charge volatile added by Dig can be inspected afterwards. var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); @@ -35,9 +35,7 @@ public class DigTests var battle = Substitute.For(); battle.EventHook.Returns(new EventHook()); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - user.BattleData.Returns(battleData); + user.Battle.Returns(battle); return (script, move, user, userVolatile, moveChoice); } @@ -250,7 +248,7 @@ public class DigTests public async Task OnAfterMoveChoice_ChoiceIsNotDigCharge_RemovesDigEffect() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DigEffect(user); @@ -273,7 +271,7 @@ public class DigTests public async Task OnAfterMoveChoice_ChoiceIsDigCharge_KeepsDigEffect() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DigEffect(user); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DisableTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DisableTests.cs index d793767..6729580 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DisableTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DisableTests.cs @@ -36,26 +36,22 @@ public class DisableTests /// Creates a fully mocked test setup where the target's volatile scripts are a real /// and its last used move is the given one (or none). /// - private static (Disable script, IExecutingMove move, IPokemon target, ScriptSet targetVolatile, IHitData hitData) - CreateTestSetup(string? lastUsedMove, bool lastMoveIsStruggle = false) + private static (Disable script, IExecutingMove move, IBattlePokemon target, ScriptSet targetVolatile, IHitData + hitData) CreateTestSetup(string? lastUsedMove, bool lastMoveIsStruggle = false) { var script = new Disable(); var move = Substitute.For(); - var user = Substitute.For(); - var userBattleData = Substitute.For(); - user.BattleData.Returns(userBattleData); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); var targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); - var targetBattleData = Substitute.For(); // Create the choice before the Returns call; configuring substitutes inside Returns is not allowed. var lastMoveChoice = lastUsedMove == null ? null : CreateMoveChoice(lastUsedMove); - targetBattleData.LastMoveChoice.Returns(lastMoveChoice); - target.BattleData.Returns(targetBattleData); + target.LastMoveChoice.Returns(lastMoveChoice); // Struggle is recognized through the misc library's replacement choice check, not by name. var miscLibrary = Substitute.For(); @@ -144,24 +140,6 @@ public class DisableTests await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } - /// - /// Technical test: without battle data on the user (outside of battle) the effect does nothing and - /// does not throw. - /// - [Test] - public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing() - { - // Arrange - var (script, move, target, targetVolatile, _) = CreateTestSetup("tackle"); - move.User.BattleData.Returns((IPokemonBattleData?)null); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); - } - /// /// Bulbapedia: "Disable temporarily prevents the target from using a specific move." Selecting the /// disabled move is prevented by the . @@ -207,7 +185,7 @@ public class DisableTests public async Task OnEndTurn_ThreeTurnsPassed_EffectStillActive() { // Arrange - var target = Substitute.For(); + var target = Substitute.For(); var targetVolatile = new ScriptSet(target); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DisableEffect(new StringKey("tackle")); @@ -230,7 +208,7 @@ public class DisableTests public async Task OnEndTurn_FourTurnsPassed_EffectRemovesItself() { // Arrange - var target = Substitute.For(); + var target = Substitute.For(); var targetVolatile = new ScriptSet(target); target.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DisableEffect(new StringKey("tackle")); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DiveTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DiveTests.cs index 73b69ab..0742007 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DiveTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DiveTests.cs @@ -18,12 +18,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class DiveTests { - private static (Dive script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice) - CreateTestSetup() + private static (Dive script, IExecutingMove move, IBattlePokemon user, ScriptSet userVolatile, IMoveChoice + moveChoice) CreateTestSetup() { var script = new Dive(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); // Use a real script set so the charge volatile added by Dive can be inspected afterwards. var userVolatile = new ScriptSet(user); user.Volatile.Returns(userVolatile); @@ -35,9 +35,7 @@ public class DiveTests var battle = Substitute.For(); battle.EventHook.Returns(new EventHook()); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - user.BattleData.Returns(battleData); + user.Battle.Returns(battle); return (script, move, user, userVolatile, moveChoice); } @@ -126,7 +124,7 @@ public class DiveTests // Arrange var (script, move, user, userVolatile, _) = CreateTestSetup(); userVolatile.Add(new DiveEffect(user)); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -248,7 +246,7 @@ public class DiveTests public async Task OnAfterMoveChoice_ChoiceIsNotDiveCharge_RemovesDiveEffect() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DiveEffect(user); @@ -271,7 +269,7 @@ public class DiveTests public async Task OnAfterMoveChoice_ChoiceIsDiveCharge_KeepsDiveEffect() { // Arrange - var user = Substitute.For(); + var user = Substitute.For(); var userVolatile = new ScriptSet(user); user.GetScripts().Returns(_ => new ScriptIterator(new List>())); var effect = new DiveEffect(user); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoomDesireTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoomDesireTests.cs index 0742338..f682c46 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoomDesireTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoomDesireTests.cs @@ -18,8 +18,8 @@ public class DoomDesireTests /// Creates a fully mocked test setup where the target is at the given position on a side whose /// volatile scripts are a real . /// - private static (DoomDesire script, IExecutingMove move, IPokemon target, IBattleSide side, ScriptSet sideScripts, - IHitData hitData) CreateTestSetup(byte position = 0, uint damage = 100) + private static (DoomDesire script, IExecutingMove move, IBattlePokemon target, IBattleSide side, ScriptSet + sideScripts, IHitData hitData) CreateTestSetup(byte position = 0, uint damage = 100) { var script = new DoomDesire(); var move = Substitute.For(); @@ -31,13 +31,11 @@ public class DoomDesireTests var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); - var target = Substitute.For(); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - battleData.SideIndex.Returns((byte)0); - battleData.Position.Returns(position); - target.BattleData.Returns(battleData); - side.Pokemon.Returns(new IPokemon?[] { target }); + var target = Substitute.For(); + target.Battle.Returns(battle); + target.SideIndex.Returns((byte)0); + target.Position.Returns(position); + side.Pokemon.Returns(new IBattlePokemon?[] { target }); var hitData = Substitute.For(); hitData.Damage.Returns(damage); @@ -175,25 +173,4 @@ public class DoomDesireTests // Assert await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse(); } - - /// - /// Technical test: without battle data on the target (outside of battle) the script does nothing and - /// does not throw. - /// - [Test] - public async Task BlockOutgoingHit_TargetHasNoBattleData_DoesNotBlock() - { - // Arrange - var script = new DoomDesire(); - var move = Substitute.For(); - var target = Substitute.For(); - target.BattleData.Returns((IPokemonBattleData?)null); - var block = false; - - // Act - script.BlockOutgoingHit(move, target, 0, ref block); - - // Assert - await Assert.That(block).IsFalse(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoublePowerIfTargetDamagedInTurnTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoublePowerIfTargetDamagedInTurnTests.cs index fb26d47..a30157b 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoublePowerIfTargetDamagedInTurnTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DoublePowerIfTargetDamagedInTurnTests.cs @@ -20,8 +20,8 @@ public class DoublePowerIfTargetDamagedInTurnTests /// Creates a fully mocked test setup where the target sits on side 0 whose volatile scripts are a /// real . /// - private static (DoublePowerIfTargetDamagedInTurn script, IExecutingMove move, IPokemon target, ScriptSet sideScripts - ) CreateTestSetup() + private static (DoublePowerIfTargetDamagedInTurn script, IExecutingMove move, IBattlePokemon target, ScriptSet + sideScripts ) CreateTestSetup() { var script = new DoublePowerIfTargetDamagedInTurn(); var move = Substitute.For(); @@ -33,17 +33,13 @@ public class DoublePowerIfTargetDamagedInTurnTests var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); - var user = Substitute.For(); - var userBattleData = Substitute.For(); - userBattleData.Battle.Returns(battle); - user.BattleData.Returns(userBattleData); + var user = Substitute.For(); + user.Battle.Returns(battle); move.User.Returns(user); - var target = Substitute.For(); - var targetBattleData = Substitute.For(); - targetBattleData.Battle.Returns(battle); - targetBattleData.SideIndex.Returns((byte)0); - target.BattleData.Returns(targetBattleData); + var target = Substitute.For(); + target.Battle.Returns(battle); + target.SideIndex.Returns((byte)0); return (script, move, target, sideScripts); } @@ -140,7 +136,7 @@ public class DoublePowerIfTargetDamagedInTurnTests var (script, move, target, sideScripts) = CreateTestSetup(); var data = new DoublePowerIfTargetDamagedInTurnData(); sideScripts.Add(data); - data.OnDamage(Substitute.For(), DamageSource.MoveDamage, 100, 80); + data.OnDamage(Substitute.For(), DamageSource.MoveDamage, 100, 80); ushort basePower = 60; // Act @@ -192,27 +188,4 @@ public class DoublePowerIfTargetDamagedInTurnTests await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())) .IsFalse(); } - - /// - /// Technical test: without battle data on the user (outside of battle) the base power is unchanged - /// and nothing throws. - /// - [Test] - public async Task ChangeBasePower_UserHasNoBattleData_BasePowerUnchanged() - { - // Arrange - var script = new DoublePowerIfTargetDamagedInTurn(); - var move = Substitute.For(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - move.User.Returns(user); - var target = Substitute.For(); - ushort basePower = 60; - - // Act - script.ChangeBasePower(move, target, 0, ref basePower); - - // Assert - await Assert.That(basePower).IsEqualTo((ushort)60); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DragonAscentTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DragonAscentTests.cs index dea7292..e18128f 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DragonAscentTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DragonAscentTests.cs @@ -12,11 +12,11 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class DragonAscentTests { - private static (DragonAscent script, IExecutingMove move, IPokemon user) CreateTestSetup() + private static (DragonAscent script, IExecutingMove move, IBattlePokemon user) CreateTestSetup() { var script = new DragonAscent(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); return (script, move, user); } @@ -25,7 +25,7 @@ public class DragonAscentTests /// Helper checking whether the given Pokémon received a ChangeStatBoost call for the given stat with /// a one-stage self-inflicted, non-forced drop. /// - private static bool ReceivedSelfInflictedDrop(IPokemon pokemon, Statistic stat) => + private static bool ReceivedSelfInflictedDrop(IBattlePokemon pokemon, Statistic stat) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" && (Statistic)c.GetArguments()[0]! == stat && (sbyte)c.GetArguments()[1]! == -1 && (bool)c.GetArguments()[2]! && !(bool)c.GetArguments()[3]!); @@ -39,7 +39,7 @@ public class DragonAscentTests { // Arrange var (script, move, user) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -57,7 +57,7 @@ public class DragonAscentTests { // Arrange var (script, move, user) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -75,7 +75,7 @@ public class DragonAscentTests { // Arrange var (script, move, _) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); @@ -93,7 +93,7 @@ public class DragonAscentTests { // Arrange var (script, move, user) = CreateTestSetup(); - var target = Substitute.For(); + var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DrainTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DrainTests.cs index add1f3f..1ffce9f 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DrainTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DrainTests.cs @@ -15,12 +15,12 @@ public class DrainTests /// /// Creates a fully mocked test setup for Drain tests. /// - private static (Drain drain, IExecutingMove move, IPokemon target, IPokemon user) CreateTestSetup(uint damage, - bool holdsBigRoot = false, Script[]? targetScripts = null) + private static (Drain drain, IExecutingMove move, IBattlePokemon target, IBattlePokemon user) CreateTestSetup( + uint damage, bool holdsBigRoot = false, Script[]? targetScripts = null) { var drain = new Drain(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); hitData.Damage.Returns(damage); move.GetHitData(target, 0).Returns(hitData); @@ -31,7 +31,7 @@ public class DrainTests .ToArray(); target.GetScripts().Returns(_ => new ScriptIterator(containers)); - var user = Substitute.For(); + var user = Substitute.For(); if (holdsBigRoot) user.HasHeldItem("big_root").Returns(true); move.User.Returns(user); @@ -42,7 +42,7 @@ public class DrainTests /// /// Helper to extract the heal amount from the user's received Heal calls. /// - private static uint? GetHealAmount(IPokemon user) + private static uint? GetHealAmount(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -51,7 +51,7 @@ public class DrainTests /// /// Helper to extract the damage amount from the user's received Damage calls. /// - private static uint? GetDamageAmount(IPokemon user) + private static uint? GetDamageAmount(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -60,7 +60,7 @@ public class DrainTests /// /// Helper to extract the damage source from the user's received Damage calls. /// - private static DamageSource? GetDamageSource(IPokemon user) + private static DamageSource? GetDamageSource(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (DamageSource)call.GetArguments()[1]! : null; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DreamEaterTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DreamEaterTests.cs index de2d3a2..5f20769 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DreamEaterTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/DreamEaterTests.cs @@ -17,12 +17,12 @@ public class DreamEaterTests /// /// Creates a fully mocked test setup for Dream Eater tests. /// - private static (DreamEater script, IExecutingMove move, IPokemon target, IPokemon user) CreateTestSetup(uint damage, - bool holdsBigRoot = false, Script[]? targetScripts = null) + private static (DreamEater script, IExecutingMove move, IBattlePokemon target, IBattlePokemon user) CreateTestSetup( + uint damage, bool holdsBigRoot = false, Script[]? targetScripts = null) { var script = new DreamEater(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); hitData.Damage.Returns(damage); move.GetHitData(target, 0).Returns(hitData); @@ -33,7 +33,7 @@ public class DreamEaterTests .ToArray(); target.GetScripts().Returns(_ => new ScriptIterator(containers)); - var user = Substitute.For(); + var user = Substitute.For(); if (holdsBigRoot) user.HasHeldItem("big_root").Returns(true); move.User.Returns(user); @@ -44,7 +44,7 @@ public class DreamEaterTests /// /// Helper to extract the heal amount from the user's received Heal calls. /// - private static uint? GetHealAmount(IPokemon user) + private static uint? GetHealAmount(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal"); return call != null ? (uint)call.GetArguments()[0]! : null; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EchoedVoiceTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EchoedVoiceTests.cs index 93020d2..f7a9f16 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EchoedVoiceTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EchoedVoiceTests.cs @@ -35,11 +35,12 @@ public class EchoedVoiceTests /// Creates a fully mocked test setup where the user's side has a real as its /// volatile scripts. /// - private static (EchoedVoice script, IExecutingMove move, IPokemon target, IScriptSet sideScripts) CreateTestSetup() + private static (EchoedVoice script, IExecutingMove move, IBattlePokemon target, IScriptSet sideScripts) + CreateTestSetup() { var script = new EchoedVoice(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); @@ -49,13 +50,10 @@ public class EchoedVoiceTests var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - battleData.SideIndex.Returns((byte)0); - - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); move.User.Returns(user); + user.SideIndex.Returns((byte)0); + user.Battle.Returns(battle); return (script, move, target, sideScripts); } @@ -155,25 +153,6 @@ public class EchoedVoiceTests await Assert.That(sideScripts.Get()!.Stacks).IsEqualTo(2); } - /// - /// Technical test: without battle data on the user (outside of battle) the base power hook does - /// nothing and does not throw. - /// - [Test] - public async Task ChangeBasePower_UserHasNoBattleData_BasePowerUnchanged() - { - // Arrange - var (script, move, target, _) = CreateTestSetup(); - move.User.BattleData.Returns((IPokemonBattleData?)null); - ushort basePower = 40; - - // Act - script.ChangeBasePower(move, target, 0, ref basePower); - - // Assert - await Assert.That(basePower).IsEqualTo((ushort)40); - } - /// /// Bulbapedia: the boost only applies on "consecutive" turns — when a Pokémon on the side chooses a /// move other than Echoed Voice, the marker removes itself, resetting diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectricTerrainTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectricTerrainTests.cs index c5209c8..81b4945 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectricTerrainTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectricTerrainTests.cs @@ -18,12 +18,10 @@ public class ElectricTerrainTests var move = Substitute.For(); var battle = Substitute.For(); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); move.User.Returns(user); + user.Battle.Returns(battle); return (script, move, battle); } @@ -40,27 +38,9 @@ public class ElectricTerrainTests var (script, move, battle) = CreateTestSetup(); // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); + script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert battle.Received(1).SetTerrain(ScriptUtils.ResolveName()); } - - /// - /// Technical test: without battle data on the user (outside of battle) the effect does nothing and - /// does not throw. - /// - [Test] - public void OnSecondaryEffect_UserHasNoBattleData_DoesNotSetTerrain() - { - // Arrange - var (script, move, battle) = CreateTestSetup(); - move.User.BattleData.Returns((IPokemonBattleData?)null); - - // Act - script.OnSecondaryEffect(move, Substitute.For(), 0); - - // Assert - battle.DidNotReceiveWithAnyArgs().SetTerrain(default); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectrifyTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectrifyTests.cs index 15d5fd0..7d31332 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectrifyTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectrifyTests.cs @@ -20,12 +20,12 @@ public class ElectrifyTests /// . When is false, the queue /// only contains another Pokémon's choice, simulating a target that already moved this turn. /// - private static (Electrify script, IExecutingMove move, IPokemon target, IScriptSet choiceVolatile, IHitData hitData) - CreateTestSetup(bool targetStillHasChoice = true, bool hasQueue = true) + private static (Electrify script, IExecutingMove move, IBattlePokemon target, IScriptSet choiceVolatile, IHitData + hitData) CreateTestSetup(bool targetStillHasChoice = true, bool hasQueue = true) { var script = new Electrify(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -40,9 +40,7 @@ public class ElectrifyTests var battle = Substitute.For(); battle.ChoiceQueue.Returns(hasQueue ? queue : null); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - target.BattleData.Returns(battleData); + target.Battle.Returns(battle); return (script, move, target, choiceVolatile, hitData); } @@ -111,10 +109,8 @@ public class ElectrifyTests var library = LibraryHelpers.LoadLibrary(); var battle = Substitute.For(); battle.Library.Returns(library); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var target = Substitute.For(); - target.BattleData.Returns(battleData); + var target = Substitute.For(); + target.Battle.Returns(battle); await Assert.That(library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var normal)).IsTrue(); await Assert.That(library.StaticLibrary.Types.TryGetTypeIdentifier("electric", out var electric)).IsTrue(); @@ -127,24 +123,4 @@ public class ElectrifyTests await Assert.That(moveType).IsNotNull(); await Assert.That(moveType!.Value).IsEqualTo(electric); } - - /// - /// Technical test: without battle data on the target the move type cannot be resolved and remains - /// unchanged. - /// - [Test] - public async Task ChangeMoveType_TargetHasNoBattleData_TypeUnchanged() - { - // Arrange - var effect = new ElectrifyEffect(); - var target = Substitute.For(); - target.BattleData.Returns((IPokemonBattleData?)null); - TypeIdentifier? moveType = null; - - // Act - effect.ChangeMoveType(Substitute.For(), target, 0, ref moveType); - - // Assert - await Assert.That(moveType).IsNull(); - } } \ No newline at end of file diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectroBallTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectroBallTests.cs index fd285e7..cdcff4b 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectroBallTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ElectroBallTests.cs @@ -14,15 +14,15 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class ElectroBallTests { - private static (ElectroBall script, IExecutingMove move, IPokemon target) CreateTestSetup(uint userSpeed, + private static (ElectroBall script, IExecutingMove move, IBattlePokemon target) CreateTestSetup(uint userSpeed, uint targetSpeed) { var script = new ElectroBall(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.BoostedStats.Returns(new StatisticSet(1, 1, 1, 1, 1, userSpeed)); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); target.BoostedStats.Returns(new StatisticSet(1, 1, 1, 1, 1, targetSpeed)); return (script, move, target); } diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EmbargoTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EmbargoTests.cs index 0d3781c..c334ce3 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EmbargoTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EmbargoTests.cs @@ -17,9 +17,9 @@ public class EmbargoTests /// /// Creates a target Pokémon whose volatile scripts are a real . /// - private static (IPokemon target, IScriptSet targetVolatile) CreateTarget() + private static (IBattlePokemon target, IScriptSet targetVolatile) CreateTarget() { - var target = Substitute.For(); + var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); @@ -57,7 +57,7 @@ public class EmbargoTests var prevented = false; // Act - effect.PreventHeldItemConsume(Substitute.For(), Substitute.For(), ref prevented); + effect.PreventHeldItemConsume(Substitute.For(), Substitute.For(), ref prevented); // Assert await Assert.That(prevented).IsTrue(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EncoreTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EncoreTests.cs index 4f9214f..74be26b 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EncoreTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EncoreTests.cs @@ -39,8 +39,9 @@ public class EncoreTests /// Creates a fully mocked test setup where the target's volatile scripts are a real /// and its last used move is the given one (or none). /// - private static (Encore script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData) - CreateTestSetup(string? lastUsedMove, bool lastMoveIsReplacement = false, bool lastMoveCantRepeat = false) + private static (Encore script, IExecutingMove move, IBattlePokemon target, IScriptSet targetVolatile, IHitData + hitData) CreateTestSetup(string? lastUsedMove, bool lastMoveIsReplacement = false, + bool lastMoveCantRepeat = false) { var script = new Encore(); var move = Substitute.For(); @@ -52,16 +53,14 @@ public class EncoreTests var battle = Substitute.For(); battle.Library.Returns(library); - var target = Substitute.For(); + var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet targetVolatile = new ScriptSet(target); target.Volatile.Returns(targetVolatile); - var targetBattleData = Substitute.For(); // Create the choice before the Returns call; configuring substitutes inside Returns is not allowed. var lastMoveChoice = lastUsedMove == null ? null : CreateMoveChoice(lastUsedMove, lastMoveCantRepeat); - targetBattleData.LastMoveChoice.Returns(lastMoveChoice); - targetBattleData.Battle.Returns(battle); - target.BattleData.Returns(targetBattleData); + target.LastMoveChoice.Returns(lastMoveChoice); + target.Battle.Returns(battle); if (lastMoveChoice != null) miscLibrary.IsReplacementChoice(lastMoveChoice).Returns(lastMoveIsReplacement); @@ -165,25 +164,6 @@ public class EncoreTests await Assert.That(moveData!.HasFlag(MoveFlags.CantRepeat)).IsTrue(); } - /// - /// Technical test: without battle data on the target (outside of battle) the effect does nothing and - /// does not throw. - /// - [Test] - public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNothing() - { - // Arrange - var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle"); - target.BattleData.Returns((IPokemonBattleData?)null); - - // Act - script.OnSecondaryEffect(move, target, 0); - - // Assert - hitData.DidNotReceive().Fail(); - await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); - } - /// /// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After two /// end-of-turn ticks the effect is still active. @@ -192,7 +172,7 @@ public class EncoreTests public async Task OnEndTurn_TwoTurnsPassed_EffectStillActive() { // Arrange - var owner = Substitute.For(); + var owner = Substitute.For(); owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet ownerVolatile = new ScriptSet(owner); var effect = new EncoreEffect(owner, new StringKey("tackle"), 3); @@ -214,7 +194,7 @@ public class EncoreTests public async Task OnEndTurn_ThreeTurnsPassed_EffectRemovesItself() { // Arrange - var owner = Substitute.For(); + var owner = Substitute.For(); owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet ownerVolatile = new ScriptSet(owner); var effect = new EncoreEffect(owner, new StringKey("tackle"), 3); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndeavorTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndeavorTests.cs index eca3931..f42d325 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndeavorTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndeavorTests.cs @@ -11,15 +11,15 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class EndeavorTests { - private static (Endeavor script, IExecutingMove move, IPokemon target) CreateTestSetup(uint userHealth, + private static (Endeavor script, IExecutingMove move, IBattlePokemon target) CreateTestSetup(uint userHealth, uint targetHealth) { var script = new Endeavor(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.CurrentHealth.Returns(userHealth); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); target.CurrentHealth.Returns(targetHealth); return (script, move, target); } diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndureTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndureTests.cs index 691033d..3ec48be 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndureTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EndureTests.cs @@ -20,12 +20,12 @@ public class EndureTests /// . The target of the secondary effect is the /// Pokémon using Endure itself, as the move is self-targeted. /// - private static (Endure script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet) + private static (Endure script, IExecutingMove move, IBattlePokemon target, IHitData hitData, IScriptSet volatileSet) CreateProtectSetup(bool userMovesLast, float randomRoll) { var script = new Endure(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); @@ -39,9 +39,7 @@ public class EndureTests battle.ChoiceQueue.Returns(queue); battle.Random.Returns(random); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - target.BattleData.Returns(battleData); + target.Battle.Returns(battle); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(target); @@ -134,7 +132,7 @@ public class EndureTests { // Arrange var effect = new EndureEffect(); - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.CurrentHealth.Returns(currentHealth); var damage = incomingDamage; @@ -154,7 +152,7 @@ public class EndureTests { // Arrange var effect = new EndureEffect(); - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.CurrentHealth.Returns(100u); var damage = 100u; @@ -174,7 +172,7 @@ public class EndureTests { // Arrange var effect = new EndureEffect(); - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.CurrentHealth.Returns(100u); var damage = 50u; @@ -193,7 +191,7 @@ public class EndureTests public async Task OnEndTurn_EffectRemovesItself() { // Arrange - var owner = Substitute.For(); + var owner = Substitute.For(); owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet ownerVolatile = new ScriptSet(owner); var effect = new EndureEffect(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EntrainmentTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EntrainmentTests.cs index 8c1ad55..521a336 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EntrainmentTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EntrainmentTests.cs @@ -25,16 +25,16 @@ public class EntrainmentTests return ability; } - private static (Entrainment script, IExecutingMove move, IPokemon target, IHitData hitData) CreateTestSetup( + private static (Entrainment script, IExecutingMove move, IBattlePokemon target, IHitData hitData) CreateTestSetup( IAbility? userAbility, IAbility? targetAbility) { var script = new Entrainment(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); - var user = Substitute.For(); + var user = Substitute.For(); user.ActiveAbility.Returns(userAbility); move.User.Returns(user); target.ActiveAbility.Returns(targetAbility); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EruptionTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EruptionTests.cs index e6b3e6a..52dbe23 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EruptionTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/EruptionTests.cs @@ -12,16 +12,16 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class EruptionTests { - private static (Eruption script, IExecutingMove move, IPokemon target) CreateTestSetup(uint currentHealth, + private static (Eruption script, IExecutingMove move, IBattlePokemon target) CreateTestSetup(uint currentHealth, uint maxHealth) { var script = new Eruption(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.CurrentHealth.Returns(currentHealth); user.BoostedStats.Returns(new StatisticSet(maxHealth, 1, 1, 1, 1, 1)); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); return (script, move, target); } diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ExplosionTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ExplosionTests.cs index 5bb82e8..910115a 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ExplosionTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/ExplosionTests.cs @@ -10,11 +10,11 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// public class ExplosionTests { - private static (Explosion script, IExecutingMove move, IPokemon user) CreateTestSetup(uint currentHealth) + private static (Explosion script, IExecutingMove move, IBattlePokemon user) CreateTestSetup(uint currentHealth) { var script = new Explosion(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.CurrentHealth.Returns(currentHealth); move.User.Returns(user); return (script, move, user); @@ -23,7 +23,7 @@ public class ExplosionTests /// /// Helper to extract the damage amount from the user's received Damage calls. /// - private static uint? GetDamageAmount(IPokemon user) + private static uint? GetDamageAmount(IBattlePokemon user) { var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -40,7 +40,7 @@ public class ExplosionTests var (script, move, user) = CreateTestSetup(100); // Act - script.OnAfterHits(move, Substitute.For()); + script.OnAfterHits(move, Substitute.For()); // Assert var damage = GetDamageAmount(user); @@ -60,7 +60,7 @@ public class ExplosionTests var (script, move, user) = CreateTestSetup(100); // Act - script.OnAfterHits(move, Substitute.For()); + script.OnAfterHits(move, Substitute.For()); // Assert var call = user.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage"); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FacadeTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FacadeTests.cs index dc50318..d03b961 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FacadeTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FacadeTests.cs @@ -16,15 +16,15 @@ public class FacadeTests { /// /// Creates a fully mocked test setup where the user has the given (or no) non-volatile status in its - /// , and is using a move of the given category. + /// , and is using a move of the given category. /// - private static (Facade facade, IExecutingMove move, IPokemon target) CreateTestSetup(Script? status, + private static (Facade facade, IExecutingMove move, IBattlePokemon target) CreateTestSetup(Script? status, MoveCategory category = MoveCategory.Physical) { var facade = new Facade(); var move = Substitute.For(); - var target = Substitute.For(); - var user = Substitute.For(); + var target = Substitute.For(); + var user = Substitute.For(); user.StatusScript.Returns(status == null ? new ScriptContainer() : new ScriptContainer(status)); move.User.Returns(user); var useMove = Substitute.For(); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FairyLockTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FairyLockTests.cs index 4fc2571..38e8acb 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FairyLockTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FairyLockTests.cs @@ -20,31 +20,29 @@ public class FairyLockTests /// Creates a fully mocked test setup where the battle has a real as its volatile /// script set. /// - private static (FairyLock script, IExecutingMove move, IPokemon target, IBattle battle, IScriptSet battleVolatile) - CreateTestSetup() + private static (FairyLock script, IExecutingMove move, IBattlePokemon target, IBattle battle, IScriptSet + battleVolatile) CreateTestSetup() { var script = new FairyLock(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); var battle = Substitute.For(); battle.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet battleVolatile = new ScriptSet(battle); battle.Volatile.Returns(battleVolatile); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - target.BattleData.Returns(battleData); + target.Battle.Returns(battle); return (script, move, target, battle, battleVolatile); } /// - /// Creates a mocked Pokémon with the given type names as its . + /// Creates a mocked Pokémon with the given type names as its . /// - private static IPokemon CreatePokemonWithTypes(params string[] types) + private static IBattlePokemon CreatePokemonWithTypes(params string[] types) { - var pokemon = Substitute.For(); + var pokemon = Substitute.For(); pokemon.Types.Returns(types.Select((name, index) => new TypeIdentifier((byte)(index + 1), new StringKey(name))) .ToList()); return pokemon; @@ -68,23 +66,6 @@ public class FairyLockTests await Assert.That(battleVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } - /// - /// Technical test: without battle data on the target (outside of battle) the secondary effect does - /// nothing and does not throw. - /// - [Test] - public void OnSecondaryEffect_TargetHasNoBattleData_DoesNotThrow() - { - // Arrange - var script = new FairyLock(); - var move = Substitute.For(); - var target = Substitute.For(); - target.BattleData.Returns((IPokemonBattleData?)null); - - // Act & Assert - should not throw - script.OnSecondaryEffect(move, target, 0); - } - /// /// Bulbapedia: "Fairy Lock prevents all Pokémon (except Ghost types) on the field from switching out". /// A non-Ghost Pokémon is prevented from switching. diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FakeOutTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FakeOutTests.cs index 35a1e2c..9766537 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FakeOutTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FakeOutTests.cs @@ -25,13 +25,11 @@ public class FakeOutTests var battle = Substitute.For(); battle.CurrentTurnNumber.Returns(currentTurn); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - battleData.SwitchInTurn.Returns(switchInTurn); - var user = Substitute.For(); - user.BattleData.Returns(battleData); + var user = Substitute.For(); move.User.Returns(user); + user.SwitchInTurn.Returns(switchInTurn); + user.Battle.Returns(battle); return (script, move); } @@ -74,28 +72,6 @@ public class FakeOutTests await Assert.That(stop).IsTrue(); } - /// - /// Technical test: without battle data on the user (outside of battle) the first-turn check does nothing - /// and the move is not stopped. - /// - [Test] - public async Task StopBeforeMove_UserHasNoBattleData_MoveNotStopped() - { - // Arrange - var script = new FakeOut(); - var move = Substitute.For(); - var user = Substitute.For(); - user.BattleData.Returns((IPokemonBattleData?)null); - move.User.Returns(user); - var stop = false; - - // Act - script.StopBeforeMove(move, ref stop); - - // Assert - await Assert.That(stop).IsFalse(); - } - /// /// Bulbapedia: "Fake Out inflicts damage and always makes the target flinch" — the secondary effect puts /// a on the target. @@ -106,7 +82,7 @@ public class FakeOutTests // Arrange var script = new FakeOut(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(target); target.Volatile.Returns(volatileSet); diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FalseSwipeTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FalseSwipeTests.cs index 8c016e7..55f90d1 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FalseSwipeTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FalseSwipeTests.cs @@ -14,11 +14,11 @@ public class FalseSwipeTests /// /// Creates a fully mocked test setup where the target has the given current health. /// - private static (FalseSwipe script, IExecutingMove move, IPokemon target) CreateTestSetup(uint currentHealth) + private static (FalseSwipe script, IExecutingMove move, IBattlePokemon target) CreateTestSetup(uint currentHealth) { var script = new FalseSwipe(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); target.CurrentHealth.Returns(currentHealth); return (script, move, target); } diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FeintTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FeintTests.cs index 8399c9a..02adf22 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FeintTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FeintTests.cs @@ -21,11 +21,11 @@ public class FeintTests /// Creates a fully mocked test setup where the target has a real as its volatile /// script set. /// - private static (Feint script, IExecutingMove move, IPokemon target, IScriptSet volatileSet) CreateTestSetup() + private static (Feint script, IExecutingMove move, IBattlePokemon target, IScriptSet volatileSet) CreateTestSetup() { var script = new Feint(); var move = Substitute.For(); - var target = Substitute.For(); + var target = Substitute.For(); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet volatileSet = new ScriptSet(target); target.Volatile.Returns(volatileSet); @@ -36,16 +36,14 @@ public class FeintTests /// Extends the test setup with a battle side that has a real as its volatile /// scripts, so side-wide protections such as can be attached. /// - private static IScriptSet AttachSide(IPokemon target) + private static IScriptSet AttachSide(IBattlePokemon target) { var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideScripts = new ScriptSet(side); side.VolatileScripts.Returns(sideScripts); - var battleData = Substitute.For(); - battleData.BattleSide.Returns(side); - target.BattleData.Returns(battleData); + target.BattleSide.Returns(side); return sideScripts; } diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FellStingerTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FellStingerTests.cs index e4a45ee..1df959b 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FellStingerTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FellStingerTests.cs @@ -14,14 +14,14 @@ public class FellStingerTests /// /// Creates a fully mocked test setup for Fell Stinger tests. /// - private static (FellStinger script, IExecutingMove move, IPokemon user, IPokemon target) CreateTestSetup( - bool targetFainted) + private static (FellStinger script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target) + CreateTestSetup(bool targetFainted) { var script = new FellStinger(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); target.IsFainted.Returns(targetFainted); return (script, move, user, target); } @@ -29,7 +29,7 @@ public class FellStingerTests /// /// Helper to find the first ChangeStatBoost call received by a Pokémon substitute. /// - private static object[]? GetStatBoostArguments(IPokemon pokemon) + private static object[]? GetStatBoostArguments(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); return call?.GetArguments()!; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FinalGambitTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FinalGambitTests.cs index 164f94d..4232253 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FinalGambitTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FinalGambitTests.cs @@ -13,22 +13,22 @@ public class FinalGambitTests /// /// Creates a fully mocked test setup for Final Gambit tests. /// - private static (FinalGambit script, IExecutingMove move, IPokemon user, IPokemon target) CreateTestSetup( - uint userCurrentHealth) + private static (FinalGambit script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target) + CreateTestSetup(uint userCurrentHealth) { var script = new FinalGambit(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); user.CurrentHealth.Returns(userCurrentHealth); move.User.Returns(user); - var target = Substitute.For(); + var target = Substitute.For(); return (script, move, user, target); } /// /// Helper to extract the damage amount from the user's received Damage calls. /// - private static uint? GetDamageAmount(IPokemon pokemon) + private static uint? GetDamageAmount(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (uint)call.GetArguments()[0]! : null; @@ -37,7 +37,7 @@ public class FinalGambitTests /// /// Helper to extract the damage source from the user's received Damage calls. /// - private static DamageSource? GetDamageSource(IPokemon pokemon) + private static DamageSource? GetDamageSource(IBattlePokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); return call != null ? (DamageSource)call.GetArguments()[1]! : null; diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FireFangTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FireFangTests.cs index 3af60f2..be96a1a 100644 --- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FireFangTests.cs +++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/FireFangTests.cs @@ -20,24 +20,22 @@ public class FireFangTests /// Creates a fully mocked test setup for Fire Fang tests. The target has not yet moved this turn when /// contains a choice for it. /// - private static (FireFang script, IExecutingMove move, IPokemon user, IPokemon target, IBattleRandom random, - IScriptSet targetVolatile) CreateTestSetup(BattleChoiceQueue? queue) + private static (FireFang script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IBattleRandom + random, IScriptSet targetVolatile) CreateTestSetup(BattleChoiceQueue? queue) { var script = new FireFang(); var move = Substitute.For(); - var user = Substitute.For(); + var user = Substitute.For(); move.User.Returns(user); var random = Substitute.For(); var battle = Substitute.For(); battle.Random.Returns(random); battle.ChoiceQueue.Returns(queue); - var battleData = Substitute.For(); - battleData.Battle.Returns(battle); - var target = Substitute.For(); - target.BattleData.Returns(battleData); + var target = Substitute.For(); var targetVolatile = Substitute.For(); + target.Battle.Returns(battle); target.Volatile.Returns(targetVolatile); return (script, move, user, target, random, targetVolatile); @@ -46,7 +44,7 @@ public class FireFangTests /// /// Creates a choice queue containing a single yet-to-execute move choice for the given Pokémon. /// - private static BattleChoiceQueue CreateQueueWithChoiceFor(IPokemon pokemon) + private static BattleChoiceQueue CreateQueueWithChoiceFor(IBattlePokemon pokemon) { var choice = Substitute.For(); choice.User.Returns(pokemon); @@ -58,7 +56,7 @@ public class FireFangTests /// matchers cannot be used for parameters: its parameterless constructor /// generates a random Guid, which breaks NSubstitute's argument specification binding.) /// - private static bool ReceivedSetStatus(IPokemon pokemon) => + private static bool ReceivedSetStatus(IBattlePokemon pokemon) => pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus"); /// @@ -126,7 +124,7 @@ public class FireFangTests // Arrange var (script, move, _, target, random, targetVolatile) = CreateTestSetup(null); var queue = CreateQueueWithChoiceFor(target); - target.BattleData!.Battle.ChoiceQueue.Returns(queue); + target.Battle.ChoiceQueue.Returns(queue); random.EffectChance(10, move, target, 0).Returns(true, true); // Act @@ -147,7 +145,7 @@ public class FireFangTests // Arrange var (script, move, _, target, random, targetVolatile) = CreateTestSetup(null); var queue = CreateQueueWithChoiceFor(target); - target.BattleData!.Battle.ChoiceQueue.Returns(queue); + target.Battle.ChoiceQueue.Returns(queue); // First roll (burn) fails, second roll (flinch) succeeds. random.EffectChance(10, move, target, 0).Returns(false, true); @@ -170,8 +168,8 @@ public class FireFangTests // Arrange var (script, move, _, target, random, targetVolatile) = CreateTestSetup(null); // The queue only holds a choice for some other Pokémon; the target's choice already executed. - var queue = CreateQueueWithChoiceFor(Substitute.For()); - target.BattleData!.Battle.ChoiceQueue.Returns(queue); + var queue = CreateQueueWithChoiceFor(Substitute.For()); + target.Battle.ChoiceQueue.Returns(queue); random.EffectChance(10, move, target, 0).Returns(true, true); // Act @@ -199,26 +197,4 @@ public class FireFangTests target.Received(1).SetStatus("burned", user); targetVolatile.DidNotReceive().Add(Arg.Any