// The page layouts of the windows (DESIGN_UE.md 8.5), drawn from the view-models of UI/ViewModels/*.h. // A page is rebuilt when the window, the tab or a view-model changes; values that move (progress bars, gauges, the // Bloom slider) are bound to the model and update without a rebuild. #include "UI/DescentWindows.h" #include "UI/NeuIcons.h" #include "UI/NeuWidgets.h" #include "Core/Tuning/DescentTune_UI.h" #include "Widgets/Layout/SBox.h" #include "Widgets/SBoxPanel.h" namespace UiTune = Descent::Tune::UI; using FPgWeak = TWeakObjectPtr; namespace { const FDescentUiModels& PgModels(const FPgWeak& W) { static const FDescentUiModels Empty; const UDescentUiSubsystem* U = W.Get(); return U ? U->Models() : Empty; } TSharedRef PgText(const FString& S, float Px = 15.f, ENeuFont Weight = ENeuFont::Regular, FLinearColor Color = Neu::Txt(), ENeuAlign Align = ENeuAlign::Left, int32 Lines = 1, float MinPx = 0.f) { return SNew(SNeuText).Text(S).Px(Px).Weight(Weight).Color(Color).HAlign(Align).MaxLines(Lines).MinPx(MinPx > 0.f ? MinPx : Px * 0.75f); } TSharedRef PgCaption(const FString& S) { return PgText(S, 12.f, ENeuFont::Bold, Neu::Cyan()); } TSharedRef PgWell(const TSharedRef& Content, FMargin Pad = FMargin(16.f)) { return SNew(SNeuPanel).Inset(true).Padding(Pad)[ Content ]; } FLinearColor PgOreColor(FName Ore) { return Neu::OreColor(Ore); } FString PgOreName(FName Ore) { if (Ore == TEXT("cobalt")) { return TEXT("COBALT"); } if (Ore == TEXT("pearl")) { return TEXT("PEARL"); } return TEXT("FERRITE"); } FString PgRefinedOf(FName Ore) { if (Ore == TEXT("cobalt")) { return TEXT("wafer"); } if (Ore == TEXT("pearl")) { return TEXT("lattice"); } return TEXT("ingot"); } FString PgCredits(int64 V) { return FDescentHudModel::FormatThousands(static_cast(V)) + TEXT(" cr"); } // --------------------------------------------------------------------------------------------------------- // Small custom views // --------------------------------------------------------------------------------------------------------- FLinearColor PgHsl(float H, float S, float L) { auto F = [&](float N) { const float K = FMath::Fmod(N + H * 12.f, 12.f); const float A = S * FMath::Min(L, 1.f - L); return L - A * FMath::Max(-1.f, FMath::Min(FMath::Min(K - 3.f, 9.f - K), 1.f)); }; FLinearColor C(F(0.f), F(8.f), F(4.f), 1.f); return C; // sRGB-looking values used as is: they are tints on a dark ground } /** The survey map drawing (a stand-in for the M9 SSurveyMap): known cells coloured by depth, markers, carrier and YOU arrows. */ class SPgMapView : public SLeafWidget { public: SLATE_BEGIN_ARGS(SPgMapView) {} SLATE_ARGUMENT(FPgWeak, Ui) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { Ui = InArgs._Ui; SetCanTick(false); } virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D(300.f, 200.f); } virtual int32 OnPaint(const FPaintArgs&, const FGeometry& Geo, const FSlateRect&, FSlateWindowElementList& Out, int32 Layer, const FWidgetStyle&, bool) const override { FNeuPainter P(Out, Geo, Layer); const FVector2f S = Geo.GetLocalSize(); P.Inset(FVector2f::ZeroVector, S, 14.f); const FDescentMapViewModel& M = PgModels(Ui).Map; const FVector2f Pad(14.f, 14.f); const FVector2f Area = S - Pad * 2.f; if (M.GridW > 0 && M.GridH > 0) { const float Cw = Area.X / M.GridW; const float Ch = Area.Y / M.GridH; for (int32 Y = 0; Y < M.GridH; ++Y) { for (int32 X = 0; X < M.GridW; ++X) { const float V = M.Cells.IsValidIndex(Y * M.GridW + X) ? M.Cells[Y * M.GridW + X] : -1.f; if (V < 0.f) { continue; } FLinearColor C = V > 1.5f ? Neu::WithAlpha(Neu::Dim(), 0.35f) : PgHsl(0.52f - 0.32f * V, 0.65f, 0.50f - 0.18f * V); P.RoundRect(Pad + FVector2f(X * Cw, Y * Ch), FVector2f(Cw + 0.5f, Ch + 0.5f), 0.f, C); } } for (int32 G = 5; G < M.GridW; G += 5) { P.Line(Pad + FVector2f(G * Cw, 0.f), Pad + FVector2f(G * Cw, Area.Y), 1.f, Neu::WithAlpha(Neu::Txt(), 0.07f)); } for (int32 G = 5; G < M.GridH; G += 5) { P.Line(Pad + FVector2f(0.f, G * Ch), Pad + FVector2f(Area.X, G * Ch), 1.f, Neu::WithAlpha(Neu::Txt(), 0.07f)); } } for (const FDescentMapMarker& Mk : M.Markers) { const FVector2f At = Pad + FVector2f(Mk.X * Area.X, Mk.Y * Area.Y); if (Mk.bRing) { P.Arc(At, 8.f, 0.f, 360.f, 2.f, Mk.Color, false); } else { P.Polygon({ At + FVector2f(0.f, -5.f), At + FVector2f(5.f, 0.f), At + FVector2f(0.f, 5.f), At + FVector2f(-5.f, 0.f) }, Mk.Color); } P.TextBox(Mk.Label, At + FVector2f(-60.f, 8.f), FVector2f(120.f, 14.f), ENeuFont::Bold, 11.f, Mk.Color, ENeuAlign::Center, ENeuVAlign::Center, 8.f); } auto Arrow = [&](FVector2f N, const FLinearColor& C, const TCHAR* Label) { const FVector2f At = Pad + FVector2f(N.X * Area.X, N.Y * Area.Y); P.Polygon({ At + FVector2f(0.f, -9.f), At + FVector2f(7.f, 7.f), At + FVector2f(0.f, 3.f), At + FVector2f(-7.f, 7.f) }, C); P.TextBox(Label, At + FVector2f(-40.f, -26.f), FVector2f(80.f, 14.f), ENeuFont::Bold, 11.f, C, ENeuAlign::Center, ENeuVAlign::Center, 8.f); }; Arrow(M.Carrier, Neu::Cyan(), TEXT("CANTICLE")); Arrow(M.You, Neu::Amber(), TEXT("YOU")); return P.Layer + 1; } private: FPgWeak Ui; }; /** The nutrient vat: a glass tank whose green level is the biomass reserve. */ class SPgTankView : public SLeafWidget { public: SLATE_BEGIN_ARGS(SPgTankView) {} SLATE_ARGUMENT(FPgWeak, Ui) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { Ui = InArgs._Ui; SetCanTick(false); } virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D(220.f, 330.f); } virtual int32 OnPaint(const FPaintArgs&, const FGeometry& Geo, const FSlateRect&, FSlateWindowElementList& Out, int32 Layer, const FWidgetStyle&, bool) const override { FNeuPainter P(Out, Geo, Layer); const FVector2f S = Geo.GetLocalSize(); const FDescentVatViewModel& V = PgModels(Ui).Vat; const float Level = V.CapFu > 0.f ? FMath::Clamp(V.ReserveFu / V.CapFu, 0.f, 1.f) : 0.f; const FVector2f TankPos(S.X * 0.5f - 62.f, 10.f); const FVector2f TankSize(124.f, S.Y - 20.f); P.Inset(TankPos, TankSize, 40.f); const float Inner = 10.f; const float FullH = TankSize.Y - 2.f * Inner; const float FillH = FullH * Level; if (FillH > 1.f) { const FLinearColor Top = Neu::WithAlpha(Neu::Green(), 0.95f); const FLinearColor Bottom = Neu::WithAlpha(Neu::Green() * 0.45f, 0.95f); P.RoundRectGradient(TankPos + FVector2f(Inner, Inner + FullH - FillH), FVector2f(TankSize.X - 2.f * Inner, FillH), 30.f, Top, Bottom); } for (int32 I = 1; I < 5; ++I) { const float Y = TankPos.Y + Inner + FullH * I / 5.f; P.Line(FVector2f(TankPos.X + TankSize.X - Inner - 18.f, Y), FVector2f(TankPos.X + TankSize.X - Inner, Y), 1.5f, Neu::WithAlpha(Neu::Txt(), 0.35f)); } P.TextBox(FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Level * 100.f)), TankPos, TankSize, ENeuFont::Bold, 30.f, Neu::Txt(), ENeuAlign::Center, ENeuVAlign::Center); return P.Layer + 1; } private: FPgWeak Ui; }; // --------------------------------------------------------------------------------------------------------- // PDA // --------------------------------------------------------------------------------------------------------- TSharedRef PgPdaInventory(const FPgWeak& Weak) { const FDescentPdaViewModel& Pda = PgModels(Weak).Pda; TSharedRef Grid = SNew(SVerticalBox); const int32 Cols = 6; for (int32 Row = 0; Row < 4; ++Row) { TSharedRef RowBox = SNew(SHorizontalBox); for (int32 C = 0; C < Cols; ++C) { const int32 I = Row * Cols + C; const FDescentPdaItem Item = Pda.Slots.IsValidIndex(I) ? Pda.Slots[I] : FDescentPdaItem(); RowBox->AddSlot().AutoWidth().Padding(0.f, 0.f, C + 1 < Cols ? 14.f : 0.f, 0.f) [ SNew(SNeuCell) .ItemId(Item.ItemId) .Count(Item.Count) .Caption(Item.Name) .Selected_Lambda([Weak, I]() { return PgModels(Weak).Pda.Selected == I; }) .OnClick([Weak, I]() { UDescentUiSubsystem* U = Weak.Get(); if (!U) { return; } FDescentPdaViewModel& M = U->Models().Pda; M.Selected = I; if (M.Slots.IsValidIndex(I)) { M.SelectedName = M.Slots[I].Name; } if (M.OnSelect) { M.OnSelect(I); } U->NotifyChanged(); }) ]; } Grid->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, Row < 3 ? 14.f : 0.f)[ RowBox ]; } int32 Used = 0; for (const FDescentPdaItem& It : Pda.Slots) { Used += It.ItemId.IsNone() ? 0 : 1; } return SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 20.f, 0.f) [ PgWell(SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 12.f)[ PgCaption(FString::Printf(TEXT("SUIT INVENTORY %d / 24"), Used)) ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)[ Grid ]) ] + SHorizontalBox::Slot().AutoWidth() [ SNew(SBox).WidthOverride(300.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgCaption(TEXT("EQUIPMENT")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 20.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding(0.f, 0.f, 14.f, 0.f) [ SNew(SNeuCell).ItemId(Pda.O2Tank.ItemId).Count(0).Caption(Pda.O2Tank.ItemId.IsNone() ? FString(TEXT("O2 TANK")) : Pda.O2Tank.Name) ] + SHorizontalBox::Slot().AutoWidth() [ SNew(SNeuCell).ItemId(TEXT("weapon")).Caption(TEXT("WEAPON")).Greyed(true) ] + SHorizontalBox::Slot().FillWidth(1.f).Padding(14.f, 0.f, 0.f, 0.f).VAlign(VAlign_Center) [ PgText(Pda.WeaponCaption, 12.f, ENeuFont::Bold, Neu::Dim(), ENeuAlign::Left, 2) ] ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgCaption(TEXT("SELECTED")) ] + SVerticalBox::Slot().FillHeight(1.f) [ PgWell(SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 6.f)[ PgText(Pda.SelectedName.IsEmpty() ? FString(TEXT("Nothing selected")) : Pda.SelectedName, 18.f, ENeuFont::Bold) ] + SVerticalBox::Slot().AutoHeight()[ PgText(Pda.SelectedDesc, 14.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Left, 6) ]) ] ] ]; } TSharedRef PgPdaMap(const FPgWeak& Weak) { const FDescentMapViewModel& Map = PgModels(Weak).Map; TSharedRef Legend = SNew(SHorizontalBox); for (int32 I = 0; I < 6; ++I) { const float T = I / 5.f; Legend->AddSlot().FillWidth(1.f)[ SNew(SBox).HeightOverride(14.f)[ SNew(SNeuPanel).Padding(FMargin(0.f)).Radius(0.f).Small(true).Body(PgHsl(0.52f - 0.32f * T, 0.65f, 0.50f - 0.18f * T))[ SNullWidget::NullWidget ] ] ]; } return SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 20.f, 0.f)[ SNew(SPgMapView).Ui(Weak) ] + SHorizontalBox::Slot().AutoWidth() [ SNew(SBox).WidthOverride(250.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("DEPTH SLICE")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 18.f)[ PgText(Map.SliceText, 14.f, ENeuFont::Bold, Neu::Txt(), ENeuAlign::Left, 2) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("TARGET")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 18.f)[ PgText(Map.TargetText, 14.f, ENeuFont::Regular, Neu::Amber(), ENeuAlign::Left, 3) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("DEPTH COLOUR")) ] + SVerticalBox::Slot().AutoHeight()[ Legend ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 4.f, 0.f, 18.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f)[ PgText(TEXT("1,740 m"), 12.f, ENeuFont::Regular, Neu::Dim()) ] + SHorizontalBox::Slot().AutoWidth()[ PgText(TEXT("3,200 m"), 12.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Right) ] ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 6.f)[ PgText(TEXT("Amber diamond: choke. Red ring: unknown contact. Grey: visited only."), 12.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Left, 4) ] ] ]; } TSharedRef PgPdaLogs(const FPgWeak& Weak) { const FDescentLogsViewModel& L = PgModels(Weak).Logs; TSharedRef List = SNew(SVerticalBox); for (int32 I = 0; I < L.Total; ++I) { const bool bFound = L.Entries.IsValidIndex(I); List->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f) [ SNew(SBox).WidthOverride(300.f) [ SNew(SNeuButton) .Text(bFound ? L.Entries[I].Title : FString::Printf(TEXT("LOG %d - NOT FOUND"), I + 1)) .Px(13.f).Height(34.f).MinWidth(300.f) .On_Lambda([Weak, I]() { return PgModels(Weak).Logs.Selected == I; }) .OnClick([Weak, I]() { UDescentUiSubsystem* U = Weak.Get(); if (U && U->Models().Logs.Entries.IsValidIndex(I)) { U->Models().Logs.Selected = I; U->NotifyChanged(); } }) ] ]; if (!bFound && List->NumSlots() > 0) { // Unfound logs are plain dim rows, not buttons. } } const FDescentLogEntry* Sel = L.Entries.IsValidIndex(L.Selected) ? &L.Entries[L.Selected] : nullptr; TSharedRef Body = SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgText(Sel ? Sel->Title : FString(TEXT("No log selected")), 20.f, ENeuFont::Bold, Neu::Cyan()) ] + SVerticalBox::Slot().AutoHeight()[ PgText(Sel ? Sel->Body : FString(TEXT("Find the dead beacons to recover their logs.")), 16.f, ENeuFont::Regular, Neu::Txt(), ENeuAlign::Left, 9) ]; if (Sel && Sel->bCutOff) { Body->AddSlot().AutoHeight().Padding(0.f, 14.f, 0.f, 0.f)[ PgText(TEXT("[SIGNAL LOST]"), 16.f, ENeuFont::Bold, Neu::Red()) ]; } return SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding(0.f, 0.f, 20.f, 0.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgCaption(FString::Printf(TEXT("RECOVERED %d / %d"), L.Entries.Num(), L.Total)) ] + SVerticalBox::Slot().AutoHeight()[ List ] ] + SHorizontalBox::Slot().FillWidth(1.f)[ PgWell(Body, FMargin(22.f)) ]; } TSharedRef PgPdaStatus(const FPgWeak& Weak) { const FDescentStatusViewModel& St = PgModels(Weak).Status; TSharedRef ColA = SNew(SVerticalBox); TSharedRef ColB = SNew(SVerticalBox); for (int32 I = 0; I < St.Lines.Num(); ++I) { const FDescentStatusLine& Ln = St.Lines[I]; TSharedRef Line = SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f)[ PgText(Ln.Label, 13.f, ENeuFont::Bold, Neu::Dim()) ] + SHorizontalBox::Slot().AutoWidth()[ PgText(Ln.Value, 16.f, ENeuFont::Bold, Neu::Txt(), ENeuAlign::Right) ] ]; if (Ln.Fraction >= 0.f) { Line->AddSlot().AutoHeight().Padding(0.f, 6.f, 0.f, 0.f)[ SNew(SNeuProgress).Value(Ln.Fraction).Color(Ln.Color).Height(12.f) ]; } (I % 2 == 0 ? ColA : ColB)->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 24.f)[ Line ]; } return SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.f) [ PgWell(SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 36.f, 0.f)[ ColA ] + SHorizontalBox::Slot().FillWidth(1.f)[ ColB ], FMargin(28.f, 24.f)) ] + SVerticalBox::Slot().AutoHeight().Padding(4.f, 12.f, 0.f, 0.f)[ PgText(St.Footer, 14.f, ENeuFont::Regular, Neu::Dim()) ]; } // --------------------------------------------------------------------------------------------------------- // Stations // --------------------------------------------------------------------------------------------------------- TSharedRef PgFabricator(const FPgWeak& Weak, FName Tab) { const FDescentFabricatorViewModel& F = PgModels(Weak).Fabricator; const FDescentFabricatorCategory* Cat = nullptr; for (const FDescentFabricatorCategory& C : F.Categories) { if (C.Id == Tab) { Cat = &C; } } if (!Cat && F.Categories.Num() > 0) { Cat = &F.Categories[0]; } TSharedRef ColA = SNew(SVerticalBox); TSharedRef ColB = SNew(SVerticalBox); if (Cat) { for (int32 I = 0; I < Cat->Rows.Num(); ++I) { const FDescentRecipeRow& R = Cat->Rows[I]; TArray Chips; for (const FDescentUiCost& C : R.Costs) { FNeuChipData D; D.Text = C.IsMet() ? FString::Printf(TEXT("%d %s"), C.Need, *C.Name) : FString::Printf(TEXT("%d %s (have %d)"), C.Need, *C.Name, C.Have); D.Color = C.IsMet() ? Neu::Green() : Neu::Red(); Chips.Add(D); } (I % 2 == 0 ? ColA : ColB)->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 12.f) [ SNew(SNeuRow).ItemId(R.IconItem).Name(R.Name).Desc(R.bLocked && !R.LockedReason.IsEmpty() ? R.LockedReason : R.Desc) .Chips(Chips).ButtonText(R.ButtonText).ButtonEnabled(R.CanAfford()).Locked(R.bLocked).OnButton(R.OnBuild) ]; } } return SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 14.f, 0.f)[ ColA ] + SHorizontalBox::Slot().FillWidth(1.f)[ ColB ] ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 6.f, 0.f, 0.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.f, 0.f, 16.f, 0.f)[ SNew(SBox).WidthOverride(260.f)[ PgText(F.StatusText.IsEmpty() ? FString(TEXT("Idle")) : F.StatusText, 14.f, ENeuFont::Bold, Neu::Dim()) ] ] + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center) [ SNew(SNeuProgress).Value_Lambda([Weak]() { return PgModels(Weak).Fabricator.BuildProgress; }).Height(18.f) ] ]; } TSharedRef PgRefinery(const FPgWeak& Weak) { const FDescentRefineryViewModel& R = PgModels(Weak).Refinery; TSharedRef Queue = SNew(SVerticalBox); int32 Total = 0; for (const FDescentRefineryEntry& E : R.Queue) { Total += E.Count; Queue->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.f, 0.f, 12.f, 0.f)[ SNew(SNeuIcon).ItemId(E.OreId).Size(34.f) ] + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center)[ PgText(FString::Printf(TEXT("%s x%d"), *PgOreName(E.OreId), E.Count), 16.f, ENeuFont::Bold) ] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[ PgText(FString::Printf(TEXT("-> %s"), *PgRefinedOf(E.OreId).ToUpper()), 13.f, ENeuFont::Bold, PgOreColor(E.OreId)) ] ]; } if (R.Queue.Num() == 0) { Queue->AddSlot().AutoHeight()[ PgText(TEXT("The queue is empty."), 15.f, ENeuFont::Regular, Neu::Dim()) ]; } auto Output = [](const TCHAR* Item, const TCHAR* Cap, int32 N) -> TSharedRef { return SNew(SBox).Padding(FMargin(0.f, 0.f, 12.f, 0.f))[ SNew(SNeuCell).ItemId(FName(Item)).Count(N).Caption(FString(Cap)) ]; }; TSharedRef Right = SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgCaption(TEXT("OUTPUT TO STORAGE")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 18.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()[ Output(TEXT("ingot"), TEXT("Ingot"), R.Ingots) ] + SHorizontalBox::Slot().AutoWidth()[ Output(TEXT("wafer"), TEXT("Wafer"), R.Wafers) ] + SHorizontalBox::Slot().AutoWidth()[ Output(TEXT("lattice"), TEXT("Lattice"), R.Lattices) ] ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("HOPPER")) ] + SVerticalBox::Slot().AutoHeight()[ PgText(R.HopperText, 14.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Left, 4) ]; if (R.bStorageFull) { Right->AddSlot().AutoHeight().Padding(0.f, 14.f, 0.f, 0.f)[ PgText(TEXT("STORAGE FULL - the job waits at 100%"), 14.f, ENeuFont::Bold, Neu::Red(), ENeuAlign::Left, 2) ]; } return SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 22.f, 0.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(FString::Printf(TEXT("FURNACE"))) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 20.f) [ SNew(SNeuProgress) .Height(26.f) .Value_Lambda([Weak]() { return PgModels(Weak).Refinery.Progress; }) .Color_Lambda([Weak]() { return PgOreColor(PgModels(Weak).Refinery.CurrentOre); }) .Text_Lambda([Weak]() { const FDescentRefineryViewModel& M = PgModels(Weak).Refinery; return M.CurrentOre.IsNone() ? FString(TEXT("IDLE")) : FString::Printf(TEXT("%s %.1f s"), *PgOreName(M.CurrentOre), M.SecondsLeft); }) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(FString::Printf(TEXT("QUEUE %d / %d"), Total, R.QueueLimit)) ] + SVerticalBox::Slot().FillHeight(1.f)[ PgWell(Queue) ] ] + SHorizontalBox::Slot().AutoWidth()[ SNew(SBox).WidthOverride(330.f)[ Right ] ]; } TSharedRef PgPacker(const FPgWeak& Weak) { const FDescentPackerViewModel& P = PgModels(Weak).Packer; static const TCHAR* Ores[3] = { TEXT("ferrite"), TEXT("cobalt"), TEXT("pearl") }; TSharedRef Gauges = SNew(SHorizontalBox); for (int32 I = 0; I < 3; ++I) { Gauges->AddSlot().AutoWidth().Padding(0.f, 0.f, 26.f, 0.f) [ SNew(SNeuGauge).Diameter(104.f).Thickness(10.f) .Value_Lambda([Weak, I]() { const FDescentPackerViewModel& M = PgModels(Weak).Packer; return M.BoxSize > 0 ? static_cast(M.Partial[I]) / M.BoxSize : 0.f; }) .ValueText_Lambda([Weak, I]() { const FDescentPackerViewModel& M = PgModels(Weak).Packer; return FString::Printf(TEXT("%d/%d"), M.Partial[I], M.BoxSize); }) .Label(PgOreName(Ores[I])).Color(PgOreColor(Ores[I])) ]; } TSharedRef Slots = SNew(SHorizontalBox); for (int32 I = 0; I < 4; ++I) { const FDescentPackerSlot& S = P.Slots[I]; Slots->AddSlot().AutoWidth().Padding(0.f, 0.f, 14.f, 0.f) [ SNew(SNeuCell).ItemId(S.OreId).Count(S.Count > 1 ? S.Count : 0) .Caption(S.OreId.IsNone() ? FString::Printf(TEXT("SLOT %d"), I + 1) : PgOreName(S.OreId)) ]; } return SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("BUFFER")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 24.f) [ SNew(SNeuProgress).Height(24.f) .Value_Lambda([Weak]() { const FDescentPackerViewModel& M = PgModels(Weak).Packer; return M.BufferCap > 0 ? static_cast(M.Buffer) / M.BufferCap : 0.f; }) .Text_Lambda([Weak]() { const FDescentPackerViewModel& M = PgModels(Weak).Packer; return FString::Printf(TEXT("%d / %d pieces"), M.Buffer, M.BufferCap); }) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("PARTIAL BOXES")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 26.f)[ Gauges ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("OUTFEED SLOTS")) ] + SVerticalBox::Slot().AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()[ Slots ] + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center).HAlign(HAlign_Right) [ SNew(SNeuButton).Text(TEXT("EJECT PARTIALS")).MinWidth(190.f).Px(15.f).Height(40.f).OnClick(P.OnEject) ] ] + SVerticalBox::Slot().FillHeight(1.f).VAlign(VAlign_Bottom)[ PgText(P.StatusText, 14.f, ENeuFont::Regular, Neu::Dim()) ]; } TSharedRef PgStorage(const FPgWeak& Weak) { const FDescentStorageViewModel& S = PgModels(Weak).Storage; auto CellRow = [](const TArray& Items) -> TSharedRef { TSharedRef Row = SNew(SHorizontalBox); for (const FDescentStorageItem& It : Items) { Row->AddSlot().AutoWidth().Padding(0.f, 0.f, 14.f, 0.f)[ SNew(SNeuCell).ItemId(It.ItemId).Count(It.Count).Caption(It.Name) ]; } return Row; }; return SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("REFINED GOODS")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 12.f) [ SNew(SNeuProgress).Height(24.f) .Value_Lambda([Weak]() { const FDescentStorageViewModel& M = PgModels(Weak).Storage; return M.Cap > 0 ? static_cast(M.Used) / M.Cap : 0.f; }) .Text_Lambda([Weak]() { const FDescentStorageViewModel& M = PgModels(Weak).Storage; return FString::Printf(TEXT("%d / %d"), M.Used, M.Cap); }) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 26.f)[ PgWell(CellRow(S.Goods), FMargin(18.f)) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("KITS (do not count against the capacity)")) ] + SVerticalBox::Slot().AutoHeight()[ PgWell(CellRow(S.Kits), FMargin(18.f)) ]; } TSharedRef PgSellRow(const FDescentSellRow& R, bool bBox) { return SNew(SBox).HeightOverride(58.f) [ SNew(SNeuPanel).Small(true).Radius(14.f).Padding(FMargin(12.f, 0.f)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.f, 0.f, 10.f, 0.f)[ SNew(SNeuIcon).ItemId(R.ItemId).Size(34.f) ] + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[ PgText(bBox ? R.Name : FString::Printf(TEXT("%s x%d"), *R.Name, R.Count), 15.f, ENeuFont::Bold) ] + SVerticalBox::Slot().AutoHeight()[ PgText(bBox ? FString::Printf(TEXT("%d cr"), R.UnitValue) : FString::Printf(TEXT("%d cr each"), R.UnitValue), 12.f, ENeuFont::Regular, Neu::Amber()) ] ] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.f, 0.f) [ bBox ? SNullWidget::NullWidget : StaticCastSharedRef(SNew(SNeuButton).Text(TEXT("SELL 1")).Px(13.f).Height(32.f).MinWidth(70.f).OnClick(R.OnSellOne)) ] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.f, 0.f) [ SNew(SNeuButton).Text(bBox ? TEXT("SELL") : TEXT("SELL ALL")).Px(13.f).Height(32.f).MinWidth(bBox ? 70.f : 86.f).OnClick(R.OnSellAll) ] ] ]; } TSharedRef PgTerminal(const FPgWeak& Weak) { const FDescentTerminalViewModel& T = PgModels(Weak).Terminal; TSharedRef Refined = SNew(SVerticalBox); for (const FDescentSellRow& R : T.Refined) { Refined->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgSellRow(R, false) ]; } TSharedRef Boxes = SNew(SVerticalBox); for (const FDescentSellRow& R : T.Boxes) { Boxes->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgSellRow(R, true) ]; } auto Ledger = [](const TCHAR* Label, const FString& Value, const FLinearColor& C) -> TSharedRef { return SNew(SBox).Padding(FMargin(0.f, 0.f, 16.f, 0.f)) [ SNew(SNeuPanel).Inset(true).Small(true).Padding(FMargin(16.f, 8.f)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding(0.f, 0.f, 10.f, 0.f)[ PgText(Label, 12.f, ENeuFont::Bold, Neu::Dim()) ] + SHorizontalBox::Slot().AutoWidth()[ PgText(Value, 16.f, ENeuFont::Bold, C) ] ] ]; }; return SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 18.f, 0.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("SELL REFINED GOODS")) ] + SVerticalBox::Slot().AutoHeight()[ Refined ] ] + SHorizontalBox::Slot().FillWidth(1.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("SELL ORE BOXES (collected by the wall chute)")) ] + SVerticalBox::Slot().AutoHeight()[ Boxes ] ] ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 8.f, 0.f, 0.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()[ Ledger(TEXT("DEBT"), PgCredits(T.Debt), Neu::Red()) ] + SHorizontalBox::Slot().AutoWidth()[ Ledger(TEXT("EARNED"), PgCredits(T.Earned), Neu::Green()) ] + SHorizontalBox::Slot().AutoWidth()[ Ledger(TEXT("SOLD"), FString::FromInt(static_cast(T.Sold)) + TEXT(" lots"), Neu::Txt()) ] ]; } TSharedRef PgVat(const FPgWeak& Weak) { const FDescentVatViewModel& V = PgModels(Weak).Vat; TSharedRef Feed = SNew(SVerticalBox); for (const FDescentVatFeedRow& R : V.Feed) { Feed->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.f, 0.f, 10.f, 0.f)[ SNew(SNeuIcon).ItemId(R.ItemId).Size(32.f) ] + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center)[ PgText(FString::Printf(TEXT("%s x%d"), *R.Name, R.Count), 15.f, ENeuFont::Bold) ] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[ PgText(FString::Printf(TEXT("%.2f FU each"), R.FuEach), 13.f, ENeuFont::Regular, Neu::Green()) ] ]; } if (V.Feed.Num() == 0) { Feed->AddSlot().AutoHeight()[ PgText(TEXT("You carry no fauna. Collect fish and polyps with the Tender's suction tool."), 14.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Left, 3) ]; } return SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding(0.f, 0.f, 28.f, 0.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("BIOMASS RESERVE")) ] + SVerticalBox::Slot().AutoHeight()[ SNew(SPgTankView).Ui(Weak) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 8.f, 0.f, 0.f) [ SNew(SNeuText).Text_Lambda([Weak]() { const FDescentVatViewModel& M = PgModels(Weak).Vat; return FString::Printf(TEXT("%.1f / %.0f FU"), M.ReserveFu, M.CapFu); }) .Px(18.f).Weight(ENeuFont::Bold).Color(Neu::Green()).HAlign(ENeuAlign::Center) ] ] + SHorizontalBox::Slot().FillWidth(1.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("FEED FROM YOUR SUIT")) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 12.f)[ PgWell(Feed) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 14.f)[ SNew(SNeuButton).Text(TEXT("DEPOSIT ALL")).MinWidth(180.f).Px(15.f).Height(40.f).OnClick(V.OnDepositAll) ] + SVerticalBox::Slot().AutoHeight()[ PgText(TEXT("1 FU = 25 points of nutrition = 10 minutes of life. The wall port draws from this reserve; when it is empty nobody eats."), 13.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Left, 5) ] ]; } TSharedRef PgBuild(const FPgWeak& Weak) { const FDescentBuildMenuViewModel& B = PgModels(Weak).Build; TSharedRef List = SNew(SVerticalBox); for (const FDescentBuildKitRow& K : B.Kits) { TArray Chips; FNeuChipData C; C.Text = FString::Printf(TEXT("%d in storage"), K.InStorage); C.Color = K.InStorage > 0 ? Neu::Green() : Neu::Red(); Chips.Add(C); List->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f) [ SNew(SNeuRow).ItemId(K.KitId).Name(K.Name).Desc(FString::Printf(TEXT("%s - %s"), *K.SizeText, *K.MountText)).Chips(Chips) .ButtonText(TEXT("PLACE")).ButtonEnabled(K.InStorage > 0).OnButton(K.OnPick) ]; } return SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 10.f)[ PgCaption(FString::Printf(TEXT("KITS IN CARRIER STORAGE PLACED %d / %d"), B.Placed, B.PlacedMax)) ] + SVerticalBox::Slot().FillHeight(1.f)[ List ]; } // --------------------------------------------------------------------------------------------------------- // Cards // --------------------------------------------------------------------------------------------------------- TSharedRef PgPause(const FPgWeak& Weak) { const FDescentPauseViewModel& P = PgModels(Weak).Pause; TSharedRef KeysA = SNew(SVerticalBox); TSharedRef KeysB = SNew(SVerticalBox); for (int32 I = 0; I < P.Keys.Num(); ++I) { (I < (P.Keys.Num() + 1) / 2 ? KeysA : KeysB)->AddSlot().AutoHeight().Padding(0.f, 0.f, 0.f, 6.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[ SNew(SBox).WidthOverride(82.f)[ PgText(P.Keys[I].Keys, 12.f, ENeuFont::Bold, Neu::Cyan(), ENeuAlign::Left, 1, 9.f) ] ] + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center)[ PgText(P.Keys[I].Action, 13.f, ENeuFont::Regular, Neu::Txt(), ENeuAlign::Left, 1, 10.f) ] ]; } const bool bBloom = Weak.IsValid() && Weak->IsBloomAvailable(); TSharedRef Slider = SNew(SNeuSlider).Width(330.f) .Value_Lambda([Weak]() { const UDescentUiSubsystem* U = Weak.Get(); return U && U->GetBloomMax() > 0.f ? U->GetBloomValue() / U->GetBloomMax() : 0.f; }) .ValueText_Lambda([Weak]() { const UDescentUiSubsystem* U = Weak.Get(); return U && U->IsBloomAvailable() ? FString::Printf(TEXT("%.2f"), U->GetBloomValue()) : FString(TEXT("n/a")); }) .OnValue([Weak](float V01) { if (UDescentUiSubsystem* U = Weak.Get()) { U->SetBloomValue(V01 * U->GetBloomMax()); } }); Slider->SetEnabled(bBloom); return SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding(0.f, 0.f, 26.f, 0.f) [ SNew(SBox).WidthOverride(360.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 20.f) [ SNew(SNeuButton).Text(TEXT("RESUME")).MinWidth(330.f).Px(18.f).Height(48.f).OnClick([Weak, Act = P.OnResume]() { if (UDescentUiSubsystem* U = Weak.Get()) { U->Close(); } if (Act) { Act(); } }) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 30.f) [ SNew(SNeuButton).Text(TEXT("SAVE GAME")).MinWidth(330.f).Px(16.f).Height(42.f).OnClick(P.OnSave) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 6.f)[ PgCaption(TEXT("BLOOM")) ] + SVerticalBox::Slot().AutoHeight()[ Slider ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 6.f, 0.f, 0.f)[ PgText(bBloom ? FString::Printf(TEXT("Bound to %s"), *Weak->GetBloomVariableName()) : FString::Printf(TEXT("%s does not exist in this build"), *Weak->GetBloomVariableName()), 12.f, ENeuFont::Regular, Neu::Dim(), ENeuAlign::Left, 2) ] ] ] + SHorizontalBox::Slot().FillWidth(1.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)[ PgCaption(TEXT("KEYS")) ] + SVerticalBox::Slot().FillHeight(1.f) [ PgWell(SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 18.f, 0.f)[ KeysA ] + SHorizontalBox::Slot().FillWidth(1.f)[ KeysB ], FMargin(18.f, 16.f)) ] ]; } TSharedRef PgTitle(const FPgWeak& Weak) { const FDescentTitleViewModel& T = PgModels(Weak).Title; TSharedRef ContinueButton = SNew(SNeuButton).Text(TEXT("CONTINUE")).MinWidth(180.f).Px(20.f).Height(54.f) .OnClick([Weak, Act = T.OnContinue]() { if (UDescentUiSubsystem* U = Weak.Get()) { U->Close(); } if (Act) { Act(); } }); ContinueButton->SetEnabled(T.bCanContinue); return SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.f)[ SNullWidget::NullWidget ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)[ PgText(TEXT("THE DESCENT"), 60.f, ENeuFont::Bold, Neu::Txt(), ENeuAlign::Center) ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.f, 4.f, 0.f, 28.f)[ PgText(T.Subtitle, 18.f, ENeuFont::Regular, Neu::Cyan(), ENeuAlign::Center) ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.f, 0.f, 0.f, 30.f) [ SNew(SBox).WidthOverride(460.f) [ SNew(SNeuProgress).Height(20.f) .Value_Lambda([Weak]() { return PgModels(Weak).Title.Progress; }) .Text_Lambda([Weak]() { return PgModels(Weak).Title.ProgressText; }) ] ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding(0.f, 0.f, 20.f, 0.f) [ SNew(SNeuButton).Text(TEXT("DIVE")).MinWidth(180.f).Px(20.f).Height(54.f).OnClick([Weak, Act = T.OnDive]() { if (UDescentUiSubsystem* U = Weak.Get()) { U->Close(); } if (Act) { Act(); } }) ] + SHorizontalBox::Slot().AutoWidth()[ ContinueButton ] ] + SVerticalBox::Slot().FillHeight(1.f)[ SNullWidget::NullWidget ]; } TSharedRef PgEnd(const FPgWeak& Weak) { const FDescentEndCardViewModel& E = PgModels(Weak).EndCard; auto Stat = [](const TCHAR* Label, const FString& Value, const FLinearColor& C) -> TSharedRef { return SNew(SBox).HeightOverride(86.f) [ SNew(SNeuPanel).Inset(true).Padding(FMargin(18.f, 10.f)) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[ PgText(Label, 12.f, ENeuFont::Bold, Neu::Dim()) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 4.f, 0.f, 0.f)[ PgText(Value, 26.f, ENeuFont::Bold, C, ENeuAlign::Left, 1, 16.f) ] ] ]; }; return SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)[ PgText(TEXT("END OF CONTRACT"), 34.f, ENeuFont::Bold, Neu::Txt(), ENeuAlign::Center) ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.f, 6.f, 0.f, 24.f)[ PgText(E.Line, 16.f, ENeuFont::Regular, Neu::Cyan(), ENeuAlign::Center) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 14.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 14.f, 0.f)[ Stat(TEXT("HULL"), FString::Printf(TEXT("%d %%"), FMath::RoundToInt(E.HullPct)), Neu::Cyan()) ] + SHorizontalBox::Slot().FillWidth(1.f)[ Stat(TEXT("BALANCE"), PgCredits(E.Balance), E.Balance < 0 ? Neu::Red() : Neu::Green()) ] ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 26.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 14.f, 0.f)[ Stat(TEXT("LOGS"), FString::Printf(TEXT("%d / %d"), E.LogsFound, E.LogsTotal), Neu::Violet()) ] + SHorizontalBox::Slot().FillWidth(1.f)[ Stat(TEXT("TIME"), E.TimeText, Neu::Txt()) ] ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center) [ SNew(SNeuButton).Text(TEXT("CONTINUE")).MinWidth(200.f).Px(18.f).Height(48.f) .OnClick([Weak, Act = E.OnContinue]() { if (UDescentUiSubsystem* U = Weak.Get()) { U->Close(); } if (Act) { Act(); } }) ]; } } TSharedRef Descent::Ui::BuildPage(UDescentUiSubsystem& Ui, EDescentWindow Window, FName Tab) { const FPgWeak Weak(&Ui); switch (Window) { case EDescentWindow::Pda: if (Tab == TEXT("map")) { return PgPdaMap(Weak); } if (Tab == TEXT("logs")) { return PgPdaLogs(Weak); } if (Tab == TEXT("status")) { return PgPdaStatus(Weak); } return PgPdaInventory(Weak); case EDescentWindow::Fabricator: return PgFabricator(Weak, Tab); case EDescentWindow::Refinery: return PgRefinery(Weak); case EDescentWindow::Packer: return PgPacker(Weak); case EDescentWindow::Storage: return PgStorage(Weak); case EDescentWindow::Terminal: return PgTerminal(Weak); case EDescentWindow::Vat: return PgVat(Weak); case EDescentWindow::Build: return PgBuild(Weak); case EDescentWindow::Pause: return PgPause(Weak); case EDescentWindow::Title: return PgTitle(Weak); case EDescentWindow::EndCard: return PgEnd(Weak); default: return SNullWidget::NullWidget; } }