From be0098782aa308dfc4f5bfcda5baff5058b622b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E6=98=8E=E4=B8=8D=E6=83=91?= Date: Thu, 5 Mar 2026 14:44:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BA=93=E5=AD=98=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Config/DefaultEngine.ini | 1 + .../AGame/Character/Player/Player_Base.uasset | Bin 73710 -> 73254 bytes .../UI/Menu/Overlap/WBP_Menu_Overlap.uasset | Bin 28395 -> 28611 bytes PHY_Editor_Integration_Checklist.md | 137 ++++++++++ PHY_ItemSystem_Attributes.md | 96 ++++++++ Source/PHY/Private/Character/PHYCharacter.cpp | 7 +- Source/PHY/Private/Character/PHYCharacter.h | 11 + .../Private/Character/PHYPlayerCharacter.cpp | 98 +++++++- .../Private/Character/PHYPlayerCharacter.h | 15 ++ .../Gameplay/Player/PHYPlayerState.cpp | 233 +++++++++++++++++- .../Private/Gameplay/Player/PHYPlayerState.h | 22 +- .../GameplayTags/PHYInventoryItemTags.cpp | 32 +++ .../GameplayTags/PHYInventoryItemTags.h | 38 +++ .../Private/Items/PHYItemBlueprintLibrary.cpp | 62 +++++ .../Private/Items/PHYItemBlueprintLibrary.h | 39 +++ .../Items/PHYItemFragment_PropertySet.cpp | 42 ++++ .../Items/PHYItemFragment_PropertySet.h | 88 +++++++ 17 files changed, 909 insertions(+), 12 deletions(-) create mode 100644 PHY_Editor_Integration_Checklist.md create mode 100644 PHY_ItemSystem_Attributes.md create mode 100644 Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.cpp create mode 100644 Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.h create mode 100644 Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.cpp create mode 100644 Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h create mode 100644 Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.cpp create mode 100644 Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.h diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index 901d060..1a7b42d 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -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 diff --git a/Content/AGame/Character/Player/Player_Base.uasset b/Content/AGame/Character/Player/Player_Base.uasset index 07c559ef597827774fed319114769ed574717655..51984f023283f9560300a221fd30061973669534 100644 GIT binary patch delta 2649 zcmbtWdr(tX9zORb;U$q*s66GVgb0!<1tmNJ8i+8E2Xxkr(_&F8uqc+nB7vCTgU)12 zQI7>GAlb0II?GTR-0oDtQWq3xtw3ZDHK48zh!~+jL_qf3ixB_lv^%pm|9s#1edl-1 z{hjmua-UzN)?T5Waj-wH(w+KKM1k|kqu=Bfci$fTr)FGQ(d^%dVV>|Vmg6Z+1z^e* z18+Y|=&9%c;0_v`b=73!8Lm*{&Vml^meAV08jiSm!v%L2__Mnkbnw`qsq{RI!RH=L zFwCPFTcf$@ZAZZ@p`~}b=7G>j9S|E{4@df_@P`>H%zBTH`*p#BVB^q3 zG!)kjg-@K${Y-_iK|(xtm3@zxa+R@yg?M>4EST5`)xmsR(L+MpZG8OQpGbHD!N5Ke zP9b=68K!LK5y$drS^JL_^o#Oo)6+4vE9~xS5`WKOzWbNw_7Pk2lVdP>rBqiG;Qh ze0-8x8o3U0glB^3@C4%iW=1}R_%TZ(PvP?ME^8z2LtJ8OTR+Ur9xH1}hXuz!%EWlvko=858rGzgiShc0KLK7rvM84k$L%b{XVOAJ5Lz z`QAHnPpN5+wZ-I|#HT*O{l~o-}Y5~K3dA*N|rAHZSF1}wN{~|~Yltdz z3`_fxCF|vyqi|@mK0&Q;NVrM!K`N*}&VcUw>GI*zfP;OdDF`&0q znW9ZCFKAw0J9ko7%+9+-`5NuG>WP&!s^$wG*rLoSk=0NAatVY)Tz=W#pm;XYhlC2( zc=-HvVD3rXnaTyL0fi%v(}P~|;E=*5{6#}rL2SiEZQEXh%2X6D|JAg0J%VPVbtqo! zH!q~5C(WzBdR&z*Y>&1|2QtPmJFgcl2r{bjUexyA7{0NX7OI)x((7UjS1k~VHMuDm z<65c9ktnoX&$U&wJcKqfpC64ClnNIj3VUW@&rq$Wc=k4r;ZI}wiok_3jLI8lTy5Dl!TB(te_n3c0tg zttB&AqEv!B`t?mWYM*7s=F4*?>x9%j&V3ctX+1}Lr^%#iT4kxawoCR5%)_J5RPu4> z^xsf1Q*r;IvFOg}hEx@GHB+hdRlk{ER!AaIN`=>;?tDgOY>5)2R94eFI4I~nKYTh! zu>nokG{7lbci{l}(l}BD1=qy!K(wl%YfON`5%=G-2XSv!hPm;P;eo$V#Q(X5G+1+R zv!5ApK<{rt)fHkqhLEZyQray(5^GU)!3I=oTk`$~p*sZ^4|?F%z3_JIR(Ww>RkO_3 z;gI8pOiY`!=k7q!#5mWM*y8qG`%V%;CeC7g@Yk{Ej%gL`|8wXq3bWzUfEwp@eF`tZ z6_z9oi?)f8Apfe&Q|rI`uF;wBG;~jPf@1MrIi~w)wi?-Hh)JLt3y1yvETZ&wMrT!4 zSdq@!MHCBgkXWT3d40Y+TYhX2&Dw3KM%t%L>~z%UN&#~DFwtsUpSY!mUYm$lE4-(! zx31r9s1tGN2zjK`QB-wgxdM=x<;$Kw<~$cs6HpR1T8zy16l9i!jhPK@iIB*G1eagl zqU2wjh(TSUMM$~!&bGj|-G*B7b`niy+5J0KZR#wpyU+N=Z(dx8E~@emJt!wl7!Sgg znPp+4$kBi}+{7Le2gb!e&#Fv%6Htp-kB~X@xB5uT4z8uT5;>l*^;&ZCdpX8O~purc%DA5&>!3;%o?ipI6+~Ymwe@7GqBAw zGqu*c|C3#-C0awwidjJiC)!#0DGDwh8=8>zj~vT@jb9}cqXr7fo%@Q4#>Bl`n|fB3 zG||x1XhpOOTYa)5xT%rH1DiXitM^HhT1Yov{5!LKJfXfy?*yRqRc~vwES<%~o&~!! zd6qY~`c5wP&lqk0iGcja03VExN6*aE*Q?CUE!iRc$e-q@v2blxy}AR2#ph$Dn(4#> z2F;KNfI=fpt;gvex0jZv03&|~H35IQ&0b^jh6aLnHS`L4ApWm48kbv=wQ%l^J^o%366?PdE8qs9s G=Klgi0Gtj0 delta 2918 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 delta 5828 zcmd5ny%-E9jC?Uq8ffTfQGm0D_n0#Ya~ZJ{ktw(V|d>AMuLf))i) zPy{@nLQI61_`{_BuqH(LAqIa)Vj`GgqQ*2PAQ2G3m=H}A&zXCs-0j|_G!S8ud%wB! zopbKYdCZ-AWV6wrZ>bP3;=n1z9XjuZl_6z)XF{B#`WA-8hxE0D$A%}iv(i|^f~~&Rt|gr{ z?G4J(Iyey(t98hGJDiQ0rO|!r*i0oLA7^B#$93Epk67I4TiDjRxvO(aUDqHF4bcv# zk_yG)vc|fma70?5x?-U>I(}j-D~(1(BR(;TMG|2E>BONl$&rObh>nTZ65wn5M9tB6 z*?u}iTLk-UX<9?yw+V+rEMriHWR*mj)v3kj;sCXAcrEa4Y-~*`Zm#JTO7W!4swtCD z5OYmggmtm~S_ya^ZrdKIFT@RW{%hPXgr8Gm{g?x$%4XWh z%gAUzkK8(UbCUgamZO6Gl>5D< zP*OD=yiT`$xq<$M`}Jj#?nk=WK%eLSoN`GoN;cOHTP^8NkT%o4pwlT+-F7N0s(zYU z21nt8HB$R9)420Wx{?F?-C9Y9rs&!)-GwyO4DB~WufdDjaj&E=2BB~1 zblEhwo$5*%kEA(bEAmP0FPWwZlW|3_ua~rYy191qMn8I}_VxxzTT|WeZesWZ+N`_r zqPDM5QeLM++X5$2K*fFCv!Ohw!aM~3Pf2O+a9V|khEmf19Es*cjMZ7Mz zU8RXdgo+I;{#OfpK8L%tjATul3tyDY zN-n%3g$ZUNmkp8Sj-EPZ78}In>za5#*;&ue#>vW6jYlGq$$$waGvQ$*NT3@@7PWQ3 zhvhN%JyjKwVW&C{Upk_x$^%XuYL##HLHBCweNXkCy*go}Q+;?|jTq@v@9x!^XpWjO zrx(J?3P;ZZo$#v{nS!^lu=1GXB*gf)GMj-LigGf?I~of@bTI-i*<-ZzH=%hty^Y$(2l2H1Zu-N z{uy0PqyyEA z2PS!4+B{v!TVh$w5}8?n*S(8v)jaO^IfvsuE3Eg0TjqdyB3sluT$)wu2XeXOSHj?G`^*GaQCStQlEL)1T zFkQ+Ra0;Na))Ym;x%wfW%3)Tb&ZzgZk|+4>!>pv0#qMz>&gPM=l|X z77?OErveziH*aSBJVrV*7*8PnU-R>+agfv$p%p`VQ(VGw);Tagcc9w!)$-n>*DC8O zqxzenx+%_9&#dvY_ZYm;64#?DnFM4aq_gNoMh_#wIJ(j9R8-T|xYZ{*=^0yqt734l zOj0J~-{>k5pr+*j7DZ2^{v>V7VZ0fQ%9+t-Lzlky=@U`HMmNcSWjaUhBoGDqyM*c z?O3E~%cds3PO!H%WlgF+Ak?er(P&TpJ5{idG1zh`WG{?Kqg#t0v@OYQv?B&>P@&H? YRboQ~ays-+OlNG`#^`m0u)8hqKgH~}u>b%7 diff --git a/PHY_Editor_Integration_Checklist.md b/PHY_Editor_Integration_Checklist.md new file mode 100644 index 0000000..90ca942 --- /dev/null +++ b/PHY_Editor_Integration_Checklist.md @@ -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`(命名规范 + 品质色 + 图标规范 + 掉落表字段模板) + diff --git a/PHY_ItemSystem_Attributes.md b/PHY_ItemSystem_Attributes.md new file mode 100644 index 0000000..7d5e064 --- /dev/null +++ b/PHY_ItemSystem_Attributes.md @@ -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 自动联动。 diff --git a/Source/PHY/Private/Character/PHYCharacter.cpp b/Source/PHY/Private/Character/PHYCharacter.cpp index b33583f..2b1aca1 100644 --- a/Source/PHY/Private/Character/PHYCharacter.cpp +++ b/Source/PHY/Private/Character/PHYCharacter.cpp @@ -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(TEXT("InventorySystem")); + EquipmentSystemComponent = CreateDefaultSubobject(TEXT("EquipmentSystem")); MovementSystemComponent = CreateDefaultSubobject(TEXT("MovementSystem")); RetargeterComponent = CreateDefaultSubobject(TEXT("Retargeter")); } @@ -27,6 +29,9 @@ void APHYCharacter::BeginPlay() if (HasAuthority() && InventorySystemComponent) { InventorySystemComponent->InitializeInventorySystem(); + if (EquipmentSystemComponent) + { + EquipmentSystemComponent->InitializeEquipmentSystem(); + } } } - diff --git a/Source/PHY/Private/Character/PHYCharacter.h b/Source/PHY/Private/Character/PHYCharacter.h index fa672b8..8dc2763 100644 --- a/Source/PHY/Private/Character/PHYCharacter.h +++ b/Source/PHY/Private/Character/PHYCharacter.h @@ -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 InventorySystemComponent; + /** + * 角色的装备系统组件(来自 GenericInventorySystem 插件 GIS)。 + * 监听装备槽变化并驱动装备属性生效。 + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "PHY|Equipment", meta = (AllowPrivateAccess = "true")) + TObjectPtr EquipmentSystemComponent; + /** * 角色的移动系统组件(来自 GenericMovementSystem 插件 GMS)。 */ diff --git a/Source/PHY/Private/Character/PHYPlayerCharacter.cpp b/Source/PHY/Private/Character/PHYPlayerCharacter.cpp index 4091989..e46b40d 100644 --- a/Source/PHY/Private/Character/PHYPlayerCharacter.cpp +++ b/Source/PHY/Private/Character/PHYPlayerCharacter.cpp @@ -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()) @@ -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()) + { + PS->HandleItemEquippedState(SourceItem, bEquipped); + } +} + void APHYPlayerCharacter::InitializeGAS() { APHYPlayerState* PS = GetPlayerState(); @@ -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(); + 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())); + } +} diff --git a/Source/PHY/Private/Character/PHYPlayerCharacter.h b/Source/PHY/Private/Character/PHYPlayerCharacter.h index d6f34dc..f0b05dc 100644 --- a/Source/PHY/Private/Character/PHYPlayerCharacter.h +++ b/Source/PHY/Private/Character/PHYPlayerCharacter.h @@ -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; }; diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp index 9b02c15..1f93a46 100644 --- a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.cpp @@ -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(Item->GetIntegerAttribute(Tag)); + } + return 0.f; + } } APHYPlayerState::APHYPlayerState() @@ -27,7 +51,7 @@ APHYPlayerState::APHYPlayerState() // 使用Minimal复制模式(推荐用于PlayerState) AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal); - + AttributeSet = CreateDefaultSubobject(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 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& 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 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(Count); + const float RestoreInnerPower = GetItemNumericAttribute(Item, ReqTag(TEXT("GIS.Attribute.Item.Consumable.RestoreInnerPower"))) * static_cast(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 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; +} diff --git a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h index 747a91d..30aa3bf 100644 --- a/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h +++ b/Source/PHY/Private/Gameplay/Player/PHYPlayerState.h @@ -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 SoftIcon = nullptr); + + /** 仅拥有者客户端执行:显示提示。 */ + UFUNCTION(Client, Reliable) + void ClientPushGameplayNotify(EPHYNotifyType Type, const FText& Message, const TSoftObjectPtr& 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 AppliedEquipmentItems; }; diff --git a/Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.cpp b/Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.cpp new file mode 100644 index 0000000..4811106 --- /dev/null +++ b/Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.cpp @@ -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"); +} + diff --git a/Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.h b/Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.h new file mode 100644 index 0000000..2fe4d71 --- /dev/null +++ b/Source/PHYInventory/Private/GameplayTags/PHYInventoryItemTags.h @@ -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); +} + diff --git a/Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.cpp b/Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.cpp new file mode 100644 index 0000000..1309393 --- /dev/null +++ b/Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.cpp @@ -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(); +} + +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(); +} + diff --git a/Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h b/Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h new file mode 100644 index 0000000..34fc7a6 --- /dev/null +++ b/Source/PHYInventory/Private/Items/PHYItemBlueprintLibrary.h @@ -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); +}; + diff --git a/Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.cpp b/Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.cpp new file mode 100644 index 0000000..19e3eb9 --- /dev/null +++ b/Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.cpp @@ -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; +} + diff --git a/Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.h b/Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.h new file mode 100644 index 0000000..2a04d2b --- /dev/null +++ b/Source/PHYInventory/Private/Items/PHYItemFragment_PropertySet.h @@ -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 BaseFloatModifiers; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Attributes", meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute")) + TArray 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; +}; +