12 KiB
Commission board / accept UI — plan
Implementation plan for the forum commission board UI (GDD §13.1 / §13.2). Not implemented yet —
this is the design to review before coding. Pairs with COMMISSIONS.md (objective backlog) and the
runtime system in Source/NakedDesire/Commissions/ (UMissionSubsystem, UCommission,
UCommissionObjective).
Goal & scope
The forum is the player-facing surface for commissions (§13). For the vertical slice this is the minimal board PLAN VS‑4 calls for: list offered commissions, accept/abandon, and track active ones with live objective progress. No threads, no other-user feed, no profile tab yet (§13 forum scope note).
The runtime already does the work; this is pure presentation + input:
UMissionSubsystem::GetOfferedCommissions()/GetAcceptedCommissions()AcceptCommission(UCommission*)/AbandonCommission(UCommission*)OnBoardChanged(rebuild signal) /OnCommissionCompleted(UCommission*)(feedback)- per-commission:
GetTitle / GetPosterUsername / GetTier / GetReward / GetObjectives / GetState, plusOnStateChanged/OnCompleted - per-objective:
GetDescription(),GetProgress()(0..1),IsSatisfied(),OnStateChanged
Reuse the established pattern
Follow WardrobeScreenWidget / WardrobeInventoryWidget exactly (CommonUI):
- Screen =
UCommonActivatableWidgetpushed ontoUGameLayoutWidget'sWidgetStack. - Lists rebuilt from the subsystem on a change delegate; rows are child widgets with click delegates.
- Subscribe in
NativeOnActivated, unsubscribe inNativeDestruct. - C++ owns logic +
BindWidgetreferences; Blueprint owns layout/visuals (GDD §17.5).
Widget breakdown (new C++ classes, BP subclasses for visuals)
1. UCommissionBoardScreenWidget : UCommonActivatableWidget
The screen. Two sections: Board (offered) and Active (accepted).
- BindWidgets:
UVerticalBox* OfferedList,UVerticalBox* ActiveList(orUScrollBox), optionalUCommonTextBlock* EmptyBoardLabel. - EditDefaultsOnly:
TSubclassOf<UCommissionEntryWidget> EntryWidgetClass. NativeOnActivated: grabUMissionSubsystem, bindOnBoardChanged → Rebuild,OnCommissionCompleted → HandleCompleted(toast/flash), callRebuild().NativeDestruct: unbind both.Rebuild():OfferedList/ActiveList->ClearChildren(); for each commission create an entry widget,Init(Commission, Subsystem), add to the right list. (Membership only changes on accept / complete / abandon / day‑roll, all of which fireOnBoardChanged, so a full rebuild is cheap and correct.)
2. UCommissionEntryWidget : UCommonUserWidget
One commission row.
- BindWidgets:
UCommonTextBlock* TitleText, PosterText, TierText, RewardText,UVerticalBox* ObjectiveList,UCommonButtonBase* ActionButton,UCommonTextBlock* ActionLabel. - EditDefaultsOnly:
TSubclassOf<UCommissionObjectiveRowWidget> ObjectiveRowClass. Init(UCommission*, UMissionSubsystem*): cache both, subscribeCommission->OnStateChanged → RefreshActionState, render header (title/poster/tier), reward summary fromFCommissionReward(money / XP / followers — hide zero fields), build an objective row perGetObjectives().- Action button is state-driven (
Commission->GetState()):Offered→ label Accept →Subsystem->AcceptCommission(Commission).Accepted→ label Abandon →Subsystem->AbandonCommission(Commission).Completed/Expired→ button hidden/disabled (the rebuild usually removes it from the board).
- Compact mode for the HUD tracker: a
bCompactflag (or a BP variant) hides poster / reward / action button and shows just title + objective rows, so the tracker (#4) reuses this same widget. NativeDestruct: unsubscribe from the commission.
3. UCommissionObjectiveRowWidget : UCommonUserWidget
One objective line.
- BindWidgets:
UCommonTextBlock* DescriptionText,UProgressBar* ProgressBar(BindWidgetOptional),UImage* DoneCheck(BindWidgetOptional). Init(UCommissionObjective*): setGetDescription(), subscribeOnStateChanged → RefreshDone.- Progress: objectives have no per-frame progress delegate — only
OnStateChanged(fires on satisfied). For the continuous ones (RequiredHoldSecondsholds,RunNakedDistance) drive the bar fromNativeTickpollingGetProgress()while the screen is open (cheap, self-contained). Binary objectives just toggleDoneCheckonOnStateChanged. (If polling ever feels wasteful, add anOnProgressChangeddelegate toUCommissionObjectivelater — not needed for the slice.)
4. UCommissionTrackerWidget : UCommonUserWidget
Always-on HUD element showing accepted commissions' current objectives + live progress while the player is in the world. Passive HUD — not activatable, never captures input.
- Hosted as a
BindWidgetchild of the existingUHUDWidget(the always-on HUD), so it shows during normal play and not just at the PC. - BindWidgets:
UVerticalBox* TrackerList; EditDefaultsOnlyTSubclassOf<UCommissionEntryWidget> TrackerEntryWidgetClass. NativeConstruct: grabUMissionSubsystem, bindOnBoardChanged → Rebuild, callRebuild().NativeDestruct: unbind.Rebuild(): clear; for eachGetAcceptedCommissions()add aUCommissionEntryWidgetin compact mode (title + objective rows, no reward/poster/button). Collapse the whole widget when the active list is empty.- Live progress comes from the reused objective rows' own
NativeTickpoll — no extra wiring here.
Entry point (how the board opens)
§13 says phone or PC; the phone stack is Phase 8, so for the slice use the apartment PC as an
interactable, mirroring AWardrobe:
- New
AForumTerminal(orAApartmentPC)IInteractable→InteractcallsHUD->GetGameLayoutWidget()->OpenCommissionBoard(). UGameLayoutWidget: addOpenCommissionBoard()+TSubclassOf<UCommissionBoardScreenWidget> CommissionBoardScreenWidgetClass+ the instance ptr (same shape asOpenWardrobe).- Add a debug input action (or console exec) to open it directly for testing before the PC art exists.
- Phone "Forum app" entry point is added in Phase 8 and calls the same
OpenCommissionBoard().
Data flow & live updates
- Membership (which list a commission is in):
OnBoardChanged→ fullRebuild(). - Action state of a visible entry (e.g. accepted→completed while open):
Commission->OnStateChanged→RefreshActionState; the subsequentOnBoardChanged(fired by the subsystem on completion) rebuilds and moves it out of Active. - Objective progress:
OnStateChangedfor done state +NativeTickpoll for the progress bar. - Completion feedback:
OnCommissionCompleted→ brief toast / reward popup (reuse existingUI/+Audio/UItone). Optional for the slice.
Layout sketch
┌─ Commission Board ───────────────────────────────────────────┐
│ BOARD (offered) ACTIVE (accepted) │
│ ┌────────────────────────────┐ ┌──────────────────────┐ │
│ │ Flash a Stranger [Daily]│ │ Beach Streak [Daily] │ │
│ │ posted by sk8r_gurl │ │ ▸ Run 50 m naked 62% │ │
│ │ ▸ Expose boobs to 1 person │ │ ▸ while at the beach │ │
│ │ $150 · 20 XP · +5 followers│ │ [ Abandon ]│ │
│ │ [ Accept ]│ └──────────────────────┘ │
│ └────────────────────────────┘ │
│ … more offered … (empty → "No active │
│ commissions") │
└──────────────────────────────────────────────────────────────┘
(Two columns is the simplest; a Board/Active tab pair is an equivalent alternative — see decisions.)
C++ vs Blueprint split
- C++ (this plan): the four widget classes above (screen, entry, objective row, HUD tracker) +
OpenCommissionBoardonGameLayoutWidget+ the PC interactable. All subsystem calls, subscriptions, list rebuilds, and button handlers live here. - Blueprint:
WBP_CommissionBoardScreen,WBP_CommissionEntry,WBP_CommissionObjectiveRow,WBP_CommissionTracker— visual layout only, satisfying theBindWidgetnames above and setting the…WidgetClassdefaults.WBP_CommissionEntrycarries the full and compact looks (compact = tracker).
Subsystem changes needed
None required — the existing API covers it. Nice-to-haves to consider during build:
- A
GetCompletedCommissions()getter if a "completed today" section is wanted (data already tracked). - Confirm
OnBoardChangedfires on every membership change (it does: accept / abandon / complete / day‑roll all call it).
Edge cases to handle
- Empty board / empty active list → show a label, not a blank panel.
- A commission completing while the screen is open → entry shows done, then rebuild removes it from Active (and the completion toast fires once).
- Day roll while the screen is open →
OnBoardChangedrebuilds; accepted-but-unfinished entries vanish (expired) and a fresh offered set appears. Make sure entry widgets unsubscribe on rebuild (NativeDestruct). - Abandon returns the commission to the board (Offered) in the same session.
- Re-entrancy: accepting an already-satisfiable commission completes instantly; the entry should tolerate
Accepted → Completedwithin one frame (drive purely offGetState()+ the rebuild).
Locked decisions (slice)
- Layout — two columns (Board | Active), not tabs. Everything visible at once; simplest.
- Entry point — apartment PC interactable (
AForumTerminal) callsOpenCommissionBoard(), plus a debug console exec to open it before the PC art exists. Phone "Forum app" reuses the same call (Phase 8). - Progress display — both: a progress bar (driven by
NativeTickpollingGetProgress()) for the continuous objectives and a checkmark for binary ones, viaBindWidgetOptionalon the row widget. - Completion feedback — minimal toast on commission completion (reuse existing
UI/+Audio/UI). - In-world HUD objective tracker — yes, in scope. A separate always-on HUD widget
(
UCommissionTrackerWidget, see breakdown #4) shows accepted commissions' current objectives + live progress while the player is out in the world (GDD §0.5 'objective-tracker widget'). The board is for browse/accept at the PC; the tracker is for following objectives in the field. It reuses the shared entry/row widgets in a compact form, so it adds one widget — not a parallel hierarchy. - Modal behavior — mirror the inventory/wardrobe screens. The board uses the same input-config /
pause setup the existing
UCommonActivatableWidgetscreens use; readUInventoryScreenWidget/UWardrobeScreenWidget(+ their BP activation /GetDesiredInputConfigsettings) and match them for a consistent menu UX. The HUD tracker is not an activatable widget — it's passive HUD and never captures input or pauses anything.
Build order (when greenlit)
- Read
UInventoryScreenWidget/UWardrobeScreenWidgetinput-config + activation settings and reuse the same convention for the board (decision #6). UCommissionObjectiveRowWidget→UCommissionEntryWidget(incl.bCompact) →UCommissionBoardScreenWidget.UGameLayoutWidget::OpenCommissionBoard()+ class ref; debug console exec to open.UCommissionTrackerWidgetonUHUDWidget(reuses the entry/row widgets in compact mode).- Author the
WBP_*assets (board screen, entry, objective row, tracker) against the BindWidget contracts. AForumTerminalinteractable (apartment PC) →OpenCommissionBoard.- Author a
UCommissionBoardConfigwith a few test commissions and play-test the full accept → satisfy → reward → expire loop, plus the HUD tracker updating in the field (ties into the VS‑4 / VS‑7 exit checks).