More abilities, implemented support for form inheritance
All checks were successful
Build / Build (push) Successful in 49s

This commit is contained in:
Deukhoofd 2025-06-13 12:24:03 +02:00
parent 6d71de375e
commit 8363b955af
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
16 changed files with 504 additions and 11 deletions

View File

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -111,11 +112,40 @@ public class SerializedForm
/// </summary> /// </summary>
public bool IsBattleOnly { get; set; } public bool IsBattleOnly { get; set; }
public string? InheritFrom { get; set; }
/// <summary> /// <summary>
/// Additional data that is not part of the standard form data. /// Additional data that is not part of the standard form data.
/// </summary> /// </summary>
[JsonExtensionData] [JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; } public Dictionary<string, JsonElement>? ExtensionData { get; set; }
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract"),
SuppressMessage("ReSharper", "NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract")]
internal void MakeInheritFrom(SerializedForm other)
{
Abilities ??= other.Abilities.ToArray();
HiddenAbilities ??= other.HiddenAbilities.ToArray();
BaseStats ??= other.BaseStats.Copy();
EVReward ??= other.EVReward.Copy();
Types ??= other.Types.ToArray();
if (Height == 0)
Height = other.Height;
if (Weight == 0)
Weight = other.Weight;
if (BaseExp == 0)
BaseExp = other.BaseExp;
Moves ??= new SerializedMoves();
if (Moves.LevelMoves == null || Moves.LevelMoves.Length == 0)
Moves.LevelMoves = other.Moves.LevelMoves?.ToArray();
if (Moves.EggMoves == null || Moves.EggMoves.Length == 0)
Moves.EggMoves = other.Moves.EggMoves?.ToArray();
if (Moves.TutorMoves == null || Moves.TutorMoves.Length == 0)
Moves.TutorMoves = other.Moves.TutorMoves?.ToArray();
if (Moves.Machine == null || Moves.Machine.Length == 0)
Moves.Machine = other.Moves.Machine?.ToArray();
Flags ??= other.Flags.ToArray();
}
} }
/// <summary> /// <summary>
@ -159,6 +189,17 @@ public record SerializedStats
/// <inheritdoc cref="PkmnLib.Static.ImmutableStatisticSet{T}.Speed"/> /// <inheritdoc cref="PkmnLib.Static.ImmutableStatisticSet{T}.Speed"/>
public ushort Speed { get; set; } public ushort Speed { get; set; }
public SerializedStats Copy() =>
new()
{
Hp = Hp,
Attack = Attack,
Defense = Defense,
SpecialAttack = SpecialAttack,
SpecialDefense = SpecialDefense,
Speed = Speed,
};
} }
/// <summary> /// <summary>

View File

@ -76,6 +76,20 @@ public static class SpeciesDataLoader
$"Egg cycles for species {id} is invalid: {serialized.EggCycles}. Must be greater than or equal to 0."); $"Egg cycles for species {id} is invalid: {serialized.EggCycles}. Must be greater than or equal to 0.");
} }
foreach (var form in serialized.Formes)
{
if (!string.IsNullOrEmpty(form.Value.InheritFrom))
{
var inheritedForm = serialized.Formes.GetValueOrDefault(form.Value.InheritFrom);
if (inheritedForm == null)
{
throw new InvalidDataException(
$"Form {form.Key} inherits from {form.Value.InheritFrom}, but that form does not exist.");
}
form.Value.MakeInheritFrom(inheritedForm);
}
}
var forms = serialized.Formes.ToDictionary(x => (StringKey)x.Key, var forms = serialized.Formes.ToDictionary(x => (StringKey)x.Key,
x => DeserializeForm(x.Key, x.Value, typeLibrary)); x => DeserializeForm(x.Key, x.Value, typeLibrary));
var evolutions = serialized.Evolutions.Select(DeserializeEvolution).ToList(); var evolutions = serialized.Evolutions.Select(DeserializeEvolution).ToList();

View File

@ -784,6 +784,7 @@ public class PokemonImpl : ScriptSource, IPokemon
{ {
var previous = HeldItem; var previous = HeldItem;
HeldItem = item; HeldItem = item;
this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, item));
return previous; return previous;
} }
@ -799,6 +800,7 @@ public class PokemonImpl : ScriptSource, IPokemon
} }
var previous = HeldItem; var previous = HeldItem;
HeldItem = null; HeldItem = null;
this.RunScriptHook(x => x.OnAfterHeldItemChange(this, previous, null));
return previous; return previous;
} }

View File

@ -419,7 +419,7 @@ public abstract class Script : IDeepCloneable
} }
/// <summary> /// <summary>
/// This function triggers when an opponent on the field faints. /// This function triggers when an opponent on the field faints due to the move that is being executed.
/// </summary> /// </summary>
public virtual void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit) public virtual void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit)
{ {
@ -794,4 +794,11 @@ public abstract class Script : IDeepCloneable
ref bool isContact) ref bool isContact)
{ {
} }
/// <summary>
/// This function allows a script to run after a held item has changed.
/// </summary>
public virtual void OnAfterHeldItemChange(IPokemon pokemon, IItem? previous, IItem? item)
{
}
} }

View File

@ -35,4 +35,43 @@ public class SpeciesDataloaderTests
var library = SpeciesDataLoader.LoadSpecies(file, typeLibrary); var library = SpeciesDataLoader.LoadSpecies(file, typeLibrary);
await Assert.That(library).IsNotNull(); await Assert.That(library).IsNotNull();
} }
[Test]
public async Task TestPrimarySpeciesFileFormInheritance()
{
IResourceProvider plugin = new Plugin.Gen7.Gen7Plugin();
var result = plugin.GetResource(ResourceFileType.Species)!;
await using var file = result.Open();
var typeLibrary = new TypeLibrary();
typeLibrary.RegisterType("Normal");
typeLibrary.RegisterType("Fire");
typeLibrary.RegisterType("Water");
typeLibrary.RegisterType("Electric");
typeLibrary.RegisterType("Grass");
typeLibrary.RegisterType("Ice");
typeLibrary.RegisterType("Fighting");
typeLibrary.RegisterType("Poison");
typeLibrary.RegisterType("Ground");
typeLibrary.RegisterType("Flying");
typeLibrary.RegisterType("Psychic");
typeLibrary.RegisterType("Bug");
typeLibrary.RegisterType("Rock");
typeLibrary.RegisterType("Ghost");
typeLibrary.RegisterType("Dragon");
typeLibrary.RegisterType("Dark");
typeLibrary.RegisterType("Steel");
typeLibrary.RegisterType("Fairy");
var library = SpeciesDataLoader.LoadSpecies(file, typeLibrary);
await Assert.That(library).IsNotNull();
await Assert.That(library.TryGet("arceus", out var species)).IsTrue();
await Assert.That(species).IsNotNull();
await Assert.That(species!.TryGetForm("arceus_fighting", out var form)).IsTrue();
await Assert.That(form).IsNotNull();
await Assert.That(form!.Types).HasCount().EqualTo(1);
await Assert.That(form.Types[0].Name).IsEqualTo("fighting");
await Assert.That(form.Flags).IsEqualTo(species.GetDefaultForm().Flags);
await Assert.That(form.BaseStats).IsEqualTo(species.GetDefaultForm().BaseStats);
}
} }

View File

@ -369,17 +369,34 @@
"misty_surge": { "misty_surge": {
"effect": "misty_surge" "effect": "misty_surge"
}, },
"mold_breaker": {}, "mold_breaker": {
"moody": {}, "effect": "mold_breaker"
"motor_drive": {}, },
"moxie": {}, "moody": {
"multiscale": {}, "effect": "moody"
"multitype": { },
"canBeChanged": false "motor_drive": {
"effect": "motor_drive"
},
"moxie": {
"effect": "moxie"
},
"multiscale": {
"effect": "multiscale"
},
"multitype": {
"canBeChanged": false,
"effect": "multitype"
},
"mummy": {
"effect": "mummy"
},
"natural_cure": {
"effect": "natural_cure"
},
"no_guard": {
"effect": "no_guard"
}, },
"mummy": {},
"natural_cure": {},
"no_guard": {},
"normalize": {}, "normalize": {},
"oblivious": {}, "oblivious": {},
"overcoat": {}, "overcoat": {},

View File

@ -4450,6 +4450,108 @@
], ],
"formeChange": [] "formeChange": []
} }
},
"arceus_fighting": {
"inheritFrom": "default",
"types": [
"fighting"
]
},
"arceus_flying": {
"inheritFrom": "default",
"types": [
"flying"
]
},
"arceus_bug": {
"inheritFrom": "default",
"types": [
"bug"
]
},
"arceus_dark": {
"inheritFrom": "default",
"types": [
"dark"
]
},
"arceus_dragon": {
"inheritFrom": "default",
"types": [
"dragon"
]
},
"arceus_electric": {
"inheritFrom": "default",
"types": [
"electric"
]
},
"arceus_fairy": {
"inheritFrom": "default",
"types": [
"fairy"
]
},
"arceus_fire": {
"inheritFrom": "default",
"types": [
"fire"
]
},
"arceus_ghost": {
"inheritFrom": "default",
"types": [
"ghost"
]
},
"arceus_grass": {
"inheritFrom": "default",
"types": [
"grass"
]
},
"arceus_ground": {
"inheritFrom": "default",
"types": [
"ground"
]
},
"arceus_ice": {
"inheritFrom": "default",
"types": [
"ice"
]
},
"arceus_poison": {
"inheritFrom": "default",
"types": [
"poison"
]
},
"arceus_psychic": {
"inheritFrom": "default",
"types": [
"psychic"
]
},
"arceus_rock": {
"inheritFrom": "default",
"types": [
"rock"
]
},
"arceus_steel": {
"inheritFrom": "default",
"types": [
"steel"
]
},
"arceus_water": {
"inheritFrom": "default",
"types": [
"water"
]
} }
}, },
"evolutions": [] "evolutions": []

View File

@ -0,0 +1,17 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Mold Breaker is an ability that allows moves to ignore the target's abilities.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Mold_Breaker_(Ability)">Bulbapedia - Mold Breaker</see>
/// </summary>
[Script(ScriptCategory.Ability, "mold_breaker")]
public class MoldBreaker : Script
{
/// <inheritdoc />
public override void OnBeforeAnyHookInvoked(ref List<ScriptCategory>? suppressedCategories)
{
suppressedCategories ??= [];
suppressedCategories.Add(ScriptCategory.Ability);
}
}

View File

@ -0,0 +1,55 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Moody is an ability that raises one stat and lowers another at the end of each turn.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Moody_(Ability)">Bulbapedia - Moody</see>
/// </summary>
[Script(ScriptCategory.Ability, "moody")]
public class Moody : Script
{
private IPokemon? _pokemon;
/// <inheritdoc />
public override void OnAddedToParent(IScriptSource source)
{
if (source is not IPokemon pokemon)
throw new InvalidOperationException("Moody script must be attached to a Pokemon.");
_pokemon = pokemon;
}
/// <inheritdoc />
public override void OnEndTurn(IBattle battle)
{
if (_pokemon == null)
return;
var stats = _pokemon.StatBoost;
Statistic? raiseStat = null;
var possibleStatsToRaise = stats.Where(x => x is
{ value: < 6, statistic: not Statistic.Accuracy and not Statistic.Evasion and not Statistic.Hp })
.Select(x => x.statistic).ToList();
if (possibleStatsToRaise.Count > 0)
{
raiseStat = possibleStatsToRaise[battle.Random.GetInt(possibleStatsToRaise.Count)];
}
Statistic? lowerStat = null;
var possibleStatsToLower = stats.Where(x => x is
{
value: > -6,
statistic: not Statistic.Accuracy and not Statistic.Evasion and not Statistic.Hp,
} && x.statistic != raiseStat).Select(x => x.statistic).ToList();
if (possibleStatsToLower.Count > 0)
{
lowerStat = possibleStatsToLower[battle.Random.GetInt(possibleStatsToLower.Count)];
}
if (raiseStat != null)
_pokemon.ChangeStatBoost(raiseStat.Value, 1, true, false);
if (lowerStat != null)
_pokemon.ChangeStatBoost(lowerStat.Value, -1, true, false);
}
}

View File

@ -0,0 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Motor Drive is an ability that grants immunity to Electric-type moves and raises Speed when hit by one.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Motor_Drive_(Ability)">Bulbapedia - Motor Drive</see>
/// </summary>
[Script(ScriptCategory.Ability, "motor_drive")]
public class MotorDrive : Script
{
/// <inheritdoc />
public override void IsInvulnerableToMove(IExecutingMove move, IPokemon target, ref bool invulnerable)
{
if (move.UseMove.MoveType.Name != "electric")
return;
invulnerable = true;
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(target));
target.ChangeStatBoost(Statistic.Speed, 1, true, false);
}
}

View File

@ -0,0 +1,16 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Moxie is an ability that raises the user's Attack stat after knocking out a Pokémon.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Moxie_(Ability)">Bulbapedia - Moxie</see>
/// </summary>
[Script(ScriptCategory.Ability, "moxie")]
public class Moxie : Script
{
/// <inheritdoc />
public override void OnOpponentFaints(IExecutingMove move, IPokemon target, byte hit)
{
move.User.ChangeStatBoost(Statistic.Attack, 1, true, false);
}
}

View File

@ -0,0 +1,19 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Multiscale is an ability that reduces damage taken when at full HP.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Multiscale_(Ability)">Bulbapedia - Multiscale</see>
/// </summary>
[Script(ScriptCategory.Ability, "multiscale")]
public class Multiscale : Script
{
/// <inheritdoc />
public override void ChangeIncomingMoveDamage(IExecutingMove move, IPokemon target, byte hit, ref uint damage)
{
if (target.CurrentHealth == target.BoostedStats.Hp)
{
damage = (uint)(damage * 0.5);
}
}
}

View File

@ -0,0 +1,79 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Multitype is an ability that changes the Pokémon's type based on its held Plate or Z-Crystal.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Multitype_(Ability)">Bulbapedia - Multitype</see>
/// </summary>
[Script(ScriptCategory.Ability, "multitype")]
public class Multitype : Script
{
/// <inheritdoc />
public override void OnAfterHeldItemChange(IPokemon pokemon, IItem? previous, IItem? item)
{
if (pokemon.Species.Name != "arceus")
return;
if (item is null && pokemon.Form.Name != "default")
{
pokemon.ChangeForm(pokemon.Species.GetDefaultForm());
}
else if (item is not null && item.Name.ToString().EndsWith("_plate", StringComparison.OrdinalIgnoreCase))
{
var platePrefix = item.Name.ToString().Replace("_plate", string.Empty, StringComparison.OrdinalIgnoreCase);
switch (platePrefix)
{
case "fist" when pokemon.Species.TryGetForm("arceus_fighting", out var fightingForm):
pokemon.ChangeForm(fightingForm);
break;
case "flame" when pokemon.Species.TryGetForm("arceus_fire", out var fireForm):
pokemon.ChangeForm(fireForm);
break;
case "shock" when pokemon.Species.TryGetForm("arceus_electric", out var electricForm):
pokemon.ChangeForm(electricForm);
break;
case "draco" when pokemon.Species.TryGetForm("arceus_dragon", out var dragonForm):
pokemon.ChangeForm(dragonForm);
break;
case "dread" when pokemon.Species.TryGetForm("arceus_dark", out var darkForm):
pokemon.ChangeForm(darkForm);
break;
case "earth" when pokemon.Species.TryGetForm("arceus_ground", out var groundForm):
pokemon.ChangeForm(groundForm);
break;
case "icicle" when pokemon.Species.TryGetForm("arceus_ice", out var iceForm):
pokemon.ChangeForm(iceForm);
break;
case "insect" when pokemon.Species.TryGetForm("arceus_bug", out var bugForm):
pokemon.ChangeForm(bugForm);
break;
case "iron" when pokemon.Species.TryGetForm("arceus_steel", out var steelForm):
pokemon.ChangeForm(steelForm);
break;
case "meadow" when pokemon.Species.TryGetForm("arceus_grass", out var grassForm):
pokemon.ChangeForm(grassForm);
break;
case "mind" when pokemon.Species.TryGetForm("arceus_psychic", out var psychicForm):
pokemon.ChangeForm(psychicForm);
break;
case "pixie" when pokemon.Species.TryGetForm("arceus_fairy", out var fairyForm):
pokemon.ChangeForm(fairyForm);
break;
case "sky" when pokemon.Species.TryGetForm("arceus_flying", out var flyingForm):
pokemon.ChangeForm(flyingForm);
break;
case "splash" when pokemon.Species.TryGetForm("arceus_water", out var waterForm):
pokemon.ChangeForm(waterForm);
break;
case "spooky" when pokemon.Species.TryGetForm("arceus_ghost", out var ghostForm):
pokemon.ChangeForm(ghostForm);
break;
case "stone" when pokemon.Species.TryGetForm("arceus_rock", out var rockForm):
pokemon.ChangeForm(rockForm);
break;
case "toxic" when pokemon.Species.TryGetForm("arceus_poison", out var poisonForm):
pokemon.ChangeForm(poisonForm);
break;
}
}
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Mummy is an ability that changes the attacker's ability to Mummy if it makes contact.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Mummy_(Ability)">Bulbapedia - Mummy</see>
/// </summary>
[Script(ScriptCategory.Ability, "mummy")]
public class Mummy : Script
{
/// <inheritdoc />
public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
if (!move.GetHitData(target, hit).IsContact || move.User.ActiveAbility?.Name == "mummy" ||
!move.Battle.Library.StaticLibrary.Abilities.TryGet("mummy", out var mummyAbility))
return;
move.Battle.EventHook.Invoke(new AbilityTriggerEvent(target));
move.User.ChangeAbility(mummyAbility);
}
}

View File

@ -0,0 +1,20 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// Natural Cure is an ability that heals status conditions when switching out.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/Natural_Cure_(Ability)">Bulbapedia - Natural Cure</see>
/// </summary>
[Script(ScriptCategory.Ability, "natural_cure")]
public class NaturalCure : Script
{
/// <inheritdoc />
public override void OnSwitchOut(IPokemon oldPokemon, byte position)
{
if (!oldPokemon.StatusScript.IsEmpty)
{
oldPokemon.BattleData?.Battle.EventHook.Invoke(new AbilityTriggerEvent(oldPokemon));
oldPokemon.ClearStatus();
}
}
}

View File

@ -0,0 +1,24 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Abilities;
/// <summary>
/// No Guard is an ability that ensures all moves used by and against the Pokémon hit without fail.
///
/// <see href="https://bulbapedia.bulbagarden.net/wiki/No_Guard_(Ability)">Bulbapedia - No Guard</see>
/// </summary>
[Script(ScriptCategory.Ability, "no_guard")]
public class NoGuard : Script
{
/// <inheritdoc />
public override void ChangeIncomingAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex,
ref int modifiedAccuracy)
{
modifiedAccuracy = 2000;
}
/// <inheritdoc />
public override void ChangeAccuracy(IExecutingMove executingMove, IPokemon target, byte hitIndex,
ref int modifiedAccuracy)
{
modifiedAccuracy = 2000;
}
}