using System.Diagnostics.CodeAnalysis; using PkmnLib.Dynamic.BattleFlow; using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.Models; /// /// 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. /// public interface IBattle : IScriptSource, IDeepCloneable, IDisposable { /// /// The library the battle uses for handling. /// IDynamicLibrary Library { get; } /// /// A list of all different parties in the battle. /// IReadOnlyList Parties { get; } /// /// Whether or not Pokemon can flee from the battle. /// bool CanFlee { get; } /// /// The number of sides in the battle. Typically 2. /// byte NumberOfSides { get; } /// /// The number of Pokemon that can be on each side. /// byte PositionsPerSide { get; } /// /// Whether this battle is a wild battle. In a wild battle, the player can catch the opposing Pokemon, /// and moves like roar will end the battle instead of switching out the Pokemon. /// bool IsWildBattle { get; } /// /// A list of all sides in the battle. /// IReadOnlyList Sides { get; } /// /// The RNG used for the battle. /// IBattleRandom Random { get; } /// /// Whether the battle has ended. /// bool HasEnded { get; } /// /// The result of the battle. If the battle has not ended, this is null. /// BattleResult? Result { get; } /// /// The handler to send all events to. /// EventHook EventHook { get; } /// /// The index of the current turn. Initially 0, until the first turn starts when all choices are made. /// uint CurrentTurnNumber { get; } /// /// A queue of the yet to be executed choices in a turn. /// BattleChoiceQueue? ChoiceQueue { get; } /// /// Get a Pokemon on the battlefield, on a specific side and an index on that side. /// IPokemon? GetPokemon(byte side, byte position); /// /// 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. /// bool CanSlotBeFilled(byte side, byte position); /// /// Validates whether the battle is still in a non-ended state. If the battle has ended, this /// properly sets who has won etc. /// void ValidateBattleState(); /// /// Forcefully ends the battle. This will set the result to inconclusive and set HasEnded to true. /// void ForceEndBattle(); /// /// 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); /// /// Checks whether a choice is actually possible. /// bool CanUse(ITurnChoice choice); /// /// Try and set the choice for the battle. If the choice is not valid, this returns false. /// bool TrySetChoice(ITurnChoice choice); /// /// The script that handles the current weather of the battle. /// IReadOnlyScriptContainer WeatherScript { get; } /// /// Sets the current weather for the battle. If null is passed, this clears the weather. /// A duration can be passed to set the duration of the weather in turns. This duration can be modified by /// other scripts before the weather is set through the script hook. /// bool SetWeather(StringKey? weatherName, int duration, EventBatchId batchId = default); /// /// Volatile scripts are scripts that are not permanent and can be removed by other scripts. /// public IScriptSet Volatile { get; } /// /// Gets the current weather of the battle. If no weather is present, this returns null. /// StringKey? WeatherName { get; } /// /// Sets the current terrain for the battle. If null is passed, this clears the terrain. /// void SetTerrain(StringKey? terrainName, EventBatchId batchId = default); /// /// Gets the current terrain of the battle. If no terrain is present, this returns null. /// StringKey? TerrainName { get; } /// /// 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. /// IReadOnlyList> PreviousTurnChoices { get; } /// /// Attempts to capture a Pokemon. This will use the current RNG to determine whether the capture is successful. /// CaptureResult AttempCapture(byte sideIndex, byte position, IItem item); } /// public class BattleImpl : ScriptSource, IBattle { /// /// The library the battle uses for data and dynamic handling. /// The parties that will be in the battle. /// Whether Pokémon are allowed to flee from the battle. /// The number of sides in the battle. Generally 2. /// The number of spots there are on each side for Pokémon. 1 for singles, 2 for doubles, etc. /// The seed for the RNG. If null, this uses a time-dependent seed. public BattleImpl(IDynamicLibrary library, IReadOnlyList parties, bool canFlee, byte numberOfSides, byte positionsPerSide, bool isWildBattle, int? randomSeed = null) { Library = library; Parties = parties; CanFlee = canFlee; NumberOfSides = numberOfSides; PositionsPerSide = positionsPerSide; IsWildBattle = isWildBattle; Volatile = new ScriptSet(this); 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(); } /// public IDynamicLibrary Library { get; } /// public IReadOnlyList Parties { get; } /// public bool CanFlee { get; } /// public byte NumberOfSides { get; } /// public byte PositionsPerSide { get; } /// public bool IsWildBattle { get; } /// public IReadOnlyList Sides { get; } /// public IBattleRandom Random { get; } /// public bool HasEnded { get; private set; } /// public BattleResult? Result { get; private set; } /// public EventHook EventHook { get; } /// public uint CurrentTurnNumber { get; private set; } /// public BattleChoiceQueue? ChoiceQueue { get; private set; } /// public IPokemon? 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.HasPokemonNotInField()); /// 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; } /// public void ForceEndBattle() { HasEnded = true; Result = BattleResult.Inconclusive; } /// public bool HasForcedTurn(IPokemon 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(battleData.SideIndex, battleData.Position, ref forcedChoice)); choice = forcedChoice; return choice != null; } /// public bool CanUse(ITurnChoice choice) { if (!choice.User.IsUsable) return false; if (HasForcedTurn(choice.User, out var forcedChoice) && choice != forcedChoice) 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; var preventMove = false; choice.RunScriptHook(script => script.PreventMoveSelection(moveChoice, ref preventMove)); if (preventMove) return false; } return true; } /// 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(); /// /// The script that handles the current weather of the battle. /// public IReadOnlyScriptContainer WeatherScript => _weatherScript; /// public bool SetWeather(StringKey? weatherName, int duration, EventBatchId batchId = default) { var preventWeatherChange = false; this.RunScriptHook(x => x.PreventWeatherChange(weatherName, ref preventWeatherChange)); if (preventWeatherChange) return false; var oldWeatherName = WeatherScript.Script?.Name; if (weatherName.HasValue) { if (weatherName == oldWeatherName) { // Extend duration of existing weather if (_weatherScript.Script is ILimitedTurnsScript existingWeatherScript) { this.RunScriptHook(x => x.ChangeWeatherDuration(weatherName.Value, ref duration)); if (duration < existingWeatherScript.TurnsRemaining) return true; existingWeatherScript.SetTurns(duration); } return true; } if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, null, out var script)) throw new InvalidOperationException($"Weather script {weatherName} not found."); if (script is ILimitedTurnsScript weatherScript) { this.RunScriptHook(x => x.ChangeWeatherDuration(weatherName.Value, ref duration)); weatherScript.SetTurns(duration); } _weatherScript.Set(script); script.OnAddedToParent(this); } else { _weatherScript.Clear(); } EventHook.Invoke(new WeatherChangeEvent(oldWeatherName, weatherName) { BatchId = batchId, }); Sides.SelectMany(x => x.Pokemon).WhereNotNull() .RunScriptHook(x => x.OnWeatherChange(this, weatherName, oldWeatherName)); return true; } /// public IScriptSet Volatile { get; } /// public StringKey? WeatherName => WeatherScript.Script?.Name; private readonly ScriptContainer _terrainScript = new(); /// public void SetTerrain(StringKey? terrainName, EventBatchId batchId = default) { var oldTerrainName = TerrainName; if (terrainName.HasValue) { if (!Library.ScriptResolver.TryResolve(ScriptCategory.Terrain, terrainName.Value, null, out var script)) throw new InvalidOperationException($"Terrain script {terrainName} not found."); _terrainScript.Set(script); script.OnAddedToParent(this); } else { _terrainScript.Clear(); } EventHook.Invoke(new TerrainChangeEvent(oldTerrainName, terrainName) { BatchId = batchId, }); } /// public StringKey? TerrainName => _terrainScript.Script?.Name; private readonly List> _previousTurnChoices = new(); /// public IReadOnlyList> PreviousTurnChoices => _previousTurnChoices; /// 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; } /// public override int ScriptCount => 3; /// public override void GetOwnScripts(List> scripts) { scripts.Add(WeatherScript); scripts.Add(_terrainScript); scripts.Add(Volatile); } /// public override void CollectScripts(List> scripts) => GetOwnScripts(scripts); /// public void Dispose() { foreach (var party in Parties) { foreach (var pokemon in party.Party.WhereNotNull()) { pokemon.ClearBattleData(); } } _weatherScript.Clear(); _terrainScript.Clear(); Volatile.Clear(); } }