diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index d0e06f0..4598177 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -378,7 +378,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Implement crouching and prone movement stances. Decision: `C` toggles crouch at `55%` movement speed and `Z` toggles prone at `25%` movement speed. Gamepad mappings are Right Shoulder for crouch and Left Shoulder for prone. Sprint is disabled while crouched or prone. - [x] Implement jumping if needed. - [x] Implement interaction trace. -- [ ] Implement interact prompt. +- [x] Implement interact prompt. Implemented as a local trace-backed HUD prompt using each interactable's `GetInteractionText`, rendered as `[E] ` through the Agrarian HUD. - [ ] Implement basic animation blueprint. - [x] Implement placeholder character mesh. - [~] Add replication for player movement and core state. @@ -1465,4 +1465,4 @@ Earliest incomplete foundation items: Immediate next item: -- [ ] Implement interact prompt. +- [ ] Implement basic animation blueprint. diff --git a/Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset b/Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset index 0779329..649362b 100644 Binary files a/Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset and b/Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset differ diff --git a/Scripts/setup_interaction_prompt.py b/Scripts/setup_interaction_prompt.py new file mode 100644 index 0000000..77f9b30 --- /dev/null +++ b/Scripts/setup_interaction_prompt.py @@ -0,0 +1,24 @@ +import unreal + + +def load(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Could not load {path}") + return asset + + +def main(): + game_mode_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode") + game_mode_cdo = unreal.get_default_object(game_mode_bp.generated_class()) + hud_class = unreal.load_class(None, "/Script/AgrarianGame.AgrarianDebugHUD") + if not hud_class: + raise RuntimeError("Could not load /Script/AgrarianGame.AgrarianDebugHUD") + + game_mode_cdo.set_editor_property("hud_class", hud_class) + unreal.EditorAssetLibrary.save_loaded_asset(game_mode_bp) + + unreal.log("Agrarian interaction prompt setup complete.") + + +main() diff --git a/Scripts/verify_interaction_prompt.py b/Scripts/verify_interaction_prompt.py new file mode 100644 index 0000000..2533598 --- /dev/null +++ b/Scripts/verify_interaction_prompt.py @@ -0,0 +1,42 @@ +import unreal + + +def load(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Could not load {path}") + return asset + + +def main(): + game_mode_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode") + game_mode_cdo = unreal.get_default_object(game_mode_bp.generated_class()) + expected_hud_class = unreal.load_class(None, "/Script/AgrarianGame.AgrarianDebugHUD") + character_class = unreal.load_class(None, "/Script/AgrarianGame.AgrarianGameCharacter") + hud_cdo = unreal.get_default_object(expected_hud_class) if expected_hud_class else None + character_cdo = unreal.get_default_object(character_class) if character_class else None + + missing = [] + if not expected_hud_class: + missing.append("could not load AgrarianDebugHUD class") + elif game_mode_cdo.get_editor_property("hud_class") != expected_hud_class: + missing.append("BP_ThirdPersonGameMode HUD class is not AgrarianDebugHUD") + elif hud_cdo: + if not bool(hud_cdo.get_editor_property("bShowInteractionPrompt")): + missing.append("AgrarianDebugHUD bShowInteractionPrompt is disabled") + if float(hud_cdo.get_editor_property("PromptTextScale")) <= 0.0: + missing.append("AgrarianDebugHUD PromptTextScale is not positive") + + if not character_class: + missing.append("could not load AgrarianGameCharacter class") + elif character_cdo: + character_cdo.get_editor_property("InteractionPromptText") + character_cdo.get_editor_property("FocusedInteractableActor") + + if missing: + raise RuntimeError("Interaction prompt verification failed: " + "; ".join(missing)) + + unreal.log("Agrarian interaction prompt verification complete.") + + +main() diff --git a/Source/AgrarianGame/AgrarianDebugHUD.cpp b/Source/AgrarianGame/AgrarianDebugHUD.cpp index 4b21374..2a58213 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.cpp +++ b/Source/AgrarianGame/AgrarianDebugHUD.cpp @@ -4,12 +4,13 @@ #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AgrarianSurvivalComponent.h" +#include "Engine/Canvas.h" void AAgrarianDebugHUD::DrawHUD() { Super::DrawHUD(); - if (!bShowDebugHUD || !Canvas) + if (!Canvas) { return; } @@ -20,12 +21,34 @@ void AAgrarianDebugHUD::DrawHUD() return; } - float Y = 32.0f; - constexpr float X = 32.0f; + DrawInteractionPrompt(AgrarianCharacter); - DrawLine(TEXT("AGRARIAN DEV HUD"), X, Y, FColor(160, 220, 140)); - DrawSurvival(AgrarianCharacter->GetSurvivalComponent(), X, Y); - DrawInventory(AgrarianCharacter->GetInventoryComponent(), X, Y); + if (bShowDebugHUD) + { + float Y = 32.0f; + constexpr float X = 32.0f; + + DrawLine(TEXT("AGRARIAN DEV HUD"), X, Y, FColor(160, 220, 140)); + 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::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y) diff --git a/Source/AgrarianGame/AgrarianDebugHUD.h b/Source/AgrarianGame/AgrarianDebugHUD.h index cd5f7a1..02da66c 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.h +++ b/Source/AgrarianGame/AgrarianDebugHUD.h @@ -23,7 +23,14 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25")) float TextScale = 1.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD") + bool bShowInteractionPrompt = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25")) + float PromptTextScale = 1.15f; + protected: + void DrawInteractionPrompt(const class AAgrarianGameCharacter* AgrarianCharacter); void DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y); void DrawInventory(const UAgrarianInventoryComponent* InventoryComponent, float X, float& Y); void DrawLine(const FString& Text, float X, float& Y, const FColor& Color = FColor::White); diff --git a/Source/AgrarianGame/AgrarianGameCharacter.cpp b/Source/AgrarianGame/AgrarianGameCharacter.cpp index 613d56c..6223337 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.cpp +++ b/Source/AgrarianGame/AgrarianGameCharacter.cpp @@ -69,6 +69,11 @@ void AAgrarianGameCharacter::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); + if (IsLocallyControlled()) + { + UpdateInteractionPrompt(); + } + ApplyMovementSpeed(); if (!HasAuthority() || !bWantsToSprint) @@ -498,6 +503,36 @@ void AAgrarianGameCharacter::DoJumpEnd() } void AAgrarianGameCharacter::TryInteract() +{ + AActor* HitActor = FindFocusedInteractable(); + if (!HitActor) + { + return; + } + + if (HasAuthority()) + { + IAgrarianInteractable::Execute_Interact(HitActor, this); + } + else + { + ServerInteract(HitActor); + } +} + +bool AAgrarianGameCharacter::HasInteractionPrompt() const +{ + return FocusedInteractableActor != nullptr && !InteractionPromptText.IsEmpty(); +} + +void AAgrarianGameCharacter::UpdateInteractionPrompt() +{ + FText NewPromptText; + FocusedInteractableActor = FindFocusedInteractable(&NewPromptText); + InteractionPromptText = FocusedInteractableActor ? NewPromptText : FText::GetEmpty(); +} + +AActor* AAgrarianGameCharacter::FindFocusedInteractable(FText* OutPromptText) const { FVector TraceStart; FRotator TraceRotation; @@ -518,26 +553,26 @@ void AAgrarianGameCharacter::TryInteract() if (!GetWorld() || !GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params)) { - return; + return nullptr; } AActor* HitActor = Hit.GetActor(); if (!HitActor || !HitActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass())) { - return; + return nullptr; } - if (HasAuthority()) + if (!IAgrarianInteractable::Execute_CanInteract(HitActor, this)) { - if (IAgrarianInteractable::Execute_CanInteract(HitActor, this)) - { - IAgrarianInteractable::Execute_Interact(HitActor, this); - } + return nullptr; } - else + + if (OutPromptText) { - ServerInteract(HitActor); + *OutPromptText = IAgrarianInteractable::Execute_GetInteractionText(HitActor, this); } + + return HitActor; } void AAgrarianGameCharacter::ServerSetWantsToSprint_Implementation(bool bNewWantsToSprint) diff --git a/Source/AgrarianGame/AgrarianGameCharacter.h b/Source/AgrarianGame/AgrarianGameCharacter.h index 758be0e..48938e7 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.h +++ b/Source/AgrarianGame/AgrarianGameCharacter.h @@ -93,6 +93,14 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Interaction", meta = (ClampMin = "100")) float InteractionDistance = 450.0f; + /** Actor currently under the local interaction trace. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Interaction", meta = (AllowPrivateAccess = "true")) + TObjectPtr FocusedInteractableActor = nullptr; + + /** Prompt text for the currently focused interactable. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Interaction", meta = (AllowPrivateAccess = "true")) + FText InteractionPromptText; + /** Baseline movement speed before sprint, skill, injury, load, and terrain modifiers. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Movement", meta = (ClampMin = "0")) float WalkSpeed = 140.0f; @@ -252,6 +260,18 @@ public: UFUNCTION(BlueprintCallable, Category="Agrarian|Interaction") virtual void TryInteract(); + /** Returns true when the local interaction trace has a usable target. */ + UFUNCTION(BlueprintPure, Category="Agrarian|Interaction") + bool HasInteractionPrompt() const; + + /** Returns the current local interaction prompt text. */ + UFUNCTION(BlueprintPure, Category="Agrarian|Interaction") + FText GetInteractionPromptText() const { return InteractionPromptText; } + + /** Returns the actor currently under the local interaction trace, if any. */ + UFUNCTION(BlueprintPure, Category="Agrarian|Interaction") + AActor* GetFocusedInteractableActor() const { return FocusedInteractableActor; } + /** Returns true when this local character is using first-person camera presentation. */ UFUNCTION(BlueprintPure, Category="Agrarian|Camera") bool IsFirstPersonCamera() const { return bFirstPersonCamera; } @@ -287,6 +307,12 @@ public: UFUNCTION(Server, Reliable) void ServerSetProne(bool bNewProne); + /** Refreshes local interactable focus and prompt text. */ + void UpdateInteractionPrompt(); + + /** Finds a valid interactable in front of this character. */ + AActor* FindFocusedInteractable(FText* OutPromptText = nullptr) const; + public: /** Returns CameraBoom subobject **/ diff --git a/Source/AgrarianGame/AgrarianGameGameMode.cpp b/Source/AgrarianGame/AgrarianGameGameMode.cpp index 998cf69..9d66d14 100644 --- a/Source/AgrarianGame/AgrarianGameGameMode.cpp +++ b/Source/AgrarianGame/AgrarianGameGameMode.cpp @@ -1,9 +1,11 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "AgrarianGameGameMode.h" +#include "AgrarianDebugHUD.h" #include "AgrarianGameState.h" AAgrarianGameGameMode::AAgrarianGameGameMode() { GameStateClass = AAgrarianGameState::StaticClass(); + HUDClass = AAgrarianDebugHUD::StaticClass(); }