Begin work on outlining dynamic side
This commit is contained in:
parent
1b501dee7e
commit
a251913ebd
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
global using LevelInt = byte;
|
||||
|
||||
public class Const
|
||||
{
|
||||
public const int MovesCount = 4;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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; }
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
public interface IBattleParty
|
||||
{
|
||||
IPokemonParty Party { get; }
|
||||
bool IsResponsibleForIndex(byte side, byte position);
|
||||
bool HasPokemonNotInField();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace PkmnLib.Dynamic.Models.Choices;
|
||||
|
||||
public interface IFleeChoice : ITurnChoice
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace PkmnLib.Dynamic.Models.Choices;
|
||||
|
||||
public interface IItemChoice : ITurnChoice
|
||||
{
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace PkmnLib.Dynamic.Models.Choices;
|
||||
|
||||
public interface IPassChoice : ITurnChoice
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace PkmnLib.Dynamic.Models.Choices;
|
||||
|
||||
public interface ISwitchChoice : ITurnChoice
|
||||
{
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
|
||||
public abstract class Script
|
||||
{
|
||||
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue