783 lines
24 KiB
C#
783 lines
24 KiB
C#
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 persistent data of a Pokemon. This holds everything that outlives a battle. All state that is only
|
|
/// relevant for the duration of a battle lives on <see cref="IBattlePokemon"/> instead.
|
|
/// </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>
|
|
/// 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. If an event hook is
|
|
/// passed, experience and level up events are sent to it.
|
|
/// </summary>
|
|
bool AddExperience(uint experience, EventHook? eventHook = null);
|
|
|
|
/// <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; }
|
|
|
|
/// <summary>
|
|
/// The happiness of the Pokemon. Also known as friendship.
|
|
/// </summary>
|
|
byte Happiness { get; set; }
|
|
|
|
/// <summary>
|
|
/// The stats of the Pokemon when disregarding any stat boosts.
|
|
/// </summary>
|
|
StatisticSet<uint> FlatStats { 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>
|
|
/// The ability of the Pokemon, as determined by its form and ability index.
|
|
/// </summary>
|
|
IAbility Ability { 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 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 types of the Pokemon, as determined by its form.
|
|
/// </summary>
|
|
IReadOnlyList<TypeIdentifier> Types { get; }
|
|
|
|
/// <summary>
|
|
/// Whether or not this Pokemon is an egg.
|
|
/// </summary>
|
|
bool IsEgg { get; }
|
|
|
|
/// <summary>
|
|
/// The script for the status.
|
|
/// </summary>
|
|
ScriptContainer StatusScript { get; }
|
|
|
|
/// <summary>
|
|
/// The number of turns left for the current non-volatile status.
|
|
/// </summary>
|
|
int? GetStatusTurnsLeft { 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? ForceSetHeldItem(IItem? item);
|
|
|
|
/// <summary>
|
|
/// Removes the held item from the Pokemon. Returns the previously held item.
|
|
/// </summary>
|
|
[MustUseReturnValue]
|
|
IItem? RemoveHeldItem();
|
|
|
|
/// <summary>
|
|
/// Uses an item on the Pokemon.
|
|
/// </summary>
|
|
void UseItem(IItem item);
|
|
|
|
/// <summary>
|
|
/// Calculates the flat stats on the Pokemon. This should be called when for example the base
|
|
/// stats, level, nature, IV, or EV changes.
|
|
/// </summary>
|
|
void RecalculateFlatStats();
|
|
|
|
/// <summary>
|
|
/// Evolves the Pokemon to a specific evolution. This will not check whether the evolution is valid, so
|
|
/// you should check that before calling this method.
|
|
/// </summary>
|
|
bool EvolveTo(IEvolution evolution, EventHook? eventHook = null);
|
|
|
|
/// <summary>
|
|
/// Change the species of the Pokemon. An optional random can be passed, which is used when the gender of
|
|
/// the Pokemon needs to be re-rolled.
|
|
/// </summary>
|
|
void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null);
|
|
|
|
/// <summary>
|
|
/// Change the form of the Pokemon.
|
|
/// </summary>
|
|
void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null);
|
|
|
|
/// <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. This only changes the health of the Pokemon;
|
|
/// battle handling, such as script hooks, events and faint handling, is done by
|
|
/// <see cref="IBattlePokemon.Damage"/>.
|
|
/// </summary>
|
|
void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false);
|
|
|
|
/// <summary>
|
|
/// Sets the health of the Pokémon to 0.
|
|
/// </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. This only sets the status script; battle handling, such as
|
|
/// prevention script hooks and events, is done by <see cref="IBattlePokemon.SetStatus"/>.
|
|
/// </summary>
|
|
bool SetStatus(StringKey status, IPokemon? originPokemon, 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>
|
|
/// Converts the data structure to a serializable format.
|
|
/// </summary>
|
|
SerializedPokemon Serialize();
|
|
}
|
|
|
|
/// <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;
|
|
|
|
Experience = library.StaticLibrary.GrowthRates.CalculateExperience(species.GrowthRate, level);
|
|
Happiness = species.BaseHappiness;
|
|
if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature))
|
|
throw new KeyNotFoundException($"Nature {natureName} not found.");
|
|
Nature = nature;
|
|
|
|
RecalculateFlatStats();
|
|
CurrentHealth = FlatStats.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;
|
|
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}.");
|
|
_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;
|
|
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; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public IForm Form { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public LevelInt Level { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public uint Experience { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public bool AddExperience(uint experience, EventHook? eventHook = null)
|
|
{
|
|
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();
|
|
eventHook?.Invoke(new ExperienceGainEvent(this, oldExperience, Experience)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
|
|
var newLevel = Library.StaticLibrary.GrowthRates.CalculateLevel(Species.GrowthRate, Experience);
|
|
if (newLevel > Level)
|
|
{
|
|
Level = newLevel;
|
|
RecalculateFlatStats();
|
|
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 (weight < 0.1f)
|
|
weight = 0.1f;
|
|
return weight;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public float HeightInMeters => Form.Height;
|
|
|
|
/// <inheritdoc />
|
|
public byte Happiness { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public StatisticSet<uint> FlatStats { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public uint MaxHealth => FlatStats.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; }
|
|
|
|
private (IAbility Ability, IForm Form, AbilityIndex Index)? _abilityCache;
|
|
|
|
/// <inheritdoc />
|
|
public IAbility Ability
|
|
{
|
|
get
|
|
{
|
|
if (_abilityCache is not null && _abilityCache.Value.Form == Form &&
|
|
_abilityCache.Value.Index == AbilityIndex)
|
|
return _abilityCache.Value.Ability;
|
|
var ability = Form.GetAbility(AbilityIndex);
|
|
if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj))
|
|
throw new KeyNotFoundException($"Ability {ability} not found.");
|
|
_abilityCache = (abilityObj, Form, AbilityIndex);
|
|
return abilityObj;
|
|
}
|
|
}
|
|
|
|
private readonly ILearnedMove?[] _learnedMoves = new ILearnedMove[Const.MovesCount];
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<ILearnedMove?> Moves => _learnedMoves;
|
|
|
|
/// <inheritdoc />
|
|
public bool HasMove(StringKey moveName) => _learnedMoves.Any(move => move?.MoveData.Name == moveName);
|
|
|
|
/// <inheritdoc />
|
|
public void SwapMoves(byte index1, byte index2)
|
|
{
|
|
if (index1 >= Const.MovesCount || index2 >= Const.MovesCount)
|
|
return;
|
|
(_learnedMoves[index1], _learnedMoves[index2]) = (_learnedMoves[index2], _learnedMoves[index1]);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool AllowedExperience { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<TypeIdentifier> Types => Form.Types;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsEgg { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public ScriptContainer StatusScript { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft();
|
|
|
|
/// <inheritdoc />
|
|
public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName;
|
|
|
|
/// <inheritdoc />
|
|
public IItem? ForceSetHeldItem(IItem? item)
|
|
{
|
|
var previous = HeldItem;
|
|
HeldItem = item;
|
|
return previous;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IItem? RemoveHeldItem()
|
|
{
|
|
if (HeldItem is not null)
|
|
{
|
|
if (HeldItem.Category == ItemCategory.FormChanger)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
var previous = HeldItem;
|
|
HeldItem = null;
|
|
return previous;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void UseItem(IItem item)
|
|
{
|
|
// TODO: actually consume the item
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecalculateFlatStats() => Library.StatCalculator.CalculateFlatStats(this, FlatStats);
|
|
|
|
/// <inheritdoc />
|
|
public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null)
|
|
{
|
|
if (!Species.EvolutionData.Contains(evolution))
|
|
return false;
|
|
if (!Library.StaticLibrary.Species.TryGet(evolution.ToSpecies, out var species))
|
|
return false;
|
|
|
|
// TODO: Consider how forms work with evolution items.
|
|
var newForm = species.GetDefaultForm();
|
|
eventHook?.Invoke(new EvolutionEvent(this, Species, Form, species, newForm, evolution));
|
|
|
|
ChangeSpecies(species, newForm, eventHook);
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null)
|
|
{
|
|
if (Species == species)
|
|
{
|
|
if (form != Form)
|
|
ChangeForm(form, new EventBatchId(), eventHook);
|
|
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)
|
|
{
|
|
Gender = species.GetRandomGender(random ?? new RandomImpl());
|
|
}
|
|
// 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();
|
|
eventHook?.Invoke(new SpeciesChangeEvent(this, species, form)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
Species = species;
|
|
ChangeForm(form, batchId, eventHook);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null)
|
|
{
|
|
if (form == Form)
|
|
return;
|
|
|
|
Form = form;
|
|
var abilityIndex = AbilityIndex;
|
|
abilityIndex = AbilityIndex.IsHidden switch
|
|
{
|
|
true when form.HiddenAbilities.Count <= abilityIndex.Index => new AbilityIndex
|
|
{
|
|
IsHidden = true, Index = (byte)(form.HiddenAbilities.Count - 1),
|
|
},
|
|
false when form.Abilities.Count <= abilityIndex.Index => new AbilityIndex
|
|
{
|
|
IsHidden = false, Index = (byte)(form.Abilities.Count - 1),
|
|
},
|
|
_ => abilityIndex,
|
|
};
|
|
|
|
var oldHealth = FlatStats.Hp;
|
|
RecalculateFlatStats();
|
|
var diffHealth = (long)FlatStats.Hp - oldHealth;
|
|
if (diffHealth > 0)
|
|
{
|
|
Heal((uint)diffHealth, true);
|
|
}
|
|
|
|
// TODO: form specific moves?
|
|
|
|
eventHook?.Invoke(new FormChangeEvent(this, form)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
/// <remarks>
|
|
/// Currently this checks the Pokémon is not an egg and not fainted.
|
|
/// </remarks>
|
|
public bool IsUsable => !IsEgg && !IsFainted;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsFainted => CurrentHealth == 0;
|
|
|
|
/// <inheritdoc />
|
|
public void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false)
|
|
{
|
|
if (IsFainted)
|
|
return;
|
|
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;
|
|
CurrentHealth -= damage;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Faint(DamageSource source, EventBatchId batchId = default)
|
|
{
|
|
CurrentHealth = 0;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false,
|
|
EventHook? customEventHook = null)
|
|
{
|
|
if (IsFainted && !allowRevive)
|
|
return false;
|
|
|
|
var maxAmount = MaxHealth - CurrentHealth;
|
|
if (heal > maxAmount)
|
|
heal = maxAmount;
|
|
if (heal == 0)
|
|
return false;
|
|
|
|
var newHealth = CurrentHealth + heal;
|
|
customEventHook?.Invoke(new HealEvent(this, CurrentHealth, newHealth)
|
|
{
|
|
BatchId = batchId,
|
|
});
|
|
CurrentHealth = newHealth;
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RestoreAllPP()
|
|
{
|
|
foreach (var move in _learnedMoves)
|
|
{
|
|
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, IPokemon? originPokemon, 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;
|
|
|
|
StatusScript.Set(statusScript);
|
|
statusScript.OnAddedToParent(this);
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ClearStatus(EventBatchId batchId = default)
|
|
{
|
|
StatusScript.Clear();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ChangeLevelBy(int change)
|
|
{
|
|
var newLevel = Level + change;
|
|
Level = (LevelInt)Math.Clamp(newLevel, 1, Library.StaticLibrary.Settings.MaxLevel);
|
|
RecalculateFlatStats();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public SerializedPokemon Serialize() => new(this);
|
|
|
|
/// <inheritdoc />
|
|
public override int ScriptCount => 1;
|
|
|
|
/// <inheritdoc />
|
|
public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts)
|
|
{
|
|
scripts.Add(StatusScript);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts) => GetOwnScripts(scripts);
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
if (!string.IsNullOrEmpty(Nickname))
|
|
return $"{Nickname} ({Species.Name})";
|
|
return Species.Name;
|
|
}
|
|
} |