From 6f0aa5274cd954ae5920d38c195f877a365e1c5f Mon Sep 17 00:00:00 2001 From: koritsa Date: Thu, 28 May 2026 21:53:34 +0300 Subject: [PATCH] Rework interaction system --- Config/DefaultGame.ini | 4 +- .../Clothing/DA_DefaultPlayerClothing.uasset | 3 - .../DA_DefaultWardrobeClothing.uasset | 3 - .../Data/Clothing/DA_DigitalShopItems.uasset | 3 - .../Interactables/A_ItemPickup.uasset | 3 + .../Interactables/BP_ClothingPickup.uasset | 3 - Content/Blueprints/Interactables/BP_PC.uasset | 3 - .../Interactables/BP_Wardrobe.uasset | 3 - Content/Blueprints/Player/BP_Player.uasset | 4 +- Content/Test/Maps/TestLevel.umap | 2 +- Content/UI/HUD/W_HUD.uasset | 4 +- .../Interaction/WBP_InteractionTrigger.uasset | 4 +- .../NakedDesire/Clothing/ClothingManager.cpp | 21 ++ Source/NakedDesire/Clothing/ClothingManager.h | 5 + .../Interactables/InteractableBase.cpp | 55 ----- .../Interactables/InteractableBase.h | 34 --- .../NakedDesire/Interactables/ItemPickup.cpp | 76 +++++++ Source/NakedDesire/Interactables/ItemPickup.h | 49 +++++ Source/NakedDesire/Interactables/Wardrobe.cpp | 1 - Source/NakedDesire/Interactables/Wardrobe.h | 4 +- Source/NakedDesire/Interaction/Interactable.h | 43 ++++ .../Interaction/InteractionComponent.cpp | 208 ++++++++++++++++++ .../Interaction/InteractionComponent.h | 64 ++++++ .../Restrictions/LocationRestriction.cpp | 8 +- .../Player/NakedDesireCharacter.cpp | 73 +----- .../NakedDesire/Player/NakedDesireCharacter.h | 32 +-- 26 files changed, 496 insertions(+), 216 deletions(-) delete mode 100644 Content/Blueprints/Data/Clothing/DA_DefaultPlayerClothing.uasset delete mode 100644 Content/Blueprints/Data/Clothing/DA_DefaultWardrobeClothing.uasset delete mode 100644 Content/Blueprints/Data/Clothing/DA_DigitalShopItems.uasset create mode 100644 Content/Blueprints/Interactables/A_ItemPickup.uasset delete mode 100644 Content/Blueprints/Interactables/BP_ClothingPickup.uasset delete mode 100644 Content/Blueprints/Interactables/BP_PC.uasset delete mode 100644 Content/Blueprints/Interactables/BP_Wardrobe.uasset delete mode 100644 Source/NakedDesire/Interactables/InteractableBase.cpp delete mode 100644 Source/NakedDesire/Interactables/InteractableBase.h create mode 100644 Source/NakedDesire/Interactables/ItemPickup.cpp create mode 100644 Source/NakedDesire/Interactables/ItemPickup.h create mode 100644 Source/NakedDesire/Interaction/Interactable.h create mode 100644 Source/NakedDesire/Interaction/InteractionComponent.cpp create mode 100644 Source/NakedDesire/Interaction/InteractionComponent.h diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 86098ec2..dcc49f71 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -141,6 +141,6 @@ bSupportsTouch=False bSupportsGamepad=True DefaultGamepadName=Generic bCanChangeGamepadType=True -+ControllerData=/Game/Input/GamepadControllerData.GamepadControllerData_C -+ControllerData=/Game/Input/KeyboardControllerData.KeyboardControllerData_C ++ControllerData=/Game/Input/CommonUI/KeyboardControllerData.KeyboardControllerData_C ++ControllerData=/Game/Input/CommonUI/GamepadControllerData.GamepadControllerData_C diff --git a/Content/Blueprints/Data/Clothing/DA_DefaultPlayerClothing.uasset b/Content/Blueprints/Data/Clothing/DA_DefaultPlayerClothing.uasset deleted file mode 100644 index 70ac06ac..00000000 --- a/Content/Blueprints/Data/Clothing/DA_DefaultPlayerClothing.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:166eaabe26878de29c9bb9ffaa8575c7b7db95b8e7da32ce56dd51de3f920aba -size 3281 diff --git a/Content/Blueprints/Data/Clothing/DA_DefaultWardrobeClothing.uasset b/Content/Blueprints/Data/Clothing/DA_DefaultWardrobeClothing.uasset deleted file mode 100644 index 7e092daa..00000000 --- a/Content/Blueprints/Data/Clothing/DA_DefaultWardrobeClothing.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de6baa1085dc11b669fe7cd872101473c5ef3f6c54da800767e16b4484278250 -size 2250 diff --git a/Content/Blueprints/Data/Clothing/DA_DigitalShopItems.uasset b/Content/Blueprints/Data/Clothing/DA_DigitalShopItems.uasset deleted file mode 100644 index 86d2dfe7..00000000 --- a/Content/Blueprints/Data/Clothing/DA_DigitalShopItems.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c5a3687898e258f772f3bc48a93874e18b880d73ee96e27f082a73fb015952cc -size 6537 diff --git a/Content/Blueprints/Interactables/A_ItemPickup.uasset b/Content/Blueprints/Interactables/A_ItemPickup.uasset new file mode 100644 index 00000000..b19667bf --- /dev/null +++ b/Content/Blueprints/Interactables/A_ItemPickup.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b22497dbe12d9cf464497ad57b601966cfe9aa9c22c8ddbc3169e2653edc2ce +size 33148 diff --git a/Content/Blueprints/Interactables/BP_ClothingPickup.uasset b/Content/Blueprints/Interactables/BP_ClothingPickup.uasset deleted file mode 100644 index 892beea8..00000000 --- a/Content/Blueprints/Interactables/BP_ClothingPickup.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d608697941e6aeba0436315576123e30bf3846f2e849c29e36c49848e04bb046 -size 75423 diff --git a/Content/Blueprints/Interactables/BP_PC.uasset b/Content/Blueprints/Interactables/BP_PC.uasset deleted file mode 100644 index a9fa2a5a..00000000 --- a/Content/Blueprints/Interactables/BP_PC.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cd6fe5bc5292e446408dce6bcb7f44d98aa98027e59bbbef63147b10f39bd33d -size 33631 diff --git a/Content/Blueprints/Interactables/BP_Wardrobe.uasset b/Content/Blueprints/Interactables/BP_Wardrobe.uasset deleted file mode 100644 index de6a170e..00000000 --- a/Content/Blueprints/Interactables/BP_Wardrobe.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21535df44f6156ba838a02ab2825a6d6ca3a5205beb5d9ee59473f156f1bfb92 -size 30040 diff --git a/Content/Blueprints/Player/BP_Player.uasset b/Content/Blueprints/Player/BP_Player.uasset index f6291eb1..5a518fea 100644 --- a/Content/Blueprints/Player/BP_Player.uasset +++ b/Content/Blueprints/Player/BP_Player.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6732be5467c8147ce87258f6ea243cd3c634c4ac5f0877e2dc2728e89f9f5dae -size 88872 +oid sha256:941780801c1e8e5e365f4a4d8020aa2032d23afa739e7c99247e4aafd5b8d913 +size 77347 diff --git a/Content/Test/Maps/TestLevel.umap b/Content/Test/Maps/TestLevel.umap index e7f597a2..9df4db20 100644 --- a/Content/Test/Maps/TestLevel.umap +++ b/Content/Test/Maps/TestLevel.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e191b583b26373269aaa340693ecfe4fbf1e32a3704bb1c2b3219885c16916f1 +oid sha256:1d1d95409b846eeade9dbe404c76370fb6905ee10aa00582ca0047c483d026e9 size 49583 diff --git a/Content/UI/HUD/W_HUD.uasset b/Content/UI/HUD/W_HUD.uasset index 785f1bc2..18f83e20 100644 --- a/Content/UI/HUD/W_HUD.uasset +++ b/Content/UI/HUD/W_HUD.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d21ba1b809f0155ca982e3162632399da9c13ede7a223357653b2b54adf75b16 -size 25046 +oid sha256:b05fa9217649a2b49ce433c1dc7c6aeaaf5ebb9b3cee644afddac95c3e2a1966 +size 27305 diff --git a/Content/UI/Interaction/WBP_InteractionTrigger.uasset b/Content/UI/Interaction/WBP_InteractionTrigger.uasset index bd07adb3..3672a6ad 100644 --- a/Content/UI/Interaction/WBP_InteractionTrigger.uasset +++ b/Content/UI/Interaction/WBP_InteractionTrigger.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbdc256c5d21bf6d619f1c63defe9ca3b06a99199cfa7b1fe51f466e27cfb7a7 -size 13020 +oid sha256:6b396a1ec980530c8666847394ef05f6cc5fec6aaaf0e391f6f50038379fdd75 +size 12630 diff --git a/Source/NakedDesire/Clothing/ClothingManager.cpp b/Source/NakedDesire/Clothing/ClothingManager.cpp index a40c691b..789c7a20 100644 --- a/Source/NakedDesire/Clothing/ClothingManager.cpp +++ b/Source/NakedDesire/Clothing/ClothingManager.cpp @@ -6,6 +6,7 @@ #include "ClothingItemInstance.h" #include "GameFramework/Character.h" #include "Kismet/GameplayStatics.h" +#include "NakedDesire/Interactables/ItemPickup.h" #include "NakedDesire/Player/NakedDesireCharacter.h" #include "NakedDesire/SaveGame/GlobalSaveGameData.h" #include "NakedDesire/SaveGame/ItemSaveRecord.h" @@ -108,6 +109,24 @@ USkeletalMeshComponent* UClothingManager::GetMeshComponent(const EClothingSlotTy } } +void UClothingManager::SpawnClothingPickup(UClothingItemInstance* ItemInstance) +{ + if (!ItemPickupActor) + { + UE_LOG(LogTemp, Warning, TEXT("UClothingManager::SpawnClothingPickup ItemPickupActor is not set")); + return; + } + + AItemPickup* NewItemPickup = GetWorld()->SpawnActor(ItemPickupActor, GetOwner()->GetActorTransform()); + if (!NewItemPickup) + { + UE_LOG(LogTemp, Warning, TEXT("UClothingManager::SpawnClothingPickup NewItemPickup == nullptr")); + return; + } + + NewItemPickup->SetItem(ItemInstance); +} + void UClothingManager::PutOnClothing(UClothingItemInstance* ClothingItemInstance) { if (!ClothingItemInstance) @@ -220,6 +239,8 @@ void UClothingManager::DropClothing(const EClothingSlotType ClothingType) FItemSaveRecord ItemSaveRecord; ItemSaveRecord.Init(ClothingItemInstance); SaveSubsystem->GetCurrentSave()->DroppedItems.Push(ItemSaveRecord); + + SpawnClothingPickup(ClothingItemInstance); OnClothingDropped.Broadcast(ClothingItemInstance); } diff --git a/Source/NakedDesire/Clothing/ClothingManager.h b/Source/NakedDesire/Clothing/ClothingManager.h index f459087d..84c61dff 100644 --- a/Source/NakedDesire/Clothing/ClothingManager.h +++ b/Source/NakedDesire/Clothing/ClothingManager.h @@ -8,6 +8,7 @@ #include "Components/ActorComponent.h" #include "ClothingManager.generated.h" +class AItemPickup; class UGlobalSaveGameData; class AClothingPickup; class UClothingItemInstance; @@ -53,7 +54,11 @@ public: private: USkeletalMeshComponent* GetMeshComponent(EClothingSlotType SlotType) const; + void SpawnClothingPickup(UClothingItemInstance* ItemInstance); UPROPERTY() TMap> EquippedClothing; + + UPROPERTY(EditDefaultsOnly, Category = "Clothing") + TSubclassOf ItemPickupActor; }; diff --git a/Source/NakedDesire/Interactables/InteractableBase.cpp b/Source/NakedDesire/Interactables/InteractableBase.cpp deleted file mode 100644 index 2ac3b3da..00000000 --- a/Source/NakedDesire/Interactables/InteractableBase.cpp +++ /dev/null @@ -1,55 +0,0 @@ -// © 2025 Naked People Team. All Rights Reserved. - - -#include "InteractableBase.h" -#include "Components/WidgetComponent.h" -#include "Kismet/GameplayStatics.h" -#include "NakedDesire/InteractionSystem/InteractionManager.h" -#include "NakedDesire/Player/NakedDesireCharacter.h" - - -AInteractableBase::AInteractableBase() -{ - PrimaryActorTick.bCanEverTick = true; - PrimaryActorTick.TickInterval = 0.25f; - - RootSceneComponent = CreateDefaultSubobject(TEXT("Root")); - RootComponent = RootSceneComponent; - - WidgetAnchor = CreateDefaultSubobject(TEXT("Widget Anchor")); - WidgetAnchor->SetupAttachment(RootComponent); - - InteractionTrigger = CreateDefaultSubobject(TEXT("Interaction Trigger")); - InteractionTrigger->SetupAttachment(WidgetAnchor); - - InteractionTrigger->SetDrawSize(FVector2D(35, 35)); - InteractionTrigger->SetWidgetSpace(EWidgetSpace::Screen); - InteractionTrigger->SetWindowFocusable(false); -} - -void AInteractableBase::Tick(float DeltaSeconds) -{ - Super::Tick(DeltaSeconds); - - if (!Player) - { - if (ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)) - { - Player = Cast(PlayerCharacter); - } - } - - if (Player) - { - const TScriptInterface NearestInteractionTarget = Player->InteractionManager->GetNearestInteractionTarget(); - if (NearestInteractionTarget.GetObject() == this) - { - InteractionTrigger->SetVisibility(true); - } - else - { - InteractionTrigger->SetVisibility(false); - } - } -} - diff --git a/Source/NakedDesire/Interactables/InteractableBase.h b/Source/NakedDesire/Interactables/InteractableBase.h deleted file mode 100644 index 4ed7e6b7..00000000 --- a/Source/NakedDesire/Interactables/InteractableBase.h +++ /dev/null @@ -1,34 +0,0 @@ -// © 2025 Naked People Team. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "GameFramework/Actor.h" -#include "NakedDesire/InteractionSystem/InteractionTarget.h" -#include "InteractableBase.generated.h" - -class ANakedDesireCharacter; -class UWidgetComponent; - -UCLASS() -class NAKEDDESIRE_API AInteractableBase : public AActor, public IInteractionTarget -{ - GENERATED_BODY() - - UPROPERTY(EditDefaultsOnly) - USceneComponent* RootSceneComponent = nullptr; - - UPROPERTY(EditDefaultsOnly) - USceneComponent* WidgetAnchor = nullptr; - - UPROPERTY(EditDefaultsOnly) - UWidgetComponent* InteractionTrigger = nullptr; - - UPROPERTY() - ANakedDesireCharacter* Player = nullptr; - -public: - AInteractableBase(); - - virtual void Tick(float DeltaSeconds) override; -}; diff --git a/Source/NakedDesire/Interactables/ItemPickup.cpp b/Source/NakedDesire/Interactables/ItemPickup.cpp new file mode 100644 index 00000000..0fc83659 --- /dev/null +++ b/Source/NakedDesire/Interactables/ItemPickup.cpp @@ -0,0 +1,76 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "ItemPickup.h" +#include "Components/BoxComponent.h" +#include "Components/WidgetComponent.h" +#include "NakedDesire/Clothing/ClothingItem.h" +#include "NakedDesire/Clothing/ClothingItemInstance.h" +#include "NakedDesire/Clothing/ClothingManager.h" +#include "NakedDesire/Player/NakedDesireCharacter.h" + +AItemPickup::AItemPickup() +{ + Mesh = CreateDefaultSubobject(TEXT("Mesh")); + SetRootComponent(Mesh); + Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); + + Collider = CreateDefaultSubobject(TEXT("Collider")); + Collider->SetupAttachment(RootComponent); + Collider->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + + InteractionHint = CreateDefaultSubobject(TEXT("Interaction Hint")); + InteractionHint->SetupAttachment(RootComponent); +} + +void AItemPickup::Interact_Implementation(ANakedDesireCharacter* Player) +{ + if (ClothingItemInstance) + { + Player->ClothingManager->TakeClothing(ClothingItemInstance); + Destroy(); + } +} + +bool AItemPickup::CanInteract_Implementation(ANakedDesireCharacter* Player) const +{ + return ClothingItemInstance != nullptr; +} + +void AItemPickup::HideInteractionHint_Implementation() +{ + ApplyOutline(Mesh, false, 0); + InteractionHint->SetVisibility(false); +} + +void AItemPickup::ShowInteractionFocusHint_Implementation() +{ + ApplyOutline(Mesh, true, 2); + InteractionHint->SetVisibility(true); +} + +void AItemPickup::ShowInteractionProximityHint_Implementation() +{ + ApplyOutline(Mesh, true, 1); + InteractionHint->SetVisibility(false); +} + +void AItemPickup::BeginPlay() +{ + Super::BeginPlay(); + + InteractionHint->SetVisibility(false); +} + +void AItemPickup::SetItem(UClothingItemInstance* InItem) +{ + ClothingItemInstance = InItem; + + Mesh->SetStaticMesh(InItem->GetClothingItem()->StaticMesh); +} + +void AItemPickup::ApplyOutline(UStaticMeshComponent* InMesh, bool bEnabled, int32 StencilValue) +{ + InMesh->SetRenderCustomDepth(bEnabled); + InMesh->SetCustomDepthStencilValue(StencilValue); +} diff --git a/Source/NakedDesire/Interactables/ItemPickup.h b/Source/NakedDesire/Interactables/ItemPickup.h new file mode 100644 index 00000000..fc673e4d --- /dev/null +++ b/Source/NakedDesire/Interactables/ItemPickup.h @@ -0,0 +1,49 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "NakedDesire/Interaction/Interactable.h" +#include "ItemPickup.generated.h" + +class UWidgetComponent; +class UBoxComponent; +class UClothingItemInstance; + +UCLASS(Abstract) +class NAKEDDESIRE_API AItemPickup : public AActor, public IInteractable +{ + GENERATED_BODY() + + AItemPickup(); + +public: + virtual void Interact_Implementation(ANakedDesireCharacter* Player) override; + virtual bool CanInteract_Implementation(ANakedDesireCharacter* Player) const override; + virtual void HideInteractionHint_Implementation() override; + virtual void ShowInteractionFocusHint_Implementation() override; + virtual void ShowInteractionProximityHint_Implementation() override; + + + void SetItem(UClothingItemInstance* InItem); + UClothingItemInstance* GetItem() const { return ClothingItemInstance; } + +protected: + virtual void BeginPlay() override; + +private: + UPROPERTY() + TObjectPtr ClothingItemInstance; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr Mesh; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr Collider; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr InteractionHint; + + void ApplyOutline(UStaticMeshComponent* InMesh, bool bEnabled, int32 StencilValue); +}; diff --git a/Source/NakedDesire/Interactables/Wardrobe.cpp b/Source/NakedDesire/Interactables/Wardrobe.cpp index b49a6a1d..8c77e72f 100644 --- a/Source/NakedDesire/Interactables/Wardrobe.cpp +++ b/Source/NakedDesire/Interactables/Wardrobe.cpp @@ -2,7 +2,6 @@ #include "Wardrobe.h" - #include "Kismet/GameplayStatics.h" #include "NakedDesire/SaveGame/GlobalSaveGameData.h" #include "NakedDesire/SaveGame/ItemSaveRecord.h" diff --git a/Source/NakedDesire/Interactables/Wardrobe.h b/Source/NakedDesire/Interactables/Wardrobe.h index 176d9a2f..a4d5aaf3 100644 --- a/Source/NakedDesire/Interactables/Wardrobe.h +++ b/Source/NakedDesire/Interactables/Wardrobe.h @@ -3,14 +3,13 @@ #pragma once #include "CoreMinimal.h" -#include "InteractableBase.h" #include "GameFramework/Actor.h" #include "Wardrobe.generated.h" class UClothingItemInstance; UCLASS(Blueprintable) -class NAKEDDESIRE_API AWardrobe : public AInteractableBase +class NAKEDDESIRE_API AWardrobe : public AActor { GENERATED_BODY() @@ -21,5 +20,6 @@ public: void AddItem(UClothingItemInstance* ClothingItemInstance); void RemoveItem(UClothingItemInstance* ClothingItemInstance) const; +protected: virtual void BeginPlay() override; }; diff --git a/Source/NakedDesire/Interaction/Interactable.h b/Source/NakedDesire/Interaction/Interactable.h new file mode 100644 index 00000000..0ae2a2a4 --- /dev/null +++ b/Source/NakedDesire/Interaction/Interactable.h @@ -0,0 +1,43 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "Interactable.generated.h" + +class ANakedDesireCharacter; + +UINTERFACE(MinimalAPI, BlueprintType) +class UInteractable : public UInterface +{ + GENERATED_BODY() +}; + +class NAKEDDESIRE_API IInteractable +{ + GENERATED_BODY() + +public: + // Returns true if this actor can currently be interacted with by the given player + UFUNCTION(BlueprintNativeEvent) + bool CanInteract(ANakedDesireCharacter* Player) const; + + // Executes the interaction (called server-side) + UFUNCTION(BlueprintNativeEvent) + void Interact(ANakedDesireCharacter* Player); + + // Text shown in the interaction prompt UI + UFUNCTION(BlueprintNativeEvent) + FText GetInteractionPrompt() const; + + // Called (client-side) when this actor enters the player's proximity radius + UFUNCTION(BlueprintNativeEvent) + void ShowInteractionProximityHint(); + + // Called (client-side) when this actor becomes the focused interaction target + UFUNCTION(BlueprintNativeEvent) + void ShowInteractionFocusHint(); + + // Called (client-side) when this actor loses proximity or focus + UFUNCTION(BlueprintNativeEvent) + void HideInteractionHint(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/Interaction/InteractionComponent.cpp b/Source/NakedDesire/Interaction/InteractionComponent.cpp new file mode 100644 index 00000000..bbe1b77c --- /dev/null +++ b/Source/NakedDesire/Interaction/InteractionComponent.cpp @@ -0,0 +1,208 @@ +#include "InteractionComponent.h" +#include "Interactable.h" +#include "Engine/OverlapResult.h" +#include "CollisionQueryParams.h" +#include "NakedDesire/Player/NakedDesireCharacter.h" + +UInteractionComponent::UInteractionComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +void UInteractionComponent::BeginPlay() +{ + Super::BeginPlay(); + + // Only run on the locally controlled pawn — outlines and focus are per-client visuals + const APawn* OwnerPawn = Cast(GetOwner()); + if (!OwnerPawn) + return; + + GetWorld()->GetTimerManager().SetTimer( + InteractionTimerHandle, + this, + &UInteractionComponent::UpdateInteraction, + 0.1f, + true + ); +} + +void UInteractionComponent::UpdateInteraction() +{ + UpdateNearbyInteractables(); + UpdateFocusedInteractable(); +} + +void UInteractionComponent::Interact() +{ + AActor* Target = FocusedInteractable.Get(); + if (!Target) + return; + + if (!IInteractable::Execute_CanInteract(Target, Cast(GetOwner()))) + return; + IInteractable::Execute_Interact(Target, Cast(GetOwner())); +} + +void UInteractionComponent::UpdateNearbyInteractables() +{ + AActor* Owner = GetOwner(); + if (!Owner) + return; + + // Sphere overlap to find all nearby actors + TArray Overlaps; + FCollisionObjectQueryParams ObjectParams; + ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic); + ObjectParams.AddObjectTypesToQuery(ECC_WorldStatic); + + GetWorld()->OverlapMultiByObjectType( + Overlaps, + Owner->GetActorLocation(), + FQuat::Identity, + ObjectParams, + FCollisionShape::MakeSphere(ProximityRadius) + ); + + // Build the new set from overlap results, filtering to IInteractable actors + TSet> NewNearby; + for (const FOverlapResult& Overlap : Overlaps) + { + AActor* Actor = Overlap.GetActor(); + if (Actor && Actor != Owner && Actor->Implements()) + { + NewNearby.Add(Actor); + } + } + + // Actors that left proximity: hide their hints + for (const TWeakObjectPtr& OldActor : NearbyInteractables) + { + if (OldActor.IsValid() && !NewNearby.Contains(OldActor)) + { + if (FocusedInteractable == OldActor) + { + FocusedInteractable = nullptr; + OnFocusedInteractableChanged.Broadcast(nullptr); + } + IInteractable::Execute_HideInteractionHint(OldActor.Get()); + } + } + + NearbyInteractables = MoveTemp(NewNearby); + + // Recompute the proximity hint for every nearby actor based on current LOS. + // Runs every tick so actors that enter/leave line-of-sight mid-session update + // immediately — e.g. a wall sliding into place hides the hint without the + // actor ever leaving the proximity radius. + // The focused actor is skipped: UpdateFocusedInteractable owns its hint state. + for (const TWeakObjectPtr& WeakActor : NearbyInteractables) + { + AActor* Actor = WeakActor.Get(); + if (!Actor || FocusedInteractable == Actor) + continue; + + if (HasLineOfSightFromPawn(Actor)) + IInteractable::Execute_ShowInteractionProximityHint(Actor); + else + IInteractable::Execute_HideInteractionHint(Actor); + } +} + +void UInteractionComponent::UpdateFocusedInteractable() +{ + FVector ViewLocation; + FRotator ViewRotation; + if (!GetOwnerViewPoint(ViewLocation, ViewRotation)) + return; + + const FVector ViewForward = ViewRotation.Vector(); + ANakedDesireCharacter* Player = Cast(GetOwner()); + + // Among nearby interactables, find the one most centered in the player's view + // that is also within interaction distance and passes CanInteract + AActor* BestActor = nullptr; + float BestDot = -1.f; + + for (const TWeakObjectPtr& WeakActor : NearbyInteractables) + { + AActor* Actor = WeakActor.Get(); + if (!Actor) + continue; + + if (FVector::DistSquared(Player->GetActorLocation(), Actor->GetActorLocation()) > FMath::Square(InteractionDistance)) + continue; + + if (!IInteractable::Execute_CanInteract(Actor, Player)) + continue; + + // Look-cone check — cheap filter before the LOS trace + const FVector ToActor = (Actor->GetActorLocation() - ViewLocation).GetSafeNormal(); + const float Dot = FVector::DotProduct(ViewForward, ToActor); + if (Dot < LookDotThreshold || Dot <= BestDot) + continue; + + // LOS from the pawn's eye position — not the camera, so looking over a + // wall with the third-person camera does not grant interaction range + if (!HasLineOfSightFromPawn(Actor)) + continue; + + BestDot = Dot; + BestActor = Actor; + } + + if (FocusedInteractable.Get() == BestActor) + return; + + // Restore the previously focused actor — only show a proximity hint if it is + // still nearby AND in line-of-sight; hide it otherwise + if (AActor* OldFocused = FocusedInteractable.Get()) + { + if (NearbyInteractables.Contains(OldFocused) && HasLineOfSightFromPawn(OldFocused)) + IInteractable::Execute_ShowInteractionProximityHint(OldFocused); + else + IInteractable::Execute_HideInteractionHint(OldFocused); + } + + FocusedInteractable = BestActor; + + if (BestActor) + IInteractable::Execute_ShowInteractionFocusHint(BestActor); + + OnFocusedInteractableChanged.Broadcast(BestActor); +} + +bool UInteractionComponent::GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const +{ + const APawn* OwnerPawn = Cast(GetOwner()); + if (!OwnerPawn) + return false; + + AController* Controller = OwnerPawn->GetController(); + if (!Controller) + return false; + + Controller->GetPlayerViewPoint(OutLocation, OutRotation); + return true; +} + +bool UInteractionComponent::HasLineOfSightFromPawn(AActor* Target) const +{ + const APawn* OwnerPawn = Cast(GetOwner()); + if (!OwnerPawn || !Target) + return false; + + // Start from the pawn's own eye position, NOT the spring-arm camera. + // This means a player who tilts the camera above a wall still cannot + // reach objects on the other side — the trace originates from the character mesh. + const FVector Start = OwnerPawn->GetPawnViewLocation(); + const FVector End = Target->GetActorLocation(); + + FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionLOS), false); + Params.AddIgnoredActor(GetOwner()); + Params.AddIgnoredActor(Target); + + FHitResult Hit; + const bool bBlocked = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params); + return !bBlocked; +} \ No newline at end of file diff --git a/Source/NakedDesire/Interaction/InteractionComponent.h b/Source/NakedDesire/Interaction/InteractionComponent.h new file mode 100644 index 00000000..88970563 --- /dev/null +++ b/Source/NakedDesire/Interaction/InteractionComponent.h @@ -0,0 +1,64 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "InteractionComponent.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFocusedInteractableChanged, AActor*, NewFocused); + +UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) +class NAKEDDESIRE_API UInteractionComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UInteractionComponent(); + + virtual void BeginPlay() override; + + // Call from interact input — sends server RPC if not authority + void Interact(); + + UFUNCTION(BlueprintPure) + AActor* GetFocusedInteractable() const { return FocusedInteractable.Get(); } + + // Broadcasts whenever the focused (interactable) actor changes; nullptr = no target + UPROPERTY(BlueprintAssignable) + FOnFocusedInteractableChanged OnFocusedInteractableChanged; + +protected: + + // Actors within this radius receive a proximity outline + UPROPERTY(EditDefaultsOnly, Category="Interaction") + float ProximityRadius = 350.f; + + // Player must be within this distance to focus an interactable (must be <= ProximityRadius) + UPROPERTY(EditDefaultsOnly, Category="Interaction") + float InteractionDistance = 150.f; + + // Minimum dot product between look direction and direction to target. + // cos(20°) ≈ 0.94 — lower values allow a wider look cone. + UPROPERTY(EditDefaultsOnly, Category="Interaction") + float LookDotThreshold = 0.9f; + +private: + FTimerHandle InteractionTimerHandle; + + void UpdateInteraction(); + + TSet> NearbyInteractables; + TWeakObjectPtr FocusedInteractable; + + // Physics scan → update NearbyInteractables set → manage proximity outlines + void UpdateNearbyInteractables(); + + // Among nearby actors, find the one best matching look + distance conditions + void UpdateFocusedInteractable(); + + bool GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const; + + // Traces from the pawn's eye position (NOT the camera) to the target. + // Returns true only if nothing blocks the line — prevents exploiting the + // third-person camera to interact through walls by looking over them. + bool HasLineOfSightFromPawn(AActor* Target) const; +}; \ No newline at end of file diff --git a/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp b/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp index 4696b6c4..1abeab69 100644 --- a/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp +++ b/Source/NakedDesire/MissionBuilder/Restrictions/LocationRestriction.cpp @@ -13,16 +13,16 @@ void ULocationRestriction::Init(ANakedDesireCharacter* PlayerCharacter) { Super::Init(PlayerCharacter); - AreaEnterHandle = PlayerCharacter->OnAreaEnter.AddUObject(this, &ULocationRestriction::OnAreaEnter); - AreaExitHandle = PlayerCharacter->OnAreaExit.AddUObject(this, &ULocationRestriction::OnAreaExit); + // AreaEnterHandle = PlayerCharacter->OnAreaEnter.AddUObject(this, &ULocationRestriction::OnAreaEnter); + // AreaExitHandle = PlayerCharacter->OnAreaExit.AddUObject(this, &ULocationRestriction::OnAreaExit); } void ULocationRestriction::Stop() { Super::Stop(); - Player->OnAreaEnter.Remove(AreaEnterHandle); - Player->OnAreaExit.Remove(AreaExitHandle); + // Player->OnAreaEnter.Remove(AreaEnterHandle); + // Player->OnAreaExit.Remove(AreaExitHandle); } FText ULocationRestriction::GetDescription() const diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.cpp b/Source/NakedDesire/Player/NakedDesireCharacter.cpp index 72b13302..21d18619 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.cpp +++ b/Source/NakedDesire/Player/NakedDesireCharacter.cpp @@ -1,12 +1,8 @@ // © 2025 Naked People Team. All Rights Reserved. #include "NakedDesireCharacter.h" -#include "NakedDesire/Locations/LocationTrigger.h" #include "NakedDesire/Clothing/ClothingManager.h" -#include "Components/CapsuleComponent.h" #include "GameFramework/CharacterMovementComponent.h" -#include "NakedDesire/InteractionSystem/InteractionManager.h" -#include "NakedDesire/InteractionSystem/InteractionTarget.h" #include "NakedDesire/MissionBuilder/MissionsManager.h" #include "NakedDesire/Stats/StatsManager.h" #include "EnhancedInputComponent.h" @@ -15,22 +11,17 @@ #include "Internationalization/Text.h" #include "NakedDesire/Clothing/ClothingItem.h" #include "NakedDesire/Clothing/ClothingItemInstance.h" -#include "NakedDesire/Clothing/ClothingSlotsData.h" #include "NakedDesire/Global/Constants.h" #include "NakedDesire/Global/NakedDesireHUD.h" #include "NakedDesire/Global/NakedDesireUserSettings.h" +#include "NakedDesire/Interaction/InteractionComponent.h" #include "NakedDesire/UI/GameLayoutWidget.h" -#include "NakedDesire/UI/HUDWidget.h" -#include "NakedDesire/UI/RadialMenu/RadialMenuController.h" #include "Perception/AIPerceptionStimuliSourceComponent.h" #include "Perception/AISense_Sight.h" ANakedDesireCharacter::ANakedDesireCharacter() { GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; - - GetCapsuleComponent()->OnComponentBeginOverlap.AddUniqueDynamic(this, &ANakedDesireCharacter::OnBeginOverlap); - GetCapsuleComponent()->OnComponentEndOverlap.AddUniqueDynamic(this, &ANakedDesireCharacter::OnEndOverlap); bUseControllerRotationYaw = false; @@ -40,8 +31,6 @@ ANakedDesireCharacter::ANakedDesireCharacter() ClothingManager = CreateDefaultSubobject("Clothing Manager"); StatsManager = CreateDefaultSubobject("Stats Manager"); MissionsManager = CreateDefaultSubobject("Missions Manager"); - InteractionManager = CreateDefaultSubobject("Interaction Manager"); - RadialMenuController = CreateDefaultSubobject(TEXT("Radial Menu Controller")); NipplesMeshComponent = CreateDefaultSubobject("Nipples"); NipplesMeshComponent->SetupAttachment(GetMesh()); @@ -90,6 +79,7 @@ ANakedDesireCharacter::ANakedDesireCharacter() AnalCensorship->SetupAttachment(GetMesh(), FName(TEXT("pelvis"))); StimuliSourceComponent = CreateDefaultSubobject(TEXT("Stimuli Source Component")); + InteractionComponent = CreateDefaultSubobject(TEXT("Interaction Component")); } EGait ANakedDesireCharacter::GetGait() const @@ -97,11 +87,6 @@ EGait ANakedDesireCharacter::GetGait() const return Gait; } -EStance ANakedDesireCharacter::GetStance() const -{ - return Stance; -} - void ANakedDesireCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); @@ -138,6 +123,7 @@ void ANakedDesireCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInp EnhancedInputComponent->BindAction(RunAction, ETriggerEvent::Completed, this, &ANakedDesireCharacter::OnRunRelease); EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Completed, this, &ANakedDesireCharacter::OnCrouchToggle); EnhancedInputComponent->BindAction(EquipmentAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnEquipmentPress); + EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnInteractPress); } } @@ -233,36 +219,6 @@ bool ANakedDesireCharacter::CheckSight(const FVector& StartLocation, const FVect return bHit; } -void ANakedDesireCharacter::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, - UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) -{ - if (OtherActor->Implements()) - { - InteractionManager->OnTargetEnter(OtherActor); - } - - if (const ALocationTrigger* AreaTrigger = Cast(OtherActor)) - { - CurrentArea = AreaTrigger->GetLocationData(); - OnAreaEnter.Broadcast(CurrentArea); - } -} - -void ANakedDesireCharacter::OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, - UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) -{ - if (OtherActor->Implements()) - { - InteractionManager->OnTargetExit(OtherActor); - } - - if (const ALocationTrigger* AreaTrigger = Cast(OtherActor)) - { - CurrentArea = nullptr; - OnAreaExit.Broadcast(AreaTrigger->GetLocationData()); - } -} - void ANakedDesireCharacter::OnClothingEquip(UClothingItemInstance* ClothingItemInstance) { if (ClothingItemInstance->GetClothingItem()->HiddenBodyParts.Contains(EBodyPart::Ass)) @@ -374,28 +330,9 @@ void ANakedDesireCharacter::OnEquipmentPress(const FInputActionValue& Value) HUD->GetGameLayoutWidget()->OpenInventory(); } -void ANakedDesireCharacter::BuildRadialMenuEntries() +void ANakedDesireCharacter::OnInteractPress(const FInputActionValue& Value) { - if (!SlotsData) - { - UE_LOG(LogTemp, Warning, TEXT("ANakedDesireCharacter::BuildRadialMenuEntries SlotsData not defined")); - return; - } - - TArray Entries; - - for (const auto& [Key, Value] : SlotsData->Slots) - { - FRadialMenuEntry Entry; - const UClothingItemInstance* EquippedItem = ClothingManager->GetSlotClothing(Key); - Entry.bEnabled = true; - Entry.DisplayName = Value.Name; - Entry.Icon = EquippedItem ? EquippedItem->GetClothingItem()->Icon : Value.Icon; - Entry.ItemId = FName(Value.Name.ToString()); - Entries.Push(Entry); - } - - RadialMenuController->Entries = Entries; + InteractionComponent->Interact(); } void ANakedDesireCharacter::NotifyNoticed(ANPCAIController* NPCController) diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.h b/Source/NakedDesire/Player/NakedDesireCharacter.h index bd4f1547..cb7701f5 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.h +++ b/Source/NakedDesire/Player/NakedDesireCharacter.h @@ -9,17 +9,15 @@ #include "NakedDesire/Global/Gait.h" #include "NakedDesire/Global/NakedDesireUserSettings.h" #include "NakedDesire/Global/Stance.h" -#include "NakedDesire/UI/RadialMenu/RadialMenuController.h" #include "Perception/AISightTargetInterface.h" #include "NakedDesireCharacter.generated.h" +class UInteractionComponent; class ANakedDesireHUD; class UClothingItem; class UClothingItemInstance; class UClothingSlotsData; -class URadialMenuController; class UAIPerceptionStimuliSourceComponent; -class UInteractionManager; class UClothingManager; class UStatsManager; class UMissionsManager; @@ -27,7 +25,6 @@ class ANPCAIController; class ULocationData; DECLARE_MULTICAST_DELEGATE_OneParam(FOnNoticedSignature, ANPCAIController*); -DECLARE_MULTICAST_DELEGATE_OneParam(FOnAreaChangeSignature, ULocationData*); UCLASS(config=Game) class ANakedDesireCharacter : public ACharacter, public IAISightTargetInterface @@ -55,6 +52,9 @@ public: UPROPERTY(EditDefaultsOnly, Category = "Input") UInputAction* EquipmentAction; + + UPROPERTY(EditDefaultsOnly, Category = "Input") + UInputAction* InteractAction; // Clothing UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Clothing") @@ -136,14 +136,11 @@ public: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) UMissionsManager* MissionsManager; - UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) - UInteractionManager* InteractionManager; - UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) UAIPerceptionStimuliSourceComponent* StimuliSourceComponent; - UPROPERTY(EditDefaultsOnly) - TObjectPtr RadialMenuController; + UPROPERTY(EditDefaultsOnly, Category = "Interaction") + TObjectPtr InteractionComponent; // Variables UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) @@ -159,18 +156,12 @@ public: FOnNoticedSignature OnNoticed; - FOnAreaChangeSignature OnAreaEnter; - FOnAreaChangeSignature OnAreaExit; - void NotifyNoticed(ANPCAIController* NPCController); void SetIsRunning(bool Value); UFUNCTION(BlueprintPure) EGait GetGait() const; - UFUNCTION(BlueprintPure) - EStance GetStance() const; - virtual void Tick(float DeltaTime) override; virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; virtual void NotifyControllerChanged() override; @@ -180,14 +171,6 @@ public: private: EGait Gait = EGait::Walk; EStance Stance = EStance::Stand; - - UFUNCTION() - void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, - int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); - - UFUNCTION() - void OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, - int32 OtherBodyIndex); UFUNCTION() void OnClothingEquip(UClothingItemInstance* ClothingItemInstance); @@ -204,8 +187,7 @@ private: void OnRunRelease(const FInputActionValue& Value); void OnCrouchToggle(const FInputActionValue& Value); void OnEquipmentPress(const FInputActionValue& Value); - - void BuildRadialMenuEntries(); + void OnInteractPress(const FInputActionValue& Value); bool CheckSight(const FVector& StartLocation, const FVector& EndLocation, FHitResult& HitResult, const AActor* IgnoreActor);