DRIFTLANDS co-op survival on drifting tides — studio progress

UX Spec: Crafting — Station Screen, Repair & Examine

Status: Reviewed — APPROVED (/ux-review final pass 2026-08-31: 0 blocking, 0 advisory) · v0.4
Author: Jeroen + ux-designer
Last Updated: 2026-08-31
Journey Phase(s): unknown — no player journey map yet (see Open Questions)
Template: UX Spec
Variants: Station (opened with Interact at a workbench: recipe list + craft + repair) · Examine (read-only look at another player's equipped gear — AC-13's third inspection context)
Requirements input: crafting Rules 1–3, 5, 7–9, 12; crafting UI Requirements (all known recipes visible, three per-cause reason strings, maker's mark everywhere, craft-beat progress with true-no-op cancel, glanceable wear, aggregation ban); EC1/EC2 commit races, EC5 disconnect at commit; harvesting no-op tone; base-building Rule 12 (stations are structures BB places; stationPresentExact / stationPresentAtLeast); art bible §7; ui-hud quick-spec Rule 4 (Crafting row) + queue item ②
Patterns used: #2 List Picker · #3 Panel/Modal · #4 Focus & Nav · #5 Tooltip/Inspect · #6 Reason String · #7 Honest No-Op Cue · #11 Craft-Beat Progress · #12 Maker's-Mark Inspect · #19 Feedback Split · #24 Interact Prompt · (new candidates: Repair action; Examine)
Related specs: design/ux/inventory.md (APPROVED — equip lives there; the rack context is its storage tooltip), design/ux/hud.md (APPROVED — E3 wear accent, E11 interact prompt)
AC-13 joint coverage: crafting AC-13's three inspection contexts are satisfied across two specs — tooltip + storage rack by inventory.md, equipped-on-another-player by this spec's Examine card; test AC-13 across both.


Purpose & Player Need

The station is where the group's hauls become the group's gear — the one screen that delivers the headline fantasy ("the smith who forged the party's iron era"). The player arrives wanting to make the thing the bag now allows, or to put a dulled tool back to spec before the next trip. It must be honest about why something can't be made (three distinct world-fact reasons — never "locked"), unhurried but not slow (a 2 s hand-tended beat, never a menu-click factory), and impossible to misread as a factory or a scoreboard: no queues, no batches, no craft counts, no "made by" grouping (crafting Rules 5, 8; CD C2). Examine exists for the other half of the fantasy — the mark is read off the item on your friend, without asking (AC-13's third context).


Player Context on Arrival


Navigation Position

Gameplay → Station (context-dependent: within stationRange 3 u of a workbench, Interact with the bench as the nearest target). Gameplay → Examine (context-dependent: a player within interaction range as the nearest target, Interact). One panel at a time (#3): opening the station replaces an open bag. The bag never opens inside the station — materials are shown through the affordability read, and CARRY ∪ in-range STORAGE draw is automatic (Rule 7).


Entry & Exit Points

Entry Source Trigger Player carries this context
Gameplay near a workbenchInteract (E / West) with the bench as the nearest target (#24)station tier; CARRY + in-range STORAGE for affordability; worn-gear list (repair candidates)
Gameplay near a playerInteract with the player as the nearest #24 target (players are #24 candidates; at equal distance a structure wins — a chest is the likelier intent); the prompt reads "examine" with no name (hud.md E4: names only in event copy — the card carries the name)that player's equipped weapon / armor + marks (read-only, replicated)
DownedInteractAn ordinary station panel (ruled 2026-08-31, mirroring inventory.md OQ 7): ADR-0015 suppresses only Strike / Dash / Demolish, and no crafting rule gates on Downed. Craft is available; if the player crawls, the beat cancels by Rule 7's existing movement rule — no new mechanic, no reason string.
Exit Destination Trigger Notes
GameplayMenu / UI.Cancel (Esc / Start / East)closes; nothing debited
Crafting beat (world)Craft (Primary) on an available recipepanel closes, the stance begins (#11); commit at craftTimeSeconds; movement / dash cancels — true no-op
Gameplaywalking out of stationRangepanel closes (no bag half to degrade to, unlike the chest)
Gameplaystation demolished while open (another player)panel closes; an in-flight beat cancels as a no-op
GameplayExamine: Menu / Cancel / target leaves range / target disconnectscard closes
Gameplaytide deposit / death / host quitforce-close; nothing persists

No one-way exits; the only persistent changes are a committed craft or repair (host-confirmed, after the panel has already closed).


Layout Specification

Information Hierarchy

Deliberately absent: sort, filter, search, favourites, craft counts, "made by" grouping, queue or batch controls (Rules 5, 8; CD C2).

Layout Zones

All px at 100 % UI scale (pattern-library scale convention).

Component Inventory

Zone Component Type Content Interactive Pattern
Z1 hdrStation headerText"Workbench · cloth"No—
Z1aRepair row(s)Pinned list rowsworn item icon + name + "(worn)"; one per repairable worn item in CARRY / equipped; absent when noneYes#2 + Repair action (new — defined here)
Z1aRecipe rowsList Picker● / ○ marker, name; unavailable rows carry the reason lineYes#2, #6
Z1bOutput cardIcon + texticon, name, class · tierNo#5 tooltip host
Z1bInput rowsTextfiber 12 / 20 ✓ · timber 0 / 4 — (have = CARRY ∪ STORAGE within stationRange)No—
Z1bCraft buttonPrimary, pointer-only"Craft · 2 s"; dashed + reason when unavailable; mirrors the focused row's actionYes (mouse)#1
Z1bRepair buttonPrimary, pointer-only"Repair" (Repair rows only)Yes (mouse)#1
Z1cReason / verb hintTextfull reason string; "Enter: craft" / "Enter: repair"No#6
Z2Examine cardCardname + per-slot gear rows with marksNoExamine card (new — defined here) + #12

New patterns flagged for interaction-patterns.md: Repair action (an instant, stateless station verb) and Examine card.

ASCII Wireframe

STATION
┌─ Workbench · cloth ──────────────────────────────────────────┐
│ ▸ Repair: Cloth Sword (worn)    │  Cloth Garb                 │
│ ───────────────────────────     │  [icon]  armor · cloth      │
│ ● Cloth Garb*                   │                             │
│ ● Cloth Sword                   │  fiber    12 / 20   ✓       │
│ ○ Cloth Garb, Reinforced         │  sinew     2 / 2    ✓       │
│   not enough sinew in reach     │                             │
│                                 │      ┌───────────────┐      │
│  (MVP set = the three cloth     │      │  Craft · 2 s  │      │
│   recipes; a Tier 1 forge row   │      └───────────────┘      │
│   would read "you need a Forge")│       pointer-only          │
├─────────────────────────────────┴─────────────────────────────┤
│ Enter: craft                                   * = focus      │
└───────────────────────────────────────────────────────────────┘
have = bag + chests within 3 u · rows in recipe-data order · no sort

EXAMINE (world-space card above the player)
┌─ ◉ Mara ────────────────────────────┐
│ ⚔ Cloth Sword · cloth · fresh        │
│   Jo — forged 4 tides ago            │
│ 🛡 Cloth Garb · cloth · worn          │
│   Mara — forged 11 tides ago         │
└─────────────────────────────────────┘

Z1 shares the bag panel's anchor so the two never coexist and spatial memory carries over. Focus starts on the first Repair row if present, else the first available recipe, else the first recipe.


States & Variants

State / Variant Trigger What Changes
Station (default)Interact at a bench in rangeZ1 with Repair rows (if any) + all known recipes
No repairsnothing worn in CARRY / equipped that this tier can repairRepair block absent (no empty header)
All recipes availablecloth tier, materials in reachevery row ●; footer shows the verb hint only
Unavailable — stationrecipe needs a station of another tier○ + "you need a Workbench" (Rule 3 cause a — canonical string; Tier 1 instance: "you need a Forge")
Unavailable — worldan input's source template has never docked○ + "no land has offered copper yet" (cause b)
Unavailable — affordabilityinputs short in reach○ + "not enough timber in reach" (cause c); the short input row shows —
Empty recipe listimpossible at MVP (every cloth recipe is known from session 1, Rule 2)defined for Tier 1: empty list; footer "no recipes known yet" (knowledge is group-level and station-independent, Rule 2 — no "here") — never a lock icon
Craft pressedPrimary / Submit on an available recipepanel closes; #11 stance + station fill begins; host commits at craftTimeSeconds
Beat cancelledmovement / dash / damagefill vanishes — true no-op (Rule 7); no cue, no string
Commit race lost (EC1 / EC2)inputs consumed by another commit / withdraw firstfill completes, no thunk, stance ends; on next open the row is ○ affordability (#7 world-family)
Repair pressedPrimary / Submit on a Repair rowinstant, host-confirmed: row disappears, the item's wear accent clears (hotbar E3 / bag strip), one thunk; panel stays open (ruled 2026-08-31)
Repair race (item transferred away mid-press)rarerow disappears without a thunk (#7)
Downedopened while DownedNo change — an ordinary station panel; a crawl during the beat cancels it under Rule 7
Two players at one benchRule 8 — no occupancy lockindependent panels; contention resolves at commit — the loser sees the race-lost state
ExamineInteract on a playerZ2 card; content updates live if they swap gear; closes on range / disconnect
Examine — nothing equippedtarget unarmed / unarmouredrows read "— · unarmed" / "— · no armor"; no mark line
Loading—none: recipes and marks are already-loaded data; affordability is a local read of replicated state

Interaction Map

Mapping for KB/M (primary) and gamepad (partial); UI capture semantics per ADR-0007 §3.

Component Action KB/M Gamepad Feedback Outcome
Recipe / Repair rowFocushover / ↑ ↓UI.Navigate3 px outline + inset tick (#4); detail pane updates instantly; tooltip after 150 ms on the output icon (#5)—
Available recipe rowCraft (the single keyboard / gamepad commit)Enter / click on the rowUI.Submitclick sound; panel closes; stance + station fill (#11, predicted)CraftCommit at the beat's end (host commits at craftTimeSeconds)
Craft button (Z1b)Craft — pointer onlyclick— (not focusable)same as the row; the button always mirrors the focused row, and on KB/M hover == focus (#4), so the row under the pointer and the button's target are the same recipe — no misfire pathsame
Unavailable recipe rowSubmitclick / EnterUI.Submitnothing moves; footer reason string re-asserts (≥ 250 ms, #6)none — no attempt is made
Repair rowRepair (single keyboard / gamepad commit)Enter / clickUI.Submitclick; on host confirm: row gone, thunk, wear accent clearsrepair command
Repair button (Z1b)Repair — pointer onlyclick—mirrors the focused Repair rowsame
Input rowInspecthoverfocus-dwell (rows focusable for tooltip only)tooltip: material name + where it is (bag / chest)—
PanelCloseEscEast / Start100–150 ms fade (#3)—
AnySecondaryRMB (UI.RightClick)—no-op—
ListFocus traversal↑ ↓UI.Navigatelist-only graph (buttons are not focusable) — full adjacency below—
Examine cardnone——read-only; Menu / UI.Cancel closes—

Focus adjacency (complete; no wrap; buttons are outside the graph):

From Up Down Left Right
Repair row nrow n−1; first Repair row → stopnext Repair row; last Repair row → first recipe row if any, else stopstopstop
Recipe row nprevious row; first recipe row → last Repair row if any, else stopnext row; last row → stopstopstop

Numbered order (QA traversal): Repair rows (if any) → recipe rows top to bottom. First focus = first Repair row, else first available recipe, else row 1. Exactly one focused element at all times; the Z1b button is never it. Submit on an unavailable row is a no-attempt (reason re-asserts) — there is no dead end because nothing leaves the list.

Divergence from #2 List Picker, stated: this list's Submit commits rather than selects — the panel has no selected-but-uncommitted state (focus drives the detail pane directly). Recorded in interaction-patterns.md #2 as an allowed variant.


Events Fired

Player Action Event Payload Persistent?
Craft (beat completes)CraftCommit command (ADR-0002 CommandKind.CraftCommit)recipeId, station instanceId, crafter PlayerKeyYes — inputs consumed, output + InstanceMetadata row + gear instanceId counter (ADR-0004)
Repairrepair command — must be a new CommandKind on HostCommitChannel (it mutates host-rail InstanceMetadata.durability; ADR-0002 §1's one-channel invariant; the enum's own comment names Repair as an ADR-0010 extension). Name + payload → ADR-0010 / crafting runtime ADRinstanceTag, station instanceIdYes — durability reset in InstanceMetadata
Beat cancellednone — client-local; the host never received a commit—no
Open / close / focus / examinenone—no
Race lostnone from this client—the host's refusal is the event

No analytics events exist in this project.


Transitions & Animations


Data Requirements

Data Source System Read / Write Notes
Known recipes (RecipeDef[])Crafting IKnownRecipes (host-auth group set, Rule 2) + GameData RecipesReadAll known at MVP; Tier 1 union via Save
Station presenceBase Building IStationPresenceProvider — bool predicates for a supplied tier: stationPresentExact(recipe.station, asker) for craft availability; stationPresentAtLeast(item.tier, asker) per worn item for repair eligibility (crafting Rule 9 "tier or higher")ReadNever cached; the host re-samples at commit. No "tier of this station" accessor exists in BB's contract — the UI never reads one
Station header label ("Workbench · cloth")Client's replicated StructureRegistry copy — nearest station's structureId → StructureDef.stationTierRead, display-onlyBB Rule 13 advisory context; may be one RTT stale; feeds no gate
Availability cause per recipeCrafting F1 legality — provisional client evaluation over replicated stateReadProvisional at open, authoritative at commit (Rule 7); a disagreement resolves as the race-lost honest no-op
have countsInventory — own CARRY (owner-predicted) ∪ ISharedStorage within stationRange (host-confirmed copies)ReadThe storage side may be one RTT stale — stated, not hidden
Worn-gear listCrafting IInstanceMetadata durability via instanceTag for CARRY + equippedReadRepair candidates = worn ∧ stationPresentAtLeast(item.tier)
Craft / repairhost-channel commandsWrite via command onlyThe UI owns nothing
Examine target's gear + marksInventory equipped slots (replicated) + IInstanceMetadataReadMarks are permanent data (never stale-dropped)
craftTimeSeconds, stationRangeGameData Configs.Craft / per-recipe dataReadThe Craft button label reads the recipe's own value
cycleIndex for "N tides ago"ITideClock (presentation clock is fine — display only)Read—

Architectural flag: the provisional legality read and the authoritative commit run the same F1 predicate over different snapshots — this spec depends on the honest no-op, never on the two agreeing.


Accessibility

Tier: Standard.


Localization Considerations

Element English max Layout-critical Priority
Reason strings — the three causes ("no land has offered copper yet")≤ 32 chars in the list (2 lines allowed); ≤ 48 in the footer (one line)list wraps to 2 within the 280 px column; footer one line under 640 pxHIGH
Craft button ("Craft · 2 s")≤ 14 chars after localisationone line in a 200 px buttonHIGH
Recipe names≤ 24 charsone line in a 280 px columnHIGH
Station header ("Workbench · cloth")≤ 24 charsone lineMedium
Input rows ("fiber 12 / 20")material ≤ 12 chars + numbersone lineMedium
Maker's-mark line≤ 48 chars; pluralised template; ≤ 2 linestooltip / Examine card wrapsHIGH (plural rules)
"(worn)", "fresh", "unarmed", "no armor"≤ 10 charsinlineLow

HIGH rows must survive 40 % expansion within their stated line cap.


Acceptance Criteria


Open Questions