Setup player preview for equipment panel

This commit is contained in:
2026-05-31 15:33:43 +03:00
parent 4218b36ac9
commit 56cc2fce98
42 changed files with 823 additions and 318 deletions
+4
View File
@@ -135,3 +135,7 @@ DefaultMediaSoundClassName=/Game/Audio/SC_Music.SC_Music
AgentRadius=35.000000
AgentMaxSlope=50.000000
[/Script/GameplayDebugger.GameplayDebuggerConfig]
CategorySlot4=Backslash
CategorySlot5=RightBracket
+3
View File
@@ -2,6 +2,9 @@
CommonButtonAcceptKeyHandling=TriggerClick
bAutoLoadData=True
[/Script/AIModule.AISense_Sight]
bAutoRegisterAllPawnsAsSources=False
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=305A61484AE3739092FF13931B45C2C6
ProjectName=Naked Desire
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14 -12
View File
@@ -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 `0921` 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 (34 days)
- `UTimeOfDaySubsystem` replacing the BP-implementable time on `NakedDesireGameMode`. 90-day calendar, week boundary, day phase `08:0020:00` (fix the `0921` 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:0020:00`. (The old `NPCSpawner.cpp:40` `0921` 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 (12 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_
+6 -5
View File
@@ -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
+13
View File
@@ -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)
@@ -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<USaveSubsystem>();
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<USaveSubsystem>();
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<AWardrobe>(FoundActor))
{
Wardrobe = WardrobeActor;
}
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
SpawnSavedWorldItems(SaveSubsystem->GetCurrentSave());
}
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
for (const FItemSaveRecord& Item : SaveSubsystem->GetCurrentSave()->GetWorldItems())
void ANakedDesireGameMode::SpawnSavedWorldItems(UGlobalSaveGameData* SaveGameData)
{
for (const FItemSaveRecord& Item : SaveGameData->GetWorldItems())
{
UClothingItemInstance* NewItemInstance = Cast<UClothingItemInstance>(UItemInstance::CreateFromRecord(this, Item));
if (!NewItemInstance)
@@ -81,15 +34,3 @@ void ANakedDesireGameMode::BeginPlay()
NewItemPickup->SetItem(NewItemInstance);
}
}
void ANakedDesireGameMode::RefreshDailyMissions()
{
const ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
if (!Player)
return;
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
int ClampedIndex = FMath::Clamp(SaveSubsystem->GetCurrentSave()->DaysPassed , 0, MissionsConfig->DailyMissions.Num() - 1);
Player->MissionsManager->RefreshDailyMissions(MissionsConfig->DailyMissions[ClampedIndex].Missions);
}
@@ -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<AItemPickup> ItemPickupClass;
void SpawnSavedWorldItems(UGlobalSaveGameData* SaveGameData);
};
@@ -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);
}
@@ -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;
};
@@ -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();
@@ -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;
@@ -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<double>(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<double>(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 023; 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<float>(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 * <daily per-follower rate> 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<USaveSubsystem>())
{
return SaveSubsystem->GetCurrentSave();
}
}
return nullptr;
}
void UTimeOfDaySubsystem::RestorePlayerEnergy() const
{
if (ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(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<USaveSubsystem>())
{
SaveSubsystem->SaveGame();
}
}
}
@@ -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:0020: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); // 023
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<FName> PauseReasons;
bool bBegunPlay = false;
};
+117
View File
@@ -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<UBoxComponent>(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<UStaticMeshComponent>(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<UWidgetComponent>(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<USessionManagerSubsystem>())
{
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<USessionLossResolver>())
{
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<UTimeOfDaySubsystem>())
{
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
+57
View File
@@ -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<UStaticMeshComponent> MeshComponent;
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UBoxComponent> ColliderComponent;
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UWidgetComponent> InteractionHint;
};
@@ -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;
}
@@ -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;
-7
View File
@@ -15,10 +15,3 @@ ANPC::ANPC()
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
void ANPC::Destroyed()
{
Super::Destroyed();
OnDestroyed.Broadcast(this);
}
-7
View File
@@ -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;
};
+17 -28
View File
@@ -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<UAIPerceptionComponent>(TEXT("PerceptionComponent"));
SetPerceptionComponent(*Perception);
UAISenseConfig_Sight* SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(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<UNavigationSystemV1>(GetWorld());
GameMode = Cast<ANakedDesireGameMode>(UGameplayStatics::GetGameMode(GetWorld()));
RunBehaviorTree(BehaviorTreeAsset);
const FVector SpawnLocation = InPawn->GetActorLocation();
TArray<AActor*> 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<ANakedDesireCharacter>(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<UAISense_Sight>())
return;
ANakedDesireCharacter* SensedPlayer = Cast<ANakedDesireCharacter>(Actor);
if (!SensedPlayer)
return;
PlayerCharacter = SensedPlayer;
const bool bSensed = Stimulus.WasSuccessfullySensed();
if (bSensed == bCurrentlyObserving)
return;
+1 -10
View File
@@ -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()
-58
View File
@@ -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<ANakedDesireGameMode>(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<ANPC>(NPCClasses[RandomIndex], GetActorLocation(), GetActorRotation(), SpawnParameters);
NewNPC->OnDestroyed.AddUObject(this, &ANPCSpawner::OnNPCDestroyed);
NPCs.Add(NewNPC);
}
}
void ANPCSpawner::OnNPCDestroyed(ANPC* NPC)
{
NPCs.Remove(NPC);
}
-49
View File
@@ -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<TSubclassOf<ANPC>> NPCClasses;
UPROPERTY(EditAnywhere)
bool AlwaysSpawn = false;
UPROPERTY()
ACharacter* PlayerCharacter = nullptr;
bool IsPlayerInRange = false;
UPROPERTY()
ANakedDesireGameMode* GameMode = nullptr;
UPROPERTY()
TArray<ANPC*> 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);
};
@@ -1,11 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "NPCTargetLocation.h"
// Sets default values
ANPCTargetLocation::ANPCTargetLocation()
{
PrimaryActorTick.bCanEverTick = false;
}
@@ -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();
};
@@ -119,7 +119,7 @@ void ANakedDesireCharacter::BeginPlay()
{
Super::BeginPlay();
StimuliSourceComponent->RegisterForSense(TSubclassOf<UAISense_Sight>());
StimuliSourceComponent->RegisterForSense(UAISense_Sight::StaticClass());
StimuliSourceComponent->RegisterWithPerceptionSystem();
// Initialize after Super::BeginPlay so clothing hydration has populated the
@@ -45,8 +45,17 @@ 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)
+19
View File
@@ -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<ANakedDesireCharacter>(GetOwningPlayerPawn());
if (!Player)
return;
Player->StatsManager->EmbarrassmentUpdate.AddUniqueDynamic(this, &UHUDWidget::OnEmbarrassmentUpdated);
}
void UHUDWidget::OnEmbarrassmentUpdated(float CurrentValue, float MaxValue)
{
EmbarrassmentBar->SetPercent(CurrentValue / MaxValue);
}
+12
View File
@@ -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<UProgressBar> EmbarrassmentBar;
protected:
virtual void NativeConstruct() override;
private:
UFUNCTION()
void OnEmbarrassmentUpdated(float CurrentValue, float MaxValue);
};
@@ -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<UPlayerPreviewCaptureSubsystem>())
{
Preview->SetPreviewActive(true);
}
}
void UInventoryScreenWidget::NativeOnDeactivated()
{
Super::NativeOnDeactivated();
if (UPlayerPreviewCaptureSubsystem* Preview = GetWorld()->GetSubsystem<UPlayerPreviewCaptureSubsystem>())
{
Preview->SetPreviewActive(false);
}
}
void UInventoryScreenWidget::HandleSlotClicked(UEquipmentSlotWidget* SlotWidget)
@@ -28,6 +28,7 @@ class NAKEDDESIRE_API UInventoryScreenWidget : public UCommonActivatableWidget
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
private:
void HandleSlotClicked(UEquipmentSlotWidget* SlotWidget);