All checks were successful
Build / Build (push) Successful in 47s
Items can be used on Pokemon outside of battle, and we want the user of the library to be able to check for what changed. This allows for example a heal event to be sent back to the user of the library after using an item.
1460 lines
44 KiB
C#
1460 lines
44 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using JetBrains.Annotations;
|
|
using PkmnLib.Dynamic.Events;
|
|
using PkmnLib.Dynamic.Libraries;
|
|
using PkmnLib.Dynamic.Models.Serialized;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Static;
|
|
using PkmnLib.Static.Species;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Dynamic.Models;
|
|
|
|
/// <summary>
|
|
/// The data of a Pokemon.
|
|
/// </summary>
|
|
public interface IPokemon : IScriptSource, IDeepCloneable
|
|
{
|
|
/// <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>
|
|
/// Sets the display species and form of the Pokemon. This is used for abilities like Illusion.
|
|
/// </summary>
|
|
void SetDisplaySpecies(ISpecies? species, IForm? form);
|
|
|
|
/// <summary>
|
|
/// The current level of the Pokemon.
|
|
/// </summary>
|
|
LevelInt Level { get; }
|
|
|
|
/// <summary>
|
|
/// The amount of experience of the Pokemon.
|
|
/// </summary>
|
|
uint Experience { get; }
|
|
|
|
/// <summary>
|
|
/// Increases the experience of the Pokemon. Returns whether any experience was gained.
|
|
/// </summary>
|
|
bool AddExperience(uint experience);
|
|
|
|
/// <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>
|
|
/// Whether the Pokemon is shiny.
|
|
/// </summary>
|
|
bool IsShiny { 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 WeightInKg { get; }
|
|
|
|
/// <summary>
|
|
/// The height of the Pokémon 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 maximum health of the Pokemon.
|
|
/// </summary>
|
|
uint MaxHealth { 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>
|
|
/// Checks whether the Pokemon has a specific move in its current moveset.
|
|
/// </summary>
|
|
bool HasMove(StringKey moveName);
|
|
|
|
/// <summary>
|
|
/// Swaps two moves of the Pokemon.
|
|
/// </summary>
|
|
void SwapMoves(byte index1, byte index2);
|
|
|
|
/// <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>
|
|
/// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore.
|
|
/// </summary>
|
|
public void MarkAsCaught();
|
|
|
|
/// <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(StringKey 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>
|
|
/// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is used for moves that remove a held item, but do not consume it. In this case, the item needs to be
|
|
/// restored after the battle.
|
|
/// </remarks>
|
|
IItem? RemoveHeldItemForBattle();
|
|
|
|
/// <summary>
|
|
/// Tries to steal the held item of the Pokémon. If successful, the item is removed from the Pokémon and returned.
|
|
/// If the Pokémon does not have a held item, or the item is a form changer, this will return false.
|
|
/// </summary>
|
|
bool TryStealHeldItem([NotNullWhen(true)] out IItem? item);
|
|
|
|
/// <summary>
|
|
/// Restores the held item of a Pokémon if it was temporarily removed.
|
|
/// </summary>
|
|
void RestoreStolenHeldItem();
|
|
|
|
/// <summary>
|
|
/// Makes the Pokemon uses its held item. Returns whether the item was consumed.
|
|
/// </summary>
|
|
bool ConsumeHeldItem();
|
|
|
|
/// <summary>
|
|
/// Uses an item on the Pokemon.
|
|
/// </summary>
|
|
void UseItem(IItem item);
|
|
|
|
/// <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>
|
|
/// <param name="force"></param>
|
|
/// <param name="batchId">The event batch ID this change is a part of. This is relevant for visual handling</param>
|
|
bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default);
|
|
|
|
/// <summary>
|
|
/// Suppresses the ability of the Pokémon.
|
|
/// </summary>
|
|
public void SuppressAbility();
|
|
|
|
/// <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, EventBatchId batchId = default);
|
|
|
|
/// <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 = default, bool forceDamage = false);
|
|
|
|
/// <summary>
|
|
/// Forces the Pokémon to faint.
|
|
/// </summary>
|
|
void Faint(DamageSource source, EventBatchId batchId = default);
|
|
|
|
/// <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 = false, EventBatchId batchId = default, bool forceHeal = false,
|
|
EventHook? customEventHook = null);
|
|
|
|
/// <summary>
|
|
/// Restores all PP of the Pokemon.
|
|
/// </summary>
|
|
void RestoreAllPP();
|
|
|
|
/// <summary>
|
|
/// Learn a move by name.
|
|
/// </summary>
|
|
void LearnMove(StringKey moveName, MoveLearnMethod method, byte index);
|
|
|
|
/// <summary>
|
|
/// Checks whether the Pokémon has a specific non-volatile status.
|
|
/// </summary>
|
|
bool HasStatus(StringKey status);
|
|
|
|
/// <summary>
|
|
/// Adds a non-volatile status to the Pokemon.
|
|
/// </summary>
|
|
bool SetStatus(StringKey status, bool selfInflicted, EventBatchId batchId = default);
|
|
|
|
/// <summary>
|
|
/// Removes the current non-volatile status from the Pokemon.
|
|
/// </summary>
|
|
void ClearStatus(EventBatchId batchId = default);
|
|
|
|
/// <summary>
|
|
/// Modifies the level by a certain amount
|
|
/// </summary>
|
|
void ChangeLevelBy(int change);
|
|
|
|
/// <summary>
|
|
/// Sets the current battle the Pokémon is in.
|
|
/// </summary>
|
|
void SetBattleData(IBattle battle, byte sideIndex);
|
|
|
|
/// <summary>
|
|
/// Sets whether the Pokémon is on the battlefield.
|
|
/// </summary>
|
|
void SetOnBattlefield(bool onBattleField);
|
|
|
|
/// <summary>
|
|
/// Sets the position the Pokémon has within its side.
|
|
/// </summary>
|
|
/// <param name="position"></param>
|
|
void SetBattleSidePosition(byte position);
|
|
|
|
/// <summary>
|
|
/// Resets the battle data of the Pokémon. This is called when the battle ends.
|
|
/// </summary>
|
|
void ClearBattleData();
|
|
|
|
/// <summary>
|
|
/// Marks a Pokemon as seen in the battle.
|
|
/// </summary>
|
|
void MarkOpponentAsSeen(IPokemon pokemon);
|
|
|
|
/// <summary>
|
|
/// Removes a type from the Pokémon. Returns whether the type was removed.
|
|
/// </summary>
|
|
bool RemoveType(TypeIdentifier type);
|
|
|
|
/// <summary>
|
|
/// Adds a type to the Pokémon. Returns whether the type was added. It will not add the type if
|
|
/// the Pokémon already has it.
|
|
/// </summary>
|
|
bool AddType(TypeIdentifier type);
|
|
|
|
/// <summary>
|
|
/// Replace the types of the Pokémon with the provided types.
|
|
/// </summary>
|
|
void SetTypes(IReadOnlyList<TypeIdentifier> types);
|
|
|
|
/// <summary>
|
|
/// Changes the ability of the Pokémon.
|
|
/// </summary>
|
|
bool ChangeAbility(IAbility ability);
|
|
|
|
/// <summary>
|
|
/// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as
|
|
/// Levitate.
|
|
/// </summary>
|
|
bool IsFloating { get; }
|
|
|
|
/// <summary>
|
|
/// Converts the data structure to a serializable format.
|
|
/// </summary>
|
|
SerializedPokemon Serialize();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The data of the Pokémon related to being in a battle.
|
|
/// This is only set when the Pokémon is on the field in a battle.
|
|
/// </summary>
|
|
public interface IPokemonBattleData : IDeepCloneable
|
|
{
|
|
/// <summary>
|
|
/// The battle the Pokémon is in.
|
|
/// </summary>
|
|
IBattle Battle { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// The index of the side of the Pokémon
|
|
/// </summary>
|
|
byte SideIndex { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// The index of the position of the Pokémon on the field
|
|
/// </summary>
|
|
byte Position { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// A list of opponents the Pokémon has seen this battle.
|
|
/// </summary>
|
|
IReadOnlyList<IPokemon> SeenOpponents { get; }
|
|
|
|
/// <summary>
|
|
/// Whether the Pokémon is on the battlefield.
|
|
/// </summary>
|
|
bool IsOnBattlefield { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// Adds an opponent to the list of seen opponents.
|
|
/// </summary>
|
|
void MarkOpponentAsSeen(IPokemon opponent);
|
|
|
|
/// <summary>
|
|
/// A list of items the Pokémon has consumed this battle.
|
|
/// </summary>
|
|
IReadOnlyList<IItem> ConsumedItems { get; }
|
|
|
|
/// <summary>
|
|
/// Marks an item as consumed.
|
|
/// </summary>
|
|
void MarkItemAsConsumed(IItem item);
|
|
|
|
/// <summary>
|
|
/// The turn the Pokémon switched in.
|
|
/// </summary>
|
|
uint SwitchInTurn { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// The side the Pokémon is on.
|
|
/// </summary>
|
|
IBattleSide BattleSide { get; }
|
|
|
|
/// <summary>
|
|
/// The species of the Pokémon at the time it was sent out.
|
|
/// </summary>
|
|
ISpecies OriginalSpecies { get; }
|
|
|
|
/// <summary>
|
|
/// The form of the Pokémon at the time it was sent out.
|
|
/// </summary>
|
|
IForm OriginalForm { get; }
|
|
}
|
|
|
|
/// <inheritdoc cref="IPokemon"/>
|
|
public class PokemonImpl : ScriptSource, IPokemon
|
|
{
|
|
/// <inheritdoc cref="PokemonImpl"/>
|
|
public PokemonImpl(IDynamicLibrary library, ISpecies species, IForm form, AbilityIndex abilityIndex, LevelInt level,
|
|
uint personalityValue, Gender gender, byte coloring, StringKey natureName)
|
|
{
|
|
Library = library;
|
|
Species = species;
|
|
Form = form;
|
|
AbilityIndex = abilityIndex;
|
|
Level = level;
|
|
PersonalityValue = personalityValue;
|
|
Gender = gender;
|
|
Coloring = coloring;
|
|
Types = form.Types.ToList();
|
|
|
|
Experience = library.StaticLibrary.GrowthRates.CalculateExperience(species.GrowthRate, level);
|
|
HeightInMeters = form.Height;
|
|
Happiness = species.BaseHappiness;
|
|
Volatile = new ScriptSet(this);
|
|
if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature))
|
|
throw new KeyNotFoundException($"Nature {natureName} not found.");
|
|
Nature = nature;
|
|
|
|
RecalculateFlatStats();
|
|
CurrentHealth = BoostedStats.Hp;
|
|
}
|
|
|
|
/// <inheritdoc cref="PokemonImpl"/>
|
|
public PokemonImpl(IDynamicLibrary library, SerializedPokemon serializedPokemon)
|
|
{
|
|
Library = library;
|
|
if (!library.StaticLibrary.Species.TryGet(serializedPokemon.Species, out var species))
|
|
throw new KeyNotFoundException($"Species {serializedPokemon.Species} not found.");
|
|
Species = species;
|
|
if (!species.TryGetForm(serializedPokemon.Form, out var form))
|
|
throw new KeyNotFoundException($"Form {serializedPokemon.Form} not found on species {species.Name}.");
|
|
Form = form;
|
|
Level = serializedPokemon.Level;
|
|
Experience = serializedPokemon.Experience;
|
|
PersonalityValue = serializedPokemon.PersonalityValue;
|
|
Gender = serializedPokemon.Gender;
|
|
Coloring = serializedPokemon.Coloring;
|
|
if (serializedPokemon.HeldItem != null)
|
|
{
|
|
if (!library.StaticLibrary.Items.TryGet(serializedPokemon.HeldItem, out var item))
|
|
throw new KeyNotFoundException($"Item {serializedPokemon.HeldItem} not found.");
|
|
HeldItem = item;
|
|
}
|
|
|
|
CurrentHealth = serializedPokemon.CurrentHealth;
|
|
HeightInMeters = form.Height;
|
|
Happiness = serializedPokemon.Happiness;
|
|
IndividualValues = serializedPokemon.IndividualValues.ToIndividualValueStatisticSet();
|
|
EffortValues = serializedPokemon.EffortValues.ToEffortValueStatisticSet();
|
|
if (!library.StaticLibrary.Natures.TryGet(serializedPokemon.Nature, out var nature))
|
|
throw new KeyNotFoundException($"Nature {serializedPokemon.Nature} not found.");
|
|
Nature = nature;
|
|
Nickname = serializedPokemon.Nickname;
|
|
if (!library.StaticLibrary.Abilities.TryGet(serializedPokemon.Ability, out var ability))
|
|
throw new KeyNotFoundException($"Ability {serializedPokemon.Ability} not found.");
|
|
AbilityIndex = form.FindAbilityIndex(ability) ??
|
|
throw new KeyNotFoundException(
|
|
$"Ability {ability.Name} not found on species {species.Name} form {form.Name}.");
|
|
Volatile = new ScriptSet(this);
|
|
_learnedMoves = serializedPokemon.Moves.Select(move =>
|
|
{
|
|
if (move == null)
|
|
return null;
|
|
if (!library.StaticLibrary.Moves.TryGet(move.MoveName, out var moveData))
|
|
throw new KeyNotFoundException($"Move {move.MoveName} not found");
|
|
return (ILearnedMove)new LearnedMoveImpl(moveData, move.LearnMethod, move.CurrentPp);
|
|
}).ToArray();
|
|
AllowedExperience = serializedPokemon.AllowedExperience;
|
|
IsEgg = serializedPokemon.IsEgg;
|
|
Types = form.Types;
|
|
RecalculateFlatStats();
|
|
|
|
if (serializedPokemon.Status != null)
|
|
{
|
|
if (!library.ScriptResolver.TryResolve(ScriptCategory.Status, serializedPokemon.Status, null,
|
|
out var statusScript))
|
|
throw new KeyNotFoundException($"Status script {serializedPokemon.Status} not found");
|
|
StatusScript.Set(statusScript);
|
|
statusScript.OnAddedToParent(this);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IDynamicLibrary Library { get; }
|
|
|
|
/// <inheritdoc />
|
|
public ISpecies Species { get; }
|
|
|
|
/// <inheritdoc />
|
|
public IForm Form { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public ISpecies? DisplaySpecies { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public IForm? DisplayForm { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public void SetDisplaySpecies(ISpecies? species, IForm? form)
|
|
{
|
|
DisplaySpecies = species;
|
|
DisplayForm = form;
|
|
|
|
BattleData?.Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form)
|
|
{
|
|
BatchId = new EventBatchId(),
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public LevelInt Level { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public uint Experience { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public bool AddExperience(uint experience)
|
|
{
|
|
if (!AllowedExperience)
|
|
return false;
|
|
var maxLevel = Library.StaticLibrary.Settings.MaxLevel;
|
|
if (Level >= maxLevel)
|
|
return false;
|
|
var oldLevel = Level;
|
|
var oldExperience = Experience;
|
|
Experience += experience;
|
|
var batchId = new EventBatchId();
|
|
BattleData?.Battle.EventHook.Invoke(new ExperienceGainEvent(this, oldExperience, Experience)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
|
|
var newLevel = Library.StaticLibrary.GrowthRates.CalculateLevel(Species.GrowthRate, Experience);
|
|
if (newLevel > Level)
|
|
{
|
|
Level = newLevel;
|
|
RecalculateFlatStats();
|
|
BattleData?.Battle.EventHook.Invoke(new LevelUpEvent(this, oldLevel, Level)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
|
|
if (newLevel >= maxLevel)
|
|
{
|
|
Experience = Library.StaticLibrary.GrowthRates.CalculateExperience(Species.GrowthRate, maxLevel);
|
|
}
|
|
}
|
|
|
|
return oldExperience != Experience;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public uint PersonalityValue { get; }
|
|
|
|
/// <inheritdoc />
|
|
public Gender Gender { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public byte Coloring { get; }
|
|
|
|
/// <inheritdoc />
|
|
public bool IsShiny => Coloring == 1;
|
|
|
|
/// <inheritdoc />
|
|
public IItem? HeldItem { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public uint CurrentHealth { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public float WeightInKg
|
|
{
|
|
get
|
|
{
|
|
var weight = Form.Weight;
|
|
if (BattleData is not null)
|
|
// ReSharper disable once AccessToModifiedClosure
|
|
this.RunScriptHook(script => script.ModifyWeight(ref weight));
|
|
if (weight < 0.1f)
|
|
weight = 0.1f;
|
|
return weight;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public float HeightInMeters { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public byte Happiness { get; }
|
|
|
|
/// <inheritdoc />
|
|
public StatisticSet<uint> FlatStats { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public StatBoostStatisticSet StatBoost { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public StatisticSet<uint> BoostedStats { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public uint MaxHealth => BoostedStats.Hp;
|
|
|
|
/// <inheritdoc />
|
|
public IndividualValueStatisticSet IndividualValues { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public EffortValueStatisticSet EffortValues { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public INature Nature { get; }
|
|
|
|
/// <inheritdoc />
|
|
public string? Nickname { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public AbilityIndex AbilityIndex { get; }
|
|
|
|
/// <inheritdoc />
|
|
public IAbility? OverrideAbility { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public IPokemonBattleData? BattleData { get; private set; }
|
|
|
|
private readonly ILearnedMove?[] _learnedMoves = new ILearnedMove[Const.MovesCount];
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<ILearnedMove?> Moves => _learnedMoves;
|
|
|
|
/// <inheritdoc />
|
|
public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName);
|
|
|
|
/// <inheritdoc />
|
|
public void SwapMoves(byte index1, byte index2)
|
|
{
|
|
if (index1 >= Const.MovesCount || index2 >= Const.MovesCount)
|
|
return;
|
|
var move1 = _learnedMoves[index1];
|
|
var move2 = _learnedMoves[index2];
|
|
_learnedMoves[index1] = move2;
|
|
_learnedMoves[index2] = move1;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool AllowedExperience { get; set; }
|
|
|
|
private List<TypeIdentifier> _types = new();
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<TypeIdentifier> Types
|
|
{
|
|
get => _types;
|
|
private set => _types = value.ToList();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool IsEgg { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public bool IsCaught { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public void MarkAsCaught()
|
|
{
|
|
IsCaught = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ScriptContainer HeldItemTriggerScript { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public ScriptContainer AbilityScript { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public ScriptContainer StatusScript { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public IScriptSet Volatile { get; }
|
|
|
|
/// <inheritdoc />
|
|
public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName;
|
|
|
|
/// <inheritdoc />
|
|
public IItem? SetHeldItem(IItem? item)
|
|
{
|
|
var previous = HeldItem;
|
|
HeldItem = item;
|
|
this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item));
|
|
return previous;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IItem? RemoveHeldItem()
|
|
{
|
|
if (HeldItem is not null)
|
|
{
|
|
if (HeldItem.Category == ItemCategory.FormChanger)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
var previous = HeldItem;
|
|
HeldItem = null;
|
|
this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null));
|
|
return previous;
|
|
}
|
|
|
|
private IItem? _stolenHeldItem;
|
|
|
|
/// <inheritdoc />
|
|
public IItem? RemoveHeldItemForBattle()
|
|
{
|
|
return _stolenHeldItem = RemoveHeldItem();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item)
|
|
{
|
|
if (HeldItem is null || HeldItem.Category == ItemCategory.FormChanger)
|
|
{
|
|
item = null;
|
|
return false;
|
|
}
|
|
var prevent = false;
|
|
this.RunScriptHook(script => script.PreventHeldItemSteal(this, HeldItem, ref prevent));
|
|
if (prevent)
|
|
{
|
|
item = null;
|
|
return false;
|
|
}
|
|
item = RemoveHeldItemForBattle();
|
|
return item is not null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RestoreStolenHeldItem()
|
|
{
|
|
_ = SetHeldItem(_stolenHeldItem);
|
|
_stolenHeldItem = null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool ConsumeHeldItem()
|
|
{
|
|
if (HeldItem is null)
|
|
return false;
|
|
if (!Library.ScriptResolver.TryResolveBattleItemScript(HeldItem, out _))
|
|
return false;
|
|
|
|
if (BattleData != null)
|
|
{
|
|
var prevented = false;
|
|
this.RunScriptHook(script => script.PreventHeldItemConsume(this, HeldItem, ref prevented));
|
|
if (prevented)
|
|
return false;
|
|
BattleData.MarkItemAsConsumed(HeldItem);
|
|
}
|
|
|
|
UseItem(SetHeldItem(null)!);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses an item on this Pokémon.
|
|
/// </summary>
|
|
/// <param name="item"></param>
|
|
public void UseItem(IItem item)
|
|
{
|
|
// TODO: actually consume the item
|
|
|
|
this.RunScriptHook(x => x.OnAfterItemConsume(this, item));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force,
|
|
EventBatchId batchId = default)
|
|
{
|
|
if (!force)
|
|
{
|
|
var prevented = false;
|
|
this.RunScriptHook(script =>
|
|
script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
|
|
if (prevented)
|
|
return false;
|
|
this.RunScriptHook(script => script.ChangeStatBoostChange(this, stat, selfInflicted, ref change));
|
|
if (change == 0)
|
|
return false;
|
|
}
|
|
var changed = false;
|
|
var oldBoost = StatBoost.GetStatistic(stat);
|
|
changed = change switch
|
|
{
|
|
> 0 => StatBoost.IncreaseStatistic(stat, change),
|
|
< 0 => StatBoost.DecreaseStatistic(stat, change),
|
|
_ => changed,
|
|
};
|
|
if (!changed)
|
|
return false;
|
|
if (BattleData != null)
|
|
{
|
|
var newBoost = StatBoost.GetStatistic(stat);
|
|
BattleData.Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
}
|
|
|
|
RecalculateBoostedStats();
|
|
this.RunScriptHook(script => script.OnAfterStatBoostChange(this, stat, selfInflicted, change));
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the ability of the Pokémon is suppressed.
|
|
/// </summary>
|
|
public bool AbilitySuppressed { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public void SuppressAbility()
|
|
{
|
|
AbilitySuppressed = true;
|
|
AbilityScript.Clear();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IAbility? ActiveAbility
|
|
{
|
|
get
|
|
{
|
|
if (AbilitySuppressed)
|
|
return null;
|
|
if (OverrideAbility != null)
|
|
return OverrideAbility;
|
|
var ability = Form.GetAbility(AbilityIndex);
|
|
if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj))
|
|
throw new KeyNotFoundException($"Ability {ability} not found.");
|
|
return abilityObj;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecalculateFlatStats()
|
|
{
|
|
Library.StatCalculator.CalculateFlatStats(this, FlatStats);
|
|
RecalculateBoostedStats();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats);
|
|
|
|
/// <inheritdoc />
|
|
public void ChangeSpecies(ISpecies species, IForm form)
|
|
{
|
|
if (Species == species)
|
|
{
|
|
if (form != Form)
|
|
ChangeForm(form, new EventBatchId());
|
|
return;
|
|
}
|
|
|
|
// If the Pokémon is genderless, but its new species is not, we want to set its gender
|
|
if (Gender != Gender.Genderless && species.GenderRate < 0.0)
|
|
{
|
|
var random = (IRandom?)BattleData?.Battle.Random ?? new RandomImpl();
|
|
Gender = species.GetRandomGender(random);
|
|
}
|
|
// Else if the new species is genderless, but the Pokémon has a gender, make the creature genderless.
|
|
else if (species.GenderRate < 0.0 && Gender != Gender.Genderless)
|
|
{
|
|
Gender = Gender.Genderless;
|
|
}
|
|
|
|
var batchId = new EventBatchId();
|
|
BattleData?.Battle.EventHook.Invoke(new SpeciesChangeEvent(this, species, form)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
ChangeForm(form, batchId);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ChangeForm(IForm form, EventBatchId batchId = default)
|
|
{
|
|
if (form == Form)
|
|
return;
|
|
|
|
var oldAbility = Form.GetAbility(AbilityIndex);
|
|
|
|
Form = form;
|
|
Types = form.Types.ToList();
|
|
HeightInMeters = form.Height;
|
|
var newAbility = Form.GetAbility(AbilityIndex);
|
|
|
|
if (OverrideAbility == null && oldAbility != newAbility)
|
|
{
|
|
AbilityScript.Clear();
|
|
if (!Library.StaticLibrary.Abilities.TryGet(newAbility, out var ability))
|
|
throw new KeyNotFoundException($"Ability {newAbility} not found.");
|
|
if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, newAbility, ability.Parameters,
|
|
out var abilityScript))
|
|
{
|
|
AbilityScript.Set(abilityScript);
|
|
abilityScript.OnAddedToParent(this);
|
|
}
|
|
else
|
|
{
|
|
AbilityScript.Clear();
|
|
}
|
|
}
|
|
|
|
var oldHealth = BoostedStats.Hp;
|
|
RecalculateFlatStats();
|
|
var diffHealth = (long)BoostedStats.Hp - oldHealth;
|
|
if (diffHealth > 0)
|
|
{
|
|
Heal((uint)diffHealth, true);
|
|
}
|
|
|
|
// TODO: form specific moves?
|
|
|
|
BattleData?.Battle.EventHook.Invoke(new FormChangeEvent(this, form)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
/// <remarks>
|
|
/// Currently this checks the Pokémon is not an egg, not caught, and not fainted.
|
|
/// </remarks>
|
|
public bool IsUsable => !IsCaught && !IsEgg && !IsFainted;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsFainted => CurrentHealth == 0;
|
|
|
|
/// <inheritdoc />
|
|
public void Damage(uint damage, DamageSource source, EventBatchId batchId, bool forceDamage = false)
|
|
{
|
|
// If the Pokémon is already fainted, we don't need to do anything.
|
|
if (IsFainted)
|
|
return;
|
|
if (BattleData is not null && !forceDamage)
|
|
{
|
|
var dmg = damage;
|
|
this.RunScriptHook(script => script.ChangeIncomingDamage(this, source, ref dmg));
|
|
damage = dmg;
|
|
}
|
|
if (damage == 0)
|
|
return;
|
|
|
|
// If the damage is more than the current health, we cap it at the current health, to prevent
|
|
// underflow.
|
|
if (damage >= CurrentHealth)
|
|
damage = CurrentHealth;
|
|
// Calculate the new health.
|
|
var newHealth = CurrentHealth - damage;
|
|
if (BattleData is not null)
|
|
{
|
|
// If the Pokémon is in a battle, we trigger an event to the front-end.
|
|
BattleData.Battle.EventHook.Invoke(new DamageEvent(this, CurrentHealth, newHealth, source)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
// And allow scripts to execute.
|
|
this.RunScriptHook(script => script.OnDamage(this, source, CurrentHealth, newHealth));
|
|
}
|
|
|
|
CurrentHealth = newHealth;
|
|
|
|
// If the Pokémon is now fainted, we also run faint handling.
|
|
if (IsFainted)
|
|
{
|
|
OnFaint(source);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Faint(DamageSource source, EventBatchId batchId = default)
|
|
{
|
|
CurrentHealth = 0;
|
|
OnFaint(source);
|
|
}
|
|
|
|
private void OnFaint(DamageSource source)
|
|
{
|
|
// If the Pokémon is not in a battle, we don't need to do anything.
|
|
if (BattleData is null)
|
|
return;
|
|
|
|
// Trigger the faint event to the front-end.
|
|
BattleData.Battle.EventHook.Invoke(new FaintEvent(this));
|
|
|
|
// Allow scripts to trigger based on the faint.
|
|
this.RunScriptHook(script => script.OnFaint(this, source));
|
|
foreach (var ally in BattleData.BattleSide.Pokemon.WhereNotNull().Where(x => x != this))
|
|
{
|
|
ally.RunScriptHook(script => script.OnAllyFaint(ally, this));
|
|
}
|
|
|
|
// Make sure the OnRemove script is run.
|
|
this.RunScriptHook(script => script.OnRemove());
|
|
|
|
// Mark the position as unfillable if it can't be filled by any party.
|
|
if (!BattleData.Battle.CanSlotBeFilled(BattleData.SideIndex, BattleData.Position))
|
|
{
|
|
BattleData.Battle.Sides[BattleData.SideIndex].MarkPositionAsUnfillable(BattleData.Position);
|
|
}
|
|
BattleData.BattleSide.MarkFaint(BattleData.Position);
|
|
|
|
// Validate the battle state to see if the battle is over.
|
|
BattleData.Battle.ValidateBattleState();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool Heal(uint heal, bool allowRevive, EventBatchId batchId = default, bool forceHeal = false,
|
|
EventHook? customEventHook = null)
|
|
{
|
|
if (IsFainted && !allowRevive)
|
|
return false;
|
|
|
|
var maxAmount = BoostedStats.Hp - CurrentHealth;
|
|
if (heal > maxAmount)
|
|
heal = maxAmount;
|
|
if (heal == 0)
|
|
return false;
|
|
if (!forceHeal)
|
|
{
|
|
var prevented = false;
|
|
this.RunScriptHook(x => x.PreventHeal(this, heal, allowRevive, ref prevented));
|
|
if (prevented)
|
|
return false;
|
|
}
|
|
|
|
var newHealth = CurrentHealth + heal;
|
|
customEventHook ??= BattleData?.Battle.EventHook;
|
|
customEventHook?.Invoke(new HealEvent(this, CurrentHealth, newHealth)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
CurrentHealth = newHealth;
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RestoreAllPP()
|
|
{
|
|
foreach (var move in Moves)
|
|
{
|
|
move?.RestoreAllUses();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
/// <remarks>
|
|
/// If the index is 255, it will try to find the first empty move slot.
|
|
/// </remarks>
|
|
public void LearnMove(StringKey moveName, MoveLearnMethod method, byte index)
|
|
{
|
|
if (index == 255)
|
|
{
|
|
for (byte i = 0; i < Moves.Count; i++)
|
|
{
|
|
if (Moves[i] is not null)
|
|
continue;
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index >= Moves.Count)
|
|
throw new InvalidOperationException("No empty move slot found.");
|
|
if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move))
|
|
throw new KeyNotFoundException($"Move {moveName} not found.");
|
|
|
|
_learnedMoves[index] = new LearnedMoveImpl(move, method);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool HasStatus(StringKey status) => StatusScript.Script?.Name == status;
|
|
|
|
/// <inheritdoc />
|
|
public bool SetStatus(StringKey status, bool selfInflicted, EventBatchId batchId = default)
|
|
{
|
|
if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript))
|
|
throw new KeyNotFoundException($"Status script {status} not found");
|
|
|
|
if (!StatusScript.IsEmpty)
|
|
return false;
|
|
var oldStatus = StatusScript.Script?.Name;
|
|
|
|
var preventStatus = false;
|
|
this.RunScriptHook(script => script.PreventStatusChange(this, status, selfInflicted, ref preventStatus));
|
|
if (preventStatus)
|
|
return false;
|
|
|
|
StatusScript.Set(statusScript);
|
|
statusScript.OnAddedToParent(this);
|
|
BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ClearStatus(EventBatchId batchId = default)
|
|
{
|
|
StatusScript.Clear();
|
|
BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, StatusScript.Script?.Name, null)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ChangeLevelBy(int change)
|
|
{
|
|
var newLevel = Level + change;
|
|
Level = (LevelInt)Math.Clamp(newLevel, 1, Library.StaticLibrary.Settings.MaxLevel);
|
|
RecalculateFlatStats();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetBattleData(IBattle battle, byte sideIndex)
|
|
{
|
|
if (BattleData is not null)
|
|
{
|
|
BattleData.Battle = battle;
|
|
BattleData.SideIndex = sideIndex;
|
|
BattleData.SwitchInTurn = battle.CurrentTurnNumber;
|
|
}
|
|
else
|
|
{
|
|
BattleData = new PokemonBattleDataImpl(battle, sideIndex, battle.CurrentTurnNumber, Species, Form);
|
|
}
|
|
if (ActiveAbility != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ActiveAbility.Name,
|
|
ActiveAbility.Parameters, out var abilityScript))
|
|
{
|
|
AbilityScript.Set(abilityScript);
|
|
abilityScript.OnAddedToParent(this);
|
|
}
|
|
else
|
|
{
|
|
AbilityScript.Clear();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetOnBattlefield(bool onBattleField)
|
|
{
|
|
if (BattleData is not null)
|
|
{
|
|
BattleData.IsOnBattlefield = onBattleField;
|
|
if (!onBattleField)
|
|
{
|
|
Volatile.Clear();
|
|
HeightInMeters = Form.Height;
|
|
Types = Form.Types;
|
|
OverrideAbility = null;
|
|
AbilitySuppressed = false;
|
|
RecalculateFlatStats();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetBattleSidePosition(byte position)
|
|
{
|
|
if (BattleData is not null)
|
|
{
|
|
BattleData.Position = position;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ClearBattleData()
|
|
{
|
|
var battleData = BattleData;
|
|
BattleData = null;
|
|
Volatile.Clear();
|
|
HeightInMeters = Form.Height;
|
|
Types = Form.Types;
|
|
OverrideAbility = null;
|
|
AbilitySuppressed = false;
|
|
StatBoost.Reset();
|
|
if (battleData != null && Form.IsBattleOnlyForm)
|
|
{
|
|
ChangeForm(battleData.OriginalSpecies == Species ? battleData.OriginalForm : Species.GetDefaultForm());
|
|
}
|
|
DisplaySpecies = null;
|
|
DisplayForm = null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon);
|
|
|
|
/// <inheritdoc />
|
|
public bool RemoveType(TypeIdentifier type) => _types.Remove(type);
|
|
|
|
/// <inheritdoc />
|
|
public bool AddType(TypeIdentifier type)
|
|
{
|
|
if (_types.Contains(type))
|
|
return false;
|
|
_types.Add(type);
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetTypes(IReadOnlyList<TypeIdentifier> types)
|
|
{
|
|
_types = types.ToList();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool ChangeAbility(IAbility ability)
|
|
{
|
|
if (!ability.CanBeChanged)
|
|
return false;
|
|
OverrideAbility = ability;
|
|
if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters,
|
|
out var abilityScript))
|
|
{
|
|
AbilityScript.Set(abilityScript);
|
|
abilityScript.OnAddedToParent(this);
|
|
}
|
|
else
|
|
{
|
|
AbilityScript.Clear();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool IsFloating
|
|
{
|
|
get
|
|
{
|
|
var isFloating = Types.Any(x => x.Name == "flying");
|
|
this.RunScriptHook(x => x.IsFloating(this, ref isFloating));
|
|
return isFloating;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public SerializedPokemon Serialize() => new(this);
|
|
|
|
/// <inheritdoc />
|
|
public override int ScriptCount
|
|
{
|
|
get
|
|
{
|
|
var c = 4;
|
|
if (BattleData != null)
|
|
{
|
|
var side = BattleData.Battle.Sides[BattleData.SideIndex];
|
|
c += side.ScriptCount;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts)
|
|
{
|
|
scripts.Add(HeldItemTriggerScript);
|
|
scripts.Add(AbilityScript);
|
|
scripts.Add(StatusScript);
|
|
scripts.Add(Volatile);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts)
|
|
{
|
|
GetOwnScripts(scripts);
|
|
if (BattleData != null)
|
|
{
|
|
var side = BattleData.Battle.Sides[BattleData.SideIndex];
|
|
side.CollectScripts(scripts);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
if (!string.IsNullOrEmpty(Nickname))
|
|
return $"{Nickname} ({Species.Name})";
|
|
return Species.Name;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public class PokemonBattleDataImpl : IPokemonBattleData
|
|
{
|
|
/// <inheritdoc cref="PokemonBattleDataImpl"/>
|
|
public PokemonBattleDataImpl(IBattle battle, byte sideIndex, uint switchInTurn, ISpecies originalSpecies,
|
|
IForm originalForm)
|
|
{
|
|
Battle = battle;
|
|
SideIndex = sideIndex;
|
|
SwitchInTurn = switchInTurn;
|
|
OriginalSpecies = originalSpecies;
|
|
OriginalForm = originalForm;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IBattle Battle { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public byte SideIndex { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public byte Position { get; set; }
|
|
|
|
private readonly List<IPokemon> _seenOpponents = [];
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<IPokemon> SeenOpponents => _seenOpponents;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsOnBattlefield { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public void MarkOpponentAsSeen(IPokemon opponent)
|
|
{
|
|
_seenOpponents.Add(opponent);
|
|
}
|
|
|
|
private readonly List<IItem> _consumedItems = [];
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<IItem> ConsumedItems => _consumedItems;
|
|
|
|
/// <inheritdoc />
|
|
public void MarkItemAsConsumed(IItem item)
|
|
{
|
|
_consumedItems.Add(item);
|
|
BattleSide.SetConsumedItem(Position, item);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public uint SwitchInTurn { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public IBattleSide BattleSide => Battle.Sides[SideIndex];
|
|
|
|
/// <inheritdoc />
|
|
public ISpecies OriginalSpecies { get; }
|
|
|
|
/// <inheritdoc />
|
|
public IForm OriginalForm { get; }
|
|
} |