using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Models.BattleFlow; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.Models; /// <summary> /// A battle is a representation of a battle in the Pokemon games. It contains all the information needed /// to simulate a battle, and can be used to simulate a battle between two parties. /// </summary> public interface IBattle : IScriptSource, IDeepCloneable { /// <summary> /// The library the battle uses for handling. /// </summary> IDynamicLibrary Library { get; } /// <summary> /// A list of all different parties in the battle. /// </summary> IReadOnlyList<IBattleParty> Parties { get; } /// <summary> /// Whether or not Pokemon can flee from the battle. /// </summary> bool CanFlee { get; } /// <summary> /// The number of sides in the battle. Typically 2. /// </summary> byte NumberOfSides { get; } /// <summary> /// The number of Pokemon that can be on each side. /// </summary> byte PositionsPerSide { get; } /// <summary> /// A list of all sides in the battle. /// </summary> IReadOnlyList<IBattleSide> Sides { get; } /// <summary> /// The RNG used for the battle. /// </summary> IBattleRandom Random { get; } /// <summary> /// Whether the battle has ended. /// </summary> bool HasEnded { get; } /// <summary> /// The result of the battle. If the battle has not ended, this is null. /// </summary> BattleResult? Result { get; } /// <summary> /// The handler to send all events to. /// </summary> EventHook EventHook { get; } /// <summary> /// The index of the current turn. Initially 0, until the first turn starts when all choices are made. /// </summary> uint CurrentTurnNumber { get; } /// <summary> /// A queue of the yet to be executed choices in a turn. /// </summary> BattleChoiceQueue? ChoiceQueue { get; } /// <summary> /// Get a Pokemon on the battlefield, on a specific side and an index on that side. /// </summary> IPokemon? GetPokemon(byte side, byte position); /// <summary> /// Returns whether a slot on the battlefield can still be filled. If no party is responsible /// for that slot, or a party is responsible, but has no remaining Pokemon to throw out anymore, /// this returns false. /// </summary> bool CanSlotBeFilled(byte side, byte position); /// <summary> /// Validates whether the battle is still in a non-ended state. If the battle has ended, this /// properly sets who has won etc. /// </summary> void ValidateBattleState(); /// <summary> /// Checks whether a choice is actually possible. /// </summary> bool CanUse(ITurnChoice choice); /// <summary> /// Try and set the choice for the battle. If the choice is not valid, this returns false. /// </summary> bool TrySetChoice(ITurnChoice choice); /// <summary> /// Sets the current weather for the battle. If null is passed, this clears the weather. /// </summary> void SetWeather(StringKey? weatherName); /// <summary> /// Gets the current weather of the battle. If no weather is present, this returns null. /// </summary> StringKey? WeatherName { get; } /// <summary> /// Gets the turn choices of the previous turn. This is a list of lists, where each list represents the choices /// for a single turn. The outer list is ordered from oldest to newest turn. /// </summary> IReadOnlyList<IReadOnlyList<ITurnChoice>> PreviousTurnChoices { get; } CaptureResult AttempCapture(byte sideIndex, byte position, IItem item); } /// <inheritdoc cref="IBattle"/> public class BattleImpl : ScriptSource, IBattle { /// <inheritdoc cref="BattleImpl"/> /// <param name="library">The library the battle uses for data and dynamic handling.</param> /// <param name="parties">The parties that will be in the battle.</param> /// <param name="canFlee">Whether Pokémon are allowed to flee from the battle.</param> /// <param name="numberOfSides">The number of sides in the battle. Generally 2.</param> /// <param name="positionsPerSide">The number of spots there are on each side for Pokémon. 1 for singles, 2 for doubles, etc.</param> /// <param name="randomSeed">The seed for the RNG. If null, this uses a time-dependent seed.</param> public BattleImpl(IDynamicLibrary library, IReadOnlyList<IBattleParty> parties, bool canFlee, byte numberOfSides, byte positionsPerSide, int? randomSeed = null) { Library = library; Parties = parties; CanFlee = canFlee; NumberOfSides = numberOfSides; PositionsPerSide = positionsPerSide; var sides = new IBattleSide[numberOfSides]; for (byte i = 0; i < numberOfSides; i++) sides[i] = new BattleSideImpl(i, positionsPerSide, this); Sides = sides; Random = randomSeed.HasValue ? new BattleRandomImpl(randomSeed.Value) : new BattleRandomImpl(); EventHook = new EventHook(); } /// <inheritdoc /> public IDynamicLibrary Library { get; } /// <inheritdoc /> public IReadOnlyList<IBattleParty> Parties { get; } /// <inheritdoc /> public bool CanFlee { get; } /// <inheritdoc /> public byte NumberOfSides { get; } /// <inheritdoc /> public byte PositionsPerSide { get; } /// <inheritdoc /> public IReadOnlyList<IBattleSide> Sides { get; } /// <inheritdoc /> public IBattleRandom Random { get; } /// <inheritdoc /> public bool HasEnded { get; private set; } /// <inheritdoc /> public BattleResult? Result { get; private set; } /// <inheritdoc /> public EventHook EventHook { get; } /// <inheritdoc /> public uint CurrentTurnNumber { get; private set; } /// <inheritdoc /> public BattleChoiceQueue? ChoiceQueue { get; private set; } /// <inheritdoc /> public IPokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position]; /// <inheritdoc /> public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x => x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasPokemonNotInField()); /// <inheritdoc /> public void ValidateBattleState() { if (HasEnded) return; var survivingSideExists = false; IBattleSide? survivingSide = null; foreach (var side in Sides) { if (side.HasFledBattle) { Result = BattleResult.Inconclusive; HasEnded = true; return; } if (!side.IsDefeated()) { // If we already found a surviving side, the battle is not over yet if (survivingSideExists) return; survivingSideExists = true; survivingSide = side; } } // If every side is defeated, the battle is a draw if (!survivingSideExists) { Result = BattleResult.Inconclusive; HasEnded = true; return; } // If only one side is left, that side has won Result = BattleResult.Conclusive(survivingSide!.Index); HasEnded = true; } /// <inheritdoc /> public bool CanUse(ITurnChoice choice) { if (!choice.User.IsUsable) return false; if (choice is IMoveChoice moveChoice) { // TODO: Hook to change number of PP needed. if (moveChoice.ChosenMove.CurrentPp < 1) return false; if (!TargetResolver.IsValidTarget(moveChoice.TargetSide, moveChoice.TargetPosition, moveChoice.ChosenMove.MoveData.Target, moveChoice.User)) return false; } return true; } /// <inheritdoc /> public bool TrySetChoice(ITurnChoice choice) { if (!CanUse(choice)) return false; if (choice.User.BattleData?.IsOnBattlefield != true) return false; var side = Sides[choice.User.BattleData!.SideIndex]; side.SetChoice(choice.User.BattleData!.Position, choice); CheckChoicesSetAndRun(); return true; } private void CheckChoicesSetAndRun() { foreach (var side in Sides) { if (!side.AllChoicesSet) return; if (!side.AllPositionsFilled()) return; } var choices = new ITurnChoice[NumberOfSides * PositionsPerSide]; for (var index = 0; index < Sides.Count; index++) { var side = Sides[index]; for (byte i = 0; i < PositionsPerSide; i++) { var choice = side.SetChoices[i]; if (choice is null) throw new InvalidOperationException("Choice is null."); if (choice is IMoveChoice moveChoice) { var priority = moveChoice.ChosenMove.MoveData.Priority; choice.RunScriptHook(script => script.ChangePriority(moveChoice, ref priority)); moveChoice.Priority = priority; } var speed = choice.User.BoostedStats.Speed; choice.RunScriptHook(script => script.ChangeSpeed(choice, ref speed)); choice.Speed = speed; choice.RandomValue = (uint)Random.GetInt(); choices[index * PositionsPerSide + i] = choice; choices[index * PositionsPerSide + i] = choice; } side.ResetChoices(); } _previousTurnChoices.Add(choices.ToList()); CurrentTurnNumber += 1; ChoiceQueue = new BattleChoiceQueue(choices); this.RunTurn(); ChoiceQueue = null; EventHook.Invoke(new EndTurnEvent()); } private readonly ScriptContainer _weatherScript = new(); /// <inheritdoc /> public void SetWeather(StringKey? weatherName) { if (weatherName.HasValue) { if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, null, out var script)) throw new InvalidOperationException($"Weather script {weatherName} not found."); _weatherScript.Set(script); } else { _weatherScript.Clear(); } // TODO: Trigger weather change script hooks } private IScriptSet Volatile { get; } = new ScriptSet(); /// <inheritdoc /> public StringKey? WeatherName => _weatherScript.Script?.Name; private readonly List<IReadOnlyList<ITurnChoice>> _previousTurnChoices = new(); /// <inheritdoc /> public IReadOnlyList<IReadOnlyList<ITurnChoice>> PreviousTurnChoices => _previousTurnChoices; /// <inheritdoc /> public CaptureResult AttempCapture(byte sideIndex, byte position, IItem item) { var target = GetPokemon(sideIndex, position); if (target is not { IsUsable: true }) return CaptureResult.Failed; var attemptCapture = Library.CaptureLibrary.TryCapture(target, item, Random); if (attemptCapture.IsCaught) { target.MarkAsCaught(); var side = Sides[target.BattleData!.SideIndex]; side.ForceClearPokemonFromField(target.BattleData.Position); } EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture)); return attemptCapture; } /// <inheritdoc /> public override int ScriptCount => 2; /// <inheritdoc /> public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts) { scripts.Add(_weatherScript); scripts.Add(Volatile); } /// <inheritdoc /> public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts) => GetOwnScripts(scripts); }