// 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(WorldItem); FItemInstanceData Item; if (!Provider || !Provider->IsItemAvailable() || !Provider->GetProvidedItem(Item) || !CanHoldItem(Item)) { return false; } UPrimitiveComponent* Prim = nullptr; if (APickupItemActor* Pickup = Cast(WorldItem)) { Prim = Pickup->GetMeshComponent(); } if (!Prim) { Prim = Cast(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(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(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& 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(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(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 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(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 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(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(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; }