Lots more work on implementing battling
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
|
||||
@@ -69,12 +71,12 @@ public interface IBattle : IScriptSource
|
||||
/// <summary>
|
||||
/// A queue of the yet to be executed choices in a turn.
|
||||
/// </summary>
|
||||
BattleChoiceQueue ChoiceQueue { get; }
|
||||
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);
|
||||
IPokemon? GetPokemon(byte side, byte position);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a slot on the battlefield can still be filled. If no party is responsible
|
||||
@@ -92,20 +94,231 @@ public interface IBattle : IScriptSource
|
||||
/// <summary>
|
||||
/// Checks whether a choice is actually possible.
|
||||
/// </summary>
|
||||
void CanUse(ITurnChoice choice);
|
||||
|
||||
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(string? weatherName);
|
||||
|
||||
void SetWeather(StringKey? weatherName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current weather of the battle. If no weather is present, this returns null.
|
||||
/// </summary>
|
||||
string? WeatherName { get; }
|
||||
StringKey? WeatherName { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IBattle"/>
|
||||
public class BattleImpl : ScriptSource, IBattle
|
||||
{
|
||||
/// <inheritdoc cref="BattleImpl"/>
|
||||
public BattleImpl(IDynamicLibrary library, IReadOnlyList<IBattleParty> 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();
|
||||
}
|
||||
|
||||
/// <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.UsedMove.CurrentPp < 1)
|
||||
return false;
|
||||
// TODO: Validate target
|
||||
}
|
||||
|
||||
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.UsedMove.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();
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey? WeatherName => _weatherScript.Script?.Name;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,4 +13,86 @@ namespace PkmnLib.Dynamic.Models;
|
||||
/// </remarks>
|
||||
public class BattleChoiceQueue
|
||||
{
|
||||
private readonly ITurnChoice?[] _choices;
|
||||
private int _currentIndex;
|
||||
|
||||
/// <inheritdoc cref="BattleChoiceQueue"/>
|
||||
public BattleChoiceQueue(ITurnChoice[] choices)
|
||||
{
|
||||
_choices = choices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues the next turn choice to be executed. This gives back the choice and sets it to null in the queue. It
|
||||
/// also increments the internal index.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ITurnChoice? Dequeue()
|
||||
{
|
||||
if (_currentIndex >= _choices.Length)
|
||||
return null;
|
||||
|
||||
var choice = _choices[_currentIndex];
|
||||
_choices[_currentIndex] = null;
|
||||
_currentIndex++;
|
||||
return choice;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This reads what the next choice to execute will be, without modifying state.
|
||||
/// </summary>
|
||||
public ITurnChoice? Peek() => _currentIndex >= _choices.Length ? null : _choices[_currentIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are any more choices to execute.
|
||||
/// </summary>
|
||||
public bool HasNext() => _currentIndex < _choices.Length;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This resorts the yet to be executed choices. This can be useful for dealing with situations
|
||||
/// such as Pokémon changing forms just after the very start of a turn, when turn order has
|
||||
/// technically already been decided.
|
||||
/// </summary>
|
||||
public void Resort()
|
||||
{
|
||||
var length = _choices.Length;
|
||||
var currentIndex = _currentIndex;
|
||||
|
||||
for (var i = currentIndex; i < length; i++)
|
||||
{
|
||||
var choice = _choices[i];
|
||||
if (choice == null)
|
||||
continue;
|
||||
// Ensure that the speed is up to date
|
||||
var speed = choice.User.BoostedStats.Speed;
|
||||
choice.User.RunScriptHook(script => script.ChangeSpeed(choice, ref speed));
|
||||
choice.Speed = speed;
|
||||
}
|
||||
|
||||
// We only sort the choices that are left
|
||||
Array.Sort(_choices, currentIndex, length - currentIndex, TurnChoiceComparer.Instance!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This moves the choice of a specific Pokémon up to the next choice to be executed.
|
||||
/// </summary>
|
||||
public bool MovePokemonChoiceNext(IPokemon pokemon)
|
||||
{
|
||||
var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon);
|
||||
if (index == -1)
|
||||
return false;
|
||||
if (index == _currentIndex)
|
||||
return true;
|
||||
var choice = _choices[index];
|
||||
_choices[index] = null;
|
||||
// Push all choices back
|
||||
for (var i = index; i > _currentIndex; i--)
|
||||
_choices[i] = _choices[i - 1];
|
||||
// And insert the choice at the front
|
||||
_choices[_currentIndex] = choice;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal IReadOnlyList<ITurnChoice?> GetChoices() => _choices;
|
||||
}
|
||||
12
PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs
Normal file
12
PkmnLib.Dynamic/Models/BattleFlow/MoveTurnExecutor.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models.BattleFlow;
|
||||
|
||||
internal static class MoveTurnExecutor
|
||||
{
|
||||
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
84
PkmnLib.Dynamic/Models/BattleFlow/TurnRunner.cs
Normal file
84
PkmnLib.Dynamic/Models/BattleFlow/TurnRunner.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models.BattleFlow;
|
||||
|
||||
public static class TurnRunner
|
||||
{
|
||||
public static void RunTurn(this IBattle battle)
|
||||
{
|
||||
var queue = battle.ChoiceQueue;
|
||||
if (queue == null)
|
||||
throw new ArgumentNullException(nameof(battle.ChoiceQueue),
|
||||
"The battle's choice queue must be set before running a turn.");
|
||||
|
||||
// We are now at the very beginning of a turn. We have assigned speeds and priorities to all
|
||||
// choices, and put them in the correct order.
|
||||
|
||||
// The first thing to do is to run the on_before_turn script hook on every choice. This script hook
|
||||
// is primarily intended to be used to reset variables on a script (for example scripts that need
|
||||
// to check whether a Pokémon was hit this turn. By resetting here, and setting a variable to true
|
||||
// they can then know this later on.)
|
||||
foreach (var choice in queue.GetChoices().WhereNotNull())
|
||||
{
|
||||
choice.RunScriptHook(script => script.OnBeforeTurnStart(choice));
|
||||
}
|
||||
|
||||
// Now we can properly begin executing choices.
|
||||
// One by one dequeue the turns, and run them. If the battle has ended we do not want to
|
||||
// continue running.
|
||||
while (queue.HasNext() && !battle.HasEnded)
|
||||
{
|
||||
var next = queue.Dequeue();
|
||||
if (next == null)
|
||||
continue;
|
||||
ExecuteChoice(battle, next);
|
||||
}
|
||||
|
||||
// If the battle is not ended, we have arrived at the normal end of a turn. and thus want
|
||||
// to run the end turn scripts.
|
||||
|
||||
// As we want all scripts to run exactly once, including those on the sides and battles,
|
||||
// we can't just use the default script hook on each Pokémon. Instead, manually call
|
||||
// the script functions on every script.
|
||||
if (!battle.HasEnded)
|
||||
{
|
||||
var scripts = new List<IEnumerable<ScriptContainer>>(10);
|
||||
foreach (var side in battle.Sides)
|
||||
{
|
||||
foreach (var pokemon in side.Pokemon.WhereNotNull())
|
||||
{
|
||||
scripts.Clear();
|
||||
pokemon.GetOwnScripts(scripts);
|
||||
scripts.RunScriptHook(x => x.OnEndTurn());
|
||||
}
|
||||
scripts.Clear();
|
||||
side.GetOwnScripts(scripts);
|
||||
scripts.RunScriptHook(x => x.OnEndTurn());
|
||||
}
|
||||
scripts.Clear();
|
||||
battle.GetOwnScripts(scripts);
|
||||
scripts.RunScriptHook(x => x.OnEndTurn());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteChoice(IBattle battle, ITurnChoice choice)
|
||||
{
|
||||
if (choice is IPassChoice)
|
||||
return;
|
||||
if (battle.HasEnded)
|
||||
return;
|
||||
if (!choice.User.IsUsable)
|
||||
return;
|
||||
if (choice.User.BattleData?.IsOnBattlefield != true)
|
||||
return;
|
||||
switch (choice)
|
||||
{
|
||||
case IMoveChoice moveChoice:
|
||||
MoveTurnExecutor.ExecuteMoveChoice(battle, moveChoice);
|
||||
break;
|
||||
// TODO: Implement other choice types
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,42 @@ public interface IBattleParty
|
||||
/// The backing Pokemon party.
|
||||
/// </summary>
|
||||
IPokemonParty Party { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the party is responsible for the specified side and position.
|
||||
/// </summary>
|
||||
bool IsResponsibleForIndex(byte side, byte position);
|
||||
|
||||
bool IsResponsibleForIndex(ResponsibleIndex index);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the party has a living Pokemon left that is not in the field.
|
||||
/// </summary>
|
||||
bool HasPokemonNotInField();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A position on the battlefield that a party is responsible for.
|
||||
/// Contains the index of the side and the position on that side.
|
||||
/// </summary>
|
||||
public record struct ResponsibleIndex(byte Side, byte Position);
|
||||
|
||||
/// <inheritdoc />
|
||||
public class BattlePartyImpl : IBattleParty
|
||||
{
|
||||
private readonly ResponsibleIndex[] _responsibleIndices;
|
||||
|
||||
/// <inheritdoc cref="BattlePartyImpl"/>
|
||||
public BattlePartyImpl(IPokemonParty party, ResponsibleIndex[] responsibleIndices)
|
||||
{
|
||||
Party = party;
|
||||
_responsibleIndices = responsibleIndices;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPokemonParty Party { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsResponsibleForIndex(ResponsibleIndex index) => _responsibleIndices.Contains(index);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPokemonNotInField() => Party.Any(x => x is { IsUsable: true, BattleData.IsOnBattlefield: false });
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
@@ -13,4 +14,20 @@ public interface IBattleRandom : IRandom
|
||||
/// chance.
|
||||
/// </summary>
|
||||
bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IBattleRandom"/>
|
||||
public class BattleRandomImpl : RandomImpl, IBattleRandom
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber)
|
||||
{
|
||||
executingMove.RunScriptHook(script => script.ChangeEffectChance(executingMove, target, hitNumber, ref chance));
|
||||
target.RunScriptHook(script => script.ChangeIncomingEffectChance(executingMove, target, hitNumber, ref chance));
|
||||
if (chance > 100.0)
|
||||
return true;
|
||||
if (chance < 0.0)
|
||||
return false;
|
||||
return GetFloat() * 100.0 < chance;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using PkmnLib.Dynamic.Events;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
@@ -59,7 +61,7 @@ public interface IBattleSide : IScriptSource
|
||||
/// 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>
|
||||
void AllPositionsFilled();
|
||||
bool AllPositionsFilled();
|
||||
|
||||
/// <summary>
|
||||
/// Sets a choice for a Pokémon on this side.
|
||||
@@ -70,11 +72,12 @@ public interface IBattleSide : IScriptSource
|
||||
/// Resets all choices on this side.
|
||||
/// </summary>
|
||||
void ResetChoices();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Forcibly removes a Pokémon from the field.
|
||||
/// </summary>
|
||||
void ForceClearPokemonFromField();
|
||||
/// <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
|
||||
@@ -117,4 +120,168 @@ public interface IBattleSide : IScriptSource
|
||||
/// 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];
|
||||
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)
|
||||
{
|
||||
_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 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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace PkmnLib.Dynamic.Models.Choices;
|
||||
/// </summary>
|
||||
public class TurnChoiceComparer : IComparer<ITurnChoice>
|
||||
{
|
||||
public static TurnChoiceComparer Instance { get; } = new();
|
||||
|
||||
private enum CompareValues
|
||||
{
|
||||
XEqualsY = 0,
|
||||
@@ -12,7 +14,7 @@ public class TurnChoiceComparer : IComparer<ITurnChoice>
|
||||
XGreaterThanY = 1
|
||||
}
|
||||
|
||||
private CompareValues CompareForSameType(ITurnChoice x, ITurnChoice y)
|
||||
private static CompareValues CompareForSameType(ITurnChoice x, ITurnChoice y)
|
||||
{
|
||||
// Higher speed goes first
|
||||
var speedComparison = x.Speed.CompareTo(y.Speed);
|
||||
@@ -23,7 +25,7 @@ public class TurnChoiceComparer : IComparer<ITurnChoice>
|
||||
return (CompareValues)x.RandomValue.CompareTo(y.RandomValue);
|
||||
}
|
||||
|
||||
private CompareValues CompareImpl(ITurnChoice? x, ITurnChoice? y)
|
||||
private static CompareValues CompareImpl(ITurnChoice? x, ITurnChoice? y)
|
||||
{
|
||||
// Deal with possible null values
|
||||
switch (x)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils.Errors;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
@@ -110,7 +111,7 @@ public interface IExecutingMove : IScriptSource
|
||||
/// <summary>
|
||||
/// Gets a hit data for a target, with a specific index.
|
||||
/// </summary>
|
||||
HitData GetHitData(IPokemon target, byte hit);
|
||||
IHitData GetHitData(IPokemon target, byte hit);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a Pokémon is a target for this move.
|
||||
@@ -125,5 +126,99 @@ public interface IExecutingMove : IScriptSource
|
||||
/// <summary>
|
||||
/// Gets a hit based on its raw index.
|
||||
/// </summary>
|
||||
HitData GetDataFromRawIndex(int index);
|
||||
IHitData GetDataFromRawIndex(int index);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IExecutingMove"/>
|
||||
public class ExecutingMoveImpl : ScriptSource, IExecutingMove
|
||||
{
|
||||
private readonly List<IPokemon?> _targets;
|
||||
private readonly IHitData[] _hits;
|
||||
|
||||
/// <inheritdoc cref="ExecutingMoveImpl"/>
|
||||
public ExecutingMoveImpl(List<IPokemon?> targets, byte numberOfHits, IPokemon user, ILearnedMove chosenMove,
|
||||
IMoveData useMove, ScriptContainer script)
|
||||
{
|
||||
_targets = targets;
|
||||
NumberOfHits = numberOfHits;
|
||||
User = user;
|
||||
ChosenMove = chosenMove;
|
||||
UseMove = useMove;
|
||||
Script = script;
|
||||
|
||||
var totalHits = targets.Count * numberOfHits;
|
||||
_hits = new IHitData[totalHits];
|
||||
for (var i = 0; i < totalHits; i++)
|
||||
{
|
||||
_hits[i] = new HitData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TargetCount => _targets.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte NumberOfHits { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPokemon User { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILearnedMove ChosenMove { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IMoveData UseMove { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScriptContainer Script { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHitData GetHitData(IPokemon target, byte hit)
|
||||
{
|
||||
var targetIndex = _targets.IndexOf(target);
|
||||
if (targetIndex == -1)
|
||||
{
|
||||
throw new ArgumentException("The target is not a target of this move.");
|
||||
}
|
||||
|
||||
var index = targetIndex * NumberOfHits + hit;
|
||||
return _hits[index];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPokemonTarget(IPokemon target) => _targets.Contains(target);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetTargetIndex(IPokemon target)
|
||||
{
|
||||
var targetIndex = _targets.IndexOf(target);
|
||||
if (targetIndex == -1)
|
||||
throw new ArgumentException("The target is not a target of this move.");
|
||||
|
||||
return targetIndex * NumberOfHits;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHitData GetDataFromRawIndex(int index)
|
||||
{
|
||||
if (index < 0 || index >= _hits.Length)
|
||||
throw new OutOfRangeException("Hit", index, _hits.Length - 1);
|
||||
return _hits[index];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int ScriptCount => 1 + User.ScriptCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts)
|
||||
{
|
||||
scripts.Add(Script);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts)
|
||||
{
|
||||
scripts.Add(Script);
|
||||
User.CollectScripts(scripts);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,11 @@ public interface ILearnedMove
|
||||
/// The maximal power points for this move.
|
||||
/// </summary>
|
||||
byte MaxPp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current power points for this move.
|
||||
/// </summary>
|
||||
byte CurrentPp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The way the move has been learned.
|
||||
|
||||
@@ -217,7 +217,7 @@ public interface IPokemon : IScriptSource
|
||||
/// <param name="stat">The stat to be changed</param>
|
||||
/// <param name="change">The amount to change the stat by</param>
|
||||
/// <param name="selfInflicted">Whether the change was self-inflicted. This can be relevant in scripts.</param>
|
||||
void ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted);
|
||||
bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the currently active ability.
|
||||
@@ -283,6 +283,27 @@ public interface IPokemon : IScriptSource
|
||||
/// </summary>
|
||||
void ChangeLevelBy(int change);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current battle the Pokémon is in.
|
||||
/// </summary>
|
||||
void SetBattleData(IBattle battle, byte sideIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the Pokémon is on the battlefield.
|
||||
/// </summary>
|
||||
void SetOnBattlefield(bool onBattleField);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the position the Pokémon has within its side.
|
||||
/// </summary>
|
||||
/// <param name="position"></param>
|
||||
void SetBattleSidePosition(byte position);
|
||||
|
||||
/// <summary>
|
||||
/// Marks a Pokemon as seen in the battle.
|
||||
/// </summary>
|
||||
void MarkOpponentAsSeen(IPokemon pokemon);
|
||||
|
||||
// TODO: (de)serialize
|
||||
}
|
||||
|
||||
@@ -295,25 +316,35 @@ public interface IPokemonBattleData
|
||||
/// <summary>
|
||||
/// The battle the Pokemon is in.
|
||||
/// </summary>
|
||||
IBattle Battle { get; }
|
||||
IBattle Battle { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the side of the Pokemon
|
||||
/// </summary>
|
||||
byte SideIndex { get; }
|
||||
byte SideIndex { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the position of the Pokemon on the field
|
||||
/// </summary>
|
||||
byte Position { get; }
|
||||
byte Position { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of opponents the Pokemon has seen this battle.
|
||||
/// </summary>
|
||||
IReadOnlyList<IPokemon> SeenOpponents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Pokemon is on the battlefield.
|
||||
/// </summary>
|
||||
bool IsOnBattlefield { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an opponent to the list of seen opponents.
|
||||
/// </summary>
|
||||
void MarkOpponentAsSeen(IPokemon opponent);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <inheritdoc cref="IPokemon"/>
|
||||
public class PokemonImpl : ScriptSource, IPokemon
|
||||
{
|
||||
/// <inheritdoc cref="PokemonImpl"/>
|
||||
@@ -358,7 +389,7 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
public IForm? DisplayForm { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public LevelInt Level { get; }
|
||||
public LevelInt Level { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Experience { get; }
|
||||
@@ -468,13 +499,42 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
/// <inheritdoc />
|
||||
public bool ConsumeHeldItem()
|
||||
{
|
||||
if (HeldItem is null)
|
||||
return false;
|
||||
if (!Library.ScriptResolver.TryResolveItemScript(HeldItem, out _))
|
||||
return false;
|
||||
// TODO: actually consume the item
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted)
|
||||
public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var prevented = false;
|
||||
this.RunScriptHook(script => script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
|
||||
if (prevented)
|
||||
return false;
|
||||
this.RunScriptHook(script => script.ChangeStatBoostChange(this, stat, selfInflicted, ref change));
|
||||
if (change == 0)
|
||||
return false;
|
||||
var changed = false;
|
||||
var oldBoost = StatBoost.GetStatistic(stat);
|
||||
changed = change switch
|
||||
{
|
||||
> 0 => StatBoost.IncreaseStatistic(stat, change),
|
||||
< 0 => StatBoost.DecreaseStatistic(stat, change),
|
||||
_ => changed
|
||||
};
|
||||
if (!changed)
|
||||
return false;
|
||||
if (BattleData != null)
|
||||
{
|
||||
var newBoost = StatBoost.GetStatistic(stat);
|
||||
BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost));
|
||||
}
|
||||
|
||||
RecalculateBoostedStats();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -499,10 +559,7 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RecalculateBoostedStats()
|
||||
{
|
||||
Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
|
||||
}
|
||||
public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ChangeSpecies(ISpecies species, IForm form)
|
||||
@@ -612,6 +669,7 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= Moves.Count)
|
||||
throw new InvalidOperationException("No empty move slot found.");
|
||||
if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
|
||||
@@ -621,17 +679,57 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearStatus()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void ClearStatus() => StatusScript.Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ChangeLevelBy(int change)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var newLevel = Level + change;
|
||||
Level = (LevelInt)Math.Clamp(newLevel, 1, Library.StaticLibrary.Settings.MaxLevel);
|
||||
RecalculateFlatStats();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetBattleData(IBattle battle, byte sideIndex)
|
||||
{
|
||||
if (BattleData is not null)
|
||||
{
|
||||
BattleData.Battle = battle;
|
||||
BattleData.SideIndex = sideIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
BattleData = new PokemonBattleDataImpl(battle, sideIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetOnBattlefield(bool onBattleField)
|
||||
{
|
||||
if (BattleData is not null)
|
||||
{
|
||||
BattleData.IsOnBattlefield = onBattleField;
|
||||
if (!onBattleField)
|
||||
{
|
||||
Volatile.Clear();
|
||||
WeightInKm = Form.Weight;
|
||||
HeightInMeters = Form.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetBattleSidePosition(byte position)
|
||||
{
|
||||
if (BattleData is not null)
|
||||
{
|
||||
BattleData.Position = position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int ScriptCount
|
||||
{
|
||||
@@ -667,4 +765,37 @@ public class PokemonImpl : ScriptSource, IPokemon
|
||||
side.CollectScripts(scripts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PokemonBattleDataImpl : IPokemonBattleData
|
||||
{
|
||||
/// <inheritdoc cref="PokemonBattleDataImpl"/>
|
||||
public PokemonBattleDataImpl(IBattle battle, byte sideIndex)
|
||||
{
|
||||
Battle = battle;
|
||||
SideIndex = sideIndex;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBattle Battle { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte SideIndex { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Position { get; set; }
|
||||
|
||||
private readonly List<IPokemon> _seenOpponents = [];
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IPokemon> SeenOpponents => _seenOpponents;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsOnBattlefield { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MarkOpponentAsSeen(IPokemon opponent)
|
||||
{
|
||||
_seenOpponents.Add(opponent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user