Implements confusion effect
All checks were successful
Build / Build (push) Successful in 52s

This commit is contained in:
Deukhoofd 2025-06-22 11:56:29 +02:00
parent 6394f4eab3
commit 6d448e4e8d
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
6 changed files with 137 additions and 33 deletions

View File

@ -189,7 +189,8 @@ public static class MoveTurnExecutor
var basePower = battle.Library.DamageCalculator.GetBasePower(executingMove, target, hitIndex, hitData); var basePower = battle.Library.DamageCalculator.GetBasePower(executingMove, target, hitIndex, hitData);
hitData.BasePower = basePower; hitData.BasePower = basePower;
hitData.Damage = battle.Library.DamageCalculator.GetDamage(executingMove, target, hitIndex, hitData); hitData.Damage = battle.Library.DamageCalculator.GetDamage(executingMove, executingMove.UseMove.Category,
executingMove.User, target, executingMove.TargetCount, hitIndex, hitData);
var accuracy = useMove.Accuracy; var accuracy = useMove.Accuracy;
// If the accuracy is 255, the move should always hit, and as such we should not allow // If the accuracy is 255, the move should always hit, and as such we should not allow

View File

@ -1,4 +1,5 @@
using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models;
using PkmnLib.Static.Moves;
namespace PkmnLib.Dynamic.Libraries; namespace PkmnLib.Dynamic.Libraries;
@ -10,7 +11,8 @@ public interface IDamageCalculator
/// <summary> /// <summary>
/// Calculate the damage for a given hit on a Pokemon. /// Calculate the damage for a given hit on a Pokemon.
/// </summary> /// </summary>
uint GetDamage(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData); uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IPokemon user, IPokemon target,
int targetCount, byte hitNumber, IHitData hitData);
/// <summary> /// <summary>
/// Calculate the base power for a given hit on a Pokemon. /// Calculate the base power for a given hit on a Pokemon.

View File

@ -56,7 +56,8 @@ public class DamageCalculatorTests
// has a double weakness to the move's Ice type // has a double weakness to the move's Ice type
hit.Effectiveness.Returns(4.0f); hit.Effectiveness.Returns(4.0f);
var damage = damageCalculator.GetDamage(executingMove, defender, 0, hit); var damage = damageCalculator.GetDamage(executingMove, executingMove.UseMove.Category, executingMove.User,
defender, executingMove.TargetCount, 0, hit);
// That means Ice Fang will do between 168 and 196 HP damage, depending on luck. // That means Ice Fang will do between 168 and 196 HP damage, depending on luck.
// Note that we are testing deterministic damage, so we expect the maximum damage. // Note that we are testing deterministic damage, so we expect the maximum damage.
await Assert.That(damage).IsEqualTo((uint)196); await Assert.That(damage).IsEqualTo((uint)196);

View File

@ -6,30 +6,30 @@ namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDamageCalculator public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDamageCalculator
{ {
/// <inheritdoc /> /// <inheritdoc />
public uint GetDamage(IExecutingMove executingMove, IPokemon target, byte hitNumber, IHitData hitData) public uint GetDamage(IExecutingMove? executingMove, MoveCategory category, IPokemon user, IPokemon target,
int targetCount, byte hitNumber, IHitData hitData)
{ {
var category = executingMove.UseMove.Category;
if (category == MoveCategory.Status) if (category == MoveCategory.Status)
return 0; return 0;
if (hitData.Effectiveness == 0) if (hitData.Effectiveness == 0)
return 0; return 0;
var levelModifier = 2.0f * executingMove.User.Level / 5.0f + 2.0f; var levelModifier = 2.0f * user.Level / 5.0f + 2.0f;
var basePower = (float)hitData.BasePower; var basePower = (float)hitData.BasePower;
var statModifier = GetStatModifier(executingMove, target, hitNumber, hitData); var statModifier = GetStatModifier(executingMove, category, user, target, hitNumber, hitData);
var damageModifier = GetDamageModifier(executingMove, target, hitNumber); var damageModifier = executingMove == null ? 1.0f : GetDamageModifier(executingMove, target, hitNumber);
var floatDamage = MathF.Floor(levelModifier * basePower); var floatDamage = MathF.Floor(levelModifier * basePower);
floatDamage = MathF.Floor(floatDamage * statModifier); floatDamage = MathF.Floor(floatDamage * statModifier);
floatDamage = MathF.Floor(floatDamage / 50.0f) + 2.0f; floatDamage = MathF.Floor(floatDamage / 50.0f) + 2.0f;
floatDamage = MathF.Floor(floatDamage * damageModifier); floatDamage = MathF.Floor(floatDamage * damageModifier);
if (executingMove.TargetCount > 1) if (targetCount > 1)
floatDamage = MathF.Floor(floatDamage * 0.75f); floatDamage = MathF.Floor(floatDamage * 0.75f);
if (hitData.IsCritical) if (hitData.IsCritical)
{ {
var critModifier = 1.5f; var critModifier = 1.5f;
executingMove.RunScriptHook(script => executingMove?.RunScriptHook(script =>
script.ChangeCriticalModifier(executingMove, target, hitNumber, ref critModifier)); script.ChangeCriticalModifier(executingMove, target, hitNumber, ref critModifier));
floatDamage = MathF.Floor(floatDamage * critModifier); floatDamage = MathF.Floor(floatDamage * critModifier);
} }
@ -46,12 +46,12 @@ public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDama
var stabModifier = 1.0f; var stabModifier = 1.0f;
var isStab = false; var isStab = false;
if (hitData.Type != null && executingMove.User.Types.Contains(hitData.Type.Value)) if (hitData.Type != null && user.Types.Contains(hitData.Type.Value))
{ {
stabModifier = 1.5f; stabModifier = 1.5f;
isStab = true; isStab = true;
} }
executingMove.RunScriptHook(script => executingMove?.RunScriptHook(script =>
script.ChangeStabModifier(executingMove, target, hitNumber, isStab, ref stabModifier)); script.ChangeStabModifier(executingMove, target, hitNumber, isStab, ref stabModifier));
floatDamage = MathF.Floor(floatDamage * stabModifier); floatDamage = MathF.Floor(floatDamage * stabModifier);
@ -62,8 +62,14 @@ public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDama
< 1 => 1, < 1 => 1,
_ => (uint)floatDamage, _ => (uint)floatDamage,
}; };
executingMove.RunScriptHook(script => script.ChangeMoveDamage(executingMove, target, hitNumber, ref damage));
target.RunScriptHook(script => script.ChangeIncomingMoveDamage(executingMove, target, hitNumber, ref damage)); if (executingMove is not null)
{
executingMove.RunScriptHook(script =>
script.ChangeMoveDamage(executingMove, target, hitNumber, ref damage));
target.RunScriptHook(script =>
script.ChangeIncomingMoveDamage(executingMove, target, hitNumber, ref damage));
}
return damage; return damage;
} }
@ -97,10 +103,9 @@ public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDama
}; };
} }
private static float GetStatModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber, private static float GetStatModifier(IExecutingMove? executingMove, MoveCategory category, IPokemon user,
IHitData hitData) IPokemon target, byte hitNumber, IHitData hitData)
{ {
var category = executingMove.UseMove.Category;
if (category == MoveCategory.Status) if (category == MoveCategory.Status)
return 1; return 1;
@ -114,19 +119,19 @@ public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDama
// move is critical, and the target has a defensive stat boost of > 0, but a script is // move is critical, and the target has a defensive stat boost of > 0, but a script is
// allowed to change this. // allowed to change this.
var bypassDefense = hitData.IsCritical && target.StatBoost.GetStatistic(defensive) > 0; var bypassDefense = hitData.IsCritical && target.StatBoost.GetStatistic(defensive) > 0;
executingMove.RunScriptHook(script => executingMove?.RunScriptHook(script =>
script.BypassDefensiveStatBoosts(executingMove, target, hitNumber, ref bypassDefense)); script.BypassDefensiveStatBoosts(executingMove, target, hitNumber, ref bypassDefense));
// Check if we can bypass the offensive stat boost on the user. We default to this if the // Check if we can bypass the offensive stat boost on the user. We default to this if the
// move is critical, and the user has an offensive stat boost of < 0, but a script is // move is critical, and the user has an offensive stat boost of < 0, but a script is
// allowed to change this. // allowed to change this.
var bypassOffense = hitData.IsCritical && executingMove.User.StatBoost.GetStatistic(offensive) < 0; var bypassOffense = hitData.IsCritical && user.StatBoost.GetStatistic(offensive) < 0;
executingMove.RunScriptHook(script => executingMove?.RunScriptHook(script =>
script.BypassOffensiveStatBoosts(executingMove, target, hitNumber, ref bypassOffense)); script.BypassOffensiveStatBoosts(executingMove, target, hitNumber, ref bypassOffense));
var userStats = executingMove.User.BoostedStats; var userStats = user.BoostedStats;
if (bypassOffense) if (bypassOffense)
userStats = executingMove.User.FlatStats; userStats = user.FlatStats;
var offensiveStat = userStats.GetStatistic(offensive); var offensiveStat = userStats.GetStatistic(offensive);
var targetStats = target.BoostedStats; var targetStats = target.BoostedStats;
@ -135,6 +140,8 @@ public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDama
var defensiveStat = targetStats.GetStatistic(defensive); var defensiveStat = targetStats.GetStatistic(defensive);
var origOffensiveStat = offensiveStat; var origOffensiveStat = offensiveStat;
if (executingMove != null)
{
executingMove.RunScriptHook(script => script.ChangeOffensiveStatValue(executingMove, target, hitNumber, executingMove.RunScriptHook(script => script.ChangeOffensiveStatValue(executingMove, target, hitNumber,
defensiveStat, targetStats, offensive, ref offensiveStat)); defensiveStat, targetStats, offensive, ref offensiveStat));
executingMove.RunScriptHook(script => script.ChangeDefensiveStatValue(executingMove, target, hitNumber, executingMove.RunScriptHook(script => script.ChangeDefensiveStatValue(executingMove, target, hitNumber,
@ -143,9 +150,10 @@ public class Gen7DamageCalculator(Gen7PluginConfiguration configuration) : IDama
defensiveStat, targetStats, offensive, ref offensiveStat)); defensiveStat, targetStats, offensive, ref offensiveStat));
target.RunScriptHook(script => script.ChangeIncomingMoveDefensiveStatValue(executingMove, target, hitNumber, target.RunScriptHook(script => script.ChangeIncomingMoveDefensiveStatValue(executingMove, target, hitNumber,
origOffensiveStat, targetStats, defensive, ref defensiveStat)); origOffensiveStat, targetStats, defensive, ref defensiveStat));
}
var modifier = (float)offensiveStat / defensiveStat; var modifier = (float)offensiveStat / defensiveStat;
executingMove.RunScriptHook(script => executingMove?.RunScriptHook(script =>
script.ChangeDamageStatModifier(executingMove, target, hitNumber, ref modifier)); script.ChangeDamageStatModifier(executingMove, target, hitNumber, ref modifier));
return modifier; return modifier;

View File

@ -27,7 +27,8 @@ public class FutureSightEffect : Script
var executingMove = new ExecutingMoveImpl([target], 1, _moveChoice.ChosenMove, var executingMove = new ExecutingMoveImpl([target], 1, _moveChoice.ChosenMove,
_moveChoice.ChosenMove.MoveData, _moveChoice, battle); _moveChoice.ChosenMove.MoveData, _moveChoice, battle);
var hitData = executingMove.GetHitData(target, 0); var hitData = executingMove.GetHitData(target, 0);
var damage = damageCalculator.GetDamage(executingMove, target, 1, hitData); var damage = damageCalculator.GetDamage(executingMove, executingMove.UseMove.Category, executingMove.User,
target, executingMove.TargetCount, 1, hitData);
target.Damage(damage, DamageSource.Misc); target.Damage(damage, DamageSource.Misc);
} }

View File

@ -1,7 +1,98 @@
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon; namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "confusion")] [Script(ScriptCategory.Pokemon, "confusion")]
public class Confusion : Script public class Confusion : Script
{ {
// TODO: Implement confusion private int _turnsConfused;
/// <inheritdoc />
public override void OnAddedToParent(IScriptSource source)
{
if (source is not IPokemon pokemon)
throw new InvalidOperationException("Confusion script can only be added to a Pokemon.");
var battleData = pokemon.BattleData;
if (battleData == null)
throw new InvalidOperationException("Confusion script requires a Pokemon to be in a battle.");
_turnsConfused = battleData.Battle.Random.GetInt(2, 5);
}
/// <inheritdoc />
public override void StopBeforeMove(IExecutingMove move, ref bool stop)
{
_turnsConfused--;
move.Battle.EventHook.Invoke(new DialogEvent("pokemon_is_confused", new Dictionary<string, object>
{
{ "pokemon", move.User },
}));
if (_turnsConfused <= 0)
{
RemoveSelf();
move.Battle.EventHook.Invoke(new DialogEvent("pokemon_snapped_out_of_confusion",
new Dictionary<string, object>
{
{ "pokemon", move.User },
}));
return;
}
if (move.Battle.Random.GetInt(0, 100) < 33)
{
var damage = move.Battle.Library.DamageCalculator.GetDamage(null, MoveCategory.Physical, move.User,
move.User, 1, 0, new ConfusionHitData());
EventBatchId batchId = new();
move.User.Damage(damage, DamageSource.Confusion, batchId);
move.Battle.EventHook.Invoke(new DialogEvent("pokemon_hit_itself_in_confusion",
new Dictionary<string, object>
{
{ "pokemon", move.User },
{ "damage", damage },
})
{
BatchId = batchId,
});
stop = true;
}
}
private class ConfusionHitData : IHitData
{
/// <inheritdoc />
public bool IsCritical => false;
/// <inheritdoc />
public ushort BasePower => 40;
/// <inheritdoc />
public float Effectiveness => 1.0f;
/// <inheritdoc />
public uint Damage { get; } = 0;
/// <inheritdoc />
public TypeIdentifier? Type => null;
/// <inheritdoc />
public bool IsContact => true;
/// <inheritdoc />
public bool HasFailed => false;
/// <inheritdoc />
public void Fail()
{
}
/// <inheritdoc />
public void SetFlag(StringKey flag)
{
}
/// <inheritdoc />
public bool HasFlag(StringKey flag) => false;
}
} }