From 4aec203ba6fa9dae34361635d1668d83b4fc5564 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 20:48:37 -0700 Subject: [PATCH] Add MVP main menu placeholder --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- Scripts/verify_mvp_main_menu_placeholder.py | 59 +++++++++ .../AgrarianGamePlayerController.cpp | 16 +++ .../AgrarianGamePlayerController.h | 7 ++ .../AgrarianMvpFrontendWidget.cpp | 117 ++++++++++++++++++ .../AgrarianGame/AgrarianMvpFrontendWidget.h | 73 +++++++++++ 6 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_mvp_main_menu_placeholder.py create mode 100644 Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp create mode 100644 Source/AgrarianGame/AgrarianMvpFrontendWidget.h diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index c5c815a..8cc363a 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -793,7 +793,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe ## 0.1.N MVP UI And UX -- [ ] Add main menu placeholder. +- [x] Add main menu placeholder. Added native `UAgrarianMvpFrontendWidget` with a scalable painted main menu panel and local player-controller spawning so the MVP frontend has an owned placeholder instead of relying on template UI. - [ ] After splash/startup screens, land on an MVP character selection landing page. - [ ] Let players choose a realistic young adult male or female character with average proportions for the MVP. - [ ] Add join server screen. diff --git a/Scripts/verify_mvp_main_menu_placeholder.py b/Scripts/verify_mvp_main_menu_placeholder.py new file mode 100644 index 0000000..0064f29 --- /dev/null +++ b/Scripts/verify_mvp_main_menu_placeholder.py @@ -0,0 +1,59 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianMvpFrontendWidget.h": ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.h", + "AgrarianMvpFrontendWidget.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp", + "AgrarianGamePlayerController.h": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h", + "AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", + "AGRARIAN_DEVELOPMENT_ROADMAP.md": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md", +} + +EXPECTED = { + "AgrarianMvpFrontendWidget.h": [ + "UAgrarianMvpFrontendWidget", + "EAgrarianMvpFrontendScreen", + "MainMenu", + "MainMenuTitle", + "MVP investor build", + "PrimaryActionLabel", + "UiScale", + ], + "AgrarianMvpFrontendWidget.cpp": [ + "DrawMainMenu", + "Placeholder flow", + "FMath::Clamp(UiScale", + ], + "AgrarianGamePlayerController.h": [ + "MvpFrontendWidgetClass", + "MvpFrontendWidget", + ], + "AgrarianGamePlayerController.cpp": [ + "UAgrarianMvpFrontendWidget::StaticClass()", + "EAgrarianMvpFrontendScreen::MainMenu", + "AddToPlayerScreen(10)", + ], + "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add main menu placeholder.", + "UAgrarianMvpFrontendWidget", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("MVP main menu placeholder verification failed: " + "; ".join(missing)) + + print("Agrarian MVP main menu placeholder verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index e59fc14..459ca17 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -7,6 +7,7 @@ #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AgrarianItemPickup.h" +#include "AgrarianMvpFrontendWidget.h" #include "AgrarianPersistenceSubsystem.h" #include "AgrarianShelterActor.h" #include "AgrarianSurvivalComponent.h" @@ -77,6 +78,21 @@ void AAgrarianGamePlayerController::BeginPlay() { Super::BeginPlay(); + if (IsLocalPlayerController()) + { + if (!MvpFrontendWidgetClass) + { + MvpFrontendWidgetClass = UAgrarianMvpFrontendWidget::StaticClass(); + } + + MvpFrontendWidget = CreateWidget(this, MvpFrontendWidgetClass); + if (MvpFrontendWidget) + { + MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu); + MvpFrontendWidget->AddToPlayerScreen(10); + } + } + // only spawn touch controls on local player controllers if (ShouldUseTouchControls() && IsLocalPlayerController()) { diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index 27e8b4f..2c8cadf 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -8,6 +8,7 @@ class UInputMappingContext; class UUserWidget; +class UAgrarianMvpFrontendWidget; class AAgrarianShelterActor; /** @@ -37,6 +38,12 @@ protected: UPROPERTY() TObjectPtr MobileControlsWidget; + UPROPERTY(EditAnywhere, Category = "Agrarian|MVP UI") + TSubclassOf MvpFrontendWidgetClass; + + UPROPERTY() + TObjectPtr MvpFrontendWidget; + /** If true, the player will use UMG touch controls even if not playing on mobile platforms */ UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls") bool bForceTouchControls = false; diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp new file mode 100644 index 0000000..93834cc --- /dev/null +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -0,0 +1,117 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianMvpFrontendWidget.h" + +#include "Rendering/DrawElements.h" +#include "Styling/CoreStyle.h" + +void UAgrarianMvpFrontendWidget::SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen) +{ + ActiveScreen = NewScreen; + InvalidateLayoutAndVolatility(); +} + +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(); + const float Scale = FMath::Clamp(UiScale, 0.75f, 1.5f); + + FSlateDrawElement::MakeBox( + OutDrawElements, + ++LayerId, + AllottedGeometry.ToPaintGeometry(FVector2f(Size), FSlateLayoutTransform(FVector2f::ZeroVector)), + FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), + ESlateDrawEffect::None, + FLinearColor(0.015f, 0.018f, 0.014f, 0.92f)); + + const FVector2D PanelSize( + FMath::Min(Size.X - 48.0f, 780.0f * Scale), + FMath::Min(Size.Y - 48.0f, 430.0f * Scale)); + const FVector2D PanelPosition( + (Size.X - PanelSize.X) * 0.5f, + (Size.Y - PanelSize.Y) * 0.5f); + + FSlateDrawElement::MakeBox( + OutDrawElements, + ++LayerId, + AllottedGeometry.ToPaintGeometry(FVector2f(PanelSize), FSlateLayoutTransform(FVector2f(PanelPosition))), + FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), + ESlateDrawEffect::None, + FLinearColor(0.035f, 0.045f, 0.034f, 0.96f)); + + FSlateDrawElement::MakeBox( + OutDrawElements, + ++LayerId, + AllottedGeometry.ToPaintGeometry(FVector2f(PanelSize.X, 4.0f * Scale), FSlateLayoutTransform(FVector2f(PanelPosition))), + FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), + ESlateDrawEffect::None, + FLinearColor(0.45f, 0.72f, 0.40f, 1.0f)); + + if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu) + { + DrawMainMenu(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); + } + + return LayerId; +} + +void UAgrarianMvpFrontendWidget::DrawMainMenu( + 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(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, MainMenuSubtitle, 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 + (230.0f * Scale)); + const FVector2D ButtonSize(FMath::Min(ContentWidth, 280.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, PrimaryActionLabel, ButtonPosition + FVector2D(22.0f * Scale, 12.0f * Scale), ButtonSize.X - (44.0f * Scale), ButtonFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f)); + DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Placeholder flow: splash, startup credits, character selection, join, loading, then Ground Zero.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (66.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); +} + +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 new file mode 100644 index 0000000..69f71a3 --- /dev/null +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -0,0 +1,73 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "AgrarianMvpFrontendWidget.generated.h" + +UENUM(BlueprintType) +enum class EAgrarianMvpFrontendScreen : uint8 +{ + MainMenu, + CharacterSelection, + JoinServer, + Loading +}; + +UCLASS() +class AGRARIANGAME_API UAgrarianMvpFrontendWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") + EAgrarianMvpFrontendScreen ActiveScreen = EAgrarianMvpFrontendScreen::MainMenu; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") + FText MainMenuTitle = FText::FromString(TEXT("Agrarian")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") + FText MainMenuSubtitle = FText::FromString(TEXT("MVP investor build")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") + FText PrimaryActionLabel = FText::FromString(TEXT("Begin")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") + FText JoinServerAddress = FText::FromString(TEXT("play.agrariangame.com:7777")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI", meta = (ClampMin = "0.75", ClampMax = "1.5")) + float UiScale = 1.0f; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI") + void SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen); + +protected: + virtual int32 NativePaint( + const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyCullingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled) const override; + +private: + void DrawMainMenu( + FSlateWindowElementList& OutDrawElements, + int32& LayerId, + const FGeometry& AllottedGeometry, + const FVector2D& PanelPosition, + const FVector2D& PanelSize, + float Scale) 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; +};