#pragma once #include "CoreMinimal.h" #include "Components/SceneComponent.h" #include "Core/DescentHandle.h" #include "CarrierCarousel.generated.h" class AKazahTender; class ADescentCarrier; class UProceduralMeshComponent; /** * Sub bay carousel and hoist (DESIGN_UE.md section 3.5, requirement V5: subs never clip). The geometry is pure functions in * carrier-local metres, so the clearance sweep of the automation test and the live `carrier.carousel.min_clearance_m` are the same code. */ namespace Descent::Carrier::Carousel { /** A box that is only turned about Z: centre, half extents, yaw. Enough for Tenders in cradles and every bay obstacle. */ struct FYawBox { FVector C = FVector::ZeroVector; FVector Half = FVector::OneVector; double YawDeg = 0.0; const TCHAR* Name = TEXT(""); }; /** Yaw of arm 0..2 for a hub angle: 90 + angle + 120 arm (arm 0 starts over the pool). */ THEDESCENT_API double ArmYawDeg(int32 Arm, double AngleDeg); /** Where a Tender hangs: origin at the cradle (radius 6 from the hub), nose tangential (toward the bow at the launch slot). */ THEDESCENT_API FTransform CradleLocalM(int32 Arm, double AngleDeg, double Z); /** The Tender's model bounds (6.99 x 3.27 x 2.51 m) around a Tender origin and yaw. */ THEDESCENT_API FYawBox TenderBox(const FVector& OriginM, double YawDeg); /** Exact distance between two yaw-only boxes (0 when they intersect). */ THEDESCENT_API double BoxDistance(const FYawBox& A, const FYawBox& B); /** * Everything in the bay a Tender must not touch: walls, ceiling, floor around the pool, the D2 gallery and its rail, the stairs and * their rail, the hub, the pipe riser and the unload boom (section 6.3, stowed; they are built in M5), the pool rails and shaft walls. */ THEDESCENT_API const TArray& BayObstacles(); /** The smallest distance from a Tender box to any bay obstacle; OutName names the obstacle. */ THEDESCENT_API double ObstacleDistance(const FYawBox& Tender, const TCHAR** OutName = nullptr); /** True when a Tender at carrier-local P may dock (section 3.5: +-9 m in x, +-7 m in y of the launch column, z -34..-11). */ THEDESCENT_API bool InDockZone(const FVector& LocalM); } enum class ECarouselPhase : uint8 { Idle, Raising, Rotating, Lowering }; /** * The carousel on the carrier: hub and three arms (a rotating blockout), a hoist carriage per arm, the three Tenders. It moves the * Tenders that are in a cradle, launching or docking (sim step Carousel); a Tender that is out belongs to its flight component. */ UCLASS(ClassGroup = (Descent)) class THEDESCENT_API UCarrierCarouselComponent : public USceneComponent { GENERATED_BODY() public: UCarrierCarouselComponent(); /** Builds the blockout. Called by the carrier's BeginPlay after the rooms exist. */ void Build(); void Shutdown(); /** Spawns the three Tenders into the cradles (boot step SpawnTenders). */ void SpawnTenders(); AKazahTender* GetTender(int32 Index) const { return Tenders.IsValidIndex(Index) ? Tenders[Index] : nullptr; } int32 NumTenders() const { return Tenders.Num(); } int32 IndexOf(const AKazahTender* T) const; // ---- state ---- double GetAngleDeg() const { return AngleDeg; } ECarouselPhase GetPhase() const { return Phase; } bool IsBusy() const { return Phase != ECarouselPhase::Idle || ActiveAnim != EAnim::None; } double GetHoistZ() const { return HoistZ; } /** The arm (= Tender index) whose cradle is the hoist over the pool right now (while rotating: the one it is turning to). */ int32 GetLaunchSlot() const { return LaunchArm; } bool AnyTenderOut() const; double GetMinClearanceM() const { return MinClearanceM; } /** The smallest clearance seen since the last ResetMinClearanceSeen (scenarios assert it "throughout"). */ double GetMinClearanceSeenM() const { return MinClearanceSeenM; } void ResetMinClearanceSeen() { MinClearanceSeenM = 1e9; } int32 GetRotationsDone() const { return RotationsDone; } /** The Tender in the hoist cradle at BOARD with nothing moving, or null. */ AKazahTender* TenderAtBoard() const; // ---- requests (false + reason when refused) ---- bool RequestRotate(FString* OutWhyNot = nullptr); bool BeginLaunch(AKazahTender* T, FString* OutWhyNot = nullptr); bool CanDock(const AKazahTender* T, FString* OutWhyNot = nullptr) const; bool BeginDock(AKazahTender* T, FString* OutWhyNot = nullptr); /** After an implosion: the Tender is "printed" again into the hoist cradle with a full hull and 60 % power. */ void ReprintTender(AKazahTender* T); /** Seconds the current launch or dock still takes (0 when none). */ double AnimRemaining() const; private: enum class EAnim : uint8 { None, Launch, Dock }; void Step(double Dt); void PlaceCradled(); void UpdateVisuals(); void UpdateClearance(); ADescentCarrier* Carrier() const; UPROPERTY(Transient) TArray> Tenders; UPROPERTY(Transient) TObjectPtr ArmsRoot; UPROPERTY(Transient) TArray> Carriages; UPROPERTY(Transient) TArray> Masts; double AngleDeg = 0.0; double RotateLeftDeg = 0.0; ECarouselPhase Phase = ECarouselPhase::Idle; double HoistZ = 0.0; int32 LaunchArm = 0; int32 RotationsDone = 0; EAnim ActiveAnim = EAnim::None; int32 AnimTender = -1; double AnimTime = 0.0; FTransform AnimFromLocal; // dock: where the Tender was (carrier-local, uu) when E was pressed double MinClearanceM = 1e9; double MinClearanceSeenM = 1e9; FDescentHandle StepHandle; bool bBuilt = false; };