Serialization of Pokemon, general fixes

This commit is contained in:
Deukhoofd 2024-09-03 09:31:32 +02:00
parent 2a0aaed4c3
commit 3214a6f29a
11 changed files with 387 additions and 51 deletions

View File

@ -40,11 +40,7 @@ public static class AbilityDataLoader
var effect = serialized.Effect;
var parameters = serialized.Parameters.ToDictionary(x => (StringKey)x.Key, x => x.Value.ToParameter());
StringKey? effectName;
if (effect == null)
effectName = null;
else
effectName = new StringKey(effect);
StringKey? effectName = effect == null ? null! : new StringKey(effect);
var ability = new AbilityImpl(name, effectName, parameters);
return ability;

View File

@ -31,8 +31,7 @@ public static class ItemDataLoader
{
if (!Enum.TryParse<ItemCategory>(serialized.ItemType, true, out var itemType))
throw new InvalidDataException($"Item type {serialized.ItemType} is not valid for item {serialized.Name}.");
BattleItemCategory battleType;
Enum.TryParse(serialized.BattleType, true, out battleType);
Enum.TryParse(serialized.BattleType, true, out BattleItemCategory battleType);
return new ItemImpl(serialized.Name, itemType, battleType, serialized.Price,
serialized.Flags.Select(x => (StringKey)x).ToImmutableHashSet());

View File

@ -94,6 +94,12 @@ public class LearnedMoveImpl : ILearnedMove
CurrentPp = MaxPp;
}
public LearnedMoveImpl(IMoveData moveData, MoveLearnMethod learnMethod, byte pp)
: this(moveData, learnMethod)
{
CurrentPp = pp;
}
/// <inheritdoc />
public IMoveData MoveData { get; }

View File

@ -1,6 +1,7 @@
using JetBrains.Annotations;
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.Serialized;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Species;
@ -79,7 +80,7 @@ public interface IPokemon : IScriptSource
/// <summary>
/// The weight of the Pokemon in kilograms.
/// </summary>
float WeightInKm { get; set; }
float WeightInKg { get; set; }
/// <summary>
/// The height of the Pokemon in meters.
@ -305,6 +306,7 @@ public interface IPokemon : IScriptSource
void MarkOpponentAsSeen(IPokemon pokemon);
// TODO: (de)serialize
SerializedPokemon Serialize();
}
/// <summary>
@ -332,7 +334,7 @@ public interface IPokemonBattleData
/// A list of opponents the Pokemon has seen this battle.
/// </summary>
IReadOnlyList<IPokemon> SeenOpponents { get; }
/// <summary>
/// Whether the Pokemon is on the battlefield.
/// </summary>
@ -362,7 +364,7 @@ public class PokemonImpl : ScriptSource, IPokemon
Types = form.Types.ToList();
Experience = library.StaticLibrary.GrowthRates.CalculateExperience(species.GrowthRate, level);
WeightInKm = form.Weight;
WeightInKg = form.Weight;
HeightInMeters = form.Height;
Happiness = species.BaseHappiness;
if (!library.StaticLibrary.Natures.TryGet(natureName, out var nature))
@ -373,6 +375,63 @@ public class PokemonImpl : ScriptSource, IPokemon
CurrentHealth = BoostedStats.Hp;
}
public PokemonImpl(IDynamicLibrary library, SerializedPokemon serializedPokemon)
{
Library = library;
if (!library.StaticLibrary.Species.TryGet(serializedPokemon.Species, out var species))
throw new KeyNotFoundException($"Species {serializedPokemon.Species} not found.");
Species = species;
if (!species.TryGetForm(serializedPokemon.Form, out var form))
throw new KeyNotFoundException($"Form {serializedPokemon.Form} not found on species {species.Name}.");
Form = form;
Level = serializedPokemon.Level;
Experience = serializedPokemon.Experience;
PersonalityValue = serializedPokemon.PersonalityValue;
Gender = serializedPokemon.Gender;
Coloring = serializedPokemon.Coloring;
if (serializedPokemon.HeldItem != null)
{
if (!library.StaticLibrary.Items.TryGet(serializedPokemon.HeldItem, out var item))
throw new KeyNotFoundException($"Item {serializedPokemon.HeldItem} not found.");
HeldItem = item;
}
CurrentHealth = serializedPokemon.CurrentHealth;
WeightInKg = form.Weight;
HeightInMeters = form.Height;
Happiness = serializedPokemon.Happiness;
IndividualValues = new IndividualValueStatisticSet(serializedPokemon.IndividualValues);
EffortValues = new EffortValueStatisticSet(serializedPokemon.EffortValues);
if (!library.StaticLibrary.Natures.TryGet(serializedPokemon.Nature, out var nature))
throw new KeyNotFoundException($"Nature {serializedPokemon.Nature} not found.");
Nature = nature;
Nickname = serializedPokemon.Nickname;
if (!library.StaticLibrary.Abilities.TryGet(serializedPokemon.Ability, out var ability))
throw new KeyNotFoundException($"Ability {serializedPokemon.Ability} not found.");
AbilityIndex = form.FindAbilityIndex(ability) ??
throw new KeyNotFoundException(
$"Ability {ability.Name} not found on species {species.Name} form {form.Name}.");
_learnedMoves = serializedPokemon.Moves.Select(move =>
{
if (move == null)
return null;
if (!library.StaticLibrary.Moves.TryGet(move.MoveName, out var moveData))
throw new KeyNotFoundException($"Move {move.MoveName} not found");
return new LearnedMoveImpl(moveData, move.LearnMethod, move.CurrentPp);
}).ToArray();
AllowedExperience = serializedPokemon.AllowedExperience;
IsEgg = serializedPokemon.IsEgg;
Types = form.Types;
RecalculateFlatStats();
if (serializedPokemon.Status != null)
{
if (!library.ScriptResolver.TryResolve(ScriptCategory.Status, serializedPokemon.Status, out var statusScript))
throw new KeyNotFoundException($"Status script {serializedPokemon.Status} not found");
StatusScript.Set(statusScript);
}
}
/// <inheritdoc />
public IDynamicLibrary Library { get; }
@ -410,7 +469,7 @@ public class PokemonImpl : ScriptSource, IPokemon
public uint CurrentHealth { get; private set; }
/// <inheritdoc />
public float WeightInKm { get; set; }
public float WeightInKg { get; set; }
/// <inheritdoc />
public float HeightInMeters { get; set; }
@ -712,7 +771,7 @@ public class PokemonImpl : ScriptSource, IPokemon
if (!onBattleField)
{
Volatile.Clear();
WeightInKm = Form.Weight;
WeightInKg = Form.Weight;
HeightInMeters = Form.Height;
}
}
@ -730,6 +789,9 @@ public class PokemonImpl : ScriptSource, IPokemon
/// <inheritdoc />
public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon);
/// <inheritdoc />
public SerializedPokemon Serialize() => new(this);
/// <inheritdoc />
public override int ScriptCount
{
@ -787,6 +849,7 @@ public class PokemonBattleDataImpl : IPokemonBattleData
public byte Position { get; set; }
private readonly List<IPokemon> _seenOpponents = [];
/// <inheritdoc />
public IReadOnlyList<IPokemon> SeenOpponents => _seenOpponents;

View File

@ -0,0 +1,71 @@
using System.Diagnostics.CodeAnalysis;
using PkmnLib.Static;
using PkmnLib.Static.Species;
namespace PkmnLib.Dynamic.Models.Serialized;
public class SerializedPokemon
{
public SerializedPokemon(){}
[SetsRequiredMembers]
public SerializedPokemon(IPokemon pokemon)
{
Species = pokemon.Species.Name;
Form = pokemon.Form.Name;
Level = pokemon.Level;
Experience = pokemon.Experience;
PersonalityValue = pokemon.PersonalityValue;
Gender = pokemon.Gender;
Coloring = pokemon.Coloring;
HeldItem = pokemon.HeldItem?.Name;
CurrentHealth = pokemon.CurrentHealth;
Happiness = pokemon.Happiness;
IndividualValues = new IndividualValueStatisticSet(pokemon.IndividualValues);
EffortValues = new EffortValueStatisticSet(pokemon.EffortValues);
Nature = pokemon.Nature.Name;
Nickname = pokemon.Nickname;
Ability = pokemon.Form.GetAbility(pokemon.AbilityIndex);
Moves = pokemon.Moves.Select(move =>
{
if (move == null)
return null;
return new SerializedLearnedMove
{
MoveName = move.MoveData.Name,
LearnMethod = move.LearnMethod,
CurrentPp = move.CurrentPp,
};
}).ToArray();
AllowedExperience = pokemon.AllowedExperience;
IsEgg = pokemon.IsEgg;
Status = pokemon.StatusScript.Script?.Name;
}
public required string Species { get; set; }
public required string Form { get; set; }
public LevelInt Level { get; set; }
public uint Experience { get; set; }
public uint PersonalityValue { get; set; }
public Gender Gender { get; set; }
public byte Coloring { get; set; }
public string? HeldItem { get; set; }
public uint CurrentHealth { get; set; }
public byte Happiness { get; set; }
public required IndividualValueStatisticSet IndividualValues { get; set; }
public required EffortValueStatisticSet EffortValues { get; set; }
public required string Nature { get; set; }
public string? Nickname { get; set; }
public required string Ability { get; set; }
public required SerializedLearnedMove?[] Moves { get; set; }
public bool AllowedExperience { get; set; }
public bool IsEgg { get; set; }
public string? Status { get; set; }
}
public class SerializedLearnedMove
{
public required string MoveName { get; set; }
public required MoveLearnMethod LearnMethod { get; set; }
public required byte CurrentPp { get; set; }
}

View File

@ -1,3 +1,4 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
namespace PkmnLib.Static;
@ -49,6 +50,16 @@ public record ImmutableStatisticSet<T>
SpecialDefense = specialDefense;
Speed = speed;
}
public ImmutableStatisticSet(ImmutableStatisticSet<T> set)
{
Hp = set.Hp;
Attack = set.Attack;
Defense = set.Defense;
SpecialAttack = set.SpecialAttack;
SpecialDefense = set.SpecialDefense;
Speed = set.Speed;
}
/// <summary>
/// Gets a statistic from the set.
@ -72,7 +83,7 @@ public record ImmutableStatisticSet<T>
/// A set of statistics that can be changed.
/// </summary>
/// <typeparam name="T"></typeparam>
public record StatisticSet<T> : ImmutableStatisticSet<T>
public record StatisticSet<T> : ImmutableStatisticSet<T>, IEnumerable<T>
where T : struct
{
/// <inheritdoc cref="StatisticSet{T}"/>
@ -85,6 +96,10 @@ public record StatisticSet<T> : ImmutableStatisticSet<T>
defense, specialAttack, specialDefense, speed)
{
}
public StatisticSet(StatisticSet<T> set) : base(set)
{
}
/// <summary>
/// Helper function to add two numerics together.
@ -189,6 +204,23 @@ public record StatisticSet<T> : ImmutableStatisticSet<T>
return true;
}
/// <inheritdoc />
public IEnumerator<T> GetEnumerator()
{
yield return Hp;
yield return Attack;
yield return Defense;
yield return SpecialAttack;
yield return SpecialDefense;
yield return Speed;
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
/// <summary>
@ -211,6 +243,10 @@ public abstract record ClampedStatisticSet<T> : StatisticSet<T>
SpecialDefense = Clamp(SpecialDefense, Min, Max);
Speed = Clamp(Speed, Min, Max);
}
protected ClampedStatisticSet(ClampedStatisticSet<T> set) : base(set)
{
}
private static T Clamp(T value, T min, T max)
{
@ -304,6 +340,10 @@ public record IndividualValueStatisticSet : ClampedStatisticSet<byte>
byte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
{
}
public IndividualValueStatisticSet(IndividualValueStatisticSet ivs) : base(ivs)
{
}
}
/// <summary>
@ -327,4 +367,8 @@ public record EffortValueStatisticSet : ClampedStatisticSet<byte>
byte speed) : base(hp, attack, defense, specialAttack, specialDefense, speed)
{
}
public EffortValueStatisticSet(EffortValueStatisticSet evs) : base(evs)
{
}
}

View File

@ -26,7 +26,10 @@ public readonly record struct StringKey
/// <summary>
/// Converts a <see cref="string"/> to a <see cref="StringKey"/>.
/// </summary>
public static implicit operator StringKey(string key) => new(key);
public static implicit operator StringKey(string key)
{
return string.IsNullOrWhiteSpace(key) ? default : new StringKey(key);
}
/// <inheritdoc cref="string.ToString()"/>
public override string ToString() => _key.ToLowerInvariant();

View File

@ -0,0 +1,115 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Serialized;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Tests.Integration;
namespace PkmnLib.Tests.Dynamic;
public class SerializationTests
{
[Test]
public void SerializePokemon()
{
var library = LibraryHelpers.LoadLibrary();
Assert.That(library.StaticLibrary.Species.TryGet("bulbasaur", out var species));
var pokemon = new PokemonImpl(library, species!, species!.GetDefaultForm(), new AbilityIndex()
{
Index = 0,
IsHidden = false,
}, 10, 1000, Gender.Male, 0, "hardy");
pokemon.LearnMove("tackle", MoveLearnMethod.LevelUp, 255);
var data = pokemon.Serialize();
Assert.That(data, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(data.Species, Is.EqualTo("bulbasaur"));
Assert.That(data.Form, Is.EqualTo("default"));
Assert.That(data.Ability, Is.EqualTo("overgrow"));
Assert.That(data.Level, Is.EqualTo(10));
Assert.That(data.PersonalityValue, Is.EqualTo(1000));
Assert.That(data.Gender, Is.EqualTo(Gender.Male));
Assert.That(data.Experience, Is.EqualTo(560));
Assert.That(data.Coloring, Is.EqualTo(0));
Assert.That(data.HeldItem, Is.Null);
Assert.That(data.CurrentHealth, Is.EqualTo(29));
Assert.That(data.Happiness, Is.EqualTo(70));
});
Assert.That(data.Moves, Has.Length.EqualTo(4));
Assert.That(data.Moves[0], Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(data.Moves[0]!.MoveName, Is.EqualTo("tackle"));
Assert.That(data.Moves[0]!.LearnMethod, Is.EqualTo(MoveLearnMethod.LevelUp));
Assert.That(data.Moves[0]!.CurrentPp, Is.EqualTo(35));
});
}
[Test]
public void DeserializePokemon()
{
var library = LibraryHelpers.LoadLibrary();
var data = new SerializedPokemon
{
Species = "bulbasaur",
Form = "default",
Ability = "overgrow",
Level = 10,
Experience = 560,
PersonalityValue = 1000,
Gender = Gender.Male,
Coloring = 0,
HeldItem = null,
CurrentHealth = 29,
Happiness = 70,
IndividualValues = new IndividualValueStatisticSet(20, 20, 20, 20, 20, 20),
EffortValues = new EffortValueStatisticSet(0, 0, 0, 0, 0, 0),
Nature = "hardy",
Nickname = "foo",
Moves = new[]
{
new SerializedLearnedMove
{
MoveName = "tackle",
LearnMethod = MoveLearnMethod.LevelUp,
CurrentPp = 23,
},
null,
null,
null,
},
};
var pokemon = new PokemonImpl(library, data);
Assert.Multiple(() =>
{
Assert.That(pokemon.Species.Name.ToString(), Is.EqualTo("bulbasaur"));
Assert.That(pokemon.Form.Name.ToString(), Is.EqualTo("default"));
Assert.That(pokemon.AbilityIndex.Index, Is.EqualTo(0));
Assert.That(pokemon.Level, Is.EqualTo(10));
Assert.That(pokemon.Experience, Is.EqualTo(560));
Assert.That(pokemon.PersonalityValue, Is.EqualTo(1000));
Assert.That(pokemon.Gender, Is.EqualTo(Gender.Male));
Assert.That(pokemon.Coloring, Is.EqualTo(0));
Assert.That(pokemon.HeldItem, Is.Null);
Assert.That(pokemon.CurrentHealth, Is.EqualTo(29));
Assert.That(pokemon.Happiness, Is.EqualTo(70));
Assert.That(pokemon.IndividualValues, Is.EqualTo(new IndividualValueStatisticSet(20, 20, 20, 20, 20, 20)));
Assert.That(pokemon.EffortValues, Is.EqualTo(new EffortValueStatisticSet(0, 0, 0, 0, 0, 0)));
Assert.That(pokemon.Nature.Name.ToString(), Is.EqualTo("hardy"));
Assert.That(pokemon.Nickname, Is.EqualTo("foo"));
});
Assert.That(pokemon.Moves, Has.Count.EqualTo(4));
Assert.That(pokemon.Moves[0], Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(pokemon.Moves[0]!.MoveData.Name.ToString(), Is.EqualTo("tackle"));
Assert.That(pokemon.Moves[0]!.LearnMethod, Is.EqualTo(MoveLearnMethod.LevelUp));
Assert.That(pokemon.Moves[0]!.CurrentPp, Is.EqualTo(23));
});
}
}

View File

@ -1,10 +1,6 @@
using System.Collections;
using System.Text.Json;
using PkmnLib.Dataloader;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Plugin.Gen7;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Species;
using PkmnLib.Tests.Integration.Models;
@ -30,42 +26,10 @@ public class IntegrationTestRunner
}
}
private static IDynamicLibrary LoadLibrary()
{
using var typesFile = File.Open("Data/Types.csv", FileMode.Open);
var types = TypeDataLoader.LoadTypeLibrary(typesFile);
using var naturesFile = File.Open("Data/Natures.csv", FileMode.Open);
var natures = NatureDataLoader.LoadNatureLibrary(naturesFile);
using var movesFile = File.Open("Data/Moves.json", FileMode.Open);
var moves = MoveDataLoader.LoadMoves(movesFile, types);
using var itemsFile = File.Open("Data/Items.json", FileMode.Open);
var items = ItemDataLoader.LoadItems(itemsFile);
using var abilitiesFile = File.Open("Data/Abilities.json", FileMode.Open);
var abilities = AbilityDataLoader.LoadAbilities(abilitiesFile);
using var growthRatesFile = File.Open("Data/GrowthRates.json", FileMode.Open);
var growthRates = GrowthRateDataLoader.LoadGrowthRates(growthRatesFile);
using var speciesFile = File.Open("Data/Pokemon.json", FileMode.Open);
var species = SpeciesDataLoader.LoadSpecies(speciesFile, types);
var staticLibrary = new StaticLibraryImpl(new LibrarySettings()
{
MaxLevel = 100,
ShinyRate = 4096,
}, species, moves, abilities, types, natures, growthRates, items);
var dynamicLibrary = DynamicLibraryImpl.Create(staticLibrary, [
new Gen7Plugin(new Gen7PluginConfiguration()
{
DamageCalculatorHasRandomness = false,
}),
]);
return dynamicLibrary;
}
[TestCaseSource(nameof(TestCases))]
public void RunIntegrationTest(IntegrationTestModel test)
{
var library = LoadLibrary();
var library = LibraryHelpers.LoadLibrary();
var parties = test.BattleSetup.Parties.Select(IBattleParty (x) =>
{

View File

@ -0,0 +1,41 @@
using PkmnLib.Dataloader;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Plugin.Gen7;
using PkmnLib.Static.Libraries;
namespace PkmnLib.Tests.Integration;
public static class LibraryHelpers
{
public static IDynamicLibrary LoadLibrary()
{
using var typesFile = File.Open("Data/Types.csv", FileMode.Open);
var types = TypeDataLoader.LoadTypeLibrary(typesFile);
using var naturesFile = File.Open("Data/Natures.csv", FileMode.Open);
var natures = NatureDataLoader.LoadNatureLibrary(naturesFile);
using var movesFile = File.Open("Data/Moves.json", FileMode.Open);
var moves = MoveDataLoader.LoadMoves(movesFile, types);
using var itemsFile = File.Open("Data/Items.json", FileMode.Open);
var items = ItemDataLoader.LoadItems(itemsFile);
using var abilitiesFile = File.Open("Data/Abilities.json", FileMode.Open);
var abilities = AbilityDataLoader.LoadAbilities(abilitiesFile);
using var growthRatesFile = File.Open("Data/GrowthRates.json", FileMode.Open);
var growthRates = GrowthRateDataLoader.LoadGrowthRates(growthRatesFile);
using var speciesFile = File.Open("Data/Pokemon.json", FileMode.Open);
var species = SpeciesDataLoader.LoadSpecies(speciesFile, types);
var staticLibrary = new StaticLibraryImpl(new LibrarySettings()
{
MaxLevel = 100,
ShinyRate = 4096,
}, species, moves, abilities, types, natures, growthRates, items);
var dynamicLibrary = DynamicLibraryImpl.Create(staticLibrary, [
new Gen7Plugin(new Gen7PluginConfiguration()
{
DamageCalculatorHasRandomness = false,
}),
]);
return dynamicLibrary;
}
}

View File

@ -0,0 +1,34 @@
using System.Linq;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static;
namespace PkmnLib.Plugin.Gen7.Moves;
/// <summary>
/// The user applies pressure to stress points, sharply boosting one of its or its allies' stats.
/// </summary>
/// <remarks>
/// Acupressure chooses one of the target's stats at random and raises it by two stages. It can raise either the
/// target's Attack, Defense, Special Attack, Special Defense, Speed, accuracy or evasion stat but will not attempt
/// to raise a stat that is already maximized, meaning that the move will fail if all stats are maximized
/// </remarks>
[Script(ScriptCategory.Move, "acupressure")]
public class Acupressure : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
// If the target has no stats to raise, the move fails
if (target.StatBoost.All(s => s == 6))
{
move.GetHitData(target, hit).Fail();
return;
}
// Choose a random stat to raise. 0 is HP, so we start at 1.
var stat = (Statistic)move.User.BattleData!.Battle.Random.GetInt(1, (int)Statistic.Speed + 1);
target.ChangeStatBoost(stat, 2, false);
}
}