Lots more work on implementing battling
This commit is contained in:
parent
554e1cf2cd
commit
a049dda240
|
@ -2,8 +2,12 @@ using PkmnLib.Dynamic.Models;
|
|||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a Pokemon takes damage.
|
||||
/// </summary>
|
||||
public record DamageEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc cref="DamageEvent"/>
|
||||
public DamageEvent(IPokemon pokemon, uint previousHealth, uint newHealth, DamageSource source)
|
||||
{
|
||||
Pokemon = pokemon;
|
||||
|
@ -12,9 +16,24 @@ public record DamageEvent : IEventData
|
|||
Source = source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pokemon that took damage.
|
||||
/// </summary>
|
||||
public IPokemon Pokemon { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The previous health of the Pokemon.
|
||||
/// </summary>
|
||||
public uint PreviousHealth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The new health of the Pokemon.
|
||||
/// </summary>
|
||||
public uint NewHealth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The source of the damage.
|
||||
/// </summary>
|
||||
public DamageSource Source { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
public class EndTurnEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
|
@ -2,13 +2,20 @@ using PkmnLib.Dynamic.Models;
|
|||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a Pokemon faints.
|
||||
/// </summary>
|
||||
public class FaintEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc cref="FaintEvent"/>
|
||||
public FaintEvent(IPokemon pokemon)
|
||||
{
|
||||
Pokemon = pokemon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pokemon that fainted.
|
||||
/// </summary>
|
||||
public IPokemon Pokemon { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -7,5 +7,9 @@ namespace PkmnLib.Dynamic.Events;
|
|||
/// </summary>
|
||||
public interface IEventData
|
||||
{
|
||||
/// <summary>
|
||||
/// The batch ID indicated which batch of events this belong to. Events with the same batch id
|
||||
/// should be displayed together.
|
||||
/// </summary>
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
|
@ -2,8 +2,12 @@ using PkmnLib.Dynamic.Models;
|
|||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a Pokemon is healed.
|
||||
/// </summary>
|
||||
public class HealEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc cref="HealEvent"/>
|
||||
public HealEvent(IPokemon pokemon, uint previousHealth, uint newHealth)
|
||||
{
|
||||
Pokemon = pokemon;
|
||||
|
@ -11,11 +15,22 @@ public class HealEvent : IEventData
|
|||
NewHealth = newHealth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pokemon that was healed.
|
||||
/// </summary>
|
||||
public IPokemon Pokemon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The previous health of the Pokemon.
|
||||
/// </summary>
|
||||
public uint PreviousHealth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The new health of the Pokemon.
|
||||
/// </summary>
|
||||
public uint NewHealth { get; }
|
||||
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; } = new();
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static;
|
||||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when a Pokemon's stat boost is changed, either positively or negatively.
|
||||
/// </summary>
|
||||
public class StatBoostEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc cref="StatBoostEvent" />
|
||||
public StatBoostEvent(IPokemon pokemon, Statistic statistic, sbyte oldBoost, sbyte newBoost)
|
||||
{
|
||||
Pokemon = pokemon;
|
||||
Statistic = statistic;
|
||||
OldBoost = oldBoost;
|
||||
NewBoost = newBoost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pokemon that had its stat boosted.
|
||||
/// </summary>
|
||||
public IPokemon Pokemon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The statistic that was boosted.
|
||||
/// </summary>
|
||||
public Statistic Statistic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The old boost value.
|
||||
/// </summary>
|
||||
public sbyte OldBoost { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The new boost value.
|
||||
/// </summary>
|
||||
public sbyte NewBoost { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The difference between the new and old boost values.
|
||||
/// </summary>
|
||||
public sbyte BoostDifference => (sbyte)(NewBoost - OldBoost);
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using PkmnLib.Dynamic.Models;
|
||||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a Pokémon is switched in.
|
||||
/// </summary>
|
||||
public class SwitchEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc cref="SwitchEvent"/>
|
||||
public SwitchEvent(byte sideIndex, byte position, IPokemon? pokemon)
|
||||
{
|
||||
SideIndex = sideIndex;
|
||||
Position = position;
|
||||
Pokemon = pokemon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The index of the side that the Pokémon is switching in on.
|
||||
/// </summary>
|
||||
public byte SideIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The position that the Pokémon is switching in to.
|
||||
/// </summary>
|
||||
public byte Position { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The Pokémon that is switching in. If null, no Pokémon is switching in, and the slot is empty after the switch.
|
||||
/// </summary>
|
||||
public IPokemon? Pokemon { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static.Libraries;
|
||||
|
||||
|
@ -30,6 +31,11 @@ public interface IDynamicLibrary
|
|||
/// calculators.
|
||||
/// </summary>
|
||||
IMiscLibrary MiscLibrary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A holder of the script types that can be resolved by this library.
|
||||
/// </summary>
|
||||
ScriptResolver ScriptResolver { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -52,17 +58,19 @@ public class DynamicLibraryImpl : IDynamicLibrary
|
|||
throw new InvalidOperationException("Stat calculator not found in plugins.");
|
||||
if (registry.MiscLibrary is null)
|
||||
throw new InvalidOperationException("Misc library not found in plugins.");
|
||||
var scriptResolver = new ScriptResolver(registry.ScriptTypes);
|
||||
return new DynamicLibraryImpl(staticLibrary, registry.BattleStatCalculator,
|
||||
registry.DamageCalculator, registry.MiscLibrary);
|
||||
registry.DamageCalculator, registry.MiscLibrary, scriptResolver);
|
||||
}
|
||||
|
||||
private DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
|
||||
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary)
|
||||
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary, ScriptResolver scriptResolver)
|
||||
{
|
||||
StaticLibrary = staticLibrary;
|
||||
StatCalculator = statCalculator;
|
||||
DamageCalculator = damageCalculator;
|
||||
MiscLibrary = miscLibrary;
|
||||
ScriptResolver = scriptResolver;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -76,4 +84,7 @@ public class DynamicLibraryImpl : IDynamicLibrary
|
|||
|
||||
/// <inheritdoc />
|
||||
public IMiscLibrary MiscLibrary { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScriptResolver ScriptResolver { get; }
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
using System.Reflection;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for scripts.
|
||||
/// </summary>
|
||||
public static class ScriptUtils
|
||||
{
|
||||
private static readonly Dictionary<Type, StringKey> NameCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Resolve name from the <see cref="ScriptAttribute"/> of the given script.
|
||||
/// </summary>
|
||||
public static StringKey ResolveName(this Script script) => ResolveName(script.GetType());
|
||||
|
||||
/// <summary>
|
||||
/// Resolve name from the <see cref="ScriptAttribute"/> of the given type.
|
||||
/// </summary>
|
||||
public static StringKey ResolveName(Type type)
|
||||
{
|
||||
if (NameCache.TryGetValue(type, out var name))
|
||||
return name;
|
||||
|
||||
var scriptAttr = type.GetCustomAttribute<ScriptAttribute>();
|
||||
if (scriptAttr == null)
|
||||
throw new InvalidOperationException($"Type {type} does not have a {nameof(ScriptAttribute)}.");
|
||||
return NameCache[type] = scriptAttr.Name;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using PkmnLib.Dynamic.Libraries;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
|
@ -17,10 +18,11 @@ public abstract class Script
|
|||
private int _suppressCount;
|
||||
|
||||
/// <summary>
|
||||
/// The name of a script is its unique identifier. This should generally be set on load, and be
|
||||
/// the same as the key that was used to load it.
|
||||
/// The name of a script is its unique identifier.
|
||||
/// If not overridden, this will resolve the name from the <see cref="ScriptAttribute"/> of the
|
||||
/// script.
|
||||
/// </summary>
|
||||
public abstract StringKey Name { get; }
|
||||
public virtual StringKey Name => this.ResolveName();
|
||||
|
||||
public bool MarkForDeletion() => _markedForDeletion = true;
|
||||
public bool IsMarkedForDeletion() => _markedForDeletion;
|
||||
|
@ -69,7 +71,7 @@ public abstract class Script
|
|||
/// <summary>
|
||||
/// This function is ran when this script starts being in effect.
|
||||
/// </summary>
|
||||
public virtual void OnInitialize(IDynamicLibrary library, IReadOnlyDictionary<StringKey, object> parameters)
|
||||
public virtual void OnInitialize(IDynamicLibrary library, IReadOnlyDictionary<StringKey, object>? parameters)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -356,7 +358,7 @@ public abstract class Script
|
|||
/// so changing this to above or equal to 100 will make it always hit, while setting it to equal
|
||||
/// or below 0 will make it never hit.
|
||||
/// </summary>
|
||||
public virtual void ChangeIncomingEffectChance(IExecutingMove move, IPokemon target, ref float chance)
|
||||
public virtual void ChangeIncomingEffectChance(IExecutingMove move, IPokemon target, byte hit, ref float chance)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
|
||||
|
@ -29,7 +30,7 @@ public class ScriptContainer : IEnumerable<ScriptContainer>
|
|||
/// <summary>
|
||||
/// The script in this container.
|
||||
/// </summary>
|
||||
public Script? Script { get; set; } = null;
|
||||
public Script? Script { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<ScriptContainer> GetEnumerator()
|
||||
|
@ -42,4 +43,27 @@ public class ScriptContainer : IEnumerable<ScriptContainer>
|
|||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public void Set(Script script)
|
||||
{
|
||||
if (Script is not null)
|
||||
{
|
||||
Script.OnRemove();
|
||||
Script.MarkForDeletion();
|
||||
}
|
||||
Script = script;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the script from this container.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (Script is not null)
|
||||
{
|
||||
Script.OnRemove();
|
||||
Script.MarkForDeletion();
|
||||
}
|
||||
Script = null;
|
||||
}
|
||||
}
|
|
@ -24,4 +24,18 @@ public static class ScriptExecution
|
|||
hook(script);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RunScriptHook(this IReadOnlyList<IEnumerable<ScriptContainer>> source, Action<Script> hook)
|
||||
{
|
||||
foreach (var container in source.SelectMany(x => x))
|
||||
{
|
||||
if (container.IsEmpty)
|
||||
continue;
|
||||
var script = container.Script;
|
||||
if (script.IsSuppressed)
|
||||
continue;
|
||||
hook(script);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for the creation of <see cref="Script"/> instances.
|
||||
/// </summary>
|
||||
public class ScriptResolver
|
||||
{
|
||||
private Dictionary<(ScriptCategory, StringKey), Func<Script>> _scriptCtors;
|
||||
|
||||
/// <inheritdoc cref="ScriptResolver"/>
|
||||
public ScriptResolver(Dictionary<(ScriptCategory, StringKey), Func<Script>> scriptCtors)
|
||||
{
|
||||
_scriptCtors = scriptCtors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try and create a new script for the given category and key. If the script does not exist, return false.
|
||||
/// </summary>
|
||||
public bool TryResolve(ScriptCategory category, StringKey key, [MaybeNullWhen(false)] out Script script)
|
||||
{
|
||||
if (!_scriptCtors.TryGetValue((category, key), out var scriptCtor))
|
||||
{
|
||||
script = null;
|
||||
return false;
|
||||
}
|
||||
script = scriptCtor();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try and resolve an item script for the given item. If the item does not have a script, return false.
|
||||
/// </summary>
|
||||
public bool TryResolveItemScript(IItem item, [MaybeNullWhen(false)] out Script script)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -115,7 +115,18 @@ public class ScriptSet : IScriptSet
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear() => _scripts.Clear();
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var script in _scripts)
|
||||
{
|
||||
if (!script.IsEmpty)
|
||||
{
|
||||
script.Script.OnRemove();
|
||||
script.Script.MarkForDeletion();
|
||||
}
|
||||
}
|
||||
_scripts.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(StringKey scriptKey) => _scripts.Any(s => s.Script?.Name == scriptKey);
|
||||
|
|
|
@ -75,6 +75,7 @@ public record ImmutableStatisticSet<T>
|
|||
public record StatisticSet<T> : ImmutableStatisticSet<T>
|
||||
where T : struct
|
||||
{
|
||||
/// <inheritdoc cref="StatisticSet{T}"/>
|
||||
public StatisticSet() : base(default, default, default, default, default, default)
|
||||
{
|
||||
}
|
||||
|
@ -270,6 +271,7 @@ public record StatBoostStatisticSet : ClampedStatisticSet<sbyte>
|
|||
/// <inheritdoc />
|
||||
protected override sbyte Max => 6;
|
||||
|
||||
/// <inheritdoc cref="StatBoostStatisticSet"/>
|
||||
public StatBoostStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
||||
{
|
||||
}
|
||||
|
@ -292,6 +294,7 @@ public record IndividualValueStatisticSet : ClampedStatisticSet<byte>
|
|||
/// <inheritdoc />
|
||||
protected override byte Max => 31;
|
||||
|
||||
/// <inheritdoc cref="IndividualValueStatisticSet"/>
|
||||
public IndividualValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
||||
{
|
||||
}
|
||||
|
@ -314,6 +317,7 @@ public record EffortValueStatisticSet : ClampedStatisticSet<byte>
|
|||
/// <inheritdoc />
|
||||
protected override byte Max => 252;
|
||||
|
||||
/// <inheritdoc cref="EffortValueStatisticSet"/>
|
||||
public EffortValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
namespace PkmnLib.Static.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IEnumerable{T}"/>.
|
||||
/// </summary>
|
||||
public static class EnumerableHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all elements of the enumerable that are not null.
|
||||
/// </summary>
|
||||
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> enumerable) where T : class =>
|
||||
enumerable.Where(x => x is not null)!;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Plugin.Gen7.Moves;
|
||||
using PkmnLib.Static;
|
||||
|
||||
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
|
||||
|
||||
public class AcrobaticsTests
|
||||
{
|
||||
[Test]
|
||||
public void ChangeBasePower_UserNotHoldingItem_BasePowerDoubles()
|
||||
{
|
||||
// Arrange
|
||||
var move = new Mock<IExecutingMove>();
|
||||
var target = new Mock<IPokemon>();
|
||||
byte basePower = 10;
|
||||
move.Setup(m => m.User).Returns(new Mock<IPokemon>().Object);
|
||||
move.Setup(m => m.User.HeldItem).Returns((IItem?)null);
|
||||
var acrobatics = new Acrobatics();
|
||||
|
||||
// Act
|
||||
acrobatics.ChangeBasePower(move.Object, target.Object, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
Assert.That(basePower, Is.EqualTo(20));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChangeBasePower_UserHoldingItem_BasePowerUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var move = new Mock<IExecutingMove>();
|
||||
var target = new Mock<IPokemon>();
|
||||
byte basePower = 10;
|
||||
move.Setup(m => m.User).Returns(new Mock<IPokemon>().Object);
|
||||
move.Setup(m => m.User.HeldItem).Returns(new Mock<IItem>().Object);
|
||||
var acrobatics = new Acrobatics();
|
||||
|
||||
// Act
|
||||
acrobatics.ChangeBasePower(move.Object, target.Object, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
Assert.That(basePower, Is.EqualTo(10));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChangeBasePower_UserNotHoldingItem_NoOverflow()
|
||||
{
|
||||
// Arrange
|
||||
var move = new Mock<IExecutingMove>();
|
||||
var target = new Mock<IPokemon>();
|
||||
byte basePower = 200;
|
||||
move.Setup(m => m.User).Returns(new Mock<IPokemon>().Object);
|
||||
move.Setup(m => m.User.HeldItem).Returns((IItem?)null);
|
||||
var acrobatics = new Acrobatics();
|
||||
|
||||
// Act
|
||||
acrobatics.ChangeBasePower(move.Object, target.Object, 0, ref basePower);
|
||||
|
||||
// Assert
|
||||
Assert.That(basePower, Is.EqualTo(byte.MaxValue));
|
||||
}
|
||||
|
||||
}
|
|
@ -14,9 +14,6 @@ namespace PkmnLib.Plugin.Gen7.Moves;
|
|||
[Script(ScriptCategory.Move, "acrobatics")]
|
||||
public class Acrobatics : Script
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override StringKey Name => "acrobatics";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref byte basePower)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue