diff --git a/PKHeX.Core/Legality/Analysis.cs b/PKHeX.Core/Legality/Analysis.cs index d27d0052e..454c47c6f 100644 --- a/PKHeX.Core/Legality/Analysis.cs +++ b/PKHeX.Core/Legality/Analysis.cs @@ -164,7 +164,7 @@ private void ParseLegality() private void ParsePK1() { - pkm.TradebackStatus = Legal.GetTradebackStatusInitial(pkm); + pkm.TradebackStatus = GBRestrictions.GetTradebackStatusInitial(pkm); UpdateInfo(); if (pkm.TradebackStatus == TradebackType.Any && Info.Generation != pkm.Format) pkm.TradebackStatus = TradebackType.WasTradeback; // Example: GSC Pokemon with only possible encounters in RBY, like the legendary birds diff --git a/PKHeX.Core/Legality/Core.cs b/PKHeX.Core/Legality/Core.cs index 63f124ea5..135f61b65 100644 --- a/PKHeX.Core/Legality/Core.cs +++ b/PKHeX.Core/Legality/Core.cs @@ -65,49 +65,6 @@ static Legal() public static void RefreshMGDB(string localDbPath) => EncounterEvent.RefreshMGDB(localDbPath); - // Moves - internal static int[] GetMinLevelLearnMoveG1(int species, List moves) - { - var r = new int[moves.Count]; - for (int i = 0; i < r.Length; i++) - r[i] = MoveLevelUp.GetIsLevelUp1(species, moves[i], 100, 0, 0).Level; - return r; - } - - internal static int[] GetMaxLevelLearnMoveG1(int species, List moves) - { - var r = new int[moves.Count]; - - int index = PersonalTable.RB.GetFormeIndex(species, 0); - if (index == 0) - return r; - - var pi_rb = ((PersonalInfoG1)PersonalTable.RB[index]).Moves; - var pi_y = ((PersonalInfoG1)PersonalTable.Y[index]).Moves; - - for (int m = 0; m < moves.Count; m++) - { - bool start = pi_rb.Contains(moves[m]) && pi_y.Contains(moves[m]); - r[m] = start ? 1 : Math.Max(GetHighest(LevelUpRB), GetHighest(LevelUpY)); - int GetHighest(IReadOnlyList learn) => learn[index].GetLevelLearnMove(moves[m]); - } - return r; - } - - internal static List[] GetExclusiveMovesG1(int species1, int species2, IEnumerable tmhm, IEnumerable moves) - { - // Return from two species the exclusive moves that only one could learn and also the current pokemon have it in its current moveset - var moves1 = MoveLevelUp.GetMovesLevelUp1(species1, 0, 1, 100); - var moves2 = MoveLevelUp.GetMovesLevelUp1(species2, 0, 1, 100); - - // Remove common moves and remove tmhm, remove not learned moves - var common = new HashSet(moves1.Intersect(moves2).Concat(tmhm)); - var hashMoves = new HashSet(moves); - moves1.RemoveAll(x => !hashMoves.Contains(x) || common.Contains(x)); - moves2.RemoveAll(x => !hashMoves.Contains(x) || common.Contains(x)); - return new[] { moves1, moves2 }; - } - internal static List[] GetValidMovesAllGens(PKM pkm, IReadOnlyList[] evoChains, int minLvLG1 = 1, int minLvLG2 = 1, bool LVL = true, bool Tutor = true, bool Machine = true, bool MoveReminder = true, bool RemoveTransferHM = true) { var Moves = new List[evoChains.Length]; @@ -336,202 +293,6 @@ internal static IEnumerable GetExclusivePreEvolutionMoves(PKM pkm, int Spec return preevomoves.Except(evomoves).Distinct(); } - // Encounter - internal static IEnumerable GetGen2Versions(LegalInfo Info) - { - if (ParseSettings.AllowGen2Crystal(Info.Korean) && Info.Game == GameVersion.C) - yield return GameVersion.C; - - // Any encounter marked with version GSC is for pokemon with the same moves in GS and C - // it is sufficient to check just GS's case - yield return GameVersion.GS; - } - - internal static IEnumerable GetGen1Versions(LegalInfo Info) - { - if (Info.EncounterMatch.Species == 133 && Info.Game == GameVersion.Stadium) - { - // Stadium Eevee; check for RB and yellow initial moves - yield return GameVersion.RB; - yield return GameVersion.YW; - yield break; - } - if (Info.Game == GameVersion.YW) - { - yield return GameVersion.YW; - yield break; - } - - // Any encounter marked with version RBY is for pokemon with the same moves and catch rate in RB and Y, - // it is sufficient to check just RB's case - yield return GameVersion.RB; - } - - internal static int GetRequiredMoveCount(PKM pk, int[] moves, LegalInfo info, int[] initialmoves) - { - if (pk.Format != 1 || !pk.Gen1_NotTradeback) // No Move Deleter in Gen 1 - return 1; // Move Deleter exits, slots from 2 onwards can allways be empty - - int required = GetRequiredMoveCount(pk, moves, info.EncounterMoves.LevelUpMoves, initialmoves); - if (required >= 4) - return 4; - - // tm, hm and tutor moves replace a free slots if the pokemon have less than 4 moves - // Ignore tm, hm and tutor moves already in the learnset table - var learn = info.EncounterMoves.LevelUpMoves; - var tmhm = info.EncounterMoves.TMHMMoves; - var tutor = info.EncounterMoves.TutorMoves; - var union = initialmoves.Union(learn[1]); - required += moves.Count(m => m != 0 && union.All(t => t != m) && (tmhm[1].Any(t => t == m) || tutor[1].Any(t => t == m))); - - return Math.Min(4, required); - } - - private static int GetRequiredMoveCount(PKM pk, int[] moves, List[] learn, int[] initialmoves) - { - if (SpecialMinMoveSlots.Contains(pk.Species)) - return GetRequiredMoveCountSpecial(pk, moves, learn); - - // A pokemon is captured with initial moves and can't forget any until have all 4 slots used - // If it has learn a move before having 4 it will be in one of the free slots - int required = GetRequiredMoveSlotsRegular(pk, moves, learn, initialmoves); - return required != 0 ? required : GetRequiredMoveCountDecrement(pk, moves, learn, initialmoves); - } - - private static int GetRequiredMoveSlotsRegular(PKM pk, int[] moves, List[] learn, int[] initialmoves) - { - int species = pk.Species; - int catch_rate = ((PK1)pk).Catch_Rate; - // Caterpie and Metapod evolution lines have different count of possible slots available if captured in different evolutionary phases - // Example: a level 7 caterpie evolved into metapod will have 3 learned moves, a captured metapod will have only 1 move - if ((species == 011 || species == 012) && catch_rate == 120) - { - // Captured as Metapod without Caterpie moves - return initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && !G1CaterpieMoves.Contains(lm)); - // There is no valid Butterfree encounter in generation 1 games - } - if ((species == 014 || species == 015) && (catch_rate == 45 || catch_rate == 120)) - { - if (species == 15 && catch_rate == 45) // Captured as Beedril without Weedle and Kakuna moves - return initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && !G1KakunaMoves.Contains(lm)); - - // Captured as Kakuna without Weedle moves - return initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && !G1WeedleMoves.Contains(lm)); - } - - return IsMoveCountRequired3(species, pk.CurrentLevel, moves) ? 3 : 0; // no match - } - - private static bool IsMoveCountRequired3(int species, int level, int[] moves) - { - // Species that evolve and learn the 4th move as evolved species at a greather level than base species - // The 4th move is included in the level up table set as a preevolution move, - // it should be removed from the used slots count if is not the learn move - switch (species) - { - case 017: return level < 21 && !moves.Contains(018); // Pidgeotto without Whirlwind - case 028: return level < 27 && !moves.Contains(040); // Sandslash without Poison Sting - case 047: return level < 30 && !moves.Contains(147); // Parasect without Spore - case 055: return level < 39 && !moves.Contains(093); // Golduck without Confusion - case 087: return level < 44 && !moves.Contains(156); // Dewgong without Rest - case 093: - case 094: return level < 29 && !moves.Contains(095); // Haunter/Gengar without Hypnosis - case 110: return level < 39 && !moves.Contains(108); // Weezing without Smoke Screen - } - return false; - } - - private static int GetRequiredMoveCountDecrement(PKM pk, int[] moves, List[] learn, int[] initialmoves) - { - int usedslots = initialmoves.Union(learn[1]).Where(m => m != 0).Distinct().Count(); - switch (pk.Species) - { - case 031: // Venonat; ignore Venomoth (by the time Venonat evolves it will always have 4 moves) - if (pk.CurrentLevel >= 11 && !moves.Contains(48)) // Supersonic - usedslots--; - if (pk.CurrentLevel >= 19 && !moves.Contains(93)) // Confusion - usedslots--; - break; - case 064: case 065: // Abra & Kadabra - int catch_rate = ((PK1)pk).Catch_Rate; - if (catch_rate != 100)// Initial Yellow Kadabra Kinesis (move 134) - usedslots--; - if (catch_rate == 200 && pk.CurrentLevel < 20) // Kadabra Disable, not learned until 20 if captured as Abra (move 50) - usedslots--; - break; - case 104: case 105: // Cubone & Marowak - if (!moves.Contains(39)) // Initial Yellow Tail Whip - usedslots--; - if (!moves.Contains(125)) // Initial Yellow Bone Club - usedslots--; - if (pk.Species == 105 && pk.CurrentLevel < 33 && !moves.Contains(116)) // Marowak evolved without Focus Energy - usedslots--; - break; - case 113: - if (!moves.Contains(39)) // Yellow Initial Tail Whip - usedslots--; - if (!moves.Contains(3)) // Yellow Lvl 12 and Initial Red/Blue Double Slap - usedslots--; - break; - case 056 when pk.CurrentLevel >= 9 && !moves.Contains(67): // Mankey (Low Kick) - case 127 when pk.CurrentLevel >= 21 && !moves.Contains(20): // Pinsir (Bind) - case 130 when pk.CurrentLevel < 32: // Gyarados - usedslots--; - break; - } - return usedslots; - } - - private static int GetRequiredMoveCountSpecial(PKM pk, int[] moves, List[] learn) - { - // Species with few mandatory slots, species with stone evolutions that could evolve at lower level and do not learn any more moves - // and Pikachu and Nidoran family, those only have mandatory the initial moves and a few have one level up moves, - // every other move could be avoided switching game or evolving - var mandatory = GetRequiredMoveCountLevel(pk); - switch (pk.Species) - { - case 103 when pk.CurrentLevel >= 28: // Exeggutor - // At level 28 learn different move if is a Exeggute or Exeggutor - if (moves.Contains(73)) - mandatory.Add(73); // Leech Seed level 28 Exeggute - if (moves.Contains(23)) - mandatory.Add(23); // Stomp level 28 Exeggutor - break; - case 25 when pk.CurrentLevel >= 33: - mandatory.Add(97); // Pikachu always learns Agility - break; - case 114: - mandatory.Add(132); // Tangela always has Constrict as Initial Move - break; - } - - // Add to used slots the non-mandatory moves from the learnset table that the pokemon have learned - return mandatory.Count + moves.Count(m => m != 0 && mandatory.All(l => l != m) && learn[1].Any(t => t == m)); - } - - private static List GetRequiredMoveCountLevel(PKM pk) - { - int species = pk.Species; - int basespecies = GetBaseSpecies(pk); - int maxlevel = 1; - int minlevel = 1; - - if (species == 114) // Tangela moves before level 32 are different in RB vs Y - { - minlevel = 32; - maxlevel = pk.CurrentLevel; - } - else if (029 <= species && species <= 034 && pk.CurrentLevel >= 8) - { - maxlevel = 8; // Always learns a third move at level 8 - } - - if (minlevel > pk.CurrentLevel) - return new List(); - - return MoveLevelUp.GetMovesLevelUp1(basespecies, 0, maxlevel, minlevel); - } - internal static bool GetWasEgg23(PKM pkm) { if (pkm.IsEgg) @@ -920,65 +681,6 @@ internal static int GetMinLevelEncounter(PKM pkm) return pkm.HasOriginalMetLocation ? pkm.Met_Level : GetMaxLevelGeneration(pkm); } - private static bool GetCatchRateMatchesPreEvolution(PKM pkm, int catch_rate, IEnumerable gen1) - { - // For species catch rate, discard any species that has no valid encounters and a different catch rate than their pre-evolutions - var Lineage = gen1.Except(Species_NotAvailable_CatchRate); - return IsCatchRateRBY(Lineage) || IsCatchRateTrade() || IsCatchRateStadium(); - - // Dragonite's Catch Rate is different than Dragonair's in Yellow, but there is no Dragonite encounter. - bool IsCatchRateRBY(IEnumerable ds) => ds.Any(s => catch_rate == PersonalTable.RB[s].CatchRate || (s != 149 && catch_rate == PersonalTable.Y[s].CatchRate)); - // Krabby encounter trade special catch rate - bool IsCatchRateTrade() => (pkm.Species == 098 || pkm.Species == 099) && catch_rate == 204; - bool IsCatchRateStadium() => Stadium_GiftSpecies.Contains(pkm.Species) && Stadium_CatchRate.Contains(catch_rate); - } - - /// - /// Gets the Tradeback status depending on various values. - /// - /// Pokémon to guess the tradeback status from. - internal static TradebackType GetTradebackStatusInitial(PKM pkm) - { - if (pkm is PK1 pk1) - return GetTradebackStatusRBY(pk1); - - if (pkm.Format == 2 || pkm.VC2) // Check for impossible tradeback scenarios - return !pkm.CanInhabitGen1() ? TradebackType.Gen2_NotTradeback : TradebackType.Any; - - // VC2 is released, we can assume it will be TradebackType.Any. - // Is impossible to differentiate a VC1 pokemon traded to Gen7 after VC2 is available. - // Met Date cannot be used definitively as the player can change their system clock. - return TradebackType.Any; - } - - /// - /// Gets the Tradeback status depending on the - /// - /// Pokémon to guess the tradeback status from. - internal static TradebackType GetTradebackStatusRBY(PK1 pkm) - { - if (!ParseSettings.AllowGen1Tradeback) - return TradebackType.Gen1_NotTradeback; - - // Detect tradeback status by comparing the catch rate(Gen1)/held item(Gen2) to the species in the pkm's evolution chain. - var catch_rate = pkm.Catch_Rate; - if (catch_rate == 0) - return TradebackType.WasTradeback; - - var table = EvolutionTree.GetEvolutionTree(1); - var lineage = table.GetValidPreEvolutions(pkm, maxLevel: pkm.CurrentLevel); - var gen1 = lineage.Select(evolution => evolution.Species); - bool matchAny = GetCatchRateMatchesPreEvolution(pkm, catch_rate, gen1); - - if (!matchAny) - return TradebackType.WasTradeback; - - if (HeldItems_GSC.Contains((ushort) catch_rate)) - return TradebackType.Any; - - return TradebackType.Gen1_NotTradeback; - } - internal static bool IsCatchRateHeldItem(int rate) => ParseSettings.AllowGen1Tradeback && HeldItems_GSC.Contains((ushort) rate); private static IEnumerable GetValidMoves(PKM pkm, GameVersion Version, IReadOnlyList> vs, int minLvLG1 = 1, int minLvLG2 = 1, bool LVL = false, bool Relearn = false, bool Tutor = false, bool Machine = false, bool MoveReminder = true, bool RemoveTransferHM = true) @@ -1082,35 +784,6 @@ private static IEnumerable GetMoves(PKM pkm, int species, int minlvlG1, int return r.Distinct(); } - internal static bool IsTradedKadabraG1(PKM pkm) - { - if (!(pkm is PK1 pk1) || pk1.Species != 64) - return false; - if (pk1.TradebackStatus == TradebackType.WasTradeback) - return true; - if (ParseSettings.ActiveTrainer.Game == (int)GameVersion.Any) - return false; - var IsYellow = ParseSettings.ActiveTrainer.Game == (int)GameVersion.YW; - if (pk1.TradebackStatus == TradebackType.Gen1_NotTradeback) - { - // If catch rate is Abra catch rate it wont trigger as invalid trade without evolution, it could be traded as Abra - // Yellow Kadabra catch rate in Red/Blue game, must be Alakazam - var table = IsYellow ? PersonalTable.RB : PersonalTable.Y; - if (pk1.Catch_Rate == table[64].CatchRate) - return true; - } - if (IsYellow) - return false; - // Yellow only moves in Red/Blue game, must be Alakazam - var moves = pk1.Moves; - if (moves.Contains(134)) // Kinesis, yellow only move - return true; - if (pk1.CurrentLevel < 20 && moves.Contains(50)) // Obtaining Disable below level 20 implies a yellow only move - return true; - - return false; - } - internal const GameVersion NONE = GameVersion.Invalid; internal static readonly LearnVersion LearnNONE = new LearnVersion(-1); diff --git a/PKHeX.Core/Legality/Encounters/Verifiers/VerifyCurrentMoves.cs b/PKHeX.Core/Legality/Encounters/Verifiers/VerifyCurrentMoves.cs index 18beabe77..5078bd2f9 100644 --- a/PKHeX.Core/Legality/Encounters/Verifiers/VerifyCurrentMoves.cs +++ b/PKHeX.Core/Legality/Encounters/Verifiers/VerifyCurrentMoves.cs @@ -170,7 +170,7 @@ private static CheckMoveResult[] ParseMovesGenGB(PKM pkm, int[] Moves, LegalInfo return ParseMovesSpecialMoveset(pkm, Moves, info); var InitialMoves = Array.Empty(); int[] SpecialMoves = GetSpecialMoves(info.EncounterMatch); - var games = info.EncounterMatch is IGeneration g && g.Generation == 1 ? Legal.GetGen1Versions(info) : Legal.GetGen2Versions(info); + var games = info.EncounterMatch is IGeneration g && g.Generation == 1 ? GBRestrictions.GetGen1Versions(info) : GBRestrictions.GetGen2Versions(info); foreach (var ver in games) { var VerInitialMoves = MoveLevelUp.GetEncounterMoves(G1Encounter.Species, 0, G1Encounter.LevelMin, ver); @@ -235,7 +235,7 @@ private static CheckMoveResult[] ParseMoves(PKM pkm, MoveParseSource source, Leg { CheckMoveResult[] res = new CheckMoveResult[4]; bool AllParsed() => res.All(r => r != null); - var required = Legal.GetRequiredMoveCount(pkm, source.CurrentMoves, info, source.Base); + var required = pkm.Format != 1 ? 1 : GBRestrictions.GetRequiredMoveCount(pkm, source.CurrentMoves, info, source.Base); // Check empty moves and relearn moves before generation specific moves for (int m = 0; m < 4; m++) @@ -518,7 +518,6 @@ private static IList GetIncompatibleRBYMoves(PKM pkm, int[] moves) // Vaporeon in Red/Blue learns Acid Armor at level 42 and level 47 in Yellow case 134 when pkm.CurrentLevel < 47 && moves.Contains(151): { - var incompatible = new List(3); if (moves.Contains(54)) incompatible.Add(54); @@ -540,69 +539,25 @@ private static IList GetIncompatibleRBYMoves(PKM pkm, int[] moves) private static void ParseEvolutionsIncompatibleMoves(PKM pkm, IList res, int[] moves, List tmhm) { - GetIncompatibleEvolutionMoves(pkm, moves, tmhm, - out var previousspecies, out var incompatible_previous, out var incompatible_current); + GBRestrictions.GetIncompatibleEvolutionMoves(pkm, moves, tmhm, + out var prevSpeciesID, + out var incompatPrev, + out var incompatCurr); - var currentspecies = SpeciesStrings[pkm.Species]; + if (prevSpeciesID == 0) + return; + + var prev = SpeciesStrings[prevSpeciesID]; + var curr = SpeciesStrings[pkm.Species]; for (int m = 0; m < 4; m++) { - if (incompatible_current.Contains(moves[m])) - res[m] = new CheckMoveResult(res[m], Severity.Invalid, string.Format(LMoveEvoFLower, currentspecies, previousspecies), CheckIdentifier.Move); - if (incompatible_previous.Contains(moves[m])) - res[m] = new CheckMoveResult(res[m], Severity.Invalid, string.Format(LMoveEvoFHigher, currentspecies, previousspecies), CheckIdentifier.Move); + if (incompatCurr.Contains(moves[m])) + res[m] = new CheckMoveResult(res[m], Severity.Invalid, string.Format(LMoveEvoFLower, curr, prev), CheckIdentifier.Move); + if (incompatPrev.Contains(moves[m])) + res[m] = new CheckMoveResult(res[m], Severity.Invalid, string.Format(LMoveEvoFHigher, curr, prev), CheckIdentifier.Move); } } - private static void GetIncompatibleEvolutionMoves(PKM pkm, int[] moves, List tmhm, out string previousspecies, out IList incompatible_previous, out IList incompatible_current) - { - switch (pkm.Species) - { - case 34 when moves.Contains(31) && moves.Contains(37): - // Nidoking learns Thrash at level 23 - // Nidorino learns Fury Attack at level 36, Nidoran♂ at level 30 - // Other moves are either learned by Nidoran♂ up to level 23 or by TM - incompatible_current = new[] {31}; - incompatible_previous = new[] {37}; - previousspecies = SpeciesStrings[33]; - return; - - case 103 when moves.Contains(23) && moves.Any(m => Legal.G1Exeggcute_IncompatibleMoves.Contains(moves[m])): - // Exeggutor learns stomp at level 28 - // Exeggcute learns Stun Spore at 32, PoisonPowder at 37 and Sleep Powder at 48 - incompatible_current = new[] {23}; - incompatible_previous = Legal.G1Exeggcute_IncompatibleMoves; - previousspecies = SpeciesStrings[103]; - return; - - case 134: - case 135: - case 136: - incompatible_previous = new List(); - incompatible_current = new List(); - previousspecies = SpeciesStrings[133]; - var ExclusiveMoves = Legal.GetExclusiveMovesG1(133, pkm.Species, tmhm, moves); - var EeveeLevels = Legal.GetMinLevelLearnMoveG1(133, ExclusiveMoves[0]); - var EvoLevels = Legal.GetMaxLevelLearnMoveG1(pkm.Species, ExclusiveMoves[1]); - - for (int i = 0; i < ExclusiveMoves[0].Count; i++) - { - // There is a evolution move with a lower level that current eevee move - if (EvoLevels.Any(ev => ev < EeveeLevels[i])) - incompatible_previous.Add(ExclusiveMoves[0][i]); - } - for (int i = 0; i < ExclusiveMoves[1].Count; i++) - { - // There is a eevee move with a greather level that current evolution move - if (EeveeLevels.Any(ev => ev > EvoLevels[i])) - incompatible_current.Add(ExclusiveMoves[1][i]); - } - return; - } - incompatible_previous = Array.Empty(); - incompatible_current = Array.Empty(); - previousspecies = string.Empty; - } - private static void ParseShedinjaEvolveMoves(PKM pkm, IList res, int[] moves) { var ShedinjaEvoMovesLearned = new List(); diff --git a/PKHeX.Core/Legality/GBRestrictions.cs b/PKHeX.Core/Legality/GBRestrictions.cs new file mode 100644 index 000000000..e0e28486e --- /dev/null +++ b/PKHeX.Core/Legality/GBRestrictions.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PKHeX.Core; + +using static PKHeX.Core.Legal; + +namespace PKHeX +{ + internal static class GBRestrictions + { + private static int[] GetMinLevelLearnMoveG1(int species, List moves) + { + var r = new int[moves.Count]; + for (int i = 0; i < r.Length; i++) + r[i] = MoveLevelUp.GetIsLevelUp1(species, moves[i], 100, 0, 0).Level; + return r; + } + + private static int[] GetMaxLevelLearnMoveG1(int species, List moves) + { + var r = new int[moves.Count]; + + int index = PersonalTable.RB.GetFormeIndex(species, 0); + if (index == 0) + return r; + + var pi_rb = ((PersonalInfoG1)PersonalTable.RB[index]).Moves; + var pi_y = ((PersonalInfoG1)PersonalTable.Y[index]).Moves; + + for (int m = 0; m < moves.Count; m++) + { + bool start = pi_rb.Contains(moves[m]) && pi_y.Contains(moves[m]); + r[m] = start ? 1 : Math.Max(GetHighest(LevelUpRB), GetHighest(LevelUpY)); + int GetHighest(IReadOnlyList learn) => learn[index].GetLevelLearnMove(moves[m]); + } + return r; + } + + private static List[] GetExclusiveMovesG1(int species1, int species2, IEnumerable tmhm, IEnumerable moves) + { + // Return from two species the exclusive moves that only one could learn and also the current pokemon have it in its current moveset + var moves1 = MoveLevelUp.GetMovesLevelUp1(species1, 0, 1, 100); + var moves2 = MoveLevelUp.GetMovesLevelUp1(species2, 0, 1, 100); + + // Remove common moves and remove tmhm, remove not learned moves + var common = new HashSet(moves1.Intersect(moves2).Concat(tmhm)); + var hashMoves = new HashSet(moves); + moves1.RemoveAll(x => !hashMoves.Contains(x) || common.Contains(x)); + moves2.RemoveAll(x => !hashMoves.Contains(x) || common.Contains(x)); + return new[] { moves1, moves2 }; + } + + internal static void GetIncompatibleEvolutionMoves(PKM pkm, int[] moves, List tmhm, out int previousspecies, out IList incompatible_previous, out IList incompatible_current) + { + switch (pkm.Species) + { + case 34 when moves.Contains(31) && moves.Contains(37): + // Nidoking learns Thrash at level 23 + // Nidorino learns Fury Attack at level 36, Nidoran♂ at level 30 + // Other moves are either learned by Nidoran♂ up to level 23 or by TM + incompatible_current = new[] { 31 }; + incompatible_previous = new[] { 37 }; + previousspecies = 33; + return; + + case 103 when moves.Contains(23) && moves.Any(m => G1Exeggcute_IncompatibleMoves.Contains(moves[m])): + // Exeggutor learns stomp at level 28 + // Exeggcute learns Stun Spore at 32, PoisonPowder at 37 and Sleep Powder at 48 + incompatible_current = new[] { 23 }; + incompatible_previous = G1Exeggcute_IncompatibleMoves; + previousspecies = 103; + return; + + case 134: + case 135: + case 136: + incompatible_previous = new List(); + incompatible_current = new List(); + previousspecies = 133; + var ExclusiveMoves = GetExclusiveMovesG1(133, pkm.Species, tmhm, moves); + var EeveeLevels = GetMinLevelLearnMoveG1(133, ExclusiveMoves[0]); + var EvoLevels = GetMaxLevelLearnMoveG1(pkm.Species, ExclusiveMoves[1]); + + for (int i = 0; i < ExclusiveMoves[0].Count; i++) + { + // There is a evolution move with a lower level that current eevee move + if (EvoLevels.Any(ev => ev < EeveeLevels[i])) + incompatible_previous.Add(ExclusiveMoves[0][i]); + } + for (int i = 0; i < ExclusiveMoves[1].Count; i++) + { + // There is a eevee move with a greather level that current evolution move + if (EeveeLevels.Any(ev => ev > EvoLevels[i])) + incompatible_current.Add(ExclusiveMoves[1][i]); + } + return; + } + incompatible_previous = Array.Empty(); + incompatible_current = Array.Empty(); + previousspecies = 0; + } + + internal static int GetRequiredMoveCount(PKM pk, int[] moves, LegalInfo info, int[] initialmoves) + { + if (!pk.Gen1_NotTradeback) // No Move Deleter in Gen 1 + return 1; // Move Deleter exits, slots from 2 onwards can always be empty + + int required = GetRequiredMoveCount(pk, moves, info.EncounterMoves.LevelUpMoves, initialmoves); + if (required >= 4) + return 4; + + // tm, hm and tutor moves replace a free slots if the pokemon have less than 4 moves + // Ignore tm, hm and tutor moves already in the learnset table + var learn = info.EncounterMoves.LevelUpMoves; + var tmhm = info.EncounterMoves.TMHMMoves; + var tutor = info.EncounterMoves.TutorMoves; + var union = initialmoves.Union(learn[1]); + required += moves.Count(m => m != 0 && union.All(t => t != m) && (tmhm[1].Any(t => t == m) || tutor[1].Any(t => t == m))); + + return Math.Min(4, required); + } + + private static int GetRequiredMoveCount(PKM pk, int[] moves, List[] learn, int[] initialmoves) + { + if (SpecialMinMoveSlots.Contains(pk.Species)) + return GetRequiredMoveCountSpecial(pk, moves, learn); + + // A pokemon is captured with initial moves and can't forget any until have all 4 slots used + // If it has learn a move before having 4 it will be in one of the free slots + int required = GetRequiredMoveSlotsRegular(pk, moves, learn, initialmoves); + return required != 0 ? required : GetRequiredMoveCountDecrement(pk, moves, learn, initialmoves); + } + + private static int GetRequiredMoveSlotsRegular(PKM pk, int[] moves, List[] learn, int[] initialmoves) + { + int species = pk.Species; + int catch_rate = ((PK1)pk).Catch_Rate; + // Caterpie and Metapod evolution lines have different count of possible slots available if captured in different evolutionary phases + // Example: a level 7 caterpie evolved into metapod will have 3 learned moves, a captured metapod will have only 1 move + if ((species == 011 || species == 012) && catch_rate == 120) + { + // Captured as Metapod without Caterpie moves + return initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && !G1CaterpieMoves.Contains(lm)); + // There is no valid Butterfree encounter in generation 1 games + } + if ((species == 014 || species == 015) && (catch_rate == 45 || catch_rate == 120)) + { + if (species == 15 && catch_rate == 45) // Captured as Beedril without Weedle and Kakuna moves + return initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && !G1KakunaMoves.Contains(lm)); + + // Captured as Kakuna without Weedle moves + return initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && !G1WeedleMoves.Contains(lm)); + } + + return IsMoveCountRequired3(species, pk.CurrentLevel, moves) ? 3 : 0; // no match + } + + private static bool IsMoveCountRequired3(int species, int level, int[] moves) + { + // Species that evolve and learn the 4th move as evolved species at a greather level than base species + // The 4th move is included in the level up table set as a preevolution move, + // it should be removed from the used slots count if is not the learn move + switch (species) + { + case 017: return level < 21 && !moves.Contains(018); // Pidgeotto without Whirlwind + case 028: return level < 27 && !moves.Contains(040); // Sandslash without Poison Sting + case 047: return level < 30 && !moves.Contains(147); // Parasect without Spore + case 055: return level < 39 && !moves.Contains(093); // Golduck without Confusion + case 087: return level < 44 && !moves.Contains(156); // Dewgong without Rest + case 093: + case 094: return level < 29 && !moves.Contains(095); // Haunter/Gengar without Hypnosis + case 110: return level < 39 && !moves.Contains(108); // Weezing without Smoke Screen + } + return false; + } + + private static int GetRequiredMoveCountDecrement(PKM pk, int[] moves, List[] learn, int[] initialmoves) + { + int usedslots = initialmoves.Union(learn[1]).Where(m => m != 0).Distinct().Count(); + switch (pk.Species) + { + case 031: // Venonat; ignore Venomoth (by the time Venonat evolves it will always have 4 moves) + if (pk.CurrentLevel >= 11 && !moves.Contains(48)) // Supersonic + usedslots--; + if (pk.CurrentLevel >= 19 && !moves.Contains(93)) // Confusion + usedslots--; + break; + case 064: + case 065: // Abra & Kadabra + int catch_rate = ((PK1)pk).Catch_Rate; + if (catch_rate != 100)// Initial Yellow Kadabra Kinesis (move 134) + usedslots--; + if (catch_rate == 200 && pk.CurrentLevel < 20) // Kadabra Disable, not learned until 20 if captured as Abra (move 50) + usedslots--; + break; + case 104: + case 105: // Cubone & Marowak + if (!moves.Contains(39)) // Initial Yellow Tail Whip + usedslots--; + if (!moves.Contains(125)) // Initial Yellow Bone Club + usedslots--; + if (pk.Species == 105 && pk.CurrentLevel < 33 && !moves.Contains(116)) // Marowak evolved without Focus Energy + usedslots--; + break; + case 113: + if (!moves.Contains(39)) // Yellow Initial Tail Whip + usedslots--; + if (!moves.Contains(3)) // Yellow Lvl 12 and Initial Red/Blue Double Slap + usedslots--; + break; + case 056 when pk.CurrentLevel >= 9 && !moves.Contains(67): // Mankey (Low Kick) + case 127 when pk.CurrentLevel >= 21 && !moves.Contains(20): // Pinsir (Bind) + case 130 when pk.CurrentLevel < 32: // Gyarados + usedslots--; + break; + } + return usedslots; + } + + private static int GetRequiredMoveCountSpecial(PKM pk, int[] moves, List[] learn) + { + // Species with few mandatory slots, species with stone evolutions that could evolve at lower level and do not learn any more moves + // and Pikachu and Nidoran family, those only have mandatory the initial moves and a few have one level up moves, + // every other move could be avoided switching game or evolving + var mandatory = GetRequiredMoveCountLevel(pk); + switch (pk.Species) + { + case 103 when pk.CurrentLevel >= 28: // Exeggutor + // At level 28 learn different move if is a Exeggute or Exeggutor + if (moves.Contains(73)) + mandatory.Add(73); // Leech Seed level 28 Exeggute + if (moves.Contains(23)) + mandatory.Add(23); // Stomp level 28 Exeggutor + break; + case 25 when pk.CurrentLevel >= 33: + mandatory.Add(97); // Pikachu always learns Agility + break; + case 114: + mandatory.Add(132); // Tangela always has Constrict as Initial Move + break; + } + + // Add to used slots the non-mandatory moves from the learnset table that the pokemon have learned + return mandatory.Count + moves.Count(m => m != 0 && mandatory.All(l => l != m) && learn[1].Any(t => t == m)); + } + + private static List GetRequiredMoveCountLevel(PKM pk) + { + int species = pk.Species; + int basespecies = GetBaseSpecies(pk); + int maxlevel = 1; + int minlevel = 1; + + if (species == 114) // Tangela moves before level 32 are different in RB vs Y + { + minlevel = 32; + maxlevel = pk.CurrentLevel; + } + else if (029 <= species && species <= 034 && pk.CurrentLevel >= 8) + { + maxlevel = 8; // Always learns a third move at level 8 + } + + if (minlevel > pk.CurrentLevel) + return new List(); + + return MoveLevelUp.GetMovesLevelUp1(basespecies, 0, maxlevel, minlevel); + } + + internal static IEnumerable GetGen2Versions(LegalInfo Info) + { + if (ParseSettings.AllowGen2Crystal(Info.Korean) && Info.Game == GameVersion.C) + yield return GameVersion.C; + + // Any encounter marked with version GSC is for pokemon with the same moves in GS and C + // it is sufficient to check just GS's case + yield return GameVersion.GS; + } + + internal static IEnumerable GetGen1Versions(LegalInfo Info) + { + if (Info.EncounterMatch.Species == 133 && Info.Game == GameVersion.Stadium) + { + // Stadium Eevee; check for RB and yellow initial moves + yield return GameVersion.RB; + yield return GameVersion.YW; + yield break; + } + if (Info.Game == GameVersion.YW) + { + yield return GameVersion.YW; + yield break; + } + + // Any encounter marked with version RBY is for pokemon with the same moves and catch rate in RB and Y, + // it is sufficient to check just RB's case + yield return GameVersion.RB; + } + + private static bool GetCatchRateMatchesPreEvolution(PKM pkm, int catch_rate, IEnumerable gen1) + { + // For species catch rate, discard any species that has no valid encounters and a different catch rate than their pre-evolutions + var Lineage = gen1.Except(Species_NotAvailable_CatchRate); + return IsCatchRateRBY(Lineage) || IsCatchRateTrade() || IsCatchRateStadium(); + + // Dragonite's Catch Rate is different than Dragonair's in Yellow, but there is no Dragonite encounter. + bool IsCatchRateRBY(IEnumerable ds) => ds.Any(s => catch_rate == PersonalTable.RB[s].CatchRate || (s != 149 && catch_rate == PersonalTable.Y[s].CatchRate)); + // Krabby encounter trade special catch rate + bool IsCatchRateTrade() => (pkm.Species == 098 || pkm.Species == 099) && catch_rate == 204; + bool IsCatchRateStadium() => Stadium_GiftSpecies.Contains(pkm.Species) && Stadium_CatchRate.Contains(catch_rate); + } + + /// + /// Gets the Tradeback status depending on various values. + /// + /// Pokémon to guess the tradeback status from. + internal static TradebackType GetTradebackStatusInitial(PKM pkm) + { + if (pkm is PK1 pk1) + return GetTradebackStatusRBY(pk1); + + if (pkm.Format == 2 || pkm.VC2) // Check for impossible tradeback scenarios + return !pkm.CanInhabitGen1() ? TradebackType.Gen2_NotTradeback : TradebackType.Any; + + // VC2 is released, we can assume it will be TradebackType.Any. + // Is impossible to differentiate a VC1 pokemon traded to Gen7 after VC2 is available. + // Met Date cannot be used definitively as the player can change their system clock. + return TradebackType.Any; + } + + /// + /// Gets the Tradeback status depending on the + /// + /// Pokémon to guess the tradeback status from. + private static TradebackType GetTradebackStatusRBY(PK1 pkm) + { + if (!ParseSettings.AllowGen1Tradeback) + return TradebackType.Gen1_NotTradeback; + + // Detect tradeback status by comparing the catch rate(Gen1)/held item(Gen2) to the species in the pkm's evolution chain. + var catch_rate = pkm.Catch_Rate; + if (catch_rate == 0) + return TradebackType.WasTradeback; + + var table = EvolutionTree.GetEvolutionTree(1); + var lineage = table.GetValidPreEvolutions(pkm, maxLevel: pkm.CurrentLevel); + var gen1 = lineage.Select(evolution => evolution.Species); + bool matchAny = GetCatchRateMatchesPreEvolution(pkm, catch_rate, gen1); + + if (!matchAny) + return TradebackType.WasTradeback; + + if (HeldItems_GSC.Contains((ushort)catch_rate)) + return TradebackType.Any; + + return TradebackType.Gen1_NotTradeback; + } + + internal static bool IsTradedKadabraG1(PKM pkm) + { + if (!(pkm is PK1 pk1) || pk1.Species != 64) + return false; + if (pk1.TradebackStatus == TradebackType.WasTradeback) + return true; + if (ParseSettings.ActiveTrainer.Game == (int)GameVersion.Any) + return false; + var IsYellow = ParseSettings.ActiveTrainer.Game == (int)GameVersion.YW; + if (pk1.TradebackStatus == TradebackType.Gen1_NotTradeback) + { + // If catch rate is Abra catch rate it wont trigger as invalid trade without evolution, it could be traded as Abra + // Yellow Kadabra catch rate in Red/Blue game, must be Alakazam + var table = IsYellow ? PersonalTable.RB : PersonalTable.Y; + if (pk1.Catch_Rate == table[64].CatchRate) + return true; + } + if (IsYellow) + return false; + // Yellow only moves in Red/Blue game, must be Alakazam + var moves = pk1.Moves; + if (moves.Contains(134)) // Kinesis, yellow only move + return true; + if (pk1.CurrentLevel < 20 && moves.Contains(50)) // Obtaining Disable below level 20 implies a yellow only move + return true; + + return false; + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs index 495cb3259..51b48f8ae 100644 --- a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs @@ -88,7 +88,7 @@ public void VerifyG1(LegalityAnalysis data) private void VerifyG1TradeEvo(LegalityAnalysis data) { var pkm = data.pkm; - var mustevolve = pkm.TradebackStatus == TradebackType.WasTradeback || (pkm.Format == 1 && !ParseSettings.IsFromActiveTrainer(pkm)) || Legal.IsTradedKadabraG1(pkm); + var mustevolve = pkm.TradebackStatus == TradebackType.WasTradeback || (pkm.Format == 1 && !ParseSettings.IsFromActiveTrainer(pkm)) || GBRestrictions.IsTradedKadabraG1(pkm); if (!mustevolve) return; // Pokemon have been traded but it is not evolved, trade evos are sequential dex numbers