// Scenario of the player's body (Player/Body): the Paragon Twinblast character as the first-person body of ADescentCharacter. // player_body first person on foot (idle, looking down, walking, in a jump, against a wall), swimming, at the helm, in a Tender, // the old-arm switch, and third-person debug shots of the whole man (front, side, back, legs, arms; guns on for one). #include "Debug/DescentScenarioUtil.h" #include "Core/Tuning/DescentTune_Body.h" #include "Core/Tuning/DescentTune_Player.h" #include "Player/Arm/DescentArmViewmodelComponent.h" #include "Player/Body/DescentBodyAnim.h" #include "Player/Body/DescentBodyProfile.h" #include "Player/Body/DescentPlayerBodyComponent.h" #include "Player/DescentMode.h" #include "Camera/CameraComponent.h" #include "Components/PointLightComponent.h" #include "Components/SkeletalMeshComponent.h" #include "Engine/PointLight.h" namespace { UDescentPlayerBodyComponent* BodyScnBody(const FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); return Ch ? Ch->Body.Get() : nullptr; } const FDescentBodyAnimState* BodyScnState(const FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* Body = BodyScnBody(C); const UDescentBodyAnimInstance* Anim = Body ? Body->GetAnim() : nullptr; return Anim ? &Anim->GetState() : nullptr; } FString BodyScnMotion(const FDescentScenarioContext& C) { const FDescentBodyAnimState* S = BodyScnState(C); return S ? FString(Descent::Body::MotionName(S->Motion())) : FString(TEXT("none")); } TWeakObjectPtr GBodyScnLamp; /** * A plain white lamp for the third-person pictures, so colours and shapes can be judged (the rooms' own lamps are dim and tinted). * One picture (tp_front_room_light) is taken without it: that is what the room alone does. * It lights only what the body is lit by: channel 2 in dry air (nothing else in a room is on it), channels 0 and 1 in the water. */ void BodyScnLamp(FDescentScenarioContext& C, bool bOn, const FVector& AtWorldUU = FVector::ZeroVector) { APointLight* Lamp = GBodyScnLamp.Get(); if (!Lamp && bOn && C.World) { Lamp = C.World->SpawnActor(AtWorldUU, FRotator::ZeroRotator); GBodyScnLamp = Lamp; if (UPointLightComponent* P = Lamp ? Cast(Lamp->GetLightComponent()) : nullptr) { P->SetMobility(EComponentMobility::Movable); P->bUseInverseSquaredFalloff = false; P->SetLightFalloffExponent(2.f); P->SetAttenuationRadius(800.f); P->SetCastShadows(false); P->SetLightColor(FLinearColor::White); P->SetIntensity(1.0f * Descent::Tune::Carrier::LampBrightnessUnit); P->MarkRenderStateDirty(); } } if (!Lamp) { return; } if (bOn) { Lamp->SetActorLocation(AtWorldUU); const UDescentPlayerBodyComponent* Body = BodyScnBody(C); const bool bDry = !Body || Body->IsInDryAir(); Lamp->GetLightComponent()->SetLightingChannels(!bDry, !bDry, bDry); } Lamp->GetLightComponent()->SetVisibility(bOn); } /** * A picture of the man from outside: the scenario camera at an offset in HIS frame (metres from the feet: x ahead of him, y to his * right, z up), looking at a point above his feet. bLamp adds the white lamp at the camera. */ void BodyScnOutside(FDescentScenarioBuilder& S, const FString& Label, const FVector& OffsetM, double LookUpM, float Fov, bool bLamp, int32 WarmFrames = 10, float WarmSec = 0.75f) { S.Do(TEXT("camera for ") + Label, [OffsetM, LookUpM, Fov, bLamp](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); if (!Ch) { C.Fail(TEXT("no character")); return; } const FVector Feet = Ch->GetFeetWorldUU(); const FTransform Frame(FRotator(0.0, Ch->GetActorRotation().Yaw, 0.0), Feet); const FVector Cam = Frame.TransformPosition(OffsetM * 100.0); const FVector Look = Feet + FVector(0.0, 0.0, LookUpM * 100.0); C.Debug->PlaceCamera(Cam / 100.0, (Look - Cam).Rotation(), Fov); // beside and above the camera, not on its axis: a lamp at the lens makes every glossy surface that faces it white BodyScnLamp(C, bLamp, Cam + (Look - Cam).Rotation().RotateVector(FVector(0.0, 130.0, 90.0))); }); S.Shot(Label, WarmFrames, WarmSec); S.Do(TEXT("lamp off"), [](FDescentScenarioContext& C) { BodyScnLamp(C, false); }); } /** Six pictures in a row of a motion that goes on (no hold, almost no warm-up): his own view, or from outside at an offset in his frame. */ void BodyScnHold(FDescentScenarioBuilder& S, bool bHold); void BodyScnStrip(FDescentScenarioBuilder& S, const TCHAR* Label, bool bOutside, const FVector& OffsetM = FVector::ZeroVector, double LookUpM = 0.9, int32 Count = 6, int32 GapFrames = 3) { // A picture takes an unknown number of fixed sim steps (the first strips of this file were not consecutive: the last frames // showed him standing after his intent had run out). So he is held for each picture and let go for GapFrames between two. for (int32 I = 1; I <= Count; ++I) { const FString L = FString::Printf(TEXT("%s_%d"), Label, I); BodyScnHold(S, true); if (bOutside) { BodyScnOutside(S, L, OffsetM, LookUpM, 50.f, true, 2, 0.f); Descent::Scn::ViewPlayer(S); } else { S.Shot(L, 2, 0.f); } BodyScnHold(S, false); S.WaitFrames(GapFrames); } } /** Holds the character still (movement and animation) while a picture is taken, or lets him go on. */ void BodyScnHold(FDescentScenarioBuilder& S, bool bHold) { S.Do(bHold ? TEXT("hold the character still for the picture") : TEXT("let the character go on"), [bHold](FDescentScenarioContext& C) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->CustomTimeDilation = bHold ? 0.f : 1.f; } }); } void BodyScnExpectMotion(FDescentScenarioBuilder& S, const TCHAR* Motion, const TCHAR* Clip = nullptr) { const FString Want(Motion), WantClip(Clip ? Clip : TEXT("")); S.Expect(FString::Printf(TEXT("the body's motion is %s%s%s"), Motion, Clip ? TEXT(" on ") : TEXT(""), Clip ? Clip : TEXT("")), [Want, WantClip](FDescentScenarioContext& C) { const FDescentBodyAnimState* St = BodyScnState(C); if (!St) { return false; } const FString Loco = Descent::Body::SeqAssetName(St->Loco.Cur), Act = Descent::Body::SeqAssetName(St->Act.Cur); UE_LOG(LogDescent, Display, TEXT("player_body: motion %s, speed %.0f cm/s, move %.2f (clip %s, rate %.2f), action %.2f (clip %s)"), *BodyScnMotion(C), St->SpeedCmS, St->MoveAlpha, *Loco, St->LocoRate, St->ActionAlpha, *Act); return BodyScnMotion(C) == Want && (WantClip.IsEmpty() || Loco == WantClip || Act == WantClip); }); } } namespace { /** * What the player's view does while he moves, frame by frame: the numbers behind "is the camera shaky". Sampled after every * actor tick AND the camera update of a frame (FWorldDelegates::OnWorldPostActorTick), so it is exactly what the frame draws: * the camera against the capsule (bob, side, roll), the body's yaw against the view's (a lag shows as jitter when turning), the * chest in the camera's frame (the body against the camera) and where the two hands are on the screen (0..1 of the picture). */ struct FBodyViewProbe { TArray CamZ, CamSide, CamFwd, Roll, YawLag, ChestY, ChestZ, HandLX, HandLY, HandRX, HandRY; int32 HandLIn = 0, HandRIn = 0; double SpeedSum = 0.0, MinSpeedCmS = 0.0; FDelegateHandle Handle; // the lower foot of every frame, in the carrier's frame (cm): what a planted foot does over the deck TArray StanceFoot; TArray StanceAt; TArray StanceDt; /** Mean speed of the planted foot over the ground, cm/s: 0 is a foot that does not skate. Planted: the lower foot, within 3 cm of the lowest it gets. */ double FootSlide(int32* OutFrames = nullptr) const { double Floor = TNumericLimits::Max(), Sum = 0.0; int32 N = 0; for (const FVector& P : StanceAt) { Floor = FMath::Min(Floor, P.Z); } for (int32 I = 1; I < StanceAt.Num(); ++I) { if (StanceFoot[I] == StanceFoot[I - 1] && StanceAt[I].Z <= Floor + 3.0 && StanceAt[I - 1].Z <= Floor + 3.0 && StanceDt[I] > 0.0) { Sum += FVector::Dist2D(StanceAt[I], StanceAt[I - 1]) / StanceDt[I]; ++N; } } if (OutFrames) { *OutFrames = N; } return N > 0 ? Sum / N : -1.0; } static double P2P(const TArray& A) { if (A.Num() == 0) { return 0.0; } double Lo = A[0], Hi = A[0]; for (double V : A) { Lo = FMath::Min(Lo, V); Hi = FMath::Max(Hi, V); } return Hi - Lo; } static double Step(const TArray& A) { double M = 0.0; for (int32 I = 1; I < A.Num(); ++I) { M = FMath::Max(M, FMath::Abs(A[I] - A[I - 1])); } return M; } /** Largest change of the frame-to-frame step: a smooth curve has small ones, a jump or a one-frame lag a large one. */ static double Jerk(const TArray& A) { double M = 0.0; for (int32 I = 2; I < A.Num(); ++I) { M = FMath::Max(M, FMath::Abs(A[I] - 2.0 * A[I - 1] + A[I - 2])); } return M; } void Sample(UWorld* World) { APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const ADescentCharacter* Ch = PC ? Cast(PC->GetPawn()) : nullptr; const USkeletalMeshComponent* Mesh = Ch ? Ch->GetMesh() : nullptr; if (!Ch || !Mesh || !PC->PlayerCameraManager || Ch->GetVelocity().Size2D() < MinSpeedCmS) { return; } const FVector CamLoc = PC->PlayerCameraManager->GetCameraLocation(); const FRotator CamRot = PC->PlayerCameraManager->GetCameraRotation(); const FVector Rel = FRotator(0.0, Ch->GetActorRotation().Yaw, 0.0).UnrotateVector(CamLoc - Ch->GetActorLocation()); CamFwd.Add(Rel.X); CamSide.Add(Rel.Y); CamZ.Add(Rel.Z); Roll.Add(FRotator::NormalizeAxis(CamRot.Roll)); YawLag.Add(FRotator::NormalizeAxis(CamRot.Yaw - Ch->GetActorRotation().Yaw)); const FDescentBodyBones& Bones = FDescentBodyProfile::Get().Bones; if (const ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(World)) { const FVector L = K->ToLocalM(Mesh->GetBoneLocation(Bones.FootL)) * 100.0, R = K->ToLocalM(Mesh->GetBoneLocation(Bones.FootR)) * 100.0; StanceFoot.Add(L.Z <= R.Z ? 0 : 1); StanceAt.Add(L.Z <= R.Z ? L : R); StanceDt.Add(World->GetDeltaSeconds()); } const FVector Chest = FTransform(CamRot, CamLoc).InverseTransformPosition(Mesh->GetBoneLocation(Bones.Chest)); ChestY.Add(Chest.Y); ChestZ.Add(Chest.Z); int32 W = 0, H = 0; PC->GetViewportSize(W, H); auto Hand = [&](const FName& Bone, TArray& X, TArray& Y, int32& In) { FVector2D Px; if (W > 0 && H > 0 && PC->ProjectWorldLocationToScreen(Mesh->GetBoneLocation(Bone), Px, false)) { X.Add(Px.X / W); Y.Add(Px.Y / H); In += (Px.X >= 0.0 && Px.X <= W && Px.Y >= 0.0 && Px.Y <= H) ? 1 : 0; } }; Hand(Bones.HandL, HandLX, HandLY, HandLIn); Hand(Bones.HandR, HandRX, HandRY, HandRIn); SpeedSum += Ch->GetVelocity().Size2D(); } void Report(const TCHAR* Name) const { const int32 N = CamZ.Num(); int32 SlideFrames = 0; const double Slide = FootSlide(&SlideFrames); UE_LOG(LogDescent, Display, TEXT("FOOTSLIDE %s: planted foot over the ground %.1f cm/s (%d frames) at %.0f cm/s"), Name, Slide, SlideFrames, N > 0 ? SpeedSum / N : 0.0); UE_LOG(LogDescent, Display, TEXT("VIEWPROBE %s: frames %d, speed %.0f cm/s | camera vs capsule cm: bob p2p %.2f step %.2f jerk %.2f, side p2p %.2f, fwd p2p %.2f, roll p2p %.2f deg | body yaw behind view: max %.2f deg | chest in camera cm: side p2p %.2f jerk %.2f, up p2p %.2f jerk %.2f | hand L on screen %d/%d x p2p %.3f y p2p %.3f step %.3f | hand R on screen %d/%d x p2p %.3f y p2p %.3f step %.3f"), Name, N, N > 0 ? SpeedSum / N : 0.0, P2P(CamZ), Step(CamZ), Jerk(CamZ), P2P(CamSide), P2P(CamFwd), P2P(Roll), FMath::Max(FMath::Abs(FMath::Max(YawLag)), FMath::Abs(FMath::Min(YawLag))), P2P(ChestY), Jerk(ChestY), P2P(ChestZ), Jerk(ChestZ), HandLIn, HandLX.Num(), P2P(HandLX), P2P(HandLY), FMath::Max(Step(HandLX), Step(HandLY)), HandRIn, HandRX.Num(), P2P(HandRX), P2P(HandRY), FMath::Max(Step(HandRX), Step(HandRY))); } }; /** One measured stretch: place, give the intent, sample every drawn frame for Seconds while he is faster than MinSpeed, report. */ void BodyScnProbe(FDescentScenarioBuilder& S, const TCHAR* Name, const TCHAR* PlaceCmd, const TCHAR* IntentCmd, double Seconds, double MinSpeedCmS, TSharedPtr* Keep = nullptr) { TSharedRef Probe = MakeShared(); Probe->MinSpeedCmS = MinSpeedCmS; if (Keep) { *Keep = Probe; } S.Exec(PlaceCmd); S.Exec(TEXT("Descent.Player.Look 0 0")); S.WaitSim(0.5); S.Exec(IntentCmd); S.WaitSim(0.8); // up to speed first S.Do(FString::Printf(TEXT("view probe %s on"), Name), [Probe](FDescentScenarioContext& C) { Probe->Handle = FWorldDelegates::OnWorldPostActorTick.AddLambda([Probe, World = C.World](UWorld* W, ELevelTick, float) { if (W == World) { Probe->Sample(W); } }); }); S.WaitSim(Seconds); S.Do(FString::Printf(TEXT("view probe %s off"), Name), [Probe, N = FString(Name)](FDescentScenarioContext& C) { FWorldDelegates::OnWorldPostActorTick.Remove(Probe->Handle); Probe->Report(*N); }); S.WaitSim(0.8); } } namespace { /** Where a joint of the body is in the picture (0..1 of it, y down) and in the camera's frame (cm: ahead, right, up). */ struct FBodyJointView { FVector2D Screen = FVector2D(-9.0, -9.0); FVector Cam = FVector::ZeroVector; bool bOn = false; }; FBodyJointView BodyScnJoint(FDescentScenarioContext& C, const FName& Bone) { FBodyJointView Out; APlayerController* PC = C.World ? C.World->GetFirstPlayerController() : nullptr; const ADescentCharacter* Ch = Descent::Scn::Char(C); if (!PC || !Ch || !PC->PlayerCameraManager) { return Out; } const FVector W = Ch->GetMesh()->GetBoneLocation(Bone); Out.Cam = FTransform(PC->PlayerCameraManager->GetCameraRotation(), PC->PlayerCameraManager->GetCameraLocation()).InverseTransformPosition(W); int32 X = 0, Y = 0; PC->GetViewportSize(X, Y); FVector2D Px; if (X > 0 && Y > 0 && PC->ProjectWorldLocationToScreen(W, Px, false)) { Out.Screen = FVector2D(Px.X / X, Px.Y / Y); Out.bOn = Out.Screen.X >= 0.0 && Out.Screen.X <= 1.0 && Out.Screen.Y >= 0.0 && Out.Screen.Y <= 1.0; } return Out; } /** Pitches the view and logs every leg and trunk joint: on the screen or not, where, and where it is from the camera. Line LOOKDOWN. */ /** Want: 0 only the log; 1 both knees and both feet must be in the picture; 2 both feet in its lower half. */ void BodyScnLookDown(FDescentScenarioBuilder& S, const TCHAR* Tag, double PitchDeg, int32 Want = 0) { S.Exec(FString::Printf(TEXT("Descent.Player.Look 0 %.0f"), PitchDeg)); S.WaitFrames(8); S.Do(FString::Printf(TEXT("joints on the screen, %s, pitch %.0f"), Tag, PitchDeg), [T = FString(Tag), PitchDeg, Want](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); if (!Ch) { return; } const FDescentBodyBones& B = FDescentBodyProfile::Get().Bones; const FBodyJointView KL = BodyScnJoint(C, B.KneeL), KR = BodyScnJoint(C, B.KneeR), FL = BodyScnJoint(C, B.FootL), FR = BodyScnJoint(C, B.FootR); if (Want == 1 && !(KL.bOn && KR.bOn && FL.bOn && FR.bOn)) { C.Fail(FString::Printf(TEXT("pitch %.0f: knees and feet are not all in the picture (knees %d %d, feet %d %d)"), PitchDeg, KL.bOn, KR.bOn, FL.bOn, FR.bOn)); } if (Want == 2 && !(FL.bOn && FR.bOn && FL.Screen.Y > 0.5 && FR.Screen.Y > 0.5)) { C.Fail(FString::Printf(TEXT("pitch %.0f: the feet are not both in the lower half of the picture (y %.2f, %.2f)"), PitchDeg, FL.Screen.Y, FR.Screen.Y)); } FString Line; for (const FName& Bone : { B.Neck, B.Chest, B.Pelvis, B.KneeL, B.KneeR, B.FootL, B.FootR, B.HandL, B.HandR }) { const FBodyJointView V = BodyScnJoint(C, Bone); Line += FString::Printf(TEXT(" | %s %s %.2f,%.2f cam %.0f,%.0f,%.0f"), *Bone.ToString(), V.bOn ? TEXT("ON") : TEXT("off"), V.Screen.X, V.Screen.Y, V.Cam.X, V.Cam.Y, V.Cam.Z); } UE_LOG(LogDescent, Display, TEXT("LOOKDOWN %s pitch %.0f: near plane %.1f cm, first-person scale %.2f (on %d), mesh at %s scale %.3f%s"), *T, PitchDeg, GNearClippingPlane, Ch->Camera->FirstPersonScale, Ch->Camera->bEnableFirstPersonScale ? 1 : 0, *Ch->GetMesh()->GetRelativeLocation().ToCompactString(), Ch->GetMesh()->GetRelativeScale3D().Z, *Line); }); } } namespace { void BodyScnSetCvar(FDescentScenarioBuilder& S, const TCHAR* Name, const TCHAR* Value) { S.Do(FString::Printf(TEXT("%s = '%s'"), Name, Value), [N = FString(Name), V = FString(Value)](FDescentScenarioContext& C) { if (IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(*N)) { Var->Set(*V, ECVF_SetByConsole); } else { C.Fail(FString::Printf(TEXT("no console variable %s"), *N)); } }); } void BodyScnOutsideOnly(FDescentScenarioBuilder& S, bool bOn) { S.Do(bOn ? TEXT("the body as any other view sees it") : TEXT("the body as its own view again"), [bOn](FDescentScenarioContext& C) { if (UDescentPlayerBodyComponent* B = BodyScnBody(C)) { B->bForceThirdPerson = bOn; } }); } } DESCENT_SCENARIO(body_run_look, "Sprint clips side by side: for each variant (locomotion as before, and baked roles played as the run) the foot slide (FOOTSLIDE lines) and a strip of six pictures from the side.") { S.TimeLimit(400.f); S.Exec(TEXT("Descent.Body.View body")); Descent::Scn::ViewPlayer(S); struct FVariant { const TCHAR* Name; const TCHAR* OldLoco; const TCHAR* RunRole; }; // (a role that is not baked, e.g. a candidate that left the table, falls back to the model's own set: the clip name in RUNLOOK shows it) // 2026-10-12: compared were the jog (the run of before), FreeSampleAnimationSet's A_RunFwd_Loop and SampleAnimationPack's MM_Run_Fwd, // which won and is the RunFwd row now. To compare another clip: add a row with a new role to Config/DescentBody.json, bake, add a variant here. for (const FVariant& V : { FVariant{ TEXT("before"), TEXT("1"), TEXT("") }, FVariant{ TEXT("run"), TEXT("0"), TEXT("") } }) { BodyScnSetCvar(S, TEXT("descent.Body.DebugOldLoco"), V.OldLoco); BodyScnSetCvar(S, TEXT("descent.Body.DebugRunRole"), V.RunRole); BodyScnOutsideOnly(S, true); BodyScnProbe(S, V.Name, TEXT("Descent.Player.Place bay"), TEXT("Descent.Player.Intent 1 0 0 0 0 2.2 0 0 1"), 1.2, 420.0); BodyScnOutsideOnly(S, false); S.Exec(TEXT("Descent.Player.Place bay")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 30 0 0 1")); S.WaitSim(0.8); S.Expect(FString::Printf(TEXT("%s: sprinting"), V.Name), [](FDescentScenarioContext& C) { const FDescentBodyAnimState* St = BodyScnState(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); if (St && B && B->GetAnim()) { UE_LOG(LogDescent, Display, TEXT("RUNLOOK set %d clip %s rate %.2f speed %.0f"), B->GetAnim()->ActiveSet(), Descent::Body::SeqAssetName(St->Loco.Cur), St->LocoRate, St->SpeedCmS); } return St && St->SpeedCmS > 400.0; }); BodyScnStrip(S, *FString::Printf(TEXT("run_%s"), V.Name), true, FVector(0.5, 3.6, 1.1), 0.9, 6, 2); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(0.5); } BodyScnSetCvar(S, TEXT("descent.Body.DebugOldLoco"), TEXT("0")); BodyScnSetCvar(S, TEXT("descent.Body.DebugRunRole"), TEXT("")); S.Quit(); } DESCENT_SCENARIO(body_view_probe,"Measures the player's view while walking, running and turning (camera bob, body against the camera, hands on the screen): log lines VIEWPROBE.") { S.TimeLimit(300.f); S.Exec(TEXT("Descent.Body.View body")); Descent::Scn::ViewPlayer(S); BodyScnProbe(S, TEXT("walk"), TEXT("Descent.Player.Place bay"), TEXT("Descent.Player.Intent 1 0 0 0 0 2.6"), 1.6, 250.0); BodyScnProbe(S, TEXT("run"), TEXT("Descent.Player.Place bay"), TEXT("Descent.Player.Intent 1 0 0 0 0 2.2 0 0 1"), 1.2, 420.0); BodyScnProbe(S, TEXT("walk_turning"), TEXT("Descent.Player.Place bay"), TEXT("Descent.Player.Intent 1 0 0 70 0 2.6"), 1.6, 200.0); BodyScnProbe(S, TEXT("strafe"), TEXT("Descent.Player.Place bay"), TEXT("Descent.Player.Intent 0 1 0 0 0 2.0"), 1.0, 250.0); // standing, looking down: where the legs are in the picture S.Exec(TEXT("Descent.Player.Place galley")); S.WaitSim(1.0); BodyScnLookDown(S, TEXT("stand"), -45.0); BodyScnLookDown(S, TEXT("stand"), -60.0, 1); BodyScnLookDown(S, TEXT("stand"), -80.0, 2); S.Exec(TEXT("Descent.Player.Look 0 0")); S.Quit(); } DESCENT_SCENARIO(player_body, "The player's body (Paragon Twinblast, red arms, machine legs, no guns): first person on foot idle, looking down, walking, in a jump, against a wall, swimming, at the helm; hidden in a Tender; the old-arm switch; third-person shots of the whole man.") { S.TimeLimit(1500.f); // ---- on foot in the galley (a lit room), the player's own view ---- S.Exec(TEXT("Descent.Body.View body")); S.Exec(TEXT("Descent.Body.ShowGuns 0")); S.Exec(TEXT("descent.Body.AnimSet 1")); S.Exec(TEXT("descent.Body.ArmsUp 0")); S.Exec(TEXT("Descent.Player.Place galley")); S.Do(TEXT("view through the character"), [](FDescentScenarioContext& C) { ADescentCharacter* Ch = Descent::Scn::Char(C); if (!Ch || !Ch->Body) { C.Fail(TEXT("the player's pawn is not a character with a body")); return; } C.World->GetFirstPlayerController()->SetViewTarget(Ch); }); S.WaitSim(1.5); S.Expect(TEXT("the body is set up: Twinblast mesh, 14 sequences, guns hidden, leg material, drawn, own view, head hidden, arm away"), [](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); const UDescentBodyAnimInstance* A = B ? B->GetAnim() : nullptr; if (!B || !A) { return false; } const USkeletalMeshComponent* Mesh = B->GetBodyMesh(); UE_LOG(LogDescent, Display, TEXT("player_body: set up %d, mesh %s, sequences %d, guns shown %d (section %d), legs %d, shown %d, own view %d, head hidden %d, first-person primitive %d, camera first-person scale %.2f (on %d), arm pose hidden %d"), B->IsSetUp(), Mesh && Mesh->GetSkeletalMeshAsset() ? *Mesh->GetSkeletalMeshAsset()->GetName() : TEXT("-"), A->CountLoadedSequences(), B->AreGunsShown(), B->IsPistolSectionShown(), B->IsLegMaterialApplied(), B->IsBodyShown(), B->IsOwnView(), B->IsHeadHidden(), Mesh && Mesh->FirstPersonPrimitiveType == EFirstPersonPrimitiveType::FirstPerson, Ch->Camera->FirstPersonScale, Ch->Camera->bEnableFirstPersonScale ? 1 : 0, Ch->Arm->GetNamedPose() == EDescentArmPose::Hidden); return B->IsSetUp() && A->CountLoadedSequences() == Descent::Body::SeqCount && !B->AreGunsShown() && !B->IsPistolSectionShown() && B->IsLegMaterialApplied() && B->IsBodyShown() && B->IsOwnView() && B->IsHeadHidden() && Mesh->FirstPersonPrimitiveType == EFirstPersonPrimitiveType::FirstPerson && Ch->Arm->GetNamedPose() == EDescentArmPose::Hidden && !Ch->Arm->IsCustomPose(); }); S.Expect(TEXT("in dry air the body takes only lighting channel 2 (the room's lamps), like the old arm"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const USkeletalMeshComponent* Mesh = B ? B->GetBodyMesh() : nullptr; return Mesh && B->IsInDryAir() && !Mesh->LightingChannels.bChannel0 && !Mesh->LightingChannels.bChannel1 && Mesh->LightingChannels.bChannel2; }); BodyScnExpectMotion(S, TEXT("idle")); S.Shot(TEXT("fp_idle")); S.Expect(TEXT("the eyes of the standing pose are at the camera height (1.65 m +- 3 cm)"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const double Eye = B ? B->MeasureEyeHeightCm() : -1.0; UE_LOG(LogDescent, Display, TEXT("player_body: Eyes_Position socket %.1f cm above the feet in the Idle pose (camera %.1f cm), mesh scale %.3f"), Eye, Descent::Tune::Player::EyeHeightM * 100.0, B ? B->GetBodyMesh()->GetRelativeScale3D().Z : 0.0); return FMath::Abs(Eye - Descent::Tune::Player::EyeHeightM * 100.0) <= 3.0; }); S.Exec(TEXT("Descent.Player.Look 0 -45")); S.WaitFrames(4); S.Shot(TEXT("fp_look_down_45")); S.Exec(TEXT("Descent.Player.Look 0 -60")); S.WaitFrames(4); S.Shot(TEXT("fp_look_down_60")); S.Exec(TEXT("Descent.Player.Look 0 -80")); S.WaitFrames(4); S.Shot(TEXT("fp_look_down_80")); S.Exec(TEXT("Descent.Player.Look 0 35")); S.WaitFrames(4); S.Shot(TEXT("fp_look_up_35")); S.Exec(TEXT("Descent.Player.Look 0 0")); S.DumpState(TEXT("idle")); // ---- third person, standing: the whole man (the scenario camera is the view, so he has his head and the pack's own pose) ---- BodyScnOutside(S, TEXT("tp_front_room_light"), FVector(3.7, 0.0, 1.2), 0.9, 50.f, false); S.Expect(TEXT("seen from outside: head back, not a first-person primitive, the pack's own arm pose"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const USkeletalMeshComponent* Mesh = B ? B->GetBodyMesh() : nullptr; const UDescentBodyAnimInstance* A = B ? B->GetAnim() : nullptr; return Mesh && A && !B->IsOwnView() && !B->IsHeadHidden() && Mesh->FirstPersonPrimitiveType == EFirstPersonPrimitiveType::None && A->Input.ArmRaiseLeftDeg == 0.0; }); BodyScnOutside(S, TEXT("tp_front"), FVector(3.7, 0.0, 1.2), 0.9, 50.f, true); BodyScnOutside(S, TEXT("tp_side_right"), FVector(0.0, 3.7, 1.2), 0.9, 50.f, true); BodyScnOutside(S, TEXT("tp_side_left"), FVector(0.0, -3.7, 1.2), 0.9, 50.f, true); BodyScnOutside(S, TEXT("tp_back"), FVector(-3.7, 0.0, 1.2), 0.9, 50.f, true); BodyScnOutside(S, TEXT("tp_legs"), FVector(1.5, 0.5, 0.55), 0.45, 50.f, true); BodyScnOutside(S, TEXT("tp_feet"), FVector(0.95, 0.25, 0.35), 0.12, 45.f, true); BodyScnOutside(S, TEXT("tp_arms"), FVector(1.25, 0.25, 1.55), 1.40, 50.f, true); // the guns switch, proven in a picture: on, then off again S.Exec(TEXT("Descent.Body.ShowGuns 1")); S.WaitFrames(3); S.Expect(TEXT("Descent.Body.ShowGuns 1 draws the pistol section"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); return B && B->AreGunsShown() && B->IsPistolSectionShown(); }); BodyScnOutside(S, TEXT("tp_front_guns_on"), FVector(3.7, 0.0, 1.2), 0.9, 50.f, true); S.Exec(TEXT("Descent.Body.ShowGuns 0")); S.WaitFrames(3); S.Expect(TEXT("Descent.Body.ShowGuns 0 hides it again"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); return B && !B->AreGunsShown() && !B->IsPistolSectionShown(); }); Descent::Scn::ViewPlayer(S); // ---- walking: forward, then the three other directions (state only), a picture in mid-stride ---- S.Exec(TEXT("Descent.Player.Place galley")); S.WaitSim(0.4); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 1.6")); S.WaitSim(0.9); BodyScnExpectMotion(S, TEXT("moving"), TEXT("Jog_Fwd")); BodyScnHold(S, true); S.Shot(TEXT("fp_walk")); BodyScnOutside(S, TEXT("tp_walk_side"), FVector(0.0, 3.7, 1.2), 0.9, 50.f, true); Descent::Scn::ViewPlayer(S); BodyScnHold(S, false); S.WaitSim(1.2); BodyScnExpectMotion(S, TEXT("idle")); S.Exec(TEXT("Descent.Player.Intent -1 0 0 0 0 0.8")); S.WaitSim(0.6); BodyScnExpectMotion(S, TEXT("moving"), TEXT("Jog_Bwd")); S.WaitSim(0.8); S.Exec(TEXT("Descent.Player.Intent 0 1 0 0 0 0.8")); S.WaitSim(0.6); BodyScnExpectMotion(S, TEXT("moving"), TEXT("Jog_Right")); S.WaitSim(0.8); S.Exec(TEXT("Descent.Player.Intent 0 -1 0 0 0 0.8")); S.WaitSim(0.6); BodyScnExpectMotion(S, TEXT("moving"), TEXT("Jog_Left")); S.WaitSim(1.0); // walking while looking down at the legs S.Exec(TEXT("Descent.Player.Place galley")); S.Exec(TEXT("Descent.Player.Look 0 -60")); S.WaitSim(0.4); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 1.6")); S.WaitSim(0.8); BodyScnHold(S, true); S.Shot(TEXT("fp_walk_look_down")); BodyScnHold(S, false); S.WaitSim(1.2); // ---- running (sprint) in the Tender bay: six pictures in a row from his own eyes, six from the side ---- S.Exec(TEXT("Descent.Player.Place bay")); S.Exec(TEXT("Descent.Player.Look 0 0")); S.WaitSim(0.5); // (the intents are long: he is held for every picture of a strip while the intent's own clock runs on) S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 40 0 0 1")); S.WaitSim(0.9); BodyScnExpectMotion(S, TEXT("moving"), TEXT("Run_Fwd")); S.Expect(TEXT("the run's play rate is the ground speed over the speed its legs cover the ground at (no skating), near 1"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const UDescentBodyAnimInstance* A = B ? B->GetAnim() : nullptr; if (!A) { return false; } const double ClipSpeed = A->GetLengths().SpeedCmS[static_cast(EDescentBodySeq::RunFwd)]; UE_LOG(LogDescent, Display, TEXT("player_body run: speed %.0f cm/s, the clip's legs %.0f cm/s, rate %.3f"), A->GetState().SpeedCmS, ClipSpeed, A->GetState().LocoRate); return ClipSpeed > 1.0 && FMath::IsNearlyEqual(A->GetState().LocoRate, A->GetState().SpeedCmS / ClipSpeed, 0.01) && A->GetState().LocoRate > 0.85 && A->GetState().LocoRate < 1.2; }); BodyScnStrip(S, TEXT("fp_run"), false, FVector::ZeroVector, 0.9, 3, 3); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(0.6); S.Exec(TEXT("Descent.Player.Place bay")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 40 0 0 1")); S.WaitSim(0.45); // the bay is short: 4.5 m ahead of where he is placed is the pool's rail (the first strip showed him standing at it) BodyScnStrip(S, TEXT("tp_run_side"), true, FVector(0.5, 3.6, 1.1), 0.9, 6, 1); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(0.6); // sprinting to the right: the same run clip, the legs turned toward where he goes, the chest with the view S.Exec(TEXT("Descent.Player.Place bay")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Intent 0 1 0 0 0 40 0 0 1")); S.WaitSim(1.0); S.Expect(TEXT("sprinting to the right: Run_Fwd forward, the lower body turned about 90 degrees to his right"), [](FDescentScenarioContext& C) { const FDescentBodyAnimState* St = BodyScnState(C); if (St) { UE_LOG(LogDescent, Display, TEXT("player_body run right: clip %s, lower turn %.0f deg, backward %d, speed %.0f"), Descent::Body::SeqAssetName(St->Loco.Cur), St->LowerTurnDeg, St->bLocoBackward, St->SpeedCmS); } return St && St->Loco.Cur == EDescentBodySeq::RunFwd && St->LowerTurnDeg > 80.0 && !St->bLocoBackward; }); BodyScnHold(S, true); BodyScnOutside(S, TEXT("tp_run_right"), FVector(3.4, 1.6, 1.2), 0.9, 50.f, true, 2, 0.f); Descent::Scn::ViewPlayer(S); BodyScnHold(S, false); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Place bay")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Intent -1 0 0 0 0 40 0 0 1")); S.Until(TEXT("sprinting backward: the run clip in reverse, the legs straight"), [](FDescentScenarioContext& C) { const FDescentBodyAnimState* St = BodyScnState(C); if (C.SecondsInStep() > 8.0) { C.Fail(FString::Printf(TEXT("no backward run: clip %s, speed %.0f cm/s, backward %d, lower turn %.0f"), St ? Descent::Body::SeqAssetName(St->Loco.Cur) : TEXT("-"), St ? St->SpeedCmS : 0.0, St ? St->bLocoBackward : 0, St ? St->LowerTurnDeg : 0.0)); return true; } return St && St->Loco.Cur == EDescentBodySeq::RunFwd && St->bLocoBackward && FMath::Abs(St->LowerTurnDeg) < 10.0; }); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(0.6); // running while looking down at the legs: two moments of the stride S.Exec(TEXT("Descent.Player.Place bay")); S.Exec(TEXT("Descent.Player.Look 0 -60")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 40 0 0 1")); S.WaitSim(0.7); BodyScnStrip(S, TEXT("fp_run_look_down"), false, FVector::ZeroVector, 0.9, 2, 4); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(0.8); // ---- the nutrition-port jack-in as the body shows it: the right arm out, held through the feed (the mechanic itself: m8_biomass) ---- S.Exec(TEXT("Descent.Player.Place galley")); S.Exec(TEXT("Descent.Player.Look 0 -8")); S.WaitSim(0.5); S.Do(TEXT("held as at the port, the jack-in visual starts"), [](FDescentScenarioContext& C) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->SetFrozen(true); Ch->StartJackVisual(Ch->EyeWorldUU() + Ch->EyeDirection() * 70.0, Ch->EyeDirection()); } }); S.WaitSim(1.6); // the two pictures below take about 0.9 s each: both fall inside the hold (1.0 to 4.1 s) S.Expect(TEXT("mid-feed: the reach clip (Touch) is on the upper body at its furthest point, the body is held"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const FDescentBodyAnimState* St = BodyScnState(C); if (!B || !St) { return false; } UE_LOG(LogDescent, Display, TEXT("player_body jack: playing %d, overlay %s at %.2f s weight %.2f, motion %s"), B->IsJackPlaying(), Descent::Body::SeqAssetName(St->OverSeq), St->OverTime, St->OverAlpha, *BodyScnMotion(C)); return B->IsJackPlaying() && St->OverSeq == EDescentBodySeq::Touch && St->OverAlpha > 0.9 && FMath::IsNearlyEqual(St->OverTime, Descent::Tune::Body::TouchPeakSec, 0.01) && BodyScnMotion(C) == TEXT("held"); }); BodyScnHold(S, true); // a picture's warm-up is real time: many fixed sim steps, the feed would be over S.Shot(TEXT("fp_jack_feed")); BodyScnOutside(S, TEXT("tp_jack_side"), FVector(0.5, 3.0, 1.4), 1.1, 50.f, true); Descent::Scn::ViewPlayer(S); BodyScnHold(S, false); S.WaitSim(3.6); S.Expect(TEXT("after 4.8 s the arm is back and the visual has ended"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const FDescentBodyAnimState* St = BodyScnState(C); return B && St && !B->IsJackPlaying() && St->OverAlpha < 0.1; }); S.Do(TEXT("released"), [](FDescentScenarioContext& C) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->SetFrozen(false); } }); // ---- a jump: start, in the air, landing ---- S.Exec(TEXT("Descent.Player.Place galley")); S.Exec(TEXT("Descent.Player.Look 0 0")); S.WaitSim(0.6); S.Exec(TEXT("Descent.Player.Intent 0 0 1 0 0 0.12")); S.Until(TEXT("0.3 s into the jump, then held still"), [](FDescentScenarioContext& C) { if (C.SecondsInStep() > 8.0) { C.Fail(FString::Printf(TEXT("no jump: motion %s"), *BodyScnMotion(C))); return true; } const FDescentBodyAnimState* St = BodyScnState(C); if (St && St->AirTime >= 0.3) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->CustomTimeDilation = 0.f; } return true; } return false; }); S.Expect(TEXT("in the air the motion is jumping or falling"), [](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); const FDescentBodyAnimState* St = BodyScnState(C); UE_LOG(LogDescent, Display, TEXT("player_body: in the jump: motion %s, action clip %s, air time %.2f s, feet %.2f m above the galley floor"), *BodyScnMotion(C), St ? Descent::Body::SeqAssetName(St->Act.Cur) : TEXT("-"), St ? St->AirTime : 0.0, Ch ? Ch->GetFeetLocalM().Z + 3.0 : 0.0); return BodyScnMotion(C) == TEXT("jumping") || BodyScnMotion(C) == TEXT("falling"); }); S.Shot(TEXT("fp_jump")); S.Exec(TEXT("Descent.Player.Look 0 -60")); S.WaitFrames(3); S.Shot(TEXT("fp_jump_look_down")); S.Exec(TEXT("Descent.Player.Look 0 0")); BodyScnOutside(S, TEXT("tp_jump_side"), FVector(0.0, 3.9, 1.3), 1.1, 50.f, true); Descent::Scn::ViewPlayer(S); { TSharedRef> Seen = MakeShared>(); BodyScnHold(S, false); S.Until(TEXT("the jump ends on the ground"), [Seen](FDescentScenarioContext& C) { Seen->Add(BodyScnMotion(C)); return C.SecondsInStep() > 10.0 || (Seen->Contains(TEXT("landing")) && BodyScnMotion(C) == TEXT("idle")); }); S.Expect(TEXT("the jump went through falling and landing and ended idle"), [Seen](FDescentScenarioContext& C) { UE_LOG(LogDescent, Display, TEXT("player_body: after the jump: motions seen %s, now %s"), *FString::Join(Seen->Array(), TEXT(",")), *BodyScnMotion(C)); // the retargeted set's jump clip (0.87 s) covers a whole standing jump: "falling" only shows on a longer drop return (Seen->Contains(TEXT("falling")) || Seen->Contains(TEXT("jumping"))) && Seen->Contains(TEXT("landing")) && BodyScnMotion(C) == TEXT("idle"); }); } // ---- against a wall: walked into the galley's port-side wall until the capsule stops; the hands must not go into it ---- S.Exec(TEXT("Descent.Player.PlaceLocal 22 -9.2 -3.0 -90")); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 1.5")); S.WaitSim(2.2); S.Expect(TEXT("pressed against the wall"), [](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); if (!Ch) { return false; } UE_LOG(LogDescent, Display, TEXT("player_body wall: stopped at carrier-local y %.3f, speed %.1f cm/s"), Ch->GetFeetLocalM().Y, Ch->GetVelocity().Size()); return Ch->GetFeetLocalM().Y < -9.3 && Ch->GetVelocity().Size() < 1.0; }); S.Shot(TEXT("fp_wall")); S.Exec(TEXT("Descent.Player.Look -90 -50")); S.WaitFrames(3); S.Shot(TEXT("fp_wall_look_down")); // ---- the blue bridge: the body takes the room's colour ---- S.Exec(TEXT("Descent.Player.Place bridge")); S.WaitSim(0.5); S.Shot(TEXT("fp_idle_bridge")); S.Exec(TEXT("Descent.Player.Look 0 -55")); S.WaitFrames(3); S.Shot(TEXT("fp_look_down_bridge")); // ---- at the helm: he stands idle at the console; the view is the carrier's bridge camera ---- 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.Exec(TEXT("Descent.Player.Use")); S.WaitSim(0.5); S.Expect(TEXT("mode Helm: the body is drawn, held, seen from outside (whole, with his head)"), [](FDescentScenarioContext& C) { const ADescentPlayerController* P = Descent::Scn::PC(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); UE_LOG(LogDescent, Display, TEXT("player_body helm: mode %s, shown %d, own view %d, head hidden %d, motion %s"), P ? DescentModeName(P->GetMode()) : TEXT("-"), B ? B->IsBodyShown() : 0, B ? B->IsOwnView() : 0, B ? B->IsHeadHidden() : 0, *BodyScnMotion(C)); return P && B && P->GetMode() == EDescentMode::Helm && B->IsBodyShown() && !B->IsOwnView() && !B->IsHeadHidden() && BodyScnMotion(C) == TEXT("held"); }); S.Shot(TEXT("helm_view")); BodyScnOutside(S, TEXT("tp_helm"), FVector(-2.6, 2.0, 1.8), 1.0, 60.f, true); S.Do(TEXT("view: the helm again"), [](FDescentScenarioContext& C) { if (ADescentPlayerController* P = Descent::Scn::PC(C)) { if (ADescentCarrier* K = Descent::Scn::Carrier(C)) { P->SetViewTarget(K); } } }); S.Exec(TEXT("Descent.Player.Use")); S.WaitSim(0.5); S.Expect(TEXT("helm released: on foot, own view again"), [](FDescentScenarioContext& C) { const ADescentPlayerController* P = Descent::Scn::PC(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); return P && B && P->GetMode() == EDescentMode::Foot && B->IsBodyShown() && B->IsOwnView() && B->IsHeadHidden() && BodyScnMotion(C) != TEXT("held"); }); // ---- swimming, outside the hull on the port side, looking along it with the lamp on ---- S.Exec(TEXT("Descent.Player.Swim 20 -24 -33")); S.Exec(TEXT("Descent.Player.Look 70 4")); S.Do(TEXT("lamp on"), [](FDescentScenarioContext& C) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->SetFlashlight(true); } }); S.WaitSim(1.5); BodyScnExpectMotion(S, TEXT("swimming"), TEXT("Swim_Idle")); BodyScnOutside(S, TEXT("tp_swim_still_side"), FVector(0.4, 3.7, 1.2), 0.9, 50.f, true); Descent::Scn::ViewPlayer(S); S.Expect(TEXT("in the water the body is lit by the sea (lighting channels 0 and 1)"), [](FDescentScenarioContext& C) { const UDescentPlayerBodyComponent* B = BodyScnBody(C); const USkeletalMeshComponent* Mesh = B ? B->GetBodyMesh() : nullptr; return Mesh && !B->IsInDryAir() && Mesh->LightingChannels.bChannel0 && Mesh->LightingChannels.bChannel1 && !Mesh->LightingChannels.bChannel2; }); S.WaitFrames(5); // his own view again after the picture from outside S.Expect(TEXT("treading water: both hands scull in the picture, low (the code-made arms)"), [](FDescentScenarioContext& C) { const FDescentBodyBones& B = FDescentBodyProfile::Get().Bones; const FBodyJointView L = BodyScnJoint(C, B.HandL), R = BodyScnJoint(C, B.HandR); UE_LOG(LogDescent, Display, TEXT("SWIMARMS treading: left hand %s %.2f,%.2f right hand %s %.2f,%.2f"), L.bOn ? TEXT("ON") : TEXT("off"), L.Screen.X, L.Screen.Y, R.bOn ? TEXT("ON") : TEXT("off"), R.Screen.X, R.Screen.Y); return L.bOn && R.bOn && L.Screen.Y > 0.75 && R.Screen.Y > 0.75; }); S.Shot(TEXT("fp_swim")); S.Exec(TEXT("Descent.Player.Look 70 -35")); // (-55 with the leaning swim pose is open water below him: a black frame) S.WaitFrames(3); S.Shot(TEXT("fp_swim_look_down")); S.Exec(TEXT("Descent.Player.Look 70 4")); S.Exec(TEXT("Descent.Player.Intent 1 0 0 0 0 90")); S.WaitSim(1.2); BodyScnExpectMotion(S, TEXT("swimming"), TEXT("Swim_Fwd")); // The arm stroke made in code, from his own eyes, at four places of its cycle (he is held at each): both hands must be in the // picture in at least three of them. { TSharedRef BothIn = MakeShared(0); for (int32 Phase = 0; Phase < 4; ++Phase) { S.Until(FString::Printf(TEXT("the stroke at %.2f of its cycle, then held"), Phase * 0.25), [Phase](FDescentScenarioContext& C) { const FDescentBodyAnimState* St = BodyScnState(C); if (C.SecondsInStep() > 20.0) { C.Fail(FString::Printf(TEXT("the stroke never came to %.2f (now %.2f)"), Phase * 0.25, St ? St->SwimStroke : -1.0)); return true; } if (St && St->SwimStroke >= Phase * 0.25 && St->SwimStroke < Phase * 0.25 + 0.07) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->CustomTimeDilation = 0.f; } return true; } return false; }); S.WaitFrames(3); S.Do(TEXT("hands in the picture?"), [Phase, BothIn](FDescentScenarioContext& C) { const FDescentBodyBones& B = FDescentBodyProfile::Get().Bones; const FBodyJointView L = BodyScnJoint(C, B.HandL), R = BodyScnJoint(C, B.HandR); *BothIn += (L.bOn && R.bOn) ? 1 : 0; UE_LOG(LogDescent, Display, TEXT("SWIMARMS stroke %.2f: left hand %s %.2f,%.2f right hand %s %.2f,%.2f"), Phase * 0.25, L.bOn ? TEXT("ON") : TEXT("off"), L.Screen.X, L.Screen.Y, R.bOn ? TEXT("ON") : TEXT("off"), R.Screen.X, R.Screen.Y); }); S.Shot(FString::Printf(TEXT("fp_swim_stroke_%d"), Phase + 1), 2, 0.f); BodyScnHold(S, false); S.WaitFrames(4); } S.Expect(TEXT("both hands are in the picture in at least three of the four stroke phases"), [BothIn](FDescentScenarioContext&) { return *BothIn >= 3; }); } S.Exec(TEXT("Descent.Player.Look 70 -40")); S.WaitFrames(3); S.Shot(TEXT("fp_swim_forward_look_down"), 1, 0.f); S.Exec(TEXT("Descent.Player.Look 70 4")); S.WaitFrames(6); BodyScnStrip(S, TEXT("tp_swim_side"), true, FVector(0.4, 3.7, 1.2), 0.9, 6, 5); S.Exec(TEXT("Descent.Player.Intent 0 0 0 0 0 0.1")); S.WaitSim(1.0); S.Do(TEXT("lamp off"), [](FDescentScenarioContext& C) { if (ADescentCharacter* Ch = Descent::Scn::Char(C)) { Ch->SetFlashlight(false); } }); // ---- in a Tender the body is hidden, and comes back when he leaves ---- S.Exec(TEXT("Descent.Player.Place bay")); S.WaitSim(0.5); S.Exec(TEXT("Descent.Tender.Board")); S.Until(TEXT("docked in the Tender"), [](FDescentScenarioContext& C) { const ADescentPlayerController* P = Descent::Scn::PC(C); if (C.SecondsInStep() > 40.0) { C.Fail(FString::Printf(TEXT("still in mode %s 40 s after boarding"), P ? DescentModeName(P->GetMode()) : TEXT("-"))); return true; } return P && P->GetMode() == EDescentMode::Docked; }); S.WaitSim(1.0); S.Expect(TEXT("in the Tender the body is not drawn"), [](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); return Ch && B && Ch->IsInTender() && !B->IsBodyShown() && !B->GetBodyMesh()->IsVisible(); }); S.Shot(TEXT("tender_docked")); S.Exec(TEXT("Descent.Tender.Exit")); S.WaitSim(1.5); S.Expect(TEXT("out of the Tender: on foot, the body is drawn again in the own view"), [](FDescentScenarioContext& C) { const ADescentPlayerController* P = Descent::Scn::PC(C); const ADescentCharacter* Ch = Descent::Scn::Char(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); UE_LOG(LogDescent, Display, TEXT("player_body: after leaving the Tender: mode %s, in tender %d, shown %d, own view %d"), P ? DescentModeName(P->GetMode()) : TEXT("-"), Ch ? Ch->IsInTender() : 0, B ? B->IsBodyShown() : 0, B ? B->IsOwnView() : 0); return P && Ch && B && P->GetMode() == EDescentMode::Foot && !Ch->IsInTender() && B->IsBodyShown() && B->GetBodyMesh()->IsVisible() && B->IsOwnView(); }); S.Shot(TEXT("fp_bay_after_tender")); // ---- the old arm is one switch away ---- S.Exec(TEXT("Descent.Player.Place galley")); S.Exec(TEXT("Descent.Body.View arm")); S.WaitSim(1.2); S.Expect(TEXT("Descent.Body.View arm: the body is not drawn and the old arm is up (idle)"), [](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); return Ch && B && !B->IsBodyShown() && Ch->Arm->GetNamedPose() == EDescentArmPose::Idle && FMath::IsNearlyEqual(Ch->Camera->FirstPersonScale, 0.45f, 0.001f); }); S.Shot(TEXT("old_arm_view")); S.Exec(TEXT("Descent.Body.View body")); S.WaitSim(1.2); S.Expect(TEXT("Descent.Body.View body: the body is back and the arm away"), [](FDescentScenarioContext& C) { const ADescentCharacter* Ch = Descent::Scn::Char(C); const UDescentPlayerBodyComponent* B = BodyScnBody(C); return Ch && B && B->IsBodyShown() && B->IsOwnView() && Ch->Arm->GetNamedPose() == EDescentArmPose::Hidden; }); S.Shot(TEXT("fp_idle_again")); S.DumpState(TEXT("end")); S.Quit(); }