#include "Player/DescentPlayerController.h" #include "Player/DescentCharacter.h" #include "Player/DescentInteraction.h" #include "Player/DescentVitals.h" #include "Systems/DescentBuild.h" #include "Systems/DescentCargo.h" #include "Systems/DescentCraft.h" #include "Player/DescentTools.h" #include "Systems/DescentSonar.h" #include "Core/DescentBoot.h" #include "Core/DescentEnvQuery.h" #include "Core/DescentGameClasses.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 "Debug/DescentStateRegistry.h" #include "Vehicles/CarrierCarousel.h" #include "Vehicles/CarrierHelm.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "World/DescentShear.h" #include "DescentUnits.h" #include "TheDescent.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Camera/PlayerCameraManager.h" #include "EnhancedInputComponent.h" #include "EnhancedInputSubsystems.h" #include "EnhancedPlayerInput.h" #include "Engine/LocalPlayer.h" #include "Engine/World.h" #include "GameFramework/CharacterMovementComponent.h" #include "InputAction.h" #include "InputMappingContext.h" namespace { namespace T = Descent::Tune::Player; /** Section 2.7: the HUD band name of a depth. */ const TCHAR* PlayerBandName(double DepthM) { if (DepthM < T::BandOpenSeaM) { return TEXT(""); } if (DepthM < T::BandThroatFloorM) { return TEXT("THE THROAT"); } if (DepthM < T::BandHoneycombM) { return TEXT("THROAT FLOOR"); } if (DepthM < T::BandSeamM) { return TEXT("THE HONEYCOMB"); } if (DepthM < T::BandBelowSurveyM) { return TEXT("HONEYCOMB · SEAM INTRUSION"); } return TEXT("BELOW SURVEY"); } bool PlayerModeInTender(EDescentMode M) { return M == EDescentMode::Docked || M == EDescentMode::Launch || M == EDescentMode::Sub || M == EDescentMode::Dock; } } ADescentPlayerController::ADescentPlayerController() { // Enhanced Input needs its own player input class and input component; the project ini does not set them, so the controller does. OverridePlayerInputClass = UEnhancedPlayerInput::StaticClass(); bShowMouseCursor = false; ModeHistory.Add(Mode); } ADescentCharacter* ADescentPlayerController::GetDescentCharacter() const { return Cast(GetPawn()); } UCarrierCarouselComponent* ADescentPlayerController::GetCarousel() const { const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); return C ? C->Carousel.Get() : nullptr; } bool ADescentPlayerController::IsUiOpen() const { const UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld()); return Ui && Ui->IsWindowOpen(); } void ADescentPlayerController::BindKey(UEnhancedInputComponent* EIC, UInputMappingContext* Ctx, const FKey& Key, bool FKeys::* Member) { UInputAction* A = NewObject(this); A->ValueType = EInputActionValueType::Boolean; Ctx->MapKey(A, Key); Actions.Add(A); TSharedRef K = Keys; EIC->BindActionValueLambda(A, ETriggerEvent::Triggered, [K, Member](const FInputActionValue&) { (*K).*Member = true; }); EIC->BindActionValueLambda(A, ETriggerEvent::Completed, [K, Member](const FInputActionValue&) { (*K).*Member = false; }); EIC->BindActionValueLambda(A, ETriggerEvent::Canceled, [K, Member](const FInputActionValue&) { (*K).*Member = false; }); } void ADescentPlayerController::BindPress(UEnhancedInputComponent* EIC, UInputMappingContext* Ctx, const FKey& Key, TFunction Fn) { UInputAction* A = NewObject(this); A->ValueType = EInputActionValueType::Boolean; Ctx->MapKey(A, Key); Actions.Add(A); EIC->BindActionValueLambda(A, ETriggerEvent::Started, [Fn](const FInputActionValue&) { Fn(); }); } void ADescentPlayerController::SetupInputComponent() { if (!InputComponent) { InputComponent = NewObject(this, UEnhancedInputComponent::StaticClass(), TEXT("PC_InputComponent0")); } Super::SetupInputComponent(); UEnhancedInputComponent* EIC = Cast(InputComponent); if (!EIC) { UE_LOG(LogDescent, Error, TEXT("Player: the input component is not an UEnhancedInputComponent; no input will work")); return; } MappingContext = NewObject(this); UInputMappingContext* Ctx = MappingContext; BindKey(EIC, Ctx, EKeys::W, &FKeys::Forward); BindKey(EIC, Ctx, EKeys::S, &FKeys::Back); BindKey(EIC, Ctx, EKeys::A, &FKeys::Left); BindKey(EIC, Ctx, EKeys::D, &FKeys::Right); BindKey(EIC, Ctx, EKeys::SpaceBar, &FKeys::Jump); BindKey(EIC, Ctx, EKeys::C, &FKeys::Down); BindKey(EIC, Ctx, EKeys::LeftControl, &FKeys::Down); BindKey(EIC, Ctx, EKeys::LeftShift, &FKeys::Sprint); BindKey(EIC, Ctx, EKeys::LeftMouseButton, &FKeys::Primary); BindKey(EIC, Ctx, EKeys::RightMouseButton, &FKeys::Secondary); BindKey(EIC, Ctx, EKeys::Left, &FKeys::LookLeft); BindKey(EIC, Ctx, EKeys::Right, &FKeys::LookRight); BindKey(EIC, Ctx, EKeys::Up, &FKeys::LookUp); BindKey(EIC, Ctx, EKeys::Down, &FKeys::LookDown); // One-shot keys (section 5.7). They act through the same functions the debug commands call. BindKey(EIC, Ctx, EKeys::E, &FKeys::Use); BindKey(EIC, Ctx, EKeys::LeftAlt, &FKeys::Alt); BindPress(EIC, Ctx, EKeys::E, [this]() { PressUse(); }); // M7 build mode (section 6.10): B menu, LMB place, RMB or Esc cancel, Q / E / wheel rotate, Alt free placement BindPress(EIC, Ctx, EKeys::B, [this]() { UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld()); const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (!Build || Mode != EDescentMode::Foot || IsUiOpen() || (Cargo && Cargo->Carried())) { return; } if (Build->IsGhosting()) { Build->CancelGhost(); } else { Build->OpenMenu(); } }); BindPress(EIC, Ctx, EKeys::RightMouseButton, [this]() { if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { Build->CancelGhost(); } }); BindPress(EIC, Ctx, EKeys::Q, [this]() { if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { if (Build->IsGhosting()) { Build->RotateGhost(-1); } } }); BindPress(EIC, Ctx, EKeys::MouseScrollUp, [this]() { if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { if (Build->IsGhosting()) { Build->RotateGhost(1); } } }); BindPress(EIC, Ctx, EKeys::MouseScrollDown, [this]() { if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { if (Build->IsGhosting()) { Build->RotateGhost(-1); } } }); // M9: R pings the sonar of whatever the player controls BindPress(EIC, Ctx, EKeys::R, [this]() { UDescentSonarSubsystem* Sonar = UDescentSonarSubsystem::Get(GetWorld()); FString Why; if (Sonar && !IsUiOpen() && !Sonar->PingFromPlayer(&Why)) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(Why); } } }); // the hotbar: 1 to 9 static const FKey Digits[9] = { EKeys::One, EKeys::Two, EKeys::Three, EKeys::Four, EKeys::Five, EKeys::Six, EKeys::Seven, EKeys::Eight, EKeys::Nine }; for (int32 D = 0; D < 9; ++D) { BindPress(EIC, Ctx, Digits[D], [this, D]() { if (UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(GetWorld())) { if (!IsUiOpen()) { Craft->SelectHotbar(D + 1); } } }); } BindPress(EIC, Ctx, EKeys::F, [this]() { ToggleLight(); }); BindPress(EIC, Ctx, EKeys::SpaceBar, [this]() { if (Mode == EDescentMode::Docked && !IsUiOpen()) { Launch(); } }); BindPress(EIC, Ctx, EKeys::LeftMouseButton, [this]() { UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld()); if (Mode == EDescentMode::Title) { SetMode(EDescentMode::Foot); } else if (Build && Build->IsGhosting() && !IsUiOpen()) { FString Why; if (!Build->ConfirmGhost(&Why)) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(FString::Printf(TEXT("Cannot place here: %s"), *Why), EDescentHudToastKind::Warn); } } } else if (!IsUiOpen() && (Mode == EDescentMode::Foot || Mode == EDescentMode::Swim)) { // the hands: a carried box goes down, otherwise the selected hotbar item is used (M8: flare, beacon, flashlight) const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (Cargo && Cargo->Carried()) { PutDownCarried(); } else if (UDescentToolsSubsystem* Tools = UDescentToolsSubsystem::Get(GetWorld())) { Tools->UseSelected(GetDescentCharacter(), Mode == EDescentMode::Swim); } } }); BindPress(EIC, Ctx, EKeys::G, [this]() { PutDownCarried(); }); // T: drill bit <-> laser head, once the Laser Cutter exists (section 4.5) BindPress(EIC, Ctx, EKeys::T, [this]() { if (Mode == EDescentMode::Sub && !IsUiOpen() && Tender.IsValid() && Tender->Flight->Upgrades.bLaser) { Tender->Flight->bLaserHead = !Tender->Flight->bLaserHead; } }); // the helm: V next camera, X all stop (section 5.7) BindPress(EIC, Ctx, EKeys::V, [this]() { if (Mode == EDescentMode::Helm && !IsUiOpen()) { if (ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld())) { K->SetHelmView(K->GetHelmView() + 1); } } }); BindPress(EIC, Ctx, EKeys::X, [this]() { if (Mode == EDescentMode::Helm && !IsUiOpen()) { if (ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld())) { K->Helm.Throttle = 0.0; } } }); BindPress(EIC, Ctx, EKeys::Tab, [this]() { OpenUi(TEXT("pda")); }); BindPress(EIC, Ctx, EKeys::M, [this]() { OpenUi(TEXT("map")); }); BindPress(EIC, Ctx, EKeys::Escape, [this]() { UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld()); if (Build && Build->IsGhosting()) { Build->CancelGhost(); } else { OpenUi(TEXT("pause")); } }); BindPress(EIC, Ctx, EKeys::P, [this]() { OpenUi(TEXT("pause")); }); BindPress(EIC, Ctx, EKeys::H, [this]() { OpenUi(TEXT("pause")); }); // the mouse: Mouse2D delivers the movement of this frame (X right, Y up) UInputAction* Look = NewObject(this); Look->ValueType = EInputActionValueType::Axis2D; Ctx->MapKey(Look, EKeys::Mouse2D); Actions.Add(Look); TSharedRef K = Keys; EIC->BindActionValueLambda(Look, ETriggerEvent::Triggered, [K](const FInputActionValue& V) { K->Mouse += V.Get(); }); } void ADescentPlayerController::BeginPlay() { Super::BeginPlay(); if (ULocalPlayer* LP = GetLocalPlayer()) { if (UEnhancedInputLocalPlayerSubsystem* Sub = LP->GetSubsystem()) { if (MappingContext) { Sub->AddMappingContext(MappingContext, 0); } } } SetInputMode(FInputModeGameOnly()); // Section 4.6: the canopy frame and the dash are close to the eye. ConsoleCommand(FString::Printf(TEXT("r.SetNearClipPlane %.0f"), T::NearClipCm), false); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { // The controller's tick (input -> live intent) runs before the sim latches it. Sim->GetSimTickFunction().AddPrerequisite(this, PrimaryActorTick); FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Player"); FocusStep = Sim->RegisterLateStep(EDescentLateStep::InteractionFocus, [this](const FDescentSimFrame&) { UpdateFocus(); }, Opt); Opt.RateHz = 10.0; // section 1.4: HUD model at 10 Hz HudStep = Sim->RegisterLateStep(EDescentLateStep::HudModel, [this](const FDescentSimFrame&) { WriteHudModel(); }, Opt); // Section 1.4 step 3: launch / dock / board animations. After the carousel (SubOrder), so a finished hoist move is seen at once. FDescentSimRegisterOptions ModeOpt; ModeOpt.Owner = TEXT("PlayerModes"); ModeOpt.SubOrder = 10; ModeStep = Sim->RegisterStep(EDescentSimStep::Carousel, [this](const FDescentSimFrame& F) { StepModes(F.Dt); }, ModeOpt); } } void ADescentPlayerController::EndPlay(const EEndPlayReason::Type Reason) { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(FocusStep); Sim->Unregister(HudStep); Sim->Unregister(ModeStep); } Super::EndPlay(Reason); } void ADescentPlayerController::SetMode(EDescentMode NewMode) { if (Mode == NewMode) { return; } UE_LOG(LogDescent, Display, TEXT("Player: mode %s -> %s"), DescentModeName(Mode), DescentModeName(NewMode)); Mode = NewMode; ModeTime = 0.0; ModeHistory.Add(NewMode); } bool ADescentPlayerController::OpenUi(const FString& WindowName) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld()); if (!Ui || Mode == EDescentMode::Title || Mode == EDescentMode::Dead || Ui->IsWindowOpen()) { return false; // an open window closes itself on its own keys (the window host has the keyboard then) } return Ui->OpenByName(WindowName); } void ADescentPlayerController::PlayerTick(float DeltaTime) { Super::PlayerTick(DeltaTime); // processes the input stack: the lambdas above fill Keys UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); if (!Sim) { return; } FKeys& K = *Keys; FDescentIntent I; const bool bTakesInput = Mode == EDescentMode::Foot || Mode == EDescentMode::Swim || Mode == EDescentMode::Sub || Mode == EDescentMode::Docked || Mode == EDescentMode::Helm; // Section 5.6: while a window is open or the game is paused, move and look are ignored (the mode underneath is kept). if (bTakesInput && !IsUiOpen() && !Sim->IsPaused() && DeltaTime > 1e-5f) { I.Move.X = (K.Forward ? 1.0 : 0.0) - (K.Back ? 1.0 : 0.0); I.Move.Y = (K.Right ? 1.0 : 0.0) - (K.Left ? 1.0 : 0.0); I.Move.Z = (K.Jump ? 1.0 : 0.0) - (K.Down ? 1.0 : 0.0); I.bJump = K.Jump; I.bDown = K.Down; I.bSprint = K.Sprint; I.bPrimary = K.Primary; I.bSecondary = K.Secondary; // In the Tender the mouse steers at 0.102 degrees per pixel (the foot sensitivity x 0.85, section 4.3). const double Sens = T::MouseDegPerPixel * (Mode == EDescentMode::Sub ? Descent::Tune::Tender::SteerScale : 1.0); I.Look.X = K.Mouse.X * Sens / DeltaTime + ((K.LookRight ? 1.0 : 0.0) - (K.LookLeft ? 1.0 : 0.0)) * T::ArrowLookDps; I.Look.Y = K.Mouse.Y * Sens / DeltaTime + ((K.LookUp ? 1.0 : 0.0) - (K.LookDown ? 1.0 : 0.0)) * T::ArrowLookDps; } K.Mouse = FVector2D::ZeroVector; Sim->SetLiveIntent(I); } FVector ADescentPlayerController::RefPointM() const { // Section 5.9: the Tender origin in every Tender mode (and when dead by implosion), the swimmer while swimming, otherwise the eye. const ADescentCharacter* C = GetDescentCharacter(); const AKazahTender* Tn = Tender.Get(); if (Tn && (PlayerModeInTender(Mode) || (Mode == EDescentMode::Board && C && C->IsSeated()) || (Mode == EDescentMode::Dead && bDeadByImplosion))) { return Descent::UUToMetres(Tn->GetActorLocation()); } if (Mode == EDescentMode::Helm) { if (const ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld())) { return Descent::UUToMetres(K->GetActorLocation()); } // every helm view: the carrier origin } if (!C) { return FVector::ZeroVector; } return Descent::UUToMetres(Mode == EDescentMode::Swim ? C->GetActorLocation() : C->EyeWorldUU()); } void ADescentPlayerController::UpdateFocus() { ADescentCharacter* C = GetDescentCharacter(); if (!C) { return; } if (Mode == EDescentMode::Foot) { C->Interaction->UpdateFocus(C->EyeWorldUU(), C->EyeDirection()); } else { C->Interaction->ClearFocus(); } } FString ADescentPlayerController::UsePrompt() const { const ADescentCharacter* C = GetDescentCharacter(); const UCarrierCarouselComponent* Car = GetCarousel(); switch (Mode) { case EDescentMode::Foot: if (const UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld())) { if (Build->IsGhosting()) { return TEXT("LMB Place Q / E Rotate RMB Cancel"); } } return (C && !C->Interaction->GetFocusLabel().IsEmpty()) ? TEXT("E ") + C->Interaction->GetFocusLabel() : FString(); case EDescentMode::Docked: return TEXT("SPACE Launch E Leave the Tender"); case EDescentMode::Sub: return (Car && Tender.IsValid() && Car->CanDock(Tender.Get())) ? TEXT("E Dock in the moon pool") : FString(); case EDescentMode::Swim: if (CanClimb()) { return TEXT("E Climb the ladder"); } if (const AKazahTender* Tn = FindEnterableTender()) { return FString::Printf(TEXT("E Enter %s %s"), Tn->Flight->Spec().Id, Tn->Flight->Spec().Name); } return FString(); default: return FString(); } } void ADescentPlayerController::WriteHudModel() { UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld()); if (!Hud) { return; } FDescentHudModel& M = Hud->Model(); M.Mode = DescentModeName(Mode); M.bUiWindowOpen = IsUiOpen(); M.bCrosshair = Mode == EDescentMode::Foot || Mode == EDescentMode::Swim; const ADescentCharacter* C = GetDescentCharacter(); M.Prompt = UsePrompt(); const FVector Ref = RefPointM(); M.DepthM = Descent::DepthOfZ(Ref.Z); M.DepthText = FDescentHudModel::FormatDepth(M.DepthM); M.Band = PlayerBandName(M.DepthM); const bool bDry = Descent::IsDry(GetWorld(), Descent::MetresToUU(Ref)); M.TemperatureC = Descent::AmbientTemperatureAt(GetWorld(), Descent::MetresToUU(Ref)); M.TemperatureLabel = bDry ? TEXT("CABIN") : TEXT("WATER"); M.TemperatureText = FDescentHudModel::FormatTemperature(M.TemperatureC, M.TemperatureLabel); // Vitals (section 5.8): O2 only shows while swimming. bool bLowO2 = false, bDrowning = false; if (C && C->Vitals) { const FDescentVitalsState& V = C->Vitals->State; M.Vitals.Health = static_cast(V.Health); M.Vitals.Nutrition = static_cast(V.Nutrition); M.Vitals.O2Seconds = static_cast(V.O2); M.Vitals.O2MaxSeconds = static_cast(V.O2Max); M.Vitals.bShowO2 = Mode == EDescentMode::Swim; bLowO2 = Mode == EDescentMode::Swim && V.O2 < T::O2LowSec; bDrowning = V.bDrowning; } // The vehicle: rating under the depth, the panel values, the warnings. One source: the Tender's dash model (section 4.6). const AKazahTender* Tn = Tender.Get(); const bool bInTender = Tn && (PlayerModeInTender(Mode) || Mode == EDescentMode::Board); bool bCrush = false, bHull = false, bPower = false; if (bInTender) { const FTenderDashModel D = Tn->Flight->MakeDashModel(); // the copies the UI keeps of the same models (state part `ui.cargo_display`, the on-screen dash) if (UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld())) { Ui->Models().Dash = D; Ui->Models().Cargo = Tn->Flight->MakeCargoModel(); } M.RatingM = D.RatingM; M.RatingText = FString::Printf(TEXT("RATING %s"), *FDescentHudModel::FormatDepth(D.RatingM)); M.Vehicle.Title = D.Title; M.Vehicle.HullPct = D.HullPct; M.Vehicle.PowerPct = D.PowerPct; M.Vehicle.HoldUsed = D.HoldUsed; M.Vehicle.HoldCap = D.HoldCap; M.Vehicle.SpeedMps = D.SpeedMps; M.Vehicle.ThrottlePct = static_cast(100.0 * FMath::Min(1.0, Tn->Flight->Thrust)); // The HUD panel is for when no dashboard is on screen (section 8.5): in the cockpit the dash shows the same model, so the panel // only appears while the pilot looks away from it in the cradle. M.Vehicle.bVisible = Mode == EDescentMode::Docked && FMath::Abs(Tn->GetCockpitLook().X) > Descent::Tune::Tender::HudPanelLookYawDeg; M.CompassHeadingDeg = static_cast(FMath::Fmod(Tn->GetActorRotation().Yaw + 360.0, 360.0)); bCrush = Tn->Flight->IsOut() && D.DepthM > D.RatingM - Descent::Tune::Tender::CrushWarnAboveM; bHull = D.HullPct < Descent::Tune::Tender::HullRedPct; bPower = D.PowerPct < Descent::Tune::Tender::PowerWarnPct; } else { M.RatingM = 0.0; M.RatingText.Reset(); // No vehicle: the panel is cleared, not left with the last Tender's numbers (after a respawn it said hull 0 while the // reprinted Tender had 100; fix list after M3). M.Vehicle = FDescentHudVehiclePanel(); M.Vehicle.bVisible = false; M.CompassHeadingDeg = static_cast(FMath::Fmod(GetControlRotation().Yaw + 360.0, 360.0)); } // The helm (section 3.6): aim reticle, proximity panel and (outside the bridge view, where the dash is) the vehicle panel. const ADescentCarrier* HelmCarrier = Mode == EDescentMode::Helm ? UDescentCarrierSubsystem::GetCarrier(GetWorld()) : nullptr; M.bHelmReticle = HelmCarrier != nullptr; M.bShowProximity = HelmCarrier && HelmCarrier->GetHelmView() != 0; M.Proximity.Reset(); if (HelmCarrier) { const FCarrierHelmState& H = HelmCarrier->Helm; M.HelmAimNormalized = FVector2D(FRotator::NormalizeAxis(H.AimYawDeg - H.YawDeg) / Descent::Tune::Helm::AimYawLimitDeg, (H.AimPitchDeg - H.PitchDeg) / Descent::Tune::Helm::AimPitchLimitDeg); const FTenderDashModel D = HelmCarrier->MakeDashModel(); M.RatingM = D.RatingM; M.RatingText = FString::Printf(TEXT("RATING %s"), *FDescentHudModel::FormatDepth(D.RatingM)); M.Vehicle.Title = D.Title; M.Vehicle.HullPct = D.HullPct; M.Vehicle.PowerPct = D.PowerPct; M.Vehicle.HoldUsed = D.HoldUsed; M.Vehicle.HoldCap = D.HoldCap; M.Vehicle.SpeedMps = D.SpeedMps; M.Vehicle.ThrottlePct = static_cast(100.0 * H.Throttle); M.Vehicle.bVisible = HelmCarrier->GetHelmView() != 0; M.CompassHeadingDeg = static_cast(FMath::Fmod(H.YawDeg + 360.0, 360.0)); for (const ADescentCarrier::FProximityRow& Row : HelmCarrier->Proximity) { FDescentHudProximityRow R; R.Label = Row.Label; R.DistanceM = static_cast(Row.DistanceM); R.bWarning = Row.DistanceM < 4.0; M.Proximity.Add(R); } bHull = H.Hull < 30.0; } FDescentHudWarnings& W = M.Warnings; W.bShear = Descent::World::FDescentShear::IsWarning(Ref); // from Z +10, 15 m before any push (section 2.11) W.bCrush = bCrush; W.bHull = bHull; W.bPower = bPower; W.bLowO2 = bLowO2; W.bHunger = false; W.bNearSoftLimit = W.bShear; // U7: the diegetic warning well before the current pushes if (bCrush && bInTender && Tn->Flight->DepthM() > Tn->Flight->Stats().Rating) { W.Text = TEXT("!! CRUSH DEPTH - HULL FAILING"); } else if (bDrowning) { W.Text = TEXT("DROWNING"); } else if (W.bShear) { W.Text = TEXT("SHEAR CURRENT — ASCENT NOT AUTHORISED"); } else if (bCrush) { W.Text = TEXT("APPROACHING CRUSH DEPTH"); } else if (bLowO2) { W.Text = TEXT("OXYGEN LOW"); } else if (bInTender && Tn->Flight->bInHonestVoid) { W.Text = TEXT("PRESSURE ANOMALY - HULL FAILING"); } else if (bHull) { W.Text = TEXT("HULL DAMAGE"); } else if (bPower) { W.Text = TEXT("POWER LOW"); } else { W.Text.Reset(); } } // ---- boot: the character on the bridge, then the first mode (section 1.5 step 5) --------------------------------------- DESCENT_BOOT_STEP(PlaceCharacter, Descent::Boot::Order::PlaceCharacter) { APlayerController* PC0 = Ctx.World->GetFirstPlayerController(); ADescentCharacter* C = Cast(PC0 ? PC0->GetPawn() : nullptr); if (!C) { Ctx.Fail(TEXT("no ADescentCharacter possessed by the player controller (is another pawn class registered at a higher priority?)")); return; } // carrier-local (40.5, 0, 1.5) facing the bow (v2) C->PlaceOnCarrier(FVector(41.8, 0.0, Descent::Tune::Carrier::DeckFloorZ3), 0.0, 0.0); // v2 says 40.5, but the chart table (radius 1.1 at x 39.5) reaches 40.6 } DESCENT_BOOT_STEP(EnterFirstMode, Descent::Boot::Order::EnterFirstMode) { ADescentPlayerController* PC = Cast(Ctx.World->GetFirstPlayerController()); if (!PC) { Ctx.Fail(TEXT("no ADescentPlayerController")); return; } UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(Ctx.World); if (Ui) { // The pause window's key list (section 5.7), and the title card's DIVE. TArray& Keys = Ui->Models().Pause.Keys; Keys.Reset(); const TCHAR* const List[][2] = { { TEXT("W A S D"), TEXT("Walk, swim, thrust; helm: lever and thrusters") }, { TEXT("Space / C"), TEXT("Jump, rise / sink, ballast") }, { TEXT("Mouse"), TEXT("Look; steer the Tender and the carrier") }, { TEXT("Shift"), TEXT("Sprint, boost") }, { TEXT("E"), TEXT("Use, board, dock, leave, carry, feed") }, { TEXT("Hold E"), TEXT("Pick up a placed object") }, { TEXT("Space"), TEXT("Launch from the cradle") }, { TEXT("LMB / RMB"), TEXT("Drill / suction; place / cancel; use item") }, { TEXT("G"), TEXT("Put the box down") }, { TEXT("R"), TEXT("Sonar ping") }, { TEXT("T"), TEXT("Drill / laser head") }, { TEXT("F"), TEXT("Flashlight, headlamp") }, { TEXT("B"), TEXT("Build menu; Q E wheel turn, Alt free") }, { TEXT("1 - 9"), TEXT("Hotbar (PDA: assign)") }, { TEXT("V / X"), TEXT("Helm: camera / all stop") }, { TEXT("Tab / M"), TEXT("PDA / map") }, { TEXT("Esc, P, H"), TEXT("Pause, save") } }; for (const auto& Row : List) { Keys.Add({ Row[0], Row[1] }); } TWeakObjectPtr WeakPC(PC); TWeakObjectPtr WeakUi(Ui); Ui->Models().Title.OnDive = [WeakPC, WeakUi]() { if (UDescentUiSubsystem* U = WeakUi.Get()) { U->Close(); } if (ADescentPlayerController* P = WeakPC.Get()) { if (P->GetMode() == EDescentMode::Title) { P->SetMode(EDescentMode::Foot); } } }; Ui->NotifyChanged(); } if (Ctx.bScenarioRunning) { PC->SetMode(EDescentMode::Foot); } else { // A normal start: the title card (section 1.5 step 5); DIVE or a click goes to Foot. PC->SetMode(EDescentMode::Title); if (Ui && Ui->HasWidgets()) { Ui->Open(EDescentWindow::Title); } UE_LOG(LogDescent, Display, TEXT("Player: normal start, mode Title, title card %s"), (Ui && Ui->GetWindow() == EDescentWindow::Title) ? TEXT("open") : TEXT("NOT open (no UI widgets)")); } PC->ClearModeHistory(); } // ---- state section `player` (section 9.4) ------------------------------------------------------------------------------- static FDescentStateProviderRegistrar GPlayerStateSection(TEXT("player"), [](UWorld* World, FJsonObject& Out) { using namespace Descent::Json; const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; const ADescentCharacter* C = PC ? PC->GetDescentCharacter() : nullptr; Out.SetBoolField(TEXT("available"), C != nullptr); if (!C) { return; } Out.SetStringField(TEXT("mode"), DescentModeName(PC->GetMode())); TArray> History; FString HistoryText; for (EDescentMode M : PC->GetModeHistory()) { History.Add(MakeShared(DescentModeName(M))); HistoryText += (HistoryText.IsEmpty() ? TEXT("") : TEXT(">")) + FString(DescentModeName(M)); } Out.SetArrayField(TEXT("mode_history"), History); Out.SetStringField(TEXT("mode_sequence"), HistoryText); Out.SetBoolField(TEXT("ui_open"), PC->IsUiOpen()); { const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World); if (Cargo && Cargo->Carried()) { Out.SetNumberField(TEXT("carrying"), Cargo->Carried()->BoxId); } else { Out.SetField(TEXT("carrying"), MakeShared()); } } const FVector Feet = C->GetFeetLocalM(); Out.SetField(TEXT("local_m"), Vec3(Feet)); Out.SetField(TEXT("world_m"), Vec3(Descent::UUToMetres(C->GetFeetWorldUU()))); const Descent::Carrier::FRoomDef* Room = Descent::Carrier::FLayout::Get().RoomAt(Feet + FVector(0, 0, 0.3)); Out.SetStringField(TEXT("room"), Room ? Room->Id.ToString() : FString()); const FVector Ref = PC->RefPointM(); Out.SetField(TEXT("ref_point_m"), Vec3(Ref)); Out.SetNumberField(TEXT("ref_depth_m"), Round(Descent::DepthOfZ(Ref.Z))); Out.SetStringField(TEXT("focus"), C->Interaction->GetFocusLabel()); Out.SetBoolField(TEXT("flashlight"), C->IsFlashlightOn()); Out.SetBoolField(TEXT("seated"), C->IsSeated()); Out.SetBoolField(TEXT("swimming"), C->IsSwimming()); Out.SetStringField(TEXT("tender"), PC->GetTender() ? PC->GetTender()->Flight->Spec().Id : TEXT("")); const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(World); const FVector V = Carrier ? Carrier->GetActorTransform().InverseTransformVectorNoScale(C->GetVelocity()) : C->GetVelocity(); Out.SetField(TEXT("velocity_local_mps"), Vec3(V / 100.0)); Out.SetField(TEXT("velocity_world_mps"), Vec3(C->GetVelocity() / 100.0)); Out.SetBoolField(TEXT("on_ground"), C->GetCharacterMovement()->IsMovingOnGround()); Out.SetStringField(TEXT("base"), C->GetMovementBaseObject() ? C->GetMovementBaseObject()->GetName() : FString()); Out.SetNumberField(TEXT("view_yaw_local_deg"), Round(C->GetViewYawLocalDeg())); Out.SetNumberField(TEXT("view_pitch_deg"), Round(FRotator::NormalizeAxis(C->GetControlRotation().Pitch))); Out.SetNumberField(TEXT("rescues"), C->RescueCount); TSharedRef W = MakeShared(); W->SetBoolField(TEXT("active"), C->GetAutoWalk().bActive); W->SetNumberField(TEXT("waypoints_left"), C->GetAutoWalk().bActive ? C->GetAutoWalk().Waypoints.Num() - C->GetAutoWalk().Index : 0); Out.SetObjectField(TEXT("autowalk"), W); }); DESCENT_REGISTER_GAME_CLASS(PlayerController, 100, ADescentPlayerController)