diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index a49b676..f2cc6cf 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -795,7 +795,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add main menu placeholder. Added native `UAgrarianMvpFrontendWidget` with a scalable painted main menu panel and local player-controller spawning so the MVP frontend has an owned placeholder instead of relying on template UI. - [x] After splash/startup screens, land on an MVP character selection landing page. The local player controller now opens the MVP frontend on `CharacterSelection`, and the native frontend widget paints a scalable character-selection landing page before gameplay. -- [ ] Let players choose a realistic young adult male or female character with average proportions for the MVP. +- [x] Let players choose a realistic young adult male or female character with average proportions for the MVP. Added a selectable MVP character archetype enum, visible selected state, keyboard selection, and `AgrarianSelectCharacter male|female` dev command while keeping both choices on the same MVP survival baseline until real character art arrives. - [ ] Add join server screen. - [ ] Add loading screen. - [~] Add HUD. diff --git a/Scripts/verify_mvp_character_archetype_choice.py b/Scripts/verify_mvp_character_archetype_choice.py new file mode 100644 index 0000000..a68f5bf --- /dev/null +++ b/Scripts/verify_mvp_character_archetype_choice.py @@ -0,0 +1,61 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianMvpFrontendWidget.h": ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.h", + "AgrarianMvpFrontendWidget.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp", + "AgrarianGamePlayerController.h": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h", + "AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", + "AGRARIAN_DEVELOPMENT_ROADMAP.md": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md", +} + +EXPECTED = { + "AgrarianMvpFrontendWidget.h": [ + "EAgrarianMvpCharacterArchetype", + "YoungAdultMale", + "YoungAdultFemale", + "SelectedCharacterArchetype", + "SetSelectedCharacterArchetype", + "NativeOnKeyDown", + ], + "AgrarianMvpFrontendWidget.cpp": [ + "EKeys::Left", + "EKeys::Right", + "EKeys::A", + "EKeys::D", + "Selected", + "Use Left/Right or A/D to choose", + "GetSelectedCharacterLabel", + ], + "AgrarianGamePlayerController.h": [ + "AgrarianSelectCharacter", + ], + "AgrarianGamePlayerController.cpp": [ + "AgrarianSelectCharacter", + "SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale)", + "SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale)", + ], + "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Let players choose a realistic young adult male or female character with average proportions for the MVP.", + "`AgrarianSelectCharacter male|female`", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("MVP character archetype choice verification failed: " + "; ".join(missing)) + + print("Agrarian MVP character archetype choice verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 7244a1c..5898357 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -280,6 +280,31 @@ void AAgrarianGamePlayerController::AgrarianCraftStatus() } } +void AAgrarianGamePlayerController::AgrarianSelectCharacter(FName Archetype) +{ + if (!MvpFrontendWidget) + { + ClientMessage(TEXT("No MVP frontend widget is active.")); + return; + } + + if (Archetype == TEXT("male") || Archetype == TEXT("YoungAdultMale")) + { + MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); + ClientMessage(TEXT("Selected MVP young adult male character archetype.")); + return; + } + + if (Archetype == TEXT("female") || Archetype == TEXT("YoungAdultFemale")) + { + MvpFrontendWidget->SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); + ClientMessage(TEXT("Selected MVP young adult female character archetype.")); + return; + } + + ClientMessage(TEXT("Usage: AgrarianSelectCharacter male|female")); +} + void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) { ServerAgrarianTravel(FVector(X, Y, Z)); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index 2c8cadf..64c8d77 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -91,6 +91,9 @@ public: UFUNCTION(Exec) void AgrarianCraftStatus(); + UFUNCTION(Exec) + void AgrarianSelectCharacter(FName Archetype); + UFUNCTION(Exec) void AgrarianTravel(float X, float Y, float Z); diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index cc84d35..e784799 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -2,15 +2,50 @@ #include "AgrarianMvpFrontendWidget.h" +#include "InputCoreTypes.h" #include "Rendering/DrawElements.h" #include "Styling/CoreStyle.h" +void UAgrarianMvpFrontendWidget::NativeConstruct() +{ + Super::NativeConstruct(); + bIsFocusable = true; + SetKeyboardFocus(); +} + +FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) +{ + if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection) + { + const FKey Key = InKeyEvent.GetKey(); + if (Key == EKeys::Left || Key == EKeys::A) + { + SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale); + return FReply::Handled(); + } + + if (Key == EKeys::Right || Key == EKeys::D) + { + SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale); + return FReply::Handled(); + } + } + + return Super::NativeOnKeyDown(InGeometry, InKeyEvent); +} + void UAgrarianMvpFrontendWidget::SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen) { ActiveScreen = NewScreen; InvalidateLayoutAndVolatility(); } +void UAgrarianMvpFrontendWidget::SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype NewArchetype) +{ + SelectedCharacterArchetype = NewArchetype; + InvalidateLayoutAndVolatility(); +} + int32 UAgrarianMvpFrontendWidget::NativePaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, @@ -122,7 +157,7 @@ void UAgrarianMvpFrontendWidget::DrawCharacterSelection( const FVector2D MaleCardPosition(ContentX, PanelPosition.Y + (142.0f * Scale)); const FVector2D FemaleCardPosition(ContentX + CardSize.X + (24.0f * Scale), MaleCardPosition.Y); - auto DrawCharacterCard = [&](const FVector2D& CardPosition, const FText& Title, const FText& Summary, const FLinearColor& AccentColor) + auto DrawCharacterCard = [&](const FVector2D& CardPosition, const FText& Title, const FText& Summary, const FLinearColor& AccentColor, bool bSelected) { FSlateDrawElement::MakeBox( OutDrawElements, @@ -140,6 +175,17 @@ void UAgrarianMvpFrontendWidget::DrawCharacterSelection( ESlateDrawEffect::None, AccentColor); + if (bSelected) + { + FSlateDrawElement::MakeBox( + OutDrawElements, + ++LayerId, + AllottedGeometry.ToPaintGeometry(FVector2f(CardSize.X, CardSize.Y), FSlateLayoutTransform(FVector2f(CardPosition))), + FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")), + ESlateDrawEffect::None, + FLinearColor(AccentColor.R, AccentColor.G, AccentColor.B, 0.16f)); + } + const FVector2D PortraitPosition = CardPosition + FVector2D(24.0f * Scale, 42.0f * Scale); const FVector2D PortraitSize(72.0f * Scale, 112.0f * Scale); FSlateDrawElement::MakeBox( @@ -152,12 +198,20 @@ void UAgrarianMvpFrontendWidget::DrawCharacterSelection( DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, Title, CardPosition + FVector2D(116.0f * Scale, 42.0f * Scale), CardSize.X - (140.0f * Scale), CardTitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f)); DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, Summary, CardPosition + FVector2D(116.0f * Scale, 82.0f * Scale), CardSize.X - (140.0f * Scale), LabelFont, FLinearColor(0.72f, 0.78f, 0.68f, 1.0f)); + DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, bSelected ? FText::FromString(TEXT("Selected")) : FText::FromString(TEXT("Available")), CardPosition + FVector2D(116.0f * Scale, 144.0f * Scale), CardSize.X - (140.0f * Scale), LabelFont, bSelected ? AccentColor : FLinearColor(0.58f, 0.62f, 0.54f, 1.0f)); }; - DrawCharacterCard(MaleCardPosition, FText::FromString(TEXT("Young adult male")), FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FLinearColor(0.36f, 0.58f, 0.78f, 1.0f)); - DrawCharacterCard(FemaleCardPosition, FText::FromString(TEXT("Young adult female")), FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FLinearColor(0.56f, 0.68f, 0.46f, 1.0f)); + DrawCharacterCard(MaleCardPosition, FText::FromString(TEXT("Young adult male")), FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FLinearColor(0.36f, 0.58f, 0.78f, 1.0f), SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultMale); + DrawCharacterCard(FemaleCardPosition, FText::FromString(TEXT("Young adult female")), FText::FromString(TEXT("Average proportions, survival baseline, placeholder visual.")), FLinearColor(0.56f, 0.68f, 0.46f, 1.0f), SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale); - DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Selection is visual-only for this milestone; both choices enter the same MVP survival baseline.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.0f * Scale)), ContentWidth, LabelFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); + DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::Format(FText::FromString(TEXT("Selected: {0}. Use Left/Right or A/D to choose; both choices enter the same MVP survival baseline.")), GetSelectedCharacterLabel()), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (46.0f * Scale)), ContentWidth, LabelFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f)); +} + +FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const +{ + return SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale + ? FText::FromString(TEXT("young adult female")) + : FText::FromString(TEXT("young adult male")); } void UAgrarianMvpFrontendWidget::DrawTextAt( diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 64cc41f..112189e 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -15,6 +15,13 @@ enum class EAgrarianMvpFrontendScreen : uint8 Loading }; +UENUM(BlueprintType) +enum class EAgrarianMvpCharacterArchetype : uint8 +{ + YoungAdultMale, + YoungAdultFemale +}; + UCLASS() class AGRARIANGAME_API UAgrarianMvpFrontendWidget : public UUserWidget { @@ -24,6 +31,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") EAgrarianMvpFrontendScreen ActiveScreen = EAgrarianMvpFrontendScreen::MainMenu; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") + EAgrarianMvpCharacterArchetype SelectedCharacterArchetype = EAgrarianMvpCharacterArchetype::YoungAdultMale; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI") FText MainMenuTitle = FText::FromString(TEXT("Agrarian")); @@ -42,7 +52,14 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI") void SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen); + UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI") + void SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype NewArchetype); + protected: + virtual void NativeConstruct() override; + + virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override; + virtual int32 NativePaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, @@ -69,6 +86,8 @@ private: const FVector2D& PanelSize, float Scale) const; + FText GetSelectedCharacterLabel() const; + void DrawTextAt( FSlateWindowElementList& OutDrawElements, int32& LayerId,