#include "Systems/DescentHorror.h" #include "Audio/DescentAudio.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Debug/DescentCommandUtil.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Player/DescentVitals.h" #include "Systems/DescentBio.h" #include "Systems/DescentCargo.h" #include "Systems/DescentSave.h" #include "Systems/DescentSonar.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/DescentCarrier.h" #include "World/DescentWorldDef.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Camera/PlayerCameraManager.h" #include "Components/InstancedStaticMeshComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "HAL/IConsoleManager.h" #include "Materials/MaterialInstanceDynamic.h" namespace DH = Descent::Horror; namespace Descent::Horror { double DepthFactor(double DepthM) { return FMath::Clamp((DepthM - 1760.0) / 1400.0, 0.0, 1.0); } double Hallucination(double Nutrition) { return Nutrition < 25.0 ? FMath::Clamp((25.0 - Nutrition) / 20.0, 0.0, 1.0) * FMath::Clamp((25.0 - Nutrition) / 20.0, 0.0, 1.0) * (3.0 - 2.0 * FMath::Clamp((25.0 - Nutrition) / 20.0, 0.0, 1.0)) : 0.0; } double LeviRadiusM(int32 Segment) { return 5.2 * FMath::Pow(FMath::Sin(UE_DOUBLE_PI * (Segment + 2.0) / (LeviSegments + 3.0)), 0.6); } } namespace { // where the eight dead beacons lie (world metres), and what each one still says before it cuts out const FVector BeaconPos[DH::NumBeacons] = { FVector(425, 25, -128), FVector(345, 250, -210), FVector(400, -470, -206), FVector(690, 205, -274), FVector(640, -230, -706), FVector(540, -330, -806), FVector(900, -420, -1026), FVector(1060, -380, -1186) }; const TCHAR* const BeaconLog[DH::NumBeacons] = { TEXT("Survey crew Halden, day 3. The canyon is clean. Tethys says the stratum is two days down. We are making good"), TEXT("Halden, day 5. Lost the second Tender in a void the map calls honest. The hull did not fail. It was"), TEXT("Day 6. The vat is at four units. We eat what the suction brings. Nobody says what the fish look like when"), TEXT("Day 8. Something answers the sonar. Not an echo. It waits one breath longer than the rock does, then"), TEXT("Day 9. The Great Void. We turned the lamps off to save power and the dark was not empty. It has"), TEXT("Day 11. Kilo Seven. The heartbeat is every ninety seconds. Ours are faster now. Whoever reads this, do not"), TEXT("Day 13. The Undercroft. Tethys stopped answering. The debt counter still runs. I think the debt is the"), TEXT("Day 14. We can see the glow under the floor. It is not ore. Do not anchor here. Do not let the gauge read"), }; UMaterialInstanceDynamic* HorrorSurface(UObject* Outer, const FLinearColor& Base, const FLinearColor& Emissive, float Strength) { UMaterialInterface* Parent = LoadObject(nullptr, TEXT("/Game/Materials/M_DescentSurface.M_DescentSurface"), nullptr, LOAD_NoWarn); if (!Parent) { return nullptr; } UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Parent, Outer); MID->SetVectorParameterValue(TEXT("BaseColor"), Base); MID->SetVectorParameterValue(TEXT("EmissiveColor"), Emissive); MID->SetScalarParameterValue(TEXT("EmissiveStrength"), Strength); MID->SetScalarParameterValue(TEXT("Roughness"), 0.3f); return MID; } } UDescentHorrorSubsystem* UDescentHorrorSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentHorrorSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Horror"); StepHandle = Sim->RegisterStep(EDescentSimStep::Horror, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } if (UDescentSaveSubsystem* Save = UDescentSaveSubsystem::Get(&InWorld)) { Save->OnCapture.AddWeakLambda(this, [this](UDescentSaveGame* S) { for (int32 I = 0; I < DH::NumBeacons; ++I) { if (bFound[I]) { S->HorrorFlags.Add(FName(*FString::Printf(TEXT("log%d"), I))); } } if (bEnding) { S->HorrorFlags.Add(TEXT("ending")); } if (bFelt) { S->HorrorFlags.Add(TEXT("felt")); } S->SimTime = Clock; }); Save->OnApply.AddWeakLambda(this, [this](const UDescentSaveGame* S) { LogsFound = 0; for (int32 I = 0; I < DH::NumBeacons; ++I) { bFound[I] = S->HorrorFlags.Contains(FName(*FString::Printf(TEXT("log%d"), I))); LogsFound += bFound[I] ? 1 : 0; } bEnding = bEndCardShown = S->HorrorFlags.Contains(TEXT("ending")); bFelt = S->HorrorFlags.Contains(TEXT("felt")); Clock = S->SimTime; EndingT = -1.0; }); } } void UDescentHorrorSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } FVector UDescentHorrorSubsystem::BeaconPosM(int32 I) const { return I >= 0 && I < DH::NumBeacons ? BeaconPos[I] : FVector::ZeroVector; } void UDescentHorrorSubsystem::Radio(const FString& Text, float Seconds) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().SetRadio(Text, Seconds); } if (UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(GetWorld())) { Audio->OneShot(TEXT("radio_blip")); } } void UDescentHorrorSubsystem::Heartbeat() { ++Heartbeats; Pulse = 1.0; HeartEcho = 1.4; if (UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(GetWorld())) { Audio->OneShot(TEXT("heartbeat"), static_cast(0.25 + 0.9 * Dz)); } } FVector UDescentHorrorSubsystem::LeviathanPointM(double AlongM) const { const FVector P = LeviStart + LeviDir * AlongM; return P + FVector(0.0, 0.0, 9.0 * FMath::Sin(AlongM / 60.0)); // a slow vertical wave } void UDescentHorrorSubsystem::StartLeviathan(bool bNear) { UWorld* World = GetWorld(); const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; if (!PC || !PC->PlayerCameraManager) { return; } const FVector Cam = PC->PlayerCameraManager->GetCameraLocation() / 100.0; const double Yaw = PC->PlayerCameraManager->GetCameraRotation().Yaw; const FVector Fwd = FRotator(0.0, Yaw, 0.0).Vector(), Right = FRotator(0.0, Yaw + 90.0, 0.0).Vector(); // it crosses ahead: 80-120 m (55-75 m on a near pass), left to right, on a 520 m line const double Ahead = bNear ? Rng.FRandRange(55.0, 75.0) : Rng.FRandRange(80.0, 120.0); const double LeadIn = bNear ? 60.0 : 0.5 * DH::LeviPathM; // a near pass starts already close to the view LeviStart = Cam + Fwd * Ahead - Right * LeadIn - FVector(0, 0, 4.0); LeviDir = Right; LeviS = 0.0; ++LeviPasses; if (UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(World)) { Audio->OneShot(TEXT("leviathan")); } } void UDescentHorrorSubsystem::ShowPhantom() { ++Phantoms; PhantomLeft = 0.35; } void UDescentHorrorSubsystem::StartEnding() { if (bEnding) { return; } bEnding = true; EndingT = 0.0; Radio(TEXT("Anchoring..."), 6.f); } void UDescentHorrorSubsystem::OnSonarPing(const FVector& OriginM, double RangeM) { UDescentSonarSubsystem* Sonar = UDescentSonarSubsystem::Get(GetWorld()); if (!Sonar) { return; } // the leviathan answers as a red cluster if (IsLeviathanActive() && FVector::Dist(LeviathanHeadM(), OriginM) <= RangeM) { for (int32 I = 0; I < 26; I += 5) { Sonar->AddContact(LeviathanPointM(LeviS - I * DH::LeviSpacingM * 1.5), Descent::Sonar::ContactLifeSec, 2, I == 0 ? TEXT("UNKNOWN - MASSIVE") : TEXT("")); } } for (int32 I = 0; I < DH::NumBeacons; ++I) { if (FVector::Dist(BeaconPos[I], OriginM) <= RangeM) { Sonar->AddContact(BeaconPos[I], Descent::Sonar::ContactLifeSec, 3, TEXT("BEACON")); } } // a ghost: a contact that is gone 2.2 s later const double Depth = Descent::DepthOfZ(OriginM.Z); const double Chance = Hallu > 0.0 ? 0.5 : (Depth > 2340.0 ? 0.14 : 0.04); if (Rng.FRand() < Chance) { Sonar->AddContact(OriginM + Rng.VRand() * RangeM * Rng.FRandRange(0.6, 0.95), Descent::Sonar::GhostLifeSec, 2, TEXT("UNKNOWN")); ++Ghosts; } } void UDescentHorrorSubsystem::Step(double Dt) { UWorld* World = GetWorld(); ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; const ADescentCharacter* Ch = PC ? PC->GetDescentCharacter() : nullptr; const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(World); UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(World); if (!PC || !Ch) { return; } Clock += Dt; const FVector Ref = PC->RefPointM(); const double Depth = Descent::DepthOfZ(Ref.Z); Dz = DH::DepthFactor(Depth); Hallu = DH::Hallucination(Ch->Vitals->State.Nutrition); const bool bInWater = PC->GetMode() == EDescentMode::Swim || PC->GetMode() == EDescentMode::Sub; Pulse = FMath::Max(0.0, Pulse - 0.35 * Dt); // hull creak and pressure groan const double Scrape = Carrier ? Carrier->Helm.Scrape : 0.0; CreakIn -= Dt * (1.0 + 2.0 * Dz + 4.0 * Scrape) * ((Carrier && Carrier->Helm.Hull < 30.0) ? 2.0 : 1.0); if (CreakIn <= 0.0) { CreakIn = Rng.FRandRange(18.0, 45.0); ++Creaks; if (Audio) { Audio->OneShot(TEXT("creak")); } } if (Dz > 0.02) { GroanIn -= Dt * Dz; if (GroanIn <= 0.0) { GroanIn = Rng.FRandRange(40.0, 80.0); ++Groans; if (Audio) { Audio->OneShot(TEXT("groan")); } } } // the heartbeat from below, with its echo HeartIn -= Dt; if (HeartIn <= 0.0) { HeartIn = Rng.FRandRange(80.0, 100.0); Heartbeat(); if (!bFelt && Depth > 1900.0) { bFelt = true; Radio(TEXT("Did you feel that?")); } } if (HeartEcho > 0.0) { HeartEcho -= Dt; if (HeartEcho <= 0.0) { Pulse = FMath::Max(Pulse, 0.6); if (Audio) { Audio->OneShot(TEXT("heartbeat"), 0.5f); } } } // hunger: whispers, the phantom, false radio if (Hallu > 0.0 || Depth > 2700.0) { WhisperIn -= Dt * (0.2 + 2.0 * Hallu + 0.4); if (WhisperIn <= 0.0) { WhisperIn = Rng.FRandRange(25.0, 45.0); ++Whispers; if (Audio) { Audio->OneShot(TEXT("whisper")); } } } if (Hallu > 0.2 && bInWater) { PhantomIn -= Dt * Hallu; if (PhantomIn <= 0.0) { PhantomIn = Rng.FRandRange(20.0, 40.0); ShowPhantom(); } } if (Hallu > 0.4 && Rng.FRand() < Dt / 40.0) { ++FalseRadios; Radio(TEXT("????? ...do not anchor... ?????"), 2.5f); } // the leviathan: first below 1,905 m, then every 160-320 s; never while the camera is inside a vessel on foot LeviIn -= Dt; if (!IsLeviathanActive() && bInWater && Depth > 1905.0 && (bLeviFirst || LeviIn <= 0.0)) { bLeviFirst = false; LeviIn = Rng.FRandRange(160.0, 320.0); StartLeviathan(false); } if (IsLeviathanActive()) { LeviS += DH::LeviSpeedMps * Dt; if (LeviS - DH::LeviSegments * DH::LeviSpacingM > DH::LeviPathM) { LeviS = -1.0; } } // dead beacons: within 24 m the log types out and cuts mid-sentence if (bInWater && TypePos < 0.0) { for (int32 I = 0; I < DH::NumBeacons; ++I) { if (!bFound[I] && FVector::Dist(BeaconPos[I], Ref) <= DH::BeaconTriggerM) { bFound[I] = true; ++LogsFound; TypeText = BeaconLog[I]; TypePos = 0.0; break; } } } if (TypePos >= 0.0) { TypePos += DH::TypeCharsPerSec * Dt; const bool bDone = TypePos >= TypeText.Len(); if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World)) { Hud->Model().SetRadio(bDone ? TypeText + TEXT(" [SIGNAL LOST]") : TypeText.Left(FMath::FloorToInt32(TypePos)), bDone ? 7.f : 3.f); } if (bDone) { TypePos = -1.0; if (Audio) { Audio->OneShot(TEXT("static_burst")); } } } // the ending: the carrier anchored in the Seam Gallery if (!bEnding && Carrier) { const Descent::World::FPlace* Goal = Descent::World::FDescentWorldDef::Get().FindPlace(TEXT("goal")); const FVector P = Carrier->Helm.PosM; if (Goal && P.Z <= -1290.0 && FVector2D(P.X - Goal->PosM.X, P.Y - Goal->PosM.Y).Size() < 90.0) { StartEnding(); } else if (Carrier->Autopilot.bDone && P.Z <= -1200.0) { StartEnding(); } // the autopilot brought her to the end of a route } if (bEnding && EndingT >= 0.0) { const double Before = EndingT; EndingT += Dt; if (Before < 6.0 && EndingT >= 6.0) { Heartbeat(); Radio(TEXT("...Depth gauge reads 66.666 km. Disregard. Disreg"), 8.f); } if (Before < 16.0 && EndingT >= 16.0) { bEndCardShown = true; EndingT = -1.0; if (UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(World)) { const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World); FDescentEndCardViewModel& M = Ui->Models().EndCard; M.HullPct = Carrier ? static_cast(Carrier->Helm.Hull) : 100.f; M.Balance = Cargo ? -static_cast(Cargo->Debt()) : 0; M.LogsFound = LogsFound; M.LogsTotal = DH::NumBeacons; M.TimeText = FString::Printf(TEXT("%d h %d min"), FMath::FloorToInt32(Clock / 3600.0), FMath::FloorToInt32(FMath::Fmod(Clock, 3600.0) / 60.0)); M.Line = TEXT("The CANTICLE is anchored at the target stratum."); Ui->NotifyChanged(); Ui->Close(); Ui->Open(EDescentWindow::EndCard); } } } UpdateVisuals(Dt); WindowTimer -= Dt; if (WindowTimer <= 0.0) { WindowTimer = 0.5; FillWindows(); } } void UDescentHorrorSubsystem::UpdateVisuals(double Dt) { UWorld* World = GetWorld(); if (!Visual && World) { Visual = World->SpawnActor(); USceneComponent* Root = NewObject(Visual, TEXT("Root")); Visual->SetRootComponent(Root); Root->RegisterComponent(); UStaticMesh* Sphere = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere")); UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); auto MakeISM = [this, Root](const TCHAR* Name, UStaticMesh* Mesh, UMaterialInterface* Mat) { UInstancedStaticMeshComponent* M = NewObject(Visual, Name); M->SetMobility(EComponentMobility::Movable); M->SetupAttachment(Root); M->SetStaticMesh(Mesh); M->SetCollisionEnabled(ECollisionEnabled::NoCollision); M->SetCastShadow(false); if (Mat) { M->SetMaterial(0, Mat); } M->RegisterComponent(); return M; }; LeviBody = MakeISM(TEXT("LeviBody"), Sphere, HorrorSurface(Visual, FLinearColor(0.012f, 0.015f, 0.02f), FLinearColor::Black, 0.f)); LeviLights = MakeISM(TEXT("LeviLights"), Sphere, HorrorSurface(Visual, FLinearColor::Black, FLinearColor(0.1f, 0.9f, 1.0f), 3.0f)); BeaconMesh = MakeISM(TEXT("DeadBeacons"), Sphere, HorrorSurface(Visual, FLinearColor::Black, FLinearColor(1.0f, 0.05f, 0.03f), 6.0f)); Phantom = NewObject(Visual, TEXT("Phantom")); Phantom->SetMobility(EComponentMobility::Movable); Phantom->SetupAttachment(Root); Phantom->SetStaticMesh(Cube); Phantom->SetCollisionEnabled(ECollisionEnabled::NoCollision); Phantom->SetCastShadow(false); if (UMaterialInstanceDynamic* MID = HorrorSurface(Visual, FLinearColor::Black, FLinearColor::Black, 0.f)) { Phantom->SetMaterial(0, MID); } Phantom->SetWorldScale3D(FVector(0.3, 0.55, 1.85)); Phantom->SetVisibility(false); Phantom->RegisterComponent(); } if (!LeviBody || !LeviLights || !BeaconMesh || !Phantom) { return; } LeviBody->ClearInstances(); LeviLights->ClearInstances(); if (IsLeviathanActive()) { for (int32 I = 0; I < DH::LeviSegments; ++I) { const FVector P = LeviathanPointM(LeviS - I * DH::LeviSpacingM); const double R = DH::LeviRadiusM(I); LeviBody->AddInstance(FTransform(LeviDir.Rotation(), P * 100.0, FVector(2.6 * R / 2.0, 2.0 * R, 2.0 * R)), true); // two rows of faint photophores for (double Side : { -1.0, 1.0 }) { LeviLights->AddInstance(FTransform(FQuat::Identity, (P + FVector(0.0, 0.0, 0.35 * R) + FVector::CrossProduct(LeviDir, FVector::UpVector) * (Side * 0.92 * R)) * 100.0, FVector(0.22)), true); } } } // the dead beacons blink red BeaconMesh->ClearInstances(); if (FMath::Fmod(Clock, 1.5) < 0.75) { for (const FVector& P : BeaconPos) { BeaconMesh->AddInstance(FTransform(FQuat::Identity, P * 100.0, FVector(0.35)), true); } } // the phantom: a black figure 12-18 m ahead for 0.35 s const bool bWas = PhantomLeft > 0.0; PhantomLeft = FMath::Max(0.0, PhantomLeft - Dt); const APlayerController* PC = World->GetFirstPlayerController(); if (bWas && !Phantom->IsVisible() && PC && PC->PlayerCameraManager) { const FVector Cam = PC->PlayerCameraManager->GetCameraLocation(); const FRotator Rot = PC->PlayerCameraManager->GetCameraRotation(); Phantom->SetWorldLocationAndRotation(Cam + Rot.Vector() * Rng.FRandRange(1200.0, 1800.0), FRotator(0.0, Rot.Yaw, 0.0)); Phantom->SetVisibility(true); } else if (PhantomLeft <= 0.0 && Phantom->IsVisible()) { Phantom->SetVisibility(false); } } void UDescentHorrorSubsystem::FillWindows() { UWorld* World = GetWorld(); UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(World); if (!Ui || Ui->IsDemo() || Ui->GetWindow() != EDescentWindow::Pda) { return; } if (Ui->GetTab() == TEXT("logs")) { FDescentLogsViewModel& M = Ui->Models().Logs; M.Entries.Reset(); M.Total = DH::NumBeacons; for (int32 I = 0; I < DH::NumBeacons; ++I) { if (bFound[I]) { M.Entries.Add({ FString::Printf(TEXT("DEAD BEACON %d"), I + 1), FString(BeaconLog[I]) + TEXT(" [SIGNAL LOST]"), true }); } } M.Selected = FMath::Clamp(M.Selected, 0, FMath::Max(0, M.Entries.Num() - 1)); TWeakObjectPtr WeakUi(Ui); M.OnSelect = [WeakUi](int32 I) { if (UDescentUiSubsystem* U = WeakUi.Get()) { U->Models().Logs.Selected = I; U->NotifyChanged(); } }; Ui->NotifyChanged(); } else if (Ui->GetTab() == TEXT("status")) { const ADescentPlayerController* PC = Cast(World->GetFirstPlayerController()); const ADescentCharacter* Ch = PC ? PC->GetDescentCharacter() : nullptr; const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(World); const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World); const UDescentBioSubsystem* Bio = UDescentBioSubsystem::Get(World); FDescentStatusViewModel& M = Ui->Models().Status; M.Lines.Reset(); auto Line = [&M](const TCHAR* Label, const FString& Value, float Fraction) { FDescentStatusLine L; L.Label = Label; L.Value = Value; L.Fraction = Fraction; M.Lines.Add(L); }; if (Ch) { const FDescentVitalsState& V = Ch->Vitals->State; Line(TEXT("HEALTH"), FString::Printf(TEXT("%.0f %%"), V.Health), static_cast(V.Health / 100.0)); Line(TEXT("NUTRITION"), FString::Printf(TEXT("%.0f %% (%.0f min left)"), V.Nutrition, V.Nutrition / Descent::Vitals::NutritionDrainPerSec / 60.0), static_cast(V.Nutrition / 100.0)); Line(TEXT("O2 TANK"), FString::Printf(TEXT("%.0f s"), V.O2Max), -1.f); } if (Bio) { Line(TEXT("BIOMASS RESERVE"), FString::Printf(TEXT("%.1f / 20 FU"), Bio->ReserveFU), static_cast(Bio->ReserveFU / 20.0)); } if (Carrier) { Line(TEXT("CARRIER HULL"), FString::Printf(TEXT("%.0f %%"), Carrier->Helm.Hull), static_cast(Carrier->Helm.Hull / 100.0)); Line(TEXT("CARRIER DEPTH"), FString::Printf(TEXT("%.0f m"), Descent::DepthOfZ(Carrier->Helm.PosM.Z)), -1.f); } if (Cargo) { Line(TEXT("DEBT TO TETHYS"), FString::Printf(TEXT("%.0f cr"), Cargo->Debt()), -1.f); Line(TEXT("STORAGE"), FString::Printf(TEXT("%d / %d"), Cargo->StorageUsed(), Cargo->StorageCap), static_cast(Cargo->StorageUsed()) / FMath::Max(1, Cargo->StorageCap)); } Line(TEXT("LOGS FOUND"), FString::Printf(TEXT("%d / %d"), LogsFound, DH::NumBeacons), -1.f); M.Footer = FString::Printf(TEXT("Contract time %d h %d min"), FMath::FloorToInt32(Clock / 3600.0), FMath::FloorToInt32(FMath::Fmod(Clock, 3600.0) / 60.0)); Ui->NotifyChanged(); } } // ---- commands and the state section `horror` (section 9.4) --------------------------------------------------------------- namespace { void CmdHorrorLevi(const TArray& Args, UWorld* World) { if (UDescentHorrorSubsystem* H = UDescentHorrorSubsystem::Get(World)) { H->StartLeviathan(Args.Num() > 0 && Args[0].Equals(TEXT("near"), ESearchCase::IgnoreCase)); } } void CmdHorrorHeartbeat(const TArray&, UWorld* World) { if (UDescentHorrorSubsystem* H = UDescentHorrorSubsystem::Get(World)) { H->Heartbeat(); } } void CmdHorrorPhantom(const TArray&, UWorld* World) { if (UDescentHorrorSubsystem* H = UDescentHorrorSubsystem::Get(World)) { H->ShowPhantom(); } } void CmdCarrierFinish(const TArray&, UWorld* World) { if (UDescentHorrorSubsystem* H = UDescentHorrorSubsystem::Get(World)) { H->StartEnding(); } } FAutoConsoleCommandWithWorldAndArgs GCmdHorrorLevi(TEXT("Descent.Horror.Levi"), TEXT("Descent.Horror.Levi [near] - start a leviathan pass ahead of the camera"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdHorrorLevi)); FAutoConsoleCommandWithWorldAndArgs GCmdHorrorHeartbeat(TEXT("Descent.Horror.Heartbeat"), TEXT("Descent.Horror.Heartbeat - the heartbeat from below, now"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdHorrorHeartbeat)); FAutoConsoleCommandWithWorldAndArgs GCmdHorrorPhantom(TEXT("Descent.Horror.Phantom"), TEXT("Descent.Horror.Phantom - show the phantom for 0.35 s"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdHorrorPhantom)); FAutoConsoleCommandWithWorldAndArgs GCmdCarrierFinish(TEXT("Descent.Carrier.Finish"), TEXT("Descent.Carrier.Finish - the ending, as if the carrier had anchored at the target stratum"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdCarrierFinish)); } static FDescentStateProviderRegistrar GHorrorStateSection(TEXT("horror"), [](UWorld* World, FJsonObject& Out) { const UDescentHorrorSubsystem* H = UDescentHorrorSubsystem::Get(World); Out.SetBoolField(TEXT("available"), H != nullptr); if (!H) { return; } Out.SetNumberField(TEXT("dz"), Descent::Json::Round(H->Dz)); Out.SetNumberField(TEXT("hallu"), Descent::Json::Round(H->Hallu)); Out.SetNumberField(TEXT("pulse"), Descent::Json::Round(H->Pulse)); Out.SetNumberField(TEXT("heartbeats"), H->Heartbeats); Out.SetNumberField(TEXT("creaks"), H->Creaks); Out.SetNumberField(TEXT("groans"), H->Groans); Out.SetNumberField(TEXT("whispers"), H->Whispers); Out.SetNumberField(TEXT("phantoms"), H->Phantoms); Out.SetNumberField(TEXT("false_radios"), H->FalseRadios); Out.SetNumberField(TEXT("sonar_ghosts"), H->Ghosts); Out.SetNumberField(TEXT("logs_found"), H->LogsFound); Out.SetBoolField(TEXT("leviathan"), H->IsLeviathanActive()); Out.SetNumberField(TEXT("leviathan_passes"), H->LeviPasses); Out.SetBoolField(TEXT("ending"), H->bEnding); Out.SetBoolField(TEXT("end_card"), H->bEndCardShown); });