using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Storage; /// /// Dictates a list of items that can be spawned. /// [Serializable] [DataDefinition] public struct EntitySpawnEntry : IPopulateDefaultValues { [ViewVariables(VVAccess.ReadWrite)] [DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer))] public string? PrototypeId; /// /// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc. /// [ViewVariables(VVAccess.ReadWrite)] [DataField("prob")] public float SpawnProbability; /// /// orGroup signifies to pick between entities designated with an ID. /// /// /// To define an orGroup in a StorageFill component you /// need to add it to the entities you want to choose between and /// add a prob field. In this example there is a 50% chance the storage /// spawns with Y or Z. /// /// /// - type: StorageFill /// contents: /// - name: X /// - name: Y /// prob: 0.50 /// orGroup: YOrZ /// - name: Z /// orGroup: YOrZ /// /// /// [ViewVariables(VVAccess.ReadWrite)] [DataField("orGroup")] public string? GroupId; [ViewVariables(VVAccess.ReadWrite)] [DataField("amount")] public int Amount; /// /// How many of this can be spawned, in total. /// If this is lesser or equal to , it will spawn exactly. /// Otherwise, it chooses a random value between and on spawn. /// [ViewVariables(VVAccess.ReadWrite)] [DataField("maxAmount")] public int MaxAmount; public void PopulateDefaultValues() { Amount = 1; MaxAmount = 1; SpawnProbability = 1; } } public static class EntitySpawnCollection { private sealed class OrGroup { public List Entries { get; set; } = new(); public float CumulativeProbability { get; set; } = 0f; } /// /// Using a collection of entity spawn entries, picks a random list of entity prototypes to spawn from that collection. /// /// /// This does not spawn the entities. The caller is responsible for doing so, since it may want to do something /// special to those entities (offset them, insert them into storage, etc) /// /// The entity spawn entries. /// Resolve param. /// A list of entity prototypes that should be spawned. public static List GetSpawns(IEnumerable entries, IRobustRandom? random = null) { IoCManager.Resolve(ref random); var spawned = new List(); var orGroupedSpawns = new Dictionary(); // collect groups together, create singular items that pass probability foreach (var entry in entries) { // Handle "Or" groups if (!string.IsNullOrEmpty(entry.GroupId)) { if (!orGroupedSpawns.TryGetValue(entry.GroupId, out OrGroup? orGroup)) { orGroup = new(); orGroupedSpawns.Add(entry.GroupId, orGroup); } orGroup.Entries.Add(entry); orGroup.CumulativeProbability += entry.SpawnProbability; continue; } // else // Check random spawn // ReSharper disable once CompareOfFloatsByEqualityOperator if (entry.SpawnProbability != 1f && !random.Prob(entry.SpawnProbability)) continue; var amount = entry.Amount; if (entry.MaxAmount > amount) amount = random.Next(amount, entry.MaxAmount); for (var i = 0; i < amount; i++) { spawned.Add(entry.PrototypeId); } } // handle orgroup spawns foreach (var spawnValue in orGroupedSpawns.Values) { // For each group use the added cumulative probability to roll a double in that range double diceRoll = random.NextDouble() * spawnValue.CumulativeProbability; // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item. var cumulative = 0.0; foreach (var entry in spawnValue.Entries) { cumulative += entry.SpawnProbability; if (diceRoll > cumulative) continue; // Dice roll succeeded, add item and break loop var amount = entry.Amount; if (entry.MaxAmount > amount) amount = random.Next(amount, entry.MaxAmount); for (var index = 0; index < amount; index++) { spawned.Add(entry.PrototypeId); } break; } } return spawned; } }