// © 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::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(); // 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; }