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> /// 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; set; } /// <summary> /// Sets the weight of the Pokémon in kilograms. Returns whether the weight was changed. /// </summary> /// <param name="weightInKg">The new weight in kilograms</param> /// <returns></returns> public bool ChangeWeightInKgBy(float weightInKg); /// <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; } 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> /// Makes the Pokemon uses its held item. Returns whether the item was consumed. /// </summary> bool ConsumeHeldItem(); /// <summary> /// Change a boosted stat by a certain amount. /// </summary> /// <param name="stat">The stat to be changed</param> /// <param name="change">The amount to change the stat by</param> /// <param name="selfInflicted">Whether the change was self-inflicted. This can be relevant in scripts.</param> bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted); /// <summary> /// Returns the currently active ability. /// </summary> IAbility ActiveAbility { get; } /// <summary> /// Calculates the flat stats on the Pokemon. This should be called when for example the base /// stats, level, nature, IV, or EV changes. This has a side effect of recalculating the boosted /// stats, as those depend on the flat stats. /// </summary> void RecalculateFlatStats(); /// <summary> /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats. /// This should be called when a stat boost changes. /// </summary> void RecalculateBoostedStats(); /// <summary> /// Change the species of the Pokemon. /// </summary> void ChangeSpecies(ISpecies species, IForm form); /// <summary> /// Change the form of the Pokemon. /// </summary> void ChangeForm(IForm form, 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); /// <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); /// <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> /// Adds a non-volatile status to the Pokemon. /// </summary> void SetStatus(StringKey status); /// <summary> /// Removes the current non-volatile status from the Pokemon. /// </summary> void ClearStatus(); /// <summary> /// Modifies the level by a certain amount /// </summary> void ChangeLevelBy(int change); /// <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> /// Marks a Pokemon as seen in the battle. /// </summary> void MarkOpponentAsSeen(IPokemon pokemon); /// <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; 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 itemName); } /// <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); WeightInKg = form.Weight; HeightInMeters = form.Height; Happiness = species.BaseHappiness; if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature)) throw new KeyNotFoundException($"Nature {natureName} not found."); Nature = nature; RecalculateFlatStats(); CurrentHealth = BoostedStats.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; WeightInKg = form.Weight; 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}."); _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 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); } } /// <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 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; set; } /// <inheritdoc /> public bool ChangeWeightInKgBy(float weightInKg) { if (WeightInKg <= 0.1f) return false; var newWeight = WeightInKg + weightInKg; if (newWeight <= 0.1f) newWeight = 0.1f; WeightInKg = newWeight; return true; } /// <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; } /// <inheritdoc /> public IReadOnlyList<TypeIdentifier> Types { get; private set; } /// <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; } = new ScriptSet(); /// <inheritdoc /> public bool HasHeldItem(StringKey itemName) => HeldItem?.Name == itemName; /// <inheritdoc /> public IItem? SetHeldItem(IItem? item) { var previous = HeldItem; HeldItem = item; return previous; } /// <inheritdoc /> public IItem? RemoveHeldItem() { var previous = HeldItem; HeldItem = null; return previous; } /// <inheritdoc /> public bool ConsumeHeldItem() { if (HeldItem is null) return false; if (!Library.ScriptResolver.TryResolveBattleItemScript(HeldItem, out _)) return false; // TODO: actually consume the item throw new NotImplementedException(); } /// <inheritdoc /> public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted) { 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)); } RecalculateBoostedStats(); return true; } /// <inheritdoc /> public IAbility ActiveAbility { get { 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) { if (form == Form) return; var oldAbility = Form.GetAbility(AbilityIndex); Form = form; Types = form.Types.ToList(); WeightInKg = form.Weight; 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); } 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) { // If the Pokémon is already fainted, we don't need to do anything. if (IsFainted) 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); } } 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)); // 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); } // Validate the battle state to see if the battle is over. BattleData.Battle.ValidateBattleState(); } /// <inheritdoc /> public bool Heal(uint heal, bool allowRevive) { if (IsFainted && !allowRevive) return false; var maxAmount = this.BoostedStats.Hp - CurrentHealth; if (heal > maxAmount) heal = maxAmount; if (heal == 0) return false; var newHealth = CurrentHealth + heal; BattleData?.Battle.EventHook.Invoke(new HealEvent(this, CurrentHealth, newHealth)); 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 void SetStatus(StringKey status) { if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript)) throw new KeyNotFoundException($"Status script {status} not found"); StatusScript.Set(statusScript); } /// <inheritdoc /> public void ClearStatus() => 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 void SetBattleData(IBattle battle, byte sideIndex) { if (BattleData is not null) { BattleData.Battle = battle; BattleData.SideIndex = sideIndex; } else { BattleData = new PokemonBattleDataImpl(battle, sideIndex); } } /// <inheritdoc /> public void SetOnBattlefield(bool onBattleField) { if (BattleData is not null) { BattleData.IsOnBattlefield = onBattleField; if (!onBattleField) { Volatile.Clear(); WeightInKg = Form.Weight; HeightInMeters = Form.Height; } } } /// <inheritdoc /> public void SetBattleSidePosition(byte position) { if (BattleData is not null) { BattleData.Position = position; } } /// <inheritdoc /> public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon); /// <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 class PokemonBattleDataImpl : IPokemonBattleData { /// <inheritdoc cref="PokemonBattleDataImpl"/> public PokemonBattleDataImpl(IBattle battle, byte sideIndex) { Battle = battle; SideIndex = sideIndex; } /// <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 itemName) { _consumedItems.Add(itemName); } }