326 lines
14 KiB
C#
326 lines
14 KiB
C#
using PkmnLib.Dynamic.Events;
|
|
using PkmnLib.Dynamic.Models;
|
|
using PkmnLib.Dynamic.Models.Choices;
|
|
using PkmnLib.Dynamic.ScriptHandling;
|
|
using PkmnLib.Static;
|
|
using PkmnLib.Static.Moves;
|
|
using PkmnLib.Static.Utils;
|
|
|
|
namespace PkmnLib.Dynamic.BattleFlow;
|
|
|
|
/// <summary>
|
|
/// Helper class for executing moves.
|
|
/// </summary>
|
|
public static class MoveTurnExecutor
|
|
{
|
|
internal static void ExecuteMoveChoice(IBattle battle, IMoveChoice moveChoice)
|
|
{
|
|
var chosenMove = moveChoice.ChosenMove;
|
|
var useMove = chosenMove.MoveData;
|
|
|
|
var moveDataName = useMove.Name;
|
|
moveChoice.RunScriptHook<IScriptChangeMove>(x => x.ChangeMove(moveChoice, ref moveDataName));
|
|
if (useMove.Name != moveDataName)
|
|
{
|
|
if (!battle.Library.StaticLibrary.Moves.TryGet(moveDataName, out useMove))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"The move was changed to '{moveDataName}' by a script, but this move does not exist.");
|
|
}
|
|
var secondaryEffect = useMove.SecondaryEffect;
|
|
if (secondaryEffect != null)
|
|
{
|
|
if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name,
|
|
secondaryEffect.Parameters, out var script))
|
|
{
|
|
moveChoice.Script.Set(script);
|
|
script.OnAddedToParent(moveChoice);
|
|
}
|
|
else
|
|
{
|
|
moveChoice.Script.Clear();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
moveChoice.Script.Clear();
|
|
}
|
|
}
|
|
moveChoice.RunScriptHook<IScriptOnBeforeMoveChoice>(x => x.OnBeforeMoveChoice(moveChoice));
|
|
|
|
var targetType = useMove.Target;
|
|
var targets =
|
|
TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType);
|
|
moveChoice.RunScriptHook<IScriptChangeTargets>(x => x.ChangeTargets(moveChoice, ref targets));
|
|
if (targets.Count == 0)
|
|
{
|
|
moveChoice.Fail();
|
|
return;
|
|
}
|
|
|
|
foreach (var target in targets.WhereNotNull())
|
|
{
|
|
target.RunScriptHook<IScriptChangeIncomingTargets>(x => x.ChangeIncomingTargets(moveChoice, ref targets));
|
|
}
|
|
|
|
byte numberOfHits = 1;
|
|
moveChoice.RunScriptHook<IScriptChangeNumberOfHits>(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits));
|
|
if (numberOfHits == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var executingMove = new ExecutingMoveImpl(targets, numberOfHits, chosenMove, useMove, moveChoice, battle);
|
|
|
|
battle.EventHook.Invoke(new MoveUseEvent(executingMove));
|
|
|
|
var prevented = false;
|
|
executingMove.RunScriptHook<IScriptPreventMove>(x => x.PreventMove(executingMove, ref prevented));
|
|
if (prevented)
|
|
return;
|
|
|
|
byte ppUsed = 1;
|
|
executingMove.RunScriptHook<IScriptModifyPPUsed>(x => x.ModifyPPUsed(executingMove, ref ppUsed));
|
|
targets.WhereNotNull()
|
|
.RunScriptHook<IScriptModifyPPUsedForIncomingMove>(x =>
|
|
x.ModifyPPUsedForIncomingMove(executingMove, ref ppUsed));
|
|
if (!executingMove.ChosenMove.TryUse(ppUsed))
|
|
return;
|
|
|
|
var failed = false;
|
|
executingMove.RunScriptHook<IScriptFailMove>(x => x.FailMove(executingMove, ref failed));
|
|
if (failed)
|
|
{
|
|
// TODO: fail handling
|
|
executingMove.MoveChoice.Fail();
|
|
return;
|
|
}
|
|
ExecuteMove(executingMove);
|
|
|
|
if (executingMove.Hits.All(x => x.HasFailed) || (executingMove.UseMove.Category != MoveCategory.Status &&
|
|
executingMove.Hits.All(x => x.Damage == 0)))
|
|
{
|
|
executingMove.MoveChoice.Fail();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes the move for its targets.
|
|
/// </summary>
|
|
/// <param name="executingMove"></param>
|
|
public static void ExecuteMove(IExecutingMove executingMove)
|
|
{
|
|
var stopped = false;
|
|
executingMove.RunScriptHook<IScriptStopBeforeMove>(x => x.StopBeforeMove(executingMove, ref stopped));
|
|
if (stopped)
|
|
return;
|
|
|
|
executingMove.RunScriptHook<IScriptOnBeforeMove>(x => x.OnBeforeMove(executingMove));
|
|
foreach (var target in executingMove.Targets.WhereNotNull())
|
|
{
|
|
ExecuteMoveChoiceForTarget(executingMove.Battle, executingMove, target);
|
|
}
|
|
executingMove.RunScriptHook<IScriptOnAfterMove>(x => x.OnAfterMove(executingMove));
|
|
}
|
|
|
|
private static readonly ThreadLocal<List<TypeIdentifier>> TypeListCache = new(() => []);
|
|
|
|
private static void ExecuteMoveChoiceForTarget(IBattle battle, IExecutingMove executingMove, IPokemon target)
|
|
{
|
|
var failed = false;
|
|
target.RunScriptHook<IScriptFailIncomingMove>(x => x.FailIncomingMove(executingMove, target, ref failed));
|
|
if (failed)
|
|
{
|
|
// TODO: fail handling
|
|
return;
|
|
}
|
|
|
|
var isInvulnerable = false;
|
|
target.RunScriptHook<IScriptIsInvulnerableToMove>(x =>
|
|
x.IsInvulnerableToMove(executingMove, target, ref isInvulnerable));
|
|
if (isInvulnerable)
|
|
{
|
|
battle.EventHook.Invoke(new MoveInvulnerableEvent(executingMove, target));
|
|
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;
|
|
executingMove.RunScriptHook<IScriptOnBeforeHit>(x => x.OnBeforeHit(executingMove, target, hitIndex));
|
|
var hitData = (HitData)executingMove.GetDataFromRawIndex(targetHitStat + i);
|
|
if (hitData.HasFailed)
|
|
break;
|
|
|
|
var useMove = executingMove.UseMove;
|
|
|
|
var isContact = useMove.HasFlag("contact");
|
|
executingMove.RunScriptHook<IScriptModifyIsContact>(x =>
|
|
x.ModifyIsContact(executingMove, target, hitIndex, ref isContact));
|
|
hitData.IsContact = isContact;
|
|
|
|
var hitType = (TypeIdentifier?)useMove.MoveType;
|
|
executingMove.RunScriptHook<IScriptChangeMoveType>(x =>
|
|
x.ChangeMoveType(executingMove, target, hitIndex, ref hitType));
|
|
|
|
hitData.Type = hitType;
|
|
|
|
// We re-use the TypeListCache to avoid allocating a new list every time.
|
|
var types = TypeListCache.Value;
|
|
types.Clear();
|
|
types.AddRange(target.Types);
|
|
|
|
executingMove.RunScriptHook<IScriptChangeTypesForMove>(x =>
|
|
x.ChangeTypesForMove(executingMove, target, hitIndex, types));
|
|
target.RunScriptHook<IScriptChangeTypesForIncomingMove>(x =>
|
|
x.ChangeTypesForIncomingMove(executingMove, target, hitIndex, types));
|
|
|
|
var effectiveness = hitType == null
|
|
? 1
|
|
: battle.Library.StaticLibrary.Types.GetEffectiveness(hitType.Value, types);
|
|
executingMove.RunScriptHook<IScriptChangeEffectiveness>(x =>
|
|
x.ChangeEffectiveness(executingMove, target, hitIndex, ref effectiveness));
|
|
target.RunScriptHook<IScriptChangeIncomingEffectiveness>(x =>
|
|
x.ChangeIncomingEffectiveness(executingMove, target, hitIndex, ref effectiveness));
|
|
hitData.Effectiveness = effectiveness;
|
|
|
|
var blockCritical = false;
|
|
executingMove.RunScriptHook<IScriptBlockCriticalHit>(x =>
|
|
x.BlockCriticalHit(executingMove, target, hitIndex, ref blockCritical));
|
|
target.RunScriptHook<IScriptBlockIncomingCriticalHit>(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, executingMove.UseMove.Category,
|
|
executingMove.User, target, executingMove.TargetCount, 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)
|
|
{
|
|
accuracy = battle.Library.StatCalculator.CalculateModifiedAccuracy(executingMove, target, hitIndex,
|
|
accuracy);
|
|
}
|
|
|
|
if (accuracy < 100 && battle.Random.GetInt(100) >= accuracy)
|
|
{
|
|
executingMove.RunScriptHook<IScriptOnMoveMiss>(x => x.OnMoveMiss(executingMove, target));
|
|
battle.EventHook.Invoke(new MoveMissEvent(executingMove));
|
|
break;
|
|
}
|
|
|
|
var blockIncomingHit = false;
|
|
target.RunScriptHook<IScriptBlockIncomingHit>(x =>
|
|
x.BlockIncomingHit(executingMove, target, hitIndex, ref blockIncomingHit));
|
|
executingMove.RunScriptHook<IScriptBlockOutgoingHit>(x =>
|
|
x.BlockOutgoingHit(executingMove, target, hitIndex, ref blockIncomingHit));
|
|
if (blockIncomingHit)
|
|
break;
|
|
if (executingMove.GetHitData(target, hitIndex).HasFailed)
|
|
break;
|
|
var category = useMove.Category;
|
|
executingMove.RunScriptHook<IScriptChangeCategory>(x =>
|
|
x.ChangeCategory(executingMove, target, hitIndex, ref category));
|
|
if (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<IScriptOnSecondaryEffect>(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<IScriptOnIncomingHit>(x =>
|
|
x.OnIncomingHit(executingMove, target, hitIndex));
|
|
}
|
|
else
|
|
{
|
|
executingMove.RunScriptHook<IScriptOnOpponentFaints>(x =>
|
|
x.OnOpponentFaints(executingMove, target, hitIndex));
|
|
}
|
|
|
|
if (!target.IsFainted)
|
|
{
|
|
var secondaryEffect = useMove.SecondaryEffect;
|
|
if (secondaryEffect != null)
|
|
{
|
|
var preventSecondary = false;
|
|
executingMove.RunScriptHook<IScriptPreventSecondaryEffect>(x =>
|
|
x.PreventSecondaryEffect(executingMove, target, hitIndex, ref preventSecondary));
|
|
target.RunScriptHook<IScriptPreventIncomingSecondaryEffect>(x =>
|
|
x.PreventIncomingSecondaryEffect(executingMove, target, hitIndex,
|
|
ref preventSecondary));
|
|
|
|
if (!preventSecondary)
|
|
{
|
|
var chance = secondaryEffect.Chance;
|
|
if (chance < 0 || battle.Random.EffectChance(chance, executingMove, target, hitIndex))
|
|
{
|
|
executingMove.RunScriptHook<IScriptOnSecondaryEffect>(x =>
|
|
x.OnSecondaryEffect(executingMove, target, hitIndex));
|
|
}
|
|
}
|
|
}
|
|
if (target.IsFainted)
|
|
{
|
|
executingMove.RunScriptHook<IScriptOnOpponentFaints>(x =>
|
|
x.OnOpponentFaints(executingMove, target, hitIndex));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (numberOfHits == 0)
|
|
{
|
|
target.RunScriptHook<IScriptOnMoveMiss>(x => x.OnMoveMiss(executingMove, target));
|
|
battle.EventHook.Invoke(new MoveMissEvent(executingMove));
|
|
}
|
|
|
|
if (!executingMove.User.IsFainted)
|
|
{
|
|
executingMove.RunScriptHook<IScriptOnAfterHits>(x => x.OnAfterHits(executingMove, target));
|
|
}
|
|
}
|
|
} |