#include "UI/DescentHudLayout.h" #include "Core/Tuning/DescentTune_UI.h" namespace UiTune = Descent::Tune::UI; namespace { FDescentHudRect HudRect(const TCHAR* Name, float X, float Y, float W, float H, bool bVisible) { FDescentHudRect R; R.Name = Name; R.X = X; R.Y = Y; R.W = W; R.H = H; R.bVisible = bVisible; return R; } bool HudModeIsTitle(const FDescentHudModel& M) { return M.Mode == TEXT("Title"); } } FDescentHudLayout FDescentHudLayout::Compute(const FVector2f& LayoutSize, const FDescentHudModel& M) { FDescentHudLayout L; L.Size = LayoutSize; const float W = LayoutSize.X; const float H = LayoutSize.Y; const float Mg = UiTune::HudMarginPx; L.bNarrow = W < UiTune::NarrowWidthPx; L.bCompact = H < UiTune::CompactHeightPx; L.Gauge = L.bNarrow ? UiTune::HudGaugeNarrowPx : UiTune::HudGaugeWidePx; const bool bShown = !HudModeIsTitle(M); const float G = L.Gauge; // ---- top centre: depth panel ---- const float DepthW = L.bNarrow ? 260.f : (L.bCompact ? 300.f : 320.f); const float DepthH = L.bNarrow ? 96.f : (L.bCompact ? 104.f : 128.f); L.Depth = HudRect(TEXT("depth"), (W - DepthW) * 0.5f, Mg, DepthW, DepthH, bShown); // ---- top right: toasts ---- const float ToastW = L.bNarrow ? 200.f : UiTune::HudToastWidthPx; const int32 NToast = FMath::Min(M.Toasts.Num(), FDescentHudModel::MaxToasts); const float ToastH = NToast > 0 ? NToast * UiTune::HudToastHeightPx + (NToast - 1) * UiTune::HudToastGapPx : 0.f; L.Toasts = HudRect(TEXT("toasts"), W - Mg - ToastW, Mg, ToastW, ToastH, bShown && NToast > 0); // ---- top left: objective (at most 30 percent of the width, three lines, one line when folded) ---- const float ObjW = FMath::Max(UiTune::HudObjectiveMinWidthPx, W * UiTune::HudObjectiveMaxFraction); const float ObjH = M.bObjectiveFolded ? UiTune::HudObjectiveFoldedHeightPx : (L.bCompact ? 70.f : 82.f); const bool bObjShown = !M.Objective.IsEmpty() && (!L.bNarrow || !M.bObjectiveFolded); L.Objective = HudRect(TEXT("objective"), Mg, Mg, ObjW, ObjH, bShown && bObjShown); // ---- bottom left: vitals ---- const int32 NGauge = 2 + (M.Vitals.bShowO2 ? 1 : 0); const float VitalsPad = 12.f; const float VitalsW = 2.f * VitalsPad + NGauge * G + (NGauge - 1) * 10.f; const float VitalsMaxW = 2.f * VitalsPad + 3.f * G + 2.f * 10.f; const float VitalsH = G + 62.f; L.Vitals = HudRect(TEXT("vitals"), Mg, H - Mg - VitalsH, VitalsW, VitalsH, bShown); // ---- bottom right: vehicle panel ---- const float VehW = L.bNarrow ? 226.f : 300.f; const float VehH = L.bNarrow ? 118.f : 134.f; L.Vehicle = HudRect(TEXT("vehicle"), W - Mg - VehW, H - Mg - VehH, VehW, VehH, bShown && M.Vehicle.bVisible); // ---- bottom centre: hotbar, radio ---- L.Slot = L.bNarrow ? 48.f : UiTune::HotbarSlotPx; const float SlotPx = L.Slot; const float HotbarW = 2.f * 10.f + 5.f * SlotPx + 4.f * 10.f; const float HotbarH = SlotPx + 20.f; L.Hotbar = HudRect(TEXT("hotbar"), (W - HotbarW) * 0.5f, H - Mg - HotbarH, HotbarW, HotbarH, bShown && M.bShowHotbar); const float SideW = FMath::Max(VitalsMaxW, L.Vehicle.bVisible ? VehW : 0.f); float RadioW = FMath::Min(UiTune::HudRadioMaxWidthPx, W - 2.f * SideW - 60.f); float RadioY; if (L.bNarrow) { // Narrow screens: the radio moves under the depth panel, between the objective column and the toast column. RadioW = FMath::Min(UiTune::HudRadioMaxWidthPx, W - 2.f * (ToastW + 2.f * Mg)); RadioY = L.Depth.Bottom() + 8.f; } else { RadioY = (L.Hotbar.bVisible ? L.Hotbar.Y : H - Mg) - (L.Hotbar.bVisible ? UiTune::HudRadioGapPx : 0.f) - UiTune::HudRadioHeightPx; } RadioW = FMath::Max(160.f, RadioW); L.Radio = HudRect(TEXT("radio"), (W - RadioW) * 0.5f, RadioY, RadioW, UiTune::HudRadioHeightPx, bShown && !M.Radio.IsEmpty()); // ---- warning banner: under the depth panel (under the radio on narrow screens) ---- const float WarnW = FMath::Max(160.f, FMath::Min(420.f, W - 2.f * (ToastW + 2.f * Mg))); const float WarnY = (L.bNarrow && L.Radio.bVisible ? L.Radio.Bottom() : L.Depth.Bottom()) + 8.f; L.Warning = HudRect(TEXT("warning"), (W - WarnW) * 0.5f, WarnY, WarnW, UiTune::HudWarningHeightPx, bShown && !M.Warnings.Text.IsEmpty()); // ---- centre: prompt and build reason, 30 below the crosshair ---- const float PromptW = FMath::Max(160.f, FMath::Min(UiTune::HudPromptMaxWidthPx, W - 2.f * (VehW + 2.f * Mg))); const float PromptY = H * 0.5f + UiTune::HudPromptOffsetPx; L.Prompt = HudRect(TEXT("prompt"), (W - PromptW) * 0.5f, PromptY, PromptW, UiTune::HudPromptHeightPx, bShown && !M.Prompt.IsEmpty()); L.BuildReason = HudRect(TEXT("build_reason"), (W - PromptW) * 0.5f, PromptY + UiTune::HudPromptHeightPx + 6.f, PromptW, 28.f, bShown && !M.BuildReason.IsEmpty()); // ---- right centre: proximity panel ---- const int32 NProx = M.Proximity.Num(); const float ProxRow = L.bCompact ? 22.f : UiTune::HudProximityRowPx; const float ProxH = 30.f + NProx * ProxRow + 8.f; L.Proximity = HudRect(TEXT("proximity"), W - Mg - UiTune::HudProximityWidthPx, (H - ProxH) * 0.5f, UiTune::HudProximityWidthPx, ProxH, bShown && M.bShowProximity && NProx > 0); return L; } TArray FDescentHudLayout::All() const { return { &Depth, &Objective, &Toasts, &Vitals, &Vehicle, &Hotbar, &Radio, &Prompt, &BuildReason, &Proximity, &Warning }; } TArray FDescentHudLayout::VisibleRects() const { TArray Out; for (const FDescentHudRect* R : All()) { if (R->bVisible) { Out.Add(R); } } return Out; } bool FDescentHudLayout::FindOverlap(FName& OutA, FName& OutB) const { const TArray V = VisibleRects(); for (int32 I = 0; I < V.Num(); ++I) { for (int32 J = I + 1; J < V.Num(); ++J) { if (V[I]->Intersects(*V[J])) { OutA = V[I]->Name; OutB = V[J]->Name; return true; } } } return false; }