From 56cc2fce9890e183dd7a931c54589e5eebe9d6d6 Mon Sep 17 00:00:00 2001 From: koritsa Date: Sun, 31 May 2026 15:33:43 +0300 Subject: [PATCH] Setup player preview for equipment panel --- Config/DefaultEngine.ini | 4 + Config/DefaultGame.ini | 3 + Content/Blueprints/GM_Main.uasset | 4 +- .../Blueprints/Interactables/BP_Bed.uasset | 3 + Content/Blueprints/NPC/BP_NPC_Girl_1.uasset | 4 +- Content/Blueprints/Player/BP_Player.uasset | 4 +- .../Data/Casual/Bra/Casual_Bra_Black.uasset | 2 +- .../Panties/Casual_Panties_Black.uasset | 2 +- Content/Environment/Room/Bed.uasset | 4 +- Content/Maps/L_EquipmentPreview.umap | 3 + Content/Test/Maps/TestLevel.umap | 4 +- Content/UI/HUD/W_HUD.uasset | 4 +- .../UI/Inventory/WBP_EquipmentPanel.uasset | 4 +- PLAN.md | 26 +- README.md | 11 +- Source/NakedDesire/Global/Constants.h | 13 + .../Global/NakedDesireGameMode.cpp | 71 +---- .../NakedDesire/Global/NakedDesireGameMode.h | 36 +-- .../Global/PlayerPreviewCaptureSubsystem.cpp | 14 + .../Global/PlayerPreviewCaptureSubsystem.h | 45 +++ .../Global/SessionLossResolver.cpp | 9 + .../NakedDesire/Global/SessionLossResolver.h | 6 + .../NakedDesire/Global/TimeOfDaySubsystem.cpp | 264 ++++++++++++++++++ .../NakedDesire/Global/TimeOfDaySubsystem.h | 143 ++++++++++ Source/NakedDesire/Interactables/Bed.cpp | 117 ++++++++ Source/NakedDesire/Interactables/Bed.h | 57 ++++ .../Interaction/InteractionComponent.cpp | 12 + .../Interaction/InteractionComponent.h | 4 + Source/NakedDesire/NPC/NPC.cpp | 7 - Source/NakedDesire/NPC/NPC.h | 7 - Source/NakedDesire/NPC/NPCAIController.cpp | 45 ++- Source/NakedDesire/NPC/NPCAIController.h | 11 +- Source/NakedDesire/NPC/NPCSpawner.cpp | 58 ---- Source/NakedDesire/NPC/NPCSpawner.h | 49 ---- Source/NakedDesire/NPC/NPCTargetLocation.cpp | 11 - Source/NakedDesire/NPC/NPCTargetLocation.h | 16 -- .../Player/NakedDesireCharacter.cpp | 2 +- .../NakedDesire/SaveGame/GlobalSaveGameData.h | 13 +- Source/NakedDesire/UI/HUDWidget.cpp | 19 ++ Source/NakedDesire/UI/HUDWidget.h | 12 + .../UI/Inventory/InventoryScreenWidget.cpp | 17 ++ .../UI/Inventory/InventoryScreenWidget.h | 1 + 42 files changed, 823 insertions(+), 318 deletions(-) create mode 100644 Content/Blueprints/Interactables/BP_Bed.uasset create mode 100644 Content/Maps/L_EquipmentPreview.umap create mode 100644 Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.cpp create mode 100644 Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.h create mode 100644 Source/NakedDesire/Global/TimeOfDaySubsystem.cpp create mode 100644 Source/NakedDesire/Global/TimeOfDaySubsystem.h create mode 100644 Source/NakedDesire/Interactables/Bed.cpp create mode 100644 Source/NakedDesire/Interactables/Bed.h delete mode 100644 Source/NakedDesire/NPC/NPCSpawner.cpp delete mode 100644 Source/NakedDesire/NPC/NPCSpawner.h delete mode 100644 Source/NakedDesire/NPC/NPCTargetLocation.cpp delete mode 100644 Source/NakedDesire/NPC/NPCTargetLocation.h diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index ea59a4fb..be05846e 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -135,3 +135,7 @@ DefaultMediaSoundClassName=/Game/Audio/SC_Music.SC_Music AgentRadius=35.000000 AgentMaxSlope=50.000000 +[/Script/GameplayDebugger.GameplayDebuggerConfig] +CategorySlot4=Backslash +CategorySlot5=RightBracket + diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index be1cc79d..2718936c 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -2,6 +2,9 @@ CommonButtonAcceptKeyHandling=TriggerClick bAutoLoadData=True +[/Script/AIModule.AISense_Sight] +bAutoRegisterAllPawnsAsSources=False + [/Script/EngineSettings.GeneralProjectSettings] ProjectID=305A61484AE3739092FF13931B45C2C6 ProjectName=Naked Desire diff --git a/Content/Blueprints/GM_Main.uasset b/Content/Blueprints/GM_Main.uasset index 95b2a25f..678e790e 100644 --- a/Content/Blueprints/GM_Main.uasset +++ b/Content/Blueprints/GM_Main.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62dbef304c692d9bfef277fbebb307a368d0c546166496f291b99a014ce6e2c3 -size 121700 +oid sha256:4045e0228df48af83d8153503379d0925da930bc561d886561b18fb21a74505e +size 54544 diff --git a/Content/Blueprints/Interactables/BP_Bed.uasset b/Content/Blueprints/Interactables/BP_Bed.uasset new file mode 100644 index 00000000..71e2c49b --- /dev/null +++ b/Content/Blueprints/Interactables/BP_Bed.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d284b6693633c77db12cc80fd59b76214471b6b7975e022ecaeefe786bcf8688 +size 46385 diff --git a/Content/Blueprints/NPC/BP_NPC_Girl_1.uasset b/Content/Blueprints/NPC/BP_NPC_Girl_1.uasset index 41c6a6fd..31c6b13d 100644 --- a/Content/Blueprints/NPC/BP_NPC_Girl_1.uasset +++ b/Content/Blueprints/NPC/BP_NPC_Girl_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffa4d240e80161b7e5d8ca6608320fb17e106a93967e2bc7060f63d780f790ea -size 209841 +oid sha256:e5e262e53d1614b6df40c1556d8601c52a6e9afb6727012e09b5aa6c9799f770 +size 56906 diff --git a/Content/Blueprints/Player/BP_Player.uasset b/Content/Blueprints/Player/BP_Player.uasset index 5a518fea..02a75530 100644 --- a/Content/Blueprints/Player/BP_Player.uasset +++ b/Content/Blueprints/Player/BP_Player.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:941780801c1e8e5e365f4a4d8020aa2032d23afa739e7c99247e4aafd5b8d913 -size 77347 +oid sha256:4d8abf574ec762faf4e535a35a5caf9baa91c7f6cbe4917700d64d76962739cf +size 73603 diff --git a/Content/Characters/Yumi/Clothing/Data/Casual/Bra/Casual_Bra_Black.uasset b/Content/Characters/Yumi/Clothing/Data/Casual/Bra/Casual_Bra_Black.uasset index 6b1de831..7982d53e 100644 --- a/Content/Characters/Yumi/Clothing/Data/Casual/Bra/Casual_Bra_Black.uasset +++ b/Content/Characters/Yumi/Clothing/Data/Casual/Bra/Casual_Bra_Black.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e68ff482214b027f06f6136e34facf0cd8214cdef15e9ea9ba3b24603c69ca07 +oid sha256:02fdd8972b7233f4971d1aec4eeac909b964718aecd74d7544d999bc4dd1e87c size 3685 diff --git a/Content/Characters/Yumi/Clothing/Data/Casual/Panties/Casual_Panties_Black.uasset b/Content/Characters/Yumi/Clothing/Data/Casual/Panties/Casual_Panties_Black.uasset index d41065d8..517f450e 100644 --- a/Content/Characters/Yumi/Clothing/Data/Casual/Panties/Casual_Panties_Black.uasset +++ b/Content/Characters/Yumi/Clothing/Data/Casual/Panties/Casual_Panties_Black.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70629453c17f676893c580df7d5d036b92d0b6d49b76f7ae4f82a8fddb65776a +oid sha256:90118295413219edeeeebaca1c2788474dde22318e3d6c371a15027e5d4c3f82 size 3908 diff --git a/Content/Environment/Room/Bed.uasset b/Content/Environment/Room/Bed.uasset index fa2ef087..47f99536 100644 --- a/Content/Environment/Room/Bed.uasset +++ b/Content/Environment/Room/Bed.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac5c1cf6b0318d06cbc2fec9ed47afc1be5b7d60b7a1024e5de6220359540cbe -size 266052 +oid sha256:6ba454ab38000e9b789d974bf7da641162e49feb23b59931ae790b80fe447fb9 +size 268605 diff --git a/Content/Maps/L_EquipmentPreview.umap b/Content/Maps/L_EquipmentPreview.umap new file mode 100644 index 00000000..996ed48e --- /dev/null +++ b/Content/Maps/L_EquipmentPreview.umap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:061365fa98b26a8962428a5b7f098bd898c092a7eb3d3db16c08a9f47aa490de +size 75500 diff --git a/Content/Test/Maps/TestLevel.umap b/Content/Test/Maps/TestLevel.umap index c6fe5265..ae751a35 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:7304281dedee8ad4ff9a9d4902f4faf9a4bd0e36c714e4ff45415de9ba6b63cb -size 61521 +oid sha256:aa70ea4662a950a582906488e3e691a8c11c8e0e8cac7f7f5d5cd001fdab0de2 +size 149849 diff --git a/Content/UI/HUD/W_HUD.uasset b/Content/UI/HUD/W_HUD.uasset index 11cea7dc..78536e98 100644 --- a/Content/UI/HUD/W_HUD.uasset +++ b/Content/UI/HUD/W_HUD.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df406d25fa438cbbbc568a27d340b7d476829bab4744539d922c796d020b0e9e -size 24359 +oid sha256:5c2d77624098634c2d86c1781664b90c31a8634a3ee198eddff6054b5f05d9c2 +size 27021 diff --git a/Content/UI/Inventory/WBP_EquipmentPanel.uasset b/Content/UI/Inventory/WBP_EquipmentPanel.uasset index 94d1b159..d679c263 100644 --- a/Content/UI/Inventory/WBP_EquipmentPanel.uasset +++ b/Content/UI/Inventory/WBP_EquipmentPanel.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec0e9ee35c8aaca94ab3b99f3e6f4e603ffac86af66f1cf58a690c9ca4c5da9c -size 47467 +oid sha256:5575ecd7954a8f9eea4d4b61bcb227e1c81124757c4fdf14a1e96ef2e0ad21e7 +size 40159 diff --git a/PLAN.md b/PLAN.md index d8a56a2b..ae59dfba 100644 --- a/PLAN.md +++ b/PLAN.md @@ -106,22 +106,22 @@ State of the C++ module as of the latest pass. File references use `Source/Naked - **Coverage (§6.3.2)** — `ClothingManager::GetEffectiveCoverage(EBodyPart)` implements the locked `max(coverage)` across covering garments (VS-1). `ClothingManager::IsBodyPartExposed` remains binary and is still used by the censorship path; the observation/embarrassment path no longer relies on it. Active expose state (§6.3.6) is not yet folded into the coverage result (VS-3). `UClothingItem::IsUnderwear` is dead spec and should be removed during the Phase 1 cleanup. - **Body-part enums — duplicated** — both `Player/PrivateBodyPartType.h` (`EPrivateBodyPartType { FrontBottom, BackBottom, FrontTop }`) and `Clothing/BodyPart.h` (`EBodyPart { Boobs, Ass, Genitals }`) exist. `UClothingItem::CoveredBodyParts` uses the **old** enum; `UClothingItem::CanExpose` uses the **new** one. Half-migrated. - **Mission system** — composable goals work but lacks the typed objective steps from §13.4 (`BeFullyNaked`, `BeFullyNakedNearNPCs`, `WalkNakedDistance`, `MoveDistanceFromClothing`, `BeObservedByNPCType`, `TakePhotoAtLocation`, `DeliverItemTo`). Missions still hand-authored in `MissionsConfig::DailyMissions` keyed by day index — no procedural generation, no Accept lifecycle (§13.1 / §13.2), no path-filtering on the generator (§13.4). -- **Day / night** — `NPCSpawner.cpp:38-41` reads `GetCurrentTime().Hours` and gates spawn cap, but does not affect embarrassment gain, NPC type weighting, or police spawning (see §1.3). +- **Day / night** — `UTimeOfDaySubsystem` now exposes `GetPhase()` / `IsDay()` and the `OnPhaseChanged` delegate (08:00 / 20:00 boundaries, §10.1). Not yet consumed: phase does not affect embarrassment gain, NPC type weighting, or police spawning. (`NPCSpawner` and its old `09–21` window are deleted from the tree.) ### 1.3 Missing -- **Session loss — remaining (GDD §4.4)** — the transactional resolver exists (`USessionLossResolver`, see §1.1), but several outcomes depend on systems not yet built: the energy-zero trudge-home cutscene, the holding-cell cutscene, the fade/teleport-to-apartment, and the time skips (one sleep cycle / fast-forward to next morning) are all delegated to BP via `OnSessionLossResolved` and not yet authored. Bag-placed-in-world loss is a TODO (bags absent, §6.4). `ClearWanted()` is a stub (no Wanted attribute until Phase 4/6, §7.7). Apartment-interior detection for loose-item loss is approximated (all `AItemPickup`s treated as "outside"). +- **Session loss — remaining (GDD §4.4)** — the transactional resolver exists (`USessionLossResolver`, see §1.1), but several outcomes depend on systems not yet built: the energy-zero trudge-home cutscene, the holding-cell cutscene, and the fade/teleport-to-apartment are delegated to BP via `OnSessionLossResolved` and not yet authored. The time skips now have a C++ entry point — `UTimeOfDaySubsystem::Sleep()` (sleep cycle) and `SkipToNextMorning()` (holding cell) — so the BP cutscenes call those rather than reimplementing the skip; this also runs the skip before the resolver's autosave, closing the prior ordering gap. Bag-placed-in-world loss is a TODO (bags absent, §6.4). `ClearWanted()` is a stub (no Wanted attribute until Phase 4/6, §7.7). Apartment-interior detection for loose-item loss is approximated (all `AItemPickup`s treated as "outside"). - **Three progression paths runtime (§5)** — enum exists; no XP pool, no per-path level derived from investment, no path-gated unlocks at runtime, no level-up flow. **XP is a single shared pool, not per-path** (GDD §5, §7.10). - **Phone (§9)** — entire system absent: camera, gallery, livestream, bank, Feetex, maps, health tracker. Includes the new sub-systems: - **Battery (§9.8)** — passive base + per-app multiplier drain; apartment charger; portable powerbank consumable (Convenience Store); hard shutdown at 0%; mid-livestream cutoff with earnings-to-date deposited; sleep always charges to 100%. - **Livestream tip requests (§9.1.1)** — viewer-driven action requests with phone popup (Accept / Decline + countdown); fail = viewer count drops, no rep hit. - **Livestream follower trickle (§9.1.1)** — `streamQualityScore` ticks through `FollowerGainCalculator` per tick. - - **Bank app income breakdown (§9.4)** — line items by source incl. the weekly follower auto-deposit at week boundary. + - **Bank app income breakdown (§9.4)** — line items by source incl. the daily follower auto-deposit at the day-roll (§20 #25). - **`PhoneSubsystem` (§17.1)** — tickable subsystem owning phone state (battery %, active app, livestream session lifecycle, charger interaction). Does not yet exist. - **Forum (§13)** — no `UCommissionTemplate`, no procedural generation, no weekly missions distinct from daily, no profile (incl. weekly follower-income summary), no posting photos. Forum scope is locked minimal (board + own profile only, no threads / no other-users feed) — that's a non-build, but worth recording. - **Photo & livestream** — absent. - **NPC types (§10.2)** — only one generic `ANPC` class. No Walker / Stalker / Blogger / Snitch / Harasser, no Police, no Wanted-poster mechanic. -- **Calendar, rent, sleep (§2.4, §15.2)** — `DaysPassed` increments in `NakedDesireGameMode::OnHourChanged(4)`, but no week, no rent, no eviction, no sleep action, no apartment bed interaction. +- **Calendar, rent, sleep (§2.4, §15.2)** — **core landed (Phase 5):** `UTimeOfDaySubsystem` (`Global/TimeOfDaySubsystem`) owns the clock, rolls the calendar at 04:00, fires `OnHourChanged`/`OnDayChanged`/`OnPhaseChanged`, charges `WEEKLY_RENT` every 7th roll with immediate eviction via `OnCampaignEnded(Evicted)`, and exposes `Sleep()`/`SkipToNextMorning()`/`SkipTime()`. Mission refresh moved to `GameMode::HandleDayChanged`. The apartment **bed interactable** (`Interactables/Bed`) is in — interacting calls `USessionLossResolver::ResolveSleepLoss()` (outside-clothing loss) then `UTimeOfDaySubsystem::Sleep()`, with a BP `PlaySleepTransition` fade hook. **Still to do:** rewire the UltraDynamicSky actor to follow `SetCurrentTime` (and stop the old BP clock from advancing independently — otherwise the clock double-advances); the §4.4 cutscenes calling into the new skip API; the ending/eviction screen reacting to `OnCampaignEnded`; daily follower deposit is stubbed (no follower count until Phase 8); phone charge-to-100% on sleep stubbed (Phase 9). `WEEKLY_RENT` is a §21 tuning placeholder. - **Item-world AActor (§6.1)** — no `AItemActor` / `AClothingPickup` base. - **Bag inventory (§6.4)** — absent. - **Theft (§6.3.4)** — new chance-based model (per-tick `P_theft` after grace period) not implemented. No theft timer / probability code at all. @@ -302,13 +302,15 @@ Phase estimates are rough and assume one engineer. Adjust as we go. ### Phase 5 — Time + calendar + rent + sleep (3–4 days) -- `UTimeOfDaySubsystem` replacing the BP-implementable time on `NakedDesireGameMode`. 90-day calendar, week boundary, day phase `08:00–20:00` (fix the `09–21` mismatch in `NPCSpawner.cpp:40`). -- Sleep action on apartment bed: triggers the same path as energy-zero (§4.4) for the "items left outside" cleanup, restores energy (clamped to current effective max), autosaves, advances calendar. **Charges the equipped phone to 100%** (§9.8). **Does NOT reset hunger** (§7.3) — only eating clears effective-max decay. -- Weekly rent transaction at week boundary; eviction if money insufficient (game over (run), §3.3). -- **Weekly follower-income auto-deposit** to the bank at week boundary (§7.9, §9.4). -- Endless-mode flag on `UGlobalSaveGameData`; rent-eviction branch checks it. +- `UTimeOfDaySubsystem` (`UTickableWorldSubsystem`, consistent with `SessionManagerSubsystem` / `SessionLossResolver`) **owns the authoritative clock in C++** and pushes time to the UltraDynamicSky actor each tick (decision 2026-05-30: invert the old BP→C++ flow; the UDS actor's time input is rewired to follow `SetCurrentTime`). Replaces the BP-implementable time on `NakedDesireGameMode`. 90-day calendar, week boundary, day phase `08:00–20:00`. (The old `NPCSpawner.cpp:40` `09–21` window is moot — `NPCSpawner` is deleted in the working tree.) +- Clock state: `CurrentDay` (= `DaysPassed`) + `MinuteOfDay` (replaces the loose `HourOfDay` float). Rate constant `INGAME_MINUTES_PER_REAL_SECOND = 16` (1440 min / 90 real-min cycle). Delegates: keep `OnHourChanged(int32)` (sky + BP gates), add `OnDayChanged(int32)` and `OnPhaseChanged(EDayPhase)`. **Calendar rolls at the fixed 04:00 boundary** (decision 2026-05-30; matches the old `Hour==4` code); daily-mission refresh moves out of `GameMode::OnHourChanged(Hour==4)` into `OnDayChanged`. +- Sleep action on apartment bed: triggers the same path as energy-zero (§4.4) for the "items left outside" cleanup, restores energy (clamped to current effective max), autosaves, advances calendar via `SkipTime(8h)`. **Charges the equipped phone to 100%** (§9.8). **Does NOT reset hunger** (§7.3) — only eating clears effective-max decay. The §4.4 cutscenes call `Sleep()` / `SkipToNextMorning()`, which also closes the resolver's autosave-ordering gap (time skip now runs in C++ before the autosave). +- Weekly rent transaction every 7th day-roll; **immediate eviction → game over (run)** if money insufficient (§3.3, §22 #8) — no grace period (decision 2026-05-30). +- **Daily follower-income auto-deposit** to the bank at each day-roll (§7.9, §9.4; reconciled to daily — README §20 #25). ⚠️ Depends on a follower-count attribute that doesn't exist until Phase 8 — wire the `OnDayChanged` hook + rate constant now; payout reads 0 until followers land. +- Endless-mode flag (`bEndlessMode`) on `UGlobalSaveGameData`; the rent-charge branch is skipped entirely when set. +- Eviction needs an end-campaign entry point — add an `OnCampaignEnded(EEndReason)` delegate rather than overloading the legacy `EndGameEmbarrassed` BIE (consistent with the session-system refactor's direction). -**Exit criteria:** play through 7 in-game days, get charged rent at the week boundary, eviction triggers on insufficient funds, endless-mode disables eviction, weekly follower income lands in the bank. +**Exit criteria:** play through 7 in-game days, get charged rent at the week boundary, eviction triggers on insufficient funds, endless-mode disables eviction, daily follower income lands in the bank, and the day/phase/calendar survives a save → quit → reload. ### Phase 6 — NPC types + recognition pipeline (1–2 weeks) @@ -419,8 +421,8 @@ Use this section for in-flight decisions, blockers, and open questions that emer - **XP is a single shared pool** (GDD §5, §7.10). Do not introduce per-path XP counters in Phase 9 or earlier — a path's level is derived from how much the player has invested into that path's attribute pool. - **Food buff hookpoints belong in Phase 4** (attribute multiplier interface), even though the food items themselves ship in Phase 10. Leave the seams; don't retrofit later. - **Hunger hookpoint also belongs in Phase 4.** Add `effectiveMaxEnergy` + decay tick when adding Lust / Pulse / etc., so Phase 10's food items only need to call an existing reset method. -- **README contradiction — follower income cadence.** §7.9 line 433 says "Followers generate money each day"; §9.4 and §13.3 say weekly auto-deposit at week boundary. Don't pick one in code until the GDD is reconciled. Phase 5 implementation depends on resolution. +- **~~README contradiction — follower income cadence.~~ RESOLVED (2026-05-30, §20 #25).** Reconciled in favor of §7.9's **daily** model: follower income accrues and auto-deposits daily at the 04:00 day-roll, distinct from the weekly rent charge. README §9.4 / §13.3 / §15.1 and §20 #25 are updated. Phase 5 wires the payout on `OnDayChanged` (blocked on the Phase 8 follower count — pays 0 until then). - **README contradiction — hiding spots vs sleep loss.** §6.3.4's new "Hiding spots" bullet says items have a *chance* of theft after sleep, but §4.4 says sleep = guaranteed loss of anything outside. Phase 10 theft work must wait until the GDD resolves whether hiding spots are an explicit exception to the sleep rule or only affect in-session theft chance. -- **~~README contradiction — masturbation gating.~~ RESOLVED (2026-05-30, §23 #25).** Home masturbation is always available to every player; *in-session* (public) masturbation is the Slut-path unlock. README §5.1 / §7.2 / §6.7 / §13.4 / §14.1 and §20 #25 are updated. Phase 4 / VS-2 implementation: gate only the in-session quick-action entry on Slut investment. +- **~~README contradiction — masturbation gating.~~ RESOLVED (2026-05-30, §20 #26).** Home masturbation is always available to every player; *in-session* (public) masturbation is the Slut-path unlock. README §5.1 / §7.2 / §6.7 / §13.4 / §14.1 and §20 #26 are updated. Phase 4 / VS-2 implementation: gate only the in-session quick-action entry on Slut investment. - **Stale §5.3 Slave path text** in the README still says "(cuffs require NPC help to remove)" — contradicts the Key + minigame removal flow. Phase 6 / Phase 10 work should not implement an NPC removal path; if encountered, treat it as stale documentation. - _empty beyond this point_ diff --git a/README.md b/README.md index b56e3213..2341a0cf 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,7 @@ Together these mean a player who never eats slowly loses their available energy ### 7.9 Followers & Money - **Followers** — running total of forum followers. Used in subscriber-style follower-gain math when posting photos. -- Followers generate money each day. +- Followers generate passive income, **auto-deposited to the bank daily** at the day-roll (§9.4). The cadence is daily — deliberately distinct from the weekly rent charge (§15.2), so follower income smooths the week rather than landing on the rent beat. - More followers means more money for completing commissions. - **Money (Yen)** — earned from commissions, livestream donations, selling worn underwear. Spent on rent, clothing, food, gym, beauty salon. @@ -486,7 +486,7 @@ The phone is the primary access point for the forum. See §13. ### 9.4 Bank app - Track balance, income, spending. Pay rent (or auto-pay). -- Income line items break down by source: commissions, livestream donations, in-stream tip-request completions, underwear sales (§15.1), and the **weekly follower-income auto-deposit** (§7.9) that lands at each week boundary. +- Income line items break down by source: commissions, livestream donations, in-stream tip-request completions, underwear sales (§15.1), and the **daily follower-income auto-deposit** (§7.9) that lands at each day-roll. ### 9.5 Feetex (deliveries) - Track pending online orders (clothing, food). Items arrive at the apartment door one in-game day after purchase. @@ -703,7 +703,7 @@ The forum is accessed via phone or PC. It is both diegetic and the primary missi - Post photos → follower gain (calculated from exposed body parts + visible coverage in the photo via the `FollowerGainCalculator`, §13.5). - Livestream is initiated from the phone at any time (see §9.1.1), not from the forum profile. The profile displays livestream history and lifetime earnings. - Level up attributes — paid for from the shared XP pool (§5, §7.10). The profile is the UI surface for choosing which path's attribute to upgrade. -- Displays weekly follower-income summary: current follower count, next weekly payout estimate, lifetime earnings from followers (the underlying auto-deposit lives in the bank app, §9.4). +- Displays follower-income summary: current follower count, next daily payout estimate, lifetime earnings from followers (the underlying auto-deposit lives in the bank app, §9.4). > **Forum scope:** The forum surface is intentionally minimal — the **commission board** (§13.1 / §13.2) and the **player's own profile**. There are no other users to browse, no threads, no popular-posts feed. The forum exists to drive the gameplay loop, not to simulate a social network. @@ -824,7 +824,7 @@ A dedicated radial wheel for selecting a facial expression / pose. Available at - **Selling worn underwear** — two delivery methods: - **Feetex shipping** — drop the item in a Feetex shipping box at the post office or convenience store. Payment arrives 1 in-game day later (mirrors Feetex's existing 1-day delivery delay). - **Drop-off** — travel to a specified location (varies per order) and leave the underwear there. Immediate payment, but the location may be in a high-risk area depending on the order. Tactical trade-off: convenience vs. drop-off-style commission tension. -- Photo posts (indirect) and accumulated followers — drive the weekly **passive follower income** (§7.9). Higher follower count → larger weekly payout and larger commission payouts. +- Photo posts (indirect) and accumulated followers — drive the daily **passive follower income** (§7.9). Higher follower count → larger daily payout and larger commission payouts. - **Casino winnings (§10.4.2)** — high-variance, negative-EV-on-average. Not a reliable income source; treat as gambling, not as a salary. ### 15.2 Costs @@ -974,7 +974,8 @@ Decisions previously open, now fixed: 22. **Restraint-removal unlock minigame.** DBD-style skill-check minigame (rotating pointer + target zone). Successful hits **speed up** the removal. Missed checks have **no penalty** — they just don't grant the speed bonus. The restraint always comes off when the baseline timer expires. No noise alerts, no key loss, no fail state. See §10.4.1. 23. **Commission rewards land on completion, not on return home.** Money wires to the bank instantly, XP credits to the shared pool, followers update on the profile. No "collect rewards" step at the apartment. See §13.1 / §13.2 / §4.3 / §3.1. 24. **Hunger via max-energy decay.** Effective max energy decays over time (a hunger rate); eating any food restores effective max to base max as a built-in universal effect (no per-food authoring). Sleep restores current energy but does NOT reset hunger — only eating does. Floors at a TBD fraction of base max so the player can't be starved into a forced game-over. See §7.3 / §6.7. -25. **Masturbation gating — home vs. public.** Home masturbation (in the apartment) is **always available to every player, regardless of path**. Masturbating *during a session* (outside the apartment) is a **Slut-path unlock** (§5.1). The §14.1 quick-action shows the masturbate entry unconditionally at home; in-session it is hidden until the player has Slut-path investment. Non-Slut players therefore cannot reset lust mid-session and rely on lust-decrease food (§6.7) or returning home. Commission `PerformAction(masturbate)` objectives are public and thus Slut-gated by the generator (§13.4). Resolves the prior §5.1 ↔ §7.2 contradiction. See §7.2 / §5.1 / §14.1. +25. **Follower income cadence — daily, not weekly.** Passive follower income accrues and auto-deposits to the bank **every in-game day** at the 04:00 day-roll, not in a weekly lump. Resolves the prior §7.9 ↔ §9.4 / §13.3 contradiction in favor of §7.9's daily model. Rent remains a separate **weekly** charge (§15.2); the two cadences are intentionally offset. See §7.9 / §9.4 / §13.3 / §15.1. +26. **Masturbation gating — home vs. public.** Home masturbation (in the apartment) is **always available to every player, regardless of path**. Masturbating *during a session* (outside the apartment) is a **Slut-path unlock** (§5.1). The §14.1 quick-action shows the masturbate entry unconditionally at home; in-session it is hidden until the player has Slut-path investment. Non-Slut players therefore cannot reset lust mid-session and rely on lust-decrease food (§6.7) or returning home. Commission `PerformAction(masturbate)` objectives are public and thus Slut-gated by the generator (§13.4). Resolves the prior §5.1 ↔ §7.2 contradiction. See §7.2 / §5.1 / §14.1. ## 21. Open Design Questions diff --git a/Source/NakedDesire/Global/Constants.h b/Source/NakedDesire/Global/Constants.h index 965ec1b5..80a35291 100644 --- a/Source/NakedDesire/Global/Constants.h +++ b/Source/NakedDesire/Global/Constants.h @@ -7,3 +7,16 @@ inline const FString DefaultSaveSlotName = TEXT("Slot1"); #define IS_DEMO false #define STARTING_MONEY 1000 +// --- Time of day / calendar (GDD §2.4, §10.1; tuning §21) --- +// 1440 in-game minutes elapse over the ~90 real-minute day/night cycle. +inline constexpr float INGAME_MINUTES_PER_REAL_SECOND = 16.0f; +inline constexpr int32 MINUTES_PER_HOUR = 60; +inline constexpr int32 MINUTES_PER_DAY = 1440; +inline constexpr float DAY_START_HOUR = 8.0f; // 08:00 — day phase begins (§10.1) +inline constexpr float NIGHT_START_HOUR = 20.0f; // 20:00 — night phase begins (§10.1) +inline constexpr int32 DAY_ROLL_HOUR = 4; // 04:00 — calendar day increments (§20 #25) +inline constexpr float SLEEP_DURATION_HOURS = 8.0f; // §2.4 sleep fast-forwards 8 hours +inline constexpr int32 CAMPAIGN_LENGTH_DAYS = 90; // §3.3 survive 90 days +inline constexpr int32 WEEK_LENGTH_DAYS = 7; // §2.4 rent due each week +inline constexpr float WEEKLY_RENT = 20000.0f; // §15.3 early-tier placeholder (§21 tuning) + diff --git a/Source/NakedDesire/Global/NakedDesireGameMode.cpp b/Source/NakedDesire/Global/NakedDesireGameMode.cpp index 31f0885d..b3d15dae 100644 --- a/Source/NakedDesire/Global/NakedDesireGameMode.cpp +++ b/Source/NakedDesire/Global/NakedDesireGameMode.cpp @@ -2,14 +2,9 @@ #include "NakedDesireGameMode.h" #include "Kismet/GameplayStatics.h" -#include "NakedDesire/Clothing/ClothingItemDefinition.h" #include "NakedDesire/Clothing/ClothingItemInstance.h" #include "NakedDesire/Interactables/ItemPickup.h" #include "UObject/ConstructorHelpers.h" -#include "NakedDesire/Interactables/Wardrobe.h" -#include "NakedDesire/MissionBuilder/MissionsConfig.h" -#include "NakedDesire/MissionBuilder/MissionsManager.h" -#include "NakedDesire/Player/NakedDesireCharacter.h" #include "NakedDesire/SaveGame/GlobalSaveGameData.h" #include "NakedDesire/SaveGame/ItemSaveRecord.h" #include "NakedDesire/SaveGame/SaveSubsystem.h" @@ -19,59 +14,17 @@ void ANakedDesireGameMode::RestartGame() UGameplayStatics::OpenLevel(this, "City"); } -AWardrobe* ANakedDesireGameMode::GetWardrobe() const -{ - return Wardrobe; -} - -void ANakedDesireGameMode::BuyItem(UClothingItemInstance* ClothingItemInstance) -{ - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - UGlobalSaveGameData* SaveGame = SaveSubsystem->GetCurrentSave(); - if (!SaveGame) - { - UE_LOG(LogTemp, Error, TEXT("ANakedDesireGameMode::BuyItem Couldn't load save game")); - return; - } - - if (SaveGame->Money < ClothingItemInstance->GetClothingItemDefinition()->BasePrice) - return; - - SaveGame->Money -= ClothingItemInstance->GetClothingItemDefinition()->BasePrice; - Wardrobe->AddItem(ClothingItemInstance); -} - -void ANakedDesireGameMode::OnHourChanged(int32 Hour) -{ - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - UGlobalSaveGameData* SaveGame = SaveSubsystem->GetCurrentSave(); - if (!SaveGame) - { - UE_LOG(LogTemp, Error, TEXT("ANakedDesireGameMode::BuyItem Couldn't load save game")); - return; - } - - if (Hour == 4) - { - SaveGame->DaysPassed++; - RefreshDailyMissions(); - } -} - void ANakedDesireGameMode::BeginPlay() { Super::BeginPlay(); - if (AActor* FoundActor = UGameplayStatics::GetActorOfClass(GetWorld(), AWardrobe::StaticClass())) - { - if (AWardrobe* WardrobeActor = Cast(FoundActor)) - { - Wardrobe = WardrobeActor; - } - } - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - for (const FItemSaveRecord& Item : SaveSubsystem->GetCurrentSave()->GetWorldItems()) + SpawnSavedWorldItems(SaveSubsystem->GetCurrentSave()); +} + +void ANakedDesireGameMode::SpawnSavedWorldItems(UGlobalSaveGameData* SaveGameData) +{ + for (const FItemSaveRecord& Item : SaveGameData->GetWorldItems()) { UClothingItemInstance* NewItemInstance = Cast(UItemInstance::CreateFromRecord(this, Item)); if (!NewItemInstance) @@ -81,15 +34,3 @@ void ANakedDesireGameMode::BeginPlay() NewItemPickup->SetItem(NewItemInstance); } } - -void ANakedDesireGameMode::RefreshDailyMissions() -{ - const ANakedDesireCharacter* Player = Cast(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); - if (!Player) - return; - - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - - int ClampedIndex = FMath::Clamp(SaveSubsystem->GetCurrentSave()->DaysPassed , 0, MissionsConfig->DailyMissions.Num() - 1); - Player->MissionsManager->RefreshDailyMissions(MissionsConfig->DailyMissions[ClampedIndex].Missions); -} diff --git a/Source/NakedDesire/Global/NakedDesireGameMode.h b/Source/NakedDesire/Global/NakedDesireGameMode.h index fdcb52fd..cf228f45 100644 --- a/Source/NakedDesire/Global/NakedDesireGameMode.h +++ b/Source/NakedDesire/Global/NakedDesireGameMode.h @@ -6,53 +6,27 @@ #include "GameFramework/GameModeBase.h" #include "NakedDesireGameMode.generated.h" +class UGlobalSaveGameData; class AItemPickup; -class UClothingItemInstance; -class UMissionsConfig; -class AWardrobe; -UCLASS(minimalapi) +UCLASS(MinimalAPI) class ANakedDesireGameMode : public AGameModeBase { GENERATED_BODY() - - UPROPERTY() - AWardrobe* Wardrobe = nullptr; - - UPROPERTY(EditDefaultsOnly) - UMissionsConfig* MissionsConfig; public: int NoticeCount = 0; void RestartGame(); - - UFUNCTION(BlueprintPure, BlueprintImplementableEvent) - FTimecode GetCurrentTime() const; - - UFUNCTION(BlueprintImplementableEvent, BlueprintCallable) - void SetCurrentTime(FTimecode TimeCode); - - UFUNCTION(BlueprintPure) - AWardrobe* GetWardrobe() const; - - UFUNCTION(BlueprintImplementableEvent) - void EndGameEmbarrassed(); - - UFUNCTION(BlueprintCallable) - void BuyItem(UClothingItemInstance* ClothingItemInstance); - - UFUNCTION(BlueprintCallable) - void OnHourChanged(int32 Hour); protected: virtual void BeginPlay() override; - + private: - void RefreshDailyMissions(); - UPROPERTY(EditDefaultsOnly, Category = "Items") TSubclassOf ItemPickupClass; + + void SpawnSavedWorldItems(UGlobalSaveGameData* SaveGameData); }; diff --git a/Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.cpp b/Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.cpp new file mode 100644 index 00000000..67a5f03f --- /dev/null +++ b/Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.cpp @@ -0,0 +1,14 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#include "PlayerPreviewCaptureSubsystem.h" + +void UPlayerPreviewCaptureSubsystem::SetPreviewActive(bool bActive) +{ + if (bPreviewActive == bActive) + { + return; + } + + bPreviewActive = bActive; + OnPreviewCaptureActiveChanged.Broadcast(bPreviewActive); +} \ No newline at end of file diff --git a/Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.h b/Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.h new file mode 100644 index 00000000..f74e37eb --- /dev/null +++ b/Source/NakedDesire/Global/PlayerPreviewCaptureSubsystem.h @@ -0,0 +1,45 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "PlayerPreviewCaptureSubsystem.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPreviewCaptureActiveChanged, bool, bActive); + +/** + * Message bus between the equipment/inventory UI (C++) and the player-preview + * sublevel that renders the impostor into a SceneCapture2D for the equipment panel. + * + * The capture is expensive, so it must run only while the panel is open. The preview + * level's Level Blueprint owns the actual 30fps capture loop; it binds to + * OnPreviewCaptureActiveChanged to start/stop that loop. The UI flips the state via + * SetPreviewActive when the inventory screen activates / deactivates. + * + * A world subsystem is shared across the persistent level and its streamed sublevels, + * so the preview level and the main UI talk through the same instance without either + * needing a hard reference to the other. + */ +UCLASS() +class NAKEDDESIRE_API UPlayerPreviewCaptureSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + // Called by the inventory/equipment UI on open (true) and close (false). + UFUNCTION(BlueprintCallable, Category = "Preview") + void SetPreviewActive(bool bActive); + + UFUNCTION(BlueprintPure, Category = "Preview") + bool IsPreviewActive() const { return bPreviewActive; } + + // Bound by the preview level's Level Blueprint to start/stop its 30fps capture loop. + // Read IsPreviewActive() on BeginPlay to sync initial state in case the panel was + // somehow already open when the sublevel streamed in. + UPROPERTY(BlueprintAssignable, Category = "Preview") + FOnPreviewCaptureActiveChanged OnPreviewCaptureActiveChanged; + +private: + bool bPreviewActive = false; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Global/SessionLossResolver.cpp b/Source/NakedDesire/Global/SessionLossResolver.cpp index 68282ff9..23118326 100644 --- a/Source/NakedDesire/Global/SessionLossResolver.cpp +++ b/Source/NakedDesire/Global/SessionLossResolver.cpp @@ -76,6 +76,15 @@ void USessionLossResolver::ResolveLoss(ESessionLossCause Cause) OnSessionLossResolved.Broadcast(Cause, bWentToHoldingCell); } +void USessionLossResolver::ResolveSleepLoss() +{ + // §4.4: sleeping is the deterministic side of the theft model — everything left + // outside the apartment is lost, regardless of the in-session grace / roll state. + // No autosave here: the sleep flow persists once via UTimeOfDaySubsystem::Sleep(), + // which runs immediately after this. Callers must ensure a save follows. + LoseAllWorldClothing(); +} + void USessionLossResolver::LoseAllWorldClothing() { UGlobalSaveGameData* Save = GetSave(); diff --git a/Source/NakedDesire/Global/SessionLossResolver.h b/Source/NakedDesire/Global/SessionLossResolver.h index c89026a1..c57ec7ba 100644 --- a/Source/NakedDesire/Global/SessionLossResolver.h +++ b/Source/NakedDesire/Global/SessionLossResolver.h @@ -39,6 +39,12 @@ public: UFUNCTION(BlueprintCallable, Category = "Session") void ResolveLoss(ESessionLossCause Cause); + // Voluntary sleep at the apartment bed (§4.4 step 2). Not a session-loss cause — the + // session already ended on entering the apartment — but any clothing left outside is + // still guaranteed lost. Lives here so ALL "what gets lost" logic stays in one place. + UFUNCTION(BlueprintCallable, Category = "Session") + void ResolveSleepLoss(); + UPROPERTY(BlueprintAssignable, Category = "Session") FOnSessionLossResolvedSignature OnSessionLossResolved; diff --git a/Source/NakedDesire/Global/TimeOfDaySubsystem.cpp b/Source/NakedDesire/Global/TimeOfDaySubsystem.cpp new file mode 100644 index 00000000..e14cb9be --- /dev/null +++ b/Source/NakedDesire/Global/TimeOfDaySubsystem.cpp @@ -0,0 +1,264 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "TimeOfDaySubsystem.h" +#include "Constants.h" +#include "Kismet/GameplayStatics.h" +#include "Misc/Timecode.h" +#include "NakedDesire/Player/NakedDesireCharacter.h" +#include "NakedDesire/SaveGame/GlobalSaveGameData.h" +#include "NakedDesire/SaveGame/SaveSubsystem.h" +#include "NakedDesire/Stats/StatsManager.h" + +void UTimeOfDaySubsystem::OnWorldBeginPlay(UWorld& InWorld) +{ + Super::OnWorldBeginPlay(InWorld); + + if (const UGlobalSaveGameData* Save = GetSave()) + { + CurrentPhase = ComputePhase(Save->MinuteOfDay); + } + + bBegunPlay = true; + PushTimeToSky(); // sync the sky to the loaded time immediately +} + +void UTimeOfDaySubsystem::Tick(float DeltaTime) +{ + if (!bBegunPlay || IsPaused()) + return; + + AdvanceClock(static_cast(DeltaTime) * INGAME_MINUTES_PER_REAL_SECOND); +} + +TStatId UTimeOfDaySubsystem::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UTimeOfDaySubsystem, STATGROUP_Tickables); +} + +int32 UTimeOfDaySubsystem::GetDay() const +{ + const UGlobalSaveGameData* Save = GetSave(); + return Save ? Save->DaysPassed : 0; +} + +float UTimeOfDaySubsystem::GetMinuteOfDay() const +{ + const UGlobalSaveGameData* Save = GetSave(); + return Save ? Save->MinuteOfDay : 0.0f; +} + +int32 UTimeOfDaySubsystem::GetHour() const +{ + return FMath::FloorToInt(GetMinuteOfDay() / MINUTES_PER_HOUR); +} + +int32 UTimeOfDaySubsystem::GetMinute() const +{ + return FMath::FloorToInt(GetMinuteOfDay()) % MINUTES_PER_HOUR; +} + +EDayPhase UTimeOfDaySubsystem::GetPhase() const +{ + return ComputePhase(GetMinuteOfDay()); +} + +void UTimeOfDaySubsystem::SkipTime(float Minutes) +{ + if (Minutes > 0.0f) + { + AdvanceClock(static_cast(Minutes)); + } +} + +void UTimeOfDaySubsystem::SkipToNextMorning() +{ + const double Target = DAY_START_HOUR * MINUTES_PER_HOUR; // 08:00 + double Delta = Target - GetMinuteOfDay(); + if (Delta <= 0.0) + { + Delta += MINUTES_PER_DAY; + } + AdvanceClock(Delta); +} + +void UTimeOfDaySubsystem::Sleep() +{ + SkipTime(SLEEP_DURATION_HOURS * MINUTES_PER_HOUR); + RestorePlayerEnergy(); + // TODO(§9.8 / Phase 9): charge the equipped phone to 100% as part of the sleep cycle. + // TODO(§7.3): sleep does NOT reset hunger / effective-max decay — only eating does. + Autosave(); +} + +void UTimeOfDaySubsystem::PushPause(FName Reason) +{ + PauseReasons.Add(Reason); +} + +void UTimeOfDaySubsystem::PopPause(FName Reason) +{ + PauseReasons.Remove(Reason); +} + +void UTimeOfDaySubsystem::AdvanceClock(double DeltaMinutes) +{ + UGlobalSaveGameData* Save = GetSave(); + if (!Save || DeltaMinutes <= 0.0) + return; + + const double Prev = Save->MinuteOfDay; + double Next = Prev + DeltaMinutes; + + // Fire an hour boundary for every integer hour crossed (handles midnight wrap and + // multi-hour skips). Each index is folded to 0–23; boundary 4 rolls the calendar. + const int32 PrevHourIdx = FMath::FloorToInt(Prev / MINUTES_PER_HOUR); + const int32 NextHourIdx = FMath::FloorToInt(Next / MINUTES_PER_HOUR); + for (int32 HourIdx = PrevHourIdx + 1; HourIdx <= NextHourIdx; ++HourIdx) + { + HandleHourBoundary(((HourIdx % 24) + 24) % 24); + } + + while (Next >= MINUTES_PER_DAY) + { + Next -= MINUTES_PER_DAY; + } + Save->MinuteOfDay = static_cast(Next); + + PushTimeToSky(); +} + +void UTimeOfDaySubsystem::HandleHourBoundary(int32 HourOfDay) +{ + OnHourChanged.Broadcast(HourOfDay); + + if (HourOfDay == DAY_ROLL_HOUR) + { + AdvanceCalendarDay(); + } + + if (HourOfDay == FMath::FloorToInt(DAY_START_HOUR)) + { + SetPhase(EDayPhase::Day); + } + else if (HourOfDay == FMath::FloorToInt(NIGHT_START_HOUR)) + { + SetPhase(EDayPhase::Night); + } +} + +void UTimeOfDaySubsystem::SetPhase(EDayPhase NewPhase) +{ + if (NewPhase == CurrentPhase) + return; + + CurrentPhase = NewPhase; + OnPhaseChanged.Broadcast(NewPhase); +} + +void UTimeOfDaySubsystem::AdvanceCalendarDay() +{ + UGlobalSaveGameData* Save = GetSave(); + if (!Save) + return; + + Save->DaysPassed++; + OnDayChanged.Broadcast(Save->DaysPassed); + + DepositDailyFollowerIncome(); + + if (Save->DaysPassed > 0 && (Save->DaysPassed % WEEK_LENGTH_DAYS) == 0) + { + ChargeWeeklyRent(); + } + + // §3.3: surviving to day 90 ends the campaign (win). Days advance one at a time, so + // an exact-match fires this once. Endless mode never ends here. + if (!Save->bEndlessMode && Save->DaysPassed == CAMPAIGN_LENGTH_DAYS) + { + OnCampaignEnded.Broadcast(ECampaignEndReason::CampaignComplete); + } +} + +void UTimeOfDaySubsystem::ChargeWeeklyRent() +{ + UGlobalSaveGameData* Save = GetSave(); + if (!Save || Save->bEndlessMode) + return; + + if (Save->Money >= WEEKLY_RENT) + { + Save->Money -= WEEKLY_RENT; + Save->LastRentChargeDay = Save->DaysPassed; + Autosave(); + } + else + { + // §3.3 / §22 #8: can't make rent → eviction → run over. No grace period. + OnCampaignEnded.Broadcast(ECampaignEndReason::Evicted); + } +} + +void UTimeOfDaySubsystem::DepositDailyFollowerIncome() +{ + // §7.9 / §20 #25: passive follower income auto-deposits to the bank each day-roll. + // TODO(Phase 8): once a follower-count attribute exists, deposit + // FollowerCount * into Save->Money here. There is no + // follower attribute yet, so this is intentionally a no-op (payout reads 0). +} + +void UTimeOfDaySubsystem::PushTimeToSky() +{ + const UGlobalSaveGameData* Save = GetSave(); + if (!Save) + return; + + const int32 CurMinute = FMath::FloorToInt(Save->MinuteOfDay); + if (CurMinute == LastPushedMinute) + return; + LastPushedMinute = CurMinute; + + const int32 Hours = CurMinute / MINUTES_PER_HOUR; + const int32 Minutes = CurMinute % MINUTES_PER_HOUR; + OnPushTimeToSky.Broadcast(FTimecode(Hours, Minutes, 0, 0, false)); +} + +EDayPhase UTimeOfDaySubsystem::ComputePhase(float InMinuteOfDay) +{ + const float Hour = InMinuteOfDay / MINUTES_PER_HOUR; + return (Hour >= DAY_START_HOUR && Hour < NIGHT_START_HOUR) ? EDayPhase::Day : EDayPhase::Night; +} + +UGlobalSaveGameData* UTimeOfDaySubsystem::GetSave() const +{ + if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance()) + { + if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem()) + { + return SaveSubsystem->GetCurrentSave(); + } + } + return nullptr; +} + +void UTimeOfDaySubsystem::RestorePlayerEnergy() const +{ + if (ANakedDesireCharacter* Player = Cast(UGameplayStatics::GetPlayerCharacter(this, SLOT_PLAYER))) + { + if (Player->StatsManager) + { + Player->StatsManager->RestoreEnergy(); + } + } +} + +void UTimeOfDaySubsystem::Autosave() const +{ + if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance()) + { + if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem()) + { + SaveSubsystem->SaveGame(); + } + } +} \ No newline at end of file diff --git a/Source/NakedDesire/Global/TimeOfDaySubsystem.h b/Source/NakedDesire/Global/TimeOfDaySubsystem.h new file mode 100644 index 00000000..7c50bc66 --- /dev/null +++ b/Source/NakedDesire/Global/TimeOfDaySubsystem.h @@ -0,0 +1,143 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "TimeOfDaySubsystem.generated.h" + +class UGlobalSaveGameData; + +/** + * Day vs. night phase (GDD §10.1). Drives NPC density and embarrassment rate. + * Day is 08:00–20:00; everything else is night. + */ +UENUM(BlueprintType) +enum class EDayPhase : uint8 +{ + Day, + Night +}; + +/** + * Why the 90-day campaign ended (GDD §3.3). Evicted is the rent-failure loss; + * CampaignComplete fires when the player survives to day 90. The ending screen / + * win-threshold logic (§21 open) lives in BP and reacts to OnCampaignEnded. + */ +UENUM(BlueprintType) +enum class ECampaignEndReason : uint8 +{ + Evicted, + CampaignComplete +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHourChangedSignature, int32, Hour); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDayChangedSignature, int32, NewDay); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPhaseChangedSignature, EDayPhase, NewPhase); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCampaignEndedSignature, ECampaignEndReason, Reason); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPushTimeToSkySignature, const FTimecode&, Timecode); + +/** + * The single authoritative clock (GDD §2.4, §10.1). Owns time-of-day and the + * calendar in C++ and pushes the current time to the UltraDynamicSky actor each + * in-game minute via ANakedDesireGameMode::SetCurrentTime — inverting the old + * BP-drives-time flow. Persists to UGlobalSaveGameData (MinuteOfDay / DaysPassed). + * + * The calendar rolls at 04:00 (DAY_ROLL_HOUR); the day phase flips at 08:00 / 20:00. + * Weekly rent is charged every WEEK_LENGTH_DAYS-th roll; follower income deposits + * each roll. Sleep / time-skips funnel through AdvanceClock so boundaries always fire. + */ +UCLASS() +class NAKEDDESIRE_API UTimeOfDaySubsystem : public UTickableWorldSubsystem +{ + GENERATED_BODY() + +public: + virtual void OnWorldBeginPlay(UWorld& InWorld) override; + + // FTickableGameObject + virtual void Tick(float DeltaTime) override; + virtual TStatId GetStatId() const override; + virtual bool IsTickable() const override { return bBegunPlay && !IsTemplate(); } + + // --- Queries --- + UFUNCTION(BlueprintPure, Category = "Time") + int32 GetDay() const; + + UFUNCTION(BlueprintPure, Category = "Time") + float GetMinuteOfDay() const; + + UFUNCTION(BlueprintPure, Category = "Time") + int32 GetHour() const; + + UFUNCTION(BlueprintPure, Category = "Time") + int32 GetMinute() const; + + UFUNCTION(BlueprintPure, Category = "Time") + EDayPhase GetPhase() const; + + UFUNCTION(BlueprintPure, Category = "Time") + bool IsDay() const { return GetPhase() == EDayPhase::Day; } + + // --- Time control --- + // Advance the clock by a number of in-game minutes, firing every boundary crossed. + UFUNCTION(BlueprintCallable, Category = "Time") + void SkipTime(float Minutes); + + // Fast-forward to the next 08:00 (used by the §4.4 holding-cell cutscene). + UFUNCTION(BlueprintCallable, Category = "Time") + void SkipToNextMorning(); + + // §2.4 sleep: fast-forward 8 hours, restore energy, autosave. (The apartment bed + // also routes outside-clothing loss through USessionLossResolver — see §4.4.) + UFUNCTION(BlueprintCallable, Category = "Time") + void Sleep(); + + // --- Pause (reason-keyed; the clock runs only while the reason set is empty). + // Note §11.17: the holding-cell cutscene deliberately does NOT pause the clock. --- + UFUNCTION(BlueprintCallable, Category = "Time") + void PushPause(FName Reason); + + UFUNCTION(BlueprintCallable, Category = "Time") + void PopPause(FName Reason); + + UFUNCTION(BlueprintPure, Category = "Time") + bool IsPaused() const { return PauseReasons.Num() > 0; } + + // --- Delegates --- + UPROPERTY(BlueprintAssignable, Category = "Time") + FOnHourChangedSignature OnHourChanged; + + UPROPERTY(BlueprintAssignable, Category = "Time") + FOnDayChangedSignature OnDayChanged; + + UPROPERTY(BlueprintAssignable, Category = "Time") + FOnPhaseChangedSignature OnPhaseChanged; + + UPROPERTY(BlueprintAssignable, Category = "Time") + FOnCampaignEndedSignature OnCampaignEnded; + + UPROPERTY(BlueprintAssignable, Category = "Time") + FOnPushTimeToSkySignature OnPushTimeToSky; + +private: + void AdvanceClock(double DeltaMinutes); + void HandleHourBoundary(int32 HourOfDay); // 0–23 + void SetPhase(EDayPhase NewPhase); + void AdvanceCalendarDay(); + void ChargeWeeklyRent(); + void DepositDailyFollowerIncome(); + void PushTimeToSky(); + + static EDayPhase ComputePhase(float InMinuteOfDay); + + UGlobalSaveGameData* GetSave() const; + void RestorePlayerEnergy() const; + void Autosave() const; + + // Last whole in-game minute pushed to the sky, to throttle the push to ~1/min. + int32 LastPushedMinute = -1; + EDayPhase CurrentPhase = EDayPhase::Day; + TSet PauseReasons; + bool bBegunPlay = false; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Interactables/Bed.cpp b/Source/NakedDesire/Interactables/Bed.cpp new file mode 100644 index 00000000..0f980f90 --- /dev/null +++ b/Source/NakedDesire/Interactables/Bed.cpp @@ -0,0 +1,117 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "Bed.h" + +#include "Components/BoxComponent.h" +#include "Components/WidgetComponent.h" +#include "NakedDesire/Global/SessionLossResolver.h" +#include "NakedDesire/Global/SessionManagerSubsystem.h" +#include "NakedDesire/Global/TimeOfDaySubsystem.h" + +#define LOCTEXT_NAMESPACE "Bed" + +ABed::ABed() +{ + ColliderComponent = CreateDefaultSubobject(TEXT("Collider")); + SetRootComponent(ColliderComponent); + // Trace-only: detected by the interaction LOS/focus line traces (ECC_Visibility), + // transparent to movement and physics. + ColliderComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + ColliderComponent->SetCollisionObjectType(ECC_WorldStatic); + ColliderComponent->SetCollisionResponseToAllChannels(ECR_Ignore); + ColliderComponent->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); + + MeshComponent = CreateDefaultSubobject(TEXT("Mesh")); + MeshComponent->SetupAttachment(RootComponent); + // Movement blocker only: stops the character capsule (ECC_Pawn), but is invisible + // to line traces so it never occludes the interaction LOS check. + MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + MeshComponent->SetCollisionObjectType(ECC_WorldStatic); + MeshComponent->SetCollisionResponseToAllChannels(ECR_Ignore); + MeshComponent->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); + + InteractionHint = CreateDefaultSubobject(TEXT("Interaction Hint")); + InteractionHint->SetupAttachment(RootComponent); +} + +void ABed::Interact_Implementation(ANakedDesireCharacter* Player) +{ + // Cosmetic transition first (BP), then resolve the sleep. The skip is instantaneous, + // so a BP fade simply overlaps it and clears on the new time. + PlaySleepTransition(); + DoSleep(); +} + +bool ABed::CanInteract_Implementation(ANakedDesireCharacter* Player) const +{ + // The bed only exists in the apartment, so reaching it means the session has ended. + // Guard defensively against sleeping while a session is still flagged active. + if (const UWorld* World = GetWorld()) + { + if (const USessionManagerSubsystem* Session = World->GetSubsystem()) + { + return !Session->IsSessionActive(); + } + } + return true; +} + +FText ABed::GetInteractionPrompt_Implementation() const +{ + return LOCTEXT("SleepPrompt", "Sleep"); +} + +void ABed::DoSleep() +{ + UWorld* World = GetWorld(); + if (!World) + return; + + // §4.4 step 2: clothing left outside the apartment is guaranteed lost on sleep. The + // loss is owned by USessionLossResolver (all "what gets lost" logic lives there). + if (USessionLossResolver* Resolver = World->GetSubsystem()) + { + Resolver->ResolveSleepLoss(); + } + + // Time skip + energy restore + phone charge + autosave (§2.4 / §9.8 / §11.19). This + // is the single save for the whole sleep flow — it also persists the loss above. + if (UTimeOfDaySubsystem* Time = World->GetSubsystem()) + { + Time->Sleep(); + } +} + +void ABed::HideInteractionHint_Implementation() +{ + ApplyOutline(false, 0); + InteractionHint->SetVisibility(false); +} + +void ABed::ShowInteractionFocusHint_Implementation() +{ + ApplyOutline(true, 2); + InteractionHint->SetVisibility(true); +} + +void ABed::ShowInteractionProximityHint_Implementation() +{ + ApplyOutline(true, 1); + InteractionHint->SetVisibility(false); +} + +void ABed::BeginPlay() +{ + Super::BeginPlay(); + + InteractionHint->SetVisibility(false); +} + +void ABed::ApplyOutline(bool bEnabled, int32 StencilValue) +{ + MeshComponent->SetRenderCustomDepth(bEnabled); + MeshComponent->SetCustomDepthStencilValue(StencilValue); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/NakedDesire/Interactables/Bed.h b/Source/NakedDesire/Interactables/Bed.h new file mode 100644 index 00000000..8de95c39 --- /dev/null +++ b/Source/NakedDesire/Interactables/Bed.h @@ -0,0 +1,57 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "NakedDesire/Interaction/Interactable.h" +#include "Bed.generated.h" + +class UBoxComponent; +class UWidgetComponent; + +/** + * The apartment bed (GDD §2.4, §4.4 step 2, §11.19). Interacting sleeps: fast-forwards + * 8 hours, restores energy, charges the phone, autosaves (all via UTimeOfDaySubsystem), + * and loses any clothing left outside the apartment. Sleep is the only place the player + * sleeps — there is no sleeping outside. + */ +UCLASS(Blueprintable) +class NAKEDDESIRE_API ABed : public AActor, public IInteractable +{ + GENERATED_BODY() + +public: + ABed(); + + virtual void Interact_Implementation(ANakedDesireCharacter* Player) override; + virtual bool CanInteract_Implementation(ANakedDesireCharacter* Player) const override; + virtual FText GetInteractionPrompt_Implementation() const override; + virtual void HideInteractionHint_Implementation() override; + virtual void ShowInteractionFocusHint_Implementation() override; + virtual void ShowInteractionProximityHint_Implementation() override; + +protected: + virtual void BeginPlay() override; + + // Cosmetic fade / SFX over the (instantaneous) time jump — authored in BP. Fired just + // before the sleep is resolved so the screen can be covered as time advances. + UFUNCTION(BlueprintImplementableEvent, Category = "Sleep") + void PlaySleepTransition(); + +private: + // Performs the actual sleep transaction (time skip + energy + loss). Split out so a BP + // fade timeline can drive it at the black midpoint if desired. + void DoSleep(); + + void ApplyOutline(bool bEnabled, int32 StencilValue); + + UPROPERTY(EditDefaultsOnly) + TObjectPtr MeshComponent; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr ColliderComponent; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr InteractionHint; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Interaction/InteractionComponent.cpp b/Source/NakedDesire/Interaction/InteractionComponent.cpp index bbe1b77c..9fdc55e7 100644 --- a/Source/NakedDesire/Interaction/InteractionComponent.cpp +++ b/Source/NakedDesire/Interaction/InteractionComponent.cpp @@ -2,6 +2,7 @@ #include "Interactable.h" #include "Engine/OverlapResult.h" #include "CollisionQueryParams.h" +#include "DrawDebugHelpers.h" #include "NakedDesire/Player/NakedDesireCharacter.h" UInteractionComponent::UInteractionComponent() @@ -204,5 +205,16 @@ bool UInteractionComponent::HasLineOfSightFromPawn(AActor* Target) const FHitResult Hit; const bool bBlocked = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params); + +#if ENABLE_DRAW_DEBUG + if (bDrawDebugLineOfSight) + { + const FColor LineColor = bBlocked ? FColor::Red : FColor::Green; + DrawDebugLine(GetWorld(), Start, bBlocked ? Hit.ImpactPoint : End, LineColor, false, 0.1f, 0, 1.f); + if (bBlocked) + DrawDebugPoint(GetWorld(), Hit.ImpactPoint, 10.f, FColor::Red, false, 0.1f); + } +#endif + return !bBlocked; } \ No newline at end of file diff --git a/Source/NakedDesire/Interaction/InteractionComponent.h b/Source/NakedDesire/Interaction/InteractionComponent.h index 88970563..45f1e762 100644 --- a/Source/NakedDesire/Interaction/InteractionComponent.h +++ b/Source/NakedDesire/Interaction/InteractionComponent.h @@ -41,6 +41,10 @@ protected: UPROPERTY(EditDefaultsOnly, Category="Interaction") float LookDotThreshold = 0.9f; + // Draw the eye→target line-of-sight trace each check (green = clear, red = blocked). + UPROPERTY(EditDefaultsOnly, Category="Interaction|Debug") + bool bDrawDebugLineOfSight = false; + private: FTimerHandle InteractionTimerHandle; diff --git a/Source/NakedDesire/NPC/NPC.cpp b/Source/NakedDesire/NPC/NPC.cpp index 0123164c..d5dd53d5 100644 --- a/Source/NakedDesire/NPC/NPC.cpp +++ b/Source/NakedDesire/NPC/NPC.cpp @@ -15,10 +15,3 @@ ANPC::ANPC() AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned; } -void ANPC::Destroyed() -{ - Super::Destroyed(); - - OnDestroyed.Broadcast(this); -} - diff --git a/Source/NakedDesire/NPC/NPC.h b/Source/NakedDesire/NPC/NPC.h index 4e70c29d..a93046cc 100644 --- a/Source/NakedDesire/NPC/NPC.h +++ b/Source/NakedDesire/NPC/NPC.h @@ -8,8 +8,6 @@ class ANPC; -DECLARE_MULTICAST_DELEGATE_OneParam(FOnNPCDestroyed, ANPC* NPC); - UCLASS() class NAKEDDESIRE_API ANPC : public ACharacter { @@ -17,9 +15,4 @@ class NAKEDDESIRE_API ANPC : public ACharacter public: ANPC(); - - FOnNPCDestroyed OnDestroyed; - -protected: - virtual void Destroyed() override; }; diff --git a/Source/NakedDesire/NPC/NPCAIController.cpp b/Source/NakedDesire/NPC/NPCAIController.cpp index aa8d6cfb..582c7072 100644 --- a/Source/NakedDesire/NPC/NPCAIController.cpp +++ b/Source/NakedDesire/NPC/NPCAIController.cpp @@ -2,44 +2,29 @@ #include "NPCAIController.h" - -#include "NavigationSystem.h" -#include "NPCTargetLocation.h" -#include "AI/NavigationSystemBase.h" -#include "BehaviorTree/BlackboardComponent.h" -#include "Kismet/GameplayStatics.h" #include "Perception/AISense_Sight.h" -#include "NakedDesire/Global/NakedDesireGameMode.h" +#include "Perception/AISenseConfig_Sight.h" #include "NakedDesire/Player/NakedDesireCharacter.h" #include "NakedDesire/Stats/StatsManager.h" #include "Perception/AIPerceptionComponent.h" -void ANPCAIController::SetShouldReactToPlayer(const bool Value) +ANPCAIController::ANPCAIController() { - Blackboard->SetValueAsBool(FName(TEXT("ShouldReactToPlayer")), Value); + UAIPerceptionComponent* Perception = CreateDefaultSubobject(TEXT("PerceptionComponent")); + SetPerceptionComponent(*Perception); + + UAISenseConfig_Sight* SightConfig = CreateDefaultSubobject(TEXT("SightConfig")); + SightConfig->DetectionByAffiliation.bDetectEnemies = true; + SightConfig->DetectionByAffiliation.bDetectNeutrals = true; + SightConfig->DetectionByAffiliation.bDetectFriendlies = true; + Perception->ConfigureSense(*SightConfig); + Perception->SetDominantSense(SightConfig->GetSenseImplementation()); } void ANPCAIController::OnPossess(APawn* InPawn) { Super::OnPossess(InPawn); - NavigationSystem = FNavigationSystem::GetCurrent(GetWorld()); - GameMode = Cast(UGameplayStatics::GetGameMode(GetWorld())); - - RunBehaviorTree(BehaviorTreeAsset); - - const FVector SpawnLocation = InPawn->GetActorLocation(); - - TArray TargetActors; - UGameplayStatics::GetAllActorsOfClass(GetWorld(), ANPCTargetLocation::StaticClass(), TargetActors); - - const int RandomIndex = FMath::RandRange(0, TargetActors.Num() - 1); - Blackboard->SetValueAsVector("TargetLocation", TargetActors[RandomIndex]->GetActorLocation()); - Blackboard->SetValueAsVector("SpawnLocation", SpawnLocation); - - PlayerCharacter = Cast(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); - Blackboard->SetValueAsObject("Player", PlayerCharacter); - PerceptionComponent->OnTargetPerceptionUpdated.AddUniqueDynamic(this, &ANPCAIController::OnTargetPerceptionUpdate); } @@ -55,11 +40,15 @@ void ANPCAIController::OnUnPossess() void ANPCAIController::OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimulus) { - if (Actor != PlayerCharacter) - return; if (Stimulus.Type != UAISense::GetSenseID()) return; + ANakedDesireCharacter* SensedPlayer = Cast(Actor); + if (!SensedPlayer) + return; + + PlayerCharacter = SensedPlayer; + const bool bSensed = Stimulus.WasSuccessfullySensed(); if (bSensed == bCurrentlyObserving) return; diff --git a/Source/NakedDesire/NPC/NPCAIController.h b/Source/NakedDesire/NPC/NPCAIController.h index 82f1ec9a..dd2384f1 100644 --- a/Source/NakedDesire/NPC/NPCAIController.h +++ b/Source/NakedDesire/NPC/NPCAIController.h @@ -7,8 +7,6 @@ #include "Perception/AIPerceptionTypes.h" #include "NPCAIController.generated.h" -class ANakedDesireGameMode; -class UNavigationSystemV1; class ANakedDesireCharacter; UCLASS() @@ -19,20 +17,13 @@ class NAKEDDESIRE_API ANPCAIController : public ADetourCrowdAIController UPROPERTY() ANakedDesireCharacter* PlayerCharacter = nullptr; - UPROPERTY() - const UNavigationSystemV1* NavigationSystem = nullptr; - - UPROPERTY() - ANakedDesireGameMode* GameMode = nullptr; - UPROPERTY(EditDefaultsOnly) UBehaviorTree* BehaviorTreeAsset = nullptr; bool bCurrentlyObserving = false; public: - UFUNCTION(BlueprintCallable) - void SetShouldReactToPlayer(bool Value); + ANPCAIController(); protected: UFUNCTION() diff --git a/Source/NakedDesire/NPC/NPCSpawner.cpp b/Source/NakedDesire/NPC/NPCSpawner.cpp deleted file mode 100644 index a2acf451..00000000 --- a/Source/NakedDesire/NPC/NPCSpawner.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// © 2025 Naked People Team. All Rights Reserved. - - -#include "NPCSpawner.h" -#include "NPC.h" -#include "Kismet/GameplayStatics.h" -#include "NakedDesire/Global/NakedDesireGameMode.h" - -ANPCSpawner::ANPCSpawner() -{ - PrimaryActorTick.bCanEverTick = false; -} - -void ANPCSpawner::BeginPlay() -{ - Super::BeginPlay(); - - PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0); - - FTimerHandle TimerHandle; - const int32 RandomDelay = FMath::RandRange(5, 30); - GetWorldTimerManager().SetTimer(TimerHandle, this, &ANPCSpawner::OnTimerTick, 30, true, RandomDelay); - OnTimerTick(); - - if (AGameModeBase* CurrentGameMode = UGameplayStatics::GetGameMode(GetWorld())) - { - GameMode = Cast(CurrentGameMode); - } -} - -void ANPCSpawner::OnTimerTick() -{ - if (!PlayerCharacter || !GameMode) - { - return; - } - - const bool IsPlayerClose = FVector::Dist(PlayerCharacter->GetActorLocation(), GetActorLocation()) < 5000; - const FTimecode CurrentTime = GameMode->GetCurrentTime(); - const bool IsDay = CurrentTime.Hours >= 9.0f && CurrentTime.Hours < 21.00f; - const bool CanSpawnMore = IsDay ? NPCs.Num() < MaxCountDay : NPCs.Num() < MaxCountNight; - - if (CanSpawnMore && PlayerCharacter && IsPlayerClose && (FMath::RandBool() && !AlwaysSpawn)) - { - FActorSpawnParameters SpawnParameters; - SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; - const int RandomIndex = FMath::RandRange(0, NPCClasses.Num() - 1); - ANPC* NewNPC = GetWorld()->SpawnActor(NPCClasses[RandomIndex], GetActorLocation(), GetActorRotation(), SpawnParameters); - NewNPC->OnDestroyed.AddUObject(this, &ANPCSpawner::OnNPCDestroyed); - NPCs.Add(NewNPC); - } -} - -void ANPCSpawner::OnNPCDestroyed(ANPC* NPC) -{ - NPCs.Remove(NPC); -} - diff --git a/Source/NakedDesire/NPC/NPCSpawner.h b/Source/NakedDesire/NPC/NPCSpawner.h deleted file mode 100644 index d67bdebe..00000000 --- a/Source/NakedDesire/NPC/NPCSpawner.h +++ /dev/null @@ -1,49 +0,0 @@ -// © 2025 Naked People Team. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "GameFramework/Actor.h" -#include "NPCSpawner.generated.h" - -class ANakedDesireGameMode; -class ANPC; - -UCLASS() -class NAKEDDESIRE_API ANPCSpawner : public AActor -{ - GENERATED_BODY() - - UPROPERTY(EditDefaultsOnly) - TArray> NPCClasses; - - UPROPERTY(EditAnywhere) - bool AlwaysSpawn = false; - - UPROPERTY() - ACharacter* PlayerCharacter = nullptr; - - bool IsPlayerInRange = false; - - UPROPERTY() - ANakedDesireGameMode* GameMode = nullptr; - - UPROPERTY() - TArray NPCs; - - UPROPERTY(EditAnywhere, meta = (ClampMin = 1, ClampMax = 30, UIMin = 1, UIMax = 30)) - int MaxCountDay = 10; - - UPROPERTY(EditAnywhere, meta = (ClampMin = 1, ClampMax = 30, UIMin = 1, UIMax = 30)) - int MaxCountNight = 5; - -public: - ANPCSpawner(); - -protected: - virtual void BeginPlay() override; - -private: - void OnTimerTick(); - void OnNPCDestroyed(ANPC* NPC); -}; diff --git a/Source/NakedDesire/NPC/NPCTargetLocation.cpp b/Source/NakedDesire/NPC/NPCTargetLocation.cpp deleted file mode 100644 index b55aea49..00000000 --- a/Source/NakedDesire/NPC/NPCTargetLocation.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// © 2025 Naked People Team. All Rights Reserved. - - -#include "NPCTargetLocation.h" - -// Sets default values -ANPCTargetLocation::ANPCTargetLocation() -{ - PrimaryActorTick.bCanEverTick = false; -} - diff --git a/Source/NakedDesire/NPC/NPCTargetLocation.h b/Source/NakedDesire/NPC/NPCTargetLocation.h deleted file mode 100644 index b63c1b2c..00000000 --- a/Source/NakedDesire/NPC/NPCTargetLocation.h +++ /dev/null @@ -1,16 +0,0 @@ -// © 2025 Naked People Team. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "GameFramework/Actor.h" -#include "NPCTargetLocation.generated.h" - -UCLASS() -class NAKEDDESIRE_API ANPCTargetLocation : public AActor -{ - GENERATED_BODY() - -public: - ANPCTargetLocation(); -}; diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.cpp b/Source/NakedDesire/Player/NakedDesireCharacter.cpp index 18cce459..66cb081c 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.cpp +++ b/Source/NakedDesire/Player/NakedDesireCharacter.cpp @@ -119,7 +119,7 @@ void ANakedDesireCharacter::BeginPlay() { Super::BeginPlay(); - StimuliSourceComponent->RegisterForSense(TSubclassOf()); + StimuliSourceComponent->RegisterForSense(UAISense_Sight::StaticClass()); StimuliSourceComponent->RegisterWithPerceptionSystem(); // Initialize after Super::BeginPlay so clothing hydration has populated the diff --git a/Source/NakedDesire/SaveGame/GlobalSaveGameData.h b/Source/NakedDesire/SaveGame/GlobalSaveGameData.h index 770f2d7c..3c6fed4b 100644 --- a/Source/NakedDesire/SaveGame/GlobalSaveGameData.h +++ b/Source/NakedDesire/SaveGame/GlobalSaveGameData.h @@ -45,9 +45,18 @@ public: UPROPERTY(SaveGame) int32 DaysPassed = 0; + // Time of day in minutes since 00:00, range [0, 1440). Owned by UTimeOfDaySubsystem. UPROPERTY(SaveGame) - float HourOfDay = 0.0f; - + float MinuteOfDay = DAY_START_HOUR * MINUTES_PER_HOUR; // start the campaign at 08:00 + + // Endless mode disables the weekly rent charge / eviction (§3.3). + UPROPERTY(SaveGame) + bool bEndlessMode = false; + + // Day index of the most recent successful rent payment (book-keeping / bank UI). + UPROPERTY(SaveGame) + int32 LastRentChargeDay = 0; + private: UPROPERTY(SaveGame) TArray WardrobeItems; diff --git a/Source/NakedDesire/UI/HUDWidget.cpp b/Source/NakedDesire/UI/HUDWidget.cpp index 201727e5..3060286a 100644 --- a/Source/NakedDesire/UI/HUDWidget.cpp +++ b/Source/NakedDesire/UI/HUDWidget.cpp @@ -2,3 +2,22 @@ #include "HUDWidget.h" +#include "Components/ProgressBar.h" +#include "NakedDesire/Player/NakedDesireCharacter.h" +#include "NakedDesire/Stats/StatsManager.h" + +void UHUDWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + const ANakedDesireCharacter* Player = Cast(GetOwningPlayerPawn()); + if (!Player) + return; + + Player->StatsManager->EmbarrassmentUpdate.AddUniqueDynamic(this, &UHUDWidget::OnEmbarrassmentUpdated); +} + +void UHUDWidget::OnEmbarrassmentUpdated(float CurrentValue, float MaxValue) +{ + EmbarrassmentBar->SetPercent(CurrentValue / MaxValue); +} diff --git a/Source/NakedDesire/UI/HUDWidget.h b/Source/NakedDesire/UI/HUDWidget.h index 460b8d27..2a50fe79 100644 --- a/Source/NakedDesire/UI/HUDWidget.h +++ b/Source/NakedDesire/UI/HUDWidget.h @@ -6,8 +6,20 @@ #include "CommonUserWidget.h" #include "HUDWidget.generated.h" +class UProgressBar; + UCLASS(Abstract) class NAKEDDESIRE_API UHUDWidget : public UCommonUserWidget { GENERATED_BODY() + + UPROPERTY(meta = (BindWidget)) + TObjectPtr EmbarrassmentBar; + +protected: + virtual void NativeConstruct() override; + +private: + UFUNCTION() + void OnEmbarrassmentUpdated(float CurrentValue, float MaxValue); }; diff --git a/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp b/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp index dd9599f9..8ff0fee8 100644 --- a/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp +++ b/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp @@ -6,6 +6,7 @@ #include "EquipmentPanelWidget.h" #include "EquipmentSlotMenuWidget.h" #include "EquipmentSlotWidget.h" +#include "NakedDesire/Global/PlayerPreviewCaptureSubsystem.h" void UInventoryScreenWidget::NativeOnActivated() { @@ -35,6 +36,22 @@ void UInventoryScreenWidget::NativeOnActivated() } CloseMenu(); + + // Spin up the impostor scene-capture preview only while this screen is open. + if (UPlayerPreviewCaptureSubsystem* Preview = GetWorld()->GetSubsystem()) + { + Preview->SetPreviewActive(true); + } +} + +void UInventoryScreenWidget::NativeOnDeactivated() +{ + Super::NativeOnDeactivated(); + + if (UPlayerPreviewCaptureSubsystem* Preview = GetWorld()->GetSubsystem()) + { + Preview->SetPreviewActive(false); + } } void UInventoryScreenWidget::HandleSlotClicked(UEquipmentSlotWidget* SlotWidget) diff --git a/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.h b/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.h index 9658de45..3ac88b4f 100644 --- a/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.h +++ b/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.h @@ -28,6 +28,7 @@ class NAKEDDESIRE_API UInventoryScreenWidget : public UCommonActivatableWidget protected: virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; private: void HandleSlotClicked(UEquipmentSlotWidget* SlotWidget);