using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.Models; /// <summary> /// A side in a battle. /// </summary> public interface IBattleSide : IScriptSource, IDeepCloneable { /// <summary> /// The index of the side on the battle. /// </summary> byte Index { get; } /// <summary> /// The number of Pokémon that can be on the side. /// </summary> byte NumberOfPositions { get; } /// <summary> /// A list of Pokémon currently on the battlefield. /// </summary> IReadOnlyList<IPokemon?> Pokemon { get; } /// <summary> /// The currently set choices for all Pokémon on the battlefield. Cleared when the turn starts. /// </summary> IReadOnlyList<ITurnChoice?> SetChoices { get; } /// <summary> /// Whether every Pokémon on this side has its choices /// </summary> bool AllChoicesSet { get; } /// <summary> /// The slots on the side that can still be filled. Once all slots are set to false, this side /// has lost the battle. /// </summary> IReadOnlyList<bool> FillablePositions { get; } /// <summary> /// A reference to the battle this side is in. /// </summary> IBattle Battle { get; } /// <summary> /// Whether this side has fled. /// </summary> bool HasFledBattle { get; } /// <summary> /// The volatile scripts that are attached to the side. /// </summary> IScriptSet VolatileScripts { get; } /// <summary> /// Returns true if there are slots that need to be filled with a new pokemon, that have parties /// responsible for them. Returns false if all slots are filled with usable pokemon, or slots are /// empty, but can't be filled by any party anymore. /// </summary> bool AllPositionsFilled(); /// <summary> /// Sets a choice for a Pokémon on this side. /// </summary> void SetChoice(byte position, ITurnChoice choice); /// <summary> /// Resets all choices on this side. /// </summary> void ResetChoices(); /// <summary> /// Forcibly removes a Pokémon from the field. /// </summary> /// <param name="index"></param> void ForceClearPokemonFromField(byte index); /// <summary> /// 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. /// </summary> IPokemon? SwapPokemon(byte position, IPokemon? pokemon); /// <summary> /// Swaps two Pokémon on the side. /// </summary> void SwapPokemon(byte position1, byte position2); /// <summary> /// Checks whether a Pokemon is on the field in this side. /// </summary> bool IsPokemonOnSide(IPokemon pokemon); /// <summary> /// Marks a slot as unfillable. This happens when no parties are able to fill the slot anymore. /// If this happens, the slot can not be used again. /// </summary> void MarkPositionAsUnfillable(byte position); /// <summary> /// Checks whether a slot is fillable. If it is not, the slot can not be used anymore. /// </summary> bool IsPositionFillable(byte position); /// <summary> /// Checks whether the side has been defeated. /// </summary> bool IsDefeated(); /// <summary> /// The number of times this side has attempted to flee. /// </summary> uint FleeAttempts { get; } /// <summary> /// Registers a flee attempt for this side. /// </summary> void RegisterFleeAttempt(); /// <summary> /// Mark the side as fled. /// </summary> void MarkAsFled(); /// <summary> /// Gets a random Pokémon on the given side. /// </summary> byte GetRandomPosition(); } /// <inheritdoc cref="IBattleSide"/> public class BattleSideImpl : ScriptSource, IBattleSide { /// <inheritdoc cref="BattleSideImpl"/> public BattleSideImpl(byte index, byte numberOfPositions, IBattle battle) { Index = index; NumberOfPositions = numberOfPositions; _pokemon = new IPokemon?[numberOfPositions]; _setChoices = new ITurnChoice?[numberOfPositions]; _fillablePositions = new bool[numberOfPositions]; for (byte i = 0; i < numberOfPositions; i++) { _fillablePositions[i] = true; } Battle = battle; VolatileScripts = new ScriptSet(); } /// <inheritdoc /> public byte Index { get; } /// <inheritdoc /> public byte NumberOfPositions { get; } private readonly IPokemon?[] _pokemon; /// <inheritdoc /> public IReadOnlyList<IPokemon?> Pokemon => _pokemon; private readonly ITurnChoice?[] _setChoices; /// <inheritdoc /> public IReadOnlyList<ITurnChoice?> SetChoices => _setChoices; /// <inheritdoc /> public bool AllChoicesSet => _setChoices.All(choice => choice is not null); private readonly bool[] _fillablePositions; /// <inheritdoc /> public IReadOnlyList<bool> FillablePositions => _fillablePositions; /// <inheritdoc /> public IBattle Battle { get; } /// <inheritdoc /> public bool HasFledBattle { get; private set; } /// <inheritdoc /> public IScriptSet VolatileScripts { get; } /// <inheritdoc /> public bool AllPositionsFilled() { for (byte i = 0; i < NumberOfPositions; i++) { var pokemon = Pokemon[i]; var isPokemonViable = pokemon is not null && pokemon.IsUsable; // If the Pokémon is not valid, but the slot can be filled, return false. if (!isPokemonViable && Battle.CanSlotBeFilled(Index, i)) { return false; } } return true; } /// <inheritdoc /> public void SetChoice(byte position, ITurnChoice choice) { _setChoices[position] = choice; } /// <inheritdoc /> public void ResetChoices() { for (byte i = 0; i < NumberOfPositions; i++) { _setChoices[i] = null; } } /// <param name="index"></param> /// <inheritdoc /> public void ForceClearPokemonFromField(byte index) { var pokemon = _pokemon[index]; if (pokemon is not null) { pokemon.RunScriptHook(script => script.OnRemove()); pokemon.SetOnBattlefield(false); } _pokemon[index] = null; } /// <inheritdoc /> public IPokemon? SwapPokemon(byte position, IPokemon? pokemon) { var oldPokemon = _pokemon[position]; if (oldPokemon is not null) { oldPokemon.RunScriptHook(script => script.OnRemove()); oldPokemon.SetOnBattlefield(false); } _pokemon[position] = pokemon; if (pokemon is not null) { pokemon.SetBattleData(Battle, Index); pokemon.SetOnBattlefield(true); pokemon.SetBattleSidePosition(position); foreach (var side in Battle.Sides) { if (side == this) continue; foreach (var opponent in side.Pokemon.WhereNotNull()) { opponent.MarkOpponentAsSeen(pokemon); pokemon.MarkOpponentAsSeen(opponent); } } Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon)); pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon)); } else { Battle.EventHook.Invoke(new SwitchEvent(Index, position, null)); } return oldPokemon; } /// <inheritdoc /> public void SwapPokemon(byte position1, byte position2) { throw new NotImplementedException(); } /// <inheritdoc /> public bool IsPokemonOnSide(IPokemon pokemon) => _pokemon.Contains(pokemon); /// <inheritdoc /> public void MarkPositionAsUnfillable(byte position) => _fillablePositions[position] = false; /// <inheritdoc /> public bool IsPositionFillable(byte position) => _fillablePositions[position]; /// <inheritdoc /> public bool IsDefeated() { return _fillablePositions.All(fillable => !fillable); } /// <inheritdoc /> public uint FleeAttempts { get; private set; } /// <inheritdoc /> public void RegisterFleeAttempt() { FleeAttempts++; } /// <inheritdoc /> public void MarkAsFled() => HasFledBattle = true; /// <inheritdoc /> public byte GetRandomPosition() => (byte)Battle.Random.GetInt(0, NumberOfPositions); /// <inheritdoc /> public override int ScriptCount => 1 + Battle.ScriptCount; /// <inheritdoc /> public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts) { scripts.Add(VolatileScripts); } /// <inheritdoc /> public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts) { scripts.Add(VolatileScripts); Battle.CollectScripts(scripts); } }