Change EncounterCriteria to use Level Range

This commit is contained in:
Kurt 2024-11-18 20:29:22 -06:00
parent 52351bf0e7
commit 0b686d428b
13 changed files with 123 additions and 34 deletions

View file

@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Object that can be fed to a <see cref="IEncounterConvertible"/> converter to ensure that the resulting <see cref="PKM"/> meets rough specifications.
/// </summary>
public sealed record EncounterCriteria : IFixedNature, IFixedAbilityNumber, IShinyPotential
public sealed record EncounterCriteria : IFixedNature, IFixedAbilityNumber, IShinyPotential, ILevelRange
{
/// <summary>
/// Default criteria with no restrictions (random) for all fields.
@ -36,10 +36,8 @@ public sealed record EncounterCriteria : IFixedNature, IFixedAbilityNumber, IShi
public sbyte IV_SPD { get; init; } = RandomIV;
public sbyte IV_SPE { get; init; } = RandomIV;
/// <summary>
/// If the Encounter yields variable level ranges (e.g. RNG correlation), force the minimum level instead of yielding first match.
/// </summary>
public bool ForceMinLevelRange { get; set; }
public byte LevelMin { get; set; }
public byte LevelMax { get; set; }
public sbyte TeraType { get; init; } = -1;
@ -49,6 +47,7 @@ public sealed record EncounterCriteria : IFixedNature, IFixedAbilityNumber, IShi
private const int RandomIV = -1;
public bool IsSpecifiedNature() => Nature != Nature.Random;
public bool IsSpecifiedLevelRange() => LevelMin != 0;
public bool IsSpecifiedTeraType() => TeraType != -1;
public bool IsSpecifiedIVs() => IV_HP != RandomIV
@ -58,6 +57,8 @@ public sealed record EncounterCriteria : IFixedNature, IFixedAbilityNumber, IShi
&& IV_SPD != RandomIV
&& IV_SPE != RandomIV;
public bool IsLevelRangeSatisfied(byte level) => LevelMin <= level && level <= LevelMax;
/// <summary>
/// Checks if the IVs are compatible with the encounter's defined IV restrictions.
/// </summary>

View file

@ -134,15 +134,26 @@ public sealed record EncounterArea3 : IEncounterArea<EncounterSlot3>, IAreaLocat
}
}
/// <summary>
/// Wild Encounter data <see cref="IEncounterTemplate"/> Type
/// </summary>
public enum SlotType3 : byte
{
/// <summary> Grass tiles </summary>
Grass = 0,
/// <summary> Water tiles </summary>
Surf = 1,
/// <summary> Fishing with Old Rod </summary>
Old_Rod = 2,
/// <summary> Fishing with Good Rod </summary>
Good_Rod = 3,
/// <summary> Fishing with Super Rod </summary>
Super_Rod = 4,
/// <summary> Using Rock Smash move </summary>
Rock_Smash = 5,
/// <summary> Swarm in grass tiles </summary>
SwarmGrass50 = 6,
/// <summary> Swarm in water tiles </summary>
SwarmFish50 = 7,
}

View file

@ -128,11 +128,14 @@ public sealed record EncounterArea4 : IEncounterArea<EncounterSlot4>, IGroundTyp
58, // 20 Floaroma Meadow
];
/// <summary>
/// Checks if the Unown form is valid for the given <see cref="PKM"/>.
/// </summary>
public static bool IsUnownFormValid(PKM pk, byte form)
{
return pk.HGSS
? RuinsOfAlph4.IsUnownFormValid(pk, form)
: SolaceonRuins4.IsUnownFormValid(pk, form);
? RuinsOfAlph4.IsFormValid(pk, form)
: SolaceonRuins4.IsFormValid(pk, form);
}
}
@ -143,28 +146,54 @@ public sealed record EncounterArea4 : IEncounterArea<EncounterSlot4>, IGroundTyp
/// Different from <see cref="GroundTileAllowed"/>, this corresponds to the method that the <see cref="IEncounterTemplate"/> may be encountered.</remarks>
public enum SlotType4 : byte
{
/// <summary> Grass tiles </summary>
Grass = 0,
/// <summary> Water tiles </summary>
Surf = 1,
/// <summary> Fishing with Old Rod </summary>
Old_Rod = 2,
/// <summary> Fishing with Good Rod </summary>
Good_Rod = 3,
/// <summary> Fishing with Super Rod </summary>
Super_Rod = 4,
/// <summary> Using Rock Smash move </summary>
Rock_Smash = 5,
/// <summary> Using Headbutt on capable trees </summary>
Headbutt = 6,
/// <summary> Using Headbutt on special trees </summary>
HeadbuttSpecial = 7,
/// <summary> Grass tiles during a Bug Catching Contest </summary>
BugContest = 8,
/// <summary> Shaking Honey Trees </summary>
HoneyTree = 9,
/// <summary> Grass tiles in the Safari Zone </summary>
Safari_Grass = 10,
/// <summary> Water tiles in the Safari Zone </summary>
Safari_Surf = 11,
/// <summary> Fishing with Old Rod in the Safari Zone </summary>
Safari_Old_Rod = 12,
/// <summary> Fishing with Good Rod in the Safari Zone </summary>
Safari_Good_Rod = 13,
/// <summary> Fishing with Super Rod in the Safari Zone </summary>
Safari_Super_Rod = 14,
}
public static class SlotType4Extensions
{
/// <summary>
/// Checks if the <see cref="type"/> is an encounter within the Safari Zone.
/// </summary>
public static bool IsSafari(this SlotType4 type) => type >= SlotType4.Safari_Grass;
/// <summary>
/// Checks if the <see cref="type"/> has a level range that is random. For D/P/Pt; this is all types except Grass.
/// </summary>
public static bool IsLevelRandDPPt(this SlotType4 type) => type != SlotType4.Grass;
/// <summary>
/// Checks if the <see cref="type"/> has a level range that is random. For HG/SS; this is all types except Grass and Safari.
/// </summary>
public static bool IsLevelRandHGSS(this SlotType4 type) => type != SlotType4.Grass && !type.IsSafari();
}

View file

@ -75,7 +75,7 @@ public sealed record EncounterSlot8a(EncounterArea8a Parent, ushort Species, byt
{
// Give a random level according to the RNG correlation.
var lvl = Overworld8aRNG.GetRandomLevel(slotSeed, LevelMin, LevelMax);
if (criteria.ForceMinLevelRange && lvl != LevelMin)
if (criteria.IsSpecifiedLevelRange() && !criteria.IsLevelRangeSatisfied(lvl))
continue;
pk.MetLevel = pk.CurrentLevel = lvl;
}

View file

@ -120,7 +120,9 @@ public static class GenerateMethodH
var gender = EntityGender.GetFromPIDAndRatio(pid, gr);
if (!criteria.IsGenderSatisfied(gender))
continue;
var lead = MethodH.GetSeed(enc, seed, pk.E, gender, criteria.ForceMinLevelRange);
var lead = criteria.IsSpecifiedLevelRange()
? MethodH.GetSeed(enc, seed, pk.E, gender, criteria)
: MethodH.GetSeed(enc, seed, pk.E, gender);
if (!lead.IsValid()) // Verifies the slot, (min) level, and nature loop; if it passes, apply the details.
continue;
@ -155,7 +157,9 @@ public static class GenerateMethodH
var gender = EntityGender.GetFromPIDAndRatio(pid, gr);
if (!criteria.IsGenderSatisfied(gender))
continue;
var lead = MethodH.GetSeed(enc, seed, pk.E, gender, criteria.ForceMinLevelRange);
var lead = criteria.IsSpecifiedLevelRange()
? MethodH.GetSeed(enc, seed, pk.E, gender, criteria)
: MethodH.GetSeed(enc, seed, pk.E, gender);
if (!lead.IsValid()) // Verifies the slot and nature loop; if it passes, apply the details.
continue;
@ -203,7 +207,7 @@ public static class GenerateMethodH
continue;
seed = LCRNG.Prev(seed);
}
var lead = MethodH.GetSeed(enc, seed, false, 2, criteria.ForceMinLevelRange);
var lead = MethodH.GetSeed(enc, seed, false, 2);
if (!lead.IsValid()) // Verifies the slot and form loop; if it passes, apply the details.
continue;
@ -229,7 +233,7 @@ public static class GenerateMethodH
var form = EntityPID.GetUnownForm3(pid);
if (form != enc.Form)
continue;
var lead = MethodH.GetSeed(enc, seed, false, 2, criteria.ForceMinLevelRange);
var lead = MethodH.GetSeed(enc, seed, false, 2);
if (!lead.IsValid()) // Verifies the slot and form loop; if it passes, apply the details.
continue;

View file

@ -33,9 +33,17 @@ public static class MethodH
}
/// <remarks>Used when generating or ignoring level ranges.</remarks>
/// <inheritdoc cref="GetSeed{TEnc,TEvo}"/>
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed, bool emerald, byte gender, bool minLevel = false)
where TEnc : IEncounterSlot3 => GetSeed(enc, seed, enc, emerald, gender, minLevel ? (byte)3 : (byte)0);
/// <inheritdoc cref="GetSeed{TEnc,TEvo}(TEnc, uint, TEvo, bool, byte, byte)"/>
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed, bool emerald, byte gender)
where TEnc : IEncounterSlot3
=> GetSeed(enc, seed, enc, emerald, gender, 0);
/// <remarks>Used when generating with specific level ranges.</remarks>
/// <inheritdoc cref="GetSeed{TEnc,TEvo}(TEnc, uint, TEvo, bool, byte, byte)"/>
public static LeadSeed GetSeed<TEnc, TEvo>(TEnc enc, uint seed, bool emerald, byte gender, TEvo evo)
where TEnc : IEncounterSlot3
where TEvo : ILevelRange
=> GetSeed(enc, seed, evo, emerald, gender, 3);
// Summary of Random Determinations:
// Nature: rand() % 25 == nature

View file

@ -102,7 +102,9 @@ public static class GenerateMethodJ
var gender = EntityGender.GetFromPIDAndRatio(pid, gr);
if (!criteria.IsGenderSatisfied(gender))
continue;
var lead = MethodJ.GetSeed(enc, seed, criteria.ForceMinLevelRange);
var lead = criteria.IsSpecifiedLevelRange()
? MethodJ.GetSeed(enc, seed, criteria)
: MethodJ.GetSeed(enc, seed);
if (!lead.IsValid()) // Verifies the slot, (min) level, and nature loop; if it passes, apply the details.
continue;

View file

@ -106,7 +106,9 @@ public static class GenerateMethodK
var gender = EntityGender.GetFromPIDAndRatio(pid, gr);
if (!criteria.IsGenderSatisfied(gender))
continue;
var lead = MethodK.GetSeed(enc, seed, criteria.ForceMinLevelRange);
var lead = criteria.IsSpecifiedLevelRange()
? MethodK.GetSeed(enc, seed, criteria)
: MethodK.GetSeed(enc, seed);
if (!lead.IsValid()) // Verifies the slot, (min) level, and nature loop; if it passes, apply the details.
continue;

View file

@ -30,9 +30,16 @@ public static class MethodJ
}
/// <remarks>Used when generating or ignoring level ranges.</remarks>
/// <inheritdoc cref="GetSeed{TEnc,TEvo}"/>
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed, bool minLevel = false) where TEnc : IEncounterSlot4
=> GetSeed(enc, seed, enc, minLevel ? (byte)4 : (byte)0);
/// <inheritdoc cref="GetSeed{TEnc,TEvo}(TEnc, uint, TEvo, byte)"/>
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed) where TEnc : IEncounterSlot4
=> GetSeed(enc, seed, enc, 0);
/// <remarks>Used when generating with specific level ranges.</remarks>
/// <inheritdoc cref="GetSeed{TEnc,TEvo}(TEnc, uint, TEvo, byte)"/>
public static LeadSeed GetSeed<TEnc, TEvo>(TEnc enc, uint seed, TEvo evo)
where TEnc : IEncounterSlot4
where TEvo : ILevelRange
=> GetSeed(enc, seed, evo, 4);
// Summary of Random Determinations:
// For constant-value rand choices, the games avoid using modulo via:

View file

@ -23,9 +23,16 @@ public static class MethodK
=> GetSeed(enc, seed, evo.LevelMin, evo.LevelMax, format);
/// <remarks>Used when generating or ignoring level ranges.</remarks>
/// <inheritdoc cref="GetSeed{TEnc,TEvo}"/>
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed, bool minLevel = false) where TEnc : IEncounterSlot4
=> GetSeed(enc, seed, enc, minLevel ? (byte)4 : (byte)0);
/// <inheritdoc cref="GetSeed{TEnc,TEvo}(TEnc, uint, TEvo, byte)"/>
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed) where TEnc : IEncounterSlot4
=> GetSeed(enc, seed, enc, 0);
/// <remarks>Used when generating with specific level ranges.</remarks>
/// <inheritdoc cref="GetSeed{TEnc,TEvo}(TEnc, uint, TEvo, byte)"/>
public static LeadSeed GetSeed<TEnc, TEvo>(TEnc enc, uint seed, TEvo evo)
where TEnc : IEncounterSlot4
where TEvo : ILevelRange
=> GetSeed(enc, seed, evo, 4);
public static LeadSeed GetSeed<TEnc>(TEnc enc, uint seed, byte levelMin, byte levelMax, byte format = Format, int depth = 0)
where TEnc : IEncounterSlot4

View file

@ -5,25 +5,35 @@ namespace PKHeX.Core;
public static class RuinsOfAlph4
{
/// <summary>
/// Checks if the requested <see cref="form"/> is valid for the given seed.
/// Checks if the requested <see cref="form"/> is valid for the given result.
/// </summary>
public static bool IsUnownFormValid(PKM pk, byte form)
public static bool IsFormValid(PKM pk, byte form)
{
if (!MethodFinder.GetLCRNGMethod1Match(pk, out var seed))
return true; // invalid anyway, don't care.
// ABCD|E(Item)|F(Form) determination
var f = LCRNG.Next6(seed);
return IsFormValid(form, f);
return IsFormValid(seed, form);
}
/// <summary>
/// Checks if the requested <see cref="form"/> is valid for the given seed.
/// </summary>
/// <param name="seed">Seed that originated the PID/IV.</param>
/// <param name="form">Form to validate</param>
/// <returns>True if the form is valid</returns>
public static bool IsFormValid(uint seed, byte form)
{
// ABCD|E(Item)|F(Form) determination
var f = LCRNG.Next6(seed);
return IsFormValidFrame(f, form);
}
/// <summary>
/// Checks if the requested <see cref="form"/> is valid for the given frame seed that calculates form.
/// </summary>
/// <param name="seed">RNG state that determines the form</param>
/// <returns></returns>
public static bool IsFormValid(byte form, uint seed)
/// <param name="form">Form to validate</param>
/// <returns>True if the form is valid</returns>
public static bool IsFormValidFrame(uint seed, byte form)
{
if (form >= 26) // Entrance
return form == GetEntranceForm(seed);

View file

@ -7,7 +7,7 @@ public static class SolaceonRuins4
/// <summary>
/// Checks if the requested <see cref="form"/> is valid for the given seed.
/// </summary>
public static bool IsUnownFormValid(PKM pk, byte form)
public static bool IsFormValid(PKM pk, byte form)
{
if (IsSingleFormRoomUnown(form))
return true; // FRIEND: Specific rooms with only one form.
@ -17,6 +17,7 @@ public static class SolaceonRuins4
if (!MethodFinder.GetLCRNGMethod1Match(pk, out var seed))
return true; // invalid anyway, don't care.
// ABCD|E(Item)|F(Form) determination
var f = LCRNG.Next6(seed) >> 16;
var expect = GetUnownForm(f, form);
return expect == form;

View file

@ -121,10 +121,17 @@ public static class Encounter9RNG
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
if (criteria.Nature != Nature.Random && nature != criteria.Nature)
return false;
pk.Nature = pk.StatNature = nature;
// Compromise on Nature -- some are fixed, some are random. If the request wants a specific nature, just mint it.
var requestNature = criteria.Nature;
if (criteria.Nature != Nature.Random && nature != requestNature)
{
if (!requestNature.IsMint())
return false;
pk.StatNature = requestNature;
}
pk.HeightScalar = enc.Height != 0 ? enc.Height : (byte)(rand.NextInt(0x81) + rand.NextInt(0x80));
pk.WeightScalar = enc.Weight != 0 ? enc.Weight : (byte)(rand.NextInt(0x81) + rand.NextInt(0x80));
pk.Scale = enc.ScaleType.GetSizeValue(enc.Scale, ref rand);