// Milestone M6 scenarios (DESIGN_UE.md section 10, "Pilot the carrier"): m6_helm_speed (U9), m6_helm_mouse (U4), m6_scrape (V12), m6_walk_moving. #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Debug/DescentDebugSubsystem.h" #include "Debug/DescentScenario.h" #include "Player/DescentCharacter.h" #include "Player/DescentInteraction.h" #include "Player/DescentPlayerController.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiShot.h" #include "Vehicles/CarrierHelm.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/DescentCarrier.h" #include "World/DescentWorldScenarioSteps.h" #include "DescentUnits.h" #include "TheDescent.h" namespace { ADescentPlayerController* M6PC(const FDescentScenarioContext& C) { return C.World ? Cast(C.World->GetFirstPlayerController()) : nullptr; } ADescentCharacter* M6Char(const FDescentScenarioContext& C) { ADescentPlayerController* PC = M6PC(C); return PC ? PC->GetDescentCharacter() : nullptr; } ADescentCarrier* M6Carrier(const FDescentScenarioContext& C) { return UDescentCarrierSubsystem::GetCarrier(C.World); } const TCHAR* const M6Void = TEXT("Descent.Carrier.Teleport 640 -230 -700 0"); // the middle of the Great Void: no wall within reach void M6TakeHelm(FDescentScenarioBuilder& S) { // on foot to the helm console, E S.Exec(TEXT("Descent.Player.PlaceLocal 42.6 0 1.5 0")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Look 0 -12")); S.WaitSim(0.4); S.Expect(TEXT("the prompt offers the helm"), [](FDescentScenarioContext& C) { const FString L = M6Char(C)->Interaction->GetFocusLabel(); UE_LOG(LogDescent, Display, TEXT("M6: focus '%s'"), *L); return L.Contains(TEXT("helm")); }); S.Exec(TEXT("Descent.Player.Use")); S.Expect(TEXT("mode Helm, the helm is held, the view is the carrier's bridge camera"), [](FDescentScenarioContext& C) { const ADescentPlayerController* PC = M6PC(C); return PC->GetMode() == EDescentMode::Helm && M6Carrier(C)->IsHelmHeld() && PC->GetViewTarget() == M6Carrier(C) && M6Carrier(C)->GetHelmView() == 0; }); } void M6HudShot(FDescentScenarioBuilder& S, const FString& Label) { struct FSt { int32 Phase = 0; bool bDone = false; Descent::Ui::FUiShotResult R; }; TSharedRef St = MakeShared(); S.WaitFrames(8); S.Until(FString::Printf(TEXT("HudShot(%s)"), *Label), [St, Label](FDescentScenarioContext& C) { if (St->Phase == 0) { TSharedRef Keep = St; if (!Descent::Ui::RequestUiShot(C.ScenarioName + TEXT("_") + Label, [Keep](const Descent::Ui::FUiShotResult& R) { Keep->R = R; Keep->bDone = true; }) && !St->bDone) { C.Fail(TEXT("UI shot refused")); return true; } St->Phase = 1; return false; } if (!St->bDone) { return false; } if (!St->R.bOk) { C.Fail(St->R.Error); } return true; }); } struct FM6Track { double T0 = -1.0, A = 0.0, B = 0.0, Max = 0.0, Min = 1e9, At = -1.0; FVector P = FVector::ZeroVector; bool bFlag = false; }; } DESCENT_SCENARIO(m6_helm_speed, "U9: from rest with throttle 1 the carrier makes at least 1.80 m/s after 4.0 s and 1.95 - 2.05 m/s after 10 s, never more than 2.05; all stop: 0.2 m/s or less within 4 s.") { S.TimeLimit(240.f); S.Exec(M6Void); S.Exec(TEXT("Descent.Carrier.Helm 1")); S.Exec(TEXT("Descent.Carrier.Throttle 1")); TSharedRef Tr = MakeShared(); S.Until(TEXT("10 s at full throttle"), [Tr](FDescentScenarioContext& C) { const double Now = C.Sim()->SimTime(), V = M6Carrier(C)->VelocityMps().Size(); if (Tr->T0 < 0.0) { Tr->T0 = Now; } Tr->Max = FMath::Max(Tr->Max, V); if (Tr->At < 0.0 && Now - Tr->T0 >= 4.0) { Tr->At = V; } Tr->B = V; return Now - Tr->T0 >= 10.0; }); S.Expect(TEXT("speed_mps >= 1.80 at 4.0 s; 1.95 - 2.05 at 10 s; never above 2.05"), [Tr](FDescentScenarioContext& C) { UE_LOG(LogDescent, Display, TEXT("M6 speed: %.3f m/s at 4 s, %.3f m/s at 10 s, largest %.3f m/s, contacts %d"), Tr->At, Tr->B, Tr->Max, M6Carrier(C)->Helm.Contacts); return Tr->At >= 1.80 && Tr->B >= 1.95 && Tr->B <= 2.05 && Tr->Max <= 2.05; }); S.Exec(TEXT("Descent.Carrier.Throttle 0")); S.WaitSim(4.0); S.Expect(TEXT("all stop: <= 0.2 m/s within 4 s"), [](FDescentScenarioContext& C) { UE_LOG(LogDescent, Display, TEXT("M6 speed: %.3f m/s 4 s after all stop"), M6Carrier(C)->VelocityMps().Size()); return M6Carrier(C)->VelocityMps().Size() <= 0.2; }); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m6_helm_mouse, "U4: at the helm the mouse moves an aim point and the heavy hull follows: +30 degrees gives 5 - 20 degrees after 3 s, 30 +- 1 within 14 s, never faster than 5.8 deg/s; -8 degrees of pitch at speed within 8 s. Shot helm_aim.") { S.TimeLimit(300.f); M6TakeHelm(S); S.Exec(M6Void); S.WaitSim(0.5); // the mouse itself (the intent path): one second of mouse to the right moves the aim, not yet the hull S.Exec(TEXT("Descent.Player.Intent 0 0 0 48 0 1 0 0 0")); S.WaitSim(1.1); S.Expect(TEXT("the mouse moved the aim point 20 degrees (0.05 degrees per pixel); the hull has barely begun to turn"), [](FDescentScenarioContext& C) { const FCarrierHelmState& H = M6Carrier(C)->Helm; UE_LOG(LogDescent, Display, TEXT("M6 mouse: after 1 s of mouse the aim is %.2f deg ahead of the start, the hull at %.2f deg"), H.AimYawDeg, H.YawDeg); return FMath::Abs(H.AimYawDeg - 20.0) < 1.5 && H.YawDeg < 4.0; }); S.Exec(TEXT("Descent.Carrier.AimDelta 10 0")); M6HudShot(S, TEXT("helm_aim")); TSharedRef Tr = MakeShared(); S.Until(TEXT("the hull follows the aim"), [Tr](FDescentScenarioContext& C) { const FCarrierHelmState& H = M6Carrier(C)->Helm; const double Now = C.Sim()->SimTime(); if (Tr->T0 < 0.0) { Tr->T0 = Now; } Tr->Max = FMath::Max(Tr->Max, FMath::Abs(M6Carrier(C)->YawRateDps())); if (Tr->At < 0.0 && FMath::Abs(H.YawDeg - 30.0) <= 1.0) { Tr->At = Now - Tr->T0 + 1.5; } // + the 1.5 s since the aim began to move return Now - Tr->T0 >= 16.0; }); S.Expect(TEXT("yaw 30 +- 1 within 14 s, yaw_rate_dps <= 5.8 always, the reticle is on the HUD"), [Tr](FDescentScenarioContext& C) { const FDescentHudModel& M = UDescentHudSubsystem::Get(C.World)->Model(); UE_LOG(LogDescent, Display, TEXT("M6 mouse: yaw within 1 degree of 30 after %.1f s, largest yaw rate %.3f deg/s, final yaw %.2f, reticle %d"), Tr->At, Tr->Max, M6Carrier(C)->Helm.YawDeg, M.bHelmReticle); return Tr->At > 0.0 && Tr->At <= 14.0 && Tr->Max <= 5.8 && M.bHelmReticle; }); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 1 0 0 0")); // W: the lever goes up 1.5 per second S.WaitSim(4.0); S.Expect(TEXT("W pushed the lever to full, the carrier is at speed"), [](FDescentScenarioContext& C) { return M6Carrier(C)->Helm.Throttle >= 0.99 && M6Carrier(C)->VelocityMps().Size() > 1.5; }); S.Exec(TEXT("Descent.Carrier.AimDelta 0 -8")); TSharedRef Pt = MakeShared(); S.Until(TEXT("pitch follows"), [Pt](FDescentScenarioContext& C) { const double Now = C.Sim()->SimTime(), P = M6Carrier(C)->GetActorRotation().Pitch; if (Pt->T0 < 0.0) { Pt->T0 = Now; } Pt->Min = FMath::Min(Pt->Min, P); Pt->Max = FMath::Max(Pt->Max, P); return Now - Pt->T0 >= 8.0; }); S.Expect(TEXT("pitch_deg -8 +- 1 within 8 s, never beyond +-10"), [Pt](FDescentScenarioContext& C) { const double P = M6Carrier(C)->GetActorRotation().Pitch; UE_LOG(LogDescent, Display, TEXT("M6 mouse: pitch %.2f deg after 8 s (lowest %.2f, highest %.2f)"), P, Pt->Min, Pt->Max); return FMath::Abs(P + 8.0) <= 1.0 && Pt->Min >= -10.0 && Pt->Max <= 10.0; }); // E leaves the helm: all stop S.Exec(TEXT("Descent.Player.Use")); S.WaitSim(6.0); S.Expect(TEXT("E: on foot again, the helm released, the carrier coming to a stop"), [](FDescentScenarioContext& C) { UE_LOG(LogDescent, Display, TEXT("M6 mouse: 6 s after leaving the helm %.3f m/s, throttle %.2f"), M6Carrier(C)->VelocityMps().Size(), M6Carrier(C)->Helm.Throttle); return M6PC(C)->GetMode() == EDescentMode::Foot && !M6Carrier(C)->IsHelmHeld() && M6Carrier(C)->VelocityMps().Size() < 0.1 && !M6Char(C)->IsSeated(); }); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m6_scrape, "V12: driven into the canyon wall the carrier touches rock (contacts > 0), the hull falls, scrape > 0; the five helm cameras and the proximity panel. Shots helm_bridge, helm_chase, helm_topdown, helm_bow, helm_keel.") { S.TimeLimit(420.f); DescentWorldScn::WaitStreamIdle(S, 60.f); M6TakeHelm(S); S.Shot(TEXT("helm_bridge")); S.Exec(TEXT("Descent.Carrier.Throttle 1")); TSharedRef Tr = MakeShared(); S.Until(TEXT("steer into the wall"), [Tr](FDescentScenarioContext& C) { ADescentCarrier* K = M6Carrier(C); const double Now = C.Sim()->SimTime(); if (Tr->T0 < 0.0) { Tr->T0 = Now; Tr->P = K->Helm.PosM; } K->Helm.AddAim(2.0, 0.0); // keep the aim hard over to starboard K->Helm.Throttle = 1.0; Tr->Max = FMath::Max(Tr->Max, K->Helm.Scrape); if (K->Helm.Contacts > 0) { Tr->bFlag = true; Tr->A += 1.0; } if (Tr->bFlag && Tr->A > 90.0) { return true; } // three seconds against the rock if (Now - Tr->T0 > 240.0) { C.Fail(TEXT("no rock touched in 240 s")); return true; } return false; }); S.Expect(TEXT("contacts > 0, hull fell, scrape > 0"), [Tr](FDescentScenarioContext& C) { const FCarrierHelmState& H = M6Carrier(C)->Helm; UE_LOG(LogDescent, Display, TEXT("M6 scrape: moved %.1f m to (%.1f, %.1f, %.1f) yaw %.1f in %.0f s; contacts %d, hull %.2f, largest scrape %.3f"), FVector::Dist(H.PosM, Tr->P), H.PosM.X, H.PosM.Y, H.PosM.Z, H.YawDeg, C.Sim()->SimTime() - Tr->T0, H.Contacts, H.Hull, Tr->Max); return Tr->bFlag && H.Hull < 100.0 && Tr->Max > 0.0; }); S.Exec(TEXT("Descent.Sim.Pause 1")); const TCHAR* Names[4] = { TEXT("helm_chase"), TEXT("helm_topdown"), TEXT("helm_bow"), TEXT("helm_keel") }; for (int32 V = 1; V <= 4; ++V) { S.Exec(FString::Printf(TEXT("Descent.Carrier.View %d"), V)); S.WaitFrames(8); S.Expect(FString::Printf(TEXT("view %d: the proximity panel and the vehicle panel are on the HUD"), V), [](FDescentScenarioContext& C) { const FDescentHudModel& M = UDescentHudSubsystem::Get(C.World)->Model(); return M.bShowProximity && M.Proximity.Num() == 6 && M.Vehicle.bVisible && M.Vehicle.Title == TEXT("CANTICLE"); }); DescentWorldScn::ShotAllowDark(S, Names[V - 1]); } M6HudShot(S, TEXT("helm_keel_hud")); S.Exec(TEXT("Descent.Sim.Pause 0")); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m6_walk_moving, "The carrier at 2 m/s, turning, pitched 8 degrees: standing still for 5 s moves player.local_m by less than 0.02 m; the walk from the bridge into the corridor succeeds; the room is never lost.") { S.TimeLimit(300.f); S.Exec(M6Void); S.Exec(TEXT("Descent.Player.Place bridge")); S.Exec(TEXT("Descent.Carrier.Helm 1")); S.Exec(TEXT("Descent.Carrier.Throttle 1")); S.Exec(TEXT("Descent.Carrier.AimDelta 40 -8")); S.WaitSim(8.0); TSharedRef Tr = MakeShared(); S.Until(TEXT("stand still for 5 s while the carrier turns"), [Tr](FDescentScenarioContext& C) { ADescentCarrier* K = M6Carrier(C); K->Helm.AddAim(1.0, 0.0); // keep turning const FVector L = M6Char(C)->GetFeetLocalM(); const double Now = C.Sim()->SimTime(); if (Tr->T0 < 0.0) { Tr->T0 = Now; Tr->P = L; } Tr->Max = FMath::Max(Tr->Max, FVector::Dist(L, Tr->P)); Tr->A = FMath::Max(Tr->A, FMath::Abs(K->GetActorRotation().Pitch)); return Now - Tr->T0 >= 5.0; }); S.Expect(TEXT("standing: player.local_m moved < 0.02 m at 2 m/s, turning, pitched 8 degrees"), [Tr](FDescentScenarioContext& C) { const ADescentCarrier* K = M6Carrier(C); UE_LOG(LogDescent, Display, TEXT("M6 walk: carrier %.2f m/s, yaw rate %.2f deg/s, pitch %.2f deg (largest %.2f); the standing player drifted %.4f m"), K->VelocityMps().Size(), K->YawRateDps(), K->GetActorRotation().Pitch, Tr->A, Tr->Max); return Tr->Max < 0.02 && K->VelocityMps().Size() > 1.8 && FMath::Abs(K->YawRateDps()) > 1.0 && Tr->A > 7.0; }); S.Exec(TEXT("Descent.Player.MoveTo corr3")); TSharedRef W = MakeShared(); S.Until(TEXT("walk bridge to corridor"), [W](FDescentScenarioContext& C) { M6Carrier(C)->Helm.AddAim(1.0, 0.0); const ADescentCharacter* P = M6Char(C); if (!Descent::Carrier::FLayout::Get().RoomAt(P->GetFeetLocalM() + FVector(0, 0, 0.3))) { W->bFlag = true; } return !P->GetAutoWalk().bActive; }); S.Expect(TEXT("arrived in the corridor; player.room never became null; no rescue"), [W](FDescentScenarioContext& C) { const ADescentCharacter* P = M6Char(C); const Descent::Carrier::FRoomDef* Room = Descent::Carrier::FLayout::Get().RoomAt(P->GetFeetLocalM() + FVector(0, 0, 0.3)); UE_LOG(LogDescent, Display, TEXT("M6 walk: arrived in %s, room lost on the way %d, rescues %d"), Room ? *Room->Id.ToString() : TEXT("(none)"), W->bFlag, P->RescueCount); return Room && Room->Id == TEXT("corr3") && !W->bFlag && P->RescueCount == 0; }); S.DumpState(TEXT("end")); S.Quit(); }