feat(hud): ship free default stat icons (visible in Studio Edit)

Owner relaxed the asset side of the content-free rule — assets in this repo are
free to use — so the engine now ships its generated HUD icons as defaults instead
of leaving blank slots. Two wins: the HUD is iconed out of the box, and the icons
render in Studio's EDIT view (no Play needed), so you can author/tune the HUD and
see the real thing — or edit it straight from the Explorer.

- StatDefs.luau: each of the 7 stats carries an `icon` default. Putting it here
  (not as a per-bar HUD attribute) keeps the admin-panel/config override working —
  a per-bar attribute would shadow it. resolveIcon: per-bar attr > config/def.icon
  (now the shipped id) > Assets > "".
- assets/hud/SurvivalHud.model.json: bake each bar's child `Icon` ImageLabel
  (Image + Visible=true) so it shows in edit mode; the Credits counter (no StatDefs
  entry) also gets a per-bar `Icon` attribute to drive its runtime resolution.
- Retire demo/client/HudIcons.client.luau (+ its demo.project.json mount): the
  runtime icon-assigning script is now redundant.
- Relax the docs/PR-template "content-free" wording to "design-free": free default
  ART may ship (overridable per stat); game-specific DESIGN (items/recipes/lore)
  still never ships. Updated design-language, survival-stats, CONTRIBUTING.
- CHANGELOG: record default icons + add the missing admin-plugin entry; note CI
  now builds all three Rojo targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Samuel Lison
2026-06-19 19:02:39 +10:00
co-authored by Claude Opus 4.8
parent 2e7bd69c2c
commit 69bbf12be9
10 changed files with 68 additions and 84 deletions
+2 -2
View File
@@ -13,6 +13,6 @@
## Checklist
- [ ] Added an entry to **`CHANGELOG.md`** under `## Unreleased` (or applied the `skip-changelog` label if no entry is warranted — e.g. a CI-only or trivial docs change)
- [ ] CI is green — `stylua --check`, `selene`, `luau-lsp analyze`, and **both** `rojo build`s pass
- [ ] CI is green — `stylua --check`, `selene`, `luau-lsp analyze`, and **all three** `rojo build`s (engine, demo, plugin) pass
- [ ] No secrets or personal data added (this is a public repo)
- [ ] **No hardcoded asset IDs** — content comes via `register()` / components / `Assets`, and the core stays content-free
- [ ] **No game-specific design content** — items / recipes / lore / world strings enter via `register()` / components / `Assets`, not the engine (free *default* art, e.g. HUD icons, is fine)
+18 -6
View File
@@ -13,21 +13,33 @@ is promoted to the new version and `main` is tagged `vX.Y.Z`.
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. 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). Afflictions
to the full roster, with per-bar **numeric readouts** (`99/100` / `%`, configurable) and per-stat
**icons** — the engine ships a free default icon set (baked into `StatDefs` + the HUD template),
so the HUD is iconed out of the box and the icons even show in Studio's **Edit** view, no Play
needed. Afflictions
(Hunger/Thirst/Fatigue/Poison) read as empty-when-safe and fill up as they worsen (`dangerHigh`),
and the header carries a **credits** readout (a bar-less `Counter` bound to a Player attribute).
HUD icons update **live** — a bar/counter shows its art the moment its `Icon` attribute is set, so
a game can assign icons at runtime (the demo ships an example flat icon set; the engine ships none). Ships with seven default stats (health/energy/hunger/thirst/fatigue/blood/
HUD icons resolve per bar/counter and update **live** — a game can swap art at runtime by setting
the `Icon` attribute, or override the shipped defaults per stat (config / admin plugin / `Assets`).
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
on `start()`, and a runtime fallback guarantees a HUD always appears. Adds the engine's first
client layer (`SurvivorCore.startClient()`). See [docs/survival-stats.md](docs/survival-stats.md).
- **Survival Stats admin plugin** (#11, first slice) — a Studio dock widget to tune the survival
stats from a validated form instead of hand-editing Attributes. It writes **deltas only** (an
attribute only when a field differs from the engine default; removed on reset / edit-back), so
owner tuning survives engine updates while untouched fields keep following improvable defaults,
and a hard guardrail makes the engine-owned `Invert`/`DangerHigh` semantics impossible to write.
Built as a separate `plugin.project.json` Rojo target (install via
`rojo build plugin.project.json --plugin …`); CI lints and builds it too.
See [docs/admin-plugin.md](docs/admin-plugin.md).
- **Continuous integration** (`.github/workflows/ci.yml`) — every push to `main`/`dev` and
every PR runs `stylua --check`, `selene`, `luau-lsp analyze` (against a Rojo sourcemap +
Roblox type defs), then builds **both** `default.project.json` (the drop-in engine model)
and `demo.project.json` (the runnable demo place) to prove they compile.
Roblox type defs), then builds **all three** Rojo targets — `default.project.json` (the drop-in
engine model), `demo.project.json` (the runnable demo place), and `plugin.project.json` (the
admin plugin) — to prove they compile.
- **Changelog enforcement** (`.github/workflows/changelog.yml`) — PRs must update
`CHANGELOG.md` unless they carry the `skip-changelog` label.
- **Contributor guide** (`CONTRIBUTING.md`) — prerequisites, local dev setup, code style,
+9 -6
View File
@@ -69,11 +69,14 @@ components, hooks, and the foundation. See [Architecture Overview](#architecture
- **Formatting & linting are enforced by CI.** Before you push, run the same checks CI does
(see [Making a Pull Request](#making-a-pull-request)). `stylua` owns formatting — don't
hand-format around it.
- **Never hardcode asset IDs in `.luau`.** Asset IDs are content. Register them through
`Assets` (`Assets.register("Sounds", "Harvest", "rbxassetid://…")`) and read them back, with
the empty-string fallback convention. The engine must contain **zero** concrete asset IDs.
- **Keep the core content-free.** No concrete items, recipes, lore, world strings, or
instance-name string matches (`name == "campfire"`) in the engine. Content enters two ways:
- **Default art is fine; keep it swappable.** Assets in this repo are free to use, so the engine
may ship free *default* art IDs (e.g. the HUD stat icons in `StatDefs`) — provided they stay
overridable (config / admin plugin / `Assets`) and never get buried in logic. For dynamic or
owner-supplied art, still route through the `Assets` registry
(`Assets.register("Sounds", "Harvest", "rbxassetid://…")`) with the empty-string fallback.
- **Keep the core free of game-specific *design*.** No concrete items, recipes, lore, world
strings, or instance-name string matches (`name == "campfire"`) in the engine — free default
*art* is the one exception (above). Content enters two ways:
- **Registries** — developers call `register()` from code (`Items`, `Recipes`, `Stats`,
`Mobs`, …).
- **Components** — creators tag their own objects and set Attributes (`Gatherable`, and the
@@ -116,7 +119,7 @@ published (which auto-posts to Discussions → Announcements).
# git checkout -b docs/my-fix main # documentation only
```
3. **Implement your change.** Write clear, typed Luau. Add comments where the "why" isn't
obvious. Keep the engine content-free.
obvious. Keep the engine free of game-specific design (free default art is fine).
4. **Test locally.** Play-test in Studio, then run the same checks CI does:
```bash
stylua --check src demo assets plugin
+18 -17
View File
@@ -167,7 +167,8 @@
"BorderSizePixel": 0
},
"attributes": {
"Counter": "Credits"
"Counter": "Credits",
"Icon": "rbxassetid://137051934393677"
},
"children": [
{
@@ -201,9 +202,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://137051934393677",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"LayoutOrder": 1
}
},
@@ -449,9 +450,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://88514622686548",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
@@ -668,9 +669,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://129856164091801",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
@@ -925,9 +926,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://138699077112926",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
@@ -1144,9 +1145,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://124374881225649",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
@@ -1363,9 +1364,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://102789813187589",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
@@ -1582,9 +1583,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://139788071928396",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
@@ -1801,9 +1802,9 @@
]
},
"BackgroundTransparency": 1,
"Image": "",
"Image": "rbxassetid://86406240258610",
"ScaleType": "Fit",
"Visible": false,
"Visible": true,
"ZIndex": 2
}
},
+1 -2
View File
@@ -17,8 +17,7 @@
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"HudLoader": { "$path": "assets/client/HudLoader.client.luau" },
"HudIcons": { "$path": "demo/client/HudIcons.client.luau" }
"HudLoader": { "$path": "assets/client/HudLoader.client.luau" }
}
},
"Workspace": {
-43
View File
@@ -1,43 +0,0 @@
--!nonstrict
--[[
DEMO ONLY assigns the demo's generated survival-stat icons to the HUD.
The engine ships ZERO asset ids (content-free); a real game does this for its own art.
Setting a bar/counter's `Icon` attribute makes the HUD binder render it (the binder
re-renders when `Icon` changes, so timing doesn't matter). These are flat, game-agnostic
icons generated for the demo via 3D AI Studio (Gemini 3 Pro).
]]
local Players = game:GetService("Players")
local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
local ICONS: { [string]: string } = {
Health = "rbxassetid://88514622686548",
Energy = "rbxassetid://129856164091801",
Hunger = "rbxassetid://138699077112926",
Thirst = "rbxassetid://124374881225649",
Fatigue = "rbxassetid://102789813187589",
Blood = "rbxassetid://139788071928396",
Poison = "rbxassetid://86406240258610",
Credits = "rbxassetid://137051934393677",
}
local function apply(hud: Instance)
for _, descendant in hud:GetDescendants() do
local key = descendant:GetAttribute("Stat") or descendant:GetAttribute("Counter")
if typeof(key) == "string" and ICONS[key] and ICONS[key] ~= "" then
descendant:SetAttribute("Icon", ICONS[key])
end
end
end
local hud = playerGui:WaitForChild("SurvivalHud", 10)
if hud then
apply(hud)
end
playerGui.ChildAdded:Connect(function(child)
if child.Name == "SurvivalHud" then
task.wait(0.2)
apply(child)
end
end)
+5 -2
View File
@@ -8,8 +8,11 @@ re-skin freely; these are the engine's clean defaults and the rules any shipped
## Principles
- **Content-free.** The engine ships **zero** `rbxassetid://`. Art is owner-supplied or
generated into the `Assets` registry; defaults are plain colors + text.
- **Free, swappable defaults.** Assets in this repo are free to use, so the engine ships free
**default** art (e.g. the HUD stat icons) — every piece overridable per stat (config / admin
plugin) or via the `Assets` registry. No look is hard-coded; where no art resolves, the fallback
is plain colors + text. (Game-specific *design* — items, recipes, lore — still never ships in the
engine; see [CONTRIBUTING](../CONTRIBUTING.md).)
- **Owner-editable.** UI is real, restyleable Instances driven by attributes — never hard-coded
layouts the owner can't change.
- **Tasteful, not flashy.** Clean, modern, readable. Flat surfaces, soft translucency, restraint.
+4 -2
View File
@@ -103,7 +103,8 @@ set only `Stat`): `Max`, `Invert`, `WarnAt`, `FillColor`, `WarnColor`, `FillAxis
**Icons** resolve from the per-bar `Icon` attribute → the stat's `icon` config → the `Assets`
registry category **`StatIcons`** (key = stat name) — e.g. `SurvivorCore.Assets.register("StatIcons",
"Hunger", "rbxassetid://…")`. The engine ships none, so slots stay blank until you supply art.
"Hunger", "rbxassetid://…")`. The engine now ships **default** icons for its built-in stats (as
each stat's `icon` default), so bars are iconed out of the box; the chain above replaces them.
**Collapse** is wired generically: a `GuiButton` with a **`HudToggle`** attribute (its value names
the container, e.g. `"Collapsible"`) toggles a container marked **`HudCollapsible`**. Both are also
@@ -115,7 +116,8 @@ To re-skin: edit the `Fill` and surrounding elements however you like. (Bars tag
## Providing your own HUD art
The engine ships **no** icons (content-free). Supply your own — first match wins:
The engine ships **default** icons for its built-in stats, so the HUD is iconed out of the box (and
they show in Studio's Edit view, no Play needed). To use your own art, set any of these — first match wins:
1. set a bar's **`Icon`** attribute (or the credits/counter element's) to an asset id — **live**:
the HUD renders it the instant it's set, so a game can assign art at runtime;
+1 -1
View File
@@ -72,7 +72,7 @@ export type ResolvedStat = {
invert: boolean,
warnAt: number,
display: boolean,
icon: string, -- HUD icon asset id, "" when none (engine ships "" — content-free)
icon: string, -- HUD icon asset id, "" when none (engine ships free defaults; overridable)
valueFormat: string, -- HUD numeric readout: "fraction" | "percent" | "value" | "none"
}
+10 -3
View File
@@ -25,7 +25,7 @@ export type StatDef = {
warnAt: number, -- warn within this percent of the dangerous end (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
icon: string?, -- HUD icon asset id; engine ships free default icons (overridable per stat)
valueFormat: string?, -- HUD numeric readout: "fraction" | "percent" | "value" | "none"
}
@@ -36,7 +36,7 @@ local function perMinutes(minutes: number): number
return 100 / (minutes * 60)
end
-- The shipped defaults. Numbers ported from The Counter Earth; content-free.
-- The shipped defaults. Numbers + default icons ported from The Counter Earth.
-- Hunger/Thirst/Fatigue/Poison start at 0 and rise toward 100 = bad (`dangerHigh = true`,
-- so their bars fill up as they worsen). Health/Energy/Blood are resources (`dangerHigh =
-- false`: 0 = bad). Health/Energy are display-only here (drain/regen + consequences = #5/#6).
@@ -50,6 +50,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://88514622686548",
},
{
name = "Energy",
@@ -60,6 +61,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://129856164091801",
},
{
name = "Hunger",
@@ -70,6 +72,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://138699077112926",
},
{
name = "Thirst",
@@ -80,6 +83,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://124374881225649",
},
{
name = "Fatigue",
@@ -90,6 +94,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://102789813187589",
},
{
name = "Poison",
@@ -100,6 +105,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://86406240258610",
},
{
name = "Blood",
@@ -110,6 +116,7 @@ StatDefs.DEFAULTS = {
invert = false,
warnAt = 25,
display = true,
icon = "rbxassetid://139788071928396",
},
}
@@ -135,7 +142,7 @@ 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
icon = def.icon or "", -- engine ships a default icon; owners override via config/Assets
valueFormat = def.valueFormat or "fraction",
}
Registries.Stats.register({