279 lines
8.4 KiB
C++
279 lines
8.4 KiB
C++
// © 2025 Naked People Team. All Rights Reserved.
|
|
|
|
|
|
#include "SessionLossResolver.h"
|
|
|
|
#include "DefaultLevelSequenceInstanceData.h"
|
|
#include "LevelSequence.h"
|
|
#include "LevelSequenceActor.h"
|
|
#include "LevelSequencePlayer.h"
|
|
#include "LossPresentationConfig.h"
|
|
#include "MovieSceneSequencePlaybackSettings.h"
|
|
#include "NakedDesireGameInstance.h"
|
|
#include "GameFramework/Character.h"
|
|
#include "GameFramework/CharacterMovementComponent.h"
|
|
#include "GameFramework/PlayerStart.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"
|
|
|
|
const FName USessionLossResolver::HomePlayerStartTag = FName(TEXT("Home"));
|
|
|
|
void USessionLossResolver::OnWorldBeginPlay(UWorld& InWorld)
|
|
{
|
|
Super::OnWorldBeginPlay(InWorld);
|
|
|
|
if (USessionManagerSubsystem* SessionManager = InWorld.GetSubsystem<USessionManagerSubsystem>())
|
|
{
|
|
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<USessionManagerSubsystem>())
|
|
{
|
|
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);
|
|
|
|
// Presentation: play the cause's cutscene, then teleport the player home on finish.
|
|
BeginLossPresentation(Cause);
|
|
}
|
|
|
|
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<AActor*> Pickups;
|
|
UGameplayStatics::GetAllActorsOfClass(this, AItemPickup::StaticClass(), Pickups);
|
|
for (AActor* Actor : Pickups)
|
|
{
|
|
AItemPickup* Pickup = Cast<AItemPickup>(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::BeginLossPresentation(ESessionLossCause Cause)
|
|
{
|
|
// SafeReturn = the player walked home under their own power; nothing to present.
|
|
if (Cause == ESessionLossCause::SafeReturn)
|
|
return;
|
|
|
|
ULevelSequence* Sequence = nullptr;
|
|
if (const ULossPresentationConfig* Config = GetPresentationConfig())
|
|
{
|
|
if (const TSoftObjectPtr<ULevelSequence>* Found = Config->LossCutscenes.Find(Cause))
|
|
{
|
|
// Synchronous load is acceptable here: the loss has already resolved and the
|
|
// player is stationary, so the brief hitch is hidden by the transition.
|
|
Sequence = Found->LoadSynchronous();
|
|
}
|
|
}
|
|
|
|
if (!Sequence)
|
|
{
|
|
// No cutscene authored for this cause — go straight home.
|
|
TeleportPlayerHome();
|
|
return;
|
|
}
|
|
|
|
// Lock the player out of movement / look while the cutscene owns the view; state is
|
|
// restored when the sequence finishes (just before we teleport home).
|
|
FMovieSceneSequencePlaybackSettings Settings;
|
|
Settings.bDisableMovementInput = true;
|
|
Settings.bDisableLookAtInput = true;
|
|
// Restore track state on finish so the view/player return to normal before we teleport.
|
|
Settings.FinishCompletionStateOverride = EMovieSceneCompletionModeOverride::ForceRestoreState;
|
|
|
|
ALevelSequenceActor* SequenceActor = nullptr;
|
|
ActiveLossSequencePlayer = ULevelSequencePlayer::CreateLevelSequencePlayer(this, Sequence, Settings, SequenceActor);
|
|
ActiveLossSequenceActor = SequenceActor;
|
|
ActiveLossSequenceActor->bOverrideInstanceData = true;
|
|
UDefaultLevelSequenceInstanceData* InstanceData = Cast<UDefaultLevelSequenceInstanceData>(ActiveLossSequenceActor->DefaultInstanceData.Get());
|
|
InstanceData->TransformOriginActor = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
|
|
|
|
if (!ActiveLossSequencePlayer)
|
|
{
|
|
// Playback couldn't be created — don't strand the player outside.
|
|
TeleportPlayerHome();
|
|
return;
|
|
}
|
|
|
|
ActiveLossSequencePlayer->OnFinished.AddDynamic(this, &USessionLossResolver::HandleLossCutsceneFinished);
|
|
ActiveLossSequencePlayer->Play();
|
|
}
|
|
|
|
void USessionLossResolver::HandleLossCutsceneFinished()
|
|
{
|
|
if (ActiveLossSequencePlayer)
|
|
{
|
|
ActiveLossSequencePlayer->OnFinished.RemoveDynamic(this, &USessionLossResolver::HandleLossCutsceneFinished);
|
|
}
|
|
|
|
TeleportPlayerHome();
|
|
|
|
// Tear down the spawned sequence player/actor now that the cutscene is done.
|
|
if (ActiveLossSequenceActor)
|
|
{
|
|
ActiveLossSequenceActor->Destroy();
|
|
}
|
|
ActiveLossSequenceActor = nullptr;
|
|
ActiveLossSequencePlayer = nullptr;
|
|
}
|
|
|
|
void USessionLossResolver::TeleportPlayerHome()
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World)
|
|
return;
|
|
|
|
APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(World, 0);
|
|
if (!PlayerPawn)
|
|
return;
|
|
|
|
// Prefer a PlayerStart tagged "Home"; fall back to the first one in the level.
|
|
TArray<AActor*> Starts;
|
|
UGameplayStatics::GetAllActorsOfClass(World, APlayerStart::StaticClass(), Starts);
|
|
|
|
APlayerStart* Home = nullptr;
|
|
for (AActor* Actor : Starts)
|
|
{
|
|
APlayerStart* Start = Cast<APlayerStart>(Actor);
|
|
if (!Start)
|
|
continue;
|
|
|
|
if (!Home)
|
|
Home = Start;
|
|
if (Start->PlayerStartTag == HomePlayerStartTag)
|
|
{
|
|
Home = Start;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!Home)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionLossResolver: no APlayerStart found; cannot teleport player home."));
|
|
return;
|
|
}
|
|
|
|
PlayerPawn->TeleportTo(Home->GetActorLocation(), Home->GetActorRotation());
|
|
|
|
// Kill residual velocity so the character doesn't slide on arrival.
|
|
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
|
{
|
|
if (UCharacterMovementComponent* Movement = Character->GetCharacterMovement())
|
|
{
|
|
Movement->StopMovementImmediately();
|
|
}
|
|
}
|
|
}
|
|
|
|
void USessionLossResolver::Autosave() const
|
|
{
|
|
if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance())
|
|
{
|
|
if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
|
|
{
|
|
SaveSubsystem->SaveGame();
|
|
}
|
|
}
|
|
}
|
|
|
|
UGlobalSaveGameData* USessionLossResolver::GetSave() const
|
|
{
|
|
if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance())
|
|
{
|
|
if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
|
|
{
|
|
return SaveSubsystem->GetCurrentSave();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
ULossPresentationConfig* USessionLossResolver::GetPresentationConfig() const
|
|
{
|
|
const UNakedDesireGameInstance* GameInstance =
|
|
Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
|
|
return GameInstance ? GameInstance->LossPresentation : nullptr;
|
|
} |