diff --git a/PKHeX.Core/Editing/Applicators/BallApplicator.cs b/PKHeX.Core/Editing/Applicators/BallApplicator.cs index d5962c444..03a32c96c 100644 --- a/PKHeX.Core/Editing/Applicators/BallApplicator.cs +++ b/PKHeX.Core/Editing/Applicators/BallApplicator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static PKHeX.Core.Ball; namespace PKHeX.Core; @@ -9,194 +8,127 @@ namespace PKHeX.Core; /// public static class BallApplicator { + private static readonly Ball[] BallList = Enum.GetValues(); + public const byte MaxBallSpanAlloc = (byte)LAOrigin + 1; + + /// + /// Requires checking the . + /// + /// + public static int GetLegalBalls(Span result, PKM pk) => GetLegalBalls(result, pk, new LegalityAnalysis(pk)); + + /// + public static int GetLegalBalls(Span result, PKM pk, LegalityAnalysis la) => GetLegalBalls(result, pk, la.EncounterOriginal); + /// /// Gets all balls that are legal for the input . /// - /// - /// Requires checking the for every that is tried. - /// + /// Result storage. /// Pokémon to retrieve a list of valid balls for. - /// Enumerable list of values that the is legal with. - public static IEnumerable GetLegalBalls(PKM pk) + /// Encounter matched to. + /// Count of values that the is legal with. + public static int GetLegalBalls(Span result, PKM pk, IEncounterTemplate enc) { - var clone = pk.Clone(); + if (enc.Species is (ushort)Species.Nincada && pk.Species is (ushort)Species.Shedinja) + return GetLegalBallsEvolvedShedinja(result, pk, enc); + return LoadLegalBalls(result, pk, enc); + } + + private static ReadOnlySpan ShedinjaEvolve4 => [Sport, Poke]; + + private static int GetLegalBallsEvolvedShedinja(Span result, PKM pk, IEncounterTemplate enc) + { + switch (enc) + { + case EncounterSlot4 when IsNincadaEvolveInOrigin(pk, enc): + ShedinjaEvolve4.CopyTo(result); + return ShedinjaEvolve4.Length; + case EncounterSlot3 when IsNincadaEvolveInOrigin(pk, enc): + return LoadLegalBalls(result, pk, enc); + } + result[0] = Poke; + return 1; + } + + private static bool IsNincadaEvolveInOrigin(PKM pk, IEncounterTemplate enc) + { + // Rough check to see if Nincada evolved in the origin context (Gen3/4). + // Does not do PID/IV checks to know the original met level. + var current = pk.CurrentLevel; + var met = pk.MetLevel; + if (pk.Format == enc.Generation) + return current > met; + return enc.LevelMin != met && current > enc.LevelMin; + } + + private static int LoadLegalBalls(Span result, PKM pk, IEncounterTemplate enc) + { + int ctr = 0; foreach (var b in BallList) { - var ball = (byte)b; - clone.Ball = ball; - if (clone.Ball != ball) - continue; // Some setters guard against out of bounds values. - if (new LegalityAnalysis(clone).Valid) - yield return b; + if (BallVerifier.VerifyBall(enc, b, pk).IsValid()) + result[ctr++] = b; } + return ctr; } /// /// Applies a random legal ball value if any exist. /// /// - /// Requires checking the for every that is tried. + /// Requires checking the . /// /// Pokémon to modify. public static byte ApplyBallLegalRandom(PKM pk) { Span balls = stackalloc Ball[MaxBallSpanAlloc]; - var count = GetBallListFromColor(pk, balls); + var count = GetLegalBalls(balls, pk); balls = balls[..count]; Util.Rand.Shuffle(balls); - return ApplyFirstLegalBall(pk, balls); + return ApplyFirstLegalBall(pk, balls, []); } + public static byte ApplyBallLegalByColor(PKM pk) => ApplyBallLegalByColor(pk, PersonalColorUtil.GetColor(pk)); + public static byte ApplyBallLegalByColor(PKM pk, PersonalColor color) => ApplyBallLegalByColor(pk, new LegalityAnalysis(pk), color); + public static byte ApplyBallLegalByColor(PKM pk, LegalityAnalysis la, PersonalColor color) => ApplyBallLegalByColor(pk, la.EncounterOriginal, color); + /// /// Applies a legal ball value if any exist, ordered by color. /// - /// - /// Requires checking the for every that is tried. - /// /// Pokémon to modify. - public static byte ApplyBallLegalByColor(PKM pk) + /// Encounter matched to. + /// Color preference to order by. + private static byte ApplyBallLegalByColor(PKM pk, IEncounterTemplate enc, PersonalColor color) { Span balls = stackalloc Ball[MaxBallSpanAlloc]; - GetBallListFromColor(pk, balls); - return ApplyFirstLegalBall(pk, balls); + var count = GetLegalBalls(balls, pk, enc); + balls = balls[..count]; + var prefer = PersonalColorUtil.GetPreferredByColor(enc, color); + return ApplyFirstLegalBall(pk, balls, prefer); } - /// - /// Applies a random ball value in a cyclical manner. - /// - /// Pokémon to modify. - public static byte ApplyBallNext(PKM pk) + private static byte ApplyFirstLegalBall(PKM pk, Span legal, ReadOnlySpan prefer) { - Span balls = stackalloc Ball[MaxBallSpanAlloc]; - GetBallList(pk.Ball, balls); - var next = balls[0]; - return pk.Ball = (byte)next; - } - - private static byte ApplyFirstLegalBall(PKM pk, ReadOnlySpan balls) - { - var initial = pk.Ball; - foreach (var b in balls) + foreach (var ball in prefer) { - var test = (byte)b; - pk.Ball = test; - if (new LegalityAnalysis(pk).Valid) - return test; + if (Contains(legal, ball)) + return pk.Ball = (byte)ball; } - return initial; // fail, revert - } - - private static int GetBallList(byte ball, Span result) - { - var balls = BallList; - var currentBall = (Ball)ball; - return GetCircularOnce(balls, currentBall, result); - } - - private static int GetBallListFromColor(PKM pk, Span result) - { - // Gen1/2 don't store color in personal info - var pi = pk.Format >= 3 ? pk.PersonalInfo : PersonalTable.USUM.GetFormEntry(pk.Species, 0); - var color = (PersonalColor)pi.Color; - var balls = BallColors[(int)color]; - var currentBall = (Ball)pk.Ball; - return GetCircularOnce(balls, currentBall, result); - } - - private static int GetCircularOnce(T[] items, T current, Span result) - { - var currentIndex = Array.IndexOf(items, current); - if (currentIndex < 0) - currentIndex = items.Length - 2; - return GetCircularOnce(items, currentIndex, result); - } - - private static int GetCircularOnce(ReadOnlySpan items, int startIndex, Span result) - { - var tail = items[(startIndex + 1)..]; - tail.CopyTo(result); - items[..startIndex].CopyTo(result[tail.Length..]); - return items.Length; - } - - private static readonly Ball[] BallList = Enum.GetValues(); - private static int MaxBallSpanAlloc => BallList.Length; - - static BallApplicator() - { - ReadOnlySpan exclude = [None, Poke]; - ReadOnlySpan end = [Poke]; - Span all = stackalloc Ball[BallList.Length - exclude.Length]; - all = all[..FillExcept(all, exclude, BallList)]; - - var colors = Enum.GetValues(); - foreach (var color in colors) + foreach (var ball in legal) { - int c = (int)color; - // Replace the array reference with a new array that appends non-matching values, followed by the end values. - var defined = BallColors[c]; - Span match = (BallColors[c] = new Ball[all.Length + end.Length]); - defined.CopyTo(match); - FillExcept(match[defined.Length..], defined, all); - end.CopyTo(match[^end.Length..]); + if (!Contains(prefer, ball)) + return pk.Ball = (byte)ball; } + return pk.Ball; // fail - static int FillExcept(Span result, ReadOnlySpan exclude, ReadOnlySpan all) + static bool Contains(ReadOnlySpan balls, Ball ball) { - int ctr = 0; - foreach (var b in all) + foreach (var b in balls) { - if (Contains(exclude, b)) - continue; - result[ctr++] = b; - } - return ctr; - - static bool Contains(ReadOnlySpan arr, Ball b) - { - foreach (var a in arr) - { - if (a == b) - return true; - } - return false; + if (b == ball) + return true; } + return false; } } - - /// - /// Priority Match ball IDs that match the color ID in descending order - /// - private static readonly Ball[][] BallColors = - [ - /* Red */ [Cherish, Repeat, Fast, Heal, Great, Dream, Lure], - /* Blue */ [Dive, Net, Great, Beast, Lure], - /* Yellow */ [Level, Ultra, Repeat, Quick, Moon], - /* Green */ [Safari, Friend, Nest, Dusk], - /* Black */ [Luxury, Heavy, Ultra, Moon, Net, Beast], - - /* Brown */ [Level, Heavy], - /* Purple */ [Master, Love, Dream, Heal], - /* Gray */ [Heavy, Premier, Luxury], - /* White */ [Premier, Timer, Luxury, Ultra], - /* Pink */ [Love, Dream, Heal], - ]; - - /// - /// Personal Data color IDs - /// - private enum PersonalColor : byte - { - Red, - Blue, - Yellow, - Green, - Black, - - Brown, - Purple, - Gray, - White, - Pink, - } } diff --git a/PKHeX.Core/Editing/Applicators/PersonalColorUtil.cs b/PKHeX.Core/Editing/Applicators/PersonalColorUtil.cs new file mode 100644 index 000000000..213d98120 --- /dev/null +++ b/PKHeX.Core/Editing/Applicators/PersonalColorUtil.cs @@ -0,0 +1,65 @@ +using System; + +namespace PKHeX.Core; + +public static class PersonalColorUtil +{ + public static PersonalColor GetColor(PKM pk) + { + // Gen1/2 don't store color in personal info + if (pk.Format < 3) + return (PersonalColor)PersonalTable.USUM[pk.Species, 0].Color; + return (PersonalColor)pk.PersonalInfo.Color; + } + + public static PersonalColor GetColor(IEncounterTemplate enc) + { + // Gen1/2 don't store color in personal info + if (enc.Generation < 3) + return (PersonalColor)PersonalTable.USUM[enc.Species, 0].Color; + + var pt = GameData.GetPersonal(enc.Version); + var pi = pt[enc.Species, enc.Form]; + return (PersonalColor)pi.Color; + } + + public static ReadOnlySpan GetPreferredByColor(IEncounterTemplate enc) => GetPreferredByColor(enc, GetColor(enc)); + + public static ReadOnlySpan GetPreferredByColor(T enc, PersonalColor color) where T : IVersion + { + if (enc.Version is GameVersion.PLA) + return GetPreferredByColorLA(color); + return GetPreferredByColor(color); + } + + /// + /// Priority Match ball IDs that match the color ID + /// + public static ReadOnlySpan GetPreferredByColor(PersonalColor color) => color switch + { + PersonalColor.Red => [Ball.Repeat, Ball.Fast, Ball.Heal, Ball.Great, Ball.Dream, Ball.Lure], + PersonalColor.Blue => [Ball.Dive, Ball.Net, Ball.Great, Ball.Lure, Ball.Beast], + PersonalColor.Yellow => [Ball.Level, Ball.Ultra, Ball.Repeat, Ball.Quick, Ball.Moon], + PersonalColor.Green => [Ball.Safari, Ball.Friend, Ball.Nest, Ball.Dusk], + PersonalColor.Black => [Ball.Luxury, Ball.Heavy, Ball.Ultra, Ball.Moon, Ball.Net, Ball.Beast], + PersonalColor.Brown => [Ball.Level, Ball.Heavy], + PersonalColor.Purple => [Ball.Master, Ball.Love, Ball.Heal, Ball.Dream], + PersonalColor.Gray => [Ball.Heavy, Ball.Premier, Ball.Luxury], + PersonalColor.White => [Ball.Premier, Ball.Timer, Ball.Luxury, Ball.Ultra], + _ => [Ball.Love, Ball.Heal, Ball.Dream], + }; + + public static ReadOnlySpan GetPreferredByColorLA(PersonalColor color) => color switch + { + PersonalColor.Red => [Ball.LAPoke], + PersonalColor.Blue => [Ball.LAFeather, Ball.LAGreat, Ball.LAJet], + PersonalColor.Yellow => [Ball.LAUltra], + PersonalColor.Green => [Ball.LAPoke], + PersonalColor.Black => [Ball.LAGigaton, Ball.LALeaden, Ball.LAHeavy, Ball.LAUltra], + PersonalColor.Brown => [Ball.LAPoke], + PersonalColor.Purple => [Ball.LAPoke], + PersonalColor.Gray => [Ball.LAGigaton, Ball.LALeaden, Ball.LAHeavy], + PersonalColor.White => [Ball.LAWing, Ball.LAJet], + _ => [Ball.LAPoke], + }; +} diff --git a/PKHeX.Core/PersonalInfo/Enums/EggGroup.cs b/PKHeX.Core/PersonalInfo/Enums/EggGroup.cs new file mode 100644 index 000000000..6d3c91253 --- /dev/null +++ b/PKHeX.Core/PersonalInfo/Enums/EggGroup.cs @@ -0,0 +1,24 @@ +namespace PKHeX.Core; + +/// +/// Personal Data egg groups for breeding compatibility +/// +public enum EggGroup : byte +{ + None = 0, + Monster = 1, + Water1 = 2, + Bug = 3, + Flying = 4, + Field = 5, + Fairy = 6, + Grass = 7, + HumanLike = 8, + Water3 = 9, + Mineral = 10, + Amorphous = 11, + Water2 = 12, + Ditto = 13, + Dragon = 14, + Undiscovered = 15, +} diff --git a/PKHeX.Core/PersonalInfo/Enums/PersonalColor.cs b/PKHeX.Core/PersonalInfo/Enums/PersonalColor.cs new file mode 100644 index 000000000..0114b91fc --- /dev/null +++ b/PKHeX.Core/PersonalInfo/Enums/PersonalColor.cs @@ -0,0 +1,19 @@ +namespace PKHeX.Core; + +/// +/// Personal Data color IDs +/// +public enum PersonalColor : byte +{ + Red = 0, + Blue = 1, + Yellow = 2, + Green = 3, + Black = 4, + + Brown = 5, + Purple = 6, + Gray = 7, + White = 8, + Pink = 9, +} diff --git a/PKHeX.WinForms/Controls/PKM Editor/BallBrowser.cs b/PKHeX.WinForms/Controls/PKM Editor/BallBrowser.cs index a8f128c88..8f10f9dd4 100644 --- a/PKHeX.WinForms/Controls/PKM Editor/BallBrowser.cs +++ b/PKHeX.WinForms/Controls/PKM Editor/BallBrowser.cs @@ -17,20 +17,21 @@ public partial class BallBrowser : Form public void LoadBalls(PKM pk) { - var legal = BallApplicator.GetLegalBalls(pk); - LoadBalls(legal, pk.MaxBallID + 1); + Span valid = stackalloc Ball[BallApplicator.MaxBallSpanAlloc]; + var legal = BallApplicator.GetLegalBalls(valid, pk); + LoadBalls(valid[..legal], pk.MaxBallID); } - private void LoadBalls(IEnumerable legal, int max) + private void LoadBalls(ReadOnlySpan legal, int max) { - Span flags = stackalloc bool[max]; + Span flags = stackalloc bool[BallApplicator.MaxBallSpanAlloc]; foreach (var ball in legal) flags[(int)ball] = true; int countLegal = 0; List controls = []; var names = GameInfo.BallDataSource; - for (byte ballID = 1; ballID < flags.Length; ballID++) + for (byte ballID = 1; ballID <= max; ballID++) { var name = GetBallName(ballID, names); var pb = GetBallView(ballID, name, flags[ballID]);