diff --git a/Documentation/16.-Catch-Volcanion-Legends-Z-A.md b/Documentation/16.-Catch-Volcanion-Legends-Z-A.md new file mode 100644 index 0000000..8a60125 --- /dev/null +++ b/Documentation/16.-Catch-Volcanion-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Volcanion + +This guide covers catching Volcanion until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to the Volcanion encounter +- Position yourself in front of Volcanion + +## Setup + +1. Stand directly in front of Volcanion +2. **Important**: If you start the routine and the battle does not begin, take one more step forward +3. Once positioned correctly, **save your game** +4. Start the bot + +## Catch Rate Cheat + +- If you select **Master Ball**: No cheat needed (100% catch rate) +- If you select **any other ball**: Enable the 100% catch rate cheat + +## How It Works + +The bot will: +1. Initiate the battle with Volcanion +2. Attempt to catch Volcanion with your selected ball +3. Check if the caught Volcanion in B1S1 matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure your positioning is correct before starting (battle should begin immediately) +- Use stop conditions to target specific Volcanion (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats \ No newline at end of file diff --git a/Documentation/17.-Catch-Genesect-Legends-Z-A.md b/Documentation/17.-Catch-Genesect-Legends-Z-A.md new file mode 100644 index 0000000..303f639 --- /dev/null +++ b/Documentation/17.-Catch-Genesect-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Genesect + +This guide covers catching Genesect until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to the Genesect encounter +- Position yourself in front of Genesect + +## Setup + +1. Stand directly in front of Genesect +2. **Important**: If you start the routine and the battle does not begin, take one more step forward +3. Once positioned correctly, **save your game** +4. Start the bot + +## Catch Rate Cheat + +- If you select **Master Ball**: No cheat needed (100% catch rate) +- If you select **any other ball**: Enable the 100% catch rate cheat + +## How It Works + +The bot will: +1. Initiate the battle with Genesect +2. Attempt to catch Genesect with your selected ball +3. Check if the caught Genesect in B1S1 matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure your positioning is correct before starting (battle should begin immediately) +- Use stop conditions to target specific Genesect (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats \ No newline at end of file diff --git a/Documentation/18.-Receive-Hoopa-Legends-Z-A.md b/Documentation/18.-Receive-Hoopa-Legends-Z-A.md new file mode 100644 index 0000000..18325cf --- /dev/null +++ b/Documentation/18.-Receive-Hoopa-Legends-Z-A.md @@ -0,0 +1,30 @@ +# Legends: Z-A: Receive Hoopa + +This guide covers receiving Hoopa until a `StopCondition` is met (e.g., specific IVs, etc.). + +## Prerequisites + +- Have access to the Hoopa event/interactions + +## Setup + +1. Stand directly in front of the interaction that gives Hoopa +2. Make sure slot 1 of box 1 (B1S1) is empty +3. **Save your game** +4. Start the bot + +## How It Works + +The bot will: +1. Repeatedly advance dialogue/interactions to receive Hoopa +2. Check B1S1 for the received Hoopa +3. Validate Hoopa against your stop conditions +4. If no match is found, reset and repeat the process + +Since Hoopa is available in B1S1 before the auto-save is triggered, this routine can run without any cheats needed. + +## Tips + +- Make sure you are positioned correctly before starting +- Keep B1S1 empty to avoid species mismatch errors +- Use stop conditions to target specific Hoopa (shiny, nature, IVs, etc.) \ No newline at end of file diff --git a/Documentation/19.-Receive-Magearna-Legends-Z-A.md b/Documentation/19.-Receive-Magearna-Legends-Z-A.md new file mode 100644 index 0000000..dbcdff1 --- /dev/null +++ b/Documentation/19.-Receive-Magearna-Legends-Z-A.md @@ -0,0 +1,30 @@ +# Legends: Z-A: Receive Magearna + +This guide covers receiving Magearna until a `StopCondition` is met (e.g., specific IVs, etc.). + +## Prerequisites + +- Have access to the Magearna event/interactions + +## Setup + +1. Stand directly in front of the interaction that gives Magearna +2. Make sure slot 1 of box 1 (B1S1) is empty +3. **Save your game** +4. Start the bot + +## How It Works + +The bot will: +1. Repeatedly advance dialogue/interactions to receive Magearna +2. Check B1S1 for the received Magearna +3. Validate Magearna against your stop conditions +4. If no match is found, reset and repeat the process + +Since Magearna is available in B1S1 before the auto-save is triggered, this routine can run without any cheats needed. + +## Tips + +- Make sure you are positioned correctly before starting +- Keep B1S1 empty to avoid species mismatch errors +- Use stop conditions to target specific Magearna (shiny, nature, IVs, etc.) \ No newline at end of file diff --git a/Documentation/20.-Catch-Marshadow-Legends-Z-A.md b/Documentation/20.-Catch-Marshadow-Legends-Z-A.md new file mode 100644 index 0000000..e0fe75c --- /dev/null +++ b/Documentation/20.-Catch-Marshadow-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Marshadow + +This guide covers catching Marshadow until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to the Marshadow encounter +- Position yourself in front of Marshadow + +## Setup + +1. Stand directly in front of Marshadow +2. **Important**: If you start the routine and the battle does not begin, take one more step forward +3. Once positioned correctly, **save your game** +4. Start the bot + +## Catch Rate Cheat + +- If you select **Master Ball**: No cheat needed (100% catch rate) +- If you select **any other ball**: Enable the 100% catch rate cheat + +## How It Works + +The bot will: +1. Initiate the battle with Marshadow +2. Attempt to catch Marshadow with your selected ball +3. Check if the caught Marshadow in B1S1 matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure your positioning is correct before starting (battle should begin immediately) +- Use stop conditions to target specific Marshadow (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats \ No newline at end of file diff --git a/Documentation/README.md b/Documentation/README.md index a7f5194..fd77062 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -25,4 +25,9 @@ Welcome to the Sysbot.NET wiki! 12. [Wild Zone Entrance](12.-Wild-Zone-Entrance-Legends-Z-A.md) 13. [Fossil reviving](13.-Fossil-Revive-Legends-Z-A.md) 14. [Receiving Floette](14.-Receive-Floette-Legends-Z-A.md) -15. [Catch Diancie](15.-Catch-Diancie-Legends-Z-A.md) \ No newline at end of file +15. [Catch Diancie](15.-Catch-Diancie-Legends-Z-A.md) +16. [Catch Volcanion](16.-Catch-Volcanion-Legends-Z-A.md) +17. [Catch Genesect](17.-Catch-Genesect-Legends-Z-A.md) +18. [Receive Hoopa](18.-Receive-Hoopa-Legends-Z-A.md) +19. [Receive Magearna](19.-Receive-Magearna-Legends-Z-A.md) +20. [Catch Marshadow](20.-Catch-Marshadow-Legends-Z-A.md) \ No newline at end of file diff --git a/README.md b/README.md index cdd0425..a09986b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ These routines are new compared to the original SysBot.NET. - Fossil reviving - Receiving Floette - Catching Diancie +- Catching Volcanion +- Catching Genesect +- Receiving Hoopa +- Receiving Magearna +- Catching Marshadow #### Scarlet/Violet diff --git a/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs b/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs index ba02726..a0462a5 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs @@ -7,7 +7,7 @@ namespace SysBot.Pokemon; public abstract class PokeRoutineExecutorBase : SwitchRoutineExecutor { - public const decimal BotbaseVersion = 2.4m; + public const decimal BotbaseVersion = 2.5m; protected PokeRoutineExecutorBase(IConsoleBotManaged cfg) : base(cfg) { diff --git a/SysBot.Pokemon/Actions/PokeRoutineType.cs b/SysBot.Pokemon/Actions/PokeRoutineType.cs index 76873a6..56a7402 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineType.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineType.cs @@ -75,6 +75,21 @@ public enum PokeRoutineType /// Retrieves Diancie until the criteria is satisfied. EncounterDiancie = 7_006, + /// Retrieves Volcanion until the criteria is satisfied. + EncounterVolcanion = 7_007, + + /// Retrieves Genesect until the criteria is satisfied. + EncounterGenesect = 7_008, + + /// Retrieves Magearna until the criteria is satisfied. + EncounterMagearna = 7_009, + + /// Retrieves Marshadow until the criteria is satisfied. + EncounterMarshadow = 7_010, + + /// Retrieves Hoopa until the criteria is satisfied. + EncounterHoopa = 7_011, + /// Keeps running circles until all party members have the Partner mark. PartnerMark = 7_050, diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs new file mode 100644 index 0000000..da2f97b --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs @@ -0,0 +1,126 @@ +namespace SysBot.Pokemon; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotGenesectLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private const ushort GenesectSpecies = (ushort)Species.Genesect; + + private const int EncounterTimeoutSeconds = 46; + private const int MashDurationMs = 26_000; + private const int MashDelayMs = 100; + private const int ReopenDelayMs = 10_000; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var deadline = DateTime.Now.AddSeconds(EncounterTimeoutSeconds); + + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Genesect encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, MashDurationMs, MashDelayMs, token).ConfigureAwait(false); + + Log($"Catching Genesect, wait till [{deadline}] before we force a game restart", false); + + await SetStick(LEFT, 0, 30_000, 1_000, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + + var result = await TryEncounterUntilDeadline(deadline, token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; + + if (result == EncounterResult.Timeout) + Log("Force restart of the game..."); + + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + await Task.Delay(ReopenDelayMs, token).ConfigureAwait(false); + } + } + + public override async Task HardStop() + { + await ReleaseHold(ZL, 0_500, CancellationToken.None).ConfigureAwait(false); + await base.HardStop().ConfigureAwait(false); + } + + private async Task TryEncounterUntilDeadline(DateTime deadline, CancellationToken token) + { + while (DateTime.Now <= deadline && !token.IsCancellationRequested) + { + await RunEncounterInput(token).ConfigureAwait(false); + + var result = await LookupSlot(token).ConfigureAwait(false); + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return result; + + if (result == EncounterResult.GenesectFound) + { + Log("Genesect found in B1S1, rebooting the game..."); + return result; + } + } + + return EncounterResult.Timeout; + } + + private async Task RunEncounterInput(CancellationToken token) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + } + + private async Task LookupSlot(CancellationToken token) + { + var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + + if (pa9.Species > 0 && pa9.Species != GenesectSpecies) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Genesect' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == GenesectSpecies && pa9 is { Valid: true, EncryptionConstant: > 0 }) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + Log("Your Pokemon has been received and placed in B1S1. Auto-save will do the rest!"); + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == GenesectSpecies + ? EncounterResult.GenesectFound + : EncounterResult.NextSlot; + } + + private async Task RepeatClick(SwitchButton button, int durationMs, int delayMs, CancellationToken token) + { + var endUtc = DateTime.UtcNow.AddMilliseconds(durationMs); + + while (DateTime.UtcNow < endUtc && !token.IsCancellationRequested) + await Click(button, delayMs, token).ConfigureAwait(false); + } + + private enum EncounterResult + { + InvalidSpecies, + ResultFound, + GenesectFound, + NextSlot, + Timeout + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs new file mode 100644 index 0000000..4a5585a --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs @@ -0,0 +1,68 @@ +namespace SysBot.Pokemon; + +using System.Threading; +using System.Threading.Tasks; +using PKHeX.Core; +using static Base.SwitchButton; + +public class EncounterBotHoopaLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private readonly ushort _hoopa = (ushort)Species.Hoopa; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var dialogueCancellationTokenSource = new CancellationTokenSource(); + _ = DialogueWalking(dialogueCancellationTokenSource.Token, token).ConfigureAwait(false); + + PA9? pa9 = null; + while (pa9 == null || pa9.Species == 0 || !pa9.Valid || pa9.EncryptionConstant == 0) + { + (pa9, var raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + if (pa9.Species > 0 && pa9.Species != _hoopa) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Hoopa' are expected"); + return; + } + + if (pa9.Species == _hoopa) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log("Your Pokémon has been received and placed in B1S1. Auto-save will do the rest!"); + } + + if (stop) + return; + } + + await Task.Delay(0_250, token); + } + + await dialogueCancellationTokenSource.CancelAsync(); + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + } + } + + private async Task DialogueWalking(CancellationToken dialogueToken, CancellationToken generalToken) + { + try + { + while (!dialogueToken.IsCancellationRequested && !generalToken.IsCancellationRequested) + { + await Click(A, 0_200, dialogueToken).ConfigureAwait(false); + } + + Log("Stopping dialogue... which is weird!"); + } + catch (TaskCanceledException) + { + Log("Dialogue is canceled"); + } + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs new file mode 100644 index 0000000..a8cf29d --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs @@ -0,0 +1,68 @@ +namespace SysBot.Pokemon; + +using System.Threading; +using System.Threading.Tasks; +using PKHeX.Core; +using static Base.SwitchButton; + +public class EncounterBotMagearnaLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private readonly ushort _magearna = (ushort)Species.Magearna; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var dialogueCancellationTokenSource = new CancellationTokenSource(); + _ = DialogueWalking(dialogueCancellationTokenSource.Token, token).ConfigureAwait(false); + + PA9? pa9 = null; + while (pa9 == null || pa9.Species == 0 || !pa9.Valid || pa9.EncryptionConstant == 0) + { + (pa9, var raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + if (pa9.Species > 0 && pa9.Species != _magearna) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Magearna' are expected"); + return; + } + + if (pa9.Species == _magearna) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log("Your Pokémon has been received and placed in B1S1. Auto-save will do the rest!"); + } + + if (stop) + return; + } + + await Task.Delay(0_250, token); + } + + await dialogueCancellationTokenSource.CancelAsync(); + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + } + } + + private async Task DialogueWalking(CancellationToken dialogueToken, CancellationToken generalToken) + { + try + { + while (!dialogueToken.IsCancellationRequested && !generalToken.IsCancellationRequested) + { + await Click(A, 0_200, dialogueToken).ConfigureAwait(false); + } + + Log("Stopping dialogue... which is weird!"); + } + catch (TaskCanceledException) + { + Log("Dialogue is canceled"); + } + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs new file mode 100644 index 0000000..5870d93 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs @@ -0,0 +1,126 @@ +namespace SysBot.Pokemon; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotMarshadowLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private const ushort MarshadowSpecies = (ushort)Species.Marshadow; + + private const int EncounterTimeoutSeconds = 100; + private const int MashDurationMs = 60_000; + private const int MashDelayMs = 100; + private const int ReopenDelayMs = 10_000; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var deadline = DateTime.Now.AddSeconds(EncounterTimeoutSeconds); + + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Marshadow encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, MashDurationMs, MashDelayMs, token).ConfigureAwait(false); + + Log($"Catching Marshadow, wait till [{deadline}] before we force a game restart", false); + + await SetStick(LEFT, 0, 30_000, 1_000, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + + var result = await TryEncounterUntilDeadline(deadline, token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; + + if (result == EncounterResult.Timeout) + Log("Force restart of the game..."); + + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + await Task.Delay(ReopenDelayMs, token).ConfigureAwait(false); + } + } + + public override async Task HardStop() + { + await ReleaseHold(ZL, 0_500, CancellationToken.None).ConfigureAwait(false); + await base.HardStop().ConfigureAwait(false); + } + + private async Task TryEncounterUntilDeadline(DateTime deadline, CancellationToken token) + { + while (DateTime.Now <= deadline && !token.IsCancellationRequested) + { + await RunEncounterInput(token).ConfigureAwait(false); + + var result = await LookupSlot(token).ConfigureAwait(false); + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return result; + + if (result == EncounterResult.MarshadowFound) + { + Log("Marshadow found in B1S1, rebooting the game..."); + return result; + } + } + + return EncounterResult.Timeout; + } + + private async Task RunEncounterInput(CancellationToken token) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + } + + private async Task LookupSlot(CancellationToken token) + { + var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + + if (pa9.Species > 0 && pa9.Species != MarshadowSpecies) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Marshadow' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == MarshadowSpecies && pa9 is { Valid: true, EncryptionConstant: > 0 }) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + Log("Your Pokemon has been received and placed in B1S1. Auto-save will do the rest!"); + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == MarshadowSpecies + ? EncounterResult.MarshadowFound + : EncounterResult.NextSlot; + } + + private async Task RepeatClick(SwitchButton button, int durationMs, int delayMs, CancellationToken token) + { + var endUtc = DateTime.UtcNow.AddMilliseconds(durationMs); + + while (DateTime.UtcNow < endUtc && !token.IsCancellationRequested) + await Click(button, delayMs, token).ConfigureAwait(false); + } + + private enum EncounterResult + { + InvalidSpecies, + ResultFound, + MarshadowFound, + NextSlot, + Timeout + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs new file mode 100644 index 0000000..d67065a --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs @@ -0,0 +1,126 @@ +namespace SysBot.Pokemon; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotVolcanionLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private const ushort VolcanionSpecies = (ushort)Species.Volcanion; + + private const int EncounterTimeoutSeconds = 145; // 45 + 100 + private const int MashDurationMs = 100_000; + private const int MashDelayMs = 100; + private const int ReopenDelayMs = 10_000; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var deadline = DateTime.Now.AddSeconds(EncounterTimeoutSeconds); + + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Volcanion encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, MashDurationMs, MashDelayMs, token).ConfigureAwait(false); + + Log($"Catching Volcanion, wait till [{deadline}] before we force a game restart", false); + + await SetStick(LEFT, 0, 30_000, 1_000, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + + var result = await TryEncounterUntilDeadline(deadline, token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; + + if (result == EncounterResult.Timeout) + Log("Force restart of the game..."); + + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + await Task.Delay(ReopenDelayMs, token).ConfigureAwait(false); + } + } + + public override async Task HardStop() + { + await ReleaseHold(ZL, 0_500, CancellationToken.None).ConfigureAwait(false); + await base.HardStop().ConfigureAwait(false); + } + + private async Task TryEncounterUntilDeadline(DateTime deadline, CancellationToken token) + { + while (DateTime.Now <= deadline && !token.IsCancellationRequested) + { + await RunEncounterInput(token).ConfigureAwait(false); + + var result = await LookupSlot(token).ConfigureAwait(false); + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return result; + + if (result == EncounterResult.VolcanionFound) + { + Log("Volcanion found in B1S1, rebooting the game..."); + return result; + } + } + + return EncounterResult.Timeout; + } + + private async Task RunEncounterInput(CancellationToken token) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + } + + private async Task LookupSlot(CancellationToken token) + { + var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + + if (pa9.Species > 0 && pa9.Species != VolcanionSpecies) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Volcanion' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == VolcanionSpecies && pa9 is { Valid: true, EncryptionConstant: > 0 }) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + Log("Your Pokemon has been received and placed in B1S1. Auto-save will do the rest!"); + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == VolcanionSpecies + ? EncounterResult.VolcanionFound + : EncounterResult.NextSlot; + } + + private async Task RepeatClick(SwitchButton button, int durationMs, int delayMs, CancellationToken token) + { + var endUtc = DateTime.UtcNow.AddMilliseconds(durationMs); + + while (DateTime.UtcNow < endUtc && !token.IsCancellationRequested) + await Click(button, delayMs, token).ConfigureAwait(false); + } + + private enum EncounterResult + { + InvalidSpecies, + ResultFound, + VolcanionFound, + NextSlot, + Timeout + } +} diff --git a/SysBot.Pokemon/LZA/BotFactory9LZA.cs b/SysBot.Pokemon/LZA/BotFactory9LZA.cs index fa8a03e..bdf22a2 100644 --- a/SysBot.Pokemon/LZA/BotFactory9LZA.cs +++ b/SysBot.Pokemon/LZA/BotFactory9LZA.cs @@ -5,22 +5,33 @@ namespace SysBot.Pokemon.ZA; public sealed class BotFactory9LZA : BotFactory { - public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBotState cfg) => cfg.NextRoutineType switch - { - PokeRoutineType.EncounterDiancie => new EncounterBotDiancieLZA(cfg, hub), - PokeRoutineType.EncounterFloette => new EncounterBotFloetteLZA(cfg, hub), - PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), - PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), + public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBotState cfg) => + cfg.NextRoutineType switch + { + PokeRoutineType.EncounterDiancie => new EncounterBotDiancieLZA(cfg, hub), + PokeRoutineType.EncounterFloette => new EncounterBotFloetteLZA(cfg, hub), + PokeRoutineType.EncounterVolcanion => new EncounterBotVolcanionLZA(cfg, hub), + PokeRoutineType.EncounterGenesect => new EncounterBotGenesectLZA(cfg, hub), + PokeRoutineType.EncounterHoopa => new EncounterBotHoopaLZA(cfg, hub), + PokeRoutineType.EncounterMagearna => new EncounterBotMagearnaLZA(cfg, hub), + PokeRoutineType.EncounterMarshadow => new EncounterBotMarshadowLZA(cfg, hub), + PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), + PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), - PokeRoutineType.RemoteControl => new RemoteControlBotLZA(cfg), + PokeRoutineType.RemoteControl => new RemoteControlBotLZA(cfg), - _ => throw new ArgumentException(nameof(cfg.NextRoutineType)), - }; + _ => throw new ArgumentException(nameof(cfg.NextRoutineType)), + }; public override bool SupportsRoutine(PokeRoutineType type) => type switch { PokeRoutineType.EncounterDiancie => true, PokeRoutineType.EncounterFloette => true, + PokeRoutineType.EncounterVolcanion => true, + PokeRoutineType.EncounterGenesect => true, + PokeRoutineType.EncounterHoopa => true, + PokeRoutineType.EncounterMagearna => true, + PokeRoutineType.EncounterMarshadow => true, PokeRoutineType.EncounterOverworld => true, PokeRoutineType.FossilBot => true,