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; /// /// 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 instead. /// public interface IPokemon : IScriptSource, IDeepCloneable { /// /// The library data of the Pokemon. /// IDynamicLibrary Library { get; } /// /// The species of the Pokemon. /// ISpecies Species { get; } /// /// The form of the Pokemon. /// IForm Form { get; } /// /// The current level of the Pokemon. /// LevelInt Level { get; } /// /// The amount of experience of the Pokemon. /// uint Experience { get; } /// /// 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. /// bool AddExperience(uint experience, EventHook? eventHook = null); /// /// The personality value of the Pokemon. /// uint PersonalityValue { get; } /// /// The gender of the Pokemon. /// Gender Gender { get; } /// /// 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. /// byte Coloring { get; } /// /// Whether the Pokemon is shiny. /// bool IsShiny { get; } /// /// The held item of the Pokemon. /// IItem? HeldItem { get; } /// /// The remaining health points of the Pokemon. /// uint CurrentHealth { get; } /// /// The weight of the Pokemon in kilograms. /// float WeightInKg { get; } /// /// The height of the Pokémon in meters. /// float HeightInMeters { get; } /// /// The happiness of the Pokemon. Also known as friendship. /// byte Happiness { get; set; } /// /// The stats of the Pokemon when disregarding any stat boosts. /// StatisticSet FlatStats { get; } /// /// The maximum health of the Pokemon. /// uint MaxHealth { get; } /// /// The individual values of the Pokemon. /// IndividualValueStatisticSet IndividualValues { get; } /// /// The effort values of the Pokemon. /// EffortValueStatisticSet EffortValues { get; } /// /// The nature of the Pokemon. /// INature Nature { get; } /// /// An optional nickname of the Pokemon. /// string? Nickname { get; } /// /// An index of the ability to find the actual ability on the form. /// AbilityIndex AbilityIndex { get; } /// /// The ability of the Pokemon, as determined by its form and ability index. /// IAbility Ability { get; } /// /// The moves the Pokemon has learned. This is of a set length of . Empty move /// slots are null. /// IReadOnlyList Moves { get; } /// /// Checks whether the Pokemon has a specific move in its moveset. /// bool HasMove(StringKey moveName); /// /// Swaps two moves of the Pokemon. /// void SwapMoves(byte index1, byte index2); /// /// Whether or not the Pokemon is allowed to gain experience. /// bool AllowedExperience { get; } /// /// The types of the Pokemon, as determined by its form. /// IReadOnlyList Types { get; } /// /// Whether or not this Pokemon is an egg. /// bool IsEgg { get; } /// /// The script for the status. /// ScriptContainer StatusScript { get; } /// /// The number of turns left for the current non-volatile status. /// int? GetStatusTurnsLeft { get; } /// /// Checks whether the Pokemon is holding an item with a specific name. /// bool HasHeldItem(StringKey itemName); /// /// Changes the held item of the Pokemon. Returns the previously held item. /// [MustUseReturnValue] IItem? ForceSetHeldItem(IItem? item); /// /// Removes the held item from the Pokemon. Returns the previously held item. /// [MustUseReturnValue] IItem? RemoveHeldItem(); /// /// Uses an item on the Pokemon. /// void UseItem(IItem item); /// /// Calculates the flat stats on the Pokemon. This should be called when for example the base /// stats, level, nature, IV, or EV changes. /// void RecalculateFlatStats(); /// /// 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. /// bool EvolveTo(IEvolution evolution, EventHook? eventHook = null); /// /// 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. /// void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null); /// /// Change the form of the Pokemon. /// void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null); /// /// Whether the Pokemon is useable in a battle. /// bool IsUsable { get; } /// /// Whether the Pokemon is fainted. /// bool IsFainted { get; } /// /// 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 /// . /// void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false); /// /// Sets the health of the Pokémon to 0. /// void Faint(DamageSource source, EventBatchId batchId = default); /// /// 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. /// bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false, EventHook? customEventHook = null); /// /// Restores all PP of the Pokemon. /// void RestoreAllPP(); /// /// Learn a move by name. /// void LearnMove(StringKey moveName, MoveLearnMethod method, byte index); /// /// Checks whether the Pokémon has a specific non-volatile status. /// bool HasStatus(StringKey status); /// /// 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 . /// bool SetStatus(StringKey status, IPokemon? originPokemon, EventBatchId batchId = default); /// /// Removes the current non-volatile status from the Pokemon. /// void ClearStatus(EventBatchId batchId = default); /// /// Modifies the level by a certain amount /// void ChangeLevelBy(int change); /// /// Converts the data structure to a serializable format. /// SerializedPokemon Serialize(); } /// public class PokemonImpl : ScriptSource, IPokemon { /// 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; } /// 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); } } /// public IDynamicLibrary Library { get; } /// public ISpecies Species { get; private set; } /// public IForm Form { get; private set; } /// public LevelInt Level { get; private set; } /// public uint Experience { get; private set; } /// 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; } /// public uint PersonalityValue { get; } /// public Gender Gender { get; private set; } /// public byte Coloring { get; } /// public bool IsShiny => Coloring == 1; /// public IItem? HeldItem { get; private set; } /// public uint CurrentHealth { get; private set; } /// public float WeightInKg { get { var weight = Form.Weight; if (weight < 0.1f) weight = 0.1f; return weight; } } /// public float HeightInMeters => Form.Height; /// public byte Happiness { get; set; } /// public StatisticSet FlatStats { get; } = new(); /// public uint MaxHealth => FlatStats.Hp; /// public IndividualValueStatisticSet IndividualValues { get; } = new(); /// public EffortValueStatisticSet EffortValues { get; } = new(); /// public INature Nature { get; } /// public string? Nickname { get; set; } /// public AbilityIndex AbilityIndex { get; } private (IAbility Ability, IForm Form, AbilityIndex Index)? _abilityCache; /// 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]; /// public IReadOnlyList Moves => _learnedMoves; /// public bool HasMove(StringKey moveName) => _learnedMoves.Any(move => move?.MoveData.Name == moveName); /// public void SwapMoves(byte index1, byte index2) { if (index1 >= Const.MovesCount || index2 >= Const.MovesCount) return; (_learnedMoves[index1], _learnedMoves[index2]) = (_learnedMoves[index2], _learnedMoves[index1]); } /// public bool AllowedExperience { get; set; } /// public IReadOnlyList Types => Form.Types; /// public bool IsEgg { get; private set; } /// public ScriptContainer StatusScript { get; } = new(); /// public int? GetStatusTurnsLeft => (StatusScript.Script as IAIInfoScriptNumberTurnsLeft)?.TurnsLeft(); /// public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName; /// public IItem? ForceSetHeldItem(IItem? item) { var previous = HeldItem; HeldItem = item; return previous; } /// public IItem? RemoveHeldItem() { if (HeldItem is not null) { if (HeldItem.Category == ItemCategory.FormChanger) { return null; } } var previous = HeldItem; HeldItem = null; return previous; } /// public void UseItem(IItem item) { // TODO: actually consume the item } /// public void RecalculateFlatStats() => Library.StatCalculator.CalculateFlatStats(this, FlatStats); /// 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; } /// 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); } /// 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, }); } /// /// /// Currently this checks the Pokémon is not an egg and not fainted. /// public bool IsUsable => !IsEgg && !IsFainted; /// public bool IsFainted => CurrentHealth == 0; /// 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; } /// public void Faint(DamageSource source, EventBatchId batchId = default) { CurrentHealth = 0; } /// 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; } /// public void RestoreAllPP() { foreach (var move in _learnedMoves) { move?.RestoreAllUses(); } } /// /// /// If the index is 255, it will try to find the first empty move slot. /// 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); } /// public bool HasStatus(StringKey status) => StatusScript.Script?.Name == status; /// 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; } /// public void ClearStatus(EventBatchId batchId = default) { StatusScript.Clear(); } /// public void ChangeLevelBy(int change) { var newLevel = Level + change; Level = (LevelInt)Math.Clamp(newLevel, 1, Library.StaticLibrary.Settings.MaxLevel); RecalculateFlatStats(); } /// public SerializedPokemon Serialize() => new(this); /// public override int ScriptCount => 1; /// public override void GetOwnScripts(List> scripts) { scripts.Add(StatusScript); } /// public override void CollectScripts(List> scripts) => GetOwnScripts(scripts); /// public override string ToString() { if (!string.IsNullOrEmpty(Nickname)) return $"{Nickname} ({Species.Name})"; return Species.Name; } }