Add comprehensive design and specification documentation (#1)

* Write the Unreal documentation set: design, steps, ideas, decisions and specs

Rewrites the design and engineering docs of the two earlier Unity projects
(Adventurer Guild, Project Malleable) for an Unreal Engine 5 prototype that
builds and proves three things in order: a movement controller, fighting and
crafting. Neither earlier core loop is carried; their ideas are catalogued as
features with what each needs.

- Docs/Design.md: the human summary (pillars, fixed decisions, glossary)
- Docs/Steps.md: a ladder of fifteen proofs, the first six in full
- Docs/Ideas.md: salvaged ideas from both projects, none scheduled
- Docs/Decisions.md: D-01..D-36, open decisions OD-01..OD-08, deferrals
- Docs/Spec/: notation, Architecture, Movement, Interaction, Combat,
  Crafting, Networking, Telemetry, UI, written as Unreal C++ skeletons
- CLAUDE.md and README.md for the repository


* Add the stat block: one attribute set on every body, every influence an effect

Every body (player, enemy, NPC) carries the same UStatBlockAttributeSet, and
every outside influence on it is a gameplay effect: base values, buffs,
debuffs, ground surfaces, carried weight, gear, being downed. Counters are
tags and attributes on the receiver: immunity blocks by tag, resistance
scales by attribute, cleanse removes by tag. Same status from several
sources: strongest wins; different statuses multiply.

- Docs/Spec/Stats.md: the block, effects and their five sources (abilities,
  areas, surfaces, items, the world), stacking, counters, readers, tests
- Movement: the movement component's tagged speed-multiplier map is removed;
  speed is tuning times the MoveSpeed attribute; the ground surface trace
  turns mud into an effect; the gym gets mud and ice patches
- Combat: the attribute set moves to Stats; enemies and kits carry
  FStatBlockDefaults; the funnel reads MagicResist and Immune.Damage tags;
  taunt and mark are statuses
- Interaction: carried weight is GE_Encumbered against CarryCapacity
- Crafting: class bonuses are the WorkSpeed and WorkQuality attributes;
  enchantments may grant a holder effect
- Steps: step 5 builds the block with mud, a volume and an immunity
- Decisions D-37 and D-38; design summary, CLAUDE.md, indexes, worklog
This commit is contained in:
2026-09-15 17:54:00 +03:00
committed by GitHub
parent abac63da16
commit 0e61a77346
18 changed files with 4026 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
# UI
Owns what interface exists, what it is built on, and the rules that keep it one interface rather than a dozen:
the HUD, the prompt, the action bar, the one theme asset, world-space text on props, localisation and the menu.
It does **not** own what any readout means; the health number belongs to [Combat.md](Combat.md), the bench panel to
[Crafting.md](Crafting.md).
Read [Architecture.md](Architecture.md) first. There is deliberately little here. A prototype about moving,
fighting and crafting needs a crosshair, a health number, a prompt, four ability slots and a way to quit.
## The rule
```
[SALVAGED] The player acts on the world physically; the game reports state conventionally.
Everything the player DOES is a thing in the world reached through the interaction system: a bench, a station,
a weapon on the ground, a downed teammate. Screen space is for readouts: health, the prompt, the crosshair, the
action bar. The test for anything new: can the player do it by touching something in the world? If yes it is a
prop. If it only tells them something, it may be HUD.
The earlier guild project made this a pillar and forbade all menus but Escape. This project keeps the test and
drops the absolutism: a world this is headed for will need screens the prototype does not, and the rule's job is
to make every one of them earn its place, not to forbid them.
```
## Decisions
```
[DECIDED] UMG with CommonUI. Widgets are Blueprint children of C++ base classes that own the data binding; the
Blueprint owns layout and look only. CommonUI supplies input routing, gamepad focus and per-device glyphs, which
is exactly the part nobody wants to write twice.
```
```
[DECIDED] One theme asset. Every colour, type size, spacing step and motion duration is a token on one data asset,
named by role and never by hue. No widget holds a literal colour, size or duration. A colourblind palette is a
second asset with the same roles.
```
```
[DECIDED] Prop text is world-space and has no canvas. Text on a thing in the world is a UTextRenderComponent, or a
world-space UWidgetComponent when it needs layout (the bench panel). It reads the same theme.
```
```
[DECIDED] Everything a player reads is FText from a string table. FString is for identifiers and logs.
```
## Layout
```
Source/<Project>/UI/
├── UITheme.h // UPrimaryDataAsset: the tokens
├── ThemedText.h // UCommonTextBlock subclass reading a type token and a colour role
├── HudWidget.h // the base: health, crosshair, prompt, action bar, downed state; binds to the local player
├── InteractionPromptWidget.h // verb, target, reason, hold fill, live glyph
├── ActionBarWidget.h // four slots: glyph, name, cooldown; reads the ability system and the input subsystem
├── MenuWidget.h // Escape: Resume, Settings, Quit. Never pauses.
└── SettingsWidget.h // comfort sliders, motion scale, rebinding rows through the engine's user settings
Content/UI/
├── DA_Theme_Default
├── WBP_Hud, WBP_InteractionPrompt, WBP_ActionBar, WBP_ActionBarSlot, WBP_Menu, WBP_Settings
└── ST_Game // the string table; one key per readable string, prop prompts included
```
## The theme
```cpp
UCLASS(BlueprintType)
class UUITheme : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
// Roles, never hues. A widget names a role; the asset says what it looks like.
UPROPERTY(EditDefaultsOnly, Category = "Colour") FLinearColor HudNeutral, HudGood, HudWarning, HudDanger;
UPROPERTY(EditDefaultsOnly, Category = "Colour") FLinearColor PromptAvailable, PromptBlocked;
UPROPERTY(EditDefaultsOnly, Category = "Colour") FLinearColor OutlineInRange, OutlineTargeted;
UPROPERTY(EditDefaultsOnly, Category = "Colour") FLinearColor Scrim, PanelSurface, PanelText;
UPROPERTY(EditDefaultsOnly, Category = "Colour") TMap<FGameplayTag, FLinearColor> DomainColours; // Domain.Forge, .Wood, .Enchant
UPROPERTY(EditDefaultsOnly, Category = "Colour") TArray<FLinearColor> PlayerColours; // by join order, low saturation, rings not fills
// Type scale, in steps rather than free numbers. HUD sizes in pixels at 1080p; world sizes in centimetres.
UPROPERTY(EditDefaultsOnly, Category = "Type") FSlateFontInfo HudPrimary, HudSecondary, Prompt, Caption;
UPROPERTY(EditDefaultsOnly, Category = "Type") float PropTitleCm = 12.f, PropBodyCm = 7.f, PropSmallCm = 5.f;
UPROPERTY(EditDefaultsOnly, Category = "Space") TArray<float> SpacingSteps = {4, 8, 16, 24, 40};
UPROPERTY(EditDefaultsOnly, Category = "Motion") float Fast = 0.12f, Normal = 0.2f, Slow = 0.4f; // every one scaled by the motion setting
};
```
Meaning never rides on hue alone: a blocked prompt is grey **and** worded differently; a danger readout is red
**and** changes shape. `HudGood` leans teal and `HudDanger` leans orange so the two separate under deuteranopia.
Substance colours are not theme tokens; they are on the substance definitions because the substance is the palette.
The placeholder look: neutral, stylised, legible. The two source projects each had a fully specified palette (a
parchment-and-ink guild hall; a warm forge with domain tints). Neither is this game's, so the default asset is
plain white HUD over greybox and the domain tints, and an art direction decides the rest when there is one.
## The HUD
One widget, added by the player controller for the local player, reading the local player state's ability system
and the pawn's interaction component. It never computes: health is the attribute, the prompt is
`UInteractionComponent::GetCurrentPrompt`, the cooldown is the cooldown tag's remaining time.
| Element | Reads | Shows |
| --- | --- | --- |
| Crosshair | nothing | four pixels, `HudNeutral` |
| Health | `Health`, `MaxHealth`, `State.Downed` | number plus a status line; colour by fraction, and shape when downed |
| Prompt | `GetCurrentPrompt` | `[glyph] Verb Target`, greyed with the reason when blocked, a radial fill while holding |
| Action bar | the four `Input.Ability.n` abilities, the input subsystem | glyph (live binding, per device), name, seconds left; dim while empty or cooling |
| Downed | `State.Downed`, `GE_BleedOut` remaining | "Downed: 24 s" and who is nearest |
| Carry | `UCarryComponent::GetHeld` | the held object's name, and "hands full" on a blocked verb |
| Statuses | active `Status.*` tags and remaining durations | a row of icons from `DT_StatusIcons`, debuffs first; the "Immune" flash on a blocked one comes from the cue, not the HUD |
Two canvases, and the split is a rebuild-cost decision: the static one (crosshair, chrome) and the volatile one
(everything that changes). Neither has hit testing; the HUD is never clicked.
## Input modes
One place decides what the cursor does and which mapping context is live, and it is CommonUI's input routing on
the activatable widget stack. Gameplay input stops while the menu is up; **the world does not.** Enemies keep
moving, the forge keeps burning. This is the difference between a mode switch and a pause, and it is the design:
opening Settings in a fight is a bad idea, which is correct, and it means no second behaviour for the same key that
would be untested in one of the two modes.
The prompt's glyph and the action bar's keys come from the engine's Enhanced Input user settings through the
input subsystem, by the device the player touched last. No widget prints a literal key.
## World-space text
Text on a prop is a `UTextRenderComponent` reading the theme's world sizes and a colour role, on the prop itself,
with no canvas: it stays readable as you walk round it and lights with the scene. The surface rule: light surface,
`PanelText`; dark surface, `HudNeutral`. The bench's assembly panel, which needs slots and a gauge, is a
`UWidgetComponent` in world space reading the same theme and laid out by the family's authored grid so a sword
reads as a sword. Its quality gauge shows the synergy segment separately, so players learn why matching themes
score higher.
## The menu
Escape: Resume, Settings, Quit. Settings: FOV per mode, head stabilisation, bob, motion scale, text scale, and a
rebind row per bindable action through the engine's user settings. Nothing else, until a step needs something
else and says why here.
## Localisation
Every readable string is a key in `ST_Game`, resolved through `FText::FromStringTable`. Prompts are keyed by verb
tag (`Interact.Verb.Revive` resolves to `prompt.verb.revive`), reasons by reason tag, ability names on the ability
class as `FText`. The engine's localisation dashboard gathers from string tables and `FText` properties; nothing
else is needed until there is a second language.
## Accessibility hooks
Hooks now, content later; none of the later work touches a widget.
| Hook | Exists | Filled in later |
| --- | --- | --- |
| Text scale | `ThemedText` multiplies every size by it | a slider |
| Colourblind palettes | roles on the theme, no literals in widgets | alternative theme assets |
| Motion scale | every theme duration, camera kick, hit stop and bob multiplies by it | the slider ships in step 3, a comfort requirement for first person |
| Rebinding | prompts read the live binding | the rows ship in step 3 through engine settings |
| Hold-to-confirm | on irreversible verbs already | an option to extend it to every verb |
## Telemetry
`settings_changed` with `setting_id` as the dotted path (`camera.fov.first_person`, `input.bindings.interact`) and
the new value. Nothing else: prompt visibility and menu opens are high frequency and low value, and
`prop_interacted` already answers whether players find things.
## Open questions
- **Q1. World-space widget legibility.** A `UWidgetComponent` at bench distance in both camera modes has to be
checked once with real text before the assembly panel is built on it. Ten minutes in step 13, not a spike.
- **Q2. A font.** The engine's default until a look exists. When one is chosen it is one face with material
presets, not three font assets.
- **Q3. Does the HUD scale with resolution?** Scale with screen size at 1080p reference is assumed; ultrawide needs
one look.