From e7cdced4145167cf84b2f21af6b5d4bd5271666b Mon Sep 17 00:00:00 2001 From: Kurt Date: Fri, 15 Apr 2022 15:41:06 -0700 Subject: [PATCH] Differentiate Aggressive & Beta(Skittish) battle start paths (#8) * Beta path options Suppose you have the following spawns: aabb Where, a = aggressive, b = beta (skittish). The next 2 spawns will be beta pokemon. This step will only dismiss 2 pokemon. By ridding of aa (not ab), the next state is bbbb, resulting in a potential 4 new entities the following step. Ridding of ab or bb will result in abbb and aabb, neither of which can be 4x advanced the next step. If the next spawn is aa instead of bb, we get aaab or aaaa if we rid bb or ab, still both separate states. Both with different possibilities. Mixed behaviors is more heavily skewed towards having bbbb, b stuff will be dominant. Lots of overlap, but there are potential end states that are only reachable by trying every single action. Co-authored-by: Lusamine <30205550+Lusamine@users.noreply.github.com> --- PermuteMMO.ConsoleApp/Program.cs | 4 +- PermuteMMO.Lib/ConsolePermuter.cs | 17 ++--- PermuteMMO.Lib/Generation/EntityResult.cs | 3 +- .../Generation/SaveFileParameter.cs | 58 ++++++++++++++ PermuteMMO.Lib/Generation/SpawnGenerator.cs | 43 +---------- PermuteMMO.Lib/Permutation/Advance.cs | 75 +++++++++---------- PermuteMMO.Lib/Permutation/PermuteMeta.cs | 12 ++- PermuteMMO.Lib/Permutation/PermuteResult.cs | 50 ++++--------- PermuteMMO.Lib/Permuter.cs | 57 ++++++++++---- PermuteMMO.Lib/SpawnState.cs | 64 +++++++++++----- PermuteMMO.Lib/Util/BehaviorUtil.cs | 2 + PermuteMMO.Lib/Util/SpawnInfo.cs | 10 +++ 12 files changed, 230 insertions(+), 165 deletions(-) create mode 100644 PermuteMMO.Lib/Generation/SaveFileParameter.cs diff --git a/PermuteMMO.ConsoleApp/Program.cs b/PermuteMMO.ConsoleApp/Program.cs index bfd4920..6224fd9 100644 --- a/PermuteMMO.ConsoleApp/Program.cs +++ b/PermuteMMO.ConsoleApp/Program.cs @@ -30,13 +30,13 @@ if (File.Exists(file)) data_mo = File.ReadAllBytes(file_mo); else - data_mo = SpawnGenerator.SaveFile.Accessor.GetBlock(0x1E0F1BA3).Data; + data_mo = SaveFileParameter.GetMassOutbreakData(); const string file_mmo = "mmo.bin"; if (File.Exists(file_mmo)) data_mmo = File.ReadAllBytes(file_mmo); else - data_mmo = SpawnGenerator.SaveFile.Accessor.GetBlock(0x7799EB86).Data; + data_mmo = SaveFileParameter.GetMassiveMassOutbreakData(); } // Compute and print. diff --git a/PermuteMMO.Lib/ConsolePermuter.cs b/PermuteMMO.Lib/ConsolePermuter.cs index 4e29d3b..d63e709 100644 --- a/PermuteMMO.Lib/ConsolePermuter.cs +++ b/PermuteMMO.Lib/ConsolePermuter.cs @@ -50,11 +50,9 @@ public static void PermuteMassiveMassOutbreak(Span data) } Console.WriteLine($"Spawner {j+1} at ({spawner.X:F1}, {spawner.Y:F1}, {spawner.Z}) shows {SpeciesName.GetSpeciesName(spawner.DisplaySpecies, 2)}"); - Console.WriteLine($"Parameters: {spawn}"); + Console.WriteLine(spawn.GetSummary("Parameters: ")); Console.WriteLine($"Seed: {seed}"); - bool skittishBase = SpawnGenerator.IsSkittish(spawn.Set.Table); - bool skittishBonus = spawn.GetNextWave(out var next) && SpawnGenerator.IsSkittish(next.Set.Table); - var lines = result.GetLines(skittishBase, skittishBonus); + var lines = result.GetLines(); foreach (var line in lines) Console.WriteLine(line); Console.WriteLine(); @@ -102,10 +100,9 @@ public static void PermuteBlockMassOutbreak(Span data) Console.WriteLine($"Found paths for {(Species)spawner.DisplaySpecies} Mass Outbreak in {areaName}:"); Console.WriteLine("=========="); Console.WriteLine($"Spawner at ({spawner.X:F1}, {spawner.Y:F1}, {spawner.Z}) shows {SpeciesName.GetSpeciesName(spawner.DisplaySpecies, 2)}"); - Console.WriteLine($"Parameters: {spawn}"); + Console.WriteLine(spawn.GetSummary("Parameters: ")); Console.WriteLine($"Seed: {seed}"); - bool skittishBase = SpawnGenerator.IsSkittish(spawner.DisplaySpecies); - var lines = result.GetLines(skittishBase); + var lines = result.GetLines(); foreach (var line in lines) Console.WriteLine(line); Console.WriteLine(); @@ -121,7 +118,7 @@ public static void PermuteSingle(SpawnInfo spawn, ulong seed, ushort species) { Console.WriteLine($"Permuting all possible paths for {seed:X16}."); Console.WriteLine($"Base Species: {SpeciesName.GetSpeciesName(species, 2)}"); - Console.WriteLine($"Parameters: {spawn}"); + Console.WriteLine(spawn.GetSummary("Parameters: ")); Console.WriteLine($"Seed: {seed}"); var result = Permuter.Permute(spawn, seed); @@ -131,9 +128,7 @@ public static void PermuteSingle(SpawnInfo spawn, ulong seed, ushort species) } else { - bool skittishBase = SpawnGenerator.IsSkittish(spawn.Set.Table); - bool skittishBonus = spawn.GetNextWave(out var next) && SpawnGenerator.IsSkittish(next.Set.Table); - var lines = result.GetLines(skittishBase, skittishBonus); + var lines = result.GetLines(); foreach (var line in lines) Console.WriteLine(line); } diff --git a/PermuteMMO.Lib/Generation/EntityResult.cs b/PermuteMMO.Lib/Generation/EntityResult.cs index ae99ed1..33f5a01 100644 --- a/PermuteMMO.Lib/Generation/EntityResult.cs +++ b/PermuteMMO.Lib/Generation/EntityResult.cs @@ -31,8 +31,9 @@ public sealed class EntityResult public byte Height { get; set; } public byte Weight { get; set; } + public bool IsOblivious => BehaviorUtil.Oblivious.Contains(Species); public bool IsSkittish => BehaviorUtil.Skittish.Contains(Species); - public bool IsAggressive => IsAlpha || !IsSkittish; + public bool IsAggressive => IsAlpha || !(IsSkittish || IsOblivious); public string GetSummary() { diff --git a/PermuteMMO.Lib/Generation/SaveFileParameter.cs b/PermuteMMO.Lib/Generation/SaveFileParameter.cs new file mode 100644 index 0000000..42edd6f --- /dev/null +++ b/PermuteMMO.Lib/Generation/SaveFileParameter.cs @@ -0,0 +1,58 @@ +using PKHeX.Core; + +namespace PermuteMMO.Lib; + +/// +/// Fetches environment specific values necessary for spawn generation. +/// +public static class SaveFileParameter +{ + #region Public Mutable - Useful for DLL consumers + + public static SAV8LA SaveFile { get; set; } = GetFake(); + public static PokedexSave8a Pokedex => SaveFile.PokedexSave; + public static byte[] BackingArray => SaveFile.Blocks.GetBlock(0x02168706).Data; + public static bool HasCharm { get; set; } = true; + public static bool UseSaveFileShinyRolls { get; set; } + + public static byte[] GetMassOutbreakData() => SaveFile.GetMassOutbreakData(); + public static byte[] GetMassiveMassOutbreakData() => SaveFile.GetMassiveMassOutbreakData(); + + public static byte[] GetMassOutbreakData(this SAV8LA sav) => sav.Accessor.GetBlock(0x1E0F1BA3).Data; + public static byte[] GetMassiveMassOutbreakData(this SAV8LA sav) => sav.Accessor.GetBlock(0x7799EB86).Data; + + #endregion + + private static SAV8LA GetFake() + { + var mainPath = AppDomain.CurrentDomain.BaseDirectory; + mainPath = Path.Combine(mainPath, "main"); + if (File.Exists(mainPath)) + return GetFromFile(mainPath); + return new SAV8LA(); + } + + private static SAV8LA GetFromFile(string mainPath) + { + var data = File.ReadAllBytes(mainPath); + var sav = new SAV8LA(data); + UseSaveFileShinyRolls = true; + HasCharm = sav.Inventory.Any(z => z.Items.Any(i => i.Index == 632 && i.Count is not 0)); + return sav; + } + + /// + /// Gets the count of shiny rolls the player is permitted to have when rolling an . + /// + /// Encounter species + /// Encounter Spawn type + /// [1,X] iteration of PID rolls permitted + public static int GetRerollCount(in int species, SpawnType type) + { + if (!UseSaveFileShinyRolls) + return (int)type; + bool perfect = Pokedex.IsPerfect(species); + bool complete = Pokedex.IsComplete(species); + return 1 + (complete ? 1 : 0) + (perfect ? 2 : 0) + (HasCharm ? 3 : 0) + (int)(type - 7); + } +} diff --git a/PermuteMMO.Lib/Generation/SpawnGenerator.cs b/PermuteMMO.Lib/Generation/SpawnGenerator.cs index 49c16b7..d41ce33 100644 --- a/PermuteMMO.Lib/Generation/SpawnGenerator.cs +++ b/PermuteMMO.Lib/Generation/SpawnGenerator.cs @@ -11,38 +11,6 @@ public static class SpawnGenerator { public static readonly IReadOnlyDictionary EncounterTables = JsonDecoder.GetDictionary(Resources.mmo_es); - #region Public Mutable - Useful for DLL consumers - public static SAV8LA SaveFile { get; set; } = GetFake(); - public static PokedexSave8a Pokedex => SaveFile.PokedexSave; - public static byte[] BackingArray => SaveFile.Blocks.GetBlock(0x02168706).Data; - public static bool HasCharm { get; set; } = true; - public static bool UseSaveFileShinyRolls { get; set; } - #endregion - - private static SAV8LA GetFake() - { - if (File.Exists("main")) - { - var data = File.ReadAllBytes("main"); - var sav = new SAV8LA(data); - UseSaveFileShinyRolls = true; - HasCharm = sav.Inventory.Any(z => z.Items.Any(i => i.Index == 632 && i.Count is not 0)); - return sav; - } - return new SAV8LA(); - } - - /// - /// Checks if a Table (or species) is skittish. - /// - /// Table hash or species ID. - /// True if skittish. - public static bool IsSkittish(ulong table) - { - var slots = GetSlots(table); - return slots.Any(z => z.IsSkittish); - } - /// /// Generates an from the input and . /// @@ -61,7 +29,7 @@ public static EntityResult Generate(in ulong seed, in ulong table, SpawnType typ var gt = PersonalTable.LA.GetFormEntry(slot.Species, slot.Form).Gender; // Get roll count from save file - int shinyRolls = GetRerollCount(slot.Species, type); + int shinyRolls = SaveFileParameter.GetRerollCount(slot.Species, type); var result = new EntityResult { @@ -109,15 +77,6 @@ private static SlotDetail[] GetFakeOutbreak(ushort species) return Outbreaks[species] = value; } - private static int GetRerollCount(in int species, SpawnType type) - { - if (!UseSaveFileShinyRolls) - return (int)type; - bool perfect = Pokedex.IsPerfect(species); - bool complete = Pokedex.IsComplete(species); - return 1 + (complete ? 1 : 0) + (perfect ? 2 : 0) + (HasCharm ? 3 : 0) + (int)type; - } - private static int GetLevel(SlotDetail slot, Xoroshiro128Plus slotrng) { var min = slot.LevelMin; diff --git a/PermuteMMO.Lib/Permutation/Advance.cs b/PermuteMMO.Lib/Permutation/Advance.cs index 5276735..7568e3b 100644 --- a/PermuteMMO.Lib/Permutation/Advance.cs +++ b/PermuteMMO.Lib/Permutation/Advance.cs @@ -9,20 +9,16 @@ public enum Advance : byte { CR, - A1, - A2, - A3, - A4, - - G1, - G2, - G3, - // G4 is equivalent to CR - - // S1 is equivalent to A1 - S2, - S3, - S4, + A1, A2, A3, A4, // Aggressive + B1, B2, B3, B4, // Beta + + O1, // Oblivious + + // S1 is equivalent to B1 + S2, S3, S4, + + // G4 is equivalent to CR + G1, G2, G3, } public static class AdvanceExtensions @@ -42,10 +38,17 @@ public static class AdvanceExtensions { CR => "Clear Remaining", - A1 => "De-spawn 1", - A2 => "Battle 2", - A3 => "Battle 3", - A4 => "Battle 4", + A1 => "1 Aggressive", + A2 => "2 Aggressive", + A3 => "3 Aggressive", + A4 => "4 Aggressive", + + B1 => "1 Beta", + B2 => "1 Beta + 1 Aggressive", + B3 => "1 Beta + 2 Aggressive", + B4 => "1 Beta + 3 Aggressive", + + O1 => "1 Oblivious", G1 => "De-spawn 1 + Leave", G2 => "De-spawn 2 + Leave", @@ -62,48 +65,40 @@ public static class AdvanceExtensions /// public static int AdvanceCount(this Advance advance) => advance switch { - A1 or G1 => 1, - A2 or S2 or G2 => 2, - A3 or S3 or G3 => 3, - A4 or S4 => 4, + A1 or B1 or G1 => 1, + A2 or B2 or S2 or G2 => 2, + A3 or B3 or S3 or G3 => 3, + A4 or B4 or S4 => 4, _ => 0, }; /// /// Indicates if a multi-battle is required for this advancement. /// - public static bool IsMulti(this Advance advance) => advance is (A2 or A3 or A4); + public static bool IsMultiAny(this Advance advance) => advance.IsMultiAggressive() || advance.IsMultiBeta() || advance.IsMultiScare(); /// /// Indicates if a multi-battle is required for this advancement. /// - public static bool IsScare(this Advance advance) => advance is (S2 or S3 or S4); + public static bool IsMultiAggressive(this Advance advance) => advance is (A2 or A3 or A4); /// - /// Indicates if any advance requires a multi-battle for advancement. + /// Indicates if a multi-battle is required for this advancement. /// - public static bool IsAnyMulti(this ReadOnlySpan advances) - { - foreach (var adv in advances) - { - if (adv.IsMulti()) - return true; - } - - return false; - } + public static bool IsMultiScare(this Advance advance) => advance is (S2 or S3 or S4); /// - /// Indicates if any advance requires a multi-scare for advancement. + /// Indicates if a multi-battle is required for this advancement. /// - public static bool IsAnyMultiScare(this ReadOnlySpan advances) + public static bool IsMultiBeta(this Advance advance) => advance is (B2 or B3 or B4); + + public static bool IsAny(this ReadOnlySpan span, Func check) { - foreach (var adv in advances) + foreach (var x in span) { - if (adv.IsScare()) + if (check(x)) return true; } - return false; } } diff --git a/PermuteMMO.Lib/Permutation/PermuteMeta.cs b/PermuteMMO.Lib/Permutation/PermuteMeta.cs index 03a39c7..fa17cec 100644 --- a/PermuteMMO.Lib/Permutation/PermuteMeta.cs +++ b/PermuteMMO.Lib/Permutation/PermuteMeta.cs @@ -52,17 +52,25 @@ public void AddResult(EntityResult entity, in int index) /// /// Calls for all objects in the result list. /// - public IEnumerable GetLines(bool skittishBase, bool skittishBonus = false) + public IEnumerable GetLines() { for (var i = 0; i < Results.Count; i++) { var result = Results[i]; var parent = FindNearestParentAdvanceResult(i, result.Advances); bool isActionMultiResult = IsActionMultiResult(i, result.Advances); - yield return result.GetLine(parent, isActionMultiResult, skittishBase, skittishBonus); + bool hasChildChain = HasChildChain(i, result.Advances); + yield return result.GetLine(parent, isActionMultiResult, hasChildChain); } } + private bool HasChildChain(int index, Advance[] parent) + { + if (++index >= Results.Count) + return false; + return IsSubset(parent, Results[index].Advances); + } + private bool IsActionMultiResult(int index, Advance[] child) { int count = 0; diff --git a/PermuteMMO.Lib/Permutation/PermuteResult.cs b/PermuteMMO.Lib/Permutation/PermuteResult.cs index 61085d9..a657604 100644 --- a/PermuteMMO.Lib/Permutation/PermuteResult.cs +++ b/PermuteMMO.Lib/Permutation/PermuteResult.cs @@ -8,14 +8,14 @@ public sealed record PermuteResult(Advance[] Advances, EntityResult Entity, in i private bool IsBonus => Array.IndexOf(Advances, Advance.CR) != -1; private int WaveIndex => Advances.Count(adv => adv == Advance.CR); - public string GetLine(PermuteResult? prev, bool isActionMultiResult, bool skittishBase, bool skittishBonus) + public string GetLine(PermuteResult? prev, bool isActionMultiResult, bool hasChildChain) { var steps = GetSteps(prev); - var feasibility = GetFeasibility(Advances, skittishBase, skittishBonus); + var feasibility = GetFeasibility(Advances); // 37 total characters for the steps: // 10+7 spawner has 6+(3)+3=12 max permutations, +"CR|", remove last |; (3*12+2)=37. var line = $"* {steps,-37} >>> {GetWaveIndicator()}Spawn{SpawnIndex} = {Entity.GetSummary()}{feasibility}"; - if (prev != null) + if (prev != null || hasChildChain) line += " ~~ Chain result!"; if (isActionMultiResult) line += " ~~ Spawns multiple results!"; @@ -42,45 +42,27 @@ public string GetSteps(PermuteResult? prev = null) return string.Concat(Enumerable.Repeat("-> ", (prevSeq.Length+2)/3)) + steps[(prevSeq.Length + 1)..]; } - private static string GetFeasibility(ReadOnlySpan advances, bool skittishBase, bool skittishBonus) + private static string GetFeasibility(ReadOnlySpan advances) { - if (!advances.IsAnyMulti() && !advances.IsAnyMultiScare()) - return " -- Single advances!"; - - if (!skittishBase && !skittishBonus) - return string.Empty; - - bool skittishMulti = false; - int bonusIndex = GetNextWaveStartIndex(advances); - if (bonusIndex != -1) - { - skittishMulti |= skittishBase && advances[..bonusIndex].IsAnyMulti(); - skittishMulti |= skittishBonus && advances[bonusIndex..].IsAnyMulti(); - } - else - { - skittishMulti |= skittishBase && advances.IsAnyMulti(); - } - - if (advances.IsAnyMultiScare()) + if (advances.IsAny(AdvanceExtensions.IsMultiScare)) { - if (skittishMulti) + if (advances.IsAny(AdvanceExtensions.IsMultiBeta)) return " -- Skittish: Multi scaring with aggressive!"; return " -- Skittish: Multi scaring!"; } - - if (skittishMulti) + if (advances.IsAny(AdvanceExtensions.IsMultiBeta)) return " -- Skittish: Aggressive!"; - return " -- Skittish: Single advances!"; - } - private static int GetNextWaveStartIndex(ReadOnlySpan advances) - { - for (int i = 0; i < advances.Length; i++) + if (advances.IsAny(z => z == Advance.B1)) { - if (advances[i] == Advance.CR) - return i; + if (!advances.IsAny(AdvanceExtensions.IsMultiAggressive)) + return " -- Skittish: Single advances!"; + return " -- Skittish: Mostly aggressive!"; } - return -1; + + if (advances.IsAny(AdvanceExtensions.IsMultiAggressive)) + return string.Empty; + + return " -- Single advances!"; } } diff --git a/PermuteMMO.Lib/Permuter.cs b/PermuteMMO.Lib/Permuter.cs index be2f037..8024dfa 100644 --- a/PermuteMMO.Lib/Permuter.cs +++ b/PermuteMMO.Lib/Permuter.cs @@ -47,10 +47,10 @@ private static void PermuteOutbreak(PermuteMeta meta, in ulong table, in ulong s { // Re-spawn to capacity var (empty, respawn, ghosts) = state.GetRespawnInfo(); - var (reseed, aggro) = GenerateSpawns(meta, table, seed, empty, ghosts); + var (reseed, aggro, beta, oblivious) = GenerateSpawns(meta, table, seed, empty, ghosts); // Update spawn state - var newState = state.Generate(respawn, aggro); + var newState = state.Generate(respawn, aggro, beta, oblivious); ContinuePermute(meta, table, reseed, newState); } @@ -63,18 +63,45 @@ private static void ContinuePermute(PermuteMeta meta, in ulong table, in ulong s return; } - // Permute our remaining options - for (int i = 1; i <= state.MaxCountBattle; i++) + // Depending on what spawns in future calls, the actions we take here can impact the options for future recursion. + // We need to try out every single potential action the player can do, and target removals for specific behaviors. + + // De-spawn: Aggressive Only + if (state.AliveAggressive != 0) { - var step = (int)Advance.A1 + (i - 1); - meta.Start((Advance)step); - var newState = state.Knockout(i); + for (int i = 1; i <= state.AliveAggressive; i++) + { + var step = (int)Advance.A1 + (i - 1); + meta.Start((Advance)step); + var newState = state.KnockoutAggressive(i); + PermuteRecursion(meta, table, seed, newState); + meta.End(); + } + } + + if (state.AliveOblivious != 0) + { + meta.Start(Advance.O1); + var newState = state.KnockoutOblivious(); PermuteRecursion(meta, table, seed, newState); meta.End(); } - // If we can scare multiple, try this route too - for (int i = 2; i <= state.MaxCountScare; i++) + // De-spawn: Single beta with aggressive(s) / none. + if (state.AliveBeta != 0) + { + for (int i = 0; i <= state.AliveAggressive; i++) + { + var step = (int)Advance.B1 + i; + meta.Start((Advance)step); + var newState = state.KnockoutBeta(i + 1); + PermuteRecursion(meta, table, seed, newState); + meta.End(); + } + } + + // De-spawn: Multiple betas (Scaring) + for (int i = 2; i <= state.AliveBeta; i++) { var step = (int)Advance.S2 + (i - 2); meta.Start((Advance)step); @@ -84,9 +111,11 @@ private static void ContinuePermute(PermuteMeta meta, in ulong table, in ulong s } } - private static (ulong Seed, int Aggressive) GenerateSpawns(PermuteMeta meta, in ulong table, in ulong seed, int count, in int ghosts) + private static (ulong Seed, int Aggressive, int Skittish, int Oblivious) GenerateSpawns(PermuteMeta meta, in ulong table, in ulong seed, int count, in int ghosts) { int aggressive = 0; + int beta = 0; + int oblivious = 0; var rng = new Xoroshiro128Plus(seed); for (int i = 1; i <= count; i++) { @@ -100,11 +129,13 @@ private static (ulong Seed, int Aggressive) GenerateSpawns(PermuteMeta meta, in if (meta.IsResult(generate)) meta.AddResult(generate, i); - if (generate.IsAggressive) - aggressive++; + if (generate.IsAlpha) aggressive++; + else if (generate.IsSkittish) beta++; + else if (generate.IsOblivious) oblivious++; + else aggressive++; } var result = rng.Next(); // Reset the seed for future spawns. - return (result, aggressive); + return (result, aggressive, beta, oblivious); } private static void PermuteNextTable(PermuteMeta meta, SpawnInfo next, in ulong seed) diff --git a/PermuteMMO.Lib/SpawnState.cs b/PermuteMMO.Lib/SpawnState.cs index 34bc4e8..4e251dd 100644 --- a/PermuteMMO.Lib/SpawnState.cs +++ b/PermuteMMO.Lib/SpawnState.cs @@ -7,10 +7,11 @@ namespace PermuteMMO.Lib; /// /// Total count of entities that can be spawned by the spawner. /// Maximum count of entities that can be alive at a given time. -/// Current count of entities alive. /// Current count of fake entities. /// Current count of aggressive entities alive. -public readonly record struct SpawnState(in int Count, in int MaxAlive, in int Alive = 0, in int Ghost = 0, in int AliveAggressive = 0) +/// Current count of timid entities alive. +/// Current count of oblivious entities alive. +public readonly record struct SpawnState(in int Count, in int MaxAlive, in int Ghost = 0, in int AliveAggressive = 0, in int AliveBeta = 0, in int AliveOblivious = 0) { /// Current count of unpopulated entities. public int Dead { get; init; } = MaxAlive; @@ -18,14 +19,6 @@ public readonly record struct SpawnState(in int Count, in int MaxAlive, in int A /// Total count of entities that can exist as ghosts. /// Completely filling with ghost slots will start the next wave rather than add ghosts. private int MaxGhosts => MaxAlive - 1; - /// Current count of timid entities alive. - public int AliveTimid => Alive - AliveAggressive; - - /// Maximum count of entities that can be battled in the current state. - public int MaxCountBattle => Math.Min(Alive, AliveAggressive + 1); - - /// Maximum count of entities that can be scared in the current state. - public int MaxCountScare => Math.Min(Alive, AliveTimid); /// Indicates if ghost entities can be added to the spawner. /// Only call this if is zero. @@ -39,36 +32,62 @@ public readonly record struct SpawnState(in int Count, in int MaxAlive, in int A /// Returns a spawner state after knocking out existing entities. /// /// - /// If is 1, this is the same as capturing a single Entity out of battle. + /// If is 1, this is the same as capturing a single Aggressive Entity out of battle. + /// + public SpawnState KnockoutAggressive(in int count) + { + // Knock out required Aggressive + var newAggro = AliveAggressive - count; + Debug.Assert(newAggro >= 0); + return this with { Dead = Dead + count, AliveAggressive = newAggro }; + } + + /// + /// Returns a spawner state after knocking out existing entities. + /// + /// + /// If is 1, this is the same as capturing a single Beta Entity out of battle. /// - public SpawnState Knockout(in int count) + public SpawnState KnockoutBeta(in int count) { - // Prefer to knock out the Skittish, and any required Aggressives + // Prefer to knock out the Skittish, and any required Aggressive var newAggro = AliveAggressive - count + 1; Debug.Assert(newAggro >= 0); - return this with { Alive = Alive - count, Dead = Dead + count, AliveAggressive = newAggro }; + return this with { Dead = Dead + count, AliveAggressive = newAggro, AliveBeta = AliveBeta - 1 }; } /// - /// Returns a spawner state after scaring existing entities away. + /// Returns a spawner state after knocking out existing entities. + /// + public SpawnState KnockoutOblivious() + { + // Knock out required Aggressive + var newOblivious = AliveOblivious - 1; + Debug.Assert(newOblivious >= 0); + return this with { Dead = Dead + 1, AliveOblivious = newOblivious }; + } + + /// + /// Returns a spawner state after scaring existing Beta entities away. /// public SpawnState Scare(in int count) { // Can only scare Skittish - Debug.Assert(AliveTimid >= count); - return this with { Alive = Alive - count, Dead = Dead + count }; + Debug.Assert(AliveBeta >= count); + return this with { AliveBeta = AliveBeta - count, Dead = Dead + count }; } /// /// Returns a spawner state after generating new entities. /// - public SpawnState Generate(in int count, in int aggro) => this with + public SpawnState Generate(in int count, in int aggro, in int beta, in int oblivious) => this with { Count = Count - count, - Alive = Alive + count, Dead = Dead - count, Ghost = Dead - count, AliveAggressive = AliveAggressive + aggro, + AliveBeta = AliveBeta + beta, + AliveOblivious = AliveOblivious + oblivious, }; /// @@ -76,7 +95,12 @@ public SpawnState Generate(in int count, in int aggro) => this with /// public SpawnState AddGhosts(in int count) => this with { - Alive = Alive - count, + // These are no longer important, don't bother choosing which to decrement. + // We only check Ghost count going forward. + AliveAggressive = 0, + AliveOblivious = 0, + AliveBeta = 0, + Dead = Dead + count, Ghost = Ghost + count, }; diff --git a/PermuteMMO.Lib/Util/BehaviorUtil.cs b/PermuteMMO.Lib/Util/BehaviorUtil.cs index a3ddfce..37544ed 100644 --- a/PermuteMMO.Lib/Util/BehaviorUtil.cs +++ b/PermuteMMO.Lib/Util/BehaviorUtil.cs @@ -4,6 +4,8 @@ namespace PermuteMMO.Lib; public static class BehaviorUtil { + public static readonly HashSet Oblivious = new() { (ushort)MrMime }; + public static readonly HashSet Skittish = new() { (ushort)Abra, diff --git a/PermuteMMO.Lib/Util/SpawnInfo.cs b/PermuteMMO.Lib/Util/SpawnInfo.cs index 6f25d19..cba5993 100644 --- a/PermuteMMO.Lib/Util/SpawnInfo.cs +++ b/PermuteMMO.Lib/Util/SpawnInfo.cs @@ -12,6 +12,16 @@ public sealed record SpawnInfo(SpawnDetail Detail, SpawnSet Set, SpawnInfo? Next private SpawnInfo? Next { get; set; } = Next; + public string GetSummary(string prefix) + { + var summary = $"{prefix}{this}"; + if (Next is not { } x) + return summary; + if (ReferenceEquals(this, x)) + return summary + " REPEATING."; + return summary + Environment.NewLine + x.GetSummary(prefix); + } + public bool GetNextWave([NotNullWhen(true)] out SpawnInfo? next) => (next = Next) != null; public SpawnInfo(MassiveOutbreakSpawner8a spawner) : this(MMO, new SpawnSet(spawner.BaseTable, spawner.BaseCount), GetBonusChain(spawner)) { }