// © 2025 Naked People Team. All Rights Reserved. #include "StatsManager.h" #include "NakedDesire/Player/NakedDesireCharacter.h" namespace { // Max raw exposure one observer can read at once: Boobs + Ass + Genitals fully exposed, equal // weights (GDD §7.1 — per-part weighting is uniform for now). Normalizes per-observer exposure // to [0,1] so EmbarrassmentGainRate reads as "gain/sec at full exposure, single close observer". constexpr float MaxObservedExposure = 3.0f; } UStatsManager::UStatsManager() { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.TickInterval = 1.0f; } void UStatsManager::BeginPlay() { Super::BeginPlay(); OwnerCharacter = Cast(GetOwner()); EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment); StaminaUpdate.Broadcast(Stamina, MaxStamina); EnergyUpdate.Broadcast(Energy, MaxEnergy); } void UStatsManager::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { DecreaseEnergy(0.9f); const float ExposureRate = ComputeObservedExposureRate(); if (ExposureRate > 0.0f) IncreaseEmbarrassment(EmbarrassmentGainRate * ExposureRate * DeltaTime); else DecreaseEmbarrassment(EmbarrassmentDecayRate * DeltaTime); } float UStatsManager::ComputeObservedExposureRate() { if (!OwnerCharacter) return 0.0f; // Sum each active observer's normalized exposure, re-traced live so redressing / exposing // updates the rate immediately even while the observer set is unchanged. float SumNormalizedExposure = 0.0f; int32 ActiveObservers = 0; for (int32 i = Observers.Num() - 1; i >= 0; --i) { AActor* Observer = Observers[i].Get(); if (!Observer) { Observers.RemoveAtSwap(i); continue; } const float Exposure = OwnerCharacter->GetObservedExposureFrom(Observer->GetActorLocation(), Observer); if (Exposure > 0.0f) { SumNormalizedExposure += Exposure / MaxObservedExposure; ++ActiveObservers; } } if (ActiveObservers == 0) return 0.0f; // Mean per-observer exposure, then a saturating crowd multiplier (diminishing returns). const float MeanExposure = SumNormalizedExposure / ActiveObservers; const float DensityMultiplier = 1.0f + ObserverDensityScale * FMath::Loge(static_cast(ActiveObservers)); return MeanExposure * DensityMultiplier; } void UStatsManager::Init(UClothingManager* InClothingManager) { ClothingManager = InClothingManager; } void UStatsManager::SetObserved(const bool bObserved, AActor* Observer) { if (!Observer) return; bool bChanged = false; if (bObserved) { if (!Observers.Contains(Observer)) { Observers.Add(Observer); bChanged = true; } } else { bChanged = Observers.Remove(Observer) > 0; } if (bChanged) OnObserversChanged.Broadcast(); } int32 UStatsManager::GetObserverCount() const { int32 Count = 0; for (const TWeakObjectPtr& Observer : Observers) { if (Observer.IsValid()) ++Count; } return Count; } 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); } void UStatsManager::DecreaseEmbarrassment(const float Amount) { Embarrassment = FMath::Clamp(Embarrassment - Amount, 0, MaxEmbarrassment); EmbarrassmentUpdate.Broadcast(Embarrassment, MaxEmbarrassment); } void UStatsManager::DecreaseStamina(const float Amount) { Stamina = FMath::Clamp(Stamina - Amount, 0, MaxStamina); StaminaUpdate.Broadcast(Stamina, MaxStamina); } void UStatsManager::IncreaseStamina(const float Amount) { Stamina = FMath::Clamp(Stamina + Amount, 0, MaxStamina); StaminaUpdate.Broadcast(Stamina, MaxStamina); } void UStatsManager::DecreaseEnergy(const float Amount) { Energy = FMath::Clamp(Energy - Amount, 0, MaxEnergy); EnergyUpdate.Broadcast(Energy, MaxEnergy); } void UStatsManager::RestoreEnergy() { Energy = MaxEnergy; EnergyUpdate.Broadcast(Energy, MaxEnergy); }