Stabilize investor frontend entry
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
|
||||
@@ -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()",
|
||||
|
||||
@@ -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<FButtonStyle>(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>(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>(UVerticalBox::StaticClass(), TEXT("MalePioneerStack"));
|
||||
MaleButton->SetContent(MaleStack);
|
||||
@@ -328,7 +306,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
|
||||
}
|
||||
|
||||
UButton* FemaleButton = WidgetTree->ConstructWidget<UButton>(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>(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>(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<UButtonSlot>(ContinueLabel->Slot))
|
||||
@@ -380,7 +356,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
|
||||
}
|
||||
|
||||
UButton* BackButton = WidgetTree->ConstructWidget<UButton>(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>(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()
|
||||
{
|
||||
DeferFrontendAction([this]()
|
||||
{
|
||||
ConfirmActiveScreen();
|
||||
});
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::HandleBackClicked()
|
||||
{
|
||||
DeferFrontendAction([this]()
|
||||
{
|
||||
BackFromActiveScreen();
|
||||
});
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked()
|
||||
{
|
||||
DeferFrontendAction([this]()
|
||||
{
|
||||
SaveAndQuit();
|
||||
});
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked()
|
||||
{
|
||||
DeferFrontendAction([this]()
|
||||
{
|
||||
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
|
||||
});
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked()
|
||||
{
|
||||
DeferFrontendAction([this]()
|
||||
{
|
||||
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
|
||||
});
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::DeferFrontendAction(TFunction<void()> Action)
|
||||
{
|
||||
if (!Action)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
FTimerDelegate DeferredAction;
|
||||
DeferredAction.BindLambda([WeakThis = TWeakObjectPtr<UAgrarianMvpFrontendWidget>(this), Action = MoveTemp(Action)]() mutable
|
||||
{
|
||||
if (WeakThis.IsValid())
|
||||
{
|
||||
Action();
|
||||
}
|
||||
});
|
||||
World->GetTimerManager().SetTimerForNextTick(DeferredAction);
|
||||
return;
|
||||
}
|
||||
|
||||
Action();
|
||||
}
|
||||
|
||||
FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const
|
||||
|
||||
@@ -112,6 +112,8 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleFemaleCharacterClicked();
|
||||
|
||||
void DeferFrontendAction(TFunction<void()> Action);
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UButton> PrimaryFocusButton;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user