diff --git a/AI/AIRunner/TestCommandRunner.cs b/AI/AIRunner/TestCommandRunner.cs
index c288ffd..2562de4 100644
--- a/AI/AIRunner/TestCommandRunner.cs
+++ b/AI/AIRunner/TestCommandRunner.cs
@@ -163,19 +163,19 @@ public static class TestCommandRunner
var pokemon1 = battle.Sides[0].Pokemon[0];
while (pokemon1 is null && !battle.HasEnded)
{
- pokemon1 = battle.Parties[0].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable);
- if (pokemon1 is null)
+ var replacement1 = battle.Parties[0].BattlePokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable);
+ if (replacement1 is null)
throw new InvalidOperationException("No usable Pokémon found in party 1.");
- battle.Sides[0].SwapPokemon(0, pokemon1);
+ battle.Sides[0].SwapPokemon(0, replacement1);
pokemon1 = battle.Sides[0].Pokemon[0];
}
var pokemon2 = battle.Sides[1].Pokemon[0];
while (pokemon2 is null && !battle.HasEnded)
{
- pokemon2 = battle.Parties[1].Party.WhereNotNull().FirstOrDefault(x => x.IsUsable);
- if (pokemon2 is null)
+ var replacement2 = battle.Parties[1].BattlePokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable);
+ if (replacement2 is null)
throw new InvalidOperationException("No usable Pokémon found in party 2.");
- battle.Sides[1].SwapPokemon(0, pokemon2);
+ battle.Sides[1].SwapPokemon(0, replacement2);
pokemon2 = battle.Sides[1].Pokemon[0];
}
if (pokemon1 is null || pokemon2 is null)
diff --git a/PkmnLib.Dynamic/AI/AIHelpers.cs b/PkmnLib.Dynamic/AI/AIHelpers.cs
index 147b2dd..888f9da 100644
--- a/PkmnLib.Dynamic/AI/AIHelpers.cs
+++ b/PkmnLib.Dynamic/AI/AIHelpers.cs
@@ -14,7 +14,7 @@ public static class AIHelpers
///
/// Estimates the amount of damage that will be done by a move against a target.
///
- public static uint CalculateDamageEstimation(IMoveData move, IPokemon user, IPokemon target,
+ public static uint CalculateDamageEstimation(IMoveData move, IBattlePokemon user, IBattlePokemon target,
IDynamicLibrary library)
{
var hitData = new CustomHitData
diff --git a/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs b/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs
index 8d19a0f..6967093 100644
--- a/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs
+++ b/PkmnLib.Dynamic/AI/Explicit/AIMoveState.cs
@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
public class AIMoveState
{
///
- public AIMoveState(IPokemon user, IMoveData move)
+ public AIMoveState(IBattlePokemon user, IMoveData move)
{
User = user;
Move = move;
@@ -18,7 +18,7 @@ public class AIMoveState
///
/// The user that's being wrapper
///
- public IPokemon User { get; }
+ public IBattlePokemon User { get; }
///
/// The move that's being wrapper
diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
index 1a0c59d..a5851db 100644
--- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
+++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Switch.cs
@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
public partial class ExplicitAI
{
- private bool TryChooseToSwitchOut(IBattle battle, IPokemon pokemon, bool terribleMoves,
+ private bool TryChooseToSwitchOut(IBattle battle, IBattlePokemon pokemon, bool terribleMoves,
[NotNullWhen(true)] out ITurnChoice? choice)
{
choice = null;
@@ -17,13 +17,13 @@ public partial class ExplicitAI
return false;
if (TrainerHighSkill)
{
- var opponentSide = battle.Sides.First(x => x != pokemon.BattleData?.BattleSide);
+ var opponentSide = battle.Sides.First(x => x != pokemon.BattleSide);
var foeCanAct = opponentSide.Pokemon.WhereNotNull().Any(CanAttack);
if (!foeCanAct)
return false;
}
var party = battle.Parties.FirstOrDefault(x => x.IsResponsibleForIndex(
- new ResponsibleIndex(pokemon.BattleData!.SideIndex, pokemon.BattleData.Position)));
+ new ResponsibleIndex(pokemon.SideIndex, pokemon.Position)));
if (party is null)
return false;
var usablePokemon = party.GetUsablePokemonNotInField().ToList();
@@ -44,20 +44,20 @@ public partial class ExplicitAI
if (!shouldSwitch)
return false;
}
- var battleSide = pokemon.BattleData!.BattleSide;
+ var battleSide = pokemon.BattleSide;
var bestReplacement = ChooseBestReplacementPokemon(terribleMoves, usablePokemon, battleSide);
if (bestReplacement is null)
{
AILogging.LogInformation(
- $"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.BattleData.Position}.");
+ $"ExplicitAI: No suitable replacement Pokemon found for {pokemon} at position {pokemon.Position}.");
return false;
}
choice = new SwitchChoice(pokemon, bestReplacement);
return true;
}
- private IPokemon? ChooseBestReplacementPokemon(bool terribleMoves, IReadOnlyList usablePokemon,
- IBattleSide battleSide)
+ private IBattlePokemon? ChooseBestReplacementPokemon(bool terribleMoves,
+ IReadOnlyList usablePokemon, IBattleSide battleSide)
{
var options = usablePokemon.Where((_, index) =>
{
@@ -84,7 +84,7 @@ public partial class ExplicitAI
private static readonly StringKey ToxicSpikesName = "toxic_spikes";
private static readonly StringKey StickyWebName = "sticky_web";
- private int RateReplacementPokemon(IPokemon pokemon, IBattleSide battleSide)
+ private int RateReplacementPokemon(IBattlePokemon pokemon, IBattleSide battleSide)
{
var score = 0;
var types = pokemon.Types;
@@ -107,7 +107,7 @@ public partial class ExplicitAI
var opponentSide = battleSide.Battle.Sides.First(x => x != battleSide);
foreach (var foe in opponentSide.Pokemon.WhereNotNull())
{
- var lastMoveUsed = foe.BattleData?.LastMoveChoice;
+ var lastMoveUsed = foe.LastMoveChoice;
if (lastMoveUsed is null || lastMoveUsed.ChosenMove.MoveData.Category == MoveCategory.Status)
continue;
var moveType = lastMoveUsed.ChosenMove.MoveData.MoveType;
@@ -134,22 +134,19 @@ public partial class ExplicitAI
///
/// Calculates the expected entry hazard damage for a given Pokémon on a given battle side.
///
- public static uint CalculateEntryHazardDamage(IPokemon pokemon, IBattleSide side)
+ public static uint CalculateEntryHazardDamage(IBattlePokemon pokemon, IBattleSide side)
{
var damage = 0u;
side.RunScriptHook(x => x.ExpectedEntryDamage(pokemon, ref damage));
return damage;
}
- private static bool CanSwitch(IPokemon pokemon)
+ private static bool CanSwitch(IBattlePokemon pokemon)
{
- var battleData = pokemon.BattleData;
- if (battleData == null)
+ if (pokemon.Battle.IsWildBattle)
return false;
- if (battleData.Battle.IsWildBattle)
- return false;
- var partyForIndex = battleData.Battle.Parties.FirstOrDefault(x =>
- x.IsResponsibleForIndex(new ResponsibleIndex(battleData.SideIndex, battleData.Position)));
+ var partyForIndex = pokemon.Battle.Parties.FirstOrDefault(x =>
+ x.IsResponsibleForIndex(new ResponsibleIndex(pokemon.SideIndex, pokemon.Position)));
return partyForIndex != null && partyForIndex.HasUsablePokemonNotInField();
}
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
index 4414f37..4b74009 100644
--- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
+++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.Utilities.cs
@@ -37,7 +37,7 @@ public partial class ExplicitAI
private static readonly StringKey KomalaName = "komala";
private static readonly StringKey MiniorName = "minior";
- private static bool CanBePoisoned(IPokemon pokemon, IBattle battle)
+ private static bool CanBePoisoned(IBattlePokemon pokemon, IBattle battle)
{
if (battle.TerrainName == MistyTerrainName)
return false;
@@ -60,7 +60,7 @@ public partial class ExplicitAI
return true;
}
- private static bool CanAbsorbMove(IPokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle)
+ private static bool CanAbsorbMove(IBattlePokemon pokemon, IMoveData move, TypeIdentifier moveType, IBattle battle)
{
if (pokemon.ActiveAbility == null)
return false;
diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs
index 928186b..898d624 100644
--- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs
+++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAI.cs
@@ -118,7 +118,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
public IRandom Random => _random;
///
- public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
+ public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon)
{
if (battle.HasForcedTurn(pokemon, out var choice))
return choice;
@@ -131,8 +131,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var moveChoices = GetMoveScores(pokemon, battle);
if (moveChoices.Count == 0)
{
- var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
- return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
+ var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
+ return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position);
}
var maxScore = moveChoices.Max(x => x.score);
if (TrainerHighSkill && CanSwitch(pokemon))
@@ -144,7 +144,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
if (!badMoves && _random.GetInt(100) < 25)
badMoves = true;
}
- else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 &&
+ else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.TurnsOnField > 2 &&
_random.GetInt(100) < 80)
{
badMoves = true;
@@ -164,8 +164,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var totalScore = considerChoices.Sum(x => x.Item2);
if (totalScore == 0)
{
- var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
- return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
+ var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
+ return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.Position);
}
var initialRandomValue = _random.GetFloat(0, totalScore);
var randomValue = initialRandomValue;
@@ -177,15 +177,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
var (index, _, targetIndex) = considerChoices[i].x;
var learnedMove = pokemon.Moves[index];
- var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
+ var opponentSide = (byte)(pokemon.SideIndex == 0 ? 1 : 0);
if (targetIndex == -1)
- targetIndex = pokemon.BattleData.Position;
+ targetIndex = pokemon.Position;
return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex);
}
throw new InvalidOperationException("No valid move choice found. This should not happen.");
}
- private List<(int index, int score, int targetIndex)> GetMoveScores(IPokemon user, IBattle battle)
+ private List<(int index, int score, int targetIndex)> GetMoveScores(IBattlePokemon user, IBattle battle)
{
var choices = new List<(int index, int score, int targetIndex)>();
foreach (var (learnedMove, index) in user.Moves.Select((x, i) => (x, i)).Where(x => x.x != null))
@@ -249,30 +249,24 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
// TODO: get redirected target
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
- var battleData = pokemon.BattleData;
- if (battleData == null)
+ if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user))
continue;
- if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
- continue;
- if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
+ if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex)
{
continue;
}
var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle);
- AddMoveToChoices(index, score, battleData.Position);
+ AddMoveToChoices(index, score, pokemon.Position);
}
}
else
{
- var targets = new List();
+ var targets = new List();
foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
{
- var battleData = pokemon.BattleData;
- if (battleData == null)
+ if (!TargetResolver.IsValidTarget(pokemon.SideIndex, pokemon.Position, target, user))
continue;
- if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
- continue;
- if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
+ if (target.TargetsFoe() && pokemon.SideIndex == user.SideIndex)
{
continue;
}
@@ -295,7 +289,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
}
}
- private bool PredictMoveFailure(IPokemon user, IBattle battle, AIMoveState aiMove)
+ private bool PredictMoveFailure(IBattlePokemon user, IBattle battle, AIMoveState aiMove)
{
if (user.HasStatus("sleep"))
{
@@ -333,14 +327,15 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey SubstituteName = new("substitute");
private static readonly StringKey InfiltratorName = new("infiltrator");
- private bool PredictMoveFailureAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
+ private bool PredictMoveFailureAgainstTarget(IBattlePokemon user, AIMoveState aiMove, IBattlePokemon target,
+ IBattle battle)
{
if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this,
aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target)))
return true;
if (aiMove.Move.Priority > 0)
{
- if (target.BattleData?.SideIndex != user.BattleData?.SideIndex)
+ if (target.SideIndex != user.SideIndex)
{
// Psychic Terrain makes all priority moves fail if the target is affected
if (battle.TerrainName == PsychicTerrainName && !target.IsFloating)
@@ -348,7 +343,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return true;
}
// Dazzling and Queenly Majesty prevent priority moves from being used against the Pokémon with those abilities
- if (target.BattleData?.BattleSide.Pokemon.WhereNotNull().Any(x =>
+ if (target.BattleSide.Pokemon.WhereNotNull().Any(x =>
x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true)
{
return true;
@@ -362,7 +357,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0)
return true;
if (user.ActiveAbility?.Name == PranksterName && aiMove.Move.Category == MoveCategory.Status &&
- target.Types.Any(x => x.Name == DarkName) && target.BattleData?.SideIndex != user.BattleData?.SideIndex)
+ target.Types.Any(x => x.Name == DarkName) && target.SideIndex != user.SideIndex)
return true;
if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating)
return true;
@@ -375,7 +370,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return false;
}
- private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList? targets = null)
+ private int GetMoveScore(IBattlePokemon user, AIMoveState aiMove, IBattle battle,
+ IReadOnlyList? targets = null)
{
var score = MoveBaseScore;
if (targets != null)
@@ -411,7 +407,8 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
return score;
}
- private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
+ private int GetMoveScoreAgainstTarget(IBattlePokemon user, AIMoveState aiMove, IBattlePokemon target,
+ IBattle battle)
{
if (_skillFlags.CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
{
@@ -427,8 +424,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
_handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score);
}
- if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
- target.BattleData?.Position != user.BattleData?.Position)
+ if (aiMove.Move.Target.TargetsFoe() && target.SideIndex == user.SideIndex && target.Position != user.Position)
{
if (score == MoveUselessScore)
return -1;
@@ -442,7 +438,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey OvercoatName = new("overcoat");
private static readonly StringKey SafetyGogglesName = new("safety_goggles");
- private static bool AffectedByPowder(IPokemon pokemon)
+ private static bool AffectedByPowder(IBattlePokemon pokemon)
{
if (pokemon.Types.Any(x => x.Name == GrassName))
return false;
@@ -456,7 +452,7 @@ public partial class ExplicitAI : PokemonAI, IExplicitAI
private static readonly StringKey TruantName = "truant";
private static readonly StringKey TruantEffectName = "truant_effect";
- private static bool CanAttack(IPokemon pokemon)
+ private static bool CanAttack(IBattlePokemon pokemon)
{
if (pokemon.Volatile.Contains("requires_recharge"))
return false;
diff --git a/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs b/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs
index 0962465..c787cee 100644
--- a/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs
+++ b/PkmnLib.Dynamic/AI/Explicit/ExplicitAIHandlers.cs
@@ -6,7 +6,7 @@ namespace PkmnLib.Dynamic.AI.Explicit;
///
/// An option where a move is used against a target
///
-public record struct MoveOption(AIMoveState Move, IBattle Battle, IPokemon? Target, uint EstimatedDamage = 0);
+public record struct MoveOption(AIMoveState Move, IBattle Battle, IBattlePokemon? Target, uint EstimatedDamage = 0);
///
/// A function that takes an explicit AI and a move option and returns a boolean value.
@@ -16,8 +16,8 @@ public delegate bool AIBoolHandler(IExplicitAI ai, MoveOption option);
///
/// A function for returning whether a Pokemon should switch.
///
-public delegate bool AISwitchBoolHandler(IExplicitAI ai, IPokemon pokemon, IBattle battle,
- IReadOnlyList reserves);
+public delegate bool AISwitchBoolHandler(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
+ IReadOnlyList reserves);
///
/// A function for returning the base power of a move.
@@ -112,7 +112,7 @@ public interface IReadOnlyExplicitAIHandlers
///
/// Indicates whether a Pokemon should switch into another Pokemon
///
- bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves);
+ bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle, IReadOnlyList reserves);
///
/// Functions that indicate whether a Pokemon should NOT switch
@@ -122,7 +122,8 @@ public interface IReadOnlyExplicitAIHandlers
///
/// Indicates whether a Pokemon should NOT switch into another Pokemon
///
- bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves);
+ bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
+ IReadOnlyList reserves);
///
/// Scores abilities
@@ -246,7 +247,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
ShouldSwitchFunctions;
///
- public bool ShouldSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves)
+ public bool ShouldSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
+ IReadOnlyList reserves)
{
var shouldSwitch = false;
foreach (var (_, handler) in ShouldSwitchFunctions)
@@ -272,7 +274,8 @@ public class ExplicitAIHandlers : IReadOnlyExplicitAIHandlers
public FunctionHandlerDictionary AbilityRanking = [];
///
- public bool ShouldNotSwitch(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList reserves)
+ public bool ShouldNotSwitch(IExplicitAI ai, IBattlePokemon pokemon, IBattle battle,
+ IReadOnlyList reserves)
{
var shouldNotSwitch = false;
foreach (var (_, handler) in ShouldNotSwitchFunctions)
diff --git a/PkmnLib.Dynamic/AI/HighestDamageAI.cs b/PkmnLib.Dynamic/AI/HighestDamageAI.cs
index 4cdb753..2a0cc88 100644
--- a/PkmnLib.Dynamic/AI/HighestDamageAI.cs
+++ b/PkmnLib.Dynamic/AI/HighestDamageAI.cs
@@ -15,9 +15,9 @@ public class HighestDamageAI : PokemonAI
}
///
- public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
+ public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon)
{
- var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0;
+ var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0;
var opponent = battle.Sides[opponentSide].Pokemon.WhereNotNull().FirstOrDefault(x => x.IsUsable);
var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0)))
.ToList();
diff --git a/PkmnLib.Dynamic/AI/PassTurnAI.cs b/PkmnLib.Dynamic/AI/PassTurnAI.cs
index b309daf..cd81d18 100644
--- a/PkmnLib.Dynamic/AI/PassTurnAI.cs
+++ b/PkmnLib.Dynamic/AI/PassTurnAI.cs
@@ -14,5 +14,5 @@ public class PassTurnAI : PokemonAI
}
///
- public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon) => new PassChoice(pokemon);
+ public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon) => new PassChoice(pokemon);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/AI/PokemonAI.cs b/PkmnLib.Dynamic/AI/PokemonAI.cs
index 73e6cbb..9b232ac 100644
--- a/PkmnLib.Dynamic/AI/PokemonAI.cs
+++ b/PkmnLib.Dynamic/AI/PokemonAI.cs
@@ -26,70 +26,69 @@ public abstract class PokemonAI
///
/// Gets the choice for the Pokémon.
///
- public abstract ITurnChoice GetChoice(IBattle battle, IPokemon pokemon);
+ public abstract ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon);
///
/// For a given user and move, returns the valid targets for that move.
///
- public IEnumerable<(byte side, byte position)> GetValidTargetsForMove(IPokemon user, ILearnedMove move)
+ public IEnumerable<(byte side, byte position)> GetValidTargetsForMove(IBattlePokemon user, ILearnedMove move)
{
- var userBattleData = user.BattleData!;
switch (move.MoveData.Target)
{
case MoveTarget.Adjacent:
- yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position);
+ yield return (GetOppositeSide(user.SideIndex), user.Position);
break;
case MoveTarget.AdjacentAlly:
- if (userBattleData.Position > 0)
- yield return (userBattleData.SideIndex, (byte)(userBattleData.Position - 1));
- if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1)
- yield return (userBattleData.SideIndex, (byte)(userBattleData.Position + 1));
+ if (user.Position > 0)
+ yield return (user.SideIndex, (byte)(user.Position - 1));
+ if (user.Battle.PositionsPerSide > user.Position + 1)
+ yield return (user.SideIndex, (byte)(user.Position + 1));
break;
case MoveTarget.AdjacentAllySelf:
- if (userBattleData.Position > 0)
- yield return (userBattleData.SideIndex, (byte)(userBattleData.Position - 1));
- if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1)
- yield return (userBattleData.SideIndex, (byte)(userBattleData.Position + 1));
- yield return (userBattleData.SideIndex, userBattleData.Position);
+ if (user.Position > 0)
+ yield return (user.SideIndex, (byte)(user.Position - 1));
+ if (user.Battle.PositionsPerSide > user.Position + 1)
+ yield return (user.SideIndex, (byte)(user.Position + 1));
+ yield return (user.SideIndex, user.Position);
break;
case MoveTarget.AdjacentOpponent:
- yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position);
- if (userBattleData.Position > 0)
- yield return (GetOppositeSide(userBattleData.SideIndex), (byte)(userBattleData.Position - 1));
- if (userBattleData.Battle.PositionsPerSide > userBattleData.Position + 1)
- yield return (GetOppositeSide(userBattleData.SideIndex), (byte)(userBattleData.Position + 1));
+ yield return (GetOppositeSide(user.SideIndex), user.Position);
+ if (user.Position > 0)
+ yield return (GetOppositeSide(user.SideIndex), (byte)(user.Position - 1));
+ if (user.Battle.PositionsPerSide > user.Position + 1)
+ yield return (GetOppositeSide(user.SideIndex), (byte)(user.Position + 1));
break;
case MoveTarget.All:
- yield return (userBattleData.SideIndex, userBattleData.Position);
+ yield return (user.SideIndex, user.Position);
break;
case MoveTarget.AllAdjacent:
- yield return (userBattleData.SideIndex, userBattleData.Position);
+ yield return (user.SideIndex, user.Position);
break;
case MoveTarget.AllAdjacentOpponent:
- yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position);
+ yield return (GetOppositeSide(user.SideIndex), user.Position);
break;
case MoveTarget.AllAlly:
- yield return (userBattleData.SideIndex, userBattleData.Position);
+ yield return (user.SideIndex, user.Position);
break;
case MoveTarget.AllOpponent:
- yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position);
+ yield return (GetOppositeSide(user.SideIndex), user.Position);
break;
case MoveTarget.Any:
- foreach (var side in userBattleData.Battle.Sides)
+ foreach (var side in user.Battle.Sides)
{
foreach (var pokemon in side.Pokemon)
{
- if (pokemon?.BattleData == null)
+ if (pokemon == null)
continue;
- yield return (side.Index, pokemon.BattleData!.Position);
+ yield return (side.Index, pokemon.Position);
}
}
break;
case MoveTarget.RandomOpponent:
- yield return (GetOppositeSide(userBattleData.SideIndex), userBattleData.Position);
+ yield return (GetOppositeSide(user.SideIndex), user.Position);
break;
case MoveTarget.SelfUse:
- yield return (userBattleData.SideIndex, userBattleData.Position);
+ yield return (user.SideIndex, user.Position);
break;
default:
throw new ArgumentOutOfRangeException();
diff --git a/PkmnLib.Dynamic/AI/PrescientAI.cs b/PkmnLib.Dynamic/AI/PrescientAI.cs
index 4d63f7a..c06cfe7 100644
--- a/PkmnLib.Dynamic/AI/PrescientAI.cs
+++ b/PkmnLib.Dynamic/AI/PrescientAI.cs
@@ -18,9 +18,9 @@ public class PrescientAI : PokemonAI
}
///
- public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
+ public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon)
{
- var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0;
+ var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0;
var moves = pokemon.Moves.WhereNotNull().Where(x => battle.CanUse(new MoveChoice(pokemon, x, opponentSide, 0)))
.ToList();
@@ -34,13 +34,13 @@ public class PrescientAI : PokemonAI
}
private static IEnumerable<(ITurnChoice Choice, float Score)> ScoreChoices(IBattle battle,
- IReadOnlyList moves, IPokemon pokemon)
+ IReadOnlyList moves, IBattlePokemon pokemon)
{
- var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0;
+ var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0;
foreach (var learnedMoveOriginal in moves.WhereNotNull())
{
var battleClone = battle.DeepClone();
- var pokemonClone = battleClone.Sides[pokemon.BattleData!.SideIndex].Pokemon[pokemon.BattleData.Position]!;
+ var pokemonClone = battleClone.Sides[pokemon.SideIndex].Pokemon[pokemon.Position]!;
var learnedMove = pokemonClone.Moves.WhereNotNull()
.First(m => m.MoveData.Name == learnedMoveOriginal.MoveData.Name);
var choice = new MoveChoice(pokemonClone, learnedMove, opponentSide, 0);
@@ -57,17 +57,16 @@ public class PrescientAI : PokemonAI
}
if (battleClone.TrySetChoice(choice))
{
- var score = CalculateScore(battleClone.Parties[pokemon.BattleData.SideIndex],
- battleClone.Parties[opponentSide]);
+ var score = CalculateScore(battleClone.Parties[pokemon.SideIndex], battleClone.Parties[opponentSide]);
var realChoice = new MoveChoice(pokemon, learnedMoveOriginal, opponentSide, 0);
yield return (realChoice, score);
}
}
}
- private static ITurnChoice GetOpponentChoice(IBattle battle, IPokemon pokemon)
+ private static ITurnChoice GetOpponentChoice(IBattle battle, IBattlePokemon pokemon)
{
- var opponentSide = pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0;
+ var opponentSide = pokemon.SideIndex == 0 ? (byte)1 : (byte)0;
var opponent = battle.Sides[opponentSide].Pokemon[0];
if (opponent is null)
{
diff --git a/PkmnLib.Dynamic/AI/RandomAI.cs b/PkmnLib.Dynamic/AI/RandomAI.cs
index defeb98..2e62377 100644
--- a/PkmnLib.Dynamic/AI/RandomAI.cs
+++ b/PkmnLib.Dynamic/AI/RandomAI.cs
@@ -19,7 +19,7 @@ public class RandomAI : PokemonAI
}
///
- public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
+ public override ITurnChoice GetChoice(IBattle battle, IBattlePokemon pokemon)
{
var moves = pokemon.Moves.WhereNotNull().Where(x => x.CurrentPp > 0).ToList();
while (moves.Count > 0)
@@ -28,7 +28,7 @@ public class RandomAI : PokemonAI
var targets = GetValidTargetsForMove(pokemon, move).ToArray();
if (move.MoveData.Category is MoveCategory.Physical or MoveCategory.Special)
{
- targets = targets.Where(x => x.side != pokemon.BattleData!.SideIndex).ToArray();
+ targets = targets.Where(x => x.side != pokemon.SideIndex).ToArray();
}
if (targets.Length == 0)
{
@@ -43,7 +43,7 @@ public class RandomAI : PokemonAI
}
moves.Remove(move);
}
- return battle.Library.MiscLibrary.ReplacementChoice(pokemon,
- pokemon.BattleData!.SideIndex == 0 ? (byte)1 : (byte)0, pokemon.BattleData.Position);
+ return battle.Library.MiscLibrary.ReplacementChoice(pokemon, pokemon.SideIndex == 0 ? (byte)1 : (byte)0,
+ pokemon.Position);
}
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs
index 0a8be67..cb449f2 100644
--- a/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs
+++ b/PkmnLib.Dynamic/BattleFlow/MoveTurnExecutor.cs
@@ -15,7 +15,7 @@ public static class MoveTurnExecutor
{
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
{
- moveChoice.User.BattleData!.LastMoveChoice = moveChoice;
+ moveChoice.User.LastMoveChoice = moveChoice;
var chosenMove = moveChoice.ChosenMove;
var useMove = chosenMove.MoveData;
@@ -126,7 +126,7 @@ public static class MoveTurnExecutor
private static readonly ThreadLocal> TypeListCache = new(() => []);
- private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IPokemon target)
+ private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IBattlePokemon target)
{
var failed = false;
target.RunScriptHook(x => x.FailIncomingMove(executingMove, target, ref failed));
diff --git a/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs b/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs
index c48baed..3e302d3 100644
--- a/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs
+++ b/PkmnLib.Dynamic/BattleFlow/TargetResolver.cs
@@ -11,7 +11,8 @@ public static class TargetResolver
///
/// Get the targets of a move based on the target type, and the selected side and position to target.
///
- public static IReadOnlyList ResolveTargets(IBattle battle, byte side, byte position, MoveTarget target)
+ public static IReadOnlyList ResolveTargets(IBattle battle, byte side, byte position,
+ MoveTarget target)
{
return target switch
{
@@ -29,13 +30,10 @@ public static class TargetResolver
///
/// Validates whether a given target is valid for a move choice. Returns true if the target is valid.
///
- public static bool IsValidTarget(byte side, byte position, MoveTarget target, IPokemon user)
+ public static bool IsValidTarget(byte side, byte position, MoveTarget target, IBattlePokemon user)
{
- var userBattleData = user.BattleData;
- if (userBattleData == null)
- throw new ArgumentNullException(nameof(user.BattleData));
- var userSide = userBattleData.SideIndex;
- var userPosition = userBattleData.Position;
+ var userSide = user.SideIndex;
+ var userPosition = user.Position;
switch (target)
{
@@ -80,7 +78,7 @@ public static class TargetResolver
throw new ArgumentOutOfRangeException(nameof(target), target, null);
}
- private static IReadOnlyList GetAllTargets(IBattle battle) =>
+ private static IReadOnlyList GetAllTargets(IBattle battle) =>
battle.Sides.SelectMany(x => x.Pokemon).ToList();
private static byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0;
@@ -89,7 +87,7 @@ public static class TargetResolver
/// Gets all Pokémon that are adjacent to of directly opposite of a Pokémon. This means the target,
/// the Pokémon left of it, the Pokémon right of it, and the Pokémon opposite of it.
///
- private static IReadOnlyList GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position)
+ private static IReadOnlyList GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position)
{
var left = position - 1;
var right = position + 1;
@@ -123,7 +121,7 @@ public static class TargetResolver
];
}
- private static IReadOnlyList GetAllAdjacent(IBattle battle, byte side, byte position)
+ private static IReadOnlyList GetAllAdjacent(IBattle battle, byte side, byte position)
{
var left = position - 1;
var right = position + 1;
diff --git a/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs b/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs
index 63a3aed..26f36e1 100644
--- a/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs
+++ b/PkmnLib.Dynamic/BattleFlow/TurnRunner.cs
@@ -85,7 +85,7 @@ public static class TurnRunner
return;
if (!choice.User.IsUsable)
return;
- if (choice.User.BattleData?.IsOnBattlefield != true)
+ if (!choice.User.IsOnBattlefield)
return;
switch (choice)
{
@@ -108,9 +108,6 @@ public static class TurnRunner
private static void ExecuteSwitchChoice(IBattle battle, ISwitchChoice fleeChoice)
{
var user = fleeChoice.User;
- var battleData = user.BattleData;
- if (battleData == null)
- return;
var preventSwitch = false;
fleeChoice.RunScriptHook(script =>
script.PreventSelfSwitch(fleeChoice, ref preventSwitch));
@@ -118,7 +115,7 @@ public static class TurnRunner
return;
foreach (var side in battle.Sides)
{
- if (side.Index == battleData.SideIndex)
+ if (side.Index == user.SideIndex)
continue;
foreach (var pokemon in side.Pokemon.WhereNotNull())
{
@@ -129,16 +126,12 @@ public static class TurnRunner
}
}
user.Volatile.Clear();
- var userSide = battle.Sides[battleData.SideIndex];
- userSide.SwapPokemon(battleData.Position, fleeChoice.SwitchTo);
+ user.BattleSide.SwapPokemon(user.Position, fleeChoice.SwitchTo);
}
private static void ExecuteFleeChoice(IBattle battle, IFleeChoice fleeChoice)
{
var user = fleeChoice.User;
- var battleData = user.BattleData;
- if (battleData == null)
- return;
if (!battle.CanFlee)
return;
@@ -150,7 +143,7 @@ public static class TurnRunner
foreach (var side in battle.Sides)
{
- if (side.Index == battleData.SideIndex)
+ if (side.Index == user.SideIndex)
continue;
foreach (var pokemon in side.Pokemon.WhereNotNull())
{
@@ -167,8 +160,7 @@ public static class TurnRunner
return;
}
- var userSide = battle.Sides[battleData.SideIndex];
- userSide.MarkAsFled();
+ user.BattleSide.MarkAsFled();
battle.EventHook.Invoke(new FleeEvent(user, true));
battle.ValidateBattleState();
}
@@ -176,9 +168,6 @@ public static class TurnRunner
private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice)
{
var user = itemChoice.User;
- var battleData = user.BattleData;
- if (battleData == null)
- return;
var target = itemChoice.GetTargetPokemon(battle);
battle.EventHook.Invoke(new ItemUseEvent(user, itemChoice.Item));
itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user, user, battle, battle.EventHook);
diff --git a/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs b/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs
index 3e41d04..643814f 100644
--- a/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs
+++ b/PkmnLib.Dynamic/Events/AbilityTriggerEvent.cs
@@ -12,7 +12,7 @@ public record AbilityTriggerEvent : IEventData
///
/// The Pokémon whose ability is being triggered.
///
- public IPokemon Pokemon { get; }
+ public IBattlePokemon Pokemon { get; }
///
/// The ability that is being triggered for the Pokémon.
@@ -22,7 +22,7 @@ public record AbilityTriggerEvent : IEventData
public Dictionary? Metadata { get; init; } = null;
///
- public AbilityTriggerEvent(IPokemon pokemon)
+ public AbilityTriggerEvent(IBattlePokemon pokemon)
{
Pokemon = pokemon;
Ability = pokemon.ActiveAbility;
diff --git a/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs b/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs
index d8bb3db..6b16822 100644
--- a/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs
+++ b/PkmnLib.Dynamic/Events/CaptureAttemptEvent.cs
@@ -10,7 +10,7 @@ namespace PkmnLib.Dynamic.Events;
public class CaptureAttemptEvent : IEventData
{
///
- public CaptureAttemptEvent(IPokemon target, CaptureResult result, IItem captureItem)
+ public CaptureAttemptEvent(IBattlePokemon target, CaptureResult result, IItem captureItem)
{
Target = target;
Result = result;
@@ -20,7 +20,7 @@ public class CaptureAttemptEvent : IEventData
///
/// The Pokémon that is being captured.
///
- public IPokemon Target { get; init; }
+ public IBattlePokemon Target { get; init; }
///
/// The result of the capture attempt.
diff --git a/PkmnLib.Dynamic/Events/DamageEvent.cs b/PkmnLib.Dynamic/Events/DamageEvent.cs
index b1ea789..12ae68d 100644
--- a/PkmnLib.Dynamic/Events/DamageEvent.cs
+++ b/PkmnLib.Dynamic/Events/DamageEvent.cs
@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public record DamageEvent : IEventData
{
///
- public DamageEvent(IPokemon pokemon, uint previousHealth, uint newHealth, DamageSource source)
+ public DamageEvent(IBattlePokemon pokemon, uint previousHealth, uint newHealth, DamageSource source)
{
Pokemon = pokemon;
PreviousHealth = previousHealth;
@@ -19,7 +19,7 @@ public record DamageEvent : IEventData
///
/// The Pokemon that took damage.
///
- public IPokemon Pokemon { get; init; }
+ public IBattlePokemon Pokemon { get; init; }
///
/// The previous health of the Pokemon.
diff --git a/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs b/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs
index 8e0f488..34c5436 100644
--- a/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs
+++ b/PkmnLib.Dynamic/Events/DisplaySpeciesChangeEvent.cs
@@ -5,11 +5,11 @@ namespace PkmnLib.Dynamic.Events;
public class DisplaySpeciesChangeEvent : IEventData
{
- public IPokemon Pokemon { get; }
+ public IBattlePokemon Pokemon { get; }
public ISpecies? Species { get; }
public IForm? Form { get; }
- public DisplaySpeciesChangeEvent(IPokemon pokemon, ISpecies? species, IForm? form)
+ public DisplaySpeciesChangeEvent(IBattlePokemon pokemon, ISpecies? species, IForm? form)
{
Pokemon = pokemon;
Species = species;
diff --git a/PkmnLib.Dynamic/Events/FaintEvent.cs b/PkmnLib.Dynamic/Events/FaintEvent.cs
index 5b2780a..54491c0 100644
--- a/PkmnLib.Dynamic/Events/FaintEvent.cs
+++ b/PkmnLib.Dynamic/Events/FaintEvent.cs
@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public class FaintEvent : IEventData
{
///
- public FaintEvent(IPokemon pokemon)
+ public FaintEvent(IBattlePokemon pokemon)
{
Pokemon = pokemon;
}
@@ -16,7 +16,7 @@ public class FaintEvent : IEventData
///
/// The Pokemon that fainted.
///
- public IPokemon Pokemon { get; init; }
+ public IBattlePokemon Pokemon { get; init; }
///
public EventBatchId BatchId { get; init; } = new();
diff --git a/PkmnLib.Dynamic/Events/FleeEvent.cs b/PkmnLib.Dynamic/Events/FleeEvent.cs
index 2a82cad..5d893b1 100644
--- a/PkmnLib.Dynamic/Events/FleeEvent.cs
+++ b/PkmnLib.Dynamic/Events/FleeEvent.cs
@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public class FleeEvent : IEventData
{
///
- public FleeEvent(IPokemon pokemon, bool success)
+ public FleeEvent(IBattlePokemon pokemon, bool success)
{
Pokemon = pokemon;
Success = success;
@@ -17,7 +17,7 @@ public class FleeEvent : IEventData
///
/// The Pokémon that attempted to flee.
///
- public IPokemon Pokemon { get; }
+ public IBattlePokemon Pokemon { get; }
///
/// Indicates whether the flee attempt was successful.
diff --git a/PkmnLib.Dynamic/Events/ItemUseEvent.cs b/PkmnLib.Dynamic/Events/ItemUseEvent.cs
index 811816e..51629fb 100644
--- a/PkmnLib.Dynamic/Events/ItemUseEvent.cs
+++ b/PkmnLib.Dynamic/Events/ItemUseEvent.cs
@@ -5,13 +5,13 @@ namespace PkmnLib.Dynamic.Events;
public record ItemUseEvent : IEventData
{
- public ItemUseEvent(IPokemon pokemon, IItem itemUsed)
+ public ItemUseEvent(IBattlePokemon pokemon, IItem itemUsed)
{
Pokemon = pokemon;
ItemUsed = itemUsed;
}
- public IPokemon Pokemon { get; set; }
+ public IBattlePokemon Pokemon { get; set; }
public IItem ItemUsed { get; set; }
///
diff --git a/PkmnLib.Dynamic/Events/MoveHitEvent.cs b/PkmnLib.Dynamic/Events/MoveHitEvent.cs
index 7969a84..3ec3e5c 100644
--- a/PkmnLib.Dynamic/Events/MoveHitEvent.cs
+++ b/PkmnLib.Dynamic/Events/MoveHitEvent.cs
@@ -20,10 +20,10 @@ public class MoveHitEvent : IEventData
///
/// The target of the move.
///
- public IPokemon Target { get; }
+ public IBattlePokemon Target { get; }
///
- public MoveHitEvent(IExecutingMove executingMove, IHitData hitData, IPokemon target)
+ public MoveHitEvent(IExecutingMove executingMove, IHitData hitData, IBattlePokemon target)
{
ExecutingMove = executingMove;
HitData = hitData;
diff --git a/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs b/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs
index d8a7bf1..f3b5fc1 100644
--- a/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs
+++ b/PkmnLib.Dynamic/Events/MoveInvulnerableEvent.cs
@@ -5,9 +5,9 @@ namespace PkmnLib.Dynamic.Events;
public class MoveInvulnerableEvent : IEventData
{
public IExecutingMove ExecutingMove { get; }
- public IPokemon Target { get; }
+ public IBattlePokemon Target { get; }
- public MoveInvulnerableEvent(IExecutingMove executingMove, IPokemon target)
+ public MoveInvulnerableEvent(IExecutingMove executingMove, IBattlePokemon target)
{
ExecutingMove = executingMove;
Target = target;
diff --git a/PkmnLib.Dynamic/Events/StatBoostEvent.cs b/PkmnLib.Dynamic/Events/StatBoostEvent.cs
index ca01d33..101a998 100644
--- a/PkmnLib.Dynamic/Events/StatBoostEvent.cs
+++ b/PkmnLib.Dynamic/Events/StatBoostEvent.cs
@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.Events;
public class StatBoostEvent : IEventData
{
///
- public StatBoostEvent(IPokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost)
+ public StatBoostEvent(IBattlePokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost)
{
Pokemon = pokemon;
Statistic = statistic;
@@ -20,7 +20,7 @@ public class StatBoostEvent : IEventData
///
/// The Pokemon that had its stat boosted.
///
- public IPokemon Pokemon { get; }
+ public IBattlePokemon Pokemon { get; }
///
/// The statistic that was boosted.
diff --git a/PkmnLib.Dynamic/Events/StatusChangeEvent.cs b/PkmnLib.Dynamic/Events/StatusChangeEvent.cs
index 8cfe7dd..9c46a1d 100644
--- a/PkmnLib.Dynamic/Events/StatusChangeEvent.cs
+++ b/PkmnLib.Dynamic/Events/StatusChangeEvent.cs
@@ -9,7 +9,7 @@ namespace PkmnLib.Dynamic.Events;
public record StatusChangeEvent : IEventData
{
///
- public StatusChangeEvent(IPokemon pokemon, StringKey? previousStatus, StringKey? newStatus)
+ public StatusChangeEvent(IBattlePokemon pokemon, StringKey? previousStatus, StringKey? newStatus)
{
Pokemon = pokemon;
PreviousStatus = previousStatus;
@@ -19,7 +19,7 @@ public record StatusChangeEvent : IEventData
///
/// The Pokémon whose status has changed.
///
- public IPokemon Pokemon { get; }
+ public IBattlePokemon Pokemon { get; }
///
/// The new status of the Pokémon after the change.
diff --git a/PkmnLib.Dynamic/Events/SwitchEvent.cs b/PkmnLib.Dynamic/Events/SwitchEvent.cs
index 95289d2..7ea0748 100644
--- a/PkmnLib.Dynamic/Events/SwitchEvent.cs
+++ b/PkmnLib.Dynamic/Events/SwitchEvent.cs
@@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Events;
public class SwitchEvent : IEventData
{
///
- public SwitchEvent(byte sideIndex, byte position, IPokemon? pokemon)
+ public SwitchEvent(byte sideIndex, byte position, IBattlePokemon? pokemon)
{
SideIndex = sideIndex;
Position = position;
@@ -28,7 +28,7 @@ public class SwitchEvent : IEventData
///
/// The Pokémon that is switching in. If null, no Pokémon is switching in, and the slot is empty after the switch.
///
- public IPokemon? Pokemon { get; init; }
+ public IBattlePokemon? Pokemon { get; init; }
///
public EventBatchId BatchId { get; init; }
diff --git a/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs b/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs
index 99b64c6..4dea97f 100644
--- a/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs
+++ b/PkmnLib.Dynamic/Libraries/BattleStatCalculator.cs
@@ -21,15 +21,16 @@ public interface IBattleStatCalculator
///
/// Calculate all the boosted stats of a Pokemon, including stat boosts.
///
- void CalculateBoostedStats(IPokemon pokemon, StatisticSet stats);
+ void CalculateBoostedStats(IBattlePokemon pokemon, StatisticSet stats);
///
/// Calculate a single boosted stat of a Pokemon, including stat boosts.
///
- uint CalculateBoostedStat(IPokemon pokemon, Statistic stat);
+ uint CalculateBoostedStat(IBattlePokemon pokemon, Statistic stat);
///
/// Calculates the accuracy for a move, taking into account any accuracy modifiers.
///
- byte CalculateModifiedAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, byte moveAccuracy);
+ byte CalculateModifiedAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
+ byte moveAccuracy);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs b/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs
index c575899..6092211 100644
--- a/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs
+++ b/PkmnLib.Dynamic/Libraries/CaptureLibrary.cs
@@ -46,5 +46,5 @@ public interface ICaptureLibrary
///
/// Attempts to capture a Pokémon using a specified item (e.g., Poké Ball).
///
- CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random);
+ CaptureResult TryCapture(IBattlePokemon target, IItem captureItem, IBattleRandom random);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Libraries/DamageCalculator.cs b/PkmnLib.Dynamic/Libraries/DamageCalculator.cs
index 93f3590..56e7bbb 100644
--- a/PkmnLib.Dynamic/Libraries/DamageCalculator.cs
+++ b/PkmnLib.Dynamic/Libraries/DamageCalculator.cs
@@ -11,16 +11,16 @@ public interface IDamageCalculator
///
/// Calculate the damage for a given hit on a Pokemon.
///
- uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IPokemon user, IPokemon target,
+ uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IBattlePokemon user, IBattlePokemon target,
int targetCount, byte hitNumber, IHitData hitData);
///
/// Calculate the base power for a given hit on a Pokemon.
///
- ushort GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData);
+ ushort GetBasePower(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, IHitData hitData);
///
/// Returns whether a specified hit should be critical or not.
///
- bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber);
+ bool IsCritical(IBattle battle, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Libraries/MiscLibrary.cs b/PkmnLib.Dynamic/Libraries/MiscLibrary.cs
index 8178190..1b1ca65 100644
--- a/PkmnLib.Dynamic/Libraries/MiscLibrary.cs
+++ b/PkmnLib.Dynamic/Libraries/MiscLibrary.cs
@@ -13,7 +13,7 @@ public interface IMiscLibrary
/// Returns the choice that's used when a Pokemon is unable to make the move choice it wants to, or when it has no
/// moves left, yet wants to make a move.
///
- ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition);
+ ITurnChoice ReplacementChoice(IBattlePokemon user, byte targetSide, byte targetPosition);
///
/// Returns whether the given choice is the choice that is used when the user is unable to make a move choice.
diff --git a/PkmnLib.Dynamic/Models/Battle.cs b/PkmnLib.Dynamic/Models/Battle.cs
index 47ebe11..5d4698d 100644
--- a/PkmnLib.Dynamic/Models/Battle.cs
+++ b/PkmnLib.Dynamic/Models/Battle.cs
@@ -92,7 +92,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
///
/// Get a Pokemon on the battlefield, on a specific side and an index on that side.
///
- IPokemon? GetPokemon(byte side, byte position);
+ IBattlePokemon? GetPokemon(byte side, byte position);
///
/// Returns whether a slot on the battlefield can still be filled. If no party is responsible
@@ -116,7 +116,7 @@ public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
/// Checks whether a Pokemon has a forced turn choice. If it does, this returns true and the choice
/// is set in the out parameter. If it does not, this returns false and the out parameter is null.
///
- bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice);
+ bool HasForcedTurn(IBattlePokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice);
///
/// Checks whether a choice is actually possible.
@@ -202,6 +202,8 @@ public class BattleImpl : ScriptSource, IBattle
Sides = sides;
Random = randomSeed.HasValue ? new BattleRandomImpl(randomSeed.Value) : new BattleRandomImpl();
EventHook = new EventHook();
+ foreach (var party in parties)
+ party.Initialize(this);
}
///
@@ -248,12 +250,22 @@ public class BattleImpl : ScriptSource, IBattle
public BattleChoiceQueue? ChoiceQueue { get; private set; }
///
- public IPokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position];
+ public IBattlePokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position];
///
public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x =>
x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField());
+ private readonly List _capturedPokemon = [];
+
+ ///
+ /// Attaches battle-wide result data, such as the captured Pokémon, to a result.
+ ///
+ private BattleResult FinalizeResult(BattleResult result) => result with
+ {
+ CapturedPokemon = _capturedPokemon.ToList(),
+ };
+
///
public void ValidateBattleState()
{
@@ -265,7 +277,7 @@ public class BattleImpl : ScriptSource, IBattle
{
if (side.HasFledBattle)
{
- Result = BattleResult.Inconclusive;
+ Result = FinalizeResult(BattleResult.Inconclusive);
HasEnded = true;
return;
}
@@ -283,13 +295,13 @@ public class BattleImpl : ScriptSource, IBattle
// If every side is defeated, the battle is a draw
if (!survivingSideExists)
{
- Result = BattleResult.Inconclusive;
+ Result = FinalizeResult(BattleResult.Inconclusive);
HasEnded = true;
return;
}
// If only one side is left, that side has won
- Result = BattleResult.Conclusive(survivingSide!.Index);
+ Result = FinalizeResult(BattleResult.Conclusive(survivingSide!.Index));
HasEnded = true;
}
@@ -297,22 +309,15 @@ public class BattleImpl : ScriptSource, IBattle
public void ForceEndBattle()
{
HasEnded = true;
- Result = BattleResult.Inconclusive;
+ Result = FinalizeResult(BattleResult.Inconclusive);
}
///
- public bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice)
+ public bool HasForcedTurn(IBattlePokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice)
{
- var battleData = pokemon.BattleData;
- if (battleData == null)
- {
- choice = null;
- return false;
- }
-
ITurnChoice? forcedChoice = null;
pokemon.RunScriptHook(script =>
- script.ForceTurnSelection(this, battleData.SideIndex, battleData.Position, ref forcedChoice));
+ script.ForceTurnSelection(this, pokemon.SideIndex, pokemon.Position, ref forcedChoice));
choice = forcedChoice;
return choice != null;
}
@@ -346,7 +351,7 @@ public class BattleImpl : ScriptSource, IBattle
if (!switchChoice.SwitchTo.IsUsable)
return false;
// Can't switch to a Pokémon already on the field
- if (switchChoice.SwitchTo.BattleData is { IsOnBattlefield: true })
+ if (switchChoice.SwitchTo.IsOnBattlefield)
return false;
if (switchChoice.SwitchTo == switchChoice.User)
return false;
@@ -389,10 +394,10 @@ public class BattleImpl : ScriptSource, IBattle
{
if (!CanUse(choice))
return false;
- if (choice.User.BattleData?.IsOnBattlefield != true)
+ if (!choice.User.IsOnBattlefield)
return false;
- var side = Sides[choice.User.BattleData!.SideIndex];
- side.SetChoice(choice.User.BattleData!.Position, choice);
+ var side = Sides[choice.User.SideIndex];
+ side.SetChoice(choice.User.Position, choice);
CheckChoicesSetAndRun();
return true;
}
@@ -555,8 +560,8 @@ public class BattleImpl : ScriptSource, IBattle
if (attemptCapture.IsCaught)
{
target.MarkAsCaught();
- var side = Sides[target.BattleData!.SideIndex];
- side.ForceClearPokemonFromField(target.BattleData.Position);
+ _capturedPokemon.Add(target.UnderlyingPokemon);
+ target.BattleSide.ForceClearPokemonFromField(target.Position);
}
EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture, item));
@@ -592,9 +597,9 @@ public class BattleImpl : ScriptSource, IBattle
{
foreach (var party in Parties)
{
- foreach (var pokemon in party.Party.WhereNotNull())
+ foreach (var pokemon in party.BattlePokemon.WhereNotNull())
{
- pokemon.ClearBattleData();
+ pokemon.OnBattleEnd();
}
}
_weatherScript.Clear();
diff --git a/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs b/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs
index ffd31ce..59d7182 100644
--- a/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs
+++ b/PkmnLib.Dynamic/Models/BattleChoiceQueue.cs
@@ -87,7 +87,7 @@ public class BattleChoiceQueue : IDeepCloneable
///
/// Returns true if the Pokémon was found and moved, false otherwise.
///
- public bool MovePokemonChoiceNext(IPokemon pokemon)
+ public bool MovePokemonChoiceNext(IBattlePokemon pokemon)
{
var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon);
if (index == -1)
@@ -110,7 +110,7 @@ public class BattleChoiceQueue : IDeepCloneable
///
/// Returns true if the Pokémon was found and moved, false otherwise.
///
- public bool MovePokemonChoiceLast(IPokemon pokemon)
+ public bool MovePokemonChoiceLast(IBattlePokemon pokemon)
{
var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon);
if (index == -1)
diff --git a/PkmnLib.Dynamic/Models/BattleParty.cs b/PkmnLib.Dynamic/Models/BattleParty.cs
index 3639770..aee4fb3 100644
--- a/PkmnLib.Dynamic/Models/BattleParty.cs
+++ b/PkmnLib.Dynamic/Models/BattleParty.cs
@@ -4,15 +4,35 @@ namespace PkmnLib.Dynamic.Models;
///
/// A battle party is a wrapper around a Pokemon party that provides additional functionality for battles.
-/// It indicates for which side and position the party is responsible.
+/// It indicates for which side and position the party is responsible, and holds the battle-scoped
+/// wrappers for the party's Pokémon.
///
public interface IBattleParty : IDeepCloneable
{
///
- /// The backing Pokemon party.
+ /// The backing Pokemon party. Battle code should generally use instead, as that
+ /// view contains the battle-scoped wrappers.
///
IPokemonParty Party { get; }
+ ///
+ /// The battle-scoped view of the party. Index-aligned with . Only available after
+ /// has been called by the battle.
+ ///
+ IReadOnlyList BattlePokemon { get; }
+
+ ///
+ /// Gets the battle-scoped wrapper for a Pokémon in this party. Returns null if the Pokémon is not in this
+ /// party. Accepts either the underlying Pokémon or the wrapper itself.
+ ///
+ IBattlePokemon? GetBattlePokemon(IPokemon pokemon);
+
+ ///
+ /// Initializes the battle-scoped wrappers for this party. This is called by the battle when it is created,
+ /// and should not be called by user code.
+ ///
+ void Initialize(IBattle battle);
+
///
/// Whether the party is responsible for the specified side and position.
///
@@ -26,7 +46,7 @@ public interface IBattleParty : IDeepCloneable
///
/// Gets all usable Pokemon that are not currently in the field.
///
- IEnumerable GetUsablePokemonNotInField();
+ IEnumerable GetUsablePokemonNotInField();
}
///
@@ -39,6 +59,8 @@ public record struct ResponsibleIndex(byte Side, byte Position);
public class BattlePartyImpl : IBattleParty
{
private readonly ResponsibleIndex[] _responsibleIndices;
+ private IBattlePokemon?[] _battlePokemon = [];
+ private IBattle? _battle;
///
public BattlePartyImpl(IPokemonParty party, ResponsibleIndex[] responsibleIndices)
@@ -50,14 +72,53 @@ public class BattlePartyImpl : IBattleParty
///
public IPokemonParty Party { get; }
+ ///
+ public IReadOnlyList BattlePokemon => _battlePokemon;
+
+ ///
+ public IBattlePokemon? GetBattlePokemon(IPokemon pokemon) =>
+ _battlePokemon.FirstOrDefault(x => x != null && (x == pokemon || x.UnderlyingPokemon == pokemon));
+
+ ///
+ public void Initialize(IBattle battle)
+ {
+ if (_battle != null)
+ throw new InvalidOperationException("This battle party has already been initialized for a battle.");
+ if (_responsibleIndices.Length == 0)
+ throw new InvalidOperationException("A battle party must be responsible for at least one position.");
+ var sideIndex = _responsibleIndices[0].Side;
+ if (_responsibleIndices.Any(x => x.Side != sideIndex))
+ throw new InvalidOperationException("A battle party can only be responsible for a single side.");
+
+ _battle = battle;
+ _battlePokemon = new IBattlePokemon?[Party.Count];
+ for (var i = 0; i < Party.Count; i++)
+ {
+ var pokemon = Party[i];
+ if (pokemon != null)
+ _battlePokemon[i] = new BattlePokemonImpl(pokemon, battle, sideIndex);
+ }
+
+ Party.OnSwapInto += (_, args) =>
+ {
+ var (pokemon, index) = args;
+ _battlePokemon[index] = pokemon == null ? null : new BattlePokemonImpl(pokemon, battle, sideIndex);
+ };
+ Party.OnSwap += (_, args) =>
+ {
+ var (index1, index2) = args;
+ (_battlePokemon[index1], _battlePokemon[index2]) = (_battlePokemon[index2], _battlePokemon[index1]);
+ };
+ }
+
///
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
///
public bool HasUsablePokemonNotInField() =>
- Party.WhereNotNull().Any(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
+ _battlePokemon.WhereNotNull().Any(x => x.IsUsable && !x.IsOnBattlefield);
///
- public IEnumerable GetUsablePokemonNotInField() =>
- Party.WhereNotNull().Where(x => x.IsUsable && x.BattleData?.IsOnBattlefield != true);
+ public IEnumerable GetUsablePokemonNotInField() =>
+ _battlePokemon.WhereNotNull().Where(x => x.IsUsable && !x.IsOnBattlefield);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/BattlePokemon.cs b/PkmnLib.Dynamic/Models/BattlePokemon.cs
new file mode 100644
index 0000000..7f4f20e
--- /dev/null
+++ b/PkmnLib.Dynamic/Models/BattlePokemon.cs
@@ -0,0 +1,1043 @@
+using System.Diagnostics.CodeAnalysis;
+using PkmnLib.Dynamic.Events;
+using PkmnLib.Dynamic.Libraries;
+using PkmnLib.Dynamic.Models.Choices;
+using PkmnLib.Dynamic.Models.Serialized;
+using PkmnLib.Dynamic.ScriptHandling;
+using PkmnLib.Static;
+using PkmnLib.Static.Species;
+using PkmnLib.Static.Utils;
+
+namespace PkmnLib.Dynamic.Models;
+
+///
+/// A Pokémon taking part in a battle. This wraps an and holds all state that is only
+/// relevant for the duration of a single battle. The wrapper is ephemeral: it is created when the battle is
+/// created, and simply dropped when the battle ends, taking all battle-only state with it. Persistent data is
+/// proxied to the underlying .
+///
+public interface IBattlePokemon : IPokemon
+{
+ ///
+ /// The persistent Pokémon this battle Pokémon wraps.
+ ///
+ IPokemon UnderlyingPokemon { get; }
+
+ ///
+ /// The battle the Pokémon is in.
+ ///
+ IBattle Battle { get; }
+
+ ///
+ /// The index of the side of the Pokémon.
+ ///
+ byte SideIndex { get; }
+
+ ///
+ /// The index of the position of the Pokémon on the field.
+ ///
+ byte Position { get; }
+
+ ///
+ /// The side the Pokémon is on.
+ ///
+ IBattleSide BattleSide { get; }
+
+ ///
+ /// Whether the Pokémon is on the battlefield.
+ ///
+ bool IsOnBattlefield { get; }
+
+ ///
+ /// A list of opponents the Pokémon has seen this battle.
+ ///
+ IReadOnlyList SeenOpponents { get; }
+
+ ///
+ /// Adds an opponent to the list of seen opponents.
+ ///
+ void MarkOpponentAsSeen(IBattlePokemon opponent);
+
+ ///
+ /// A list of items the Pokémon has consumed this battle.
+ ///
+ IReadOnlyList ConsumedItems { get; }
+
+ ///
+ /// Marks an item as consumed.
+ ///
+ void MarkItemAsConsumed(IItem item);
+
+ ///
+ /// The turn the Pokémon last switched in.
+ ///
+ uint SwitchInTurn { get; }
+
+ ///
+ /// The number of turns the Pokémon has been on the field.
+ ///
+ uint TurnsOnField { get; }
+
+ ///
+ /// The species of the Pokémon at the time the battle started.
+ ///
+ ISpecies OriginalSpecies { get; }
+
+ ///
+ /// The form of the Pokémon at the time the battle started.
+ ///
+ IForm OriginalForm { get; }
+
+ ///
+ /// The last move choice executed by the Pokémon.
+ ///
+ IMoveChoice? LastMoveChoice { get; set; }
+
+ ///
+ /// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below -6.
+ ///
+ StatBoostStatisticSet StatBoost { get; }
+
+ ///
+ /// The stats of the Pokemon including the stat boosts.
+ ///
+ StatisticSet BoostedStats { get; }
+
+ ///
+ /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats.
+ /// This should be called when a stat boost changes.
+ ///
+ void RecalculateBoostedStats();
+
+ ///
+ /// Change a boosted stat by a certain amount.
+ ///
+ /// The stat to be changed
+ /// The amount to change the stat by
+ /// Whether the change was self-inflicted. This can be relevant in scripts.
+ /// Whether to skip the script hooks that can prevent or change the boost
+ /// The event batch ID this change is a part of. This is relevant for visual handling
+ bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default);
+
+ ///
+ /// The volatile status scripts of the Pokemon.
+ ///
+ IScriptSet Volatile { get; }
+
+ ///
+ /// The script for the ability.
+ ///
+ ScriptContainer AbilityScript { get; }
+
+ ///
+ /// The script for the held item.
+ ///
+ ScriptContainer HeldItemTriggerScript { get; }
+
+ ///
+ /// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy
+ /// ability.
+ ///
+ IAbility? OverrideAbility { get; }
+
+ ///
+ /// Changes the ability of the Pokémon.
+ ///
+ bool ChangeAbility(IAbility ability);
+
+ ///
+ /// Whether the ability of the Pokémon is suppressed.
+ ///
+ bool AbilitySuppressed { get; }
+
+ ///
+ /// Suppresses the ability of the Pokémon.
+ ///
+ bool SuppressAbility();
+
+ ///
+ /// Returns the currently active ability, taking suppression and overrides into account.
+ ///
+ IAbility? ActiveAbility { get; }
+
+ ///
+ /// An optional display species of the Pokemon. If this is set, the client should display this
+ /// species. An example of usage for this is the Illusion ability.
+ ///
+ ISpecies? DisplaySpecies { get; }
+
+ ///
+ /// An optional display form of the Pokemon. If this is set, the client should display this
+ /// form. An example of usage for this is the Illusion ability.
+ ///
+ IForm? DisplayForm { get; }
+
+ ///
+ /// Sets the display species and form of the Pokemon. This is used for abilities like Illusion.
+ ///
+ void SetDisplaySpecies(ISpecies? species, IForm? form);
+
+ ///
+ /// The height of the Pokémon in meters. This can be changed during battle by effects such as Autotomize.
+ ///
+ new float HeightInMeters { get; set; }
+
+ ///
+ /// Removes a type from the Pokémon. Returns whether the type was removed.
+ ///
+ bool RemoveType(TypeIdentifier type);
+
+ ///
+ /// Adds a type to the Pokémon. Returns whether the type was added. It will not add the type if
+ /// the Pokémon already has it.
+ ///
+ bool AddType(TypeIdentifier type);
+
+ ///
+ /// Replace the types of the Pokémon with the provided types.
+ ///
+ void SetTypes(IReadOnlyList types);
+
+ ///
+ /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as
+ /// Levitate.
+ ///
+ bool IsFloating { get; }
+
+ ///
+ /// The permanently learned moves of the Pokemon, ignoring any temporary replacements made through
+ /// . This is of a set length of . Empty move
+ /// slots are null.
+ ///
+ IReadOnlyList BaseMoves { get; }
+
+ ///
+ /// Temporarily replaces the move in the given slot until the Pokemon leaves the battlefield. The permanently
+ /// learned move in the slot is left untouched and becomes visible again automatically. Used by effects such as
+ /// Mimic.
+ ///
+ /// Thrown when the move is not found in the move library.
+ void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index);
+
+ ///
+ /// Whether or not this Pokemon was caught this battle.
+ ///
+ bool IsCaught { get; }
+
+ ///
+ /// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore.
+ ///
+ void MarkAsCaught();
+
+ ///
+ /// Whether the held item has been removed for the duration of the battle.
+ ///
+ bool HasItemBeenRemovedForBattle { get; }
+
+ ///
+ /// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item.
+ ///
+ ///
+ /// This is used for moves that remove a held item, but do not consume it. The held item of the underlying
+ /// Pokémon is never touched; the removal simply ends with the battle.
+ ///
+ IItem? RemoveHeldItemForBattle();
+
+ ///
+ /// Tries to steal the held item of the Pokémon. If successful, the item is removed from the Pokémon and returned.
+ /// If the Pokémon does not have a held item, or the item is a form changer, this will return false.
+ ///
+ bool TryStealHeldItem([NotNullWhen(true)] out IItem? item);
+
+ ///
+ /// Restores the held item of a Pokémon if it was temporarily removed.
+ ///
+ void RestoreRemovedHeldItem();
+
+ ///
+ /// Makes the Pokemon uses its held item. Returns whether the item was consumed.
+ ///
+ bool ConsumeHeldItem();
+
+ ///
+ /// Called by the battle when the Pokémon is sent onto the battlefield. This should not be called by
+ /// user code; use instead.
+ ///
+ void OnSwitchedIn(byte position);
+
+ ///
+ /// Called by the battle when the Pokémon leaves the battlefield. Resets all state that only lasts while
+ /// the Pokémon is on the field. This should not be called by user code.
+ ///
+ void OnSwitchedOut();
+
+ ///
+ /// Sets the position the Pokémon has within its side, without running switch handling.
+ ///
+ void SetPosition(byte position);
+
+ ///
+ /// Called when the battle has ended. Reverts the few battle effects that write through to the underlying
+ /// Pokémon, such as battle-only forms.
+ ///
+ void OnBattleEnd();
+}
+
+///
+public class BattlePokemonImpl : ScriptSource, IBattlePokemon
+{
+ private readonly IPokemon _pokemon;
+
+ ///
+ public BattlePokemonImpl(IPokemon pokemon, IBattle battle, byte sideIndex)
+ {
+ _pokemon = pokemon;
+ Battle = battle;
+ SideIndex = sideIndex;
+ OriginalSpecies = pokemon.Species;
+ OriginalForm = pokemon.Form;
+ Volatile = new ScriptSet(this);
+ _types = pokemon.Types.ToList();
+ HeightInMeters = pokemon.HeightInMeters;
+ _heldItem = pokemon.HeldItem;
+ RecalculateBoostedStats();
+ // A status that was set outside of the battle is parented to the underlying Pokémon. Re-parent it to
+ // this wrapper, so that it has battle context for the duration of the battle.
+ pokemon.StatusScript.Script?.OnAddedToParent(this);
+ }
+
+ ///
+ public IPokemon UnderlyingPokemon => _pokemon;
+
+ ///
+ public IBattle Battle { get; }
+
+ ///
+ public byte SideIndex { get; }
+
+ ///
+ public byte Position { get; private set; }
+
+ ///
+ public IBattleSide BattleSide => Battle.Sides[SideIndex];
+
+ ///
+ public bool IsOnBattlefield { get; private set; }
+
+ private readonly List _seenOpponents = [];
+
+ ///
+ public IReadOnlyList SeenOpponents => _seenOpponents;
+
+ ///
+ public void MarkOpponentAsSeen(IBattlePokemon opponent)
+ {
+ if (!_seenOpponents.Contains(opponent))
+ _seenOpponents.Add(opponent);
+ }
+
+ private readonly List _consumedItems = [];
+
+ ///
+ public IReadOnlyList ConsumedItems => _consumedItems;
+
+ ///
+ public void MarkItemAsConsumed(IItem item)
+ {
+ _consumedItems.Add(item);
+ BattleSide.SetConsumedItem(Position, item);
+ }
+
+ ///
+ public uint SwitchInTurn { get; private set; }
+
+ ///
+ public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn;
+
+ ///
+ public ISpecies OriginalSpecies { get; }
+
+ ///
+ public IForm OriginalForm { get; }
+
+ ///
+ public IMoveChoice? LastMoveChoice { get; set; }
+
+ ///
+ public void OnSwitchedIn(byte position)
+ {
+ Position = position;
+ SwitchInTurn = Battle.CurrentTurnNumber;
+ IsOnBattlefield = true;
+ ResolveAbilityScript();
+ }
+
+ ///
+ public void OnSwitchedOut()
+ {
+ IsOnBattlefield = false;
+ Volatile.Clear();
+ _temporaryMoves = null;
+ HeightInMeters = _pokemon.Form.Height;
+ _types = _pokemon.Form.Types.ToList();
+ OverrideAbility = null;
+ AbilitySuppressed = false;
+ StatBoost.Reset();
+ RecalculateBoostedStats();
+ }
+
+ ///
+ public void SetPosition(byte position) => Position = position;
+
+ ///
+ public void OnBattleEnd()
+ {
+ if (_pokemon.Form.IsBattleOnlyForm)
+ {
+ _pokemon.ChangeForm(OriginalSpecies == _pokemon.Species ? OriginalForm : _pokemon.Species.GetDefaultForm());
+ }
+ }
+
+ private void ResolveAbilityScript()
+ {
+ var ability = ActiveAbility;
+ if (ability != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name,
+ ability.Parameters, out var abilityScript))
+ {
+ AbilityScript.Set(abilityScript);
+ abilityScript.OnAddedToParent(this);
+ }
+ else
+ {
+ AbilityScript.Clear();
+ }
+ }
+
+ ///
+ public IDynamicLibrary Library => _pokemon.Library;
+
+ ///
+ public ISpecies Species => _pokemon.Species;
+
+ ///
+ public IForm Form => _pokemon.Form;
+
+ ///
+ public LevelInt Level => _pokemon.Level;
+
+ ///
+ public uint Experience => _pokemon.Experience;
+
+ ///
+ public uint PersonalityValue => _pokemon.PersonalityValue;
+
+ ///
+ public Gender Gender => _pokemon.Gender;
+
+ ///
+ public byte Coloring => _pokemon.Coloring;
+
+ ///
+ public bool IsShiny => _pokemon.IsShiny;
+
+ ///
+ public uint CurrentHealth => _pokemon.CurrentHealth;
+
+ ///
+ public byte Happiness
+ {
+ get => _pokemon.Happiness;
+ set => _pokemon.Happiness = value;
+ }
+
+ ///
+ public StatisticSet FlatStats => _pokemon.FlatStats;
+
+ ///
+ public IndividualValueStatisticSet IndividualValues => _pokemon.IndividualValues;
+
+ ///
+ public EffortValueStatisticSet EffortValues => _pokemon.EffortValues;
+
+ ///
+ public INature Nature => _pokemon.Nature;
+
+ ///
+ public string? Nickname => _pokemon.Nickname;
+
+ ///
+ public AbilityIndex AbilityIndex => _pokemon.AbilityIndex;
+
+ ///
+ public IAbility Ability => _pokemon.Ability;
+
+ ///
+ public bool AllowedExperience => _pokemon.AllowedExperience;
+
+ ///
+ public bool IsEgg => _pokemon.IsEgg;
+
+ ///
+ public ScriptContainer StatusScript => _pokemon.StatusScript;
+
+ ///
+ public int? GetStatusTurnsLeft => _pokemon.GetStatusTurnsLeft;
+
+ ///
+ public bool HasStatus(StringKey status) => _pokemon.HasStatus(status);
+
+ ///
+ public bool IsFainted => _pokemon.IsFainted;
+
+ ///
+ public SerializedPokemon Serialize() => _pokemon.Serialize();
+
+ ///
+ public bool AddExperience(uint experience, EventHook? eventHook = null) =>
+ _pokemon.AddExperience(experience, eventHook ?? Battle.EventHook);
+
+ ///
+ public void RestoreAllPP()
+ {
+ foreach (var move in Moves)
+ {
+ move?.RestoreAllUses();
+ }
+ }
+
+ ///
+ public void LearnMove(StringKey moveName, MoveLearnMethod method, byte index) =>
+ _pokemon.LearnMove(moveName, method, index);
+
+ ///
+ public void ChangeLevelBy(int change)
+ {
+ _pokemon.ChangeLevelBy(change);
+ RecalculateBoostedStats();
+ }
+
+ ///
+ public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null) =>
+ _pokemon.EvolveTo(evolution, eventHook ?? Battle.EventHook);
+
+ ///
+ public StatBoostStatisticSet StatBoost { get; } = new();
+
+ ///
+ public StatisticSet BoostedStats { get; } = new();
+
+ ///
+ public uint MaxHealth => BoostedStats.Hp;
+
+ ///
+ public void RecalculateFlatStats()
+ {
+ _pokemon.RecalculateFlatStats();
+ RecalculateBoostedStats();
+ }
+
+ ///
+ public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
+
+ ///
+ public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force,
+ EventBatchId batchId = default)
+ {
+ if (!force)
+ {
+ var prevented = false;
+ this.RunScriptHook(script =>
+ script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
+ if (prevented)
+ return false;
+ this.RunScriptHook(script =>
+ script.ChangeStatBoostChange(this, stat, selfInflicted, ref change));
+ if (change == 0)
+ return false;
+ }
+ var changed = false;
+ var oldBoost = StatBoost.GetStatistic(stat);
+ changed = change switch
+ {
+ > 0 => StatBoost.IncreaseStatistic(stat, change),
+ < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change),
+ _ => changed,
+ };
+ if (!changed)
+ return false;
+ var newBoost = StatBoost.GetStatistic(stat);
+ Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost)
+ {
+ BatchId = batchId,
+ });
+
+ RecalculateBoostedStats();
+ this.RunScriptHook(script =>
+ script.OnAfterStatBoostChange(this, stat, selfInflicted, change));
+ return true;
+ }
+
+ ///
+ public ISpecies? DisplaySpecies { get; private set; }
+
+ ///
+ public IForm? DisplayForm { get; private set; }
+
+ ///
+ public void SetDisplaySpecies(ISpecies? species, IForm? form)
+ {
+ DisplaySpecies = species;
+ DisplayForm = form;
+
+ Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form)
+ {
+ BatchId = new EventBatchId(),
+ });
+ }
+
+ ///
+ public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null)
+ {
+ var oldAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex);
+ _pokemon.ChangeSpecies(species, form, eventHook ?? Battle.EventHook, random ?? Battle.Random);
+ OnAfterFormWriteThrough(oldAbilityName);
+ }
+
+ ///
+ public void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null)
+ {
+ if (form == Form)
+ return;
+ var oldAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex);
+ _pokemon.ChangeForm(form, batchId, eventHook ?? Battle.EventHook);
+ OnAfterFormWriteThrough(oldAbilityName);
+ }
+
+ ///
+ /// A form or species change writes through to the underlying Pokémon; the battle overlays that derive
+ /// from the form need to be re-initialized from the new form.
+ ///
+ private void OnAfterFormWriteThrough(StringKey oldAbilityName)
+ {
+ _types = _pokemon.Form.Types.ToList();
+ HeightInMeters = _pokemon.Form.Height;
+ var newAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex);
+ if (OverrideAbility == null && !AbilitySuppressed && oldAbilityName != newAbilityName)
+ ResolveAbilityScript();
+ RecalculateBoostedStats();
+ }
+
+ private List _types;
+
+ ///
+ public IReadOnlyList Types => _types;
+
+ ///
+ public bool RemoveType(TypeIdentifier type) => _types.Remove(type);
+
+ ///
+ public bool AddType(TypeIdentifier type)
+ {
+ if (_types.Contains(type))
+ return false;
+ _types.Add(type);
+ return true;
+ }
+
+ ///
+ public void SetTypes(IReadOnlyList types) => _types = types.ToList();
+
+ ///
+ public float HeightInMeters { get; set; }
+
+ ///
+ public float WeightInKg
+ {
+ get
+ {
+ var weight = _pokemon.WeightInKg;
+ // ReSharper disable once AccessToModifiedClosure
+ this.RunScriptHook(script => script.ModifyWeight(ref weight));
+ if (weight < 0.1f)
+ weight = 0.1f;
+ return weight;
+ }
+ }
+
+ private static readonly StringKey FlyingTypeName = "flying";
+
+ ///
+ public bool IsFloating
+ {
+ get
+ {
+ var isFloating = Types.Any(x => x.Name == FlyingTypeName);
+ this.RunScriptHook(x => x.IsFloating(this, ref isFloating));
+ return isFloating;
+ }
+ }
+
+ ///
+ public IAbility? OverrideAbility { get; private set; }
+
+ ///
+ public bool AbilitySuppressed { get; private set; }
+
+ ///
+ public IAbility? ActiveAbility
+ {
+ get
+ {
+ if (AbilitySuppressed)
+ return null;
+ if (OverrideAbility != null)
+ return OverrideAbility;
+ return _pokemon.Ability;
+ }
+ }
+
+ ///
+ public bool SuppressAbility()
+ {
+ if (ActiveAbility?.CanBeChanged == false)
+ return false;
+
+ AbilitySuppressed = true;
+ AbilityScript.Clear();
+ return true;
+ }
+
+ ///
+ public bool ChangeAbility(IAbility ability)
+ {
+ if (!ability.CanBeChanged)
+ return false;
+ OverrideAbility = ability;
+ if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters,
+ out var abilityScript))
+ {
+ AbilityScript.Set(abilityScript);
+ abilityScript.OnAddedToParent(this);
+ }
+ else
+ {
+ AbilityScript.Clear();
+ }
+ return true;
+ }
+
+ ///
+ /// Battle-only per-slot overrides of the underlying moveset. The permanent moveset is never mutated by
+ /// temporary moves; discarding this array is all that is needed to restore the original moves.
+ ///
+ private ILearnedMove?[]? _temporaryMoves;
+
+ ///
+ public IReadOnlyList Moves =>
+ _temporaryMoves?.Select((move, index) => move ?? BaseMoves[index]).ToArray() ?? BaseMoves;
+
+ ///
+ public IReadOnlyList BaseMoves => _pokemon.Moves;
+
+ ///
+ public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName);
+
+ ///
+ public void SwapMoves(byte index1, byte index2)
+ {
+ if (index1 >= Const.MovesCount || index2 >= Const.MovesCount)
+ return;
+ _pokemon.SwapMoves(index1, index2);
+ if (_temporaryMoves != null)
+ (_temporaryMoves[index1], _temporaryMoves[index2]) = (_temporaryMoves[index2], _temporaryMoves[index1]);
+ }
+
+ ///
+ public void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index)
+ {
+ if (index >= Const.MovesCount)
+ throw new ArgumentOutOfRangeException(nameof(index), $"Move slot {index} is out of range.");
+ if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
+ throw new KeyNotFoundException($"Move {moveName} not found.");
+
+ _temporaryMoves ??= new ILearnedMove?[Const.MovesCount];
+ _temporaryMoves[index] = new LearnedMoveImpl(move, method);
+ }
+
+ private IItem? _heldItem;
+
+ ///
+ public IItem? HeldItem => _heldItem;
+
+ ///
+ public bool HasHeldItem(StringKey itemName) => _heldItem?.Name == itemName;
+
+ ///
+ public IItem? ForceSetHeldItem(IItem? item)
+ {
+ var previous = _heldItem;
+ _heldItem = item;
+ this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item));
+ return previous;
+ }
+
+ ///
+ public IItem? RemoveHeldItem()
+ {
+ if (_heldItem is not null && _heldItem.Category == ItemCategory.FormChanger)
+ return null;
+ var previous = _heldItem;
+ _heldItem = null;
+ this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null));
+ return previous;
+ }
+
+ private IItem? _removedHeldItem;
+
+ ///
+ public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null;
+
+ ///
+ public IItem? RemoveHeldItemForBattle() => _removedHeldItem = RemoveHeldItem();
+
+ ///
+ public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item)
+ {
+ if (_heldItem is null || _heldItem.Category == ItemCategory.FormChanger)
+ {
+ item = null;
+ return false;
+ }
+ var prevent = false;
+ this.RunScriptHook(script =>
+ script.PreventHeldItemSteal(this, _heldItem, ref prevent));
+ if (prevent)
+ {
+ item = null;
+ return false;
+ }
+ item = RemoveHeldItemForBattle();
+ return item is not null;
+ }
+
+ ///
+ public void RestoreRemovedHeldItem()
+ {
+ _ = ForceSetHeldItem(_removedHeldItem);
+ _removedHeldItem = null;
+ }
+
+ ///
+ public bool ConsumeHeldItem()
+ {
+ if (_heldItem is null)
+ return false;
+ if (!Library.ScriptResolver.TryResolveBattleItemScript(_heldItem, out _))
+ return false;
+
+ var prevented = false;
+ this.RunScriptHook(script =>
+ script.PreventHeldItemConsume(this, _heldItem, ref prevented));
+ if (prevented)
+ return false;
+ MarkItemAsConsumed(_heldItem);
+
+ UseItem(ForceSetHeldItem(null)!);
+ return true;
+ }
+
+ ///
+ public void UseItem(IItem item)
+ {
+ // TODO: actually consume the item
+
+ this.RunScriptHook(x => x.OnAfterItemConsume(this, item));
+ }
+
+ ///
+ public bool IsCaught { get; private set; }
+
+ ///
+ public void MarkAsCaught()
+ {
+ IsCaught = true;
+ }
+
+ ///
+ public bool IsUsable => !IsCaught && _pokemon.IsUsable;
+
+ ///
+ public void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false)
+ {
+ if (IsFainted)
+ return;
+ if (!forceDamage)
+ {
+ var dmg = damage;
+ this.RunScriptHook(script =>
+ script.ChangeIncomingDamage(this, source, ref dmg));
+ damage = dmg;
+ }
+ if (damage == 0)
+ return;
+
+ // If the damage is more than the current health, we cap it at the current health, to prevent
+ // underflow.
+ if (damage >= CurrentHealth)
+ damage = CurrentHealth;
+ var newHealth = CurrentHealth - damage;
+ // Trigger an event to the front-end.
+ Battle.EventHook.Invoke(new DamageEvent(this, CurrentHealth, newHealth, source)
+ {
+ BatchId = batchId,
+ });
+ // And allow scripts to execute.
+ this.RunScriptHook(script => script.OnDamage(this, source, CurrentHealth, newHealth));
+
+ _pokemon.Damage(damage, source, batchId, true);
+
+ // If the Pokémon is now fainted, we also run faint handling.
+ if (IsFainted)
+ {
+ OnFaint(source);
+ }
+ }
+
+ ///
+ public void Faint(DamageSource source, EventBatchId batchId = default)
+ {
+ _pokemon.Faint(source, batchId);
+ OnFaint(source);
+ }
+
+ private void OnFaint(DamageSource source)
+ {
+ // Trigger the faint event to the front-end.
+ Battle.EventHook.Invoke(new FaintEvent(this));
+
+ // Allow scripts to trigger based on the faint.
+ this.RunScriptHook(script => script.OnFaint(this, source));
+ foreach (var ally in BattleSide.Pokemon.WhereNotNull().Where(x => x != this))
+ {
+ ally.RunScriptHook(script => script.OnAllyFaint(ally, this));
+ }
+
+ // Make sure the OnRemove script is run.
+ this.RunScriptHook(script => script.OnRemove());
+
+ // Mark the position as unfillable if it can't be filled by any party.
+ if (!Battle.CanSlotBeFilled(SideIndex, Position))
+ {
+ BattleSide.MarkPositionAsUnfillable(Position);
+ }
+ BattleSide.MarkFaint(Position);
+ BattleSide.ForceClearPokemonFromField(Position);
+
+ foreach (var opponent in SeenOpponents)
+ {
+ if (!opponent.IsUsable)
+ continue;
+ if (!opponent.AllowedExperience)
+ continue;
+ opponent.AddExperience(Library.ExperienceGainCalculator.CalculateExperienceGain(this, opponent));
+ }
+
+ // Validate the battle state to see if the battle is over.
+ Battle.ValidateBattleState();
+ }
+
+ ///
+ public bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false,
+ EventHook? customEventHook = null)
+ {
+ if (IsFainted && !allowRevive)
+ return false;
+
+ var maxAmount = BoostedStats.Hp - CurrentHealth;
+ if (heal > maxAmount)
+ heal = maxAmount;
+ if (heal == 0)
+ return false;
+ if (!forceHeal)
+ {
+ var prevented = false;
+ this.RunScriptHook(x => x.PreventHeal(this, heal, allowRevive, ref prevented));
+ if (prevented)
+ return false;
+ }
+
+ customEventHook ??= Battle.EventHook;
+ return _pokemon.Heal(heal, allowRevive, batchId, true, customEventHook);
+ }
+
+ ///
+ public bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default)
+ {
+ if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript))
+ throw new KeyNotFoundException($"Status script {status} not found");
+
+ if (!StatusScript.IsEmpty)
+ return false;
+ var oldStatus = StatusScript.Script?.Name;
+ var selfInflicted = originPokemon == this ||
+ (originPokemon is IBattlePokemon origin && origin.UnderlyingPokemon == _pokemon);
+
+ var preventStatus = false;
+ this.RunScriptHook(script =>
+ script.PreventStatusChange(this, status, selfInflicted, ref preventStatus));
+ if (preventStatus)
+ return false;
+
+ StatusScript.Set(statusScript);
+ statusScript.OnAddedToParent(this);
+ Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status)
+ {
+ BatchId = batchId,
+ });
+ this.RunScriptHook(script =>
+ script.OnAfterStatusChange(this, status, originPokemon));
+ return true;
+ }
+
+ ///
+ public void ClearStatus(EventBatchId batchId = default)
+ {
+ var oldStatus = StatusScript.Script?.Name;
+ _pokemon.ClearStatus(batchId);
+ Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, null)
+ {
+ BatchId = batchId,
+ });
+ }
+
+ ///
+ public ScriptContainer HeldItemTriggerScript { get; } = new();
+
+ ///
+ public ScriptContainer AbilityScript { get; } = new();
+
+ ///
+ public IScriptSet Volatile { get; }
+
+ ///
+ public override int ScriptCount => 4 + BattleSide.ScriptCount;
+
+ ///
+ public override void GetOwnScripts(List> scripts)
+ {
+ scripts.Add(HeldItemTriggerScript);
+ scripts.Add(AbilityScript);
+ scripts.Add(StatusScript);
+ scripts.Add(Volatile);
+ }
+
+ ///
+ public override void CollectScripts(List> scripts)
+ {
+ GetOwnScripts(scripts);
+ BattleSide.CollectScripts(scripts);
+ }
+
+ ///
+ public override string ToString() => _pokemon.ToString()!;
+}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/BattleRandom.cs b/PkmnLib.Dynamic/Models/BattleRandom.cs
index 013ce3c..fb7fdee 100644
--- a/PkmnLib.Dynamic/Models/BattleRandom.cs
+++ b/PkmnLib.Dynamic/Models/BattleRandom.cs
@@ -13,7 +13,7 @@ public interface IBattleRandom : IRandom, IDeepCloneable
/// rolls whether it triggers. As a side effect this run scripts to allow modifying this random
/// chance.
///
- bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber);
+ bool EffectChance(float chance, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber);
}
///
@@ -36,7 +36,7 @@ public class BattleRandomImpl : RandomImpl, IBattleRandom
}
///
- public bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber)
+ public bool EffectChance(float chance, IExecutingMove executingMove, IBattlePokemon target, byte hitNumber)
{
executingMove.RunScriptHook(script =>
script.ChangeEffectChance(executingMove, target, hitNumber, ref chance));
diff --git a/PkmnLib.Dynamic/Models/BattleResult.cs b/PkmnLib.Dynamic/Models/BattleResult.cs
index 62a24ce..b76ec8a 100644
--- a/PkmnLib.Dynamic/Models/BattleResult.cs
+++ b/PkmnLib.Dynamic/Models/BattleResult.cs
@@ -30,4 +30,11 @@ public record struct BattleResult
/// The side that won the battle. If null, no side has won.
///
public byte? WinningSide { get; }
+
+ ///
+ /// The Pokémon that were captured during the battle. These are the underlying Pokémon, so they remain
+ /// valid after the battle has been disposed. This is set for both conclusive and inconclusive results, as
+ /// captures complete the moment they happen.
+ ///
+ public IReadOnlyList CapturedPokemon { get; init; } = [];
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/BattleSide.cs b/PkmnLib.Dynamic/Models/BattleSide.cs
index b6cd47b..bb486f3 100644
--- a/PkmnLib.Dynamic/Models/BattleSide.cs
+++ b/PkmnLib.Dynamic/Models/BattleSide.cs
@@ -24,7 +24,7 @@ public interface IBattleSide : IScriptSource, IDeepCloneable
///
/// A list of Pokémon currently on the battlefield.
///
- IReadOnlyList Pokemon { get; }
+ IReadOnlyList Pokemon { get; }
///
/// The currently set choices for all Pokémon on the battlefield. Cleared when the turn starts.
@@ -84,7 +84,14 @@ public interface IBattleSide : IScriptSource, IDeepCloneable
/// Switches out a spot on the field for a different Pokémon. If null is passed, the spot is
/// cleared. Returns the Pokémon that was previously in the spot.
///
- IPokemon? SwapPokemon(byte position, IPokemon? pokemon);
+ IBattlePokemon? SwapPokemon(byte position, IBattlePokemon? pokemon);
+
+ ///
+ /// Sends out a Pokémon by its persistent representation. This resolves the battle-scoped wrapper for
+ /// the Pokémon from the parties in the battle, and swaps it into the given position. This is the
+ /// convenient entry point for hosts, which generally hold the persistent party Pokémon.
+ ///
+ IBattlePokemon? SendOut(byte position, IPokemon pokemon);
///
/// Swaps two Pokémon on the side.
@@ -94,7 +101,7 @@ public interface IBattleSide : IScriptSource, IDeepCloneable
///
/// Checks whether a Pokemon is on the field in this side.
///
- bool IsPokemonOnSide(IPokemon pokemon);
+ bool IsPokemonOnSide(IBattlePokemon pokemon);
///
/// Marks a slot as unfillable. This happens when no parties are able to fill the slot anymore.
@@ -166,7 +173,7 @@ public class BattleSideImpl : ScriptSource, IBattleSide
{
Index = index;
NumberOfPositions = numberOfPositions;
- _pokemon = new IPokemon?[numberOfPositions];
+ _pokemon = new IBattlePokemon?[numberOfPositions];
_setChoices = new ITurnChoice?[numberOfPositions];
_fillablePositions = new bool[numberOfPositions];
for (byte i = 0; i < numberOfPositions; i++)
@@ -183,10 +190,10 @@ public class BattleSideImpl : ScriptSource, IBattleSide
///
public byte NumberOfPositions { get; }
- private readonly IPokemon?[] _pokemon;
+ private readonly IBattlePokemon?[] _pokemon;
///
- public IReadOnlyList Pokemon => _pokemon;
+ public IReadOnlyList Pokemon => _pokemon;
private readonly ITurnChoice?[] _setChoices;
@@ -249,28 +256,31 @@ public class BattleSideImpl : ScriptSource, IBattleSide
if (pokemon is not null)
{
pokemon.RunScriptHook(script => script.OnRemove());
- pokemon.SetOnBattlefield(false);
+ pokemon.OnSwitchedOut();
}
_pokemon[index] = null;
}
///
- public IPokemon? SwapPokemon(byte position, IPokemon? pokemon)
+ public IBattlePokemon? SwapPokemon(byte position, IBattlePokemon? pokemon)
{
var oldPokemon = _pokemon[position];
if (oldPokemon is not null)
{
oldPokemon.RunScriptHook(script => script.OnSwitchOut(oldPokemon, position));
oldPokemon.RunScriptHook(script => script.OnRemove());
- oldPokemon.SetOnBattlefield(false);
+ oldPokemon.OnSwitchedOut();
}
_pokemon[position] = pokemon;
if (pokemon is not null)
{
- pokemon.SetBattleData(Battle, Index);
- pokemon.SetOnBattlefield(true);
- pokemon.SetBattleSidePosition(position);
+ if (pokemon.SideIndex != Index)
+ {
+ throw new InvalidOperationException(
+ "A battle Pokémon can only be sent out on the side its party is responsible for.");
+ }
+ pokemon.OnSwitchedIn(position);
Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon));
pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position));
@@ -300,6 +310,14 @@ public class BattleSideImpl : ScriptSource, IBattleSide
return oldPokemon;
}
+ ///
+ public IBattlePokemon? SendOut(byte position, IPokemon pokemon)
+ {
+ var wrapper = Battle.Parties.Select(p => p.GetBattlePokemon(pokemon)).FirstOrDefault(x => x != null) ??
+ throw new InvalidOperationException("The Pokémon does not belong to any party in this battle.");
+ return SwapPokemon(position, wrapper);
+ }
+
///
public void SwapPokemon(byte position1, byte position2)
{
@@ -307,7 +325,7 @@ public class BattleSideImpl : ScriptSource, IBattleSide
}
///
- public bool IsPokemonOnSide(IPokemon pokemon) => _pokemon.Contains(pokemon);
+ public bool IsPokemonOnSide(IBattlePokemon pokemon) => _pokemon.Contains(pokemon);
///
public void MarkPositionAsUnfillable(byte position) => _fillablePositions[position] = false;
diff --git a/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs b/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs
index 4595e47..ef6caa5 100644
--- a/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/FleeChoice.cs
@@ -13,7 +13,7 @@ public interface IFleeChoice : ITurnChoice
public class FleeTurnChoice : TurnChoice, IFleeChoice
{
///
- public FleeTurnChoice(IPokemon user) : base(user)
+ public FleeTurnChoice(IBattlePokemon user) : base(user)
{
}
diff --git a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs
index 4396cb7..ff7d4ad 100644
--- a/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/ItemChoice.cs
@@ -16,15 +16,15 @@ public interface IItemChoice : ITurnChoice
///
/// The target Pokémon of the item, if any.
///
- IPokemon? GetTargetPokemon(IBattle battle);
+ IBattlePokemon? GetTargetPokemon(IBattle battle);
}
///
public class ItemChoice : TurnChoice, IItemChoice
{
///
- private ItemChoice(IPokemon user, IItem item, byte? targetSide, byte? targetPosition, IPokemon? targetPokemon) :
- base(user)
+ private ItemChoice(IBattlePokemon user, IItem item, byte? targetSide, byte? targetPosition,
+ IBattlePokemon? targetPokemon) : base(user)
{
Item = item;
TargetSide = targetSide;
@@ -32,13 +32,13 @@ public class ItemChoice : TurnChoice, IItemChoice
TargetPokemon = targetPokemon;
}
- public static ItemChoice CreateWithoutTarget(IPokemon user, IItem item) =>
+ public static ItemChoice CreateWithoutTarget(IBattlePokemon user, IItem item) =>
new(user, item, null, null, null);
- public static ItemChoice CreateForOpponent(IPokemon user, IItem item, byte targetSide, byte targetPosition) =>
+ public static ItemChoice CreateForOpponent(IBattlePokemon user, IItem item, byte targetSide, byte targetPosition) =>
new(user, item, targetSide, targetPosition, null);
- public static ItemChoice CreateForPartyMember(IPokemon user, IItem item, IPokemon targetPokemon) =>
+ public static ItemChoice CreateForPartyMember(IBattlePokemon user, IItem item, IBattlePokemon targetPokemon) =>
new(user, item, null, null, targetPokemon);
///
@@ -47,7 +47,7 @@ public class ItemChoice : TurnChoice, IItemChoice
public IItem Item { get; }
///
- public IPokemon? GetTargetPokemon(IBattle battle)
+ public IBattlePokemon? GetTargetPokemon(IBattle battle)
{
if (TargetPokemon != null)
return TargetPokemon;
@@ -71,7 +71,7 @@ public class ItemChoice : TurnChoice, IItemChoice
///
/// The target Pokémon of the item, if any. This is used for party members.
///
- private IPokemon? TargetPokemon { get; }
+ private IBattlePokemon? TargetPokemon { get; }
///
public override int ScriptCount => User.ScriptCount;
diff --git a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs
index b756d3d..e07b64b 100644
--- a/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/MoveChoice.cs
@@ -51,7 +51,7 @@ public interface IMoveChoice : ITurnChoice
public class MoveChoice : TurnChoice, IMoveChoice
{
///
- public MoveChoice(IPokemon user, ILearnedMove usedMove, byte targetSide, byte targetPosition) : base(user)
+ public MoveChoice(IBattlePokemon user, ILearnedMove usedMove, byte targetSide, byte targetPosition) : base(user)
{
ChosenMove = usedMove;
TargetSide = targetSide;
diff --git a/PkmnLib.Dynamic/Models/Choices/PassChoice.cs b/PkmnLib.Dynamic/Models/Choices/PassChoice.cs
index 797cdc6..81d6545 100644
--- a/PkmnLib.Dynamic/Models/Choices/PassChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/PassChoice.cs
@@ -13,7 +13,7 @@ public interface IPassChoice : ITurnChoice
public class PassChoice : TurnChoice, IPassChoice
{
///
- public PassChoice(IPokemon user) : base(user)
+ public PassChoice(IBattlePokemon user) : base(user)
{
}
diff --git a/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs b/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs
index b67e0f9..d65c6a6 100644
--- a/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/SwitchChoice.cs
@@ -10,20 +10,20 @@ public interface ISwitchChoice : ITurnChoice
///
/// The Pokémon to switch to.
///
- IPokemon SwitchTo { get; }
+ IBattlePokemon SwitchTo { get; }
}
///
public class SwitchChoice : TurnChoice, ISwitchChoice
{
///
- public SwitchChoice(IPokemon user, IPokemon switchTo) : base(user)
+ public SwitchChoice(IBattlePokemon user, IBattlePokemon switchTo) : base(user)
{
SwitchTo = switchTo;
}
///
- public IPokemon SwitchTo { get; }
+ public IBattlePokemon SwitchTo { get; }
///
public override int ScriptCount => User.ScriptCount;
diff --git a/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs b/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs
index 6be3be6..ad31eea 100644
--- a/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs
+++ b/PkmnLib.Dynamic/Models/Choices/TurnChoice.cs
@@ -11,7 +11,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable
///
/// The user of the turn choice
///
- IPokemon User { get; }
+ IBattlePokemon User { get; }
///
/// The speed of the user at the beginning of the turn.
@@ -34,7 +34,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable
/// Fails the choice. This will prevent it from executing and run a specific fail handling during
/// execution. Note that this can not be undone.
///
- public void Fail();
+ void Fail();
}
///
@@ -43,7 +43,7 @@ public interface ITurnChoice : IScriptSource, IDeepCloneable
public abstract class TurnChoice : ScriptSource, ITurnChoice
{
///
- protected TurnChoice(IPokemon user)
+ protected TurnChoice(IBattlePokemon user)
{
User = user;
}
@@ -51,7 +51,7 @@ public abstract class TurnChoice : ScriptSource, ITurnChoice
///
/// The Pokemon for which the choice is made.
///
- public IPokemon User { get; }
+ public IBattlePokemon User { get; }
///
/// The speed of the user at the beginning of the turn.
diff --git a/PkmnLib.Dynamic/Models/ExecutingMove.cs b/PkmnLib.Dynamic/Models/ExecutingMove.cs
index 2a632e5..a1b1e2b 100644
--- a/PkmnLib.Dynamic/Models/ExecutingMove.cs
+++ b/PkmnLib.Dynamic/Models/ExecutingMove.cs
@@ -129,7 +129,7 @@ public interface IExecutingMove : IScriptSource
///
/// The user of the move.
///
- IPokemon User { get; }
+ IBattlePokemon User { get; }
///
/// The move the user has actually chosen to do.
@@ -151,17 +151,17 @@ public interface IExecutingMove : IScriptSource
///
/// Gets a hit data for a target, with a specific index.
///
- IHitData GetHitData(IPokemon target, byte hit);
+ IHitData GetHitData(IBattlePokemon target, byte hit);
///
/// Checks whether a Pokémon is a target for this move.
///
- bool IsPokemonTarget(IPokemon target);
+ bool IsPokemonTarget(IBattlePokemon target);
///
/// Gets the index of the hits in this move where the hits for a specific target start.
///
- int GetTargetIndex(IPokemon target);
+ int GetTargetIndex(IBattlePokemon target);
///
/// Gets a hit based on its raw index.
@@ -171,7 +171,7 @@ public interface IExecutingMove : IScriptSource
///
/// Gets the targets of this move.
///
- IReadOnlyList Targets { get; }
+ IReadOnlyList Targets { get; }
///
/// The underlying move choice.
@@ -192,12 +192,12 @@ public interface IExecutingMove : IScriptSource
///
public class ExecutingMoveImpl : ScriptSource, IExecutingMove
{
- private readonly IReadOnlyList _targets;
+ private readonly IReadOnlyList _targets;
private readonly IHitData[] _hits;
private readonly IBattle _battle;
///
- public ExecutingMoveImpl(IReadOnlyList targets, byte numberOfHits, ILearnedMove chosenMove,
+ public ExecutingMoveImpl(IReadOnlyList targets, byte numberOfHits, ILearnedMove chosenMove,
IMoveData useMove, IMoveChoice moveChoice, IBattle battle)
{
_targets = targets;
@@ -222,7 +222,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
public byte NumberOfHits { get; }
///
- public IPokemon User => MoveChoice.User;
+ public IBattlePokemon User => MoveChoice.User;
///
public ILearnedMove ChosenMove { get; }
@@ -239,7 +239,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
public IScriptSet Volatile => MoveChoice.Volatile;
///
- public IHitData GetHitData(IPokemon target, byte hit)
+ public IHitData GetHitData(IBattlePokemon target, byte hit)
{
var targetIndex = _targets.IndexOf(target);
if (targetIndex == -1)
@@ -252,10 +252,10 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
}
///
- public bool IsPokemonTarget(IPokemon target) => _targets.Contains(target);
+ public bool IsPokemonTarget(IBattlePokemon target) => _targets.Contains(target);
///
- public int GetTargetIndex(IPokemon target)
+ public int GetTargetIndex(IBattlePokemon target)
{
var targetIndex = _targets.IndexOf(target);
if (targetIndex == -1)
@@ -273,7 +273,7 @@ public class ExecutingMoveImpl : ScriptSource, IExecutingMove
}
///
- public IReadOnlyList Targets => _targets.ToList();
+ public IReadOnlyList Targets => _targets.ToList();
///
public IMoveChoice MoveChoice { get; }
diff --git a/PkmnLib.Dynamic/Models/ItemTargetType.cs b/PkmnLib.Dynamic/Models/ItemTargetType.cs
index ac21259..424dacc 100644
--- a/PkmnLib.Dynamic/Models/ItemTargetType.cs
+++ b/PkmnLib.Dynamic/Models/ItemTargetType.cs
@@ -35,29 +35,30 @@ public static class ItemTargetTypeHelpers
///
/// Determines if the given target is valid based on the ItemTargetType.
///
- public static bool IsValidTarget(this ItemTargetType targetType, IBattle battle, IPokemon user, IPokemon target)
+ public static bool IsValidTarget(this ItemTargetType targetType, IBattle battle, IBattlePokemon user,
+ IBattlePokemon target)
{
if (targetType == ItemTargetType.None)
return true;
if (targetType.HasFlag(ItemTargetType.OwnPokemon))
{
- var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user));
- var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target));
+ var userParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(user));
+ var targetParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(target));
if (userParty is not null && targetParty is not null && userParty == targetParty)
return true;
}
if (targetType.HasFlag(ItemTargetType.AllyPokemon))
{
- if (user.BattleData?.BattleSide == target.BattleData?.BattleSide)
+ if (user.BattleSide == target.BattleSide)
return true;
}
if (targetType.HasFlag(ItemTargetType.FoePokemon))
{
- var userParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(user));
- var targetParty = battle.Parties.FirstOrDefault(x => x.Party.Contains(target));
+ var userParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(user));
+ var targetParty = battle.Parties.FirstOrDefault(x => x.BattlePokemon.Contains(target));
if (userParty is not null && targetParty is not null && userParty != targetParty)
return true;
}
diff --git a/PkmnLib.Dynamic/Models/Pokemon.cs b/PkmnLib.Dynamic/Models/Pokemon.cs
index 4a2749e..8d87462 100644
--- a/PkmnLib.Dynamic/Models/Pokemon.cs
+++ b/PkmnLib.Dynamic/Models/Pokemon.cs
@@ -1,7 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
-using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.Models.Serialized;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
@@ -11,7 +9,8 @@ using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Models;
///
-/// The data of a Pokemon.
+/// The persistent data of a Pokemon. This holds everything that outlives a battle. All state that is only
+/// relevant for the duration of a battle lives on instead.
///
public interface IPokemon : IScriptSource, IDeepCloneable
{
@@ -30,23 +29,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
IForm Form { get; }
- ///
- /// An optional display species of the Pokemon. If this is set, the client should display this
- /// species. An example of usage for this is the Illusion ability.
- ///
- ISpecies? DisplaySpecies { get; }
-
- ///
- /// An optional display form of the Pokemon. If this is set, the client should display this
- /// form. An example of usage for this is the Illusion ability.
- ///
- IForm? DisplayForm { get; }
-
- ///
- /// Sets the display species and form of the Pokemon. This is used for abilities like Illusion.
- ///
- void SetDisplaySpecies(ISpecies? species, IForm? form);
-
///
/// The current level of the Pokemon.
///
@@ -58,9 +40,10 @@ public interface IPokemon : IScriptSource, IDeepCloneable
uint Experience { get; }
///
- /// Increases the experience of the Pokemon. Returns whether any experience was gained.
+ /// Increases the experience of the Pokemon. Returns whether any experience was gained. If an event hook is
+ /// passed, experience and level up events are sent to it.
///
- bool AddExperience(uint experience);
+ bool AddExperience(uint experience, EventHook? eventHook = null);
///
/// The personality value of the Pokemon.
@@ -101,7 +84,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
/// The height of the Pokémon in meters.
///
- float HeightInMeters { get; set; }
+ float HeightInMeters { get; }
///
/// The happiness of the Pokemon. Also known as friendship.
@@ -113,17 +96,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
StatisticSet FlatStats { get; }
- ///
- /// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below
- /// -6.
- ///
- StatBoostStatisticSet StatBoost { get; }
-
- ///
- /// The stats of the Pokemon including the stat boosts
- ///
- StatisticSet BoostedStats { get; }
-
///
/// The maximum health of the Pokemon.
///
@@ -155,32 +127,18 @@ public interface IPokemon : IScriptSource, IDeepCloneable
AbilityIndex AbilityIndex { get; }
///
- /// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy
- /// ability.
+ /// The ability of the Pokemon, as determined by its form and ability index.
///
- IAbility? OverrideAbility { get; }
+ IAbility Ability { get; }
///
- /// If in battle, we have additional data.
- ///
- IPokemonBattleData? BattleData { get; }
-
- ///
- /// The moves the Pokemon has learned. This is of a set length of . Empty move slots
- /// are null. If a move slot is temporarily replaced (see ), this shows the
- /// replacement move.
+ /// The moves the Pokemon has learned. This is of a set length of . Empty move
+ /// slots are null.
///
IReadOnlyList Moves { get; }
///
- /// The permanently learned moves of the Pokemon, ignoring any temporary replacements made through
- /// . This is of a set length of . Empty move
- /// slots are null.
- ///
- IReadOnlyList BaseMoves { get; }
-
- ///
- /// Checks whether the Pokemon has a specific move in its current moveset.
+ /// Checks whether the Pokemon has a specific move in its moveset.
///
bool HasMove(StringKey moveName);
@@ -189,21 +147,13 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
void SwapMoves(byte index1, byte index2);
- ///
- /// Temporarily replaces the move in the given slot until the Pokemon leaves the battlefield. The permanently
- /// learned move in the slot is left untouched and becomes visible again automatically. Used by effects such as
- /// Mimic.
- ///
- /// Thrown when the move is not found in the move library.
- void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index);
-
///
/// Whether or not the Pokemon is allowed to gain experience.
///
bool AllowedExperience { get; }
///
- /// The current types of the Pokemon.
+ /// The types of the Pokemon, as determined by its form.
///
IReadOnlyList Types { get; }
@@ -212,26 +162,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
bool IsEgg { get; }
- ///
- /// Whether or not this Pokemon was caught this battle.
- ///
- bool IsCaught { get; }
-
- ///
- /// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore.
- ///
- void MarkAsCaught();
-
- ///
- /// The script for the held item.
- ///
- ScriptContainer HeldItemTriggerScript { get; }
-
- ///
- /// The script for the ability.
- ///
- ScriptContainer AbilityScript { get; }
-
///
/// The script for the status.
///
@@ -242,11 +172,6 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
int? GetStatusTurnsLeft { get; }
- ///
- /// The volatile status scripts of the Pokemon.
- ///
- IScriptSet Volatile { get; }
-
///
/// Checks whether the Pokemon is holding an item with a specific name.
///
@@ -264,74 +189,17 @@ public interface IPokemon : IScriptSource, IDeepCloneable
[MustUseReturnValue]
IItem? RemoveHeldItem();
- ///
- /// Whether the held item has been removed for the duration of the battle.
- ///
- bool HasItemBeenRemovedForBattle { get; }
-
- ///
- /// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item.
- ///
- ///
- /// This is used for moves that remove a held item, but do not consume it. In this case, the item needs to be
- /// restored after the battle.
- ///
- IItem? RemoveHeldItemForBattle();
-
- ///
- /// Tries to steal the held item of the Pokémon. If successful, the item is removed from the Pokémon and returned.
- /// If the Pokémon does not have a held item, or the item is a form changer, this will return false.
- ///
- bool TryStealHeldItem([NotNullWhen(true)] out IItem? item);
-
- ///
- /// Restores the held item of a Pokémon if it was temporarily removed.
- ///
- void RestoreRemovedHeldItem();
-
- ///
- /// Makes the Pokemon uses its held item. Returns whether the item was consumed.
- ///
- bool ConsumeHeldItem();
-
///
/// Uses an item on the Pokemon.
///
void UseItem(IItem item);
- ///
- /// Change a boosted stat by a certain amount.
- ///
- /// The stat to be changed
- /// The amount to change the stat by
- /// Whether the change was self-inflicted. This can be relevant in scripts.
- ///
- /// The event batch ID this change is a part of. This is relevant for visual handling
- bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default);
-
- ///
- /// Suppresses the ability of the Pokémon.
- ///
- bool SuppressAbility();
-
- ///
- /// Returns the currently active ability.
- ///
- IAbility? ActiveAbility { get; }
-
///
/// Calculates the flat stats on the Pokemon. This should be called when for example the base
- /// stats, level, nature, IV, or EV changes. This has a side effect of recalculating the boosted
- /// stats, as those depend on the flat stats.
+ /// stats, level, nature, IV, or EV changes.
///
void RecalculateFlatStats();
- ///
- /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats.
- /// This should be called when a stat boost changes.
- ///
- void RecalculateBoostedStats();
-
///
/// Evolves the Pokemon to a specific evolution. This will not check whether the evolution is valid, so
/// you should check that before calling this method.
@@ -339,9 +207,10 @@ public interface IPokemon : IScriptSource, IDeepCloneable
bool EvolveTo(IEvolution evolution, EventHook? eventHook = null);
///
- /// Change the species of the Pokemon.
+ /// Change the species of the Pokemon. An optional random can be passed, which is used when the gender of
+ /// the Pokemon needs to be re-rolled.
///
- void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null);
+ void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null);
///
/// Change the form of the Pokemon.
@@ -359,12 +228,14 @@ public interface IPokemon : IScriptSource, IDeepCloneable
bool IsFainted { get; }
///
- /// Damages the Pokemon by a certain amount of damage, from a damage source.
+ /// Damages the Pokemon by a certain amount of damage. This only changes the health of the Pokemon;
+ /// battle handling, such as script hooks, events and faint handling, is done by
+ /// .
///
void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false);
///
- /// Forces the Pokémon to faint.
+ /// Sets the health of the Pokémon to 0.
///
void Faint(DamageSource source, EventBatchId batchId = default);
@@ -391,7 +262,8 @@ public interface IPokemon : IScriptSource, IDeepCloneable
bool HasStatus(StringKey status);
///
- /// Adds a non-volatile status to the Pokemon.
+ /// Adds a non-volatile status to the Pokemon. This only sets the status script; battle handling, such as
+ /// prevention script hooks and events, is done by .
///
bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default);
@@ -405,157 +277,12 @@ public interface IPokemon : IScriptSource, IDeepCloneable
///
void ChangeLevelBy(int change);
- ///
- /// Sets the current battle the Pokémon is in.
- ///
- void SetBattleData(IBattle battle, byte sideIndex);
-
- ///
- /// Sets whether the Pokémon is on the battlefield.
- ///
- void SetOnBattlefield(bool onBattleField);
-
- ///
- /// Sets the position the Pokémon has within its side.
- ///
- ///
- void SetBattleSidePosition(byte position);
-
- ///
- /// Resets the battle data of the Pokémon. This is called when the battle ends.
- ///
- void ClearBattleData();
-
- ///
- /// Marks a Pokemon as seen in the battle.
- ///
- void MarkOpponentAsSeen(IPokemon pokemon);
-
- ///
- /// Removes a type from the Pokémon. Returns whether the type was removed.
- ///
- bool RemoveType(TypeIdentifier type);
-
- ///
- /// Adds a type to the Pokémon. Returns whether the type was added. It will not add the type if
- /// the Pokémon already has it.
- ///
- bool AddType(TypeIdentifier type);
-
- ///
- /// Replace the types of the Pokémon with the provided types.
- ///
- void SetTypes(IReadOnlyList types);
-
- ///
- /// Changes the ability of the Pokémon.
- ///
- bool ChangeAbility(IAbility ability);
-
- ///
- /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as
- /// Levitate.
- ///
- bool IsFloating { get; }
-
///
/// Converts the data structure to a serializable format.
///
SerializedPokemon Serialize();
}
-///
-/// The data of the Pokémon related to being in a battle.
-/// This is only set when the Pokémon is on the field in a battle.
-///
-public interface IPokemonBattleData : IDeepCloneable
-{
- ///
- /// Sets the battle data of the Pokémon.
- ///
- void SetBattle(IBattle battle, byte sideIndex, uint switchInTurn);
-
- ///
- /// Sets the position of the Pokémon on the field.
- ///
- void SetPosition(byte position);
-
- ///
- /// The battle the Pokémon is in.
- ///
- IBattle Battle { get; }
-
- ///
- /// The index of the side of the Pokémon
- ///
- byte SideIndex { get; }
-
- ///
- /// The index of the position of the Pokémon on the field
- ///
- byte Position { get; }
-
- ///
- /// A list of opponents the Pokémon has seen this battle.
- ///
- IReadOnlyList SeenOpponents { get; }
-
- ///
- /// Sets whether the Pokémon is on the battlefield.
- ///
- void SetOnBattlefield(bool onBattleField);
-
- ///
- /// Whether the Pokémon is on the battlefield.
- ///
- bool IsOnBattlefield { get; }
-
- ///
- /// Adds an opponent to the list of seen opponents.
- ///
- void MarkOpponentAsSeen(IPokemon opponent);
-
- ///
- /// A list of items the Pokémon has consumed this battle.
- ///
- IReadOnlyList ConsumedItems { get; }
-
- ///
- /// Marks an item as consumed.
- ///
- void MarkItemAsConsumed(IItem item);
-
- ///
- /// The turn the Pokémon switched in.
- ///
- uint SwitchInTurn { get; }
-
- ///
- /// The number of turns the Pokémon has been on the field.
- ///
- uint TurnsOnField { get; }
-
- ///
- /// The side the Pokémon is on.
- ///
- IBattleSide BattleSide { get; }
-
- ///
- /// The species of the Pokémon at the time it was sent out.
- ///
- ISpecies OriginalSpecies { get; }
-
- ///
- /// The form of the Pokémon at the time it was sent out.
- ///
- IForm OriginalForm { get; }
-
- ///
- /// The last move choice executed by the Pokémon.
- ///
- IMoveChoice? LastMoveChoice { get; set; }
-}
-
///
public class PokemonImpl : ScriptSource, IPokemon
{
@@ -571,18 +298,15 @@ public class PokemonImpl : ScriptSource, IPokemon
PersonalityValue = personalityValue;
Gender = gender;
Coloring = coloring;
- Types = form.Types.ToList();
Experience = library.StaticLibrary.GrowthRates.CalculateExperience(species.GrowthRate, level);
- HeightInMeters = form.Height;
Happiness = species.BaseHappiness;
- Volatile = new ScriptSet(this);
if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature))
throw new KeyNotFoundException($"Nature {natureName} not found.");
Nature = nature;
RecalculateFlatStats();
- CurrentHealth = BoostedStats.Hp;
+ CurrentHealth = FlatStats.Hp;
}
///
@@ -608,7 +332,6 @@ public class PokemonImpl : ScriptSource, IPokemon
}
CurrentHealth = serializedPokemon.CurrentHealth;
- HeightInMeters = form.Height;
Happiness = serializedPokemon.Happiness;
IndividualValues = serializedPokemon.IndividualValues.ToIndividualValueStatisticSet();
EffortValues = serializedPokemon.EffortValues.ToEffortValueStatisticSet();
@@ -621,7 +344,6 @@ public class PokemonImpl : ScriptSource, IPokemon
AbilityIndex = form.FindAbilityIndex(ability) ??
throw new KeyNotFoundException(
$"Ability {ability.Name} not found on species {species.Name} form {form.Name}.");
- Volatile = new ScriptSet(this);
_learnedMoves = serializedPokemon.Moves.Select(move =>
{
if (move == null)
@@ -632,7 +354,6 @@ public class PokemonImpl : ScriptSource, IPokemon
}).ToArray();
AllowedExperience = serializedPokemon.AllowedExperience;
IsEgg = serializedPokemon.IsEgg;
- Types = form.Types;
RecalculateFlatStats();
if (serializedPokemon.Status != null)
@@ -654,24 +375,6 @@ public class PokemonImpl : ScriptSource, IPokemon
///
public IForm Form { get; private set; }
- ///
- public ISpecies? DisplaySpecies { get; set; }
-
- ///
- public IForm? DisplayForm { get; set; }
-
- ///
- public void SetDisplaySpecies(ISpecies? species, IForm? form)
- {
- DisplaySpecies = species;
- DisplayForm = form;
-
- BattleData?.Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form)
- {
- BatchId = new EventBatchId(),
- });
- }
-
///
public LevelInt Level { get; private set; }
@@ -679,7 +382,7 @@ public class PokemonImpl : ScriptSource, IPokemon
public uint Experience { get; private set; }
///
- public bool AddExperience(uint experience)
+ public bool AddExperience(uint experience, EventHook? eventHook = null)
{
if (!AllowedExperience)
return false;
@@ -690,7 +393,7 @@ public class PokemonImpl : ScriptSource, IPokemon
var oldExperience = Experience;
Experience += experience;
var batchId = new EventBatchId();
- BattleData?.Battle.EventHook.Invoke(new ExperienceGainEvent(this, oldExperience, Experience)
+ eventHook?.Invoke(new ExperienceGainEvent(this, oldExperience, Experience)
{
BatchId = batchId,
});
@@ -700,7 +403,7 @@ public class PokemonImpl : ScriptSource, IPokemon
{
Level = newLevel;
RecalculateFlatStats();
- BattleData?.Battle.EventHook.Invoke(new LevelUpEvent(this, oldLevel, Level)
+ eventHook?.Invoke(new LevelUpEvent(this, oldLevel, Level)
{
BatchId = batchId,
});
@@ -738,9 +441,6 @@ public class PokemonImpl : ScriptSource, IPokemon
get
{
var weight = Form.Weight;
- if (BattleData is not null)
- // ReSharper disable once AccessToModifiedClosure
- this.RunScriptHook(script => script.ModifyWeight(ref weight));
if (weight < 0.1f)
weight = 0.1f;
return weight;
@@ -748,7 +448,7 @@ public class PokemonImpl : ScriptSource, IPokemon
}
///
- public float HeightInMeters { get; set; }
+ public float HeightInMeters => Form.Height;
///
public byte Happiness { get; set; }
@@ -757,13 +457,7 @@ public class PokemonImpl : ScriptSource, IPokemon
public StatisticSet FlatStats { get; } = new();
///
- public StatBoostStatisticSet StatBoost { get; } = new();
-
- ///
- public StatisticSet BoostedStats { get; } = new();
-
- ///
- public uint MaxHealth => BoostedStats.Hp;
+ public uint MaxHealth => FlatStats.Hp;
///
public IndividualValueStatisticSet IndividualValues { get; } = new();
@@ -780,30 +474,31 @@ public class PokemonImpl : ScriptSource, IPokemon
///
public AbilityIndex AbilityIndex { get; }
- ///
- public IAbility? OverrideAbility { get; private set; }
+ private (IAbility Ability, IForm Form, AbilityIndex Index)? _abilityCache;
///
- public IPokemonBattleData? BattleData { get; private set; }
+ public IAbility Ability
+ {
+ get
+ {
+ if (_abilityCache is not null && _abilityCache.Value.Form == Form &&
+ _abilityCache.Value.Index == AbilityIndex)
+ return _abilityCache.Value.Ability;
+ var ability = Form.GetAbility(AbilityIndex);
+ if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj))
+ throw new KeyNotFoundException($"Ability {ability} not found.");
+ _abilityCache = (abilityObj, Form, AbilityIndex);
+ return abilityObj;
+ }
+ }
private readonly ILearnedMove?[] _learnedMoves = new ILearnedMove[Const.MovesCount];
- ///
- /// Battle-only per-slot overrides of . The permanent moveset is never mutated by
- /// temporary moves; discarding this array is all that is needed to restore the original moves.
- ///
- private ILearnedMove?[]? _temporaryMoves;
+ ///
+ public IReadOnlyList Moves => _learnedMoves;
///
- public IReadOnlyList Moves => _temporaryMoves == null
- ? _learnedMoves
- : _temporaryMoves.Select((move, index) => move ?? _learnedMoves[index]).ToArray();
-
- ///
- public IReadOnlyList BaseMoves => _learnedMoves;
-
- ///
- public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName);
+ public bool HasMove(StringKey moveName) => _learnedMoves.Any(move => move?.MoveData.Name == moveName);
///
public void SwapMoves(byte index1, byte index2)
@@ -811,49 +506,23 @@ public class PokemonImpl : ScriptSource, IPokemon
if (index1 >= Const.MovesCount || index2 >= Const.MovesCount)
return;
(_learnedMoves[index1], _learnedMoves[index2]) = (_learnedMoves[index2], _learnedMoves[index1]);
- if (_temporaryMoves != null)
- (_temporaryMoves[index1], _temporaryMoves[index2]) = (_temporaryMoves[index2], _temporaryMoves[index1]);
}
///
public bool AllowedExperience { get; set; }
- private List _types = new();
-
///
- public IReadOnlyList Types
- {
- get => _types;
- private set => _types = value.ToList();
- }
+ public IReadOnlyList Types => Form.Types;
///
public bool IsEgg { get; private set; }
- ///
- public bool IsCaught { get; private set; }
-
- ///
- public void MarkAsCaught()
- {
- IsCaught = true;
- }
-
- ///
- public ScriptContainer HeldItemTriggerScript { get; } = new();
-
- ///
- public ScriptContainer AbilityScript { get; } = new();
-
///
public ScriptContainer StatusScript { get; } = new();
///
public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft();
- ///
- public IScriptSet Volatile { get; }
-
///
public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName;
@@ -862,7 +531,6 @@ public class PokemonImpl : ScriptSource, IPokemon
{
var previous = HeldItem;
HeldItem = item;
- this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item));
return previous;
}
@@ -878,168 +546,17 @@ public class PokemonImpl : ScriptSource, IPokemon
}
var previous = HeldItem;
HeldItem = null;
- this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null));
return previous;
}
///
- public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null;
-
- private IItem? _removedHeldItem;
-
- ///
- public IItem? RemoveHeldItemForBattle()
- {
- return _removedHeldItem = RemoveHeldItem();
- }
-
- ///
- public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item)
- {
- if (HeldItem is null || HeldItem.Category == ItemCategory.FormChanger)
- {
- item = null;
- return false;
- }
- var prevent = false;
- this.RunScriptHook(script =>
- script.PreventHeldItemSteal(this, HeldItem, ref prevent));
- if (prevent)
- {
- item = null;
- return false;
- }
- item = RemoveHeldItemForBattle();
- return item is not null;
- }
-
- ///
- public void RestoreRemovedHeldItem()
- {
- _ = ForceSetHeldItem(_removedHeldItem);
- _removedHeldItem = null;
- }
-
- ///
- public bool ConsumeHeldItem()
- {
- if (HeldItem is null)
- return false;
- if (!Library.ScriptResolver.TryResolveBattleItemScript(HeldItem, out _))
- return false;
-
- if (BattleData != null)
- {
- var prevented = false;
- this.RunScriptHook(script =>
- script.PreventHeldItemConsume(this, HeldItem, ref prevented));
- if (prevented)
- return false;
- BattleData.MarkItemAsConsumed(HeldItem);
- }
-
- UseItem(ForceSetHeldItem(null)!);
- return true;
- }
-
- ///
- /// Uses an item on this Pokémon.
- ///
- ///
public void UseItem(IItem item)
{
// TODO: actually consume the item
-
- this.RunScriptHook(x => x.OnAfterItemConsume(this, item));
}
///
- public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force,
- EventBatchId batchId = default)
- {
- if (!force)
- {
- var prevented = false;
- this.RunScriptHook(script =>
- script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
- if (prevented)
- return false;
- this.RunScriptHook(script =>
- script.ChangeStatBoostChange(this, stat, selfInflicted, ref change));
- if (change == 0)
- return false;
- }
- var changed = false;
- var oldBoost = StatBoost.GetStatistic(stat);
- changed = change switch
- {
- > 0 => StatBoost.IncreaseStatistic(stat, change),
- < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change),
- _ => changed,
- };
- if (!changed)
- return false;
- if (BattleData != null)
- {
- var newBoost = StatBoost.GetStatistic(stat);
- BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost)
- {
- BatchId = batchId,
- });
- }
-
- RecalculateBoostedStats();
- this.RunScriptHook(script =>
- script.OnAfterStatBoostChange(this, stat, selfInflicted, change));
- return true;
- }
-
- ///
- /// Whether the ability of the Pokémon is suppressed.
- ///
- public bool AbilitySuppressed { get; private set; }
-
- ///
- public bool SuppressAbility()
- {
- if (ActiveAbility?.CanBeChanged == false)
- return false;
-
- AbilitySuppressed = true;
- AbilityScript.Clear();
- return true;
- }
-
- private (IAbility, AbilityIndex)? _abilityCache;
-
- ///
- public IAbility? ActiveAbility
- {
- get
- {
- if (AbilitySuppressed)
- return null;
- if (OverrideAbility != null)
- return OverrideAbility;
- if (_abilityCache is not null && _abilityCache.Value.Item2 == AbilityIndex)
- return _abilityCache.Value.Item1;
- var ability = Form.GetAbility(AbilityIndex);
- if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj))
- throw new KeyNotFoundException($"Ability {ability} not found.");
- _abilityCache = (abilityObj, AbilityIndex);
- return abilityObj;
- }
- }
-
- ///
- public void RecalculateFlatStats()
- {
- Library.StatCalculator.CalculateFlatStats(this, FlatStats);
- RecalculateBoostedStats();
- }
-
- ///
- public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
+ public void RecalculateFlatStats() => Library.StatCalculator.CalculateFlatStats(this, FlatStats);
///
public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null)
@@ -1058,21 +575,19 @@ public class PokemonImpl : ScriptSource, IPokemon
}
///
- public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null)
+ public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null)
{
- eventHook ??= BattleData?.Battle.EventHook;
if (Species == species)
{
if (form != Form)
- ChangeForm(form, new EventBatchId());
+ ChangeForm(form, new EventBatchId(), eventHook);
return;
}
// If the Pokémon is genderless, but its new species is not, we want to set its gender
if (Gender != Gender.Genderless && species.GenderRate < 0.0)
{
- var random = (IRandom?)BattleData?.Battle.Random ?? new RandomImpl();
- Gender = species.GetRandomGender(random);
+ Gender = species.GetRandomGender(random ?? new RandomImpl());
}
// Else if the new species is genderless, but the Pokémon has a gender, make the creature genderless.
else if (species.GenderRate < 0.0 && Gender != Gender.Genderless)
@@ -1086,7 +601,7 @@ public class PokemonImpl : ScriptSource, IPokemon
BatchId = batchId,
});
Species = species;
- ChangeForm(form, batchId);
+ ChangeForm(form, batchId, eventHook);
}
///
@@ -1094,13 +609,8 @@ public class PokemonImpl : ScriptSource, IPokemon
{
if (form == Form)
return;
- eventHook ??= BattleData?.Battle.EventHook;
-
- var oldAbility = Form.GetAbility(AbilityIndex);
Form = form;
- Types = form.Types.ToList();
- HeightInMeters = form.Height;
var abilityIndex = AbilityIndex;
abilityIndex = AbilityIndex.IsHidden switch
{
@@ -1115,28 +625,9 @@ public class PokemonImpl : ScriptSource, IPokemon
_ => abilityIndex,
};
- var newAbility = Form.GetAbility(abilityIndex);
-
- if (OverrideAbility == null && oldAbility != newAbility)
- {
- AbilityScript.Clear();
- if (!Library.StaticLibrary.Abilities.TryGet(newAbility, out var ability))
- throw new KeyNotFoundException($"Ability {newAbility} not found.");
- if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, newAbility, ability.Parameters,
- out var abilityScript))
- {
- AbilityScript.Set(abilityScript);
- abilityScript.OnAddedToParent(this);
- }
- else
- {
- AbilityScript.Clear();
- }
- }
-
- var oldHealth = BoostedStats.Hp;
+ var oldHealth = FlatStats.Hp;
RecalculateFlatStats();
- var diffHealth = (long)BoostedStats.Hp - oldHealth;
+ var diffHealth = (long)FlatStats.Hp - oldHealth;
if (diffHealth > 0)
{
Heal((uint)diffHealth, true);
@@ -1152,26 +643,18 @@ public class PokemonImpl : ScriptSource, IPokemon
///
///
- /// Currently this checks the Pokémon is not an egg, not caught, and not fainted.
+ /// Currently this checks the Pokémon is not an egg and not fainted.
///
- public bool IsUsable => !IsCaught && !IsEgg && !IsFainted;
+ public bool IsUsable => !IsEgg && !IsFainted;
///
public bool IsFainted => CurrentHealth == 0;
///
- public void Damage(uint damage, DamageSource source, EventBatchId batchId, bool forceDamage = false)
+ public void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false)
{
- // If the Pokémon is already fainted, we don't need to do anything.
if (IsFainted)
return;
- if (BattleData is not null && !forceDamage)
- {
- var dmg = damage;
- this.RunScriptHook(script =>
- script.ChangeIncomingDamage(this, source, ref dmg));
- damage = dmg;
- }
if (damage == 0)
return;
@@ -1179,97 +662,29 @@ public class PokemonImpl : ScriptSource, IPokemon
// underflow.
if (damage >= CurrentHealth)
damage = CurrentHealth;
- // Calculate the new health.
- var newHealth = CurrentHealth - damage;
- if (BattleData is not null)
- {
- // If the Pokémon is in a battle, we trigger an event to the front-end.
- BattleData.Battle.EventHook.Invoke(new DamageEvent(this, CurrentHealth, newHealth, source)
- {
- BatchId = batchId,
- });
- // And allow scripts to execute.
- this.RunScriptHook(script => script.OnDamage(this, source, CurrentHealth, newHealth));
- }
-
- CurrentHealth = newHealth;
-
- // If the Pokémon is now fainted, we also run faint handling.
- if (IsFainted)
- {
- OnFaint(source);
- }
+ CurrentHealth -= damage;
}
///
public void Faint(DamageSource source, EventBatchId batchId = default)
{
CurrentHealth = 0;
- OnFaint(source);
- }
-
- private void OnFaint(DamageSource source)
- {
- // If the Pokémon is not in a battle, we don't need to do anything.
- if (BattleData is null)
- return;
-
- // Trigger the faint event to the front-end.
- BattleData.Battle.EventHook.Invoke(new FaintEvent(this));
-
- // Allow scripts to trigger based on the faint.
- this.RunScriptHook(script => script.OnFaint(this, source));
- foreach (var ally in BattleData.BattleSide.Pokemon.WhereNotNull().Where(x => x != this))
- {
- ally.RunScriptHook(script => script.OnAllyFaint(ally, this));
- }
-
- // Make sure the OnRemove script is run.
- this.RunScriptHook(script => script.OnRemove());
-
- // Mark the position as unfillable if it can't be filled by any party.
- if (!BattleData.Battle.CanSlotBeFilled(BattleData.SideIndex, BattleData.Position))
- {
- BattleData.Battle.Sides[BattleData.SideIndex].MarkPositionAsUnfillable(BattleData.Position);
- }
- BattleData.BattleSide.MarkFaint(BattleData.Position);
- BattleData.BattleSide.ForceClearPokemonFromField(BattleData.Position);
-
- foreach (var opponent in BattleData.SeenOpponents.WhereNotNull())
- {
- if (!opponent.IsUsable)
- continue;
- if (!opponent.AllowedExperience)
- continue;
- opponent.AddExperience(Library.ExperienceGainCalculator.CalculateExperienceGain(this, opponent));
- }
-
- // Validate the battle state to see if the battle is over.
- BattleData.Battle.ValidateBattleState();
}
///
- public bool Heal(uint heal, bool allowRevive, EventBatchId batchId = default, bool forceHeal = false,
+ public bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false,
EventHook? customEventHook = null)
{
if (IsFainted && !allowRevive)
return false;
- var maxAmount = BoostedStats.Hp - CurrentHealth;
+ var maxAmount = MaxHealth - CurrentHealth;
if (heal > maxAmount)
heal = maxAmount;
if (heal == 0)
return false;
- if (!forceHeal)
- {
- var prevented = false;
- this.RunScriptHook(x => x.PreventHeal(this, heal, allowRevive, ref prevented));
- if (prevented)
- return false;
- }
var newHealth = CurrentHealth + heal;
- customEventHook ??= BattleData?.Battle.EventHook;
customEventHook?.Invoke(new HealEvent(this, CurrentHealth, newHealth)
{
BatchId = batchId,
@@ -1281,7 +696,7 @@ public class PokemonImpl : ScriptSource, IPokemon
///
public void RestoreAllPP()
{
- foreach (var move in Moves)
+ foreach (var move in _learnedMoves)
{
move?.RestoreAllUses();
}
@@ -1312,18 +727,6 @@ public class PokemonImpl : ScriptSource, IPokemon
_learnedMoves[index] = new LearnedMoveImpl(move, method);
}
- ///
- public void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index)
- {
- if (index >= Const.MovesCount)
- throw new ArgumentOutOfRangeException(nameof(index), $"Move slot {index} is out of range.");
- if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
- throw new KeyNotFoundException($"Move {moveName} not found.");
-
- _temporaryMoves ??= new ILearnedMove?[Const.MovesCount];
- _temporaryMoves[index] = new LearnedMoveImpl(move, method);
- }
-
///
public bool HasStatus(StringKey status) => StatusScript.Script?.Name == status;
@@ -1335,23 +738,9 @@ public class PokemonImpl : ScriptSource, IPokemon
if (!StatusScript.IsEmpty)
return false;
- var oldStatus = StatusScript.Script?.Name;
- var selfInflicted = originPokemon == this;
-
- var preventStatus = false;
- this.RunScriptHook(script =>
- script.PreventStatusChange(this, status, selfInflicted, ref preventStatus));
- if (preventStatus)
- return false;
StatusScript.Set(statusScript);
statusScript.OnAddedToParent(this);
- BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status)
- {
- BatchId = batchId,
- });
- this.RunScriptHook(script =>
- script.OnAfterStatusChange(this, status, originPokemon));
return true;
}
@@ -1359,10 +748,6 @@ public class PokemonImpl : ScriptSource, IPokemon
public void ClearStatus(EventBatchId batchId = default)
{
StatusScript.Clear();
- BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, StatusScript.Script?.Name, null)
- {
- BatchId = batchId,
- });
}
///
@@ -1373,166 +758,20 @@ public class PokemonImpl : ScriptSource, IPokemon
RecalculateFlatStats();
}
- ///
- public void SetBattleData(IBattle battle, byte sideIndex)
- {
- if (BattleData is not null)
- {
- BattleData.SetBattle(battle, sideIndex, battle.CurrentTurnNumber);
- }
- else
- {
- BattleData = new PokemonBattleDataImpl(battle, sideIndex, battle.CurrentTurnNumber, Species, Form);
- }
- if (ActiveAbility != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ActiveAbility.Name,
- ActiveAbility.Parameters, out var abilityScript))
- {
- AbilityScript.Set(abilityScript);
- abilityScript.OnAddedToParent(this);
- }
- else
- {
- AbilityScript.Clear();
- }
- }
-
- ///
- public void SetOnBattlefield(bool onBattleField)
- {
- if (BattleData is not null)
- {
- BattleData.SetOnBattlefield(onBattleField);
- if (!onBattleField)
- {
- Volatile.Clear();
- _temporaryMoves = null;
- HeightInMeters = Form.Height;
- Types = Form.Types;
- OverrideAbility = null;
- AbilitySuppressed = false;
- RecalculateFlatStats();
- }
- }
- }
-
- ///
- public void SetBattleSidePosition(byte position)
- {
- BattleData?.SetPosition(position);
- }
-
- ///
- public void ClearBattleData()
- {
- var battleData = BattleData;
- BattleData = null;
- Volatile.Clear();
- _temporaryMoves = null;
- HeightInMeters = Form.Height;
- Types = Form.Types;
- OverrideAbility = null;
- AbilitySuppressed = false;
- StatBoost.Reset();
- if (battleData != null && Form.IsBattleOnlyForm)
- {
- ChangeForm(battleData.OriginalSpecies == Species ? battleData.OriginalForm : Species.GetDefaultForm());
- }
- DisplaySpecies = null;
- DisplayForm = null;
- IsCaught = false;
- }
-
- ///
- public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon);
-
- ///
- public bool RemoveType(TypeIdentifier type) => _types.Remove(type);
-
- ///
- public bool AddType(TypeIdentifier type)
- {
- if (_types.Contains(type))
- return false;
- _types.Add(type);
- return true;
- }
-
- ///
- public void SetTypes(IReadOnlyList types)
- {
- _types = types.ToList();
- }
-
- ///
- public bool ChangeAbility(IAbility ability)
- {
- if (!ability.CanBeChanged)
- return false;
- OverrideAbility = ability;
- if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters,
- out var abilityScript))
- {
- AbilityScript.Set(abilityScript);
- abilityScript.OnAddedToParent(this);
- }
- else
- {
- AbilityScript.Clear();
- }
- return true;
- }
-
- private static readonly StringKey FlyingTypeName = "flying";
-
- ///
- public bool IsFloating
- {
- get
- {
- var isFloating = Types.Any(x => x.Name == FlyingTypeName);
- this.RunScriptHook(x => x.IsFloating(this, ref isFloating));
- return isFloating;
- }
- }
-
///
public SerializedPokemon Serialize() => new(this);
///
- public override int ScriptCount
- {
- get
- {
- var c = 4;
- if (BattleData != null)
- {
- var side = BattleData.Battle.Sides[BattleData.SideIndex];
- c += side.ScriptCount;
- }
-
- return c;
- }
- }
+ public override int ScriptCount => 1;
///
public override void GetOwnScripts(List> scripts)
{
- scripts.Add(HeldItemTriggerScript);
- scripts.Add(AbilityScript);
scripts.Add(StatusScript);
- scripts.Add(Volatile);
}
///
- public override void CollectScripts(List> scripts)
- {
- GetOwnScripts(scripts);
- if (BattleData != null)
- {
- var side = BattleData.Battle.Sides[BattleData.SideIndex];
- side.CollectScripts(scripts);
- }
- }
+ public override void CollectScripts(List> scripts) => GetOwnScripts(scripts);
///
public override string ToString()
@@ -1541,92 +780,4 @@ public class PokemonImpl : ScriptSource, IPokemon
return $"{Nickname} ({Species.Name})";
return Species.Name;
}
-}
-
-///
-public class PokemonBattleDataImpl : IPokemonBattleData
-{
- ///
- public PokemonBattleDataImpl(IBattle battle, byte sideIndex, uint switchInTurn, ISpecies originalSpecies,
- IForm originalForm)
- {
- Battle = battle;
- SideIndex = sideIndex;
- SwitchInTurn = switchInTurn;
- OriginalSpecies = originalSpecies;
- OriginalForm = originalForm;
- }
-
- ///
- public void SetBattle(IBattle battle, byte sideIndex, uint switchInTurn)
- {
- Battle = battle;
- SideIndex = sideIndex;
- SwitchInTurn = switchInTurn;
- }
-
- ///
- public void SetPosition(byte position)
- {
- Position = position;
- }
-
- ///
- public IBattle Battle { get; set; }
-
- ///
- public byte SideIndex { get; set; }
-
- ///
- public byte Position { get; set; }
-
- private readonly List _seenOpponents = [];
-
- ///
- public IReadOnlyList SeenOpponents => _seenOpponents;
-
- ///
- public void SetOnBattlefield(bool onBattleField)
- {
- IsOnBattlefield = onBattleField;
- }
-
- ///
- public bool IsOnBattlefield { get; set; }
-
- ///
- public void MarkOpponentAsSeen(IPokemon opponent)
- {
- _seenOpponents.Add(opponent);
- }
-
- private readonly List _consumedItems = [];
-
- ///
- public IReadOnlyList ConsumedItems => _consumedItems;
-
- ///
- public void MarkItemAsConsumed(IItem item)
- {
- _consumedItems.Add(item);
- BattleSide.SetConsumedItem(Position, item);
- }
-
- ///
- public uint SwitchInTurn { get; set; }
-
- ///
- public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn;
-
- ///
- public IBattleSide BattleSide => Battle.Sides[SideIndex];
-
- ///
- public ISpecies OriginalSpecies { get; }
-
- ///
- public IForm OriginalForm { get; }
-
- ///
- public IMoveChoice? LastMoveChoice { get; set; }
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs b/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs
index 8bcaef7..5743e6d 100644
--- a/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs
+++ b/PkmnLib.Dynamic/Models/Serialized/SerializedPokemon.cs
@@ -33,7 +33,7 @@ public record SerializedPokemon
Nature = pokemon.Nature.Name;
Nickname = pokemon.Nickname;
Ability = pokemon.Form.GetAbility(pokemon.AbilityIndex);
- Moves = pokemon.BaseMoves.Select(move =>
+ Moves = pokemon.Moves.Select(move =>
{
if (move == null)
return null;
diff --git a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs
index 5b2786f..7cde65f 100644
--- a/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/ItemScript.cs
@@ -41,7 +41,7 @@ public abstract class ItemScript : IDeepCloneable
///
/// Returns whether the item is usable on the given target.
///
- public virtual bool IsTargetValid(IPokemon target) => false;
+ public virtual bool IsTargetValid(IBattlePokemon target) => false;
///
/// Returns whether the item can be held by a Pokémon.
@@ -51,7 +51,7 @@ public abstract class ItemScript : IDeepCloneable
///
/// Returns whether the item can be held by the given target.
///
- public virtual bool CanTargetHold(IPokemon pokemon) => true;
+ public virtual bool CanTargetHold(IBattlePokemon pokemon) => true;
///
/// Handles the use of the item.
@@ -63,7 +63,7 @@ public abstract class ItemScript : IDeepCloneable
///
/// Handles the use of the item on the given target.
///
- public virtual void OnUseWithTarget(IPokemon target, EventHook eventHook)
+ public virtual void OnUseWithTarget(IBattlePokemon target, EventHook eventHook)
{
}
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs b/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs
index 4fdc006..8cc9a45 100644
--- a/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/PokeballScript.cs
@@ -17,9 +17,9 @@ public abstract class PokeballScript : ItemScript
///
/// Returns the catch rate of the Pokéball against the given target Pokémon.
///
- public abstract void ChangeCatchRate(IPokemon target, ref byte catchRate);
+ public abstract void ChangeCatchRate(IBattlePokemon target, ref byte catchRate);
- public virtual void OnAfterSuccessfulCapture(IPokemon target)
+ public virtual void OnAfterSuccessfulCapture(IBattlePokemon target)
{
// Default implementation does nothing.
// Override this method in derived classes to add custom behavior after a successful capture.
@@ -32,16 +32,13 @@ public abstract class PokeballScript : ItemScript
public override ItemTargetType TargetType => ItemTargetType.FoePokemon;
///
- public override bool IsTargetValid(IPokemon target) =>
- target.BattleData is not null && target.BattleData.Battle.IsWildBattle;
+ public override bool IsTargetValid(IBattlePokemon target) => target.Battle.IsWildBattle;
///
- public override void OnUseWithTarget(IPokemon target, EventHook eventHook)
+ public override void OnUseWithTarget(IBattlePokemon target, EventHook eventHook)
{
- var battleData = target.BattleData;
-
- var result = battleData?.Battle.AttempCapture(battleData.SideIndex, battleData.Position, Item);
- if (result is { IsCaught: true })
+ var result = target.Battle.AttempCapture(target.SideIndex, target.Position, Item);
+ if (result.IsCaught)
{
OnAfterSuccessfulCapture(target);
}
diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs
index 8dcdaa8..c745b89 100644
--- a/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/ScriptAIInformationInterfaces.cs
@@ -25,7 +25,7 @@ public interface IAIInfoScriptExpectedEndOfTurnDamage
/// This function returns the expected end of turn damage for the script. This is used for scripts that
/// have an end of turn effect, such as Poison or Burn.
///
- void ExpectedEndOfTurnDamage(IPokemon pokemon, ref int damage);
+ void ExpectedEndOfTurnDamage(IBattlePokemon pokemon, ref int damage);
}
///
@@ -37,5 +37,5 @@ public interface IAIInfoScriptExpectedEntryDamage
/// This function returns the expected entry damage for the script. This is used for scripts that have
/// an entry hazard effect, such as Spikes or Stealth Rock.
///
- void ExpectedEntryDamage(IPokemon pokemon, ref uint damage);
+ void ExpectedEntryDamage(IBattlePokemon pokemon, ref uint damage);
}
\ No newline at end of file
diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs
index 1d9d216..dd068e6 100644
--- a/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/ScriptExecution.cs
@@ -100,8 +100,8 @@ public static class ScriptExecution
///
/// Executes a script on an item.
///
- public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IPokemon? target, IPokemon user,
- IBattle battle, EventHook eventHook)
+ public static void RunItemScript(this IItem item, ScriptResolver scriptResolver, IBattlePokemon? target,
+ IBattlePokemon user, IBattle battle, EventHook eventHook)
{
if (!scriptResolver.TryResolveBattleItemScript(item, out var itemScript))
{
diff --git a/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs b/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs
index 067ea73..f97208e 100644
--- a/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs
+++ b/PkmnLib.Dynamic/ScriptHandling/ScriptInterfaces.cs
@@ -125,7 +125,7 @@ public interface IScriptChangeTargets
///
/// Changes the targets of a move choice. This allows for changing the targets of a move before the move starts.
///
- void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList targets);
+ void ChangeTargets(IMoveChoice moveChoice, ref IReadOnlyList targets);
}
///
@@ -136,7 +136,7 @@ public interface IScriptChangeIncomingTargets
///
/// This function allows you to change the targets of a move choice before the move starts.
///
- void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList targets);
+ void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList targets);
}
///
@@ -219,7 +219,7 @@ public interface IScriptChangeMoveType
/// This function allows the script to change the actual type that is used for the move on a target.
/// If this is set to null, the move will be treated as a typeless move.
///
- void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit, ref TypeIdentifier? typeIdentifier);
+ void ChangeMoveType(IExecutingMove move, IBattlePokemon target, byte hit, ref TypeIdentifier? typeIdentifier);
}
///
@@ -230,7 +230,7 @@ public interface IScriptChangeEffectiveness
///
/// This function allows the script to change how effective a move is on a target.
///
- void ChangeEffectiveness(IExecutingMove move, IPokemon target, byte hit, ref float effectiveness);
+ void ChangeEffectiveness(IExecutingMove move, IBattlePokemon target, byte hit, ref float effectiveness);
}
///
@@ -241,7 +241,7 @@ public interface IScriptChangeIncomingEffectiveness
///
/// This function allows the script to override how effective a move is on a target.
///
- void ChangeIncomingEffectiveness(IExecutingMove executingMove, IPokemon target, byte hitIndex,
+ void ChangeIncomingEffectiveness(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
ref float effectiveness);
}
@@ -253,7 +253,7 @@ public interface IScriptBlockCriticalHit
///
/// This function allows a script to block an outgoing move from being critical.
///
- void BlockCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block);
+ void BlockCriticalHit(IExecutingMove move, IBattlePokemon target, byte hit, ref bool block);
}
///
@@ -264,7 +264,7 @@ public interface IScriptBlockIncomingCriticalHit
///
/// This function allows a script to block an incoming move from being critical.
///
- void BlockIncomingCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block);
+ void BlockIncomingCriticalHit(IExecutingMove move, IBattlePokemon target, byte hit, ref bool block);
}
///
@@ -276,7 +276,7 @@ public interface IScriptOnIncomingHit
/// This function triggers when an incoming hit happens. This triggers after the damage is done,
/// but before the secondary effect of the move happens.
///
- void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit);
+ void OnIncomingHit(IExecutingMove move, IBattlePokemon target, byte hit);
}
///
@@ -287,7 +287,7 @@ public interface IScriptOnOpponentFaints
///
/// This function triggers when an opponent on the f ield faints due to the move that is being executed.
///
- void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit);
+ void OnOpponentFaints(IExecutingMove move, IBattlePokemon target, byte hit);
}
///
@@ -300,7 +300,7 @@ public interface IScriptOnSecondaryEffect
/// secondary effects here. Status moves should implement their actual functionality in this
/// function as well, as status moves effects are defined as secondary effects for simplicity.
///
- void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit);
+ void OnSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit);
}
///
@@ -312,7 +312,7 @@ public interface IScriptFailIncomingMove
/// This function allows a script to prevent a move that is targeted at its owner. If set to true
/// the move fails, and fail events get triggered.
///
- void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail);
+ void FailIncomingMove(IExecutingMove move, IBattlePokemon target, ref bool fail);
}
///
@@ -323,7 +323,7 @@ public interface IScriptIsInvulnerableToMove
///
/// This function allows a script to make its owner invulnerable to an incoming move.
///
- void IsInvulnerableToMove(IExecutingMove move, IPokemon target, ref bool invulnerable);
+ void IsInvulnerableToMove(IExecutingMove move, IBattlePokemon target, ref bool invulnerable);
}
///
@@ -335,7 +335,7 @@ public interface IScriptOnMoveMiss
/// This function allows a script to run when a move misses its target. This is used for moves
/// that have a secondary effect that should run even if the move misses, such as Spore.
///
- void OnMoveMiss(IExecutingMove move, IPokemon target);
+ void OnMoveMiss(IExecutingMove move, IBattlePokemon target);
}
///
@@ -347,7 +347,7 @@ public interface IScriptChangeAccuracyModifier
/// This function allows a script to modify the accuracy of a move used. This value represents
/// the percentage accuracy, so anything above 100% will make it always hit.
///
- void ChangeAccuracyModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier);
+ void ChangeAccuracyModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
}
///
@@ -358,7 +358,7 @@ public interface IScriptChangeCriticalStage
///
/// This function allows a script to change the critical stage of the move used.
///
- void ChangeCriticalStage(IExecutingMove move, IPokemon target, byte hit, ref byte stage);
+ void ChangeCriticalStage(IExecutingMove move, IBattlePokemon target, byte hit, ref byte stage);
}
///
@@ -370,7 +370,7 @@ public interface IScriptChangeCriticalModifier
/// This function allows a script to change the damage modifier of a critical hit. This will only
/// run when a hit is critical.
///
- void ChangeCriticalModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier);
+ void ChangeCriticalModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
}
///
@@ -382,7 +382,7 @@ public interface IScriptChangeStabModifier
/// This function allows a script to change the damage modifier of a Same Type Attack Bonus, which
/// occurs when the user has the move type as one of its own types.
///
- void ChangeStabModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, bool isStab,
+ void ChangeStabModifier(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber, bool isStab,
ref float modifier);
}
@@ -394,7 +394,7 @@ public interface IScriptChangeBasePower
///
/// This function allows a script to change the effective base power of a move hit.
///
- void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower);
+ void ChangeBasePower(IExecutingMove move, IBattlePokemon target, byte hit, ref ushort basePower);
}
///
@@ -407,7 +407,7 @@ public interface IScriptBypassDefensiveStatBoosts
/// If this is true, the damage will be calculated as if the target has no positive stat boosts. Negative
/// stat boosts will still be applied.
///
- void BypassDefensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass);
+ void BypassDefensiveStatBoosts(IExecutingMove move, IBattlePokemon target, byte hit, ref bool bypass);
}
///
@@ -419,7 +419,7 @@ public interface IScriptBypassEvasionStatBoosts
/// This function allows a script to bypass evasion stat boosts for a move hit.
/// If this is true, the move will handle the evasion stat boosts as if the target has no positive stat boosts.
///
- void BypassEvasionStatBoosts(IExecutingMove move, IPokemon target, byte hitIndex, ref bool bypass);
+ void BypassEvasionStatBoosts(IExecutingMove move, IBattlePokemon target, byte hitIndex, ref bool bypass);
}
///
@@ -432,7 +432,7 @@ public interface IScriptBypassOffensiveStatBoosts
/// If this is true, the damage will be calculated as if the user has no negative offensive stat boosts. Positive
/// stat boosts will still be applied.
///
- void BypassOffensiveStatBoosts(IExecutingMove move, IPokemon target, byte hit, ref bool bypass);
+ void BypassOffensiveStatBoosts(IExecutingMove move, IBattlePokemon target, byte hit, ref bool bypass);
}
///
@@ -443,7 +443,7 @@ public interface IScriptChangeOffensiveStatValue
///
/// This function allows a script to change the actual offensive stat values used when calculating damage
///
- void ChangeOffensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint defensiveStat,
+ void ChangeOffensiveStatValue(IExecutingMove move, IBattlePokemon target, byte hit, uint defensiveStat,
ImmutableStatisticSet targetStats, Statistic stat, ref uint value);
}
@@ -455,7 +455,7 @@ public interface IScriptChangeDefensiveStatValue
///
/// This function allows a script to change the actual defensive stat values used when calculating damage.
///
- void ChangeDefensiveStatValue(IExecutingMove move, IPokemon target, byte hit, uint offensiveStat,
+ void ChangeDefensiveStatValue(IExecutingMove move, IBattlePokemon target, byte hit, uint offensiveStat,
ImmutableStatisticSet targetStats, Statistic stat, ref uint value);
}
@@ -467,7 +467,7 @@ public interface IScriptChangeIncomingMoveOffensiveStatValue
///
/// This function allows a script to change the offensive stat value of an incoming move.
///
- void ChangeIncomingMoveOffensiveStatValue(IExecutingMove executingMove, IPokemon target, byte hitNumber,
+ void ChangeIncomingMoveOffensiveStatValue(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber,
uint defensiveStat, StatisticSet targetStats, Statistic offensive, ref uint offensiveStat);
}
@@ -479,7 +479,7 @@ public interface IScriptChangeIncomingMoveDefensiveStatValue
///
/// This function allows a script to change the defensive stat value of an incoming move.
///
- void ChangeIncomingMoveDefensiveStatValue(IExecutingMove executingMove, IPokemon target, byte hitNumber,
+ void ChangeIncomingMoveDefensiveStatValue(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber,
uint origOffensiveStat, StatisticSet targetStats, Statistic defensive, ref uint defensiveStat);
}
@@ -492,7 +492,7 @@ public interface IScriptChangeDamageStatModifier
/// This function allows a script to change the raw modifier we retrieved from the stats of the
/// defender and attacker. The default value is the offensive stat divided by the defensive stat.
///
- void ChangeDamageStatModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier);
+ void ChangeDamageStatModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
}
///
@@ -503,7 +503,7 @@ public interface IScriptChangeDamageModifier
///
/// This function allows a script to apply a raw multiplier to the damage done by a move.
///
- void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier);
+ void ChangeDamageModifier(IExecutingMove move, IBattlePokemon target, byte hit, ref float modifier);
}
///
@@ -514,7 +514,7 @@ public interface IScriptChangeIncomingMoveDamageModifier
///
/// This function allows a script to change the damage modifier of an incoming move.
///
- void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber,
+ void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IBattlePokemon target, byte hitNumber,
ref float modifier);
}
@@ -526,7 +526,7 @@ public interface IScriptChangeMoveDamage
///
/// This function allows a script to modify the outgoing damage done by a move.
///
- void ChangeMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage);
+ void ChangeMoveDamage(IExecutingMove move, IBattlePokemon target, byte hit, ref uint damage);
}
///
@@ -537,7 +537,7 @@ public interface IScriptChangeIncomingMoveDamage
///
/// This function allows a script to modify the incoming damage done by a move.
///
- void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage);
+ void ChangeIncomingMoveDamage(IExecutingMove move, IBattlePokemon target, byte hit, ref uint damage);
}
///
@@ -549,7 +549,8 @@ public interface IScriptPreventStatBoostChange
/// This function allows a script attached to a Pokemon or its parents to prevent stat boost
/// changes on that Pokemon.
///
- void PreventStatBoostChange(IPokemon target, Statistic stat, sbyte amount, bool selfInflicted, ref bool prevent);
+ void PreventStatBoostChange(IBattlePokemon target, Statistic stat, sbyte amount, bool selfInflicted,
+ ref bool prevent);
}
///
@@ -562,7 +563,7 @@ public interface IScriptChangeStatBoostChange
/// which the stat boost will change. If the stat boost is done by the user itself, self
/// inflicted will be true, otherwise it will be false.
///
- void ChangeStatBoostChange(IPokemon target, Statistic stat, bool selfInflicted, ref sbyte amount);
+ void ChangeStatBoostChange(IBattlePokemon target, Statistic stat, bool selfInflicted, ref sbyte amount);
}
///
@@ -573,7 +574,7 @@ public interface IScriptOnAfterStatBoostChange
///
/// This function allows a script to run after a stat boost change has been applied.
///
- void OnAfterStatBoostChange(IPokemon pokemon, Statistic stat, bool selfInflicted, sbyte change);
+ void OnAfterStatBoostChange(IBattlePokemon pokemon, Statistic stat, bool selfInflicted, sbyte change);
}
///
@@ -586,7 +587,7 @@ public interface IScriptPreventSecondaryEffect
/// This means the move will still hit and do damage, but not trigger its secondary effect. Note that this
/// function is not called for status moves.
///
- void PreventSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent);
+ void PreventSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit, ref bool prevent);
}
///
@@ -599,7 +600,7 @@ public interface IScriptPreventIncomingSecondaryEffect
/// secondary effect. This means the move will still hit and do damage, but not trigger its
/// secondary effect. Note that this function is not called for status moves.
///
- void PreventIncomingSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent);
+ void PreventIncomingSecondaryEffect(IExecutingMove move, IBattlePokemon target, byte hit, ref bool prevent);
}
///
@@ -613,7 +614,7 @@ public interface IScriptChangeEffectChance
/// changing this to above or equal to 100 will make it always hit, while setting it to equal or
/// below 0 will make it never hit.
///
- void ChangeEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance);
+ void ChangeEffectChance(IExecutingMove move, IBattlePokemon target, byte hit, ref float chance);
}
///
@@ -627,7 +628,7 @@ public interface IScriptChangeIncomingEffectChance
/// so changing this to above or equal to 100 will make it always hit, while setting it to equal
/// or below 0 will make it never hit.
///
- void ChangeIncomingEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance);
+ void ChangeIncomingEffectChance(IExecutingMove move, IBattlePokemon target, byte hit, ref float chance);
}
///
@@ -638,7 +639,7 @@ public interface IScriptOnAfterHits
///
/// This function triggers on a move or its parents when all hits on a target are finished.
///
- void OnAfterHits(IExecutingMove move, IPokemon target);
+ void OnAfterHits(IExecutingMove move, IBattlePokemon target);
}
///
@@ -706,7 +707,7 @@ public interface IScriptOnDamage
///
/// This function is triggered on a Pokemon and its parents when the given Pokemon takes damage.
///
- void OnDamage(IPokemon pokemon, DamageSource source, uint oldHealth, uint newHealth);
+ void OnDamage(IBattlePokemon pokemon, DamageSource source, uint oldHealth, uint newHealth);
}
///
@@ -717,7 +718,7 @@ public interface IScriptOnFaint
///
/// This function is triggered on a Pokemon and its parents when the given Pokemon faints.
///
- void OnFaint(IPokemon pokemon, DamageSource source);
+ void OnFaint(IBattlePokemon pokemon, DamageSource source);
}
///
@@ -728,7 +729,7 @@ public interface IScriptOnAllyFaint
///
/// This function is triggered on a Pokemon when an ally Pokemon faints.
///
- void OnAllyFaint(IPokemon ally, IPokemon faintedPokemon);
+ void OnAllyFaint(IBattlePokemon ally, IBattlePokemon faintedPokemon);
}
///
@@ -740,7 +741,7 @@ public interface IScriptOnSwitchOut
/// This function is triggered on a Pokemon and its parents when the given Pokemon switches out
/// of the battlefield.
///
- void OnSwitchOut(IPokemon oldPokemon, byte position);
+ void OnSwitchOut(IBattlePokemon oldPokemon, byte position);
}
///
@@ -752,7 +753,7 @@ public interface IScriptOnSwitchIn
/// This function is triggered on a Pokemon and its parents when the given Pokemon is switched into
/// the battlefield.
///
- void OnSwitchIn(IPokemon pokemon, byte position);
+ void OnSwitchIn(IBattlePokemon pokemon, byte position);
}
///
@@ -763,7 +764,7 @@ public interface IScriptOnOpponentSwitchIn
///
/// This function is triggered on a Pokemon and its parents when an opponent switches in.
///
- void OnOpponentSwitchIn(IPokemon pokemon, byte position);
+ void OnOpponentSwitchIn(IBattlePokemon pokemon, byte position);
}
///
@@ -787,7 +788,7 @@ public interface IScriptOnAfterItemConsume
/// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the
/// held item it had.
///
- void OnAfterItemConsume(IPokemon pokemon, IItem item);
+ void OnAfterItemConsume(IBattlePokemon pokemon, IItem item);
}
///
@@ -798,7 +799,7 @@ public interface IScriptBlockIncomingHit
///
/// This function allows a script to block an incoming hit.
///
- void BlockIncomingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block);
+ void BlockIncomingHit(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool block);
}
///
@@ -809,7 +810,7 @@ public interface IScriptBlockOutgoingHit
///
/// This function allows a script to block an outgoing hit.
///
- void BlockOutgoingHit(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool block);
+ void BlockOutgoingHit(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool block);
}
///
@@ -820,7 +821,7 @@ public interface IScriptPreventHeldItemConsume
///
/// This function allows a script to prevent a held item from being consumed.
///
- void PreventHeldItemConsume(IPokemon pokemon, IItem heldItem, ref bool prevented);
+ void PreventHeldItemConsume(IBattlePokemon pokemon, IItem heldItem, ref bool prevented);
}
///
@@ -831,7 +832,7 @@ public interface IScriptChangeIncomingDamage
///
/// This function allows a script to change any kind of damage that is incoming.
///
- void ChangeIncomingDamage(IPokemon pokemon, DamageSource source, ref uint damage);
+ void ChangeIncomingDamage(IBattlePokemon pokemon, DamageSource source, ref uint damage);
}
///
@@ -853,7 +854,7 @@ public interface IScriptPreventHeal
///
/// This function allows a script to prevent a Pokemon from being healed.
///
- void PreventHeal(IPokemon pokemon, uint heal, bool allowRevive, ref bool prevented);
+ void PreventHeal(IBattlePokemon pokemon, uint heal, bool allowRevive, ref bool prevented);
}
///
@@ -865,7 +866,8 @@ public interface IScriptChangeTypesForMove
/// This function allows a script to change the types a target has. Multiple types can be set, and will be used
/// for the effectiveness calculation.
///
- void ChangeTypesForMove(IExecutingMove executingMove, IPokemon target, byte hitIndex, IList types);
+ void ChangeTypesForMove(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
+ IList types);
}
///
@@ -877,7 +879,7 @@ public interface IScriptChangeTypesForIncomingMove
/// This function allows a script to change the types a Pokemon has for a move that's incoming. Multiple types can
/// be set, and will be used for the effectiveness calculation.
///
- void ChangeTypesForIncomingMove(IExecutingMove executingMove, IPokemon target, byte hitIndex,
+ void ChangeTypesForIncomingMove(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
IList types);
}
@@ -890,7 +892,7 @@ public interface IScriptChangeCategory
/// This function allows a script to change the handling of the move category. This is used for moves that
/// are sometimes a status move, and sometimes a damaging move, such as pollen puff.
///
- void ChangeCategory(IExecutingMove move, IPokemon target, byte hitIndex, ref MoveCategory category);
+ void ChangeCategory(IExecutingMove move, IBattlePokemon target, byte hitIndex, ref MoveCategory category);
}
///
@@ -901,7 +903,7 @@ public interface IScriptOnBeforeHit
///
/// Triggers first when we're about to hit a target.
///
- void OnBeforeHit(IExecutingMove move, IPokemon target, byte hitIndex);
+ void OnBeforeHit(IExecutingMove move, IBattlePokemon target, byte hitIndex);
}
///
@@ -912,7 +914,7 @@ public interface IScriptPreventStatusChange
///
/// This function allows a script to prevent a Pokemon from being affected by a status condition.
///
- void PreventStatusChange(IPokemon pokemon, StringKey status, bool selfInflicted, ref bool preventStatus);
+ void PreventStatusChange(IBattlePokemon pokemon, StringKey status, bool selfInflicted, ref bool preventStatus);
}
///
@@ -923,7 +925,7 @@ public interface IScriptOnAfterStatusChange
///
/// This function triggers after a status condition has been applied to a Pokemon.
///
- void OnAfterStatusChange(IPokemon pokemon, StringKey status, IPokemon? originPokemon);
+ void OnAfterStatusChange(IBattlePokemon pokemon, StringKey status, IPokemon? originPokemon);
}
///
@@ -946,7 +948,7 @@ public interface IScriptIsFloating
/// This function allows a script to make the Pokémon it is attached to float. This is used for moves
/// such as levitate, and allows for moves such as earthquake to not hit the Pokémon.
///
- void IsFloating(IPokemon pokemon, ref bool isFloating);
+ void IsFloating(IBattlePokemon pokemon, ref bool isFloating);
}
///
@@ -991,7 +993,7 @@ public interface IScriptModifyIsContact
///
/// Modifies whether a move is a contact move or not. This is used for abilities such as Long Reach.
///
- void ModifyIsContact(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref bool isContact);
+ void ModifyIsContact(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref bool isContact);
}
///
@@ -1002,7 +1004,7 @@ public interface IScriptPreventHeldItemSteal
///
/// This function allows a script to prevent a held item from being stolen by an effect such as Thief or Covet.
///
- void PreventHeldItemSteal(IPokemon pokemon, IItem heldItem, ref bool prevent);
+ void PreventHeldItemSteal(IBattlePokemon pokemon, IItem heldItem, ref bool prevent);
}
///
@@ -1013,7 +1015,7 @@ public interface IScriptOnAfterHeldItemChange
///
/// This function allows a script to run after a held item has changed.
///
- void OnAfterHeldItemChange(IPokemon pokemon, IItem? previous, IItem? item);
+ void OnAfterHeldItemChange(IBattlePokemon pokemon, IItem? previous, IItem? item);
}
///
@@ -1080,7 +1082,7 @@ public interface IScriptChangeExperienceGained
/// This function is triggered on a Pokemon and its parents when the given Pokemon gains experience,
/// and allows for changing this amount of experience.
///
- void ChangeExperienceGained(IPokemon faintedPokemon, IPokemon winningPokemon, ref uint amount);
+ void ChangeExperienceGained(IBattlePokemon faintedPokemon, IBattlePokemon winningPokemon, ref uint amount);
}
///
@@ -1093,7 +1095,8 @@ public interface IScriptShareExperience
/// and allows for making the experience be shared across multiple Pokemon.
/// Amount is the modifier for how much experience is shared, with 1 being the default amount.
///
- void ShareExperience(IPokemon faintedPokemon, IPokemon winningPokemon, ref bool share, ref float amount);
+ void ShareExperience(IBattlePokemon faintedPokemon, IBattlePokemon winningPokemon, ref bool share,
+ ref float amount);
}
///
@@ -1117,7 +1120,7 @@ public interface IScriptChangeCatchRateBonus
/// rate of this attempt. Pokeball modifier effects should be implemented here, as well as for
/// example status effects that change capture rates.
///
- void ChangeCatchRateBonus(IPokemon pokemon, IItem pokeball, ref byte modifier);
+ void ChangeCatchRateBonus(IBattlePokemon pokemon, IItem pokeball, ref byte modifier);
}
///
@@ -1142,7 +1145,7 @@ public interface IScriptChangeAccuracy
/// A custom case goes when 255 is returned, in which case the entire accuracy check is skipped, and the move
/// will always hit.
///
- void ChangeAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy);
+ void ChangeAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex, ref int modifiedAccuracy);
}
///
@@ -1155,5 +1158,6 @@ public interface IScriptChangeIncomingAccuracy
/// A custom case goes when 255 is returned, in which case the entire accuracy check is skipped, and the move
/// will always hit.
///
- void ChangeIncomingAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex, ref int modifiedAccuracy);
+ void ChangeIncomingAccuracy(IExecutingMove executingMove, IBattlePokemon target, byte hitIndex,
+ ref int modifiedAccuracy);
}
\ No newline at end of file
diff --git a/PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs b/PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs
new file mode 100644
index 0000000..2282aa8
--- /dev/null
+++ b/PkmnLib.Tests/Dynamic/BattleLifecycleTests.cs
@@ -0,0 +1,160 @@
+using PkmnLib.Dynamic.Libraries;
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Static;
+using PkmnLib.Static.Species;
+using PkmnLib.Tests.Integration;
+
+namespace PkmnLib.Tests.Dynamic;
+
+///
+/// Regression tests for the battle lifecycle of : battle-only state must die
+/// with the battle, and the underlying must come out of a battle unchanged except
+/// for the deliberately persistent parts (health, PP, non-volatile status, experience).
+///
+public class BattleLifecycleTests
+{
+ private static IPokemon CreatePokemon(IDynamicLibrary library, string speciesName)
+ {
+ if (!library.StaticLibrary.Species.TryGet(speciesName, out var species))
+ throw new InvalidOperationException($"Failed to load {speciesName} species.");
+ return new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
+ {
+ Index = 0,
+ IsHidden = false,
+ }, 50, 0, Gender.Male, 0, "hardy");
+ }
+
+ private static IBattle CreateBattle(IDynamicLibrary library, IPokemon own, IPokemon opponent,
+ bool isWildBattle = false, int seed = 0)
+ {
+ var party = new PokemonPartyImpl(1);
+ party.SwapInto(own, 0);
+ var opponentParty = new PokemonPartyImpl(1);
+ opponentParty.SwapInto(opponent, 0);
+ var parties = new[]
+ {
+ new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]),
+ new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]),
+ };
+ var battle = new BattleImpl(library, parties, false, 2, 1, isWildBattle, "grass", seed);
+ battle.Sides[0].SendOut(0, own);
+ battle.Sides[1].SendOut(0, opponent);
+ return battle;
+ }
+
+ ///
+ /// The original bug this refactor removes by construction: a captured Pokémon must not carry any
+ /// battle state into the party of its captor. The capture is reported through
+ /// as the persistent Pokémon.
+ ///
+ [Test]
+ public async Task Capture_ReportsPersistentPokemonAndLeavesItUsable()
+ {
+ var library = LibraryHelpers.LoadLibrary();
+ if (!library.StaticLibrary.Items.TryGet("master_ball", out var masterBall))
+ throw new InvalidOperationException("Failed to load master ball.");
+
+ // The capture roll is random; find a seed where the capture succeeds. The loop is deterministic,
+ // so the test always exercises the same battle.
+ var result = CaptureResult.Failed;
+ IBattle? battle = null;
+ IPokemon? wildPokemon = null;
+ for (var seed = 0; seed < 100 && !result.IsCaught; seed++)
+ {
+ battle?.Dispose();
+ var own = CreatePokemon(library, "bulbasaur");
+ wildPokemon = CreatePokemon(library, "caterpie");
+ battle = CreateBattle(library, own, wildPokemon, true, seed);
+ var wildBattlePokemon = battle.GetPokemon(1, 0)!;
+ wildBattlePokemon.Damage(wildBattlePokemon.CurrentHealth - 1, DamageSource.MoveDamage);
+ result = battle.AttempCapture(1, 0, masterBall);
+ }
+
+ await Assert.That(result.IsCaught).IsTrue();
+ await Assert.That(battle!.HasEnded).IsTrue();
+ await Assert.That(battle.Result!.Value.CapturedPokemon).Contains(wildPokemon!);
+
+ battle.Dispose();
+
+ // The persistent Pokémon left the battle without any battle state: it is usable and simply damaged.
+ await Assert.That(wildPokemon!.IsUsable).IsTrue();
+ await Assert.That(wildPokemon.CurrentHealth).IsEqualTo(1u);
+ }
+
+ ///
+ /// Items removed or stolen during a battle are a battle-only overlay: after the battle, the victim
+ /// still holds its item.
+ ///
+ [Test]
+ public async Task StolenHeldItem_IsRestoredAfterBattle()
+ {
+ var library = LibraryHelpers.LoadLibrary();
+ if (!library.StaticLibrary.Items.TryGet("oran_berry", out var berry))
+ throw new InvalidOperationException("Failed to load oran berry.");
+ var own = CreatePokemon(library, "bulbasaur");
+ _ = own.ForceSetHeldItem(berry);
+ var opponent = CreatePokemon(library, "charmander");
+
+ var battle = CreateBattle(library, own, opponent);
+ var battlePokemon = battle.GetPokemon(0, 0)!;
+ await Assert.That(battlePokemon.TryStealHeldItem(out _)).IsTrue();
+ await Assert.That(battlePokemon.HeldItem).IsNull();
+
+ battle.Dispose();
+
+ await Assert.That(own.HeldItem).IsSameReferenceAs(berry);
+ }
+
+ ///
+ /// A battle-only form (such as a mega evolution) reverts to the original form when the battle ends.
+ ///
+ [Test]
+ public async Task BattleOnlyForm_RevertsWhenBattleEnds()
+ {
+ var library = LibraryHelpers.LoadLibrary();
+ var own = CreatePokemon(library, "absol");
+ var originalForm = own.Form;
+ if (!own.Species.TryGetForm("mega", out var megaForm))
+ throw new InvalidOperationException("Absol has no mega form.");
+ await Assert.That(megaForm.IsBattleOnlyForm).IsTrue();
+ var opponent = CreatePokemon(library, "charmander");
+
+ var battle = CreateBattle(library, own, opponent);
+ var battlePokemon = battle.GetPokemon(0, 0)!;
+ battlePokemon.ChangeForm(megaForm);
+ await Assert.That(own.Form).IsSameReferenceAs(megaForm);
+
+ battle.Dispose();
+
+ await Assert.That(own.Form).IsSameReferenceAs(originalForm);
+ }
+
+ ///
+ /// A second battle with the same party starts with fresh battle state: no seen opponents or stale
+ /// original species from the previous battle.
+ ///
+ [Test]
+ public async Task SecondBattleWithSameParty_StartsWithFreshBattleState()
+ {
+ var library = LibraryHelpers.LoadLibrary();
+ var own = CreatePokemon(library, "bulbasaur");
+ var opponent1 = CreatePokemon(library, "charmander");
+
+ var battle1 = CreateBattle(library, own, opponent1);
+ var firstWrapper = battle1.GetPokemon(0, 0)!;
+ await Assert.That(firstWrapper.SeenOpponents.Count).IsEqualTo(1);
+ firstWrapper.ChangeStatBoost(Statistic.Attack, 3, true, false);
+ battle1.Dispose();
+
+ var opponent2 = CreatePokemon(library, "squirtle");
+ var battle2 = CreateBattle(library, own, opponent2);
+ var secondWrapper = battle2.GetPokemon(0, 0)!;
+
+ await Assert.That(secondWrapper).IsNotSameReferenceAs(firstWrapper);
+ await Assert.That(secondWrapper.SeenOpponents.Count).IsEqualTo(1);
+ await Assert.That(secondWrapper.SeenOpponents[0].UnderlyingPokemon).IsSameReferenceAs(opponent2);
+ await Assert.That(secondWrapper.StatBoost.Attack).IsEqualTo((sbyte)0);
+ await Assert.That(secondWrapper.OriginalSpecies).IsSameReferenceAs(own.Species);
+ battle2.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/PkmnLib.Tests/Dynamic/BattlePokemonTests.cs b/PkmnLib.Tests/Dynamic/BattlePokemonTests.cs
new file mode 100644
index 0000000..4cdbc36
--- /dev/null
+++ b/PkmnLib.Tests/Dynamic/BattlePokemonTests.cs
@@ -0,0 +1,219 @@
+using PkmnLib.Dynamic.Models;
+using PkmnLib.Static;
+using PkmnLib.Static.Species;
+using PkmnLib.Tests.Integration;
+
+namespace PkmnLib.Tests.Dynamic;
+
+///
+/// Tests for the ephemeral wrapper. Battle-only state must live on the wrapper
+/// and never leak into the underlying ; dropping the wrapper is all the cleanup a
+/// battle needs.
+///
+public class BattlePokemonTests
+{
+ private static (IBattle battle, IBattlePokemon wrapper, IPokemon inner) CreateBattleWithWrapper(
+ string heldItem = "oran_berry")
+ {
+ var library = LibraryHelpers.LoadLibrary();
+ if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var bulbasaur))
+ throw new InvalidOperationException("Failed to load bulbasaur species.");
+ if (!library.StaticLibrary.Species.TryGet("charmander", out var charmander))
+ throw new InvalidOperationException("Failed to load charmander species.");
+ if (!library.StaticLibrary.Items.TryGet(heldItem, out var item))
+ throw new InvalidOperationException($"Failed to load item {heldItem}.");
+
+ var pokemon = new PokemonImpl(library, bulbasaur, bulbasaur.GetDefaultForm(), new AbilityIndex
+ {
+ Index = 0,
+ IsHidden = false,
+ }, 50, 0, Gender.Male, 0, "hardy");
+ pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0);
+ _ = pokemon.ForceSetHeldItem(item);
+
+ var opponent = new PokemonImpl(library, charmander, charmander.GetDefaultForm(), new AbilityIndex
+ {
+ Index = 0,
+ IsHidden = false,
+ }, 50, 0, Gender.Male, 0, "hardy");
+
+ var party1 = new PokemonPartyImpl(6);
+ party1.SwapInto(pokemon, 0);
+ var party2 = new PokemonPartyImpl(6);
+ party2.SwapInto(opponent, 0);
+ var parties = new[]
+ {
+ new BattlePartyImpl(party1, [new ResponsibleIndex(0, 0)]),
+ new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
+ };
+ var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0);
+ var wrapper = parties[0].GetBattlePokemon(pokemon) ??
+ throw new InvalidOperationException("Wrapper not created.");
+ return (battle, wrapper, pokemon);
+ }
+
+ [Test]
+ public async Task PersistentMembersAreProxiedToUnderlyingPokemon()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+
+ await Assert.That(wrapper.UnderlyingPokemon).IsSameReferenceAs(inner);
+ await Assert.That(wrapper.Species).IsSameReferenceAs(inner.Species);
+ await Assert.That(wrapper.Form).IsSameReferenceAs(inner.Form);
+ await Assert.That(wrapper.Level).IsEqualTo(inner.Level);
+ await Assert.That(wrapper.CurrentHealth).IsEqualTo(inner.CurrentHealth);
+ await Assert.That(wrapper.Nature).IsSameReferenceAs(inner.Nature);
+ await Assert.That(wrapper.OriginalSpecies).IsSameReferenceAs(inner.Species);
+ await Assert.That(wrapper.OriginalForm).IsSameReferenceAs(inner.Form);
+ }
+
+ [Test]
+ public async Task TypeOverlayDoesNotTouchUnderlyingPokemon()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+ var originalTypes = inner.Types.ToList();
+
+ wrapper.SetTypes([new TypeIdentifier(18, "water")]);
+
+ await Assert.That(wrapper.Types.Count).IsEqualTo(1);
+ await Assert.That(inner.Types.SequenceEqual(originalTypes)).IsTrue();
+
+ wrapper.OnSwitchedOut();
+ await Assert.That(wrapper.Types.SequenceEqual(inner.Form.Types)).IsTrue();
+ }
+
+ [Test]
+ public async Task TemporaryMoveOverlayDoesNotTouchUnderlyingPokemonAndResetsOnSwitchOut()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+
+ wrapper.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
+
+ await Assert.That(wrapper.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
+ await Assert.That(wrapper.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
+ await Assert.That(inner.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
+
+ wrapper.OnSwitchedOut();
+ await Assert.That(wrapper.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
+ }
+
+ [Test]
+ public async Task StatBoostLivesOnWrapperAndResetsOnSwitchOut()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+ var innerFlatAttack = inner.FlatStats.Attack;
+
+ var changed = wrapper.ChangeStatBoost(Statistic.Attack, 2, true, false);
+
+ await Assert.That(changed).IsTrue();
+ await Assert.That(wrapper.StatBoost.Attack).IsEqualTo((sbyte)2);
+ await Assert.That(wrapper.BoostedStats.Attack > wrapper.FlatStats.Attack).IsTrue();
+ await Assert.That(inner.FlatStats.Attack).IsEqualTo(innerFlatAttack);
+
+ wrapper.OnSwitchedOut();
+ await Assert.That(wrapper.StatBoost.Attack).IsEqualTo((sbyte)0);
+ await Assert.That(wrapper.BoostedStats.Attack).IsEqualTo(wrapper.FlatStats.Attack);
+ }
+
+ [Test]
+ public async Task HeldItemOverlayDoesNotTouchUnderlyingPokemon()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+ var innerItem = inner.HeldItem;
+ await Assert.That(innerItem).IsNotNull();
+
+ var removed = wrapper.RemoveHeldItemForBattle();
+
+ await Assert.That(removed).IsSameReferenceAs(innerItem);
+ await Assert.That(wrapper.HeldItem).IsNull();
+ await Assert.That(wrapper.HasItemBeenRemovedForBattle).IsTrue();
+ await Assert.That(inner.HeldItem).IsSameReferenceAs(innerItem);
+
+ wrapper.RestoreRemovedHeldItem();
+ await Assert.That(wrapper.HeldItem).IsSameReferenceAs(innerItem);
+ await Assert.That(wrapper.HasItemBeenRemovedForBattle).IsFalse();
+ }
+
+ [Test]
+ public async Task StealingHeldItemDoesNotTouchUnderlyingPokemon()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+ var innerItem = inner.HeldItem;
+
+ var stolen = wrapper.TryStealHeldItem(out var item);
+
+ await Assert.That(stolen).IsTrue();
+ await Assert.That(item).IsSameReferenceAs(innerItem);
+ await Assert.That(wrapper.HeldItem).IsNull();
+ await Assert.That(inner.HeldItem).IsSameReferenceAs(innerItem);
+ }
+
+ [Test]
+ public async Task MarkAsCaughtOnlyAffectsWrapper()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+
+ wrapper.MarkAsCaught();
+
+ await Assert.That(wrapper.IsCaught).IsTrue();
+ await Assert.That(wrapper.IsUsable).IsFalse();
+ await Assert.That(inner.IsUsable).IsTrue();
+ }
+
+ [Test]
+ public async Task AbilityOverrideAndSuppressionLiveOnWrapper()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+ var naturalAbility = inner.Ability;
+ await Assert.That(naturalAbility).IsNotNull();
+
+ wrapper.SuppressAbility();
+ await Assert.That(wrapper.AbilitySuppressed).IsTrue();
+ await Assert.That(wrapper.ActiveAbility).IsNull();
+ await Assert.That(inner.Ability).IsSameReferenceAs(naturalAbility);
+
+ wrapper.OnSwitchedOut();
+ await Assert.That(wrapper.AbilitySuppressed).IsFalse();
+ await Assert.That(wrapper.ActiveAbility).IsSameReferenceAs(naturalAbility);
+ }
+
+ [Test]
+ public async Task GetOwnScriptsUsesUnderlyingStatusScript()
+ {
+ var (_, wrapper, inner) = CreateBattleWithWrapper();
+
+ var scripts = new List>();
+ wrapper.GetOwnScripts(scripts);
+
+ await Assert.That(scripts.Count).IsEqualTo(4);
+ await Assert.That(scripts[0]).IsSameReferenceAs(wrapper.HeldItemTriggerScript);
+ await Assert.That(scripts[1]).IsSameReferenceAs(wrapper.AbilityScript);
+ await Assert.That(scripts[2]).IsSameReferenceAs(inner.StatusScript);
+ await Assert.That(scripts[3]).IsSameReferenceAs(wrapper.Volatile);
+ }
+
+ [Test]
+ public async Task GetBattlePokemonFindsWrapperByInnerAndByWrapper()
+ {
+ var (battle, wrapper, inner) = CreateBattleWithWrapper();
+ var party = battle.Parties[0];
+
+ await Assert.That(party.GetBattlePokemon(inner)).IsSameReferenceAs(wrapper);
+ await Assert.That(party.GetBattlePokemon(wrapper)).IsSameReferenceAs(wrapper);
+ await Assert.That(battle.Parties[1].GetBattlePokemon(inner)).IsNull();
+ }
+
+ [Test]
+ public async Task OnSwitchedInTracksTurnAndPosition()
+ {
+ var (_, wrapper, _) = CreateBattleWithWrapper();
+ await Assert.That(wrapper.IsOnBattlefield).IsFalse();
+
+ wrapper.OnSwitchedIn(0);
+
+ await Assert.That(wrapper.IsOnBattlefield).IsTrue();
+ await Assert.That(wrapper.Position).IsEqualTo((byte)0);
+ // Bulbasaur's ability (Overgrow) has no script in the Gen7 plugin, so the ability script stays empty.
+ await Assert.That(wrapper.AbilityScript.IsEmpty).IsTrue();
+ }
+}
\ No newline at end of file
diff --git a/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs b/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs
index a11f5b4..b4d481b 100644
--- a/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs
+++ b/PkmnLib.Tests/Dynamic/ChoiceQueueTests.cs
@@ -8,8 +8,8 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_HighSpeedFirstWhenPriorityEqual()
{
- var pokemon1 = Substitute.For();
- var pokemon2 = Substitute.For();
+ var pokemon1 = Substitute.For();
+ var pokemon2 = Substitute.For();
var choice1 = Substitute.For();
choice1.User.Returns(pokemon1);
@@ -28,8 +28,8 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_HighPriorityFirst()
{
- var pokemon1 = Substitute.For();
- var pokemon2 = Substitute.For();
+ var pokemon1 = Substitute.For();
+ var pokemon2 = Substitute.For();
var choice1 = Substitute.For();
choice1.User.Returns(pokemon1);
@@ -48,10 +48,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceNext()
{
- var pokemon1 = Substitute.For();
- var pokemon2 = Substitute.For();
- var pokemon3 = Substitute.For();
- var pokemon4 = Substitute.For();
+ var pokemon1 = Substitute.For();
+ var pokemon2 = Substitute.For();
+ var pokemon3 = Substitute.For();
+ var pokemon4 = Substitute.For();
var choice1 = Substitute.For();
choice1.User.Returns(pokemon1);
@@ -75,10 +75,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceNextFailsIfAlreadyExecuted()
{
- var pokemon1 = Substitute.For();
- var pokemon2 = Substitute.For();
- var pokemon3 = Substitute.For();
- var pokemon4 = Substitute.For();
+ var pokemon1 = Substitute.For();
+ var pokemon2 = Substitute.For();
+ var pokemon3 = Substitute.For();
+ var pokemon4 = Substitute.For();
var choice1 = Substitute.For();
choice1.User.Returns(pokemon1);
@@ -103,10 +103,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceLast()
{
- var pokemon1 = Substitute.For();
- var pokemon2 = Substitute.For();
- var pokemon3 = Substitute.For();
- var pokemon4 = Substitute.For();
+ var pokemon1 = Substitute.For();
+ var pokemon2 = Substitute.For();
+ var pokemon3 = Substitute.For();
+ var pokemon4 = Substitute.For();
var choice1 = Substitute.For();
choice1.User.Returns(pokemon1);
@@ -133,10 +133,10 @@ public class ChoiceQueueTests
[Test]
public async Task ChoiceQueue_MovePokemonChoiceLastFailsIfAlreadyExecuted()
{
- var pokemon1 = Substitute.For();
- var pokemon2 = Substitute.For();
- var pokemon3 = Substitute.For();
- var pokemon4 = Substitute.For();
+ var pokemon1 = Substitute.For();
+ var pokemon2 = Substitute.For();
+ var pokemon3 = Substitute.For();
+ var pokemon4 = Substitute.For();
var choice1 = Substitute.For();
choice1.User.Returns(pokemon1);
diff --git a/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs b/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs
index 911d89a..7a8d346 100644
--- a/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs
+++ b/PkmnLib.Tests/Dynamic/PokemonStatBoostTests.cs
@@ -7,16 +7,31 @@ namespace PkmnLib.Tests.Dynamic;
public class PokemonStatBoostTests
{
- private static IPokemon CreatePokemon()
+ private static IBattlePokemon CreatePokemon()
{
var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species))
throw new InvalidOperationException("Failed to load bulbasaur species.");
- return new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
+ var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
{
Index = 0,
IsHidden = false,
}, 50, 0, Gender.Male, 0, "hardy");
+ var party = new PokemonPartyImpl(1);
+ party.SwapInto(pokemon, 0);
+ var opponentParty = new PokemonPartyImpl(1);
+ opponentParty.SwapInto(new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
+ {
+ Index = 0,
+ IsHidden = false,
+ }, 50, 0, Gender.Male, 0, "hardy"), 0);
+ var parties = new[]
+ {
+ new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]),
+ new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]),
+ };
+ var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0);
+ return battle.Parties[0].GetBattlePokemon(pokemon)!;
}
[Test]
diff --git a/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs b/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs
index 3deec7c..4e136cd 100644
--- a/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs
+++ b/PkmnLib.Tests/Dynamic/PokemonTemporaryMoveTests.cs
@@ -6,65 +6,78 @@ using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic;
///
-/// Tests for the temporary move overlay (), used by effects such as
-/// Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay must be
-/// discarded by the engine itself when the Pokemon leaves the battlefield.
+/// Tests for the temporary move overlay (), used by effects
+/// such as Mimic. The permanently learned moves must never be mutated by a temporary move, and the overlay
+/// must be discarded by the engine itself when the Pokemon leaves the battlefield.
///
public class PokemonTemporaryMoveTests
{
- private static IPokemon CreatePokemon()
+ private static (IBattle battle, IBattlePokemon battlePokemon, IPokemon pokemon) CreateBattlePokemon()
{
var library = LibraryHelpers.LoadLibrary();
if (!library.StaticLibrary.Species.TryGet("bulbasaur", out var species))
throw new InvalidOperationException("Failed to load bulbasaur species.");
- var pokemon = new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
- {
- Index = 0,
- IsHidden = false,
- }, 50, 0, Gender.Male, 0, "hardy");
+
+ IPokemon CreateBulbasaur() =>
+ new PokemonImpl(library, species, species.GetDefaultForm(), new AbilityIndex
+ {
+ Index = 0,
+ IsHidden = false,
+ }, 50, 0, Gender.Male, 0, "hardy");
+
+ var pokemon = CreateBulbasaur();
pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 0);
- return pokemon;
+ var party = new PokemonPartyImpl(1);
+ party.SwapInto(pokemon, 0);
+ var opponentParty = new PokemonPartyImpl(1);
+ opponentParty.SwapInto(CreateBulbasaur(), 0);
+ var parties = new[]
+ {
+ new BattlePartyImpl(party, [new ResponsibleIndex(0, 0)]),
+ new BattlePartyImpl(opponentParty, [new ResponsibleIndex(1, 0)]),
+ };
+ var battle = new BattleImpl(library, parties, false, 2, 1, false, "grass", 0);
+ return (battle, battle.Parties[0].GetBattlePokemon(pokemon)!, pokemon);
}
[Test]
public async Task LearnTemporaryMove_ReplacesMoveInMovesButNotInBaseMoves()
{
- var pokemon = CreatePokemon();
+ var (_, battlePokemon, pokemon) = CreateBattlePokemon();
- pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
+ battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
- await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
- await Assert.That(pokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic);
- await Assert.That(pokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
+ await Assert.That(battlePokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("swords_dance");
+ await Assert.That(battlePokemon.Moves[0]!.LearnMethod).IsEqualTo(MoveLearnMethod.Mimic);
+ await Assert.That(battlePokemon.BaseMoves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
+ await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
}
[Test]
- public async Task SetOnBattlefield_LeavingField_RestoresOriginalMoveWithItsPP()
+ public async Task OnSwitchedOut_RestoresOriginalMoveWithItsPP()
{
- var pokemon = CreatePokemon();
- pokemon.SetBattleData(Substitute.For(), 0);
- pokemon.SetOnBattlefield(true);
+ var (_, battlePokemon, _) = CreateBattlePokemon();
+ battlePokemon.OnSwitchedIn(0);
// Use the original move once, so we can verify its PP survives the temporary replacement untouched.
- var originalMove = pokemon.Moves[0]!;
+ var originalMove = battlePokemon.Moves[0]!;
originalMove.TryUse();
var expectedPp = originalMove.CurrentPp;
- pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
- pokemon.SetOnBattlefield(false);
+ battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
+ battlePokemon.OnSwitchedOut();
- await Assert.That(ReferenceEquals(pokemon.Moves[0], originalMove)).IsTrue();
- await Assert.That(pokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp);
+ await Assert.That(ReferenceEquals(battlePokemon.Moves[0], originalMove)).IsTrue();
+ await Assert.That(battlePokemon.Moves[0]!.CurrentPp).IsEqualTo(expectedPp);
}
[Test]
- public async Task ClearBattleData_RestoresOriginalMove()
+ public async Task BattleEnd_LeavesOriginalMovesUntouched()
{
- var pokemon = CreatePokemon();
- pokemon.SetBattleData(Substitute.For(), 0);
- pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
+ var (battle, battlePokemon, pokemon) = CreateBattlePokemon();
+ battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
- pokemon.ClearBattleData();
+ battle.Dispose();
await Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString()).IsEqualTo("tackle");
}
@@ -72,10 +85,10 @@ public class PokemonTemporaryMoveTests
[Test]
public async Task Serialize_WithActiveTemporaryMove_WritesOriginalMove()
{
- var pokemon = CreatePokemon();
- pokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
+ var (_, battlePokemon, _) = CreateBattlePokemon();
+ battlePokemon.LearnTemporaryMove("swords_dance", MoveLearnMethod.Mimic, 0);
- var serialized = pokemon.Serialize();
+ var serialized = battlePokemon.Serialize();
await Assert.That(serialized.Moves[0]!.MoveName.ToString()).IsEqualTo("tackle");
}
diff --git a/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs b/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs
index 9e97c20..1ad02a5 100644
--- a/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs
+++ b/PkmnLib.Tests/Integration/Models/IntegrationTestAction.cs
@@ -22,7 +22,7 @@ public class SetPokemonAction : IntegrationTestAction
public override Task Execute(IBattle battle)
{
- var mon = battle.Parties[FromParty[0]].Party[FromParty[1]];
+ var mon = battle.Parties[FromParty[0]].BattlePokemon[FromParty[1]];
battle.Sides[Place[0]].SwapPokemon(Place[1], mon);
Console.WriteLine($"Set: {mon} to place {Place[0]}:{Place[1]}");
return Task.CompletedTask;
diff --git a/PkmnLib.Tests/Static/DeepCloneTests.cs b/PkmnLib.Tests/Static/DeepCloneTests.cs
index c1d158c..3014400 100644
--- a/PkmnLib.Tests/Static/DeepCloneTests.cs
+++ b/PkmnLib.Tests/Static/DeepCloneTests.cs
@@ -110,11 +110,12 @@ public class DeepCloneTests
new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
};
using var battle = new BattleImpl(library, parties, false, 2, 3, false, "grass", 0);
- battle.Sides[0].SwapPokemon(0, party1[0]);
- battle.Sides[1].SwapPokemon(0, party2[0]);
- party1[0]!.ChangeStatBoost(Statistic.Defense, 2, true, false);
- await Assert.That(party1[0]!.StatBoost.Defense).IsEqualTo((sbyte)2);
- party1[0]!.Volatile.Add(new ChargeBounceEffect(party1[0]!));
+ battle.Sides[0].SendOut(0, party1[0]!);
+ battle.Sides[1].SendOut(0, party2[0]!);
+ var battlePokemon1 = parties[0].GetBattlePokemon(party1[0]!)!;
+ battlePokemon1.ChangeStatBoost(Statistic.Defense, 2, true, false);
+ await Assert.That(battlePokemon1.StatBoost.Defense).IsEqualTo((sbyte)2);
+ battlePokemon1.Volatile.Add(new ChargeBounceEffect(battlePokemon1));
var clone = battle.DeepClone();
await Assert.That(clone).IsNotEqualTo(battle);
@@ -129,11 +130,10 @@ public class DeepCloneTests
var pokemon = clone.Sides[0].Pokemon[0]!;
await Assert.That(pokemon).IsNotNull();
await Assert.That(pokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!);
- await Assert.That(pokemon.BattleData).IsNotNull();
- await Assert.That(pokemon.BattleData).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.BattleData!);
- await Assert.That(pokemon.BattleData!.Battle).IsEqualTo((IBattle)clone);
- await Assert.That(pokemon.BattleData!.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
- await Assert.That(pokemon.BattleData!.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
+ await Assert.That(pokemon.UnderlyingPokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.UnderlyingPokemon);
+ await Assert.That(pokemon.Battle).IsEqualTo((IBattle)clone);
+ await Assert.That(pokemon.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
+ await Assert.That(pokemon.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2);
await Assert.That(pokemon.Volatile.Get()).IsNotNull();
await Assert.That(pokemon.Volatile.Get()).IsNotEqualTo(
@@ -142,7 +142,7 @@ public class DeepCloneTests
var ownerGetter =
typeof(ChargeBounceEffect).GetField("_owner", BindingFlags.NonPublic | BindingFlags.Instance)!;
var owner = ownerGetter.GetValue(pokemon.Volatile.Get()!);
- await Assert.That((IPokemon)owner!).IsEqualTo(pokemon);
+ await Assert.That((IBattlePokemon)owner!).IsEqualTo(pokemon);
pokemon.Volatile.Remove();
await Assert.That(pokemon.Volatile.Get()).IsNull();
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs
index 3ed9f1e..dd1f0bc 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/DamageCalculatorTests.cs
@@ -23,7 +23,7 @@ public class DamageCalculatorTests
[Test]
public async Task BulbapediaExampleDamageTest()
{
- var attacker = Substitute.For();
+ var attacker = Substitute.For();
// Imagine a level 75 Glaceon
attacker.Level.Returns((byte)75);
// with an effective Attack stat of 123
@@ -31,7 +31,7 @@ public class DamageCalculatorTests
// We use 10 as the Ice type
attacker.Types.Returns([new TypeIdentifier(10, "ice")]);
- var defender = Substitute.For();
+ var defender = Substitute.For();
// a Garchomp with an effective Defense stat of 163
defender.BoostedStats.Returns(new StatisticSet(1, 1, 163, 1, 1, 1));
defender.GetScripts().Returns(new ScriptIterator([]));
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs
index 43c47b7..0d47c06 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/AftermathTests.cs
@@ -14,12 +14,12 @@ public class AftermathTests
///
/// Creates a fully mocked test setup for Aftermath tests.
///
- private static (Aftermath aftermath, IExecutingMove move, IPokemon target, IPokemon user, EventHook eventHook,
- IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100)
+ private static (Aftermath aftermath, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, EventHook
+ eventHook, IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100)
{
var aftermath = new Aftermath();
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
var hitData = Substitute.For();
hitData.IsContact.Returns(isContact);
move.GetHitData(target, 0).Returns(hitData);
@@ -30,20 +30,14 @@ public class AftermathTests
// Setup empty sides by default (no Damp on field)
var side = Substitute.For();
- side.Pokemon.Returns(new List());
+ side.Pokemon.Returns(new List());
battle.Sides.Returns(new[] { side });
- var battleData = Substitute.For();
- battleData.Battle.Returns(battle);
-
- var user = Substitute.For();
+ var user = Substitute.For();
user.IsUsable.Returns(true);
+ user.Battle.Returns(battle);
user.MaxHealth.Returns(userMaxHealth);
- // Configure BattleData return value using NSubstitute's callback pattern
- // First return null to suppress auto-substitution, then configure actual value
- user.BattleData.Returns(battleData);
-
move.User.Returns(user);
return (aftermath, move, target, user, eventHook, battle);
@@ -52,7 +46,7 @@ public class AftermathTests
///
/// Helper to extract the damage amount from a substitute's received Damage calls.
///
- private static uint GetDamageDealt(IPokemon user)
+ private static uint GetDamageDealt(IBattlePokemon user)
{
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return damageCall != null ? (uint)damageCall.GetArguments()[0]! : 0;
@@ -61,7 +55,7 @@ public class AftermathTests
///
/// Helper to extract the damage source from a substitute's received Damage calls.
///
- private static DamageSource? GetDamageSource(IPokemon user)
+ private static DamageSource? GetDamageSource(IBattlePokemon user)
{
var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return damageCall != null ? (DamageSource)damageCall.GetArguments()[1]! : null;
@@ -151,12 +145,12 @@ public class AftermathTests
// Arrange
var aftermath = new Aftermath();
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
var hitData = Substitute.For();
hitData.IsContact.Returns(true);
move.GetHitData(target, 0).Returns(hitData);
- var user = Substitute.For();
+ var user = Substitute.For();
user.IsUsable.Returns(false); // Attacker already fainted
move.User.Returns(user);
@@ -168,34 +162,6 @@ public class AftermathTests
await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
}
- ///
- /// Bulbapedia: "the attacking Pokémon takes damage".
- /// If the attacker has no battle data, no damage should be dealt.
- ///
- [Test]
- public async Task OnFaint_AttackerHasNoBattleData_DoesNotDealDamage()
- {
- // Arrange
- var aftermath = new Aftermath();
- var move = Substitute.For();
- var target = Substitute.For();
- var hitData = Substitute.For();
- hitData.IsContact.Returns(true);
- move.GetHitData(target, 0).Returns(hitData);
-
- var user = Substitute.For();
- user.IsUsable.Returns(true);
- user.BattleData.Returns((IPokemonBattleData?)null);
- move.User.Returns(user);
-
- // Act
- aftermath.OnIncomingHit(move, target, 0);
- aftermath.OnFaint(target, DamageSource.MoveDamage);
-
- // Assert - Damage should never be called because BattleData is null
- await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
- }
-
///
/// Technical test: Verifies OnFaint handles the case where no attack was received.
///
@@ -204,7 +170,7 @@ public class AftermathTests
{
// Arrange
var aftermath = new Aftermath();
- var target = Substitute.For();
+ var target = Substitute.For();
// Act & Assert - Should not throw when _lastAttack is null
aftermath.OnFaint(target, DamageSource.MoveDamage);
@@ -220,19 +186,16 @@ public class AftermathTests
{
// Arrange
var aftermath = new Aftermath();
- var target = Substitute.For();
+ var target = Substitute.For();
var battle = Substitute.For();
var eventHook = new EventHook();
battle.EventHook.Returns(eventHook);
// Setup empty sides (no Damp on field)
var side = Substitute.For();
- side.Pokemon.Returns(new List());
+ side.Pokemon.Returns(new List());
battle.Sides.Returns(new[] { side });
- var battleData = Substitute.For();
- battleData.Battle.Returns(battle);
-
// First hit - non-contact
var move1 = Substitute.For();
var hitData1 = Substitute.For();
@@ -244,10 +207,10 @@ public class AftermathTests
var hitData2 = Substitute.For();
hitData2.IsContact.Returns(true);
move2.GetHitData(target, 0).Returns(hitData2);
- var user = Substitute.For();
+ var user = Substitute.For();
user.IsUsable.Returns(true);
- user.BattleData.Returns(battleData);
user.MaxHealth.Returns(100u);
+ user.Battle.Returns(battle);
move2.User.Returns(user);
// Act
@@ -268,7 +231,7 @@ public class AftermathTests
{
// Arrange
var aftermath = new Aftermath();
- var target = Substitute.For();
+ var target = Substitute.For();
// First hit - contact
var move1 = Substitute.For();
@@ -346,14 +309,14 @@ public class AftermathTests
var (aftermath, move, target, user, _, battle) = CreateFullTestSetup(true, 100);
// Create a Pokemon with Damp ability on the field
- var dampPokemon = Substitute.For();
+ var dampPokemon = Substitute.For();
var dampAbility = Substitute.For();
dampAbility.Name.Returns(new StringKey("damp"));
dampPokemon.ActiveAbility.Returns(dampAbility);
// Update battle sides to include the Damp Pokemon
var side = Substitute.For();
- side.Pokemon.Returns(new List { dampPokemon });
+ side.Pokemon.Returns(new List { dampPokemon });
battle.Sides.Returns(new[] { side });
// Act
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs
index cc7215d..1084e1e 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Abilities/MegaLauncherTests.cs
@@ -16,9 +16,9 @@ public class MegaLauncherTests
{
// Arrange
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
var healPercent = 0.5f;
- var user = Substitute.For();
+ var user = Substitute.For();
move.User.Returns(user);
move.UseMove.Category.Returns(MoveCategory.Special);
move.UseMove.HasFlag(MoveFlags.Pulse).Returns(true);
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs
index 1575224..25822aa 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireGrassPledgeMoveTests.cs
@@ -22,7 +22,7 @@ public class FireGrassPledgeMoveTests
/// Creates a fully mocked test setup. The target's battle side gets a real as
/// its volatile script set, so the sea of fire created by the combined move can be inspected and driven.
///
- private static (FireGrassPledgeMove script, IExecutingMove move, IPokemon target, IBattleSide side, IScriptSet
+ private static (FireGrassPledgeMove script, IExecutingMove move, IBattlePokemon target, IBattleSide side, IScriptSet
sideVolatile) CreateTestSetup()
{
var script = new FireGrassPledgeMove();
@@ -33,11 +33,9 @@ public class FireGrassPledgeMoveTests
var sideVolatile = new ScriptSet(side);
side.VolatileScripts.Returns(sideVolatile);
- var battleData = Substitute.For();
- battleData.BattleSide.Returns(side);
- var target = Substitute.For();
- target.BattleData.Returns(battleData);
+ var target = Substitute.For();
+ target.BattleSide.Returns(side);
return (script, move, target, side, sideVolatile);
}
@@ -45,9 +43,9 @@ public class FireGrassPledgeMoveTests
/// Creates a Pokémon substitute of the given type with the given maximum HP, for placing on the side
/// covered by the sea of fire.
///
- private static IPokemon CreateSidePokemon(string typeName, uint maxHealth)
+ private static IBattlePokemon CreateSidePokemon(string typeName, uint maxHealth)
{
- var pokemon = Substitute.For();
+ var pokemon = Substitute.For();
pokemon.Types.Returns(new List { new(1, typeName) });
pokemon.MaxHealth.Returns(maxHealth);
return pokemon;
@@ -56,7 +54,7 @@ public class FireGrassPledgeMoveTests
///
/// Helper to count the Damage calls a Pokémon received.
///
- private static int CountDamageCalls(IPokemon pokemon) =>
+ private static int CountDamageCalls(IBattlePokemon pokemon) =>
pokemon.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage");
///
@@ -112,24 +110,6 @@ public class FireGrassPledgeMoveTests
await Assert.That(sideVolatile.Get()).IsNotNull();
}
- ///
- /// Technical test: if the target has no , no sea of fire is created
- /// and the script does not throw.
- ///
- [Test]
- public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNothing()
- {
- // Arrange
- var (script, move, target, _, sideVolatile) = CreateTestSetup();
- target.BattleData.Returns((IPokemonBattleData?)null);
-
- // Act
- script.OnSecondaryEffect(move, target, 0);
-
- // Assert
- await Assert.That(sideVolatile.Count).IsEqualTo(0);
- }
-
///
/// Bulbapedia: "The sea of fire damages all non-Fire Pokémon on that side of the field for 1/8 of
/// their maximum HP at the end of each turn."
@@ -141,7 +121,7 @@ public class FireGrassPledgeMoveTests
// Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup();
var waterPokemon = CreateSidePokemon("water", maxHealth);
- side.Pokemon.Returns(new List { waterPokemon });
+ side.Pokemon.Returns(new List { waterPokemon });
script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get()!;
@@ -164,7 +144,7 @@ public class FireGrassPledgeMoveTests
// Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup();
var firePokemon = CreateSidePokemon("fire", 100);
- side.Pokemon.Returns(new List { firePokemon });
+ side.Pokemon.Returns(new List { firePokemon });
script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get()!;
@@ -185,7 +165,7 @@ public class FireGrassPledgeMoveTests
// Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup();
var waterPokemon = CreateSidePokemon("water", 96);
- side.Pokemon.Returns(new List { waterPokemon });
+ side.Pokemon.Returns(new List { waterPokemon });
script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get()!;
var battle = Substitute.For();
@@ -207,7 +187,7 @@ public class FireGrassPledgeMoveTests
{
// Arrange
var (script, move, target, side, sideVolatile) = CreateTestSetup();
- side.Pokemon.Returns(new List());
+ side.Pokemon.Returns(new List());
script.OnSecondaryEffect(move, target, 0);
var seaOfFire = sideVolatile.Get()!;
var battle = Substitute.For();
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs
index 7c9606d..dbd46c0 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/MoveVolatile/FireWaterPledgeMoveTests.cs
@@ -20,7 +20,7 @@ public class FireWaterPledgeMoveTests
/// Creates a fully mocked test setup. The user's battle side gets a real as its
/// volatile script set, so the rainbow created by the combined move can be inspected and driven.
///
- private static (FireWaterPledgeMove script, IExecutingMove move, IPokemon user, IBattleSide side, IScriptSet
+ private static (FireWaterPledgeMove script, IExecutingMove move, IBattlePokemon user, IBattleSide side, IScriptSet
sideVolatile) CreateTestSetup()
{
var script = new FireWaterPledgeMove();
@@ -31,11 +31,9 @@ public class FireWaterPledgeMoveTests
var sideVolatile = new ScriptSet(side);
side.VolatileScripts.Returns(sideVolatile);
- var battleData = Substitute.For();
- battleData.BattleSide.Returns(side);
- var user = Substitute.For();
- user.BattleData.Returns(battleData);
+ var user = Substitute.For();
move.User.Returns(user);
+ user.BattleSide.Returns(side);
return (script, move, user, side, sideVolatile);
}
@@ -67,7 +65,7 @@ public class FireWaterPledgeMoveTests
{
// Arrange
var (script, move, _, _, _) = CreateTestSetup();
- var target = Substitute.For();
+ var target = Substitute.For();
ushort basePower = 80;
// Act
@@ -86,7 +84,7 @@ public class FireWaterPledgeMoveTests
{
// Arrange
var (script, move, _, _, sideVolatile) = CreateTestSetup();
- var target = Substitute.For();
+ var target = Substitute.For();
// Act
script.OnSecondaryEffect(move, target, 0);
@@ -95,25 +93,6 @@ public class FireWaterPledgeMoveTests
await Assert.That(sideVolatile.Get()).IsNotNull();
}
- ///
- /// Technical test: if the user has no , no rainbow is created and the
- /// script does not throw.
- ///
- [Test]
- public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
- {
- // Arrange
- var (script, move, user, _, sideVolatile) = CreateTestSetup();
- user.BattleData.Returns((IPokemonBattleData?)null);
- var target = Substitute.For();
-
- // Act
- script.OnSecondaryEffect(move, target, 0);
-
- // Assert
- await Assert.That(sideVolatile.Count).IsEqualTo(0);
- }
-
///
/// Bulbapedia: "The rainbow doubles the probability of additional effects taking place for moves used
/// by that side of the field".
@@ -123,7 +102,7 @@ public class FireWaterPledgeMoveTests
{
// Arrange
var (script, move, _, _, sideVolatile) = CreateTestSetup();
- var target = Substitute.For();
+ var target = Substitute.For();
script.OnSecondaryEffect(move, target, 0);
var rainbow = sideVolatile.Get()!;
@@ -143,7 +122,7 @@ public class FireWaterPledgeMoveTests
{
// Arrange
var (script, move, _, side, sideVolatile) = CreateTestSetup();
- var target = Substitute.For();
+ var target = Substitute.For();
script.OnSecondaryEffect(move, target, 0);
var rainbow = sideVolatile.Get()!;
var battle = Substitute.For();
@@ -165,7 +144,7 @@ public class FireWaterPledgeMoveTests
{
// Arrange
var (script, move, _, side, sideVolatile) = CreateTestSetup();
- var target = Substitute.For();
+ var target = Substitute.For();
script.OnSecondaryEffect(move, target, 0);
var rainbow = sideVolatile.Get()!;
var battle = Substitute.For();
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs
index 001ca84..5c83413 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcrobaticsTests.cs
@@ -11,9 +11,9 @@ public class AcrobaticsTests
{
// Arrange
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
ushort basePower = 10;
- var user = Substitute.For();
+ var user = Substitute.For();
user.HeldItem.Returns((IItem?)null);
move.User.Returns(user);
var acrobatics = new Acrobatics();
@@ -30,9 +30,9 @@ public class AcrobaticsTests
{
// Arrange
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
ushort basePower = 10;
- var user = Substitute.For();
+ var user = Substitute.For();
user.HeldItem.Returns(Substitute.For());
move.User.Returns(user);
var acrobatics = new Acrobatics();
@@ -49,9 +49,9 @@ public class AcrobaticsTests
{
// Arrange
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
ushort basePower = ushort.MaxValue - 100;
- var user = Substitute.For();
+ var user = Substitute.For();
move.User.Returns(user);
user.HeldItem.Returns((IItem?)null);
var acrobatics = new Acrobatics();
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs
index 6073220..86beefb 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AcupressureTests.cs
@@ -13,24 +13,22 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
public class AcupressureTests
{
- private static (Acupressure script, IExecutingMove move, IPokemon target, IBattleRandom random, IHitData hitData)
- CreateTestSetup()
+ private static (Acupressure script, IExecutingMove move, IBattlePokemon target, IBattleRandom random, IHitData
+ hitData) CreateTestSetup()
{
var script = new Acupressure();
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
var hitData = Substitute.For();
move.GetHitData(target, 0).Returns(hitData);
var random = Substitute.For();
var battle = Substitute.For();
battle.Random.Returns(random);
- var battleData = Substitute.For();
- battleData.Battle.Returns(battle);
- var user = Substitute.For();
- user.BattleData.Returns(battleData);
+ var user = Substitute.For();
move.User.Returns(user);
+ user.Battle.Returns(battle);
target.StatBoost.Returns(new StatBoostStatisticSet());
@@ -40,7 +38,7 @@ public class AcupressureTests
///
/// Helper to extract the stat argument of a ChangeStatBoost call received by the target.
///
- private static object?[]? GetStatBoostCallArgs(IPokemon target)
+ private static object?[]? GetStatBoostCallArgs(IBattlePokemon target)
{
var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
return call?.GetArguments();
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs
index 755ea69..4d84fe4 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AfterYouTests.cs
@@ -12,7 +12,7 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
public class AfterYouTests
{
- private static IMoveChoice CreateChoice(IPokemon user, uint speed)
+ private static IMoveChoice CreateChoice(IBattlePokemon user, uint speed)
{
var choice = Substitute.For();
choice.User.Returns(user);
@@ -25,16 +25,14 @@ public class AfterYouTests
var script = new AfterYou();
var move = Substitute.For();
var hitData = Substitute.For();
- move.GetHitData(Arg.Any(), Arg.Any()).Returns(hitData);
+ move.GetHitData(Arg.Any(), Arg.Any()).Returns(hitData);
var battle = Substitute.For();
battle.ChoiceQueue.Returns(queue);
- var battleData = Substitute.For();
- battleData.Battle.Returns(battle);
- var user = Substitute.For();
- user.BattleData.Returns(battleData);
+ var user = Substitute.For();
move.User.Returns(user);
+ user.Battle.Returns(battle);
return (script, move, hitData);
}
@@ -47,9 +45,9 @@ public class AfterYouTests
public async Task OnSecondaryEffect_TargetLaterInQueue_TargetMovesNext()
{
// Arrange
- var user = Substitute.For();
- var target = Substitute.For();
- var other = Substitute.For();
+ var user = Substitute.For();
+ var target = Substitute.For();
+ var other = Substitute.For();
// Sorted by speed: user (100), other (75), target (50)
var queue = new BattleChoiceQueue([
CreateChoice(user, 100), CreateChoice(other, 75), CreateChoice(target, 50),
@@ -75,8 +73,8 @@ public class AfterYouTests
public void OnSecondaryEffect_TargetAlreadyMoved_Fails()
{
// Arrange
- var user = Substitute.For();
- var target = Substitute.For();
+ var user = Substitute.For();
+ var target = Substitute.For();
// Sorted by speed: target (100), user (50)
var queue = new BattleChoiceQueue([
CreateChoice(target, 100), CreateChoice(user, 50),
@@ -103,8 +101,8 @@ public class AfterYouTests
public void OnSecondaryEffect_TargetAlreadyNext_Fails()
{
// Arrange
- var user = Substitute.For();
- var target = Substitute.For();
+ var user = Substitute.For();
+ var target = Substitute.For();
// Sorted by speed: user (100), target (50)
var queue = new BattleChoiceQueue([
CreateChoice(user, 100), CreateChoice(target, 50),
@@ -132,7 +130,7 @@ public class AfterYouTests
var (script, move, hitData) = CreateTestSetup(null);
// Act
- script.OnSecondaryEffect(move, Substitute.For(), 0);
+ script.OnSecondaryEffect(move, Substitute.For(), 0);
// Assert
hitData.DidNotReceive().Fail();
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs
index 10bcbf1..366eaaa 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AssistTests.cs
@@ -15,9 +15,9 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
public class AssistTests
{
- private static IPokemon CreatePokemonWithMoves(params string[] moveNames)
+ private static IBattlePokemon CreatePokemonWithMoves(params string[] moveNames)
{
- var pokemon = Substitute.For();
+ var pokemon = Substitute.For();
var moves = moveNames.Select(name =>
{
var learned = Substitute.For();
@@ -30,27 +30,23 @@ public class AssistTests
return pokemon;
}
- private static (Assist script, IMoveChoice choice, IPokemon user, IBattleRandom random) CreateTestSetup(
- params IPokemon?[] otherPartyMembers)
+ private static (Assist script, IMoveChoice choice, IBattlePokemon user, IBattleRandom random) CreateTestSetup(
+ params IBattlePokemon?[] otherPartyMembers)
{
var script = new Assist();
var user = CreatePokemonWithMoves("tackle");
- var members = new List { user };
+ var members = new List { user };
members.AddRange(otherPartyMembers);
- var party = Substitute.For();
- party.GetEnumerator().Returns(_ => members.GetEnumerator());
var battleParty = Substitute.For();
- battleParty.Party.Returns(party);
+ battleParty.BattlePokemon.Returns(members);
var random = Substitute.For();
var battle = Substitute.For();
battle.Parties.Returns(new[] { battleParty });
battle.Random.Returns(random);
- var battleData = Substitute.For();
- battleData.Battle.Returns(battle);
- user.BattleData.Returns(battleData);
+ user.Battle.Returns(battle);
var choice = Substitute.For();
choice.User.Returns(user);
@@ -180,26 +176,4 @@ public class AssistTests
await Assert.That(moveName).IsEqualTo(new StringKey("growl"));
choice.DidNotReceive().Fail();
}
-
- ///
- /// Technical test: outside of battle (no battle data) the script returns without failing the choice.
- ///
- [Test]
- public async Task ChangeMove_NoBattleData_DoesNothing()
- {
- // Arrange
- var script = new Assist();
- var user = Substitute.For();
- user.BattleData.Returns((IPokemonBattleData?)null);
- var choice = Substitute.For();
- choice.User.Returns(user);
- StringKey moveName = "assist";
-
- // Act
- script.ChangeMove(choice, ref moveName);
-
- // Assert
- choice.DidNotReceive().Fail();
- await Assert.That(moveName).IsEqualTo(new StringKey("assist"));
- }
}
\ No newline at end of file
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs
index 36627ed..bd76a58 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AttractTests.cs
@@ -15,12 +15,12 @@ namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
public class AttractTests
{
- private static (Attract script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData)
- CreateTestSetup(Gender userGender, Gender targetGender)
+ private static (Attract script, IExecutingMove move, IBattlePokemon target, IScriptSet targetVolatile, IHitData
+ hitData) CreateTestSetup(Gender userGender, Gender targetGender)
{
var script = new Attract();
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
var hitData = Substitute.For();
move.GetHitData(target, 0).Returns(hitData);
@@ -28,7 +28,7 @@ public class AttractTests
target.Volatile.Returns(targetVolatile);
target.Gender.Returns(targetGender);
- var user = Substitute.For();
+ var user = Substitute.For();
user.Gender.Returns(userGender);
move.User.Returns(user);
diff --git a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs
index 9f0b8ce..e5707a2 100644
--- a/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs
+++ b/Plugins/PkmnLib.Plugin.Gen7.Tests/Scripts/Moves/AuroraVeilTests.cs
@@ -31,12 +31,12 @@ public class AuroraVeilTests
}
}
- private static (AuroraVeil script, IExecutingMove move, IPokemon target, IPokemon user, IScriptSet sideScripts,
- IHitData hitData) CreateTestSetup(StringKey? weather)
+ private static (AuroraVeil script, IExecutingMove move, IBattlePokemon target, IBattlePokemon user, IScriptSet
+ sideScripts, IHitData hitData) CreateTestSetup(StringKey? weather)
{
var script = new AuroraVeil();
var move = Substitute.For();
- var target = Substitute.For();
+ var target = Substitute.For();
var hitData = Substitute.For();
move.GetHitData(target, 0).Returns(hitData);
@@ -51,13 +51,10 @@ public class AuroraVeilTests
side.VolatileScripts.Returns(sideScripts);
battle.Sides.Returns(new[] { side });
- var battleData = Substitute.For();
- battleData.Battle.Returns(battle);
- battleData.SideIndex.Returns((byte)0);
-
- var user = Substitute.For();
- user.BattleData.Returns(battleData);
+ var user = Substitute.For();
user.GetScripts().Returns(_ => new ScriptIterator(new List>()));
+ user.SideIndex.Returns((byte)0);
+ user.Battle.Returns(battle);
move.User.Returns(user);
return (script, move, target, user, sideScripts, hitData);
@@ -154,25 +151,4 @@ public class AuroraVeilTests
var effect = (AuroraVeilEffect)((Func