First couple abilities implemented
All checks were successful
Build / Build (push) Successful in 48s

This commit is contained in:
Deukhoofd 2025-05-31 12:29:03 +02:00
parent c1a7b806b1
commit b090aa65f9
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
34 changed files with 733 additions and 50 deletions

View File

@ -263,6 +263,8 @@ public static class MoveTurnExecutor
} }
} }
} }
if (target.IsFainted)
executingMove.RunScriptHook(x => x.OnOpponentFaints(executingMove, target, hitIndex));
} }
} }
} }

View File

@ -0,0 +1,20 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Events;
public record AbilityTriggerEvent : IEventData
{
public IPokemon Pokemon { get; }
public IAbility? Ability { get; }
public AbilityTriggerEvent(IPokemon pokemon)
{
Pokemon = pokemon;
Ability = pokemon.ActiveAbility;
}
/// <inheritdoc />
public EventBatchId BatchId { get; init; }
}

View File

@ -59,7 +59,7 @@ public static class AbilityDataLoader
var flags = serialized.Flags.Select(x => new StringKey(x)).ToImmutableHashSet(); var flags = serialized.Flags.Select(x => new StringKey(x)).ToImmutableHashSet();
var ability = new AbilityImpl(name, effectName, parameters, flags); var ability = new AbilityImpl(name, effectName, parameters, flags, serialized.CanBeChanged ?? true);
return ability; return ability;
} }
} }

View File

@ -19,4 +19,6 @@ public class SerializedAbility
/// A collection of arbitrary flags that can be used to mark the ability with specific properties. /// A collection of arbitrary flags that can be used to mark the ability with specific properties.
/// </summary> /// </summary>
public string[] Flags { get; set; } = []; public string[] Flags { get; set; } = [];
public bool? CanBeChanged { get; set; }
} }

View File

@ -271,18 +271,25 @@ public class BattleSideImpl : ScriptSource, IBattleSide
pokemon.SetBattleData(Battle, Index); pokemon.SetBattleData(Battle, Index);
pokemon.SetOnBattlefield(true); pokemon.SetOnBattlefield(true);
pokemon.SetBattleSidePosition(position); pokemon.SetBattleSidePosition(position);
Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon));
pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position));
foreach (var side in Battle.Sides) foreach (var side in Battle.Sides)
{ {
if (side == this) if (side == this)
continue; continue;
var scripts = new List<IEnumerable<ScriptContainer>>(10);
foreach (var opponent in side.Pokemon.WhereNotNull()) foreach (var opponent in side.Pokemon.WhereNotNull())
{ {
opponent.MarkOpponentAsSeen(pokemon); opponent.MarkOpponentAsSeen(pokemon);
pokemon.MarkOpponentAsSeen(opponent); pokemon.MarkOpponentAsSeen(opponent);
scripts.Clear();
opponent.GetOwnScripts(scripts);
opponent.RunScriptHook(script => script.OnOpponentSwitchIn(pokemon, position));
} }
side.RunScriptHook(script => script.OnOpponentSwitchIn(pokemon, position));
} }
Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon));
pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position));
} }
else else
{ {

View File

@ -46,6 +46,16 @@ public interface IHitData
/// Fails the hit. /// Fails the hit.
/// </summary> /// </summary>
void Fail(); void Fail();
/// <summary>
/// Sets a flag on the hit data. This is used to mark certain conditions or states
/// </summary>
void SetFlag(StringKey flag);
/// <summary>
/// Checks whether a flag is set on the hit data.
/// </summary>
bool HasFlag(StringKey flag);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -71,6 +81,18 @@ public record HitData : IHitData
/// <inheritdoc /> /// <inheritdoc />
public void Fail() => HasFailed = true; public void Fail() => HasFailed = true;
private HashSet<StringKey>? _flags;
/// <inheritdoc />
public void SetFlag(StringKey flag)
{
_flags ??= [];
_flags.Add(flag);
}
/// <inheritdoc />
public bool HasFlag(StringKey flag) => _flags != null && _flags.Contains(flag);
} }
/// <summary> /// <summary>

View File

@ -401,7 +401,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// <summary> /// <summary>
/// Changes the ability of the Pokémon. /// Changes the ability of the Pokémon.
/// </summary> /// </summary>
void ChangeAbility(IAbility ability); bool ChangeAbility(IAbility ability);
/// <summary> /// <summary>
/// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as /// Whether the Pokémon is levitating. This is used for moves like Magnet Rise, and abilities such as
@ -1197,8 +1197,10 @@ public class PokemonImpl : ScriptSource, IPokemon
} }
/// <inheritdoc /> /// <inheritdoc />
public void ChangeAbility(IAbility ability) public bool ChangeAbility(IAbility ability)
{ {
if (!ability.CanBeChanged)
return false;
OverrideAbility = ability; OverrideAbility = ability;
if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters, if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters,
out var abilityScript)) out var abilityScript))
@ -1210,6 +1212,7 @@ public class PokemonImpl : ScriptSource, IPokemon
{ {
AbilityScript.Clear(); AbilityScript.Clear();
} }
return true;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -546,6 +546,13 @@ public abstract class Script : IDeepCloneable
{ {
} }
/// <summary>
/// This function is triggered on a Pokemon and its parents when an opponent switches in.
/// </summary>
public virtual void OnOpponentSwitchIn(IPokemon pokemon, byte position)
{
}
/// <summary> /// <summary>
/// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the /// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the
/// held item it had. /// held item it had.

View File

@ -34,10 +34,7 @@ public class ScriptResolver
} }
script = scriptCtor(); script = scriptCtor();
if (parameters != null) script.OnInitialize(parameters);
{
script.OnInitialize(parameters);
}
return true; return true;
} }

View File

@ -23,6 +23,8 @@ public interface IAbility : INamedValue
/// Checks whether the ability has a specific flag. /// Checks whether the ability has a specific flag.
/// </summary> /// </summary>
bool HasFlag(StringKey key); bool HasFlag(StringKey key);
bool CanBeChanged { get; }
} }
/// <inheritdoc /> /// <inheritdoc />
@ -30,12 +32,13 @@ public class AbilityImpl : IAbility
{ {
/// <inheritdoc cref="AbilityImpl" /> /// <inheritdoc cref="AbilityImpl" />
public AbilityImpl(StringKey name, StringKey? effect, IReadOnlyDictionary<StringKey, object?> parameters, public AbilityImpl(StringKey name, StringKey? effect, IReadOnlyDictionary<StringKey, object?> parameters,
ImmutableHashSet<StringKey> flags) ImmutableHashSet<StringKey> flags, bool canBeChanged)
{ {
Name = name; Name = name;
Effect = effect; Effect = effect;
Parameters = parameters; Parameters = parameters;
Flags = flags; Flags = flags;
CanBeChanged = canBeChanged;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -54,6 +57,9 @@ public class AbilityImpl : IAbility
/// <inheritdoc /> /// <inheritdoc />
public bool HasFlag(StringKey key) => Flags.Contains(key); public bool HasFlag(StringKey key) => Flags.Contains(key);
/// <inheritdoc />
public bool CanBeChanged { get; }
} }
/// <summary> /// <summary>

View File

@ -5,6 +5,12 @@ namespace PkmnLib.Static.Utils;
/// </summary> /// </summary>
public static class NumericHelpers public static class NumericHelpers
{ {
/// <summary>
/// Checks if two floating-point values are approximately equal within a specified tolerance.
/// </summary>
public static bool IsApproximatelyEqualTo(this float value, float other, float tolerance = 0.0001f) =>
MathF.Abs(value - other) <= tolerance;
/// <summary> /// <summary>
/// Multiplies two values. If this overflows, returns <see cref="byte.MaxValue"/>. /// Multiplies two values. If this overflows, returns <see cref="byte.MaxValue"/>.
/// </summary> /// </summary>

View File

@ -0,0 +1,56 @@
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static.Species;
namespace PkmnLib.Plugin.Gen7.Tests.DataTests;
public class AbilityDataTests
{
public record TestCaseData(IDynamicLibrary Library, IAbility Ability)
{
/// <inheritdoc />
public override string ToString() => Ability.Name + " has valid scripts";
}
public static IEnumerable<Func<TestCaseData>> AllAbilitiesHaveValidScriptsData()
{
var library = LibraryHelpers.LoadLibrary();
var abilityLibrary = library.StaticLibrary.Abilities;
foreach (var ability in abilityLibrary)
{
if (ability.Effect is null)
continue;
yield return () => new TestCaseData(library, ability);
}
}
[Test, MethodDataSource(nameof(AllAbilitiesHaveValidScriptsData)), Explicit]
public async Task AllAbilitiesEffectsHaveValidScripts(TestCaseData test)
{
var scriptName = test.Ability.Effect;
if (scriptName is null)
return;
try
{
await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Ability, scriptName.Value,
test.Ability.Parameters, out _)).IsTrue();
}
catch (Exception e)
{
// Helper method to find the line number of the effect in the JSON file
var file = Path.GetFullPath("../../../../Plugins/PkmnLib.Plugin.Gen7/Data/Abilities.json");
var json = await File.ReadAllLinesAsync(file);
var moveLineNumber = json.Select((line, index) => new { line, index })
.FirstOrDefault(x => x.line.Contains($"\"name\": \"{test.Ability.Effect}\""))?.index + 1;
var effectLineNumber = moveLineNumber + json.Skip(moveLineNumber ?? 0)
.Select((line, index) => new { line, index }).FirstOrDefault(x => x.line.Contains("effect"))
?.index +
1 ?? 0;
await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{file}:{effectLineNumber}");
throw new AggregateException($"Failed to resolve script for move {test.Ability} with effect {scriptName}",
e);
}
}
}

View File

@ -1,68 +1,75 @@
{ {
"adaptability": { "adaptability": {
"effect": "IncreasedStab" "effect": "increased_stab"
}, },
"aerilate": { "aerilate": {
"effect": "ChangeMoveType", "effect": "change_move_type",
"parameters": { "parameters": {
"from": "normal", "from_type": "normal",
"to": "flying" "to_type": "flying"
} }
}, },
"aftermath": { "aftermath": {
"effect": "Aftermath" "effect": "aftermath"
}, },
"air_lock": { "air_lock": {
"effect": "SuppressWeather" "effect": "suppress_weather"
}, },
"analytic": { "analytic": {
"effect": "Analytic" "effect": "analytic"
}, },
"anger_point": { "anger_point": {
"effect": "AngerPoint" "effect": "anger_point"
}, },
"anticipation": { "anticipation": {
"effect": "Anticipation" "effect": "anticipation"
}, },
"arena_trap": { "arena_trap": {
"effect": "ArenaTrap" "effect": "arena_trap"
}, },
"aroma_veil": { "aroma_veil": {
"effect": "AromaVeil" "effect": "aroma_veil"
}, },
"aura_break": { "aura_break": {
"effect": "AuraBreal" "effect": "aura_break"
}, },
"bad_dreams": { "bad_dreams": {
"effect": "BadDreams" "effect": "bad_dreams"
}, },
"battery": { "battery": {
"effect": "Battery" "effect": "battery"
}, },
"battle_armor": { "battle_armor": {
"effect": "PreventCritical" "effect": "prevent_critical"
}, },
"battle_bond": { "battle_bond": {
"effect": "BattleBond", "effect": "battle_bond",
"flags": ["cant_be_changed"] "canBeChanged": false,
"flags": [
"cant_be_copied"
]
}, },
"beast_boost": { "beast_boost": {
"effect": "BeastBoost" "effect": "beast_boost"
}, },
"berserk": { "berserk": {
"effect": "Berserk" "effect": "berserk"
}, },
"big_pecks": { "big_pecks": {
"effect": "PreventDefLowering" "effect": "prevent_stat_lowering",
"parameters": {
"stat": "defense"
}
}, },
"blaze": { "blaze": {
"effect": "PowerUpType", "effect": "power_up_type_at_low_health",
"parameters": { "parameters": {
"type": "fire" "type": "fire",
"threshold": 0.33333
} }
}, },
"bulletproof": { "bulletproof": {
"effect": "Bulletproof" "effect": "bulletproof"
}, },
"cheek_pouch": { "cheek_pouch": {
"effect": "CheekPouch" "effect": "CheekPouch"
@ -83,7 +90,7 @@
"effect": "ColorChange" "effect": "ColorChange"
}, },
"comatose": { "comatose": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"competitive": {}, "competitive": {},
"compound_eyes": {}, "compound_eyes": {},
@ -100,7 +107,10 @@
"delta_stream": {}, "delta_stream": {},
"desolate_land": {}, "desolate_land": {},
"disguise": { "disguise": {
"flags": ["cant_be_changed", "cant_be_copied"] "canBeChanged": false,
"flags": [
"cant_be_copied"
]
}, },
"download": {}, "download": {},
"drizzle": {}, "drizzle": {},
@ -116,12 +126,16 @@
"flare_boost": {}, "flare_boost": {},
"flash_fire": {}, "flash_fire": {},
"flower_gift": { "flower_gift": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"flower_veil": {}, "flower_veil": {},
"fluffy": {}, "fluffy": {},
"forecast": { "forecast": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"forewarn": {}, "forewarn": {},
"friend_guard": {}, "friend_guard": {},
@ -147,11 +161,15 @@
"ice_body": {}, "ice_body": {},
"illuminate": {}, "illuminate": {},
"illusion": { "illusion": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"immunity": {}, "immunity": {},
"imposter": { "imposter": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"infiltrator": {}, "infiltrator": {},
"innards_out": {}, "innards_out": {},
@ -187,7 +205,7 @@
"moxie": {}, "moxie": {},
"multiscale": {}, "multiscale": {},
"multitype": { "multitype": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"mummy": {}, "mummy": {},
"natural_cure": {}, "natural_cure": {},
@ -206,10 +224,15 @@
"poison_point": {}, "poison_point": {},
"poison_touch": {}, "poison_touch": {},
"power_construct": { "power_construct": {
"flags": ["cant_be_changed", "cant_be_copied"] "canBeChanged": false,
"flags": [
"cant_be_copied"
]
}, },
"power_of_alchemy": { "power_of_alchemy": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"prankster": {}, "prankster": {},
"pressure": {}, "pressure": {},
@ -223,14 +246,16 @@
"rain_dish": {}, "rain_dish": {},
"rattled": {}, "rattled": {},
"receiver": { "receiver": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"reckless": {}, "reckless": {},
"refrigerate": {}, "refrigerate": {},
"regenerator": {}, "regenerator": {},
"rivalry": {}, "rivalry": {},
"rks_system": { "rks_system": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"rock_head": {}, "rock_head": {},
"rough_skin": {}, "rough_skin": {},
@ -241,7 +266,7 @@
"sand_veil": {}, "sand_veil": {},
"sap_sipper": {}, "sap_sipper": {},
"schooling": { "schooling": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"scrappy": {}, "scrappy": {},
"serene_grace": {}, "serene_grace": {},
@ -252,7 +277,7 @@
"shell_armor": {}, "shell_armor": {},
"shield_dust": {}, "shield_dust": {},
"shields_down": { "shields_down": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"simple": {}, "simple": {},
"skill_link": {}, "skill_link": {},
@ -270,7 +295,7 @@
"stall": {}, "stall": {},
"stamina": {}, "stamina": {},
"stance_change": { "stance_change": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"static": {}, "static": {},
"steadfast": {}, "steadfast": {},
@ -299,11 +324,13 @@
"tough_claws": {}, "tough_claws": {},
"toxic_boost": {}, "toxic_boost": {},
"trace": { "trace": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
}, },
"triage": {}, "triage": {},
"truant": { "truant": {
"flags": ["cant_be_changed"] "canBeChanged": false
}, },
"turboblaze": {}, "turboblaze": {},
"unaware": {}, "unaware": {},
@ -322,6 +349,8 @@
"wonder_guard": {}, "wonder_guard": {},
"wonder_skin": {}, "wonder_skin": {},
"zen_mode": { "zen_mode": {
"flags": ["cant_be_copied"] "flags": [
"cant_be_copied"
]
} }
} }

View File

@ -0,0 +1,33 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "aftermath")]
public class Aftermath : Script
{
private IExecutingMove? _lastAttack;
/// <inheritdoc />
public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
_lastAttack = move;
}
/// <inheritdoc />
public override void OnFaint(IPokemon pokemon, DamageSource source)
{
if (source != DamageSource.MoveDamage)
return;
if (_lastAttack is null || !_lastAttack.UseMove.HasFlag("contact"))
return;
var user = _lastAttack.User;
if (!user.IsUsable)
return;
if (user.BattleData is null)
return;
EventBatchId eventBatchId = new();
user.BattleData.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon)
{
BatchId = eventBatchId,
});
user.Damage(user.MaxHealth / 4, DamageSource.Misc, eventBatchId);
}
}

View File

@ -0,0 +1,15 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "analytic")]
public class Analytic : Script
{
/// <inheritdoc />
public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
{
if (move.Battle.ChoiceQueue?.HasNext() == false)
{
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User));
modifier *= 1.3f;
}
}
}

View File

@ -0,0 +1,19 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "anger_point")]
public class AngerPoint : Script
{
/// <inheritdoc />
public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
if (move.GetHitData(target, hit).IsCritical)
{
EventBatchId batchId = new();
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(target)
{
BatchId = batchId,
});
target.ChangeStatBoost(Statistic.Attack, 12, true, batchId);
}
}
}

View File

@ -0,0 +1,39 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "anticipation")]
public class Anticipation : Script
{
private IPokemon? _owner;
/// <inheritdoc />
public override void OnAddedToParent(IScriptSource source)
{
if (source is not IPokemon pokemon)
throw new ArgumentException("Anticipation script can only be added to a Pokemon.", nameof(source));
_owner = pokemon;
}
/// <inheritdoc />
public override void OnOpponentSwitchIn(IPokemon pokemon, byte position)
{
if (_owner is null)
return;
var pokemonMoves = pokemon.Moves.WhereNotNull();
var typeLibrary = pokemon.Library.StaticLibrary.Types;
var relevantMoves = pokemonMoves.Any(move =>
// Either the move is super effective against the owner or
typeLibrary.GetEffectiveness(move.MoveData.MoveType, _owner.Types) > 1.0f ||
// the move is a OHKO move
move.MoveData.SecondaryEffect?.Name == "one_hit_ko" ||
// the move is a self-destruct move
move.MoveData.SecondaryEffect?.Name == "self_destruct" ||
move.MoveData.SecondaryEffect?.Name == "explosion");
if (relevantMoves)
{
pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(_owner));
}
}
}

View File

@ -0,0 +1,35 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "arena_trap")]
public class ArenaTrap : Script
{
private IPokemon? _owner;
/// <inheritdoc />
public override void OnAddedToParent(IScriptSource source)
{
if (source is not IPokemon pokemon)
throw new InvalidOperationException("ArenaTrap can only be added to a Pokemon.");
_owner = pokemon;
}
/// <inheritdoc />
public override void PreventOpponentRunAway(IFleeChoice choice, ref bool prevent)
{
if (choice.User.IsFloating)
return;
if (_owner is not null)
choice.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(_owner));
prevent = true;
}
/// <inheritdoc />
public override void PreventOpponentSwitch(ISwitchChoice choice, ref bool prevent)
{
if (choice.User.IsFloating)
return;
if (_owner is not null)
choice.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(_owner));
prevent = true;
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "aroma_veil")]
public class AromaVeil : Script
{
/// <inheritdoc />
public override void OnSwitchIn(IPokemon pokemon, byte position)
{
var side = pokemon.BattleData?.BattleSide;
var effect = side?.VolatileScripts.Add(new Side.AromaVeilEffect())?.Script as Side.AromaVeilEffect;
effect?.PlacerActivated(pokemon);
}
/// <inheritdoc />
public override void OnSwitchOut(IPokemon oldPokemon, byte position)
{
var side = oldPokemon.BattleData?.BattleSide;
var effect = side?.VolatileScripts.Get<Side.AromaVeilEffect>();
effect?.PlacerDeactivated(oldPokemon);
}
}

View File

@ -0,0 +1,7 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "aura_break")]
public class AuraBreak : Script
{
// FIXME: Implement together with Dark Aura and Fairy Aura.
}

View File

@ -0,0 +1,35 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "bad_dreams")]
public class BadDreams : Script
{
private IPokemon? _owner;
/// <inheritdoc />
public override void OnAddedToParent(IScriptSource source)
{
if (source is not IPokemon pokemon)
throw new InvalidOperationException("Bad Dreams ability can only be added to a Pokemon.");
_owner = pokemon;
}
/// <inheritdoc />
public override void OnEndTurn(IBattle battle)
{
if (_owner is null)
return;
var opponents = battle.Sides.Where(x => x != _owner?.BattleData?.BattleSide).SelectMany(x => x.Pokemon)
.WhereNotNull();
foreach (var opponent in opponents)
{
if (!opponent.HasStatus(ScriptUtils.ResolveName<Status.Sleep>()))
continue;
EventBatchId batchId = new();
battle.EventHook.Invoke(new AbilityTriggerEvent(_owner));
opponent.Damage(opponent.MaxHealth / 8, DamageSource.Misc, batchId);
}
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "battery")]
public class Battery : Script
{
/// <inheritdoc />
public override void OnSwitchIn(IPokemon pokemon, byte position)
{
var side = pokemon.BattleData?.BattleSide;
var effect = side?.VolatileScripts.Add(new Side.BatteryAbilityEffect())?.Script as Side.BatteryAbilityEffect;
effect?.PlacerActivated(pokemon);
}
/// <inheritdoc />
public override void OnSwitchOut(IPokemon oldPokemon, byte position)
{
var side = oldPokemon.BattleData?.BattleSide;
var effect = side?.VolatileScripts.Get<Side.BatteryAbilityEffect>();
effect?.PlacerDeactivated(oldPokemon);
}
}

View File

@ -0,0 +1,32 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "battle_bond")]
public class BattleBond : Script
{
/// <inheritdoc />
public override void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit)
{
if (move.User.Species.Name == "greninja" && move.User.Form.Name != "ash" &&
move.User.Species.TryGetForm("ash", out var ashForm))
{
move.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User));
move.User.ChangeForm(ashForm);
}
}
/// <inheritdoc />
public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
if (move.UseMove.Name == "water_shuriken" && move.User.Form.Name == "ash")
basePower = 20;
}
/// <inheritdoc />
public override void ChangeNumberOfHits(IMoveChoice choice, ref byte numberOfHits)
{
if (choice.ChosenMove.MoveData.Name == "water_shuriken" && choice.User.Form.Name == "ash")
{
numberOfHits = 3;
}
}
}

View File

@ -0,0 +1,17 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "beast_boost")]
public class BeastBoost : Script
{
/// <inheritdoc />
public override void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit)
{
var highestStat = move.User.BoostedStats.OrderByDescending(x => x.value).First().statistic;
EventBatchId batchId = new();
move.User.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User)
{
BatchId = batchId,
});
move.User.ChangeStatBoost(highestStat, 1, true, batchId);
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "berserk")]
public class Berserk : Script
{
/// <inheritdoc />
public override void OnDamage(IPokemon pokemon, DamageSource source, uint oldHealth, uint newHealth)
{
if (source is not DamageSource.MoveDamage)
return;
if (oldHealth > pokemon.MaxHealth / 2 || newHealth > pokemon.MaxHealth / 2)
return;
EventBatchId batchId = new();
pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon)
{
BatchId = batchId,
});
pokemon.ChangeStatBoost(Statistic.SpecialAttack, 1, true, batchId);
}
}

View File

@ -0,0 +1,12 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "bulletproof")]
public class Bulletproof : Script
{
/// <inheritdoc />
public override void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail)
{
if (move.UseMove.HasFlag("ballistics"))
fail = true;
}
}

View File

@ -0,0 +1,47 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "change_move_type")]
public class ChangeMoveTypeAbility : Script
{
private StringKey _fromType;
private StringKey _toType;
/// <inheritdoc />
public override void OnInitialize(IReadOnlyDictionary<StringKey, object?>? parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (!parameters.TryGetValue("from_type", out var fromTypeObj) || fromTypeObj is not string fromType)
throw new ArgumentException("Missing 'from_type' parameter.", nameof(parameters));
if (!parameters.TryGetValue("to_type", out var toTypeObj) || toTypeObj is not string toType)
throw new ArgumentException("Missing 'to_type' parameter.", nameof(parameters));
_fromType = fromType;
_toType = toType;
}
/// <inheritdoc />
public override void ChangeMoveType(IExecutingMove move, IPokemon target, byte hit,
ref TypeIdentifier? typeIdentifier)
{
var typeLibrary = target.Library.StaticLibrary.Types;
// Both types must be valid and the current type must match the from type
if (!typeLibrary.TryGetTypeIdentifier(_fromType, out var fromType) ||
!typeLibrary.TryGetTypeIdentifier(_toType, out var toType) || typeIdentifier != fromType)
{
return;
}
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(move.User));
typeIdentifier = toType;
move.GetHitData(target, hit).SetFlag("change_move_type_ability");
}
/// <inheritdoc />
public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref ushort basePower)
{
if (move.GetHitData(target, hit).HasFlag("change_move_type_ability"))
basePower = basePower.MultiplyOrMax(1.3f);
}
}

View File

@ -0,0 +1,18 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "increased_stab")]
public class IncreasedStab : Script
{
/// <inheritdoc />
public override void ChangeStabModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber,
ref float modifier)
{
if (modifier.IsApproximatelyEqualTo(1.5f))
{
executingMove.Battle.EventHook.Invoke(new AbilityTriggerEvent(executingMove.User));
modifier = 2.0f;
}
}
}

View File

@ -0,0 +1,38 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "power_up_type_at_low_health")]
public class PowerUpTypeAtLowHealth : Script
{
private StringKey _type;
private float _threshold;
/// <inheritdoc />
public override void OnInitialize(IReadOnlyDictionary<StringKey, object?>? parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (!parameters.TryGetValue("type", out var type) || type is not string typeName)
throw new ArgumentException("Parameter 'type' is required and must be a string.", nameof(parameters));
if (!parameters.TryGetValue("threshold", out var threshold) || threshold is not float thresholdValue)
throw new ArgumentException("Parameter 'threshold' is required and must be a float.", nameof(parameters));
if (thresholdValue < 0 || thresholdValue > 1)
throw new ArgumentOutOfRangeException(nameof(threshold), "Threshold must be between 0 and 1.");
_type = typeName;
_threshold = thresholdValue;
}
/// <inheritdoc />
public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
{
var currentHealthFraction = move.User.CurrentHealth / (float)move.User.MaxHealth;
if (currentHealthFraction <= _threshold && move.GetHitData(target, hit).Type?.Name == _type)
{
modifier *= 1.5f;
}
}
}

View File

@ -0,0 +1,9 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "prevent_critical")]
public class PreventCritical : Script
{
/// <inheritdoc />
public override void BlockIncomingCriticalHit(IExecutingMove move, IPokemon target, byte hit, ref bool block) =>
block = true;
}

View File

@ -0,0 +1,30 @@
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "prevent_stat_lowering")]
public class PreventStatLowering : Script
{
private Statistic _statistic;
/// <inheritdoc />
public override void OnInitialize(IReadOnlyDictionary<StringKey, object?>? parameters)
{
if (parameters is null)
throw new ArgumentNullException(nameof(parameters), "Parameters cannot be null.");
if (!parameters.TryGetValue("stat", out var statObj) || statObj is not string statStr)
throw new ArgumentException("Parameter 'stat' is required and must be a string.", nameof(parameters));
if (!Enum.TryParse(statStr, true, out Statistic stat))
throw new ArgumentException($"Invalid statistic '{statStr}' provided.", nameof(statStr));
_statistic = stat;
}
/// <inheritdoc />
public override void PreventStatBoostChange(IPokemon target, Statistic stat, sbyte amount, bool selfInflicted,
ref bool prevent)
{
if (!selfInflicted)
prevent = false;
}
}

View File

@ -0,0 +1,18 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
[Script(ScriptCategory.Ability, "suppress_weather")]
public class SuppressWeatherAbility : Script
{
/// <inheritdoc />
public override void OnBeforeAnyHookInvoked(ref List<ScriptCategory>? suppressedCategories)
{
suppressedCategories ??= [];
suppressedCategories.Add(ScriptCategory.Weather);
}
/// <inheritdoc />
public override void OnSwitchIn(IPokemon pokemon, byte position)
{
pokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(pokemon));
}
}

View File

@ -0,0 +1,32 @@
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "aroma_veil")]
public class AromaVeilEffect : Script
{
private HashSet<IPokemon> _placers = new();
public void PlacerActivated(IPokemon placer) => _placers.Add(placer);
public void PlacerDeactivated(IPokemon placer)
{
_placers.Remove(placer);
if (_placers.Count == 0)
RemoveSelf();
}
/// <inheritdoc />
public override void FailIncomingMove(IExecutingMove move, IPokemon target, ref bool fail)
{
if (move.UseMove.HasFlag("mental") && move.UseMove.Category == MoveCategory.Status)
fail = true;
}
/// <inheritdoc />
public override void PreventSecondaryEffect(IExecutingMove move, IPokemon target, byte hit, ref bool prevent)
{
if (move.UseMove.HasFlag("mental"))
prevent = true;
}
}

View File

@ -0,0 +1,27 @@
using PkmnLib.Static.Moves;
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "battery")]
public class BatteryAbilityEffect : Script
{
private HashSet<IPokemon> _placers = new();
public void PlacerActivated(IPokemon placer) => _placers.Add(placer);
public void PlacerDeactivated(IPokemon placer)
{
_placers.Remove(placer);
if (_placers.Count == 0)
RemoveSelf();
}
/// <inheritdoc />
public override void ChangeDamageModifier(IExecutingMove move, IPokemon target, byte hit, ref float modifier)
{
if (move.UseMove.Category == MoveCategory.Special)
{
modifier *= 5325f / 4096f; // ~1.3x
}
}
}