diff --git a/PKHeX.Core/Legality/Localization/LegalityCheckLocalization.cs b/PKHeX.Core/Legality/Localization/LegalityCheckLocalization.cs index 03374be9a..683b95dab 100644 --- a/PKHeX.Core/Legality/Localization/LegalityCheckLocalization.cs +++ b/PKHeX.Core/Legality/Localization/LegalityCheckLocalization.cs @@ -94,6 +94,7 @@ public sealed class LegalityCheckLocalization public string EggLocationTrade { get; init; } = "Able to hatch a traded Egg at Met Location."; public string EggLocationTradeFail { get; init; } = "Invalid Egg Location, shouldn't be 'traded' while an Egg."; public string EggMetLocationFail { get; init; } = "Can't obtain Egg from Egg Location."; + public string EggMoveCombination { get; init; } = "These Egg Moves cannot be inherited in combination in this generation."; public string EggNature { get; init; } = "Eggs cannot have their Stat Alignment changed."; public string EggPP { get; init; } = "Eggs cannot have modified move PP counts."; public string EggPPUp { get; init; } = "Cannot apply PP Ups to an Egg."; diff --git a/PKHeX.Core/Legality/Localization/LegalityCheckResultCodeExtensions.cs b/PKHeX.Core/Legality/Localization/LegalityCheckResultCodeExtensions.cs index b68e867d5..43c1c0b4f 100644 --- a/PKHeX.Core/Legality/Localization/LegalityCheckResultCodeExtensions.cs +++ b/PKHeX.Core/Legality/Localization/LegalityCheckResultCodeExtensions.cs @@ -83,6 +83,7 @@ public static class LegalityCheckResultCodeExtensions EggLocationTrade => localization.EggLocationTrade, EggLocationTradeFail => localization.EggLocationTradeFail, EggMetLocationFail => localization.EggMetLocationFail, + EggMoveCombination => localization.EggMoveCombination, EggNature => localization.EggNature, EggPP => localization.EggPP, EggPPUp => localization.EggPPUp, diff --git a/PKHeX.Core/Legality/Moves/Breeding/ChainBreedLegality.cs b/PKHeX.Core/Legality/Moves/Breeding/ChainBreedLegality.cs new file mode 100644 index 000000000..dc97728cd --- /dev/null +++ b/PKHeX.Core/Legality/Moves/Breeding/ChainBreedLegality.cs @@ -0,0 +1,994 @@ +using System; +using System.Text; +using static PKHeX.Core.GameVersion; + +namespace PKHeX.Core; + +/// +/// Verifies if a Generation 2-5 egg move set can be produced by a single compatible father chain. +/// +public static class ChainBreedLegality +{ + private const byte FlagBase = 1 << 0; + private const byte FlagLevelUp = 1 << 1; + private const byte FlagGeneral = 1 << 2; + private const int MaxMoveCount = 4; + // The longest known in-generation chain is five fathers. Leave room for + // an evolution/baby-species transition without allowing unbounded search. + private const int MaxChainDepth = 8; + + public static bool IsValid(ushort species, GameVersion version, params ReadOnlySpan moves) + => TryValidate(species, version, moves, out _); + + public static bool TryValidate(ushort species, GameVersion version, ReadOnlySpan moves, out ChainBreedSummary summary) + { + summary = default; + int count = moves.IndexOf((ushort)0); + if (count == 0) + return false; + if (count == -1) + count = moves.Length; + if (count > MaxMoveCount) + return false; + + var generation = version.Generation; + if (generation is < 2 or > 5) + return IsValidRelaxed(species, version, moves, out summary); + + var learn = GameData.GetLearnSource(version); + var learnset = learn.GetLearnset(species, 0); + var baseMoves = learnset.GetBaseEggMoves(GetEggLevel(generation)); + + Span flags = stackalloc byte[count]; + if (!MarkChildMoveFlags(species, version, moves[..count], learnset, flags)) + return false; + + // For male-only split breed species (Volbeat/Nidoran-M) in Gen 2-5, we need special validation: + // All EGG moves must come from a single father. + // Level-up moves don't need this restriction (can breed with Ditto). + var isMaleSplit = IsMaleOnlySplitBreed(species); + if (isMaleSplit) + { + // Filter to only egg moves (not level-up moves) + Span eggMovesOnly = stackalloc ushort[MaxMoveCount]; + int eggMoveCount = 0; + for (int i = 0; i < count; i++) + { + // Skip if this move can be obtained via level-up + if ((flags[i] & FlagLevelUp) != 0) + continue; + eggMovesOnly[eggMoveCount++] = moves[i]; + } + + // If there are egg moves, check if a single father can pass them all + if (eggMoveCount > 0) + { + RemapSpeciesToMother(ref species); + return CanSingleFatherPassAllMoves(species, version, eggMovesOnly[..eggMoveCount], out summary); + } + } + + Span visited = stackalloc ChainQueryState[MaxChainDepth]; + return TryValidateCore(species, version, moves[..count], baseMoves, flags, visited, 0, out summary); + } + + private static void RemapSpeciesToMother(ref ushort species) + { + if (species is (ushort)Species.NidoranM) + species = (ushort)Species.NidoranF; + if (species is (ushort)Species.Volbeat) + species = (ushort)Species.Illumise; + } + + private static bool IsMaleOnlySplitBreed(ushort species) + { + return species is (ushort)Species.NidoranM or (ushort)Species.Volbeat; + } + + private static bool IsValidRelaxed(ushort species, GameVersion version, ReadOnlySpan moves, out ChainBreedSummary summary) + { + // Gen 6+ games have relaxed breeding rules where most chains are valid as Mothers can now pass, allowing for fusing chains. + // However, only-Female offspring still have some restrictions: + // A Smoochum(no egg group; bred from Jynx, in the Human-like group) can't inherit Powder Snow (learned at level 4) + // Jynx is Female only, and there are no other species of the Human-like group that can know Powder Snow. + // So, it must breed with a male from the same egg group; that male must know the moves needed. + + // However, Confusion is valid because Alakazam (Human-like) can learn it and breed with Jynx. + + // Additionally, for male-only split breed species (Volbeat/Nidoran-M), in Gen 6-7: + // - Mother (Illumise/Nidoran-F) can pass moves she learns as egg moves + // - Father must pass moves the mother cannot learn + // - In Gen 8+, egg move sharing means this restriction doesn't apply + summary = default; + + var generation = version.Generation; + var table = GameData.GetPersonal(version); + if (!table.IsPresentInGame(species, 0)) + return true; + + var pi = table[species, 0]; + + // Genderless species must breed with Ditto, but since they always must breed with Ditto, + // they are already handled by the relaxed rules (no level-up moves can be inherited). + if (pi.Genderless) + return true; + + var isMaleSplit = IsMaleOnlySplitBreed(species); + RemapSpeciesToMother(ref species); + + // For male-only split breed species in Gen 6-7, check if father can pass moves mother can't learn + if (isMaleSplit && generation < 8) + { + return CanMotherAndFatherPassAllMoves(species, version, moves, out summary); + } + + // Check if this is a baby Pokemon bred from a female-only species (Gen 8+ restriction) + if (generation < 8) + return true; // Gen 6-7: fully relaxed for non-male-split species + + var context = version.Context; + var tree = EvolutionTree.GetEvolutionTree(context); + var evolutions = tree.Forward.GetEvolutions(species, 0); + + foreach (var (evoSpecies, _) in evolutions) + { + if (!table.IsPresentInGame(evoSpecies, 0)) + continue; + + var evoPi = table[evoSpecies, 0]; + + // If the evolved form is female-only, it must breed with a compatible father + if (evoPi.OnlyFemale) + { + // Check if any of the moves are level-up moves that cannot be inherited + var learn = GameData.GetLearnSource(version); + + // Check each move to see if it's a level-up move that has no compatible father + foreach (var move in moves) + { + if (move == 0) + break; + + // Check if this is a level-up move for the baby species or evolved species + var babyLearnset = learn.GetLearnset(species, 0); + var evoLearnset = learn.GetLearnset(evoSpecies, 0); + + bool isLevelUpMove = babyLearnset.TryGetLevelLearnMove(move, out _) || + evoLearnset.TryGetLevelLearnMove(move, out _); + + if (!isLevelUpMove) + continue; // Not a level-up move, so it can be inherited normally + + // This is a level-up move. Check if any compatible father can learn it. + if (!CanAnyCompatibleFatherLearnMove(evoPi, move, table, learn)) + return false; // No compatible father can pass this level-up move + } + } + } + + return true; + } + + private static bool CanAnyCompatibleFatherLearnMove(IPersonalInfo motherInfo, ushort move, IPersonalTable table, ILearnSource learn) + { + // Check if any species in the mother's egg groups can learn this move + var maxSpecies = table.MaxSpeciesID; + for (ushort fatherSpecies = 1; fatherSpecies <= maxSpecies; fatherSpecies++) + { + if (!table.IsPresentInGame(fatherSpecies, 0)) + continue; + + var fatherInfo = table[fatherSpecies, 0]; + + // Father must be in the same egg group and not be Ditto (or genderless/female-only) + if (!IsCompatibleFatherForMove(motherInfo, fatherSpecies, fatherInfo)) + continue; + + // Check if this father can learn the move via level-up + var fatherLearnset = learn.GetLearnset(fatherSpecies, 0); + if (fatherLearnset.TryGetLevelLearnMove(move, out _)) + return true; // Found a compatible father that can learn this move + } + + return false; // No compatible father found + } + + private static bool IsCompatibleFatherForMove(IPersonalInfo mother, ushort fatherSpecies, IPersonalInfo father) + { + // Ditto can't pass down level-up moves + if (fatherSpecies == (ushort)Species.Ditto) + return false; + + // Father can't be genderless or female-only + if (father.Genderless || father.OnlyFemale) + return false; + + // Father must share an egg group with the mother + if (!SharesEggGroup(mother.EggGroup1, mother.EggGroup2, father.EggGroup1)) + return false; + if (!SharesEggGroup(mother.EggGroup1, mother.EggGroup2, father.EggGroup2)) + return false; + + return true; + } + + private static bool CanSingleFatherPassAllMoves(ushort motherSpecies, GameVersion version, ReadOnlySpan moves, out ChainBreedSummary summary) + { + // For male-only split breed species in Gen 2-5, all egg moves must come from a single father + summary = default; + var table = GameData.GetPersonal(version); + if (!table.IsPresentInGame(motherSpecies, 0)) + return false; + + var motherInfo = table[motherSpecies, 0]; + var learn = GameData.GetLearnSource(version); + var maxSpecies = table.MaxSpeciesID; + + // Try each potential father species + for (ushort fatherSpecies = 1; fatherSpecies <= maxSpecies; fatherSpecies++) + { + if (!table.IsPresentInGame(fatherSpecies, 0)) + continue; + + var fatherInfo = table[fatherSpecies, 0]; + + // Father must be in the same egg group and not be Ditto (or genderless/female-only) + if (!IsCompatibleFatherForBreeding(motherInfo, fatherSpecies, fatherInfo)) + continue; + + // Check if this father can learn ALL the moves + bool canLearnAll = true; + + foreach (var move in moves) + { + if (move == 0) + break; + + // Father must be able to learn this move as an egg move or level-up move + if (!CanFatherLearnMoveForEgg(fatherSpecies, move, learn)) + { + canLearnAll = false; + break; + } + } + + if (canLearnAll) + { + summary = new ChainBreedSummary(motherSpecies, fatherSpecies, 1); + return true; + } + } + + return false; // No single father can pass all moves + } + + private static bool CanMotherAndFatherPassAllMoves(ushort motherSpecies, GameVersion version, ReadOnlySpan moves, out ChainBreedSummary summary) + { + // For male-only split breed species in Gen 6-7: + // - Mother can pass moves she learns as egg moves + // - Father must pass moves the mother cannot learn + summary = default; + var table = GameData.GetPersonal(version); + if (!table.IsPresentInGame(motherSpecies, 0)) + return false; + + var motherInfo = table[motherSpecies, 0]; + var learn = GameData.GetLearnSource(version); + + // Determine which moves the mother can learn as egg moves + Span motherCanLearn = stackalloc bool[MaxMoveCount]; + Span fatherMustPass = stackalloc ushort[MaxMoveCount]; + int fatherMoveCount = 0; + + for (int i = 0; i < moves.Length; i++) + { + var move = moves[i]; + if (move == 0) + break; + + // Check if mother can learn this move as an egg move + if (CanMotherLearnMoveAsEgg(motherSpecies, move, learn)) + { + motherCanLearn[i] = true; + } + else + { + // Father must pass this move + fatherMustPass[fatherMoveCount++] = move; + } + } + + // If mother can learn all moves, it's valid + if (fatherMoveCount == 0) + { + summary = new ChainBreedSummary(motherSpecies, 0, 0); + return true; + } + + // Check if a single father can pass all the moves the mother cannot learn + var maxSpecies = table.MaxSpeciesID; + for (ushort fatherSpecies = 1; fatherSpecies <= maxSpecies; fatherSpecies++) + { + if (!table.IsPresentInGame(fatherSpecies, 0)) + continue; + + var fatherInfo = table[fatherSpecies, 0]; + + // Father must be in the same egg group + if (!IsCompatibleFatherForBreeding(motherInfo, fatherSpecies, fatherInfo)) + continue; + + // Check if this father can learn all the moves mother cannot learn + bool canLearnAll = true; + for (int i = 0; i < fatherMoveCount; i++) + { + var move = fatherMustPass[i]; + if (CanFatherLearnMoveForEgg(fatherSpecies, move, learn)) + continue; + canLearnAll = false; + break; + } + + if (canLearnAll) + { + summary = new ChainBreedSummary(motherSpecies, fatherSpecies, 1); + return true; + } + } + + return false; // No father can pass all the moves mother cannot learn + } + + private static bool CanFatherLearnMoveForEgg(ushort fatherSpecies, ushort move, ILearnSource learn) + { + // For male-only split breed validation, check if the father can pass this move to the child + // The father can pass a move if he can HAVE it in his moveset via level-up only + // (not via egg moves, since that would require another breeding chain) + var learnset = learn.GetLearnset(fatherSpecies, 0); + + // Only check level-up + return learnset.TryGetLevelLearnMove(move, out _); + } + + private static bool CanMotherLearnMoveAsEgg(ushort motherSpecies, ushort move, ILearnSource learn) + { + // Check if the mother can learn this move as an egg move + var eggMoves = learn.GetEggMoves(motherSpecies, 0); + return eggMoves.Contains(move); + } + + private static bool IsCompatibleFatherForBreeding(IPersonalInfo mother, ushort fatherSpecies, IPersonalInfo father) + { + // Ditto can't pass down egg moves + if (fatherSpecies == (ushort)Species.Ditto) + return false; + + // Father can't be genderless or female-only + if (father.Genderless || father.OnlyFemale) + return false; + + // Father must share at least one egg group with the mother + if (SharesEggGroup(mother.EggGroup1, mother.EggGroup2, father.EggGroup1)) + return true; + if (SharesEggGroup(mother.EggGroup1, mother.EggGroup2, father.EggGroup2)) + return true; + + return false; // No shared egg groups + } + + private static bool TryValidateCore(ushort eggSpecies, GameVersion version, ReadOnlySpan moves, ReadOnlySpan baseMoves, ReadOnlySpan flags, Span visited, int depth, out ChainBreedSummary summary) + { + summary = default; + if ((uint)depth >= (uint)visited.Length) + return false; + + var state = new ChainQueryState(eggSpecies, moves); + // Check against visited[0] through visited[depth-1] for exact duplicates + for (int i = 0; i < depth; i++) + { + if (visited[i].Equals(state)) + return false; + } + // Also check if we're about to overwrite the same state at visited[depth] + // (happens when recursing at the same depth level via CanFatherKnowAllMoves) + if ((uint)depth < (uint)visited.Length && visited[depth].Equals(state)) + return false; + + visited[depth] = state; + + int maxBase = Math.Min(moves.Length, baseMoves.Length); + Span inheritedMoves = stackalloc ushort[MaxMoveCount]; + return TryValidateBaseCounts(eggSpecies, version, moves, baseMoves, flags, inheritedMoves, maxBase, visited, depth, out summary); + } + + private static bool TryValidateBaseCounts(ushort eggSpecies, GameVersion version, ReadOnlySpan moves, ReadOnlySpan baseMoves, ReadOnlySpan flags, Span inheritedMoves, int maxBase, Span visited, int depth, out ChainBreedSummary summary) + { + summary = default; + for (int baseCount = 0; baseCount <= maxBase; baseCount++) + { + if (!IsValidBaseCount(baseCount, moves, baseMoves, flags)) + continue; + + int inheritedCount = moves.Length - baseCount; + if (inheritedCount == 0) + return true; + // Note: Even if eggSpecies is in Undiscovered (like baby Pokemon), it can still have + // inherited moves if its evolved forms can breed (e.g., Tyrogue from Hitmonlee/Hitmonchan/Hitmontop). + // Let TryResolveInheritedSources handle the validation through TryResolveFatherViaEvolution. + + var suffix = moves.Slice(baseCount, inheritedCount); + var suffixFlags = flags.Slice(baseCount, inheritedCount); + for (int i = 0; i < inheritedCount; i++) + inheritedMoves[i] = suffix[i]; + + if (TryResolveInheritedSources(eggSpecies, version, inheritedMoves[..inheritedCount], suffixFlags, 0, visited, depth + 1, out summary)) + return true; + } + + return false; + } + + private static bool TryResolveInheritedSources(ushort eggSpecies, GameVersion version, ReadOnlySpan moves, ReadOnlySpan flags, int index, Span visited, int depth, out ChainBreedSummary summary) + { + if (index == moves.Length) + return TryResolveFather(eggSpecies, version, moves, visited, depth, out summary); + + var flag = flags[index]; + if ((flag & FlagGeneral) != 0) + { + if (TryResolveInheritedSources(eggSpecies, version, moves, flags, index + 1, visited, depth, out summary)) + return true; + } + + if ((flag & FlagLevelUp) != 0) + { + if (TryResolveInheritedSources(eggSpecies, version, moves, flags, index + 1, visited, depth, out summary)) + return true; + } + + summary = default; + return false; + } + + private static bool TryResolveFather(ushort eggSpecies, GameVersion version, ReadOnlySpan moves, Span visited, int depth, out ChainBreedSummary summary) + { + summary = default; + var table = GameData.GetPersonal(version); + if (!table.IsPresentInGame(eggSpecies, 0)) + return false; + + var mother = table[eggSpecies, 0]; + + // If the egg species can't breed (baby Pokemon like Tyrogue), check if its evolutions can act as fathers + if (mother.Genderless || mother.OnlyMale || mother.EggGroup1 == (int)EggGroup.Undiscovered) + { + // Try to find evolved forms that can breed and produce this egg species + return TryResolveFatherViaEvolution(eggSpecies, version, moves, visited, depth, out summary); + } + + ushort maxSpecies = table.MaxSpeciesID; + for (ushort fatherSpecies = 1; fatherSpecies <= maxSpecies; fatherSpecies++) + { + if (!table.IsPresentInGame(fatherSpecies, 0)) + continue; + + var father = table[fatherSpecies, 0]; + if (!IsCompatibleFather(mother, fatherSpecies, father)) + continue; + + if (!CanFatherKnowAllMoves(fatherSpecies, version, moves, visited, depth, out var chainDepth)) + continue; + + summary = new ChainBreedSummary(eggSpecies, fatherSpecies, chainDepth); + return true; + } + + return false; + } + + private static bool TryResolveFatherViaEvolution(ushort eggSpecies, GameVersion version, ReadOnlySpan moves, Span visited, int depth, out ChainBreedSummary summary) + { + summary = default; + var tree = EvolutionTree.GetEvolutionTree(version.Context); + var evos = tree.Forward.GetEvolutions(eggSpecies, 0); + + foreach (var (evoSpecies, _) in evos) + { + if (!CanFatherKnowAllMoves(evoSpecies, version, moves, visited, depth, out var chainDepth)) + continue; + + summary = new ChainBreedSummary(eggSpecies, evoSpecies, chainDepth); + return true; + } + + return false; + } + + private static bool CanFatherKnowAllMoves(ushort fatherSpecies, GameVersion version, ReadOnlySpan moves, Span visited, int depth, out byte chainDepth) + { + chainDepth = 1; + Span pending = stackalloc ushort[MaxMoveCount]; + int pendingCount = 0; + for (int i = 0; i < moves.Length; i++) + { + var move = moves[i]; + if (!CanLearnDirectlyInLine(fatherSpecies, version, move)) + pending[pendingCount++] = move; + } + + if (pendingCount == 0) + return true; + + Span eggSpecies = stackalloc ushort[2]; + int eggSpeciesCount = GetEggSpeciesCandidates(fatherSpecies, version, eggSpecies); + Span flags = stackalloc byte[MaxMoveCount]; + for (int i = 0; i < eggSpeciesCount; i++) + { + var candidate = eggSpecies[i]; + if (candidate == 0) + continue; + + var learn = GameData.GetLearnSource(version); + var learnset = learn.GetLearnset(candidate, 0); + flags.Clear(); + if (!MarkChildMoveFlags(candidate, version, pending[..pendingCount], learnset, flags)) + continue; + + var baseMoves = learnset.GetBaseEggMoves(GetEggLevel(version.Generation)); + if (!TryValidateCore(candidate, version, pending[..pendingCount], baseMoves, flags, visited, depth, out var nested)) + continue; + + chainDepth = (byte)(nested.ChainDepth + 1); + return true; + } + + return false; + } + + private static bool CanLearnDirectlyInLine(ushort species, GameVersion version, ushort move) + { + if (species == (ushort)Species.Smeargle) + return MoveInfo.IsSketchValid(move, version.Context); + + var tree = EvolutionTree.GetEvolutionTree(version.Context); + + // Check backwards through pre-evolutions + ushort current = species; + while (true) + { + if (CanLearnDirectly(current, version, move)) + return true; + + ref readonly var node = ref tree.Reverse.GetReverse(current, 0); + var previous = node.First.Species; + if (previous == 0) + break; + current = previous; + } + + // Check forward through evolutions (e.g., Tyrogue -> Hitmonlee/Hitmonchan/Hitmontop) + var evos = tree.Forward.GetEvolutions(species, 0); + foreach (var (evoSpecies, _) in evos) + { + if (CanLearnDirectly(evoSpecies, version, move)) + return true; + } + + // Check cross-generation and special encounter sources + if (CanLearnFromHistoricalSource(species, version, move)) + return true; + + return false; + } + + private static bool CanLearnFromHistoricalSource(ushort species, GameVersion version, ushort move) + { + var generation = version.Generation; + + // Gen 3: Can use XD/Colo special encounters + if (generation == 3) + { + if (CanLearnFromGen3Special(species, move)) + return true; + } + + // Gen 4: Can transfer from Gen 3 (including XD/Colo), but NOT native Gen4-only moves through XD + if (generation == 4) + { + // Only allow Gen 3 XD/Colo moves (not Gen 4 moves) + if (move <= Legal.MaxMoveID_3 && CanLearnFromGen3Special(species, move)) + return true; + } + + // Gen 5: Can transfer from Gen 3/4 + if (generation == 5) + { + // Gen 4 TMs (e.g., Shellder + Avalanche via Gen4 TM72) + // Check if the move is a Gen4-exclusive move learnable via TM in Gen4 + if (move is (> Legal.MaxMoveID_3 and <= Legal.MaxMoveID_4)) + { + // Check all Gen4 versions for TM availability + if (CanLearnDirectly(species, Pt, move) || + CanLearnDirectly(species, HGSS, move)) + return true; + } + + // Gen 3 special encounters (XD/Colo) + if (move <= Legal.MaxMoveID_3 && CanLearnFromGen3Special(species, move)) + return true; + } + + return false; + } + + private static bool CanLearnFromGen3Special(ushort species, ushort move) + { + // Check XD Shadow Pokemon encounters + foreach (var enc in Encounters3XD.Shadow) + { + if (enc.Species == species) + { + var moves = enc.Moves.AsSpan(); + for (int i = 0; i < moves.Length; i++) + { + if (moves[i] == 0) + break; + if (moves[i] == move) + return true; + } + } + } + + return false; + } + + private static bool CanLearnDirectly(ushort species, GameVersion version, ushort move) => version switch + { + GD or SI or GS => CanLearnDirectly2(LearnSource2GS.Instance, species, move, false), + C or GSC => CanLearnDirectly2(LearnSource2C.Instance, species, move, true), + + R or S or RS => CanLearnDirectly3(LearnSource3RS.Instance, species, move), + E or RSE => CanLearnDirectly3(LearnSource3E.Instance, species, move), + FR or FRLG => CanLearnDirectly3(LearnSource3FR.Instance, species, move), + LG => CanLearnDirectly3(LearnSource3LG.Instance, species, move), + + D or P or DP => CanLearnDirectly4(LearnSource4DP.Instance, species, move, false), + Pt or DPPt => CanLearnDirectly4(LearnSource4Pt.Instance, species, move, false), + HG or SS or HGSS => CanLearnDirectly4(LearnSource4HGSS.Instance, species, move, true), + + B or W or BW => CanLearnDirectly5(LearnSource5BW.Instance, species, move), + B2 or W2 or B2W2 => CanLearnDirectly5(LearnSource5B2W2.Instance, species, move), + _ => false, + }; + + private static bool CanLearnDirectly2(ILearnSource source, ushort species, ushort move, bool crystal) + { + if (!source.TryGetPersonal(species, 0, out var pi)) + return false; + if (source.GetLearnset(species, 0).GetIsLearn(move)) + return true; + + var tmIndex = PersonalInfo2.MachineMoves.IndexOf((byte)move); + if (move <= Legal.MaxMoveID_2 && tmIndex >= 0 && pi.GetIsLearnTM(tmIndex)) + return true; + + var tutorIndex = PersonalInfo2.TutorMoves.IndexOf((byte)move); + return crystal && tutorIndex >= 0 && pi.GetIsLearnTutorType(tutorIndex); + } + + private static bool CanLearnDirectly3(ILearnSource source, ushort species, ushort move) + { + if (!source.TryGetPersonal(species, 0, out var pi)) + return false; + if (source.GetLearnset(species, 0).GetIsLearn(move)) + return true; + + var tmIndex = PersonalInfo3.MachineMovesTechnical.IndexOf(move); + if (tmIndex >= 0 && pi.TMHM[tmIndex]) + return true; + + var hmIndex = PersonalInfo3.MachineMovesHidden.IndexOf(move); + return hmIndex >= 0 && pi.TMHM[50 + hmIndex]; + } + + private static bool CanLearnDirectly4(ILearnSource source, ushort species, ushort move, bool hgss) + { + if (!source.TryGetPersonal(species, 0, out var pi)) + return false; + if (source.GetLearnset(species, 0).GetIsLearn(move)) + return true; + + var tmIndex = PersonalInfo4.MachineMovesTechnical.IndexOf(move); + if (tmIndex >= 0 && pi.GetIsLearnTM(tmIndex)) + return true; + + var hms = hgss ? PersonalInfo4.MachineMovesHiddenHGSS : PersonalInfo4.MachineMovesHiddenDPPt; + var hmIndex = hms.IndexOf(move); + return hmIndex >= 0 && pi.GetIsLearnHM(hmIndex); + } + + private static bool CanLearnDirectly5(ILearnSource source, ushort species, ushort move) where T : PersonalInfo + { + if (!source.TryGetPersonal(species, 0, out var pi)) + return false; + if (source.GetLearnset(species, 0).GetIsLearn(move)) + return true; + if (pi is not IPersonalInfoTM tm) + return false; + + var tmIndex = PersonalInfo5BW.MachineMoves.IndexOf(move); + return tmIndex >= 0 && tm.GetIsLearnTM(tmIndex); + } + + private static bool MarkChildMoveFlags(ushort species, GameVersion version, ReadOnlySpan moves, Learnset learnset, Span flags) => version switch + { + GD or SI or GS => MarkChildMoveFlags2(species, LearnSource2GS.Instance, PersonalTable.GS[species], version, moves, learnset, flags), + C or GSC => MarkChildMoveFlags2(species, LearnSource2C.Instance, PersonalTable.C[species], version, moves, learnset, flags), + + R or S or RS => MarkChildMoveFlags3(species, LearnSource3RS.Instance, PersonalTable.RS[species], moves, learnset, flags), + E or RSE or COLO or XD or CXD or EFL => MarkChildMoveFlags3(species, LearnSource3E.Instance, PersonalTable.E[species], moves, learnset, flags), + FR or FRLG => MarkChildMoveFlags3(species, LearnSource3FR.Instance, PersonalTable.FR[species], moves, learnset, flags), + LG => MarkChildMoveFlags3(species, LearnSource3LG.Instance, PersonalTable.LG[species], moves, learnset, flags), + + D or P or DP => MarkChildMoveFlags4(species, LearnSource4DP.Instance, PersonalTable.DP[species], version, moves, learnset, flags), + Pt or DPPt => MarkChildMoveFlags4(species, LearnSource4Pt.Instance, PersonalTable.Pt[species], version, moves, learnset, flags), + HG or SS or HGSS => MarkChildMoveFlags4(species, LearnSource4HGSS.Instance, PersonalTable.HGSS[species], version, moves, learnset, flags), + + B or W or BW => MarkChildMoveFlags5(species, LearnSource5BW.Instance, PersonalTable.BW[species], moves, learnset, flags), + B2 or W2 or B2W2 => MarkChildMoveFlags5(species, LearnSource5B2W2.Instance, PersonalTable.B2W2[species], moves, learnset, flags), + _ => false, + }; + + private static bool MarkChildMoveFlags2(ushort species, ILearnSource source, PersonalInfo2 info, GameVersion version, ReadOnlySpan moves, Learnset learnset, Span flags) + { + bool inheritLevelUp = Breeding.GetCanInheritMoves(species); + var baseMoves = learnset.GetBaseEggMoves(GetEggLevel(2)); + var eggMoves = source.GetEggMoves(species, 0); + var tmMoves = PersonalInfo2.MachineMoves; + var tutorMoves = PersonalInfo2.TutorMoves; + + for (int i = 0; i < moves.Length; i++) + { + var move = moves[i]; + byte value = 0; + if (baseMoves.Contains(move)) + value |= FlagBase; + if (move <= Legal.MaxMoveID_2) + { + if (eggMoves.Contains(move)) + value |= FlagGeneral; + if (info.GetIsLearnTM(tmMoves.IndexOf((byte)move))) + value |= FlagGeneral; + if (inheritLevelUp && learnset.GetIsLearn(move)) + value |= FlagLevelUp; + if (version is C or GSC && info.GetIsLearnTutorType(tutorMoves.IndexOf((byte)move))) + value |= FlagGeneral; + } + if (value == 0) + return false; + flags[i] = value; + } + return true; + } + + private static bool MarkChildMoveFlags3(ushort species, ILearnSource source, PersonalInfo3 info, ReadOnlySpan moves, Learnset learnset, Span flags) + { + bool inheritLevelUp = Breeding.GetCanInheritMoves(species); + var baseMoves = learnset.GetBaseEggMoves(GetEggLevel(3)); + var eggMoves = source.GetEggMoves(species, 0); + var tms = PersonalInfo3.MachineMovesTechnical; + var hms = PersonalInfo3.MachineMovesHidden; + var tmhm = info.TMHM; + + for (int i = 0; i < moves.Length; i++) + { + var move = moves[i]; + byte value = 0; + if (baseMoves.Contains(move)) + value |= FlagBase; + if (eggMoves.Contains(move)) + value |= FlagGeneral; + if (inheritLevelUp && learnset.GetIsLearn(move)) + value |= FlagLevelUp; + + int tmIndex = tms.IndexOf(move); + if (tmIndex != -1 && tmhm[tmIndex]) + value |= FlagGeneral; + + int hmIndex = hms.IndexOf(move); + if (hmIndex != -1 && tmhm[50 + hmIndex]) + value |= FlagGeneral; + + if (value == 0) + return false; + flags[i] = value; + } + return true; + } + + private static bool MarkChildMoveFlags4(ushort species, ILearnSource source, PersonalInfo4 info, GameVersion version, ReadOnlySpan moves, Learnset learnset, Span flags) + { + bool inheritLevelUp = Breeding.GetCanInheritMoves(species); + var baseMoves = learnset.GetBaseEggMoves(GetEggLevel(4)); + var eggMoves = source.GetEggMoves(species, 0); + var tms = PersonalInfo4.MachineMovesTechnical; + var hms = version is HG or SS or HGSS ? PersonalInfo4.MachineMovesHiddenHGSS : PersonalInfo4.MachineMovesHiddenDPPt; + + for (int i = 0; i < moves.Length; i++) + { + var move = moves[i]; + byte value = 0; + if (baseMoves.Contains(move)) + value |= FlagBase; + if (eggMoves.Contains(move)) + value |= FlagGeneral; + if (inheritLevelUp && learnset.GetIsLearn(move)) + value |= FlagLevelUp; + + int tmIndex = tms.IndexOf(move); + if (tmIndex != -1 && info.GetIsLearnTM(tmIndex)) + value |= FlagGeneral; + + int hmIndex = hms.IndexOf(move); + if (hmIndex != -1 && info.GetIsLearnHM(hmIndex)) + value |= FlagGeneral; + + if (value == 0) + return false; + flags[i] = value; + } + return true; + } + + private static bool MarkChildMoveFlags5(ushort species, ILearnSource source, IPersonalInfoTM info, ReadOnlySpan moves, Learnset learnset, Span flags) + { + bool inheritLevelUp = Breeding.GetCanInheritMoves(species); + var baseMoves = learnset.GetBaseEggMoves(GetEggLevel(5)); + var eggMoves = source.GetEggMoves(species, 0); + var tms = PersonalInfo5BW.MachineMoves; + + for (int i = 0; i < moves.Length; i++) + { + var move = moves[i]; + byte value = 0; + if (baseMoves.Contains(move)) + value |= FlagBase; + if (eggMoves.Contains(move)) + value |= FlagGeneral; + if (inheritLevelUp && learnset.GetIsLearn(move)) + value |= FlagLevelUp; + + int tmIndex = tms.IndexOf(move); + if (tmIndex != -1 && info.GetIsLearnTM(tmIndex)) + value |= FlagGeneral; + + if (value == 0) + return false; + flags[i] = value; + } + return true; + } + + private static bool IsValidBaseCount(int baseCount, ReadOnlySpan moves, ReadOnlySpan baseMoves, ReadOnlySpan flags) + { + if (baseMoves.Length < baseCount) + return false; + + for (int i = 0; i < baseCount; i++) + { + if ((flags[i] & FlagBase) == 0) + return false; + + var expected = baseMoves[baseMoves.Length - baseCount + i]; + if (moves[i] != expected) + return false; + } + + for (int i = baseCount; i < moves.Length; i++) + { + if ((flags[i] & (FlagLevelUp | FlagGeneral)) == 0) + return false; + + if ((flags[i] & FlagBase) == 0) + continue; + + int baseIndex = baseMoves.IndexOf(moves[i]); + if (baseIndex == -1) + continue; + + int min = moves.Length - baseMoves.Length + baseIndex; + if (i < min + baseCount) + return false; + } + + return true; + } + + private static bool IsCompatibleFather(PersonalInfo mother, ushort fatherSpecies, PersonalInfo father) + { + if (fatherSpecies == (ushort)Species.Ditto) + return false; + if (father.Genderless || father.OnlyFemale) + return false; + return SharesEggGroup(mother, father); + } + + private static bool SharesEggGroup(PersonalInfo left, PersonalInfo right) + { + return SharesEggGroup(left.EggGroup1, left.EggGroup2, right.EggGroup1) + || SharesEggGroup(left.EggGroup1, left.EggGroup2, right.EggGroup2); + } + + private static bool SharesEggGroup(int group1, int group2, int other) + { + if (!IsBreedGroup(other)) + return false; + return group1 == other || group2 == other; + } + + private static bool IsBreedGroup(int group) => group is not ((int)EggGroup.None or (int)EggGroup.Ditto or (int)EggGroup.Undiscovered); + + private static int GetEggSpeciesCandidates(ushort species, GameVersion version, Span result) + { + var tree = EvolutionTree.GetEvolutionTree(version.Context); + ushort current = species; + ushort baseSpecies; + ushort splitSpecies = 0; + + while (true) + { + baseSpecies = current; + if (splitSpecies == 0 && Breeding.IsSplitBreedNotBabySpecies(current, version.Generation)) + splitSpecies = current; + + ref readonly var node = ref tree.Reverse.GetReverse(current, 0); + var previous = node.First.Species; + if (previous == 0) + break; + current = previous; + } + + result[0] = baseSpecies; + int count = 1; + if (splitSpecies != 0 && splitSpecies != baseSpecies) + result[count++] = splitSpecies; + return count; + } + + private static byte GetEggLevel(byte generation) => EggStateLegality.GetEggLevel(generation); + + private readonly record struct ChainQueryState + { + private readonly Species Species; + private readonly byte Count; + private readonly ushort Move1; + private readonly ushort Move2; + private readonly ushort Move3; + private readonly ushort Move4; + + public ChainQueryState(ushort species, ReadOnlySpan moves) + { + Species = (Species)species; + Count = (byte)moves.Length; + Move1 = moves.Length > 0 ? moves[0] : (ushort)0; + Move2 = moves.Length > 1 ? moves[1] : (ushort)0; + Move3 = moves.Length > 2 ? moves[2] : (ushort)0; + Move4 = moves.Length > 3 ? moves[3] : (ushort)0; + } + + public string Format(ReadOnlySpan species, ReadOnlySpan moves) + { + var sb = new StringBuilder(); + sb.Append(species[(int)Species]); + sb.Append(" ["); + if (Count > 0) sb.Append(moves[Move1]); + if (Count > 1) { sb.Append(", "); sb.Append(moves[Move2]); } + if (Count > 2) { sb.Append(", "); sb.Append(moves[Move3]); } + if (Count > 3) { sb.Append(", "); sb.Append(moves[Move4]); } + sb.Append(']'); + return sb.ToString(); + } + } +} + +public readonly record struct ChainBreedSummary(ushort EggSpecies, ushort FatherSpecies, byte ChainDepth); diff --git a/PKHeX.Core/Legality/Structures/LegalityCheckResultCode.cs b/PKHeX.Core/Legality/Structures/LegalityCheckResultCode.cs index 3518fa6a6..93ba34a11 100644 --- a/PKHeX.Core/Legality/Structures/LegalityCheckResultCode.cs +++ b/PKHeX.Core/Legality/Structures/LegalityCheckResultCode.cs @@ -70,6 +70,7 @@ public enum LegalityCheckResultCode : ushort EggLocationTrade, EggLocationTradeFail, EggMetLocationFail, + EggMoveCombination, EggNature, EggPP, EggPPUp, diff --git a/PKHeX.Core/Legality/Verifiers/Egg/EggVerifier.cs b/PKHeX.Core/Legality/Verifiers/Egg/EggVerifier.cs index cb60fd444..dd128c2c2 100644 --- a/PKHeX.Core/Legality/Verifiers/Egg/EggVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/Egg/EggVerifier.cs @@ -1,3 +1,4 @@ +using System; using static PKHeX.Core.LegalityCheckResultCode; using static PKHeX.Core.CheckIdentifier; @@ -10,27 +11,30 @@ internal sealed class EggVerifier : Verifier public override void Verify(LegalityAnalysis data) { var pk = data.Entity; + if (data.EncounterOriginal is IEncounterEgg egg) + VerifyEggBreedingChain(data, pk, egg); if (pk.IsEgg) - Verify(data, pk); + VerifyWhileEgg(data, pk); } - internal void Verify(LegalityAnalysis data, PKM pk) + private void VerifyWhileEgg(LegalityAnalysis data, PKM pk) { - VerifyCommon(data, pk); + // Not hatched yet, must have sane properties while still an egg. + var enc = data.EncounterOriginal; + VerifyCommon(data, pk, enc); // No egg have contest stats from the encounter. if (pk is IContestStatsReadOnly s && s.HasContestStats()) data.AddLine(GetInvalid(Egg, EggContest)); // Cannot transfer eggs across contexts (must be hatched). - var e = data.EncounterOriginal; - if (e.Context != pk.Context) + if (enc.Context != pk.Context) data.AddLine(GetInvalid(Egg, TransferEggVersion)); switch (pk) { // Side Game: No Eggs - case SK2 or CK3 or XK3 or BK4 or RK4 when e.Context == pk.Context: + case SK2 or CK3 or XK3 or BK4 or RK4 when enc.Context == pk.Context: // same context to not double-flag data.AddLine(GetInvalid(Egg, TransferEggVersion)); break; @@ -44,9 +48,8 @@ internal void Verify(LegalityAnalysis data, PKM pk) data.AddLine(GetInvalid(TransferTrackerShouldBeZero)); } - internal void VerifyCommon(LegalityAnalysis data, PKM pk) + internal void VerifyCommon(LegalityAnalysis data, PKM pk, IEncounterTemplate enc) { - var enc = data.EncounterMatch; if (!EggStateLegality.GetIsEggHatchCyclesValid(pk, enc)) data.AddLine(GetInvalid(Egg, EggHatchCycles)); @@ -74,4 +77,46 @@ private static bool MovesMatchRelearn(PKM pk) return false; return true; } + + private void VerifyEggBreedingChain(LegalityAnalysis data, PKM pk, IEncounterEgg egg) + { + // Check if we have any moves that are as a result of breeding chain. + Span moves = stackalloc ushort[4]; + pk.GetMoves(moves); + var personal = GameData.GetPersonal(egg.Version)[egg.Species, 0]; + var includeInheritedLevelUp = personal.OnlyFemale + && !Breeding.IsGenderSpeciesDetermination(egg.Species); + var count = GatherInheritedMoves(moves, data.Info.Moves, includeInheritedLevelUp); + if (count == 0 || (count == 1 && !includeInheritedLevelUp)) + return; + moves = moves[..count]; + + if (!ChainBreedLegality.IsValid(egg.Species, egg.Version, moves)) + data.AddLine(GetInvalid(Egg, EggMoveCombination)); + } + + private static int GatherInheritedMoves(Span moves, ReadOnlySpan parse, bool includeInheritedLevelUp) + { + // Collapse the list of moves to only those that are relevant for breeding chain verification. + int count = 0; + for (int i = 0; i < moves.Length; i++) + { + if (moves[i] == 0) + break; + + if (!IsInherited(parse[i], includeInheritedLevelUp)) + continue; + + moves[count] = moves[i]; + count++; + } + return count; + } + + private static bool IsInherited(MoveResult parsed, bool includeInheritedLevelUp) + { + var method = parsed.Info.Method; + return method is LearnMethod.EggMove + || (includeInheritedLevelUp && method is LearnMethod.InheritLevelUp); + } } diff --git a/PKHeX.Core/Legality/Verifiers/Misc/MiscG1Verifier.cs b/PKHeX.Core/Legality/Verifiers/Misc/MiscG1Verifier.cs index 9a96c5244..7cc05da10 100644 --- a/PKHeX.Core/Legality/Verifiers/Misc/MiscG1Verifier.cs +++ b/PKHeX.Core/Legality/Verifiers/Misc/MiscG1Verifier.cs @@ -13,30 +13,39 @@ internal sealed class MiscG1Verifier : Verifier internal void VerifyG1(LegalityAnalysis data, PKM pk) { - if (pk.IsEgg) - Eggs.VerifyCommon(data, pk); + Eggs.Verify(data); - if (pk is not PK1 pk1) - { - if (pk is ICaughtData2 { CaughtData: not 0 } t) - { - var time = t.MetTimeOfDay; - bool valid = data.EncounterOriginal switch - { - EncounterGift2 g2 when (!g2.IsEgg || pk.IsEgg) => time == 0, - EncounterTrade2 => time == 0, - _ => time is 1 or 2 or 3, - }; - if (!valid) - data.AddLine(GetInvalid(Encounter, MetDetailTimeOfDay)); - } - return; - } + if (pk is PK1 pk1) + VerifyAsFormat1(data, pk1); + else + VerifyAsFormat2(data, pk); + } + private void VerifyAsFormat1(LegalityAnalysis data, PK1 pk1) + { + // PK1-specific properties: personal Types and Catch Rate (held item). VerifyMiscG1Types(data, pk1); VerifyMiscG1CatchRate(data, pk1); } + private static void VerifyAsFormat2(LegalityAnalysis data, PKM pk) + { + // The only additional property in Gen2 is the Caught u16 with Time of Day and OT gender. + // OT Gender is checked separately in the Trainer verifier; still need to verify Time of Day. + if (pk is not ICaughtData2 { CaughtData: not 0 } t) + return; + + var time = t.MetTimeOfDay; + bool valid = data.EncounterOriginal switch + { + EncounterGift2 g2 when (!g2.IsEgg || pk.IsEgg) => time == 0, + EncounterTrade2 => time == 0, + _ => time is 1 or 2 or 3, + }; + if (!valid) + data.AddLine(GetInvalid(Encounter, MetDetailTimeOfDay)); + } + private void VerifyMiscG1Types(LegalityAnalysis data, PK1 pk1) { var species = pk1.Species; diff --git a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs index eb444d982..1260e2a79 100644 --- a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs @@ -36,8 +36,8 @@ public sealed class MiscVerifier : Verifier public override void Verify(LegalityAnalysis data) { var pk = data.Entity; - if (pk.IsEgg) - Eggs.Verify(data, pk); + // If it originated from an egg, check various egg rules. + Eggs.Verify(data); // Verify gimmick data switch (pk) diff --git a/PKHeX.Core/Resources/byte/eggmove/eggmove_gs.pkl b/PKHeX.Core/Resources/byte/eggmove/eggmove_gs.pkl index 5150d5a92..c658243e4 100644 Binary files a/PKHeX.Core/Resources/byte/eggmove/eggmove_gs.pkl and b/PKHeX.Core/Resources/byte/eggmove/eggmove_gs.pkl differ diff --git a/PKHeX.Core/Resources/localize/legality/legality_de.json b/PKHeX.Core/Resources/localize/legality/legality_de.json index de01faca5..95961ebf5 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_de.json +++ b/PKHeX.Core/Resources/localize/legality/legality_de.json @@ -53,6 +53,7 @@ "EggLocationTrade": "Ein getauschtes Ei kann am Fundort ausgebrütet werden.", "EggLocationTradeFail": "Ungültiger Ei-Ort, sollte im Ei-Status nicht 'getauscht' sein.", "EggMetLocationFail": "Ei kann an diesem Ei-Ort nicht erhalten werden.", + "EggMoveCombination": "Diese Kombination von Ei-Attacken kann in dieser Generation nicht vererbt werden.", "EggNature": "Statuswertanpassung eines Ei kann nicht geändert werden.", "EggPP": "Eier können keine modifizierten AP-Werte haben.", "EggPPUp": "AP-Plus kann nicht auf ein Ei angewendet werden.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_en.json b/PKHeX.Core/Resources/localize/legality/legality_en.json index cb95d66c8..8d5a0461f 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_en.json +++ b/PKHeX.Core/Resources/localize/legality/legality_en.json @@ -53,6 +53,7 @@ "EggLocationTrade": "Able to hatch a traded Egg at Met Location.", "EggLocationTradeFail": "Invalid Egg Location, shouldn't be 'traded' while an Egg.", "EggMetLocationFail": "Can't obtain Egg from Egg Location.", + "EggMoveCombination": "These Egg Moves cannot be inherited in combination in this generation.", "EggNature": "Eggs cannot have their Stat Alignment changed.", "EggPP": "Eggs cannot have modified move PP counts.", "EggPPUp": "Cannot apply PP Ups to an Egg.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_es-419.json b/PKHeX.Core/Resources/localize/legality/legality_es-419.json index 6d395fdac..2694aa128 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_es-419.json +++ b/PKHeX.Core/Resources/localize/legality/legality_es-419.json @@ -53,6 +53,7 @@ "EggLocationTrade": "Se puede eclosionar el Huevo intercambiado en la localización.", "EggLocationTradeFail": "Localización del Huevo inválida, no debería haber sido intercambiado mientras era un Huevo.", "EggMetLocationFail": "No se puede obtener el Huevo en la localización.", + "EggMoveCombination": "Esta combinación de movimientos huevo no puede heredarse en esta generación.", "EggNature": "Los Huevos no pueden tener ajustes de estadísticas.", "EggPP": "Los Huevos no pueden tener los contadores de PP modificados.", "EggPPUp": "No se puede aplicar Más PP a un huevo.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_es.json b/PKHeX.Core/Resources/localize/legality/legality_es.json index 00d739b56..042dc2f8a 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_es.json +++ b/PKHeX.Core/Resources/localize/legality/legality_es.json @@ -53,6 +53,7 @@ "EggLocationTrade": "Se puede eclosionar el Huevo intercambiado en la localización.", "EggLocationTradeFail": "Localización del Huevo inválida, no debería haber sido intercambiado mientras era un Huevo.", "EggMetLocationFail": "No se puede obtener el Huevo en la localización.", + "EggMoveCombination": "Esta combinación de movimientos huevo no puede heredarse en esta generación.", "EggNature": "Los Huevos no pueden tener ajustes de estadísticas.", "EggPP": "Los Huevos no pueden tener los contadores de PP modificados.", "EggPPUp": "No se puede aplicar Más PP a un huevo.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_fr.json b/PKHeX.Core/Resources/localize/legality/legality_fr.json index 7e721ea8a..3b5002b6a 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_fr.json +++ b/PKHeX.Core/Resources/localize/legality/legality_fr.json @@ -53,6 +53,7 @@ "EggLocationTrade": "Œuf échangé peut éclore au Lieu de Rencontre.", "EggLocationTradeFail": "Lieu de Rencontre de l'Œuf non valide, ne devrait pas être « échangé » en Œuf.", "EggMetLocationFail": "L'Œuf ne peut pas être obtenu au Lieu de Rencontre.", + "EggMoveCombination": "Cette combinaison de capacités Repro ne peut pas être transmise dans cette génération.", "EggNature": "Impossible de modifier l'ajustement des statistiques d'un Œuf.", "EggPP": "Impossible de modifier les PP d'un Œuf.", "EggPPUp": "Impossible d'appliquer des PP Plus à un Œuf.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_it.json b/PKHeX.Core/Resources/localize/legality/legality_it.json index fdc9926ac..08a76b65a 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_it.json +++ b/PKHeX.Core/Resources/localize/legality/legality_it.json @@ -53,6 +53,7 @@ "EggLocationTrade": "È possibile schiudere un Uovo scambiato nel luogo di incontro.", "EggLocationTradeFail": "Il luogo di incontro non dovrebbe essere 'Scambio', se è un Uovo.", "EggMetLocationFail": "Impossibile ottenre l'Uovo nel Luogo dell'Uovo.", + "EggMoveCombination": "Questa combinazione di mosse Uovo non può essere ereditata in questa generazione.", "EggNature": "Le Uova non possono avere l'aggiustamento delle statistiche cambiato.", "EggPP": "Le Uova non possono avere modifiche ai PP.", "EggPPUp": "Non si possono applicare PP Up alle Uova.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_ja.json b/PKHeX.Core/Resources/localize/legality/legality_ja.json index df91f4af7..38690221c 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_ja.json +++ b/PKHeX.Core/Resources/localize/legality/legality_ja.json @@ -31,16 +31,16 @@ "BallSpecies": "このボールでは捕獲できません。", "BallSpeciesPass": "ポケモンによっては捕獲可能なボール。", "BallUnavailable": "元の世代では入手できないボール。", - "BallG4Sinnoh": "Ball value for D/P/Pt (0x83) is not within range.", - "BallG4Johto": "Extended Ball value for HG/SS (0x86) is not within range.", + "BallG4Sinnoh": "D/P/Pt (0x83) のボール値が範囲外です。", + "BallG4Johto": "HG/SS (0x86) の拡張ボール値が範囲外です。", "ContestZero": "コンディションは0でなければなりません。", "ContestZeroSheen": "けづやは0でなければなりません。", "ContestSheenGEQ_0": "けづやは >= {0} でなければなりません。", "ContestSheenLEQ_0": "けづやは <= {0} でなければなりません。", - "DateCalendarInvalidMet": "Met Date is not a valid calendar date.", - "DateCalendarInvalidEgg": "Egg Met Date is not a valid calendar date.", - "DateLocalInvalidDate": "Local Date is outside of console's local time window.", - "DateLocalInvalidTime": "Local Time is not a valid timestamp.", + "DateCalendarInvalidMet": "出会った日が有効なカレンダー日付ではありません。", + "DateCalendarInvalidEgg": "タマゴの出会った日が有効なカレンダー日付ではありません。", + "DateLocalInvalidDate": "ローカル日付がコンソールのローカル時間の範囲外です。", + "DateLocalInvalidTime": "ローカル時間が有効なタイムスタンプではありません。", "DateOutsideDistributionWindow": "出会った日が配布期間外です。", "EggContest": "タマゴはコンディションを上げることはできません。", "EggEXP": "タマゴは経験値を得ることはできません。", @@ -53,6 +53,7 @@ "EggLocationTrade": "出会った場所で交換したタマゴを孵化させることができます。", "EggLocationTradeFail": "もらった場所が無効です。タマゴの状態で交換してはいけません。", "EggMetLocationFail": "そのもらった場所では、タマゴを入手できません。", + "EggMoveCombination": "このタマゴわざの組み合わせは、今の世代では遺伝できません。", "EggNature": "タマゴは能力調整を変更できません。", "EggPP": "タマゴは技のPPを変更することはできません。", "EggPPUp": "タマゴにポイントアップは使用できません", diff --git a/PKHeX.Core/Resources/localize/legality/legality_ko.json b/PKHeX.Core/Resources/localize/legality/legality_ko.json index 33d8f17b8..1282eeae8 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_ko.json +++ b/PKHeX.Core/Resources/localize/legality/legality_ko.json @@ -53,6 +53,7 @@ "EggLocationTrade": "만난 장소에서 교환한 알을 부화시킬 수 있습니다.", "EggLocationTradeFail": "알을 만난 장소가 잘못되었습니다. 부화 전 알을 교환할 수 없습니다.", "EggMetLocationFail": "알을 만난 장소에서 알을 얻을 수 없습니다.", + "EggMoveCombination": "이 알기술 조합은 현재 세대에서 유전될 수 없습니다.", "EggNature": "알은 능력치 조정을 변경할 수 없습니다.", "EggPP": "알은 PP를 수정할 수 없습니다.", "EggPPUp": "알에는 PP업을 적용할 수 없습니다.", diff --git a/PKHeX.Core/Resources/localize/legality/legality_zh-hans.json b/PKHeX.Core/Resources/localize/legality/legality_zh-hans.json index f90358312..44f6cd759 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_zh-hans.json +++ b/PKHeX.Core/Resources/localize/legality/legality_zh-hans.json @@ -53,6 +53,7 @@ "EggLocationTrade": "能在相遇地点孵化交易的蛋。", "EggLocationTradeFail": "非法蛋取得场所, 不能在还是蛋时“交换”", "EggMetLocationFail": "不能在蛋取得场所获得蛋。", + "EggMoveCombination": "这一组蛋招式在本世代中无法遗传。", "EggNature": "不能改变蛋的能力调整(薄荷)。", "EggPP": "蛋不能有PP数变动。", "EggPPUp": "不能对蛋使用PP提升剂。", diff --git a/PKHeX.Core/Resources/localize/legality/legality_zh-hant.json b/PKHeX.Core/Resources/localize/legality/legality_zh-hant.json index 559c50ebb..d546cc1b4 100644 --- a/PKHeX.Core/Resources/localize/legality/legality_zh-hant.json +++ b/PKHeX.Core/Resources/localize/legality/legality_zh-hant.json @@ -53,6 +53,7 @@ "EggLocationTrade": "可於遇見地點孵化「連線交換」所得蛋。", "EggLocationTradeFail": "不合法之蛋取得場所, 無法於寶可夢仍是蛋時「連線交換」。", "EggMetLocationFail": "無法於設定之蛋取得場所獲得該蛋。", + "EggMoveCombination": "這組蛋招式在本世代中無法遺傳。", "EggNature": "無法使用「薄荷」修正蛋之能力調整。", "EggPP": "蛋不能修改 PP 值。", "EggPPUp": "不能對蛋使用 PP 提升劑。", diff --git a/Tests/PKHeX.Core.Tests/Legality/ChainBreedLegalityTests.cs b/Tests/PKHeX.Core.Tests/Legality/ChainBreedLegalityTests.cs new file mode 100644 index 000000000..b7b712272 --- /dev/null +++ b/Tests/PKHeX.Core.Tests/Legality/ChainBreedLegalityTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Runtime.InteropServices; +using FluentAssertions; +using Xunit; +using static PKHeX.Core.GameVersion; +using static PKHeX.Core.Move; +using static PKHeX.Core.Species; + +namespace PKHeX.Core.Tests.Legality; + +public class ChainBreedLegalityTests +{ + private static ReadOnlySpan GetMoves(ReadOnlySpan moves) + => MemoryMarshal.Cast(moves); + + [Theory] + [InlineData(BW, Marill, BellyDrum, AquaJet)] + [InlineData(B2W2, Azurill, BellyDrum, AquaJet)] + [InlineData(FR, Squirtle, Haze, Flail)] + [InlineData(B2W2, Chansey, EggBomb)] + [InlineData(GS, Oddish, Flail, RazorLeaf, SwordsDance, Synthesis)] + [InlineData(GS, Smoochum, LovelyKiss)] // egg move removed from table (no parents) + public void DetectsInvalidChains(GameVersion version, Species species, params Move[] movelist) + { + var moves = GetMoves(movelist); + ChainBreedLegality.IsValid((ushort)species, version, moves).Should().BeFalse(); + } + + [Theory] + [InlineData(HGSS, Slugma, Smokescreen, HeatWave)] // Heat Wave is a Tutor in HG/SS. + [InlineData(GS, Paras, Counter, Flail, LightScreen)] + [InlineData(HGSS, Mankey, Encore, Meditate, SmellingSalts)] + [InlineData(GS, Chansey, DoubleEdge)] // via Jigglypuff (Level 39) + [InlineData(Pt, Shellder, RapidSpin, IcicleSpear)] + public void DetectsValidChains(GameVersion version, Species species, params Move[] movelist) + => ValidateSimple(version, species, movelist); + + [Theory] + // Avalanche learned in Gen4 TM, TakeDown learned via *special encounter* move in Gen3 XD. + [InlineData(B2W2, Shellder, TakeDown)] // Gen3 encounter move (in XD) + [InlineData(B2W2, Shellder, Avalanche)] // Gen4 TM move + [InlineData(B2W2, Shellder, Avalanche, TakeDown)] // Valid Gen5 parent from a Gen3 encounter=>Gen4=>Gen5 transfer route. + public void DetectValidChainPastFather(GameVersion version, Species species, params Move[] movelist) + => ValidateSimple(version, species, movelist); + + [Theory] + // evolve=>pass chain with same species lineage: multiple Tyrogue evolutions (hitmonlee, hitmonchan, hitmontop), all providing one move. + [InlineData(GS, Tyrogue, HighJumpKick, MachPunch, RapidSpin)] + public void DetectValidChainCyclic(GameVersion version, Species species, params Move[] movelist) + => ValidateSimple(version, species, movelist); + + private static void ValidateSimple(GameVersion version, Species species, ReadOnlySpan movelist) + { + var moves = GetMoves(movelist); + ChainBreedLegality.TryValidate((ushort)species, version, moves, out var summary).Should().BeTrue(); + summary.EggSpecies.Should().Be((ushort)species); + summary.FatherSpecies.Should().NotBe(0); + summary.ChainDepth.Should().BeGreaterThan(0); + } + + [Theory] + [InlineData(HGSS, Mankey, Smeargle, Encore, Meditate, SmellingSalts)] + public void DetectsValidChainSmeargle(GameVersion version, Species species, Species father, params Move[] movelist) + { + var moves = GetMoves(movelist); + ChainBreedLegality.TryValidate((ushort)species, version, moves, out var summary).Should().BeTrue(); + summary.EggSpecies.Should().Be((ushort)species); + summary.FatherSpecies.Should().Be((ushort)father); + summary.ChainDepth.Should().BeGreaterThan(0); + } + + [Theory] + [InlineData(BD, Smoochum, false, PowderSnow)] // none in Human-like + [InlineData(BD, Smoochum, true, Confusion)] // via Alakazam + // Female only species: Male must be able to pass inheritable level up move. + public void DetectsInvalidInheritedLevelUpMove(GameVersion version, Species species, bool expect, params Move[] movelist) + { + var moves = GetMoves(movelist); + ChainBreedLegality.IsValid((ushort)species, version, moves).Should().Be(expect); + } + + [Theory] + // For breeding purposes of Volbeat and Nidoran-M, the father must be able to pass all egg moves. + // If Gen6+, the mother can aggregate egg moves, so mother egg moves can be ignored. + // Volbeat can get Lunge from Mothim and Dewpider, but Dizzy Punch is only from Spinda and Lopunny. + // Level up moves are not considered, as Volbeat/etc can breed with Ditto to pass level-up and acquired egg chain moves. + // This check is only relevant up to Gen7, as egg move sharing became a thing. + [InlineData(US, Volbeat, false, DizzyPunch, Lunge)] // mother can't learn either, father must pass both (none can do both). + [InlineData(B2, Volbeat, false, DizzyPunch, SeismicToss)] // father must pass both in Gen5 + [InlineData(B2, Volbeat, true, DizzyPunch)] // father must pass both in Gen5 + [InlineData(B2, Volbeat, true, SeismicToss)] // father must pass both in Gen5 + // Amnesia via Psyduck, Head Smash via Rampardos. No father can pass both when breeding with Nidoran-F. + [InlineData(X, NidoranM, false, Amnesia, HeadSmash)] // mother can't learn either, father must pass both (none can do both). + [InlineData(Y, NidoranM, true, BeatUp, HeadSmash)] // mother can pass Beat Up, father can pass Head Smash. + [InlineData(B2, NidoranM, false, BeatUp, HeadSmash)] // father must pass both in Gen5 + public void DetectInvalidSpeciesMaleSplit(GameVersion version, Species species, bool expect, params Move[] movelist) + { + var moves = GetMoves(movelist); + ChainBreedLegality.IsValid((ushort)species, version, moves).Should().Be(expect); + } +} diff --git a/Tests/PKHeX.Core.Tests/Legality/Legal/Generation 2/043 - ODDISH - 2DCE.pk2 b/Tests/PKHeX.Core.Tests/Legality/Illegal/Gen2/043 - ODDISH - 2DCE noChain.pk2 similarity index 100% rename from Tests/PKHeX.Core.Tests/Legality/Legal/Generation 2/043 - ODDISH - 2DCE.pk2 rename to Tests/PKHeX.Core.Tests/Legality/Illegal/Gen2/043 - ODDISH - 2DCE noChain.pk2