Implements a bunch more moves
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-05-17 17:44:15 +02:00
parent ecabe2fd10
commit a17cb92c5a
62 changed files with 1180 additions and 81 deletions

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using PkmnLib.Dynamic.BattleFlow;
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.BattleFlow;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;

View File

@@ -1,273 +0,0 @@
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Models.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(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();
}
}
var targetType = useMove.Target;
var targets =
TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType);
moveChoice.RunScriptHook(x => x.ChangeTargets(moveChoice, ref targets));
var targetSide = battle.Sides[moveChoice.TargetSide];
targetSide.RunScriptHook(x => x.ChangeIncomingTargets(moveChoice, ref targets));
byte numberOfHits = 1;
moveChoice.RunScriptHook(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits));
if (numberOfHits == 0)
{
return;
}
var executingMove = new ExecutingMoveImpl(targets, numberOfHits, chosenMove, useMove, moveChoice, battle);
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
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(x => x.StopBeforeMove(executingMove, ref stopped));
if (stopped)
return;
executingMove.RunScriptHook(x => x.OnBeforeMove(executingMove));
foreach (var target in executingMove.Targets.WhereNotNull())
{
ExecuteMoveChoiceForTarget(executingMove.Battle, executingMove, target);
}
executingMove.RunScriptHook(x => x.OnAfterMove(executingMove));
}
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;
executingMove.RunScriptHook(x => x.OnBeforeHit(executingMove, target, hitIndex));
var useMove = executingMove.UseMove;
var hitType = (TypeIdentifier?)useMove.MoveType;
executingMove.RunScriptHook(x => x.ChangeMoveType(executingMove, target, hitIndex, ref hitType));
var hitData = (HitData)executingMove.GetDataFromRawIndex(targetHitStat + i);
hitData.Type = hitType;
var types = target.Types.ToList();
executingMove.RunScriptHook(x => x.ChangeTypesForMove(executingMove, target, hitIndex, types));
target.RunScriptHook(x => x.ChangeTypesForIncomingMove(executingMove, target, hitIndex, types));
var effectiveness = hitType == null
? 1
: battle.Library.StaticLibrary.Types.GetEffectiveness(hitType.Value, types);
executingMove.RunScriptHook(x => x.ChangeEffectiveness(executingMove, target, hitIndex, ref effectiveness));
target.RunScriptHook(x =>
x.ChangeIncomingEffectiveness(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)
{
accuracy = battle.Library.StatCalculator.CalculateModifiedAccuracy(executingMove, target, hitIndex,
accuracy);
}
if (accuracy < 100 && battle.Random.GetInt(100) >= accuracy)
{
executingMove.RunScriptHook(x => x.OnMoveMiss(executingMove, target));
battle.EventHook.Invoke(new MoveMissEvent(executingMove));
break;
}
var blockIncomingHit = false;
target.RunScriptHook(x => x.BlockIncomingHit(executingMove, target, hitIndex, ref blockIncomingHit));
executingMove.RunScriptHook(x => x.BlockOutgoingHit(executingMove, target, hitIndex, ref blockIncomingHit));
if (blockIncomingHit)
break;
if (executingMove.GetHitData(target, hitIndex).HasFailed)
break;
var category = useMove.Category;
executingMove.RunScriptHook(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(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));
}
}
}

View File

@@ -1,151 +0,0 @@
using PkmnLib.Static.Moves;
namespace PkmnLib.Dynamic.Models.BattleFlow;
/// <summary>
/// Helper class for resolving the targets of a move.
/// </summary>
public static class TargetResolver
{
/// <summary>
/// Get the targets of a move based on the target type, and the selected side and position to target.
/// </summary>
public static IReadOnlyList<IPokemon?> ResolveTargets(IBattle battle, byte side, byte position, MoveTarget target)
{
return target switch
{
MoveTarget.Adjacent or MoveTarget.AdjacentAlly or MoveTarget.AdjacentAllySelf or MoveTarget.AdjacentOpponent
or MoveTarget.Any or MoveTarget.RandomOpponent or MoveTarget.SelfUse =>
[battle.GetPokemon(side, position)],
MoveTarget.All => GetAllTargets(battle),
MoveTarget.AllAdjacentOpponent => GetAllAdjacentAndOpponent(battle, side, position),
MoveTarget.AllAdjacent => GetAllAdjacent(battle, side, position),
MoveTarget.AllAlly => battle.Sides[side].Pokemon.ToList(),
MoveTarget.AllOpponent => battle.Sides[GetOppositeSide(side)].Pokemon.ToList(),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
};
}
/// <summary>
/// Validates whether a given target is valid for a move choice. Returns true if the target is valid.
/// </summary>
public static bool IsValidTarget(byte side, byte position, MoveTarget target, IPokemon user)
{
var userBattleData = user.BattleData;
if (userBattleData == null)
throw new ArgumentNullException(nameof(user.BattleData));
var userSide = userBattleData.SideIndex;
var userPosition = userBattleData.Position;
switch (target)
{
case MoveTarget.Adjacent:
case MoveTarget.AllAdjacent:
{
var diff = Math.Abs(position - userPosition);
if (diff == 0)
return userSide == side;
return diff <= 1;
}
case MoveTarget.AdjacentAlly:
{
if (userSide != side)
return false;
return Math.Abs(position - userPosition) == 1;
}
case MoveTarget.AdjacentAllySelf:
{
if (userSide != side)
return false;
return Math.Abs(position - userPosition) <= 1;
}
case MoveTarget.AdjacentOpponent:
case MoveTarget.AllAdjacentOpponent:
{
if (userSide == side)
return false;
return Math.Abs(position - userPosition) <= 1;
}
case MoveTarget.All:
case MoveTarget.Any:
case MoveTarget.RandomOpponent:
return true;
case MoveTarget.AllAlly:
return userSide == side;
case MoveTarget.AllOpponent:
return userSide != side;
case MoveTarget.SelfUse:
return userSide == side && userPosition == position;
}
throw new ArgumentOutOfRangeException(nameof(target), target, null);
}
private static IReadOnlyList<IPokemon?> GetAllTargets(IBattle battle) =>
battle.Sides.SelectMany(x => x.Pokemon).ToList();
private static byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0;
/// <summary>
/// Gets all Pokémon that are adjacent to of directly opposite of a Pokémon. This means the target,
/// the Pokémon left of it, the Pokémon right of it, and the Pokémon opposite of it.
/// </summary>
private static IReadOnlyList<IPokemon?> GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position)
{
var left = position - 1;
var right = position + 1;
if (left < 0 && right >= battle.PositionsPerSide)
{
return [battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position)];
}
if (left < 0)
{
return
[
battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position),
battle.GetPokemon(side, (byte)right),
];
}
if (right >= battle.PositionsPerSide)
{
return
[
battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position),
battle.GetPokemon(side, (byte)left),
];
}
return
[
battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position),
battle.GetPokemon(side, (byte)left), battle.GetPokemon(side, (byte)right),
];
}
private static IReadOnlyList<IPokemon?> GetAllAdjacent(IBattle battle, byte side, byte position)
{
var left = position - 1;
var right = position + 1;
if (left < 0 && right >= battle.PositionsPerSide)
{
return [battle.GetPokemon(side, position)];
}
if (left < 0)
{
return [battle.GetPokemon(side, position), battle.GetPokemon(side, (byte)right)];
}
if (right >= battle.PositionsPerSide)
{
return [battle.GetPokemon(side, position), battle.GetPokemon(side, (byte)left)];
}
return
[
battle.GetPokemon(side, position), battle.GetPokemon(side, (byte)left),
battle.GetPokemon(side, (byte)right),
];
}
}

View File

@@ -1,178 +0,0 @@
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Models.BattleFlow;
/// <summary>
/// Helper class for handling the running of a turn in a battle.
/// </summary>
public static class TurnRunner
{
/// <summary>
/// Runs a single turn in a battle to completion.
/// </summary>
public static void RunTurn(this IBattle battle)
{
var queue = battle.ChoiceQueue;
if (queue == null)
{
throw new ArgumentNullException(nameof(battle.ChoiceQueue),
"The battle's choice queue must be set before running a turn.");
}
// We are now at the very beginning of a turn. We have assigned speeds and priorities to all
// choices, and put them in the correct order.
// The first thing to do is to run the on_before_turn script hook on every choice. This script hook
// is primarily intended to be used to reset variables on a script (for example scripts that need
// to check whether a Pokémon was hit this turn. By resetting here, and setting a variable to true
// they can then know this later on.)
foreach (var choice in queue.GetChoices().WhereNotNull())
{
choice.RunScriptHook(script => script.OnBeforeTurnStart(choice));
}
// Now we can properly begin executing choices.
// One by one dequeue the turns, and run them. If the battle has ended we do not want to
// continue running.
while (queue.HasNext() && !battle.HasEnded)
{
var next = queue.Dequeue();
if (next == null)
continue;
ExecuteChoice(battle, next);
}
// If the battle is not ended, we have arrived at the normal end of a turn. and thus want
// to run the end turn scripts.
// As we want all scripts to run exactly once, including those on the sides and battles,
// we can't just use the default script hook on each Pokémon. Instead, manually call
// the script functions on every script.
if (!battle.HasEnded)
{
var scripts = new List<IEnumerable<ScriptContainer>>(10);
foreach (var side in battle.Sides)
{
foreach (var pokemon in side.Pokemon.WhereNotNull())
{
scripts.Clear();
pokemon.GetOwnScripts(scripts);
scripts.RunScriptHook(x => x.OnEndTurn(battle));
}
scripts.Clear();
side.GetOwnScripts(scripts);
scripts.RunScriptHook(x => x.OnEndTurn(battle));
}
scripts.Clear();
battle.GetOwnScripts(scripts);
scripts.RunScriptHook(x => x.OnEndTurn(battle));
}
}
/// <summary>
/// Runs a single choice in a battle.
/// </summary>
public static void ExecuteChoice(IBattle battle, ITurnChoice choice)
{
if (choice is IPassChoice)
return;
if (battle.HasEnded)
return;
if (!choice.User.IsUsable)
return;
if (choice.User.BattleData?.IsOnBattlefield != true)
return;
switch (choice)
{
case IMoveChoice moveChoice:
MoveTurnExecutor.ExecuteMoveChoice(battle, moveChoice);
break;
case ISwitchChoice switchChoice:
ExecuteSwitchChoice(battle, switchChoice);
break;
case IFleeChoice fleeChoice:
ExecuteFleeChoice(battle, fleeChoice);
break;
case IItemChoice itemChoice:
ExecuteItemChoice(battle, itemChoice);
break;
}
}
private static void ExecuteSwitchChoice(IBattle battle, ISwitchChoice fleeChoice)
{
var user = fleeChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return;
var preventSwitch = false;
fleeChoice.RunScriptHook(script => script.PreventSelfSwitch(fleeChoice, ref preventSwitch));
if (preventSwitch)
return;
foreach (var side in battle.Sides)
{
if (side.Index == battleData.SideIndex)
continue;
foreach (var pokemon in side.Pokemon.WhereNotNull())
{
pokemon.RunScriptHook(script => script.PreventOpponentSwitch(fleeChoice, ref preventSwitch));
if (preventSwitch)
return;
}
}
user.Volatile.Clear();
var userSide = battle.Sides[battleData.SideIndex];
userSide.SwapPokemon(battleData.Position, fleeChoice.SwitchTo);
}
private static void ExecuteFleeChoice(IBattle battle, IFleeChoice fleeChoice)
{
var user = fleeChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return;
if (!battle.CanFlee)
return;
var preventFlee = false;
fleeChoice.RunScriptHook(script => script.PreventSelfRunAway(fleeChoice, ref preventFlee));
if (preventFlee)
return;
foreach (var side in battle.Sides)
{
if (side.Index == battleData.SideIndex)
continue;
foreach (var pokemon in side.Pokemon.WhereNotNull())
{
pokemon.RunScriptHook(script => script.PreventOpponentRunAway(fleeChoice, ref preventFlee));
if (preventFlee)
return;
}
}
if (!battle.Library.MiscLibrary.CanFlee(battle, fleeChoice))
return;
var userSide = battle.Sides[battleData.SideIndex];
userSide.MarkAsFled();
battle.ValidateBattleState();
}
private static void ExecuteItemChoice(IBattle battle, IItemChoice itemChoice)
{
var user = itemChoice.User;
var battleData = user.BattleData;
if (battleData == null)
return;
IPokemon? target = null;
if (itemChoice is { TargetSide: not null, TargetPosition: not null })
{
var side = battle.Sides[itemChoice.TargetSide.Value];
target = side.Pokemon[itemChoice.TargetPosition.Value];
}
itemChoice.Item.RunItemScript(battle.Library.ScriptResolver, target ?? user);
}
}