From 69bbf12be9c3290f492e51def780e35456506029 Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Fri, 19 Jun 2026 19:02:39 +1000 Subject: [PATCH] feat(hud): ship free default stat icons (visible in Studio Edit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/pull_request_template.md | 4 +-- CHANGELOG.md | 24 ++++++++++++----- CONTRIBUTING.md | 15 ++++++----- assets/hud/SurvivalHud.model.json | 35 +++++++++++++------------ demo.project.json | 3 +-- demo/client/HudIcons.client.luau | 43 ------------------------------- docs/design-language.md | 7 +++-- docs/survival-stats.md | 6 +++-- src/stats/StatConfig.luau | 2 +- src/stats/StatDefs.luau | 13 +++++++--- 10 files changed, 68 insertions(+), 84 deletions(-) delete mode 100644 demo/client/HudIcons.client.luau diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 99c1983..852aad4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md index f464e9d..ad0d4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9649ddc..10c0e49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/assets/hud/SurvivalHud.model.json b/assets/hud/SurvivalHud.model.json index 327d1c3..0db9fdb 100644 --- a/assets/hud/SurvivalHud.model.json +++ b/assets/hud/SurvivalHud.model.json @@ -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 } }, diff --git a/demo.project.json b/demo.project.json index f459bc4..7fa96ea 100644 --- a/demo.project.json +++ b/demo.project.json @@ -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": { diff --git a/demo/client/HudIcons.client.luau b/demo/client/HudIcons.client.luau deleted file mode 100644 index 2c8f6f9..0000000 --- a/demo/client/HudIcons.client.luau +++ /dev/null @@ -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) diff --git a/docs/design-language.md b/docs/design-language.md index dbbc8fd..e8760a5 100644 --- a/docs/design-language.md +++ b/docs/design-language.md @@ -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. diff --git a/docs/survival-stats.md b/docs/survival-stats.md index e053cf6..9d03381 100644 --- a/docs/survival-stats.md +++ b/docs/survival-stats.md @@ -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; diff --git a/src/stats/StatConfig.luau b/src/stats/StatConfig.luau index e5a8b05..8c39bb8 100644 --- a/src/stats/StatConfig.luau +++ b/src/stats/StatConfig.luau @@ -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" } diff --git a/src/stats/StatDefs.luau b/src/stats/StatDefs.luau index 9464d96..d16f192 100644 --- a/src/stats/StatDefs.luau +++ b/src/stats/StatDefs.luau @@ -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({