From 543359fff6f2663816da726077540073518ec735 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 7 Dec 2021 00:54:39 -0800 Subject: [PATCH] Add contest stat sheen checking Closes #3324 --- .../Formatting/LegalityCheckStrings.cs | 3 + .../Legality/Verifiers/ContestStatVerifier.cs | 86 +++++++--- .../Verifiers/Misc/ContestStatGranting.cs | 13 ++ .../Verifiers/Misc/ContestStatInfo.cs | 157 ++++++++++++++++++ .../Legality/Verifiers/TrainerNameVerifier.cs | 3 + PKHeX.Core/PKM/Shared/IContestStats.cs | 40 +++++ 6 files changed, 276 insertions(+), 26 deletions(-) create mode 100644 PKHeX.Core/Legality/Verifiers/Misc/ContestStatGranting.cs create mode 100644 PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs diff --git a/PKHeX.Core/Legality/Formatting/LegalityCheckStrings.cs b/PKHeX.Core/Legality/Formatting/LegalityCheckStrings.cs index e72bcd195..fa1e68ade 100644 --- a/PKHeX.Core/Legality/Formatting/LegalityCheckStrings.cs +++ b/PKHeX.Core/Legality/Formatting/LegalityCheckStrings.cs @@ -94,6 +94,9 @@ namespace PKHeX.Core public static string LBallUnavailable { get; set; } = "Ball unobtainable in origin Generation."; public static string LContestZero { get; set; } = "Contest Stats should be 0."; + public static string LContestZeroSheen { get; set; } = "Contest Stat Sheen should be 0."; + public static string LContestSheenTooLow_0 { get; set; } = "Contest Stat Sheen should be >= {0}."; + public static string LContestSheenTooHigh_0 { get; set; } = "Contest Stat Sheen should be <= {0}."; public static string LDateOutsideDistributionWindow { get; set; } = "Met Date is outside of distribution window."; diff --git a/PKHeX.Core/Legality/Verifiers/ContestStatVerifier.cs b/PKHeX.Core/Legality/Verifiers/ContestStatVerifier.cs index e0efdb888..6a1d5330d 100644 --- a/PKHeX.Core/Legality/Verifiers/ContestStatVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/ContestStatVerifier.cs @@ -1,33 +1,67 @@ -namespace PKHeX.Core +using static PKHeX.Core.ContestStatGranting; +using static PKHeX.Core.ContestStatInfo; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core; + +/// +/// Verifies the Contest stat details. +/// +public sealed class ContestStatVerifier : Verifier { - /// - /// Verifies the Contest stat details. - /// - public sealed class ContestStatVerifier : Verifier + protected override CheckIdentifier Identifier => CheckIdentifier.Memory; + public override void Verify(LegalityAnalysis data) { - protected override CheckIdentifier Identifier => CheckIdentifier.Memory; - public override void Verify(LegalityAnalysis data) + var pkm = data.pkm; + if (pkm is not IContestStats s) + return; + + // If no stats have been increased from the initial amount, then we're done here. + // some encounters have contest stats built in. they're already checked by the initial encounter match. + if (!s.HasContestStats()) + return; + + // Check the correlation of Stats & Sheen! + // In generations 3,4 and BDSP, blocks/poffins have a feel(sheen) equal to sheen=sum(stats)/5, with +/- 10% for a favored stat. + // In generation 6 (ORAS), they don't award any sheen, so any value is legal. + + var correlation = GetContestStatRestriction(pkm, data.Info.Generation); + if (correlation == None) { - var pkm = data.pkm; - if (pkm.Format <= 4) - return; // legal || not present - - if (pkm is IContestStats s && s.HasContestStats() && !CanHaveContestStats(pkm, s, data.Info.Generation)) - data.AddLine(GetInvalid(LegalityCheckStrings.LContestZero)); - - // some encounters have contest stats built in. they're already checked by the initial encounter match. + // We're only here because we have contest stat values. We aren't permitted to have any, so flag it. + data.AddLine(GetInvalid(LContestZero)); } - - private static bool CanHaveContestStats(PKM pkm, IContestStats s, int generation) => generation switch + else if (correlation == NoSheen) { - 1 => false, - 2 => false, - 3 => true, - 4 => true, - 5 => s.CNT_Sheen == 0 && pkm.Format >= 6, // ORAS Contests - 6 => s.CNT_Sheen == 0 && (!pkm.IsUntraded || pkm.AO), - 8 => pkm.BDSP, // BDSP Contests - _ => false, - }; + // We can get contest stat values, but we can't get any for Sheen. + // Any combination of non-sheen is ok, but nonzero sheen is illegal. + if (s.CNT_Sheen != 0) + data.AddLine(GetInvalid(LContestZeroSheen)); + } + else if (correlation == CorrelateSheen) + { + bool gen3 = data.Info.Generation == 3; + + // Check for stat values that exceed a valid sheen value. + var initial = GetReferenceTemplate(data.Info.EncounterMatch); + var minSheen = CalculateMinimumSheen(s, pkm.Nature, initial, gen3); + if (s.CNT_Sheen < minSheen) + data.AddLine(GetInvalid(string.Format(LContestSheenTooLow_0, minSheen))); + + // Check for sheen values that are too high. + var maxSheen = CalculateMaximumSheen(s, pkm.Nature, initial, gen3); + if (s.CNT_Sheen > maxSheen) + data.AddLine(GetInvalid(string.Format(LContestSheenTooHigh_0, maxSheen))); + } + else if (correlation == Mixed) + { + bool gen3 = data.Info.Generation == 3; + + // Check for sheen values that are too high. + var initial = GetReferenceTemplate(data.Info.EncounterMatch); + var maxSheen = CalculateMaximumSheen(s, pkm.Nature, initial, gen3); + if (s.CNT_Sheen > maxSheen) + data.AddLine(GetInvalid(string.Format(LContestSheenTooHigh_0, maxSheen))); + } } } diff --git a/PKHeX.Core/Legality/Verifiers/Misc/ContestStatGranting.cs b/PKHeX.Core/Legality/Verifiers/Misc/ContestStatGranting.cs new file mode 100644 index 000000000..39e571833 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/Misc/ContestStatGranting.cs @@ -0,0 +1,13 @@ +namespace PKHeX.Core; + +public enum ContestStatGranting +{ + /// Not possible to get any contest stats. + None, + /// Contest stats are possible to obtain, but must be correlated to sheen at most 1:1. + CorrelateSheen, + /// Contest stats are possible to obtain, but cannot obtain any sheen value. + NoSheen, + /// Contest stats are possible to obtain, and has visited a multitude of games such that any value of sheen is possible. + Mixed, +} diff --git a/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs b/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs new file mode 100644 index 000000000..a70f97d0f --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs @@ -0,0 +1,157 @@ +using System; +using static PKHeX.Core.ContestStatGranting; + +namespace PKHeX.Core; + +public static class ContestStatInfo +{ + private const int WorstFeelBlock = 3; + private const int WorstFeelPoffin = 17; + private const int MaxContestStat = 255; + + public static void SetSuggestedContestStats(PKM pk, IEncounterTemplate enc) + { + if (pk is not IContestStatsMutable s) + return; + + var restrict = GetContestStatRestriction(pk, pk.Generation); + if (restrict == None) + s.SetAllContestStatsTo(0, 0); // zero + if (pk.Species is not (int)Species.Milotic) + GetReferenceTemplate(enc).CopyContestStatsTo(s); // reset + else + s.SetAllContestStatsTo(MaxContestStat, restrict == NoSheen ? (byte)0 : (byte)255); + } + + public static ContestStatGranting GetContestStatRestriction(PKM pk, int origin) => origin switch + { + 3 => pk.Format < 6 ? CorrelateSheen : Mixed, + 4 => pk.Format < 6 ? CorrelateSheen : Mixed, + + 5 => pk.Format >= 6 ? NoSheen : None, // ORAS Contests + 6 => pk.AO || !pk.IsUntraded ? NoSheen : None, + 8 => pk.BDSP ? CorrelateSheen : None, // BDSP Contests + _ => None, + }; + + public static int CalculateMaximumSheen(IContestStats s, int nature, IContestStats initial, bool pokeBlock3) + { + if (s.IsAnyContestStatMax()) + return MaxContestStat; + + if (s.IsContestEqual(initial)) + return initial.CNT_Sheen; + + var avg = GetAverageFeel(s, nature, initial); + if (avg <= 0) + return initial.CNT_Sheen; + + if (pokeBlock3) + { + var fudge = (avg * 225) / 100; + return Math.Min(MaxContestStat, Math.Max(WorstFeelBlock, fudge)); + } + + // Can get trash poffins by burning and spilling on purpose. + return Math.Min(MaxContestStat, avg * WorstFeelPoffin); + } + + public static int CalculateMinimumSheen(IContestStats s, int nature, IContestStats initial, bool pokeBlock3) + { + if (s.IsContestEqual(initial)) + return initial.CNT_Sheen; + + var rawAvg = GetAverageFeel(s, 0, initial); + if (rawAvg == MaxContestStat) + return MaxContestStat; + + var avg = Math.Max(1, nature % 6 == 0 ? rawAvg : GetAverageFeel(s, nature, initial)); + avg = Math.Min(rawAvg, avg); // be generous + if (pokeBlock3) + return Math.Min(MaxContestStat, Math.Max(WorstFeelBlock, avg)); + else + return Math.Min(MaxContestStat, Math.Max(WorstFeelPoffin, avg)); + } + + private static int GetAverageFeel(IContestStats s, int nature, IContestStats initial) + { + ReadOnlySpan span = NatureAmpTable.AsSpan(5 * nature, 5); + int sum = 0; + sum += GetAmpedStat(span, 0, s.CNT_Cool - initial.CNT_Cool); + sum += GetAmpedStat(span, 1, s.CNT_Beauty - initial.CNT_Beauty); + sum += GetAmpedStat(span, 2, s.CNT_Cute - initial.CNT_Cute); + sum += GetAmpedStat(span, 3, s.CNT_Smart - initial.CNT_Smart); + sum += GetAmpedStat(span, 4, s.CNT_Tough - initial.CNT_Tough); + return sum / 5; + } + + private static int GetAmpedStat(ReadOnlySpan amps, int index, int gain) + { + var amp = amps[index]; + if (amp == 0) + return gain; + return gain + GetStatAdjustment(gain, amp); + } + + private static int GetStatAdjustment(int gain, sbyte amp) + { + // Undo the favor factor + var undoFactor = amp == 1 ? 11 : 9; + var boost = Boost(gain, undoFactor); + return amp == -1 ? boost : -boost; + + static int Boost(int stat, int factor) + { + var remainder = stat % factor; + var boost = stat / factor; + + if (remainder >= 5) + ++boost; + return boost; + } + } + + private static readonly DummyContestNone DummyNone = new(); + + public static IContestStats GetReferenceTemplate(IEncounterTemplate initial) => initial is IContestStats s ? s : DummyNone; + + private class DummyContestNone : IContestStats + { + public byte CNT_Cool => 0; + public byte CNT_Beauty => 0; + public byte CNT_Cute => 0; + public byte CNT_Smart => 0; + public byte CNT_Tough => 0; + public byte CNT_Sheen => 0; + } + + private static readonly sbyte[] NatureAmpTable = + { + // Spicy, Dry, Sweet, Bitter, Sour + 0, 0, 0, 0, 0, // Hardy + 1, 0, 0, 0,-1, // Lonely + 1, 0,-1, 0, 0, // Brave + 1,-1, 0, 0, 0, // Adamant + 1, 0, 0,-1, 0, // Naughty + -1, 0, 0, 0, 1, // Bold + 0, 0, 0, 0, 0, // Docile + 0, 0,-1, 0, 1, // Relaxed + 0,-1, 0, 0, 1, // Impish + 0, 0, 0,-1, 1, // Lax + -1, 0, 1, 0, 0, // Timid + 0, 0, 1, 0,-1, // Hasty + 0, 0, 0, 0, 0, // Serious + 0,-1, 1, 0, 0, // Jolly + 0, 0, 1,-1, 0, // Naive + -1, 1, 0, 0, 0, // Modest + 0, 1, 0, 0,-1, // Mild + 0, 1,-1, 0, 0, // Quiet + 0, 0, 0, 0, 0, // Bashful + 0, 1, 0,-1, 0, // Rash + -1, 0, 0, 1, 0, // Calm + 0, 0, 0, 1,-1, // Gentle + 0, 0,-1, 1, 0, // Sassy + 0,-1, 0, 1, 0, // Careful + 0, 0, 0, 0, 0, // Quirky + }; +} diff --git a/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs b/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs index 7d1f5d431..51a43eb49 100644 --- a/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs @@ -74,6 +74,9 @@ namespace PKHeX.Core return ot.Length <= len; } + if (e is EncounterTrade { HasTrainerName: true }) + return true; // already verified + if (e is MysteryGift mg && mg.OT_Name.Length == ot.Length) return true; // Mattle Ho-Oh return false; diff --git a/PKHeX.Core/PKM/Shared/IContestStats.cs b/PKHeX.Core/PKM/Shared/IContestStats.cs index 85c7b3b24..f267cd130 100644 --- a/PKHeX.Core/PKM/Shared/IContestStats.cs +++ b/PKHeX.Core/PKM/Shared/IContestStats.cs @@ -73,6 +73,23 @@ return true; } + public static bool IsContestEqual(this IContestStats current, IContestStats initial) + { + if (current.CNT_Cool != initial.CNT_Cool) + return false; + if (current.CNT_Beauty != initial.CNT_Beauty) + return false; + if (current.CNT_Cute != initial.CNT_Cute) + return false; + if (current.CNT_Smart != initial.CNT_Smart) + return false; + if (current.CNT_Tough != initial.CNT_Tough) + return false; + if (current.CNT_Sheen != initial.CNT_Sheen) + return false; + return true; + } + public static void CopyContestStatsTo(this IContestStats source, IContestStatsMutable dest) { dest.CNT_Cool = source.CNT_Cool; @@ -82,5 +99,28 @@ dest.CNT_Tough = source.CNT_Tough; dest.CNT_Sheen = source.CNT_Sheen; } + + public static void SetAllContestStatsTo(this IContestStatsMutable dest, byte value, byte sheen) + { + dest.CNT_Cool = value; + dest.CNT_Beauty = value; + dest.CNT_Cute = value; + dest.CNT_Smart = value; + dest.CNT_Tough = value; + dest.CNT_Sheen = sheen; + } + + private const byte CONTEST_MAX = 255; + + /// + /// Check if any contest stat besides is equal to . + /// + /// Entity to check + /// True if any equals + public static bool IsAnyContestStatMax(this IContestStats s) => CONTEST_MAX == s.CNT_Cool + || CONTEST_MAX == s.CNT_Beauty + || CONTEST_MAX == s.CNT_Cute + || CONTEST_MAX == s.CNT_Smart + || CONTEST_MAX == s.CNT_Tough; } }