Rework interaction system
This commit is contained in:
@@ -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<AItemPickup>(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);
|
||||
}
|
||||
|
||||
@@ -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<EClothingSlotType, TObjectPtr<UClothingItemInstance>> EquippedClothing;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Clothing")
|
||||
TSubclassOf<AItemPickup> ItemPickupActor;
|
||||
};
|
||||
|
||||
@@ -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<USceneComponent>(TEXT("Root"));
|
||||
RootComponent = RootSceneComponent;
|
||||
|
||||
WidgetAnchor = CreateDefaultSubobject<USceneComponent>(TEXT("Widget Anchor"));
|
||||
WidgetAnchor->SetupAttachment(RootComponent);
|
||||
|
||||
InteractionTrigger = CreateDefaultSubobject<UWidgetComponent>(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<ANakedDesireCharacter>(PlayerCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
if (Player)
|
||||
{
|
||||
const TScriptInterface<IInteractionTarget> NearestInteractionTarget = Player->InteractionManager->GetNearestInteractionTarget();
|
||||
if (NearestInteractionTarget.GetObject() == this)
|
||||
{
|
||||
InteractionTrigger->SetVisibility(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
InteractionTrigger->SetVisibility(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
SetRootComponent(Mesh);
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
|
||||
Collider = CreateDefaultSubobject<UBoxComponent>(TEXT("Collider"));
|
||||
Collider->SetupAttachment(RootComponent);
|
||||
Collider->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
|
||||
InteractionHint = CreateDefaultSubobject<UWidgetComponent>(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);
|
||||
}
|
||||
@@ -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<UClothingItemInstance> ClothingItemInstance;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<UStaticMeshComponent> Mesh;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<UBoxComponent> Collider;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly)
|
||||
TObjectPtr<UWidgetComponent> InteractionHint;
|
||||
|
||||
void ApplyOutline(UStaticMeshComponent* InMesh, bool bEnabled, int32 StencilValue);
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
#include "Wardrobe.h"
|
||||
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
|
||||
#include "NakedDesire/SaveGame/ItemSaveRecord.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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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<APawn>(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<ANakedDesireCharacter>(GetOwner())))
|
||||
return;
|
||||
IInteractable::Execute_Interact(Target, Cast<ANakedDesireCharacter>(GetOwner()));
|
||||
}
|
||||
|
||||
void UInteractionComponent::UpdateNearbyInteractables()
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner)
|
||||
return;
|
||||
|
||||
// Sphere overlap to find all nearby actors
|
||||
TArray<FOverlapResult> 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<TWeakObjectPtr<AActor>> NewNearby;
|
||||
for (const FOverlapResult& Overlap : Overlaps)
|
||||
{
|
||||
AActor* Actor = Overlap.GetActor();
|
||||
if (Actor && Actor != Owner && Actor->Implements<UInteractable>())
|
||||
{
|
||||
NewNearby.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Actors that left proximity: hide their hints
|
||||
for (const TWeakObjectPtr<AActor>& 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<AActor>& 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<ANakedDesireCharacter>(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<AActor>& 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<APawn>(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<APawn>(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;
|
||||
}
|
||||
@@ -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<TWeakObjectPtr<AActor>> NearbyInteractables;
|
||||
TWeakObjectPtr<AActor> 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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<UClothingManager>("Clothing Manager");
|
||||
StatsManager = CreateDefaultSubobject<UStatsManager>("Stats Manager");
|
||||
MissionsManager = CreateDefaultSubobject<UMissionsManager>("Missions Manager");
|
||||
InteractionManager = CreateDefaultSubobject<UInteractionManager>("Interaction Manager");
|
||||
RadialMenuController = CreateDefaultSubobject<URadialMenuController>(TEXT("Radial Menu Controller"));
|
||||
|
||||
NipplesMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>("Nipples");
|
||||
NipplesMeshComponent->SetupAttachment(GetMesh());
|
||||
@@ -90,6 +79,7 @@ ANakedDesireCharacter::ANakedDesireCharacter()
|
||||
AnalCensorship->SetupAttachment(GetMesh(), FName(TEXT("pelvis")));
|
||||
|
||||
StimuliSourceComponent = CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("Stimuli Source Component"));
|
||||
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(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<UInteractionTarget>())
|
||||
{
|
||||
InteractionManager->OnTargetEnter(OtherActor);
|
||||
}
|
||||
|
||||
if (const ALocationTrigger* AreaTrigger = Cast<ALocationTrigger>(OtherActor))
|
||||
{
|
||||
CurrentArea = AreaTrigger->GetLocationData();
|
||||
OnAreaEnter.Broadcast(CurrentArea);
|
||||
}
|
||||
}
|
||||
|
||||
void ANakedDesireCharacter::OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
|
||||
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
if (OtherActor->Implements<UInteractionTarget>())
|
||||
{
|
||||
InteractionManager->OnTargetExit(OtherActor);
|
||||
}
|
||||
|
||||
if (const ALocationTrigger* AreaTrigger = Cast<ALocationTrigger>(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<FRadialMenuEntry> 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)
|
||||
|
||||
@@ -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<URadialMenuController> RadialMenuController;
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Interaction")
|
||||
TObjectPtr<UInteractionComponent> 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user