diff --git a/CHANGELOG.md b/CHANGELOG.md index eee67e2..b4af4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ is promoted to the new version and `main` is tagged `vX.Y.Z`. drain/regen, stored as auto-replicating Player Attributes, and a reactive **top-left HUD** renders them (no RemoteEvents). The HUD is a real, designer-editable `SurvivalHud` ScreenGui in StarterGui — restyle it in Studio with zero code; bars bind by a `Stat` attribute and a - `Fill` child. Ships with seven default stats (health/energy/hunger/thirst/fatigue/blood/ + `Fill` child. It's a translucent panel that shows **Health + Energy** by default and **expands** + to the full roster, with per-bar **numeric readouts** (`99/100` / `%`, configurable) and blank + **icon slots** (filled from the `Assets` registry — the engine ships none). Ships with seven default stats (health/energy/hunger/thirst/fatigue/blood/ poison), tunable **without code** via a `SurvivalStatsConfig` Configuration instance (or, for developers, `Config.override("SurvivalStats", …)` / `Stats.defineStat`). Works out of the box for every distribution — the demo/Rojo source mount it, the drop-in `.rbxm` auto-installs it diff --git a/assets/config/SurvivalStatsConfig.model.json b/assets/config/SurvivalStatsConfig.model.json index 2588f52..9e07105 100644 --- a/assets/config/SurvivalStatsConfig.model.json +++ b/assets/config/SurvivalStatsConfig.model.json @@ -10,7 +10,9 @@ "Start": 100, "WarnAt": 25, "Invert": false, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } }, { @@ -22,7 +24,9 @@ "Start": 100, "WarnAt": 25, "Invert": false, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } }, { @@ -34,7 +38,9 @@ "Start": 0, "WarnAt": 25, "Invert": true, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } }, { @@ -46,7 +52,9 @@ "Start": 0, "WarnAt": 25, "Invert": true, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } }, { @@ -58,7 +66,9 @@ "Start": 0, "WarnAt": 25, "Invert": true, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } }, { @@ -70,7 +80,9 @@ "Start": 100, "WarnAt": 25, "Invert": false, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } }, { @@ -82,7 +94,9 @@ "Start": 0, "WarnAt": 25, "Invert": true, - "Display": true + "Display": true, + "Icon": "", + "ValueFormat": "fraction" } } ] diff --git a/assets/hud/SurvivalHud.model.json b/assets/hud/SurvivalHud.model.json index 8a93c50..89815d1 100644 --- a/assets/hud/SurvivalHud.model.json +++ b/assets/hud/SurvivalHud.model.json @@ -2,14 +2,14 @@ "className": "ScreenGui", "properties": { "ResetOnSpawn": false, - "IgnoreGuiInset": true, + "IgnoreGuiInset": false, "ZIndexBehavior": "Sibling", "DisplayOrder": 2 }, "children": [ { "className": "Frame", - "name": "Bars", + "name": "Panel", "properties": { "Position": { "UDim2": [ @@ -19,7 +19,7 @@ ], [ 0, - 16 + 12 ] ] }, @@ -27,7 +27,7 @@ "UDim2": [ [ 0, - 232 + 260 ], [ 0, @@ -36,17 +36,78 @@ ] }, "AutomaticSize": "Y", - "BackgroundTransparency": 1, + "BackgroundColor3": { + "Color3": [ + 0.0784, + 0.0902, + 0.1176 + ] + }, + "BackgroundTransparency": 0.25, "BorderSizePixel": 0 }, "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 10 + ] + } + } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.85 + } + }, + { + "className": "UIPadding", + "properties": { + "PaddingTop": { + "UDim": [ + 0, + 10 + ] + }, + "PaddingBottom": { + "UDim": [ + 0, + 10 + ] + }, + "PaddingLeft": { + "UDim": [ + 0, + 14 + ] + }, + "PaddingRight": { + "UDim": [ + 0, + 14 + ] + } + } + }, { "className": "UIListLayout", "properties": { "Padding": { "UDim": [ 0, - 6 + 8 ] }, "SortOrder": "LayoutOrder", @@ -55,62 +116,155 @@ }, { "className": "Frame", - "name": "Health", + "name": "Header", "properties": { "Size": { "UDim2": [ [ - 0, - 232 + 1, + 0 ], [ 0, - 22 + 18 ] ] }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 + "BackgroundTransparency": 1, + "BorderSizePixel": 0, + "LayoutOrder": 0 + }, + "children": [ + { + "className": "TextLabel", + "name": "Title", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -22 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Status", + "TextColor3": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center" + } + }, + { + "className": "TextButton", + "name": "Toggle", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 18 + ], + [ + 0, + 18 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -18 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "AutoButtonColor": true, + "Text": "v", + "TextColor3": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "TextSize": 14, + "Font": "GothamBold", + "TextYAlignment": "Center", + "TextXAlignment": "Center" + }, + "attributes": { + "HudToggle": "Collapsible" + } + } + ] + }, + { + "className": "Frame", + "name": "Primary", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 0, + 0 + ] ] }, + "AutomaticSize": "Y", + "BackgroundTransparency": 1, "BorderSizePixel": 0, "LayoutOrder": 1 }, - "attributes": { - "Stat": "Health" - }, "children": [ { - "className": "UICorner", + "className": "UIListLayout", "properties": { - "CornerRadius": { + "Padding": { "UDim": [ 0, 6 ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] }, - "Thickness": 1, - "Transparency": 0.7 + "SortOrder": "LayoutOrder", + "FillDirection": "Vertical" } }, { "className": "Frame", - "name": "Fill", + "name": "Health", "properties": { "Size": { "UDim2": [ @@ -119,19 +273,23 @@ 0 ], [ - 1, - 0 + 0, + 24 ] ] }, "BackgroundColor3": { "Color3": [ - 0.9098, - 0.2745, - 0.2745 + 0.1098, + 0.1255, + 0.1647 ] }, - "BorderSizePixel": 0 + "BorderSizePixel": 0, + "LayoutOrder": 1 + }, + "attributes": { + "Stat": "Health" }, "children": [ { @@ -144,113 +302,445 @@ ] } } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.9098, + 0.2745, + 0.2745 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Health", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 + } } ] }, { - "className": "TextLabel", - "name": "Label", + "className": "Frame", + "name": "Energy", "properties": { "Size": { "UDim2": [ - [ - 1, - -16 - ], [ 1, 0 - ] - ] - }, - "Position": { - "UDim2": [ - [ - 0, - 10 ], [ 0, - 0 + 24 ] ] }, - "BackgroundTransparency": 1, - "Text": "Health", - "TextColor3": { + "BackgroundColor3": { "Color3": [ - 0.9608, - 0.9608, - 0.9608 + 0.1098, + 0.1255, + 0.1647 ] }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } + "BorderSizePixel": 0, + "LayoutOrder": 2 + }, + "attributes": { + "Stat": "Energy" + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.3529, + 0.8039, + 0.4706 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Energy", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 + } + } + ] } ] }, { "className": "Frame", - "name": "Energy", + "name": "Collapsible", "properties": { "Size": { "UDim2": [ [ - 0, - 232 + 1, + 0 ], [ 0, - 22 + 0 ] ] }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 - ] - }, + "AutomaticSize": "Y", + "BackgroundTransparency": 1, "BorderSizePixel": 0, - "LayoutOrder": 2 - }, - "attributes": { - "Stat": "Energy" + "LayoutOrder": 2, + "Visible": false }, "children": [ { - "className": "UICorner", + "className": "UIListLayout", "properties": { - "CornerRadius": { + "Padding": { "UDim": [ 0, 6 ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] }, - "Thickness": 1, - "Transparency": 0.7 + "SortOrder": "LayoutOrder", + "FillDirection": "Vertical" } }, { "className": "Frame", - "name": "Fill", + "name": "Hunger", "properties": { "Size": { "UDim2": [ @@ -259,19 +749,23 @@ 0 ], [ - 1, - 0 + 0, + 24 ] ] }, "BackgroundColor3": { "Color3": [ - 0.3529, - 0.8039, - 0.4706 + 0.1098, + 0.1255, + 0.1647 ] }, - "BorderSizePixel": 0 + "BorderSizePixel": 0, + "LayoutOrder": 1 + }, + "attributes": { + "Stat": "Hunger" }, "children": [ { @@ -284,113 +778,188 @@ ] } } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.8627, + 0.549, + 0.1569 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Hunger", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 + } } ] }, - { - "className": "TextLabel", - "name": "Label", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - -16 - ], - [ - 1, - 0 - ] - ] - }, - "Position": { - "UDim2": [ - [ - 0, - 10 - ], - [ - 0, - 0 - ] - ] - }, - "BackgroundTransparency": 1, - "Text": "Energy", - "TextColor3": { - "Color3": [ - 0.9608, - 0.9608, - 0.9608 - ] - }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } - } - ] - }, - { - "className": "Frame", - "name": "Hunger", - "properties": { - "Size": { - "UDim2": [ - [ - 0, - 232 - ], - [ - 0, - 22 - ] - ] - }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 - ] - }, - "BorderSizePixel": 0, - "LayoutOrder": 3 - }, - "attributes": { - "Stat": "Hunger" - }, - "children": [ - { - "className": "UICorner", - "properties": { - "CornerRadius": { - "UDim": [ - 0, - 6 - ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] - }, - "Thickness": 1, - "Transparency": 0.7 - } - }, { "className": "Frame", - "name": "Fill", + "name": "Thirst", "properties": { "Size": { "UDim2": [ @@ -399,19 +968,23 @@ 0 ], [ - 1, - 0 + 0, + 24 ] ] }, "BackgroundColor3": { "Color3": [ - 0.8627, - 0.549, - 0.1569 + 0.1098, + 0.1255, + 0.1647 ] }, - "BorderSizePixel": 0 + "BorderSizePixel": 0, + "LayoutOrder": 2 + }, + "attributes": { + "Stat": "Thirst" }, "children": [ { @@ -424,113 +997,188 @@ ] } } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.2353, + 0.6275, + 0.902 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Thirst", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 + } } ] }, - { - "className": "TextLabel", - "name": "Label", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - -16 - ], - [ - 1, - 0 - ] - ] - }, - "Position": { - "UDim2": [ - [ - 0, - 10 - ], - [ - 0, - 0 - ] - ] - }, - "BackgroundTransparency": 1, - "Text": "Hunger", - "TextColor3": { - "Color3": [ - 0.9608, - 0.9608, - 0.9608 - ] - }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } - } - ] - }, - { - "className": "Frame", - "name": "Thirst", - "properties": { - "Size": { - "UDim2": [ - [ - 0, - 232 - ], - [ - 0, - 22 - ] - ] - }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 - ] - }, - "BorderSizePixel": 0, - "LayoutOrder": 4 - }, - "attributes": { - "Stat": "Thirst" - }, - "children": [ - { - "className": "UICorner", - "properties": { - "CornerRadius": { - "UDim": [ - 0, - 6 - ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] - }, - "Thickness": 1, - "Transparency": 0.7 - } - }, { "className": "Frame", - "name": "Fill", + "name": "Fatigue", "properties": { "Size": { "UDim2": [ @@ -539,19 +1187,23 @@ 0 ], [ - 1, - 0 + 0, + 24 ] ] }, "BackgroundColor3": { "Color3": [ - 0.2353, - 0.6275, - 0.902 + 0.1098, + 0.1255, + 0.1647 ] }, - "BorderSizePixel": 0 + "BorderSizePixel": 0, + "LayoutOrder": 3 + }, + "attributes": { + "Stat": "Fatigue" }, "children": [ { @@ -564,113 +1216,188 @@ ] } } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.6275, + 0.3922, + 0.8627 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Fatigue", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 + } } ] }, - { - "className": "TextLabel", - "name": "Label", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - -16 - ], - [ - 1, - 0 - ] - ] - }, - "Position": { - "UDim2": [ - [ - 0, - 10 - ], - [ - 0, - 0 - ] - ] - }, - "BackgroundTransparency": 1, - "Text": "Thirst", - "TextColor3": { - "Color3": [ - 0.9608, - 0.9608, - 0.9608 - ] - }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } - } - ] - }, - { - "className": "Frame", - "name": "Fatigue", - "properties": { - "Size": { - "UDim2": [ - [ - 0, - 232 - ], - [ - 0, - 22 - ] - ] - }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 - ] - }, - "BorderSizePixel": 0, - "LayoutOrder": 5 - }, - "attributes": { - "Stat": "Fatigue" - }, - "children": [ - { - "className": "UICorner", - "properties": { - "CornerRadius": { - "UDim": [ - 0, - 6 - ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] - }, - "Thickness": 1, - "Transparency": 0.7 - } - }, { "className": "Frame", - "name": "Fill", + "name": "Blood", "properties": { "Size": { "UDim2": [ @@ -679,19 +1406,23 @@ 0 ], [ - 1, - 0 + 0, + 24 ] ] }, "BackgroundColor3": { "Color3": [ - 0.6275, - 0.3922, - 0.8627 + 0.1098, + 0.1255, + 0.1647 ] }, - "BorderSizePixel": 0 + "BorderSizePixel": 0, + "LayoutOrder": 4 + }, + "attributes": { + "Stat": "Blood" }, "children": [ { @@ -704,113 +1435,188 @@ ] } } + }, + { + "className": "UIStroke", + "properties": { + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 + ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.7059, + 0.1569, + 0.1569 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } + } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Blood", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 + } } ] }, - { - "className": "TextLabel", - "name": "Label", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - -16 - ], - [ - 1, - 0 - ] - ] - }, - "Position": { - "UDim2": [ - [ - 0, - 10 - ], - [ - 0, - 0 - ] - ] - }, - "BackgroundTransparency": 1, - "Text": "Fatigue", - "TextColor3": { - "Color3": [ - 0.9608, - 0.9608, - 0.9608 - ] - }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } - } - ] - }, - { - "className": "Frame", - "name": "Blood", - "properties": { - "Size": { - "UDim2": [ - [ - 0, - 232 - ], - [ - 0, - 22 - ] - ] - }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 - ] - }, - "BorderSizePixel": 0, - "LayoutOrder": 6 - }, - "attributes": { - "Stat": "Blood" - }, - "children": [ - { - "className": "UICorner", - "properties": { - "CornerRadius": { - "UDim": [ - 0, - 6 - ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] - }, - "Thickness": 1, - "Transparency": 0.7 - } - }, { "className": "Frame", - "name": "Fill", + "name": "Poison", "properties": { "Size": { "UDim2": [ @@ -819,19 +1625,23 @@ 0 ], [ - 1, - 0 + 0, + 24 ] ] }, "BackgroundColor3": { "Color3": [ - 0.7059, - 0.1569, - 0.1569 + 0.1098, + 0.1255, + 0.1647 ] }, - "BorderSizePixel": 0 + "BorderSizePixel": 0, + "LayoutOrder": 5 + }, + "attributes": { + "Stat": "Poison" }, "children": [ { @@ -844,194 +1654,189 @@ ] } } - } - ] - }, - { - "className": "TextLabel", - "name": "Label", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - -16 - ], - [ - 1, - 0 - ] - ] }, - "Position": { - "UDim2": [ - [ - 0, - 10 - ], - [ - 0, - 0 - ] - ] - }, - "BackgroundTransparency": 1, - "Text": "Blood", - "TextColor3": { - "Color3": [ - 0.9608, - 0.9608, - 0.9608 - ] - }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } - } - ] - }, - { - "className": "Frame", - "name": "Poison", - "properties": { - "Size": { - "UDim2": [ - [ - 0, - 232 - ], - [ - 0, - 22 - ] - ] - }, - "BackgroundColor3": { - "Color3": [ - 0.1098, - 0.1255, - 0.1647 - ] - }, - "BorderSizePixel": 0, - "LayoutOrder": 7 - }, - "attributes": { - "Stat": "Poison" - }, - "children": [ - { - "className": "UICorner", - "properties": { - "CornerRadius": { - "UDim": [ - 0, - 6 - ] - } - } - }, - { - "className": "UIStroke", - "properties": { - "Color": { - "Color3": [ - 0.8235, - 0.8627, - 0.9608 - ] - }, - "Thickness": 1, - "Transparency": 0.7 - } - }, - { - "className": "Frame", - "name": "Fill", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - 0 - ], - [ - 1, - 0 - ] - ] - }, - "BackgroundColor3": { - "Color3": [ - 0.4706, - 0.7059, - 0.2353 - ] - }, - "BorderSizePixel": 0 - }, - "children": [ { - "className": "UICorner", + "className": "UIStroke", "properties": { - "CornerRadius": { - "UDim": [ - 0, - 6 + "Color": { + "Color3": [ + 0.8235, + 0.8627, + 0.9608 ] + }, + "Thickness": 1, + "Transparency": 0.7 + } + }, + { + "className": "Frame", + "name": "Fill", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + }, + "BackgroundColor3": { + "Color3": [ + 0.4706, + 0.7059, + 0.2353 + ] + }, + "BorderSizePixel": 0 + }, + "children": [ + { + "className": "UICorner", + "properties": { + "CornerRadius": { + "UDim": [ + 0, + 6 + ] + } + } } + ] + }, + { + "className": "ImageLabel", + "name": "Icon", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 16 + ], + [ + 0, + 16 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 6 + ], + [ + 0.5, + -8 + ] + ] + }, + "BackgroundTransparency": 1, + "Image": "", + "ScaleType": "Fit", + "Visible": false, + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Label", + "properties": { + "Size": { + "UDim2": [ + [ + 1, + -90 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 0, + 28 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "Poison", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 13, + "Font": "GothamMedium", + "TextXAlignment": "Left", + "TextYAlignment": "Center", + "ZIndex": 2 + } + }, + { + "className": "TextLabel", + "name": "Value", + "properties": { + "Size": { + "UDim2": [ + [ + 0, + 56 + ], + [ + 1, + 0 + ] + ] + }, + "Position": { + "UDim2": [ + [ + 1, + -62 + ], + [ + 0, + 0 + ] + ] + }, + "BackgroundTransparency": 1, + "Text": "", + "TextColor3": { + "Color3": [ + 0.9608, + 0.9608, + 0.9608 + ] + }, + "TextSize": 12, + "Font": "GothamMedium", + "TextXAlignment": "Right", + "TextYAlignment": "Center", + "ZIndex": 2 } } ] - }, - { - "className": "TextLabel", - "name": "Label", - "properties": { - "Size": { - "UDim2": [ - [ - 1, - -16 - ], - [ - 1, - 0 - ] - ] - }, - "Position": { - "UDim2": [ - [ - 0, - 10 - ], - [ - 0, - 0 - ] - ] - }, - "BackgroundTransparency": 1, - "Text": "Poison", - "TextColor3": { - "Color3": [ - 0.9608, - 0.9608, - 0.9608 - ] - }, - "TextSize": 13, - "Font": "GothamMedium", - "TextXAlignment": "Left", - "TextYAlignment": "Center", - "ZIndex": 2 - } } - ] + ], + "attributes": { + "HudCollapsible": true + } } ] } diff --git a/src/client/Hud.luau b/src/client/Hud.luau index d95f766..4c843d7 100644 --- a/src/client/Hud.luau +++ b/src/client/Hud.luau @@ -8,12 +8,27 @@ Per-bar contract (attributes on the tagged Frame; most default from the stat config, so owners usually set only `Stat`): - • Stat (string) — Player attribute to bind, e.g. "Hunger" (required) - • Max (number) — fill denominator - • Invert (bool) — fill = 1 - value/Max (bar full = healthy) - • WarnAt (number) — warn when the displayed bar drops below this percent + • Stat (string) — Player attribute to bind, e.g. "Hunger" (required) + • Max (number) — fill denominator + • Invert (bool) — fill = 1 - value/Max (bar full = healthy) + • WarnAt (number) — warn when the displayed bar drops below this percent • FillColor/WarnColor (Color3), FillAxis ("X"|"Y") + • ValueFormat (string) — numeric readout: "fraction" "99/100" | "percent" "99%" + | "value" "99" | "none" (hide). Defaults to the stat config. + • Icon (string) — icon asset id override (defaults to the stat config / Assets). Required child: a GuiObject named `Fill`. Everything else is free-form styling. + + OPTIONAL per-bar children the binder also drives if present (absent => ignored): + • TextLabel `Value` — filled with the formatted number (per ValueFormat above). + • ImageLabel `Icon` — Image set from the resolved icon id; hidden while the id is "". + + OPTIONAL collapse controls (so the default view shows only the primary bars and the + rest reveal on click) — all discovered by name/attribute/tag, never by fixed path: + • A GuiButton with a `HudToggle` attribute (or tagged "HudToggle") toggles a + collapsible container. The attribute's string value names the container; empty + falls back to a "Collapsible"-named / "HudCollapsible"-tagged GuiObject. + • The container starts Visible=false; the binder flips it and rotates the chevron. + Everything new is optional, so a minimal HUD (or the fallback) binds exactly as before. ]] local CollectionService = game:GetService("CollectionService") @@ -24,12 +39,53 @@ local RunService = game:GetService("RunService") assert(RunService:IsClient(), "SurvivorCore HUD is client-only — boot it via SurvivorCore.startClient()") local StatConfig = require(script.Parent.Parent.stats.StatConfig) +local Assets = require(script.Parent.Parent.foundation.Assets) local HudFallback = require(script.Parent.HudFallback) local BAR_TAG = "SurvivorStatBar" local HUD_NAME = "SurvivalHud" local FALLBACK_WAIT = 1 -- seconds to wait for an authored HUD before building the fallback local DEFAULT_WARN_COLOR = Color3.fromRGB(200, 40, 40) +local DEFAULT_VALUE_FORMAT = "fraction" +local ICON_CATEGORY = "StatIcons" -- Assets category an owner can populate (key = stat name) + +-- Collapse-control discovery (attribute primary, CollectionService tag as a rename-proof +-- fallback the owner can apply in Studio; the shipped template uses attributes only). +local TOGGLE_NAME = "Toggle" +local TOGGLE_TAG = "HudToggle" +local TOGGLE_ATTR = "HudToggle" +local COLLAPSIBLE_NAME = "Collapsible" +local COLLAPSIBLE_TAG = "HudCollapsible" +local COLLAPSIBLE_ATTR = "HudCollapsible" + +-- Resolve the bar's numeric readout string for the given raw value/max. `value` is the +-- RAW stat (not the inverted fill ratio), so an inverted Hunger bar still reads "12/100". +local function formatValue(format: string, value: any, max: number): string + local n = if typeof(value) == "number" then value else 0 + if format == "none" then + return "" + elseif format == "percent" then + local pct = if max > 0 then math.floor((n / max) * 100 + 0.5) else 0 + return string.format("%d%%", math.clamp(pct, 0, 100)) + elseif format == "value" then + return tostring(math.floor(n + 0.5)) + end + -- "fraction" (default / unknown) + return string.format("%d/%d", math.floor(n + 0.5), math.floor(max + 0.5)) +end + +-- Resolve a bar's icon id: per-bar `Icon` attribute > stat config `icon` > Assets registry +-- (category "StatIcons", key = stat name). Returns "" when none — never a hardcoded id. +local function resolveIcon(bar: Instance, statName: string, def: any): string + local id = bar:GetAttribute("Icon") + if typeof(id) == "string" and id ~= "" then + return id + end + if def and typeof(def.icon) == "string" and def.icon ~= "" then + return def.icon + end + return Assets.tryGet(ICON_CATEGORY, statName) -- silent: "" when unregistered +end local Hud = {} local started = false @@ -89,6 +145,10 @@ function Hud.start(_options: { [string]: any }?) end end + -- Optional per-bar extras (resolved once; absent => the binder no-ops on them). + local valueLabel = bar:FindFirstChild("Value") + local iconLabel = bar:FindFirstChild("Icon") + local function render() local def = byName[statName] local max = bar:GetAttribute("Max") or (def and def.max) or 100 @@ -115,6 +175,24 @@ function Hud.start(_options: { [string]: any }?) fillObject.Size = UDim2.new(fillRatio, 0, size.Y.Scale, size.Y.Offset) end setColor(if fillRatio * 100 < warnAt then warnColor else normalColor) + + -- Numeric readout (optional `Value` TextLabel). Uses the RAW value, not the + -- inverted fill — owners expect "99/100" to mean the real attribute. + if valueLabel and valueLabel:IsA("TextLabel") then + local format = bar:GetAttribute("ValueFormat") + if typeof(format) ~= "string" then + format = (def and def.valueFormat) or DEFAULT_VALUE_FORMAT + end + valueLabel.Text = formatValue(format, value, max) + end + + -- Icon (optional `Icon` ImageLabel). Blank id => hidden, so a fresh engine + -- shows no empty box until the owner supplies art. + if iconLabel and (iconLabel:IsA("ImageLabel") or iconLabel:IsA("ImageButton")) then + local imageId = resolveIcon(bar, statName, def); + (iconLabel :: ImageLabel).Image = imageId; + (iconLabel :: GuiObject).Visible = imageId ~= "" + end end local connection = localPlayer:GetAttributeChangedSignal(statName):Connect(render) @@ -140,14 +218,83 @@ function Hud.start(_options: { [string]: any }?) return instance:IsA("GuiObject") and instance:GetAttribute("Stat") ~= nil end + -- A collapse toggle = a GuiButton with a `HudToggle` attribute (or tagged "HudToggle"). + local function isToggle(instance: Instance): boolean + if not instance:IsA("GuiButton") then + return false + end + return instance:GetAttribute(TOGGLE_ATTR) ~= nil + or instance.Name == TOGGLE_NAME + or CollectionService:HasTag(instance, TOGGLE_TAG) + end + + -- Find the collapsible container a toggle controls. Prefer the container named by the + -- toggle's `HudToggle` attribute (searched from the toggle's HUD root), else fall back + -- to a "HudCollapsible"-tagged / `HudCollapsible`-attributed / "Collapsible"-named one. + local function findCollapsible(button: Instance): Instance? + local hudRoot = button:FindFirstAncestorWhichIsA("ScreenGui") or playerGui + local named = button:GetAttribute(TOGGLE_ATTR) + if typeof(named) == "string" and named ~= "" then + local found = hudRoot:FindFirstChild(named, true) + if found and found:IsA("GuiObject") then + return found + end + end + for _, tagged in CollectionService:GetTagged(COLLAPSIBLE_TAG) do + if tagged:IsDescendantOf(hudRoot) and tagged:IsA("GuiObject") then + return tagged + end + end + for _, descendant in hudRoot:GetDescendants() do + if descendant:IsA("GuiObject") and descendant:GetAttribute(COLLAPSIBLE_ATTR) ~= nil then + return descendant + end + end + local byNameMatch = hudRoot:FindFirstChild(COLLAPSIBLE_NAME, true) + if byNameMatch and byNameMatch:IsA("GuiObject") then + return byNameMatch + end + return nil + end + + local toggles: { [Instance]: () -> () } = {} + local function wireToggle(button: Instance) + if toggles[button] or not isToggle(button) or not button:IsDescendantOf(playerGui) then + return + end + local target = findCollapsible(button) + if not target or not target:IsA("GuiObject") then + return -- minimal HUDs with a button but no container: skip silently + end + local container: GuiObject = target + local guiButton: GuiButton = button :: GuiButton + + local function apply() + -- Rotate the same glyph 180° to flip the chevron — no second glyph / asset. + guiButton.Rotation = if container.Visible then 180 else 0 + end + apply() + local connection = guiButton.Activated:Connect(function() + container.Visible = not container.Visible + apply() + end) + toggles[button] = function() + connection:Disconnect() + end + end + for _, instance in playerGui:GetDescendants() do if isBar(instance) then tryBind(instance) + elseif isToggle(instance) then + wireToggle(instance) end end playerGui.DescendantAdded:Connect(function(instance) if isBar(instance) then tryBind(instance) + elseif isToggle(instance) then + wireToggle(instance) end end) playerGui.DescendantRemoving:Connect(function(instance) @@ -157,6 +304,11 @@ function Hud.start(_options: { [string]: any }?) end bound[instance] = nil renderers[instance] = nil + local untoggle = toggles[instance] + if untoggle then + untoggle() + toggles[instance] = nil + end end) for _, bar in CollectionService:GetTagged(BAR_TAG) do @@ -164,6 +316,11 @@ function Hud.start(_options: { [string]: any }?) end CollectionService:GetInstanceAddedSignal(BAR_TAG):Connect(tryBind) + for _, button in CollectionService:GetTagged(TOGGLE_TAG) do + wireToggle(button) + end + CollectionService:GetInstanceAddedSignal(TOGGLE_TAG):Connect(wireToggle) + -- Re-render bars live when the owner edits the Studio config (WarnAt / Max / colors). local function rerenderAll() byName = StatConfig.resolve().byName diff --git a/src/client/HudFallback.luau b/src/client/HudFallback.luau index da05d9f..d6a7ceb 100644 --- a/src/client/HudFallback.luau +++ b/src/client/HudFallback.luau @@ -32,16 +32,45 @@ function HudFallback.build(playerGui: Instance, stats: { any }) local screen = Instance.new("ScreenGui") screen.Name = HUD_NAME screen.ResetOnSpawn = false - screen.IgnoreGuiInset = true + -- Respect the ~36px Roblox topbar inset so bars never render under the CoreGui menu. + screen.IgnoreGuiInset = false screen.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + -- A dark translucent background panel so the fallback reads cleanly, matching the + -- authored template's look. Auto-sizes to its bars; the owner never edits this file. + local panel = Instance.new("Frame") + panel.Name = "Panel" + panel.Position = UDim2.fromOffset(16, 12) + panel.Size = UDim2.fromOffset(248, 0) + panel.AutomaticSize = Enum.AutomaticSize.Y + panel.BackgroundColor3 = Color3.fromRGB(20, 23, 30) + panel.BackgroundTransparency = 0.25 + panel.BorderSizePixel = 0 + panel.Parent = screen + + local corner = Instance.new("UICorner") + corner.CornerRadius = UDim.new(0, 10) + corner.Parent = panel + + local stroke = Instance.new("UIStroke") + stroke.Color = Color3.fromRGB(210, 220, 245) + stroke.Thickness = 1 + stroke.Transparency = 0.85 + stroke.Parent = panel + + local padding = Instance.new("UIPadding") + padding.PaddingTop = UDim.new(0, 10) + padding.PaddingBottom = UDim.new(0, 10) + padding.PaddingLeft = UDim.new(0, 12) + padding.PaddingRight = UDim.new(0, 12) + padding.Parent = panel + local container = Instance.new("Frame") container.Name = "Bars" - container.Position = UDim2.fromOffset(18, 18) - container.Size = UDim2.fromOffset(220, 0) + container.Size = UDim2.fromScale(1, 0) container.AutomaticSize = Enum.AutomaticSize.Y container.BackgroundTransparency = 1 - container.Parent = screen + container.Parent = panel local layout = Instance.new("UIListLayout") layout.Padding = UDim.new(0, 6) @@ -75,14 +104,31 @@ function HudFallback.build(playerGui: Instance, stats: { any }) local label = Instance.new("TextLabel") label.Name = "Label" label.BackgroundTransparency = 1 - label.Size = UDim2.fromScale(1, 1) + label.Size = UDim2.new(1, -64, 1, 0) + label.Position = UDim2.fromOffset(8, 0) label.Text = stat.name label.TextColor3 = Color3.fromRGB(245, 245, 245) label.TextSize = 12 label.Font = Enum.Font.GothamMedium + label.TextXAlignment = Enum.TextXAlignment.Left label.ZIndex = 2 label.Parent = bar + -- Empty `Value` label the binder fills with the formatted number (optional but + -- cheap; keeps the fallback consistent with the authored template). + local valueLabel = Instance.new("TextLabel") + valueLabel.Name = "Value" + valueLabel.BackgroundTransparency = 1 + valueLabel.Size = UDim2.new(0, 52, 1, 0) + valueLabel.Position = UDim2.new(1, -58, 0, 0) + valueLabel.Text = "" + valueLabel.TextColor3 = Color3.fromRGB(245, 245, 245) + valueLabel.TextSize = 12 + valueLabel.Font = Enum.Font.GothamMedium + valueLabel.TextXAlignment = Enum.TextXAlignment.Right + valueLabel.ZIndex = 2 + valueLabel.Parent = bar + CollectionService:AddTag(bar, BAR_TAG) -- Parent last, fully built, so the binder sees Stat + Fill when it fires. bar.Parent = container diff --git a/src/foundation/Assets.luau b/src/foundation/Assets.luau index 2895ebf..1e00669 100644 --- a/src/foundation/Assets.luau +++ b/src/foundation/Assets.luau @@ -31,4 +31,16 @@ function Assets.get(category: string, key: string): string return id end +-- Like `get` but SILENT: returns "" for a missing/empty asset without warning. +-- Use it on hot paths (e.g. the HUD resolving an optional per-stat icon every render) +-- where a missing id is an expected, non-error case. +function Assets.tryGet(category: string, key: string): string + local cat = store[category] + local id = cat and cat[key] + if typeof(id) == "string" then + return id + end + return "" +end + return Assets diff --git a/src/stats/StatConfig.luau b/src/stats/StatConfig.luau index de9e9da..b04cf82 100644 --- a/src/stats/StatConfig.luau +++ b/src/stats/StatConfig.luau @@ -29,6 +29,8 @@ local STUDIO_ATTR_MAP = { WarnAt = "warnAt", Invert = "invert", Display = "display", + Icon = "icon", + ValueFormat = "valueFormat", } local function applyTable(target: { [string]: any }, override: { [string]: any }?) @@ -65,6 +67,8 @@ export type ResolvedStat = { invert: boolean, warnAt: number, display: boolean, + icon: string, -- HUD icon asset id, "" when none (engine ships "" — content-free) + valueFormat: string, -- HUD numeric readout: "fraction" | "percent" | "value" | "none" } -- Returns the merged stat models, both as an ordered array and a name lookup. @@ -84,6 +88,8 @@ function StatConfig.resolve(): { stats: { ResolvedStat }, byName: { [string]: Re invert = def.invert, warnAt = def.warnAt, display = def.display, + icon = def.icon or "", + valueFormat = def.valueFormat or "fraction", } if section then applyTable(resolved, section[def.name]) diff --git a/src/stats/StatDefs.luau b/src/stats/StatDefs.luau index 4caff0f..5255af5 100644 --- a/src/stats/StatDefs.luau +++ b/src/stats/StatDefs.luau @@ -23,6 +23,8 @@ export type StatDef = { warnAt: number, -- warn when the displayed bar drops below this percent (0-100) display: boolean, -- show in the HUD by default attribute: string, -- Player attribute name (defaults to `name`) + icon: string?, -- HUD icon asset id; ALWAYS "" here (content-free) — owners supply art + valueFormat: string?, -- HUD numeric readout: "fraction" | "percent" | "value" | "none" } local StatDefs = {} @@ -91,6 +93,8 @@ function StatDefs.install() invert = def.invert, warnAt = def.warnAt, display = def.display, + icon = def.icon or "", -- content-free default; owners supply art via config/Assets + valueFormat = def.valueFormat or "fraction", } Registries.Stats.register({ name = def.name, @@ -101,6 +105,8 @@ function StatDefs.install() warnAt = def.warnAt, display = def.display, attribute = def.name, + icon = def.icon or "", + valueFormat = def.valueFormat or "fraction", }) end Config.defineSection("SurvivalStats", section)