Adds more abilities

This commit is contained in:
Deukhoofd 2025-06-07 10:58:58 +02:00
parent 232b94b04c
commit b2ba3d96ba
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
25 changed files with 429 additions and 16 deletions

View File

@ -1,14 +1,24 @@
using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models;
using PkmnLib.Static.Species; using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Events; namespace PkmnLib.Dynamic.Events;
/// <summary>
/// AbilityTriggerEvent is triggered when a Pokémon's ability is activated.
/// </summary>
public record AbilityTriggerEvent : IEventData public record AbilityTriggerEvent : IEventData
{ {
/// <summary>
/// The Pokémon whose ability is being triggered.
/// </summary>
public IPokemon Pokemon { get; } public IPokemon Pokemon { get; }
/// <summary>
/// The ability that is being triggered for the Pokémon.
/// </summary>
public IAbility? Ability { get; } public IAbility? Ability { get; }
/// <inheritdoc cref="AbilityTriggerEvent"/>
public AbilityTriggerEvent(IPokemon pokemon) public AbilityTriggerEvent(IPokemon pokemon)
{ {
Pokemon = pokemon; Pokemon = pokemon;

View File

@ -20,5 +20,8 @@ public class SerializedAbility
/// </summary> /// </summary>
public string[] Flags { get; set; } = []; public string[] Flags { get; set; } = [];
/// <summary>
/// Indicates whether the ability can be changed by effects such as Skill Swap or Role Play.
/// </summary>
public bool? CanBeChanged { get; set; } public bool? CanBeChanged { get; set; }
} }

View File

@ -814,6 +814,10 @@ public class PokemonImpl : ScriptSource, IPokemon
return true; return true;
} }
/// <summary>
/// Uses an item on this Pokémon.
/// </summary>
/// <param name="item"></param>
public void UseItem(IItem item) public void UseItem(IItem item)
{ {
// TODO: actually consume the item // TODO: actually consume the item
@ -851,6 +855,7 @@ public class PokemonImpl : ScriptSource, IPokemon
} }
RecalculateBoostedStats(); RecalculateBoostedStats();
this.RunScriptHook(script => script.OnAfterStatBoostChange(this, stat, selfInflicted, change));
return true; return true;
} }

View File

@ -418,6 +418,13 @@ public abstract class Script : IDeepCloneable
{ {
} }
/// <summary>
/// This function allows a script to run after a stat boost change has been applied.
/// </summary>
public virtual void OnAfterStatBoostChange(IPokemon pokemon, Statistic stat, bool selfInflicted, sbyte change)
{
}
/// <summary> /// <summary>
/// This function allows a script attached to a Pokemon or its parents to prevent an incoming /// This function allows a script attached to a Pokemon or its parents to prevent an incoming
/// secondary effect. This means the move will still hit and do damage, but not trigger its /// secondary effect. This means the move will still hit and do damage, but not trigger its

View File

@ -37,6 +37,34 @@ public static class ScriptExecution
} }
} }
/// <summary>
/// Executes a hook on all scripts in a source.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RunScriptHook(this IEnumerable<IScriptSource> sources, Action<Script> hook)
{
var iterator = sources.Distinct().SelectMany(x => x.GetScripts()).ToArray();
List<ScriptCategory>? suppressedCategories = null;
foreach (var container in iterator)
{
if (container.IsEmpty)
continue;
var script = container.Script;
script.OnBeforeAnyHookInvoked(ref suppressedCategories);
}
foreach (var container in iterator)
{
if (container.IsEmpty)
continue;
var script = container.Script;
if (script.IsSuppressed)
continue;
if (suppressedCategories != null && suppressedCategories.Contains(script.Category))
continue;
hook(script);
}
}
/// <summary> /// <summary>
/// Executes a hook on all scripts in a list of sources. Note that this does not walk through the parents of the /// Executes a hook on all scripts in a list of sources. Note that this does not walk through the parents of the
/// sources, but only the sources themselves. /// sources, but only the sources themselves.

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EV/@EntryIndexedValue">EV</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EV/@EntryIndexedValue">EV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PP/@EntryIndexedValue">PP</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PP/@EntryIndexedValue">PP</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bulbapedia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pkmn/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=pkmn/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -24,6 +24,9 @@ public interface IAbility : INamedValue
/// </summary> /// </summary>
bool HasFlag(StringKey key); bool HasFlag(StringKey key);
/// <summary>
/// Indicates whether the ability can be changed by effects such as Skill Swap or Role Play.
/// </summary>
bool CanBeChanged { get; } bool CanBeChanged { get; }
} }

View File

@ -92,20 +92,45 @@
"effect": "color_change" "effect": "color_change"
}, },
"comatose": { "comatose": {
"effect": "comatose",
"canBeChanged": false "canBeChanged": false
}, },
"competitive": {}, "competitive": {
"compound_eyes": {}, "effect": "competitive"
"contrary": {}, },
"corrosion": {}, "compound_eyes": {
"cursed_body": {}, "effect": "compound_eyes"
"cute_charm": {}, },
"damp": {}, "contrary": {
"dancer": {}, "effect": "contrary"
"dark_aura": {}, },
"dazzling": {}, "corrosion": {
"defeatist": {}, "effect": "corrosion"
"defiant": {}, },
"cursed_body": {
"effect": "cursed_body"
},
"cute_charm": {
"effect": "cute_charm"
},
"damp": {
"effect": "damp"
},
"dancer": {
"effect": "dancer"
},
"dark_aura": {
"effect": "dark_aura"
},
"dazzling": {
"effect": "dazzling"
},
"defeatist": {
"effect": "defeatist"
},
"defiant": {
"effect": "defiant"
},
"delta_stream": {}, "delta_stream": {},
"desolate_land": {}, "desolate_land": {},
"disguise": { "disguise": {

View File

@ -1,7 +1,32 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities; namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Aura Break is an ability that reverses the effects of Dark Aura and Fairy Aura.
/// When this ability is present, Dark-type and Fairy-type moves are reduced in power by 25% instead of being boosted.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Aura_Break_(Ability)">Bulbapedia - Aura Break</see>
/// </summary>
[Script(ScriptCategory.Ability, "aura_break")] [Script(ScriptCategory.Ability, "aura_break")]
public class AuraBreak : Script public class AuraBreak : Script
{ {
// FIXME: Implement together with Dark Aura and Fairy Aura. /// <inheritdoc />
public override void CustomTrigger(StringKey eventName, IDictionary<StringKey, object?>? parameters)
{
if (eventName != CustomTriggers.ModifyAuraEffect || parameters == null)
return;
if (!parameters.TryGetValue("aura_type", out var auraTypeObj) || auraTypeObj is not string auraType)
return;
if (auraType is "dark" or "fairy")
{
if (parameters.TryGetValue("modifier", out var modifierObj) && modifierObj is float)
{
// Reverse the aura effect by reducing power by 25%
parameters["modifier"] = 0.75f;
}
}
}
} }

View File

@ -0,0 +1,32 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Comatose is an ability that makes the Pokémon always be in a sleep state, but still able to attack.
/// The Pokémon cannot be affected by other status conditions, and moves that would normally fail on sleeping Pokémon
/// will work normally. The Pokémon will still be affected by sleep-related moves and abilities.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Comatose_(Ability)">Bulbapedia - Comatose</see>
/// </summary>
[Script(ScriptCategory.Ability, "comatose")]
public class Comatose : Script
{
/// <inheritdoc />
public override void PreventStatusChange(IPokemon pokemonImpl, StringKey status, ref bool preventStatus)
{
if (status == ScriptUtils.ResolveName<Status.Sleep>())
{
preventStatus = true;
}
}
/// <inheritdoc />
public override void CustomTrigger(StringKey eventName, IDictionary<StringKey, object?>? parameters)
{
if (eventName == CustomTriggers.BypassSleep && parameters != null)
{
parameters["bypass_sleep"] = true;
}
}
}

View File

@ -0,0 +1,27 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Competitive is an ability that raises the Pokémon's Special Attack by two stages whenever any of its stats are lowered.
/// This includes stat drops from moves, abilities, or items. The ability will not activate if the stat drop is self-inflicted.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Competitive_(Ability)">Bulbapedia - Competitive</see>
/// </summary>
[Script(ScriptCategory.Ability, "competitive")]
public class Competitive : Script
{
/// <inheritdoc />
/// <inheritdoc />
public override void OnAfterStatBoostChange(IPokemon pokemon, Statistic stat, bool selfInflicted, sbyte change)
{
if (change >= 0)
return;
if (selfInflicted)
return;
EventBatchId batchId = new();
pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon)
{
BatchId = batchId,
});
pokemon.ChangeStatBoost(Statistic.SpecialAttack, 2, true, batchId);
}
}

View File

@ -0,0 +1,18 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Compound Eyes is an ability that increases the accuracy of the Pokémon's moves by 30%.
/// This applies to all moves used by the Pokémon, making it particularly effective with low-accuracy moves.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Compound_Eyes_(Ability)">Bulbapedia - Compound Eyes</see>
/// </summary>
[Script(ScriptCategory.Ability, "compound_eyes")]
public class CompoundEyes : Script
{
/// <inheritdoc />
public override void ChangeAccuracy(IExecutingMove move, IPokemon target, byte hit, ref int modifiedAccuracy)
{
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User));
modifiedAccuracy = (int)(modifiedAccuracy * 1.3f);
}
}

View File

@ -0,0 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Contrary is an ability that inverts all stat changes for the Pokémon.
/// When a stat would normally be raised, it is lowered instead, and vice versa.
/// This applies to both self-inflicted stat changes and those caused by other Pokémon.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Contrary_(Ability)">Bulbapedia - Contrary</see>
/// </summary>
[Script(ScriptCategory.Ability, "contrary")]
public class Contrary : Script
{
/// <inheritdoc />
public override void ChangeStatBoostChange(IPokemon target, Statistic stat, bool selfInflicted, ref sbyte amount)
{
// Invert the stat change
amount = (sbyte)-amount;
target.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(target));
}
}

View File

@ -0,0 +1,13 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Corrosion is an ability that allows the Pokémon to poison Steel-type and Poison-type Pokémon.
/// This ability bypasses the natural immunity these types have to poison status.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Corrosion_(Ability)">Bulbapedia - Corrosion</see>
/// </summary>
[Script(ScriptCategory.Ability, "corrosion")]
public class Corrosion : Script
{
// TODO: Implement Corrosion ability logic
}

View File

@ -0,0 +1,25 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Cursed Body is an ability that has a 30% chance to disable the last move used against the Pokémon.
/// When triggered, the opponent's move becomes disabled for 4 turns, preventing its use.
/// This ability can be triggered by any damaging move that hits the Pokémon.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Cursed_Body_(Ability)">Bulbapedia - Cursed Body</see>
/// </summary>
[Script(ScriptCategory.Ability, "cursed_body")]
public class CursedBody : Script
{
/// <inheritdoc />
public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
// 30% chance to disable the move
if (move.Battle.Random.GetFloat() > 0.3f)
return;
target.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(target));
target.Volatile.Add(new DisableEffect(move.ChosenMove.MoveData.Name));
}
}

View File

@ -0,0 +1,34 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Static.Species;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Cute Charm is an ability that has a 30% chance to infatuate the opponent when hit by a contact move.
/// Infatuation prevents the affected Pokémon from attacking 50% of the time.
/// This ability only works on Pokémon of the opposite gender.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Cute_Charm_(Ability)">Bulbapedia - Cute Charm</see>
/// </summary>
[Script(ScriptCategory.Ability, "cute_charm")]
public class CuteCharm : Script
{
/// <inheritdoc />
public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
// Only trigger on contact moves
if (!move.UseMove.HasFlag("contact"))
return;
// 30% chance to infatuate
if (move.Battle.Random.GetFloat() > 0.3f)
return;
if (target.Gender == move.User.Gender || target.Gender == Gender.Genderless ||
move.User.Gender == Gender.Genderless)
return;
target.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(target));
move.User.Volatile.Add(new Infatuated());
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Damp is an ability that prevents the use of Self-Destruct and Explosion moves.
/// When a Pokémon with this ability is on the field, any attempt to use these moves will fail.
/// This ability affects both allies and opponents.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Damp_(Ability)">Bulbapedia - Damp</see>
/// </summary>
[Script(ScriptCategory.Ability, "damp")]
public class Damp : Script
{
/// <inheritdoc />
public override void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail)
{
if (move.UseMove.Name == "self_destruct" || move.UseMove.Name == "explosion")
{
fail = true;
}
}
}

View File

@ -0,0 +1,13 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Dancer is an ability that allows the Pokémon to copy any dance move used by any Pokémon on the field.
/// When a dance move is used, the Pokémon with Dancer will immediately use the same move after the original user.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Dancer_(Ability)">Bulbapedia - Dancer</see>
/// </summary>
[Script(ScriptCategory.Ability, "dancer")]
public class Dancer : Script
{
// TODO: Implement Dancer ability logic
}

View File

@ -0,0 +1,35 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Dark Aura is an ability that increases the power of Dark-type moves by 33% for all Pokémon on the field.
/// The effect can be modified by other abilities (such as Aura Break) via a custom script hook.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Dark_Aura_(Ability)">Bulbapedia - Dark Aura</see>
/// </summary>
[Script(ScriptCategory.Ability, "dark_aura")]
public class DarkAura : Script
{
/// <inheritdoc />
public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
{
if (move.GetHitData(target, hit).Type?.Name == "dark")
{
var auraModifier = 5448f / 4096f;
var parameters = new Dictionary<StringKey, object?>
{
["aura_type"] = "dark",
["modifier"] = auraModifier,
};
move.Battle.Sides.SelectMany(side => side.Pokemon).WhereNotNull()
.RunScriptHook(x => x.CustomTrigger(CustomTriggers.ModifyAuraEffect, parameters));
if (parameters.TryGetValue("modifier", out var modObj) && modObj is float modValue)
{
auraModifier = modValue;
}
modifier *= auraModifier;
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User));
}
}
}

View File

@ -0,0 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Dazzling is an ability that prevents opposing Pokémon from using moves with increased priority.
/// This includes moves like Quick Attack, Aqua Jet, and Extreme Speed.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Dazzling_(Ability)">Bulbapedia - Dazzling</see>
/// </summary>
[Script(ScriptCategory.Ability, "dazzling")]
public class Dazzling : Script
{
/// <inheritdoc />
public override void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail)
{
if (move.UseMove.Priority > 0)
{
fail = true;
}
}
}

View File

@ -0,0 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Defeatist is an ability that halves the Pokémon's Attack and Special Attack when its HP drops below 50%.
/// This reduction applies to all moves used by the Pokémon, making it significantly weaker when damaged.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Defeatist_(Ability)">Bulbapedia - Defeatist</see>
/// </summary>
[Script(ScriptCategory.Ability, "defeatist")]
public class Defeatist : Script
{
/// <inheritdoc />
public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
{
if (move.User.CurrentHealth < move.User.MaxHealth / 2)
{
modifier *= 0.5f;
}
}
}

View File

@ -0,0 +1,27 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Defiant is an ability that raises the Pokémon's Attack by two stages whenever any of its stats are lowered.
/// This includes stat drops from moves, abilities, or items. The ability will not activate if the stat drop is self-inflicted.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Defiant_(Ability)">Bulbapedia - Defiant</see>
/// </summary>
[Script(ScriptCategory.Ability, "defiant")]
public class Defiant : Script
{
/// <inheritdoc />
/// <inheritdoc />
public override void OnAfterStatBoostChange(IPokemon pokemon, Statistic stat, bool selfInflicted, sbyte change)
{
if (change >= 0)
return;
if (selfInflicted)
return;
EventBatchId batchId = new();
pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon)
{
BatchId = batchId,
});
pokemon.ChangeStatBoost(Statistic.Attack, 2, true, batchId);
}
}

View File

@ -18,7 +18,7 @@ public class SpeedModifierInWeather : Script
throw new ArgumentException("Parameter 'weather' is required and must be a string.", nameof(parameters)); throw new ArgumentException("Parameter 'weather' is required and must be a string.", nameof(parameters));
if (!parameters.TryGetValue("modifier", out var modifierObj)) if (!parameters.TryGetValue("modifier", out var modifierObj))
throw new ArgumentException("Parameter 'modifier' is required", nameof(parameters)); throw new ArgumentException("Parameter 'modifier' is required", nameof(parameters));
if (modifierObj is not float modifier && modifierObj is not int) if (modifierObj is not float && modifierObj is not int)
throw new ArgumentException("Parameter 'modifier' must be a float or int.", nameof(parameters)); throw new ArgumentException("Parameter 'modifier' must be a float or int.", nameof(parameters));
var weatherModifier = modifierObj is float modFloat ? modFloat : (int)modifierObj; var weatherModifier = modifierObj is float modFloat ? modFloat : (int)modifierObj;

View File

@ -19,4 +19,6 @@ public static class CustomTriggers
public static readonly StringKey BypassSleep = "bypass_sleep"; public static readonly StringKey BypassSleep = "bypass_sleep";
public static readonly StringKey Whirlpool = "whirlpool"; public static readonly StringKey Whirlpool = "whirlpool";
public static readonly StringKey ModifyAuraEffect = "modify_aura_effect";
} }

View File

@ -1,5 +1,4 @@
using PkmnLib.Static.Moves; using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Status; namespace PkmnLib.Plugin.Gen7.Scripts.Status;