diff --git a/COMMISSIONS-BOARD.md b/COMMISSIONS-BOARD.md new file mode 100644 index 00000000..f3584e73 --- /dev/null +++ b/COMMISSIONS-BOARD.md @@ -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 EntryWidgetClass`. +- `NativeOnActivated`: grab `UMissionSubsystem`, bind `OnBoardChanged → Rebuild`, + `OnCommissionCompleted → HandleCompleted` (toast/flash), call `Rebuild()`. +- `NativeDestruct`: unbind both. +- `Rebuild()`: `OfferedList/ActiveList->ClearChildren()`; for each commission create an entry widget, + `Init(Commission, Subsystem)`, add to the right list. (Membership only changes on accept / complete / + abandon / day‑roll, all of which fire `OnBoardChanged`, so a full rebuild is cheap and correct.) + +### 2. `UCommissionEntryWidget : UCommonUserWidget` +One commission row. +- BindWidgets: `UCommonTextBlock* TitleText, PosterText, TierText, RewardText`, + `UVerticalBox* ObjectiveList`, `UCommonButtonBase* ActionButton`, `UCommonTextBlock* ActionLabel`. +- EditDefaultsOnly: `TSubclassOf ObjectiveRowClass`. +- `Init(UCommission*, UMissionSubsystem*)`: cache both, subscribe `Commission->OnStateChanged → + RefreshActionState`, render header (title/poster/tier), reward summary from `FCommissionReward` + (money / XP / followers — hide zero fields), build an objective row per `GetObjectives()`. +- Action button is state-driven (`Commission->GetState()`): + - `Offered` → label **Accept** → `Subsystem->AcceptCommission(Commission)`. + - `Accepted` → label **Abandon** → `Subsystem->AbandonCommission(Commission)`. + - `Completed` / `Expired` → button hidden/disabled (the rebuild usually removes it from the board). +- **Compact mode** for the HUD tracker: a `bCompact` flag (or a BP variant) hides poster / reward / + action button and shows just title + objective rows, so the tracker (#4) reuses this same widget. +- `NativeDestruct`: unsubscribe from the commission. + +### 3. `UCommissionObjectiveRowWidget : UCommonUserWidget` +One objective line. +- BindWidgets: `UCommonTextBlock* DescriptionText`, `UProgressBar* ProgressBar` (BindWidgetOptional), + `UImage* DoneCheck` (BindWidgetOptional). +- `Init(UCommissionObjective*)`: set `GetDescription()`, subscribe `OnStateChanged → RefreshDone`. +- Progress: objectives have **no per-frame progress delegate** — only `OnStateChanged` (fires on + satisfied). For the continuous ones (`RequiredHoldSeconds` holds, `RunNakedDistance`) drive the bar + from `NativeTick` polling `GetProgress()` **while the screen is open** (cheap, self-contained). Binary + objectives just toggle `DoneCheck` on `OnStateChanged`. (If polling ever feels wasteful, add an + `OnProgressChanged` delegate to `UCommissionObjective` later — not needed for the slice.) + +### 4. `UCommissionTrackerWidget : UCommonUserWidget` +Always-on HUD element showing accepted commissions' current objectives + live progress while the player +is in the world. **Passive HUD — not activatable, never captures input.** +- Hosted as a `BindWidget` child of the existing `UHUDWidget` (the always-on HUD), so it shows during + normal play and not just at the PC. +- BindWidgets: `UVerticalBox* TrackerList`; EditDefaultsOnly `TSubclassOf + TrackerEntryWidgetClass`. +- `NativeConstruct`: grab `UMissionSubsystem`, bind `OnBoardChanged → Rebuild`, call `Rebuild()`. + `NativeDestruct`: unbind. +- `Rebuild()`: clear; for each `GetAcceptedCommissions()` add a `UCommissionEntryWidget` in **compact + mode** (title + objective rows, no reward/poster/button). Collapse the whole widget when the active + list is empty. +- Live progress comes from the reused objective rows' own `NativeTick` poll — no extra wiring here. + +## Entry point (how the board opens) + +§13 says phone or PC; the phone stack is Phase 8, so for the slice use the **apartment PC** as an +interactable, mirroring `AWardrobe`: +- New `AForumTerminal` (or `AApartmentPC`) `IInteractable` → `Interact` calls + `HUD->GetGameLayoutWidget()->OpenCommissionBoard()`. +- `UGameLayoutWidget`: add `OpenCommissionBoard()` + `TSubclassOf + CommissionBoardScreenWidgetClass` + the instance ptr (same shape as `OpenWardrobe`). +- Add a debug input action (or console exec) to open it directly for testing before the PC art exists. +- Phone "Forum app" entry point is added in Phase 8 and calls the same `OpenCommissionBoard()`. + +## Data flow & live updates + +- **Membership** (which list a commission is in): `OnBoardChanged` → full `Rebuild()`. +- **Action state** of a visible entry (e.g. accepted→completed while open): `Commission->OnStateChanged` + → `RefreshActionState`; the subsequent `OnBoardChanged` (fired by the subsystem on completion) rebuilds + and moves it out of Active. +- **Objective progress**: `OnStateChanged` for done state + `NativeTick` poll for the progress bar. +- **Completion feedback**: `OnCommissionCompleted` → brief toast / reward popup (reuse existing + `UI/` + `Audio/UI` tone). Optional for the slice. + +## Layout sketch + +``` +┌─ Commission Board ───────────────────────────────────────────┐ +│ BOARD (offered) ACTIVE (accepted) │ +│ ┌────────────────────────────┐ ┌──────────────────────┐ │ +│ │ Flash a Stranger [Daily]│ │ Beach Streak [Daily] │ │ +│ │ posted by sk8r_gurl │ │ ▸ Run 50 m naked 62% │ │ +│ │ ▸ Expose boobs to 1 person │ │ ▸ while at the beach │ │ +│ │ $150 · 20 XP · +5 followers│ │ [ Abandon ]│ │ +│ │ [ Accept ]│ └──────────────────────┘ │ +│ └────────────────────────────┘ │ +│ … more offered … (empty → "No active │ +│ commissions") │ +└──────────────────────────────────────────────────────────────┘ +``` +(Two columns is the simplest; a Board/Active tab pair is an equivalent alternative — see decisions.) + +## C++ vs Blueprint split + +- **C++ (this plan):** the four widget classes above (screen, entry, objective row, HUD tracker) + + `OpenCommissionBoard` on `GameLayoutWidget` + the PC interactable. All subsystem calls, subscriptions, + list rebuilds, and button handlers live here. +- **Blueprint:** `WBP_CommissionBoardScreen`, `WBP_CommissionEntry`, `WBP_CommissionObjectiveRow`, + `WBP_CommissionTracker` — visual layout only, satisfying the `BindWidget` names above and setting the + `…WidgetClass` defaults. `WBP_CommissionEntry` carries the full and compact looks (compact = tracker). + +## Subsystem changes needed + +None required — the existing API covers it. Nice-to-haves to consider during build: +- A `GetCompletedCommissions()` getter if a "completed today" section is wanted (data already tracked). +- Confirm `OnBoardChanged` fires on **every** membership change (it does: accept / abandon / complete / + day‑roll all call it). + +## Edge cases to handle + +- Empty board / empty active list → show a label, not a blank panel. +- A commission completing while the screen is open → entry shows done, then rebuild removes it from + Active (and the completion toast fires once). +- Day roll while the screen is open → `OnBoardChanged` rebuilds; accepted-but-unfinished entries vanish + (expired) and a fresh offered set appears. Make sure entry widgets unsubscribe on rebuild (`NativeDestruct`). +- Abandon returns the commission to the board (Offered) in the same session. +- Re-entrancy: accepting an already-satisfiable commission completes instantly; the entry should tolerate + `Accepted → Completed` within one frame (drive purely off `GetState()` + the rebuild). + +## Locked decisions (slice) + +1. **Layout — two columns** (Board | Active), not tabs. Everything visible at once; simplest. +2. **Entry point — apartment PC interactable** (`AForumTerminal`) calls `OpenCommissionBoard()`, plus a + debug console exec to open it before the PC art exists. Phone "Forum app" reuses the same call (Phase 8). +3. **Progress display — both:** a progress bar (driven by `NativeTick` polling `GetProgress()`) for the + continuous objectives and a checkmark for binary ones, via `BindWidgetOptional` on the row widget. +4. **Completion feedback — minimal toast** on commission completion (reuse existing `UI/` + `Audio/UI`). +5. **In-world HUD objective tracker — yes, in scope.** A separate always-on HUD widget + (`UCommissionTrackerWidget`, see breakdown #4) shows accepted commissions' current objectives + live + progress while the player is out in the world (GDD §0.5 'objective-tracker widget'). The board is for + browse/accept at the PC; the tracker is for following objectives in the field. It reuses the shared + entry/row widgets in a compact form, so it adds one widget — not a parallel hierarchy. +6. **Modal behavior — mirror the inventory/wardrobe screens.** The board uses the same input-config / + pause setup the existing `UCommonActivatableWidget` screens use; read `UInventoryScreenWidget` / + `UWardrobeScreenWidget` (+ their BP activation / `GetDesiredInputConfig` settings) and match them for a + consistent menu UX. The HUD tracker is **not** an activatable widget — it's passive HUD and never + captures input or pauses anything. + +## Build order (when greenlit) + +1. Read `UInventoryScreenWidget` / `UWardrobeScreenWidget` input-config + activation settings and reuse + the same convention for the board (decision #6). +2. `UCommissionObjectiveRowWidget` → `UCommissionEntryWidget` (incl. `bCompact`) → `UCommissionBoardScreenWidget`. +3. `UGameLayoutWidget::OpenCommissionBoard()` + class ref; debug console exec to open. +4. `UCommissionTrackerWidget` on `UHUDWidget` (reuses the entry/row widgets in compact mode). +5. Author the `WBP_*` assets (board screen, entry, objective row, tracker) against the BindWidget contracts. +6. `AForumTerminal` interactable (apartment PC) → `OpenCommissionBoard`. +7. Author a `UCommissionBoardConfig` with a few test commissions and play-test the full accept → satisfy → + reward → expire loop, plus the HUD tracker updating in the field (ties into the VS‑4 / VS‑7 exit checks). \ No newline at end of file diff --git a/COMMISSIONS.md b/COMMISSIONS.md new file mode 100644 index 00000000..2a296a91 --- /dev/null +++ b/COMMISSIONS.md @@ -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. \ No newline at end of file diff --git a/Content/Blueprints/Data/CommissionBoardConfig.uasset b/Content/Blueprints/Data/CommissionBoardConfig.uasset new file mode 100644 index 00000000..30a98f6f --- /dev/null +++ b/Content/Blueprints/Data/CommissionBoardConfig.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:766a229225a5528bc11db90fa0564a9bd3ed7ad9ffd6d07fe22a17273909dbce +size 2969 diff --git a/Content/Blueprints/Data/MissionsConfig.uasset b/Content/Blueprints/Data/MissionsConfig.uasset deleted file mode 100644 index c9df33df..00000000 --- a/Content/Blueprints/Data/MissionsConfig.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9afdf03c599de034c559bc05d9be891b78fd5ec8a173f037d3c4adc506e0ee1b -size 3785 diff --git a/Content/Test/Maps/TestLevel.umap b/Content/Test/Maps/TestLevel.umap index 2a80bf38..48cbf08c 100644 --- a/Content/Test/Maps/TestLevel.umap +++ b/Content/Test/Maps/TestLevel.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ab375fb9c0f53f57aaa96223898f268cbf8fae54f2b23010322466a179070cb -size 149095 +oid sha256:9f8b7f453570c2d869f6da438384698746dce88eb1ea10025ddcac1b336d39a8 +size 148528 diff --git a/PLAN.md b/PLAN.md index 24838ebb..fa4a5db8 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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*`). - **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. -- **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). -- **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 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). diff --git a/Source/NakedDesire/Commissions/Commission.cpp b/Source/NakedDesire/Commissions/Commission.cpp new file mode 100644 index 00000000..df6ee382 --- /dev/null +++ b/Source/NakedDesire/Commissions/Commission.cpp @@ -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(); +} \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Commission.h b/Source/NakedDesire/Commissions/Commission.h new file mode 100644 index 00000000..be39a63e --- /dev/null +++ b/Source/NakedDesire/Commissions/Commission.h @@ -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 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 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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/CommissionBoardConfig.h b/Source/NakedDesire/Commissions/CommissionBoardConfig.h new file mode 100644 index 00000000..2b731045 --- /dev/null +++ b/Source/NakedDesire/Commissions/CommissionBoardConfig.h @@ -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 Commissions; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/CommissionConstraint.cpp b/Source/NakedDesire/Commissions/CommissionConstraint.cpp new file mode 100644 index 00000000..3082b241 --- /dev/null +++ b/Source/NakedDesire/Commissions/CommissionConstraint.cpp @@ -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(); +} \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/CommissionConstraint.h b/Source/NakedDesire/Commissions/CommissionConstraint.h new file mode 100644 index 00000000..5abefc82 --- /dev/null +++ b/Source/NakedDesire/Commissions/CommissionConstraint.h @@ -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(); } +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/CommissionObjective.cpp b/Source/NakedDesire/Commissions/CommissionObjective.cpp new file mode 100644 index 00000000..f8e4d4e9 --- /dev/null +++ b/Source/NakedDesire/Commissions/CommissionObjective.cpp @@ -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; +} \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/CommissionObjective.h b/Source/NakedDesire/Commissions/CommissionObjective.h new file mode 100644 index 00000000..b3c6a377 --- /dev/null +++ b/Source/NakedDesire/Commissions/CommissionObjective.h @@ -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> 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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/CommissionTypes.h b/Source/NakedDesire/Commissions/CommissionTypes.h new file mode 100644 index 00000000..42f6cc92 --- /dev/null +++ b/Source/NakedDesire/Commissions/CommissionTypes.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/DayPhaseConstraint.cpp b/Source/NakedDesire/Commissions/Constraints/DayPhaseConstraint.cpp new file mode 100644 index 00000000..6302065e --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/DayPhaseConstraint.cpp @@ -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() : nullptr; +} + +FText UDayPhaseConstraint::GetDescription() const +{ + return (RequiredPhase == EDayPhase::Night) + ? LOCTEXT("Night", "at night") + : LOCTEXT("Day", "during the day"); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/DayPhaseConstraint.h b/Source/NakedDesire/Commissions/Constraints/DayPhaseConstraint.h new file mode 100644 index 00000000..2edf1315 --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/DayPhaseConstraint.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/LocationConstraint.cpp b/Source/NakedDesire/Commissions/Constraints/LocationConstraint.cpp new file mode 100644 index 00000000..2df8c237 --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/LocationConstraint.cpp @@ -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() : nullptr; +} + +FText ULocationConstraint::GetDescription() const +{ + return FText::Format(LOCTEXT("AtLocation", "while at {0}"), FText::FromName(RequiredLocation.GetTagName())); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/LocationConstraint.h b/Source/NakedDesire/Commissions/Constraints/LocationConstraint.h new file mode 100644 index 00000000..9a83123f --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/LocationConstraint.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/ObservedConstraint.cpp b/Source/NakedDesire/Commissions/Constraints/ObservedConstraint.cpp new file mode 100644 index 00000000..2fa0a405 --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/ObservedConstraint.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/ObservedConstraint.h b/Source/NakedDesire/Commissions/Constraints/ObservedConstraint.h new file mode 100644 index 00000000..9a64ddbd --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/ObservedConstraint.h @@ -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(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/WearingSlotConstraint.cpp b/Source/NakedDesire/Commissions/Constraints/WearingSlotConstraint.cpp new file mode 100644 index 00000000..03344d82 --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/WearingSlotConstraint.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Constraints/WearingSlotConstraint.h b/Source/NakedDesire/Commissions/Constraints/WearingSlotConstraint.h new file mode 100644 index 00000000..02f3653f --- /dev/null +++ b/Source/NakedDesire/Commissions/Constraints/WearingSlotConstraint.h @@ -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); +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/MissionSubsystem.cpp b/Source/NakedDesire/Commissions/MissionSubsystem.cpp new file mode 100644 index 00000000..ec5e0377 --- /dev/null +++ b/Source/NakedDesire/Commissions/MissionSubsystem.cpp @@ -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()) + Time->OnDayChanged.AddUniqueDynamic(this, &UMissionSubsystem::HandleDayChanged); + + BuildBoard(); + RestoreFromSave(); + OnBoardChanged.Broadcast(); +} + +void UMissionSubsystem::Deinitialize() +{ + if (const UWorld* World = GetWorld()) + { + if (UTimeOfDaySubsystem* Time = World->GetSubsystem()) + 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(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 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(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); +} + +UGlobalSaveGameData* UMissionSubsystem::GetSave() const +{ + UGameInstance* GameInstance = GetWorld() ? GetWorld()->GetGameInstance() : nullptr; + USaveSubsystem* SaveSubsystem = GameInstance ? GameInstance->GetSubsystem() : nullptr; + return SaveSubsystem ? SaveSubsystem->GetCurrentSave() : nullptr; +} + +UCommissionBoardConfig* UMissionSubsystem::GetBoardConfig() const +{ + const UNakedDesireGameInstance* GameInstance = + Cast(GetWorld() ? GetWorld()->GetGameInstance() : nullptr); + return GameInstance ? GameInstance->CommissionBoard : nullptr; +} \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/MissionSubsystem.h b/Source/NakedDesire/Commissions/MissionSubsystem.h new file mode 100644 index 00000000..50b68275 --- /dev/null +++ b/Source/NakedDesire/Commissions/MissionSubsystem.h @@ -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& GetOfferedCommissions() const { return OfferedCommissions; } + + UFUNCTION(BlueprintPure) + const TArray& 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 OfferedCommissions; + + UPROPERTY() + TArray AcceptedCommissions; + + UPROPERTY() + TArray CompletedCommissions; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/BeFullyNakedNearNPCsObjective.cpp b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedNearNPCsObjective.cpp new file mode 100644 index 00000000..26eabcc3 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedNearNPCsObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/BeFullyNakedNearNPCsObjective.h b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedNearNPCsObjective.h new file mode 100644 index 00000000..0cf1cc95 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedNearNPCsObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/BeFullyNakedObjective.cpp b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedObjective.cpp new file mode 100644 index 00000000..e2b205b7 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/BeFullyNakedObjective.h b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedObjective.h new file mode 100644 index 00000000..40a0e73a --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/BeFullyNakedObjective.h @@ -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(); } +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/BeObservedWhileExposedObjective.cpp b/Source/NakedDesire/Commissions/Objectives/BeObservedWhileExposedObjective.cpp new file mode 100644 index 00000000..d038b1a5 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/BeObservedWhileExposedObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/BeObservedWhileExposedObjective.h b/Source/NakedDesire/Commissions/Objectives/BeObservedWhileExposedObjective.h new file mode 100644 index 00000000..62eebf46 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/BeObservedWhileExposedObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/CoverageObjectiveBase.cpp b/Source/NakedDesire/Commissions/Objectives/CoverageObjectiveBase.cpp new file mode 100644 index 00000000..c5c75fdd --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/CoverageObjectiveBase.cpp @@ -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(); +} \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/CoverageObjectiveBase.h b/Source/NakedDesire/Commissions/Objectives/CoverageObjectiveBase.h new file mode 100644 index 00000000..d9c77e4b --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/CoverageObjectiveBase.h @@ -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); +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/ExposeBodyPartObjective.cpp b/Source/NakedDesire/Commissions/Objectives/ExposeBodyPartObjective.cpp new file mode 100644 index 00000000..6d0266fc --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/ExposeBodyPartObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/ExposeBodyPartObjective.h b/Source/NakedDesire/Commissions/Objectives/ExposeBodyPartObjective.h new file mode 100644 index 00000000..56dc6dc6 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/ExposeBodyPartObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/GatherCrowdObjective.cpp b/Source/NakedDesire/Commissions/Objectives/GatherCrowdObjective.cpp new file mode 100644 index 00000000..ee0f4cb1 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/GatherCrowdObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/GatherCrowdObjective.h b/Source/NakedDesire/Commissions/Objectives/GatherCrowdObjective.h new file mode 100644 index 00000000..93d1f8ce --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/GatherCrowdObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/ObserverObjectiveBase.cpp b/Source/NakedDesire/Commissions/Objectives/ObserverObjectiveBase.cpp new file mode 100644 index 00000000..881223ec --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/ObserverObjectiveBase.cpp @@ -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(); +} \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/ObserverObjectiveBase.h b/Source/NakedDesire/Commissions/Objectives/ObserverObjectiveBase.h new file mode 100644 index 00000000..b8a89052 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/ObserverObjectiveBase.h @@ -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(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/ReachEmbarrassmentObjective.cpp b/Source/NakedDesire/Commissions/Objectives/ReachEmbarrassmentObjective.cpp new file mode 100644 index 00000000..bf0766ff --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/ReachEmbarrassmentObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/ReachEmbarrassmentObjective.h b/Source/NakedDesire/Commissions/Objectives/ReachEmbarrassmentObjective.h new file mode 100644 index 00000000..246f3d82 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/ReachEmbarrassmentObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/RunNakedDistanceObjective.cpp b/Source/NakedDesire/Commissions/Objectives/RunNakedDistanceObjective.cpp new file mode 100644 index 00000000..3b2df5b8 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/RunNakedDistanceObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/RunNakedDistanceObjective.h b/Source/NakedDesire/Commissions/Objectives/RunNakedDistanceObjective.h new file mode 100644 index 00000000..7719b150 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/RunNakedDistanceObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/StayUnseenWhileNakedObjective.cpp b/Source/NakedDesire/Commissions/Objectives/StayUnseenWhileNakedObjective.cpp new file mode 100644 index 00000000..f1bfced6 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/StayUnseenWhileNakedObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/StayUnseenWhileNakedObjective.h b/Source/NakedDesire/Commissions/Objectives/StayUnseenWhileNakedObjective.h new file mode 100644 index 00000000..1547b6f6 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/StayUnseenWhileNakedObjective.h @@ -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; } +}; \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/WearOnlyUnderwearObjective.cpp b/Source/NakedDesire/Commissions/Objectives/WearOnlyUnderwearObjective.cpp new file mode 100644 index 00000000..5d5e74d5 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/WearOnlyUnderwearObjective.cpp @@ -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 \ No newline at end of file diff --git a/Source/NakedDesire/Commissions/Objectives/WearOnlyUnderwearObjective.h b/Source/NakedDesire/Commissions/Objectives/WearOnlyUnderwearObjective.h new file mode 100644 index 00000000..e2ff7133 --- /dev/null +++ b/Source/NakedDesire/Commissions/Objectives/WearOnlyUnderwearObjective.h @@ -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; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Global/NakedDesireGameInstance.h b/Source/NakedDesire/Global/NakedDesireGameInstance.h index d8d776cb..d0ef9144 100644 --- a/Source/NakedDesire/Global/NakedDesireGameInstance.h +++ b/Source/NakedDesire/Global/NakedDesireGameInstance.h @@ -6,6 +6,7 @@ #include "NakedDesireGameInstance.generated.h" class UStartingSaveData; +class UCommissionBoardConfig; UCLASS() class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance @@ -15,4 +16,8 @@ class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance public: UPROPERTY(EditDefaultsOnly, Category = "Save") TObjectPtr StartingSaveData; + + // Hand-authored commission pool the UMissionSubsystem offers (§13). + UPROPERTY(EditDefaultsOnly, Category = "Commissions") + TObjectPtr CommissionBoard; }; \ No newline at end of file diff --git a/Source/NakedDesire/Global/NakedDesireGameplayTags.cpp b/Source/NakedDesire/Global/NakedDesireGameplayTags.cpp new file mode 100644 index 00000000..998282e0 --- /dev/null +++ b/Source/NakedDesire/Global/NakedDesireGameplayTags.cpp @@ -0,0 +1,6 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "NakedDesireGameplayTags.h" + +UE_DEFINE_GAMEPLAY_TAG(TAG_Location_Apartment, "Location.Apartment"); \ No newline at end of file diff --git a/Source/NakedDesire/Global/NakedDesireGameplayTags.h b/Source/NakedDesire/Global/NakedDesireGameplayTags.h new file mode 100644 index 00000000..2d43b020 --- /dev/null +++ b/Source/NakedDesire/Global/NakedDesireGameplayTags.h @@ -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); \ No newline at end of file diff --git a/Source/NakedDesire/Global/SessionManagerSubsystem.cpp b/Source/NakedDesire/Global/SessionManagerSubsystem.cpp index fb5e83f8..fc1f58f3 100644 --- a/Source/NakedDesire/Global/SessionManagerSubsystem.cpp +++ b/Source/NakedDesire/Global/SessionManagerSubsystem.cpp @@ -5,6 +5,9 @@ #include "Kismet/GameplayStatics.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/Stats/StatsManager.h" @@ -12,6 +15,13 @@ void USessionManagerSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); + // The apartment threshold is now a location event (Location.Apartment), not a per-trigger flag. + if (ULocationSubsystem* Locations = InWorld.GetSubsystem()) + { + 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 world begins play, so defer binding by one tick. InWorld.GetTimerManager().SetTimerForNextTick(this, &USessionManagerSubsystem::BindToPlayerStats); @@ -30,19 +40,19 @@ void USessionManagerSubsystem::BindToPlayerStats() 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). - if (bSessionActive) + // Returning to the apartment is the safe end of a session (§4.3). + if (Location && Location->Tag.MatchesTag(TAG_Location_Apartment) && bSessionActive) { EndSession(ESessionLossCause::SafeReturn); } } -void USessionManagerSubsystem::NotifyLeftApartment() +void USessionManagerSubsystem::HandleLocationExited(ULocationData* Location) { // 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(); } diff --git a/Source/NakedDesire/Global/SessionManagerSubsystem.h b/Source/NakedDesire/Global/SessionManagerSubsystem.h index b435276d..90c4c546 100644 --- a/Source/NakedDesire/Global/SessionManagerSubsystem.h +++ b/Source/NakedDesire/Global/SessionManagerSubsystem.h @@ -7,6 +7,7 @@ #include "SessionManagerSubsystem.generated.h" class UStatsManager; +class ULocationData; /** * 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 * detected by subscribing to UStatsManager. This replaces the EndGameEmbarrassed * 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() class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem @@ -42,10 +47,6 @@ class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem public: 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") bool IsSessionActive() const { return bSessionActive; } @@ -69,6 +70,12 @@ private: void BindToPlayerStats(); + UFUNCTION() + void HandleLocationEntered(ULocationData* Location); + + UFUNCTION() + void HandleLocationExited(ULocationData* Location); + UFUNCTION() void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue); diff --git a/Source/NakedDesire/Locations/LocationSubsystem.cpp b/Source/NakedDesire/Locations/LocationSubsystem.cpp new file mode 100644 index 00000000..3867ccef --- /dev/null +++ b/Source/NakedDesire/Locations/LocationSubsystem.cpp @@ -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, 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, int32>& Pair : ActiveCounts) + { + if (Pair.Key && Pair.Key->Tag.IsValid()) + ActiveTags.AddTag(Pair.Key->Tag); + } +} \ No newline at end of file diff --git a/Source/NakedDesire/Locations/LocationSubsystem.h b/Source/NakedDesire/Locations/LocationSubsystem.h new file mode 100644 index 00000000..ec5df596 --- /dev/null +++ b/Source/NakedDesire/Locations/LocationSubsystem.h @@ -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, int32> ActiveCounts; + + FGameplayTagContainer ActiveTags; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Locations/LocationTrigger.cpp b/Source/NakedDesire/Locations/LocationTrigger.cpp index 461682be..894d8484 100644 --- a/Source/NakedDesire/Locations/LocationTrigger.cpp +++ b/Source/NakedDesire/Locations/LocationTrigger.cpp @@ -1,10 +1,10 @@ -// © 2025 Naked People Team. All Rights Reserved. +// © 2025 Naked People Team. All Rights Reserved. #include "LocationTrigger.h" #include "Components/BoxComponent.h" -#include "NakedDesire/Global/SessionManagerSubsystem.h" +#include "LocationSubsystem.h" #include "NakedDesire/Player/NakedDesireCharacter.h" @@ -25,11 +25,8 @@ void ALocationTrigger::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, @@ -38,10 +35,8 @@ void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComp if (!OtherActor || !OtherActor->IsA()) return; - if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem()) - { - SessionManager->NotifyEnteredApartment(); - } + if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem()) + Locations->EnterLocation(LocationData); } void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, @@ -50,9 +45,6 @@ void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedCompon if (!OtherActor || !OtherActor->IsA()) return; - if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem()) - { - SessionManager->NotifyLeftApartment(); - } -} - + if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem()) + Locations->ExitLocation(LocationData); +} \ No newline at end of file diff --git a/Source/NakedDesire/Locations/LocationTrigger.h b/Source/NakedDesire/Locations/LocationTrigger.h index fa80f26f..7bc0d482 100644 --- a/Source/NakedDesire/Locations/LocationTrigger.h +++ b/Source/NakedDesire/Locations/LocationTrigger.h @@ -1,4 +1,4 @@ -// © 2025 Naked People Team. All Rights Reserved. +// © 2025 Naked People Team. All Rights Reserved. #pragma once @@ -9,6 +9,9 @@ class ULocationData; 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() class NAKEDDESIRE_API ALocationTrigger : public AActor { @@ -20,12 +23,6 @@ class NAKEDDESIRE_API ALocationTrigger : public AActor UPROPERTY(EditAnywhere) 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: ALocationTrigger(); @@ -42,4 +39,4 @@ private: UFUNCTION() void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); -}; +}; \ No newline at end of file diff --git a/Source/NakedDesire/MissionBuilder/GoalRestriction.cpp b/Source/NakedDesire/MissionBuilder/GoalRestriction.cpp deleted file mode 100644 index 7f4c3fe1..00000000 --- a/Source/NakedDesire/MissionBuilder/GoalRestriction.cpp +++ /dev/null @@ -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(); -} diff --git a/Source/NakedDesire/MissionBuilder/GoalRestriction.h b/Source/NakedDesire/MissionBuilder/GoalRestriction.h deleted file mode 100644 index ba73fb22..00000000 --- a/Source/NakedDesire/MissionBuilder/GoalRestriction.h +++ /dev/null @@ -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; -}; diff --git a/Source/NakedDesire/MissionBuilder/Goals/FlashGoal.cpp b/Source/NakedDesire/MissionBuilder/Goals/FlashGoal.cpp deleted file mode 100644 index 7ee74056..00000000 --- a/Source/NakedDesire/MissionBuilder/Goals/FlashGoal.cpp +++ /dev/null @@ -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(); - } -} diff --git a/Source/NakedDesire/MissionBuilder/Goals/FlashGoal.h b/Source/NakedDesire/MissionBuilder/Goals/FlashGoal.h deleted file mode 100644 index f8f99564..00000000 --- a/Source/NakedDesire/MissionBuilder/Goals/FlashGoal.h +++ /dev/null @@ -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 NoticedActors; - -public: - virtual void Init(ANakedDesireCharacter* PlayerCharacter) override; - virtual void Complete() override; - virtual FText GetDescription() const override; - -private: - void OnPlayerNoticed(ANPCAIController* NPC); -}; diff --git a/Source/NakedDesire/MissionBuilder/Goals/MinTimeGoal.cpp b/Source/NakedDesire/MissionBuilder/Goals/MinTimeGoal.cpp deleted file mode 100644 index 9a84556b..00000000 --- a/Source/NakedDesire/MissionBuilder/Goals/MinTimeGoal.cpp +++ /dev/null @@ -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(); -} diff --git a/Source/NakedDesire/MissionBuilder/Goals/MinTimeGoal.h b/Source/NakedDesire/MissionBuilder/Goals/MinTimeGoal.h deleted file mode 100644 index 7cc87106..00000000 --- a/Source/NakedDesire/MissionBuilder/Goals/MinTimeGoal.h +++ /dev/null @@ -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(); -}; diff --git a/Source/NakedDesire/MissionBuilder/Mission.cpp b/Source/NakedDesire/MissionBuilder/Mission.cpp deleted file mode 100644 index ef5f16a9..00000000 --- a/Source/NakedDesire/MissionBuilder/Mission.cpp +++ /dev/null @@ -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; -} diff --git a/Source/NakedDesire/MissionBuilder/Mission.h b/Source/NakedDesire/MissionBuilder/Mission.h deleted file mode 100644 index 337dde37..00000000 --- a/Source/NakedDesire/MissionBuilder/Mission.h +++ /dev/null @@ -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 Goals; - - UPROPERTY(EditDefaultsOnly) - int MoneyReward = 0; - - bool IsCompleted = false; - - TArray GoalUpdateHandles; - -public: - FMissionCompleteSignature OnComplete; - - void Init(ANakedDesireCharacter* PlayerCharacter); - - UFUNCTION(BlueprintPure) - int GetMoneyReward() const - { - return MoneyReward; - } - - UFUNCTION(BlueprintPure) - TArray 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(); -}; diff --git a/Source/NakedDesire/MissionBuilder/MissionGoal.cpp b/Source/NakedDesire/MissionBuilder/MissionGoal.cpp deleted file mode 100644 index 8b42f2fc..00000000 --- a/Source/NakedDesire/MissionBuilder/MissionGoal.cpp +++ /dev/null @@ -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); -} diff --git a/Source/NakedDesire/MissionBuilder/MissionGoal.h b/Source/NakedDesire/MissionBuilder/MissionGoal.h deleted file mode 100644 index 0a09b63f..00000000 --- a/Source/NakedDesire/MissionBuilder/MissionGoal.h +++ /dev/null @@ -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 Restrictions; - - TArray RestrictionUpdateHandles; - -public: - virtual void Init(ANakedDesireCharacter* PlayerCharacter); - virtual void Complete(); - - UFUNCTION(BlueprintPure) - bool GetIsCompleted() const - { - return IsCompleted; - } - - UFUNCTION(BlueprintPure) - TArray 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; -}; diff --git a/Source/NakedDesire/MissionBuilder/MissionsConfig.cpp b/Source/NakedDesire/MissionBuilder/MissionsConfig.cpp deleted file mode 100644 index a7545025..00000000 --- a/Source/NakedDesire/MissionBuilder/MissionsConfig.cpp +++ /dev/null @@ -1,4 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - - -#include "MissionsConfig.h" diff --git a/Source/NakedDesire/MissionBuilder/MissionsConfig.h b/Source/NakedDesire/MissionBuilder/MissionsConfig.h deleted file mode 100644 index ab82fd3e..00000000 --- a/Source/NakedDesire/MissionBuilder/MissionsConfig.h +++ /dev/null @@ -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 Missions; -}; - -/** - * - */ -UCLASS() -class NAKEDDESIRE_API UMissionsConfig : public UPrimaryDataAsset -{ - GENERATED_BODY() - -public: - UPROPERTY(EditDefaultsOnly) - TArray DailyMissions; - - UPROPERTY(EditDefaultsOnly) - TArray WeeklyMissions; -}; diff --git a/Source/NakedDesire/MissionBuilder/MissionsManager.cpp b/Source/NakedDesire/MissionBuilder/MissionsManager.cpp deleted file mode 100644 index 66dc56b3..00000000 --- a/Source/NakedDesire/MissionBuilder/MissionsManager.cpp +++ /dev/null @@ -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(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& 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); - } -} - diff --git a/Source/NakedDesire/MissionBuilder/MissionsManager.h b/Source/NakedDesire/MissionBuilder/MissionsManager.h deleted file mode 100644 index 43033570..00000000 --- a/Source/NakedDesire/MissionBuilder/MissionsManager.h +++ /dev/null @@ -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 AvailableMissions; - - UPROPERTY(BlueprintReadWrite) - TArray CompletedMissions; - - UPROPERTY(BlueprintAssignable) - FOnMissionCompletedSignature OnMissionCompleted; - - UPROPERTY(BlueprintAssignable) - FOnRewardsCollected OnRewardsCollected; - - void CompleteMission(UMission* Mission); - - UFUNCTION() - void RefreshDailyMissions(const TArray& NewMissions); - - virtual void BeginPlay() override; -}; diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/EquipClothingRestriction.cpp b/Source/NakedDesire/MissionBuilder/Restrictions/EquipClothingRestriction.cpp deleted file mode 100644 index 92045b0c..00000000 --- a/Source/NakedDesire/MissionBuilder/Restrictions/EquipClothingRestriction.cpp +++ /dev/null @@ -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; - // } - // } -} diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/EquipClothingRestriction.h b/Source/NakedDesire/MissionBuilder/Restrictions/EquipClothingRestriction.h deleted file mode 100644 index 5516ac79..00000000 --- a/Source/NakedDesire/MissionBuilder/Restrictions/EquipClothingRestriction.h +++ /dev/null @@ -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 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(); -}; diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/ExposeBodyPartRestriction.cpp b/Source/NakedDesire/MissionBuilder/Restrictions/ExposeBodyPartRestriction.cpp deleted file mode 100644 index 7cf31e97..00000000 --- a/Source/NakedDesire/MissionBuilder/Restrictions/ExposeBodyPartRestriction.cpp +++ /dev/null @@ -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(); - // } -} diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/ExposeBodyPartRestriction.h b/Source/NakedDesire/MissionBuilder/Restrictions/ExposeBodyPartRestriction.h deleted file mode 100644 index 19e60692..00000000 --- a/Source/NakedDesire/MissionBuilder/Restrictions/ExposeBodyPartRestriction.h +++ /dev/null @@ -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(); -}; diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp b/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp deleted file mode 100644 index 1abeab69..00000000 --- a/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp +++ /dev/null @@ -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); - } -} diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.h b/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.h deleted file mode 100644 index 66d60d41..00000000 --- a/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.h +++ /dev/null @@ -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); -}; diff --git a/Source/NakedDesire/NakedDesire.Build.cs b/Source/NakedDesire/NakedDesire.Build.cs index 1c182888..1347bf46 100644 --- a/Source/NakedDesire/NakedDesire.Build.cs +++ b/Source/NakedDesire/NakedDesire.Build.cs @@ -11,7 +11,7 @@ public class NakedDesire : ModuleRules PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "CommonUI", "NavigationSystem", - "AIModule", "GameplayTags", "Slate", "SlateCore", "StructUtils" + "AIModule", "GameplayTags", "Slate", "SlateCore" }); } } \ No newline at end of file diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.cpp b/Source/NakedDesire/Player/NakedDesireCharacter.cpp index 66cb081c..ac3f33cb 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.cpp +++ b/Source/NakedDesire/Player/NakedDesireCharacter.cpp @@ -3,19 +3,15 @@ #include "NakedDesireCharacter.h" #include "NakedDesire/Clothing/ClothingManager.h" #include "GameFramework/CharacterMovementComponent.h" -#include "NakedDesire/MissionBuilder/MissionsManager.h" #include "NakedDesire/Stats/StatsManager.h" #include "EnhancedInputComponent.h" #include "EnhancedInputSubsystems.h" #include "Kismet/GameplayStatics.h" -#include "Internationalization/Text.h" #include "NakedDesire/Censorship/CensorshipComponent.h" #include "NakedDesire/Clothing/ClothingItemDefinition.h" #include "NakedDesire/Clothing/ClothingItemInstance.h" #include "NakedDesire/Clothing/ClothingVisualsComponent.h" -#include "NakedDesire/Global/Constants.h" #include "NakedDesire/Global/NakedDesireHUD.h" -#include "NakedDesire/Global/NakedDesireUserSettings.h" #include "NakedDesire/Interaction/InteractionComponent.h" #include "NakedDesire/UI/GameLayoutWidget.h" #include "Perception/AIPerceptionStimuliSourceComponent.h" @@ -32,7 +28,6 @@ ANakedDesireCharacter::ANakedDesireCharacter() ClothingManager = CreateDefaultSubobject("Clothing Manager"); StatsManager = CreateDefaultSubobject("Stats Manager"); - MissionsManager = CreateDefaultSubobject("Missions Manager"); ClothingVisualsComponent = CreateDefaultSubobject(TEXT("Clothing Visuals Component")); diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.h b/Source/NakedDesire/Player/NakedDesireCharacter.h index 88ddf7a7..63a3aabd 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.h +++ b/Source/NakedDesire/Player/NakedDesireCharacter.h @@ -22,7 +22,6 @@ class UClothingManager; class UClothingVisualsComponent; class UCensorshipComponent; class UStatsManager; -class UMissionsManager; class ANPCAIController; class ULocationData; @@ -87,9 +86,6 @@ public: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) UStatsManager* StatsManager; - UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) - UMissionsManager* MissionsManager; - UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) UAIPerceptionStimuliSourceComponent* StimuliSourceComponent; diff --git a/Source/NakedDesire/SaveGame/GlobalSaveGameData.h b/Source/NakedDesire/SaveGame/GlobalSaveGameData.h index 3c6fed4b..93b7a9d3 100644 --- a/Source/NakedDesire/SaveGame/GlobalSaveGameData.h +++ b/Source/NakedDesire/SaveGame/GlobalSaveGameData.h @@ -6,6 +6,7 @@ #include "GameFramework/SaveGame.h" #include "NakedDesire/Global/Constants.h" #include "ItemSaveRecord.h" +#include "NakedDesire/Commissions/CommissionTypes.h" #include "GlobalSaveGameData.generated.h" class UItemInstance; @@ -42,6 +43,10 @@ public: bool RemoveWorldItem(UItemInstance* ItemInstance); TArray GetWorldItems() const { return WorldItems; } + // Commission board state (§13). State-level only; see FCommissionSaveRecord. + TArray GetCommissionRecords() const { return Commissions; } + void SetCommissionRecords(const TArray& InRecords) { Commissions = InRecords; } + UPROPERTY(SaveGame) int32 DaysPassed = 0; @@ -66,4 +71,7 @@ private: UPROPERTY(SaveGame) TArray WorldItems; + + UPROPERTY(SaveGame) + TArray Commissions; }; \ No newline at end of file diff --git a/Source/NakedDesire/Stats/StatsManager.cpp b/Source/NakedDesire/Stats/StatsManager.cpp index 089a183a..6af5e706 100644 --- a/Source/NakedDesire/Stats/StatsManager.cpp +++ b/Source/NakedDesire/Stats/StatsManager.cpp @@ -88,10 +88,33 @@ void UStatsManager::SetObserved(const bool bObserved, AActor* Observer) if (!Observer) return; + bool bChanged = false; if (bObserved) - Observers.AddUnique(Observer); + { + if (!Observers.Contains(Observer)) + { + Observers.Add(Observer); + bChanged = true; + } + } else - Observers.Remove(Observer); + { + bChanged = Observers.Remove(Observer) > 0; + } + + if (bChanged) + OnObserversChanged.Broadcast(); +} + +int32 UStatsManager::GetObserverCount() const +{ + int32 Count = 0; + for (const TWeakObjectPtr& Observer : Observers) + { + if (Observer.IsValid()) + ++Count; + } + return Count; } void UStatsManager::IncreaseEmbarrassment(const float Amount) diff --git a/Source/NakedDesire/Stats/StatsManager.h b/Source/NakedDesire/Stats/StatsManager.h index 4d0a56bc..aba5b205 100644 --- a/Source/NakedDesire/Stats/StatsManager.h +++ b/Source/NakedDesire/Stats/StatsManager.h @@ -9,6 +9,7 @@ class UClothingManager; class ANakedDesireCharacter; DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAttributeUpdateSignature, float, CurrentValue, float, MaxValue); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FObserversChangedSignature); UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) @@ -47,6 +48,14 @@ public: // body parts that observer can actually see (GDD §7.1). 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) void IncreaseEmbarrassment(float Amount); void DecreaseEmbarrassment(float Amount);