diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index b84cb1b..a4a13c5 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -568,7 +568,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe data assets, using wood, fiber, and hide as MVP ingredients for later placed-storage/container systems. - [x] Add bandage or basic treatment recipe. -- [ ] Add crafting UI. +- [x] Add crafting UI. Added a compact crafting HUD panel that lists known MVP + recipes, shows current ingredient counts, highlights craftable rows, and + preloads the Agrarian player Blueprint with primitive recipe assets. - [x] Add multiplayer authority checks. - [~] Add crafting debug tools. diff --git a/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset b/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset index 2ce7503..55acd0e 100644 --- a/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset +++ b/Content/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8158faf669bcfe01895de54e8e148244aef0124cf37b1c29942d252d4de457a1 -size 46695 +oid sha256:bd25b6e8fc63a663f5bf7f756946f6f938a323a43b846c3abe44d5bf083b4a5a +size 48236 diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 0e3ad2c..eb7ca80 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -147,6 +147,12 @@ array, shows occupied slots, total carried weight, and a short visible stack list, and leaves mutation actions on the existing server-authoritative commands and RPCs until a full UMG inventory screen is introduced. +The MVP crafting UI is a compact `AAgrarianDebugHUD` crafting panel. The +Agrarian player Blueprint preloads primitive `KnownRecipeAssets`, and the HUD +reads those recipes plus the replicated inventory to show craftable status and +ingredient counts. Interactive UMG recipe browsing, hotkeys, and queued +crafting controls are deferred until the primitive loop settles. + Inventory persistence saves `UAgrarianInventoryComponent::Items` into `FAgrarianSavedPlayer::Inventory` and restores through `UAgrarianInventoryComponent::RestoreSavedItems`. Restore broadcasts diff --git a/Scripts/setup_agrarian_player_blueprints.py b/Scripts/setup_agrarian_player_blueprints.py index 035f21e..abdc5eb 100644 --- a/Scripts/setup_agrarian_player_blueprints.py +++ b/Scripts/setup_agrarian_player_blueprints.py @@ -10,6 +10,17 @@ DEST_CHARACTER = f"{DEST_ROOT}/BP_AgrarianPlayerCharacter" DEST_CONTROLLER = f"{DEST_ROOT}/BP_AgrarianPlayerController" DEST_GAME_MODE = f"{DEST_ROOT}/BP_AgrarianGameMode" +KNOWN_RECIPE_ASSETS = [ + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_Campfire", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveFrame", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveWallPanel", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveRoofPanel", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveShelter", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_BasicTool", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_SimpleContainer", + "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_Bandage", +] + def ensure_directory(path): if not unreal.EditorAssetLibrary.does_directory_exist(path): @@ -40,6 +51,13 @@ def load_blueprint_class(path): return generated_class +def load_required_asset(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Required asset not found: {path}") + return asset + + def main(): ensure_directory(DEST_ROOT) @@ -49,11 +67,21 @@ def main(): character_class = load_blueprint_class(DEST_CHARACTER) controller_class = load_blueprint_class(DEST_CONTROLLER) + character_cdo = unreal.get_default_object(character_class) game_mode_cdo = unreal.get_default_object(game_mode_bp.generated_class()) game_mode_cdo.set_editor_property("default_pawn_class", character_class) game_mode_cdo.set_editor_property("player_controller_class", controller_class) + crafting_component = character_cdo.get_editor_property("crafting_component") + if not crafting_component: + raise RuntimeError("BP_AgrarianPlayerCharacter is missing CraftingComponent") + crafting_component.set_editor_property( + "known_recipe_assets", + [load_required_asset(path) for path in KNOWN_RECIPE_ASSETS], + ) + + unreal.BlueprintEditorLibrary.compile_blueprint(character_bp) unreal.EditorAssetLibrary.save_loaded_asset(character_bp) unreal.EditorAssetLibrary.save_loaded_asset(controller_bp) unreal.EditorAssetLibrary.save_loaded_asset(game_mode_bp) diff --git a/Scripts/verify_agrarian_player_blueprints.py b/Scripts/verify_agrarian_player_blueprints.py index 9e29234..0845b33 100644 --- a/Scripts/verify_agrarian_player_blueprints.py +++ b/Scripts/verify_agrarian_player_blueprints.py @@ -4,6 +4,16 @@ import unreal CHARACTER_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerCharacter" CONTROLLER_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianPlayerController" GAME_MODE_BLUEPRINT_PATH = "/Game/Agrarian/Blueprints/Characters/BP_AgrarianGameMode" +EXPECTED_RECIPE_IDS = [ + "campfire", + "primitive_frame", + "primitive_wall_panel", + "primitive_roof_panel", + "primitive_shelter", + "basic_tool", + "simple_container", + "bandage", +] def class_path(value): @@ -48,6 +58,19 @@ def main(): character_cdo = unreal.get_default_object(character_class) if not character_cdo: failures.append(f"{CHARACTER_BLUEPRINT_PATH} CDO missing") + else: + crafting_component = character_cdo.get_editor_property("crafting_component") + recipe_assets = crafting_component.get_editor_property("known_recipe_assets") if crafting_component else [] + recipe_ids = [ + str(asset.get_editor_property("recipe").get_editor_property("recipe_id")) + for asset in recipe_assets + if asset + ] + if recipe_ids != EXPECTED_RECIPE_IDS: + failures.append( + "BP_AgrarianPlayerCharacter known recipes expected " + f"{EXPECTED_RECIPE_IDS}, got {recipe_ids}" + ) controller_cdo = unreal.get_default_object(controller_class) if not controller_cdo: @@ -63,4 +86,3 @@ def main(): main() - diff --git a/Scripts/verify_crafting_ui.py b/Scripts/verify_crafting_ui.py new file mode 100644 index 0000000..68d87a9 --- /dev/null +++ b/Scripts/verify_crafting_ui.py @@ -0,0 +1,63 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.h": [ + "bool bShowCraftingHUD = true;", + "int32 MaxCraftingPanelRows = 8;", + "void DrawCraftingPanel", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp": [ + "DrawCraftingPanel(AgrarianCharacter", + "CraftingComponent->GetKnownRecipes(Recipes)", + "CraftingComponent->CanCraft(Recipe.RecipeId, FailureReason)", + "InventoryComponent->GetItemCount(Ingredient.ItemId)", + "CRAFTING %d recipes", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianCraftingComponent.h": [ + "void GetKnownRecipes(TArray& OutRecipes) const;", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianCraftingComponent.cpp": [ + "void UAgrarianCraftingComponent::GetKnownRecipes", + "RecipeAsset->Recipe.RecipeId != NAME_None", + "OutRecipes.Add(RecipeAsset->Recipe)", + ], + ROOT / "Scripts" / "setup_agrarian_player_blueprints.py": [ + "KNOWN_RECIPE_ASSETS", + "DA_Recipe_SimpleContainer", + "known_recipe_assets", + ], + ROOT / "Scripts" / "verify_agrarian_player_blueprints.py": [ + "EXPECTED_RECIPE_IDS", + "simple_container", + "known_recipe_assets", + ], + ROOT / "Docs" / "TechnicalDesignDocument.md": [ + "The MVP crafting UI is a compact `AAgrarianDebugHUD` crafting panel", + "`KnownRecipeAssets`", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "- [x] Add crafting UI.", + "compact crafting HUD panel", + ], +} + + +def main(): + missing = [] + for path, snippets in EXPECTED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}") + + if missing: + raise RuntimeError("Crafting UI verification failed: " + "; ".join(missing)) + + print("PASS: crafting UI HUD panel and known recipe wiring are present.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianCraftingComponent.cpp b/Source/AgrarianGame/AgrarianCraftingComponent.cpp index 34189b9..c81f785 100644 --- a/Source/AgrarianGame/AgrarianCraftingComponent.cpp +++ b/Source/AgrarianGame/AgrarianCraftingComponent.cpp @@ -133,6 +133,27 @@ bool UAgrarianCraftingComponent::FindRecipe(FName RecipeId, FAgrarianRecipe& Out return false; } +void UAgrarianCraftingComponent::GetKnownRecipes(TArray& OutRecipes) const +{ + OutRecipes.Reset(); + + for (const UAgrarianRecipeDataAsset* RecipeAsset : KnownRecipeAssets) + { + if (RecipeAsset && RecipeAsset->Recipe.RecipeId != NAME_None) + { + OutRecipes.Add(RecipeAsset->Recipe); + } + } + + for (const FAgrarianRecipe& Recipe : KnownRecipes) + { + if (Recipe.RecipeId != NAME_None) + { + OutRecipes.Add(Recipe); + } + } +} + UAgrarianInventoryComponent* UAgrarianCraftingComponent::GetInventory() const { return GetOwner() ? GetOwner()->FindComponentByClass() : nullptr; diff --git a/Source/AgrarianGame/AgrarianCraftingComponent.h b/Source/AgrarianGame/AgrarianCraftingComponent.h index 7bda54b..703457b 100644 --- a/Source/AgrarianGame/AgrarianCraftingComponent.h +++ b/Source/AgrarianGame/AgrarianCraftingComponent.h @@ -48,6 +48,9 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Crafting") bool FindRecipe(FName RecipeId, FAgrarianRecipe& OutRecipe) const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Crafting") + void GetKnownRecipes(TArray& OutRecipes) const; + protected: UAgrarianInventoryComponent* GetInventory() const; void FailCraft(FName RecipeId, const FText& Reason); diff --git a/Source/AgrarianGame/AgrarianDebugHUD.cpp b/Source/AgrarianGame/AgrarianDebugHUD.cpp index 3debf7c..c01883b 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.cpp +++ b/Source/AgrarianGame/AgrarianDebugHUD.cpp @@ -1,6 +1,7 @@ // Copyright Pacificao. All Rights Reserved. #include "AgrarianDebugHUD.h" +#include "AgrarianCraftingComponent.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AgrarianSurvivalComponent.h" @@ -24,7 +25,8 @@ void AAgrarianDebugHUD::DrawHUD() DrawInteractionPrompt(AgrarianCharacter); DrawCriticalStats(AgrarianCharacter->GetSurvivalComponent()); - DrawInventoryPanel(AgrarianCharacter); + const float InventoryBottomY = DrawInventoryPanel(AgrarianCharacter); + DrawCraftingPanel(AgrarianCharacter, InventoryBottomY + 16.0f); if (bShowDebugHUD) { @@ -110,17 +112,17 @@ void AAgrarianDebugHUD::DrawCriticalStats(const UAgrarianSurvivalComponent* Surv DrawScaledLine(FString::Printf(TEXT("Sickness %3.0f"), Survival.SicknessSeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.SicknessSeverity, true)); } -void AAgrarianDebugHUD::DrawInventoryPanel(const AAgrarianGameCharacter* AgrarianCharacter) +float AAgrarianDebugHUD::DrawInventoryPanel(const AAgrarianGameCharacter* AgrarianCharacter) { if (!bShowInventoryHUD || !AgrarianCharacter || !Canvas) { - return; + return 32.0f; } const UAgrarianInventoryComponent* InventoryComponent = AgrarianCharacter->GetInventoryComponent(); if (!InventoryComponent) { - return; + return 32.0f; } const float Scale = FMath::Max(0.25f, InventoryTextScale); @@ -152,7 +154,7 @@ void AAgrarianDebugHUD::DrawInventoryPanel(const AAgrarianGameCharacter* Agraria if (InventoryComponent->Items.IsEmpty()) { DrawText(TEXT("Empty"), FColor::Silver, X, Y, nullptr, Scale, false); - return; + return (32.0f + PanelHeight); } for (int32 Index = 0; Index < VisibleRows; ++Index) @@ -175,6 +177,95 @@ void AAgrarianDebugHUD::DrawInventoryPanel(const AAgrarianGameCharacter* Agraria false); Y += LineHeight; } + + return (32.0f + PanelHeight); +} + +void AAgrarianDebugHUD::DrawCraftingPanel(const AAgrarianGameCharacter* AgrarianCharacter, float TopY) +{ + if (!bShowCraftingHUD || !AgrarianCharacter || !Canvas) + { + return; + } + + const UAgrarianCraftingComponent* CraftingComponent = AgrarianCharacter->GetCraftingComponent(); + const UAgrarianInventoryComponent* InventoryComponent = AgrarianCharacter->GetInventoryComponent(); + if (!CraftingComponent || !InventoryComponent) + { + return; + } + + TArray Recipes; + CraftingComponent->GetKnownRecipes(Recipes); + + const float Scale = FMath::Max(0.25f, InventoryTextScale); + const float PanelWidth = 420.0f * Scale; + const float X = FMath::Max(32.0f, Canvas->ClipX - PanelWidth - 32.0f); + float Y = FMath::Max(32.0f, TopY); + + const int32 VisibleRows = Recipes.IsEmpty() + ? 1 + : FMath::Min(Recipes.Num(), FMath::Max(1, MaxCraftingPanelRows)); + const float LineHeight = 18.0f * Scale; + const float PanelHeight = (56.0f * Scale) + (VisibleRows * LineHeight); + + DrawRect(FLinearColor(0.02f, 0.025f, 0.02f, 0.72f), X - (12.0f * Scale), Y - (10.0f * Scale), PanelWidth, PanelHeight); + DrawText( + FString::Printf(TEXT("CRAFTING %d recipes"), Recipes.Num()), + FColor(160, 220, 140), + X, + Y, + nullptr, + Scale, + false); + Y += 24.0f * Scale; + + if (Recipes.IsEmpty()) + { + DrawText(TEXT("No known recipes"), FColor::Silver, X, Y, nullptr, Scale, false); + return; + } + + for (int32 Index = 0; Index < VisibleRows; ++Index) + { + const FAgrarianRecipe& Recipe = Recipes[Index]; + FText FailureReason; + const bool bCanCraft = CraftingComponent->CanCraft(Recipe.RecipeId, FailureReason); + FString RecipeName = Recipe.DisplayName.IsEmpty() ? Recipe.RecipeId.ToString() : Recipe.DisplayName.ToString(); + if (RecipeName.Len() > 18) + { + RecipeName = RecipeName.Left(15) + TEXT("..."); + } + + FString IngredientSummary; + for (const FAgrarianItemStack& Ingredient : Recipe.Ingredients) + { + if (!IngredientSummary.IsEmpty()) + { + IngredientSummary += TEXT(" "); + } + IngredientSummary += FString::Printf( + TEXT("%s %d/%d"), + *Ingredient.ItemId.ToString(), + InventoryComponent->GetItemCount(Ingredient.ItemId), + Ingredient.Quantity); + } + + if (IngredientSummary.Len() > 28) + { + IngredientSummary = IngredientSummary.Left(25) + TEXT("..."); + } + + DrawText( + FString::Printf(TEXT("%02d %-18s %-28s"), Index + 1, *RecipeName, *IngredientSummary), + bCanCraft ? FColor(225, 235, 220) : FColor(170, 150, 125), + X, + Y, + nullptr, + Scale, + false); + Y += LineHeight; + } } void AAgrarianDebugHUD::DrawPlayerStatus(const AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y) diff --git a/Source/AgrarianGame/AgrarianDebugHUD.h b/Source/AgrarianGame/AgrarianDebugHUD.h index 5f112c9..7d72532 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.h +++ b/Source/AgrarianGame/AgrarianDebugHUD.h @@ -7,6 +7,7 @@ #include "AgrarianDebugHUD.generated.h" class UAgrarianInventoryComponent; +class UAgrarianCraftingComponent; class UAgrarianSurvivalComponent; UCLASS() @@ -26,6 +27,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") bool bShowInventoryHUD = true; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") + bool bShowCraftingHUD = true; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25")) float TextScale = 1.0f; @@ -38,6 +42,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "1", ClampMax = "12")) int32 MaxInventoryPanelRows = 6; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "1", ClampMax = "12")) + int32 MaxCraftingPanelRows = 8; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") bool bShowInteractionPrompt = true; @@ -47,7 +54,8 @@ public: protected: void DrawInteractionPrompt(const class AAgrarianGameCharacter* AgrarianCharacter); void DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent); - void DrawInventoryPanel(const class AAgrarianGameCharacter* AgrarianCharacter); + float DrawInventoryPanel(const class AAgrarianGameCharacter* AgrarianCharacter); + void DrawCraftingPanel(const class AAgrarianGameCharacter* AgrarianCharacter, float TopY); void DrawPlayerStatus(const class AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y); void DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y); void DrawInventory(const UAgrarianInventoryComponent* InventoryComponent, float X, float& Y);