# 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//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 DomainColours; // Domain.Forge, .Wood, .Enchant UPROPERTY(EditDefaultsOnly, Category = "Colour") TArray 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 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.