diff --git a/Content/Testing/BP_Campfire.uasset b/Content/Testing/BP_Campfire.uasset index 332628c..952a409 100644 Binary files a/Content/Testing/BP_Campfire.uasset and b/Content/Testing/BP_Campfire.uasset differ diff --git a/Content/UI/WBP_InteractionOptionEntry.uasset b/Content/UI/WBP_InteractionOptionEntry.uasset index 2d6444d..9e34e30 100644 Binary files a/Content/UI/WBP_InteractionOptionEntry.uasset and b/Content/UI/WBP_InteractionOptionEntry.uasset differ diff --git a/Content/UI/WBP_InteractionQuick.uasset b/Content/UI/WBP_InteractionQuick.uasset index 6a66df8..57974fc 100644 Binary files a/Content/UI/WBP_InteractionQuick.uasset and b/Content/UI/WBP_InteractionQuick.uasset differ diff --git a/Source/KingshearthLegacy/Interaction/InteractionComponent.cpp b/Source/KingshearthLegacy/Interaction/InteractionComponent.cpp index 674e1df..4f85289 100644 --- a/Source/KingshearthLegacy/Interaction/InteractionComponent.cpp +++ b/Source/KingshearthLegacy/Interaction/InteractionComponent.cpp @@ -164,11 +164,13 @@ void UInteractionComponent::TryEndInteract() if (bContextMenuOpen) { ExecuteOption(HighlightedIndex); + RefreshFocusedOptions(); CloseContextMenu(); } else if (!bHoldConsumed && CurrentOptions.Num() >= 1) { ExecuteOption(0); + RefreshFocusedOptions(); } ClearCachedTarget(); @@ -181,8 +183,17 @@ void UInteractionComponent::CycleContextMenuOptions(float Direction) return; } - const int32 Step = Direction > 0.0f ? 1 : -1; - HighlightedIndex = (HighlightedIndex + Step + CurrentOptions.Num()) % CurrentOptions.Num(); + // Scrolling up (positive Direction) moves the highlight toward the top of the list (index 0), + // matching how the mouse wheel scrolls content elsewhere - hence the flipped sign here. + const int32 Step = Direction > 0.0f ? -1 : 1; + const int32 NewHighlightedIndex = FMath::Clamp(HighlightedIndex + Step, 0, CurrentOptions.Num() - 1); + + if (NewHighlightedIndex == HighlightedIndex) + { + return; + } + + HighlightedIndex = NewHighlightedIndex; OnContextMenuHighlightChanged.Broadcast(HighlightedIndex); } @@ -193,6 +204,7 @@ void UInteractionComponent::HandleHoldThresholdReached() if (CurrentOptions.Num() == 2) { ExecuteOption(1); + RefreshFocusedOptions(); ClearCachedTarget(); } else if (CurrentOptions.Num() >= 3) @@ -225,6 +237,18 @@ void UInteractionComponent::ExecuteOption(int32 Index) IInteractable::Execute_ExecuteInteraction(Target, GetOwner(), CurrentOptions[Index].ActionID); } +void UInteractionComponent::RefreshFocusedOptions() +{ + AActor* Actor = FocusedActor.Get(); + if (!Actor) + { + return; + } + + FocusedOptions = IInteractable::Execute_GetInteractionOptions(Actor, GetOwner()); + OnFocusedInteractableChanged.Broadcast(Actor, FocusedOptions); +} + void UInteractionComponent::ClearCachedTarget() { CurrentTarget = nullptr; diff --git a/Source/KingshearthLegacy/Interaction/InteractionComponent.h b/Source/KingshearthLegacy/Interaction/InteractionComponent.h index dd10ce2..27163b7 100644 --- a/Source/KingshearthLegacy/Interaction/InteractionComponent.h +++ b/Source/KingshearthLegacy/Interaction/InteractionComponent.h @@ -130,6 +130,15 @@ protected: /** Executes CurrentOptions[Index] against CurrentTarget, if both are still valid. */ void ExecuteOption(int32 Index); + /** + * Re-pulls GetInteractionOptions() for the still-focused actor and re-broadcasts + * OnFocusedInteractableChanged. Call right after ExecuteOption - the interaction just run + * may have changed what that same actor now offers (e.g. a chest that's now empty), and + * UpdateFocus alone would never notice since it only re-pulls options when the focused + * actor's identity changes, not when its offered options do. No-op if nothing is focused. + */ + void RefreshFocusedOptions(); + void ClearCachedTarget(); /** Actor currently under the look trace and implementing IInteractable, updated every tick by UpdateFocus. */ diff --git a/Source/KingshearthLegacy/Interaction/InteractionTypes.h b/Source/KingshearthLegacy/Interaction/InteractionTypes.h index 8172338..47d95cb 100644 --- a/Source/KingshearthLegacy/Interaction/InteractionTypes.h +++ b/Source/KingshearthLegacy/Interaction/InteractionTypes.h @@ -37,4 +37,22 @@ struct FInteractionOption */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction") FText PromptOverride; + + /** + * Optional per-option override for the Context Menu row's text color (both normal and + * highlighted state use this same color, so the indication doesn't disappear on hover) - + * e.g. red for an action you currently can't perform, gold for a special one. Overrides + * UInteractionOptionEntryWidget's NormalTextColor/HighlightedTextColor for this option + * only. Alpha 0 (the default) means "use the entry widget's own colors". + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction") + FLinearColor TextColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f); + + /** + * Optional per-option override for the Context Menu row's highlight tint - overrides + * UInteractionOptionEntryWidget's HighlightColor for this option only. Alpha 0 (the + * default) means "use the entry widget's own color". + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction") + FLinearColor HighlightColorOverride = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f); }; diff --git a/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.cpp b/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.cpp index 5a70655..b8c5fe1 100644 --- a/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.cpp +++ b/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.cpp @@ -11,6 +11,17 @@ void UInteractionOptionEntryWidget::NativeConstruct() { Super::NativeConstruct(); + // Capture Background's Designer-authored color before ApplyOptionVisuals ever runs, so + // "no override" can fall back to it later instead of an arbitrary hardcoded default. + if (UImage* BackgroundImage = Cast(Background.Get())) + { + DefaultBackgroundColor = BackgroundImage->GetColorAndOpacity(); + } + else if (UBorder* BackgroundBorder = Cast(Background.Get())) + { + DefaultBackgroundColor = BackgroundBorder->GetBrushColor(); + } + // The ListView only calls NativeOnItemSelectionChanged when a row's selection state // actually changes, so a freshly-spawned row that has never been (de)selected would // otherwise keep whatever visibility Highlight has in the Designer - force it into @@ -59,26 +70,47 @@ void UInteractionOptionEntryWidget::ApplyOptionVisuals() Icon->SetVisibility(ESlateVisibility::Collapsed); } } + + if (Background) + { + const FLinearColor ResolvedBackgroundColor = Option.HighlightColorOverride.A > 0.0f ? Option.HighlightColorOverride : DefaultBackgroundColor; + + if (UImage* BackgroundImage = Cast(Background.Get())) + { + BackgroundImage->SetColorAndOpacity(ResolvedBackgroundColor); + } + else if (UBorder* BackgroundBorder = Cast(Background.Get())) + { + BackgroundBorder->SetBrushColor(ResolvedBackgroundColor); + } + } } void UInteractionOptionEntryWidget::ApplyHighlightVisuals() { + // Alpha 0 (the struct default) means the option didn't request an override - fall back + // to this widget's own Style colors. + const FLinearColor ResolvedHighlightColor = Option.HighlightColorOverride.A > 0.0f ? Option.HighlightColorOverride : HighlightColor; + const FLinearColor ResolvedTextColor = Option.TextColorOverride.A > 0.0f + ? Option.TextColorOverride + : (bHighlighted ? HighlightedTextColor : NormalTextColor); + if (Highlight) { Highlight->SetVisibility(bHighlighted ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden); if (UImage* HighlightImage = Cast(Highlight.Get())) { - HighlightImage->SetColorAndOpacity(HighlightColor); + HighlightImage->SetColorAndOpacity(ResolvedHighlightColor); } else if (UBorder* HighlightBorder = Cast(Highlight.Get())) { - HighlightBorder->SetBrushColor(HighlightColor); + HighlightBorder->SetBrushColor(ResolvedHighlightColor); } } if (InteractionText) { - InteractionText->SetColorAndOpacity(bHighlighted ? HighlightedTextColor : NormalTextColor); + InteractionText->SetColorAndOpacity(ResolvedTextColor); } } diff --git a/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.h b/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.h index 465d392..42c2d0b 100644 --- a/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.h +++ b/Source/KingshearthLegacy/UI/InteractionOptionEntryWidget.h @@ -61,6 +61,17 @@ protected: UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) TObjectPtr Highlight; + /** + * Optional persistent row background - bind a Border or an Image named exactly + * "Background" in the Designer. Unlike Highlight, this isn't gated on selection - it's + * tinted with the option's HighlightColorOverride (from FInteractionOption) whenever one + * is set, so a "can't do this" or "special" row stays visually marked regardless of + * whether it's currently highlighted. Left at its Designer-authored appearance when the + * option has no override. + */ + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true)) + TObjectPtr Background; + /** Tint applied to Highlight while this row is highlighted. */ UPROPERTY(EditAnywhere, Category = "Interaction|Style") FLinearColor HighlightColor = FLinearColor(1.0f, 0.85f, 0.3f, 1.0f); @@ -78,4 +89,15 @@ protected: UPROPERTY(BlueprintReadOnly, Category = "Interaction") bool bHighlighted = false; + +private: + + /** + * Background's Designer-authored color, captured once in NativeConstruct before any + * option is ever assigned. Rows are recycled by the ListView, so a row previously tinted + * by one option's HighlightColorOverride needs an explicit color to fall back to when + * reassigned to a different, unconfigured option - rather than just leaving whatever + * tint the recycled widget last had. + */ + FLinearColor DefaultBackgroundColor = FLinearColor::White; }; diff --git a/Source/KingshearthLegacy/UI/InteractionPromptWidget.cpp b/Source/KingshearthLegacy/UI/InteractionPromptWidget.cpp index bce4851..564cf2e 100644 --- a/Source/KingshearthLegacy/UI/InteractionPromptWidget.cpp +++ b/Source/KingshearthLegacy/UI/InteractionPromptWidget.cpp @@ -154,7 +154,7 @@ void UInteractionPromptWidget::UpdatePromptVisibility() FString Hint = TEXT("Press E"); if (LastFocusedOptions.Num() == 2) { - Hint += TEXT("\nHold E for secondary interaction"); + Hint += FString::Printf(TEXT("\nHold E for %s"), *LastFocusedOptions[1].DisplayName.ToString()); } else if (LastFocusedOptions.Num() >= 3) {