mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-05-04 12:37:21 -05:00
243 lines
9.6 KiB
C#
243 lines
9.6 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Runtime.CompilerServices;
|
|
using static PKHeX.Core.ContestStatGranting;
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
/// <summary>
|
|
/// Logic for checking and applying <see cref="IContestStatsReadOnly"/>.
|
|
/// </summary>
|
|
public static class ContestStatInfo
|
|
{
|
|
private const int LowestFeelBlock3 = 1; // quad Nutpea
|
|
private const int LowestFeelPoffin4 = 11; // Beauty Haircut
|
|
private const int LowestFeelPoffin8b = 7;
|
|
private const int HighestFeelPoffin8b = 79;
|
|
|
|
private const byte MaxContestStat = 255;
|
|
|
|
/// <summary>
|
|
/// By giving out all-Nutpea blocks in 3, you can have contest stats all maxed while feel is at 214 if the random stats of the foul blocks line up perfectly.
|
|
/// </summary>
|
|
private const int BestSheenStat3 = 214;
|
|
|
|
/// <summary>
|
|
/// Using optimal poffins in BD/SP, roughly 120-140; be generous.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Liechi-Pomeg-Qualot-Occa etc. (2 each), which puts sheen at 140 and will max all stats if the cook times are 47.43 or faster
|
|
/// Leppa-Pomeg-Qualot-Occa etc. (3 each), which puts sheen at 135 and will max all stats if the cook times are 43.32 or faster
|
|
/// Liechi-Leppa-Pomeg-Qualot etc. (2 each), which puts sheen at 120 and will max all stats if the cook times area 40.40 or faster
|
|
/// </remarks>
|
|
private const int BestSheenStat8b = 120;
|
|
|
|
extension(PKM pk)
|
|
{
|
|
public void SetSuggestedContestStats(IEncounterTemplate enc, EvolutionHistory h)
|
|
{
|
|
if (pk is not IContestStats s)
|
|
return;
|
|
|
|
var restrict = GetContestStatRestriction(pk, pk.Generation, h);
|
|
var baseStat = GetReferenceTemplate(enc);
|
|
if (restrict == None || pk.Species is not (int)Species.Milotic)
|
|
baseStat.CopyContestStatsTo(s); // reset
|
|
else
|
|
s.SetAllContestStatsTo(MaxContestStat, restrict == NoSheen ? baseStat.ContestSheen : MaxContestStat);
|
|
}
|
|
|
|
public void SetMaxContestStats(IEncounterTemplate enc, EvolutionHistory h)
|
|
{
|
|
if (pk is not IContestStats s)
|
|
return;
|
|
var restrict = GetContestStatRestriction(pk, enc.Generation, h);
|
|
var baseStat = GetReferenceTemplate(enc);
|
|
if (restrict == None)
|
|
return;
|
|
s.SetAllContestStatsTo(MaxContestStat, restrict == NoSheen ? baseStat.ContestSheen : MaxContestStat);
|
|
}
|
|
}
|
|
|
|
public static ContestStatGranting GetContestStatRestriction(PKM pk, byte origin, EvolutionHistory h) => origin switch
|
|
{
|
|
3 when pk.Format == 3 && !ParseSettings.AllowGBACrossTransferRSE(pk) => None,
|
|
3 => pk.Format < 6 ? CorrelateSheen : Mixed,
|
|
4 => pk.Format < 6 ? CorrelateSheen : Mixed,
|
|
|
|
5 => pk.Format < 6 ? None : !h.HasVisitedBDSP ? NoSheen : Mixed, // OR/AS Contests
|
|
6 => pk is { AO: false, IsUntraded: true } ? None : !h.HasVisitedBDSP ? NoSheen : Mixed,
|
|
|
|
_ => h.HasVisitedBDSP ? CorrelateSheen : None, // BD/SP Contests
|
|
};
|
|
|
|
public static byte CalculateMaximumSheen(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial, bool pokeBlock3)
|
|
{
|
|
if (s.IsAnyContestStatMax())
|
|
return MaxContestStat;
|
|
|
|
if (s.IsContestEqual(initial))
|
|
return initial.ContestSheen;
|
|
|
|
if (pokeBlock3)
|
|
return CalculateMaximumSheen3(s, nature, initial);
|
|
|
|
var avg = GetAverageFeel(s, nature, initial);
|
|
if (avg <= 0)
|
|
return initial.ContestSheen;
|
|
|
|
if (avg <= 2)
|
|
return 59;
|
|
|
|
// Can get trash poffins by burning and spilling on purpose.
|
|
return (byte)Math.Min(MaxContestStat, avg * HighestFeelPoffin8b);
|
|
}
|
|
|
|
public static byte CalculateMinimumSheen(IContestStatsReadOnly s, IContestStatsReadOnly initial, INature pk, ContestStatGrantingSheen method) => method switch
|
|
{
|
|
ContestStatGrantingSheen.Gen8b => CalculateMinimumSheen8b(s, pk.Nature, initial),
|
|
ContestStatGrantingSheen.Gen3 => CalculateMinimumSheen3(s, pk.Nature, initial),
|
|
ContestStatGrantingSheen.Gen4 => CalculateMinimumSheen4(s, pk.Nature, initial),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(method)),
|
|
};
|
|
|
|
// BD/SP has a slightly better stat:sheen ratio than Gen4; prefer if it has visited.
|
|
public static byte CalculateMinimumSheen8b(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial)
|
|
{
|
|
if (s.IsContestEqual(initial))
|
|
return initial.ContestSheen;
|
|
|
|
var rawAvg = GetAverageFeel(s, 0, initial);
|
|
if (rawAvg == MaxContestStat)
|
|
return BestSheenStat8b;
|
|
|
|
var avg = Math.Max(1, (byte)nature % 6 == 0 ? rawAvg : GetAverageFeel(s, nature, initial));
|
|
avg = Math.Min(rawAvg, avg); // be generous
|
|
avg = (BestSheenStat8b * avg) / MaxContestStat;
|
|
|
|
return (byte)Math.Clamp(avg, LowestFeelPoffin8b, BestSheenStat8b);
|
|
}
|
|
|
|
public static byte CalculateMinimumSheen3(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial)
|
|
{
|
|
if (s.IsContestEqual(initial))
|
|
return initial.ContestSheen;
|
|
|
|
var rawAvg = GetAverageFeel(s, 0, initial);
|
|
if (rawAvg == MaxContestStat)
|
|
return BestSheenStat3;
|
|
|
|
var avg = Math.Max(1, (byte)nature % 6 == 0 ? rawAvg : GetAverageFeel(s, nature, initial));
|
|
avg = Math.Min(rawAvg, avg); // be generous
|
|
|
|
avg = (BestSheenStat3 * avg) / MaxContestStat;
|
|
return (byte)Math.Clamp(avg, LowestFeelBlock3, BestSheenStat3);
|
|
}
|
|
|
|
public static byte CalculateMinimumSheen4(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial)
|
|
{
|
|
if (s.IsContestEqual(initial))
|
|
return initial.ContestSheen;
|
|
|
|
var rawAvg = GetAverageFeel(s, 0, initial);
|
|
if (rawAvg == MaxContestStat)
|
|
return MaxContestStat;
|
|
|
|
var avg = Math.Max(1, (byte)nature % 6 == 0 ? rawAvg : GetAverageFeel(s, nature, initial));
|
|
avg = Math.Min(rawAvg, avg); // be generous
|
|
|
|
return (byte)Math.Clamp(avg, LowestFeelPoffin4, MaxContestStat);
|
|
}
|
|
|
|
private static byte CalculateMaximumSheen3(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial)
|
|
{
|
|
// By using Enigma and Lansat and a 25 +1/-1, can get a +9/+19s at minimum RPM
|
|
// By using Strib, Chilan, Niniku, or Topo, can get a black +2/2/2 & 83 block (6:83) at minimum RPM.
|
|
// https://github.com/kwsch/PKHeX/issues/3517
|
|
var sum = GetGainedSum(s, nature, initial);
|
|
if (sum == 0)
|
|
return 0;
|
|
|
|
static int Gained2(byte a, byte b) => a - b >= 2 ? 1 : 0;
|
|
int gained = Gained2(s.ContestCool, initial.ContestCool)
|
|
+ Gained2(s.ContestBeauty, initial.ContestBeauty)
|
|
+ Gained2(s.ContestCute, initial.ContestCute)
|
|
+ Gained2(s.ContestSmart, initial.ContestSmart)
|
|
+ Gained2(s.ContestTough, initial.ContestTough);
|
|
bool has3 = gained >= 3;
|
|
|
|
// Prefer the bad-black-block correlation if more than 3 stats have gains >= 2.
|
|
var permit = has3 ? (sum * 83 / 6) : (sum * 19 / 9);
|
|
return (byte)Math.Clamp(permit, LowestFeelBlock3, MaxContestStat);
|
|
}
|
|
|
|
private static int GetAverageFeel(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial)
|
|
{
|
|
var sum = GetGainedSum(s, nature, initial);
|
|
return (int)Math.Ceiling(sum / 5f);
|
|
}
|
|
|
|
// Indexes into the NatureAmpTable
|
|
private const int AmpIndexCool = 0; // Spicy
|
|
private const int AmpIndexTough = 1; // Sour
|
|
private const int AmpIndexBeauty = 2; // Dry
|
|
private const int AmpIndexSmart = 3; // Bitter
|
|
private const int AmpIndexCute = 4; // Sweet
|
|
|
|
private static int GetGainedSum(IContestStatsReadOnly s, Nature nature, IContestStatsReadOnly initial)
|
|
{
|
|
var span = NatureAmp.GetAmps(nature);
|
|
int sum = 0;
|
|
sum += GetAmpedStat(span, AmpIndexCool, s.ContestCool, initial.ContestCool);
|
|
sum += GetAmpedStat(span, AmpIndexTough, s.ContestTough, initial.ContestTough);
|
|
sum += GetAmpedStat(span, AmpIndexBeauty, s.ContestBeauty, initial.ContestBeauty);
|
|
sum += GetAmpedStat(span, AmpIndexSmart, s.ContestSmart, initial.ContestSmart);
|
|
sum += GetAmpedStat(span, AmpIndexCute, s.ContestCute, initial.ContestCute);
|
|
return sum;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int GetAmpedStat(ReadOnlySpan<sbyte> amps, [ConstantExpected] int index, byte current, byte initial)
|
|
{
|
|
var gain = current - initial;
|
|
if (gain <= 0)
|
|
return 0;
|
|
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 IContestStatsReadOnly GetReferenceTemplate(IEncounterTemplate initial) => initial as IContestStatsReadOnly ?? DummyNone;
|
|
|
|
private sealed class DummyContestNone : IContestStatsReadOnly
|
|
{
|
|
public byte ContestCool => 0;
|
|
public byte ContestBeauty => 0;
|
|
public byte ContestCute => 0;
|
|
public byte ContestSmart => 0;
|
|
public byte ContestTough => 0;
|
|
public byte ContestSheen => 0;
|
|
}
|
|
}
|