using System.Diagnostics.CodeAnalysis; using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.Models.Serialized; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static; using PkmnLib.Static.Species; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.Models; /// /// A Pokémon taking part in a battle. This wraps an and holds all state that is only /// relevant for the duration of a single battle. The wrapper is ephemeral: it is created when the battle is /// created, and simply dropped when the battle ends, taking all battle-only state with it. Persistent data is /// proxied to the underlying . /// public interface IBattlePokemon : IPokemon { /// /// The persistent Pokémon this battle Pokémon wraps. /// IPokemon UnderlyingPokemon { get; } /// /// The battle the Pokémon is in. /// IBattle Battle { get; } /// /// The index of the side of the Pokémon. /// byte SideIndex { get; } /// /// The index of the position of the Pokémon on the field. /// byte Position { get; } /// /// The side the Pokémon is on. /// IBattleSide BattleSide { get; } /// /// Whether the Pokémon is on the battlefield. /// bool IsOnBattlefield { get; } /// /// A list of opponents the Pokémon has seen this battle. /// IReadOnlyList SeenOpponents { get; } /// /// Adds an opponent to the list of seen opponents. /// void MarkOpponentAsSeen(IBattlePokemon opponent); /// /// A list of items the Pokémon has consumed this battle. /// IReadOnlyList ConsumedItems { get; } /// /// Marks an item as consumed. /// void MarkItemAsConsumed(IItem item); /// /// The turn the Pokémon last switched in. /// uint SwitchInTurn { get; } /// /// The number of turns the Pokémon has been on the field. /// uint TurnsOnField { get; } /// /// The species of the Pokémon at the time the battle started. /// ISpecies OriginalSpecies { get; } /// /// The form of the Pokémon at the time the battle started. /// IForm OriginalForm { get; } /// /// The last move choice executed by the Pokémon. /// IMoveChoice? LastMoveChoice { get; set; } /// /// The statistics boosts of the Pokemon. Will prevent the value from going above 6, and below -6. /// StatBoostStatisticSet StatBoost { get; } /// /// The stats of the Pokemon including the stat boosts. /// StatisticSet BoostedStats { get; } /// /// Calculates the boosted stats on the Pokemon, _without_ recalculating the flat stats. /// This should be called when a stat boost changes. /// void RecalculateBoostedStats(); /// /// Change a boosted stat by a certain amount. /// /// The stat to be changed /// The amount to change the stat by /// Whether the change was self-inflicted. This can be relevant in scripts. /// Whether to skip the script hooks that can prevent or change the boost /// The event batch ID this change is a part of. This is relevant for visual handling bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default); /// /// The volatile status scripts of the Pokemon. /// IScriptSet Volatile { get; } /// /// The script for the ability. /// ScriptContainer AbilityScript { get; } /// /// The script for the held item. /// ScriptContainer HeldItemTriggerScript { get; } /// /// An ability can be overriden to an arbitrary ability. This is for example used for the Mummy /// ability. /// IAbility? OverrideAbility { get; } /// /// Changes the ability of the Pokémon. /// bool ChangeAbility(IAbility ability); /// /// Whether the ability of the Pokémon is suppressed. /// bool AbilitySuppressed { get; } /// /// Suppresses the ability of the Pokémon. /// bool SuppressAbility(); /// /// Returns the currently active ability, taking suppression and overrides into account. /// IAbility? ActiveAbility { get; } /// /// 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. /// ISpecies? DisplaySpecies { get; } /// /// 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. /// IForm? DisplayForm { get; } /// /// Sets the display species and form of the Pokemon. This is used for abilities like Illusion. /// void SetDisplaySpecies(ISpecies? species, IForm? form); /// /// The height of the Pokémon in meters. This can be changed during battle by effects such as Autotomize. /// new float HeightInMeters { get; set; } /// /// Removes a type from the Pokémon. Returns whether the type was removed. /// bool RemoveType(TypeIdentifier type); /// /// 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. /// bool AddType(TypeIdentifier type); /// /// Replace the types of the Pokémon with the provided types. /// void SetTypes(IReadOnlyList types); /// /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as /// Levitate. /// bool IsFloating { get; } /// /// The permanently learned moves of the Pokemon, ignoring any temporary replacements made through /// . This is of a set length of . Empty move /// slots are null. /// IReadOnlyList BaseMoves { get; } /// /// Temporarily replaces the move in the given slot until the Pokemon leaves the battlefield. The permanently /// learned move in the slot is left untouched and becomes visible again automatically. Used by effects such as /// Mimic. /// /// Thrown when the move is not found in the move library. void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index); /// /// Whether or not this Pokemon was caught this battle. /// bool IsCaught { get; } /// /// Marks the Pokemon as caught. This makes it so that the Pokemon is not considered valid in battle anymore. /// void MarkAsCaught(); /// /// Whether the held item has been removed for the duration of the battle. /// bool HasItemBeenRemovedForBattle { get; } /// /// Removes the held item from the Pokemon for the duration of the battle. Returns the previously held item. /// /// /// This is used for moves that remove a held item, but do not consume it. The held item of the underlying /// Pokémon is never touched; the removal simply ends with the battle. /// IItem? RemoveHeldItemForBattle(); /// /// 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. /// bool TryStealHeldItem([NotNullWhen(true)] out IItem? item); /// /// Restores the held item of a Pokémon if it was temporarily removed. /// void RestoreRemovedHeldItem(); /// /// Makes the Pokemon uses its held item. Returns whether the item was consumed. /// bool ConsumeHeldItem(); /// /// Called by the battle when the Pokémon is sent onto the battlefield. This should not be called by /// user code; use instead. /// void OnSwitchedIn(byte position); /// /// Called by the battle when the Pokémon leaves the battlefield. Resets all state that only lasts while /// the Pokémon is on the field. This should not be called by user code. /// void OnSwitchedOut(); /// /// Sets the position the Pokémon has within its side, without running switch handling. /// void SetPosition(byte position); /// /// Called when the battle has ended. Reverts the few battle effects that write through to the underlying /// Pokémon, such as battle-only forms. /// void OnBattleEnd(); } /// public class BattlePokemonImpl : ScriptSource, IBattlePokemon { private readonly IPokemon _pokemon; /// public BattlePokemonImpl(IPokemon pokemon, IBattle battle, byte sideIndex) { _pokemon = pokemon; Battle = battle; SideIndex = sideIndex; OriginalSpecies = pokemon.Species; OriginalForm = pokemon.Form; Volatile = new ScriptSet(this); _types = pokemon.Types.ToList(); HeightInMeters = pokemon.HeightInMeters; _heldItem = pokemon.HeldItem; RecalculateBoostedStats(); // A status that was set outside of the battle is parented to the underlying Pokémon. Re-parent it to // this wrapper, so that it has battle context for the duration of the battle. pokemon.StatusScript.Script?.OnAddedToParent(this); } /// public IPokemon UnderlyingPokemon => _pokemon; /// public IBattle Battle { get; } /// public byte SideIndex { get; } /// public byte Position { get; private set; } /// public IBattleSide BattleSide => Battle.Sides[SideIndex]; /// public bool IsOnBattlefield { get; private set; } private readonly List _seenOpponents = []; /// public IReadOnlyList SeenOpponents => _seenOpponents; /// public void MarkOpponentAsSeen(IBattlePokemon opponent) { if (!_seenOpponents.Contains(opponent)) _seenOpponents.Add(opponent); } private readonly List _consumedItems = []; /// public IReadOnlyList ConsumedItems => _consumedItems; /// public void MarkItemAsConsumed(IItem item) { _consumedItems.Add(item); BattleSide.SetConsumedItem(Position, item); } /// public uint SwitchInTurn { get; private set; } /// public uint TurnsOnField => Battle.CurrentTurnNumber - SwitchInTurn; /// public ISpecies OriginalSpecies { get; } /// public IForm OriginalForm { get; } /// public IMoveChoice? LastMoveChoice { get; set; } /// public void OnSwitchedIn(byte position) { Position = position; SwitchInTurn = Battle.CurrentTurnNumber; IsOnBattlefield = true; ResolveAbilityScript(); } /// public void OnSwitchedOut() { IsOnBattlefield = false; Volatile.Clear(); _temporaryMoves = null; HeightInMeters = _pokemon.Form.Height; _types = _pokemon.Form.Types.ToList(); OverrideAbility = null; AbilitySuppressed = false; StatBoost.Reset(); RecalculateBoostedStats(); } /// public void SetPosition(byte position) => Position = position; /// public void OnBattleEnd() { if (_pokemon.Form.IsBattleOnlyForm) { _pokemon.ChangeForm(OriginalSpecies == _pokemon.Species ? OriginalForm : _pokemon.Species.GetDefaultForm()); } } private void ResolveAbilityScript() { var ability = ActiveAbility; if (ability != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters, out var abilityScript)) { AbilityScript.Set(abilityScript); abilityScript.OnAddedToParent(this); } else { AbilityScript.Clear(); } } /// public IDynamicLibrary Library => _pokemon.Library; /// public ISpecies Species => _pokemon.Species; /// public IForm Form => _pokemon.Form; /// public LevelInt Level => _pokemon.Level; /// public uint Experience => _pokemon.Experience; /// public uint PersonalityValue => _pokemon.PersonalityValue; /// public Gender Gender => _pokemon.Gender; /// public byte Coloring => _pokemon.Coloring; /// public bool IsShiny => _pokemon.IsShiny; /// public uint CurrentHealth => _pokemon.CurrentHealth; /// public byte Happiness { get => _pokemon.Happiness; set => _pokemon.Happiness = value; } /// public StatisticSet FlatStats => _pokemon.FlatStats; /// public IndividualValueStatisticSet IndividualValues => _pokemon.IndividualValues; /// public EffortValueStatisticSet EffortValues => _pokemon.EffortValues; /// public INature Nature => _pokemon.Nature; /// public string? Nickname => _pokemon.Nickname; /// public AbilityIndex AbilityIndex => _pokemon.AbilityIndex; /// public IAbility Ability => _pokemon.Ability; /// public bool AllowedExperience => _pokemon.AllowedExperience; /// public bool IsEgg => _pokemon.IsEgg; /// public ScriptContainer StatusScript => _pokemon.StatusScript; /// public int? GetStatusTurnsLeft => _pokemon.GetStatusTurnsLeft; /// public bool HasStatus(StringKey status) => _pokemon.HasStatus(status); /// public bool IsFainted => _pokemon.IsFainted; /// public SerializedPokemon Serialize() => _pokemon.Serialize(); /// public bool AddExperience(uint experience, EventHook? eventHook = null) => _pokemon.AddExperience(experience, eventHook ?? Battle.EventHook); /// public void RestoreAllPP() { foreach (var move in Moves) { move?.RestoreAllUses(); } } /// public void LearnMove(StringKey moveName, MoveLearnMethod method, byte index) => _pokemon.LearnMove(moveName, method, index); /// public void ChangeLevelBy(int change) { _pokemon.ChangeLevelBy(change); RecalculateBoostedStats(); } /// public bool EvolveTo(IEvolution evolution, EventHook? eventHook = null) => _pokemon.EvolveTo(evolution, eventHook ?? Battle.EventHook); /// public StatBoostStatisticSet StatBoost { get; } = new(); /// public StatisticSet BoostedStats { get; } = new(); /// public uint MaxHealth => BoostedStats.Hp; /// public void RecalculateFlatStats() { _pokemon.RecalculateFlatStats(); RecalculateBoostedStats(); } /// public void RecalculateBoostedStats() => Library.StatCalculator.CalculateBoostedStats(this, BoostedStats); /// public bool ChangeStatBoost(Statistic stat, sbyte change, bool selfInflicted, bool force, EventBatchId batchId = default) { if (!force) { var prevented = false; this.RunScriptHook(script => script.PreventStatBoostChange(this, stat, change, selfInflicted, ref prevented)); if (prevented) return false; this.RunScriptHook(script => script.ChangeStatBoostChange(this, stat, selfInflicted, ref change)); if (change == 0) return false; } var changed = false; var oldBoost = StatBoost.GetStatistic(stat); changed = change switch { > 0 => StatBoost.IncreaseStatistic(stat, change), < 0 => StatBoost.DecreaseStatistic(stat, (sbyte)-change), _ => changed, }; if (!changed) return false; var newBoost = StatBoost.GetStatistic(stat); Battle.EventHook.Invoke(new StatBoostEvent(this, stat, oldBoost, newBoost) { BatchId = batchId, }); RecalculateBoostedStats(); this.RunScriptHook(script => script.OnAfterStatBoostChange(this, stat, selfInflicted, change)); return true; } /// public ISpecies? DisplaySpecies { get; private set; } /// public IForm? DisplayForm { get; private set; } /// public void SetDisplaySpecies(ISpecies? species, IForm? form) { DisplaySpecies = species; DisplayForm = form; Battle.EventHook.Invoke(new DisplaySpeciesChangeEvent(this, species, form) { BatchId = new EventBatchId(), }); } /// public void ChangeSpecies(ISpecies species, IForm form, EventHook? eventHook = null, IRandom? random = null) { var oldAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex); _pokemon.ChangeSpecies(species, form, eventHook ?? Battle.EventHook, random ?? Battle.Random); OnAfterFormWriteThrough(oldAbilityName); } /// public void ChangeForm(IForm form, EventBatchId batchId = default, EventHook? eventHook = null) { if (form == Form) return; var oldAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex); _pokemon.ChangeForm(form, batchId, eventHook ?? Battle.EventHook); OnAfterFormWriteThrough(oldAbilityName); } /// /// A form or species change writes through to the underlying Pokémon; the battle overlays that derive /// from the form need to be re-initialized from the new form. /// private void OnAfterFormWriteThrough(StringKey oldAbilityName) { _types = _pokemon.Form.Types.ToList(); HeightInMeters = _pokemon.Form.Height; var newAbilityName = _pokemon.Form.GetAbility(_pokemon.AbilityIndex); if (OverrideAbility == null && !AbilitySuppressed && oldAbilityName != newAbilityName) ResolveAbilityScript(); RecalculateBoostedStats(); } private List _types; /// public IReadOnlyList Types => _types; /// public bool RemoveType(TypeIdentifier type) => _types.Remove(type); /// public bool AddType(TypeIdentifier type) { if (_types.Contains(type)) return false; _types.Add(type); return true; } /// public void SetTypes(IReadOnlyList types) => _types = types.ToList(); /// public float HeightInMeters { get; set; } /// public float WeightInKg { get { var weight = _pokemon.WeightInKg; // ReSharper disable once AccessToModifiedClosure this.RunScriptHook(script => script.ModifyWeight(ref weight)); if (weight < 0.1f) weight = 0.1f; return weight; } } private static readonly StringKey FlyingTypeName = "flying"; /// public bool IsFloating { get { var isFloating = Types.Any(x => x.Name == FlyingTypeName); this.RunScriptHook(x => x.IsFloating(this, ref isFloating)); return isFloating; } } /// public IAbility? OverrideAbility { get; private set; } /// public bool AbilitySuppressed { get; private set; } /// public IAbility? ActiveAbility { get { if (AbilitySuppressed) return null; if (OverrideAbility != null) return OverrideAbility; return _pokemon.Ability; } } /// public bool SuppressAbility() { if (ActiveAbility?.CanBeChanged == false) return false; AbilitySuppressed = true; AbilityScript.Clear(); return true; } /// 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; } /// /// Battle-only per-slot overrides of the underlying moveset. The permanent moveset is never mutated by /// temporary moves; discarding this array is all that is needed to restore the original moves. /// private ILearnedMove?[]? _temporaryMoves; /// public IReadOnlyList Moves => _temporaryMoves?.Select((move, index) => move ?? BaseMoves[index]).ToArray() ?? BaseMoves; /// public IReadOnlyList BaseMoves => _pokemon.Moves; /// public bool HasMove(StringKey moveName) => Moves.Any(move => move?.MoveData.Name == moveName); /// public void SwapMoves(byte index1, byte index2) { if (index1 >= Const.MovesCount || index2 >= Const.MovesCount) return; _pokemon.SwapMoves(index1, index2); if (_temporaryMoves != null) (_temporaryMoves[index1], _temporaryMoves[index2]) = (_temporaryMoves[index2], _temporaryMoves[index1]); } /// public void LearnTemporaryMove(StringKey moveName, MoveLearnMethod method, byte index) { if (index >= Const.MovesCount) throw new ArgumentOutOfRangeException(nameof(index), $"Move slot {index} is out of range."); if (!Library.StaticLibrary.Moves.TryGet(moveName, out var move)) throw new KeyNotFoundException($"Move {moveName} not found."); _temporaryMoves ??= new ILearnedMove?[Const.MovesCount]; _temporaryMoves[index] = new LearnedMoveImpl(move, method); } private IItem? _heldItem; /// public IItem? HeldItem => _heldItem; /// public bool HasHeldItem(StringKey itemName) => _heldItem?.Name == itemName; /// public IItem? ForceSetHeldItem(IItem? item) { var previous = _heldItem; _heldItem = item; this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item)); return previous; } /// public IItem? RemoveHeldItem() { if (_heldItem is not null && _heldItem.Category == ItemCategory.FormChanger) return null; var previous = _heldItem; _heldItem = null; this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null)); return previous; } private IItem? _removedHeldItem; /// public bool HasItemBeenRemovedForBattle => _removedHeldItem is not null; /// public IItem? RemoveHeldItemForBattle() => _removedHeldItem = RemoveHeldItem(); /// public bool TryStealHeldItem([NotNullWhen(true)] out IItem? item) { if (_heldItem is null || _heldItem.Category == ItemCategory.FormChanger) { item = null; return false; } var prevent = false; this.RunScriptHook(script => script.PreventHeldItemSteal(this, _heldItem, ref prevent)); if (prevent) { item = null; return false; } item = RemoveHeldItemForBattle(); return item is not null; } /// public void RestoreRemovedHeldItem() { _ = ForceSetHeldItem(_removedHeldItem); _removedHeldItem = null; } /// public bool ConsumeHeldItem() { if (_heldItem is null) return false; if (!Library.ScriptResolver.TryResolveBattleItemScript(_heldItem, out _)) return false; var prevented = false; this.RunScriptHook(script => script.PreventHeldItemConsume(this, _heldItem, ref prevented)); if (prevented) return false; MarkItemAsConsumed(_heldItem); UseItem(ForceSetHeldItem(null)!); return true; } /// public void UseItem(IItem item) { // TODO: actually consume the item this.RunScriptHook(x => x.OnAfterItemConsume(this, item)); } /// public bool IsCaught { get; private set; } /// public void MarkAsCaught() { IsCaught = true; } /// public bool IsUsable => !IsCaught && _pokemon.IsUsable; /// public void Damage(uint damage, DamageSource source, EventBatchId batchId = default, bool forceDamage = false) { if (IsFainted) return; if (!forceDamage) { var dmg = damage; this.RunScriptHook(script => script.ChangeIncomingDamage(this, source, ref dmg)); damage = dmg; } if (damage == 0) return; // If the damage is more than the current health, we cap it at the current health, to prevent // underflow. if (damage >= CurrentHealth) damage = CurrentHealth; var newHealth = CurrentHealth - damage; // Trigger an event to the front-end. 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)); _pokemon.Damage(damage, source, batchId, true); // If the Pokémon is now fainted, we also run faint handling. if (IsFainted) { OnFaint(source); } } /// public void Faint(DamageSource source, EventBatchId batchId = default) { _pokemon.Faint(source, batchId); OnFaint(source); } private void OnFaint(DamageSource source) { // Trigger the faint event to the front-end. Battle.EventHook.Invoke(new FaintEvent(this)); // Allow scripts to trigger based on the faint. this.RunScriptHook(script => script.OnFaint(this, source)); foreach (var ally in BattleSide.Pokemon.WhereNotNull().Where(x => x != this)) { ally.RunScriptHook(script => script.OnAllyFaint(ally, this)); } // Make sure the OnRemove script is run. this.RunScriptHook(script => script.OnRemove()); // Mark the position as unfillable if it can't be filled by any party. if (!Battle.CanSlotBeFilled(SideIndex, Position)) { BattleSide.MarkPositionAsUnfillable(Position); } BattleSide.MarkFaint(Position); BattleSide.ForceClearPokemonFromField(Position); foreach (var opponent in SeenOpponents) { if (!opponent.IsUsable) continue; if (!opponent.AllowedExperience) continue; opponent.AddExperience(Library.ExperienceGainCalculator.CalculateExperienceGain(this, opponent)); } // Validate the battle state to see if the battle is over. Battle.ValidateBattleState(); } /// public bool Heal(uint heal, bool allowRevive = false, EventBatchId batchId = default, bool forceHeal = false, EventHook? customEventHook = null) { if (IsFainted && !allowRevive) return false; var maxAmount = BoostedStats.Hp - CurrentHealth; if (heal > maxAmount) heal = maxAmount; if (heal == 0) return false; if (!forceHeal) { var prevented = false; this.RunScriptHook(x => x.PreventHeal(this, heal, allowRevive, ref prevented)); if (prevented) return false; } customEventHook ??= Battle.EventHook; return _pokemon.Heal(heal, allowRevive, batchId, true, customEventHook); } /// 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 || (originPokemon is IBattlePokemon origin && origin.UnderlyingPokemon == _pokemon); var preventStatus = false; this.RunScriptHook(script => script.PreventStatusChange(this, status, selfInflicted, ref preventStatus)); if (preventStatus) return false; StatusScript.Set(statusScript); statusScript.OnAddedToParent(this); Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status) { BatchId = batchId, }); this.RunScriptHook(script => script.OnAfterStatusChange(this, status, originPokemon)); return true; } /// public void ClearStatus(EventBatchId batchId = default) { var oldStatus = StatusScript.Script?.Name; _pokemon.ClearStatus(batchId); Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, null) { BatchId = batchId, }); } /// public ScriptContainer HeldItemTriggerScript { get; } = new(); /// public ScriptContainer AbilityScript { get; } = new(); /// public IScriptSet Volatile { get; } /// public override int ScriptCount => 4 + BattleSide.ScriptCount; /// public override void GetOwnScripts(List> scripts) { scripts.Add(HeldItemTriggerScript); scripts.Add(AbilityScript); scripts.Add(StatusScript); scripts.Add(Volatile); } /// public override void CollectScripts(List> scripts) { GetOwnScripts(scripts); BattleSide.CollectScripts(scripts); } /// public override string ToString() => _pokemon.ToString()!; }