// Copyright Pacificao. All Rights Reserved. #include "AgrarianSurvivalComponent.h" #include "AgrarianGameState.h" #include "AgrarianPerformanceStats.h" #include "AgrarianShelterActor.h" #include "AgrarianWeatherExposureZone.h" #include "Components/BoxComponent.h" #include "Engine/World.h" #include "Net/UnrealNetwork.h" #include "ProfilingDebugging/CpuProfilerTrace.h" UAgrarianSurvivalComponent::UAgrarianSurvivalComponent() { PrimaryComponentTick.bCanEverTick = true; SetIsReplicatedByDefault(true); } void UAgrarianSurvivalComponent::BeginPlay() { Super::BeginPlay(); ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { SCOPE_CYCLE_COUNTER(STAT_AgrarianSurvivalTick); TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianSurvivalTick); Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!GetOwner() || !GetOwner()->HasAuthority() || !IsAlive()) { return; } const float Minutes = DeltaTime / 60.0f; Survival.Hunger -= HungerDecayPerMinute * Minutes; Survival.Thirst -= ThirstDecayPerMinute * Minutes; Survival.Stamina += StaminaRecoveryPerSecond * DeltaTime; CurrentWeatherProtection = CalculateCurrentWeatherProtection(); CurrentWeatherExposureMultiplier = CalculateCurrentWeatherExposureMultiplier(); CurrentWeatherTemperatureOffsetC = CalculateCurrentWeatherTemperatureOffsetC(); CareHistory.ShelterQuality = FMath::FInterpTo(CareHistory.ShelterQuality, CurrentWeatherProtection, DeltaTime, 0.02f); if (Survival.Stamina <= LowStaminaExhaustionThreshold) { Survival.Exhaustion += ExhaustionGainPerLowStaminaSecond * DeltaTime; } else if (Survival.Hunger > 10.0f && Survival.Thirst > 10.0f && Survival.BodyTemperature >= 35.0f) { Survival.Exhaustion -= ExhaustionRecoveryPerSecond * DeltaTime; } if (Survival.SicknessSeverity > 0.0f) { Survival.Exhaustion += (Survival.SicknessSeverity / 100.0f) * 0.08f * DeltaTime; CareHistory.IllnessBurden += (Survival.SicknessSeverity / 100.0f) * 0.001f * DeltaTime; if (Survival.Hunger > 20.0f && Survival.Thirst > 20.0f && Survival.BodyTemperature >= 35.0f) { Survival.SicknessSeverity -= SicknessRecoveryPerSecond * DeltaTime * FMath::Max(0.25f, CareHistory.TreatmentQuality); } } if (Survival.BleedingSeverity > 0.0f) { const float BleedingRatio = Survival.BleedingSeverity / 100.0f; Survival.Health -= BleedingDamagePerMinute * BleedingRatio * Minutes; Survival.Exhaustion += BleedingExhaustionPerSecond * BleedingRatio * DeltaTime; CareHistory.InjuryBurden += BleedingRatio * 0.001f * DeltaTime; } if (Survival.SprainSeverity > 0.0f) { const float SprainRatio = Survival.SprainSeverity / 100.0f; Survival.Exhaustion += SprainExhaustionPerSecond * SprainRatio * DeltaTime; CareHistory.InjuryBurden += SprainRatio * 0.0005f * DeltaTime; } if (const UWorld* World = GetWorld()) { if (const AAgrarianGameState* AgrarianGameState = World->GetGameState()) { const float ExposureProtectionMultiplier = 1.0f - FMath::Clamp(CurrentWeatherProtection, 0.0f, 1.0f); const float EffectiveAmbientTemperatureC = AgrarianGameState->AmbientTemperatureC + CurrentWeatherTemperatureOffsetC; const float ExposureDelta = (EffectiveAmbientTemperatureC - 18.0f) * 0.002f * DeltaTime * ExposureProtectionMultiplier * CurrentWeatherExposureMultiplier; Survival.BodyTemperature += FMath::Clamp(ExposureDelta, -0.035f, 0.02f); } } if (Survival.Hunger <= 0.0f) { Survival.Health -= StarvationDamagePerMinute * Minutes; } if (Survival.Thirst <= 0.0f) { Survival.Health -= DehydrationDamagePerMinute * Minutes; } if (Survival.BodyTemperature < 35.0f) { Survival.Health -= ColdDamagePerMinute * Minutes * (1.0f - FMath::Clamp(CurrentWeatherProtection, 0.0f, 1.0f)) * CurrentWeatherExposureMultiplier; } if (Survival.SicknessSeverity >= 60.0f) { Survival.Health -= SicknessDamagePerMinute * (Survival.SicknessSeverity / 100.0f) * Minutes; } ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } void UAgrarianSurvivalComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(UAgrarianSurvivalComponent, Survival); DOREPLIFETIME(UAgrarianSurvivalComponent, CareHistory); DOREPLIFETIME(UAgrarianSurvivalComponent, CurrentWeatherProtection); DOREPLIFETIME(UAgrarianSurvivalComponent, CurrentWeatherExposureMultiplier); DOREPLIFETIME(UAgrarianSurvivalComponent, CurrentWeatherTemperatureOffsetC); } bool UAgrarianSurvivalComponent::IsAlive() const { return !Survival.bIsDead && Survival.Health > 0.0f; } bool UAgrarianSurvivalComponent::IsDead() const { return Survival.bIsDead || Survival.Health <= 0.0f; } void UAgrarianSurvivalComponent::ApplyDamage(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.Health -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::RestoreHealth(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { if (Survival.bIsDead) { return; } Survival.Health += FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::MarkDead(FName Reason) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.Health = 0.0f; Survival.Stamina = 0.0f; Survival.bIsDead = true; Survival.LastDeathReason = Reason.IsNone() ? FName(TEXT("unknown")) : Reason; BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::Revive(float HealthAmount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.bIsDead = false; Survival.LastDeathReason = NAME_None; Survival.Health = FMath::Clamp(HealthAmount, 1.0f, 100.0f); Survival.Stamina = FMath::Max(Survival.Stamina, 25.0f); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddFood(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.Hunger += FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddWater(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.Thirst += FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddWarmth(float DegreesCelsius) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.BodyTemperature += DegreesCelsius; ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddInjury(float Severity) { if (GetOwner() && GetOwner()->HasAuthority()) { const float PositiveSeverity = FMath::Max(0.0f, Severity); Survival.InjurySeverity += PositiveSeverity; Survival.BleedingSeverity += PositiveSeverity * 0.35f; Survival.SprainSeverity += PositiveSeverity * 0.20f; CareHistory.InjuryBurden += PositiveSeverity / 100.0f; Survival.Health -= PositiveSeverity * 5.0f; ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::ReduceInjury(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.InjurySeverity -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddBleeding(float Severity) { if (GetOwner() && GetOwner()->HasAuthority()) { const float PositiveSeverity = FMath::Max(0.0f, Severity); Survival.BleedingSeverity += PositiveSeverity; CareHistory.InjuryBurden += PositiveSeverity / 200.0f; ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::ReduceBleeding(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.BleedingSeverity -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddSprain(float Severity) { if (GetOwner() && GetOwner()->HasAuthority()) { const float PositiveSeverity = FMath::Max(0.0f, Severity); Survival.SprainSeverity += PositiveSeverity; CareHistory.InjuryBurden += PositiveSeverity / 200.0f; ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::ReduceSprain(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.SprainSeverity -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddSickness(float Severity) { if (GetOwner() && GetOwner()->HasAuthority()) { const float PositiveSeverity = FMath::Max(0.0f, Severity); Survival.SicknessSeverity += PositiveSeverity; CareHistory.IllnessBurden += PositiveSeverity / 100.0f; ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::ReduceSickness(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.SicknessSeverity -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::ApplySavedState(const FAgrarianSurvivalSnapshot& SavedSurvival, const FAgrarianCareHistorySnapshot& SavedCareHistory) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival = SavedSurvival; CareHistory = SavedCareHistory; ClampSurvival(); ClampCareHistory(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::SpendStamina(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { const float PositiveAmount = FMath::Max(0.0f, Amount); Survival.Stamina -= PositiveAmount; Survival.Exhaustion += PositiveAmount * 0.05f; ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::AddExhaustion(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.Exhaustion += FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } void UAgrarianSurvivalComponent::ReduceExhaustion(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { Survival.Exhaustion -= FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } float UAgrarianSurvivalComponent::CalculateCurrentWeatherProtection() const { const AActor* Owner = GetOwner(); if (!Owner) { return 0.0f; } TArray OverlappingShelterActors; Owner->GetOverlappingActors(OverlappingShelterActors, AAgrarianShelterActor::StaticClass()); float BestProtection = 0.0f; for (const AActor* Actor : OverlappingShelterActors) { const AAgrarianShelterActor* Shelter = Cast(Actor); if (!Shelter || !Shelter->ProtectionVolume || !Shelter->ProtectionVolume->IsOverlappingActor(Owner)) { continue; } BestProtection = FMath::Max(BestProtection, FMath::Clamp(Shelter->WeatherProtection, 0.0f, 1.0f)); } return BestProtection; } float UAgrarianSurvivalComponent::CalculateCurrentWeatherExposureMultiplier() const { const AActor* Owner = GetOwner(); if (!Owner) { return 1.0f; } TArray OverlappingZoneActors; Owner->GetOverlappingActors(OverlappingZoneActors, AAgrarianWeatherExposureZone::StaticClass()); float StrongestMultiplierDelta = 0.0f; for (const AActor* Actor : OverlappingZoneActors) { const AAgrarianWeatherExposureZone* Zone = Cast(Actor); if (!Zone || !Zone->ExposureVolume || !Zone->ExposureVolume->IsOverlappingActor(Owner)) { continue; } const float ZoneDelta = FMath::Clamp(Zone->ExposureMultiplier, 0.0f, 3.0f) - 1.0f; if (FMath::Abs(ZoneDelta) > FMath::Abs(StrongestMultiplierDelta)) { StrongestMultiplierDelta = ZoneDelta; } } return FMath::Clamp(1.0f + StrongestMultiplierDelta, 0.0f, 3.0f); } float UAgrarianSurvivalComponent::CalculateCurrentWeatherTemperatureOffsetC() const { const AActor* Owner = GetOwner(); if (!Owner) { return 0.0f; } TArray OverlappingZoneActors; Owner->GetOverlappingActors(OverlappingZoneActors, AAgrarianWeatherExposureZone::StaticClass()); float StrongestOffset = 0.0f; for (const AActor* Actor : OverlappingZoneActors) { const AAgrarianWeatherExposureZone* Zone = Cast(Actor); if (!Zone || !Zone->ExposureVolume || !Zone->ExposureVolume->IsOverlappingActor(Owner)) { continue; } const float ZoneOffset = FMath::Clamp(Zone->TemperatureOffsetC, -20.0f, 20.0f); if (FMath::Abs(ZoneOffset) > FMath::Abs(StrongestOffset)) { StrongestOffset = ZoneOffset; } } return StrongestOffset; } void UAgrarianSurvivalComponent::OnRep_Survival() { BroadcastSurvivalChanged(); } void UAgrarianSurvivalComponent::OnRep_CareHistory() { ClampCareHistory(); BroadcastSurvivalChanged(); } void UAgrarianSurvivalComponent::ClampSurvival() { Survival.Health = FMath::Clamp(Survival.Health, 0.0f, 100.0f); Survival.Stamina = FMath::Clamp(Survival.Stamina, 0.0f, 100.0f); Survival.Exhaustion = FMath::Clamp(Survival.Exhaustion, 0.0f, 100.0f); Survival.Hunger = FMath::Clamp(Survival.Hunger, 0.0f, 100.0f); Survival.Thirst = FMath::Clamp(Survival.Thirst, 0.0f, 100.0f); Survival.BodyTemperature = FMath::Clamp(Survival.BodyTemperature, 30.0f, 42.0f); Survival.InjurySeverity = FMath::Clamp(Survival.InjurySeverity, 0.0f, 100.0f); Survival.BleedingSeverity = FMath::Clamp(Survival.BleedingSeverity, 0.0f, 100.0f); Survival.SprainSeverity = FMath::Clamp(Survival.SprainSeverity, 0.0f, 100.0f); Survival.SicknessSeverity = FMath::Clamp(Survival.SicknessSeverity, 0.0f, 100.0f); UpdateDeathState(); } void UAgrarianSurvivalComponent::ClampCareHistory() { CareHistory.NutritionQuality = FMath::Clamp(CareHistory.NutritionQuality, 0.0f, 1.0f); CareHistory.IllnessBurden = FMath::Clamp(CareHistory.IllnessBurden, 0.0f, 1.0f); CareHistory.InjuryBurden = FMath::Clamp(CareHistory.InjuryBurden, 0.0f, 1.0f); CareHistory.SleepQuality = FMath::Clamp(CareHistory.SleepQuality, 0.0f, 1.0f); CareHistory.ShelterQuality = FMath::Clamp(CareHistory.ShelterQuality, 0.0f, 1.0f); CareHistory.StressBurden = FMath::Clamp(CareHistory.StressBurden, 0.0f, 1.0f); CareHistory.WorkloadBurden = FMath::Clamp(CareHistory.WorkloadBurden, 0.0f, 1.0f); CareHistory.TreatmentQuality = FMath::Clamp(CareHistory.TreatmentQuality, 0.0f, 1.0f); } void UAgrarianSurvivalComponent::BroadcastSurvivalChanged() { OnSurvivalChanged.Broadcast(Survival); if (bLastBroadcastDeathState != Survival.bIsDead || LastBroadcastDeathReason != Survival.LastDeathReason) { bLastBroadcastDeathState = Survival.bIsDead; LastBroadcastDeathReason = Survival.LastDeathReason; OnDeathStateChanged.Broadcast(Survival.bIsDead, Survival.LastDeathReason); } } void UAgrarianSurvivalComponent::UpdateDeathState() { if (Survival.Health <= 0.0f) { Survival.Health = 0.0f; Survival.Stamina = 0.0f; Survival.bIsDead = true; if (Survival.LastDeathReason.IsNone()) { Survival.LastDeathReason = FName(TEXT("health_depleted")); } } else if (!Survival.bIsDead) { Survival.LastDeathReason = NAME_None; } }