# 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`, plus `OnStateChanged` / `OnCompleted` - per-objective: `GetDescription()`, `GetProgress()` (0..1), `IsSatisfied()`, `OnStateChanged` ## Reuse the established pattern Follow `WardrobeScreenWidget` / `WardrobeInventoryWidget` exactly (CommonUI): - Screen = `UCommonActivatableWidget` pushed onto `UGameLayoutWidget`'s `WidgetStack`. - Lists rebuilt from the subsystem on a change delegate; rows are child widgets with click delegates. - Subscribe in `NativeOnActivated`, unsubscribe in `NativeDestruct`. - C++ owns logic + `BindWidget` references; 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` (or `UScrollBox`), optional `UCommonTextBlock* EmptyBoardLabel`. - EditDefaultsOnly: `TSubclassOf EntryWidgetClass`. - `NativeOnActivated`: grab `UMissionSubsystem`, bind `OnBoardChanged → Rebuild`, `OnCommissionCompleted → HandleCompleted` (toast/flash), call `Rebuild()`. - `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 fire `OnBoardChanged`, 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 ObjectiveRowClass`. - `Init(UCommission*, UMissionSubsystem*)`: cache both, subscribe `Commission->OnStateChanged → RefreshActionState`, render header (title/poster/tier), reward summary from `FCommissionReward` (money / XP / followers — hide zero fields), build an objective row per `GetObjectives()`. - 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 `bCompact` flag (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*)`: set `GetDescription()`, subscribe `OnStateChanged → RefreshDone`. - Progress: objectives have **no per-frame progress delegate** — only `OnStateChanged` (fires on satisfied). For the continuous ones (`RequiredHoldSeconds` holds, `RunNakedDistance`) drive the bar from `NativeTick` polling `GetProgress()` **while the screen is open** (cheap, self-contained). Binary objectives just toggle `DoneCheck` on `OnStateChanged`. (If polling ever feels wasteful, add an `OnProgressChanged` delegate to `UCommissionObjective` later — 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 `BindWidget` child of the existing `UHUDWidget` (the always-on HUD), so it shows during normal play and not just at the PC. - BindWidgets: `UVerticalBox* TrackerList`; EditDefaultsOnly `TSubclassOf TrackerEntryWidgetClass`. - `NativeConstruct`: grab `UMissionSubsystem`, bind `OnBoardChanged → Rebuild`, call `Rebuild()`. `NativeDestruct`: unbind. - `Rebuild()`: clear; for each `GetAcceptedCommissions()` add a `UCommissionEntryWidget` in **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 `NativeTick` poll — 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` (or `AApartmentPC`) `IInteractable` → `Interact` calls `HUD->GetGameLayoutWidget()->OpenCommissionBoard()`. - `UGameLayoutWidget`: add `OpenCommissionBoard()` + `TSubclassOf CommissionBoardScreenWidgetClass` + the instance ptr (same shape as `OpenWardrobe`). - 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` → full `Rebuild()`. - **Action state** of a visible entry (e.g. accepted→completed while open): `Commission->OnStateChanged` → `RefreshActionState`; the subsequent `OnBoardChanged` (fired by the subsystem on completion) rebuilds and moves it out of Active. - **Objective progress**: `OnStateChanged` for done state + `NativeTick` poll for the progress bar. - **Completion feedback**: `OnCommissionCompleted` → brief toast / reward popup (reuse existing `UI/` + `Audio/UI` tone). 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) + `OpenCommissionBoard` on `GameLayoutWidget` + 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 the `BindWidget` names above and setting the `…WidgetClass` defaults. `WBP_CommissionEntry` carries 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 `OnBoardChanged` fires 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 → `OnBoardChanged` rebuilds; 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 → Completed` within one frame (drive purely off `GetState()` + the rebuild). ## Locked decisions (slice) 1. **Layout — two columns** (Board | Active), not tabs. Everything visible at once; simplest. 2. **Entry point — apartment PC interactable** (`AForumTerminal`) calls `OpenCommissionBoard()`, plus a debug console exec to open it before the PC art exists. Phone "Forum app" reuses the same call (Phase 8). 3. **Progress display — both:** a progress bar (driven by `NativeTick` polling `GetProgress()`) for the continuous objectives and a checkmark for binary ones, via `BindWidgetOptional` on the row widget. 4. **Completion feedback — minimal toast** on commission completion (reuse existing `UI/` + `Audio/UI`). 5. **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. 6. **Modal behavior — mirror the inventory/wardrobe screens.** The board uses the same input-config / pause setup the existing `UCommonActivatableWidget` screens use; read `UInventoryScreenWidget` / `UWardrobeScreenWidget` (+ their BP activation / `GetDesiredInputConfig` settings) 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) 1. Read `UInventoryScreenWidget` / `UWardrobeScreenWidget` input-config + activation settings and reuse the same convention for the board (decision #6). 2. `UCommissionObjectiveRowWidget` → `UCommissionEntryWidget` (incl. `bCompact`) → `UCommissionBoardScreenWidget`. 3. `UGameLayoutWidget::OpenCommissionBoard()` + class ref; debug console exec to open. 4. `UCommissionTrackerWidget` on `UHUDWidget` (reuses the entry/row widgets in compact mode). 5. Author the `WBP_*` assets (board screen, entry, objective row, tracker) against the BindWidget contracts. 6. `AForumTerminal` interactable (apartment PC) → `OpenCommissionBoard`. 7. Author a `UCommissionBoardConfig` with 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).