added session loss sequence, added energy and stamina to HUD, added home location trigger
This commit is contained in:
@@ -110,6 +110,21 @@ FText UCommissionObjective::GetDescription() const
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
|
||||
FText UCommissionObjective::GetConstraintsDescription() const
|
||||
{
|
||||
TArray<FString> Parts;
|
||||
for (const UCommissionConstraint* Constraint : Constraints)
|
||||
{
|
||||
if (!Constraint)
|
||||
continue;
|
||||
|
||||
const FString Text = Constraint->GetDescription().ToString();
|
||||
if (!Text.IsEmpty())
|
||||
Parts.Add(Text);
|
||||
}
|
||||
return FText::FromString(FString::Join(Parts, TEXT(", ")));
|
||||
}
|
||||
|
||||
float UCommissionObjective::GetProgress() const
|
||||
{
|
||||
if (bSatisfied)
|
||||
|
||||
@@ -39,6 +39,11 @@ public:
|
||||
UFUNCTION(BlueprintPure)
|
||||
virtual float GetProgress() const;
|
||||
|
||||
// Combined "while Y" text for every attached constraint (e.g. "while at Beach, during Night"),
|
||||
// or empty when the objective is unconstrained. For UI to show alongside GetDescription().
|
||||
UFUNCTION(BlueprintPure)
|
||||
FText GetConstraintsDescription() const;
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
ANakedDesireCharacter* Player = nullptr;
|
||||
|
||||
@@ -16,6 +16,7 @@ inline constexpr float DAY_START_HOUR = 8.0f; // 08:00 — day phase begins
|
||||
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 float SLEEP_TRANSITION_SECONDS = 3.0f; // real seconds to sweep the sky across the sleep skip
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "SessionManagerSubsystem.h"
|
||||
#include "LossPresentationConfig.generated.h"
|
||||
|
||||
class ULevelSequence;
|
||||
|
||||
/**
|
||||
* Data-driven cutscene-per-loss-cause map (GDD §4.4, §17.4). The USessionLossResolver
|
||||
* looks up the sequence for the resolved cause, plays it, then teleports the player home.
|
||||
* A cause with no entry skips straight to the teleport; SafeReturn never plays one (the
|
||||
* player walked home). Soft refs so cutscene assets only load when a loss actually fires.
|
||||
*/
|
||||
UCLASS()
|
||||
class NAKEDDESIRE_API ULossPresentationConfig : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Loss")
|
||||
TMap<ESessionLossCause, TSoftObjectPtr<ULevelSequence>> LossCutscenes;
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
class UStartingSaveData;
|
||||
class UCommissionBoardConfig;
|
||||
class UNPCDirectorConfig;
|
||||
class ULossPresentationConfig;
|
||||
|
||||
UCLASS()
|
||||
class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
|
||||
@@ -25,4 +26,8 @@ public:
|
||||
// Crowd population tuning the UNPCDirectorSubsystem uses (§10.2, §17.1).
|
||||
UPROPERTY(EditDefaultsOnly, Category = "NPC")
|
||||
TObjectPtr<UNPCDirectorConfig> NPCDirector;
|
||||
|
||||
// Cutscene-per-loss-cause map the USessionLossResolver plays before teleporting home (§4.4).
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Session")
|
||||
TObjectPtr<ULossPresentationConfig> LossPresentation;
|
||||
};
|
||||
@@ -3,12 +3,24 @@
|
||||
|
||||
#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);
|
||||
@@ -74,6 +86,9 @@ void USessionLossResolver::ResolveLoss(ESessionLossCause Cause)
|
||||
Autosave();
|
||||
|
||||
OnSessionLossResolved.Broadcast(Cause, bWentToHoldingCell);
|
||||
|
||||
// Presentation: play the cause's cutscene, then teleport the player home on finish.
|
||||
BeginLossPresentation(Cause);
|
||||
}
|
||||
|
||||
void USessionLossResolver::ResolveSleepLoss()
|
||||
@@ -117,6 +132,122 @@ 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())
|
||||
@@ -138,4 +269,11 @@ UGlobalSaveGameData* USessionLossResolver::GetSave() const
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ULossPresentationConfig* USessionLossResolver::GetPresentationConfig() const
|
||||
{
|
||||
const UNakedDesireGameInstance* GameInstance =
|
||||
Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
|
||||
return GameInstance ? GameInstance->LossPresentation : nullptr;
|
||||
}
|
||||
@@ -48,6 +48,11 @@ public:
|
||||
UPROPERTY(BlueprintAssignable, Category = "Session")
|
||||
FOnSessionLossResolvedSignature OnSessionLossResolved;
|
||||
|
||||
// Teleport the player pawn to the home APlayerStart (preferring one tagged "Home").
|
||||
// Called automatically when a loss cutscene finishes; exposed for debug / direct use.
|
||||
UFUNCTION(BlueprintCallable, Category = "Session")
|
||||
void TeleportPlayerHome();
|
||||
|
||||
private:
|
||||
// EnergyZero / sleep: every clothing item left outside the apartment is guaranteed lost.
|
||||
void LoseAllWorldClothing();
|
||||
@@ -55,9 +60,28 @@ private:
|
||||
// §7.7: cleared on police capture. No-op until the Wanted attribute exists (Phase 6).
|
||||
void ClearWanted();
|
||||
|
||||
// Plays the configured cutscene for the resolved cause, then teleports the player home
|
||||
// on finish. No cutscene authored → teleports immediately. SafeReturn → no presentation.
|
||||
void BeginLossPresentation(ESessionLossCause Cause);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleLossCutsceneFinished();
|
||||
|
||||
void Autosave() const;
|
||||
class UGlobalSaveGameData* GetSave() const;
|
||||
class ULossPresentationConfig* GetPresentationConfig() const;
|
||||
|
||||
// §21 tuning placeholder — the police-capture money penalty.
|
||||
float PoliceCaptureMoneyPenalty = 200.0f;
|
||||
|
||||
// PlayerStart tag of the home spawn, preferred over an untagged start (GDD §4.4).
|
||||
static const FName HomePlayerStartTag;
|
||||
|
||||
// The level-sequence player/actor for the in-flight loss cutscene. Held so they survive
|
||||
// GC until OnFinished fires; torn down in HandleLossCutsceneFinished.
|
||||
UPROPERTY()
|
||||
TObjectPtr<class ULevelSequencePlayer> ActiveLossSequencePlayer;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<class ALevelSequenceActor> ActiveLossSequenceActor;
|
||||
};
|
||||
@@ -31,7 +31,18 @@ void UTimeOfDaySubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||
|
||||
void UTimeOfDaySubsystem::Tick(float DeltaTime)
|
||||
{
|
||||
if (!bBegunPlay || IsPaused())
|
||||
if (!bBegunPlay)
|
||||
return;
|
||||
|
||||
// The sleep sweep owns the clock while it runs — it drives AdvanceClock itself, so
|
||||
// skip the normal real-time advancement (and ignore pause, which sleep doesn't honor).
|
||||
if (bSleeping)
|
||||
{
|
||||
TickSleep(DeltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsPaused())
|
||||
return;
|
||||
|
||||
AdvanceClock(static_cast<double>(DeltaTime) * INGAME_MINUTES_PER_REAL_SECOND);
|
||||
@@ -91,7 +102,41 @@ void UTimeOfDaySubsystem::SkipToNextMorning()
|
||||
|
||||
void UTimeOfDaySubsystem::Sleep()
|
||||
{
|
||||
SkipTime(SLEEP_DURATION_HOURS * MINUTES_PER_HOUR);
|
||||
if (bSleeping)
|
||||
return; // already sweeping; ignore re-entrant interacts
|
||||
|
||||
// Kick off the animated sweep — Tick advances the clock a slice at a time so the sky
|
||||
// sun/lighting interpolate across the 8 hours instead of snapping. Energy restore and
|
||||
// autosave are deferred to FinishSleep() so they land on the final time, not the start.
|
||||
const double TotalMinutes = SLEEP_DURATION_HOURS * MINUTES_PER_HOUR;
|
||||
SleepMinutesRemaining = TotalMinutes;
|
||||
SleepMinutesPerRealSecond = TotalMinutes / FMath::Max(SLEEP_TRANSITION_SECONDS, KINDA_SMALL_NUMBER);
|
||||
bSleeping = true;
|
||||
}
|
||||
|
||||
void UTimeOfDaySubsystem::TickSleep(float DeltaTime)
|
||||
{
|
||||
// Clamp the final slice so we land exactly on +8h rather than overshooting.
|
||||
double Step = SleepMinutesPerRealSecond * static_cast<double>(DeltaTime);
|
||||
if (Step >= SleepMinutesRemaining)
|
||||
Step = SleepMinutesRemaining;
|
||||
|
||||
SleepMinutesRemaining -= Step;
|
||||
// Throttled push (bForceSkyPush=false) keeps the sky at the smooth 30fps cadence; the
|
||||
// per-hour boundaries (phase flip, day-roll, rent) still fire as each hour is crossed.
|
||||
AdvanceClock(Step);
|
||||
|
||||
if (SleepMinutesRemaining <= 0.0)
|
||||
FinishSleep();
|
||||
}
|
||||
|
||||
void UTimeOfDaySubsystem::FinishSleep()
|
||||
{
|
||||
bSleeping = false;
|
||||
SleepMinutesRemaining = 0.0;
|
||||
SleepMinutesPerRealSecond = 0.0;
|
||||
|
||||
PushTimeToSky(/*bForce=*/true); // snap the sky to the exact final time
|
||||
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.
|
||||
|
||||
@@ -90,9 +90,15 @@ public:
|
||||
|
||||
// §2.4 sleep: fast-forward 8 hours, restore energy, autosave. (The apartment bed
|
||||
// also routes outside-clothing loss through USessionLossResolver — see §4.4.)
|
||||
// The skip is animated over SLEEP_TRANSITION_SECONDS — the clock advances a little
|
||||
// each frame so the sky sweeps smoothly; energy/autosave land when the sweep finishes.
|
||||
UFUNCTION(BlueprintCallable, Category = "Time")
|
||||
void Sleep();
|
||||
|
||||
// True while the sleep time-lapse is running (BP can hold a dim overlay / lock input).
|
||||
UFUNCTION(BlueprintPure, Category = "Time")
|
||||
bool IsSleeping() const { return bSleeping; }
|
||||
|
||||
// --- 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")
|
||||
@@ -124,6 +130,9 @@ private:
|
||||
// bForceSkyPush bypasses the 30fps throttle for discrete jumps (skips / load),
|
||||
// so the sky snaps to the new time immediately instead of waiting a frame.
|
||||
void AdvanceClock(double DeltaMinutes, bool bForceSkyPush = false);
|
||||
// Per-frame driver for the animated sleep sweep; finalizes when the budget is spent.
|
||||
void TickSleep(float DeltaTime);
|
||||
void FinishSleep();
|
||||
void HandleHourBoundary(int32 HourOfDay); // 0–23
|
||||
void SetPhase(EDayPhase NewPhase);
|
||||
void AdvanceCalendarDay();
|
||||
@@ -142,4 +151,10 @@ private:
|
||||
EDayPhase CurrentPhase = EDayPhase::Day;
|
||||
TSet<FName> PauseReasons;
|
||||
bool bBegunPlay = false;
|
||||
|
||||
// Animated sleep state. While bSleeping, Tick drives the clock at SleepMinutesPerRealSecond
|
||||
// instead of the normal rate, advancing SleepMinutesRemaining in-game minutes total.
|
||||
bool bSleeping = false;
|
||||
double SleepMinutesRemaining = 0.0;
|
||||
double SleepMinutesPerRealSecond = 0.0;
|
||||
};
|
||||
@@ -7,9 +7,6 @@
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "LocationData.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class NAKEDDESIRE_API ULocationData : public UPrimaryDataAsset
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
|
||||
#include "LocationTrigger.h"
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "LocationSubsystem.h"
|
||||
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
|
||||
ALocationTrigger::ALocationTrigger()
|
||||
@@ -27,16 +28,26 @@ void ALocationTrigger::BeginPlay()
|
||||
|
||||
BoxTrigger->OnComponentBeginOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerBeginOverlap);
|
||||
BoxTrigger->OnComponentEndOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerEndOverlap);
|
||||
|
||||
// Defer to next tick so the player pawn is spawned and positioned and physics overlaps
|
||||
// have been computed before we check whether it started out inside this volume.
|
||||
GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ALocationTrigger::SeedInitialPlayerOverlap);
|
||||
}
|
||||
|
||||
void ALocationTrigger::OnConstruction(const FTransform& Transform)
|
||||
{
|
||||
Super::OnConstruction(Transform);
|
||||
|
||||
BoxTrigger->SetBoxExtent(TriggerSize);
|
||||
}
|
||||
|
||||
void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
|
||||
return;
|
||||
|
||||
if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem<ULocationSubsystem>())
|
||||
Locations->EnterLocation(LocationData);
|
||||
SetPlayerInside(true);
|
||||
}
|
||||
|
||||
void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
@@ -45,6 +56,39 @@ void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedCompon
|
||||
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
|
||||
return;
|
||||
|
||||
// Another of the player's components may still overlap the box (capsule vs. mesh); only
|
||||
// report the exit once the player has fully left, so we fire exactly one Exit per Enter.
|
||||
if (IsPlayerOverlapping())
|
||||
return;
|
||||
|
||||
SetPlayerInside(false);
|
||||
}
|
||||
|
||||
void ALocationTrigger::SeedInitialPlayerOverlap()
|
||||
{
|
||||
if (IsPlayerOverlapping())
|
||||
SetPlayerInside(true);
|
||||
}
|
||||
|
||||
void ALocationTrigger::SetPlayerInside(const bool bInside)
|
||||
{
|
||||
if (bInside == bPlayerInside)
|
||||
return;
|
||||
|
||||
bPlayerInside = bInside;
|
||||
|
||||
if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem<ULocationSubsystem>())
|
||||
Locations->ExitLocation(LocationData);
|
||||
{
|
||||
if (bInside)
|
||||
Locations->EnterLocation(LocationData);
|
||||
else
|
||||
Locations->ExitLocation(LocationData);
|
||||
}
|
||||
}
|
||||
|
||||
bool ALocationTrigger::IsPlayerOverlapping() const
|
||||
{
|
||||
TArray<AActor*> Overlapping;
|
||||
BoxTrigger->GetOverlappingActors(Overlapping, ANakedDesireCharacter::StaticClass());
|
||||
return Overlapping.Num() > 0;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -22,10 +22,15 @@ class NAKEDDESIRE_API ALocationTrigger : public AActor
|
||||
|
||||
UPROPERTY(EditAnywhere)
|
||||
ULocationData* LocationData;
|
||||
|
||||
UPROPERTY(EditAnywhere)
|
||||
FVector TriggerSize;
|
||||
|
||||
public:
|
||||
ALocationTrigger();
|
||||
|
||||
virtual void OnConstruction(const FTransform& Transform) override;
|
||||
|
||||
ULocationData* GetLocationData() const;
|
||||
|
||||
protected:
|
||||
@@ -39,4 +44,16 @@ private:
|
||||
UFUNCTION()
|
||||
void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
|
||||
|
||||
// A pawn that spawns already inside this volume (e.g. the apartment at game start) gets no
|
||||
// begin-overlap event, so seed the state one tick after BeginPlay once the world settles.
|
||||
void SeedInitialPlayerOverlap();
|
||||
|
||||
// Forwards a single enter/exit to ULocationSubsystem on a real transition. Idempotent so the
|
||||
// seed above can't double-count against a begin-overlap the engine does deliver.
|
||||
void SetPlayerInside(bool bInside);
|
||||
|
||||
bool IsPlayerOverlapping() const;
|
||||
|
||||
bool bPlayerInside = false;
|
||||
};
|
||||
@@ -11,7 +11,7 @@ public class NakedDesire : ModuleRules
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "CommonUI", "NavigationSystem",
|
||||
"AIModule", "GameplayTags", "Slate", "SlateCore"
|
||||
"AIModule", "GameplayTags", "Slate", "SlateCore", "LevelSequence", "MovieScene"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,21 @@ void UHUDWidget::NativeConstruct()
|
||||
return;
|
||||
|
||||
Player->StatsManager->EmbarrassmentUpdate.AddUniqueDynamic(this, &UHUDWidget::OnEmbarrassmentUpdated);
|
||||
Player->StatsManager->EnergyUpdate.AddUniqueDynamic(this, &UHUDWidget::OnEnergyUpdated);
|
||||
Player->StatsManager->StaminaUpdate.AddUniqueDynamic(this, &UHUDWidget::OnStaminaUpdated);
|
||||
}
|
||||
|
||||
void UHUDWidget::OnEmbarrassmentUpdated(float CurrentValue, float MaxValue)
|
||||
{
|
||||
EmbarrassmentBar->SetPercent(CurrentValue / MaxValue);
|
||||
}
|
||||
|
||||
void UHUDWidget::OnEnergyUpdated(float CurrentValue, float MaxValue)
|
||||
{
|
||||
EnergyBar->SetPercent(CurrentValue / MaxValue);
|
||||
}
|
||||
|
||||
void UHUDWidget::OnStaminaUpdated(float CurrentValue, float MaxValue)
|
||||
{
|
||||
StaminaBar->SetPercent(CurrentValue / MaxValue);
|
||||
}
|
||||
|
||||
@@ -16,10 +16,22 @@ class NAKEDDESIRE_API UHUDWidget : public UCommonUserWidget
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UProgressBar> EmbarrassmentBar;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UProgressBar> EnergyBar;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UProgressBar> StaminaBar;
|
||||
|
||||
protected:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void OnEmbarrassmentUpdated(float CurrentValue, float MaxValue);
|
||||
|
||||
UFUNCTION()
|
||||
void OnEnergyUpdated(float CurrentValue, float MaxValue);
|
||||
|
||||
UFUNCTION()
|
||||
void OnStaminaUpdated(float CurrentValue, float MaxValue);
|
||||
};
|
||||
|
||||
@@ -104,8 +104,17 @@ FString UForumCommissionWidget::BuildObjectivesString() const
|
||||
if (!Result.IsEmpty())
|
||||
Result += LINE_TERMINATOR;
|
||||
|
||||
FString Line = FString::Printf(TEXT("• %s"), *Objective->GetDescription().ToString());
|
||||
|
||||
// Append any "while Y" constraint text so the row reads "Be fully naked while at Beach (50%)".
|
||||
const FString Constraints = Objective->GetConstraintsDescription().ToString();
|
||||
if (!Constraints.IsEmpty())
|
||||
Line += FString::Printf(TEXT(" %s"), *Constraints);
|
||||
|
||||
const int32 Pct = FMath::RoundToInt(Objective->GetProgress() * 100.0f);
|
||||
Result += FString::Printf(TEXT("• %s (%d%%)"), *Objective->GetDescription().ToString(), Pct);
|
||||
Line += FString::Printf(TEXT(" (%d%%)"), Pct);
|
||||
|
||||
Result += Line;
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user