Added commissions system

This commit is contained in:
2026-06-01 00:27:56 +03:00
parent a3e23393dc
commit 003d9992e2
81 changed files with 2418 additions and 1065 deletions
+180
View File
@@ -0,0 +1,180 @@
# 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 **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<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`) `IInteractable` → `Interact` 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->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 /
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. `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 VS4 / VS7 exit checks).