Begin work on outlining dynamic side

This commit is contained in:
Deukhoofd 2024-07-27 16:26:45 +02:00
parent 1b501dee7e
commit a251913ebd
44 changed files with 2150 additions and 19 deletions

View File

@ -0,0 +1,17 @@
namespace PkmnLib.Dynamic.Events;
/// <summary>
/// Allows for the batching of multiple events that should be shown at the same time.
/// </summary>
/// <remarks>
/// This is useful for when we send multiple events to the client in sequence, but we want to show them in one go.
/// For example, when a Pokemon gets hurt by poison, we want to show the purple poison animation and the damage at the
/// same time. This is done by batching the events together.
/// </remarks>
public record struct EventBatchId()
{
/// <summary>
/// The unique identifier for this batch of events.
/// </summary>
public Guid Id { get; init; } = Guid.NewGuid();
}

View File

@ -0,0 +1,11 @@
namespace PkmnLib.Dynamic.Events;
/// <summary>
/// An event is something that happens during a battle. This can be used by front-end code to
/// display information about the battle to the user. This is the only way for the front-end to
/// know what is happening in the battle.
/// </summary>
public interface IEventData
{
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Dynamic.Events;
/// <summary>
/// The event hook of a battle. This is used to hook into the battle and get notified when something
/// happens that a front-end might want to know about.
/// </summary>
public class EventHook
{
/// <summary>
/// The event handler that is called when the event is triggered.
/// </summary>
public event EventHandler<IEventData>? Handler;
/// <summary>
/// Triggers the event, calling the handler with the given data.
/// </summary>
public void Invoke(IEventData data)
{
Handler?.Invoke(this, data);
}
}

View File

@ -0,0 +1,6 @@
global using LevelInt = byte;
public class Const
{
public const int MovesCount = 4;
}

View File

@ -0,0 +1,30 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
namespace PkmnLib.Dynamic.Libraries;
/// <summary>
/// A battle stat calculator is used to calculate stats for a Pokemon.
/// </summary>
public interface IBattleStatCalculator
{
/// <summary>
/// Calculate all the flat stats of a Pokemon, disregarding stat boosts.
/// </summary>
void CalculateFlatStats(IPokemon pokemon, StatisticSet<uint> stats);
/// <summary>
/// Calculate a single flat stat of a Pokemon, disregarding stat boosts.
/// </summary>
uint CalculateFlatStat(IPokemon pokemon, Statistic stat);
/// <summary>
/// Calculate all the boosted stats of a Pokemon, including stat boosts.
/// </summary>
void CalculateBoostedStats(IPokemon pokemon, StatisticSet<uint> stats);
/// <summary>
/// Calculate a single boosted stat of a Pokemon, including stat boosts.
/// </summary>
uint CalculateBoostedStat(IPokemon pokemon, Statistic stat);
}

View File

@ -0,0 +1,24 @@
using PkmnLib.Dynamic.Models;
namespace PkmnLib.Dynamic.Libraries;
/// <summary>
/// A damage library holds the functions related to the calculation of damage.
/// </summary>
public interface IDamageCalculator
{
/// <summary>
/// Calculate the damage for a given hit on a Pokemon.
/// </summary>
uint GetDamage(IExecutingMove executingMove, IPokemon target, byte hitNumber, HitData hitData);
/// <summary>
/// Calculate the base power for a given hit on a Pokemon.
/// </summary>
byte GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, HitData hitData);
/// <summary>
/// Returns whether a specified hit should be critical or not.
/// </summary>
bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber);
}

View File

@ -0,0 +1,55 @@
using PkmnLib.Static.Libraries;
namespace PkmnLib.Dynamic.Libraries;
/// <summary>
/// The dynamic library stores a static data library, as well as holding different libraries and
/// calculators that might be customized between different generations and implementations.
/// </summary>
public interface IDynamicLibrary
{
/// <summary>
/// The static data is the immutable storage data for this library.
/// </summary>
IStaticLibrary StaticLibrary { get; }
/// <summary>
/// The stat calculator deals with the calculation of flat and boosted stats, based on the
/// Pokémon's attributes.
/// </summary>
IBattleStatCalculator StatCalculator { get; }
/// <summary>
/// The damage calculator deals with the calculation of things relating to damage.
/// </summary>
IDamageCalculator DamageCalculator { get; }
/// <summary>
/// The Misc Library holds minor functions that do not fall in any of the other libraries and
/// calculators.
/// </summary>
IMiscLibrary MiscLibrary { get; }
}
public class DynamicLibraryImpl : IDynamicLibrary
{
public DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary)
{
StaticLibrary = staticLibrary;
StatCalculator = statCalculator;
DamageCalculator = damageCalculator;
MiscLibrary = miscLibrary;
}
public IStaticLibrary StaticLibrary { get; }
/// <inheritdoc />
public IBattleStatCalculator StatCalculator { get; }
/// <inheritdoc />
public IDamageCalculator DamageCalculator { get; }
/// <inheritdoc />
public IMiscLibrary MiscLibrary { get; }
}

View File

@ -0,0 +1,19 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Static;
namespace PkmnLib.Dynamic.Libraries;
public interface IMiscLibrary
{
/// <summary>
/// Returns the choice that's used when a Pokemon is unable to make the move choice it wants to, or when it has no
/// moves left, yet wants to make a move.
/// </summary>
ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition);
/// <summary>
/// Gets the current time of day for the battle.
/// </summary>
TimeOfDay GetTimeOfDay();
}

View File

@ -0,0 +1,114 @@
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// A battle is a representation of a battle in the Pokemon games. It contains all the information needed
/// to simulate a battle, and can be used to simulate a battle between two parties.
/// </summary>
public interface IBattle : IScriptSource
{
/// <summary>
/// The library the battle uses for handling.
/// </summary>
IDynamicLibrary Library { get; }
/// <summary>
/// A list of all different parties in the battle.
/// </summary>
IReadOnlyList<IBattleParty> Parties { get; }
/// <summary>
/// Whether or not Pokemon can flee from the battle.
/// </summary>
bool CanFlee { get; }
/// <summary>
/// The number of sides in the battle. Typically 2.
/// </summary>
byte NumberOfSides { get; }
/// <summary>
/// The number of Pokemon that can be on each side.
/// </summary>
byte PositionsPerSide { get; }
/// <summary>
/// A list of all sides in the battle.
/// </summary>
IReadOnlyList<IBattleSide> Sides { get; }
/// <summary>
/// The RNG used for the battle.
/// </summary>
IBattleRandom Random { get; }
/// <summary>
/// Whether the battle has ended.
/// </summary>
bool HasEnded { get; }
/// <summary>
/// The result of the battle. If the battle has not ended, this is null.
/// </summary>
BattleResult? Result { get; }
/// <summary>
/// The handler to send all events to.
/// </summary>
EventHook EventHook { get; }
/// <summary>
/// The index of the current turn. Initially 0, until the first turn starts when all choices are made.
/// </summary>
uint CurrentTurnNumber { get; }
/// <summary>
/// A queue of the yet to be executed choices in a turn.
/// </summary>
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);
/// <summary>
/// Returns whether a slot on the battlefield can still be filled. If no party is responsible
/// for that slot, or a party is responsible, but has no remaining Pokemon to throw out anymore,
/// this returns false.
/// </summary>
bool CanSlotBeFilled(byte side, byte position);
/// <summary>
/// Validates whether the battle is still in a non-ended state. If the battle has ended, this
/// properly sets who has won etc.
/// </summary>
void ValidateBattleState();
/// <summary>
/// Checks whether a choice is actually possible.
/// </summary>
void 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);
/// <summary>
/// Gets the current weather of the battle. If no weather is present, this returns null.
/// </summary>
string? WeatherName { get; }
}

View File

@ -0,0 +1,13 @@
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// The ChoiceQueue is used to run choices one by one.
/// </summary>
/// <remarks>
/// It functions internally by holding a list of choices, and one by one setting items that have been returned to null,
/// It holds several helper functions to change the turn order while doing the execution. This is needed, as several
/// moves in Pokémon actively mess with this order.
/// </remarks>
public class BattleChoiceQueue
{
}

View File

@ -0,0 +1,8 @@
namespace PkmnLib.Dynamic.Models;
public interface IBattleParty
{
IPokemonParty Party { get; }
bool IsResponsibleForIndex(byte side, byte position);
bool HasPokemonNotInField();
}

View File

@ -0,0 +1,13 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Models;
public interface IBattleRandom : IRandom
{
/// <summary>
/// Gets whether or not a move triggers its secondary effect. This takes its chance, and
/// rolls whether it triggers. As a side effect this run scripts to allow modifying this random
/// chance.
/// </summary>
bool EffectChance(float chance, IExecutingMove executingMove, IPokemon target, byte hitNumber);
}

View File

@ -0,0 +1,23 @@
namespace PkmnLib.Dynamic.Models;
public record struct BattleResult
{
private BattleResult(bool conclusiveResult, byte? winningSide)
{
ConclusiveResult = conclusiveResult;
WinningSide = winningSide;
}
public static BattleResult Inconclusive => new(false, null);
public static BattleResult Conclusive(byte winningSide) => new(true, winningSide);
/// <summary>
/// Whether the battle has a conclusive result. If false, no side has won.
/// </summary>
public bool ConclusiveResult { get; }
/// <summary>
/// The side that won the battle. If null, no side has won.
/// </summary>
public byte? WinningSide { get; }
}

View File

@ -0,0 +1,120 @@
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// A side in a battle.
/// </summary>
public interface IBattleSide : IScriptSource
{
/// <summary>
/// The index of the side on the battle.
/// </summary>
byte Index { get; }
/// <summary>
/// The number of Pokémon that can be on the side.
/// </summary>
byte NumberOfPositions { get; }
/// <summary>
/// A list of Pokémon currently on the battlefield.
/// </summary>
IReadOnlyList<IPokemon?> Pokemon { get; }
/// <summary>
/// The currently set choices for all Pokémon on the battlefield. Cleared when the turn starts.
/// </summary>
IReadOnlyList<ITurnChoice?> SetChoices { get; }
/// <summary>
/// Whether every Pokémon on this side has its choices
/// </summary>
bool AllChoicesSet { get; }
/// <summary>
/// The slots on the side that can still be filled. Once all slots are set to false, this side
/// has lost the battle.
/// </summary>
IReadOnlyList<bool> FillablePositions { get; }
/// <summary>
/// A reference to the battle this side is in.
/// </summary>
IBattle Battle { get; }
/// <summary>
/// Whether this side has fled.
/// </summary>
bool HasFledBattle { get; }
/// <summary>
/// The volatile scripts that are attached to the side.
/// </summary>
IScriptSet VolatileScripts { get; }
/// <summary>
/// Returns true if there are slots that need to be filled with a new pokemon, that have parties
/// 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();
/// <summary>
/// Sets a choice for a Pokémon on this side.
/// </summary>
void SetChoice(byte position, ITurnChoice choice);
/// <summary>
/// Resets all choices on this side.
/// </summary>
void ResetChoices();
/// <summary>
/// Forcibly removes a Pokémon from the field.
/// </summary>
void ForceClearPokemonFromField();
/// <summary>
/// Switches out a spot on the field for a different Pokémon. If null is passed, the spot is
/// cleared. Returns the Pokémon that was previously in the spot.
/// </summary>
IPokemon? SwapPokemon(byte position, IPokemon? pokemon);
/// <summary>
/// Swaps two Pokémon on the side.
/// </summary>
void SwapPokemon(byte position1, byte position2);
/// <summary>
/// Checks whether a Pokemon is on the field in this side.
/// </summary>
bool IsPokemonOnSide(IPokemon pokemon);
/// <summary>
/// Marks a slot as unfillable. This happens when no parties are able to fill the slot anymore.
/// If this happens, the slot can not be used again.
/// </summary>
void MarkPositionAsUnfillable(byte position);
/// <summary>
/// Checks whether a slot is fillable. If it is not, the slot can not be used anymore.
/// </summary>
bool IsPositionFillable(byte position);
/// <summary>
/// Checks whether the side has been defeated.
/// </summary>
bool IsDefeated();
/// <summary>
/// Mark the side as fled.
/// </summary>
void MarkAsFled();
/// <summary>
/// Gets a random Pokémon on the given side.
/// </summary>
byte GetRandomPosition();
}

View File

@ -0,0 +1,6 @@
namespace PkmnLib.Dynamic.Models.Choices;
public interface IFleeChoice : ITurnChoice
{
}

View File

@ -0,0 +1,6 @@
namespace PkmnLib.Dynamic.Models.Choices;
public interface IItemChoice : ITurnChoice
{
}

View File

@ -0,0 +1,74 @@
using PkmnLib.Dynamic.ScriptHandling;
namespace PkmnLib.Dynamic.Models.Choices;
/// <summary>
/// The choice of a Pokémon to use a move.
/// </summary>
public interface IMoveChoice : ITurnChoice
{
/// <summary>
/// The move that is used.
/// </summary>
ILearnedMove UsedMove { get; }
/// <summary>
/// The side the move is targeted at.
/// </summary>
byte TargetSide { get; }
/// <summary>
/// The position the move is targeted at.
/// </summary>
byte TargetPosition { get; }
/// <summary>
/// The priority of the move.
/// </summary>
sbyte Priority { get; set; }
/// <summary>
/// The underlying script of the move.
/// </summary>
ScriptContainer Script { get; set; }
}
/// <inheritdoc cref="IMoveChoice"/>
public class MoveChoice : TurnChoice, IMoveChoice
{
/// <inheritdoc cref="MoveChoice"/>
public MoveChoice(IPokemon user, ILearnedMove usedMove, byte targetSide, byte targetPosition) : base(user)
{
UsedMove = usedMove;
TargetSide = targetSide;
TargetPosition = targetPosition;
}
/// <inheritdoc />
public ILearnedMove UsedMove { get; }
/// <inheritdoc />
public byte TargetSide { get; }
/// <inheritdoc />
public byte TargetPosition { get; }
/// <inheritdoc />
public sbyte Priority { get; set; }
/// <inheritdoc />
public ScriptContainer Script { get; set; } = new();
/// <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)
{
GetOwnScripts(scripts);
User.CollectScripts(scripts);
}
}

View File

@ -0,0 +1,6 @@
namespace PkmnLib.Dynamic.Models.Choices;
public interface IPassChoice : ITurnChoice
{
}

View File

@ -0,0 +1,6 @@
namespace PkmnLib.Dynamic.Models.Choices;
public interface ISwitchChoice : ITurnChoice
{
}

View File

@ -0,0 +1,56 @@
using PkmnLib.Dynamic.ScriptHandling;
namespace PkmnLib.Dynamic.Models.Choices;
public interface ITurnChoice : IScriptSource
{
/// <summary>
/// The user of the turn choice
/// </summary>
IPokemon User { get; }
/// <summary>
/// The speed of the user at the beginning of the turn.
/// </summary>
uint Speed { get; set; }
/// <summary>
/// This random value is set at the beginning of the turn. It is used for tie breaking of the
/// turn order in a predictable way, regardless of implementation and hardware.
/// </summary>
uint RandomValue { get; set; }
/// <summary>
/// Whether the choice has failed. A failed choice will stop running, and execute special
/// fail handling during turn execution.
/// </summary>
bool HasFailed { get; }
/// <summary>
/// Fails the choice. This will prevent it from executing and run a specific fail handling during
/// execution. Note that this can not be undone.
/// </summary>
public void Fail();
}
public abstract class TurnChoice : ScriptSource, ITurnChoice
{
protected TurnChoice(IPokemon user)
{
User = user;
}
public IPokemon User { get; }
public uint Speed { get; set; }
public uint RandomValue { get; set; }
public bool HasFailed { get; private set; }
public void Fail()
{
HasFailed = true;
}
}

View File

@ -0,0 +1,83 @@
namespace PkmnLib.Dynamic.Models.Choices;
/// <summary>
/// Comparer for turnchoices, to determine the order in which they should be executed.
/// </summary>
public class TurnChoiceComparer : IComparer<ITurnChoice>
{
private enum CompareValues
{
XEqualsY = 0,
XLessThanY = -1,
XGreaterThanY = 1
}
private CompareValues CompareForSameType(ITurnChoice x, ITurnChoice y)
{
// Higher speed goes first
var speedComparison = x.Speed.CompareTo(y.Speed);
if (speedComparison != 0)
return (CompareValues)speedComparison;
// If speed is equal, we use the random values we've given to each choice to tiebreak.
// This is to ensure that the order of choices is deterministic.
return (CompareValues)x.RandomValue.CompareTo(y.RandomValue);
}
private CompareValues CompareImpl(ITurnChoice? x, ITurnChoice? y)
{
// Deal with possible null values
switch (x)
{
case null when y is null:
return CompareValues.XEqualsY;
case null:
return CompareValues.XLessThanY;
}
if (y is null)
return CompareValues.XGreaterThanY;
switch (x)
{
case IMoveChoice moveX:
// Move choices go first
if (y is IMoveChoice moveY)
{
// Higher priority goes first
var priorityComparison = moveX.Priority.CompareTo(moveY.Priority);
if (priorityComparison != 0)
return (CompareValues)priorityComparison;
return CompareForSameType(moveX, moveY);
}
return CompareValues.XGreaterThanY;
case IItemChoice itemX:
// Item choices go second
return y switch
{
IMoveChoice => CompareValues.XLessThanY,
IItemChoice itemY => CompareForSameType(itemX, itemY),
_ => CompareValues.XGreaterThanY
};
case ISwitchChoice switchX:
// Switch choices go third
return y switch
{
IMoveChoice or IItemChoice => CompareValues.XLessThanY,
ISwitchChoice switchY => CompareForSameType(switchX, switchY),
_ => CompareValues.XGreaterThanY
};
case IPassChoice passX:
// Pass choices go last
return y switch
{
IMoveChoice or IItemChoice or ISwitchChoice => CompareValues.XLessThanY,
IPassChoice passY => CompareForSameType(passX, passY),
_ => CompareValues.XGreaterThanY
};
}
return CompareValues.XLessThanY;
}
/// <inheritdoc />
public int Compare(ITurnChoice? x, ITurnChoice? y) => (int) CompareImpl(x, y);
}

View File

@ -0,0 +1,22 @@
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// Where the damage comes from.
/// </summary>
public enum DamageSource
{
/// <summary>
/// The damage is done by a move.
/// </summary>
MoveDamage = 0,
/// <summary>
/// The damage is done by something else.
/// </summary>
Misc = 1,
/// <summary>
/// The damage is done because of struggling.
/// </summary>
Struggle = 2,
}

View File

@ -0,0 +1,104 @@
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Moves;
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// A hit data is the data for a single hit, on a single target.
/// </summary>
public record HitData
{
/// <summary>
/// Whether the hit is critical.
/// </summary>
public bool IsCritical { get; internal set; }
/// <summary>
/// The base power of the hit.
/// </summary>
public byte BasePower { get; internal set; }
/// <summary>
/// The effectiveness of the hit.
/// </summary>
public float Effectiveness { get; internal set; }
/// <summary>
/// The damage done by the hit.
/// </summary>
public uint Damage { get; internal set; }
/// <summary>
/// The type of the hit.
/// </summary>
public TypeIdentifier Type { get; internal set; }
/// <summary>
/// Whether the hit has failed.
/// </summary>
public bool HasFailed { get; private set; }
/// <summary>
/// Fails the hit.
/// </summary>
public void Fail() => HasFailed = true;
}
/// <summary>
/// An executing move is the data of the move for while it is executing.
/// </summary>
public interface IExecutingMove : IScriptSource
{
/// <summary>
/// The number of targets this move has.
/// </summary>
int TargetCount { get; }
/// <summary>
/// The number of hits this move has per target.
/// </summary>
byte NumberOfHits { get; }
/// <summary>
/// The user of the move.
/// </summary>
IPokemon User { get; }
/// <summary>
/// The move the user has actually chosen to do.
/// </summary>
ILearnedMove ChosenMove { get; }
/// <summary>
/// The move that the user is actually going to do. This can be different from the chosen move, for example
/// when metronome is used, in which case the chosen move will be metronome, and the movedata will be the
/// move that metronome has chosen.
/// </summary>
IMoveData UseMove { get; }
/// <summary>
/// The script of the move.
/// </summary>
ScriptContainer Script { get; }
/// <summary>
/// Gets a hit data for a target, with a specific index.
/// </summary>
HitData GetHitData(IPokemon target, byte hit);
/// <summary>
/// Checks whether a Pokémon is a target for this move.
/// </summary>
bool IsPokemonTarget(IPokemon target);
/// <summary>
/// Gets the index of the hits in this move where the hits for a specific target start.
/// </summary>
int GetTargetIndex(IPokemon target);
/// <summary>
/// Gets a hit based on its raw index.
/// </summary>
HitData GetDataFromRawIndex(int index);
}

View File

@ -0,0 +1,114 @@
using PkmnLib.Static.Moves;
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// The different ways a move can be learned.
/// </summary>
public enum MoveLearnMethod
{
/// <summary>
/// We do not know the learn method.
/// </summary>
Unknown,
/// <summary>
/// The move is learned by leveling up.
/// </summary>
LevelUp,
/// <summary>
/// The move is learned when the Pokémon is hatched from an egg.
/// </summary>
Egg,
/// <summary>
/// The move is learned by using a tutor in the game.
/// </summary>
Tutor,
/// <summary>
/// The move is learned by using a TM or HM.
/// </summary>
Machine,
/// <summary>
/// The move is learned when the Pokémon changes form.
/// </summary>
FormChange
}
/// <summary>
/// A learned move is the data attached to a Pokemon for a move it has learned. It has information
/// such as the remaining amount of users, how it has been learned, etc.
/// </summary>
public interface ILearnedMove
{
/// <summary>
/// The immutable move information of the move.
/// </summary>
IMoveData MoveData { get; }
/// <summary>
/// The maximal power points for this move.
/// </summary>
byte MaxPp { get; }
/// <summary>
/// The way the move has been learned.
/// </summary>
MoveLearnMethod LearnMethod { get; }
/// <summary>
/// Try and reduce the PP by a certain amount. If the amount is higher than the current uses,
/// return false. Otherwise, reduce the PP, and return true.
/// </summary>
bool TryUse(byte amount = 1);
/// <summary>
/// Set the remaining PP to the max amount of PP.
/// </summary>
void RestoreAllUses();
/// <summary>
/// Restore the remaining PP by a certain amount. Will prevent it from going above max PP.
/// </summary>
void RestoreUses(byte amount);
}
/// <inheritdoc />
public class LearnedMoveImpl : ILearnedMove
{
private byte _maxPpModification = 0;
public LearnedMoveImpl(IMoveData moveData, MoveLearnMethod learnMethod)
{
MoveData = moveData;
LearnMethod = learnMethod;
CurrentPp = MaxPp;
}
/// <inheritdoc />
public IMoveData MoveData { get; }
/// <inheritdoc />
public byte MaxPp => (byte)(MoveData.BaseUsages + _maxPpModification);
/// <inheritdoc />
public MoveLearnMethod LearnMethod { get; }
public byte CurrentPp { get; private set; }
public bool TryUse(byte amount = 1)
{
if (CurrentPp < amount)
return false;
CurrentPp -= amount;
return true;
}
public void RestoreAllUses() => CurrentPp = MaxPp;
public void RestoreUses(byte amount) => CurrentPp = (byte)Math.Min(CurrentPp + amount, MaxPp);
}

View File

@ -0,0 +1,292 @@
using JetBrains.Annotations;
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Species;
namespace PkmnLib.Dynamic.Models;
/// <summary>
/// The data of a Pokemon.
/// </summary>
public interface IPokemon : IScriptSource
{
/// <summary>
/// The library data of the Pokemon.
/// </summary>
IDynamicLibrary Library { get; }
/// <summary>
/// The species of the Pokemon.
/// </summary>
ISpecies Species { get; }
/// <summary>
/// The form of the Pokemon.
/// </summary>
IForm Form { get; }
/// <summary>
/// An optional display species of the Pokemon. If this is set, the client should display this
/// species. An example of usage for this is the Illusion ability.
/// </summary>
ISpecies? DisplaySpecies { get; }
/// <summary>
/// An optional display form of the Pokemon. If this is set, the client should display this
/// form. An example of usage for this is the Illusion ability.
/// </summary>
IForm? DisplayForm { get; }
/// <summary>
/// The current level of the Pokemon.
/// </summary>
LevelInt Level { get; }
/// <summary>
/// The amount of experience of the Pokemon.
/// </summary>
uint Experience { get; }
/// <summary>
/// The personality value of the Pokemon.
/// </summary>
uint PersonalityValue { get; }
/// <summary>
/// The gender of the Pokemon.
/// </summary>
Gender Gender { get; }
/// <summary>
/// The coloring of the Pokemon. Value 0 is the default, value 1 means shiny. Other values are
/// currently not used, and can be used for other implementations.
/// </summary>
byte Coloring { get; }
/// <summary>
/// The held item of the Pokemon.
/// </summary>
IItem? HeldItem { get; }
/// <summary>
/// The remaining health points of the Pokemon.
/// </summary>
uint CurrentHealth { get; }
/// <summary>
/// The weight of the Pokemon in kilograms.
/// </summary>
float WeightInKm { get; set; }
/// <summary>
/// The height of the Pokemon in meters.
/// </summary>
float HeightInMeters { get; set; }
/// <summary>
/// The happiness of the Pokemon. Also known as friendship.
/// </summary>
byte Happiness { get; }
/// <summary>
/// The stats of the Pokemon when disregarding any stat boosts.
/// </summary>
StatisticSet<uint> FlatStats { get; }
/// <summary>
/// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below
/// -6.
/// </summary>
StatBoostStatisticSet StatBoost { get; }
/// <summary>
/// The stats of the Pokemon including the stat boosts
/// </summary>
StatisticSet<uint> BoostedStats { get; }
/// <summary>
/// The individual values of the Pokemon.
/// </summary>
IndividualValueStatisticSet IndividualValues { get; }
/// <summary>
/// The effort values of the Pokemon.
/// </summary>
EffortValueStatisticSet EffortValues { get; }
/// <summary>
/// The nature of the Pokemon.
/// </summary>
INature Nature { get; }
/// <summary>
/// An optional nickname of the Pokemon.
/// </summary>
string? Nickname { get; }
/// <summary>
/// An index of the ability to find the actual ability on the form.
/// </summary>
AbilityIndex AbilityIndex { get; }
/// <summary>
/// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy
/// ability.
/// </summary>
IAbility? OverrideAbility { get; }
/// <summary>
/// If in battle, we have additional data.
/// </summary>
IPokemonBattleData? BattleData { get; }
/// <summary>
/// The moves the Pokemon has learned. This is of a set length of <see cref="Const.MovesCount"/>. Empty move slots
/// are null.
/// </summary>
IReadOnlyList<ILearnedMove?> Moves { get; }
/// <summary>
/// Whether or not the Pokemon is allowed to gain experience.
/// </summary>
bool AllowedExperience { get; }
/// <summary>
/// The current types of the Pokemon.
/// </summary>
IReadOnlyList<TypeIdentifier> Types { get; }
/// <summary>
/// Whether or not this Pokemon is an egg.
/// </summary>
bool IsEgg { get; }
/// <summary>
/// Whether or not this Pokemon was caught this battle.
/// </summary>
bool IsCaught { get; }
/// <summary>
/// The script for the held item.
/// </summary>
ScriptContainer HeldItemTriggerScript { get; }
/// <summary>
/// The script for the ability.
/// </summary>
ScriptContainer AbilityScript { get; }
/// <summary>
/// The script for the status.
/// </summary>
ScriptContainer StatusScript { get; }
/// <summary>
/// The volatile status scripts of the Pokemon.
/// </summary>
IScriptSet Volatile { get; }
/// <summary>
/// Checks whether the Pokemon is holding an item with a specific name.
/// </summary>
bool HasHeldItem(string itemName);
/// <summary>
/// Changes the held item of the Pokemon. Returns the previously held item.
/// </summary>
[MustUseReturnValue] IItem? SetHeldItem(IItem? item);
/// <summary>
/// Removes the held item from the Pokemon. Returns the previously held item.
/// </summary>
[MustUseReturnValue] IItem? RemoveHeldItem();
/// <summary>
/// Makes the Pokemon uses its held item. Returns whether the item was consumed.
/// </summary>
bool ConsumeHeldItem();
/// <summary>
/// Change a boosted stat by a certain amount.
/// </summary>
/// <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);
/// <summary>
/// Returns the currently active ability.
/// </summary>
IAbility ActiveAbility { get; }
/// <summary>
/// Calculates the flat stats on the Pokemon. This should be called when for example the base
/// stats, level, nature, IV, or EV changes. This has a side effect of recalculating the boosted
/// stats, as those depend on the flat stats.
/// </summary>
void RecalculateFlatStats();
/// <summary>
/// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats.
/// This should be called when a stat boost changes.
/// </summary>
void RecalculateBoostedStats();
/// <summary>
/// Change the species of the Pokemon.
/// </summary>
void ChangeSpecies(ISpecies species, IForm form);
/// <summary>
/// Change the form of the Pokemon.
/// </summary>
void ChangeForm(IForm form);
/// <summary>
/// Whether the Pokemon is useable in a battle.
/// </summary>
bool IsUsable { get; }
/// <summary>
/// Whether the Pokemon is fainted.
/// </summary>
bool IsFainted { get; }
/// <summary>
/// Damages the Pokemon by a certain amount of damage, from a damage source.
/// </summary>
void Damage(uint damage, DamageSource source, EventBatchId batchId);
/// <summary>
/// Heals the Pokemon by a specific amount. Unless allow_revive is set to true, this will not
/// heal if the Pokemon has 0 health. If the amount healed is 0, this will return false.
/// </summary>
bool Heal(uint heal, bool allowRevive);
/// <summary>
/// Learn a move by name.
/// </summary>
void LearnMove(string moveName, MoveLearnMethod method, byte index);
/// <summary>
/// Removes the current non-volatile status from the Pokemon.
/// </summary>
void ClearStatus();
/// <summary>
/// Modifies the level by a certain amount
/// </summary>
void ChangeLevelBy(int change);
// TODO: (de)serialize
}
/// <summary>
/// The data of the Pokemon related to being in a battle.
/// </summary>
public interface IPokemonBattleData
{
IBattle? Battle { get; }
}

View File

@ -0,0 +1,59 @@
using System.Collections;
namespace PkmnLib.Dynamic.Models;
public interface IPokemonParty : IReadOnlyList<IPokemon?>
{
/// <summary>
/// Sets the Pokemon at an index to a Pokemon, returning the old Pokemon.
/// </summary>
IPokemon? SwapInto(IPokemon pokemon, int index);
/// <summary>
/// Swaps two Pokemon in the party around.
/// </summary>
void Swap(int index1, int index2);
bool HasUsablePokemon();
}
public class PokemonParty : IPokemonParty
{
private readonly IPokemon?[] _pokemon;
public PokemonParty(int size)
{
_pokemon = new IPokemon[size];
}
/// <summary>
/// Sets the Pokemon at an index to a Pokemon, returning the old Pokemon.
/// </summary>
public IPokemon? SwapInto(IPokemon pokemon, int index)
{
var old = _pokemon[index];
_pokemon[index] = pokemon;
return old;
}
/// <summary>
/// Swaps two Pokemon in the party around.
/// </summary>
public void Swap(int index1, int index2) =>
(_pokemon[index1], _pokemon[index2]) = (_pokemon[index2], _pokemon[index1]);
public bool HasUsablePokemon() => _pokemon.Any(p => p != null && p.IsUsable);
/// <inheritdoc />
public IEnumerator<IPokemon?> GetEnumerator() => ((IEnumerable<IPokemon?>)_pokemon).GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc />
public int Count => _pokemon.Length;
/// <inheritdoc />
public IPokemon? this[int index] => _pokemon[index];
}

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\netstandard2.1\PkmnLib.Dynamic.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\netstandard2.1\PkmnLib.Dynamic.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentResults" Version="3.16.0" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
/// <summary>
/// A plugin is a way to register scripts and other dynamic components to the script registry.
/// </summary>
public abstract class Plugin
{
/// <summary>
/// The name of the plugin. Mostly used for debugging purposes.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// When the plugin should be loaded. Lower values are loaded first.
/// 0 should be reserved for the core battle scripts.
/// </summary>
public abstract uint LoadOrder { get; }
/// <summary>
/// Run the registration of the plugin when we're building the library.
/// </summary>
public abstract void Register(ScriptRegistry registry);
}

View File

@ -0,0 +1,14 @@
namespace PkmnLib.Dynamic.ScriptHandling;
[AttributeUsage(AttributeTargets.Class)]
public class ScriptAttribute : Attribute
{
public ScriptCategory Category { get; }
public string Name { get; }
public ScriptAttribute(ScriptCategory category, string name)
{
Category = category;
Name = name;
}
}

View File

@ -0,0 +1,56 @@
using System.Linq.Expressions;
using System.Reflection;
using PkmnLib.Dynamic.Libraries;
namespace PkmnLib.Dynamic.ScriptHandling;
public class ScriptRegistry
{
private Dictionary<(ScriptCategory category, string name), Func<Script>> _scriptTypes = new();
private IBattleStatCalculator? _battleStatCalculator;
private IDamageCalculator? _damageCalculator;
private IMiscLibrary? _miscLibrary;
public void RegisterAssemblyScripts(Assembly assembly)
{
var baseType = typeof(Script);
foreach (var type in assembly.GetTypes().Where(t => baseType.IsAssignableFrom(t)))
{
var attribute = type.GetCustomAttribute<ScriptAttribute>();
if (attribute == null)
continue;
RegisterScriptType(attribute.Category, attribute.Name, type);
}
}
public void RegisterScriptType(ScriptCategory category, string name, Type type)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (type == null)
throw new ArgumentNullException(nameof(type));
var constructor = type.GetConstructor(Type.EmptyTypes);
if (constructor == null)
throw new ArgumentException("The type must have a parameterless constructor.");
// We create a lambda that creates a new instance of the script type.
// This is more performant than using Activator.CreateInstance.
_scriptTypes[(category, name)] = Expression.Lambda<Func<Script>>(Expression.New(constructor)).Compile();
}
public void RegisterBattleStatCalculator<T>(T battleStatCalculator)
where T : IBattleStatCalculator => _battleStatCalculator = battleStatCalculator;
public void RegisterDamageCalculator<T>(T damageCalculator)
where T : IDamageCalculator => _damageCalculator = damageCalculator;
public void RegisterMiscLibrary<T>(T miscLibrary) where T : IMiscLibrary
=> _miscLibrary = miscLibrary;
internal Dictionary<(ScriptCategory category, string name), Func<Script>> ScriptTypes => _scriptTypes;
internal IBattleStatCalculator? BattleStatCalculator => _battleStatCalculator;
internal IDamageCalculator? DamageCalculator => _damageCalculator;
internal IMiscLibrary? MiscLibrary => _miscLibrary;
}

View File

@ -0,0 +1,6 @@
namespace PkmnLib.Dynamic.ScriptHandling;
public abstract class Script
{
}

View File

@ -0,0 +1,57 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
namespace PkmnLib.Dynamic.ScriptHandling;
/// <summary>
/// A script category defines a sub-group of scripts. This can be used to have multiple scripts with
/// the same name, but a different script. It should be completely valid for a move to have the same
/// name as an ability, or more commonly: for a script attached to a Pokemon to have the same name as
/// a move that placed it there.
/// </summary>
public enum ScriptCategory
{
/// <summary>
/// A script that belongs to a move. This generally is only the script that is attached to a
/// <see cref="IMoveChoice"/> and <see cref="IExecutingMove"/>
/// </summary>
Move = 0,
/// <summary>
/// An ability script. Scripts in this category are always abilities, and therefore always
/// attached to a Pokemon.
/// </summary>
Ability = 1,
/// <summary>
/// A non volatile status script. Scripts in this category are always non volatile statuses, and
/// therefore always attached to a Pokemon.
/// </summary>
Status = 2,
/// <summary>
/// A volatile status script. Scripts in this category are always volatile status effects, and
/// therefore always attached to a Pokemon.
/// </summary>
Pokemon = 3,
/// <summary>
/// A script that can be attached to an entire side.
/// </summary>
Side = 4,
/// <summary>
/// A script that can be attached to the entire battle.
/// </summary>
Battle = 5,
/// <summary>
/// A special script for weather, for use on battles.
/// </summary>
Weather = 6,
/// <summary>
/// A special script for held items. As they're part of a held item, they're attached to a Pokemon.
/// </summary>
ItemBattleTrigger = 7,
}

View File

@ -0,0 +1,22 @@
using System.Collections;
namespace PkmnLib.Dynamic.ScriptHandling;
public class ScriptContainer : IEnumerable<ScriptContainer>
{
private Script? _script = null;
public bool IsEmpty => _script is null;
/// <inheritdoc />
public IEnumerator<ScriptContainer> GetEnumerator()
{
yield return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -0,0 +1,71 @@
using System.Collections;
namespace PkmnLib.Dynamic.ScriptHandling;
public class ScriptIterator : IEnumerable<ScriptContainer>
{
private readonly IReadOnlyList<IEnumerable<ScriptContainer>> _scripts;
private int _index = -1;
private int _setIndex = -1;
public ScriptIterator(IReadOnlyList<IEnumerable<ScriptContainer>> scripts)
{
_scripts = scripts;
}
bool IncrementToNext()
{
if (_index != -1)
{
var current = _scripts[_index];
if (current is IScriptSet)
{
_setIndex += 1;
if (_setIndex >= current.Count())
{
_setIndex = -1;
}
else
{
return true;
}
}
}
_index += 1;
for (; _index < _scripts.Count; _index++)
{
switch (_scripts[_index])
{
case IScriptSet:
_setIndex = 0;
return true;
case ScriptContainer { IsEmpty: false }:
return true;
}
}
return false;
}
/// <inheritdoc />
public IEnumerator<ScriptContainer> GetEnumerator()
{
while (IncrementToNext())
{
var current = _scripts[_index];
yield return current switch
{
IScriptSet set => set.At(_setIndex),
ScriptContainer container => container,
_ => throw new InvalidOperationException("Invalid script type")
};
}
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -0,0 +1,16 @@
using FluentResults;
namespace PkmnLib.Dynamic.ScriptHandling;
public interface IScriptSet : IEnumerable<ScriptContainer>
{
Result<ScriptContainer> Add(Script script);
Result<ScriptContainer?> Add(string scriptKey);
ScriptContainer? Get(string scriptKey);
void Remove(string scriptKey);
void Clear();
void Contains(string scriptKey);
ScriptContainer At(int index);
int Count { get; }
IEnumerable<string> GetScriptNames();
}

View File

@ -0,0 +1,52 @@
namespace PkmnLib.Dynamic.ScriptHandling;
public interface IScriptSource
{
ScriptIterator GetScripts();
/// <summary>
/// The number of scripts that are expected to be relevant for this source. This generally is
/// The number of its own scripts + the number of scripts for any parents.
/// </summary>
int ScriptCount { get; }
/// <summary>
/// This should add all scripts belonging to this source to the scripts Vec, disregarding its
/// potential parents.
/// </summary>
/// <param name="scripts"></param>
void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts);
/// <summary>
/// This should add all scripts that are relevant to the source the the scripts Vec, including
/// everything that belongs to its parents.
/// </summary>
/// <param name="scripts"></param>
void CollectScripts(List<IEnumerable<ScriptContainer>> scripts);
}
public abstract class ScriptSource : IScriptSource
{
/// <inheritdoc />
public ScriptIterator GetScripts()
{
if (_scripts == null)
{
_scripts = new List<IEnumerable<ScriptContainer>>(ScriptCount);
CollectScripts(_scripts);
}
return new ScriptIterator(_scripts);
}
/// <inheritdoc />
public abstract int ScriptCount { get; }
/// <inheritdoc />
public abstract void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts);
/// <inheritdoc />
public abstract void CollectScripts(List<IEnumerable<ScriptContainer>> scripts);
/// <inheritdoc />
private List<IEnumerable<ScriptContainer>>? _scripts;
}

View File

@ -0,0 +1,15 @@
namespace PkmnLib.Dynamic;
public static class StaticHelpers
{
/// <summary>
/// A function to get the current date and time. This can be replaced in cases where the date and time
/// may not be the same as the system time.
/// </summary>
public static Func<DateTime> DateTimeProvider { get; set; } = () => DateTime.Now;
/// <summary>
/// Get the current date and time.
/// </summary>
public static DateTime GetCurrentDateTime() => DateTimeProvider();
}

View File

@ -4,6 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Static", "PkmnLib.S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Tests", "PkmnLib.Tests\PkmnLib.Tests.csproj", "{42DE3095-0468-4827-AF5C-691C94BA7F92}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Dynamic", "PkmnLib.Dynamic\PkmnLib.Dynamic.csproj", "{D0CBA9A9-7288-41B4-B76B-CB4F20036AB2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Scripts.Gen7", "PkmnLib.Scripts.Gen7\PkmnLib.Scripts.Gen7.csproj", "{FA5380F0-28CC-4AEC-8963-814B347A89BA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,5 +22,13 @@ Global
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Release|Any CPU.Build.0 = Release|Any CPU
{D0CBA9A9-7288-41B4-B76B-CB4F20036AB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0CBA9A9-7288-41B4-B76B-CB4F20036AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0CBA9A9-7288-41B4-B76B-CB4F20036AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0CBA9A9-7288-41B4-B76B-CB4F20036AB2}.Release|Any CPU.Build.0 = Release|Any CPU
{FA5380F0-28CC-4AEC-8963-814B347A89BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA5380F0-28CC-4AEC-8963-814B347A89BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA5380F0-28CC-4AEC-8963-814B347A89BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA5380F0-28CC-4AEC-8963-814B347A89BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,23 @@
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Scripts.Gen7.Libraries;
namespace PkmnLib.Scripts.Gen7;
public class Gen7Plugin : Plugin
{
/// <inheritdoc />
public override string Name => "Gen7";
/// <inheritdoc />
public override uint LoadOrder => 0;
/// <inheritdoc />
public override void Register(ScriptRegistry registry)
{
registry.RegisterAssemblyScripts(typeof(Gen7Plugin).Assembly);
registry.RegisterBattleStatCalculator(new Gen7BattleStatCalculator());
registry.RegisterDamageCalculator(new Gen7DamageCalculator(true));
registry.RegisterMiscLibrary(new Gen7MiscLibrary());
}
}

View File

@ -0,0 +1,96 @@
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
namespace PkmnLib.Scripts.Gen7.Libraries;
public class Gen7BattleStatCalculator : IBattleStatCalculator
{
/// <inheritdoc />
public void CalculateFlatStats(IPokemon pokemon, StatisticSet<uint> stats)
{
stats.SetStatistic(Statistic.Hp, CalculateHealthStat(pokemon));
stats.SetStatistic(Statistic.Attack, CalculateNormalStat(pokemon, Statistic.Attack));
stats.SetStatistic(Statistic.Defense, CalculateNormalStat(pokemon, Statistic.Defense));
stats.SetStatistic(Statistic.SpecialAttack, CalculateNormalStat(pokemon, Statistic.SpecialAttack));
stats.SetStatistic(Statistic.SpecialDefense, CalculateNormalStat(pokemon, Statistic.SpecialDefense));
stats.SetStatistic(Statistic.Speed, CalculateNormalStat(pokemon, Statistic.Speed));
}
/// <inheritdoc />
public uint CalculateFlatStat(IPokemon pokemon, Statistic stat)
{
return stat switch
{
Statistic.Hp => CalculateHealthStat(pokemon),
_ => CalculateNormalStat(pokemon, stat)
};
}
/// <inheritdoc />
public void CalculateBoostedStats(IPokemon pokemon, StatisticSet<uint> stats)
{
stats.SetStatistic(Statistic.Hp, CalculateBoostedStat(pokemon, Statistic.Hp));
stats.SetStatistic(Statistic.Attack, CalculateBoostedStat(pokemon, Statistic.Attack));
stats.SetStatistic(Statistic.Defense, CalculateBoostedStat(pokemon, Statistic.Defense));
stats.SetStatistic(Statistic.SpecialAttack, CalculateBoostedStat(pokemon, Statistic.SpecialAttack));
stats.SetStatistic(Statistic.SpecialDefense, CalculateBoostedStat(pokemon, Statistic.SpecialDefense));
stats.SetStatistic(Statistic.Speed, CalculateBoostedStat(pokemon, Statistic.Speed));
}
/// <inheritdoc />
public uint CalculateBoostedStat(IPokemon pokemon, Statistic stat)
{
var flatStat = CalculateFlatStat(pokemon, stat);
var boostModifier = GetStatBoostModifier(pokemon, stat);
var boostedStat = flatStat * boostModifier;
if (boostedStat > uint.MaxValue) boostedStat = uint.MaxValue;
return (uint)boostedStat;
}
private uint CalculateHealthStat(IPokemon pokemon)
{
var baseValue = (ulong)pokemon.Form.BaseStats.Hp;
var iv = (ulong)pokemon.IndividualValues.Hp;
var ev = (ulong)pokemon.EffortValues.Hp;
var level = (ulong)pokemon.Level;
var health = (((2 * baseValue + iv + (ev / 4)) * level) / 100) + level + 10;
if (health > uint.MaxValue) health = uint.MaxValue;
return (uint)health;
}
private uint CalculateNormalStat(IPokemon pokemon, Statistic statistic)
{
var baseValue = (ulong)pokemon.Form.BaseStats.GetStatistic(statistic);
var iv = (ulong)pokemon.IndividualValues.GetStatistic(statistic);
var ev = (ulong)pokemon.EffortValues.GetStatistic(statistic);
var level = (ulong)pokemon.Level;
var unmodified = (((2 * baseValue + iv + (ev / 4)) * level) / 100) + 5;
var natureModifier = pokemon.Nature.GetStatModifier(statistic);
var modified = (unmodified * natureModifier);
if (modified > uint.MaxValue) modified = uint.MaxValue;
return (uint)modified;
}
private float GetStatBoostModifier(IPokemon pokemon, Statistic statistic)
{
var boost = pokemon.StatBoost.GetStatistic(statistic);
return boost switch
{
-6 => 2.0f / 8.0f,
-5 => 2.0f / 7.0f,
-4 => 2.0f / 6.0f,
-3 => 2.0f / 5.0f,
-2 => 2.0f / 4.0f,
-1 => 2.0f / 3.0f,
0 => 1.0f,
1 => 3.0f / 2.0f,
2 => 4.0f / 2.0f,
3 => 5.0f / 2.0f,
4 => 6.0f / 2.0f,
5 => 7.0f / 2.0f,
6 => 8.0f / 2.0f,
_ => throw new System.ArgumentException("Stat boost was out of expected range of -6 to 6")
};
}
}

View File

@ -0,0 +1,155 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
using PkmnLib.Static.Moves;
namespace PkmnLib.Scripts.Gen7.Libraries;
public class Gen7DamageCalculator(bool hasRandomness) : IDamageCalculator
{
/// <inheritdoc />
public uint GetDamage(IExecutingMove executingMove, IPokemon target, byte hitNumber, HitData hitData)
{
var category = executingMove.UseMove.Category;
if (category == MoveCategory.Status)
return 0;
if (hitData.Effectiveness == 0)
return 0;
var levelModifier = (2.0f * executingMove.User.Level) / 5.0f + 2.0f;
var basePower = (float)hitData.BasePower;
var statModifier = GetStatModifier(executingMove, target, hitNumber, hitData);
var damageModifier = GetDamageModifier(executingMove, target, hitNumber, hitData);
var floatDamage = MathF.Floor(levelModifier * basePower);
floatDamage = MathF.Floor(floatDamage * statModifier);
floatDamage = MathF.Floor(floatDamage / 50.0f) + 2.0f;
floatDamage = MathF.Floor(floatDamage * damageModifier);
if (executingMove.TargetCount > 1)
floatDamage = MathF.Floor(floatDamage * 0.75f);
if (hitData.IsCritical)
{
var critModifier = 1.5f;
// TODO: script hook to change the critical modifier
floatDamage = MathF.Floor(floatDamage * critModifier);
}
if (hasRandomness)
{
var battle = target.BattleData?.Battle;
if (battle == null)
throw new InvalidOperationException("Randomness is enabled, but no battle is set.");
var random = battle.Random;
var randomFactor = random.GetInt(85, 101) / 100.0f;
floatDamage = MathF.Floor(floatDamage * randomFactor);
}
if (executingMove.User.Types.Contains(hitData.Type))
{
var stabModifier = 1.5f;
// TODO: script hook to change the STAB modifier
floatDamage = MathF.Floor(floatDamage * stabModifier);
}
floatDamage = MathF.Floor(floatDamage * hitData.Effectiveness);
uint damage = floatDamage switch
{
> uint.MaxValue => uint.MaxValue,
< 1 => 1,
_ => (uint)floatDamage
};
// TODO: script hook to modify the damage
// TODO: script hook to modify incoming damage
return damage;
}
/// <inheritdoc />
public byte GetBasePower(IExecutingMove executingMove, IPokemon target, byte hitNumber, HitData hitData)
{
if (executingMove.UseMove.Category == MoveCategory.Status)
return 0;
var basePower = hitData.BasePower;
// TODO: script hook to modify the base power
return basePower;
}
/// <inheritdoc />
[SuppressMessage("ReSharper", "UnreachableSwitchArmDueToIntegerAnalysis")] // disabled because of the TODO
public bool IsCritical(IBattle battle, IExecutingMove executingMove, IPokemon target, byte hitNumber)
{
if (executingMove.UseMove.Category == MoveCategory.Status)
return false;
byte critStage = 0;
// TODO: script hook to modify the crit stage
var random = battle.Random;
return critStage switch
{
0 => random.GetInt(24) == 0,
1 => random.GetInt(8) == 0,
2 => random.GetInt(2) == 0,
_ => true
};
}
private static float GetStatModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, HitData hitData)
{
var category = executingMove.UseMove.Category;
if (category == MoveCategory.Status)
return 1;
var (offensive, defensive) = category switch
{
MoveCategory.Physical => (Statistic.Attack, Statistic.Defense),
_ => (Statistic.SpecialAttack, Statistic.SpecialDefense),
};
// Check if we can bypass the defensive stat boost on the target. We default to this if the
// move is critical, and the target has a defensive stat boost of > 0, but a script is
// allowed to change this.
var bypassDefense = hitData.IsCritical && target.StatBoost.GetStatistic(defensive) > 0;
// TODO: script hook
// Check if we can bypass the offensive stat boost on the user. We default to this if the
// move is critical, and the user has an offensive stat boost of < 0, but a script is
// allowed to change this.
var bypassOffense = hitData.IsCritical && executingMove.User.StatBoost.GetStatistic(offensive) < 0;
// TODO: script hook
var userStats = executingMove.User.BoostedStats;
if (bypassOffense)
userStats = executingMove.User.FlatStats;
var offensiveStat = userStats.GetStatistic(offensive);
var targetStats = target.BoostedStats;
if (bypassDefense)
targetStats = target.FlatStats;
var defensiveStat = targetStats.GetStatistic(defensive);
// TODO: script hook to modify the stats above
var modifier = (float)offensiveStat / defensiveStat;
// TODO: script hook to modify the modifier
return modifier;
}
/// <summary>
/// Gets the damage modifier. This is a value that defaults to 1.0, but can be modified by scripts
/// to apply a raw modifier to the damage.
/// </summary>
private static float GetDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber,
HitData hitData)
{
var modifier = 1.0f;
// TODO: script hook to modify the modifier
return modifier;
}
}

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using PkmnLib.Dynamic;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Static;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Scripts.Gen7.Libraries;
public class Gen7MiscLibrary : IMiscLibrary
{
private readonly IMoveData _struggleData = new MoveDataImpl("struggle", new TypeIdentifier(0),
MoveCategory.Physical, 50,
255, 255, MoveTarget.Any, 0,
new SecondaryEffectImpl(-1, "struggle", new Dictionary<StringKey, object>()), []);
/// <inheritdoc />
public ITurnChoice ReplacementChoice(IPokemon user, byte targetSide, byte targetPosition) =>
new MoveChoice(user, new LearnedMoveImpl(_struggleData, MoveLearnMethod.Unknown), targetSide,
targetPosition);
/// <inheritdoc />
public TimeOfDay GetTimeOfDay()
{
var time = StaticHelpers.GetCurrentDateTime();
var hour = time.Hour;
return hour switch
{
>= 0 and <= 5 => TimeOfDay.Night,
>= 6 and <= 9 => TimeOfDay.Morning,
>= 10 and <= 16 => TimeOfDay.Day,
17 => TimeOfDay.Evening,
_ => TimeOfDay.Night
};
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PkmnLib.Dynamic\PkmnLib.Dynamic.csproj" />
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj" />
</ItemGroup>
</Project>

View File

@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
namespace PkmnLib.Static;
/// <summary>
@ -79,10 +81,13 @@ public record StatisticSet<T> : ImmutableStatisticSet<T>
{
}
protected T Add(T a, T b) => (T)Convert.ChangeType(Convert.ToInt32(a) + Convert.ToInt32(b), typeof(T));
protected T Subtract(T a, T b) => (T)Convert.ChangeType(Convert.ToInt32(a) - Convert.ToInt32(b), typeof(T));
/// <summary>
/// Modifies a statistic in the set.
/// </summary>
public void SetStatistic(Statistic stat, T value)
public virtual void SetStatistic(Statistic stat, T value)
{
switch (stat)
{
@ -112,64 +117,186 @@ public record StatisticSet<T> : ImmutableStatisticSet<T>
/// <summary>
/// Increases a statistic in the set by a value.
/// </summary>
public void IncreaseStatistic(Statistic stat, T value)
public virtual bool IncreaseStatistic(Statistic stat, T value)
{
switch (stat)
{
case Statistic.Hp:
Hp = (T)Convert.ChangeType(Convert.ToInt32(Hp) + Convert.ToInt32(value), typeof(T));
Hp = Add(Hp, value);
break;
case Statistic.Attack:
Attack = (T)Convert.ChangeType(Convert.ToInt32(Attack) + Convert.ToInt32(value), typeof(T));
Attack = Add(Attack, value);
break;
case Statistic.Defense:
Defense = (T)Convert.ChangeType(Convert.ToInt32(Defense) + Convert.ToInt32(value), typeof(T));
Defense = Add(Defense, value);
break;
case Statistic.SpecialAttack:
SpecialAttack =
(T)Convert.ChangeType(Convert.ToInt32(SpecialAttack) + Convert.ToInt32(value), typeof(T));
SpecialAttack = Add(SpecialAttack, value);
break;
case Statistic.SpecialDefense:
SpecialDefense =
(T)Convert.ChangeType(Convert.ToInt32(SpecialDefense) + Convert.ToInt32(value), typeof(T));
SpecialDefense = Add(SpecialDefense, value);
break;
case Statistic.Speed:
Speed = (T)Convert.ChangeType(Convert.ToInt32(Speed) + Convert.ToInt32(value), typeof(T));
Speed = Add(Speed, value);
break;
default:
throw new ArgumentException("Invalid statistic.");
}
return true;
}
/// <summary>
/// Decreases a statistic in the set by a value.
/// </summary>
public void DecreaseStatistic(Statistic stat, T value)
public virtual bool DecreaseStatistic(Statistic stat, T value)
{
switch (stat)
{
case Statistic.Hp:
Hp = (T)Convert.ChangeType(Convert.ToInt32(Hp) - Convert.ToInt32(value), typeof(T));
Hp = Subtract(Hp, value);
break;
case Statistic.Attack:
Attack = (T)Convert.ChangeType(Convert.ToInt32(Attack) - Convert.ToInt32(value), typeof(T));
Attack = Subtract(Attack, value);
break;
case Statistic.Defense:
Defense = (T)Convert.ChangeType(Convert.ToInt32(Defense) - Convert.ToInt32(value), typeof(T));
Defense = Subtract(Defense, value);
break;
case Statistic.SpecialAttack:
SpecialAttack =
(T)Convert.ChangeType(Convert.ToInt32(SpecialAttack) - Convert.ToInt32(value), typeof(T));
SpecialAttack = Subtract(SpecialAttack, value);
break;
case Statistic.SpecialDefense:
SpecialDefense =
(T)Convert.ChangeType(Convert.ToInt32(SpecialDefense) - Convert.ToInt32(value), typeof(T));
SpecialDefense = Subtract(SpecialDefense, value);
break;
case Statistic.Speed:
Speed = (T)Convert.ChangeType(Convert.ToInt32(Speed) - Convert.ToInt32(value), typeof(T));
Speed = Subtract(Speed, value);
break;
default:
throw new ArgumentException("Invalid statistic.");
}
return true;
}
}
/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value.
/// </summary>
public abstract record ClampedStatisticSet<T> : StatisticSet<T>
where T : struct, IComparable<T>
{
/// <inheritdoc />
[SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")]
protected ClampedStatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed) : base(hp,
attack, defense, specialAttack, specialDefense, speed)
{
if (Min.CompareTo(Max) > 0)
throw new ArgumentException("Minimum value must be less than or equal to maximum value.");
Hp = Clamp(Hp, Min, Max);
Attack = Clamp(Attack, Min, Max);
Defense = Clamp(Defense, Min, Max);
SpecialAttack = Clamp(SpecialAttack, Min, Max);
SpecialDefense = Clamp(SpecialDefense, Min, Max);
Speed = Clamp(Speed, Min, Max);
}
private static T Clamp(T value, T min, T max)
{
if (value.CompareTo(min) < 0)
return min;
if (value.CompareTo(max) > 0)
return max;
return value;
}
/// <summary>
/// The minimum value for the statistics.
/// </summary>
protected abstract T Min { get; }
/// <summary>
/// The maximum value for the statistics.
/// </summary>
protected abstract T Max { get; }
/// <inheritdoc />
public override void SetStatistic(Statistic stat, T value) => base.SetStatistic(stat, Clamp(value, Min, Max));
/// <inheritdoc />
public override bool IncreaseStatistic(Statistic stat, T value)
{
var current = GetStatistic(stat);
var newValue = Add(current, value);
if (newValue.CompareTo(Max) > 0)
value = Subtract(Max, current);
if (value.CompareTo(default) == 0)
return false;
return base.IncreaseStatistic(stat, value);
}
/// <inheritdoc />
public override bool DecreaseStatistic(Statistic stat, T value)
{
var current = GetStatistic(stat);
var newValue = Subtract(current, value);
if (newValue.CompareTo(Min) < 0)
value = Subtract(current, Min);
if (value.CompareTo(default) == 0)
return false;
return base.DecreaseStatistic(stat, value);
}
}
/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value of -6 and 6.
/// </summary>
public record StatBoostStatisticSet : ClampedStatisticSet<sbyte>
{
/// <inheritdoc />
protected override sbyte Min => -6;
/// <inheritdoc />
protected override sbyte Max => 6;
/// <inheritdoc cref="StatBoostStatisticSet"/>
public StatBoostStatisticSet(sbyte hp, sbyte attack, sbyte defense, sbyte specialAttack, sbyte specialDefense,
sbyte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
{
}
}
/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value of 0 and 31.
/// </summary>
public record IndividualValueStatisticSet : ClampedStatisticSet<byte>
{
/// <inheritdoc />
protected override byte Min => 0;
/// <inheritdoc />
protected override byte Max => 31;
/// <inheritdoc cref="IndividualValueStatisticSet"/>
public IndividualValueStatisticSet(byte hp, byte attack, byte defense, byte specialAttack, byte specialDefense,
byte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
{
}
}
/// <summary>
/// A set of statistics that can be changed, but are clamped to a minimum and maximum value of 0 and 252.
/// </summary>
public record EffortValueStatisticSet : ClampedStatisticSet<byte>
{
/// <inheritdoc />
protected override byte Min => 0;
/// <inheritdoc />
protected override byte Max => 252;
/// <inheritdoc cref="EffortValueStatisticSet"/>
public EffortValueStatisticSet(byte hp, byte attack, byte defense, byte specialAttack, byte specialDefense,
byte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
{
}
}