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)) { }