#include "Systems/DescentBio.h" #include "Audio/DescentAudio.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Debug/DescentCommandUtil.h" #include "Debug/DescentStateRegistry.h" #include "Player/Arm/DescentArmViewmodelComponent.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Player/DescentVitals.h" #include "Systems/DescentCargo.h" #include "Systems/DescentCraft.h" #include "Systems/DescentSave.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/CarrierBlockout.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "DescentUnits.h" #include "TheDescent.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 DBio = Descent::Bio; namespace Descent::Bio { void FeedPlan(double Nutrition, double ReserveFU, double& OutTarget, double& OutUsedFU) { OutTarget = FMath::Min(100.0, Nutrition + NutritionPerFU * FMath::Max(0.0, ReserveFU)); OutUsedFU = FMath::Max(0.0, OutTarget - Nutrition) / NutritionPerFU; } double FaunaValue(FName Item) { if (Item == TEXT("silverfin")) { return 0.10; } if (Item == TEXT("lanternfish")) { return 0.15; } if (Item == TEXT("jelly")) { return 0.60; } if (Item == TEXT("polyp")) { return 0.40; } return 0.0; } const TArray& BioPipeRouteM() { // from the belly coupling of the Tender at BOARD, up across the bay, along the D2 ceiling at y 6, down into the vat in the galley static TArray Route; if (Route.Num() == 0) { const FVector A = Descent::Cargo::OrePipeRouteM()[0] + FVector(0.0, 0.5, 0.0); const double Zc = Descent::Tune::Carrier::DeckFloorZ2 + Descent::Tune::Carrier::RoomHeightM - 0.45; Route = { A, FVector(A.X + 2.0, 6.0, Zc), FVector(21.2, 6.0, Zc), FVector(21.2, 6.0, Descent::Tune::Carrier::DeckFloorZ2 + 2.8) }; } return Route; } } namespace { UMaterialInstanceDynamic* BioSurface(UObject* Outer, 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"), Emissive * 0.2f); MID->SetVectorParameterValue(TEXT("EmissiveColor"), Emissive); MID->SetScalarParameterValue(TEXT("EmissiveStrength"), Strength); return MID; } const FLinearColor BioGreen(0.25f, 1.0f, 0.35f), PortCyan(0.2f, 0.9f, 1.0f), PortRed(1.0f, 0.08f, 0.04f); } UDescentBioSubsystem* UDescentBioSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentBioSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Bio"); Opt.SubOrder = 40; StepHandle = Sim->RegisterStep(EDescentSimStep::Cargo, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } if (UDescentSaveSubsystem* Save = UDescentSaveSubsystem::Get(&InWorld)) { Save->OnCapture.AddWeakLambda(this, [this](UDescentSaveGame* S) { S->BiomassFU = ReserveFU; }); Save->OnApply.AddWeakLambda(this, [this](const UDescentSaveGame* S) { ReserveFU = FMath::Clamp(S->BiomassFU, 0.0, DBio::CapFU); Pieces.Reset(); }); } } void UDescentBioSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } double UDescentBioSubsystem::AddFU(double Fu) { const double In = FMath::Clamp(Fu, 0.0, DBio::CapFU - ReserveFU); ReserveFU += In; return In; } bool UDescentBioSubsystem::BeginJackIn(ADescentCharacter* User, const FVector& PortLocalM, double PortYawDeg, FString* OutWhyNot) { UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld()); ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (!User || !C || IsJackActive()) { if (OutWhyNot) { *OutWhyNot = TEXT("busy"); } return false; } if (ReserveFU < DBio::EmptyBelowFU) { // U3: an empty reserve means no feeding ++PortRefused; if (Hud) { Hud->Model().SetRadio(TEXT("Nutrient reserve empty. The sea is full of protein, contractor."), 7.f); Hud->Model().PushToast(TEXT("NUTRIENT RESERVE EMPTY"), EDescentHudToastKind::Bad); } if (OutWhyNot) { *OutWhyNot = TEXT("the reserve is empty"); } return false; } FDescentVitalsState& V = User->Vitals->State; if (V.Nutrition >= 99.5) { if (Hud) { Hud->Model().PushToast(TEXT("You are not hungry")); } if (OutWhyNot) { *OutWhyNot = TEXT("not hungry"); } return false; } JackN0 = V.Nutrition; DBio::FeedPlan(JackN0, ReserveFU, JackTarget, JackUsed); JackT = 0.0; JackUser = User; if (UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(GetWorld())) { Audio->OneShot(TEXT("jack")); } User->SetFrozen(true); // what the player sees of it: the old arm's sequence (reach, plug, feed, release) when the arm is the view, nothing with the body. // The timing above (JackT) does not depend on it. { const FVector Normal = C->GetActorTransform().TransformVectorNoScale(FRotator(0.0, PortYawDeg, 0.0).Vector()); User->StartJackVisual(C->ToWorldUU(PortLocalM), -Normal); } return true; } double UDescentBioSubsystem::UseVat(ADescentCharacter* User) { UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(GetWorld()); UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld()); double Loaded = 0.0; if (Craft) { for (const TCHAR* Name : { TEXT("polyp"), TEXT("jelly"), TEXT("silverfin"), TEXT("lanternfish") }) { const FName Item(Name); while (Craft->Count(Item) > 0 && ReserveFU + DBio::FaunaValue(Item) <= DBio::CapFU + 1e-6) { Craft->RemoveItem(Item, 1); Loaded += AddFU(DBio::FaunaValue(Item)); } } } if (Loaded > 0.0) { HandFU += Loaded; if (Hud) { Hud->Model().PushToast(FString::Printf(TEXT("Biomass +%.1f FU"), Loaded), EDescentHudToastKind::Good); } } else if (UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld())) { FDescentVatViewModel& M = Ui->Models().Vat; M.ReserveFu = static_cast(ReserveFU); M.CapFu = static_cast(DBio::CapFU); M.Feed.Reset(); Ui->NotifyChanged(); Ui->Open(EDescentWindow::Vat); } return Loaded; } void UDescentBioSubsystem::Sleep(ADescentCharacter* User) { UWorld* World = GetWorld(); if (!User || !World) { return; } FDescentVitalsState& V = User->Vitals->State; V.Health = 100.0; V.Nutrition = FMath::Max(FMath::Min(V.Nutrition, 8.0), V.Nutrition - 10.0); if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World)) { for (ADescentRefinery* R : Cargo->GetRefineries()) { if (R) { for (int32 I = 0; I < 120; ++I) { R->Step(1.0); } } } } if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World)) { for (const TWeakObjectPtr& W : Fleet->All()) { AKazahTender* T = W.Get(); if (T && !T->Flight->IsOut()) { T->Flight->PowerPct = 100.0; T->Flight->HullHP = T->Flight->Stats().HullMax; } } } ++Sleeps; if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World)) { Hud->Model().PushToast(TEXT("You slept. Two hours passed."), EDescentHudToastKind::Good); } if (UDescentSaveSubsystem* Save = UDescentSaveSubsystem::Get(World)) { Save->SaveToSlot(); } } void UDescentBioSubsystem::Step(double Dt) { UWorld* World = GetWorld(); ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(World); if (!C) { return; } // the jack-in: nutrition rises from n0 to the target between 1.0 s and 4.1 s; the reserve is charged at the end if (IsJackActive()) { ADescentCharacter* User = JackUser.Get(); JackT += Dt; if (User) { const double F = FMath::Clamp((JackT - DBio::FeedFromSec) / (DBio::FeedToSec - DBio::FeedFromSec), 0.0, 1.0); User->Vitals->State.Nutrition = FMath::Lerp(JackN0, JackTarget, F); } if (JackT >= DBio::JackTotalSec || !User) { if (User) { User->Vitals->State.Nutrition = JackTarget; User->Vitals->State.Debt += DBio::JackFeeCr; User->SetFrozen(false); } ReserveFU = FMath::Max(0.0, ReserveFU - JackUsed); LastUsedFU = JackUsed; ++Feeds; JackT = -1.0; if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World)) { Hud->Model().PushToast(FString::Printf(TEXT("Fed. Reserve %.1f FU. Billed 12 cr."), ReserveFU), EDescentHudToastKind::Good); } } } // the bio pipe: a docked Tender's bio-tank empties into the vat, 0.05 FU a piece const double Length = Descent::Cargo::RouteLengthM(DBio::BioPipeRouteM()); for (int32 I = Pieces.Num() - 1; I >= 0; --I) { Pieces[I] += DBio::PipeSpeedMps * Dt; if (Pieces[I] >= Length) { PipedFU += AddFU(DBio::PieceFU); Pieces.RemoveAt(I); } } FeedTimer -= Dt; if (FeedTimer <= 0.0) { FeedTimer = DBio::PipeFeedEverySec; if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World)) { for (const TWeakObjectPtr& W : Fleet->All()) { AKazahTender* T = W.Get(); if (!T || T->Flight->State != ETenderState::Cradle || T->Flight->BioFu <= 1e-6) { continue; } if (ReserveFU + (Pieces.Num() + 1) * DBio::PieceFU > DBio::CapFU + 1e-6) { break; } // the vat is full: the rest stays in the tank const double Take = FMath::Min(DBio::PieceFU, T->Flight->BioFu); T->Flight->BioFu -= Take; if (Take >= DBio::PieceFU - 1e-9) { Pieces.Add(0.0); } else { PipedFU += AddFU(Take); } break; } } } // the HUD readout if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World)) { const ADescentPlayerController* PC = Cast(World->GetFirstPlayerController()); Hud->Model().Vitals.BioReserveFu = static_cast(ReserveFU); Hud->Model().Vitals.BioCapFu = static_cast(DBio::CapFU); Hud->Model().Vitals.bShowBio = PC && PC->GetMode() == EDescentMode::Foot; } UpdateVisuals(); } void UDescentBioSubsystem::EnsureVisuals() { ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (Visual || !C) { return; } Visual = GetWorld()->SpawnActor(); USceneComponent* Root = NewObject(Visual, TEXT("Root")); Root->SetMobility(EComponentMobility::Movable); Visual->SetRootComponent(Root); Root->RegisterComponent(); Visual->SetActorTransform(C->GetActorTransform()); Visual->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); UStaticMesh* Cylinder = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cylinder.Cylinder")); auto Add = [this, Root](const TCHAR* Name, UStaticMesh* Mesh, UMaterialInterface* Mat) { UStaticMeshComponent* 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); } UCarrierBlockoutComponent::MakeInterior(M); M->RegisterComponent(); return M; }; // the vat's liquid: its level IS the reserve Liquid = Add(TEXT("Liquid"), Cylinder, BioSurface(Visual, BioGreen, 0.8f)); // a ring on each wall port: cyan while the reserve can feed, red when it is empty const Descent::Carrier::FLayout& L = Descent::Carrier::FLayout::Get(); int32 N = 0; for (const Descent::Carrier::FAnchorDef& A : L.Anchors) { if (A.Kind != TEXT("Port")) { continue; } UMaterialInstanceDynamic* MID = BioSurface(Visual, PortCyan, 2.0f); UStaticMeshComponent* Ring = Add(*FString::Printf(TEXT("PortRing_%d"), N++), Cube, MID); Ring->SetRelativeLocation(Descent::MetresToUU(A.PosM + FRotator(0.0, A.YawDeg, 0.0).Vector() * 0.05)); Ring->SetRelativeRotation(FRotator(0.0, A.YawDeg, 0.0)); Ring->SetRelativeScale3D(FVector(0.03, 0.34, 0.34)); RingMIDs.Add(MID); } // the bio pipe (glass) and its pieces UMaterialInterface* Glass = LoadObject(nullptr, TEXT("/Game/Materials/M_DescentGlass.M_DescentGlass"), nullptr, LOAD_NoWarn); const TArray& Route = DBio::BioPipeRouteM(); for (int32 I = 1; I < Route.Num(); ++I) { const FVector A = Route[I - 1], B = Route[I]; UStaticMeshComponent* Seg = Add(*FString::Printf(TEXT("BioPipe_%d"), I), Cube, Glass); Seg->SetRelativeLocation(Descent::MetresToUU(0.5 * (A + B))); Seg->SetRelativeRotation((B - A).Rotation()); Seg->SetRelativeScale3D(FVector(FVector::Dist(A, B) + 0.24, 0.24, 0.24)); } PieceMesh = NewObject(Visual, TEXT("BioPieces")); PieceMesh->SetMobility(EComponentMobility::Movable); PieceMesh->SetupAttachment(Root); PieceMesh->SetStaticMesh(Cube); PieceMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); PieceMesh->SetCastShadow(false); if (UMaterialInstanceDynamic* MID = BioSurface(Visual, BioGreen, 2.5f)) { PieceMesh->SetMaterial(0, MID); } UCarrierBlockoutComponent::MakeInterior(PieceMesh); PieceMesh->RegisterComponent(); } void UDescentBioSubsystem::UpdateVisuals() { EnsureVisuals(); if (!Liquid || !PieceMesh) { return; } const double Level = FMath::Clamp(ReserveFU / DBio::CapFU, 0.0, 1.0), H = FMath::Max(0.02, 2.8 * Level); Liquid->SetRelativeLocation(Descent::MetresToUU(FVector(21.2, 6.0, Descent::Tune::Carrier::DeckFloorZ2 + 0.5 * H))); Liquid->SetRelativeScale3D(FVector(1.9, 1.9, H)); const FLinearColor Ring = ReserveFU < DBio::EmptyBelowFU ? PortRed : PortCyan; for (UMaterialInstanceDynamic* MID : RingMIDs) { if (MID) { MID->SetVectorParameterValue(TEXT("EmissiveColor"), Ring); } } PieceMesh->ClearInstances(); const TArray& Route = DBio::BioPipeRouteM(); for (double D : Pieces) { PieceMesh->AddInstance(FTransform(FQuat::Identity, Descent::MetresToUU(Descent::Cargo::RoutePointM(Route, D)), FVector(0.14))); } } // ---- commands and the state section `bio` ----------------------------------------------------------------------------- namespace { void CmdBioSet(const TArray& Args, UWorld* World) { UDescentBioSubsystem* S = UDescentBioSubsystem::Get(World); TArray N; if (!S || !Descent::Cmd::ParseNumbers(Args, 0, 1, N)) { Descent::Cmd::Fail(World, TEXT("Descent.Bio.Set"), TEXT("usage: Descent.Bio.Set ")); return; } S->ReserveFU = FMath::Clamp(N[0], 0.0, DBio::CapFU); } FAutoConsoleCommandWithWorldAndArgs GCmdBioSet(TEXT("Descent.Bio.Set"), TEXT("Descent.Bio.Set v - set the biomass reserve (FU)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdBioSet)); } static FDescentStateProviderRegistrar GBioStateSection(TEXT("bio"), [](UWorld* World, FJsonObject& Out) { const UDescentBioSubsystem* S = UDescentBioSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } Out.SetNumberField(TEXT("reserve_fu"), Descent::Json::Round(S->ReserveFU)); Out.SetNumberField(TEXT("cap_fu"), DBio::CapFU); Out.SetNumberField(TEXT("port_refused"), S->PortRefused); Out.SetNumberField(TEXT("feeds"), S->Feeds); Out.SetNumberField(TEXT("last_used_fu"), Descent::Json::Round(S->LastUsedFU)); Out.SetBoolField(TEXT("jack_active"), S->IsJackActive()); Out.SetNumberField(TEXT("jack_t"), Descent::Json::Round(FMath::Max(0.0, S->JackTime()))); Out.SetNumberField(TEXT("pipe_pieces"), S->PipePieces()); Out.SetNumberField(TEXT("piped_fu"), Descent::Json::Round(S->PipedFU)); Out.SetNumberField(TEXT("hand_fu"), Descent::Json::Round(S->HandFU)); Out.SetNumberField(TEXT("sleeps"), S->Sleeps); });