Setup player preview for equipment panel
This commit is contained in:
@@ -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>();
|
||||
for (const FItemSaveRecord& Item : SaveSubsystem->GetCurrentSave()->GetWorldItems())
|
||||
SpawnSavedWorldItems(SaveSubsystem->GetCurrentSave());
|
||||
}
|
||||
|
||||
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 0–23; 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:00–20: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); // 0–23
|
||||
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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -15,10 +15,3 @@ ANPC::ANPC()
|
||||
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
|
||||
}
|
||||
|
||||
void ANPC::Destroyed()
|
||||
{
|
||||
Super::Destroyed();
|
||||
|
||||
OnDestroyed.Broadcast(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,9 +45,18 @@ 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)
|
||||
TArray<FItemSaveRecord> WardrobeItems;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user