From 0c94a411dc9afe968777c0f17138972d742ebca6 Mon Sep 17 00:00:00 2001 From: koritsa Date: Sat, 30 May 2026 00:18:14 +0300 Subject: [PATCH] Added session system --- PLAN.md | 12 +- .../Global/SessionLossResolver.cpp | 132 ++++++++++++++++++ .../NakedDesire/Global/SessionLossResolver.h | 57 ++++++++ .../Global/SessionManagerSubsystem.cpp | 80 +++++++++++ .../Global/SessionManagerSubsystem.h | 80 +++++++++++ .../NakedDesire/Locations/LocationTrigger.cpp | 37 +++++ .../NakedDesire/Locations/LocationTrigger.h | 18 +++ Source/NakedDesire/Stats/StatsManager.cpp | 9 +- 8 files changed, 413 insertions(+), 12 deletions(-) create mode 100644 Source/NakedDesire/Global/SessionLossResolver.cpp create mode 100644 Source/NakedDesire/Global/SessionLossResolver.h create mode 100644 Source/NakedDesire/Global/SessionManagerSubsystem.cpp create mode 100644 Source/NakedDesire/Global/SessionManagerSubsystem.h diff --git a/PLAN.md b/PLAN.md index 6e1b605a..9a38685d 100644 --- a/PLAN.md +++ b/PLAN.md @@ -24,7 +24,9 @@ State of the C++ module as of the latest pass. File references use `Source/Naked - **AI sight + behavior tree** — `NPC/NPCAIController.cpp` runs a BT with `Player` / `TargetLocation` / `SpawnLocation` blackboard keys. `NPC/NPCSpawner.cpp` proximity-gated spawn with day / night caps. - **Mission framework** — `Mission` → `MissionGoal` → `GoalRestriction` composition (`MissionBuilder/`). Two goals (`FlashGoal`, `MinTimeGoal`), three restrictions (`EquipClothing`, `ExposeBodyPart`, `Location`). Iterate-and-mutate bug in `MissionsManager::RefreshDailyMissions` resolved (`MissionsManager.cpp:53-68`). - **Daily-mission OOB guarded** — `NakedDesireGameMode::RefreshDailyMissions` now clamps `DaysPassed` to the authored array bounds (`NakedDesireGameMode.cpp:70-71`). Still a hand-authored list (see §1.3). -- **Location triggers** — `Locations/LocationTrigger`, `Locations/LocationData` via gameplay tags (GDD §10.4 area foundation). +- **Location triggers** — `Locations/LocationTrigger`, `Locations/LocationData` via gameplay tags (GDD §10.4 area foundation). `ALocationTrigger` now carries a `bIsApartment` flag; when set, its box overlap drives session start / end on the session subsystem. +- **Session manager (GDD §4.1–§4.4)** — `Global/SessionManagerSubsystem.h/.cpp` (`UWorldSubsystem`). Tracks `bSessionActive`, emits `OnSessionStart` / `OnSessionEnd(ESessionLossCause)`; `ESessionLossCause = { SafeReturn, EmbarrassmentMax, EnergyZero, PoliceCapture }`. Apartment `ALocationTrigger` starts a session on exit and safely ends it on re-entry. Subscribes (next-tick after world begin play) to `UStatsManager::EmbarrassmentUpdate` (max-hit → `EmbarrassmentMax`) and `EnergyUpdate` (≤0 → `EnergyZero`). Exposes `bPoliceChaseActive` (+ setter / getter) for the §4.4 loss-precedence rule the resolver owns. Replaces the old `EndGameEmbarrassed` GameMode BP call, which `UStatsManager::IncreaseEmbarrassment` no longer invokes. The `EndGameEmbarrassed` BlueprintImplementableEvent declaration still exists on `ANakedDesireGameMode` but is now dead from C++ and should be removed once BP no longer references it. +- **Session loss resolver (GDD §4.4)** — `Global/SessionLossResolver.h/.cpp` (`UWorldSubsystem`). Single entry point `ResolveLoss(ESessionLossCause)`, bound to `USessionManagerSubsystem::OnSessionEnd`. Applies the police-chase precedence override (any cause → `PoliceCapture` while `bPoliceChaseActive`), then per cause: `EmbarrassmentMax` no-cost; `EnergyZero` destroys every world `AItemPickup` + clears its save record (guaranteed sleep loss); `PoliceCapture` deducts `PoliceCaptureMoneyPenalty` if affordable else flags a holding-cell outcome; `SafeReturn` no loss. Never strips equipped clothing. Autosaves, then broadcasts `OnSessionLossResolved(FinalCause, bWentToHoldingCell)` for the BP presentation / time-skip layer. See §1.3 for the pieces still delegated to BP / later phases. - **Movement** — `EnhancedInput`, walk / run / crouch (`NakedDesireCharacter.cpp:115-127`), stamina-gated run (`Tick` lines 91-113). - **Wardrobe interactable** — `Interactables/Wardrobe.h` holds `TArray> ClothingItems`; `NakedDesireGameMode::BuyItem` charges money and pushes into the wardrobe. @@ -40,7 +42,7 @@ State of the C++ module as of the latest pass. File references use `Source/Naked ### 1.3 Missing -- **Session system (GDD §4)** — no `USessionManager`, no session start / end events, no `USessionLossResolver`. Game-over on embarrassment is a single `EndGameEmbarrassed` BP event in `NakedDesireGameMode`; none of §4.4's behavior (energy-zero cutscene to home, bag-placed-in-world loss, sleep-deterministic clothing loss, embarrassment-max → apartment with no extras) is implemented. +- **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"). - **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%. @@ -198,8 +200,8 @@ Phase estimates are rough and assume one engineer. Adjust as we go. ### Phase 3 — Session system + loss resolver (1 week) -- `USessionManager` (subsystem on `UNakedDesireGameInstance` or a `UWorldSubsystem`). Apartment `ALocationTrigger` flag drives session start / end. Emits `OnSessionStart` / `OnSessionEnd` with cause. -- `USessionLossResolver` — single class, one method `ResolveLoss(ESessionLossCause Cause)`. Implements GDD §4.4: +- ~~`USessionManager` (subsystem on `UNakedDesireGameInstance` or a `UWorldSubsystem`). Apartment `ALocationTrigger` flag drives session start / end. Emits `OnSessionStart` / `OnSessionEnd` with cause.~~ **DONE (DEV-99)** — `USessionManagerSubsystem` (`UWorldSubsystem`, `Global/`). See §1.1. +- ~~`USessionLossResolver` — single class, one method `ResolveLoss(ESessionLossCause Cause)`.~~ **DONE (DEV-100)** — `Global/SessionLossResolver` (`UWorldSubsystem`). See §1.1. C++ owns the transaction (precedence, money, item loss, autosave); cutscenes / fade / time-skip are delegated to BP via `OnSessionLossResolved`. Implements GDD §4.4: - **Police chase precedence**: if a chase is active (between detection and the disengage timer, §10.3), force `Cause = PoliceCapture` regardless of what triggered the loss. Chase always wins. - Equipped clothing **stays** equipped (do not strip). - Bag placed in world: mark its world record for deletion. @@ -208,7 +210,7 @@ Phase estimates are rough and assume one engineer. Adjust as we go. - **Embarrassment = max**: fade to apartment. **No extra cost** (no time skip, no money, no rep). - **Police capture (can pay)**: fade to apartment, deduct money penalty, clear `wanted`. - **Police capture (can't pay)**: short non-interactive holding-cell cutscene; time fast-forwards to next morning; debt settled; clear `wanted`. Already-accepted commissions still expire at day end with their normal `failurePenalty` — the cutscene does not pause the day clock. -- Wire `StatsManager::IncreaseEmbarrassment` max-hit and energy-zero into `USessionLossResolver`. Replace `EndGameEmbarrassed` BP-event with the C++ path. +- ~~Wire `StatsManager::IncreaseEmbarrassment` max-hit and energy-zero into~~ the session subsystem — **DONE (DEV-99)**: `USessionManagerSubsystem` subscribes to `EmbarrassmentUpdate` / `EnergyUpdate` and the C++ embarrassment path no longer calls `EndGameEmbarrassed`. DEV-100 routes `OnSessionEnd` into `USessionLossResolver`. - Add a debug overlay showing the current loss state and what would be lost if a given cause fired. **Exit criteria:** all four code paths (safe end, embarrassment max, energy zero, police capture) produce the §4.4 outcomes deterministically. Inventory state after each loss matches the §6.6 summary table 1:1. diff --git a/Source/NakedDesire/Global/SessionLossResolver.cpp b/Source/NakedDesire/Global/SessionLossResolver.cpp new file mode 100644 index 00000000..68282ff9 --- /dev/null +++ b/Source/NakedDesire/Global/SessionLossResolver.cpp @@ -0,0 +1,132 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "SessionLossResolver.h" + +#include "Kismet/GameplayStatics.h" +#include "NakedDesire/Clothing/ClothingItemInstance.h" +#include "NakedDesire/Interactables/ItemPickup.h" +#include "NakedDesire/SaveGame/GlobalSaveGameData.h" +#include "NakedDesire/SaveGame/SaveSubsystem.h" + +void USessionLossResolver::OnWorldBeginPlay(UWorld& InWorld) +{ + Super::OnWorldBeginPlay(InWorld); + + if (USessionManagerSubsystem* SessionManager = InWorld.GetSubsystem()) + { + SessionManager->OnSessionEnd.AddDynamic(this, &USessionLossResolver::ResolveLoss); + } +} + +void USessionLossResolver::ResolveLoss(ESessionLossCause Cause) +{ + // §4.4 precedence: if a chase is in progress, the loss always resolves as + // police capture — the chase wins regardless of what actually fired. + if (const USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem()) + { + if (SessionManager->IsPoliceChaseActive()) + { + Cause = ESessionLossCause::PoliceCapture; + } + } + + // §4.4: equipped clothing is never forcibly removed — the resolver simply never + // touches equipped items. + // TODO(§6.4): when bags exist, a bag placed in the world is lost with its + // contents — mark its world record for deletion here. + + bool bWentToHoldingCell = false; + + switch (Cause) + { + case ESessionLossCause::SafeReturn: + // Normal session end; nothing is lost. Autosave below. + break; + + case ESessionLossCause::EmbarrassmentMax: + // Fade to apartment with no extra cost — no time skip, money, or rep hit. + break; + + case ESessionLossCause::EnergyZero: + // Forced sleep cycle: everything left outside the apartment is guaranteed lost. + LoseAllWorldClothing(); + break; + + case ESessionLossCause::PoliceCapture: + { + UGlobalSaveGameData* Save = GetSave(); + if (Save && Save->Money >= PoliceCaptureMoneyPenalty) + { + // Can pay: deduct the penalty and go home. + Save->Money -= PoliceCaptureMoneyPenalty; + } + else + { + // Can't pay: a night in the holding cell settles the debt; no money owed. + bWentToHoldingCell = true; + } + ClearWanted(); + } + break; + } + + Autosave(); + + OnSessionLossResolved.Broadcast(Cause, bWentToHoldingCell); +} + +void USessionLossResolver::LoseAllWorldClothing() +{ + UGlobalSaveGameData* Save = GetSave(); + + // Loose clothing in the world is represented by AItemPickup actors. Dropping only + // happens outside, and the apartment stores items in the wardrobe (not as pickups), + // so every pickup currently counts as "outside the apartment". + // TODO: once an apartment volume query exists, spare pickups that sit inside it. + TArray Pickups; + UGameplayStatics::GetAllActorsOfClass(this, AItemPickup::StaticClass(), Pickups); + for (AActor* Actor : Pickups) + { + AItemPickup* Pickup = Cast(Actor); + if (!Pickup) + continue; + + if (Save) + { + if (UClothingItemInstance* Item = Pickup->GetItem()) + { + Save->RemoveWorldItem(Item); + } + } + Pickup->Destroy(); + } +} + +void USessionLossResolver::ClearWanted() +{ + // TODO(§7.7 / Phase 6): clear the `wanted` tag once the Wanted attribute exists. +} + +void USessionLossResolver::Autosave() const +{ + if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance()) + { + if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem()) + { + SaveSubsystem->SaveGame(); + } + } +} + +UGlobalSaveGameData* USessionLossResolver::GetSave() const +{ + if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance()) + { + if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem()) + { + return SaveSubsystem->GetCurrentSave(); + } + } + return nullptr; +} \ No newline at end of file diff --git a/Source/NakedDesire/Global/SessionLossResolver.h b/Source/NakedDesire/Global/SessionLossResolver.h new file mode 100644 index 00000000..c89026a1 --- /dev/null +++ b/Source/NakedDesire/Global/SessionLossResolver.h @@ -0,0 +1,57 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "SessionManagerSubsystem.h" +#include "SessionLossResolver.generated.h" + +/** + * Broadcast once a loss has been resolved transactionally. The presentation layer + * (fade / cutscene / teleport) and the time skip live in Blueprint / the future + * time subsystem and react to this, switching on FinalCause: + * - EmbarrassmentMax → fade to apartment. No time skip. + * - EnergyZero → trudge-home cutscene, then advance one sleep cycle. + * - PoliceCapture (!cell) → fade to apartment. + * - PoliceCapture (cell) → holding-cell cutscene, then fast-forward to next morning. + * - SafeReturn → no special presentation. + * The C++ side has already applied money / item / wanted changes by the time this fires. + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSessionLossResolvedSignature, ESessionLossCause, FinalCause, bool, bWentToHoldingCell); + +/** + * The single, deterministic owner of "what gets lost" on session end (GDD §4.4). + * Subscribes to USessionManagerSubsystem::OnSessionEnd and routes every cause + * through ResolveLoss. Per the architecture rule, no loss-handling logic should + * live anywhere else. + */ +UCLASS() +class NAKEDDESIRE_API USessionLossResolver : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + virtual void OnWorldBeginPlay(UWorld& InWorld) override; + + // Single entry point for all §4.4 outcomes. Bound to OnSessionEnd; also callable + // directly (e.g. from a debug command) to exercise a path. + UFUNCTION(BlueprintCallable, Category = "Session") + void ResolveLoss(ESessionLossCause Cause); + + UPROPERTY(BlueprintAssignable, Category = "Session") + FOnSessionLossResolvedSignature OnSessionLossResolved; + +private: + // EnergyZero / sleep: every clothing item left outside the apartment is guaranteed lost. + void LoseAllWorldClothing(); + + // §7.7: cleared on police capture. No-op until the Wanted attribute exists (Phase 6). + void ClearWanted(); + + void Autosave() const; + class UGlobalSaveGameData* GetSave() const; + + // §21 tuning placeholder — the police-capture money penalty. + float PoliceCaptureMoneyPenalty = 200.0f; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Global/SessionManagerSubsystem.cpp b/Source/NakedDesire/Global/SessionManagerSubsystem.cpp new file mode 100644 index 00000000..fb5e83f8 --- /dev/null +++ b/Source/NakedDesire/Global/SessionManagerSubsystem.cpp @@ -0,0 +1,80 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "SessionManagerSubsystem.h" + +#include "Kismet/GameplayStatics.h" +#include "NakedDesire/Global/Constants.h" +#include "NakedDesire/Player/NakedDesireCharacter.h" +#include "NakedDesire/Stats/StatsManager.h" + +void USessionManagerSubsystem::OnWorldBeginPlay(UWorld& InWorld) +{ + Super::OnWorldBeginPlay(InWorld); + + // The player pawn and its UStatsManager may not have finished BeginPlay when + // the world begins play, so defer binding by one tick. + InWorld.GetTimerManager().SetTimerForNextTick(this, &USessionManagerSubsystem::BindToPlayerStats); +} + +void USessionManagerSubsystem::BindToPlayerStats() +{ + ANakedDesireCharacter* Player = Cast(UGameplayStatics::GetPlayerCharacter(this, SLOT_PLAYER)); + if (!Player || !Player->StatsManager) + { + UE_LOG(LogTemp, Warning, TEXT("USessionManagerSubsystem: no player StatsManager to bind to; loss detection disabled.")); + return; + } + + Player->StatsManager->EmbarrassmentUpdate.AddDynamic(this, &USessionManagerSubsystem::HandleEmbarrassmentUpdate); + Player->StatsManager->EnergyUpdate.AddDynamic(this, &USessionManagerSubsystem::HandleEnergyUpdate); +} + +void USessionManagerSubsystem::NotifyEnteredApartment() +{ + // Returning home is the safe end of a session (§4.3). + if (bSessionActive) + { + EndSession(ESessionLossCause::SafeReturn); + } +} + +void USessionManagerSubsystem::NotifyLeftApartment() +{ + // Leaving the apartment is the only way to start a session (§4.1). + if (!bSessionActive) + { + StartSession(); + } +} + +void USessionManagerSubsystem::StartSession() +{ + bSessionActive = true; + OnSessionStart.Broadcast(); +} + +void USessionManagerSubsystem::EndSession(const ESessionLossCause Cause) +{ + if (!bSessionActive) + return; + + bSessionActive = false; + OnSessionEnd.Broadcast(Cause); +} + +void USessionManagerSubsystem::HandleEmbarrassmentUpdate(const float CurrentValue, const float MaxValue) +{ + if (bSessionActive && MaxValue > 0.0f && CurrentValue >= MaxValue) + { + EndSession(ESessionLossCause::EmbarrassmentMax); + } +} + +void USessionManagerSubsystem::HandleEnergyUpdate(const float CurrentValue, const float MaxValue) +{ + if (bSessionActive && CurrentValue <= 0.0f) + { + EndSession(ESessionLossCause::EnergyZero); + } +} \ No newline at end of file diff --git a/Source/NakedDesire/Global/SessionManagerSubsystem.h b/Source/NakedDesire/Global/SessionManagerSubsystem.h new file mode 100644 index 00000000..b435276d --- /dev/null +++ b/Source/NakedDesire/Global/SessionManagerSubsystem.h @@ -0,0 +1,80 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "SessionManagerSubsystem.generated.h" + +class UStatsManager; + +/** + * Why a session ended (GDD §4.4). SafeReturn is a non-loss end (player walked + * home); the other three are the session-loss conditions from §3.3. The actual + * "what gets lost" handling lives in USessionLossResolver (DEV-100), which also + * applies police-chase precedence via bPoliceChaseActive. + */ +UENUM(BlueprintType) +enum class ESessionLossCause : uint8 +{ + SafeReturn, + EmbarrassmentMax, + EnergyZero, + PoliceCapture +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSessionStartSignature); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionEndSignature, ESessionLossCause, Cause); + +/** + * Owns the "going outside" session state (GDD §4). A session starts when the + * player leaves the apartment and ends when they return safely or hit a loss + * condition (embarrassment max / energy zero / police capture). The apartment + * ALocationTrigger drives start / end; embarrassment-max and energy-zero are + * detected by subscribing to UStatsManager. This replaces the EndGameEmbarrassed + * Blueprint event on ANakedDesireGameMode. + */ +UCLASS() +class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + virtual void OnWorldBeginPlay(UWorld& InWorld) override; + + // Called by the apartment ALocationTrigger as the player crosses the threshold. + void NotifyEnteredApartment(); + void NotifyLeftApartment(); + + UFUNCTION(BlueprintPure, Category = "Session") + bool IsSessionActive() const { return bSessionActive; } + + // Set by the police AI pipeline (Phase 6). Read by USessionLossResolver for + // the §4.4 loss precedence rule: a loss fired mid-chase resolves as capture. + UFUNCTION(BlueprintCallable, Category = "Session") + void SetPoliceChaseActive(bool bActive) { bPoliceChaseActive = bActive; } + + UFUNCTION(BlueprintPure, Category = "Session") + bool IsPoliceChaseActive() const { return bPoliceChaseActive; } + + UPROPERTY(BlueprintAssignable, Category = "Session") + FOnSessionStartSignature OnSessionStart; + + UPROPERTY(BlueprintAssignable, Category = "Session") + FOnSessionEndSignature OnSessionEnd; + +private: + void StartSession(); + void EndSession(ESessionLossCause Cause); + + void BindToPlayerStats(); + + UFUNCTION() + void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue); + + UFUNCTION() + void HandleEnergyUpdate(float CurrentValue, float MaxValue); + + bool bSessionActive = false; + bool bPoliceChaseActive = false; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Locations/LocationTrigger.cpp b/Source/NakedDesire/Locations/LocationTrigger.cpp index 6a13ebc8..461682be 100644 --- a/Source/NakedDesire/Locations/LocationTrigger.cpp +++ b/Source/NakedDesire/Locations/LocationTrigger.cpp @@ -4,6 +4,8 @@ #include "LocationTrigger.h" #include "Components/BoxComponent.h" +#include "NakedDesire/Global/SessionManagerSubsystem.h" +#include "NakedDesire/Player/NakedDesireCharacter.h" ALocationTrigger::ALocationTrigger() @@ -19,3 +21,38 @@ ULocationData* ALocationTrigger::GetLocationData() const return LocationData; } +void ALocationTrigger::BeginPlay() +{ + Super::BeginPlay(); + + if (bIsApartment) + { + BoxTrigger->OnComponentBeginOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerBeginOverlap); + BoxTrigger->OnComponentEndOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerEndOverlap); + } +} + +void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, + UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) +{ + if (!OtherActor || !OtherActor->IsA()) + return; + + if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem()) + { + SessionManager->NotifyEnteredApartment(); + } +} + +void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, + UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) +{ + if (!OtherActor || !OtherActor->IsA()) + return; + + if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem()) + { + SessionManager->NotifyLeftApartment(); + } +} + diff --git a/Source/NakedDesire/Locations/LocationTrigger.h b/Source/NakedDesire/Locations/LocationTrigger.h index daefd03a..fa80f26f 100644 --- a/Source/NakedDesire/Locations/LocationTrigger.h +++ b/Source/NakedDesire/Locations/LocationTrigger.h @@ -20,8 +20,26 @@ class NAKEDDESIRE_API ALocationTrigger : public AActor UPROPERTY(EditAnywhere) ULocationData* LocationData; + // When set, the player crossing this trigger drives session start / end on + // USessionManagerSubsystem (GDD §4.1 / §4.3). Exactly one trigger — the + // apartment — should have this checked. + UPROPERTY(EditAnywhere, Category = "Session") + bool bIsApartment = false; + public: ALocationTrigger(); ULocationData* GetLocationData() const; + +protected: + virtual void BeginPlay() override; + +private: + UFUNCTION() + void OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, + UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); + + UFUNCTION() + void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, + UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); }; diff --git a/Source/NakedDesire/Stats/StatsManager.cpp b/Source/NakedDesire/Stats/StatsManager.cpp index 86bdd29d..a013ac96 100644 --- a/Source/NakedDesire/Stats/StatsManager.cpp +++ b/Source/NakedDesire/Stats/StatsManager.cpp @@ -2,8 +2,6 @@ #include "StatsManager.h" -#include "Kismet/GameplayStatics.h" -#include "NakedDesire/Global/NakedDesireGameMode.h" UStatsManager::UStatsManager() { @@ -45,12 +43,9 @@ void UStatsManager::SetObserved(const bool bObserved, const float CoverageWeight void UStatsManager::IncreaseEmbarrassment(const float Amount) { Embarrassment = FMath::Clamp(Embarrassment + Amount, 0, MaxEmbarrassment); + // Embarrassment-max session loss is handled by USessionManagerSubsystem, which + // subscribes to EmbarrassmentUpdate (GDD §4.4). Broadcast is the integration point. EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment); - if (Embarrassment == MaxEmbarrassment) - { - ANakedDesireGameMode* GameMode = Cast(UGameplayStatics::GetGameMode(this)); - GameMode->EndGameEmbarrassed(); - } } void UStatsManager::DecreaseEmbarrassment(const float Amount)