Add MVP character proxy selection

This commit is contained in:
2026-05-19 10:32:08 -07:00
parent 11a13042b6
commit bc7617b08b
11 changed files with 264 additions and 6 deletions
+1 -1
View File
@@ -820,7 +820,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [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. - [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.
- [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. - [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.
- [x] 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. Added a packaged-client GPU startup visual test runbook, a Windows helper that checks the packaged demo and Sunshine service before capture, and verifier coverage requiring Moonlight/Sunshine evidence instead of QEMU guest-agent screenshots. - [x] 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. Added a packaged-client GPU startup visual test runbook, a Windows helper that checks the packaged demo and Sunshine service before capture, and verifier coverage requiring Moonlight/Sunshine evidence instead of QEMU guest-agent screenshots.
- [ ] Add first realistic playable character proxies for the selected young adult male and female archetypes, replacing the default mannequin/dummy presentation for investor builds. - [x] Add first realistic playable character proxies for the selected young adult male and female archetypes, replacing the default mannequin/dummy presentation for investor builds. Added selected male/female MVP proxy application to the player controller, workwear material generation, cook coverage for Agrarian character assets, documentation, and verification so the menu-selected archetype changes the possessed pawn instead of leaving one default dummy presentation.
- [ ] Replace box/sphere/cylinder survival objects with readable MVP meshes for campfires, primitive shelter pieces, resource pickups, water sources, wildlife, and gathered items. - [ ] Replace box/sphere/cylinder survival objects with readable MVP meshes for campfires, primitive shelter pieces, resource pickups, water sources, wildlife, and gathered items.
- [ ] Replace the placeholder Ground Zero environment presentation with investor-facing biome dressing: believable terrain material, grass, brush, shrubs, bushes, trees, rocks, water visuals, and local coastal-scrub color variation. - [ ] Replace the placeholder Ground Zero environment presentation with investor-facing biome dressing: believable terrain material, grass, brush, shrubs, bushes, trees, rocks, water visuals, and local coastal-scrub color variation.
- [ ] Add a real water-source visual pass with surface material, edge treatment, scale, and placement that reads as collectable freshwater instead of a placeholder plane. - [ ] Add a real water-source visual pass with surface material, edge treatment, scale, and placement that reads as collectable freshwater instead of a placeholder plane.
+1
View File
@@ -30,3 +30,4 @@ IncludePrerequisites=True
IncludeAppLocalPrerequisites=False IncludeAppLocalPrerequisites=False
Build=IfProjectHasCode Build=IfProjectHasCode
+MapsToCook=(FilePath="/Game/Agrarian/Maps/L_GroundZeroTerrain_Test") +MapsToCook=(FilePath="/Game/Agrarian/Maps/L_GroundZeroTerrain_Test")
+DirectoriesToAlwaysCook=(Path="/Game/Agrarian/Characters")
+39
View File
@@ -0,0 +1,39 @@
# MVP Character Proxies
The 0.1.O investor visual pass introduces first playable character proxies for
the startup character selection flow. These are not final production humans;
they are practical, human-scale stand-ins that remove the single default dummy
presentation while the final realistic character art pipeline is still pending.
## Current Proxies
- Young adult male:
`/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple`
- Young adult female:
`/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple`
- Male workwear material:
`/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male`
- Female workwear material:
`/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female`
The materials use muted earth-tone workwear colors so the character reads as an
Agrarian survivor/pioneer proxy instead of an untouched template dummy.
## Runtime Flow
The MVP frontend stores the selected young-adult archetype. When the loading
segment closes, it issues `AgrarianSelectCharacter male` or
`AgrarianSelectCharacter female`. The player controller records the selected
proxy and applies the matching mesh/material to the possessed Agrarian player
character.
The character asset folder is always cooked for investor builds so both proxy
materials remain available in packaged clients.
## Replacement Path
Final character work should replace these proxies with grounded realistic human
assets, production clothing, age/condition variation, and replication/persistence
of visual state. The current C++ selection path is intentionally simple so final
assets can slot into the same male/female archetype switch without reworking the
menu flow.
+63
View File
@@ -0,0 +1,63 @@
import unreal
MATERIAL_FOLDER = "/Game/Agrarian/Characters/Materials"
CHARACTER_PROXY_MATERIALS = {
"M_AGR_CharacterProxy_Workwear_Male": {
"color": unreal.LinearColor(0.23, 0.20, 0.15, 1.0),
"roughness": 0.88,
},
"M_AGR_CharacterProxy_Workwear_Female": {
"color": unreal.LinearColor(0.20, 0.24, 0.18, 1.0),
"roughness": 0.88,
},
}
def ensure_character_proxy_materials():
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
created = []
for asset_name, spec in CHARACTER_PROXY_MATERIALS.items():
asset_path = f"{MATERIAL_FOLDER}/{asset_name}"
material = unreal.EditorAssetLibrary.load_asset(asset_path)
if not material:
material = asset_tools.create_asset(asset_name, MATERIAL_FOLDER, unreal.Material, unreal.MaterialFactoryNew())
if not material:
raise RuntimeError(f"Could not create character proxy material: {asset_path}")
base_color = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant3Vector, -420, -120
)
base_color.set_editor_property("constant", spec["color"])
unreal.MaterialEditingLibrary.connect_material_property(
base_color, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant, -420, 80
)
roughness.set_editor_property("r", spec["roughness"])
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
unreal.MaterialEditingLibrary.recompile_material(material)
created.append(asset_path)
unreal.EditorAssetLibrary.save_loaded_asset(material)
return created
def main():
created = ensure_character_proxy_materials()
if created:
for asset_path in created:
unreal.log(f"Created MVP character proxy material: {asset_path}")
else:
unreal.log("MVP character proxy materials already exist.")
main()
@@ -27,15 +27,17 @@ EXPECTED = {
"Selected", "Selected",
"Click a card or use Left/Right", "Click a card or use Left/Right",
"GetSelectedCharacterLabel", "GetSelectedCharacterLabel",
"NativeOnMouseButtonDown", "HandleMaleCharacterClicked",
"IsPointInside(LocalMousePosition, MaleCardPosition, CardSize)", "HandleFemaleCharacterClicked",
"IsPointInside(LocalMousePosition, FemaleCardPosition, CardSize)", "AgrarianSelectCharacter male",
"AgrarianSelectCharacter female",
], ],
"AgrarianGamePlayerController.h": [ "AgrarianGamePlayerController.h": [
"AgrarianSelectCharacter", "AgrarianSelectCharacter",
], ],
"AgrarianGamePlayerController.cpp": [ "AgrarianGamePlayerController.cpp": [
"AgrarianSelectCharacter", "AgrarianSelectCharacter",
"ApplyMvpCharacterProxyToPawn",
"SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale)", "SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale)",
"SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale)", "SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale)",
], ],
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Verify first MVP male/female playable character proxy setup."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONTROLLER_H = ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h"
CONTROLLER_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp"
FRONTEND_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp"
CONFIG = ROOT / "Config" / "DefaultGame.ini"
DOC = ROOT / "Docs" / "Characters" / "MvpCharacterProxies.md"
SETUP = ROOT / "Scripts" / "setup_mvp_character_proxies.py"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
MALE_MATERIAL = ROOT / "Content" / "Agrarian" / "Characters" / "Materials" / "M_AGR_CharacterProxy_Workwear_Male.uasset"
FEMALE_MATERIAL = ROOT / "Content" / "Agrarian" / "Characters" / "Materials" / "M_AGR_CharacterProxy_Workwear_Female.uasset"
def require(condition: bool, message: str) -> None:
if not condition:
raise SystemExit(f"FAILED: {message}")
def main() -> None:
controller_h = CONTROLLER_H.read_text(encoding="utf-8")
controller = CONTROLLER_CPP.read_text(encoding="utf-8")
frontend = FRONTEND_CPP.read_text(encoding="utf-8")
config = CONFIG.read_text(encoding="utf-8")
doc = DOC.read_text(encoding="utf-8")
setup = SETUP.read_text(encoding="utf-8")
roadmap = ROADMAP.read_text(encoding="utf-8")
for token in (
"ApplyMvpCharacterProxyToPawn",
"SelectedMvpCharacterProxyId",
):
require(token in controller_h, f"controller header missing {token}")
for token in (
"SKM_Manny_Simple",
"SKM_Quinn_Simple",
"M_AGR_CharacterProxy_Workwear_Male",
"M_AGR_CharacterProxy_Workwear_Female",
"SetSkeletalMesh",
"SetMaterial",
"SelectedMvpCharacterProxyId = TEXT(\"male\")",
"SelectedMvpCharacterProxyId = TEXT(\"female\")",
"ApplyMvpCharacterProxyToPawn();",
):
require(token in controller, f"controller implementation missing {token}")
for token in (
"AgrarianSelectCharacter female",
"AgrarianSelectCharacter male",
"practical workwear proxy",
):
require(token in frontend, f"frontend character proxy flow missing {token}")
for token in (
"M_AGR_CharacterProxy_Workwear_Male",
"M_AGR_CharacterProxy_Workwear_Female",
"MaterialFactoryNew",
"MP_BASE_COLOR",
"MP_ROUGHNESS",
):
require(token in setup, f"setup script missing {token}")
require(
'+DirectoriesToAlwaysCook=(Path="/Game/Agrarian/Characters")' in config,
"character proxy folder is not always cooked",
)
for token in (
"SKM_Manny_Simple",
"SKM_Quinn_Simple",
"not final production humans",
"Final character work should replace these proxies",
):
require(token in doc, f"character proxy doc missing {token}")
require(
"- [x] Add first realistic playable character proxies for the selected young adult male and female archetypes" in roadmap,
"0.1.O character proxy roadmap item is not checked off",
)
require(MALE_MATERIAL.exists(), "male character proxy material asset is missing")
require(FEMALE_MATERIAL.exists(), "female character proxy material asset is missing")
print("OK: MVP male/female playable character proxy flow is wired and documented.")
if __name__ == "__main__":
main()
@@ -12,12 +12,15 @@
#include "AgrarianPersistenceSubsystem.h" #include "AgrarianPersistenceSubsystem.h"
#include "AgrarianShelterActor.h" #include "AgrarianShelterActor.h"
#include "AgrarianSurvivalComponent.h" #include "AgrarianSurvivalComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputSubsystems.h" #include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h" #include "Engine/LocalPlayer.h"
#include "Engine/SkeletalMesh.h"
#include "GameFramework/Character.h" #include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
#include "InputMappingContext.h" #include "InputMappingContext.h"
#include "Materials/MaterialInterface.h"
#include "Blueprint/UserWidget.h" #include "Blueprint/UserWidget.h"
#include "TimerManager.h" #include "TimerManager.h"
#include "AgrarianGame.h" #include "AgrarianGame.h"
@@ -75,6 +78,20 @@ namespace
return false; return false;
} }
const TCHAR* GetMvpCharacterProxyMeshPath(const FName ProxyId)
{
return ProxyId == TEXT("female")
? TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple")
: TEXT("/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple.SKM_Manny_Simple");
}
const TCHAR* GetMvpCharacterProxyMaterialPath(const FName ProxyId)
{
return ProxyId == TEXT("female")
? TEXT("/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female.M_AGR_CharacterProxy_Workwear_Female")
: TEXT("/Game/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male.M_AGR_CharacterProxy_Workwear_Male");
}
} }
void AAgrarianGamePlayerController::BeginPlay() void AAgrarianGamePlayerController::BeginPlay()
@@ -258,6 +275,30 @@ void AAgrarianGamePlayerController::HandleMvpEscapeInput()
ShowMvpPauseMenu(); ShowMvpPauseMenu();
} }
void AAgrarianGamePlayerController::ApplyMvpCharacterProxyToPawn()
{
AAgrarianGameCharacter* AgrarianCharacter = GetPawn<AAgrarianGameCharacter>();
USkeletalMeshComponent* MeshComponent = AgrarianCharacter ? AgrarianCharacter->GetMesh() : nullptr;
if (!MeshComponent)
{
return;
}
if (USkeletalMesh* ProxyMesh = LoadObject<USkeletalMesh>(nullptr, GetMvpCharacterProxyMeshPath(SelectedMvpCharacterProxyId)))
{
MeshComponent->SetSkeletalMesh(ProxyMesh);
}
if (UMaterialInterface* ProxyMaterial = LoadObject<UMaterialInterface>(nullptr, GetMvpCharacterProxyMaterialPath(SelectedMvpCharacterProxyId)))
{
const int32 MaterialCount = FMath::Max(1, MeshComponent->GetNumMaterials());
for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
{
MeshComponent->SetMaterial(MaterialIndex, ProxyMaterial);
}
}
}
void AAgrarianGamePlayerController::AgrarianGrantItem(FName ItemId, int32 Quantity) void AAgrarianGamePlayerController::AgrarianGrantItem(FName ItemId, int32 Quantity)
{ {
if (ItemId == NAME_None || Quantity <= 0) if (ItemId == NAME_None || Quantity <= 0)
@@ -498,14 +539,18 @@ void AAgrarianGamePlayerController::AgrarianSelectCharacter(FName Archetype)
if (Archetype == TEXT("male") || Archetype == TEXT("YoungAdultMale")) if (Archetype == TEXT("male") || Archetype == TEXT("YoungAdultMale"))
{ {
SelectedMvpCharacterProxyId = TEXT("male");
MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
ApplyMvpCharacterProxyToPawn();
ClientMessage(TEXT("Selected MVP young adult male character archetype.")); ClientMessage(TEXT("Selected MVP young adult male character archetype."));
return; return;
} }
if (Archetype == TEXT("female") || Archetype == TEXT("YoungAdultFemale")) if (Archetype == TEXT("female") || Archetype == TEXT("YoungAdultFemale"))
{ {
SelectedMvpCharacterProxyId = TEXT("female");
MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
ApplyMvpCharacterProxyToPawn();
ClientMessage(TEXT("Selected MVP young adult female character archetype.")); ClientMessage(TEXT("Selected MVP young adult female character archetype."));
return; return;
} }
@@ -66,6 +66,9 @@ protected:
void HandleMvpConfirmInput(); void HandleMvpConfirmInput();
void HandleMvpBackInput(); void HandleMvpBackInput();
void HandleMvpEscapeInput(); void HandleMvpEscapeInput();
void ApplyMvpCharacterProxyToPawn();
FName SelectedMvpCharacterProxyId = TEXT("male");
public: public:
UFUNCTION(Exec) UFUNCTION(Exec)
@@ -226,6 +226,13 @@ void UAgrarianMvpFrontendWidget::CompleteFrontendFlow()
{ {
if (APlayerController* PlayerController = GetOwningPlayer()) if (APlayerController* PlayerController = GetOwningPlayer())
{ {
if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
{
PlayerController->ConsoleCommand(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale
? TEXT("AgrarianSelectCharacter female")
: TEXT("AgrarianSelectCharacter male"));
}
PlayerController->SetInputMode(FInputModeGameOnly()); PlayerController->SetInputMode(FInputModeGameOnly());
PlayerController->bShowMouseCursor = false; PlayerController->bShowMouseCursor = false;
PlayerController->SetIgnoreMoveInput(false); PlayerController->SetIgnoreMoveInput(false);
@@ -311,7 +318,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
UVerticalBox* MaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("MalePioneerStack")); UVerticalBox* MaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("MalePioneerStack"));
MaleButton->SetContent(MaleStack); MaleButton->SetContent(MaleStack);
AddText(MaleStack, FText::FromString(TEXT("Young adult male")), FMath::RoundToInt(21.0f * Scale), true, TextColor, 8.0f * Scale); AddText(MaleStack, FText::FromString(TEXT("Young adult male")), FMath::RoundToInt(21.0f * Scale), true, TextColor, 8.0f * Scale);
AddText(MaleStack, FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 18.0f * Scale); AddText(MaleStack, FText::FromString(TEXT("Average build, practical workwear proxy, survival baseline.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 18.0f * Scale);
AddText(MaleStack, bMaleSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), FMath::RoundToInt(15.0f * Scale), true, bMaleSelected ? AccentColor : MutedTextColor, 0.0f); AddText(MaleStack, bMaleSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), FMath::RoundToInt(15.0f * Scale), true, bMaleSelected ? AccentColor : MutedTextColor, 0.0f);
if (UHorizontalBoxSlot* MaleSlot = CharacterRow->AddChildToHorizontalBox(MaleButton)) if (UHorizontalBoxSlot* MaleSlot = CharacterRow->AddChildToHorizontalBox(MaleButton))
{ {
@@ -325,7 +332,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
UVerticalBox* FemaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack")); UVerticalBox* FemaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack"));
FemaleButton->SetContent(FemaleStack); FemaleButton->SetContent(FemaleStack);
AddText(FemaleStack, FText::FromString(TEXT("Young adult female")), FMath::RoundToInt(21.0f * Scale), true, TextColor, 8.0f * Scale); AddText(FemaleStack, FText::FromString(TEXT("Young adult female")), FMath::RoundToInt(21.0f * Scale), true, TextColor, 8.0f * Scale);
AddText(FemaleStack, FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 18.0f * Scale); AddText(FemaleStack, FText::FromString(TEXT("Average build, practical workwear proxy, survival baseline.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 18.0f * Scale);
AddText(FemaleStack, bFemaleSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), FMath::RoundToInt(15.0f * Scale), true, bFemaleSelected ? AccentColor : MutedTextColor, 0.0f); AddText(FemaleStack, bFemaleSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), FMath::RoundToInt(15.0f * Scale), true, bFemaleSelected ? AccentColor : MutedTextColor, 0.0f);
if (UHorizontalBoxSlot* FemaleSlot = CharacterRow->AddChildToHorizontalBox(FemaleButton)) if (UHorizontalBoxSlot* FemaleSlot = CharacterRow->AddChildToHorizontalBox(FemaleButton))
{ {