添加库存系统组件
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
137
PHY_Editor_Integration_Checklist.md
Normal file
137
PHY_Editor_Integration_Checklist.md
Normal 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`(命名规范 + 品质色 + 图标规范 + 掉落表字段模板)
|
||||
|
||||
96
PHY_ItemSystem_Attributes.md
Normal file
96
PHY_ItemSystem_Attributes.md
Normal 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 自动联动。
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)。
|
||||
*/
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
39
Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h
Normal file
39
Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h
Normal 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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user