Segment MVP startup and pause flow

This commit is contained in:
2026-05-19 10:20:12 -07:00
parent af1edb51bc
commit de02b20786
7 changed files with 136 additions and 10 deletions
+1 -1
View File
@@ -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.
@@ -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",
@@ -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()
+1 -1
View File
@@ -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",
@@ -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)
@@ -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);
@@ -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();