// Copyright Pacificao. All Rights Reserved. #include "AgrarianDebugHUD.h" #include "AgrarianCraftingComponent.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AgrarianSurvivalComponent.h" #include "Engine/Canvas.h" #include "GameFramework/CharacterMovementComponent.h" void AAgrarianDebugHUD::DrawHUD() { Super::DrawHUD(); if (!Canvas) { return; } const AAgrarianGameCharacter* AgrarianCharacter = Cast(GetOwningPawn()); if (!AgrarianCharacter) { return; } DrawInteractionPrompt(AgrarianCharacter); DrawCriticalStats(AgrarianCharacter->GetSurvivalComponent()); const float InventoryBottomY = DrawInventoryPanel(AgrarianCharacter); DrawCraftingPanel(AgrarianCharacter, InventoryBottomY + 16.0f); if (bShowDebugHUD) { float Y = 32.0f; constexpr float X = 32.0f; DrawLine(TEXT("AGRARIAN DEV HUD"), X, Y, FColor(160, 220, 140)); DrawPlayerStatus(AgrarianCharacter, X, Y); DrawSurvival(AgrarianCharacter->GetSurvivalComponent(), X, Y); DrawInventory(AgrarianCharacter->GetInventoryComponent(), X, Y); } } void AAgrarianDebugHUD::DrawInteractionPrompt(const AAgrarianGameCharacter* AgrarianCharacter) { if (!bShowInteractionPrompt || !AgrarianCharacter || !AgrarianCharacter->HasInteractionPrompt() || !Canvas) { return; } const FString Prompt = FString::Printf(TEXT("[E] %s"), *AgrarianCharacter->GetInteractionPromptText().ToString()); float TextWidth = 0.0f; float TextHeight = 0.0f; GetTextSize(Prompt, TextWidth, TextHeight, nullptr, PromptTextScale); const float X = (Canvas->ClipX - TextWidth) * 0.5f; const float Y = Canvas->ClipY * 0.62f; DrawText(Prompt, FColor(245, 245, 225), X, Y, nullptr, PromptTextScale, true); } void AAgrarianDebugHUD::DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent) { if (!bShowCriticalStatsHUD || !SurvivalComponent || !Canvas) { return; } const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival; float Y = FMath::Max(32.0f, Canvas->ClipY - (178.0f * CriticalStatsTextScale)); constexpr float X = 32.0f; const FColor CriticalColor(230, 90, 70); const FColor WarningColor(245, 190, 80); const FColor StableColor(225, 235, 220); const FColor HeaderColor(160, 220, 140); auto StatusColor = [CriticalColor, WarningColor, StableColor](float Value, bool bHigherIsWorse = false) { if (bHigherIsWorse) { if (Value >= 75.0f) { return CriticalColor; } if (Value >= 40.0f) { return WarningColor; } return StableColor; } if (Value <= 20.0f) { return CriticalColor; } if (Value <= 45.0f) { return WarningColor; } return StableColor; }; DrawScaledLine(TEXT("SURVIVAL"), X, Y, CriticalStatsTextScale, HeaderColor); DrawScaledLine(FString::Printf(TEXT("Health %3.0f"), Survival.Health), X, Y, CriticalStatsTextScale, StatusColor(Survival.Health)); DrawScaledLine(FString::Printf(TEXT("Stamina %3.0f"), Survival.Stamina), X, Y, CriticalStatsTextScale, StatusColor(Survival.Stamina)); DrawScaledLine(FString::Printf(TEXT("Food %3.0f"), Survival.Hunger), X, Y, CriticalStatsTextScale, StatusColor(Survival.Hunger)); DrawScaledLine(FString::Printf(TEXT("Water %3.0f"), Survival.Thirst), X, Y, CriticalStatsTextScale, StatusColor(Survival.Thirst)); DrawScaledLine(FString::Printf(TEXT("Temp %4.1f C"), Survival.BodyTemperature), X, Y, CriticalStatsTextScale, Survival.BodyTemperature < 35.0f ? CriticalColor : StableColor); DrawScaledLine(FString::Printf(TEXT("Shelter %3.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y, CriticalStatsTextScale, SurvivalComponent->CurrentWeatherProtection > 0.0f ? StableColor : WarningColor); DrawScaledLine(FString::Printf(TEXT("Expose x%.2f %+3.1f C"), SurvivalComponent->CurrentWeatherExposureMultiplier, SurvivalComponent->CurrentWeatherTemperatureOffsetC), X, Y, CriticalStatsTextScale, SurvivalComponent->CurrentWeatherExposureMultiplier > 1.0f ? WarningColor : StableColor); DrawScaledLine(FString::Printf(TEXT("Exhaust %3.0f"), Survival.Exhaustion), X, Y, CriticalStatsTextScale, StatusColor(Survival.Exhaustion, true)); DrawScaledLine(FString::Printf(TEXT("Injury %3.0f"), Survival.InjurySeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.InjurySeverity, true)); DrawScaledLine(FString::Printf(TEXT("Sickness %3.0f"), Survival.SicknessSeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.SicknessSeverity, true)); } float AAgrarianDebugHUD::DrawInventoryPanel(const AAgrarianGameCharacter* AgrarianCharacter) { if (!bShowInventoryHUD || !AgrarianCharacter || !Canvas) { return 32.0f; } const UAgrarianInventoryComponent* InventoryComponent = AgrarianCharacter->GetInventoryComponent(); if (!InventoryComponent) { return 32.0f; } const float Scale = FMath::Max(0.25f, InventoryTextScale); const float PanelWidth = 360.0f * Scale; const float X = FMath::Max(32.0f, Canvas->ClipX - PanelWidth - 32.0f); float Y = 32.0f; const int32 VisibleRows = InventoryComponent->Items.IsEmpty() ? 1 : FMath::Min(InventoryComponent->Items.Num(), FMath::Max(1, MaxInventoryPanelRows)); 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("INVENTORY %d/%d slots %.1f wt"), InventoryComponent->Items.Num(), InventoryComponent->MaxSlots, InventoryComponent->GetTotalWeight()), FColor(160, 220, 140), X, Y, nullptr, Scale, false); Y += 24.0f * Scale; if (InventoryComponent->Items.IsEmpty()) { DrawText(TEXT("Empty"), FColor::Silver, X, Y, nullptr, Scale, false); return (32.0f + PanelHeight); } for (int32 Index = 0; Index < VisibleRows; ++Index) { const FAgrarianItemStack& Stack = InventoryComponent->Items[Index]; const FText DisplayName = Stack.DisplayName.IsEmpty() ? FText::FromName(Stack.ItemId) : Stack.DisplayName; FString ItemName = DisplayName.ToString(); if (ItemName.Len() > 24) { ItemName = ItemName.Left(21) + TEXT("..."); } DrawText( FString::Printf(TEXT("%02d %-24s x%-3d %5.1f"), Index + 1, *ItemName, Stack.Quantity, Stack.UnitWeight * Stack.Quantity), FColor(225, 235, 220), X, Y, nullptr, Scale, 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) { if (!AgrarianCharacter) { DrawLine(TEXT("Player: unavailable"), X, Y, FColor::Red); return; } const UCharacterMovementComponent* MovementComponent = AgrarianCharacter->GetCharacterMovement(); const FVector Location = AgrarianCharacter->GetActorLocation(); const float HorizontalSpeed = AgrarianCharacter->GetVelocity().Size2D(); const FString MovementMode = MovementComponent ? UEnum::GetValueAsString(MovementComponent->MovementMode) : TEXT("Unavailable"); DrawLine(FString::Printf( TEXT("Role: %s | Local: %s"), AgrarianCharacter->HasAuthority() ? TEXT("Authority") : TEXT("Simulated/Autonomous"), AgrarianCharacter->IsLocallyControlled() ? TEXT("yes") : TEXT("no")), X, Y, FColor(160, 220, 140)); DrawLine(FString::Printf(TEXT("Location: X %.0f Y %.0f Z %.0f"), Location.X, Location.Y, Location.Z), X, Y); DrawLine(FString::Printf(TEXT("Speed: %.0f uu/s | Mode: %s"), HorizontalSpeed, *MovementMode), X, Y); DrawLine(FString::Printf( TEXT("Stance: %s | Sprint: %s | Camera: %s"), AgrarianCharacter->IsProne() ? TEXT("Prone") : (AgrarianCharacter->bIsCrouched ? TEXT("Crouched") : TEXT("Standing")), AgrarianCharacter->IsSprinting() ? TEXT("yes") : TEXT("no"), AgrarianCharacter->IsFirstPersonCamera() ? TEXT("First") : TEXT("Third")), X, Y); DrawLine(FString::Printf( TEXT("Move Mult: %.2f | Carry: %.1f"), AgrarianCharacter->GetCurrentMovementSpeedMultiplier(), AgrarianCharacter->GetCurrentCarryWeight()), X, Y); DrawLine(FString::Printf( TEXT("Age %.1f | Cond %.2f | Str %.2f | End %.2f | Terrain %.2f"), AgrarianCharacter->GetAgeYears(), AgrarianCharacter->GetPhysicalConditionMultiplier(), AgrarianCharacter->GetStrengthMultiplier(), AgrarianCharacter->GetEnduranceMultiplier(), AgrarianCharacter->GetTerrainMovementMultiplier()), X, Y); Y += 10.0f * TextScale; } void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y) { if (!SurvivalComponent) { DrawLine(TEXT("Survival: unavailable"), X, Y, FColor::Red); return; } const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival; DrawLine(FString::Printf(TEXT("Health: %.0f"), Survival.Health), X, Y); DrawLine(FString::Printf(TEXT("Stamina: %.0f"), Survival.Stamina), X, Y); DrawLine(FString::Printf(TEXT("Exhaust: %.0f"), Survival.Exhaustion), X, Y); DrawLine(FString::Printf(TEXT("Hunger: %.0f"), Survival.Hunger), X, Y); DrawLine(FString::Printf(TEXT("Thirst: %.0f"), Survival.Thirst), X, Y); DrawLine(FString::Printf(TEXT("Temp: %.1f C"), Survival.BodyTemperature), X, Y); DrawLine(FString::Printf(TEXT("Shelter: %.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y); DrawLine(FString::Printf(TEXT("Expose: x%.2f %+3.1f C"), SurvivalComponent->CurrentWeatherExposureMultiplier, SurvivalComponent->CurrentWeatherTemperatureOffsetC), X, Y); DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y); DrawLine(FString::Printf(TEXT("Sick: %.0f"), Survival.SicknessSeverity), X, Y); const FAgrarianCareHistorySnapshot& Care = SurvivalComponent->CareHistory; DrawLine(FString::Printf(TEXT("Care N/S/T: %.2f %.2f %.2f"), Care.NutritionQuality, Care.SleepQuality, Care.TreatmentQuality), X, Y, FColor::Silver); DrawLine(FString::Printf(TEXT("Burden I/I/S/W: %.2f %.2f %.2f %.2f"), Care.IllnessBurden, Care.InjuryBurden, Care.StressBurden, Care.WorkloadBurden), X, Y, FColor::Silver); Y += 10.0f * TextScale; } void AAgrarianDebugHUD::DrawInventory(const UAgrarianInventoryComponent* InventoryComponent, float X, float& Y) { if (!InventoryComponent) { DrawLine(TEXT("Inventory: unavailable"), X, Y, FColor::Red); return; } DrawLine(FString::Printf(TEXT("Inventory: %d/%d slots"), InventoryComponent->Items.Num(), InventoryComponent->MaxSlots), X, Y, FColor(160, 220, 140)); if (InventoryComponent->Items.IsEmpty()) { DrawLine(TEXT("- empty"), X, Y, FColor::Silver); return; } for (const FAgrarianItemStack& Stack : InventoryComponent->Items) { const FText DisplayName = Stack.DisplayName.IsEmpty() ? FText::FromName(Stack.ItemId) : Stack.DisplayName; DrawLine(FString::Printf(TEXT("- %s x%d"), *DisplayName.ToString(), Stack.Quantity), X, Y); } } void AAgrarianDebugHUD::DrawLine(const FString& Text, float X, float& Y, const FColor& Color) { DrawScaledLine(Text, X, Y, TextScale, Color); } void AAgrarianDebugHUD::DrawScaledLine(const FString& Text, float X, float& Y, float Scale, const FColor& Color) { DrawText(Text, Color, X, Y, nullptr, Scale, false); Y += 18.0f * Scale; }