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.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 { /// /// 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; } /// /// 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(); /// /// 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); /// /// Sets the current weather for the battle. If null is passed, this clears the weather. /// void SetWeather(StringKey? weatherName); /// /// Gets the current weather of the battle. If no weather is present, this returns null. /// StringKey? WeatherName { get; } } /// public class BattleImpl : ScriptSource, IBattle { /// public BattleImpl(IDynamicLibrary library, IReadOnlyList parties, bool canFlee, byte numberOfSides, byte positionsPerSide) { 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 = 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 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 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; // TODO: Validate target } 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(); } CurrentTurnNumber += 1; ChoiceQueue = new BattleChoiceQueue(choices); this.RunTurn(); ChoiceQueue = null; EventHook.Invoke(new EndTurnEvent()); } private readonly ScriptContainer _weatherScript = new(); /// public void SetWeather(StringKey? weatherName) { if (weatherName.HasValue) { if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, out var script)) throw new InvalidOperationException($"Weather script {weatherName} not found."); _weatherScript.Set(script); script.OnInitialize(Library, null); } else { _weatherScript.Clear(); } } private IScriptSet Volatile { get; } = new ScriptSet(); /// public StringKey? WeatherName => _weatherScript.Script?.Name; /// public override int ScriptCount => 2; /// public override void GetOwnScripts(List> scripts) { scripts.Add(_weatherScript ); scripts.Add(Volatile); } /// public override void CollectScripts(List> scripts) => GetOwnScripts(scripts); }