Files
Naked-Desire/COMMISSIONS-BOARD.md
2026-06-03 15:17:02 +03:00

12 KiB
Raw Permalink Blame History

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 VS4 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<UCommissionEntryWidget> 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 / dayroll, 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<UCommissionObjectiveRowWidget> 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 AcceptSubsystem->AcceptCommission(Commission).
    • Accepted → label AbandonSubsystem->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<UCommissionEntryWidget> 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) IInteractableInteract calls HUD->GetGameLayoutWidget()->OpenCommissionBoard().
  • UGameLayoutWidget: add OpenCommissionBoard() + TSubclassOf<UCommissionBoardScreenWidget> 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->OnStateChangedRefreshActionState; 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 / dayroll 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. UCommissionObjectiveRowWidgetUCommissionEntryWidget (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 VS4 / VS7 exit checks).