mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
Refer to pull request notes and the eventual changelog for a high-level summary. Co-authored-by: Matt <17801814+sora10pls@users.noreply.github.com> Co-authored-by: Lusamine <30205550+Lusamine@users.noreply.github.com> Co-authored-by: SciresM <8676005+SciresM@users.noreply.github.com>
276 lines
10 KiB
C#
276 lines
10 KiB
C#
using System;
|
|
using static PKHeX.Core.LegalityCheckResultCode;
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
public sealed class LegendsZAVerifier : Verifier
|
|
{
|
|
protected override CheckIdentifier Identifier => CheckIdentifier.RelearnMove;
|
|
|
|
public override void Verify(LegalityAnalysis data)
|
|
{
|
|
if (data.Entity is not PA9 pa9)
|
|
return;
|
|
CheckLearnset(data, pa9);
|
|
CheckFlagsTM(data, pa9);
|
|
CheckFlagsPlus(data, pa9);
|
|
|
|
if (pa9.LevelBoost is not 0)
|
|
data.AddLine(GetInvalid(LevelBoostNotZero));
|
|
}
|
|
|
|
private void CheckLearnset(LegalityAnalysis data, PA9 pa)
|
|
{
|
|
if (data.EncounterMatch is not IEncounter9a e9a)
|
|
return; // Don't bother.
|
|
|
|
var moveCount = pa.MoveCount;
|
|
if (moveCount == 4)
|
|
return;
|
|
|
|
// TODO ZA HOME
|
|
// // Flag move slots that are empty.
|
|
// if (pa.Tracker != 0 || !ParseSettings.IgnoreTransferIfNoTracker)
|
|
// return; // Can delete moves in PA9 moveset via HOME.
|
|
|
|
// Get the bare minimum moveset.
|
|
Span<ushort> expect = stackalloc ushort[4];
|
|
_ = LoadBareMinimumMoveset(e9a, pa, expect);
|
|
|
|
// Expected move can be empty due to user rearranging.
|
|
// Account for player rearrangement of moves.
|
|
var moves = data.Info.Moves;
|
|
for (int i = 0; i < moves.Length; i++)
|
|
{
|
|
var currentMove = pa.GetMove(i);
|
|
var index = expect.IndexOf(currentMove);
|
|
if (index != -1 && index != i) // Swapped move. Swap the expected slot.
|
|
(expect[i], expect[index]) = (expect[index], expect[i]);
|
|
}
|
|
|
|
// Now that we've rearranged the expected moves to matching slots (if any), flag any mismatches.
|
|
// Basically only will flag "(None) => Expected {non-zero-move} instead."
|
|
for (int i = 0; i < expect.Length; i++)
|
|
{
|
|
var move = expect[i];
|
|
if (pa.GetMove(i) != move)
|
|
moves[i] = MoveResult.Unobtainable(move);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the expected minimum count of moves, and modifies the input <see cref="moves"/> with the bare minimum move IDs.
|
|
/// </summary>
|
|
private static int LoadBareMinimumMoveset(IEncounter9a enc, PA9 pa, Span<ushort> moves)
|
|
{
|
|
// Get any encounter moves
|
|
var ls = LearnSource8LA.Instance;
|
|
var moveset = ls.GetLearnset(enc.Species, enc.Form);
|
|
GetInitialMoves(enc, pa, moves);
|
|
var count = moves.IndexOf((ushort)0);
|
|
if ((uint)count >= 4)
|
|
return 4;
|
|
|
|
// If it can be leveled up in other games, level it up in other games.
|
|
if (pa is IHomeTrack { HasTracker: true })
|
|
return count;
|
|
|
|
// Level up moves never repeat, so just level up to current level.
|
|
var ms = moveset.GetMoveRange(pa.CurrentLevel, (byte)(pa.MetLevel + 1));
|
|
foreach (var move in ms)
|
|
{
|
|
if (moves.Contains(move)) // just in case.
|
|
continue;
|
|
moves[count++] = move;
|
|
if ((uint)count >= 4)
|
|
return 4;
|
|
}
|
|
|
|
// Check if any TM/Evo/Relearn moves were learned. They'll be anything we've not been able to learn yet.
|
|
// Don't check the validity of a foreign move.
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
var move = pa.GetMove(i);
|
|
if (move == 0)
|
|
continue;
|
|
if (moves.Contains(move))
|
|
continue;
|
|
moves[count++] = move;
|
|
if ((uint)count >= 4)
|
|
return 4;
|
|
}
|
|
|
|
// No other tutor sources.
|
|
return count;
|
|
}
|
|
|
|
private static void GetInitialMoves(IEncounter9a enc, PA9 pa9, Span<ushort> moves)
|
|
{
|
|
if (enc is IMoveset { Moves: { HasMoves: true } m })
|
|
{
|
|
m.CopyTo(moves);
|
|
return;
|
|
}
|
|
var level = Math.Max((byte)1, pa9.MetLevel);
|
|
var learn = LearnSource9ZA.Instance.GetLearnset(enc.Species, enc.Form);
|
|
if (!enc.IsAlpha)
|
|
{
|
|
learn.SetEncounterMoves(level, moves);
|
|
return;
|
|
}
|
|
learn.SetEncounterMovesBackwards(level, moves, sameDescend: false);
|
|
moves[0] = PersonalTable.ZA[enc.Species, enc.Form].AlphaMove;
|
|
}
|
|
|
|
private void CheckFlagsTM(LegalityAnalysis data, PA9 pa9)
|
|
{
|
|
// Wild Alphas automatically come with a specific move.
|
|
var enc = data.EncounterMatch;
|
|
if (enc.Context is not EntityContext.Gen9a)
|
|
return;
|
|
if (enc is WA9 or not IAlphaReadOnly { IsAlpha: true })
|
|
return;
|
|
|
|
var pi = PersonalTable.ZA[enc.Species, enc.Form];
|
|
var move = pi.AlphaMove;
|
|
var indexPlus = PersonalInfo9ZA.PlusMoves.IndexOf(move);
|
|
|
|
if (indexPlus != -1)
|
|
{
|
|
if (!pa9.GetMovePlusFlag(indexPlus))
|
|
data.AddLine(GetInvalid(PlusMoveAlphaMissing_0, move));
|
|
}
|
|
}
|
|
|
|
private void CheckFlagsPlus(LegalityAnalysis la, PA9 pk)
|
|
{
|
|
var permit = (IPermitPlus)la.PersonalInfo;
|
|
|
|
// Check for any impossible-to-set flag indexes.
|
|
if (pk.GetMovePlusFlagAnyImpossible())
|
|
la.AddLine(GetInvalid(PlusMoveCountInvalid));
|
|
|
|
// Check for all required indexes.
|
|
var (_, plus) = LearnSource9ZA.GetLearnsetAndPlus(pk.Species, pk.Form);
|
|
var currentLevel = pk.CurrentLevel;
|
|
CheckPlusMoveFlags(la, pk, permit, plus, currentLevel);
|
|
|
|
// Check for indexes set that cannot be set via TM or NPC.
|
|
int max = permit.PlusCountUsed;
|
|
var evos = la.Info.EvoChainsAllGens.Gen9a;
|
|
var invalidMove = GetInvalidPlusMove(pk, max, permit, evos);
|
|
if (invalidMove == 0)
|
|
return;
|
|
|
|
// The above result stores the first invalid move, or a magic value for multiple invalid moves.
|
|
var msg = invalidMove is MultipleInvalidPlusMoves
|
|
? GetInvalid(PlusMoveMultipleInvalid)
|
|
: GetInvalid(PlusMoveInvalid_0, invalidMove);
|
|
la.AddLine(msg);
|
|
}
|
|
|
|
private void CheckPlusMoveFlags<T>(LegalityAnalysis la, T pk, IPermitPlus permit, Learnset plus, byte currentLevel) where T : IPlusRecord
|
|
{
|
|
var levels = plus.GetAllLevels();
|
|
var moves = plus.GetAllMoves();
|
|
for (int i = 0; i < levels.Length; i++)
|
|
{
|
|
var level = levels[i];
|
|
if (level > currentLevel)
|
|
break; // not able to be Plus'd, therefore no need to check.
|
|
|
|
var move = moves[i];
|
|
var index = permit.PlusMoveIndexes.IndexOf(move);
|
|
if (index == -1)
|
|
throw new IndexOutOfRangeException("Unexpected learn move index, not in Plus moves?");
|
|
|
|
if (pk.GetMovePlusFlag(index))
|
|
continue; // All good, flagged.
|
|
|
|
// Trade evolutions forget to set the Plus flags, unlike triggered evolutions.
|
|
// If the move is not present as a previous-evolution learnset move, and the head species is a Trade evo, skip the error.
|
|
// Assume the best case -- evolved at current level, so none would get set.
|
|
if (IsTradeEvoSkip(la.Info.EvoChainsAllGens.Gen9a, move))
|
|
continue;
|
|
|
|
la.AddLine(GetInvalid(PlusMoveSufficientLevelMissing_0, move, level));
|
|
}
|
|
}
|
|
|
|
private static bool IsTradeEvoSkip(ReadOnlySpan<EvoCriteria> evos, ushort move)
|
|
{
|
|
if (evos.Length <= 1)
|
|
return false;
|
|
|
|
if (!evos[0].Method.IsTrade())
|
|
return false;
|
|
|
|
// Check if the pre-evolution could have learned it before evolving.
|
|
for (int i = 1; i < evos.Length; i++)
|
|
{
|
|
var evo = evos[i];
|
|
var (_, plus) = LearnSource9ZA.GetLearnsetAndPlus(evo.Species, evo.Form);
|
|
var moves = plus.GetAllMoves();
|
|
|
|
var index = moves.IndexOf(move);
|
|
if (index == -1)
|
|
continue; // can't learn
|
|
|
|
// if the evo must have traversed this level range (and not the head's level range), then it must have been flagged.
|
|
var levels = plus.GetAllLevels();
|
|
var plusLevel = levels[index];
|
|
var headLevel = evos[0].LevelMin;
|
|
if (plusLevel <= headLevel)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private const ushort MultipleInvalidPlusMoves = ushort.MaxValue;
|
|
|
|
private static ushort GetInvalidPlusMove<T>(T pk, int maxIndex, IPermitPlus permit, ReadOnlySpan<EvoCriteria> evos)
|
|
where T : IPlusRecord
|
|
{
|
|
ushort invalid = 0;
|
|
for (int i = 0; i < maxIndex; i++)
|
|
{
|
|
if (!pk.GetMovePlusFlag(i))
|
|
continue;
|
|
var move = permit.PlusMoveIndexes[i];
|
|
var index = permit.RecordPermitIndexes.IndexOf(move);
|
|
if (CanAnyEvoLearnMovePlus<PersonalTable9ZA, PersonalInfo9ZA, LearnSource9ZA>(evos, index, move, PersonalTable.ZA, LearnSource9ZA.Instance))
|
|
continue; // OK
|
|
|
|
if (invalid != 0) // Multiple invalid moves
|
|
return MultipleInvalidPlusMoves;
|
|
invalid = move;
|
|
}
|
|
return invalid;
|
|
}
|
|
|
|
private static bool CanAnyEvoLearnMovePlus<TTable, TInfo, TSource>(ReadOnlySpan<EvoCriteria> evos, int tmIndex, ushort move,
|
|
TTable table, TSource source)
|
|
where TTable : IPersonalTable<TInfo>
|
|
where TInfo : IPersonalInfo, IPersonalInfoTM
|
|
where TSource : ILearnSourceBonus
|
|
{
|
|
// Seed of Mastery can be used on any currently-known move to grant the Plus Move flag.
|
|
// Without using one, moves that are naturally learned on level-up/evolution will be automatically marked as Plus when a higher level threshold is met.
|
|
// For our purposes, we are only checking legality, so assume that a Seed of Mastery is used in all cases (bypassing the higher level threshold).
|
|
|
|
foreach (var evo in evos)
|
|
{
|
|
// If the move can be learned as TM, can be marked as Plus Move regardless of level via Seed of Mastery.
|
|
var pi = table[evo.Species, evo.Form];
|
|
if (tmIndex != -1 && pi.GetIsLearnTM(tmIndex))
|
|
return true;
|
|
|
|
// If the move can be learned via learnset. Seed of Mastery allows marking as Plus Move regardless of level.
|
|
var (learn, _) = source.GetLearnsetAndOther(evo.Species, evo.Form);
|
|
if (learn.TryGetLevelLearnMove(move, out var level) && level <= evo.LevelMax)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|