#include "Vehicles/CarrierLayout.h" #include "Algo/Reverse.h" #include "Core/DescentMath.h" namespace Descent::Carrier { namespace Tune = Descent::Tune::Carrier; double HullRadius(double X) { if (X > Tune::HullCylinderHalfLengthM) { const double T = (X - Tune::HullCylinderHalfLengthM) / 20.0; return Tune::HullRadiusM * FMath::Sqrt(FMath::Max(0.0, 1.0 - T * T)); } if (X >= -Tune::HullCylinderHalfLengthM) { return Tune::HullRadiusM; } return Tune::HullRadiusM - 5.0 * Descent::Smooth(40.0, 60.0, -X); } bool IsDryLocal(const FVector& P) { if (FMath::Abs(P.X) >= Tune::HullHalfLengthM) { return false; } if (FMath::Sqrt(P.Y * P.Y + P.Z * P.Z) >= HullRadius(P.X) - Tune::DryHullMarginM) { return false; } const bool bInPoolRect = P.X >= Tune::PoolX0 && P.X <= Tune::PoolX1 && P.Y >= Tune::PoolY0 && P.Y <= Tune::PoolY1; return !(bInPoolRect && P.Z < Tune::PoolWaterZ); } double FDoorDef::FloorZ() const { switch (Deck) { case 1: return Tune::DeckFloorZ1; case 2: return Tune::DeckFloorZ2; default: return Tune::DeckFloorZ3; } } const FLayout& FLayout::Get() { static const FLayout Instance; return Instance; } FLayout::FLayout() { Build(); } const FRoomDef* FLayout::FindRoom(FName Id) const { return Rooms.FindByPredicate([Id](const FRoomDef& R) { return R.Id == Id; }); } const FRoomDef* FLayout::RoomAt(const FVector& P) const { const FRoomDef* Best = nullptr; double BestMargin = -1e9; for (const FRoomDef& R : Rooms) { if (P.Z < R.FloorZ - 0.3 || P.Z > R.CeilZ) { continue; } const double Margin = FMath::Min(FMath::Min(P.X - R.X0, R.X1 - P.X), FMath::Min(P.Y - R.Y0, R.Y1 - P.Y)); if (Margin >= -1e-9 && Margin > BestMargin) { BestMargin = Margin; Best = &R; } } return Best; } bool FLayout::DoorRooms(const FDoorDef& D, const FRoomDef*& OutA, const FRoomDef*& OutB) const { const FVector2D Mid = D.Mid(); const double Z = D.FloorZ() + 1.0; const FVector2D N = D.WallAtConstX() ? FVector2D(1, 0) : FVector2D(0, 1); OutA = RoomAt(FVector(Mid.X - 0.8 * N.X, Mid.Y - 0.8 * N.Y, Z)); OutB = RoomAt(FVector(Mid.X + 0.8 * N.X, Mid.Y + 0.8 * N.Y, Z)); return OutA && OutB && OutA != OutB; } void FLayout::WallHoles(const FRoomDef& R, bool bConstX, double At, double Lo, double Hi, TArray& OutAlong, TArray& OutZ) const { for (const FDoorDef& D : Doors) { // A door belongs to a wall of this room if its deck is a deck the room spans (tall rooms span two). const double DZ = D.FloorZ(); if (DZ < R.FloorZ - 0.01 || DZ + Tune::DoorHeightM > R.CeilZ + 0.01) { continue; } if (D.WallAtConstX() != bConstX) { continue; } const double Thin0 = bConstX ? D.X0 : D.Y0, Thin1 = bConstX ? D.X1 : D.Y1; const double A0 = bConstX ? D.Y0 : D.X0, A1 = bConstX ? D.Y1 : D.X1; if (At < Thin0 - 1e-6 || At > Thin1 + 1e-6 || A1 <= Lo || A0 >= Hi) { continue; } OutAlong.Add(FVector2D(FMath::Max(Lo, A0), FMath::Min(Hi, A1))); OutZ.Add(FVector2D(DZ, DZ + Tune::DoorHeightM)); } for (const FWindowDef& W : Windows) { if (W.Room == R.Id && W.bWallAtConstX == bConstX && FMath::IsNearlyEqual(W.At, At, 1e-6)) { OutAlong.Add(FVector2D(W.A0, W.A1)); OutZ.Add(FVector2D(R.FloorZ + W.H0, R.FloorZ + W.H1)); } } } FName FLayout::NodeOf(const FRoomDef& R, int32 DoorDeck) const { return DoorDeck != R.Deck ? FName(*(R.Id.ToString() + TEXT("_up"))) : R.Id; } bool FLayout::FindRoute(const FVector& From, FName ToRoom, const FVector2D* ToPoint, TArray& Out) const { Out.Reset(); const FRoomDef* Start = RoomAt(FVector(From.X, From.Y, From.Z + 0.3)); // "stair_up" / "bay_up" name the upper level of a tall room (the D3 landing, the D2 gallery) as a target FName TargetNode = ToRoom; FVector2D UpStop = FVector2D::ZeroVector; bool bUpTarget = false; if (ToRoom == TEXT("stair_up")) { TargetNode = ToRoom; bUpTarget = true; UpStop = FVector2D(7.2, 5.0); } else if (ToRoom == TEXT("bay_up")) { TargetNode = ToRoom; bUpTarget = true; UpStop = FVector2D(-19.2, -1.0); } const FRoomDef* Target = FindRoom(bUpTarget ? FName(*ToRoom.ToString().Replace(TEXT("_up"), TEXT(""))) : ToRoom); if (!Start || !Target) { return false; } // Node of the start: a tall room entered at its upper level is the "_up" node. FName StartNode = Start->Id; if (Start->bTall && From.Z > Start->FloorZ + 2.0) { StartNode = FName(*(Start->Id.ToString() + TEXT("_up"))); } // Breadth-first search over links. TMap Prev; // node -> index of link used to reach it (signed: +1+i forward, -1-i reverse) TArray Queue{ StartNode }; TSet Seen{ StartNode }; for (int32 Head = 0; Head < Queue.Num(); ++Head) { const FName Cur = Queue[Head]; for (int32 I = 0; I < Links.Num(); ++I) { const FLink& L = Links[I]; FName Next = NAME_None; int32 Code = 0; if (L.A == Cur) { Next = L.B; Code = 1 + I; } else if (L.B == Cur) { Next = L.A; Code = -1 - I; } if (Next != NAME_None && !Seen.Contains(Next)) { Seen.Add(Next); Prev.Add(Next, Code); Queue.Add(Next); } } } if (!Seen.Contains(TargetNode.IsNone() || bUpTarget ? TargetNode : Target->Id)) { return false; } TArray> Legs; for (FName Node = bUpTarget ? TargetNode : Target->Id; Node != StartNode;) { const int32 Code = Prev[Node]; const FLink& L = Links[(Code > 0 ? Code : -Code) - 1]; TArray Leg = L.Via; if (Code < 0) { Algo::Reverse(Leg); } Legs.Add(MoveTemp(Leg)); Node = Code > 0 ? L.A : L.B; } for (int32 I = Legs.Num() - 1; I >= 0; --I) { Out.Append(Legs[I]); } Out.Add(ToPoint ? *ToPoint : (bUpTarget ? UpStop : Target->Stop)); return true; } namespace { FLinearColor Hex(uint32 Rgb) { return FLinearColor::FromSRGBColor(FColor((Rgb >> 16) & 0xFF, (Rgb >> 8) & 0xFF, Rgb & 0xFF)); } } void FLayout::Build() { auto AddRoom = [this](const TCHAR* Id, const TCHAR* Name, int32 Deck, double X0, double X1, double Y0, double Y1, bool bTall, uint32 Colour, double Intensity, std::initializer_list Lamps, double StopX, double StopY, double StopYaw) -> FRoomDef& { FRoomDef R; R.Id = Id; R.Name = Name; R.Deck = Deck; R.X0 = X0; R.X1 = X1; R.Y0 = Y0; R.Y1 = Y1; R.bTall = bTall; R.FloorZ = Deck == 1 ? Tune::DeckFloorZ1 : (Deck == 2 ? Tune::DeckFloorZ2 : Tune::DeckFloorZ3); R.CeilZ = R.FloorZ + (bTall ? Tune::TallRoomHeightM : Tune::RoomHeightM); R.LampColor = Hex(Colour); R.LampIntensity = Intensity; R.Lamps = TArray(Lamps); R.Stop = FVector2D(StopX, StopY); R.StopYawDeg = StopYaw; return Rooms.Add_GetRef(R); }; // Section 3.3 room table (x, y ranges as the design lists them) and lamp table (x, y under the ceiling). AddRoom(TEXT("bridge"), TEXT("BRIDGE"), 3, 36, 48, -8, 8, false, 0x8fc8ff, 1.3, { {41, 0}, {44, -5}, {44, 5} }, 38.0, -5.0, 25); AddRoom(TEXT("q1"), TEXT("QUARTERS A"), 3, 28, 36, -9, -1.6, false, 0xffd2a0, 1.1, { {32, -5} }, 30.5, -2.5, -60); AddRoom(TEXT("q2"), TEXT("QUARTERS B"), 3, 20, 28, -9, -1.6, false, 0xffd2a0, 1.1, { {24, -5} }, 22.5, -3.0, -55); AddRoom(TEXT("q3"), TEXT("QUARTERS C"), 3, 28, 36, 1.6, 9, false, 0xffd2a0, 1.0, { {32, 5} }, 30.5, 2.5, 60); AddRoom(TEXT("q4"), TEXT("QUARTERS D"), 3, 20, 28, 1.6, 9, false, 0xff3018, 1.4, { {24, 5} }, 22.5, 3.0, 55).bEmergency = true; AddRoom(TEXT("hab"), TEXT("HABITATION"), 3, 6, 20, -9, -1.6, false, 0xffe0b8, 1.2, { {16, -5}, {9, -5} }, 17.5, -3.0, -160); AddRoom(TEXT("fab"), TEXT("FABRICATION"), 3, -8, 6, -9, -1.6, false, 0xa8e8ff, 1.4, { {2, -5}, {-5, -5} }, 3.0, -3.0, -135); AddRoom(TEXT("comp"), TEXT("COMPONENTS"), 3, -8, 6, 1.6, 9, false, 0x9affc8, 0.9, { {-1, 5} }, 4.5, 3.0, 120); AddRoom(TEXT("corr3"), TEXT("UPPER DECK"), 3, -8, 36, -1.6, 1.6, false, 0xd8e8ff, 0.9, { {30, 0}, {18, 0}, {6, 0}, {-4, 0} }, 14, 0, 0); AddRoom(TEXT("stair"), TEXT("STAIRWELL"), 2, 6, 20, 1.6, 9, true, 0xd8e8ff, 1.0, { {13, 5}, {10, 5, 4.5} }, 19.0, 3.0, 150); AddRoom(TEXT("galley"), TEXT("GALLEY"), 2, 20, 36, -10, 10, false, 0xffd8a8, 1.2, { {28, -5}, {28, 5}, {33, 0} }, 23.5, 0, 0); AddRoom(TEXT("cargo"), TEXT("CARGO BAY"), 2, 6, 20, -10, -1.6, false, 0xd0e0ff, 1.1, { {13, -6} }, 11.0, -4.5, -20); AddRoom(TEXT("refinery"), TEXT("ORE PROCESSING"), 2, -16, 6, -10, -1.6, false, 0xffb070, 1.2, { {0, -6}, {-10, -6} }, -8.0, -4.0, -15); AddRoom(TEXT("engine"), TEXT("ENGINE ROOM"), 2, -16, 6, 1.6, 10, false, 0x80c0ff, 1.0, { {0, 6}, {-10, 6} }, -11.0, 3.5, 20).bPulses = true; AddRoom(TEXT("corr2"), TEXT("MID DECK"), 2, -18, 20, -1.6, 1.6, false, 0xd8e8ff, 0.9, { {14, 0}, {2, 0}, {-10, 0} }, 8.0, 0.0, 180); AddRoom(TEXT("bay"), TEXT("SUB BAY"), 1, -40, -18, -9, 9, true, 0xc8e8ff, 1.5, { {-24, -5}, {-34, 5}, {-36, -5}, {-22, 5}, {-29, -8} }, -26.0, -2.0, 90); // Doors (design 3.3): deck, y range, x range. auto AddDoor = [this](int32 Deck, double Y0, double Y1, double X0, double X1) { Doors.Add({ Deck, Y0, Y1, X0, X1 }); }; AddDoor(3, -1.2, 1.2, 35.6, 36.4); // bridge AddDoor(3, -2, -1.2, 31, 33); // q1 AddDoor(3, -2, -1.2, 23, 25); // q2 AddDoor(3, 1.2, 2, 31, 33); // q3 AddDoor(3, 1.2, 2, 23, 25); // q4 AddDoor(3, -2, -1.2, 12, 14); // hab AddDoor(3, 1.2, 2, 6.3, 8); // stair landing AddDoor(3, -2, -1.2, -2, 0); // fab AddDoor(3, 1.2, 2, -2, 0); // comp AddDoor(2, -1.2, 1.2, 19.6, 20.4); // galley AddDoor(2, -2, -1.2, 13, 15); // cargo AddDoor(2, 1.2, 2, 15, 17); // stair AddDoor(2, -2, -1.2, -6, -4); // refinery AddDoor(2, 1.2, 2, -6, -4); // engine AddDoor(2, -1.2, 1.2, -18.4, -17.6); // bay gallery // Windows (v2 WINDOWS, converted): bridge bow wall and sides, hab / q1 portholes, galley both sides. auto AddWin = [this](const TCHAR* Room, bool bConstX, double At, double A0, double A1, double H0, double H1) { Windows.Add({ FName(Room), bConstX, At, A0, A1, H0, H1 }); }; AddWin(TEXT("bridge"), true, 48, -6.5, 6.5, 1.6, 3.8); AddWin(TEXT("bridge"), false, -8, 42, 46.5, 1.8, 3.4); AddWin(TEXT("bridge"), false, 8, 42, 46.5, 1.8, 3.4); AddWin(TEXT("hab"), false, -9, 12, 17, 0.6, 2.2); AddWin(TEXT("q1"), false, -9, 31, 33, 1.4, 2.4); AddWin(TEXT("galley"), false, -10, 24, 32, 1.2, 2.6); AddWin(TEXT("galley"), false, 10, 24, 32, 1.2, 2.6); // Fixed stations (3.4). auto AddAnchor = [this](const TCHAR* Id, const TCHAR* Kind, double X, double Y, double Z, double Yaw, const TCHAR* Prompt, double Range, double Dot) { FAnchorDef A; A.Id = Id; A.Kind = Kind; A.PosM = FVector(X, Y, Z); A.YawDeg = Yaw; A.Prompt = Prompt; A.RangeM = Range; A.MinDot = Dot; Anchors.Add(A); }; AddAnchor(TEXT("helm"), TEXT("Helm"), 44.3, 0, 2.7, 180, TEXT("Take the helm - pilot the CANTICLE"), 2.6, 0.55); AddAnchor(TEXT("chart"), TEXT("Chart"), 39.5, 0, 2.4, 0, TEXT("Chart table - open the survey map"), 2.4, 0.6); AddAnchor(TEXT("bed_q1"), TEXT("Bed"), 33, -7.3, 2.1, 90, TEXT("Sleep - pass time (the refinery keeps working)"), 2.4, 0.5); AddAnchor(TEXT("bed_q2"), TEXT("Bed"), 25, -7.3, 2.1, 90, TEXT("Sleep - pass time (the refinery keeps working)"), 2.4, 0.5); AddAnchor(TEXT("bed_q3"), TEXT("Bed"), 33, 7.3, 2.1, -90, TEXT("Sleep - pass time (the refinery keeps working)"), 2.4, 0.5); AddAnchor(TEXT("bed_q4"), TEXT("Bed"), 25, 7.3, 2.1, -90, TEXT("Sleep - pass time (the refinery keeps working)"), 2.4, 0.5); AddAnchor(TEXT("port_1"), TEXT("Port"), 24, -9.76, -1.55, 90, TEXT("Jack in - nutrient feed"), 2.3, 0.75); AddAnchor(TEXT("port_2"), TEXT("Port"), 24, 9.76, -1.55, -90, TEXT("Jack in - nutrient feed"), 2.3, 0.75); AddAnchor(TEXT("port_3"), TEXT("Port"), 32, -9.76, -1.55, 90, TEXT("Jack in - nutrient feed"), 2.3, 0.75); AddAnchor(TEXT("vat"), TEXT("Vat"), 21.2, 6.0, -1.6, 0, TEXT("Nutrient vat - biomass reserve"), 2.4, 0.6); AddAnchor(TEXT("packer"), TEXT("Packer"), 18.6, -5.0, -1.6, 0, TEXT("Ore packer"), 2.6, 0.55); AddAnchor(TEXT("storage"), TEXT("Storage"), 13, -9.0, -1.8, 90, TEXT("Carrier storage lockers"), 2.8, 0.55); AddAnchor(TEXT("terminal"), TEXT("Terminal"), 9, -1.8, -1.6, 90, TEXT("Tethys sales terminal - sell ore and refined goods"), 2.4, 0.7); AddAnchor(TEXT("carousel"), TEXT("Carousel"), -22, 1.6, -6.3, 0, TEXT("Carousel control - rotate the next Tender to the launch cradle"), 2.4, 0.6); AddAnchor(TEXT("ladder"), TEXT("Ladder"), -29, 3.3, -7.3, 0, TEXT("Drop into the moon pool (swim)"), 2.2, 0.4); // Route graph. Every door is a link; its waypoints are one metre before, in and one metre after the door. for (const FDoorDef& D : Doors) { const FRoomDef* A = nullptr; const FRoomDef* B = nullptr; if (!DoorRooms(D, A, B)) { continue; } const FVector2D Mid = D.Mid(); const FVector2D N = D.WallAtConstX() ? FVector2D(1, 0) : FVector2D(0, 1); FLink L; L.A = NodeOf(*A, D.Deck); L.B = NodeOf(*B, D.Deck); L.Via = { Mid - N, Mid, Mid + N }; Links.Add(L); } { FLink Ramp; // D2 floor of the stairwell -> D3 landing, up the 22.9 degree ramp (y 5.25..8.55) Ramp.A = TEXT("stair"); Ramp.B = TEXT("stair_up"); Ramp.Via = { FVector2D(19.3, 3.5), FVector2D(19.3, 6.9), FVector2D(17.5, 6.9), FVector2D(9.0, 6.9), FVector2D(7.2, 6.9) }; Links.Add(Ramp); FLink Bay; // gallery (D2) -> bay floor (D1), down the stairs along the port wall (y -8.75..-7.25) Bay.A = TEXT("bay_up"); Bay.B = TEXT("bay"); Bay.Via = { FVector2D(-19.0, -1.0), FVector2D(-19.0, -8.0), FVector2D(-21.0, -8.0), FVector2D(-29.0, -8.0), FVector2D(-31.5, -7.0) }; Links.Add(Bay); } } }