// HudSpawner.cs // A spawner HUD. Players click the Open Button object (a cube in the scene) to get it: a button per item, a How Many // dropdown, a Cooldown slider and a bar that fills while they wait. Each player's HUD is their own: what one player // picks or moves never changes anyone else's. // // Set up: slot the cube in Open Button, then fill Items (what to spawn), Icons and Names (entry N of each list goes // with item N; no icon = the name only, no name = "Item N"). Optional: Logo, Background, Title and its colour, Font. using System.Collections; using System.Collections.Generic; using IHXR.Simulation; public class HudSpawner : ScriptBase { [Header("Opening it")] [Tooltip("The object players click to get the spawner (a cube in the scene).")] public SceneObject OpenButton; public string OpenHint = "Open the spawner"; [Header("What it spawns (entry #N of each list belongs together)")] public List Items = new List(); public List Icons = new List(); public List Names = new List(); [Header("Look")] public string Title = "Spawner"; public Color TitleColor = new Color(1f, 0.85f, 0.3f); [Tooltip("A picture centred at the top.")] public TextureItem Logo; [Tooltip("A picture behind everything (fills the HUD).")] public TextureItem Background; public Color BackgroundColor = new Color(0.06f, 0.09f, 0.16f); [Range(0.1f, 1f)] public float BackgroundOpacity = 0.92f; public HudFont Font = HudFont.Rounded; public Color ButtonTextColor = new Color(1f, 1f, 1f); public Color ButtonColor = new Color(0.45f, 0.1f, 0.6f); [Tooltip("The colour of the slider and the cooldown bar.")] public Color AccentColor = new Color(0.3f, 0.75f, 1f); [Range(1, 6)] public int Columns = 3; [Header("Spawning")] [Tooltip("The choices in the How Many dropdown.")] public List AmountChoices = new List { "1", "2", "3", "5", "10" }; [Range(0, 30)] public float DefaultCooldown = 2f; [Range(1, 60)] public float MaxCooldown = 10f; [Tooltip("A player's oldest items go when they would have more than this.")] [Range(1, 100)] public int MaxItemsPerPlayer = 20; [Tooltip("Seconds before a spawned item is removed (0 = it stays).")] [Range(0, 600)] public float RemoveAfterSeconds = 120f; public float SpawnDistance = 1.5f; public float SpawnHeight = 1f; private ScriptHud hud; private HudDropdown amount; private HudSlider cooldown; private HudProgressBar readyBar; private HudItem status; private readonly Dictionary readyAt = new Dictionary(); private readonly Dictionary> spawned = new Dictionary>(); public override void Start() { BuildHud(); if (OpenButton != null && !OpenButton.IsNull) MakeInteractable(OpenButton, click => hud.Show(click.Username), true, null, true, 10f, OpenHint); else Log("HudSpawner: slot the object players click (Open Button) in the Inspector."); } private void BuildHud() { int columns = Mathf.Clamp(Columns, 1, 6); hud = CreateHud("spawner", 340); hud.SetTitle(Title) .SetTitleStyle(TitleColor, 22) .SetBackgroundColor(new Color(BackgroundColor.r, BackgroundColor.g, BackgroundColor.b, BackgroundOpacity)) .SetFont(Font, 14) .SetButtonColors(ButtonTextColor, ButtonColor) .SetAccentColor(AccentColor) .SetLayout(columns, 6, 12); if (Logo != null && !Logo.IsEmpty) hud.SetLogo(Logo, 64); if (Background != null && !Background.IsEmpty) hud.SetBackgroundImage(Background, HudImageFit.Cover); // One button per item. int buttons = 0; for (int i = 0; i < Items.Count; i++) { if (Items[i] == null || Items[i].IsEmpty) continue; int index = i; string name = NameOf(i); TextureItem icon = i < Icons.Count ? Icons[i] : null; HudButton button = icon != null && !icon.IsEmpty ? hud.AddButton(name, icon, e => Spawn(e, index)) : hud.AddButton(name, e => Spawn(e, index)); button.SetTooltip("Spawn " + name); buttons++; } if (buttons == 0) hud.AddLabel("Nothing to spawn yet: add items to the Items list in the Inspector.").SetSpan(columns); hud.AddDivider().SetSpan(columns); amount = hud.AddDropdown("How many", AmountChoices, 0); amount.SetSpan(columns); cooldown = hud.AddSlider("Cooldown (seconds)", 0f, Mathf.Max(1f, MaxCooldown), Mathf.Clamp(DefaultCooldown, 0f, MaxCooldown), null, 0.5f); cooldown.SetSpan(columns); readyBar = hud.AddProgressBar(1f, "Ready"); readyBar.SetSpan(columns); status = hud.AddLabel("Pick something to spawn."); status.SetSpan(columns); status.SetAlign(HudAlign.Center); HudButton clear = hud.AddButton("Remove my items", e => RemoveAll(e.Username, true)); clear.SetSpan(columns); clear.SetColors(new Color(1f, 1f, 1f), new Color(0.55f, 0.1f, 0.1f)); } private string NameOf(int index) { return index < Names.Count && !string.IsNullOrWhiteSpace(Names[index]) ? Names[index].Trim() : "Item " + (index + 1); } // A button: How Many of that item in a row in front of the player, then the player's cooldown starts. private void Spawn(HudEvent e, int index) { string user = e.Username; float now = Time.time; if (readyAt.TryGetValue(Key(user), out float ready) && now < ready) { status.SetText("Wait " + (ready - now).ToString("0.0") + " s", user); return; } UserInfo player = e.User; if (player == null) return; int count = Mathf.Clamp(ParseAmount(amount.GetSelectedText(user)), 1, MaxItemsPerPlayer); List mine = Mine(user); while (mine.Count > 0 && mine.Count + count > MaxItemsPerPlayer) Remove(user, mine[0]); // the oldest go first Vector3 forward = player.Forward; Vector3 right = new Vector3(forward.z, 0f, -forward.x); Vector3 front = player.Position + forward * SpawnDistance + new Vector3(0f, SpawnHeight, 0f); int made = 0; for (int n = 0; n < count; n++) { float side = (n - (count - 1) * 0.5f) * 0.6f; SceneObject item = SpawnInventoryItem(Items[index], front + right * side, new Vector3(0f, player.Yaw, 0f), Vector3.one); if (item == null || item.IsNull) continue; mine.Add(item); made++; if (RemoveAfterSeconds > 0f) StartCoroutine(RemoveLater(user, item, RemoveAfterSeconds)); } status.SetText(made > 0 ? "Spawned " + made + " x " + NameOf(index) : "Could not spawn " + NameOf(index), user); float wait = cooldown.GetValue(user); if (wait > 0f) { readyAt[Key(user)] = now + wait; StartCoroutine(CooldownBar(user, wait)); } } // The bar fills up again while the player waits (only their own bar). private IEnumerator CooldownBar(string user, float seconds) { float start = Time.time; readyBar.SetText("Cooling down", user); while (Time.time - start < seconds) { readyBar.SetValue((Time.time - start) / seconds, user); yield return new WaitForSeconds(0.1f); } readyBar.SetValue(1f, user); readyBar.SetText("Ready", user); } private IEnumerator RemoveLater(string user, SceneObject item, float seconds) { yield return new WaitForSeconds(seconds); Remove(user, item); } private void Remove(string user, SceneObject item) { if (Mine(user).Remove(item)) DespawnSpawnedObject(item); } private void RemoveAll(string user, bool tell) { List mine = Mine(user); for (int i = mine.Count - 1; i >= 0; i--) DespawnSpawnedObject(mine[i]); mine.Clear(); if (tell) status.SetText("Your items are gone.", user); } private List Mine(string user) { string key = Key(user); if (!spawned.TryGetValue(key, out List list)) { list = new List(); spawned[key] = list; } return list; } private static string Key(string user) { return (user ?? "").ToLowerInvariant(); } private static int ParseAmount(string text) { return int.TryParse((text ?? "").Trim(), out int value) ? value : 1; } public override void OnUserPart(UserInfo user) { if (user == null) return; RemoveAll(user.Username, false); readyAt.Remove(Key(user.Username)); } public override void OnDestroy() { foreach (string user in new List(spawned.Keys)) RemoveAll(user, false); } }