From 3132ed462e2b0248189ac64f1fc006db4e5d5753 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 15 May 2026 15:43:37 -0700 Subject: [PATCH] Implement interaction prompt --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- .../Blueprints/BP_ThirdPersonGameMode.uasset | Bin 130 -> 21794 bytes Scripts/setup_interaction_prompt.py | 24 ++++++++ Scripts/verify_interaction_prompt.py | 42 ++++++++++++++ Source/AgrarianGame/AgrarianDebugHUD.cpp | 35 ++++++++++-- Source/AgrarianGame/AgrarianDebugHUD.h | 7 +++ Source/AgrarianGame/AgrarianGameCharacter.cpp | 53 +++++++++++++++--- Source/AgrarianGame/AgrarianGameCharacter.h | 26 +++++++++ Source/AgrarianGame/AgrarianGameGameMode.cpp | 2 + 9 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 Scripts/setup_interaction_prompt.py create mode 100644 Scripts/verify_interaction_prompt.py 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 07793294d751c045a961f36874301df83dfbb6ed..649362b6417b851b6f7b5135119d0ac739048beb 100644 GIT binary patch literal 21794 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 literal 130 zcmWN?K@x)?3;@8pukZtv5->!6Lm;WlwAE?QLto$SWiRKShvqOrz{IIt=+LmF)HOz3Yh_g}wWhTj?212S_6_5lo5r+t>f~=gClTS$(@d+fw NGFbb{2tF-w_yGGetSurvivalComponent(), 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(); }