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;
|
namespace PkmnLib.Dynamic.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event triggered when a Pokemon takes damage.
|
||||||
|
/// </summary>
|
||||||
public record DamageEvent : IEventData
|
public record DamageEvent : IEventData
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="DamageEvent"/>
|
||||||
public DamageEvent(IPokemon pokemon, uint previousHealth, uint newHealth, DamageSource source)
|
public DamageEvent(IPokemon pokemon, uint previousHealth, uint newHealth, DamageSource source)
|
||||||
{
|
{
|
||||||
Pokemon = pokemon;
|
Pokemon = pokemon;
|
||||||
@ -12,9 +16,24 @@ public record DamageEvent : IEventData
|
|||||||
Source = source;
|
Source = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Pokemon that took damage.
|
||||||
|
/// </summary>
|
||||||
public IPokemon Pokemon { get; init; }
|
public IPokemon Pokemon { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The previous health of the Pokemon.
|
||||||
|
/// </summary>
|
||||||
public uint PreviousHealth { get; init; }
|
public uint PreviousHealth { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new health of the Pokemon.
|
||||||
|
/// </summary>
|
||||||
public uint NewHealth { get; init; }
|
public uint NewHealth { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source of the damage.
|
||||||
|
/// </summary>
|
||||||
public DamageSource Source { get; init; }
|
public DamageSource Source { get; init; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
7
PkmnLib.Dynamic/Events/EndTurnEvent.cs
Normal file
7
PkmnLib.Dynamic/Events/EndTurnEvent.cs
Normal file
@ -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;
|
namespace PkmnLib.Dynamic.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event triggered when a Pokemon faints.
|
||||||
|
/// </summary>
|
||||||
public class FaintEvent : IEventData
|
public class FaintEvent : IEventData
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="FaintEvent"/>
|
||||||
public FaintEvent(IPokemon pokemon)
|
public FaintEvent(IPokemon pokemon)
|
||||||
{
|
{
|
||||||
Pokemon = pokemon;
|
Pokemon = pokemon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Pokemon that fainted.
|
||||||
|
/// </summary>
|
||||||
public IPokemon Pokemon { get; init; }
|
public IPokemon Pokemon { get; init; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -7,5 +7,9 @@ namespace PkmnLib.Dynamic.Events;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEventData
|
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; }
|
public EventBatchId BatchId { get; init; }
|
||||||
}
|
}
|
@ -2,8 +2,12 @@ using PkmnLib.Dynamic.Models;
|
|||||||
|
|
||||||
namespace PkmnLib.Dynamic.Events;
|
namespace PkmnLib.Dynamic.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event triggered when a Pokemon is healed.
|
||||||
|
/// </summary>
|
||||||
public class HealEvent : IEventData
|
public class HealEvent : IEventData
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="HealEvent"/>
|
||||||
public HealEvent(IPokemon pokemon, uint previousHealth, uint newHealth)
|
public HealEvent(IPokemon pokemon, uint previousHealth, uint newHealth)
|
||||||
{
|
{
|
||||||
Pokemon = pokemon;
|
Pokemon = pokemon;
|
||||||
@ -11,8 +15,19 @@ public class HealEvent : IEventData
|
|||||||
NewHealth = newHealth;
|
NewHealth = newHealth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Pokemon that was healed.
|
||||||
|
/// </summary>
|
||||||
public IPokemon Pokemon { get; }
|
public IPokemon Pokemon { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The previous health of the Pokemon.
|
||||||
|
/// </summary>
|
||||||
public uint PreviousHealth { get; }
|
public uint PreviousHealth { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new health of the Pokemon.
|
||||||
|
/// </summary>
|
||||||
public uint NewHealth { get; }
|
public uint NewHealth { get; }
|
||||||
|
|
||||||
|
|
||||||
|
47
PkmnLib.Dynamic/Events/StatBoostEvent.cs
Normal file
47
PkmnLib.Dynamic/Events/StatBoostEvent.cs
Normal file
@ -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; }
|
||||||
|
}
|
35
PkmnLib.Dynamic/Events/SwitchEvent.cs
Normal file
35
PkmnLib.Dynamic/Events/SwitchEvent.cs
Normal file
@ -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.Dynamic.ScriptHandling.Registry;
|
||||||
using PkmnLib.Static.Libraries;
|
using PkmnLib.Static.Libraries;
|
||||||
|
|
||||||
@ -30,6 +31,11 @@ public interface IDynamicLibrary
|
|||||||
/// calculators.
|
/// calculators.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IMiscLibrary MiscLibrary { get; }
|
IMiscLibrary MiscLibrary { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A holder of the script types that can be resolved by this library.
|
||||||
|
/// </summary>
|
||||||
|
ScriptResolver ScriptResolver { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -52,17 +58,19 @@ public class DynamicLibraryImpl : IDynamicLibrary
|
|||||||
throw new InvalidOperationException("Stat calculator not found in plugins.");
|
throw new InvalidOperationException("Stat calculator not found in plugins.");
|
||||||
if (registry.MiscLibrary is null)
|
if (registry.MiscLibrary is null)
|
||||||
throw new InvalidOperationException("Misc library not found in plugins.");
|
throw new InvalidOperationException("Misc library not found in plugins.");
|
||||||
|
var scriptResolver = new ScriptResolver(registry.ScriptTypes);
|
||||||
return new DynamicLibraryImpl(staticLibrary, registry.BattleStatCalculator,
|
return new DynamicLibraryImpl(staticLibrary, registry.BattleStatCalculator,
|
||||||
registry.DamageCalculator, registry.MiscLibrary);
|
registry.DamageCalculator, registry.MiscLibrary, scriptResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
|
private DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
|
||||||
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary)
|
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary, ScriptResolver scriptResolver)
|
||||||
{
|
{
|
||||||
StaticLibrary = staticLibrary;
|
StaticLibrary = staticLibrary;
|
||||||
StatCalculator = statCalculator;
|
StatCalculator = statCalculator;
|
||||||
DamageCalculator = damageCalculator;
|
DamageCalculator = damageCalculator;
|
||||||
MiscLibrary = miscLibrary;
|
MiscLibrary = miscLibrary;
|
||||||
|
ScriptResolver = scriptResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -76,4 +84,7 @@ public class DynamicLibraryImpl : IDynamicLibrary
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IMiscLibrary MiscLibrary { get; }
|
public IMiscLibrary MiscLibrary { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ScriptResolver ScriptResolver { get; }
|
||||||
}
|
}
|
@ -1,7 +1,9 @@
|
|||||||
using PkmnLib.Dynamic.Events;
|
using PkmnLib.Dynamic.Events;
|
||||||
using PkmnLib.Dynamic.Libraries;
|
using PkmnLib.Dynamic.Libraries;
|
||||||
|
using PkmnLib.Dynamic.Models.BattleFlow;
|
||||||
using PkmnLib.Dynamic.Models.Choices;
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
using PkmnLib.Dynamic.ScriptHandling;
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
namespace PkmnLib.Dynamic.Models;
|
namespace PkmnLib.Dynamic.Models;
|
||||||
|
|
||||||
@ -69,12 +71,12 @@ public interface IBattle : IScriptSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A queue of the yet to be executed choices in a turn.
|
/// A queue of the yet to be executed choices in a turn.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BattleChoiceQueue ChoiceQueue { get; }
|
BattleChoiceQueue? ChoiceQueue { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a Pokemon on the battlefield, on a specific side and an index on that side.
|
/// Get a Pokemon on the battlefield, on a specific side and an index on that side.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IPokemon GetPokemon(byte side, byte position);
|
IPokemon? GetPokemon(byte side, byte position);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether a slot on the battlefield can still be filled. If no party is responsible
|
/// Returns whether a slot on the battlefield can still be filled. If no party is responsible
|
||||||
@ -92,7 +94,7 @@ public interface IBattle : IScriptSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether a choice is actually possible.
|
/// Checks whether a choice is actually possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void CanUse(ITurnChoice choice);
|
bool CanUse(ITurnChoice choice);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try and set the choice for the battle. If the choice is not valid, this returns false.
|
/// Try and set the choice for the battle. If the choice is not valid, this returns false.
|
||||||
@ -102,10 +104,221 @@ public interface IBattle : IScriptSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the current weather for the battle. If null is passed, this clears the weather.
|
/// Sets the current weather for the battle. If null is passed, this clears the weather.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void SetWeather(string? weatherName);
|
void SetWeather(StringKey? weatherName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current weather of the battle. If no weather is present, this returns null.
|
/// Gets the current weather of the battle. If no weather is present, this returns null.
|
||||||
/// </summary>
|
/// </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;
|
namespace PkmnLib.Dynamic.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -10,4 +13,86 @@ namespace PkmnLib.Dynamic.Models;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class BattleChoiceQueue
|
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.
|
/// The backing Pokemon party.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IPokemonParty Party { get; }
|
IPokemonParty Party { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the party is responsible for the specified side and position.
|
/// Whether the party is responsible for the specified side and position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsResponsibleForIndex(byte side, byte position);
|
bool IsResponsibleForIndex(ResponsibleIndex index);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the party has a living Pokemon left that is not in the field.
|
/// Whether the party has a living Pokemon left that is not in the field.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool HasPokemonNotInField();
|
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;
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
namespace PkmnLib.Dynamic.Models;
|
namespace PkmnLib.Dynamic.Models;
|
||||||
@ -14,3 +15,19 @@ public interface IBattleRandom : IRandom
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber);
|
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.Models.Choices;
|
||||||
using PkmnLib.Dynamic.ScriptHandling;
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
namespace PkmnLib.Dynamic.Models;
|
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
|
/// 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.
|
/// empty, but can't be filled by any party anymore.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void AllPositionsFilled();
|
bool AllPositionsFilled();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a choice for a Pokémon on this side.
|
/// Sets a choice for a Pokémon on this side.
|
||||||
@ -74,7 +76,8 @@ public interface IBattleSide : IScriptSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forcibly removes a Pokémon from the field.
|
/// Forcibly removes a Pokémon from the field.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ForceClearPokemonFromField();
|
/// <param name="index"></param>
|
||||||
|
void ForceClearPokemonFromField(byte index);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Switches out a spot on the field for a different Pokémon. If null is passed, the spot is
|
/// Switches out a spot on the field for a different Pokémon. If null is passed, the spot is
|
||||||
@ -118,3 +121,167 @@ public interface IBattleSide : IScriptSource
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
byte GetRandomPosition();
|
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>
|
/// </summary>
|
||||||
public class TurnChoiceComparer : IComparer<ITurnChoice>
|
public class TurnChoiceComparer : IComparer<ITurnChoice>
|
||||||
{
|
{
|
||||||
|
public static TurnChoiceComparer Instance { get; } = new();
|
||||||
|
|
||||||
private enum CompareValues
|
private enum CompareValues
|
||||||
{
|
{
|
||||||
XEqualsY = 0,
|
XEqualsY = 0,
|
||||||
@ -12,7 +14,7 @@ public class TurnChoiceComparer : IComparer<ITurnChoice>
|
|||||||
XGreaterThanY = 1
|
XGreaterThanY = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompareValues CompareForSameType(ITurnChoice x, ITurnChoice y)
|
private static CompareValues CompareForSameType(ITurnChoice x, ITurnChoice y)
|
||||||
{
|
{
|
||||||
// Higher speed goes first
|
// Higher speed goes first
|
||||||
var speedComparison = x.Speed.CompareTo(y.Speed);
|
var speedComparison = x.Speed.CompareTo(y.Speed);
|
||||||
@ -23,7 +25,7 @@ public class TurnChoiceComparer : IComparer<ITurnChoice>
|
|||||||
return (CompareValues)x.RandomValue.CompareTo(y.RandomValue);
|
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
|
// Deal with possible null values
|
||||||
switch (x)
|
switch (x)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using PkmnLib.Dynamic.ScriptHandling;
|
using PkmnLib.Dynamic.ScriptHandling;
|
||||||
using PkmnLib.Static;
|
using PkmnLib.Static;
|
||||||
using PkmnLib.Static.Moves;
|
using PkmnLib.Static.Moves;
|
||||||
|
using PkmnLib.Static.Utils.Errors;
|
||||||
|
|
||||||
namespace PkmnLib.Dynamic.Models;
|
namespace PkmnLib.Dynamic.Models;
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ public interface IExecutingMove : IScriptSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a hit data for a target, with a specific index.
|
/// Gets a hit data for a target, with a specific index.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
HitData GetHitData(IPokemon target, byte hit);
|
IHitData GetHitData(IPokemon target, byte hit);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether a Pokémon is a target for this move.
|
/// Checks whether a Pokémon is a target for this move.
|
||||||
@ -125,5 +126,99 @@ public interface IExecutingMove : IScriptSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a hit based on its raw index.
|
/// Gets a hit based on its raw index.
|
||||||
/// </summary>
|
/// </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);
|
||||||
|
}
|
||||||
}
|
}
|
@ -54,6 +54,11 @@ public interface ILearnedMove
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
byte MaxPp { get; }
|
byte MaxPp { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current power points for this move.
|
||||||
|
/// </summary>
|
||||||
|
byte CurrentPp { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The way the move has been learned.
|
/// The way the move has been learned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -217,7 +217,7 @@ public interface IPokemon : IScriptSource
|
|||||||
/// <param name="stat">The stat to be changed</param>
|
/// <param name="stat">The stat to be changed</param>
|
||||||
/// <param name="change">The amount to change the stat by</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>
|
/// <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>
|
/// <summary>
|
||||||
/// Returns the currently active ability.
|
/// Returns the currently active ability.
|
||||||
@ -283,6 +283,27 @@ public interface IPokemon : IScriptSource
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void ChangeLevelBy(int change);
|
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
|
// TODO: (de)serialize
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,25 +316,35 @@ public interface IPokemonBattleData
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The battle the Pokemon is in.
|
/// The battle the Pokemon is in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IBattle Battle { get; }
|
IBattle Battle { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The index of the side of the Pokemon
|
/// The index of the side of the Pokemon
|
||||||
/// </summary>
|
/// </summary>
|
||||||
byte SideIndex { get; }
|
byte SideIndex { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The index of the position of the Pokemon on the field
|
/// The index of the position of the Pokemon on the field
|
||||||
/// </summary>
|
/// </summary>
|
||||||
byte Position { get; }
|
byte Position { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of opponents the Pokemon has seen this battle.
|
/// A list of opponents the Pokemon has seen this battle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IReadOnlyList<IPokemon> SeenOpponents { get; }
|
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
|
public class PokemonImpl : ScriptSource, IPokemon
|
||||||
{
|
{
|
||||||
/// <inheritdoc cref="PokemonImpl"/>
|
/// <inheritdoc cref="PokemonImpl"/>
|
||||||
@ -358,7 +389,7 @@ public class PokemonImpl : ScriptSource, IPokemon
|
|||||||
public IForm? DisplayForm { get; set; }
|
public IForm? DisplayForm { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LevelInt Level { get; }
|
public LevelInt Level { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public uint Experience { get; }
|
public uint Experience { get; }
|
||||||
@ -468,13 +499,42 @@ public class PokemonImpl : ScriptSource, IPokemon
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool ConsumeHeldItem()
|
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();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 />
|
/// <inheritdoc />
|
||||||
@ -499,10 +559,7 @@ public class PokemonImpl : ScriptSource, IPokemon
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void RecalculateBoostedStats()
|
public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
|
||||||
{
|
|
||||||
Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ChangeSpecies(ISpecies species, IForm form)
|
public void ChangeSpecies(ISpecies species, IForm form)
|
||||||
@ -612,6 +669,7 @@ public class PokemonImpl : ScriptSource, IPokemon
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= Moves.Count)
|
if (index >= Moves.Count)
|
||||||
throw new InvalidOperationException("No empty move slot found.");
|
throw new InvalidOperationException("No empty move slot found.");
|
||||||
if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
|
if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
|
||||||
@ -621,17 +679,57 @@ public class PokemonImpl : ScriptSource, IPokemon
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ClearStatus()
|
public void ClearStatus() => StatusScript.Clear();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ChangeLevelBy(int change)
|
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 />
|
/// <inheritdoc />
|
||||||
public override int ScriptCount
|
public override int ScriptCount
|
||||||
{
|
{
|
||||||
@ -668,3 +766,36 @@ public class PokemonImpl : ScriptSource, IPokemon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
}
|
31
PkmnLib.Dynamic/ScriptHandling/Registry/ScriptUtils.cs
Normal file
31
PkmnLib.Dynamic/ScriptHandling/Registry/ScriptUtils.cs
Normal file
@ -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.Libraries;
|
||||||
using PkmnLib.Dynamic.Models;
|
using PkmnLib.Dynamic.Models;
|
||||||
using PkmnLib.Dynamic.Models.Choices;
|
using PkmnLib.Dynamic.Models.Choices;
|
||||||
|
using PkmnLib.Dynamic.ScriptHandling.Registry;
|
||||||
using PkmnLib.Static;
|
using PkmnLib.Static;
|
||||||
using PkmnLib.Static.Utils;
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
@ -17,10 +18,11 @@ public abstract class Script
|
|||||||
private int _suppressCount;
|
private int _suppressCount;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of a script is its unique identifier. This should generally be set on load, and be
|
/// The name of a script is its unique identifier.
|
||||||
/// the same as the key that was used to load it.
|
/// If not overridden, this will resolve the name from the <see cref="ScriptAttribute"/> of the
|
||||||
|
/// script.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract StringKey Name { get; }
|
public virtual StringKey Name => this.ResolveName();
|
||||||
|
|
||||||
public bool MarkForDeletion() => _markedForDeletion = true;
|
public bool MarkForDeletion() => _markedForDeletion = true;
|
||||||
public bool IsMarkedForDeletion() => _markedForDeletion;
|
public bool IsMarkedForDeletion() => _markedForDeletion;
|
||||||
@ -69,7 +71,7 @@ public abstract class Script
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// This function is ran when this script starts being in effect.
|
/// This function is ran when this script starts being in effect.
|
||||||
/// </summary>
|
/// </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
|
/// 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.
|
/// or below 0 will make it never hit.
|
||||||
/// </summary>
|
/// </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.Collections;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using PkmnLib.Static.Utils;
|
||||||
|
|
||||||
namespace PkmnLib.Dynamic.ScriptHandling;
|
namespace PkmnLib.Dynamic.ScriptHandling;
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ public class ScriptContainer : IEnumerable<ScriptContainer>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The script in this container.
|
/// The script in this container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Script? Script { get; set; } = null;
|
public Script? Script { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerator<ScriptContainer> GetEnumerator()
|
public IEnumerator<ScriptContainer> GetEnumerator()
|
||||||
@ -42,4 +43,27 @@ public class ScriptContainer : IEnumerable<ScriptContainer>
|
|||||||
{
|
{
|
||||||
return GetEnumerator();
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
41
PkmnLib.Dynamic/ScriptHandling/ScriptResolver.cs
Normal file
41
PkmnLib.Dynamic/ScriptHandling/ScriptResolver.cs
Normal file
@ -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 />
|
/// <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 />
|
/// <inheritdoc />
|
||||||
public bool Contains(StringKey scriptKey) => _scripts.Any(s => s.Script?.Name == scriptKey);
|
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>
|
public record StatisticSet<T> : ImmutableStatisticSet<T>
|
||||||
where T : struct
|
where T : struct
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="StatisticSet{T}"/>
|
||||||
public StatisticSet() : base(default, default, default, default, default, default)
|
public StatisticSet() : base(default, default, default, default, default, default)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -270,6 +271,7 @@ public record StatBoostStatisticSet : ClampedStatisticSet<sbyte>
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override sbyte Max => 6;
|
protected override sbyte Max => 6;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="StatBoostStatisticSet"/>
|
||||||
public StatBoostStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
public StatBoostStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -292,6 +294,7 @@ public record IndividualValueStatisticSet : ClampedStatisticSet<byte>
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override byte Max => 31;
|
protected override byte Max => 31;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IndividualValueStatisticSet"/>
|
||||||
public IndividualValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
public IndividualValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -314,6 +317,7 @@ public record EffortValueStatisticSet : ClampedStatisticSet<byte>
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override byte Max => 252;
|
protected override byte Max => 252;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="EffortValueStatisticSet"/>
|
||||||
public EffortValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
public EffortValueStatisticSet() : base(0, 0, 0, 0, 0, 0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
namespace PkmnLib.Static.Utils;
|
namespace PkmnLib.Static.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for <see cref="IEnumerable{T}"/>.
|
||||||
|
/// </summary>
|
||||||
public static class EnumerableHelpers
|
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 =>
|
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> enumerable) where T : class =>
|
||||||
enumerable.Where(x => x is not null)!;
|
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")]
|
[Script(ScriptCategory.Move, "acrobatics")]
|
||||||
public class Acrobatics : Script
|
public class Acrobatics : Script
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public override StringKey Name => "acrobatics";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref byte basePower)
|
public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref byte basePower)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user