Lots more work on implementing battling

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

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