From 1b5ef088b34a8ed19d398a68128d6c23435a57c0 Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Tue, 23 Jun 2026 17:23:17 +1000 Subject: [PATCH] fix(hud): keep a single HUD, preferring the authored one over the fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the authored template and the built-in fallback name their ScreenGui "SurvivalHud", and the fallback fires on a 1s timer if no bars have bound yet. The dedupe ran only once at startup, so a fallback that raced the template (more likely now startClient does more before the template copies in) left two HUDs on screen — a restart usually shuffled the timing enough to hide it, until it didn't. Tag the fallback, make the dedupe continuous (re-runs whenever another HUD appears) and always prefer the authored HUD, and don't build the fallback when a HUD ScreenGui already exists. Also link the inventory/hotbar demo video in docs/inventory.md. Co-Authored-By: Claude Opus 4.8 --- docs/inventory.md | 2 ++ src/client/Hud.luau | 42 ++++++++++++++++++++++++++++++------- src/client/HudFallback.luau | 5 +++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/inventory.md b/docs/inventory.md index 7bc12b4..5f13c8b 100644 --- a/docs/inventory.md +++ b/docs/inventory.md @@ -7,6 +7,8 @@ Character Sheet, with Codex / Achievements / Quests scaffolded). Like the surviv (icons, counts, fills) — never layout or colors. The engine ships **zero items**; your game registers them, and their display data replicates to clients automatically. +> 📹 **See it in action:** [Inventory & hotbar system](https://makertube.net/w/wXRkuJo323AHMpZKVwWxt3) + - **Data layer (server):** [src/systems/Inventory.luau](../src/systems/Inventory.luau) - **UI (client):** `PanelManager`, `InventoryUi`, `Hotbar`, `CharacterSheet`, `DragDrop`, `SlotGrid`, `UiFallback` under [src/client/](../src/client) diff --git a/src/client/Hud.luau b/src/client/Hud.luau index f8023c3..c3c0462 100644 --- a/src/client/Hud.luau +++ b/src/client/Hud.luau @@ -105,15 +105,31 @@ end local Hud = {} local started = false --- installHud() + the StarterGui->PlayerGui copy can briefly produce two HUDs; keep one. +-- Several paths can land more than one HUD in PlayerGui — installHud()'s clone + the +-- StarterGui->PlayerGui copy, or a late fallback racing the authored template. Keep exactly one, +-- always preferring an AUTHORED HUD over the built-in fallback (and dropping extra copies). local function dedupeHuds(playerGui: Instance) - local kept: Instance? = nil + local authored: Instance? = nil + local fallbacks: { Instance } = {} for _, gui in playerGui:GetChildren() do if gui:IsA("ScreenGui") and gui.Name == HUD_NAME then - if kept then - gui:Destroy() + if gui:GetAttribute(HudFallback.FALLBACK_ATTRIBUTE) then + table.insert(fallbacks, gui) + elseif authored then + gui:Destroy() -- a second authored copy else - kept = gui + authored = gui + end + end + end + if authored then + for _, fb in fallbacks do + fb:Destroy() -- authored wins; the fallback is only a no-template safety net + end + else + for index, fb in fallbacks do + if index > 1 then + fb:Destroy() end end end @@ -296,6 +312,13 @@ function Hud.start(_options: { [string]: any }?) end dedupeHuds(playerGui) + -- Run it again whenever another HUD shows up (a fallback racing the template, a late + -- StarterGui copy, installHud's clone) so we never end up with two on screen. + playerGui.ChildAdded:Connect(function(child) + if child:IsA("ScreenGui") and child.Name == HUD_NAME then + dedupeHuds(playerGui) + end + end) -- A stat bar is any GuiObject carrying a `Stat` attribute (how the authored -- template marks them) OR tagged `SurvivorStatBar` (for the Builder UI / fallback). @@ -442,11 +465,14 @@ function Hud.start(_options: { [string]: any }?) end end) - -- Zero-setup safety net: if nothing showed up, build the minimal fallback. + -- Zero-setup safety net: if no HUD showed up at all, build the minimal fallback. Guard on the + -- ScreenGui's presence (not just whether bars bound yet) so a slow authored HUD never gets a + -- fallback built alongside it; if it still arrives late, the ChildAdded dedupe drops the extra. task.delay(FALLBACK_WAIT, function() - if next(bound) == nil then - HudFallback.build(playerGui, StatConfig.resolve().stats) + if next(bound) ~= nil or playerGui:FindFirstChild(HUD_NAME) then + return end + HudFallback.build(playerGui, StatConfig.resolve().stats) end) end diff --git a/src/client/HudFallback.luau b/src/client/HudFallback.luau index d6a7ceb..5546a3b 100644 --- a/src/client/HudFallback.luau +++ b/src/client/HudFallback.luau @@ -28,9 +28,14 @@ local FALLBACK_COLORS = { local HudFallback = {} +-- Marks a HUD as the built-in fallback (vs an authored template), so the binder's dedupe can +-- always drop the fallback in favour of an authored HUD when both end up present. +HudFallback.FALLBACK_ATTRIBUTE = "SurvivorCoreHudFallback" + function HudFallback.build(playerGui: Instance, stats: { any }) local screen = Instance.new("ScreenGui") screen.Name = HUD_NAME + screen:SetAttribute(HudFallback.FALLBACK_ATTRIBUTE, true) screen.ResetOnSpawn = false -- Respect the ~36px Roblox topbar inset so bars never render under the CoreGui menu. screen.IgnoreGuiInset = false