diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset index 68ab36a..01aadde 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:319c13875c6229171a95026874c28c55e8e6e67c1f03df98f67a17ccb472204a -size 5465 +oid sha256:c9b9e6aa6e4e0e2fc1000814d517fa41935a838495459c087b2c00cd50197baf +size 5529 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset index 4643980..5765781 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d72c2711649942c1616deb9f3a148dc8b1220451061b279b56d6869e88622979 -size 5471 +oid sha256:b35c68ba9583e152a68855afe61bfc4306c18dd38877ce9831a20151d96ef005 +size 5535 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset index 1f62a46..6d31394 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:830f96bb928091a0edad65cfe567eafe2b173816351fb3aa47a7773596a5337e -size 5459 +oid sha256:a32f70be8d479887d17948efa560cd290fd17e843d092ff9457b7d8cb998f4e5 +size 5523 diff --git a/Docs/QA/InvestorReadinessAudit-2026-05-19.md b/Docs/QA/InvestorReadinessAudit-2026-05-19.md new file mode 100644 index 0000000..16eb823 --- /dev/null +++ b/Docs/QA/InvestorReadinessAudit-2026-05-19.md @@ -0,0 +1,57 @@ +# Investor Readiness Audit - 2026-05-19 + +## Current Verdict + +The current build should not be presented as investor visual MVP until it passes +a real-GPU packaged-client visual and stability check. The systems are further +along than the presentation, but the player-facing first impression is still at +risk. + +## Findings + +- Packaged crash logs show a Slate/UI paint crash while the character-selection + frontend is active. The crash callstack centers on `SButton::OnPaint` and + `FSlateDrawElement::MakeBox`, so the immediate fix removes custom button brush + styling, defers frontend actions to the next tick, and shortens the investor + path from character selection directly into Ground Zero. +- The project content library still contains only placeholder meshes plus the + Manny/Quinn mannequin assets for characters. The current character and object + visuals are proxy-quality, not final investor-quality realism. +- Ground Zero editor verification confirms landscape, water/resource actors, + foliage variation actors, and environment materials exist, but this does not + prove the packaged executable looks good through the real GPU path. +- Cooked runtime logs warned that tree, shrub, and grass materials were missing + instanced-static-mesh usage. Those flags are now set on the assets and in the + repeatable map setup script. +- The generated investor roadmap/report can show old milestone completion state + if it is not regenerated from the current roadmap after fixes. The source + roadmap still contains explicit visual QA acceptance items that remain open + until real packaged screenshots or clips are captured. + +## Required Before Investor-Ready Label + +- Launch the packaged Windows demo through the real GPU desktop path. +- Capture startup credits, character selection, first spawn, terrain, vegetation, + water, campfire, shelter, pause menu, and save/quit. +- Confirm selecting a character and entering Ground Zero does not crash. +- Confirm the first player view is not ground/legs, the menu does not appear + after gameplay starts, water is visible, foliage is visible, and debug-looking + primitives are not the dominant read. +- Replace proxy/mannequin art with real or production-directed free/internal art + assets before calling the build visually investor-ready. + +## Verification Completed In This Pass + +- `verify_mvp_menu_input_and_quit_flow.py` +- `verify_mvp_character_archetype_choice.py` +- `verify_mvp_character_proxies.py` +- `verify_mvp_frontend_umg_flow.py` +- `verify_ground_zero_natural_environment_pass.py` +- Windows package BuildCookRun completed successfully after the fixes. + +## Remaining Risk + +The packaged build still needs a real interactive visual pass through +Sunshine/Moonlight or direct Windows display access. Static checks and editor +commandlets are not enough to clear the user-reported crash and visual quality +concerns. diff --git a/Scripts/fix_ground_zero_instanced_material_usage.py b/Scripts/fix_ground_zero_instanced_material_usage.py new file mode 100644 index 0000000..3e1114f --- /dev/null +++ b/Scripts/fix_ground_zero_instanced_material_usage.py @@ -0,0 +1,43 @@ +import unreal + +MATERIAL_PATHS = [ + "/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak", + "/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush", + "/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal", +] + + +def set_instanced_usage(material): + applied = False + for property_name in ( + "used_with_instanced_static_meshes", + "bUsedWithInstancedStaticMeshes", + ): + try: + material.set_editor_property(property_name, True) + applied = True + except Exception: + pass + + return applied + + +dirty_assets = [] +for material_path in MATERIAL_PATHS: + material = unreal.EditorAssetLibrary.load_asset(material_path) + if not material: + raise RuntimeError(f"Missing material: {material_path}") + + if not set_instanced_usage(material): + raise RuntimeError(f"Could not set instanced static mesh usage on {material_path}") + + unreal.MaterialEditingLibrary.recompile_material(material) + dirty_assets.append(material_path) + +for material_path in dirty_assets: + if not unreal.EditorAssetLibrary.save_asset(material_path, only_if_is_dirty=False): + raise RuntimeError(f"Failed to save updated material: {material_path}") + +print("Updated instanced static mesh usage for Ground Zero foliage materials:") +for material_path in dirty_assets: + print(f" - {material_path}") diff --git a/Scripts/setup_ground_zero_demo_map.py b/Scripts/setup_ground_zero_demo_map.py index f21f613..ac3a7a2 100644 --- a/Scripts/setup_ground_zero_demo_map.py +++ b/Scripts/setup_ground_zero_demo_map.py @@ -52,16 +52,19 @@ ENVIRONMENT_MATERIALS = { "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak", "color": unreal.LinearColor(0.18, 0.31, 0.16, 1.0), "roughness": 0.88, + "used_with_instanced_static_meshes": True, }, "shrub": { "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush", "color": unreal.LinearColor(0.31, 0.39, 0.20, 1.0), "roughness": 0.9, + "used_with_instanced_static_meshes": True, }, "grass": { "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal", "color": unreal.LinearColor(0.47, 0.42, 0.23, 1.0), "roughness": 0.95, + "used_with_instanced_static_meshes": True, }, "wood_resource": { "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource", @@ -807,6 +810,10 @@ def ensure_environment_materials(): unreal.MaterialEditingLibrary.recompile_material(material) unreal.EditorAssetLibrary.save_asset(spec["path"]) unreal.log(f"Created Ground Zero environment material: {spec['path']}") + if spec.get("used_with_instanced_static_meshes"): + material.set_editor_property("used_with_instanced_static_meshes", True) + unreal.MaterialEditingLibrary.recompile_material(material) + unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False) created_or_loaded[key] = material return created_or_loaded diff --git a/Scripts/verify_mvp_frontend_umg_flow.py b/Scripts/verify_mvp_frontend_umg_flow.py index 545b429..ed91984 100644 --- a/Scripts/verify_mvp_frontend_umg_flow.py +++ b/Scripts/verify_mvp_frontend_umg_flow.py @@ -37,9 +37,8 @@ def main() -> None: "UButton::StaticClass()", "UTextBlock::StaticClass()", "OnClicked.AddDynamic", - "OnHovered.AddDynamic", - "ButtonStyle.Hovered.TintColor", - "ButtonStyle.Pressed.TintColor", + "SetBackgroundColor", + "DeferFrontendAction", "SetIsFocusable(true)", "SetKeyboardFocus()", "BackFromActiveScreen()", diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index 0061915..f12c353 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -17,22 +17,6 @@ #include "Styling/CoreStyle.h" #include "TimerManager.h" -namespace -{ -FButtonStyle MakeAgrarianButtonStyle(const FLinearColor& NormalColor, const FLinearColor& HoveredColor) -{ - FButtonStyle ButtonStyle = FCoreStyle::Get().GetWidgetStyle(TEXT("Button")); - ButtonStyle.Normal.TintColor = FSlateColor(NormalColor); - ButtonStyle.Hovered.TintColor = FSlateColor(HoveredColor); - ButtonStyle.Pressed.TintColor = FSlateColor(FLinearColor( - FMath::Min(HoveredColor.R + 0.12f, 1.0f), - FMath::Min(HoveredColor.G + 0.12f, 1.0f), - FMath::Min(HoveredColor.B + 0.12f, 1.0f), - HoveredColor.A)); - return ButtonStyle; -} -} - void UAgrarianMvpFrontendWidget::NativeConstruct() { Super::NativeConstruct(); @@ -175,7 +159,7 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen() { if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) { - SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer); + CompleteFrontendFlow(); return; } @@ -201,11 +185,6 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen() CompleteFrontendFlow(); return; } - - if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit) - { - return; - } } void UAgrarianMvpFrontendWidget::ReturnFromActiveScreen() @@ -226,7 +205,7 @@ void UAgrarianMvpFrontendWidget::CompleteFrontendFlow() { if (APlayerController* PlayerController = GetOwningPlayer()) { - if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading) + if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection || ActiveScreen == EAgrarianMvpFrontendScreen::Loading) { PlayerController->ConsoleCommand(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale ? TEXT("AgrarianSelectCharacter female") @@ -291,7 +270,6 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() 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); UButton* QuitButton = AddButton(Panel, FText::FromString(TEXT("Save & Quit")), QuitButtonColor, FLinearColor(0.58f, 0.28f, 0.22f, 1.0f), 34.0f * Scale); QuitButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked); @@ -314,7 +292,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() const bool bMaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultMale; const bool bFemaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale; UButton* MaleButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("MalePioneerButton")); - MaleButton->SetStyle(MakeAgrarianButtonStyle(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor)); + MaleButton->SetBackgroundColor(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor); MaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked); UVerticalBox* MaleStack = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("MalePioneerStack")); MaleButton->SetContent(MaleStack); @@ -328,7 +306,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() } UButton* FemaleButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("FemalePioneerButton")); - FemaleButton->SetStyle(MakeAgrarianButtonStyle(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor)); + FemaleButton->SetBackgroundColor(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor); FemaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked); UVerticalBox* FemaleStack = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack")); FemaleButton->SetContent(FemaleStack); @@ -341,9 +319,8 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() FemaleSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); } - PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Continue")), ButtonColor, ButtonHoverColor, 10.0f * Scale); + PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 10.0f * Scale); PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); - PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); AddText(Panel, FText::Format(FText::FromString(TEXT("Selected {0}: {1}. Click a card or use Left/Right, then continue.")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f); return; } @@ -363,9 +340,8 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() } PrimaryFocusButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("ContinueToLoadingButton")); - PrimaryFocusButton->SetStyle(MakeAgrarianButtonStyle(ButtonColor, ButtonHoverColor)); + PrimaryFocusButton->SetBackgroundColor(ButtonColor); PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); - PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); UTextBlock* ContinueLabel = AddText(nullptr, FText::FromString(TEXT("Continue to loading")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f); PrimaryFocusButton->SetContent(ContinueLabel); if (UButtonSlot* ContinueLabelSlot = Cast(ContinueLabel->Slot)) @@ -380,7 +356,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() } UButton* BackButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("BackButton")); - BackButton->SetStyle(MakeAgrarianButtonStyle(SecondaryButtonColor, ButtonHoverColor)); + BackButton->SetBackgroundColor(SecondaryButtonColor); BackButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleBackClicked); UTextBlock* BackLabel = AddText(nullptr, FText::FromString(TEXT("Back")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f); BackButton->SetContent(BackLabel); @@ -413,7 +389,6 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() AddText(Panel, FText::Format(FText::FromString(TEXT("{0}: {1} | Server: {2}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel(), JoinServerAddress), FMath::RoundToInt(18.0f * Scale), false, TextColor, 34.0f * Scale); PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 14.0f * Scale); PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked); - PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton); AddText(Panel, FText::FromString(TEXT("Click or press Enter to close the MVP menu and begin testing.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f); } @@ -440,7 +415,7 @@ UTextBlock* UAgrarianMvpFrontendWidget::AddText(UVerticalBox* Parent, const FTex UButton* UAgrarianMvpFrontendWidget::AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding) { UButton* Button = WidgetTree->ConstructWidget(UButton::StaticClass()); - Button->SetStyle(MakeAgrarianButtonStyle(NormalColor, HoveredColor)); + Button->SetBackgroundColor(NormalColor); Button->SetClickMethod(EButtonClickMethod::MouseDown); Button->SetTouchMethod(EButtonTouchMethod::Down); @@ -481,27 +456,66 @@ void UAgrarianMvpFrontendWidget::FocusPrimaryButton() void UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked() { - ConfirmActiveScreen(); + DeferFrontendAction([this]() + { + ConfirmActiveScreen(); + }); } void UAgrarianMvpFrontendWidget::HandleBackClicked() { - BackFromActiveScreen(); + DeferFrontendAction([this]() + { + BackFromActiveScreen(); + }); } void UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked() { - SaveAndQuit(); + DeferFrontendAction([this]() + { + SaveAndQuit(); + }); } void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked() { - SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); + DeferFrontendAction([this]() + { + SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); + }); } void UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked() { - SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); + DeferFrontendAction([this]() + { + SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); + }); +} + +void UAgrarianMvpFrontendWidget::DeferFrontendAction(TFunction Action) +{ + if (!Action) + { + return; + } + + if (UWorld* World = GetWorld()) + { + FTimerDelegate DeferredAction; + DeferredAction.BindLambda([WeakThis = TWeakObjectPtr(this), Action = MoveTemp(Action)]() mutable + { + if (WeakThis.IsValid()) + { + Action(); + } + }); + World->GetTimerManager().SetTimerForNextTick(DeferredAction); + return; + } + + Action(); } FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 07bf34b..00579e7 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -112,6 +112,8 @@ private: UFUNCTION() void HandleFemaleCharacterClicked(); + void DeferFrontendAction(TFunction Action); + UPROPERTY() TObjectPtr PrimaryFocusButton;