Files
inventory-system-plugin/Source/ItemWorld/Private/HeldItemComponent.cpp
Bonchellon 7f7e043a88 Initial commit: Item Interaction Ecosystem plugin (UE5.7)
Server-authoritative, data-driven item/inventory/interaction/carry/equipment/
world-slot/placement/crafting framework. 8 modules, compiles against UE 5.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:49:56 +03:00

337 lines
9.0 KiB
C++

// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "HeldItemComponent.h"
#include "PickupItemActor.h"
#include "ItemWorldLog.h"
#include "ItemNativeTags.h"
#include "ItemDatabaseSubsystem.h"
#include "ItemDefinitionRow.h"
#include "ItemAuxiliaryRows.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/PrimitiveComponent.h"
#include "PhysicsEngine/PhysicsHandleComponent.h"
#include "Net/UnrealNetwork.h"
UHeldItemComponent::UHeldItemComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
SetIsReplicatedByDefault(true);
}
void UHeldItemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (HeldItem.Mode == EHeldItemMode::CarryPhysicsHandle && HasAuthority())
{
UpdatePhysicsCarryTarget();
}
}
bool UHeldItemComponent::ServerHoldWorldItemPhysics(AActor* WorldItem)
{
if (!HasAuthority() || HeldItem.bHasHeldItem || !WorldItem)
{
return false;
}
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(WorldItem);
FItemInstanceData Item;
if (!Provider || !Provider->IsItemAvailable() || !Provider->GetProvidedItem(Item) || !CanHoldItem(Item))
{
return false;
}
UPrimitiveComponent* Prim = nullptr;
if (APickupItemActor* Pickup = Cast<APickupItemActor>(WorldItem))
{
Prim = Pickup->GetMeshComponent();
}
if (!Prim)
{
Prim = Cast<UPrimitiveComponent>(WorldItem->GetRootComponent());
}
if (!Prim)
{
return false;
}
// Claim the item but keep physics enabled (unlike attached carry).
Provider->SetCarried(GetOwner(), true);
Prim->SetSimulatePhysics(true);
Prim->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
if (!PhysicsHandle)
{
PhysicsHandle = NewObject<UPhysicsHandleComponent>(this, TEXT("CarryPhysicsHandle"));
PhysicsHandle->RegisterComponent();
}
PhysicsHandle->GrabComponentAtLocation(Prim, NAME_None, Prim->GetComponentLocation());
HeldItem.bHasHeldItem = true;
HeldItem.InstanceId = Item.InstanceId;
HeldItem.ItemId = Item.ItemId;
HeldItem.Mode = EHeldItemMode::CarryPhysicsHandle;
HeldItem.HeldWorldActor = WorldItem;
HeldItem.MoveSpeedMultiplier = 0.9f;
HeldItem.Item = Item;
HeldItem.Item.LocationType = EItemLocationType::Held;
ApplyMovementState();
SetComponentTickEnabled(true);
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
void UHeldItemComponent::UpdatePhysicsCarryTarget()
{
if (!PhysicsHandle || !PhysicsHandle->GetGrabbedComponent() || !GetOwner())
{
return;
}
FRotator ViewRot = GetOwner()->GetActorRotation();
if (const APawn* Pawn = Cast<APawn>(GetOwner()))
{
ViewRot = Pawn->GetControlRotation();
}
const FVector Target = GetOwner()->GetActorLocation() + ViewRot.Vector() * PhysicsHoldDistance + FVector(0, 0, 50.f);
PhysicsHandle->SetTargetLocationAndRotation(Target, ViewRot);
}
void UHeldItemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UHeldItemComponent, HeldItem);
}
bool UHeldItemComponent::HasAuthority() const
{
return GetOwner() && GetOwner()->HasAuthority();
}
bool UHeldItemComponent::CanHoldItem(const FItemInstanceData& Item) const
{
if (HeldItem.bHasHeldItem || !Item.IsValid())
{
return false;
}
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
{
return Def->bCanWorldCarry;
}
}
return false;
}
bool UHeldItemComponent::ServerHoldWorldItem(AActor* WorldItem)
{
if (!HasAuthority() || HeldItem.bHasHeldItem || !WorldItem)
{
return false;
}
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(WorldItem);
if (!Provider || !Provider->IsItemAvailable())
{
return false;
}
FItemInstanceData Item;
if (!Provider->GetProvidedItem(Item) || !CanHoldItem(Item))
{
return false;
}
// Attach the world actor to the character's carry socket.
USceneComponent* AttachParent = nullptr;
if (const ACharacter* Char = Cast<ACharacter>(GetOwner()))
{
AttachParent = Char->GetMesh();
}
if (!AttachParent)
{
AttachParent = GetOwner()->GetRootComponent();
}
WorldItem->AttachToComponent(AttachParent, FAttachmentTransformRules::SnapToTargetIncludingScale, CarrySocket);
Provider->SetCarried(GetOwner(), true);
// Resolve carry move-speed multiplier from the feature table.
float MoveMult = 1.f;
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
TArray<FItemFeatureRow> Features;
DB->GetFeatureRows(Item.ItemId, Features);
for (const FItemFeatureRow& Row : Features)
{
if (Row.Param == FName("MoveSpeedMultiplier"))
{
MoveMult = Row.GetFloat(1.f);
break;
}
}
}
HeldItem.bHasHeldItem = true;
HeldItem.InstanceId = Item.InstanceId;
HeldItem.ItemId = Item.ItemId;
HeldItem.Mode = EHeldItemMode::CarryAttached;
HeldItem.HeldWorldActor = WorldItem;
HeldItem.HoldSocket = CarrySocket;
HeldItem.MoveSpeedMultiplier = MoveMult;
HeldItem.bBlocksSprint = MoveMult < 0.9f;
HeldItem.Item = Item;
HeldItem.Item.LocationType = EItemLocationType::Held;
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] %s carried %s"), *GetNameSafe(GetOwner()), *Item.ItemId.ToString());
return true;
}
void UHeldItemComponent::RequestDrop()
{
Server_Drop();
}
void UHeldItemComponent::Server_Drop_Implementation()
{
ServerDropHeldItem();
}
bool UHeldItemComponent::ServerDropHeldItem()
{
if (!HasAuthority() || !HeldItem.bHasHeldItem)
{
return false;
}
const FVector Fwd = GetOwner()->GetActorForwardVector();
const FTransform DropXf(GetOwner()->GetActorRotation(),
GetOwner()->GetActorLocation() + Fwd * 100.f + FVector(0, 0, 20.f));
if (APickupItemActor* Pickup = Cast<APickupItemActor>(HeldItem.HeldWorldActor))
{
Pickup->ReleaseToWorld(DropXf);
}
else
{
APickupItemActor::SpawnPickupFromInstance(this, HeldItem.Item, DropXf);
if (HeldItem.HeldWorldActor)
{
HeldItem.HeldWorldActor->Destroy();
}
}
// NoiseOnDrop feature: emit a hearing stimulus (section 21.6).
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
TArray<FItemFeatureRow> Noise;
DB->GetFeatureRows(HeldItem.ItemId, ItemEcosystemTags::Feature_NoiseOnDrop, Noise);
for (const FItemFeatureRow& Row : Noise)
{
if (Row.Param == FName("Radius"))
{
const float Radius = Row.GetFloat(0.f);
if (Radius > 0.f)
{
GetOwner()->MakeNoise(1.f, Cast<APawn>(GetOwner()), DropXf.GetLocation(), Radius);
}
break;
}
}
}
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
SetComponentTickEnabled(false);
HeldItem = FHeldItemState();
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::ServerConsumeHeldItem(FItemInstanceData& OutItem)
{
if (!HasAuthority() || !HeldItem.bHasHeldItem)
{
return false;
}
OutItem = HeldItem.Item;
if (HeldItem.HeldWorldActor)
{
HeldItem.HeldWorldActor->Destroy();
}
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
SetComponentTickEnabled(false);
HeldItem = FHeldItemState();
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::ServerHandOffHeldItem(FItemInstanceData& OutItem, AActor*& OutActor)
{
if (!HasAuthority() || !HeldItem.bHasHeldItem)
{
return false;
}
OutItem = HeldItem.Item;
OutActor = HeldItem.HeldWorldActor;
if (OutActor)
{
OutActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
}
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
SetComponentTickEnabled(false);
HeldItem = FHeldItemState();
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::ServerUpdateHeldItem(const FItemInstanceData& NewItem)
{
if (!HasAuthority() || !HeldItem.bHasHeldItem || NewItem.InstanceId != HeldItem.InstanceId)
{
return false;
}
HeldItem.Item = NewItem;
HeldItem.Item.LocationType = EItemLocationType::Held;
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::GetHeldItem(FItemInstanceData& OutItem) const
{
if (!HeldItem.bHasHeldItem)
{
return false;
}
OutItem = HeldItem.Item;
return true;
}
void UHeldItemComponent::OnRep_HeldItem(FHeldItemState /*OldState*/)
{
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
}
void UHeldItemComponent::ApplyMovementState()
{
ACharacter* Char = Cast<ACharacter>(GetOwner());
UCharacterMovementComponent* Move = Char ? Char->GetCharacterMovement() : nullptr;
if (!Move)
{
return;
}
// Cache the unmodified speed once, then re-derive from it each time.
if (CachedBaseWalkSpeed <= 0.f)
{
CachedBaseWalkSpeed = Move->MaxWalkSpeed;
}
const float Mult = HeldItem.bHasHeldItem ? HeldItem.MoveSpeedMultiplier : 1.f;
Move->MaxWalkSpeed = CachedBaseWalkSpeed * Mult;
}