350 lines
11 KiB
C#
350 lines
11 KiB
C#
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;
|
|
|
|
/// <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; }
|
|
}
|
|
|
|
/// <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();
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
} |