diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index ca2c0d2..6bb72b8 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -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] 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. -- [ ] 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 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. diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 85984a7..ed839ac 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -30,3 +30,4 @@ IncludePrerequisites=True IncludeAppLocalPrerequisites=False Build=IfProjectHasCode +MapsToCook=(FilePath="/Game/Agrarian/Maps/L_GroundZeroTerrain_Test") ++DirectoriesToAlwaysCook=(Path="/Game/Agrarian/Characters") diff --git a/Content/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female.uasset b/Content/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female.uasset new file mode 100755 index 0000000..9cdde59 --- /dev/null +++ b/Content/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Female.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0252f86f0012590ccf380c4b2b0839cbf3fc0b94e9bd5b63a9cda826e2680d6 +size 5553 diff --git a/Content/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male.uasset b/Content/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male.uasset new file mode 100755 index 0000000..2f0343e --- /dev/null +++ b/Content/Agrarian/Characters/Materials/M_AGR_CharacterProxy_Workwear_Male.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59d26e16bc1f94dba51709c4b28c4b488f3ea5e43f640d525e159cd67907ce66 +size 5541 diff --git a/Docs/Characters/MvpCharacterProxies.md b/Docs/Characters/MvpCharacterProxies.md new file mode 100644 index 0000000..3f1e9f6 --- /dev/null +++ b/Docs/Characters/MvpCharacterProxies.md @@ -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. diff --git a/Scripts/setup_mvp_character_proxies.py b/Scripts/setup_mvp_character_proxies.py new file mode 100644 index 0000000..0d52601 --- /dev/null +++ b/Scripts/setup_mvp_character_proxies.py @@ -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() diff --git a/Scripts/verify_mvp_character_archetype_choice.py b/Scripts/verify_mvp_character_archetype_choice.py index eee76be..63b23dd 100644 --- a/Scripts/verify_mvp_character_archetype_choice.py +++ b/Scripts/verify_mvp_character_archetype_choice.py @@ -27,15 +27,17 @@ EXPECTED = { "Selected", "Click a card or use Left/Right", "GetSelectedCharacterLabel", - "NativeOnMouseButtonDown", - "IsPointInside(LocalMousePosition, MaleCardPosition, CardSize)", - "IsPointInside(LocalMousePosition, FemaleCardPosition, CardSize)", + "HandleMaleCharacterClicked", + "HandleFemaleCharacterClicked", + "AgrarianSelectCharacter male", + "AgrarianSelectCharacter female", ], "AgrarianGamePlayerController.h": [ "AgrarianSelectCharacter", ], "AgrarianGamePlayerController.cpp": [ "AgrarianSelectCharacter", + "ApplyMvpCharacterProxyToPawn", "SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale)", "SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale)", ], diff --git a/Scripts/verify_mvp_character_proxies.py b/Scripts/verify_mvp_character_proxies.py new file mode 100644 index 0000000..99e398a --- /dev/null +++ b/Scripts/verify_mvp_character_proxies.py @@ -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() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 3ba60b9..edf746b 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -12,12 +12,15 @@ #include "AgrarianPersistenceSubsystem.h" #include "AgrarianShelterActor.h" #include "AgrarianSurvivalComponent.h" +#include "Components/SkeletalMeshComponent.h" #include "EnhancedInputSubsystems.h" #include "Engine/LocalPlayer.h" +#include "Engine/SkeletalMesh.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" #include "InputCoreTypes.h" #include "InputMappingContext.h" +#include "Materials/MaterialInterface.h" #include "Blueprint/UserWidget.h" #include "TimerManager.h" #include "AgrarianGame.h" @@ -75,6 +78,20 @@ namespace 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() @@ -258,6 +275,30 @@ void AAgrarianGamePlayerController::HandleMvpEscapeInput() ShowMvpPauseMenu(); } +void AAgrarianGamePlayerController::ApplyMvpCharacterProxyToPawn() +{ + AAgrarianGameCharacter* AgrarianCharacter = GetPawn(); + USkeletalMeshComponent* MeshComponent = AgrarianCharacter ? AgrarianCharacter->GetMesh() : nullptr; + if (!MeshComponent) + { + return; + } + + if (USkeletalMesh* ProxyMesh = LoadObject(nullptr, GetMvpCharacterProxyMeshPath(SelectedMvpCharacterProxyId))) + { + MeshComponent->SetSkeletalMesh(ProxyMesh); + } + + if (UMaterialInterface* ProxyMaterial = LoadObject(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) { if (ItemId == NAME_None || Quantity <= 0) @@ -498,14 +539,18 @@ void AAgrarianGamePlayerController::AgrarianSelectCharacter(FName Archetype) if (Archetype == TEXT("male") || Archetype == TEXT("YoungAdultMale")) { + SelectedMvpCharacterProxyId = TEXT("male"); MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); + ApplyMvpCharacterProxyToPawn(); ClientMessage(TEXT("Selected MVP young adult male character archetype.")); return; } if (Archetype == TEXT("female") || Archetype == TEXT("YoungAdultFemale")) { + SelectedMvpCharacterProxyId = TEXT("female"); MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); + ApplyMvpCharacterProxyToPawn(); ClientMessage(TEXT("Selected MVP young adult female character archetype.")); return; } diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index f2bb863..12c178b 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -66,6 +66,9 @@ protected: void HandleMvpConfirmInput(); void HandleMvpBackInput(); void HandleMvpEscapeInput(); + void ApplyMvpCharacterProxyToPawn(); + + FName SelectedMvpCharacterProxyId = TEXT("male"); public: UFUNCTION(Exec) diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index bd0a50d..7379308 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -226,6 +226,13 @@ void UAgrarianMvpFrontendWidget::CompleteFrontendFlow() { if (APlayerController* PlayerController = GetOwningPlayer()) { + if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading) + { + PlayerController->ConsoleCommand(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale + ? TEXT("AgrarianSelectCharacter female") + : TEXT("AgrarianSelectCharacter male")); + } + PlayerController->SetInputMode(FInputModeGameOnly()); PlayerController->bShowMouseCursor = false; PlayerController->SetIgnoreMoveInput(false); @@ -311,7 +318,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() UVerticalBox* MaleStack = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("MalePioneerStack")); 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("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); if (UHorizontalBoxSlot* MaleSlot = CharacterRow->AddChildToHorizontalBox(MaleButton)) { @@ -325,7 +332,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() UVerticalBox* FemaleStack = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack")); 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("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); if (UHorizontalBoxSlot* FemaleSlot = CharacterRow->AddChildToHorizontalBox(FemaleButton)) {