2025-07-11 17:03:08 +02:00

1501 lines
45 KiB
C#

using System.Diagnostics.CodeAnalysis;
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 number of turns left for the current non-volatile status.
/// </summary>
int? GetStatusTurnsLeft { 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? ForceSetHeldItem(IItem? item);
/// <summary>
/// Removes the held item from the Pokemon. Returns the previously held item.
/// </summary>
[MustUseReturnValue]
IItem? RemoveHeldItem();
bool HasItemBeenRemovedForBattle { get; }
/// <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 RestoreRemovedHeldItem();
/// <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, 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>
/// 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<IScriptModifyWeight>(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 int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft();
/// <inheritdoc />
public IScriptSet Volatile { get; }
/// <inheritdoc />
public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName;
/// <inheritdoc />
public IItem? ForceSetHeldItem(IItem? item)
{
var previous = HeldItem;
HeldItem = item;
this.RunScriptHook<IScriptOnAfterHeldItemChange>(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<IScriptOnAfterHeldItemChange>(x => x.OnAfterHeldItemChange(this, previous, null));
return previous;
}
/// <inheritdoc />
public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null;
private IItem? _removedHeldItem;
/// <inheritdoc />
public IItem? RemoveHeldItemForBattle()
{
return _removedHeldItem = 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<IScriptPreventHeldItemSteal>(script =>
script.PreventHeldItemSteal(this, HeldItem, ref prevent));
if (prevent)
{
item = null;
return false;
}
item = RemoveHeldItemForBattle();
return item is not null;
}
/// <inheritdoc />
public void RestoreRemovedHeldItem()
{
_ = ForceSetHeldItem(_removedHeldItem);
_removedHeldItem = 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<IScriptPreventHeldItemConsume>(script =>
script.PreventHeldItemConsume(this, HeldItem, ref prevented));
if (prevented)
return false;
BattleData.MarkItemAsConsumed(HeldItem);
}
UseItem(ForceSetHeldItem(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<IScriptOnAfterItemConsume>(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<IScriptPreventStatBoostChange>(script =>
script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented));
if (prevented)
return false;
this.RunScriptHook<IScriptChangeStatBoostChange>(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, (sbyte)-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<IScriptOnAfterStatBoostChange>(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();
}
private (IAbility, AbilityIndex)? _abilityCache;
/// <inheritdoc />
public IAbility? ActiveAbility
{
get
{
if (AbilitySuppressed)
return null;
if (OverrideAbility != null)
return OverrideAbility;
if (_abilityCache is not null && _abilityCache.Value.Item2 == AbilityIndex)
return _abilityCache.Value.Item1;
var ability = Form.GetAbility(AbilityIndex);
if (!Library.StaticLibrary.Abilities.TryGet(ability, out var abilityObj))
throw new KeyNotFoundException($"Ability {ability} not found.");
_abilityCache = (abilityObj, AbilityIndex);
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 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 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<IScriptChangeIncomingDamage>(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<IScriptOnDamage>(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<IScriptOnFaint>(script => script.OnFaint(this, source));
foreach (var ally in BattleData.BattleSide.Pokemon.WhereNotNull().Where(x => x != this))
{
ally.RunScriptHook<IScriptOnAllyFaint>(script => script.OnAllyFaint(ally, this));
}
// Make sure the OnRemove script is run.
this.RunScriptHook<IScriptOnRemove>(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);
BattleData.BattleSide.ForceClearPokemonFromField(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<IScriptPreventHeal>(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, 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;
var oldStatus = StatusScript.Script?.Name;
var selfInflicted = originPokemon == this;
var preventStatus = false;
this.RunScriptHook<IScriptPreventStatusChange>(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,
});
this.RunScriptHook<IScriptOnAfterStatusChange>(script =>
script.OnAfterStatusChange(this, status, originPokemon));
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<IScriptIsFloating>(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; }
}