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 ability = new AbilityImpl(name, effectName, parameters, flags);
var ability = new AbilityImpl(name, effectName, parameters, flags, serialized.CanBeChanged ?? true);
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.
/// </summary>
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.SetOnBattlefield(true);
pokemon.SetBattleSidePosition(position);
Battle.EventHook.Invoke(new SwitchEvent(Index, position, pokemon));
pokemon.RunScriptHook(script => script.OnSwitchIn(pokemon, position));
foreach (var side in Battle.Sides)
{
if (side == this)
continue;
var scripts = new List<IEnumerable<ScriptContainer>>(10);
foreach (var opponent in side.Pokemon.WhereNotNull())
{
opponent.MarkOpponentAsSeen(pokemon);
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
{

View File

@ -46,6 +46,16 @@ public interface IHitData
/// Fails the hit.
/// </summary>
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 />
@ -71,6 +81,18 @@ public record HitData : IHitData
/// <inheritdoc />
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>

View File

@ -401,7 +401,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// <summary>
/// Changes the ability of the Pokémon.
/// </summary>
void ChangeAbility(IAbility ability);
bool ChangeAbility(IAbility ability);
/// <summary>
/// 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 />
public void ChangeAbility(IAbility ability)
public bool ChangeAbility(IAbility ability)
{
if (!ability.CanBeChanged)
return false;
OverrideAbility = ability;
if (Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ability.Name, ability.Parameters,
out var abilityScript))
@ -1210,6 +1212,7 @@ public class PokemonImpl : ScriptSource, IPokemon
{
AbilityScript.Clear();
}
return true;
}
/// <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>
/// This function is triggered on a Pokemon and its parents when the given Pokemon consumes the
/// held item it had.

View File

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

View File

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

View File

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