diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 4bac515..4624888 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -817,7 +817,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe ## 0.1.O Investor Visual MVP And Menu Polish - [x] Reclassify the current investor demo as systems-first, not investor visual MVP, until the visual/menu quality gate below is complete. Added an explicit investor-demo status document, legal notice wording, startup notice wording, packaged README classification, and verification so the current demo is presented as a systems-first investor prototype until 0.1.O visual/menu gates are actually complete. -- [ ] Replace the native painted MVP frontend with a proper UMG menu flow using real button widgets, hover/pressed states, keyboard/controller focus, mouse click targets, and predictable back/escape behavior. +- [x] Replace the native painted MVP frontend with a proper UMG menu flow using real button widgets, hover/pressed states, keyboard/controller focus, mouse click targets, and predictable back/escape behavior. Converted the MVP frontend to a WidgetTree-built UMG flow with UButton/UTextBlock controls, hover/pressed button styling, focusable primary actions, mouse-click character selection, back handling, and save/quit actions while preserving the existing player-controller API. - [ ] Make startup credits, character selection, server/join, loading, pause, save, and quit feel like separate intentional segments, with no impression that gameplay has started underneath the UI. - [ ] Add a visually verified packaged-client startup test using Sunshine/Moonlight or another real GPU desktop capture path, because QEMU guest-agent screenshots cannot validate the interactive rendered desktop. - [ ] Add first realistic playable character proxies for the selected young adult male and female archetypes, replacing the default mannequin/dummy presentation for investor builds. diff --git a/Scripts/verify_mvp_frontend_umg_flow.py b/Scripts/verify_mvp_frontend_umg_flow.py new file mode 100644 index 0000000..545b429 --- /dev/null +++ b/Scripts/verify_mvp_frontend_umg_flow.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Verify the MVP frontend uses real UMG widgets instead of native paint hit boxes.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +HEADER = REPO_ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.h" +SOURCE = REPO_ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp" +ROADMAP = REPO_ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +def require(condition: bool, message: str) -> None: + if not condition: + raise SystemExit(f"FAILED: {message}") + + +def main() -> None: + header = HEADER.read_text(encoding="utf-8") + source = SOURCE.read_text(encoding="utf-8") + roadmap = ROADMAP.read_text(encoding="utf-8") + + for token in ( + "class UButton;", + "class UTextBlock;", + "RebuildFrontendTree", + "HandlePrimaryActionClicked", + "HandleBackClicked", + "HandleSaveAndQuitClicked", + "HandleMaleCharacterClicked", + "HandleFemaleCharacterClicked", + ): + require(token in header, f"missing frontend UMG declaration: {token}") + + for token in ( + "WidgetTree->ConstructWidget", + "UButton::StaticClass()", + "UTextBlock::StaticClass()", + "OnClicked.AddDynamic", + "OnHovered.AddDynamic", + "ButtonStyle.Hovered.TintColor", + "ButtonStyle.Pressed.TintColor", + "SetIsFocusable(true)", + "SetKeyboardFocus()", + "BackFromActiveScreen()", + "SaveAndQuit()", + ): + require(token in source, f"missing frontend UMG implementation token: {token}") + + for removed_token in ( + "FSlateDrawElement", + "NativePaint", + "NativeOnMouseButtonDown", + "GetPanelLayout", + "IsPointInside", + "DrawMainMenu", + "DrawCharacterSelection", + "DrawJoinServer", + "DrawLoading", + "DrawTextAt", + ): + require(removed_token not in source, f"native-painted frontend path still present: {removed_token}") + require(removed_token not in header, f"native-painted frontend declaration still present: {removed_token}") + + require( + "- [x] Replace the native painted MVP frontend with a proper UMG menu flow" in roadmap, + "0.1.O UMG frontend roadmap item is not checked off", + ) + + print("OK: MVP frontend uses UMG controls with mouse, focus, hover, back, and save/quit paths.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index ffe6d72..5f7be33 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -2,16 +2,42 @@ #include "AgrarianMvpFrontendWidget.h" +#include "Blueprint/WidgetTree.h" +#include "Components/Border.h" +#include "Components/Button.h" +#include "Components/ButtonSlot.h" +#include "Components/HorizontalBox.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/SizeBox.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" #include "GameFramework/PlayerController.h" #include "InputCoreTypes.h" -#include "Rendering/DrawElements.h" #include "Styling/CoreStyle.h" +namespace +{ +FButtonStyle MakeAgrarianButtonStyle(const FLinearColor& NormalColor, const FLinearColor& HoveredColor) +{ + FButtonStyle ButtonStyle = FCoreStyle::Get().GetWidgetStyle(TEXT("Button")); + ButtonStyle.Normal.TintColor = FSlateColor(NormalColor); + ButtonStyle.Hovered.TintColor = FSlateColor(HoveredColor); + ButtonStyle.Pressed.TintColor = FSlateColor(FLinearColor( + FMath::Min(HoveredColor.R + 0.12f, 1.0f), + FMath::Min(HoveredColor.G + 0.12f, 1.0f), + FMath::Min(HoveredColor.B + 0.12f, 1.0f), + HoveredColor.A)); + return ButtonStyle; +} +} + void UAgrarianMvpFrontendWidget::NativeConstruct() { Super::NativeConstruct(); SetIsFocusable(true); - SetKeyboardFocus(); + RebuildFrontendTree(); + FocusPrimaryButton(); } FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) @@ -80,131 +106,32 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry, return Super::NativeOnKeyDown(InGeometry, InKeyEvent); } -FReply UAgrarianMvpFrontendWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) -{ - if (InMouseEvent.GetEffectingButton() != EKeys::LeftMouseButton) - { - return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); - } - - const FVector2D LocalMousePosition = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); - float Scale = 1.0f; - FVector2D PanelPosition = FVector2D::ZeroVector; - FVector2D PanelSize = FVector2D::ZeroVector; - if (!GetPanelLayout(InGeometry.GetLocalSize(), Scale, PanelPosition, PanelSize)) - { - return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); - } - - const float ContentX = PanelPosition.X + (44.0f * Scale); - const float ContentWidth = PanelSize.X - (88.0f * Scale); - - if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) - { - const FVector2D CardSize((ContentWidth - (24.0f * Scale)) * 0.5f, 206.0f * Scale); - const FVector2D MaleCardPosition(ContentX, PanelPosition.Y + (142.0f * Scale)); - const FVector2D FemaleCardPosition(ContentX + CardSize.X + (24.0f * Scale), MaleCardPosition.Y); - const FVector2D ButtonPosition(ContentX, PanelPosition.Y + PanelSize.Y - (88.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(ContentWidth, 280.0f * Scale), 52.0f * Scale); - - if (IsPointInside(LocalMousePosition, MaleCardPosition, CardSize)) - { - SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); - return FReply::Handled(); - } - - if (IsPointInside(LocalMousePosition, FemaleCardPosition, CardSize)) - { - SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); - return FReply::Handled(); - } - - if (IsPointInside(LocalMousePosition, ButtonPosition, ButtonSize)) - { - ConfirmActiveScreen(); - return FReply::Handled(); - } - } - else if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer) - { - const float JoinContentX = PanelPosition.X + (48.0f * Scale); - const float JoinContentWidth = PanelSize.X - (96.0f * Scale); - const FVector2D ButtonPosition(JoinContentX, PanelPosition.Y + (300.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(JoinContentWidth, 330.0f * Scale), 58.0f * Scale); - const FVector2D BackButtonPosition(ButtonPosition.X + ButtonSize.X + (16.0f * Scale), ButtonPosition.Y); - const FVector2D BackButtonSize(FMath::Min(JoinContentWidth - ButtonSize.X - (16.0f * Scale), 180.0f * Scale), ButtonSize.Y); - - if (IsPointInside(LocalMousePosition, ButtonPosition, ButtonSize)) - { - ConfirmActiveScreen(); - return FReply::Handled(); - } - - if (BackButtonSize.X > 96.0f && IsPointInside(LocalMousePosition, BackButtonPosition, BackButtonSize)) - { - BackFromActiveScreen(); - return FReply::Handled(); - } - } - else if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading) - { - const float LoadingContentX = PanelPosition.X + (48.0f * Scale); - const float LoadingContentWidth = PanelSize.X - (96.0f * Scale); - const FVector2D ButtonPosition(LoadingContentX, PanelPosition.Y + (318.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(LoadingContentWidth, 310.0f * Scale), 56.0f * Scale); - - if (IsPointInside(LocalMousePosition, ButtonPosition, ButtonSize)) - { - ConfirmActiveScreen(); - return FReply::Handled(); - } - } - else if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu) - { - const float MenuContentX = PanelPosition.X + (48.0f * Scale); - const float MenuContentWidth = PanelSize.X - (96.0f * Scale); - const FVector2D ResumeButtonPosition(MenuContentX, PanelPosition.Y + (220.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(MenuContentWidth, 310.0f * Scale), 56.0f * Scale); - const FVector2D QuitButtonPosition(MenuContentX, ResumeButtonPosition.Y + (72.0f * Scale)); - - if (IsPointInside(LocalMousePosition, ResumeButtonPosition, ButtonSize)) - { - ConfirmActiveScreen(); - return FReply::Handled(); - } - - if (IsPointInside(LocalMousePosition, QuitButtonPosition, ButtonSize)) - { - SaveAndQuit(); - return FReply::Handled(); - } - } - - return FReply::Handled(); -} - void UAgrarianMvpFrontendWidget::SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen) { ActiveScreen = NewScreen; - InvalidateLayoutAndVolatility(); + RebuildFrontendTree(); + FocusPrimaryButton(); } void UAgrarianMvpFrontendWidget::SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype NewArchetype) { SelectedCharacterArchetype = NewArchetype; - InvalidateLayoutAndVolatility(); + RebuildFrontendTree(); + FocusPrimaryButton(); } void UAgrarianMvpFrontendWidget::SetUiScale(float NewUiScale) { UiScale = FMath::Clamp(NewUiScale, 0.75f, 1.5f); - InvalidateLayoutAndVolatility(); + RebuildFrontendTree(); + FocusPrimaryButton(); } void UAgrarianMvpFrontendWidget::SetHighContrastMode(bool bNewUseHighContrast) { bUseHighContrast = bNewUseHighContrast; - InvalidateLayoutAndVolatility(); + RebuildFrontendTree(); + FocusPrimaryButton(); } void UAgrarianMvpFrontendWidget::ConfirmActiveScreen() @@ -226,98 +153,6 @@ void UAgrarianMvpFrontendWidget::SaveAndQuit() } } -int32 UAgrarianMvpFrontendWidget::NativePaint( - const FPaintArgs& Args, - const FGeometry& AllottedGeometry, - const FSlateRect& MyCullingRect, - FSlateWindowElementList& OutDrawElements, - int32 LayerId, - const FWidgetStyle& InWidgetStyle, - bool bParentEnabled) const -{ - LayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); - - const FVector2D Size = AllottedGeometry.GetLocalSize(); - float Scale = 1.0f; - FVector2D PanelPosition = FVector2D::ZeroVector; - FVector2D PanelSize = FVector2D::ZeroVector; - GetPanelLayout(Size, Scale, PanelPosition, PanelSize); - const FLinearColor BackdropColor = bUseHighContrast ? FLinearColor(0.0f, 0.0f, 0.0f, 1.0f) : FLinearColor(0.015f, 0.018f, 0.014f, 0.99f); - const FLinearColor PanelColor = bUseHighContrast ? FLinearColor(0.0f, 0.0f, 0.0f, 0.98f) : FLinearColor(0.035f, 0.045f, 0.034f, 0.96f); - const FLinearColor AccentColor = bUseHighContrast ? FLinearColor(0.95f, 0.95f, 0.30f, 1.0f) : FLinearColor(0.45f, 0.72f, 0.40f, 1.0f); - - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(Size), FSlateLayoutTransform(FVector2f::ZeroVector)), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - BackdropColor); - - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(PanelSize), FSlateLayoutTransform(FVector2f(PanelPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - PanelColor); - - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(PanelSize.X, 4.0f * Scale), FSlateLayoutTransform(FVector2f(PanelPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - AccentColor); - - if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu) - { - DrawMainMenu(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); - } - else if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) - { - DrawCharacterSelection(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); - } - else if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer) - { - DrawJoinServer(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); - } - else if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading) - { - DrawLoading(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); - } - - return LayerId; -} - -bool UAgrarianMvpFrontendWidget::GetPanelLayout(const FVector2D& WidgetSize, float& OutScale, FVector2D& OutPanelPosition, FVector2D& OutPanelSize) const -{ - OutScale = FMath::Clamp(UiScale, 0.75f, 1.5f); - constexpr float MinimumPanelMargin = 24.0f; - constexpr float PreferredPanelWidth = 780.0f; - constexpr float PreferredPanelHeight = 430.0f; - const FVector2D AvailablePanelSize( - FMath::Max(320.0f, WidgetSize.X - (MinimumPanelMargin * 2.0f)), - FMath::Max(240.0f, WidgetSize.Y - (MinimumPanelMargin * 2.0f))); - - OutPanelSize = FVector2D( - FMath::Min(AvailablePanelSize.X, PreferredPanelWidth * OutScale), - FMath::Min(AvailablePanelSize.Y, PreferredPanelHeight * OutScale)); - OutPanelPosition = FVector2D( - (WidgetSize.X - OutPanelSize.X) * 0.5f, - (WidgetSize.Y - OutPanelSize.Y) * 0.5f); - - return WidgetSize.X > 0.0f && WidgetSize.Y > 0.0f; -} - -bool UAgrarianMvpFrontendWidget::IsPointInside(const FVector2D& Point, const FVector2D& Position, const FVector2D& Size) const -{ - return Point.X >= Position.X - && Point.Y >= Position.Y - && Point.X <= Position.X + Size.X - && Point.Y <= Position.Y + Size.Y; -} - void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen() { if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) @@ -371,238 +206,253 @@ void UAgrarianMvpFrontendWidget::CompleteFrontendFlow() RemoveFromParent(); } -void UAgrarianMvpFrontendWidget::DrawMainMenu( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const +void UAgrarianMvpFrontendWidget::RebuildFrontendTree() { - const float ContentX = PanelPosition.X + (48.0f * Scale); - const float ContentWidth = PanelSize.X - (96.0f * Scale); - const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(54.0f * Scale)); - const FSlateFontInfo SubtitleFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(22.0f * Scale)); - const FSlateFontInfo ButtonFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(24.0f * Scale)); - const FSlateFontInfo HintFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(16.0f * Scale)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, MainMenuTitle, FVector2D(ContentX, PanelPosition.Y + (58.0f * Scale)), ContentWidth, TitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Game menu")), FVector2D(ContentX + (4.0f * Scale), PanelPosition.Y + (128.0f * Scale)), ContentWidth, SubtitleFont, FLinearColor(0.72f, 0.80f, 0.68f, 1.0f)); - - const FVector2D ButtonPosition(ContentX, PanelPosition.Y + (220.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(ContentWidth, 310.0f * Scale), 56.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(ButtonSize), FSlateLayoutTransform(FVector2f(ButtonPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.35f, 0.58f, 0.30f, 0.95f)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Resume")), ButtonPosition + FVector2D(22.0f * Scale, 11.0f * Scale), ButtonSize.X - (44.0f * Scale), ButtonFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f)); - - const FVector2D QuitButtonPosition(ContentX, ButtonPosition.Y + (72.0f * Scale)); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(ButtonSize), FSlateLayoutTransform(FVector2f(QuitButtonPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.42f, 0.22f, 0.18f, 0.95f)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Save & Quit")), QuitButtonPosition + FVector2D(22.0f * Scale, 11.0f * Scale), ButtonSize.X - (44.0f * Scale), ButtonFont, FLinearColor(1.0f, 0.92f, 0.84f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Press Escape to open this menu. Save & Quit writes the current world save before closing.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (66.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); -} - -void UAgrarianMvpFrontendWidget::DrawCharacterSelection( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const -{ - const float ContentX = PanelPosition.X + (44.0f * Scale); - const float ContentWidth = PanelSize.X - (88.0f * Scale); - const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(34.0f * Scale)); - const FSlateFontInfo BodyFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(18.0f * Scale)); - const FSlateFontInfo CardTitleFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(22.0f * Scale)); - const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(15.0f * Scale)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Choose your first pioneer")), FVector2D(ContentX, PanelPosition.Y + (34.0f * Scale)), ContentWidth, TitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Select the person who will step into Ground Zero.")), FVector2D(ContentX + (2.0f * Scale), PanelPosition.Y + (82.0f * Scale)), ContentWidth, BodyFont, FLinearColor(0.72f, 0.80f, 0.68f, 1.0f)); - - const FVector2D CardSize((ContentWidth - (24.0f * Scale)) * 0.5f, 206.0f * Scale); - const FVector2D MaleCardPosition(ContentX, PanelPosition.Y + (142.0f * Scale)); - const FVector2D FemaleCardPosition(ContentX + CardSize.X + (24.0f * Scale), MaleCardPosition.Y); - - auto DrawCharacterCard = [&](const FVector2D& CardPosition, const FText& Title, const FText& Summary, const FLinearColor& AccentColor, bool bSelected) + if (!WidgetTree) { - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(CardSize), FSlateLayoutTransform(FVector2f(CardPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.055f, 0.065f, 0.05f, 0.95f)); + return; + } - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(CardSize.X, 3.0f * Scale), FSlateLayoutTransform(FVector2f(CardPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - AccentColor); + PrimaryFocusButton = nullptr; - if (bSelected) + const float Scale = FMath::Clamp(UiScale, 0.75f, 1.5f); + const FLinearColor BackdropColor = bUseHighContrast ? FLinearColor::Black : FLinearColor(0.015f, 0.018f, 0.014f, 0.99f); + const FLinearColor PanelColor = bUseHighContrast ? FLinearColor(0.0f, 0.0f, 0.0f, 0.98f) : FLinearColor(0.035f, 0.045f, 0.034f, 0.96f); + const FLinearColor AccentColor = bUseHighContrast ? FLinearColor(0.95f, 0.95f, 0.30f, 1.0f) : FLinearColor(0.45f, 0.72f, 0.40f, 1.0f); + const FLinearColor TextColor = bUseHighContrast ? FLinearColor::White : FLinearColor(0.92f, 0.98f, 0.84f, 1.0f); + const FLinearColor MutedTextColor = bUseHighContrast ? FLinearColor(0.86f, 0.86f, 0.66f, 1.0f) : FLinearColor(0.72f, 0.80f, 0.68f, 1.0f); + const FLinearColor ButtonColor = bUseHighContrast ? FLinearColor(0.30f, 0.30f, 0.02f, 1.0f) : FLinearColor(0.22f, 0.40f, 0.18f, 0.98f); + const FLinearColor ButtonHoverColor = bUseHighContrast ? FLinearColor(0.48f, 0.48f, 0.08f, 1.0f) : FLinearColor(0.35f, 0.58f, 0.30f, 1.0f); + const FLinearColor SecondaryButtonColor = bUseHighContrast ? FLinearColor(0.12f, 0.12f, 0.12f, 1.0f) : FLinearColor(0.10f, 0.13f, 0.09f, 0.98f); + const FLinearColor QuitButtonColor = bUseHighContrast ? FLinearColor(0.32f, 0.10f, 0.08f, 1.0f) : FLinearColor(0.42f, 0.22f, 0.18f, 0.98f); + + UBorder* RootBorder = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("MvpFrontendRoot")); + RootBorder->SetBrushColor(BackdropColor); + RootBorder->SetPadding(FMargin(24.0f * Scale)); + RootBorder->SetHorizontalAlignment(HAlign_Center); + RootBorder->SetVerticalAlignment(VAlign_Center); + WidgetTree->RootWidget = RootBorder; + + USizeBox* PanelSizeBox = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("MvpFrontendPanelSize")); + PanelSizeBox->SetWidthOverride(780.0f * Scale); + PanelSizeBox->SetMinDesiredWidth(420.0f); + PanelSizeBox->SetMinDesiredHeight(300.0f); + RootBorder->SetContent(PanelSizeBox); + + UBorder* PanelBorder = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("MvpFrontendPanel")); + PanelBorder->SetBrushColor(PanelColor); + PanelBorder->SetPadding(FMargin(42.0f * Scale, 34.0f * Scale)); + PanelSizeBox->SetContent(PanelBorder); + + UVerticalBox* Panel = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("MvpFrontendStack")); + PanelBorder->SetContent(Panel); + + if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu) + { + AddText(Panel, MainMenuTitle, FMath::RoundToInt(54.0f * Scale), true, TextColor, 6.0f * Scale); + AddText(Panel, FText::FromString(TEXT("Game menu")), FMath::RoundToInt(22.0f * Scale), false, MutedTextColor, 72.0f * Scale); + PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Resume")), ButtonColor, ButtonHoverColor, 16.0f * Scale); + PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); + PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); + + UButton* QuitButton = AddButton(Panel, FText::FromString(TEXT("Save & Quit")), QuitButtonColor, FLinearColor(0.58f, 0.28f, 0.22f, 1.0f), 34.0f * Scale); + QuitButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked); + AddText(Panel, FText::FromString(TEXT("Escape opens this menu. Save & Quit writes the current world save before closing.")), FMath::RoundToInt(16.0f * Scale), false, MutedTextColor, 0.0f); + return; + } + + if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) + { + AddText(Panel, FText::FromString(TEXT("Choose your first pioneer")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 6.0f * Scale); + AddText(Panel, FText::FromString(TEXT("Select the person who will step into Ground Zero.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 24.0f * Scale); + + UHorizontalBox* CharacterRow = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("CharacterRow")); + if (UVerticalBoxSlot* RowSlot = Panel->AddChildToVerticalBox(CharacterRow)) { - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(CardSize.X, CardSize.Y), FSlateLayoutTransform(FVector2f(CardPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(AccentColor.R, AccentColor.G, AccentColor.B, 0.16f)); + RowSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 22.0f * Scale)); } - const FVector2D PortraitPosition = CardPosition + FVector2D(24.0f * Scale, 42.0f * Scale); - const FVector2D PortraitSize(72.0f * Scale, 112.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(PortraitSize), FSlateLayoutTransform(FVector2f(PortraitPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.14f, 0.16f, 0.13f, 1.0f)); + const bool bMaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultMale; + const bool bFemaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale; + UButton* MaleButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("MalePioneerButton")); + MaleButton->SetStyle(MakeAgrarianButtonStyle(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor)); + MaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked); + UVerticalBox* MaleStack = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("MalePioneerStack")); + MaleButton->SetContent(MaleStack); + AddText(MaleStack, FText::FromString(TEXT("Young adult male")), FMath::RoundToInt(21.0f * Scale), true, TextColor, 8.0f * Scale); + AddText(MaleStack, FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 18.0f * Scale); + AddText(MaleStack, bMaleSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), FMath::RoundToInt(15.0f * Scale), true, bMaleSelected ? AccentColor : MutedTextColor, 0.0f); + if (UHorizontalBoxSlot* MaleSlot = CharacterRow->AddChildToHorizontalBox(MaleButton)) + { + MaleSlot->SetPadding(FMargin(0.0f, 0.0f, 12.0f * Scale, 0.0f)); + MaleSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, Title, CardPosition + FVector2D(116.0f * Scale, 42.0f * Scale), CardSize.X - (140.0f * Scale), CardTitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, Summary, CardPosition + FVector2D(116.0f * Scale, 82.0f * Scale), CardSize.X - (140.0f * Scale), LabelFont, FLinearColor(0.72f, 0.78f, 0.68f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, bSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), CardPosition + FVector2D(116.0f * Scale, 144.0f * Scale), CardSize.X - (140.0f * Scale), LabelFont, bSelected ? AccentColor : FLinearColor(0.58f, 0.62f, 0.54f, 1.0f)); - }; + UButton* FemaleButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("FemalePioneerButton")); + FemaleButton->SetStyle(MakeAgrarianButtonStyle(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor)); + FemaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked); + UVerticalBox* FemaleStack = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack")); + FemaleButton->SetContent(FemaleStack); + AddText(FemaleStack, FText::FromString(TEXT("Young adult female")), FMath::RoundToInt(21.0f * Scale), true, TextColor, 8.0f * Scale); + AddText(FemaleStack, FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 18.0f * Scale); + AddText(FemaleStack, bFemaleSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), FMath::RoundToInt(15.0f * Scale), true, bFemaleSelected ? AccentColor : MutedTextColor, 0.0f); + if (UHorizontalBoxSlot* FemaleSlot = CharacterRow->AddChildToHorizontalBox(FemaleButton)) + { + FemaleSlot->SetPadding(FMargin(12.0f * Scale, 0.0f, 0.0f, 0.0f)); + FemaleSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } - DrawCharacterCard(MaleCardPosition, FText::FromString(TEXT("Young adult male")), FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FLinearColor(0.36f, 0.58f, 0.78f, 1.0f), SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultMale); - DrawCharacterCard(FemaleCardPosition, FText::FromString(TEXT("Young adult female")), FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FLinearColor(0.56f, 0.68f, 0.46f, 1.0f), SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale); - - const FVector2D ButtonPosition(ContentX, PanelPosition.Y + PanelSize.Y - (88.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(ContentWidth, 280.0f * Scale), 52.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(ButtonSize), FSlateLayoutTransform(FVector2f(ButtonPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.35f, 0.58f, 0.30f, 0.95f)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Continue")), ButtonPosition + FVector2D(22.0f * Scale, 10.0f * Scale), ButtonSize.X - (44.0f * Scale), CardTitleFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::Format(FText::FromString(TEXT("Selected {0}: {1}. Click a card or use Left/Right, then continue.")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (26.0f * Scale)), ContentWidth, LabelFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); -} - -void UAgrarianMvpFrontendWidget::DrawJoinServer( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const -{ - const float ContentX = PanelPosition.X + (48.0f * Scale); - const float ContentWidth = PanelSize.X - (96.0f * Scale); - const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(34.0f * Scale)); - const FSlateFontInfo BodyFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(18.0f * Scale)); - const FSlateFontInfo AddressFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(24.0f * Scale)); - const FSlateFontInfo HintFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(15.0f * Scale)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Join MVP server")), FVector2D(ContentX, PanelPosition.Y + (44.0f * Scale)), ContentWidth, TitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::Format(FText::FromString(TEXT("{0}: {1}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FVector2D(ContentX + (2.0f * Scale), PanelPosition.Y + (94.0f * Scale)), ContentWidth, BodyFont, FLinearColor(0.72f, 0.80f, 0.68f, 1.0f)); - - const FVector2D AddressPanelPosition(ContentX, PanelPosition.Y + (164.0f * Scale)); - const FVector2D AddressPanelSize(ContentWidth, 96.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(AddressPanelSize), FSlateLayoutTransform(FVector2f(AddressPanelPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.055f, 0.065f, 0.05f, 0.96f)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Server address")), AddressPanelPosition + FVector2D(24.0f * Scale, 14.0f * Scale), ContentWidth - (48.0f * Scale), HintFont, FLinearColor(0.58f, 0.64f, 0.54f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, JoinServerAddress, AddressPanelPosition + FVector2D(24.0f * Scale, 42.0f * Scale), ContentWidth - (48.0f * Scale), AddressFont, FLinearColor(0.86f, 0.94f, 0.78f, 1.0f)); - - const FVector2D ButtonPosition(ContentX, PanelPosition.Y + (300.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(ContentWidth, 330.0f * Scale), 58.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(ButtonSize), FSlateLayoutTransform(FVector2f(ButtonPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.35f, 0.58f, 0.30f, 0.95f)); - - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Continue to loading")), ButtonPosition + FVector2D(22.0f * Scale, 12.0f * Scale), ButtonSize.X - (44.0f * Scale), AddressFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f)); - const FVector2D BackButtonPosition(ButtonPosition.X + ButtonSize.X + (16.0f * Scale), ButtonPosition.Y); - const FVector2D BackButtonSize(FMath::Min(ContentWidth - ButtonSize.X - (16.0f * Scale), 180.0f * Scale), ButtonSize.Y); - if (BackButtonSize.X > 96.0f) - { - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(BackButtonSize), FSlateLayoutTransform(FVector2f(BackButtonPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.12f, 0.15f, 0.11f, 0.95f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Back")), BackButtonPosition + FVector2D(22.0f * Scale, 12.0f * Scale), BackButtonSize.X - (44.0f * Scale), AddressFont, FLinearColor(0.82f, 0.88f, 0.76f, 1.0f)); + PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Continue")), ButtonColor, ButtonHoverColor, 10.0f * Scale); + PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); + PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); + AddText(Panel, FText::Format(FText::FromString(TEXT("Selected {0}: {1}. Click a card or use Left/Right, then continue.")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f); + return; } - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Click continue, press Enter, or press Backspace/Escape to revise your pioneer.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); + + if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer) + { + AddText(Panel, FText::FromString(TEXT("Join MVP server")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 8.0f * Scale); + AddText(Panel, FText::Format(FText::FromString(TEXT("{0}: {1}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 36.0f * Scale); + AddText(Panel, FText::FromString(TEXT("Server address")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 4.0f * Scale); + AddText(Panel, JoinServerAddress, FMath::RoundToInt(24.0f * Scale), true, TextColor, 42.0f * Scale); + + UHorizontalBox* ButtonRow = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("JoinButtonRow")); + if (UVerticalBoxSlot* RowSlot = Panel->AddChildToVerticalBox(ButtonRow)) + { + RowSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 18.0f * Scale)); + } + + PrimaryFocusButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("ContinueToLoadingButton")); + PrimaryFocusButton->SetStyle(MakeAgrarianButtonStyle(ButtonColor, ButtonHoverColor)); + PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); + PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); + UTextBlock* ContinueLabel = AddText(nullptr, FText::FromString(TEXT("Continue to loading")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f); + PrimaryFocusButton->SetContent(ContinueLabel); + if (UButtonSlot* ContinueLabelSlot = Cast(ContinueLabel->Slot)) + { + ContinueLabelSlot->SetPadding(FMargin(22.0f, 12.0f)); + ContinueLabelSlot->SetHorizontalAlignment(HAlign_Center); + } + if (UHorizontalBoxSlot* ContinueSlot = ButtonRow->AddChildToHorizontalBox(PrimaryFocusButton)) + { + ContinueSlot->SetPadding(FMargin(0.0f, 0.0f, 12.0f * Scale, 0.0f)); + ContinueSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } + + UButton* BackButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("BackButton")); + BackButton->SetStyle(MakeAgrarianButtonStyle(SecondaryButtonColor, ButtonHoverColor)); + BackButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleBackClicked); + UTextBlock* BackLabel = AddText(nullptr, FText::FromString(TEXT("Back")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f); + BackButton->SetContent(BackLabel); + if (UButtonSlot* BackLabelSlot = Cast(BackLabel->Slot)) + { + BackLabelSlot->SetPadding(FMargin(22.0f, 12.0f)); + BackLabelSlot->SetHorizontalAlignment(HAlign_Center); + } + if (UHorizontalBoxSlot* BackSlot = ButtonRow->AddChildToHorizontalBox(BackButton)) + { + BackSlot->SetPadding(FMargin(12.0f * Scale, 0.0f, 0.0f, 0.0f)); + BackSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } + + AddText(Panel, FText::FromString(TEXT("Click continue, press Enter, or press Backspace/Escape to revise your pioneer.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f); + return; + } + + AddText(Panel, FText::FromString(TEXT("Preparing Ground Zero")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 8.0f * Scale); + AddText(Panel, FText::FromString(TEXT("Loading terrain, weather, survival state, and server session data.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 70.0f * Scale); + AddText(Panel, FText::Format(FText::FromString(TEXT("{0}: {1} | Server: {2}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel(), JoinServerAddress), FMath::RoundToInt(18.0f * Scale), false, TextColor, 34.0f * Scale); + PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 14.0f * Scale); + PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); + PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); + AddText(Panel, FText::FromString(TEXT("Click or press Enter to close the MVP menu and begin testing.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f); } -void UAgrarianMvpFrontendWidget::DrawLoading( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const +UTextBlock* UAgrarianMvpFrontendWidget::AddText(UVerticalBox* Parent, const FText& Text, int32 FontSize, bool bBold, const FLinearColor& Color, float BottomPadding) { - const float ContentX = PanelPosition.X + (48.0f * Scale); - const float ContentWidth = PanelSize.X - (96.0f * Scale); - const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", FMath::RoundToInt(34.0f * Scale)); - const FSlateFontInfo BodyFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(18.0f * Scale)); - const FSlateFontInfo HintFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(15.0f * Scale)); + UTextBlock* TextBlock = WidgetTree->ConstructWidget(UTextBlock::StaticClass()); + TextBlock->SetText(Text); + TextBlock->SetFont(FCoreStyle::GetDefaultFontStyle(bBold ? "Bold" : "Regular", FontSize)); + TextBlock->SetColorAndOpacity(FSlateColor(Color)); + TextBlock->SetAutoWrapText(true); + TextBlock->SetJustification(ETextJustify::Left); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Preparing Ground Zero")), FVector2D(ContentX, PanelPosition.Y + (58.0f * Scale)), ContentWidth, TitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Loading terrain, weather, survival state, and server session data.")), FVector2D(ContentX + (2.0f * Scale), PanelPosition.Y + (108.0f * Scale)), ContentWidth, BodyFont, FLinearColor(0.72f, 0.80f, 0.68f, 1.0f)); + if (Parent) + { + if (UVerticalBoxSlot* TextSlot = Parent->AddChildToVerticalBox(TextBlock)) + { + TextSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, BottomPadding)); + } + } - const FVector2D BarPosition(ContentX, PanelPosition.Y + (214.0f * Scale)); - const FVector2D BarSize(ContentWidth, 18.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(BarSize), FSlateLayoutTransform(FVector2f(BarPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.065f, 0.075f, 0.06f, 1.0f)); + return TextBlock; +} - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(BarSize.X * 0.62f, BarSize.Y), FSlateLayoutTransform(FVector2f(BarPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.45f, 0.72f, 0.40f, 1.0f)); +UButton* UAgrarianMvpFrontendWidget::AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding) +{ + UButton* Button = WidgetTree->ConstructWidget(UButton::StaticClass()); + Button->SetStyle(MakeAgrarianButtonStyle(NormalColor, HoveredColor)); + Button->SetClickMethod(EButtonClickMethod::MouseDown); + Button->SetTouchMethod(EButtonTouchMethod::Down); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::Format(FText::FromString(TEXT("{0}: {1} | Server: {2}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel(), JoinServerAddress), FVector2D(ContentX, PanelPosition.Y + (270.0f * Scale)), ContentWidth, BodyFont, FLinearColor(0.78f, 0.84f, 0.72f, 1.0f)); + if (!Text.IsEmpty()) + { + UTextBlock* Label = AddText(nullptr, Text, FMath::RoundToInt(20.0f * FMath::Clamp(UiScale, 0.75f, 1.5f)), true, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f), 0.0f); + Button->SetContent(Label); + if (UButtonSlot* LabelSlot = Cast(Label->Slot)) + { + LabelSlot->SetPadding(FMargin(22.0f, 12.0f)); + LabelSlot->SetHorizontalAlignment(HAlign_Center); + } + } - const FVector2D ButtonPosition(ContentX, PanelPosition.Y + (318.0f * Scale)); - const FVector2D ButtonSize(FMath::Min(ContentWidth, 310.0f * Scale), 56.0f * Scale); - FSlateDrawElement::MakeBox( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(ButtonSize), FSlateLayoutTransform(FVector2f(ButtonPosition))), - FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), - ESlateDrawEffect::None, - FLinearColor(0.35f, 0.58f, 0.30f, 0.95f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Enter Ground Zero")), ButtonPosition + FVector2D(22.0f * Scale, 11.0f * Scale), ButtonSize.X - (44.0f * Scale), BodyFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f)); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Click or press Enter to close the MVP menu and begin testing.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); + if (Parent) + { + if (UVerticalBoxSlot* ButtonSlot = Parent->AddChildToVerticalBox(Button)) + { + ButtonSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, BottomPadding)); + ButtonSlot->SetHorizontalAlignment(HAlign_Left); + } + } + + return Button; +} + +void UAgrarianMvpFrontendWidget::FocusPrimaryButton() +{ + if (PrimaryFocusButton) + { + PrimaryFocusButton->SetKeyboardFocus(); + } + else + { + SetKeyboardFocus(); + } +} + +void UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked() +{ + ConfirmActiveScreen(); +} + +void UAgrarianMvpFrontendWidget::HandleBackClicked() +{ + BackFromActiveScreen(); +} + +void UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked() +{ + SaveAndQuit(); +} + +void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked() +{ + SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); +} + +void UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked() +{ + SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); } FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const @@ -616,23 +466,3 @@ FText UAgrarianMvpFrontendWidget::GetSelectedRoleLabel() const { return FText::FromString(TEXT("Pioneer")); } - -void UAgrarianMvpFrontendWidget::DrawTextAt( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FText& Text, - const FVector2D& Position, - float Width, - const FSlateFontInfo& Font, - const FLinearColor& Color) const -{ - FSlateDrawElement::MakeText( - OutDrawElements, - ++LayerId, - AllottedGeometry.ToPaintGeometry(FVector2f(FMath::Max(96.0f, Width), Font.Size + 18.0f), FSlateLayoutTransform(FVector2f(Position))), - Text, - Font, - ESlateDrawEffect::None, - Color); -} diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 13f1c21..200b650 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -6,6 +6,10 @@ #include "Blueprint/UserWidget.h" #include "AgrarianMvpFrontendWidget.generated.h" +class UButton; +class UTextBlock; +class UVerticalBox; + UENUM(BlueprintType) enum class EAgrarianMvpFrontendScreen : uint8 { @@ -77,66 +81,36 @@ protected: virtual void NativeConstruct() override; virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override; - virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; - - virtual int32 NativePaint( - const FPaintArgs& Args, - const FGeometry& AllottedGeometry, - const FSlateRect& MyCullingRect, - FSlateWindowElementList& OutDrawElements, - int32 LayerId, - const FWidgetStyle& InWidgetStyle, - bool bParentEnabled) const override; private: - bool GetPanelLayout(const FVector2D& WidgetSize, float& OutScale, FVector2D& OutPanelPosition, FVector2D& OutPanelSize) const; - bool IsPointInside(const FVector2D& Point, const FVector2D& Position, const FVector2D& Size) const; void ContinueFromActiveScreen(); void ReturnFromActiveScreen(); void CompleteFrontendFlow(); + void RebuildFrontendTree(); + UTextBlock* AddText(UVerticalBox* Parent, const FText& Text, int32 FontSize, bool bBold, const FLinearColor& Color, float BottomPadding); + UButton* AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding); - void DrawMainMenu( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const; + UFUNCTION() + void FocusPrimaryButton(); - void DrawCharacterSelection( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const; + UFUNCTION() + void HandlePrimaryActionClicked(); - void DrawJoinServer( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const; + UFUNCTION() + void HandleBackClicked(); - void DrawLoading( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FVector2D& PanelPosition, - const FVector2D& PanelSize, - float Scale) const; + UFUNCTION() + void HandleSaveAndQuitClicked(); + + UFUNCTION() + void HandleMaleCharacterClicked(); + + UFUNCTION() + void HandleFemaleCharacterClicked(); + + UPROPERTY() + TObjectPtr PrimaryFocusButton; FText GetSelectedCharacterLabel() const; FText GetSelectedRoleLabel() const; - - void DrawTextAt( - FSlateWindowElementList& OutDrawElements, - int32& LayerId, - const FGeometry& AllottedGeometry, - const FText& Text, - const FVector2D& Position, - float Width, - const FSlateFontInfo& Font, - const FLinearColor& Color) const; };