Encounter Crossover/Wander tolerance (30f) (#3683)

* Update encounter_wild_paldea.pkl

* Add weather/time mark checks

* Set tolerance to 30f, swap crabrawler evos
This commit is contained in:
Kurt 2022-12-10 19:53:59 -08:00 committed by GitHub
parent d4835392a0
commit 3ef12c3ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 206 additions and 59 deletions

View File

@ -12,6 +12,8 @@ public sealed record EncounterArea9 : EncounterArea
{
public readonly EncounterSlot9[] Slots;
public ushort CrossFrom { get; init; }
protected override IReadOnlyList<EncounterSlot> Raw => Slots;
public static EncounterArea9[] GetAreas(BinLinkerAccessor input, GameVersion game)
@ -25,12 +27,13 @@ public static EncounterArea9[] GetAreas(BinLinkerAccessor input, GameVersion gam
private EncounterArea9(ReadOnlySpan<byte> areaData, GameVersion game) : base(game)
{
Location = areaData[0];
Slots = ReadSlots(areaData[2..]);
CrossFrom = areaData[2];
Slots = ReadSlots(areaData[4..]);
}
private EncounterSlot9[] ReadSlots(ReadOnlySpan<byte> areaData)
{
const int size = 6;
const int size = 8;
var result = new EncounterSlot9[areaData.Length / size];
for (int i = 0; i < result.Length; i++)
{
@ -38,9 +41,12 @@ private EncounterSlot9[] ReadSlots(ReadOnlySpan<byte> areaData)
var species = ReadUInt16LittleEndian(slot);
var form = slot[2];
var gender = (sbyte)slot[3];
var min = slot[4];
var max = slot[5];
result[i] = new EncounterSlot9(this, species, form, min, max, gender);
var time = slot[6];
result[i] = new EncounterSlot9(this, species, form, min, max, gender, time);
}
return result;
}

View File

@ -0,0 +1,42 @@
using System;
using static PKHeX.Core.RibbonIndex;
using static PKHeX.Core.AreaWeather9;
namespace PKHeX.Core;
/// <summary>
/// Encounter Conditions for <see cref="GameVersion.SV"/>
/// </summary>
[Flags]
public enum AreaWeather9 : ushort
{
None,
Normal = 1,
Overcast = 1 << 1,
Raining = 1 << 2,
Thunderstorm = 1 << 3,
Intense_Sun = 1 << 4,
Snowing = 1 << 5,
Snowstorm = 1 << 6,
Sandstorm = 1 << 7,
Heavy_Fog = 1 << 8,
Standard = Normal | Overcast | Raining | Thunderstorm,
Sand = Normal | Overcast | Raining | Sandstorm,
Snow = Normal | Overcast | Snowing | Snowstorm,
Inside = Normal | Overcast,
}
public static class AreaWeather9Extensions
{
public static bool IsMarkCompatible(this AreaWeather9 weather, RibbonIndex m) => m switch
{
MarkCloudy => (weather & Overcast) != 0,
MarkRainy => (weather & Raining) != 0,
MarkStormy => (weather & Thunderstorm) != 0,
MarkSnowy => (weather & Snowing) != 0,
MarkBlizzard => (weather & Snowstorm) != 0,
MarkSandstorm => (weather & Sandstorm) != 0,
_ => true,
};
}

View File

@ -1,3 +1,6 @@
using System.Collections.Generic;
using static PKHeX.Core.AreaWeather9;
namespace PKHeX.Core;
/// <summary>
@ -9,10 +12,12 @@ public sealed record EncounterSlot9 : EncounterSlot
public override int Generation => 9;
public override EntityContext Context => EntityContext.Gen9;
public sbyte Gender { get; }
public byte Time { get; } // disallow at time bit flag
public EncounterSlot9(EncounterArea9 area, ushort species, byte form, byte min, byte max, sbyte gender) : base(area, species, form, min, max)
public EncounterSlot9(EncounterArea9 area, ushort species, byte form, byte min, byte max, sbyte gender, byte time) : base(area, species, form, min, max)
{
Gender = gender;
Time = time;
}
protected override void ApplyDetails(ITrainerInfo sav, EncounterCriteria criteria, PKM pk)
@ -33,4 +38,61 @@ protected override void ApplyDetails(ITrainerInfo sav, EncounterCriteria criteri
pk.Nature = ToxtricityUtil.GetRandomNature(ref rand, Form);
pk9.EncryptionConstant = Util.Rand32();
}
private static int GetTime(RibbonIndex mark) => mark switch
{
RibbonIndex.MarkLunchtime => 0,
RibbonIndex.MarkSleepyTime => 1,
RibbonIndex.MarkDusk => 2,
RibbonIndex.MarkDawn => 3,
_ => 4,
};
public bool CanSpawnAtTime(RibbonIndex mark) => (Time & (1 << GetTime(mark))) == 0;
public bool CanSpawnInWeather(RibbonIndex mark)
{
if (AreaWeather.TryGetValue((byte)Area.Location, out var areaWeather))
return areaWeather.IsMarkCompatible(mark);
return false;
}
/// <summary>
/// Location IDs matched with possible weather types.
/// </summary>
internal static readonly Dictionary<byte, AreaWeather9> AreaWeather = new()
{
{ 6, Standard }, // South Province (Area One)
{ 10, Standard }, // Pokémon League
{ 12, Standard }, // South Province (Area Two)
{ 14, Standard }, // South Province (Area Four)
{ 16, Standard }, // South Province (Area Six)
{ 18, Standard }, // South Province (Area Five)
{ 20, Standard }, // South Province (Area Three)
{ 22, Standard }, // West Province (Area One)
{ 24, Sand }, // Asado Desert
{ 26, Standard }, // West Province (Area Two)
{ 28, Standard }, // West Province (Area Three)
{ 30, Standard }, // Tagtree Thicket
{ 32, Standard }, // East Province (Area Three)
{ 34, Standard }, // East Province (Area One)
{ 36, Standard }, // East Province (Area Two)
{ 38, Snow }, // Glaseado Mountain (1)
{ 40, Standard }, // Casseroya Lake
{ 44, Standard }, // North Province (Area Three)
{ 46, Standard }, // North Province (Area One)
{ 48, Standard }, // North Province (Area Two)
{ 50, Standard }, // Great Crater of Paldea
{ 56, Standard }, // South Paldean Sea
{ 58, Standard }, // West Paldean Sea
{ 60, Standard }, // East Paldean Sea
{ 62, Standard }, // North Paldean Sea
{ 64, Inside }, // Inlet Grotto
{ 67, Inside }, // Alfornada Cavern
{ 69, Standard | Inside | Snow | Snow },// Dalizapa Passage (Near Medali, Tunnels, Near Pokémon Center, Near Zapico)
{ 70, Standard }, // Poco Path
{ 80, Standard }, // Cabo Poco
{ 109, Standard }, // Socarrat Trail
{ 124, Inside }, // Area Zero (5)
};
}

View File

@ -37,7 +37,8 @@ private static EncounterFixed9 ReadEncounter(ReadOnlySpan<byte> data) => new()
Level = data[0x03],
FlawlessIVCount = data[0x04],
TeraType = (GemType)data[0x05],
// 2 bytes reserved
Gender = (sbyte)data[0x06],
// 1 byte reserved
Moves = new Moveset(
BinaryPrimitives.ReadUInt16LittleEndian(data[0x08..]),
BinaryPrimitives.ReadUInt16LittleEndian(data[0x0A..]),
@ -57,6 +58,13 @@ protected override bool IsMatchLocation(PKM pk)
return loc == Location0 || loc == Location1 || loc == Location2;
}
protected override bool IsMatchForm(PKM pk, EvoCriteria evo)
{
if (Species is (int)Core.Species.Deerling or (int)Core.Species.Sawsbuck)
return pk.Form <= 3;
return base.IsMatchForm(pk, evo);
}
public override bool IsMatchExact(PKM pk, EvoCriteria evo)
{
if (TeraType != GemType.Random && pk is ITeraType t && !Tera9RNG.IsMatchTeraType(TeraType, Species, Form, (byte)t.TeraTypeOriginal))

View File

@ -62,22 +62,26 @@ private static IEnumerable<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] c
yield break;
}
// Static Encounters can collide with wild encounters (close match); don't break if a Static Encounter is yielded.
var encs = GetValidStaticEncounter(pk, chain, game);
foreach (var z in encs)
if (pk is not IRibbonIndex r || !r.HasEncounterMark())
{
var match = z.GetMatchRating(pk);
if (match == Match)
var encs = GetValidStaticEncounter(pk, chain, game);
foreach (var z in encs)
{
yield return z;
}
else if (match < rating)
{
cache = z;
rating = match;
var match = z.GetMatchRating(pk);
if (match == Match)
{
yield return z;
}
else if (match < rating)
{
cache = z;
rating = match;
}
}
}
// Wild encounters are more permissive than static encounters.
// Can have encounter marks, can have varied scales/shiny states.
foreach (var z in GetValidWildEncounters(pk, chain, game))
{
var match = z.GetMatchRating(pk);

View File

@ -48,7 +48,7 @@ private void VerifyMarksPresent(LegalityAnalysis data, IRibbonIndex m)
return;
}
bool result = MarkRules.IsMarkValid8(mark, data.Entity, data.EncounterMatch);
bool result = MarkRules.IsEncounterMarkValid(mark, data.Entity, data.EncounterMatch);
if (!result)
{
data.AddLine(GetInvalid(string.Format(LRibbonMarkingFInvalid_0, GetRibbonNameSafe(mark))));
@ -103,7 +103,7 @@ private void VerifyShedinjaAffixed(LegalityAnalysis data, RibbonIndex affix, PKM
var enc = data.EncounterOriginal;
if (affix.IsEncounterMark())
{
if (!MarkRules.IsMarkValid8(affix, pk, enc))
if (!MarkRules.IsEncounterMarkValid(affix, pk, enc))
data.AddLine(GetInvalid(string.Format(LRibbonMarkingAffixedF_0, GetRibbonNameSafe(affix))));
return;
}

View File

@ -8,11 +8,6 @@ namespace PKHeX.Core;
/// <see cref="RibbonRules"/>
public static class MarkRules
{
/// <summary>
/// Checks if the ribbon index is one of the specific SW/SH encounter-only marks. These marks are granted when the encounter spawns in the wild.
/// </summary>
public static bool IsEncounterMark(this RibbonIndex m) => (byte)m is >= (int)MarkLunchtime and <= (int)MarkSlump;
/// <summary>
/// Checks if an encounter-only mark is possible to obtain for the encounter, if not lost via data manipulation.
/// </summary>
@ -35,39 +30,50 @@ public static bool IsEncounterMarkLost(LegalityAnalysis data)
/// <summary>
/// Checks if a SW/SH mark is valid.
/// </summary>
public static bool IsMarkValid8(RibbonIndex mark, PKM pk, IEncounterTemplate enc)
public static bool IsEncounterMarkValid(RibbonIndex mark, PKM pk, IEncounterTemplate enc) => enc switch
{
return IsEncounterMarkAllowedAny(enc) && IsMarkAllowedSpecific(mark, pk, enc);
}
EncounterSlot8 or EncounterStatic8 { Gift: false, ScriptedNoMarks: false } => IsMarkAllowedSpecific8(mark, pk, enc),
EncounterSlot9 s => IsMarkAllowedSpecific9(mark, s),
_ => false,
};
/// <summary>
/// Checks if a specific encounter mark is disallowed.
/// </summary>
/// <returns>False if mark is disallowed based on specific conditions.</returns>
public static bool IsMarkAllowedSpecific(RibbonIndex mark, PKM pk, IEncounterTemplate x) => mark switch
public static bool IsMarkAllowedSpecific8(RibbonIndex mark, PKM pk, IEncounterTemplate x) => mark switch
{
MarkCurry when !IsMarkAllowedCurry(pk, x) => false,
MarkFishing when !IsMarkAllowedFishing(x) => false,
MarkMisty when x.Generation == 8 && pk.Met_Level < EncounterArea8.BoostLevel && EncounterArea8.IsBoostedArea60Fog(pk.Met_Location) => false,
MarkDestiny => x is EncounterSlot9, // Capture on Birthday
>= MarkCloudy and <= MarkMisty => IsWeatherPermitted(mark, x),
>= MarkCloudy and <= MarkMisty => IsWeatherPermitted8(mark, x),
_ => true,
};
private static bool IsWeatherPermitted(RibbonIndex mark, IEncounterTemplate enc)
/// <summary>
/// Checks if a specific encounter mark is disallowed.
/// </summary>
/// <returns>False if mark is disallowed based on specific conditions.</returns>
public static bool IsMarkAllowedSpecific9(RibbonIndex mark, EncounterSlot9 x) => mark switch
{
var permit = mark.GetWeather8();
MarkCurry => false,
MarkFishing => false,
MarkDestiny => true, // Capture on Birthday
>= MarkLunchtime and <= MarkDawn => x.CanSpawnAtTime(mark),
>= MarkCloudy and <= MarkMisty => x.CanSpawnInWeather(mark),
_ => true,
};
// Encounter slots check location weather, while static encounters check weather per encounter.
return enc switch
{
EncounterSlot8 w => IsSlotWeatherPermitted(permit, w),
EncounterStatic8 s => s.Weather.HasFlag(permit),
_ => false,
};
}
// Encounter slots check location weather, while static encounters check weather per encounter.
private static bool IsWeatherPermitted8(RibbonIndex mark, IEncounterTemplate enc) => enc switch
{
EncounterSlot8 w => IsSlotWeatherPermittedSWSH(mark.GetWeather8(), w),
EncounterStatic8 s => s.Weather.HasFlag(mark.GetWeather8()),
_ => false,
};
private static bool IsSlotWeatherPermitted(AreaWeather8 permit, EncounterSlot8 s)
private static bool IsSlotWeatherPermittedSWSH(AreaWeather8 permit, EncounterSlot8 s)
{
var location = s.Location;
// If it's not in the main table, it can only have Normal weather.
@ -85,18 +91,7 @@ private static bool IsSlotWeatherPermitted(AreaWeather8 permit, EncounterSlot8 s
}
/// <summary>
/// Checks if any encounter-only mark is available for the <see cref="enc"/>.
/// </summary>
public static bool IsEncounterMarkAllowedAny(IEncounterTemplate enc) => enc.Generation >= 8 && enc switch
{
// Gen 8
EncounterSlot8 or EncounterStatic8 { Gift: false, ScriptedNoMarks: false } => true,
EncounterSlot9 => true,
_ => false,
};
/// <summary>
/// Checks if a <see cref="RibbonIndex.MarkCurry"/> mark is valid.
/// Checks if a <see cref="MarkCurry"/> mark is valid.
/// </summary>
public static bool IsMarkAllowedCurry(PKM pk, IEncounterTemplate enc)
{
@ -109,7 +104,7 @@ public static bool IsMarkAllowedCurry(PKM pk, IEncounterTemplate enc)
}
/// <summary>
/// Checks if a <see cref="RibbonIndex.MarkFishing"/> mark is valid.
/// Checks if a <see cref="MarkFishing"/> mark is valid.
/// </summary>
public static bool IsMarkAllowedFishing(IEncounterTemplate enc)
{

View File

@ -129,7 +129,20 @@ public static class RibbonIndexExtensions
{
public static bool GetRibbonIndex(this IRibbonIndex x, RibbonIndex r) => x.GetRibbon((int)r);
public static void SetRibbonIndex(this IRibbonIndex x, RibbonIndex r, bool value = true) => x.SetRibbon((int)r, value);
public static bool IsMark(this RibbonIndex r) => r is >= MarkLunchtime and <= MarkSlump;
public static bool IsEncounterMark(this RibbonIndex r) => r is >= MarkLunchtime and <= MarkSlump;
/// <summary>
/// Checks if the ribbon index is one of the specific wild encounter-only marks. These marks are granted when the encounter spawns in the wild.
/// </summary>
public static bool HasEncounterMark(this IRibbonIndex m)
{
for (int i = (int)MarkLunchtime; i <= (int)MarkSlump; i++)
{
if (m.GetRibbon(i))
return true;
}
return false;
}
public static AreaWeather8 GetWeather8(this RibbonIndex x) => x switch
{
@ -147,7 +160,7 @@ public static class RibbonIndexExtensions
private enum RibbonIndexGroup : byte
{
None,
Mark,
EncounterMark,
CountMemory,
Common3,
Common4,
@ -161,8 +174,8 @@ private enum RibbonIndexGroup : byte
private static RibbonIndexGroup GetGroup(this RibbonIndex r)
{
if (r.IsMark())
return RibbonIndexGroup.Mark;
if (r.IsEncounterMark())
return RibbonIndexGroup.EncounterMark;
return r switch
{
ChampionG3 => RibbonIndexGroup.Common3,
@ -249,8 +262,8 @@ public static void Fix(this RibbonIndex r, RibbonVerifierArguments args, bool st
var group = r.GetGroup();
switch (group)
{
case RibbonIndexGroup.Mark:
r.FixMark(pk, state);
case RibbonIndexGroup.EncounterMark:
r.FixEncounterMark(pk, state);
return;
case RibbonIndexGroup.CountMemory:
if (pk is not IRibbonSetMemory6 m6)
@ -365,7 +378,7 @@ public static void Fix(this RibbonIndex r, RibbonVerifierArguments args, bool st
}
}
private static void FixMark(this RibbonIndex r, PKM pk, bool state)
private static void FixEncounterMark(this RibbonIndex r, PKM pk, bool state)
{
if (pk is not IRibbonSetMark8 m)
return;

View File

@ -36,4 +36,21 @@ public class LegalityData
t2.IsLevelUpRequired().Should().BeTrue();
}
}
[Fact]
public void EvolutionsOrderedSV()
{
// SV Crabrawler added a second, UseItem evolution method. Need to be sure it's before the more restrictive level-up method.
var tree = EvolutionTree.Evolves9;
var fEntries = typeof(EvolutionTree).GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(z => z.Name == "Entries");
if (fEntries.GetValue(tree) is not IReadOnlyList<EvolutionMethod[]> entries)
throw new ArgumentException(nameof(entries));
var crab = entries[(int)Species.Crabrawler];
var t1 = crab[0].Method;
var t2 = crab[1].Method;
t1.IsLevelUpRequired().Should().BeFalse();
t2.IsLevelUpRequired().Should().BeTrue();
}
}