#include "Vehicles/CarrierCarousel.h" #include "Vehicles/CarrierBlockout.h" #include "Vehicles/CarrierMeshBuilder.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "Systems/DescentCargo.h" #include "Core/DescentBoot.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Core/Tuning/DescentTune_Tender.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Engine/World.h" #include "ProceduralMeshComponent.h" namespace CarTune = Descent::Tune::Tender; namespace CarBay = Descent::Tune::Carrier; namespace Descent::Carrier::Carousel { namespace { /** The four corners of a yaw box in the XY plane. */ void CarouselCorners(const FYawBox& B, FVector2D Out[4]) { const double R = FMath::DegreesToRadians(B.YawDeg); const FVector2D Ax(FMath::Cos(R), FMath::Sin(R)), Ay(-Ax.Y, Ax.X); const FVector2D C(B.C.X, B.C.Y); Out[0] = C + Ax * B.Half.X + Ay * B.Half.Y; Out[1] = C - Ax * B.Half.X + Ay * B.Half.Y; Out[2] = C - Ax * B.Half.X - Ay * B.Half.Y; Out[3] = C + Ax * B.Half.X - Ay * B.Half.Y; } double CarouselPointSegment(const FVector2D& P, const FVector2D& A, const FVector2D& B) { const FVector2D AB = B - A; const double T = FMath::Clamp(FVector2D::DotProduct(P - A, AB) / FMath::Max(1e-12, AB.SizeSquared()), 0.0, 1.0); return (P - (A + AB * T)).Size(); } /** Distance between two convex quads in the plane; 0 when they overlap (separating axis test first). */ double CarouselQuadDistance(const FVector2D A[4], const FVector2D B[4]) { bool bSeparated = false; for (int32 Pass = 0; Pass < 2 && !bSeparated; ++Pass) { const FVector2D* Q = Pass == 0 ? A : B; for (int32 I = 0; I < 2; ++I) { const FVector2D E = Q[(I + 1) % 4] - Q[I]; const FVector2D N(-E.Y, E.X); double A0 = 1e30, A1 = -1e30, B0 = 1e30, B1 = -1e30; for (int32 K = 0; K < 4; ++K) { const double Da = FVector2D::DotProduct(A[K], N), Db = FVector2D::DotProduct(B[K], N); A0 = FMath::Min(A0, Da); A1 = FMath::Max(A1, Da); B0 = FMath::Min(B0, Db); B1 = FMath::Max(B1, Db); } if (A1 < B0 || B1 < A0) { bSeparated = true; break; } } } if (!bSeparated) { return 0.0; } double Best = 1e30; for (int32 I = 0; I < 4; ++I) { for (int32 K = 0; K < 4; ++K) { Best = FMath::Min(Best, CarouselPointSegment(A[I], B[K], B[(K + 1) % 4])); Best = FMath::Min(Best, CarouselPointSegment(B[I], A[K], A[(K + 1) % 4])); } } return Best; } FYawBox CarouselMinMax(const TCHAR* Name, double X0, double X1, double Y0, double Y1, double Z0, double Z1) { FYawBox B; B.Name = Name; B.C = FVector(0.5 * (X0 + X1), 0.5 * (Y0 + Y1), 0.5 * (Z0 + Z1)); B.Half = FVector(0.5 * FMath::Abs(X1 - X0), 0.5 * FMath::Abs(Y1 - Y0), 0.5 * FMath::Abs(Z1 - Z0)); return B; } } double ArmYawDeg(int32 Arm, double AngleDeg) { return CarTune::LaunchArmYawDeg + AngleDeg + CarTune::RotateStepDeg * Arm; } FTransform CradleLocalM(int32 Arm, double AngleDeg, double Z) { const double Yaw = ArmYawDeg(Arm, AngleDeg); const double R = FMath::DegreesToRadians(Yaw); const FVector Pos(CarTune::HubX + CarTune::ArmRadiusM * FMath::Cos(R), CarTune::HubY + CarTune::ArmRadiusM * FMath::Sin(R), Z); // nose tangential; at the launch slot (arm yaw 90) it points to the bow return FTransform(FRotator(0.0, FRotator::NormalizeAxis(Yaw - CarTune::LaunchArmYawDeg), 0.0), Pos); } FYawBox TenderBox(const FVector& OriginM, double YawDeg) { const FVector Min = AKazahTender::GetModelBoundsMinCm() / 100.0, Max = AKazahTender::GetModelBoundsMaxCm() / 100.0; FYawBox B; B.Name = TEXT("tender"); B.Half = 0.5 * (Max - Min); B.YawDeg = YawDeg; B.C = OriginM + FRotator(0.0, YawDeg, 0.0).RotateVector(0.5 * (Max + Min)); return B; } double BoxDistance(const FYawBox& A, const FYawBox& B) { FVector2D Qa[4], Qb[4]; CarouselCorners(A, Qa); CarouselCorners(B, Qb); const double Dxy = CarouselQuadDistance(Qa, Qb); const double Dz = FMath::Max(0.0, FMath::Abs(A.C.Z - B.C.Z) - (A.Half.Z + B.Half.Z)); return FMath::Sqrt(Dxy * Dxy + Dz * Dz); } const TArray& BayObstacles() { static const TArray Boxes = []() { TArray O; const double T = CarBay::WallThicknessM; const double X0 = -40.0, X1 = -18.0, Y0 = -9.0, Y1 = 9.0; // the bay room (CarrierLayout) const double Fz = CarBay::DeckFloorZ1, Cz = Fz + CarBay::TallRoomHeightM; O.Add(CarouselMinMax(TEXT("bay wall aft"), X0 - 2.0, X0 + T, Y0, Y1, Fz, Cz)); O.Add(CarouselMinMax(TEXT("bay wall forward"), X1 - T, X1 + 2.0, Y0, Y1, Fz, Cz)); O.Add(CarouselMinMax(TEXT("bay wall port"), X0, X1, Y0 - 2.0, Y0 + T, Fz, Cz)); O.Add(CarouselMinMax(TEXT("bay wall starboard"), X0, X1, Y1 - T, Y1 + 2.0, Fz, Cz)); O.Add(CarouselMinMax(TEXT("bay ceiling"), X0, X1, Y0, Y1, Cz, Cz + 1.0)); O.Add(CarouselMinMax(TEXT("bay floor"), X0, X1, Y0, CarBay::PoolY0, Fz - 0.2, Fz)); O.Add(CarouselMinMax(TEXT("bay floor"), X0, X1, CarBay::PoolY1, Y1, Fz - 0.2, Fz)); O.Add(CarouselMinMax(TEXT("bay floor"), X0, CarBay::PoolX0, CarBay::PoolY0, CarBay::PoolY1, Fz - 0.2, Fz)); O.Add(CarouselMinMax(TEXT("bay floor"), CarBay::PoolX1, X1, CarBay::PoolY0, CarBay::PoolY1, Fz - 0.2, Fz)); O.Add(CarouselMinMax(TEXT("D2 gallery"), -19.8, X1, Y0, Y1, CarBay::DeckFloorZ2 - 0.3, CarBay::DeckFloorZ2)); O.Add(CarouselMinMax(TEXT("gallery rail"), -19.84, -19.76, -7.2, Y1, CarBay::DeckFloorZ2, CarBay::DeckFloorZ2 + 1.3)); // the stairs down the port wall (x -19.7 .. -30.6, y -8.75 .. -7.25), as eight steps with their rail (1.3 m above) for (int32 I = 0; I < 8; ++I) { const double Xa = -19.7 - 10.9 * I / 8.0, Xb = -19.7 - 10.9 * (I + 1) / 8.0; const double Top = CarBay::DeckFloorZ2 - 4.5 * I / 8.0; O.Add(CarouselMinMax(TEXT("bay stairs"), Xb, Xa, -8.75, -7.16, Fz, Top + 1.3)); } O.Add(CarouselMinMax(TEXT("carousel hub"), CarTune::HubX - CarTune::HubRadiusM, CarTune::HubX + CarTune::HubRadiusM, CarTune::HubY - CarTune::HubRadiusM, CarTune::HubY + CarTune::HubRadiusM, 0.6, Cz)); // section 6.3 (built in M5): the pipe riser up the starboard wall and the unload boom stowed along the starboard pool edge O.Add(CarouselMinMax(TEXT("pipe riser"), -23.05, -22.55, 8.5, 9.0, -7.35, 0.9)); O.Add(CarouselMinMax(TEXT("unload boom"), -29.9, -22.8, 8.5, 9.0, -7.6, -7.1)); // pool rails (1.3 m) and the shaft walls down to the belly const double Rx0 = CarBay::PoolX0 - 0.25, Rx1 = CarBay::PoolX1 + 0.25, Ry0 = CarBay::PoolY0 - 0.25, Ry1 = CarBay::PoolY1 + 0.25; O.Add(CarouselMinMax(TEXT("pool rail"), Rx0, Rx1, Ry0 - 0.03, Ry0 + 0.03, Fz, Fz + 1.3)); O.Add(CarouselMinMax(TEXT("pool rail"), Rx0, Rx1, Ry1 - 0.03, Ry1 + 0.03, Fz, Fz + 1.3)); O.Add(CarouselMinMax(TEXT("pool rail"), Rx0 - 0.03, Rx0 + 0.03, Ry0, Ry1, Fz, Fz + 1.3)); O.Add(CarouselMinMax(TEXT("pool rail"), Rx1 - 0.03, Rx1 + 0.03, Ry0, Ry1, Fz, Fz + 1.3)); const double Sb = CarBay::PoolShaftBottomZ; O.Add(CarouselMinMax(TEXT("pool shaft"), CarBay::PoolX0, CarBay::PoolX1, CarBay::PoolY0 - 0.15, CarBay::PoolY0, Sb, Fz)); O.Add(CarouselMinMax(TEXT("pool shaft"), CarBay::PoolX0, CarBay::PoolX1, CarBay::PoolY1, CarBay::PoolY1 + 0.15, Sb, Fz)); O.Add(CarouselMinMax(TEXT("pool shaft"), CarBay::PoolX0 - 0.15, CarBay::PoolX0, CarBay::PoolY0, CarBay::PoolY1, Sb, Fz)); O.Add(CarouselMinMax(TEXT("pool shaft"), CarBay::PoolX1, CarBay::PoolX1 + 0.15, CarBay::PoolY0, CarBay::PoolY1, Sb, Fz)); return O; }(); return Boxes; } double ObstacleDistance(const FYawBox& Tender, const TCHAR** OutName) { double Best = 1e30; for (const FYawBox& O : BayObstacles()) { const double D = BoxDistance(Tender, O); if (D < Best) { Best = D; if (OutName) { *OutName = O.Name; } } } return Best; } bool InDockZone(const FVector& P) { const FVector Slot = CradleLocalM(0, 0.0, 0.0).GetLocation(); return FMath::Abs(P.X - Slot.X) <= CarTune::DockZoneX && FMath::Abs(P.Y - Slot.Y) <= CarTune::DockZoneY && P.Z >= CarTune::DockZoneZ0 && P.Z <= CarTune::DockZoneZ1; } } using namespace Descent::Carrier::Carousel; UCarrierCarouselComponent::UCarrierCarouselComponent() { PrimaryComponentTick.bCanEverTick = false; HoistZ = CarTune::BoardZ; } ADescentCarrier* UCarrierCarouselComponent::Carrier() const { return Cast(GetOwner()); } int32 UCarrierCarouselComponent::IndexOf(const AKazahTender* T) const { return Tenders.IndexOfByKey(T); } void UCarrierCarouselComponent::Build() { if (bBuilt) { return; } bBuilt = true; using namespace Descent::Carrier; AActor* Owner = GetOwner(); auto NewMesh = [&](const TCHAR* Name, USceneComponent* Parent) -> UProceduralMeshComponent* { UProceduralMeshComponent* C = NewObject(Owner, Name); C->SetMobility(EComponentMobility::Movable); C->bUseAsyncCooking = false; C->SetupAttachment(Parent); C->SetCollisionEnabled(ECollisionEnabled::NoCollision); // everything here hangs above head height or over the pool UCarrierBlockoutComponent::MakeInterior(C); C->RegisterComponent(); return C; }; const double Cz = CarBay::DeckFloorZ1 + CarBay::TallRoomHeightM; { FMeshBuilder B; // the hub hangs from the bay ceiling B.Cylinder(EMat::DarkMetal, FVector(CarTune::HubX, CarTune::HubY, 0.5 * (0.6 + Cz)), CarTune::HubRadiusM, Cz - 0.6, 20, 2, false); B.Commit(NewMesh(TEXT("CarouselHub"), this), false); } ArmsRoot = NewObject(Owner, TEXT("CarouselArms")); ArmsRoot->SetMobility(EComponentMobility::Movable); ArmsRoot->SetupAttachment(this); ArmsRoot->SetRelativeLocation(Descent::MetresToUU(FVector(CarTune::HubX, CarTune::HubY, CarTune::HubZ))); ArmsRoot->RegisterComponent(); { FMeshBuilder B; // three beams under the ceiling, in the arms' own frame (arm 0 along the launch direction at angle 0) for (int32 Arm = 0; Arm < CarTune::FleetCount; ++Arm) { const double Yaw = FMath::DegreesToRadians(ArmYawDeg(Arm, 0.0)); const FVector Dir(FMath::Cos(Yaw), FMath::Sin(Yaw), 0.0); B.Box(EMat::DarkMetal, Dir * (0.5 * CarTune::ArmRadiusM + 0.2) + FVector(0, 0, CarTune::ArmBeamZ), FVector(CarTune::ArmRadiusM + 0.4, 0.34, 0.22), false, FQuat(FVector(0, 0, 1), Yaw)); B.Box(EMat::Hazard, Dir * CarTune::ArmRadiusM + FVector(0, 0, CarTune::ArmBeamZ - 0.13), FVector(0.7, 0.7, 0.06), false, FQuat(FVector(0, 0, 1), Yaw)); } B.Commit(NewMesh(TEXT("CarouselBeams"), ArmsRoot), false); } for (int32 Arm = 0; Arm < CarTune::FleetCount; ++Arm) { const double YawDeg = ArmYawDeg(Arm, 0.0); const double Yaw = FMath::DegreesToRadians(YawDeg); const FVector At(CarTune::ArmRadiusM * FMath::Cos(Yaw), CarTune::ArmRadiusM * FMath::Sin(Yaw), 0.0); // The carriage: a clamp plate on the hatch collar with four grips, in the Tender's frame (origin = the Tender origin). FMeshBuilder C; const double Pz = CarTune::ClampAboveOriginM; C.Box(EMat::Hazard, FVector(0.1, 0.0, Pz + 0.05), FVector(1.5, 1.0, 0.10), false); for (double Sx : { -0.55, 0.75 }) { for (double Sy : { -0.56, 0.56 }) { C.Box(EMat::DarkMetal, FVector(Sx, Sy, Pz - 0.12), FVector(0.14, 0.10, 0.34), false); } } UProceduralMeshComponent* Carriage = NewMesh(*FString::Printf(TEXT("CarouselCarriage_%d"), Arm), ArmsRoot); Carriage->SetRelativeLocationAndRotation(Descent::MetresToUU(At), FRotator(0.0, YawDeg - CarTune::LaunchArmYawDeg, 0.0)); C.Commit(Carriage, false); Carriages.Add(Carriage); // The mast between the beam and the carriage: a unit column hanging from its top, stretched by its Z scale. FMeshBuilder M; M.Box(EMat::DarkMetal, FVector(0, 0, -0.5), FVector(0.18, 0.18, 1.0), false); UProceduralMeshComponent* Mast = NewMesh(*FString::Printf(TEXT("CarouselMast_%d"), Arm), ArmsRoot); Mast->SetRelativeLocation(Descent::MetresToUU(At + FVector(0, 0, CarTune::ArmBeamZ - 0.1))); M.Commit(Mast, false); Masts.Add(Mast); } UpdateVisuals(); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Carousel"); StepHandle = Sim->RegisterStep(EDescentSimStep::Carousel, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UCarrierCarouselComponent::Shutdown() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } StepHandle.Reset(); } void UCarrierCarouselComponent::SpawnTenders() { ADescentCarrier* C = Carrier(); UWorld* World = GetWorld(); if (!C || !World || Tenders.Num() > 0) { return; } for (int32 I = 0; I < CarTune::FleetCount; ++I) { FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Params.Name = FName(*FString::Printf(TEXT("Tender_%s"), Descent::Tender::Spec(I).Id).Replace(TEXT("-"), TEXT(""))); AKazahTender* T = World->SpawnActor(C->GetActorLocation(), C->GetActorRotation(), Params); if (!T) { continue; } T->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); T->InitFleet(I); Tenders.Add(T); } PlaceCradled(); UpdateClearance(); } bool UCarrierCarouselComponent::AnyTenderOut() const { for (const AKazahTender* T : Tenders) { if (T && T->Flight->State != ETenderState::Cradle) { return true; } } return false; } AKazahTender* UCarrierCarouselComponent::TenderAtBoard() const { if (IsBusy() || !Tenders.IsValidIndex(LaunchArm) || !FMath::IsNearlyEqual(HoistZ, CarTune::BoardZ, 1e-6)) { return nullptr; } AKazahTender* T = Tenders[LaunchArm]; return (T && T->Flight->State == ETenderState::Cradle) ? T : nullptr; } double UCarrierCarouselComponent::AnimRemaining() const { if (ActiveAnim == EAnim::Launch) { return FMath::Max(0.0, CarTune::LaunchSec - AnimTime); } if (ActiveAnim == EAnim::Dock) { return FMath::Max(0.0, CarTune::DockBlendSec + CarTune::DockRiseSec - AnimTime); } return 0.0; } bool UCarrierCarouselComponent::RequestRotate(FString* OutWhyNot) { auto Refuse = [OutWhyNot](const TCHAR* Why) { if (OutWhyNot) { *OutWhyNot = Why; } return false; }; if (IsBusy()) { return Refuse(TEXT("the carousel is moving")); } if (AnyTenderOut()) { return Refuse(TEXT("a Tender is out")); } if (UDescentCargoSubsystem::Get(GetWorld()) && UDescentCargoSubsystem::Get(GetWorld())->IsUnloading()) { return Refuse(TEXT("a hold is unloading")); } Phase = ECarouselPhase::Raising; return true; } bool UCarrierCarouselComponent::BeginLaunch(AKazahTender* T, FString* OutWhyNot) { if (!T || TenderAtBoard() != T) { if (OutWhyNot) { *OutWhyNot = TEXT("the Tender is not at BOARD in the hoist cradle"); } return false; } ActiveAnim = EAnim::Launch; AnimTender = LaunchArm; AnimTime = 0.0; T->Flight->State = ETenderState::Launching; return true; } bool UCarrierCarouselComponent::CanDock(const AKazahTender* T, FString* OutWhyNot) const { auto Refuse = [OutWhyNot](const TCHAR* Why) { if (OutWhyNot) { *OutWhyNot = Why; } return false; }; const ADescentCarrier* C = Carrier(); const int32 Index = IndexOf(T); if (!C || Index == INDEX_NONE) { return Refuse(TEXT("not a Tender of this carrier")); } if (!T->Flight->IsOut()) { return Refuse(TEXT("the Tender is not out")); } if (IsBusy()) { return Refuse(TEXT("the hoist is moving")); } if (Index != LaunchArm || !FMath::IsNearlyEqual(HoistZ, CarTune::BoardZ, 1e-6)) { return Refuse(TEXT("the hoist cradle is not free")); } if (!InDockZone(C->ToLocalM(T->GetActorLocation()))) { return Refuse(TEXT("not under the moon pool")); } return true; } bool UCarrierCarouselComponent::BeginDock(AKazahTender* T, FString* OutWhyNot) { if (!CanDock(T, OutWhyNot)) { return false; } ADescentCarrier* C = Carrier(); T->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); AnimFromLocal = T->GetActorTransform().GetRelativeTransform(C->GetActorTransform()); ActiveAnim = EAnim::Dock; AnimTender = IndexOf(T); AnimTime = 0.0; T->Flight->State = ETenderState::Docking; T->Flight->VelMps = FVector::ZeroVector; T->Flight->Thrust = 0.0; return true; } void UCarrierCarouselComponent::ReprintTender(AKazahTender* T) { ADescentCarrier* C = Carrier(); if (!T || !C || IndexOf(T) == INDEX_NONE) { return; } if (ActiveAnim != EAnim::None && AnimTender == IndexOf(T)) { ActiveAnim = EAnim::None; } T->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); T->Flight->ResetAsNew(CarTune::ReplacementPowerPct); T->Flight->State = ETenderState::Cradle; T->SetInBay(true); PlaceCradled(); } void UCarrierCarouselComponent::PlaceCradled() { for (int32 I = 0; I < Tenders.Num(); ++I) { AKazahTender* T = Tenders[I]; if (T && T->Flight->State == ETenderState::Cradle) { const FTransform L = CradleLocalM(I, AngleDeg, I == LaunchArm ? HoistZ : CarTune::StowedZ); T->SetActorRelativeLocation(Descent::MetresToUU(L.GetLocation())); T->SetActorRelativeRotation(L.GetRotation()); } } } void UCarrierCarouselComponent::Step(double Dt) { ADescentCarrier* C = Carrier(); if (!C || Dt <= 0.0) { return; } switch (Phase) { case ECarouselPhase::Raising: HoistZ = FMath::Min(CarTune::StowedZ, HoistZ + CarTune::HoistSpeedMps * Dt); if (HoistZ >= CarTune::StowedZ) { Phase = ECarouselPhase::Rotating; RotateLeftDeg = CarTune::RotateStepDeg; } break; case ECarouselPhase::Rotating: { const double D = FMath::Min(RotateLeftDeg, FMath::RadiansToDegrees(CarTune::RotateRateRadPerSec) * Dt); AngleDeg += D; RotateLeftDeg -= D; if (RotateLeftDeg <= 1e-9) { // +120 degrees brings the arm that was 120 degrees behind over the pool LaunchArm = (LaunchArm + CarTune::FleetCount - 1) % CarTune::FleetCount; ++RotationsDone; AngleDeg = FMath::RoundToDouble(AngleDeg / CarTune::RotateStepDeg) * CarTune::RotateStepDeg; // no drift over many turns Phase = ECarouselPhase::Lowering; } break; } case ECarouselPhase::Lowering: HoistZ = FMath::Max(CarTune::BoardZ, HoistZ - CarTune::HoistSpeedMps * Dt); if (HoistZ <= CarTune::BoardZ) { Phase = ECarouselPhase::Idle; } break; default: break; } PlaceCradled(); AKazahTender* T = Tenders.IsValidIndex(AnimTender) ? Tenders[AnimTender].Get() : nullptr; if (ActiveAnim != EAnim::None && !T) { ActiveAnim = EAnim::None; } if (ActiveAnim == EAnim::Launch) { // Section 3.5: straight down from BOARD to DOCK_BELOW over 3.2 s (smoothstep), then control is handed over. AnimTime += Dt; const double S = Descent::Smooth(0.0, CarTune::LaunchSec, AnimTime); const FTransform L = CradleLocalM(AnimTender, AngleDeg, Descent::Lerp(CarTune::BoardZ, CarTune::DockBelowZ, S)); T->SetActorRelativeLocation(Descent::MetresToUU(L.GetLocation())); T->SetActorRelativeRotation(L.GetRotation()); T->SetInBay(L.GetLocation().Z > CarBay::PoolShaftBottomZ); if (AnimTime >= CarTune::LaunchSec) { T->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); T->SetInBay(false); T->Flight->SyncFromActor(); T->Flight->VelMps = C->VelocityMps() + FVector(0.0, 0.0, -CarTune::LaunchExitDownMps); T->Flight->State = ETenderState::Piloted; ActiveAnim = EAnim::None; } } else if (ActiveAnim == EAnim::Dock) { // 1.6 s blend to DOCK_BELOW and the carrier's heading, then 2.6 s rise to BOARD. AnimTime += Dt; const FTransform Below = CradleLocalM(AnimTender, AngleDeg, CarTune::DockBelowZ); FVector Pos; FQuat Rot; if (AnimTime < CarTune::DockBlendSec) { const double S = Descent::Smooth(0.0, CarTune::DockBlendSec, AnimTime); Pos = FMath::Lerp(AnimFromLocal.GetLocation() / 100.0, Below.GetLocation(), S); Rot = FQuat::Slerp(AnimFromLocal.GetRotation(), Below.GetRotation(), S); } else { const double S = Descent::Smooth(CarTune::DockBlendSec, CarTune::DockBlendSec + CarTune::DockRiseSec, AnimTime); Pos = FVector(Below.GetLocation().X, Below.GetLocation().Y, Descent::Lerp(CarTune::DockBelowZ, CarTune::BoardZ, S)); Rot = Below.GetRotation(); } T->SetActorRelativeLocation(Descent::MetresToUU(Pos)); T->SetActorRelativeRotation(Rot); T->SetInBay(Pos.Z > CarBay::PoolShaftBottomZ); if (AnimTime >= CarTune::DockBlendSec + CarTune::DockRiseSec) { T->Flight->State = ETenderState::Cradle; T->Flight->SyncFromActor(); T->SetInBay(true); ActiveAnim = EAnim::None; PlaceCradled(); } } UpdateVisuals(); UpdateClearance(); } void UCarrierCarouselComponent::UpdateVisuals() { if (!ArmsRoot) { return; } ArmsRoot->SetRelativeRotation(FRotator(0.0, AngleDeg, 0.0)); for (int32 Arm = 0; Arm < Carriages.Num(); ++Arm) { const double Z = Arm == LaunchArm ? HoistZ : CarTune::StowedZ; FVector L = Carriages[Arm]->GetRelativeLocation(); L.Z = Descent::MetresToUU(Z - CarTune::HubZ); Carriages[Arm]->SetRelativeLocation(L); const double MastLen = (CarTune::ArmBeamZ - 0.1) - (Z + CarTune::ClampAboveOriginM + 0.10); Masts[Arm]->SetRelativeScale3D(FVector(1.0, 1.0, FMath::Max(0.01, MastLen))); } } void UCarrierCarouselComponent::UpdateClearance() { // Live V5 number: the smallest distance between any two Tenders that are in the bay or in the shaft, and from each to the bay itself. const ADescentCarrier* C = Carrier(); TArray> Boxes; for (int32 I = 0; I < Tenders.Num(); ++I) { const AKazahTender* T = Tenders[I]; if (!T || !C) { continue; } const ETenderState S = T->Flight->State; const bool bRising = S == ETenderState::Docking && AnimTime >= CarTune::DockBlendSec; if (S == ETenderState::Cradle || S == ETenderState::Launching || bRising) { const FTransform L = T->GetActorTransform().GetRelativeTransform(C->GetActorTransform()); Boxes.Add(TenderBox(L.GetLocation() / 100.0, L.Rotator().Yaw)); } } double Best = 1e9; for (int32 I = 0; I < Boxes.Num(); ++I) { Best = FMath::Min(Best, ObstacleDistance(Boxes[I])); for (int32 K = I + 1; K < Boxes.Num(); ++K) { Best = FMath::Min(Best, BoxDistance(Boxes[I], Boxes[K])); } } MinClearanceM = Best; MinClearanceSeenM = FMath::Min(MinClearanceSeenM, Best); } // ---- boot: three Tenders into the cradles (section 1.5 step 3) ------------------------------------------------------ DESCENT_BOOT_STEP(SpawnTenders, Descent::Boot::Order::SpawnTenders) { ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(Ctx.World); if (!C || !C->Carousel) { Ctx.Fail(TEXT("no carrier with a carousel to hang the Tenders in")); return; } C->Carousel->SpawnTenders(); if (C->Carousel->NumTenders() != Descent::Tune::Tender::FleetCount) { Ctx.Fail(FString::Printf(TEXT("spawned %d of %d Tenders"), C->Carousel->NumTenders(), Descent::Tune::Tender::FleetCount)); } }