Move all battle state from IPokemon to an ephemeral IBattlePokemon wrapper

This commit is contained in:
2026-08-28 15:20:26 +02:00
parent 942be8eaeb
commit 8a2733a0a9
859 changed files with 4408 additions and 5331 deletions

View File

@@ -163,19 +163,19 @@ public static class TestCommandRunner
var pokemon1 = battle.Sides[0].Pokemon[0]; var pokemon1 = battle.Sides[0].Pokemon[0];
while (pokemon1 is null && !battle.HasEnded) while (pokemon1 is null && !battle.HasEnded)
{ {
pokemon1 = battle.Parties[0].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable); var replacement1 = battle.Parties[0].BattlePokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable);
if (pokemon1 is null) if (replacement1 is null)
throw new InvalidOperationException("No usable Pokémon found in party 1."); 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]; pokemon1 = battle.Sides[0].Pokemon[0];
} }
var pokemon2 = battle.Sides[1].Pokemon[0]; var pokemon2 = battle.Sides[1].Pokemon[0];
while (pokemon2 is null && !battle.HasEnded) while (pokemon2 is null && !battle.HasEnded)
{ {
pokemon2 = battle.Parties[1].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable); var replacement2 = battle.Parties[1].BattlePokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable);
if (pokemon2 is null) if (replacement2 is null)
throw new InvalidOperationException("No usable Pokémon found in party 2."); 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]; pokemon2 = battle.Sides[1].Pokemon[0];
} }
if (pokemon1 is null || pokemon2 is null) if (pokemon1 is null || pokemon2 is null)

View File

@@ -14,7 +14,7 @@ public static class AIHelpers
/// <summary> /// <summary>
/// Estimates the amount of damage that will be done by a move against a target. /// Estimates the amount of damage that will be done by a move against a target.
/// </summary> /// </summary>
public static uint CalculateDamageEstimation(IMoveData move, IPokemon user, IPokemon target, public static uint CalculateDamageEstimation(IMoveData move, IBattlePokemon user, IBattlePokemon target,
IDynamicLibrary library) IDynamicLibrary library)
{ {
var hitData = new CustomHitData var hitData = new CustomHitData

View File

@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
public class AIMoveState public class AIMoveState
{ {
/// <inheritdoc cref="AIMoveState" /> /// <inheritdoc cref="AIMoveState" />
public AIMoveState(IPokemon user, IMoveData move) public AIMoveState(IBattlePokemon user, IMoveData move)
{ {
User = user; User = user;
Move = move; Move = move;
@@ -18,7 +18,7 @@ public class AIMoveState
/// <summary> /// <summary>
/// The user that's being wrapper /// The user that's being wrapper
/// </summary> /// </summary>
public IPokemon User { get; } public IBattlePokemon User { get; }
/// <summary> /// <summary>
/// The move that's being wrapper /// The move that's being wrapper

View File

@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
public partial class ExplicitAI 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) [NotNullWhen(true)] out ITurnChoice? choice)
{ {
choice = null; choice = null;
@@ -17,13 +17,13 @@ public partial class ExplicitAI
return false; return false;
if (TrainerHighSkill) 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); var foeCanAct = opponentSide.Pokemon.WhereNotNull().Any(CanAttack);
if (!foeCanAct) if (!foeCanAct)
return false; return false;
} }
var party = battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex( 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) if (party is null)
return false; return false;
var usablePokemon = party.GetUsablePokemonNotInField().ToList(); var usablePokemon = party.GetUsablePokemonNotInField().ToList();
@@ -44,20 +44,20 @@ public partial class ExplicitAI
if (!shouldSwitch) if (!shouldSwitch)
return false; return false;
} }
var battleSide = pokemon.BattleData!.BattleSide; var battleSide = pokemon.BattleSide;
var bestReplacement = ChooseBestReplacementPokemon(terribleMoves, usablePokemon, battleSide); var bestReplacement = ChooseBestReplacementPokemon(terribleMoves, usablePokemon, battleSide);
if (bestReplacement is null) if (bestReplacement is null)
{ {
AILogging.LogInformation( 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; return false;
} }
choice = new SwitchChoice(pokemon, bestReplacement); choice = new SwitchChoice(pokemon, bestReplacement);
return true; return true;
} }
private IPokemon? ChooseBestReplacementPokemon(bool terribleMoves, IReadOnlyList<IPokemon> usablePokemon, private IBattlePokemon? ChooseBestReplacementPokemon(bool terribleMoves,
IBattleSide battleSide) IReadOnlyList<IBattlePokemon> usablePokemon, IBattleSide battleSide)
{ {
var options = usablePokemon.Where((_, index) => var options = usablePokemon.Where((_, index) =>
{ {
@@ -84,7 +84,7 @@ public partial class ExplicitAI
private static readonly StringKey ToxicSpikesName = "toxic_spikes"; private static readonly StringKey ToxicSpikesName = "toxic_spikes";
private static readonly StringKey StickyWebName = "sticky_web"; 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 score = 0;
var types = pokemon.Types; var types = pokemon.Types;
@@ -107,7 +107,7 @@ public partial class ExplicitAI
var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide); var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide);
foreach (var foe in opponentSide.Pokemon.WhereNotNull()) 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) if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status)
continue; continue;
var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType; var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType;
@@ -134,22 +134,19 @@ public partial class ExplicitAI
/// <summary> /// <summary>
/// Calculates the expected entry hazard damage for a given Pokémon on a given battle side. /// Calculates the expected entry hazard damage for a given Pokémon on a given battle side.
/// </summary> /// </summary>
public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side) public static uint CalculateEntryHazardDamage(IBattlePokemon pokemon, IBattleSide side)
{ {
var damage = 0u; var damage = 0u;
side.RunScriptHook<IAIInfoScriptExpectedEntryDamage>(x => x.ExpectedEntryDamage(pokemon, ref damage)); side.RunScriptHook<IAIInfoScriptExpectedEntryDamage>(x => x.ExpectedEntryDamage(pokemon, ref damage));
return damage; return damage;
} }
private static bool CanSwitch(IPokemon pokemon) private static bool CanSwitch(IBattlePokemon pokemon)
{ {
var battleData = pokemon.BattleData; if (pokemon.Battle.IsWildBattle)
if (battleData == null)
return false; return false;
if (battleData.Battle.IsWildBattle) var partyForIndex = pokemon.Battle.Parties.FirstOrDefault(x =>
return false; x.IsResponsibleForIndex(new ResponsibleIndex(pokemon.SideIndex, pokemon.Position)));
var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x =>
x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position)));
return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField(); return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField();
} }
} }

View File

@@ -37,7 +37,7 @@ public partial class ExplicitAI
private static readonly StringKey KomalaName = "komala"; private static readonly StringKey KomalaName = "komala";
private static readonly StringKey MiniorName = "minior"; 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) if (battle.TerrainName == MistyTerrainName)
return false; return false;
@@ -60,7 +60,7 @@ public partial class ExplicitAI
return true; 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) if (pokemon.ActiveAbility == null)
return false; return false;

View File

@@ -118,7 +118,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
public IRandom Random => _random; public IRandom Random => _random;
/// <inheritdoc /> /// <inheritdoc />
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon)
{ {
if (battle.HasForcedTurn(pokemon, out var choice)) if (battle.HasForcedTurn(pokemon, out var choice))
return choice; return choice;
@@ -131,8 +131,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var moveChoices = GetMoveScores(pokemon, battle); var moveChoices = GetMoveScores(pokemon, battle);
if (moveChoices.Count == 0) if (moveChoices.Count == 0)
{ {
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0); var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position); return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position);
} }
var maxScore = moveChoices.Max(x => x.score); var maxScore = moveChoices.Max(x => x.score);
if (TrainerHighSkill && CanSwitch(pokemon)) if (TrainerHighSkill && CanSwitch(pokemon))
@@ -144,7 +144,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
if (!badMoves && _random.GetInt(100) < 25) if (!badMoves && _random.GetInt(100) < 25)
badMoves = true; badMoves = true;
} }
else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 && else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.TurnsOnField > 2 &&
_random.GetInt(100) < 80) _random.GetInt(100) < 80)
{ {
badMoves = true; badMoves = true;
@@ -164,8 +164,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var totalScore = considerChoices.Sum(x => x.Item2); var totalScore = considerChoices.Sum(x => x.Item2);
if (totalScore == 0) if (totalScore == 0)
{ {
var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0); var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position); return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position);
} }
var initialRandomValue = _random.GetFloat(0, totalScore); var initialRandomValue = _random.GetFloat(0, totalScore);
var randomValue = initialRandomValue; var randomValue = initialRandomValue;
@@ -177,15 +177,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var (index, _, targetIndex) = considerChoices[i].x; var (index, _, targetIndex) = considerChoices[i].x;
var learnedMove = pokemon.Moves[index]; 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) if (targetIndex == -1)
targetIndex = pokemon.BattleData.Position; targetIndex = pokemon.Position;
return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex); return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex);
} }
throw new InvalidOperationException("No valid move choice found. This should not happen."); 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)>(); 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)) 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 // TODO: get redirected target
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull()) foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{ {
var battleData = pokemon.BattleData; if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user))
if (battleData == null)
continue; continue;
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user)) if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex)
continue;
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
{ {
continue; continue;
} }
var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle); var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle);
AddMoveToChoices(index, score, battleData.Position); AddMoveToChoices(index, score, pokemon.Position);
} }
} }
else else
{ {
var targets = new List<IPokemon>(); var targets = new List<IBattlePokemon>();
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull()) foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{ {
var battleData = pokemon.BattleData; if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user))
if (battleData == null)
continue; continue;
if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user)) if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex)
continue;
if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
{ {
continue; 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")) 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 SubstituteName = new("substitute");
private static readonly StringKey InfiltratorName = new("infiltrator"); 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, if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this,
aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target))) aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target)))
return true; return true;
if (aiMove.Move.Priority > 0) 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 // Psychic Terrain makes all priority moves fail if the target is affected
if (battle.TerrainName == PsychicTerrainName && !target.IsFloating) if (battle.TerrainName == PsychicTerrainName && !target.IsFloating)
@@ -348,7 +343,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return true; return true;
} }
// Dazzling and Queenly Majesty prevent priority moves from being used against the Pokémon with those abilities // 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) x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true)
{ {
return true; return true;
@@ -362,7 +357,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0) if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0)
return true; return true;
if (user.ActiveAbility?.Name == PranksterName && aiMove.Move.Category == MoveCategory.Status && 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; return true;
if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating) if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating)
return true; return true;
@@ -375,7 +370,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return false; return false;
} }
private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList<IPokemon>? targets = null) private int GetMoveScore(IBattlePokemon user, AIMoveState aiMove, IBattle battle,
IReadOnlyList<IBattlePokemon>? targets = null)
{ {
var score = MoveBaseScore; var score = MoveBaseScore;
if (targets != null) if (targets != null)
@@ -411,7 +407,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return score; 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)) if (_skillFlags.CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
{ {
@@ -427,8 +424,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score); _handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score);
} }
if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex && if (aiMove.Move.Target.TargetsFoe() && target.SideIndex == user.SideIndex && target.Position != user.Position)
target.BattleData?.Position != user.BattleData?.Position)
{ {
if (score == MoveUselessScore) if (score == MoveUselessScore)
return -1; return -1;
@@ -442,7 +438,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey OvercoatName = new("overcoat"); private static readonly StringKey OvercoatName = new("overcoat");
private static readonly StringKey SafetyGogglesName = new("safety_goggles"); 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)) if (pokemon.Types.Any(x => x.Name == GrassName))
return false; return false;
@@ -456,7 +452,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey TruantName = "truant"; private static readonly StringKey TruantName = "truant";
private static readonly StringKey TruantEffectName = "truant_effect"; 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")) if (pokemon.Volatile.Contains("requires_recharge"))
return false; return false;

View File

@@ -6,7 +6,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
/// <summary> /// <summary>
/// An option where a move is used against a target /// An option where a move is used against a target
/// </summary> /// </summary>
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);
/// <summary> /// <summary>
/// A function that takes an explicit AI and a move option and returns a boolean value. /// 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);
/// <summary> /// <summary>
/// A function for returning whether a Pokemon should switch. /// A function for returning whether a Pokemon should switch.
/// </summary> /// </summary>
public delegate bool AISwitchBoolHandler(IExplicitAI ai, IPokemon pokemon, IBattle battle, public delegate bool AISwitchBoolHandler(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IPokemon> reserves); IReadOnlyList<IBattlePokemon> reserves);
/// <summary> /// <summary>
/// A function for returning the base power of a move. /// A function for returning the base power of a move.
@@ -112,7 +112,7 @@ public interface IReadOnlyExplicitAIHandlers
/// <summary> /// <summary>
/// Indicates whether a Pokemon should switch into another Pokemon /// Indicates whether a Pokemon should switch into another Pokemon
/// </summary> /// </summary>
bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves); bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, IReadOnlyList<IBattlePokemon> reserves);
/// <summary> /// <summary>
/// Functions that indicate whether a Pokemon should NOT switch /// Functions that indicate whether a Pokemon should NOT switch
@@ -122,7 +122,8 @@ public interface IReadOnlyExplicitAIHandlers
/// <summary> /// <summary>
/// Indicates whether a Pokemon should NOT switch into another Pokemon /// Indicates whether a Pokemon should NOT switch into another Pokemon
/// </summary> /// </summary>
bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves); bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves);
/// <summary> /// <summary>
/// Scores abilities /// Scores abilities
@@ -246,7 +247,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
ShouldSwitchFunctions; ShouldSwitchFunctions;
/// <inheritdoc /> /// <inheritdoc />
public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves) public bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves)
{ {
var shouldSwitch = false; var shouldSwitch = false;
foreach (var (_, handler) in ShouldSwitchFunctions) foreach (var (_, handler) in ShouldSwitchFunctions)
@@ -272,7 +274,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = []; public FunctionHandlerDictionary<AIScoreMoveHandler> AbilityRanking = [];
/// <inheritdoc /> /// <inheritdoc />
public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves) public bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
IReadOnlyList<IBattlePokemon> reserves)
{ {
var shouldNotSwitch = false; var shouldNotSwitch = false;
foreach (var (_, handler) in ShouldNotSwitchFunctions) foreach (var (_, handler) in ShouldNotSwitchFunctions)

View File

@@ -15,9 +15,9 @@ public class HighestDamageAI : PokemonAI
} }
/// <inheritdoc /> /// <inheritdoc />
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 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))) var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0)))
.ToList(); .ToList();

View File

@@ -14,5 +14,5 @@ public class PassTurnAI : PokemonAI
} }
/// <inheritdoc /> /// <inheritdoc />
public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) => new PassChoice(pokemon); public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) => new PassChoice(pokemon);
} }

View File

@@ -26,70 +26,69 @@ public abstract class PokemonAI
/// <summary> /// <summary>
/// Gets the choice for the Pokémon. /// Gets the choice for the Pokémon.
/// </summary> /// </summary>
public abstract ITurnChoice GetChoice(IBattle battle, IPokemon pokemon); public abstract ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon);
/// <summary> /// <summary>
/// For a given user and move, returns the valid targets for that move. /// For a given user and move, returns the valid targets for that move.
/// </summary> /// </summary>
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) switch (move.MoveData.Target)
{ {
case MoveTarget.Adjacent: case MoveTarget.Adjacent:
yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); yield return (GetOppositeSide(user.SideIndex), user.Position);
break; break;
case MoveTarget.AdjacentAlly: case MoveTarget.AdjacentAlly:
if (userBattleData.Position > 0) if (user.Position > 0)
yield return (userBattleData.SideIndex, (byte)(userBattleData.Position - 1)); yield return (user.SideIndex, (byte)(user.Position - 1));
if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1) if (user.Battle.PositionsPerSide > user.Position + 1)
yield return (userBattleData.SideIndex, (byte)(userBattleData.Position + 1)); yield return (user.SideIndex, (byte)(user.Position + 1));
break; break;
case MoveTarget.AdjacentAllySelf: case MoveTarget.AdjacentAllySelf:
if (userBattleData.Position > 0) if (user.Position > 0)
yield return (userBattleData.SideIndex, (byte)(userBattleData.Position - 1)); yield return (user.SideIndex, (byte)(user.Position - 1));
if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1) if (user.Battle.PositionsPerSide > user.Position + 1)
yield return (userBattleData.SideIndex, (byte)(userBattleData.Position + 1)); yield return (user.SideIndex, (byte)(user.Position + 1));
yield return (userBattleData.SideIndex, userBattleData.Position); yield return (user.SideIndex, user.Position);
break; break;
case MoveTarget.AdjacentOpponent: case MoveTarget.AdjacentOpponent:
yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); yield return (GetOppositeSide(user.SideIndex), user.Position);
if (userBattleData.Position > 0) if (user.Position > 0)
yield return (GetOppositeSide(userBattleData.SideIndex), (byte)(userBattleData.Position - 1)); yield return (GetOppositeSide(user.SideIndex), (byte)(user.Position - 1));
if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1) if (user.Battle.PositionsPerSide > user.Position + 1)
yield return (GetOppositeSide(userBattleData.SideIndex), (byte)(userBattleData.Position + 1)); yield return (GetOppositeSide(user.SideIndex), (byte)(user.Position + 1));
break; break;
case MoveTarget.All: case MoveTarget.All:
yield return (userBattleData.SideIndex, userBattleData.Position); yield return (user.SideIndex, user.Position);
break; break;
case MoveTarget.AllAdjacent: case MoveTarget.AllAdjacent:
yield return (userBattleData.SideIndex, userBattleData.Position); yield return (user.SideIndex, user.Position);
break; break;
case MoveTarget.AllAdjacentOpponent: case MoveTarget.AllAdjacentOpponent:
yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); yield return (GetOppositeSide(user.SideIndex), user.Position);
break; break;
case MoveTarget.AllAlly: case MoveTarget.AllAlly:
yield return (userBattleData.SideIndex, userBattleData.Position); yield return (user.SideIndex, user.Position);
break; break;
case MoveTarget.AllOpponent: case MoveTarget.AllOpponent:
yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); yield return (GetOppositeSide(user.SideIndex), user.Position);
break; break;
case MoveTarget.Any: case MoveTarget.Any:
foreach (var side in userBattleData.Battle.Sides) foreach (var side in user.Battle.Sides)
{ {
foreach (var pokemon in side.Pokemon) foreach (var pokemon in side.Pokemon)
{ {
if (pokemon?.BattleData == null) if (pokemon == null)
continue; continue;
yield return (side.Index, pokemon.BattleData!.Position); yield return (side.Index, pokemon.Position);
} }
} }
break; break;
case MoveTarget.RandomOpponent: case MoveTarget.RandomOpponent:
yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position); yield return (GetOppositeSide(user.SideIndex), user.Position);
break; break;
case MoveTarget.SelfUse: case MoveTarget.SelfUse:
yield return (userBattleData.SideIndex, userBattleData.Position); yield return (user.SideIndex, user.Position);
break; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();

View File

@@ -18,9 +18,9 @@ public class PrescientAI : PokemonAI
} }
/// <inheritdoc /> /// <inheritdoc />
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))) var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0)))
.ToList(); .ToList();
@@ -34,13 +34,13 @@ public class PrescientAI : PokemonAI
} }
private static IEnumerable<(ITurnChoice Choice, float Score)> ScoreChoices(IBattle battle, private static IEnumerable<(ITurnChoice Choice, float Score)> ScoreChoices(IBattle battle,
IReadOnlyList<ILearnedMove> moves, IPokemon pokemon) IReadOnlyList<ILearnedMove> 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()) foreach (var learnedMoveOriginal in moves.WhereNotNull())
{ {
var battleClone = battle.DeepClone(); 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() var learnedMove = pokemonClone.Moves.WhereNotNull()
.First(m => m.MoveData.Name == learnedMoveOriginal.MoveData.Name); .First(m => m.MoveData.Name == learnedMoveOriginal.MoveData.Name);
var choice = new MoveChoice(pokemonClone, learnedMove, opponentSide, 0); var choice = new MoveChoice(pokemonClone, learnedMove, opponentSide, 0);
@@ -57,17 +57,16 @@ public class PrescientAI : PokemonAI
} }
if (battleClone.TrySetChoice(choice)) if (battleClone.TrySetChoice(choice))
{ {
var score = CalculateScore(battleClone.Parties[pokemon.BattleData.SideIndex], var score = CalculateScore(battleClone.Parties[pokemon.SideIndex], battleClone.Parties[opponentSide]);
battleClone.Parties[opponentSide]);
var realChoice = new MoveChoice(pokemon, learnedMoveOriginal, opponentSide, 0); var realChoice = new MoveChoice(pokemon, learnedMoveOriginal, opponentSide, 0);
yield return (realChoice, score); 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]; var opponent = battle.Sides[opponentSide].Pokemon[0];
if (opponent is null) if (opponent is null)
{ {

View File

@@ -19,7 +19,7 @@ public class RandomAI : PokemonAI
} }
/// <inheritdoc /> /// <inheritdoc />
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(); var moves = pokemon.Moves.WhereNotNull().Where(x => x.CurrentPp > 0).ToList();
while (moves.Count > 0) while (moves.Count > 0)
@@ -28,7 +28,7 @@ public class RandomAI : PokemonAI
var targets = GetValidTargetsForMove(pokemon, move).ToArray(); var targets = GetValidTargetsForMove(pokemon, move).ToArray();
if (move.MoveData.Category is MoveCategory.Physical or MoveCategory.Special) 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) if (targets.Length == 0)
{ {
@@ -43,7 +43,7 @@ public class RandomAI : PokemonAI
} }
moves.Remove(move); moves.Remove(move);
} }
return battle.Library.MiscLibrary.ReplacementChoice(pokemon, return battle.Library.MiscLibrary.ReplacementChoice(pokemon, pokemon.SideIndex == 0 ? (byte)1 : (byte)0,
pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0, pokemon.BattleData.Position); pokemon.Position);
} }
} }

View File

@@ -15,7 +15,7 @@ public static class MoveTurnExecutor
{ {
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice) internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
{ {
moveChoice.User.BattleData!.LastMoveChoice = moveChoice; moveChoice.User.LastMoveChoice = moveChoice;
var chosenMove = moveChoice.ChosenMove; var chosenMove = moveChoice.ChosenMove;
var useMove = chosenMove.MoveData; var useMove = chosenMove.MoveData;
@@ -126,7 +126,7 @@ public static class MoveTurnExecutor
private static readonly ThreadLocal<List<TypeIdentifier>> TypeListCache = new(() => []); private static readonly ThreadLocal<List<TypeIdentifier>> 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; var failed = false;
target.RunScriptHook<IScriptFailIncomingMove>(x => x.FailIncomingMove(executingMove, target, ref failed)); target.RunScriptHook<IScriptFailIncomingMove>(x => x.FailIncomingMove(executingMove, target, ref failed));

View File

@@ -11,7 +11,8 @@ public static class TargetResolver
/// <summary> /// <summary>
/// Get the targets of a move based on the target type, and the selected side and position to target. /// Get the targets of a move based on the target type, and the selected side and position to target.
/// </summary> /// </summary>
public static IReadOnlyList<IPokemon?> ResolveTargets(IBattle battle, byte side, byte position, MoveTarget target) public static IReadOnlyList<IBattlePokemon?> ResolveTargets(IBattle battle, byte side, byte position,
MoveTarget target)
{ {
return target switch return target switch
{ {
@@ -29,13 +30,10 @@ public static class TargetResolver
/// <summary> /// <summary>
/// Validates whether a given target is valid for a move choice. Returns true if the target is valid. /// Validates whether a given target is valid for a move choice. Returns true if the target is valid.
/// </summary> /// </summary>
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; var userSide = user.SideIndex;
if (userBattleData == null) var userPosition = user.Position;
throw new ArgumentNullException(nameof(user.BattleData));
var userSide = userBattleData.SideIndex;
var userPosition = userBattleData.Position;
switch (target) switch (target)
{ {
@@ -80,7 +78,7 @@ public static class TargetResolver
throw new ArgumentOutOfRangeException(nameof(target), target, null); throw new ArgumentOutOfRangeException(nameof(target), target, null);
} }
private static IReadOnlyList<IPokemon?> GetAllTargets(IBattle battle) => private static IReadOnlyList<IBattlePokemon?> GetAllTargets(IBattle battle) =>
battle.Sides.SelectMany(x => x.Pokemon).ToList(); battle.Sides.SelectMany(x => x.Pokemon).ToList();
private static byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0; 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, /// 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. /// the Pokémon left of it, the Pokémon right of it, and the Pokémon opposite of it.
/// </summary> /// </summary>
private static IReadOnlyList<IPokemon?> GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position) private static IReadOnlyList<IBattlePokemon?> GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position)
{ {
var left = position - 1; var left = position - 1;
var right = position + 1; var right = position + 1;
@@ -123,7 +121,7 @@ public static class TargetResolver
]; ];
} }
private static IReadOnlyList<IPokemon?> GetAllAdjacent(IBattle battle, byte side, byte position) private static IReadOnlyList<IBattlePokemon?> GetAllAdjacent(IBattle battle, byte side, byte position)
{ {
var left = position - 1; var left = position - 1;
var right = position + 1; var right = position + 1;

View File

@@ -85,7 +85,7 @@ public static class TurnRunner
return; return;
if (!choice.User.IsUsable) if (!choice.User.IsUsable)
return; return;
if (choice.User.BattleData?.IsOnBattlefield != true) if (!choice.User.IsOnBattlefield)
return; return;
switch (choice) switch (choice)
{ {
@@ -108,9 +108,6 @@ public static class TurnRunner
private static void ExecuteSwitchChoice(IBattle battle, ISwitchChoice fleeChoice) private static void ExecuteSwitchChoice(IBattle battle, ISwitchChoice fleeChoice)
{ {
var user = fleeChoice.User; var user = fleeChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return;
var preventSwitch = false; var preventSwitch = false;
fleeChoice.RunScriptHook<IScriptPreventSelfSwitch>(script => fleeChoice.RunScriptHook<IScriptPreventSelfSwitch>(script =>
script.PreventSelfSwitch(fleeChoice, ref preventSwitch)); script.PreventSelfSwitch(fleeChoice, ref preventSwitch));
@@ -118,7 +115,7 @@ public static class TurnRunner
return; return;
foreach (var side in battle.Sides) foreach (var side in battle.Sides)
{ {
if (side.Index == battleData.SideIndex) if (side.Index == user.SideIndex)
continue; continue;
foreach (var pokemon in side.Pokemon.WhereNotNull()) foreach (var pokemon in side.Pokemon.WhereNotNull())
{ {
@@ -129,16 +126,12 @@ public static class TurnRunner
} }
} }
user.Volatile.Clear(); user.Volatile.Clear();
var userSide = battle.Sides[battleData.SideIndex]; user.BattleSide.SwapPokemon(user.Position, fleeChoice.SwitchTo);
userSide.SwapPokemon(battleData.Position, fleeChoice.SwitchTo);
} }
private static void ExecuteFleeChoice(IBattle battle, IFleeChoice fleeChoice) private static void ExecuteFleeChoice(IBattle battle, IFleeChoice fleeChoice)
{ {
var user = fleeChoice.User; var user = fleeChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return;
if (!battle.CanFlee) if (!battle.CanFlee)
return; return;
@@ -150,7 +143,7 @@ public static class TurnRunner
foreach (var side in battle.Sides) foreach (var side in battle.Sides)
{ {
if (side.Index == battleData.SideIndex) if (side.Index == user.SideIndex)
continue; continue;
foreach (var pokemon in side.Pokemon.WhereNotNull()) foreach (var pokemon in side.Pokemon.WhereNotNull())
{ {
@@ -167,8 +160,7 @@ public static class TurnRunner
return; return;
} }
var userSide = battle.Sides[battleData.SideIndex]; user.BattleSide.MarkAsFled();
userSide.MarkAsFled();
battle.EventHook.Invoke(new FleeEvent(user, true)); battle.EventHook.Invoke(new FleeEvent(user, true));
battle.ValidateBattleState(); battle.ValidateBattleState();
} }
@@ -176,9 +168,6 @@ public static class TurnRunner
private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice) private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice)
{ {
var user = itemChoice.User; var user = itemChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return;
var target = itemChoice.GetTargetPokemon(battle); var target = itemChoice.GetTargetPokemon(battle);
battle.EventHook.Invoke(new ItemUseEvent(user, itemChoice.Item)); battle.EventHook.Invoke(new ItemUseEvent(user, itemChoice.Item));
itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, user, battle, battle.EventHook); itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, user, battle, battle.EventHook);

View File

@@ -12,7 +12,7 @@ public record AbilityTriggerEvent : IEventData
/// <summary> /// <summary>
/// The Pokémon whose ability is being triggered. /// The Pokémon whose ability is being triggered.
/// </summary> /// </summary>
public IPokemon Pokemon { get; } public IBattlePokemon Pokemon { get; }
/// <summary> /// <summary>
/// The ability that is being triggered for the Pokémon. /// The ability that is being triggered for the Pokémon.
@@ -22,7 +22,7 @@ public record AbilityTriggerEvent : IEventData
public Dictionary<StringKey, object?>? Metadata { get; init; } = null; public Dictionary<StringKey, object?>? Metadata { get; init; } = null;
/// <inheritdoc cref="AbilityTriggerEvent"/> /// <inheritdoc cref="AbilityTriggerEvent"/>
public AbilityTriggerEvent(IPokemon pokemon) public AbilityTriggerEvent(IBattlePokemon pokemon)
{ {
Pokemon = pokemon; Pokemon = pokemon;
Ability = pokemon.ActiveAbility; Ability = pokemon.ActiveAbility;

View File

@@ -10,7 +10,7 @@ namespace PkmnLib.Dynamic.Events;
public class CaptureAttemptEvent : IEventData public class CaptureAttemptEvent : IEventData
{ {
/// <inheritdoc cref="CaptureAttemptEvent"/> /// <inheritdoc cref="CaptureAttemptEvent"/>
public CaptureAttemptEvent(IPokemon target, CaptureResult result, IItem captureItem) public CaptureAttemptEvent(IBattlePokemon target, CaptureResult result, IItem captureItem)
{ {
Target = target; Target = target;
Result = result; Result = result;
@@ -20,7 +20,7 @@ public class CaptureAttemptEvent : IEventData
/// <summary> /// <summary>
/// The Pokémon that is being captured. /// The Pokémon that is being captured.
/// </summary> /// </summary>
public IPokemon Target { get; init; } public IBattlePokemon Target { get; init; }
/// <summary> /// <summary>
/// The result of the capture attempt. /// The result of the capture attempt.

View File

@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public record DamageEvent : IEventData public record DamageEvent : IEventData
{ {
/// <inheritdoc cref="DamageEvent"/> /// <inheritdoc cref="DamageEvent"/>
public DamageEvent(IPokemon pokemon, uint previousHealth, uint newHealth, DamageSource source) public DamageEvent(IBattlePokemon pokemon, uint previousHealth, uint newHealth, DamageSource source)
{ {
Pokemon = pokemon; Pokemon = pokemon;
PreviousHealth = previousHealth; PreviousHealth = previousHealth;
@@ -19,7 +19,7 @@ public record DamageEvent : IEventData
/// <summary> /// <summary>
/// The Pokemon that took damage. /// The Pokemon that took damage.
/// </summary> /// </summary>
public IPokemon Pokemon { get; init; } public IBattlePokemon Pokemon { get; init; }
/// <summary> /// <summary>
/// The previous health of the Pokemon. /// The previous health of the Pokemon.

View File

@@ -5,11 +5,11 @@ namespace PkmnLib.Dynamic.Events;
public class DisplaySpeciesChangeEvent : IEventData public class DisplaySpeciesChangeEvent : IEventData
{ {
public IPokemon Pokemon { get; } public IBattlePokemon Pokemon { get; }
public ISpecies? Species { get; } public ISpecies? Species { get; }
public IForm? Form { get; } public IForm? Form { get; }
public DisplaySpeciesChangeEvent(IPokemon pokemon, ISpecies? species, IForm? form) public DisplaySpeciesChangeEvent(IBattlePokemon pokemon, ISpecies? species, IForm? form)
{ {
Pokemon = pokemon; Pokemon = pokemon;
Species = species; Species = species;

View File

@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public class FaintEvent : IEventData public class FaintEvent : IEventData
{ {
/// <inheritdoc cref="FaintEvent"/> /// <inheritdoc cref="FaintEvent"/>
public FaintEvent(IPokemon pokemon) public FaintEvent(IBattlePokemon pokemon)
{ {
Pokemon = pokemon; Pokemon = pokemon;
} }
@@ -16,7 +16,7 @@ public class FaintEvent : IEventData
/// <summary> /// <summary>
/// The Pokemon that fainted. /// The Pokemon that fainted.
/// </summary> /// </summary>
public IPokemon Pokemon { get; init; } public IBattlePokemon Pokemon { get; init; }
/// <inheritdoc /> /// <inheritdoc />
public EventBatchId BatchId { get; init; } = new(); public EventBatchId BatchId { get; init; } = new();

View File

@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public class FleeEvent : IEventData public class FleeEvent : IEventData
{ {
/// <inheritdoc cref="FleeEvent"/> /// <inheritdoc cref="FleeEvent"/>
public FleeEvent(IPokemon pokemon, bool success) public FleeEvent(IBattlePokemon pokemon, bool success)
{ {
Pokemon = pokemon; Pokemon = pokemon;
Success = success; Success = success;
@@ -17,7 +17,7 @@ public class FleeEvent : IEventData
/// <summary> /// <summary>
/// The Pokémon that attempted to flee. /// The Pokémon that attempted to flee.
/// </summary> /// </summary>
public IPokemon Pokemon { get; } public IBattlePokemon Pokemon { get; }
/// <summary> /// <summary>
/// Indicates whether the flee attempt was successful. /// Indicates whether the flee attempt was successful.

View File

@@ -5,13 +5,13 @@ namespace PkmnLib.Dynamic.Events;
public record ItemUseEvent : IEventData public record ItemUseEvent : IEventData
{ {
public ItemUseEvent(IPokemon pokemon, IItem itemUsed) public ItemUseEvent(IBattlePokemon pokemon, IItem itemUsed)
{ {
Pokemon = pokemon; Pokemon = pokemon;
ItemUsed = itemUsed; ItemUsed = itemUsed;
} }
public IPokemon Pokemon { get; set; } public IBattlePokemon Pokemon { get; set; }
public IItem ItemUsed { get; set; } public IItem ItemUsed { get; set; }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -20,10 +20,10 @@ public class MoveHitEvent : IEventData
/// <summary> /// <summary>
/// The target of the move. /// The target of the move.
/// </summary> /// </summary>
public IPokemon Target { get; } public IBattlePokemon Target { get; }
/// <inheritdoc cref="MoveHitEvent"/> /// <inheritdoc cref="MoveHitEvent"/>
public MoveHitEvent(IExecutingMove executingMove, IHitData hitData, IPokemon target) public MoveHitEvent(IExecutingMove executingMove, IHitData hitData, IBattlePokemon target)
{ {
ExecutingMove = executingMove; ExecutingMove = executingMove;
HitData = hitData; HitData = hitData;

View File

@@ -5,9 +5,9 @@ namespace PkmnLib.Dynamic.Events;
public class MoveInvulnerableEvent : IEventData public class MoveInvulnerableEvent : IEventData
{ {
public IExecutingMove ExecutingMove { get; } 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; ExecutingMove = executingMove;
Target = target; Target = target;

View File

@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.Events;
public class StatBoostEvent : IEventData public class StatBoostEvent : IEventData
{ {
/// <inheritdoc cref="StatBoostEvent" /> /// <inheritdoc cref="StatBoostEvent" />
public StatBoostEvent(IPokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost) public StatBoostEvent(IBattlePokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost)
{ {
Pokemon = pokemon; Pokemon = pokemon;
Statistic = statistic; Statistic = statistic;
@@ -20,7 +20,7 @@ public class StatBoostEvent : IEventData
/// <summary> /// <summary>
/// The Pokemon that had its stat boosted. /// The Pokemon that had its stat boosted.
/// </summary> /// </summary>
public IPokemon Pokemon { get; } public IBattlePokemon Pokemon { get; }
/// <summary> /// <summary>
/// The statistic that was boosted. /// The statistic that was boosted.

View File

@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.Events;
public record StatusChangeEvent : IEventData public record StatusChangeEvent : IEventData
{ {
/// <inheritdoc cref="StatusChangeEvent"/> /// <inheritdoc cref="StatusChangeEvent"/>
public StatusChangeEvent(IPokemon pokemon, StringKey? previousStatus, StringKey? newStatus) public StatusChangeEvent(IBattlePokemon pokemon, StringKey? previousStatus, StringKey? newStatus)
{ {
Pokemon = pokemon; Pokemon = pokemon;
PreviousStatus = previousStatus; PreviousStatus = previousStatus;
@@ -19,7 +19,7 @@ public record StatusChangeEvent : IEventData
/// <summary> /// <summary>
/// The Pokémon whose status has changed. /// The Pokémon whose status has changed.
/// </summary> /// </summary>
public IPokemon Pokemon { get; } public IBattlePokemon Pokemon { get; }
/// <summary> /// <summary>
/// The new status of the Pokémon after the change. /// The new status of the Pokémon after the change.

View File

@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public class SwitchEvent : IEventData public class SwitchEvent : IEventData
{ {
/// <inheritdoc cref="SwitchEvent"/> /// <inheritdoc cref="SwitchEvent"/>
public SwitchEvent(byte sideIndex, byte position, IPokemon? pokemon) public SwitchEvent(byte sideIndex, byte position, IBattlePokemon? pokemon)
{ {
SideIndex = sideIndex; SideIndex = sideIndex;
Position = position; Position = position;
@@ -28,7 +28,7 @@ public class SwitchEvent : IEventData
/// <summary> /// <summary>
/// The Pokémon that is switching in. If null, no Pokémon is switching in, and the slot is empty after the switch. /// The Pokémon that is switching in. If null, no Pokémon is switching in, and the slot is empty after the switch.
/// </summary> /// </summary>
public IPokemon? Pokemon { get; init; } public IBattlePokemon? Pokemon { get; init; }
/// <inheritdoc /> /// <inheritdoc />
public EventBatchId BatchId { get; init; } public EventBatchId BatchId { get; init; }

View File

@@ -21,15 +21,16 @@ public interface IBattleStatCalculator
/// <summary> /// <summary>
/// Calculate all the boosted stats of a Pokemon, including stat boosts. /// Calculate all the boosted stats of a Pokemon, including stat boosts.
/// </summary> /// </summary>
void CalculateBoostedStats(IPokemon pokemon, StatisticSet<uint> stats); void CalculateBoostedStats(IBattlePokemon pokemon, StatisticSet<uint> stats);
/// <summary> /// <summary>
/// Calculate a single boosted stat of a Pokemon, including stat boosts. /// Calculate a single boosted stat of a Pokemon, including stat boosts.
/// </summary> /// </summary>
uint CalculateBoostedStat(IPokemon pokemon, Statistic stat); uint CalculateBoostedStat(IBattlePokemon pokemon, Statistic stat);
/// <summary> /// <summary>
/// Calculates the accuracy for a move, taking into account any accuracy modifiers. /// Calculates the accuracy for a move, taking into account any accuracy modifiers.
/// </summary> /// </summary>
byte CalculateModifiedAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, byte moveAccuracy); byte CalculateModifiedAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
byte moveAccuracy);
} }

View File

@@ -46,5 +46,5 @@ public interface ICaptureLibrary
/// <summary> /// <summary>
/// Attempts to capture a Pokémon using a specified item (e.g., Poké Ball). /// Attempts to capture a Pokémon using a specified item (e.g., Poké Ball).
/// </summary> /// </summary>
CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random); CaptureResult TryCapture(IBattlePokemon target, IItem captureItem, IBattleRandom random);
} }

View File

@@ -11,16 +11,16 @@ public interface IDamageCalculator
/// <summary> /// <summary>
/// Calculate the damage for a given hit on a Pokemon. /// Calculate the damage for a given hit on a Pokemon.
/// </summary> /// </summary>
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); int targetCount, byte hitNumber, IHitData hitData);
/// <summary> /// <summary>
/// Calculate the base power for a given hit on a Pokemon. /// Calculate the base power for a given hit on a Pokemon.
/// </summary> /// </summary>
ushort GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData); ushort GetBasePower(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, IHitData hitData);
/// <summary> /// <summary>
/// Returns whether a specified hit should be critical or not. /// Returns whether a specified hit should be critical or not.
/// </summary> /// </summary>
bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber); bool IsCritical(IBattle battle, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber);
} }

View File

@@ -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 /// 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. /// moves left, yet wants to make a move.
/// </summary> /// </summary>
ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition); ITurnChoice ReplacementChoice(IBattlePokemon user, byte targetSide, byte targetPosition);
/// <summary> /// <summary>
/// Returns whether the given choice is the choice that is used when the user is unable to make a move choice. /// Returns whether the given choice is the choice that is used when the user is unable to make a move choice.

View File

@@ -92,7 +92,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
/// <summary> /// <summary>
/// Get a Pokemon on the battlefield, on a specific side and an index on that side. /// Get a Pokemon on the battlefield, on a specific side and an index on that side.
/// </summary> /// </summary>
IPokemon? GetPokemon(byte side, byte position); IBattlePokemon? GetPokemon(byte side, byte position);
/// <summary> /// <summary>
/// Returns whether a slot on the battlefield can still be filled. If no party is responsible /// 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 /// 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. /// is set in the out parameter. If it does not, this returns false and the out parameter is null.
/// </summary> /// </summary>
bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice); bool HasForcedTurn(IBattlePokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice);
/// <summary> /// <summary>
/// Checks whether a choice is actually possible. /// Checks whether a choice is actually possible.
@@ -202,6 +202,8 @@ public class BattleImpl : ScriptSource, IBattle
Sides = sides; Sides = sides;
Random = randomSeed.HasValue ? new BattleRandomImpl(randomSeed.Value) : new BattleRandomImpl(); Random = randomSeed.HasValue ? new BattleRandomImpl(randomSeed.Value) : new BattleRandomImpl();
EventHook = new EventHook(); EventHook = new EventHook();
foreach (var party in parties)
party.Initialize(this);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -248,12 +250,22 @@ public class BattleImpl : ScriptSource, IBattle
public BattleChoiceQueue? ChoiceQueue { get; private set; } public BattleChoiceQueue? ChoiceQueue { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public IPokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position]; public IBattlePokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position];
/// <inheritdoc /> /// <inheritdoc />
public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x => public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x =>
x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField()); x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField());
private readonly List<IPokemon> _capturedPokemon = [];
/// <summary>
/// Attaches battle-wide result data, such as the captured Pokémon, to a result.
/// </summary>
private BattleResult FinalizeResult(BattleResult result) => result with
{
CapturedPokemon = _capturedPokemon.ToList(),
};
/// <inheritdoc /> /// <inheritdoc />
public void ValidateBattleState() public void ValidateBattleState()
{ {
@@ -265,7 +277,7 @@ public class BattleImpl : ScriptSource, IBattle
{ {
if (side.HasFledBattle) if (side.HasFledBattle)
{ {
Result = BattleResult.Inconclusive; Result = FinalizeResult(BattleResult.Inconclusive);
HasEnded = true; HasEnded = true;
return; return;
} }
@@ -283,13 +295,13 @@ public class BattleImpl : ScriptSource, IBattle
// If every side is defeated, the battle is a draw // If every side is defeated, the battle is a draw
if (!survivingSideExists) if (!survivingSideExists)
{ {
Result = BattleResult.Inconclusive; Result = FinalizeResult(BattleResult.Inconclusive);
HasEnded = true; HasEnded = true;
return; return;
} }
// If only one side is left, that side has won // If only one side is left, that side has won
Result = BattleResult.Conclusive(survivingSide!.Index); Result = FinalizeResult(BattleResult.Conclusive(survivingSide!.Index));
HasEnded = true; HasEnded = true;
} }
@@ -297,22 +309,15 @@ public class BattleImpl : ScriptSource, IBattle
public void ForceEndBattle() public void ForceEndBattle()
{ {
HasEnded = true; HasEnded = true;
Result = BattleResult.Inconclusive; Result = FinalizeResult(BattleResult.Inconclusive);
} }
/// <inheritdoc /> /// <inheritdoc />
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; ITurnChoice? forcedChoice = null;
pokemon.RunScriptHook<IScriptForceTurnSelection>(script => pokemon.RunScriptHook<IScriptForceTurnSelection>(script =>
script.ForceTurnSelection(this, battleData.SideIndex, battleData.Position, ref forcedChoice)); script.ForceTurnSelection(this, pokemon.SideIndex, pokemon.Position, ref forcedChoice));
choice = forcedChoice; choice = forcedChoice;
return choice != null; return choice != null;
} }
@@ -346,7 +351,7 @@ public class BattleImpl : ScriptSource, IBattle
if (!switchChoice.SwitchTo.IsUsable) if (!switchChoice.SwitchTo.IsUsable)
return false; return false;
// Can't switch to a Pokémon already on the field // Can't switch to a Pokémon already on the field
if (switchChoice.SwitchTo.BattleData is { IsOnBattlefield: true }) if (switchChoice.SwitchTo.IsOnBattlefield)
return false; return false;
if (switchChoice.SwitchTo == switchChoice.User) if (switchChoice.SwitchTo == switchChoice.User)
return false; return false;
@@ -389,10 +394,10 @@ public class BattleImpl : ScriptSource, IBattle
{ {
if (!CanUse(choice)) if (!CanUse(choice))
return false; return false;
if (choice.User.BattleData?.IsOnBattlefield != true) if (!choice.User.IsOnBattlefield)
return false; return false;
var side = Sides[choice.User.BattleData!.SideIndex]; var side = Sides[choice.User.SideIndex];
side.SetChoice(choice.User.BattleData!.Position, choice); side.SetChoice(choice.User.Position, choice);
CheckChoicesSetAndRun(); CheckChoicesSetAndRun();
return true; return true;
} }
@@ -555,8 +560,8 @@ public class BattleImpl : ScriptSource, IBattle
if (attemptCapture.IsCaught) if (attemptCapture.IsCaught)
{ {
target.MarkAsCaught(); target.MarkAsCaught();
var side = Sides[target.BattleData!.SideIndex]; _capturedPokemon.Add(target.UnderlyingPokemon);
side.ForceClearPokemonFromField(target.BattleData.Position); target.BattleSide.ForceClearPokemonFromField(target.Position);
} }
EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture, item)); EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture, item));
@@ -592,9 +597,9 @@ public class BattleImpl : ScriptSource, IBattle
{ {
foreach (var party in Parties) 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(); _weatherScript.Clear();

View File

@@ -87,7 +87,7 @@ public class BattleChoiceQueue : IDeepCloneable
/// <returns> /// <returns>
/// Returns true if the Pokémon was found and moved, false otherwise. /// Returns true if the Pokémon was found and moved, false otherwise.
/// </returns> /// </returns>
public bool MovePokemonChoiceNext(IPokemon pokemon) public bool MovePokemonChoiceNext(IBattlePokemon pokemon)
{ {
var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon); var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon);
if (index == -1) if (index == -1)
@@ -110,7 +110,7 @@ public class BattleChoiceQueue : IDeepCloneable
/// <returns> /// <returns>
/// Returns true if the Pokémon was found and moved, false otherwise. /// Returns true if the Pokémon was found and moved, false otherwise.
/// </returns> /// </returns>
public bool MovePokemonChoiceLast(IPokemon pokemon) public bool MovePokemonChoiceLast(IBattlePokemon pokemon)
{ {
var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon); var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon);
if (index == -1) if (index == -1)

View File

@@ -4,15 +4,35 @@ namespace PkmnLib.Dynamic.Models;
/// <summary> /// <summary>
/// A battle party is a wrapper around a Pokemon party that provides additional functionality for battles. /// 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
/// <see cref="IBattlePokemon"/> wrappers for the party's Pokémon.
/// </summary> /// </summary>
public interface IBattleParty : IDeepCloneable public interface IBattleParty : IDeepCloneable
{ {
/// <summary> /// <summary>
/// The backing Pokemon party. /// The backing Pokemon party. Battle code should generally use <see cref="BattlePokemon"/> instead, as that
/// view contains the battle-scoped wrappers.
/// </summary> /// </summary>
IPokemonParty Party { get; } IPokemonParty Party { get; }
/// <summary>
/// The battle-scoped view of the party. Index-aligned with <see cref="Party"/>. Only available after
/// <see cref="Initialize"/> has been called by the battle.
/// </summary>
IReadOnlyList<IBattlePokemon?> BattlePokemon { get; }
/// <summary>
/// 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.
/// </summary>
IBattlePokemon? GetBattlePokemon(IPokemon pokemon);
/// <summary>
/// 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.
/// </summary>
void Initialize(IBattle battle);
/// <summary> /// <summary>
/// Whether the party is responsible for the specified side and position. /// Whether the party is responsible for the specified side and position.
/// </summary> /// </summary>
@@ -26,7 +46,7 @@ public interface IBattleParty : IDeepCloneable
/// <summary> /// <summary>
/// Gets all usable Pokemon that are not currently in the field. /// Gets all usable Pokemon that are not currently in the field.
/// </summary> /// </summary>
IEnumerable<IPokemon> GetUsablePokemonNotInField(); IEnumerable<IBattlePokemon> GetUsablePokemonNotInField();
} }
/// <summary> /// <summary>
@@ -39,6 +59,8 @@ public record struct ResponsibleIndex(byte Side, byte Position);
public class BattlePartyImpl : IBattleParty public class BattlePartyImpl : IBattleParty
{ {
private readonly ResponsibleIndex[] _responsibleIndices; private readonly ResponsibleIndex[] _responsibleIndices;
private IBattlePokemon?[] _battlePokemon = [];
private IBattle? _battle;
/// <inheritdoc cref="BattlePartyImpl"/> /// <inheritdoc cref="BattlePartyImpl"/>
public BattlePartyImpl(IPokemonParty party, ResponsibleIndex[] responsibleIndices) public BattlePartyImpl(IPokemonParty party, ResponsibleIndex[] responsibleIndices)
@@ -50,14 +72,53 @@ public class BattlePartyImpl : IBattleParty
/// <inheritdoc /> /// <inheritdoc />
public IPokemonParty Party { get; } public IPokemonParty Party { get; }
/// <inheritdoc />
public IReadOnlyList<IBattlePokemon?> BattlePokemon => _battlePokemon;
/// <inheritdoc />
public IBattlePokemon? GetBattlePokemon(IPokemon pokemon) =>
_battlePokemon.FirstOrDefault(x => x != null && (x == pokemon || x.UnderlyingPokemon == pokemon));
/// <inheritdoc />
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]);
};
}
/// <inheritdoc /> /// <inheritdoc />
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index); public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
/// <inheritdoc /> /// <inheritdoc />
public bool HasUsablePokemonNotInField() => public bool HasUsablePokemonNotInField() =>
Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true); _battlePokemon.WhereNotNull().Any(x => x.IsUsable && !x.IsOnBattlefield);
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<IPokemon> GetUsablePokemonNotInField() => public IEnumerable<IBattlePokemon> GetUsablePokemonNotInField() =>
Party.WhereNotNull().Where(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true); _battlePokemon.WhereNotNull().Where(x => x.IsUsable && !x.IsOnBattlefield);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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 /// rolls whether it triggers. As a side effect this run scripts to allow modifying this random
/// chance. /// chance.
/// </summary> /// </summary>
bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber); bool EffectChance(float chance, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber);
} }
/// <inheritdoc cref="IBattleRandom"/> /// <inheritdoc cref="IBattleRandom"/>
@@ -36,7 +36,7 @@ public class BattleRandomImpl : RandomImpl, IBattleRandom
} }
/// <inheritdoc /> /// <inheritdoc />
public bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber) public bool EffectChance(float chance, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber)
{ {
executingMove.RunScriptHook<IScriptChangeEffectChance>(script => executingMove.RunScriptHook<IScriptChangeEffectChance>(script =>
script.ChangeEffectChance(executingMove, target, hitNumber, ref chance)); script.ChangeEffectChance(executingMove, target, hitNumber, ref chance));

View File

@@ -30,4 +30,11 @@ public record struct BattleResult
/// The side that won the battle. If null, no side has won. /// The side that won the battle. If null, no side has won.
/// </summary> /// </summary>
public byte? WinningSide { get; } public byte? WinningSide { get; }
/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<IPokemon> CapturedPokemon { get; init; } = [];
} }

View File

@@ -24,7 +24,7 @@ public interface IBattleSide : IScriptSource, IDeepCloneable
/// <summary> /// <summary>
/// A list of Pokémon currently on the battlefield. /// A list of Pokémon currently on the battlefield.
/// </summary> /// </summary>
IReadOnlyList<IPokemon?> Pokemon { get; } IReadOnlyList<IBattlePokemon?> Pokemon { get; }
/// <summary> /// <summary>
/// The currently set choices for all Pokémon on the battlefield. Cleared when the turn starts. /// 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 /// 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. /// cleared. Returns the Pokémon that was previously in the spot.
/// </summary> /// </summary>
IPokemon? SwapPokemon(byte position, IPokemon? pokemon); IBattlePokemon? SwapPokemon(byte position, IBattlePokemon? pokemon);
/// <summary>
/// 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.
/// </summary>
IBattlePokemon? SendOut(byte position, IPokemon pokemon);
/// <summary> /// <summary>
/// Swaps two Pokémon on the side. /// Swaps two Pokémon on the side.
@@ -94,7 +101,7 @@ public interface IBattleSide : IScriptSource, IDeepCloneable
/// <summary> /// <summary>
/// Checks whether a Pokemon is on the field in this side. /// Checks whether a Pokemon is on the field in this side.
/// </summary> /// </summary>
bool IsPokemonOnSide(IPokemon pokemon); bool IsPokemonOnSide(IBattlePokemon pokemon);
/// <summary> /// <summary>
/// Marks a slot as unfillable. This happens when no parties are able to fill the slot anymore. /// 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; Index = index;
NumberOfPositions = numberOfPositions; NumberOfPositions = numberOfPositions;
_pokemon = new IPokemon?[numberOfPositions]; _pokemon = new IBattlePokemon?[numberOfPositions];
_setChoices = new ITurnChoice?[numberOfPositions]; _setChoices = new ITurnChoice?[numberOfPositions];
_fillablePositions = new bool[numberOfPositions]; _fillablePositions = new bool[numberOfPositions];
for (byte i = 0; i < numberOfPositions; i++) for (byte i = 0; i < numberOfPositions; i++)
@@ -183,10 +190,10 @@ public class BattleSideImpl : ScriptSource, IBattleSide
/// <inheritdoc /> /// <inheritdoc />
public byte NumberOfPositions { get; } public byte NumberOfPositions { get; }
private readonly IPokemon?[] _pokemon; private readonly IBattlePokemon?[] _pokemon;
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<IPokemon?> Pokemon => _pokemon; public IReadOnlyList<IBattlePokemon?> Pokemon => _pokemon;
private readonly ITurnChoice?[] _setChoices; private readonly ITurnChoice?[] _setChoices;
@@ -249,28 +256,31 @@ public class BattleSideImpl : ScriptSource, IBattleSide
if (pokemon is not null) if (pokemon is not null)
{ {
pokemon.RunScriptHook<IScriptOnRemove>(script => script.OnRemove()); pokemon.RunScriptHook<IScriptOnRemove>(script => script.OnRemove());
pokemon.SetOnBattlefield(false); pokemon.OnSwitchedOut();
} }
_pokemon[index] = null; _pokemon[index] = null;
} }
/// <inheritdoc /> /// <inheritdoc />
public IPokemon? SwapPokemon(byte position, IPokemon? pokemon) public IBattlePokemon? SwapPokemon(byte position, IBattlePokemon? pokemon)
{ {
var oldPokemon = _pokemon[position]; var oldPokemon = _pokemon[position];
if (oldPokemon is not null) if (oldPokemon is not null)
{ {
oldPokemon.RunScriptHook<IScriptOnSwitchOut>(script => script.OnSwitchOut(oldPokemon, position)); oldPokemon.RunScriptHook<IScriptOnSwitchOut>(script => script.OnSwitchOut(oldPokemon, position));
oldPokemon.RunScriptHook<IScriptOnRemove>(script => script.OnRemove()); oldPokemon.RunScriptHook<IScriptOnRemove>(script => script.OnRemove());
oldPokemon.SetOnBattlefield(false); oldPokemon.OnSwitchedOut();
} }
_pokemon[position] = pokemon; _pokemon[position] = pokemon;
if (pokemon is not null) if (pokemon is not null)
{ {
pokemon.SetBattleData(Battle, Index); if (pokemon.SideIndex != Index)
pokemon.SetOnBattlefield(true); {
pokemon.SetBattleSidePosition(position); 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)); Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon));
pokemon.RunScriptHook<IScriptOnSwitchIn>(script => script.OnSwitchIn(pokemon, position)); pokemon.RunScriptHook<IScriptOnSwitchIn>(script => script.OnSwitchIn(pokemon, position));
@@ -300,6 +310,14 @@ public class BattleSideImpl : ScriptSource, IBattleSide
return oldPokemon; return oldPokemon;
} }
/// <inheritdoc />
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);
}
/// <inheritdoc /> /// <inheritdoc />
public void SwapPokemon(byte position1, byte position2) public void SwapPokemon(byte position1, byte position2)
{ {
@@ -307,7 +325,7 @@ public class BattleSideImpl : ScriptSource, IBattleSide
} }
/// <inheritdoc /> /// <inheritdoc />
public bool IsPokemonOnSide(IPokemon pokemon) => _pokemon.Contains(pokemon); public bool IsPokemonOnSide(IBattlePokemon pokemon) => _pokemon.Contains(pokemon);
/// <inheritdoc /> /// <inheritdoc />
public void MarkPositionAsUnfillable(byte position) => _fillablePositions[position] = false; public void MarkPositionAsUnfillable(byte position) => _fillablePositions[position] = false;

View File

@@ -13,7 +13,7 @@ public interface IFleeChoice : ITurnChoice
public class FleeTurnChoice : TurnChoice, IFleeChoice public class FleeTurnChoice : TurnChoice, IFleeChoice
{ {
/// <inheritdoc /> /// <inheritdoc />
public FleeTurnChoice(IPokemon user) : base(user) public FleeTurnChoice(IBattlePokemon user) : base(user)
{ {
} }

View File

@@ -16,15 +16,15 @@ public interface IItemChoice : ITurnChoice
/// <summary> /// <summary>
/// The target Pokémon of the item, if any. /// The target Pokémon of the item, if any.
/// </summary> /// </summary>
IPokemon? GetTargetPokemon(IBattle battle); IBattlePokemon? GetTargetPokemon(IBattle battle);
} }
/// <inheritdoc cref="IItemChoice"/> /// <inheritdoc cref="IItemChoice"/>
public class ItemChoice : TurnChoice, IItemChoice public class ItemChoice : TurnChoice, IItemChoice
{ {
/// <inheritdoc cref="ItemChoice"/> /// <inheritdoc cref="ItemChoice"/>
private ItemChoice(IPokemon user, IItem item, byte? targetSide, byte? targetPosition, IPokemon? targetPokemon) : private ItemChoice(IBattlePokemon user, IItem item, byte? targetSide, byte? targetPosition,
base(user) IBattlePokemon? targetPokemon) : base(user)
{ {
Item = item; Item = item;
TargetSide = targetSide; TargetSide = targetSide;
@@ -32,13 +32,13 @@ public class ItemChoice : TurnChoice, IItemChoice
TargetPokemon = targetPokemon; TargetPokemon = targetPokemon;
} }
public static ItemChoice CreateWithoutTarget(IPokemon user, IItem item) => public static ItemChoice CreateWithoutTarget(IBattlePokemon user, IItem item) =>
new(user, item, null, null, null); 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); 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); new(user, item, null, null, targetPokemon);
/// <summary> /// <summary>
@@ -47,7 +47,7 @@ public class ItemChoice : TurnChoice, IItemChoice
public IItem Item { get; } public IItem Item { get; }
/// <inheritdoc /> /// <inheritdoc />
public IPokemon? GetTargetPokemon(IBattle battle) public IBattlePokemon? GetTargetPokemon(IBattle battle)
{ {
if (TargetPokemon != null) if (TargetPokemon != null)
return TargetPokemon; return TargetPokemon;
@@ -71,7 +71,7 @@ public class ItemChoice : TurnChoice, IItemChoice
/// <summary> /// <summary>
/// The target Pokémon of the item, if any. This is used for party members. /// The target Pokémon of the item, if any. This is used for party members.
/// </summary> /// </summary>
private IPokemon? TargetPokemon { get; } private IBattlePokemon? TargetPokemon { get; }
/// <inheritdoc /> /// <inheritdoc />
public override int ScriptCount => User.ScriptCount; public override int ScriptCount => User.ScriptCount;

View File

@@ -51,7 +51,7 @@ public interface IMoveChoice : ITurnChoice
public class MoveChoice : TurnChoice, IMoveChoice public class MoveChoice : TurnChoice, IMoveChoice
{ {
/// <inheritdoc cref="MoveChoice"/> /// <inheritdoc cref="MoveChoice"/>
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; ChosenMove = usedMove;
TargetSide = targetSide; TargetSide = targetSide;

View File

@@ -13,7 +13,7 @@ public interface IPassChoice : ITurnChoice
public class PassChoice : TurnChoice, IPassChoice public class PassChoice : TurnChoice, IPassChoice
{ {
/// <inheritdoc cref="PassChoice"/> /// <inheritdoc cref="PassChoice"/>
public PassChoice(IPokemon user) : base(user) public PassChoice(IBattlePokemon user) : base(user)
{ {
} }

View File

@@ -10,20 +10,20 @@ public interface ISwitchChoice : ITurnChoice
/// <summary> /// <summary>
/// The Pokémon to switch to. /// The Pokémon to switch to.
/// </summary> /// </summary>
IPokemon SwitchTo { get; } IBattlePokemon SwitchTo { get; }
} }
/// <inheritdoc cref="ISwitchChoice"/> /// <inheritdoc cref="ISwitchChoice"/>
public class SwitchChoice : TurnChoice, ISwitchChoice public class SwitchChoice : TurnChoice, ISwitchChoice
{ {
/// <inheritdoc cref="SwitchChoice"/> /// <inheritdoc cref="SwitchChoice"/>
public SwitchChoice(IPokemon user, IPokemon switchTo) : base(user) public SwitchChoice(IBattlePokemon user, IBattlePokemon switchTo) : base(user)
{ {
SwitchTo = switchTo; SwitchTo = switchTo;
} }
/// <inheritdoc /> /// <inheritdoc />
public IPokemon SwitchTo { get; } public IBattlePokemon SwitchTo { get; }
/// <inheritdoc /> /// <inheritdoc />
public override int ScriptCount => User.ScriptCount; public override int ScriptCount => User.ScriptCount;

View File

@@ -11,7 +11,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable
/// <summary> /// <summary>
/// The user of the turn choice /// The user of the turn choice
/// </summary> /// </summary>
IPokemon User { get; } IBattlePokemon User { get; }
/// <summary> /// <summary>
/// The speed of the user at the beginning of the turn. /// 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 /// Fails the choice. This will prevent it from executing and run a specific fail handling during
/// execution. Note that this can not be undone. /// execution. Note that this can not be undone.
/// </summary> /// </summary>
public void Fail(); void Fail();
} }
/// <summary> /// <summary>
@@ -43,7 +43,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable
public abstract class TurnChoice : ScriptSource, ITurnChoice public abstract class TurnChoice : ScriptSource, ITurnChoice
{ {
/// <inheritdoc cref="TurnChoice"/> /// <inheritdoc cref="TurnChoice"/>
protected TurnChoice(IPokemon user) protected TurnChoice(IBattlePokemon user)
{ {
User = user; User = user;
} }
@@ -51,7 +51,7 @@ public abstract class TurnChoice : ScriptSource, ITurnChoice
/// <summary> /// <summary>
/// The Pokemon for which the choice is made. /// The Pokemon for which the choice is made.
/// </summary> /// </summary>
public IPokemon User { get; } public IBattlePokemon User { get; }
/// <summary> /// <summary>
/// The speed of the user at the beginning of the turn. /// The speed of the user at the beginning of the turn.

View File

@@ -129,7 +129,7 @@ public interface IExecutingMove : IScriptSource
/// <summary> /// <summary>
/// The user of the move. /// The user of the move.
/// </summary> /// </summary>
IPokemon User { get; } IBattlePokemon User { get; }
/// <summary> /// <summary>
/// The move the user has actually chosen to do. /// The move the user has actually chosen to do.
@@ -151,17 +151,17 @@ public interface IExecutingMove : IScriptSource
/// <summary> /// <summary>
/// Gets a hit data for a target, with a specific index. /// Gets a hit data for a target, with a specific index.
/// </summary> /// </summary>
IHitData GetHitData(IPokemon target, byte hit); IHitData GetHitData(IBattlePokemon target, byte hit);
/// <summary> /// <summary>
/// Checks whether a Pokémon is a target for this move. /// Checks whether a Pokémon is a target for this move.
/// </summary> /// </summary>
bool IsPokemonTarget(IPokemon target); bool IsPokemonTarget(IBattlePokemon target);
/// <summary> /// <summary>
/// Gets the index of the hits in this move where the hits for a specific target start. /// Gets the index of the hits in this move where the hits for a specific target start.
/// </summary> /// </summary>
int GetTargetIndex(IPokemon target); int GetTargetIndex(IBattlePokemon target);
/// <summary> /// <summary>
/// Gets a hit based on its raw index. /// Gets a hit based on its raw index.
@@ -171,7 +171,7 @@ public interface IExecutingMove : IScriptSource
/// <summary> /// <summary>
/// Gets the targets of this move. /// Gets the targets of this move.
/// </summary> /// </summary>
IReadOnlyList<IPokemon?> Targets { get; } IReadOnlyList<IBattlePokemon?> Targets { get; }
/// <summary> /// <summary>
/// The underlying move choice. /// The underlying move choice.
@@ -192,12 +192,12 @@ public interface IExecutingMove : IScriptSource
/// <inheritdoc cref="IExecutingMove"/> /// <inheritdoc cref="IExecutingMove"/>
public class ExecutingMoveImpl : ScriptSource, IExecutingMove public class ExecutingMoveImpl : ScriptSource, IExecutingMove
{ {
private readonly IReadOnlyList<IPokemon?> _targets; private readonly IReadOnlyList<IBattlePokemon?> _targets;
private readonly IHitData[] _hits; private readonly IHitData[] _hits;
private readonly IBattle _battle; private readonly IBattle _battle;
/// <inheritdoc cref="ExecutingMoveImpl"/> /// <inheritdoc cref="ExecutingMoveImpl"/>
public ExecutingMoveImpl(IReadOnlyList<IPokemon?> targets, byte numberOfHits, ILearnedMove chosenMove, public ExecutingMoveImpl(IReadOnlyList<IBattlePokemon?> targets, byte numberOfHits, ILearnedMove chosenMove,
IMoveData useMove, IMoveChoice moveChoice, IBattle battle) IMoveData useMove, IMoveChoice moveChoice, IBattle battle)
{ {
_targets = targets; _targets = targets;
@@ -222,7 +222,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
public byte NumberOfHits { get; } public byte NumberOfHits { get; }
/// <inheritdoc /> /// <inheritdoc />
public IPokemon User => MoveChoice.User; public IBattlePokemon User => MoveChoice.User;
/// <inheritdoc /> /// <inheritdoc />
public ILearnedMove ChosenMove { get; } public ILearnedMove ChosenMove { get; }
@@ -239,7 +239,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
public IScriptSet Volatile => MoveChoice.Volatile; public IScriptSet Volatile => MoveChoice.Volatile;
/// <inheritdoc /> /// <inheritdoc />
public IHitData GetHitData(IPokemon target, byte hit) public IHitData GetHitData(IBattlePokemon target, byte hit)
{ {
var targetIndex = _targets.IndexOf(target); var targetIndex = _targets.IndexOf(target);
if (targetIndex == -1) if (targetIndex == -1)
@@ -252,10 +252,10 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
} }
/// <inheritdoc /> /// <inheritdoc />
public bool IsPokemonTarget(IPokemon target) => _targets.Contains(target); public bool IsPokemonTarget(IBattlePokemon target) => _targets.Contains(target);
/// <inheritdoc /> /// <inheritdoc />
public int GetTargetIndex(IPokemon target) public int GetTargetIndex(IBattlePokemon target)
{ {
var targetIndex = _targets.IndexOf(target); var targetIndex = _targets.IndexOf(target);
if (targetIndex == -1) if (targetIndex == -1)
@@ -273,7 +273,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<IPokemon?> Targets => _targets.ToList(); public IReadOnlyList<IBattlePokemon?> Targets => _targets.ToList();
/// <inheritdoc /> /// <inheritdoc />
public IMoveChoice MoveChoice { get; } public IMoveChoice MoveChoice { get; }

View File

@@ -35,29 +35,30 @@ public static class ItemTargetTypeHelpers
/// <summary> /// <summary>
/// Determines if the given target is valid based on the ItemTargetType. /// Determines if the given target is valid based on the ItemTargetType.
/// </summary> /// </summary>
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) if (targetType == ItemTargetType.None)
return true; return true;
if (targetType.HasFlag(ItemTargetType.OwnPokemon)) if (targetType.HasFlag(ItemTargetType.OwnPokemon))
{ {
var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); var userParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(user));
var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target)); var targetParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(target));
if (userParty is not null && targetParty is not null && userParty == targetParty) if (userParty is not null && targetParty is not null && userParty == targetParty)
return true; return true;
} }
if (targetType.HasFlag(ItemTargetType.AllyPokemon)) if (targetType.HasFlag(ItemTargetType.AllyPokemon))
{ {
if (user.BattleData?.BattleSide == target.BattleData?.BattleSide) if (user.BattleSide == target.BattleSide)
return true; return true;
} }
if (targetType.HasFlag(ItemTargetType.FoePokemon)) if (targetType.HasFlag(ItemTargetType.FoePokemon))
{ {
var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user)); var userParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(user));
var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target)); var targetParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(target));
if (userParty is not null && targetParty is not null && userParty != targetParty) if (userParty is not null && targetParty is not null && userParty != targetParty)
return true; return true;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ public record SerializedPokemon
Nature = pokemon.Nature.Name; Nature = pokemon.Nature.Name;
Nickname = pokemon.Nickname; Nickname = pokemon.Nickname;
Ability = pokemon.Form.GetAbility(pokemon.AbilityIndex); Ability = pokemon.Form.GetAbility(pokemon.AbilityIndex);
Moves = pokemon.BaseMoves.Select(move => Moves = pokemon.Moves.Select(move =>
{ {
if (move == null) if (move == null)
return null; return null;

View File

@@ -41,7 +41,7 @@ public abstract class ItemScript : IDeepCloneable
/// <summary> /// <summary>
/// Returns whether the item is usable on the given target. /// Returns whether the item is usable on the given target.
/// </summary> /// </summary>
public virtual bool IsTargetValid(IPokemon target) => false; public virtual bool IsTargetValid(IBattlePokemon target) => false;
/// <summary> /// <summary>
/// Returns whether the item can be held by a Pokémon. /// Returns whether the item can be held by a Pokémon.
@@ -51,7 +51,7 @@ public abstract class ItemScript : IDeepCloneable
/// <summary> /// <summary>
/// Returns whether the item can be held by the given target. /// Returns whether the item can be held by the given target.
/// </summary> /// </summary>
public virtual bool CanTargetHold(IPokemon pokemon) => true; public virtual bool CanTargetHold(IBattlePokemon pokemon) => true;
/// <summary> /// <summary>
/// Handles the use of the item. /// Handles the use of the item.
@@ -63,7 +63,7 @@ public abstract class ItemScript : IDeepCloneable
/// <summary> /// <summary>
/// Handles the use of the item on the given target. /// Handles the use of the item on the given target.
/// </summary> /// </summary>
public virtual void OnUseWithTarget(IPokemon target, EventHook eventHook) public virtual void OnUseWithTarget(IBattlePokemon target, EventHook eventHook)
{ {
} }
} }

View File

@@ -17,9 +17,9 @@ public abstract class PokeballScript : ItemScript
/// <summary> /// <summary>
/// Returns the catch rate of the Pokéball against the given target Pokémon. /// Returns the catch rate of the Pokéball against the given target Pokémon.
/// </summary> /// </summary>
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. // Default implementation does nothing.
// Override this method in derived classes to add custom behavior after a successful capture. // 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 ItemTargetType TargetType => ItemTargetType.FoePokemon;
/// <inheritdoc /> /// <inheritdoc />
public override bool IsTargetValid(IPokemon target) => public override bool IsTargetValid(IBattlePokemon target) => target.Battle.IsWildBattle;
target.BattleData is not null && target.BattleData.Battle.IsWildBattle;
/// <inheritdoc /> /// <inheritdoc />
public override void OnUseWithTarget(IPokemon target, EventHook eventHook) public override void OnUseWithTarget(IBattlePokemon target, EventHook eventHook)
{ {
var battleData = target.BattleData; var result = target.Battle.AttempCapture(target.SideIndex, target.Position, Item);
if (result.IsCaught)
var result = battleData?.Battle.AttempCapture(battleData.SideIndex, battleData.Position, Item);
if (result is { IsCaught: true })
{ {
OnAfterSuccessfulCapture(target); OnAfterSuccessfulCapture(target);
} }

View File

@@ -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 /// 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. /// have an end of turn effect, such as Poison or Burn.
/// </summary> /// </summary>
void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage); void ExpectedEndOfTurnDamage(IBattlePokemon pokemon, ref int damage);
} }
/// <summary> /// <summary>
@@ -37,5 +37,5 @@ public interface IAIInfoScriptExpectedEntryDamage
/// This function returns the expected entry damage for the script. This is used for scripts that have /// 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. /// an entry hazard effect, such as Spikes or Stealth Rock.
/// </summary> /// </summary>
void ExpectedEntryDamage(IPokemon pokemon, ref uint damage); void ExpectedEntryDamage(IBattlePokemon pokemon, ref uint damage);
} }

View File

@@ -100,8 +100,8 @@ public static class ScriptExecution
/// <summary> /// <summary>
/// Executes a script on an item. /// Executes a script on an item.
/// </summary> /// </summary>
public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IPokemon? target, IPokemon user, public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IBattlePokemon? target,
IBattle battle, EventHook eventHook) IBattlePokemon user, IBattle battle, EventHook eventHook)
{ {
if (!scriptResolver.TryResolveBattleItemScript(item, out var itemScript)) if (!scriptResolver.TryResolveBattleItemScript(item, out var itemScript))
{ {

View File

@@ -125,7 +125,7 @@ public interface IScriptChangeTargets
/// <summary> /// <summary>
/// Changes the targets of a move choice. This allows for changing the targets of a move before the move starts. /// Changes the targets of a move choice. This allows for changing the targets of a move before the move starts.
/// </summary> /// </summary>
void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets); void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList<IBattlePokemon?> targets);
} }
/// <summary> /// <summary>
@@ -136,7 +136,7 @@ public interface IScriptChangeIncomingTargets
/// <summary> /// <summary>
/// This function allows you to change the targets of a move choice before the move starts. /// This function allows you to change the targets of a move choice before the move starts.
/// </summary> /// </summary>
void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets); void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList<IBattlePokemon?> targets);
} }
/// <summary> /// <summary>
@@ -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. /// 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. /// If this is set to null, the move will be treated as a typeless move.
/// </summary> /// </summary>
void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? typeIdentifier); void ChangeMoveType(IExecutingMove move, IBattlePokemon target, byte hit, ref TypeIdentifier? typeIdentifier);
} }
/// <summary> /// <summary>
@@ -230,7 +230,7 @@ public interface IScriptChangeEffectiveness
/// <summary> /// <summary>
/// This function allows the script to change how effective a move is on a target. /// This function allows the script to change how effective a move is on a target.
/// </summary> /// </summary>
void ChangeEffectiveness(IExecutingMove move, IPokemon target, byte hit, ref float effectiveness); void ChangeEffectiveness(IExecutingMove move, IBattlePokemon target, byte hit, ref float effectiveness);
} }
/// <summary> /// <summary>
@@ -241,7 +241,7 @@ public interface IScriptChangeIncomingEffectiveness
/// <summary> /// <summary>
/// This function allows the script to override how effective a move is on a target. /// This function allows the script to override how effective a move is on a target.
/// </summary> /// </summary>
void ChangeIncomingEffectiveness(IExecutingMove executingMove, IPokemon target, byte hitIndex, void ChangeIncomingEffectiveness(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
ref float effectiveness); ref float effectiveness);
} }
@@ -253,7 +253,7 @@ public interface IScriptBlockCriticalHit
/// <summary> /// <summary>
/// This function allows a script to block an outgoing move from being critical. /// This function allows a script to block an outgoing move from being critical.
/// </summary> /// </summary>
void BlockCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block); void BlockCriticalHit(IExecutingMove move, IBattlePokemon target, byte hit, ref bool block);
} }
/// <summary> /// <summary>
@@ -264,7 +264,7 @@ public interface IScriptBlockIncomingCriticalHit
/// <summary> /// <summary>
/// This function allows a script to block an incoming move from being critical. /// This function allows a script to block an incoming move from being critical.
/// </summary> /// </summary>
void BlockIncomingCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block); void BlockIncomingCriticalHit(IExecutingMove move, IBattlePokemon target, byte hit, ref bool block);
} }
/// <summary> /// <summary>
@@ -276,7 +276,7 @@ public interface IScriptOnIncomingHit
/// This function triggers when an incoming hit happens. This triggers after the damage is done, /// This function triggers when an incoming hit happens. This triggers after the damage is done,
/// but before the secondary effect of the move happens. /// but before the secondary effect of the move happens.
/// </summary> /// </summary>
void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit); void OnIncomingHit(IExecutingMove move, IBattlePokemon target, byte hit);
} }
/// <summary> /// <summary>
@@ -287,7 +287,7 @@ public interface IScriptOnOpponentFaints
/// <summary> /// <summary>
/// This function triggers when an opponent on the f ield faints due to the move that is being executed. /// This function triggers when an opponent on the f ield faints due to the move that is being executed.
/// </summary> /// </summary>
void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit); void OnOpponentFaints(IExecutingMove move, IBattlePokemon target, byte hit);
} }
/// <summary> /// <summary>
@@ -300,7 +300,7 @@ public interface IScriptOnSecondaryEffect
/// secondary effects here. Status moves should implement their actual functionality in this /// 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. /// function as well, as status moves effects are defined as secondary effects for simplicity.
/// </summary> /// </summary>
void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit); void OnSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// the move fails, and fail events get triggered.
/// </summary> /// </summary>
void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail); void FailIncomingMove(IExecutingMove move, IBattlePokemon target, ref bool fail);
} }
/// <summary> /// <summary>
@@ -323,7 +323,7 @@ public interface IScriptIsInvulnerableToMove
/// <summary> /// <summary>
/// This function allows a script to make its owner invulnerable to an incoming move. /// This function allows a script to make its owner invulnerable to an incoming move.
/// </summary> /// </summary>
void IsInvulnerableToMove(IExecutingMove move, IPokemon target, ref bool invulnerable); void IsInvulnerableToMove(IExecutingMove move, IBattlePokemon target, ref bool invulnerable);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// that have a secondary effect that should run even if the move misses, such as Spore.
/// </summary> /// </summary>
void OnMoveMiss(IExecutingMove move, IPokemon target); void OnMoveMiss(IExecutingMove move, IBattlePokemon target);
} }
/// <summary> /// <summary>
@@ -347,7 +347,7 @@ public interface IScriptChangeAccuracyModifier
/// This function allows a script to modify the accuracy of a move used. This value represents /// 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. /// the percentage accuracy, so anything above 100% will make it always hit.
/// </summary> /// </summary>
void ChangeAccuracyModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); void ChangeAccuracyModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
} }
/// <summary> /// <summary>
@@ -358,7 +358,7 @@ public interface IScriptChangeCriticalStage
/// <summary> /// <summary>
/// This function allows a script to change the critical stage of the move used. /// This function allows a script to change the critical stage of the move used.
/// </summary> /// </summary>
void ChangeCriticalStage(IExecutingMove move, IPokemon target, byte hit, ref byte stage); void ChangeCriticalStage(IExecutingMove move, IBattlePokemon target, byte hit, ref byte stage);
} }
/// <summary> /// <summary>
@@ -370,7 +370,7 @@ public interface IScriptChangeCriticalModifier
/// This function allows a script to change the damage modifier of a critical hit. This will only /// This function allows a script to change the damage modifier of a critical hit. This will only
/// run when a hit is critical. /// run when a hit is critical.
/// </summary> /// </summary>
void ChangeCriticalModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); void ChangeCriticalModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
} }
/// <summary> /// <summary>
@@ -382,7 +382,7 @@ public interface IScriptChangeStabModifier
/// This function allows a script to change the damage modifier of a Same Type Attack Bonus, which /// 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. /// occurs when the user has the move type as one of its own types.
/// </summary> /// </summary>
void ChangeStabModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, bool isStab, void ChangeStabModifier(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, bool isStab,
ref float modifier); ref float modifier);
} }
@@ -394,7 +394,7 @@ public interface IScriptChangeBasePower
/// <summary> /// <summary>
/// This function allows a script to change the effective base power of a move hit. /// This function allows a script to change the effective base power of a move hit.
/// </summary> /// </summary>
void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower); void ChangeBasePower(IExecutingMove move, IBattlePokemon target, byte hit, ref ushort basePower);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// stat boosts will still be applied.
/// </summary> /// </summary>
void BypassDefensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass); void BypassDefensiveStatBoosts(IExecutingMove move, IBattlePokemon target, byte hit, ref bool bypass);
} }
/// <summary> /// <summary>
@@ -419,7 +419,7 @@ public interface IScriptBypassEvasionStatBoosts
/// This function allows a script to bypass evasion stat boosts for a move hit. /// 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. /// If this is true, the move will handle the evasion stat boosts as if the target has no positive stat boosts.
/// </summary> /// </summary>
void BypassEvasionStatBoosts(IExecutingMove move, IPokemon target, byte hitIndex, ref bool bypass); void BypassEvasionStatBoosts(IExecutingMove move, IBattlePokemon target, byte hitIndex, ref bool bypass);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// stat boosts will still be applied.
/// </summary> /// </summary>
void BypassOffensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass); void BypassOffensiveStatBoosts(IExecutingMove move, IBattlePokemon target, byte hit, ref bool bypass);
} }
/// <summary> /// <summary>
@@ -443,7 +443,7 @@ public interface IScriptChangeOffensiveStatValue
/// <summary> /// <summary>
/// This function allows a script to change the actual offensive stat values used when calculating damage /// This function allows a script to change the actual offensive stat values used when calculating damage
/// </summary> /// </summary>
void ChangeOffensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint defensiveStat, void ChangeOffensiveStatValue(IExecutingMove move, IBattlePokemon target, byte hit, uint defensiveStat,
ImmutableStatisticSet<uint> targetStats, Statistic stat, ref uint value); ImmutableStatisticSet<uint> targetStats, Statistic stat, ref uint value);
} }
@@ -455,7 +455,7 @@ public interface IScriptChangeDefensiveStatValue
/// <summary> /// <summary>
/// This function allows a script to change the actual defensive stat values used when calculating damage. /// This function allows a script to change the actual defensive stat values used when calculating damage.
/// </summary> /// </summary>
void ChangeDefensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint offensiveStat, void ChangeDefensiveStatValue(IExecutingMove move, IBattlePokemon target, byte hit, uint offensiveStat,
ImmutableStatisticSet<uint> targetStats, Statistic stat, ref uint value); ImmutableStatisticSet<uint> targetStats, Statistic stat, ref uint value);
} }
@@ -467,7 +467,7 @@ public interface IScriptChangeIncomingMoveOffensiveStatValue
/// <summary> /// <summary>
/// This function allows a script to change the offensive stat value of an incoming move. /// This function allows a script to change the offensive stat value of an incoming move.
/// </summary> /// </summary>
void ChangeIncomingMoveOffensiveStatValue(IExecutingMove executingMove, IPokemon target, byte hitNumber, void ChangeIncomingMoveOffensiveStatValue(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber,
uint defensiveStat, StatisticSet<uint> targetStats, Statistic offensive, ref uint offensiveStat); uint defensiveStat, StatisticSet<uint> targetStats, Statistic offensive, ref uint offensiveStat);
} }
@@ -479,7 +479,7 @@ public interface IScriptChangeIncomingMoveDefensiveStatValue
/// <summary> /// <summary>
/// This function allows a script to change the defensive stat value of an incoming move. /// This function allows a script to change the defensive stat value of an incoming move.
/// </summary> /// </summary>
void ChangeIncomingMoveDefensiveStatValue(IExecutingMove executingMove, IPokemon target, byte hitNumber, void ChangeIncomingMoveDefensiveStatValue(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber,
uint origOffensiveStat, StatisticSet<uint> targetStats, Statistic defensive, ref uint defensiveStat); uint origOffensiveStat, StatisticSet<uint> 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 /// 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. /// defender and attacker. The default value is the offensive stat divided by the defensive stat.
/// </summary> /// </summary>
void ChangeDamageStatModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); void ChangeDamageStatModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
} }
/// <summary> /// <summary>
@@ -503,7 +503,7 @@ public interface IScriptChangeDamageModifier
/// <summary> /// <summary>
/// This function allows a script to apply a raw multiplier to the damage done by a move. /// This function allows a script to apply a raw multiplier to the damage done by a move.
/// </summary> /// </summary>
void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier); void ChangeDamageModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
} }
/// <summary> /// <summary>
@@ -514,7 +514,7 @@ public interface IScriptChangeIncomingMoveDamageModifier
/// <summary> /// <summary>
/// This function allows a script to change the damage modifier of an incoming move. /// This function allows a script to change the damage modifier of an incoming move.
/// </summary> /// </summary>
void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber,
ref float modifier); ref float modifier);
} }
@@ -526,7 +526,7 @@ public interface IScriptChangeMoveDamage
/// <summary> /// <summary>
/// This function allows a script to modify the outgoing damage done by a move. /// This function allows a script to modify the outgoing damage done by a move.
/// </summary> /// </summary>
void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage); void ChangeMoveDamage(IExecutingMove move, IBattlePokemon target, byte hit, ref uint damage);
} }
/// <summary> /// <summary>
@@ -537,7 +537,7 @@ public interface IScriptChangeIncomingMoveDamage
/// <summary> /// <summary>
/// This function allows a script to modify the incoming damage done by a move. /// This function allows a script to modify the incoming damage done by a move.
/// </summary> /// </summary>
void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage); void ChangeIncomingMoveDamage(IExecutingMove move, IBattlePokemon target, byte hit, ref uint damage);
} }
/// <summary> /// <summary>
@@ -549,7 +549,8 @@ public interface IScriptPreventStatBoostChange
/// This function allows a script attached to a Pokemon or its parents to prevent stat boost /// This function allows a script attached to a Pokemon or its parents to prevent stat boost
/// changes on that Pokemon. /// changes on that Pokemon.
/// </summary> /// </summary>
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);
} }
/// <summary> /// <summary>
@@ -562,7 +563,7 @@ public interface IScriptChangeStatBoostChange
/// which the stat boost will change. If the stat boost is done by the user itself, self /// 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. /// inflicted will be true, otherwise it will be false.
/// </summary> /// </summary>
void ChangeStatBoostChange(IPokemon target, Statistic stat, bool selfInflicted, ref sbyte amount); void ChangeStatBoostChange(IBattlePokemon target, Statistic stat, bool selfInflicted, ref sbyte amount);
} }
/// <summary> /// <summary>
@@ -573,7 +574,7 @@ public interface IScriptOnAfterStatBoostChange
/// <summary> /// <summary>
/// This function allows a script to run after a stat boost change has been applied. /// This function allows a script to run after a stat boost change has been applied.
/// </summary> /// </summary>
void OnAfterStatBoostChange(IPokemon pokemon, Statistic stat, bool selfInflicted, sbyte change); void OnAfterStatBoostChange(IBattlePokemon pokemon, Statistic stat, bool selfInflicted, sbyte change);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// function is not called for status moves.
/// </summary> /// </summary>
void PreventSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent); void PreventSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit, ref bool prevent);
} }
/// <summary> /// <summary>
@@ -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. 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. /// secondary effect. Note that this function is not called for status moves.
/// </summary> /// </summary>
void PreventIncomingSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent); void PreventIncomingSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit, ref bool prevent);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// below 0 will make it never hit.
/// </summary> /// </summary>
void ChangeEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance); void ChangeEffectChance(IExecutingMove move, IBattlePokemon target, byte hit, ref float chance);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// or below 0 will make it never hit.
/// </summary> /// </summary>
void ChangeIncomingEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance); void ChangeIncomingEffectChance(IExecutingMove move, IBattlePokemon target, byte hit, ref float chance);
} }
/// <summary> /// <summary>
@@ -638,7 +639,7 @@ public interface IScriptOnAfterHits
/// <summary> /// <summary>
/// This function triggers on a move or its parents when all hits on a target are finished. /// This function triggers on a move or its parents when all hits on a target are finished.
/// </summary> /// </summary>
void OnAfterHits(IExecutingMove move, IPokemon target); void OnAfterHits(IExecutingMove move, IBattlePokemon target);
} }
/// <summary> /// <summary>
@@ -706,7 +707,7 @@ public interface IScriptOnDamage
/// <summary> /// <summary>
/// This function is triggered on a Pokemon and its parents when the given Pokemon takes damage. /// This function is triggered on a Pokemon and its parents when the given Pokemon takes damage.
/// </summary> /// </summary>
void OnDamage(IPokemon pokemon, DamageSource source, uint oldHealth, uint newHealth); void OnDamage(IBattlePokemon pokemon, DamageSource source, uint oldHealth, uint newHealth);
} }
/// <summary> /// <summary>
@@ -717,7 +718,7 @@ public interface IScriptOnFaint
/// <summary> /// <summary>
/// This function is triggered on a Pokemon and its parents when the given Pokemon faints. /// This function is triggered on a Pokemon and its parents when the given Pokemon faints.
/// </summary> /// </summary>
void OnFaint(IPokemon pokemon, DamageSource source); void OnFaint(IBattlePokemon pokemon, DamageSource source);
} }
/// <summary> /// <summary>
@@ -728,7 +729,7 @@ public interface IScriptOnAllyFaint
/// <summary> /// <summary>
/// This function is triggered on a Pokemon when an ally Pokemon faints. /// This function is triggered on a Pokemon when an ally Pokemon faints.
/// </summary> /// </summary>
void OnAllyFaint(IPokemon ally, IPokemon faintedPokemon); void OnAllyFaint(IBattlePokemon ally, IBattlePokemon faintedPokemon);
} }
/// <summary> /// <summary>
@@ -740,7 +741,7 @@ public interface IScriptOnSwitchOut
/// This function is triggered on a Pokemon and its parents when the given Pokemon switches out /// This function is triggered on a Pokemon and its parents when the given Pokemon switches out
/// of the battlefield. /// of the battlefield.
/// </summary> /// </summary>
void OnSwitchOut(IPokemon oldPokemon, byte position); void OnSwitchOut(IBattlePokemon oldPokemon, byte position);
} }
/// <summary> /// <summary>
@@ -752,7 +753,7 @@ public interface IScriptOnSwitchIn
/// This function is triggered on a Pokemon and its parents when the given Pokemon is switched into /// This function is triggered on a Pokemon and its parents when the given Pokemon is switched into
/// the battlefield. /// the battlefield.
/// </summary> /// </summary>
void OnSwitchIn(IPokemon pokemon, byte position); void OnSwitchIn(IBattlePokemon pokemon, byte position);
} }
/// <summary> /// <summary>
@@ -763,7 +764,7 @@ public interface IScriptOnOpponentSwitchIn
/// <summary> /// <summary>
/// This function is triggered on a Pokemon and its parents when an opponent switches in. /// This function is triggered on a Pokemon and its parents when an opponent switches in.
/// </summary> /// </summary>
void OnOpponentSwitchIn(IPokemon pokemon, byte position); void OnOpponentSwitchIn(IBattlePokemon pokemon, byte position);
} }
/// <summary> /// <summary>
@@ -787,7 +788,7 @@ public interface IScriptOnAfterItemConsume
/// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the /// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the
/// held item it had. /// held item it had.
/// </summary> /// </summary>
void OnAfterItemConsume(IPokemon pokemon, IItem item); void OnAfterItemConsume(IBattlePokemon pokemon, IItem item);
} }
/// <summary> /// <summary>
@@ -798,7 +799,7 @@ public interface IScriptBlockIncomingHit
/// <summary> /// <summary>
/// This function allows a script to block an incoming hit. /// This function allows a script to block an incoming hit.
/// </summary> /// </summary>
void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block); void BlockIncomingHit(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool block);
} }
/// <summary> /// <summary>
@@ -809,7 +810,7 @@ public interface IScriptBlockOutgoingHit
/// <summary> /// <summary>
/// This function allows a script to block an outgoing hit. /// This function allows a script to block an outgoing hit.
/// </summary> /// </summary>
void BlockOutgoingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block); void BlockOutgoingHit(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool block);
} }
/// <summary> /// <summary>
@@ -820,7 +821,7 @@ public interface IScriptPreventHeldItemConsume
/// <summary> /// <summary>
/// This function allows a script to prevent a held item from being consumed. /// This function allows a script to prevent a held item from being consumed.
/// </summary> /// </summary>
void PreventHeldItemConsume(IPokemon pokemon, IItem heldItem, ref bool prevented); void PreventHeldItemConsume(IBattlePokemon pokemon, IItem heldItem, ref bool prevented);
} }
/// <summary> /// <summary>
@@ -831,7 +832,7 @@ public interface IScriptChangeIncomingDamage
/// <summary> /// <summary>
/// This function allows a script to change any kind of damage that is incoming. /// This function allows a script to change any kind of damage that is incoming.
/// </summary> /// </summary>
void ChangeIncomingDamage(IPokemon pokemon, DamageSource source, ref uint damage); void ChangeIncomingDamage(IBattlePokemon pokemon, DamageSource source, ref uint damage);
} }
/// <summary> /// <summary>
@@ -853,7 +854,7 @@ public interface IScriptPreventHeal
/// <summary> /// <summary>
/// This function allows a script to prevent a Pokemon from being healed. /// This function allows a script to prevent a Pokemon from being healed.
/// </summary> /// </summary>
void PreventHeal(IPokemon pokemon, uint heal, bool allowRevive, ref bool prevented); void PreventHeal(IBattlePokemon pokemon, uint heal, bool allowRevive, ref bool prevented);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// for the effectiveness calculation.
/// </summary> /// </summary>
void ChangeTypesForMove(IExecutingMove executingMove, IPokemon target, byte hitIndex, IList<TypeIdentifier> types); void ChangeTypesForMove(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
IList<TypeIdentifier> types);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// be set, and will be used for the effectiveness calculation.
/// </summary> /// </summary>
void ChangeTypesForIncomingMove(IExecutingMove executingMove, IPokemon target, byte hitIndex, void ChangeTypesForIncomingMove(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
IList<TypeIdentifier> types); IList<TypeIdentifier> 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 /// 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. /// are sometimes a status move, and sometimes a damaging move, such as pollen puff.
/// </summary> /// </summary>
void ChangeCategory(IExecutingMove move, IPokemon target, byte hitIndex, ref MoveCategory category); void ChangeCategory(IExecutingMove move, IBattlePokemon target, byte hitIndex, ref MoveCategory category);
} }
/// <summary> /// <summary>
@@ -901,7 +903,7 @@ public interface IScriptOnBeforeHit
/// <summary> /// <summary>
/// Triggers first when we're about to hit a target. /// Triggers first when we're about to hit a target.
/// </summary> /// </summary>
void OnBeforeHit(IExecutingMove move, IPokemon target, byte hitIndex); void OnBeforeHit(IExecutingMove move, IBattlePokemon target, byte hitIndex);
} }
/// <summary> /// <summary>
@@ -912,7 +914,7 @@ public interface IScriptPreventStatusChange
/// <summary> /// <summary>
/// This function allows a script to prevent a Pokemon from being affected by a status condition. /// This function allows a script to prevent a Pokemon from being affected by a status condition.
/// </summary> /// </summary>
void PreventStatusChange(IPokemon pokemon, StringKey status, bool selfInflicted, ref bool preventStatus); void PreventStatusChange(IBattlePokemon pokemon, StringKey status, bool selfInflicted, ref bool preventStatus);
} }
/// <summary> /// <summary>
@@ -923,7 +925,7 @@ public interface IScriptOnAfterStatusChange
/// <summary> /// <summary>
/// This function triggers after a status condition has been applied to a Pokemon. /// This function triggers after a status condition has been applied to a Pokemon.
/// </summary> /// </summary>
void OnAfterStatusChange(IPokemon pokemon, StringKey status, IPokemon? originPokemon); void OnAfterStatusChange(IBattlePokemon pokemon, StringKey status, IPokemon? originPokemon);
} }
/// <summary> /// <summary>
@@ -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 /// 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. /// such as levitate, and allows for moves such as earthquake to not hit the Pokémon.
/// </summary> /// </summary>
void IsFloating(IPokemon pokemon, ref bool isFloating); void IsFloating(IBattlePokemon pokemon, ref bool isFloating);
} }
/// <summary> /// <summary>
@@ -991,7 +993,7 @@ public interface IScriptModifyIsContact
/// <summary> /// <summary>
/// Modifies whether a move is a contact move or not. This is used for abilities such as Long Reach. /// Modifies whether a move is a contact move or not. This is used for abilities such as Long Reach.
/// </summary> /// </summary>
void ModifyIsContact(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool isContact); void ModifyIsContact(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool isContact);
} }
/// <summary> /// <summary>
@@ -1002,7 +1004,7 @@ public interface IScriptPreventHeldItemSteal
/// <summary> /// <summary>
/// This function allows a script to prevent a held item from being stolen by an effect such as Thief or Covet. /// This function allows a script to prevent a held item from being stolen by an effect such as Thief or Covet.
/// </summary> /// </summary>
void PreventHeldItemSteal(IPokemon pokemon, IItem heldItem, ref bool prevent); void PreventHeldItemSteal(IBattlePokemon pokemon, IItem heldItem, ref bool prevent);
} }
/// <summary> /// <summary>
@@ -1013,7 +1015,7 @@ public interface IScriptOnAfterHeldItemChange
/// <summary> /// <summary>
/// This function allows a script to run after a held item has changed. /// This function allows a script to run after a held item has changed.
/// </summary> /// </summary>
void OnAfterHeldItemChange(IPokemon pokemon, IItem? previous, IItem? item); void OnAfterHeldItemChange(IBattlePokemon pokemon, IItem? previous, IItem? item);
} }
/// <summary> /// <summary>
@@ -1080,7 +1082,7 @@ public interface IScriptChangeExperienceGained
/// This function is triggered on a Pokemon and its parents when the given Pokemon gains experience, /// This function is triggered on a Pokemon and its parents when the given Pokemon gains experience,
/// and allows for changing this amount of experience. /// and allows for changing this amount of experience.
/// </summary> /// </summary>
void ChangeExperienceGained(IPokemon faintedPokemon, IPokemon winningPokemon, ref uint amount); void ChangeExperienceGained(IBattlePokemon faintedPokemon, IBattlePokemon winningPokemon, ref uint amount);
} }
/// <summary> /// <summary>
@@ -1093,7 +1095,8 @@ public interface IScriptShareExperience
/// and allows for making the experience be shared across multiple Pokemon. /// 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. /// Amount is the modifier for how much experience is shared, with 1 being the default amount.
/// </summary> /// </summary>
void ShareExperience(IPokemon faintedPokemon, IPokemon winningPokemon, ref bool share, ref float amount); void ShareExperience(IBattlePokemon faintedPokemon, IBattlePokemon winningPokemon, ref bool share,
ref float amount);
} }
/// <summary> /// <summary>
@@ -1117,7 +1120,7 @@ public interface IScriptChangeCatchRateBonus
/// rate of this attempt. Pokeball modifier effects should be implemented here, as well as for /// rate of this attempt. Pokeball modifier effects should be implemented here, as well as for
/// example status effects that change capture rates. /// example status effects that change capture rates.
/// </summary> /// </summary>
void ChangeCatchRateBonus(IPokemon pokemon, IItem pokeball, ref byte modifier); void ChangeCatchRateBonus(IBattlePokemon pokemon, IItem pokeball, ref byte modifier);
} }
/// <summary> /// <summary>
@@ -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 /// A custom case goes when 255 is returned, in which case the entire accuracy check is skipped, and the move
/// will always hit. /// will always hit.
/// </summary> /// </summary>
void ChangeAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy); void ChangeAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref int modifiedAccuracy);
} }
/// <summary> /// <summary>
@@ -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 /// A custom case goes when 255 is returned, in which case the entire accuracy check is skipped, and the move
/// will always hit. /// will always hit.
/// </summary> /// </summary>
void ChangeIncomingAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy); void ChangeIncomingAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
ref int modifiedAccuracy);
} }

View File

@@ -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;
/// <summary>
/// Regression tests for the battle lifecycle of <see cref="IBattlePokemon"/>: battle-only state must die
/// with the battle, and the underlying <see cref="IPokemon"/> must come out of a battle unchanged except
/// for the deliberately persistent parts (health, PP, non-volatile status, experience).
/// </summary>
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;
}
/// <summary>
/// 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
/// <see cref="BattleResult.CapturedPokemon"/> as the persistent Pokémon.
/// </summary>
[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);
}
/// <summary>
/// Items removed or stolen during a battle are a battle-only overlay: after the battle, the victim
/// still holds its item.
/// </summary>
[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);
}
/// <summary>
/// A battle-only form (such as a mega evolution) reverts to the original form when the battle ends.
/// </summary>
[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);
}
/// <summary>
/// A second battle with the same party starts with fresh battle state: no seen opponents or stale
/// original species from the previous battle.
/// </summary>
[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();
}
}

View File

@@ -0,0 +1,219 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic;
/// <summary>
/// Tests for the ephemeral <see cref="IBattlePokemon"/> wrapper. Battle-only state must live on the wrapper
/// and never leak into the underlying <see cref="IPokemon"/>; dropping the wrapper is all the cleanup a
/// battle needs.
/// </summary>
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<IEnumerable<PkmnLib.Dynamic.ScriptHandling.ScriptContainer>>();
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();
}
}

View File

@@ -8,8 +8,8 @@ public class ChoiceQueueTests
[Test] [Test]
public async Task ChoiceQueue_HighSpeedFirstWhenPriorityEqual() public async Task ChoiceQueue_HighSpeedFirstWhenPriorityEqual()
{ {
var pokemon1 = Substitute.For<IPokemon>(); var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IPokemon>(); var pokemon2 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>(); var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1); choice1.User.Returns(pokemon1);
@@ -28,8 +28,8 @@ public class ChoiceQueueTests
[Test] [Test]
public async Task ChoiceQueue_HighPriorityFirst() public async Task ChoiceQueue_HighPriorityFirst()
{ {
var pokemon1 = Substitute.For<IPokemon>(); var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IPokemon>(); var pokemon2 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>(); var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1); choice1.User.Returns(pokemon1);
@@ -48,10 +48,10 @@ public class ChoiceQueueTests
[Test] [Test]
public async Task ChoiceQueue_MovePokemonChoiceNext() public async Task ChoiceQueue_MovePokemonChoiceNext()
{ {
var pokemon1 = Substitute.For<IPokemon>(); var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IPokemon>(); var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IPokemon>(); var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IPokemon>(); var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>(); var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1); choice1.User.Returns(pokemon1);
@@ -75,10 +75,10 @@ public class ChoiceQueueTests
[Test] [Test]
public async Task ChoiceQueue_MovePokemonChoiceNextFailsIfAlreadyExecuted() public async Task ChoiceQueue_MovePokemonChoiceNextFailsIfAlreadyExecuted()
{ {
var pokemon1 = Substitute.For<IPokemon>(); var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IPokemon>(); var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IPokemon>(); var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IPokemon>(); var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>(); var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1); choice1.User.Returns(pokemon1);
@@ -103,10 +103,10 @@ public class ChoiceQueueTests
[Test] [Test]
public async Task ChoiceQueue_MovePokemonChoiceLast() public async Task ChoiceQueue_MovePokemonChoiceLast()
{ {
var pokemon1 = Substitute.For<IPokemon>(); var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IPokemon>(); var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IPokemon>(); var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IPokemon>(); var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>(); var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1); choice1.User.Returns(pokemon1);
@@ -133,10 +133,10 @@ public class ChoiceQueueTests
[Test] [Test]
public async Task ChoiceQueue_MovePokemonChoiceLastFailsIfAlreadyExecuted() public async Task ChoiceQueue_MovePokemonChoiceLastFailsIfAlreadyExecuted()
{ {
var pokemon1 = Substitute.For<IPokemon>(); var pokemon1 = Substitute.For<IBattlePokemon>();
var pokemon2 = Substitute.For<IPokemon>(); var pokemon2 = Substitute.For<IBattlePokemon>();
var pokemon3 = Substitute.For<IPokemon>(); var pokemon3 = Substitute.For<IBattlePokemon>();
var pokemon4 = Substitute.For<IPokemon>(); var pokemon4 = Substitute.For<IBattlePokemon>();
var choice1 = Substitute.For<IMoveChoice>(); var choice1 = Substitute.For<IMoveChoice>();
choice1.User.Returns(pokemon1); choice1.User.Returns(pokemon1);

View File

@@ -7,16 +7,31 @@ namespace PkmnLib.Tests.Dynamic;
public class PokemonStatBoostTests public class PokemonStatBoostTests
{ {
private static IPokemon CreatePokemon() private static IBattlePokemon CreatePokemon()
{ {
var library = LibraryHelpers.LoadLibrary(); var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species)) if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species))
throw new InvalidOperationException("Failed to load bulbasaur 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, Index = 0,
IsHidden = false, IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy"); }, 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] [Test]

View File

@@ -6,65 +6,78 @@ using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic; namespace PkmnLib.Tests.Dynamic;
/// <summary> /// <summary>
/// Tests for the temporary move overlay (<see cref="IPokemon.LearnTemporaryMove"/>), used by effects such as /// Tests for the temporary move overlay (<see cref="IBattlePokemon.LearnTemporaryMove"/>), used by effects
/// Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay must be /// such as Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay
/// discarded by the engine itself when the Pokemon leaves the battlefield. /// must be discarded by the engine itself when the Pokemon leaves the battlefield.
/// </summary> /// </summary>
public class PokemonTemporaryMoveTests public class PokemonTemporaryMoveTests
{ {
private static IPokemon CreatePokemon() private static (IBattle battle, IBattlePokemon battlePokemon, IPokemon pokemon) CreateBattlePokemon()
{ {
var library = LibraryHelpers.LoadLibrary(); var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species)) if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species))
throw new InvalidOperationException("Failed to load bulbasaur species."); throw new InvalidOperationException("Failed to load bulbasaur species.");
var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
{ IPokemon CreateBulbasaur() =>
Index = 0, new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
IsHidden = false, {
}, 50, 0, Gender.Male, 0, "hardy"); Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
var pokemon = CreateBulbasaur();
pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0); 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] [Test]
public async Task LearnTemporaryMove_ReplacesMoveInMovesButNotInBaseMoves() 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(battlePokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
await Assert.That(pokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic); await Assert.That(battlePokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic);
await Assert.That(pokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); await Assert.That(battlePokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
} }
[Test] [Test]
public async Task SetOnBattlefield_LeavingField_RestoresOriginalMoveWithItsPP() public async Task OnSwitchedOut_RestoresOriginalMoveWithItsPP()
{ {
var pokemon = CreatePokemon(); var (_, battlePokemon, _) = CreateBattlePokemon();
pokemon.SetBattleData(Substitute.For<IBattle>(), 0); battlePokemon.OnSwitchedIn(0);
pokemon.SetOnBattlefield(true);
// Use the original move once, so we can verify its PP survives the temporary replacement untouched. // 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(); originalMove.TryUse();
var expectedPp = originalMove.CurrentPp; var expectedPp = originalMove.CurrentPp;
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
pokemon.SetOnBattlefield(false); battlePokemon.OnSwitchedOut();
await Assert.That(ReferenceEquals(pokemon.Moves[0], originalMove)).IsTrue(); await Assert.That(ReferenceEquals(battlePokemon.Moves[0], originalMove)).IsTrue();
await Assert.That(pokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp); await Assert.That(battlePokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp);
} }
[Test] [Test]
public async Task ClearBattleData_RestoresOriginalMove() public async Task BattleEnd_LeavesOriginalMovesUntouched()
{ {
var pokemon = CreatePokemon(); var (battle, battlePokemon, pokemon) = CreateBattlePokemon();
pokemon.SetBattleData(Substitute.For<IBattle>(), 0); battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
pokemon.ClearBattleData(); battle.Dispose();
await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle"); await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
} }
@@ -72,10 +85,10 @@ public class PokemonTemporaryMoveTests
[Test] [Test]
public async Task Serialize_WithActiveTemporaryMove_WritesOriginalMove() public async Task Serialize_WithActiveTemporaryMove_WritesOriginalMove()
{ {
var pokemon = CreatePokemon(); var (_, battlePokemon, _) = CreateBattlePokemon();
pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0); 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"); await Assert.That(serialized.Moves[0]!.MoveName.ToString()).IsEqualTo("tackle");
} }

View File

@@ -22,7 +22,7 @@ public class SetPokemonAction : IntegrationTestAction
public override Task Execute(IBattle battle) 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); battle.Sides[Place[0]].SwapPokemon(Place[1], mon);
Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}"); Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}");
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -110,11 +110,12 @@ public class DeepCloneTests
new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]), new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
}; };
using var battle = new BattleImpl(library, parties, false, 2, 3, false, "grass", 0); using var battle = new BattleImpl(library, parties, false, 2, 3, false, "grass", 0);
battle.Sides[0].SwapPokemon(0, party1[0]); battle.Sides[0].SendOut(0, party1[0]!);
battle.Sides[1].SwapPokemon(0, party2[0]); battle.Sides[1].SendOut(0, party2[0]!);
party1[0]!.ChangeStatBoost(Statistic.Defense, 2, true, false); var battlePokemon1 = parties[0].GetBattlePokemon(party1[0]!)!;
await Assert.That(party1[0]!.StatBoost.Defense).IsEqualTo((sbyte)2); battlePokemon1.ChangeStatBoost(Statistic.Defense, 2, true, false);
party1[0]!.Volatile.Add(new ChargeBounceEffect(party1[0]!)); await Assert.That(battlePokemon1.StatBoost.Defense).IsEqualTo((sbyte)2);
battlePokemon1.Volatile.Add(new ChargeBounceEffect(battlePokemon1));
var clone = battle.DeepClone(); var clone = battle.DeepClone();
await Assert.That(clone).IsNotEqualTo(battle); await Assert.That(clone).IsNotEqualTo(battle);
@@ -129,11 +130,10 @@ public class DeepCloneTests
var pokemon = clone.Sides[0].Pokemon[0]!; var pokemon = clone.Sides[0].Pokemon[0]!;
await Assert.That(pokemon).IsNotNull(); await Assert.That(pokemon).IsNotNull();
await Assert.That(pokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!); await Assert.That(pokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!);
await Assert.That(pokemon.BattleData).IsNotNull(); await Assert.That(pokemon.UnderlyingPokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.UnderlyingPokemon);
await Assert.That(pokemon.BattleData).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.BattleData!); await Assert.That(pokemon.Battle).IsEqualTo((IBattle)clone);
await Assert.That(pokemon.BattleData!.Battle).IsEqualTo((IBattle)clone); await Assert.That(pokemon.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
await Assert.That(pokemon.BattleData!.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!); await Assert.That(pokemon.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
await Assert.That(pokemon.BattleData!.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2); await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2);
await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotNull(); await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotNull();
await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotEqualTo( await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotEqualTo(
@@ -142,7 +142,7 @@ public class DeepCloneTests
var ownerGetter = var ownerGetter =
typeof(ChargeBounceEffect).GetField("_owner", BindingFlags.NonPublic | BindingFlags.Instance)!; typeof(ChargeBounceEffect).GetField("_owner", BindingFlags.NonPublic | BindingFlags.Instance)!;
var owner = ownerGetter.GetValue(pokemon.Volatile.Get<ChargeBounceEffect>()!); var owner = ownerGetter.GetValue(pokemon.Volatile.Get<ChargeBounceEffect>()!);
await Assert.That((IPokemon)owner!).IsEqualTo(pokemon); await Assert.That((IBattlePokemon)owner!).IsEqualTo(pokemon);
pokemon.Volatile.Remove<ChargeBounceEffect>(); pokemon.Volatile.Remove<ChargeBounceEffect>();
await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNull(); await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNull();

View File

@@ -23,7 +23,7 @@ public class DamageCalculatorTests
[Test] [Test]
public async Task BulbapediaExampleDamageTest() public async Task BulbapediaExampleDamageTest()
{ {
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
// Imagine a level 75 Glaceon // Imagine a level 75 Glaceon
attacker.Level.Returns((byte)75); attacker.Level.Returns((byte)75);
// with an effective Attack stat of 123 // with an effective Attack stat of 123
@@ -31,7 +31,7 @@ public class DamageCalculatorTests
// We use 10 as the Ice type // We use 10 as the Ice type
attacker.Types.Returns([new TypeIdentifier(10, "ice")]); attacker.Types.Returns([new TypeIdentifier(10, "ice")]);
var defender = Substitute.For<IPokemon>(); var defender = Substitute.For<IBattlePokemon>();
// a Garchomp with an effective Defense stat of 163 // a Garchomp with an effective Defense stat of 163
defender.BoostedStats.Returns(new StatisticSet<uint>(1, 1, 163, 1, 1, 1)); defender.BoostedStats.Returns(new StatisticSet<uint>(1, 1, 163, 1, 1, 1));
defender.GetScripts().Returns(new ScriptIterator([])); defender.GetScripts().Returns(new ScriptIterator([]));

View File

@@ -14,12 +14,12 @@ public class AftermathTests
/// <summary> /// <summary>
/// Creates a fully mocked test setup for Aftermath tests. /// Creates a fully mocked test setup for Aftermath tests.
/// </summary> /// </summary>
private static (Aftermath aftermath, IExecutingMove move, IPokemon target, IPokemon user, EventHook eventHook, private static (Aftermath aftermath, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, EventHook
IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100) eventHook, IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100)
{ {
var aftermath = new Aftermath(); var aftermath = new Aftermath();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(isContact); hitData.IsContact.Returns(isContact);
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -30,20 +30,14 @@ public class AftermathTests
// Setup empty sides by default (no Damp on field) // Setup empty sides by default (no Damp on field)
var side = Substitute.For<IBattleSide>(); var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?>()); side.Pokemon.Returns(new List<IBattlePokemon?>());
battle.Sides.Returns(new[] { side }); battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>(); var user = Substitute.For<IBattlePokemon>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>();
user.IsUsable.Returns(true); user.IsUsable.Returns(true);
user.Battle.Returns(battle);
user.MaxHealth.Returns(userMaxHealth); 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); move.User.Returns(user);
return (aftermath, move, target, user, eventHook, battle); return (aftermath, move, target, user, eventHook, battle);
@@ -52,7 +46,7 @@ public class AftermathTests
/// <summary> /// <summary>
/// Helper to extract the damage amount from a substitute's received Damage calls. /// Helper to extract the damage amount from a substitute's received Damage calls.
/// </summary> /// </summary>
private static uint GetDamageDealt(IPokemon user) private static uint GetDamageDealt(IBattlePokemon user)
{ {
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return damageCall != null ? (uint)damageCall.GetArguments()[0]! : 0; return damageCall != null ? (uint)damageCall.GetArguments()[0]! : 0;
@@ -61,7 +55,7 @@ public class AftermathTests
/// <summary> /// <summary>
/// Helper to extract the damage source from a substitute's received Damage calls. /// Helper to extract the damage source from a substitute's received Damage calls.
/// </summary> /// </summary>
private static DamageSource? GetDamageSource(IPokemon user) private static DamageSource? GetDamageSource(IBattlePokemon user)
{ {
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return damageCall != null ? (DamageSource)damageCall.GetArguments()[1]! : null; return damageCall != null ? (DamageSource)damageCall.GetArguments()[1]! : null;
@@ -151,12 +145,12 @@ public class AftermathTests
// Arrange // Arrange
var aftermath = new Aftermath(); var aftermath = new Aftermath();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(true); hitData.IsContact.Returns(true);
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.IsUsable.Returns(false); // Attacker already fainted user.IsUsable.Returns(false); // Attacker already fainted
move.User.Returns(user); move.User.Returns(user);
@@ -168,34 +162,6 @@ public class AftermathTests
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse(); await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
} }
/// <summary>
/// Bulbapedia: "the attacking Pokémon takes damage".
/// If the attacker has no battle data, no damage should be dealt.
/// </summary>
[Test]
public async Task OnFaint_AttackerHasNoBattleData_DoesNotDealDamage()
{
// Arrange
var aftermath = new Aftermath();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(true);
move.GetHitData(target, 0).Returns(hitData);
var user = Substitute.For<IPokemon>();
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();
}
/// <summary> /// <summary>
/// Technical test: Verifies OnFaint handles the case where no attack was received. /// Technical test: Verifies OnFaint handles the case where no attack was received.
/// </summary> /// </summary>
@@ -204,7 +170,7 @@ public class AftermathTests
{ {
// Arrange // Arrange
var aftermath = new Aftermath(); var aftermath = new Aftermath();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act & Assert - Should not throw when _lastAttack is null // Act & Assert - Should not throw when _lastAttack is null
aftermath.OnFaint(target, DamageSource.MoveDamage); aftermath.OnFaint(target, DamageSource.MoveDamage);
@@ -220,19 +186,16 @@ public class AftermathTests
{ {
// Arrange // Arrange
var aftermath = new Aftermath(); var aftermath = new Aftermath();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
var eventHook = new EventHook(); var eventHook = new EventHook();
battle.EventHook.Returns(eventHook); battle.EventHook.Returns(eventHook);
// Setup empty sides (no Damp on field) // Setup empty sides (no Damp on field)
var side = Substitute.For<IBattleSide>(); var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?>()); side.Pokemon.Returns(new List<IBattlePokemon?>());
battle.Sides.Returns(new[] { side }); battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
// First hit - non-contact // First hit - non-contact
var move1 = Substitute.For<IExecutingMove>(); var move1 = Substitute.For<IExecutingMove>();
var hitData1 = Substitute.For<IHitData>(); var hitData1 = Substitute.For<IHitData>();
@@ -244,10 +207,10 @@ public class AftermathTests
var hitData2 = Substitute.For<IHitData>(); var hitData2 = Substitute.For<IHitData>();
hitData2.IsContact.Returns(true); hitData2.IsContact.Returns(true);
move2.GetHitData(target, 0).Returns(hitData2); move2.GetHitData(target, 0).Returns(hitData2);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.IsUsable.Returns(true); user.IsUsable.Returns(true);
user.BattleData.Returns(battleData);
user.MaxHealth.Returns(100u); user.MaxHealth.Returns(100u);
user.Battle.Returns(battle);
move2.User.Returns(user); move2.User.Returns(user);
// Act // Act
@@ -268,7 +231,7 @@ public class AftermathTests
{ {
// Arrange // Arrange
var aftermath = new Aftermath(); var aftermath = new Aftermath();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// First hit - contact // First hit - contact
var move1 = Substitute.For<IExecutingMove>(); var move1 = Substitute.For<IExecutingMove>();
@@ -346,14 +309,14 @@ public class AftermathTests
var (aftermath, move, target, user, _, battle) = CreateFullTestSetup(true, 100); var (aftermath, move, target, user, _, battle) = CreateFullTestSetup(true, 100);
// Create a Pokemon with Damp ability on the field // Create a Pokemon with Damp ability on the field
var dampPokemon = Substitute.For<IPokemon>(); var dampPokemon = Substitute.For<IBattlePokemon>();
var dampAbility = Substitute.For<IAbility>(); var dampAbility = Substitute.For<IAbility>();
dampAbility.Name.Returns(new StringKey("damp")); dampAbility.Name.Returns(new StringKey("damp"));
dampPokemon.ActiveAbility.Returns(dampAbility); dampPokemon.ActiveAbility.Returns(dampAbility);
// Update battle sides to include the Damp Pokemon // Update battle sides to include the Damp Pokemon
var side = Substitute.For<IBattleSide>(); var side = Substitute.For<IBattleSide>();
side.Pokemon.Returns(new List<IPokemon?> { dampPokemon }); side.Pokemon.Returns(new List<IBattlePokemon?> { dampPokemon });
battle.Sides.Returns(new[] { side }); battle.Sides.Returns(new[] { side });
// Act // Act

View File

@@ -16,9 +16,9 @@ public class MegaLauncherTests
{ {
// Arrange // Arrange
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var healPercent = 0.5f; var healPercent = 0.5f;
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
move.UseMove.Category.Returns(MoveCategory.Special); move.UseMove.Category.Returns(MoveCategory.Special);
move.UseMove.HasFlag(MoveFlags.Pulse).Returns(true); move.UseMove.HasFlag(MoveFlags.Pulse).Returns(true);

View File

@@ -22,7 +22,7 @@ public class FireGrassPledgeMoveTests
/// Creates a fully mocked test setup. The target's battle side gets a real <see cref="ScriptSet"/> as /// Creates a fully mocked test setup. The target's battle side gets a real <see cref="ScriptSet"/> as
/// its volatile script set, so the sea of fire created by the combined move can be inspected and driven. /// its volatile script set, so the sea of fire created by the combined move can be inspected and driven.
/// </summary> /// </summary>
private static (FireGrassPledgeMove script, IExecutingMove move, IPokemon target, IBattleSide side, IScriptSet private static (FireGrassPledgeMove script, IExecutingMove move, IBattlePokemon target, IBattleSide side, IScriptSet
sideVolatile) CreateTestSetup() sideVolatile) CreateTestSetup()
{ {
var script = new FireGrassPledgeMove(); var script = new FireGrassPledgeMove();
@@ -33,11 +33,9 @@ public class FireGrassPledgeMoveTests
var sideVolatile = new ScriptSet(side); var sideVolatile = new ScriptSet(side);
side.VolatileScripts.Returns(sideVolatile); side.VolatileScripts.Returns(sideVolatile);
var battleData = Substitute.For<IPokemonBattleData>(); var target = Substitute.For<IBattlePokemon>();
battleData.BattleSide.Returns(side);
var target = Substitute.For<IPokemon>();
target.BattleData.Returns(battleData);
target.BattleSide.Returns(side);
return (script, move, target, side, sideVolatile); 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 /// 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. /// covered by the sea of fire.
/// </summary> /// </summary>
private static IPokemon CreateSidePokemon(string typeName, uint maxHealth) private static IBattlePokemon CreateSidePokemon(string typeName, uint maxHealth)
{ {
var pokemon = Substitute.For<IPokemon>(); var pokemon = Substitute.For<IBattlePokemon>();
pokemon.Types.Returns(new List<TypeIdentifier> { new(1, typeName) }); pokemon.Types.Returns(new List<TypeIdentifier> { new(1, typeName) });
pokemon.MaxHealth.Returns(maxHealth); pokemon.MaxHealth.Returns(maxHealth);
return pokemon; return pokemon;
@@ -56,7 +54,7 @@ public class FireGrassPledgeMoveTests
/// <summary> /// <summary>
/// Helper to count the Damage calls a Pokémon received. /// Helper to count the Damage calls a Pokémon received.
/// </summary> /// </summary>
private static int CountDamageCalls(IPokemon pokemon) => private static int CountDamageCalls(IBattlePokemon pokemon) =>
pokemon.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage"); pokemon.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage");
/// <summary> /// <summary>
@@ -112,24 +110,6 @@ public class FireGrassPledgeMoveTests
await Assert.That(sideVolatile.Get<SeaOfFireEffect>()).IsNotNull(); await Assert.That(sideVolatile.Get<SeaOfFireEffect>()).IsNotNull();
} }
/// <summary>
/// Technical test: if the target has no <see cref="IPokemon.BattleData"/>, no sea of fire is created
/// and the script does not throw.
/// </summary>
[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);
}
/// <summary> /// <summary>
/// Bulbapedia: "The sea of fire damages all non-Fire Pokémon on that side of the field for 1/8 of /// 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." /// their maximum HP at the end of each turn."
@@ -141,7 +121,7 @@ public class FireGrassPledgeMoveTests
// Arrange // Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup(); var (script, move, target, side, sideVolatile) = CreateTestSetup();
var waterPokemon = CreateSidePokemon("water", maxHealth); var waterPokemon = CreateSidePokemon("water", maxHealth);
side.Pokemon.Returns(new List<IPokemon?> { waterPokemon }); side.Pokemon.Returns(new List<IBattlePokemon?> { waterPokemon });
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!; var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!;
@@ -164,7 +144,7 @@ public class FireGrassPledgeMoveTests
// Arrange // Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup(); var (script, move, target, side, sideVolatile) = CreateTestSetup();
var firePokemon = CreateSidePokemon("fire", 100); var firePokemon = CreateSidePokemon("fire", 100);
side.Pokemon.Returns(new List<IPokemon?> { firePokemon }); side.Pokemon.Returns(new List<IBattlePokemon?> { firePokemon });
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!; var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!;
@@ -185,7 +165,7 @@ public class FireGrassPledgeMoveTests
// Arrange // Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup(); var (script, move, target, side, sideVolatile) = CreateTestSetup();
var waterPokemon = CreateSidePokemon("water", 96); var waterPokemon = CreateSidePokemon("water", 96);
side.Pokemon.Returns(new List<IPokemon?> { waterPokemon }); side.Pokemon.Returns(new List<IBattlePokemon?> { waterPokemon });
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!; var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!;
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
@@ -207,7 +187,7 @@ public class FireGrassPledgeMoveTests
{ {
// Arrange // Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup(); var (script, move, target, side, sideVolatile) = CreateTestSetup();
side.Pokemon.Returns(new List<IPokemon?>()); side.Pokemon.Returns(new List<IBattlePokemon?>());
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!; var seaOfFire = sideVolatile.Get<SeaOfFireEffect>()!;
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();

View File

@@ -20,7 +20,7 @@ public class FireWaterPledgeMoveTests
/// Creates a fully mocked test setup. The user's battle side gets a real <see cref="ScriptSet"/> as its /// Creates a fully mocked test setup. The user's battle side gets a real <see cref="ScriptSet"/> as its
/// volatile script set, so the rainbow created by the combined move can be inspected and driven. /// volatile script set, so the rainbow created by the combined move can be inspected and driven.
/// </summary> /// </summary>
private static (FireWaterPledgeMove script, IExecutingMove move, IPokemon user, IBattleSide side, IScriptSet private static (FireWaterPledgeMove script, IExecutingMove move, IBattlePokemon user, IBattleSide side, IScriptSet
sideVolatile) CreateTestSetup() sideVolatile) CreateTestSetup()
{ {
var script = new FireWaterPledgeMove(); var script = new FireWaterPledgeMove();
@@ -31,11 +31,9 @@ public class FireWaterPledgeMoveTests
var sideVolatile = new ScriptSet(side); var sideVolatile = new ScriptSet(side);
side.VolatileScripts.Returns(sideVolatile); side.VolatileScripts.Returns(sideVolatile);
var battleData = Substitute.For<IPokemonBattleData>(); var user = Substitute.For<IBattlePokemon>();
battleData.BattleSide.Returns(side);
var user = Substitute.For<IPokemon>();
user.BattleData.Returns(battleData);
move.User.Returns(user); move.User.Returns(user);
user.BattleSide.Returns(side);
return (script, move, user, side, sideVolatile); return (script, move, user, side, sideVolatile);
} }
@@ -67,7 +65,7 @@ public class FireWaterPledgeMoveTests
{ {
// Arrange // Arrange
var (script, move, _, _, _) = CreateTestSetup(); var (script, move, _, _, _) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 80; ushort basePower = 80;
// Act // Act
@@ -86,7 +84,7 @@ public class FireWaterPledgeMoveTests
{ {
// Arrange // Arrange
var (script, move, _, _, sideVolatile) = CreateTestSetup(); var (script, move, _, _, sideVolatile) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
@@ -95,25 +93,6 @@ public class FireWaterPledgeMoveTests
await Assert.That(sideVolatile.Get<RainbowEffect>()).IsNotNull(); await Assert.That(sideVolatile.Get<RainbowEffect>()).IsNotNull();
} }
/// <summary>
/// Technical test: if the user has no <see cref="IPokemon.BattleData"/>, no rainbow is created and the
/// script does not throw.
/// </summary>
[Test]
public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
{
// Arrange
var (script, move, user, _, sideVolatile) = CreateTestSetup();
user.BattleData.Returns((IPokemonBattleData?)null);
var target = Substitute.For<IPokemon>();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(sideVolatile.Count).IsEqualTo(0);
}
/// <summary> /// <summary>
/// Bulbapedia: "The rainbow doubles the probability of additional effects taking place for moves used /// Bulbapedia: "The rainbow doubles the probability of additional effects taking place for moves used
/// by that side of the field". /// by that side of the field".
@@ -123,7 +102,7 @@ public class FireWaterPledgeMoveTests
{ {
// Arrange // Arrange
var (script, move, _, _, sideVolatile) = CreateTestSetup(); var (script, move, _, _, sideVolatile) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var rainbow = sideVolatile.Get<RainbowEffect>()!; var rainbow = sideVolatile.Get<RainbowEffect>()!;
@@ -143,7 +122,7 @@ public class FireWaterPledgeMoveTests
{ {
// Arrange // Arrange
var (script, move, _, side, sideVolatile) = CreateTestSetup(); var (script, move, _, side, sideVolatile) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var rainbow = sideVolatile.Get<RainbowEffect>()!; var rainbow = sideVolatile.Get<RainbowEffect>()!;
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
@@ -165,7 +144,7 @@ public class FireWaterPledgeMoveTests
{ {
// Arrange // Arrange
var (script, move, _, side, sideVolatile) = CreateTestSetup(); var (script, move, _, side, sideVolatile) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
var rainbow = sideVolatile.Get<RainbowEffect>()!; var rainbow = sideVolatile.Get<RainbowEffect>()!;
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();

View File

@@ -11,9 +11,9 @@ public class AcrobaticsTests
{ {
// Arrange // Arrange
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 10; ushort basePower = 10;
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.HeldItem.Returns((IItem?)null); user.HeldItem.Returns((IItem?)null);
move.User.Returns(user); move.User.Returns(user);
var acrobatics = new Acrobatics(); var acrobatics = new Acrobatics();
@@ -30,9 +30,9 @@ public class AcrobaticsTests
{ {
// Arrange // Arrange
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 10; ushort basePower = 10;
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.HeldItem.Returns(Substitute.For<IItem>()); user.HeldItem.Returns(Substitute.For<IItem>());
move.User.Returns(user); move.User.Returns(user);
var acrobatics = new Acrobatics(); var acrobatics = new Acrobatics();
@@ -49,9 +49,9 @@ public class AcrobaticsTests
{ {
// Arrange // Arrange
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = ushort.MaxValue - 100; ushort basePower = ushort.MaxValue - 100;
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
user.HeldItem.Returns((IItem?)null); user.HeldItem.Returns((IItem?)null);
var acrobatics = new Acrobatics(); var acrobatics = new Acrobatics();

View File

@@ -13,24 +13,22 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class AcupressureTests public class AcupressureTests
{ {
private static (Acupressure script, IExecutingMove move, IPokemon target, IBattleRandom random, IHitData hitData) private static (Acupressure script, IExecutingMove move, IBattlePokemon target, IBattleRandom random, IHitData
CreateTestSetup() hitData) CreateTestSetup()
{ {
var script = new Acupressure(); var script = new Acupressure();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
var random = Substitute.For<IBattleRandom>(); var random = Substitute.For<IBattleRandom>();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Random.Returns(random); battle.Random.Returns(random);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BattleData.Returns(battleData);
move.User.Returns(user); move.User.Returns(user);
user.Battle.Returns(battle);
target.StatBoost.Returns(new StatBoostStatisticSet()); target.StatBoost.Returns(new StatBoostStatisticSet());
@@ -40,7 +38,7 @@ public class AcupressureTests
/// <summary> /// <summary>
/// Helper to extract the stat argument of a ChangeStatBoost call received by the target. /// Helper to extract the stat argument of a ChangeStatBoost call received by the target.
/// </summary> /// </summary>
private static object?[]? GetStatBoostCallArgs(IPokemon target) private static object?[]? GetStatBoostCallArgs(IBattlePokemon target)
{ {
var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
return call?.GetArguments(); return call?.GetArguments();

View File

@@ -12,7 +12,7 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class AfterYouTests public class AfterYouTests
{ {
private static IMoveChoice CreateChoice(IPokemon user, uint speed) private static IMoveChoice CreateChoice(IBattlePokemon user, uint speed)
{ {
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
@@ -25,16 +25,14 @@ public class AfterYouTests
var script = new AfterYou(); var script = new AfterYou();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(Arg.Any<IPokemon>(), Arg.Any<byte>()).Returns(hitData); move.GetHitData(Arg.Any<IBattlePokemon>(), Arg.Any<byte>()).Returns(hitData);
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.ChoiceQueue.Returns(queue); battle.ChoiceQueue.Returns(queue);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BattleData.Returns(battleData);
move.User.Returns(user); move.User.Returns(user);
user.Battle.Returns(battle);
return (script, move, hitData); return (script, move, hitData);
} }
@@ -47,9 +45,9 @@ public class AfterYouTests
public async Task OnSecondaryEffect_TargetLaterInQueue_TargetMovesNext() public async Task OnSecondaryEffect_TargetLaterInQueue_TargetMovesNext()
{ {
// Arrange // Arrange
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var other = Substitute.For<IPokemon>(); var other = Substitute.For<IBattlePokemon>();
// Sorted by speed: user (100), other (75), target (50) // Sorted by speed: user (100), other (75), target (50)
var queue = new BattleChoiceQueue([ var queue = new BattleChoiceQueue([
CreateChoice(user, 100), CreateChoice(other, 75), CreateChoice(target, 50), CreateChoice(user, 100), CreateChoice(other, 75), CreateChoice(target, 50),
@@ -75,8 +73,8 @@ public class AfterYouTests
public void OnSecondaryEffect_TargetAlreadyMoved_Fails() public void OnSecondaryEffect_TargetAlreadyMoved_Fails()
{ {
// Arrange // Arrange
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Sorted by speed: target (100), user (50) // Sorted by speed: target (100), user (50)
var queue = new BattleChoiceQueue([ var queue = new BattleChoiceQueue([
CreateChoice(target, 100), CreateChoice(user, 50), CreateChoice(target, 100), CreateChoice(user, 50),
@@ -103,8 +101,8 @@ public class AfterYouTests
public void OnSecondaryEffect_TargetAlreadyNext_Fails() public void OnSecondaryEffect_TargetAlreadyNext_Fails()
{ {
// Arrange // Arrange
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Sorted by speed: user (100), target (50) // Sorted by speed: user (100), target (50)
var queue = new BattleChoiceQueue([ var queue = new BattleChoiceQueue([
CreateChoice(user, 100), CreateChoice(target, 50), CreateChoice(user, 100), CreateChoice(target, 50),
@@ -132,7 +130,7 @@ public class AfterYouTests
var (script, move, hitData) = CreateTestSetup(null); var (script, move, hitData) = CreateTestSetup(null);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
hitData.DidNotReceive().Fail(); hitData.DidNotReceive().Fail();

View File

@@ -15,9 +15,9 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class AssistTests public class AssistTests
{ {
private static IPokemon CreatePokemonWithMoves(params string[] moveNames) private static IBattlePokemon CreatePokemonWithMoves(params string[] moveNames)
{ {
var pokemon = Substitute.For<IPokemon>(); var pokemon = Substitute.For<IBattlePokemon>();
var moves = moveNames.Select(name => var moves = moveNames.Select(name =>
{ {
var learned = Substitute.For<ILearnedMove>(); var learned = Substitute.For<ILearnedMove>();
@@ -30,27 +30,23 @@ public class AssistTests
return pokemon; return pokemon;
} }
private static (Assist script, IMoveChoice choice, IPokemon user, IBattleRandom random) CreateTestSetup( private static (Assist script, IMoveChoice choice, IBattlePokemon user, IBattleRandom random) CreateTestSetup(
params IPokemon?[] otherPartyMembers) params IBattlePokemon?[] otherPartyMembers)
{ {
var script = new Assist(); var script = new Assist();
var user = CreatePokemonWithMoves("tackle"); var user = CreatePokemonWithMoves("tackle");
var members = new List<IPokemon?> { user }; var members = new List<IBattlePokemon?> { user };
members.AddRange(otherPartyMembers); members.AddRange(otherPartyMembers);
var party = Substitute.For<IPokemonParty>();
party.GetEnumerator().Returns(_ => members.GetEnumerator());
var battleParty = Substitute.For<IBattleParty>(); var battleParty = Substitute.For<IBattleParty>();
battleParty.Party.Returns(party); battleParty.BattlePokemon.Returns(members);
var random = Substitute.For<IBattleRandom>(); var random = Substitute.For<IBattleRandom>();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Parties.Returns(new[] { battleParty }); battle.Parties.Returns(new[] { battleParty });
battle.Random.Returns(random); battle.Random.Returns(random);
var battleData = Substitute.For<IPokemonBattleData>(); user.Battle.Returns(battle);
battleData.Battle.Returns(battle);
user.BattleData.Returns(battleData);
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
@@ -180,26 +176,4 @@ public class AssistTests
await Assert.That(moveName).IsEqualTo(new StringKey("growl")); await Assert.That(moveName).IsEqualTo(new StringKey("growl"));
choice.DidNotReceive().Fail(); choice.DidNotReceive().Fail();
} }
/// <summary>
/// Technical test: outside of battle (no battle data) the script returns without failing the choice.
/// </summary>
[Test]
public async Task ChangeMove_NoBattleData_DoesNothing()
{
// Arrange
var script = new Assist();
var user = Substitute.For<IPokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
var choice = Substitute.For<IMoveChoice>();
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"));
}
} }

View File

@@ -15,12 +15,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class AttractTests public class AttractTests
{ {
private static (Attract script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData) private static (Attract script, IExecutingMove move, IBattlePokemon target, IScriptSet targetVolatile, IHitData
CreateTestSetup(Gender userGender, Gender targetGender) hitData) CreateTestSetup(Gender userGender, Gender targetGender)
{ {
var script = new Attract(); var script = new Attract();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -28,7 +28,7 @@ public class AttractTests
target.Volatile.Returns(targetVolatile); target.Volatile.Returns(targetVolatile);
target.Gender.Returns(targetGender); target.Gender.Returns(targetGender);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.Gender.Returns(userGender); user.Gender.Returns(userGender);
move.User.Returns(user); move.User.Returns(user);

View File

@@ -31,12 +31,12 @@ public class AuroraVeilTests
} }
} }
private static (AuroraVeil script, IExecutingMove move, IPokemon target, IPokemon user, IScriptSet sideScripts, private static (AuroraVeil script, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, IScriptSet
IHitData hitData) CreateTestSetup(StringKey? weather) sideScripts, IHitData hitData) CreateTestSetup(StringKey? weather)
{ {
var script = new AuroraVeil(); var script = new AuroraVeil();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -51,13 +51,10 @@ public class AuroraVeilTests
side.VolatileScripts.Returns(sideScripts); side.VolatileScripts.Returns(sideScripts);
battle.Sides.Returns(new[] { side }); battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>(); var user = Substitute.For<IBattlePokemon>();
battleData.Battle.Returns(battle);
battleData.SideIndex.Returns((byte)0);
var user = Substitute.For<IPokemon>();
user.BattleData.Returns(battleData);
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>())); user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
user.SideIndex.Returns((byte)0);
user.Battle.Returns(battle);
move.User.Returns(user); move.User.Returns(user);
return (script, move, target, user, sideScripts, hitData); return (script, move, target, user, sideScripts, hitData);
@@ -154,25 +151,4 @@ public class AuroraVeilTests
var effect = (AuroraVeilEffect)((Func<Script?>)call.GetArguments()[1]!)()!; var effect = (AuroraVeilEffect)((Func<Script?>)call.GetArguments()[1]!)()!;
await Assert.That(effect.NumberOfTurns).IsEqualTo(8); await Assert.That(effect.NumberOfTurns).IsEqualTo(8);
} }
/// <summary>
/// Technical test: outside of battle (no battle data) the script returns without throwing.
/// </summary>
[Test]
public async Task OnSecondaryEffect_NoBattleData_DoesNothing()
{
// Arrange
var script = new AuroraVeil();
var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>();
var user = Substitute.For<IPokemon>();
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();
}
} }

View File

@@ -17,8 +17,8 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class AutotomizeTests public class AutotomizeTests
{ {
private static (Autotomize script, IExecutingMove move, IPokemon user, IScriptSet userVolatile, EventHook eventHook) private static (Autotomize script, IExecutingMove move, IBattlePokemon user, IScriptSet userVolatile, EventHook
CreateTestSetup(float weightInKg, bool speedRaiseSucceeds, AutotomizeEffect? existingEffect = null) eventHook) CreateTestSetup(float weightInKg, bool speedRaiseSucceeds, AutotomizeEffect? existingEffect = null)
{ {
var script = new Autotomize(); var script = new Autotomize();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
@@ -26,15 +26,13 @@ public class AutotomizeTests
var eventHook = new EventHook(); var eventHook = new EventHook();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.EventHook.Returns(eventHook); battle.EventHook.Returns(eventHook);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var userVolatile = Substitute.For<IScriptSet>(); var userVolatile = Substitute.For<IScriptSet>();
userVolatile.Get<AutotomizeEffect>().Returns(existingEffect); userVolatile.Get<AutotomizeEffect>().Returns(existingEffect);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BattleData.Returns(battleData);
user.Volatile.Returns(userVolatile); user.Volatile.Returns(userVolatile);
user.Battle.Returns(battle);
user.WeightInKg.Returns(weightInKg); user.WeightInKg.Returns(weightInKg);
user.ChangeStatBoost(Arg.Any<Statistic>(), Arg.Any<sbyte>(), Arg.Any<bool>(), Arg.Any<bool>()) user.ChangeStatBoost(Arg.Any<Statistic>(), Arg.Any<sbyte>(), Arg.Any<bool>(), Arg.Any<bool>())
.Returns(speedRaiseSucceeds); .Returns(speedRaiseSucceeds);
@@ -56,7 +54,7 @@ public class AutotomizeTests
var (script, move, user, _, _) = CreateTestSetup(100f, true); var (script, move, user, _, _) = CreateTestSetup(100f, true);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false); user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false);
@@ -79,7 +77,7 @@ public class AutotomizeTests
}; };
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue(); await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue();
@@ -100,7 +98,7 @@ public class AutotomizeTests
eventHook.Handler += (_, _) => eventFired = true; eventHook.Handler += (_, _) => eventFired = true;
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(ReceivedStackOrAdd(userVolatile)).IsFalse(); await Assert.That(ReceivedStackOrAdd(userVolatile)).IsFalse();
@@ -119,7 +117,7 @@ public class AutotomizeTests
var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true); var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue(); await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue();
@@ -140,7 +138,7 @@ public class AutotomizeTests
var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true, existingEffect); var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true, existingEffect);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert - the weight should still be reduced (to the minimum) // Assert - the weight should still be reduced (to the minimum)
await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue(); await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue();
@@ -160,7 +158,7 @@ public class AutotomizeTests
eventHook.Handler += (_, _) => eventFired = true; eventHook.Handler += (_, _) => eventFired = true;
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false); user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false);

View File

@@ -19,12 +19,12 @@ public class BanefulBunkerTests
/// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the /// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the
/// Pokémon using Baneful Bunker itself, as the move is self-targeted. /// Pokémon using Baneful Bunker itself, as the move is self-targeted.
/// </summary> /// </summary>
private static (BanefulBunker script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet private static (BanefulBunker script, IExecutingMove move, IBattlePokemon target, IHitData hitData, IScriptSet
) CreateProtectSetup(bool userMovesLast, float randomRoll) volatileSet ) CreateProtectSetup(bool userMovesLast, float randomRoll)
{ {
var script = new BanefulBunker(); var script = new BanefulBunker();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -38,9 +38,7 @@ public class BanefulBunkerTests
battle.ChoiceQueue.Returns(queue); battle.ChoiceQueue.Returns(queue);
battle.Random.Returns(random); battle.Random.Returns(random);
var battleData = Substitute.For<IPokemonBattleData>(); target.Battle.Returns(battle);
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
// Give the mock a real script iterator (used by the volatile add hook) and a real volatile script set. // 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<IEnumerable<ScriptContainer>>())); target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
@@ -54,12 +52,12 @@ public class BanefulBunkerTests
/// Creates a fully mocked setup for driving <see cref="BanefulBunkerEffect.BlockIncomingHit"/>, the /// Creates a fully mocked setup for driving <see cref="BanefulBunkerEffect.BlockIncomingHit"/>, the
/// volatile script that <see cref="BanefulBunker"/> attaches to its user. /// volatile script that <see cref="BanefulBunker"/> attaches to its user.
/// </summary> /// </summary>
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) CreateBlockSetup(bool isContact, bool hasProtectFlag, MoveCategory category = MoveCategory.Physical)
{ {
var effect = new BanefulBunkerEffect(); var effect = new BanefulBunkerEffect();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(isContact); hitData.IsContact.Returns(isContact);
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -69,19 +67,17 @@ public class BanefulBunkerTests
useMove.Category.Returns(category); useMove.Category.Returns(category);
move.UseMove.Returns(useMove); move.UseMove.Returns(useMove);
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>())); attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
move.User.Returns(attacker); move.User.Returns(attacker);
target.BattleData.Returns(Substitute.For<IPokemonBattleData>());
return (effect, move, target, attacker); return (effect, move, target, attacker);
} }
/// <summary> /// <summary>
/// Helper to extract the status name from a Pokémon's received <see cref="IPokemon.SetStatus"/> calls. /// Helper to extract the status name from a Pokémon's received <see cref="IBattlePokemon.SetStatus"/> calls.
/// </summary> /// </summary>
private static string? GetStatusSet(IPokemon pokemon) private static string? GetStatusSet(IBattlePokemon pokemon)
{ {
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus"); var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus");
return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null; return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null;

View File

@@ -21,9 +21,9 @@ public class BatonPassTests
/// Creates a mocked Pokémon with a real volatile <see cref="ScriptSet"/> and a real /// Creates a mocked Pokémon with a real volatile <see cref="ScriptSet"/> and a real
/// <see cref="StatBoostStatisticSet"/>. /// <see cref="StatBoostStatisticSet"/>.
/// </summary> /// </summary>
private static IPokemon CreateMockPokemon(out IScriptSet volatileSet) private static IBattlePokemon CreateMockPokemon(out IScriptSet volatileSet)
{ {
var pokemon = Substitute.For<IPokemon>(); var pokemon = Substitute.For<IBattlePokemon>();
pokemon.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>())); pokemon.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
var set = new ScriptSet(pokemon); var set = new ScriptSet(pokemon);
pokemon.Volatile.Returns(set); 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 /// Creates a fully mocked test setup for Baton Pass tests. The Pokémon to switch in is stored in the
/// move choice's <see cref="IMoveChoice.AdditionalData"/> under the <c>to_switch</c> key. /// move choice's <see cref="IMoveChoice.AdditionalData"/> under the <c>to_switch</c> key.
/// </summary> /// </summary>
private static (BatonPass script, IExecutingMove move, IPokemon user, IPokemon toSwitch, IBattleSide side, private static (BatonPass script, IExecutingMove move, IBattlePokemon user, IBattlePokemon toSwitch, IBattleSide
IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup() side, IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup()
{ {
var script = new BatonPass(); var script = new BatonPass();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
@@ -52,11 +52,9 @@ public class BatonPassTests
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Sides.Returns(new[] { side }); battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>(); user.Battle.Returns(battle);
battleData.Battle.Returns(battle); user.SideIndex.Returns((byte)0);
battleData.SideIndex.Returns((byte)0); user.Position.Returns((byte)1);
battleData.Position.Returns((byte)1);
user.BattleData.Returns(battleData);
move.User.Returns(user); move.User.Returns(user);
return (script, move, user, toSwitch, side, userVolatile, switchInVolatile); return (script, move, user, toSwitch, side, userVolatile, switchInVolatile);
@@ -213,7 +211,7 @@ public class BatonPassTests
script.OnSecondaryEffect(move, user, 0); script.OnSecondaryEffect(move, user, 0);
// Assert // Assert
side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>()); side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IBattlePokemon?>());
} }
/// <summary> /// <summary>
@@ -230,26 +228,6 @@ public class BatonPassTests
script.OnSecondaryEffect(move, user, 0); script.OnSecondaryEffect(move, user, 0);
// Assert // Assert
side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>()); side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IBattlePokemon?>());
}
/// <summary>
/// Technical test: if the user has no <see cref="IPokemon.BattleData"/>, the script returns without
/// switching and without clearing the user's volatile scripts.
/// </summary>
[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<byte>(), Arg.Any<IPokemon?>());
await Assert.That(userVolatile.Get<AutotomizeEffect>()).IsNotNull();
} }
} }

View File

@@ -17,28 +17,19 @@ public class BeakBlastTests
/// <summary> /// <summary>
/// Creates a fully mocked setup for driving <see cref="BeakBlast.OnBeforeTurnStart"/>. /// Creates a fully mocked setup for driving <see cref="BeakBlast.OnBeforeTurnStart"/>.
/// </summary> /// </summary>
private static (BeakBlast script, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook eventHook) private static (BeakBlast script, ITurnChoice choice, IBattlePokemon user, IScriptSet volatileSet, EventHook
CreateChargeSetup(bool hasBattleData = true) eventHook) CreateChargeSetup()
{ {
var script = new BeakBlast(); var script = new BeakBlast();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>())); user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
IScriptSet volatileSet = new ScriptSet(user); IScriptSet volatileSet = new ScriptSet(user);
user.Volatile.Returns(volatileSet); user.Volatile.Returns(volatileSet);
var eventHook = new EventHook(); var eventHook = new EventHook();
if (hasBattleData) var battle = Substitute.For<IBattle>();
{ battle.EventHook.Returns(eventHook);
var battle = Substitute.For<IBattle>(); user.Battle.Returns(battle);
battle.EventHook.Returns(eventHook);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
user.BattleData.Returns(battleData);
}
else
{
user.BattleData.Returns((IPokemonBattleData?)null);
}
var choice = Substitute.For<ITurnChoice>(); var choice = Substitute.For<ITurnChoice>();
choice.User.Returns(user); choice.User.Returns(user);
@@ -85,24 +76,7 @@ public class BeakBlastTests
// Assert // Assert
await Assert.That(capturedEvent).IsNotNull(); await Assert.That(capturedEvent).IsNotNull();
await Assert.That(capturedEvent!.Message).IsEqualTo("beak_blast_charge"); await Assert.That(capturedEvent!.Message).IsEqualTo("beak_blast_charge");
await Assert.That((IPokemon)capturedEvent.Parameters!["user"]).IsEqualTo(user); await Assert.That((IBattlePokemon)capturedEvent.Parameters!["user"]).IsEqualTo(user);
}
/// <summary>
/// Technical test: a Pokémon without <see cref="IPokemon.BattleData"/> is not in battle, so no charging
/// phase starts.
/// </summary>
[Test]
public async Task OnBeforeTurnStart_NoBattleData_DoesNotAddChargeEffect()
{
// Arrange
var (script, choice, _, volatileSet, _) = CreateChargeSetup(false);
// Act
script.OnBeforeTurnStart(choice);
// Assert
await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNull();
} }
/// <summary> /// <summary>
@@ -115,8 +89,8 @@ public class BeakBlastTests
// Arrange // Arrange
var script = new BeakBlast(); var script = new BeakBlast();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>())); user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
IScriptSet volatileSet = new ScriptSet(user); IScriptSet volatileSet = new ScriptSet(user);
user.Volatile.Returns(volatileSet); user.Volatile.Returns(volatileSet);
@@ -133,26 +107,26 @@ public class BeakBlastTests
/// <summary> /// <summary>
/// Creates a fully mocked setup for driving <see cref="BeakBlastEffect.OnIncomingHit"/>. /// Creates a fully mocked setup for driving <see cref="BeakBlastEffect.OnIncomingHit"/>.
/// </summary> /// </summary>
private static (BeakBlastEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker) private static (BeakBlastEffect effect, IExecutingMove move, IBattlePokemon target, IBattlePokemon attacker)
CreateIncomingHitSetup(bool isContact) CreateIncomingHitSetup(bool isContact)
{ {
var effect = new BeakBlastEffect(); var effect = new BeakBlastEffect();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
hitData.IsContact.Returns(isContact); hitData.IsContact.Returns(isContact);
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
move.User.Returns(attacker); move.User.Returns(attacker);
return (effect, move, target, attacker); return (effect, move, target, attacker);
} }
/// <summary> /// <summary>
/// Helper to extract the status name from a Pokémon's received <see cref="IPokemon.SetStatus"/> calls. /// Helper to extract the status name from a Pokémon's received <see cref="IBattlePokemon.SetStatus"/> calls.
/// </summary> /// </summary>
private static string? GetStatusSet(IPokemon pokemon) private static string? GetStatusSet(IBattlePokemon pokemon)
{ {
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus"); var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus");
return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null; return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null;

View File

@@ -19,11 +19,11 @@ public class BeatUpTests
{ {
/// <summary> /// <summary>
/// Creates a mocked party member with the given base Attack stat, usability, and optional non-volatile /// Creates a mocked party member with the given base Attack stat, usability, and optional non-volatile
/// status script in its <see cref="IPokemon.StatusScript"/>. /// status script in its <see cref="IBattlePokemon.StatusScript"/>.
/// </summary> /// </summary>
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<IPokemon>(); var pokemon = Substitute.For<IBattlePokemon>();
pokemon.IsUsable.Returns(usable); pokemon.IsUsable.Returns(usable);
pokemon.StatusScript.Returns(status == null ? new ScriptContainer() : new ScriptContainer(status)); pokemon.StatusScript.Returns(status == null ? new ScriptContainer() : new ScriptContainer(status));
var form = Substitute.For<IForm>(); var form = Substitute.For<IForm>();
@@ -35,24 +35,20 @@ public class BeatUpTests
/// <summary> /// <summary>
/// Creates a fully mocked test setup where the user and the given other Pokémon form a party in a battle. /// Creates a fully mocked test setup where the user and the given other Pokémon form a party in a battle.
/// </summary> /// </summary>
private static (BeatUp script, IMoveChoice choice, IExecutingMove move) CreateTestSetup(IPokemon user, private static (BeatUp script, IMoveChoice choice, IExecutingMove move) CreateTestSetup(IBattlePokemon user,
params IPokemon?[] otherPartyMembers) params IBattlePokemon?[] otherPartyMembers)
{ {
var script = new BeatUp(); var script = new BeatUp();
var members = new List<IPokemon?> { user }; var members = new List<IBattlePokemon?> { user };
members.AddRange(otherPartyMembers); members.AddRange(otherPartyMembers);
var party = Substitute.For<IPokemonParty>();
party.GetEnumerator().Returns(_ => members.GetEnumerator());
var battleParty = Substitute.For<IBattleParty>(); var battleParty = Substitute.For<IBattleParty>();
battleParty.Party.Returns(party); battleParty.BattlePokemon.Returns(members);
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Parties.Returns(new[] { battleParty }); battle.Parties.Returns(new[] { battleParty });
var battleData = Substitute.For<IPokemonBattleData>(); user.Battle.Returns(battle);
battleData.Battle.Returns(battle);
user.BattleData.Returns(battleData);
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
@@ -121,28 +117,6 @@ public class BeatUpTests
await Assert.That(numberOfHits).IsEqualTo((byte)1); await Assert.That(numberOfHits).IsEqualTo((byte)1);
} }
/// <summary>
/// Technical test: outside of battle (no <see cref="IPokemon.BattleData"/>) there are no relevant party
/// members, and the number of hits falls back to a single strike.
/// </summary>
[Test]
public async Task ChangeNumberOfHits_NoBattleData_SingleHit()
{
// Arrange
var script = new BeatUp();
var user = Substitute.For<IPokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
byte numberOfHits = 3;
// Act
script.ChangeNumberOfHits(choice, ref numberOfHits);
// Assert
await Assert.That(numberOfHits).IsEqualTo((byte)1);
}
/// <summary> /// <summary>
/// Bulbapedia: "the base power per strike is no longer 10, but instead individually based on the Attack /// 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". /// base stats of the party Pokémon: BasePower = BaseAttack(PartyMember)/10 + 5".
@@ -155,7 +129,7 @@ public class BeatUpTests
// Arrange // Arrange
var user = CreatePartyMember(baseAttack); var user = CreatePartyMember(baseAttack);
var (script, _, move) = CreateTestSetup(user); var (script, _, move) = CreateTestSetup(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 10; ushort basePower = 10;
// Act // Act
@@ -175,7 +149,7 @@ public class BeatUpTests
// Arrange // Arrange
var user = CreatePartyMember(100); var user = CreatePartyMember(100);
var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250)); var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250));
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 10; ushort basePower = 10;
// Act // Act
@@ -197,7 +171,7 @@ public class BeatUpTests
var user = CreatePartyMember(100); var user = CreatePartyMember(100);
var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250, status: new Burned()), var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250, status: new Burned()),
CreatePartyMember(60)); CreatePartyMember(60));
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 10; ushort basePower = 10;
// Act // Act
@@ -217,7 +191,7 @@ public class BeatUpTests
// Arrange // Arrange
var user = CreatePartyMember(100); var user = CreatePartyMember(100);
var (script, _, move) = CreateTestSetup(user); var (script, _, move) = CreateTestSetup(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
ushort basePower = 10; ushort basePower = 10;
// Act // Act

View File

@@ -13,13 +13,13 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
public class BelchTests public class BelchTests
{ {
/// <summary> /// <summary>
/// Creates a fully mocked test setup where the user's <see cref="IPokemonBattleData.ConsumedItems"/> /// Creates a fully mocked test setup where the user's <see cref="IBattlePokemon.ConsumedItems"/>
/// contains one item per given <see cref="ItemCategory"/>. /// contains one item per given <see cref="ItemCategory"/>.
/// </summary> /// </summary>
private static (Belch script, IMoveChoice choice) CreateTestSetup(params ItemCategory[] consumedItemCategories) private static (Belch script, IMoveChoice choice) CreateTestSetup(params ItemCategory[] consumedItemCategories)
{ {
var script = new Belch(); var script = new Belch();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var items = consumedItemCategories.Select(category => var items = consumedItemCategories.Select(category =>
{ {
@@ -27,9 +27,7 @@ public class BelchTests
item.Category.Returns(category); item.Category.Returns(category);
return item; return item;
}).ToArray(); }).ToArray();
var battleData = Substitute.For<IPokemonBattleData>(); user.ConsumedItems.Returns(items);
battleData.ConsumedItems.Returns(items);
user.BattleData.Returns(battleData);
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
@@ -108,26 +106,4 @@ public class BelchTests
// Assert // Assert
await Assert.That(prevent).IsFalse(); await Assert.That(prevent).IsFalse();
} }
/// <summary>
/// Technical test: outside of battle (no <see cref="IPokemon.BattleData"/>) the script does not prevent
/// selection.
/// </summary>
[Test]
public async Task PreventMoveSelection_NoBattleData_SelectionAllowed()
{
// Arrange
var script = new Belch();
var user = Substitute.For<IPokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
var prevent = false;
// Act
script.PreventMoveSelection(choice, ref prevent);
// Assert
await Assert.That(prevent).IsFalse();
}
} }

View File

@@ -14,11 +14,11 @@ public class BellyDrumTests
/// <summary> /// <summary>
/// Creates a fully mocked test setup for Belly Drum. The target of the secondary effect is the user itself. /// Creates a fully mocked test setup for Belly Drum. The target of the secondary effect is the user itself.
/// </summary> /// </summary>
private static (BellyDrum script, IExecutingMove move, IPokemon user, IHitData hitData) CreateTestSetup(uint maxHp, private static (BellyDrum script, IExecutingMove move, IBattlePokemon user, IHitData hitData) CreateTestSetup(
uint currentHp, sbyte attackBoost = 0) uint maxHp, uint currentHp, sbyte attackBoost = 0)
{ {
var script = new BellyDrum(); var script = new BellyDrum();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 10, 10, 10, 10, 10)); user.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 10, 10, 10, 10, 10));
user.CurrentHealth.Returns(currentHp); user.CurrentHealth.Returns(currentHp);
user.StatBoost.Returns(new StatBoostStatisticSet(0, attackBoost, 0, 0, 0, 0)); user.StatBoost.Returns(new StatBoostStatisticSet(0, attackBoost, 0, 0, 0, 0));
@@ -34,7 +34,7 @@ public class BellyDrumTests
/// <summary> /// <summary>
/// Helper to extract the received Damage call from the user, if any. /// Helper to extract the received Damage call from the user, if any.
/// </summary> /// </summary>
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"); var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
if (call == null) if (call == null)
@@ -46,7 +46,7 @@ public class BellyDrumTests
/// <summary> /// <summary>
/// Helper to extract the received ChangeStatBoost call from the user, if any. /// Helper to extract the received ChangeStatBoost call from the user, if any.
/// </summary> /// </summary>
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"); var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
if (call == null) if (call == null)

View File

@@ -12,17 +12,17 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class BestowTests 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) CreateTestSetup(IItem? userItem, IItem? targetItem)
{ {
var script = new Bestow(); var script = new Bestow();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
target.HeldItem.Returns(targetItem); target.HeldItem.Returns(targetItem);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.HeldItem.Returns(userItem); user.HeldItem.Returns(userItem);
user.RemoveHeldItemForBattle().Returns(userItem); user.RemoveHeldItemForBattle().Returns(userItem);
move.User.Returns(user); move.User.Returns(user);
@@ -68,7 +68,7 @@ public class BestowTests
/// <summary> /// <summary>
/// Bulbapedia: "Items given away in Trainer battles return to the original Pokémon after the battle." /// Bulbapedia: "Items given away in Trainer battles return to the original Pokémon after the battle."
/// The item is taken from the user through <see cref="IPokemon.RemoveHeldItemForBattle"/>, which only /// The item is taken from the user through <see cref="IBattlePokemon.RemoveHeldItemForBattle"/>, which only
/// removes the item for the duration of the battle. /// removes the item for the duration of the battle.
/// </summary> /// </summary>
[Test] [Test]

View File

@@ -13,16 +13,16 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class BideTests public class BideTests
{ {
private static (Bide script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet userVolatile, IHitData private static (Bide script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IScriptSet
hitData) CreateTestSetup() userVolatile, IHitData hitData) CreateTestSetup()
{ {
var script = new Bide(); var script = new Bide();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>())); user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
// A real script set so the volatile Bide effect can actually be added, retrieved and removed. // A real script set so the volatile Bide effect can actually be added, retrieved and removed.
var userVolatile = new ScriptSet(user); var userVolatile = new ScriptSet(user);
@@ -32,12 +32,10 @@ public class BideTests
return (script, move, user, target, userVolatile, hitData); 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<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
var battleData = Substitute.For<IPokemonBattleData>(); attacker.IsOnBattlefield.Returns(onBattlefield);
battleData.IsOnBattlefield.Returns(onBattlefield);
attacker.BattleData.Returns(battleData);
return attacker; return attacker;
} }
@@ -45,8 +43,8 @@ public class BideTests
/// Adds a <see cref="BideEffect"/> to the user's volatile scripts, as if Bide has already been storing /// Adds a <see cref="BideEffect"/> to the user's volatile scripts, as if Bide has already been storing
/// energy for the given number of executed turns. /// energy for the given number of executed turns.
/// </summary> /// </summary>
private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IPokemon user, byte turns, uint damageTaken, private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IBattlePokemon user, byte turns,
params IPokemon[] hitBy) uint damageTaken, params IBattlePokemon[] hitBy)
{ {
var effect = new BideEffect(user) var effect = new BideEffect(user)
{ {
@@ -61,13 +59,13 @@ public class BideTests
/// <summary> /// <summary>
/// Helper to check whether a Pokémon received any Damage call. /// Helper to check whether a Pokémon received any Damage call.
/// </summary> /// </summary>
private static bool ReceivedDamage(IPokemon pokemon) => private static bool ReceivedDamage(IBattlePokemon pokemon) =>
pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage"); pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage");
/// <summary> /// <summary>
/// Helper to extract the damage amount from a Pokémon's received Damage calls. /// Helper to extract the damage amount from a Pokémon's received Damage calls.
/// </summary> /// </summary>
private static uint? GetDamageAmount(IPokemon pokemon) private static uint? GetDamageAmount(IBattlePokemon pokemon)
{ {
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (uint)call.GetArguments()[0]! : null; return call != null ? (uint)call.GetArguments()[0]! : null;
@@ -76,7 +74,7 @@ public class BideTests
/// <summary> /// <summary>
/// Helper to extract the damage source from a Pokémon's received Damage calls. /// Helper to extract the damage source from a Pokémon's received Damage calls.
/// </summary> /// </summary>
private static DamageSource? GetDamageSource(IPokemon pokemon) private static DamageSource? GetDamageSource(IBattlePokemon pokemon)
{ {
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (DamageSource)call.GetArguments()[1]! : null; return call != null ? (DamageSource)call.GetArguments()[1]! : null;
@@ -245,7 +243,7 @@ public class BideTests
public async Task BideEffect_OnDamage_AccumulatesDamageTaken() public async Task BideEffect_OnDamage_AccumulatesDamageTaken()
{ {
// Arrange // Arrange
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var effect = new BideEffect(user); var effect = new BideEffect(user);
// Act - the user drops from 100 to 60 HP, then from 60 to 50 HP // 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() public async Task BideEffect_OnIncomingHit_RecordsAttacker()
{ {
// Arrange // Arrange
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var effect = new BideEffect(user); var effect = new BideEffect(user);
var incomingMove = Substitute.For<IExecutingMove>(); var incomingMove = Substitute.For<IExecutingMove>();
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
incomingMove.User.Returns(attacker); incomingMove.User.Returns(attacker);
// Act // Act

View File

@@ -44,16 +44,16 @@ public class BindTests
} }
} }
private static (Bind script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile) private static (Bind script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IScriptSet
CreateTestSetup(params Script[] userScripts) targetVolatile) CreateTestSetup(params Script[] userScripts)
{ {
var script = new Bind(); var script = new Bind();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var targetVolatile = Substitute.For<IScriptSet>(); var targetVolatile = Substitute.For<IScriptSet>();
target.Volatile.Returns(targetVolatile); target.Volatile.Returns(targetVolatile);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
// RunScriptHook iterates the user's scripts; give the mock a real iterator so the ModifyBind trigger // 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). // pass runs (empty unless the test attaches scripts such as the Grip Claw / Binding Band stand-in).
var containers = userScripts.Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s)).ToArray(); var containers = userScripts.Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s)).ToArray();
@@ -73,7 +73,7 @@ public class BindTests
/// <summary> /// <summary>
/// Helper to extract the damage amount from the target's first received Damage call. /// Helper to extract the damage amount from the target's first received Damage call.
/// </summary> /// </summary>
private static uint? GetDamageAmount(IPokemon pokemon) private static uint? GetDamageAmount(IBattlePokemon pokemon)
{ {
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage"); var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (uint)call.GetArguments()[0]! : null; return call != null ? (uint)call.GetArguments()[0]! : null;
@@ -82,7 +82,7 @@ public class BindTests
/// <summary> /// <summary>
/// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target. /// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target.
/// </summary> /// </summary>
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<IBattle>(); var battle = Substitute.For<IBattle>();
for (var i = 0; i < maxTurns; i++) for (var i = 0; i < maxTurns; i++)
@@ -204,7 +204,7 @@ public class BindTests
public async Task BindEffect_WhileActive_PreventsSwitching() public async Task BindEffect_WhileActive_PreventsSwitching()
{ {
// Arrange // Arrange
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var effect = new BindEffect(target, 5, 1f / 8f); var effect = new BindEffect(target, 5, 1f / 8f);
var prevent = false; var prevent = false;
@@ -223,7 +223,7 @@ public class BindTests
public async Task BindEffect_WhileActive_PreventsRunningAway() public async Task BindEffect_WhileActive_PreventsRunningAway()
{ {
// Arrange // Arrange
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var effect = new BindEffect(target, 5, 1f / 8f); var effect = new BindEffect(target, 5, 1f / 8f);
var prevent = false; var prevent = false;
@@ -242,7 +242,7 @@ public class BindTests
public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching() public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching()
{ {
// Arrange // Arrange
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
target.MaxHealth.Returns(160u); target.MaxHealth.Returns(160u);
var effect = new BindEffect(target, 1, 1f / 8f); var effect = new BindEffect(target, 1, 1f / 8f);

View File

@@ -14,14 +14,15 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class BlockTests 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 script = new Block();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Use a real script set so the volatile script added by Block can be inspected afterwards. // Use a real script set so the volatile script added by Block can be inspected afterwards.
var targetVolatile = new ScriptSet(target); var targetVolatile = new ScriptSet(target);
target.Volatile.Returns(targetVolatile); target.Volatile.Returns(targetVolatile);
@@ -105,10 +106,8 @@ public class BlockTests
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Library.Returns(library); battle.Library.Returns(library);
var battleData = Substitute.For<IPokemonBattleData>(); move.User.Battle.Returns(battle);
battleData.Battle.Returns(battle); target.Battle.Returns(battle);
move.User.BattleData.Returns(battleData);
target.BattleData.Returns(battleData);
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);

View File

@@ -20,12 +20,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class BounceTests public class BounceTests
{ {
private static (Bounce script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice, private static (Bounce script, IExecutingMove move, IBattlePokemon user, ScriptSet userVolatile, IMoveChoice
IBattleRandom random) CreateTestSetup() moveChoice, IBattleRandom random) CreateTestSetup()
{ {
var script = new Bounce(); var script = new Bounce();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
// Use a real script set so the charge volatile added by Bounce can be inspected afterwards. // Use a real script set so the charge volatile added by Bounce can be inspected afterwards.
var userVolatile = new ScriptSet(user); var userVolatile = new ScriptSet(user);
user.Volatile.Returns(userVolatile); user.Volatile.Returns(userVolatile);
@@ -39,9 +39,7 @@ public class BounceTests
var random = Substitute.For<IBattleRandom>(); var random = Substitute.For<IBattleRandom>();
battle.Random.Returns(random); battle.Random.Returns(random);
battle.EventHook.Returns(new EventHook()); battle.EventHook.Returns(new EventHook());
var battleData = Substitute.For<IPokemonBattleData>(); user.Battle.Returns(battle);
battleData.Battle.Returns(battle);
user.BattleData.Returns(battleData);
return (script, move, user, userVolatile, moveChoice, random); return (script, move, user, userVolatile, moveChoice, random);
} }
@@ -148,7 +146,7 @@ public class BounceTests
{ {
// Arrange // Arrange
var (script, move, user, _, _, random) = CreateTestSetup(); var (script, move, user, _, _, random) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
random.EffectChance(30, move, target, 0).Returns(true); random.EffectChance(30, move, target, 0).Returns(true);
// Act // Act
@@ -168,7 +166,7 @@ public class BounceTests
{ {
// Arrange // Arrange
var (script, move, _, _, _, random) = CreateTestSetup(); var (script, move, _, _, _, random) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
random.EffectChance(30, move, target, 0).Returns(false); random.EffectChance(30, move, target, 0).Returns(false);
// Act // Act
@@ -187,7 +185,7 @@ public class BounceTests
{ {
// Arrange // Arrange
var (script, move, _, _, _, random) = CreateTestSetup(); var (script, move, _, _, _, random) = CreateTestSetup();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
@@ -197,24 +195,6 @@ public class BounceTests
await Assert.That(random.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "EffectChance")).IsTrue(); await Assert.That(random.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "EffectChance")).IsTrue();
} }
/// <summary>
/// Technical test: outside of battle (no battle data) the secondary effect does nothing and does not throw.
/// </summary>
[Test]
public async Task OnSecondaryEffect_NoBattleData_DoesNotParalyzeTarget()
{
// Arrange
var (script, move, user, _, _, _) = CreateTestSetup();
user.BattleData.Returns((IPokemonBattleData?)null);
var target = Substitute.For<IPokemon>();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
}
/// <summary> /// <summary>
/// Bulbapedia: "While in Sky-High status, the user is invulnerable to most moves". /// Bulbapedia: "While in Sky-High status, the user is invulnerable to most moves".
/// The <see cref="ChargeBounceEffect"/> added by the charge turn blocks incoming hits from moves that /// The <see cref="ChargeBounceEffect"/> added by the charge turn blocks incoming hits from moves that

View File

@@ -15,21 +15,19 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
public class BrickBreakTests public class BrickBreakTests
{ {
/// <summary> /// <summary>
/// Creates a mocked Pokémon whose <see cref="IPokemonBattleData.BattleSide"/> is the given side. /// Creates a mocked Pokémon whose <see cref="IBattlePokemon.BattleSide"/> is the given side.
/// </summary> /// </summary>
private static IPokemon CreatePokemonOnSide(IBattleSide side) private static IBattlePokemon CreatePokemonOnSide(IBattleSide side)
{ {
var pokemon = Substitute.For<IPokemon>(); var pokemon = Substitute.For<IBattlePokemon>();
var battleData = Substitute.For<IPokemonBattleData>(); pokemon.BattleSide.Returns(side);
battleData.BattleSide.Returns(side);
pokemon.BattleData.Returns(battleData);
return pokemon; return pokemon;
} }
/// <summary> /// <summary>
/// Creates a fully mocked test setup where the user targets a Pokémon on the opposing side. /// Creates a fully mocked test setup where the user targets a Pokémon on the opposing side.
/// </summary> /// </summary>
private static (BrickBreak script, IExecutingMove move, IPokemon user, IScriptSet userSideScripts, IScriptSet private static (BrickBreak script, IExecutingMove move, IBattlePokemon user, IScriptSet userSideScripts, IScriptSet
targetSideScripts) CreateTestSetup() targetSideScripts) CreateTestSetup()
{ {
var script = new BrickBreak(); var script = new BrickBreak();
@@ -45,7 +43,7 @@ public class BrickBreakTests
var user = CreatePokemonOnSide(userSide); var user = CreatePokemonOnSide(userSide);
move.User.Returns(user); move.User.Returns(user);
var target = CreatePokemonOnSide(targetSide); var target = CreatePokemonOnSide(targetSide);
move.Targets.Returns(new IPokemon?[] { target }); move.Targets.Returns(new IBattlePokemon?[] { target });
return (script, move, user, userSideScripts, targetSideScripts); return (script, move, user, userSideScripts, targetSideScripts);
} }
@@ -115,8 +113,8 @@ public class BrickBreakTests
{ {
// Arrange // Arrange
var (script, move, user, userSideScripts, _) = CreateTestSetup(); var (script, move, user, userSideScripts, _) = CreateTestSetup();
var ally = CreatePokemonOnSide(user.BattleData!.BattleSide); var ally = CreatePokemonOnSide(user.BattleSide);
move.Targets.Returns(new IPokemon?[] { ally }); move.Targets.Returns(new IBattlePokemon?[] { ally });
// Act // Act
script.OnBeforeMove(move); script.OnBeforeMove(move);
@@ -146,7 +144,7 @@ public class BrickBreakTests
var user = CreatePokemonOnSide(userSide); var user = CreatePokemonOnSide(userSide);
move.User.Returns(user); move.User.Returns(user);
var target = CreatePokemonOnSide(targetSide); var target = CreatePokemonOnSide(targetSide);
move.Targets.Returns(new IPokemon?[] { target }); move.Targets.Returns(new IBattlePokemon?[] { target });
// Act // Act
script.OnBeforeMove(move); script.OnBeforeMove(move);
@@ -165,10 +163,9 @@ public class BrickBreakTests
// Arrange // Arrange
var script = new BrickBreak(); var script = new BrickBreak();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
move.User.Returns(user); move.User.Returns(user);
move.Targets.Returns(Array.Empty<IPokemon?>()); move.Targets.Returns(Array.Empty<IBattlePokemon?>());
// Act & Assert // Act & Assert
await Assert.That(() => script.OnBeforeMove(move)).ThrowsNothing(); await Assert.That(() => script.OnBeforeMove(move)).ThrowsNothing();

View File

@@ -12,13 +12,13 @@ public class BrineTests
{ {
/// <summary> /// <summary>
/// Creates a fully mocked test setup for Brine tests, with a target whose max HP /// Creates a fully mocked test setup for Brine tests, with a target whose max HP
/// (<see cref="IPokemon.BoostedStats"/>) and <see cref="IPokemon.CurrentHealth"/> are configured. /// (<see cref="IBattlePokemon.BoostedStats"/>) and <see cref="IBattlePokemon.CurrentHealth"/> are configured.
/// </summary> /// </summary>
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 brine = new Brine();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
target.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 0, 0, 0, 0, 0)); target.BoostedStats.Returns(new StatisticSet<uint>(maxHp, 0, 0, 0, 0, 0));
target.CurrentHealth.Returns(currentHp); target.CurrentHealth.Returns(currentHp);
return (brine, move, target); return (brine, move, target);

View File

@@ -41,8 +41,8 @@ public class BugBiteTests
/// real resolver with a single registered item script constructor for <see cref="BerryEffectName"/>, so /// real resolver with a single registered item script constructor for <see cref="BerryEffectName"/>, so
/// eating a Berry runs a <see cref="RecordingItemScript"/> that the test can inspect. /// eating a Berry runs a <see cref="RecordingItemScript"/> that the test can inspect.
/// </summary> /// </summary>
private static (BugBite bugBite, IExecutingMove move, IPokemon target, IHitData hitData, List<RecordingItemScript> private static (BugBite bugBite, IExecutingMove move, IBattlePokemon target, IHitData hitData,
createdItemScripts) CreateTestSetup(IItem? targetHeldItem, bool canSteal = true) List<RecordingItemScript> createdItemScripts) CreateTestSetup(IItem? targetHeldItem, bool canSteal = true)
{ {
var bugBite = new BugBite(); var bugBite = new BugBite();
@@ -66,17 +66,14 @@ public class BugBiteTests
dynamicLibrary.ScriptResolver.Returns(resolver); dynamicLibrary.ScriptResolver.Returns(resolver);
battle.Library.Returns(dynamicLibrary); battle.Library.Returns(dynamicLibrary);
var battleData = Substitute.For<IPokemonBattleData>(); var user = Substitute.For<IBattlePokemon>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>();
user.BattleData.Returns(battleData);
user.Battle.Returns(battle);
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.User.Returns(user); move.User.Returns(user);
move.Battle.Returns(battle); move.Battle.Returns(battle);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
target.HeldItem.Returns(targetHeldItem); target.HeldItem.Returns(targetHeldItem);
if (targetHeldItem != null && canSteal) if (targetHeldItem != null && canSteal)
{ {
@@ -111,7 +108,7 @@ public class BugBiteTests
/// <summary> /// <summary>
/// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect." /// 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 <see cref="IPokemon.ForceSetHeldItem"/>. /// Eating the Berry removes it from the target via <see cref="IBattlePokemon.ForceSetHeldItem"/>.
/// </summary> /// </summary>
[Test] [Test]
public async Task OnSecondaryEffect_TargetHoldsBerry_BerryIsRemovedFromTarget() public async Task OnSecondaryEffect_TargetHoldsBerry_BerryIsRemovedFromTarget()
@@ -202,7 +199,7 @@ public class BugBiteTests
/// <summary> /// <summary>
/// Bulbapedia: "Bug Bite will not consume the Berry of a target that has the Ability Sticky Hold." /// Bulbapedia: "Bug Bite will not consume the Berry of a target that has the Ability Sticky Hold."
/// When the Berry cannot be stolen (<see cref="IPokemon.TryStealHeldItem"/> returns false, as with /// When the Berry cannot be stolen (<see cref="IBattlePokemon.TryStealHeldItem"/> returns false, as with
/// Sticky Hold), the target keeps its Berry and the effect fails. /// Sticky Hold), the target keeps its Berry and the effect fails.
/// </summary> /// </summary>
[Test] [Test]
@@ -219,25 +216,4 @@ public class BugBiteTests
await Assert.That(createdItemScripts.Count).IsEqualTo(0); await Assert.That(createdItemScripts.Count).IsEqualTo(0);
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue(); await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
} }
/// <summary>
/// If the user has no <see cref="IPokemon.BattleData"/>, the script does nothing: no Berry is eaten
/// and the hit is not failed.
/// </summary>
[Test]
public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
{
// Arrange
var (bugBite, move, target, hitData, _) = CreateTestSetup(CreateBerry());
var user = Substitute.For<IPokemon>();
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();
}
} }

View File

@@ -18,8 +18,8 @@ public class BurnUpTests
/// <see cref="TypeLibrary"/> with "fire" and "water" registered. The user is a Water-type, optionally /// <see cref="TypeLibrary"/> with "fire" and "water" registered. The user is a Water-type, optionally
/// also carrying the Fire type. /// also carrying the Fire type.
/// </summary> /// </summary>
private static (BurnUp burnUp, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData, TypeIdentifier private static (BurnUp burnUp, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData hitData,
fireType) CreateTestSetup(bool userIsFireType, bool userIsFrozen = false) TypeIdentifier fireType) CreateTestSetup(bool userIsFireType, bool userIsFrozen = false)
{ {
var burnUp = new BurnUp(); var burnUp = new BurnUp();
@@ -33,11 +33,9 @@ public class BurnUpTests
dynamicLibrary.StaticLibrary.Returns(staticLibrary); dynamicLibrary.StaticLibrary.Returns(staticLibrary);
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Library.Returns(dynamicLibrary); battle.Library.Returns(dynamicLibrary);
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BattleData.Returns(battleData); user.Battle.Returns(battle);
user.Types.Returns(userIsFireType user.Types.Returns(userIsFireType
? new List<TypeIdentifier> { fireType, waterType } ? new List<TypeIdentifier> { fireType, waterType }
: new List<TypeIdentifier> { waterType }); : new List<TypeIdentifier> { waterType });
@@ -46,7 +44,7 @@ public class BurnUpTests
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -54,9 +52,9 @@ public class BurnUpTests
} }
/// <summary> /// <summary>
/// Helper that checks whether <see cref="IPokemon.RemoveType"/> was called with the given type. /// Helper that checks whether <see cref="IBattlePokemon.RemoveType"/> was called with the given type.
/// </summary> /// </summary>
private static bool ReceivedRemoveType(IPokemon user, TypeIdentifier type) => private static bool ReceivedRemoveType(IBattlePokemon user, TypeIdentifier type) =>
user.ReceivedCalls().Any(c => user.ReceivedCalls().Any(c =>
c.GetMethodInfo().Name == "RemoveType" && type.Equals((TypeIdentifier)c.GetArguments()[0]!)); c.GetMethodInfo().Name == "RemoveType" && type.Equals((TypeIdentifier)c.GetArguments()[0]!));
@@ -177,23 +175,4 @@ public class BurnUpTests
// Assert // Assert
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse(); await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse();
} }
/// <summary>
/// If the user has no <see cref="IPokemon.BattleData"/>, the script does nothing: no type is removed
/// and the hit is not failed.
/// </summary>
[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();
}
} }

View File

@@ -13,7 +13,7 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class CamouflageTests 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) string? terrainName, string environmentName)
{ {
var script = new Camouflage(); var script = new Camouflage();
@@ -24,7 +24,7 @@ public class CamouflageTests
battle.TerrainName.Returns(terrainName == null ? null : new StringKey?(new StringKey(terrainName))); battle.TerrainName.Returns(terrainName == null ? null : new StringKey?(new StringKey(terrainName)));
battle.EnvironmentName.Returns(new StringKey(environmentName)); battle.EnvironmentName.Returns(new StringKey(environmentName));
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.User.Returns(user); move.User.Returns(user);
move.Battle.Returns(battle); move.Battle.Returns(battle);
@@ -34,9 +34,9 @@ public class CamouflageTests
/// <summary> /// <summary>
/// Helper that returns the single type the user was changed to, or null when /// Helper that returns the single type the user was changed to, or null when
/// <see cref="IPokemon.SetTypes"/> was never called. /// <see cref="IBattlePokemon.SetTypes"/> was never called.
/// </summary> /// </summary>
private static TypeIdentifier? GetSetType(IPokemon user) private static TypeIdentifier? GetSetType(IBattlePokemon user)
{ {
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes"); var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes");
return call != null ? ((IReadOnlyList<TypeIdentifier>)call.GetArguments()[0]!).Single() : null; return call != null ? ((IReadOnlyList<TypeIdentifier>)call.GetArguments()[0]!).Single() : null;
@@ -56,7 +56,7 @@ public class CamouflageTests
battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected); battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected); await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
@@ -75,7 +75,7 @@ public class CamouflageTests
battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected); battle.Library.StaticLibrary.Types.TryGetTypeIdentifier(expectedType, out var expected);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected); await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
@@ -92,7 +92,7 @@ public class CamouflageTests
battle.Library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var expected); battle.Library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var expected);
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected); await Assert.That(GetSetType(user)!.Value).IsEqualTo(expected);
@@ -107,7 +107,7 @@ public class CamouflageTests
{ {
// Arrange // Arrange
var (script, move, _, _) = CreateTestSetup(null, "field"); var (script, move, _, _) = CreateTestSetup(null, "field");
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);

View File

@@ -14,16 +14,16 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class CaptivateTests 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) CreateTestSetup(Gender userGender, Gender targetGender)
{ {
var script = new Captivate(); var script = new Captivate();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.Gender.Returns(userGender); user.Gender.Returns(userGender);
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
target.Gender.Returns(targetGender); target.Gender.Returns(targetGender);
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
@@ -34,7 +34,7 @@ public class CaptivateTests
/// <summary> /// <summary>
/// Helper that checks whether a stat boost change was applied to the given Pokémon. /// Helper that checks whether a stat boost change was applied to the given Pokémon.
/// </summary> /// </summary>
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" && pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" &&
(Statistic)c.GetArguments()[0]! == stat && (Statistic)c.GetArguments()[0]! == stat &&
(sbyte)c.GetArguments()[1]! == amount); (sbyte)c.GetArguments()[1]! == amount);

View File

@@ -25,7 +25,7 @@ public class ChangeTargetSpecialDefenseTests
/// <summary> /// <summary>
/// Helper to extract the arguments of the ChangeStatBoost call received by a substitute target. /// Helper to extract the arguments of the ChangeStatBoost call received by a substitute target.
/// </summary> /// </summary>
private static object?[]? GetStatBoostCallArgs(IPokemon target) private static object?[]? GetStatBoostCallArgs(IBattlePokemon target)
{ {
var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
return call?.GetArguments(); return call?.GetArguments();
@@ -41,9 +41,9 @@ public class ChangeTargetSpecialDefenseTests
// Arrange // Arrange
var script = CreateInitializedScript(); var script = CreateInitializedScript();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
@@ -66,9 +66,9 @@ public class ChangeTargetSpecialDefenseTests
// Arrange // Arrange
var script = CreateInitializedScript(); var script = CreateInitializedScript();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
@@ -90,9 +90,9 @@ public class ChangeTargetSpecialDefenseTests
// Arrange // Arrange
var script = CreateInitializedScript(); var script = CreateInitializedScript();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
@@ -114,9 +114,9 @@ public class ChangeTargetSpecialDefenseTests
// Arrange // Arrange
var script = CreateInitializedScript(); var script = CreateInitializedScript();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);
@@ -137,7 +137,7 @@ public class ChangeTargetSpecialDefenseTests
// Arrange // Arrange
var script = CreateInitializedScript(); var script = CreateInitializedScript();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
// Act - the user is hit by its own move (e.g. redirected) // Act - the user is hit by its own move (e.g. redirected)
@@ -159,9 +159,9 @@ public class ChangeTargetSpecialDefenseTests
// Arrange // Arrange
var script = CreateInitializedScript(amount); var script = CreateInitializedScript(amount);
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
// Act // Act
script.OnSecondaryEffect(move, target, 0); script.OnSecondaryEffect(move, target, 0);

View File

@@ -15,11 +15,11 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class ChargeTests 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 script = new Charge();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
// Use a real script set so the volatile script added by Charge can be inspected afterwards. // Use a real script set so the volatile script added by Charge can be inspected afterwards.
var userVolatile = new ScriptSet(user); var userVolatile = new ScriptSet(user);
user.Volatile.Returns(userVolatile); 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 /// Creates an executing move of the given type whose damage modifier can be changed by
/// <see cref="ChargeEffect.ChangeDamageModifier"/>. /// <see cref="ChargeEffect.ChangeDamageModifier"/>.
/// </summary> /// </summary>
private static (IExecutingMove move, IPokemon target) CreateExecutingMoveOfType(string typeName) private static (IExecutingMove move, IBattlePokemon target) CreateExecutingMoveOfType(string typeName)
{ {
var library = LibraryHelpers.LoadLibrary(); var library = LibraryHelpers.LoadLibrary();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Library.Returns(library); battle.Library.Returns(library);
var battleData = Substitute.For<IPokemonBattleData>(); var target = Substitute.For<IBattlePokemon>();
battleData.Battle.Returns(battle);
var target = Substitute.For<IPokemon>();
target.BattleData.Returns(battleData);
target.Battle.Returns(battle);
library.StaticLibrary.Types.TryGetTypeIdentifier(typeName, out var typeIdentifier); library.StaticLibrary.Types.TryGetTypeIdentifier(typeName, out var typeIdentifier);
var useMove = Substitute.For<IMoveData>(); var useMove = Substitute.For<IMoveData>();
useMove.MoveType.Returns(typeIdentifier); useMove.MoveType.Returns(typeIdentifier);
@@ -61,7 +59,7 @@ public class ChargeTests
var (script, move, user, _) = CreateTestSetup(); var (script, move, user, _) = CreateTestSetup();
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
var boost = user.ReceivedCalls().SingleOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); var boost = user.ReceivedCalls().SingleOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
@@ -82,7 +80,7 @@ public class ChargeTests
var (script, move, _, userVolatile) = CreateTestSetup(); var (script, move, _, userVolatile) = CreateTestSetup();
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeEffect>())).IsTrue(); await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<ChargeEffect>())).IsTrue();
@@ -134,7 +132,7 @@ public class ChargeTests
{ {
// Arrange // Arrange
var (script, move, user, userVolatile) = CreateTestSetup(); var (script, move, user, userVolatile) = CreateTestSetup();
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
await Assert.That(userVolatile.TryGet<ChargeEffect>(out var effect)).IsTrue(); await Assert.That(userVolatile.TryGet<ChargeEffect>(out var effect)).IsTrue();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();

View File

@@ -22,7 +22,8 @@ public class ChipAwayTests
var bypass = false; var bypass = false;
// Act // Act
script.BypassDefensiveStatBoosts(Substitute.For<IExecutingMove>(), Substitute.For<IPokemon>(), 0, ref bypass); script.BypassDefensiveStatBoosts(Substitute.For<IExecutingMove>(), Substitute.For<IBattlePokemon>(), 0,
ref bypass);
// Assert // Assert
await Assert.That(bypass).IsTrue(); await Assert.That(bypass).IsTrue();
@@ -40,7 +41,8 @@ public class ChipAwayTests
var bypass = false; var bypass = false;
// Act // Act
script.BypassEvasionStatBoosts(Substitute.For<IExecutingMove>(), Substitute.For<IPokemon>(), 0, ref bypass); script.BypassEvasionStatBoosts(Substitute.For<IExecutingMove>(), Substitute.For<IBattlePokemon>(), 0,
ref bypass);
// Assert // Assert
await Assert.That(bypass).IsTrue(); await Assert.That(bypass).IsTrue();

View File

@@ -19,8 +19,8 @@ public class Conversion2Tests
{ {
private static readonly IDynamicLibrary Library = LibraryHelpers.LoadLibrary(); private static readonly IDynamicLibrary Library = LibraryHelpers.LoadLibrary();
private static (Conversion2 script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData) private static (Conversion2 script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData
CreateTestSetup(TypeIdentifier? lastMoveType) hitData) CreateTestSetup(TypeIdentifier? lastMoveType)
{ {
var script = new Conversion2(); var script = new Conversion2();
@@ -30,15 +30,13 @@ public class Conversion2Tests
battle.Library.Returns(Library); battle.Library.Returns(Library);
battle.Random.Returns(random); battle.Random.Returns(random);
var userBattleData = Substitute.For<IPokemonBattleData>(); var user = Substitute.For<IBattlePokemon>();
userBattleData.Battle.Returns(battle); user.Battle.Returns(battle);
var user = Substitute.For<IPokemon>();
user.BattleData.Returns(userBattleData);
var targetBattleData = Substitute.For<IPokemonBattleData>(); var target = Substitute.For<IBattlePokemon>();
if (lastMoveType == null) if (lastMoveType == null)
{ {
targetBattleData.LastMoveChoice.Returns((IMoveChoice?)null); target.LastMoveChoice.Returns((IMoveChoice?)null);
} }
else else
{ {
@@ -48,12 +46,9 @@ public class Conversion2Tests
learnedMove.MoveData.Returns(moveData); learnedMove.MoveData.Returns(moveData);
var lastChoice = Substitute.For<IMoveChoice>(); var lastChoice = Substitute.For<IMoveChoice>();
lastChoice.ChosenMove.Returns(learnedMove); lastChoice.ChosenMove.Returns(learnedMove);
targetBattleData.LastMoveChoice.Returns(lastChoice); target.LastMoveChoice.Returns(lastChoice);
} }
var target = Substitute.For<IPokemon>();
target.BattleData.Returns(targetBattleData);
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.User.Returns(user); move.User.Returns(user);
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
@@ -70,9 +65,9 @@ public class Conversion2Tests
/// <summary> /// <summary>
/// Helper that returns the single type the user was changed to, or null when /// Helper that returns the single type the user was changed to, or null when
/// <see cref="IPokemon.SetTypes"/> was never called. /// <see cref="IBattlePokemon.SetTypes"/> was never called.
/// </summary> /// </summary>
private static TypeIdentifier? GetSetType(IPokemon user) private static TypeIdentifier? GetSetType(IBattlePokemon user)
{ {
var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes"); var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetTypes");
return call != null ? ((IReadOnlyList<TypeIdentifier>)call.GetArguments()[0]!).Single() : null; return call != null ? ((IReadOnlyList<TypeIdentifier>)call.GetArguments()[0]!).Single() : null;

View File

@@ -22,15 +22,15 @@ public class ConversionTests
return learned; return learned;
} }
private static (Conversion script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData) private static (Conversion script, IExecutingMove move, IBattlePokemon user, IBattlePokemon target, IHitData hitData
CreateTestSetup(params ILearnedMove?[] moves) ) CreateTestSetup(params ILearnedMove?[] moves)
{ {
var script = new Conversion(); var script = new Conversion();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.Moves.Returns(moves); user.Moves.Returns(moves);
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
move.GetHitData(target, 0).Returns(hitData); move.GetHitData(target, 0).Returns(hitData);
return (script, move, user, target, hitData); return (script, move, user, target, hitData);

View File

@@ -17,11 +17,10 @@ public class CopycatTests
private static (Copycat script, IMoveChoice choice) CreateTestSetup(string? lastMoveName) private static (Copycat script, IMoveChoice choice) CreateTestSetup(string? lastMoveName)
{ {
var script = new Copycat(); var script = new Copycat();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var battleData = Substitute.For<IPokemonBattleData>();
if (lastMoveName == null) if (lastMoveName == null)
{ {
battleData.LastMoveChoice.Returns((IMoveChoice?)null); user.LastMoveChoice.Returns((IMoveChoice?)null);
} }
else else
{ {
@@ -31,9 +30,8 @@ public class CopycatTests
learnedMove.MoveData.Returns(moveData); learnedMove.MoveData.Returns(moveData);
var lastChoice = Substitute.For<IMoveChoice>(); var lastChoice = Substitute.For<IMoveChoice>();
lastChoice.ChosenMove.Returns(learnedMove); lastChoice.ChosenMove.Returns(learnedMove);
battleData.LastMoveChoice.Returns(lastChoice); user.LastMoveChoice.Returns(lastChoice);
} }
user.BattleData.Returns(battleData);
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
@@ -94,25 +92,4 @@ public class CopycatTests
choice.Received(1).Fail(); choice.Received(1).Fail();
await Assert.That(moveName).IsEqualTo(new StringKey("copycat")); await Assert.That(moveName).IsEqualTo(new StringKey("copycat"));
} }
/// <summary>
/// Technical test: outside of battle (no battle data) there is no last move, so Copycat fails.
/// </summary>
[Test]
public void ChangeMove_NoBattleData_Fails()
{
// Arrange
var script = new Copycat();
var user = Substitute.For<IPokemon>();
user.BattleData.Returns((IPokemonBattleData?)null);
var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user);
StringKey moveName = "copycat";
// Act
script.ChangeMove(choice, ref moveName);
// Assert
choice.Received(1).Fail();
}
} }

View File

@@ -14,16 +14,14 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class CoreEnforcerTests 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() hitData, IBattle battle) CreateTestSetup()
{ {
var script = new CoreEnforcer(); var script = new CoreEnforcer();
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
var battleData = Substitute.For<IPokemonBattleData>(); var target = Substitute.For<IBattlePokemon>();
battleData.Battle.Returns(battle);
var target = Substitute.For<IPokemon>();
target.BattleData.Returns(battleData);
target.Battle.Returns(battle);
var currentChoice = Substitute.For<IMoveChoice>(); var currentChoice = Substitute.For<IMoveChoice>();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.MoveChoice.Returns(currentChoice); move.MoveChoice.Returns(currentChoice);
@@ -120,24 +118,6 @@ public class CoreEnforcerTests
hitData.Received(1).Fail(); hitData.Received(1).Fail();
} }
/// <summary>
/// Technical test: a target without battle data (not in battle) is left untouched.
/// </summary>
[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();
}
/// <summary> /// <summary>
/// Bulbapedia: the condition is that "the target has already used a move" — an action by a different /// 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. /// 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 // Arrange
var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup(); var (script, move, currentChoice, target, hitData, battle) = CreateTestSetup();
var allyChoice = Substitute.For<IMoveChoice>(); var allyChoice = Substitute.For<IMoveChoice>();
allyChoice.User.Returns(Substitute.For<IPokemon>()); allyChoice.User.Returns(Substitute.For<IBattlePokemon>());
SetTurnChoices(battle, allyChoice, currentChoice); SetTurnChoices(battle, allyChoice, currentChoice);
// Act // Act
@@ -162,7 +142,7 @@ public class CoreEnforcerTests
/// <summary> /// <summary>
/// Bulbapedia: "The move cannot suppress certain signature abilities including Multitype, Stance /// Bulbapedia: "The move cannot suppress certain signature abilities including Multitype, Stance
/// Change, Schooling, Comatose, Shields Down, Disguise, RKS System, Battle Bond, Power Construct". /// Change, Schooling, Comatose, Shields Down, Disguise, RKS System, Battle Bond, Power Construct".
/// The script requests the suppression unconditionally; <see cref="IPokemon.SuppressAbility"/> refuses /// The script requests the suppression unconditionally; <see cref="IBattlePokemon.SuppressAbility"/> refuses
/// it when <see cref="IAbility.CanBeChanged"/> is false, so these abilities must carry that flag in the /// it when <see cref="IAbility.CanBeChanged"/> is false, so these abilities must carry that flag in the
/// Gen7 data. /// Gen7 data.
/// </summary> /// </summary>

View File

@@ -21,9 +21,9 @@ public class CounterTests
/// Creates a user whose volatile scripts contain a <see cref="CounterHelperEffect"/> that has recorded /// Creates a user whose volatile scripts contain a <see cref="CounterHelperEffect"/> that has recorded
/// an incoming physical hit of the given damage by <paramref name="attacker"/>. /// an incoming physical hit of the given damage by <paramref name="attacker"/>.
/// </summary> /// </summary>
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<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var userVolatile = new ScriptSet(user); var userVolatile = new ScriptSet(user);
user.Volatile.Returns(userVolatile); user.Volatile.Returns(userVolatile);
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>())); user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
@@ -55,7 +55,7 @@ public class CounterTests
{ {
// Arrange // Arrange
var script = new Counter(); var script = new Counter();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
var userVolatile = new ScriptSet(user); var userVolatile = new ScriptSet(user);
user.Volatile.Returns(userVolatile); user.Volatile.Returns(userVolatile);
user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>())); user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
@@ -78,11 +78,11 @@ public class CounterTests
{ {
// Arrange // Arrange
var script = new Counter(); var script = new Counter();
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
var user = CreateUserHitBy(attacker, 40); var user = CreateUserHitBy(attacker, 40);
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
IReadOnlyList<IPokemon?> targets = new IPokemon?[] { Substitute.For<IPokemon>() }; IReadOnlyList<IBattlePokemon?> targets = new IBattlePokemon?[] { Substitute.For<IBattlePokemon>() };
// Act // Act
script.ChangeTargets(choice, ref targets); script.ChangeTargets(choice, ref targets);
@@ -103,7 +103,7 @@ public class CounterTests
var user = CreateUserHitBy(null, 0); var user = CreateUserHitBy(null, 0);
var choice = Substitute.For<IMoveChoice>(); var choice = Substitute.For<IMoveChoice>();
choice.User.Returns(user); choice.User.Returns(user);
IReadOnlyList<IPokemon?> targets = new IPokemon?[] { Substitute.For<IPokemon>() }; IReadOnlyList<IBattlePokemon?> targets = new IBattlePokemon?[] { Substitute.For<IBattlePokemon>() };
// Act // Act
script.ChangeTargets(choice, ref targets); script.ChangeTargets(choice, ref targets);
@@ -122,7 +122,7 @@ public class CounterTests
{ {
// Arrange // Arrange
var script = new Counter(); var script = new Counter();
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
var user = CreateUserHitBy(attacker, damageTaken); var user = CreateUserHitBy(attacker, damageTaken);
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.User.Returns(user); move.User.Returns(user);
@@ -144,9 +144,9 @@ public class CounterTests
{ {
// Arrange // Arrange
var script = new Counter(); var script = new Counter();
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
var user = CreateUserHitBy(attacker, 40); var user = CreateUserHitBy(attacker, 40);
var someoneElse = Substitute.For<IPokemon>(); var someoneElse = Substitute.For<IBattlePokemon>();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
move.User.Returns(user); move.User.Returns(user);
var hitData = Substitute.For<IHitData>(); var hitData = Substitute.For<IHitData>();
@@ -167,7 +167,7 @@ public class CounterTests
public async Task CounterHelperEffect_PhysicalHit_RecordsAttackerAndDamage() public async Task CounterHelperEffect_PhysicalHit_RecordsAttackerAndDamage()
{ {
// Arrange // Arrange
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
var user = CreateUserHitBy(attacker, 40); var user = CreateUserHitBy(attacker, 40);
// Assert // Assert
@@ -184,7 +184,7 @@ public class CounterTests
public async Task CounterHelperEffect_SpecialHit_IsNotRecorded() public async Task CounterHelperEffect_SpecialHit_IsNotRecorded()
{ {
// Arrange // Arrange
var attacker = Substitute.For<IPokemon>(); var attacker = Substitute.For<IBattlePokemon>();
var user = CreateUserHitBy(attacker, 40, false); var user = CreateUserHitBy(attacker, 40, false);
// Assert // Assert

View File

@@ -12,17 +12,17 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class CovetTests 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) IItem? targetItem)
{ {
var script = new Covet(); var script = new Covet();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
// Explicitly return null to suppress NSubstitute's auto-substitution; Covet must see an empty-handed user. // Explicitly return null to suppress NSubstitute's auto-substitution; Covet must see an empty-handed user.
user.HeldItem.Returns((IItem?)null); user.HeldItem.Returns((IItem?)null);
move.User.Returns(user); move.User.Returns(user);
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
target.HeldItem.Returns(targetItem); target.HeldItem.Returns(targetItem);
if (targetItem != null) if (targetItem != null)
{ {

View File

@@ -26,13 +26,11 @@ public class CraftyShieldTests
var battle = Substitute.For<IBattle>(); var battle = Substitute.For<IBattle>();
battle.Sides.Returns(new[] { side }); battle.Sides.Returns(new[] { side });
var battleData = Substitute.For<IPokemonBattleData>();
battleData.Battle.Returns(battle);
battleData.SideIndex.Returns((byte)0);
var user = Substitute.For<IPokemon>(); var user = Substitute.For<IBattlePokemon>();
user.BattleData.Returns(battleData);
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
user.SideIndex.Returns((byte)0);
user.Battle.Returns(battle);
move.User.Returns(user); move.User.Returns(user);
return (script, move, sideVolatile); return (script, move, sideVolatile);
@@ -58,7 +56,7 @@ public class CraftyShieldTests
var (script, move, sideVolatile) = CreateTestSetup(); var (script, move, sideVolatile) = CreateTestSetup();
// Act // Act
script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0); script.OnSecondaryEffect(move, Substitute.For<IBattlePokemon>(), 0);
// Assert // Assert
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<CraftyShieldEffect>())).IsTrue(); await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<CraftyShieldEffect>())).IsTrue();
@@ -99,21 +97,4 @@ public class CraftyShieldTests
// Assert // Assert
await Assert.That(stop).IsFalse(); await Assert.That(stop).IsFalse();
} }
/// <summary>
/// Technical test: outside of battle (no battle data) no shield can be raised and nothing happens.
/// </summary>
[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<IPokemon>(), 0);
// Assert - no effect is added
await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName<CraftyShieldEffect>())).IsFalse();
}
} }

View File

@@ -11,12 +11,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
/// </summary> /// </summary>
public class CrushGripTests 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) uint maxHealth)
{ {
var script = new CrushGrip(); var script = new CrushGrip();
var move = Substitute.For<IExecutingMove>(); var move = Substitute.For<IExecutingMove>();
var target = Substitute.For<IPokemon>(); var target = Substitute.For<IBattlePokemon>();
target.CurrentHealth.Returns(currentHealth); target.CurrentHealth.Returns(currentHealth);
target.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1)); target.BoostedStats.Returns(new StatisticSet<uint>(maxHealth, 1, 1, 1, 1, 1));
return (script, move, target); return (script, move, target);

Some files were not shown because too many files have changed in this diff Show More