diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionLineage.cs b/PKHeX.Core/Legality/Evolutions/EvolutionLineage.cs deleted file mode 100644 index 018344d8d..000000000 --- a/PKHeX.Core/Legality/Evolutions/EvolutionLineage.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace PKHeX.Core -{ - /// - /// Information pertaining to a 's evolution lineage. - /// - public sealed class EvolutionLineage - { - public readonly List> Chain = new List>(); - - public void Insert(EvolutionMethod entry) - { - int matchChain = -1; - for (int i = 0; i < Chain.Count; i++) - { - if (Chain[i].Any(e => e.Species == entry.Species)) - matchChain = i; - } - - if (matchChain != -1) - Chain[matchChain].Add(entry); - else - Chain.Insert(0, new List {entry}); - } - - public List GetExplicitLineage(PKM pkm, int maxLevel, bool skipChecks, int maxSpeciesOrigin, int minLevel) - { - int lvl = maxLevel; - var first = new EvoCriteria {Species = pkm.Species, Level = lvl, Form = pkm.AltForm}; - var dl = new List(3) { first }; - for (int i = Chain.Count - 1; i >= 0; i--) // reverse evolution! - { - bool oneValid = false; - foreach (var evo in Chain[i]) - { - if (!evo.Valid(pkm, lvl, skipChecks)) - continue; - - if (evo.RequiresLevelUp && minLevel >= lvl) - break; // impossible evolution - - oneValid = true; - UpdateMinValues(dl, evo); - int species = evo.Species; - - if (evo.RequiresLevelUp) - lvl--; - dl.Add(evo.GetEvoCriteria(species, lvl)); - break; - } - if (!oneValid) - break; - } - - // Remove future gen pre-evolutions; no Munchlax from a Gen3 Snorlax, no Pichu from a Gen1-only Raichu, etc - var last = dl[dl.Count - 1]; - if (last.Species > maxSpeciesOrigin && dl.Any(d => d.Species <= maxSpeciesOrigin)) - dl.RemoveAt(dl.Count - 1); - - // Last species is the wild/hatched species, the minimum level is 1 because it has not evolved from previous species - last = dl[dl.Count - 1]; - last.MinLevel = 1; - last.RequiresLvlUp = false; - return dl; - } - - private static void UpdateMinValues(IReadOnlyList dl, EvolutionMethod evo) - { - var last = dl[dl.Count - 1]; - if (!evo.RequiresLevelUp) - { - // Evolutions like elemental stones, trade, etc - last.MinLevel = 1; - return; - } - if (evo.Level == 0) - { - // Friendship based Evolutions, Pichu -> Pikachu, Eevee -> Umbreon, etc - last.MinLevel = 2; - - var first = dl[0]; - if (dl.Count > 1 && !first.RequiresLvlUp) - first.MinLevel = 2; // Raichu from Pikachu would have a minimum level of 1; accounting for Pichu (level up required) results in a minimum level of 2 - } - else // level up evolutions - { - last.MinLevel = evo.Level; - - var first = dl[0]; - if (dl.Count > 1) - { - if (first.RequiresLvlUp) - { - if (first.MinLevel <= evo.Level) - first.MinLevel = evo.Level + 1; // Pokemon like Crobat, its minimum level is Golbat minimum level + 1 - } - else - { - if (first.MinLevel < evo.Level) - first.MinLevel = evo.Level; // Pokemon like Nidoqueen who evolve with an evolution stone, minimum level is prior evolution minimum level - } - } - } - last.RequiresLvlUp = evo.RequiresLevelUp; - } - } -} \ No newline at end of file diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs b/PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs index c65912e3c..a1d9382bc 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using static PKHeX.Core.EvolutionType; namespace PKHeX.Core @@ -17,10 +14,7 @@ public sealed class EvolutionMethod public int Level; // Not stored in binary data - public bool RequiresLevelUp; // tracks if this method requires a Level Up - internal IReadOnlyCollection Banlist = Array.Empty(); - - internal static readonly IReadOnlyCollection BanSM = new[] {GameVersion.SN, GameVersion.MN}; + public bool RequiresLevelUp; // tracks if this method requires a Level Up, lazily set /// /// Checks the for validity by comparing against the data. @@ -28,16 +22,10 @@ public sealed class EvolutionMethod /// Entity to check /// Current level /// Option to skip some comparisons to return a 'possible' evolution. - /// + /// True if a evolution criteria is valid. public bool Valid(PKM pkm, int lvl, bool skipChecks) { RequiresLevelUp = false; - - // Check for unavailable evolution methods for an un-traded specimen. - // Example: Sun/Moon lack Ultra's Kantonian evolution methods. - if (!skipChecks && Banlist.Count > 0 && Banlist.Contains((GameVersion)pkm.Version) && pkm.IsUntraded) - return false; - switch ((EvolutionType)Method) { case UseItem: @@ -115,26 +103,14 @@ private bool HasMetLevelIncreased(PKM pkm, int lvl) } } - public EvoCriteria GetEvoCriteria(int species, int lvl) + public EvoCriteria GetEvoCriteria(int species, int form, int lvl) { return new EvoCriteria { Species = species, Level = lvl, - Form = Form, - Method = Method, - }; - } - - public EvolutionMethod Copy(int species, int form) - { - return new EvolutionMethod - { - Method = Method, - Species = species, - Argument = Argument, Form = form, - Level = Level + Method = Method, }; } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs b/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs index 8dd7886cc..4a90f3c5f 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using static PKHeX.Core.GameVersion; namespace PKHeX.Core @@ -38,7 +39,7 @@ static EvolutionTree() Evolves7b = new EvolutionTree(unpack("gg"), Gen7, PersonalTable.GG, Legal.MaxSpeciesID_7b); Evolves8 = new EvolutionTree(unpack("ss"), Gen8, PersonalTable.SWSH, Legal.MaxSpeciesID_8); - // There's always oddballs. + // Throw in banned evolution data! Evolves7.FixEvoTreeSM(); Evolves8.FixEvoTreeSS(); } @@ -74,10 +75,13 @@ internal static EvolutionTree GetEvolutionTree(PKM pkm, int generation) } private readonly IReadOnlyList Entries; - private readonly EvolutionLineage[] Lineage; private readonly GameVersion Game; private readonly PersonalTable Personal; private readonly int MaxSpeciesTree; + private readonly ILookup Lineage; + private static int GetLookupKey(int species, int form) => species | (form << 11); + + #region Constructor private EvolutionTree(IReadOnlyList data, GameVersion game, PersonalTable personal, int maxSpeciesTree) { @@ -85,7 +89,32 @@ private EvolutionTree(IReadOnlyList data, GameVersion game, PersonalTabl Personal = personal; MaxSpeciesTree = maxSpeciesTree; Entries = GetEntries(data); - Lineage = CreateTree(); + var connections = CreateTree(); + Lineage = connections.ToLookup(obj => obj.Key, obj => obj.Value); + } + + private IEnumerable> CreateTree() + { + for (int sSpecies = 1; sSpecies <= MaxSpeciesTree; sSpecies++) + { + var fc = Personal[sSpecies].FormeCount; + for (int sForm = 0; sForm < fc; sForm++) + { + var index = Personal.GetFormeIndex(sSpecies, sForm); + var evos = Entries[index]; + foreach (var evo in evos) + { + var dSpecies = evo.Species; + if (dSpecies == 0) + continue; + var dForm = evo.Form < 0 ? sForm : evo.Form; + var key = GetLookupKey(dSpecies, dForm); + + var link = new EvolutionLink(sSpecies, sForm, evo); + yield return new KeyValuePair(key, link); + } + } + } } private IReadOnlyList GetEntries(IReadOnlyList data) @@ -104,149 +133,34 @@ private IReadOnlyList GetEntries(IReadOnlyList data) }; } - private EvolutionLineage[] CreateTree() - { - var lineage = new EvolutionLineage[Entries.Count]; - for (int i = 0; i < Entries.Count; i++) - lineage[i] = new EvolutionLineage(); - if (Game == Gen6) - Array.Resize(ref lineage, MaxSpeciesTree + 1); - - if (Game.GetGeneration() <= 6) - GenerateEntriesSpeciesOnly(lineage); - else - GenerateEntriesSpeciesForm(lineage); - - return lineage; - } - - private void GenerateEntriesSpeciesOnly(IReadOnlyList lineage) - { - for (int species = 1; species < lineage.Count; species++) - CreateBranch(lineage, species, 0, species); - } - - private void GenerateEntriesSpeciesForm(IReadOnlyList lineage) - { - for (int species = 1; species <= MaxSpeciesTree; species++) - { - var pi = Personal[species]; - var fc = pi.FormeCount; - for (int form = 0; form < fc; form++) - { - var index = Personal.GetFormeIndex(species, form); - CreateBranch(lineage, species, form, index); - } - } - } - - private void CreateBranch(IReadOnlyList lineage, int species, int form, int index) - { - var evos = Entries[index]; - // Iterate over all possible evolutions - foreach (var evo in evos) - CreateLeaf(lineage, evo, species, form, index); - } - - private void CreateLeaf(IReadOnlyList lineage, EvolutionMethod evo, int species, int form, int index) - { - int evolveTo = GetIndex(evo); - if (evolveTo < 0) - return; - - var chainTo = lineage[evolveTo]; - var current = lineage[index]; - var sourceEvo = evo.Copy(species, form); - - chainTo.Insert(sourceEvo); - // If current entries has a pre-evolution, propagate to evolution as well - if (current.Chain.Count != 0) - chainTo.Chain.Insert(0, current.Chain[0]); - - if (evolveTo >= index) - return; - - // If destination species evolves into something (ie a 'baby' Pokemon like Cleffa) - // Add it to the corresponding parent chains - foreach (var method in Entries[evolveTo]) - { - int newIndex = GetIndex(method); - if (newIndex < 0) - continue; - - lineage[newIndex].Insert(sourceEvo); - } - } - private void FixEvoTreeSM() { - UnpackForms((int)Species.Wormadam, 2); - UnpackForms((int)Species.Gastrodon, 1); - UnpackForms((int)Species.Meowstic, 1); - UnpackForms((int)Species.Gourgeist, 3); - - BanEvo((int)Species.Raichu, 1, EvolutionMethod.BanSM); - BanEvo((int)Species.Marowak, 0, EvolutionMethod.BanSM); - BanEvo((int)Species.Raichu, 0, EvolutionMethod.BanSM); + // Sun/Moon lack Ultra's Kantonian evolution methods. + BanEvo((int)Species.Raichu, 1, pkm => pkm.IsUntraded && pkm.SM); + BanEvo((int)Species.Marowak, 0, pkm => pkm.IsUntraded && pkm.SM); + BanEvo((int)Species.Raichu, 0, pkm => pkm.IsUntraded && pkm.SM); } private void FixEvoTreeSS() { - SpreadForms((int)Species.Silvally, 17); + // Gigantamax Pikachu, Meowth-0, and Eevee are prevented from evolving. + BanEvo((int)Species.Raichu, 0, pkm => pkm is IGigantamax g && g.CanGigantamax); + BanEvo((int)Species.Raichu, 1, pkm => pkm is IGigantamax g && g.CanGigantamax); + BanEvo((int)Species.Persian, 0, pkm => pkm is IGigantamax g && g.CanGigantamax); + + foreach (var s in GetEvolutions((int)Species.Eevee, 0)) // Eeveelutions + BanEvo(s, 0, pkm => pkm is IGigantamax g && g.CanGigantamax); } - private void BanEvo(int species, int type, IReadOnlyCollection versionsBanned) + private void BanEvo(int species, int form, Func func) { - var entry = Personal.GetFormeIndex(species, 0); - var lin = Lineage[entry]; - lin.Chain[type][0].Banlist = versionsBanned; + var key = GetLookupKey(species, form); + var node = Lineage[key]; + foreach (var link in node) + link.IsBanned = func; } - private void UnpackForms(int species, int formCount) - { - var baseChain = Lineage[species]; - for (int i = 1; i <= formCount; i++) - { - var entry = Personal.GetFormeIndex(species, i); - var lin = Lineage[entry]; - lin.Chain.Add(new List { baseChain.Chain[0][i] }); - } - baseChain.Chain[0].RemoveRange(1, formCount); - } - - private void SpreadForms(int species, int formCount) - { - var baseChain = Lineage[species]; - for (int i = 1; i <= formCount; i++) - { - var entry = Personal.GetFormeIndex(species, i); - var lin = Lineage[entry]; - lin.Chain.Add(baseChain.Chain[0]); - } - } - - private int GetIndex(PKM pkm) - { - if (pkm.Format < 7) - return pkm.Species; - return Personal.GetFormeIndex(pkm.Species, pkm.AltForm); - } - - private int GetIndex(EvolutionMethod evo) - { - int evolvesToSpecies = evo.Species; - if (evolvesToSpecies == 0) - return -1; - - if (Personal == null) - return evolvesToSpecies; - - int evolvesToForm = evo.Form; - if (evolvesToForm < 0) - evolvesToForm = 0; - - return Personal.GetFormeIndex(evolvesToSpecies, evolvesToForm); - } + #endregion /// /// Gets a list of evolutions for the input by checking each evolution in the chain. @@ -259,10 +173,9 @@ private int GetIndex(EvolutionMethod evo) /// public List GetValidPreEvolutions(PKM pkm, int maxLevel, int maxSpeciesOrigin = -1, bool skipChecks = false, int minLevel = 1) { - int index = GetIndex(pkm); if (maxSpeciesOrigin <= 0) maxSpeciesOrigin = Legal.GetMaxSpeciesOrigin(pkm); - return Lineage[index].GetExplicitLineage(pkm, maxLevel, skipChecks, maxSpeciesOrigin, minLevel); + return GetExplicitLineage(pkm, maxLevel, skipChecks, maxSpeciesOrigin, minLevel); } /// @@ -282,28 +195,161 @@ public IEnumerable GetEvolutionsAndPreEvolutions(int species, int form) private IEnumerable GetPreEvolutions(int species, int form) { - int index = Personal.GetFormeIndex(species, form); + int index = GetLookupKey(species, form); var node = Lineage[index]; - foreach (var methods in node.Chain) + foreach (var method in node) { - foreach (var prevo in methods) - yield return prevo.Species; + var s = method.Species; + var f = method.Form; + yield return s; + var preEvolutions = GetPreEvolutions(s, f); + foreach (var preEvo in preEvolutions) + yield return preEvo; } } private IEnumerable GetEvolutions(int species, int form) { int index = Personal.GetFormeIndex(species, form); - var node = Entries[index]; - foreach (var z in node) + var node = Lineage[index]; + foreach (var method in node) { - var s = z.Species; - if (s == 0) - continue; + var s = method.Species; yield return s; foreach (var next in GetEvolutions(s, form)) yield return next; } } + + /// + /// Generates the reverse evolution path for the input . + /// + /// Entity data + /// Maximum level + /// Skip the secondary checks that validate the evolution + /// Clamp for maximum species ID + /// Minimum level + /// + private List GetExplicitLineage(PKM pkm, int maxLevel, bool skipChecks, int maxSpeciesOrigin, int minLevel) + { + int lvl = maxLevel; + var first = new EvoCriteria { Species = pkm.Species, Level = lvl, Form = pkm.AltForm }; + + const int maxEvolutions = 3; + var dl = new List(maxEvolutions) { first }; + + // There aren't any circular evolution paths, and all lineages have at most 3 evolutions total. + // There aren't any convergent evolution paths, so only yield the first connection. + int species = pkm.Species; + int form = pkm.AltForm; + while (true) + { + var key = GetLookupKey(species, form); + var node = Lineage[key]; + + bool oneValid = false; + foreach (var link in node) + { + if (link.IsEvolutionBanned(pkm)) + continue; + + var evo = link.Method; + if (!evo.Valid(pkm, lvl, skipChecks)) + continue; + + if (evo.RequiresLevelUp && minLevel >= lvl) + break; // impossible evolution + + oneValid = true; + UpdateMinValues(dl, evo); + if (evo.RequiresLevelUp) + lvl--; + + species = link.Species; + form = link.Form; + var detail = evo.GetEvoCriteria(species, form, lvl); + dl.Add(detail); + break; + } + if (!oneValid) + break; + } + + // Remove future gen pre-evolutions; no Munchlax from a Gen3 Snorlax, no Pichu from a Gen1-only Raichu, etc + var last = dl[dl.Count - 1]; + if (last.Species > maxSpeciesOrigin && dl.Any(d => d.Species <= maxSpeciesOrigin)) + dl.RemoveAt(dl.Count - 1); + + // Last species is the wild/hatched species, the minimum level is 1 because it has not evolved from previous species + last = dl[dl.Count - 1]; + last.MinLevel = 1; + last.RequiresLvlUp = false; + return dl; + } + + private static void UpdateMinValues(IReadOnlyList dl, EvolutionMethod evo) + { + var last = dl[dl.Count - 1]; + if (!evo.RequiresLevelUp) + { + // Evolutions like elemental stones, trade, etc + last.MinLevel = 1; + return; + } + if (evo.Level == 0) + { + // Friendship based Evolutions, Pichu -> Pikachu, Eevee -> Umbreon, etc + last.MinLevel = 2; + + var first = dl[0]; + if (dl.Count > 1 && !first.RequiresLvlUp) + first.MinLevel = 2; // Raichu from Pikachu would have a minimum level of 1; accounting for Pichu (level up required) results in a minimum level of 2 + } + else // level up evolutions + { + last.MinLevel = evo.Level; + + var first = dl[0]; + if (dl.Count > 1) + { + if (first.RequiresLvlUp) + { + if (first.MinLevel <= evo.Level) + first.MinLevel = evo.Level + 1; // Pokemon like Crobat, its minimum level is Golbat minimum level + 1 + } + else + { + if (first.MinLevel < evo.Level) + first.MinLevel = evo.Level; // Pokemon like Nidoqueen who evolve with an evolution stone, minimum level is prior evolution minimum level + } + } + } + last.RequiresLvlUp = evo.RequiresLevelUp; + } + + /// + /// Links a to the source and that the method can be triggered from. + /// + private sealed class EvolutionLink + { + public readonly int Species; + public readonly int Form; + public readonly EvolutionMethod Method; + public Func? IsBanned { private get; set; } + + public EvolutionLink(int species, int form, EvolutionMethod method) + { + Species = species; + Form = form; + Method = method; + } + + /// + /// Checks if the is allowed. + /// + /// Entity to check + /// True if banned, false if allowed. + public bool IsEvolutionBanned(PKM pkm) => IsBanned != null && IsBanned(pkm); + } } } diff --git a/PKHeX.Core/Resources/byte/personal_dp b/PKHeX.Core/Resources/byte/personal_dp index 671fa7899..c68bf8a78 100644 Binary files a/PKHeX.Core/Resources/byte/personal_dp and b/PKHeX.Core/Resources/byte/personal_dp differ diff --git a/PKHeX.Core/Resources/byte/personal_hgss b/PKHeX.Core/Resources/byte/personal_hgss index cca533668..401de3622 100644 Binary files a/PKHeX.Core/Resources/byte/personal_hgss and b/PKHeX.Core/Resources/byte/personal_hgss differ diff --git a/PKHeX.Core/Resources/byte/personal_pt b/PKHeX.Core/Resources/byte/personal_pt index e6a3e24fb..861dc50bb 100644 Binary files a/PKHeX.Core/Resources/byte/personal_pt and b/PKHeX.Core/Resources/byte/personal_pt differ