#include "Vehicles/CarrierBuild.h" #include "Core/DescentMath.h" namespace Descent::Carrier { namespace Tune = Descent::Tune::Carrier; namespace { /** A box given in v2 terms (x, y up, z toward the stern; sizes sx, sy, sz): converted with (X = -z, Y = x, Z = y). */ void V2Box(FMeshBuilder& B, EMat M, double Sx, double Sy, double Sz, double X, double Y, double Z, bool bCol = true, double YawRad = 0.0) { B.Box(M, FVector(-Z, X, Y), FVector(Sz, Sx, Sy), bCol, FQuat(FVector(0, 0, 1), YawRad)); } void V2Cyl(FMeshBuilder& B, EMat M, double R, double H, double X, double Y, double Z, int32 Segs, bool bCol = true) { B.Cylinder(M, FVector(-Z, X, Y), R, H, Segs, 2, bCol); } /** A thin rail from A to B at 1 m above the walking line, plus a tall invisible collision slab under it. */ void Rail(FMeshBuilder& B, const FVector& A, const FVector& C, bool bCollide = true) { const FVector Dir = (C - A).GetSafeNormal(); const double Len = (C - A).Size(); const FQuat Q = FQuat::FindBetweenNormals(FVector(1, 0, 0), Dir); B.Box(EMat::Hazard, 0.5 * (A + C) + FVector(0, 0, 1.0), FVector(Len, 0.05, 0.05), false, Q); B.Box(EMat::Wall2, 0.5 * (A + C) + FVector(0, 0, 0.5), FVector(Len, 0.04, 0.04), false, Q); if (bCollide) { FVector Side(-Dir.Y, Dir.X, 0.0); Side = Side.GetSafeNormal() * 0.04; TArray P; for (const FVector& E : { A, C }) { P.Add(E + Side); P.Add(E - Side); P.Add(E + Side + FVector(0, 0, 1.3)); P.Add(E - Side + FVector(0, 0, 1.3)); } B.AddConvexPoints(P); } } void Walls(const FRoomDef& R, FMeshBuilder& B) { const FLayout& L = FLayout::Get(); const double T = Tune::WallThicknessM; auto Wall = [&](bool bConstX, double At, double Lo, double Hi, double Inward) { TArray Along, Zr; L.WallHoles(R, bConstX, At, Lo, Hi, Along, Zr); TArray Order; for (int32 I = 0; I < Along.Num(); ++I) { Order.Add(I); } Order.Sort([&Along](int32 A, int32 C) { return Along[A].X < Along[C].X; }); const double Off = Inward * 0.5 * T; auto Seg = [&](double U0, double U1, double V0, double V1) { if (U1 - U0 < 0.02 || V1 - V0 < 0.02) { return; } const double Cu = 0.5 * (U0 + U1), Cv = 0.5 * (V0 + V1); if (bConstX) { B.Box(EMat::Wall, FVector(At + Off, Cu, Cv), FVector(T, U1 - U0, V1 - V0), true); } else { B.Box(EMat::Wall, FVector(Cu, At + Off, Cv), FVector(U1 - U0, T, V1 - V0), true); } }; double U = Lo; for (int32 K : Order) { const FVector2D H = Along[K], Z = Zr[K]; Seg(U, H.X, R.FloorZ, R.CeilZ); Seg(H.X, H.Y, R.FloorZ, Z.X); Seg(H.X, H.Y, Z.Y, R.CeilZ); U = H.Y; } Seg(U, Hi, R.FloorZ, R.CeilZ); }; Wall(true, R.X0, R.Y0, R.Y1, +1.0); Wall(true, R.X1, R.Y0, R.Y1, -1.0); Wall(false, R.Y0, R.X0, R.X1, +1.0); Wall(false, R.Y1, R.X0, R.X1, -1.0); } /** Steps drawn as solid blocks (no collision) along a ramp rising from (XLow, ZLow) to (XHigh, ZHigh), y range Y0..Y1. */ void StepBlocks(FMeshBuilder& B, double XLow, double XHigh, double ZLow, double ZHigh, double ZBase, double Y0, double Y1, int32 Count) { for (int32 I = 0; I < Count; ++I) { const double T0 = static_cast(I) / Count, T1 = static_cast(I + 1) / Count; const double Xa = Descent::Lerp(XLow, XHigh, T0), Xb = Descent::Lerp(XLow, XHigh, T1); const double Top = Descent::Lerp(ZLow, ZHigh, 0.5 * (T0 + T1)); B.BoxMinMax(EMat::Wall2, FVector(FMath::Min(Xa, Xb), Y0, ZBase), FVector(FMath::Max(Xa, Xb), Y1, Top), false); } } void RoomShell(const FRoomDef& R, FMeshBuilder& B) { const double W = R.X1 - R.X0, D = R.Y1 - R.Y0, Cx = 0.5 * (R.X0 + R.X1), Cy = 0.5 * (R.Y0 + R.Y1); if (R.Id == TEXT("bay")) { // D1 floor with the moon pool left out: one big slab west of the pool edge (y < 3.5) and slabs around the other three sides. auto Slab = [&](double X0, double X1, double Y0, double Y1) { B.BoxMinMax(EMat::Floor, FVector(X0, Y0, R.FloorZ - Tune::FloorSlabM), FVector(X1, Y1, R.FloorZ), true); }; Slab(R.X0, R.X1, R.Y0, Tune::PoolY0); Slab(R.X0, R.X1, Tune::PoolY1, R.Y1); Slab(R.X0, Tune::PoolX0, Tune::PoolY0, Tune::PoolY1); Slab(Tune::PoolX1, R.X1, Tune::PoolY0, Tune::PoolY1); // D2 gallery along the bow wall of the bay. B.BoxMinMax(EMat::Floor, FVector(-19.8, R.Y0, Tune::DeckFloorZ2 - 0.3), FVector(R.X1, R.Y1, Tune::DeckFloorZ2), true); } else { B.Box(EMat::Floor, FVector(Cx, Cy, R.FloorZ - 0.5 * Tune::FloorSlabM), FVector(W, D, Tune::FloorSlabM), true); } B.Box(EMat::Ceiling, FVector(Cx, Cy, R.CeilZ + 0.5 * Tune::CeilingSlabM), FVector(W, D, Tune::CeilingSlabM), true); Walls(R, B); for (const FLampDef& Lamp : R.Lamps) { const double Z = Lamp.ZAboveFloor >= 0.0 ? R.FloorZ + Lamp.ZAboveFloor : R.CeilZ - 0.12; B.Box(R.bEmergency ? EMat::LampRed : EMat::LampWhite, FVector(Lamp.X, Lamp.Y, Z), FVector(0.3, 1.2, 0.08), false); } } void Props(const FRoomDef& R, FMeshBuilder& B) { const double Y3 = Tune::DeckFloorZ3, Y2 = Tune::DeckFloorZ2, Y1 = Tune::DeckFloorZ1; const FName Id = R.Id; if (Id == TEXT("bridge")) { for (double X : { -6.0, 6.0 }) { V2Box(B, EMat::Wall2, 1.4, 1.0, 3, X, Y3 + 0.5, -44); } } else if (Id == TEXT("q1") || Id == TEXT("q2") || Id == TEXT("q3") || Id == TEXT("q4")) { // the locker at the foot of the bed (the bed itself is a fixture) const double BedX = (Id == TEXT("q1")) ? 33 : (Id == TEXT("q2") ? 25 : (Id == TEXT("q3") ? 33 : 25)); const double Side = (Id == TEXT("q1") || Id == TEXT("q2")) ? -1.0 : 1.0; B.Box(EMat::Wall2, FVector(BedX - 2.8, Side * 6.4, Y3 + 0.95), FVector(0.6, 0.8, 1.9), true); if (Id == TEXT("q4")) { // the emergency cot room: a folded cot and a locker door left open B.Box(EMat::Wall2, FVector(21.2, 3.4, Y3 + 0.42), FVector(0.7, 1.9, 0.12), true); B.Box(EMat::Fabric, FVector(21.2, 3.4, Y3 + 0.5), FVector(0.62, 1.8, 0.1), false); B.Box(EMat::Wall2, FVector(21.6, 8.2, Y3 + 1.0), FVector(0.08, 0.9, 2.0), false, FQuat(FVector(0, 0, 1), -0.5)); } } else if (Id == TEXT("hab")) { // habitation lounge (start placeables later, section 3.4): couch against the -y wall, table, shelf B.Box(EMat::Fabric, FVector(14, -8.0, Y3 + 0.22), FVector(3.4, 1.0, 0.45), true); B.Box(EMat::Fabric, FVector(14, -8.6, Y3 + 0.6), FVector(3.4, 0.3, 0.8), true); B.Box(EMat::Wood, FVector(14, -5.2, Y3 + 0.25), FVector(0.9, 1.4, 0.5), true); B.Box(EMat::Wood, FVector(9, -2.0, Y3 + 1.1), FVector(3.5, 0.4, 2.2), true); } else if (Id == TEXT("fab")) { // fabricator (start placeable) at (-1, -6.5) facing +y, and a workbench // M7: the fabricator itself is spawned as a placeable (boot step SpawnStartFabricator) B.Box(EMat::Wall2, FVector(-5.5, -7.5, Y3 + 0.45), FVector(1.0, 3.0, 0.9), true); } else if (Id == TEXT("comp")) { for (int32 I = 0; I < 4; ++I) { const double X = 4.0 - 2.8 * I; B.Box(EMat::Wall2, FVector(X, 8.3, Y3 + 1.1), FVector(0.6, 1.0, 2.2), true); B.Box(I % 2 ? EMat::LampCyan : EMat::LampAmber, FVector(X, 7.77, Y3 + 1.6), FVector(0.3, 0.05, 0.2), false); } } else if (Id == TEXT("corr3") || Id == TEXT("corr2")) { const double Xa = Id == TEXT("corr3") ? -8.0 : -18.0, Xb = Id == TEXT("corr3") ? 36.0 : 20.0; const double Zc = R.CeilZ; const struct { double Y, Rad; EMat M; } Pipes[] = { { -1.15, 0.1, EMat::Wall2 }, { -0.75, 0.06, EMat::Hazard }, { 1.15, 0.09, EMat::Wall2 }, { 0.8, 0.05, EMat::Wall } }; for (const auto& P : Pipes) { B.Cylinder(P.M, FVector(0.5 * (Xa + Xb), P.Y, Zc - 0.28), P.Rad, Xb - Xa, 8, 0, false); } } else if (Id == TEXT("stair")) { // D3 landing slab, the ramp (smooth collision wedge + stepped blocks), rails B.BoxMinMax(EMat::Floor, FVector(6.0, 1.6, Y3 - 0.3), FVector(8.0, 9.0, Y3), true); const TArray Poly{ FVector2D(18.6, Y2), FVector2D(7.95, Y3), FVector2D(7.95, Y2) }; B.ExtrudeY(EMat::Wall2, Poly, 5.25, 8.55, true); StepBlocks(B, 18.6, 7.95, Y2, Y3, Y2, 5.25, 8.55, 16); Rail(B, FVector(18.6, 5.2, Y2), FVector(7.95, 5.2, Y3)); Rail(B, FVector(7.95, 1.7, Y3), FVector(7.95, 5.2, Y3)); } else if (Id == TEXT("galley")) { for (double Yc : { -5.5, 0.0, 5.5 }) { B.Box(EMat::Wood, FVector(28, Yc, Y2 + 0.78), FVector(1.2, 3.0, 0.08), true); B.Box(EMat::Wall2, FVector(28, Yc, Y2 + 0.38), FVector(0.2, 0.2, 0.75), true); B.Box(EMat::Wall2, FVector(26.9, Yc, Y2 + 0.22), FVector(0.4, 3.0, 0.45), true); B.Box(EMat::Wall2, FVector(29.1, Yc, Y2 + 0.22), FVector(0.4, 3.0, 0.45), true); } } else if (Id == TEXT("cargo")) { // stacked crates and drums (v2 storage), kept clear of the packer, the locker wall and the stop point const struct { double X, Y, S; int32 Up; } Crates[] = { { 7.5, -7.2, 1.1, 2 }, { 8.9, -7.6, 1.0, 1 }, { 7.9, -5.8, 0.9, 1 }, { 12.5, -7.8, 1.0, 2 } }; // M5: the first three stood in front of the outfeed slots of the packer (x 17, y -6.2..-3.8); moved to the aft wall for (const auto& C : Crates) { for (int32 K = 0; K < C.Up; ++K) { B.Box((K % 2) ? EMat::Olive : EMat::Wood, FVector(C.X, C.Y, Y2 + C.S * 0.425 + K * C.S * 0.85), FVector(C.S, C.S, C.S * 0.85), true, FQuat(FVector(0, 0, 1), 0.08 * (C.X + C.Y))); } } for (const FVector2D& Dp : { FVector2D(9.0, -8.3), FVector2D(9.3, -7.3), FVector2D(8.6, -7.6) }) { B.Cylinder(EMat::Rust, FVector(Dp.X, Dp.Y, Y2 + 0.45), 0.32, 0.9, 10, 2, true); } } else if (Id == TEXT("refinery")) { // furnace at (-2, -6.5) (start placeable), glowing slit toward +y, conveyor and hazard-striped hopper rail B.Cylinder(EMat::Wall2, FVector(-2, -6.5, Y2 + 1.6), 1.6, 3.2, 20, 2, true); B.Box(EMat::Furnace, FVector(-2, -4.88, Y2 + 1.2), FVector(1.4, 0.1, 0.4), false); B.Box(EMat::Wall2, FVector(-9.5, -7.5, Y2 + 0.6), FVector(4.0, 1.6, 1.2), true); B.Box(EMat::Hazard, FVector(-12.5, -6.0, Y2 + 1.0), FVector(0.8, 6.0, 0.2), false); } else if (Id == TEXT("engine")) { B.Cylinder(EMat::Wall2, FVector(-4, 6, Y2 + 1.9), 1.8, 3.8, 24, 2, true); B.Cylinder(EMat::Core, FVector(-4, 6, Y2 + 1.95), 0.7, 3.9, 16, 2, false); // glowing slits in the reactor housing (the pulsing core shows through them) B.Box(EMat::Core, FVector(-5.82, 6, Y2 + 1.9), FVector(0.06, 0.45, 3.0), false); B.Box(EMat::Core, FVector(-4, 4.18, Y2 + 1.9), FVector(0.45, 0.06, 3.0), false); for (double X : { 3.0, -11.0 }) { B.Cylinder(EMat::Wall2, FVector(X, 8.5, Y2 + 1.8), 0.9, 3.6, 12, 2, true); } for (int32 I = 0; I < 5; ++I) { B.Cylinder(EMat::Wall2, FVector(-5, 2.2 + I * 0.35, Y2 + 3.6 - (I % 2) * 0.3), 0.15, 22, 8, 0, false); } } else if (Id == TEXT("bay")) { // stairs down from the D2 gallery (y -8.75..-7.25), rails, bay lamps on the walls are the room lamps const TArray Poly{ FVector2D(-19.7, Y2), FVector2D(-30.6, Y1), FVector2D(-19.7, Y1) }; B.ExtrudeY(EMat::Wall2, Poly, -8.75, -7.25, true); StepBlocks(B, -19.7, -30.6, Y2, Y1, Y1, -8.75, -7.25, 16); Rail(B, FVector(-19.8, -7.2, Y2), FVector(-19.8, 9.0, Y2)); Rail(B, FVector(-19.8, -7.2, Y2), FVector(-30.6, -7.2, Y1)); } } } void BuildRoomMesh(const FRoomDef& R, FMeshBuilder& B) { RoomShell(R, B); Props(R, B); } void BuildDoorsMesh(FMeshBuilder& B) { const FLayout& L = FLayout::Get(); for (const FDoorDef& D : L.Doors) { const FVector2D Mid = D.Mid(); const double Z = D.FloorZ(); if (D.WallAtConstX()) { B.Box(EMat::LampCyan, FVector(Mid.X, Mid.Y, Z + 2.45), FVector(0.2, D.Width() + 0.3, 0.08), false); B.BoxMinMax(EMat::Floor, FVector(Mid.X - 0.9, D.Y0, Z - 0.3), FVector(Mid.X + 0.9, D.Y1, Z), true); } else { B.Box(EMat::LampCyan, FVector(Mid.X, Mid.Y, Z + 2.45), FVector(D.Width() + 0.3, 0.2, 0.08), false); B.BoxMinMax(EMat::Floor, FVector(D.X0, Mid.Y - 0.9, Z - 0.3), FVector(D.X1, Mid.Y + 0.9, Z), true); } } } void BuildPoolMesh(FMeshBuilder& S, FMeshBuilder& W) { const double Top = Tune::DeckFloorZ1, Bot = Tune::PoolShaftBottomZ, Sh = Top - Bot; const double Xc = 0.5 * (Tune::PoolX0 + Tune::PoolX1), Yc = 0.5 * (Tune::PoolY0 + Tune::PoolY1); const double Lx = Tune::PoolX1 - Tune::PoolX0, Wy = Tune::PoolY1 - Tune::PoolY0; S.Box(EMat::Shaft, FVector(Xc, Tune::PoolY0 - 0.075, Bot + 0.5 * Sh), FVector(Lx, 0.15, Sh), true); S.Box(EMat::Shaft, FVector(Xc, Tune::PoolY1 + 0.075, Top - 0.5), FVector(Lx, 0.15, 1.0), true); S.Box(EMat::Shaft, FVector(Tune::PoolX0 - 0.075, Yc, Bot + 0.5 * Sh), FVector(0.15, Wy, Sh), true); S.Box(EMat::Shaft, FVector(Tune::PoolX1 + 0.075, Yc, Bot + 0.5 * Sh), FVector(0.15, Wy, Sh), true); // hazard-striped lips at deck level S.Box(EMat::Hazard, FVector(Xc, Tune::PoolY0 - 0.17, Top + 0.015), FVector(Lx + 0.7, 0.35, 0.03), false); S.Box(EMat::Hazard, FVector(Xc, Tune::PoolY1 + 0.17, Top + 0.015), FVector(Lx + 0.7, 0.35, 0.03), false); S.Box(EMat::Hazard, FVector(Tune::PoolX0 - 0.17, Yc, Top + 0.015), FVector(0.35, Wy + 0.7, 0.03), false); S.Box(EMat::Hazard, FVector(Tune::PoolX1 + 0.17, Yc, Top + 0.015), FVector(0.35, Wy + 0.7, 0.03), false); // railings on all four sides; the inboard side (y 3.5) keeps a gap at the ladder, x -30.3..-27.7 const double Ry0 = Tune::PoolY0 - 0.25, Ry1 = Tune::PoolY1 + 0.25, Rx0 = Tune::PoolX0 - 0.25, Rx1 = Tune::PoolX1 + 0.25; const double GapA = -30.3, GapB = -27.7; for (double H : { 0.55, 1.0 }) { S.Box(EMat::Hazard, FVector(0.5 * (Rx0 + GapA), Ry0, Top + H), FVector(GapA - Rx0, 0.04, 0.04), false); S.Box(EMat::Hazard, FVector(0.5 * (GapB + Rx1), Ry0, Top + H), FVector(Rx1 - GapB, 0.04, 0.04), false); S.Box(EMat::Hazard, FVector(0.5 * (Rx0 + Rx1), Ry1, Top + H), FVector(Rx1 - Rx0, 0.04, 0.04), false); S.Box(EMat::Hazard, FVector(Rx0, 0.5 * (Ry0 + Ry1), Top + H), FVector(0.04, Ry1 - Ry0, 0.04), false); S.Box(EMat::Hazard, FVector(Rx1, 0.5 * (Ry0 + Ry1), Top + H), FVector(0.04, Ry1 - Ry0, 0.04), false); } for (double X = Rx0; X <= Rx1 + 0.01; X += (Rx1 - Rx0) / 4.0) { if (X < GapA || X > GapB) { S.Box(EMat::Wall2, FVector(X, Ry0, Top + 0.52), FVector(0.07, 0.07, 1.05), false); } S.Box(EMat::Wall2, FVector(X, Ry1, Top + 0.52), FVector(0.07, 0.07, 1.05), false); } for (double Y = Ry0; Y <= Ry1 + 0.01; Y += (Ry1 - Ry0) / 4.0) { S.Box(EMat::Wall2, FVector(Rx0, Y, Top + 0.52), FVector(0.07, 0.07, 1.05), false); S.Box(EMat::Wall2, FVector(Rx1, Y, Top + 0.52), FVector(0.07, 0.07, 1.05), false); } // collision of the railings: tall slabs along each side; the ladder gap (x -30.3..-27.7) is open: through it the player drops into the pool (M3) S.ColBox(FVector(Rx0, Ry0 - 0.03, Top), FVector(GapA, Ry0 + 0.03, Top + 1.3)); S.ColBox(FVector(GapB, Ry0 - 0.03, Top), FVector(Rx1, Ry0 + 0.03, Top + 1.3)); S.ColBox(FVector(Rx0, Ry1 - 0.03, Top), FVector(Rx1, Ry1 + 0.03, Top + 1.3)); S.ColBox(FVector(Rx0 - 0.03, Ry0, Top), FVector(Rx0 + 0.03, Ry1, Top + 1.3)); S.ColBox(FVector(Rx1 - 0.03, Ry0, Top), FVector(Rx1 + 0.03, Ry1, Top + 1.3)); // ladder rungs and rails at the gap (x -29) for (int32 I = 0; I < 9; ++I) { S.Box(EMat::Hazard, FVector(-29, Tune::PoolY0 + 0.1, Top - 2.6 + I * 0.35), FVector(0.9, 0.05, 0.05), false); } for (double Dx : { -0.45, 0.45 }) { S.Box(EMat::Hazard, FVector(-29 + Dx, Tune::PoolY0 + 0.1, Top - 1.1), FVector(0.06, 0.06, 3.2), false); } // water surface (opaque side up and down so it reads from below) const FVector A(Tune::PoolX0, Tune::PoolY0, Tune::PoolWaterZ), Bq(Tune::PoolX1, Tune::PoolY0, Tune::PoolWaterZ), C(Tune::PoolX1, Tune::PoolY1, Tune::PoolWaterZ), D(Tune::PoolX0, Tune::PoolY1, Tune::PoolWaterZ); W.Quad(EMat::PoolWater, A, Bq, C, D, FVector(0, 0, 1)); W.Quad(EMat::PoolWater, A, Bq, C, D, FVector(0, 0, -1)); } void BuildFixtureMesh(const FAnchorDef& A, FMeshBuilder& B) { const double Y3 = Tune::DeckFloorZ3, Y2 = Tune::DeckFloorZ2, Y1 = Tune::DeckFloorZ1; const FName K = A.Kind; const FVector P = A.PosM; if (K == TEXT("Helm")) { V2Box(B, EMat::Wall2, 3.2, 1.0, 1.0, 0, Y3 + 0.5, -45.6); // console B.Box(EMat::ScreenTeal, FVector(45.05, 0, Y3 + 1.3), FVector(0.04, 2.6, 0.8), false); // screen V2Box(B, EMat::Fabric, 0.9, 0.5, 0.9, 0, Y3 + 0.45, -43.6); // seat V2Box(B, EMat::Fabric, 0.9, 1.0, 0.15, 0, Y3 + 1.1, -43.1); // back } else if (K == TEXT("Chart")) { B.Cylinder(EMat::Wall2, FVector(39.5, 0, Y3 + 0.45), 1.1, 0.9, 24, 2, true); B.Cylinder(EMat::ScreenTeal, FVector(39.5, 0, Y3 + 0.92), 1.0, 0.03, 24, 2, false); } else if (K == TEXT("Bed")) { const double S = P.Y < 0 ? -1.0 : 1.0; B.Box(EMat::Wall2, FVector(P.X, P.Y, Y3 + 0.22), FVector(1.0, 2.1, 0.45), true); B.Box(EMat::Fabric, FVector(P.X, P.Y, Y3 + 0.54), FVector(0.95, 2.0, 0.18), false); B.Box(EMat::White, FVector(P.X, P.Y + S * 0.75, Y3 + 0.7), FVector(0.7, 0.4, 0.14), false); } else if (K == TEXT("Port")) { const double In = -FMath::Sign(P.Y); // toward the room centre const double Wall = P.Y - In * 0.14; B.Box(EMat::Wall2, FVector(P.X, Wall + In * 0.06, Y2 + 1.3), FVector(0.9, 0.12, 1.4), true); B.Box(EMat::LampCyan, FVector(P.X, P.Y, P.Z), FVector(0.24, 0.03, 0.24), false); B.Box(EMat::DarkMetal, FVector(P.X, P.Y + In * 0.012, P.Z), FVector(0.14, 0.02, 0.14), false); } else if (K == TEXT("Vat")) { B.Cylinder(EMat::Wall2, FVector(P.X, P.Y, Y2 + 1.2), 0.9, 2.4, 20, 2, true); B.Cylinder(EMat::Olive, FVector(P.X, P.Y, Y2 + 1.2), 0.82, 2.0, 20, 2, false); } else if (K == TEXT("Packer")) { B.Box(EMat::Wall2, FVector(P.X, P.Y, Y2 + 0.9), FVector(2.0, 2.4, 1.8), true); for (double Dy : { -1.2, -0.4, 0.4, 1.2 }) { B.Box(EMat::LampAmber, FVector(17.0, P.Y + Dy - 0.6, Y2 + 0.2), FVector(0.4, 0.4, 0.04), false); } } else if (K == TEXT("Storage")) { for (int32 I = 0; I < 5; ++I) { const double X = 18.5 - 2.6 * I; B.Box(EMat::Wall2, FVector(X, -9.4, Y2 + 1.15), FVector(1.2, 0.8, 2.3), true); B.Box(EMat::Wall, FVector(X, -8.98, Y2 + 1.2), FVector(0.9, 0.04, 1.6), false); } } else if (K == TEXT("Terminal")) { B.Box(EMat::Wall2, FVector(9, -1.66, Y2 + 1.4), FVector(1.3, 0.1, 0.9), true); B.Box(EMat::ScreenTeal, FVector(9, -1.72, Y2 + 1.4), FVector(1.2, 0.02, 0.8), false); } else if (K == TEXT("Carousel")) { B.Box(EMat::Wall2, FVector(-22, 1.6, Y1 + 0.6), FVector(0.6, 0.8, 1.2), true); B.Box(EMat::ScreenAmber, FVector(-22.31, 1.6, Y1 + 1.25), FVector(0.02, 0.7, 0.45), false); } else if (K == TEXT("Ladder")) { // the ladder itself is part of the pool mesh; this is only the prompt target on the deck } } void BuildHullMesh(FMeshBuilder& B) { // ---- lathe ------------------------------------------------------------------------------------------- TArray Xs; for (int32 I = 0; I <= 18; ++I) { Xs.Add(40.0 + 20.0 * FMath::Cos(UE_DOUBLE_HALF_PI * I / 18.0)); } for (double X = 38.0; X > -60.0; X -= 2.0) { Xs.Add(X); } Xs.Add(-60.0); const int32 Segs = 64; auto Ring = [&](double X, int32 J, double Off) -> FVector { const double Ph = 2.0 * UE_DOUBLE_PI * J / Segs, R = HullRadius(X) + Off; return FVector(X, FMath::Cos(Ph) * R, FMath::Sin(Ph) * R); }; for (int32 I = 0; I + 1 < Xs.Num(); ++I) { for (int32 J = 0; J < Segs; ++J) { const FVector P00 = Ring(Xs[I], J, 0), P01 = Ring(Xs[I], J + 1, 0), P10 = Ring(Xs[I + 1], J, 0), P11 = Ring(Xs[I + 1], J + 1, 0); const FVector Cen = 0.25 * (P00 + P01 + P10 + P11); if (FVector::DistSquared(P00, P10) < 1e-8 && FVector::DistSquared(P01, P11) < 1e-8) { continue; } // the moon-pool opening in the belly if (Cen.Z < 0.0 && Cen.Y > Tune::PoolY0 - 0.3 && Cen.Y < Tune::PoolY1 + 0.3 && Cen.X > Tune::PoolX0 - 0.5 && Cen.X < Tune::PoolX1 + 0.5) { continue; } const double DR = HullRadius(Xs[I + 1]) - HullRadius(Xs[I]); const double DX = Xs[I + 1] - Xs[I]; const FVector Radial = FVector(0, Cen.Y, Cen.Z).GetSafeNormal(); const FVector N = FVector(DX != 0.0 ? -DR / DX : 0.0, Radial.Y, Radial.Z).GetSafeNormal(); B.Quad(EMat::Hull, P00, P01, P11, P10, N); // dim warm glow where the bridge windows are (v2): x 47.5..52, |y| <= 6.5, z 3.2..5.2 if (Cen.X > 47.5 && Cen.X < 52.0 && FMath::Abs(Cen.Y) < 6.5 && Cen.Z > 3.2 && Cen.Z < 5.2) { B.Quad(EMat::WindowGlow, Ring(Xs[I], J, 0.05), Ring(Xs[I], J + 1, 0.05), Ring(Xs[I + 1], J + 1, 0.05), Ring(Xs[I + 1], J, 0.05), N); } } } // stern cap for (int32 J = 0; J < 32; ++J) { const double A0 = 2.0 * UE_DOUBLE_PI * J / 32, A1 = 2.0 * UE_DOUBLE_PI * (J + 1) / 32; B.Tri(EMat::DarkMetal, FVector(-60, 0, 0), FVector(-60, 7 * FMath::Cos(A0), 7 * FMath::Sin(A0)), FVector(-60, 7 * FMath::Cos(A1), 7 * FMath::Sin(A1)), FVector(-1, 0, 0)); } // ---- appendages -------------------------------------------------------------------------------------- const TArray Sail{ FVector2D(30, 11.2), FVector2D(22, 16.7), FVector2D(6, 18.2), FVector2D(-6, 16.4), FVector2D(-10, 11.2) }; B.ExtrudeY(EMat::Hull, Sail, -3.0, 3.0, false); B.Cylinder(EMat::DarkMetal, FVector(8, 0, 20.3), 0.16, 5.0, 8, 2, false); for (double S : { -1.0, 1.0 }) { B.Cylinder(EMat::DarkMetal, FVector(-30, S * 13.2, -3.5), 1.9, 20.0, 18, 0, false); B.Box(EMat::DarkMetal, FVector(-30, S * 11.6, -3.2), FVector(10, 3, 0.6), false); } B.Box(EMat::DarkMetal, FVector(-52, 0, 9.5), FVector(9, 0.5, 7), false); B.Box(EMat::DarkMetal, FVector(-52, 0, -9.5), FVector(9, 0.5, 7), false); B.Box(EMat::DarkMetal, FVector(-52, 9.5, 0), FVector(9, 7, 0.5), false); B.Box(EMat::DarkMetal, FVector(-52, -9.5, 0), FVector(9, 7, 0.5), false); B.TorusX(EMat::DarkMetal, FVector(-61, 0, 0), 6.8, 0.9, 40, 10); B.Box(EMat::DarkMetal, FVector(6, 0, -12.2), FVector(60, 1.2, 1.4), false); { // bow sonar dome: half ellipsoid hanging under the nose const FVector Ctr(42, 0, -8.8), Rad(7.2, 4.5, 2.25); const int32 NLat = 6, NLon = 16; auto Pt = [&](int32 I, int32 J) -> FVector { const double Th = UE_DOUBLE_HALF_PI * I / NLat, Ph = 2.0 * UE_DOUBLE_PI * J / NLon; return Ctr + FVector(Rad.X * FMath::Sin(Th) * FMath::Cos(Ph), Rad.Y * FMath::Sin(Th) * FMath::Sin(Ph), -Rad.Z * FMath::Cos(Th)); }; for (int32 I = 0; I < NLat; ++I) { for (int32 J = 0; J < NLon; ++J) { const FVector Cen = 0.25 * (Pt(I, J) + Pt(I + 1, J) + Pt(I, J + 1) + Pt(I + 1, J + 1)) - Ctr; B.Quad(EMat::RustMetal, Pt(I, J), Pt(I, J + 1), Pt(I + 1, J + 1), Pt(I + 1, J), FVector(Cen.X / (Rad.X * Rad.X), Cen.Y / (Rad.Y * Rad.Y), Cen.Z / (Rad.Z * Rad.Z)).GetSafeNormal()); } } } // pool rim (v2): four hazard boxes around the belly opening V2Box(B, EMat::Hazard, 6.6, 1.4, 0.8, 6.0, -10.4, 22.6, false); V2Box(B, EMat::Hazard, 6.6, 1.4, 0.8, 6.0, -10.4, 35.4, false); V2Box(B, EMat::Hazard, 0.8, 1.4, 13.6, 3.1, -11.3, 29.0, false); V2Box(B, EMat::Hazard, 0.8, 1.4, 13.6, 8.9, -8.6, 29.0, false); // exterior light housings: the six lights themselves are components (UCarrierBlockoutComponent) B.Cylinder(EMat::NavWhite, FVector(Tune::FloodX, Tune::FloodY, Tune::FloodZ - 0.1), 0.6, 0.5, 12, 2, false); } void CollectSigns(TArray& Out) { const FLayout& L = FLayout::Get(); for (const FDoorDef& D : L.Doors) { const FRoomDef* A = nullptr; const FRoomDef* Bq = nullptr; if (!L.DoorRooms(D, A, Bq)) { continue; } const FVector2D Mid = D.Mid(); const FVector2D N = D.WallAtConstX() ? FVector2D(1, 0) : FVector2D(0, 1); const double Z = D.FloorZ() + 2.92; // on the A side, facing A: names the room beyond (B); on the B side, facing B: names A FSignDef SA; SA.Text = FString::Printf(TEXT("DECK %d %s"), D.Deck, *Bq->Name); SA.PosM = FVector(Mid.X - 0.24 * N.X, Mid.Y - 0.24 * N.Y, Z); SA.YawDeg = FMath::RadiansToDegrees(FMath::Atan2(-N.Y, -N.X)); Out.Add(SA); FSignDef SB; SB.Text = FString::Printf(TEXT("DECK %d %s"), D.Deck, *A->Name); SB.PosM = FVector(Mid.X + 0.24 * N.X, Mid.Y + 0.24 * N.Y, Z); SB.YawDeg = FMath::RadiansToDegrees(FMath::Atan2(N.Y, N.X)); Out.Add(SB); } { FSignDef S; S.Text = TEXT("SUB BAY - CAROUSEL"); S.PosM = FVector(-39.78, 0, -1.0); S.YawDeg = 0; S.HeightM = 0.5; Out.Add(S); FSignDef S2; S2.Text = TEXT("MOON POOL - KEEP CLEAR OF CRADLE"); S2.PosM = FVector(-39.78, 0, -1.7); S2.YawDeg = 0; S2.HeightM = 0.22; S2.Colour = FLinearColor(0.1f, 0.5f, 0.5f); Out.Add(S2); FSignDef T; T.Text = TEXT("TETHYS SALES - LEDGER"); T.PosM = FVector(9, -1.76, Tune::DeckFloorZ2 + 1.4); T.YawDeg = -90.0; T.HeightM = 0.12; Out.Add(T); for (const FAnchorDef& A : L.Anchors) { if (A.Kind == TEXT("Port")) { FSignDef P; P.Text = TEXT("NUTRIENT FEED"); const double In = -FMath::Sign(A.PosM.Y); P.PosM = FVector(A.PosM.X, A.PosM.Y + In * 0.16, Tune::DeckFloorZ2 + 1.95); P.YawDeg = In > 0 ? 90.0 : -90.0; P.HeightM = 0.1; P.Colour = FLinearColor(0.1f, 0.6f, 0.65f); Out.Add(P); } } } } }