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>
This commit is contained in:
150
Source/ItemWorld/Private/EquipmentComponent.cpp
Normal file
150
Source/ItemWorld/Private/EquipmentComponent.cpp
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "EquipmentComponent.h"
|
||||
#include "ItemWorldLog.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemDefinitionRow.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
UEquipmentComponent::UEquipmentComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UEquipmentComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
DOREPLIFETIME(UEquipmentComponent, Equipped);
|
||||
}
|
||||
|
||||
void UEquipmentComponent::OnUnregister()
|
||||
{
|
||||
if (HasAuthority())
|
||||
{
|
||||
for (const FEquippedItemEntry& Entry : Equipped)
|
||||
{
|
||||
if (Entry.EquippedActor)
|
||||
{
|
||||
Entry.EquippedActor->Destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
Super::OnUnregister();
|
||||
}
|
||||
|
||||
bool UEquipmentComponent::HasAuthority() const
|
||||
{
|
||||
return GetOwner() && GetOwner()->HasAuthority();
|
||||
}
|
||||
|
||||
USceneComponent* UEquipmentComponent::GetAttachTarget() const
|
||||
{
|
||||
if (const ACharacter* Char = Cast<ACharacter>(GetOwner()))
|
||||
{
|
||||
return Char->GetMesh();
|
||||
}
|
||||
return GetOwner() ? GetOwner()->GetRootComponent() : nullptr;
|
||||
}
|
||||
|
||||
FName UEquipmentComponent::ResolveSocket(const FGameplayTag& SlotTag) const
|
||||
{
|
||||
if (const FName* Found = SlotSockets.Find(SlotTag))
|
||||
{
|
||||
return *Found;
|
||||
}
|
||||
return DefaultEquipSocket;
|
||||
}
|
||||
|
||||
bool UEquipmentComponent::IsSlotEquipped(FGameplayTag SlotTag) const
|
||||
{
|
||||
return Equipped.ContainsByPredicate([&SlotTag](const FEquippedItemEntry& E) { return E.SlotTag == SlotTag; });
|
||||
}
|
||||
|
||||
bool UEquipmentComponent::ServerEquip(FGuid InstanceId, FGameplayTag SlotTag)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UInventoryComponent* Inv = GetOwner()->FindComponentByClass<UInventoryComponent>();
|
||||
if (!Inv)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FItemInstanceData Item;
|
||||
if (!Inv->FindItem(InstanceId, Item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||
if (!Def || !Def->bCanEquip)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Slot compatibility (if the item restricts equipment slots).
|
||||
if (!Def->AllowedEquipmentSlots.IsEmpty() && !Def->AllowedEquipmentSlots.HasTag(SlotTag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free the slot first.
|
||||
ServerUnequip(SlotTag);
|
||||
|
||||
FEquippedItemEntry Entry;
|
||||
Entry.SlotTag = SlotTag;
|
||||
Entry.InstanceId = InstanceId;
|
||||
Entry.ItemId = Item.ItemId;
|
||||
|
||||
if (!Def->EquippedActorClass.IsNull())
|
||||
{
|
||||
if (UClass* ActorClass = Def->EquippedActorClass.LoadSynchronous())
|
||||
{
|
||||
FActorSpawnParameters Params;
|
||||
Params.Owner = GetOwner();
|
||||
Params.Instigator = Cast<APawn>(GetOwner());
|
||||
AActor* Spawned = GetWorld()->SpawnActor<AActor>(ActorClass, FTransform::Identity, Params);
|
||||
if (Spawned)
|
||||
{
|
||||
Spawned->AttachToComponent(GetAttachTarget(), FAttachmentTransformRules::SnapToTargetIncludingScale, ResolveSocket(SlotTag));
|
||||
Entry.EquippedActor = Spawned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Equipped.Add(Entry);
|
||||
OnEquipmentChanged.Broadcast();
|
||||
UE_LOG(LogItemWorld, Log, TEXT("[Inventory] equipped %s in %s"), *Item.ItemId.ToString(), *SlotTag.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UEquipmentComponent::ServerUnequip(FGameplayTag SlotTag)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int32 Index = Equipped.IndexOfByPredicate([&SlotTag](const FEquippedItemEntry& E) { return E.SlotTag == SlotTag; });
|
||||
if (Index == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Equipped[Index].EquippedActor)
|
||||
{
|
||||
Equipped[Index].EquippedActor->Destroy();
|
||||
}
|
||||
Equipped.RemoveAt(Index);
|
||||
OnEquipmentChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UEquipmentComponent::OnRep_Equipped()
|
||||
{
|
||||
OnEquipmentChanged.Broadcast();
|
||||
}
|
||||
50
Source/ItemWorld/Private/FuelTankComponent.cpp
Normal file
50
Source/ItemWorld/Private/FuelTankComponent.cpp
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "FuelTankComponent.h"
|
||||
#include "ItemWorldLog.h"
|
||||
#include "ItemNativeTags.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
UFuelTankComponent::UFuelTankComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UFuelTankComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
DOREPLIFETIME(UFuelTankComponent, CurrentFuel);
|
||||
}
|
||||
|
||||
bool UFuelTankComponent::HasAuthority() const
|
||||
{
|
||||
return GetOwner() && GetOwner()->HasAuthority();
|
||||
}
|
||||
|
||||
bool UFuelTankComponent::ReceiveHeldUse(FName ResultVerb, FItemInstanceData& HeldItem, AActor* Instigator)
|
||||
{
|
||||
if (!HasAuthority() || ResultVerb != FName("TransferFuel"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float Available = HeldItem.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||
const float Room = FMath::Max(0.f, MaxFuel - CurrentFuel);
|
||||
const float Moved = FMath::Min3(PourAmount, Available, Room);
|
||||
if (Moved <= 0.f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentFuel += Moved;
|
||||
HeldItem.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, Available - Moved);
|
||||
OnFuelChanged.Broadcast(CurrentFuel);
|
||||
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] poured %.1f fuel (now %.1f/%.1f)"), Moved, CurrentFuel, MaxFuel);
|
||||
return true;
|
||||
}
|
||||
|
||||
void UFuelTankComponent::OnRep_Fuel()
|
||||
{
|
||||
OnFuelChanged.Broadcast(CurrentFuel);
|
||||
}
|
||||
336
Source/ItemWorld/Private/HeldItemComponent.cpp
Normal file
336
Source/ItemWorld/Private/HeldItemComponent.cpp
Normal file
@ -0,0 +1,336 @@
|
||||
// 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;
|
||||
}
|
||||
8
Source/ItemWorld/Private/ItemWorldModule.cpp
Normal file
8
Source/ItemWorld/Private/ItemWorldModule.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ItemWorldLog.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogItemWorld);
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemWorld)
|
||||
206
Source/ItemWorld/Private/PickupItemActor.cpp
Normal file
206
Source/ItemWorld/Private/PickupItemActor.cpp
Normal file
@ -0,0 +1,206 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "PickupItemActor.h"
|
||||
#include "ItemWorldLog.h"
|
||||
#include "InteractableComponent.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemDefinitionRow.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
APickupItemActor::APickupItemActor()
|
||||
{
|
||||
bReplicates = true;
|
||||
SetReplicateMovement(true);
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
SetRootComponent(Root);
|
||||
|
||||
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
Mesh->SetupAttachment(Root);
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
|
||||
Mesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
|
||||
Interactable = CreateDefaultSubobject<UInteractableComponent>(TEXT("Interactable"));
|
||||
}
|
||||
|
||||
void APickupItemActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
DOREPLIFETIME(APickupItemActor, ItemData);
|
||||
DOREPLIFETIME(APickupItemActor, bIsLocked);
|
||||
DOREPLIFETIME(APickupItemActor, CurrentHolder);
|
||||
}
|
||||
|
||||
void APickupItemActor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
void APickupItemActor::InitializeFromItem(const FItemInstanceData& InItem)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemData = InItem;
|
||||
ItemData.EnsureInstanceId();
|
||||
ItemData.LocationType = EItemLocationType::World;
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
APickupItemActor* APickupItemActor::SpawnPickup(UObject* WorldContext, FName ItemId, int32 Quantity, const FTransform& Transform)
|
||||
{
|
||||
UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
if (!DB || !DB->HasItem(ItemId))
|
||||
{
|
||||
UE_LOG(LogItemWorld, Warning, TEXT("SpawnPickup: unknown item '%s'."), *ItemId.ToString());
|
||||
return nullptr;
|
||||
}
|
||||
const FItemInstanceData Item = DB->MakeItemInstance(ItemId, Quantity);
|
||||
return SpawnPickupFromInstance(WorldContext, Item, Transform);
|
||||
}
|
||||
|
||||
APickupItemActor* APickupItemActor::SpawnPickupFromInstance(UObject* WorldContext, const FItemInstanceData& Item, const FTransform& Transform)
|
||||
{
|
||||
UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr;
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Prefer the item's authored pickup actor class; fall back to the base class.
|
||||
TSubclassOf<AActor> SpawnClass = APickupItemActor::StaticClass();
|
||||
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext))
|
||||
{
|
||||
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
||||
{
|
||||
if (!Def->PickupActorClass.IsNull())
|
||||
{
|
||||
if (UClass* Loaded = Def->PickupActorClass.LoadSynchronous())
|
||||
{
|
||||
if (Loaded->IsChildOf(APickupItemActor::StaticClass()))
|
||||
{
|
||||
SpawnClass = Loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
APickupItemActor* Pickup = World->SpawnActor<APickupItemActor>(SpawnClass, Transform, Params);
|
||||
if (Pickup)
|
||||
{
|
||||
Pickup->InitializeFromItem(Item);
|
||||
}
|
||||
return Pickup;
|
||||
}
|
||||
|
||||
bool APickupItemActor::GetProvidedItem(FItemInstanceData& OutItem) const
|
||||
{
|
||||
if (!ItemData.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutItem = ItemData;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APickupItemActor::IsItemAvailable() const
|
||||
{
|
||||
return !bIsLocked && ItemData.IsValid();
|
||||
}
|
||||
|
||||
void APickupItemActor::NotifyItemTaken(AActor* Taker)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
UE_LOG(LogItemWorld, Verbose, TEXT("Pickup %s taken by %s; destroying."), *GetName(), *GetNameSafe(Taker));
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void APickupItemActor::UpdateProvidedItem(const FItemInstanceData& RemainingItem)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (RemainingItem.Quantity <= 0)
|
||||
{
|
||||
Destroy();
|
||||
return;
|
||||
}
|
||||
ItemData = RemainingItem;
|
||||
ItemData.LocationType = EItemLocationType::World;
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
void APickupItemActor::SetCarried(AActor* Holder, bool bCarried)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
bIsLocked = bCarried;
|
||||
CurrentHolder = bCarried ? Holder : nullptr;
|
||||
if (Mesh)
|
||||
{
|
||||
Mesh->SetSimulatePhysics(false);
|
||||
Mesh->SetCollisionEnabled(bCarried ? ECollisionEnabled::NoCollision : ECollisionEnabled::QueryAndPhysics);
|
||||
}
|
||||
}
|
||||
|
||||
void APickupItemActor::ReleaseToWorld(const FTransform& WorldTransform)
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
|
||||
SetActorTransform(WorldTransform);
|
||||
bIsLocked = false;
|
||||
CurrentHolder = nullptr;
|
||||
if (Mesh)
|
||||
{
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
}
|
||||
if (ItemData.IsValid())
|
||||
{
|
||||
ItemData.LocationType = EItemLocationType::World;
|
||||
}
|
||||
}
|
||||
|
||||
void APickupItemActor::OnRep_ItemData()
|
||||
{
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
void APickupItemActor::RefreshVisuals()
|
||||
{
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemData.ItemId) : nullptr;
|
||||
if (!Def)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mesh && !Def->WorldMesh.IsNull())
|
||||
{
|
||||
if (UStaticMesh* LoadedMesh = Def->WorldMesh.LoadSynchronous())
|
||||
{
|
||||
Mesh->SetStaticMesh(LoadedMesh);
|
||||
}
|
||||
}
|
||||
if (Interactable && Interactable->DisplayName.IsEmpty())
|
||||
{
|
||||
Interactable->DisplayName = Def->DisplayName;
|
||||
}
|
||||
}
|
||||
21
Source/ItemWorld/Private/StorageContainerActor.cpp
Normal file
21
Source/ItemWorld/Private/StorageContainerActor.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "StorageContainerActor.h"
|
||||
#include "InteractableComponent.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
|
||||
AStorageContainerActor::AStorageContainerActor()
|
||||
{
|
||||
bReplicates = true;
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
SetRootComponent(Mesh);
|
||||
|
||||
Interactable = CreateDefaultSubobject<UInteractableComponent>(TEXT("Interactable"));
|
||||
|
||||
Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));
|
||||
// Shared container: visible to whoever opens it, not just an owning client.
|
||||
Storage->bOwnerOnlyReplication = false;
|
||||
}
|
||||
106
Source/ItemWorld/Private/WorldInteractionActions.cpp
Normal file
106
Source/ItemWorld/Private/WorldInteractionActions.cpp
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "WorldInteractionActions.h"
|
||||
#include "ItemInteractionComponent.h"
|
||||
#include "ItemNativeTags.h"
|
||||
#include "GameFramework/Character.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool RouteToComponent(const FInteractionContext& Context, EItemInteractionMode Mode, const FGameplayTag& ActionTag)
|
||||
{
|
||||
if (!Context.Character)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (UItemInteractionComponent* Comp = Context.Character->FindComponentByClass<UItemInteractionComponent>())
|
||||
{
|
||||
return Comp->ServerExecuteMode(Context, Mode, ActionTag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
UInteractionAction_CarryAttached::UInteractionAction_CarryAttached()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_Carry;
|
||||
Mode = EItemInteractionMode::CarryAttached;
|
||||
Priority = 500;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "Carry", "Carry");
|
||||
}
|
||||
void UInteractionAction_CarryAttached::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
|
||||
UInteractionAction_CarryPhysics::UInteractionAction_CarryPhysics()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_Carry;
|
||||
Mode = EItemInteractionMode::CarryPhysicsHandle;
|
||||
Priority = 500;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "CarryPhysics", "Pick Up");
|
||||
}
|
||||
void UInteractionAction_CarryPhysics::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
|
||||
UInteractionAction_Equip::UInteractionAction_Equip()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_Equip;
|
||||
Mode = EItemInteractionMode::EquipFromInventory;
|
||||
Priority = 800;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "Equip", "Equip");
|
||||
}
|
||||
void UInteractionAction_Equip::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
|
||||
UInteractionAction_DropHeldItem::UInteractionAction_DropHeldItem()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_Drop;
|
||||
Mode = EItemInteractionMode::DropHeldItem;
|
||||
Priority = 300;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "Drop", "Drop");
|
||||
}
|
||||
void UInteractionAction_DropHeldItem::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
|
||||
UInteractionAction_InstallIntoWorldSlot::UInteractionAction_InstallIntoWorldSlot()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_Install;
|
||||
Mode = EItemInteractionMode::InstallIntoWorldSlot;
|
||||
Priority = 900;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "Install", "Install");
|
||||
}
|
||||
void UInteractionAction_InstallIntoWorldSlot::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
|
||||
UInteractionAction_RemoveFromWorldSlot::UInteractionAction_RemoveFromWorldSlot()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_Remove;
|
||||
Mode = EItemInteractionMode::RemoveFromWorldSlot;
|
||||
Priority = 400;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "Remove", "Remove");
|
||||
}
|
||||
void UInteractionAction_RemoveFromWorldSlot::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
|
||||
UInteractionAction_UseHeldOnTarget::UInteractionAction_UseHeldOnTarget()
|
||||
{
|
||||
ActionTag = ItemEcosystemTags::Action_UseHeldOnTarget;
|
||||
Mode = EItemInteractionMode::UseHeldOnTarget;
|
||||
Priority = 800;
|
||||
DisplayText = NSLOCTEXT("ItemWorld", "Use", "Use");
|
||||
}
|
||||
void UInteractionAction_UseHeldOnTarget::Execute_Implementation(const FInteractionContext& Context)
|
||||
{
|
||||
RouteToComponent(Context, Mode, ActionTag);
|
||||
}
|
||||
261
Source/ItemWorld/Private/WorldInteractionComponent.cpp
Normal file
261
Source/ItemWorld/Private/WorldInteractionComponent.cpp
Normal file
@ -0,0 +1,261 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "WorldInteractionComponent.h"
|
||||
#include "HeldItemComponent.h"
|
||||
#include "EquipmentComponent.h"
|
||||
#include "WorldItemSlotComponent.h"
|
||||
#include "PickupItemActor.h"
|
||||
#include "ItemWorldLog.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemDefinitionRow.h"
|
||||
#include "ItemAuxiliaryRows.h"
|
||||
#include "ItemInteractionInterfaces.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
|
||||
{
|
||||
switch (Option.Mode)
|
||||
{
|
||||
case EItemInteractionMode::CarryAttached:
|
||||
return ExecuteCarry(Context, /*bPhysics*/ false);
|
||||
|
||||
case EItemInteractionMode::CarryPhysicsHandle:
|
||||
return ExecuteCarry(Context, /*bPhysics*/ true);
|
||||
|
||||
case EItemInteractionMode::DropHeldItem:
|
||||
return ExecuteDropHeld(Context);
|
||||
|
||||
case EItemInteractionMode::InstallIntoWorldSlot:
|
||||
return ExecuteInstall(Context);
|
||||
|
||||
case EItemInteractionMode::RemoveFromWorldSlot:
|
||||
return ExecuteRemoveFromSlot(Context);
|
||||
|
||||
case EItemInteractionMode::EquipFromInventory:
|
||||
return ExecuteEquip(Context, Request);
|
||||
|
||||
case EItemInteractionMode::UseHeldOnTarget:
|
||||
return ExecuteUseHeldOnTarget(Context, Option);
|
||||
|
||||
default:
|
||||
return Super::ExecuteAuthorized(Context, Option, Request);
|
||||
}
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteCarry(const FInteractionContext& Context, bool bPhysics)
|
||||
{
|
||||
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||
if (!Held || !Context.TargetActor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return bPhysics ? Held->ServerHoldWorldItemPhysics(Context.TargetActor)
|
||||
: Held->ServerHoldWorldItem(Context.TargetActor);
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteDropHeld(const FInteractionContext& Context)
|
||||
{
|
||||
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||
return Held && Held->ServerDropHeldItem();
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteInstall(const FInteractionContext& Context)
|
||||
{
|
||||
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||
if (!Held || !Held->HasHeldItem() || !Context.TargetActor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FItemInstanceData HeldItem;
|
||||
Held->GetHeldItem(HeldItem);
|
||||
|
||||
// Find a compatible, free slot on the target.
|
||||
UWorldItemSlotComponent* Chosen = nullptr;
|
||||
TArray<UWorldItemSlotComponent*> Slots;
|
||||
Context.TargetActor->GetComponents<UWorldItemSlotComponent>(Slots);
|
||||
for (UWorldItemSlotComponent* Slot : Slots)
|
||||
{
|
||||
if (Slot && Slot->CanInstall(HeldItem))
|
||||
{
|
||||
Chosen = Slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Chosen)
|
||||
{
|
||||
UE_LOG(LogItemWorld, Warning, TEXT("[ServerReject] Install failed: no compatible free slot."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hand the carried actor over to the slot as its visual.
|
||||
FItemInstanceData Item;
|
||||
AActor* Visual = nullptr;
|
||||
if (!Held->ServerHandOffHeldItem(Item, Visual))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Chosen->InstallItem(Item, Visual))
|
||||
{
|
||||
// Roll back: drop the item back to the world so it is not lost.
|
||||
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 100.f);
|
||||
if (APickupItemActor* Pickup = Cast<APickupItemActor>(Visual))
|
||||
{
|
||||
Pickup->ReleaseToWorld(Xf);
|
||||
}
|
||||
else
|
||||
{
|
||||
APickupItemActor::SpawnPickupFromInstance(this, Item, Xf);
|
||||
if (Visual) { Visual->Destroy(); }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteEquip(const FInteractionContext& Context, const FInteractionRequest& Request)
|
||||
{
|
||||
UEquipmentComponent* Equip = GetOwner()->FindComponentByClass<UEquipmentComponent>();
|
||||
if (!Equip)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FGameplayTag SlotTag = Request.TargetSlotTag;
|
||||
if (!SlotTag.IsValid() && Context.PlayerInventory)
|
||||
{
|
||||
// Fall back to the item's first allowed equipment slot.
|
||||
FItemInstanceData Item;
|
||||
if (Context.PlayerInventory->FindItem(Request.ItemInstanceId, Item))
|
||||
{
|
||||
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||
{
|
||||
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
||||
{
|
||||
if (!Def->AllowedEquipmentSlots.IsEmpty())
|
||||
{
|
||||
SlotTag = Def->AllowedEquipmentSlots.First();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Equip->ServerEquip(Request.ItemInstanceId, SlotTag);
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteUseHeldOnTarget(const FInteractionContext& Context, const FInteractionOption& Option)
|
||||
{
|
||||
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||
if (!Held || !Held->HasHeldItem() || !Context.TargetActor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FItemInstanceData HeldItem;
|
||||
Held->GetHeldItem(HeldItem);
|
||||
|
||||
// Find the result verb from the data-driven interaction row for this held item.
|
||||
FName ResultVerb = NAME_None;
|
||||
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||
{
|
||||
TArray<FItemInteractionRow> Rows;
|
||||
DB->GetInteractionRows(HeldItem.ItemId, Rows);
|
||||
for (const FItemInteractionRow& Row : Rows)
|
||||
{
|
||||
if (Row.ActionTag == Option.ActionTag) { ResultVerb = Row.Result; break; }
|
||||
}
|
||||
}
|
||||
if (ResultVerb.IsNone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Locate an IItemUseTarget on the target actor or its components.
|
||||
IItemUseTarget* UseTarget = Cast<IItemUseTarget>(Context.TargetActor);
|
||||
if (!UseTarget)
|
||||
{
|
||||
for (UActorComponent* Comp : Context.TargetActor->GetComponents())
|
||||
{
|
||||
if (IItemUseTarget* AsTarget = Cast<IItemUseTarget>(Comp)) { UseTarget = AsTarget; break; }
|
||||
}
|
||||
}
|
||||
if (!UseTarget)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (UseTarget->ReceiveHeldUse(ResultVerb, HeldItem, GetOwner()))
|
||||
{
|
||||
Held->ServerUpdateHeldItem(HeldItem);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::ExecuteRemoveFromSlot(const FInteractionContext& Context)
|
||||
{
|
||||
if (!Context.TargetActor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Find the first occupied slot on the target.
|
||||
UWorldItemSlotComponent* Occupied = nullptr;
|
||||
TArray<UWorldItemSlotComponent*> Slots;
|
||||
Context.TargetActor->GetComponents<UWorldItemSlotComponent>(Slots);
|
||||
for (UWorldItemSlotComponent* Slot : Slots)
|
||||
{
|
||||
if (Slot && Slot->IsOccupied()) { Occupied = Slot; break; }
|
||||
}
|
||||
if (!Occupied)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FItemInstanceData Item;
|
||||
if (!Occupied->RemoveItem(Item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer the player's inventory; otherwise drop it at their feet.
|
||||
UInventoryComponent* Inv = Context.PlayerInventory;
|
||||
if (Inv && Inv->AddItem(Item) && Item.Quantity <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 120.f + FVector(0, 0, 30.f));
|
||||
APickupItemActor::SpawnPickupFromInstance(this, Item, Xf);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UWorldInteractionComponent::HandleDropFromInventory(FGuid InstanceId)
|
||||
{
|
||||
APawn* Pawn = Cast<APawn>(GetOwner());
|
||||
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||
if (!Inv)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FItemInstanceData Item;
|
||||
if (!Inv->FindItem(InstanceId, Item) || Item.Lock.bLocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||
if (Def && !Def->bCanDrop)
|
||||
{
|
||||
UE_LOG(LogItemWorld, Warning, TEXT("[ServerReject] Drop failed: %s cannot be dropped."), *Item.ItemId.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 120.f + FVector(0, 0, 20.f));
|
||||
if (APickupItemActor::SpawnPickupFromInstance(this, Item, Xf))
|
||||
{
|
||||
Inv->RemoveItem(InstanceId);
|
||||
UE_LOG(LogItemWorld, Log, TEXT("[Inventory] dropped %s"), *Item.ItemId.ToString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
129
Source/ItemWorld/Private/WorldItemSlotComponent.cpp
Normal file
129
Source/ItemWorld/Private/WorldItemSlotComponent.cpp
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "WorldItemSlotComponent.h"
|
||||
#include "PickupItemActor.h"
|
||||
#include "ItemWorldLog.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemDefinitionRow.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
|
||||
UWorldItemSlotComponent::UWorldItemSlotComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UWorldItemSlotComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
DOREPLIFETIME(UWorldItemSlotComponent, InstalledItem);
|
||||
DOREPLIFETIME(UWorldItemSlotComponent, InstalledVisual);
|
||||
}
|
||||
|
||||
bool UWorldItemSlotComponent::HasAuthority() const
|
||||
{
|
||||
return GetOwner() && GetOwner()->HasAuthority();
|
||||
}
|
||||
|
||||
USceneComponent* UWorldItemSlotComponent::GetAttachTarget() const
|
||||
{
|
||||
return GetOwner() ? GetOwner()->GetRootComponent() : nullptr;
|
||||
}
|
||||
|
||||
bool UWorldItemSlotComponent::CanInstall(const FItemInstanceData& Item) const
|
||||
{
|
||||
if (InstalledItem.bHasItem || !Item.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||
if (!Def || !Def->bCanInstallIntoWorldSlot)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// The item must declare this slot as compatible.
|
||||
if (!Def->CompatibleWorldSlots.HasTag(SlotTag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Optional extra tag gate.
|
||||
if (!AcceptedItemTags.IsEmpty())
|
||||
{
|
||||
FGameplayTagContainer ItemTags = Def->ItemTags;
|
||||
ItemTags.AddTag(Def->ItemType);
|
||||
if (!ItemTags.HasAny(AcceptedItemTags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UWorldItemSlotComponent::InstallItem(const FItemInstanceData& Item, AActor* ProvidedVisual)
|
||||
{
|
||||
if (!HasAuthority() || !CanInstall(Item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
InstalledItem.bHasItem = true;
|
||||
InstalledItem.Item = Item;
|
||||
InstalledItem.Item.LocationType = EItemLocationType::Installed;
|
||||
|
||||
if (ProvidedVisual)
|
||||
{
|
||||
// Reuse the carried actor as the visual.
|
||||
if (USceneComponent* Target = GetAttachTarget())
|
||||
{
|
||||
ProvidedVisual->AttachToComponent(Target, FAttachmentTransformRules::SnapToTargetIncludingScale, AttachSocketName);
|
||||
}
|
||||
if (IWorldItemProvider* Provider = Cast<IWorldItemProvider>(ProvidedVisual))
|
||||
{
|
||||
Provider->SetCarried(GetOwner(), true); // keep collision off, locked
|
||||
}
|
||||
InstalledVisual = ProvidedVisual;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Spawn a fresh visual pickup and attach it.
|
||||
const FTransform Xf = GetAttachTarget() ? GetAttachTarget()->GetComponentTransform() : GetOwner()->GetActorTransform();
|
||||
if (APickupItemActor* Visual = APickupItemActor::SpawnPickupFromInstance(this, Item, Xf))
|
||||
{
|
||||
Visual->SetCarried(GetOwner(), true);
|
||||
if (USceneComponent* Target = GetAttachTarget())
|
||||
{
|
||||
Visual->AttachToComponent(Target, FAttachmentTransformRules::SnapToTargetIncludingScale, AttachSocketName);
|
||||
}
|
||||
InstalledVisual = Visual;
|
||||
}
|
||||
}
|
||||
|
||||
OnSlotChanged.Broadcast();
|
||||
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] installed %s into %s"), *Item.ItemId.ToString(), *SlotTag.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UWorldItemSlotComponent::RemoveItem(FItemInstanceData& OutItem)
|
||||
{
|
||||
if (!HasAuthority() || !InstalledItem.bHasItem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutItem = InstalledItem.Item;
|
||||
|
||||
if (InstalledVisual)
|
||||
{
|
||||
InstalledVisual->Destroy();
|
||||
InstalledVisual = nullptr;
|
||||
}
|
||||
|
||||
InstalledItem = FWorldSlotInstalledState();
|
||||
OnSlotChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UWorldItemSlotComponent::OnRep_InstalledItem()
|
||||
{
|
||||
OnSlotChanged.Broadcast();
|
||||
}
|
||||
Reference in New Issue
Block a user