添加库存系统组件

This commit is contained in:
不明不惑
2026-03-05 14:44:34 +08:00
parent 70e3731c09
commit be0098782a
17 changed files with 909 additions and 12 deletions

View File

@@ -4,6 +4,7 @@
#include "Character/PHYCharacter.h"
#include "GIS_InventorySystemComponent.h"
#include "Equipping/GIS_EquipmentSystemComponent.h"
#include "GMS_CharacterMovementSystemComponent.h"
#include "Components/RetargeterComponent.h"
@@ -14,6 +15,7 @@ APHYCharacter::APHYCharacter()
PrimaryActorTick.bCanEverTick = true;
InventorySystemComponent = CreateDefaultSubobject<UGIS_InventorySystemComponent>(TEXT("InventorySystem"));
EquipmentSystemComponent = CreateDefaultSubobject<UGIS_EquipmentSystemComponent>(TEXT("EquipmentSystem"));
MovementSystemComponent = CreateDefaultSubobject<UGMS_CharacterMovementSystemComponent>(TEXT("MovementSystem"));
RetargeterComponent = CreateDefaultSubobject<URetargeterComponent>(TEXT("Retargeter"));
}
@@ -27,6 +29,9 @@ void APHYCharacter::BeginPlay()
if (HasAuthority() && InventorySystemComponent)
{
InventorySystemComponent->InitializeInventorySystem();
if (EquipmentSystemComponent)
{
EquipmentSystemComponent->InitializeEquipmentSystem();
}
}
}

View File

@@ -9,6 +9,7 @@
class URetargeterComponent;
class UGIS_InventorySystemComponent;
class UGIS_EquipmentSystemComponent;
class UGMS_CharacterMovementSystemComponent;
@@ -24,6 +25,9 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Inventory")
UGIS_InventorySystemComponent* GetInventorySystemComponent() const { return InventorySystemComponent; }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Equipment")
UGIS_EquipmentSystemComponent* GetEquipmentSystemComponent() const { return EquipmentSystemComponent; }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PHY|Movement")
UGMS_CharacterMovementSystemComponent* GetMovementSystemComponent() const { return MovementSystemComponent; }
@@ -41,6 +45,13 @@ protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Inventory", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UGIS_InventorySystemComponent> InventorySystemComponent;
/**
* 角色的装备系统组件(来自 GenericInventorySystem 插件 GIS
* 监听装备槽变化并驱动装备属性生效。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Equipment", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UGIS_EquipmentSystemComponent> EquipmentSystemComponent;
/**
* 角色的移动系统组件(来自 GenericMovementSystem 插件 GMS
*/

View File

@@ -12,6 +12,11 @@
#include "GameplayTags/InputTags.h"
#include "Components/RetargeterComponent.h"
#include "AbilitySystemComponent.h"
#include "GIS_InventorySystemComponent.h"
#include "Core/Items/GIS_ItemInfo.h"
#include "Core/Items/GIS_ItemInstance.h"
#include "Equipping/GIS_EquipmentInterface.h"
#include "Equipping/GIS_EquipmentSystemComponent.h"
#include "AbilitySystem/PHYClassDefaults.h"
#include "AbilitySystem/Attributes/PHYAttributeSet.h"
#include "AbilitySystem/Effects/PHYGE_DerivedAttributes.h"
@@ -77,6 +82,7 @@ void APHYPlayerCharacter::PossessedBy(AController* NewController)
Super::PossessedBy(NewController);
InitializeGAS();
BindEquipmentEventsIfNeeded();
// 初始化hud
InitializeHUD();
if (APHYPlayerState* PS = GetPlayerState<APHYPlayerState>())
@@ -107,10 +113,44 @@ void APHYPlayerCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
InitializeGAS();
BindEquipmentEventsIfNeeded();
// 初始化hud
InitializeHUD();
}
void APHYPlayerCharacter::BindEquipmentEventsIfNeeded()
{
if (!HasAuthority() || bEquipmentEventBound)
{
return;
}
if (UGIS_EquipmentSystemComponent* EquipmentSystem = GetEquipmentSystemComponent())
{
EquipmentSystem->OnEquipmentStateChangedEvent.AddDynamic(this, &ThisClass::OnEquipmentStateChanged);
bEquipmentEventBound = true;
}
}
void APHYPlayerCharacter::OnEquipmentStateChanged(UObject* Equipment, FGameplayTag SlotTag, bool bEquipped)
{
if (!HasAuthority())
{
return;
}
UGIS_ItemInstance* SourceItem = nullptr;
if (IsValid(Equipment) && Equipment->GetClass()->ImplementsInterface(UGIS_EquipmentInterface::StaticClass()))
{
SourceItem = IGIS_EquipmentInterface::Execute_GetSourceItem(Equipment);
}
if (APHYPlayerState* PS = GetPlayerState<APHYPlayerState>())
{
PS->HandleItemEquippedState(SourceItem, bEquipped);
}
}
void APHYPlayerCharacter::InitializeGAS()
{
APHYPlayerState* PS = GetPlayerState<APHYPlayerState>();
@@ -120,7 +160,7 @@ void APHYPlayerCharacter::InitializeGAS()
if (!ASC) return;
ASC->InitAbilityActorInfo(PS, this);
// Server applies init effects.
if (HasAuthority())
{
@@ -220,3 +260,59 @@ void APHYPlayerCharacter::RegenTick()
Spec.Data->SetSetByCallerMagnitude(RegenTags::Tag__Data_Regen_InnerPower, FMath::Max(0.f, InnerPowerDelta));
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
}
void APHYPlayerCharacter::ServerUseConsumableByItemId_Implementation(FGuid ItemId, int32 Count)
{
if (!HasAuthority() || !ItemId.IsValid() || Count <= 0)
{
return;
}
UGIS_InventorySystemComponent* Inventory = GetInventorySystemComponent();
APHYPlayerState* PS = GetPlayerState<APHYPlayerState>();
if (!Inventory || !PS)
{
return;
}
FGIS_ItemInfo SourceInfo;
for (const FGIS_ItemInfo& Info : Inventory->GetItemInfos())
{
if (Info.IsValid() && IsValid(Info.Item) && Info.Item->GetItemId() == ItemId)
{
SourceInfo = Info;
break;
}
}
if (!SourceInfo.IsValid() || !IsValid(SourceInfo.Item) || SourceInfo.Amount <= 0)
{
PS->NotifyOwner(EPHYNotifyType::Warning,
NSLOCTEXT("PHY", "Notify_UseConsumable_NotFound", "道具不存在或数量不足"));
return;
}
const FGameplayTag ConsumableTag = FGameplayTag::RequestGameplayTag(FName(TEXT("GIS.Item.Type.Consumable")), false);
if (!ConsumableTag.IsValid() || !SourceInfo.Item->GetItemTags().HasTag(ConsumableTag))
{
PS->NotifyOwner(EPHYNotifyType::Warning,
FText::Format(NSLOCTEXT("PHY", "Notify_UseConsumable_NotConsumable", "{0} 不是可使用药品"), SourceInfo.Item->GetItemName()));
return;
}
const int32 ConsumeCount = FMath::Min(Count, SourceInfo.Amount);
FGIS_ItemInfo RemoveRequest(SourceInfo.Item, ConsumeCount, SourceInfo);
FGIS_ItemInfo Removed = Inventory->RemoveItem(RemoveRequest);
if (!Removed.IsValid() || Removed.Amount <= 0)
{
PS->NotifyOwner(EPHYNotifyType::Warning,
NSLOCTEXT("PHY", "Notify_UseConsumable_RemoveFailed", "消耗失败,请稍后重试"));
return;
}
if (!PS->ApplyConsumableFromItem(SourceInfo.Item, Removed.Amount))
{
PS->NotifyOwner(EPHYNotifyType::Warning,
FText::Format(NSLOCTEXT("PHY", "Notify_UseConsumable_NoEffect", "{0} 当前无法生效"), SourceInfo.Item->GetItemName()));
}
}

View File

@@ -6,12 +6,15 @@
#include "PHYCharacter.h"
#include "AbilitySystem/PHYCharacterClass.h"
#include "Pawn/UGC_PawnInterface.h"
#include "GameplayTagContainer.h"
#include "PHYPlayerCharacter.generated.h"
class UGIPS_InputSystemComponent;
class USpringArmComponent;
class UCameraComponent;
class URetargeterComponent;
class UGIS_ItemInstance;
struct FGuid;
UCLASS()
class PHY_API APHYPlayerCharacter : public APHYCharacter,
@@ -57,6 +60,16 @@ protected:
void InitializeHUD();
virtual void OnRep_PlayerState() override;
/** 服务器:按道具 ID 使用消耗品并扣除数量。 */
UFUNCTION(Server, Reliable, BlueprintCallable, Category="PHY|Inventory")
void ServerUseConsumableByItemId(FGuid ItemId, int32 Count = 1);
/** 装备系统事件(服务器):装备/卸下时应用或回滚属性。 */
UFUNCTION()
void OnEquipmentStateChanged(UObject* Equipment, FGameplayTag SlotTag, bool bEquipped);
void BindEquipmentEventsIfNeeded();
/** Server: apply init effects. Both sides: init actor info. */
void InitializeGAS();
@@ -70,4 +83,6 @@ protected:
FTimerHandle RegenTimerHandle;
void RegenTick();
void StopRegen();
bool bEquipmentEventBound = false;
};

View File

@@ -11,11 +11,35 @@
#include "Gameplay/GameLevelInfo.h"
#include "Net/UnrealNetwork.h"
#include "Net/Core/PushModel/PushModel.h"
#include "Core/Items/GIS_ItemInstance.h"
#include "Items/GIS_ItemDefinition.h"
namespace
{
// 你可以把这些做成配置/曲线/数据表这里先给一个默认规则每升1级给 1 点属性点
constexpr int32 AttributePointsPerLevel = 4;
static FGameplayTag ReqTag(const TCHAR* Name)
{
return FGameplayTag::RequestGameplayTag(FName(Name), false);
}
static float GetItemNumericAttribute(const UGIS_ItemInstance* Item, const FGameplayTag& Tag)
{
if (!IsValid(Item) || !Tag.IsValid())
{
return 0.f;
}
if (Item->HasFloatAttribute(Tag))
{
return Item->GetFloatAttribute(Tag);
}
if (Item->HasIntegerAttribute(Tag))
{
return static_cast<float>(Item->GetIntegerAttribute(Tag));
}
return 0.f;
}
}
APHYPlayerState::APHYPlayerState()
@@ -27,7 +51,7 @@ APHYPlayerState::APHYPlayerState()
// 使用Minimal复制模式推荐用于PlayerState
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
AttributeSet = CreateDefaultSubobject<UPHYAttributeSet>(TEXT("AttributeSet"));
}
UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const
@@ -86,11 +110,10 @@ void APHYPlayerState::AddToXP(const int32 InXP)
return;
}
// 本地提示:获得经验(只在本地玩家的 PlayerState 上显示即可)
if (IsOwnedBy(GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr))
{
PushGainedXPNotify(InXP);
}
// NOTE:
// XP/等级的权威逻辑走服务器;客户端通过复制拿到 XP。
// “获得经验”的 UI 提示优先在客户端触发OnRep_XP
// 但 ListenServer/Standalone 的本地玩家通常不会走 OnRep因此这里需要兜底。
MARK_PROPERTY_DIRTY_FROM_NAME(APHYPlayerState, XP, this);
XP += InXP;
@@ -98,6 +121,12 @@ void APHYPlayerState::AddToXP(const int32 InXP)
// 只在权威端Server/Standalone做升级计算客户端靠复制拿到最终 Level/XP
if (HasAuthority())
{
// ListenServer/Standalone本地玩家在权威端OnRep_XP 不会触发,兜底弹“获得经验”
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushGainedXPNotify(InXP);
}
const int32 NewLevel = FMath::Clamp(FGameLevelInfo::GetLevelForTotalXP(XP), FGameLevelInfo::MinLevel, FGameLevelInfo::MaxLevel);
if (NewLevel > Level)
{
@@ -116,10 +145,16 @@ void APHYPlayerState::AddToLevel(const int32 InLevel)
Level += InLevel;
OnLevelChangedDelegate.Broadcast(Level,true);
// 本地提示:升级
if (IsOwnedBy(GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr))
// UI 通知策略:
// - 纯客户端:走 OnRep_Level。
// - DedicatedServer没有本地玩家不需要弹。
// - ListenServer/Standalone本地玩家在权威端不会走 OnRep因此这里兜底触发一次。
if (HasAuthority())
{
PushLevelUpNotify(Level);
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushLevelUpNotify(Level);
}
}
}
@@ -128,6 +163,33 @@ void APHYPlayerState::PushGameplayNotify(EPHYNotifyType Type, const FText& Messa
OnGameplayNotifyDelegate.Broadcast(Type, Message, SoftIcon);
}
void APHYPlayerState::NotifyOwner(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon)
{
if (!HasAuthority())
{
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushGameplayNotify(Type, Message, SoftIcon);
}
return;
}
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushGameplayNotify(Type, Message, SoftIcon);
return;
}
ClientPushGameplayNotify(Type, Message, SoftIcon);
}
void APHYPlayerState::ClientPushGameplayNotify_Implementation(EPHYNotifyType Type, const FText& Message,
const TSoftObjectPtr<UTexture2D>& SoftIcon)
{
PushGameplayNotify(Type, Message, SoftIcon);
}
void APHYPlayerState::PushGainedXPNotify(int32 DeltaXP)
{
if (DeltaXP <= 0) return;
@@ -202,11 +264,38 @@ void APHYPlayerState::SetAttributePoints(const int32 InPoints)
void APHYPlayerState::OnRep_Level(int32 OldLevel)
{
OnLevelChangedDelegate.Broadcast(Level, true);
// 客户端侧提示升级DedicatedServer 不会进到这里(也不会有本地控制器)。
if (!HasAuthority())
{
if (Level > OldLevel)
{
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushLevelUpNotify(Level);
}
}
}
}
void APHYPlayerState::OnRep_XP(int32 OldXP)
{
OnXPChangedDelegate.Broadcast(XP);
// 仅客户端:本地提示“获得经验”。
// - DedicatedServer 上不会有本地玩家,因此不触发。
// - ListenServer 的主机本地玩家会走本地分支。
if (!HasAuthority())
{
const int32 Delta = XP - OldXP;
if (Delta > 0)
{
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushGainedXPNotify(Delta);
}
}
}
}
void APHYPlayerState::OnRep_AttributePoints(int32 OldAttributePoints)
@@ -252,3 +341,129 @@ int32 APHYPlayerState::GetXPRequirementForNextLevel() const
FGameLevelInfo::GetProgressForTotalXP(XP, L, Into, ToNext);
return ToNext;
}
void APHYPlayerState::ApplyItemAttributesWithSign(UGIS_ItemInstance* Item, const float Sign)
{
if (!HasAuthority() || !IsValid(Item) || FMath::IsNearlyZero(Sign))
{
return;
}
UAbilitySystemComponent* ASC = GetAbilitySystemComponent();
const UPHYAttributeSet* AS = GetAttributeSet();
if (!ASC || !AS)
{
return;
}
auto AddDelta = [ASC](const FGameplayAttribute& Attribute, const float Delta)
{
if (!Attribute.IsValid() || FMath::IsNearlyZero(Delta))
{
return;
}
ASC->SetNumericAttributeBase(Attribute, ASC->GetNumericAttribute(Attribute) + Delta);
};
AddDelta(UPHYAttributeSet::GetPhysicalAttackAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Attack"))) * Sign);
AddDelta(UPHYAttributeSet::GetPhysicalDefenseAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Defense"))) * Sign);
AddDelta(UPHYAttributeSet::GetCritChanceAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.CritRate"))) * Sign);
AddDelta(UPHYAttributeSet::GetCritDamageAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.CritDamage"))) * Sign);
AddDelta(UPHYAttributeSet::GetHitChanceAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Hit"))) * Sign);
AddDelta(UPHYAttributeSet::GetDodgeChanceAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Dodge"))) * Sign);
AddDelta(UPHYAttributeSet::GetParryChanceAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Parry"))) * Sign);
AddDelta(UPHYAttributeSet::GetCounterChanceAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.CounterChance"))) * Sign);
AddDelta(UPHYAttributeSet::GetArmorPenetrationAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.ArmorPenetration"))) * Sign);
AddDelta(UPHYAttributeSet::GetDamageReductionAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.DamageReduction"))) * Sign);
AddDelta(UPHYAttributeSet::GetLifeStealAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.LifeSteal"))) * Sign);
AddDelta(UPHYAttributeSet::GetTenacityAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Resilience"))) * Sign);
AddDelta(UPHYAttributeSet::GetMoveSpeedAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.MoveSpeed"))) * Sign);
AddDelta(UPHYAttributeSet::GetHealthRegenRateAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.HealthRegenRate"))) * Sign);
AddDelta(UPHYAttributeSet::GetInnerPowerRegenRateAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.InnerPowerRegenRate"))) * Sign);
AddDelta(UPHYAttributeSet::GetMaxHealthAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.MaxHealth"))) * Sign);
AddDelta(UPHYAttributeSet::GetMaxInnerPowerAttribute(), GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.MaxInnerPower"))) * Sign);
// 上限变更后,当前值做一次夹取,避免越界。
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetHealthAttribute(), FMath::Clamp(AS->GetHealth(), 0.f, AS->GetMaxHealth()));
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetInnerPowerAttribute(), FMath::Clamp(AS->GetInnerPower(), 0.f, AS->GetMaxInnerPower()));
}
void APHYPlayerState::HandleItemEquippedState(UGIS_ItemInstance* Item, const bool bEquipped)
{
if (!HasAuthority() || !IsValid(Item))
{
return;
}
const FGuid ItemId = Item->GetItemId();
if (!ItemId.IsValid())
{
return;
}
const TSoftObjectPtr<UTexture2D> Icon = Item->GetDefinition() ? Item->GetDefinition()->Icon : nullptr;
if (bEquipped)
{
if (AppliedEquipmentItems.Contains(ItemId))
{
return;
}
AppliedEquipmentItems.Add(ItemId);
ApplyItemAttributesWithSign(Item, +1.f);
NotifyOwner(EPHYNotifyType::Info,
FText::Format(NSLOCTEXT("PHY", "Notify_EquipItem", "装备 {0}"), Item->GetItemName()),
Icon);
}
else
{
if (!AppliedEquipmentItems.Contains(ItemId))
{
return;
}
AppliedEquipmentItems.Remove(ItemId);
ApplyItemAttributesWithSign(Item, -1.f);
NotifyOwner(EPHYNotifyType::Info,
FText::Format(NSLOCTEXT("PHY", "Notify_UnequipItem", "卸下 {0}"), Item->GetItemName()),
Icon);
}
}
bool APHYPlayerState::ApplyConsumableFromItem(UGIS_ItemInstance* Item, const int32 Count)
{
if (!HasAuthority() || !IsValid(Item) || Count <= 0)
{
return false;
}
UAbilitySystemComponent* ASC = GetAbilitySystemComponent();
const UPHYAttributeSet* AS = GetAttributeSet();
if (!ASC || !AS)
{
return false;
}
const float RestoreHealth = GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Consumable.RestoreHealth"))) * static_cast<float>(Count);
const float RestoreInnerPower = GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Consumable.RestoreInnerPower"))) * static_cast<float>(Count);
if (RestoreHealth <= 0.f && RestoreInnerPower <= 0.f)
{
return false;
}
if (RestoreHealth > 0.f)
{
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetHealthAttribute(), FMath::Clamp(AS->GetHealth() + RestoreHealth, 0.f, AS->GetMaxHealth()));
}
if (RestoreInnerPower > 0.f)
{
ASC->SetNumericAttributeBase(UPHYAttributeSet::GetInnerPowerAttribute(), FMath::Clamp(AS->GetInnerPower() + RestoreInnerPower, 0.f, AS->GetMaxInnerPower()));
}
const TSoftObjectPtr<UTexture2D> Icon = Item->GetDefinition() ? Item->GetDefinition()->Icon : nullptr;
NotifyOwner(EPHYNotifyType::Reward,
FText::Format(NSLOCTEXT("PHY", "Notify_UseConsumable", "使用 {0} x{1}"), Item->GetItemName(), FText::AsNumber(Count)),
Icon);
return true;
}

View File

@@ -11,6 +11,7 @@
class UAbilitySystemComponent;
class UPHYAttributeSet;
class UGIS_ItemInstance;
/**
* 玩家状态类包含GAS支持
@@ -57,7 +58,7 @@ public:
UAbilitySystemComponent* GetPHYAbilitySystemComponent() const { return AbilitySystemComponent; }
UFUNCTION(BlueprintCallable, Category = "Abilities")
const UPHYAttributeSet* GetAttributeSet() const { return AttributeSet; }
UPHYAttributeSet* GetAttributeSet() const { return AttributeSet; }
// 原有的ReTargeterTag相关代码
FOnPlayerStatChanged OnXPChangedDelegate;
@@ -84,6 +85,19 @@ public:
UFUNCTION(Server, Reliable, BlueprintCallable, Category="Player|Progress")
void ServerAddXP(int32 DeltaXP);
/** 服务端:根据装备穿脱状态应用/回滚道具属性。 */
void HandleItemEquippedState(UGIS_ItemInstance* Item, bool bEquipped);
/** 服务端应用药品恢复Count 为消耗数量)。 */
bool ApplyConsumableFromItem(UGIS_ItemInstance* Item, int32 Count);
/** 向拥有者玩家推送提示(会自动处理 DS/Listen/Standalone。 */
void NotifyOwner(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon = nullptr);
/** 仅拥有者客户端执行:显示提示。 */
UFUNCTION(Client, Reliable)
void ClientPushGameplayNotify(EPHYNotifyType Type, const FText& Message, const TSoftObjectPtr<UTexture2D>& SoftIcon);
FGameplayTag GetReTargeterTag() const { return ReTargeterTag; }
FORCEINLINE int32 GetPlayerLevel() const { return Level; }
@@ -134,4 +148,10 @@ private:
/** 仅本地推送“获得物品”提示ItemName x Count */
void PushGainedItemNotify(const FText& ItemName, int32 Count);
/** 服务端:把道具属性按符号(+1/-1叠加到角色属性。 */
void ApplyItemAttributesWithSign(UGIS_ItemInstance* Item, float Sign);
/** 已生效的装备道具,避免重复叠加。 */
TSet<FGuid> AppliedEquipmentItems;
};

View File

@@ -0,0 +1,32 @@
// Copyright 2026 PHY. All Rights Reserved.
#include "GameplayTags/PHYInventoryItemTags.h"
namespace PHYInventoryItemTags
{
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_Type_Weapon, "GIS.Item.Type.Weapon", "Weapon item category");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_Type_Equipment, "GIS.Item.Type.Equipment", "Equipment item category");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_Type_Consumable, "GIS.Item.Type.Consumable", "Consumable item category");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_Type_Material, "GIS.Item.Type.Material", "Material item category");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_MainHand, "GIS.Item.EquipSlot.MainHand", "Main hand slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_OffHand, "GIS.Item.EquipSlot.OffHand", "Off hand slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_Head, "GIS.Item.EquipSlot.Head", "Head slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_Chest, "GIS.Item.EquipSlot.Chest", "Chest slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_Legs, "GIS.Item.EquipSlot.Legs", "Legs slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_Feet, "GIS.Item.EquipSlot.Feet", "Feet slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Item_EquipSlot_Accessory, "GIS.Item.EquipSlot.Accessory", "Accessory slot");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_Attack, "GIS.Attribute.Item.Attack", "Item Attack bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_Defense, "GIS.Attribute.Item.Defense", "Item Defense bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_CritRate, "GIS.Attribute.Item.CritRate", "Item CritRate bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_CritDamage, "GIS.Attribute.Item.CritDamage", "Item CritDamage bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_Hit, "GIS.Attribute.Item.Hit", "Item Hit bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_Dodge, "GIS.Attribute.Item.Dodge", "Item Dodge bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_Parry, "GIS.Attribute.Item.Parry", "Item Parry bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_ArmorPenetration, "GIS.Attribute.Item.ArmorPenetration", "Item ArmorPenetration bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_DamageReduction, "GIS.Attribute.Item.DamageReduction", "Item DamageReduction bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_LifeSteal, "GIS.Attribute.Item.LifeSteal", "Item LifeSteal bonus");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Tag__GIS_Attribute_Item_Resilience, "GIS.Attribute.Item.Resilience", "Item Resilience bonus");
}

View File

@@ -0,0 +1,38 @@
// Copyright 2026 PHY. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NativeGameplayTags.h"
namespace PHYInventoryItemTags
{
// Item category tags
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_Type_Weapon);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_Type_Equipment);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_Type_Consumable);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_Type_Material);
// Equipment slot tags
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_MainHand);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_OffHand);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_Head);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_Chest);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_Legs);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_Feet);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Item_EquipSlot_Accessory);
// Common item attributes used by this project
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_Attack);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_Defense);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_CritRate);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_CritDamage);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_Hit);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_Dodge);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_Parry);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_ArmorPenetration);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_DamageReduction);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_LifeSteal);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(Tag__GIS_Attribute_Item_Resilience);
}

View File

@@ -0,0 +1,62 @@
// Copyright 2026 PHY. All Rights Reserved.
#include "Items/PHYItemBlueprintLibrary.h"
#include "GIS_ItemInstance.h"
const UPHYItemFragment_PropertySet* UPHYItemBlueprintLibrary::FindPropertyFragment(const UGIS_ItemInstance* ItemInstance)
{
if (!IsValid(ItemInstance))
{
return nullptr;
}
return ItemInstance->FindFragmentByClass<UPHYItemFragment_PropertySet>();
}
EPHYItemArchetype UPHYItemBlueprintLibrary::GetItemArchetype(const UGIS_ItemInstance* ItemInstance)
{
if (const UPHYItemFragment_PropertySet* Fragment = FindPropertyFragment(ItemInstance))
{
return Fragment->ItemArchetype;
}
return EPHYItemArchetype::Material;
}
EPHYEquipSlotType UPHYItemBlueprintLibrary::GetEquipSlotType(const UGIS_ItemInstance* ItemInstance)
{
if (const UPHYItemFragment_PropertySet* Fragment = FindPropertyFragment(ItemInstance))
{
return Fragment->EquipSlot;
}
return EPHYEquipSlotType::None;
}
bool UPHYItemBlueprintLibrary::IsWeaponItem(const UGIS_ItemInstance* ItemInstance)
{
return GetItemArchetype(ItemInstance) == EPHYItemArchetype::Weapon;
}
bool UPHYItemBlueprintLibrary::IsEquipmentItem(const UGIS_ItemInstance* ItemInstance)
{
const EPHYItemArchetype Archetype = GetItemArchetype(ItemInstance);
return Archetype == EPHYItemArchetype::Weapon || Archetype == EPHYItemArchetype::Equipment;
}
bool UPHYItemBlueprintLibrary::IsConsumableItem(const UGIS_ItemInstance* ItemInstance)
{
return GetItemArchetype(ItemInstance) == EPHYItemArchetype::Consumable;
}
FPHYConsumablePayload UPHYItemBlueprintLibrary::GetConsumablePayload(const UGIS_ItemInstance* ItemInstance)
{
if (const UPHYItemFragment_PropertySet* Fragment = FindPropertyFragment(ItemInstance))
{
return Fragment->ConsumablePayload;
}
return FPHYConsumablePayload();
}

View File

@@ -0,0 +1,39 @@
// Copyright 2026 PHY. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Items/PHYItemFragment_PropertySet.h"
#include "PHYItemBlueprintLibrary.generated.h"
class UGIS_ItemInstance;
UCLASS()
class UPHYItemBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
static EPHYItemArchetype GetItemArchetype(const UGIS_ItemInstance* ItemInstance);
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
static EPHYEquipSlotType GetEquipSlotType(const UGIS_ItemInstance* ItemInstance);
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
static bool IsWeaponItem(const UGIS_ItemInstance* ItemInstance);
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
static bool IsEquipmentItem(const UGIS_ItemInstance* ItemInstance);
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
static bool IsConsumableItem(const UGIS_ItemInstance* ItemInstance);
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
static FPHYConsumablePayload GetConsumablePayload(const UGIS_ItemInstance* ItemInstance);
private:
static const UPHYItemFragment_PropertySet* FindPropertyFragment(const UGIS_ItemInstance* ItemInstance);
};

View File

@@ -0,0 +1,42 @@
// Copyright 2026 PHY. All Rights Reserved.
#include "Items/PHYItemFragment_PropertySet.h"
#include "GIS_ItemInstance.h"
void UPHYItemFragment_PropertySet::OnInstanceCreated(UGIS_ItemInstance* ItemInstance) const
{
if (!IsValid(ItemInstance))
{
return;
}
for (const FGIS_GameplayTagFloat& Modifier : BaseFloatModifiers)
{
if (Modifier.Tag.IsValid())
{
ItemInstance->SetFloatAttribute(Modifier.Tag, Modifier.Value);
}
}
for (const FGIS_GameplayTagInteger& Modifier : BaseIntegerModifiers)
{
if (Modifier.Tag.IsValid())
{
ItemInstance->SetIntegerAttribute(Modifier.Tag, Modifier.Value);
}
}
Super::OnInstanceCreated(ItemInstance);
}
bool UPHYItemFragment_PropertySet::IsEquippable() const
{
return ItemArchetype == EPHYItemArchetype::Weapon || ItemArchetype == EPHYItemArchetype::Equipment;
}
bool UPHYItemFragment_PropertySet::IsConsumable() const
{
return ItemArchetype == EPHYItemArchetype::Consumable;
}

View File

@@ -0,0 +1,88 @@
// Copyright 2026 PHY. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_GameplayTagFloat.h"
#include "GIS_GameplayTagInteger.h"
#include "GIS_ItemFragment.h"
#include "PHYItemFragment_PropertySet.generated.h"
UENUM(BlueprintType)
enum class EPHYItemArchetype : uint8
{
Weapon,
Equipment,
Consumable,
Material,
Quest
};
UENUM(BlueprintType)
enum class EPHYEquipSlotType : uint8
{
None,
MainHand,
OffHand,
Head,
Chest,
Legs,
Feet,
Accessory
};
USTRUCT(BlueprintType)
struct FPHYConsumablePayload
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Consumable")
float RestoreHealth = 0.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Consumable")
float RestoreInnerPower = 0.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Consumable")
float DurationSeconds = 0.0f;
};
/**
* 统一道具属性片段:
* - 定义道具大类(武器/装备/药品...
* - 定义装备槽位
* - 定义创建实例时写入的动态属性(可用于随机词条前的基础值)
* - 定义药品基础效果载荷
*/
UCLASS(DisplayName="PHY Item Property Settings", Category="PHY")
class UPHYItemFragment_PropertySet : public UGIS_ItemFragment
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Item")
EPHYItemArchetype ItemArchetype = EPHYItemArchetype::Material;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Item")
EPHYEquipSlotType EquipSlot = EPHYEquipSlotType::None;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Item", meta=(Categories="GIS.Item"))
FGameplayTagContainer ExtraItemTags;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Attributes", meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute"))
TArray<FGIS_GameplayTagFloat> BaseFloatModifiers;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Attributes", meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute"))
TArray<FGIS_GameplayTagInteger> BaseIntegerModifiers;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Consumable")
FPHYConsumablePayload ConsumablePayload;
virtual void OnInstanceCreated(UGIS_ItemInstance* ItemInstance) const override;
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
bool IsEquippable() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Item")
bool IsConsumable() const;
};