From 5efd81ca4bc4dbfa14fcb2f2406fbf81542f44e5 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 20:57:08 -0700 Subject: [PATCH] Add MVP character selection landing --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- .../verify_mvp_character_selection_landing.py | 50 +++++++++++++++ .../AgrarianGamePlayerController.cpp | 2 +- .../AgrarianMvpFrontendWidget.cpp | 64 +++++++++++++++++++ .../AgrarianGame/AgrarianMvpFrontendWidget.h | 8 +++ 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 Scripts/verify_mvp_character_selection_landing.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 8cc363a..a49b676 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -794,7 +794,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe ## 0.1.N MVP UI And UX - [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. +- [x] After splash/startup screens, land on an MVP character selection landing page. The local player controller now opens the MVP frontend on `CharacterSelection`, and the native frontend widget paints a scalable character-selection landing page before gameplay. - [ ] Let players choose a realistic young adult male or female character with average proportions for the MVP. - [ ] Add join server screen. - [ ] Add loading screen. diff --git a/Scripts/verify_mvp_character_selection_landing.py b/Scripts/verify_mvp_character_selection_landing.py new file mode 100644 index 0000000..f5269c9 --- /dev/null +++ b/Scripts/verify_mvp_character_selection_landing.py @@ -0,0 +1,50 @@ +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.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", + "AGRARIAN_DEVELOPMENT_ROADMAP.md": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md", +} + +EXPECTED = { + "AgrarianMvpFrontendWidget.h": [ + "CharacterSelection", + "DrawCharacterSelection", + ], + "AgrarianMvpFrontendWidget.cpp": [ + "EAgrarianMvpFrontendScreen::CharacterSelection", + "DrawCharacterSelection", + "Choose your first settler", + "Young adult male", + "Young adult female", + "MVP placeholder landing page", + ], + "AgrarianGamePlayerController.cpp": [ + "SetActiveScreen(EAgrarianMvpFrontendScreen::CharacterSelection)", + ], + "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] After splash/startup screens, land on an MVP character selection landing page.", + "`CharacterSelection`", + ], +} + + +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 character selection landing verification failed: " + "; ".join(missing)) + + print("Agrarian MVP character selection landing verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 459ca17..7244a1c 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -88,7 +88,7 @@ void AAgrarianGamePlayerController::BeginPlay() MvpFrontendWidget = CreateWidget(this, MvpFrontendWidgetClass); if (MvpFrontendWidget) { - MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu); + MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::CharacterSelection); MvpFrontendWidget->AddToPlayerScreen(10); } } diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index 93834cc..cc84d35 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -60,6 +60,10 @@ int32 UAgrarianMvpFrontendWidget::NativePaint( { DrawMainMenu(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); } + else if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) + { + DrawCharacterSelection(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); + } return LayerId; } @@ -96,6 +100,66 @@ void UAgrarianMvpFrontendWidget::DrawMainMenu( 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::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 settler")), 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("MVP placeholder landing page before entering 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) + { + 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)); + + FSlateDrawElement::MakeBox( + OutDrawElements, + ++LayerId, + AllottedGeometry.ToPaintGeometry(FVector2f(CardSize.X, 3.0f * Scale), FSlateLayoutTransform(FVector2f(CardPosition))), + FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), + ESlateDrawEffect::None, + AccentColor); + + 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)); + + 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)); + }; + + 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)); + 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)); + + DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Selection is visual-only for this milestone; both choices enter the same MVP survival baseline.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.0f * Scale)), ContentWidth, LabelFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); +} + void UAgrarianMvpFrontendWidget::DrawTextAt( FSlateWindowElementList& OutDrawElements, int32& LayerId, diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 69f71a3..64cc41f 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -61,6 +61,14 @@ private: const FVector2D& PanelSize, float Scale) const; + void DrawCharacterSelection( + FSlateWindowElementList& OutDrawElements, + int32& LayerId, + const FGeometry& AllottedGeometry, + const FVector2D& PanelPosition, + const FVector2D& PanelSize, + float Scale) const; + void DrawTextAt( FSlateWindowElementList& OutDrawElements, int32& LayerId,