#include "Player/DescentVitals.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Debug/DescentStateRegistry.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Vehicles/CarrierHullShape.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/TenderFlight.h" #include "Engine/World.h" namespace VitTune = Descent::Tune::Player; void FDescentVitalsState::Advance(const FDescentVitalsInput& In, double Dt) { if (bDead || Dt <= 0.0) { return; } bDrowning = false; if (In.bSwimming) { if (In.bHeadAtPoolSurface) { O2 = FMath::Min(O2Max, O2 + VitTune::O2RefillPerSec * Dt); } else { O2 = FMath::Max(0.0, O2 - VitTune::O2DrainPerSec * (In.bSprint ? VitTune::O2SprintFactor : 1.0) * Dt); } if (O2 <= 0.0) { bDrowning = true; Health -= VitTune::DrownHPPerSec * Dt; } if (In.bHonestVoid) { Health -= VitTune::SwimHonestVoidHPPerSec * Dt; } } else { // O2 refills everywhere out of the water; health comes back slowly while fed. O2 = FMath::Min(O2Max, O2 + VitTune::O2RefillPerSec * Dt); if (Nutrition > VitTune::HungerPct) { Health = FMath::Min(100.0, Health + VitTune::HealthRegenPerSec * Dt); } } // U2 (M8): 100 % of nutrition lasts 40 minutes (v2: 9); at 0 the body starves Nutrition = FMath::Max(0.0, Nutrition - Descent::Vitals::NutritionDrainPerSec * Dt); bool bStarved = false; if (Nutrition <= 0.0) { Health -= Descent::Vitals::StarveHPPerSec * Dt; bStarved = Health <= 0.0; } if (Health <= 0.0) { Health = 0.0; Die(bStarved ? TEXT("starve") : TEXT("drown")); } } void FDescentVitalsState::Die(const TCHAR* Reason) { if (bDead) { return; } bDead = true; DeadReason = Reason; ++Deaths; Debt += DeadReason == TEXT("implode") ? VitTune::DebtImplodeCr : VitTune::DebtDrownCr; } void FDescentVitalsState::Respawn() { bDead = false; bDrowning = false; Health = VitTune::RespawnHealth; O2 = O2Max; } UDescentVitalsComponent::UDescentVitalsComponent() { PrimaryComponentTick.bCanEverTick = false; // section 1.4: stepped by the sim } void UDescentVitalsComponent::BeginPlay() { Super::BeginPlay(); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Vitals"); StepHandle = Sim->RegisterStep(EDescentSimStep::Vitals, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UDescentVitalsComponent::EndPlay(const EEndPlayReason::Type Reason) { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::EndPlay(Reason); } void UDescentVitalsComponent::Step(double Dt) { const ADescentCharacter* C = Cast(GetOwner()); ADescentPlayerController* PC = C ? Cast(C->GetController()) : nullptr; if (!PC) { return; // not possessed (a scenario flies a fly pawn): nothing happens to the body } const EDescentMode Mode = PC->GetMode(); if (Mode == EDescentMode::Title || Mode == EDescentMode::Dead) { return; } FDescentVitalsInput In; In.bSwimming = Mode == EDescentMode::Swim; if (In.bSwimming) { const UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); In.bSprint = Sim && Sim->GetIntent().bSprint && !Sim->GetIntent().Move.IsNearlyZero(); In.bHonestVoid = Descent::Tender::InHonestVoid(Descent::UUToMetres(C->GetActorLocation())); if (const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(GetWorld())) { const FVector Head = Carrier->ToLocalM(C->EyeWorldUU()); In.bHeadAtPoolSurface = Descent::Carrier::InPoolShaft(FVector(Head.X, Head.Y, Descent::Tune::Carrier::PoolWaterZ - 0.01)) && Head.Z > Descent::Tune::Carrier::PoolWaterZ - VitTune::O2RefillHeadRangeM; } } State.Advance(In, Dt); if (State.bDead) { PC->OnPlayerDied(); } } // ---- state section `vitals` (section 9.4; `econ` is the cargo subsystem's since M5) ------------------------------------------- static const UDescentVitalsComponent* VitalsOfWorld(UWorld* World) { const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; const ADescentCharacter* C = PC ? PC->GetDescentCharacter() : nullptr; return C ? C->Vitals : nullptr; } static FDescentStateProviderRegistrar GVitalsStateSection(TEXT("vitals"), [](UWorld* World, FJsonObject& Out) { using namespace Descent::Json; const UDescentVitalsComponent* V = VitalsOfWorld(World); Out.SetBoolField(TEXT("available"), V != nullptr); if (!V) { return; } Out.SetNumberField(TEXT("nutrition"), Round(V->State.Nutrition)); Out.SetNumberField(TEXT("nutrition_rate_per_s"), Round(Descent::Vitals::NutritionDrainPerSec)); Out.SetNumberField(TEXT("o2_s"), Round(V->State.O2)); Out.SetNumberField(TEXT("o2_max_s"), Round(V->State.O2Max)); Out.SetNumberField(TEXT("health"), Round(V->State.Health)); Out.SetBoolField(TEXT("drowning"), V->State.bDrowning); Out.SetBoolField(TEXT("dead"), V->State.bDead); Out.SetStringField(TEXT("dead_reason"), V->State.DeadReason); Out.SetNumberField(TEXT("deaths"), V->State.Deaths); });