添加库存系统组件

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

@@ -88,6 +88,7 @@ FontDPI=72
[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/PHY")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/PHY")
GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=True

View File

@@ -0,0 +1,137 @@
# PHY 编辑器打通清单(装备/药品/通知)
> 目标:打通你当前 C++ 已实现的链路:
> - 装备穿脱 -> 属性生效/回滚
> - 药品使用Server RPC-> 扣除库存 -> 恢复属性
> - 通知飘字Info/Reward/Warning
## 0. 代码前提(已实现)
- `APHYCharacter` 已挂载:`InventorySystem` + `EquipmentSystem`
- 服务器 `BeginPlay` 会初始化库存与装备系统。
- `APHYPlayerCharacter::ServerUseConsumableByItemId` 已有严格校验:
- 必须是 `GIS.Item.Type.Consumable`
- 找不到道具/扣除失败会发 Warning 通知
- `APHYPlayerState` 已接入 owner 通知分发(支持 DS/Listen/Standalone
## 1. GameplayTags必须先做
请在 `Project Settings -> Gameplay Tags``Config/DefaultGameplayTags.ini` 中确认存在:
### 1.1 类型标签(必须)
- `GIS.Item.Type.Weapon`
- `GIS.Item.Type.Equipment`
- `GIS.Item.Type.Consumable`
- `GIS.Item.Type.Material`
### 1.2 药品效果标签(必须)
- `GIS.Attribute.Item.Consumable.RestoreHealth`
- `GIS.Attribute.Item.Consumable.RestoreInnerPower`
### 1.3 装备属性标签(建议)
- `GIS.Attribute.Item.Attack`
- `GIS.Attribute.Item.Defense`
- `GIS.Attribute.Item.CritRate`
- `GIS.Attribute.Item.CritDamage`
- `GIS.Attribute.Item.Hit`
- `GIS.Attribute.Item.Dodge`
- `GIS.Attribute.Item.Parry`
- `GIS.Attribute.Item.CounterChance`
- `GIS.Attribute.Item.ArmorPenetration`
- `GIS.Attribute.Item.DamageReduction`
- `GIS.Attribute.Item.LifeSteal`
- `GIS.Attribute.Item.Resilience`
- `GIS.Attribute.Item.MoveSpeed`
- `GIS.Attribute.Item.HealthRegenRate`
- `GIS.Attribute.Item.InnerPowerRegenRate`
- `GIS.Attribute.Item.MaxHealth`
- `GIS.Attribute.Item.MaxInnerPower`
## 2. 角色蓝图组件配置
打开你的玩家角色蓝图(继承 `APHYPlayerCharacter`
1. 选中 `InventorySystem` 组件:
- 确认存在普通背包集合(如 `GIS.Collection.Inventory`
- 确认存在装备槽集合(如 `GIS.Collection.Equipped`SlotCollection
2. 选中 `EquipmentSystem` 组件:
- `TargetCollectionTag` 设为装备槽集合标签(如 `GIS.Collection.Equipped`
> 若 `TargetCollectionTag` 不匹配,装备状态事件不会触发,属性不会生效。
## 3. 创建三类物品UGIS_ItemDefinition
## 3.1 药品示例:`ID_Cons_HealthSmall`
- `DisplayName`: 小还丹
- `ItemTags`: 添加 `GIS.Item.Type.Consumable`
- 图标:设置 `Icon`
- 动态或静态属性至少配置其一:
- `GIS.Attribute.Item.Consumable.RestoreHealth = 120`
- `GIS.Attribute.Item.Consumable.RestoreInnerPower = 0`
## 3.2 武器示例:`ID_Wpn_BronzeSword`
- `DisplayName`: 青铜剑
- `ItemTags`: 添加 `GIS.Item.Type.Weapon`
- 属性:
- `GIS.Attribute.Item.Attack = 35`
- `GIS.Attribute.Item.CritRate = 0.03`
## 3.3 装备示例:`ID_Equip_ClothArmor`
- `DisplayName`: 布甲
- `ItemTags`: 添加 `GIS.Item.Type.Equipment`
- 属性:
- `GIS.Attribute.Item.Defense = 20`
- `GIS.Attribute.Item.DamageReduction = 0.02`
## 4. 物品“使用”按钮绑定UI
在你的物品操作逻辑中(右键菜单/快捷键):
1.`UItemData``ItemInfo.Item`
2. `Item->GetItemId()` 拿到 `FGuid`
3. `GetOwningPlayerPawn` -> Cast `APHYPlayerCharacter`
4. 调用:`ServerUseConsumableByItemId(ItemId, 1)`
## 5. 通知验证点
你当前 UI 会接 `OnGameplayNotifyDelegate`,预期:
- 使用药品成功:`Reward`(例如:使用 小还丹 x1
- 使用非药品:`Warning`
- 数量不足/扣除失败:`Warning`
- 装备成功:`Info`(装备 xxx
- 卸下成功:`Info`(卸下 xxx
## 6. 联机验收顺序(建议)
1. Standalone
2. Listen Server + 1 Client
3. Dedicated Server + 1 Client
每轮都检查:
- 药品是否扣库存
- 生命/内力是否恢复
- 装备/卸下是否改变属性
- 通知是否只在拥有者客户端显示
## 7. 常见问题
- **现象:药品点了无反应**
- 常见原因:缺少 `GIS.Item.Type.Consumable`
- **现象:装备后属性不变**
- 常见原因:`EquipmentSystem.TargetCollectionTag` 不匹配装备集合
- **现象Dedicated Server 下无提示**
- 先确认 HUD/UI 绑定是否在客户端建立;`PlayerState` 已支持 owner 通知分发
---
如果你需要,我可以继续生成第二份文档:
- `PHY_Item_Asset_Convention.md`(命名规范 + 品质色 + 图标规范 + 掉落表字段模板)

View File

@@ -0,0 +1,96 @@
# PHY Item Attribute System (GIS)
## 本次新增
- `Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.h`
- `Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.cpp`
- `Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.h`
- `Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.cpp`
- `Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h`
- `Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.cpp`
- `Source/PHY/Private/Character/PHYCharacter.h`
- `Source/PHY/Private/Character/PHYCharacter.cpp`
- `Source/PHY/Private/Character/PHYPlayerCharacter.h`
- `Source/PHY/Private/Character/PHYPlayerCharacter.cpp`
- `Source/PHY/Private/Gameplay/Player/PHYPlayerState.h`
- `Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp`
## 新增联动能力12
### 1) 装备属性自动生效/回滚(服务器权威)
- 在基础角色上新增 `UGIS_EquipmentSystemComponent`
- 服务器 `BeginPlay` 时:先初始化 `InventorySystem`,再初始化 `EquipmentSystem`
- `APHYPlayerCharacter` 监听 `OnEquipmentStateChangedEvent`
- 事件触发后通过 `APHYPlayerState::HandleItemEquippedState` 应用或回滚属性。
- 使用 `AppliedEquipmentItems` 防止同一个 ItemId 重复叠加。
### 2) 药品 Server RPC 使用链路
-`APHYPlayerCharacter` 新增:
- `UFUNCTION(Server, Reliable) ServerUseConsumableByItemId(FGuid ItemId, int32 Count=1)`
- 服务端流程:
-`ItemId` 在库存中查找道具
- 扣减库存数量(`RemoveItem`
- 调用 `APHYPlayerState::ApplyConsumableFromItem` 应用恢复
- 当前默认读取道具动态属性标签:
- `GIS.Attribute.Item.Consumable.RestoreHealth`
- `GIS.Attribute.Item.Consumable.RestoreInnerPower`
## 设计
- 基于 GIS 的 `UGIS_ItemDefinition` + Fragment。
- 新增 `UPHYItemFragment_PropertySet` 作为统一“武器/装备/药品”属性片段。
- 道具实例创建时会把 `BaseFloatModifiers` / `BaseIntegerModifiers` 写入 `UGIS_ItemInstance` 动态属性。
## 主要能力
- 统一道具大类:`EPHYItemArchetype`
- `Weapon`
- `Equipment`
- `Consumable`
- `Material`
- `Quest`
- 统一装备槽:`EPHYEquipSlotType`
- 药品效果载荷:`FPHYConsumablePayload`
- `RestoreHealth`
- `RestoreInnerPower`
- `DurationSeconds`
## GameplayTags 管理
标签已集中在 `Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.*`,避免代码中散落字符串:
- `GIS.Item.Type.Weapon`
- `GIS.Item.Type.Equipment`
- `GIS.Item.Type.Consumable`
- `GIS.Item.EquipSlot.MainHand`
- `GIS.Attribute.Item.Attack``GIS.Attribute.Item.CritRate`
## 如何创建三类物品
1. 在编辑器创建 `UGIS_ItemDefinition` 资产。
2. 给该资产添加 Fragment`PHY Item Property Settings`
3. 按类型配置:
- 武器:`ItemArchetype=Weapon``EquipSlot=MainHand/OffHand`,填写攻击/暴击等属性。
- 装备:`ItemArchetype=Equipment`,设置对应 `EquipSlot`,填写防御/免伤/韧性等属性。
- 药品:`ItemArchetype=Consumable`,填写 `ConsumablePayload`(回血/回内力)。
4. 把物品加入你的掉落表/默认背包配置。
## 蓝图辅助
`UPHYItemBlueprintLibrary` 提供:
- `GetItemArchetype`
- `GetEquipSlotType`
- `IsWeaponItem`
- `IsEquipmentItem`
- `IsConsumableItem`
- `GetConsumablePayload`
用于 UI 显示和使用逻辑分流。
## 备注
- 当前为 MVP先打通类型与属性承载。
- 下一步可扩展随机词条Roll、品质颜色、套装、与 GAS GameplayEffect 自动联动。

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>();
@@ -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,18 +145,51 @@ 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())
{
if (AController* Controller = GetOwningController(); Controller && Controller->IsLocalController())
{
PushLevelUpNotify(Level);
}
}
}
void APHYPlayerState::PushGameplayNotify(EPHYNotifyType Type, const FText& Message, TSoftObjectPtr<UTexture2D> SoftIcon)
{
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;
};