diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index f2cc6cf..3bb7ec9 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -796,7 +796,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [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. - [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. - [x] Let players choose a realistic young adult male or female character with average proportions for the MVP. Added a selectable MVP character archetype enum, visible selected state, keyboard selection, and `AgrarianSelectCharacter male|female` dev command while keeping both choices on the same MVP survival baseline until real character art arrives. -- [ ] Add join server screen. +- [x] Add join server screen. Added a native `JoinServer` frontend screen showing the selected character and `play.agrariangame.com:7777`, with keyboard flow from character selection and a dev command to display the screen directly. - [ ] Add loading screen. - [~] Add HUD. - [ ] Add inventory UI. diff --git a/Scripts/verify_mvp_join_server_screen.py b/Scripts/verify_mvp_join_server_screen.py new file mode 100644 index 0000000..f797a52 --- /dev/null +++ b/Scripts/verify_mvp_join_server_screen.py @@ -0,0 +1,57 @@ +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": [ + "JoinServer", + "JoinServerAddress", + "play.agrariangame.com:7777", + "DrawJoinServer", + ], + "AgrarianMvpFrontendWidget.cpp": [ + "EAgrarianMvpFrontendScreen::JoinServer", + "SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer)", + "Join MVP server", + "Continue to loading", + ], + "AgrarianGamePlayerController.h": [ + "AgrarianShowMvpScreen", + ], + "AgrarianGamePlayerController.cpp": [ + "AgrarianShowMvpScreen", + "SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer)", + "Usage: AgrarianShowMvpScreen main|character|join", + ], + "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add join server screen.", + "`JoinServer`", + "play.agrariangame.com:7777", + ], +} + + +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 join server screen verification failed: " + "; ".join(missing)) + + print("Agrarian MVP join server screen verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 5898357..f8987ab 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -305,6 +305,38 @@ void AAgrarianGamePlayerController::AgrarianSelectCharacter(FName Archetype) ClientMessage(TEXT("Usage: AgrarianSelectCharacter male|female")); } +void AAgrarianGamePlayerController::AgrarianShowMvpScreen(FName ScreenName) +{ + if (!MvpFrontendWidget) + { + ClientMessage(TEXT("No MVP frontend widget is active.")); + return; + } + + if (ScreenName == TEXT("main") || ScreenName == TEXT("MainMenu")) + { + MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu); + ClientMessage(TEXT("MVP frontend screen: main menu.")); + return; + } + + if (ScreenName == TEXT("character") || ScreenName == TEXT("CharacterSelection")) + { + MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::CharacterSelection); + ClientMessage(TEXT("MVP frontend screen: character selection.")); + return; + } + + if (ScreenName == TEXT("join") || ScreenName == TEXT("JoinServer")) + { + MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer); + ClientMessage(TEXT("MVP frontend screen: join server.")); + return; + } + + ClientMessage(TEXT("Usage: AgrarianShowMvpScreen main|character|join")); +} + void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) { ServerAgrarianTravel(FVector(X, Y, Z)); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index 64c8d77..d52a722 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -94,6 +94,9 @@ public: UFUNCTION(Exec) void AgrarianSelectCharacter(FName Archetype); + UFUNCTION(Exec) + void AgrarianShowMvpScreen(FName ScreenName); + UFUNCTION(Exec) void AgrarianTravel(float X, float Y, float Z); diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index e784799..b312c8e 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -29,6 +29,21 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry, SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); return FReply::Handled(); } + + if (Key == EKeys::Enter || Key == EKeys::SpaceBar) + { + SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer); + return FReply::Handled(); + } + } + else if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer) + { + const FKey Key = InKeyEvent.GetKey(); + if (Key == EKeys::BackSpace || Key == EKeys::Escape) + { + SetActiveScreen(EAgrarianMvpFrontendScreen::CharacterSelection); + return FReply::Handled(); + } } return Super::NativeOnKeyDown(InGeometry, InKeyEvent); @@ -99,6 +114,10 @@ int32 UAgrarianMvpFrontendWidget::NativePaint( { DrawCharacterSelection(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); } + else if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer) + { + DrawJoinServer(OutDrawElements, LayerId, AllottedGeometry, PanelPosition, PanelSize, Scale); + } return LayerId; } @@ -207,6 +226,51 @@ void UAgrarianMvpFrontendWidget::DrawCharacterSelection( DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::Format(FText::FromString(TEXT("Selected: {0}. Use Left/Right or A/D to choose; both choices enter the same MVP survival baseline.")), GetSelectedCharacterLabel()), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.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("Character: {0}")), 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)); + DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Press Backspace or Escape to return to character selection.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); +} + FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const { return SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 112189e..aae2eaa 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -86,6 +86,14 @@ private: const FVector2D& PanelSize, float Scale) const; + void DrawJoinServer( + FSlateWindowElementList& OutDrawElements, + int32& LayerId, + const FGeometry& AllottedGeometry, + const FVector2D& PanelPosition, + const FVector2D& PanelSize, + float Scale) const; + FText GetSelectedCharacterLabel() const; void DrawTextAt(