Initial commit
This commit is contained in:
commit
3845f91601
|
@ -0,0 +1,7 @@
|
|||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
*.DotSettings.user
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Static", "PkmnLib.Static\PkmnLib.Static.csproj", "{312782DA-1066-4490-BD0E-DF4DF8713B4A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PkmnLib.Tests", "PkmnLib.Tests\PkmnLib.Tests.csproj", "{42DE3095-0468-4827-AF5C-691C94BA7F92}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{312782DA-1066-4490-BD0E-DF4DF8713B4A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{42DE3095-0468-4827-AF5C-691C94BA7F92}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1 @@
|
|||
global using LevelInt = byte;
|
|
@ -0,0 +1,81 @@
|
|||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static;
|
||||
|
||||
/// <summary>
|
||||
/// A growth rate defines how much experience is required per level.
|
||||
/// </summary>
|
||||
public interface IGrowthRate
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the growth rate.
|
||||
/// </summary>
|
||||
public StringKey Name { get; }
|
||||
|
||||
///<summary>
|
||||
/// Calculate the level something with this growth rate would have at a certain experience.
|
||||
/// </summary>
|
||||
/// <param name="experience">The experience to calculate the level for.</param>
|
||||
/// <returns>The level at the given experience.</returns>
|
||||
LevelInt CalculateLevel(uint experience);
|
||||
|
||||
///<summary>
|
||||
/// Calculate the experience something with this growth rate would have at a certain level.
|
||||
/// </summary>
|
||||
/// <param name="level">The level to calculate the experience for.</param>
|
||||
/// <returns>The starting experience at the given level.</returns>
|
||||
uint CalculateExperience(LevelInt level);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of the growth rate that uses a lookup table for experience.
|
||||
/// </summary>
|
||||
public class LookupGrowthRate : IGrowthRate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public StringKey Name { get; }
|
||||
|
||||
private readonly uint[] _experienceTable;
|
||||
|
||||
/// <inheritdoc cref="LookupGrowthRate" />
|
||||
public LookupGrowthRate(StringKey name, IEnumerable<uint> experienceTable)
|
||||
{
|
||||
Name = name;
|
||||
_experienceTable = experienceTable.ToArray();
|
||||
if (_experienceTable.Length < 1)
|
||||
{
|
||||
throw new ArgumentException("Experience table must have at least one entry.");
|
||||
}
|
||||
|
||||
if (_experienceTable[0] != 0)
|
||||
{
|
||||
throw new ArgumentException("Experience table must start at 0.");
|
||||
}
|
||||
|
||||
if (_experienceTable.Length > LevelInt.MaxValue)
|
||||
{
|
||||
throw new ArgumentException($"Experience table may have at most {LevelInt.MaxValue} entries.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LevelInt CalculateLevel(uint experience)
|
||||
{
|
||||
for (LevelInt level = 0; level < _experienceTable.Length; level++)
|
||||
{
|
||||
if (_experienceTable[level] > experience)
|
||||
{
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
return (LevelInt)(_experienceTable.Length);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint CalculateExperience(LevelInt level)
|
||||
{
|
||||
if (level < 1) level = 1;
|
||||
return level >= _experienceTable.Length ? _experienceTable[^1] : _experienceTable[level - 1];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
using System.Collections.Immutable;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static;
|
||||
|
||||
/// <summary>
|
||||
/// An item category defines which bag slot items are stored in.
|
||||
/// </summary>
|
||||
public enum ItemCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// This is where most items should go.
|
||||
/// </summary>
|
||||
MiscItem,
|
||||
|
||||
/// <summary>
|
||||
/// Pokeballs are used for capturing Pokemons.
|
||||
/// </summary>
|
||||
Pokeball,
|
||||
|
||||
/// <summary>
|
||||
/// Medicine is used for healing HP, PP, and status effects.
|
||||
/// </summary>
|
||||
Medicine,
|
||||
|
||||
/// <summary>
|
||||
/// Berry is used for all berries.
|
||||
/// </summary>
|
||||
Berry,
|
||||
|
||||
/// <summary>
|
||||
/// TMHM is used for Technical and Hidden Machines.
|
||||
/// </summary>
|
||||
TmHm,
|
||||
|
||||
/// <summary>
|
||||
/// Form Changer is used for items that change forms, such as mega stones.
|
||||
/// </summary>
|
||||
FormChanger,
|
||||
|
||||
/// <summary>
|
||||
/// Key Items are single stored items, generally used for story progression.
|
||||
/// </summary>
|
||||
KeyItem,
|
||||
|
||||
/// <summary>
|
||||
/// Mail is used for mail items.
|
||||
/// </summary>
|
||||
Mail,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A battle item category defines how the item is categorized when in battle.
|
||||
/// </summary>
|
||||
public enum BattleItemCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// This item can't be used in battle.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// This item is used for healing Pokemon.
|
||||
/// </summary>
|
||||
Healing,
|
||||
|
||||
/// <summary>
|
||||
/// This item is used for healing Pokemon from a status.
|
||||
/// </summary>
|
||||
StatusHealing,
|
||||
|
||||
/// <summary>
|
||||
/// This item is used for capturing Pokemon.
|
||||
/// </summary>
|
||||
Pokeball,
|
||||
|
||||
/// <summary>
|
||||
/// This item does not belong in above categories, but is still a battle item.
|
||||
/// </summary>
|
||||
MiscBattleItem,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An item is an object which the player can pick up, keep in their Bag, and use in some manner.
|
||||
/// </summary>
|
||||
public interface IItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the item.
|
||||
/// </summary>
|
||||
StringKey Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Which bag slot items are stored in.
|
||||
/// </summary>
|
||||
ItemCategory Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How the item is categorized when in battle.
|
||||
/// </summary>
|
||||
BattleItemCategory BattleCategory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The buying value of the item.
|
||||
/// </summary>
|
||||
int Price { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A set of arbitrary flags that can be set on the item.
|
||||
/// </summary>
|
||||
ImmutableHashSet<StringKey> Flags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the item has a specific flag.
|
||||
/// </summary>
|
||||
/// <param name="key">The flag to check for.</param>
|
||||
/// <returns>True if the item has the flag, false otherwise.</returns>
|
||||
bool HasFlag(string key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ItemImpl : IItem
|
||||
{
|
||||
/// <inheritdoc cref="ItemImpl"/>
|
||||
public ItemImpl(StringKey name, ItemCategory category, BattleItemCategory battleCategory, int price,
|
||||
IEnumerable<StringKey> flags)
|
||||
{
|
||||
Name = name;
|
||||
Category = category;
|
||||
BattleCategory = battleCategory;
|
||||
Price = price;
|
||||
Flags = [..flags];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ItemCategory Category { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BattleItemCategory BattleCategory { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Price { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImmutableHashSet<StringKey> Flags { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(string key)
|
||||
{
|
||||
return Flags.Contains(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
using System.Collections.Immutable;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// The move category defines what global kind of move this move is.
|
||||
/// </summary>
|
||||
public enum MoveCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// A physical move uses the physical attack stats and physical defense stats to calculate damage.
|
||||
/// </summary>
|
||||
Physical = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A special move uses the special attack stats and special defense stats to calculate damage.
|
||||
/// </summary>
|
||||
Special = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A status move does not do damage, and only runs a secondary effect.
|
||||
/// </summary>
|
||||
Status = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The move target defines what kind of targets the move can touch.
|
||||
/// </summary>
|
||||
public enum MoveTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Adjacent allows a move to target any Pokemon that is either directly to the left or right of
|
||||
/// the user, opposed to the user, or left or right of the slot that is opposing the user.
|
||||
/// </summary>
|
||||
Adjacent = 0,
|
||||
|
||||
/// <summary>
|
||||
/// AdjacentAlly allows a move to target any Pokemon that is directly to the left or right of
|
||||
/// the user.
|
||||
/// </summary>
|
||||
AdjacentAlly,
|
||||
|
||||
/// <summary>
|
||||
/// AdjacentAllySelf allows a move to target any Pokemon that is either directly to the left or
|
||||
/// right of the user, or the user itself.
|
||||
/// </summary>
|
||||
AdjacentAllySelf,
|
||||
|
||||
/// <summary>
|
||||
/// AdjacentOpponent allows a move to target any Pokemon that is either the opponent, or directly
|
||||
/// to the left or right of it.
|
||||
/// </summary>
|
||||
AdjacentOpponent,
|
||||
|
||||
/// <summary>
|
||||
/// All makes the move target everything on the field.
|
||||
/// </summary>
|
||||
All,
|
||||
|
||||
/// <summary>
|
||||
/// AllAdjacent makes the move target everything adjacent on the field.
|
||||
/// </summary>
|
||||
AllAdjacent,
|
||||
|
||||
/// <summary>
|
||||
/// AllAdjacentOpponent makes the move target everything adjacent to the opponent, and the opponent.
|
||||
/// </summary>
|
||||
AllAdjacentOpponent,
|
||||
|
||||
/// <summary>
|
||||
/// AllAlly targets all Pokemon on the same side as the user.
|
||||
/// </summary>
|
||||
AllAlly,
|
||||
|
||||
/// <summary>
|
||||
/// AllOpponent targets all Pokemon on an opposing side from the user.
|
||||
/// </summary>
|
||||
AllOpponent,
|
||||
|
||||
/// <summary>
|
||||
/// Any allows a move to target a single Pokemon, in any position.
|
||||
/// </summary>
|
||||
Any,
|
||||
|
||||
/// <summary>
|
||||
/// RandomOpponent allows a move to target a single Pokemon, in a random position.
|
||||
/// </summary>
|
||||
RandomOpponent,
|
||||
|
||||
/// <summary>
|
||||
/// SelfUse makes the move target the user itself.
|
||||
/// </summary>
|
||||
SelfUse,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A move is the skill Pokémon primarily use in battle. This is the data related to that.
|
||||
/// </summary>
|
||||
public interface IMoveData
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the move.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The attacking type of the move.
|
||||
/// </summary>
|
||||
TypeIdentifier MoveType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The category of the move.
|
||||
/// </summary>
|
||||
MoveCategory Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base power, not considering any modifiers, the move has.
|
||||
/// </summary>
|
||||
byte BasePower { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The accuracy of the move in percentage. Should be 255 for moves that always hit.
|
||||
/// </summary>
|
||||
byte Accuracy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of times the move can be used. This can be modified on actually learned moves using PP-Ups
|
||||
/// </summary>
|
||||
byte BaseUsages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How the move handles targets.
|
||||
/// </summary>
|
||||
MoveTarget Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The priority of the move. A higher priority means the move should go before other moves.
|
||||
/// </summary>
|
||||
sbyte Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The optional secondary effect the move has.
|
||||
/// </summary>
|
||||
ISecondaryEffect? SecondaryEffect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Arbitrary flags that can be applied to the move.
|
||||
/// </summary>
|
||||
bool HasFlag(string key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class MoveDataImpl : IMoveData
|
||||
{
|
||||
/// <inheritdoc cref="MoveDataImpl" />
|
||||
public MoveDataImpl(string name, TypeIdentifier moveType, MoveCategory category, byte basePower, byte accuracy,
|
||||
byte baseUsages, MoveTarget target, sbyte priority, ISecondaryEffect? secondaryEffect,
|
||||
IEnumerable<StringKey> flags)
|
||||
{
|
||||
Name = name;
|
||||
MoveType = moveType;
|
||||
Category = category;
|
||||
BasePower = basePower;
|
||||
Accuracy = accuracy;
|
||||
BaseUsages = baseUsages;
|
||||
Target = target;
|
||||
Priority = priority;
|
||||
SecondaryEffect = secondaryEffect;
|
||||
_flags = [..flags];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeIdentifier MoveType { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MoveCategory Category { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte BasePower { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Accuracy { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte BaseUsages { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MoveTarget Target { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public sbyte Priority { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISecondaryEffect? SecondaryEffect { get; }
|
||||
|
||||
private readonly ImmutableHashSet<StringKey> _flags;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(string key) => _flags.Contains(key);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// A secondary effect is an effect on a move that happens after it hits.
|
||||
/// </summary>
|
||||
public interface ISecondaryEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The chance in percentages that the effect triggers. When less than 0, the effect is always active.
|
||||
/// </summary>
|
||||
public float Chance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the effect.
|
||||
/// </summary>
|
||||
public StringKey Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the effect.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<StringKey, object> Parameters { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SecondaryEffectImpl : ISecondaryEffect
|
||||
{
|
||||
/// <inheritdoc cref="SecondaryEffectImpl" />
|
||||
public SecondaryEffectImpl(float chance, StringKey name, IReadOnlyDictionary<StringKey, object> parameters)
|
||||
{
|
||||
Chance = chance;
|
||||
Name = name;
|
||||
Parameters = parameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Chance { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<StringKey, object> Parameters { get; }
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
|
||||
namespace PkmnLib.Static;
|
||||
|
||||
/// <summary>
|
||||
/// A nature is an attribute on a Pokemon that modifies the effective base stats on a Pokemon. They
|
||||
/// can have an increased statistic and a decreased statistic, or be neutral.
|
||||
/// </summary>
|
||||
public interface INature
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the nature.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The stat that should receive the increased modifier.
|
||||
/// </summary>
|
||||
Statistic IncreasedStat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The stat that should receive the decreased modifier.
|
||||
/// </summary>
|
||||
Statistic DecreasedStat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the increased stat gets modified by.
|
||||
/// </summary>
|
||||
float IncreasedModifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the decreased stat gets modified by.
|
||||
/// </summary>
|
||||
float DecreasedModifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the modifier for a given stat. If it's the increased stat, returns the increased
|
||||
/// modifier, if it's the decreased stat, returns the decreased modifier. Otherwise returns 1.0.
|
||||
/// </summary>
|
||||
/// <param name="stat">The stat to calculate the modifier for.</param>
|
||||
/// <returns>The calculated modifier.</returns>
|
||||
float GetStatModifier(Statistic stat);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two natures are equal.
|
||||
/// </summary>
|
||||
/// <param name="other">The other nature to compare to.</param>
|
||||
/// <returns>True if the natures are equal, false otherwise.</returns>
|
||||
bool Equals(INature other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class Nature(
|
||||
string name,
|
||||
Statistic increaseStat,
|
||||
Statistic decreaseStat,
|
||||
float increaseModifier,
|
||||
float decreaseModifier)
|
||||
: INature
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name { get; } = name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Statistic IncreasedStat { get; } = increaseStat;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Statistic DecreasedStat { get; } = decreaseStat;
|
||||
|
||||
/// <inheritdoc />
|
||||
public float IncreasedModifier { get; } = increaseModifier;
|
||||
|
||||
/// <inheritdoc />
|
||||
public float DecreasedModifier { get; } = decreaseModifier;
|
||||
|
||||
/// <inheritdoc />
|
||||
public float GetStatModifier(Statistic stat)
|
||||
{
|
||||
if (stat == IncreasedStat && stat != DecreasedStat)
|
||||
return IncreasedModifier;
|
||||
if (stat == DecreasedStat && stat != IncreasedStat)
|
||||
return DecreasedModifier;
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(INature? other)
|
||||
{
|
||||
return other is not null && StringComparer.InvariantCultureIgnoreCase.Equals(Name, other.Name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\netstandard2.1\PkmnLib.Static.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\Release\netstandard2.1\PkmnLib.Static.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentResults" Version="3.16.0" />
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Libraries\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,63 @@
|
|||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static.Species;
|
||||
|
||||
/// <summary>
|
||||
/// An ability is a passive effect in battle that is attached to a Pokemon.
|
||||
/// </summary>
|
||||
public interface IAbility
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the ability.
|
||||
/// </summary>
|
||||
StringKey Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the script effect of the ability. This should refer to the name of the script that will be executed
|
||||
/// when the ability is triggered.
|
||||
/// </summary>
|
||||
StringKey Effect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The parameters for the script effect of the ability.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, object> Parameters { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class AbilityImpl : IAbility
|
||||
{
|
||||
/// <inheritdoc cref="AbilityImpl" />
|
||||
public AbilityImpl(StringKey name, StringKey effect, IReadOnlyDictionary<StringKey, object> parameters)
|
||||
{
|
||||
Name = name;
|
||||
Effect = effect;
|
||||
Parameters = parameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey Effect { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<StringKey, object> Parameters { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An ability index allows us to find an ability on a form. It combines a bool for whether the
|
||||
/// ability is hidden or not, and then an index of the ability.
|
||||
/// </summary>
|
||||
public readonly record struct AbilityIndex
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the ability we're referring to is a hidden ability.
|
||||
/// </summary>
|
||||
public required bool IsHidden { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the ability.
|
||||
/// </summary>
|
||||
public required byte Index { get; init; }
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static.Species;
|
||||
|
||||
/// <summary>
|
||||
/// Data about how and into which Pokemon a species can evolve.
|
||||
/// </summary>
|
||||
public interface IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The species that the Pokemon evolves into.
|
||||
/// </summary>
|
||||
StringKey ToSpecies { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when a certain level is reached.
|
||||
/// </summary>
|
||||
public record LevelEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The level at which the Pokemon evolves.
|
||||
/// </summary>
|
||||
public required uint Level { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when a certain level is reached, and the Pokemon is a specific gender
|
||||
/// </summary>
|
||||
public record LevelGenderEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The level at which the Pokemon evolves.
|
||||
/// </summary>
|
||||
public required uint Level { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The gender the Pokemon needs to have to evolve
|
||||
/// </summary>
|
||||
public required Gender Gender { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when an item is used on the Pokemon.
|
||||
/// </summary>
|
||||
public record ItemUseEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that needs to be used.
|
||||
/// </summary>
|
||||
public required StringKey Item { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when an item is used on the Pokemon, and the Pokemon is a specific gender
|
||||
/// </summary>
|
||||
public record ItemGenderEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that needs to be used.
|
||||
/// </summary>
|
||||
public required StringKey Item { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The gender the Pokemon needs to have to evolve
|
||||
/// </summary>
|
||||
public Gender Gender { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when an item is held by the Pokemon, and the Pokemon levels up.
|
||||
/// </summary>
|
||||
public record HoldItemEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that needs to be held.
|
||||
/// </summary>
|
||||
public required StringKey Item { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when an item is held by the Pokemon, and the Pokemon levels up, and it's day.
|
||||
/// </summary>
|
||||
public record DayHoldItemEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that needs to be held.
|
||||
/// </summary>
|
||||
public required StringKey Item { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when an item is held by the Pokemon, and the Pokemon levels up, and it's night.
|
||||
/// </summary>
|
||||
public record NightHoldItemEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that needs to be held.
|
||||
/// </summary>
|
||||
public required StringKey Item { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when the Pokemon knows a certain move, and the Pokemon levels up.
|
||||
/// </summary>
|
||||
public record HasMoveEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the move that needs to be known.
|
||||
/// </summary>
|
||||
public required StringKey MoveName { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when above a certain happiness level, and the Pokemon levels up.
|
||||
/// </summary>
|
||||
public record HappinessEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The happiness level that needs to be reached.
|
||||
/// </summary>
|
||||
public required byte Happiness { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when above a certain happiness level, and the Pokemon levels up, and it's day.
|
||||
/// </summary>
|
||||
public record HappinessDayEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The happiness level that needs to be reached.
|
||||
/// </summary>
|
||||
public required byte Happiness { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when above a certain happiness level, and the Pokemon levels up, and it's night.
|
||||
/// </summary>
|
||||
public record HappinessNightEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The happiness level that needs to be reached.
|
||||
/// </summary>
|
||||
public required byte Happiness { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when traded.
|
||||
/// </summary>
|
||||
public record TradeEvolution : IEvolution
|
||||
{
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when traded with a certain species.
|
||||
/// </summary>
|
||||
public record TradeSpeciesEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The species that needs to be traded with.
|
||||
/// </summary>
|
||||
public required StringKey WithSpecies { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evolves when traded while it's holding a certain item.
|
||||
/// </summary>
|
||||
public record TradeItemEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The item that needs to be held.
|
||||
/// </summary>
|
||||
public required StringKey Item { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom evolution method, implemented by the user.
|
||||
/// </summary>
|
||||
public record CustomEvolution : IEvolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the custom evolution method. This should refer to the name of the script that will be executed.
|
||||
/// </summary>
|
||||
public required StringKey Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The parameters of the custom evolution method.
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<StringKey, object> Parameters { get; init; }
|
||||
|
||||
/// < inheritdoc />
|
||||
public required StringKey ToSpecies { get; init; }
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
using System.Collections.Immutable;
|
||||
using FluentResults;
|
||||
using PkmnLib.Static.Utils;
|
||||
using PkmnLib.Static.Utils.Errors;
|
||||
|
||||
namespace PkmnLib.Static.Species;
|
||||
|
||||
/// <summary>
|
||||
/// A form is a variant of a specific species. A species always has at least one form, but can have
|
||||
/// many more.
|
||||
/// </summary>
|
||||
public interface IForm
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the form.
|
||||
/// </summary>
|
||||
StringKey Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The height of the form in meters.
|
||||
/// </summary>
|
||||
float Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The weight of the form in kilograms.
|
||||
/// </summary>
|
||||
float Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base amount of experience that is gained when beating a Pokemon with this form.
|
||||
/// </summary>
|
||||
uint BaseExperience { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The normal types a Pokemon with this form has.
|
||||
/// </summary>
|
||||
IReadOnlyList<TypeIdentifier> Types { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The inherent values of a form of species that are used for the stats of a Pokemon.
|
||||
/// </summary>
|
||||
StaticStatisticSet<ushort> BaseStats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The possible abilities a Pokemon with this form can have.
|
||||
/// </summary>
|
||||
IReadOnlyList<StringKey> Abilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The possible hidden abilities a Pokemon with this form can have.
|
||||
/// </summary>
|
||||
IReadOnlyList<StringKey> HiddenAbilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The moves a Pokemon with this form can learn.
|
||||
/// </summary>
|
||||
ILearnableMoves Moves { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Arbitrary flags can be set on a form for scripting use.
|
||||
/// </summary>
|
||||
ImmutableHashSet<StringKey> Flags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a type of the form at a certain index.
|
||||
/// </summary>
|
||||
Result<TypeIdentifier> GetType(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single base stat value.
|
||||
/// </summary>
|
||||
ushort GetBaseStat(Statistic stat);
|
||||
|
||||
/// <summary>
|
||||
/// Find the index of an ability that can be on this form.
|
||||
/// </summary>
|
||||
AbilityIndex? FindAbilityIndex(IAbility ability);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ability from the form.
|
||||
/// </summary>
|
||||
Result<StringKey> GetAbility(AbilityIndex index);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random ability from the form.
|
||||
/// </summary>
|
||||
StringKey GetRandomAbility(IRandom rand);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random hidden ability from the form.
|
||||
/// </summary>
|
||||
StringKey GetRandomHiddenAbility(IRandom rand);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the form has a specific flag set.
|
||||
/// </summary>
|
||||
bool HasFlag(string key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class FormImpl : IForm
|
||||
{
|
||||
/// <inheritdoc cref="FormImpl" />
|
||||
public FormImpl(StringKey name, float height, float weight, uint baseExperience,
|
||||
IEnumerable<TypeIdentifier> types, StaticStatisticSet<ushort> baseStats, IEnumerable<StringKey> abilities,
|
||||
IEnumerable<StringKey> hiddenAbilities, ILearnableMoves moves, ImmutableHashSet<StringKey> flags)
|
||||
{
|
||||
Name = name;
|
||||
Height = height;
|
||||
Weight = weight;
|
||||
BaseExperience = baseExperience;
|
||||
Types = [..types];
|
||||
BaseStats = baseStats;
|
||||
Abilities = [..abilities];
|
||||
HiddenAbilities = [..hiddenAbilities];
|
||||
Moves = moves;
|
||||
Flags = flags;
|
||||
|
||||
if (Types.Count == 0)
|
||||
throw new ArgumentException("A form must have at least one type.");
|
||||
if (Abilities.Count == 0)
|
||||
throw new ArgumentException("A form must have at least one ability.");
|
||||
if (HiddenAbilities.Count == 0)
|
||||
throw new ArgumentException("A form must have at least one hidden ability.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Height { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float Weight { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint BaseExperience { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<TypeIdentifier> Types { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StaticStatisticSet<ushort> BaseStats { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<StringKey> Abilities { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<StringKey> HiddenAbilities { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILearnableMoves Moves { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImmutableHashSet<StringKey> Flags { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Result<TypeIdentifier> GetType(int index)
|
||||
{
|
||||
if (index < 0 || index >= Types.Count)
|
||||
return Result.Fail(new OutOfRange("Type", index, Types.Count));
|
||||
return Types[index];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort GetBaseStat(Statistic stat) => BaseStats.GetStatistic(stat);
|
||||
|
||||
/// <inheritdoc />
|
||||
public AbilityIndex? FindAbilityIndex(IAbility ability)
|
||||
{
|
||||
for (var i = 0; i < Abilities.Count && i < 255; i++)
|
||||
{
|
||||
if (Abilities[i] == ability.Name)
|
||||
return new AbilityIndex
|
||||
{
|
||||
IsHidden = false,
|
||||
Index = (byte)i
|
||||
};
|
||||
}
|
||||
for (var i = 0; i < HiddenAbilities.Count && i < 255; i++)
|
||||
{
|
||||
if (HiddenAbilities[i] == ability.Name)
|
||||
return new AbilityIndex
|
||||
{
|
||||
IsHidden = true,
|
||||
Index = (byte)i
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Result<StringKey> GetAbility(AbilityIndex index)
|
||||
{
|
||||
var array = index.IsHidden ? HiddenAbilities : Abilities;
|
||||
if (index.Index >= array.Count)
|
||||
return Result.Fail(new OutOfRange("Ability", index.Index, array.Count));
|
||||
return array[index.Index];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey GetRandomAbility(IRandom rand)
|
||||
{
|
||||
return Abilities[rand.GetInt(Abilities.Count)];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey GetRandomHiddenAbility(IRandom rand)
|
||||
{
|
||||
return HiddenAbilities[rand.GetInt(HiddenAbilities.Count)];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(string key)
|
||||
{
|
||||
return Flags.Contains(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
namespace PkmnLib.Static;
|
||||
|
||||
/// <summary>
|
||||
/// Gender is a Pokemon characteristic.
|
||||
///
|
||||
/// Required for standard pokemon functions, but somewhat controversial nowadays. Consider adding a feature
|
||||
/// that allows for a more progressive gender system for those that want it?
|
||||
/// </summary>
|
||||
public enum Gender : byte
|
||||
{
|
||||
/// The Pokemon has no gender.
|
||||
Genderless,
|
||||
/// <summary>
|
||||
/// The Pokemon is male.
|
||||
/// </summary>
|
||||
Male,
|
||||
/// <summary>
|
||||
/// The Pokemon is female.
|
||||
/// </summary>
|
||||
Female
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static.Species;
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the moves a Pokemon can learn.
|
||||
/// </summary>
|
||||
public interface ILearnableMoves
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new level move the Pokemon can learn.
|
||||
/// </summary>
|
||||
/// <param name="level">The level the Pokemon learns the move at.</param>
|
||||
/// <param name="move">The move the Pokemon learns.</param>
|
||||
/// <returns>Whether the move was added successfully.</returns>
|
||||
void AddLevelMove(LevelInt level, StringKey move);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all moves a Pokemon can learn when leveling up to a specific level.
|
||||
/// </summary>
|
||||
/// <param name="level">The level the Pokemon is learning moves at.</param>
|
||||
/// <returns>The moves the Pokemon learns at that level.</returns>
|
||||
IReadOnlyList<StringKey> GetLearnedByLevel(LevelInt level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the distinct moves a Pokemon can learn through leveling up.
|
||||
/// </summary>
|
||||
/// <returns>The moves the Pokemon can learn through leveling up.</returns>
|
||||
IReadOnlyList<StringKey> GetDistinctLevelMoves();
|
||||
}
|
||||
|
||||
public class LearnableMovesImpl : ILearnableMoves
|
||||
{
|
||||
private readonly Dictionary<LevelInt, List<StringKey>> _learnedByLevel = new();
|
||||
private readonly HashSet<StringKey> _distinctLevelMoves = new();
|
||||
|
||||
|
||||
public void AddLevelMove(LevelInt level, StringKey move)
|
||||
{
|
||||
if (!_learnedByLevel.TryGetValue(level, out var value))
|
||||
_learnedByLevel[level] = [move];
|
||||
else
|
||||
value.Add(move);
|
||||
_distinctLevelMoves.Add(move);
|
||||
}
|
||||
|
||||
public IReadOnlyList<StringKey> GetLearnedByLevel(LevelInt level)
|
||||
{
|
||||
if (!_learnedByLevel.TryGetValue(level, out var value))
|
||||
return Array.Empty<StringKey>();
|
||||
return value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<StringKey> GetDistinctLevelMoves()
|
||||
{
|
||||
return _distinctLevelMoves.ToList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static.Species;
|
||||
|
||||
/// <summary>
|
||||
/// The data belonging to a Pokemon with certain characteristics.
|
||||
/// </summary>
|
||||
public interface ISpecies
|
||||
{
|
||||
/// <summary>
|
||||
/// The national dex identifier of the Pokemon.
|
||||
/// </summary>
|
||||
ushort Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the Pokemon.
|
||||
/// </summary>
|
||||
StringKey Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The chance between 0.0 and 1.0 that a Pokemon is female. 0.0 means always male, 1.0 means always female.
|
||||
/// If less than 0, the Pokemon is genderless.
|
||||
/// </summary>
|
||||
float GenderRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How much experience is required for a level.
|
||||
/// </summary>
|
||||
StringKey GrowthRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How hard it is to capture a Pokemon. 255 means this will be always caught, 0 means this is
|
||||
/// uncatchable.
|
||||
/// </summary>
|
||||
byte CaptureRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base happiness of the Pokemon.
|
||||
/// </summary>
|
||||
byte BaseHappiness { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The forms that belong to this Pokemon.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<StringKey, IForm> Forms { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The arbitrary flags that can be set on a Pokemon for script use.
|
||||
/// </summary>
|
||||
ImmutableHashSet<StringKey> Flags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a form by name.
|
||||
/// </summary>
|
||||
bool TryGetForm(StringKey id, [MaybeNullWhen(false)] out IForm form);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the form the Pokemon will have by default, if no other form is specified.
|
||||
/// </summary>
|
||||
IForm GetDefaultForm();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random gender.
|
||||
/// </summary>
|
||||
Gender GetRandomGender(IRandom rand);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the Pokemon has a specific flag set.
|
||||
/// </summary>
|
||||
bool HasFlag(string key);
|
||||
|
||||
/// <summary>
|
||||
/// The data regarding into which Pokemon this species can evolve, and how.
|
||||
/// </summary>
|
||||
IReadOnlyList<IEvolution> EvolutionData { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SpeciesImpl : ISpecies
|
||||
{
|
||||
/// <inheritdoc cref="SpeciesImpl" />
|
||||
public SpeciesImpl(ushort id, StringKey name, float genderRate, StringKey growthRate, byte captureRate,
|
||||
byte baseHappiness, IReadOnlyDictionary<StringKey, IForm> forms, ImmutableHashSet<StringKey> flags,
|
||||
IReadOnlyList<IEvolution> evolutionData)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
GenderRate = genderRate;
|
||||
GrowthRate = growthRate;
|
||||
CaptureRate = captureRate;
|
||||
BaseHappiness = baseHappiness;
|
||||
Forms = forms;
|
||||
Flags = flags;
|
||||
EvolutionData = evolutionData;
|
||||
if (Forms.Count == 0)
|
||||
throw new ArgumentException("Species must have at least one form.");
|
||||
if (!Forms.ContainsKey("default"))
|
||||
throw new ArgumentException("Species must have a default form.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float GenderRate { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public StringKey GrowthRate { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte CaptureRate { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte BaseHappiness { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<StringKey, IForm> Forms { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImmutableHashSet<StringKey> Flags { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IEvolution> EvolutionData { get; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetForm(StringKey id, [MaybeNullWhen(false)] out IForm form) => Forms.TryGetValue(id, out form);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IForm GetDefaultForm() => Forms["default"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Gender GetRandomGender(IRandom rand)
|
||||
{
|
||||
if (GenderRate < 0.0f)
|
||||
return Gender.Genderless;
|
||||
var v = rand.GetFloat();
|
||||
return v < GenderRate ? Gender.Female : Gender.Male;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFlag(string key) => Flags.Contains(key);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
namespace PkmnLib.Static;
|
||||
|
||||
/// <summary>
|
||||
/// Stats are numerical values on Pokemon that are used in battle.
|
||||
/// </summary>
|
||||
public enum Statistic : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Health Points determine how much damage a Pokemon can receive before fainting.
|
||||
/// </summary>
|
||||
Hp,
|
||||
/// <summary>
|
||||
/// Attack determines how much damage a Pokemon deals when using a physical attack.
|
||||
/// </summary>
|
||||
Attack,
|
||||
/// <summary>
|
||||
/// Defense determines how much damage a Pokemon receives when it is hit by a physical attack.
|
||||
/// </summary>
|
||||
Defense,
|
||||
/// <summary>
|
||||
/// Special Attack determines how much damage a Pokemon deals when using a special attack.
|
||||
/// </summary>
|
||||
SpecialAttack,
|
||||
/// <summary>
|
||||
/// Special Defense determines how much damage a Pokemon receives when it is hit by a special attack.
|
||||
/// </summary>
|
||||
SpecialDefense,
|
||||
/// <summary>
|
||||
/// Speed determines the order that a Pokemon can act in battle.
|
||||
/// </summary>
|
||||
Speed
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
using System;
|
||||
|
||||
namespace PkmnLib.Static;
|
||||
|
||||
public record StaticStatisticSet<T>
|
||||
where T : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// The health points stat value.
|
||||
/// </summary>
|
||||
public T Hp { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The physical attack stat value.
|
||||
/// </summary>
|
||||
public T Attack { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The physical defense stat value.
|
||||
/// </summary>
|
||||
public T Defense { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The special attack stat value.
|
||||
/// </summary>
|
||||
public T SpecialAttack { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The special defense stat value.
|
||||
/// </summary>
|
||||
public T SpecialDefense { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The speed stat value.
|
||||
/// </summary>
|
||||
public T Speed { get; protected set; }
|
||||
|
||||
public StaticStatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed)
|
||||
{
|
||||
Hp = hp;
|
||||
Attack = attack;
|
||||
Defense = defense;
|
||||
SpecialAttack = specialAttack;
|
||||
SpecialDefense = specialDefense;
|
||||
Speed = speed;
|
||||
}
|
||||
|
||||
public T GetStatistic(Statistic stat)
|
||||
{
|
||||
return stat switch
|
||||
{
|
||||
Statistic.Hp => Hp,
|
||||
Statistic.Attack => Attack,
|
||||
Statistic.Defense => Defense,
|
||||
Statistic.SpecialAttack => SpecialAttack,
|
||||
Statistic.SpecialDefense => SpecialDefense,
|
||||
Statistic.Speed => Speed,
|
||||
_ => throw new ArgumentException("Invalid statistic.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public record StatisticSet<T> : StaticStatisticSet<T>
|
||||
where T : struct
|
||||
{
|
||||
public StatisticSet(T hp, T attack, T defense, T specialAttack, T specialDefense, T speed) : base(hp, attack,
|
||||
defense, specialAttack, specialDefense, speed)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetStatistic(Statistic stat, T value)
|
||||
{
|
||||
switch (stat)
|
||||
{
|
||||
case Statistic.Hp:
|
||||
Hp = value;
|
||||
break;
|
||||
case Statistic.Attack:
|
||||
Attack = value;
|
||||
break;
|
||||
case Statistic.Defense:
|
||||
Defense = value;
|
||||
break;
|
||||
case Statistic.SpecialAttack:
|
||||
SpecialAttack = value;
|
||||
break;
|
||||
case Statistic.SpecialDefense:
|
||||
SpecialDefense = value;
|
||||
break;
|
||||
case Statistic.Speed:
|
||||
Speed = value;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid statistic.");
|
||||
}
|
||||
}
|
||||
|
||||
public void IncreaseStatistic(Statistic stat, T value)
|
||||
{
|
||||
switch (stat)
|
||||
{
|
||||
case Statistic.Hp:
|
||||
Hp = (T)Convert.ChangeType(Convert.ToInt32(Hp) + Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.Attack:
|
||||
Attack = (T)Convert.ChangeType(Convert.ToInt32(Attack) + Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.Defense:
|
||||
Defense = (T)Convert.ChangeType(Convert.ToInt32(Defense) + Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.SpecialAttack:
|
||||
SpecialAttack =
|
||||
(T)Convert.ChangeType(Convert.ToInt32(SpecialAttack) + Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.SpecialDefense:
|
||||
SpecialDefense =
|
||||
(T)Convert.ChangeType(Convert.ToInt32(SpecialDefense) + Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.Speed:
|
||||
Speed = (T)Convert.ChangeType(Convert.ToInt32(Speed) + Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid statistic.");
|
||||
}
|
||||
}
|
||||
|
||||
public void DecreaseStatistic(Statistic stat, T value)
|
||||
{
|
||||
switch (stat)
|
||||
{
|
||||
case Statistic.Hp:
|
||||
Hp = (T)Convert.ChangeType(Convert.ToInt32(Hp) - Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.Attack:
|
||||
Attack = (T)Convert.ChangeType(Convert.ToInt32(Attack) - Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.Defense:
|
||||
Defense = (T)Convert.ChangeType(Convert.ToInt32(Defense) - Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.SpecialAttack:
|
||||
SpecialAttack =
|
||||
(T)Convert.ChangeType(Convert.ToInt32(SpecialAttack) - Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.SpecialDefense:
|
||||
SpecialDefense =
|
||||
(T)Convert.ChangeType(Convert.ToInt32(SpecialDefense) - Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
case Statistic.Speed:
|
||||
Speed = (T)Convert.ChangeType(Convert.ToInt32(Speed) - Convert.ToInt32(value), typeof(T));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid statistic.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
namespace PkmnLib.Static;
|
||||
|
||||
/// <summary>
|
||||
/// The time of day. These values are the 4 different groups of time of day in Pokemon games since
|
||||
/// gen 5. The exact times these correspond to differ between games.
|
||||
/// </summary>
|
||||
public enum TimeOfDay : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The morning.
|
||||
/// </summary>
|
||||
Morning = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The day.
|
||||
/// </summary>
|
||||
Day = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The evening.
|
||||
/// </summary>
|
||||
Evening = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The night.
|
||||
/// </summary>
|
||||
Night = 3,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace PkmnLib.Static;
|
||||
|
||||
public readonly record struct TypeIdentifier
|
||||
{
|
||||
private byte Value { get; init; }
|
||||
|
||||
public TypeIdentifier(byte value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator TypeIdentifier(byte value) => new(value);
|
||||
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using FluentResults;
|
||||
|
||||
namespace PkmnLib.Static.Utils.Errors;
|
||||
|
||||
public class OutOfRange : Error
|
||||
{
|
||||
public OutOfRange(string hint, int index, int max)
|
||||
: base($"{hint} index {index} is out of range. Must be between 0 and {max - 1}.")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
namespace PkmnLib.Static.Utils;
|
||||
|
||||
public interface IRandom
|
||||
{
|
||||
public int GetInt(int min, int max);
|
||||
public int GetInt(int max);
|
||||
public int GetInt();
|
||||
public float GetFloat();
|
||||
public float GetFloat(float min, float max);
|
||||
public bool GetBool();
|
||||
}
|
||||
|
||||
public class RandomImpl : IRandom
|
||||
{
|
||||
private Random _random;
|
||||
|
||||
public RandomImpl(Random random)
|
||||
{
|
||||
_random = random;
|
||||
}
|
||||
|
||||
public RandomImpl(int seed)
|
||||
{
|
||||
_random = new Random(seed);
|
||||
}
|
||||
|
||||
public RandomImpl()
|
||||
{
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
public int GetInt(int min, int max) => _random.Next(min, max);
|
||||
|
||||
public int GetInt(int max) => _random.Next(max);
|
||||
|
||||
public int GetInt() => _random.Next();
|
||||
|
||||
public float GetFloat() => (float)_random.NextDouble();
|
||||
|
||||
public float GetFloat(float min, float max) => (float)(_random.NextDouble() * (max - min) + min);
|
||||
|
||||
public bool GetBool() => _random.Next(2) == 1;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
|
||||
namespace PkmnLib.Static.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A case-insensitive string key. We use this class for things like looking up data from dictionaries, etc.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a struct, as it's effectively just a wrapper around a single reference object. Heap allocation would be silly.
|
||||
/// </remarks>
|
||||
public readonly record struct StringKey
|
||||
{
|
||||
private readonly string _key;
|
||||
|
||||
public StringKey(string key)
|
||||
{
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public static implicit operator string(StringKey key) => key._key;
|
||||
public static implicit operator StringKey(string key) => new(key);
|
||||
|
||||
public override string ToString() => _key.ToLowerInvariant();
|
||||
|
||||
public bool Equals(StringKey other)
|
||||
{
|
||||
return string.Equals(_key, other._key, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return StringComparer.InvariantCultureIgnoreCase.GetHashCode(_key);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
global using NUnit.Framework;
|
||||
global using FluentAssertions;
|
||||
|
||||
global using LevelInt = byte;
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||
<PackageReference Include="NUnit" Version="3.13.3"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,46 @@
|
|||
using PkmnLib.Static;
|
||||
|
||||
namespace PkmnLib.Tests.Static;
|
||||
|
||||
public class GrowthRateTests
|
||||
{
|
||||
[Test]
|
||||
public void EmptyLookupGrowthRateTestShouldThrowArgumentException()
|
||||
{
|
||||
Action act = () => _ = new LookupGrowthRate("Test", []);
|
||||
act.Should().Throw<ArgumentException>().WithMessage("Experience table must have at least one entry.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NonZeroLookupGrowthRateTestShouldThrowArgumentException()
|
||||
{
|
||||
Action act = () => _ = new LookupGrowthRate("Test", [1]);
|
||||
act.Should().Throw<ArgumentException>().WithMessage("Experience table must start at 0.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TooLargeLookupGrowthRateTestShouldThrowArgumentException()
|
||||
{
|
||||
Action act = () => _ = new LookupGrowthRate("Test", Enumerable.Range(0, LevelInt.MaxValue + 1).Select(i => (uint)i));
|
||||
act.Should().Throw<ArgumentException>().WithMessage($"Experience table may have at most {LevelInt.MaxValue} entries.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LookupGrowthRateTest()
|
||||
{
|
||||
var growthRate = new LookupGrowthRate("Test", [0, 1, 2, 3, 4, 5]);
|
||||
growthRate.CalculateLevel(0).Should().Be(1);
|
||||
growthRate.CalculateLevel(1).Should().Be(2);
|
||||
growthRate.CalculateLevel(2).Should().Be(3);
|
||||
growthRate.CalculateLevel(3).Should().Be(4);
|
||||
growthRate.CalculateLevel(4).Should().Be(5);
|
||||
growthRate.CalculateLevel(5).Should().Be(6);
|
||||
|
||||
growthRate.CalculateExperience(1).Should().Be(0);
|
||||
growthRate.CalculateExperience(2).Should().Be(1);
|
||||
growthRate.CalculateExperience(3).Should().Be(2);
|
||||
growthRate.CalculateExperience(4).Should().Be(3);
|
||||
growthRate.CalculateExperience(5).Should().Be(4);
|
||||
growthRate.CalculateExperience(6).Should().Be(5);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
using System.Collections;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Tests.Static;
|
||||
|
||||
public class StringKeyTests
|
||||
{
|
||||
private static IEnumerable StringKeyEqualityTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new TestCaseData("test", "test").Returns(true);
|
||||
yield return new TestCaseData("test", "test2").Returns(false);
|
||||
yield return new TestCaseData("test2", "test2").Returns(true);
|
||||
yield return new TestCaseData("Test", "test").Returns(true);
|
||||
yield return new TestCaseData("TeSt", "tesT").Returns(true);
|
||||
yield return new TestCaseData("TeSt", "tesv").Returns(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(StringKeyEqualityTestCases))]
|
||||
public bool StringKeyEqualityTest(string k1, string k2)
|
||||
{
|
||||
var sk1 = new StringKey(k1);
|
||||
var sk2 = new StringKey(k2);
|
||||
return sk1 == sk2;
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(StringKeyEqualityTestCases))]
|
||||
public bool HashCodeEqualityTest(string k1, string k2)
|
||||
{
|
||||
var sk1 = new StringKey(k1);
|
||||
var sk2 = new StringKey(k2);
|
||||
return sk1.GetHashCode() == sk2.GetHashCode();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(StringKeyEqualityTestCases))]
|
||||
public bool HashSetEqualityTest(string k1, string k2)
|
||||
{
|
||||
var sk1 = new StringKey(k1);
|
||||
var sk2 = new StringKey(k2);
|
||||
var hs = new HashSet<StringKey> { sk1 };
|
||||
return hs.Contains(sk2);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue