diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 4624888..72da273 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -818,7 +818,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [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. - [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. +- [x] 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. Switched startup/menu control to UI-only input, added explicit segment labels for character selection, server join, loading, pause, and saving, and made Save & Quit transition through a dedicated saving screen before issuing save/quit. - [ ] 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. - [ ] Replace box/sphere/cylinder survival objects with readable MVP meshes for campfires, primitive shelter pieces, resource pickups, water sources, wildlife, and gathered items. diff --git a/Scripts/verify_mvp_menu_input_and_quit_flow.py b/Scripts/verify_mvp_menu_input_and_quit_flow.py index 9f11eae..047e473 100644 --- a/Scripts/verify_mvp_menu_input_and_quit_flow.py +++ b/Scripts/verify_mvp_menu_input_and_quit_flow.py @@ -16,10 +16,12 @@ EXPECTED = { "SaveAndQuit", ], "AgrarianMvpFrontendWidget.cpp": [ - "NativeOnMouseButtonDown", - "IsPointInside(LocalMousePosition, MaleCardPosition, CardSize)", - "IsPointInside(LocalMousePosition, FemaleCardPosition, CardSize)", + "UButton::StaticClass()", + "HandleMaleCharacterClicked", + "HandleFemaleCharacterClicked", + "OnClicked.AddDynamic", "Save & Quit", + "Saving World", "ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))", "ConsoleCommand(TEXT(\"quit\"))", "PlayerController->SetIgnoreMoveInput(false)", @@ -32,6 +34,7 @@ EXPECTED = { "AgrarianGamePlayerController.cpp": [ "SetIgnoreMoveInput(true)", "SetIgnoreLookInput(true)", + "FInputModeUIOnly", "InputComponent->BindKey(EKeys::Enter", "InputComponent->BindKey(EKeys::SpaceBar", "InputComponent->BindKey(EKeys::Escape", diff --git a/Scripts/verify_mvp_segmented_startup_pause_flow.py b/Scripts/verify_mvp_segmented_startup_pause_flow.py new file mode 100644 index 0000000..8efb9c9 --- /dev/null +++ b/Scripts/verify_mvp_segmented_startup_pause_flow.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Verify the MVP startup/menu flow is segmented and modal.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FRONTEND_H = ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.h" +FRONTEND_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp" +CONTROLLER_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +def require(condition: bool, message: str) -> None: + if not condition: + raise SystemExit(f"FAILED: {message}") + + +def main() -> None: + header = FRONTEND_H.read_text(encoding="utf-8") + frontend = FRONTEND_CPP.read_text(encoding="utf-8") + controller = CONTROLLER_CPP.read_text(encoding="utf-8") + roadmap = ROADMAP.read_text(encoding="utf-8") + + for token in ( + "SavingAndQuit", + "ExecuteSaveAndQuit", + ): + require(token in header, f"missing segmented flow declaration: {token}") + + for token in ( + "Character Selection", + "Server Join", + "Loading Segment", + "Pause Menu", + "Gameplay is paused while this menu is active.", + "Saving World", + "Writing the current world state", + "SetActiveScreen(EAgrarianMvpFrontendScreen::SavingAndQuit)", + "GetTimerManager().SetTimer", + "ExecuteSaveAndQuit", + "ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))", + "ConsoleCommand(TEXT(\"quit\"))", + ): + require(token in frontend, f"missing segmented frontend token: {token}") + + require( + controller.count("FInputModeUIOnly") >= 3, + "startup/frontend/pause paths should use UI-only input while menus are active", + ) + for token in ( + "SetIgnoreMoveInput(true)", + "SetIgnoreLookInput(true)", + "SetInputMode(FInputModeGameOnly())", + "SetIgnoreMoveInput(false)", + "SetIgnoreLookInput(false)", + "saving", + ): + require(token in controller + frontend, f"missing modal input or debug token: {token}") + + require( + "- [x] Make startup credits, character selection, server/join, loading, pause, save, and quit feel like separate intentional segments" in roadmap, + "0.1.O segmented startup/menu roadmap item is not checked off", + ) + + print("OK: MVP startup, menu, loading, pause, save, and quit flow is segmented and modal.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_startup_credits_sequence.py b/Scripts/verify_startup_credits_sequence.py index 2d548a3..e59a3b0 100644 --- a/Scripts/verify_startup_credits_sequence.py +++ b/Scripts/verify_startup_credits_sequence.py @@ -39,7 +39,7 @@ REQUIRED = { ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp": [ "GetWorldTimerManager().SetTimer", "ShowMvpFrontend", - "FInputModeGameAndUI", + "FInputModeUIOnly", ], ROOT / "Config" / "DefaultGame.ini": [ "ProjectVersion=0.1.N-investor.20260518", diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index fed83fd..3ba60b9 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -87,6 +87,7 @@ void AAgrarianGamePlayerController::BeginPlay() { SetIgnoreMoveInput(true); SetIgnoreLookInput(true); + SetInputMode(FInputModeUIOnly()); bShowMouseCursor = false; GetWorldTimerManager().SetTimer( MvpFrontendStartupTimerHandle, @@ -186,7 +187,7 @@ void AAgrarianGamePlayerController::ShowMvpFrontend() { MvpFrontendWidget->AddToPlayerScreen(10); } - SetInputMode(FInputModeGameAndUI().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock)); + SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock)); bShowMouseCursor = true; SetIgnoreMoveInput(true); SetIgnoreLookInput(true); @@ -223,7 +224,7 @@ void AAgrarianGamePlayerController::ShowMvpPauseMenu() } MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu); - SetInputMode(FInputModeGameAndUI().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock)); + SetInputMode(FInputModeUIOnly().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock)); bShowMouseCursor = true; SetIgnoreMoveInput(true); SetIgnoreLookInput(true); @@ -548,7 +549,14 @@ void AAgrarianGamePlayerController::AgrarianShowMvpScreen(FName ScreenName) return; } - ClientMessage(TEXT("Usage: AgrarianShowMvpScreen main|character|join|loading")); + if (ScreenName == TEXT("saving") || ScreenName == TEXT("SavingAndQuit")) + { + MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::SavingAndQuit); + ClientMessage(TEXT("MVP frontend screen: saving and quit.")); + return; + } + + ClientMessage(TEXT("Usage: AgrarianShowMvpScreen main|character|join|loading|saving")); } void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index 5f7be33..bd0a50d 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -15,6 +15,7 @@ #include "GameFramework/PlayerController.h" #include "InputCoreTypes.h" #include "Styling/CoreStyle.h" +#include "TimerManager.h" namespace { @@ -102,6 +103,10 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry, return FReply::Handled(); } } + else if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit) + { + return FReply::Handled(); + } return Super::NativeOnKeyDown(InGeometry, InKeyEvent); } @@ -145,6 +150,19 @@ void UAgrarianMvpFrontendWidget::BackFromActiveScreen() } void UAgrarianMvpFrontendWidget::SaveAndQuit() +{ + SetActiveScreen(EAgrarianMvpFrontendScreen::SavingAndQuit); + if (UWorld* World = GetWorld()) + { + FTimerHandle SaveAndQuitTimerHandle; + World->GetTimerManager().SetTimer(SaveAndQuitTimerHandle, this, &UAgrarianMvpFrontendWidget::ExecuteSaveAndQuit, 0.35f, false); + return; + } + + ExecuteSaveAndQuit(); +} + +void UAgrarianMvpFrontendWidget::ExecuteSaveAndQuit() { if (APlayerController* PlayerController = GetOwningPlayer()) { @@ -173,9 +191,20 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen() return; } + if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit) + { + return; + } + if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu) { CompleteFrontendFlow(); + return; + } + + if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit) + { + return; } } @@ -249,8 +278,9 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu) { + AddText(Panel, FText::FromString(TEXT("Pause Menu")), FMath::RoundToInt(20.0f * Scale), true, AccentColor, 6.0f * Scale); 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); + AddText(Panel, FText::FromString(TEXT("Gameplay is paused while this menu is active.")), 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); @@ -263,6 +293,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) { + AddText(Panel, FText::FromString(TEXT("Character Selection")), FMath::RoundToInt(18.0f * Scale), true, AccentColor, 6.0f * Scale); 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); @@ -311,6 +342,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer) { + AddText(Panel, FText::FromString(TEXT("Server Join")), FMath::RoundToInt(18.0f * Scale), true, AccentColor, 6.0f * Scale); 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); @@ -359,6 +391,15 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() return; } + if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit) + { + AddText(Panel, FText::FromString(TEXT("Saving World")), FMath::RoundToInt(20.0f * Scale), true, AccentColor, 6.0f * Scale); + AddText(Panel, FText::FromString(TEXT("Writing the current world state")), FMath::RoundToInt(34.0f * Scale), true, TextColor, 8.0f * Scale); + AddText(Panel, FText::FromString(TEXT("The demo will close after the save command is issued.")), FMath::RoundToInt(18.0f * Scale), false, MutedTextColor, 0.0f); + return; + } + + AddText(Panel, FText::FromString(TEXT("Loading Segment")), FMath::RoundToInt(18.0f * Scale), true, AccentColor, 6.0f * Scale); 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); diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 200b650..07bf34b 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -16,7 +16,8 @@ enum class EAgrarianMvpFrontendScreen : uint8 MainMenu, CharacterSelection, JoinServer, - Loading + Loading, + SavingAndQuit }; UENUM(BlueprintType) @@ -93,6 +94,9 @@ private: UFUNCTION() void FocusPrimaryButton(); + UFUNCTION() + void ExecuteSaveAndQuit(); + UFUNCTION() void HandlePrimaryActionClicked();