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>
163 lines
4.9 KiB
C++
163 lines
4.9 KiB
C++
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
|
|
|
#include "InteractionAction.h"
|
|
#include "InteractableComponent.h"
|
|
#include "ItemInteractionLog.h"
|
|
#include "ItemNativeTags.h"
|
|
#include "ItemDatabaseSubsystem.h"
|
|
#include "ItemDefinitionRow.h"
|
|
#include "InventoryComponent.h"
|
|
#include "GameFramework/Character.h"
|
|
|
|
namespace
|
|
{
|
|
/** Picks the item a requirement should inspect: held first, else the target item. */
|
|
const FItemInstanceData* ResolveSubjectItem(const FInteractionContext& Ctx)
|
|
{
|
|
if (Ctx.bHasHeldItem) { return &Ctx.HeldItem; }
|
|
if (Ctx.bHasTargetItem) { return &Ctx.TargetItem; }
|
|
return nullptr;
|
|
}
|
|
|
|
bool ItemHasTag(const UObject* WorldCtx, const FItemInstanceData& Item, const FGameplayTag& Tag)
|
|
{
|
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldCtx))
|
|
{
|
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
|
{
|
|
return Def->ItemTags.HasTag(Tag) || Def->ItemType.MatchesTag(Tag);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool EvaluateRequirement(const FInteractionRequirement& Req, const FInteractionContext& Ctx)
|
|
{
|
|
switch (Req.RequirementType)
|
|
{
|
|
case EInteractionRequirementType::None:
|
|
return true;
|
|
|
|
case EInteractionRequirementType::HasInventorySpace:
|
|
{
|
|
if (!Ctx.PlayerInventory) { return false; }
|
|
const FItemInstanceData* Subject = Ctx.bHasTargetItem ? &Ctx.TargetItem : ResolveSubjectItem(Ctx);
|
|
if (!Subject) { return true; }
|
|
TArray<FInventoryContainer> Containers;
|
|
Ctx.PlayerInventory->GetContainers(Containers);
|
|
for (const FInventoryContainer& C : Containers)
|
|
{
|
|
if (Ctx.PlayerInventory->CanAcceptItem(*Subject, C.ContainerId))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
case EInteractionRequirementType::HasItemWithTag:
|
|
{
|
|
if (!Ctx.PlayerInventory) { return false; }
|
|
TArray<FItemInstanceData> Items;
|
|
Ctx.PlayerInventory->GetAllItems(Items);
|
|
for (const FItemInstanceData& It : Items)
|
|
{
|
|
if (ItemHasTag(Ctx.PlayerInventory.Get(), It, Req.RequiredTag)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
case EInteractionRequirementType::HasHeldItemWithTag:
|
|
return Ctx.bHasHeldItem && ItemHasTag(Ctx.Character.Get(), Ctx.HeldItem, Req.RequiredTag);
|
|
|
|
case EInteractionRequirementType::PropertyGreaterThan:
|
|
{
|
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
|
return Subject && Subject->RuntimeProperties.GetFloat(Req.RequiredTag) > Req.RequiredValue;
|
|
}
|
|
|
|
case EInteractionRequirementType::PropertyLessThan:
|
|
{
|
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
|
return Subject && Subject->RuntimeProperties.GetFloat(Req.RequiredTag) < Req.RequiredValue;
|
|
}
|
|
|
|
case EInteractionRequirementType::TargetHasFreeSlotTag:
|
|
// Re-validated against the world slot component at execution time (stage 8).
|
|
return Ctx.TargetActor != nullptr;
|
|
|
|
case EInteractionRequirementType::NotWet:
|
|
{
|
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
|
return !Subject || Subject->RuntimeProperties.GetFloat(ItemEcosystemTags::Property_Wetness) < 0.5f;
|
|
}
|
|
|
|
case EInteractionRequirementType::NotBroken:
|
|
{
|
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
|
return !Subject || !Subject->StateTags.HasTag(ItemEcosystemTags::State_Broken);
|
|
}
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UInteractionAction::CanExecute_Implementation(const FInteractionContext& Context, FText& OutDisabledReason) const
|
|
{
|
|
for (const FInteractionRequirement& Req : Requirements)
|
|
{
|
|
if (!EvaluateRequirement(Req, Context))
|
|
{
|
|
OutDisabledReason = Req.FailureText.IsEmpty()
|
|
? NSLOCTEXT("ItemInteraction", "RequirementFailed", "Requirement not met")
|
|
: Req.FailureText;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void UInteractionAction::Execute_Implementation(const FInteractionContext& Context)
|
|
{
|
|
// Base does nothing; concrete actions / the interaction component handle effects.
|
|
UE_LOG(LogItemInteraction, Verbose, TEXT("UInteractionAction '%s' base Execute (no-op)."), *ActionTag.ToString());
|
|
}
|
|
|
|
FInteractionOption UInteractionAction::BuildOption(const FInteractionContext& Context) const
|
|
{
|
|
FInteractionOption Option;
|
|
Option.ActionTag = ActionTag;
|
|
Option.Mode = Mode;
|
|
Option.DisplayText = DisplayText;
|
|
Option.Priority = Priority;
|
|
Option.bRequiresHold = bRequiresHold;
|
|
Option.HoldDuration = HoldDuration;
|
|
Option.ActionDuration = Execution.bHasDuration ? Execution.Duration : 0.f;
|
|
|
|
FText Reason;
|
|
Option.bIsEnabled = CanExecute(Context, Reason);
|
|
Option.DisabledReason = Reason;
|
|
|
|
Option.Request.Mode = Mode;
|
|
Option.Request.ActionTag = ActionTag;
|
|
Option.Request.TargetActor = Context.TargetActor;
|
|
if (Context.bHasTargetItem) { Option.Request.ItemInstanceId = Context.TargetItem.InstanceId; }
|
|
else if (Context.bHasHeldItem) { Option.Request.ItemInstanceId = Context.HeldItem.InstanceId; }
|
|
return Option;
|
|
}
|
|
|
|
UWorld* UInteractionAction::GetWorld() const
|
|
{
|
|
if (HasAllFlags(RF_ClassDefaultObject))
|
|
{
|
|
return nullptr;
|
|
}
|
|
if (const UObject* Outer = GetOuter())
|
|
{
|
|
return Outer->GetWorld();
|
|
}
|
|
return nullptr;
|
|
}
|