Added commissions system
This commit is contained in:
@@ -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 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<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 / 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<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 /
|
||||||
|
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).
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
# Commission objective backlog
|
||||||
|
|
||||||
|
Idea catalog + build status for the commission system (`Source/NakedDesire/Commissions/`). Pairs with
|
||||||
|
GDD §13 (the design source of truth) and `PLAN.md` (live status). When an idea is implemented, mark it
|
||||||
|
and record the class name.
|
||||||
|
|
||||||
|
**Feasibility legend**
|
||||||
|
- ✅ buildable now — the signals it needs already exist.
|
||||||
|
- 🔶 needs a planned-but-partial system — NPC types (VS‑5), lust & pulse (VS‑2), photo/livestream &
|
||||||
|
followers (Phase 8), toys, restraints.
|
||||||
|
- 🔭 needs an unbuilt system — theft (§6.3.4), rip & tear (§6.3.5), recognition/wanted (§7.6).
|
||||||
|
|
||||||
|
**Path flavor:** Exhibitionist = be *seen* · Slut = be *lewd* · Slave = be *used / controlled*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structural features (content multipliers — build these first)
|
||||||
|
|
||||||
|
These aren't objectives; they make every objective compose, so a handful of conditions yields a large
|
||||||
|
content space.
|
||||||
|
|
||||||
|
- **Constraints / modifiers** — a constraint decorates an objective; the step only counts while all its
|
||||||
|
constraints also hold (the base ANDs them into the satisfaction check). "Do X **while** Y" with no new
|
||||||
|
objective code.
|
||||||
|
- `WhileObservedBy(count)` ✅ — **`UObservedConstraint`** *(implemented)*
|
||||||
|
- `DuringPhase(Day/Night)` ✅ — **`UDayPhaseConstraint`** *(implemented)*
|
||||||
|
- `WhileWearing(slot)` / `WhileNotWearing(slot)` ✅ — **`UWearingSlotConstraint`** *(implemented)*
|
||||||
|
- `WhileAtLocation(tag)` ✅ — **`ULocationConstraint`** *(implemented)* — backed by `ULocationSubsystem`.
|
||||||
|
- `WhileMoving` / `WhileRunning` ✅* — needs a gait-changed signal or a cheap poll.
|
||||||
|
- **Sequential steps** — `bSequentialObjectives` on a commission: objectives activate one at a time in
|
||||||
|
order ("strip at A → walk naked to B → masturbate at C"). *(implemented)*
|
||||||
|
- **Optional bonus objective** — a non-required step that bumps the reward (risk/reward texture). *(idea)*
|
||||||
|
- **Time limit** — a per-commission deadline is a *failure* condition, not a "while" gate; it belongs with
|
||||||
|
`failurePenalty` / day-end expiry, not the constraint set.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exposure / coverage (mostly ✅ — same signals the dial already uses)
|
||||||
|
|
||||||
|
- **`BeFullyNaked(duration)`** ✅ — **`UBeFullyNakedObjective`** *(implemented)*
|
||||||
|
- **`ExposeBodyPart(part, duration)`** ✅ — **`UExposeBodyPartObjective`** *(implemented)*
|
||||||
|
- **`BeFullyNakedNearNPCs(count, duration)`** ✅ — **`UBeFullyNakedNearNPCsObjective`** *(implemented)*
|
||||||
|
- `WearOnlyUnderwear(duration)` ✅ — **`UWearOnlyUnderwearObjective`** *(implemented)*. Exhibitionist.
|
||||||
|
- `BeToplessOnly` / `BeBottomlessOnly(duration)` ✅ — one region bare, the other covered.
|
||||||
|
- `StayBelowCoverage(threshold, duration)` ✅ — total/region coverage under X (revealing, not nude).
|
||||||
|
- `ExposeWhileWalking(part, meters)` ✅ — keep a part revealing across N meters (per-part walk).
|
||||||
|
- `UseExposeAction(part, count)` ✅ (after VS‑3) — trigger the §6.3.6 flash action N times.
|
||||||
|
|
||||||
|
## Observation / NPC reactions (✅ count-based; 🔶 type-based)
|
||||||
|
|
||||||
|
- `GatherCrowd(count)` ✅ — **`UGatherCrowdObjective`** *(implemented)*. N NPCs observing simultaneously.
|
||||||
|
- `BeObservedWhileExposed(count, duration)` ✅ — **`UBeObservedWhileExposedObjective`** *(implemented)*. N observers while any region is revealing (partial exposure counts).
|
||||||
|
- `StayUnseenWhileNaked(duration)` ✅ — **`UStayUnseenWhileNakedObjective`** *(implemented)*. Fully naked with **zero** observers (stealth exhibitionism / risk).
|
||||||
|
- `BeObservedByNPCType(type, count|duration)` 🔶 (in §13.4) — Walker / Stalker / Blogger / etc.
|
||||||
|
- `BePhotographedByBlogger(count)` 🔶 · `EndureStalker(duration)` 🔶 · `EscapeAfterSnitch()` 🔶 (VS‑5).
|
||||||
|
|
||||||
|
## Attributes: embarrassment / arousal (✅ embarrassment; 🔶 lust/pulse)
|
||||||
|
|
||||||
|
- `ReachEmbarrassment(threshold)` / `SustainEmbarrassment(threshold, duration)` ✅ — **`UReachEmbarrassmentObjective`** *(implemented; a hold duration turns Reach into Sustain)*.
|
||||||
|
- `ReachLust(threshold)` / `EdgeWithoutResolving(duration)` 🔶 (VS‑2).
|
||||||
|
- `MasturbateNearNPCs(count)` / `MasturbateAtLocation(tag)` 🔶 — `PerformAction`, Slut-gated (§23 #26).
|
||||||
|
- `KeepPulseElevated(threshold, duration)` 🔶.
|
||||||
|
|
||||||
|
## Location / travel (✅ — `ULocationSubsystem` now provides enter/leave + tag queries)
|
||||||
|
|
||||||
|
- `EnterLocationNaked(tag)` ✅ · `TourLocationsNaked(tagset)` ✅ · `LingerExposed(tag, duration)` ✅
|
||||||
|
(the last two = `BeFullyNaked` + a `ULocationConstraint`).
|
||||||
|
- `ReachLocationAwayFromClothing(tag, meters)` ✅ — arrive having left clothes ≥N m behind.
|
||||||
|
- `ReturnHomeNaked()` ✅-ish — end the session at the apartment with no clothing (loop-closer).
|
||||||
|
|
||||||
|
## Movement / endurance (✅)
|
||||||
|
|
||||||
|
- `WalkNakedDistance(meters)` ✅ (in §13.4) · `MoveDistanceFromClothing(meters)` ✅ (in §13.4).
|
||||||
|
- `RunNakedDistance(meters)` ✅ — **`URunNakedDistanceObjective`** *(implemented)*. Distance accrues only while running + naked.
|
||||||
|
- `WalkNakedWhileObserved(meters, minObservers)` ✅.
|
||||||
|
|
||||||
|
## Clothing / outfits / restraints
|
||||||
|
|
||||||
|
- `WearOutfit(tag, duration)` ✅ — go out in a specific authored set (needs an outfit id/tag on items).
|
||||||
|
- `GoOutInBodysuitOnly(duration)` ✅.
|
||||||
|
- `WearRestraintInPublic(slot, duration)` 🔶 (restraints §6.3.7) — Slave flavor.
|
||||||
|
- `WearVibratingToy(duration)` / `ActivateToyNearNPCs(count)` 🔶 (toy vibration audible to NPCs).
|
||||||
|
- `DegradeConditionTo(x)` / `RipClothing` 🔭 (rip & tear) · `LetItemBeStolen()` 🔭 (theft).
|
||||||
|
|
||||||
|
## Photo / livestream / followers (🔶 Phase 8)
|
||||||
|
|
||||||
|
- `TakePhotoAtLocation(tag)` 🔶 (in §13.4) · `PostPhotoExposing(part)` 🔶 · `PhotoExposureScoreAbove(x)` 🔶.
|
||||||
|
- `SelfieAtLandmark(tag)` 🔶 · `LivestreamForMinutes(n)` 🔶 · `LivestreamWhileWalkingNaked(meters)` 🔶.
|
||||||
|
- `GainFollowers(count)` 🔶 · `CompleteTipRequests(count)` 🔶.
|
||||||
|
|
||||||
|
## Items / economy (✅ / 🔶)
|
||||||
|
|
||||||
|
- `DeliverItemTo(target)` ✅ (in §13.4) · `DeliverWhileNaked(target)` ✅ — deliver fully naked (§15.1).
|
||||||
|
- `DropClothingAtLocation(tag)` ✅ — a "dead drop" leaving a garment behind.
|
||||||
|
- `SellWornUnderwearAtDropoff()` 🔶 — risk-based drop-off (§15.1).
|
||||||
|
|
||||||
|
## Risk / loss (🔶 / 🔭)
|
||||||
|
|
||||||
|
- `SurviveSessionNaked()` ✅-ish — complete a session never wearing clothing.
|
||||||
|
- `GetCaughtAndEscape()` 🔶 (police, VS‑5) · `WantedWhileExposed()` 🔶 (recognition, §7.6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested build order
|
||||||
|
|
||||||
|
1. **Structural** — constraints framework + sequential steps. *(done)*
|
||||||
|
2. **All-✅ objectives** — done: `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`,
|
||||||
|
`WearOnlyUnderwear`, `RunNakedDistance`, `ReachEmbarrassment`/`SustainEmbarrassment`. Location
|
||||||
|
objectives (`EnterLocationNaked`, `LingerExposed`, …) need no new class — author them as
|
||||||
|
`BeFullyNaked` / `ExposeBodyPart` + a `ULocationConstraint`.
|
||||||
|
3. **Location system** — done: `ULocationSubsystem` + `ULocationConstraint`; the whole Location/travel
|
||||||
|
group and `WhileAtLocation` are unblocked. (Build the location *objectives* with the ✅ batch.)
|
||||||
|
4. **Per-system batches** as VS‑5 (NPC types), VS‑2 (lust/pulse), toys, restraints, and Phase 8
|
||||||
|
(photo/livestream/followers) land.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -90,9 +90,10 @@ State of the C++ module as of the latest pass. File references use `Source/Naked
|
|||||||
- **Equip / unequip / drop event** — `Clothing/ClothingManager.cpp:88-163` (slot-based, broadcasts `OnClothingEquip / Unequip / Dropped` with `UClothingItemInstance*`).
|
- **Equip / unequip / drop event** — `Clothing/ClothingManager.cpp:88-163` (slot-based, broadcasts `OnClothingEquip / Unequip / Dropped` with `UClothingItemInstance*`).
|
||||||
- **Censorship toggle** — compliance feature on `NakedDesireCharacter` (`BoobL/R`, `Front/BackBottom` static meshes), driven by `UNakedDesireUserSettings`.
|
- **Censorship toggle** — compliance feature on `NakedDesireCharacter` (`BoobL/R`, `Front/BackBottom` static meshes), driven by `UNakedDesireUserSettings`.
|
||||||
- **AI sight + behavior tree** — `NPC/NPCAIController.cpp` runs a BT with `Player` / `TargetLocation` / `SpawnLocation` blackboard keys. `NPC/NPCSpawner.cpp` proximity-gated spawn with day / night caps.
|
- **AI sight + behavior tree** — `NPC/NPCAIController.cpp` runs a BT with `Player` / `TargetLocation` / `SpawnLocation` blackboard keys. `NPC/NPCSpawner.cpp` proximity-gated spawn with day / night caps.
|
||||||
- **Mission framework** — `Mission` → `MissionGoal` → `GoalRestriction` composition (`MissionBuilder/`). Two goals (`FlashGoal`, `MinTimeGoal`), three restrictions (`EquipClothing`, `ExposeBodyPart`, `Location`). Iterate-and-mutate bug in `MissionsManager::RefreshDailyMissions` resolved (`MissionsManager.cpp:53-68`).
|
- **Commission system — rebuilt (`Commissions/`, slice-first foundation).** Replaces the old `MissionBuilder/` Goal/Restriction model with the §13.4 vocabulary. `UCommissionObjective` is the unified typed step (owns its condition + an optional `RequiredHoldSeconds` hold timer — "expose for N s" vs "expose once" is a data value, not a class); concrete steps derive from a shared base chain — `UCommissionObjective` → `UObserverObjectiveBase` (reacts to the observer count) → `UCoverageObjectiveBase` (re-evaluates on equip/unequip via `ClothingManager::GetEffectiveCoverage`). Implemented steps: `BeFullyNaked`, `ExposeBodyPart`, `BeFullyNakedNearNPCs`, `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`, `WearOnlyUnderwear`, `RunNakedDistance` (own distance-sampling timer), `ReachEmbarrassment`/`SustainEmbarrassment`. Location objectives (`EnterLocationNaked`, etc.) are authored as a coverage step + `ULocationConstraint` (no new class). `UCommission` is the `Offered→Accepted→Completed/Expired` state machine + `FCommissionReward` (money/XP/followers). `UMissionSubsystem` (`UWorldSubsystem`, like `TimeOfDaySubsystem`) offers a hand-authored `UCommissionBoardConfig` pool (set on `UNakedDesireGameInstance::CommissionBoard`), drives accept/abandon, **pays rewards instantly on completion** (money→save, XP→character; followers stubbed, Phase 8), **expires accepted commissions on `OnDayChanged`**, and persists state to a new `UGlobalSaveGameData::Commissions` bucket (id-keyed, state-level — objective mid-progress not preserved). Added `UStatsManager::GetObserverCount()` + `OnObserversChanged` so "near NPCs" reuses the embarrassment observer set. **Composition:** objectives gate on **constraints** (`UCommissionConstraint` → `UObservedConstraint`, `UDayPhaseConstraint`, `UWearingSlotConstraint`) — "do X while Y" with no new objective code; and a commission can set `bSequentialObjectives` to require its steps in array order (strip → walk → …). The full objective/constraint idea backlog with feasibility tags lives in `COMMISSIONS.md`. **Follow-ups:** no board/commission UI yet (the old forum widget isn't wired to this); `failurePenalty` is a hook only (no reputation/followers yet); procedural generation + path-filtering + the remaining §13.4 step types (`WalkNakedDistance`, `MoveDistanceFromClothing`, `PerformAction`, `BeObservedByNPCType`, `TakePhotoAtLocation`, `DeliverItemTo`) are Phase 7.
|
||||||
|
- **Old mission framework — parked, not deleted.** `MissionBuilder/` (`Mission`/`MissionGoal`/`GoalRestriction`, `FlashGoal`/`MinTimeGoal`, the 3 restrictions, `MissionsConfig`) and the `ANakedDesireCharacter::MissionsManager` component remain on disk but dormant; remove in a cleanup pass once the new system's UI is wired and no Blueprint references the old classes.
|
||||||
- **Daily-mission OOB guarded** — `NakedDesireGameMode::RefreshDailyMissions` now clamps `DaysPassed` to the authored array bounds (`NakedDesireGameMode.cpp:70-71`). Still a hand-authored list (see §1.3).
|
- **Daily-mission OOB guarded** — `NakedDesireGameMode::RefreshDailyMissions` now clamps `DaysPassed` to the authored array bounds (`NakedDesireGameMode.cpp:70-71`). Still a hand-authored list (see §1.3).
|
||||||
- **Location triggers** — `Locations/LocationTrigger`, `Locations/LocationData` via gameplay tags (GDD §10.4 area foundation). `ALocationTrigger` now carries a `bIsApartment` flag; when set, its box overlap drives session start / end on the session subsystem.
|
- **Location system (GDD §10.4) — unified on `ULocationSubsystem`.** `Locations/LocationSubsystem` (`UWorldSubsystem`) is the single authority on which tagged locations the player occupies: `ALocationTrigger` volumes report player enter/leave (ref-counted, so overlapping boxes of one place don't churn), and it exposes `IsPlayerInLocation(tag)` (hierarchical via `FGameplayTagContainer::HasTag`), `GetCurrentLocation()`, and `OnLocationEntered/Exited`. Locations are identified by `ULocationData::Tag` and **nest** (inside `Location.City.Beach` you also match `Location.City`). `bIsApartment` is **gone**: `USessionManagerSubsystem` now subscribes to the subsystem and starts/ends the session on the native tag `TAG_Location_Apartment` (`Global/NakedDesireGameplayTags`). Commission location gating is `Commissions/Constraints/LocationConstraint`. **Content requirement:** the apartment trigger's `ULocationData` must be tagged `Location.Apartment` (or a child); each trigger box needs overlap-with-Pawn collision.
|
||||||
- **Session manager (GDD §4.1–§4.4)** — `Global/SessionManagerSubsystem.h/.cpp` (`UWorldSubsystem`). Tracks `bSessionActive`, emits `OnSessionStart` / `OnSessionEnd(ESessionLossCause)`; `ESessionLossCause = { SafeReturn, EmbarrassmentMax, EnergyZero, PoliceCapture }`. Apartment `ALocationTrigger` starts a session on exit and safely ends it on re-entry. Subscribes (next-tick after world begin play) to `UStatsManager::EmbarrassmentUpdate` (max-hit → `EmbarrassmentMax`) and `EnergyUpdate` (≤0 → `EnergyZero`). Exposes `bPoliceChaseActive` (+ setter / getter) for the §4.4 loss-precedence rule the resolver owns. Replaces the old `EndGameEmbarrassed` GameMode BP call, which `UStatsManager::IncreaseEmbarrassment` no longer invokes. The `EndGameEmbarrassed` BlueprintImplementableEvent declaration still exists on `ANakedDesireGameMode` but is now dead from C++ and should be removed once BP no longer references it.
|
- **Session manager (GDD §4.1–§4.4)** — `Global/SessionManagerSubsystem.h/.cpp` (`UWorldSubsystem`). Tracks `bSessionActive`, emits `OnSessionStart` / `OnSessionEnd(ESessionLossCause)`; `ESessionLossCause = { SafeReturn, EmbarrassmentMax, EnergyZero, PoliceCapture }`. Apartment `ALocationTrigger` starts a session on exit and safely ends it on re-entry. Subscribes (next-tick after world begin play) to `UStatsManager::EmbarrassmentUpdate` (max-hit → `EmbarrassmentMax`) and `EnergyUpdate` (≤0 → `EnergyZero`). Exposes `bPoliceChaseActive` (+ setter / getter) for the §4.4 loss-precedence rule the resolver owns. Replaces the old `EndGameEmbarrassed` GameMode BP call, which `UStatsManager::IncreaseEmbarrassment` no longer invokes. The `EndGameEmbarrassed` BlueprintImplementableEvent declaration still exists on `ANakedDesireGameMode` but is now dead from C++ and should be removed once BP no longer references it.
|
||||||
- **Session loss resolver (GDD §4.4)** — `Global/SessionLossResolver.h/.cpp` (`UWorldSubsystem`). Single entry point `ResolveLoss(ESessionLossCause)`, bound to `USessionManagerSubsystem::OnSessionEnd`. Applies the police-chase precedence override (any cause → `PoliceCapture` while `bPoliceChaseActive`), then per cause: `EmbarrassmentMax` no-cost; `EnergyZero` destroys every world `AItemPickup` + clears its save record (guaranteed sleep loss); `PoliceCapture` deducts `PoliceCaptureMoneyPenalty` if affordable else flags a holding-cell outcome; `SafeReturn` no loss. Never strips equipped clothing. Autosaves, then broadcasts `OnSessionLossResolved(FinalCause, bWentToHoldingCell)` for the BP presentation / time-skip layer. See §1.3 for the pieces still delegated to BP / later phases.
|
- **Session loss resolver (GDD §4.4)** — `Global/SessionLossResolver.h/.cpp` (`UWorldSubsystem`). Single entry point `ResolveLoss(ESessionLossCause)`, bound to `USessionManagerSubsystem::OnSessionEnd`. Applies the police-chase precedence override (any cause → `PoliceCapture` while `bPoliceChaseActive`), then per cause: `EmbarrassmentMax` no-cost; `EnergyZero` destroys every world `AItemPickup` + clears its save record (guaranteed sleep loss); `PoliceCapture` deducts `PoliceCaptureMoneyPenalty` if affordable else flags a holding-cell outcome; `SafeReturn` no loss. Never strips equipped clothing. Autosaves, then broadcasts `OnSessionLossResolved(FinalCause, bWentToHoldingCell)` for the BP presentation / time-skip layer. See §1.3 for the pieces still delegated to BP / later phases.
|
||||||
- **Movement** — `EnhancedInput`, walk / run / crouch (`NakedDesireCharacter.cpp:115-127`), stamina-gated run (`Tick` lines 91-113).
|
- **Movement** — `EnhancedInput`, walk / run / crouch (`NakedDesireCharacter.cpp:115-127`), stamina-gated run (`Tick` lines 91-113).
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "Commission.h"
|
||||||
|
|
||||||
|
#include "CommissionObjective.h"
|
||||||
|
|
||||||
|
void UCommission::Accept(ANakedDesireCharacter* InPlayer)
|
||||||
|
{
|
||||||
|
if (State != ECommissionState::Offered)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Player = InPlayer;
|
||||||
|
SetState(ECommissionState::Accepted);
|
||||||
|
ArmObjectives();
|
||||||
|
|
||||||
|
// Arming evaluates immediately, so a commission already satisfied on accept completes now.
|
||||||
|
if (AreAllObjectivesSatisfied())
|
||||||
|
Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::Abandon()
|
||||||
|
{
|
||||||
|
if (State != ECommissionState::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DisarmObjectives();
|
||||||
|
SetState(ECommissionState::Offered);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::Expire()
|
||||||
|
{
|
||||||
|
if (State != ECommissionState::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DisarmObjectives();
|
||||||
|
SetState(ECommissionState::Expired);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::RestoreState(ECommissionState SavedState, ANakedDesireCharacter* InPlayer)
|
||||||
|
{
|
||||||
|
Player = InPlayer;
|
||||||
|
|
||||||
|
if (SavedState == ECommissionState::Accepted)
|
||||||
|
{
|
||||||
|
SetState(ECommissionState::Accepted);
|
||||||
|
ArmObjectives();
|
||||||
|
if (AreAllObjectivesSatisfied())
|
||||||
|
Complete();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetState(SavedState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::Complete()
|
||||||
|
{
|
||||||
|
if (State != ECommissionState::Accepted)
|
||||||
|
return; // already resolved — guards against double completion / double payout
|
||||||
|
|
||||||
|
DisarmObjectives();
|
||||||
|
SetState(ECommissionState::Completed);
|
||||||
|
OnCompleted.Broadcast(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UCommission::AreAllObjectivesSatisfied() const
|
||||||
|
{
|
||||||
|
if (Objectives.Num() == 0)
|
||||||
|
return false; // a commission with no objectives never auto-completes
|
||||||
|
|
||||||
|
for (const UCommissionObjective* Objective : Objectives)
|
||||||
|
{
|
||||||
|
if (!Objective || !Objective->IsSatisfied())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::ArmObjectives()
|
||||||
|
{
|
||||||
|
// Subscribe to all objectives up front; activation differs by mode.
|
||||||
|
for (UCommissionObjective* Objective : Objectives)
|
||||||
|
{
|
||||||
|
if (Objective)
|
||||||
|
Objective->OnStateChanged.AddUObject(this, &UCommission::HandleObjectiveStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bSequentialObjectives)
|
||||||
|
{
|
||||||
|
ActiveObjectiveIndex = INDEX_NONE;
|
||||||
|
AdvanceSequential();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (UCommissionObjective* Objective : Objectives)
|
||||||
|
{
|
||||||
|
if (Objective)
|
||||||
|
Objective->Activate(Player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::DisarmObjectives()
|
||||||
|
{
|
||||||
|
for (UCommissionObjective* Objective : Objectives)
|
||||||
|
{
|
||||||
|
if (!Objective)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Objective->OnStateChanged.RemoveAll(this);
|
||||||
|
Objective->Deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveObjectiveIndex = INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::AdvanceSequential()
|
||||||
|
{
|
||||||
|
for (int32 Index = 0; Index < Objectives.Num(); ++Index)
|
||||||
|
{
|
||||||
|
UCommissionObjective* Objective = Objectives[Index];
|
||||||
|
if (!Objective)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!Objective->IsSatisfied())
|
||||||
|
{
|
||||||
|
// Activate the first unsatisfied step (only if it isn't already the armed one — Activate
|
||||||
|
// resets progress, so re-activating the live objective would wipe its hold timer).
|
||||||
|
if (ActiveObjectiveIndex != Index)
|
||||||
|
{
|
||||||
|
ActiveObjectiveIndex = Index;
|
||||||
|
Objective->Activate(Player);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveObjectiveIndex = INDEX_NONE;
|
||||||
|
Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::SetState(ECommissionState NewState)
|
||||||
|
{
|
||||||
|
State = NewState;
|
||||||
|
OnStateChanged.Broadcast(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommission::HandleObjectiveStateChanged(UCommissionObjective* Objective)
|
||||||
|
{
|
||||||
|
if (State != ECommissionState::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (bSequentialObjectives)
|
||||||
|
AdvanceSequential(); // the armed step finished -> activate the next, or complete
|
||||||
|
else if (AreAllObjectivesSatisfied())
|
||||||
|
Complete();
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "CommissionTypes.h"
|
||||||
|
#include "Commission.generated.h"
|
||||||
|
|
||||||
|
class UCommissionObjective;
|
||||||
|
class ANakedDesireCharacter;
|
||||||
|
class UCommission;
|
||||||
|
|
||||||
|
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCommissionChangedSignature, UCommission*);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A commission (GDD §13). Authored inline in a UCommissionBoardConfig and duplicated to a runtime
|
||||||
|
* instance by UMissionSubsystem; the duplicate owns the state machine and its live objectives.
|
||||||
|
* Completes when every objective reports satisfied; the subsystem pays the reward and persists state.
|
||||||
|
*/
|
||||||
|
UCLASS(EditInlineNew, BlueprintType)
|
||||||
|
class NAKEDDESIRE_API UCommission : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
// --- Authoring ---
|
||||||
|
// Stable id used for save/restore (must be unique within the board config).
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
FName CommissionId;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
FText Title;
|
||||||
|
|
||||||
|
// Lore-only forum poster (§13.3) — no gameplay effect.
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
FText PosterUsername;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
ECommissionTier Tier = ECommissionTier::Daily;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
FCommissionReward Reward;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, Instanced)
|
||||||
|
TArray<UCommissionObjective*> Objectives;
|
||||||
|
|
||||||
|
// When true, objectives activate one at a time in array order (strip at A -> walk to B -> ... ).
|
||||||
|
// When false (default), all objectives are active at once and may be satisfied in any order.
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
bool bSequentialObjectives = false;
|
||||||
|
|
||||||
|
// --- Runtime ---
|
||||||
|
FOnCommissionChangedSignature OnStateChanged; // any transition
|
||||||
|
FOnCommissionChangedSignature OnCompleted; // -> Completed
|
||||||
|
|
||||||
|
ECommissionState GetState() const { return State; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure) ECommissionTier GetTier() const { return Tier; }
|
||||||
|
UFUNCTION(BlueprintPure) FText GetTitle() const { return Title; }
|
||||||
|
UFUNCTION(BlueprintPure) FText GetPosterUsername() const { return PosterUsername; }
|
||||||
|
UFUNCTION(BlueprintPure) FCommissionReward GetReward() const { return Reward; }
|
||||||
|
UFUNCTION(BlueprintPure) TArray<UCommissionObjective*> GetObjectives() const { return Objectives; }
|
||||||
|
|
||||||
|
void Accept(ANakedDesireCharacter* InPlayer); // Offered -> Accepted; arms objectives
|
||||||
|
void Abandon(); // Accepted -> Offered; no penalty (§13.2)
|
||||||
|
void Expire(); // Accepted -> Expired (deadline)
|
||||||
|
|
||||||
|
// Re-apply a persisted state on load (re-arms objectives when restoring Accepted).
|
||||||
|
void RestoreState(ECommissionState SavedState, ANakedDesireCharacter* InPlayer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Complete();
|
||||||
|
bool AreAllObjectivesSatisfied() const;
|
||||||
|
void ArmObjectives();
|
||||||
|
void DisarmObjectives();
|
||||||
|
void AdvanceSequential(); // activate the first unsatisfied objective, or complete if none remain
|
||||||
|
void SetState(ECommissionState NewState);
|
||||||
|
|
||||||
|
void HandleObjectiveStateChanged(UCommissionObjective* Objective);
|
||||||
|
|
||||||
|
ECommissionState State = ECommissionState::Offered;
|
||||||
|
int32 ActiveObjectiveIndex = INDEX_NONE; // sequential mode: the objective currently armed
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
ANakedDesireCharacter* Player = nullptr;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "CommissionBoardConfig.generated.h"
|
||||||
|
|
||||||
|
class UCommission;
|
||||||
|
|
||||||
|
// Hand-authored commission pool (GDD §13.4 procedural generation is Phase 7). For the slice the board
|
||||||
|
// simply offers everything authored here; the daily/weekly split lives on each commission's Tier.
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class NAKEDDESIRE_API UCommissionBoardConfig : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(EditDefaultsOnly, Instanced)
|
||||||
|
TArray<UCommission*> Commissions;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "CommissionConstraint.h"
|
||||||
|
|
||||||
|
void UCommissionConstraint::Activate(ANakedDesireCharacter* InPlayer)
|
||||||
|
{
|
||||||
|
Player = InPlayer;
|
||||||
|
OnActivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionConstraint::Deactivate()
|
||||||
|
{
|
||||||
|
OnDeactivate();
|
||||||
|
Player = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UCommissionConstraint::GetDescription() const
|
||||||
|
{
|
||||||
|
return FText::GetEmpty();
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "CommissionConstraint.generated.h"
|
||||||
|
|
||||||
|
class ANakedDesireCharacter;
|
||||||
|
|
||||||
|
DECLARE_MULTICAST_DELEGATE(FOnConstraintChangedSignature);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "while Y" gate attached to a UCommissionObjective. The objective only counts progress while every
|
||||||
|
* constraint reports IsMet(). A constraint subscribes to whatever world signal it watches and fires
|
||||||
|
* OnConstraintChanged so the owning objective re-evaluates. Constraints compose with any objective, so
|
||||||
|
* "expose boobs" + "while at the beach" + "during night" is data, not new objective code (see §13.4).
|
||||||
|
*/
|
||||||
|
UCLASS(Abstract, EditInlineNew, BlueprintType)
|
||||||
|
class NAKEDDESIRE_API UCommissionConstraint : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
FOnConstraintChangedSignature OnConstraintChanged;
|
||||||
|
|
||||||
|
void Activate(ANakedDesireCharacter* InPlayer);
|
||||||
|
void Deactivate();
|
||||||
|
|
||||||
|
virtual bool IsMet() const { return true; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
virtual FText GetDescription() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY()
|
||||||
|
ANakedDesireCharacter* Player = nullptr;
|
||||||
|
|
||||||
|
virtual void OnActivate() {}
|
||||||
|
virtual void OnDeactivate() {}
|
||||||
|
|
||||||
|
// Subclasses call this when the thing they watch changes; the owning objective re-evaluates.
|
||||||
|
void NotifyChanged() { OnConstraintChanged.Broadcast(); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "CommissionObjective.h"
|
||||||
|
|
||||||
|
#include "TimerManager.h"
|
||||||
|
#include "CommissionConstraint.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
void UCommissionObjective::Activate(ANakedDesireCharacter* InPlayer)
|
||||||
|
{
|
||||||
|
Player = InPlayer;
|
||||||
|
bSatisfied = false;
|
||||||
|
|
||||||
|
OnActivate();
|
||||||
|
|
||||||
|
for (UCommissionConstraint* Constraint : Constraints)
|
||||||
|
{
|
||||||
|
if (!Constraint)
|
||||||
|
continue;
|
||||||
|
Constraint->OnConstraintChanged.AddUObject(this, &UCommissionObjective::HandleConstraintChanged);
|
||||||
|
Constraint->Activate(Player);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyConditionChanged(); // evaluate the starting state (e.g. already naked on accept)
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionObjective::Deactivate()
|
||||||
|
{
|
||||||
|
ClearHoldTimer();
|
||||||
|
|
||||||
|
for (UCommissionConstraint* Constraint : Constraints)
|
||||||
|
{
|
||||||
|
if (!Constraint)
|
||||||
|
continue;
|
||||||
|
Constraint->OnConstraintChanged.RemoveAll(this);
|
||||||
|
Constraint->Deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
OnDeactivate();
|
||||||
|
Player = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UCommissionObjective::AreConstraintsMet() const
|
||||||
|
{
|
||||||
|
for (const UCommissionConstraint* Constraint : Constraints)
|
||||||
|
{
|
||||||
|
if (Constraint && !Constraint->IsMet())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionObjective::HandleConstraintChanged()
|
||||||
|
{
|
||||||
|
NotifyConditionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionObjective::NotifyConditionChanged()
|
||||||
|
{
|
||||||
|
if (bSatisfied || !Player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsConditionMet() && AreConstraintsMet())
|
||||||
|
{
|
||||||
|
if (RequiredHoldSeconds <= 0.0f)
|
||||||
|
{
|
||||||
|
MarkSatisfied();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HoldTimerHandle.IsValid())
|
||||||
|
{
|
||||||
|
HoldStartTime = Player->GetWorld()->GetTimeSeconds();
|
||||||
|
Player->GetWorldTimerManager().SetTimer(HoldTimerHandle, this, &UCommissionObjective::OnHoldElapsed, RequiredHoldSeconds, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClearHoldTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionObjective::OnHoldElapsed()
|
||||||
|
{
|
||||||
|
MarkSatisfied();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionObjective::MarkSatisfied()
|
||||||
|
{
|
||||||
|
if (bSatisfied)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bSatisfied = true;
|
||||||
|
ClearHoldTimer();
|
||||||
|
OnStateChanged.Broadcast(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCommissionObjective::ClearHoldTimer()
|
||||||
|
{
|
||||||
|
if (HoldTimerHandle.IsValid() && Player)
|
||||||
|
Player->GetWorldTimerManager().ClearTimer(HoldTimerHandle);
|
||||||
|
|
||||||
|
HoldTimerHandle.Invalidate();
|
||||||
|
HoldStartTime = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UCommissionObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
return FText::GetEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
float UCommissionObjective::GetProgress() const
|
||||||
|
{
|
||||||
|
if (bSatisfied)
|
||||||
|
return 1.0f;
|
||||||
|
|
||||||
|
if (HoldTimerHandle.IsValid() && Player && RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
const float Elapsed = Player->GetWorld()->GetTimeSeconds() - HoldStartTime;
|
||||||
|
return FMath::Clamp(Elapsed / RequiredHoldSeconds, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "CommissionObjective.generated.h"
|
||||||
|
|
||||||
|
class ANakedDesireCharacter;
|
||||||
|
class UCommissionObjective;
|
||||||
|
class UCommissionConstraint;
|
||||||
|
|
||||||
|
DECLARE_MULTICAST_DELEGATE_OneParam(FOnObjectiveStateChangedSignature, UCommissionObjective*);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One typed objective step (GDD §13.4). Replaces the old Goal/Restriction split: a step owns its own
|
||||||
|
* condition and, optionally, a sustained-hold requirement (RequiredHoldSeconds). Subclasses override
|
||||||
|
* IsConditionMet() and call NotifyConditionChanged() when their inputs change; count-style objectives
|
||||||
|
* can instead call MarkSatisfied() directly. The hold timer lives here, so "expose for N seconds" and
|
||||||
|
* "expose once" differ only by a data value, not a class.
|
||||||
|
*/
|
||||||
|
UCLASS(Abstract, EditInlineNew, BlueprintType)
|
||||||
|
class NAKEDDESIRE_API UCommissionObjective : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
FOnObjectiveStateChangedSignature OnStateChanged;
|
||||||
|
|
||||||
|
void Activate(ANakedDesireCharacter* InPlayer);
|
||||||
|
void Deactivate();
|
||||||
|
|
||||||
|
bool IsSatisfied() const { return bSatisfied; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
virtual FText GetDescription() const;
|
||||||
|
|
||||||
|
// 0..1 for UI. Default: 1 when satisfied, partial while a hold timer runs, else 0.
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
virtual float GetProgress() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY()
|
||||||
|
ANakedDesireCharacter* Player = nullptr;
|
||||||
|
|
||||||
|
// Continuous seconds the condition must hold to satisfy. 0 = satisfied the instant it is first met.
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0))
|
||||||
|
float RequiredHoldSeconds = 0.0f;
|
||||||
|
|
||||||
|
// "While Y" gates: the objective only counts progress while every constraint reports IsMet().
|
||||||
|
UPROPERTY(EditDefaultsOnly, Instanced)
|
||||||
|
TArray<TObjectPtr<UCommissionConstraint>> Constraints;
|
||||||
|
|
||||||
|
virtual void OnActivate() {}
|
||||||
|
virtual void OnDeactivate() {}
|
||||||
|
|
||||||
|
// Sustained objectives override this; the base ANDs it with the constraints to drive the hold timer.
|
||||||
|
virtual bool IsConditionMet() const { return false; }
|
||||||
|
|
||||||
|
// True when every attached constraint is currently satisfied (count-style subclasses gate on this too).
|
||||||
|
bool AreConstraintsMet() const;
|
||||||
|
|
||||||
|
// Re-check the condition and (re)arm or cancel the hold timer accordingly.
|
||||||
|
void NotifyConditionChanged();
|
||||||
|
|
||||||
|
void MarkSatisfied();
|
||||||
|
|
||||||
|
bool bSatisfied = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleConstraintChanged();
|
||||||
|
void OnHoldElapsed();
|
||||||
|
void ClearHoldTimer();
|
||||||
|
|
||||||
|
FTimerHandle HoldTimerHandle;
|
||||||
|
float HoldStartTime = 0.0f;
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CommissionTypes.generated.h"
|
||||||
|
|
||||||
|
// Daily vs. weekly draw from the same model; the tier only changes reward bands / step depth (§13.4).
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ECommissionTier : uint8
|
||||||
|
{
|
||||||
|
Daily,
|
||||||
|
Weekly
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle state (GDD §13.1 / §13.2). Offered commissions on the board carry no obligation until accepted.
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ECommissionState : uint8
|
||||||
|
{
|
||||||
|
Offered, // on the board, not committed
|
||||||
|
Accepted, // committed; objectives armed
|
||||||
|
Completed, // all objectives satisfied; rewards paid
|
||||||
|
Expired // accepted but not finished by the deadline
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reward paid instantly on completion (§23 #23). Money/XP exist today; followers land with Phase 8.
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct NAKEDDESIRE_API FCommissionReward
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||||
|
int32 Money = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||||
|
float XP = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||||
|
int32 Followers = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Persisted commission state, keyed by the authored CommissionId. State-level only — objective
|
||||||
|
// mid-progress is not saved; an accepted commission re-arms from the start on load.
|
||||||
|
USTRUCT()
|
||||||
|
struct NAKEDDESIRE_API FCommissionSaveRecord
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(SaveGame)
|
||||||
|
FName CommissionId;
|
||||||
|
|
||||||
|
UPROPERTY(SaveGame)
|
||||||
|
ECommissionState State = ECommissionState::Offered;
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "DayPhaseConstraint.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Constraints.DayPhase"
|
||||||
|
|
||||||
|
void UDayPhaseConstraint::OnActivate()
|
||||||
|
{
|
||||||
|
if (UTimeOfDaySubsystem* Time = GetTime())
|
||||||
|
Time->OnPhaseChanged.AddUniqueDynamic(this, &UDayPhaseConstraint::HandlePhaseChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDayPhaseConstraint::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (UTimeOfDaySubsystem* Time = GetTime())
|
||||||
|
Time->OnPhaseChanged.RemoveDynamic(this, &UDayPhaseConstraint::HandlePhaseChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDayPhaseConstraint::IsMet() const
|
||||||
|
{
|
||||||
|
const UTimeOfDaySubsystem* Time = GetTime();
|
||||||
|
return Time && Time->GetPhase() == RequiredPhase;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDayPhaseConstraint::HandlePhaseChanged(EDayPhase NewPhase)
|
||||||
|
{
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTimeOfDaySubsystem* UDayPhaseConstraint::GetTime() const
|
||||||
|
{
|
||||||
|
UWorld* World = Player ? Player->GetWorld() : nullptr;
|
||||||
|
return World ? World->GetSubsystem<UTimeOfDaySubsystem>() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UDayPhaseConstraint::GetDescription() const
|
||||||
|
{
|
||||||
|
return (RequiredPhase == EDayPhase::Night)
|
||||||
|
? LOCTEXT("Night", "at night")
|
||||||
|
: LOCTEXT("Day", "during the day");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionConstraint.h"
|
||||||
|
#include "NakedDesire/Global/TimeOfDaySubsystem.h"
|
||||||
|
#include "DayPhaseConstraint.generated.h"
|
||||||
|
|
||||||
|
// Holds only during the chosen day phase (§10.1).
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "During Day Phase")
|
||||||
|
class NAKEDDESIRE_API UDayPhaseConstraint : public UCommissionConstraint
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool IsMet() const override;
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
EDayPhase RequiredPhase = EDayPhase::Night;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandlePhaseChanged(EDayPhase NewPhase);
|
||||||
|
|
||||||
|
UTimeOfDaySubsystem* GetTime() const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "LocationConstraint.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Locations/LocationSubsystem.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Constraints.Location"
|
||||||
|
|
||||||
|
void ULocationConstraint::OnActivate()
|
||||||
|
{
|
||||||
|
if (ULocationSubsystem* Locations = GetLocations())
|
||||||
|
{
|
||||||
|
Locations->OnLocationEntered.AddUniqueDynamic(this, &ULocationConstraint::HandleLocationChanged);
|
||||||
|
Locations->OnLocationExited.AddUniqueDynamic(this, &ULocationConstraint::HandleLocationChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ULocationConstraint::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (ULocationSubsystem* Locations = GetLocations())
|
||||||
|
{
|
||||||
|
Locations->OnLocationEntered.RemoveDynamic(this, &ULocationConstraint::HandleLocationChanged);
|
||||||
|
Locations->OnLocationExited.RemoveDynamic(this, &ULocationConstraint::HandleLocationChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ULocationConstraint::IsMet() const
|
||||||
|
{
|
||||||
|
const ULocationSubsystem* Locations = GetLocations();
|
||||||
|
return Locations && Locations->IsPlayerInLocation(RequiredLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ULocationConstraint::HandleLocationChanged(ULocationData* Location)
|
||||||
|
{
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
ULocationSubsystem* ULocationConstraint::GetLocations() const
|
||||||
|
{
|
||||||
|
UWorld* World = Player ? Player->GetWorld() : nullptr;
|
||||||
|
return World ? World->GetSubsystem<ULocationSubsystem>() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText ULocationConstraint::GetDescription() const
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("AtLocation", "while at {0}"), FText::FromName(RequiredLocation.GetTagName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionConstraint.h"
|
||||||
|
#include "LocationConstraint.generated.h"
|
||||||
|
|
||||||
|
class ULocationData;
|
||||||
|
class ULocationSubsystem;
|
||||||
|
|
||||||
|
// Holds while the player occupies a location matching (or nesting under) RequiredLocation.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "While At Location")
|
||||||
|
class NAKEDDESIRE_API ULocationConstraint : public UCommissionConstraint
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool IsMet() const override;
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
FGameplayTag RequiredLocation;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleLocationChanged(ULocationData* Location);
|
||||||
|
|
||||||
|
ULocationSubsystem* GetLocations() const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "ObservedConstraint.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
#include "NakedDesire/Stats/StatsManager.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Constraints.Observed"
|
||||||
|
|
||||||
|
void UObservedConstraint::OnActivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->StatsManager)
|
||||||
|
Player->StatsManager->OnObserversChanged.AddUniqueDynamic(this, &UObservedConstraint::HandleObserversChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UObservedConstraint::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->StatsManager)
|
||||||
|
Player->StatsManager->OnObserversChanged.RemoveDynamic(this, &UObservedConstraint::HandleObserversChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UObservedConstraint::IsMet() const
|
||||||
|
{
|
||||||
|
if (!Player || !Player->StatsManager)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Player->StatsManager->GetObserverCount() >= MinObservers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UObservedConstraint::HandleObserversChanged()
|
||||||
|
{
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UObservedConstraint::GetDescription() const
|
||||||
|
{
|
||||||
|
return (MinObservers == 1)
|
||||||
|
? LOCTEXT("Single", "while someone is watching")
|
||||||
|
: FText::Format(LOCTEXT("Multiple", "while {0} people are watching"), FText::AsNumber(MinObservers));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionConstraint.h"
|
||||||
|
#include "ObservedConstraint.generated.h"
|
||||||
|
|
||||||
|
// Holds only while at least MinObservers NPCs are perceiving the player (the embarrassment observer set).
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "While Observed By N NPCs")
|
||||||
|
class NAKEDDESIRE_API UObservedConstraint : public UCommissionConstraint
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool IsMet() const override;
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
|
||||||
|
int32 MinObservers = 1;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleObserversChanged();
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "WearingSlotConstraint.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Clothing/ClothingManager.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Constraints.WearingSlot"
|
||||||
|
|
||||||
|
void UWearingSlotConstraint::OnActivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->ClothingManager)
|
||||||
|
{
|
||||||
|
Player->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
|
||||||
|
Player->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWearingSlotConstraint::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->ClothingManager)
|
||||||
|
{
|
||||||
|
Player->ClothingManager->OnClothingEquip.RemoveDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
|
||||||
|
Player->ClothingManager->OnClothingUnequip.RemoveDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWearingSlotConstraint::IsMet() const
|
||||||
|
{
|
||||||
|
if (!Player || !Player->ClothingManager)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Player->ClothingManager->IsClothingTypeOn(Slot) == bMustBeWorn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWearingSlotConstraint::HandleClothingChanged(UClothingItemInstance* ClothingItemInstance)
|
||||||
|
{
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UWearingSlotConstraint::GetDescription() const
|
||||||
|
{
|
||||||
|
return bMustBeWorn
|
||||||
|
? LOCTEXT("Wearing", "while dressed in the required item")
|
||||||
|
: LOCTEXT("NotWearing", "while that slot is bare");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Clothing/ClothingSlotType.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionConstraint.h"
|
||||||
|
#include "WearingSlotConstraint.generated.h"
|
||||||
|
|
||||||
|
class UClothingItemInstance;
|
||||||
|
|
||||||
|
// Holds while the chosen slot is occupied (bMustBeWorn) or empty (!bMustBeWorn). Covers both
|
||||||
|
// "while wearing a coat" (Outerwear occupied) and "while not wearing a top" (Top empty).
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "While Wearing / Not Wearing Slot")
|
||||||
|
class NAKEDDESIRE_API UWearingSlotConstraint : public UCommissionConstraint
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool IsMet() const override;
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
EClothingSlotType Slot = EClothingSlotType::Outerwear;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
bool bMustBeWorn = true;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleClothingChanged(UClothingItemInstance* ClothingItemInstance);
|
||||||
|
};
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "MissionSubsystem.h"
|
||||||
|
|
||||||
|
#include "Commission.h"
|
||||||
|
#include "CommissionBoardConfig.h"
|
||||||
|
#include "CommissionTypes.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "NakedDesire/Global/NakedDesireGameInstance.h"
|
||||||
|
#include "NakedDesire/Global/TimeOfDaySubsystem.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
|
||||||
|
#include "NakedDesire/SaveGame/SaveSubsystem.h"
|
||||||
|
|
||||||
|
void UMissionSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||||
|
{
|
||||||
|
Super::OnWorldBeginPlay(InWorld);
|
||||||
|
|
||||||
|
if (UTimeOfDaySubsystem* Time = InWorld.GetSubsystem<UTimeOfDaySubsystem>())
|
||||||
|
Time->OnDayChanged.AddUniqueDynamic(this, &UMissionSubsystem::HandleDayChanged);
|
||||||
|
|
||||||
|
BuildBoard();
|
||||||
|
RestoreFromSave();
|
||||||
|
OnBoardChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::Deinitialize()
|
||||||
|
{
|
||||||
|
if (const UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
if (UTimeOfDaySubsystem* Time = World->GetSubsystem<UTimeOfDaySubsystem>())
|
||||||
|
Time->OnDayChanged.RemoveDynamic(this, &UMissionSubsystem::HandleDayChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::AcceptCommission(UCommission* Commission)
|
||||||
|
{
|
||||||
|
if (!Commission || !OfferedCommissions.Contains(Commission))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ANakedDesireCharacter* Player = GetPlayer();
|
||||||
|
if (!Player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Move to Accepted before arming: Accept() may complete synchronously (objectives already met),
|
||||||
|
// and HandleCommissionCompleted expects the commission to be in AcceptedCommissions.
|
||||||
|
OfferedCommissions.Remove(Commission);
|
||||||
|
AcceptedCommissions.Add(Commission);
|
||||||
|
Commission->OnCompleted.AddUObject(this, &UMissionSubsystem::HandleCommissionCompleted);
|
||||||
|
|
||||||
|
Commission->Accept(Player);
|
||||||
|
|
||||||
|
PersistState();
|
||||||
|
OnBoardChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::AbandonCommission(UCommission* Commission)
|
||||||
|
{
|
||||||
|
if (!Commission || !AcceptedCommissions.Contains(Commission))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Commission->OnCompleted.RemoveAll(this);
|
||||||
|
Commission->Abandon();
|
||||||
|
|
||||||
|
AcceptedCommissions.Remove(Commission);
|
||||||
|
OfferedCommissions.Add(Commission);
|
||||||
|
|
||||||
|
PersistState();
|
||||||
|
OnBoardChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::HandleDayChanged(int32 NewDay)
|
||||||
|
{
|
||||||
|
// Day rolls expire anything still accepted, then a fresh board is offered. No RestoreFromSave here:
|
||||||
|
// the persisted records are only meaningful for a load, and PersistState below clears them.
|
||||||
|
ExpireAccepted();
|
||||||
|
BuildBoard();
|
||||||
|
PersistState();
|
||||||
|
OnBoardChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::BuildBoard()
|
||||||
|
{
|
||||||
|
OfferedCommissions.Reset();
|
||||||
|
|
||||||
|
const UCommissionBoardConfig* Config = GetBoardConfig();
|
||||||
|
if (!Config)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (UCommission* Authored : Config->Commissions)
|
||||||
|
{
|
||||||
|
if (!Authored)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Duplicate so each run gets fresh objective state and an outer in this world.
|
||||||
|
UCommission* Runtime = DuplicateObject<UCommission>(Authored, this);
|
||||||
|
OfferedCommissions.Add(Runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::RestoreFromSave()
|
||||||
|
{
|
||||||
|
const UGlobalSaveGameData* Save = GetSave();
|
||||||
|
if (!Save)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ANakedDesireCharacter* Player = GetPlayer();
|
||||||
|
|
||||||
|
for (const FCommissionSaveRecord& Record : Save->GetCommissionRecords())
|
||||||
|
{
|
||||||
|
UCommission** Found = OfferedCommissions.FindByPredicate(
|
||||||
|
[&Record](const UCommission* C) { return C && C->CommissionId == Record.CommissionId; });
|
||||||
|
if (!Found || !*Found)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
UCommission* Commission = *Found;
|
||||||
|
|
||||||
|
if (Record.State == ECommissionState::Accepted)
|
||||||
|
{
|
||||||
|
OfferedCommissions.Remove(Commission);
|
||||||
|
AcceptedCommissions.Add(Commission);
|
||||||
|
Commission->OnCompleted.AddUObject(this, &UMissionSubsystem::HandleCommissionCompleted);
|
||||||
|
Commission->RestoreState(ECommissionState::Accepted, Player);
|
||||||
|
}
|
||||||
|
else if (Record.State == ECommissionState::Completed)
|
||||||
|
{
|
||||||
|
OfferedCommissions.Remove(Commission);
|
||||||
|
CompletedCommissions.Add(Commission);
|
||||||
|
Commission->RestoreState(ECommissionState::Completed, Player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::ExpireAccepted()
|
||||||
|
{
|
||||||
|
for (UCommission* Commission : AcceptedCommissions)
|
||||||
|
{
|
||||||
|
if (!Commission)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Commission->OnCompleted.RemoveAll(this);
|
||||||
|
Commission->Expire();
|
||||||
|
// TODO(§13.4): apply failurePenalty here once reputation / followers exist.
|
||||||
|
}
|
||||||
|
|
||||||
|
AcceptedCommissions.Reset();
|
||||||
|
CompletedCommissions.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::HandleCommissionCompleted(UCommission* Commission)
|
||||||
|
{
|
||||||
|
AcceptedCommissions.Remove(Commission);
|
||||||
|
CompletedCommissions.AddUnique(Commission);
|
||||||
|
|
||||||
|
ApplyReward(Commission->GetReward());
|
||||||
|
|
||||||
|
PersistState();
|
||||||
|
OnCommissionCompleted.Broadcast(Commission);
|
||||||
|
OnBoardChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::ApplyReward(const FCommissionReward& Reward)
|
||||||
|
{
|
||||||
|
// Money wires instantly to the save (§23 #23).
|
||||||
|
if (UGlobalSaveGameData* Save = GetSave())
|
||||||
|
Save->Money += Reward.Money;
|
||||||
|
|
||||||
|
// XP credits to the shared pool (currently a float on the character; §7.10 GAS migration later).
|
||||||
|
if (ANakedDesireCharacter* Player = GetPlayer())
|
||||||
|
Player->XP += Reward.XP;
|
||||||
|
|
||||||
|
// TODO: followers — no follower / profile system yet (Phase 8); Reward.Followers is dropped for now.
|
||||||
|
}
|
||||||
|
|
||||||
|
void UMissionSubsystem::PersistState() const
|
||||||
|
{
|
||||||
|
UGlobalSaveGameData* Save = GetSave();
|
||||||
|
if (!Save)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TArray<FCommissionSaveRecord> Records;
|
||||||
|
for (const UCommission* Commission : AcceptedCommissions)
|
||||||
|
{
|
||||||
|
if (Commission)
|
||||||
|
Records.Add({ Commission->CommissionId, ECommissionState::Accepted });
|
||||||
|
}
|
||||||
|
for (const UCommission* Commission : CompletedCommissions)
|
||||||
|
{
|
||||||
|
if (Commission)
|
||||||
|
Records.Add({ Commission->CommissionId, ECommissionState::Completed });
|
||||||
|
}
|
||||||
|
|
||||||
|
Save->SetCommissionRecords(Records);
|
||||||
|
}
|
||||||
|
|
||||||
|
ANakedDesireCharacter* UMissionSubsystem::GetPlayer() const
|
||||||
|
{
|
||||||
|
return Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
UGlobalSaveGameData* UMissionSubsystem::GetSave() const
|
||||||
|
{
|
||||||
|
UGameInstance* GameInstance = GetWorld() ? GetWorld()->GetGameInstance() : nullptr;
|
||||||
|
USaveSubsystem* SaveSubsystem = GameInstance ? GameInstance->GetSubsystem<USaveSubsystem>() : nullptr;
|
||||||
|
return SaveSubsystem ? SaveSubsystem->GetCurrentSave() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UCommissionBoardConfig* UMissionSubsystem::GetBoardConfig() const
|
||||||
|
{
|
||||||
|
const UNakedDesireGameInstance* GameInstance =
|
||||||
|
Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
|
||||||
|
return GameInstance ? GameInstance->CommissionBoard : nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/WorldSubsystem.h"
|
||||||
|
#include "MissionSubsystem.generated.h"
|
||||||
|
|
||||||
|
class UCommission;
|
||||||
|
class UCommissionBoardConfig;
|
||||||
|
class ANakedDesireCharacter;
|
||||||
|
class UGlobalSaveGameData;
|
||||||
|
struct FCommissionReward;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCommissionBoardChangedSignature);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCommissionCompletedSignature, UCommission*, Commission);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime owner of the commission board (GDD §13). Offers the hand-authored pool, drives the
|
||||||
|
* accept / complete / expire lifecycle, pays rewards instantly on completion (§23 #23), and persists
|
||||||
|
* commission state to the save. A WorldSubsystem (like UTimeOfDaySubsystem / SessionManager): it needs
|
||||||
|
* world access + OnDayChanged, and rehydrates durable state from the GameInstance save each level.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class NAKEDDESIRE_API UMissionSubsystem : public UWorldSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
const TArray<UCommission*>& GetOfferedCommissions() const { return OfferedCommissions; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
const TArray<UCommission*>& GetAcceptedCommissions() const { return AcceptedCommissions; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void AcceptCommission(UCommission* Commission);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void AbandonCommission(UCommission* Commission);
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnCommissionBoardChangedSignature OnBoardChanged;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnCommissionCompletedSignature OnCommissionCompleted;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleDayChanged(int32 NewDay);
|
||||||
|
|
||||||
|
void BuildBoard(); // instantiate offered commissions from the config
|
||||||
|
void RestoreFromSave(); // re-apply persisted accepted / completed states
|
||||||
|
void ExpireAccepted(); // deadline: accepted & unfinished -> expired
|
||||||
|
void HandleCommissionCompleted(UCommission* Commission);
|
||||||
|
void ApplyReward(const FCommissionReward& Reward);
|
||||||
|
void PersistState() const;
|
||||||
|
|
||||||
|
ANakedDesireCharacter* GetPlayer() const;
|
||||||
|
UGlobalSaveGameData* GetSave() const;
|
||||||
|
UCommissionBoardConfig* GetBoardConfig() const;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<UCommission*> OfferedCommissions;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<UCommission*> AcceptedCommissions;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<UCommission*> CompletedCommissions;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "BeFullyNakedNearNPCsObjective.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BeFullyNakedNearNPCs"
|
||||||
|
|
||||||
|
bool UBeFullyNakedNearNPCsObjective::IsConditionMet() const
|
||||||
|
{
|
||||||
|
return IsFullyNaked() && GetObserverCount() >= RequiredNPCs;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UBeFullyNakedNearNPCsObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
const FText People = (RequiredNPCs == 1)
|
||||||
|
? LOCTEXT("Person", "1 person")
|
||||||
|
: FText::Format(LOCTEXT("People", "{0} people"), FText::AsNumber(RequiredNPCs));
|
||||||
|
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Be fully naked in front of {0} for {1} seconds"),
|
||||||
|
People, FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText::Format(LOCTEXT("Instant", "Get fully naked in front of {0}"), People);
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "BeFullyNakedNearNPCsObjective.generated.h"
|
||||||
|
|
||||||
|
// §13.4 BeFullyNakedNearNPCs(count, durationSeconds): fully naked while at least `count` NPCs observe,
|
||||||
|
// sustained for the duration. Observer count is inherited from UObserverObjectiveBase.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Be Fully Naked Near NPCs")
|
||||||
|
class NAKEDDESIRE_API UBeFullyNakedNearNPCsObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
|
||||||
|
int32 RequiredNPCs = 1;
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "BeFullyNakedObjective.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BeFullyNaked"
|
||||||
|
|
||||||
|
FText UBeFullyNakedObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Be fully naked for {0} seconds"),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LOCTEXT("Instant", "Get fully naked");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "BeFullyNakedObjective.generated.h"
|
||||||
|
|
||||||
|
// §13.4 BeFullyNaked(durationSeconds): fully unclothed for RequiredHoldSeconds; no NPC requirement.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Be Fully Naked")
|
||||||
|
class NAKEDDESIRE_API UBeFullyNakedObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override { return IsFullyNaked(); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "BeObservedWhileExposedObjective.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BeObservedWhileExposed"
|
||||||
|
|
||||||
|
FText UBeObservedWhileExposedObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
const FText People = (RequiredObservers == 1)
|
||||||
|
? LOCTEXT("Person", "someone")
|
||||||
|
: FText::Format(LOCTEXT("People", "{0} people"), FText::AsNumber(RequiredObservers));
|
||||||
|
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Stay exposed in front of {0} for {1} seconds"),
|
||||||
|
People, FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText::Format(LOCTEXT("Instant", "Get exposed in front of {0}"), People);
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "BeObservedWhileExposedObjective.generated.h"
|
||||||
|
|
||||||
|
// At least RequiredObservers NPCs observing while any region is revealing (not necessarily fully nude).
|
||||||
|
// Generalizes "naked near NPCs" to partial exposure.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Be Observed While Exposed")
|
||||||
|
class NAKEDDESIRE_API UBeObservedWhileExposedObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override { return IsAnyPartRevealing() && GetObserverCount() >= RequiredObservers; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
|
||||||
|
int32 RequiredObservers = 1;
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Clothing/ClothingManager.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
void UCoverageObjectiveBase::OnActivate()
|
||||||
|
{
|
||||||
|
Super::OnActivate(); // observer subscription
|
||||||
|
|
||||||
|
if (Player && Player->ClothingManager)
|
||||||
|
{
|
||||||
|
Player->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
|
||||||
|
Player->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCoverageObjectiveBase::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->ClothingManager)
|
||||||
|
{
|
||||||
|
Player->ClothingManager->OnClothingEquip.RemoveDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
|
||||||
|
Player->ClothingManager->OnClothingUnequip.RemoveDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Super::OnDeactivate(); // observer unsubscription
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UCoverageObjectiveBase::IsFullyNaked() const
|
||||||
|
{
|
||||||
|
if (!Player || !Player->ClothingManager)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
UClothingManager* CM = Player->ClothingManager;
|
||||||
|
return CM->GetEffectiveCoverage(EBodyPart::Boobs) <= 0.0f
|
||||||
|
&& CM->GetEffectiveCoverage(EBodyPart::Ass) <= 0.0f
|
||||||
|
&& CM->GetEffectiveCoverage(EBodyPart::Genitals) <= 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UCoverageObjectiveBase::IsPartRevealing(EBodyPart Part) const
|
||||||
|
{
|
||||||
|
if (!Player || !Player->ClothingManager)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Player->ClothingManager->GetEffectiveCoverage(Part) < Player->ObservationRevealThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UCoverageObjectiveBase::IsAnyPartRevealing() const
|
||||||
|
{
|
||||||
|
return IsPartRevealing(EBodyPart::Boobs)
|
||||||
|
|| IsPartRevealing(EBodyPart::Ass)
|
||||||
|
|| IsPartRevealing(EBodyPart::Genitals);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCoverageObjectiveBase::HandleClothingChanged(UClothingItemInstance* ClothingItemInstance)
|
||||||
|
{
|
||||||
|
NotifyConditionChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Clothing/BodyPart.h"
|
||||||
|
#include "ObserverObjectiveBase.h"
|
||||||
|
#include "CoverageObjectiveBase.generated.h"
|
||||||
|
|
||||||
|
class UClothingItemInstance;
|
||||||
|
|
||||||
|
// Shared base for objectives that read body coverage; re-evaluates on any equip / unequip. Inherits
|
||||||
|
// the observer subscription from UObserverObjectiveBase, so coverage objectives can also gate on the
|
||||||
|
// current observer count (e.g. "naked in front of N people") without extra wiring.
|
||||||
|
UCLASS(Abstract)
|
||||||
|
class NAKEDDESIRE_API UCoverageObjectiveBase : public UObserverObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
// Fully unclothed: no effective coverage on any of the three exposable regions.
|
||||||
|
bool IsFullyNaked() const;
|
||||||
|
|
||||||
|
// Part reads as "revealing" — below the player's observation reveal threshold.
|
||||||
|
bool IsPartRevealing(EBodyPart Part) const;
|
||||||
|
|
||||||
|
// Any of the three regions is revealing.
|
||||||
|
bool IsAnyPartRevealing() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleClothingChanged(UClothingItemInstance* ClothingItemInstance);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "ExposeBodyPartObjective.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.ExposeBodyPart"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
FText BodyPartText(EBodyPart Part)
|
||||||
|
{
|
||||||
|
switch (Part)
|
||||||
|
{
|
||||||
|
case EBodyPart::Boobs: return LOCTEXT("Boobs", "boobs");
|
||||||
|
case EBodyPart::Ass: return LOCTEXT("Ass", "ass");
|
||||||
|
case EBodyPart::Genitals: return LOCTEXT("Genitals", "genitals");
|
||||||
|
default: return LOCTEXT("None", "nothing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UExposeBodyPartObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Expose your {0} for {1} seconds"),
|
||||||
|
BodyPartText(Part), FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText::Format(LOCTEXT("Instant", "Expose your {0}"), BodyPartText(Part));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Clothing/BodyPart.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "ExposeBodyPartObjective.generated.h"
|
||||||
|
|
||||||
|
// §13.4 ExposeBodyPart(part, durationSeconds): the named part reads as revealing for the duration.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Expose Body Part")
|
||||||
|
class NAKEDDESIRE_API UExposeBodyPartObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override { return IsPartRevealing(Part); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly)
|
||||||
|
EBodyPart Part = EBodyPart::Boobs;
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "GatherCrowdObjective.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.GatherCrowd"
|
||||||
|
|
||||||
|
FText UGatherCrowdObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Hold a crowd of {0} watchers for {1} seconds"),
|
||||||
|
FText::AsNumber(RequiredObservers), FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText::Format(LOCTEXT("Instant", "Gather a crowd of {0} watchers"), FText::AsNumber(RequiredObservers));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "ObserverObjectiveBase.h"
|
||||||
|
#include "GatherCrowdObjective.generated.h"
|
||||||
|
|
||||||
|
// Draw a crowd: at least RequiredObservers NPCs observing the player simultaneously. With
|
||||||
|
// RequiredHoldSeconds > 0 the crowd must hold for that long; 0 = the instant the count is reached.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Gather a Crowd")
|
||||||
|
class NAKEDDESIRE_API UGatherCrowdObjective : public UObserverObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override { return GetObserverCount() >= RequiredObservers; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
|
||||||
|
int32 RequiredObservers = 3;
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "ObserverObjectiveBase.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
#include "NakedDesire/Stats/StatsManager.h"
|
||||||
|
|
||||||
|
void UObserverObjectiveBase::OnActivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->StatsManager)
|
||||||
|
Player->StatsManager->OnObserversChanged.AddUniqueDynamic(this, &UObserverObjectiveBase::HandleObserversChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UObserverObjectiveBase::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->StatsManager)
|
||||||
|
Player->StatsManager->OnObserversChanged.RemoveDynamic(this, &UObserverObjectiveBase::HandleObserversChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UObserverObjectiveBase::GetObserverCount() const
|
||||||
|
{
|
||||||
|
return (Player && Player->StatsManager) ? Player->StatsManager->GetObserverCount() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UObserverObjectiveBase::HandleObserversChanged()
|
||||||
|
{
|
||||||
|
NotifyConditionChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionObjective.h"
|
||||||
|
#include "ObserverObjectiveBase.generated.h"
|
||||||
|
|
||||||
|
// Base for objectives that react to how many NPCs are currently observing the player (the same
|
||||||
|
// observer set that drives embarrassment). Re-evaluates whenever that count changes.
|
||||||
|
UCLASS(Abstract)
|
||||||
|
class NAKEDDESIRE_API UObserverObjectiveBase : public UCommissionObjective
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
int32 GetObserverCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleObserversChanged();
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "ReachEmbarrassmentObjective.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
#include "NakedDesire/Stats/StatsManager.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.ReachEmbarrassment"
|
||||||
|
|
||||||
|
void UReachEmbarrassmentObjective::OnActivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->StatsManager)
|
||||||
|
Player->StatsManager->EmbarrassmentUpdate.AddDynamic(this, &UReachEmbarrassmentObjective::HandleEmbarrassmentUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UReachEmbarrassmentObjective::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (Player && Player->StatsManager)
|
||||||
|
Player->StatsManager->EmbarrassmentUpdate.RemoveDynamic(this, &UReachEmbarrassmentObjective::HandleEmbarrassmentUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UReachEmbarrassmentObjective::IsConditionMet() const
|
||||||
|
{
|
||||||
|
return CachedMax > 0.0f && CachedCurrent >= ThresholdFraction * CachedMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UReachEmbarrassmentObjective::HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue)
|
||||||
|
{
|
||||||
|
CachedCurrent = CurrentValue;
|
||||||
|
CachedMax = MaxValue;
|
||||||
|
NotifyConditionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UReachEmbarrassmentObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
const FText Percent = FText::AsNumber(FMath::RoundToInt(ThresholdFraction * 100.0f));
|
||||||
|
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Keep embarrassment above {0}% for {1} seconds"),
|
||||||
|
Percent, FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FText::Format(LOCTEXT("Instant", "Push your embarrassment past {0}%"), Percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionObjective.h"
|
||||||
|
#include "ReachEmbarrassmentObjective.generated.h"
|
||||||
|
|
||||||
|
// Push embarrassment to ThresholdFraction of max. RequiredHoldSeconds = 0 satisfies on reaching it
|
||||||
|
// (ReachEmbarrassment); > 0 requires staying at/above it for the duration (SustainEmbarrassment).
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Reach Embarrassment")
|
||||||
|
class NAKEDDESIRE_API UReachEmbarrassmentObjective : public UCommissionObjective
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
virtual bool IsConditionMet() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0.0, ClampMax = 1.0))
|
||||||
|
float ThresholdFraction = 0.8f;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue);
|
||||||
|
|
||||||
|
float CachedCurrent = 0.0f;
|
||||||
|
float CachedMax = 0.0f;
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "RunNakedDistanceObjective.h"
|
||||||
|
|
||||||
|
#include "TimerManager.h"
|
||||||
|
#include "NakedDesire/Global/Gait.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.RunNakedDistance"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr float SampleIntervalSeconds = 0.2f;
|
||||||
|
constexpr float MaxStepCm = 1000.0f; // ignore single-sample jumps larger than this (teleports / streaming)
|
||||||
|
}
|
||||||
|
|
||||||
|
void URunNakedDistanceObjective::OnActivate()
|
||||||
|
{
|
||||||
|
Super::OnActivate();
|
||||||
|
|
||||||
|
AccumulatedCm = 0.0f;
|
||||||
|
|
||||||
|
if (Player)
|
||||||
|
{
|
||||||
|
LastLocation = Player->GetActorLocation();
|
||||||
|
Player->GetWorldTimerManager().SetTimer(SampleTimer, this, &URunNakedDistanceObjective::SampleDistance, SampleIntervalSeconds, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void URunNakedDistanceObjective::OnDeactivate()
|
||||||
|
{
|
||||||
|
if (Player)
|
||||||
|
Player->GetWorldTimerManager().ClearTimer(SampleTimer);
|
||||||
|
SampleTimer.Invalidate();
|
||||||
|
|
||||||
|
Super::OnDeactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void URunNakedDistanceObjective::SampleDistance()
|
||||||
|
{
|
||||||
|
if (bSatisfied || !Player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const FVector Now = Player->GetActorLocation();
|
||||||
|
const float Delta = FVector::Dist(Now, LastLocation);
|
||||||
|
LastLocation = Now;
|
||||||
|
|
||||||
|
// Only running, naked, and within any constraints counts toward the distance.
|
||||||
|
if (IsFullyNaked() && Player->GetGait() == EGait::Run && AreConstraintsMet())
|
||||||
|
{
|
||||||
|
AccumulatedCm += FMath::Min(Delta, MaxStepCm);
|
||||||
|
if (AccumulatedCm >= RequiredMeters * 100.0f)
|
||||||
|
MarkSatisfied();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float URunNakedDistanceObjective::GetProgress() const
|
||||||
|
{
|
||||||
|
if (bSatisfied)
|
||||||
|
return 1.0f;
|
||||||
|
|
||||||
|
const float TargetCm = RequiredMeters * 100.0f;
|
||||||
|
return TargetCm > 0.0f ? FMath::Clamp(AccumulatedCm / TargetCm, 0.0f, 1.0f) : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText URunNakedDistanceObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Run", "Run {0} m naked"), FText::AsNumber(FMath::RoundToInt(RequiredMeters)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "RunNakedDistanceObjective.generated.h"
|
||||||
|
|
||||||
|
// Cover RequiredMeters on foot while fully naked AND in the run gait. Distance accrues only while that
|
||||||
|
// condition (and any constraints) hold; per-sample movement is clamped so a teleport can't satisfy it.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Run Naked Distance")
|
||||||
|
class NAKEDDESIRE_API URunNakedDistanceObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
virtual float GetProgress() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void OnActivate() override;
|
||||||
|
virtual void OnDeactivate() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SampleDistance();
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
|
||||||
|
float RequiredMeters = 50.0f;
|
||||||
|
|
||||||
|
float AccumulatedCm = 0.0f;
|
||||||
|
FVector LastLocation = FVector::ZeroVector;
|
||||||
|
FTimerHandle SampleTimer;
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "StayUnseenWhileNakedObjective.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.StayUnseenWhileNaked"
|
||||||
|
|
||||||
|
FText UStayUnseenWhileNakedObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Stay fully naked and unseen for {0} seconds"),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LOCTEXT("Instant", "Be fully naked with nobody watching");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "StayUnseenWhileNakedObjective.generated.h"
|
||||||
|
|
||||||
|
// Stealth exhibitionism: be fully naked with ZERO observers for the duration (streak between patrols).
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Stay Unseen While Naked")
|
||||||
|
class NAKEDDESIRE_API UStayUnseenWhileNakedObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override { return IsFullyNaked() && GetObserverCount() == 0; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "WearOnlyUnderwearObjective.h"
|
||||||
|
|
||||||
|
#include "NakedDesire/Clothing/ClothingManager.h"
|
||||||
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Commissions.Objectives.WearOnlyUnderwear"
|
||||||
|
|
||||||
|
bool UWearOnlyUnderwearObjective::IsConditionMet() const
|
||||||
|
{
|
||||||
|
if (!Player || !Player->ClothingManager)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
UClothingManager* CM = Player->ClothingManager;
|
||||||
|
|
||||||
|
const bool bUnderwearWorn =
|
||||||
|
CM->IsClothingTypeOn(EClothingSlotType::UnderwearTop) ||
|
||||||
|
CM->IsClothingTypeOn(EClothingSlotType::UnderwearBottom);
|
||||||
|
|
||||||
|
const bool bOuterClear =
|
||||||
|
!CM->IsClothingTypeOn(EClothingSlotType::Outerwear) &&
|
||||||
|
!CM->IsClothingTypeOn(EClothingSlotType::Top) &&
|
||||||
|
!CM->IsClothingTypeOn(EClothingSlotType::Bottom) &&
|
||||||
|
!CM->IsClothingTypeOn(EClothingSlotType::Bodysuit);
|
||||||
|
|
||||||
|
return bUnderwearWorn && bOuterClear;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UWearOnlyUnderwearObjective::GetDescription() const
|
||||||
|
{
|
||||||
|
if (RequiredHoldSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
return FText::Format(LOCTEXT("Timed", "Stay in just your underwear for {0} seconds"),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LOCTEXT("Instant", "Strip down to just your underwear");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "CoverageObjectiveBase.h"
|
||||||
|
#include "WearOnlyUnderwearObjective.generated.h"
|
||||||
|
|
||||||
|
// Stripped to underwear: at least one underwear slot worn while every outer body-clothing slot
|
||||||
|
// (Outerwear / Top / Bottom / Bodysuit) is empty. Socks / footwear / accessories are ignored.
|
||||||
|
UCLASS(EditInlineNew, DisplayName = "Wear Only Underwear")
|
||||||
|
class NAKEDDESIRE_API UWearOnlyUnderwearObjective : public UCoverageObjectiveBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual FText GetDescription() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool IsConditionMet() const override;
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "NakedDesireGameInstance.generated.h"
|
#include "NakedDesireGameInstance.generated.h"
|
||||||
|
|
||||||
class UStartingSaveData;
|
class UStartingSaveData;
|
||||||
|
class UCommissionBoardConfig;
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
|
class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
|
||||||
@@ -15,4 +16,8 @@ class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
|
|||||||
public:
|
public:
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "Save")
|
UPROPERTY(EditDefaultsOnly, Category = "Save")
|
||||||
TObjectPtr<UStartingSaveData> StartingSaveData;
|
TObjectPtr<UStartingSaveData> StartingSaveData;
|
||||||
|
|
||||||
|
// Hand-authored commission pool the UMissionSubsystem offers (§13).
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category = "Commissions")
|
||||||
|
TObjectPtr<UCommissionBoardConfig> CommissionBoard;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "NakedDesireGameplayTags.h"
|
||||||
|
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(TAG_Location_Apartment, "Location.Apartment");
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "NativeGameplayTags.h"
|
||||||
|
|
||||||
|
// Native gameplay tags shared across systems. The apartment is identified by its ULocationData tag
|
||||||
|
// matching (or nesting under) Location.Apartment — that is how the session boundary is detected now,
|
||||||
|
// replacing the old per-trigger bIsApartment flag.
|
||||||
|
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Location_Apartment);
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "NakedDesire/Global/Constants.h"
|
#include "NakedDesire/Global/Constants.h"
|
||||||
|
#include "NakedDesire/Global/NakedDesireGameplayTags.h"
|
||||||
|
#include "NakedDesire/Locations/LocationData.h"
|
||||||
|
#include "NakedDesire/Locations/LocationSubsystem.h"
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
#include "NakedDesire/Stats/StatsManager.h"
|
#include "NakedDesire/Stats/StatsManager.h"
|
||||||
|
|
||||||
@@ -12,6 +15,13 @@ void USessionManagerSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
|||||||
{
|
{
|
||||||
Super::OnWorldBeginPlay(InWorld);
|
Super::OnWorldBeginPlay(InWorld);
|
||||||
|
|
||||||
|
// The apartment threshold is now a location event (Location.Apartment), not a per-trigger flag.
|
||||||
|
if (ULocationSubsystem* Locations = InWorld.GetSubsystem<ULocationSubsystem>())
|
||||||
|
{
|
||||||
|
Locations->OnLocationEntered.AddDynamic(this, &USessionManagerSubsystem::HandleLocationEntered);
|
||||||
|
Locations->OnLocationExited.AddDynamic(this, &USessionManagerSubsystem::HandleLocationExited);
|
||||||
|
}
|
||||||
|
|
||||||
// The player pawn and its UStatsManager may not have finished BeginPlay when
|
// The player pawn and its UStatsManager may not have finished BeginPlay when
|
||||||
// the world begins play, so defer binding by one tick.
|
// the world begins play, so defer binding by one tick.
|
||||||
InWorld.GetTimerManager().SetTimerForNextTick(this, &USessionManagerSubsystem::BindToPlayerStats);
|
InWorld.GetTimerManager().SetTimerForNextTick(this, &USessionManagerSubsystem::BindToPlayerStats);
|
||||||
@@ -30,19 +40,19 @@ void USessionManagerSubsystem::BindToPlayerStats()
|
|||||||
Player->StatsManager->EnergyUpdate.AddDynamic(this, &USessionManagerSubsystem::HandleEnergyUpdate);
|
Player->StatsManager->EnergyUpdate.AddDynamic(this, &USessionManagerSubsystem::HandleEnergyUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
void USessionManagerSubsystem::NotifyEnteredApartment()
|
void USessionManagerSubsystem::HandleLocationEntered(ULocationData* Location)
|
||||||
{
|
{
|
||||||
// Returning home is the safe end of a session (§4.3).
|
// Returning to the apartment is the safe end of a session (§4.3).
|
||||||
if (bSessionActive)
|
if (Location && Location->Tag.MatchesTag(TAG_Location_Apartment) && bSessionActive)
|
||||||
{
|
{
|
||||||
EndSession(ESessionLossCause::SafeReturn);
|
EndSession(ESessionLossCause::SafeReturn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void USessionManagerSubsystem::NotifyLeftApartment()
|
void USessionManagerSubsystem::HandleLocationExited(ULocationData* Location)
|
||||||
{
|
{
|
||||||
// Leaving the apartment is the only way to start a session (§4.1).
|
// Leaving the apartment is the only way to start a session (§4.1).
|
||||||
if (!bSessionActive)
|
if (Location && Location->Tag.MatchesTag(TAG_Location_Apartment) && !bSessionActive)
|
||||||
{
|
{
|
||||||
StartSession();
|
StartSession();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "SessionManagerSubsystem.generated.h"
|
#include "SessionManagerSubsystem.generated.h"
|
||||||
|
|
||||||
class UStatsManager;
|
class UStatsManager;
|
||||||
|
class ULocationData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Why a session ended (GDD §4.4). SafeReturn is a non-loss end (player walked
|
* Why a session ended (GDD §4.4). SafeReturn is a non-loss end (player walked
|
||||||
@@ -33,6 +34,10 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionEndSignature, ESessionLoss
|
|||||||
* ALocationTrigger drives start / end; embarrassment-max and energy-zero are
|
* ALocationTrigger drives start / end; embarrassment-max and energy-zero are
|
||||||
* detected by subscribing to UStatsManager. This replaces the EndGameEmbarrassed
|
* detected by subscribing to UStatsManager. This replaces the EndGameEmbarrassed
|
||||||
* Blueprint event on ANakedDesireGameMode.
|
* Blueprint event on ANakedDesireGameMode.
|
||||||
|
*
|
||||||
|
* The apartment threshold is detected via ULocationSubsystem: a session starts when the player leaves
|
||||||
|
* a location tagged Location.Apartment and ends (SafeReturn) when they re-enter one. There is no
|
||||||
|
* per-trigger apartment flag — the apartment is just a tagged location.
|
||||||
*/
|
*/
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem
|
class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem
|
||||||
@@ -42,10 +47,6 @@ class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem
|
|||||||
public:
|
public:
|
||||||
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
||||||
|
|
||||||
// Called by the apartment ALocationTrigger as the player crosses the threshold.
|
|
||||||
void NotifyEnteredApartment();
|
|
||||||
void NotifyLeftApartment();
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Session")
|
UFUNCTION(BlueprintPure, Category = "Session")
|
||||||
bool IsSessionActive() const { return bSessionActive; }
|
bool IsSessionActive() const { return bSessionActive; }
|
||||||
|
|
||||||
@@ -69,6 +70,12 @@ private:
|
|||||||
|
|
||||||
void BindToPlayerStats();
|
void BindToPlayerStats();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleLocationEntered(ULocationData* Location);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleLocationExited(ULocationData* Location);
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue);
|
void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
|
#include "LocationSubsystem.h"
|
||||||
|
|
||||||
|
#include "LocationData.h"
|
||||||
|
|
||||||
|
void ULocationSubsystem::EnterLocation(ULocationData* Location)
|
||||||
|
{
|
||||||
|
if (!Location)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int32& Count = ActiveCounts.FindOrAdd(Location);
|
||||||
|
++Count;
|
||||||
|
|
||||||
|
if (Count == 1)
|
||||||
|
{
|
||||||
|
RebuildActiveTags();
|
||||||
|
OnLocationEntered.Broadcast(Location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ULocationSubsystem::ExitLocation(ULocationData* Location)
|
||||||
|
{
|
||||||
|
if (!Location)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int32* Count = ActiveCounts.Find(Location);
|
||||||
|
if (!Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (--(*Count) <= 0)
|
||||||
|
{
|
||||||
|
ActiveCounts.Remove(Location);
|
||||||
|
RebuildActiveTags();
|
||||||
|
OnLocationExited.Broadcast(Location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ULocationSubsystem::IsPlayerInLocation(FGameplayTag Query) const
|
||||||
|
{
|
||||||
|
return Query.IsValid() && ActiveTags.HasTag(Query);
|
||||||
|
}
|
||||||
|
|
||||||
|
ULocationData* ULocationSubsystem::GetCurrentLocation() const
|
||||||
|
{
|
||||||
|
ULocationData* Best = nullptr;
|
||||||
|
int32 BestDepth = -1;
|
||||||
|
|
||||||
|
for (const TPair<TObjectPtr<ULocationData>, int32>& Pair : ActiveCounts)
|
||||||
|
{
|
||||||
|
ULocationData* Location = Pair.Key;
|
||||||
|
if (!Location || !Location->Tag.IsValid())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// More tag segments = more specific (Location.City.Beach beats Location.City).
|
||||||
|
const int32 Depth = Location->Tag.GetGameplayTagParents().Num();
|
||||||
|
if (Depth > BestDepth)
|
||||||
|
{
|
||||||
|
BestDepth = Depth;
|
||||||
|
Best = Location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Best;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ULocationSubsystem::RebuildActiveTags()
|
||||||
|
{
|
||||||
|
ActiveTags.Reset();
|
||||||
|
for (const TPair<TObjectPtr<ULocationData>, int32>& Pair : ActiveCounts)
|
||||||
|
{
|
||||||
|
if (Pair.Key && Pair.Key->Tag.IsValid())
|
||||||
|
ActiveTags.AddTag(Pair.Key->Tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "Subsystems/WorldSubsystem.h"
|
||||||
|
#include "LocationSubsystem.generated.h"
|
||||||
|
|
||||||
|
class ULocationData;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLocationChangedSignature, ULocationData*, Location);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for which tagged locations the player currently occupies (GDD §10.4).
|
||||||
|
* Fed by ALocationTrigger overlaps; queried + subscribed by everything else (session boundary,
|
||||||
|
* commission location constraints, future shop entry). World-scoped and transient — nothing to save.
|
||||||
|
*
|
||||||
|
* Locations are identified by their ULocationData FGameplayTag, so they nest: while inside
|
||||||
|
* Location.City.Beach the player also matches Location.City. The player can be in several at once.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class NAKEDDESIRE_API ULocationSubsystem : public UWorldSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Reported by ALocationTrigger as the player enters / leaves a tagged volume.
|
||||||
|
void EnterLocation(ULocationData* Location);
|
||||||
|
void ExitLocation(ULocationData* Location);
|
||||||
|
|
||||||
|
// True while the player occupies a location whose tag matches (or nests under) Query.
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Location")
|
||||||
|
bool IsPlayerInLocation(FGameplayTag Query) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Location")
|
||||||
|
FGameplayTagContainer GetPlayerLocationTags() const { return ActiveTags; }
|
||||||
|
|
||||||
|
// Most-specific current location (deepest tag) — for HUD / prompts. Null when outdoors / untagged.
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Location")
|
||||||
|
ULocationData* GetCurrentLocation() const;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Location")
|
||||||
|
FOnLocationChangedSignature OnLocationEntered;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Location")
|
||||||
|
FOnLocationChangedSignature OnLocationExited;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RebuildActiveTags();
|
||||||
|
|
||||||
|
// Ref-counted: a location can be several overlapping trigger volumes, so we only fire enter on
|
||||||
|
// 0->1 and exit on 1->0 — crossing between two boxes of the same place doesn't churn events.
|
||||||
|
UPROPERTY()
|
||||||
|
TMap<TObjectPtr<ULocationData>, int32> ActiveCounts;
|
||||||
|
|
||||||
|
FGameplayTagContainer ActiveTags;
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
|
|
||||||
#include "LocationTrigger.h"
|
#include "LocationTrigger.h"
|
||||||
|
|
||||||
#include "Components/BoxComponent.h"
|
#include "Components/BoxComponent.h"
|
||||||
#include "NakedDesire/Global/SessionManagerSubsystem.h"
|
#include "LocationSubsystem.h"
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -25,11 +25,8 @@ void ALocationTrigger::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
if (bIsApartment)
|
BoxTrigger->OnComponentBeginOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerBeginOverlap);
|
||||||
{
|
BoxTrigger->OnComponentEndOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerEndOverlap);
|
||||||
BoxTrigger->OnComponentBeginOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerBeginOverlap);
|
|
||||||
BoxTrigger->OnComponentEndOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerEndOverlap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||||
@@ -38,10 +35,8 @@ void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComp
|
|||||||
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
|
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem<USessionManagerSubsystem>())
|
if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem<ULocationSubsystem>())
|
||||||
{
|
Locations->EnterLocation(LocationData);
|
||||||
SessionManager->NotifyEnteredApartment();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||||
@@ -50,9 +45,6 @@ void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedCompon
|
|||||||
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
|
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem<USessionManagerSubsystem>())
|
if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem<ULocationSubsystem>())
|
||||||
{
|
Locations->ExitLocation(LocationData);
|
||||||
SessionManager->NotifyLeftApartment();
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
// © 2025 Naked People Team. All Rights Reserved.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
class ULocationData;
|
class ULocationData;
|
||||||
class UBoxComponent;
|
class UBoxComponent;
|
||||||
|
|
||||||
|
// A tagged volume. On player overlap it reports enter / leave to ULocationSubsystem, which is the
|
||||||
|
// single authority on where the player is. Everything (session boundary, commission location
|
||||||
|
// constraints, etc.) consumes the subsystem — the trigger itself has no consumer-specific logic.
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class NAKEDDESIRE_API ALocationTrigger : public AActor
|
class NAKEDDESIRE_API ALocationTrigger : public AActor
|
||||||
{
|
{
|
||||||
@@ -20,12 +23,6 @@ class NAKEDDESIRE_API ALocationTrigger : public AActor
|
|||||||
UPROPERTY(EditAnywhere)
|
UPROPERTY(EditAnywhere)
|
||||||
ULocationData* LocationData;
|
ULocationData* LocationData;
|
||||||
|
|
||||||
// When set, the player crossing this trigger drives session start / end on
|
|
||||||
// USessionManagerSubsystem (GDD §4.1 / §4.3). Exactly one trigger — the
|
|
||||||
// apartment — should have this checked.
|
|
||||||
UPROPERTY(EditAnywhere, Category = "Session")
|
|
||||||
bool bIsApartment = false;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ALocationTrigger();
|
ALocationTrigger();
|
||||||
|
|
||||||
@@ -42,4 +39,4 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
|
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
|
||||||
};
|
};
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "GoalRestriction.h"
|
|
||||||
|
|
||||||
void UGoalRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
IsSuccess = false;
|
|
||||||
Player = PlayerCharacter;
|
|
||||||
OnUpdate.Broadcast(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UGoalRestriction::Complete()
|
|
||||||
{
|
|
||||||
Complete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UGoalRestriction::Complete(const bool Value)
|
|
||||||
{
|
|
||||||
IsSuccess = Value;
|
|
||||||
OnUpdate.Broadcast(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
FText UGoalRestriction::GetDescription() const
|
|
||||||
{
|
|
||||||
return FText::GetEmpty();
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "UObject/Object.h"
|
|
||||||
#include "GoalRestriction.generated.h"
|
|
||||||
|
|
||||||
class ANakedDesireCharacter;
|
|
||||||
class UGoalRestriction;
|
|
||||||
|
|
||||||
DECLARE_MULTICAST_DELEGATE_OneParam(FMissionRestrictionUpdateSignature, UGoalRestriction*);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS(EditInlineNew, BlueprintType)
|
|
||||||
class NAKEDDESIRE_API UGoalRestriction : public UObject
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
FMissionRestrictionUpdateSignature OnUpdate;
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
bool GetIsSuccess() const
|
|
||||||
{
|
|
||||||
return IsSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Init(ANakedDesireCharacter* PlayerCharacter);
|
|
||||||
virtual void Complete();
|
|
||||||
virtual void Complete(bool Value);
|
|
||||||
virtual void Stop() {};
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
virtual FText GetDescription() const;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
UPROPERTY()
|
|
||||||
ANakedDesireCharacter* Player = nullptr;
|
|
||||||
|
|
||||||
bool IsSuccess = false;
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "FlashGoal.h"
|
|
||||||
|
|
||||||
#include "NakedDesire/NPC/NPCAIController.h"
|
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "Missions.Goals.Flash"
|
|
||||||
const FText GoalsFlashSingle = LOCTEXT("Description.Single", "Flash someone {BodyPart} once");
|
|
||||||
const FText GoalsFlashMultiple = LOCTEXT("Description.Multiple", "Flash {BodyPart} to {PeopleCount} people");
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
|
||||||
|
|
||||||
void UFlashGoal::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
Super::Init(PlayerCharacter);
|
|
||||||
|
|
||||||
PlayerNoticedHandle = PlayerCharacter->OnNoticed.AddUObject(this, &UFlashGoal::OnPlayerNoticed);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UFlashGoal::Complete()
|
|
||||||
{
|
|
||||||
Super::Complete();
|
|
||||||
|
|
||||||
Player->OnNoticed.Remove(PlayerNoticedHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
FText UFlashGoal::GetDescription() const
|
|
||||||
{
|
|
||||||
FText BodyTypeString;
|
|
||||||
|
|
||||||
switch (BodyType)
|
|
||||||
{
|
|
||||||
case EBodyPart::Ass:
|
|
||||||
BodyTypeString = FText::FromString("Ass");
|
|
||||||
break;
|
|
||||||
case EBodyPart::Boobs:
|
|
||||||
BodyTypeString = FText::FromString("Boobs");
|
|
||||||
break;
|
|
||||||
case EBodyPart::Genitals:
|
|
||||||
BodyTypeString = FText::FromString("Genitals");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
BodyTypeString = FText::FromString("None");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RequiredFlashCount == 1)
|
|
||||||
{
|
|
||||||
return FText::Format(GoalsFlashSingle,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("BodyPart"), BodyTypeString}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return FText::Format(GoalsFlashMultiple,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("BodyPart"), BodyTypeString},
|
|
||||||
{TEXT("PeopleCount"), RequiredFlashCount}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void UFlashGoal::OnPlayerNoticed(ANPCAIController* NPC)
|
|
||||||
{
|
|
||||||
if (IsCompleted || !CheckRestrictions())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NoticedActors.Contains(NPC))
|
|
||||||
{
|
|
||||||
NoticedActors.Add(NPC);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NoticedActors.Num() >= RequiredFlashCount)
|
|
||||||
{
|
|
||||||
Complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "NakedDesire/Clothing/BodyPart.h"
|
|
||||||
#include "NakedDesire/MissionBuilder/MissionGoal.h"
|
|
||||||
#include "FlashGoal.generated.h"
|
|
||||||
|
|
||||||
class ANPCAIController;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS(EditInlineNew)
|
|
||||||
class NAKEDDESIRE_API UFlashGoal : public UMissionGoal
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
|
|
||||||
int RequiredFlashCount = 1;
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
EBodyPart BodyType;
|
|
||||||
|
|
||||||
FDelegateHandle PlayerNoticedHandle;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
TSet<AActor*> NoticedActors;
|
|
||||||
|
|
||||||
public:
|
|
||||||
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
|
|
||||||
virtual void Complete() override;
|
|
||||||
virtual FText GetDescription() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void OnPlayerNoticed(ANPCAIController* NPC);
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#include "MinTimeGoal.h"
|
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "Missions.Goals.MinTime"
|
|
||||||
const FText GoalsMinTimeDescription = LOCTEXT("Description", "Do following at least {MinTime} seconds");
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
|
||||||
|
|
||||||
|
|
||||||
FText UMinTimeGoal::GetDescription() const
|
|
||||||
{
|
|
||||||
return FText::Format(GoalsMinTimeDescription,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("MinTime"), MinTime}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMinTimeGoal::OnRestrictionUpdated(UGoalRestriction* Restriction)
|
|
||||||
{
|
|
||||||
Super::OnRestrictionUpdated(Restriction);
|
|
||||||
|
|
||||||
if (CheckRestrictions())
|
|
||||||
{
|
|
||||||
if (!TimerHandle.IsValid())
|
|
||||||
{
|
|
||||||
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UMinTimeGoal::TimeIsUp, MinTime, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (TimerHandle.IsValid())
|
|
||||||
{
|
|
||||||
GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMinTimeGoal::TimeIsUp()
|
|
||||||
{
|
|
||||||
Complete();
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "../MissionGoal.h"
|
|
||||||
#include "MinTimeGoal.generated.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS(EditInlineNew)
|
|
||||||
class NAKEDDESIRE_API UMinTimeGoal : public UMissionGoal
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
virtual FText GetDescription() const override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void OnRestrictionUpdated(UGoalRestriction* Restriction) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
float MinTime = 3.f;
|
|
||||||
|
|
||||||
FTimerHandle TimerHandle;
|
|
||||||
|
|
||||||
void TimeIsUp();
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "Mission.h"
|
|
||||||
|
|
||||||
#include "MissionGoal.h"
|
|
||||||
|
|
||||||
void UMission::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
Player = PlayerCharacter;
|
|
||||||
|
|
||||||
for (const auto& Goal : Goals)
|
|
||||||
{
|
|
||||||
Goal->Init(Player);
|
|
||||||
auto Handle = Goal->OnUpdate.AddUObject(this, &UMission::OnGoalUpdated);
|
|
||||||
GoalUpdateHandles.Add(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMission::OnGoalUpdated(UMissionGoal* MissionGoal)
|
|
||||||
{
|
|
||||||
if (IsCompleted)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CheckGoals())
|
|
||||||
{
|
|
||||||
Complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMission::Complete()
|
|
||||||
{
|
|
||||||
IsCompleted = true;
|
|
||||||
|
|
||||||
for (const auto& Goal : Goals)
|
|
||||||
{
|
|
||||||
for (const auto& Handle : GoalUpdateHandles)
|
|
||||||
{
|
|
||||||
Goal->OnUpdate.Remove(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OnComplete.Broadcast(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UMission::CheckGoals()
|
|
||||||
{
|
|
||||||
if (IsCompleted)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& Goal : Goals)
|
|
||||||
{
|
|
||||||
if (!Goal->GetIsCompleted() || !Goal->CheckRestrictions())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "UObject/Object.h"
|
|
||||||
#include "Mission.generated.h"
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE_MULTICAST_DELEGATE_OneParam(FMissionCompleteSignature, class UMission*);
|
|
||||||
|
|
||||||
class UMissionGoal;
|
|
||||||
class ANakedDesireCharacter;
|
|
||||||
class UGoalRestriction;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS(EditInlineNew, BlueprintType)
|
|
||||||
class NAKEDDESIRE_API UMission : public UObject
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, Instanced)
|
|
||||||
TArray<UMissionGoal*> Goals;
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
int MoneyReward = 0;
|
|
||||||
|
|
||||||
bool IsCompleted = false;
|
|
||||||
|
|
||||||
TArray<FDelegateHandle> GoalUpdateHandles;
|
|
||||||
|
|
||||||
public:
|
|
||||||
FMissionCompleteSignature OnComplete;
|
|
||||||
|
|
||||||
void Init(ANakedDesireCharacter* PlayerCharacter);
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
int GetMoneyReward() const
|
|
||||||
{
|
|
||||||
return MoneyReward;
|
|
||||||
}
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
TArray<UMissionGoal*> GetGoals() const
|
|
||||||
{
|
|
||||||
return Goals;
|
|
||||||
}
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
bool GetIsCompleted() const
|
|
||||||
{
|
|
||||||
return IsCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
UPROPERTY()
|
|
||||||
ANakedDesireCharacter* Player = nullptr;
|
|
||||||
|
|
||||||
private:
|
|
||||||
UFUNCTION()
|
|
||||||
void OnGoalUpdated(UMissionGoal* MissionGoal);
|
|
||||||
|
|
||||||
void Complete();
|
|
||||||
|
|
||||||
bool CheckGoals();
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "MissionGoal.h"
|
|
||||||
|
|
||||||
#include "GoalRestriction.h"
|
|
||||||
|
|
||||||
void UMissionGoal::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
Player = PlayerCharacter;
|
|
||||||
|
|
||||||
for (const auto& Elem : Restrictions)
|
|
||||||
{
|
|
||||||
Elem->Init(Player);
|
|
||||||
auto Handle = Elem->OnUpdate.AddUObject(this, &UMissionGoal::OnRestrictionUpdated);
|
|
||||||
RestrictionUpdateHandles.Add(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMissionGoal::Complete()
|
|
||||||
{
|
|
||||||
if (!CheckRestrictions())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsCompleted = true;
|
|
||||||
for (const auto& Restriction : Restrictions)
|
|
||||||
{
|
|
||||||
Restriction->Stop();
|
|
||||||
for (const auto& Handle : RestrictionUpdateHandles)
|
|
||||||
{
|
|
||||||
Restriction->OnUpdate.Remove(Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OnUpdate.Broadcast(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UMissionGoal::CheckRestrictions()
|
|
||||||
{
|
|
||||||
if (IsCompleted)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& Restriction : Restrictions)
|
|
||||||
{
|
|
||||||
if (!Restriction->GetIsSuccess())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
FText UMissionGoal::GetDescription() const
|
|
||||||
{
|
|
||||||
return FText::GetEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMissionGoal::OnRestrictionUpdated(UGoalRestriction* Restriction)
|
|
||||||
{
|
|
||||||
if (IsCompleted)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnUpdate.Broadcast(this);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "UObject/Object.h"
|
|
||||||
#include "MissionGoal.generated.h"
|
|
||||||
|
|
||||||
class ANakedDesireCharacter;
|
|
||||||
class UMissionGoal;
|
|
||||||
class UGoalRestriction;
|
|
||||||
|
|
||||||
DECLARE_MULTICAST_DELEGATE_OneParam(FGoalUpdateSignature, UMissionGoal*);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS(EditInlineNew, BlueprintType)
|
|
||||||
class NAKEDDESIRE_API UMissionGoal : public UObject
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, Instanced)
|
|
||||||
TArray<UGoalRestriction*> Restrictions;
|
|
||||||
|
|
||||||
TArray<FDelegateHandle> RestrictionUpdateHandles;
|
|
||||||
|
|
||||||
public:
|
|
||||||
virtual void Init(ANakedDesireCharacter* PlayerCharacter);
|
|
||||||
virtual void Complete();
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
bool GetIsCompleted() const
|
|
||||||
{
|
|
||||||
return IsCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
TArray<UGoalRestriction*> GetRestrictions() const
|
|
||||||
{
|
|
||||||
return Restrictions;
|
|
||||||
}
|
|
||||||
|
|
||||||
FGoalUpdateSignature OnUpdate;
|
|
||||||
|
|
||||||
bool CheckRestrictions();
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure)
|
|
||||||
virtual FText GetDescription() const;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
UPROPERTY()
|
|
||||||
ANakedDesireCharacter* Player = nullptr;
|
|
||||||
|
|
||||||
virtual void OnRestrictionUpdated(UGoalRestriction* Restriction);
|
|
||||||
|
|
||||||
bool IsCompleted = false;
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// Fill out your copyright notice in the Description page of Project Settings.
|
|
||||||
|
|
||||||
|
|
||||||
#include "MissionsConfig.h"
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// Fill out your copyright notice in the Description page of Project Settings.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "Engine/DataAsset.h"
|
|
||||||
#include "MissionsConfig.generated.h"
|
|
||||||
|
|
||||||
class UMission;
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct NAKEDDESIRE_API FMissionsConfigItem
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, Instanced)
|
|
||||||
TArray<UMission*> Missions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS()
|
|
||||||
class NAKEDDESIRE_API UMissionsConfig : public UPrimaryDataAsset
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
TArray<FMissionsConfigItem> DailyMissions;
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
TArray<FMissionsConfigItem> WeeklyMissions;
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "MissionsManager.h"
|
|
||||||
#include "Mission.h"
|
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
|
||||||
|
|
||||||
|
|
||||||
UMissionsManager::UMissionsManager()
|
|
||||||
{
|
|
||||||
PrimaryComponentTick.bCanEverTick = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMissionsManager::BeginPlay()
|
|
||||||
{
|
|
||||||
Super::BeginPlay();
|
|
||||||
|
|
||||||
Player = Cast<ANakedDesireCharacter>(GetOwner());
|
|
||||||
|
|
||||||
for (const auto& Mission : AvailableMissions)
|
|
||||||
{
|
|
||||||
Mission->Init(Player);
|
|
||||||
Mission->OnComplete.AddUObject(this, &UMissionsManager::CompleteMission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMissionsManager::CompleteMission(UMission* Mission)
|
|
||||||
{
|
|
||||||
CompletedMissions.Add(Mission);
|
|
||||||
AvailableMissions.Remove(Mission);
|
|
||||||
OnMissionCompleted.Broadcast(Mission);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UMissionsManager::RefreshDailyMissions(const TArray<UMission*>& NewMissions)
|
|
||||||
{
|
|
||||||
for (UMission* Mission : AvailableMissions)
|
|
||||||
{
|
|
||||||
Mission->OnComplete.RemoveAll(this);
|
|
||||||
}
|
|
||||||
AvailableMissions.Reset();
|
|
||||||
|
|
||||||
AvailableMissions.Append(NewMissions);
|
|
||||||
|
|
||||||
for (UMission* Mission : AvailableMissions)
|
|
||||||
{
|
|
||||||
Mission->Init(Player);
|
|
||||||
Mission->OnComplete.AddUObject(this, &UMissionsManager::CompleteMission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "Components/ActorComponent.h"
|
|
||||||
#include "MissionsManager.generated.h"
|
|
||||||
|
|
||||||
|
|
||||||
class UMission;
|
|
||||||
class ANakedDesireCharacter;
|
|
||||||
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMissionCompletedSignature, UMission*, Mission);
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRewardsCollected, int, Reward);
|
|
||||||
|
|
||||||
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
|
|
||||||
class NAKEDDESIRE_API UMissionsManager : public UActorComponent
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
ANakedDesireCharacter* Player = nullptr;
|
|
||||||
|
|
||||||
public:
|
|
||||||
UMissionsManager();
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, Instanced, BlueprintReadOnly)
|
|
||||||
TArray<UMission*> AvailableMissions;
|
|
||||||
|
|
||||||
UPROPERTY(BlueprintReadWrite)
|
|
||||||
TArray<UMission*> CompletedMissions;
|
|
||||||
|
|
||||||
UPROPERTY(BlueprintAssignable)
|
|
||||||
FOnMissionCompletedSignature OnMissionCompleted;
|
|
||||||
|
|
||||||
UPROPERTY(BlueprintAssignable)
|
|
||||||
FOnRewardsCollected OnRewardsCollected;
|
|
||||||
|
|
||||||
void CompleteMission(UMission* Mission);
|
|
||||||
|
|
||||||
UFUNCTION()
|
|
||||||
void RefreshDailyMissions(const TArray<UMission*>& NewMissions);
|
|
||||||
|
|
||||||
virtual void BeginPlay() override;
|
|
||||||
};
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "EquipClothingRestriction.h"
|
|
||||||
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
|
|
||||||
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
|
||||||
#include "NakedDesire/Clothing/ClothingManager.h"
|
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "Missions.Restriction.EquipClothing"
|
|
||||||
const FText RestrictionEquipClothingDescriptionSingle = LOCTEXT("Description.Single", "Equip {ItemName}");
|
|
||||||
const FText RestrictionEquipClothingDescriptionMultiple = LOCTEXT("Description.Multiple", "Equip one of: {ItemNames}");
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
|
||||||
|
|
||||||
void UEquipClothingRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
Super::Init(PlayerCharacter);
|
|
||||||
|
|
||||||
if (!ClothingEquippedDelegateHandle.IsValid())
|
|
||||||
{
|
|
||||||
Player->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UEquipClothingRestriction::OnClothingEquipped);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ClothingUnequippedDelegateHandle.IsValid())
|
|
||||||
{
|
|
||||||
Player->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UEquipClothingRestriction::OnClothingUnequipped);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckClothing();
|
|
||||||
}
|
|
||||||
|
|
||||||
void UEquipClothingRestriction::Stop()
|
|
||||||
{
|
|
||||||
Super::Stop();
|
|
||||||
|
|
||||||
Player->ClothingManager->OnClothingEquip.Remove(this, TEXT("OnClothingEquipped"));
|
|
||||||
Player->ClothingManager->OnClothingUnequip.Remove(this, TEXT("OnClothingUnequipped"));
|
|
||||||
}
|
|
||||||
|
|
||||||
FText UEquipClothingRestriction::GetDescription() const
|
|
||||||
{
|
|
||||||
if (ClothingItems.Num() == 1 && ClothingItems[0])
|
|
||||||
{
|
|
||||||
return FText::Format(RestrictionEquipClothingDescriptionSingle,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("ItemName"), ClothingItems[0]->Name}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
FString Items = TEXT("");
|
|
||||||
for (const auto& Item : ClothingItems)
|
|
||||||
{
|
|
||||||
if (Item)
|
|
||||||
{
|
|
||||||
Items += Item->Name.ToString() + ", ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FText::Format(RestrictionEquipClothingDescriptionMultiple,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("ItemNames"), FText::FromString(Items)}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void UEquipClothingRestriction::OnClothingEquipped(UClothingItemInstance* ClothingItemInstance)
|
|
||||||
{
|
|
||||||
const bool IsTargetClothing = ClothingItems.FindByPredicate([&ClothingItemInstance](const UClothingItemDefinition* Item)
|
|
||||||
{
|
|
||||||
return Item && Item->Name.EqualTo(ClothingItemInstance->GetClothingItemDefinition()->Name);
|
|
||||||
}) != nullptr;
|
|
||||||
if (IsTargetClothing)
|
|
||||||
{
|
|
||||||
Complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UEquipClothingRestriction::OnClothingUnequipped(UClothingItemInstance* ClothingItemInstance)
|
|
||||||
{
|
|
||||||
const bool IsTargetClothing = ClothingItems.FindByPredicate([&ClothingItemInstance](const UClothingItemDefinition* Item)
|
|
||||||
{
|
|
||||||
return Item && Item->Name.EqualTo(ClothingItemInstance->GetClothingItemDefinition()->Name);
|
|
||||||
}) != nullptr;
|
|
||||||
if (IsTargetClothing)
|
|
||||||
{
|
|
||||||
Init(Player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UEquipClothingRestriction::CheckClothing()
|
|
||||||
{
|
|
||||||
// for (const FClothingSlotData& ClothingSlot : Player->ClothingManager->ClothingSlots)
|
|
||||||
// {
|
|
||||||
// if (!ClothingSlot.ClothingItemInstance)
|
|
||||||
// {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const bool IsTargetClothing = ClothingItems.FindByPredicate([&ClothingSlot](const UClothingItem* Item)
|
|
||||||
// {
|
|
||||||
// return Item && Item->Name.EqualTo(ClothingSlot.ClothingItemInstance->GetClothingItem()->Name);
|
|
||||||
// }) != nullptr;
|
|
||||||
// if (IsTargetClothing)
|
|
||||||
// {
|
|
||||||
// Complete();
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "NakedDesire/MissionBuilder/GoalRestriction.h"
|
|
||||||
#include "EquipClothingRestriction.generated.h"
|
|
||||||
|
|
||||||
class UClothingItemInstance;
|
|
||||||
class UClothingItemDefinition;
|
|
||||||
|
|
||||||
UCLASS(EditInlineNew)
|
|
||||||
class NAKEDDESIRE_API UEquipClothingRestriction : public UGoalRestriction
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, meta = (ToolTip =
|
|
||||||
"One of provided clothing items should be equipped by player. If multiple clothing items required provide multiple restrictions"))
|
|
||||||
TArray<UClothingItemDefinition*> ClothingItems;
|
|
||||||
|
|
||||||
public:
|
|
||||||
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
|
|
||||||
virtual void Stop() override;
|
|
||||||
virtual FText GetDescription() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
FDelegateHandle ClothingEquippedDelegateHandle;
|
|
||||||
FDelegateHandle ClothingUnequippedDelegateHandle;
|
|
||||||
|
|
||||||
UFUNCTION()
|
|
||||||
void OnClothingEquipped(UClothingItemInstance* ClothingItemInstance);
|
|
||||||
|
|
||||||
UFUNCTION()
|
|
||||||
void OnClothingUnequipped(UClothingItemInstance* ClothingItemInstance);
|
|
||||||
|
|
||||||
void CheckClothing();
|
|
||||||
};
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "ExposeBodyPartRestriction.h"
|
|
||||||
|
|
||||||
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
|
|
||||||
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
|
||||||
#include "NakedDesire/Clothing/ClothingManager.h"
|
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "Missions.Restrictions.ExposeBodyPart"
|
|
||||||
const FText RestrictionsExposeBodyPartDescription = LOCTEXT("Description", "Expose {BodyPart}");
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
|
||||||
|
|
||||||
void UExposeBodyPartRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
Super::Init(PlayerCharacter);
|
|
||||||
|
|
||||||
PlayerCharacter->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UExposeBodyPartRestriction::EquipClothing);
|
|
||||||
PlayerCharacter->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UExposeBodyPartRestriction::UnequipClothing);
|
|
||||||
|
|
||||||
CheckClothing();
|
|
||||||
}
|
|
||||||
|
|
||||||
void UExposeBodyPartRestriction::Stop()
|
|
||||||
{
|
|
||||||
Super::Stop();
|
|
||||||
|
|
||||||
Player->ClothingManager->OnClothingEquip.Remove(this, TEXT("ClothingEquip"));
|
|
||||||
Player->ClothingManager->OnClothingUnequip.Remove(this, TEXT("ClothingUnequip"));
|
|
||||||
}
|
|
||||||
|
|
||||||
FText UExposeBodyPartRestriction::GetDescription() const
|
|
||||||
{
|
|
||||||
FText BodyPartString;
|
|
||||||
switch (BodyPart)
|
|
||||||
{
|
|
||||||
case EBodyPart::Ass:
|
|
||||||
BodyPartString = FText::FromString("Ass");
|
|
||||||
break;
|
|
||||||
case EBodyPart::Boobs:
|
|
||||||
BodyPartString = FText::FromString("Boobs");
|
|
||||||
break;
|
|
||||||
case EBodyPart::Genitals:
|
|
||||||
BodyPartString = FText::FromString("Genitals");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
BodyPartString = FText::FromString("None");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FText::Format(RestrictionsExposeBodyPartDescription,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("BodyPart"), BodyPartString}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void UExposeBodyPartRestriction::EquipClothing(UClothingItemInstance* ClothingItemInstance)
|
|
||||||
{
|
|
||||||
if (IsSuccess) // TODO: Add covered body part resolution
|
|
||||||
{
|
|
||||||
Init(Player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UExposeBodyPartRestriction::UnequipClothing(UClothingItemInstance* ClothingItemInstance)
|
|
||||||
{
|
|
||||||
if (IsSuccess)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CheckClothing();
|
|
||||||
}
|
|
||||||
|
|
||||||
void UExposeBodyPartRestriction::CheckClothing()
|
|
||||||
{
|
|
||||||
// if (!Player || !Player->ClothingManager || Player->ClothingManager->ClothingSlots.IsEmpty())
|
|
||||||
// {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const FClothingSlotData* TargetClothingItem = Player->ClothingManager->ClothingSlots.FindByPredicate([this](const FClothingSlotData& ClothingSlot)
|
|
||||||
// {
|
|
||||||
// if (ClothingSlot.ClothingItemInstance)
|
|
||||||
// {
|
|
||||||
// return true; // TODO: Add exposed body part resolution
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return false;
|
|
||||||
// });
|
|
||||||
// if (!TargetClothingItem)
|
|
||||||
// {
|
|
||||||
// Complete();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "NakedDesire/Clothing/BodyPart.h"
|
|
||||||
#include "NakedDesire/MissionBuilder/GoalRestriction.h"
|
|
||||||
#include "ExposeBodyPartRestriction.generated.h"
|
|
||||||
|
|
||||||
class UClothingItemInstance;
|
|
||||||
|
|
||||||
UCLASS(EditInlineNew)
|
|
||||||
class NAKEDDESIRE_API UExposeBodyPartRestriction : public UGoalRestriction
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
FDelegateHandle EquipClothingHandle;
|
|
||||||
FDelegateHandle UnequipClothingHandle;
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
EBodyPart BodyPart;
|
|
||||||
|
|
||||||
public:
|
|
||||||
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
|
|
||||||
virtual void Stop() override;
|
|
||||||
virtual FText GetDescription() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
UFUNCTION()
|
|
||||||
void EquipClothing(UClothingItemInstance* ClothingItemInstance);
|
|
||||||
|
|
||||||
UFUNCTION()
|
|
||||||
void UnequipClothing(UClothingItemInstance* ClothingItemInstance);
|
|
||||||
|
|
||||||
void CheckClothing();
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
#include "LocationRestriction.h"
|
|
||||||
#include "NakedDesire/Locations/LocationData.h"
|
|
||||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "Missions.Restrictions.Location"
|
|
||||||
const FText RestrictionsLocationDescription = LOCTEXT("Description", "Visit {LocationName}");
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
|
||||||
|
|
||||||
void ULocationRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
|
|
||||||
{
|
|
||||||
Super::Init(PlayerCharacter);
|
|
||||||
|
|
||||||
// AreaEnterHandle = PlayerCharacter->OnAreaEnter.AddUObject(this, &ULocationRestriction::OnAreaEnter);
|
|
||||||
// AreaExitHandle = PlayerCharacter->OnAreaExit.AddUObject(this, &ULocationRestriction::OnAreaExit);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ULocationRestriction::Stop()
|
|
||||||
{
|
|
||||||
Super::Stop();
|
|
||||||
|
|
||||||
// Player->OnAreaEnter.Remove(AreaEnterHandle);
|
|
||||||
// Player->OnAreaExit.Remove(AreaExitHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
FText ULocationRestriction::GetDescription() const
|
|
||||||
{
|
|
||||||
return FText::Format(RestrictionsLocationDescription,
|
|
||||||
FFormatNamedArguments
|
|
||||||
{
|
|
||||||
{TEXT("LocationName"), TargetLocation->Name}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ULocationRestriction::OnAreaEnter(ULocationData* LocationData)
|
|
||||||
{
|
|
||||||
if (!IsSuccess && LocationData->Tag.MatchesTag(TargetLocation->Tag))
|
|
||||||
{
|
|
||||||
Complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ULocationRestriction::OnAreaExit(ULocationData* LocationData)
|
|
||||||
{
|
|
||||||
if (IsSuccess && LocationData->Tag.MatchesTag(TargetLocation->Tag))
|
|
||||||
{
|
|
||||||
Init(Player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// © 2025 Naked People Team. All Rights Reserved.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "NakedDesire/MissionBuilder/GoalRestriction.h"
|
|
||||||
#include "LocationRestriction.generated.h"
|
|
||||||
|
|
||||||
class ULocationData;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
UCLASS()
|
|
||||||
class NAKEDDESIRE_API ULocationRestriction : public UGoalRestriction
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
FDelegateHandle AreaEnterHandle;
|
|
||||||
FDelegateHandle AreaExitHandle;
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly)
|
|
||||||
ULocationData* TargetLocation;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
|
|
||||||
virtual void Stop() override;
|
|
||||||
virtual FText GetDescription() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void OnAreaEnter(ULocationData* LocationData);
|
|
||||||
void OnAreaExit(ULocationData* LocationData);
|
|
||||||
};
|
|
||||||
@@ -11,7 +11,7 @@ public class NakedDesire : ModuleRules
|
|||||||
PublicDependencyModuleNames.AddRange(new string[]
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "CommonUI", "NavigationSystem",
|
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "CommonUI", "NavigationSystem",
|
||||||
"AIModule", "GameplayTags", "Slate", "SlateCore", "StructUtils"
|
"AIModule", "GameplayTags", "Slate", "SlateCore"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,19 +3,15 @@
|
|||||||
#include "NakedDesireCharacter.h"
|
#include "NakedDesireCharacter.h"
|
||||||
#include "NakedDesire/Clothing/ClothingManager.h"
|
#include "NakedDesire/Clothing/ClothingManager.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
#include "NakedDesire/MissionBuilder/MissionsManager.h"
|
|
||||||
#include "NakedDesire/Stats/StatsManager.h"
|
#include "NakedDesire/Stats/StatsManager.h"
|
||||||
#include "EnhancedInputComponent.h"
|
#include "EnhancedInputComponent.h"
|
||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "Internationalization/Text.h"
|
|
||||||
#include "NakedDesire/Censorship/CensorshipComponent.h"
|
#include "NakedDesire/Censorship/CensorshipComponent.h"
|
||||||
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
|
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
|
||||||
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
||||||
#include "NakedDesire/Clothing/ClothingVisualsComponent.h"
|
#include "NakedDesire/Clothing/ClothingVisualsComponent.h"
|
||||||
#include "NakedDesire/Global/Constants.h"
|
|
||||||
#include "NakedDesire/Global/NakedDesireHUD.h"
|
#include "NakedDesire/Global/NakedDesireHUD.h"
|
||||||
#include "NakedDesire/Global/NakedDesireUserSettings.h"
|
|
||||||
#include "NakedDesire/Interaction/InteractionComponent.h"
|
#include "NakedDesire/Interaction/InteractionComponent.h"
|
||||||
#include "NakedDesire/UI/GameLayoutWidget.h"
|
#include "NakedDesire/UI/GameLayoutWidget.h"
|
||||||
#include "Perception/AIPerceptionStimuliSourceComponent.h"
|
#include "Perception/AIPerceptionStimuliSourceComponent.h"
|
||||||
@@ -32,7 +28,6 @@ ANakedDesireCharacter::ANakedDesireCharacter()
|
|||||||
|
|
||||||
ClothingManager = CreateDefaultSubobject<UClothingManager>("Clothing Manager");
|
ClothingManager = CreateDefaultSubobject<UClothingManager>("Clothing Manager");
|
||||||
StatsManager = CreateDefaultSubobject<UStatsManager>("Stats Manager");
|
StatsManager = CreateDefaultSubobject<UStatsManager>("Stats Manager");
|
||||||
MissionsManager = CreateDefaultSubobject<UMissionsManager>("Missions Manager");
|
|
||||||
|
|
||||||
ClothingVisualsComponent = CreateDefaultSubobject<UClothingVisualsComponent>(TEXT("Clothing Visuals Component"));
|
ClothingVisualsComponent = CreateDefaultSubobject<UClothingVisualsComponent>(TEXT("Clothing Visuals Component"));
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class UClothingManager;
|
|||||||
class UClothingVisualsComponent;
|
class UClothingVisualsComponent;
|
||||||
class UCensorshipComponent;
|
class UCensorshipComponent;
|
||||||
class UStatsManager;
|
class UStatsManager;
|
||||||
class UMissionsManager;
|
|
||||||
class ANPCAIController;
|
class ANPCAIController;
|
||||||
class ULocationData;
|
class ULocationData;
|
||||||
|
|
||||||
@@ -87,9 +86,6 @@ public:
|
|||||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||||
UStatsManager* StatsManager;
|
UStatsManager* StatsManager;
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
|
||||||
UMissionsManager* MissionsManager;
|
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
|
||||||
UAIPerceptionStimuliSourceComponent* StimuliSourceComponent;
|
UAIPerceptionStimuliSourceComponent* StimuliSourceComponent;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "GameFramework/SaveGame.h"
|
#include "GameFramework/SaveGame.h"
|
||||||
#include "NakedDesire/Global/Constants.h"
|
#include "NakedDesire/Global/Constants.h"
|
||||||
#include "ItemSaveRecord.h"
|
#include "ItemSaveRecord.h"
|
||||||
|
#include "NakedDesire/Commissions/CommissionTypes.h"
|
||||||
#include "GlobalSaveGameData.generated.h"
|
#include "GlobalSaveGameData.generated.h"
|
||||||
|
|
||||||
class UItemInstance;
|
class UItemInstance;
|
||||||
@@ -42,6 +43,10 @@ public:
|
|||||||
bool RemoveWorldItem(UItemInstance* ItemInstance);
|
bool RemoveWorldItem(UItemInstance* ItemInstance);
|
||||||
TArray<FItemSaveRecord> GetWorldItems() const { return WorldItems; }
|
TArray<FItemSaveRecord> GetWorldItems() const { return WorldItems; }
|
||||||
|
|
||||||
|
// Commission board state (§13). State-level only; see FCommissionSaveRecord.
|
||||||
|
TArray<FCommissionSaveRecord> GetCommissionRecords() const { return Commissions; }
|
||||||
|
void SetCommissionRecords(const TArray<FCommissionSaveRecord>& InRecords) { Commissions = InRecords; }
|
||||||
|
|
||||||
UPROPERTY(SaveGame)
|
UPROPERTY(SaveGame)
|
||||||
int32 DaysPassed = 0;
|
int32 DaysPassed = 0;
|
||||||
|
|
||||||
@@ -66,4 +71,7 @@ private:
|
|||||||
|
|
||||||
UPROPERTY(SaveGame)
|
UPROPERTY(SaveGame)
|
||||||
TArray<FItemSaveRecord> WorldItems;
|
TArray<FItemSaveRecord> WorldItems;
|
||||||
|
|
||||||
|
UPROPERTY(SaveGame)
|
||||||
|
TArray<FCommissionSaveRecord> Commissions;
|
||||||
};
|
};
|
||||||
@@ -88,10 +88,33 @@ void UStatsManager::SetObserved(const bool bObserved, AActor* Observer)
|
|||||||
if (!Observer)
|
if (!Observer)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
bool bChanged = false;
|
||||||
if (bObserved)
|
if (bObserved)
|
||||||
Observers.AddUnique(Observer);
|
{
|
||||||
|
if (!Observers.Contains(Observer))
|
||||||
|
{
|
||||||
|
Observers.Add(Observer);
|
||||||
|
bChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
Observers.Remove(Observer);
|
{
|
||||||
|
bChanged = Observers.Remove(Observer) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bChanged)
|
||||||
|
OnObserversChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UStatsManager::GetObserverCount() const
|
||||||
|
{
|
||||||
|
int32 Count = 0;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Observer : Observers)
|
||||||
|
{
|
||||||
|
if (Observer.IsValid())
|
||||||
|
++Count;
|
||||||
|
}
|
||||||
|
return Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UStatsManager::IncreaseEmbarrassment(const float Amount)
|
void UStatsManager::IncreaseEmbarrassment(const float Amount)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
class UClothingManager;
|
class UClothingManager;
|
||||||
class ANakedDesireCharacter;
|
class ANakedDesireCharacter;
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAttributeUpdateSignature, float, CurrentValue, float, MaxValue);
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAttributeUpdateSignature, float, CurrentValue, float, MaxValue);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FObserversChangedSignature);
|
||||||
|
|
||||||
|
|
||||||
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
|
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
|
||||||
@@ -47,6 +48,14 @@ public:
|
|||||||
// body parts that observer can actually see (GDD §7.1).
|
// body parts that observer can actually see (GDD §7.1).
|
||||||
void SetObserved(bool bObserved, AActor* Observer);
|
void SetObserved(bool bObserved, AActor* Observer);
|
||||||
|
|
||||||
|
// Number of NPCs currently perceiving the player (used by commission objectives, §13.4).
|
||||||
|
UFUNCTION(BlueprintPure)
|
||||||
|
int32 GetObserverCount() const;
|
||||||
|
|
||||||
|
// Fires whenever the observer set changes (an NPC gains or loses sight of the player).
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FObserversChangedSignature OnObserversChanged;
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable)
|
UFUNCTION(BlueprintCallable)
|
||||||
void IncreaseEmbarrassment(float Amount);
|
void IncreaseEmbarrassment(float Amount);
|
||||||
void DecreaseEmbarrassment(float Amount);
|
void DecreaseEmbarrassment(float Amount);
|
||||||
|
|||||||
Reference in New Issue
Block a user