using PkmnLib.Dynamic.Events; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Static.Moves; using PkmnLib.Static.Utils; namespace PkmnLib.Dynamic.Models.BattleFlow; internal static class MoveTurnExecutor { internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice) { var chosenMove = moveChoice.ChosenMove; var moveData = chosenMove.MoveData; var moveDataName = moveData.Name; moveChoice.RunScriptHook(x => x.ChangeMove(moveChoice, ref moveDataName)); if (moveData.Name != moveDataName) { if (!battle.Library.StaticLibrary.Moves.TryGet(moveDataName, out moveData)) { throw new InvalidOperationException( $"The move was changed to '{moveDataName}' by a script, but this move does not exist."); } // FIXME: Change the script on the move when it is changed. } var targetType = moveData.Target; var targets = TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType); byte numberOfHits = 1; moveChoice.RunScriptHook(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits)); if (numberOfHits == 0) { return; } var executingMove = new ExecutingMoveImpl(targets, numberOfHits, moveChoice.User, chosenMove, moveData, moveChoice.Script); var prevented = false; executingMove.RunScriptHook(x => x.PreventMove(executingMove, ref prevented)); if (prevented) return; byte ppUsed = 1; // TODO: Modify the PP used by the move. if (!executingMove.ChosenMove.TryUse(ppUsed)) return; battle.EventHook.Invoke(new MoveUseEvent(executingMove)); var failed = false; executingMove.RunScriptHook(x => x.FailMove(executingMove, ref failed)); if (failed) { // TODO: fail handling return; } var stopped = false; executingMove.RunScriptHook(x => x.StopBeforeMove(executingMove, ref stopped)); if (stopped) return; executingMove.RunScriptHook(x => x.OnBeforeMove(executingMove)); foreach (var target in targets.WhereNotNull()) { ExecuteMoveChoiceForTarget(battle, executingMove, target); } } private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IPokemon target) { var failed = false; target.RunScriptHook(x => x.FailIncomingMove(executingMove, target, ref failed)); if (failed) { // TODO: fail handling return; } var isInvulnerable = false; target.RunScriptHook(x => x.IsInvulnerableToMove(executingMove, target, ref isInvulnerable)); if (isInvulnerable) { // TODO: event? return; } var numberOfHits = executingMove.NumberOfHits; var targetHitStat = executingMove.GetTargetIndex(target) * numberOfHits; for (byte i = 0; i < numberOfHits; i++) { if (battle.HasEnded) break; if (executingMove.User.IsFainted) break; if (target.IsFainted) break; var hitIndex = i; var useMove = executingMove.UseMove; var hitType = useMove.MoveType; executingMove.RunScriptHook(x => x.ChangeMoveType(executingMove, target, hitIndex, ref hitType)); var hitData = (HitData)executingMove.GetDataFromRawIndex(targetHitStat + i); hitData.Type = hitType; var effectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(hitType, target.Types); executingMove.RunScriptHook(x => x.ChangeEffectiveness(executingMove, target, hitIndex, ref effectiveness)); hitData.Effectiveness = effectiveness; var blockCritical = false; executingMove.RunScriptHook(x => x.BlockCriticalHit(executingMove, target, hitIndex, ref blockCritical)); target.RunScriptHook(x => x.BlockIncomingCriticalHit(executingMove, target, hitIndex, ref blockCritical)); if (!blockCritical) { var critical = battle.Library.DamageCalculator.IsCritical(battle, executingMove, target, hitIndex); hitData.IsCritical = critical; } var basePower = battle.Library.DamageCalculator.GetBasePower(executingMove, target, hitIndex, hitData); hitData.BasePower = basePower; hitData.Damage = battle.Library.DamageCalculator.GetDamage(executingMove, target, hitIndex, hitData); var accuracy = useMove.Accuracy; // If the accuracy is 255, the move should always hit, and as such we should not allow // modifying it. if (accuracy != 255) { executingMove.RunScriptHook(x => x.ChangeAccuracy(executingMove, target, hitIndex, ref accuracy)); } // TODO: Deal with accuracy/evasion stats. if (accuracy < 100 && battle.Random.GetInt(100) >= accuracy) { executingMove.RunScriptHook(x => x.OnMoveMiss(executingMove, target)); battle.EventHook.Invoke(new MoveMissEvent(executingMove)); break; } if (useMove.Category == MoveCategory.Status) { var secondaryEffect = useMove.SecondaryEffect; if (secondaryEffect != null) { var chance = secondaryEffect.Chance; if (chance < 0 || battle.Random.EffectChance(chance, executingMove, target, hitIndex)) { executingMove.RunScriptHook(x => x.OnSecondaryEffect(executingMove, target, hitIndex)); } } } // else if the move is a physical or special move, we should apply the damage. else { var currentHealth = target.CurrentHealth; if (hitData.Damage > currentHealth) { hitData.Damage = currentHealth; } var damage = hitData.Damage; if (damage > 0) { var hitEventBatch = new EventBatchId(); battle.EventHook.Invoke(new MoveHitEvent(executingMove, hitData, target) { BatchId = hitEventBatch, }); target.Damage(damage, DamageSource.MoveDamage, hitEventBatch); if (!target.IsFainted) target.RunScriptHook(x => x.OnIncomingHit(executingMove, target, hitIndex)); else executingMove.RunScriptHook(x => x.OnOpponentFaints(executingMove, target, hitIndex)); if (!target.IsFainted) { var secondaryEffect = useMove.SecondaryEffect; if (secondaryEffect != null) { var preventSecondary = false; target.RunScriptHook(x => x.PreventSecondaryEffect(executingMove, target, hitIndex, ref preventSecondary)); if (!preventSecondary) { var chance = secondaryEffect.Chance; if (chance < 0 || battle.Random.EffectChance(chance, executingMove, target, hitIndex)) { executingMove.RunScriptHook(x => x.OnSecondaryEffect(executingMove, target, hitIndex)); } } } } } } } if (numberOfHits == 0) { target.RunScriptHook(x => x.OnMoveMiss(executingMove, target)); battle.EventHook.Invoke(new MoveMissEvent(executingMove)); } if (!executingMove.User.IsFainted) { executingMove.RunScriptHook(x => x.OnAfterHits(executingMove, target)); } } }