diff --git a/Source/VocationLife/Private/VocationMainMenuWidget.cpp b/Source/VocationLife/Private/VocationMainMenuWidget.cpp index 44f78a8..086562e 100644 --- a/Source/VocationLife/Private/VocationMainMenuWidget.cpp +++ b/Source/VocationLife/Private/VocationMainMenuWidget.cpp @@ -3,23 +3,32 @@ #include "VocationMainMenuWidget.h" #include "VocationSessionSubsystem.h" #include "VocationGameInstance.h" +#include "VocationGraphicsLibrary.h" #include "Components/Button.h" #include "Components/ComboBoxString.h" #include "Components/EditableTextBox.h" #include "Components/TextBlock.h" #include "Components/VerticalBox.h" -#include "Components/HorizontalBox.h" #include "Components/VerticalBoxSlot.h" +#include "Components/WidgetSwitcher.h" +#include "Components/Border.h" +#include "Components/SizeBox.h" +#include "Components/Overlay.h" +#include "Components/OverlaySlot.h" +#include "Components/Spacer.h" #include "Blueprint/WidgetTree.h" #include "Engine/GameInstance.h" +#include "Kismet/KismetSystemLibrary.h" namespace { -UTextBlock* MakeLabel(UWidgetTree* Tree, const FName& Name, const FString& Text) +void AddPadded(UVerticalBox* Box, UWidget* Child, float PadY = 6.f) { - UTextBlock* Label = Tree->ConstructWidget(UTextBlock::StaticClass(), Name); - Label->SetText(FText::FromString(Text)); - return Label; + if (UVerticalBoxSlot* Slot = Box->AddChildToVerticalBox(Child)) + { + Slot->SetPadding(FMargin(0.f, PadY)); + Slot->SetHorizontalAlignment(HAlign_Fill); + } } } @@ -28,6 +37,7 @@ void UVocationMainMenuWidget::NativeConstruct() Super::NativeConstruct(); BuildUI(); RefreshMaps(); + ShowPage(EMenuPage::Main); if (UGameInstance* GI = GetGameInstance()) { @@ -35,9 +45,7 @@ void UVocationMainMenuWidget::NativeConstruct() { Sessions->OnSessionSearchDetailed.AddDynamic(this, &UVocationMainMenuWidget::OnSessionSearchDetailed); Sessions->OnSessionJoined.AddDynamic(this, &UVocationMainMenuWidget::OnSessionJoined); - SetStatus(FString::Printf(TEXT("Online subsystem: %s | Steam: %s"), - *Sessions->GetActiveSubsystemName(), - Sessions->IsSteamAvailable() ? TEXT("yes") : TEXT("no"))); + SetStatus(FString::Printf(TEXT("Steam: %s"), Sessions->IsSteamAvailable() ? TEXT("ready") : TEXT("offline / LAN"))); } } } @@ -55,6 +63,60 @@ void UVocationMainMenuWidget::NativeDestruct() Super::NativeDestruct(); } +UTextBlock* UVocationMainMenuWidget::MakeLabel(const FName& Name, const FString& Text, float Size) const +{ + UTextBlock* Label = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), Name); + Label->SetText(FText::FromString(Text)); + FSlateFontInfo Font = Label->GetFont(); + Font.Size = Size; + Label->SetFont(Font); + Label->SetJustification(ETextJustify::Center); + Label->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.f))); + return Label; +} + +void UVocationMainMenuWidget::StyleMenuButton(UButton* Button) const +{ + if (!Button) + { + return; + } + + FButtonStyle Style = Button->GetStyle(); + FSlateBrush Normal; + Normal.TintColor = FSlateColor(FLinearColor(0.28f, 0.28f, 0.28f, 0.95f)); + Normal.DrawAs = ESlateBrushDrawType::Box; + Style.SetNormal(Normal); + + FSlateBrush Hovered = Normal; + Hovered.TintColor = FSlateColor(FLinearColor(0.45f, 0.45f, 0.2f, 1.f)); + Style.SetHovered(Hovered); + + FSlateBrush Pressed = Normal; + Pressed.TintColor = FSlateColor(FLinearColor(0.2f, 0.2f, 0.2f, 1.f)); + Style.SetPressed(Pressed); + + Button->SetStyle(Style); +} + +UButton* UVocationMainMenuWidget::MakeMenuButton(const FName& Name, const FString& Label, void (UVocationMainMenuWidget::*Callback)()) +{ + UButton* Button = WidgetTree->ConstructWidget(UButton::StaticClass(), Name); + StyleMenuButton(Button); + + USizeBox* Size = WidgetTree->ConstructWidget(USizeBox::StaticClass(), *FString::Printf(TEXT("%s_Size"), *Name.ToString())); + Size->SetWidthOverride(400.f); + Size->SetHeightOverride(44.f); + Size->SetContent(Button); + + UTextBlock* Text = MakeLabel(*FString::Printf(TEXT("%s_Label"), *Name.ToString()), Label, 18.f); + Button->SetContent(Text); + + // Bind via a thin lambda isn't possible with AddDynamic — callers bind after. + (void)Callback; + return Button; +} + void UVocationMainMenuWidget::BuildUI() { if (!WidgetTree) @@ -62,117 +124,313 @@ void UVocationMainMenuWidget::BuildUI() return; } - UVerticalBox* Column = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("Column")); - WidgetTree->RootWidget = Column; + UBorder* Root = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("RootBorder")); + Root->SetBrushColor(FLinearColor(0.05f, 0.08f, 0.12f, 0.88f)); + Root->SetPadding(FMargin(24.f)); + WidgetTree->RootWidget = Root; - auto AddToColumn = [Column](UWidget* Child) + UOverlay* Overlay = WidgetTree->ConstructWidget(UOverlay::StaticClass(), TEXT("RootOverlay")); + Root->SetContent(Overlay); + + UVerticalBox* CenterColumn = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("CenterColumn")); + if (UOverlaySlot* OverlaySlot = Overlay->AddChildToOverlay(CenterColumn)) { - if (UVerticalBoxSlot* Slot = Column->AddChildToVerticalBox(Child)) - { - Slot->SetPadding(FMargin(12.f, 6.f)); - } + OverlaySlot->SetHorizontalAlignment(HAlign_Center); + OverlaySlot->SetVerticalAlignment(VAlign_Center); + } + + TitleText = MakeLabel(TEXT("Title"), TEXT("VocationLife"), 56.f); + AddPadded(CenterColumn, TitleText, 4.f); + + SplashText = MakeLabel(TEXT("Splash"), TEXT("A life of many vocations!"), 16.f); + SplashText->SetColorAndOpacity(FSlateColor(FLinearColor(1.f, 0.9f, 0.2f, 1.f))); + AddPadded(CenterColumn, SplashText, 2.f); + + USpacer* Spacer = WidgetTree->ConstructWidget(USpacer::StaticClass(), TEXT("TitleSpacer")); + Spacer->SetSize(FVector2D(1.f, 28.f)); + AddPadded(CenterColumn, Spacer, 0.f); + + PageSwitcher = WidgetTree->ConstructWidget(UWidgetSwitcher::StaticClass(), TEXT("PageSwitcher")); + AddPadded(CenterColumn, PageSwitcher, 8.f); + + // ---- Main page ---- + UVerticalBox* MainPage = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("MainPage")); + PageSwitcher->AddChild(MainPage); + + UButton* BtnSingleplayer = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("BtnSingleplayer")); + StyleMenuButton(BtnSingleplayer); + { + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("BtnSingleplayerSize")); + S->SetWidthOverride(400.f); + S->SetHeightOverride(46.f); + S->SetContent(BtnSingleplayer); + BtnSingleplayer->SetContent(MakeLabel(TEXT("BtnSingleplayerLbl"), TEXT("Singleplayer"), 18.f)); + BtnSingleplayer->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnSingleplayerClicked); + AddPadded(MainPage, S, 5.f); + } + + UButton* BtnMultiplayer = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("BtnMultiplayer")); + StyleMenuButton(BtnMultiplayer); + { + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("BtnMultiplayerSize")); + S->SetWidthOverride(400.f); + S->SetHeightOverride(46.f); + S->SetContent(BtnMultiplayer); + BtnMultiplayer->SetContent(MakeLabel(TEXT("BtnMultiplayerLbl"), TEXT("Multiplayer"), 18.f)); + BtnMultiplayer->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnMultiplayerClicked); + AddPadded(MainPage, S, 5.f); + } + + UButton* BtnOptions = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("BtnOptions")); + StyleMenuButton(BtnOptions); + { + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("BtnOptionsSize")); + S->SetWidthOverride(400.f); + S->SetHeightOverride(46.f); + S->SetContent(BtnOptions); + BtnOptions->SetContent(MakeLabel(TEXT("BtnOptionsLbl"), TEXT("Options..."), 18.f)); + BtnOptions->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnOptionsClicked); + AddPadded(MainPage, S, 5.f); + } + + UButton* BtnQuit = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("BtnQuit")); + StyleMenuButton(BtnQuit); + { + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("BtnQuitSize")); + S->SetWidthOverride(400.f); + S->SetHeightOverride(46.f); + S->SetContent(BtnQuit); + BtnQuit->SetContent(MakeLabel(TEXT("BtnQuitLbl"), TEXT("Quit Game"), 18.f)); + BtnQuit->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnQuitClicked); + AddPadded(MainPage, S, 5.f); + } + + // ---- Singleplayer page ---- + UVerticalBox* SoloPage = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("SoloPage")); + PageSwitcher->AddChild(SoloPage); + AddPadded(SoloPage, MakeLabel(TEXT("SoloTitle"), TEXT("Singleplayer"), 28.f), 4.f); + AddPadded(SoloPage, MakeLabel(TEXT("SoloMapLbl"), TEXT("Map"), 14.f), 2.f); + MapCombo = WidgetTree->ConstructWidget(UComboBoxString::StaticClass(), TEXT("MapCombo")); + { + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("MapComboSize")); + S->SetWidthOverride(400.f); + S->SetContent(MapCombo); + AddPadded(SoloPage, S, 4.f); + } + { + UButton* Play = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("PlaySoloBtn")); + StyleMenuButton(Play); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("PlaySoloSize")); + S->SetWidthOverride(400.f); + S->SetHeightOverride(46.f); + S->SetContent(Play); + Play->SetContent(MakeLabel(TEXT("PlaySoloLbl"), TEXT("Play World"), 18.f)); + Play->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnPlaySoloClicked); + AddPadded(SoloPage, S, 8.f); + } + { + UButton* Back = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("SoloBackBtn")); + StyleMenuButton(Back); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("SoloBackSize")); + S->SetWidthOverride(400.f); + S->SetHeightOverride(40.f); + S->SetContent(Back); + Back->SetContent(MakeLabel(TEXT("SoloBackLbl"), TEXT("Back"), 16.f)); + Back->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnBackClicked); + AddPadded(SoloPage, S, 4.f); + } + + // ---- Multiplayer page ---- + UVerticalBox* MpPage = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("MpPage")); + PageSwitcher->AddChild(MpPage); + AddPadded(MpPage, MakeLabel(TEXT("MpTitle"), TEXT("Multiplayer"), 28.f), 4.f); + + auto AddField = [&](UVerticalBox* Page, const TCHAR* LabelName, const TCHAR* Label, UWidget* Field, const TCHAR* SizeName) + { + AddPadded(Page, MakeLabel(LabelName, Label, 13.f), 2.f); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), SizeName); + S->SetWidthOverride(400.f); + S->SetContent(Field); + AddPadded(Page, S, 2.f); }; - AddToColumn(MakeLabel(WidgetTree, TEXT("Title"), TEXT("VocationLife — Start Menu"))); + AddPadded(MpPage, MakeLabel(TEXT("MpMapLbl"), TEXT("Map"), 13.f), 2.f); + MpMapCombo = WidgetTree->ConstructWidget(UComboBoxString::StaticClass(), TEXT("MpMapCombo")); + { + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("MpMapComboSize")); + S->SetWidthOverride(400.f); + S->SetContent(MpMapCombo); + AddPadded(MpPage, S, 2.f); + } - AddToColumn(MakeLabel(WidgetTree, TEXT("MapLabel"), TEXT("Map"))); - MapCombo = WidgetTree->ConstructWidget(UComboBoxString::StaticClass(), TEXT("MapCombo")); - AddToColumn(MapCombo); - - AddToColumn(MakeLabel(WidgetTree, TEXT("BackendLabel"), TEXT("Network Backend"))); BackendCombo = WidgetTree->ConstructWidget(UComboBoxString::StaticClass(), TEXT("BackendCombo")); BackendCombo->AddOption(TEXT("Auto")); BackendCombo->AddOption(TEXT("Steam")); BackendCombo->AddOption(TEXT("LAN")); BackendCombo->SetSelectedIndex(0); - AddToColumn(BackendCombo); + AddField(MpPage, TEXT("BackendLbl"), TEXT("Network"), BackendCombo, TEXT("BackendSize")); - AddToColumn(MakeLabel(WidgetTree, TEXT("NameLabel"), TEXT("Server Name"))); ServerNameBox = WidgetTree->ConstructWidget(UEditableTextBox::StaticClass(), TEXT("ServerNameBox")); ServerNameBox->SetText(FText::FromString(TEXT("VocationLife"))); - AddToColumn(ServerNameBox); + AddField(MpPage, TEXT("NameLbl"), TEXT("Server Name"), ServerNameBox, TEXT("NameSize")); - AddToColumn(MakeLabel(WidgetTree, TEXT("PortLabel"), TEXT("Host Port"))); PortBox = WidgetTree->ConstructWidget(UEditableTextBox::StaticClass(), TEXT("PortBox")); PortBox->SetText(FText::FromString(TEXT("7777"))); - AddToColumn(PortBox); + AddField(MpPage, TEXT("PortLbl"), TEXT("Host Port"), PortBox, TEXT("PortSize")); - AddToColumn(MakeLabel(WidgetTree, TEXT("MaxLabel"), TEXT("Max Players"))); MaxPlayersBox = WidgetTree->ConstructWidget(UEditableTextBox::StaticClass(), TEXT("MaxPlayersBox")); MaxPlayersBox->SetText(FText::FromString(TEXT("4"))); - AddToColumn(MaxPlayersBox); + AddField(MpPage, TEXT("MaxLbl"), TEXT("Max Players"), MaxPlayersBox, TEXT("MaxSize")); - HostButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("HostButton")); + auto AddMpButton = [&](const TCHAR* Name, const TCHAR* Label, TFunction Bind) { - UTextBlock* L = MakeLabel(WidgetTree, TEXT("HostBtnLabel"), TEXT("Host Game")); - HostButton->SetContent(L); - } - HostButton->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnHostClicked); - AddToColumn(HostButton); + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), Name); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), *FString::Printf(TEXT("%sSize"), Name)); + S->SetWidthOverride(400.f); + S->SetHeightOverride(42.f); + S->SetContent(B); + B->SetContent(MakeLabel(*FString::Printf(TEXT("%sLbl"), Name), Label, 16.f)); + Bind(); + AddPadded(MpPage, S, 4.f); + return B; + }; - SoloButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("SoloButton")); { - UTextBlock* L = MakeLabel(WidgetTree, TEXT("SoloBtnLabel"), TEXT("Play Solo (selected map)")); - SoloButton->SetContent(L); + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("HostBtn")); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("HostBtnSize")); + S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B); + B->SetContent(MakeLabel(TEXT("HostBtnLbl"), TEXT("Host Game"), 16.f)); + B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnHostClicked); + AddPadded(MpPage, S, 4.f); + } + { + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("FindSteamBtn")); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("FindSteamBtnSize")); + S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B); + B->SetContent(MakeLabel(TEXT("FindSteamBtnLbl"), TEXT("Find Steam Games"), 16.f)); + B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnFindSteamClicked); + AddPadded(MpPage, S, 4.f); + } + { + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("FindLanBtn")); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("FindLanBtnSize")); + S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B); + B->SetContent(MakeLabel(TEXT("FindLanBtnLbl"), TEXT("Find LAN Games"), 16.f)); + B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnFindLanClicked); + AddPadded(MpPage, S, 4.f); } - SoloButton->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnSoloClicked); - AddToColumn(SoloButton); - UHorizontalBox* FindRow = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("FindRow")); - FindSteamButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("FindSteamButton")); - FindSteamButton->SetContent(MakeLabel(WidgetTree, TEXT("FindSteamLbl"), TEXT("Find Steam"))); - FindSteamButton->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnFindSteamClicked); - FindLanButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("FindLanButton")); - FindLanButton->SetContent(MakeLabel(WidgetTree, TEXT("FindLanLbl"), TEXT("Find LAN"))); - FindLanButton->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnFindLanClicked); - FindRow->AddChildToHorizontalBox(FindSteamButton); - FindRow->AddChildToHorizontalBox(FindLanButton); - AddToColumn(FindRow); - - AddToColumn(MakeLabel(WidgetTree, TEXT("SessionsLabel"), TEXT("Found Sessions"))); SessionCombo = WidgetTree->ConstructWidget(UComboBoxString::StaticClass(), TEXT("SessionCombo")); - AddToColumn(SessionCombo); + AddField(MpPage, TEXT("SessionsLbl"), TEXT("Found Sessions"), SessionCombo, TEXT("SessionSize")); + { + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("JoinBtn")); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("JoinBtnSize")); + S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B); + B->SetContent(MakeLabel(TEXT("JoinBtnLbl"), TEXT("Join Selected"), 16.f)); + B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnJoinSelectedClicked); + AddPadded(MpPage, S, 4.f); + } - JoinButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("JoinButton")); - JoinButton->SetContent(MakeLabel(WidgetTree, TEXT("JoinBtnLabel"), TEXT("Join Selected Session"))); - JoinButton->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnJoinSelectedClicked); - AddToColumn(JoinButton); - - AddToColumn(MakeLabel(WidgetTree, TEXT("IpLabel"), TEXT("Direct Connect IP"))); IpBox = WidgetTree->ConstructWidget(UEditableTextBox::StaticClass(), TEXT("IpBox")); IpBox->SetText(FText::FromString(TEXT("127.0.0.1"))); - AddToColumn(IpBox); + AddField(MpPage, TEXT("IpLbl"), TEXT("Direct Connect IP"), IpBox, TEXT("IpSize")); - AddToColumn(MakeLabel(WidgetTree, TEXT("JoinPortLabel"), TEXT("Direct Connect Port"))); JoinPortBox = WidgetTree->ConstructWidget(UEditableTextBox::StaticClass(), TEXT("JoinPortBox")); JoinPortBox->SetText(FText::FromString(TEXT("7777"))); - AddToColumn(JoinPortBox); + AddField(MpPage, TEXT("JoinPortLbl"), TEXT("Port"), JoinPortBox, TEXT("JoinPortSize")); + { + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("ConnectBtn")); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("ConnectBtnSize")); + S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B); + B->SetContent(MakeLabel(TEXT("ConnectBtnLbl"), TEXT("Connect by IP:Port"), 16.f)); + B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnConnectIpClicked); + AddPadded(MpPage, S, 4.f); + } + { + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("MpBackBtn")); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("MpBackBtnSize")); + S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B); + B->SetContent(MakeLabel(TEXT("MpBackBtnLbl"), TEXT("Back"), 16.f)); + B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnBackClicked); + AddPadded(MpPage, S, 4.f); + } - ConnectIpButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("ConnectIpButton")); - ConnectIpButton->SetContent(MakeLabel(WidgetTree, TEXT("ConnectIpLbl"), TEXT("Connect by IP:Port"))); - ConnectIpButton->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnConnectIpClicked); - AddToColumn(ConnectIpButton); + // ---- Options page ---- + UVerticalBox* OptPage = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("OptPage")); + PageSwitcher->AddChild(OptPage); + AddPadded(OptPage, MakeLabel(TEXT("OptTitle"), TEXT("Options"), 28.f), 4.f); + AddPadded(OptPage, MakeLabel(TEXT("GfxLbl"), TEXT("Graphics Preset"), 14.f), 4.f); - StatusText = MakeLabel(WidgetTree, TEXT("Status"), TEXT("Ready")); - AddToColumn(StatusText); + auto AddGfxButton = [&](const TCHAR* Name, const TCHAR* Label) + { + UButton* B = WidgetTree->ConstructWidget(UButton::StaticClass(), Name); + StyleMenuButton(B); + USizeBox* S = WidgetTree->ConstructWidget(USizeBox::StaticClass(), *FString::Printf(TEXT("%sSize"), Name)); + S->SetWidthOverride(400.f); + S->SetHeightOverride(40.f); + S->SetContent(B); + B->SetContent(MakeLabel(*FString::Printf(TEXT("%sLbl"), Name), Label, 16.f)); + AddPadded(OptPage, S, 4.f); + return B; + }; + + AddGfxButton(TEXT("GfxLow"), TEXT("Low"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsLow); + AddGfxButton(TEXT("GfxMed"), TEXT("Medium"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsMedium); + AddGfxButton(TEXT("GfxHigh"), TEXT("High"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsHigh); + AddGfxButton(TEXT("GfxUltra"), TEXT("Ultra"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsUltra); + AddGfxButton(TEXT("GfxRT"), TEXT("Ray Tracing"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsRayTracing); + AddGfxButton(TEXT("OptBack"), TEXT("Back"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnBackClicked); + + StatusText = MakeLabel(TEXT("Status"), TEXT(""), 13.f); + StatusText->SetColorAndOpacity(FSlateColor(FLinearColor(0.75f, 0.8f, 0.85f, 1.f))); + AddPadded(CenterColumn, StatusText, 12.f); + + VersionText = MakeLabel(TEXT("Version"), TEXT("VocationLife 0.1.0"), 12.f); + VersionText->SetJustification(ETextJustify::Left); + if (UOverlaySlot* VerSlot = Overlay->AddChildToOverlay(VersionText)) + { + VerSlot->SetHorizontalAlignment(HAlign_Left); + VerSlot->SetVerticalAlignment(VAlign_Bottom); + VerSlot->SetPadding(FMargin(16.f)); + } +} + +void UVocationMainMenuWidget::ShowPage(EMenuPage Page) +{ + if (PageSwitcher) + { + PageSwitcher->SetActiveWidgetIndex(static_cast(Page)); + } } void UVocationMainMenuWidget::RefreshMaps() { CachedMaps = UVocationServerSettings::GetAvailableMaps(); - if (!MapCombo) + auto Fill = [this](UComboBoxString* Combo) { - return; - } - MapCombo->ClearOptions(); - for (const FVocationMapOption& Map : CachedMaps) - { - MapCombo->AddOption(Map.DisplayName); - } - if (CachedMaps.Num() > 0) - { - MapCombo->SetSelectedIndex(0); - } + if (!Combo) + { + return; + } + Combo->ClearOptions(); + for (const FVocationMapOption& Map : CachedMaps) + { + Combo->AddOption(Map.DisplayName); + } + if (CachedMaps.Num() > 0) + { + Combo->SetSelectedIndex(0); + } + }; + Fill(MapCombo); + Fill(MpMapCombo); } void UVocationMainMenuWidget::SetStatus(const FString& Message) @@ -189,16 +447,38 @@ void UVocationMainMenuWidget::LeaveStartMenu() { GI->MarkGameplayStarted(); } - if (APlayerController* PC = GetOwningPlayer()) { PC->bShowMouseCursor = false; PC->SetInputMode(FInputModeGameOnly()); } - RemoveFromParent(); } +void UVocationMainMenuWidget::OnSingleplayerClicked() { ShowPage(EMenuPage::Singleplayer); } +void UVocationMainMenuWidget::OnMultiplayerClicked() { ShowPage(EMenuPage::Multiplayer); } +void UVocationMainMenuWidget::OnOptionsClicked() { ShowPage(EMenuPage::Options); } +void UVocationMainMenuWidget::OnBackClicked() { ShowPage(EMenuPage::Main); } + +void UVocationMainMenuWidget::OnQuitClicked() +{ + if (APlayerController* PC = GetOwningPlayer()) + { + UKismetSystemLibrary::QuitGame(this, PC, EQuitPreference::Quit, false); + } +} + +void UVocationMainMenuWidget::OnPlaySoloClicked() +{ + if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem()) + { + const int32 MapIndex = MapCombo ? MapCombo->GetSelectedIndex() : 0; + const FString MapPath = CachedMaps.IsValidIndex(MapIndex) ? CachedMaps[MapIndex].MapPath : TEXT("/Engine/Maps/Entry"); + LeaveStartMenu(); + Sessions->TravelToMap(MapPath, false, 7777); + } +} + void UVocationMainMenuWidget::OnHostClicked() { UVocationSessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr; @@ -207,7 +487,7 @@ void UVocationMainMenuWidget::OnHostClicked() return; } - const int32 MapIndex = MapCombo ? MapCombo->GetSelectedIndex() : 0; + const int32 MapIndex = MpMapCombo ? MpMapCombo->GetSelectedIndex() : 0; const FString MapPath = CachedMaps.IsValidIndex(MapIndex) ? CachedMaps[MapIndex].MapPath : TEXT("/Engine/Maps/Entry"); const int32 Port = PortBox ? FCString::Atoi(*PortBox->GetText().ToString()) : 7777; const int32 MaxPlayers = MaxPlayersBox ? FCString::Atoi(*MaxPlayersBox->GetText().ToString()) : 4; @@ -227,7 +507,7 @@ void UVocationMainMenuWidget::OnFindSteamClicked() { if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem()) { - SetStatus(TEXT("Searching Steam sessions...")); + SetStatus(TEXT("Searching Steam...")); Sessions->FindSessions(EVocationNetBackend::Steam, false); } } @@ -236,7 +516,7 @@ void UVocationMainMenuWidget::OnFindLanClicked() { if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem()) { - SetStatus(TEXT("Searching LAN sessions...")); + SetStatus(TEXT("Searching LAN...")); Sessions->FindSessions(EVocationNetBackend::LAN, true); } } @@ -251,7 +531,7 @@ void UVocationMainMenuWidget::OnJoinSelectedClicked() SetStatus(TEXT("No session selected")); return; } - SetStatus(TEXT("Joining session...")); + SetStatus(TEXT("Joining...")); LeaveStartMenu(); Sessions->JoinSessionByIndex(Index); } @@ -263,20 +543,54 @@ void UVocationMainMenuWidget::OnConnectIpClicked() { const FString IP = IpBox ? IpBox->GetText().ToString() : TEXT("127.0.0.1"); const int32 Port = JoinPortBox ? FCString::Atoi(*JoinPortBox->GetText().ToString()) : 7777; - SetStatus(FString::Printf(TEXT("Connecting to %s:%d ..."), *IP, Port)); + SetStatus(FString::Printf(TEXT("Connecting %s:%d"), *IP, Port)); LeaveStartMenu(); Sessions->ConnectByIP(IP, Port); } } -void UVocationMainMenuWidget::OnSoloClicked() +void UVocationMainMenuWidget::OnGraphicsLow() { - if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem()) + if (UVocationGameInstance* GI = GetGameInstance()) { - const int32 MapIndex = MapCombo ? MapCombo->GetSelectedIndex() : 0; - const FString MapPath = CachedMaps.IsValidIndex(MapIndex) ? CachedMaps[MapIndex].MapPath : TEXT("/Engine/Maps/Entry"); - LeaveStartMenu(); - Sessions->TravelToMap(MapPath, false, 7777); + GI->SetGraphicsPreset(EVocationGraphicsPreset::Low, false); + SetStatus(TEXT("Graphics: Low")); + } +} + +void UVocationMainMenuWidget::OnGraphicsMedium() +{ + if (UVocationGameInstance* GI = GetGameInstance()) + { + GI->SetGraphicsPreset(EVocationGraphicsPreset::Medium, false); + SetStatus(TEXT("Graphics: Medium")); + } +} + +void UVocationMainMenuWidget::OnGraphicsHigh() +{ + if (UVocationGameInstance* GI = GetGameInstance()) + { + GI->SetGraphicsPreset(EVocationGraphicsPreset::High, false); + SetStatus(TEXT("Graphics: High")); + } +} + +void UVocationMainMenuWidget::OnGraphicsUltra() +{ + if (UVocationGameInstance* GI = GetGameInstance()) + { + GI->SetGraphicsPreset(EVocationGraphicsPreset::Ultra, false); + SetStatus(TEXT("Graphics: Ultra")); + } +} + +void UVocationMainMenuWidget::OnGraphicsRayTracing() +{ + if (UVocationGameInstance* GI = GetGameInstance()) + { + GI->SetGraphicsPreset(EVocationGraphicsPreset::RayTracing, true); + SetStatus(TEXT("Graphics: Ray Tracing")); } } @@ -290,12 +604,11 @@ void UVocationMainMenuWidget::OnSessionSearchDetailed(const TArrayClearOptions(); for (const FVocationSessionSearchEntry& Entry : Sessions) { - SessionCombo->AddOption(FString::Printf(TEXT("%s [%s] %d/%d %s"), + SessionCombo->AddOption(FString::Printf(TEXT("%s [%s] %d/%d"), *Entry.DisplayName, *Entry.MapName, Entry.MaxPlayers - Entry.OpenSlots, - Entry.MaxPlayers, - Entry.bIsLAN ? TEXT("LAN") : TEXT("Online"))); + Entry.MaxPlayers)); } if (Sessions.Num() > 0) { @@ -310,7 +623,7 @@ void UVocationMainMenuWidget::OnSessionSearchDetailed(const TArray& Sessions); - - UFUNCTION() - void OnSessionJoined(bool bSuccess); - + void ShowPage(EMenuPage Page); + UButton* MakeMenuButton(const FName& Name, const FString& Label, void (UVocationMainMenuWidget::*Callback)()); + UTextBlock* MakeLabel(const FName& Name, const FString& Text, float Size = 18.f) const; + void StyleMenuButton(UButton* Button) const; void LeaveStartMenu(); - UPROPERTY() - TObjectPtr MapCombo; + UFUNCTION() void OnSingleplayerClicked(); + UFUNCTION() void OnMultiplayerClicked(); + UFUNCTION() void OnOptionsClicked(); + UFUNCTION() void OnQuitClicked(); + UFUNCTION() void OnBackClicked(); - UPROPERTY() - TObjectPtr BackendCombo; + UFUNCTION() void OnPlaySoloClicked(); + UFUNCTION() void OnHostClicked(); + UFUNCTION() void OnFindSteamClicked(); + UFUNCTION() void OnFindLanClicked(); + UFUNCTION() void OnJoinSelectedClicked(); + UFUNCTION() void OnConnectIpClicked(); - UPROPERTY() - TObjectPtr SessionCombo; + UFUNCTION() void OnGraphicsLow(); + UFUNCTION() void OnGraphicsMedium(); + UFUNCTION() void OnGraphicsHigh(); + UFUNCTION() void OnGraphicsUltra(); + UFUNCTION() void OnGraphicsRayTracing(); - UPROPERTY() - TObjectPtr ServerNameBox; + UFUNCTION() void OnSessionSearchDetailed(const TArray& Sessions); + UFUNCTION() void OnSessionJoined(bool bSuccess); - UPROPERTY() - TObjectPtr PortBox; + UPROPERTY() TObjectPtr PageSwitcher; + UPROPERTY() TObjectPtr TitleText; + UPROPERTY() TObjectPtr SplashText; + UPROPERTY() TObjectPtr StatusText; + UPROPERTY() TObjectPtr VersionText; - UPROPERTY() - TObjectPtr IpBox; - - UPROPERTY() - TObjectPtr JoinPortBox; - - UPROPERTY() - TObjectPtr MaxPlayersBox; - - UPROPERTY() - TObjectPtr StatusText; - - UPROPERTY() - TObjectPtr HostButton; - - UPROPERTY() - TObjectPtr FindSteamButton; - - UPROPERTY() - TObjectPtr FindLanButton; - - UPROPERTY() - TObjectPtr JoinButton; - - UPROPERTY() - TObjectPtr ConnectIpButton; - - UPROPERTY() - TObjectPtr SoloButton; + UPROPERTY() TObjectPtr MapCombo; + UPROPERTY() TObjectPtr MpMapCombo; + UPROPERTY() TObjectPtr BackendCombo; + UPROPERTY() TObjectPtr SessionCombo; + UPROPERTY() TObjectPtr ServerNameBox; + UPROPERTY() TObjectPtr PortBox; + UPROPERTY() TObjectPtr IpBox; + UPROPERTY() TObjectPtr JoinPortBox; + UPROPERTY() TObjectPtr MaxPlayersBox; TArray CachedMaps; TArray CachedSessions;