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>
337 lines
9.0 KiB
C++
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;
|
|
}
|