Lots more work on implementing battling

This commit is contained in:
Deukhoofd 2024-08-10 09:44:46 +02:00
parent 554e1cf2cd
commit a049dda240
29 changed files with 1226 additions and 48 deletions

View File

@ -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 />

View File

@ -0,0 +1,7 @@
namespace PkmnLib.Dynamic.Events;
public class EndTurnEvent : IEventData
{
/// <inheritdoc />
public EventBatchId BatchId { get; init; }
}

View File

@ -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 />

View File

@ -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; }
}

View File

@ -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();
}

View 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; }
}

View 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; }
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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;
}

View 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)
{
}
}

View 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
}
}
}

View File

@ -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 });
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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)
{
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View 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();
}
}

View File

@ -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);

View File

@ -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)
{
}

View File

@ -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)!;
}

View File

@ -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));
}
}

View File

@ -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)
{