// The mode state machine of the player controller (DESIGN_UE.md section 5.6), the M3 part: // // Foot -E Tender at BOARD-> Board -(1.4 s)-> Docked -Space-> Launch -(3.2 s)-> Sub // Docked -E-> Foot (at the pool rim) Sub -E under the pool-> Dock -(4.2 s)-> Docked // Sub -E elsewhere-> Swim -E near a free Tender-> Sub // Foot -through the pool gap-> Swim -E at the ladder-> Ladder -(1.6 s)-> Foot // drown / implode -> Dead -(2.6 s)-> respawn -> Foot (galley) // // Keys and debug commands call the same functions. The timed modes advance in the sim (step 3, after the carousel). #include "Player/DescentPlayerController.h" #include "Player/DescentCharacter.h" #include "Player/DescentCharacterMovement.h" #include "Player/DescentInteraction.h" #include "Player/DescentVitals.h" #include "Life/DescentFauna.h" #include "Systems/DescentBuild.h" #include "Systems/DescentCraft.h" #include "Systems/DescentCargo.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Core/Tuning/DescentTune_Player.h" #include "Core/Tuning/DescentTune_Tender.h" #include "Vehicles/CarrierCarousel.h" #include "Vehicles/CarrierHelm.h" #include "Vehicles/CarrierHullShape.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "DescentUnits.h" #include "TheDescent.h" #include "UI/DescentHudSubsystem.h" #include "Camera/PlayerCameraManager.h" #include "Engine/World.h" namespace { namespace MT = Descent::Tune::Player; namespace MTender = Descent::Tune::Tender; bool ModesRefuse(FString* OutWhyNot, const TCHAR* Why) { if (OutWhyNot) { *OutWhyNot = Why; } return false; } } void ADescentPlayerController::SeatIn(AKazahTender* T, float BlendSeconds) { Tender = T; if (ADescentCharacter* C = GetDescentCharacter()) { C->SetSeatedIn(T); } T->SetCockpitLook(0.0, 0.0); SetViewTargetWithBlend(T, BlendSeconds, VTBlend_Cubic); } void ADescentPlayerController::Unseat() { ADescentCharacter* C = GetDescentCharacter(); if (C) { C->SetSeatedIn(nullptr); SetViewTarget(C); } } bool ADescentPlayerController::BeginBoard(AKazahTender* T, FString* OutWhyNot) { const UCarrierCarouselComponent* Car = GetCarousel(); if (Mode != EDescentMode::Foot) { return ModesRefuse(OutWhyNot, TEXT("only on foot")); } if (UDescentCargoSubsystem::Get(GetWorld()) && UDescentCargoSubsystem::Get(GetWorld())->Carried()) { return ModesRefuse(OutWhyNot, TEXT("put the ore box down first")); } if (!T || !Car || Car->TenderAtBoard() != T) { return ModesRefuse(OutWhyNot, TEXT("that Tender does not hang at BOARD")); } Tender = T; bBoardViewStarted = false; T->SetCanopyOpen(true); // 0.5 s, then the camera moves to the seat (0.9 s) LidTender.Reset(); if (ADescentCharacter* C = GetDescentCharacter()) { C->StopAutoWalk(); } SetMode(EDescentMode::Board); return true; } bool ADescentPlayerController::Launch(FString* OutWhyNot) { UCarrierCarouselComponent* Car = GetCarousel(); AKazahTender* T = Tender.Get(); if (Mode != EDescentMode::Docked || !T || !Car) { return ModesRefuse(OutWhyNot, TEXT("only docked in a Tender")); } if (!Car->BeginLaunch(T, OutWhyNot)) { return false; } if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->AbortUnload(); } // decouple: pieces in the pipe keep going, the rest stays in the hold T->SetCockpitLook(0.0, 0.0); T->SetCanopyOpen(false); SetMode(EDescentMode::Launch); return true; } bool ADescentPlayerController::Dock(FString* OutWhyNot) { UCarrierCarouselComponent* Car = GetCarousel(); AKazahTender* T = Tender.Get(); if (Mode != EDescentMode::Sub || !T || !Car) { return ModesRefuse(OutWhyNot, TEXT("only while piloting a Tender")); } if (!Car->BeginDock(T, OutWhyNot)) { return false; } if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld())) { Fleet->Piloted.Reset(); } SetMode(EDescentMode::Dock); return true; } bool ADescentPlayerController::LeaveDocked(FString* OutWhyNot) { AKazahTender* T = Tender.Get(); ADescentCharacter* C = GetDescentCharacter(); if (Mode != EDescentMode::Docked || !T || !C) { return ModesRefuse(OutWhyNot, TEXT("only docked in a Tender")); } T->SetCanopyOpen(true); LidTender = T; LidTimer = MT::LidCloseAfterSec; Tender.Reset(); // PlaceOnCarrier unseats, gives the view back and sets Foot (OnPlacedOnFoot). SetViewTarget(C); C->PlaceOnCarrier(FVector(MT::LadderExitX, MT::LadderExitY, MT::LadderExitZ), MT::LadderExitYawDeg, 0.0); SetMode(EDescentMode::Foot); return true; } bool ADescentPlayerController::ExitToSwim(FString* OutWhyNot) { AKazahTender* T = Tender.Get(); ADescentCharacter* C = GetDescentCharacter(); if (Mode != EDescentMode::Sub || !T || !C) { return ModesRefuse(OutWhyNot, TEXT("only while piloting a Tender")); } // Section 5.3: the roof hatch opens, the swimmer appears at Tender-local (1.2, -2.2, 0.2). The Tender stays where it is (4.7). T->SetHatchOpen(true); LidTender = T; LidTimer = MT::LidCloseAfterSec; T->Flight->State = ETenderState::Abandoned; if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld())) { Fleet->Piloted.Reset(); } const FRotator Level(0.0, T->GetActorRotation().Yaw, 0.0); const FVector Where = T->GetActorLocation() + Level.RotateVector(Descent::MetresToUU(FVector(MTender::SwimmerExitX, MTender::SwimmerExitY, MTender::SwimmerExitZ))); Unseat(); C->StartSwimming(&Where); SetControlRotation(FRotator(0.0, T->GetActorRotation().Yaw, 0.0)); Tender.Reset(); SetMode(EDescentMode::Swim); return true; } AKazahTender* ADescentPlayerController::FindEnterableTender() const { const ADescentCharacter* C = GetDescentCharacter(); const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld()); AKazahTender* Best = nullptr; double BestDist = MTender::EnterRangeM; if (C && Fleet) { for (const TWeakObjectPtr& W : Fleet->All()) { AKazahTender* T = W.Get(); if (!T || T->Flight->State != ETenderState::Abandoned) { continue; } const double D = Descent::UUToMetres(FVector::Dist(T->GetActorLocation(), C->GetActorLocation())); if (D <= BestDist) { BestDist = D; Best = T; } } } return Best; } bool ADescentPlayerController::EnterTender(AKazahTender* T, bool bIgnoreRange, FString* OutWhyNot) { if (Mode != EDescentMode::Swim) { return ModesRefuse(OutWhyNot, TEXT("only while swimming")); } if (!T) { T = FindEnterableTender(); } if (!T || T->Flight->State != ETenderState::Abandoned) { return ModesRefuse(OutWhyNot, TEXT("no free Tender out in the water")); } if (!bIgnoreRange && T != FindEnterableTender()) { const ADescentCharacter* C = GetDescentCharacter(); if (!C || Descent::UUToMetres(FVector::Dist(T->GetActorLocation(), C->GetActorLocation())) > MTender::EnterRangeM) { return ModesRefuse(OutWhyNot, TEXT("the Tender is more than 4.5 m away")); } } T->SetHatchOpen(true); LidTender = T; LidTimer = MT::LidCloseAfterSec; SeatIn(T, static_cast(MT::EnterViewBlendSec)); T->Flight->State = ETenderState::Piloted; T->Flight->SyncFromActor(); if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld())) { Fleet->Piloted = T; } SetMode(EDescentMode::Sub); return true; } bool ADescentPlayerController::EnterSwim(const FVector* WorldUU) { ADescentCharacter* C = GetDescentCharacter(); if (!C || (Mode != EDescentMode::Foot && Mode != EDescentMode::Swim)) { return false; } C->StartSwimming(WorldUU); SetMode(EDescentMode::Swim); return true; } bool ADescentPlayerController::PoolDrop(FString* OutWhyNot) { ADescentCharacter* C = GetDescentCharacter(); const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (Mode != EDescentMode::Foot || !C || !Carrier) { return ModesRefuse(OutWhyNot, TEXT("only on foot")); } // Into the pool below the ladder gap; the eased surface rule of the swim step settles the head under the water. const FVector Where = Carrier->ToWorldUU(FVector(MT::PoolDropX, MT::PoolDropY, MT::PoolDropFeetZ + MT::CapsuleHalfHeightM)); return EnterSwim(&Where); } bool ADescentPlayerController::CanClimb() const { // Section 5.3: E in the shaft, within 1.6 m of the water surface and y < 5.3. const ADescentCharacter* C = GetDescentCharacter(); const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (Mode != EDescentMode::Swim || !C || !Carrier) { return false; } namespace CT = Descent::Tune::Carrier; const FVector Head = Carrier->ToLocalM(C->EyeWorldUU()); return Head.X >= CT::PoolX0 && Head.X <= CT::PoolX1 && Head.Y >= CT::PoolY0 && Head.Y < MT::LadderMaxY && Head.Z <= CT::PoolWaterZ + 0.5 && Head.Z >= CT::PoolWaterZ - MT::LadderRangeM; } bool ADescentPlayerController::Climb(bool bIgnoreRange, FString* OutWhyNot) { ADescentCharacter* C = GetDescentCharacter(); if (Mode != EDescentMode::Swim || !C) { return ModesRefuse(OutWhyNot, TEXT("only while swimming")); } if (!bIgnoreRange && !CanClimb()) { return ModesRefuse(OutWhyNot, TEXT("not at the ladder (in the shaft, within 1.6 m of the surface, y < 5.3)")); } C->SetFrozen(true); SetMode(EDescentMode::Ladder); return true; } bool ADescentPlayerController::EnterHelm(FString* OutWhyNot) { ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld()); ADescentCharacter* C = GetDescentCharacter(); if (Mode != EDescentMode::Foot || !K || !C) { return ModesRefuse(OutWhyNot, TEXT("only on foot")); } if (UDescentCargoSubsystem::Get(GetWorld()) && UDescentCargoSubsystem::Get(GetWorld())->Carried()) { return ModesRefuse(OutWhyNot, TEXT("put the ore box down first")); } C->SetHeldBy(K); K->SetHelmHeld(true); K->SetHelmView(0); SetViewTarget(K); SetMode(EDescentMode::Helm); return true; } bool ADescentPlayerController::LeaveHelm() { ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld()); ADescentCharacter* C = GetDescentCharacter(); if (Mode != EDescentMode::Helm || !K || !C) { return false; } K->SetHelmHeld(false); // all stop (BRIEF section 3: leaving the helm brings the carrier to a stop) C->SetHeldBy(nullptr); SetViewTarget(C); SetMode(EDescentMode::Foot); return true; } void ADescentPlayerController::PressUse() { if (IsUiOpen()) { return; } switch (Mode) { case EDescentMode::Foot: if (ADescentCharacter* C = GetDescentCharacter()) { UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld()); if (Build && Build->IsGhosting()) { Build->RotateGhost(1); } // E turns the ghost else if (Cast(C->Interaction->GetFocus())) { // a placed object: used when E is released, picked up when E is held for 0.8 s UseHoldTarget = C->Interaction->GetFocus(); UseHoldTime = 0.0; } else { C->UseFocus(); } } break; case EDescentMode::Docked: LeaveDocked(); break; case EDescentMode::Helm: LeaveHelm(); break; case EDescentMode::Sub: if (!Dock()) { ExitToSwim(); } // dock under the pool, leave to swim anywhere else break; case EDescentMode::Swim: if (!Climb() && !EnterTender(nullptr)) { // M8 (U3): fauna within reach is taken by hand and carried to the vat UDescentFaunaSubsystem* Fauna = UDescentFaunaSubsystem::Get(GetWorld()); UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(GetWorld()); const FName Got = Fauna ? Fauna->TakeByHand(RefPointM()) : NAME_None; if (!Got.IsNone() && Craft) { Craft->AddItem(Got, 1); if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(TEXT("Lantern polyp taken (0.4 FU at the vat)"), EDescentHudToastKind::Good); } } } break; default: break; } } void ADescentPlayerController::PutDownCarried() { UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); ADescentCharacter* C = GetDescentCharacter(); if (Mode != EDescentMode::Foot || IsUiOpen() || !Cargo || !C || !Cargo->Carried()) { return; } FString Why; if (!Cargo->PutDown(C, &Why)) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(Why, EDescentHudToastKind::Warn); } } } void ADescentPlayerController::ToggleLight() { if (IsUiOpen()) { return; } if (Mode == EDescentMode::Foot || Mode == EDescentMode::Swim) { if (ADescentCharacter* C = GetDescentCharacter()) { C->SetFlashlight(!C->IsFlashlightOn()); } } else if (Mode == EDescentMode::Sub) { if (AKazahTender* T = Tender.Get()) { T->SetHeadlight(!T->Flight->bLamp); } } } void ADescentPlayerController::OnPlacedOnFoot() { if (Mode == EDescentMode::Title || Mode == EDescentMode::Dead) { return; } if (Mode != EDescentMode::Foot) { // A debug placement out of a Tender mode leaves the Tender as it is: one that was being flown is abandoned, not teleported. if (AKazahTender* T = Tender.Get()) { if (T->Flight->State == ETenderState::Piloted) { T->Flight->State = ETenderState::Abandoned; } } if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld())) { Fleet->Piloted.Reset(); } Tender.Reset(); if (ADescentCharacter* C = GetDescentCharacter()) { SetViewTarget(C); } SetMode(EDescentMode::Foot); } } void ADescentPlayerController::OnPlayerDied() { if (Mode == EDescentMode::Dead) { return; } ADescentCharacter* C = GetDescentCharacter(); const FString Reason = (C && C->Vitals) ? C->Vitals->State.DeadReason : FString(); bDeadByImplosion = Reason == TEXT("implode"); if (C && !C->IsSeated()) { C->SetFrozen(true); } if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld())) { Fleet->Piloted.Reset(); } if (PlayerCameraManager) { PlayerCameraManager->StartCameraFade(0.f, 1.f, static_cast(MT::DeathFadeSec), FLinearColor::Black, false, true); } if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().SetRadio(bDeadByImplosion ? TEXT("Tethys: hull lost. A replacement is being printed. 4,800 credits are added to your account.") : TEXT("Tethys: operator recovered. 900 credits are added to your account."), 8.f); } UE_LOG(LogDescent, Display, TEXT("Player: died (%s)"), *Reason); SetMode(EDescentMode::Dead); } void ADescentPlayerController::Respawn() { ADescentCharacter* C = GetDescentCharacter(); UCarrierCarouselComponent* Car = GetCarousel(); if (bDeadByImplosion && Car && Tender.IsValid()) { // Section 5.8: that Tender's hold and bio-tank are lost; a replacement hull is printed into the free cradle, full hull, 60 % power. Car->ReprintTender(Tender.Get()); } Tender.Reset(); bDeadByImplosion = false; if (C) { if (C->Vitals) { C->Vitals->State.Respawn(); } SetViewTarget(C); // PlaceOnCarrier unseats and unfreezes; the mode is set below (OnPlacedOnFoot leaves Dead alone). C->PlaceOnCarrier(FVector(MT::RespawnX, MT::RespawnY, MT::RespawnZ), 0.0, 0.0); } if (PlayerCameraManager) { PlayerCameraManager->StartCameraFade(1.f, 0.f, 0.8f, FLinearColor::Black, false, false); } SetMode(EDescentMode::Foot); } void ADescentPlayerController::StepModes(double Dt) { ModeTime += Dt; UCarrierCarouselComponent* Car = GetCarousel(); UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(GetWorld()); const UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); ADescentCharacter* C = GetDescentCharacter(); AKazahTender* T = Tender.Get(); // M7: build mode lives on foot only; E on a placed object is a press or a hold if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { if (Build->IsGhosting() && Mode != EDescentMode::Foot) { Build->CancelGhost(); } Build->bFreePlacement = Keys->Alt; if (ADescentPlaceable* P = Cast(UseHoldTarget.Get())) { if (Mode != EDescentMode::Foot || IsUiOpen() || !C) { UseHoldTarget.Reset(); } else if (!Keys->Use) { UseHoldTarget.Reset(); P->Use(C); } else if ((UseHoldTime += Dt) >= 0.8) { UseHoldTarget.Reset(); FString Why; const FString Name = P->KitId.ToString(); UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld()); if (Build->PickUp(P, &Why)) { if (Hud) { Hud->Model().PushToast(FString::Printf(TEXT("Picked up: %s kit"), *Name), EDescentHudToastKind::Good); } } else if (Hud) { Hud->Model().PushToast(Why, EDescentHudToastKind::Warn); } } } } // a lid opened for leaving or entering closes again if (LidTender.IsValid()) { LidTimer -= Dt; if (LidTimer <= 0.0) { LidTender->SetCanopyOpen(false); LidTender->SetHatchOpen(false); LidTender.Reset(); } } switch (Mode) { case EDescentMode::Board: if (!T) { SetMode(EDescentMode::Foot); break; } if (!bBoardViewStarted && ModeTime >= MTender::CanopyOpenSec) { bBoardViewStarted = true; SeatIn(T, static_cast(MTender::BoardCameraSec)); } if (ModeTime >= MTender::CanopyOpenSec + MTender::BoardCameraSec) { T->SetCanopyOpen(false); SetMode(EDescentMode::Docked); } break; case EDescentMode::Docked: if (T && Sim) { // Section 4.6: in the cradle the mouse looks around the cockpit. const FDescentIntent& I = Sim->GetIntent(); const FVector2D L = T->GetCockpitLook(); T->SetCockpitLook(L.X + I.Look.X * Dt, L.Y + I.Look.Y * Dt); } break; case EDescentMode::Helm: if (ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld())) { if (Sim && K->IsHelmHeld() && !K->Autopilot.bOn) { // U4: the mouse moves the aim point (0.05 / 0.03 degrees per pixel; the intent is a rate at 0.12 degrees per pixel); // W / S the lever, A / D the lateral thrusters, Space / C the ballast. namespace HT = Descent::Tune::Helm; const FDescentIntent& I = Sim->GetIntent(); K->Helm.AddAim(I.Look.X * (HT::AimYawDegPerPixel / MT::MouseDegPerPixel) * Dt, I.Look.Y * (HT::AimPitchDegPerPixel / MT::MouseDegPerPixel) * Dt); K->Helm.Throttle = FMath::Clamp(K->Helm.Throttle + I.Move.X * HT::LeverPerSec * Dt, HT::ThrottleMin, HT::ThrottleMax); K->Helm.Strafe = I.Move.Y; K->Helm.Ballast = I.Move.Z; } } break; case EDescentMode::Launch: if (T && T->Flight->State == ETenderState::Piloted) { if (Fleet) { Fleet->Piloted = T; } SetMode(EDescentMode::Sub); } break; case EDescentMode::Sub: // Rising into the moon pool is docking (see Tune::Tender::AutoDockAboveZ): the pilot is never held under the open mouth by nothing. if (T && Car && Sim && Car->CanDock(T)) { namespace CT = Descent::Tune::Carrier; const ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld()); const FVector L = K->ToLocalM(T->GetActorLocation()); const bool bUnderMouth = L.X >= CT::PoolX0 && L.X <= CT::PoolX1 && L.Y >= CT::PoolY0 && L.Y <= CT::PoolY1 && L.Z > MTender::AutoDockAboveZ; const bool bRising = Sim->GetIntent().Move.Z > 0.1 || (Sim->GetIntent().Move.X > 0.1 && T->Flight->PitchDeg > 20.0); if (bUnderMouth && bRising) { Dock(); } } break; case EDescentMode::Dock: if (T && T->Flight->State == ETenderState::Cradle) { SetMode(EDescentMode::Docked); } break; case EDescentMode::Ladder: if (ModeTime >= MT::LadderSec && C) { C->PlaceOnCarrier(FVector(MT::LadderExitX, MT::LadderExitY, MT::LadderExitZ), MT::LadderExitYawDeg - 180.0, 0.0); SetMode(EDescentMode::Foot); } break; case EDescentMode::Dead: if (ModeTime >= MT::RespawnAfterSec) { Respawn(); } break; default: break; } // Hull 0 (section 4.3): the pilot dies with it ("implode"); a Tender nobody sits in is simply printed again. if (Fleet && Car) { for (const TWeakObjectPtr& W : Fleet->All()) { AKazahTender* F = W.Get(); if (!F || !F->Flight->bImploded) { continue; } const bool bMine = F == Tender.Get() && (Mode == EDescentMode::Sub || Mode == EDescentMode::Launch || Mode == EDescentMode::Dock); if (bMine) { F->Flight->bImploded = false; // handled once; the hull stays at 0 until the reprint at respawn F->Flight->State = ETenderState::Abandoned; if (C && C->Vitals) { C->Vitals->State.Die(TEXT("implode")); } OnPlayerDied(); } else if (Mode != EDescentMode::Dead || F != Tender.Get()) { Car->ReprintTender(F); } } } }